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

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

このコミットは、Go言語のランタイムにおけるパニック処理の内部実装に関するものです。具体的には、src/pkg/runtime/panic.c ファイルが変更されています。このファイルは、Goプログラムがパニック状態に陥った際に、どのようにそのパニックが処理され、スタックアンワインドやrecoverメカニズムが機能するかを定義するC言語のコードを含んでいます。Goランタイムの非常に低レベルな部分であり、Goプログラムの安定性と堅牢性に直接影響を与える重要なコンポーネントです。

コミット

runtime: stack allocate Panic structure during runtime.panic

Update #7347

When runtime.panic is called the *Panic is malloced from the heap. This can lead to a gc cycle while panicing which can make a bad situation worse.

It appears to be possible to stack allocate the Panic and avoid malloc'ing during a panic.

Ref: https://groups.google.com/d/topic/golang-dev/OfxqpklGkh0/discussion

LGTM=minux.ma, dvyukov, rsc
R=r, minux.ma, gobot, rsc, dvyukov
CC=golang-codereviews
https://golang.org/cl/66830043

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

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

元コミット内容

Goランタイムにおいて、runtime.panicが呼び出された際に、Panic構造体がヒープ上にmalloc(メモリ確保)されていました。このヒープ確保は、パニック処理中にガベージコレクション(GC)サイクルを引き起こす可能性があり、既に問題のある状況をさらに悪化させる原因となっていました。このコミットは、Panic構造体をスタック上に確保することで、パニック発生時のmallocを回避し、GCサイクルの発生を防ぐことを目的としています。これにより、パニック処理の堅牢性とパフォーマンスが向上します。

変更の背景

Go言語のパニックメカニズムは、プログラムの異常終了を処理するための重要な機能です。しかし、このコミットが修正しようとしている問題は、パニック発生という既に不安定な状況下で、さらにメモリ確保(malloc)とそれに伴うガベージコレクション(GC)が発生することでした。

具体的には、Goランタイムがパニックを処理する際に、パニックに関する情報を保持するPanic構造体を動的にヒープから確保していました。ヒープからのメモリ確保は、Goのランタイムが管理するメモリ領域(ヒープ)から空き領域を探し、割り当てる操作です。この操作は、ヒープが断片化していたり、空き領域が不足している場合に、ガベージコレクションをトリガーする可能性があります。

パニックは通常、予期せぬエラーやプログラムの整合性の破壊によって発生します。このような状況下でGCが実行されると、以下のような問題が発生する可能性があります。

  1. デッドロックや競合状態の悪化: GCはランタイムの他の部分と協調して動作するため、パニック処理中にGCが割り込むことで、既にデッドロックや競合状態にある可能性のあるプログラムの状態をさらに悪化させる可能性があります。
  2. パフォーマンスの低下: GCは一時的にプログラムの実行を停止させる(ストップ・ザ・ワールド)ことがあります。パニック処理は迅速に行われるべきですが、GCが介入することで処理が遅延し、問題の診断や回復が困難になる可能性があります。
  3. メモリ不足の連鎖: パニックの原因がメモリ不足である場合、パニック処理中にさらにメモリを確保しようとすると、メモリ不足が連鎖的に発生し、システム全体の不安定化を招く可能性があります。

これらの問題を回避するため、パニック処理のようなクリティカルなパスでは、可能な限りメモリ確保を避けることが望ましいとされていました。このコミットは、Panic構造体をスタックに割り当てることで、この問題を根本的に解決しようとしたものです。

前提知識の解説

