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

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

このコミットは、Goランタイムにおいて、GODEBUG=allocfreetrace 環境変数が設定されている場合に、ガベージコレクション (GC) 実行時のスタックトレースを出力する機能を追加するものです。これにより、メモリの割り当てと解放のトレース情報に加えて、GCがいつ、どのコンテキストで発生したかを詳細に把握できるようになり、メモリ関連のデバッグやプロファイリングがより容易になります。

コミット

commit 3ec60c253d9d419eb7a35a40e01315e3d1497465
Author: Russ Cox <rsc@golang.org>
Date:   Tue Jan 14 10:39:50 2014 -0500

    runtime: emit collection stacks in GODEBUG=allocfreetrace mode
    
    R=khr, dvyukov
    CC=golang-codereviews
    https://golang.org/cl/51830043

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

https://github.com/golang/go/commit/3ec60c253d9d419eb7a35a40e01315e3d1497465

元コミット内容

runtime: emit collection stacks in GODEBUG=allocfreetrace mode

R=khr, dvyukov
CC=golang-codereviews
https://golang.org/cl/51830043

変更の背景

Go言語のランタイムは、自動メモリ管理(ガベージコレクション)によって開発者がメモリ管理の複雑さから解放されるように設計されています。しかし、メモリリーク、過剰なメモリ使用量、GCの一時停止によるパフォーマンス問題など、メモリ関連のデバッグは依然として困難な場合があります。

GODEBUG=allocfreetrace は、Goプログラムがメモリを割り当てたり解放したりするたびに、そのイベントと関連するスタックトレースを出力するデバッグモードです。これは、特定のメモリ割り当てがどこで行われたか、または特定のオブジェクトがどこで解放されたかを追跡するのに非常に役立ちます。しかし、このモードではGC自体のイベントに関する詳細な情報、特にGCがどのコードパスからトリガーされたかについての情報が不足していました。

このコミットの背景には、GCが予期せぬタイミングで発生したり、特定のコードパスがGCを頻繁にトリガーしている場合に、その原因を特定しやすくするという目的があります。GCイベント発生時のスタックトレースを出力することで、開発者はGCのトリガー元をより正確に把握し、メモリ使用パターンやGCの挙動を最適化するための洞察を得ることができます。

前提知識の解説

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

Goのガベージコレクタは、並行マーク&スイープ方式を採用しています。これは、プログラムの実行と並行してGCが動作し、アプリケーションの一時停止(ストップ・ザ・ワールド)時間を最小限に抑えるように設計されています。GCは、到達可能なオブジェクトをマークし、到達不能なオブジェクトを解放することでメモリを再利用します。GCは、ヒープの使用量がある閾値を超えた場合や、特定の時間間隔で自動的にトリガーされます。

GODEBUG 環境変数

GODEBUG は、Goランタイムのデバッグ情報を制御するための環境変数です。様々なオプションがあり、ランタイムの内部動作に関する詳細な情報を出力させることができます。例えば、gctrace=1 はGCの実行に関する統計情報を出力し、schedtrace=1000ms はスケジューラの動作を1秒ごとにトレースします。

GODEBUG=allocfreetrace

このオプションは、Goプログラムがメモリを割り当て(alloc)たり解放(free)したりするたびに、そのイベントと関連するスタックトレースを標準エラー出力にダンプします。これにより、どのコードがメモリを割り当て、どのコードがそのメモリを解放したかを追跡できます。これは、メモリリークの特定や、不要なメモリ割り当ての削減に非常に強力なツールです。

スタックトレースと runtime.callers

スタックトレースは、プログラムの実行中に現在アクティブな関数呼び出しのリストです。これにより、ある関数がどのように呼び出されたかの「呼び出し履歴」を把握できます。Goランタイムの内部では、runtime.callers 関数が現在のゴルーチンのスタックトレースを取得するために使用されます。この関数は、指定されたスキップ数(呼び出し元をスキップする数)とスタックフレームの最大数を指定して、uintptr のスライスにプログラムカウンタのリストを返します。

runtime パッケージ

runtime パッケージは、Goプログラムの実行環境、ガベージコレクション、スケジューリング、低レベルのメモリ管理など、Goランタイムのコア機能を提供します。このパッケージの関数は、Go言語で直接呼び出すことはできませんが、Goコンパイラやリンカによって内部的に使用されます。

技術的詳細

