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

[インデックス 15014] ファイルの概要

このコミットは、Go言語の実験的なSSA (Static Single Assignment) パッケージ exp/ssa における重要な変更を導入しています。主な目的は、関数のパラメータの扱い方を根本的に変更し、より効率的なSSA形式の構築と最適化を可能にすることです。具体的には、パラメータを「値」として扱うようにし、初期のSSA構築段階で全てのパラメータを明示的にスタックフレームに「スピル」する戦略を採用しています。これにより、ローカルな Alloc (スタック上の変数) が Capture (クロージャなどによる外部参照) を介してエスケープするケースのハンドリングも改善されています。

副次的な変更として、BasicBlock のサクセッサ(後続ブロック)を格納するスライス Succs のためのメモリをインラインで確保する最適化も含まれています。

コミット

commit aa0b573ad610ea659902c8a54183e9fa30d8380e
Author: Alan Donovan <adonovan@google.com>
Date:   Tue Jan 29 10:49:16 2013 -0500

    exp/ssa: make Parameters values, not addresses.
    
    We explicitly spill all parameters to the frame during initial
    SSA construction.  (Later passes will remove spills.)
    We now properly handle local Allocs escaping via Captures.
    
    Also: allocate BasicBlock.Succs inline.
    
    R=iant, iant
    CC=golang-dev
    https://golang.org/cl/7231050

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/aa0b573ad610ea659902c8a54183e9fa30d8380e

元コミット内容

このコミットの元の内容は以下の通りです。

  • 件名: exp/ssa: make Parameters values, not addresses.
  • 本文:
    • 初期SSA構築時に、全てのパラメータを明示的にスタックフレームにスピルする。
    • 後続の最適化パスでこれらのスピルは除去される。
    • Capture を介してエスケープするローカルな Alloc のハンドリングを適切に行うようになった。
    • BasicBlock.Succs をインラインでアロケートする。
  • レビューア: iant
  • CC: golang-dev
  • Gerrit Change-ID: https://golang.org/cl/7231050

変更の背景

このコミットの背景には、コンパイラのSSA中間表現におけるパラメータの扱いの改善と、メモリ割り当ての最適化という二つの主要な動機があります。

  1. パラメータの表現の統一と最適化の促進: 従来の exp/ssa パッケージでは、関数のパラメータが「アドレス」(ポインタ)として表現されていました。これは、パラメータがメモリ上のどこかに存在するという概念を直接的に反映していますが、SSA形式の目的である「各変数が一度だけ代入される」という原則とは必ずしも整合しません。パラメータを最初から「値」として扱い、必要に応じてスタックにスピル(一時的にメモリに書き出すこと)することで、SSAの最適化パス(特にレジスタ割り当てやスピル除去)がより効果的に機能するようになります。初期段階で全てをスピルすることで、後続のパスが不要なスピルを特定し、除去する機会が増え、最終的なコードの効率が向上します。

  2. エスケープ解析とクロージャの改善: Go言語では、変数がそのスコープを越えて参照される「エスケープ」という概念があります。特にクロージャ(Goでは「関数リテラル」)は、外部のローカル変数を「キャプチャ」することがあります。従来のパラメータがアドレスとして扱われるモデルでは、パラメータがエスケープする際の処理が複雑になる可能性がありました。パラメータを値として扱い、必要に応じて Alloc (スタック上のローカル変数) としてスピルすることで、エスケープ解析と Capture の処理がより統一的かつ正確に行えるようになります。これにより、クロージャが外部変数を参照する際のメモリ管理が改善されます。

  3. メモリ割り当ての効率化: BasicBlock のサクセッサ(後続ブロック)のリストは、多くのブロックで要素数が少ない(通常0, 1, または2)という特性があります。スライスを動的に割り当てる代わりに、BasicBlock 構造体内に小さな固定サイズの配列(succs2)をインラインで持つことで、ヒープ割り当てのオーバーヘッドを削減し、キャッシュ効率を向上させることができます。これは、コンパイラのパフォーマンス向上に寄与します。

