JITの出力するx64アセンブリを深追いしてみた

こないだ行ってきたThe Server Side Java Symposium 2008で、JITの出力するアセンブリコードを見る方法がわかったので、早速試してみました。


まず最初に注意事項。

  • 僕はJVMのパフォーマンスの専門家じゃありません
  • この手の結果を利用してJavaコードをばりばりチューニングするのは賢明ではありません。やめましょう。

要するに、VMは日々進化しているので、この結果が将来のJVMでも有効だとは限りません、ということです。でも、現在でもVMはこの程度の事は既にやっているんだ、という役には立つでしょう。


さて、この機能を利用するには、デバッグ版のJDKが必要です。このテスト走行ではJDK6 u10 b14を使いました。ここからダウンロードできます。

$ java -fullversion
java full version "1.6.0_10-beta-fastdebug-b14"

まずは小手調べ。

public class Main {
    public static void main(String[] args) {
        for(int i=0; i<100; i++)
            foo();
    }

    private static void foo() {
        for(int i=0; i<100; i++)
            bar();
    }

    private static void bar() {
    }
}

これをコンパイルして、次のようにして走らせます。

$ java -XX:+PrintOptoAssembly -server -cp . Main


この-XX:+PrintOptoAssemblyが魔法のオプションです。この例では、fooメソッドがコンパイルされると次の結果が表示されました:

000   B1: #     N1 <- BLOCK HEAD IS JUNK   Freq: 100
000     pushq   rbp
        subq    rsp, #16        # Create frame
        nop     # nop for patch_verified_entry
