В середине 2013 с выходом Java 8, язык начал поддерживать lambda-выражения, с тех пор минуло 4 года, было выпущено множество update-ов, грядет уже и выход Java 9 (которую, вероятно, мы сможем увидеть в этом году, не смотря на постоянные переносы сроков), таким образом на стыке времён, хотелось бы подвести итоги и оценить производительность нового в java и старого, как мир инструмента, дав ему количественную оценку.
Оценки производительности уже неоднократно производились, как в прочем обсуждались достоинства и недостатки lambda-выражений:
к первым можно отнести:
— упрощение и сокращение кода
— возможность реализации функционального стиля
— возможность параллельных вычислений
ко вторым:
— уменьшение производительности
— трудность debug'а
Неплохая методика и тесты были описаны и использованы еще 5 лет назад пользователем dmmm (https://habrahabr.ru/post/146718/), собственно они за основу и были взяты.
Если, в вкратце то сами тесты поставлялись с проектом LambdaJ. dmmm добавил тесты для проекта Guava и откорректировал их после обсуждения с habr-сообществом (2012).
Мною (то бишь blutovi) были переписаны все lambda-тесты (поскольку тестирование проходило на альфа-версиях java, и в текущем состоянии код просто не компилируем), также при помощи Java Mission Control было проанализировано состояние памяти и для каждого теста приложен jfr-файл с результатами. (Java Mission Control — средство диагностики и мониторинга JVM — доступно начиная с версии java 7 update 40, как результат объединения идей двух JVM: HotSpot от Sun и JRockit от Oracle).
Сами junit-тесты состоят из трех фаз: инициализация коллекций, холостое выполнение, выполнение со сбором статистики (подсчет времени ведется только на последней стадии, а анализ работы с памятью осуществляется на протяжении всего цикла).
Замер времени осуществляется следующим образом: для каждой из итераций, в течении времени , происходит последовательный вызов тестируемого-алгоритма. По его истечении, определяется среднее арифметическое время выполнения алгоритма :
путем деления суммарного времени его выполнения на количество раз , которое он успел выполнится. На основе итераций, находим общее среднее время выполнения алгоритма:
На величины (количество итераций) и (время в течении, которого происходит вызов тестируемого алгоритма) можно повлиять изменив значения констант в классе ch.lambdaj.demo.AbstractMeasurementTest:
Запуск тестов происходил с активированными параметрами для работы Java Mission Control, что позволило в автоматическом режиме формировать диагностические файлы.
в абсолютных величинах (чем значения меньше, тем лучше)
в относительных величинах (100% — это база, чем значения меньше, тем лучше)
Если обобщить, то можно отметить, что одни и те же вещи при помощи lambda-выражений можно реализовать по-разному: однако даже самые оптимальные варианты по производительности будут уступать простым решениям, а не оптимальные способны значительно сказаться на производительности.
Таким образом, iterative подход дает возможность сделать акцент на скорости выполнения, а Guava позволяет акцентировать внимание на количестве используемой памяти, сведя ее к минимуму. Использование же lambda-выражений, похоже дает только эстетическое наслаждение (к моему великому удивлению).
Любые предложения как изменить и улучшить тесты приветствуются.
Оценки производительности уже неоднократно производились, как в прочем обсуждались достоинства и недостатки lambda-выражений:
к первым можно отнести:
— упрощение и сокращение кода
— возможность реализации функционального стиля
— возможность параллельных вычислений
ко вторым:
— уменьшение производительности
— трудность debug'а
Неплохая методика и тесты были описаны и использованы еще 5 лет назад пользователем dmmm (https://habrahabr.ru/post/146718/), собственно они за основу и были взяты.
Если, в вкратце то сами тесты поставлялись с проектом LambdaJ. dmmm добавил тесты для проекта Guava и откорректировал их после обсуждения с habr-сообществом (2012).
Мною (то бишь blutovi) были переписаны все lambda-тесты (поскольку тестирование проходило на альфа-версиях java, и в текущем состоянии код просто не компилируем), также при помощи Java Mission Control было проанализировано состояние памяти и для каждого теста приложен jfr-файл с результатами. (Java Mission Control — средство диагностики и мониторинга JVM — доступно начиная с версии java 7 update 40, как результат объединения идей двух JVM: HotSpot от Sun и JRockit от Oracle).
Как тестировалось?
Сами junit-тесты состоят из трех фаз: инициализация коллекций, холостое выполнение, выполнение со сбором статистики (подсчет времени ведется только на последней стадии, а анализ работы с памятью осуществляется на протяжении всего цикла).
Замер времени осуществляется следующим образом: для каждой из итераций, в течении времени , происходит последовательный вызов тестируемого-алгоритма. По его истечении, определяется среднее арифметическое время выполнения алгоритма :
путем деления суммарного времени его выполнения на количество раз , которое он успел выполнится. На основе итераций, находим общее среднее время выполнения алгоритма:
На величины (количество итераций) и (время в течении, которого происходит вызов тестируемого алгоритма) можно повлиять изменив значения констант в классе ch.lambdaj.demo.AbstractMeasurementTest:
public static final int WARMUP_ITERATIONS_COUNT = 10;
public static final int ITERATIONS_COUNT = 10;
public static final int ITERATION_DURATION_MSEC = 1000;
Запуск тестов происходил с активированными параметрами для работы Java Mission Control, что позволило в автоматическом режиме формировать диагностические файлы.
Результаты
в абсолютных величинах (чем значения меньше, тем лучше)
for | iterate | guava | jdk8 lambda | |||||
---|---|---|---|---|---|---|---|---|
time, ns | heap, Mb | time, ns | heap, Mb | time, ns | heap, Mb | time, ns | heap, Mb | |
Extract cars original cost | 220.4 | 313 | 200.6 | 324 | 188.1 | 302 | 266.2 | 333 |
Age of youngest who bought for > 50k | 6367 | 260 | 4245 | 271 | 6367 | 260 | 6157 | 278 |
Find buys of youngest person | 6036 | 270 | 6411.4 | 259 | 6206 | 260 | 6235 | 288 |
Find most bought car | 2356 | 167 | 2423 | 171 | 5882 | 193 | 2971 | 190 |
Find most costly sale | 49 | 71 | 58.1 | 66 | 431.6 | 297 | 196.1 | 82 |
Group sales by buyers and sellers | 12144 | 250 | 12053 | 254 | 8259 | 206 | 18447 | 242 |
Index cars by brand | 263.3 | 289 | 275.0 | 297 | 2828 | 226 | 307.5 | 278 |
Print all brands | 473.2 | 355 | 455.3 | 365 | 540.3 | 281 | 514.2 | 337 |
Select all sales of a Ferrari | 199.3 | 66 | 265.1 | 53 | 210.4 | 111 | 200.2 | 65 |
Sort sales by cost | 1075 | 74 | 1075 | 74 | 1069 | 72 | 1566 | 124 |
Sum costs where both are males | 67.0 | 63 | 72.9 | 58 | 215.9 | 88 | 413.7 | 114 |
for | iterate | guava | jdk8 lambda | |||||
---|---|---|---|---|---|---|---|---|
time | heap | time | heap | time | heap | time | heap | |
Extract cars original cost | 117% | 104% | 107% | 107% | 100% | 100% | 142% | 110% |
Age of youngest who bought for > 50k | 150% | 100% | 100% | 104% | 150% | 100% | 145% | 107% |
Find buys of youngest person | 100% | 104% | 106% | 100% | 103% | 100% | 103% | 111% |
Find most bought car | 100% | 100% | 103% | 102% | 250% | 116% | 126% | 114% |
Find most costly sale | 100% | 108% | 119% | 100% | 881% | 450% | 400% | 124% |
Group sales by buyers and sellers | 147% | 121% | 146% | 123% | 100% | 100% | 223% | 117% |
Index cars by brand | 100% | 128% | 104% | 131% | 1074% | 100% | 117% | 123% |
Print all brands | 104% | 126% | 100% | 130% | 119% | 100% | 113% | 120% |
Select all sales of a Ferrari | 100% | 125% | 133% | 100% | 106% | 209% | 100% | 123% |
Sort sales by cost | 101% | 103% | 101% | 103% | 100% | 100% | 147% | 172% |
Sum costs where both are males | 100% | 109% | 109% | 100% | 322% | 152% | 617% | 197% |
Выводы
Если обобщить, то можно отметить, что одни и те же вещи при помощи lambda-выражений можно реализовать по-разному: однако даже самые оптимальные варианты по производительности будут уступать простым решениям, а не оптимальные способны значительно сказаться на производительности.
Таким образом, iterative подход дает возможность сделать акцент на скорости выполнения, а Guava позволяет акцентировать внимание на количестве используемой памяти, сведя ее к минимуму. Использование же lambda-выражений, похоже дает только эстетическое наслаждение (к моему великому удивлению).
Любые предложения как изменить и улучшить тесты приветствуются.
Ссылки
- Исходный код и результаты тестирования доступны в git-репозитории
- Файлы диагностики одним архивом
- Загрузить Java 8 (на момент тестирования, последняя версия 8u131)
Поделиться с друзьями
Комментарии (6)
turbanoff
19.06.2017 15:13+7Кто-то ещё пытается бенчмаркать без использования JMH в 2017 году?
Так вы пытаетесь анализировать производительность стримов, а не лямбд. Заголовок misleading.
poxvuibr
19.06.2017 17:54+3Поискал в статье про замеры производительность буквосочетание jmh, не нашёл, и очень удивился. Вы сознательно не стали им пользоваться?
blutovi
19.06.2017 17:59-1Я сознательно переписал, тесты пятилетней давности, чтобы глянуть на результат, но похоже сообщество требует benchmarks. Видимо придется реализовать!
igormich88
19.06.2017 21:22+1Насколько я понимаю версия с преобразованием к стриму примитивов должна быть эффективней.
final Double = sales .stream() .mapToDouble(e -> e.getCost()) .max() .getAsDouble();
igormich88
20.06.2017 00:10Немного подумал и понял что если измерять производительность именно лямбд, то логичнее сравнивать такие реализации:
final Double maxCost = Collections.max(Collections2.transform(sales, new Function<Sale, Double>() { @Override public Double apply(final Sale input) { return input.getCost(); } }));
final maxCost = Collections.max(Collections2.transform(sales, e -> e.getCost()));
Предполагаю что в этом случае производительность не будет существенно отличаться, но версия с лямбда выражением гораздо компактней.
time2rfc
а какова была цель данной лабораторной работы ?