В середине 2013 с выходом Java 8, язык начал поддерживать lambda-выражения, с тех пор минуло 4 года, было выпущено множество update-ов, грядет уже и выход Java 9 (которую, вероятно, мы сможем увидеть в этом году, не смотря на постоянные переносы сроков), таким образом на стыке времён, хотелось бы подвести итоги и оценить производительность нового в java и старого, как мир инструмента, дав ему количественную оценку.
socmetr.lambda.comparison
Оценки производительности уже неоднократно производились, как в прочем обсуждались достоинства и недостатки 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-тесты состоят из трех фаз: инициализация коллекций, холостое выполнение, выполнение со сбором статистики (подсчет времени ведется только на последней стадии, а анализ работы с памятью осуществляется на протяжении всего цикла).

Замер времени осуществляется следующим образом: для каждой из ${z}$ итераций, в течении времени ${Y}$, происходит последовательный вызов тестируемого-алгоритма. По его истечении, определяется среднее арифметическое время выполнения алгоритма $\bar{Y}_{z}$:

$\bar{Y}^{z}= \frac{1}{{n}_{z}}\sum_{i=0}^{{n}_{z}}{Y}^{'}_{i}$

путем деления суммарного времени его выполнения ${Y}^{'}$ на количество раз ${n}$, которое он успел выполнится. На основе ${z}$ итераций, находим общее среднее время выполнения алгоритма:

$\bar{Y}= \frac{1}{z}\sum_{i=0}^{z}\bar{Y}^{z}_{i}$

На величины ${z}$ (количество итераций) и ${Y}$ (время в течении, которого происходит вызов тестируемого алгоритма) можно повлиять изменив значения констант в классе 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
в относительных величинах (100% — это база, чем значения меньше, тем лучше)
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-выражений, похоже дает только эстетическое наслаждение (к моему великому удивлению).

Любые предложения как изменить и улучшить тесты приветствуются.

Ссылки


Поделиться с друзьями
-->

Комментарии (6)


  1. time2rfc
    19.06.2017 15:02
    +1

    а какова была цель данной лабораторной работы ?


  1. turbanoff
    19.06.2017 15:13
    +7

    Кто-то ещё пытается бенчмаркать без использования JMH в 2017 году?
    Так вы пытаетесь анализировать производительность стримов, а не лямбд. Заголовок misleading.


  1. poxvuibr
    19.06.2017 17:54
    +3

    Поискал в статье про замеры производительность буквосочетание jmh, не нашёл, и очень удивился. Вы сознательно не стали им пользоваться?


  1. blutovi
    19.06.2017 17:59
    -1

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


  1. igormich88
    19.06.2017 21:22
    +1

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

    			final Double = sales
    	                .stream()
    	                .mapToDouble(e -> e.getCost())
    	                .max()
    	                .getAsDouble();
    


    1. 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()));
      

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