[インデックス 16547] ファイルの概要
このコミットは、Goコンパイラ(cmd/gc
)におけるインライン化の挙動を修正するものです。具体的には、関数がコンパイルされた後に発生するgenwrappers
でのインライン化において発生する問題を解決し、ローカル変数のリストが不適切に削除されることによる誤ったコンパイルを防ぎます。
コミット
commit ae5e791ed20076bf67e5da20fee769ec86a7a969
Author: Ian Lance Taylor <iant@golang.org>
Date: Tue Jun 11 20:23:21 2013 -0700
cmd/gc: save local var list before inlining
This avoids problems with inlining in genwrappers, which
occurs after functions have been compiled. Compiling a
function may cause some unused local vars to be removed from
the list. Since a local var may be unused due to
optimization, it is possible that a removed local var winds up
beingused in the inlined version, in which case hilarity
ensues.
Fixes #5515.
R=golang-dev, khr, dave
CC=golang-dev
https://golang.org/cl/10210043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/ae5e791ed20076bf67e5da20fee769ec86a7a969
元コミット内容
cmd/gc: save local var list before inlining
このコミットは、Goコンパイラ(cmd/gc
)において、インライン化を行う前にローカル変数のリストを保存するように変更します。これにより、genwrappers
でのインライン化時に発生する問題を回避します。genwrappers
は関数がコンパイルされた後に実行されるため、コンパイル過程で最適化により不要なローカル変数がリストから削除される可能性があります。しかし、削除されたローカル変数がインライン化されたコード内で使用される場合、予期せぬ問題("hilarity ensues")が発生する可能性がありました。この変更は、Go issue 5515を修正します。
変更の背景
この変更は、Goコンパイラにおける特定のバグ、具体的にはGo issue 5515を修正するために導入されました。この問題は、メソッドラッパーの生成(genwrappers
)時にインライン化が行われる際に発生する誤ったコンパイルに関連していました。
Goコンパイラは、コードの最適化の一環として、小さな関数を呼び出し元に直接展開する「インライン化」を行います。これにより、関数呼び出しのオーバーヘッドを削減し、パフォーマンスを向上させることができます。しかし、このインライン化のプロセスが、関数のコンパイル後に実行されるgenwrappers
というフェーズと組み合わさることで問題が生じていました。
genwrappers
は、インターフェースメソッドの呼び出しや、特定の型に対するメソッドのディスパッチを効率的に行うためのラッパー関数を生成する役割を担っています。このラッパー関数内でインライン化が行われる際、元の関数が既にコンパイルされ、その過程で「未使用」と判断されたローカル変数がリストから削除されている可能性がありました。
問題は、コンパイラの最適化によって「未使用」と判断され削除されたローカル変数が、実際にはインライン化されたコード内で必要とされる場合に発生しました。このような状況では、コンパイラが参照すべき変数の情報を持たないため、誤ったコードが生成され、プログラムのクラッシュや予期せぬ動作につながる可能性がありました。
このコミットは、インライン化の前にローカル変数の完全なリストを保持することで、この問題を根本的に解決しようとするものです。
前提知識の解説
このコミットを理解するためには、以下のGoコンパイラの概念と関連技術についての知識が必要です。
- Goコンパイラ (
cmd/gc
): Go言語の公式コンパイラであり、Goのソースコードを機械語に変換する役割を担っています。コンパイル過程には、構文解析、型チェック、最適化、コード生成など、複数のフェーズが含まれます。 - インライン化 (Inlining): コンパイラ最適化の一種で、小さな関数や頻繁に呼び出される関数を、その呼び出し箇所に直接展開する技術です。これにより、関数呼び出しのオーバーヘッド(スタックフレームの作成、引数の渡し、戻り値の処理など)を削減し、プログラムの実行速度を向上させます。インライン化されたコードは、より大きなブロックとして最適化される機会も増えます。
genwrappers
: Goコンパイラの内部フェーズの一つで、特にインターフェースメソッドの呼び出しや、特定の型に対するメソッドのディスパッチを効率的に行うための「ラッパー関数」を生成します。Goのインターフェースは動的なディスパッチを可能にしますが、その実装にはある程度のオーバーヘッドが伴います。genwrappers
は、このオーバーヘッドを削減するために、具体的な型に応じた最適化されたラッパーコードを生成することがあります。- ローカル変数 (Local Variables): 関数内で宣言され、その関数スコープ内でのみ有効な変数です。コンパイラは、これらの変数のメモリ割り当てや使用状況を管理します。
- コンパイラの最適化: コンパイラが生成する機械語コードの効率を向上させるための様々な技術の総称です。これには、デッドコード削除(未使用のコードの削除)、定数畳み込み、ループ最適化、レジスタ割り当てなどが含まれます。ローカル変数が「未使用」と判断されるのは、このような最適化の一環です。
Node
構造体: GoコンパイラのAST(Abstract Syntax Tree: 抽象構文木)におけるノードを表す内部構造体です。Goのプログラムは、コンパイルの初期段階でこのASTに変換され、コンパイラの各フェーズはこのASTを操作して最適化やコード生成を行います。Node
構造体には、関数、変数、式など、プログラムの様々な要素に関する情報が格納されます。NodeList
:Node
のリストを表すコンパイラ内部のデータ構造です。このコミットでは、関数のローカル変数リスト(dcl
)やインライン化用のローカル変数リスト(inldcl
)がNodeList
として管理されます。
技術的詳細
このコミットの技術的な核心は、Goコンパイラの内部データ構造とインライン化処理の変更にあります。
-
Node
構造体へのinldcl
フィールドの追加:src/cmd/gc/go.h
ファイルにおいて、Node
構造体にNodeList* inldcl;
という新しいフィールドが追加されました。dcl
: 既存のフィールドで、その関数またはクロージャの自動宣言(ローカル変数)のリストを保持します。inl
: 既存のフィールドで、インライン化のために使用される関数のボディのコピーを保持します。inldcl
: 新しく追加されたフィールドで、インライン化のために使用されるローカル変数のリストのコピーを保持します。
この
inldcl
フィールドの導入が、今回の修正の鍵となります。コンパイル過程でdcl
リストから未使用の変数が削除される可能性があるため、インライン化の際には、コンパイル前の完全なローカル変数リストのコピーをinldcl
として保持することで、後続のインライン化処理で参照される可能性のある変数が失われることを防ぎます。 -
caninl
関数におけるinldcl
のコピー:src/cmd/gc/inl.c
ファイル内のcaninl
関数(インライン化が可能かどうかを判断し、インライン化に必要な準備を行う関数)が変更されました。 変更前は、関数のボディ(fn->nbody
)のみがfn->nname->inl
にコピーされていましたが、変更後は以下の行が追加されました。fn->nname->inldcl = inlcopylist(fn->nname->defn->dcl);
この行は、関数の定義(
fn->nname->defn
)が持つローカル変数リスト(dcl
)をinlcopylist
関数を使ってディープコピーし、その結果を新しく追加されたfn->nname->inldcl
フィールドに格納します。これにより、インライン化の準備段階で、ローカル変数の完全なスナップショットが取得されます。 -
mkinlcall1
関数におけるローカル変数リストの参照変更:src/cmd/gc/inl.c
ファイル内のmkinlcall1
関数(インライン化された関数呼び出しを生成する関数)が変更されました。 変更前は、ローカル関数(fn->defn
が存在する場合)のローカル変数リストとしてfn->defn->dcl
を参照していました。- if (fn->defn) // local function - dcl = fn->defn->dcl;
変更後は、新しく保存された
fn->inldcl
を参照するように変更されました。+ if(fn->defn) // local function + dcl = fn->inldcl;
この変更により、インライン化されたコードがローカル変数を参照する際に、コンパイル後の最適化によって変更された可能性のある
dcl
ではなく、インライン化前に保存された完全なinldcl
リストを使用するようになります。これにより、最適化によって削除された変数がインライン化されたコードで必要とされた場合に発生する問題を回避します。
これらの変更は、Goコンパイラのインライン化処理の堅牢性を高め、特にgenwrappers
のようなコンパイル後フェーズでのインライン化における潜在的なバグを修正することを目的としています。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更箇所は以下の3ファイルにまたがっています。
-
src/cmd/gc/go.h
:Node
構造体に新しいフィールドが追加されました。--- a/src/cmd/gc/go.h +++ b/src/cmd/gc/go.h @@ -284,6 +284,7 @@ struct Node NodeList* cvars; // closure params NodeList* dcl; // autodcl for this func/closure NodeList* inl; // copy of the body for use in inlining + NodeList* inldcl; // copy of dcl for use in inlining // OLITERAL/OREGISTER Val val;
-
src/cmd/gc/inl.c
:caninl
関数とmkinlcall1
関数が変更されました。-
caninl
関数内でのinldcl
へのコピー処理の追加:--- a/src/cmd/gc/inl.c +++ b/src/cmd/gc/inl.c @@ -146,6 +146,7 @@ caninl(Node *fn) fn->nname->inl = fn->nbody; fn->nbody = inlcopylist(fn->nname->inl); + fn->nname->inldcl = inlcopylist(fn->nname->defn->dcl); // hack, TODO, check for better way to link method nodes back to the thing with the ->inl // this is so export can find the body of a method
-
mkinlcall1
関数内でのローカル変数リストの参照変更:--- a/src/cmd/gc/inl.c +++ b/src/cmd/gc/inl.c @@ -559,8 +560,8 @@ mkinlcall1(Node **np, Node *fn, int isddd) //dumplist("ninit pre", ninit); - if (fn->defn) // local function - dcl = fn->defn->dcl; + if(fn->defn) // local function + dcl = fn->inldcl; else // imported function dcl = fn->dcl;
-
-
test/fixedbugs/issue5515.go
: このコミットによって修正されるバグを再現するための新しいテストケースが追加されました。--- /dev/null +++ b/test/fixedbugs/issue5515.go @@ -0,0 +1,34 @@ +// 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 5515: miscompilation doing inlining in generated method wrapper + +package main + +type T uint32 + +func main() { + b := make([]T, 8) + b[0] = 0xdeadbeef + rs := Slice(b) + sort(rs) +} + +type Slice []T + +func (s Slice) Swap(i, j int) { + tmp := s[i] + s[i] = s[j] + s[j] = tmp +} + +type Interface interface { + Swap(i, j int) +} + +func sort(data Interface) { + data.Swap(0, 4) +}
コアとなるコードの解説
このコミットの核心は、Goコンパイラのインライン化処理におけるローカル変数の管理方法の改善にあります。
-
Node
構造体へのinldcl
の追加: Goコンパイラは、プログラムの各要素をNode
構造体として表現します。関数もまたNode
として扱われ、その定義に関する情報(ローカル変数リストなど)を保持します。 これまでのNode
構造体には、関数のローカル変数リストを指すdcl
フィールドがありました。しかし、このdcl
リストはコンパイルの過程で最適化によって変更される可能性がありました。特に、未使用と判断されたローカル変数がこのリストから削除されることがありました。 今回追加されたinldcl
フィールドは、インライン化のために、関数のローカル変数リストの「オリジナルのコピー」を保持するためのものです。これにより、コンパイル後の最適化によってdcl
が変更されても、インライン化処理は常に完全なローカル変数リストを参照できるようになります。 -
caninl
関数でのinldcl
の生成:caninl
関数は、ある関数がインライン化可能であるかを判断し、インライン化に必要な準備を行うコンパイラ内部の関数です。この関数内で、インライン化される関数のボディがコピーされるのと同様に、そのローカル変数リストもinlcopylist
関数を使ってディープコピーされ、inldcl
フィールドに格納されます。inlcopylist
は、リスト内の各要素を再帰的にコピーする関数であり、これにより元のdcl
リストへの参照ではなく、独立したコピーがinldcl
に保持されることが保証されます。このステップが、インライン化時のローカル変数参照の整合性を保つ上で非常に重要です。 -
mkinlcall1
関数でのinldcl
の利用:mkinlcall1
関数は、実際にインライン化された関数呼び出しのコードを生成する役割を担っています。この関数内で、インライン化される関数のローカル変数リストを参照する際に、これまではfn->defn->dcl
(コンパイル後のローカル変数リスト)を使用していました。 しかし、このコミットにより、参照先がfn->inldcl
(インライン化前に保存されたローカル変数リストのコピー)に変更されました。 この変更により、インライン化されたコードが、最適化によって削除された可能性のあるローカル変数を参照しようとした場合でも、inldcl
に保存されている完全なリストからその情報を見つけることができるようになります。これにより、Go issue 5515で報告されたような、インライン化と最適化の相互作用によって引き起こされる誤ったコンパイルが防止されます。
要するに、この変更は、インライン化処理が、コンパイル後の最適化によって変更されたローカル変数リストではなく、インライン化の判断が下された時点でのローカル変数リストの完全なスナップショットを使用するようにすることで、コンパイラの堅牢性を向上させています。
関連リンク
- Go issue 5515: https://github.com/golang/go/issues/5515
- Go CL 10210043: https://golang.org/cl/10210043
参考にした情報源リンク
- Go issue 5515のGitHubページ
- Go CL 10210043のGerritページ
- Goコンパイラのソースコード(
src/cmd/gc/go.h
,src/cmd/gc/inl.c
) - Go言語のコンパイラに関する一般的なドキュメントや解説記事 (Goコンパイラの内部構造、インライン化、最適化に関する情報)
- Go言語のインターフェースとメソッドディスパッチに関するドキュメント