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

[インデックス 17906] ファイルの概要

このコミットは、Goランタイムのガベージコレクション(GC)におけるスタック走査の効率化を目的としています。具体的には、src/pkg/runtime/mgc0.c ファイルが変更されており、これはGoランタイムのメモリ管理とガベージコレクタの主要な部分を実装しているC言語のソースファイルです。このファイルは、GCのマークフェーズにおけるオブジェクトの走査、ポインタの識別、およびワークバッファの管理など、GCの低レベルな動作を定義しています。

コミット

コミットハッシュ: 0368a7ceb6e741a641a07e3ae381bdc9fc160a15 作者: Carl Shapiro cshapiro@google.com コミット日時: Tue Dec 3 14:12:55 2013 -0800

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/0368a7ceb6e741a641a07e3ae381bdc9fc160a15

元コミット内容

runtime: move stack scanning into the parallel mark phase

This change reduces the cost of the stack scanning by frames.
It moves the stack scanning from the serial root enumeration
phase to the parallel tracing phase.  The output that follows
are timings for the issue 6482 benchmark

Baseline

BenchmarkGoroutineSelect              50         108027405 ns/op
BenchmarkGoroutineBlocking            50          89573332 ns/op
BenchmarkGoroutineForRange            20          95614116 ns/op
BenchmarkGoroutineIdle                20         122809512 ns/op

Stack scan by frames, non-parallel

BenchmarkGoroutineSelect              20         297138929 ns/op
BenchmarkGoroutineBlocking            20         301137599 ns/op
BenchmarkGoroutineForRange            10         312499469 ns/op
BenchmarkGoroutineIdle                10         209428876 ns/op

Stack scan by frames, parallel

BenchmarkGoroutineSelect              20         183938431 ns/op
BenchmarkGoroutineBlocking            20         170109999 ns/op
BenchmarkGoroutineForRange            20         179628882 ns/op
BenchmarkGoroutineIdle                20         157541498 ns/op

The remaining performance disparity is due to inefficiencies
in gentraceback and its callees.  The effect was isolated by
using a parallel stack scan where scanstack was modified to do
a conservative scan of the stack segments without gentraceback
followed by a call of gentrackback with a no-op callback.

The output that follows are the top-10 most frequent tops of
stacks as determined by the Linux perf record facility.

Baseline

+  25.19%  gc.test  gc.test            [.] runtime.xchg
+  19.00%  gc.test  gc.test            [.] scanblock
+   8.53%  gc.test  gc.test            [.] scanstack
+   8.46%  gc.test  gc.test            [.] flushptrbuf
+   5.08%  gc.test  gc.test            [.] procresize
+   3.57%  gc.test  gc.test            [.] runtime.chanrecv
+   2.94%  gc.test  gc.test            [.] dequeue
+   2.74%  gc.test  gc.test            [.] addroots
+   2.25%  gc.test  gc.test            [.] runtime.ready
+   1.33%  gc.test  gc.test            [.] runtime.cas64

Gentraceback

+  18.12%  gc.test  gc.test             [.] runtime.xchg
+  14.68%  gc.test  gc.test             [.] scanblock
+   8.20%  gc.test  gc.test             [.] runtime.gentraceback
+   7.38%  gc.test  gc.test             [.] flushptrbuf
+   6.84%  gc.test  gc.test             [.] scanstack
+   5.92%  gc.test  gc.test             [.] runtime.findfunc
+   3.62%  gc.test  gc.test             [.] procresize
+   3.15%  gc.test  gc.test             [.] readvarint
+   1.92%  gc.test  gc.test             [.] addroots
+   1.87%  gc.test  gc.test             [.] runtime.chanrecv

R=golang-dev, dvyukov, rsc
CC=golang-dev
https://golang.org/cl/17410043

変更の背景

このコミットの主な目的は、Goランタイムのガベージコレクション(GC)におけるスタック走査のパフォーマンスを向上させることです。以前の実装では、スタック走査はGCの「シリアルなルート列挙フェーズ」で行われていました。このフェーズは、GCがヒープ上の到達可能なオブジェクトを特定するための出発点(ルート)をすべて見つけるために、アプリケーションの実行を一時停止(Stop-the-World: STW)させる必要がありました。スタック走査がこのシリアルなSTWフェーズで行われると、その処理時間がGCの一時停止時間に直接影響し、アプリケーションの応答性に悪影響を与えます。

この変更では、スタック走査を「並列トレースフェーズ」に移行させることで、このボトルネックを解消しようとしています。並列トレースフェーズは、GCのマークフェーズの一部であり、複数のヘルパーゴルーチンが同時にヒープを走査して到達可能なオブジェクトをマークします。スタック走査をこの並列フェーズに組み込むことで、GCの一時停止時間を短縮し、全体的なGCの効率を向上させることが期待されます。

コミットメッセージには、この変更によるベンチマーク結果が示されており、特に「Stack scan by frames, non-parallel」と比較して「Stack scan by frames, parallel」が大幅に改善されていることがわかります。これは、スタック走査の並列化が成功し、GCのパフォーマンスが向上したことを裏付けています。ただし、gentracebackとその呼び出し元に起因する非効率性が依然として残っていることも指摘されており、さらなる最適化の余地があることが示唆されています。

前提知識の解説

Goのガベージコレクション (GC) の基本

Goのガベージコレクタは、コンカレントなトライカラーマーク&スイープ方式を採用しています。これは、アプリケーションの実行と並行してGCが動作し、一時停止時間(Stop-the-World: STW)を最小限に抑えるように設計されています。

  • トライカラーマーク&スイープ: オブジェクトを「白(未訪問)」「灰(訪問済みだが子を未走査)」「黒(訪問済みで子も走査済み)」の3色に分類し、到達可能なオブジェクトをマークし、マークされなかったオブジェクトを解放します。
  • STWフェーズ: GCサイクルには、ごく短いSTWフェーズがいくつか存在します。
    • マーク開始 (Mark Start): GCルート(グローバル変数、レジスタ、ゴルーチンのスタックなど)を特定するためにSTWが発生します。
    • マーク終了 (Mark Termination): すべてのゴルーチンがGCセーフポイントに到達していることを確認し、スタックとグローバル変数を再度走査して、到達可能なオブジェクトが見落とされていないことを確認するためにSTWが発生します。

GCにおけるルートの重要性

ガベージコレクタは、ヒープ上のオブジェクトがまだ使用されているかどうかを判断するために、「ルート」と呼ばれる一連のポインタから走査を開始します。ルートは、GCが到達可能なオブジェクトを識別するための出発点となります。主なルートには以下のものがあります。

  • グローバル変数: プログラム全体からアクセス可能な変数。
  • レジスタ: CPUのレジスタに格納されているポインタ。
  • ゴルーチンのスタック: 各ゴルーチンの実行スタック上に存在するローカル変数や引数に含まれるポインタ。

