前回はGraalのグラフを見ました。
実際にどのようにグラフを作るかという謎はありますが、視点を変えて実際に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しています。