[インデックス 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)上で自身に戻るような「自己ループ」を持つ基本ブロックが生成されていたと推測されます。このような表現方法は、以下の問題を引き起こす可能性があります。
- 基本ブロックの増加: 自己ループを持つ基本ブロックは、プログラムの論理的な流れとは異なる余分なブロックを生成し、SSA形式全体の基本ブロック数を増加させます。
- 解析の複雑化: 余分な基本ブロックや特殊な制御フローパターンは、データフロー解析や最適化パスの複雑性を増大させる可能性があります。
- セマンティクスの不明瞭さ:
panic
という特殊な挙動を通常の関数呼び出しと自己ループの組み合わせで表現することは、その意図をSSA形式上で明確に伝える上での課題となります。
これらの問題を解決し、SSA形式の効率性と明確性を向上させるために、panic
の挙動を直接的に表現する専用の Panic
命令が導入されました。
前提知識の解説
このコミットを理解するためには、以下の概念についての基本的な知識が必要です。
- Go言語の
panic
とrecover
:panic
は、プログラムの通常の実行フローを中断させる組み込み関数です。通常、回復不能なエラーや予期せぬ状況が発生した場合に呼び出されます。panic
が発生すると、現在のゴルーチンの実行が停止し、遅延関数(defer
)が実行されながらスタックが巻き戻されます。recover
は、panic
から回復するためにdefer
関数内で呼び出される組み込み関数です。recover
が呼び出されると、panic
の値が返され、プログラムの実行を再開できます。
- コンパイラと中間表現 (IR):
- コンパイラは、ソースコードを機械語に変換するソフトウェアです。この変換プロセスでは、通常、ソースコードを直接機械語に変換するのではなく、いくつかの段階的な中間表現(IR)を経由します。
- IRは、ソース言語とターゲットマシンコードの中間的な抽象化レベルを提供し、最適化や解析を容易にします。
- 抽象構文木 (AST: Abstract Syntax Tree):
- ソースコードの構文構造を木構造で表現したものです。コンパイラの最初の段階で生成され、プログラムの構造を抽象的に表現します。
- SSA (Static Single Assignment) 形式:
- 中間表現の一種で、各変数がプログラム内で一度だけ代入されることを保証する特性を持ちます。これにより、データフロー解析が大幅に簡素化され、より効果的な最適化が可能になります。
- SSA形式では、変数の再代入は新しい変数として扱われます(例:
x = 1
,x = 2
はx1 = 1
,x2 = 2
となる)。
- 基本ブロック (Basic Block):
- 制御フローグラフ(CFG)の構成要素であり、単一の入り口と単一の出口を持つ命令のシーケンスです。基本ブロック内の命令は、常に順序通りに実行されます。
- 制御フローグラフ (CFG: Control Flow Graph):
- プログラムの実行可能なすべてのパスを抽象的に表現したグラフです。ノードは基本ブロックを表し、エッジは基本ブロック間の制御フローの遷移を表します。
- 組み込み関数 (Built-in Functions):
- Go言語に最初から用意されている特別な関数で、
len
,cap
,new
,make
,panic
,recover
,print
,println
などがあります。これらは通常の関数とは異なり、コンパイラによって特別に扱われます。
- Go言語に最初から用意されている特別な関数で、
技術的詳細
このコミットの技術的な核心は、GoのSSA中間表現に Panic
という新しい命令タイプを導入し、panic
呼び出しのセマンティクスをより直接的かつ効率的に表現することにあります。
以前のSSA生成では、panic
呼び出しは通常の関数呼び出しとして扱われた後、その後のコードが到達不能であることを示すために、制御フローグラフに特殊な構造(自己ループを持つ基本ブロック)が追加されていたと考えられます。これは、panic
が呼び出された時点でそのブロック以降の実行が停止するという事実をSSA形式で表現するための回避策でした。
新しい Panic
命令の導入により、以下の変更が行われました。
Panic
命令の定義:src/pkg/exp/ssa/ssa.go
にPanic
構造体が追加されました。この構造体は、panic
の引数となる値 (X
) を持ち、SSA命令としての振る舞いを定義します。重要なのは、Panic
命令が「その基本ブロックの最後の命令でなければならず、後続の基本ブロックを持ってはならない」という制約が課せられている点です。これは、panic
が呼び出された時点で制御フローが終了することを直接的に表現するためです。- SSAビルダーの変更 (
src/pkg/exp/ssa/builder.go
):builtin
関数内でpanic
組み込み関数が検出された場合、新しいPanic
命令が生成され、現在の基本ブロックが「到達不能」な新しい基本ブロックに切り替えられます。これにより、panic
呼び出し後のコードが生成されないようになります。- 以前の
panic
処理で使われていたwasPanic
フラグや、自己ループを生成していたemitSelfLoop
の呼び出しが削除されました。これは、Panic
命令がその役割を担うようになったためです。 print
,println
,panic
などの組み込み関数の引数型がtEface
(interface{}) に統一されました。
- SSAエミッターの変更 (
src/pkg/exp/ssa/emit.go
):- 自己ループを生成していた
emitSelfLoop
関数が完全に削除されました。
- 自己ループを生成していた
- 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)
のように、panic
がgo
ステートメントやdefer
ステートメントの引数として渡される場合は、引き続き通常の組み込み関数呼び出しとして扱われることがコメントで明記されました。これは、これらのケースではpanic
が直接的な制御フローの終了を意味するのではなく、ゴルーチンや遅延実行のコンテキストで評価されるためです。
- SSAインタープリタは、生成されたSSA命令を実行する役割を担います。新しい
- SSAの健全性チェック (
src/pkg/exp/ssa/sanity.go
):- SSA形式の整合性をチェックする
sanity
パッケージも更新されました。Panic
命令が基本ブロックの最後の命令であること、およびPanic
で終了するブロックが後続のブロックを持たないことを検証するチェックが追加されました。これにより、SSA形式の生成が正しく行われていることを保証します。
- SSA形式の整合性をチェックする
- 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{}
型の表現を統一するためのものです。
GoStmt
とDeferStmt
のコメント更新:panic
がgo
やdefer
ステートメント内で使用される場合、もはや「組み込み関数ではない」というコメントが削除され、「通常の関数呼び出しとして扱われる」という記述に更新されました。これは、直接的な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
の挙動を正確にシミュレートできます。
- SSAインタープリタが
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言語の
panic
とrecover
について: https://go.dev/blog/defer-panic-and-recover - SSA (Static Single Assignment) 形式について: https://en.wikipedia.org/wiki/Static_single-assignment_form
- Goコンパイラの内部構造に関する情報 (一般的な情報源):
- Goの公式ドキュメントやブログ
- Goのソースコードリポジトリ (
src/cmd/compile/internal/ssa
など)
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語のソースコード (特に
src/pkg/exp/ssa
ディレクトリ) - コンパイラ設計に関する一般的な知識 (SSA形式、基本ブロック、制御フローグラフなど)
- GitHubのコミット履歴と差分表示
- https://golang.org/cl/7403043 (元のGo Gerritの変更リスト)