[インデックス 15691] ファイルの概要
このコミットは、Goコンパイラ(cmd/gc
)におけるメソッド呼び出しの最適化に関するものです。具体的には、インターフェースに対するメソッド呼び出しにおいて生成されるラッパーメソッド内でのインライン化を有効にすることで、パフォーマンスを大幅に改善しています。
変更されたファイルは以下の通りです。
src/cmd/gc/inl.c
: インライン化処理に関連するコード。主に、インライン化される関数の引数や戻り値の処理方法が変更されています。src/cmd/gc/obj.c
: オブジェクトファイルのダンプ(出力)に関連するコード。追加のグローバル変数をダンプするための変更が含まれています。src/cmd/gc/subr.c
: コンパイラのサブルーチン。特に、メソッドラッパーを生成するgenwrapper
関数内でインライン化をトリガーする呼び出しが追加されています。
コミット
commit 386ad0ab9056e2f9a0d05d7f86c8ae323262228b
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date: Mon Mar 11 21:24:51 2013 +0100
cmd/gc: enable inlining in generated method wrappers.
Method calls on interfaces with large stored values
will call the pointer receiver method which may be
a wrapper over a method with value receiver.
This is particularly inefficient for very small bodies.
Inlining the wrapped method body saves a potentially expensive
function call.
benchmark old ns/op new ns/op delta
BenchmarkSortString1K 802295 641387 -20.06%
BenchmarkSortInt1K 359914 238234 -33.81%
BenchmarkSortInt64K 35764226 22803078 -36.24%
Fixes #4707.
R=golang-dev, daniel.morsing, rsc
CC=golang-dev
https://golang.org/cl/7214044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/386ad0ab9056e2f9a0d05d7f86c8ae323262228b
元コミット内容
cmd/gc: enable inlining in generated method wrappers.
インターフェースに対するメソッド呼び出しにおいて、大きな値が格納されている場合、ポインタレシーバのメソッドが呼び出されます。このポインタレシーバのメソッドは、値レシーバのメソッドに対するラッパーである可能性があります。
特に本体が非常に小さいメソッドの場合、このラッパーを介した呼び出しは非常に非効率的です。ラップされたメソッドの本体をインライン化することで、潜在的に高コストな関数呼び出しを削減できます。
ベンチマーク結果は以下の通りです。
BenchmarkSortString1K
: 802295 ns/op -> 641387 ns/op (-20.06%)BenchmarkSortInt1K
: 359914 ns/op -> 238234 ns/op (-33.81%)BenchmarkSortInt64K
: 35764226 ns/op -> 22803078 ns/op (-36.24%)
Issue #4707 を修正します。
変更の背景
Go言語では、インターフェースは多態性(ポリモーフィズム)を実現するための強力な機能です。しかし、インターフェースを介してメソッドを呼び出す際には、直接的な関数呼び出しとは異なるオーバーヘッドが発生する可能性があります。
特に、値レシーバで定義されたメソッドがインターフェース型を通じて呼び出される場合、Goコンパイラは内部的に「ラッパーメソッド」を生成することがあります。このラッパーメソッドは、インターフェースの値(通常はポインタと型情報)を受け取り、実際の値レシーバメソッドを呼び出す前に、必要に応じて値のコピーやポインタのデリファレンスを行います。
問題は、このラッパーメソッドの本体が非常に小さい(例えば、単にフィールドにアクセスするだけのような)場合でも、通常の関数呼び出しのオーバーヘッド(スタックフレームのセットアップ、レジスタの保存・復元など)が発生してしまう点にありました。これにより、特にループ内で頻繁に呼び出されるようなケースでは、無視できないパフォーマンスの低下を引き起こしていました。
このコミットの目的は、このような「生成されたメソッドラッパー」の内部で、ラップされている実際のメソッド本体をインライン化できるようにすることで、このオーバーヘッドを削減し、全体的な実行速度を向上させることにあります。ベンチマーク結果が示すように、ソートのようなデータ処理において顕著な改善が見られました。
前提知識の解説
このコミットを理解するためには、以下のGo言語およびコンパイラの概念を理解しておく必要があります。
-
レシーバ (Receiver): Goのメソッドは、レシーバと呼ばれる特別な引数を持ちます。レシーバには「値レシーバ」と「ポインタレシーバ」の2種類があります。
- 値レシーバ:
func (s MyStruct) MethodName() {}
のように定義されます。メソッドが呼び出される際、レシーバの値がコピーされて渡されます。元の値は変更されません。 - ポインタレシーバ:
func (s *MyStruct) MethodName() {}
のように定義されます。メソッドが呼び出される際、レシーバのポインタが渡されます。メソッド内でレシーバの値を変更すると、元の値も変更されます。
- 値レシーバ:
-
インターフェース (Interface): インターフェースは、メソッドのシグネチャの集合を定義する型です。ある型がインターフェースのすべてのメソッドを実装していれば、その型はそのインターフェースを満たします。インターフェース型の変数は、そのインターフェースを満たす任意の型の値を保持できます。
-
インターフェースメソッド呼び出しのメカニズム: Goのインターフェースは、内部的に「型情報」と「値(または値へのポインタ)」のペアとして表現されます。インターフェースメソッドが呼び出されるとき、Goランタイムは保持されている型情報に基づいて、適切なメソッドを動的にディスパッチします。
- 値レシーバのメソッドがインターフェースを介して呼び出される場合、Goコンパイラは、インターフェースが保持するポインタから実際の値をデリファレンスし、その値のコピーに対してメソッドを呼び出すための「ラッパーメソッド」を生成することがあります。
- ポインタレシーバのメソッドがインターフェースを介して呼び出される場合、インターフェースが保持するポインタをそのまま使用してメソッドを呼び出します。
-
インライン化 (Inlining): コンパイラ最適化の一種で、関数呼び出しのオーバーヘッドを削減するために行われます。呼び出される関数の本体を、呼び出し元のコードに直接埋め込む(インライン展開する)ことで、関数呼び出しのコスト(スタックフレームの作成、引数の渡し、レジスタの保存・復元、ジャンプ命令など)をなくします。これにより、プログラムの実行速度が向上しますが、コードサイズが増加する可能性があります。コンパイラは、関数のサイズや呼び出し頻度などに基づいて、インライン化を行うかどうかを決定します。
-
Goコンパイラ (
cmd/gc
): Go言語の公式コンパイラです。ソースコードを機械語に変換する過程で、様々な最適化を行います。このコミットは、その最適化フェーズの一部、特にインライン化のロジックに手を入れています。
技術的詳細
このコミットの核心は、Goコンパイラがインターフェースメソッド呼び出しのために生成する「ラッパーメソッド」に対して、インライン化を適用できるようにすることです。
Goコンパイラは、インターフェース型を通じて値レシーバのメソッドが呼び出される際に、以下のような状況でラッパーメソッドを生成します。
type MyStruct struct {
// large fields
}
func (s MyStruct) MyMethod() int {
return 1 // very small body
}
func main() {
var i interface{} = MyStruct{}
_ = i.(interface{ MyMethod() int }).MyMethod() // インターフェースを介した呼び出し
}
この場合、i.MyMethod()
が呼び出されると、GoコンパイラはMyStruct.MyMethod
を直接呼び出すのではなく、MyStruct
の値をインターフェースから取り出し、その値のコピーに対してMyMethod
を呼び出すための内部的なラッパー関数を生成します。このラッパー関数自体は非常に小さく、その中で実際のMyMethod
が呼び出されます。
以前のコンパイラでは、この生成されたラッパーメソッドはインライン化の対象外でした。そのため、たとえMyMethod
の本体が非常に小さく、インライン化に適していても、ラッパーメソッドを介した呼び出しのオーバーヘッドが常に発生していました。
このコミットは、以下の変更によってこの問題を解決します。
-
src/cmd/gc/subr.c
のgenwrapper
関数: この関数は、Goコンパイラがインターフェースメソッド呼び出しのためにラッパーメソッドを生成する場所です。変更前は、ラッパーメソッドの生成後にfunccompile
を呼び出すだけでしたが、変更後はfunccompile
の前にinlcalls(fn);
が追加されました。inlcalls(fn)
は、指定された関数fn
の呼び出しをインライン化しようとするコンパイラのパスです。これにより、生成されたラッパーメソッドの内部で、ラップされている実際のメソッド(MyMethod
など)の呼び出しがインライン化の候補として扱われるようになります。 -
src/cmd/gc/inl.c
のmkinlcall1
関数: この関数は、インライン化される関数の引数と戻り値を処理するロジックを含んでいます。変更は、特に匿名戻り値の処理と、PARAMOUT
(戻り値)の変数の扱いに関するものです。- 以前は、
PARAMOUT
の変数はdcl
リストを走査する際にスキップされていましたが、新しいコードではPARAMOUT
の変数をinlvar
として適切に処理し、ninit
リストに追加するように変更されました。 - 匿名戻り値の処理も改善され、
inlretvars
リストへの追加ロジックがより堅牢になりました。これにより、インライン化された関数が複数の戻り値を持つ場合でも、正しく処理されるようになります。
- 以前は、
-
src/cmd/gc/obj.c
のdumpobj
関数: このファイルは、コンパイラが生成するオブジェクトファイル(.o
ファイル)のダンプ(出力)に関連しています。変更は、externdcl
リスト(外部宣言のリスト)を一時的に操作して、追加のグローバル変数をダンプするためのものです。これは、インライン化の変更によってコンパイラの内部状態が変化し、より多くの情報がオブジェクトファイルにダンプされる必要が生じたためと考えられます。この変更自体は直接インライン化のロジックに影響を与えるものではなく、コンパイラのデバッグや内部処理の正確性を保つためのものです。
これらの変更により、Goコンパイラは、インターフェースを介したメソッド呼び出しの際に生成されるラッパーメソッドの内部で、実際のメソッド本体をインライン化できるようになりました。これにより、特に小さなメソッドが頻繁に呼び出される場合に、関数呼び出しのオーバーヘッドが削減され、実行速度が向上します。
コアとなるコードの変更箇所
src/cmd/gc/inl.c
--- a/src/cmd/gc/inl.c
+++ b/src/cmd/gc/inl.c
@@ -565,24 +565,31 @@ mkinlcall1(Node **np, Node *fn, int isddd)\n inlretvars = nil;\n i = 0;\n // Make temp names to use instead of the originals\n- for(ll = dcl; ll; ll=ll->next)\n+ for(ll = dcl; ll; ll=ll->next) {\n+ if(ll->n->class == PPARAMOUT) // return values handled below.\n+ continue;\n if(ll->n->op == ONAME) {\n ll->n->inlvar = inlvar(ll->n);\n // Typecheck because inlvar is not necessarily a function parameter.\n typecheck(&ll->n->inlvar, Erv);\n if ((ll->n->class&~PHEAP) != PAUTO)\n ninit = list(ninit, nod(ODCL, ll->n->inlvar, N)); // otherwise gen won't emit the allocations for heapallocs\n- if (ll->n->class == PPARAMOUT) // we rely on the order being correct here\n- inlretvars = list(inlretvars, ll->n->inlvar);\n }\n+ }\n \n- // anonymous return values, synthesize names for use in assignment that replaces return\n- if(inlretvars == nil && fn->type->outtuple > 0)\n- for(t = getoutargx(fn->type)->type; t; t = t->down) {\n+ // temporaries for return values.\n+ for(t = getoutargx(fn->type)->type; t; t = t->down) {\n+ if(t != T && t->nname != N && !isblank(t->nname)) {\n+ m = inlvar(t->nname);\n+ typecheck(&m, Erv);\n+ t->nname->inlvar = m;\n+ } else {\n+ // anonymous return values, synthesize names for use in assignment that replaces return\n m = retvar(t, i++);\n-\t\t\tninit = list(ninit, nod(ODCL, m, N));\n-\t\t\tinlretvars = list(inlretvars, m);\n }\n+ ninit = list(ninit, nod(ODCL, m, N));\n+ inlretvars = list(inlretvars, m);\n+ }\n \n // assign receiver.\n if(fn->type->thistuple && n->left->op == ODOTMETH) {
src/cmd/gc/obj.c
--- a/src/cmd/gc/obj.c
+++ b/src/cmd/gc/obj.c
@@ -16,6 +16,8 @@ static void dumpglobls(void);\n void\ndumpobj(void)\n {\n+\tNodeList *externs, *tmp;\n+\n \tbout = Bopen(outfile, OWRITE);\n \tif(bout == nil) {\n \t\tflusherrors();\n@@ -31,8 +33,20 @@ dumpobj(void)\n \n \touthist(bout);\n \n+\texterns = nil;\n+\tif(externdcl != nil)\n+\t\texterns = externdcl->end;\n+\n \tdumpglobls();\n \tdumptypestructs();\n+\n+\t// Dump extra globals.\n+\ttmp = externdcl;\n+\tif(externs != nil)\n+\t\texterndcl = externs->next;\n+\tdumpglobls();\n+\texterndcl = tmp;\n+\n \tdumpdata();\n \tdumpfuncs();\n \n```
### `src/cmd/gc/subr.c`
```diff
--- a/src/cmd/gc/subr.c
+++ b/src/cmd/gc/subr.c
@@ -2565,6 +2565,7 @@ genwrapper(Type *rcvr, Type *method, Sym *newnam, int iface)\n fn->dupok = 1;\n typecheck(&fn, Etop);\n typechecklist(fn->nbody, Etop);\n+\tinlcalls(fn);\n curfn = nil;\n funccompile(fn, 0);\n }\n```
## コアとなるコードの解説
### `src/cmd/gc/inl.c` の変更
`mkinlcall1`関数は、インライン化される関数呼び出しの準備を行う部分です。
変更のポイントは、インライン化される関数の戻り値(`PARAMOUT`クラスの変数)の扱いを改善した点です。
* 変更前は、`dcl`(宣言リスト)を走査する際に`PARAMOUT`の変数をスキップし、匿名戻り値の処理を別のブロックで行っていました。
* 変更後は、`dcl`リストの走査で`PARAMOUT`の変数をスキップするロジックが削除され、代わりに匿名戻り値と名前付き戻り値の両方を統一的に処理する新しいループが導入されました。
* 新しいループでは、戻り値ごとに一時変数(`inlvar`)を生成し、`ninit`リスト(初期化ノードのリスト)に追加することで、コンパイラがこれらの戻り値のためのメモリ割り当てを正しく行えるようにしています。これにより、インライン化された関数が複数の戻り値を持つ場合でも、正しく処理されるようになります。
### `src/cmd/gc/obj.c` の変更
`dumpobj`関数は、コンパイルされたオブジェクトファイルをディスクに書き出す際に呼び出されます。
この変更は、主にコンパイラの内部状態のダンプに関するものです。
* `externdcl`は、外部宣言(グローバル変数など)のリストを保持しています。
* この変更は、`externdcl`リストを一時的に操作し、追加のグローバル変数をオブジェクトファイルにダンプするためのロジックを追加しています。
* 具体的には、現在の`externdcl`の終端を保存し、`externdcl`をその終端の次から再設定して`dumpglobls()`を再度呼び出すことで、以前の`dumpglobls()`呼び出しではダンプされなかった新しいグローバル変数をダンプします。
* これは、インライン化の変更によってコンパイラの内部で新しいグローバル変数が生成される可能性があり、それらをオブジェクトファイルに含めることで、リンカが正しく動作するようにするための保守的な変更と考えられます。
### `src/cmd/gc/subr.c` の変更
`genwrapper`関数は、Goコンパイラがインターフェースメソッド呼び出しのためにラッパーメソッドを生成する主要な場所です。
この変更は非常にシンプルですが、最も重要な変更点です。
* `funccompile(fn, 0);` の直前に `inlcalls(fn);` が追加されました。
* `inlcalls(fn)` は、Goコンパイラのインライン化パスを呼び出す関数です。
* この変更により、`genwrapper`によって生成されたラッパーメソッド(`fn`)が、コンパイルされる前にインライン化の対象として処理されるようになります。
* 結果として、ラッパーメソッドの内部で呼び出される実際のメソッド本体が、そのラッパーメソッドのコードに直接インライン展開される可能性が生まれ、関数呼び出しのオーバーヘッドが削減されます。
## 関連リンク
* Go Code Review: [https://golang.org/cl/7214044](https://golang.org/cl/7214044)
* `Fixes #4707` とありますが、Goの公開Issueトラッカーでは直接この番号のIssueは見つかりませんでした。これは、非常に古いIssueであるか、内部的なトラッカーの番号である可能性があります。
## 参考にした情報源リンク
* Go言語のコミット履歴とソースコード
* Go言語のインターフェースとメソッドに関する一般的なドキュメント
* コンパイラのインライン化最適化に関する一般的な知識