[インデックス 18511] ファイルの概要
このコミットは、Goコンパイラのcmd/gc
パッケージにおけるエスケープ解析の挙動を調整するものです。具体的には、変数のアドレスが取られる(&x
)際のエスケープ解析において、その変数が宣言されたループのネスト深度をより正確に考慮するように変更されています。これにより、一部の変数が不必要にヒープに割り当てられることを防ぎ、より効率的なコード生成を目指します。
コミット
commit e5d742fcadf9677a40336d6cecd3ff464a94730f
Author: Russ Cox <rsc@golang.org>
Date: Thu Feb 13 19:59:09 2014 -0500
cmd/gc: relax address-of escape analysis
Make the loop nesting depth of &x depend on where x is declared,
not on where the &x appears. The latter is only a conservative
estimate of the former. Being more careful can avoid some
variables escaping, and it is easier to reason about.
It would have avoided issue 7313, although that was still a bug
worth fixing.
Not much effect in the tree: one variable in the whole tree
is saved from a heap allocation (something in x509 parsing).
LGTM=daniel.morsing
R=daniel.morsing
CC=golang-codereviews
https://golang.org/cl/62380043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/e5d742fcadf9677a40336d6cecd3ff464a94730f
元コミット内容
cmd/gc: relax address-of escape analysis
&x
のループネスト深度を、&x
が出現する場所ではなく、x
が宣言された場所に依存させるように変更します。後者は前者の保守的な見積もりに過ぎません。より注意深くすることで、一部の変数がエスケープするのを回避でき、推論が容易になります。
これはissue 7313を回避できたはずですが、それはそれでも修正する価値のあるバグでした。
ツリー全体への影響は大きくありません。ツリー全体で1つの変数がヒープ割り当てから救われました(x509解析の何か)。
変更の背景
このコミットの背景には、Goコンパイラのエスケープ解析の精度向上という目的があります。エスケープ解析は、変数がスタックに割り当てられるべきか、それともヒープに割り当てられるべきかを決定する重要な最適化ステップです。変数がヒープに割り当てられると、ガベージコレクションの対象となり、パフォーマンスに影響を与える可能性があります。
従来のGoコンパイラのエスケープ解析では、&x
(変数のアドレスを取る操作)が行われた際の変数の「ループネスト深度」の計算が、&x
が出現する場所のループ深度に依存していました。しかし、これは必ずしも変数が実際に宣言された場所のループ深度を正確に反映しているわけではなく、より保守的な(つまり、ヒープへの割り当てを促しやすい)見積もりになっていました。
この保守的な見積もりにより、実際にはスタックに割り当てられるべき変数が誤ってヒープに割り当てられてしまうケースが発生していました。コミットメッセージで言及されている「issue 7313」も、このようなエスケープ解析の不正確さが原因で発生したバグの一つです。このバグは、特定の状況下で変数が不必要にヒープにエスケープし、予期せぬ動作を引き起こす可能性がありました。
このコミットは、&x
のループネスト深度の計算を、x
が宣言された場所のループ深度に依存させることで、より正確なエスケープ解析を実現し、不必要なヒープ割り当てを減らすことを目的としています。これにより、生成されるバイナリのパフォーマンスが向上し、ガベージコレクションの負荷が軽減されることが期待されます。
前提知識の解説
エスケープ解析 (Escape Analysis)
エスケープ解析は、コンパイラ最適化の一種で、プログラム内の変数がそのスコープを「エスケープ」するかどうかを決定します。ここでいう「エスケープ」とは、変数がその宣言された関数やブロックの実行が終了した後も参照され続ける可能性があることを意味します。
- スタック割り当て (Stack Allocation): 変数がそのスコープ内で完結し、関数呼び出しの終了とともに不要になる場合、その変数はスタックに割り当てられます。スタック割り当ては非常に高速で、ガベージコレクションのオーバーヘッドがありません。
- ヒープ割り当て (Heap Allocation): 変数がそのスコープを超えて参照される可能性がある場合(例: ポインタが関数から返される、グローバル変数に代入されるなど)、その変数はヒープに割り当てられます。ヒープ割り当てはスタック割り当てよりも遅く、ガベージコレクションの対象となるため、パフォーマンスに影響を与える可能性があります。
エスケープ解析の目的は、可能な限り多くの変数をスタックに割り当てることで、プログラムの実行効率を最大化することです。
ループネスト深度 (Loop Nesting Depth)
ループネスト深度とは、あるコードがどれだけ深いループの中に存在するかを示す指標です。例えば、関数直下のコードは深度0、1つのループの中は深度1、二重ループの最も内側のコードは深度2、といった具合です。エスケープ解析において、変数の寿命を判断する際に、その変数が宣言されたループの深度が考慮されることがあります。変数がより深いループ内で宣言され、そのアドレスがより浅いループや関数の外に「エスケープ」する場合、ヒープ割り当てが必要になる可能性が高まります。
cmd/gc
cmd/gc
は、Go言語の公式コンパイラの一部であり、Goソースコードを機械語に変換する役割を担っています。エスケープ解析は、このコンパイラの最適化フェーズの一部として実行されます。
Node
構造体とescloopdepth
Goコンパイラの内部では、ソースコードは抽象構文木(AST)として表現され、各要素はNode
構造体で表されます。escloopdepth
は、このNode
構造体に含まれるフィールドの一つで、エスケープ解析の過程で計算される、そのノード(変数など)が関連するループのネスト深度を示す値です。
OADDR
OADDR
は、GoコンパイラのASTにおけるオペレーションコードの一つで、&
演算子(アドレス取得演算子)を表します。例えば、&x
という式は、AST上ではOADDR
オペレーションと、そのオペランドである変数x
のノードとして表現されます。
PAUTO
, PPARAM
, PPARAMOUT
これらはGoコンパイラの内部で使われる変数のクラス(種類)を示す定数です。
PAUTO
: ローカル変数(自動変数)PPARAM
: 関数の引数PPARAMOUT
: 関数の戻り値(名前付き戻り値の場合など)
これらの変数は通常、スタックに割り当てられる候補となりますが、エスケープ解析の結果によってはヒープに割り当てられることもあります。
技術的詳細
このコミットの技術的な核心は、src/cmd/gc/esc.c
ファイル内のエスケープ解析ロジックの変更にあります。
変更前は、OADDR
(アドレス取得演算子)のノードに対して、そのノードが出現する現在のループ深度(e->loopdepth
)をescloopdepth
として設定していました。これは、&x
という操作が行われた時点でのループ深度を、x
の寿命の目安としていたことを意味します。しかし、x
自体がより浅いループや関数スコープで宣言されている場合、&x
の出現場所のループ深度は、x
の実際の寿命よりも深い値を示す可能性があり、結果としてx
が不必要にヒープにエスケープすると判断される原因となっていました。
このコミットでは、OADDR
の処理が変更され、&x
のescloopdepth
を、x
が宣言された場所のescloopdepth
に依存させるようになりました。具体的には、&x
のオペランドがONAME
(変数名)であり、その変数のクラスがPAUTO
(ローカル変数)、PPARAM
(引数)、PPARAMOUT
(戻り値)のいずれかである場合、&x
のescloopdepth
は、その変数x
自身のescloopdepth
がコピーされるようになりました。
これにより、&x
の操作が行われる場所が深いループ内であっても、x
自体が浅いスコープで宣言されていれば、x
のescloopdepth
は浅い値のままとなり、エスケープ解析がより正確な寿命を判断できるようになります。結果として、スタックに割り当てられるべき変数がヒープにエスケープするのを防ぎ、より効率的なコードが生成されます。
また、PPARAM
(関数の引数)の初期化時にもll->n->escloopdepth = 1;
が設定されるようになりました。これは、関数の引数が少なくとも関数スコープ(深度1)で有効であることを示し、エスケープ解析の初期段階での正確性を高めます。
コアとなるコードの変更箇所
src/cmd/gc/esc.c
--- a/src/cmd/gc/esc.c
+++ b/src/cmd/gc/esc.c
@@ -328,6 +328,7 @@ escfunc(EscState *e, Node *func)\
ll->n->escloopdepth = 0;\
break;\
case PPARAM:\
+ ll->n->escloopdepth = 1;
if(ll->n->type && !haspointers(ll->n->type))\
break;\
if(curfn->nbody == nil && !curfn->noescape)\
@@ -335,7 +336,6 @@ escfunc(EscState *e, Node *func)\
else\
ll->n->esc = EscNone; // prime for escflood later\
e->noesc = list(e->noesc, ll->n);\
- ll->n->escloopdepth = 1;
break;\
}\
}\
@@ -630,7 +630,6 @@ esc(EscState *e, Node *n)\
escassign(e, n, a);\
}\
// fallthrough\
-\tcase OADDR:\
case OMAKECHAN:\
case OMAKEMAP:\
case OMAKESLICE:\
@@ -639,6 +638,24 @@ esc(EscState *e, Node *n)\
n->esc = EscNone; // until proven otherwise\
e->noesc = list(e->noesc, n);\
break;\
+\
+\tcase OADDR:\
+\t\tn->esc = EscNone; // until proven otherwise;\
+\t\te->noesc = list(e->noesc, n);\
+\t\t// current loop depth is an upper bound on actual loop depth\
+\t\t// of addressed value.\
+\t\tn->escloopdepth = e->loopdepth;\
+\t\t// for &x, use loop depth of x.\
+\t\tif(n->left->op == ONAME) {\
+\t\t\tswitch(n->left->class) {\
+\t\t\tcase PAUTO:\
+\t\t\tcase PPARAM:\
+\t\t\tcase PPARAMOUT:\
+\t\t\t\tn->escloopdepth = n->left->escloopdepth;\
+\t\t\t\tbreak;\
+\t\t\t}\
+\t\t}\
+\t\tbreak;\
}\
\
lineno = lno;\
test/escape2.go
--- a/test/escape2.go
+++ b/test/escape2.go
@@ -1389,3 +1389,13 @@ func foo148(l List) { // ERROR \" l does not escape\"\
for p := &l; p.Next != nil; p = p.Next { // ERROR \"&l does not escape\"\
}\
}\
+\
+// related: address of variable should have depth of variable, not of loop\
+\
+func foo149(l List) { // ERROR \" l does not escape\"\
+ var p *List\
+ for {\
+ for p = &l; p.Next != nil; p = p.Next { // ERROR \"&l does not escape\"\
+ }\
+ }\
+}\
コアとなるコードの解説
src/cmd/gc/esc.c
の変更点
-
escfunc
関数内のPPARAM
処理の変更:- 変更前は、
PPARAM
(関数の引数)の場合、ll->n->escloopdepth = 1;
の行がif(curfn->nbody == nil && !curfn->noescape)
のブロックの後にありました。 - 変更後は、この行がブロックの前に移動し、常に
escloopdepth
が1
に初期化されるようになりました。これにより、関数の引数が常に少なくとも関数スコープ(深度1)で有効であるという初期設定が保証されます。
- 変更前は、
-
esc
関数内のOADDR
処理の分離と詳細化:- 変更前は、
OADDR
(アドレス取得)はOMAKECHAN
などと同じfallthrough
ブロック内にあり、単純にn->esc = EscNone;
とe->noesc = list(e->noesc, n);
が適用されていました。escloopdepth
の設定は明示的に行われていませんでした。 - 変更後は、
OADDR
が独立したcase
として扱われるようになりました。- まず、
n->esc = EscNone;
とe->noesc = list(e->noesc, n);
は引き続き適用されます。 - 次に、
n->escloopdepth = e->loopdepth;
が設定されます。これは、&x
という操作が行われた時点での現在のループ深度を、&x
ノード自体のescloopdepth
の初期値として設定します。これは、アドレスが取られた値の実際のループ深度の上限となります。 - 最も重要な変更点:
if(n->left->op == ONAME)
のブロックが追加されました。これは、&
演算子のオペランドが変数名(ONAME
)である場合に適用されます。- さらに、その変数のクラスが
PAUTO
(ローカル変数)、PPARAM
(引数)、PPARAMOUT
(戻り値)のいずれかである場合、n->escloopdepth = n->left->escloopdepth;
が実行されます。 - これは、
&x
のescloopdepth
を、x
が宣言された場所のescloopdepth
に上書きすることを意味します。これにより、&x
の出現場所のループ深度ではなく、x
自体の寿命をより正確に反映したescloopdepth
が設定され、エスケープ解析の精度が向上します。
- さらに、その変数のクラスが
- まず、
- 変更前は、
test/escape2.go
の変更点
func foo149(l List)
という新しいテストケースが追加されました。- このテストケースは、
&l
(リストl
のアドレス)が深いネストされたループ内で使用されているにもかかわらず、l
自体は関数の引数として宣言されているため、ヒープにエスケープしないことを検証しています。 // ERROR " l does not escape"
と// ERROR "&l does not escape"
というコメントは、コンパイラがこの変数をスタックに割り当てると判断することを期待していることを示しています。このテストは、今回のエスケープ解析の変更が正しく機能していることを確認するためのものです。
これらの変更により、Goコンパイラは変数のアドレスが取られる際の寿命をより正確に判断できるようになり、不必要なヒープ割り当てを減らすことで、生成されるコードの効率を向上させます。
関連リンク
- Go issue 7313: https://github.com/golang/go/issues/7313 (コミットメッセージで言及されている関連バグ)
- Go Code Review 62380043: https://golang.org/cl/62380043 (このコミットのコードレビューページ)
参考にした情報源リンク
- Go言語のエスケープ解析に関する一般的な情報源 (例: Go公式ドキュメント、Goブログ、技術記事など)
- Goコンパイラのソースコード (
src/cmd/gc/esc.c
の周辺コード) - Go言語のASTとオペレーションコードに関する情報