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

[インデックス 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言語およびコンパイラの概念に関する知識が不可欠です。

  1. Goコンパイラ (cmd/gc): Go言語の公式コンパイラは、主にcmd/compile(以前はcmd/gcとして知られていた)というツールです。これはGoソースコードを機械語に変換する役割を担います。コンパイラは複数のフェーズ(字句解析、構文解析、型チェック、中間表現生成、最適化、コード生成など)を経て実行されます。

  2. エスケープ解析 (Escape Analysis): Goコンパイラの重要な最適化の一つです。変数がその宣言されたスコープ(通常は関数内)を「エスケープ」するかどうかを判断します。

    • エスケープしない場合: 変数は関数のスタックフレームに割り当てられます。関数が終了すると、そのメモリは自動的に解放されます。これは非常に高速で、ガベージコレクションの対象外です。
    • エスケープする場合: 変数はヒープに割り当てられます。これは、変数が関数の外部から参照され続ける可能性がある(例: グローバル変数に代入される、ポインタが返される、クロージャによってキャプチャされるなど)ためです。ヒープに割り当てられたメモリはガベージコレクタによって管理されます。 エスケープ解析の目的は、可能な限り多くの変数をスタックに割り当てることで、ガベージコレクションの頻度と負荷を減らし、プログラムのパフォーマンスを向上させることです。
  3. メソッド値 (Method Values) とメソッド式 (Method Expressions): Goでは、メソッドを関数のように扱うことができます。

    • メソッド式: T.Method のように型からメソッドを参照する形式です。これは通常の関数のように呼び出され、レシーバを最初の引数として明示的に渡す必要があります(例: T.Method(t_instance, args...))。
    • メソッド値: t.Method のように具体的なインスタンスからメソッドを参照し、それを変数に代入する形式です(例: f := t.Method)。この場合、レシーバtは既にfにバインドされており、fを呼び出す際にはレシーバを渡す必要がありません(例: f(args...))。コンパイラは、このメソッド値の呼び出しを可能にするために、内部的に「メソッドラッパー」または「サンク (thunk)」と呼ばれる小さな補助関数を生成します。このラッパーが、元のメソッドを呼び出す際にレシーバを適切に渡す役割を担います。
  4. Nodedclfunc: Goコンパイラの内部では、ソースコードは抽象構文木 (AST) として表現されます。ASTの各要素はNode構造体で表されます。dclfuncは、関数宣言を表すNodeの一種です。コンパイラが関数を処理する際、このdclfuncノードに関連付けられた情報(例えば、その関数が現在の処理対象であること)が必要になります。

  5. 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.gofoo)が、本来スタックに割り当てられるべきであるにもかかわらず、ヒープに割り当てられてしまう可能性がありました。

このコミットでは、makepartialcall関数内でcurfnの値を一時的に保存し、メソッドラッパーのdclfuncノード(xfunc)の生成と型チェックの間にcurfnxfuncに設定し、その後元のcurfnに戻すという処理を追加しています。これにより、xfuncが型チェックされ、エスケープ解析が実行される際に、curfnが正しくxfuncを指すようになり、エスケープ解析がメソッドラッパーに対しても適切に実行されるようになりました。

具体的には、以下の変更が行われました。

  1. savecurfnという新しいNodeポインタ変数を導入し、現在のcurfnの値を一時的に保存します。
  2. メソッドラッパーのdclfuncノード(xfunc)が作成される直前に、curfnN(nil)に設定します。これは、新しい関数コンテキストの準備のためと考えられます。
  3. xfuncが作成された直後に、curfnxfunc自体に設定します。これにより、xfuncの型チェックやその後のエスケープ解析が、このメソッドラッパーのコンテキストで正しく行われるようになります。
  4. xfuncの型チェックが完了し、sym->def = xfunc;xtop = list(xtop, xfunc);xfuncがコンパイラの内部リストに追加された後、curfnsavecurfnに保存しておいた元の値に戻します。これは、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ノードを作成します。

  1. Node *savecurfn; の追加: makepartialcall関数のローカル変数として、savecurfnという新しいNodeポインタが宣言されました。これは、関数が実行される前のcurfnの値を一時的に保存するために使用されます。

  2. savecurfn = curfn;: メソッドラッパーの生成処理に入る前に、現在のcurfnの値をsavecurfnに保存します。これにより、メソッドラッパーの処理が完了した後に、curfnを元の状態に戻すことができます。

  3. curfn = N;: savecurfnに元のcurfnを保存した後、curfnN(nil)に設定します。これは、新しい関数コンテキスト(メソッドラッパー)の構築を開始する準備として、一時的にグローバルなcurfnをクリアする意図があると考えられます。

  4. xfunc = nod(ODCLFUNC, N, N);: ここで、メソッドラッパーを表す新しい関数宣言ノードxfuncが作成されます。

  5. curfn = xfunc;: xfuncが作成された直後に、グローバルなcurfnを新しく作成されたxfuncに設定します。この行が最も重要です。これにより、xfuncの型チェック(typecheck(&xfunc, Etop);)や、その後のエスケープ解析が実行される際に、コンパイラが「現在処理している関数はxfuncである」と正しく認識できるようになります。エスケープ解析は、このcurfnの値を見て、どの関数内の変数を解析すべきかを判断するため、この設定が不可欠でした。

  6. curfn = savecurfn;: xfuncの型チェックが完了し、コンパイラの内部構造に組み込まれた後、curfnsavecurfnに保存しておいた元の値に戻します。これにより、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(...) } は、基本的な機能が壊れていないことを確認するためのアサーションです。

このテストは、コンパイラがメソッドラッパーを生成し、そのラッパーに対してエスケープ解析が正しく実行されるようになったことを暗黙的に検証します。もしエスケープ解析が機能しない場合、特定の最適化が適用されず、潜在的なパフォーマンス問題が発生する可能性がありますが、このテストはコンパイラの内部的な挙動の修正を目的としているため、直接的な実行時エラーではなく、コンパイラの健全性を保証するものです。

関連リンク

参考にした情報源リンク