#java #performance #jvm #jmh
#java #Производительность #jvm #jmh
Вопрос:
Производительность (пропускная способность) суммирования всех элементов массива в цикле for на более новых JVM ниже, чем на JVM из Java 1.8.0 JDK. Я выполнил тест JHM (графики ниже). Перед каждым тестированием исходные тексты компилировались предоставленными javac.exe и запускаются java.exe оба двоичных файла предоставлены выбранным JDK. Тесты были выполнены в Windows 10 и запущены сценарием powershell без каких-либо программ, работающих в фоновом режиме (никаких других jvm). Компьютер был оснащен 32 ГБ оперативной памяти, поэтому виртуальная память на жестком диске не использовалась.
10 МИЛЛИОНОВ элементов в массиве:
100 МИЛЛИОНОВ элементов в массиве:
Исходный код моего теста:
@Param({"10000000", "100000000"})
public static int ELEMENTS;
public static void main(String[] args) throws RunnerException, IOException {
File outputFile = new File(args[0]);
int javaMajorVersion = Integer.parseInt(System.getProperty("java.version").split("\.")[0]);
ChainedOptionsBuilder builder = new OptionsBuilder()
.include(IteratingBenchmark.class.getSimpleName())
.mode(Mode.Throughput)
.forks(2)
.measurementTime(TimeValue.seconds(10))
.measurementIterations(50)
.warmupTime(TimeValue.seconds(2))
.warmupIterations(10)
.resultFormat(ResultFormatType.SCSV)
.result(outputFile.getAbsolutePath());
if (javaMajorVersion > 8) {
builder = builder.jvmArgs("-Xms20g", "-Xmx20g", "--enable-preview");
} else {
builder = builder.jvmArgs("-Xms20g", "-Xmx20g");
}
new Runner(builder.build()).run();
}
@Benchmark
public static void cStyleForLoop(Blackhole bh, MockData data) {
long sum = 0;
for (int i = 0; i < data.randomInts.length; i ) {
sum = data.randomInts[i];
}
bh.consume(sum);
}
@State(Scope.Thread)
public static class MockData {
private int[] randomInts = new int[ELEMENTS];
@Setup(Level.Iteration)
public void setup() {
Random r = new Random();
this.randomInts = Stream.iterate(r.nextInt(), i -> i r.nextInt(1022) 1).mapToInt(Integer::intValue).limit(ELEMENTS).toArray();
}
}
Необработанные данные:
JDK 1.8.0_241:
"Benchmark";"Mode";"Threads";"Samples";"Score";"Score Error (99,9%)";"Unit";"Param: ELEMENTS"
"benchmark.IteratingBenchmark.cStyleForLoop";"thrpt";1;100;331,446104;5,563589;"ops/s";10000000
"benchmark.IteratingBenchmark.cStyleForLoop";"thrpt";1;100;33,757268;0,431403;"ops/s";100000000
JDK 11.0.2:
"Benchmark";"Mode";"Threads";"Samples";"Score";"Score Error (99,9%)";"Unit";"Param: ELEMENTS"
"benchmark.IteratingBenchmark.cStyleForLoop";"thrpt";1;100;322,728461;4,823611;"ops/s";10000000
"benchmark.IteratingBenchmark.cStyleForLoop";"thrpt";1;100;31,075948;0,062830;"ops/s";100000000
JDK 12.0.1:
"Benchmark";"Mode";"Threads";"Samples";"Score";"Score Error (99,9%)";"Unit";"Param: ELEMENTS"
"benchmark.IteratingBenchmark.cStyleForLoop";"thrpt";1;100;322,914782;4,450969;"ops/s";10000000
"benchmark.IteratingBenchmark.cStyleForLoop";"thrpt";1;100;31,095232;0,075051;"ops/s";100000000
JDK 13.0.1:
"Benchmark";"Mode";"Threads";"Samples";"Score";"Score Error (99,9%)";"Unit";"Param: ELEMENTS"
"benchmark.IteratingBenchmark.cStyleForLoop";"thrpt";1;100;325,103055;4,933257;"ops/s";10000000
"benchmark.IteratingBenchmark.cStyleForLoop";"thrpt";1;100;31,228403;0,067954;"ops/s";100000000
JDK 14.0.1:
"Benchmark";"Mode";"Threads";"Samples";"Score";"Score Error (99,9%)";"Unit";"Param: ELEMENTS"
"benchmark.IteratingBenchmark.cStyleForLoop";"thrpt";1;100;300,861148;0,443404;"ops/s";10000000
"benchmark.IteratingBenchmark.cStyleForLoop";"thrpt";1;100;29,863602;0,035781;"ops/s";100000000
OpenJDK 14.0.2:
"Benchmark";"Mode";"Threads";"Samples";"Score";"Score Error (99,9%)";"Unit";"Param: ELEMENTS"
"benchmark.IteratingBenchmark.cStyleForLoop";"thrpt";1;100;300,781930;0,481579;"ops/s";10000000
"benchmark.IteratingBenchmark.cStyleForLoop";"thrpt";1;100;29,873509;0,033055;"ops/s";100000000
OpenJDK 15:
"Benchmark";"Mode";"Threads";"Samples";"Score";"Score Error (99,9%)";"Unit";"Param: ELEMENTS"
"benchmark.IteratingBenchmark.cStyleForLoop";"thrpt";1;100;343,530895;0,445551;"ops/s";10000000
"benchmark.IteratingBenchmark.cStyleForLoop";"thrpt";1;100;34,287083;0,035028;"ops/s";100000000
Есть ли какое-либо обоснованное объяснение, почему более новые версии Java работают медленнее, чем 1.8 (за исключением OpenJDK 15)?
ОБНОВЛЕНИЕ 1:
Я запускаю одни и те же тесты для разных значений Xmx / Xms (для каждого теста Xmx == Xms), результаты ниже:
ОБНОВЛЕНИЕ 2:
- Во-первых, я изменил
Level.Iteration
наLevel.Trial
. - Во-вторых, я запустил сборщик мусора G1.
- В-третьих, для Xmx / Xms было установлено значение 8 ГБ
Необработанные данные:
JDK 1.8.0_241:
"Benchmark";"Mode";"Threads";"Samples";"Score";"Score Error (99,9%)";"Unit";"Param: ELEMENTS"
"benchmark.IteratingBenchmark.cStyleForLoop";"thrpt";1;15;33,760346;0,089646;"ops/s";100000000
JDK 11.0.2:
"Benchmark";"Mode";"Threads";"Samples";"Score";"Score Error (99,9%)";"Unit";"Param: ELEMENTS"
"benchmark.IteratingBenchmark.cStyleForLoop";"thrpt";1;15;31,075120;0,086171;"ops/s";100000000
JDK 12.0.1:
"Benchmark";"Mode";"Threads";"Samples";"Score";"Score Error (99,9%)";"Unit";"Param: ELEMENTS"
"benchmark.IteratingBenchmark.cStyleForLoop";"thrpt";1;15;31,173939;0,044176;"ops/s";100000000
JDK 13.0.1:
"Benchmark";"Mode";"Threads";"Samples";"Score";"Score Error (99,9%)";"Unit";"Param: ELEMENTS"
"benchmark.IteratingBenchmark.cStyleForLoop";"thrpt";1;15;31,219283;0,062329;"ops/s";100000000
JDK 14.0.1:
"Benchmark";"Mode";"Threads";"Samples";"Score";"Score Error (99,9%)";"Unit";"Param: ELEMENTS"
"benchmark.IteratingBenchmark.cStyleForLoop";"thrpt";1;15;29,808609;0,072664;"ops/s";100000000
OpenJDK 14.0.2:
"Benchmark";"Mode";"Threads";"Samples";"Score";"Score Error (99,9%)";"Unit";"Param: ELEMENTS"
"benchmark.IteratingBenchmark.cStyleForLoop";"thrpt";1;15;29,845817;0,074315;"ops/s";100000000
OpenJDK 15:
"Benchmark";"Mode";"Threads";"Samples";"Score";"Score Error (99,9%)";"Unit";"Param: ELEMENTS"
"benchmark.IteratingBenchmark.cStyleForLoop";"thrpt";1;15;34,310620;0,087412;"ops/s";100000000
ОБНОВЛЕНИЕ 3:
Я создал репозиторий GitHub, содержащий исходный код бенчмарка, и скрипт для выполнения бенчмарка с используемыми мной параметрами JMH, который автоматически генерирует графики в формате png. Кроме того, я выполнил тест на другой машине (Linux).
Результаты с Linux-машины кажутся более оптимистичными:
К сожалению, на моем компьютере с Windows результаты по-прежнему показывают снижение производительности (исключая JDK 15).
Комментарии:
1. Есть ли причина, по которой ваши случайные целые числа на самом деле не случайны, а возрастают (если не происходит переполнения)?
2. Не совсем. Я хотел иметь массив с минимальным количеством дублирующихся значений. Этот подход с возрастающими значениями со случайным шагом кажется довольно быстрым для заполнения массива.
3. Почему количество дубликатов имеет значение? Просто используйте
this.randomInts = r.ints(ELEMENTS).toArray();
. Было бы интересно, влияет ли предоставление им всем одинакового начального значения. Я бы не ожидал какого-либо влияния фактических значений int на производительность.4. Раньше это было важно, потому что я использовал это в другом тесте. Когда я заметил странную производительность между разными версиями JVMS, я максимально очистил код и опубликовал здесь. Я согласен, это интересно. Я проверю это позже.
5. Результаты тестов, похоже, искажены из-за накладных расходов на сборку, вызванных ненужными выделениями в
setup
. 1) ИспользуйтеLevel.Trial
вместоLevel.Iteration
. 2) Установите одинаковый GC для всех версий JDK (GC по умолчанию отличается в JDK 8 и 9 )
Ответ №1:
Даже после дословного копирования вашего бенчмарка с GitHub и запуска с теми же параметрами я все еще не могу воспроизвести результаты. В моей среде JDK 14 работает так же быстро, как JDK 8 (даже немного быстрее). Итак, в этом ответе я проанализирую разницу между обеими версиями на основе дизассемблирования скомпилированного кода.
Для начала давайте рассмотрим самые последние сборки OpenJDK от того же поставщика.
Здесь я сравниваю Liberica JDK 8u265 1 и Liberica JDK 14.0.2 13 для 64-разрядной версии Windows.
Оценки JMH следующие:
Benchmark (ELEMENTS) Mode Cnt Score Error Units
IteratingBenchmark.cStyleForLoop 10000000 thrpt 30 263,137 ± 0,484 ops/s # JDK 8
IteratingBenchmark.cStyleForLoop 10000000 thrpt 30 264,406 ± 0,788 ops/s # JDK 14
Теперь давайте запустим JMH со встроенным -prof xperfasm
профилировщиком, чтобы увидеть разборку самой горячей части бенчмарка. Ожидаемо, ~ 99,5% процессорного времени тратится в C2-скомпилированном cStyleForLoop
методе.
Самая горячая область в JDK 8
....[Hottest Region 1]..............................................................................
C2, level 4, codes.dbg.IteratingBenchmark::cStyleForLoop, version 574 (71 bytes)
0x0000028c5607fc5f: add r10d,0fffffff9h
0x0000028c5607fc63: lea rax,[r12 rcx*8]
0x0000028c5607fc67: mov ebx,80000000h
0x0000028c5607fc6c: cmp r9d,r10d
0x0000028c5607fc6f: cmovl r10d,ebx
0x0000028c5607fc73: mov r9d,1h
0x0000028c5607fc79: cmp r10d,1h
╭ 0x0000028c5607fc7d: jle 28c5607fccch
│ 0x0000028c5607fc7f: nop ;*lload_2
│ ; - codes.dbg.IteratingBenchmark::cStyleForLoop@15 (line 25)
0,07% │↗ 0x0000028c5607fc80: movsxd rbx,dword ptr [rax r9*4 10h]
0,06% ││ 0x0000028c5607fc85: add rbx,r8
8,93% ││ 0x0000028c5607fc88: movsxd rcx,r9d
0,41% ││ 0x0000028c5607fc8b: movsxd r8,dword ptr [rax rcx*4 2ch]
25,02% ││ 0x0000028c5607fc90: movsxd rdi,dword ptr [rax rcx*4 14h]
0,10% ││ 0x0000028c5607fc95: movsxd rsi,dword ptr [rax rcx*4 18h]
8,56% ││ 0x0000028c5607fc9a: movsxd rbp,dword ptr [rax rcx*4 28h]
0,58% ││ 0x0000028c5607fc9f: movsxd r13,dword ptr [rax rcx*4 1ch]
0,41% ││ 0x0000028c5607fca4: movsxd r14,dword ptr [rax rcx*4 20h]
0,20% ││ 0x0000028c5607fca9: movsxd rcx,dword ptr [rax rcx*4 24h]
8,85% ││ 0x0000028c5607fcae: add rdi,rbx
0,38% ││ 0x0000028c5607fcb1: add rsi,rdi
0,15% ││ 0x0000028c5607fcb4: add r13,rsi
8,57% ││ 0x0000028c5607fcb7: add r14,r13
13,76% ││ 0x0000028c5607fcba: add rcx,r14
5,51% ││ 0x0000028c5607fcbd: add rbp,rcx
8,50% ││ 0x0000028c5607fcc0: add r8,rbp ;*ladd
││ ; - codes.dbg.IteratingBenchmark::cStyleForLoop@24 (line 25)
8,95% ││ 0x0000028c5607fcc3: add r9d,8h ;*iinc
││ ; - codes.dbg.IteratingBenchmark::cStyleForLoop@26 (line 24)
0,40% ││ 0x0000028c5607fcc7: cmp r9d,r10d
│╰ 0x0000028c5607fcca: jl 28c5607fc80h ;*if_icmpge
│ ; - codes.dbg.IteratingBenchmark::cStyleForLoop@12 (line 24)
↘ 0x0000028c5607fccc: cmp r9d,edx
0x0000028c5607fccf: jnl 28c5607fce4h
0x0000028c5607fcd1: nop ;*lload_2
; - codes.dbg.IteratingBenchmark::cStyleForLoop@15 (line 25)
0x0000028c5607fcd4: movsxd r10,dword ptr [rax r9*4 10h]
0x0000028c5607fcd9: add r8,r10 ;*ladd
; - codes.dbg.IteratingBenchmark::cStyleForLoop@24 (line 25)
....................................................................................................
Самая горячая область в JDK 14
....[Hottest Region 1]..............................................................................
c2, level 4, codes.dbg.IteratingBenchmark::cStyleForLoop, version 622 (147 bytes)
; - codes.dbg.IteratingBenchmark::cStyleForLoop@23 (line 25)
0x000001e844438f72: mov r11d,r10d
0x000001e844438f75: add r11d,0fffffff9h
0x000001e844438f79: lea rax,[r12 r9*8]
0x000001e844438f7d: mov ebx,1h
0x000001e844438f82: cmp r11d,1h
0x000001e844438f86: jle 1e8444390c0h ;*goto {reexecute=0 rethrow=0 return_oop=0}
; - codes.dbg.IteratingBenchmark::cStyleForLoop@29 (line 24)
╭ 0x000001e844438f8c: jmp 1e844438ffah
│ 0x000001e844438f8e: nop
0,04% │↗ 0x000001e844438f90: mov rsi,r8 ;*lload_2 {reexecute=0 rethrow=0 return_oop=0}
││ ; - codes.dbg.IteratingBenchmark::cStyleForLoop@15 (line 25)
0,04% ││ ↗ 0x000001e844438f93: movsxd rdx,dword ptr [rax rbx*4 10h]
8,41% ││ │ 0x000001e844438f98: movsxd rbp,dword ptr [rax rbx*4 14h]
1,23% ││ │ 0x000001e844438f9d: movsxd r13,dword ptr [rax rbx*4 18h]
0,03% ││ │ 0x000001e844438fa2: movsxd r8,dword ptr [rax rbx*4 2ch]
23,87% ││ │ 0x000001e844438fa7: movsxd r11,dword ptr [rax rbx*4 28h]
8,22% ││ │ 0x000001e844438fac: movsxd r9,dword ptr [rax rbx*4 24h]
1,25% ││ │ 0x000001e844438fb1: movsxd rcx,dword ptr [rax rbx*4 20h]
0,14% ││ │ 0x000001e844438fb6: movsxd r14,dword ptr [rax rbx*4 1ch]
0,28% ││ │ 0x000001e844438fbb: add rdx,rsi
7,82% ││ │ 0x000001e844438fbe: add rbp,rdx
1,14% ││ │ 0x000001e844438fc1: add r13,rbp
0,17% ││ │ 0x000001e844438fc4: add r14,r13
14,57% ││ │ 0x000001e844438fc7: add rcx,r14
11,05% ││ │ 0x000001e844438fca: add r9,rcx
5,26% ││ │ 0x000001e844438fcd: add r11,r9
6,32% ││ │ 0x000001e844438fd0: add r8,r11 ;*ladd {reexecute=0 rethrow=0 return_oop=0}
││ │ ; - codes.dbg.IteratingBenchmark::cStyleForLoop@24 (line 25)
8,45% ││ │ 0x000001e844438fd3: add ebx,8h ;*iinc {reexecute=0 rethrow=0 return_oop=0}
││ │ ; - codes.dbg.IteratingBenchmark::cStyleForLoop@26 (line 24)
1,15% ││ │ 0x000001e844438fd6: cmp ebx,edi
│╰ │ 0x000001e844438fd8: jl 1e844438f90h ;*if_icmpge {reexecute=0 rethrow=0 return_oop=0}
│ │ ; - codes.dbg.IteratingBenchmark::cStyleForLoop@12 (line 24)
│ │ 0x000001e844438fda: mov r11,qword ptr [r15 110h] ; ImmutableOopMap {rax=Oop xmm0=Oop xmm1=Oop }
│ │ ;*goto {reexecute=1 rethrow=0 return_oop=0}
│ │ ; - (reexecute) codes.dbg.IteratingBenchmark::cStyleForLoop@29 (line 24)
0,00% │ │ 0x000001e844438fe1: test dword ptr [r11],eax ;*goto {reexecute=0 rethrow=0 return_oop=0}
│ │ ; - codes.dbg.IteratingBenchmark::cStyleForLoop@29 (line 24)
│ │ ; {poll}
0,02% │ │ 0x000001e844438fe4: cmp ebx,dword ptr [rsp]
│ ╭│ 0x000001e844438fe7: jnl 1e844439028h
0,00% │ ││ 0x000001e844438fe9: mov rsi,r8
│ ││ 0x000001e844438fec: vmovq r8,xmm0
│ ││ 0x000001e844438ff1: vmovq rdx,xmm1
0,01% │ ││ 0x000001e844438ff6: mov r11d,dword ptr [rsp]
↘ ││ 0x000001e844438ffa: mov ecx,r10d
││ 0x000001e844438ffd: sub ecx,ebx
││ 0x000001e844438fff: add ecx,0fffffff9h
0,00% ││ 0x000001e844439002: mov r9d,1f40h
││ 0x000001e844439008: cmp r9d,ecx
││ 0x000001e84443900b: mov edi,1f40h
││ 0x000001e844439010: cmovnle edi,ecx
0,02% ││ 0x000001e844439013: add edi,ebx
││ 0x000001e844439015: vmovq xmm0,r8
││ 0x000001e84443901a: vmovq xmm1,rdx
││ 0x000001e84443901f: mov dword ptr [rsp],r11d
0,01% │╰ 0x000001e844439023: jmp 1e844438f93h
↘ 0x000001e844439028: vmovq rdx,xmm1
0x000001e84443902d: cmp ebx,r10d
0x000001e844439030: jnl 1e844439043h
0x000001e844439032: nop ;*lload_2 {reexecute=0 rethrow=0 return_oop=0}
; - codes.dbg.IteratingBenchmark::cStyleForLoop@15 (line 25)
0x000001e844439034: movsxd r11,dword ptr [rax rbx*4 10h]
0x000001e844439039: add r8,r11 ;*ladd {reexecute=0 rethrow=0 return_oop=0}
; - codes.dbg.IteratingBenchmark::cStyleForLoop@24 (line 25)
0x000001e84443903c: inc ebx ;*iinc {reexecute=0 rethrow=0 return_oop=0}
; - codes.dbg.IteratingBenchmark::cStyleForLoop@26 (line 24)
....................................................................................................
Как мы можем видеть, тело цикла компилируется одинаково на обоих JDK:
- развернуто 8 итераций цикла;
- выполняется 8 загрузок из массива без проверки границ, за которыми следуют 8
add
инструкций; - порядок загрузки немного отличается, но все адреса в любом случае используют одну и ту же строку кэша или соседнюю строку кэша.
Ключевое отличие заключается в том, что в JDK 14 итерация цикла разделена на два вложенных блока. Это результат оптимизации Loop strip mining, появившейся в JDK 10. Идея этой оптимизации состоит в том, чтобы разделить цикл подсчета на горячую внутреннюю часть без опроса safepoint и внешнюю часть с инструкцией опроса safepoint.
C2 JIT преобразует цикл во что-то вроде
for (int i = 0; i < array.length; i = 8000) {
for (int j = 0; j < 8000; j = 8) {
int ix = i j;
int v0 = array[ix];
int v1 = array[ix 1];
...
int v7 = array[ix 7];
sum = v0 v1 ... v7;
}
safepoint_poll();
}
Обратите внимание, что в версии JDK 8 вообще нет опроса safepoint внутри подсчитываемого цикла. С одной стороны, это может ускорить выполнение цикла. Но, с другой стороны, это на самом деле плохо для приложений с низкой задержкой, поскольку время паузы может увеличиться на продолжительность всего цикла.
JDK 14 вставляет опрос safepoint внутри цикла. Это может быть причиной наблюдаемого вами замедления, но я в это не очень верю, поскольку из-за оптимизации майнинга с разделением цикла опрос безопасной точки выполняется только один раз за 8000 итераций.
Чтобы убедиться в этом, вы можете отключить опрос безопасных точек с помощью -XX:-UseCountedLoopSafepoints
опции JVM. В этом случае скомпилированная версия JDK 14 будет выглядеть почти идентично версии JDK 8. И так же будут оцениваться результаты тестов.
Комментарии:
1. Действительно хорошее описание, хорошая работа @apangin. К сожалению я не могу есть самые горячие регионах благодаря программе xperf ошибка (xperf.exe — ошибка : инструкция по адресу обратилась к памяти по 0x00007FFFDFA8F88B 0xFFFFFFFFFFFFFFFF. Не удалось прочитать память.). Я думаю, что снижение производительности происходит только на моей машине, потому что она была аналогичной или увеличивалась на других. Для меня вашего ответа достаточно. Спасибо, что уделили мне время.