Новый перевод от команды Spring АйО является продолжением разговора о JIT (Just in Time) компиляции, а также дает представление о различных инструментах, позволяющих работать со скомпилированным кодом, визуализировать его и отлаживать в интерактивной форме. Перед прочтением рекомендуем ознакомиться с первой статьей из цикла статей про JIT.
Во второй части мы рассмотрим:
Инлайнинг GVN (global value numbering) во время синтаксического анализа (parsing).
Использование IGV (Ideal Graph Visualizer) и rr (отладчик), чтобы посмотреть на IR и его трансформации.
Простая “идеализация”
101 * a + 202 * a
в303 * a
.Упражнения для читателя: несколько дополнительных трансформаций, которые читатель сможет проанализировать.
Новый пример: инлайнинг и GVN во время синтаксического анализа (parsing)
Я несколько расширил пример из первой части:
public class Test {
public static void main(String[] args) {
// Especially with a debug build, the JVM startup can take a while,
// so it can take a while until our code is executed.
System.out.println("Run");
// Repeatedly call the test method, so that it can become hot and
// get JIT compiled.
for (int i = 0; i < 10_000; i++) {
test(i, i + 1);
}
System.out.println("Done");
}
// The test method we will focus on.
public static int test(int a, int b) {
return multiply(101, a) + multiply(202, a) + multiply(53, b);
}
public static int multiply(int a, int b) {
return a * b;
}
}
Мы видим, что Test.test вызывает Test.multiply три раза и складывает результаты. Если мы оценим код вручную, мы видим, что он вычисляет 101 * a + 202 * a + 53 * b
, что можно было бы упростить как 303 * a + 53 * b
.
Мы прогоним этот пример следующим образом, чтобы увидеть сгенерированный IR:
$ java -XX:CompileCommand=printcompilation,Test::* -XX:CompileCommand=compileonly,Test::test -Xbatch -XX:-TieredCompilation -XX:+PrintIdeal Test.java
CompileCommand: PrintCompilation Test.* bool PrintCompilation = true
CompileCommand: compileonly Test.test bool compileonly = true
Run
8130 85 b Test::test (22 bytes)
AFTER: print_ideal
0 Root === 0 66 [[ 0 1 3 52 50 ]] inner
3 Start === 3 0 [[ 3 5 6 7 8 9 10 11 ]] #{0:control, 1:abIO, 2:memory, 3:rawptr:BotPTR, 4:return_address, 5:int, 6:int}
5 Parm === 3 [[ 66 ]] Control !jvms: Test::test @ bci:-1 (line 17)
6 Parm === 3 [[ 66 ]] I_O !jvms: Test::test @ bci:-1 (line 17)
7 Parm === 3 [[ 66 ]] Memory Memory: @BotPTR *+bot, idx=Bot; !jvms: Test::test @ bci:-1 (line 17)
8 Parm === 3 [[ 66 ]] FramePtr !jvms: Test::test @ bci:-1 (line 17)
9 Parm === 3 [[ 66 ]] ReturnAdr !jvms: Test::test @ bci:-1 (line 17)
10 Parm === 3 [[ 51 ]] Parm0: int !jvms: Test::test @ bci:-1 (line 17)
11 Parm === 3 [[ 64 ]] Parm1: int !jvms: Test::test @ bci:-1 (line 17)
50 ConI === 0 [[ 51 ]] #int:303
51 MulI === _ 10 50 [[ 65 ]] !jvms: Test::test @ bci:13 (line 17)
52 ConI === 0 [[ 64 ]] #int:53
64 MulI === _ 11 52 [[ 65 ]] !jvms: Test::multiply @ bci:2 (line 21) Test::test @ bci:17 (line 17)
65 AddI === _ 51 64 [[ 66 ]] !jvms: Test::test @ bci:20 (line 17)
66 Return === 5 6 7 8 9 returns 65 [[ 0 ]]
Done
А вот визуализация графа (использовался не IGV, а мой собственный инструмент):

Примечание: на данный момент можно просто проигнорировать многочисленные ноды Parm
, Root
и Start
, мы посмотрим на них позже.
А с помощью -XX:CompileCommand=print,Test::test
мы можем найти соответствующие ассемблерные инструкции:
------------------------ OptoAssembly for Compile_id = 85 -----------------------
...
01a imull RAX, RSI, #303 # int
020 imull R11, RDX, #53 # int
024 addl RAX, R11 # int
...
036 ret
...
----------------------------------- Assembly -----------------------------------
# {method} {0x00007f9bed094400} 'test' '(II)I' in 'Test'
# parm0: rsi = int
# parm1: rdx = int
...
0x00007f9c1118799a: imul $0x12f,%esi,%eax
0x00007f9c111879a0: imul $0x35,%edx,%r11d
0x00007f9c111879a4: add %r11d,%eax
...
0x00007f9c111879b6: retq
Мы видим, что компиляция и в самом деле была упрощена до 303 * a + 53 * b
. Как это произошло?
CompileCommand PrintInlining
В приведенном выше дампе PrintIdeal
можно увидеть, что аннотации справа показывают, что код берется как из Test::test
, так и из Test::multiply
. Отсюда мы можем сделать вывод, что код из Test::multiply
был заинлайнен в компиляцию Test::test
.
Мы подтверждаем это при помощи флага PrintInlining
:
$ java -XX:CompileCommand=printcompilation,Test::* -XX:CompileCommand=compileonly,Test::test -Xbatch -XX:-TieredCompilation -XX:CompileCommand=printinlining,Test::test Test.java
CompileCommand: PrintCompilation Test.* bool PrintCompilation = true
CompileCommand: compileonly Test.test bool compileonly = true
CompileCommand: PrintInlining Test.test bool PrintInlining = true
Run
8233 85 b Test::test (22 bytes)
@ 3 Test::multiply (4 bytes) inline (hot)
@ 10 Test::multiply (4 bytes) inline (hot)
@ 17 Test::multiply (4 bytes) inline (hot)
Done
Мы видим, что multiply
инлайнится три раза (на байткодах 3, 10 и 17, относящихся к test
). Отметим, что мы можем видеть также и причину, по которой метод был заинлайнен. В данном случае мы обнаруживаем, что метод достаточно горячий, чтобы его следовало заинлайнить.
Мы также можем запретить инлайнинг в явном виде при помощи -XX:CompileCommand=dontinline,Test::test
.
$ java -XX:CompileCommand=printcompilation,Test::* -XX:CompileCommand=compileonly,Test::test -Xbatch -XX:-TieredCompilation -XX:CompileCommand=printinlining,Test::test -XX:CompileCommand=dontinline,Test::* Test.java
CompileCommand: PrintCompilation Test.* bool PrintCompilation = true
CompileCommand: compileonly Test.test bool compileonly = true
CompileCommand: PrintInlining Test.test bool PrintInlining = true
CompileCommand: dontinline Test.* bool dontinline = true
Run
8263 85 b Test::test (22 bytes)
@ 3 Test::multiply (4 bytes) failed to inline: disallowed by CompileCommand
@ 10 Test::multiply (4 bytes) failed to inline: disallowed by CompileCommand
@ 17 Test::multiply (4 bytes) failed to inline: disallowed by CompileCommand
Done
Это может быть полезным, например, когда мы анализируем IR метода с большим количеством заинлайненного кода и пытаемся уменьшить сложность графа. Однако, в этом примере соответствующий IR становится более сложным без инлайнинга, потому что мы получаем все вызывающие ноды с их проекциями.
Соответствующий IR становится сложным, потому что вместо инлайнинга кода мы получаем три вызова multiply
. Я немного упростил граф:

Здесь мы видим голубые ConI
, которые передают каждый из аргументов 101, 202 и 53 в ноды CallStaticJava
, что соответствует индивидуальным вызовам multiply
. После этих вызовов мы должны произвести проверки на наличие исключений — поскольку мы не инлайним метод, мы не можем узнать о том, что они никогда не выбрасывают никаких исключений. Отметим, что, когда инлайнинг не выполняется, вызов метода по сути становится черным ящиком для текущей компиляции. Зеленые ноды Proj
забирают возвращаемые значения от вызовов и передают их в AddI
, которые складывают возвращаемые значения. Catch
, CatchProj
и CreateEx
передают свои возвращаемые значения в Rethrow
, который принимает их, если было выброшено исключение. Если исключения не было, мы идем по пути к следующему CallStaticJava
и рано или поздно попадаем в Return
.
Все это выглядит несколько сложно, но вам на этом этапе не обязательно понимать все досконально. Мы посмотрим на другие простые примеры с управлением потока позже. Часто IR может выглядеть весьма сложно, и требуется время, чтобы разобраться с тем, что означают все ноды.
Теперь мы увидели, что инлайнинг приводит нас от:
return multiply(101, a) + multiply(202, a) + multiply(53, b);
к
return 101 * a + 202 * a + 53 * b;
Но как мы получили 303 * a + 53 * b
?
Мы отследим все шаги сначала в IGV и затем в отладчике.
Использование IGV, идеального визуализатора графов
IdealGraphVisualizer является визуализатором для C2 IR.
Я обычно запускаю его следующим образом:
cd src/utils/IdealGraphVisualizer/
echo $JAVA_HOME
// If that prints nothing, you must set it to a JDK version between 17 and 21
// For example, I do:
// export JAVA_HOME=/oracle-work/jdk-17.0.8/
mvn clean install
// The install takes a while... and once complete we can launch IGV:
bash ./igv.sh
Сначала вы должны получить вот такое окно (оно может выглядеть несколько иначе, поскольку мы постоянно улучшаем IGV):

Затем вы можете отправить граф из нашего тестового прогона в IGV, используя -XX:PrintIdealGraphLevel=1
(работает только на отладочной сборке, но не на релизной):
java -XX:CompileCommand=printcompilation,Test::* -XX:CompileCommand=compileonly,Test::test -Xbatch -XX:-TieredCompilation -XX:CompileCommand=printinlining,Test::test -XX:PrintIdealGraphLevel=1 Test.java
На этом этапе IGV должен получить первый каталог, который содержит последовательность из трех графов:

Мы видим, что граф уже был сжат (folded) во время парсинга (см ноду 51 MulI
с вводом от 50 ConI
, где содержится значение int:303
):
Комментарий от редакции Spring АйО
Имеется в виду, что С2 IR провел оптимизацию "constant folding"-а. В нашем случае, речь про константу 303, которая получилось из суммы 101 и 202. Такая оптимизация в данном случае как раз стала возможна благодаря инлайнингу. Подробнее про constant folding можно почитать на Wikipedia: http://ru.wikipedia.org/wiki/%D0%A1%D0%B2%D1%91%D1%80%D1%82%D0%BA%D0%B0_%D0%BA%D0%BE%D0%BD%D1%81%D1%82%D0%B0%D0%BD%D1%82

Некоторое сжатие констант уже произошло во время парсинга. Можно сделать визуализацию графа более подробной с помощью -XX:PrintIdealGraphLevel=6
, а также получить второй каталог, содержащий еще больше графов:

