На прошлой неделе я представил некоторые результаты тестирования производительности потоков в Java 8 . Вы, ребята, и девушки были достаточно заинтересованы, чтобы оставить некоторые идеи, что еще можно профилировать.
Вот что я сделал, и вот результаты.
обзор
Пролог последнего поста также применим и здесь. Прочитайте его, чтобы узнать, почему все цифры лгут, как я их придумал и как вы можете их воспроизвести.
Я добавил новый класс CommentOperationsBenchmark в код на GitHub, который включает в себя именно те тесты, которые обсуждались в этом посте. Я также обновил таблицу Google, чтобы включить новые цифры.
Влияние сравнений
Ницца. Давно говорим, что писать java, чтобы быть как Ansi C быстрее (массивы не списки).
Следующим шагом вниз по кроличьей норе является …
попробуй {для (int i = 0 ;;) делать вещи; } catch (Exex ex) {бла-бла; }
Ни в коем случае не проверяйте цикл, а просто ловите исключение, подходящее для обработки пикселей HD.
WAT? Люди это делают?
Взлом ArrayIndexOotOfBoundsException
01
02
03
04
05
06
07
08
09
10
|
public int array_max_forWithException() { int m = Integer.MIN_VALUE; try { for ( int i = 0 ; ; i++) if (intArray[i] > m) m = intArray[i]; } catch (ArrayIndexOutOfBoundsException ex) { return m; } } |
Может быть, они должны остановиться, потому что похоже, что это не улучшает производительность:
время выполнения в мс нормализовано до 1’000’000 элементов | ||||||
---|---|---|---|---|---|---|
50’000 | 500’000 | 1’000’000 | 5’000’000 | 10’000’000 | 50’000’000 | |
array_max_for | 0,261 | 0,261 | 0,277 | 0,362 | 0,347 | 0,380 |
array_max_forWithException | 0,265 | 0,265 | 0,273 | 0,358 | 0,347 | 0,386 |
Похоже, механизм, используемый для разрыва цикла, не оказывает ощутимого влияния. Это имеет смысл, поскольку развертывание цикла может избежать большинства сравнений, а стоимость создания исключения находится в области нескольких микросекунд и, таким образом, на несколько порядков меньше, чем здесь.
И это предполагает, что у компилятора есть еще больше хитростей. Возможно, он понимает циклы на гораздо более глубоком уровне, и JIT компилирует оба метода в одни и те же инструкции.
Обратите внимание: как у array_max_forWithException нет оператора return после цикла?
Оказывается, компилятор Java распознает простые бесконечные циклы . Вот Это Да! Таким образом, он знает, что каждый путь кода с конечными вычислениями возвращается и не заботится о бесконечных.
Сложив, это компилирует:
Ничего не возвращая
1
2
3
|
public int infiniteLoop() { for (;;); } |
Ты никогда не перестаешь учиться …
Влияние назначений
[F] или «максимальные» тесты, я ожидаю, что есть некоторая задержка от обновления локальной переменной на каждой итерации. Мне любопытно, работает ли нахождение минимального значения за сопоставимый промежуток времени.
Это относится к тому факту, что все тесты выполнялись для массивов или списков, элементы которых равнялись индексу в структуре, то есть [0, 1, 2,…, n-1]. Таким образом, поиск максимума действительно требует n назначений.
А как насчет поиска минимума, который занимает только одно задание?
время выполнения в мс нормализовано до 1’000’000 элементов | ||||||
---|---|---|---|---|---|---|
50’000 | 500’000 | 1’000’000 | 5’000’000 | 10’000’000 | 50’000’000 | |
array_max_for | 0,261 | 0,261 | 0,277 | 0,362 | 0,347 | 0,380 |
array_min_for | 0,264 | 0,260 | 0,280 | 0,353 | 0,348 | 0,359 |
Нет, нет разницы. Я предполагаю, что из-за конвейерной передачи назначение эффективно бесплатно.
Влияние бокса
Было два комментария по поводу бокса.
Также было бы неплохо увидеть реализацию Integer [], чтобы подтвердить подозрение о боксе.
Хорошо, давайте сделаем это. Следующие числа показывают цикл for и цикл for-each над int [], Integer [] и List <Integer>:
время выполнения в мс нормализовано до 1’000’000 элементов | ||||||
---|---|---|---|---|---|---|
50’000 | 500’000 | 1’000’000 | 5’000’000 | 10’000’000 | 50’000’000 | |
array_max_for | 0,261 | 0,261 | 0,277 | 0,362 | 0,347 | 0,380 |
array_max_forEach | 0,269 | 0,262 | 0,271 | 0,349 | 0,349 | 0,356 |
boxedArray_max_for | 0,804 | 1,180 | 1,355 | 1,387 | 1,306 | 1,476 |
boxedArray_max_forEach | 0,805 | 1,195 | 1,338 | 1,405 | 1,292 | 1,421 |
list_max_for | 0,921 | 1,306 | 1,436 | 1,644 | 1,509 | 1,604 |
list_max_forEach | 1,042 | 1,472 | 1,579 | 1,704 | 1,561 | 1,629 |
Мы можем ясно видеть, что доминирующим индикатором для среды выполнения является наличие в структуре данных примитивов или объектов. Но упаковка массива Integer в список вызывает дополнительное замедление.
Ян Ле Таллек также прокомментировал бокс:
intList.stream () макс (Math :: макс.); влечет за собой больше распаковки, чем необходимо.
intList.stream (). mapToInt (x -> x) .max (); примерно в два раза быстрее и ближе к версии массива.
Это утверждение соответствует тому, что мы вывели в предыдущем посте: как можно скорее распаковка потока может повысить производительность.
Просто чтобы проверить еще раз:
время выполнения в мс, нормированное на 1’000’000 элементов (ошибка в%) | ||||||
---|---|---|---|---|---|---|
50’000 | 500’000 | 1’000’000 | 5’000’000 | 10’000’000 | 50’000’000 | |
boxedArray_max _stream | 4,231 (43%) | 5,715 (3%) | 5,004 (27%) | 5,461 (53%) | 5,307 (56%) | 5.507 (54%) |
boxedArray_max _stream_unbox | 3.367 (<1%) | 3,515 (<1%) | 3,548 (2%) | 3,632 (1%) | 3,554 (1%) | 3,600 (2%) |
list_max _stream | 7,230 (7%) | 6,492 (<1%) | 5,595 (36%) | 5,619 (48%) | 5,852 (45%) | 5,631 (51%) |
list_max _stream_unbox | 3,370 (<1%) | 3,515 (1%) | 3,527 (<1%) | 3,666 (3%) | 3,807 (2%) | 3,702 (5%) |
Кажется, это подтверждает претензию. Но результаты выглядят очень подозрительно, потому что ошибки огромны. Повторный запуск этих тестов с разными настройками выявил закономерность:
- Существуют два уровня производительности: один на ~ 3,8 нс / операционный и один на ~ 7,5 нс / операционный.
- Распакованные потоки работают исключительно лучше.
- Отдельные итерации коробочных потоков обычно выполняются на любом из этих двух уровней, но редко включаются в другое время.
- Чаще всего поведение меняется только от форка к форку (то есть от одного набора итераций к следующему).
Все это пахнет подозрительно проблемами с моей настройкой теста. Мне было бы очень интересно услышать от кого-то, кто знает, что происходит.
Обновить
У Янна действительно была идея, и он указал на этот интересный вопрос и отличный ответ на StackOverflow. Теперь я думаю, что коробочные потоки могут работать на уровне не упакованных, но они могут молиться о случайной деоптимизации.
Влияние оборудования
Redditor robi2106 управлял комплектом для 500 000 элементов на своем «i5-4310 @ 2 ГГц с 8 ГБ DDR2». Я добавил результаты в таблицу .
Трудно сделать выводы из данных. Роби отметил: «Я не прекращал использовать свою систему в течение этих 2,5 часов», что может объяснить огромные границы ошибок. Они в среднем на 23 и в среднем в 168 раз больше, чем у меня. (С другой стороны, я также продолжал использовать свою систему, но с довольно низкой нагрузкой.)
Если вы прищурите достаточно сильно, вы можете сделать вывод, что i5-4310 немного быстрее при простых вычислениях, но отстает от более сложных. Параллельная производительность, как правило, соответствует ожиданиям, учитывая, что у i7-4800 вдвое больше ядер.
Влияние языка
Было бы интересно сравнить это со Scala (с @specialized).
Я до сих пор не пробовал Scala и не чувствую, что хочу пробиться к нему для единственного теста. Может быть, кто-то более опытный или менее брезгливый может попробовать?
отражение
При интерпретации этих чисел помните, что итерации выполняли чрезвычайно дешевую операцию. В прошлый раз мы обнаружили, что уже простые арифметические операции вызывают достаточную нагрузку на процессор, чтобы почти полностью компенсировать разницу в механизмах итерации . Так что, как обычно, не оптимизируйте преждевременно!
В общем, я бы сказал: никаких новых открытий. Но мне понравилось играть с вашими идеями, и если у вас есть больше, оставьте комментарий. Или даже лучше, попробуйте сами и опубликуйте результаты.
Ссылка: | Потоковая производительность — Ваши идеи от нашего партнера JCG Николая Парлога в блоге CodeFx . |