Чи є різниця в продуктивності між циклом for для циклу і для кожного циклу?


184

Яка різниця в продуктивності між цими наступними петлями?

for (Object o: objectArrayList) {
    o.DoSomething();
}

і

for (int i=0; i<objectArrayList.size(); i++) {
    objectArrayList.get(i).DoSomething();
}

@Keparo: Це цикл "для кожного", а не цикл "for-in"
Ande Turner

в java його називається "для кожного", але коли мова йде про "Objective C", то вона називається циклом "for In".
damithH


Збільшений для виконання петлі обговорюється тут: stackoverflow.com/questions/12155987 / ...
Екес

Навіть якщо буде різниця, вона була б настільки незначною, що вам слід надати перевагу читабельності, якщо цей фрагмент коду не виконується трильйони разів на секунду. І тоді вам знадобиться належний орієнтир, щоб уникнути передчасної оптимізації.
Забузар

Відповіді:


212

З пункту 46 " Дієвої Java " Джошуа Блоха:

Цикл «for-every», введений у версії 1.5, позбавляється від захаращення та можливості помилки, повністю заховавши ітератор або змінну індексу. Отримана ідіома однаковою мірою стосується колекцій та масивів:

// The preferred idiom for iterating over collections and arrays
for (Element e : elements) {
    doSomething(e);
}

Коли ви побачите товсту кишку (:), прочитайте її як "в". Таким чином, цикл, зазначений вище, означає "для кожного елемента е в елементах". Зауважте, що не застосовується штраф за ефективність використання циклу "для кожного", навіть для масивів. Насправді він може запропонувати невелику перевагу в порівнянні із звичайним циклом для певних обставин, оскільки він обчислює межу індексу масиву лише один раз. Хоча це можна зробити вручну (Пункт 45), програмісти не завжди роблять це.


48
Варто згадати, що у циклі for for кожного немає доступу до лічильника індексів (оскільки його не існує)
basszero

Так, але цей лічильник зараз видно за межами циклу. Звичайно, це просте виправлення, але так є для кожного!
Вкриття

74
Існує штрафний показник за виділення ітератора. У живих шпалер для Android у мене був якийсь паралельний код. Я бачив, що смітник збирається з розуму. Це тому, що для кожної петлі виділяли тимчасові ітератори у багатьох різних (недовговічних) потоках, викликаючи сміттєзбірника багато роботи. Перехід до регулярних циклів на основі індексу виправив проблему.
gsingh2011

1
@ gsingh2011 Але це також залежить від того, використовуєте ви список випадкового доступу чи ні. Використання доступу на основі індексу до випадкових списків доступу буде набагато гірше, ніж використання для кожного зі списками випадкового доступу, я думаю. Якщо ви працюєте зі списком інтерфейсів і, отже, не знаєте фактичного типу реалізації, ви можете перевірити, чи список є екземпляром (реалізується) RandomAccess, якщо вам це дуже важливо: docs.oracle.com/javase/8 /docs/api/java/util/RandomAccess.html
Пуце

