[インデックス 14342] ファイルの概要
このコミットは、Goコンパイラのcmd/gc
におけるエスケープ解析のバグ修正に関するものです。具体的には、エスケープ解析が変数のエスケープタイプを誤って判断し、重要なEscScope
のエスケープが無視される問題に対処しています。この修正により、コンパイラは変数のライフタイムをより正確に判断し、適切なメモリ割り当て(スタックまたはヒープ)を行うことができるようになります。
変更されたファイルは以下の通りです。
src/cmd/gc/esc.c
: エスケープ解析の主要なロジックが含まれるファイル。src/cmd/gc/subr.c
: コンパイラのユーティリティ関数が含まれるファイル。test/escape5.go
: エスケープ解析のバグを再現し、修正を検証するためのテストファイル。
コミット
commit 71282131a1ab0291834f41e606ebab6c5f0ca438
Author: Russ Cox <rsc@golang.org>
Date: Wed Nov 7 15:15:21 2012 -0500
cmd/gc: fix escape analysis bug
The code assumed that the only choices were EscNone, EscScope, and EscHeap,
so that it makes sense to set EscScope only if the current setting is EscNone.
Now that we have the many variants of EscReturn, this logic is false, and it was
causing important EscScopes to be ignored in favor of EscReturn.
Fixes #4360.
R=ken2
CC=golang-dev, lvd
https://golang.org/cl/6816103
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/71282131a1ab0291834f41e606ebab6c5f0ca438
元コミット内容
commit 71282131a1ab0291834f41e606ebab6c5f0ca438
Author: Russ Cox <rsc@golang.org>
Date: Wed Nov 7 15:15:21 2012 -0500
cmd/gc: fix escape analysis bug
The code assumed that the only choices were EscNone, EscScope, and EscHeap,
so that it makes sense to set EscScope only if the current setting is EscNone.
Now that we have the many variants of EscReturn, this logic is false, and it was
causing important EscScopes to be ignored in favor of EscReturn.
Fixes #4360.
R=ken2
CC=golang-dev, lvd
https://golang.org/cl/6816103
変更の背景
このコミットは、Goコンパイラのエスケープ解析におけるバグを修正するために行われました。エスケープ解析は、Goプログラムのパフォーマンスを最適化する上で非常に重要な役割を担っています。変数がスタックに割り当てられるか、ヒープに割り当てられるかを決定することで、ガベージコレクションの負荷を軽減し、メモリ効率を向上させます。
問題は、エスケープ解析のコードが、変数のエスケープタイプとしてEscNone
、EscScope
、EscHeap
の3つしか存在しないという誤った前提に基づいて動作していた点にありました。特に、EscScope
を設定する際に、現在のエスケープ設定がEscNone
である場合にのみ設定するというロジックが問題でした。
しかし、Goのエスケープ解析には、関数からの戻り値としてエスケープする様々なケースを示すEscReturn
のバリアントが導入されていました。この新しいEscReturn
の存在により、上記の間違った前提が崩れ、結果として重要なEscScope
のエスケープがEscReturn
によって上書きされ、無視されてしまうというバグが発生していました。これにより、本来スタックに割り当てられるべき変数が不必要にヒープに割り当てられ、パフォーマンスの低下やメモリ使用量の増加につながる可能性がありました。
このバグはIssue #4360として報告されており、このコミットはその問題を解決することを目的としています。
前提知識の解説
Goのエスケープ解析 (Escape Analysis)
Goのエスケープ解析は、コンパイラが行う最適化の一つで、変数がスタックに割り当てられるべきか、それともヒープに割り当てられるべきかを決定します。
- スタック割り当て: 関数内で宣言された変数が、その関数の実行中にのみ生存し、関数が終了すると自動的に解放される場合、スタックに割り当てられます。スタック割り当ては高速で、ガベージコレクタの負担を軽減します。
- ヒープ割り当て: 変数のライフタイムが、それを宣言した関数のスコープを超えて続く場合(例: ポインタが関数から返される、グローバル変数に格納されるなど)、ヒープに割り当てられます。ヒープに割り当てられたメモリはガベージコレクタによって管理されます。
エスケープ解析の目的は、可能な限り多くの変数をスタックに割り当てることで、プログラムの効率を高めることです。
エスケープタイプ
Goコンパイラは、変数のエスケープ挙動を分類するためにいくつかのエスケープタイプを使用します。
EscNone
(Does Not Escape): 変数のライフタイムが、それが宣言された関数内に完全に閉じていることを示します。この場合、変数はスタックに安全に割り当てられます。EscHeap
(Escapes to Heap): 変数のライフタイムが、それを宣言した関数のスコープを超えて続く必要があることを示します。この場合、変数はヒープに割り当てられます。これは、ポインタが関数から返される、グローバル変数に格納される、ゴルーチンに渡される、コンパイル時にサイズが決定できない、またはスタックに収まらないほど大きい場合などに発生します。EscReturn
(Escapes on Return):EscHeap
の特定のケースです。変数のアドレスまたはそのポインタが関数から返されるために、ヒープにエスケープする必要があることを意味します。関数が終了した後もアクセス可能であるためには、ヒープに割り当てられる必要があります。EscScope
: これは、EscNone
、EscHeap
、EscReturn
のような直接的なエスケープタイプとは少し異なります。エスケープ解析は変数の「動的なスコープ」(実際の生存期間)を決定します。変数の動的なスコープがその字句的なスコープ(宣言された場所)を超えて延長される場合、それは「エスケープ」し、ヒープに割り当てられます。したがって、EscScope
は、変数の使用方法がその直接の関数のスタックフレームよりも長く存続する必要がある状況を暗黙的に指し、結果としてヒープ割り当てにつながります。このコミットの文脈では、EscScope
は特定の変数が特定のスコープ内でエスケープするべきであることを示す内部的なフラグとして機能していると考えられます。
このコミットのバグは、EscReturn
のバリアントが導入されたにもかかわらず、エスケープ解析のロジックが古い前提(EscNone
, EscScope
, EscHeap
のみ)に固執していたために発生しました。
技術的詳細
このバグは、src/cmd/gc/esc.c
内のescwalk
関数におけるエスケープ解析のロジックに起因していました。具体的には、src->esc = EscScope;
という行が、src->esc == EscNone
の場合にのみ実行されるという条件が問題でした。
元のコードでは、エスケープタイプがEscNone
である場合にのみ、EscScope
を設定していました。これは、EscNone
、EscScope
、EscHeap
の3つのエスケープタイプしか存在しないという前提に基づいています。この前提では、EscNone
が最も弱いエスケープであり、EscScope
はそれよりも強いエスケープであるため、EscNone
からEscScope
への昇格は理にかなっていました。
しかし、EscReturn
の様々なバリアントが導入されたことで、この前提は誤りとなりました。EscReturn
は、変数が関数から戻り値としてエスケープすることを示すものであり、EscNone
よりも強いエスケープです。問題は、ある変数が既にEscReturn
としてマークされている場合でも、その変数がEscScope
としてマークされるべき状況が存在するにもかかわらず、src->esc == EscNone
の条件のためにEscScope
が設定されなかったことです。
これにより、本来EscScope
として扱われるべき変数が、EscReturn
のまま、あるいは他のエスケープタイプとして誤って扱われ、結果として不適切なヒープ割り当てが発生していました。
修正は以下の2点です。
src/cmd/gc/esc.c
のescwalk
関数内で、EscReturn
が設定された後にgoto recurse;
が二重に存在していたのを修正し、論理的なフローを改善しました。src/cmd/gc/esc.c
のescwalk
関数内で、src->esc == EscNone
という条件をsrc->esc != EscHeap
に変更しました。これにより、変数が既にEscHeap
としてマークされていない限り、EscScope
を設定できるようになります。EscHeap
は最も強いエスケープタイプであり、一度EscHeap
と判断された変数は、それ以上弱いエスケープタイプ(EscScope
を含む)に降格されることはありません。この変更により、EscReturn
のバリアントが設定されている場合でも、必要に応じてEscScope
が正しく適用されるようになります。
また、src/cmd/gc/subr.c
には、デバッグフラグdebug['m']
が設定されている場合にエラーをフラッシュする行が追加されています。これは、エスケープ解析のデバッグ出力がより確実に行われるようにするための補助的な変更と考えられます。
コアとなるコードの変更箇所
diff --git a/src/cmd/gc/esc.c b/src/cmd/gc/esc.c
index f789386bc9..f067cc5305 100644
--- a/src/cmd/gc/esc.c
+++ b/src/cmd/gc/esc.c
@@ -1005,8 +1005,8 @@ escwalk(EscState *e, int level, Node *dst, Node *src)
if((src->esc&EscMask) != EscReturn)
src->esc = EscReturn;
src->esc |= 1<<((dst->vargen-1) + EscBits);
+\t\t\tgoto recurse;
}
-\t\t\tgoto recurse;
}
@@ -1014,7 +1014,7 @@ escwalk(EscState *e, int level, Node *dst, Node *src)
switch(src->op) {
case ONAME:
-\t\tif(src->class == PPARAM && leaks && src->esc == EscNone) {
+\t\tif(src->class == PPARAM && leaks && src->esc != EscHeap) {
src->esc = EscScope;
if(debug['m'])
warnl(src->lineno, "leaking param: %hN", src);
diff --git a/src/cmd/gc/subr.c b/src/cmd/gc/subr.c
index 142921153d..71417bb0a0 100644
--- a/src/cmd/gc/subr.c
+++ b/src/cmd/gc/subr.c
@@ -219,6 +219,8 @@ warnl(int line, char *fmt, ...)
va_start(arg, fmt);
adderr(line, fmt, arg);
va_end(arg);
+\tif(debug['m'])
+\t\tflusherrors();
}
void
diff --git a/test/escape5.go b/test/escape5.go
index 22c324f902..6b327fe9e3 100644
--- a/test/escape5.go
+++ b/test/escape5.go
@@ -117,3 +117,28 @@ func leakrecursive2(p, q *int) (*int, *int) { // ERROR "leaking param: p" "leaki
return p, q
}
+
+var global interface{}
+
+type T1 struct {
+ X *int
+}
+
+type T2 struct {
+ Y *T1
+}
+
+func f8(p *T1) (k T2) { // ERROR "leaking param: p to result k" "leaking param: p"
+ if p == nil {
+ k = T2{}
+ return
+ }
+
+ global = p // should make p leak always
+ return T2{p}
+}
+
+func f9() {
+ var j T1 // ERROR "moved to heap: j"
+ f8(&j) // ERROR "&j escapes to heap"
+}
コアとなるコードの解説
src/cmd/gc/esc.c
の変更点
-
goto recurse;
の重複修正:@@ -1005,8 +1005,8 @@ escwalk(EscState *e, int level, Node *dst, Node *src) if((src->esc&EscMask) != EscReturn) src->esc = EscReturn; src->esc |= 1<<((dst->vargen-1) + EscBits); +\t\t\tgoto recurse; } -\t\t\tgoto recurse; }
この変更は、
EscReturn
が設定されるブロック内でgoto recurse;
が二重に存在していたのを修正しています。元のコードでは、if
ブロックの内部と外部の両方にgoto recurse;
があり、冗長でした。この修正により、if
ブロックの内部にのみgoto recurse;
が残され、コードの可読性と論理的なフローが改善されました。これは直接的なバグ修正というよりは、コードのクリーンアップと正確性の向上を目的としています。 -
EscScope
設定条件の変更:@@ -1014,7 +1014,7 @@ escwalk(EscState *e, int level, Node *dst, Node *src) switch(src->op) { case ONAME: -\t\tif(src->class == PPARAM && leaks && src->esc == EscNone) { +\t\tif(src->class == PPARAM && leaks && src->esc != EscHeap) { src->esc = EscScope; if(debug['m']) warnl(src->lineno, "leaking param: %hN", src);
これがこのコミットの主要なバグ修正箇所です。
- 変更前:
src->esc == EscNone
- これは、「変数のエスケープタイプが現在
EscNone
である場合にのみ、EscScope
を設定する」という意味でした。 - 前述の通り、
EscReturn
のバリアントが導入されたことで、この条件は不十分になりました。変数が既にEscReturn
としてマークされている場合でも、EscScope
としてマークされるべき状況が存在するにもかかわらず、この条件のためにEscScope
が設定されませんでした。
- これは、「変数のエスケープタイプが現在
- 変更後:
src->esc != EscHeap
- これは、「変数のエスケープタイプが現在
EscHeap
ではない場合に、EscScope
を設定する」という意味です。 EscHeap
は最も強いエスケープタイプであり、一度EscHeap
と判断された変数は、それ以上弱いエスケープタイプに降格されることはありません。- この変更により、変数が
EscNone
であるか、あるいはEscReturn
のいずれかのバリアントである場合でも、EscHeap
でない限りEscScope
が正しく適用されるようになります。これにより、エスケープ解析が変数のライフタイムをより正確に判断し、適切なメモリ割り当てを行うことができるようになります。
- これは、「変数のエスケープタイプが現在
- 変更前:
src/cmd/gc/subr.c
の変更点
diff --git a/src/cmd/gc/subr.c b/src/cmd/gc/subr.c
index 142921153d..71417bb0a0 100644
--- a/src/cmd/gc/subr.c
+++ b/src/cmd/gc/subr.c
@@ -219,6 +219,8 @@ warnl(int line, char *fmt, ...)
va_start(arg, fmt);
adderr(line, fmt, arg);
va_end(arg);
+\tif(debug['m'])
+\t\tflusherrors();
}
warnl
関数(警告メッセージを出力する関数)に、debug['m']
フラグが設定されている場合にflusherrors()
を呼び出す行が追加されました。
debug['m']
は、Goコンパイラのエスケープ解析のデバッグ出力を有効にするためのフラグです(go build -gcflags="-m"
)。flusherrors()
は、おそらくバッファリングされたエラーメッセージを強制的にフラッシュする関数です。- この変更は、エスケープ解析のデバッグ出力がより確実に行われるようにするための補助的なものであり、バグ修正の直接的な原因ではありませんが、デバッグ時の情報提供を改善します。
test/escape5.go
の変更点
このファイルには、新しいテストケースf8
とf9
が追加されています。これらのテストケースは、EscScope
が正しく適用されるべき状況で、以前のバグによって誤ったエスケープ解析が行われていたケースを再現し、修正が正しく機能することを確認するために使用されます。
f8
関数は、p *T1
というパラメータを受け取り、T2
型の戻り値を返します。global = p
という行により、p
がグローバル変数にエスケープし、ヒープに割り当てられるべきであることを示します。また、戻り値k
にp
が含まれるため、p
が戻り値としてエスケープする(EscReturn
)と同時に、そのスコープも考慮されるべき状況を作り出しています。f9
関数は、f8
を呼び出し、ローカル変数j
のアドレスを渡しています。このテストは、j
がヒープに移動し、&j
がヒープにエスケープすることを検証しています。
これらのテストケースは、特にEscReturn
とEscScope
の相互作用が正しく処理されることを保証するために重要です。
関連リンク
- Go Issue #4360: https://code.google.com/p/go/issues/detail?id=4360 (現在はGitHubに移行済み)
- Go CL 6816103: https://golang.org/cl/6816103
参考にした情報源リンク
- Go Escape Analysis: https://dev.to/ (Vertex AI Search経由)
- Understanding Go Escape Analysis: https://medium.com/ (Vertex AI Search経由)
- Go Escape Analysis Explained: https://syntactic-sugar.dev/ (Vertex AI Search経由)
- Go: Escape Analysis: https://dzone.com/ (Vertex AI Search経由)
- Go escape analysis for beginners: https://stackoverflow.com/ (Vertex AI Search経由)
- Go: Escape Analysis: https://topofmind.dev/ (Vertex AI Search経由)