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

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

このコミットは、Go言語のランタイムにおけるガベージコレクタ(GC)に統計収集機能を追加するものです。具体的には、GCの実行中に様々な内部メトリクスを収集し、GOGCTRACE=1が設定され、かつCollectStats定数が非ゼロの場合にそれらの統計情報を出力する機能が導入されました。これにより、開発者はGCの動作をより詳細に分析し、パフォーマンスのボトルネックを特定できるようになります。

コミット

commit cea46387b9c042ea19d0b30c7accf0f9a45d8e11
Author: Jan Ziak <0xe2.0x9a.0x9b@gmail.com>
Date:   Mon Mar 4 16:54:37 2013 +0100

    runtime: add garbage collector statistics
    
    If the constant CollectStats is non-zero and GOGCTRACE=1
    the garbage collector will print basic statistics about executed
    GC instructions.
    
    R=golang-dev, dvyukov
    CC=golang-dev, rsc
    https://golang.org/cl/7413049

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

https://github.com/golang/go/commit/cea46387b9c042ea19d0b30c7accf0f9a45d8e11

元コミット内容

Goランタイムのガベージコレクタに統計収集機能を追加します。 CollectStats定数が非ゼロで、かつGOGCTRACE=1が設定されている場合、ガベージコレクタは実行されたGC命令に関する基本的な統計情報を出力します。

変更の背景

Go言語の初期のガベージコレクタは、その動作の透明性が低く、開発者がGCのパフォーマンス特性を理解し、アプリケーションのチューニングを行うための詳細な情報が不足していました。特に、GCが内部でどのような処理を行い、どれくらいの時間やリソースを消費しているのかを把握することは困難でした。

このコミットは、このような情報不足を解消し、GCの内部動作を可視化することを目的としています。GCの各フェーズや特定の操作(ポインタのスキャン、オブジェクトの走査、命令の実行など)に関する詳細な統計情報を収集・出力することで、開発者はGCの挙動をより深く理解し、メモリ管理に関する問題を診断・解決するための手がかりを得られるようになります。これは、Goアプリケーションのパフォーマンス最適化において非常に重要なステップとなります。

前提知識の解説

Go言語のランタイム (Runtime)

Go言語のランタイムは、Goプログラムの実行を管理する低レベルのシステムです。これには、スケジューラ(ゴルーチンの管理)、メモリ管理(ガベージコレクタを含む)、チャネルの実装、システムコールインターフェースなどが含まれます。ランタイムはGoプログラムとオペレーティングシステムの間で抽象化レイヤーとして機能し、Goの並行処理モデルやメモリ安全性などの特徴を支えています。

ガベージコレクション (Garbage Collection, GC)

ガベージコレクションは、プログラムが動的に確保したメモリ領域のうち、もはやどの部分からも参照されなくなった(到達不能になった)ものを自動的に解放するプロセスです。これにより、開発者は手動でのメモリ管理から解放され、メモリリークなどのバグを減らすことができます。GoのGCは、主に「マーク&スイープ(Mark and Sweep)」アルゴリズムをベースにしています。

  • マークフェーズ (Mark Phase): GCは、ルート(グローバル変数、スタック上の変数など)から到達可能なすべてのオブジェクトを「マーク」します。これは、生きているオブジェクトを識別するプロセスです。
  • スイープフェーズ (Sweep Phase): マークされなかった(到達不能な)オブジェクトが「ガベージ」と見なされ、そのメモリが解放され、再利用可能な状態になります。

このコミットで変更されているmgc0.cファイルは、GoランタイムのGCのマークフェーズに関連するコードが含まれている可能性が高いです。

GOGCTRACE環境変数

GOGCTRACEは、Goランタイムのガベージコレクタのトレース情報を出力するための環境変数です。通常、GOGCTRACE=1を設定すると、GCの実行タイミング、ヒープサイズの変化、GCにかかった時間などの概要情報が標準エラー出力に表示されます。このコミットは、この既存のトレース機能に加えて、より詳細な内部統計情報を出力するオプションを追加するものです。

runtime·xadd64関数

