Fight the Future

Java言語とJVM、そしてJavaエコシステム全般にまつわること

Graalが使うJVMCIとは

前回ではGraalをJITコンパイラとして使うため、EnableJVMCIでJVMCIを有効にしました。

jyukutyo.hatenablog.com

引き続き

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"のことであり、JavaJITコンパイラを実装するためのインタフェースを提供するものです。

日本語での情報としては、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を表している感じです。

f:id:jyukutyo:20171124150819p:plain

これ以降のコードは、各CPU用の実装でLIR(低水準中間表現)を出力する内容です。私にはまだわからなかったので、これでおしまいにします。