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

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

このコミットは、Goランタイムにおけるメモリプロファイリングに関連するnomemprofというメカニズムを削除するものです。この変更は、特定の再帰的な状況を回避するために導入されていたnomemprofが、現在のランタイムの設計では不要になったことを示しています。

コミット

commit f6329700aee750e3eaded14cf64b2971ace839f6
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Fri Oct 18 10:45:19 2013 +0400

    runtime: remove nomemprof
    Nomemprof seems to be unneeded now, there is no recursion.
    If the recursion will be re-introduced, it will break loudly by deadlocking.
    Fixes #6566.
    
    R=golang-dev, minux.ma, rsc
    CC=golang-dev
    https://golang.org/cl/14695044

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

https://github.com/golang/go/commit/f6329700aee750e3eaded14cf64b2971ace839f6

元コミット内容

Goランタイムからnomemprofというフラグおよび関連するコードを削除します。このフラグは、メモリプロファイリング中に再帰的な呼び出しが発生するのを防ぐために使用されていました。コミットメッセージによると、現在のランタイムには再帰がないため、このフラグは不要になったと判断されています。もし将来的に再帰が再導入された場合、この変更によってデッドロックが発生し、問題が明確に表面化する("break loudly by deadlocking")と述べられています。この変更はIssue #6566を修正します。

変更の背景

Goのランタイムは、ガベージコレクション(GC)やスケジューリングなど、多くの低レベルな操作を行います。これらの操作の中には、メモリの割り当てや解放が頻繁に発生するものがあります。Goには、プログラムのメモリ使用状況を分析するためのプロファイリングツール(pprofなど)が組み込まれています。メモリプロファイリングは、メモリの割り当てや解放のイベントをフックして、その呼び出しスタックやサイズなどを記録することで機能します。

しかし、プロファイリング自体がメモリを割り当てたり、ランタイムの内部関数を呼び出したりする場合があります。もし、メモリプロファイリングのコードが、プロファイリング対象の関数(例えば、メモリ割り当て関数)を再帰的に呼び出すような構造になっていた場合、無限ループやデッドロックといった問題を引き起こす可能性があります。

nomemprofフラグは、このような再帰的なプロファイリングを防ぐためのセーフガードとして導入されていたと考えられます。このフラグがセットされている間は、メモリプロファイリングの処理がスキップされることで、無限再帰やデッドロックを回避していました。

このコミットの背景には、Goランタイムの進化があります。ランタイムの設計が変更され、メモリプロファイリングのコードパスにおいて再帰的な呼び出しが発生しなくなったため、このセーフガードが不要になったと判断されたのです。開発者は、もし将来的に再帰が誤って再導入された場合でも、nomemprofがないことでデッドロックが発生し、その問題がすぐに検出されることを意図しています。これは、サイレントに問題を抱え込むよりも、早期に問題を顕在化させるという設計思想に基づいています。

前提知識の解説

  • Goランタイム (Go Runtime): Goプログラムの実行を管理する低レベルなコンポーネント群です。ガベージコレクション、スケジューリング、メモリ管理、システムコールなどが含まれます。Goプログラムは、OS上で直接実行されるのではなく、このランタイム上で動作します。
  • メモリプロファイリング (Memory Profiling): プログラムがどのようにメモリを使用しているかを分析するプロセスです。Goでは、pprofツールを使用して、メモリの割り当てパターン、リークの可能性、メモリ使用量の多いコードパスなどを特定できます。メモリプロファイリングは、通常、特定のサンプリングレートでメモリ割り当てイベントを記録することで行われます。
  • 再帰 (Recursion): 関数が自分自身を呼び出すことです。プログラミングにおいて一般的なパターンですが、無限再帰はスタックオーバーフローやパフォーマンスの問題を引き起こす可能性があります。
  • デッドロック (Deadlock): 複数のプロセスやスレッドが、互いに相手が保持しているリソースの解放を待ち続け、結果としてどのプロセスも処理を進められなくなる状態です。並行処理において、ロックの取得順序の誤りなどが原因で発生します。
  • m (M struct): Goランタイムの内部構造体で、OSスレッド(Machine)を表します。各Mは、Goルーチンを実行するためのP(Processor)と関連付けられ、スタック、レジスタ、現在のGoルーチンなどの情報を含みます。nomemprofはこのM構造体のフィールドとして定義されていました。
  • runtime·MProf_Malloc / runtime·MProf_Free: Goランタイム内部のメモリプロファイリング関数で、それぞれメモリの割り当てと解放のイベントを処理します。これらの関数は、runtime·mallocgcruntime·freeといった実際のメモリ管理関数から呼び出されます。
  • runtime·lock / runtime·unlock: Goランタイム内部の低レベルなロックプリミティブです。複数のM(OSスレッド)が同時に共有データにアクセスする際に、データ競合を防ぐために使用されます。

