Производительность традиционного цикла for против Iterator / foreach в Java

Есть ли какие-либо результаты тестирования производительности при сравнении традиционного цикла for и Iterator при просмотре ArrayList, HashMap и других коллекций?

Или просто зачем мне использовать Iterator для цикла или наоборот?


person Harish    schedule 10.12.2009    source источник
comment
Обратите внимание, что причина, по которой цикл for работает медленнее со связанным списком, заключается в том, что каждый вызов get(i) повторяется из заголовка списка i раз. Я уверен, что это интуитивно очевидно для всех присутствующих, но мне потребовалась минута, чтобы понять, почему.   -  person Frank Thomas    schedule 15.01.2015
comment
@Harish Insightful   -  person RK_15    schedule 03.04.2019


Ответы (9)


Предполагая, что это то, что вы имели в виду:

// traditional for loop
for (int i = 0; i < collection.size(); i++) {
  T obj = collection.get(i);
  // snip
}

// using iterator
Iterator<T> iter = collection.iterator();
while (iter.hasNext()) {
  T obj = iter.next();
  // snip
}

// using iterator internally (confirm it yourself using javap -c)
for (T obj : collection) {
   // snip
}

Итератор работает быстрее для коллекций без произвольного доступа (например, TreeSet, HashMap, LinkedList). Для массивов и ArrayLists разница в производительности должна быть незначительной.

Изменить: я считаю, что микротестирование - это корень зла, как и ранняя оптимизация. Но опять же, я думаю, что хорошо иметь представление о последствиях таких довольно тривиальных вещей. Поэтому я провел небольшой тест:

  • перебирать LinkedList и ArrayList соответственно
  • со 100 000 "случайных" строк
  • суммируя их длину (просто чтобы избежать того, что компилятор оптимизирует весь цикл)
  • используя все 3 стиля цикла (итератор для каждого, для со счетчиком)

Результаты аналогичны для всех, кроме «для со счетчиком» с LinkedList. Всем остальным пяти потребовалось менее 20 миллисекунд, чтобы перебрать весь список. Использование list.get(i) в LinkedList 100 000 раз заняло более 2 минут (!) (В 60 000 раз медленнее). Вау! :) Следовательно, лучше использовать итератор (явно или неявно для каждого), особенно если вы не знаете, с каким типом и размером списка вы имеете дело.

person sfussenegger    schedule 10.12.2009
comment
Ваш результат LinkedList показывает, что происходит, когда вы переходите от O (n) к O (n ^ 2) (или более) - person Thorbjørn Ravn Andersen; 10.12.2009
comment
Всем остальным пяти потребовалось менее 20 миллисекунд для итерации по всему списку похоже, что началась оптимизация мертвого кода JVM ... Разница между итерацией LinkedList и ArrayList значительна (в пользу ArrayList) - person bestsss; 07.06.2011
comment
@bestsss нет, конечно, нет. Я сгенерировал 100000 случайных строк (на самом деле UUID) и просуммировал их длины, которые были выведены на стандартный вывод после цикла. Конечно, UUID имеют одинаковую длину, что делает вывод предсказуемым, но компилятор не настолько умен. Вы не поверите, но современный процессор может сделать это за 20 мс. Чтобы дать другую точку зрения: мой процессор имеет 4000 BogoMips на ядро. Итак, мы говорим о миллиардах инструкций в секунду или миллионах в миллисекундах. Таким образом, возможно повторение более 100 000 строк с помощью нескольких миллионов инструкций. Процессоры быстрее, чем думает большинство разработчиков :) - person sfussenegger; 07.06.2011
comment
Подводя итог, это жизнеспособный вариант, и компилятор ничего не оптимизирует (кроме безумной предварительной загрузки). Корпус отлично вписался бы и в кеш L2 (даже с LinkedList). Если не все элементы добавляются последовательно, выход из кеша L2 окажет большее влияние на LinkedList. - person bestsss; 07.06.2011
comment
как насчет смешанного способа? )) Iterator<T> iter = collection.iterator(); int l = collection.size(); for (int i = 0, i < l; i++) { T obj = iter.next(); // snip } - person JustAnotherCoder; 20.01.2021
comment
Производительность @JustAnotherCoder в основном такая же, как и при использовании итератора, поскольку hasNext() не должна быть сложной операцией независимо от фактической реализации. Однако это, безусловно, следует рассматривать как плохое использование API. - person sfussenegger; 20.01.2021

Первая причина использовать итератор - это очевидная правильность. Если вы используете ручной индекс, могут быть очень безобидные отдельные ошибки, которые вы сможете увидеть, только если внимательно присмотритесь: вы начали с 1 или с 0? Вы закончили на length - 1? Вы использовали < или <=? Если вы используете итератор, гораздо легче увидеть, что он действительно выполняет итерацию всего массива. «Говори, что делаешь, делай, что говоришь».

Вторая причина - единообразный доступ к разным структурам данных. К массиву можно получить эффективный доступ через индекс, но связанный список лучше всего просматривать, запоминая последний доступный элемент (в противном случае вы получите "Шлемиэль художник "). Хэш-карта еще сложнее. Предоставляя единообразный интерфейс для этих и других структур данных (например, вы также можете выполнять обход дерева), вы снова получаете очевидную корректность. Логика обхода должна быть реализована только один раз, и код, использующий ее, может кратко «сказать, что он делает, и сделать то, что он говорит».

person Svante    schedule 10.12.2009

Производительность в большинстве случаев одинаковая.

Однако всякий раз, когда код получает список и зацикливается на нем, есть хорошо известный случай:
Итератор лучше подходит для всех реализаций списков, которые не реализуют RandomAccess (пример: LinkedList) .

Причина в том, что для этих списков доступ к элементу по индексу не является операцией с постоянным временем.

Таким образом, вы также можете рассматривать Iterator как более надежный (в отношении деталей реализации).


Как всегда, производительность не должна скрывать проблемы с удобочитаемостью.
Цикл java5 foreach является большим хитом в этом аспекте :-)