このコミットの理解には、以下のGo言語およびコンピュータサイエンスの基本的な概念が不可欠です。

  1. Go言語のパニックとリカバリー (Panic and Recover):

    • パニック (Panic): Goプログラムが実行中に回復不可能なエラーに遭遇した際に発生するメカニズムです。パニックが発生すると、通常のプログラムフローは中断され、現在のゴルーチン(軽量スレッド)の関数呼び出しスタックを遡りながら、defer関数が実行されます。
    • リカバリー (Recover): defer関数内でrecover()を呼び出すことで、パニックを捕捉し、プログラムの実行を継続させることができます。これにより、プログラムのクラッシュを防ぎ、エラーハンドリングの柔軟性を提供します。recover()nil以外の値を返した場合、パニックは捕捉され、通常の実行が再開されます。
  2. スタックとヒープ (Stack vs. Heap):

    • スタック (Stack): プログラムの実行中に、関数呼び出しやローカル変数などが一時的に格納されるメモリ領域です。スタックはLIFO(後入れ先出し)の構造を持ち、メモリの割り当てと解放が非常に高速です。関数が呼び出されるとスタックフレームが積まれ、関数が終了するとスタックフレームが解放されます。コンパイル時にサイズが決定できるような、生存期間が短いデータに適しています。
    • ヒープ (Heap): プログラムの実行中に動的にメモリを割り当てるための領域です。ヒープはスタックとは異なり、メモリの割り当てと解放がプログラマによって明示的に、またはガベージコレクタによって行われます。生存期間が長く、コンパイル時にサイズが決定できないようなデータに適しています。ヒープからのメモリ確保はスタックに比べてオーバーヘッドが大きいです。
  3. ガベージコレクション (Garbage Collection - GC):

    • Go言語は自動メモリ管理を採用しており、不要になったメモリ領域を自動的に解放するガベージコレクタを備えています。GCは、ヒープ上に割り当てられたオブジェクトのうち、どのオブジェクトも参照されなくなったものを「ガベージ(ゴミ)」と判断し、そのメモリを再利用可能にします。
    • GCの実行は、プログラムの実行を一時的に停止させる「ストップ・ザ・ワールド(Stop-the-World)」フェーズを伴うことがあります。このフェーズは、GCの効率を向上させるために必要ですが、アプリケーションのレイテンシに影響を与える可能性があります。GoのGCは非常に効率的ですが、それでもクリティカルなパスでのGCの発生は避けたい状況です。
  4. Goランタイム (Go Runtime):

    • Goランタイムは、Goプログラムの実行を管理する低レベルのシステムです。これには、ゴルーチンのスケジューリング、メモリ管理(GCを含む)、チャネル通信、パニック処理、システムコールなどが含まれます。Goランタイムのコードは、主にGoとC(またはPlan 9 C)で書かれており、Goプログラムのパフォーマンスと動作の基盤を形成しています。src/pkg/runtime/ディレクトリには、これらのランタイムのコアコンポーネントのソースコードが含まれています。

技術的詳細

このコミットの核心は、Goランタイムがパニックを処理する際に使用するPanic構造体のメモリ割り当て方法を、ヒープからスタックに変更することです。

変更前(ヒープ割り当て): 変更前は、runtime.panic関数が呼び出されると、Panic構造体はruntime·mal関数(Goランタイム内部のメモリ確保関数で、実質的にはmallocに相当)を使ってヒープ上に動的に確保されていました。

Panic *p; // Panic構造体へのポインタを宣言
// ...
p = runtime·mal(sizeof *p); // ヒープからPanic構造体分のメモリを確保
// ...
g->panic = p; // グローバルなパニックリストにポインタを追加
// ...
runtime·free(p); // パニックが回復された場合にヒープメモリを解放

このアプローチの問題点は、パニックが発生した際に、メモリ確保という追加の操作が必要になることです。特に、ヒープが断片化していたり、メモリ使用量が多い状況では、runtime·malがガベージコレクション(GC)をトリガーする可能性があります。パニックは通常、プログラムが既に不安定な状態にあることを示しており、このような状況でGCが実行されると、以下のような悪影響が考えられます。

  • GCのオーバーヘッド: GCは、プログラムの実行を一時的に停止させる「ストップ・ザ・ワールド」フェーズを伴うことがあります。パニック処理中にこの停止が発生すると、パニックの伝播が遅延したり、デバッグが困難になったりする可能性があります。
  • リソース競合: GCはメモリリソースにアクセスするため、パニックの原因がメモリ関連の問題である場合、GCの実行がさらなるリソース競合を引き起こし、状況を悪化させる可能性があります。
  • デッドロックの可能性: GCはランタイムの他の部分と協調して動作するため、パニック処理中にGCが割り込むことで、既にデリティカルな状態にあるランタイム内部のロックやミューテックスの競合を引き起こし、デッドロックにつながる可能性もゼロではありません。

