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

[インデックス 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コンパイラの内部データ構造とインライン化処理の変更にあります。

  1. Node構造体へのinldclフィールドの追加: src/cmd/gc/go.hファイルにおいて、Node構造体にNodeList* inldcl;という新しいフィールドが追加されました。

    • dcl: 既存のフィールドで、その関数またはクロージャの自動宣言(ローカル変数)のリストを保持します。
    • inl: 既存のフィールドで、インライン化のために使用される関数のボディのコピーを保持します。
    • inldcl: 新しく追加されたフィールドで、インライン化のために使用されるローカル変数のリストのコピーを保持します。

    このinldclフィールドの導入が、今回の修正の鍵となります。コンパイル過程でdclリストから未使用の変数が削除される可能性があるため、インライン化の際には、コンパイル前の完全なローカル変数リストのコピーをinldclとして保持することで、後続のインライン化処理で参照される可能性のある変数が失われることを防ぎます。

  2. 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フィールドに格納します。これにより、インライン化の準備段階で、ローカル変数の完全なスナップショットが取得されます。

  3. 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ファイルにまたがっています。

  1. 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;
    
  2. 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;
       
      
  3. 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コンパイラのインライン化処理におけるローカル変数の管理方法の改善にあります。

  1. Node構造体へのinldclの追加: Goコンパイラは、プログラムの各要素をNode構造体として表現します。関数もまたNodeとして扱われ、その定義に関する情報(ローカル変数リストなど)を保持します。 これまでのNode構造体には、関数のローカル変数リストを指すdclフィールドがありました。しかし、このdclリストはコンパイルの過程で最適化によって変更される可能性がありました。特に、未使用と判断されたローカル変数がこのリストから削除されることがありました。 今回追加されたinldclフィールドは、インライン化のために、関数のローカル変数リストの「オリジナルのコピー」を保持するためのものです。これにより、コンパイル後の最適化によってdclが変更されても、インライン化処理は常に完全なローカル変数リストを参照できるようになります。

  2. caninl関数でのinldclの生成: caninl関数は、ある関数がインライン化可能であるかを判断し、インライン化に必要な準備を行うコンパイラ内部の関数です。この関数内で、インライン化される関数のボディがコピーされるのと同様に、そのローカル変数リストもinlcopylist関数を使ってディープコピーされ、inldclフィールドに格納されます。 inlcopylistは、リスト内の各要素を再帰的にコピーする関数であり、これにより元のdclリストへの参照ではなく、独立したコピーがinldclに保持されることが保証されます。このステップが、インライン化時のローカル変数参照の整合性を保つ上で非常に重要です。

  3. mkinlcall1関数でのinldclの利用: mkinlcall1関数は、実際にインライン化された関数呼び出しのコードを生成する役割を担っています。この関数内で、インライン化される関数のローカル変数リストを参照する際に、これまではfn->defn->dcl(コンパイル後のローカル変数リスト)を使用していました。 しかし、このコミットにより、参照先がfn->inldcl(インライン化前に保存されたローカル変数リストのコピー)に変更されました。 この変更により、インライン化されたコードが、最適化によって削除された可能性のあるローカル変数を参照しようとした場合でも、inldclに保存されている完全なリストからその情報を見つけることができるようになります。これにより、Go issue 5515で報告されたような、インライン化と最適化の相互作用によって引き起こされる誤ったコンパイルが防止されます。

要するに、この変更は、インライン化処理が、コンパイル後の最適化によって変更されたローカル変数リストではなく、インライン化の判断が下された時点でのローカル変数リストの完全なスナップショットを使用するようにすることで、コンパイラの堅牢性を向上させています。

関連リンク

参考にした情報源リンク

  • Go issue 5515のGitHubページ
  • Go CL 10210043のGerritページ
  • Goコンパイラのソースコード(src/cmd/gc/go.h, src/cmd/gc/inl.c
  • Go言語のコンパイラに関する一般的なドキュメントや解説記事 (Goコンパイラの内部構造、インライン化、最適化に関する情報)
  • Go言語のインターフェースとメソッドディスパッチに関するドキュメント