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

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

このコミットは、Go言語の実験的なSSA (Static Single Assignment) パッケージ (exp/ssa) において、panic 関数呼び出しを表現するための専用の Panic 命令を導入するものです。これにより、panic 呼び出し後の制御フローの扱いが改善され、中間表現の効率性と明確性が向上します。

コミット

exp/ssa: 専用の Panic 命令を追加。

panic 呼び出し後の自己ループの必要性を回避することで、基本ブロックの数を大幅に削減します。

R=gri CC=golang-dev, iant https://golang.org/cl/7403043

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

https://github.com/golang/go/commit/92cbf82f1443223e21856f408b50821082babecc

元コミット内容

commit 92cbf82f1443223e21856f408b50821082babecc
Author: Alan Donovan <adonovan@google.com>
Date:   Thu Feb 21 12:14:33 2013 -0500

    exp/ssa: add dedicated Panic instruction.
    
    By avoiding the need for self-loops following calls to panic,
    we reduce the number of basic blocks considerably.
    
    R=gri
    CC=golang-dev, iant
    https://golang.org/cl/7403043

変更の背景

Go言語のコンパイラは、ソースコードを中間表現に変換し、様々な最適化や解析を行います。この中間表現の一つにSSA (Static Single Assignment) 形式があります。panic はGoプログラムの実行を中断させる特殊な関数であり、その呼び出しは通常の関数呼び出しとは異なる制御フローの挙動を伴います。

このコミット以前は、panic 呼び出しがSSA形式でどのように表現されていたか、具体的な実装はコミットメッセージからは読み取れませんが、「自己ループの必要性を回避する」という記述から、おそらくpanic呼び出しの直後に、その後のコードが到達不能であることを示すために、制御フローグラフ(CFG)上で自身に戻るような「自己ループ」を持つ基本ブロックが生成されていたと推測されます。このような表現方法は、以下の問題を引き起こす可能性があります。

  1. 基本ブロックの増加: 自己ループを持つ基本ブロックは、プログラムの論理的な流れとは異なる余分なブロックを生成し、SSA形式全体の基本ブロック数を増加させます。
  2. 解析の複雑化: 余分な基本ブロックや特殊な制御フローパターンは、データフロー解析や最適化パスの複雑性を増大させる可能性があります。
  3. セマンティクスの不明瞭さ: panicという特殊な挙動を通常の関数呼び出しと自己ループの組み合わせで表現することは、その意図をSSA形式上で明確に伝える上での課題となります。

これらの問題を解決し、SSA形式の効率性と明確性を向上させるために、panicの挙動を直接的に表現する専用の Panic 命令が導入されました。

前提知識の解説

このコミットを理解するためには、以下の概念についての基本的な知識が必要です。

  • Go言語の panicrecover:
    • panic は、プログラムの通常の実行フローを中断させる組み込み関数です。通常、回復不能なエラーや予期せぬ状況が発生した場合に呼び出されます。panic が発生すると、現在のゴルーチンの実行が停止し、遅延関数(defer)が実行されながらスタックが巻き戻されます。
    • recover は、panic から回復するために defer 関数内で呼び出される組み込み関数です。recover が呼び出されると、panic の値が返され、プログラムの実行を再開できます。
  • コンパイラと中間表現 (IR):
    • コンパイラは、ソースコードを機械語に変換するソフトウェアです。この変換プロセスでは、通常、ソースコードを直接機械語に変換するのではなく、いくつかの段階的な中間表現(IR)を経由します。
    • IRは、ソース言語とターゲットマシンコードの中間的な抽象化レベルを提供し、最適化や解析を容易にします。
  • 抽象構文木 (AST: Abstract Syntax Tree):
    • ソースコードの構文構造を木構造で表現したものです。コンパイラの最初の段階で生成され、プログラムの構造を抽象的に表現します。
  • SSA (Static Single Assignment) 形式:
    • 中間表現の一種で、各変数がプログラム内で一度だけ代入されることを保証する特性を持ちます。これにより、データフロー解析が大幅に簡素化され、より効果的な最適化が可能になります。
    • SSA形式では、変数の再代入は新しい変数として扱われます(例: x = 1, x = 2x1 = 1, x2 = 2 となる)。
  • 基本ブロック (Basic Block):
    • 制御フローグラフ(CFG)の構成要素であり、単一の入り口と単一の出口を持つ命令のシーケンスです。基本ブロック内の命令は、常に順序通りに実行されます。
  • 制御フローグラフ (CFG: Control Flow Graph):
    • プログラムの実行可能なすべてのパスを抽象的に表現したグラフです。ノードは基本ブロックを表し、エッジは基本ブロック間の制御フローの遷移を表します。
  • 組み込み関数 (Built-in Functions):
    • Go言語に最初から用意されている特別な関数で、len, cap, new, make, panic, recover, print, println などがあります。これらは通常の関数とは異なり、コンパイラによって特別に扱われます。

