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

[インデックス 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に近いサイズのオブジェクトが、統計的に期待されるよりもサンプリングされにくくなるというバイアスが生じていました。

このコミットでは、この問題を解決するために以下の変更が導入されました。

  1. profilealloc関数の導入: サンプリングロジックがruntime.mallocgcからprofileallocという新しい静的関数に分離されました。これにより、サンプリングロジックがより明確になり、再利用性が向上します。
  2. 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 の変更

  1. profilealloc 関数の宣言と定義の追加:

    • static void profilealloc(void *v, uintptr size, uintptr typ);runtime·mallocgcの前に宣言されました。
    • largealloc関数の後に、profilealloc関数の具体的な実装が追加されました。この関数は、実際のメモリプロファイリングのサンプリングロジックをカプセル化します。
  2. 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内でより詳細な計算が行われるようになりました。
  3. 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のメモリプロファイラは、さまざまなサイズのオブジェクトに対してより均一で正確なサンプリングを行うことができるようになり、プロファイル結果の信頼性が向上しました。

関連リンク

参考にした情報源リンク