Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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

このコードブロックは、以下のロジックを導入しています。

  1. case ODOT:: 現在処理しているノードが構造体のフィールドアクセス(ODOT)であることを示します。
  2. if(src->type && !haspointers(src->type)):
    • src->type: 代入元のソース(src)の型が存在することを確認します。
    • !haspointers(src->type): src の型がポインタを含まない(つまり、スカラー型であるか、ポインタを含まない複合型である)ことを確認します。
  3. break;: もし src がポインタを含まない型であり、かつ ODOT ノードである場合、このエスケープフローは構造体全体のエスケープには影響しないため、ここで処理を中断します。これにより、不必要なヒープ割り当ての推論が回避されます。
  4. // 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言語のエスケープ解析に関する公式ドキュメントやブログ記事 (一般的な情報源として)
  • Goコンパイラのソースコード (src/cmd/gc/esc.c)
  • Go言語の構造体とポインタに関する基本情報