Fight the Future

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

TLABとBump the Pointer

JJUG CCC 2018 Fallのセッション、数村さんのGCを発生させないJVMとコーディングスタイルにて、TLABとBump the Pointerについての説明がありました。

https://image.slidesharecdn.com/escape-analysis-cccfall2018-181216035628/95/gcjvm-15-638.jpg?cb=1544932702https://www.slideshare.net/kenjikazumura/gcjvm より抜粋)

TLAB、理屈は知っているけれど、よく考えると何も見たことがないなと思いました。そして、Red Hat社のAlekseyさんの個人サイトに、TLABの話があったなあ、と思い出しました。

JVM Anatomy Park #4: TLAB allocation

なぞりながら、実行してみると同時に、GCを発生させないJVMとコーディングスタイルのセッション内容の理解を深めることにします。

前提

TLABが何をするものかは、知っている前提です。

newする部分をPrintAssemblyする

TLABへのオブジェクトのアロケーションを見ます。単にObjectをnewするコードを書きました。

public class Tlab {
    public static void main(String[] args) throws Exception {
        Tlab t = new Tlab();
        System.out.println(t.test());
        System.out.println(t.test());
    }

    public Object test() throws Exception {
        Object o = new Object();
        return o;
    }
}

Alekseyさんの説明にあるように、JITコンパイルさせて、PrintAssemblyを出力したいので、newするコードはメソッドにし、コードを削除されないようにインスタンスはreturnしてprintlnします。

PrintAssemblyするには、HSDISが必要です。ビルドして配置します。Ubuntuならsudo apt-get install libhsdis0-fcmlでも入りました。

www.sakatakoichi.com

実行します。-Xcompをつけて、testメソッドの最初の呼び出しで、強制的にJITコンパイルさせます。

javac Tlab.java
java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:CompileCommand=quiet -XX:CompileOnly=Tlab::test -Xcomp Tlab

アセンブリが出力されます。HSDISは、デフォルトではAT&Tのシンタックスとなります。

TLABのバッファサイズと、使用領域のtopを指すポインタの位置を比較している部分を探します。Alekseyさんの記事のものと異なり、prefetchntaではなくprefetchwでした。

  0x00007f38d4ad016c: mov    0x138(%r15),%rax
  0x00007f38d4ad0173: mov    %rax,%r10
  0x00007f38d4ad0176: add    $0x10,%r10
  0x00007f38d4ad017a: cmp    0x148(%r15),%r10
  0x00007f38d4ad0181: jae    0x00007f38d4ad01cb
 ;; B2: #   B3 <- B1  Freq: 0.9999

  0x00007f38d4ad0183: mov    %r10,0x138(%r15)
  0x00007f38d4ad018a: prefetchw 0x100(%r10)
  0x00007f38d4ad0192: mov    $0x20000208,%r11d  ;   {metadata('java/lang/Object')}
  0x00007f38d4ad0198: movabs $0x0,%r10
  0x00007f38d4ad01a2: lea    (%r10,%r11,8),%r10
  0x00007f38d4ad01a6: mov    0xb8(%r10),%r10
  0x00007f38d4ad01ad: mov    %r10,(%rax)
  0x00007f38d4ad01b0: movl   $0x20000208,0x8(%rax)  ;   {metadata('java/lang/Object')}
  0x00007f38d4ad01b7: mov    %r12d,0xc(%rax)    ;*synchronization entry
                                                ; - Tlab::test@-1 (line 9)

数村さんのセッションで、HotSpotでTLABが使うレジスタはr15と決まっている、という説明がありました。この出力で、自分でも確認ができました。もしバッファサイズを超えた場合に戻れるように、raxからr10にコピーして、以降r10を使って、オブジェクトがアロケートできるかテストしています。

add $0x10,%r10で、$0x10=16、つまりnew Object()したことで、オブジェクトのサイズである16を、現在のtopに加算しています。