person KLE    schedule 10.12.2009
comment
Спасибо, а как насчет ArrayList? - person Harish; 10.12.2009
comment
ArrayList реализует RandomAccess, поэтому list.get (i) работает быстро. разница в производительности должна быть в значительной степени незначительной. - person sfussenegger; 10.12.2009
comment
Примечание. Хотя я не знаю, написан ли LinkedList в JDK таким образом, было бы тривиально написать реализацию LinkedList, в которой традиционный цикл for будет работать так же быстро, как и произвольный доступ. Все, что было бы необходимо, это сохранить внутренний указатель на последний элемент, для которого запрашивается произвольный доступ. Это кажется такой тривиальной реализацией, которая ускоряет так много фрагментов кода, что я не могу представить, чтобы их там не было. - person tster; 10.12.2009
comment
@tster: на самом деле это именно то, что делает итератор. - person sfussenegger; 10.12.2009

Да, это имеет значение для коллекций, которые не имеют произвольного доступа, как LinkedList. Связанный список внутренне реализуется узлами, указывающими на следующий (начиная с головного узла).

Метод get (i) в связанном списке начинается с головного узла и перемещается по ссылкам до i-го узла. Когда вы выполняете итерацию по связанному списку с использованием традиционного цикла for, вы каждый раз начинаете заново с головного узла, таким образом, общий обход становится квадратичным по времени.

for( int i = 0; i< list.size(); i++ ) {
    list.get(i); //this starts everytime from the head node instead of previous node
}

В то время как for каждый цикл перебирает итератор, полученный из связанного списка, и вызывает его метод next (). Итератор поддерживает состояния последнего доступа и, таким образом, не запускается каждый раз полностью с головы.

for( Object item: list ) {
    //item element is obtained from the iterator's next method.
}
person mickeymoon    schedule 05.02.2015

Одна из лучших причин использовать итератор поверх синтаксиса i ++ заключается в том, что не все структуры данных будут поддерживать произвольный доступ, не говоря уже о том, чтобы он работал хорошо. Вы также должны программировать интерфейс списка или коллекции, чтобы, если вы позже решили, что другая структура данных будет более эффективной, вы могли бы заменить ее без масштабной операции. В этом случае (в случае кодирования интерфейса) вам не обязательно знать детали реализации, и, вероятно, будет разумнее отложить это до самой структуры данных.

person Jason Tholstrup    schedule 10.12.2009

Одна из причин, по которой я научился придерживаться for each, заключается в том, что он упрощает вложенные циклы, особенно более 2-х мерных циклов. Все «i», «j» и «k», которыми вы можете в конечном итоге манипулировать, могут очень быстро запутаться.

person Ashton K    schedule 10.12.2009

Используйте JAD или JD-GUI против вашего сгенерированного кода, и вы увидите, что реальной разницы нет. Преимущество новой формы итератора в том, что она выглядит чище в вашей кодовой базе.

Изменить: из других ответов я вижу, что вы действительно имели в виду разницу между использованием get (i) и итератором. Я понял, что исходный вопрос означает разницу между старым и новым способами использования итератора.

Использование get (i) и поддержание собственного счетчика, особенно для классов List, не является хорошей идеей по причинам, указанным в принятом ответе.

person Paul Wagland    schedule 10.12.2009

Я не верю в это

for (T obj : collection) {

вычисляет .size () каждый раз в цикле и, следовательно, быстрее, чем

for (int i = 0; i < collection.size(); i++) {
person MeBigFatGuy    schedule 12.12.2009
comment
Легко исправить с помощью for (int i = 0, l = collection.size(); i < l; i++) { - person Adrian Bartholomew; 03.07.2013
comment
первый получает итератор коллекций, вызывая метод collection.iterator (), а затем выполняет итерацию, вызывая методы итератора next () и hasNext (). - person mickeymoon; 05.02.2015

+1 к тому, что сказал сфуссенеггер. К вашему сведению, используете ли вы явный итератор или неявный (то есть для каждого), это не повлияет на производительность, потому что они компилируются с одним и тем же байтовым кодом.

person erturne    schedule 10.12.2009
comment
Они не компилируются в один и тот же байт-код. Цикл forEach выполняет итерацию по итерации и получает итератор, который выполняет итерацию по списку. Для связанного списка метод get (i) начинается с первого узла, проходит весь путь и возвращает объект. Итак, если вы используете i = от 1 до 5 каждый раз, когда он начинается с самого начала. мой ответ ниже. - person mickeymoon; 05.02.2015
comment
Мой ответ сравнивал forEach с явным использованием Iterator, а не сравнивал его с традиционным циклом for с использованием индексных переменных. docs.oracle.com/ javase / specs / jls / se7 / html / - person erturne; 06.02.2015