技術的詳細

このコミットの技術的な核心は、GoのSSA中間表現に Panic という新しい命令タイプを導入し、panic 呼び出しのセマンティクスをより直接的かつ効率的に表現することにあります。

以前のSSA生成では、panic 呼び出しは通常の関数呼び出しとして扱われた後、その後のコードが到達不能であることを示すために、制御フローグラフに特殊な構造(自己ループを持つ基本ブロック)が追加されていたと考えられます。これは、panic が呼び出された時点でそのブロック以降の実行が停止するという事実をSSA形式で表現するための回避策でした。

新しい Panic 命令の導入により、以下の変更が行われました。

  1. Panic 命令の定義: src/pkg/exp/ssa/ssa.goPanic 構造体が追加されました。この構造体は、panic の引数となる値 (X) を持ち、SSA命令としての振る舞いを定義します。重要なのは、Panic 命令が「その基本ブロックの最後の命令でなければならず、後続の基本ブロックを持ってはならない」という制約が課せられている点です。これは、panic が呼び出された時点で制御フローが終了することを直接的に表現するためです。
  2. SSAビルダーの変更 (src/pkg/exp/ssa/builder.go):
    • builtin 関数内で panic 組み込み関数が検出された場合、新しい Panic 命令が生成され、現在の基本ブロックが「到達不能」な新しい基本ブロックに切り替えられます。これにより、panic 呼び出し後のコードが生成されないようになります。
    • 以前の panic 処理で使われていた wasPanic フラグや、自己ループを生成していた emitSelfLoop の呼び出しが削除されました。これは、Panic 命令がその役割を担うようになったためです。
    • print, println, panic などの組み込み関数の引数型が tEface (interface{}) に統一されました。
  3. SSAエミッターの変更 (src/pkg/exp/ssa/emit.go):
    • 自己ループを生成していた emitSelfLoop 関数が完全に削除されました。
  4. SSAインタープリタの変更 (src/pkg/exp/ssa/interp/interp.go, src/pkg/exp/ssa/interp/ops.go):
    • SSAインタープリタは、生成されたSSA命令を実行する役割を担います。新しい Panic 命令が導入されたため、インタープリタもこの命令を認識し、適切に処理するように更新されました。具体的には、*ssa.Panic 命令が実行された際に、Goの panic 機構を模倣して targetPanic 型のパニックを発生させるようになりました。
    • go panic(x)defer panic(x) のように、panicgo ステートメントや defer ステートメントの引数として渡される場合は、引き続き通常の組み込み関数呼び出しとして扱われることがコメントで明記されました。これは、これらのケースでは panic が直接的な制御フローの終了を意味するのではなく、ゴルーチンや遅延実行のコンテキストで評価されるためです。
  5. SSAの健全性チェック (src/pkg/exp/ssa/sanity.go):
    • SSA形式の整合性をチェックする sanity パッケージも更新されました。Panic 命令が基本ブロックの最後の命令であること、および Panic で終了するブロックが後続のブロックを持たないことを検証するチェックが追加されました。これにより、SSA形式の生成が正しく行われていることを保証します。
  6. SSAの出力形式 (src/pkg/exp/ssa/print.go):
    • Panic 命令がSSA形式で出力される際の文字列表現 (panic t0 のような形式) が定義されました。

