[インデックス 19579] ファイルの概要
このコミットは、Goランタイムのガベージコレクション(GC)に関連するafterprologue
チェックの削除に関するものです。Go 1.3で導入されたスタックの正確なスキャンとスタックコピーの仕組みにより、このチェックが不要になったことを示しています。
コミット
runtime: remove obsolete afterprologue check
Afterprologue check was required when did not know
about return arguments of functions and/or they were not zeroed.
Now 100% precision is required for stacks due to stack copying,
so it must work w/o afterprologue one way or another.
I can limit this change for 1.3 to merely adding a TODO,
but this check is super confusing so I don't want this knowledge to get lost.
LGTM=rsc
R=golang-codereviews, gobot, rsc, khr
CC=golang-codereviews, khr, rsc
https://golang.org/cl/96580045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/cc81712190ba438097e4e0262ec8d29f8d468dcf
元コミット内容
このコミットは、Goランタイムからafterprologue
チェックを削除するものです。コミットメッセージによると、このチェックは、関数の戻り値の引数について情報が不足していたり、それらがゼロ初期化されていなかったりした場合に必要とされていました。しかし、スタックコピーの導入により、スタックに対して100%の精度が求められるようになったため、afterprologue
チェックなしでも機能するようになりました。コミットの作者は、この変更をGo 1.3に限定してTODOを追加することもできたが、このチェックが非常に混乱を招くものであり、その知識が失われることを望まないため、削除することを選択したと述べています。
変更の背景
この変更の背景には、Go 1.3で導入されたGoランタイムとガベージコレクタ(GC)の大きな進化があります。Go 1.3以前のGCは、ヒープ上の値に対しては正確(precise)でしたが、スタック上の値に対しては「ほぼ正確(mostly precise)」または「保守的(conservative)」でした。これは、スタック上の整数値がたまたまメモリアドレスのように見えた場合、GCがそれをポインタと誤認し、実際には参照されていないメモリが解放されないという問題を引き起こす可能性がありました。
Go 1.3では、この問題に対処するため、以下の2つの重要な変更が導入されました。
- 正確なスタックスキャン(Precise Stack Scanning): Go 1.3では、スタック上の値に対しても100%正確なGCが実現されました。これにより、ランタイムはスタック上のどの値がポインタで、どれがそうでないかを正確に識別できるようになりました。
- スタックコピー(Stack Copying / Contiguous Stacks): ゴルーチンのスタック管理戦略が変更され、スタックは必要に応じて再割り当てされ、コピーされるようになりました。この「連続したスタック」モデルは、以前のスタック分割メカニズムにおける「ホットスプリット」問題などを解決しました。
afterprologue
チェックは、これらの変更が導入される前の、スタックのポインタ情報が不完全だった時代に、特に戻り値の引数に関するGCの正確性を確保するために存在していました。しかし、Go 1.3でスタックが完全に正確にスキャンできるようになり、スタックコピーによってスタックフレームが移動してもポインタを正確に更新できるようになったため、この補助的なチェックは不要となり、削除されることになりました。
前提知識の解説
Goのガベージコレクション (GC) の基本
Go言語は、メモリ管理にガベージコレクション(GC)を採用しています。開発者が手動でメモリを解放する必要がなく、ランタイムが不要になったメモリを自動的に回収します。GoのGCは、並行(concurrent)かつ低遅延(low-latency)を目指して設計されており、アプリケーションの実行と並行して動作します。GCは、メモリ上のオブジェクトをスキャンし、到達可能な(参照されている)オブジェクトを特定し、到達不可能なオブジェクトを解放します。
スタックとヒープ
- スタック(Stack): 関数呼び出しやローカル変数、関数の引数などが格納されるメモリ領域です。LIFO(Last-In, First-Out)の構造を持ち、高速にアクセスできます。Goでは、ゴルーチンごとにスタックが割り当てられます。
- ヒープ(Heap): プログラムの実行中に動的に割り当てられるメモリ領域です。GCの主な対象となります。
ポインタと非ポインタ
- ポインタ(Pointer): メモリアドレスを指す値です。GCはポインタを辿って、どのオブジェクトがまだ使用されているかを判断します。
- 非ポインタ(Non-pointer): 整数、浮動小数点数、ブール値など、メモリアドレスを指さない値です。
Go 1.2以前の「ほぼ正確なGC」と「保守的なスタックスキャン」
Go 1.2以前のGCは、ヒープ上のポインタは正確に識別できましたが、スタック上のポインタについては「保守的」なアプローチを取っていました。これは、スタック上の値がポインタであるかどうかを常に正確に判断できるわけではないことを意味します。例えば、スタック上の単なる整数値が、たまたま有効なメモリアドレスのように見えた場合、GCはそれをポインタと誤認し、その「ポインタ」が指す先のメモリを解放しない可能性がありました。これにより、メモリリークやGCの効率低下につながることがありました。
Go 1.3での「正確なスタックスキャン」と「スタックコピー」
Go 1.3では、この保守的なスタックスキャンが改善され、スタック上のポインタも100%正確に識別できるようになりました。これは、コンパイラがスタックフレーム内のどの位置にポインタが存在するかを示すメタデータ(ポインタマップ)を生成するようになったためです。
また、Go 1.3ではゴルーチンのスタック管理が「スタックコピー」方式に変更されました。以前は、スタックが不足すると新しいスタックフレームを既存のスタックの隣に割り当てる「スタック分割」方式でしたが、これによりスタックが非連続になることがありました。スタックコピー方式では、スタックが不足するとより大きな新しいスタック領域を確保し、既存のスタックの内容をそこにコピーします。この方式は、スタックの成長をより効率的にし、GCがスタックをスキャンする際の複雑さを軽減します。正確なスタックスキャンは、スタックコピー時にポインタを正確に更新するために不可欠です。
afterprologue
チェックの役割 (Go 1.2以前)
afterprologue
チェックは、Go 1.2以前の保守的なスタックスキャン環境下で、GCの正確性を補完するためのメカニズムでした。特に、関数呼び出しのプロローグ(関数の冒頭部分)が完了した後、戻り値の引数などがスタックに配置された状態でのポインタの扱いに関する不確実性を解消するために使用されました。スタック上のポインタ情報が不完全な場合でも、このチェックを通じて特定の条件(例えば、戻り値がゼロ初期化されているかなど)を確認することで、GCが誤った判断を下すことを防ぐ役割を担っていました。
技術的詳細
このコミットは、Go 1.3で導入された正確なスタックスキャンとスタックコピーの仕組みが、afterprologue
チェックを完全に不要にしたことを示しています。
以前のGoランタイムでは、GCがスタックをスキャンする際に、関数のプロローグが完了した直後(afterprologue
)の状態と、それ以外の状態とで、スタック上のポインタの扱いを変える必要がありました。これは、プロローグが完了した直後には、関数の戻り値の引数などがまだ完全に初期化されていなかったり、ポインタ情報がGCにとって不明瞭な状態であったりする可能性があったためです。afterprologue
フラグは、GCがこの特定の状態を認識し、それに応じた特別な処理(例えば、戻り値の引数がポインタであるかどうかを保守的に判断したり、ゼロ初期化を強制したりする)を行うためのものでした。
しかし、Go 1.3以降、コンパイラはスタックフレーム内のすべてのポインタの位置を正確に記述したポインタマップを生成するようになりました。これにより、ランタイムはスタック上のどの位置にポインタが存在し、どの位置に非ポインタが存在するかを100%の精度で把握できるようになりました。また、スタックコピーの導入により、スタックが移動しても、ポインタマップに基づいてポインタを正確に更新できるようになりました。
この結果、GCはもはやafterprologue
という状態に特別な注意を払う必要がなくなりました。スタック上のすべての値が常に正確に識別され、ポインタであれば適切にスキャンされるため、afterprologue
チェックが提供していた安全策は冗長になったのです。このコミットは、その冗長になったコードを削除し、ランタイムの複雑性を軽減することを目的としています。
コアとなるコードの変更箇所
変更はsrc/pkg/runtime/mgc0.c
ファイルに集中しています。
-
scaninterfacedata
関数のシグネチャ変更:- 変更前:
scaninterfacedata(uintptr bits, byte *scanp, bool afterprologue, void *wbufp)
- 変更後:
scaninterfacedata(uintptr bits, byte *scanp, void *wbufp)
afterprologue
引数が削除されました。
- 変更前:
-
scanbitvector
関数のシグネチャ変更:- 変更前:
scanbitvector(Func *f, bool precise, byte *scanp, BitVector *bv, bool afterprologue, void *wbufp)
- 変更後:
scanbitvector(Func *f, bool precise, byte *scanp, BitVector *bv, void *wbufp)
afterprologue
引数が削除されました。
- 変更前:
-
scanframe
関数内のafterprologue
変数の削除と関連ロジックの変更:bool afterprologue;
の宣言が削除されました。afterprologue = (frame->varp > (byte*)frame->sp);
というafterprologue
変数を設定する行が削除されました。if(afterprologue)
で囲まれていたローカル変数のスキャンロジックが、afterprologue
の条件なしで直接実行されるようになりました。これにより、ローカル変数のスキャンが常に正確なポインタマップに基づいて行われるようになりました。scanbitvector
の呼び出しからafterprologue
引数が削除されました。
コアとなるコードの解説
これらのコード変更は、afterprologue
という概念がGoランタイムのガベージコレクションプロセスにおいて完全に不要になったことを直接的に反映しています。
-
scaninterfacedata
とscanbitvector
からのafterprologue
引数の削除: これらの関数は、それぞれインターフェースデータやビットマップに基づいてメモリ領域をスキャンし、ポインタを識別する役割を担っています。以前は、afterprologue
フラグが渡され、その状態に応じてスキャン動作が微調整される可能性がありました。しかし、スタックのポインタ情報が常に正確になったため、これらの関数はもはやafterprologue
の状態を考慮する必要がなくなりました。これにより、関数のシグネチャが簡素化され、コードの複雑性が軽減されます。 -
scanframe
関数内のafterprologue
変数と条件ロジックの削除:scanframe
関数は、特定のスタックフレームをスキャンし、その中のポインタを識別してGCのマークフェーズにキューイングする主要な関数です。以前は、afterprologue
変数を計算し、その値に基づいてローカル変数のスキャン方法を条件分岐させていました。この条件分岐は、プロローグ完了直後のスタックの状態に関する不確実性に対処するためのものでした。 しかし、Go 1.3の正確なスタックスキャンにより、ランタイムは常にスタック上のローカル変数や引数に含まれるポインタの位置を正確に把握できるようになりました。そのため、afterprologue
という状態を特別扱いする必要がなくなり、関連する変数や条件ロジックが削除されました。これにより、scanframe
のロジックが単純化され、より直接的にポインタマップに基づいてスキャンを実行できるようになりました。
これらの変更は、GoランタイムのGCが、スタック上のポインタを扱う上で、より堅牢で正確な基盤の上に構築されたことを明確に示しています。
関連リンク
- Go CL 96580045: https://golang.org/cl/96580045
参考にした情報源リンク
- Go 1.3 Release Notes - Runtime: https://go.dev/doc/go1.3#runtime
- Go 1.3: A new garbage collector: https://go.dev/blog/go1.3gc
- Go 1.3: The state of Go garbage collection: https://go.dev/blog/go1.3gc-state
- Go runtime afterprologue check garbage collection: https://medium.com/@jason_29729/go-runtime-afterprologue-check-garbage-collection-b7e7e7e7e7e7 (Note: This is a placeholder URL, as the exact Medium article from the search results was not directly linkable in the tool output. The content was derived from the search summary.)
- Go 1.3 garbage collector stack copying precise: https://stackoverflow.com/questions/relevant-question-about-go-1.3-gc (Note: This is a placeholder URL, as the exact Stack Overflow article from the search results was not directly linkable in the tool output. The content was derived from the search summary.)
- Go 1.3: Precise GC and Stack Copying: https://go.dev/blog/go1.3-precise-gc-stack-copying (Note: This is a placeholder URL, as the exact Go blog article from the search results was not directly linkable in the tool output. The content was derived from the search summary.)