[インデックス 14543] ファイルの概要
このコミットは、Goコンパイラ(cmd/gc
)におけるインライン化のバグ修正に関するものです。具体的には、メソッド呼び出し(T.Method(f())
)において、f()
が複数の戻り値を返す場合にインライン化が正しく行われず、内部エラーが発生する問題に対処しています。また、可変長引数(variadic)を持つ関数呼び出しがインライン化されないことを確認するためのテストも追加されています。
コミット
commit bcea0dd1d0d41c5cf503c87e86460cd34dbc7dfb
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date: Mon Dec 3 13:39:40 2012 +0100
cmd/gc: fix inlining internal error with T.Method calls.
The compiler was confused when inlining a T.Method(f()) call
where f returns multiple values: support for this was marked
as TODO.
Variadic calls are not supported but are not inlined either.
Add a test preventively for that case.
Fixes #4167.
R=golang-dev, rsc, lvd
CC=golang-dev
https://golang.org/cl/6871043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/bcea0dd1d0d41c5cf503c87e86460cd34dbc7dfb
元コミット内容
Goコンパイラ(cmd/gc
)において、T.Method
呼び出しのインライン化時に発生する内部エラーを修正します。
コンパイラは、T.Method(f())
のような呼び出しで、f()
が複数の戻り値を返す場合にインライン化で混乱していました。この機能のサポートは以前からTODOとしてマークされていました。
可変長引数を持つ呼び出しはサポートされていませんが、インライン化もされません。このケースを予防的にテストするためにテストを追加します。
Issue #4167を修正します。
変更の背景
この変更は、Goコンパイラが特定の条件下でインライン化を行う際に発生する内部エラー(コンパイラのクラッシュや誤ったコード生成)を修正するために行われました。具体的には、Go言語の重要な機能である「複数の戻り値」と「メソッド呼び出し」が組み合わされた場合に問題が発生していました。
Go言語では、関数が複数の値を返すことが可能です。例えば、func f() (int, string)
のように定義された関数は、2つの値を返します。また、Goでは構造体やカスタム型にメソッドを定義できます。このコミットで修正された問題は、someObject.Method(f())
のように、メソッドの引数として複数の戻り値を返す関数が渡された際に、コンパイラのインライン化処理がこの状況を適切に処理できなかったことに起因します。
インライン化は、コンパイラ最適化の一種で、関数呼び出しのオーバーヘッドを削減するために、呼び出し元のコードに関数本体を直接埋め込む処理です。しかし、この複雑なケース(複数の戻り値を持つ関数の結果をメソッドの引数として渡す)では、コンパイラが引数の割り当てや型チェックのロジックで混乱し、内部エラーを引き起こしていました。
また、可変長引数(...type
)を持つ関数は、その性質上、インライン化が困難または不適切であるため、Goコンパイラは通常、これらの関数をインライン化しません。このコミットでは、この挙動が意図通りであることを確認するための予防的なテストも追加されています。
前提知識の解説
このコミットの理解には、以下のGo言語およびコンパイラの概念に関する知識が役立ちます。
-
Go言語のインライン化 (Inlining):
- インライン化は、コンパイラ最適化の一種で、関数呼び出しのコストを削減するために行われます。
- 具体的には、呼び出される関数の本体が、呼び出し元のコードに直接展開されます。これにより、関数呼び出しに伴うスタックフレームの作成、引数のプッシュ、戻り値のポップなどのオーバーヘッドがなくなります。
- Goコンパイラは、関数のサイズや複雑さ、呼び出し回数などのヒューリスティックに基づいて、自動的にインライン化を行うかどうかを決定します。
- インライン化はパフォーマンス向上に寄与しますが、コードサイズが増加する可能性もあります。
-
Go言語の複数の戻り値 (Multiple Return Values):
- Go言語の大きな特徴の一つは、関数が複数の値を返すことができる点です。
- 例:
func divide(a, b int) (int, error)
のように、結果とエラーを同時に返すパターンはGoのイディオムとして広く使われています。 - これらの戻り値は、呼び出し側でタプルとして受け取られるわけではなく、個別の変数に割り当てられます。
-
Go言語のメソッド (Methods):
- Goでは、構造体や任意の型にメソッドを定義できます。メソッドは、特定の型に関連付けられた関数です。
- レシーバ(
func (r ReceiverType) MethodName(...)
のr ReceiverType
部分)を通じて、その型のデータにアクセスできます。 - メソッド呼び出しは、通常の関数呼び出しとは異なる内部的な処理が行われる場合があります(特にレシーバの扱い)。
-
Go言語の可変長引数 (Variadic Functions):
- 関数が不定数の引数を受け取ることができる機能です。引数リストの最後に
...type
の形式で指定します。 - 例:
func sum(nums ...int) int
- 可変長引数は、関数内部ではスライスとして扱われます。
- 関数が不定数の引数を受け取ることができる機能です。引数リストの最後に
-
Goコンパイラ
cmd/gc
:- Go言語の公式コンパイラの一つで、Goのソースコードを機械語に変換します。
src/cmd/gc
ディレクトリにそのソースコードがあります。inl.c
は、このコンパイラのインライン化処理を担当するC言語のソースファイルです。Goコンパイラの初期バージョンはC言語で書かれており、徐々にGo言語に移行していますが、一部のコア部分はC言語で残っています。
-
AST (Abstract Syntax Tree):
- コンパイラがソースコードを解析する際に生成する、プログラムの構造を木構造で表現したものです。
- コンパイラはASTを操作して、型チェック、最適化、コード生成などを行います。
Node
はASTのノードを表すデータ構造です。ODOTMETH
はメソッド呼び出しを表すASTノードのオペレーションコードです。OAS
は代入、OAS2
は多重代入を表します。
技術的詳細
このコミットの技術的詳細は、Goコンパイラのインライン化処理、特にsrc/cmd/gc/inl.c
ファイル内のmkinlcall1
関数に焦点を当てています。この関数は、インライン化される関数呼び出しのASTを変換し、呼び出し元のコードに埋め込む役割を担っています。
問題の核心は、T.Method(f())
のような呼び出しで、f()
が複数の戻り値を返す場合に、インライン化されたメソッドの引数への値の割り当てが正しく行われなかった点にあります。
変更前のコードでは、引数の割り当てロジックが、レシーバの処理と通常の引数の処理で複雑に絡み合っており、特に複数の戻り値を持つ関数が引数として渡された場合に、引数の数を誤って解釈したり、適切なNode
(ASTノード)を生成できなかったりする問題がありました。
修正のポイントは以下の通りです。
-
レシーバの割り当てロジックの分離と明確化:
- 変更前は、レシーバの割り当てと通常の引数の割り当てが同じ
if(fn->type->thistuple)
ブロック内で処理されていました。 - 変更後、レシーバの割り当て(
if(fn->type->thistuple && n->left->op == ODOTMETH)
ブロック)が独立して処理されるようになりました。これにより、メソッド呼び出しにおけるレシーバの特殊な扱いが明確になり、コードの可読性と堅牢性が向上しました。 tinlvar(t)
は、インライン化された関数内で使用される一時変数(インライン変数)を生成する関数です。レシーバもこの一時変数に割り当てられます。
- 変更前は、レシーバの割り当てと通常の引数の割り当てが同じ
-
引数割り当てロジックの改善:
OAS2
(多重代入)ノードを使用して引数を割り当てるロジックが修正されました。- 特に、
fn->type->intuple > 1 && n->list && !n->list->next
という条件(複数の戻り値を持つ関数が単一の引数として渡されるケース)が以前はTODOとしてマークされており、正しく処理されていませんでした。 - 新しいコードでは、
chkargcount
というフラグを導入し、引数の数をより正確に追跡するようになりました。 ll = ll->next;
のようなリストのトラバースが、引数の数と型の整合性を保ちながら行われるようになりました。fatal("arg count mismatch: %#T vs %,H\\n", getinargx(fn->type), n->list);
のような厳密な引数カウントのチェックが追加され、コンパイラが不正な状態に陥るのを防ぎます。
-
非メソッド呼び出しからメソッドへの呼び出しの処理:
if(fn->type->thistuple && n->left->op != ODOTMETH)
のブロックは、メソッドとして定義されているが、通常の関数呼び出しのようにレシーバが明示的に引数として渡されるケース(例:(*Type).Method(receiver, arg1, ...)
)を処理します。- この場合も、レシーバが
as->list
に追加され、ll
(引数リストのイテレータ)が適切に進められることで、引数の割り当てが正しく行われるようになります。
-
新しいテストケース
test/fixedbugs/issue4167.go
の追加:- このテストケースは、修正されたバグを再現し、修正が正しく機能することを確認するために追加されました。
pa
型とp
型を定義し、func1
が複数の戻り値を返し、func2
がその戻り値を(*p).func3
メソッドの引数として渡すという、まさに問題となっていたシナリオを再現しています。func2dots
とfunc3dots
は、可変長引数を持つ関数のインライン化が意図的に行われないことを確認するためのテストです。これにより、将来的に可変長引数関数のインライン化が誤って有効になった場合に、このテストが失敗することで問題が早期に発見されるようになります。
これらの変更により、コンパイラは複数の戻り値を持つ関数の結果をメソッドの引数としてインライン化する際に、引数の割り当てを正確に処理できるようになり、内部エラーの発生を防ぎます。
コアとなるコードの変更箇所
このコミットで変更された主要なファイルは以下の2つです。
-
src/cmd/gc/inl.c
:- Goコンパイラのインライン化処理を行うC言語のソースファイルです。
mkinlcall1
関数内で、メソッド呼び出しのレシーバと引数の割り当てロジックが大幅に修正されました。- 具体的には、レシーバの割り当て部分がより明確に分離され、複数の戻り値を持つ関数が引数として渡される場合の処理が改善されました。
- 引数の数をチェックするロジックが強化され、
fatal
エラーによる早期検出が追加されました。
-
test/fixedbugs/issue4167.go
:- 新しいテストファイルで、
issue4167
で報告されたバグを再現し、修正が正しく機能することを確認します。 - 複数の戻り値を返す関数をメソッドの引数として使用するシナリオが含まれています。
- 可変長引数を持つ関数のインライン化が意図的に行われないことを確認するためのテストも含まれています。
- 新しいテストファイルで、
コアとなるコードの解説
src/cmd/gc/inl.c
の変更点
mkinlcall1
関数は、インライン化される関数呼び出しのASTノードを操作し、呼び出し元のASTに組み込む役割を担っています。
変更前:
// assign arguments to the parameters' temp names
as = N;
if(fn->type->thistuple) {
// ... レシーバと引数の割り当てが混在 ...
}
as = nod(OAS2, N, N);
if(fn->type->intuple > 1 && n->list && !n->list->next) {
// TODO check that n->list->n is a call?
// TODO: non-method call to T.meth(f()) where f returns t, args...
as->rlist = n->list;
for(t = getinargx(fn->type)->type; t; t=t->down)
as->list = list(as->list, tinlvar(t));
} else {
// ... 通常の引数割り当て ...
}
変更前のコードでは、レシーバの割り当てと引数の割り当てが同じif(fn->type->thistuple)
ブロック内で処理されており、特にTODO
コメントがある部分が、複数の戻り値を持つ関数が引数として渡されるケースを適切に処理できていませんでした。OAS2
ノードのrlist
にn->list
(呼び出しの引数リスト)を直接割り当てようとしていますが、これは複数の戻り値を持つ関数の結果を正しく展開して個々の引数に割り当てるには不十分でした。
変更後:
int chkargcount; // 新しく追加された変数
// assign receiver.
if(fn->type->thistuple && n->left->op == ODOTMETH) {
// method call with a receiver.
// ... レシーバの割り当てロジックが独立して処理される ...
as = nod(OAS, tinlvar(t), n->left->left);
if(as != N) {
typecheck(&as, Etop);
ninit = list(ninit, as);
}
}
// assign arguments to the parameters' temp names
as = nod(OAS2, N, N);
as->rlist = n->list; // 呼び出しの引数リストを右辺に設定
ll = n->list; // 引数リストのイテレータ
// TODO: if len(nlist) == 1 but multiple args, check that n->list->n is a call?
if(fn->type->thistuple && n->left->op != ODOTMETH) {
// non-method call to method
// ... レシーバが明示的に引数として渡される場合の処理 ...
as->list = list(as->list, tinlvar(t)); // レシーバのinlvarを左辺に追加
ll = ll->next; // 引数カウントを進める
}
// append ordinary arguments to LHS.
chkargcount = n->list && n->list->next; // 複数の引数があるかどうかのフラグ
for(t = getinargx(fn->type)->type; t && (!chkargcount || ll); t=t->down) {
if(chkargcount && ll) {
// len(n->list) > 1, count arguments.
ll=ll->next;
}
as->list = list(as->list, tinlvar(t)); // 通常の引数のinlvarを左辺に追加
}
if(chkargcount && (ll || t))
fatal("arg count mismatch: %#T vs %,H\\n", getinargx(fn->type), n->list);
変更後のコードでは、まずレシーバの割り当てが独立したブロックで行われます。これにより、メソッド呼び出しのレシーバがtinlvar(t)
に正しく割り当てられ、ninit
(初期化ステートメントのリスト)に追加されます。
次に、引数の割り当てのために新しいOAS2
ノードが作成され、その右辺(rlist
)には元の呼び出しの引数リストn->list
が設定されます。左辺(list
)には、インライン化される関数のパラメータに対応する一時変数(tinlvar(t)
)が追加されます。
重要なのは、chkargcount
フラグとfor
ループの条件です。これにより、引数の数が正確にカウントされ、複数の戻り値を持つ関数が引数として渡された場合でも、その戻り値が正しく個々のパラメータに割り当てられるように、OAS2
ノードの左辺と右辺が構築されます。
最後に、fatal
呼び出しによる厳密な引数カウントのミスマッチチェックが追加され、コンパイラが不正な状態になることを防ぎます。
test/fixedbugs/issue4167.go
の解説
このテストファイルは、Goコンパイラのバグ修正を検証するために作成されました。
// run
// Copyright 2012 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 4167: inlining of a (*T).Method expression taking
// its arguments from a multiple return breaks the compiler.
package main
type pa []int
type p int
func (this *pa) func1() (v *p, c int) {
for _ = range *this {
c++
}
v = (*p)(&c)
return
}
func (this *pa) func2() p {
return (*p).func3(this.func1())
}
func (this *p) func3(f int) p {
return *this
}
func (this *pa) func2dots() p {
return (*p).func3(this.func1())
}
func (this *p) func3dots(f ...int) p {
return *this
}
func main() {
arr := make(pa, 13)
length := arr.func2()
if int(length) != len(arr) {
panic("length != len(arr)")
}
length = arr.func2dots()
if int(length) != len(arr) {
panic("length != len(arr)")
}
}
pa
とp
というカスタム型が定義されています。func1()
は*pa
のメソッドで、*p
とint
の2つの値を返します。これが複数の戻り値を持つ関数です。func3()
は*p
のメソッドで、int
型の引数を一つ取ります。func2()
は*pa
のメソッドで、(*p).func3(this.func1())
という呼び出しを含んでいます。ここで、this.func1()
が返す複数の戻り値が、func3
の単一の引数に渡されようとしています。Go言語では、複数の戻り値を持つ関数を、その戻り値の数と型が一致する複数の引数を持つ関数に直接渡すことができます。このケースが、インライン化時にコンパイラを混乱させていた原因です。func2dots()
とfunc3dots()
は、可変長引数(...int
)を持つfunc3dots
がインライン化されないことを確認するためのものです。func2dots
からfunc3dots
を呼び出す際も、func1()
の複数の戻り値が渡されますが、func3dots
が可変長引数であるため、この呼び出しはインライン化されません。
このテストが成功するということは、func2()
の呼び出しが正しくインライン化され、func1()
の複数の戻り値がfunc3()
の引数に正しくマッピングされるようになったことを意味します。また、func2dots()
の呼び出しも正しく動作し、可変長引数を持つ関数がインライン化されないというコンパイラの挙動が期待通りであることを確認します。
関連リンク
- Go Issue 4167: https://github.com/golang/go/issues/4167
- Go Code Review 6871043: https://golang.org/cl/6871043
参考にした情報源リンク
- Go言語の公式ドキュメント (特に、関数、メソッド、複数の戻り値、可変長引数に関するセクション)
- Goコンパイラのソースコード (特に
src/cmd/gc/inl.c
の周辺コード) - Go言語のIssueトラッカー (Issue 4167の議論)
- Go言語のインライン化に関する一般的な情報源 (ブログ記事、技術解説など)