-
Notifications
You must be signed in to change notification settings - Fork 38
/
Copy pathwbarrier.re
276 lines (226 loc) · 14.6 KB
/
wbarrier.re
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
= ライトバリアのコスト
いよいよ本書も最後の章となりました。
本章では少し視点を変えてHotspotVM内のライトバリアのコストについて調べていきます。
== 実行時のGC切り替え
本書でも説明してきたとおり、HotspotVMは複数のGCアルゴリズムを選択できます。
しかも、起動オプションとしてGCアルゴリズムを指定する方式ですので、Javaプログラム実行時(いわば動的)にGCを切り替えなくてはいけません。
=== 性能劣化の懸念
実行時にGCを切り替える以外にも、たとえばコンパイル時にGCを切り替えるという方法があります。
G1GC用にコンパイルしたOpenJDK、CMS用にコンパイルしたOpenJDK、という風に異なるGCアルゴリズムごとにビルドしたバイナリを配布する方法です。
ですが、この方法だと開発者はGCアルゴリズムを追加するたびにバイナリを増やさないといけません。
管理するバイナリが増えてしまい、よけいな手間がかかってしまうことは容易に想像が付きます。
また、言語利用者の利便性という面からもあまり現実的ではありません。
やはり、実行時にGCを切り替えつつ、お手軽にいろいろ試したいわけですから。
ただ、実行時にGCを切り替える方法はいいことずくめに見えますが、コンパイル時にGCを切り替える方法と比べて性能劣化をまねくという欠点があります。
具体的なサンプルコードとしてC言語で書いたGCを実行する@<code>{gc_start()}という関数を次に示します。
//list[dynamic_gc_options_with_c][例: 実行時GC切り替えのGC起動関数]{
void
gc_start(gc_state state) {
switch (state) {
case gc_state_g1gc;
g1gc_gc_start();
break;
case gc_state_cms;
cms_gc_start();
break;
case gc_state_serial;
serial_gc_start();
break;
}
};
//}
//list[static_gc_options_with_c][例: コンパイル時GC切り替えのGC起動関数]{
void
gc_start(void) {
#ifdef GC_STATE_G1GC
g1gc_gc_start();
#elif GC_STATE_CMS
cms_gc_start();
#elif GC_STATE_SERIAL
serial_gc_start();
#endif
};
//}
@<list>{dynamic_gc_options_with_c}では@<code>{gc_start()}を実行するときに条件分岐の処理が入ってしまいます。
一方、@<list>{static_gc_options_with_c}はコンパイル時に@<code>{gc_start()}内で呼び出す関数が決定するので、実行時に条件分岐の処理が不要です。
=== ライトバリアのコスト増加
実行時にGCを切り替える際、もっとも性能劣化が懸念される場所はライトバリアです。
ライトバリアは頻繁に実行され、ボトルネックになりやすい処理です。
異なるライトバリアが必要なGCを実行時に切り替える際には、ライトバリアも実行時に切り替わることになります。
つまり、@<list>{dynamic_gc_options_with_c}で説明した条件分岐のような切り替えがライトバリア内で必要になってくるわけです。
そのため、ライトバリアに余計にコストがかかってしまい、ミューテータの速度に影響がでてしまいます。
== インタプリタのライトバリア
では、どのようにしてGCごとにライトバリアを切り替えているか、実際に実装を見ていきましょう。
まずは、JITコンパイラを利用していない、素直にJavaバイトコードを実行するインタプリタで実行されるライトバリアについてです。
=== ライトバリアの切り替え
@<code>{oop_store()}という関数によって、オブジェクトフィールドへ参照型の値を格納します。
//source[share/vm/oops/oop.inline.hpp]{
518: template <class T> inline void oop_store(volatile T* p, oop v) {
519: update_barrier_set_pre((T*)p, v);
521: oopDesc::release_encode_store_heap_oop(p, v);
522: update_barrier_set((void*)p, v);
523: }
//}
521行目でフィールド内に値を格納します。
519行目の@<code>{update_barrier_set_pre()}関数がフィールドに値を設定する前のライトバリアで、522行目の@<code>{update_barrier_set()}が設定した後のライトバリアです。
//source[share/vm/oops/oop.inline.hpp]{
499: inline void update_barrier_set(void* p, oop v) {
501: oopDesc::bs()->write_ref_field(p, v);
502: }
503:
504: template <class T> inline void update_barrier_set_pre(T* p, oop v) {
505: oopDesc::bs()->write_ref_field_pre(p, v);
506: }
//}
上記に示した通り、それぞれの関数は@<code>{oopDesc::bs()}で取得したインスタンスに対して、関数を呼び出しているだけです。
この@<code>{oopDesc::bs()}は@<code>{SharedHeap}クラスの@<code>{set_barrier_set()}というメンバ関数で設定されます。
//source[share/vm/memory/sharedHeap.cpp]{
273: void SharedHeap::set_barrier_set(BarrierSet* bs) {
274: _barrier_set = bs;
276: oopDesc::set_bs(bs);
277: }
//}
そして、@<code>{set_barrier_set()}はそれぞれのVMヒープクラスの初期化時に呼び出されます。
G1GCの場合には@<code>{G1SATBCardTableLoggingModRefBS}というクラスのインスタンスが、それ以外の場合は@<code>{CardTableModRefBSForCTRS}というクラスのインスタンスが@<code>{set_barrier_set()}の引数として渡されます。
@<code>{G1SATBCardTableLoggingModRefBS}と@<code>{CardTableModRefBSForCTRS}はどちらとも@<code>{BarrierSet}の子クラスとして定義されたクラスです。
では、@<code>{BarrierSet}の@<code>{write_ref_field_pre()}と@<code>{write_ref_field()}の中身を見てみましょう。
//source[share/vm/memory/barrierSet.inline.hpp]{
35: template <class T> void BarrierSet::write_ref_field_pre(
T* field, oop new_val) {
36: if (kind() == CardTableModRef) {
37: ((CardTableModRefBS*)this)->inline_write_ref_field_pre(field, new_val);
38: } else {
39: write_ref_field_pre_work(field, new_val);
40: }
41: }
42:
43: void BarrierSet::write_ref_field(void* field, oop new_val) {
44: if (kind() == CardTableModRef) {
45: ((CardTableModRefBS*)this)->inline_write_ref_field(field, new_val);
46: } else {
47: write_ref_field_work(field, new_val);
48: }
49: }
//}
それぞれのメンバ関数で分岐しているのがわかります。
36・44行目で@<code>{kind()}が@<code>{CardTableModRef}だった場合、自身は@<code>{CardTableModRefBSForCTRS}のインスタンスと判断し、適切な関数を呼び出します。
それ以外は@<code>{write_ref_field(_pre)_work()}を呼び出します。
//source[share/vm/memory/barrierSet.hpp]{
99: virtual void write_ref_field_pre_work( oop* field, oop new_val) {};
// ..
106: virtual void write_ref_field_work(void* field, oop new_val) = 0;
//}
それぞれ@<code>{BarrierSet}クラスの仮想関数として定義してありますが、今のところ実装しているのは@<code>{G1SATBCardTableLoggingModRefBS}クラスだけです。
つまり、現在のHotspotVMのライトバリアはG1GCとそれ以外のものの2種類を実行時に切り替えて動作しているのです。
=== G1GCが入るまでライトバリアの種類はひとつだけだった
調べて驚いたのですけど、G1GCが入る前(OpenJDK7より前)はライトバリアの実行時切り替えがないんですね。
カードテーブルに書き換えられたことを記録するだけの単純なものだけしか実装されていませんでした(@<code>{CardTableModRefBSForCTRS}のみ)。
よく考えてみたら世代別もインクリメンタルGCもそれだけでいけるんですよね…。
G1GCのライトバリアが特殊すぎるだけだよな、と。
OpenJDK7からはG1GCの導入によってライトバリアの切り替えが発生しますので、インタプリタのオブジェクトへの代入操作は少しだけ性能が劣化するでしょう。
== JITコンパイラのライトバリア
HotspotVMではある程度の呼び出し回数を超えたメソッドはJITコンパイルするという特徴があります。
もしメソッド内でオブジェクトフィールドへの代入があれば、ライトバリアの処理も一緒にマシン語にコンパイルされます。
今まで実行時のライトバリアの切り替えは条件分岐が入ってしまいコストがかかるという話をしてきましたが、JITコンパイラが絡んでくるとこの状況は変わってきます。
=== C1コンパイラ
JITコンパイラには@<b>{C1}、@<b>{C2}、@<b>{Shark}とよばれる3種類のコンパイラがあります。
本書ではそのうちC1を取り上げたいと思います。
C1はクライアントサイドでよく使われるJITコンパイラで、Javaの起動オプションで@<code>{-client}を指定したときに利用されます。
クライアント側で利用するため、コンパイル時間が比較的短く、メモリ使用量も少ないですが、その代わりに最適化はほどほどで抑えられる、という特徴をもちます。
=== ライトバリアのマシン語生成
オブジェクトフィールドへの代入操作をJITコンパイルしている箇所は@<code>{LIRGenerator}クラスの@<code>{do_StoreField()}というメンバ関数です。
//source[share/vm/c1/c1_LIRGenerator.cpp]{
1638: void LIRGenerator::do_StoreField(StoreField* x) {
1708: if (is_oop) {
1710: pre_barrier(LIR_OprFact::address(address),
1711: LIR_OprFact::illegalOpr /* pre_val */,
1712: true /* do_load*/,
1713: needs_patching,
1714: (info ? new CodeEmitInfo(info) : NULL));
1715: }
1716:
1717: if (is_volatile && !needs_patching) {
1718: volatile_field_store(value.result(), address, info);
1719: } else {
1720: LIR_PatchCode patch_code =
needs_patching ? lir_patch_normal : lir_patch_none;
1721: __ store(value.result(), address, info, patch_code);
1722: }
1723:
1724: if (is_oop) {
1726: post_barrier(object.result(), value.result());
1727: }
//}
1717〜1722行目がオブジェクトフィールドへの代入操作のマシン語を生成している部分です。
ライトバリアの生成は@<code>{pre_barrier()}と@<code>{post_barrier()}の中でおこなわれます。
まずは@<code>{pre_barrier()}を見てみましょう。
//source[share/vm/c1/c1_LIRGenerator.cpp]{
1386: void LIRGenerator::pre_barrier(
LIR_Opr addr_opr, LIR_Opr pre_val,
1387: bool do_load, bool patch, CodeEmitInfo* info) {
1389: switch (_bs->kind()) {
1391: case BarrierSet::G1SATBCT:
1392: case BarrierSet::G1SATBCTLogging:
1393: G1SATBCardTableModRef_pre_barrier(
addr_opr, pre_val, do_load, patch, info);
1394: break;
1396: case BarrierSet::CardTableModRef:
1397: case BarrierSet::CardTableExtension:
1398: // No pre barriers
1399: break;
1400: case BarrierSet::ModRef:
1401: case BarrierSet::Other:
1402: // No pre barriers
1403: break;
1404: default :
1405: ShouldNotReachHere();
1406:
1407: }
1408: }
//}
1389行目に登場している@<code>{kind()}というのはインタプリタのライトバリアで説明したのと同じものです。
もしG1GCのものであれば、1393行目のcase文に入りG1GC用のライトバリアをおこなうマシン語を生成します。
それ以外であれば何も生成しません。
1400〜1403行目のcaseは通らない場所なので単純に無視してください。
次に@<code>{post_barrier()}を見てみます。
//source[share/vm/c1/c1_LIRGenerator.cpp]{
1410: void LIRGenerator::post_barrier(
LIR_OprDesc* addr, LIR_OprDesc* new_val) {
1411: switch (_bs->kind()) {
1413: case BarrierSet::G1SATBCT:
1414: case BarrierSet::G1SATBCTLogging:
1415: G1SATBCardTableModRef_post_barrier(addr, new_val);
1416: break;
1418: case BarrierSet::CardTableModRef:
1419: case BarrierSet::CardTableExtension:
1420: CardTableModRef_post_barrier(addr, new_val);
1421: break;
1422: case BarrierSet::ModRef:
1423: case BarrierSet::Other:
1424: // No post barriers
1425: break;
1426: default :
1427: ShouldNotReachHere();
1428: }
1429: }
//}
こちらも同じように@<code>{kind()}の値をみてどのライトバリアを生成するか決定しています。
G1GCであれば1415行目でG1GC用のライトバリアを生成します。
それ以外であれば1420行目でカードテーブルに単純に書き換えを記録するライトバリアを生成します。
1422〜1424行目のcaseは通らない場所なので単純に無視します。
このようにJITコンパイラの時点ではすでに利用するGCアルゴリズムは決定しているので、そのGCにあったライトバリアを生成できます。
そのため、JITコンパイルされたコードではライトバリア切り替えのコストがまったくないのです。
JITさん、カワイイ…。
===[column] コードリーディングの感想
OpenJDKのソースコードを読んできて、何箇所か読みづらい箇所がありました。
たとえばコールバック地獄です。
関数の引数にとったインスタンスの関数を呼び出して、さらにその呼び出した関数の引数にとったインスタンスの関数を呼び出して、さらに…みたいなコードにはうんざりしました。
あと継承地獄。
継承関係が4階層・5階層と平気であります。これはさすがにコード読んでいて迷います。
またクラス分けの粒度が歴史的な背景もあってツギハギになっている箇所がちらほらあり、一貫性がないのもマイナスです。
一貫性があればまだ覚えやすいのですが…。
とはいえ、やはり現在進行形で拡張され続けているソースコードだけあって、抽象化は大変うまくできています。
VMとOS間の抽象化も実用的にできているし、GCも容易に追加できるようになっているし。
慣れ親しんだ開発者にとってはとてもハックしやすい、まさに「おれたちのVM」感があって憎めないな、と感じました。
ブツブツ文句をいいながらも楽しみながら読めたのはやはりOpenJDKを実装してきてくれた開発者のみなさんのおかげです。
こんなに楽しく読めるものをほんとうにありがとうございます(いや皮肉じゃなくて)。