cmp 0x148(%r15),%r10で、進めたtopの位置と、TLABのendの位置を比較しています。もし、topがTLABのendを超えれば、jaeでジャンプします。

OKであれば、mov %r10,0x138(%r15)で、topの位置を、0x138(%r15)のメモリ上に保存します。最初の行でそこから値を取り出していますので、上書きした、ということです。

mov %r10,(%rax)で、r10でのテストがOKだったので、r10からをraxに書き込んでいます。

movl $0x20000208,0x8(%rax)では、{metadata('java/lang/Object')}とあるように、いわゆるklassへの参照を書き込んでいます。

これもまた、わかったつもり…

アセンブリを読んで、TLABとBump the Pointerについて、理解が深まった気でいました。ここで、さらなるアドバイスをもらうことができました。

たしかに、cmp 0x148(%r15),%r10で、0x148(%r15)がTLABのendであるというのは、説明を読んでそう考えているだけで、実際にendかどうかは確認できていないのです。

ブレークポイントとGDBを使って検証する

r15など、レジスタの値を見るのは、GDBを使うのがよさそうと考えました。CLIでのGDBも試しましたが、CLionでのデバッグ環境を整えたので、CLionを使います。

www.sakatakoichi.com

src/hotspot/share/gc/shared/threadLocalAllocBuffer.inline.hppにブレークポイントを打ちました。ThreadLocalAllocBuffer::allocate(size_t size)関数です。この関数が、TLABにオブジェクトをアロケートしています。

inline HeapWord* ThreadLocalAllocBuffer::allocate(size_t size) {
  invariants();
  HeapWord* obj = top();
  if (pointer_delta(end(), obj) >= size) {
    // successful thread-local allocation
#ifdef ASSERT
    // Skip mangling the space corresponding to the object header to
    // ensure that the returned space is not considered parsable by
    // any concurrent GC thread.
    size_t hdr_size = oopDesc::header_size();
    Copy::fill_to_words(obj + hdr_size, size - hdr_size, badHeapWordVal);
#endif // ASSERT
    // This addition is safe because we know that top is
    // at least size below end, so the add can't wrap.
    set_top(obj + size);

    invariants();
    return obj;
  }
  return NULL;
}

CLionで、同様に-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:CompileCommand=quiet -XX:CompileOnly=Tlab::test -Xcomp Tlabを実行します。SIGSEGVでブレークしますが、何度がresumeすると、allocate関数で止まります。

f:id:jyukutyo:20190108182100p:plain

ThreadLocalAllocBufferでのtopが0x83600000、endが0x836ac058とわかりました。0x148(%r15)の値が0x836ac058であれば、cmp 0x148(%r15),%r10がendとポインタの位置の比較であったと確信が持てます。

CLionは、ブレークポイントで止まると、自動的にGDBもアタッチしてくれます。便利です。ただ、GUIですし、マシンリソースはそれなりに使います。

GDBでメモリの値を見ます。

(gdb) info register r15
r15            0x7ffba401b000      140718765092864

r15の値は0x7ffba401b000でした。0x148(%r15)ですので、この値に0x148を加算すると、0x7ffba401b148です。このメモリアドレスの値を見ます。

(gdb) x 0x7FFBA401B148
0x7ffba401b148: 0xffffffff836ac058

0xffffffff836ac058!つまり、0x836ac058です!endは0x836ac058でしたので、0x148(%r15)にはendの位置が格納されていると、はっきりしました!

まとめ

TLABへのアロケーションを調べました。実際に、TLABへアロケートしている関数、アセンブリだけでなく、実行時の値を見て動作を確認できました。

JVM部分はとくに動かして学ぶということがしづらく、手を抜いて文献を読んだだけで、理解したつもりになってしまっていることがあります。広く浅くと、狭く深くを使い分けつつ、T型、逆クラゲ型にJVMの開発能力を作っていきたいです。