Новый JIT-компилятор Graal

Как уже говорилось в предыдущих статьях, в JVM HotSpot имеется два JIT-компилятора: C1 и C2. Первый компилятор — клиентский (client), который быстренько компилирует код при старте и первых минутах работы JVM, потом некоторое время собирает статистику по работе приложения, после чего в дело вступает компилятор C2 — серверный (server) — который делает более глубокие оптимизации и создает более быстрый машинный код.

Компилятор C2 — надежная рабочая лошадка

Компилятор C2 замечательно себя зарекомендовал за столько лет. Он прекрасно справляется со своей работой. Но пришло время признать, что время жизни компилятора C2 подошло к концу:

  • C2 компилятор написан на C++. И это создает определенные проблемы для дальнейшего развития. Его трудно писать, изменять и поддерживать: малейшая ошибка в коде может привести к краху JVM и важного приложения, а хороших C++ программистов не так уж много и они дорого стоят.
  • Больше улучшать в нем уже нечего, ресурс улучшений исчерпан. За последнее время в компилятор C2 не вносилось никаких существенных изменений, а дальнейшие улучшения в C2 компиляторе планируются только за счет использования новых дополнительных instrinsics.

Какой был предложен выход

Первое, было предложено открыть API компилятора в JVM так, чтобы он был доступен извне и вместо одного компилятора можно было бы подключать к JVM любой другой. Начиная с Java 9 в JVM этот новый API — JVMCI (JVM Compiler Interface) — стал доступен в качестве экспериментальной опции, которая включается так:

-XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI

Написание собственного компилятора — нетривиальная задача. Но в JVM она упрощается тем фактом, что код из исходников уже скомпилирован в байт-код JVM. Этот байт-код JVM интерпретирует на лету и выполняет его до тех пор, пока не вступит в силу JIT-компилятор, задача которого взять байт-код и преобразовать его в набор машинных инструкций. То есть на входе компилятор получает массив байтов (byte[] in) и выдает на выходе тоже набор байтов (byte[] out).

В теории нет никаких причин, по которым это преобразование одного массива байтов в другой должно непременно быть написано на С++. Его можно написать и на Java! И такой компилятор не будет работать, как нечто инородное, а как набор потоков, работающих параллельно с потоками самого приложения. Такой компилятор, конечно, будет работать медленнее чем, написанный на C++, но ведь код компилятора тоже может быть соптимизирован, то есть компилятор может компилировать сам себя. И работать после такой компиляции он будет также быстро, как если б он был изначально написан на С++!

Начиная с Java 10 JVM HotSpot поставляется с двумя встроенными JIT-компиляторами: C2, который включается по умолчанию, и новый JIT-компилятор graal, который включен в JVM как экспериментальный (JEP 317) и написан на Java.

Внимание: в JDK 17 компилятор graal отсутствует! Читайте Update в конце статьи.

Как включить graal

Новый экспериментальный JIT-компилятор graal включается с помощью следующих флагов:

-XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI -XX:+UseJVMCICompiler

-XX:+UnlockExperimentalVMOptions — открывает доступ к экспериментальным опциям

-XX:+EnableJVMCI — открывает доступ к интерфейсу компилятора

-XX:+UseJVMCICompiler — включает внутренний экспериментальный JIT-компилятор graal

Если JVM запущена с флагом -XX:+UseJVMCICompiler, но без флага -XX:+EnableJVMCI, будет использован JIT-компилятор graal, но не будет доступа к API.

Если -XX:+EnableJVMCI, а -XX:+UseJVMCICompiler отсутствует, JVM будет использовать C2 JIT-компилятор.

Короче, для запуска JVM с JIT-компилятором graal как минимум ОДИН флаг -XX:+UseJVMCICompiler, а для полного доступа к API — ДВА флага: -XX:+EnableJVMCI и -XX:+UseJVMCICompiler.

Как увидеть, какой компилятор работает