これらの変更は、GoコンパイラのSSAバックエンドの堅牢性と最適化能力を高めるための重要なステップと言えます。

前提知識の解説

このコミットを理解するためには、以下の技術的な概念を把握しておく必要があります。

  1. SSA (Static Single Assignment) 形式:

    • 定義: SSA形式は、コンパイラの中間表現(IR)の一種で、プログラム中の各変数が一度だけ代入されるように変換されたものです。例えば、x = a + b; x = x + 1; はSSAでは x1 = a + b; x2 = x1 + 1; のようになります。
    • 利点: SSA形式は、データフロー解析や最適化(デッドコード削除、定数伝播、共通部分式除去、レジスタ割り当てなど)を大幅に簡素化し、効率的に行えるようにします。変数の定義と使用の関係が明確になるため、コンパイラがプログラムの振る舞いをより正確に推論できます。
    • GoコンパイラにおけるSSA: Goコンパイラは、Go 1.7以降、SSA形式を主要な中間表現として採用しています。exp/ssa パッケージは、その初期の実験的な実装の一部です。
  2. コンパイラのバックエンドと中間表現:

    • フロントエンド: ソースコードを解析し、抽象構文木(AST)などの初期の中間表現を生成します。
    • 中間表現 (IR): コンパイラが最適化やコード生成を行うために使用する、プログラミング言語に依存しない形式です。SSAはその一種です。
    • バックエンド: 中間表現を最適化し、最終的な機械語コードやアセンブリコードを生成します。
  3. パラメータとスタックフレーム:

    • パラメータ: 関数が呼び出される際に渡される引数です。
    • スタックフレーム: 関数が呼び出されるたびに、その関数に必要なローカル変数、パラメータ、リターンアドレスなどを格納するためにスタック上に確保されるメモリ領域です。
    • スピル (Spilling): レジスタに収まらない変数や、アドレスが取られる変数などを、一時的にメモリ(通常はスタックフレーム)に書き出す操作を指します。後続の最適化パス(レジスタ割り当てなど)で、これらのスピルが不要であれば除去されます。
  4. エスケープ解析 (Escape Analysis):

    • Goコンパイラが行う重要な最適化の一つです。変数がその宣言されたスコープを越えて参照される可能性があるかどうかを分析します。
    • もし変数がエスケープすると判断された場合、その変数はスタックではなくヒープに割り当てられる必要があります。これにより、関数がリターンした後も変数が有効な状態を保つことができます。
    • エスケープ解析は、ガベージコレクションの負荷を軽減し、プログラムのパフォーマンスを向上させるために不可欠です。
  5. AllocCapture:

    • Alloc: SSA形式において、スタック上に割り当てられたローカル変数を表す命令または値です。通常、そのアドレスがSSAグラフ内で使用されます。
    • Capture: クロージャ(関数リテラル)が外部スコープの変数を参照する際に生成されるSSA要素です。Capture は、キャプチャされた変数のアドレスへのポインタとして機能します。このコミット以前は Parameter もキャプチャの対象になり得ましたが、変更後は Alloc のみが対象となります。
  6. BasicBlock と制御フロー:

    • BasicBlock (基本ブロック): 制御フローグラフ(CFG)の基本的な構成要素です。単一の入り口と単一の出口を持ち、内部に分岐命令を含まない命令のシーケンスです。
    • Preds (Predecessors): その基本ブロックに制御が移る可能性のある先行ブロックのリスト。
    • Succs (Successors): その基本ブロックから制御が移る可能性のある後続ブロックのリスト。
    • 制御フローグラフ (CFG): プログラムの実行パスを基本ブロックとそれらの間の遷移(エッジ)で表現したグラフです。

これらの概念を理解することで、コミットがGoコンパイラの内部でどのように機能し、どのような影響を与えるのかを深く把握できます。

技術的詳細

