Есть ли разница в производительности между циклом for и циклом for-each?

186

Какая разница в производительности между следующими двумя циклами, если таковая имеется?

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

а также

for (int i=0; i<objectArrayList.size(); i++) {
    objectArrayList.get(i).DoSomething();
}
Eflles
источник
1
@Keparo: Это цикл «для каждого», а не «входящий»,
Анде Тернер
в java он называется «для каждого», но когда дело доходит до Objective C, он называется циклом «for In».
damithH
Производительность расширенного цикла for обсуждается здесь: stackoverflow.com/questions/12155987/…
eckes
Даже если бы была разница, она была бы настолько незначительной, что вы должны отдавать предпочтение удобочитаемости, если этот фрагмент кода не выполняется триллионы раз в секунду. И тогда вам понадобится правильный тест, чтобы в любом случае избежать преждевременной оптимизации.
Забузард

Ответы:

212

Из пункта 46 книги Джошуа Блоха « Эффективная Java »:

Цикл for-each, представленный в версии 1.5, устраняет беспорядок и возможность ошибки, полностью скрывая итератор или индексную переменную. Результирующая идиома одинаково применима к коллекциям и массивам:

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

Когда вы увидите двоеточие (:), прочтите его как «in». Таким образом, приведенный выше цикл читается как «для каждого элемента e в elements». Обратите внимание, что использование цикла for-each не снижает производительности даже для массивов. Фактически, при некоторых обстоятельствах он может дать небольшое преимущество в производительности по сравнению с обычным циклом for, поскольку он вычисляет предел индекса массива только один раз. Хотя вы можете сделать это вручную (совет 45), программисты не всегда это делают.