3
@ gsingh2011 Документи Android ( developer.android.com/training/articles/perf-tips.html#Loops ) зазначають, що ви будете штрафувати за ефективність, використовуючи лише функцію foreach над ArrayList, а не за іншими колекціями. Мені цікаво, чи це було у вашому випадку.
Вікарі

29

Усі ці петлі роблять точно так само, я просто хочу їх показати, перш ніж кинути свої два центи.

По-перше, класичний спосіб прокручування списку:

for (int i=0; i < strings.size(); i++) { /* do something using strings.get(i) */ }

По-друге, кращий спосіб, оскільки це менше схильність до помилок (скільки разів ви ВИ зробили "ой, змішали змінні i і j в цих петлях в циклі")?

for (String s : strings) { /* do something using s */ }

По-третє, мікрооптимізований для циклу:

int size = strings.size();
for (int i = -1; ++i < size;) { /* do something using strings.get(i) */ }

Тепер фактичні два центи: принаймні, коли я тестував їх, третій був найшвидшим під час підрахунку мілісекунд на те, скільки часу знадобилося для кожного типу циклу, і проста операція в ньому повторилася кілька мільйонів разів - для цього використовується Java 5 з jre1.6u10 для Windows на випадок, коли когось цікавить.

Хоча це, принаймні, здається, що третя найшвидша, ви дійсно повинні запитати себе, чи хочете ви ризикувати впроваджувати цю оптимізацію виводків скрізь у вашому циклі циклу, оскільки з того, що я бачив, фактичне циклічне перетворення t, як правило, найбільш трудомістка частина будь-якої реальної програми (а може, я просто працюю на неправильному полі, хто знає). А також, як я вже згадував у приводі для Java для кожного циклу (деякі посилаються на нього як цикл Iterator, а інші - на цикл for-in ), ви менше шанси натрапити на цю конкретну дурну помилку під час її використання. І перш ніж обговорювати, як це навіть може бути швидше, ніж інші, пам’ятайте, що javac взагалі не оптимізує байт-код (ну, майже взагалі), він просто компілює його.

Якщо ви перебуваєте у мікрооптимізації, хоча та / або у вашому програмному забезпеченні використовується безліч рекурсивних циклів, і тоді вам може бути цікавий тип третього циклу. Просто пам’ятайте, що добре орієнтувати ваше програмне забезпечення як до, так і після зміни циклів для циклів, які ви маєте на цей дивний, мікрооптимізований.


5
Зверніть увагу, що цикл для циклу з ++ i <= size "на основі 1", наприклад, метод get всередині циклу буде викликаний для значень 1, 2, 3 тощо.
заголовок

15
Кращий спосіб записати мікрооптимізований цикл - це (int i = 0, size = strings.size (); ++ i <= size;) {} Це є кращим, оскільки воно мінімізує область розміру
Dónal

1
не третій починається з i = 1, коли він проходить через цикл, пропускаючи перший елемент. і бути циклом for не потрібно. int n = string.length; while (n -> 0) {System.out.println ("" + n + "" + рядки [n]); }
Лассі Кіннунен

1
@ Dónal, що цей зразок циклу пропускає перший і дає IOOBE. Це працює: для (int i = -1, size = list.size (); ++ i <size;)
Натан Адамс

1
"Усі ці петлі роблять точно так само" невірно. Один використовує випадковий доступ get(int), інший використовує Iterator. Подумайте, LinkedListде продуктивність for(int i=0;i<strings.size();i++) { /* do something using strings.get(i) */ }набагато гірша, оскільки це робиться в get(int)n разів.
Стів Куо

13

Як правило, перевага віддається кожному циклу. Підхід "отримати" може бути повільнішим, якщо реалізація списку, яку ви використовуєте, не підтримує випадковий доступ. Наприклад, якщо використовується LinkedList, ви несете прохідну вартість, тоді як для кожного підходу використовується ітератор, який відстежує його позицію в списку. Більше інформації про нюанси для кожного циклу .

Я думаю, стаття зараз тут: нове місце

Посилання, показане тут, було мертвим.


12

Ну, ефективність роботи в основному незначна, але не дорівнює нулю. Якщо ви подивитеся на RandomAccessінтерфейс JavaDoc :

Як правило, реалізація списку повинна реалізувати цей інтерфейс, якщо для типових примірників класу цей цикл:

for (int i=0, n=list.size(); i < n; i++)
    list.get(i);

працює швидше, ніж цей цикл:

for (Iterator i=list.iterator(); i.hasNext();)
      i.next();

І для-кожного циклу використовується версія з ітератором, так, ArrayListнаприклад, для-кожного циклу не найшвидший.


Дійсно кожен? Навіть один із масивом? Я читаю тут stackoverflow.com/questions/1006395/…, що він не включає ітератор.
Ondra Žižka

@ OndraŽižka: Для кожного циклу використовується ітератори під час циклу через Iterables, але не масиви. Для масивів використовується for-цикл із змінною індексу. Інформація про це є в JLS .
Лій

так, вам спочатку потрібно створити його в масив з toArray, маючи вартість.
Лассі Кіннунен

6

На жаль, різниця, на жаль, є.

Якщо подивитися на згенерований код байтів для обох типів циклів, вони відрізняються.

Ось приклад з вихідного коду Log4j.

У /log4j-api/src/main/java/org/apache/logging/log4j/MarkerManager.java у нас є статичний внутрішній клас під назвою Log4jMarker, який визначає:

    /*
     * Called from add while synchronized.
     */
    private static boolean contains(final Marker parent, final Marker... localParents) {
        //noinspection ForLoopReplaceableByForEach
        for (final Marker marker : localParents) {
            if (marker == parent) {
                return true;
            }
        }
        return false;
    }

Зі стандартним циклом:

  private static boolean contains(org.apache.logging.log4j.Marker, org.apache.logging.log4j.Marker...);
    Code:
       0: iconst_0
       1: istore_2
       2: aload_1
       3: arraylength
       4: istore_3
       5: iload_2
       6: iload_3
       7: if_icmpge     29
      10: aload_1
      11: iload_2
      12: aaload
      13: astore        4
      15: aload         4
      17: aload_0
      18: if_acmpne     23
      21: iconst_1
      22: ireturn
      23: iinc          2, 1
      26: goto          5
      29: iconst_0
      30: ireturn

З кожним:

  private static boolean contains(org.apache.logging.log4j.Marker, org.apache.logging.log4j.Marker...);
    Code:
       0: aload_1
       1: astore_2
       2: aload_2
       3: arraylength
       4: istore_3
       5: iconst_0
       6: istore        4
       8: iload         4
      10: iload_3
      11: if_icmpge     34
      14: aload_2
      15: iload         4
      17: aaload
      18: astore        5
      20: aload         5
      22: aload_0
      23: if_acmpne     28
      26: iconst_1
      27: ireturn
      28: iinc          4, 1
      31: goto          8
      34: iconst_0
      35: ireturn

Що з цим Oracle?

Я спробував це з Java 7 і 8 на Windows 7.


7
Для тих, хто намагається прочитати демонтаж, чистим результатом є те, що код, сформований всередині циклу, ідентичний, але налаштування для кожного видається, що створило додаткову тимчасову змінну, що містить посилання на другий аргумент. Якщо додаткова прихована змінна зареєстрована, але сам параметр не під час генерації коду, то для-кожного буде швидше; якщо параметр зареєстровано у прикладі for (;;), час виконання буде ідентичним. Отриманий показник?
Робін Девіс

4

Завжди краще використовувати ітератор, а не індексувати. Це тому, що ітератор, швидше за все, оптимізований для реалізації списку, хоча індексований (виклик get) може не бути. Наприклад, LinkedList - це Список, але індексація через його елементи буде повільнішою, ніж повторення за допомогою ітератора.


10
Я думаю, немає такого поняття, як "завжди" в оптимізації продуктивності.)
eckes