Теперь мы сможем увидеть граф после парсинга каждой инструкции Java байткода. Графы выглядят довольно большими и несколько неопрятными, поскольку они еще не прошли чистку. Лично я нахожу IGV достаточно хорошим, чтобы получить общее представление о графе, но когда дело доходит до конкретных вопросов, я предпочитаю использовать прямую отладку с использованием rr, где я могу получить более точечный контроль и увидеть, как он соотносится с кодом на C++, который содержится в компиляторе.
Использование отладчика rr
Многие знакомы с gdb, часто используемым отладчиком для C / C++ / ассемблера. rr предлагает расширение функциональности gdb, позволяя выполнять reverse execution. Этот инструмент всегда был очень важен для отладки C2 компилятора: я часто вижу текущее состояние IR и не понимаю, как мы до этого дошли. Тогда я устанавливаю watchpoint-ы или breakpoint-ы и позволяю rr выполнять reverse execution, что приводит меня к более раннему состоянию и, в идеале, дает мне больше информации о том, что случилось и почему.
Возможно, вам следует обратиться к онлайн-туториалу, если вы никогда его не использовали. По сути, он поддерживает “навигационные” команды из gdb, но вы можете добавить слово reverse-
, чтобы пойти в обратном направлении (например, reverse-step
, reverse-continue
, reverse-next
или соответствующие шорткаты rs
, rc
, and rn
, соответственно). Далее я просто покажу, как я использую rr, но это не даст вам полной картины всего того, что вы можете делать с rr.
Из моего личного опыта могу сказать, что fastdebug
сборка от Hotspot намного быстрее чем slowdebug
, но для отладки я предпочитаю использовать slowdebug
, потому что этот способ кажется более надежным, а именно, дает меньше потерь при оптимизации. Также slowdebug
позволяет вам вызывать любые методы в том виде, как они выглядят в исходном коде, что не всегда верно для fastdebug
, поскольку они встраиваются напрямую. Локальные переменные тоже не теряются при оптимизации при использовании slowdebug
.
Теперь давайте запишем прогон с rr:
rr ./java -XX:CompileCommand=printcompilation,Test::* -XX:CompileCommand=compileonly,Test::test -Xbatch -XX:-TieredCompilation -XX:CompileCommand=printinlining,Test::test Test.java
Сразу после записи мы можем выполнить rr replay
на этом прогоне столько раз, сколько нам захочется, и он будет всегда проходить одинаково. Запись позволяет нам также выполнить reverse execution, поскольку у отладчика есть история, по которой он может ходить туда и сюда. Отметим, что вы все еще можете вызывать методы, модифицирующие состояние, на breakpoint-е или модифицировать переменные, память и т.д. Но как только вы оставите breakpoint, переместившись вперед или назад, все будет сброшено к состоянию, обнаруженному в записанном выполнении. На моей системе это выглядит следующим образом:
$ rr replay
GNU gdb (Ubuntu 9.2-0ubuntu1~20.04.2) 9.2
Copyright (C) 2020 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from /oracle-work/jdk-fork0/build/linux-x64-slowdebug/jdk/bin/java...
Reading symbols from /oracle-work/jdk-fork0/build/linux-x64-slowdebug/jdk/bin/java.debuginfo...
Really redefine built-in command "restart"? (y or n) [answered Y; input not from terminal]
Really redefine built-in command "jump"? (y or n) [answered Y; input not from terminal]
Remote debugging using 127.0.0.1:48849
Reading symbols from /lib64/ld-linux-x86-64.so.2...
Reading symbols from /usr/lib/debug/.build-id/db/0420f708b806cf03260aadb916c330049580b7.debug...
0x00007f0d32dfd100 in _start () from /lib64/ld-linux-x86-64.so.2
(rr) c
Continuing.
[New Thread 244952.244953]
Thread 2 received signal SIGSEGV, Segmentation fault.
[Switching to Thread 244952.244953]
0x00007f0d2cb3966c in ?? ()
(rr) b Optimize
Breakpoint 1 at 0x7f0d30a3b3ad: file /oracle-work/jdk-fork0/open/src/hotspot/share/opto/compile.cpp, line 2220.
(rr) c
Continuing.
CompileCommand: PrintCompilation Test.* bool PrintCompilation = true
CompileCommand: compileonly Test.test bool compileonly = true
CompileCommand: PrintInlining Test.test bool PrintInlining = true
[New Thread 244952.244961]
Run
9897 85 b Test::test (22 bytes)
[New Thread 244952.244954]
[New Thread 244952.244955]
[New Thread 244952.244956]
[New Thread 244952.244957]
[New Thread 244952.244958]
[New Thread 244952.244959]
[New Thread 244952.244960]
[New Thread 244952.244962]
[New Thread 244952.244963]
[Switching to Thread 244952.244961]
Thread 3 hit Breakpoint 1, Compile::Optimize (this=0x7f0d2556a870) at /oracle-work/jdk-fork0/open/src/hotspot/share/opto/compile.cpp:2220
warning: Source file is more recent than executable.
2220 TracePhase tp(_t_optimizer);
(rr)
Как только rr replay
запускается для java
, он имеет тенденцию прерываться в точке _start
. На этом этапе он еще не загрузил JVM код, и значит, мы еще не можем установить breakpoint-ы. Поэтому я даю команду continue
(или просто c
) один раз. Теперь он приостановится при получении SIGSEGV
, что ожидаемо (JVM, например, использует неявные null-проверки, то есть предполагает, что ссылка является not null, но когда она оказывается null, обработчик сигналов это отлавливает и далее идет по пути с исключением). Если вы получаете слишком много SIGSEGV
, вы можете пропустить их или заглушить их при помощи команды handle SIGSEGV nostop noprint
(вы также можете создать файл .gdbinit
, содержащий такую команду, в своем домашнем каталоге, чтобы она всегда выполнялась при следующем сеансе отладки). Поскольку все символы теперь доступны, я могу установить breakpoint в блоке Compile::Optimize
при помощи b Optimize
(вы можете установить breakpoint-ы раньше, rr только пожалуется об этом). Продолжаем идти вперед с помощью команды c
, и мы доберемся до Breakpoint 1
в блоке Compile::Optimize
.
Комментарий от редакции Spring АйО
В прошлой статье мы делали сноску с пояснениями, что понимается под символами в данном контексте: https://habr.com/ru/companies/spring_aio/articles/886142/
Мы видим, что добрались до строки:
2220 TracePhase tp(_t_optimizer);
Мы не можем сразу увидеть окружающий код. Можно использовать команду list
от gdb, чтобы быстро показать окружающий код:
(rr) list
2215 }
2216
2217 //------------------------------Optimize---------------------------------------
2218 // Given a graph, optimize it.
2219 void Compile::Optimize() {
2220 TracePhase tp(_t_optimizer);
2221
2222 #ifndef PRODUCT
2223 if (env()->break_at_compile()) {
2224 BREAKPOINT;
Обычно я использую TUI от gdb (Text User Interface, текстовый пользовательский интерфейс) как визуальный гайд, который можно включить через комбинацию клавиш Ctrl-x-a
:

Мы также можем получить backtrace/stacktrace:
(rr) bt
#0 Compile::Optimize (this=0x7f0d2556a870) at /oracle-work/jdk-fork0/open/src/hotspot/share/opto/compile.cpp:2220
#1 0x00007f0d30a34a28 in Compile::Compile (this=0x7f0d2556a870, ci_env=0x7f0d2556b6e0, target=0x7f0d284cc758,
osr_bci=-1, options=..., directive=0x7f0d283b3440)
at /oracle-work/jdk-fork0/open/src/hotspot/share/opto/compile.cpp:852
#2 0x00007f0d309003af in C2Compiler::compile_method (this=0x7f0d2813daa0, env=0x7f0d2556b6e0, target=0x7f0d284cc758,
entry_bci=-1, install_code=true, directive=0x7f0d283b3440)
at /oracle-work/jdk-fork0/open/src/hotspot/share/opto/c2compiler.cpp:142
#3 0x00007f0d30a57fc3 in CompileBroker::invoke_compiler_on_method (task=0x7f0d286a86d0)
at /oracle-work/jdk-fork0/open/src/hotspot/share/compiler/compileBroker.cpp:2319
#4 0x00007f0d30a56a44 in CompileBroker::compiler_thread_loop ()
at /oracle-work/jdk-fork0/open/src/hotspot/share/compiler/compileBroker.cpp:1977
#5 0x00007f0d30a76aab in CompilerThread::thread_entry (thread=0x7f0d28188ff0, __the_thread__=0x7f0d28188ff0)
at /oracle-work/jdk-fork0/open/src/hotspot/share/compiler/compilerThread.cpp:68
#6 0x00007f0d30ed61fa in JavaThread::thread_main_inner (this=0x7f0d28188ff0)
at /oracle-work/jdk-fork0/open/src/hotspot/share/runtime/javaThread.cpp:772
#7 0x00007f0d30ed608f in JavaThread::run (this=0x7f0d28188ff0)
at /oracle-work/jdk-fork0/open/src/hotspot/share/runtime/javaThread.cpp:757
#8 0x00007f0d31682995 in Thread::call_run (this=0x7f0d28188ff0)
at /oracle-work/jdk-fork0/open/src/hotspot/share/runtime/thread.cpp:232
#9 0x00007f0d313fc5d0 in thread_native_entry (thread=0x7f0d28188ff0)
at /oracle-work/jdk-fork0/open/src/hotspot/os/linux/os_linux.cpp:849
#10 0x00007f0d32b74609 in start_thread (arg=<optimized out>) at pthread_create.c:477
#11 0x00007f0d32a97353 in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:95
(rr)
Мы можем перемещаться вверх и вниз по backtrace при помощи команд up
и down
. Если мы перемещаемся на один уровень командой up
, мы видим, что в блоке Compile::Compile
много кода, но главное, мы получаем ciMethod* target
, для чего мы:
Осуществляем парсинг и потенциально рекурсивно встраиваем код. Это дает нам C2 IR.
Optimize
: здесь происходит более серьезная оптимизация.Code_Gen
: мы генерируем машинный код из оптимизированного IR.
Давайте посмотрим на IR от начала блока Compile::Optimize
:
(rr) p find_nodes_by_dump("")
0 Root === 0 66 [[ 0 1 3 52 50 ]]
1 Con === 0 [[ ]] #top
3 Start === 3 0 [[ 3 5 6 7 8 9 10 11 ]] #{0:control, 1:abIO, 2:memory, 3:rawptr:BotPTR, 4:return_address, 5:int, 6:int}
5 Parm === 3 [[ 66 ]] Control !jvms: Test::test @ bci:-1 (line 17)
6 Parm === 3 [[ 66 ]] I_O !jvms: Test::test @ bci:-1 (line 17)
7 Parm === 3 [[ 66 ]] Memory Memory: @BotPTR *+bot, idx=Bot; !jvms: Test::test @ bci:-1 (line 17)
8 Parm === 3 [[ 66 ]] FramePtr !jvms: Test::test @ bci:-1 (line 17)
9 Parm === 3 [[ 66 ]] ReturnAdr !jvms: Test::test @ bci:-1 (line 17)
10 Parm === 3 [[ 51 ]] Parm0: int !jvms: Test::test @ bci:-1 (line 17)
11 Parm === 3 [[ 64 ]] Parm1: int !jvms: Test::test @ bci:-1 (line 17)
50 ConI === 0 [[ 51 ]] #int:303
51 MulI === _ 10 50 [[ 65 ]] !jvms: Test::test @ bci:13 (line 17)
52 ConI === 0 [[ 64 ]] #int:53
64 MulI === _ 11 52 [[ 65 ]] !jvms: Test::multiply @ bci:2 (line 21) Test::test @ bci:17 (line 17)
65 AddI === _ 51 64 [[ 66 ]] !jvms: Test::test @ bci:20 (line 17)
66 Return === 5 6 7 8 9 returns 65 [[ 0 ]]
$1 = void
(rr)
Команда find_nodes_by_dump("")
проходится по всем IR нодам и выводит на печать те из них, которые соответствуют поисковой строке. При помощи пустой строки мы матчим все ноды и выводим на экран весь граф полностью. Отметим, что мы могли бы также проинспектировать текущий граф в IGV, выполнив команду p igv_print(true)
на этом breakpoint-е. Она отправляет текущий граф напрямую в IGV, где он затем попадает в список слева, соответствующий новому каталогу. Но в оставшейся части статьи мы сосредоточимся на рассмотрении графа напрямую через rr.
Мы также можем вывести на экран подграф следующим образом:
(rr) p find_node(65)->dump_bfs(2, 0, "#")
dist dump
---------------------------------------------
2 52 ConI === 0 [[ 64 ]] #int:53
2 11 Parm === 3 [[ 64 ]] Parm1: int !jvms: Test::test @ bci:-1 (line 17)
2 50 ConI === 0 [[ 51 ]] #int:303
2 10 Parm === 3 [[ 51 ]] Parm0: int !jvms: Test::test @ bci:-1 (line 17)
1 64 MulI === _ 11 52 [[ 65 ]] !jvms: Test::multiply @ bci:2 (line 21) Test::test @ bci:17 (line 17)
1 51 MulI === _ 10 50 [[ 65 ]] !jvms: Test::test @ bci:13 (line 17)
0 65 AddI === _ 51 64 [[ 66 ]] !jvms: Test::test @ bci:20 (line 17)
$3 = void
(rr)
При помощи команды find_node(65)
мы можем осуществить поиск по графу, чтобы найти ноду с индексом 65. Если она найдется, она будет возвращена, и мы можем выполнить на ней запрос dump_bfs()
(если вы используете несуществующий индекс ноды, будет возвращено значение nullptr
, и rr зависнет, пытаясь выполнить dump_bfs()
на nullptr
). dump_bfs()
— это мощный инструмент для печати различных подграфов, начиная с произвольно выбранной ноды, например, 65 AddI
. Если вы хотите узнать больше о том, какая функциональность поддерживается этим инструментом, введите команду dump_bfs(0,0,"h")
. Отметим, что символ #
включает цветную печать, которую я использую почти всегда.
Когда вас интересует только одна нода, вы можете вызвать dump()
напрямую из ноды:
(rr) p find_node(50)->dump()
50 ConI === 0 [[ 51 ]] #int:303
Мы также можем посмотреть на входы в ноду, используя dump(1)
:
10 Parm === 3 [[ 51 ]] Parm0: int !jvms: Test::test @ bci:-1 (line 17)
50 ConI === 0 [[ 51 ]] #int:303
51 MulI === _ 10 50 [[ 65 ]] !jvms: Test::test @ bci:13 (line 17)
Или напрямую выбирая отдельные входы в ноду, опрашивая их входы через in(input_index)
. Например, следующий код напечатает вход на индексе 2:
(rr) p find_node(51)->in(2)->dump()
50 ConI === 0 [[ 51 ]] #int:303
Когда мы смотрим на полный IR дамп, мы видим, что выражение 303 * a + 53 * b
на этом этапе уже прошло процедуру сжатия констант (constant-folding). Давайте разберемся, как это произошло, отступив на шаг назад в rr. Но отступать на шаг вручную было бы исключительно утомительным занятием.
Мы знаем, что константа 303
получилась путем сложения 202 + 101
и представлена как новая нода 50 ConI
, которая является входом в 51 MulI
. Мы можем сделать вывод, что 50 ConI
должна быть установлена как новых вход для 51 MulI
сразу после constant folding. Можем ли мы выяснить, где именно поменялась эта связь? В JVM в C++ коде файла node.hpp
мы видим, что input-edges, принадлежащие к Node, сохраняются в массиве in
. Я могу отслеживать изменения на таком input edge, установив watchpoint на адресе в памяти in[2]
следующим образом:
### Get the second input of 51 MulI which is 50 ConI
(rr) p find_node(51)->in(2)
$6 = (Node *) 0x7f0d284be0c8 <--- 50 ConI
### Get the same second input of 51 MulI but by directly fetching it from the input edge storing array
(rr) p find_node(51)->_in[2]
$7 = (Node *) 0x7f0d284be0c8 <--- 50 ConI
### Grab the memory location where the second input of 51 MulI (i.e. 50 ConI)
is currently stored as a pointer.
(rr) p &find_node(51)->_in[2]
$8 = (Node **) 0x7f0d284be1b8 <--- Memory location of the pointer to 50 ConI
### Set a watchpoint to break whenever this memory location changes.
This happens when we store 50 ConI as new second input to 51 MulI
(rr) watch *0x7f0d284be1b8
Hardware watchpoint 2: *0x7f0d284be1b8
### Reverse continue to the point where this change happens (i.e. the watchpoint triggers)
(rr) rc
Continuing.
Thread 3 hit Hardware watchpoint 2: *0x7f0d284be1b8
Old value = 676061384
New value = -1414812757
0x00007f0d313a71b8 in Node::Node (this=0x7f0d284be140, n0=0x0, n1=0x7f0d284bbea0, n2=0x7f0d284be0c8) at /oracle-work/jdk-fork0/open/src/hotspot/share/opto/node.cpp:380
380 _in[2] = n2; if (n2 != nullptr) n2->add_out((Node *)this);
(rr) bt
#0 0x00007f0d313a71b8 in Node::Node (this=0x7f0d284be140, n0=0x0, n1=0x7f0d284bbea0, n2=0x7f0d284be0c8) at /oracle-work/jdk-fork0/open/src/hotspot/share/opto/node.cpp:380
#1 0x00007f0d3068d693 in MulNode::MulNode (this=0x7f0d284be140, in1=0x7f0d284bbea0, in2=0x7f0d284be0c8) at /oracle-work/jdk-fork0/open/src/hotspot/share/opto/mulnode.hpp:44
#2 0x00007f0d3068d6e5 in MulINode::MulINode (this=0x7f0d284be140, in1=0x7f0d284bbea0, in2=0x7f0d284be0c8) at /oracle-work/jdk-fork0/open/src/hotspot/share/opto/mulnode.hpp:94
#3 0x00007f0d31374463 in MulNode::make (in1=0x7f0d284bbea0, in2=0x7f0d284be0c8, bt=T_INT) at /oracle-work/jdk-fork0/open/src/hotspot/share/opto/mulnode.cpp:218
#4 0x00007f0d30686d45 in AddNode::IdealIL (this=0x7f0d284bdfc8, phase=0x7f0d25569dc0, can_reshape=false, bt=T_INT) at /oracle-work/jdk-fork0/open/src/hotspot/share/opto/addnode.cpp:375
#5 0x00007f0d3068796a in AddINode::Ideal (this=0x7f0d284bdfc8, phase=0x7f0d25569dc0, can_reshape=false) at /oracle-work/jdk-fork0/open/src/hotspot/share/opto/addnode.cpp:583
#6 0x00007f0d31462fc4 in PhaseGVN::apply_ideal (this=0x7f0d25569dc0, k=0x7f0d284bdfc8, can_reshape=false) at /oracle-work/jdk-fork0/open/src/hotspot/share/opto/phaseX.cpp:669
#7 0x00007f0d3146300b in PhaseGVN::transform (this=0x7f0d25569dc0, n=0x7f0d284bdfc8) at /oracle-work/jdk-fork0/open/src/hotspot/share/opto/phaseX.cpp:682
#8 0x00007f0d3144bbaa in Parse::do_one_bytecode (this=0x7f0d25569990) at /oracle-work/jdk-fork0/open/src/hotspot/share/opto/parse2.cpp:2232
#9 0x00007f0d3143c9a5 in Parse::do_one_block (this=0x7f0d25569990) at /oracle-work/jdk-fork0/open/src/hotspot/share/opto/parse1.cpp:1587
#10 0x00007f0d314388fd in Parse::do_all_blocks (this=0x7f0d25569990) at /oracle-work/jdk-fork0/open/src/hotspot/share/opto/parse1.cpp:725
#11 0x00007f0d3143841f in Parse::Parse (this=0x7f0d25569990, caller=0x7f0d284c78b0, parse_method=0x7f0d284cc758, expected_uses=6784)
at /oracle-work/jdk-fork0/open/src/hotspot/share/opto/parse1.cpp:629
#12 0x00007f0d30902bb1 in ParseGenerator::generate (this=0x7f0d284c7898, jvms=0x7f0d284c78b0) at /oracle-work/jdk-fork0/open/src/hotspot/share/opto/callGenerator.cpp:99
#13 0x00007f0d30a34600 in Compile::Compile (this=0x7f0d2556a870, ci_env=0x7f0d2556b6e0, target=0x7f0d284cc758, osr_bci=-1, options=..., directive=0x7f0d283b3440)
at /oracle-work/jdk-fork0/open/src/hotspot/share/opto/compile.cpp:797
#14 0x00007f0d309003af in C2Compiler::compile_method (this=0x7f0d2813daa0, env=0x7f0d2556b6e0, target=0x7f0d284cc758, entry_bci=-1, install_code=true, directive=0x7f0d283b3440)
at /oracle-work/jdk-fork0/open/src/hotspot/share/opto/c2compiler.cpp:142
#15 0x00007f0d30a57fc3 in CompileBroker::invoke_compiler_on_method (task=0x7f0d286a86d0) at /oracle-work/jdk-fork0/open/src/hotspot/share/compiler/compileBroker.cpp:2319
#16 0x00007f0d30a56a44 in CompileBroker::compiler_thread_loop () at /oracle-work/jdk-fork0/open/src/hotspot/share/compiler/compileBroker.cpp:1977
#17 0x00007f0d30a76aab in CompilerThread::thread_entry (thread=0x7f0d28188ff0, __the_thread__=0x7f0d28188ff0)
at /oracle-work/jdk-fork0/open/src/hotspot/share/compiler/compilerThread.cpp:68
#18 0x00007f0d30ed61fa in JavaThread::thread_main_inner (this=0x7f0d28188ff0) at /oracle-work/jdk-fork0/open/src/hotspot/share/runtime/javaThread.cpp:772
#19 0x00007f0d30ed608f in JavaThread::run (this=0x7f0d28188ff0) at /oracle-work/jdk-fork0/open/src/hotspot/share/runtime/javaThread.cpp:757
#20 0x00007f0d31682995 in Thread::call_run (this=0x7f0d28188ff0) at /oracle-work/jdk-fork0/open/src/hotspot/share/runtime/thread.cpp:232
#21 0x00007f0d313fc5d0 in thread_native_entry (thread=0x7f0d28188ff0) at /oracle-work/jdk-fork0/open/src/hotspot/os/linux/os_linux.cpp:849
#22 0x00007f0d32b74609 in start_thread (arg=<optimized out>) at pthread_create.c:477
#23 0x00007f0d32a97353 in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:95
(rr)
Мы видим, что этот edge был установлен в MulINode::MulINode
, конструкторе 51 MulI
. Если пройти еще дальше вверх по backtrace, мы увидим, что это случилось в AddINode::Ideal
(идеализация некоторых AddI
— мы посмотрим на это ниже). Еще дальше вверх, и мы увидим, что это происходит во время PhaseGVN::transform
(мы осуществляем GVN - global value numbering, глобальную нумерацию значений). И еще дальше вверх, и мы увидим, что это происходит во время Parse::do_one_bytecode
(парсинг байткода) и, наконец, внутри Compile::Compile
.
Давайте посмотрим на все эти шаги поочередно.
В Parse::do_one_bytecode
, мы вроде бы парсим iadd
байткод инструкцию (см. parse2.cpp):
case Bytecodes::_iadd:
b = pop(); a = pop();
push( _gvn.transform( new AddINode(a,b) ) );
break;
Здесь применяется семантика iadd
bytecode:
pop
— извлекаем два аргумента из стека.Вычисляем сумму: здесь мы создаем
AddINode
, и GVN сразу же трансформирует ее.push
— помещаем результат обратно в стек.
Во время PhaseGVN::transform
мы уже производим некоторые локальные оптимизации (см. phaseX.hpp):
apply_ideal
итеративно вызываетNode::Ideal
(при этом опцияcan_reshape
отключена, расскажу об этом больше далее): это попытка сконструировать более “идеальный” (каноничный и/или очищенный) подграф.Node::Value
: Вычисляется более узкий тип, возможное сжатие констант на ноде (см.singleton
).Node::Identity
: пытается найти другую ноду, которая “делает то же самое” и заменяет текущую ноду на найденную.
GVN очень важна, поскольку она канонизирует граф в рамках подготовки к другим оптимизациям (им теперь надо только матчиться с каноничными паттернами, что упрощает оптимизацию записи). И она также выполняет локальные оптимизации, такие как сжатие констант.
Теперь давайте посмотрим на самую нижнюю часть backtrace, где происходит идеализация. В AddNode::IdealIL
мы видим, что у нас получилось сматчиться под определенный Associative
паттерн (см. addnode.cpp).
(rr) p this->dump_bfs(2,0,"#")
dist dump
---------------------------------------------
2 35 ConI === 0 [[ 25 43 47 49 ]] #int:202
2 23 ConI === 0 [[ 22 31 34 49 ]] #int:101
2 10 Parm === 3 [[ 4 19 22 22 25 31 34 43 47 51 ]] Parm0: int !jvms: Test::test @ bci:-1 (line 17)
1 47 MulI === _ 10 35 [[ 42 37 48 ]] !jvms: Test::multiply @ bci:2 (line 21) Test::test @ bci:10 (line 17)
1 34 MulI === _ 10 23 [[ 30 25 37 48 ]] !jvms: Test::multiply @ bci:2 (line 21) Test::test @ bci:3 (line 17)
0 48 AddI === _ 34 47 [[ ]] !jvms: Test::test @ bci:13 (line 17)
Это (a * 101) + (a * 202)
, которое матчится под паттерн Convert "a*b+a*c into a*(b+c)
в коде. Распечатав локальные результаты, мы получим:
(rr) p add_in2->dump()
35 ConI === 0 [[ 25 43 47 49 ]] #int:202
(rr) p add_in1->dump()
23 ConI === 0 [[ 22 31 34 49 ]] #int:101
(rr) p add_in2->dump()
35 ConI === 0 [[ 25 43 47 49 ]] #int:202
(rr) p add->dump()
50 ConI === 0 [[ ]] #int:303
(rr) p mul_in->dump()
10 Parm === 3 [[ 4 19 22 22 25 31 34 43 47 51 ]] Parm0: int !jvms: Test::test @ bci:-1 (line 17)
Оказывается, что в Node* add = phase->transform(AddNode::make(add_in1, add_in2, bt));
GVN transform уже сконвертировал b + c = 202 + 101
в 303
.
Ну и наконец-то AddNode::IdealIL
выполняет return MulNode::make(mul_in, add, bt);
, что дает нам a * 303
. Это 51 MulI
с константным вводом 303
, для которого мы установили watchpoint раньше.
Примечание: мне понадобилось много времени, чтобы почувствовать себя комфортно, перемещаясь по коду от C2 с помощью rr и отслеживая такие преобразования графов. Поэтому не пугайтесь, если воспроизведение этих действий сначала покажется вам трудной задачей.Мне помогло рисование IR графов на бумаге и предварительное планирование различных шагов при помощи “дорожной карты”.
Я также хочу упомянуть о том, что я представил здесь только один возможный способ понять, где было произведено сжатие констант с помощью rr. Существует много других трюков, позволяющих достичь этого результата в rr, например, выяснив, где создавалась новая нода с определенным индексом. Я пока не буду в это углубляться, но, возможно, когда-нибудь напишу блог-пост о распространенных приемах работы с rr.
Упражнения для читателя
Я рекомендую вам заново пройти описанные выше шаги и посмотреть, сможете ли вы отследить их в коде. Ниже я дам вам еще несколько примеров таких GVN трансформаций, чтобы вы могли поизучать код и самостоятельно найти, где эти трансформации были произведены.
public class Test2 {
public static void main(String[] args) {
System.out.println("Run test1");
for (int i = 0; i < 10_000; i++) {
test1(i, i + 1);
}
System.out.println("Run test2");
for (int i = 0; i < 10_000; i++) {
test2(i);
}
System.out.println("Run test3");
for (int i = 0; i < 10_000; i++) {
test3(i);
}
System.out.println("Done");
}
public static int test1(int a, int b) {
// Transformed into: a + b
return ((42 + a) + b) - 42;
}
public static int var2 = 0;
public static int test2(int a) {
// putstatic / StoreI to var2
var2 = a;
// loadstatic / LoadI from var2
// -> is replaced with "a", we forward the stored value. The load is optimized away.
return var2;
}
public static int test3(int a) {
// Transformed into: (a << 6) + a
// Bonus question: why might this be better?
return a * 65;
}
}
А вот сгенерированный bytecode:
javac Test2.java
javap -c Test2.class
public class Test2 {
public static int var2;
...
public static int test1(int, int);
Code:
0: bipush 42
2: iload_0
3: iadd
4: iload_1
5: iadd
6: bipush 42
8: isub
9: ireturn
public static int test2(int);
Code:
0: iload_0
1: putstatic #40 // Field var2:I
4: getstatic #40 // Field var2:I
7: ireturn
public static int test3(int);
Code:
0: iload_0
1: bipush 65
3: imul
4: ireturn
static {};
Code:
0: iconst_0
1: putstatic #40 // Field var2:I
4: return
}
Этот код приведен только для того, чтобы показать, что javac
еще не произвел никаких трансформаций ;)
А вот сгенерированный IR после трансформаций:
java -XX:CompileCommand=printcompilation,Test2::* -XX:CompileCommand=compileonly,Test2::test* -Xbatch -XX:-TieredCompilation -XX:+PrintIdeal Test2.java
CompileCommand: PrintCompilation Test2.* bool PrintCompilation = true
CompileCommand: compileonly Test2.test* bool compileonly = true
Run test1
8461 85 b Test2::test1 (10 bytes)
AFTER: print_ideal
0 Root === 0 31 [[ 0 1 3 ]] inner
3 Start === 3 0 [[ 3 5 6 7 8 9 10 11 ]] #{0:control, 1:abIO, 2:memory, 3:rawptr:BotPTR, 4:return_address, 5:int, 6:int}
5 Parm === 3 [[ 31 ]] Control !jvms: Test2::test1 @ bci:-1 (line 23)
6 Parm === 3 [[ 31 ]] I_O !jvms: Test2::test1 @ bci:-1 (line 23)
7 Parm === 3 [[ 31 ]] Memory Memory: @BotPTR *+bot, idx=Bot; !jvms: Test2::test1 @ bci:-1 (line 23)
8 Parm === 3 [[ 31 ]] FramePtr !jvms: Test2::test1 @ bci:-1 (line 23)
9 Parm === 3 [[ 31 ]] ReturnAdr !jvms: Test2::test1 @ bci:-1 (line 23)
10 Parm === 3 [[ 26 ]] Parm0: int !jvms: Test2::test1 @ bci:-1 (line 23)
11 Parm === 3 [[ 26 ]] Parm1: int !jvms: Test2::test1 @ bci:-1 (line 23)
26 AddI === _ 10 11 [[ 31 ]] !orig=24 !jvms: Test2::test1 @ bci:3 (line 23)
31 Return === 5 6 7 8 9 returns 26 [[ 0 ]]
Run test2
8462 86 b Test2::test2 (8 bytes)
AFTER: print_ideal
0 Root === 0 30 [[ 0 1 3 22 23 ]] inner
1 Con === 0 [[ ]] #top
3 Start === 3 0 [[ 3 5 6 7 8 9 10 ]] #{0:control, 1:abIO, 2:memory, 3:rawptr:BotPTR, 4:return_address, 5:int}
5 Parm === 3 [[ 30 26 ]] Control !jvms: Test2::test2 @ bci:-1 (line 29)
6 Parm === 3 [[ 30 ]] I_O !jvms: Test2::test2 @ bci:-1 (line 29)
7 Parm === 3 [[ 16 26 ]] Memory Memory: @BotPTR *+bot, idx=Bot; !jvms: Test2::test2 @ bci:-1 (line 29)
8 Parm === 3 [[ 30 ]] FramePtr !jvms: Test2::test2 @ bci:-1 (line 29)
9 Parm === 3 [[ 30 ]] ReturnAdr !jvms: Test2::test2 @ bci:-1 (line 29)
10 Parm === 3 [[ 30 26 ]] Parm0: int !jvms: Test2::test2 @ bci:-1 (line 29)
16 MergeMem === _ 1 7 1 26 [[ 30 ]] { - N26:java/lang/Class (java/io/Serializable,java/lang/constant/Constable,java/lang/reflect/AnnotatedElement,java/lang/invoke/TypeDescriptor,java/lang/reflect/GenericDeclaration,java/lang/reflect/Type,java/lang/invoke/TypeDescriptor$OfField):exact+112 * } Memory: @BotPTR *+bot, idx=Bot;
22 ConP === 0 [[ 25 25 ]] #java/lang/Class (java/io/Serializable,java/lang/constant/Constable,java/lang/reflect/AnnotatedElement,java/lang/invoke/TypeDescriptor,java/lang/reflect/GenericDeclaration,java/lang/reflect/Type,java/lang/invoke/TypeDescriptor$OfField):exact * Oop:java/lang/Class (java/io/Serializable,java/lang/constant/Constable,java/lang/reflect/AnnotatedElement,java/lang/invoke/TypeDescriptor,java/lang/reflect/GenericDeclaration,java/lang/reflect/Type,java/lang/invoke/TypeDescriptor$OfField):exact *
23 ConL === 0 [[ 25 ]] #long:112
25 AddP === _ 22 22 23 [[ 26 ]] Oop:java/lang/Class (java/io/Serializable,java/lang/constant/Constable,java/lang/reflect/AnnotatedElement,java/lang/invoke/TypeDescriptor,java/lang/reflect/GenericDeclaration,java/lang/reflect/Type,java/lang/invoke/TypeDescriptor$OfField):exact+112 * !jvms: Test2::test2 @ bci:1 (line 29)
26 StoreI === 5 7 25 10 [[ 16 ]] @java/lang/Class (java/io/Serializable,java/lang/constant/Constable,java/lang/reflect/AnnotatedElement,java/lang/invoke/TypeDescriptor,java/lang/reflect/GenericDeclaration,java/lang/reflect/Type,java/lang/invoke/TypeDescriptor$OfField):exact+112 *, name=var2, idx=4; Memory: @java/lang/Class (java/io/Serializable,java/lang/constant/Constable,java/lang/reflect/AnnotatedElement,java/lang/invoke/TypeDescriptor,java/lang/reflect/GenericDeclaration,java/lang/reflect/Type,java/lang/invoke/TypeDescriptor$OfField):exact+112 *, name=var2, idx=4; !jvms: Test2::test2 @ bci:1 (line 29)
30 Return === 5 6 16 8 9 returns 10 [[ 0 ]]
Run test3
8463 87 b Test2::test3 (5 bytes)
AFTER: print_ideal
0 Root === 0 29 [[ 0 1 3 26 ]] inner
3 Start === 3 0 [[ 3 5 6 7 8 9 10 ]] #{0:control, 1:abIO, 2:memory, 3:rawptr:BotPTR, 4:return_address, 5:int}
5 Parm === 3 [[ 29 ]] Control !jvms: Test2::test3 @ bci:-1 (line 37)
6 Parm === 3 [[ 29 ]] I_O !jvms: Test2::test3 @ bci:-1 (line 37)
7 Parm === 3 [[ 29 ]] Memory Memory: @BotPTR *+bot, idx=Bot; !jvms: Test2::test3 @ bci:-1 (line 37)
8 Parm === 3 [[ 29 ]] FramePtr !jvms: Test2::test3 @ bci:-1 (line 37)
9 Parm === 3 [[ 29 ]] ReturnAdr !jvms: Test2::test3 @ bci:-1 (line 37)
10 Parm === 3 [[ 28 27 ]] Parm0: int !jvms: Test2::test3 @ bci:-1 (line 37)
26 ConI === 0 [[ 27 ]] #int:6
27 LShiftI === _ 10 26 [[ 28 ]] !jvms: Test2::test3 @ bci:3 (line 37)
28 AddI === _ 27 10 [[ 29 ]] !jvms: Test2::test3 @ bci:3 (line 37)
29 Return === 5 6 7 8 9 returns 28 [[ 0 ]]
Done
Теперь ваша задача - посмотреть на эти три метода поочередно. Ограничьте компиляцию только одним методом, запишите ее в rr и пройдитесь по коду шаг за шагом.
Вы можете описать свои наблюдения в комментариях внизу и обсудить свои решения.

Регистрируйтесь на главную конференцию про Spring на русском языке от сообщества Spring АйО! В мероприятии примут участие не только наши эксперты, но и приглашенные лидеры индустрии.