[インデックス 15447] ファイルの概要
このコミットは、Go言語の実験的なSSA (Static Single Assignment) 形式のコンパイラバックエンドである exp/ssa
パッケージにおける、フィールド選択(構造体のフィールドへのアクセス)のロジックを根本的に再実装するものです。特に、Goの構造体埋め込みによって「プロモートされたフィールド」へのアクセスをSSA命令に変換する際の処理が改善されています。また、デバッグを容易にするためのロギングユーティリティも追加されています。
コミット
commit bd92dd6a5f2576fbf8144da0e531a57b4ebc8961
Author: Alan Donovan <adonovan@google.com>
Date: Tue Feb 26 13:32:22 2013 -0500
exp/ssa: reimplement logic for field selection.
The previous approach desugared the ast.SelectorExpr
to make implicit field selections explicit. But:
1) it was clunky since it required allocating temporary
syntax trees.
2) it was not thread-safe since it required poking
types into the shared type map for the new ASTs.
3) the desugared syntax had no place to represent the
package lexically enclosing each implicit field
selection, so it was as if they all occurred in the
same package as the explicit field selection.
This meant unexported field names changed meaning.
This CL does what I should have done all along: just
generate the SSA instructions directly from the original
AST and the promoted field information.
Also:
- add logStack util for paired start/end log messages.
Useful for debugging crashes.
R=gri
CC=golang-dev
https://golang.org/cl/7395052
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/bd92dd6a5f2576fbf8144da0e531a57b4ebc8961
元コミット内容
exp/ssa: reimplement logic for field selection.
The previous approach desugared the ast.SelectorExpr
to make implicit field selections explicit. But:
1) it was clunky since it required allocating temporary
syntax trees.
2) it was not thread-safe since it required poking
types into the shared type map for the new ASTs.
3) the desugared syntax had no place to represent the
package lexically enclosing each implicit field
selection, so it was as if they all occurred in the
same package as the explicit field selection.
This meant unexported field names changed meaning.
This CL does what I should have done all along: just
generate the SSA instructions directly from the original
AST and the promoted field information.
Also:
- add logStack util for paired start/end log messages.
Useful for debugging crashes.
R=gri
CC=golang-dev
https://golang.org/cl/7395052
変更の背景
Go言語のコンパイラは、ソースコードを抽象構文木 (AST) にパースした後、中間表現 (IR) に変換します。このコミットが対象とする exp/ssa
パッケージは、Goコンパイラの実験的なSSA形式のバックエンドであり、ASTからSSA形式の命令を生成する役割を担っています。
以前のフィールド選択の処理 (demoteSelector
関数) は、ast.SelectorExpr
(例: obj.Field
) を処理する際に、Goの構造体埋め込みによる「プロモートされたフィールド」への暗黙的なアクセスを、明示的なフィールド選択のASTノードに「脱糖 (desugar)」していました。例えば、struct { T; F int }
という構造体 S
があり、T
が struct { G int }
であった場合、s.G
というアクセスは内部的に s.T.G
のように変換されていました。
しかし、この「脱糖」アプローチには以下の深刻な問題がありました。
- 非効率性と複雑性: 一時的なASTノードを生成する必要があり、これはメモリ割り当てと処理のオーバーヘッドを伴い、コードも複雑でした。
- スレッドセーフティの欠如: 新しく生成されたASTノードの型情報を、共有されている型マップ (
Builder.types
) に書き込む必要がありました。これは、複数のゴルーチン(スレッド)が同時にコンパイル処理を行う場合に競合状態を引き起こし、データ破損や予測不能な動作につながる可能性がありました。コンパイラのビルドプロセスは並列化されることが多いため、これは致命的な問題です。 - レキシカルスコープの問題: 脱糖されたASTは、暗黙的なフィールド選択がどのパッケージのレキシカルスコープで発生したかという情報を保持できませんでした。これにより、エクスポートされていない(小文字で始まる)フィールド名が、本来の定義パッケージとは異なるパッケージで解決されてしまい、意味が変わってしまうというバグが発生していました。Goでは、エクスポートされていない識別子は定義されたパッケージ内でのみアクセス可能であり、このルールが破られていました。
これらの問題を解決するため、コミットの作者は、ASTを再構築するのではなく、元のASTとプロモートされたフィールドの情報を直接利用してSSA命令を生成する、より直接的なアプローチに切り替えることを決定しました。
前提知識の解説
Go言語の構造体と埋め込み (Structs and Embedding in Go)
Go言語の構造体は、異なる型のフィールドをまとめるための複合データ型です。Goにはクラスベースの継承はありませんが、「構造体の埋め込み (struct embedding)」というメカニズムを通じて、ある構造体の中に別の構造体を匿名で含めることができます。
例:
package main
import "fmt"
type Point struct {
X, Y int
}
type Circle struct {
Point // Point構造体を匿名で埋め込み
Radius int
}
func main() {
c := Circle{Point{10, 20}, 5}
fmt.Println(c.X) // c.Point.X ではなく c.X でアクセス可能
}
この例では、Circle
構造体は Point
構造体を埋め込んでいます。これにより、Circle
のインスタンス c
から、c.X
や c.Y
のように、あたかも Circle
自身のフィールドであるかのように Point
のフィールドに直接アクセスできます。これを「フィールドのプロモーション (Field Promotion)」と呼びます。
フィールドのプロモーション (Field Promotion)
構造体埋め込みの際、埋め込まれた型のフィールドやメソッドは、外側の構造体のフィールドやメソッドとして「プロモート」されます。これにより、埋め込まれた型のフィールドに直接アクセスできるようになり、コードの簡潔性が向上します。コンパイラは、このようなアクセスを内部的に解決し、適切なフィールドへのパスを決定します。
抽象構文木 (AST) と go/ast
パッケージ
抽象構文木 (Abstract Syntax Tree, AST) は、ソースコードの構造を木構造で表現したものです。コンパイラのフロントエンドがソースコードを解析(パース)して生成します。ASTは、プログラムの構文的な構造を抽象化しており、コンパイラの後の段階(型チェック、中間コード生成など)で利用されます。
Go言語の標準ライブラリには、GoソースコードのASTを扱うための go/ast
パッケージが含まれています。例えば、ast.SelectorExpr
は X.Sel
の形式の式を表し、これはフィールド選択 (obj.Field
) やメソッド呼び出し (obj.Method()
)、パッケージ修飾子付きの識別子 (pkg.Ident
) などに利用されます。
型情報と go/types
パッケージ
go/types
パッケージは、Goプログラムの型情報を表現し、型チェックを行うための機能を提供します。ASTノードに対応する型情報(変数の型、関数のシグネチャ、構造体のフィールドの型など)を保持し、プログラムのセマンティックな解析に不可欠です。コンパイラは、ASTと型情報を組み合わせて、正しいSSA命令を生成します。
SSA (Static Single Assignment) 形式
SSA (Static Single Assignment) 形式は、コンパイラの中間表現 (IR) の一種です。SSA形式では、各変数が一度だけ代入されるという特性を持ちます。これにより、データフロー解析や最適化が容易になります。
例:
// 通常のコード
x = a + b
x = x * c
// SSA形式
x1 = a + b
x2 = x1 * c
SSA形式は、コンパイラのバックエンドで、最適化や最終的な機械語コード生成の前に利用されることが多いです。exp/ssa
パッケージは、GoのASTからこのSSA形式の命令を生成する役割を担っています。
コンパイラのフロントエンドとバックエンド、中間表現 (IR)
コンパイラは通常、以下の主要なフェーズに分かれます。
- フロントエンド: ソースコードを解析し、ASTを生成し、型チェックを行います。
- 中間表現 (IR): フロントエンドとバックエンドの間に位置し、プラットフォームに依存しない抽象的なコード表現です。SSA形式はその一例です。
- バックエンド: 中間表現を最適化し、最終的な機械語コードやバイトコードを生成します。
スレッドセーフティの重要性
スレッドセーフティとは、複数のスレッド(またはゴルーチン)が同時に同じデータやリソースにアクセスしても、プログラムが正しく動作し、データが破損しないことを保証する特性です。コンパイラのように大規模なソフトウェアでは、ビルド時間を短縮するために並列処理が頻繁に利用されます。共有されるデータ構造(例: 型マップ)がスレッドセーフでない場合、競合状態 (race condition) が発生し、誤った結果やクラッシュにつながる可能性があります。
Goにおけるエクスポートされていない識別子とパッケージスコープ
Go言語では、識別子(変数名、関数名、型名、フィールド名など)が大文字で始まる場合、その識別子は「エクスポート」され、他のパッケージからアクセス可能です。小文字で始まる場合、その識別子は「エクスポートされていない」と見なされ、定義されたパッケージ内からのみアクセス可能です。
このルールは、Goのモジュール性とカプセル化の重要な側面です。以前の demoteSelector
の問題点の一つは、このパッケージスコープのルールを破り、エクスポートされていないフィールドが誤って他のパッケージからアクセス可能であるかのように扱われてしまう可能性があったことです。
技術的詳細
旧アプローチ demoteSelector
の具体的な問題点
demoteSelector
関数は、ast.SelectorExpr
を受け取り、プロモートされたフィールドへのアクセスを明示的なASTノードの連鎖に変換していました。例えば、c.X
(ここで c
は Circle
型で Point
を埋め込み、X
は Point
のフィールド) は、((c).Point).X
のようなAST構造に変換されていました。
この変換は、以下の問題を引き起こしました。
- ASTの再構築とオーバーヘッド:
makeSelector
関数が再帰的に呼び出され、新しいast.SelectorExpr
ノードが動的に生成されていました。これは、ASTノードのメモリ割り当てと、それらをリンクする処理のオーバーヘッドを意味します。コンパイル時に頻繁に発生する操作であるため、パフォーマンスに影響を与えます。 - 共有型マップへの書き込みとスレッドセーフティ:
demoteSelector
内で、b.types[sel] = path.field.Type
のように、新しく生成されたASTノード (sel
) とその型情報を、Builder
構造体の共有フィールドであるb.types
マップに書き込んでいました。Builder.types
は、コンパイルプロセス全体で共有される可能性のあるリソースであり、複数のゴルーチンが同時にこのマップに書き込もうとすると、競合状態が発生し、マップのデータが破壊されたり、不正な型情報が格納されたりするリスクがありました。コミットメッセージのTODO(adonovan): fix: let's not do that.
やTODO(adonovan): opt: not thread-safe
というコメントは、この問題が認識されていたことを示しています。 - レキシカルスコープ情報の欠落: 脱糖されたASTノードは、元の
ast.SelectorExpr
がどのパッケージのコンテキストで書かれたかという情報を失っていました。Goでは、エクスポートされていないフィールド(小文字で始まるフィールド)は、そのフィールドが定義されたパッケージ内からのみアクセス可能です。しかし、脱糖されたASTでは、この「定義されたパッケージ」のコンテキストが失われ、すべての暗黙的なフィールド選択が、あたかも明示的なフィールド選択と同じパッケージで発生したかのように扱われていました。これにより、本来アクセスできないはずのエクスポートされていないフィールドにアクセスできてしまう、あるいはその逆の誤った解決が行われる可能性がありました。
新アプローチ selector
, fieldAddr
, fieldExpr
による解決
このコミットでは、demoteSelector
関数を完全に削除し、代わりに selector
, fieldAddr
, fieldExpr
という新しい関数を導入しています。これらの関数は、ASTを再構築するのではなく、元のASTノードとプロモートされたフィールドのパス情報を直接利用して、SSA命令を生成します。
-
selector(fn *Function, e *ast.SelectorExpr, wantAddr, escaping bool) Value
:- この関数は、
ast.SelectorExpr
e
を評価し、その値(wantAddr
がfalse
の場合)またはアドレス(wantAddr
がtrue
の場合)をSSAValue
として返します。 - プロモートされたフィールドの場合、
findPromotedField
を使用して、元の構造体から目的のフィールドまでのパス (anonFieldPath
) と最終的なフィールドのインデックス (index
) を特定します。 wantAddr
の値に応じて、fieldAddr
またはfieldExpr
を呼び出します。これにより、SSA命令の生成が、ASTの再構築という間接的なステップを介さずに、直接行われるようになります。
- この関数は、
-
fieldAddr(fn *Function, base ast.Expr, path *anonFieldPath, index int, fieldType types.Type, escaping bool) Value
:- 構造体またはポインタ型 (
*struct
) のベース式base
を評価し、path
で指定された暗黙的なフィールド選択を適用した後、最終的なフィールド (index
) のアドレスをSSAValue
として生成します。 path
が存在する場合(プロモートされたフィールドの場合)、再帰的にfieldAddr
またはfieldExpr
を呼び出して、パス上の各埋め込み構造体のアドレスまたは値を解決します。- 最終的に、
FieldAddr
命令(構造体フィールドのアドレスを取得するSSA命令)を生成し、その型をポインタ型に設定して返します。
- 構造体またはポインタ型 (
-
fieldExpr(fn *Function, base ast.Expr, path *anonFieldPath, index int, fieldType types.Type) Value
:fieldAddr
と同様に、ベース式base
とパスpath
を評価しますが、最終的なフィールドの値そのものをSSAValue
として生成します。- ベースが構造体型 (
*types.Struct
) の場合、Field
命令(構造体フィールドの値を取得するSSA命令)を生成します。 - ベースがポインタ型 (
*types.Pointer
) の場合、FieldAddr
命令でアドレスを取得した後、emitLoad
を呼び出してそのアドレスから値をロードする命令を生成します。
この新しいアプローチにより、ASTの再構築が不要になり、共有型マップへの不必要な書き込みもなくなります。これにより、パフォーマンスが向上し、スレッドセーフティの問題が解消され、レキシカルスコープのコンテキストも正しく保持されるようになります。
logStack
ユーティリティの導入
このコミットでは、デバッグを目的として src/pkg/exp/ssa/util.go
に logStack
という新しいユーティリティ関数が追加されています。
// logStack prints the formatted "start" message to stderr and
// returns a closure that prints the corresponding "end" message.
// Call using 'defer logStack(...)()' to show builder stack on panic.
// Don't forget trailing parens!
func logStack(format string, args ...interface{}) func() {
msg := fmt.Sprintf(format, args...)
io.WriteString(os.Stderr, msg)
io.WriteString(os.Stderr, "\n")
return func() {
io.WriteString(os.Stderr, msg)
io.WriteString(os.Stderr, " end\n")
}
}
この関数は、特定の処理の開始時にメッセージを標準エラー出力に表示し、その処理が終了したときに(defer
ステートメントと組み合わせることで、パニック時でも)対応する「end」メッセージを表示するためのクロージャを返します。これにより、コンパイラのビルドプロセス中にどの関数が実行されているかを追跡し、特にクラッシュが発生した場合に、どの処理が原因で問題が発生したかを特定するのに役立ちます。
builder.go
, func.go
, promote.go
の各所で、以前の fmt.Fprintln(os.Stderr, ...)
や fmt.Fprintf(os.Stderr, ...)
といった直接的なロギング呼び出しが defer logStack(...)()
に置き換えられています。これにより、ロギングの開始と終了がペアになり、デバッグ出力がより構造化されます。
コアとなるコードの変更箇所
このコミットにおける主要な変更は以下のファイルと関数に集中しています。
src/pkg/exp/ssa/builder.go
:demoteSelector
関数の削除。- 新しい関数
selector
,fieldAddr
,fieldExpr
の追加。 addr
関数とexpr
関数が、フィールド選択の処理にselector
関数を使用するように変更。globalValueSpec
とbuildFunction
関数で、デバッグロギングにlogStack
を使用するように変更。
src/pkg/exp/ssa/func.go
:start
関数とfinish
関数から、直接的なデバッグロギング (fmt.Fprintf
) の呼び出しを削除。これらはbuilder.go
のbuildFunction
でlogStack
に置き換えられました。
src/pkg/exp/ssa/promote.go
:makeBridgeMethod
とmakeImethodThunk
関数で、デバッグロギングにlogStack
を使用するように変更。
src/pkg/exp/ssa/ssa.go
:- コメントの修正 (
AnInstruction
->anInstruction
)。これは機能的な変更ではありません。
- コメントの修正 (
src/pkg/exp/ssa/util.go
:- 新しいユーティリティ関数
logStack
の追加。
- 新しいユーティリティ関数
コアとなるコードの解説
src/pkg/exp/ssa/builder.go
の変更
demoteSelector
の削除と新しいフィールド選択ロジック
以前の demoteSelector
関数は、ast.SelectorExpr
を受け取り、プロモートされたフィールドのアクセスを明示的なASTノードに変換していました。この関数は完全に削除されました。
代わりに、以下の3つの新しい関数が導入されました。
-
selector(fn *Function, e *ast.SelectorExpr, wantAddr, escaping bool) Value
この関数は、ast.SelectorExpr
e
をSSA形式に変換する主要なエントリポイントです。id := makeId(e.Sel.Name, fn.Pkg.Types)
: 選択されたフィールドの名前とパッケージの型情報から識別子を生成します。st := underlyingType(deref(b.exprType(e.X))).(*types.Struct)
: セレクタのベース (e.X
) の型を取得し、それが指す構造体の基底型を取得します。index := -1
: フィールドのインデックスを初期化します。for i, f := range st.Fields
: まず、名前付きフィールドとして直接存在するかをチェックします。if index == -1
: 直接見つからない場合、プロモートされたフィールドである可能性があるので、findPromotedField(st, id)
を呼び出して、プロモートされたフィールドへのパス (path
) とインデックス (index
) を検索します。if wantAddr
: 呼び出し元がアドレスを求めている場合(L-value)、b.fieldAddr
を呼び出します。else
: 呼び出し元が値を求めている場合(R-value)、b.fieldExpr
を呼び出します。
-
fieldAddr(fn *Function, base ast.Expr, path *anonFieldPath, index int, fieldType types.Type, escaping bool) Value
この関数は、フィールドのアドレスをSSAValue
として生成します。var x Value
: ベースのアドレスまたは値を保持する変数。if path != nil
: プロモートされたフィールドの場合、path
を辿って中間のアドレスまたは値を再帰的に取得します。- 埋め込みフィールドが構造体の場合 (
*types.Struct
)、b.fieldAddr
を再帰的に呼び出してそのアドレスを取得します。 - 埋め込みフィールドがポインタの場合 (
*types.Pointer
)、b.fieldExpr
を再帰的に呼び出してその値(ポインタ)を取得します。
- 埋め込みフィールドが構造体の場合 (
else
: プロモートされていない直接のフィールドの場合、ベース式base
のアドレスを取得します。- ベースが構造体の場合 (
*types.Struct
)、b.addr(fn, base, escaping).(address).addr
でそのアドレスを取得します。 - ベースがポインタの場合 (
*types.Pointer
)、b.expr(fn, base)
でポインタ値を取得します。
- ベースが構造体の場合 (
v := &FieldAddr{X: x, Field: index}
: 最終的に、FieldAddr
SSA命令を生成します。これは、X
のアドレスからindex
番目のフィールドのアドレスを計算する命令です。v.setType(pointer(fieldType))
: 生成された命令の型を、フィールドの型へのポインタ型に設定します。return fn.emit(v)
: 命令を関数に発行し、その結果のSSAValue
を返します。
-
fieldExpr(fn *Function, base ast.Expr, path *anonFieldPath, index int, fieldType types.Type) Value
この関数は、フィールドの値をSSAValue
として生成します。var x Value
: ベースの値またはアドレスを保持する変数。if path != nil
: プロモートされたフィールドの場合、path
を辿って中間のアドレスまたは値を再帰的に取得します。この場合、常にb.fieldExpr
を再帰的に呼び出して値を取得します。else
: プロモートされていない直接のフィールドの場合、ベース式base
の値を取得します (b.expr(fn, base)
)。switch underlyingType(x.Type()).(type)
: ベースの型に応じて処理を分岐します。case *types.Struct
: ベースが構造体の場合、Field
SSA命令を生成します。これは、X
の値からindex
番目のフィールドの値を取得する命令です。case *types.Pointer
: ベースがポインタ (*struct
) の場合、まずFieldAddr
命令でフィールドのアドレスを取得し、次にemitLoad
を呼び出してそのアドレスから値をロードする命令を生成します。
return fn.emit(v)
: 命令を関数に発行し、その結果のSSAValue
を返します。
addr
および expr
関数の変更
addr
関数(L-value、アドレスを生成)と expr
関数(R-value、値を生成)は、ast.SelectorExpr
の処理において、削除された demoteSelector
の代わりに新しく導入された selector
関数を呼び出すように変更されました。
-
addr
内のast.SelectorExpr
のケース:// 変更前: // e, index := b.demoteSelector(e, fn.Pkg) // ... (FieldAddr 命令を直接生成) // 変更後: return address{b.selector(fn, e, true, escaping)}
selector
のwantAddr
引数をtrue
に設定して呼び出し、アドレスを生成させます。 -
expr
内のast.SelectorExpr
のケース:// 変更前: // e, index := b.demoteSelector(e, fn.Pkg) // ... (Field または FieldAddr + Load 命令を直接生成) // 変更後: return b.selector(fn, e, false, false)
selector
のwantAddr
引数をfalse
に設定して呼び出し、値を生成させます。
これらの変更により、フィールド選択のロジックが一元化され、ASTの再構築という間接的なステップが排除されました。
logStack
の利用
globalValueSpec
と buildFunction
関数では、デバッグロギングに logStack
が導入されました。
globalValueSpec
内:// 変更前: // fmt.Fprintln(os.Stderr, "build globals", spec.Names) // ugly... // 変更後: defer logStack("build globals %s", spec.Names)()
buildFunction
内:// 変更前: (func.go にあったロギングがこちらに移動) // 変更後: if fn.Prog.mode&LogSource != 0 { defer logStack("build function %s @ %s", fn.FullName(), fn.Prog.Files.Position(fn.Pos))() }
これにより、関数のビルドやグローバル変数の処理の開始と終了が、より明確にログに記録されるようになりました。
src/pkg/exp/ssa/func.go
の変更
Function.start
と Function.finish
から、直接的なデバッグロギングの呼び出しが削除されました。これは、builder.go
の buildFunction
で logStack
を使用する形に統合されたためです。
src/pkg/exp/ssa/promote.go
の変更
makeBridgeMethod
と makeImethodThunk
関数でも、デバッグロギングに logStack
が導入されました。
makeBridgeMethod
内:// 変更前: // fmt.Fprintf(os.Stderr, "makeBridgeMethod %s, %s, type %s\n", typ, cand, &sig) // 変更後: defer logStack("makeBridgeMethod %s, %s, type %s", typ, cand, &sig)()
makeImethodThunk
内:// 変更前: // fmt.Fprintf(os.Stderr, "makeImethodThunk %s.%s\n", typ, id) // 変更後: defer logStack("makeImethodThunk %s.%s", typ, id)()
src/pkg/exp/ssa/util.go
の変更
新しいユーティリティ関数 logStack
が追加されました。この関数の実装は前述の「技術的詳細」セクションで説明した通りです。
関連リンク
- Go言語の公式ドキュメント: https://go.dev/doc/
- Go言語のコンパイラに関する情報: https://go.dev/doc/compiler
- SSA形式に関する一般的な情報 (Wikipedia): https://ja.wikipedia.org/wiki/%E9%9D%99%E7%9A%84%E5%8D%98%E4%B8%80%E4%BB%A3%E5%85%A5%E5%BD%A2%E5%BC%8F
参考にした情報源リンク
- Go言語のソースコード (特に
go/ast
,go/types
,go/ssa
パッケージ) - コンパイラ設計に関する一般的な知識
- Go言語の構造体埋め込みとフィールドプロモーションに関する言語仕様
- スレッドセーフティと並行プログラミングの原則