4

foreach робить чіткий намір вашого коду, і це, як правило, перевагу над дуже незначним покращенням швидкості - якщо така є.

Щоразу, коли я бачу індексований цикл, я мушу його проаналізувати трохи довше, щоб переконатися, що він робить те, що я думаю, що це робить. Наприклад, чи починається він з нуля, чи включає він чи виключає кінцеву точку тощо?

Більшість мого часу, здається, витрачається на читання коду (який я написав або хтось інший написав), а чіткість майже завжди важливіша за ефективність. Цього дня легко відмовитись від роботи, оскільки Hotspot робить таку дивовижну роботу.


4

Наступний код:

import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.List;

interface Function<T> {
    long perform(T parameter, long x);
}

class MyArray<T> {

    T[] array;
    long x;

    public MyArray(int size, Class<T> type, long x) {
        array = (T[]) Array.newInstance(type, size);
        this.x = x;
    }

    public void forEach(Function<T> function) {
        for (T element : array) {
            x = function.perform(element, x);
        }
    }
}

class Compute {
    int factor;
    final long constant;

    public Compute(int factor, long constant) {
        this.factor = factor;
        this.constant = constant;
    }

    public long compute(long parameter, long x) {
        return x * factor + parameter + constant;
    }
}

public class Main {

    public static void main(String[] args) {
        List<Long> numbers = new ArrayList<Long>(50000000);
        for (int i = 0; i < 50000000; i++) {
            numbers.add(i * i + 5L);
        }

        long x = 234553523525L;

        long time = System.currentTimeMillis();
        for (int i = 0; i < numbers.size(); i++) {
            x += x * 7 + numbers.get(i) + 3;
        }
        System.out.println(System.currentTimeMillis() - time);
        System.out.println(x);
        x = 0;
        time = System.currentTimeMillis();
        for (long i : numbers) {
            x += x * 7 + i + 3;
        }
        System.out.println(System.currentTimeMillis() - time);
        System.out.println(x);
        x = 0;
        numbers = null;
        MyArray<Long> myArray = new MyArray<Long>(50000000, Long.class, 234553523525L);
        for (int i = 0; i < 50000000; i++) {
            myArray.array[i] = i * i + 3L;
        }
        time = System.currentTimeMillis();
        myArray.forEach(new Function<Long>() {

            public long perform(Long parameter, long x) {
                return x * 8 + parameter + 5L;
            }
        });
        System.out.println(System.currentTimeMillis() - time);
        System.out.println(myArray.x);
        myArray = null;
        myArray = new MyArray<Long>(50000000, Long.class, 234553523525L);
        for (int i = 0; i < 50000000; i++) {
            myArray.array[i] = i * i + 3L;
        }
        time = System.currentTimeMillis();
        myArray.forEach(new Function<Long>() {

            public long perform(Long parameter, long x) {
                return new Compute(8, 5).compute(parameter, x);
            }
        });
        System.out.println(System.currentTimeMillis() - time);
        System.out.println(myArray.x);
    }
}

