Fight the Future

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

GraalでのJavaバイトコードからグラフの生成

前回はGraalのグラフを見ました。

jyukutyo.hatenablog.com

実際にどのようにグラフを作るかという謎はありますが、視点を変えて実際にJavaバイトコードをマシンコードに変換する部分を見てみます。

Chris Seatonさんの記事に沿ってGraalの理解を深めます。

Understanding How Graal Works - a Java JIT Compiler Written in Java

サンプルとして加算するメソッドを用意します。

class Demo {
    public static void main(String[] args) {
        while (true) {
            new Demo().workload(14, 2);
        }
    }

    private int workload(int a, int b) {
        return a + b;
    }
}

JITコンパイルをする前にどんなバイトコードなのかを出力しておきます。org.graalvm.compiler.hotspot.HotSpotGraalCompilerです。

    @Override
    public CompilationRequestResult compileMethod(CompilationRequest request) {
        System.err.println(request.getMethod().getName() + " bytecode: "
                + Arrays.toString(request.getMethod().getCode()));
        return compileMethod(request, true);
    }

eclipseであればIDE設定がうまくできているので、ビルドの必要はないそうです(元記事より)。僕はIntelliJ IDEAなので、outの設定やらモジュール間の依存関係設定がうまくいっていないので、mx buildでビルドしました。

で、実行します。

$ java \
--module-path=/Users/jyukutyo/code/graal/sdk/mxbuild/modules/org.graalvm.graal_sdk:/Users/jyukutyo/code/graal/truffle/mxbuild/modules/com.oracle.truffle.truffle_api.jar \
--upgrade-module-path=/Users/jyukutyo/code/graal/compiler/mxbuild/modules/jdk.internal.vm.compiler \
-XX:+UnlockExperimentalVMOptions \
-XX:+EnableJVMCI \
-XX:+UseJVMCICompiler \
-XX:-TieredCompilation \
-XX:+PrintCompilation \
-XX:CompileOnly=Demo::workload \
-XX:CompileCommand=quiet \
Demo
...
workload bytecode: [26, 27, 96, -84]
...

96があるので、iaddだとわかりますね*1

このバイト配列を、前回の内容であるグラフにします。グラフビルダがあります。実装はorg.graalvm.compiler.replacements.IntrinsicGraphBuilderのように思います。

/**
 * Implementation of {@link GraphBuilderContext} used to produce a graph for a method based on an
 * {@link InvocationPlugin} for the method.
 */
public class IntrinsicGraphBuilder implements GraphBuilderContext, Receiver {

    protected final MetaAccessProvider metaAccess;
    protected final ConstantReflectionProvider constantReflection;
    protected final ConstantFieldProvider constantFieldProvider;
    protected final StampProvider stampProvider;
    protected final StructuredGraph graph;
    protected final Bytecode code;
    protected final ResolvedJavaMethod method;
    protected final int invokeBci;
    protected FixedWithNextNode lastInstr;
    protected ValueNode[] arguments;
    protected ValueNode returnValue;
...

ここにValueNodeというクラスがあります。グラフの値を表すオブジェクトです。サブクラスがものすごい数あります。

今回は加算ですから、AddNodeというクラスがあります。org.graalvm.compiler.nodes.calc.AddNodeです。

@NodeInfo(shortName = "+")
public class AddNode extends BinaryArithmeticNode<Add> implements NarrowableArithmeticNode, BinaryCommutative<ValueNode> {
...
    public static ValueNode create(ValueNode x, ValueNode y) {
        BinaryOp<Add> op = ArithmeticOpTable.forStamp(x.stamp()).getAdd();
        Stamp stamp = op.foldStamp(x.stamp(), y.stamp());
        ConstantNode tryConstantFold = tryConstantFold(op, x, y, stamp);
        if (tryConstantFold != null) {
            return tryConstantFold;
        }
        if (x.isConstant() && !y.isConstant()) {
            return canonical(null, op, y, x);
        } else {
            return canonical(null, op, x, y);
        }
    }
...

AddNodeの生成はcreate()メソッドです。ではこのcreate()メソッドは誰が呼び出しているのか?Call Hierarchyします。

    BinaryArithmeticNode.add(StructuredGraph, ValueNode, ValueNode)  (org.graalvm.compiler.nodes.calc)
    BinaryArithmeticNode.add(ValueNode, ValueNode)  (org.graalvm.compiler.nodes.calc)
    BytecodeParser.genFloatAdd(ValueNode, ValueNode)  (org.graalvm.compiler.java)
    BytecodeParser.genIntegerAdd(ValueNode, ValueNode)  (org.graalvm.compiler.java)
    LessThanOp in IntegerLessThanNode.findSynonym(ValueNode, ValueNode)(2 usages)  (org.graalvm.compiler.nodes.calc)
    MulNode.canonical(Stamp, ValueNode, long)(2 usages)  (org.graalvm.compiler.nodes.calc)

BytecodeParser.genIntegerAddがいかにもです。

    protected ValueNode genIntegerAdd(ValueNode x, ValueNode y) {
        return AddNode.create(x, y);
    }

ばっちりです。ではさらにgenIntegerAdd()をCall Hierarchyします。

    BytecodeParser.genArithmeticOp(JavaKind, int)  (org.graalvm.compiler.java)
    BytecodeParser.genIncrement()  (org.graalvm.compiler.java)

genArithmeticOp()です。

    private void genArithmeticOp(JavaKind kind, int opcode) {
        ValueNode y = frameState.pop(kind);
        ValueNode x = frameState.pop(kind);
        ValueNode v;
        switch (opcode) {
            case IADD:
            case LADD:
                v = genIntegerAdd(x, y);
                break;
...
        frameState.push(kind, append(v));

OpCodeがiAddならAddNodeを作る、と。想定通りです。このBytecodeParserクラスがJavaバイトコードをパースして、グラフノードを作っていることがわかりました。

これは抽象解釈というものです。バイトコードインタプリタのようにとらえることもできます。もしこれが実際のJVMインタプリタなら、pop()を2回呼び出している部分はスタックから加算する2つの値をpopする処理で、続いて加算処理をして結果をpushします。このコードはまさにJVMインタプリタのような内容です。実際のコードは計算する値ノードを2つpopして、新しく加算ノードを作り、計算結果を表すスタックにpushしています。