[インデックス 15799] ファイルの概要
このコミットは、Go言語のランタイムにおけるガベージコレクション(GC)の動作を改善するものです。具体的には、インターフェース型の値が持つ型情報とデータポインタの走査(スキャン)方法を修正し、GCがヒープ上のオブジェクトをより正確に追跡できるようにします。これにより、メモリリークや誤ったメモリ解放を防ぎ、GCの堅牢性を向上させます。
コミット
commit 92c153d5f4fcc6aac88916f4b21186f8428fdc26
Author: Jan Ziak <0xe2.0x9a.0x9b@gmail.com>
Date: Fri Mar 15 16:07:52 2013 -0400
runtime: scan the type of an interface value
R=golang-dev, rsc, bradfitz
CC=golang-dev
https://golang.org/cl/7744047
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/92c153d5f4fcc6aac88916f4b21186f8428fdc26
元コミット内容
runtime: scan the type of an interface value
R=golang-dev, rsc, bradfitz
CC=golang-dev
https://golang.org/cl/7744047
変更の背景
Go言語のガベージコレクタは、ヒープ上に割り当てられたオブジェクトのうち、プログラムから到達可能なもの(参照されているもの)を特定し、到達不可能なオブジェクトを解放することでメモリを管理します。この「到達可能性」を判断するためには、スタックやレジスタ、グローバル変数などから始まるポインタを辿って、ヒープ上のオブジェクトをスキャンしていく必要があります。
インターフェースはGo言語の重要な機能であり、その内部表現はポインタを含んでいます。具体的には、空のインターフェース(interface{}
)はEface
構造体として、メソッドを持つインターフェースはIface
構造体としてランタイムで扱われます。これらの構造体は、格納されている値の型情報へのポインタと、実際のデータへのポインタ(またはデータそのもの)を含んでいます。
このコミット以前のGCの実装では、インターフェース値のスキャンにおいて、特定の条件下で不正確な動作をする可能性がありました。特に、インターフェースが保持する値の型が「ポインタを含まない型」(KindNoPointers
フラグが設定されている型、例えばint
やbool
など)である場合に、GCがインターフェースのデータ部分の走査を早期に打ち切ってしまう可能性がありました。しかし、インターフェースのデータ部分自体がヒープ上の別のオブジェクトへのポインタである場合(例えば、interface{}
に*int
やstring
が格納されている場合)、そのポインタを辿ってスキャンを継続する必要があります。
この不正確なスキャンは、本来到達可能なオブジェクトを到達不可能と誤って判断し、早期に解放してしまう(Use-After-Freeバグの原因となる)か、あるいは到達不可能なオブジェクトを解放せずに残してしまう(メモリリークの原因となる)可能性がありました。このコミットは、この問題を解決し、インターフェース値の型情報とデータポインタの両方を確実にスキャンすることで、GCの正確性と堅牢性を向上させることを目的としています。
前提知識の解説
Go言語のインターフェースの内部表現
Go言語のインターフェースは、コンパイル時に型が決定されない動的な型情報を持つことができます。ランタイム内部では、インターフェースは通常、2つのワード(ポインタサイズ)で表現されます。
-
空のインターフェース (
interface{}
):Eface
構造体として表現されます。type
ポインタ: 格納されている値の動的な型情報(_type
構造体)へのポインタ。data
ポインタ: 格納されている実際の値へのポインタ。値がポインタサイズ以下の場合、data
フィールドに直接値が格納されることもあります(インライン化)。
-
メソッドを持つインターフェース:
Iface
構造体として表現されます。tab
ポインタ: インターフェーステーブル(itab
構造体)へのポインタ。itab
は、インターフェースの型情報と、そのインターフェースが要求するメソッドの実装へのポインタを含みます。data
ポインタ: 格納されている実際の値へのポインタ。
ガベージコレクタは、これらのtype
、tab
、data
の各ポインタがヒープ上の有効なオブジェクトを指しているかどうかを判断し、指している場合はそのオブジェクトを「到達可能」としてマークし、さらにそのオブジェクトが持つポインタを辿ってスキャンを続けます。
Goランタイムとガベージコレクション (GC)
Goのガベージコレクタは、並行(concurrent)かつ三色マーク&スイープ(tri-color mark-and-sweep)方式を採用しています。GCのサイクルは以下のフェーズで構成されます。
- マークフェーズ: プログラムのルート(スタック、グローバル変数など)から到達可能なすべてのオブジェクトをマークします。この際、ポインタを辿ってヒープ上のオブジェクトを走査します。
- スイープフェーズ: マークされなかったオブジェクト(到達不可能なオブジェクト)をメモリから解放します。
src/pkg/runtime/mgc0.c
ファイルは、Goランタイムのメモリ管理とガベージコレクションの初期実装に関連するコードを含んでいます。このファイル内のscanblock
関数は、GCのマークフェーズにおいて、特定のメモリブロック(例えばスタックフレーム)を走査し、そこに含まれるポインタを識別して、それらが指すヒープ上のオブジェクトをマークする役割を担っています。
KindNoPointers
Goの型システムには、その型がポインタを含むかどうかを示すKindNoPointers
というフラグがあります。これはGCの最適化に利用されます。例えば、int
やbool
のようなプリミティブ型はポインタを含まないため、KindNoPointers
が設定されます。GCは、KindNoPointers
が設定された型のオブジェクトの内部を再帰的にスキャンする必要がないと判断できます。
しかし、重要なのは、KindNoPointers
が設定された型の値そのものが、別のヒープオブジェクトへのポインタとして扱われる可能性があるという点です。例えば、interface{}
にint
が格納されている場合、Eface.data
にはint
の値が直接格納されることが多いですが、*int
が格納されている場合はEface.data
はヒープ上のint
へのポインタとなります。この場合、int
型自体はKindNoPointers
ですが、Eface.data
が指す先はスキャン対象となります。
技術的詳細
このコミットの技術的な核心は、src/pkg/runtime/mgc0.c
内のscanblock
関数におけるGC_EFACE
(空のインターフェース)とGC_IFACE
(メソッドを持つインターフェース)の処理ロジックの変更です。
GC_EFACE
(空のインターフェース) の変更点
変更前は、eface->type
がnil
でないこと、かつeface->data
がヒープ領域内にある場合にのみ、eface->data
のスキャンが試みられていました。さらに、eface->type
がKindNoPointers
を持つ型である場合、break
文によってGC_EFACE
ケースの処理が中断され、eface->data
が指す先のオブジェクトが適切にスキャンされない可能性がありました。
変更後は、以下の点が改善されています。
eface->type == nil
の早期チェック:eface->type
がnil
の場合(インターフェースがnil
値である場合)、すぐにcontinue
して次のスキャン対象に移ります。これにより、不要な処理をスキップします。eface->type
自体のスキャン:eface->type
ポインタがヒープ領域内を指している場合、そのポインタ自体をGCの対象としてptrbuf
に追加し、スキャンキューに投入します。これは、型情報オブジェクトもヒープ上に存在し、GCの対象となる可能性があるためです。KindNoPointers
時のbreak
からcontinue
への変更: 最も重要な変更点です。eface->data
が指す値の型がKindNoPointers
を持つ場合でも、break
ではなくcontinue
を使用するように変更されました。これにより、GC_EFACE
ケースの処理が中断されることなく、scanblock
関数のメインループが次のスキャン対象へと進みます。これは、eface->data
自体がヒープ上のポインタである可能性を考慮したものです。たとえ格納されている値の型がポインタを持たなくても、その値がヒープに割り当てられていれば、eface->data
はそのヒープ上のアドレスを指すため、GCはそのアドレスを辿る必要があります。
GC_IFACE
(メソッドを持つインターフェース) の変更点
GC_EFACE
と同様に、iface->tab == nil
の場合や、iface->tab->type
がKindNoPointers
を持つ型である場合に、break
ではなくcontinue
を使用するように変更されました。これにより、GC_IFACE
ケースの処理が中断されることなく、iface->data
が指す先のオブジェクトが適切にスキャンされるようになります。また、iface->tab
ポインタ自体もヒープ上のオブジェクトを指す可能性があるため、eface->type
と同様に明示的にスキャン対象として追加されます。
GC_END
の変更点
GC_END
ケースでは、スタックフレームの走査におけるポインタ計算がわずかに最適化されました。elemsize
という一時変数を使用せず、stack_top.elemsize
を直接使用するように変更されています。これは機能的な変更ではなく、コードの簡素化と効率化を目的としたものです。
scanblock
関数の一般的な変更
scanblock
関数のメインループの最後で、objbufpos == objbuf_end
の場合にbreak
からcontinue
に変更されています。これは、オブジェクトバッファが満杯になった際にフラッシュした後も、scanblock
のループが継続して次のスキャン対象を処理できるようにするための修正です。
これらの変更により、GoのGCはインターフェースを介したポインタをより正確に追跡できるようになり、メモリ管理の信頼性が向上しました。
コアとなるコードの変更箇所
変更はすべて src/pkg/runtime/mgc0.c
ファイル内で行われています。
--- a/src/pkg/runtime/mgc0.c
+++ b/src/pkg/runtime/mgc0.c
@@ -754,11 +754,22 @@ scanblock(Workbuf *wbuf, Obj *wp, uintptr nobj, bool keepworking)
case GC_EFACE:
eface = (Eface*)(stack_top.b + pc[1]);
pc += 2;
- if(eface->type != nil && (eface->data >= arena_start && eface->data < arena_used)) {
- t = eface->type;
+ if(eface->type == nil)
+ continue;
+
+ // eface->type
+ t = eface->type;
+ if((void*)t >= arena_start && (void*)t < arena_used) {
+ *ptrbufpos++ = (PtrTarget){t, 0};
+ if(ptrbufpos == ptrbuf_end)
+ flushptrbuf(ptrbuf, &ptrbufpos, &wp, &wbuf, &nobj);
+ }
+
+ // eface->data
+ if(eface->data >= arena_start && eface->data < arena_used) {
if(t->size <= sizeof(void*)) {
if((t->kind & KindNoPointers))\
- break;
+ continue;
obj = eface->data;
if((t->kind & ~KindNoPointers) == KindPtr)\
@@ -774,7 +785,7 @@ scanblock(Workbuf *wbuf, Obj *wp, uintptr nobj, bool keepworking)
iface = (Iface*)(stack_top.b + pc[1]);
pc += 2;
if(iface->tab == nil)
- break;
+ continue;
// iface->tab
if((void*)iface->tab >= arena_start && (void*)iface->tab < arena_used) {
@@ -788,7 +799,7 @@ scanblock(Workbuf *wbuf, Obj *wp, uintptr nobj, bool keepworking)
t = iface->tab->type;
if(t->size <= sizeof(void*)) {
if((t->kind & KindNoPointers))\
- break;
+ continue;
obj = iface->data;
if((t->kind & ~KindNoPointers) == KindPtr)\
@@ -815,9 +826,8 @@ scanblock(Workbuf *wbuf, Obj *wp, uintptr nobj, bool keepworking)
case GC_END:
if(--stack_top.count != 0) {
// Next iteration of a loop if possible.
- elemsize = stack_top.elemsize;
- stack_top.b += elemsize;
- if(stack_top.b + elemsize <= end_b+PtrSize) {
+ stack_top.b += stack_top.elemsize;
+ if(stack_top.b + stack_top.elemsize <= end_b+PtrSize) {
pc = stack_top.loop_or_ret;
continue;
}
@@ -945,7 +955,7 @@ scanblock(Workbuf *wbuf, Obj *wp, uintptr nobj, bool keepworking)
*objbufpos++ = (Obj){obj, size, objti};\
if(objbufpos == objbuf_end)\
flushobjbuf(objbuf, &objbufpos, &wp, &wbuf, &nobj);\
- break;
+ continue;
case GC_CHAN:
// There are no heap pointers in struct Hchan,
コアとなるコードの解説
GC_EFACE
および GC_IFACE
の変更
-
if(eface->type == nil) continue;
(GC_EFACE) およびif(iface->tab == nil) continue;
(GC_IFACE):- インターフェースが
nil
値である場合(つまり、type
ポインタやtab
ポインタがnil
である場合)、そのインターフェースは何も参照していないため、それ以上スキャンする必要はありません。以前はbreak
していましたが、continue
に変更することで、scanblock
のメインループが次のスキャン対象へと適切に進むようになります。
- インターフェースが
-
// eface->type
および// iface->tab
の追加スキャンロジック:t = eface->type;
およびt = iface->tab->type;
の後、if((void*)t >= arena_start && (void*)t < arena_used)
の条件で、eface->type
やiface->tab
がヒープ領域内のオブジェクトを指している場合に、それらをptrbuf
(ポインタバッファ)に追加しています。これは、型情報やインターフェーステーブル自体もヒープ上に割り当てられる可能性があり、GCの対象となるため、明示的にスキャン対象としてマークする必要があることを示しています。
-
if((t->kind & KindNoPointers)) break;
からif((t->kind & KindNoPointers)) continue;
への変更:- これがこのコミットの最も重要な変更点です。
KindNoPointers
は、その型の値が内部にポインタを含まないことを示します。例えば、int
やbool
などです。- 変更前は、インターフェースが保持する値の型が
KindNoPointers
である場合、break
文によってGC_EFACE
またはGC_IFACE
のswitch
ケースから抜けていました。これは、その値の内部をスキャンする必要がないため、最適化として行われていたと考えられます。 - しかし、このロジックには問題がありました。たとえ格納されている値の型が
KindNoPointers
であっても、インターフェースのdata
フィールド自体がヒープ上の別のオブジェクトへのポインタである可能性があります。例えば、interface{}
に*int
が格納されている場合、Eface.data
はヒープ上のint
へのポインタを指します。この*int
の型はint
であり、KindNoPointers
ですが、Eface.data
が指す先はGCが追跡すべきヒープオブジェクトです。 break
をcontinue
に変更することで、KindNoPointers
の条件が満たされてもswitch
ケースから抜け出すことなく、scanblock
のメインループの次のイテレーションに進みます。これにより、eface->data
やiface->data
がヒープ上のポインタである場合に、そのポインタが適切にスキャンされる機会が確保されます。
GC_END
の変更
elemsize = stack_top.elemsize; stack_top.b += elemsize;
がstack_top.b += stack_top.elemsize;
に変更されました。- これは、
elemsize
という一時変数への代入を省略し、stack_top.elemsize
を直接使用するようにした、わずかなコードの簡素化と最適化です。機能的な変更はありません。
- これは、
scanblock
関数の一般的な変更
*objbufpos++ = (Obj){obj, size, objti};
の後のbreak;
がcontinue;
に変更されました。- これは、
objbuf
(オブジェクトバッファ)が満杯になった際にflushobjbuf
を呼び出した後、scanblock
関数のメインループが中断されることなく、次のスキャン対象の処理を継続できるようにするための修正です。以前のbreak
では、バッファが満杯になるとループが終了してしまい、残りのスキャン対象が処理されない可能性がありました。
- これは、
これらの変更は、Goのガベージコレクタがインターフェースを介して参照されるヒープオブジェクトをより正確に識別し、追跡できるようにするための重要な改善です。
関連リンク
- Go言語のインターフェースに関する公式ドキュメントやブログ記事
- Goランタイムのガベージコレクションに関する技術記事やGoのソースコード解説
参考にした情報源リンク
- Go言語のソースコード (
src/pkg/runtime/mgc0.c
) - Go言語のガベージコレクションに関する一般的な知識
- Go言語のインターフェースの内部表現に関する情報
- golang/go GitHubリポジトリ
- Gerrit Change-Id: 7744047 (Goのコードレビューシステム)
[インデックス 15799] ファイルの概要
このコミットは、Go言語のランタイムにおけるガベージコレクション(GC)の動作を改善するものです。具体的には、インターフェース型の値が持つ型情報とデータポインタの走査(スキャン)方法を修正し、GCがヒープ上のオブジェクトをより正確に追跡できるようにします。これにより、メモリリークや誤ったメモリ解放を防ぎ、GCの堅牢性を向上させます。
コミット
commit 92c153d5f4fcc6aac88916f4b21186f8428fdc26
Author: Jan Ziak <0xe2.0x9a.0x9b@gmail.com>
Date: Fri Mar 15 16:07:52 2013 -0400
runtime: scan the type of an interface value
R=golang-dev, rsc, bradfitz
CC=golang-dev
https://golang.org/cl/7744047
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/92c153d5f4fcc6aac88916f4b21186f8428fdc26
元コミット内容
runtime: scan the type of an interface value
R=golang-dev, rsc, bradfitz
CC=golang-dev
https://golang.org/cl/7744047
変更の背景
Go言語のガベージコレクタは、ヒープ上に割り当てられたオブジェクトのうち、プログラムから到達可能なもの(参照されているもの)を特定し、到達不可能なオブジェクトを解放することでメモリを管理します。この「到達可能性」を判断するためには、スタックやレジスタ、グローバル変数などから始まるポインタを辿って、ヒープ上のオブジェクトをスキャンしていく必要があります。
インターフェースはGo言語の重要な機能であり、その内部表現はポインタを含んでいます。具体的には、空のインターフェース(interface{}
)はEface
構造体として、メソッドを持つインターフェースはIface
構造体としてランタイムで扱われます。これらの構造体は、格納されている値の型情報へのポインタと、実際のデータへのポインタ(またはデータそのもの)を含んでいます。
このコミット以前のGCの実装では、インターフェース値のスキャンにおいて、特定の条件下で不正確な動作をする可能性がありました。特に、インターフェースが保持する値の型が「ポインタを含まない型」(KindNoPointers
フラグが設定されている型、例えばint
やbool
など)である場合に、GCがインターフェースのデータ部分の走査を早期に打ち切ってしまう可能性がありました。しかし、インターフェースのデータ部分自体がヒープ上の別のオブジェクトへのポインタである場合(例えば、interface{}
に*int
やstring
が格納されている場合)、そのポインタを辿ってスキャンを継続する必要があります。
この不正確なスキャンは、本来到達可能なオブジェクトを到達不可能と誤って判断し、早期に解放してしまう(Use-After-Freeバグの原因となる)か、あるいは到達不可能なオブジェクトを解放せずに残してしまう(メモリリークの原因となる)可能性がありました。このコミットは、この問題を解決し、インターフェース値の型情報とデータポインタの両方を確実にスキャンすることで、GCの正確性と堅牢性を向上させることを目的としています。
前提知識の解説
Go言語のインターフェースの内部表現
Go言語のインターフェースは、コンパイル時に型が決定されない動的な型情報を持つことができます。ランタイム内部では、インターフェースは通常、2つのワード(ポインタサイズ)で表現されます。
-
空のインターフェース (
interface{}
):Eface
構造体として表現されます。type
ポインタ: 格納されている値の動的な型情報(_type
構造体)へのポインタ。data
ポインタ: 格納されている実際の値へのポインタ。値がポインタサイズ以下の場合、data
フィールドに直接値が格納されることもあります(インライン化)。
-
メソッドを持つインターフェース:
Iface
構造体として表現されます。tab
ポインタ: インターフェーステーブル(itab
構造体)へのポインタ。itab
は、インターフェースの型情報と、そのインターフェースが要求するメソッドの実装へのポインタを含みます。data
ポインタ: 格納されている実際の値へのポインタ。
ガベージコレクタは、これらのtype
、tab
、data
の各ポインタがヒープ上の有効なオブジェクトを指しているかどうかを判断し、指している場合はそのオブジェクトを「到達可能」としてマークし、さらにそのオブジェクトが持つポインタを辿ってスキャンを続けます。
Goランタイムとガベージコレクション (GC)
Goのガベージコレクタは、並行(concurrent)かつ三色マーク&スイープ(tri-color mark-and-sweep)方式を採用しています。GCのサイクルは以下のフェーズで構成されます。
- マークフェーズ: プログラムのルート(スタック、グローバル変数など)から到達可能なすべてのオブジェクトをマークします。この際、ポインタを辿ってヒープ上のオブジェクトを走査します。
- スイープフェーズ: マークされなかったオブジェクト(到達不可能なオブジェクト)をメモリから解放します。
src/pkg/runtime/mgc0.c
ファイルは、Goランタイムのメモリ管理とガベージコレクションの初期実装に関連するコードを含んでいます。このファイル内のscanblock
関数は、GCのマークフェーズにおいて、特定のメモリブロック(例えばスタックフレーム)を走査し、そこに含まれるポインタを識別して、それらが指すヒープ上のオブジェクトをマークする役割を担っています。
KindNoPointers
Goの型システムには、その型がポインタを含むかどうかを示すKindNoPointers
というフラグがあります。これはGCの最適化に利用されます。例えば、int
やbool
のようなプリミティブ型はポインタを含まないため、KindNoPointers
が設定されます。GCは、KindNoPointers
が設定された型のオブジェクトの内部を再帰的にスキャンする必要がないと判断できます。
しかし、重要なのは、KindNoPointers
が設定された型の値そのものが、別のヒープオブジェクトへのポインタとして扱われる可能性があるという点です。例えば、interface{}
にint
が格納されている場合、Eface.data
にはint
の値が直接格納されることが多いですが、*int
が格納されている場合はEface.data
はヒープ上のint
へのポインタとなります。この場合、int
型自体はKindNoPointers
ですが、Eface.data
が指す先はスキャン対象となります。
技術的詳細
このコミットの技術的な核心は、src/pkg/runtime/mgc0.c
内のscanblock
関数におけるGC_EFACE
(空のインターフェース)とGC_IFACE
(メソッドを持つインターフェース)の処理ロジックの変更です。
GC_EFACE
(空のインターフェース) の変更点
変更前は、eface->type
がnil
でないこと、かつeface->data
がヒープ領域内にある場合にのみ、eface->data
のスキャンが試みられていました。さらに、eface->type
がKindNoPointers
を持つ型である場合、break
文によってGC_EFACE
ケースの処理が中断され、eface->data
が指す先のオブジェクトが適切にスキャンされない可能性がありました。
変更後は、以下の点が改善されています。
eface->type == nil
の早期チェック:eface->type
がnil
の場合(インターフェースがnil
値である場合)、すぐにcontinue
して次のスキャン対象に移ります。これにより、不要な処理をスキップします。eface->type
自体のスキャン:eface->type
ポインタがヒープ領域内を指している場合、そのポインタ自体をGCの対象としてptrbuf
に追加し、スキャンキューに投入します。これは、型情報オブジェクトもヒープ上に存在し、GCの対象となる可能性があるためです。KindNoPointers
時のbreak
からcontinue
への変更: 最も重要な変更点です。eface->data
が指す値の型がKindNoPointers
を持つ場合でも、break
ではなくcontinue
を使用するように変更されました。これにより、GC_EFACE
ケースの処理が中断されることなく、scanblock
関数のメインループが次のスキャン対象へと進みます。これは、eface->data
自体がヒープ上のポインタである可能性を考慮したものです。たとえ格納されている値の型がポインタを持たなくても、その値がヒープに割り当てられていれば、eface->data
はそのヒープ上のアドレスを指すため、GCはそのアドレスを辿る必要があります。
GC_IFACE
(メソッドを持つインターフェース) の変更点
GC_EFACE
と同様に、iface->tab == nil
の場合や、iface->tab->type
がKindNoPointers
を持つ型である場合に、break
ではなくcontinue
を使用するように変更されました。これにより、GC_IFACE
ケースの処理が中断されることなく、iface->data
が指す先のオブジェクトが適切にスキャンされるようになります。また、iface->tab
ポインタ自体もヒープ上のオブジェクトを指す可能性があるため、eface->type
と同様に明示的にスキャン対象として追加されます。
GC_END
の変更点
GC_END
ケースでは、スタックフレームの走査におけるポインタ計算がわずかに最適化されました。elemsize
という一時変数を使用せず、stack_top.elemsize
を直接使用するように変更されています。これは機能的な変更ではなく、コードの簡素化と効率化を目的としています。
scanblock
関数の一般的な変更
scanblock
関数のメインループの最後で、objbufpos == objbuf_end
の場合にbreak
からcontinue
に変更されています。これは、オブジェクトバッファが満杯になった際にフラッシュした後も、scanblock
のループが継続して次のスキャン対象を処理できるようにするための修正です。
これらの変更により、GoのGCはインターフェースを介したポインタをより正確に追跡できるようになり、メモリ管理の信頼性が向上しました。
コアとなるコードの変更箇所
変更はすべて src/pkg/runtime/mgc0.c
ファイル内で行われています。
--- a/src/pkg/runtime/mgc0.c
+++ b/src/pkg/runtime/mgc0.c
@@ -754,11 +754,22 @@ scanblock(Workbuf *wbuf, Obj *wp, uintptr nobj, bool keepworking)
case GC_EFACE:
eface = (Eface*)(stack_top.b + pc[1]);
pc += 2;
- if(eface->type != nil && (eface->data >= arena_start && eface->data < arena_used)) {
- t = eface->type;
+ if(eface->type == nil)
+ continue;
+
+ // eface->type
+ t = eface->type;
+ if((void*)t >= arena_start && (void*)t < arena_used) {
+ *ptrbufpos++ = (PtrTarget){t, 0};
+ if(ptrbufpos == ptrbuf_end)
+ flushptrbuf(ptrbuf, &ptrbufpos, &wp, &wbuf, &nobj);
+ }
+
+ // eface->data
+ if(eface->data >= arena_start && eface->data < arena_used) {
if(t->size <= sizeof(void*)) {
if((t->kind & KindNoPointers))\
- break;
+ continue;
obj = eface->data;
if((t->kind & ~KindNoPointers) == KindPtr)\
@@ -774,7 +785,7 @@ scanblock(Workbuf *wbuf, Obj *wp, uintptr nobj, bool keepworking)
iface = (Iface*)(stack_top.b + pc[1]);
pc += 2;
if(iface->tab == nil)
- break;
+ continue;
// iface->tab
if((void*)iface->tab >= arena_start && (void*)iface->tab < arena_used) {
@@ -788,7 +799,7 @@ scanblock(Workbuf *wbuf, Obj *wp, uintptr nobj, bool keepworking)
t = iface->tab->type;
if(t->size <= sizeof(void*)) {
if((t->kind & KindNoPointers))\
- break;
+ continue;
obj = iface->data;
if((t->kind & ~KindNoPointers) == KindPtr)\
@@ -815,9 +826,8 @@ scanblock(Workbuf *wbuf, Obj *wp, uintptr nobj, bool keepworking)
case GC_END:
if(--stack_top.count != 0) {
// Next iteration of a loop if possible.
- elemsize = stack_top.elemsize;
- stack_top.b += elemsize;
- if(stack_top.b + elemsize <= end_b+PtrSize) {
+ stack_top.b += stack_top.elemsize;
+ if(stack_top.b + stack_top.elemsize <= end_b+PtrSize) {
pc = stack_top.loop_or_ret;
continue;
}
@@ -945,7 +955,7 @@ scanblock(Workbuf *wbuf, Obj *wp, uintptr nobj, bool keepworking)
*objbufpos++ = (Obj){obj, size, objti};\
if(objbufpos == objbuf_end)\
flushobjbuf(objbuf, &objbufpos, &wp, &wbuf, &nobj);\
- break;
+ continue;
case GC_CHAN:
// There are no heap pointers in struct Hchan,
コアとなるコードの解説
GC_EFACE
および GC_IFACE
の変更
-
if(eface->type == nil) continue;
(GC_EFACE) およびif(iface->tab == nil) continue;
(GC_IFACE):- インターフェースが
nil
値である場合(つまり、type
ポインタやtab
ポインタがnil
である場合)、そのインターフェースは何も参照していないため、それ以上スキャンする必要はありません。以前はbreak
していましたが、continue
に変更することで、scanblock
のメインループが次のスキャン対象へと適切に進むようになります。
- インターフェースが
-
// eface->type
および// iface->tab
の追加スキャンロジック:t = eface->type;
およびt = iface->tab->type;
の後、if((void*)t >= arena_start && (void*)t < arena_used)
の条件で、eface->type
やiface->tab
がヒープ領域内のオブジェクトを指している場合に、それらをptrbuf
(ポインタバッファ)に追加しています。これは、型情報やインターフェーステーブル自体もヒープ上に割り当てられる可能性があり、GCの対象となるため、明示的にスキャン対象としてマークする必要があることを示しています。
-
if((t->kind & KindNoPointers)) break;
からif((t->kind & KindNoPointers)) continue;
への変更:- これがこのコミットの最も重要な変更点です。
KindNoPointers
は、その型の値が内部にポインタを含まないことを示します。例えば、int
やbool
などです。- 変更前は、インターフェースが保持する値の型が
KindNoPointers
である場合、break
文によってGC_EFACE
またはGC_IFACE
のswitch
ケースから抜けていました。これは、その値の内部をスキャンする必要がないため、最適化として行われていたと考えられます。 - しかし、このロジックには問題がありました。たとえ格納されている値の型が
KindNoPointers
であっても、インターフェースのdata
フィールド自体がヒープ上の別のオブジェクトへのポインタである可能性があります。例えば、interface{}
に*int
が格納されている場合、Eface.data
はヒープ上のint
へのポインタを指します。この*int
の型はint
であり、KindNoPointers
ですが、Eface.data
が指す先はGCが追跡すべきヒープオブジェクトです。 break
をcontinue
に変更することで、KindNoPointers
の条件が満たされてもswitch
ケースから抜け出すことなく、scanblock
のメインループの次のイテレーションに進みます。これにより、eface->data
やiface->data
がヒープ上のポインタである場合に、そのポインタが適切にスキャンされる機会が確保されます。
GC_END
の変更
elemsize = stack_top.elemsize; stack_top.b += elemsize;
がstack_top.b += stack_top.elemsize;
に変更されました。- これは、
elemsize
という一時変数への代入を省略し、stack_top.elemsize
を直接使用するようにした、わずかなコードの簡素化と最適化です。機能的な変更はありません。
- これは、
scanblock
関数の一般的な変更
*objbufpos++ = (Obj){obj, size, objti};
の後のbreak;
がcontinue;
に変更されました。- これは、
objbuf
(オブジェクトバッファ)が満杯になった際にflushobjbuf
を呼び出した後、scanblock
関数のメインループが中断されることなく、次のスキャン対象の処理を継続できるようにするための修正です。以前のbreak
では、バッファが満杯になるとループが終了してしまい、残りのスキャン対象が処理されない可能性がありました。
- これは、
これらの変更は、Goのガベージコレクタがインターフェースを介して参照されるヒープオブジェクトをより正確に識別し、追跡できるようにするための重要な改善です。
関連リンク
- Go言語のインターフェースに関する公式ドキュメントやブログ記事
- Goランタイムのガベージコレクションに関する技術記事やGoのソースコード解説
参考にした情報源リンク
- Go言語のソースコード (
src/pkg/runtime/mgc0.c
) - Go言語のガベージコレクションに関する一般的な知識
- Go言語のインターフェースの内部表現に関する情報
- golang/go GitHubリポジトリ
- Gerrit Change-Id: 7744047 (Goのコードレビューシステム)
- Go's runtime garbage collector (GC) handles interface scanning as part of its concurrent, mark-and-sweep process.
- Interface values in Go contain memory addresses that the GC must trace.
- The GC utilizes an internal concept called "GC Shapes," which are implementation details related to type layouts.
- Go's GC is designed to be precise, meaning it accurately identifies pointers within memory.