[インデックス 18941] ファイルの概要
このコミットは、Goランタイムのスタックマップエントリのエンコーディングを変更し、ガベージコレクション(GC)における「誤った参照保持(false retention)」の問題を回避することを目的としています。特に、ゼロ長の文字列やゼロ容量のスライスが、実際には参照していないメモリ領域をGCが誤って参照していると判断し、そのメモリが解放されない問題を解決します。
コミット
Author: Keith Randall khr@golang.org Date: Tue Mar 25 14:11:34 2014 -0700
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/1b45cc45e37cfe67733ebff5eb5cabfef207eef6
元コミット内容
runtime: redo stack map entries to avoid false retention
Change two-bit stack map entries to encode:
0 = dead
1 = scalar
2 = pointer
3 = multiword
If multiword, the two-bit entry for the following word encodes:
0 = string
1 = slice
2 = iface
3 = eface
That way, during stack scanning we can check if a string
is zero length or a slice has zero capacity. We can avoid
following the contained pointer in those cases. It is safe
to do so because it can never be dereferenced, and it is
desirable to do so because it may cause false retention
of the following block in memory.
Slice feature turned off until issue 7564 is fixed.
Update #7549
LGTM=rsc
R=golang-codereviews, bradfitz, rsc
CC=golang-codereviews
https://golang.org/cl/76380043
変更の背景
Goのガベージコレクション(GC)は、プログラムが不要になったメモリを自動的に解放する役割を担っています。GCが正しく機能するためには、スタック上に存在するどの値がヒープ上のオブジェクトへのポインタであるかを正確に識別する必要があります。この識別を誤ると、「誤った参照保持(false retention)」という問題が発生します。これは、実際にはプログラムから到達不可能であるにもかかわらず、GCが誤って参照されていると判断し、メモリが解放されない状態を指します。
特に問題となるのは、Goの文字列(string)やスライス(slice)の内部表現です。これらはデータへのポインタと長さ(およびスライスでは容量)を持つ構造体です。例えば、長さがゼロの文字列や容量がゼロのスライスは、内部的にはデータへのポインタを持っていても、そのポインタが指すメモリ領域は実際には使用されていません。しかし、GCがそのポインタを単なるポインタとして認識してしまうと、たとえそのポインタが指すメモリが不要であっても、GCはそれを「生きている」と判断し、解放せずに保持してしまう可能性がありました。
このコミットは、スタックマップのエントリに、より詳細な型情報(特に文字列やスライス、インターフェースの特殊なケース)をエンコードすることで、この誤った参照保持の問題を解決しようとしています。これにより、GCはスタックをスキャンする際に、ゼロ長の文字列やゼロ容量のスライスに遭遇した場合、その内部ポインタを追跡する必要がないことを認識し、関連するメモリブロックの誤った保持を防ぐことができます。
前提知識の解説
Goのガベージコレクション (GC)
GoのGCは、並行マーク&スイープ方式を採用しています。これは、プログラムの実行と並行してGCが動作し、アプリケーションの停止時間(STW: Stop-The-World)を最小限に抑えることを目指しています。GCの基本的な流れは以下の通りです。
- マークフェーズ: GCは、プログラムが現在使用しているオブジェクト(「生きている」オブジェクト)を識別します。この識別は、スタックやグローバル変数など、プログラムが直接アクセスできる「GCルート」からポインタをたどることで行われます。
- スイープフェーズ: マークされなかったオブジェクト(「死んでいる」オブジェクト)は、不要なメモリとして解放されます。
スタックマップ
GoのGCがスタック上のポインタを正確に識別するために使用されるのが「スタックマップ」です。各関数のスタックフレームには、そのフレーム内のどの位置にポインタが存在するかを示すメタデータ(ビットベクトル)がコンパイラによって生成されます。GCはこのスタックマップを参照して、スタック上のポインタを効率的かつ正確にスキャンします。
ポインタスキャン
GCは、スタックマップの情報に基づいてスタック上のメモリ領域をスキャンし、ポインタを見つけます。見つかったポインタがヒープ上のオブジェクトを指している場合、そのオブジェクトを「生きている」とマークし、さらにそのオブジェクトが持つポインタをたどって、到達可能なすべてのオブジェクトをマークしていきます。
Goのデータ型の内部表現
- 文字列 (string): Goの文字列は不変なバイトシーケンスです。内部的には、データへのポインタと文字列の長さ(バイト数)の2つのフィールドを持つ構造体として表現されます。
type StringHeader struct { Data uintptr // データへのポインタ Len int // 長さ(バイト数) }
- スライス (slice): Goのスライスは、基になる配列への動的なビューを提供します。内部的には、データへのポインタ、スライスの長さ、そして容量の3つのフィールドを持つ構造体として表現されます。
type SliceHeader struct { Data uintptr // データへのポインタ Len int // 長さ(要素数) Cap int // 容量(要素数) }
- インターフェース (interface): Goのインターフェースは、型と値のペアとして内部的に表現されます。
- 空インターフェース (
interface{}
またはany
): 型情報へのポインタと、値へのポインタの2つのフィールドを持ちます。 - 非空インターフェース (メソッドを持つインターフェース): インターフェーステーブル(
itab
)へのポインタと、値へのポインタの2つのフィールドを持ちます。itab
は、具体的な型情報と、その型がインターフェースのメソッドをどのように実装しているかの情報を含みます。
- 空インターフェース (
技術的詳細
このコミットの核心は、スタックマップエントリのエンコーディングを拡張し、より詳細な型情報をGCに提供することです。以前は、ポインタか非ポインタかといった大まかな情報しか持っていませんでしたが、新しいエンコーディングでは以下の4つのカテゴリを2ビットで表現します。
- 0 = dead (デッド): そのメモリ位置には有効な値がなく、GCがスキャンする必要がないことを示します。
- 1 = scalar (スカラ): そのメモリ位置がポインタではないスカラ値(整数、浮動小数点数など)であることを示します。GCはスキャンする必要がありません。
- 2 = pointer (ポインタ): そのメモリ位置がヒープ上のオブジェクトを指すポインタであることを示します。GCはこのポインタを追跡して、参照先のオブジェクトをマークします。
- 3 = multiword (マルチワード): そのメモリ位置が、複数のワード(ポインタサイズ)にまたがる複合型(文字列、スライス、インターフェースなど)の最初のワードであることを示します。
multiword
の場合、その次のワードの2ビットエントリが、さらに詳細な型情報をエンコードします。
- 0 = string (文字列):
multiword
の次のワードが文字列の長さフィールドであることを示します。 - 1 = slice (スライス):
multiword
の次のワードがスライスの長さフィールドであることを示します。 - 2 = iface (インターフェース):
multiword
の次のワードがインターフェースの型情報(itab
または_type
)へのポインタであることを示します。 - 3 = eface (空インターフェース):
multiword
の次のワードが空インターフェースの型情報(_type
)へのポインタであることを示します。
この新しいエンコーディングにより、GCはスタックをスキャンする際に、文字列やスライス、インターフェースの構造をより正確に理解できるようになります。
特に重要なのは、文字列とスライスのケースです。
- ゼロ長の文字列:
Data
ポインタは存在するものの、Len
がゼロの場合、そのポインタが指すメモリは実際には使用されていません。新しいエンコーディングでは、GCはこれが文字列であることを認識し、Len
フィールドがゼロであれば、Data
ポインタを追跡する必要がないと判断できます。これにより、不要なメモリの誤った保持を防ぎます。 - ゼロ容量のスライス: 同様に、
Data
ポインタが存在してもCap
がゼロの場合、そのポインタが指すメモリは使用されていません。GCはこれがスライスであることを認識し、Cap
フィールドがゼロであれば、Data
ポインタを追跡する必要がないと判断できます。
このコミットでは、gcdead
という新しいデバッグオプションも導入されています。GODEBUG=gcdead=1
を設定すると、GCが「dead」と判断したスタック上のスロットを特定のパターン(0x6969696969696969LL
や0x6868686868686868LL
)で上書きするようになります。これにより、誤って「dead」とマークされたメモリが後で参照された場合に、クラッシュなどの問題を引き起こし、デバッグを容易にすることができます。
ただし、コミットメッセージには「Slice feature turned off until issue 7564 is fixed.」とあり、スライスに関する最適化は、当時未解決だったissue 7564
が修正されるまで無効化されていたことが示唆されています。issue 7564
は公開されているGoリポジトリでは直接見つかりませんでしたが、これは内部的なトラッキング番号である可能性があります。
コアとなるコードの変更箇所
このコミットは、Goコンパイラ(cmd/cc
, cmd/gc
)とランタイム(pkg/runtime
)の複数のファイルにわたる変更を含んでいます。
src/cmd/cc/pgen.c
: Cコンパイラのコード生成部分で、スタックマップの生成ロジックが変更されています。特に、ポインタ型に対してBitsPointer
(2)をセットするようになり、スカラ型に対してもBitsScalar
(1)をセットするループが追加されています。src/cmd/gc/plive.c
: Goコンパイラのライブネス解析部分で、スタックマップの生成ロジックが変更されています。twobitwalktype1
関数内で、文字列、スライス、インターフェースの型に対してBitsMultiWord
とそれに続く詳細な型情報(BitsString
,BitsSlice
,BitsIface
,BitsEface
)をエンコードするロジックが追加されています。src/pkg/runtime/extern.go
:gcdead
デバッグオプションの定義が追加されています。src/pkg/runtime/malloc.h
: スタックマップエントリの新しいエンコーディング定数(BitsDead
,BitsScalar
,BitsPointer
,BitsMultiWord
,BitsString
,BitsSlice
,BitsIface
,BitsEface
)が定義されています。src/pkg/runtime/mfinal_test.go
: ゼロ長スライスとゼロ長文字列が次のオブジェクトをメモリにピン留めしないことを確認するための新しいテストケースTestEmptySlice
とTestEmptyString
が追加されています。TestEmptySlice
はissue 7564
が修正されるまで無効化されています。src/pkg/runtime/mgc0.c
: GCのスタックスキャンロジック(scanbitvector
関数)が大幅に変更されています。新しいスタックマップエンコーディングを解釈し、BitsMultiWord
の場合に続くワードの型情報に基づいて、文字列やスライスの長さ/容量をチェックし、不要なポインタ追跡をスキップするロジックが実装されています。また、gcdead
オプションが有効な場合にデッドなスロットを上書きする処理も追加されています。src/pkg/runtime/runtime.c
:gcdead
デバッグオプションをランタイムに登録するエントリが追加されています。src/pkg/runtime/runtime.h
:DebugVars
構造体にgcdead
フィールドが追加されています。src/pkg/runtime/stack.c
: スタックのポインタ調整ロジック(adjustpointers
関数)が変更されています。ここでも新しいスタックマップエンコーディングを解釈し、BitsMultiWord
の場合に文字列、スライス、インターフェースの調整ロジックが更新されています。gcdead
オプションが有効な場合にデッドなスロットを上書きする処理も追加されています。
コアとなるコードの解説
src/pkg/runtime/malloc.h
の変更
enum {
// Pointer map
BitsPerPointer = 2,
BitsDead = 0,
BitsScalar = 1,
BitsPointer = 2,
BitsMultiWord = 3,
// BitsMultiWord will be set for the first word of a multi-word item.
// When it is set, one of the following will be set for the second word.
BitsString = 0,
BitsSlice = 1,
BitsIface = 2,
BitsEface = 3,
};
この変更は、スタックマップエントリの新しいエンコーディングを定義しています。BitsPerPointer
が2ビットであることを示し、各2ビットがDead
、Scalar
、Pointer
、MultiWord
のいずれかを表します。MultiWord
の場合、次の2ビットがさらにString
、Slice
、Iface
、Eface
のいずれかを表すことで、複合型の詳細な情報をGCに伝えます。
src/pkg/runtime/mgc0.c
の scanbitvector
関数の変更
// 変更前 (簡略化)
// if(bits != BitsNoPointer && *(void**)scanp != nil)
// if(bits == BitsPointer)
// enqueue1(wbufp, (Obj){scanp, PtrSize, 0});
// else
// scaninterfacedata(bits, scanp, afterprologue, wbufp);
// 変更後 (抜粋)
switch(bits) {
case BitsDead:
if(runtime·debug.gcdead)
*(uintptr*)scanp = (uintptr)0x6969696969696969LL;
break;
case BitsScalar:
break;
case BitsPointer:
p = *(byte**)scanp;
if(p != nil)
enqueue1(wbufp, (Obj){scanp, PtrSize, 0});
break;
case BitsMultiWord:
p = *(byte**)scanp;
if(p != nil) {
word >>= BitsPerPointer; // 次のワードのエンコーディングを取得
scanp += PtrSize;
i--;
// ... (ビットの読み込みロジック) ...
switch(word & 3) { // 次のワードのエンコーディングをチェック
case BitsString:
if(((String*)(scanp - PtrSize))->len != 0) // 文字列の長さがゼロでない場合のみマーク
markonly(p);
break;
case BitsSlice:
// ... (スライスの容量チェックとマークロジック) ...
if(((Slice*)(scanp - PtrSize))->cap != 0) // スライスの容量がゼロでない場合のみマーク
enqueue1(wbufp, (Obj){scanp - PtrSize, PtrSize, 0});
break;
case BitsIface:
case BitsEface:
scaninterfacedata(word & 3, scanp - PtrSize, afterprologue, wbufp);
break;
}
}
break;
}
このscanbitvector
関数は、GCがスタック上のビットベクトルをスキャンする際の主要なロジックを含んでいます。変更前はBitsNoPointer
とBitsPointer
の2種類しか区別していませんでしたが、変更後はswitch
文を使って新しい4種類のエンコーディング(BitsDead
, BitsScalar
, BitsPointer
, BitsMultiWord
)を処理します。
特にBitsMultiWord
のケースでは、さらに次のワードのエンコーディングを読み取り、それがBitsString
であれば文字列のlen
フィールドを、BitsSlice
であればスライスのcap
フィールドをチェックします。len
やcap
がゼロの場合、たとえポインタが存在しても、そのポインタが指すメモリは実際には使用されていないため、GCはそれを追跡しないようにmarkonly
やenqueue1
の呼び出しを条件付きにしています。これにより、ゼロ長の文字列やゼロ容量のスライスによる誤った参照保持を防ぎます。
BitsDead
のケースでは、runtime.debug.gcdead
が有効な場合に、そのメモリ位置を特定のパターンで上書きすることで、デバッグを容易にしています。
src/cmd/gc/plive.c
の twobitwalktype1
関数の変更
// 変更前 (簡略化)
// case TSTRING:
// bvset(bv, (*xoffset / widthptr) * BitsPerPointer); // ポインタとしてマーク
// 変更後 (抜粋)
case TSTRING:
// struct { byte *str; intgo len; }
// ...
bvset(bv, (*xoffset / widthptr) * BitsPerPointer + 0); // BitsMultiWord
bvset(bv, (*xoffset / widthptr) * BitsPerPointer + 1); // BitsString
*xoffset += t->width;
break;
// 変更前 (簡略化)
// case TARRAY: // スライスも含む
// bvset(bv, (*xoffset / widthptr) * BitsPerPointer); // ポインタとしてマーク
// 変更後 (抜粋)
case TARRAY: // スライスの場合
// struct { byte *array; uintgo len; uintgo cap; }
// ...
if(0) { // このブロックはissue 7564が修正されるまで無効
bvset(bv, (*xoffset / widthptr) * BitsPerPointer + 0); // BitsMultiWord
bvset(bv, (*xoffset / widthptr) * BitsPerPointer + 1); // BitsSlice
bvset(bv, (*xoffset / widthptr) * BitsPerPointer + 2); // 3:1 = multiword/slice
} else {
// Until bug 7564 is fixed, we consider a slice as
// a separate pointer and integer.
bvset(bv, (*xoffset / widthptr) * BitsPerPointer + 1); // 2 = live ptr (ポインタ)
bvset(bv, (*xoffset / widthptr) * BitsPerPointer + 2); // 1 = live scalar (スカラ)
}
// mark capacity as live
bvset(bv, (*xoffset / widthptr) * BitsPerPointer + 4); // 1 = live scalar (スカラ)
*xoffset += t->width;
break;
// インターフェースの変更も同様にBitsMultiWordとBitsIface/BitsEfaceをセット
twobitwalktype1
関数は、コンパイラが型をウォークしてスタックマップのビットベクトルを生成する際に使用されます。この変更により、文字列、スライス、インターフェースといった複合型に対して、単なるポインタとしてではなく、BitsMultiWord
とその後の詳細な型情報(BitsString
, BitsSlice
, BitsIface
, BitsEface
)をエンコードするようになりました。これにより、GCがこれらの型をより正確に識別できるようになります。
スライスの部分にはif(0)
で囲まれたブロックがあり、これはissue 7564
が修正されるまでスライスの最適化が一時的に無効化されていたことを示しています。代わりに、スライスはポインタとスカラ(長さと容量)の組み合わせとして扱われていました。
関連リンク
- Go CL 76380043: https://golang.org/cl/76380043
- Go Issue 7549: https://github.com/golang/go/issues/7549 (このコミットが更新したIssue)
参考にした情報源リンク
- Go runtime stack map:
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQF2HVt1AoQkxTDcwpVHqgbuB6yTFIHN4rb_2K590_wLlMnMBrGjiJd-OXpe5LokaJguUk6qCWwtWFLJ5nV0QLrpMeFIbowWBe9MSUkrQEQZxxbnXnLfPLeMvOGddnjUnoLCJZxRWOEi-l7rXoBEjyA=
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQFm33CEMc3hHmoLNzVv3MeCgazACaF6IQol0fKUFdgdtM2yUjPspJLeqow71rVjO_G5o_vdLefkAi1twWBpyu1_UTGdr9NLoYNAlhw9ha4XzGhyrwPpEkyvTqzouJyx5TXMO0sTmvo=
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHu7RvwGBNLWUE9qh4btATM5bM2UHuXfxMGCcJ1jTRjjEUv4gTW6HeEqjqNNxsqIviZb2OX7-BF72XUlrQejKUaD3u90OS0KlR9x14BTz3Vz9LQZHww_uSrv3rjRKkYBWgUesup28kmBYQ3qKhgF3wQ
- Go garbage collection false retention:
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQERWcxIyBYA1CrMTxO5dEEan7De0VZSdUGOvbmlLg4-LrjuoHG0QsRYWDNCxhlwTU_ZHNkVPRdrr4P11hFfDDh52II6hPSlLFOAg1__eGVyRSaggwptwYPNB2o4G6y9WRliXOCOuUROLlAazoCL0Q5merbX0AfIGh-vsMRZQC2jGC3Tymu_74fbj2p874if__Qy4kXta-8KMsNlCnT-NhcQxNWIFxyapfY6bcg=
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHgkGixO7wyzWU3gs90zISRqgcGXZkOrpCX5WL6XRnieqknbmJUGa7d9Cot6GsFLm4dRm7N6HkZzy78BTQZuSwuKy9rUEPUEMAbqFVl_ygRBYDo0Hi2xLRtLVNmrqrIViKz5y4dDUDFO5sX
- Go string internal representation:
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQGmEXeSjM-xulptVDt9SZEia_5JLV-5NohJxMBZMR3oepSzeird9HM-ucQXuOx3M5WCLxEaPK4wXVGQTXElyX0e0X4HfXD0aSbbmqFxRs1wKdcEENOK86E14mL4F8z-k3Q=
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQGYCAl9b-cmiqMAhGDuUJYXEsVUWun8DI2IYoLw1Myjh7_sMD7c5Uhk84FgsYXEpxfrypsgVTn-XSXKpCdn8gOtqF535Hrga0mUmY2AR8Qd4POtpp_MMJruVOeTVrF7xXqCLF9W_P0dGToT0jbPQNoPnrHZAVgtAvLYuOLnS5-WH18Z
- Go slice internal representation:
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQEina2XAx_mWEYlhCgw6qfWj0d-kro8ro5XcFMV9qgROlF22b3B9UAPUAhjTaDGVSp0Q29ODK2SnRo7yaFzym0tRyS1RFZAH6ON4jRBHIT9450FR_MRDHbAl0cq
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQGslVB5VZE57C52LmT5YUWq1lcdsGzNsvwquXi-RCn75eaYungZ6cv_F8zJ_4_2O2reOpKoCG0kMrjsvu4lAeBMyU0kTmxogeH0G1ZmBMZCYN_hxdr4EUUwzUtrH65ERB9tyvz9Dg==
- Go interface internal representation:
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQFvRvLMULS8Qop-Eu5uTl5S2-nzDWEjfv7zWzuHwx1cFaQYzX1utNxSsoSfm6lLfHRk79SYZTtMJoB7q_wGP3eVgY5sC0dUVOkx4pPIJ-rCFnRlEPjuCe_FK9DyxkVkmbrIn3MiFm9Ot4KeatoDnP_UeYlAzn9JOLTt_QVvDWmmpjpREoWRabKBM-1i7sg=
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHEBJL69NTYyuc8oC4udf3Wm7fRe0p7tSNICzB9P7kBBsaA0WKvUgqp-cBtB1cr84qC-8UT6Ypxfje9u6tF_ye4GqH8YCEP-gWdjiSujBv9xnZNB7vfLgXk2pnbYNOIa5CQB2dPrXM=
- Go runtime debug gcdead (Note: This specific flag might not be publicly documented in recent Go versions, but was introduced by this commit):
- Go issue 7564 (Note: Public search did not yield a direct match in the golang/go repository, suggesting it might be an internal tracking number or a very old, unindexed issue):