このコミットの技術的な核心は、GODEBUG=allocfreetrace が有効な場合に、ガベージコレクションの開始時に runtime.MProf_TraceGC 関数を呼び出すように変更された点です。

  1. runtime.MProf_TraceGC 関数の追加: src/pkg/runtime/mprof.gocruntime.MProf_TraceGC という新しい関数が追加されました。この関数は、GCイベントが発生したことを報告するためにコレクタによって呼び出されます。 この関数は、runtime.callers(1, stk, nelem(stk)) を使用して現在のスタックトレースを取得します。1 をスキップ数として指定することで、runtime.MProf_TraceGC 自体の呼び出しフレームをスキップし、その呼び出し元(つまりGCをトリガーしたコードパス)からスタックトレースを取得します。 取得したスタックトレースは printstackframes 関数によって整形され、標準出力に「MProf_TraceGC」というプレフィックスとともにダンプされます。

  2. runtime.gc からの runtime.MProf_TraceGC の呼び出し: src/pkg/runtime/mgc0.cruntime.gc 関数(Goのガベージコレクションの主要なエントリポイント)内に、runtime.debug.allocfreetrace が真(つまり GODEBUG=allocfreetrace が有効)である場合に runtime.MProf_TraceGC() を呼び出す条件分岐が追加されました。これにより、GCが開始されるたびに、その時点のスタックトレースが allocfreetrace の出力の一部として記録されるようになります。

  3. malloc.h の更新: src/pkg/runtime/malloc.hruntime.MProf_TraceGC のプロトタイプ宣言が追加され、他のランタイムファイルからこの関数が呼び出せるようになりました。

  4. runtime.callersnelem(stk) への変更: src/pkg/runtime/mprof.goc 内の runtime.MProf_Malloc および runtime.blockevent 関数における runtime.callers の呼び出しで、スタックフレームの最大数をハードコードされた 32 から nelem(stk)(スタック配列の要素数)に変更しています。これは、より汎用的なアプローチであり、スタック配列のサイズ変更に自動的に対応できるようになります。

これらの変更により、GODEBUG=allocfreetrace を有効にしてGoプログラムを実行すると、メモリの割り当てと解放のトレースに加えて、ガベージコレクションが実行された時点のスタックトレースも出力されるようになります。これにより、GCがどのコードパスによってトリガーされたのか、そのGCがアプリケーションのどの部分のメモリ使用パターンに関連しているのかを、より詳細に分析できるようになります。

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

src/pkg/runtime/malloc.h

--- a/src/pkg/runtime/malloc.h
+++ b/src/pkg/runtime/malloc.h
@@ -521,6 +521,7 @@ enum
 void	runtime·MProf_Malloc(void*, uintptr, uintptr);
 void	runtime·MProf_Free(Bucket*, void*, uintptr);
 void	runtime·MProf_GC(void);
+void	runtime·MProf_TraceGC(void);
 int32	runtime·gcprocs(void);
 void	runtime·helpgc(int32 nproc);
 void	runtime·gchelper(void);

src/pkg/runtime/mgc0.c

--- a/src/pkg/runtime/mgc0.c
+++ b/src/pkg/runtime/mgc0.c
@@ -2163,6 +2163,9 @@ runtime·gc(int32 force)
 	a.start_time = runtime·nanotime();
 	m->gcing = 1;
 	runtime·stoptheworld();
+	
+	if(runtime·debug.allocfreetrace)
+		runtime·MProf_TraceGC();
 
 	clearpools();
 

src/pkg/runtime/mprof.goc

--- a/src/pkg/runtime/mprof.goc
+++ b/src/pkg/runtime/mprof.goc
@@ -173,6 +173,18 @@ printstackframes(uintptr *stk, int32 nstk)
 	}
 }
 
+// Called by collector to report a gc in allocfreetrace mode.
+void
+runtime·MProf_TraceGC(void)
+{
+	uintptr stk[32];
+	int32 nstk;
+
+	nstk = runtime·callers(1, stk, nelem(stk));
+	runtime·printf("MProf_TraceGC\n");
+	printstackframes(stk, nstk);
+}
+
 // Called by malloc to record a profiled block.
 void
 runtime·MProf_Malloc(void *p, uintptr size, uintptr typ)
@@ -183,14 +195,14 @@ runtime·MProf_Malloc(void *p, uintptr size, uintptr typ)
 	int8 *name;
 	int32 nstk;
 
-	nstk = runtime·callers(1, stk, 32);
+	nstk = runtime·callers(1, stk, nelem(stk));
 	runtime·lock(&proflock);
