[インデックス 17356] ファイルの概要
このコミットは、Goランタイムのガベージコレクション(GC)におけるスタックスキャン処理、特にインターフェース型の値の扱いに関する変更です。以前のコミット(CL 13010045 / 04f8101b46dd)を元に戻しつつ、その問題点を修正することを目的としています。具体的には、引数領域(arguments area)におけるインターフェース型のスキャン方法を調整し、32-bitビルドの破損を修正します。
コミット
commit 87fdb8fb9ab8de4e008fa7c1561b16e3df01223a
Author: Carl Shapiro <cshapiro@google.com>
Date: Wed Aug 21 13:51:00 2013 -0700
undo CL 13010045 / 04f8101b46dd
Update the original change but do not read interface types in
the arguments area. Once the arguments area is zeroed as the
locals area is we can safely read interface type values there
too.
««« original CL description
undo CL 12785045 / 71ce80dc4195
This has broken the 32-bit builds.
««« original CL description
cmd/gc, runtime: use type information to scan interface values
R=golang-dev, rsc, dvyukov
CC=golang-dev
https://golang.org/cl/12785045
»»»
R=khr, golang-dev, khr
CC=golang-dev
https://golang.org/cl/13010045
»»»
R=khr, khr
CC=golang-dev
https://golang.org/cl/13073045
---
src/cmd/gc/pgen.c | 1 -
src/pkg/runtime/mgc0.c | 54 ++++++++++++++++++++++++++++++++++++++++----------
2 files changed, 43 insertions(+), 12 deletions(-)
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/87fdb8fb9ab8de4e008fa7c1561b16e3df01223a
元コミット内容
このコミットは、CL 13010045 / 04f8101b46dd
を元に戻すものです。
CL 13010045
自体は、CL 12785045 / 71ce80dc4195
を元に戻すものでした。
CL 12785045
の内容は「cmd/gc, runtime: use type information to scan interface values
」(型情報を使用してインターフェース値をスキャンする)というものでしたが、これが32-bitビルドを破損させたため、CL 13010045
で元に戻されました。
今回のコミット 87fdb8fb9ab8de4e008fa7c1561b16e3df01223a
は、CL 13010045
を元に戻しつつ、元の変更(型情報を使ったインターフェース値のスキャン)を更新し、引数領域ではインターフェース型を読み取らないように修正しています。これは、引数領域がローカル変数領域と同様にゼロクリアされるようになれば、安全にインターフェース値を読み取れるようになるという前提に基づいています。
変更の背景
Goのガベージコレクション(GC)は、プログラムが使用しているメモリを追跡し、不要になったメモリを解放する役割を担っています。このプロセスの一部として、スタック上のポインタを正確に識別し、それらが指すオブジェクトをマークする必要があります。特に、インターフェース型の値は、内部にポインタを含む可能性があるため、GCが正しくスキャンすることが重要です。
以前のコミット CL 12785045
では、GCがインターフェース値をスキャンする際に型情報を利用するようになりました。これは、より正確で効率的なスキャンを可能にするための改善でしたが、結果として32-bitビルドで問題を引き起こしました。この問題は、おそらくスタック上の引数領域におけるインターフェース値の表現や、GCがその領域をスキャンする方法に起因するものと考えられます。
CL 13010045
は、この32-bitビルドの破損を修正するために、CL 12785045
の変更を一時的に元に戻しました。しかし、型情報を使ったスキャンは長期的に見て望ましい改善であるため、今回のコミット 87fdb8fb9ab8de4e008fa7c1561b16e3df01223a
では、その変更を再導入しつつ、32-bitビルドの問題を回避するための修正が加えられました。具体的には、引数領域が安全にスキャンできるようになるまで、その領域のインターフェース型はスキャンしないというアプローチが取られています。
前提知識の解説
このコミットを理解するためには、以下のGoランタイムとガベージコレクションに関する知識が必要です。
- Goのガベージコレクション (GC): Goはトレース型GCを採用しており、到達可能性に基づいて不要なオブジェクトを特定し、メモリを解放します。GCは、ヒープだけでなく、スタック上のポインタもスキャンして、到達可能なオブジェクトをマークします。
- スタックスキャン: GCは実行中のGoroutineのスタックをスキャンし、スタック上に存在するポインタを識別します。これにより、スタックから参照されているヒープ上のオブジェクトがGCによって誤って解放されるのを防ぎます。
- ポインタマップ (Pointer Map): GoのGCは、スタックフレームやオブジェクト内のどこにポインタが存在するかを示す「ポインタマップ」を利用します。これにより、GCはメモリブロック全体をスキャンするのではなく、ポインタが確実に存在する場所だけを効率的にスキャンできます。このポインタマップは、コンパイラによって生成され、ランタイムに提供されます。
- インターフェース型 (Interface Types): Goのインターフェースは、内部的に2つのワードで構成されます。
- 型情報 (Type Word): インターフェースが保持している具体的な値の型(
_type
構造体へのポインタ)。 - 値 (Value Word): インターフェースが保持している具体的な値(ポインタまたは直接値)。 インターフェースがポインタ型の値を保持している場合、その値ワードはヒープ上のオブジェクトへのポインタとなります。GCは、このポインタを正しく識別し、参照先のオブジェクトをマークする必要があります。
- 型情報 (Type Word): インターフェースが保持している具体的な値の型(
Itab
とEface
:Itab
(Interface Table): 具象型がインターフェースを満たすための情報(型情報、メソッドテーブルなど)を格納する構造体です。interface{}
ではない特定のインターフェース型(例:io.Reader
)の場合に利用されます。Eface
(Empty Interface):interface{}
型を表す構造体です。型情報と値の2つのポインタで構成されます。 GCは、これらの構造体を解析して、内部に含まれるポインタを識別します。
BitsPerPointer
と関連する定数:src/pkg/runtime/mgc0.c
に定義されているこれらの定数は、ポインタマップのビット表現に関連しています。BitsPerPointer = 2
: 各ポインタエントリが2ビットで表現されることを示します。BitsNoPointer = 0
: ポインタではないことを示します。BitsPointer = 1
: 通常のポインタであることを示します。BitsIface = 2
: 特定のインターフェース型(Itab
を含む)であることを示します。BitsEface = 3
: 空のインターフェース型(Eface
を含む)であることを示します。 これらのビットは、scanbitvector
関数で利用され、メモリ上の各ワードがポインタであるか、インターフェースであるか、あるいはポインタではないかをGCに伝えます。
技術的詳細
このコミットの主要な変更は、src/pkg/runtime/mgc0.c
ファイル内のガベージコレクション関連のコードに集中しています。
-
BitsPerPointer
関連の定数の追加:src/pkg/runtime/mgc0.c
のenum
に、BitsNoPointer
,BitsPointer
,BitsIface
,BitsEface
の4つの定数が追加されました。これらは、ポインタマップの各2ビットが示す意味を明確にします。enum { // Pointer map BitsPerPointer = 2, BitsNoPointer = 0, BitsPointer = 1, BitsIface = 2, BitsEface = 3, };
-
scaninterfacedata
関数の追加: この新しい関数は、インターフェース型がポインタであることを示す場合に、インターフェースのデータ部分をスキャンするために導入されました。bits
引数によってBitsIface
(特定のインターフェース) かBitsEface
(空のインターフェース) かを判断し、それぞれItab
または_type
ポインタを介して型情報を取得します。afterprologue
引数は、関数プロローグが完了しているかどうかを示し、スタックフレームが完全に設定されている場合にのみ型情報を安全に読み取れるようにします。 最終的に、インターフェースの値部分(scanp+PtrSize
)をルートとしてGCに登録します。static void scaninterfacedata(uintptr bits, byte *scanp, bool afterprologue) { Itab *tab; Type *type; if(afterprologue) { if(bits == BitsIface) { tab = *(Itab**)scanp; if(tab->type->size <= sizeof(void*) && (tab->type->kind & KindNoPointers)) return; } else { // bits == BitsEface type = *(Type**)scanp; if(type->size <= sizeof(void*) && (type->kind & KindNoPointers)) return; } } addroot((Obj){scanp+PtrSize, PtrSize, 0}); }
-
scanbitvector
関数の変更: この関数は、ポインタマップ(BitVector
)に基づいてメモリ領域をスキャンし、ポインタをGCルートとして登録します。 変更点として、afterprologue
という新しい引数が追加されました。 ループ内で各2ビットのword
を評価し、bits
がBitsNoPointer
でない場合、かつ値がnil
でない場合に処理を行います。BitsPointer
の場合は、通常のポインタとしてaddroot
します。BitsIface
またはBitsEface
の場合は、新しく追加されたscaninterfacedata
関数を呼び出してインターフェースデータをスキャンします。
static void scanbitvector(byte *scanp, BitVector *bv, bool afterprologue) { uintptr word, bits; uint32 *wordp; int32 i, remptrs; wordp = bv->data; for(remptrs = bv->n; remptrs > 0; remptrs -= 32) { word = *wordp++; if(remptrs < 32) i = remptrs; else i = 32; i /= BitsPerPointer; for(; i > 0; i--) { bits = word & 3; if(bits != BitsNoPointer && *(void**)scanp != nil) if(bits == BitsPointer) addroot((Obj){scanp, PtrSize, 0}); else scaninterfacedata(bits, scanp, afterprologue); word >>= BitsPerPointer; scanp += PtrSize; } } }
-
addframeroots
関数の変更: この関数は、スタックフレームのローカル変数と引数をGCルートとして登録します。afterprologue
というbool
型のローカル変数が追加され、frame->varp > (byte*)frame->sp
の条件で初期化されます。これは、関数プロローグが完了し、ローカル変数領域が有効になっているかどうかを判断します。- ローカル変数(
locals
)のスキャンにおいて、scanbitvector
の呼び出しにafterprologue
引数が渡されるようになりました。これにより、プロローグが完了していない場合はインターフェース型のスキャンが抑制されます。 - 引数(
args
)のスキャンにおいて、scanbitvector
の呼び出しにfalse
が渡されるようになりました。これは、引数領域ではインターフェース型をスキャンしないというコミットの意図を反映しています。
void addframeroots(Stkframe *frame, void*) { Func *f; BitVector *args, *locals; uintptr size; bool afterprologue; // 追加 f = frame->fn; // Scan local variables if stack frame has been allocated. // Use pointer information if known. afterprologue = (frame->varp > (byte*)frame->sp); // 変更 if(afterprologue) { // 変更 locals = runtime·funcdata(f, FUNCDATA_GCLocals); if(locals == nil) { // No locals information, scan everything. addroot((Obj){frame->varp - frame->varlen, frame->varlen, 0}); } else { // Locals bitmap information, scan just the // pointers in locals. size = (locals->n*PtrSize) / BitsPerPointer; scanbitvector(frame->varp - size, locals, afterprologue); // 変更 } } // Scan arguments. // Use pointer information if known. args = runtime·funcdata(f, FUNCDATA_GCArgs); if(args != nil && args->n > 0) scanbitvector(frame->argp, args, false); // 変更 else addroot((Obj){frame->argp, frame->arglen, 0}); }
-
src/cmd/gc/pgen.c
の変更:walktype1
関数から以下の行が削除されました。bvset(bv, ((*xoffset + widthptr) / widthptr) * BitsPerPointer);
この行は、インターフェース型を処理する際に、ポインタマップに特定のビットを設定する役割を持っていました。この削除は、インターフェース型のスキャンロジックがランタイム側(
mgc0.c
)でより詳細に制御されるようになったことと関連している可能性があります。特に、引数領域でのインターフェース型のスキャンを抑制するという今回のコミットの目的と合致していると考えられます。
コアとなるコードの変更箇所
このコミットのコアとなる変更は、主に以下の2つのファイルにあります。
-
src/pkg/runtime/mgc0.c
:BitsNoPointer
,BitsPointer
,BitsIface
,BitsEface
の定数定義の追加。scaninterfacedata
関数の新規追加。scanbitvector
関数のシグネチャ変更(bool afterprologue
引数の追加)と、インターフェース型スキャンロジックの追加(scaninterfacedata
の呼び出し)。addframeroots
関数のafterprologue
変数の導入と、scanbitvector
呼び出しへのafterprologue
およびfalse
引数の渡し方。
-
src/cmd/gc/pgen.c
:walktype1
関数内の特定のbvset
呼び出しの削除。
コアとなるコードの解説
このコミットの核心は、Goのガベージコレクタがスタック上のインターフェース値をどのように扱うかを、より細かく制御することにあります。
以前の CL 12785045
では、コンパイラ(cmd/gc
)とランタイム(runtime
)が連携して、インターフェース値の型情報を利用してポインタをスキャンするようになりました。これは、インターフェースが保持する具体的な値がポインタであるかどうかを正確に判断し、不要なスキャンを避けるための最適化でした。しかし、この変更が32-bitビルドで問題を引き起こしました。
今回のコミットは、その問題を解決するために、以下の戦略を取っています。
-
ポインタマップの粒度向上:
BitsIface
とBitsEface
という新しいビットパターンを導入することで、ポインタマップが単に「ポインタがあるかないか」だけでなく、「インターフェース型であるか」という情報も保持できるようになりました。これにより、ランタイムはインターフェース値をより具体的に識別できます。 -
インターフェースデータスキャンの分離:
scaninterfacedata
関数を導入することで、インターフェース値の内部スキャンロジックが独立しました。この関数は、インターフェースの型情報(Itab
や_type
)を調べて、そのインターフェースが保持する値がポインタであるかどうか、そしてそのポインタがGCの対象となるべきかを判断します。 -
プロローグ後の安全なスキャン:
addframeroots
関数とscanbitvector
関数にafterprologue
引数を導入したことが重要です。関数が呼び出された直後(プロローグ中)は、スタックフレームが完全に構築されておらず、ローカル変数や引数領域がまだ初期化されていない可能性があります。この状態では、これらの領域に存在するインターフェース値の型情報を安全に読み取ることができません。- ローカル変数領域については、
afterprologue
がtrue
の場合(プロローグ完了後)にのみ、インターフェース型のスキャンが実行されます。 - 引数領域については、
scanbitvector
に常にfalse
が渡されるようになりました。これは、コミットメッセージにある「引数領域がローカル変数領域と同様にゼロクリアされるようになるまで、インターフェース型を読み取らない」という意図を直接反映しています。つまり、引数領域のインターフェース値は、その領域が安全にスキャンできる状態になるまで、型情報に基づいた詳細なスキャンは行わず、代わりに一般的なポインタとして扱われるか、あるいはスキャン自体が抑制されることを意味します。これにより、32-bitビルドで発生していた問題が回避されます。
- ローカル変数領域については、
-
コンパイラ側の調整:
src/cmd/gc/pgen.c
からのbvset
呼び出しの削除は、コンパイラが生成するポインタマップのインターフェース型に関するビット設定を、ランタイム側の新しい、より洗練されたスキャンロジックに委ねることを示唆しています。これにより、コンパイラとランタイム間の責任分担が明確化され、ランタイムがインターフェース型のスキャンをより柔軟に制御できるようになります。
これらの変更により、GoのGCは、インターフェース値をより正確かつ安全にスキャンできるようになり、特にスタック上の引数領域における潜在的な問題を回避しつつ、型情報に基づいたスキャンという最適化の恩恵を享受できるようになりました。
関連リンク
- 元の変更 (CL 12785045):
cmd/gc, runtime: use type information to scan interface values
- 最初のUndo (CL 13010045):
undo CL 12785045 / 71ce80dc4195
- このコミットに対応するCL (CL 13073045):
undo CL 13010045 / 04f8101b46dd
参考にした情報源リンク
- Goのガベージコレクションに関する公式ドキュメントやブログ記事 (当時のGoのバージョンに合わせたもの)
- Goのインターフェースの内部表現に関する資料
- Goのランタイムソースコード (
src/pkg/runtime/mgc0.c
および関連ファイル) - Goのコンパイラソースコード (
src/cmd/gc/pgen.c
および関連ファイル)