これらのルートから辿れるオブジェクトは「生きている(live)」と判断され、マークされます。このコミットでは、特にゴルーチンのスタック走査に焦点を当てています。

スタックの構造とポインタの識別(Go 1.3以降の正確なGC)

Goのゴルーチンは、動的にサイズが変化するスタックを持っています。各関数呼び出しはスタック上に新しいスタックフレームを割り当て、そのフレームにはローカル変数、関数引数、戻りアドレスなどが格納されます。

Go 1.3以降、GoのGCは「完全に正確(fully precise)」になりました。これは、GCがスタック上の値がポインタであるか非ポインタであるかを正確に区別できることを意味します。これにより、GCが誤って整数値をポインタと解釈してしまい、不要なメモリを保持してしまう(メモリリーク)という問題を防ぐことができます。この精度は、Goコンパイラがスタックフレームのレイアウトに関するメタデータをGCに提供することで実現されています。

gentracebackの役割

runtime.gentracebackは、Goランタイムの内部関数であり、スタックトレースを生成する役割を担っています。これは、パニック発生時やruntime.Stack()のような関数が呼び出された際に、現在の関数呼び出しの履歴(コールスタック)を辿って情報を収集するために使用されます。gentracebackは、スタックフレームを一つずつ遡り、各フレームのプログラムカウンタ(PC)、スタックポインタ(SP)、およびその他の関連情報を取得します。この関数は非常に複雑であり、様々な種類のフレーム(Goの物理フレーム、インライン化されたフレーム、Cgoフレームなど)を処理する能力を持っています。

コミットメッセージでは、gentracebackが依然としてパフォーマンスのボトルネックである可能性が示唆されており、スタック走査の並列化後もその非効率性が残っていることが指摘されています。

技術的詳細

このコミットの技術的な核心は、Goランタイムのガベージコレクションにおけるスタック走査のロジックを、シリアルなルート列挙フェーズから並列マークフェーズへと移行させた点にあります。

スタックスキャンがシリアルからパラレルになったことの意味

以前のGo GCでは、スタックの走査はGCの初期段階、つまり「マーク開始」のSTWフェーズで行われていました。このフェーズでは、すべてのゴルーチンの実行が一時停止され、GCは各ゴルーチンのスタックを順番に走査して、ヒープ上のオブジェクトへのポインタ(ルート)を識別していました。このシリアルな処理は、ゴルーチンの数やスタックの深さに比例してSTW時間が長くなる原因となっていました。

この変更により、スタック走査はGCの「並列マークフェーズ」に組み込まれました。並列マークフェーズでは、複数のGCヘルパーゴルーチンが同時に動作し、ヒープ上のオブジェクトをマークします。スタック走査をこの並列処理の一部とすることで、GCの一時停止時間を大幅に短縮し、全体的なスループットを向上させることが可能になります。これは、GCのパフォーマンス特性、特にレイテンシ(一時停止時間)を改善する上で重要なステップです。

Scanbuf構造体の導入とその役割

このコミットでは、Scanbufという新しい構造体が導入されています。これは、スタック走査中に見つかったポインタやオブジェクトを一時的にバッファリングするためのものです。

typedef	struct Scanbuf Scanbuf;
struct	Scanbuf
{
	struct {
		PtrTarget *begin;
		PtrTarget *end;
		PtrTarget *pos;
	} ptr;
	struct {
		Obj *begin;
		Obj *end;
		Obj *pos;
	} obj;
	Workbuf *wbuf;
	Obj *wp;
	uintptr nobj;
};
  • ptr: PtrTarget(ポインタとその型情報)を格納するバッファの開始、終了、現在の位置を管理します。
  • obj: Obj(オブジェクトのベースアドレス、サイズ、型情報)を格納するバッファの開始、終了、現在の位置を管理します。
  • wbuf, wp, nobj: GCのワークバッファ(Workbuf)と、その中のオブジェクト(Obj)およびオブジェクト数(nobj)へのポインタを保持します。これらは、Scanbufが満杯になった際に、バッファの内容を実際のGCワークバッファにフラッシュするために使用されます。

Scanbufの導入により、スタック走査中に見つかったポインタやオブジェクトを効率的に収集し、バッファが満杯になったときにまとめて処理できるようになります。これにより、個々のポインタやオブジェクトが見つかるたびにGCワークバッファに直接書き込むオーバーヘッドを削減し、並列処理の効率を高めることができます。

flushptrbufflushobjbufの変更点

flushptrbufflushobjbufは、それぞれポインタバッファとオブジェクトバッファの内容をGCのメインワークバッファにフラッシュする関数です。このコミットでは、これらの関数の引数が変更され、個々のポインタやワークバッファ関連の引数ではなく、新しく導入されたScanbuf構造体へのポインタを受け取るようになりました。

// 変更前
static void flushptrbuf(PtrTarget *ptrbuf, PtrTarget **ptrbufpos, Obj **_wp, Workbuf **_wbuf, uintptr *_nobj)
static void flushobjbuf(Obj *objbuf, Obj **objbufpos, Obj **_wp, Workbuf **_wbuf, uintptr *_nobj)

// 変更後
static void flushptrbuf(Scanbuf *sbuf)
static void flushobjbuf(Scanbuf *sbuf)

この変更により、これらの関数はScanbufを通じて必要なすべての情報(バッファの開始/終了/現在位置、ワークバッファへのポインタなど)にアクセスできるようになり、コードの簡潔性と保守性が向上します。また、Scanbufがバッファリングの抽象化を提供することで、GCの内部ロジックがよりクリーンになります。

scanblockにおけるScanbufの利用

scanblock関数は、GCのマークフェーズにおいて、メモリブロックを走査してポインタを識別し、到達可能なオブジェクトをマークする主要な関数です。このコミットでは、scanblock内でScanbufが初期化され、ポインタやオブジェクトの収集に利用されるようになりました。

以前は、ptrbuf, ptrbuf_end, ptrbufpos, objbuf, objbuf_end, objbufposといった個別のポインタやバッファ管理変数が使用されていましたが、これらがScanbuf sbuf;に置き換えられました。

// 変更前 (一部抜粋)
PtrTarget *ptrbuf, *ptrbuf_end, *ptrbufpos;
Obj *objbuf, *objbuf_end, *objbufpos;
// ...
ptrbufpos = ptrbuf;
objbufpos = objbuf;