Виджей Дев
источник
48
Стоит упомянуть, что в цикле for-each нет возможности получить доступ к счетчику индекса (так как он не существует)
basszero
Да, но этот счетчик теперь виден за пределами цикла. Конечно, это простое решение, но оно подходит для всех!
Indolering
76
Распределение итератора снижает производительность. У меня был очень параллельный код в живых обоях Android. Я видел, что сборщик мусора сходил с ума. Это произошло потому, что для каждого цикла были выделены временные итераторы во многих разных (недолговечных) потоках, что заставляло сборщик мусора выполнять большую работу. Переключение на обычные циклы на основе индекса устранило проблему.
gsingh2011
1
@ gsingh2011 Но это также зависит от того, используете ли вы список с произвольным доступом или нет. Я полагаю, что использование доступа на основе индекса к спискам неслучайного доступа будет намного хуже, чем использование for-each со списками произвольного доступа. Если вы работаете с интерфейсом List и, следовательно, не знаете фактического типа реализации, вы можете проверить, является ли список экземпляром (реализует) RandomAccess, если вам это действительно так важно: docs.oracle.com/javase/8 /docs/api/java/util/RandomAccess.html
Puce
3
@ gsingh2011 В документации по Android ( developer.android.com/training/articles/perf-tips.html#Loops ) упоминается, что при использовании foreach только для ArrayList, а не других коллекций, у вас будет снижение производительности. Мне любопытно, было ли это твоим случаем.
Viccari
29

Все эти петли делают то же самое, я просто хочу показать их, прежде чем бросить свои два цента.

Во-первых, классический способ перебора List:

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

Во-вторых, предпочтительный способ, поскольку он менее подвержен ошибкам (сколько раз ВЫ делали «упс, смешали переменные i и j в этих циклах внутри циклов»?).

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

В-третьих, микрооптимизированный цикл for:

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

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

Хотя, по крайней мере, кажется, что третий является самым быстрым, вы действительно должны спросить себя, хотите ли вы рискнуть реализовать эту оптимизацию глазка повсюду в вашем циклическом коде, поскольку, как я видел, на самом деле цикл не является t обычно самая трудоемкая часть любой реальной программы (или, может быть, я просто работаю не в том поле, кто знает). И также, как я уже упоминал в предлоге для цикла Java for-each (некоторые называют его циклом Iterator, а другими - циклом for-in ), вы с меньшей вероятностью столкнетесь с этой конкретной глупой ошибкой при ее использовании. И прежде чем обсуждать, как это может быть даже быстрее, чем другие, помните, что javac вообще не оптимизирует байт-код (ну, почти вообще), он просто компилирует его.

Если вы занимаетесь микрооптимизацией и / или ваше программное обеспечение использует множество рекурсивных циклов и тому подобное, то вас может заинтересовать третий тип цикла. Просто не забудьте хорошо протестировать свое программное обеспечение как до, так и после смены цикла for на этот странный, микрооптимизированный.

P Arrayah
источник
5
Обратите внимание, что цикл for с ++ i <= size основан на «1», например, метод get внутри цикла будет вызываться для значений 1, 2, 3 и т. Д.
залп
15
Лучший способ написать микрооптимизированный цикл - for (int i = 0, size = strings.size (); ++ i <= size;) {} Это предпочтительнее, поскольку он минимизирует объем размера
Dónal
1
Разве третий не начинается с i = 1 при первом прохождении цикла, пропуская первый элемент. и в этом цикле for нет необходимости. int n = длина строки; while (n -> 0) {System.out.println ("" + n + "" + строки [n]); }
Ласси Киннунен
1
@ Dónal этот шаблон цикла пропускает первый и дает IOOBE. Это работает: for (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 раз.
Стив Куо
12

Что ж, влияние на производительность в основном незначительное, но не нулевое. Если вы посмотрите на JavaDoc RandomAccessинтерфейса:

Как правило, реализация List должна реализовывать этот интерфейс, если для типичных экземпляров класса этот цикл:

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

работает быстрее, чем этот цикл:

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

И для каждого цикла используется версия с итератором, поэтому, ArrayListнапример, цикл for-each не самый быстрый.

Сармун
источник
Неужели каждый? Даже с массивом? Я читал здесь stackoverflow.com/questions/1006395/…, что здесь нет итератора.
Ондра Жижка
@ OndraŽižka: цикл for-each использует итераторы при цикле по Iterables, но не массивы. Для массивов используется цикл for с индексной переменной. Информация об этом есть в JLS .
Lii
да, поэтому вам сначала нужно будет создать его в массиве с toArray, имеющим стоимость.
Ласси Киннунен
12

Обычно предпочтительнее использовать цикл for-each. Подход «get» может быть медленнее, если используемая вами реализация List не поддерживает произвольный доступ. Например, если используется LinkedList, вы понесете затраты на обход, тогда как подход для каждого использует итератор, который отслеживает его позицию в списке. Подробнее о нюансах для каждого цикла .

Я думаю, что статья теперь здесь: новое место

Ссылка, показанная здесь, была мертва.

Зак Скривена
источник
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

Что случилось с ЭТОМ оракулом?

Я пробовал это с Java 7 и 8 в Windows 7.

Гэри Грегори
источник
7
Для тех, кто пытается прочитать разборку, в конечном итоге код, сгенерированный внутри цикла, идентичен, но для каждой настройки создается дополнительная временная переменная, содержащая ссылку на второй аргумент. Если дополнительная скрытая переменная зарегистрирована, но сам параметр не во время генерации кода, то для каждого будет быстрее; если параметр зарегистрирован в примере for (;;), время выполнения будет идентичным. Нужен тест?
Робин Дэвис,
5

foreach делает намерение вашего кода более ясным, и это обычно предпочтительнее, чем очень незначительное улучшение скорости - если оно есть.

Всякий раз, когда я вижу проиндексированный цикл, мне приходится анализировать его немного дольше, чтобы убедиться, что он делает то, что я думаю. Например, он начинается с нуля, включает ли он конечную точку или исключает ее и т. Д.?

Похоже, что большую часть времени я трачу на чтение кода (который написал я или написал кто-то другой), и ясность почти всегда важнее производительности. В наши дни легко отказаться от производительности, потому что Hotspot делает такую ​​потрясающую работу.

Fortyrunner
источник
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 alpha с OracleJDK 1.7 update 6.

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

С другой стороны, индексированный get в LinkedList намного медленнее, чем вызов next on итератора для LinkedList, поэтому вы можете избежать этого снижения производительности, сохраняя читаемость при использовании итераторов (явно или неявно в цикле for-each).

Wibowit
источник
3

Вот краткий анализ различий, проведенный командой разработчиков Android:

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

Результатом является то , что есть разница, и в очень стесненных условиях с очень большими списками это может быть заметная разница. В их тестировании цикл for каждый занимал вдвое больше времени. Тем не менее, их тестирование длилось около 400 000 целых чисел. Фактическая разница на элемент в массиве составила 6 микросекунд . Я не тестировал, и они не сказали, но я ожидал, что разница будет немного больше с использованием объектов, а не примитивов, но даже все же, если вы не создаете код библиотеки, где вы не знаете масштаб того, что вас спросят чтобы перебрать, думаю, не стоит особо подчеркивать разницу.

TBridges42
источник
2

Даже с чем-то вроде ArrayList или Vector, где get - это простой поиск по массиву, второй цикл по-прежнему имеет дополнительные накладные расходы, которых нет в первом. Я ожидал, что он будет немного медленнее, чем первый.

Пол Томблин
источник
Первый цикл тоже должен получить каждый элемент. Для этого он за кулисами создает итератор. Они действительно эквивалентны.
Bill the Lizard
Думая в терминах C, итератор может просто увеличивать указатель, но get должен каждый раз умножать значение i на ширину указателя.
Пол Томблин
Это зависит от того, какой тип списка вы используете. Я думаю, что вы правы, использование get никогда не будет быстрее, а иногда и медленнее.
Bill the Lizard
2

Всегда лучше использовать итератор вместо индексации. Это связано с тем, что итератор, скорее всего, оптимизирован для реализации List, в то время как индексированный (вызов get) может не быть. Например, LinkedList - это список, но индексация его элементов будет медленнее, чем итерация с использованием итератора.

Стив Куо
источник
11
Я думаю, что в оптимизации производительности не бывает такого понятия, как «всегда».)
eckes
Я думаю, вам нужно здесь дать более четкое определение . Если мы говорим о производительности (о чем идет речь), то говорить « всегда здесь » просто неправильно . Циклы на основе итераторов работают медленнее, чем индексированные циклы для RandomAccessсписков. Обратите внимание, что это включает в себя ArrayListодну из наиболее часто используемых структур данных в Java. В большинстве случаев разница в производительности имеет меньшее значение, чем читаемость цикла, но штраф здесь, если он находится на вашем горячем пути, поэтому, когда задают вопрос о производительности, этот ответ действительно вводит в заблуждение.
Джоффри
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();
}

