JDK 13のShenandoah GC パート1:ロードリファレンスバリア

Red Hat で Solution Architect として OpenJDK を担当している伊藤ちひろ(@chiroito)です。

この記事は、Red Hat Developerのブログ記事、Shenandoah GC in JDK 13, Part 1: Load reference barriers | Red Hat Developer の翻訳記事です。


https://developers.redhat.com/sites/default/files/styles/article_feature/public/blog/2019/05/shenandoah-GC.jpg?itok=PHsRG6Ut

この連載では、JDK 13で登場するShenandoah GCの新しい進歩を紹介します。おそらく最も重要な変更は、ユーザには直接見えないものの、Shenandoahのバリアモデルをロードリファレンスバリアに切り替えたことです。この変更により、Shenandoahに対する批判の主要なポイントの1つである、高価なプリミティブリードバリアが解決されました。ここでは、この変更の意味について詳しく説明します。

Shenandoah(他のコレクターと同様)は、ヒープの一貫性を確保するためにバリアを採用しています。より具体的には、Shenandoah GCは「to-space-invariant」と呼ぶものを保証するためにバリアを採用しています。これは、ShenandoahがGCしているときに、オブジェクトをいわゆる「from-space」から「to-space」にコピーしていることを意味し、Javaスレッドが実行されている間(同時)にそれを行います。

そのため、JVM内には2つのオブジェクトのコピーが存在する可能性があります。ヒープの一貫性を保つためには、次のいずれかを保証する必要があります。

  • 書き込みはto-spaceのコピーに発生し、読み込みは両方のコピーから発生します。メモリモデルの制約を受ける=弱いto-space invariant、または
  • 書き込みと読み込みが常にto-spaceコピーに発生する=強いto-space invariant。

これを保証する方法は、読み取りと書き込みが起こるたびに、対応するタイプのバリアを採用することです。以下の疑似コードを考えてみましょう。