runtime·xadd64は、Goランタイム内部で使用されるアトミック操作(不可分操作)の一つです。これは、指定されたメモリ位置の64ビット整数に値をアトミックに加算する関数です。複数のゴルーチンが同時に同じカウンタを更新しようとした場合でも、競合状態を防ぎ、正確な統計情報を収集するために使用されます。

runtime·memclr関数

runtime·memclrは、指定されたメモリ領域をゼロで埋める(クリアする)関数です。このコミットでは、GC統計を収集する前に、統計構造体gcstatsのすべてのカウンタをリセットするために使用されています。

技術的詳細

このコミットの主要な目的は、Goランタイムのガベージコレクタの内部動作に関する詳細な統計情報を収集し、出力するメカニズムを導入することです。これは、src/pkg/runtime/mgc0.cファイルに以下の変更を加えることで実現されています。

  1. CollectStats定数の導入: enumブロックにCollectStats = 0が追加されました。この定数を非ゼロに設定することで、GC統計の収集が有効になります。これはコンパイル時に設定されるフラグであり、デバッグや詳細な分析が必要な場合にのみ有効にすることを意図しています。

  2. gcstats構造体の定義: GCの様々な側面を追跡するためのgcstatsという静的構造体が定義されました。この構造体には、以下のようなフィールドが含まれています。

    • ptr.sum, ptr.cnt: ポインタバッファのサイズに関する統計。
    • nbytes: スキャンされたバイト数。
    • obj.sum, obj.cnt: スキャンされたオブジェクトの数に関する統計。
    • obj.notype: 型情報が見つからなかったオブジェクトの数。
    • obj.typelookup: 型ルックアップが実行された回数。
    • rescan, rescanbytes: 再スキャンされた回数とバイト数。
    • instr[GC_NUM_INSTR2]: GC命令の実行回数を記録する配列。GC_NUM_INSTR2はGC命令の種類の総数を表します。
    • putempty, getfull: ワークバッファの操作に関する統計。
  3. 統計情報の収集ロジックの追加: GCの主要な関数やパスにCollectStatsが有効な場合にgcstats構造体を更新するコードが追加されました。

    • flushptrbuf: ポインタバッファがフラッシュされる際に、gcstats.ptr.sumgcstats.ptr.cntを更新します。
    • scanblock: オブジェクトブロックがスキャンされる際に、gcstats.nbytesgcstats.obj.sumgcstats.obj.cntを更新します。また、型情報が見つからない場合や型ルックアップが行われた場合にgcstats.obj.notypegcstats.obj.typelookupを更新します。さらに、GC命令が実行されるたびにgcstats.instr配列の対応するカウンタをインクリメントします。再スキャンが発生した場合にはgcstats.rescangcstats.rescanbytesを更新します。
    • putempty, getfull: ワークバッファのputempty(空のバッファを戻す)およびgetfull(フルバッファを取得する)操作の回数をgcstats.putemptygcstats.getfullでカウントします。 これらの更新には、アトミック操作であるruntime·xadd64が使用されており、並行実行環境下での正確なカウントを保証しています。
  4. 統計情報の出力ロジックの追加: gc関数(GCのメインエントリポイント)の最後に、CollectStatsが有効な場合に収集された統計情報をruntime·printfを使用して標準出力に整形して表示するコードが追加されました。出力される情報には、スキャンされたバイト数、オブジェクト数、型情報のないオブジェクト数、型ルックアップ数、ポインタバッファの平均サイズ、オブジェクトの平均サイズ、再スキャン回数、GC命令ごとの実行回数、ワークバッファ操作の回数などが含まれます。

  5. 統計構造体の初期化: gc関数の開始時に、CollectStatsが有効な場合にruntime·memclr((byte*)&gcstats, sizeof(gcstats));を呼び出してgcstats構造体をゼロクリアし、前回のGC実行の統計が残らないようにしています。

これらの変更により、GoのGCの内部動作をより詳細にプロファイリングし、パフォーマンス特性を理解するための強力なツールが提供されます。

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

