[インデックス 15862] ファイルの概要
このコミットは、Goコンパイラ(cmd/gc
)におけるメソッド値のエスケープ解析のバグを修正するものです。具体的には、メソッド値がどのようにメモリに割り当てられ、どこで使われるかをコンパイラが正しく判断できていなかった問題に対処しています。この修正により、不必要なヒープ割り当てが削減され、生成されるバイナリの効率が向上します。
コミット
commit 38e9b0773d486beb1d91ce018586a888bbb20e45
Author: Russ Cox <rsc@golang.org>
Date: Wed Mar 20 23:53:27 2013 -0400
cmd/gc: fix escape analysis of method values
R=ken2
CC=golang-dev
https://golang.org/cl/7518050
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/38e9b0773d486beb1d91ce018586a888bbb20e45
元コミット内容
cmd/gc: fix escape analysis of method values
R=ken2
CC=golang-dev
https://golang.org/cl/7518050
変更の背景
Go言語では、メソッドを値として変数に代入することができます(例: f = t.M
)。これを「メソッド値(method value)」と呼びます。メソッド値は、レシーバ(t
)とメソッド(M
)の両方をカプセル化した関数のようなものです。
エスケープ解析は、コンパイラが変数がヒープに割り当てられるべきか、それともスタックに割り当てられるべきかを決定するための最適化手法です。スタック割り当てはヒープ割り当てよりも高速であるため、コンパイラは可能な限りスタック割り当てを選択しようとします。
このコミット以前は、Goコンパイラのエスケープ解析がメソッド値の扱いを誤っていました。特に、メソッド値が作成された際に、そのレシーバが不必要にヒープにエスケープしてしまう問題がありました。これは、メソッド値が「部分適用された関数呼び出し」としてコンパイラ内部で表現されるOCALLPART
というノードタイプに関連していました。コンパイラがOCALLPART
ノードを正しくエスケープ解析の対象として認識していなかったため、メソッド値のレシーバが実際にはスタックに留まるべきであるにもかかわらず、ヒープに割り当てられてしまうことがありました。これにより、プログラムの実行効率が低下する可能性がありました。
このバグは、特にsync.Once
のような同期プリミティブと組み合わせてメソッド値を使用する際に顕在化し、sync.Once.Do
に渡されたメソッド値のレシーバが不必要にヒープにエスケープするという形で現れていました。
前提知識の解説
Goコンパイラ (cmd/gc
)
cmd/gc
は、Go言語の公式コンパイラです。Goのソースコードを機械語に変換する役割を担っています。コンパイルプロセスには、字句解析、構文解析、型チェック、中間表現の生成、最適化、コード生成などが含まれます。
エスケープ解析 (Escape Analysis)
エスケープ解析は、コンパイラの最適化技術の一つで、変数がその宣言されたスコープを「エスケープ」するかどうかを判断します。
- スタック割り当て: 変数がその関数スコープ内で完結し、関数が終了すると不要になる場合、その変数はスタックに割り当てられます。スタックは高速で、ガベージコレクションの対象外です。
- ヒープ割り当て: 変数がその関数スコープを超えて参照され続ける可能性がある場合(例: グローバル変数に代入される、ポインタが返されるなど)、その変数はヒープに割り当てられます。ヒープはガベージコレクションの対象となり、スタックよりもアクセスが遅い傾向があります。 エスケープ解析の目的は、可能な限り多くの変数をスタックに割り当てることで、メモリ効率と実行速度を向上させることです。
メソッド値 (Method Values)
Go言語では、構造体のメソッドを関数として変数に代入することができます。 例:
type MyStruct struct {
value int
}
func (m *MyStruct) GetValue() int {
return m.value
}
func main() {
s := &MyStruct{value: 10}
f := s.GetValue // fはメソッド値
fmt.Println(f()) // 10が出力される
}
このf
がメソッド値です。f
はs
という特定のレシーバとGetValue
メソッドを組み合わせたものです。コンパイラ内部では、このようなメソッド値は特殊な構造として扱われます。
OCALLPART
OCALLPART
は、Goコンパイラの内部で使われるノードタイプの一つです。これは「部分適用された関数呼び出し(partial function application)」を表します。メソッド値は、レシーバが部分適用された関数呼び出しとして表現されるため、このOCALLPART
ノードが使用されます。
技術的詳細
このコミットの技術的詳細は、Goコンパイラの内部構造、特に中間表現(IR)とエスケープ解析のパスに深く関わっています。
-
OCALLPART
ノードの導入と修正:src/cmd/gc/closure.c
のtypecheckpartialcall
関数は、メソッド値が作成される際にOCALLPART
ノードを生成します。以前は、このノードのtype
フィールドがfn->right->type
(メソッドの型)に設定されていましたが、修正後はfn->nname->type
(生成されたクロージャの型)に設定されるようになりました。これは、OCALLPART
が単なるメソッドではなく、レシーバがバインドされた新しい関数(クロージャ)として扱われるべきであることを反映しています。- また、
fn->right = sym;
という行が追加され、OCALLPART
ノードのright
フィールドにレシーバのシンボルが明示的に設定されるようになりました。これにより、エスケープ解析がレシーバの情報をより正確に追跡できるようになります。
-
エスケープ解析 (
esc.c
) におけるOCALLPART
の認識:src/cmd/gc/esc.c
はエスケープ解析の主要なロジックを含んでいます。esc
関数(エスケープ解析のメインループ)のswitch
文にOCALLPART
ケースが追加されました。これにより、OCALLPART
ノードがエスケープ解析の対象として明示的に処理されるようになります。n->esc = EscNone;
:初期状態ではエスケープしないと仮定します。e->noesc = list(e->noesc, n);
:エスケープしない可能性のあるノードのリストに追加します。n->escloopdepth = e->loopdepth;
:現在のループ深度を記録します。escassign(e, &e->theSink, n->left);
:OCALLPART
の左側(通常はレシーバ)がメモリに書き込まれる可能性があるため、そのエスケープ状態を追跡します。
escassign
関数(代入のエスケープ解析)のswitch
文にもOCALLPART
が追加されました。これにより、OCALLPART
が代入の右辺にある場合に、そのエスケープ状態が適切に伝播されるようになります。escwalk
関数(エスケープフローの伝播)のswitch
文にもOCALLPART
が追加されました。これにより、OCALLPART
がヒープにエスケープする場合、その内部の要素(レシーバなど)もヒープにエスケープするようになります。
-
デバッグ出力 (
fmt.c
) の改善:src/cmd/gc/fmt.c
は、コンパイラの内部ノードを文字列としてフォーマットする関数を含んでいます。opprec
配列にOCALLPART
の優先順位が追加されました。exprfmt
関数にOCALLPART
ケースが追加され、OCALLPART
ノードがデバッグ出力で適切に表示されるようになりました。これにより、コンパイラの開発者が内部状態をデバッグしやすくなります。
-
テストケースの追加:
test/escape2.go
にfoo141
とfoo142
という新しいテスト関数が追加されました。これらは、メソッド値のエスケープ解析が正しく機能するかどうかを検証します。foo141
では、t.M
がローカル変数f
に代入される場合、t
がヒープにエスケープしないことを期待します。foo142
では、t.M
がグローバル変数gf
に代入される場合、t
がヒープにエスケープすることを期待します。
test/fixedbugs/bug474.go
という新しいテストファイルが追加されました。これは、sync.Once.Do
にメソッド値を渡した場合にレシーバが不必要にヒープにエスケープするバグを再現し、修正後にそれが解決されていることを確認するためのものです。
これらの変更により、コンパイラはメソッド値の内部表現をより正確に理解し、それらのレシーバがいつヒープにエスケープする必要があるかを適切に判断できるようになりました。結果として、不必要なヒープ割り当てが減少し、生成されるGoプログラムのパフォーマンスが向上します。
コアとなるコードの変更箇所
src/cmd/gc/closure.c
--- a/src/cmd/gc/closure.c
+++ b/src/cmd/gc/closure.c
@@ -270,8 +270,9 @@ typecheckpartialcall(Node *fn, Node *sym)\n \n // Create top-level function.\n fn->nname = makepartialcall(fn, fn->type, sym);\n+ fn->right = sym;\n fn->op = OCALLPART;\
- fn->type = fn->right->type;\n+ fn->type = fn->nname->type;\
}\n \n static Node*\
src/cmd/gc/esc.c
--- a/src/cmd/gc/esc.c
+++ b/src/cmd/gc/esc.c
@@ -596,6 +596,14 @@ esc(EscState *e, Node *n)\
// Contents make it to memory, lose track.\
escassign(e, &e->theSink, n->left);\
break;\
+\ \n+\tcase OCALLPART:\
+\t\tn->esc = EscNone; // until proven otherwise\
+\t\te->noesc = list(e->noesc, n);\
+\t\tn->escloopdepth = e->loopdepth;\
+\t\t// Contents make it to memory, lose track.\
+\t\tescassign(e, &e->theSink, n->left);\
+\t\tbreak;\
\n case OMAPLIT:\
\t\tn->esc = EscNone; // until proven otherwise\
@@ -667,6 +675,7 @@ escassign(EscState *e, Node *dst, Node *src)\
case OCONVNOP:\
case OMAPLIT:\
case OSTRUCTLIT:\
+\tcase OCALLPART:\
break;\
\n case ONAME:\
@@ -713,6 +722,7 @@ escassign(EscState *e, Node *dst, Node *src)\
case OMAKESLICE:\
case ONEW:\
case OCLOSURE:\
+\tcase OCALLPART:\
escflows(e, dst, src);\
break;\
\n@@ -1073,6 +1083,7 @@ escwalk(EscState *e, int level, Node *dst, Node *src)\
case OMAPLIT:\
case ONEW:\
case OCLOSURE:\
+\tcase OCALLPART:\
if(leaks) {\
src->esc = EscHeap;\
if(debug['m'])\
src/cmd/gc/fmt.c
--- a/src/cmd/gc/fmt.c
+++ b/src/cmd/gc/fmt.c
@@ -1022,6 +1022,7 @@ static int opprec[] = {\
[ODOTTYPE] = 8,\
[ODOT] = 8,\
[OXDOT] = 8,\
+\t[OCALLPART] = 8,\
\n [OPLUS] = 7,\
[ONOT] = 7,\
@@ -1269,9 +1270,10 @@ exprfmt(Fmt *f, Node *n, int prec)\
case ODOTPTR:\
case ODOTINTER:\
case ODOTMETH:\
+\tcase OCALLPART:\
exprfmt(f, n->left, nprec);\
if(n->right == N || n->right->sym == S)\
-\t\t\tfmtstrcpy(f, ".<nil>");\
+\t\t\treturn fmtstrcpy(f, ".<nil>");\
return fmtprint(f, ".%hhS", n->right->sym);\
\n case ODOTTYPE:\
test/escape2.go
--- a/test/escape2.go
+++ b/test/escape2.go
@@ -1303,3 +1303,25 @@ func G() {\
var buf4 [10]byte // ERROR "moved to heap: buf4"\
F4(buf4[:]) // ERROR "buf4 escapes to heap"\
}\
+\
+type Tm struct {\
+\tx int\
+}\
+\
+func (t *Tm) M() { // ERROR "t does not escape"\
+}\
+\
+func foo141() {\
+\tvar f func()\
+\t\
+\tt := new(Tm) // ERROR "escapes to heap"\
+\tf = t.M // ERROR "t.M does not escape"\
+\t_ = f\
+}\
+\
+var gf func()\
+\
+func foo142() {\
+\tt := new(Tm) // ERROR "escapes to heap"\
+\tgf = t.M // ERROR "t.M escapes to heap"\
+}\
test/fixedbugs/bug474.go
--- /dev/null
+++ b/test/fixedbugs/bug474.go
@@ -0,0 +1,29 @@
+// run
+\
+// Copyright 2013 The Go Authors. All rights reserved.\
+// Use of this source code is governed by a BSD-style\
+// license that can be found in the LICENSE file.\
+\
+// Bug in method values: escape analysis was off.\
+\
+package main
+\
+import "sync"\
+\
+var called = false\
+\
+type T struct {\
+\tonce sync.Once\
+}\
+\
+func (t *T) M() {\
+\tcalled = true\
+}\
+\
+func main() {\
+\tvar t T\
+\tt.once.Do(t.M)\
+\tif !called {\
+\t\tpanic("not called")\
+\t}\
+}\
コアとなるコードの解説
src/cmd/gc/closure.c
の変更
fn->right = sym;
の追加:OCALLPART
ノードのright
フィールドに、メソッド値のレシーバ(sym
)が明示的に設定されるようになりました。これにより、エスケープ解析がレシーバの情報を正確に参照できるようになります。fn->type = fn->nname->type;
への変更:OCALLPART
ノードの型が、生成されるクロージャの型(fn->nname->type
)に設定されるようになりました。これは、メソッド値が単なるメソッドの型ではなく、レシーバがバインドされた新しい関数としての型を持つべきであることを示しています。
src/cmd/gc/esc.c
の変更
OCALLPART
ケースの追加:esc
関数内でOCALLPART
ノードが明示的に処理されるようになりました。これにより、メソッド値がエスケープ解析の対象として認識され、そのレシーバのエスケープ状態が追跡されます。n->esc = EscNone;
は、デフォルトでエスケープしないと仮定し、後続の解析で必要に応じてエスケープ状態が変更されることを意味します。escassign(e, &e->theSink, n->left);
は、OCALLPART
の左側(通常はレシーバ)がメモリに書き込まれる可能性があるため、そのエスケープ状態を追跡します。
escassign
およびescwalk
関数へのOCALLPART
の追加: これらの関数は、変数の代入やデータフローを通じてエスケープ状態を伝播させる役割を担っています。OCALLPART
がこれらの関数で考慮されるようになったことで、メソッド値のレシーバがヒープにエスケープすべきかどうかをより正確に判断できるようになりました。
src/cmd/gc/fmt.c
の変更
OCALLPART
の追加: コンパイラの内部ノードを文字列として表現する際に、OCALLPART
が正しくフォーマットされるようになりました。これは、デバッグやコンパイラの内部状態の可視性向上に役立ちます。
テストファイルの追加と変更
test/escape2.go
の追加テスト:foo141
とfoo142
は、メソッド値がローカル変数に代入される場合とグローバル変数に代入される場合で、レシーバのエスケープ挙動が異なることを検証します。これにより、エスケープ解析がこれらのシナリオを正しく処理していることを確認します。test/fixedbugs/bug474.go
の追加: この新しいテストは、sync.Once.Do
にメソッド値を渡した際に発生していた、レシーバの不必要なヒープエスケープのバグを具体的に再現し、修正が適用された後にこの問題が解決されていることを確認します。
これらの変更は、Goコンパイラがメソッド値のセマンティクスをより深く理解し、エスケープ解析の精度を向上させることで、より効率的なコードを生成するための重要なステップです。
関連リンク
- Go言語のエスケープ解析に関する公式ドキュメントやブログ記事(当時のものがあれば)
- Goコンパイラのソースコードリポジトリ
- Go言語のメソッド値に関する仕様
参考にした情報源リンク
- Go issue 474: cmd/gc: method values escape analysis is off (このコミットが修正したバグのIssue)
- Go CL 7518050: cmd/gc: fix escape analysis of method values (このコミットのGerritレビューページ)
- Go Escape Analysis (Go言語のメモリ管理とエスケープ解析に関する一般的な情報)
- Go Method Values (Go言語のメソッド値に関する一般的な情報)
- Goコンパイラのソースコード (
src/cmd/gc/
) - Go言語の仕様書
- 一般的なコンパイラ最適化、特にエスケープ解析に関する情報# [インデックス 15862] ファイルの概要
このコミットは、Goコンパイラ(cmd/gc
)におけるメソッド値のエスケープ解析のバグを修正するものです。具体的には、メソッド値がどのようにメモリに割り当てられ、どこで使われるかをコンパイラが正しく判断できていなかった問題に対処しています。この修正により、不必要なヒープ割り当てが削減され、生成されるバイナリの効率が向上します。
コミット
commit 38e9b0773d486beb1d91ce018586a888bbb20e45
Author: Russ Cox <rsc@golang.org>
Date: Wed Mar 20 23:53:27 2013 -0400
cmd/gc: fix escape analysis of method values
R=ken2
CC=golang-dev
https://golang.org/cl/7518050
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/38e9b0773d486beb1d91ce018586a888bbb20e45
元コミット内容
cmd/gc: fix escape analysis of method values
R=ken2
CC=golang-dev
https://golang.org/cl/7518050
変更の背景
Go言語では、メソッドを値として変数に代入することができます(例: f = t.M
)。これを「メソッド値(method value)」と呼びます。メソッド値は、レシーバ(t
)とメソッド(M
)の両方をカプセル化した関数のようなものです。
エスケープ解析は、コンパイラが変数がヒープに割り当てられるべきか、それともスタックに割り当てられるべきかを決定するための最適化手法です。スタック割り当てはヒープ割り当てよりも高速であるため、コンパイラは可能な限りスタック割り当てを選択しようとします。
このコミット以前は、Goコンパイラのエスケープ解析がメソッド値の扱いを誤っていました。特に、メソッド値が作成された際に、そのレシーバが不必要にヒープにエスケープしてしまう問題がありました。これは、メソッド値が「部分適用された関数呼び出し」としてコンパイラ内部で表現されるOCALLPART
というノードタイプに関連していました。コンパイラがOCALLPART
ノードを正しくエスケープ解析の対象として認識していなかったため、メソッド値のレシーバが実際にはスタックに留まるべきであるにもかかわらず、ヒープに割り当てられてしまうことがありました。これにより、プログラムの実行効率が低下する可能性がありました。
このバグは、特にsync.Once
のような同期プリミティブと組み合わせてメソッド値を使用する際に顕在化し、sync.Once.Do
に渡されたメソッド値のレシーバが不必要にヒープにエスケープするという形で現れていました。
前提知識の解説
Goコンパイラ (cmd/gc
)
cmd/gc
は、Go言語の公式コンパイラです。Goのソースコードを機械語に変換する役割を担っています。コンパイルプロセスには、字句解析、構文解析、型チェック、中間表現の生成、最適化、コード生成などが含まれます。
エスケープ解析 (Escape Analysis)
エスケープ解析は、コンパイラの最適化技術の一つで、変数がその宣言されたスコープを「エスケープ」するかどうかを判断します。
- スタック割り当て: 変数がその関数スコープ内で完結し、関数が終了すると不要になる場合、その変数はスタックに割り当てられます。スタックは高速で、ガベージコレクションの対象外です。
- ヒープ割り当て: 変数がその関数スコープを超えて参照され続ける可能性がある場合(例: グローバル変数に代入される、ポインタが返されるなど)、その変数はヒープに割り当てられます。ヒープはガベージコレクションの対象となり、スタックよりもアクセスが遅い傾向があります。 エスケープ解析の目的は、可能な限り多くの変数をスタックに割り当てることで、メモリ効率と実行速度を向上させることです。
メソッド値 (Method Values)
Go言語では、構造体のメソッドを関数として変数に代入することができます。 例:
type MyStruct struct {
value int
}
func (m *MyStruct) GetValue() int {
return m.value
}
func main() {
s := &MyStruct{value: 10}
f := s.GetValue // fはメソッド値
fmt.Println(f()) // 10が出力される
}
このf
がメソッド値です。f
はs
という特定のレシーバとGetValue
メソッドを組み合わせたものです。コンパイラ内部では、このようなメソッド値は特殊な構造として扱われます。
OCALLPART
OCALLPART
は、Goコンパイラの内部で使われるノードタイプの一つです。これは「部分適用された関数呼び出し(partial function application)」を表します。メソッド値は、レシーバが部分適用された関数呼び出しとして表現されるため、このOCALLPART
ノードが使用されます。
技術的詳細
このコミットの技術的詳細は、Goコンパイラの内部構造、特に中間表現(IR)とエスケープ解析のパスに深く関わっています。
-
OCALLPART
ノードの導入と修正:src/cmd/gc/closure.c
のtypecheckpartialcall
関数は、メソッド値が作成される際にOCALLPART
ノードを生成します。以前は、このノードのtype
フィールドがfn->right->type
(メソッドの型)に設定されていましたが、修正後はfn->nname->type
(生成されたクロージャの型)に設定されるようになりました。これは、OCALLPART
が単なるメソッドではなく、レシーバがバインドされた新しい関数(クロージャ)として扱われるべきであることを反映しています。- また、
fn->right = sym;
という行が追加され、OCALLPART
ノードのright
フィールドにレシーバのシンボルが明示的に設定されるようになりました。これにより、エスケープ解析がレシーバの情報をより正確に追跡できるようになります。
-
エスケープ解析 (
esc.c
) におけるOCALLPART
の認識:src/cmd/gc/esc.c
はエスケープ解析の主要なロジックを含んでいます。esc
関数(エスケープ解析のメインループ)のswitch
文にOCALLPART
ケースが追加されました。これにより、OCALLPART
ノードがエスケープ解析の対象として明示的に処理されるようになります。n->esc = EscNone;
:初期状態ではエスケープしないと仮定します。e->noesc = list(e->noesc, n);
:エスケープしない可能性のあるノードのリストに追加します。n->escloopdepth = e->loopdepth;
:現在のループ深度を記録します。escassign(e, &e->theSink, n->left);
:OCALLPART
の左側(通常はレシーバ)がメモリに書き込まれる可能性があるため、そのエスケープ状態を追跡します。
escassign
関数(代入のエスケープ解析)のswitch
文にもOCALLPART
が追加されました。これにより、OCALLPART
が代入の右辺にある場合に、そのエスケープ状態が適切に伝播されるようになります。escwalk
関数(エスケープフローの伝播)のswitch
文にもOCALLPART
が追加されました。これにより、OCALLPART
がヒープにエスケープする場合、その内部の要素(レシーバなど)もヒープにエスケープするようになります。
-
デバッグ出力 (
fmt.c
) の改善:src/cmd/gc/fmt.c
は、コンパイラの内部ノードを文字列としてフォーマットする関数を含んでいます。opprec
配列にOCALLPART
の優先順位が追加されました。exprfmt
関数にOCALLPART
ケースが追加され、OCALLPART
ノードがデバッグ出力で適切に表示されるようになりました。これにより、コンパイラの開発者が内部状態をデバッグしやすくなります。
-
テストケースの追加:
test/escape2.go
にfoo141
とfoo142
という新しいテスト関数が追加されました。これらは、メソッド値のエスケープ解析が正しく機能するかどうかを検証します。foo141
では、t.M
がローカル変数f
に代入される場合、t
がヒープにエスケープしないことを期待します。foo142
では、t.M
がグローバル変数gf
に代入される場合、t
がヒープにエスケープすることを期待します。
test/fixedbugs/bug474.go
という新しいテストファイルが追加されました。これは、sync.Once.Do
にメソッド値を渡した場合にレシーバが不必要にヒープにエスケープするバグを再現し、修正後にそれが解決されていることを確認するためのものです。
これらの変更により、コンパイラはメソッド値の内部表現をより正確に理解し、それらのレシーバがいつヒープにエスケープする必要があるかを適切に判断できるようになりました。結果として、不必要なヒープ割り当てが減少し、生成されるGoプログラムのパフォーマンスが向上します。
コアとなるコードの変更箇所
src/cmd/gc/closure.c
--- a/src/cmd/gc/closure.c
+++ b/src/cmd/gc/closure.c
@@ -270,8 +270,9 @@ typecheckpartialcall(Node *fn, Node *sym)\
\n // Create top-level function.\
fn->nname = makepartialcall(fn, fn->type, sym);\
+ fn->right = sym;\
fn->op = OCALLPART;\
- fn->type = fn->right->type;\
+ fn->type = fn->nname->type;\
}\n \n static Node*\
src/cmd/gc/esc.c
--- a/src/cmd/gc/esc.c
+++ b/src/cmd/gc/esc.c
@@ -596,6 +596,14 @@ esc(EscState *e, Node *n)\
// Contents make it to memory, lose track.\
escassign(e, &e->theSink, n->left);\
break;\
+\ \n+\tcase OCALLPART:\
+\t\tn->esc = EscNone; // until proven otherwise\
+\t\te->noesc = list(e->noesc, n);\
+\t\tn->escloopdepth = e->loopdepth;\
+\t\t// Contents make it to memory, lose track.\
+\t\tescassign(e, &e->theSink, n->left);\
+\t\tbreak;\
\n case OMAPLIT:\
\t\tn->esc = EscNone; // until proven otherwise\
@@ -667,6 +675,7 @@ escassign(EscState *e, Node *dst, Node *src)\
case OCONVNOP:\
case OMAPLIT:\
case OSTRUCTLIT:\
+\tcase OCALLPART:\
break;\
\n case ONAME:\
@@ -713,6 +722,7 @@ escassign(e, Node *dst, Node *src)\
case OMAKESLICE:\
case ONEW:\
case OCLOSURE:\
+\tcase OCALLPART:\
escflows(e, dst, src);\
break;\
\n@@ -1073,6 +1083,7 @@ escwalk(EscState *e, int level, Node *dst, Node *src)\
case OMAPLIT:\
case ONEW:\
case OCLOSURE:\
+\tcase OCALLPART:\
if(leaks) {\
src->esc = EscHeap;\
if(debug['m'])\
src/cmd/gc/fmt.c
--- a/src/cmd/gc/fmt.c
+++ b/src/cmd/gc/fmt.c
@@ -1022,6 +1022,7 @@ static int opprec[] = {\
[ODOTTYPE] = 8,\
[ODOT] = 8,\
[OXDOT] = 8,\
+\t[OCALLPART] = 8,\
\n [OPLUS] = 7,\
[ONOT] = 7,\
@@ -1269,9 +1270,10 @@ exprfmt(Fmt *f, Node *n, int prec)\
case ODOTPTR:\
case ODOTINTER:\
case ODOTMETH:\
+\tcase OCALLPART:\
exprfmt(f, n->left, nprec);\
if(n->right == N || n->right->sym == S)\
-\t\t\tfmtstrcpy(f, ".<nil>");\
+\t\t\treturn fmtstrcpy(f, ".<nil>");\
return fmtprint(f, ".%hhS", n->right->sym);\
\n case ODOTTYPE:\
test/escape2.go
--- a/test/escape2.go
+++ b/test/escape2.go
@@ -1303,3 +1303,25 @@ func G() {\
var buf4 [10]byte // ERROR "moved to heap: buf4"\
F4(buf4[:]) // ERROR "buf4 escapes to heap"\
}\
+\
+type Tm struct {\
+\tx int\
+}\
+\
+func (t *Tm) M() { // ERROR "t does not escape"\
+}\
+\
+func foo141() {\
+\tvar f func()\
+\t\
+\tt := new(Tm) // ERROR "escapes to heap"\
+\tf = t.M // ERROR "t.M does not escape"\
+\t_ = f\
+}\
+\
+var gf func()\
+\
+func foo142() {\
+\tt := new(Tm) // ERROR "escapes to heap"\
+\tgf = t.M // ERROR "t.M escapes to heap"\
+}\
test/fixedbugs/bug474.go
--- /dev/null
+++ b/test/fixedbugs/bug474.go
@@ -0,0 +1,29 @@
+// run
+\
+// Copyright 2013 The Go Authors. All rights reserved.\
+// Use of this source code is governed by a BSD-style\
+// license that can be found in the LICENSE file.\
+\
+// Bug in method values: escape analysis was off.\
+\
+package main
+\
+import "sync"\
+\
+var called = false\
+\
+type T struct {\
+\tonce sync.Once\
+}\
+\
+func (t *T) M() {\
+\tcalled = true\
+}\
+\
+func main() {\
+\tvar t T\
+\tt.once.Do(t.M)\
+\tif !called {\
+\t\tpanic("not called")\
+\t}\
+}\
コアとなるコードの解説
src/cmd/gc/closure.c
の変更
fn->right = sym;
の追加:OCALLPART
ノードのright
フィールドに、メソッド値のレシーバ(sym
)が明示的に設定されるようになりました。これにより、エスケープ解析がレシーバの情報を正確に参照できるようになります。fn->type = fn->nname->type;
への変更:OCALLPART
ノードの型が、生成されるクロージャの型(fn->nname->type
)に設定されるようになりました。これは、メソッド値が単なるメソッドの型ではなく、レシーバがバインドされた新しい関数としての型を持つべきであることを示しています。
src/cmd/gc/esc.c
の変更
OCALLPART
ケースの追加:esc
関数内でOCALLPART
ノードが明示的に処理されるようになりました。これにより、メソッド値がエスケープ解析の対象として認識され、そのレシーバのエスケープ状態が追跡されます。n->esc = EscNone;
は、デフォルトでエスケープしないと仮定し、後続の解析で必要に応じてエスケープ状態が変更されることを意味します。escassign(e, &e->theSink, n->left);
は、OCALLPART
の左側(通常はレシーバ)がメモリに書き込まれる可能性があるため、そのエスケープ状態を追跡します。
escassign
およびescwalk
関数へのOCALLPART
の追加: これらの関数は、変数の代入やデータフローを通じてエスケープ状態を伝播させる役割を担っています。OCALLPART
がこれらの関数で考慮されるようになったことで、メソッド値のレシーバがヒープにエスケープすべきかどうかをより正確に判断できるようになりました。
src/cmd/gc/fmt.c
の変更
OCALLPART
の追加: コンパイラの内部ノードを文字列として表現する際に、OCALLPART
が正しくフォーマットされるようになりました。これは、デバッグやコンパイラの内部状態の可視性向上に役立ちます。
テストファイルの追加と変更
test/escape2.go
の追加テスト:foo141
とfoo142
は、メソッド値がローカル変数に代入される場合とグローバル変数に代入される場合で、レシーバのエスケープ挙動が異なることを検証します。これにより、エスケープ解析がこれらのシナリオを正しく処理していることを確認します。test/fixedbugs/bug474.go
の追加: この新しいテストは、sync.Once.Do
にメソッド値を渡した際に発生していた、レシーバの不必要なヒープエスケープのバグを具体的に再現し、修正が適用された後にこの問題が解決されていることを確認します。
これらの変更は、Goコンパイラがメソッド値のセマンティクスをより深く理解し、エスケープ解析の精度を向上させることで、より効率的なコードを生成するための重要なステップです。
関連リンク
- Go言語のエスケープ解析に関する公式ドキュメントやブログ記事(当時のものがあれば)
- Goコンパイラのソースコードリポジトリ
- Go言語のメソッド値に関する仕様
参考にした情報源リンク
- Go issue 474: cmd/gc: method values escape analysis is off (このコミットが修正したバグのIssue)
- Go CL 7518050: cmd/gc: fix escape analysis of method values (このコミットのGerritレビューページ)
- Go Escape Analysis (Go言語のメモリ管理とエスケープ解析に関する一般的な情報)
- Go Method Values (Go言語のメソッド値に関する一般的な情報)
- Goコンパイラのソースコード (
src/cmd/gc/
) - Go言語の仕様書
- 一般的なコンパイラ最適化、特にエスケープ解析に関する情報