Эффективный способ перебора объектов коллекции в Groovy 2.x? - PullRequest
0 голосов
/ 01 октября 2018

Какой самый лучший и быстрый способ перебора объектов Collection в Groovy.Я знаю, что есть несколько утилит Groovy.Но они используют медленные замыкания.

1 Ответ

0 голосов
/ 01 октября 2018

Конечный результат в вашем конкретном случае может быть другим, однако бенчмаркинг 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

Заключение

  1. Java для каждого так же быстр, как Stream + анонимный класс (Groovy 2.x делаетне разрешать использование лямбда-выражений).
  2. Старый for (int i = 0; ... почти в два раза медленнее по сравнению с for-each - скорее всего, из-за дополнительных усилий по возврату значения из массива с заданным индексом.
  3. Метод 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.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...