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

[インデックス 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の貢献度やボトルネックを正確に把握することが困難になります。

変更の具体的な内容は以下の通りです。

  1. GCStats構造体の導入: src/pkg/runtime/runtime.hGCStatsという新しい構造体が定義されました。この構造体は、GC中にMごとに収集される統計情報を保持します。

    • nhandoff: ワークバッファのハンドオフ回数。
    • nhandoffcnt: ハンドオフされたオブジェクトの総数。
    • nprocyield: runtime·procyieldが呼び出された回数。
    • nosyield: runtime·osyieldが呼び出された回数。
    • nsleep: runtime·usleepが呼び出された回数。 この構造体は、uint64型のみで構成されるように設計されており、uint64[]にキャストしてメモリをクリアしたり、統計を合計したりする際に効率的です。
  2. M構造体へのGCStatsの追加: src/pkg/runtime/runtime.hM構造体にGCStats gcstats;フィールドが追加されました。これにより、各Mが自身のGC統計を保持できるようになります。

  3. 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ごとに記録されます。
  4. cachestats関数の変更: cachestats関数は、GC統計を集計する役割を担っています。この関数は、引数としてGCStats *statsを受け取るように変更されました。

    • statsnilでない場合、すべてのMのgcstatsを合計してstatsに格納し、各Mのgcstatsをクリアします。これにより、GCサイクルごとにMごとの統計をリセットし、次のGCサイクルで新しい統計を収集できるようになります。
    • runtime·gc関数内で、GCの開始時にcachestats(nil)が呼び出され、GCの終了時にcachestats(&stats)が呼び出されるようになりました。これにより、GCの開始時に古い統計がクリアされ、終了時に新しい統計が集計されます。
  5. GCトレース出力の変更: gctraceが有効な場合に出力されるGC統計のフォーマットが変更され、Mごとの統計(ハンドオフ回数、ハンドオフされたオブジェクト数、yield回数、スリープ回数)も表示されるようになりました。これにより、GCの動作をより詳細にデバッグ・分析できるようになります。

これらの変更により、Goランタイムは並列GCの動作をより正確に測定し、最適化するための基盤を得ました。各MのGC活動を個別に追跡することで、GCのボトルネックを特定し、全体的なパフォーマンスを向上させるための具体的な改善策を講じることが可能になります。

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

このコミットでは、以下の2つのファイルが変更されています。

  1. src/pkg/runtime/mgc0.c: ガベージコレクションの主要なロジックが実装されているファイルです。GC統計の収集と集計に関する変更が加えられています。
  2. 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 の変更点

  1. 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.が削除されていることからも、この変更が計画的なものであることがわかります。

  2. 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ワーカーがワークバッファからオブジェクトを取得する際に、他のワーカーが作業を完了するのを待つためにprocyieldosyieldusleepを呼び出すことがあります。これらの呼び出しの際に、現在のMのgcstatsにそれぞれのカウンタ(nprocyield, nosyield, nsleep)がインクリメントされるようになりました。これにより、GC中にMがどれだけCPUを譲渡したり、スリープしたりしたかを追跡できます。

  3. 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に記録されるようになりました。

  4. 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という引数を取るように変更されました。

    • statsnilでない場合、すべてのMのm->gcstatsフィールドを合計して*statsに格納し、その後各Mのm->gcstatsをゼロクリアします。これにより、GCサイクルごとにMごとの統計をリセットし、次のGCサイクルで新しい統計を収集できるようになります。
    • srcdstポインタを使ってuint64の配列としてGCStats構造体を扱うことで、効率的に統計を合計し、クリアしています。これは、GCStats構造体がuint64型のみで構成されているという設計上の制約を利用しています。
  5. 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 の変更点

  1. 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[]へのキャスト)を可能にするための設計上の考慮事項を示しています。

  2. M 構造体への gcstats フィールドの追加: M構造体にGCStats gcstats;フィールドが追加されました。これにより、各OSスレッド(M)が自身のGC統計を保持できるようになり、Mごとの詳細なGC活動の追跡が可能になります。

これらの変更は、GoランタイムのGCメカニズムをより洗練させ、特に並列GCの文脈において、パフォーマンスのボトルネックを特定し、最適化するための重要な基盤を築いています。

関連リンク

参考にした情報源リンク