[インデックス 16697] ファイルの概要
このコミットは、Goコンパイラ(cmd/gc
)におけるエスケープ解析の不具合修正に関するものです。具体的には、メソッドラッパーに対してエスケープ解析が正しく実行されない問題に対処しています。この問題は、コンパイラがメソッドラッパーの生成時にcurfn
(現在の関数を示すポインタ)の値を適切に設定していなかったために発生していました。
コミット
commit 7cfa8310c75bfe8534a61f0f64116cb508d6f10d
Author: Daniel Morsing <daniel.morsing@gmail.com>
Date: Tue Jul 2 17:12:08 2013 +0200
cmd/gc: fix issue with method wrappers not having escape analysis run on them.
Escape analysis needs the right curfn value on a dclfunc node, otherwise it will not analyze the function.
When generating method value wrappers, we forgot to set the curfn correctly.
Fixes #5753.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/10383048
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/7cfa8310c75bfe8534a61f0f64116cb508d6f10d
元コミット内容
cmd/gc: fix issue with method wrappers not having escape analysis run on them.
このコミットは、Goコンパイラのcmd/gc
部分におけるバグを修正するものです。メソッドラッパーに対してエスケープ解析が実行されないという問題がありました。エスケープ解析は、dclfunc
ノード(関数宣言ノード)に対して正しいcurfn
(現在の関数)の値が設定されている必要がありますが、メソッド値ラッパーを生成する際にこのcurfn
が正しく設定されていなかったため、エスケープ解析がスキップされていました。この修正により、issue #5753
が解決されます。
変更の背景
Go言語のコンパイラには、プログラムの効率を向上させるための重要な最適化フェーズとして「エスケープ解析 (Escape Analysis)」があります。エスケープ解析は、変数がスタックに割り当てられるべきか、それともヒープに割り当てられるべきかを決定します。スタック割り当ては、ヒープ割り当てに比べてガベージコレクションのオーバーヘッドがないため、一般的にパフォーマンスが優れています。
このコミットが修正しようとしている問題は、Goコンパイラが特定の種類の関数、特に「メソッドラッパー」を処理する際に、このエスケープ解析が正しく機能しないというものでした。メソッドラッパーとは、Goのメソッドが値として扱われる(例えば、t.broken
のようにメソッドを変数に代入する)場合に、コンパイラが内部的に生成する小さな補助関数(サンクとも呼ばれる)です。これらのラッパーは、元のメソッド呼び出しを適切に処理するために必要な追加のロジック(レシーバのバインディングなど)を含んでいます。
問題の根本原因は、コンパイラがこれらのメソッドラッパーを生成する際に、エスケープ解析が必要とする内部的なコンテキスト情報であるcurfn
("current function"、現在の処理対象関数を示すポインタ)を正しく設定していなかったことにありました。curfn
が正しくない、またはN
(nil)に設定されている場合、エスケープ解析は対象の関数を解析対象として認識せず、結果としてその関数内の変数のエスケープ挙動を最適化できませんでした。これにより、本来スタックに割り当てられるべき変数が不必要にヒープに割り当てられ、ガベージコレクションの負荷が増加し、パフォーマンスが低下する可能性がありました。
issue #5753
は、この具体的な問題を報告したものであり、このコミットはその解決を目的としています。
前提知識の解説
このコミットの理解には、以下のGo言語およびコンパイラの概念に関する知識が不可欠です。
-
Goコンパイラ (
cmd/gc
): Go言語の公式コンパイラは、主にcmd/compile
(以前はcmd/gc
として知られていた)というツールです。これはGoソースコードを機械語に変換する役割を担います。コンパイラは複数のフェーズ(字句解析、構文解析、型チェック、中間表現生成、最適化、コード生成など)を経て実行されます。 -
エスケープ解析 (Escape Analysis): Goコンパイラの重要な最適化の一つです。変数がその宣言されたスコープ(通常は関数内)を「エスケープ」するかどうかを判断します。
- エスケープしない場合: 変数は関数のスタックフレームに割り当てられます。関数が終了すると、そのメモリは自動的に解放されます。これは非常に高速で、ガベージコレクションの対象外です。
- エスケープする場合: 変数はヒープに割り当てられます。これは、変数が関数の外部から参照され続ける可能性がある(例: グローバル変数に代入される、ポインタが返される、クロージャによってキャプチャされるなど)ためです。ヒープに割り当てられたメモリはガベージコレクタによって管理されます。 エスケープ解析の目的は、可能な限り多くの変数をスタックに割り当てることで、ガベージコレクションの頻度と負荷を減らし、プログラムのパフォーマンスを向上させることです。
-
メソッド値 (Method Values) とメソッド式 (Method Expressions): Goでは、メソッドを関数のように扱うことができます。
- メソッド式:
T.Method
のように型からメソッドを参照する形式です。これは通常の関数のように呼び出され、レシーバを最初の引数として明示的に渡す必要があります(例:T.Method(t_instance, args...)
)。 - メソッド値:
t.Method
のように具体的なインスタンスからメソッドを参照し、それを変数に代入する形式です(例:f := t.Method
)。この場合、レシーバt
は既にf
にバインドされており、f
を呼び出す際にはレシーバを渡す必要がありません(例:f(args...)
)。コンパイラは、このメソッド値の呼び出しを可能にするために、内部的に「メソッドラッパー」または「サンク (thunk)」と呼ばれる小さな補助関数を生成します。このラッパーが、元のメソッドを呼び出す際にレシーバを適切に渡す役割を担います。
- メソッド式:
-
Node
とdclfunc
: Goコンパイラの内部では、ソースコードは抽象構文木 (AST) として表現されます。ASTの各要素はNode
構造体で表されます。dclfunc
は、関数宣言を表すNode
の一種です。コンパイラが関数を処理する際、このdclfunc
ノードに関連付けられた情報(例えば、その関数が現在の処理対象であること)が必要になります。 -
curfn
: コンパイラの内部状態変数の一つで、現在処理中の関数(dclfunc
ノード)を指すグローバルポインタです。エスケープ解析のような最適化フェーズは、このcurfn
の値を利用して、どの関数内の変数を解析すべきかを判断します。curfn
が正しく設定されていない場合、エスケープ解析はその関数をスキップしてしまう可能性があります。
技術的詳細
このコミットの技術的な核心は、Goコンパイラのsrc/cmd/gc/closure.c
ファイル内のmakepartialcall
関数にあります。この関数は、Goの「メソッド値」が使用される際に、コンパイラが内部的に生成する「メソッドラッパー」(またはサンク)を作成する役割を担っています。
問題は、makepartialcall
がメソッドラッパーを表すdclfunc
ノード(xfunc
)を生成する際に、コンパイラのグローバルな「現在の関数」ポインタであるcurfn
を適切に設定していなかった点にありました。
修正前のコードでは、makepartialcall
関数内で新しいdclfunc
ノードxfunc
が作成された後、curfn
は一時的にN
(nil)に設定され、その後xfunc
に設定されていました。しかし、この設定がエスケープ解析が実行されるタイミングと合致していなかった、あるいはエスケープ解析が期待するcurfn
のライフサイクルと異なっていたため、エスケープ解析がこのxfunc
(メソッドラッパー)を解析対象として認識できませんでした。
エスケープ解析は、dclfunc
ノードが持つcurfn
の値が、その関数自身を指していることを期待します。これにより、解析器は関数内の変数のスコープとライフタイムを正確に追跡できます。curfn
が正しく設定されていないと、エスケープ解析は「この関数は解析対象ではない」と判断し、その結果、メソッドラッパー内で宣言されたローカル変数(例えば、issue5753.go
のfoo
)が、本来スタックに割り当てられるべきであるにもかかわらず、ヒープに割り当てられてしまう可能性がありました。
このコミットでは、makepartialcall
関数内でcurfn
の値を一時的に保存し、メソッドラッパーのdclfunc
ノード(xfunc
)の生成と型チェックの間にcurfn
をxfunc
に設定し、その後元のcurfn
に戻すという処理を追加しています。これにより、xfunc
が型チェックされ、エスケープ解析が実行される際に、curfn
が正しくxfunc
を指すようになり、エスケープ解析がメソッドラッパーに対しても適切に実行されるようになりました。
具体的には、以下の変更が行われました。
savecurfn
という新しいNode
ポインタ変数を導入し、現在のcurfn
の値を一時的に保存します。- メソッドラッパーの
dclfunc
ノード(xfunc
)が作成される直前に、curfn
をN
(nil)に設定します。これは、新しい関数コンテキストの準備のためと考えられます。 xfunc
が作成された直後に、curfn
をxfunc
自体に設定します。これにより、xfunc
の型チェックやその後のエスケープ解析が、このメソッドラッパーのコンテキストで正しく行われるようになります。xfunc
の型チェックが完了し、sym->def = xfunc;
とxtop = list(xtop, xfunc);
でxfunc
がコンパイラの内部リストに追加された後、curfn
をsavecurfn
に保存しておいた元の値に戻します。これは、makepartialcall
関数が終了し、呼び出し元のコンテキストに戻る際に、curfn
が元の正しい状態に戻ることを保証するためです。
この修正により、メソッドラッパー内の変数が適切にエスケープ解析され、不要なヒープ割り当てが回避されることで、Goプログラムの実行効率が向上します。
コアとなるコードの変更箇所
変更は主にsrc/cmd/gc/closure.c
ファイル内のmakepartialcall
関数に集中しています。
--- a/src/cmd/gc/closure.c
+++ b/src/cmd/gc/closure.c
@@ -280,7 +280,7 @@ typecheckpartialcall(Node *fn, Node *sym)
static Node*
makepartialcall(Node *fn, Type *t0, Node *meth)
{
- Node *ptr, *n, *fld, *call, *xtype, *xfunc, *cv;
+ Node *ptr, *n, *fld, *call, *xtype, *xfunc, *cv, *savecurfn;
Type *rcvrtype, *basetype, *t;
NodeList *body, *l, *callargs, *retargs;
char *p;
@@ -304,6 +304,9 @@ makepartialcall(Node *fn, Type *t0, Node *meth)
if(sym->flags & SymUniq)
return sym->def;
sym->flags |= SymUniq;
+
+ savecurfn = curfn;
+ curfn = N;
xtype = nod(OTFUNC, N, N);
i = 0;
@@ -311,6 +314,7 @@ makepartialcall(Node *fn, Type *t0, Node *meth)
callargs = nil;
ddd = 0;
xfunc = nod(ODCLFUNC, N, N);
+ curfn = xfunc;
for(t = getinargx(t0)->type; t; t = t->down) {
snprint(namebuf, sizeof namebuf, "a%d", i++);
n = newname(lookup(namebuf));
@@ -385,6 +389,7 @@ makepartialcall(Node *fn, Type *t0, Node *meth)
typecheck(&xfunc, Etop);
sym->def = xfunc;
xtop = list(xtop, xfunc);
+ curfn = savecurfn;
return xfunc;
}
また、この修正の動作を検証するための新しいテストファイルtest/fixedbugs/issue5753.go
が追加されています。
// 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.
// issue 5753: bad typecheck info causes escape analysis to
// not run on method thunks.
package main
type Thing struct{}
func (t *Thing) broken(s string) []string {
foo := [1]string{s}
return foo[:]
}
func main() {
t := &Thing{}
f := t.broken
s := f("foo")
_ = f("bar")
if s[0] != "foo" {
panic(`s[0] != "foo"`)
}
}
コアとなるコードの解説
src/cmd/gc/closure.c
の変更点
makepartialcall
関数は、メソッド値(例: f := t.broken
)が使用される際に、コンパイラが内部的に生成するメソッドラッパー(サンク)のASTノードを作成します。
-
Node *savecurfn;
の追加:makepartialcall
関数のローカル変数として、savecurfn
という新しいNode
ポインタが宣言されました。これは、関数が実行される前のcurfn
の値を一時的に保存するために使用されます。 -
savecurfn = curfn;
: メソッドラッパーの生成処理に入る前に、現在のcurfn
の値をsavecurfn
に保存します。これにより、メソッドラッパーの処理が完了した後に、curfn
を元の状態に戻すことができます。 -
curfn = N;
:savecurfn
に元のcurfn
を保存した後、curfn
をN
(nil)に設定します。これは、新しい関数コンテキスト(メソッドラッパー)の構築を開始する準備として、一時的にグローバルなcurfn
をクリアする意図があると考えられます。 -
xfunc = nod(ODCLFUNC, N, N);
: ここで、メソッドラッパーを表す新しい関数宣言ノードxfunc
が作成されます。 -
curfn = xfunc;
:xfunc
が作成された直後に、グローバルなcurfn
を新しく作成されたxfunc
に設定します。この行が最も重要です。これにより、xfunc
の型チェック(typecheck(&xfunc, Etop);
)や、その後のエスケープ解析が実行される際に、コンパイラが「現在処理している関数はxfunc
である」と正しく認識できるようになります。エスケープ解析は、このcurfn
の値を見て、どの関数内の変数を解析すべきかを判断するため、この設定が不可欠でした。 -
curfn = savecurfn;
:xfunc
の型チェックが完了し、コンパイラの内部構造に組み込まれた後、curfn
をsavecurfn
に保存しておいた元の値に戻します。これにより、makepartialcall
関数が終了し、呼び出し元のコンテキストに戻る際に、curfn
が元の正しい状態に戻ることが保証されます。これは、コンパイラの他の部分が期待するcurfn
の状態を維持するために重要です。
test/fixedbugs/issue5753.go
のテストケース
このテストケースは、修正された問題が実際に解決されたことを検証するために追加されました。
type Thing struct{}
とfunc (t *Thing) broken(s string) []string
というシンプルな構造体とメソッドを定義しています。broken
メソッド内で、foo := [1]string{s}
という配列foo
を宣言し、そのスライスfoo[:]
を返しています。- 修正前は、
t.broken
をメソッド値としてf := t.broken
のように代入すると、このbroken
メソッドのラッパーが生成されます。このラッパーに対してエスケープ解析が正しく実行されないため、foo
が不必要にヒープに割り当てられる可能性がありました。
- 修正前は、
main
関数内で、f := t.broken
としてメソッド値を生成し、それを呼び出しています。s := f("foo")
と_ = f("bar")
の呼び出しは、メソッドラッパーが実際に使用されるシナリオをシミュレートしています。if s[0] != "foo" { panic(...) }
は、基本的な機能が壊れていないことを確認するためのアサーションです。
このテストは、コンパイラがメソッドラッパーを生成し、そのラッパーに対してエスケープ解析が正しく実行されるようになったことを暗黙的に検証します。もしエスケープ解析が機能しない場合、特定の最適化が適用されず、潜在的なパフォーマンス問題が発生する可能性がありますが、このテストはコンパイラの内部的な挙動の修正を目的としているため、直接的な実行時エラーではなく、コンパイラの健全性を保証するものです。
関連リンク
- Go Issue 5753: https://github.com/golang/go/issues/5753
- Go CL 10383048: https://golang.org/cl/10383048
参考にした情報源リンク
- Go Escape Analysis:
- Go Method Values and Method Expressions:
- Go Compiler Internals (General):
- https://go.dev/src/cmd/compile/internal/gc/README (現在の
cmd/compile
のREADME) - https://github.com/golang/go/wiki/Compiler (Goコンパイラに関するWikiページ)
- https://go.dev/src/cmd/compile/internal/gc/README (現在の
- 関連するGoのIssue (エスケープ解析とインターフェース/メソッド):
- https://github.com/golang/go/issues/36964 (cmd/compile: escape analysis on interface calls)
- https://github.com/golang/go/issues/72036 (cmd/compile: teach escape analysis to conditionally stack alloc interface method call parameters like a slice in w.Write(b))