void example(Foo foo) {
  Bar b1 = foo.bar;             // 読み込み
  while (..) {
    Baz baz = b1.baz;           // 読み込み
    b1.x = makeSomeValue(baz);  // 書き込み
}

Shenandoahのバリアを採用すると、次のようになります(JVM+GCが内部で行う)。

void example(Foo foo) {
  Bar b1 = readBarrier(foo).bar;             // 読み込み
  while (..) {
    Baz baz = readBarrier(b1).baz;           // 読み込み
    X value = makeSomeValue(baz);
    writeBarrier(b1).x = readBarrier(value); // 書き込み
}

言い換えれば、オブジェクトから読み取る場合には、まずリードバリアを介してオブジェクトを解決し、オブジェクトに書き込む場合には、おそらくオブジェクトをto-spaceにコピーすることになります。ここでは詳細を説明しませんが、どちらの操作も多少のコストがかかることだけは覚えておいてください。

また、ヒープ参照が更新されている間、to-space参照のみをフィールドに書き込むことを保証するために、ここでの書き込みの値にリードバリアが必要であることにも注意してください(Shenandoahの古いバリアモデルのもう一つの厄介な点)。

バリアはコストのかかるものなので、最適化にはかなり力を入れました。重要な最適化は、バリアをループの外に出すことです。この例では、b1はループの外で定義されていますが、ループの中でのみ使用されています。ループの中で何度もバリアを実行するのではなく、ループの外で一度だけバリアを実行すればいいのです。

void example(Foo foo) {
  Bar b1 = readBarrier(foo).bar;  // 読み込み
  Bar b1' = readBarrier(b1);
  Bar b1'' = writeBarrier(b1);
  while (..) {
    Baz baz = b1'.baz;            // 読み込み
    X value = makeSomeValue(baz);
    b1''.x = readBarrier(value);  // 書き込み
}

そして、ライトバリアはリードバリアよりも強いので、この2つを畳みこむことができるのです。

void example(Foo foo) {
  Bar b1 = readBarrier(foo).bar; // 読み込み
  Bar b1' = writeBarrier(b1);
  while (..) {
    Baz baz = b1'.baz;           // 読み込み
    X value = makeSomeValue(baz);
    b1'.x = readBarrier(value);  // 書き込み
}

これは素晴らしいことで、かなりうまく機能していますが、このための最適化パスが非常に複雑であるという点で、厄介でもあります。あらゆるオブジェクトのfrom-spaceと2つのspaceコピーがいつでもJVMの中を浮遊しているという事実は、頭痛と複雑さの大きな原因です。例えば、あるオブジェクトを別のコピーと比較する場合に備えて、オブジェクトを比較するための余分なバリアが必要です。リードバリアとライトバリアは、非常に頻繁に行われるプリミティブな読み書きを含め、あらゆる読み書きに対して挿入する必要があります。

では、これを最適化して、オブジェクトがメモリからロードされた時点で、to-space-invarianceを強く保証すればいいのではないでしょうか。そこで登場するのが、ロードリファレンスバリアです。ロードリファレンスバリアは、これまでのライトバリアとほぼ同様に機能しますが、使用時点(オブジェクトからの読み込み時やオブジェクトへの保存時)では採用されません。その代わりに、オブジェクトがロードされたときのもっと早い段階(定義時点)で使用されます。

void example(Foo foo) {
  Bar b1' = loadReferenceBarrier(foo.bar);
  while (..) {
    Baz baz = loadReferenceBarrier(b1'.baz); // 読み込み
    X value = makeSomeValue(baz);
    b1'.x = value;                           // 書き込み
}

このコードは基本的には最適化後のものですが、まだ何も最適化する必要がなかったことがわかります。また、格納した値のリードバリアがなくなっています。なぜなら、makeSomeValue()が何をしたとしても、(強いto-space-invariantのおかげで)今は分かっているからです。これは、必要であれば、すでにロードリファレンスバリアを採用しているからです。この新しいロードリファレンスバリアは、以前のライトバリアとほぼ100%同じです。

このバリアモデルの利点はたくさんあります(私たちGC開発者にとって)。

  • 強力なinvariantにより、GCやオブジェクトの状態についての推論が非常に容易になります。
  • ずっとシンプルなバリア・インタフェースです。実際、JDK11以降にGCのバリアインタフェースに追加した多くのものは、今では使われなくなっています:プリミティブ上のバリアは必要ありませんし、オブジェクトのイコールバリアも必要ありません。
  • 最適化が非常に容易になります(上記参照)。バリアは、最もホットな場所(使用時点)ではなく、最もホットでない場所(定義時点)に自然に配置され、そこから離れたところで最適化を試みます(常に成功するとは限りません)。
  • オブジェクト・イコール・バリアが不要になります。
  • "resolve "バリアが不要になりました。(主にintrinsicsで使用される、少々特殊な種類のバリアで、リードのようなライトのような操作を行う場所で使用されます)
  • すべてのバリアが条件付きになったことで、後の最適化の機会が増えました。
  • 高速な JNI ゲッタは、from-spaceの参照をうまく処理できないため、以前は無効にする必要がありましたが、このような最適化を再び有効にできます。

ユーザにとって、この変更はほとんど目に見えませんが、要はShenandoahの全体的なパフォーマンスが向上したということです。また、転送ポインタの廃止など、さらなる改善への道も開かれていますが、これについては次の記事でご紹介します。

ロードリファレンスバリアは、2019年4月にJDK 13開発リポジトリに統合されました。近々、ShenandoahのJDK 11とJDK 8のバックポートにも着手する予定です。待ちたくない方は、すでに手に入れられます:詳細はShenandoah GC Wikiをご覧ください。

もっと読む

JDK 13のShenandoah GC パート2:転送ポインタワードの廃止

JDK 13のShenandoah GC パート3:アーキテクチャとオペレーティングシステム

* 各記事は著者の見解によるものでありその所属組織を代表する公式なものではありません。その内容については非公式見解を含みます。