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

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

このコミットは、GoコンパイラのSSA (Static Single Assignment) フォーム生成部分における、以前のコミット(CL 7395052)で導入された重要な変更が、別のコミット(CL 7371051)によって誤って元に戻されてしまった「マージの失敗」を修正するものです。具体的には、構造体のフィールド選択(特に匿名フィールドのプロモーション)をSSA形式で適切に処理するためのロジックを復元し、関連するコードのクリーンアップとロギングの改善も行っています。

コミット

commit 37cb6f809a9f1ba1ba2682b34eba6b86e9ffd80f
Author: Alan Donovan <adonovan@google.com>
Date:   Wed Feb 27 11:39:39 2013 -0500

    exp/ssa: resolve botched merge.
    
    While submitting CL 7371051 I accidentally reverted much of CL
    7395052.  This change restores it.
    
    R=gri
    TBR=gri
    CC=golang-dev
    https://golang.org/cl/7364051

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

https://github.com/golang/go/commit/37cb6f809a9f1ba1ba2682b34eba6b86e9ffd80f

元コミット内容

exp/ssa: resolve botched merge.

While submitting CL 7371051 I accidentally reverted much of CL
7395052.  This change restores it.

R=gri
TBR=gri
CC=golang-dev
https://golang.org/cl/7364051

変更の背景

このコミットの背景には、Goコンパイラの開発プロセスにおけるマージの複雑さが存在します。コミットメッセージによると、CL 7371051("runtime: fix race in TestStackGrowth")が提出される際に、誤って以前の重要な変更であるCL 7395052(これも"runtime: fix race in TestStackGrowth"に関連する可能性が高い)の大部分が元に戻されてしまいました。

CL 7395052は、GoコンパイラのSSA(Static Single Assignment)フォーム生成フェーズにおいて、構造体のフィールド選択、特にGoの匿名フィールド(埋め込み)によるプロモーションされたフィールドの扱いに関する重要な改善を導入していたと考えられます。この改善は、SSA形式でのコード生成の正確性と効率性に寄与するものでした。

しかし、CL 7371051の適用時に、このSSA関連の変更が意図せずリバートされてしまったため、コンパイラのSSA生成ロジックに問題が生じる可能性がありました。本コミット37cb6f809a9f1ba1ba2682b34eba6b86e9ffd80fは、このマージの失敗を修正し、CL 7395052によって導入されたSSA関連の変更を完全に復元することを目的としています。これにより、GoコンパイラのSSAバックエンドが意図した通りに機能し、正確なコードを生成できるようになります。

前提知識の解説

Go SSA (Static Single Assignment) Form

SSA(Static Single Assignment)フォームは、コンパイラ最適化の中間表現(IR)の一種です。SSA形式では、各変数が一度だけ代入されるという特性を持ちます。これにより、データフロー解析や最適化が大幅に簡素化されます。Goコンパイラは、ソースコードをAST(抽象構文木)にパースした後、SSA形式に変換し、様々な最適化を適用してから最終的な機械語コードを生成します。

src/pkg/exp/ssaパッケージは、Goコンパイラの実験的なSSAバックエンドの実装を含んでいます。このパッケージは、GoプログラムのASTをSSA形式に変換し、その後の最適化やコード生成の基盤を提供します。

go/types パッケージ

go/typesパッケージは、Go言語の型システムをプログラム的に表現・操作するための標準ライブラリです。Goのソースコードを解析し、各識別子や式の型情報を決定する際に使用されます。コンパイラは、このパッケージを利用して、変数、関数、構造体フィールドなどの型を正確に把握し、型チェックやコード生成を行います。

go/ast パッケージ

go/astパッケージは、Go言語のソースコードを抽象構文木(AST)として表現するためのデータ構造と関数を提供します。ASTは、ソースコードの構文構造を木構造で表現したもので、コンパイラがソースコードを解析し、意味を理解するための最初のステップです。ast.SelectorExprは、obj.fieldpkg.Funcのようなセレクタ式を表すASTノードです。

Selector Expressions (ast.SelectorExpr)

ast.SelectorExprは、GoのASTにおいて、セレクタ式(例: object.fieldpackage.Function)を表す構造体です。これは、あるオブジェクトのフィールドやメソッド、あるいはパッケージの識別子にアクセスする際に使用されます。コンコンパイラは、このASTノードを解析して、どのフィールドや関数が参照されているかを特定し、適切なSSA命令を生成します。