このコミットの技術的詳細は、主に exp/ssa パッケージ内の ParameterAllocCapture の定義と、それらがSSA構築および最適化パスでどのように扱われるかの変更に集約されます。

  1. Parameter の「値」化:

    • 変更前: src/pkg/exp/ssa/ssa.goParameter 構造体には Type_ *types.PointerHeap bool フィールドがありました。これは、パラメータが常にポインタ型であり、ヒープに割り当てられる可能性があることを示唆していました。コメントも「Parameters are addresses and thus have pointer types.」と明記されていました。
    • 変更後: Parameter 構造体から Heap bool フィールドが削除され、Type_types.Type に変更されました。これにより、Parameter はその基となるGoの型の「値」そのものを表すようになりました。これは、SSA形式の変数が値であるという原則に合致します。
    • 影響: パラメータが値として扱われることで、レジスタ割り当てなどの最適化パスが、パラメータを直接レジスタに割り当てたり、不要なメモリ操作を省略したりする機会が増えます。
  2. 明示的なパラメータのスピル (addSpilledParam):

    • 新しい戦略: パラメータが値として扱われるようになったため、そのアドレスが必要な場合(例えば、エスケープする場合や、メモリ上の場所が必要な場合)には、明示的にスタックフレームにスピルする必要があります。
    • src/pkg/exp/ssa/func.go の変更:
      • addObjParam 関数が削除され、代わりに addSpilledParam 関数が導入されました。
      • addSpilledParam は、与えられた types.Object (Goの変数やフィールド) に対して、まず Parameter (値) を作成し、次にそのパラメータを格納するための Alloc (スタック上のメモリ割り当て) を作成します。
      • そして、emit メソッドを使って Alloc と、Parameter の値を Alloc に格納する Store 命令をSSAグラフに追加します。
      • このプロセスにより、関数のレシーバや引数は、SSA構築の初期段階で全てスタックにスピルされた状態になります。
    • 最適化との連携: コミットメッセージにあるように、「Later passes will remove spills.」とあるように、後続の最適化パス(特にレジスタ割り当てやスピル除去)が、これらの明示的なスピルを分析し、不要なものを除去したり、レジスタに割り当て直したりします。これにより、最終的な機械語コードは効率的になります。
  3. Capture のエスケープハンドリングの改善:

    • 変更前: src/pkg/exp/ssa/func.golookup 関数内で、Capture がエスケープする場合の処理が不完全でした(コメントで TODO があった)。ParameterAlloc がエスケープする場合は Heap = true とマークされていました。
    • 変更後: lookup 関数内のエスケープ処理が変更されました。Capture の場合は、その Outer (キャプチャ元の値) を辿っていき、最終的に Alloc に到達します。そして、その AllocHeap = true とマークします。
    • 理由: パラメータが値として扱われ、必要に応じて Alloc にスピルされるようになったため、Capture が参照するのは最終的に Alloc になります。この変更により、クロージャが外部変数をキャプチャし、その変数がエスケープする場合の処理がより正確かつ統一的に行えるようになりました。
  4. BasicBlock.Succs のインライン割り当て:

    • 変更前: BasicBlock 構造体には Succs []*BasicBlock フィールドがあり、これは通常のGoのスライスでした。スライスは動的にメモリを割り当てるため、小さなスライスでもヒープ割り当てが発生します。
    • 変更後: src/pkg/exp/ssa/ssa.goBasicBlock 構造体に succs2 [2]*BasicBlock という固定サイズの配列が追加されました。
    • 利用方法: src/pkg/exp/ssa/blockopt.gofuseBlocks 関数と src/pkg/exp/ssa/func.gonewBasicBlock 関数で、この succs2 配列が Succs スライスのバッキングストアとして利用されるようになりました。
      • b.Succs = b.succs2[:0] (新しいブロック作成時)
      • a.Succs = append(a.succs2[:0], b.Succs...) (ブロック結合時)
    • 効果: 多くの基本ブロックは0個、1個、または2個のサクセッサしか持たないため、この小さなインライン配列を再利用することで、ヒープ割り当ての回数を大幅に削減し、コンパイラの実行速度とメモリ効率を向上させます。これは、Goの標準ライブラリや他の高性能なコードでも見られる一般的な最適化パターンです。

