[インデックス 15835] ファイルの概要
このコミットは、Goランタイムのガベージコレクタ(GC)におけるルートオブジェクトの追加(addroot
)に関する最適化を目的としています。具体的には、スタック上のルートと、MSpan
構造体内の型情報(MSpan.types.data
)の扱いが変更されています。これにより、GCの効率が向上し、不要なルートの走査が削減されます。
コミット
commit ab08eac5b78c05672a1bcdc5012f9f9384f86fba
Author: Jan Ziak <0xe2.0x9a.0x9b@gmail.com>
Date: Tue Mar 19 19:57:15 2013 +0100
runtime: optimize calls to addroot()
R=golang-dev, rsc
CC=dvyukov, golang-dev
https://golang.org/cl/7879043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/ab08eac5b78c05672a1bcdc5012f9f9384f86fba
元コミット内容
このコミットの元の内容は、Goランタイムのガベージコレクタ(GC)がルートオブジェクトを登録する際のaddroot()
関数の呼び出しを最適化することです。具体的には、スタック上のルートと、メモリ管理に使用されるMSpan
構造体内の型情報に関する処理が変更されています。
変更の背景
Goのガベージコレクタは、プログラムが使用しているメモリを効率的に管理するために、到達可能なオブジェクトを特定し、到達不可能なオブジェクトを解放します。このプロセスにおいて、「ルート」と呼ばれるオブジェクトはGCが走査を開始する起点となります。ルートには、グローバル変数、実行中の関数のスタック上の変数、レジスタ内の値などが含まれます。
このコミットの背景には、GCのパフォーマンス向上と正確性の確保があります。特に、以下の点が考慮されたと考えられます。
- スタック走査の精度向上: スタックは一時的な変数が格納される領域であり、GCが正確にポインタを識別し、到達可能なオブジェクトをマークすることが重要です。以前の実装では、スタック上のルートの扱いがより汎用的であった可能性がありますが、特定のフラグ(
PRECISE | LOOP
)を導入することで、より精密な走査とマークが可能になります。 - 型情報の重複排除と効率化: Goのランタイムは、メモリ上のオブジェクトの型情報を管理しています。
MSpan
構造体は、特定のメモリ領域(スパン)がどのような型のオブジェクトを保持しているかを示す情報を含んでいます。以前は、この型情報自体もGCのルートとして登録されていた可能性があります。しかし、コンパイラによって生成される型情報はヒープ外に存在すること、およびreflect
パッケージによってランタイムで生成される型情報はキャッシュされており、GCがそのキャッシュを介して参照を見つけられることが判明したため、MSpan.types.data
を直接ルートとして追加する必要がなくなりました。これにより、GCのルートセットが小さくなり、走査のオーバーヘッドが削減されます。
これらの変更は、GCのサイクルタイムを短縮し、全体的なアプリケーションのパフォーマンスを向上させることを目的としています。
前提知識の解説
このコミットを理解するためには、以下のGoランタイムとガベージコレクタに関する基本的な知識が必要です。
- ガベージコレクタ (GC):
- GoのGCは、並行マーク&スイープ方式を採用しています。これは、プログラムの実行と並行してGCが動作し、アプリケーションの一時停止(ストップ・ザ・ワールド)時間を最小限に抑えることを目指すものです。
- マークフェーズ: GCは、プログラムから直接的または間接的に到達可能なすべてのオブジェクトを「マーク」します。このマークを開始する起点が「ルート」です。
- スイープフェーズ: マークされなかった(到達不可能な)オブジェクトが占めるメモリ領域を解放し、再利用可能にします。
- ルート (GC Roots):
- GCが到達可能性の走査を開始するオブジェクトの集合です。これには、グローバル変数、実行中のゴルーチンのスタック上の変数、CPUレジスタ内の値などが含まれます。
- GCは、これらのルートからポインタをたどって、到達可能なすべてのオブジェクトをマークしていきます。
runtime
パッケージ:- Goのランタイムシステムを実装しているパッケージです。ガベージコレクタ、スケジューラ、メモリ管理などが含まれます。
src/pkg/runtime/mgc0.c
は、GoのGCの初期バージョン(または主要な部分)をC言語で実装したファイルです。Goランタイムの一部はC言語で書かれています。
MSpan
構造体:- Goのメモリ管理において、ヒープメモリは「スパン(span)」と呼ばれる連続したメモリブロックに分割されます。
MSpan
構造体は、個々のスパンに関するメタデータ(例えば、スパンが保持するオブジェクトのサイズ、状態、型情報など)を管理します。 MSpan.types
は、そのスパンに格納されているオブジェクトの型情報に関するデータを含んでいます。これは、GCがオブジェクトのサイズや内部のポインタを正確に識別するために必要です。
- Goのメモリ管理において、ヒープメモリは「スパン(span)」と呼ばれる連続したメモリブロックに分割されます。
addroot()
関数:- GCのルートセットにオブジェクトを追加するための関数です。GCは、このルートセットから走査を開始します。
markonly()
関数:- このコミットで導入または変更された関数で、特定のオブジェクトをマークするが、必ずしもGCのルートとして追加するわけではない、より軽量なマーキング操作である可能性があります。これは、オブジェクトが既に別の方法で到達可能であることが分かっている場合や、特定の目的のためにのみマークが必要な場合に使用されます。
Obj
構造体:- GCが扱うオブジェクトの抽象的な表現で、ポインタ、サイズ、そして型やGCの走査方法に関するフラグなどを含む可能性があります。
技術的詳細
このコミットは、Goランタイムのガベージコレクタの内部動作に深く関わる最適化です。
-
addstackroots
関数における変更:addstackroots
関数は、実行中のゴルーチンのスタックを走査し、スタック上に存在するポインタ(ヒープ上のオブジェクトへの参照)をGCのルートとして登録する役割を担っています。- 変更前:
addroot((Obj){sp, (byte*)stk - sp, 0});
- ここで
0
が渡されているのは、おそらく汎用的なフラグまたはデフォルトのGC走査モードを示していました。
- ここで
- 変更後:
addroot((Obj){sp, (byte*)stk - sp, (uintptr)defaultProg | PRECISE | LOOP});
defaultProg
: これは、GCがスタックを走査する際に使用するデフォルトの「GCプログラム」または「走査ロジック」を指す可能性があります。GoのGCは、オブジェクトの型情報に基づいてポインタを識別し、走査のパスを決定します。PRECISE
: このフラグは、スタック走査が「正確(precise)」であることを示唆しています。正確なGCは、ポインタと非ポインタデータを明確に区別し、誤って非ポインタデータをポインタとして解釈することを防ぎます。これにより、GCの安全性と効率が向上します。LOOP
: このフラグは、スタック走査がループ処理を伴うことを示唆している可能性があります。例えば、スタックフレーム内の複数のポインタを繰り返し走査するような動作です。- これらのフラグの組み合わせにより、スタック上のポインタの識別とマークがより精密かつ効率的に行われるようになります。
-
addroots
関数における変更とコメントの追加:addroots
関数は、GCの初期マークフェーズで、グローバル変数やその他の主要なルートを登録する役割を担っています。- 変更前は、
MSpan.types.data
(スパン内のオブジェクトの型情報)もGCのルートとしてaddroot
されていました。これにはTODO
コメントで「defaultProg
を使うことを検討する」と書かれていました。 - 変更後:
addroot((Obj){(byte*)&s->types.data, sizeof(void*), 0});
がmarkonly((byte*)s->types.data);
に置き換えられました。markonly()
は、addroot()
とは異なり、オブジェクトをGCルートとして追加するのではなく、単にマークするだけの関数であると推測されます。これは、s->types.data
がGCの走査パスにおいて、他のルートから既に到達可能であるか、あるいはGCが特別に扱うべきデータであることを示唆しています。
- 追加されたコメントは、この変更の理由を明確に説明しています。
// The garbage collector ignores type pointers stored in MSpan.types:
- GCが
MSpan.types
に格納されている型ポインタを「無視する」というよりは、直接ルートとして扱わないという意味合いが強いでしょう。
- GCが
// - Compiler-generated types are stored outside of heap.
- コンパイラによって生成される型記述子(
_type
構造体など)は、通常、プログラムのデータセクションに配置され、ヒープ上にはありません。GCはヒープ上のオブジェクトを対象とするため、ヒープ外のデータは直接GCの対象とはなりません。
- コンパイラによって生成される型記述子(
// - The reflect package has runtime-generated types cached in its data structures.
// The garbage collector relies on finding the references via that cache.
reflect
パッケージは、ランタイムで型情報を動的に生成・キャッシュします。GCは、このreflect
パッケージのキャッシュ構造を走査することで、ランタイムで生成された型情報への参照を見つけることができます。したがって、MSpan.types.data
を個別にルートとして追加する必要がない、ということです。
これらの変更は、GCが不要なルートを走査するのを避け、より効率的かつ正確にメモリを管理するための洗練されたアプローチを示しています。
コアとなるコードの変更箇所
変更はsrc/pkg/runtime/mgc0.c
ファイルに集中しています。
--- a/src/pkg/runtime/mgc0.c
+++ b/src/pkg/runtime/mgc0.c
@@ -1357,7 +1357,7 @@ addstackroots(G *gp)
runtime·printf("scanstack inconsistent: g%D#%d sp=%p not in [%p,%p]\\n", gp->goid, n, sp, guard-StackGuard, stk);
runtime·throw("scanstack");
}
- addroot((Obj){sp, (byte*)stk - sp, 0});
+ addroot((Obj){sp, (byte*)stk - sp, (uintptr)defaultProg | PRECISE | LOOP});
sp = (byte*)stk->gobuf.sp;
guard = stk->stackguard;
stk = (Stktop*)stk->stackbase;
@@ -1399,14 +1399,17 @@ addroots(void)
for(spanidx=0; spanidx<runtime·mheap->nspan; spanidx++) {
s = allspans[spanidx];
if(s->state == MSpanInUse) {
+\t\t\t// The garbage collector ignores type pointers stored in MSpan.types:
+\t\t\t// - Compiler-generated types are stored outside of heap.
+\t\t\t// - The reflect package has runtime-generated types cached in its data structures.
+\t\t\t// The garbage collector relies on finding the references via that cache.
switch(s->types.compression) {
case MTypes_Empty:
case MTypes_Single:
break;
case MTypes_Words:
case MTypes_Bytes:
-\t\t\t\t// TODO(atom): consider using defaultProg instead of 0
-\t\t\t\taddroot((Obj){(byte*)&s->types.data, sizeof(void*), 0});
+\t\t\t\tmarkonly((byte*)s->types.data);\n
break;
}
}
コアとなるコードの解説
-
addstackroots
関数内の変更:addroot((Obj){sp, (byte*)stk - sp, 0});
からaddroot((Obj){sp, (byte*)stk - sp, (uintptr)defaultProg | PRECISE | LOOP});
への変更。- これは、スタック上のルート(
sp
からstk
までの範囲)をGCルートとして登録する際の挙動を変更しています。以前は汎用的なフラグ(0
)が使われていましたが、新しいフラグ(defaultProg | PRECISE | LOOP
)は、GCがスタックを走査する際に、より精密なポインタ識別(PRECISE
)と、特定の走査ロジック(defaultProg
)および反復的な処理(LOOP
)を適用することを示唆しています。これにより、スタック上のポインタの検出精度と効率が向上します。
-
addroots
関数内の変更:addroot((Obj){(byte*)&s->types.data, sizeof(void*), 0});
がmarkonly((byte*)s->types.data);
に置き換えられました。- 以前は、
MSpan
構造体内の型情報(s->types.data
)もGCのルートとして直接追加されていました。しかし、この変更により、markonly
という関数が呼び出されるようになりました。これは、s->types.data
がGCのルートセットに直接追加されるのではなく、GCの走査中に他の方法で到達可能であることが保証されているため、単にマークするだけで十分であることを意味します。 - 追加されたコメントは、この変更の理由を明確にしています。コンパイラ生成の型はヒープ外にあり、
reflect
パッケージのランタイム生成型はキャッシュを介してGCが参照を見つけられるため、MSpan.types.data
を明示的にルートとして追加する必要がないという最適化です。これにより、GCのルートセットが不要に肥大化するのを防ぎ、GCの走査効率が向上します。
関連リンク
- Goのガベージコレクタに関する公式ドキュメントやブログ記事(当時のもの)
- Goランタイムのソースコード(特に
src/runtime
ディレクトリ) - Goのメモリ管理に関する解説記事
参考にした情報源リンク
- https://golang.org/cl/7879043 (Goのコードレビューシステムにおけるこのコミットの変更リスト)
- Goの公式ドキュメントやブログ記事(特にガベージコレクタの進化に関するもの)
- Goのランタイムソースコード(
src/runtime/mgc0.c
および関連ファイル) - Goのガベージコレクタの仕組みを解説している技術ブログや論文