これらの変更により、panic のセマンティクスがSSA形式でより正確かつ効率的に表現されるようになり、コンパイラのバックエンドでの解析や最適化が簡素化されることが期待されます。特に、基本ブロックの削減は、コンパイラの処理速度や生成されるコードの品質に良い影響を与える可能性があります。

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

src/pkg/exp/ssa/builder.go

--- a/src/pkg/exp/ssa/builder.go
+++ b/src/pkg/exp/ssa/builder.go
@@ -62,6 +62,7 @@ var (
  	tInvalid    = types.Typ[types.Invalid]
  	tUntypedNil = types.Typ[types.UntypedNil]
  	tRangeIter  = &types.Basic{Name: "iter"} // the type of all "range" iterators
+	tEface      = new(types.Interface)
 
  	// The result type of a "select".
  	tSelect = &types.Result{Values: []*types.Var{
@@ -512,6 +513,11 @@ func (b *Builder) builtin(fn *Function, name string, args []ast.Expr, typ types.
  			return intLiteral(at.Len)
  		}
  		// Otherwise treat as normal.
+
+	case "panic":
+		fn.emit(&Panic{X: emitConv(fn, b.expr(fn, args[0]), tEface)})
+		fn.currentBlock = fn.newBasicBlock("unreachable")
+		return vFalse // any non-nil Value will do
  	}
  	return nil // treat all others as a regular function call
  }
@@ -774,32 +780,20 @@ func (b *Builder) expr(fn *Function, e ast.Expr) Value {
  			// Type conversion, e.g. string(x) or big.Int(x)
  			return emitConv(fn, b.expr(fn, e.Args[0]), typ)
  		}
-		// Call to "intrinsic" built-ins, e.g. new, make.
-		wasPanic := false
+		// Call to "intrinsic" built-ins, e.g. new, make, panic.
  		if id, ok := e.Fun.(*ast.Ident); ok {
  			obj := b.obj(id)
  			if _, ok := fn.Prog.Builtins[obj]; ok {
  				if v := b.builtin(fn, id.Name, e.Args, typ); v != nil {
  					return v
  				}
-				wasPanic = id.Name == "panic"
  			}
  		}
  		// Regular function call.
  		var v Call
  		b.setCall(fn, e, &v.CallCommon)
  		v.setType(typ)
-		fn.emit(&v)
-
-		// Compile panic as if followed by for{} so that its
-		// successor is unreachable.
-		// TODO(adonovan): consider a dedicated Panic instruction
-		// (in which case, don't forget Go and Defer).
-		if wasPanic {
-			emitSelfLoop(fn)
-			fn.currentBlock = fn.newBasicBlock("unreachable")
-		}
-		return &v
+		return fn.emit(&v)
 
  	case *ast.UnaryExpr:
  		switch e.Op {
@@ -1161,7 +1155,7 @@ func (b *Builder) setCall(fn *Function, e *ast.CallExpr, c *CallCommon) {
  			bptypes = append(bptypes, nil) // map
  			bptypes = append(bptypes, nil) // key
  		case "print", "println": // print{,ln}(any, ...any)
-			vt = new(types.Interface) // variadic
+			vt = tEface // variadic
  			if !c.HasEllipsis {
  				args, varargs = args[:1], args[1:]
  			}
@@ -1188,7 +1182,7 @@ func (b *Builder) setCall(fn *Function, e *ast.CallExpr, c *CallCommon) {
  			}
  			bptypes = append(bptypes, argType, argType)
  		case "panic":
-			bptypes = append(bptypes, new(types.Interface))
+			bptypes = append(bptypes, tEface)
  		case "recover":
  			// no-op
  		default:
@@ -2257,14 +2251,14 @@ start:
 
  	case *ast.GoStmt:
  		// The "intrinsics" new/make/len/cap are forbidden here.
-		// panic() is not forbidden, but is not (yet) an intrinsic.
+		// panic is treated like an ordinary function call.
  		var v Go
  		b.setCall(fn, s.Call, &v.CallCommon)
  		fn.emit(&v)
 
  	case *ast.DeferStmt:
  		// The "intrinsics" new/make/len/cap are forbidden here.
-		// panic() is not forbidden, but is not (yet) an intrinsic.
+		// panic is treated like an ordinary function call.
  		var v Defer
  		b.setCall(fn, s.Call, &v.CallCommon)
  		fn.emit(&v)

src/pkg/exp/ssa/emit.go

--- a/src/pkg/exp/ssa/emit.go
+++ b/src/pkg/exp/ssa/emit.go
@@ -247,15 +247,3 @@ func emitTailCall(f *Function, call *Call) {
  	f.emit(&ret)
  	f.currentBlock = nil
 }
-
-// emitSelfLoop emits to f a self-loop.
-// This is a defensive measure to ensure control-flow integrity.
-// It should never be reachable.
-// Postcondition: f.currentBlock is nil.
-//
-func emitSelfLoop(f *Function) {
-	loop := f.newBasicBlock("selfloop")
-	emitJump(f, loop)
-	f.currentBlock = loop
-	emitJump(f, loop)
-}

src/pkg/exp/ssa/interp/interp.go

--- a/src/pkg/exp/ssa/interp/interp.go
+++ b/src/pkg/exp/ssa/interp/interp.go
@@ -171,6 +171,9 @@ func visitInstr(fr *frame, instr ssa.Instruction) continuation {
  		}
  		return kReturn
 
+	case *ssa.Panic:
+		panic(targetPanic{fr.get(instr.X)})
+
  	case *ssa.Send:
  		fr.get(instr.Chan).(chan value) <- copyVal(fr.get(instr.X))
 
@@ -475,7 +478,7 @@ func callSSA(i *interpreter, caller *frame, callpos token.Pos, fn *ssa.Function,
 
  	for {
  		if i.mode&EnableTracing != 0 {
-			fmt.Fprintf(os.Stderr, ".%s:\n", fr.block.Name)
+			fmt.Fprintf(os.Stderr, ".%s:\n", fr.block)
  		}
  	block:
  		for _, instr = range fr.block.Instrs {

src/pkg/exp/ssa/interp/ops.go

--- a/src/pkg/exp/ssa/interp/ops.go
+++ b/src/pkg/exp/ssa/interp/ops.go
@@ -16,6 +16,9 @@ type targetPanic struct {
  	v value
  }
 
+// If the target program calls exit, the interpreter panics with this type.
+type exitPanic int
+
 // literalValue returns the value of the literal with the
 // dynamic type tag appropriate for l.Type().
 func literalValue(l *ssa.Literal) value {
@@ -974,6 +977,8 @@ func callBuiltin(caller *frame, callpos token.Pos, fn *ssa.Builtin, args []value
  		}
 
  	case "panic":
+		// ssa.Panic handles most cases; this is only for "go
+		// panic" or "defer panic".
  		panic(targetPanic{args[0]})
 
  	case "recover":

src/pkg/exp/ssa/print.go

--- a/src/pkg/exp/ssa/print.go
+++ b/src/pkg/exp/ssa/print.go
@@ -282,6 +282,10 @@ func (s *Go) String() string {
  	return printCall(&s.CallCommon, "go ", s)
  }
 
+func (s *Panic) String() string {
+	return "panic " + relName(s.X, s)
+}
+
 func (s *Ret) String() string {
  	var b bytes.Buffer
  	b.WriteString("ret")

src/pkg/exp/ssa/sanity.go

--- a/src/pkg/exp/ssa/sanity.go
+++ b/src/pkg/exp/ssa/sanity.go
@@ -96,7 +96,7 @@ func findDuplicate(blocks []*BasicBlock) *BasicBlock {
 
  func (s *sanity) checkInstr(idx int, instr Instruction) {
  	switch instr := instr.(type) {
-	case *If, *Jump, *Ret:
+	case *If, *Jump, *Ret, *Panic:
  		s.errorf("control flow instruction not at end of block")
  	case *Phi:
  		if idx == 0 {
@@ -192,6 +192,12 @@ func (s *sanity) checkFinalInstr(idx int, instr Instruction) {
  		}
  		// TODO(adonovan): check number and types of results
 
+	case *Panic:
+		if nsuccs := len(s.block.Succs); nsuccs != 0 {
+			s.errorf("Panic-terminated block has %d successors; expected none", nsuccs)
+			return
+		}
+
  	default:
  		s.errorf("non-control flow instruction at end of block")
  	}

src/pkg/exp/ssa/ssa.go

--- a/src/pkg/exp/ssa/ssa.go
+++ b/src/pkg/exp/ssa/ssa.go
@@ -248,7 +248,7 @@ type Function struct {
  // An SSA basic block.
  //
  // The final element of Instrs is always an explicit transfer of
-// control (If, Jump or Ret).
+// control (If, Jump, Ret or Panic).
 //
 // A block may contain no Instructions only if it is unreachable,
 // i.e. Preds is nil.  Empty blocks are typically pruned.
@@ -842,6 +842,22 @@ type Ret struct {
  	Results []Value
  }
 
+// Panic initiates a panic with value X.
+//
+// A Panic instruction must be the last instruction of its containing
+// BasicBlock, which must have no successors.
+//
+// NB: 'go panic(x)' and 'defer panic(x)' do not use this instruction;
+// they are treated as calls to a built-in function.
+//
+// Example printed form:
+// 	panic t0
+//
+type Panic struct {
+	anInstruction
+	X Value // an interface{}
+}
+
 // Go creates a new goroutine and calls the specified function
 // within it.
 //
@@ -1125,6 +1141,7 @@ func (*MakeMap) ImplementsInstruction()         {}
 func (*MakeSlice) ImplementsInstruction()       {}
 func (*MapUpdate) ImplementsInstruction()       {}
 func (*Next) ImplementsInstruction()            {}
+func (*Panic) ImplementsInstruction()           {}
 func (*Phi) ImplementsInstruction()             {}
 func (*Range) ImplementsInstruction()           {}
 func (*Ret) ImplementsInstruction()             {}
@@ -1227,6 +1244,10 @@ func (v *Next) Operands(rands []*Value) []*Value {
  	return append(rands, &v.Iter)
  }
 
+func (s *Panic) Operands(rands []*Value) []*Value {
+	return append(rands, &s.X)
+}
+
 func (v *Phi) Operands(rands []*Value) []*Value {
  	for i := range v.Edges {
  		rands = append(rands, &v.Edges[i])

コアとなるコードの解説

src/pkg/exp/ssa/builder.go

  • tEface = new(types.Interface) の追加: panic の引数や print/println の可変長引数が interface{} 型として扱われるための型定義です。
  • builtin 関数内の panic 処理の追加:
    • case "panic": ブロックが追加され、panic 組み込み関数が検出された際の特殊な処理が定義されました。
    • fn.emit(&Panic{X: emitConv(fn, b.expr(fn, args[0]), tEface)}) により、新しい Panic 命令がSSA形式で生成されます。b.expr(fn, args[0])panic の引数をSSA値に変換し、emitConv はそれを interface{} 型に変換します。
    • fn.currentBlock = fn.newBasicBlock("unreachable") は、panic が呼び出された時点で現在の基本ブロック以降のコードが到達不能になることを示します。これにより、コンパイラはこれ以上コードを生成する必要がなくなります。
  • expr 関数からの wasPanic フラグと emitSelfLoop の削除:
    • 以前は panic 呼び出し後に wasPanic フラグをチェックし、emitSelfLoop を呼び出して自己ループを生成していましたが、専用の Panic 命令が導入されたため、これらの処理は不要になりました。これにより、SSA生成ロジックが簡素化されます。
  • setCall 関数内の panic 引数型の変更:
    • print, println, panic の引数型が new(types.Interface) から tEface に変更されました。これは、interface{} 型の表現を統一するためのものです。
  • GoStmtDeferStmt のコメント更新:
    • panicgodefer ステートメント内で使用される場合、もはや「組み込み関数ではない」というコメントが削除され、「通常の関数呼び出しとして扱われる」という記述に更新されました。これは、直接的な panic 呼び出しとは異なり、これらのコンテキストでは Panic 命令が生成されないことを示唆しています。

src/pkg/exp/ssa/emit.go

  • emitSelfLoop 関数の削除:
    • panic 呼び出し後に制御フローの到達不能性を示すために使用されていた emitSelfLoop 関数が完全に削除されました。これは、Panic 命令がその役割を担うようになったためです。

src/pkg/exp/ssa/interp/interp.go

  • visitInstr 関数内の *ssa.Panic 処理の追加:
    • SSAインタープリタが Panic 命令に遭遇した場合の処理が追加されました。
    • panic(targetPanic{fr.get(instr.X)}) により、Panic 命令の引数 X の値を使って、Goのランタイムパニックを模倣した targetPanic 型のパニックが実際に発生します。これにより、SSAインタープリタは panic の挙動を正確にシミュレートできます。
  • fmt.Fprintf の変更:
    • fr.block.Name から fr.block への変更は、デバッグ出力における基本ブロックの表示方法の微調整であり、機能的な変更ではありません。

src/pkg/exp/ssa/interp/ops.go

  • exitPanic 型の追加:
    • ターゲットプログラムが exit を呼び出した場合にインタープリタがパニックする際に使用される新しい型です。このコミットの直接的な主題である panic 命令とは異なりますが、関連するパニック処理の改善の一部です。
  • callBuiltin 関数内の panic 処理のコメント更新:
    • case "panic": ブロックに // ssa.Panic handles most cases; this is only for "go // panic" or "defer panic". というコメントが追加されました。これは、ほとんどの panic 呼び出しは新しい Panic 命令によって処理されるが、go panic(x)defer panic(x) のようなケースでは、引き続き組み込み関数呼び出しとして扱われることを明確にしています。

src/pkg/exp/ssa/print.go

  • Panic 命令の String() メソッドの追加:
    • func (s *Panic) String() string { return "panic " + relName(s.X, s) } が追加されました。これにより、Panic 命令がSSA形式のテキスト表現として出力される際に、panic t0 のような可読性の高い形式で表示されるようになります。

src/pkg/exp/ssa/sanity.go

  • checkInstr 関数内の制御フロー命令リストへの *Panic の追加:
    • *If, *Jump, *Ret と並んで *Panic が追加されました。これは、Panic 命令が基本ブロックの最後の命令でなければならないという制約をチェックするためのものです。
  • checkFinalInstr 関数内の *Panic 処理の追加:
    • case *Panic: ブロックが追加され、Panic 命令で終了する基本ブロックが後続のブロックを持たないことを検証するチェックが実装されました。if nsuccs := len(s.block.Succs); nsuccs != 0 { ... } は、後続ブロックが存在しないことを保証します。これにより、panic のセマンティクス(実行がそこで終了する)がSSA形式で正しく表現されていることを確認します。

src/pkg/exp/ssa/ssa.go

  • Function 構造体のコメント更新:
    • Instrs の最後の要素が制御フロー命令であることを示すコメントに、Panic が追加されました。
  • Panic 構造体の定義:
    • type Panic struct { anInstruction; X Value } として新しいSSA命令が定義されました。
    • コメントで、Panic 命令が panic の値 X を持つこと、および「その基本ブロックの最後の命令でなければならず、後続のブロックを持ってはならない」という重要な制約が明記されています。
    • go panic(x)defer panic(x) がこの命令を使用しないことも注記されています。
    • 出力形式の例 (panic t0) も示されています。
  • ImplementsInstruction() メソッドへの *Panic の追加:
    • func (*Panic) ImplementsInstruction() {} が追加され、Panic がSSA命令インターフェースを実装していることを示します。
  • Operands() メソッドへの *Panic の追加:
    • func (s *Panic) Operands(rands []*Value) []*Value { return append(rands, &s.X) } が追加されました。これにより、Panic 命令がオペランド(この場合は panic の引数 X)を持つことがSSAフレームワークに認識されます。

これらの変更は、GoコンパイラのSSAバックエンドにおける panic 処理の根本的な改善を示しており、より効率的で正確なコード生成と解析を可能にします。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Go言語のソースコード (特に src/pkg/exp/ssa ディレクトリ)
  • コンパイラ設計に関する一般的な知識 (SSA形式、基本ブロック、制御フローグラフなど)
  • GitHubのコミット履歴と差分表示
  • https://golang.org/cl/7403043 (元のGo Gerritの変更リスト)