これらの変更は、GoコンパイラのSSAバックエンドの設計思想を、よりSSA形式の原則に忠実で、かつ効率的なコード生成を可能にする方向へと進化させています。

コアとなるコードの変更箇所

このコミットにおける主要なコード変更は以下のファイルに集中しています。

  1. src/pkg/exp/ssa/ssa.go:

    • BasicBlock 構造体に succs2 [2]*BasicBlock フィールドが追加されました。
    • Parameter 構造体から Heap bool フィールドが削除され、Type_ の型が *types.Pointer から types.Type に変更されました。
    • Capture および Parameter 構造体に関するコメントが、変更されたセマンティクスに合わせて更新されました。
  2. src/pkg/exp/ssa/func.go:

    • addParam 関数の Type_ の型が pointer(typ) から typ に変更されました。
    • addObjParam 関数が削除されました。
    • addSpilledParam という新しい関数が追加されました。この関数は、パラメータを Parameter (値) として追加し、それを格納するための Alloc を作成し、Store 命令を発行することで、明示的にスタックにスピルします。
    • f.start 関数内で、レシーバと関数のパラメータを追加する際に、f.addObjParam の呼び出しが f.addSpilledParam に変更されました。
    • lookup 関数内で、escapingtrue の場合の Capture の処理ロジックが変更され、CaptureOuter を辿って最終的な AllocHeap=true にマークするようになりました。
    • DumpTo 関数で、関数の型表示が f.Type() から f.Signature に変更されました。
    • newBasicBlock 関数で、新しく作成される BasicBlockSuccs スライスが b.succs2[:0] を使って初期化されるようになりました。
  3. src/pkg/exp/ssa/blockopt.go:

    • fuseBlocks 関数内で、a.Succs = b.Succs の行が a.Succs = append(a.succs2[:0], b.Succs...) に変更されました。これにより、Succs スライスの結合時にインライン配列 succs2 が利用されるようになりました。
    • BasicBlock.Succs のインライン割り当てに関する古いコメントが削除されました。

これらの変更は、SSA中間表現のデータ構造と、それを構築・操作するロジックの両方に影響を与えています。

コアとなるコードの解説

ここでは、上記の変更箇所の中から特に重要な部分を抜粋し、その役割と意味を詳しく解説します。

src/pkg/exp/ssa/ssa.go における Parameter 構造体の変更

// 変更前
type Parameter struct {
	Name_ string
	Type_ *types.Pointer // address of param
	Heap  bool
}

// 変更後
type Parameter struct {
	Name_ string
	Type_ types.Type
}
  • Type_ *types.Pointer から types.Type への変更:

    • これは、Parameter がもはやメモリ上のアドレス(ポインタ)ではなく、Goの型システムにおける純粋な「値」を表すようになったことを意味します。
    • SSA形式では、各変数は一度だけ代入される「値」として扱われるのが理想です。この変更により、Goの関数のパラメータもこのSSAの原則に沿うようになりました。
    • これにより、コンパイラはパラメータをレジスタに直接割り当てたり、より柔軟な最適化を適用したりできるようになります。
  • Heap bool フィールドの削除:

    • 以前は、パラメータがヒープに割り当てられる可能性があることを示す Heap フラグがありました。
    • パラメータが値として扱われ、必要に応じて明示的にスタックにスピルされるようになったため、このフラグは不要になりました。ヒープへの割り当ては、エスケープ解析の結果、Alloc がヒープに割り当てられるべきと判断された場合にのみ行われます。

src/pkg/exp/ssa/func.go における addSpilledParam の導入

