Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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フラグが設定されている型、例えばintboolなど)である場合に、GCがインターフェースのデータ部分の走査を早期に打ち切ってしまう可能性がありました。しかし、インターフェースのデータ部分自体がヒープ上の別のオブジェクトへのポインタである場合(例えば、interface{}*intstringが格納されている場合)、そのポインタを辿ってスキャンを継続する必要があります。

この不正確なスキャンは、本来到達可能なオブジェクトを到達不可能と誤って判断し、早期に解放してしまう(Use-After-Freeバグの原因となる)か、あるいは到達不可能なオブジェクトを解放せずに残してしまう(メモリリークの原因となる)可能性がありました。このコミットは、この問題を解決し、インターフェース値の型情報とデータポインタの両方を確実にスキャンすることで、GCの正確性と堅牢性を向上させることを目的としています。

前提知識の解説

Go言語のインターフェースの内部表現

Go言語のインターフェースは、コンパイル時に型が決定されない動的な型情報を持つことができます。ランタイム内部では、インターフェースは通常、2つのワード(ポインタサイズ)で表現されます。

  1. 空のインターフェース (interface{}):

    • Eface構造体として表現されます。
    • typeポインタ: 格納されている値の動的な型情報(_type構造体)へのポインタ。
    • dataポインタ: 格納されている実際の値へのポインタ。値がポインタサイズ以下の場合、dataフィールドに直接値が格納されることもあります(インライン化)。
  2. メソッドを持つインターフェース:

    • Iface構造体として表現されます。
    • tabポインタ: インターフェーステーブル(itab構造体)へのポインタ。itabは、インターフェースの型情報と、そのインターフェースが要求するメソッドの実装へのポインタを含みます。
    • dataポインタ: 格納されている実際の値へのポインタ。

ガベージコレクタは、これらのtypetabdataの各ポインタがヒープ上の有効なオブジェクトを指しているかどうかを判断し、指している場合はそのオブジェクトを「到達可能」としてマークし、さらにそのオブジェクトが持つポインタを辿ってスキャンを続けます。

Goランタイムとガベージコレクション (GC)

Goのガベージコレクタは、並行(concurrent)かつ三色マーク&スイープ(tri-color mark-and-sweep)方式を採用しています。GCのサイクルは以下のフェーズで構成されます。

  • マークフェーズ: プログラムのルート(スタック、グローバル変数など)から到達可能なすべてのオブジェクトをマークします。この際、ポインタを辿ってヒープ上のオブジェクトを走査します。
  • スイープフェーズ: マークされなかったオブジェクト(到達不可能なオブジェクト)をメモリから解放します。

src/pkg/runtime/mgc0.cファイルは、Goランタイムのメモリ管理とガベージコレクションの初期実装に関連するコードを含んでいます。このファイル内のscanblock関数は、GCのマークフェーズにおいて、特定のメモリブロック(例えばスタックフレーム)を走査し、そこに含まれるポインタを識別して、それらが指すヒープ上のオブジェクトをマークする役割を担っています。

KindNoPointers

Goの型システムには、その型がポインタを含むかどうかを示すKindNoPointersというフラグがあります。これはGCの最適化に利用されます。例えば、intboolのようなプリミティブ型はポインタを含まないため、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->typenilでないこと、かつeface->dataがヒープ領域内にある場合にのみ、eface->dataのスキャンが試みられていました。さらに、eface->typeKindNoPointersを持つ型である場合、break文によってGC_EFACEケースの処理が中断され、eface->dataが指す先のオブジェクトが適切にスキャンされない可能性がありました。

変更後は、以下の点が改善されています。

  1. eface->type == nil の早期チェック: eface->typenilの場合(インターフェースがnil値である場合)、すぐにcontinueして次のスキャン対象に移ります。これにより、不要な処理をスキップします。
  2. eface->type自体のスキャン: eface->typeポインタがヒープ領域内を指している場合、そのポインタ自体をGCの対象としてptrbufに追加し、スキャンキューに投入します。これは、型情報オブジェクトもヒープ上に存在し、GCの対象となる可能性があるためです。
  3. 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->typeKindNoPointersを持つ型である場合に、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->typeiface->tabがヒープ領域内のオブジェクトを指している場合に、それらをptrbuf(ポインタバッファ)に追加しています。これは、型情報やインターフェーステーブル自体もヒープ上に割り当てられる可能性があり、GCの対象となるため、明示的にスキャン対象としてマークする必要があることを示しています。
  • if((t->kind & KindNoPointers)) break; から if((t->kind & KindNoPointers)) continue; への変更:

    • これがこのコミットの最も重要な変更点です。
    • KindNoPointersは、その型の値が内部にポインタを含まないことを示します。例えば、intboolなどです。
    • 変更前は、インターフェースが保持する値の型がKindNoPointersである場合、break文によってGC_EFACEまたはGC_IFACEswitchケースから抜けていました。これは、その値の内部をスキャンする必要がないため、最適化として行われていたと考えられます。
    • しかし、このロジックには問題がありました。たとえ格納されている値の型がKindNoPointersであっても、インターフェースのdataフィールド自体がヒープ上の別のオブジェクトへのポインタである可能性があります。例えば、interface{}*intが格納されている場合、Eface.dataはヒープ上のintへのポインタを指します。この*intの型はintであり、KindNoPointersですが、Eface.dataが指す先はGCが追跡すべきヒープオブジェクトです。
    • breakcontinueに変更することで、KindNoPointersの条件が満たされてもswitchケースから抜け出すことなく、scanblockのメインループの次のイテレーションに進みます。これにより、eface->dataiface->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フラグが設定されている型、例えばintboolなど)である場合に、GCがインターフェースのデータ部分の走査を早期に打ち切ってしまう可能性がありました。しかし、インターフェースのデータ部分自体がヒープ上の別のオブジェクトへのポインタである場合(例えば、interface{}*intstringが格納されている場合)、そのポインタを辿ってスキャンを継続する必要があります。