変更後(スタック割り当て): このコミットでは、Panic構造体をスタック上に直接割り当てるように変更されました。

Panic p; // Panic構造体を直接宣言(スタック上に確保される)
// ...
runtime·memclr((byte*)&p, sizeof p); // スタック上のメモリをゼロクリア
// ...
g->panic = &p; // グローバルなパニックリストにスタック上のPanic構造体のアドレスを追加
// ...
// runtime·free(p); の削除 // ヒープ確保がなくなったため、解放も不要

スタック割り当ての利点は以下の通りです。

  • GCの回避: スタック上のメモリは、関数が終了すると自動的に解放されるため、ガベージコレクションの対象になりません。これにより、パニック処理中にGCがトリガーされる可能性がなくなります。
  • 高速なメモリ割り当て: スタックへのメモリ割り当ては、スタックポインタを移動するだけで済むため、ヒープ割り当てに比べて非常に高速です。これにより、パニック処理のオーバーヘッドが削減され、より迅速なエラーハンドリングが可能になります。
  • 堅牢性の向上: パニック処理は、プログラムが最も脆弱な状態にあるときに実行されるため、外部要因(GCなど)による影響を最小限に抑えることが重要です。スタック割り当ては、このクリティカルなパスの堅牢性を向上させます。

ただし、スタック割り当てには、割り当てられるメモリサイズがコンパイル時に決定される必要があるという制約があります。Panic構造体のサイズは固定であり、パニック処理中に動的に変化することはないため、スタック割り当てに適しています。また、スタックに割り当てられたPanic構造体へのポインタ(&p)がグローバルなパニックリスト(g->panic)に格納されることで、関数が終了してもその情報が失われないように工夫されています。これは、パニックがrecoverされるまで、そのPanic構造体の情報が必要とされるためです。

この変更は、Goランタイムの安定性とパフォーマンスを向上させるための、低レベルながらも非常に重要な最適化と言えます。

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

src/pkg/runtime/panic.c ファイルにおける変更は以下の通りです。