// 新しく追加された関数
func (f *Function) addSpilledParam(obj types.Object) {
	name := obj.GetName()
	param := f.addParam(name, obj.GetType()) // Parameter (値) を追加
	spill := &Alloc{
		Name_: name + "~", // "~" means "spilled"
		Type_: pointer(obj.GetType()),
	}
	f.objects[obj] = spill // GoのオブジェクトとスピルされたAllocを関連付け
	f.Locals = append(f.Locals, spill)
	f.emit(spill) // Alloc命令をSSAグラフに追加
	f.emit(&Store{Addr: spill, Val: param}) // Parameterの値をAllocに格納するStore命令を追加
}
  • 目的: この関数は、Goの関数パラメータ(レシーバや引数)をSSAグラフに追加する際に、それらを「値」として表現しつつ、同時にスタックフレームに明示的に「スピル」する役割を担います。
  • 処理の流れ:
    1. f.addParam(name, obj.GetType()) を呼び出し、Goのオブジェクト(変数)に対応する Parameter をSSAグラフに「値」として追加します。
    2. &Alloc{...} を使って、このパラメータの値を格納するためのスタック上のメモリ領域(Alloc)を作成します。Name_~ を付加することで、これがスピルされた変数であることを示唆しています。
    3. f.objects[obj] = spill により、元のGoのオブジェクトと、SSAグラフ内のスピルされた Alloc を関連付けます。これにより、後でこのオブジェクトを参照する際に、スピルされた場所が使われるようになります。
    4. f.emit(spill)Alloc 命令をSSAグラフに挿入します。これは、スタック上にメモリを割り当てることを表します。
    5. f.emit(&Store{Addr: spill, Val: param})Store 命令を挿入します。これは、先ほど作成した Parameter の「値」を、スピルされた Alloc のアドレスに書き込むことを表します。
  • 意義: このメカニズムにより、SSA構築の初期段階で全てのパラメータがメモリ上に「実体」を持つことになります。これにより、エスケープ解析やレジスタ割り当てなどの後続の最適化パスが、メモリ上のデータとレジスタ上のデータの両方を考慮して、より柔軟かつ効率的なコードを生成できるようになります。不要なスピルは後で除去されます。

src/pkg/exp/ssa/func.go における lookup 関数の Capture 処理の変更

// 変更後 (抜粋)
func (f *Function) lookup(obj types.Object, escaping bool) Value {
	if v, ok := f.objects[obj]; ok {
		if escaping {
			// Walk up the chain of Captures.
			x := v
			for {
				if c, ok := x.(*Capture); ok {
					x = c.Outer
				} else {
					break
				}
			}
			// By construction, all captures are ultimately Allocs in the
			// naive SSA form.  Parameters are pre-spilled to the stack.
			x.(*Alloc).Heap = true
		}
		return v // function-local var (address)
	}
    // ...
}
  • 目的: クロージャが外部変数をキャプチャし、そのキャプチャされた変数がエスケープする場合の処理を正確に行うことです。
  • 処理の流れ:
    1. if escaping ブロックに入ると、変数がエスケープすると判断された場合です。
    2. x := v で現在のSSA値 vx に代入します。
    3. for ループで、xCapture 型である限り、x をその Outer (キャプチャ元の値) に更新し続けます。これは、多重にネストされたクロージャで変数がキャプチャされている場合でも、最終的にその変数の「元の定義」に辿り着くためです。
    4. ループを抜けたとき、xCapture ではない、つまりキャプチャチェーンの終端にある値です。コミットのコメントにあるように、「By construction, all captures are ultimately Allocs in the naive SSA form. Parameters are pre-spilled to the stack.」であるため、この xAlloc 型であると仮定されます。
    5. x.(*Alloc).Heap = true により、最終的に辿り着いた Alloc をヒープに割り当てるべきであるとマークします。
  • 意義: この変更により、クロージャがキャプチャした変数がエスケープする場合でも、その変数がメモリ上で適切にヒープに割り当てられることが保証されます。これは、Goのクロージャのセマンティクスを正しく実装し、メモリ安全性を確保するために不可欠です。

