Pull to refresh

Comments 31

Пожалуйста, выложите куда-нибудь эти данные таблицами. На графиках много кривых, а масштаб невелик, они сливаются.
В посте есть ссылка на csv с результатами: https://pastebin.com/sMsNDDm4
Логарифмическая шкала вводит в заблуждение
На обычной для коллекций малого размера вообще все неразличимо.
(если я взял правильные столбики)
"collections.chain.JavaFor.benchmark","avgt",1,5,1220522.342690,762881.441383,"ns/op",1000
"collections.chain.JavaStreams.benchmark","avgt",1,5,1159477.123056,779167.349278,"ns/op",1000
(762881.441383-779167.349278)/762881.441383
-0.021347888428739102

"collections.chain.JavaFor.benchmark","avgt",1,5,10819.745871,3922.925920,"ns/op",10
"collections.chain.JavaStreams.benchmark","avgt",1,5,10495.110603,5289.096391,"ns/op",10
(10495.110603-10819.745871)/10495.110603
-0.030932048291821133
В csv «Benchmark»,«Mode»,«Threads»,«Samples»,«Score»,«Score Error (99.9%)»,«Unit»,«Param: collectionSize»
Вы для Score Error разницу посчитали.
Но как уже ниже написали, есть «ощутимая» разница, которую плохо видно. Но в абсолютном выражении — это ничто.
Исправил комментарий т.к. во втором случае действительно взял не оттуда.
3% это не так уж что бы и ничто…
Это в идеальном случае prefetch'а процессором.
Как только вылетите за кэш процессора или возьмете коллекцию, которая не умещается в памяти несколько раз, то… как вы думаете что происходит
Интересно, про такую особенность стримов не знал, спасибо за ссылку.
В целом получилось, что для всех случаев у функционального подхода если и есть проигрыш, то он несущественный (в пределах погрешности измерений).

Манипуляции какие-то, у вас же логарифмический масштаб — и ваши "пределы погрешности" составляют разницу в три раза (скажем на последнем графике javaFor vs javaSteams)

Согласен, но в абсолютном выражении для коллекции размером 30 000 это 0,5 миллисекунды… Подправил в статье.
это 0,5 миллисекунды…

И что, это много или мало?
Вот вроде айпад новый вышел с 120 фпс = 8 мс на кадр. Можно ли в кадре потерять 0.5 мс? Нет.

Я считаю, что с точки зрения цены времени программиста — это мало.
Если вы для вас скорость критична — это много.
Если вы разрабатываете «обычное» ПО, и у вас есть другие источники тормозов — база данных, пинг HTTP, накладные расходы от вашего любимого Application Server и т.д., то это мало.

База данных и пинг HTTP, они асинхронны и процессорное время внутри приложения не кушают. А вот код — вполне себе. Полмиллисекунды здесь, полторы там, всё это внутри часто вызываемой процедурки, а потом сервер полностью жрёт 8-ядерный Xeon на сотне пользователей.

вы правы, вот только «рано остановились», нужно довести эту логику до денег.

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

А практика (увы, подкрепить цифрами не могу) написания кода показывает, что функциональный оптимизировать легче. Он гораздо более наглядно передает суть «что тут вообще происходит», чем императивная лапша.

Это означает лишь тот факт, что не следует перебирать коллекцию в 30 000 элементов каждый кадр.

Если у меня 30К объектов на игровом поле, как их прикажете обсчитывать?

Например, можно каждый кадр обновлять лишь те, что видит игрок, а остальные — реже
Я еще в школу ходил, игрушки были еще 2D, но люди уже понимали, что «отрисовывать» то, что не видно — нет никакого смысла. Можно сказать не «реже», а «вообще не нужно».

Пытаясь вернуться в контекст статьи — если в коллекции «поле» 30000 объектов, то перед отрисовкой нужно сделать viewField.filter(isVisible()), вдруг в итоге останется вообще один объект, потому что игрок носом уперся в стену.
viewField.filter(isVisible())
Так это же и будет вызов функции filter на 30 000 элементов.
Я не говорил про отрисовку — я говорил именно про разные проходы (циклы) этих 30 000.

Отказаться от модели "каждый кадр отображаем все объекты" и придумать алгоритм по-лучше.


К примеру, в Factorio где-то посередине ветки .14 однажды отказались от поиска коллизий между конвейером и лежащим на земле предметом при каждом обновлении, и стали явно связывать предмет с конвейером при бросании предмета на землю или при строительстве конвейера.


А в версии .15 вместо хранения координат едущего по конвейеру предмета с обновлением на каждом кадре стали хранить расстояние до следующего предмета на конвейере (или до края конвейера) — такие расстояния надо обновлять только для первого предмета из группы.


Обе этих оптимизации ускоряли игру на несколько порядков ощутимее чем обсуждаемая тут разница между javaFor и javaStream.


Там, кстати, этих предметов в свободной игре во всяческих мега-заводах запросто может 30 тысяч набраться.

Вам не кажется странным сравнивать функциональщину и императивщину на задачах создания новой коллекции? Сравните на задачах вида "добавление элемента в список" или "сортировка огромных списков". И не забудьте померить потребление и фрагментацию памяти.

В среднем у меня получались в два раза медленнее ява стримы чем императивный код. В узких местах приходится ArrayList как ни крути.


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

Нас накололи, никаких преимуществ по скорости ленивость не дает...

Для больших коллекций при большом количестве chain преобразований думаю ленивый подход лучше себя покажет
Я делал бенчмарки, где-то от тысячи элементов начинается незначительный выигрыш. Который зависит от погоды на Марсе. А на маленьких коллекциях стримы в полтора раза медленнее, тк им приходится много лямбд аллоцировать и итераторов.
Не раскрыта тема нагрузки на CPU и влияния на GC
Полагаю, во всех случаях CPU будет загружен на 100% :-)

То-то я думаю как это в колтине цикл медленней. Вроде же одно и то же должно получаться. Так он из-за боксинга проигрывает!
В функциональном варианте компилятор не мудрит и компилирует это как обычный foreach на Integer. А вот в цикле компилятор обманул сам себя. Он видит что цикл по notnullable интам, потому превращает их в Integer.intValue(). После чего, радостно, боксит обратно, чтобы засунуть в result (это ведь ArrayList обычный).


Это еще раз очень наглядно демострирует бессмысленность бенчей без дальнейшего анализа. Ибо разница вообще не циклах. На любой коллекции небоксящихся типов (или боксящихся, но "с вопросиком") разницы вообще нет.

Было бы интересно еще результаты с учетом потребления памяти и нагрузки на сборщик мусора.

Если глянуть внутрь, например, foldLeft, можно увидеть var и цикл while.
Но тут понятно, что это локальная мутабельность, используемая для повышения производительности.


image

Sign up to leave a comment.