前回ではGraalをJITコンパイラとして使うため、EnableJVMCI
でJVMCIを有効にしました。
引き続き
Understanding How Graal Works - a Java JIT Compiler Written in Java
を参考に、Graalの理解を深めます。
ところでJVMCIとは何なのでしょうか?JVMCIはJEP 243で定義されています。
JEP 243: Java-Level JVM Compiler Interface
つまりJVMCIは"JVM compiler interface"のことであり、JavaでJITコンパイラを実装するためのインタフェースを提供するものです。
日本語での情報としては、JavaDay Tokyo 2017での、OpenJDK Reviwerである末永さんのセッションスライドがわかりやすいです。
www.slideshare.net
JITコンパイルでの処理
ところで、JITコンパイルは何をするものだったでしょう?Javaバイトコードからマシンコード(機械語)を生成するものですね。
これをJavaのメソッドとして定義するとどうなるでしょう?入力はJavaバイトコード、つまりbyte配列となります。マシンコードは?CPUごとの命令セットを使うわけですから、これもbyte配列で表せます。なので、そのメソッドはこんな感じですね。
byte[] compileMethod(byte[] bytecode)
JITコンパイラの実装としては、実は単にbyte配列からbyte配列を生成するだけ、と考えることもできます。さて、実際のインタフェースであるJVMCICompiler
を見てみます。
package jdk.vm.ci.runtime; import jdk.vm.ci.code.CompilationRequest; import jdk.vm.ci.code.CompilationRequestResult; public interface JVMCICompiler { int INVOCATION_ENTRY_BCI = -1; /** * Services a compilation request. This object should compile the method to machine code and * install it in the code cache if the compilation is successful. */ CompilationRequestResult compileMethod(CompilationRequest request); }
byte配列そのままではありませんが、CompilationRequest
を受け取りCompilationRequestResult
を返すメソッドとなっています。まず引数のCompilationRequest
クラスを見てみます。
package jdk.vm.ci.code; import jdk.vm.ci.meta.ResolvedJavaMethod; /** * Represents a request to compile a method. */ public class CompilationRequest { private final ResolvedJavaMethod method; ... /** * Gets the method to be compiled. */ public ResolvedJavaMethod getMethod() { return method; } ... }
ResolvedJavaMethod getMethod()
でコンパイルの対象となるJavaのメソッドを取得します。ResolvedJavaMethod
インタフェースを見ます。
/** * Represents a resolved Java method. Methods, like fields and types, are resolved through * {@link ConstantPool constant pools}. */ public interface ResolvedJavaMethod extends JavaMethod, InvokeTarget, ModifiersProvider, AnnotatedElement { /** * Returns the bytecode of this method, if the method has code. The returned byte array does not * contain breakpoints or non-Java bytecodes. This may return null if the * {@link #getDeclaringClass() holder} is not {@link ResolvedJavaType#isLinked() linked}. * * The contained constant pool indices may not be the ones found in the original class file but * they can be used with the JVMCI API (e.g. methods in {@link ConstantPool}). * * @return the bytecode of the method, or {@code null} if {@code getCodeSize() == 0} or if the * code is not ready. */ byte[] getCode(); ... }
getCode()
するとbyte配列が取得できます。これをマシンコードにすればいいわけですね。ほかにもResolvedJavaMethod
からコンパイル対象となるメソッドに関するさまざまな情報を取得できます。
では戻り値のCompilationRequestResult
インタフェースを見ます。
package jdk.vm.ci.code; /** * Provides information about the result of a {@link CompilationRequest}. */ public interface CompilationRequestResult { /** * Determines if the compilation was successful. * * @return a non-null object whose {@link Object#toString()} describes the failure or null if * compilation was successful */ Object getFailure(); }
おや、getFailure()
しかありません。これは失敗のメッセージを取得するものです。マシンコードは返していません。どうするのでしょう??
JVMCICompiler
をもう一度見てみると"compile the method to machine code and install it in the code cache"とあります。コンパイルしたコードは戻り値で返すのではなくコードキャッシュにインストールするものでした。
どのようにコードキャッシュにインストールするかは仕様としては定義していないので、ここから先は各JITコンパイラの実装で異なりそうです。Graalを見てみます。
Graalの場合はorg.graalvm.compiler.hotspot.HotSpotGraalCompiler
がJVMCICompilerの実装クラスです。そのcompileMethod
メソッドを読み進めていくと、コードキャッシュへのインストールはorg.graalvm.compiler.hotspot.CompilationTask#installMethod
でやっています。
private void installMethod(DebugContext debug, final CompilationResult compResult) { final CodeCacheProvider codeCache = jvmciRuntime.getHostJVMCIBackend().getCodeCache(); HotSpotBackend backend = compiler.getGraalRuntime().getHostBackend(); installedCode = null; Object[] context = {new DebugDumpScope(getIdString(), true), codeCache, getMethod(), compResult}; try (DebugContext.Scope s = debug.scope("CodeInstall", context)) { installedCode = (HotSpotInstalledCode) backend.createInstalledCode(debug, getRequest().getMethod(), getRequest(), compResult, getRequest().getMethod().getSpeculationLog(), null, installAsDefault, context); } catch (Throwable e) { throw debug.handle(e); } }
backend.createInstalledCode()
にJITコンパイル結果であるCompilationResult
を渡しているので、ここでコードキャッシュへのインストールを実行しています。
HotSpotBackend
は各CPUを表している感じです。
これ以降のコードは、各CPU用の実装でLIR(低水準中間表現)を出力する内容です。私にはまだわからなかったので、これでおしまいにします。