Red Hat で Solution Architect として OpenJDK を担当している伊藤ちひろ(@chiroito)です。
この記事は、Red Hat Developerのブログ記事、Shenandoah GC in JDK 13, Part 2: Eliminating the forward pointer word | Red Hat Developer の翻訳記事です。
この連載では、JDK 13で登場するShenandoah GCの新展開についてご紹介します。パート1では、Shenandoahのバリアモデルがロードリファレンスバリアに変更されたことと、その意味について見てきました。
ここでお話ししたい変更点は、Shenandoah GCに関するもう1つのよくある(おそらく最もよくある)懸念に対処するものです。それは、オブジェクトごとに余分なワードが必要なことです。多くの人は、これがShenandoahのコア要件であると信じていますが、以下で見るように、実際にはそうではありません。
まず、Hotspot JVMでの通常のオブジェクトレイアウトを見てみましょう。
0: | [mark-word ] |
8: | [class-word ] |
16: | [field 1 ] |
24: | [field 2 ] |
32: | [field 3 ] |
ここでの各セクションは、ヒープのワードを示しています。それは、64ビットアーキテクチャでは64ビット、32ビットアーキテクチャでは32ビットになります。
最初のワードは、いわゆるマークワード、つまりオブジェクトのヘッダです。これは様々な目的で使用されます。例えば、オブジェクトのハッシュコードを保持できます。3つのビットがあり、様々なロック状態に使用されます。GCの中には、オブジェクトの年齢やマーキングの状態を追跡するために使用するものもあります。また、"displaced "マークへのポインタ、"inflated "ロックへのポインタ、あるいはGC時には転送ポインタを "オーバーレイ "できます。
2番目のワードはklassポインタ用に予約されています。これは単に、オブジェクトのクラスを表すHotspot内部のデータ構造へのポインタです。
配列の場合は、配列長を格納するために次のワードが追加されます。その後に続くのは、オブジェクトの実際のペイロード、つまりフィールドと配列の要素です。
Shenandoahを有効にして実行した場合、レイアウトは代わりに以下のようになります。
-8: | [fwd pointer] |
0: | [mark-word ] |
8: | [class-word ] |
16: | [field 1 ] |
24: | [field 2 ] |
32: | [field 3 ] |
フォワードポインタは、Shenandoahの同時退避プロトコルに使用されます。
- 通常、それは自分自身を指しています -> そのオブジェクトはまだ退避していません。
- (GCまたはライトバリアを介して)退避するときは、まずオブジェクトをコピーし、そのコピーにアトミックな比較とスワップを使用して新しいフォワードポインタをインストールします。その際、問題のあるコピーへのポインタを渡す可能性があります。勝つのは1つのコピーだけです。
- これで、この転送ポインタを読むだけで、読み書きする正規のコピーを簡単に見つけられます。
このプロトコルの利点は、単純で安価であることです。安価であるという点がここでは重要です。というのも、Shenandoahはプリミティブなものも含めて、すべての読み書きに対してフォワーダを解決する必要があるからです。そして、このプロトコルを使用すると、このための読み取りバリアは1つの命令になります。
mov %rax, (%rax, -8)
それくらいシンプルなものです。
デメリットとしては、当然ながらメモリが多く必要になります。最悪の場合、ペイロードを持たないオブジェクトの場合、2ワードのオブジェクトのために1ワード増えることになります。つまり、50%増です。より現実的なオブジェクトサイズの分布であれば、5%~10%のオーバーヘッドが増えることになりますが、効果は人によって異なります。。これはパフォーマンスの低下にもつながります。これは、同じ数のオブジェクトを割り当てると、オーバーヘッドがない場合よりも早く上限に達し、GCがより頻繁に促されるため、スループットが低下するということです。
ここまで注意深く読んでくださった方は、マークワードがいくつかのGCで転送ポインタを運ぶためにも使われている/オーバーレイされることにお気づきでしょう。では、なぜShenandoahでも同じようにしないのでしょうか?その答えは(かつては)、転送ポインタを読み取るにはもう少し作業が必要だったからです。真のマークワードと転送ポインタをどうにかして区別する必要があります。そのためには、マークワードの最下位2ビットを設定しました。これらのビットは通常、ロックビットとして使用されますが、0b11という組み合わせは、ロックビットの合法的な組み合わせではありません。つまり、これらのビットが設定されると、最下位ビットが0にマスクされたマークワードは、転送ポインタとして解釈されることになります。このマークワードのデコードは、上記の単純な転送ポインタの読み取りよりもかなり複雑です。私は実際に少し前にプロトタイプを作ったのですが、リードバリアの追加コストは法外なものであり、節約分を正当化するものではありませんでした。
そんな状況を一変させたのが、最近登場したロードリファレンスバリアです:
- 特に(非常に頻繁な)プリミティブリードでは、リードバリアを必要としなくなりました。
- ロードリファレンスバリアは条件付きです。これは、GCがアクティブで、問題のオブジェクトがコレクションセットにある場合にのみ、スローパス(実際の解決)が有効になることを意味します。これはかなり稀なことです。これを以前のリードバリアと比較してみましょう。これは常にオンになっています。
- from-spaceコピーへのアクセスはもはや許されません。強力な不変性により、我々は常にto-spaceコピーからの読み取りとto-spaceコピーへの書き込みのみを行うことが保証されています。
2つの結果は以下の通りです。from-spaceコピーは実際には何にも使われていないので、転送ポインタのために余分なワードを確保する代わりに、そのスペースを使えます。基本的に、from-spaceコピーの内容全体を消去して、転送ポインタをどこにでも置けます。「転送されていない」(他の内容は気にしない)と「転送されている」(残りは転送ポインタ)を区別することができればよいのです。
また、ロードリファレンスバリアの実際の中間のパスとスローパスはそれほどホットではないので、そこに少しのデコードをする余裕があるということです。それは次のようなものになります(擬似コードで)。
oop decode_forwarding(oop obj) { mark m = obj->load_mark(); if ((m & 0b11) == 0b11) { return (oop) (m & ~0b11); } else { return obj; } }
これは、転送ポインタの単純なロードよりも明らかに複雑に見えますが、それでも基本的には無料でもらえるモノです。なぜなら、これはロードリファレンスバリアのあまりホットではない中間のパスでのみ実行されるからです。これにより、新しいオブジェクトのレイアウトは次のようになります。
0: | [mark word (or fwd pointer)] |
8: | [class word] |
16: | [field 1] |
24: | [field 2] |
32: | [field 3] |
この方法にはいくつかの利点があります。
- 明らかに、余分なワードを排除することで、Shenandoahのメモリフットプリントを削減します。
- それほど明らかではありませんが、結果的にスループットが向上します。GCトリガーを起動する前に、より多くのオブジェクトを割り当てることができるようになりました。これにより、実際のGCに費やされるサイクル数が減少します。
- オブジェクトはよりキツく詰め込まれます。これにより、CPUのキャッシュ圧力が改善されます。
- ここでも、必要なGCインタフェースがよりシンプルになりました。これまでは,(余分なワードを確保して初期化するために)アロケーションパスの特別な実装が必要でした。他のGCと同じアロケーションコードを使用できるようになりました。
スループットの向上の一例として、私が試したGCに敏感なベンチマークでは、すべて10%から15%の向上が見られました。他のベンチマークでは、それ以下か、まったく効果がありませんでした。しかし、これはGCをまったく行わないベンチマークでは驚くべきことではありません。しかし、余分なデコードコストは実際にはどこにも現れないことに注意する必要があります。基本的には無視できるレベルです。ただし、退避が激しいワークロードでは、おそらく表示されるでしょう。しかし、ほとんどのアプリケーションはそれほど退避しませんし、ほとんどの作業はGCスレッドで行われます。これにより、中間のパスデコーディングが十分に安価になっています。
この実装は、Shenandoah/JDKのリポジトリに最近プッシュされました。現在、最後の既知のバグを排除しており、その後、JDK 13 リポジトリにアップストリームする準備が整います。計画では、最終的にShenandoahのJDK 11およびJDK 8バックポートリポジトリにバックポートし、そこからRPMに移行する予定です。もし待ちたくないのであれば、すでに手に入れることができます:the Shenandoah GC Wikiをチェックしてください。