-        if(runtime·debug.allocfreetrace) {
+	if(runtime·debug.allocfreetrace) {
 		type = (Type*)(typ & ~3);
 		name = typeinfoname(typ & 3);
 		runtime·printf("MProf_Malloc(p=%p, size=%p, type=%p <%s", p, size, type, name);
 		if(type != nil)
-                	runtime·printf(" of %S", *type->string);
+			runtime·printf(" of %S", *type->string);
 		runtime·printf(">)\\n\");
 		printstackframes(stk, nstk);
 	}
@@ -247,7 +259,7 @@ runtime·blockevent(int64 cycles, int32 skip)
 	if(rate <= 0 || (rate > cycles && runtime·fastrand1()%rate > cycles))
 		return;
 
-	nstk = runtime·callers(skip, stk, 32);
+	nstk = runtime·callers(skip, stk, nelem(stk));
 	runtime·lock(&proflock);\
 	b = stkbucket(BProf, stk, nstk, true);
 	b->count++;

コアとなるコードの解説

src/pkg/runtime/malloc.h の変更

  • void runtime·MProf_TraceGC(void); の追加: これは、runtime.MProf_TraceGC 関数のプロトタイプ宣言です。この宣言により、他のランタイムコンポーネント(特に mgc0.c)がこの関数を認識し、呼び出すことができるようになります。GoランタイムのCコードとGoコード(実際にはGoの擬似Cコードである.gocファイル)間のインターフェースを定義する役割を果たします。

src/pkg/runtime/mgc0.c の変更

  • if(runtime·debug.allocfreetrace) runtime·MProf_TraceGC(); の追加: runtime·gc 関数は、Goのガベージコレクションプロセスを開始する主要な関数です。この変更により、GCが開始される直前(runtime·stoptheworld() の後、つまりGCが実際に作業を開始する直前)に、runtime·debug.allocfreetrace フラグが真であるかどうかがチェックされます。 もしフラグが真であれば、新しく追加された runtime·MProf_TraceGC() 関数が呼び出されます。これにより、GODEBUG=allocfreetrace が有効な場合にのみ、GCイベントのスタックトレースが出力されるようになります。

src/pkg/runtime/mprof.goc の変更

  • runtime·MProf_TraceGC 関数の新規追加: この関数は、GCイベントのスタックトレースを収集し、出力する役割を担います。

    • uintptr stk[32];: スタックトレースを格納するための uintptr 型の配列 stk を宣言します。最大32フレームを格納できます。
    • int32 nstk;: 取得されたスタックフレームの数を格納する変数です。
    • nstk = runtime·callers(1, stk, nelem(stk));: runtime·callers を呼び出してスタックトレースを取得します。
      • 第一引数の 1 は、runtime·MProf_TraceGC 関数自体のフレームをスキップし、その呼び出し元からトレースを開始することを意味します。
      • 第二引数の stk は、スタックフレームを格納するバッファです。
      • 第三引数の nelem(stk) は、stk 配列の要素数(この場合は32)を渡し、取得するスタックフレームの最大数を指定します。
    • runtime·printf("MProf_TraceGC\n");: GCイベントが発生したことを示す識別子を出力します。
    • printstackframes(stk, nstk);: 取得したスタックフレームを整形して標準出力にダンプします。
  • runtime·MProf_Malloc および runtime·blockevent における runtime·callers の引数変更: nstk = runtime·callers(1, stk, nelem(stk)); (旧: 32) nstk = runtime·callers(skip, stk, nelem(stk)); (旧: 32) これらの変更は、runtime·callers の第三引数(スタックフレームの最大数)をハードコードされた 32 から nelem(stk) に変更しています。nelem はGoランタイム内部のマクロで、配列の要素数を返します。これにより、stk 配列のサイズが変更された場合でも、コードを修正することなく、常に配列の最大容量までスタックトレースを取得できるようになり、より堅牢で保守性の高いコードになります。

これらの変更により、GODEBUG=allocfreetrace を使用する開発者は、メモリ割り当てと解放のイベントだけでなく、ガベージコレクションがいつ、どのコードパスからトリガーされたかという重要なデバッグ情報を得られるようになり、Goアプリケーションのメモリプロファイリングとデバッグ能力が大幅に向上します。

関連リンク

参考にした情報源リンク

  • Goのソースコード (特に src/runtime ディレクトリ)
  • Goの公式ドキュメント
  • GODEBUG 環境変数に関する情報 (GoのIssueや関連するブログ記事)
  • runtime.callers 関数の挙動に関する情報 (Goのソースコードコメントや関連する議論)