[インデックス 12843] ファイルの概要
このコミットは、Goランタイムのガベージコレクション(GC)統計を、各M(OSスレッド)ごとに管理するように変更するものです。これは、並列GCの実現に向けた大きな変更の一部であり、GCのパフォーマンス測定と最適化をより詳細に行うための基盤を構築します。具体的には、GC中に発生する様々なイベント(例えば、ワークバッファのハンドオフ、プロセッサのyield、OSのyield、スリープなど)の統計を、グローバルなカウンタではなく、GCを実行しているM(OSスレッド)に紐付けて記録するように変更しています。これにより、GCの動作をより細かく分析し、並列GCの効率を向上させるための洞察を得ることが可能になります。
コミット
commit d839a809b22a6e7b1b434917bdc48caac32507e8
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Thu Apr 5 20:48:28 2012 +0400
runtime: make GC stats per-M
This is factored out part of:
https://golang.org/cl/5279048/
(Parallel GC)
benchmark old ns/op new ns/op delta
garbage.BenchmarkParser 3999106750 3975026500 -0.60%
garbage.BenchmarkParser-2 3720553750 3719196500 -0.04%
garbage.BenchmarkParser-4 3502857000 3474980500 -0.80%
garbage.BenchmarkParser-8 3375448000 3341310500 -1.01%
garbage.BenchmarkParserLastPause 329401000 324097000 -1.61%
garbage.BenchmarkParserLastPause-2 208953000 214222000 +2.52%
garbage.BenchmarkParserLastPause-4 110933000 111656000 +0.65%
garbage.BenchmarkParserLastPause-8 71969000 78230000 +8.70%
garbage.BenchmarkParserPause 230808842 197237400 -14.55%
garbage.BenchmarkParserPause-2 123674365 125197595 +1.23%
garbage.BenchmarkParserPause-4 80518525 85710333 +6.45%
garbage.BenchmarkParserPause-8 58310243 56940512 -2.35%
garbage.BenchmarkTree2 31471700 31289400 -0.58%
garbage.BenchmarkTree2-2 21536800 21086300 -2.09%
garbage.BenchmarkTree2-4 11074700 10880000 -1.76%
garbage.BenchmarkTree2-8 7568600 7351400 -2.87%
garbage.BenchmarkTree2LastPause 314664000 312840000 -0.58%
garbage.BenchmarkTree2LastPause-2 215319000 210815000 -2.09%
garbage.BenchmarkTree2LastPause-4 110698000 108751000 -1.76%
garbage.BenchmarkTree2LastPause-8 75635000 73463000 -2.87%
garbage.BenchmarkTree2Pause 174280857 173147571 -0.65%
garbage.BenchmarkTree2Pause-2 131332714 129665761 -1.27%
garbage.BenchmarkTree2Pause-4 93803095 93422904 -0.41%
garbage.BenchmarkTree2Pause-8 86242333 85146761 -1.27%
R=rsc
CC=golang-dev
https://golang.org/cl/5987045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/d839a809b22a6e7b1b434917bdc48caac32507e8
元コミット内容
このコミットは、GoランタイムのGC統計をM(OSスレッド)ごとに管理するように変更するものです。これは、並列GCの作業の一部として切り出されたものです。ベンチマーク結果も示されており、全体的なパフォーマンスに大きな影響はないものの、一部のベンチマークでは改善が見られます。
変更の背景
このコミットの主な背景は、Goランタイムにおける並列ガベージコレクション(Parallel GC)の導入です。GoのGCは、アプリケーションの実行を一時停止させる「Stop-the-World (STW)」フェーズを最小限に抑えることを目指して進化してきました。並列GCは、複数のM(OSスレッド)が同時にGC作業を行うことで、STW時間をさらに短縮し、アプリケーションのスループットと応答性を向上させるための重要なステップです。
GC統計をMごとに管理するように変更することで、各MがGC中にどれだけの作業を行い、どれだけの時間を費やしたかを詳細に追跡できるようになります。これにより、並列GCのボトルネックを特定し、負荷分散を最適化するための貴重なデータが得られます。例えば、特定のMがGC作業に過度に集中している場合や、GC中に不必要な待機が発生している場合などを特定し、改善策を講じることが可能になります。
コミットメッセージに記載されている https://golang.org/cl/5279048/ (Parallel GC)
は、この変更が並列GCプロジェクトのより大きな文脈の一部であることを明確に示しています。
前提知識の解説
GoランタイムのM-P-Gモデル
Goランタイムは、ゴルーチン(G)、論理プロセッサ(P)、OSスレッド(M)という3つの主要な抽象化を用いて並行性を管理しています。
- G (Goroutine): Goにおける軽量な実行単位です。OSスレッドよりもはるかに軽量で、数百万個のゴルーチンを同時に実行できます。
- P (Processor/Context): 論理プロセッサを表します。Pは、ゴルーチンを実行するためにMが必要とするコンテキストを提供します。各Pは、実行可能なゴルーチンのローカルキューを保持しています。
- M (Machine/OS Thread): OSスレッドを表します。Mは、Pからゴルーチンを取得し、実際にCPU上でコードを実行する責任を負います。
Goスケジューラは、これらの要素を組み合わせて、多数のゴルーチンを限られた数のOSスレッドに効率的に多重化します。MはPにアタッチされ、Pのローカルキューからゴルーチンを実行します。ゴルーチンがブロックされると、MはPからデタッチされ、Pは別のMにアタッチされて他のゴルーチンを実行し続けることができます。
Goのガベージコレクション (GC)
GoのGCは、主にマーク&スイープアルゴリズムを使用しています。Go 1.5以降、GCは並行(concurrent)かつ並列(parallel)に動作するように設計されており、STWポーズ時間を最小限に抑えています。
- マークフェーズ: GCは、プログラムのルート(グローバル変数やゴルーチンのスタックなど)から到達可能な「生きている」オブジェクトを識別し、マークします。このフェーズは、アプリケーションと並行して実行されます。
- スイープフェーズ: マーク後、GCはマークされていない(到達不能な)オブジェクトからメモリを回収します。このフェーズも並行して動作します。
GoのGCは、世代別GCではなく、すべてのオブジェクトを均一に扱います。また、メモリ内のオブジェクトを再配置しない非コンパクションGCです。並行マーク中にメモリの整合性を維持するために、トライカラーマーキングアルゴリズムとライトバリアを使用しています。
procyield
, osyield
, usleep
これらはGoランタイムの内部関数であり、ゴルーチンのスケジューリングとCPU使用率の最適化、特にビジーウェイトや競合が発生するシナリオで使用されます。
procyield
: 短時間、論理プロセッサを譲渡するために使用されます。スピンロックのシナリオで、CPUを独占するのを防ぎます。CPU固有の命令(例: ARM64のYIELD
、x86のPAUSE
)を使用し、プロセッサに現在のスレッドがスピンウェイトループにあることをヒントとして伝えます。osyield
: CPUをOSスケジューラに譲渡します。procyield
よりも一般的な譲渡操作で、OSが他のスレッドやプロセスをスケジューリングできるようにします。GCルーチンや、ゴルーチンがリソースを待機している場合など、Goランタイムの様々な場所で使用されます。usleep
: 現在のゴルーチンをマイクロ秒単位で指定された期間スリープさせます。通常、nanosleep
システムコールを呼び出します。Goランタイムの内部で、短く正確な遅延のために使用されます。
これらの関数は、Goランタイムの効率的なスケジューリングとリソース管理に貢献する低レベルのメカニズムであり、Go開発者が直接使用することを意図したものではありません。
技術的詳細
このコミットの核心は、GC統計の収集方法をグローバルな集計からMごとの集計へと変更することです。これまでのGC統計は、nhandoff
のようなグローバル変数に集約されていましたが、並列GCでは複数のMが同時にGC作業を行うため、グローバルな統計だけでは各Mの貢献度やボトルネックを正確に把握することが困難になります。
変更の具体的な内容は以下の通りです。
-
GCStats
構造体の導入:src/pkg/runtime/runtime.h
にGCStats
という新しい構造体が定義されました。この構造体は、GC中にMごとに収集される統計情報を保持します。nhandoff
: ワークバッファのハンドオフ回数。nhandoffcnt
: ハンドオフされたオブジェクトの総数。nprocyield
:runtime·procyield
が呼び出された回数。nosyield
:runtime·osyield
が呼び出された回数。nsleep
:runtime·usleep
が呼び出された回数。 この構造体は、uint64
型のみで構成されるように設計されており、uint64[]
にキャストしてメモリをクリアしたり、統計を合計したりする際に効率的です。
-
M
構造体へのGCStats
の追加:src/pkg/runtime/runtime.h
のM
構造体にGCStats gcstats;
フィールドが追加されました。これにより、各Mが自身のGC統計を保持できるようになります。 -
GC統計のMごとの収集:
src/pkg/runtime/mgc0.c
において、GC中に発生するイベントの統計が、グローバル変数ではなく、現在のMのgcstats
フィールドに記録されるように変更されました。getfull
関数内で、runtime·procyield
,runtime·osyield
,runtime·usleep
が呼び出される際に、それぞれm->gcstats.nprocyield++
,m->gcstats.nosyield++
,m->gcstats.nsleep++
がインクリメントされるようになりました。handoff
関数内で、ワークバッファがハンドオフされる際に、m->gcstats.nhandoff++
とm->gcstats.nhandoffcnt += n;
がインクリメントされるようになりました。これにより、ハンドオフの回数とハンドオフされたオブジェクトの総数がMごとに記録されます。
-
cachestats
関数の変更:cachestats
関数は、GC統計を集計する役割を担っています。この関数は、引数としてGCStats *stats
を受け取るように変更されました。stats
がnil
でない場合、すべてのMのgcstats
を合計してstats
に格納し、各Mのgcstats
をクリアします。これにより、GCサイクルごとにMごとの統計をリセットし、次のGCサイクルで新しい統計を収集できるようになります。runtime·gc
関数内で、GCの開始時にcachestats(nil)
が呼び出され、GCの終了時にcachestats(&stats)
が呼び出されるようになりました。これにより、GCの開始時に古い統計がクリアされ、終了時に新しい統計が集計されます。
-
GCトレース出力の変更:
gctrace
が有効な場合に出力されるGC統計のフォーマットが変更され、Mごとの統計(ハンドオフ回数、ハンドオフされたオブジェクト数、yield回数、スリープ回数)も表示されるようになりました。これにより、GCの動作をより詳細にデバッグ・分析できるようになります。
これらの変更により、Goランタイムは並列GCの動作をより正確に測定し、最適化するための基盤を得ました。各MのGC活動を個別に追跡することで、GCのボトルネックを特定し、全体的なパフォーマンスを向上させるための具体的な改善策を講じることが可能になります。
コアとなるコードの変更箇所
このコミットでは、以下の2つのファイルが変更されています。
src/pkg/runtime/mgc0.c
: ガベージコレクションの主要なロジックが実装されているファイルです。GC統計の収集と集計に関する変更が加えられています。src/pkg/runtime/runtime.h
: Goランタイムの主要なデータ構造と関数の宣言が含まれるヘッダーファイルです。GCStats
構造体の定義と、M
構造体へのgcstats
フィールドの追加が行われています。
変更の概要:
src/pkg/runtime/mgc0.c
: 47行の追加、19行の削除。src/pkg/runtime/runtime.h
: 12行の追加。
合計で42行の追加と17行の削除が行われています。
コアとなるコードの解説
src/pkg/runtime/mgc0.c
の変更点
-
nhandoff
グローバル変数の削除:--- a/src/pkg/runtime/mgc0.c +++ b/src/pkg/runtime/mgc0.c @@ -67,9 +67,6 @@ enum { // uint32 runtime·worldsema = 1; -// TODO: Make these per-M. -static uint64 nhandoff; - static int32 gctrace;
これまでグローバル変数として存在していた
nhandoff
が削除されました。これは、ハンドオフ統計がMごとに管理されるようになるためです。コメントの// TODO: Make these per-M.
が削除されていることからも、この変更が計画的なものであることがわかります。 -
getfull
関数における統計の追加:--- a/src/pkg/runtime/mgc0.c +++ b/src/pkg/runtime/mgc0.c @@ -529,12 +526,16 @@ getfull(Workbuf *b) } if(work.nwait == work.nproc) return nil; -\t\tif(i < 10)\n+\t\tif(i < 10) {\n+\t\t\tm->gcstats.nprocyield++;\n \truntime·procyield(20);\n -\t\telse if(i < 20)\n+\t\t} else if(i < 20) {\n+\t\t\tm->gcstats.nosyield++;\n \truntime·osyield(); -\t\telse\n+\t\t} else {\n+\t\t\tm->gcstats.nsleep++;\n \truntime·usleep(100);\n+\t\t}\n }\n }
getfull
関数は、GCワーカーがワークバッファからオブジェクトを取得する際に、他のワーカーが作業を完了するのを待つためにprocyield
、osyield
、usleep
を呼び出すことがあります。これらの呼び出しの際に、現在のMのgcstats
にそれぞれのカウンタ(nprocyield
,nosyield
,nsleep
)がインクリメントされるようになりました。これにより、GC中にMがどれだけCPUを譲渡したり、スリープしたりしたかを追跡できます。 -
handoff
関数における統計の追加:--- a/src/pkg/runtime/mgc0.c +++ b/src/pkg/runtime/mgc0.c @@ -550,7 +551,8 @@ handoff(Workbuf *b) b->nobj -= n; b1->nobj = n; runtime·memmove(b1->obj, b->obj+b->nobj, n*sizeof b1->obj[0]); -\tnhandoff += n;\n+\tm->gcstats.nhandoff++;\n+\tm->gcstats.nhandoffcnt += n;\n // Put b on full list - let first half of b get stolen. runtime·lock(&work.fmu);
handoff
関数は、GCワーカーが自身のワークバッファの一部を他のワーカーに「ハンドオフ」する際に呼び出されます。この変更により、ハンドオフの回数(m->gcstats.nhandoff
)とハンドオフされたオブジェクトの総数(m->gcstats.nhandoffcnt
)が、ハンドオフを実行したMのgcstats
に記録されるようになりました。 -
cachestats
関数の変更:--- a/src/pkg/runtime/mgc0.c +++ b/src/pkg/runtime/mgc0.c @@ -852,20 +854,30 @@ stealcache(void) } static void -cachestats(void) +cachestats(GCStats *stats) { M *m; MCache *c; int32 i; uint64 stacks_inuse; uint64 stacks_sys; +\tuint64 *src, *dst;\n +\tif(stats)\n+\t\truntime·memclr((byte*)stats, sizeof(*stats));\n stacks_inuse = 0; stacks_sys = 0; for(m=runtime·allm; m; m=m->alllink) { runtime·purgecachedstats(m); stacks_inuse += m->stackalloc->inuse; stacks_sys += m->stackalloc->sys; +\t\tif(stats) {\n+\t\t\tsrc = (uint64*)&m->gcstats;\n+\t\t\tdst = (uint64*)stats;\n+\t\t\tfor(i=0; i<sizeof(*stats)/sizeof(uint64); i++)\n+\t\t\t\tdst[i] += src[i];\n+\t\t\truntime·memclr((byte*)&m->gcstats, sizeof(m->gcstats));\n+\t\t}\n c = m->mcache; for(i=0; i<nelem(c->local_by_size); i++) { mstats.by_size[i].nmalloc += c->local_by_size[i].nmalloc;
cachestats
関数は、GCの開始時と終了時に呼び出され、メモリキャッシュの統計を更新します。この関数は、GCStats *stats
という引数を取るように変更されました。stats
がnil
でない場合、すべてのMのm->gcstats
フィールドを合計して*stats
に格納し、その後各Mのm->gcstats
をゼロクリアします。これにより、GCサイクルごとにMごとの統計をリセットし、次のGCサイクルで新しい統計を収集できるようになります。src
とdst
ポインタを使ってuint64
の配列としてGCStats
構造体を扱うことで、効率的に統計を合計し、クリアしています。これは、GCStats
構造体がuint64
型のみで構成されているという設計上の制約を利用しています。
-
runtime·gc
関数におけるcachestats
の呼び出しとトレース出力の変更:--- a/src/pkg/runtime/mgc0.c +++ b/src/pkg/runtime/mgc0.c @@ -885,6 +897,7 @@ runtime·gc(int32 force) uint64 heap0, heap1, obj0, obj1; byte *p; bool extra; +\tGCStats stats;\n // The gc is turned off (via enablegc) until // the bootstrap has completed. @@ -920,12 +933,11 @@ runtime·gc(int32 force) } t0 = runtime·nanotime(); -\tnhandoff = 0;\n m->gcing = 1; runtime·stoptheworld(); -\tcachestats();\n+\tcachestats(nil);\n heap0 = mstats.heap_alloc; obj0 = mstats.nmalloc - mstats.nfree; @@ -955,13 +967,13 @@ runtime·gc(int32 force) t2 = runtime·nanotime(); stealcache(); -\tcachestats();\n+\tcachestats(&stats);\n mstats.next_gc = mstats.heap_alloc+mstats.heap_alloc*gcpercent/100; m->gcing = 0; -\tm->locks++;\t// disable gc during the mallocs in newproc\n \tif(finq != nil) { +\t\tm->locks++;\t// disable gc during the mallocs in newproc\n \t// kick off or wake up goroutine to run queued finalizers \tif(fing == nil) \t\tfing = runtime·newproc1((byte*)runfinq, nil, 0, 0, runtime·gc); @@ -969,10 +981,9 @@ runtime·gc(int32 force) \t\tfingwait = 0; \t\truntime·ready(fing); \t} +\t\tm->locks--;\n }\n -\tm->locks--;\n -\tcachestats();\n heap1 = mstats.heap_alloc; obj1 = mstats.nmalloc - mstats.nfree; @@ -985,11 +996,13 @@ runtime·gc(int32 force) runtime·printf("pause %D\\n", t3-t0); if(gctrace) { -\t\truntime·printf("gc%d(%d): %D+%D+%D ms %D -> %D MB %D -> %D (%D-%D) objects %D handoff\\n",\n+\t\truntime·printf("gc%d(%d): %D+%D+%D ms, %D -> %D MB %D -> %D (%D-%D) objects,\\"\n+\t\t\t\t" %D(%D) handoff, %D/%D/%D yields\\n",\n \tmstats.numgc, work.nproc, (t1-t0)/1000000, (t2-t1)/1000000, (t3-t2)/1000000, \theap0>>20, heap1>>20, obj0, obj1, \tmstats.nmalloc, mstats.nfree, -\t\t\tnhandoff);\n+\t\t\tstats.nhandoff, stats.nhandoffcnt,\n+\t\t\tstats.nprocyield, stats.nosyield, stats.nsleep);\n } \t runtime·MProf_GC(); @@ -1022,7 +1035,7 @@ runtime·ReadMemStats(MStats *stats) runtime·semacquire(&runtime·worldsema); m->gcing = 1; runtime·stoptheworld(); -\tcachestats();\n+\tcachestats(nil);\n *stats = mstats; m->gcing = 0; runtime·semrelease(&runtime·worldsema);
runtime·gc
関数内で、GC開始時にcachestats(nil)
が呼び出され、すべてのMのGC統計がクリアされます。- GC終了時に
cachestats(&stats)
が呼び出され、すべてのMのGC統計が集計され、ローカル変数stats
に格納されます。 gctrace
が有効な場合の出力フォーマットが変更され、stats.nhandoff
,stats.nhandoffcnt
,stats.nprocyield
,stats.nosyield
,stats.nsleep
といったMごとの集計値が表示されるようになりました。これにより、GCの動作をより詳細に分析できるようになります。runtime·ReadMemStats
関数でも、cachestats()
の呼び出しがcachestats(nil)
に変更され、統計のクリアが行われるようになりました。
src/pkg/runtime/runtime.h
の変更点
-
GCStats
構造体の定義:--- a/src/pkg/runtime/runtime.h +++ b/src/pkg/runtime/runtime.h @@ -71,6 +71,7 @@ typedef struct Complex128 Complex128; typedef struct WinCall WinCall; typedef struct Timers Timers; typedef struct Timer Timer; +typedef struct GCStats GCStats;\n /* * per-cpu declaration. @@ -166,6 +167,16 @@ struct Gobuf byte*\tpc; G*\tg; }; +struct GCStats\n+{\n+\t// the struct must consist of only uint64\'s,\n+\t// because it is casted to uint64[].\n+\tuint64\tnhandoff;\n+\tuint64\tnhandoffcnt;\n+\tuint64\tnprocyield;\n+\tuint64\tnosyield;\n+\tuint64\tnsleep;\n+};\n struct G { byte*\tstackguard; // cannot move - also known to linker, libmach, runtime/cgo @@ -243,6 +254,7 @@ struct M uintptr waitsema; // semaphore for parking on locks uint32 waitsemacount; uint32 waitsemalock; +\tGCStats\tgcstats;\n #ifdef GOOS_windows void*\tthread; // thread handle
GCStats
構造体が新しく定義されました。この構造体は、GC中にMごとに収集される統計情報を保持します。すべてのフィールドがuint64
型であるというコメントは、cachestats
関数での効率的な操作(uint64[]
へのキャスト)を可能にするための設計上の考慮事項を示しています。 -
M
構造体へのgcstats
フィールドの追加:M
構造体にGCStats gcstats;
フィールドが追加されました。これにより、各OSスレッド(M)が自身のGC統計を保持できるようになり、Mごとの詳細なGC活動の追跡が可能になります。
これらの変更は、GoランタイムのGCメカニズムをより洗練させ、特に並列GCの文脈において、パフォーマンスのボトルネックを特定し、最適化するための重要な基盤を築いています。
関連リンク
- 元の並列GCの変更セット: https://golang.org/cl/5279048/
- このコミットの変更セット: https://golang.org/cl/5987045
参考にした情報源リンク
- Go runtime M goroutine P: https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQEyRt20FBtIMfpkNmODtdmGo-L1jlwHO0RQe_hzw2MaEowlJGLTYogrhPfqNhn1AkgjMmJJTVagHqqpXlbJ6d7Fi51SIcw4K4n2iTDAT4zJl3dthazqrqtSuXto14-D
- Go parallel garbage collection: https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQH9t1QUdteQjMxyHO_PdGR-CxCU8jiwHWh7DhzX3GhasqRQNuvCExt455z_391RCMVeSAO7lQpXNoZ2Zan4t96CHytiJrwzv2gYkff_oTO2aDtpKKo8yxIoUTG4j7Aa6RyUosR9k9TveRmwHU2mxxTPlJdWfGdWYQ8yZJZ_qhTlojTCpHpq
- Go GC nhandoff: https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQEN90RIRY1YzzKIlsP8dPKsGklaChKXpSj9Zaym4Ic0mfesxlHG6kH98V-OmPIFkvgRLotQC9oQBEQU9nB2Qe_Gw9ICS2V7P4ZB65Kq0Xvn1rdrmR5zKxYI-7W7haO5SyG5dGd4rc_qd3auiv04pOTnn3siqZHbAuT511amwfQP5IIqlN8t7wNQR0I=
- Go runtime procyield osyield usleep: https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHW_b2mnoNp7qv8UbgEOFj-1omj92W16DkhYwoU9YpItDt2i8yiyal6XLQDUktE6C9vw3ricNrITtS0yf_Tvj8QwfjuC-v_DcoSgtdUoIOXMyJ3OzGvFFEjfoyDvw==