[インデックス 17325] ファイルの概要
このコミットは、Go言語のガベージコレクタ(GC)がインターフェース値をスキャンする際の挙動を改善し、より正確かつ効率的に型情報を使用するように変更を加えるものです。具体的には、インターフェースが保持する実際の値の型情報に基づいて、ポインタのスキャンを最適化します。これにより、GCの精度とパフォーマンスが向上します。
コミット
commit 21ea5103a4dab347baeb497675e0786424d6c974
Author: Carl Shapiro <cshapiro@google.com>
Date: Mon Aug 19 10:19:59 2013 -0700
cmd/gc, runtime: use type information to scan interface values
R=golang-dev, rsc, dvyukov
CC=golang-dev
https://golang.org/cl/12785045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/21ea5103a4dab347baeb497675e0786424d6c974
元コミット内容
cmd/gc, runtime: use type information to scan interface values
変更の背景
Go言語のガベージコレクタは、メモリ上のポインタを正確に識別し、到達可能なオブジェクトをマークすることで、不要になったメモリを解放します。インターフェースはGoの強力な機能の一つですが、その内部表現は「型情報」と「値(データ)へのポインタ」のペアで構成されています。
従来のGCスキャンでは、インターフェース値のデータ部分が常にポインタとして扱われるか、あるいは型情報が十分に活用されずにスキャンされる可能性がありました。これにより、以下のような問題が生じる可能性がありました。
- 過剰なスキャン (Over-scanning): インターフェースがポインタを含まない小さな値(例:
int
,bool
)を保持している場合でも、そのデータ部分をポインタとしてスキャンしようとすることで、無駄なCPUサイクルを消費する。 - 不正確なスキャン (Under-scanning): インターフェースが実際にポインタを含む複雑な構造体などを保持しているにもかかわらず、そのポインタが適切に識別されず、GCによって誤って解放されてしまう(Use-After-Freeなどのバグにつながる)リスク。
このコミットは、これらの問題を解決し、GCがインターフェース値をスキャンする際に、その内部の型情報をより詳細に利用することで、スキャンの精度と効率を向上させることを目的としています。
前提知識の解説
このコミットを理解するためには、以下のGo言語の内部構造とガベージコレクションに関する知識が必要です。
-
Goのインターフェースの内部表現:
- Goのインターフェース値は、内部的に2つのワード(通常はポインタサイズ)で構成されます。
- 1つ目のワードは「型情報 (type descriptor)」へのポインタです。これは、インターフェースが現在保持している具体的な値の型(例:
*MyStruct
,string
,int
など)を記述します。 - 2つ目のワードは「データ (data)」へのポインタです。これは、インターフェースが保持している実際の値がメモリ上のどこにあるかを示します。値がポインタサイズに収まる場合は直接データが格納されることもありますが、通常はヒープ上のオブジェクトへのポインタとなります。
interface{}
(空インターフェース)とinterface{ T }
(型付きインターフェース)で、型情報の表現方法が若干異なりますが、基本的な構造は同じです。空インターフェースは_type
構造体へのポインタを、型付きインターフェースはitab
構造体へのポインタを型情報として持ちます。
-
Goのガベージコレクション (GC):
- GoのGCは、主にマーク&スイープ方式を採用しています。
- マークフェーズ: プログラムの実行中に到達可能なすべてのオブジェクトを識別し、マークします。この際、スタック、グローバル変数、レジスタなどから「ルートポインタ」をたどり、それらが指すオブジェクトをマークし、さらにそれらのオブジェクトが指すオブジェクトを再帰的にマークしていきます。
- スキャン: GCがメモリ領域を走査し、ポインタを識別してマーク対象のオブジェクトを見つけるプロセスです。このスキャンは、スタックフレーム、ヒープ上のオブジェクト、グローバルデータなど、様々なメモリ領域に対して行われます。
- ポインタマップ/ビットマップ: GoのGCは、メモリ領域内のどこにポインタが存在するかを示す「ポインタマップ」または「ビットマップ」を利用します。これにより、GCはメモリ全体をバイト単位でスキャンするのではなく、ポインタが存在する可能性のある場所だけを効率的にチェックできます。
mgc0.c
に見られるBitVector
はその一例です。
-
src/cmd/gc/pgen.c
とsrc/pkg/runtime/mgc0.c
:src/cmd/gc/pgen.c
: Goコンパイラの一部であり、主にポインタ生成(pointer generation)に関連する処理、特にGCが利用する型情報やポインタマップの生成に関わっています。src/pkg/runtime/mgc0.c
: Goランタイムの一部であり、ガベージコレクタのコアロジックが実装されています。メモリのマーク、スキャン、解放などの処理が含まれます。
技術的詳細
このコミットの核心は、GCがインターフェース値をスキャンする際に、より詳細な型情報(ポインタの有無など)を利用するように変更された点です。
-
新しいポインタビットの導入:
src/pkg/runtime/mgc0.c
において、BitsPerPointer
(ポインタあたりのビット数)の定義が拡張され、以下の新しい定数が導入されました。BitsNoPointer = 0
: ポインタではないことを示す。BitsPointer = 1
: 通常のポインタであることを示す。BitsIface = 2
: 型付きインターフェース(interface{ T }
)であることを示す。BitsEface = 3
: 空インターフェース(interface{}
)であることを示す。 これにより、GCはメモリ上の各ワードが「ポインタではない」「通常のポインタ」「インターフェース値」のいずれであるかを、ビットマップを通じて識別できるようになります。
-
インターフェースデータスキャン専用関数の追加:
scaninterfacedata
という新しい静的関数がsrc/pkg/runtime/mgc0.c
に追加されました。この関数は、インターフェース値の「データ部分」をスキャンするために特化しています。BitsIface
またはBitsEface
のビットが設定されている場合、この関数が呼び出されます。- 関数内では、インターフェースが保持する具体的な値の型情報(
Itab
または_type
)にアクセスします。 tab->type->size <= sizeof(void*) && (tab->type->kind & KindNoPointers)
という条件で、インターフェースが保持する値がポインタを含まない小さな型(例:int
,bool
,string
のデータ部分など)であるかをチェックします。もしそうであれば、そのデータ部分にはポインタが含まれないため、スキャンをスキップして効率化を図ります。- それ以外の場合、
addroot((Obj){scanp+PtrSize, PtrSize, 0});
を呼び出し、インターフェースのデータ部分が指すオブジェクトをGCルートとして追加します。これにより、インターフェースが参照している実際のオブジェクトがGCによってマークされ、生存が保証されます。
-
scanbitvector
関数の変更:scanbitvector
関数は、メモリ領域をビットマップに基づいてスキャンする汎用的な関数です。この関数が変更され、新しいポインタビット(BitsIface
,BitsEface
)を認識するようになりました。- 以前は
if(w & 3)
のように、単にビットが立っているかどうかでポインタを判断していましたが、変更後はbits = word & 3;
で2ビットの情報を抽出し、BitsPointer
、BitsIface
、BitsEface
のいずれであるかを判別します。 BitsPointer
であれば従来通りaddroot
を呼び出します。BitsIface
またはBitsEface
であれば、新しく追加されたscaninterfacedata
関数を呼び出し、インターフェースのデータ部分の型に応じたスキャンを行います。
- 以前は
-
addframeroots
関数の変更:addframeroots
関数は、スタックフレーム上のローカル変数や引数からGCルートを追加する役割を担います。scanbitvector
関数がinprologue
という新しい引数を取るようになったため、addframeroots
もこの引数を適切に渡すように変更されました。afterprologue
というフラグが導入され、関数のプロローグ(スタックフレームのセットアップ部分)が完了しているかどうかに応じて、scanbitvector
に渡すinprologue
の値が決定されます。これは、スタックフレームが完全に構築される前にスキャンを行うと、不正確な結果を招く可能性があるため、そのタイミングを制御するためのものです。
-
src/cmd/gc/pgen.c
の変更:src/cmd/gc/pgen.c
からは、bvset(bv, ((*xoffset + widthptr) / widthptr) * BitsPerPointer);
という行が削除されました。これは、コンパイラがポインタマップを生成する際の方法が、より洗練されたインターフェース値の型情報に基づくアプローチに置き換えられたことを示唆しています。古い、より一般的なポインタマーキングのロジックが不要になったと考えられます。
これらの変更により、GoのGCはインターフェース値をより正確に、かつ効率的に処理できるようになり、ガベージコレクションのオーバーヘッド削減と信頼性向上に貢献します。
コアとなるコードの変更箇所
src/cmd/gc/pgen.c
--- a/src/cmd/gc/pgen.c
+++ b/src/cmd/gc/pgen.c
@@ -257,7 +257,6 @@ walktype1(Type *t, vlong *xoffset, Bvec *bv)
tbvset(bv, ((*xoffset / widthptr) * BitsPerPointer) + 1);
if(isnilinter(t))
tbvset(bv, ((*xoffset / widthptr) * BitsPerPointer));
- tbvset(bv, ((*xoffset + widthptr) / widthptr) * BitsPerPointer);
*xoffset += t->width;
break;
src/pkg/runtime/mgc0.c
--- a/src/pkg/runtime/mgc0.c
+++ b/src/pkg/runtime/mgc0.c
@@ -36,6 +36,10 @@ enum {
// Pointer map
BitsPerPointer = 2,
+ BitsNoPointer = 0,
+ BitsPointer = 1,
+ BitsIface = 2,
+ BitsEface = 3,
};
// Bits in per-word bitmap.
@@ -1398,26 +1402,52 @@ struct BitVector
uint32 data[];
};
+// Scans an interface data value when the interface type indicates
+// that it is a pointer.
+static void
+scaninterfacedata(uintptr bits, byte *scanp, bool inprologue)
+{
+ Itab *tab;
+ Type *type;
+
+ if(!inprologue) {
+ 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});
+}
+
// Starting from scanp, scans words corresponding to set bits.
static void
-scanbitvector(byte *scanp, BitVector *bv)\n {
-\tuint32 *wp;\n-\tuint32 w;\n+\tuintptr word, bits;\n+\tuint32 *wordp;\n \tint32 i, remptrs;\n \n-\twp = bv->data;\n+\twordp = bv->data;\n \tfor(remptrs = bv->n; remptrs > 0; remptrs -= 32) {\n-\t\tw = *wp++;\n+\t\tword = *wordp++;\n \t\tif(remptrs < 32)\n \t\t\ti = remptrs;\n \t\telse\n \t\t\ti = 32;\n \t\ti /= BitsPerPointer;\n \t\tfor(; i > 0; i--) {\n-\t\t\tif(w & 3)\n-\t\t\t\taddroot((Obj){scanp, PtrSize, 0});\n-\t\t\tw >>= BitsPerPointer;\n+\t\t\tbits = word & 3;\n+\t\t\tif(bits != BitsNoPointer && *(void**)scanp != nil)\n+\t\t\t\tif(bits == BitsPointer)\n+\t\t\t\t\taddroot((Obj){scanp, PtrSize, 0});\n+\t\t\t\telse\n+\t\t\t\t\tscaninterfacedata(bits, scanp, inprologue);\n+\t\t\tword >>= BitsPerPointer;\n \t\t\tscanp += PtrSize;\n \t\t}\n \t}\n@@ -1430,12 +1460,14 @@ addframeroots(Stkframe *frame, void*)\n \tFunc *f;\n \tBitVector *args, *locals;\n \tuintptr size;\n+\tbool afterprologue;\n \n \tf = frame->fn;\n \n \t// Scan local variables if stack frame has been allocated.\n \t// Use pointer information if known.\n-\tif(frame->varp > (byte*)frame->sp) {\n+\tafterprologue = (frame->varp > (byte*)frame->sp);\n+\tif(afterprologue) {\n \t\tlocals = runtime·funcdata(f, FUNCDATA_GCLocals);\n \t\tif(locals == nil) {\n \t\t\t// No locals information, scan everything.\n@@ -1450,7 +1482,7 @@ addframeroots(Stkframe *frame, void*)\n \t\t\t// pointers in locals.\n \t\t\tsize = (locals->n*PtrSize) / BitsPerPointer;\n-\t\t\tscanbitvector(frame->varp - size, locals);\n+\t\t\tscanbitvector(frame->varp - size, locals, false);\n \t\t}\n \t}\n \n@@ -1458,7 +1490,7 @@ addframeroots(Stkframe *frame, void*)\n \t// Use pointer information if known.\n \targs = runtime·funcdata(f, FUNCDATA_GCArgs);\n \tif(args != nil && args->n > 0)\n-\t\tscanbitvector(frame->argp, args);\n+\t\tscanbitvector(frame->argp, args, !afterprologue);\n \telse\n \t\taddroot((Obj){frame->argp, frame->arglen, 0});\n }\n```
## コアとなるコードの解説
### `src/cmd/gc/pgen.c`の変更
削除された行 `tbvset(bv, ((*xoffset + widthptr) / widthptr) * BitsPerPointer);` は、コンパイラがポインタビットマップを生成する際の一部のロジックでした。この行が削除されたのは、インターフェース値のスキャン方法が`mgc0.c`側でより詳細に制御されるようになったため、コンパイラ側での汎用的なポインタマーキングが不要になったか、あるいはより正確な方法に置き換えられたためと考えられます。これは、GCがインターフェースのデータ部分を「常にポインタとして扱う」のではなく、「型情報に基づいて判断する」という新しいアプローチと整合しています。
### `src/pkg/runtime/mgc0.c`の変更
1. **`enum`の拡張**:
`BitsNoPointer`, `BitsPointer`, `BitsIface`, `BitsEface`の追加は、GCがメモリ上の各ワードをスキャンする際に、そのワードが指すものが「ポインタではない」「通常のポインタ」「型付きインターフェース」「空インターフェース」のいずれであるかを、ビットマップを通じて識別するための新しい分類を提供します。これにより、GCはより粒度の高い情報に基づいてスキャン戦略を決定できます。
2. **`scaninterfacedata`関数の追加**:
この関数は、インターフェース値のデータ部分をスキャンするための新しい専用ロジックです。
* `uintptr bits`引数は、`BitsIface`または`BitsEface`のいずれかを受け取ります。
* `scanp`はインターフェース値の先頭(型情報へのポインタがある場所)を指します。インターフェースのデータ部分へのポインタは`scanp + PtrSize`にあります。
* `inprologue`は、スタックフレームのプロローグ処理中かどうかを示すフラグです。プロローグ中はスタックの状態が不安定なため、スキャンをスキップするなどの安全策が取られます。
* `if(bits == BitsIface)`と`else { // bits == BitsEface }`の分岐により、型付きインターフェースと空インターフェースのどちらであるかを判別します。
* `tab = *(Itab**)scanp;`または`type = *(Type**)scanp;`で、インターフェースが保持する具体的な値の型情報(`Itab`または`_type`)を取得します。
* `if(tab->type->size <= sizeof(void*) && (tab->type->kind & KindNoPointers))`の条件は非常に重要です。これは、インターフェースが保持する値が、ポインタを含まない(`KindNoPointers`)かつポインタサイズ以下(`size <= sizeof(void*)`)の小さな型である場合に`return;`でスキャンをスキップします。例えば、`interface{}`に`int`が格納されている場合、`int`自体はポインタを含まないため、そのデータ部分をスキャンする必要はありません。この最適化により、GCの効率が向上します。
* `addroot((Obj){scanp+PtrSize, PtrSize, 0});`は、インターフェースのデータ部分が指す実際のオブジェクトをGCルートとして追加します。これにより、インターフェースを介して参照されているオブジェクトがGCによって生存していると判断され、回収されなくなります。
3. **`scanbitvector`関数の変更**:
この関数は、ビットマップを走査してポインタを識別するGCの主要なスキャン関数です。
* `bits = word & 3;`で、現在のワードに対応する2ビットのポインタ情報を抽出します。
* `if(bits != BitsNoPointer && *(void**)scanp != nil)`: ポインタではないとマークされていない、かつ値が`nil`ではない場合にのみ処理を進めます。
* `if(bits == BitsPointer)`: 抽出したビットが通常のポインタを示す場合、`addroot`でそのポインタをGCルートに追加します。
* `else`: 抽出したビットが`BitsIface`または`BitsEface`(つまりインターフェース)を示す場合、新しく追加された`scaninterfacedata`関数を呼び出します。これにより、インターフェースのデータ部分が型情報に基づいて適切にスキャンされるようになります。
* `scanbitvector`が`inprologue`引数を受け取るようになったことで、スタックフレームのスキャン時に、プロローグの完了状態に応じた安全なスキャンが可能になります。
4. **`addframeroots`関数の変更**:
この関数は、スタックフレーム上のローカル変数と引数をGCルートとして追加する役割を担います。
* `bool afterprologue = (frame->varp > (byte*)frame->sp);`という行が追加され、関数のプロローグが完了しているかどうかを判断するフラグが導入されました。`frame->varp`はローカル変数の開始アドレス、`frame->sp`はスタックポインタを示します。プロローグが完了すると`frame->varp`が`frame->sp`よりも大きいアドレスを指すようになります。
* `scanbitvector`を呼び出す際に、この`afterprologue`フラグに基づいて`inprologue`引数を渡すようになりました。ローカル変数のスキャン時には`false`(プロローグ後)、引数のスキャン時には`!afterprologue`(プロローグ中またはプロローグ前)を渡すことで、スタックの状態に応じた適切なスキャンタイミングを保証します。
これらの変更は、Goのガベージコレクタがインターフェース値を扱う際の精度と効率を大幅に向上させ、Goプログラムの全体的なパフォーマンスと安定性に貢献します。
## 関連リンク
* Go言語のガベージコレクションに関する公式ドキュメントやブログ記事(当時のものがあれば)
* Goのインターフェースの内部構造に関する解説記事
* Goのランタイムソースコード(特に`src/pkg/runtime/mgc0.c`)
## 参考にした情報源リンク
* Go言語のソースコード(`src/cmd/gc/pgen.c`, `src/pkg/runtime/mgc0.c`)
* Goのガベージコレクションに関する一般的な知識
* Goのインターフェースの内部表現に関する一般的な知識
* [Goのインターフェースの内部構造に関する解説記事 (例: The Laws of Reflection)](https://go.dev/blog/laws-of-reflection)
* [Goのガベージコレクションに関する解説記事 (例: Go's new GC in Go 1.5)](https://go.dev/blog/go15gc)
* [Goの型システムとランタイムに関する詳細な解説 (例: Go Data Structures)](https://go.dev/src/runtime/type.go)
* [GoのGCビットマップに関する解説 (例: Go's runtime and garbage collection)](https://go.dev/src/runtime/mgc.go)
* [Goのコミット履歴](https://github.com/golang/go/commits/master)
* [Goのコードレビューシステム (Gerrit)](https://go-review.googlesource.com/c/go/+/12785045) (コミットメッセージに記載されているCLリンク)
# [インデックス 17325] ファイルの概要
このコミットは、Go言語のガベージコレクタ(GC)がインターフェース値をスキャンする際の挙動を改善し、より正確かつ効率的に型情報を使用するように変更を加えるものです。具体的には、インターフェースが保持する実際の値の型情報に基づいて、ポインタのスキャンを最適化します。これにより、GCの精度とパフォーマンスが向上します。
## コミット
commit 21ea5103a4dab347baeb497675e0786424d6c974 Author: Carl Shapiro cshapiro@google.com Date: Mon Aug 19 10:19:59 2013 -0700
cmd/gc, runtime: use type information to scan interface values
R=golang-dev, rsc, dvyukov
CC=golang-dev
https://golang.org/cl/12785045
## GitHub上でのコミットページへのリンク
[https://github.com/golang/go/commit/21ea5103a4dab347baeb497675e0786424d6c974](https://github.com/golang/go/commit/21ea5103a4dab347baeb497675e0786424d6c974)
## 元コミット内容
cmd/gc, runtime: use type information to scan interface values
## 変更の背景
Go言語のガベージコレクタは、メモリ上のポインタを正確に識別し、到達可能なオブジェクトをマークすることで、不要になったメモリを解放します。インターフェースはGoの強力な機能の一つですが、その内部表現は「型情報」と「値(データ)へのポインタ」のペアで構成されています。
従来のGCスキャンでは、インターフェース値のデータ部分が常にポインタとして扱われるか、あるいは型情報が十分に活用されずにスキャンされる可能性がありました。これにより、以下のような問題が生じる可能性がありました。
1. **過剰なスキャン (Over-scanning)**: インターフェースがポインタを含まない小さな値(例: `int`, `bool`)を保持している場合でも、そのデータ部分をポインタとしてスキャンしようとすることで、無駄なCPUサイクルを消費する。
2. **不正確なスキャン (Under-scanning)**: インターフェースが実際にポインタを含む複雑な構造体などを保持しているにもかかわらず、そのポインタが適切に識別されず、GCによって誤って解放されてしまう(Use-After-Freeなどのバグにつながる)リスク。
このコミットは、これらの問題を解決し、GCがインターフェース値をスキャンする際に、その内部の型情報をより詳細に利用することで、スキャンの精度と効率を向上させることを目的としています。
## 前提知識の解説
このコミットを理解するためには、以下のGo言語の内部構造とガベージコレクションに関する知識が必要です。
1. **Goのインターフェースの内部表現**:
* Goのインターフェース値は、内部的に2つのワード(通常はポインタサイズ)で構成されます。
* 1つ目のワードは「型情報 (type descriptor)」へのポインタです。これは、インターフェースが現在保持している具体的な値の型(例: `*MyStruct`, `string`, `int`など)を記述します。
* 2つ目のワードは「データ (data)」へのポインタです。これは、インターフェースが保持している実際の値がメモリ上のどこにあるかを示します。値がポインタサイズに収まる場合は直接データが格納されることもありますが、通常はヒープ上のオブジェクトへのポインタとなります。
* `interface{}`(空インターフェース)と`interface{ T }`(型付きインターフェース)で、型情報の表現方法が若干異なりますが、基本的な構造は同じです。空インターフェースは`_type`構造体へのポインタを、型付きインターフェースは`itab`構造体へのポインタを型情報として持ちます。
2. **Goのガベージコレクション (GC)**:
* GoのGCは、主にマーク&スイープ方式を採用しています。
* **マークフェーズ**: プログラムの実行中に到達可能なすべてのオブジェクトを識別し、マークします。この際、スタック、グローバル変数、レジスタなどから「ルートポインタ」をたどり、それらが指すオブジェクトをマークし、さらにそれらのオブジェクトが指すオブジェクトを再帰的にマークしていきます。
* **スキャン**: GCがメモリ領域を走査し、ポインタを識別してマーク対象のオブジェクトを見つけるプロセスです。このスキャンは、スタックフレーム、ヒープ上のオブジェクト、グローバルデータなど、様々なメモリ領域に対して行われます。
* **ポインタマップ/ビットマップ**: GoのGCは、メモリ領域内のどこにポインタが存在するかを示す「ポインタマップ」または「ビットマップ」を利用します。これにより、GCはメモリ全体をバイト単位でスキャンするのではなく、ポインタが存在する可能性のある場所だけを効率的にチェックできます。`mgc0.c`に見られる`BitVector`はその一例です。
3. **`src/cmd/gc/pgen.c`と`src/pkg/runtime/mgc0.c`**:
* `src/cmd/gc/pgen.c`: Goコンパイラの一部であり、主にポインタ生成(pointer generation)に関連する処理、特にGCが利用する型情報やポインタマップの生成に関わっています。
* `src/pkg/runtime/mgc0.c`: Goランタイムの一部であり、ガベージコレクタのコアロジックが実装されています。メモリのマーク、スキャン、解放などの処理が含まれます。
## 技術的詳細
このコミットの核心は、GCがインターフェース値をスキャンする際に、より詳細な型情報(ポインタの有無など)を利用するように変更された点です。
1. **新しいポインタビットの導入**:
`src/pkg/runtime/mgc0.c`において、`BitsPerPointer`(ポインタあたりのビット数)の定義が拡張され、以下の新しい定数が導入されました。
* `BitsNoPointer = 0`: ポインタではないことを示す。
* `BitsPointer = 1`: 通常のポインタであることを示す。
* `BitsIface = 2`: 型付きインターフェース(`interface{ T }`)であることを示す。
* `BitsEface = 3`: 空インターフェース(`interface{}`)であることを示す。
これにより、GCはメモリ上の各ワードが「ポインタではない」「通常のポインタ」「インターフェース値」のいずれであるかを、ビットマップを通じて識別できるようになります。
2. **インターフェースデータスキャン専用関数の追加**:
`scaninterfacedata`という新しい静的関数が`src/pkg/runtime/mgc0.c`に追加されました。この関数は、インターフェース値の「データ部分」をスキャンするために特化しています。
* `BitsIface`または`BitsEface`のビットが設定されている場合、この関数が呼び出されます。
* 関数内では、インターフェースが保持する具体的な値の型情報(`Itab`または`_type`)にアクセスします。
* `tab->type->size <= sizeof(void*) && (tab->type->kind & KindNoPointers)`という条件で、インターフェースが保持する値がポインタを含まない小さな型(例: `int`, `bool`, `string`のデータ部分など)であるかをチェックします。もしそうであれば、そのデータ部分にはポインタが含まれないため、スキャンをスキップして効率化を図ります。
* それ以外の場合、`addroot((Obj){scanp+PtrSize, PtrSize, 0});`を呼び出し、インターフェースのデータ部分が指すオブジェクトをGCルートとして追加します。これにより、インターフェースが参照している実際のオブジェクトがGCによってマークされ、生存が保証されます。
3. **`scanbitvector`関数の変更**:
`scanbitvector`関数は、メモリ領域をビットマップに基づいてスキャンする汎用的な関数です。この関数が変更され、新しいポインタビット(`BitsIface`, `BitsEface`)を認識するようになりました。
* 以前は`if(w & 3)`のように、単にビットが立っているかどうかでポインタを判断していましたが、変更後は`bits = word & 3;`で2ビットの情報を抽出し、`BitsPointer`、`BitsIface`、`BitsEface`のいずれであるかを判別します。
* `BitsPointer`であれば従来通り`addroot`を呼び出します。
* `BitsIface`または`BitsEface`であれば、新しく追加された`scaninterfacedata`関数を呼び出し、インターフェースのデータ部分の型に応じたスキャンを行います。
4. **`addframeroots`関数の変更**:
`addframeroots`関数は、スタックフレーム上のローカル変数や引数からGCルートを追加する役割を担います。
* `scanbitvector`関数が`inprologue`という新しい引数を取るようになったため、`addframeroots`もこの引数を適切に渡すように変更されました。
* `afterprologue`というフラグが導入され、関数のプロローグ(スタックフレームのセットアップ部分)が完了しているかどうかに応じて、`scanbitvector`に渡す`inprologue`の値が決定されます。これは、スタックフレームが完全に構築される前にスキャンを行うと、不正確な結果を招く可能性があるため、そのタイミングを制御するためのものです。
5. **`src/cmd/gc/pgen.c`の変更**:
`src/cmd/gc/pgen.c`からは、`bvset(bv, ((*xoffset + widthptr) / widthptr) * BitsPerPointer);`という行が削除されました。これは、コンパイラがポインタマップを生成する際の方法が、より洗練されたインターフェース値の型情報に基づくアプローチに置き換えられたことを示唆しています。古い、より一般的なポインタマーキングのロジックが不要になったと考えられます。
これらの変更により、GoのGCはインターフェース値をより正確に、かつ効率的に処理できるようになり、ガベージコレクションのオーバーヘッド削減と信頼性向上に貢献します。
## コアとなるコードの変更箇所
### `src/cmd/gc/pgen.c`
```diff
--- a/src/cmd/gc/pgen.c
+++ b/src/cmd/gc/pgen.c
@@ -257,7 +257,6 @@ walktype1(Type *t, vlong *xoffset, Bvec *bv)
tbvset(bv, ((*xoffset / widthptr) * BitsPerPointer) + 1);
if(isnilinter(t))
tbvset(bv, ((*xoffset / widthptr) * BitsPerPointer));
- tbvset(bv, ((*xoffset + widthptr) / widthptr) * BitsPerPointer);
*xoffset += t->width;
break;
src/pkg/runtime/mgc0.c
--- a/src/pkg/runtime/mgc0.c
+++ b/src/pkg/runtime/mgc0.c
@@ -36,6 +36,10 @@ enum {
// Pointer map
BitsPerPointer = 2,
+ BitsNoPointer = 0,
+ BitsPointer = 1,
+ BitsIface = 2,
+ BitsEface = 3,
};
// Bits in per-word bitmap.
@@ -1398,26 +1402,52 @@ struct BitVector
uint32 data[];
};
+// Scans an interface data value when the interface type indicates
+// that it is a pointer.
+static void
+scaninterfacedata(uintptr bits, byte *scanp, bool inprologue)
+{
+ Itab *tab;
+ Type *type;
+
+ if(!inprologue) {
+ 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});
+}
+
// Starting from scanp, scans words corresponding to set bits.
static void
-scanbitvector(byte *scanp, BitVector *bv)\n {
-\tuint32 *wp;\n-\tuint32 w;\n+\tuintptr word, bits;\n+\tuint32 *wordp;\n \tint32 i, remptrs;\n \n-\twp = bv->data;\n+\twordp = bv->data;\n \tfor(remptrs = bv->n; remptrs > 0; remptrs -= 32) {\n-\t\tw = *wp++;\n+\t\tword = *wordp++;\n \t\tif(remptrs < 32)\n \t\t\ti = remptrs;\n \t\telse\n \t\t\ti = 32;\n \t\ti /= BitsPerPointer;\n \t\tfor(; i > 0; i--) {\n-\t\t\tif(w & 3)\n-\t\t\t\taddroot((Obj){scanp, PtrSize, 0});\n-\t\t\tw >>= BitsPerPointer;\n+\t\t\tbits = word & 3;\n+\t\t\tif(bits != BitsNoPointer && *(void**)scanp != nil)\n+\t\t\t\tif(bits == BitsPointer)\n+\t\t\t\t\taddroot((Obj){scanp, PtrSize, 0});\n+\t\t\t\telse\n+\t\t\t\t\tscaninterfacedata(bits, scanp, inprologue);\n+\t\t\tword >>= BitsPerPointer;\n \t\t\tscanp += PtrSize;\n \t\t}\n \t}\n@@ -1430,12 +1460,14 @@ addframeroots(Stkframe *frame, void*)\n \tFunc *f;\n \tBitVector *args, *locals;\n \tuintptr size;\n+\tbool afterprologue;\n \n \tf = frame->fn;\n \n \t// Scan local variables if stack frame has been allocated.\n \t// Use pointer information if known.\n-\tif(frame->varp > (byte*)frame->sp) {\n+\tafterprologue = (frame->varp > (byte*)frame->sp);\n+\tif(afterprologue) {\n \t\tlocals = runtime·funcdata(f, FUNCDATA_GCLocals);\n \t\tif(locals == nil) {\n \t\t\t// No locals information, scan everything.\n@@ -1450,7 +1482,7 @@ addframeroots(Stkframe *frame, void*)\n \t\t\t// pointers in locals.\n \t\t\tsize = (locals->n*PtrSize) / BitsPerPointer;\n-\t\t\tscanbitvector(frame->varp - size, locals);\n+\t\t\tscanbitvector(frame->varp - size, locals, false);\n \t\t}\n \t}\n \n@@ -1458,7 +1490,7 @@ addframeroots(Stkframe *frame, void*)\n \t// Use pointer information if known.\n \targs = runtime·funcdata(f, FUNCDATA_GCArgs);\n \tif(args != nil && args->n > 0)\n-\t\tscanbitvector(frame->argp, args);\n+\t\tscanbitvector(frame->argp, args, !afterprologue);\n \telse\n \t\taddroot((Obj){frame->argp, frame->arglen, 0});\n }\n```
## コアとなるコードの解説
### `src/cmd/gc/pgen.c`の変更
削除された行 `tbvset(bv, ((*xoffset + widthptr) / widthptr) * BitsPerPointer);` は、コンパイラがポインタビットマップを生成する際の一部のロジックでした。この行が削除されたのは、インターフェース値のスキャン方法が`mgc0.c`側でより詳細に制御されるようになったため、コンパイラ側での汎用的なポインタマーキングが不要になったか、あるいはより正確な方法に置き換えられたためと考えられます。これは、GCがインターフェースのデータ部分を「常にポインタとして扱う」のではなく、「型情報に基づいて判断する」という新しいアプローチと整合しています。
### `src/pkg/runtime/mgc0.c`の変更
1. **`enum`の拡張**:
`BitsNoPointer`, `BitsPointer`, `BitsIface`, `BitsEface`の追加は、GCがメモリ上の各ワードをスキャンする際に、そのワードが指すものが「ポインタではない」「通常のポインタ」「型付きインターフェース」「空インターフェース」のいずれであるかを、ビットマップを通じて識別するための新しい分類を提供します。これにより、GCはより粒度の高い情報に基づいてスキャン戦略を決定できます。
2. **`scaninterfacedata`関数の追加**:
この関数は、インターフェース値のデータ部分をスキャンするための新しい専用ロジックです。
* `uintptr bits`引数は、`BitsIface`または`BitsEface`のいずれかを受け取ります。
* `scanp`はインターフェース値の先頭(型情報へのポインタがある場所)を指します。インターフェースのデータ部分へのポインタは`scanp + PtrSize`にあります。
* `inprologue`は、スタックフレームのプロローグ処理中かどうかを示すフラグです。プロローグ中はスタックの状態が不安定なため、スキャンをスキップするなどの安全策が取られます。
* `if(bits == BitsIface)`と`else { // bits == BitsEface }`の分岐により、型付きインターフェースと空インターフェースのどちらであるかを判別します。
* `tab = *(Itab**)scanp;`または`type = *(Type**)scanp;`で、インターフェースが保持する具体的な値の型情報(`Itab`または`_type`)を取得します。
* `if(tab->type->size <= sizeof(void*) && (tab->type->kind & KindNoPointers))`の条件は非常に重要です。これは、インターフェースが保持する値が、ポインタを含まない(`KindNoPointers`)かつポインタサイズ以下(`size <= sizeof(void*)`)の小さな型である場合に`return;`でスキャンをスキップします。例えば、`interface{}`に`int`が格納されている場合、`int`自体はポインタを含まないため、そのデータ部分をスキャンする必要はありません。この最適化により、GCの効率が向上します。
* `addroot((Obj){scanp+PtrSize, PtrSize, 0});`は、インターフェースのデータ部分が指す実際のオブジェクトをGCルートとして追加します。これにより、インターフェースを介して参照されているオブジェクトがGCによって生存していると判断され、回収されなくなります。
3. **`scanbitvector`関数の変更**:
この関数は、ビットマップを走査してポインタを識別するGCの主要なスキャン関数です。
* `bits = word & 3;`で、現在のワードに対応する2ビットのポインタ情報を抽出します。
* `if(bits != BitsNoPointer && *(void**)scanp != nil)`: ポインタではないとマークされていない、かつ値が`nil`ではない場合にのみ処理を進めます。
* `if(bits == BitsPointer)`: 抽出したビットが通常のポインタを示す場合、`addroot`でそのポインタをGCルートに追加します。
* `else`: 抽出したビットが`BitsIface`または`BitsEface`(つまりインターフェース)を示す場合、新しく追加された`scaninterfacedata`関数を呼び出します。これにより、インターフェースのデータ部分が型情報に基づいて適切にスキャンされるようになります。
* `scanbitvector`が`inprologue`引数を受け取るようになったことで、スタックフレームのスキャン時に、プロローグの完了状態に応じた安全なスキャンが可能になります。
4. **`addframeroots`関数の変更**:
この関数は、スタックフレーム上のローカル変数と引数をGCルートとして追加する役割を担います。
* `bool afterprologue = (frame->varp > (byte*)frame->sp);`という行が追加され、関数のプロローグが完了しているかどうかを判断するフラグが導入されました。`frame->varp`はローカル変数の開始アドレス、`frame->sp`はスタックポインタを示します。プロローグが完了すると`frame->varp`が`frame->sp`よりも大きいアドレスを指すようになります。
* `scanbitvector`を呼び出す際に、この`afterprologue`フラグに基づいて`inprologue`引数を渡すようになりました。ローカル変数のスキャン時には`false`(プロローグ後)、引数のスキャン時には`!afterprologue`(プロローグ中またはプロローグ前)を渡すことで、スタックの状態に応じた適切なスキャンタイミングを保証します。
これらの変更は、Goのガベージコレクタがインターフェース値を扱う際の精度と効率を大幅に向上させ、Goプログラムの全体的なパフォーマンスと安定性に貢献します。
## 関連リンク
* Go言語のガベージコレクションに関する公式ドキュメントやブログ記事(当時のものがあれば)
* Goのインターフェースの内部構造に関する解説記事
* Goのランタイムソースコード(特に`src/pkg/runtime/mgc0.c`)
## 参考にした情報源リンク
* Go言語のソースコード(`src/cmd/gc/pgen.c`, `src/pkg/runtime/mgc0.c`)
* Goのガベージコレクションに関する一般的な知識
* Goのインターフェースの内部表現に関する一般的な知識
* [Goのインターフェースの内部構造に関する解説記事 (例: The Laws of Reflection)](https://go.dev/blog/laws-of-reflection)
* [Goのガベージコレクションに関する解説記事 (例: Go's new GC in Go 1.5)](https://go.dev/blog/go15gc)
* [Goの型システムとランタイムに関する詳細な解説 (例: Go Data Structures)](https://go.dev/src/runtime/type.go)
* [GoのGCビットマップに関する解説 (例: Go's runtime and garbage collection)](https://go.dev/src/runtime/mgc.go)
* [Goのコミット履歴](https://github.com/golang/go/commits/master)
* [Goのコードレビューシステム (Gerrit)](https://go-review.googlesource.com/c/go/+/12785045) (コミットメッセージに記載されているCLリンク)