技術的詳細

このコミットは、src/pkg/runtime/mprof.gocsrc/pkg/runtime/proc.csrc/pkg/runtime/runtime.hの3つのファイルにわたる変更です。

  1. src/pkg/runtime/runtime.h:

    • struct Mからint32 nomemprof;フィールドが削除されます。これは、各OSスレッド(M)が持つメモリプロファイリングの再帰防止フラグが不要になったことを意味します。
  2. src/pkg/runtime/mprof.goc:

    • runtime·MProf_Malloc関数とruntime·MProf_Free関数から、m->nomemprofに関連するチェックとインクリメント/デクリメントのコードが削除されます。
    • 具体的には、以下のコードブロックが削除されています。
      if(m->nomemprof > 0)
          return;
      m->nomemprof++;
      // ... 処理 ...
      m->nomemprof--;
      
    • このコードは、m->nomemprofが0より大きい場合(つまり、既にメモリプロファイリングが進行中である場合)には、現在のプロファイリング処理をスキップし、再帰を防ぐ役割を果たしていました。処理の前後でnomemprofをインクリメント/デクリメントすることで、プロファイリング処理の「進入」と「退出」をマークしていました。この削除により、これらの関数は常にプロファイリング処理を実行するようになります。
  3. src/pkg/runtime/proc.c:

    • runtime·schedinit関数から、m->nomemprof++;m->nomemprof--;の呼び出しが削除されます。
    • runtime·schedinitは、Goランタイムの初期化時に呼び出される関数です。この関数内でnomemprofが設定されていたのは、初期化プロセス中に発生する可能性のあるメモリ割り当てが、プロファイリングの再帰を引き起こさないようにするためと考えられます。この削除は、初期化プロセスにおいてもnomemprofによる保護が不要になったことを示しています。

これらの変更は、Goランタイムのメモリプロファイリングサブシステムが、もはや再帰的な呼び出しパスを持たないように再設計されたことを強く示唆しています。もし将来的に、メモリプロファイリングコードが、自身がプロファイリングしているメモリ割り当て関数を呼び出すような状況が再発生した場合、nomemprofによる保護がないため、デッドロック(おそらくproflockなどのロックを巡る競合)が発生し、プログラムがハングアップすることで問題が明確に表面化するでしょう。これは、開発者が意図的に「早期に失敗する (fail fast)」設計を選択したことを意味します。

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

src/pkg/runtime/mprof.goc

--- a/src/pkg/runtime/mprof.goc
+++ b/src/pkg/runtime/mprof.goc
@@ -255,10 +255,6 @@ runtime·MProf_Malloc(void *p, uintptr size)
 	uintptr stk[32];
 	Bucket *b;
 
-	if(m->nomemprof > 0)
-		return;
-
-	m->nomemprof++;
 	nstk = runtime·callers(1, stk, 32);
 	runtime·lock(&proflock);
 	b = stkbucket(MProf, stk, nstk, true);
@@ -266,7 +262,6 @@ runtime·MProf_Malloc(void *p, uintptr size)
 	b->recent_alloc_bytes += size;
 	setaddrbucket((uintptr)p, b);
 	runtime·unlock(&proflock);
-	m->nomemprof--;
 }
 
 // Called when freeing a profiled block.
@@ -275,10 +270,6 @@ runtime·MProf_Free(void *p, uintptr size)
 {
 	Bucket *b;
 
-	if(m->nomemprof > 0)
-		return;
-
-	m->nomemprof++;
 	runtime·lock(&proflock);
 	b = getaddrbucket((uintptr)p);
 	if(b != nil) {
@@ -286,7 +277,6 @@ runtime·MProf_Free(void *p, uintptr size)
 		b->recent_free_bytes += size;
 	}
 	runtime·unlock(&proflock);
-	m->nomemprof--;
 }
 
 int64 runtime·blockprofilerate;  // in CPU ticks

