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

[インデックス 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がメソッド値です。fsという特定のレシーバとGetValueメソッドを組み合わせたものです。コンパイラ内部では、このようなメソッド値は特殊な構造として扱われます。

OCALLPART

OCALLPARTは、Goコンパイラの内部で使われるノードタイプの一つです。これは「部分適用された関数呼び出し(partial function application)」を表します。メソッド値は、レシーバが部分適用された関数呼び出しとして表現されるため、このOCALLPARTノードが使用されます。

技術的詳細

このコミットの技術的詳細は、Goコンパイラの内部構造、特に中間表現(IR)とエスケープ解析のパスに深く関わっています。

  1. OCALLPARTノードの導入と修正:

    • src/cmd/gc/closure.ctypecheckpartialcall関数は、メソッド値が作成される際にOCALLPARTノードを生成します。以前は、このノードのtypeフィールドがfn->right->type(メソッドの型)に設定されていましたが、修正後はfn->nname->type(生成されたクロージャの型)に設定されるようになりました。これは、OCALLPARTが単なるメソッドではなく、レシーバがバインドされた新しい関数(クロージャ)として扱われるべきであることを反映しています。
    • また、fn->right = sym;という行が追加され、OCALLPARTノードのrightフィールドにレシーバのシンボルが明示的に設定されるようになりました。これにより、エスケープ解析がレシーバの情報をより正確に追跡できるようになります。
  2. エスケープ解析 (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がヒープにエスケープする場合、その内部の要素(レシーバなど)もヒープにエスケープするようになります。
  3. デバッグ出力 (fmt.c) の改善:

    • src/cmd/gc/fmt.cは、コンパイラの内部ノードを文字列としてフォーマットする関数を含んでいます。
    • opprec配列にOCALLPARTの優先順位が追加されました。
    • exprfmt関数にOCALLPARTケースが追加され、OCALLPARTノードがデバッグ出力で適切に表示されるようになりました。これにより、コンパイラの開発者が内部状態をデバッグしやすくなります。
  4. テストケースの追加:

    • test/escape2.gofoo141foo142という新しいテスト関数が追加されました。これらは、メソッド値のエスケープ解析が正しく機能するかどうかを検証します。
      • 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の追加テスト: foo141foo142は、メソッド値がローカル変数に代入される場合とグローバル変数に代入される場合で、レシーバのエスケープ挙動が異なることを検証します。これにより、エスケープ解析がこれらのシナリオを正しく処理していることを確認します。
  • test/fixedbugs/bug474.goの追加: この新しいテストは、sync.Once.Doにメソッド値を渡した際に発生していた、レシーバの不必要なヒープエスケープのバグを具体的に再現し、修正が適用された後にこの問題が解決されていることを確認します。

これらの変更は、Goコンパイラがメソッド値のセマンティクスをより深く理解し、エスケープ解析の精度を向上させることで、より効率的なコードを生成するための重要なステップです。

関連リンク

  • Go言語のエスケープ解析に関する公式ドキュメントやブログ記事(当時のものがあれば)
  • Goコンパイラのソースコードリポジトリ
  • Go言語のメソッド値に関する仕様

参考にした情報源リンク

このコミットは、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がメソッド値です。fsという特定のレシーバとGetValueメソッドを組み合わせたものです。コンパイラ内部では、このようなメソッド値は特殊な構造として扱われます。

OCALLPART

OCALLPARTは、Goコンパイラの内部で使われるノードタイプの一つです。これは「部分適用された関数呼び出し(partial function application)」を表します。メソッド値は、レシーバが部分適用された関数呼び出しとして表現されるため、このOCALLPARTノードが使用されます。

技術的詳細

このコミットの技術的詳細は、Goコンパイラの内部構造、特に中間表現(IR)とエスケープ解析のパスに深く関わっています。

  1. OCALLPARTノードの導入と修正:

    • src/cmd/gc/closure.ctypecheckpartialcall関数は、メソッド値が作成される際にOCALLPARTノードを生成します。以前は、このノードのtypeフィールドがfn->right->type(メソッドの型)に設定されていましたが、修正後はfn->nname->type(生成されたクロージャの型)に設定されるようになりました。これは、OCALLPARTが単なるメソッドではなく、レシーバがバインドされた新しい関数(クロージャ)として扱われるべきであることを反映しています。
    • また、fn->right = sym;という行が追加され、OCALLPARTノードのrightフィールドにレシーバのシンボルが明示的に設定されるようになりました。これにより、エスケープ解析がレシーバの情報をより正確に追跡できるようになります。
  2. エスケープ解析 (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がヒープにエスケープする場合、その内部の要素(レシーバなど)もヒープにエスケープするようになります。
  3. デバッグ出力 (fmt.c) の改善:

    • src/cmd/gc/fmt.cは、コンパイラの内部ノードを文字列としてフォーマットする関数を含んでいます。
    • opprec配列にOCALLPARTの優先順位が追加されました。
    • exprfmt関数にOCALLPARTケースが追加され、OCALLPARTノードがデバッグ出力で適切に表示されるようになりました。これにより、コンパイラの開発者が内部状態をデバッグしやすくなります。
  4. テストケースの追加:

    • test/escape2.gofoo141foo142という新しいテスト関数が追加されました。これらは、メソッド値のエスケープ解析が正しく機能するかどうかを検証します。
      • 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の追加テスト: foo141foo142は、メソッド値がローカル変数に代入される場合とグローバル変数に代入される場合で、レシーバのエスケープ挙動が異なることを検証します。これにより、エスケープ解析がこれらのシナリオを正しく処理していることを確認します。
  • test/fixedbugs/bug474.goの追加: この新しいテストは、sync.Once.Doにメソッド値を渡した際に発生していた、レシーバの不必要なヒープエスケープのバグを具体的に再現し、修正が適用された後にこの問題が解決されていることを確認します。

これらの変更は、Goコンパイラがメソッド値のセマンティクスをより深く理解し、エスケープ解析の精度を向上させることで、より効率的なコードを生成するための重要なステップです。

関連リンク

  • Go言語のエスケープ解析に関する公式ドキュメントやブログ記事(当時のものがあれば)
  • Goコンパイラのソースコードリポジトリ
  • Go言語のメソッド値に関する仕様

参考にした情報源リンク