[インデックス 18472] ファイルの概要
このコミットは、Goランタイムのメモリプロファイリングにおけるサンプリングの精度を向上させることを目的としています。特に、MemProfileRate
に近いサイズのオブジェクトがアンダーサンプリングされるという既存のバイアスを修正し、より正確なプロファイリング結果を得られるようにします。
コミット
commit bf0d71af2907401a83f846d2f6baff38029aa4cd
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Wed Feb 12 22:36:45 2014 +0400
runtime: more precise mprof sampling
Better sampling of objects that are close in size to sampling rate.
See the comment for details.
LGTM=rsc
R=golang-codereviews, rsc
CC=golang-codereviews
https://golang.org/cl/43830043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/bf0d71af2907401a83f846d2f6baff38029aa4cd
元コミット内容
runtime: more precise mprof sampling
Better sampling of objects that are close in size to sampling rate.
See the comment for details.
変更の背景
Goのメモリプロファイラはサンプリングベースで動作します。これは、すべてのメモリ割り当てを記録するのではなく、設定されたレートに基づいて一部の割り当てをサンプリングすることを意味します。このサンプリングメカニズムは、特にMemProfileRate
(メモリプロファイリングのサンプリングレート)の設定によっては、バイアス(偏り)を導入する可能性がありました。
具体的には、割り当てられたオブジェクトのサイズがMemProfileRate
に近い場合、それらのオブジェクトが期待よりも少なくサンプリングされるという問題がありました。これは、サンプリングの決定ロジックにおいて、現在の割り当てサイズが次のサンプリングポイントまでの残りバイト数から単純に差し引かれるため、特定のサイズのオブジェクトが常に「残りのバイト数」を消費し、結果としてサンプリングの機会を逃すことがあったためです。このアンダーサンプリングは、メモリプロファイルの精度を低下させ、実際のメモリ使用パターンを正確に反映しない可能性がありました。
このコミットは、このサンプリングバイアスを修正し、MemProfileRate
に近いサイズのオブジェクトでもより正確にサンプリングされるようにすることで、メモリプロファイラの信頼性を向上させることを目的としています。
前提知識の解説
Goのメモリプロファイリング
Goには、アプリケーションのメモリ使用量を分析するためのプロファイリングツールが組み込まれています。これは主にpprof
ツールを通じて利用されます。メモリプロファイラは、ヒープ割り当てをサンプリングし、どのコードパスがメモリを割り当てているかを特定するのに役立ちます。
MemProfileRate
MemProfileRate
は、Goのメモリプロファイリングにおけるサンプリングレートを制御する変数です。この値は、平均して何バイトのメモリ割り当てごとに1回サンプリングを行うかを示します。例えば、MemProfileRate
が512KBの場合、プロファイラは平均して512KBの割り当てごとに1回サンプリングを行うことを目指します。
- サンプリングメカニズム: Goのメモリプロファイラは、ヒープ割り当てをサンプリングします。ヒープメモリが割り当てられる際にスタック情報が記録されます。サンプリングプロセスは、指数分布に基づく擬似乱数ジェネレータに依存しています。このジェネレータは、割り当てられたメモリサイズに関してサンプル間の距離を定義します。次のランダムなサンプリングポイントを越える割り当てのみがサンプリングされます。
- デフォルト値: デフォルトのサンプリングレートは、割り当てられたメモリ512KBあたり1サンプルです。
MemProfileRate
を1に設定:MemProfileRate
を1に設定すると、プロファイラはすべての割り当てに関する情報を記録します。これによりサンプリングバイアスはなくなりますが、すべての割り当てを記録し、スタックトレースをアンワインドするオーバーヘッドのため、アプリケーションの速度が大幅に低下する可能性があります。MemProfileRate
を0に設定:MemProfileRate
を0に設定すると、メモリプロファイリングが完全にオフになります。
サンプリングバイアス
サンプリングプロファイラでは、サンプリングの性質上、ある種のバイアスが発生する可能性があります。
- 大きなオブジェクトはサンプリングされやすい: オブジェクトがサンプリングされる確率はそのサイズに比例します。これは、大きな割り当てほど小さな割り当てよりもサンプリングされる可能性が高いことを意味します。
- 小さな割り当てが見逃される可能性:
MemProfileRate
が高すぎる場合(例:デフォルトの512KB)、小さく頻繁な割り当てはサンプリング閾値を超えることがなく、結果としてプロファイルが空になったり不完全になったりする可能性があります。これにより、多くの小さな割り当てによって引き起こされるメモリ問題を特定することが難しくなります。 - 精度への影響: サンプリングの性質上、プロファイルは近似値です。主要なメモリ消費者を特定するのには一般的に効果的ですが、特に多様な割り当てパターンを持つプログラムの場合、真の割り当て分布を完全に表すとは限りません。
このコミットで修正されるバイアスは、特にMemProfileRate
に近いサイズのオブジェクトが、サンプリングポイントを「消費」する際に、その「残り」が適切に考慮されないために発生していました。
技術的詳細
このコミットの核心は、メモリプロファイリングのサンプリングロジック、特にruntime.mallocgc
関数内のnext_sample
の計算方法の変更にあります。
以前のサンプリングロジックでは、m->mcache->next_sample
という変数が次のサンプリングまでの残りバイト数を示していました。新しい割り当てが行われるたびに、その割り当てサイズがnext_sample
から差し引かれました。next_sample
が0以下になった場合、その割り当てはサンプリングされ、新しいnext_sample
がランダムに設定されました。
問題は、size >= rate
の場合に即座にサンプリングされるロジックと、size < rate
の場合のnext_sample
の更新ロジックにありました。特にsize < rate
の場合、m->mcache->next_sample -= size;
という単純な減算では、next_sample
が負の値になる可能性があり、その「残り」が次のサンプリングポイントの計算に適切に引き継がれませんでした。これにより、MemProfileRate
に近いサイズのオブジェクトが、統計的に期待されるよりもサンプリングされにくくなるというバイアスが生じていました。
このコミットでは、この問題を解決するために以下の変更が導入されました。
profilealloc
関数の導入: サンプリングロジックがruntime.mallocgc
からprofilealloc
という新しい静的関数に分離されました。これにより、サンプリングロジックがより明確になり、再利用性が向上します。next_sample
の計算の改善:profilealloc
関数内で、size < rate
の場合のnext_sample
の計算が改善されました。- 新しい
next_sample
は、runtime·fastrand1() % (2*rate)
によってランダムに生成されます。これは以前と同じです。 - しかし、重要な変更は、
next -= (size - c->next_sample);
という行が追加されたことです。ここでc->next_sample
は、サンプリングが行われる直前のnext_sample
の値(つまり、割り当てサイズを差し引く前の値)です。 - この計算は、現在の割り当てがサンプリングポイントをどれだけ「超過」したか(または「不足」したか)を考慮に入れます。具体的には、
size - c->next_sample
は、現在の割り当てがサンプリングポイントをどれだけ超えていたか(または、サンプリングポイントに到達するためにどれだけ不足していたか)を示します。この「残り」を次のサンプリングポイントから差し引くことで、サンプリングの統計的特性がより正確に保たれます。 if(next < 0) next = 0;
というガードも追加され、next_sample
が負の値にならないようにしています。
- 新しい
この修正により、MemProfileRate
に近いサイズのオブジェクトがサンプリングされる確率が、統計的に期待される値に近づき、メモリプロファイルの精度が向上します。
コアとなるコードの変更箇所
misc/pprof
--- a/misc/pprof
+++ b/misc/pprof
@@ -2652,6 +2652,7 @@ sub RemoveUninterestingFrames {
'makechan',
'makemap',
'mal',
+ 'profilealloc',
'runtime.new',
'makeslice1',
'runtime.malloc',
src/pkg/runtime/malloc.goc
--- a/src/pkg/runtime/malloc.goc
+++ b/src/pkg/runtime/malloc.goc
@@ -28,6 +28,7 @@ extern MStats mstats; // defined in zruntime_def_$GOOS_$GOARCH.go
extern volatile intgo runtime·MemProfileRate;
static void* largealloc(uint32, uintptr*);
+static void profilealloc(void *v, uintptr size, uintptr typ);\n
// Allocate an object of at least size bytes.
// Small objects are allocated from the per-thread cache's free lists.
@@ -191,29 +192,23 @@ runtime·mallocgc(uintptr size, uintptr typ, uint32 flag)
runtime·settype_flush(m);
if(raceenabled)
runtime·racemalloc(v, size);
- m->locks--;
- if(m->locks == 0 && g->preempt) // restore the preemption request in case we've cleared it in newstack
- g->stackguard0 = StackPreempt;
if(runtime·debug.allocfreetrace)
goto profile;
if(!(flag & FlagNoProfiling) && (rate = runtime·MemProfileRate) > 0) {
- if(size >= rate)
- goto profile;
- if(m->mcache->next_sample > size)
- m->mcache->next_sample -= size;
+ if(size < rate && size < c->next_sample)
+ c->next_sample -= size;
else {
- // pick next profile time
- // If you change this, also change allocmcache.
- // If you change this, also change allocmcache.
- if(rate > 0x3fffffff) // make 2*rate not overflow
- rate = 0x3fffffff;
- m->mcache->next_sample = runtime·fastrand1() % (2*rate);
profile:
- runtime·MProf_Malloc(v, size, typ);
+ profilealloc(v, size, typ);
}
}
+ m->locks--;
+ if(m->locks == 0 && g->preempt) // restore the preemption request in case we've cleared it in newstack
+ g->stackguard0 = StackPreempt;
+
if(!(flag & FlagNoInvokeGC) && mstats.heap_alloc >= mstats.next_gc)
runtime·gc(0);
@@ -245,6 +240,32 @@ largealloc(uint32 flag, uintptr *sizep)
return v;
}
+static void
+profilealloc(void *v, uintptr size, uintptr typ)
+{
+ uintptr rate;
+ int32 next;
+ MCache *c;
+
+ c = m->mcache;
+ rate = runtime·MemProfileRate;
+ if(size < rate) {
+ // pick next profile time
+ // If you change this, also change allocmcache.
+ if(rate > 0x3fffffff) // make 2*rate not overflow
+ rate = 0x3fffffff;
+ next = runtime·fastrand1() % (2*rate);
+ // Subtract the "remainder" of the current allocation.
+ // Otherwise objects that are close in size to sampling rate
+ // will be under-sampled, because we consistently discard this remainder.
+ next -= (size - c->next_sample);
+ if(next < 0)
+ next = 0;
+ c->next_sample = next;
+ }
+ runtime·MProf_Malloc(v, size, typ);
+}
+
void*
runtime·malloc(uintptr size)
{
コアとなるコードの解説
misc/pprof
の変更
misc/pprof
はGoのプロファイリングツールが使用するスクリプトです。RemoveUninterestingFrames
サブルーチンに'profilealloc'
が追加されました。これは、プロファイル結果から特定のフレーム(関数呼び出し)を除外するためのリストです。profilealloc
関数自体はプロファイリングの内部ロジックであり、ユーザーが直接関心を持つ呼び出しスタックの一部ではないため、プロファイル結果のノイズを減らすために除外対象に追加されました。
src/pkg/runtime/malloc.goc
の変更
-
profilealloc
関数の宣言と定義の追加:static void profilealloc(void *v, uintptr size, uintptr typ);
がruntime·mallocgc
の前に宣言されました。largealloc
関数の後に、profilealloc
関数の具体的な実装が追加されました。この関数は、実際のメモリプロファイリングのサンプリングロジックをカプセル化します。
-
runtime·mallocgc
内のサンプリングロジックの変更:- 以前は
runtime·mallocgc
内に直接記述されていたサンプリングロジックが、profilealloc
関数への呼び出しに置き換えられました。 - 変更前:
if(size >= rate) goto profile; if(m->mcache->next_sample > size) m->mcache->next_sample -= size; else { // pick next profile time // If you change this, also change allocmcache. if(rate > 0x3fffffff) // make 2*rate not overflow rate = 0x3fffffff; m->mcache->next_sample = runtime·fastrand1() % (2*rate); profile: runtime·MProf_Malloc(v, size, typ); }
- 変更後:
if(size < rate && size < c->next_sample) c->next_sample -= size; else { profile: profilealloc(v, size, typ); }
- この変更により、
size >= rate
の場合の即時サンプリングロジックが削除され、すべてのサンプリングがprofilealloc
関数を通じて行われるようになりました。また、size < rate
の場合のnext_sample
の減算ロジックも簡素化され、profilealloc
内でより詳細な計算が行われるようになりました。
- 以前は
-
profilealloc
関数の実装:- この関数は、割り当てられたオブジェクト
v
、そのサイズsize
、型typ
を受け取ります。 rate = runtime·MemProfileRate;
で現在のサンプリングレートを取得します。if(size < rate)
のブロック内で、サンプリングの決定とnext_sample
の更新が行われます。next = runtime·fastrand1() % (2*rate);
で次のサンプリングポイントをランダムに決定します。これは、平均してrate
バイトごとに1回サンプリングされるようにするための一般的な手法です。2*rate
は、サンプリング間隔の最大値を設定し、オーバーフローを防ぐためのものです。next -= (size - c->next_sample);
: これがこのコミットの最も重要な変更点です。c->next_sample
は、現在の割り当てが行われる前の、次のサンプリングまでの残りバイト数です。size - c->next_sample
は、現在の割り当てがc->next_sample
をどれだけ「超過」したか(または「不足」したか)を示します。- この「超過分」または「不足分」を次のサンプリングポイント
next
から差し引くことで、サンプリングの統計的特性がより正確に保たれます。例えば、もし現在の割り当てがサンプリングポイントを大きく超えていた場合、次のサンプリングポイントはそれだけ早く来るように調整されます。これにより、MemProfileRate
に近いサイズのオブジェクトがアンダーサンプリングされるバイアスが解消されます。
if(next < 0) next = 0;
は、計算結果が負になった場合にnext_sample
が0になるようにするガードです。c->next_sample = next;
で更新されたnext_sample
がキャッシュに保存されます。
runtime·MProf_Malloc(v, size, typ);
は、実際にメモリプロファイルに割り当て情報を記録する関数です。これは、サンプリングが決定された場合にのみ呼び出されます。
- この関数は、割り当てられたオブジェクト
この変更により、Goのメモリプロファイラは、さまざまなサイズのオブジェクトに対してより均一で正確なサンプリングを行うことができるようになり、プロファイル結果の信頼性が向上しました。
関連リンク
- Goのメモリプロファイリングに関する公式ドキュメント: https://go.dev/blog/pprof
- Goの
runtime
パッケージのソースコード: https://github.com/golang/go/tree/master/src/runtime
参考にした情報源リンク
- Go's memory profiler: sampling bias and MemProfileRate: https://hackernoon.com/go-memory-profiler-sampling-bias-and-memprofilerate
- Go Memory Profiling: A Deep Dive: https://austburn.me/go-memory-profiling-a-deep-dive/
- Go Memory Profiling: https://go.dev/doc/diagnose-memory
- Understanding Go Memory Profiling: https://medium.com/@felixge/understanding-go-memory-profiling-b2c117137394
- Go issue related to memory profiling: https://github.com/golang/go/issues/23454 (これは直接このコミットに関連するものではないが、メモリプロファイリングの課題を示す例として挙げた)
- Go runtime source code on GitHub: https://github.com/golang/go/