--- a/src/pkg/runtime/panic.c
+++ b/src/pkg/runtime/panic.c
@@ -211,14 +211,14 @@ void
 runtime·panic(Eface e)
 {
 	Defer *d;
-	Panic *p;
+	Panic p; // Panic構造体をポインタではなく直接宣言
 	void *pc, *argp;
 	
-	p = runtime·mal(sizeof *p); // ヒープからのメモリ確保を削除
-	p->arg = e;
-	p->link = g->panic;
-	p->stackbase = g->stackbase;
-	g->panic = p;
+	runtime·memclr((byte*)&p, sizeof p); // スタック上のメモリをゼロクリア
+	p.arg = e; // ポインタアクセスから直接アクセスへ変更
+	p.link = g->panic; // ポインタアクセスから直接アクセスへ変更
+	p.stackbase = g->stackbase; // ポインタアクセスから直接アクセスへ変更
+	g->panic = &p; // スタック上のPanic構造体のアドレスを格納
 
 	for(;;) {
 		d = g->defer;
@@ -231,11 +231,10 @@ runtime·panic(Eface e)
 		pc = d->pc;
 		runtime·newstackcall(d->fn, (byte*)d->args, d->siz);
 		freedefer(d);
-		if(p->recovered) { // ポインタアクセスから直接アクセスへ変更
-			g->panic = p->link;
+		if(p.recovered) {
+			g->panic = p.link;
 			if(g->panic == nil)	// must be done with signal
 				g->sig = 0;
-			runtime·free(p); // ヒープメモリの解放を削除
 			// Pass information about recovering frame to recovery.
 			g->sigcode0 = (uintptr)argp;
 			g->sigcode1 = (uintptr)pc;

コアとなるコードの解説

このコミットの主要な変更点は、runtime·panic関数内でPanic構造体のメモリ割り当て方法が根本的に変更されたことです。

  1. Panic構造体の宣言変更:

    • 変更前: Panic *p; (Panic構造体へのポインタを宣言)
    • 変更後: Panic p; (Panic構造体を直接宣言) この変更により、pはヒープ上に確保されるポインタではなく、現在の関数のスタックフレーム内に直接Panic構造体分のメモリが確保されるようになりました。
  2. メモリ確保の削除とゼロクリアの追加:

    • 変更前: p = runtime·mal(sizeof *p); (runtime·mal関数によるヒープからのメモリ確保)
    • 変更後: runtime·memclr((byte*)&p, sizeof p); (runtime·memclr関数によるスタック上のpのメモリ領域のゼロクリア) ヒープ確保が不要になったため、runtime·malの呼び出しが削除されました。代わりに、スタック上に確保されたPanic構造体のメモリ領域をruntime·memclrでゼロクリアしています。これは、C言語においてスタック上のローカル変数は初期化されないため、予期せぬ値が入るのを防ぐための安全策です。
  3. 構造体メンバーへのアクセス方法の変更:

    • 変更前: p->arg = e;, p->link = g->panic;, p->stackbase = g->stackbase; (ポインタpを介したメンバーアクセス)
    • 変更後: p.arg = e;, p.link = g->panic;, p.stackbase = g->stackbase; (直接宣言された構造体pへのメンバーアクセス) pがポインタではなくなったため、メンバーへのアクセスは->演算子から.演算子に変更されました。
  4. グローバルなパニックリストへの登録方法の変更:

    • 変更前: g->panic = p; (ヒープ上のPanic構造体へのポインタを格納)
    • 変更後: g->panic = &p; (スタック上のPanic構造体のアドレスを格納) g->panicは、現在のゴルーチン(g)が持つパニックのリンクリストの先頭を指します。pがスタック変数になったため、そのアドレス(&p)をリストに格納することで、関数が終了してもPanic構造体の情報が失われずに参照され続けるようにしています。これは、パニックがrecoverされるまで、この情報が必要となるためです。
  5. ヒープメモリ解放の削除:

    • 変更前: runtime·free(p); (パニックが回復された際にヒープメモリを解放)
    • 変更後: 削除 Panic構造体がヒープに割り当てられなくなったため、対応するruntime·freeの呼び出しも不要となり、削除されました。

これらの変更により、パニック処理のクリティカルパスからヒープ割り当てとそれに伴うGCの可能性が排除され、Goランタイムの堅牢性とパフォーマンスが向上しました。

関連リンク

  • Go CL (Change List): https://golang.org/cl/66830043
  • Go Developers Mailing List Discussion: https://groups.google.com/d/topic/golang-dev/OfxqpklGkh0/discussion このリンクは、この変更が提案され、議論されたGo開発者メーリングリストのスレッドです。このスレッドを読むことで、変更の背景にある詳細な議論や、他の開発者からのフィードバック、代替案の検討などを深く理解することができます。

参考にした情報源リンク

  • Go言語の公式ドキュメント (Panic and Recover): https://go.dev/blog/defer-panic-and-recover
  • Go言語のメモリ管理とガベージコレクションに関する一般的な情報源 (例: Goの公式ブログや技術記事)
  • C言語におけるスタックとヒープの概念に関する一般的な情報源
  • Goランタイムのソースコード (特にsrc/runtime/ディレクトリ内の関連ファイル)
  • Issue 7347: https://go.dev/issue/7347 (このコミットが解決した問題のトラッキング)