[インデックス 11122] ファイルの概要
このコミットは、Goコンパイラのガベージコレクション(gc)におけるエスケープ解析の誤検出(false positives)を修正するものです。具体的には、構造体(struct)内の非ポインタ(scalar)フィールドがエスケープする際に、構造体全体がエスケープすると誤って判断される問題を解決します。
コミット
gc: avoid false positives when using scalar struct fields.
The escape analysis code does not make a distinction between scalar and pointers fields in structs. Non-pointer fields that escape should not make the whole struct escape.
R=lvd, rsc CC=golang-dev, remy https://golang.org/cl/5489128
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/94ff311d1b91d0bfe2dc53d20d06e81af5a3c46f
元コミット内容
commit 94ff311d1b91d0bfe2dc53d20d06e81af5a3c46f
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date: Thu Jan 12 12:08:40 2012 +0100
gc: avoid false positives when using scalar struct fields.
The escape analysis code does not make a distinction between
scalar and pointers fields in structs. Non-pointer fields
that escape should not make the whole struct escape.
R=lvd, rsc
CC=golang-dev, remy
https://golang.org/cl/5489128
---
src/cmd/gc/esc.c | 6 +++++-\n test/escape2.go | 42 ++++++++++++++++++++++++++++++++++++------
2 files changed, 41 insertions(+), 7 deletions(-)
diff --git a/src/cmd/gc/esc.c b/src/cmd/gc/esc.c
index 037067be7f..43986c6af2 100644
--- a/src/cmd/gc/esc.c
+++ b/src/cmd/gc/esc.c
@@ -469,10 +469,14 @@ escassign(Node *dst, Node *src)\n \t\tescflows(dst, src);\n \t\tbreak;\n \n+\tcase ODOT:\n+\t\t// A non-pointer escaping from a struct does not concern us.\n+\t\tif(src->type && !haspointers(src->type))\n+\t\t\tbreak;\n+\t\t// fallthrough\n \tcase OCONV:\n \tcase OCONVIFACE:\n \tcase OCONVNOP:\n-\tcase ODOT:\n \tcase ODOTMETH:\t// treat recv.meth as a value with recv in it, only happens in ODEFER and OPROC\n \t\t\t// iface.method already leaks iface in esccall, no need to put in extra ODOTINTER edge here\n \tcase ODOTTYPE:\ndiff --git a/test/escape2.go b/test/escape2.go
index c2cbefbe61..73b2a7e589 100644
--- a/test/escape2.go
+++ b/test/escape2.go
@@ -126,10 +126,36 @@ func (b *Bar) NoLeak() int { // ERROR \"b does not escape\"\n \treturn *(b.ii)\n }\n \n+func (b *Bar) Leak() *int { // ERROR \"leaking param: b\"\n+\treturn &b.i // ERROR \"&b.i escapes to heap\"\n+}\n+\n func (b *Bar) AlsoNoLeak() *int { // ERROR \"b does not escape\"\n \treturn b.ii\n }\n \n+func (b Bar) AlsoLeak() *int { // ERROR \"leaking param: b\"\n+\treturn b.ii\n+}\n+\n+func (b Bar) LeaksToo() *int { // ERROR \"leaking param: b\"\n+\tv := 0\t// ERROR \"moved to heap: v\"\n+\tb.ii = &v // ERROR \"&v escapes\"\n+\treturn b.ii\n+}\n+\n+func (b *Bar) LeaksABit() *int { // ERROR \"b does not escape\"\n+\tv := 0\t// ERROR \"moved to heap: v\"\n+\tb.ii = &v // ERROR \"&v escapes\"\n+\treturn b.ii\n+}\n+\n+func (b Bar) StillNoLeak() int { // ERROR \"b does not escape\"\n+\tv := 0\n+\tb.ii = &v // ERROR \"&v does not escape\"\n+\treturn b.i\n+}\n+\n func goLeak(b *Bar) { // ERROR \"leaking param: b\"\n \tgo b.NoLeak()\n }\n@@ -148,20 +174,24 @@ func (b *Bar2) NoLeak() int { // ERROR \"b does not escape\"\n }\n \n func (b *Bar2) Leak() []int { // ERROR \"leaking param: b\"\n-\treturn b.i[:] // ERROR \"b.i escapes to heap\"\n+\treturn b.i[:] // ERROR \"b.i escapes to heap\"\n }\n \n func (b *Bar2) AlsoNoLeak() []int { // ERROR \"b does not escape\"\n \treturn b.ii[0:1]\n }\n \n+func (b Bar2) AgainNoLeak() [12]int { // ERROR \"b does not escape\"\n+\treturn b.i\n+}\n+\n func (b *Bar2) LeakSelf() { // ERROR \"leaking param: b\"\n-\tb.ii = b.i[0:4] // ERROR \"b.i escapes to heap\"\n+\tb.ii = b.i[0:4] // ERROR \"b.i escapes to heap\"\n }\n \n func (b *Bar2) LeakSelf2() { // ERROR \"leaking param: b\"\n \tvar buf []int\n-\tbuf = b.i[0:] // ERROR \"b.i escapes to heap\"\n+\tbuf = b.i[0:] // ERROR \"b.i escapes to heap\"\n \tb.ii = buf\n }\n \n@@ -1018,7 +1048,7 @@ func foo122() {\n \n \tgoto L1\n L1:\n-\ti = new(int)\t// ERROR \"does not escape\"\n+\ti = new(int) // ERROR \"does not escape\"\n \t_ = i\n }\n \n@@ -1027,8 +1057,8 @@ func foo123() {\n \tvar i *int\n \n L1:\n-\ti = new(int) // ERROR \"escapes\"\n+\ti = new(int) // ERROR \"escapes\"\n \n \tgoto L1\n \t_ = i\n-}\n\\ No newline at end of file\n+}\n```
## 変更の背景
Go言語のコンパイラには、プログラムの実行効率を向上させるための「エスケープ解析(Escape Analysis)」という重要な最適化機能が組み込まれています。エスケープ解析は、変数がヒープ(heap)に割り当てられるべきか、それともスタック(stack)に割り当てられるべきかを決定します。スタック割り当てはヒープ割り当てよりも高速であるため、可能な限りスタック割り当てが選択されます。
このコミットが修正しようとしている問題は、エスケープ解析が構造体(struct)のフィールドを扱う際に発生する誤検出(false positive)です。具体的には、構造体の中にポインタではない「スカラー(scalar)」型のフィールド(例: `int`, `string`, `bool`など)が含まれている場合、そのスカラーフィールドが何らかの理由でエスケープ(つまり、関数のスコープ外で参照され続ける可能性があると判断される)と、エスケープ解析が誤って構造体全体がヒープに割り当てられるべきだと判断してしまうことがありました。
本来、ポインタではないスカラーフィールドがエスケープしたとしても、そのフィールド自体がヒープに移動する必要があるだけで、そのフィールドを含む構造体全体がヒープに移動する必要はありません。この誤った判断は、不必要なヒープ割り当てを引き起こし、ガベージコレクションの負荷を増加させ、結果としてプログラムのパフォーマンスを低下させる可能性がありました。
このコミットは、このような「スカラー構造体フィールドの使用時に発生する誤検出」を回避し、エスケープ解析の精度を向上させることを目的としています。
## 前提知識の解説
### 1. エスケープ解析 (Escape Analysis)
Goコンパイラの重要な最適化の一つです。変数が関数のスコープを「エスケープ」するかどうかを分析します。
* **スタック割り当て (Stack Allocation):** 変数が関数の実行中にのみ存在し、関数が終了すると破棄される場合、その変数はスタックに割り当てられます。スタックは高速で、ガベージコレクションの対象外です。
* **ヒープ割り当て (Heap Allocation):** 変数が関数のスコープを超えても参照され続ける可能性がある場合(例: グローバル変数に代入される、関数の戻り値として返されるなど)、その変数はヒープに割り当てられます。ヒープはガベージコレクションの対象であり、割り当てと解放にオーバーヘッドが発生します。
エスケープ解析の目的は、可能な限り多くの変数をスタックに割り当てることで、ガベージコレクションの頻度を減らし、プログラムの実行速度を向上させることです。
### 2. 構造体 (Struct)
Goにおける複合データ型の一つで、異なる型のフィールド(メンバー)をまとめることができます。例えば、`type Person struct { Name string; Age int }` のように定義されます。
### 3. スカラーフィールドとポインタフィールド
* **スカラーフィールド (Scalar Field):** `int`, `float64`, `bool`, `string` などの基本的な値型(非ポインタ型)のフィールドです。これらのフィールドは、それ自体がメモリ上の値を直接保持します。
* **ポインタフィールド (Pointer Field):** `*int`, `*MyStruct` のように、他のメモリ位置へのアドレスを保持するフィールドです。ポインタフィールドがエスケープすると、そのポインタが指す先のデータもエスケープする可能性があります。
### 4. `ODOT` ノード
Goコンパイラの内部表現(AST: Abstract Syntax Tree)において、`ODOT` は構造体のフィールドアクセスを表すノードです。例えば、`myStruct.field` のような操作は `ODOT` ノードとして表現されます。
### 5. `haspointers` 関数
Goコンパイラの内部関数で、与えられた型がポインタを含むかどうかを判定します。構造体の場合、その構造体のフィールドにポインタ型が含まれているかどうかをチェックします。
## 技術的詳細
このコミットの技術的な核心は、Goコンパイラのガベージコレクション(gc)部分、特にエスケープ解析を担当する `src/cmd/gc/esc.c` ファイル内の `escassign` 関数にあります。
`escassign` 関数は、代入操作(`dst = src`)におけるエスケープのフローを分析します。これまでのエスケープ解析コードは、構造体のフィールドアクセス(`ODOT` ノード)を処理する際に、そのフィールドがスカラー型であるかポインタ型であるかを区別していませんでした。そのため、スカラーフィールドがエスケープすると判断された場合でも、ポインタフィールドと同様に、そのフィールドを含む構造体全体がヒープにエスケープすると誤って推論していました。
この修正では、`ODOT` ケースの処理に新しい条件が追加されました。
```c
case ODOT:
// A non-pointer escaping from a struct does not concern us.
if(src->type && !haspointers(src->type))
break;
// fallthrough
このコードブロックは、以下のロジックを導入しています。
case ODOT:
: 現在処理しているノードが構造体のフィールドアクセス(ODOT
)であることを示します。if(src->type && !haspointers(src->type))
:src->type
: 代入元のソース(src
)の型が存在することを確認します。!haspointers(src->type)
:src
の型がポインタを含まない(つまり、スカラー型であるか、ポインタを含まない複合型である)ことを確認します。
break;
: もしsrc
がポインタを含まない型であり、かつODOT
ノードである場合、このエスケープフローは構造体全体のエスケープには影響しないため、ここで処理を中断します。これにより、不必要なヒープ割り当ての推論が回避されます。// fallthrough
: 上記の条件に合致しない場合(つまり、src
がポインタを含む型であるか、ODOT
以外のノードである場合)、処理は次のcase
へとフォールスルーし、既存のエスケープ解析ロジックが適用されます。
この変更により、エスケープ解析は構造体のスカラーフィールドがエスケープしても、構造体全体をヒープに移動させるという誤った判断を下さなくなります。これにより、より正確なエスケープ解析が可能となり、Goプログラムのメモリ使用効率とパフォーマンスが向上します。
また、test/escape2.go
ファイルには、この修正が正しく機能することを確認するための新しいテストケースが多数追加されています。これらのテストケースは、様々なシナリオでスカラーフィールドのエスケープが構造体全体のエスケープを引き起こさないことを検証しています。
コアとなるコードの変更箇所
src/cmd/gc/esc.c
ファイルの escassign
関数内の ODOT
ケースに以下のコードが追加されました。
--- a/src/cmd/gc/esc.c
+++ b/src/cmd/gc/esc.c
@@ -469,10 +469,14 @@ escassign(Node *dst, Node *src)\n \t\tescflows(dst, src);\n \t\tbreak;\n \n+\tcase ODOT:\n+\t\t// A non-pointer escaping from a struct does not concern us.\n+\t\tif(src->type && !haspointers(src->type))\n+\t\t\tbreak;\n+\t\t// fallthrough\n \tcase OCONV:\
\tcase OCONVIFACE:\
\tcase OCONVNOP:\
-\tcase ODOT:\
\tcase ODOTMETH:\t// treat recv.meth as a value with recv in it, only happens in ODEFER and OPROC\
\t\t\t// iface.method already leaks iface in esccall, no need to put in extra ODOTINTER edge here\
\tcase ODOTTYPE:\
また、test/escape2.go
には、この変更によってエスケープ解析の挙動が正しくなることを検証するための多数のテストケースが追加されています。
コアとなるコードの解説
追加されたコードは、escassign
関数内で構造体のフィールドアクセス(ODOT
)を処理する際の挙動を変更します。
以前は、ODOT
は他の型変換ノード(OCONV
, OCONVIFACE
など)と同じように扱われ、フォールスルーして一般的なエスケープ解析ロジックが適用されていました。しかし、この修正により、ODOT
ノードが特別に処理されるようになりました。
新しい ODOT
ケース内の if(src->type && !haspointers(src->type))
条件は、代入元のソース(src
)が構造体のフィールドであり、かつそのフィールドの型がポインタを含まない(つまり、スカラー型である)場合に真となります。
この条件が真の場合、break;
ステートメントが実行され、現在のエスケープフローの分析が中断されます。これは、「ポインタではないフィールドが構造体からエスケープしても、それは我々(エスケープ解析)の関心事ではない」というコメントが示す通り、構造体全体をヒープにエスケープさせる必要がないことを意味します。
この変更により、Goコンパイラは、構造体のスカラーフィールドがエスケープしたとしても、その事実だけで構造体全体をヒープに割り当てるという誤った判断を回避できるようになります。これにより、エスケープ解析の精度が向上し、不必要なヒープ割り当てが削減され、Goプログラムの実行時パフォーマンスが改善されます。
関連リンク
- Go CL 5489128: https://golang.org/cl/5489128
参考にした情報源リンク
- Go言語のエスケープ解析に関する公式ドキュメントやブログ記事 (一般的な情報源として)
- Goコンパイラのソースコード (
src/cmd/gc/esc.c
) - Go言語の構造体とポインタに関する基本情報