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

[インデックス 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 があり、Tstruct { G int } であった場合、s.G というアクセスは内部的に s.T.G のように変換されていました。

しかし、この「脱糖」アプローチには以下の深刻な問題がありました。

  1. 非効率性と複雑性: 一時的なASTノードを生成する必要があり、これはメモリ割り当てと処理のオーバーヘッドを伴い、コードも複雑でした。
  2. スレッドセーフティの欠如: 新しく生成されたASTノードの型情報を、共有されている型マップ (Builder.types) に書き込む必要がありました。これは、複数のゴルーチン(スレッド)が同時にコンパイル処理を行う場合に競合状態を引き起こし、データ破損や予測不能な動作につながる可能性がありました。コンパイラのビルドプロセスは並列化されることが多いため、これは致命的な問題です。
  3. レキシカルスコープの問題: 脱糖された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.Xc.Y のように、あたかも Circle 自身のフィールドであるかのように Point のフィールドに直接アクセスできます。これを「フィールドのプロモーション (Field Promotion)」と呼びます。

フィールドのプロモーション (Field Promotion)

構造体埋め込みの際、埋め込まれた型のフィールドやメソッドは、外側の構造体のフィールドやメソッドとして「プロモート」されます。これにより、埋め込まれた型のフィールドに直接アクセスできるようになり、コードの簡潔性が向上します。コンパイラは、このようなアクセスを内部的に解決し、適切なフィールドへのパスを決定します。

抽象構文木 (AST) と go/ast パッケージ

抽象構文木 (Abstract Syntax Tree, AST) は、ソースコードの構造を木構造で表現したものです。コンパイラのフロントエンドがソースコードを解析(パース)して生成します。ASTは、プログラムの構文的な構造を抽象化しており、コンパイラの後の段階(型チェック、中間コード生成など)で利用されます。

Go言語の標準ライブラリには、GoソースコードのASTを扱うための go/ast パッケージが含まれています。例えば、ast.SelectorExprX.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 (ここで cCircle 型で Point を埋め込み、XPoint のフィールド) は、((c).Point).X のようなAST構造に変換されていました。

この変換は、以下の問題を引き起こしました。

  1. ASTの再構築とオーバーヘッド: makeSelector 関数が再帰的に呼び出され、新しい ast.SelectorExpr ノードが動的に生成されていました。これは、ASTノードのメモリ割り当てと、それらをリンクする処理のオーバーヘッドを意味します。コンパイル時に頻繁に発生する操作であるため、パフォーマンスに影響を与えます。
  2. 共有型マップへの書き込みとスレッドセーフティ: 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 というコメントは、この問題が認識されていたことを示しています。
  3. レキシカルスコープ情報の欠落: 脱糖された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 を評価し、その値(wantAddrfalse の場合)またはアドレス(wantAddrtrue の場合)をSSA Value として返します。
    • プロモートされたフィールドの場合、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) のアドレスをSSA Value として生成します。
    • path が存在する場合(プロモートされたフィールドの場合)、再帰的に fieldAddr または fieldExpr を呼び出して、パス上の各埋め込み構造体のアドレスまたは値を解決します。
    • 最終的に、FieldAddr 命令(構造体フィールドのアドレスを取得するSSA命令)を生成し、その型をポインタ型に設定して返します。
  • fieldExpr(fn *Function, base ast.Expr, path *anonFieldPath, index int, fieldType types.Type) Value:

    • fieldAddr と同様に、ベース式 base とパス path を評価しますが、最終的なフィールドの値そのものをSSA Value として生成します。
    • ベースが構造体型 (*types.Struct) の場合、Field 命令(構造体フィールドの値を取得するSSA命令)を生成します。
    • ベースがポインタ型 (*types.Pointer) の場合、FieldAddr 命令でアドレスを取得した後、emitLoad を呼び出してそのアドレスから値をロードする命令を生成します。

この新しいアプローチにより、ASTの再構築が不要になり、共有型マップへの不必要な書き込みもなくなります。これにより、パフォーマンスが向上し、スレッドセーフティの問題が解消され、レキシカルスコープのコンテキストも正しく保持されるようになります。

logStack ユーティリティの導入

このコミットでは、デバッグを目的として src/pkg/exp/ssa/util.gologStack という新しいユーティリティ関数が追加されています。

// 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 関数を使用するように変更。
    • globalValueSpecbuildFunction 関数で、デバッグロギングに logStack を使用するように変更。
  • src/pkg/exp/ssa/func.go:
    • start 関数と finish 関数から、直接的なデバッグロギング (fmt.Fprintf) の呼び出しを削除。これらは builder.gobuildFunctionlogStack に置き換えられました。
  • src/pkg/exp/ssa/promote.go:
    • makeBridgeMethodmakeImethodThunk 関数で、デバッグロギングに 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つの新しい関数が導入されました。

  1. 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 を呼び出します。
  2. fieldAddr(fn *Function, base ast.Expr, path *anonFieldPath, index int, fieldType types.Type, escaping bool) Value この関数は、フィールドのアドレスをSSA Value として生成します。

    • 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): 命令を関数に発行し、その結果のSSA Value を返します。
  3. fieldExpr(fn *Function, base ast.Expr, path *anonFieldPath, index int, fieldType types.Type) Value この関数は、フィールドの値をSSA Value として生成します。

    • 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): 命令を関数に発行し、その結果のSSA Value を返します。

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)}
    

    selectorwantAddr 引数を true に設定して呼び出し、アドレスを生成させます。

  • expr 内の ast.SelectorExpr のケース:

    // 変更前:
    // e, index := b.demoteSelector(e, fn.Pkg)
    // ... (Field または FieldAddr + Load 命令を直接生成)
    // 変更後:
    return b.selector(fn, e, false, false)
    

    selectorwantAddr 引数を false に設定して呼び出し、値を生成させます。

これらの変更により、フィールド選択のロジックが一元化され、ASTの再構築という間接的なステップが排除されました。

logStack の利用

globalValueSpecbuildFunction 関数では、デバッグロギングに 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.startFunction.finish から、直接的なデバッグロギングの呼び出しが削除されました。これは、builder.gobuildFunctionlogStack を使用する形に統合されたためです。

src/pkg/exp/ssa/promote.go の変更

makeBridgeMethodmakeImethodThunk 関数でも、デバッグロギングに 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言語のソースコード (特に go/ast, go/types, go/ssa パッケージ)
  • コンパイラ設計に関する一般的な知識
  • Go言語の構造体埋め込みとフィールドプロモーションに関する言語仕様
  • スレッドセーフティと並行プログラミングの原則