変更はsrc/pkg/runtime/mgc0.cファイルに集中しています。

  1. CollectStats定数の追加:

    --- a/src/pkg/runtime/mgc0.c
    +++ b/src/pkg/runtime/mgc0.c
    @@ -17,6 +17,7 @@
     enum {
      	Debug = 0,
      	DebugMark = 0,  // run second pass to check mark
    +	CollectStats = 0,
     
      	// Four bits per word (see #defines below).
      	wordsPerBitmapWord = sizeof(void*)*8/4,
    
  2. gcstats構造体の定義:

    --- a/src/pkg/runtime/mgc0.c
    +++ b/src/pkg/runtime/mgc0.c
    @@ -165,8 +166,29 @@ enum {
      	GC_DEFAULT_PTR = GC_NUM_INSTR,
      	GC_MAP_NEXT,
      	GC_CHAN,
    +
    +	GC_NUM_INSTR2
     };
     
    +static struct {
    +	struct {
    +		uint64 sum;
    +		uint64 cnt;
    +	} ptr;
    +	uint64 nbytes;
    +	struct {
    +		uint64 sum;
    +		uint64 cnt;
    +		uint64 notype;
    +		uint64 typelookup;
    +	} obj;
    +	uint64 rescan;
    +	uint64 rescanbytes;
    +	uint64 instr[GC_NUM_INSTR2];
    +	uint64 putempty;
    +	uint64 getfull;
    +} gcstats;
    +
     // markonly marks an object. It returns true if the object
     // has been marked by this function, false otherwise.
     // This function isn\'t thread-safe and doesn\'t append the object to any buffer.
    
  3. 統計収集と出力ロジックの追加: flushptrbuf, scanblock, putempty, getfull, gc関数内にCollectStatsの条件付きで統計更新と出力のコードが追加されています。

    • flushptrbufでのポインタバッファ統計更新:

      --- a/src/pkg/runtime/mgc0.c
      +++ b/src/pkg/runtime/mgc0.c
      @@ -315,6 +337,11 @@ flushptrbuf(PtrTarget *ptrbuf, PtrTarget **ptrbufpos, Obj **_wp, Workbuf **_wbuf
       	n = ptrbuf_end - ptrbuf;
       	*ptrbufpos = ptrbuf;
       
      +	if(CollectStats) {
      +		runtime·xadd64(&gcstats.ptr.sum, n);
      +		runtime·xadd64(&gcstats.ptr.cnt, 1);
      +	}
      +
       	// If buffer is nearly full, get a new one.
       	if(wbuf == nil || nobj+n >= nelem(wbuf->obj)) {
       		if(wbuf != nil)
      
    • scanblockでのオブジェクト、バイト、型、命令、再スキャン統計更新:

      --- a/src/pkg/runtime/mgc0.c
      +++ b/src/pkg/runtime/mgc0.c
      @@ -621,6 +648,12 @@ scanblock(Workbuf *wbuf, Obj *wp, uintptr nobj, bool keepworking)\n         		runtime·printf("scanblock %p %D\n", b, (int64)n);\n         	}\n         
      +	if(CollectStats) {
      +		runtime·xadd64(&gcstats.nbytes, n);
      +		runtime·xadd64(&gcstats.obj.sum, nobj);
      +		runtime·xadd64(&gcstats.obj.cnt, 1);
      +	}
      +
       	if(ti != 0) {
       		pc = (uintptr*)(ti & ~(uintptr)PC_BITS);\n         		precise_type = (ti & PRECISE);\n        @@ -634,8 +667,14 @@ scanblock(Workbuf *wbuf, Obj *wp, uintptr nobj, bool keepworking)\n         			stack_top.count = 1;\n         		}\n         	} else if(UseSpanType) {\n        +		if(CollectStats)\n        +			runtime·xadd64(&gcstats.obj.notype, 1);\n        +\n         		type = runtime·gettype(b);\n         		if(type != 0) {\n        +			if(CollectStats)\n        +				runtime·xadd64(&gcstats.obj.typelookup, 1);\n        +\n         			t = (Type*)(type & ~(uintptr)(PtrSize-1));\n         			switch(type & (PtrSize-1)) {\n         			case TypeInfo_SingleObject:\n        @@ -692,6 +731,9 @@ scanblock(Workbuf *wbuf, Obj *wp, uintptr nobj, bool keepworking)\n         	end_b = (uintptr)b + n - PtrSize;\n         
       	for(;;) {\n        +		if(CollectStats)\n        +			runtime·xadd64(&gcstats.instr[pc[0]], 1);\n        +\n         		obj = nil;\n         		objti = 0;\n         		switch(pc[0]) {\n        @@ -807,6 +849,10 @@ scanblock(Workbuf *wbuf, Obj *wp, uintptr nobj, bool keepworking)\n         						// Found a value that may be a pointer.\n         						// Do a rescan of the entire block.\n         						enqueue((Obj){b, n, 0}, &wbuf, &wp, &nobj);\n        +					if(CollectStats) {\n        +						runtime·xadd64(&gcstats.rescan, 1);\n        +						runtime·xadd64(&gcstats.rescanbytes, n);\n        +					}\n         						break;\n         					}\n         				}\n        ```
      
      
    • putemptygetfullでのワークバッファ統計更新:

      --- a/src/pkg/runtime/mgc0.c
      +++ b/src/pkg/runtime/mgc0.c
      @@ -1164,6 +1210,9 @@ getempty(Workbuf *b)\n         static void\n         putempty(Workbuf *b)\n         {\n        +	if(CollectStats)\n        +		runtime·xadd64(&gcstats.putempty, 1);\n        +\n         	runtime·lfstackpush(&work.empty, &b->node);\n         }\n         
      @@ -1173,6 +1222,9 @@ getfull(Workbuf *b)\n         {\n         	int32 i;\n         
      +	if(CollectStats)\n        +		runtime·xadd64(&gcstats.getfull, 1);\n        +\n         	if(b != nil)\n         		runtime·lfstackpush(&work.empty, &b->node);\n         	b = (Workbuf*)runtime·lfstackpop(&work.full);\n        ```
      
      
    • gc関数でのgcstats初期化と統計出力:

      --- a/src/pkg/runtime/mgc0.c
      +++ b/src/pkg/runtime/mgc0.c
      @@ -1747,7 +1799,7 @@ static void
       gc(struct gc_args *args)\n         {\n         	int64 t0, t1, t2, t3, t4;\n        -	uint64 heap0, heap1, obj0, obj1;\n        +	uint64 heap0, heap1, obj0, obj1, ninstr;\n         	GCStats stats;\n         	M *mp;\n         	uint32 i;\n        @@ -1764,6 +1816,9 @@ gc(struct gc_args *args)\n         	m->gcing = 1;\n         	runtime·stoptheworld();\n         
      +	if(CollectStats)\n        +		runtime·memclr((byte*)&gcstats, sizeof(gcstats));\n        +\n         	for(mp=runtime·allm; mp; mp=mp->alllink)\n         		runtime·settype_flush(mp, false);\n         
      @@ -1859,6 +1914,27 @@ gc(struct gc_args *args)\n         			stats.nhandoff, stats.nhandoffcnt,\n         			work.sweepfor->nsteal, work.sweepfor->nstealcnt,\n         			stats.nprocyield, stats.nosyield, stats.nsleep);\n        +	if(CollectStats) {\n        +		runtime·printf("scan: %D bytes, %D objects, %D untyped, %D types from MSpan\n",\n        +			gcstats.nbytes, gcstats.obj.cnt, gcstats.obj.notype, gcstats.obj.typelookup);\n        +		if(gcstats.ptr.cnt != 0)\n        +			runtime·printf("avg ptrbufsize: %D (%D/%D)\n",\n        +				gcstats.ptr.sum/gcstats.ptr.cnt, gcstats.ptr.sum, gcstats.ptr.cnt);\n        +		if(gcstats.obj.cnt != 0)\n        +			runtime·printf("avg nobj: %D (%D/%D)\n",\n        +				gcstats.obj.sum/gcstats.obj.cnt, gcstats.obj.sum, gcstats.obj.cnt);\n        +		runtime·printf("rescans: %D, %D bytes\n", gcstats.rescan, gcstats.rescanbytes);\n        +\n        +		runtime·printf("instruction counts:\n");\n        +		ninstr = 0;\n        +		for(i=0; i<nelem(gcstats.instr); i++) {\n        +			runtime·printf("\t%d:\t%D\n", i, gcstats.instr[i]);\n        +			ninstr += gcstats.instr[i];\n        +		}\n        +		runtime·printf("\ttotal:\t%D\n", ninstr);\n        +\n        +		runtime·printf("putempty: %D, getfull: %D\n", gcstats.putempty, gcstats.getfull);\n        +	}\n         }\n         
       	runtime·MProf_GC();\n        ```
      
      

コアとなるコードの解説

このコミットの核心は、Goのガベージコレクタの内部に「プローブ(探針)」を埋め込み、その動作に関する詳細な数値データを収集する点にあります。

  1. CollectStats定数: これは、GC統計収集機能を有効にするためのコンパイル時フラグとして機能します。デフォルトでは0(無効)に設定されており、必要な場合にのみ有効にすることで、通常のビルドではオーバーヘッドを発生させないように配慮されています。

  2. gcstats構造体: この構造体は、GCの様々な側面を定量的に把握するためのカウンターと集計値を保持します。

    • ptrフィールドは、GCがポインタを処理する際のバッファサイズに関する情報を提供します。sumは処理されたポインタの総数、cntはバッファ処理の回数を記録し、これらから平均バッファサイズを算出できます。
    • nbytesは、GCがスキャンしたメモリの総バイト数を示します。これはGCの作業量を示す重要な指標です。
    • objフィールドは、スキャンされたオブジェクトに関する詳細な統計を提供します。sumはスキャンされたオブジェクトの総数、cntはオブジェクトスキャン操作の回数です。notypeは、GCがオブジェクトの型情報を特定できなかった回数を記録し、これは潜在的な問題(例えば、型情報が欠落している、または破損している)を示唆する可能性があります。typelookupは、GCが型情報をルックアップした回数を記録します。
    • rescanrescanbytesは、GCが特定のメモリ領域を再スキャンした回数とバイト数を示します。これは、GCがポインタの解決のために追加の作業を必要とした場合に発生します。
    • instr配列は、GCが実行する様々な内部命令(GC_NUM_INSTR2で定義される各命令)の実行回数をカウントします。これにより、GCのどの部分が最も頻繁に実行されているかを特定できます。
    • putemptygetfullは、GCが内部で使用するワークバッファの管理効率に関する情報を提供します。
  3. runtime·xadd64によるアトミックな更新: gcstats構造体の各フィールドは、GCが並行して動作する可能性があるため、複数のゴルーチンから同時に更新される可能性があります。runtime·xadd64のようなアトミック操作を使用することで、これらのカウンターが競合状態に陥ることなく、正確にインクリメントされることが保証されます。これにより、統計データの信頼性が高まります。

  4. runtime·memclrによる初期化: 各GCサイクルが開始される前にgcstats構造体をゼロクリアすることで、統計情報が現在のGCサイクルのものだけを反映するようにします。これにより、過去のGCサイクルのデータが混入することなく、正確な単一サイクルごとの分析が可能になります。

  5. runtime·printfによる出力: GCサイクルが完了すると、収集された統計情報がruntime·printfによって整形された形式で標準出力に表示されます。この出力は、GOGCTRACE=1が設定されている場合にのみ行われるため、通常の運用では詳細な統計情報が冗長に出力されることはありません。

これらの変更は、GoのGCの「ブラックボックス」的な側面を減らし、開発者がGCのパフォーマンスを理解し、デバッグし、最適化するための重要な可視性を提供します。特に、GC命令ごとの実行回数や型ルックアップの回数などの詳細なメトリクスは、GCのアルゴリズムやデータ構造の効率性を評価する上で非常に有用です。

関連リンク

  • Go言語のガベージコレクションに関する公式ドキュメントやブログ記事(コミット当時の情報にアクセスできる場合は特に有用)
  • Goのランタイムソースコード(src/runtime/ディレクトリ)
  • GoのIssue TrackerやChange List (CL) で、GCのパフォーマンスやプロファイリングに関する議論。

参考にした情報源リンク

  • Go言語の公式ドキュメント: https://go.dev/doc/
  • Goのソースコードリポジトリ: https://github.com/golang/go
  • GoのChange List (CL) 7413049: https://golang.org/cl/7413049 (コミットメッセージに記載されているリンク)
  • Goのガベージコレクションに関するブログ記事や論文(一般的なGCの概念、マーク&スイープアルゴリズムなど)
  • アトミック操作に関する一般的な情報源(並行プログラミングの基礎知識)