// 変更後 (一部抜粋)
Scanbuf sbuf;
// ...
sbuf.ptr.begin = sbuf.ptr.pos = &scanbuffers->ptrtarget[0];
sbuf.ptr.end = sbuf.ptr.begin + nelem(scanbuffers->ptrtarget);
sbuf.obj.begin = sbuf.obj.pos = &scanbuffers->obj[0];
sbuf.obj.end = sbuf.obj.begin + nelem(scanbuffers->obj);
sbuf.wbuf = wbuf;
sbuf.wp = wp;
sbuf.nobj = nobj;

これにより、scanblock内のポインタおよびオブジェクトのバッファリングロジックがScanbufを通じて一元的に管理されるようになり、コードの可読性と保守性が向上します。また、flushptrbufflushobjbufの呼び出しもflushptrbuf(&sbuf);のように簡略化されています。

scaninterfacedatascanbitvectorの変更点

scaninterfacedataはインターフェースのデータ部分を走査し、scanbitvectorはビットマップに基づいてメモリ領域を走査してポインタを識別する関数です。これらの関数も、Scanbuf構造体へのポインタを引数として受け取るように変更されました。

// 変更前
static void scaninterfacedata(uintptr bits, byte *scanp, bool afterprologue)
static void scanbitvector(byte *scanp, BitVector *bv, bool afterprologue)

// 変更後
static void scaninterfacedata(uintptr bits, byte *scanp, bool afterprologue, Scanbuf *sbuf)
static void scanbitvector(byte *scanp, BitVector *bv, bool afterprologue, Scanbuf *sbuf)

これにより、これらの関数内で見つかったポインタやオブジェクトが、直接Scanbufを通じてバッファリングされるようになり、GCのワークバッファへの追加がより効率的に行われるようになります。

addstackrootsからscanstackへの変更とgentracebackの利用

以前はaddstackrootsという関数がゴルーチンのスタックを走査してルートを追加していましたが、このコミットではaddstackrootsが削除され、代わりにscanstackという新しい関数が導入されました。

scanstack関数は、特定のゴルーチン(G* gp)のスタックを走査し、gentraceback関数を利用してスタックフレームを一つずつ辿ります。gentracebackは、スタックフレームごとにscanframeというコールバック関数を呼び出します。このscanframe関数が、各スタックフレーム内のローカル変数や引数に含まれるポインタを識別し、それらをScanbufを通じてGCのワークバッファに追加します。

// 新しい scanstack 関数
static void scanstack(G* gp, void *scanbuf) {
    // ...
    runtime·gentraceback(pc, sp, lr, gp, 0, nil, 0x7fffffff, scanframe, scanbuf, false);
}

// 新しい scanframe 関数
static void scanframe(Stkframe *frame, void *arg) {
    Scanbuf *sbuf;
    sbuf = arg;
    // ... スタックフレーム内のポインタを sbuf に追加するロジック ...
}

この変更は、スタック走査のロジックをgentracebackのフレーム走査メカニズムと統合することで、より堅牢で効率的なスタック走査を実現しています。また、scanbufgentracebackのコールバック引数として渡すことで、スタック走査中に見つかったポインタを直接Scanbufに格納できるようになり、並列処理の恩恵を最大限に受けることができます。

ベンチマーク結果の分析

コミットメッセージには、issue 6482ベンチマークのタイミング結果が示されています。

  • Baseline: 変更前の基本的なパフォーマンス。
  • Stack scan by frames, non-parallel: スタックをフレーム単位で走査するが、並列化されていない場合のパフォーマンス。この結果はBaselineと比較して大幅に悪化しており、フレーム単位の走査がシリアルに行われると非常にコストが高いことを示しています。
  • Stack scan by frames, parallel: スタックをフレーム単位で走査し、並列化された場合のパフォーマンス。この結果は「non-parallel」の場合と比較して大幅に改善されており、並列化がスタック走査のコストを効果的に削減したことを示しています。ただし、Baselineと比較するとまだパフォーマンスの差があります。

gentracebackが依然としてボトルネックであることの言及

ベンチマーク結果の分析に加えて、コミットメッセージでは「The remaining performance disparity is due to inefficiencies in gentraceback and its callees.」と明記されています。これは、スタック走査の並列化によってパフォーマンスは向上したものの、runtime.gentraceback関数とその内部で呼び出される関数(例えばruntime.findfuncなど)の非効率性が、依然としてGCのパフォーマンスにおけるボトルネックとなっていることを示唆しています。これは、今後のGoランタイムの最適化において、gentracebackのさらなる改善が重要であることを示しています。

コアとなるコードの変更箇所