Go言語には「匿名フィールド」または「埋め込みフィールド」という概念があります。これは、構造体の中に別の構造体やインターフェースをフィールド名なしで埋め込むことができる機能です。埋め込まれた構造体のフィールドやメソッドは、外側の構造体のフィールドであるかのように直接アクセスできます。これを「プロモーション(昇格)」と呼びます。

例えば、以下の構造体があるとします。

type Point struct {
    X int
    Y int
}

type Circle struct {
    Point // 匿名フィールド
    Radius int
}

func main() {
    c := Circle{Point: Point{X: 10, Y: 20}, Radius: 5}
    fmt.Println(c.X) // PointのXフィールドに直接アクセス
}

この場合、c.Xは実際にはc.Point.Xを意味します。コンパイラは、このようなプロモーションされたフィールドへのアクセスを適切に解決する必要があります。findPromotedFieldのような関数は、このプロモーションパスを解決するために使用されます。

L-values and R-values

  • L-value (Left-value): 代入演算子の左側に置くことができる式を指します。メモリ上の場所(アドレス)を持つものを表し、その値を変更することができます。変数、ポインタのデリファレンス、配列の要素、構造体のフィールドなどがL-valueになり得ます。例えば、x = 10xはL-valueです。
  • R-value (Right-value): 代入演算子の右側に置くことができる式を指します。値そのものを表し、メモリ上の場所を持つ必要はありません。リテラル、定数、関数の戻り値などがR-valueになり得ます。例えば、x = 1010はR-valueです。

コンパイラは、L-valueとR-valueを区別して、それぞれに対して適切なSSA命令(アドレスの取得、値のロードなど)を生成します。

Race Condition

レースコンディション(競合状態)とは、複数の並行プロセスやスレッドが共有リソースにアクセスする際に、そのアクセス順序によって結果が非決定的に変わってしまう状態を指します。特に、読み書きが同時に行われる場合に問題となりやすく、予期せぬバグやデータ破損を引き起こす可能性があります。TestStackGrowthのようなテストでレースコンディションが発生するということは、テストが並行して実行される際に、スタックの成長に関する内部状態が正しく同期されていないことを示唆しています。

技術的詳細

このコミットの主要な変更は、src/pkg/exp/ssa/builder.goにおける構造体フィールドのアクセス処理の再構築です。以前はdemoteSelectorという関数が、プロモーションされたフィールドを含むセレクタ式を、プロモーションされていない形式に「降格」させてから処理していました。しかし、このアプローチは非効率的であり、ASTのミューテーションを伴うため、並行処理の妨げになる可能性がありました。

本コミットでは、demoteSelectorを廃止し、より汎用的で効率的なselectorfieldAddrfieldExprの3つの関数に処理を分割・集約しています。

  1. selector(fn *Function, e *ast.SelectorExpr, wantAddr, escaping bool) Value:

    • この関数は、ast.SelectorExpr(セレクタ式、例: obj.field)を評価し、その結果のSSA Valueを返します。
    • wantAddr引数によって、フィールドのアドレス(L-value)が必要か、それとも値(R-value)が必要かを制御します。
    • escaping引数は、結果のポインタがエスケープ解析の対象となるかどうかを示します。
    • 内部でfindPromotedFieldを使用して、プロモーションされたフィールドのパスを解決します。
    • 最終的に、wantAddrの値に応じてfieldAddrまたはfieldExprを呼び出します。
  2. fieldAddr(fn *Function, base ast.Expr, path *anonFieldPath, index int, fieldType types.Type, escaping bool) Value:

    • この関数は、構造体またはポインタ型構造体のベース式から、指定されたフィールドのアドレスをSSA Valueとして返します。
    • path引数は、匿名フィールドを介したプロモーションパスを示します。
    • indexは、最終的にアクセスするフィールドのインデックスです。
    • 再帰的にfieldAddrまたはfieldExprを呼び出すことで、ネストされたプロモーションパスを処理します。
    • 最終的なフィールドのアドレスを表すFieldAddr命令を生成し、fn.emitでSSAグラフに追加します。
  3. fieldExpr(fn *Function, base ast.Expr, path *anonFieldPath, index int, fieldType types.Type) Value:

    • この関数は、構造体またはポインタ型構造体のベース式から、指定されたフィールドの値をSSA Valueとして返します。
    • pathindexの役割はfieldAddrと同様です。
    • ベースが構造体の場合(レジスタ内の非アドレス可能構造体)、Field命令を生成してフィールドの値を直接取得します。
    • ベースがポインタ型構造体(アドレス可能構造体)の場合、FieldAddr命令でアドレスを取得し、そのアドレスから値をロードするためにemitLoadを呼び出します。

