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

[インデックス 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ランタイムのヒープダンプにメモリプロファイリングの統計情報を統合することです。これを実現するために、以下の変更が行われています。

  1. 新しいヒープダンプタグの導入: src/pkg/runtime/heapdump.cにおいて、ヒープダンプファイル内でメモリプロファイリングデータと割り当てサンプルを識別するための新しいタグが定義されました。

    • TagMemProf = 16: メモリプロファイリングのバケット情報(スタックトレースごとの統計)を示すために使用されます。
    • TagAllocSample = 17: 個々の割り当てサンプル情報を示すために使用されます。
  2. メモリプロファイリングデータのダンプ関数 dumpmemprof_callback の追加: src/pkg/runtime/heapdump.cdumpmemprof_callbackという静的関数が追加されました。この関数は、メモリプロファイラの各Bucket(特定の呼び出しスタックからの割り当ての集計)に対して呼び出されるコールバック関数として機能します。

    • Bucketのポインタ、スタックトレースの深さ、スタックトレースのPC(プログラムカウンタ)値、割り当てサイズ、割り当て回数、解放回数を受け取ります。
    • これらの情報をTagMemProfタグとともにヒープダンプに書き込みます。
    • スタックトレースのPC値から関数名、ファイル名、行番号を解決し、ダンプに含めます。特に、PC値から正確な呼び出し元を特定するために、PC値を調整するロジック(pc--pc -= 4)が含まれています。これは、プロファイリングにおける一般的なテクニックで、命令の直前のアドレスを指すように調整することで、より正確なソースコードの位置を特定します。
  3. メモリプロファイリングのイテレーション関数 runtime·iterate_memprof の追加: src/pkg/runtime/mprof.gocruntime·iterate_memprof関数が追加され、src/pkg/runtime/malloc.hでそのプロトタイプが宣言されました。

    • この関数は、メモリプロファイラが管理するすべてのBucketを安全に(proflockというロックを使用して)イテレートし、各Bucketに対してdumpmemprof_callbackのようなコールバック関数を呼び出すメカニズムを提供します。
    • これにより、ヒープダンプ処理がメモリプロファイラの内部データにアクセスできるようになります。
  4. 割り当てサンプルのダンプ: dumpmemprof関数は、runtime·iterate_memprofを呼び出してバケット情報をダンプするだけでなく、MSpanSpecialProfileを走査して、個々の割り当てサンプルもダンプします。

    • MSpanはメモリのチャンクを表し、SpecialProfileは特定のメモリ領域に関連付けられたプロファイリング情報(どのBucketに関連するかなど)を保持します。
    • これにより、TagAllocSampleタグとともに、割り当てられたオブジェクトのアドレスと、それに関連するBucketのポインタがヒープダンプに書き込まれます。これにより、ヒープダンプ内の特定のオブジェクトがどのメモリプロファイルバケットに属するかを追跡できるようになります。
  5. ヒープダンプへの統合: src/pkg/runtime/heapdump.cmdump関数(ヒープダンプのメインエントリポイント)内で、新しく追加された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 の変更: TagMemProfTagAllocSample という新しい定数が追加されました。これらはヒープダンプファイル内でメモリプロファイルデータと個々の割り当てサンプルを区別するための識別子として機能します。
  • dumpmemprof_callback 関数:
    • この関数は、メモリプロファイリングの各「バケット」(特定の呼び出しスタックからの割り当ての集計)に対して呼び出されるコールバックです。
    • dumpintdumpcstr を使用して、バケットのポインタ、割り当てサイズ、スタックトレースの深さ、スタックトレースの各PC値、割り当て回数、解放回数をヒープダンプに書き込みます。
    • runtime·findfuncruntime·funcnameruntime·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.cmprof.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.cdumpmemprof_callback になります。

これらの変更により、Goのヒープダンプは、メモリの割り当て状況をより詳細に分析するための強力なツールとなり、メモリ関連のパフォーマンス問題やリークの特定に大きく貢献します。

関連リンク

参考にした情報源リンク

  • Goのソースコード (特に src/runtime ディレクトリ)
  • Goの公式ドキュメント
  • Goのメモリ管理とプロファイリングに関する技術ブログや記事 (一般的な知識の補完のため)
  • Goのコミット履歴 (変更の背景と意図を理解するため)
  • https://golang.org/cl/97010043 (このコミットのGo Code Reviewサイトのリンク)