[インデックス 16423] ファイルの概要
このコミットは、Goランタイムにおける内部シンボルテーブルの初期化戦略を変更するものです。具体的には、シンボルテーブルの構築を必要に応じて行う「遅延初期化」から、プログラム起動時に必ず行う「先行初期化」へと移行しています。これにより、ガベージコレクション(GC)やCPUプロファイリングといった、シンボルテーブルを必要とするランタイムの重要な機能の安定性と効率が向上します。
コミット
commit 081129e286fcda2c9525dd08bd90ff6883df0698
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Tue May 28 21:10:10 2013 +0400
runtime: allocate internal symbol table eagerly
we need it for GC anyway.
R=golang-dev, khr, dave, khr
CC=golang-dev
https://golang.org/cl/9728044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/081129e286fcda2c9525dd08bd90ff6883df0698
元コミット内容
runtime: allocate internal symbol table eagerly
we need it for GC anyway.
変更の背景
この変更の背景には、Goランタイムにおけるシンボルテーブルの利用と、その初期化タイミングに関する課題がありました。
-
シグナルハンドラ内での安全性の確保: GoのCPUプロファイリングは、OSのシグナル(例:
SIGPROF
)を利用して定期的にプログラムの実行を中断し、現在のスタックトレースを収集することで行われます。このスタックトレースの解決には、関数名や行番号といったシンボル情報が必要であり、シンボルテーブルが利用されます。しかし、シグナルハンドラは非同期に、かつ任意のタイミングで実行されるため、その内部でメモリ割り当て(malloc
)やロックの取得といった、競合状態やデッドロックを引き起こす可能性のある操作を行うことは非常に危険です。以前の実装では、runtime·SetCPUProfileRate
関数内でruntime·findfunc(0)
を呼び出すことで、シグナルハンドラが有効になる前にシンボルテーブルの構築を強制していました。これは、シグナルハンドラ内でテーブルを構築するのを避けるための回避策でした。 -
ガベージコレクション (GC) の要件: Goのガベージコレクタは、プログラムの実行中に到達可能なオブジェクトを特定するために、スタックをスキャンし、関数呼び出しのコンテキストを理解する必要があります。このプロセスでも、シンボルテーブルの情報が利用されることがあります。コミットメッセージにある「we need it for GC anyway.」という記述は、GCがシンボルテーブルを必要とするため、どうせ初期化が必要になるのであれば、早期に(プログラム起動時に)初期化しておく方が、GCの実行時のオーバーヘッドを減らし、全体的なランタイムの安定性を向上させるという意図を示唆しています。
-
クラッシュ時のデバッグ情報の安定性: 以前の
runtime·proc.c
内のコメントアウトされたコードにも示されているように、プログラムがクラッシュした際にデバッグ情報を取得する際にもシンボルテーブルは重要です。クラッシュ時にmalloc
を呼び出してシンボルテーブルを構築しようとすると、さらなる不安定性やデッドロックを引き起こす可能性があります。そのため、クラッシュ前にシンボルテーブルが完全に構築されていることが望ましいとされていました。
これらの理由から、シンボルテーブルの初期化をプログラムのライフサイクルの早い段階(スケジューラの初期化時)に「先行」して行うことで、上記の問題を根本的に解決し、ランタイム全体の堅牢性とパフォーマンスを向上させることを目指しました。
前提知識の解説
このコミットを理解するためには、以下のGoランタイムに関する基本的な概念を理解しておく必要があります。
-
Goランタイム (Go Runtime): Goプログラムは、OSによって直接実行されるのではなく、Goランタイムと呼ばれる軽量な実行環境上で動作します。Goランタイムは、ガベージコレクション、ゴルーチンのスケジューリング、チャネル通信、メモリ管理、プロファイリングなど、Go言語の並行性モデルとメモリ安全性を実現するための低レベルな機能を提供します。C言語で書かれた部分とGo言語で書かれた部分が混在しています。
-
シンボルテーブル (Symbol Table): プログラムの実行可能ファイル(バイナリ)には、関数名、変数名、ファイル名、行番号といった「シンボル」に関する情報が含まれています。シンボルテーブルは、これらのシンボルと、それらがメモリ上のどこに配置されているか(アドレス)をマッピングするデータ構造です。デバッグ、プロファイリング、スタックトレースの生成、エラーレポートなど、実行中のプログラムの内部状態を解析する際に不可欠な情報源となります。Goランタイムは、実行時にこのシンボルテーブルをメモリ上に構築し、利用します。
-
ガベージコレクション (GC): Goは自動メモリ管理(ガベージコレクション)を採用しています。GCは、プログラムが動的に確保したメモリ領域のうち、もはやどの部分からも参照されなくなった(到達不可能になった)ものを自動的に解放し、再利用可能にする仕組みです。GoのGCは、並行かつ低遅延で動作するように設計されており、実行中のプログラムのスタックをスキャンして、どのメモリがまだ使用されているかを判断する際に、シンボルテーブルの情報を利用することがあります。
-
CPUプロファイリング (CPU Profiling): プロファイリングは、プログラムのパフォーマンス特性を分析する手法です。CPUプロファイリングは、プログラムがCPU時間をどこで消費しているかを特定するのに役立ちます。Goでは、
runtime/pprof
パッケージを通じてCPUプロファイリング機能が提供されています。これは、定期的に(例えば100Hzで)OSのシグナル(SIGPROF
)をGoプロセスに送信し、シグナルハンドラ内で現在の実行中のゴルーチンのスタックトレースをサンプリングすることで実現されます。 -
シグナルハンドラ (Signal Handler): オペレーティングシステムは、プログラムに対して様々なイベント(シグナル)を通知できます。例えば、プログラムの終了要求(
SIGTERM
)、不正なメモリアクセス(SIGSEGV
)、タイマーイベント(SIGPROF
)などです。シグナルハンドラは、これらのシグナルを受け取った際にOSによって呼び出される特別な関数です。シグナルハンドラは、プログラムの通常の実行フローを中断して実行されるため、その内部で実行できる操作には厳しい制約があります。特に、ロックの取得や動的なメモリ割り当て(malloc
)は、シグナルハンドラが割り込んだ元のコードが同じリソースを使用している場合に、デッドロックや競合状態を引き起こす可能性があるため、極力避けるべきとされています。 -
先行割り当て (Eager Allocation) と 遅延割り当て (Lazy Allocation):
- 遅延割り当て (Lazy Allocation): リソース(この場合はシンボルテーブル)が実際に必要になったときに初めて割り当てる(構築する)戦略です。メリットは、不要なリソースの割り当てを避け、起動時間を短縮できる可能性があることです。デメリットは、リソースが必要になった瞬間に割り当てのコストがかかること、また、割り当てが行われるコンテキスト(例: シグナルハンドラ内)によっては問題が発生する可能性があることです。
- 先行割り当て (Eager Allocation): リソースが将来的に必要になることが分かっている場合、プログラムの起動時など、早い段階で事前に割り当てておく戦略です。メリットは、リソースが必要になったときにすぐに利用できること、割り当てのコストが起動時に集中するため、実行中のパフォーマンスに影響を与えにくいこと、そして、割り当てが安全なコンテキストで行われることです。デメリットは、実際にリソースが使われない場合でも割り当てが行われるため、メモリ使用量が増える可能性があることです。
このコミットは、シンボルテーブルの初期化を遅延割り当てから先行割り当てへと変更することで、シグナルハンドラ内での安全性の問題や、GCの効率性、クラッシュ時の安定性といった課題を解決しようとしています。
技術的詳細
このコミットの核心は、Goランタイムの内部シンボルテーブルの初期化ロジックを、より早期かつ安全なタイミングで実行するように変更した点にあります。
以前のGoランタイムでは、シンボルテーブルの構築は必要に応じて行われる「遅延初期化」の側面を持っていました。具体的には、以下の場所でシンボルテーブルの初期化がトリガーされる可能性がありました。
-
CPUプロファイリングの開始時 (
src/pkg/runtime/cpuprof.c
):runtime·SetCPUProfileRate
関数内で、runtime·findfunc(0)
が呼び出されていました。このコメントには「Call findfunc now so that it won't have to build tables during the signal handler.」と明記されており、シグナルハンドラ内でシンボルテーブルを構築するのを避けるための予防策でした。シグナルハンドラは非同期に実行されるため、その内部で複雑なデータ構造の構築やメモリ割り当てを行うことは、デッドロックや競合状態のリスクを伴います。 -
スケジューラの初期化時 (
src/pkg/runtime/proc.c
):runtime·schedinit
関数内には、コメントアウトされたruntime·findfunc(0)
の呼び出しがありました。このコメントは「For debugging: Allocate internal symbol table representation now, so that we don't need to call malloc when we crash.」とあり、クラッシュ時にmalloc
を呼び出す必要がないように、デバッグ目的でシンボルテーブルを早期に割り当てる可能性が示唆されていました。
これらの状況は、シンボルテーブルの初期化が複数の場所で、かつ特定の状況下でトリガーされる可能性があり、そのタイミングや安全性に懸念があったことを示しています。
このコミットでは、以下の変更によってこの問題を解決しています。
-
専用の初期化関数の導入: シンボルテーブルの構築ロジックを
buildfuncs
という静的関数から、runtime·symtabinit
というグローバル関数にリネームし、外部から呼び出し可能にしました。これにより、シンボルテーブルの初期化が明確な単一のエントリポイントを持つことになります。 -
スケジューラ初期化時への統合:
runtime·schedinit
関数(Goプログラムの起動時にスケジューラが初期化されるタイミングで呼び出される)から、新しく公開されたruntime·symtabinit()
を直接呼び出すように変更しました。これにより、Goプログラムが起動し、スケジューラが準備される段階で、シンボルテーブルが常に「先行」して構築されることが保証されます。このタイミングは、まだユーザーコードが実行されておらず、ランタイムが安定した状態にあるため、メモリ割り当てやデータ構造の構築を安全に行うのに適しています。 -
冗長な初期化ロジックの削除:
src/pkg/runtime/symtab.c
から、funcinit
とfunclock
という静的変数、およびruntime·findfunc
内の二重チェックロック(double-checked locking)によるシンボルテーブルの遅延初期化ロジックが削除されました。これは、シンボルテーブルがruntime·schedinit
で一度だけ、かつ安全なコンテキストで初期化されるようになったため、複雑な同期メカニズムや多重初期化のチェックが不要になったためです。 -
CPUプロファイリングからの依存関係の削除:
src/pkg/runtime/cpuprof.c
からruntime·findfunc(0)
の呼び出しが削除されました。シンボルテーブルが既にruntime·schedinit
で初期化されているため、プロファイリング開始時に再度初期化をトリガーする必要がなくなりました。これにより、プロファイリングコードの簡素化と、シグナルハンドラ関連の潜在的な問題の回避が実現されます。
結果として、この変更により、シンボルテーブルはGoプログラムの実行開始直後に一度だけ、安全かつ確実に構築されるようになります。これは、GCがシンボルテーブルの情報を利用する際の安定性を高め、CPUプロファイリングがシグナルハンドラ内で安全に動作することを保証し、クラッシュ時のデバッグ情報取得の信頼性を向上させるという、複数のメリットをもたらします。コミットメッセージの「we need it for GC anyway.」は、GCがシンボルテーブルを必要とするため、どうせなら早期に初期化してしまおうという、実用的な判断を示しています。
コアとなるコードの変更箇所
このコミットによる主要なコード変更は以下の4つのファイルにわたります。
-
src/pkg/runtime/cpuprof.c
runtime·SetCPUProfileRate
関数内から、シンボルテーブルの初期化をトリガーしていたruntime·findfunc(0);
の呼び出しが削除されました。
--- a/src/pkg/runtime/cpuprof.c +++ b/src/pkg/runtime/cpuprof.c @@ -128,10 +128,6 @@ runtime·SetCPUProfileRate(intgo hz) uintptr *p; uintptr n; - // Call findfunc now so that it won't have to - // build tables during the signal handler. - runtime·findfunc(0); - // Clamp hz to something reasonable. if(hz < 0) hz = 0;
-
src/pkg/runtime/proc.c
runtime·schedinit
関数内で、コメントアウトされていたruntime·findfunc(0);
の呼び出しが、新しく導入されたruntime·symtabinit();
の呼び出しに置き換えられました。これにより、スケジューラの初期化時にシンボルテーブルが明示的に初期化されるようになります。
--- a/src/pkg/runtime/proc.c +++ b/src/pkg/runtime/proc.c @@ -133,10 +133,8 @@ runtime·schedinit(void) runtime·goargs(); runtime·goenvs(); - // For debugging: - // Allocate internal symbol table representation now, - // so that we don't need to call malloc when we crash. - // runtime·findfunc(0); + // Allocate internal symbol table representation now, we need it for GC anyway. + runtime·symtabinit(); runtime·sched.lastpoll = runtime·nanotime(); procs = 1;
-
src/pkg/runtime/runtime.h
runtime·symtabinit(void);
の関数プロトタイプが追加されました。これにより、この関数がGoランタイムの他の部分から呼び出し可能になります。
--- a/src/pkg/runtime/runtime.h +++ b/src/pkg/runtime/runtime.h @@ -749,6 +749,7 @@ void runtime·mpreinit(M*); void runtime·minit(void); void runtime·unminit(void); void runtime·signalstack(byte*, int32); +void runtime·symtabinit(void); Func* runtime·findfunc(uintptr); int32 runtime·funcline(Func*, uintptr); void* runtime·stackalloc(uint32);
-
src/pkg/runtime/symtab.c
- シンボルテーブル構築の主要ロジックが含まれていた
buildfuncs
静的関数が、runtime·symtabinit
というグローバル関数にリネームされ、外部から呼び出し可能になりました。 - シンボルテーブルの遅延初期化に使用されていた
funcinit
とfunclock
という静的変数、およびruntime·findfunc
内の二重チェックロックによる初期化ロジックが完全に削除されました。
--- a/src/pkg/runtime/symtab.c +++ b/src/pkg/runtime/symtab.c @@ -193,8 +193,6 @@ static int32 nfunc; static byte **fname; static int32 nfname; -static uint32 funcinit; -static Lock funclock; static uintptr lastvalue; static void -buildfuncs(void) +runtime·symtabinit(void) { extern byte etext[]; @@ -591,26 +589,6 @@ runtime·findfunc(uintptr addr) Func *f; int32 nf, n; - // Use atomic double-checked locking, - // because when called from pprof signal - // handler, findfunc must run without - // grabbing any locks. - // (Before enabling the signal handler, - // SetCPUProfileRate calls findfunc to trigger - // the initialization outside the handler.) - // Avoid deadlock on fault during malloc - // by not calling buildfuncs if we're already in malloc. - if(!m->mallocing && !m->gcing) { - if(runtime·atomicload(&funcinit) == 0) { - runtime·lock(&funclock); - if(funcinit == 0) { - buildfuncs(); - runtime·atomicstore(&funcinit, 1); - } - runtime·unlock(&funclock); - } - } - if(nfunc == 0) return nil; if(addr < func[0].entry || addr >= func[nfunc].entry)
- シンボルテーブル構築の主要ロジックが含まれていた
コアとなるコードの解説
このコミットの核となる変更は、src/pkg/runtime/symtab.c
と src/pkg/runtime/proc.c
に集中しています。
-
src/pkg/runtime/symtab.c
の変更:buildfuncs
からruntime·symtabinit
へのリネームと公開: 以前はstatic void buildfuncs(void)
という静的関数として定義されていたシンボルテーブル構築のロジックが、void runtime·symtabinit(void)
というグローバル関数にリネームされました。static
キーワードが削除されたことで、この関数はsymtab.c
の外部からも呼び出し可能になりました。これは、シンボルテーブルの初期化をGoランタイムの他の部分(特にproc.c
)から制御するための重要な変更です。- 遅延初期化ロジックの削除:
static uint32 funcinit;
とstatic Lock funclock;
という変数が削除されました。これらは、シンボルテーブルがまだ初期化されていないかどうかを追跡し、複数のゴルーチンからの同時アクセスを防ぐためのロックメカニズム(二重チェックロック)に使用されていました。runtime·findfunc
関数内にあった、funcinit
とfunclock
を使用した複雑な初期化ロジック(if(!m->mallocing && !m->gcing) { ... }
のブロック)も完全に削除されました。このロジックは、findfunc
がプロファイリングのシグナルハンドラから呼び出される可能性があるため、ロックを安全に取得できない状況を考慮して設計されていましたが、先行初期化によって不要になりました。 この削除は、シンボルテーブルの初期化がプログラム起動時に一度だけ、かつ安全なコンテキストで行われるようになったため、実行時の複雑な同期処理が不要になったことを意味します。コードが大幅に簡素化され、潜在的な競合状態のリスクが排除されました。
-
src/pkg/runtime/proc.c
の変更:runtime·schedinit
からのruntime·symtabinit
の呼び出し:runtime·schedinit
は、Goランタイムのスケジューラが初期化される際に呼び出される非常に重要な関数です。この関数内で、以前はコメントアウトされていたruntime·findfunc(0);
の代わりに、新しく公開されたruntime·symtabinit();
が呼び出されるようになりました。 この変更により、Goプログラムが起動し、スケジューラが完全に準備される前に、シンボルテーブルが必ず構築されることが保証されます。これは、GCやプロファイリングなど、シンボルテーブルに依存するすべてのランタイム機能が、その情報を必要とする前に利用可能になることを意味します。コミットメッセージの「Allocate internal symbol table representation now, we need it for GC anyway.」というコメントが、この変更の意図を明確に示しています。
-
src/pkg/runtime/cpuprof.c
の変更:runtime·SetCPUProfileRate
からのruntime·findfunc(0)
の削除: 以前は、CPUプロファイリングを開始する際に、シグナルハンドラ内でシンボルテーブルが構築されるのを避けるために、runtime·findfunc(0)
を呼び出して先行して初期化をトリガーしていました。しかし、シンボルテーブルがruntime·schedinit
で既に初期化されるようになったため、この冗長な呼び出しは不要となり、削除されました。これにより、プロファイリングコードが簡素化され、依存関係が明確になりました。
これらの変更は、Goランタイムの起動プロセスにおけるシンボルテーブルの初期化を集中化し、より予測可能で安全なものにすることで、ランタイム全体の堅牢性とパフォーマンスを向上させています。
関連リンク
- Go言語公式ドキュメント:
- Goランタイムに関するブログ記事や資料:
- Goのガベージコレクションに関する詳細な解説(例: "Go's Garbage Collector: A Comprehensive Guide" など、GoのGCの進化を追った記事)
- Goのプロファイリングに関する公式ブログ記事やチュートリアル(例: "Profiling Go Programs")
- Goのソースコードリポジトリ:
- golang/go on GitHub
- 特に
src/runtime
ディレクトリはGoランタイムのC/Goコードが含まれています。
参考にした情報源リンク
- Go言語の公式ドキュメントおよびソースコード
- Goランタイム、ガベージコレクション、プロファイリングに関する一般的な技術記事や解説
- シグナルハンドラにおけるプログラミングの制約に関する一般的なOSの知識
- 先行割り当てと遅延割り当てに関するソフトウェア設計の原則