006     addq    rsp, 16 # Destroy frame
        popq    rbp
        testl   rax, [rip + #offset_to_poll_page]       # Safepoint: poll for GC

011     ret

見てわかるように、bar()の呼び出しとループは死んでいるコードと正しく判定されて、跡形もありません。素晴らしい。


では次はもう少し面白いコードに行ってみましょう。

    private static byte[] foo() {
        byte[] buf = new byte[256];
        for( int i=0; i<buf.length; i++ )
            buf[i] = 0;
        return buf;
    }

これは次のような結果になります。

000   B1: #     B15 B2 <- BLOCK HEAD IS JUNK   Freq: 78
000     # stack bang
        pushq   rbp
        subq    rsp, #80        # Create frame
00c     # TLS is in R15
00c     movq    R8, [R15 + #120 (8-bit)]        # ptr
010     movq    R10, R8 # spill
013     addq    R10, #280       # ptr
01a     cmpq    R10, [R15 + #136 (32-bit)]      # raw ptr
021     jge,u   B15  P=0.000100 C=-1.000000
021
027   B2: #     B3 <- B1  Freq: 77.9922
027     movq    [R15 + #120 (8-bit)], R10       # ptr
02b     PREFETCHNTA [R10 + #256 (32-bit)]       # Prefetch to non-temporal cache for write
033     movq    [R8], 0x0000000000000001        # ptr
03a     PREFETCHNTA [R10 + #320 (32-bit)]       # Prefetch to non-temporal cache for write
042     movq    RDI, R8 # spill
045     addq    RDI, #24        # ptr
049     PREFETCHNTA [R10 + #384 (32-bit)]       # Prefetch to non-temporal cache for write
051     movl    RCX, #32        # long (unsigned 32-bit)
056     movq    R10, precise klass [B: 0x00002aaaab076708:Constant:exact *      # ptr
060     movq    [R8 + #8 (8-bit)], R10  # ptr
064     movl    [R8 + #16 (8-bit)], #256        # int
06c     xorl    rax, rax        # ClearArray:
        rep stosq       # Store rax to *rdi++ while rcx--
071
071   B3: #     B4 <- B16 B2  Freq: 78
071   
071     # checkcastPP of R8
071     xorl    R10, R10        # int
074     movl    R9, #256        # int
        nop     # 2 bytes pad for loops and calls

07c   B4: #     B17 B5 <- B3 B5         Loop: B4-B5 inner stride: not constant pre of N153 Freq: 19850.2
07c     cmpl    R10, #256       # unsigned
083     jge,u   B17  P=0.000001 C=-1.000000
083
089   B5: #     B4 B6 <- B4  Freq: 19850.2
089     movslq  R11, R10        # i2l
08c     movb    [R8 + #24 + R11], #0    # byte
092     incl    R10     # int
095     cmpl    R10, #8
099     jlt,s   B4  P=0.996072 C=22313.000000
099
09b   B6: #     B11 B7 <- B5  Freq: 77.9799
09b     subl    R9, R10 # int
09e     andl    R9, #-16        # int
0a2     addl    R9, R10 # int
0a5     cmpl    R10, R9
0a8     jge,s   B11  P=0.500000 C=-1.000000
0a8
0aa   B7: #     B8 <- B6  Freq: 38.9899
0aa     PXOR  XMM0,XMM0 ! replicate8B
        nop     # 2 bytes pad for loops and calls

0b0   B8: #     B10 B9 <- B7 B9         Loop: B8-B9 inner stride: not constant main of N85 Freq: 9925.09
0b0     movslq  R11, R10        # i2l
0b3     MOVQ  [R8 + #24 + R11],XMM0     ! packed8B
0ba     movl    R11, R10        # spill
0bd     addl    R11, #16        # int
0c1     movslq  R10, R10        # i2l
0c4     MOVQ  [R8 + #32 + R10],XMM0     ! packed8B
0cb     cmpl    R11, R9
0ce     jge,s   B10  P=0.003928 C=22313.000000
0ce
0d0   B9: #     B8 <- B8  Freq: 9886.1
0d0     movl    R10, R11        # spill
0d3     jmp,s   B8
0d3
0d5   B10: #    B11 <- B8  Freq: 38.9899
0d5     movl    R10, R11        # spill
0d5
0d8   B11: #    B14 B12 <- B6 B10  Freq: 77.9799
0d8     cmpl    R10, #256
0df     jge,s   B14  P=0.500000 C=-1.000000
        nop     # 3 bytes pad for loops and calls

0e4   B12: #    B17 B13 <- B11 B13      Loop: B12-B13 inner stride: not constant post of N153 Freq: 9922.54
0e4     cmpl    R10, #256       # unsigned
0eb     jge,us  B17  P=0.000001 C=-1.000000
0eb
0ed   B13: #    B12 B14 <- B12  Freq: 9922.53
0ed     movslq  R11, R10        # i2l
0f0     movb    [R8 + #24 + R11], #0    # byte
0f6     incl    R10     # int
0f9     cmpl    R10, #256
100     jlt,s   B12  P=0.996072 C=22313.000000
100
102   B14: #    N1 <- B13 B11  Freq: 77.9698
102     movq    RAX, R8 # spill
105     addq    rsp, 80 # Destroy frame
        popq    rbp
        testl   rax, [rip + #offset_to_poll_page]       # Safepoint: poll for GC

110     ret
110
111   B15: #    B18 B16 <- B1  Freq: 0.00780129
111     movq    RSI, precise klass [B: 0x00002aaaab076708:Constant:exact *      # ptr
11b     movl    RDX, #256       # int
120     nop     # 3 bytes pad for loops and calls
123     call,static  wrapper for: _new_array_Java
        # Main::foo @ bci:3  L[0]=_ L[1]=_
        # 
128
128   B16: #    B3 <- B15  Freq: 0.00780114
        # Block is sole successor of call
128     movq    R8, RAX # spill
12b     jmp     B3
12b
130   B17: #    N1 <- B12 B4  Freq: 1e-06
130     movl    RSI, #-28       # int
135     movq    RBP, R8 # spill
138     movl    [rsp + #0], R10 # spill
13c     nop     # 3 bytes pad for loops and calls
13f     call,static  wrapper for: uncommon_trap(reason='range_check' action='make_not_entrant')
        # Main::foo @ bci:17  L[0]=RBP L[1]=rsp + #0 STK[0]=RBP STK[1]=rsp + #0 STK[2]=#0
        # AllocatedObj(0x0000000040c31880)

144     int3    # ShouldNotReachHere
144
151   B18: #    N1 <- B15  Freq: 7.80129e-08
151     # exception oop is in rax; no code emitted
151     movq    RSI, RAX        # spill
154     addq    rsp, 80 # Destroy frame
        popq    rbp

159     jmp     rethrow_stub

僕のようにx86アセンブラの知識が止まっている人向けに復習すると、R8-R15はamd64で追加された汎用レジスタです。R0-R7は旧来のAX,BX,...レジスタを64bitに拡張したものです。amd64についてのより詳しい説明はこちらへどうぞ


冒頭の部分(00c-027)が配列をアロケートする部分で、出だしから既にかなり面白いです。 コメントにあるように、R15はスレッド毎のデータ構造へのポインタとして使われているようで、R15[120]は中でもスレッドローカルなヒープの利用可能領域の先頭を指しているようです。つまり、byte[]の割り当ては、単にこのスレッドローカルな領域から256+32バイトを割り振るだけです。Cにありがちな空きメモリリストを辿ったりとかいった処理は一切ありません。かなり高速です。ここでスレッドローカル領域にメモリが足りないと(リミットはR15[136])、ラベルB15から始まる遅いアロケートコードにジャンプします。このコードは、エデン領域から新たなスレッドローカルヒープを割り当てたりするのでしょう。


でもって、06cで配列の先頭がR8にセットされると、その後には初期化コードが続きます(033-071)。オブジェクト毎に24バイトのヘッダブロックがあるようで、最初の8バイトはロックかGCか何かに使われているようで、次の8バイトがクラスへのポインタ、でさらに次の8バイトが配列の長さです。06cが配列をゼロクリアしているのですが、元のJavaコードでは配列の値は後で書き換えるのでこの初期化は必要ないのですが、JITはこの分の最適化には失敗しています。とはいえ、配列の長さが8の倍数なのを活かして、stosbではなくstosqを使って8バイトづつ初期化しているのは見逃せません。


02b, 03aと049にあるプリフェッチは今ひとつよくわかりません。見た感じ、次のオブジェクトアロケーションに備えてヒープの次の領域をキャッシュに持ってくるような感じに見えますが、256,320,384のオフセット値はどういう意味があるんでしょうか?amd64のキャッシュのライン長って何バイトだっけ?


074の時点では、R8は'buf'変数のポインタ、R9が配列の長さになっています。注目するべき部分は、buf.lengthがいつも256だというのを正しくJITは認識しているらしく、R9の初期化はmovl R9,256であってmovl R9,[R8+16]ではありません。賢いですね。R9の初期化がループの外なのも注目です。ということは良くありがちな次のようなコードには全く意味がないということがわかります。

int len = buf.length;
for( int i=0; i<len; i++ )
  ...

同様に、ループの方向を逆にしてi<buf.lengthを節約しようというせせこましい最適化にも、もはや意味がないことがわかります。


このループのコンパイルされ方は非常に興味深いです。まず、ウォームアップとでもいうべき部分(07c-099)があって、想像ではこれは8バイト境界に並ぶまでの部分を担当し、次にメインのループ部分(09b-0d3)が続いて、一気に8バイトづつ0クリアしていきます。これが終わるとクールダウンの部分(0d5-100)で、最後の8バイト境界から残りの部分を埋めると思われます。元のJavaコードでは配列の大きさは必ず8の倍数なのでこのメイン部分以外は不要なのですが、JITはそこには気づいていないようです。同様に、配列はそもそも0に初期化されているのでこのループ全体が無意味なのですが、ループ全体を捨てる、というような最適化はできていません。


とはいえ、このループの展開の仕方はなかなか見事です。1バイト毎の書き込みを8バイトごとにまとめて安全だということには正しく気がついているからです。ループをもう少し複雑にした時にどうなるかは興味深いところです。特にメインループ部分に配列の境界チェックをするコードがないのも秀逸です。







さて、では次に行ってみましょう。JDK6にはロック粗粒度化とロック除去が入っているのは有名な話ですね。では、この働きを見てみましょう。このために、次のコードをコンパイルします。

    private static void foo() {
        Vector v = new Vector();
        v.add("abc");
        v.add("def");
        v.add("ghi");
    }

実行結果は次の様になります。

000   B1: #     B10 B2 <- BLOCK HEAD IS JUNK   Freq: 20168
000     # stack bang
        pushq   rbp
        subq    rsp, #80        # Create frame
00c     # TLS is in R15
00c     movq    RAX, [R15 + #120 (8-bit)]       # ptr
010     movq    R10, RAX        # spill
013     addq    R10, #40        # ptr
017     # TLS is in R15
017     cmpq    R10, [R15 + #136 (32-bit)]      # raw ptr
01e     jge,u   B10  P=0.000100 C=-1.000000
01e
024   B2: #     B3 <- B1  Freq: 20166
024     # TLS is in R15
024     movq    [R15 + #120 (8-bit)], R10       # ptr
028     PREFETCHNTA [R10 + #256 (32-bit)]       # Prefetch to non-temporal cache for write
030     movq    R10, precise klass java/util/Vector: 0x00002aaaf2649f38:Constant:exact *        # ptr
03a     movq    R11, [R10 + #176 (32-bit)]      # ptr
041     movq    [RAX], R11      # ptr
044     movq    [RAX + #8 (8-bit)], R10 # ptr
048     movq    [RAX + #16 (8-bit)], #0 # long
050     movq    [RAX + #24 (8-bit)], #0 # long
058     movq    [RAX + #32 (8-bit)], #0 # long
058
060   B3: #     B12 B4 <- B11 B2  Freq: 20168
060     
060     movq    RBP, RAX        # spill
063     # checkcastPP of RBP
063     # TLS is in R15
063     movq    R11, [R15 + #120 (8-bit)]       # ptr
067     movq    R10, R11        # spill
06a     addq    R10, #104       # ptr
06e     # TLS is in R15
06e     cmpq    R10, [R15 + #136 (32-bit)]      # raw ptr
075     jge,u   B12  P=0.000100 C=-1.000000
075
07b   B4: #     B5 <- B3  Freq: 20166
07b     # TLS is in R15
07b     movq    [R15 + #120 (8-bit)], R10       # ptr
07f     PREFETCHNTA [R10 + #256 (32-bit)]       # Prefetch to non-temporal cache for write
087     movq    [R11], 0x0000000000000001       # ptr
08e     PREFETCHNTA [R10 + #320 (32-bit)]       # Prefetch to non-temporal cache for write
096     movq    RDI, R11        # spill
099     addq    RDI, #24        # ptr
09d     PREFETCHNTA [R10 + #384 (32-bit)]       # Prefetch to non-temporal cache for write
0a5     movq    R10, precise klass [Ljava/lang/Object;: 0x00002aaaf264e928:Constant:exact *     # ptr
0af     movq    [R11 + #8 (8-bit)], R10 # ptr
0b3     movl    [R11 + #16 (8-bit)], #10        # int
0bb     movl    RCX, #10        # long (unsigned 32-bit)
0c0     xorl    rax, rax        # ClearArray:
        rep stosq       # Store rax to *rdi++ while rcx--
0c5
0c5   B5: #     B16 B6 <- B13 B4  Freq: 20168
0c5     
0c5     # checkcastPP of R11
0c5     movq    [RBP + #32 (8-bit)], R11        # ptr ! Field java/util/Vector.elementData
0c9     movq    R10, RBP        # ptr -> long
0cc     shrq    R10, #9
0d0     movq    RDX, java/lang/String:exact *   # ptr
0da     movq    R11, 0x00002a959c9da580 # ptr
0e4     movb    [R11 + R10], #0 # byte
0e9     movq    RSI, RBP        # spill
0ec     nop     # 3 bytes pad for loops and calls
0ef     call,static  java.util.Vector::add
        # Main::foo @ bci:11  L[0]=RBP
        # AllocatedObj(0x0000000040b30680)

0f4
0f4   B6: #     B15 B7 <- B5  Freq: 20167.6
        # Block is sole successor of call
0f4     movq    RDX, java/lang/String:exact *   # ptr
0fe     movq    RSI, RBP        # spill
101     nop     # 2 bytes pad for loops and calls
103     call,static  java.util.Vector::add
        # Main::foo @ bci:18  L[0]=RBP
        # AllocatedObj(0x0000000040b30680)

108
108   B7: #     B14 B8 <- B6  Freq: 20167.2
        # Block is sole successor of call
108     movq    RDX, java/lang/String:exact *   # ptr
112     movq    RSI, RBP        # spill
115     nop     # 2 bytes pad for loops and calls
117     call,static  java.util.Vector::add
        # Main::foo @ bci:25  L[0]=_
        # 
11c
11c   B8: #     N1 <- B7  Freq: 20166.8
        # Block is sole successor of call
11c     addq    rsp, 80 # Destroy frame
        popq    rbp
        testl   rax, [rip + #offset_to_poll_page]       # Safepoint: poll for GC
        
127     ret
(以下略)

Vectorのアロケート部分(00c-058)は前見た配列確保のこーどとそっくりなので、あえて説明の必要はないでしょう。048-058の部分はフィールドを初期化する部分です。続いてはVector.elementDataを確保する似たようなコードが続きます(060-0C0)。


ここで注目すべきはコンストラクタの展開のされ方です。元のソースではこれは次のように定義されているのですが、

    public Vector(int initialCapacity, int capacityIncrement) {
        super();
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        this.elementData = new Object[initialCapacity];
        this.capacityIncrement = capacityIncrement;
    }

    public Vector(int initialCapacity) {
        this(initialCapacity, 0);
    }

    public Vector() {
        this(10);
    }

熱いインライン展開と死んだコードの除去により、最終的にはあたかも元のコードが次のように書かれていたかのようになっています。素晴らしいですね。

    public Vector() {
        this.elementData = new Object[10];
        this.capacityIncrement = 0;
    }

しかし残念ながら素晴らしいのはここまでで、この後に続いているのは単なるVector.addの三連続呼び出しです。ロック関係の最適化は何もおこっていません。なんたる事。Vector.addが複雑過ぎてインライン展開できないのが原因でしょうか?次のコードを試してみましょう。

    private static void foo() {
        Foo foo = new Foo();
        foo.inc();
        foo.inc();
        foo.inc();
    }

    private static final class Foo {
        int i=0;

        public synchronized void inc() {
            i++;
        }
    }

実行結果は次の様になります。

000   B1: #     B6 B2 <- BLOCK HEAD IS JUNK   Freq: 19972
000     # stack bang
        pushq   rbp
        subq    rsp, #80        # Create frame
00c     # TLS is in R15
00c     movq    RBP, [R15 + #120 (8-bit)]       # ptr
010     movq    R10, RBP        # spill
013     addq    R10, #24        # ptr
017     cmpq    R10, [R15 + #136 (32-bit)]      # raw ptr
01e     jge,u   B6  P=0.000100 C=-1.000000
01e
024   B2: #     B3 <- B1  Freq: 19970
024     movq    [R15 + #120 (8-bit)], R10       # ptr
028     PREFETCHNTA [R10 + #256 (32-bit)]       # Prefetch to non-temporal cache for write
030     movq    R10, precise klass Main$Foo: 0x00002aaaf2646e58:Constant:exact *        # ptr
03a     movq    R11, [R10 + #176 (32-bit)]      # ptr
041     movq    [RBP], R11      # ptr
045     movq    [RBP + #8 (8-bit)], R10 # ptr
049     movq    [RBP + #16 (8-bit)], #0 # long
049
051   B3: #     B8 B4 <- B7 B2  Freq: 19972
051     
051     # checkcastPP of RBP
051     leaq    R11, [rsp + #64]        # box lock
056     fastlock RBP,R11,RAX,R10
135     jne     B8  P=0.000001 C=-1.000000
135
13b   B4: #     B9 B5 <- B8 B3  Freq: 19972
13b     MEMBAR-acquire (prior CMPXCHG in FastLock so empty encoding)
13b     movl    R11, [RBP + #16 (8-bit)]        # int ! Field Main$Foo.i
13f     incl    R11     # int
142     movl    [RBP + #16 (8-bit)], R11        # int ! Field Main$Foo.i
146     MEMBAR-release
146     MEMBAR-acquire
146     movl    R11, [RBP + #16 (8-bit)]        # int ! Field Main$Foo.i
14a     incl    R11     # int
14d     movl    [RBP + #16 (8-bit)], R11        # int ! Field Main$Foo.i
151     MEMBAR-release
151     MEMBAR-acquire
151     movl    R11, [RBP + #16 (8-bit)]        # int ! Field Main$Foo.i
155     incl    R11     # int
158     movl    [RBP + #16 (8-bit)], R11        # int ! Field Main$Foo.i
15c     MEMBAR-release (a FastUnlock follows so empty encoding)
15c     leaq    RAX, [rsp + #64]        # box lock
161     fastunlock RBP, RAX, R10
218     jne,s   B9  P=0.000001 C=-1.000000
218
21a   B5: #     N1 <- B9 B4  Freq: 19972
21a     addq    rsp, 80 # Destroy frame
        popq    rbp
        testl   rax, [rip + #offset_to_poll_page]       # Safepoint: poll for GC
        
225     ret
(以下略)

「fastlock」は明らかに疑似命令です。そんなCPU命令は聞いたことがないし、大体一つの命令が223バイトもコード領域を消費するわけがありません。実際にはここに定型のコードが挿入されているものと思われます。しかし、ここでは見事、ロックが粗粒度化されていて、一回のロックで3回のinc()呼び出しが起こっています。やった!!MEMBAR-acquire/releaseは命令長が0なので、メモリバリアを示す疑似命令でしょう。


しかし、ここでも残念ながらロックは依然として残っています。Fooがスタックから逃げ出さないことは明らかなので、ここではロックが完全に除去されるべきところなのですが...。ちょっと調べてみたところ、JDK6はescape analysisをほとんどやってないというブログが見付かりましたが、いっぽう、何か下らない間違いを僕がしている可能性も十分あります。


また、メモリバリアのなごりか、ロックが粗粒度化されたのにも関わらず、数字のインクリメントは一々メモリに書き戻されています。これは残念な事です。実際、ここで「synchronized」キーワードを除去すると次の結果が得られ、

000   B1: #     B4 B2 <- BLOCK HEAD IS JUNK   Freq: 27066
000     # stack bang
        pushq   rbp
        subq    rsp, #16        # Create frame
00c     # TLS is in R15
00c     movq    RAX, [R15 + #120 (8-bit)]       # ptr
010     movq    R10, RAX        # spill
013     addq    R10, #24        # ptr
017     cmpq    R10, [R15 + #136 (32-bit)]      # raw ptr
01e     jge,us  B4  P=0.000100 C=-1.000000
01e
020   B2: #     B3 <- B1  Freq: 27063.3
020     movq    [R15 + #120 (8-bit)], R10       # ptr
024     PREFETCHNTA [R10 + #256 (32-bit)]       # Prefetch to non-temporal cache for write
02c     movq    R10, precise klass Main$Foo: 0x00002aaaf25dfbc8:Constant:exact *        # ptr
036     movq    R11, [R10 + #176 (32-bit)]      # ptr
03d     movq    [RAX], R11      # ptr
040     movq    [RAX + #8 (8-bit)], R10 # ptr
040
044   B3: #     N1 <- B5 B2  Freq: 27066
044     movq    [RAX + #16 (8-bit)], #3 # long
04c     
04c     addq    rsp, 16 # Destroy frame
        popq    rbp
        testl   rax, [rip + #offset_to_poll_page]       # Safepoint: poll for GC
        
057     ret
(以下略)

3回のインクリメントと初期化が全部まとめられて単なるmovq rax[16],3という風に素晴らしく最適化されます。さすが。


随分長くなりましたが、現代のJVMはどうでしょうか?なかなかいい線を行っていると思いますが、どうでしょうか。バイトコードの逐次変換のような単純な処理とは一線を画しているのが見て取れますね。一方、エスケープ解析を利用した高度な最適化はまだ全然行われていないようです。もったいないですね。


たまには皆さんも深追いしてJITと戯れてみましょう。面白いですよ。