Fight the Future

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

Graalのノードからマシンコードの生成

前回はグラフノードの生成を見ました。

jyukutyo.hatenablog.com

この記事に沿ってGraalの理解を深めています。

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

最終的には、グラフにある各ノードに対してマシンコードを生成します(グラフ自体を簡素化するアプローチは別途)。マシンコードが関連してくるので、メソッドの処理を加算ではなくインクリメントにします。

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

    private int workload(int a) {
        return a + 1;
    }
}

マシンコードは、org.graalvm.compiler.asm.Assemblerクラスのサブクラスで生成します。

/**
 * The platform-independent base class for the assembler.
 */
public abstract class Assembler {

マシンコードですので、各CPUごとにサブクラスがあります。

Assembler (org.graalvm.compiler.asm)
    AArch64Assembler (org.graalvm.compiler.asm.aarch64)
        AArch64MacroAssembler (org.graalvm.compiler.asm.aarch64)
        TestProtectedAssembler (org.graalvm.compiler.asm.aarch64.test)
    AMD64Assembler (org.graalvm.compiler.asm.amd64)
        AMD64MacroAssembler (org.graalvm.compiler.asm.amd64)
    SPARCAssembler (org.graalvm.compiler.asm.sparc)
        SPARCMacroAssembler (org.graalvm.compiler.asm.sparc)

Intel 64ビットCPUですので、AMD64Assemblerです。CPUのinstructionと対応するので、INC命令と対応するメソッドがあります。

    protected final void incl(Register dst) {
        // Use two-byte form (one-byte from is a REX prefix in 64-bit mode)
        int encode = prefixAndEncode(dst.encoding);
        emitByte(0xFF);
        emitByte(0xC0 | encode);
    }

このコードの理解を深めるため、"Intel® 64 and IA-32 Architectures Software Developer’s Manual"を見ます。Instruction Set Referenceですので、INC命令の記載があります。

f:id:jyukutyo:20171128145543p:plain

https://www.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-software-developer-instruction-set-reference-manual-325383.pdf

64ビットの場合REX.W + FF /0のようです。Javaコードのコメントにある"Use two-byte form (one-byte from is a REX prefix in 64-bit mode)"と合っています。emitByte()メソッドに0xFFを渡しているのも、INC命令のオペコードがFFだからですね。そのあとの0xC0 | encodeが何を表すか、僕にはわかっていません。

emitByte()は最終的にorg.graalvm.compiler.asm.Buffer#emitByte()を実行します。

    public void emitByte(int b) {
        assert NumUtil.isUByte(b) || NumUtil.isByte(b);
        ensureSize(data.position() + 1);
        data.put((byte) (b & 0xFF));
    }

このbはincl()メソッドで渡した0xFFなんですが、どうしてここで別の0xFFとビット演算の論理積を取るんでしょうね??

datajava.nio.ByteBufferオブジェクトです。ByteBuffer#array()でバイト配列になります。

さて、生成されたマシンコードを出力してみましょう。org.graalvm.compiler.hotspot.HotSpotGraalCompilerクラスに戻ります。

