[インデックス 19300] ファイルの概要
このコミットは、Goランタイムのヒープダンプ機能にメモリプロファイリング統計情報を追加するものです。これにより、ヒープダンプが単なるメモリのスナップショットだけでなく、どのコードパスがメモリを割り当てたかという詳細な情報を含むようになり、メモリリークや非効率なメモリ使用のデバッグが大幅に容易になります。
コミット
commit 65c63dc4aabba3ecd320427fb20bc1cdbe0d2a3d
Author: Keith Randall <khr@golang.org>
Date: Thu May 8 08:35:49 2014 -0700
runtime: write memory profile statistics to the heap dump.
LGTM=rsc
R=rsc, khr
CC=golang-codereviews
https://golang.org/cl/97010043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/65c63dc4aabba3ecd320427fb20bc1cdbe0d2a3d
元コミット内容
このコミットは、Goランタイムが生成するヒープダンプに、メモリプロファイリングの統計情報を書き込む機能を追加します。これにより、ヒープダンプを解析する際に、どのコードがどの程度のメモリを割り当てているかという詳細な情報を得られるようになります。
変更の背景
Goのランタイムは、プログラムのメモリ使用状況を分析するためのプロファイリングツールを提供しています。これには、CPUプロファイリング、メモリプロファイリング、ブロックプロファイリングなどが含まれます。メモリプロファイリングは、プログラムがどこでメモリを割り当てているかを追跡し、メモリリークや過剰なメモリ使用の原因を特定するのに役立ちます。
以前のGoランタイムのヒープダンプは、ヒープ上のオブジェクトの構造や相互参照関係を示すものでしたが、これらのオブジェクトが「どこで」割り当てられたかという、呼び出しスタックの情報は直接含まれていませんでした。メモリプロファイリングデータは別途取得する必要があり、ヒープダンプとメモリプロファイルを関連付けて分析するのは手間がかかる作業でした。
このコミットの背景には、ヒープダンプの有用性を高め、メモリ関連の問題のデバッグプロセスを効率化したいという意図があります。メモリプロファイリングの統計情報をヒープダンプに直接埋め込むことで、単一のファイルからより包括的なメモリ使用状況のビューを提供できるようになります。これにより、開発者はメモリリークの根本原因をより迅速に特定し、メモリ使用量を最適化するための洞察を得ることができます。
前提知識の解説
このコミットを理解するためには、以下のGoランタイムの概念に関する知識が必要です。
-
ヒープダンプ (Heap Dump): Goプログラムの実行中に、ヒープメモリの現在の状態をスナップショットとして取得したものです。これには、割り当てられているオブジェクト、それらのサイズ、型、およびオブジェクト間の参照関係などが含まれます。ヒープダンプは、メモリリークや過剰なメモリ使用をデバッグする際に非常に有用です。通常、
go tool pprof
などのツールで解析されます。 -
メモリプロファイリング (Memory Profiling): プログラムがメモリを割り当てる場所と量を追跡するプロセスです。Goのメモリプロファイラは、メモリ割り当てが発生した際の呼び出しスタックをサンプリングし、どの関数が最も多くのメモリを割り当てているかを特定するのに役立ちます。これにより、メモリ使用量のホットスポットを特定し、最適化の機会を見つけることができます。
-
runtime
パッケージ: Goのランタイムシステムを実装しているパッケージです。ガベージコレクション、スケジューリング、メモリ管理、プロファイリングなど、Goプログラムの実行を支える低レベルな機能を提供します。 -
Bucket
(メモリプロファイリングにおけるバケット): Goのメモリプロファイラでは、同じ呼び出しスタックから発生したメモリ割り当てを「バケット」としてグループ化します。各バケットは、そのスタックトレースからの総割り当てサイズ、割り当て回数、解放回数などの統計情報を保持します。 -
MSpan
: Goのメモリ管理において、ヒープメモリはページ単位で管理され、これらのページはMSpan
と呼ばれる構造体によってグループ化されます。MSpan
は、連続したメモリページの範囲を表し、そのメモリが現在使用中か、空いているか、特定のオブジェクトサイズのために予約されているかなどの状態を管理します。 -
SpecialProfile
: Goランタイムの内部構造で、特定のメモリ領域に関連付けられたプロファイリング情報を保持するために使用されます。このコミットでは、MSpan
内の個々の割り当てサンプルを追跡するために利用されています。 -
Tag
(ヒープダンプタグ): ヒープダンプファイルは、異なる種類の情報を識別するためにタグを使用します。例えば、オブジェクトの型情報、スタックトレース、ガベージコレクション統計など、各データブロックの先頭にタグが付与されます。このコミットでは、メモリプロファイリング統計と割り当てサンプルを識別するための新しいタグが導入されています。
技術的詳細
このコミットの主要な目的は、Goランタイムのヒープダンプにメモリプロファイリングの統計情報を統合することです。これを実現するために、以下の変更が行われています。
-
新しいヒープダンプタグの導入:
src/pkg/runtime/heapdump.c
において、ヒープダンプファイル内でメモリプロファイリングデータと割り当てサンプルを識別するための新しいタグが定義されました。TagMemProf = 16
: メモリプロファイリングのバケット情報(スタックトレースごとの統計)を示すために使用されます。TagAllocSample = 17
: 個々の割り当てサンプル情報を示すために使用されます。
-
メモリプロファイリングデータのダンプ関数
dumpmemprof_callback
の追加:src/pkg/runtime/heapdump.c
にdumpmemprof_callback
という静的関数が追加されました。この関数は、メモリプロファイラの各Bucket
(特定の呼び出しスタックからの割り当ての集計)に対して呼び出されるコールバック関数として機能します。Bucket
のポインタ、スタックトレースの深さ、スタックトレースのPC(プログラムカウンタ)値、割り当てサイズ、割り当て回数、解放回数を受け取ります。- これらの情報を
TagMemProf
タグとともにヒープダンプに書き込みます。 - スタックトレースのPC値から関数名、ファイル名、行番号を解決し、ダンプに含めます。特に、PC値から正確な呼び出し元を特定するために、PC値を調整するロジック(
pc--
やpc -= 4
)が含まれています。これは、プロファイリングにおける一般的なテクニックで、命令の直前のアドレスを指すように調整することで、より正確なソースコードの位置を特定します。
-
メモリプロファイリングのイテレーション関数
runtime·iterate_memprof
の追加:src/pkg/runtime/mprof.goc
にruntime·iterate_memprof
関数が追加され、src/pkg/runtime/malloc.h
でそのプロトタイプが宣言されました。- この関数は、メモリプロファイラが管理するすべての
Bucket
を安全に(proflock
というロックを使用して)イテレートし、各Bucket
に対してdumpmemprof_callback
のようなコールバック関数を呼び出すメカニズムを提供します。 - これにより、ヒープダンプ処理がメモリプロファイラの内部データにアクセスできるようになります。
- この関数は、メモリプロファイラが管理するすべての
-
割り当てサンプルのダンプ:
dumpmemprof
関数は、runtime·iterate_memprof
を呼び出してバケット情報をダンプするだけでなく、MSpan
とSpecialProfile
を走査して、個々の割り当てサンプルもダンプします。MSpan
はメモリのチャンクを表し、SpecialProfile
は特定のメモリ領域に関連付けられたプロファイリング情報(どのBucket
に関連するかなど)を保持します。- これにより、
TagAllocSample
タグとともに、割り当てられたオブジェクトのアドレスと、それに関連するBucket
のポインタがヒープダンプに書き込まれます。これにより、ヒープダンプ内の特定のオブジェクトがどのメモリプロファイルバケットに属するかを追跡できるようになります。
-
ヒープダンプへの統合:
src/pkg/runtime/heapdump.c
のmdump
関数(ヒープダンプのメインエントリポイント)内で、新しく追加されたdumpmemprof()
関数が呼び出されるようになりました。これにより、ヒープダンプが生成されるたびに、メモリプロファイリングの統計情報も自動的に含まれるようになります。
これらの変更により、Goのヒープダンプは、メモリ使用量の「スナップショット」と「割り当て履歴」の両方を提供する、より強力なデバッグツールへと進化しました。
コアとなるコードの変更箇所
src/pkg/runtime/heapdump.c
@@ -49,6 +49,8 @@ enum {
TagBss = 13,
TagDefer = 14,
TagPanic = 15,
+ TagMemProf = 16,
+ TagAllocSample = 17,
};
@@ -689,6 +691,74 @@ dumpmemstats(void)
dumpint(mstats.numgc);
}
+static void
+dumpmemprof_callback(Bucket *b, uintptr nstk, uintptr *stk, uintptr size, uintptr allocs, uintptr frees)
+{
+ uintptr i, pc;
+ Func *f;
+ byte buf[20];
+ String file;
+ int32 line;
+
+ dumpint(TagMemProf);
+ dumpint((uintptr)b);
+ dumpint(size);
+ dumpint(nstk);
+ for(i = 0; i < nstk; i++) {
+ pc = stk[i];
+ f = runtime·findfunc(pc);
+ if(f == nil) {
+ runtime·snprintf(buf, sizeof(buf), "%X", (uint64)pc);
+ dumpcstr((int8*)buf);
+ dumpcstr("?");
+ dumpint(0);
+ } else {
+ dumpcstr(runtime·funcname(f));
+ // TODO: Why do we need to back up to a call instruction here?
+ // Maybe profiler should do this.
+ if(i > 0 && pc > f->entry) {
+ if(thechar == '6' || thechar == '8')
+ pc--;
+ else
+ pc -= 4; // arm, etc
+ }
+ line = runtime·funcline(f, pc, &file);
+ dumpstr(file);
+ dumpint(line);
+ }
+ }
+ dumpint(allocs);
+ dumpint(frees);
+}
+
+static void
+dumpmemprof(void)
+{
+ MSpan *s, **allspans;
+ uint32 spanidx;
+ Special *sp;
+ SpecialProfile *spp;
+ byte *p;
+
+ runtime·iterate_memprof(dumpmemprof_callback);
+
+ allspans = runtime·mheap.allspans;
+ for(spanidx=0; spanidx<runtime·mheap.nspan; spanidx++) {
+ s = allspans[spanidx];
+ if(s->state != MSpanInUse)
+ continue;
+ for(sp = s->specials; sp != nil; sp = sp->next) {
+ if(sp->kind != KindSpecialProfile)
+ continue;
+ spp = (SpecialProfile*)sp;
+ p = (byte*)((s->start << PageShift) + spp->offset);
+ dumpint(TagAllocSample);
+ dumpint((uintptr)p);
+ dumpint((uintptr)spp->b);
+ }
+ }
+}
+
static void
mdump(G *gp)
{
@@ -713,6 +783,7 @@ mdump(G *gp)
dumpms();
dumproots();
dumpmemstats();
+ dumpmemprof();
dumpint(TagEOF);
flush();
src/pkg/runtime/malloc.h
@@ -570,6 +570,7 @@ enum
void runtime·MProf_Malloc(void*, uintptr);
void runtime·MProf_Free(Bucket*, uintptr, bool);
void runtime·MProf_GC(void);
+void runtime·iterate_memprof(void (*callback)(Bucket*, uintptr, uintptr*, uintptr, uintptr, uintptr));
int32 runtime·gcprocs(void);
void runtime·helpgc(int32 nproc);
void runtime·gchelper(void);
src/pkg/runtime/mprof.goc
@@ -309,6 +309,18 @@ func MemProfile(p Slice, include_inuse_zero bool) (n int, ok bool) {
runtime·unlock(&proflock);
}
+void
+runtime·iterate_memprof(void (*callback)(Bucket*, uintptr, uintptr*, uintptr, uintptr, uintptr))
+{
+ Bucket *b;
+
+ runtime·lock(&proflock);
+ for(b=mbuckets; b; b=b->allnext) {
+ callback(b, b->nstk, b->stk, b->size, b->allocs, b->frees);
+ }
+ runtime·unlock(&proflock);
+}
+
// Must match BlockProfileRecord in debug.go.
typedef struct BRecord BRecord;
struct BRecord {
コアとなるコードの解説
src/pkg/runtime/heapdump.c
enum
の変更:TagMemProf
とTagAllocSample
という新しい定数が追加されました。これらはヒープダンプファイル内でメモリプロファイルデータと個々の割り当てサンプルを区別するための識別子として機能します。dumpmemprof_callback
関数:- この関数は、メモリプロファイリングの各「バケット」(特定の呼び出しスタックからの割り当ての集計)に対して呼び出されるコールバックです。
dumpint
やdumpcstr
を使用して、バケットのポインタ、割り当てサイズ、スタックトレースの深さ、スタックトレースの各PC値、割り当て回数、解放回数をヒープダンプに書き込みます。runtime·findfunc
、runtime·funcname
、runtime·funcline
を用いてPC値から関数名、ファイル名、行番号を解決し、人間が読める形式でスタックトレース情報を提供します。pc--
やpc -= 4
といったPC値の調整は、プロファイリングにおいて呼び出し命令の正確な開始位置を特定するための一般的な手法です。これは、PCが通常、命令の次のアドレスを指すため、実際の呼び出し命令のアドレスに戻す必要があるためです。
dumpmemprof
関数:- この関数は、メモリプロファイリングデータのダンプ処理全体を調整します。
- まず、
runtime·iterate_memprof(dumpmemprof_callback)
を呼び出し、メモリプロファイラが保持するすべてのバケット情報をdumpmemprof_callback
を通じてヒープダンプに書き込ませます。 - 次に、
runtime·mheap.allspans
を走査し、現在使用中のMSpan
(メモリのチャンク)を調べます。 - 各
MSpan
内のSpecial
構造体(特にKindSpecialProfile
を持つSpecialProfile
)をチェックし、個々の割り当てサンプルを特定します。 - 見つかった割り当てサンプルに対して、
TagAllocSample
タグ、割り当てられたメモリのアドレス、およびその割り当てが属するBucket
のポインタをヒープダンプに書き込みます。これにより、ヒープダンプ内の特定のオブジェクトがどのメモリプロファイルバケットに関連付けられているかを追跡できます。
mdump
関数の変更:- ヒープダンプのメイン関数である
mdump
の最後にdumpmemprof()
の呼び出しが追加されました。これにより、ヒープダンプが生成されるたびに、メモリプロファイリングの統計情報も自動的に含まれるようになります。
- ヒープダンプのメイン関数である
src/pkg/runtime/malloc.h
runtime·iterate_memprof
のプロトタイプ宣言:void runtime·iterate_memprof(void (*callback)(Bucket*, uintptr, uintptr*, uintptr, uintptr, uintptr));
この行は、runtime·iterate_memprof
という関数が、Bucket
、スタックトレースの深さ、スタックトレースのPC配列、割り当てサイズ、割り当て回数、解放回数を受け取るコールバック関数を引数として取ることを宣言しています。これは、heapdump.c
がmprof.goc
内のメモリプロファイルデータにアクセスするためのインターフェースを提供します。
src/pkg/runtime/mprof.goc
runtime·iterate_memprof
関数の実装:- この関数は、
malloc.h
で宣言されたruntime·iterate_memprof
の実際のGoランタイム側の実装です。 runtime·lock(&proflock)
とruntime·unlock(&proflock)
を使用して、メモリプロファイリングデータへのアクセスを同期し、スレッドセーフティを確保しています。for(b=mbuckets; b; b=b->allnext)
ループを使用して、メモリプロファイラが管理するすべてのBucket
をイテレートします。mbuckets
は、すべてのメモリプロファイルバケットのリンクリストの先頭を指すグローバル変数です。- 各
Bucket
に対して、引数として渡されたcallback
関数を呼び出し、そのバケットの関連情報(スタックトレース、サイズ、割り当て回数、解放回数など)を渡します。このコールバックがheapdump.c
のdumpmemprof_callback
になります。
- この関数は、
これらの変更により、Goのヒープダンプは、メモリの割り当て状況をより詳細に分析するための強力なツールとなり、メモリ関連のパフォーマンス問題やリークの特定に大きく貢献します。
関連リンク
- Goのプロファイリングに関する公式ドキュメント: https://go.dev/doc/diagnose-memory-leaks (メモリリーク診断の一般的な情報)
go tool pprof
の使用方法: https://go.dev/blog/pprof (Goプロファイリングツールの詳細)
参考にした情報源リンク
- Goのソースコード (特に
src/runtime
ディレクトリ) - Goの公式ドキュメント
- Goのメモリ管理とプロファイリングに関する技術ブログや記事 (一般的な知識の補完のため)
- Goのコミット履歴 (変更の背景と意図を理解するため)
- https://golang.org/cl/97010043 (このコミットのGo Code Reviewサイトのリンク)