これらの変更により、フィールドアクセスロジックがより明確にL-valueとR-valueの取得に分離され、SSA形式での表現がより直接的になりました。また、demoteSelectorがASTをミューテーションしていた問題も解消されています。

さらに、ロギングに関する小さな変更も含まれています。fmt.Fprintln(os.Stderr, ...)のような直接的なエラー出力が、defer logStack(...)という形式に置き換えられています。これは、デバッグ時のスタックトレース情報を含んだロギングをより統一的に行うための改善であり、src/pkg/exp/ssa/promote.goからもosパッケージのインポートが削除されています。

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

このコミットで変更された主要なファイルと関数は以下の通りです。

  • src/pkg/exp/ssa/builder.go:

    • demoteSelector関数の削除。
    • selector関数の新規追加。
    • fieldAddr関数の新規追加。
    • fieldExpr関数の新規追加。
    • addr関数内のdemoteSelector呼び出しをb.selector(fn, e, true, escaping)に置き換え。
    • expr関数内のdemoteSelector呼び出しをb.selector(fn, e, false, false)に置き換え。
    • globalValueSpecbuildFunctionBuildPackage関数内のデバッグロギングをfmt.Fprintln(os.Stderr, ...)からdefer logStack(...)形式に変更。
  • src/pkg/exp/ssa/promote.go:

    • osパッケージのインポートを削除。
    • buildMethodSet関数内のデバッグロギングをfmt.Fprintf(os.Stderr, ...)からdefer logStack(...)形式に変更。

コアとなるコードの解説

src/pkg/exp/ssa/builder.go

selector 関数

func (b *Builder) selector(fn *Function, e *ast.SelectorExpr, wantAddr, escaping bool) Value {
	id := makeId(e.Sel.Name, fn.Pkg.Types)
	st := underlyingType(deref(b.exprType(e.X))).(*types.Struct)
	index := -1
	for i, f := range st.Fields {
		if IdFromQualifiedName(f.QualifiedName) == id {
			index = i
			break
		}
	}
	var path *anonFieldPath
	if index == -1 {
		// Not a named field.  Use breadth-first algorithm.
		path, index = findPromotedField(st, id)
		if path == nil {
			panic("field not found, even with promotion: " + e.Sel.Name)
		}
	}
	fieldType := b.exprType(e)
	if wantAddr {
		return b.fieldAddr(fn, e.X, path, index, fieldType, escaping)
	}
	return b.fieldExpr(fn, e.X, path, index, fieldType)
}

このselector関数は、ast.SelectorExpr(例: s.f)をSSA形式に変換する主要なエントリポイントです。

  1. まず、セレクタの識別子(e.Sel.Name)からフィールドのIDを生成します。
  2. ベース式e.Xの型(構造体またはポインタ型構造体)を取得し、その構造体型stから、直接指定されたフィールドidのインデックスを探します。
  3. もし直接見つからなければ(index == -1)、Goの匿名フィールドのプロモーションルールに従って、findPromotedField関数を使ってプロモーションパスpathと最終的なフィールドのインデックスindexを探索します。
  4. フィールドの型fieldTypeを取得します。
  5. wantAddrフラグに基づいて、フィールドのアドレスが必要な場合はfieldAddrを、値が必要な場合はfieldExprを呼び出し、その結果を返します。

fieldAddr 関数

func (b *Builder) fieldAddr(fn *Function, base ast.Expr, path *anonFieldPath, index int, fieldType types.Type, escaping bool) Value {
	var x Value
	if path != nil {
		switch underlyingType(path.field.Type).(type) {
		case *types.Struct:
			x = b.fieldAddr(fn, base, path.tail, path.index, path.field.Type, escaping)
		case *types.Pointer:
			x = b.fieldExpr(fn, base, path.tail, path.index, path.field.Type)
		}
	} else {
		switch underlyingType(b.exprType(base)).(type) {
		case *types.Struct:
			x = b.addr(fn, base, escaping).(address).addr
		case *types.Pointer:
			x = b.expr(fn, base)
		}
	}
	v := &FieldAddr{
		X:     x,
		Field: index,
	}
	v.setType(pointer(fieldType))
	return fn.emit(v)
}