Дає такий вихід у моїй системі:

224
-699150247503735895
221
-699150247503735895
220
-699150247503735895
219
-699150247503735895

Я використовую Ubuntu 12.10 альфа з оновленням OracleJDK 1.7 6.

Взагалі HotSpot оптимізує безліч непрямих та простих зайвих операцій, тому в цілому вам не варто турбуватися про них, якщо їх багато в послідовності або вони сильно вкладені.

З іншого боку, індексоване потрапляння на LinkedList набагато повільніше, ніж виклик наступного в ітераторі для LinkedList, щоб ви могли уникнути цього показника продуктивності, зберігаючи читабельність при використанні ітераторів (явно чи неявно в кожному циклі).


3

Навіть із чимось на зразок ArrayList або Vector, де "get" - це простий пошук масиву, другий цикл все ще має додаткові накладні витрати, ніж перший. Я б очікував, що це буде трохи крихітніше, ніж перший.


Перший цикл повинен також отримувати кожен елемент. Він створює ітератор за лаштунками для цього. Вони справді рівноцінні.
Білл Ящірка

Розмірковуючи з точки зору C, ітератор може просто збільшити покажчик, але get повинен буде кожен раз помножувати значення i на ширину вказівника.
Пол Томблін

Це залежить від того, який тип списку ви використовуєте. Я думаю, що ти маєш рацію, але використання get ніколи не було б швидше, а іноді і повільніше.
Білл Ящірка


3

Ось короткий аналіз різниці, розроблений командою розробників Android:

https://www.youtube.com/watch?v=MZOf3pOAM6A

Результатом є те , що є різниця, і в дуже обмежених умовах з дуже великими списками це може бути помітна різниця. У їх тестуванні для кожного циклу потрібно було вдвічі більше. Однак їх тестування було проведено на масиві 400 000 цілих чисел. Фактична різниця на елемент у масиві становила 6 мікросекунд . Я не тестував, і вони не говорили, але я б очікував, що різниця буде трохи більшою, використовуючи об'єкти, а не примітиви, але навіть все-таки, якщо ви не будуєте код бібліотеки, де ви не маєте уявлення про масштаб того, про що вас запитають. щоб повторити, я думаю, що різницю не варто наголошувати.