Чтобы увидеть, как происходит компиляция, добавьте параметр -XX:+PrintCompilation. В консоли вывода вы увидите следующие строки:

 82    1       3       java.lang.String::charAt (25 bytes)
 83    3       3       java.lang.String::isLatin1 (19 bytes)
 83    2       3       java.lang.StringLatin1::charAt (28 bytes)
 84    5       3       java.lang.Object::<init> (1 bytes)
 84    6       3       java.lang.StringLatin1::hashCode (42 bytes)
 84    4       3       java.lang.String::hashCode (60 bytes)

Если же включить graal, то среди строк компиляции вы увидите следующее:

1310 3958 % 1 org.graalvm.compiler.phases.schedule.SchedulePhase$Instance$GuardOrder::propagatePriority @ 53 (307 bytes)
1312 4013 % 1 org.graalvm.compiler.loop.LoopFragment::computeNodes @ 396 (653 bytes)
1316 3836 1 org.graalvm.compiler.nodes.cfg.Block::getKillLocationsBetweenThisAndDominator (179 bytes)
1317 3761 % 1 org.graalvm.compiler.lir.alloc.lsra.LinearScanLifetimeAnalysisPhase::numberInstructions @ 145 (309 bytes)

Строки org.graalvm.compiler … говорят то том, что graal включен и он компилирует сам себя.

Как подключить другой JIT-компилятор

Как было сказано выше, API JVMCI позволяет подключить и внешний JIT-компилятор, который написал кто-то другой или вы сами. Для этого предусмотрен дополнительный флаг -Djvmci.Compiler=<compiler-name>. Например,

-Djvmci.Compiler=graal 

требует от JVM включить компилятор graal, который и так включается по-умолчанию. Если в вашем распоряжении имеется свой компилятор на замену graal-ю, укажите его название в этом флаге, скажем:

-Djvmci.Compiler=mySuperPuperJITCompiler

JMVC API позволяет управлять более тонко работой компилятора с помощью определенных флагов:

 bool BootstrapJVMCI                           = false 
 bool EagerJVMCI                               = false 
 bool EnableJVMCI                              = true  
 bool EnableJVMCIProduct                       = false 
 intx JVMCICounterSize                         = 0     
 bool JVMCICountersExcludeCompiler             = true  
 intx JVMCIHostThreads                         = 1     
ccstr JVMCILibDumpJNIConfig                    =       
ccstr JVMCILibPath                             =       
 intx JVMCINMethodSizeLimit                    = 655360
 bool JVMCIPrintProperties                     = false
 intx JVMCIThreads                             = 1   
 intx JVMCITraceLevel                          = 0   
 bool PrintBootstrap                           = true
 bool UseJVMCICompiler                         = true
 bool UseJVMCINativeLibrary                    = false

Например, флаг -XX:JVMCIThreads=<n> позволяет задать количество потоков, в которых будет работать компилятор. Флаг -XX:+JVMCIPrintProperties позволяет вывести все свойства компилятора graal, которыми тоже можно управлять. И прочее.

Выводы

JIT-компилятор graal по-прежнему (даже в свежем JDK 15) является экспериментальным. По-прежнему компилятор C2 является вашим надежным оружием в достижении low-latency. Но не забывайте, что за graal — будущее и с каждым новым релизом JDK это будущее приближается. JVMCI API позволяет подключить свой компилятор и настроить компиляцию его под свои требования к low-latency намного лучше, чем это позволяет C2.

Компилятор graal не следует путать с проектом GraalVM, который охватывает несколько технологий: 1) технологию JIT-компилятора (graal), 2) технологию поддержки множества языков на VM и 3) runtime для этого.

Update: В сентябре 2022 года вышла версия Java 17. В ней был реализован JEP 410, который удаляет компилятор graal из JDK 17. Не понимаю, почему столь интересная и полезная функция была удалена, и никто не протестовал против этого. Интерфейс JVMCI оставлен и может быть использован по-прежнему для подключения стороннего компилятора.

Ссылки

Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход /  Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход /  Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход /  Изменить )

Connecting to %s