Оба делают то же самое, но для простого и безопасного программирования для каждого из них существуют возможности для подверженности ошибкам при втором способе использования.

Сантош
источник
1

Странно, что никто не упомянул очевидное - foreach выделяет память (в виде итератора), тогда как обычный цикл for не выделяет никакой памяти. Для игр на Android это проблема, потому что это означает, что сборщик мусора будет запускаться периодически. В игре вы не хотите, чтобы сборщик мусора работал ... НИКОГДА. Поэтому не используйте циклы foreach в методе рисования (или рендеринга).

нервный
источник
1

Принятый ответ отвечает на вопрос, за исключением исключительного случая ArrayList ...

Поскольку большинство разработчиков полагаются на ArrayList (по крайней мере, я так считаю)

Поэтому я обязан добавить сюда правильный ответ.

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

Расширенный цикл for (также известный как цикл «for-each») можно использовать для коллекций, реализующих интерфейс Iterable, и для массивов. В коллекциях итератор выделяется для выполнения интерфейсных вызовов hasNext () и next (). С ArrayList рукописный счетный цикл примерно в 3 раза быстрее (с JIT или без него), но для других коллекций улучшенный синтаксис цикла for будет точно эквивалентен явному использованию итератора.

Есть несколько альтернатив для перебора массива:

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 () быстрее. Он помещает все в локальные переменные, избегая поиска. Только длина массива дает преимущество в производительности.

two () является самым быстрым для устройств без JIT и неотличим от one () для устройств с JIT. Он использует расширенный синтаксис цикла for, представленный в версии 1.5 языка программирования Java.

Таким образом, вы должны использовать расширенный цикл for по умолчанию, но подумайте о написанном от руки цикле с подсчетом для критичной для производительности итерации ArrayList.

Срикант Каруманагхат
источник
-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+" ");
        }
    }
}
СС Тивари
источник