2

За назвою змінної objectArrayListя припускаю, що це екземпляр java.util.ArrayList. У такому випадку різниця в продуктивності була б непомітною.

З іншого боку, якщо це екземпляр java.util.LinkedList, другий підхід буде набагато повільніше, оскільки List#get(int)операція O (n).

Отже, перший підхід завжди бажаний, якщо індекс не потрібен логіці в циклі.


1
1. for(Object o: objectArrayList){
    o.DoSomthing();
}
and

2. for(int i=0; i<objectArrayList.size(); i++){
    objectArrayList.get(i).DoSomthing();
}

І те й інше, але для легкого та безпечного використання програмування для кожного, є можливості для помилок, схильних до 2-го способу використання.


1

Дивно, що ніхто не згадував очевидного - foreach виділяє пам'ять (у формі ітератора), тоді як нормальний цикл не виділяє жодної пам'яті. Для ігор на Android це проблема, оскільки це означає, що сміттєзбірник буде періодично працювати. У грі ви не хочете, щоб сміттєзбірник працював ... ВСЕ. Тому не використовуйте петлі foreach у методі малювання (або візуалізації).


1

Отримана відповідь відповідає на питання, крім виняткового випадку ArrayList ...

Оскільки більшість розробників покладаються на ArrayList (принаймні я так вважаю)

Тож я зобов’язаний додати тут правильну відповідь.

Прямо з документації розробника: -

Покращений цикл (також інколи відомий як цикл "для кожного") може використовуватися для колекцій, що реалізують інтерфейс Iterable, і для масивів. З колекціями виділяється ітератор для здійснення інтерфейсних викликів до hasNext () та next (). З ArrayList рукописний відлічений цикл приблизно в 3 рази швидший (з JIT або без нього), але для інших колекцій розширений синтаксис циклу буде точно еквівалентний явному використанню ітератора.

Існує кілька альтернатив для ітерації через масив:

static class Foo {
    int mSplat;
}

Foo[] mArray = ...

public void zero() {
    int sum = 0;
    for (int i = 0; i < mArray.length; ++i) {
        sum += mArray[i].mSplat;
    }
}

public void one() {
    int sum = 0;
    Foo[] localArray = mArray;
    int len = localArray.length;

    for (int i = 0; i < len; ++i) {
        sum += localArray[i].mSplat;
    }
}

public void two() {
    int sum = 0;
    for (Foo a : mArray) {
        sum += a.mSplat;
    }
}

zero () є найповільнішим, оскільки JIT поки не може оптимізувати витрати на отримання довжини масиву один раз за кожну ітерацію через цикл.

one () швидше. Він витягує все з локальних змінних, уникаючи пошуку. Тільки довжина масиву пропонує перевагу продуктивності.

два () - найшвидший для пристроїв, що не мають JIT, і не відрізняються від одного () для пристроїв з JIT. Він використовує розширений синтаксис для циклу, введений у версії 1.5 мови програмування Java.

Таким чином, вам слід використовувати розширений цикл за замовчуванням, але розглянути рукописний відлічений цикл для критичної продуктивності ітерації ArrayList.


-2

Так, for-eachваріант швидше, ніж зазвичай index-based-for-loop.

for-eachваріант використання iterator. Тож проїзд швидше, ніж звичайний forцикл, який базується на індексі.
Це тому iterator, що оптимізовано для переходу, тому що він вказує безпосередньо перед наступним елементом і відразу після попереднього елемента . Однією з причин того, index-based-for-loopщоб бути повільним, є те, що він повинен кожного разу обчислювати та переходити до позиції елемента, що не є з iterator.


-3
public class FirstJavaProgram {

    public static void main(String[] args) 
    {
        int a[]={1,2,3,45,6,6};

// Method 1: this is simple way to print array 

        for(int i=0;i<a.length;i++) 
        { 
            System.out.print(a[i]+" ");
        }

// Method 2: Enhanced For loop

        for(int i:a)
        {
            System.out.print(i+" ");
        }
    }
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.