src/pkg/runtime/proc.c

--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -133,7 +133,6 @@ runtime·schedinit(void)
 	runtime·sched.maxmcount = 10000;
 	runtime·precisestack = haveexperiment("precisestack");
 
-	m->nomemprof++;
 	runtime·mprofinit();
 	runtime·mallocinit();
 	mcommoninit(m);
@@ -163,7 +162,6 @@ runtime·schedinit(void)
 	procresize(procs);
 
 	mstats.enablegc = 1;
-	m->nomemprof--;
 
 	if(raceenabled)
 		g->racectx = runtime·raceinit();

src/pkg/runtime/runtime.h

--- a/src/pkg/runtime/runtime.h
+++ b/src/pkg/runtime/runtime.h
@@ -310,7 +310,6 @@ struct	M
 	int32	throwing;
 	int32	gcing;
 	int32	locks;
-	int32	nomemprof;
 	int32	dying;
 	int32	profilehz;
 	int32	helpgc;

コアとなるコードの解説

上記の差分が示すように、主要な変更点は以下の通りです。

  1. M構造体からのnomemprofフィールドの削除:

    • src/pkg/runtime/runtime.hにおいて、OSスレッドを表すM構造体からnomemprofというint32型のフィールドが完全に削除されました。これにより、各スレッドがメモリプロファイリングの再帰状態を追跡するためのメカニズムがなくなりました。
  2. runtime·MProf_Mallocおよびruntime·MProf_Freeからのガードコードの削除:

    • src/pkg/runtime/mprof.goc内のruntime·MProf_Malloc(メモリ割り当てプロファイリング)とruntime·MProf_Free(メモリ解放プロファイリング)の両関数から、m->nomemprofをチェックし、インクリメント/デクリメントするコードブロックが削除されました。
    • 削除されたコードは、プロファイリング関数が再帰的に呼び出されるのを防ぐためのものでした。具体的には、m->nomemprof > 0であれば即座にreturnし、プロファイリング処理をスキップしていました。また、処理の開始時にm->nomemprof++、終了時にm->nomemprof--を行うことで、プロファイリングがアクティブであることを示していました。
    • このコードの削除により、runtime·MProf_Mallocruntime·MProf_Freeは、呼び出されるたびに無条件でプロファイリングロジックを実行するようになります。これは、これらの関数が再帰的に呼び出される可能性がなくなった、あるいは再帰が発生しても問題ないようにランタイムの他の部分が設計されたことを意味します。
  3. runtime·schedinitからのnomemprof操作の削除:

    • src/pkg/runtime/proc.c内のruntime·schedinit関数は、ランタイムの初期化を行う重要な関数です。この関数内でも、以前はm->nomemprof++m->nomemprof--が呼び出されていました。これは、ランタイムの初期化中に発生する可能性のあるメモリ割り当てが、プロファイリングの再帰を引き起こさないようにするための予防措置でした。
    • これらの呼び出しが削除されたことで、初期化プロセスにおいてもnomemprofによる保護が不要になったことが確認できます。

これらの変更は、Goランタイムのメモリプロファイリングサブシステムが、再帰的な呼び出しパスを持たないように根本的に再設計されたことを示しています。これにより、nomemprofという複雑なセーフガードが不要になり、コードの簡素化とパフォーマンスの向上が期待されます。同時に、もし将来的にランタイムの変更によってメモリプロファイリングの再帰が誤って再導入された場合、このコミットによってデッドロックが発生し、問題が早期に検出されるという「フェイルファスト」の原則が適用されています。

関連リンク

参考にした情報源リンク

  • Goのソースコード (上記コミットの差分)
  • GoのIssueトラッカー (Issue #6566)
  • GoのGerritコードレビューシステム (CL 14695044)
  • Goのメモリプロファイリングに関する一般的なドキュメント (例: go tool pprofruntime/pprofパッケージのドキュメント)
  • 並行処理におけるデッドロックに関する一般的な知識
  • Goランタイムの内部構造に関する一般的な知識 (M, P, Gスケジューラなど)