Статьи

Потоковое представление — ваши идеи

На прошлой неделе я представил некоторые результаты тестирования производительности потоков в Java 8 . Вы, ребята, и девушки были достаточно заинтересованы, чтобы оставить некоторые идеи, что еще можно профилировать.

Вот что я сделал, и вот результаты.

обзор

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

Я добавил новый класс CommentOperationsBenchmark в код на GitHub, который включает в себя именно те тесты, которые обсуждались в этом посте. Я также обновил таблицу Google, чтобы включить новые цифры.

Влияние сравнений

Ницца. Давно говорим, что писать java, чтобы быть как Ansi C быстрее (массивы не списки).

Следующим шагом вниз по кроличьей норе является …

попробуй {для (int i = 0 ;;) делать вещи; } catch (Exex ex) {бла-бла; }

Ни в коем случае не проверяйте цикл, а просто ловите исключение, подходящее для обработки пикселей HD.

Chaoslab

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] или «максимальные» тесты, я ожидаю, что есть некоторая задержка от обновления локальной переменной на каждой итерации. Мне любопытно, работает ли нахождение минимального значения за сопоставимый промежуток времени.

b0b0b0b

Это относится к тому факту, что все тесты выполнялись для массивов или списков, элементы которых равнялись индексу в структуре, то есть [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

Нет, нет разницы. Я предполагаю, что из-за конвейерной передачи назначение эффективно бесплатно.

Опубликовано Халидом Альбайхом под CC-BY 2.0 - поле зрения, измененное мной.

Опубликовано Халидом Альбайхом под CC-BY 2.0 — поле зрения, измененное мной.

Влияние бокса

Было два комментария по поводу бокса.

Также было бы неплохо увидеть реализацию Integer [], чтобы подтвердить подозрение о боксе.

ickysticky

Хорошо, давайте сделаем это. Следующие числа показывают цикл 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).

cryptos6

Я до сих пор не пробовал Scala и не чувствую, что хочу пробиться к нему для единственного теста. Может быть, кто-то более опытный или менее брезгливый может попробовать?

отражение

При интерпретации этих чисел помните, что итерации выполняли чрезвычайно дешевую операцию. В прошлый раз мы обнаружили, что уже простые арифметические операции вызывают достаточную нагрузку на процессор, чтобы почти полностью компенсировать разницу в механизмах итерации . Так что, как обычно, не оптимизируйте преждевременно!

В общем, я бы сказал: никаких новых открытий. Но мне понравилось играть с вашими идеями, и если у вас есть больше, оставьте комментарий. Или даже лучше, попробуйте сами и опубликуйте результаты.

Ссылка: Потоковая производительность — Ваши идеи от нашего партнера JCG Николая Парлога в блоге CodeFx .