[インデックス 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·mallocgc
やruntime·free
といった実際のメモリ管理関数から呼び出されます。runtime·lock
/runtime·unlock
: Goランタイム内部の低レベルなロックプリミティブです。複数のM(OSスレッド)が同時に共有データにアクセスする際に、データ競合を防ぐために使用されます。
技術的詳細
このコミットは、src/pkg/runtime/mprof.goc
、src/pkg/runtime/proc.c
、src/pkg/runtime/runtime.h
の3つのファイルにわたる変更です。
-
src/pkg/runtime/runtime.h
:struct M
からint32 nomemprof;
フィールドが削除されます。これは、各OSスレッド(M)が持つメモリプロファイリングの再帰防止フラグが不要になったことを意味します。
-
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
をインクリメント/デクリメントすることで、プロファイリング処理の「進入」と「退出」をマークしていました。この削除により、これらの関数は常にプロファイリング処理を実行するようになります。
-
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;
コアとなるコードの解説
上記の差分が示すように、主要な変更点は以下の通りです。
-
M
構造体からのnomemprof
フィールドの削除:src/pkg/runtime/runtime.h
において、OSスレッドを表すM
構造体からnomemprof
というint32
型のフィールドが完全に削除されました。これにより、各スレッドがメモリプロファイリングの再帰状態を追跡するためのメカニズムがなくなりました。
-
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_Malloc
とruntime·MProf_Free
は、呼び出されるたびに無条件でプロファイリングロジックを実行するようになります。これは、これらの関数が再帰的に呼び出される可能性がなくなった、あるいは再帰が発生しても問題ないようにランタイムの他の部分が設計されたことを意味します。
-
runtime·schedinit
からのnomemprof
操作の削除:src/pkg/runtime/proc.c
内のruntime·schedinit
関数は、ランタイムの初期化を行う重要な関数です。この関数内でも、以前はm->nomemprof++
とm->nomemprof--
が呼び出されていました。これは、ランタイムの初期化中に発生する可能性のあるメモリ割り当てが、プロファイリングの再帰を引き起こさないようにするための予防措置でした。- これらの呼び出しが削除されたことで、初期化プロセスにおいても
nomemprof
による保護が不要になったことが確認できます。
これらの変更は、Goランタイムのメモリプロファイリングサブシステムが、再帰的な呼び出しパスを持たないように根本的に再設計されたことを示しています。これにより、nomemprof
という複雑なセーフガードが不要になり、コードの簡素化とパフォーマンスの向上が期待されます。同時に、もし将来的にランタイムの変更によってメモリプロファイリングの再帰が誤って再導入された場合、このコミットによってデッドロックが発生し、問題が早期に検出されるという「フェイルファスト」の原則が適用されています。
関連リンク
- Go Issue #6566: https://github.com/golang/go/issues/6566 (このコミットが修正したIssue)
- Go CL 14695044: https://golang.org/cl/14695044 (このコミットに対応するGerritの変更リスト)
参考にした情報源リンク
- Goのソースコード (上記コミットの差分)
- GoのIssueトラッカー (Issue #6566)
- GoのGerritコードレビューシステム (CL 14695044)
- Goのメモリプロファイリングに関する一般的なドキュメント (例:
go tool pprof
、runtime/pprof
パッケージのドキュメント) - 並行処理におけるデッドロックに関する一般的な知識
- Goランタイムの内部構造に関する一般的な知識 (M, P, Gスケジューラなど)