Search
Write a publication
Pull to refresh
115.15

За кулисами JIT: Секреты HotSpot JVM C2 компилятора (Часть 2)

Level of difficultyMedium
Reading time23 min
Views1.8K
Original author: Emanuel Peter

Новый перевод от команды 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 АйО! В мероприятии примут участие не только наши эксперты, но и приглашенные лидеры индустрии.

Tags:
Hubs:
Total votes 6: ↑6 and ↓0+8
Comments0

Articles

Information

Website
t.me
Registered
Employees
11–30 employees