    public CompilationResult compileHelper(CompilationResultBuilderFactory crbf, CompilationResult result, StructuredGraph graph, ResolvedJavaMethod method, int entryBCI, boolean useProfilingInfo,
                    OptionValues options) {
...
        System.err.println(method.getName() + " machine code: "
          + Arrays.toString(result.getTargetCode()));
        return result;
    }
$ 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=Demo2::workload \
-XX:CompileCommand=quiet \
Demo2
...
workload bytecode: [27, 4, 96, -84]
...
workload machine code: [68, -117, 86, 8, 73, -63, -30, 3, 73, 59, -62, 15, -123, -17, -1, -1, -1, -112, 15, 31, -128, 0, 0, 0, 0, 15, 31, -128, 0, 0, 0, 0, 15, 31, 68, 0, 0, -1, -62, -117, -62, -123, 5, 0, 0, 0, 0, -59, -8, 119, -61, -24, 0, 0, 0, 0, -112, -24, 0, 0, 0, 0, -112, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

マシンコードが出ましたが、何もわかりません!disassembleしましょう。そのためにはhsdis(HotSpot disAssembler)が必要です。これはGraal関連のことではなく、HotSpot標準です。ビルド方法はこちらを参照ください。最新のbinutilsではうまくビルドできなかったので、2.27を利用しました。

jyukutyo.hatenablog.com

ビルドするとbuild/macosx-amd64ディレクトリにhsdis-amd64.dylibがあるので、これを適切なディレクトリに置きます。JDK 9なら$JAVA_HOME/lib/です。disassembleした結果を出力するために2つオプションを追加します。-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssemblyです。

$ 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=Demo2::workload \
-XX:CompileCommand=quiet \
-XX:+UnlockDiagnosticVMOptions \
-XX:+PrintAssembly \
Demo2
Java HotSpot(TM) 64-Bit Server VM warning: PrintAssembly is enabled; turning on DebugNonSafepoints to gain additional output
Loaded disassembler from /Library/Java/JavaVirtualMachines/jdk-9.jdk/Contents/Home/lib/hsdis-amd64.dylib

かなり長い出力が出ますが、最後にこのメソッドのdisassembleした出力があります。

----------------------------------------------------------------------
Demo2.workload(I)I (Demo2.workload(int))  [0x000000010b997b20, 0x000000010b997b60]  64 bytes
[Entry Point]
[Constants]
  # {method} {0x000000012709faf8} 'workload' '(I)I' in 'Demo2'
  # this:     rsi:rsi   = 'Demo2'
  # parm0:    rdx       = int
  #           [sp+0x10]  (sp of caller)
  0x000000010b997b20: mov    0x8(%rsi),%r10d
  0x000000010b997b24: shl    $0x3,%r10
  0x000000010b997b28: cmp    %r10,%rax
  0x000000010b997b2b: jne    0x000000010b92f000  ;   {runtime_call ic_miss_stub}
  0x000000010b997b31: nop
  0x000000010b997b32: nopl   0x0(%rax)
  0x000000010b997b39: nopl   0x0(%rax)
[Verified Entry Point]
  0x000000010b997b40: nopl   0x0(%rax,%rax,1)
  0x000000010b997b45: inc    %edx               ;*iadd {reexecute=0 rethrow=0 return_oop=0}
                                                ; - Demo2::workload@2 (line 9)

  0x000000010b997b47: mov    %edx,%eax          ;*ireturn {reexecute=0 rethrow=0 return_oop=0}
                                                ; - Demo2::workload@3 (line 9)

  0x000000010b997b49: test   %eax,-0x15a2b49(%rip)        # 0x000000010a3f5006
                                                ;   {poll_return}
  0x000000010b997b4f: vzeroupper
  0x000000010b997b52: retq
[Exception Handler]
  0x000000010b997b53: callq  0x000000010b997480  ;   {runtime_call Stub<ExceptionHandlerStub.exceptionHandler>}
  0x000000010b997b58: nop
[Deopt Handler Code]
  0x000000010b997b59: callq  0x000000010b92ff20  ;   {runtime_call DeoptimizationBlob}
  0x000000010b997b5e: nop
[Stub Code]
  0x000000010b997b5f: hlt

0x000000010b997b45: inc %edx ;*iadd {reexecute=0 rethrow=0 return_oop=0} ; - Demo2::workload@2 (line 9)で、Javaバイトコードiaddが、CPUのINC命令となっていることが読み取れます。

まとめ

インクリメントするコードは、[27, 4, 96, -84] -> [68, -117, 86, 8, 73, -63, -30, 3, 73, 59, -62, 15, -123, ...]とJITコンパイルでマシンコードに変換されることがわかりました。