[インデックス 15375] ファイルの概要
このコミットは、Go言語の実験的なSSA (Static Single Assignment) パッケージ exp/ssa
におけるバグ修正と複数のリファクタリングを含んでいます。主な修正は、匿名フィールドのプロモーション処理におけるバグ(タイポ)の修正です。これにより、SSA形式のコード生成と解析の正確性が向上しています。
コミット
commit 71c37e1c88884edd7c9681ebbed9e3bbc1a08915
Author: Alan Donovan <adonovan@google.com>
Date: Fri Feb 22 12:35:45 2013 -0500
exp/ssa: fixed bug (typo) in findPromotedField.
By appending to the wrong (always empty) list, only the last
anonymous field was being considered for promotion.
Also:
- eliminated "function-local NamedTypes" TODO; nothing to do.
- fixed Function.DumpTo: printing of anon receivers was "( T)",
now "(T)"; extracted writeSignature into own function.
- eliminated blockNames function;
thanks to BasicBlock.String, "%s" of []*BasicBlock is fine.
- extracted buildReferrers into own function.
exp/ssa can now build its own transitive closure.
R=gri
CC=golang-dev
https://golang.org/cl/7384054
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/71c37e1c88884edd7c9681ebbed9e3bbc1a08915
元コミット内容
exp/ssa: fixed bug (typo) in findPromotedField.
このコミットは、exp/ssa
パッケージ内の findPromotedField
関数におけるタイプミスによるバグを修正しました。このバグにより、匿名フィールドのプロモーション処理が正しく行われず、常に最後の匿名フィールドのみが考慮されるという問題が発生していました。
また、以下の改善も含まれています。
- 「関数ローカルなNamedTypes」に関するTODOコメントが削除されました。これは、対応すべき事項がないと判断されたためです。
Function.DumpTo
関数における匿名レシーバーの表示形式が修正されました。以前は( T)
のようにスペースが含まれていましたが、(T)
となり、より整形された出力になりました。また、シグネチャの書き出し処理がwriteSignature
という独立した関数に抽出されました。blockNames
関数が削除されました。BasicBlock.String
メソッドが[]*BasicBlock
の文字列化に十分対応できるため、冗長な関数が不要になりました。buildReferrers
処理が独立した関数として抽出されました。
これらの変更により、exp/ssa
パッケージは自身の推移的閉包を構築できるようになりました。
変更の背景
このコミットの主な背景は、GoコンパイラのSSAバックエンドの正確性と堅牢性を向上させることです。
- 匿名フィールドのプロモーションバグ: Go言語の重要な機能の一つに、構造体の匿名フィールドのプロモーションがあります。これにより、埋め込まれた構造体のフィールドやメソッドが、外側の構造体のフィールドやメソッドであるかのように直接アクセスできます。
exp/ssa
パッケージは、このセマンティクスをSSA形式で正確に表現する必要があります。findPromotedField
のバグは、この重要な言語機能のSSA表現に誤りをもたらし、コンパイラの最適化や解析の正確性に影響を与える可能性がありました。 - コードの整理と保守性:
Function.DumpTo
からwriteSignature
の抽出、blockNames
の削除、buildReferrers
の関数化は、コードのモジュール化、可読性、および保守性の向上を目的としたリファクタリングです。冗長なコードの削除や関心の分離は、大規模なコードベースにおいてバグの発生を抑制し、将来の機能追加や変更を容易にします。 - SSAの自己完結性: 「
exp/ssa
can now build its own transitive closure」という記述は、SSAパッケージが自身の内部データ構造(特に参照関係)をより効率的かつ正確に管理できるようになったことを示唆しています。これは、SSA形式のコード解析や最適化の基盤となる重要な能力です。
これらの変更は、GoコンパイラのSSAバックエンドがより成熟し、信頼性の高いものになるための継続的な取り組みの一環です。
前提知識の解説
このコミットを理解するためには、以下の概念に関する基本的な知識が必要です。
-
SSA (Static Single Assignment) 形式:
- コンパイラ最適化の中間表現(IR)の一種です。
- 各変数が一度だけ代入されるという特性を持ちます。これにより、データフロー解析が容易になり、多くの最適化(例: 共通部分式除去、定数伝播、デッドコード除去)が効率的に行えます。
- Goコンパイラは、ソースコードを抽象構文木(AST)に変換した後、SSA形式に変換して様々な最適化を適用し、最終的に機械語を生成します。
exp/ssa
は、Go言語のプログラムをSSA形式に変換し、操作するための実験的なパッケージでした。
-
Go言語の構造体と匿名フィールド (Embedded Fields):
- Goの構造体は、他の構造体を匿名フィールドとして埋め込むことができます。
- 匿名フィールドを埋め込むと、埋め込まれた構造体のフィールドやメソッドが、外側の構造体のフィールドやメソッドであるかのように直接アクセスできるようになります。これを「プロモーション (Promotion)」と呼びます。
- 例:
type Inner struct { X int } type Outer struct { Inner // 匿名フィールド Y int } func main() { o := Outer{Inner: Inner{X: 10}, Y: 20} fmt.Println(o.X) // Inner.X が Outer にプロモーションされている }
- コンパイラは、このプロモーションを正しく処理し、SSA形式でもそのセマンティクスを維持する必要があります。
-
データフロー解析と参照関係 (Referrers):
- コンパイラは、プログラム内のデータの流れ(データフロー)を解析します。
- SSA形式では、各変数の定義と使用の関係(def-use/use-def chain)が明確になります。
Value.Referrers
は、特定のSSA値(変数や計算結果)がプログラムのどこで使われているか(参照されているか)を示すリストです。この情報は、デッドコード除去やレジスタ割り当てなど、多くの最適化で不可欠です。
-
Goの
types
パッケージ:- Goの標準ライブラリの一部で、Goプログラムの型情報を表現・操作するためのパッケージです。
- コンパイラやツール(
go vet
など)が、プログラムの型チェックやセマンティック解析を行う際に利用します。 types.Signature
は関数のシグネチャ(引数と戻り値の型)を表し、types.Struct
は構造体の型情報を表します。
-
Gerrit (golang.org/cl/):
- Goプロジェクトがコードレビューと変更管理に使用しているシステムです。
https://golang.org/cl/7384054
は、このコミットに対応するGerritのチェンジリスト(Change-ID)を示しています。
技術的詳細
このコミットは、主に exp/ssa
パッケージの内部実装に焦点を当てています。
-
findPromotedField
のバグ修正 (src/pkg/exp/ssa/promote.go
):- この関数は、構造体
st
内で特定のIDを持つプロモートされた匿名フィールドを検索します。 - 元のコードでは、匿名フィールドのパスを探索する際に、
list = append(next, ...)
という誤った記述がありました。ここでnext
は常に空のスライスでした。 - このため、ループの各イテレーションで
list
は新しい匿名フィールドパスで上書きされ、結果として最後に発見された匿名フィールドしかlist
に残らない状態でした。 - 正しい修正は
list = append(list, ...)
です。これにより、発見されたすべての匿名フィールドパスがlist
に追加され、プロモーションの探索が正しく行われるようになりました。これは典型的な「タイポ」によるバグであり、スライスのappend
操作のセマンティクスに関する誤解が原因です。
- この関数は、構造体
-
buildReferrers
の抽出 (src/pkg/exp/ssa/func.go
):- 以前は
Function.finish()
メソッド内に直接記述されていた、SSA値の参照者(Referrers)情報を構築するロジックが、buildReferrers
という独立した関数に切り出されました。 - この関数は、関数のすべての基本ブロック(BasicBlock)と命令(Instr)を走査し、各命令のオペランド(
rand
)がどのSSA値を参照しているかを特定します。 - そして、参照されているSSA値の
Referrers()
スライスに、その命令(instr
)を追加します。 - この分離により、
finish()
メソッドの責務が明確になり、buildReferrers
のロジックが再利用可能になりました。
- 以前は
-
writeSignature
の抽出とFunction.DumpTo
の改善 (src/pkg/exp/ssa/func.go
):Function.DumpTo
は、SSA関数の人間が読める形式の「逆アセンブル」を出力する関数です。- 関数のシグネチャ(レシーバー、パラメータ、戻り値)を整形して出力するロジックが、
writeSignature
という新しいヘルパー関数に抽出されました。 - これにより、
DumpTo
のコードが簡潔になり、シグネチャの出力ロジックが一箇所に集約されました。 - また、匿名レシーバーの出力形式が
( T)
から(T)
に修正され、より標準的なGoの構文に近づきました。これは、fmt.Fprintf
の書式指定ではなく、直接文字列操作でスペースを制御することで実現されています。
-
blockNames
関数の削除 (src/pkg/exp/ssa/sanity.go
):sanity.go
はSSA形式の健全性チェックを行うファイルです。- 以前は、
[]*BasicBlock
のリストを人間が読める文字列に変換するためのblockNames
というヘルパー関数が存在しました。 - しかし、
BasicBlock
型自体がString()
メソッドを実装しており、fmt.Sprintf
やfmt.Fprintf
で[]*BasicBlock
を%s
フォーマットで出力すると、Goのデフォルトの文字列化メカニズムが各要素のString()
メソッドを呼び出して適切に連結するため、blockNames
関数は冗長であることが判明しました。 - このため、
blockNames
関数は削除され、直接b.Preds
やb.Succs
を%s
で出力するように変更されました。これにより、コードが簡潔になり、不必要な抽象化が取り除かれました。
コアとなるコードの変更箇所
このコミットで変更された主要なファイルと、その中のコアとなる変更箇所は以下の通りです。
-
src/pkg/exp/ssa/promote.go
:--- a/src/pkg/exp/ssa/promote.go +++ b/src/pkg/exp/ssa/promote.go @@ -408,7 +408,7 @@ func findPromotedField(st *types.Struct, id Id) (*anonFieldPath, int) { var list, next []*anonFieldPath for i, f := range st.Fields { if f.IsAnonymous { - list = append(next, &anonFieldPath{nil, i, f}) + list = append(list, &anonFieldPath{nil, i, f}) } }
findPromotedField
関数内のappend
の対象がnext
からlist
に変更されました。これが匿名フィールドのプロモーションバグの修正です。
-
src/pkg/exp/ssa/func.go
:--- a/src/pkg/exp/ssa/func.go +++ b/src/pkg/exp/ssa/func.go @@ -265,6 +265,25 @@ func numberRegisters(f *Function) { } } +// buildReferrers populates the def/use information in all non-nil +// Value.Referrers slice. +// Precondition: all such slices are initially empty. +func buildReferrers(f *Function) { + var rands []*Value + for _, b := range f.Blocks { + for _, instr := range b.Instrs { + rands = instr.Operands(rands[:0]) // recycle storage + for _, rand := range rands { + if r := *rand; r != nil { + if ref := r.Referrers(); ref != nil { + *ref = append(*ref, instr) + } + } + } + } + } +} + // finish() finalizes the function after SSA code generation of its body. func (f *Function) finish() { f.objects = nil @@ -289,20 +308,7 @@ func (f *Function) finish() { optimizeBlocks(f) - // Build immediate-use (referrers) graph. - var rands []*Value - for _, b := range f.Blocks { - for _, instr := range b.Instrs { - rands = instr.Operands(rands[:0]) // recycle storage - for _, rand := range rands { - if r := *rand; r != nil { - if ref := r.Referrers(); ref != nil { - *ref = append(*ref, instr) - } - } - } - } - } + buildReferrers(f) if f.Prog.mode&NaiveForm == 0 { // For debugging pre-state of lifting pass: @@ -460,39 +466,22 @@ func (f *Function) fullName(from *Package) string { return f.Name_ } -// DumpTo prints to w a human readable "disassembly" of the SSA code of -// all basic blocks of function f. +// writeSignature writes to w the signature sig in declaration syntax. +// Derived from types.Signature.String(). // -func (f *Function) DumpTo(w io.Writer) { - fmt.Fprintf(w, "# Name: %s\\n", f.FullName()) - fmt.Fprintf(w, "# Declared at %s\\n", f.Prog.Files.Position(f.Pos)) - - if f.Enclosing != nil { - fmt.Fprintf(w, "# Parent: %s\\n", f.Enclosing.Name()) - } - - if f.FreeVars != nil { - io.WriteString(w, "# Free variables:\\n") - for i, fv := range f.FreeVars { - fmt.Fprintf(w, "# % 3d:\\t%s %s\\n", i, fv.Name(), fv.Type()) - } - } - - if len(f.Locals) > 0 { - io.WriteString(w, "# Locals:\\n") - for i, l := range f.Locals { - fmt.Fprintf(w, "# % 3d:\\t%s %s\\n", i, l.Name(), indirectType(l.Type())) - } - } - - // Function Signature in declaration syntax; derived from types.Signature.String(). +func writeSignature(w io.Writer, name string, sig *types.Signature, params []*Parameter) { io.WriteString(w, "func ") - params := f.Params - if f.Signature.Recv != nil { - fmt.Fprintf(w, "(%s %s) ", params[0].Name(), params[0].Type()) + if sig.Recv != nil { + io.WriteString(w, "(") + if n := params[0].Name(); n != "" { + io.WriteString(w, n) + io.WriteString(w, " ") + } + io.WriteString(w, params[0].Type().String()) + io.WriteString(w, ") ") params = params[1:] } - io.WriteString(w, f.Name()) + io.WriteString(w, name) io.WriteString(w, "(") for i, v := range params { if i > 0 { @@ -500,13 +489,13 @@ func (f *Function) DumpTo(w io.Writer) { } io.WriteString(w, v.Name()) io.WriteString(w, " ") - if f.Signature.IsVariadic && i == len(params)-1 { + if sig.IsVariadic && i == len(params)-1 { io.WriteString(w, "...") } io.WriteString(w, v.Type().String()) } io.WriteString(w, ")") - if res := f.Signature.Results; res != nil { + if res := sig.Results; res != nil { io.WriteString(w, " ") var t types.Type if len(res) == 1 && res[0].Name == "" { @@ -516,6 +505,34 @@ func (f *Function) DumpTo(w io.Writer) { } io.WriteString(w, t.String()) } +} + +// DumpTo prints to w a human readable "disassembly" of the SSA code of +// all basic blocks of function f. +// +func (f *Function) DumpTo(w io.Writer) { + fmt.Fprintf(w, "# Name: %s\\n", f.FullName()) + fmt.Fprintf(w, "# Declared at %s\\n", f.Prog.Files.Position(f.Pos)) + + if f.Enclosing != nil { + fmt.Fprintf(w, "# Parent: %s\\n", f.Enclosing.Name()) + } + + if f.FreeVars != nil { + io.WriteString(w, "# Free variables:\\n") + for i, fv := range f.FreeVars { + fmt.Fprintf(w, "# % 3d:\\t%s %s\\n", i, fv.Name(), fv.Type()) + } + } + + if len(f.Locals) > 0 { + io.WriteString(w, "# Locals:\\n") + for i, l := range f.Locals { + fmt.Fprintf(w, "# % 3d:\\t%s %s\\n", i, l.Name(), indirectType(l.Type())) + } + } + + writeSignature(w, f.Name(), f.Signature, f.Params) io.WriteString(w, ":\\n") if f.Blocks == nil { @@ -530,7 +547,7 @@ func (f *Function) DumpTo(w io.Writer) { } fmt.Fprintf(w, ".%s:\\t\\t\\t\\t\\t\\t\\t P:%d S:%d\\n\", b, len(b.Preds), len(b.Succs))\n if false { // CFG debugging - fmt.Fprintf(w, "\\t# CFG: %s --> %s --> %s\\n\", blockNames(b.Preds), b, blockNames(b.Succs))\n + fmt.Fprintf(w, "\\t# CFG: %s --> %s --> %s\\n\", b.Preds, b, b.Succs)\n }\n for _, instr := range b.Instrs { io.WriteString(w, "\\t")
buildReferrers
関数が新しく追加され、Function.finish()
からその呼び出しに置き換えられました。writeSignature
関数が新しく追加され、Function.DumpTo
からシグネチャ出力ロジックが移動しました。Function.DumpTo
内で、writeSignature
が呼び出されるようになりました。Function.DumpTo
内のCFGデバッグ出力で、blockNames
の代わりに直接b.Preds
とb.Succs
が使われるようになりました。
-
src/pkg/exp/ssa/sanity.go
:--- a/src/pkg/exp/ssa/sanity.go +++ b/src/pkg/exp/ssa/sanity.go @@ -4,7 +4,6 @@ package ssa // Currently it checks CFG invariants but little at the instruction level. import ( - "bytes" "fmt" "io" "os" @@ -41,20 +40,6 @@ func MustSanityCheck(fn *Function, reporter io.Writer) { } } -// blockNames returns the names of the specified blocks as a -// human-readable string. -// -func blockNames(blocks []*BasicBlock) string { - var buf bytes.Buffer - for i, b := range blocks { - if i > 0 { - io.WriteString(&buf, ", ") - } - io.WriteString(&buf, b.String()) - } - return buf.String() -} - func (s *sanity) diagnostic(prefix, format string, args ...interface{}) { fmt.Fprintf(s.reporter, "%s: function %s", prefix, s.fn.FullName()) if s.block != nil { @@ -236,7 +221,7 @@ func (s *sanity) checkBlock(b *BasicBlock, index int) { } } if !found { - s.errorf("expected successor edge in predecessor %s; found only: %s", a, blockNames(a.Succs))\n + s.errorf("expected successor edge in predecessor %s; found only: %s", a, a.Succs)\n } if a.Func != s.fn { s.errorf("predecessor %s belongs to different function %s", a, a.Func.FullName()) } @@ -251,7 +236,7 @@ func (s *sanity) checkBlock(b *BasicBlock, index int) { } } if !found { - s.errorf("expected predecessor edge in successor %s; found only: %s", c, blockNames(c.Preds))\n + s.errorf("expected predecessor edge in successor %s; found only: %s", c, c.Preds)\n } if c.Func != s.fn { s.errorf("successor %s belongs to different function %s", c, c.Func.FullName()) }
blockNames
関数が完全に削除されました。checkBlock
関数内のエラーメッセージで、blockNames
の代わりに直接a.Succs
やc.Preds
が使われるようになりました。
コアとなるコードの解説
src/pkg/exp/ssa/promote.go
の修正
// promote.go
var list, next []*anonFieldPath
for i, f := range st.Fields {
if f.IsAnonymous {
// 修正前: list = append(next, &anonFieldPath{nil, i, f})
// 修正後: list = append(list, &anonFieldPath{nil, i, f})
}
}
このコードスニペットは、構造体 st
の匿名フィールドを探索し、それらのパスを list
に追加する部分です。
元のコードでは list = append(next, ...)
となっていました。ここで next
はループの各イテレーションで初期化されるか、あるいは常に空のスライスとして扱われていたため、append
操作は常に新しい要素を空のスライスに追加し、その結果を list
に代入していました。これにより、list
には常に最後に処理された匿名フィールドしか含まれず、それ以前に見つかった匿名フィールドの情報が失われていました。
修正後の list = append(list, ...)
は、既存の list
スライスに新しい匿名フィールドパスを追加します。これにより、すべての匿名フィールドパスが正しく収集され、プロモーションのロジックが意図通りに機能するようになりました。これは、Goのスライス操作における一般的な落とし穴の一つであり、スライスの再割り当てと元のスライスの参照を正しく理解することが重要です。
src/pkg/exp/ssa/func.go
の変更
buildReferrers
関数の抽出
// func.go
func buildReferrers(f *Function) {
var rands []*Value
for _, b := range f.Blocks {
for _, instr := range b.Instrs {
rands = instr.Operands(rands[:0]) // recycle storage
for _, rand := range rands {
if r := *rand; r != nil {
if ref := r.Referrers(); ref != nil {
*ref = append(*ref, instr)
}
}
}
}
}
}
func (f *Function) finish() {
// ...
buildReferrers(f) // 以前はここに直接ロジックがあった
// ...
}
buildReferrers
関数は、SSA形式の関数 f
内のすべてのSSA値(Value
)に対する参照者(Referrers
)情報を構築します。これは、各SSA値がプログラムのどの命令によって使用されているかを追跡するための重要なデータ構造です。
このロジックが finish
メソッドから独立した関数に切り出されたことで、finish
メソッドの責務が軽減され、コードのモジュール性が向上しました。また、buildReferrers
は他の場所からも呼び出せるようになり、再利用性が高まります。rands = instr.Operands(rands[:0])
のようにスライスを再利用するイディオムは、アロケーションを減らしパフォーマンスを向上させるためのGoの一般的なパターンです。
writeSignature
関数の抽出と Function.DumpTo
の変更
// func.go
func writeSignature(w io.Writer, name string, sig *types.Signature, params []*Parameter) {
io.WriteString(w, "func ")
if sig.Recv != nil {
io.WriteString(w, "(")
if n := params[0].Name(); n != "" {
io.WriteString(w, n)
io.WriteString(w, " ")
}
io.WriteString(w, params[0].Type().String())
io.WriteString(w, ") ")
params = params[1:]
}
io.WriteString(w, name)
io.WriteString(w, "(")
// ... パラメータの出力ロジック ...
io.WriteString(w, ")")
if res := sig.Results; res != nil {
io.WriteString(w, " ")
// ... 戻り値の出力ロジック ...
}
}
func (f *Function) DumpTo(w io.Writer) {
// ...
writeSignature(w, f.Name(), f.Signature, f.Params) // 以前はここに直接ロジックがあった
io.WriteString(w, ":\\n")
// ...
}
writeSignature
関数は、Goの関数のシグネチャ(レシーバー、関数名、パラメータ、戻り値)を、宣言構文(例: func (r ReceiverType) MyFunc(arg1 Type1) (ret1 Type2)
)で出力するためのヘルパー関数です。
この関数が Function.DumpTo
から分離されたことで、DumpTo
の主要な目的であるSSAコードのダンプに集中できるようになりました。また、匿名レシーバーの出力形式が ( T)
から (T)
に修正されたのは、params[0].Name()
が空文字列の場合に余分なスペースが出力されないように、条件分岐でスペースの追加を制御しているためです。これは、出力の整形と一貫性を保つための細かな改善です。
src/pkg/exp/ssa/sanity.go
の変更
// sanity.go
// 削除された blockNames 関数
// func blockNames(blocks []*BasicBlock) string { ... }
func (s *sanity) checkBlock(b *BasicBlock, index int) {
// ...
if !found {
// 修正前: s.errorf("...", blockNames(a.Succs))
// 修正後: s.errorf("...", a.Succs)
}
// ...
}
blockNames
関数は、[]*BasicBlock
のスライスを文字列に変換するために使用されていました。しかし、Goの fmt
パッケージは、スライス内の要素が String()
メソッドを実装している場合、そのメソッドを呼び出して要素を文字列化し、それらを適切に連結して出力する機能を持っています。BasicBlock
型は String()
メソッドを実装しているため、blockNames
関数は冗長でした。
このコミットでは、blockNames
関数が削除され、エラーメッセージのフォーマット文字列内で直接 a.Succs
や c.Preds
といった []*BasicBlock
型の変数を %s
で出力するように変更されました。これにより、コードが簡潔になり、Goの標準的な文字列化メカニズムをより活用するようになりました。
関連リンク
- Go言語のSSAパッケージに関する公式ドキュメントや関連資料は、Goのソースコードリポジトリや公式ブログで探すことができます。
- Go言語の構造体と匿名フィールド(埋め込み)に関する公式ドキュメント: https://go.dev/tour/methods/10
- Go言語のコンパイラに関する一般的な情報: https://go.dev/doc/compiler
参考にした情報源リンク
- コミットメッセージと差分 (diff) 情報: GitHub上のコミットページ https://github.com/golang/go/commit/71c37e1c88884edd7c9681ebbed9e3bbc1a08915
- Go言語の公式ドキュメント (特に
types
パッケージや言語仕様に関する部分) - SSA形式に関する一般的なコンパイラ理論の知識