На прошлой неделе случился релиз десятки — и хотя Graal был доступен и раньше, теперь он стал ещё доступней — Congratulations, you're running #Graal! — просто добавьте
-XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler
Что конкретно это может нам дать и где можно ожидать улучшений, и какие велосипеды надо начинать выпиливать?
Пример, который я буду рассматривать — частично надуманный, однако, основанный на реальных событиях.
Guava
Наверняка многие используют класс Preconditions из библиотеки guava:
checkArgument(value > 0, "Non-negative value is expected, was %s", value);
И всё было бы хорошо, если бы подобный кусок не попадался на критическом пути в коде — проблема в неявном создании мусора.
Так выглядит тело метода checkArgument
:
public static void checkArgument(
boolean expression,
@Nullable String errorMessageTemplate,
@Nullable Object... errorMessageArgs) {
if (!expression) {
throw new IllegalArgumentException(format(errorMessageTemplate, errorMessageArgs));
}
}
Сделаем же неявное явным:
boolean expression = value > 0;
Object[] errorMessageArgs = new Object[]{Integer.valueOf(value)};
if (!expression) {
throw new IllegalArgumentException(format(errorMessageTemplate, errorMessageArgs));
}
Здесь возникает дялемма шашечки-или ехать: Как правило похожие проверки в production коде это перестаховки, и с одной стороны не хочется за них платить дополнительным мусором, но с другой стороны fast fail не хочется выбрасывать.
Проблема в объектах порождаемых autoboxing и varargs, которые могут быть не использованы. Увы, но сталкиваясь с ветвлением Escape Analysis уже не в состоянии определить объект как ненужый.
Как можно решить проблему?
Например, перегрузив метод checkArgument
(что в общем-то и сделано в guava >=20):
public static void checkArgument(boolean expression, @Nullable String errorMessageTemplate, int p1) {
if (!expression) {
throw new IllegalArgumentException(format(errorMessageTemplate, p1));
}
}
Но, что если у нас не один аргумент, а больше двух — для которых есть перегруженные методы в guava? Писать свой костыль, либо страдать от мусора? В нашем коде мы столкнулись с местом, которое содержит комбинацию из 3х int, одной строки, которое выполняется миллионы раз и время отклика ограничено.
Graal
Java 10 и -XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler
Graal несёт на себе множество новых оптимизаций, и в частности Partial Escape Analysis — суть которого, среди прочего, заключается в том, что он в состоянии определить, что созданные объекты используются только в одном из ветвлении — и можно переместить создание этих объектов внутрь него.
Момент истины — какие ваши доказательства ?
JMH
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Fork(1)
@Warmup(iterations = 5, time = 5000, timeUnit = TimeUnit.MILLISECONDS)
@Measurement(iterations = 5, time = 5000, timeUnit = TimeUnit.MILLISECONDS)
@State(Scope.Benchmark)
public class PartialEATest {
@Param(value = {"-1", "1"})
private int value;
@Benchmark
public void allocate(Blackhole bh) {
checkArg(bh, value > 0, "expected non-negative value: %s, %s", value, 1000, "A", 700);
}
private static void checkArg(Blackhole bh, boolean cond, String msg, Object ... args){
if (!cond){
bh.consume(String.format(msg, args));
}
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(PartialEATest.class.getSimpleName())
.addProfiler(GCProfiler.class)
.build();
new Runner(opt).run();
}
}
Из всех цифр нас интересуют аллокации — именно поэтому включил GCProfiler :
Options | Benchmark | (value) | Score | Error | Units |
---|---|---|---|---|---|
-Graal | PartialEATest.allocate:·gc.alloc.rate.norm | -1 | 1008,000 | ± 0,001 | B/op |
-Graal | PartialEATest.allocate:·gc.alloc.rate.norm | 1 | 32,000 | ± 0,001 | B/op |
+Graal | PartialEATest.allocate:·gc.alloc.rate.norm | -1 | 1024,220 | ± 0,908 | B/op |
+Graal | PartialEATest.allocate:·gc.alloc.rate.norm | 1 | ≈ 10⁻⁴ | B/op |
Что вполне наглядно демонстрирует, что Graal не создает объекты без надобности — и самое время выпиливать оптимизационные костыли.
Дополнено:
olegchir резонно заметил: хорошо бы видеть во что именно компилируется код ?
Compiled method
Посмотрим же какой ассемблерный код получается в результате компиляции старым добрым C2 и Graal — для этого нам потребуется hsdis — качаем или собираем сами, добавляем параметры в запуск:
-XX:+UnlockDiagnosticVMOptions
-XX:PrintAssemblyOptions=intel
-XX:CompileCommand=print,"com/elastic/PartialEATest.*"
Compiled method :: C2
Кода оочень много — весь скомпилированный код — до первого autoboxing:
ImmutableOopMap{rbx=Oop }pc offsets: 1684 1697 Compiled method (c2) 619 736 4 com.elastic.PartialEATest::allocate (55 bytes)
total in heap [0x00000001189a0c90,0x00000001189a1410] = 1920
relocation [0x00000001189a0e08,0x00000001189a0e38] = 48
main code [0x00000001189a0e40,0x00000001189a1060] = 544
stub code [0x00000001189a1060,0x00000001189a1078] = 24
oops [0x00000001189a1078,0x00000001189a10a0] = 40
metadata [0x00000001189a10a0,0x00000001189a10b0] = 16
scopes data [0x00000001189a10b0,0x00000001189a1210] = 352
scopes pcs [0x00000001189a1210,0x00000001189a13c0] = 432
dependencies [0x00000001189a13c0,0x00000001189a13c8] = 8
handler table [0x00000001189a13c8,0x00000001189a1410] = 72
----------------------------------------------------------------------
com/elastic/PartialEATest.allocate(Lorg/openjdk/jmh/infra/Blackhole;)V [0x00000001189a0e40, 0x00000001189a1078] 568 bytes
[Entry Point]
[Constants]
# {method} {0x000000022ea937b8} 'allocate' '(Lorg/openjdk/jmh/infra/Blackhole;)V' in 'com/elastic/PartialEATest'
# this: rsi:rsi = 'com/elastic/PartialEATest'
# parm0: rdx:rdx = 'org/openjdk/jmh/infra/Blackhole'
# [sp+0x30] (sp of caller)
0x00000001189a0e40: cmp rax,QWORD PTR [rsi+0x8]
0x00000001189a0e44: jne 0x0000000110eb7580 ; {runtime_call ic_miss_stub}
0x00000001189a0e4a: xchg ax,ax
0x00000001189a0e4c: nop DWORD PTR [rax+0x0]
[Verified Entry Point]
0x00000001189a0e50: mov DWORD PTR [rsp-0x14000],eax
0x00000001189a0e57: push rbp
0x00000001189a0e58: sub rsp,0x20 ;*synchronization entry
; - com.elastic.PartialEATest::allocate@-1 (line 26)
0x00000001189a0e5c: mov r11d,DWORD PTR [rsi+0x10]
;*getfield value {reexecute=0 rethrow=0 return_oop=0}
; - com.elastic.PartialEATest::allocate@1 (line 26)
0x00000001189a0e60: mov DWORD PTR [rsp],r11d
0x00000001189a0e64: test r11d,r11d
0x00000001189a0e67: jle 0x00000001189a0ffc ;*ifle {reexecute=0 rethrow=0 return_oop=0}
; - com.elastic.PartialEATest::allocate@4 (line 26)
0x00000001189a0e6d: cmp r11d,0xffffff80
0x00000001189a0e71: jl 0x00000001189a100e ;*if_icmplt {reexecute=0 rethrow=0 return_oop=0}
; - java.lang.Integer::valueOf@3 (line 1048)
; - com.elastic.PartialEATest::allocate@24 (line 26)
0x00000001189a0e77: cmp r11d,0x7f
0x00000001189a0e7b: jg 0x00000001189a0ea9 ;*if_icmpgt {reexecute=0 rethrow=0 return_oop=0}
; - java.lang.Integer::valueOf@10 (line 1048)
; - com.elastic.PartialEATest::allocate@24 (line 26)
0x00000001189a0e7d: mov ebp,r11d
0x00000001189a0e80: add ebp,0x80 ;*iadd {reexecute=0 rethrow=0 return_oop=0}
; - java.lang.Integer::valueOf@20 (line 1049)
; - com.elastic.PartialEATest::allocate@24 (line 26)
0x00000001189a0e86: cmp ebp,0x100
0x00000001189a0e8c: jae 0x00000001189a101e
0x00000001189a0e92: movsxd r10,r11d
0x00000001189a0e95: movabs r11,0x12ed02000 ; {oop(a 'java/lang/Integer'[256] {0x000000012ed02000})}
0x00000001189a0e9f: mov rbp,QWORD PTR [r11+r10*8+0x418]
;*aaload {reexecute=0 rethrow=0 return_oop=0}
; - java.lang.Integer::valueOf@21 (line 1049)
; - com.elastic.PartialEATest::allocate@24 (line 26)
................
Compiled method :: Graal
ImmutableOopMap{rbx=Oop }pc offsets: 251 264 Compiled method (JVMCI) 1850 3888 4 com.elastic.PartialEATest::allocate (55 bytes)
total in heap [0x0000000119292590,0x0000000119292830] = 672
relocation [0x0000000119292708,0x0000000119292718] = 16
main code [0x0000000119292720,0x0000000119292795] = 117
stub code [0x0000000119292795,0x0000000119292798] = 3
oops [0x0000000119292798,0x00000001192927a0] = 8
metadata [0x00000001192927a0,0x00000001192927a8] = 8
scopes data [0x00000001192927a8,0x00000001192927c8] = 32
scopes pcs [0x00000001192927c8,0x0000000119292828] = 96
dependencies [0x0000000119292828,0x0000000119292830] = 8
----------------------------------------------------------------------
com/elastic/PartialEATest.allocate(Lorg/openjdk/jmh/infra/Blackhole;)V (com.elastic.PartialEATest.allocate(Blackhole)) [0x0000000119292720, 0x0000000119292798] 120 bytes
[Entry Point]
[Constants]
# {method} {0x0000000231e007b8} 'allocate' '(Lorg/openjdk/jmh/infra/Blackhole;)V' in 'com/elastic/PartialEATest'
# this: rsi:rsi = 'com/elastic/PartialEATest'
# parm0: rdx:rdx = 'org/openjdk/jmh/infra/Blackhole'
# [sp+0x20] (sp of caller)
0x0000000119292720: cmp rax,QWORD PTR [rsi+0x8]
0x0000000119292724: jne 0x000000010eadc300 ; {runtime_call ic_miss_stub}
0x000000011929272a: nop
0x000000011929272b: data16 data16 nop WORD PTR [rax+rax*1+0x0]
0x0000000119292736: data16 nop WORD PTR [rax+rax*1+0x0]
[Verified Entry Point]
0x0000000119292740: mov DWORD PTR [rsp-0x14000],eax
0x0000000119292747: sub rsp,0x18
0x000000011929274b: mov QWORD PTR [rsp+0x10],rbp
0x0000000119292750: cmp DWORD PTR [rsi+0x10],0x1
0x0000000119292754: jl 0x000000011929276d ;*ifle {reexecute=0 rethrow=0 return_oop=0}
; - com.elastic.PartialEATest::allocate@4 (line 26)
0x000000011929275a: mov rbp,QWORD PTR [rsp+0x10]
0x000000011929275f: add rsp,0x18
0x0000000119292763: mov rcx,QWORD PTR [r15+0x70]
0x0000000119292767: test DWORD PTR [rcx],eax ; {poll_return}
0x0000000119292769: vzeroupper
0x000000011929276c: ret ;*return {reexecute=0 rethrow=0 return_oop=0}
; - com.elastic.PartialEATest::allocate@54 (line 27)
0x000000011929276d: mov DWORD PTR [r15+0x314],0xffffffed
;*ifle {reexecute=0 rethrow=0 return_oop=0}
; - com.elastic.PartialEATest::allocate@4 (line 26)
0x0000000119292778: mov QWORD PTR [r15+0x320],0x0
0x0000000119292783: call 0x000000010eadd2a4 ; ImmutableOopMap{rsi=Oop }
;*aload_0 {reexecute=1 rethrow=0 return_oop=0}
; - com.elastic.PartialEATest::allocate@0 (line 26)
; {runtime_call DeoptimizationBlob}
0x0000000119292788: nop
Можно заметить насколько код, скомпилированный C2 больше, чем код скомпилированный Graal — и autoboxing, и varargs, тогда как версия Graal по сути только вызов метода.