[インデックス 18500] ファイルの概要
このコミットは、Goコンパイラ(cmd/gc)におけるエスケープ解析のバグ修正に関するものです。具体的には、forループの初期化ステートメントが、エスケープ解析によって誤ってループ内部のスコープとして扱われていた問題を解決します。これにより、本来エスケープしないはずの変数が誤ってヒープに割り当てられる可能性がありました。
コミット
e0a55a6c9826f3b0548a2d78be82931ad73ac218
Author: Daniel Morsing daniel.morsing@gmail.com
Date: Thu Feb 13 19:04:43 2014 +0000
cmd/gc: for loop init statement misanalyzed by escape analysis
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/e0a55a6c9826f3b0548a2d78be82931ad73ac218
元コミット内容
cmd/gc: for loop init statement misanalyzed by escape analysis
Logically, the init statement is in the enclosing scopes loopdepth, not inside the for loop.
Fixes #7313.
LGTM=rsc
R=golang-codereviews, gobot, rsc
CC=golang-codereviews
https://golang.org/cl/62430043
変更の背景
Go言語のコンパイラには、プログラムのパフォーマンスを最適化するための「エスケープ解析(Escape Analysis)」という重要な機能があります。エスケープ解析は、変数がヒープに割り当てられるべきか、それともスタックに割り当てられるべきかを決定します。スタック割り当てはヒープ割り当てよりも高速であり、ガベージコレクションの負荷を軽減するため、可能な限りスタック割り当てが推奨されます。
このコミットが修正する問題は、Goのforループの初期化ステートメント(例: for p := &l; ... の p := &l 部分)が、エスケープ解析の過程で誤ってループ本体のスコープ(loopdepth)に属すると判断されていたことです。論理的には、初期化ステートメントはループが始まる前の、そのループを囲むスコープの一部として実行されます。しかし、コンパイラのエスケープ解析器がこの論理的なスコープと物理的なコード構造を混同し、初期化ステートメントで宣言された変数が、ループ内部でエスケープすると誤って判断される可能性がありました。
具体的には、Issue #7313で報告された問題は、forループの初期化部分で宣言されたポインタが、実際にはループの外側のスコープで有効であるにもかかわらず、ループ内部のloopdepthでエスケープすると誤って判断され、不必要にヒープに割り当てられてしまうというものでした。これは、プログラムの実行効率に悪影響を与える可能性があります。
前提知識の解説
Goコンパイラ (cmd/gc)
cmd/gcは、Go言語の公式コンパイラです。Goのソースコードを機械語に変換する役割を担っています。コンパイルの過程で、構文解析、型チェック、最適化(エスケープ解析を含む)、コード生成など、様々なフェーズを実行します。
エスケープ解析 (Escape Analysis)
エスケープ解析は、コンパイラ最適化の一種で、変数のメモリ割り当て場所(スタックかヒープか)を決定します。
- スタック割り当て: 関数内で宣言され、その関数の実行が終了すると不要になる変数は、通常スタックに割り当てられます。スタックは高速で、メモリの解放も自動的に行われます。
- ヒープ割り当て: 変数が関数のスコープを「エスケープ」し、関数の終了後も参照され続ける可能性がある場合(例: ポインタが関数の外に返される、グローバル変数に代入されるなど)、その変数はヒープに割り当てられます。ヒープはより柔軟なメモリ管理を提供しますが、割り当てと解放(ガベージコレクション)のオーバーヘッドがあります。
エスケープ解析の目的は、不必要なヒープ割り当てを減らし、ガベージコレクションの頻度と時間を削減することで、プログラムのパフォーマンスを向上させることです。
コンパイラのAST (Abstract Syntax Tree) と Node 構造体
Goコンパイラは、ソースコードを解析する際に、その構造を抽象構文木(AST)として表現します。ASTの各要素はNode構造体で表されます。Nodeは、変数宣言、関数呼び出し、ループ、条件分岐など、プログラムのあらゆる構文要素を表現します。
forループを表すNodeには、以下のようなフィールドがあります。
ninit: ループの初期化ステートメント(例:for i := 0; ...のi := 0)。ntest: ループの継続条件(例:...; i < 10; ...のi < 10)。nincr: ループの各イテレーション後に実行されるステートメント(例:...; i++のi++)。nbody: ループの本体。op: ノードの種類を示すオペレーションコード(例:OFORはforループ、ORANGEはfor rangeループ)。
loopdepth
エスケープ解析のコンテキストにおいて、loopdepthは現在のコードがどれだけ深いループの中にいるかを示すカウンターのようなものです。この値は、変数の寿命とエスケープの可能性を判断する際に使用されます。ループの深さが変わると、変数のスコープや寿命に関するエスケープ解析のルールも変わる可能性があります。
技術的詳細
このコミットの核心は、forループの初期化ステートメント(ninit)が、エスケープ解析のloopdepthの扱いで誤解されていた点にあります。
Goのforループの構文は以下のようになります。
for init; condition; post {
body
}
ここで、initステートメントはループが始まる前に一度だけ実行されます。論理的には、このinitステートメントで宣言された変数のスコープは、forループ自体を囲むスコープと同じloopdepthに属します。しかし、エスケープ解析のコードでは、OFORやORANGEノードを処理する際に、まずloopdepthをインクリメントしてから、ninitを含むすべてのサブノードに対してエスケープ解析を実行していました。
これにより、ninitで宣言された変数が、あたかもループ本体の内部で宣言されたかのように、誤ったloopdepthで解析されていました。結果として、本来スタックに割り当てられるべき変数が、ループ内部でエスケープすると誤って判断され、ヒープに割り当てられてしまうというバグが発生していました。
この修正は、ninitステートメントのエスケープ解析を、loopdepthをインクリメントする前に行うことで、この論理的な誤りを正しています。つまり、ninitはループを囲むスコープのloopdepthで解析され、その後のcondition、post、bodyはインクリメントされたloopdepthで解析されるようになります。
コアとなるコードの変更箇所
変更は主にsrc/cmd/gc/esc.cファイルと、その動作を検証するためのtest/escape2.goファイルにあります。
src/cmd/gc/esc.c
--- a/src/cmd/gc/esc.c
+++ b/src/cmd/gc/esc.c
@@ -423,6 +423,9 @@ esc(EscState *e, Node *n)\
lno = setlineno(n);\
+ // ninit logically runs at a different loopdepth than the rest of the for loop.
+ esclist(e, n->ninit);\
+
if(n->op == OFOR || n->op == ORANGE)\
e->loopdepth++;
@@ -430,7 +433,6 @@ esc(EscState *e, Node *n)\
esc(e, n->right);\
esc(e, n->ntest);\
esc(e, n->nincr);\
- esclist(e, n->ninit);\
esclist(e, n->nbody);\
esclist(e, n->nelse);\
esclist(e, n->list);\
test/escape2.go
--- a/test/escape2.go
+++ b/test/escape2.go
@@ -1357,3 +1357,35 @@ func foo144() {\
//go:noescape
func foo144b(*int)\
+\
+// issue 7313: for loop init should not be treated as "in loop"\
+\
+type List struct {\
+\tNext *List\
+}\
+\
+func foo145(l List) { // ERROR "l does not escape"\
+\tvar p *List\
+\tfor p = &l; p.Next != nil; p = p.Next { // ERROR "&l does not escape"\
+\t}\
+}\
+\
+func foo146(l List) { // ERROR "l does not escape"\
+\tvar p *List\
+\tp = &l // ERROR "&l does not escape"\
+\tfor ; p.Next != nil; p = p.Next {\
+\t}\
+}\
+\
+func foo147(l List) { // ERROR "l does not escape"\
+\tvar p *List\
+\tp = &l // ERROR "&l does not escape"\
+\tfor p.Next != nil {\
+\t\tp = p.Next\
+\t}\
+}\
+\
+func foo148(l List) { // ERROR " l does not escape"\
+\tfor p := &l; p.Next != nil; p = p.Next { // ERROR "&l does not escape"\
+\t}\
+}\
コアとなるコードの解説
src/cmd/gc/esc.c の変更
esc関数は、Goコンパイラのエスケープ解析の主要な部分です。この関数はASTのノードを再帰的にトラバースし、各ノード内の変数のエスケープ挙動を分析します。
変更前:
if(n->op == OFOR || n->op == ORANGE)
e->loopdepth++;
esc(e, n->right);
esc(e, n->ntest);
esc(e, n->nincr);
esclist(e, n->ninit); // ここでninitが処理されていた
esclist(e, n->nbody);
esclist(e, n->nelse);
esclist(e, n->list);
変更後:
// ninit logically runs at a different loopdepth than the rest of the for loop.
esclist(e, n->ninit); // ここに移動
if(n->op == OFOR || n->op == ORANGE)
e->loopdepth++;
esc(e, n->right);
esc(e, n->ntest);
esc(e, n->nincr);
- // esclist(e, n->ninit); // 削除
esclist(e, n->nbody);
esclist(e, n->nelse);
esclist(e, n->list);
この変更のポイントは、esclist(e, n->ninit); の呼び出しが、e->loopdepth++ の行よりも前に移動したことです。
esclist関数は、ノードのリストに対してエスケープ解析を適用します。- 変更前は、
forループ(OFORまたはORANGE)のloopdepthがインクリメントされた後にninitが解析されていました。これは、ninitがループ本体と同じloopdepthにあると誤解釈される原因となっていました。 - 変更後は、
ninitがloopdepthをインクリメントする前に解析されるため、ninitはループを囲むスコープのloopdepthで正しく評価されます。これにより、forループの初期化ステートメントで宣言された変数が、不必要にヒープにエスケープすると誤って判断されることがなくなります。
test/escape2.go の追加テストケース
test/escape2.goには、このバグが修正されたことを確認するための新しいテストケースが追加されています。
foo145からfoo148までの関数は、様々な形式のforループの初期化ステートメントでポインタを宣言し、それらがエスケープしないことを// ERRORコメントでアサートしています。
例えば、foo145は以下のようになっています。
func foo145(l List) { // ERROR "l does not escape"
var p *List
for p = &l; p.Next != nil; p = p.Next { // ERROR "&l does not escape"
}
}
このテストケースでは、&l(lのアドレス)がforループの初期化部分でpに代入されています。この&lは、関数foo145のスコープ内で完結しており、関数の外にエスケープすることはありません。したがって、エスケープ解析は&lがヒープに割り当てられる必要がないと判断し、スタックに割り当てられるべきです。修正前は、この&lが誤ってエスケープすると判断される可能性がありましたが、修正後は正しく「does not escape」と報告されるようになります。
これらのテストケースは、forループの初期化ステートメントにおけるエスケープ解析の挙動が、論理的なスコープに従って正しく行われるようになったことを検証しています。
関連リンク
- Go Issue #7313: cmd/gc: for loop init statement misanalyzed by escape analysis (現在はGitHubに移行済み)
- Go CL 62430043: https://golang.org/cl/62430043
参考にした情報源リンク
- Go言語の公式ドキュメント (エスケープ解析に関する一般的な情報)
- Goコンパイラのソースコード (
src/cmd/gc/esc.cの周辺コード) - Go言語のASTに関する情報 (Goの
go/astパッケージのドキュメントなど) - Go言語の
forループの構文に関する情報 - Issue #7313の議論内容 (GitHubのIssueトラッカー)
- Go CL 62430043のレビューコメント (Gerritのレビューシステム)