Ответ 1
Есть несколько вопросов, которые происходят здесь параллельно.
Во-первых, решение параллельной задачи всегда предполагает выполнение более реальной работы, чем выполнение ее последовательно. Накладные расходы связаны с разделением работы между несколькими потоками и объединением или объединением результатов. Такие проблемы, как преобразование коротких строк в нижний регистр, достаточно малы, что они могут быть завалены параллельными раскалывающимися служебными данными.
Вторая проблема заключается в том, что бенчмаркинг Java-программы очень тонкий, и очень легко получить путаные результаты. Двумя распространенными проблемами являются компиляция JIT и устранение мертвого кода. Короткие тесты часто заканчиваются до или во время компиляции JIT, поэтому они не измеряют максимальную пропускную способность, и действительно, они могут измерять JIT. Когда происходит компиляция, она несколько не детерминирована, поэтому она может также сильно изменяться.
Для небольших синтетических тестов рабочая нагрузка часто вычисляет результаты, которые отбрасываются. Компиляторы JIT неплохо разбираются в этом и устраняют код, который не дает результатов, которые используются где угодно. Вероятно, этого не происходит в этом случае, но если вы перейдете к другим синтетическим нагрузкам, это, безусловно, произойдет. Конечно, если JIT устраняет рабочую нагрузку на эталонную рабочую нагрузку, это делает ненужным эталон.
Я настоятельно рекомендую использовать хорошо разработанную базовую платформу, такую как JMH вместо ручной перемотки одной из ваших собственных. У JMH есть возможности, позволяющие избежать общих ошибок, включая их, и довольно легко настроить и запустить. Здесь ваш тест преобразуется в JMH:
package com.stackoverflow.questions;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import java.util.concurrent.TimeUnit;
import org.openjdk.jmh.annotations.*;
public class SO23170832 {
@State(Scope.Benchmark)
public static class BenchmarkState {
static String[] array;
static {
array = new String[1000000];
Arrays.fill(array, "AbabagalamagA");
}
}
@GenerateMicroBenchmark
@OutputTimeUnit(TimeUnit.SECONDS)
public List<String> sequential(BenchmarkState state) {
return
Arrays.stream(state.array)
.map(x -> x.toLowerCase())
.collect(Collectors.toList());
}
@GenerateMicroBenchmark
@OutputTimeUnit(TimeUnit.SECONDS)
public List<String> parallel(BenchmarkState state) {
return
Arrays.stream(state.array)
.parallel()
.map(x -> x.toLowerCase())
.collect(Collectors.toList());
}
}
Я выполнил это с помощью команды:
java -jar dist/microbenchmarks.jar ".*SO23170832.*" -wi 5 -i 5 -f 1
(Параметры указывают пять итераций прогрева, пять итераций по эталонам и одну раздвоенную JVM.) Во время своего запуска JMH испускает множество подробных сообщений, которые я отклонил. Сводные результаты заключаются в следующем.
Benchmark Mode Samples Mean Mean error Units
c.s.q.SO23170832.parallel thrpt 5 4.600 5.995 ops/s
c.s.q.SO23170832.sequential thrpt 5 1.500 1.727 ops/s
Обратите внимание, что результаты выполняются в ops в секунду, поэтому похоже, что параллельный запуск был примерно в три раза быстрее, чем последовательный прогон. Но моя машина имеет только два ядра. Хммм. И средняя ошибка за ход на самом деле больше, чем средняя продолжительность работы! WAT? Здесь происходит что-то рыбное.
Это приводит нас к третьему вопросу. Более внимательно изучая рабочую нагрузку, мы видим, что он выделяет новый объект String для каждого входа, а также собирает результаты в список, что связано с большим количеством перераспределения и копирования. Я предполагаю, что это приведет к большому количеству сбора мусора. Мы увидим это путем повторения теста с включенными сообщениями GC:
java -verbose:gc -jar dist/microbenchmarks.jar ".*SO23170832.*" -wi 5 -i 5 -f 1
Это дает следующие результаты:
[GC (Allocation Failure) 512K->432K(130560K), 0.0024130 secs]
[GC (Allocation Failure) 944K->520K(131072K), 0.0015740 secs]
[GC (Allocation Failure) 1544K->777K(131072K), 0.0032490 secs]
[GC (Allocation Failure) 1801K->1027K(132096K), 0.0023940 secs]
# Run progress: 0.00% complete, ETA 00:00:20
# VM invoker: /Users/src/jdk/jdk8-b132.jdk/Contents/Home/jre/bin/java
# VM options: -verbose:gc
# Fork: 1 of 1
[GC (Allocation Failure) 512K->424K(130560K), 0.0015460 secs]
[GC (Allocation Failure) 933K->552K(131072K), 0.0014050 secs]
[GC (Allocation Failure) 1576K->850K(131072K), 0.0023050 secs]
[GC (Allocation Failure) 3075K->1561K(132096K), 0.0045140 secs]
[GC (Allocation Failure) 1874K->1059K(132096K), 0.0062330 secs]
# Warmup: 5 iterations, 1 s each
# Measurement: 5 iterations, 1 s each
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: com.stackoverflow.questions.SO23170832.parallel
# Warmup Iteration 1: [GC (Allocation Failure) 7014K->5445K(132096K), 0.0184680 secs]
[GC (Allocation Failure) 7493K->6346K(135168K), 0.0068380 secs]
[GC (Allocation Failure) 10442K->8663K(135168K), 0.0155600 secs]
[GC (Allocation Failure) 12759K->11051K(139776K), 0.0148190 secs]
[GC (Allocation Failure) 18219K->15067K(140800K), 0.0241780 secs]
[GC (Allocation Failure) 22167K->19214K(145920K), 0.0208510 secs]
[GC (Allocation Failure) 29454K->25065K(147456K), 0.0333080 secs]
[GC (Allocation Failure) 35305K->30729K(153600K), 0.0376610 secs]
[GC (Allocation Failure) 46089K->39406K(154624K), 0.0406060 secs]
[GC (Allocation Failure) 54766K->48299K(164352K), 0.0550140 secs]
[GC (Allocation Failure) 71851K->62725K(165376K), 0.0612780 secs]
[GC (Allocation Failure) 86277K->74864K(184320K), 0.0649210 secs]
[GC (Allocation Failure) 111216K->94203K(185856K), 0.0875710 secs]
[GC (Allocation Failure) 130555K->114932K(199680K), 0.1030540 secs]
[GC (Allocation Failure) 162548K->141952K(203264K), 0.1315720 secs]
[Full GC (Ergonomics) 141952K->59696K(159232K), 0.5150890 secs]
[GC (Allocation Failure) 105613K->85547K(184832K), 0.0738530 secs]
1.183 ops/s
Примечание: строки, начинающиеся с #
, являются нормальными выходными линиями JMH. Все остальное - сообщения GC. Это всего лишь первая из пяти итераций разминки, которая предшествует пяти итерационным эталонам. Сообщения GC продолжались в том же ключе во время остальных итераций. Я думаю, что можно с уверенностью сказать, что в измеренной производительности преобладают расходы на ХК и что не следует полагать, что результаты не считаются.
В этот момент неясно, что делать. Это чисто синтетическая рабочая нагрузка. Это явно связано с очень небольшим количеством процессорного времени, выполняющим фактическую работу по сравнению с распределением и копированием. Трудно сказать, что вы на самом деле пытаетесь измерить здесь. Один из подходов состоял бы в том, чтобы придумать другую рабочую нагрузку, которая в некотором смысле более "реальна". Другим подходом было бы изменение параметров кучи и GC, чтобы избежать GC во время контрольного прогона.