この不正確なスキャンは、本来到達可能なオブジェクトを到達不可能と誤って判断し、早期に解放してしまう(Use-After-Freeバグの原因となる)か、あるいは到達不可能なオブジェクトを解放せずに残してしまう(メモリリークの原因となる)可能性がありました。このコミットは、この問題を解決し、インターフェース値の型情報とデータポインタの両方を確実にスキャンすることで、GCの正確性と堅牢性を向上させることを目的としています。

前提知識の解説

Go言語のインターフェースの内部表現

Go言語のインターフェースは、コンパイル時に型が決定されない動的な型情報を持つことができます。ランタイム内部では、インターフェースは通常、2つのワード(ポインタサイズ)で表現されます。

  1. 空のインターフェース (interface{}):

    • Eface構造体として表現されます。
    • typeポインタ: 格納されている値の動的な型情報(_type構造体)へのポインタ。
    • dataポインタ: 格納されている実際の値へのポインタ。値がポインタサイズ以下の場合、dataフィールドに直接値が格納されることもあります(インライン化)。
  2. メソッドを持つインターフェース:

    • Iface構造体として表現されます。
    • tabポインタ: インターフェーステーブル(itab構造体)へのポインタ。itabは、インターフェースの型情報と、そのインターフェースが要求するメソッドの実装へのポインタを含みます。
    • dataポインタ: 格納されている実際の値へのポインタ。

ガベージコレクタは、これらのtypetabdataの各ポインタがヒープ上の有効なオブジェクトを指しているかどうかを判断し、指している場合はそのオブジェクトを「到達可能」としてマークし、さらにそのオブジェクトが持つポインタを辿ってスキャンを続けます。

Goランタイムとガベージコレクション (GC)

Goのガベージコレクタは、並行(concurrent)かつ三色マーク&スイープ(tri-color mark-and-sweep)方式を採用しています。GCのサイクルは以下のフェーズで構成されます。

  • マークフェーズ: プログラムのルート(スタック、グローバル変数など)から到達可能なすべてのオブジェクトをマークします。この際、ポインタを辿ってヒープ上のオブジェクトを走査します。
  • スイープフェーズ: マークされなかったオブジェクト(到達不可能なオブジェクト)をメモリから解放します。

src/pkg/runtime/mgc0.cファイルは、Goランタイムのメモリ管理とガベージコレクションの初期実装に関連するコードを含んでいます。このファイル内のscanblock関数は、GCのマークフェーズにおいて、特定のメモリブロック(例えばスタックフレーム)を走査し、そこに含まれるポインタを識別して、それらが指すヒープ上のオブジェクトをマークする役割を担っています。

KindNoPointers

Goの型システムには、その型がポインタを含むかどうかを示すKindNoPointersというフラグがあります。これはGCの最適化に利用されます。例えば、intboolのようなプリミティブ型はポインタを含まないため、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->typenilでないこと、かつeface->dataがヒープ領域内にある場合にのみ、eface->dataのスキャンが試みられていました。さらに、eface->typeKindNoPointersを持つ型である場合、break文によってGC_EFACEケースの処理が中断され、eface->dataが指す先のオブジェクトが適切にスキャンされない可能性がありました。

変更後は、以下の点が改善されています。

  1. eface->type == nil の早期チェック: eface->typenilの場合(インターフェースがnil値である場合)、すぐにcontinueして次のスキャン対象に移ります。これにより、不要な処理をスキップします。
  2. eface->type自体のスキャン: eface->typeポインタがヒープ領域内を指している場合、そのポインタ自体をGCの対象としてptrbufに追加し、スキャンキューに投入します。これは、型情報オブジェクトもヒープ上に存在し、GCの対象となる可能性があるためです。
  3. 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->typeKindNoPointersを持つ型である場合に、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->typeiface->tabがヒープ領域内のオブジェクトを指している場合に、それらをptrbuf(ポインタバッファ)に追加しています。これは、型情報やインターフェーステーブル自体もヒープ上に割り当てられる可能性があり、GCの対象となるため、明示的にスキャン対象としてマークする必要があることを示しています。
  • if((t->kind & KindNoPointers)) break; から if((t->kind & KindNoPointers)) continue; への変更:

    • これがこのコミットの最も重要な変更点です。
    • KindNoPointersは、その型の値が内部にポインタを含まないことを示します。例えば、intboolなどです。
    • 変更前は、インターフェースが保持する値の型がKindNoPointersである場合、break文によってGC_EFACEまたはGC_IFACEswitchケースから抜けていました。これは、その値の内部をスキャンする必要がないため、最適化として行われていたと考えられます。
    • しかし、このロジックには問題がありました。たとえ格納されている値の型がKindNoPointersであっても、インターフェースのdataフィールド自体がヒープ上の別のオブジェクトへのポインタである可能性があります。例えば、interface{}*intが格納されている場合、Eface.dataはヒープ上のintへのポインタを指します。この*intの型はintであり、KindNoPointersですが、Eface.dataが指す先はGCが追跡すべきヒープオブジェクトです。
    • breakcontinueに変更することで、KindNoPointersの条件が満たされてもswitchケースから抜け出すことなく、scanblockのメインループの次のイテレーションに進みます。これにより、eface->dataiface->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のソースコード解説

参考にした情報源リンク