Потоки Java 8: почему параллельный поток работает медленнее?
Я играю с потоками Java 8 и не могу понять результаты производительности, которые получаю. У меня 2-ядерный процессор (Intel i73520M), Windows 8 x64 и 64-разрядная версия Java 8 update 5. Я делаю простое сопоставление по потоку / параллельному потоку строк и обнаружил, что параллельная версия работает несколько медленнее.
printTime.accept(Arrays.stream(array)); // prints around 600 printTime.accept(Arrays.stream(array).parallel()); // prints around 900
Разве параллельная версия не должна быть быстрее, учитывая тот факт, что у меня 2 ядра процессора? Кто-нибудь может подсказать мне, почему параллельная версия медленнее?
Переведено автоматически
Ответ 1
Здесь как бы параллельно происходит несколько проблем.
Во-первых, параллельное решение проблемы всегда требует выполнения большего объема реальной работы, чем выполнение ее последовательно. Накладные расходы связаны с разделением работы между несколькими потоками и объединением результатов. Проблемы, такие как преобразование коротких строк в нижний регистр, достаточно малы, и существует опасность того, что они могут быть перегружены накладными расходами на параллельное разделение.
Вторая проблема заключается в том, что сравнительный анализ Java-программы очень тонкий, и очень легко получить запутанные результаты. Две распространенные проблемы - это JIT-компиляция и устранение мертвого кода. Короткие тесты часто завершаются до или во время компиляции JIT, поэтому они не измеряют пиковую пропускную способность, и действительно, они могут измерять саму JIT. Процесс компиляции несколько недетерминирован, поэтому результаты также могут сильно отличаться.
Для небольших синтетических тестов рабочая нагрузка часто вычисляет результаты, которые выбрасываются. JIT-компиляторы довольно хороши в обнаружении этого и устранении кода, который не дает результатов, которые нигде не используются. Вероятно, в данном случае этого не происходит, но если вы поработаете с другими синтетическими рабочими нагрузками, это, безусловно, может произойти. Конечно, если JIT устраняет рабочую нагрузку бенчмарка, это делает бенчмарк бесполезным.
Я настоятельно рекомендую использовать хорошо развитый фреймворк бенчмаркинга, такой как JMH, вместо того, чтобы создавать свой собственный вручную. В JMH есть средства, помогающие избежать распространенных ошибок бенчмаркинга, включая эти, и его довольно легко настроить и запустить. Вот ваш бенчмарк, преобразованный для использования JMH:
(Параметры указывают на пять итераций прогрева, пять итераций тестирования и одну разветвленную JVM.) Во время выполнения JMH выдает множество подробных сообщений, которые я пропустил. Итоговые результаты следующие.
Benchmark Mode Samples Mean Mean error Units c.s.q.SO23170832.parallel thrpt 54.6005.995 ops/s c.s.q.SO23170832.sequential thrpt 51.5001.727 ops/s
Обратите внимание, что результаты выражаются в операциях в секунду, поэтому похоже, что параллельный запуск был примерно в три раза быстрее последовательного. Но на моей машине всего два ядра. Хммм. И средняя ошибка за один запуск на самом деле больше, чем среднее время выполнения! ЧТО? Здесь происходит что-то подозрительное.
Это подводит нас к третьей проблеме. При более внимательном рассмотрении рабочей нагрузки мы можем видеть, что она выделяет новый объект String для каждого ввода, а также собирает результаты в список, что требует большого количества перераспределений и копирования. Я бы предположил, что это приведет к значительному объему сборки мусора. Мы можем убедиться в этом, повторно запустив бенчмарк с включенными сообщениями GC:
Примечание: строки, начинающиеся с #, являются обычными строками вывода JMH. Все остальное - сообщения GC. Это только первая из пяти итераций прогрева, которая предшествует пяти итерациям тестирования. Сообщения GC продолжались в том же духе на протяжении остальных итераций. Я думаю, можно с уверенностью сказать, что в измеряемой производительности преобладают накладные расходы GC и что сообщаемым результатам не следует верить.
На данный момент неясно, что делать. Это чисто синтетическая рабочая нагрузка. Очевидно, что для выполнения реальной работы требуется очень мало процессорного времени по сравнению с распределением и копированием. Трудно сказать, что вы на самом деле пытаетесь здесь измерить. Одним из подходов было бы придумать другую рабочую нагрузку, которая в некотором смысле была бы более "реальной". Другим подходом было бы изменить параметры heap и GC, чтобы избежать GC во время выполнения теста.
Ответ 2
При выполнении тестов вам следует обращать внимание на JIT-компиляцию и на то, что поведение по времени может меняться в зависимости от количества путей кода, скомпилированных JIT. Если я добавлю этап прогрева в вашу тестовую программу, параллельная версия будет немного быстрее, чем последовательная версия. Вот результаты.:
Warmup... Benchmark... Run 0: sequential 0.12s - parallel 0.11s Run 1: sequential 0.13s - parallel 0.08s Run 2: sequential 0.15s - parallel 0.08s Run 3: sequential 0.12s - parallel 0.11s Run 4: sequential 0.13s - parallel 0.08s
Следующий фрагмент кода содержит полный исходный код, который я использовал для этого теста.
Использование нескольких потоков для обработки ваших данных сопряжено с некоторыми первоначальными затратами на настройку, например инициализацию пула потоков. Эти затраты могут перевесить выгоду от использования этих потоков, особенно если время выполнения уже довольно низкое. Кроме того, при наличии конфликта, например, запущенных других потоков, фоновых процессов и т.д., производительность параллельной обработки может еще больше снизиться.
Эта проблема не нова для параллельной обработки. В этой статье приведены некоторые подробности в свете Java 8 parallel() и еще несколько моментов, которые следует учитывать: https://dzone.com/articles/think-twice-using-java-8
Ответ 4
Реализация потока в Java по умолчанию является последовательной, за исключением случаев, когда это явно не указано параллельно. Когда поток выполняется параллельно, среда выполнения Java разбивает поток на несколько подпотоков. Операции агрегирования повторяют и обрабатывают эти подпотоки параллельно, а затем объединяют результаты. Итак, параллельные потоки можно использовать, если разработчики хотят повысить производительность при использовании последовательных потоков. Пожалуйста, проверьте сравнение производительности : https://github.com/prathamket/Java-8/blob/master/Performance_Implications.java Вы получите общее представление о производительности.