src/pkg/exp/ssa/ssa.go における BasicBlock 構造体の変更と Succs の利用

// 変更後 (抜粋)
type BasicBlock struct {
	Name         string         // label; no semantic significance
	Func         *Function      // containing function
	Instrs       []Instruction  // instructions in order
	Preds, Succs []*BasicBlock  // predecessors and successors
	succs2       [2]*BasicBlock // initial space for Succs.
}
// src/pkg/exp/ssa/func.go の newBasicBlock 関数 (抜粋)
func (f *Function) newBasicBlock(name string) *BasicBlock {
	b := &BasicBlock{
		Name: fmt.Sprintf("%d.%s", len(f.Blocks), name),
		Func: f,
	}
	b.Succs = b.succs2[:0] // ここでインライン配列を利用
	f.Blocks = append(f.Blocks, b)
	return b
}
// src/pkg/exp/ssa/blockopt.go の fuseBlocks 関数 (抜粋)
func fuseBlocks(f *Function, a *BasicBlock) bool {
	// ...
	// A inherits B's successors
	a.Succs = append(a.succs2[:0], b.Succs...) // ここでインライン配列を利用
	// ...
}
  • succs2 [2]*BasicBlock フィールド:
    • これは、BasicBlock 構造体自体の中に、2つの *BasicBlock ポインタを格納できる固定サイズの配列を埋め込んだものです。
    • Goの BasicBlock は、通常、最大で2つのサクセッサ(条件分岐のtrue/falseパスなど)しか持たないため、このサイズは多くのケースをカバーできます。
  • b.Succs = b.succs2[:0]:
    • newBasicBlock で新しい BasicBlock が作成される際に、Succs スライスが succs2 配列をバッキングストアとして初期化されます。[:0] は、スライスの長さが0であることを意味し、容量は succs2 のサイズ(2)になります。
    • これにより、最初の2つのサクセッサを追加する際には、ヒープ割り当てが発生せず、succs2 のメモリが直接利用されます。
  • a.Succs = append(a.succs2[:0], b.Succs...):
    • fuseBlocks でブロックが結合される際にも、同様に a ブロックの succs2 を利用して Succs スライスが再構築されます。
  • 意義: この最適化は、Goのコンパイラが生成するSSAグラフの BasicBlock が非常に多数存在することを考慮したものです。小さなスライスであっても、その都度ヒープ割り当てが発生すると、GCの負荷やキャッシュミスが増加し、コンパイラの実行速度に悪影響を与えます。インライン配列を利用することで、これらのオーバーヘッドを削減し、コンパイラのパフォーマンスを向上させます。これは、Goの標準ライブラリの多くの場所で見られる、ヒープ割り当てを避けるための一般的なイディオムです。

これらの変更は、GoコンパイラのSSAバックエンドの内部動作を深く理解する上で非常に重要であり、コンパイラの効率と生成されるコードの品質に直接影響を与えます。

関連リンク

  • Go SSA ドキュメント (公式): GoコンパイラのSSAバックエンドに関する公式ドキュメントや設計ドキュメントは、Goのソースコードリポジトリ内や、Goの公式ブログなどで公開されています。
  • Gerrit Change-ID:

参考にした情報源リンク

  • Static Single Assignment Form (Wikipedia):
  • Escape analysis in Go:
  • Go Slices: usage and internals:
  • Compiler Design Handbooks:
    • "Compilers: Principles, Techniques, and Tools" (通称 Dragon Book) - コンパイラ設計の古典的な教科書で、SSA形式や最適化に関する詳細な情報が含まれています。
    • "Advanced Compiler Design and Implementation" by Steven S. Muchnick - より高度なコンパイラ最適化技術について解説されています。

これらの情報源は、GoコンパイラのSSAバックエンドの設計思想、実装の詳細、および関連するコンパイラ最適化技術を深く理解する上で役立ちます。