このコミットにおける主要なコード変更は、src/pkg/runtime/mgc0.cファイルに集中しています。

  1. ScanStackByFramesのデフォルト値変更:

    --- a/src/pkg/runtime/mgc0.c
    +++ b/src/pkg/runtime/mgc0.c
    @@ -19,7 +19,7 @@ enum {
     	Debug = 0,
     	DebugMark = 0,  // run second pass to check mark
     	CollectStats = 0,
    -	ScanStackByFrames = 0,
    +	ScanStackByFrames = 1,
     	IgnorePreciseGC = 0,
    

    ScanStackByFrames0から1に変更され、スタックをフレーム単位で走査するモードがデフォルトで有効になりました。

  2. Scanbuf構造体の追加:

    --- a/src/pkg/runtime/mgc0.c
    +++ b/src/pkg/runtime/mgc0.c
    @@ -317,6 +319,24 @@ struct PtrTarget
     	uintptr ti;
     };
     
    +typedef	struct Scanbuf Scanbuf;
    +struct	Scanbuf
    +{
    +	struct {
    +		PtrTarget *begin;
    +		PtrTarget *end;
    +		PtrTarget *pos;
    +	} ptr;
    +	struct {
    +		Obj *begin;
    +		Obj *end;
    +		Obj *pos;
    +	} obj;
    +	Workbuf *wbuf;
    +	Obj *wp;
    +	uintptr nobj;
    +};
    +
     typedef struct BufferList BufferList;
     struct BufferList
     {
    

    スタック走査中に見つかったポインタやオブジェクトをバッファリングするためのScanbuf構造体が定義されました。

  3. flushptrbufflushobjbufの引数変更とScanbufの利用:

    --- a/src/pkg/runtime/mgc0.c
    +++ b/src/pkg/runtime/mgc0.c
    @@ -350,7 +370,7 @@ static void enqueue(Obj obj, Workbuf **_wbuf, Obj **_wp, uintptr *_nobj);\n //     flushptrbuf
     //  (find block start, mark and enqueue)
     static void
    -flushptrbuf(PtrTarget *ptrbuf, PtrTarget **ptrbufpos, Obj **_wp, Workbuf **_wbuf, uintptr *_nobj)
    +flushptrbuf(Scanbuf *sbuf)
     {
     	byte *p, *arena_start, *obj;\n     	uintptr size, *bitp, bits, shift, j, x, xbits, off, nobj, ti, n;\n     	PageID k;\n     	Obj *wp;\n     	Workbuf *wbuf;\n    +	PtrTarget *ptrbuf;\n     	PtrTarget *ptrbuf_end;\n     
    -	wp = *_wp;\n    -	wbuf = *_wbuf;\n    -	nobj = *_nobj;\n    +	wp = sbuf->wp;\n    +	wbuf = sbuf->wbuf;\n    +	nobj = sbuf->nobj;\n     
    -	ptrbuf_end = *ptrbufpos;\n    -	n = ptrbuf_end - ptrbuf;\n    -	*ptrbufpos = ptrbuf;\n    +	ptrbuf = sbuf->ptr.begin;\n    +	ptrbuf_end = sbuf->ptr.pos;\n    +	n = ptrbuf_end - sbuf->ptr.begin;\n    +	sbuf->ptr.pos = sbuf->ptr.begin;\n     
     	if(CollectStats) {\n     	\truntime·xadd64(&gcstats.ptr.sum, n);\n    @@ -514,25 +536,27 @@ flushptrbuf(PtrTarget *ptrbuf, PtrTarget **ptrbufpos, Obj **_wp, Workbuf **_wbuf\n     		}\n     	}\n     
    -	*_wp = wp;\n    -	*_wbuf = wbuf;\n    -	*_nobj = nobj;\n    +	sbuf->wp = wp;\n    +	sbuf->wbuf = wbuf;\n    +	sbuf->nobj = nobj;\n     }\n     
     static void\n    -flushobjbuf(Obj *objbuf, Obj **objbufpos, Obj **_wp, Workbuf **_wbuf, uintptr *_nobj)
    +flushobjbuf(Scanbuf *sbuf)
     {
     	uintptr nobj, off;\n     	Obj *wp, obj;\n     	Workbuf *wbuf;\n    +	Obj *objbuf;\n     	Obj *objbuf_end;\n     
    -	wp = *_wp;\n    -	wbuf = *_wbuf;\n    -	nobj = *_nobj;\n    +	wp = sbuf->wp;\n    +	wbuf = sbuf->wbuf;\n    +	nobj = sbuf->nobj;\n     
    -	objbuf_end = *objbufpos;\n    -	*objbufpos = objbuf;\n    +	objbuf = sbuf->obj.begin;\n    +	objbuf_end = sbuf->obj.pos;\n    +	sbuf->obj.pos = sbuf->obj.begin;\n     
     	while(objbuf < objbuf_end) {\n     	\tobj = *objbuf++;\n    @@ -570,9 +594,9 @@ flushobjbuf(Obj *objbuf, Obj **objbufpos, Obj **_wp, Workbuf **_wbuf, uintptr *_\n     	\twp = wbuf->obj + nobj;\n     	}\n     
    -	*_wp = wp;\n    -	*_wbuf = wbuf;\n    -	*_nobj = nobj;\n    +	sbuf->wp = wp;\n    +	sbuf->wbuf = wbuf;\n    +	sbuf->nobj = nobj;\n     }\n    ```
    `flushptrbuf`と`flushobjbuf`の引数が`Scanbuf *sbuf`に変更され、内部で`sbuf`のメンバーを通じてバッファ情報にアクセスするようになりました。
    
    
  4. scanblockでのScanbufの初期化と利用:

    --- a/src/pkg/runtime/mgc0.c
    +++ b/src/pkg/runtime/mgc0.c
    @@ -666,8 +693,7 @@ scanblock(Workbuf *wbuf, Obj *wp, uintptr nobj, bool keepworking)\n     	Slice *sliceptr;\n     	Frame *stack_ptr, stack_top, stack[GC_STACK_CAPACITY+4];\n     	BufferList *scanbuffers;\n    -	PtrTarget *ptrbuf, *ptrbuf_end, *ptrbufpos;\n    -	Obj *objbuf, *objbuf_end, *objbufpos;\n    +	Scanbuf sbuf;\n     	Eface *eface;\n     	Iface *iface;\n     	Hchan *chan;\n    @@ -681,21 +707,22 @@ scanblock(Workbuf *wbuf, Obj *wp, uintptr nobj, bool keepworking)\n     	arena_used = runtime·mheap.arena_used;\n     
     	stack_ptr = stack+nelem(stack)-1;\n    -	\n    +\n     	precise_type = false;\n     	nominal_size = 0;\n     
    -	// Allocate ptrbuf\n    -	{\n    -		scanbuffers = &bufferList[m->helpgc];\n    -		ptrbuf = &scanbuffers->ptrtarget[0];\n    -		ptrbuf_end = &scanbuffers->ptrtarget[0] + nelem(scanbuffers->ptrtarget);\n    -		objbuf = &scanbuffers->obj[0];\n    -		objbuf_end = &scanbuffers->obj[0] + nelem(scanbuffers->obj);\n    -	}\n    +	// Initialize sbuf\n    +	scanbuffers = &bufferList[m->helpgc];\n     
    -	ptrbufpos = ptrbuf;\n    -	objbufpos = objbuf;\n    +	sbuf.ptr.begin = sbuf.ptr.pos = &scanbuffers->ptrtarget[0];\n    +	sbuf.ptr.end = sbuf.ptr.begin + nelem(scanbuffers->ptrtarget);\n    +\n    +	sbuf.obj.begin = sbuf.obj.pos = &scanbuffers->obj[0];\n    +	sbuf.obj.end = sbuf.obj.begin + nelem(scanbuffers->obj);\n    +\n    +	sbuf.wbuf = wbuf;\n    +	sbuf.wp = wp;\n    +	sbuf.nobj = nobj;\n     
     	// (Silence the compiler)\n     	chan = nil;\n    @@ -713,7 +740,7 @@ scanblock(Workbuf *wbuf, Obj *wp, uintptr nobj, bool keepworking)\n     
     	\tif(CollectStats) {\n     	\t\truntime·xadd64(&gcstats.nbytes, n);\n    -	\t\t\truntime·xadd64(&gcstats.obj.sum, nobj);\n    +	\t\t\truntime·xadd64(&gcstats.obj.sum, sbuf.nobj);\n     	\t\t\truntime·xadd64(&gcstats.obj.cnt, 1);\n     	\t}\n     
    @@ -839,9 +866,9 @@ scanblock(Workbuf *wbuf, Obj *wp, uintptr nobj, bool keepworking)\n     		\t// eface->type\n     		\tt = eface->type;\n     		\tif((void*)t >= arena_start && (void*)t < arena_used) {\n    -		\t\t*ptrbufpos++ = (PtrTarget){t, 0};\n    -		\t\tif(ptrbufpos == ptrbuf_end)\n    -		\t\t\tflushptrbuf(ptrbuf, &ptrbufpos, &wp, &wbuf, &nobj);\n    +		\t\t*sbuf.ptr.pos++ = (PtrTarget){t, 0};\n    +		\t\tif(sbuf.ptr.pos == sbuf.ptr.end)\n    +		\t\t\tflushptrbuf(&sbuf);\n     		\t}\n     
     		\t// eface->data\n    @@ -868,9 +895,9 @@ scanblock(Workbuf *wbuf, Obj *wp, uintptr nobj, bool keepworking)\n     		\t\n     		\t// iface->tab\n     		\tif((void*)iface->tab >= arena_start && (void*)iface->tab < arena_used) {\n    -		\t\t*ptrbufpos++ = (PtrTarget){iface->tab, (uintptr)itabtype->gc};\n    -		\t\tif(ptrbufpos == ptrbuf_end)\n    -		\t\t\tflushptrbuf(ptrbuf, &ptrbufpos, &wp, &wbuf, &nobj);\n    +		\t\t*sbuf.ptr.pos++ = (PtrTarget){iface->tab, (uintptr)itabtype->gc};\n    +		\t\tif(sbuf.ptr.pos == sbuf.ptr.end)\n    +		\t\t\tflushptrbuf(&sbuf);\n     		\t}\n     
     		\t// iface->data\n    @@ -895,9 +922,9 @@ scanblock(Workbuf *wbuf, Obj *wp, uintptr nobj, bool keepworking)\n     		\t\tobj = *(byte**)stack_top.b;\n     		\t\tstack_top.b += PtrSize;\n     		\t\tif(obj >= arena_start && obj < arena_used) {\n    -		\t\t\t*ptrbufpos++ = (PtrTarget){obj, 0};\n    -		\t\t\tif(ptrbufpos == ptrbuf_end)\n    -		\t\t\t\tflushptrbuf(ptrbuf, &ptrbufpos, &wp, &wbuf, &nobj);\n    +		\t\t\t*sbuf.ptr.pos++ = (PtrTarget){obj, 0};\n    +		\t\t\tif(sbuf.ptr.pos == sbuf.ptr.end)\n    +		\t\t\t\tflushptrbuf(&sbuf);\n     		\t}\n     		\t}\n     		\tgoto next_block;\n    @@ -926,7 +953,7 @@ scanblock(Workbuf *wbuf, Obj *wp, uintptr nobj, bool keepworking)\n     		\t\t\tif(*(byte**)i != nil) {\n     		\t\t\t\t// Found a value that may be a pointer.\n     		\t\t\t\t// Do a rescan of the entire block.\n    -		\t\t\t\tenqueue((Obj){b, n, 0}, &wbuf, &wp, &nobj);\n    +		\t\t\t\tenqueue((Obj){b, n, 0}, &sbuf.wbuf, &sbuf.wp, &sbuf.nobj);\n     		\t\t\t\tif(CollectStats) {\n     		\t\t\t\t\truntime·xadd64(&gcstats.rescan, 1);\n     		\t\t\t\t\truntime·xadd64(&gcstats.rescanbytes, n);\n    @@ -972,9 +999,9 @@ scanblock(Workbuf *wbuf, Obj *wp, uintptr nobj, bool keepworking)\n     		\t\tobjti = pc[3];\n     		\t\tpc += 4;\n     
    -		\t\t*objbufpos++ = (Obj){obj, size, objti};\n    -		\t\tif(objbufpos == objbuf_end)\n    -		\t\t\tflushobjbuf(objbuf, &objbufpos, &wp, &wbuf, &nobj);\n    +		\t\t*sbuf.obj.pos++ = (Obj){obj, size, objti};\n    +		\t\tif(sbuf.obj.pos == sbuf.obj.end)\n    +		\t\t\tflushobjbuf(&sbuf);\n     		\t\tcontinue;\n     
     		\tcase GC_CHAN_PTR:\n    @@ -1007,10 +1034,10 @@ scanblock(Workbuf *wbuf, Obj *wp, uintptr nobj, bool keepworking)\n     		\t\t\t\t\t// in-use part of the circular buffer is scanned.\n     		\t\t\t\t\t// (Channel routines zero the unused part, so the current\n     		\t\t\t\t\t// code does not lead to leaks, it\'s just a little inefficient.)\n    -		\t\t\t\t\t*objbufpos++ = (Obj){(byte*)chan+runtime·Hchansize, chancap*chantype->elem->size,\n    +		\t\t\t\t\t*sbuf.obj.pos++ = (Obj){(byte*)chan+runtime·Hchansize, chancap*chantype->elem->size,\n     		\t\t\t\t\t\t(uintptr)chantype->elem->gc | PRECISE | LOOP};\n    -		\t\t\t\t\tif(objbufpos == objbuf_end)\n    -		\t\t\t\t\t\tflushobjbuf(objbuf, &objbufpos, &wp, &wbuf, &nobj);\n    +		\t\t\t\t\tif(sbuf.obj.pos == sbuf.obj.end)\n    +		\t\t\t\t\t\tflushobjbuf(&sbuf);\n     		\t\t\t\t}\n     		\t\t\t}\n     		\t\tif(chan_ret == nil)\n    @@ -1018,15 +1045,20 @@ scanblock(Workbuf *wbuf, Obj *wp, uintptr nobj, bool keepworking)\n     		\t\tpc = chan_ret;\n     		\t\tcontinue;\n     
    +		\t\tcase GC_G_PTR:\n    +		\t\t\tobj = (void*)stack_top.b;\n    +		\t\t\tscanstack(obj, &sbuf);\n    +		\t\t\tgoto next_block;\n    +\n     		\tdefault:\n     		\t\truntime·throw(\"scanblock: invalid GC instruction\");\n     		\t\treturn;\n     		\t}\n     
     		\tif(obj >= arena_start && obj < arena_used) {\n    -		\t\t*ptrbufpos++ = (PtrTarget){obj, objti};\n    -		\t\tif(ptrbufpos == ptrbuf_end)\n    -		\t\t\tflushptrbuf(ptrbuf, &ptrbufpos, &wp, &wbuf, &nobj);\n    +		\t\t*sbuf.ptr.pos++ = (PtrTarget){obj, objti};\n    +		\t\tif(sbuf.ptr.pos == sbuf.ptr.end)\n    +		\t\t\tflushptrbuf(&sbuf);\n     		\t}\n     		}\n     
    @@ -1034,34 +1066,32 @@ scanblock(Workbuf *wbuf, Obj *wp, uintptr nobj, bool keepworking)\n     		\t// Done scanning [b, b+n).  Prepare for the next iteration of\n     		\t// the loop by setting b, n, ti to the parameters for the next block.\n     
    -		\tif(nobj == 0) {\n    -		\t\tflushptrbuf(ptrbuf, &ptrbufpos, &wp, &wbuf, &nobj);\n    -		\t\tflushobjbuf(objbuf, &objbufpos, &wp, &wbuf, &nobj);\n    +		\tif(sbuf.nobj == 0) {\n    +		\t\tflushptrbuf(&sbuf);\n    +		\t\tflushobjbuf(&sbuf);\n     
    -		\t\tif(nobj == 0) {\n    +		\t\tif(sbuf.nobj == 0) {\n     		\t\t\tif(!keepworking) {\n    -		\t\t\t\tif(wbuf)\n    -		\t\t\t\t\tputempty(wbuf);\n    -		\t\t\t\tgoto endscan;\n    +		\t\t\t\tif(sbuf.wbuf)\n    +		\t\t\t\t\tputempty(sbuf.wbuf);\n    +		\t\t\t\treturn;\n     		\t\t\t}\n     		\t\t\t// Emptied our buffer: refill.\n    -		\t\t\twbuf = getfull(wbuf);\n    -		\t\t\tif(wbuf == nil)\n    -		\t\t\t\tgoto endscan;\n    -		\t\t\tnobj = wbuf->nobj;\n    -		\t\t\twp = wbuf->obj + wbuf->nobj;\n    +		\t\t\tsbuf.wbuf = getfull(sbuf.wbuf);\n    +		\t\t\tif(sbuf.wbuf == nil)\n    +		\t\t\t\treturn;\n    +		\t\t\tsbuf.nobj = sbuf.wbuf->nobj;\n    +		\t\t\tsbuf.wp = sbuf.wbuf->obj + sbuf.wbuf->nobj;\n     		\t\t}\n     		\t}\n     
     		\t// Fetch b from the work buffer.\n    -		\t--wp;\n    -		\tb = wp->p;\n    -		\tn = wp->n;\n    -		\tti = wp->ti;\n    -		\tnobj--;\n    +		\t--sbuf.wp;\n    +		\tb = sbuf.wp->p;\n    +		\tn = sbuf.wp->n;\n    +		\tti = sbuf.wp->ti;\n    +		\tsbuf.nobj--;\n     		}\n    -\n    -endscan:;\n     }\n    ```
    `scanblock`関数内で、個別のバッファ管理変数が`Scanbuf sbuf;`に置き換えられ、`sbuf`のメンバーを通じてバッファの初期化、ポインタ/オブジェクトの追加、および`flushptrbuf`/`flushobjbuf`の呼び出しが行われるようになりました。
    
    
  5. scaninterfacedatascanbitvectorへのScanbuf引数の追加:

    --- a/src/pkg/runtime/mgc0.c
    +++ b/src/pkg/runtime/mgc0.c
    @@ -1340,7 +1370,7 @@ struct BitVector
     // Scans an interface data value when the interface type indicates
     // that it is a pointer.\n     static void\n    -scaninterfacedata(uintptr bits, byte *scanp, bool afterprologue)
    +scaninterfacedata(uintptr bits, byte *scanp, bool afterprologue, Scanbuf *sbuf)
     {
     	Itab *tab;\n     	Type *type;\n    @@ -1356,12 +1386,14 @@ scaninterfacedata(uintptr bits, byte *scanp, bool afterprologue)\n     		\t\t\t\treturn;\n     		\t}\n     		}\n    -	addroot((Obj){scanp+PtrSize, PtrSize, 0});\n    +	*sbuf->obj.pos++ = (Obj){scanp+PtrSize, PtrSize, 0};\n    +	if(sbuf->obj.pos == sbuf->obj.end)\n    +		flushobjbuf(sbuf);\n     }\n     
     // Starting from scanp, scans words corresponding to set bits.\n     static void\n    -scanbitvector(byte *scanp, BitVector *bv, bool afterprologue)
    +scanbitvector(byte *scanp, BitVector *bv, bool afterprologue, Scanbuf *sbuf)
     {
     	uintptr word, bits;\n     	uint32 *wordp;\n    @@ -1378,75 +1410,126 @@ scanbitvector(byte *scanp, BitVector *bv, bool afterprologue)\n     		for(; i > 0; i--) {\n     		\tbits = word & 3;\n     		\tif(bits != BitsNoPointer && *(void**)scanp != nil)\n    -		\t\tif(bits == BitsPointer)\n    -		\t\t\taddroot((Obj){scanp, PtrSize, 0});\n    -		\t\telse\n    -		\t\t\tscaninterfacedata(bits, scanp, afterprologue);\n    +		\t\tif(bits == BitsPointer) {\n    +		\t\t\t*sbuf->obj.pos++ = (Obj){scanp, PtrSize, 0};\n    +		\t\t\tif(sbuf->obj.pos == sbuf->obj.end)\n    +		\t\t\t\tflushobjbuf(sbuf);\n    +		\t} else\n    +		\t\tscaninterfacedata(bits, scanp, afterprologue, sbuf);\n     		\tword >>= BitsPerPointer;\n     		\tscanp += PtrSize;\n     		}\n     	}\n     }\n     
    -// Scan a stack frame: local variables and function arguments/results.\n     static void\n    -addframeroots(Stkframe *frame, void*)\n    +addstackroots(G *gp)\n    +{\n    +\tM *mp;\n    +\tint32 n;\n    +\tStktop *stk;\n    +\tuintptr sp, guard;\n    +\tvoid *base;\n    +\tuintptr size;\n    +\n    +\tif(gp == g)\n    +\t\truntime·throw(\"can\'t scan our own stack\");\n    +\tif((mp = gp->m) != nil && mp->helpgc)\n    +\t\truntime·throw(\"can\'t scan gchelper stack\");\n    +\tif(gp->syscallstack != (uintptr)nil) {\n    +\t\t// Scanning another goroutine that is about to enter or might\n    +\t\t// have just exited a system call. It may be executing code such\n    +\t\t// as schedlock and may have needed to start a new stack segment.\n    +\t\t// Use the stack segment and stack pointer at the time of\n    +\t\t// the system call instead, since that won\'t change underfoot.\n    +\t\tsp = gp->syscallsp;\n    +\t\tstk = (Stktop*)gp->syscallstack;\n    +\t\tguard = gp->syscallguard;\n    +\t} else {\n    +\t\t// Scanning another goroutine\'s stack.\n    +\t\t// The goroutine is usually asleep (the world is stopped).\n    +\t\tsp = gp->sched.sp;\n    +\t\tstk = (Stktop*)gp->stackbase;\n    +\t\tguard = gp->stackguard;\n    +\t\t// For function about to start, context argument is a root too.\n    +\t\tif(gp->sched.ctxt != 0 && runtime·mlookup(gp->sched.ctxt, &base, &size, nil))\n    +\t\t\taddroot((Obj){base, size, 0});\n    +\t}\n    +\tif(ScanStackByFrames) {\n    +\t\tUSED(sp);\n    +\t\tUSED(stk);\n    +\t\tUSED(guard);\n    +\t\taddroot((Obj){(byte*)gp, PtrSize, (uintptr)gptrProg});\n    +\t} else {\n    +\t\tn = 0;\n    +\t\twhile(stk) {\n    +\t\t\tif(sp < guard-StackGuard || (uintptr)stk < sp) {\n    +\t\t\t\truntime·printf(\"scanstack inconsistent: g%D#%d sp=%p not in [%p,%p]\\n\", gp->goid, n, sp, guard-StackGuard, stk);\n    +\t\t\t\truntime·throw(\"scanstack\");\n    +\t\t\t}\n    +\t\t\taddroot((Obj){(byte*)sp, (uintptr)stk - sp, (uintptr)defaultProg | PRECISE | LOOP});\n    +\t\t\tsp = stk->gobuf.sp;\n    +\t\t\tguard = stk->stackguard;\n    +\t\t\tstk = (Stktop*)stk->stackbase;\n    +\t\t\tn++;\n    +\t\t}\n    +\t}\n    +}\n    +\n    +static void\n    +scanframe(Stkframe *frame, void *arg)\n     {\n    -\tFunc *f;\n     \tBitVector *args, *locals;\n    +\tScanbuf *sbuf;\n     \tuintptr size;\n     \tbool afterprologue;\n     
    -\tf = frame->fn;\n    -\n    +\tsbuf = arg;\n     \t// Scan local variables if stack frame has been allocated.\n     \t// Use pointer information if known.\n     \tafterprologue = (frame->varp > (byte*)frame->sp);\n     \tif(afterprologue) {\n    -\t\tlocals = runtime·funcdata(f, FUNCDATA_GCLocals);\n    +\t\tlocals = runtime·funcdata(frame->fn, FUNCDATA_GCLocals);\n     \t\tif(locals == nil) {\n     \t\t\t// No locals information, scan everything.\n     \t\t\tsize = frame->varp - (byte*)frame->sp;\n    -	\t\taddroot((Obj){frame->varp - size, size, 0});\n    +	\t\t*sbuf->obj.pos++ = (Obj){frame->varp - size, size, 0};\n    +	\t\tif(sbuf->obj.pos == sbuf->obj.end)\n    +	\t\t\tflushobjbuf(sbuf);\n     \t\t} else if(locals->n < 0) {\n     \t\t\t// Locals size information, scan just the\n     \t\t\t// locals.\n     \t\t\tsize = -locals->n;\n    -	\t\taddroot((Obj){frame->varp - size, size, 0});\n    +	\t\t*sbuf->obj.pos++ = (Obj){frame->varp - size, size, 0};\n    +	\t\tif(sbuf->obj.pos == sbuf->obj.end)\n    +	\t\t\tflushobjbuf(sbuf);\n     \t\t} else if(locals->n > 0) {\n     \t\t\t// Locals bitmap information, scan just the\n     \t\t\t// pointers in locals.\n     \t\t\tsize = (locals->n*PtrSize) / BitsPerPointer;\n    -	\t\tscanbitvector(frame->varp - size, locals, afterprologue);\n    +	\t\tscanbitvector(frame->varp - size, locals, afterprologue, sbuf);\n     \t\t}\n     \t}\n     
     \t// Scan arguments.\n     \t// Use pointer information if known.\n    -	args = runtime·funcdata(f, FUNCDATA_GCArgs);\n    +	args = runtime·funcdata(frame->fn, FUNCDATA_GCArgs);\n     \tif(args != nil && args->n > 0)\n    -	\tscanbitvector(frame->argp, args, false);\n    -	else\n    -	\taddroot((Obj){frame->argp, frame->arglen, 0});\n    +	\tscanbitvector(frame->argp, args, false, sbuf);\n    +	else {\n    +	\t*sbuf->obj.pos++ = (Obj){frame->argp, frame->arglen, 0};\n    +	\tif(sbuf->obj.pos == sbuf.obj.end)\n    +	\t\tflushobjbuf(sbuf);\n    +	}\n     }\n     
     static void\n    -addstackroots(G *gp)\n    +scanstack(G* gp, void *scanbuf)\n     {\n    -\tM *mp;\n    -\tint32 n;\n    -\tStktop *stk;\n    -\tuintptr sp, guard, pc, lr;\n    -\tvoid *base;\n    -\tuintptr size;\n    +\tuintptr pc;\n    +\tuintptr sp;\n    +\tuintptr lr;\n     
    -\tstk = (Stktop*)gp->stackbase;\n    -\tguard = gp->stackguard;\n    -\n    -\tif(gp == g)\n    -\t\truntime·throw(\"can\'t scan our own stack\");\n    -\tif((mp = gp->m) != nil && mp->helpgc)\n    -\t\truntime·throw(\"can\'t scan gchelper stack\");\n     \tif(gp->syscallstack != (uintptr)nil) {\n     \t\t// Scanning another goroutine that is about to enter or might\n     \t\t// have just exited a system call. It may be executing code such\n    @@ -1456,39 +1539,14 @@ addstackroots(G *gp)\n     \t\tsp = gp->syscallsp;\n     \t\tpc = gp->syscallpc;\n     \t\tlr = 0;\n    -\t\tstk = (Stktop*)gp->syscallstack;\n    -\t\tguard = gp->syscallguard;\n     \t} else {\n     \t\t// Scanning another goroutine\'s stack.\n     \t\t// The goroutine is usually asleep (the world is stopped).\n     \t\tsp = gp->sched.sp;\n     \t\tpc = gp->sched.pc;\n     \t\tlr = gp->sched.lr;\n    -\n    -\t\t// For function about to start, context argument is a root too.\n    -\t\tif(gp->sched.ctxt != 0 && runtime·mlookup(gp->sched.ctxt, &base, &size, nil))\n    -\t\t\taddroot((Obj){base, size, 0});\n    -\t}\n    -\tif(ScanStackByFrames) {\n    -\t\tUSED(stk);\n    -\t\tUSED(guard);\n    -\t\truntime·gentraceback(pc, sp, lr, gp, 0, nil, 0x7fffffff, addframeroots, nil, false);\n    -\t} else {\n    -\t\tUSED(lr);\n    -\t\tUSED(pc);\n    -\t\tn = 0;\n    -\t\twhile(stk) {\n    -\t\t\tif(sp < guard-StackGuard || (uintptr)stk < sp) {\n    -\t\t\t\truntime·printf(\"scanstack inconsistent: g%D#%d sp=%p not in [%p,%p]\\n\", gp->goid, n, sp, guard-StackGuard, stk);\n    -\t\t\t\truntime·throw(\"scanstack\");\n    -\t\t\t}\n    -\t\t\taddroot((Obj){(byte*)sp, (uintptr)stk - sp, (uintptr)defaultProg | PRECISE | LOOP});\n    -\t\t\tsp = stk->gobuf.sp;\n    -\t\t\tguard = stk->stackguard;\n    -\t\t\tstk = (Stktop*)stk->stackbase;\n    -\t\t\tn++;\n    -\t\t}\n     \t}\n    +\truntime·gentraceback(pc, sp, lr, gp, 0, nil, 0x7fffffff, scanframe, scanbuf, false);\n     }\n    ```
    `scaninterfacedata`と`scanbitvector`の関数シグネチャに`Scanbuf *sbuf`が追加され、内部で`sbuf`を通じてオブジェクトが追加されるようになりました。また、`addstackroots`関数が削除され、`scanstack`関数が新しく追加されました。`scanstack`は`gentraceback`を呼び出し、コールバック関数として`scanframe`を渡すようになりました。`scanframe`も`Scanbuf *sbuf`を引数として受け取るように変更されました。
    
    
  6. GC_G_PTRの追加:

    --- a/src/pkg/runtime/mgc0.c
    +++ b/src/pkg/runtime/mgc0.c
    @@ -176,6 +177,7 @@ static struct {\n     enum {\n     	GC_DEFAULT_PTR = GC_NUM_INSTR,\n     	GC_CHAN,\n    +	GC_G_PTR,\n     
     	GC_NUM_INSTR2\n     };
    

    GC_G_PTRという新しいGC命令が追加されました。これは、G*(ゴルーチン構造体へのポインタ)を処理するためのものです。

コアとなるコードの解説

ScanStackByFramesのデフォルト値変更

ScanStackByFrames1に設定されたことで、Goランタイムはデフォルトでスタックをフレーム単位で走査するようになりました。これは、より正確なGCを実現するために重要です。以前は、スタック全体を保守的に走査する(ポインタでない可能性のある値もポインタとして扱う)ことがありましたが、フレーム単位の走査により、ポインタと非ポインタをより厳密に区別できるようになります。これにより、不要なオブジェクトの保持を防ぎ、メモリ使用効率を向上させます。

Scanbuf構造体の導入

Scanbufは、スタック走査中に見つかったポインタやオブジェクトを一時的に保持するためのバッファです。GCの並列マークフェーズでは、複数のヘルパーゴルーチンが同時に動作するため、共有リソースへのアクセスを効率的に管理する必要があります。Scanbufは、各ヘルパーゴルーチンが自身のローカルバッファとして使用することで、ロックの競合を減らし、並列処理のスループットを向上させます。バッファが満杯になったときにまとめてGCのメインワークバッファにフラッシュすることで、細粒度なロックや同期のオーバーヘッドを削減します。

flushptrbufflushobjbufの引数変更

これらの関数がScanbuf *sbufを引数として受け取るようになったことで、GCのバッファフラッシュロジックがよりモジュール化されました。以前は、複数の引数を個別に渡す必要がありましたが、Scanbufにすべての関連情報がカプセル化されたことで、関数の呼び出しが簡潔になり、コードの保守性が向上しました。また、Scanbuf内部のポインタを直接操作することで、効率的なバッファ管理が可能になります。

scanblockでのScanbufの利用

scanblock関数はGCのマークフェーズの中心的な部分であり、ここでScanbufが導入されたことは、GCの並列処理戦略において重要な意味を持ちます。scanblock内でScanbufを初期化し、ポインタやオブジェクトの収集に利用することで、GCヘルパーゴルーチンが独立してバッファリングを行い、必要に応じてまとめてフラッシュできるようになります。これにより、GCの並列性が向上し、全体的なマークフェーズの実行時間が短縮されます。

scaninterfacedatascanbitvectorへのScanbuf引数の追加

これらの関数は、特定のデータ構造(インターフェースやビットマップで表現されたポインタ情報)を走査してポインタを識別します。Scanbufを引数として受け取るようになったことで、これらの関数内で見つかったポインタも直接Scanbufに格納されるようになります。これにより、GCのワークバッファへのポインタ追加パスが統一され、効率的なバッファリングの恩恵をこれらの低レベルな走査関数でも享受できるようになります。

addstackrootsからscanstackへの変更とgentracebackの利用

この変更は、スタック走査のアーキテクチャにおける最も重要な変更点の一つです。

  • addstackrootsの削除: 以前のaddstackrootsは、スタックを直接走査するロジックを含んでいましたが、これは複雑で保守が困難でした。
  • scanstackの導入とgentracebackの活用: 新しいscanstack関数は、Goランタイムの既存のスタックトレース生成メカニズムであるgentracebackを再利用します。gentracebackは、スタックフレームを正確に辿るための堅牢なロジックを既に持っています。scanstackgentracebackを呼び出し、各スタックフレームが見つかるたびにscanframeコールバック関数を呼び出すように設定します。
  • scanframeScanbuf: scanframeは、gentracebackから渡されたスタックフレーム情報と、scanstackから渡されたScanbufへのポインタを受け取ります。scanframeの内部では、runtime·funcdataを使用して関数のローカル変数や引数のGCメタデータ(ポインタのビットマップなど)を取得し、それに基づいてスタックフレーム内のポインタを正確に識別します。識別されたポインタは、Scanbufを通じてGCのワークバッファに効率的に追加されます。

このアプローチにより、スタック走査のロジックが簡素化され、gentracebackの既存の堅牢性を活用できるようになりました。また、Scanbufを介してポインタをバッファリングすることで、並列処理の効率がさらに向上します。

GC_G_PTRの追加

GC_G_PTRは、G*(ゴルーチン構造体へのポインタ)をGCの対象として扱うための新しい命令です。scanblock内でGC_G_PTR命令が処理されると、そのポインタが指すゴルーチンのスタックがscanstack関数によって走査されるようになります。これは、GCがゴルーチン自体をルートとして認識し、そのスタックを辿ってヒープ上のオブジェクトをマークするためのメカニズムを提供します。

関連リンク

参考にした情報源リンク