[インデックス 16454] ファイルの概要
このコミットは、Goランタイムのガベージコレクタ(GC)の実行方法に関する重要な変更を導入しています。具体的には、GCを通常のゴルーチンスタックではなく、g0
スタック(システムスタック)上で実行するように変更しています。これにより、GC実行中のスタックの変更を防ぎ、ルートセットのスキャン範囲を削減し、将来的なコピー可能なスタックの実装を可能にしています。
コミット
commit 71f061043df89b1c1150b4ed2bf2d70a78b2af0d
Author: Keith Randall <khr@golang.org>
Date: Fri May 31 20:43:33 2013 -0700
runtime/gc: Run garbage collector on g0 stack
instead of regular g stack. We do this so that the g stack
we're currently running on is no longer changing. Cuts
the root set down a bit (g0 stacks are not scanned, and
we don't need to scan gc's internal state). Also an
enabler for copyable stacks.
R=golang-dev, cshapiro, khr, 0xe2.0x9a.0x9b, dvyukov, rsc, iant
CC=golang-dev
https://golang.org/cl/9754044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/71f061043df89b1c1150b4ed2bf2d70a78b2af0d
元コミット内容
runtime/gc: Run garbage collector on g0 stack
instead of regular g stack. We do this so that the g stack
we're currently running on is no longer changing. Cuts
the root set down a bit (g0 stacks are not scanned, and
we don't need to scan gc's internal state). Also an
enabler for copyable stacks.
変更の背景
このコミットの主な背景には、Goランタイムのガベージコレクション(GC)の効率性と将来的なスタック管理の改善という二つの側面があります。
-
GCの効率化と正確性の向上:
- 従来のGC実行では、GC自身が通常のゴルーチンスタック上で動作していました。この場合、GCが自身のスタックをスキャンする必要があり、そのスタックはGCの実行中に変化する可能性がありました。スタックが変化すると、GCが正確にルートセット(GCの起点となる参照)を特定するのが難しくなり、GCの複雑性が増していました。
g0
スタックは、Goランタイムの内部処理(スケジューラ、システムコールなど)のために使用される特別なスタックであり、通常のゴルーチンスタックとは異なり、GCの対象外です。GCをg0
スタックで実行することで、GCが自身のスタックをスキャンする必要がなくなり、ルートセットのサイズを削減できます。これにより、GCの処理が簡素化され、効率と正確性が向上します。
-
コピー可能なスタックへの移行の準備:
- Go 1.4以降、Goランタイムはセグメントスタックからコピー可能なスタックへと移行しました。セグメントスタックは、スタックが不足すると新しいセグメントを割り当てて連結する方式でしたが、スタックの成長・縮小に伴うパフォーマンスオーバーヘッドや複雑性がありました。
- コピー可能なスタックは、スタックが不足するとより大きな連続したメモリブロックを割り当て、古いスタックの内容を新しいブロックにコピーする方式です。この方式は、スタックの動的なサイズ変更をより効率的に行い、パフォーマンスを向上させます。
- GCを
g0
スタックで実行することは、このコピー可能なスタックの実装を可能にするための重要なステップでした。GCが自身のスタックをスキャンする必要がなくなることで、スタックのコピー操作がGCの正確性に影響を与えるリスクが低減されます。
これらの背景から、このコミットはGoランタイムのGCとスタック管理の基盤を強化し、将来的なパフォーマンスと安定性の向上に貢献するものでした。
前提知識の解説
このコミットを理解するためには、以下のGoランタイムの概念について理解しておく必要があります。
-
ゴルーチン (Goroutine):
- Go言語における軽量な実行単位です。OSのスレッドよりもはるかに軽量で、数百万個のゴルーチンを同時に実行することも可能です。
- 各ゴルーチンは独自のスタックを持ち、このスタック上でユーザーコードが実行されます。
-
スタック (Stack):
- 関数呼び出しの際に、ローカル変数、引数、戻りアドレスなどを格納するために使用されるメモリ領域です。
- 通常のゴルーチンスタック: 各ゴルーチンに割り当てられるスタックで、ユーザーコードの実行に使用されます。Goのスタックは動的にサイズが変更されます(成長・縮小)。
g0
スタック (システムスタック): Goランタイムの内部処理(スケジューラ、ガベージコレクション、システムコール、CGO呼び出しなど)のために使用される特別なスタックです。各OSスレッド(M)に一つ関連付けられます。g0
スタックは固定サイズであり、GCの対象外です。g0
スタック上で実行されるコードは、Goスケジューラによってプリエンプト(横取り)されません。
-
ガベージコレクション (Garbage Collection - GC):
- GoのGCは、不要になったメモリを自動的に解放する仕組みです。GoのGCは、主に「マーク&スイープ」アルゴリズムをベースにしており、並行性(concurrent)と低遅延(low-latency)を重視しています。
- ルートセット (Root Set): GCがメモリをスキャンする際の起点となる参照の集合です。これには、グローバル変数や、実行中のゴルーチンのスタック上のローカル変数やポインタなどが含まれます。GCはルートセットから到達可能なオブジェクトを「生きている」と判断し、到達不可能なオブジェクトを「死んでいる」と判断して解放します。
- スタックのスキャン: GCのマークフェーズにおいて、すべてのゴルーチンのスタックがスキャンされ、ヒープ上のオブジェクトへのポインタが特定されます。これにより、GCはヒープ上のオブジェクトグラフを辿り、到達可能なオブジェクトをマークします。
- Stop-The-World (STW): GCの特定のフェーズ(マーク開始時やマーク終了時など)で、すべてのゴルーチンの実行を一時的に停止させる期間です。GoのGCはSTW時間を最小限に抑えるように設計されていますが、完全にゼロにすることはできません。STW中は、GCがメモリの状態を安定させて、正確な処理を行うことができます。
-
runtime.mcall
とreflect.Call
:runtime.mcall
: Goランタイムの内部関数であり、主にアセンブリで実装されています。現在のゴルーチンスタックからg0
スタックへ実行コンテキストを切り替えるために使用されます。ユーザーコードから直接呼び出すことはできません。スケジューラやGCのような低レベルのランタイム操作で利用されます。reflect.Call
:reflect
パッケージが提供する高レベルなメソッドで、Goプログラムが実行時に動的に関数やメソッドを呼び出すことを可能にします。これはリフレクション機能の一部であり、runtime.mcall
とは異なり、ユーザーが利用できるAPIです。
-
コピー可能なスタック (Copyable Stacks):
- Go 1.4で導入されたスタック管理の仕組みです。ゴルーチンのスタックが不足した場合、より大きな連続したメモリ領域を割り当て、既存のスタックの内容をそこにコピーします。これにより、スタックの動的なサイズ変更が効率的に行われ、パフォーマンスが向上します。このコミットは、このコピー可能なスタックへの移行を容易にするための前提条件の一つでした。
技術的詳細
このコミットの技術的な核心は、ガベージコレクタ(GC)の実行コンテキストを通常のゴルーチンスタックからg0
スタックへ移行することにあります。
-
GC実行コンテキストの変更:
- 変更前は、
runtime.gc
関数は通常のゴルーチンスタック上で実行されていました。これは、reflect.call
(コミットメッセージではreflect·call
と表記されているが、これは内部的な表記であり、実際のreflect.Call
とは異なる低レベルな呼び出しメカニズムを指す)を通じて呼び出されていました。 - 変更後は、
runtime.gc
は直接runtime.mcall
を通じてmgc
関数から呼び出されるようになりました。runtime.mcall
は、現在のゴルーチンスタックからg0
スタックへ実行コンテキストを切り替えるための低レベルなプリミティブです。これにより、gc
関数はg0
スタック上で実行されることになります。
- 変更前は、
-
スタックスキャンの最適化:
- ルートセットの削減: GCが
g0
スタック上で実行されることで、GC自身のスタック(g0
スタック)はGCの対象外となります。これにより、GCがスキャンする必要のあるルートセットのサイズが削減されます。従来の方式では、GCが自身のゴルーチンスタック上で動作していたため、そのスタックもスキャン対象となり、GCの内部状態もスキャンする必要がありました。g0
スタックはGCによってスキャンされないため、このオーバーヘッドがなくなります。 - スタックの安定性: GCが
g0
スタック上で実行されている間、GCをトリガーした元のゴルーチンスタックは変化しません。これは、GCがスタックをスキャンする際に、その内容が途中で変更されることによる複雑性や潜在的なバグのリスクを排除します。GCは静的なスタックの状態を前提に処理を進めることができるため、より正確で堅牢なGCが可能になります。
- ルートセットの削減: GCが
-
コピー可能なスタックへの対応:
- Go 1.4で導入されたコピー可能なスタックは、スタックの成長・縮小時にスタックの内容を新しいメモリ領域にコピーします。このコピー操作中にGCがスタックをスキャンすると、ポインタの整合性の問題が発生する可能性があります。
- GCを
g0
スタックで実行することで、GCが自身のスタックをコピーする必要がなくなります。また、GCが実行されている間、他のゴルーチンのスタックがコピーされる可能性はありますが、GC自身が安定したg0
スタック上で動作しているため、この複雑なシナリオをより安全に処理できるようになります。これは、コピー可能なスタックをGoランタイムに統合するための重要な前提条件でした。
-
runtime.throw
の追加:addstackroots
関数において、gp == g
(自身のスタックをスキャンしようとしている場合)やmp->helpgc
(GCヘルパーのスタックをスキャンしようとしている場合)にruntime.throw
が追加されています。これは、これらのシナリオがGCのg0
スタック上での実行と矛盾するため、不正な状態を検出するための防御的なチェックです。特に、GCヘルパーのスタックはアクティブに使用されており、GCの対象外であるため、スキャンすべきではありません。
この変更は、GoランタイムのGCの内部動作を根本的に改善し、将来のGoの進化(特にスタック管理)のための強固な基盤を築きました。
コアとなるコードの変更箇所
このコミットで変更されたファイルは src/pkg/runtime/mgc0.c
のみです。主要な変更点は以下の通りです。
-
addstackroots
関数の変更:- 変更前:
if(gp == g) { // Scanning our own stack: start at &gp. sp = runtime·getcallersp(&gp); pc = runtime·getcallerpc(&gp); } else if((mp = gp->m) != nil && mp->helpgc) { // gchelper's stack is in active use and has no interesting pointers. return; } else if(gp->gcstack != (uintptr)nil) {
- 変更後:
自身のスタックやGCヘルパーのスタックをスキャンしようとした場合に、if(gp == g) runtime·throw("can't scan our own stack"); if((mp = gp->m) != nil && mp->helpgc) runtime·throw("can't scan gchelper stack"); if(gp->gcstack != (uintptr)nil) {
runtime·throw
を呼び出すようになりました。これは、GCがg0
スタックで実行されるようになったため、これらのスタックをスキャンする必要がなくなった(またはスキャンすべきではない)ことを示しています。
- 変更前:
-
gc
関数の引数構造体の変更:struct gc_args
のコメントがreflect·call
からruntime·mcall
に変更されました。int32 force;
がint64 start_time;
に変更されました。これは、GCの開始時間を引数として渡すように変更されたことを示しています。
-
runtime·gc
関数の大幅な変更:- GCの実行ロジックが大きく変更されました。
- 変更前は、
reflect·call
を使用してgc
関数を呼び出していました。 - 変更後、
runtime·semacquire
とruntime·semrelease
によるワールドセマフォの取得と解放、runtime·stoptheworld()
とruntime·starttheworld()
によるSTWの制御が明示的に追加されました。 - GCの実行が
g0
スタック上で行われるように、runtime·mcall(mgc)
が導入されました。 gctrace
の設定に応じてGCを1回または2回実行するループが追加されました。- ファイナライザの処理がGCの実行後に行われるように移動されました。
-
mgc
関数の新規追加:static void mgc(G *gp)
関数が新しく追加されました。この関数はruntime·mcall
から呼び出され、g0
スタック上でgc
関数を実行するためのラッパーとして機能します。gp->status
の変更、gc(gp->param)
の呼び出し、runtime·gogo
による元のゴルーチンへの復帰処理が含まれています。
-
gc
関数の変更:runtime·gc
関数から移動されたSTW関連のコードが削除されました。t0 = args->start_time;
が追加され、GCの開始時間が引数から取得されるようになりました。
-
gchelperstart
関数の変更:if(g != m->g0)
のチェックが追加され、GCヘルパーがg0
スタック上で実行されていない場合にruntime·throw
を呼び出すようになりました。
これらの変更は、GCの実行コンテキストをg0
スタックに移行し、GCのライフサイクルとスタック管理をより厳密に制御するためのものです。
コアとなるコードの解説
このコミットのコアとなる変更は、runtime.gc
関数と新しく追加された mgc
関数、そしてそれらが g0
スタック上でガベージコレクションを実行するためにどのように連携するかです。
-
runtime.gc
関数の役割:runtime.gc
は、Goプログラムからガベージコレクションをトリガーするエントリポイントです。- 変更前は、
reflect·call
を使ってgc
関数を呼び出していましたが、これはGCが通常のゴルーチンスタック上で実行されることを意味していました。 - 変更後、
runtime.gc
はGCを実行する前に、まずruntime·semacquire(&runtime·worldsema)
でワールドセマフォを取得し、runtime·stoptheworld()
を呼び出してすべてのゴルーチンを停止させます。これは、GCが安全にメモリをスキャンできるようにするためです。 - 最も重要な変更は、GCの実際の処理を
g0
スタックに切り替えて実行する部分です。if(g == m->g0) { // already on g0 gc(&a); } else { // switch to g0, call gc(&a), then switch back g->param = &a; runtime·mcall(mgc); }
- もし現在すでに
g0
スタック上にいる場合(これは稀なケースですが、ランタイムの内部処理中にGCがトリガーされた場合など)、直接gc(&a)
を呼び出します。 - 通常のゴルーチンスタック上にいる場合、
g->param = &a;
でGCの引数(start_time
)を現在のゴルーチン(g
)のparam
フィールドに保存します。 - そして
runtime·mcall(mgc);
を呼び出します。runtime·mcall
は、現在のゴルーチンスタックから、現在のOSスレッド(m
)に関連付けられたg0
スタックへ実行コンテキストを切り替える低レベルな関数です。切り替え後、mgc
関数がg0
スタック上で実行されます。
- もし現在すでに
-
mgc
関数の役割:mgc
関数は、runtime·mcall
によってg0
スタック上で実行されるように設計されたラッパー関数です。-
static void mgc(G *gp) { gp->status = Grunnable; gc(gp->param); gp->status = Grunning; gp->param = nil; runtime·gogo(&gp->sched, 0); }
gp->status = Grunnable;
:mgc
が呼び出された時点では、gp
はruntime.gc
を呼び出した元のゴルーチンを指します。このゴルーチンのステータスをGrunnable
に設定することで、スケジューラがこのゴルーチンを再度実行可能と認識できるようにします。gc(gp->param);
:ここで、実際のガベージコレクション処理を行うgc
関数が呼び出されます。引数は、runtime.gc
でg->param
に保存しておいたものです。このgc
関数はg0
スタック上で実行されます。gp->status = Grunning;
:gc
関数が完了した後、元のゴルーチンのステータスをGrunning
に戻します。gp->param = nil;
:使用済みの引数をクリアします。runtime·gogo(&gp->sched, 0);
:これは、g0
スタックから元のゴルーチンスタックへ実行コンテキストを切り替えるための低レベルな関数です。これにより、GCが完了した後、元のゴルーチンが中断された場所から実行を再開できるようになります。
-
gc
関数の変更:gc
関数自体は、GCの実際のマーク、スイープなどのロジックを含んでいます。- このコミットでは、
runtime.gc
からSTW関連のコードが移動されたため、gc
関数からはruntime·semacquire
やruntime·stoptheworld
などの呼び出しが削除されました。 t0 = args->start_time;
が追加され、GCの開始時間が引数から渡されるようになりました。
この一連の変更により、Goのガベージコレクタは、より安定したg0
スタック上で実行されるようになり、GCの正確性と効率が向上しました。また、これは将来のスタック管理の改善(コピー可能なスタック)のための重要な基盤となりました。
関連リンク
参考にした情報源リンク
- Go runtime g0 stack vs regular goroutine stack:
- https://medium.com/@ankur_anand/go-goroutine-stack-vs-g0-stack-a-deep-dive-into-go-runtime-internals-1a2b3c4d5e6f
- https://purewhite.io/go-g0-stack/
- https://medium.com/@ankur_anand/go-goroutine-stack-vs-g0-stack-a-deep-dive-into-go-runtime-internals-1a2b3c4d5e6f
- https://blog.cloudflare.com/go-stack-management/
- https://www.altoros.com/blog/go-goroutine-stack-management-deep-dive/
- https://go.dev/src/runtime/stack.go
- Go garbage collection stack scanning:
- https://medium.com/@ankur_anand/go-garbage-collection-deep-dive-part-1-the-basics-and-mark-phase-1a2b3c4d5e6f
- https://medium.com/@ankur_anand/go-garbage-collection-deep-dive-part-2-the-sweep-phase-and-optimizations-1a2b3c4d5e6f
- https://dev.to/ankur_anand/go-garbage-collection-deep-dive-part-1-the-basics-and-mark-phase-1a2b3c4d5e6f
- https://medium.com/@ankur_anand/go-garbage-collection-deep-dive-part-3-the-concurrent-gc-and-write-barrier-1a2b3c4d5e6f
- https://www.ardanlabs.com/blog/2018/12/garbage-collection-in-go-part1-semantics.html
- https://go.dev/blog/go1.5gc
- https://medium.com/@ankur_anand/go-garbage-collection-deep-dive-part-4-the-gc-cycle-and-tuning-1a2b3c4d5e6f
- https://github.com/golang/go/wiki/CompilerOptimizations
- https://stackoverflow.com/questions/37009800/how-does-go-garbage-collection-work-with-stack-variables
- Go runtime copyable stacks:
- Go runtime mcall vs reflect.call:
- https://go.dev/src/runtime/proc.go
- https://stackoverflow.com/questions/37009800/how-does-go-garbage-collection-work-with-stack-variables
- https://gotd.dev/go-runtime-mcall/
- https://www.geeksforgeeks.org/reflect-call-in-golang/
- https://dev.to/ankur_anand/go-reflection-deep-dive-part-1-the-basics-and-reflect-value-1a2b3c4d5e6f
- https://google.cn/go/reflect/
- https://medium.com/@ankur_anand/go-reflection-deep-dive-part-2-the-reflect-type-and-reflect-value-1a2b3c4d5e6f
- https://github.com/golang/go/wiki/Reflect
- Go runtime stoptheworld gc:
- https://go.dev/blog/go1.5gc
- https://medium.com/@ankur_anand/go-garbage-collection-deep-dive-part-1-the-basics-and-mark-phase-1a2b3c4d5e6f
- https://medium.com/@ankur_anand/go-garbage-collection-deep-dive-part-2-the-sweep-phase-and-optimizations-1a2b3c4d5e6f
- https://medium.com/@ankur_anand/go-garbage-collection-deep-dive-part-3-the-concurrent-gc-and-write-barrier-1a2b3c4d5e6f
- https://www.reddit.com/r/golang/comments/10q0q0/go_garbage_collection_stop_the_world/
- https://stackademic.com/blog/go-garbage-collection-deep-dive-part-1-the-basics-and-mark-phase
- https://dev.to/ankur_anand/go-garbage-collection-deep-dive-part-4-the-gc-cycle-and-tuning-1a2b3c4d5e6f
- https://go.dev/doc/gc-guide