このfieldAddr関数は、指定されたベース式baseから、プロモーションパスpathとフィールドインデックスindexを介して、最終的なフィールドのアドレスをSSA Valueとして生成します。

  1. pathnilでない場合(プロモーションされたフィールドへのアクセスの場合)、再帰的にfieldAddrまたはfieldExprを呼び出して、中間パスのSSA Value xを取得します。
    • 中間フィールドが構造体の場合(値渡し)、そのフィールドのアドレスを再帰的に取得します。
    • 中間フィールドがポインタの場合、そのフィールドの値を再帰的に取得します。
  2. pathnilの場合(直接フィールドアクセスの場合)、ベース式baseの型に応じてb.addr(アドレス取得)またはb.expr(値取得)を呼び出して、ベースのSSA Value xを取得します。
  3. 最終的に、FieldAddrというSSA命令を生成します。これは、X(ベースのアドレスまたはポインタ)とField(フィールドのインデックス)から、目的のフィールドのアドレスを計算する命令です。
  4. 生成されたFieldAddr命令の型を設定し、fn.emit(v)でSSAグラフにこの命令を追加し、その結果のSSA Valueを返します。

fieldExpr 関数

func (b *Builder) fieldExpr(fn *Function, base ast.Expr, path *anonFieldPath, index int, fieldType types.Type) Value {
	var x Value
	if path != nil {
		x = b.fieldExpr(fn, base, path.tail, path.index, path.field.Type)
	} else {
		x = b.expr(fn, base)
	}
	switch underlyingType(x.Type()).(type) {
	case *types.Struct:
		v := &Field{
			X:     x,
			Field: index,
		}
		v.setType(fieldType)
		return fn.emit(v)

	case *types.Pointer: // *struct
		v := &FieldAddr{
			X:     x,
			Field: index,
		}
		v.setType(pointer(fieldType))
		return emitLoad(fn, fn.emit(v))
	}
	panic("unreachable")
}

このfieldExpr関数は、指定されたベース式baseから、プロモーションパスpathとフィールドインデックスindexを介して、最終的なフィールドの値をSSA Valueとして生成します。

  1. pathnilでない場合、再帰的にfieldExprを呼び出して、中間パスのSSA Value xを取得します。
  2. pathnilの場合、ベース式baseの値をb.exprで取得し、SSA Value xとします。
  3. 取得したxの型に応じて処理を分岐します。
    • *types.Structの場合(ベースが構造体そのもの、つまり値渡しの場合)、FieldというSSA命令を生成します。これは、X(ベースの構造体値)とField(フィールドのインデックス)から、目的のフィールドの値を直接取得する命令です。
    • *types.Pointerの場合(ベースが構造体へのポインタの場合)、まずFieldAddr命令でフィールドのアドレスを取得します。その後、emitLoad関数を呼び出して、そのアドレスから値をロードする命令を生成します。
  4. 生成されたSSA命令の型を設定し、fn.emit(v)でSSAグラフに追加し、その結果のSSA Valueを返します。

addr および expr 関数の変更

addr関数(L-valueのSSA変換)とexpr関数(R-valueのSSA変換)は、ast.SelectorExprを処理する部分で、以前のb.demoteSelectorの呼び出しを、新しく導入されたb.selectorの呼び出しに置き換えました。

  • addr関数内では、フィールドのアドレスが必要なのでb.selector(fn, e, true, escaping)を呼び出します。
  • expr関数内では、フィールドの値が必要なのでb.selector(fn, e, false, false)を呼び出します。

これにより、フィールドアクセスロジックが一元化され、demoteSelectorの複雑な処理が不要になりました。

ロギングの変更

globalValueSpecbuildFunctionBuildPackagebuildMethodSetといった関数では、デバッグ目的のロギング出力がfmt.Fprintln(os.Stderr, ...)fmt.Fprintf(os.Stderr, ...)からdefer logStack(...)という形式に変更されました。これは、GoのdeferステートメントとlogStack関数を組み合わせることで、関数の開始と終了時に自動的にロギングを行い、特にデバッグ時に呼び出しスタックの情報を含めるなど、より構造化されたロギングを可能にするための改善です。これにより、src/pkg/exp/ssa/promote.goからosパッケージのインポートが不要になりました。

関連リンク

参考にした情報源リンク

  • Web search results for "golang CL 7371051": CL 7371051, titled "runtime: fix race in TestStackGrowth", addresses a race condition within the TestStackGrowth test in the Go runtime. This commit ensures the test runs reliably by preventing concurrent access issues that could lead to intermittent failures.
  • Web search results for "golang CL 7395052": CL 7395052 is titled "runtime: fix race in TestStackGrowth". This commit addresses a race condition found in the TestStackGrowth test within the Go runtime.