Конечный результат в вашем конкретном случае может быть другим, однако бенчмаркинг 5 различных вариантов итераций, доступных для Groovy, показывают, что старый цикл Java для каждого является наиболее эффективным.Взгляните на следующий пример, где мы перебираем более 100 миллионов элементов и вычисляем общую сумму этих чисел очень обязательным образом:
@Grab(group='org.gperfutils', module='gbench', version='0.4.3-groovy-2.4')
import java.util.concurrent.atomic.AtomicLong
import java.util.function.Consumer
def numbers = (1..100_000_000)
def r = benchmark {
'numbers.each {}' {
final AtomicLong result = new AtomicLong()
numbers.each { number -> result.addAndGet(number) }
}
'for (int i = 0 ...)' {
final AtomicLong result = new AtomicLong()
for (int i = 0; i < numbers.size(); i++) {
result.addAndGet(numbers[i])
}
}
'for-each' {
final AtomicLong result = new AtomicLong()
for (int number : numbers) {
result.addAndGet(number)
}
}
'stream + closure' {
final AtomicLong result = new AtomicLong()
numbers.stream().forEach { number -> result.addAndGet(number) }
}
'stream + anonymous class' {
final AtomicLong result = new AtomicLong()
numbers.stream().forEach(new Consumer<Integer>() {
@Override
void accept(Integer number) {
result.addAndGet(number)
}
})
}
}
r.prettyPrint()
Это просто простой пример, в котором мы пытаемся провести сравнительный анализстоимость итерации по коллекции, независимо от того, какая операция выполняется для каждого элемента из коллекции (все варианты используют одну и ту же операцию для получения наиболее точных результатов).И вот результаты (измерения времени выражены в наносекундах):
Environment
===========
* Groovy: 2.4.12
* JVM: OpenJDK 64-Bit Server VM (25.181-b15, Oracle Corporation)
* JRE: 1.8.0_181
* Total Memory: 236 MB
* Maximum Memory: 3497 MB
* OS: Linux (4.18.9-100.fc27.x86_64, amd64)
Options
=======
* Warm Up: Auto (- 60 sec)
* CPU Time Measurement: On
WARNING: Timed out waiting for "numbers.each {}" to be stable
user system cpu real
numbers.each {} 7139971394 11352278 7151323672 7246652176
for (int i = 0 ...) 6349924690 5159703 6355084393 6447856898
for-each 3449977333 826138 3450803471 3497716359
stream + closure 8199975894 193599 8200169493 8307968464
stream + anonymous class 3599977808 3218956 3603196764 3653224857
Заключение
- Java для каждого так же быстр, как Stream + анонимный класс (Groovy 2.x делаетне разрешать использование лямбда-выражений).
- Старый
for (int i = 0; ...
почти в два раза медленнее по сравнению с for-each - скорее всего, из-за дополнительных усилий по возврату значения из массива с заданным индексом. - Метод Groovy
each
немного быстрее, чем вариант stream + замыкание, и оба более чем в два раза медленнее по сравнению с самым быстрым.
Важно запустить тесты для конкретного использованиядело, чтобы получить наиболее точный ответ.Например, Stream API, скорее всего, будет лучшим выбором, если рядом с итерацией будут применены другие операции (фильтрация, отображение и т. Д.).Для простых итераций от первого до последнего элемента данной коллекции выбор старого Java для каждого может дать наилучшие результаты, поскольку он не приводит к большим накладным расходам.
Кроме того, размер коллекции имеет значение.Например, если мы используем приведенный выше пример, но вместо итерации более 100 миллионов элементов мы будем повторять более 100 тыс. Элементов, то самый медленный вариант будет стоить 0.82
мс против 0.38
мс.Если вы строите систему, в которой важна каждая наносекунда, вам нужно выбрать наиболее эффективное решение.Но если вы создаете простое приложение CRUD, тогда не имеет значения, займет ли итерация по коллекции 0.82
или 0.38
миллисекунд - стоимость соединения с базой данных как минимум в 50 раз больше, поэтому экономия примерно 0.44
миллисекунд не будетоказать любое влияние.
// Results for iterating over 100k elements
Environment
===========
* Groovy: 2.4.12
* JVM: OpenJDK 64-Bit Server VM (25.181-b15, Oracle Corporation)
* JRE: 1.8.0_181
* Total Memory: 236 MB
* Maximum Memory: 3497 MB
* OS: Linux (4.18.9-100.fc27.x86_64, amd64)
Options
=======
* Warm Up: Auto (- 60 sec)
* CPU Time Measurement: On
user system cpu real
numbers.each {} 717422 0 717422 722944
for (int i = 0 ...) 593016 0 593016 600860
for-each 381976 0 381976 387252
stream + closure 811506 5884 817390 827333
stream + anonymous class 408662 1183 409845 416381
ОБНОВЛЕНИЕ: динамический вызов и статическая компиляция
Существует также еще один фактор, который стоит учитывать - статическая компиляция.Ниже вы можете найти результаты теста производительности итераций для 10 миллионов элементов:
Environment
===========
* Groovy: 2.4.12
* JVM: OpenJDK 64-Bit Server VM (25.181-b15, Oracle Corporation)
* JRE: 1.8.0_181
* Total Memory: 236 MB
* Maximum Memory: 3497 MB
* OS: Linux (4.18.10-100.fc27.x86_64, amd64)
Options
=======
* Warm Up: Auto (- 60 sec)
* CPU Time Measurement: On
user system cpu real
Dynamic each {} 727357070 0 727357070 731017063
Static each {} 141425428 344969 141770397 143447395
Dynamic for-each 369991296 619640 370610936 375825211
Static for-each 92998379 27666 93026045 93904478
Dynamic for (int i = 0; ...) 679991895 1492518 681484413 690961227
Static for (int i = 0; ...) 173188913 0 173188913 175396602
Как вы можете видеть, включение статической компиляции (например, с помощью аннотации класса * 1036) - это изменит правила игры.Конечно, Java for-each по-прежнему наиболее эффективен, однако его статический вариант почти в 4 раза быстрее динамического.Static Groovy each {}
быстрее в 5 раз быстрее, чем динамический each {}
.И статический цикл for также в 4 раза быстрее, чем динамический цикл.
Вывод - для 10 миллионов элементов статический numbers.each {}
занимает 143 миллисекунды, в то время как статический for-each занимает 93 миллисекунды для коллекции того же размера.Это означает, что для коллекции размером 100 КБ статическая numbers.each {}
будет стоить 0.14
мс, а статическая для каждого займет приблизительно 0.09
мс.И то, и другое очень быстрое, и настоящая разница начинается, когда размер коллекции увеличивается до +100 миллионов элементов.
Поток Java из скомпилированного класса Java
И чтобы дать вам представление - вот Javaкласс с stream().forEach()
для 10 миллионов элементов для сравнения:
Java stream.forEach() 87271350 160988 87432338 88563305
Чуть быстрее, чем статически скомпилированный for-each в коде Groovy.