[インデックス 15463] ファイルの概要
このコミットは、Go言語の実験的なSSA (Static Single Assignment) パッケージにおいて、defer
ステートメントと名前付き戻り値のセマンティクスを正確に実装するための重要な変更を導入しています。具体的には、遅延実行されるプロシージャ呼び出し(defer
)の実行を明示的に制御する新しいSSA命令 RunDefers
を導入し、それによって名前付き戻り値の評価、defer
の実行、そして最終的な戻り値の形成という、Go言語の仕様に合致した厳密な順序を保証します。
コミット
commit 5a09f1b3be354ebb69a9124076236cb7aa83edc9
Author: Alan Donovan <adonovan@google.com>
Date: Wed Feb 27 10:35:23 2013 -0500
exp/ssa: make invokation of deferred procedure calls explicit.
The correct semantics of named result parameters and deferred
procedures cannot be implemented with the existing Ret
instruction alone, since the required sequence is:
(1) evaluate return operands and parallel-assign them to
named result parameters
(2) invoke deferred procedures
(3) load named result parameters to form result tuple.
We introduce a new 'rundefers' instruction that explicitly
invokes the deferred procedure calls, and we generate code
that follows the sequence above.
Most functions do not use deferred procedures but this cannot
be known in a single pass. So, we add an optimisation to
eliminate redundant 'rundefers'; it is piggybacked on the
existing pass done for "lifting".
Added tests.
R=gri
CC=golang-dev
https://golang.org/cl/7411043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/5a09f1b3be354ebb69a9124076236cb7aa83edc9
元コミット内容
このコミットは、Go言語の実験的なSSA (Static Single Assignment) パッケージにおいて、遅延実行されるプロシージャ呼び出し(defer
)のセマンティクスを明示的に扱うための変更です。既存のRet
(戻り値)命令だけでは、名前付き戻り値とdefer
の正しいセマンティクスを実装できないという問題がありました。Go言語の仕様では、関数が戻る際に以下の厳密な順序が求められます。
- 戻り値のオペランドを評価し、それらを名前付き戻り値パラメータに並行代入する。
- 遅延実行されるプロシージャ(
defer
関数)を呼び出す。 - 名前付き戻り値パラメータをロードして、最終的な戻り値のタプルを形成する。
この問題を解決するため、新しいSSA命令であるRunDefers
が導入されました。この命令は、遅延実行されるプロシージャ呼び出しを明示的に実行します。これにより、上記の正しい順序でコードが生成されるようになります。
また、ほとんどの関数はdefer
を使用しないため、不要なRunDefers
命令を削除する最適化も追加されました。この最適化は、既存の"lifting"パスに便乗して実装されています。変更の正当性を確認するために、関連するテストも追加されています。
変更の背景
Go言語のdefer
ステートメントは、関数がリターンする直前、またはパニックが発生した際に実行される関数を登録する強力な機能です。また、Go言語は名前付き戻り値(Named Result Parameters, NRPs)をサポートしており、これにより戻り値に変数を割り当て、関数本体内でその変数を操作し、return
ステートメントで明示的に値を指定せずに戻すことができます。
これらの機能は非常に便利ですが、その実行順序には厳密なセマンティクスがあります。特に、名前付き戻り値が使用され、かつdefer
関数が戻り値を変更する可能性がある場合、その順序は非常に重要になります。Go言語の仕様では、関数が戻る際には、まず戻り値が名前付き戻り値変数に代入され、その後にdefer
関数が実行され、最後に名前付き戻り値変数の最終的な値が関数の戻り値として使用される、という順序が定められています。
従来のSSA中間表現では、Ret
命令が単一の操作として戻り値を処理していました。しかし、この単一のRet
命令では、名前付き戻り値への代入、defer
の実行、そして名前付き戻り値からの最終的な値のロードという3つの異なるステップを、正しい順序で表現することが困難でした。このため、defer
が名前付き戻り値を変更するような複雑なケースで、コンパイラが生成するコードがGo言語のセマンティクスに違反する可能性がありました。
このコミットは、このセマンティクス上のギャップを埋め、GoコンパイラのSSAバックエンドがdefer
と名前付き戻り値の相互作用を正確にモデル化できるようにすることを目的としています。
前提知識の解説
このコミットを理解するためには、以下のGo言語の概念とコンパイラのSSAに関する基本的な知識が必要です。
1. Go言語の defer
ステートメント
defer
ステートメントは、そのステートメントを含む関数がリターンする直前、またはパニックが発生した際に、指定された関数呼び出しを遅延実行するために使用されます。defer
された関数はLIFO(後入れ先出し)順で実行されます。
func example() (result int) {
result = 0
defer func() {
result++ // defer関数内で名前付き戻り値を変更できる
}()
return 10 // ここで result は 10 になるが、defer実行後に 11 になる
}
重要なのは、defer
関数が実行される時点では、すでに戻り値が名前付き戻り値変数に代入されているため、defer
関数内でこれらの変数を読み書きできる点です。
2. Go言語の名前付き戻り値 (Named Result Parameters)
Go言語では、関数の戻り値に変数を命名することができます。これにより、関数本体内でこれらの変数に値を代入し、return
ステートメントで明示的に値を指定せずに戻すことができます。
func calculate(x, y int) (sum int, diff int) {
sum = x + y
diff = x - y
return // sum と diff の現在の値が戻り値となる
}
return
ステートメントが実行される際、まず名前付き戻り値変数に値が代入され、その後defer
関数が実行され、最後に名前付き戻り値変数の最終的な値が関数の戻り値として使用されます。
3. SSA (Static Single Assignment) 形式
SSAは、コンパイラの中間表現(IR)の一種です。SSA形式では、各変数が一度だけ代入されるという特性を持ちます。これにより、データフロー解析や最適化が容易になります。
- 命令 (Instruction): SSA形式における基本的な操作単位。例えば、
Add
,Load
,Store
,Call
,Ret
などがあります。 - 値 (Value): 命令の結果として生成されるデータ。SSAでは、各値は一度だけ定義されます。
- 基本ブロック (Basic Block): 制御フローグラフのノード。単一の入り口と単一の出口を持ち、内部に分岐を含まない命令のシーケンスです。
Phi
関数: 複数の制御フローパスが合流する点で、異なるパスから来る同じ論理的な変数に異なるSSA値が割り当てられている場合に、それらを結合するために使用されます。
このコミットでは、新しいSSA命令RunDefers
が導入され、既存のRet
命令のセマンティクスが変更されています。これは、SSA形式における命令の粒度とセマンティクスを調整することで、Go言語の複雑な実行モデルを正確に表現しようとするものです。
4. init
関数と初期化ブロック
Go言語のinit
関数は、パッケージが初期化される際に自動的に実行されます。複数のinit
関数がある場合、それらは定義された順序で実行されます。コンパイラは、これらのinit
関数やトップレベルの変数初期化を単一の大きな初期化関数に結合することがあります。この結合された関数内でもdefer
が正しく動作する必要があります。
技術的詳細
このコミットの核心は、Go言語のdefer
と名前付き戻り値の正確なセマンティクスをSSA中間表現で表現するために、新しいSSA命令RunDefers
を導入し、既存のRet
命令の役割を再定義した点にあります。
RunDefers
命令の導入
- 目的:
RunDefers
命令は、現在の関数スコープで登録されているすべての遅延実行関数(defer
された関数)を、LIFO順に実行することを明示的に指示します。 - 配置:
RunDefers
命令は、関数の戻りパス(return
ステートメントの直前や、関数の終端に到達した場合)に挿入されます。これにより、名前付き戻り値への代入が完了した後、かつ最終的な戻り値が形成される前にdefer
が実行されることが保証されます。 - 多重挿入の許容:
RunDefers
命令は、単一の制御フローパス中に複数回出現することが許容されます。これは、特に複数のinit
ブロックが結合された関数のようなケースで有用です。各init
ブロックの終端でdefer
が実行されるべきですが、これらが単一の関数に結合されると、その結合された関数の終端で一度だけRunDefers
が実行されるだけでは不十分になるためです。
Ret
命令のセマンティクス変更
- 従来の
Ret
命令は、戻り値の評価、名前付き戻り値への代入、defer
の実行、最終的な戻り値の形成といった一連の操作を暗黙的に含んでいるか、あるいはその順序が不明確でした。 - この変更により、
Ret
命令は純粋に「関数から戻る」という制御フローの役割に特化されます。戻り値の評価と名前付き戻り値への代入はRet
命令の前に明示的に行われ、defer
の実行は新しいRunDefers
命令によって明示的に制御されます。
名前付き戻り値の処理フローの変更
コミットメッセージに記載されているように、名前付き戻り値とdefer
の正しいセマンティクスを実現するために、以下の3ステップがSSAコード生成時に厳密に適用されます。
- 戻り値の評価と名前付き戻り値への並行代入:
return
ステートメントで指定された値(または名前付き戻り値が暗黙的に持つ値)が評価され、関数の名前付き戻り値パラメータ(f.namedResults
)に並行して代入されます。これはemitStore
命令などによって行われます。 RunDefers
命令の挿入: このステップの直後にRunDefers
命令が挿入され、登録されたdefer
関数が実行されます。- 名前付き戻り値のロードと結果タプルの形成:
RunDefers
の実行後、名前付き戻り値パラメータの最終的な値がロードされ(emitLoad
)、それらが関数の最終的な戻り値タプルとしてRet
命令に渡されます。
この明確な分離と順序付けにより、defer
関数が名前付き戻り値を変更するようなケースでも、Go言語の仕様に完全に準拠したコードが生成されるようになります。
最適化: 不要な RunDefers
の除去
- ほとんどの関数は
defer
を使用しないため、そのような関数にRunDefers
命令を無条件に挿入すると、不要なオーバーヘッドが生じます。 - このコミットでは、既存の"lifting"パス(SSA最適化の一種で、変数の割り当てを最適化する)に便乗して、不要な
RunDefers
命令を削除する最適化が追加されました。 - "lifting"パス中に、関数内に
Defer
命令が存在するかどうかを追跡します。もしDefer
命令が一つも存在しない関数であれば、その関数内のすべてのRunDefers
命令は冗長であると判断され、削除されます。これにより、defer
を使用しない関数のパフォーマンスが不必要に低下するのを防ぎます。
影響を受けるファイルと変更点
src/pkg/exp/ssa/builder.go
:buildReturn
関数内で、RunDefers
命令の挿入ロジックが追加されました。名前付き戻り値の処理が、defer
実行の前後に分割されています。- 関数の終端に到達した場合の暗黙的な
return
や、init
ブロックの終端にもRunDefers
が挿入されるようになりました。
src/pkg/exp/ssa/ssa.go
:- 新しいSSA命令
RunDefers
の構造体が定義されました。 Function
構造体のresults
フィールドがnamedResults
にリネームされ、名前付き戻り値の管理がより明確になりました。
- 新しいSSA命令
src/pkg/exp/ssa/interp/interp.go
:- SSAインタープリタに
RunDefers
命令を処理するためのロジックが追加されました。rundefers
というヘルパー関数が導入され、fr.defers
スタックを逆順に実行するようになりました。 callSSA
関数内のdefer
実行ロジックがfr.rundefers()
の呼び出しに置き換えられました。
- SSAインタープリタに
src/pkg/exp/ssa/lift.go
:lift
関数内で、Defer
命令の存在を追跡するusesDefer
フラグが追加されました。RunDefers
命令の数をカウントするb.rundefers
フィールドが追加されました。usesDefer
がfalse
の場合(つまり、関数内にDefer
命令がない場合)、RunDefers
命令を削除するロジックが追加されました。
src/pkg/exp/ssa/func.go
:f.results
がf.namedResults
にリネームされ、関連する初期化とクリアのロジックが更新されました。
src/pkg/exp/ssa/print.go
:RunDefers
命令の文字列表現("rundefers"
)が追加されました。
src/pkg/exp/ssa/sanity.go
:- SSA命令の健全性チェックに
RunDefers
が追加されました。
- SSA命令の健全性チェックに
src/pkg/exp/ssa/interp/testdata/coverage.go
:defer
が名前付き戻り値を変更するケースや、init
関数内でのdefer
の実行順序に関する新しいテストケースが追加されました。これにより、変更がGo言語のセマンティクスに正しく準拠していることが検証されます。
コアとなるコードの変更箇所
このコミットの主要な変更は、src/pkg/exp/ssa/builder.go
におけるRunDefers
命令の挿入ロジックと、src/pkg/exp/ssa/ssa.go
におけるRunDefers
命令の定義、そしてsrc/pkg/exp/ssa/lift.go
における最適化ロジックです。
src/pkg/exp/ssa/builder.go
(抜粋)
--- a/src/pkg/exp/ssa/builder.go
+++ b/src/pkg/exp/ssa/builder.go
@@ -2276,40 +2276,45 @@ start:
block = t._break
}
}
+ // Run function calls deferred in this init
+ // block when explicitly returning from it.
+ fn.emit(new(RunDefers))
emitJump(fn, block)
fn.currentBlock = fn.newBasicBlock("unreachable")
return
}
- var results []Value
- // Per the spec, there are three distinct cases of return.
- switch {
- case len(s.Results) == 0:
- // Return with no arguments.
- // Prior assigns to named result params are
- // reloaded into results tuple.
- // A void function is a degenerate case of this.
- for _, r := range fn.results {
- results = append(results, emitLoad(fn, r))
- }
- case len(s.Results) == 1 && len(fn.Signature.Results) > 1:
+ var results []Value
+ if len(s.Results) == 1 && len(fn.Signature.Results) > 1 {
// Return of one expression in a multi-valued function.
tuple := b.exprN(fn, s.Results[0])
for i, v := range tuple.Type().(*types.Result).Values {
results = append(results, emitExtract(fn, tuple, i, v.Type))
}
-
- default:
- // Return one or more single-valued expressions.
- // These become the scalar or tuple result.
- for _, r := range s.Results {
- results = append(results, b.expr(fn, r))
+ } else {
+ // 1:1 return, or no-arg return in non-void function.
+ for i, r := range s.Results {
+ v := emitConv(fn, b.expr(fn, r), fn.Signature.Results[i].Type)
+ results = append(results, v)
}
}
- // Perform implicit conversions.
- for i := range results {
- results[i] = emitConv(fn, results[i], fn.Signature.Results[i].Type)
+ if fn.namedResults != nil {
+ // Function has named result parameters (NRPs).
+ // Perform parallel assignment of return operands to NRPs.
+ for i, r := range results {
+ emitStore(fn, fn.namedResults[i], r)
+ }
+ }
+ // Run function calls deferred in this
+ // function when explicitly returning from it.
+ fn.emit(new(RunDefers))
+ if fn.namedResults != nil {
+ // Reload NRPs to form the result tuple.
+ results = results[:0]
+ for _, r := range fn.namedResults {
+ results = append(results, emitLoad(fn, r))
+ }
}
fn.emit(&Ret{Results: results})
fn.currentBlock = fn.newBasicBlock("unreachable")
@@ -2410,7 +2415,9 @@ func (b *Builder) buildFunction(fn *Function) {
fn.start(b.idents)
b.stmt(fn, fn.syntax.body)
if cb := fn.currentBlock; cb != nil && (cb == fn.Blocks[0] || cb.Preds != nil) {
-\t\t// We fell off the end: an implicit no-arg return statement.
+\t\t// Run function calls deferred in this function when
+\t\t// falling off the end of the body block.
+\t\tfn.emit(new(RunDefers))
\t\tfn.emit(new(Ret))\n \t}\n \tfn.finish()\n@@ -2686,6 +2693,9 @@ func (b *Builder) buildDecl(pkg *Package, decl ast.Decl) {\n \t\t\t\t_break: next,\n \t\t\t}\n \t\t\tb.stmt(init, decl.Body)\n+\t\t\t// Run function calls deferred in this init\n+\t\t\t// block when falling off the end of the block.\n+\t\t\tinit.emit(new(RunDefers))\n \t\t\temitJump(init, next)\n \t\t\tinit.targets = init.targets.tail\n \t\t\tinit.currentBlock = next\n@@ -2792,6 +2802,7 @@ func (b *Builder) BuildPackage(p *Package) {\n \t// Finish up.\n \temitJump(init, done)\n \tinit.currentBlock = done\n+\tinit.emit(new(RunDefers))\n \tinit.emit(new(Ret))\n \tinit.finish()\n }\n```
### `src/pkg/exp/ssa/ssa.go` (抜粋)
```diff
--- a/src/pkg/exp/ssa/ssa.go
+++ b/src/pkg/exp/ssa/ssa.go
@@ -240,7 +240,7 @@ type Function struct {
// then cleared.
currentBlock *BasicBlock // where to emit code
objects map[types.Object]Value // addresses of local variables
- results []*Alloc // tuple of named results
+ namedResults []*Alloc // tuple of named results
syntax *funcSyntax // abstract syntax trees for Go source functions
targets *targets // linked stack of branch targets
lblocks map[*ast.Object]*lblock // labelled blocks
@@ -270,6 +270,7 @@ type BasicBlock struct {
succs2 [2]*BasicBlock // initial space for Succs.
dom *domNode // node in dominator tree; optional.
gaps int // number of nil Instrs (transient).
+ rundefers int // number of rundefers (transient)
}
// Pure values ----------------------------------------
@@ -817,9 +818,7 @@ type If struct {
// Ret returns values and control back to the calling function.
//
// len(Results) is always equal to the number of results in the
-// function's signature. A source-level 'return' statement with no
-// operands in a multiple-return value function is desugared to make
-// the results explicit.
+// function's signature.
//
// If len(Results) > 1, Ret returns a tuple value with the specified
// components which the caller must access using Extract instructions.
@@ -827,9 +826,6 @@ type If struct {
// There is no instruction to return a ready-made tuple like those
// returned by a "value,ok"-mode TypeAssert, Lookup or UnOp(ARROW) or
// a tail-call to a function with multiple result parameters.
-// TODO(adonovan): consider defining one; but: dis- and re-assembling
-// the tuple is unavoidable if assignability conversions are required
-// on the components.
//
// Ret must be the last instruction of its containing BasicBlock.
// Such a block has no successors.
@@ -843,6 +839,20 @@ type Ret struct {
Results []Value
}
+// RunDefers pops and invokes the entire stack of procedure calls
+// pushed by Defer instructions in this function.
+//
+// It is legal to encounter multiple 'rundefers' instructions in a
+// single control-flow path through a function; this is useful in
+// the combined init() function, for example.
+//
+// Example printed form:
+// rundefers
+//
+type RunDefers struct {
+ anInstruction
+}
+
// Panic initiates a panic with value X.
//
// A Panic instruction must be the last instruction of its containing
@@ -875,8 +885,7 @@ type Go struct {
}
// Defer pushes the specified call onto a stack of functions
-// to be called immediately prior to returning from the
-// current function.
+// to be called by a RunDefers instruction or by a panic.
//
// See CallCommon for generic function call documentation.
//
@@ -1146,6 +1155,7 @@ func (*Panic) ImplementsInstruction() {}\n func (*Phi) ImplementsInstruction() {}\n func (*Range) ImplementsInstruction() {}\n func (*Ret) ImplementsInstruction() {}\n+func (*RunDefers) ImplementsInstruction() {}\n func (*Select) ImplementsInstruction() {}\n func (*Send) ImplementsInstruction() {}\n func (*Slice) ImplementsInstruction() {}\n@@ -1267,6 +1277,10 @@ func (s *Ret) Operands(rands []*Value) []*Value {\n \treturn rands\n }\n \n+func (*RunDefers) Operands(rands []*Value) []*Value {\n+\treturn rands\n+}\n+\n func (v *Select) Operands(rands []*Value) []*Value {\n \tfor i := range v.States {\n \t\trands = append(rands, &v.States[i].Chan, &v.States[i].Send)\n```
### `src/pkg/exp/ssa/lift.go` (抜粋)
```diff
--- a/src/pkg/exp/ssa/lift.go
+++ b/src/pkg/exp/ssa/lift.go
@@ -154,22 +154,33 @@ func lift(fn *Function) {
// concatenation of all non-dead newPhis and non-nil Instrs
// for the block, reusing the original array if space permits.
+\t// While we're here, we also eliminate 'rundefers'
+\t// instructions in functions that contain no 'defer'
+\t// instructions.
+\tusesDefer := false
+\n // Determine which allocs we can lift and number them densely.\n // The renaming phase uses this numbering for compact maps.\n numAllocs := 0\n for _, b := range fn.Blocks {\n \t\tb.gaps = 0
+\t\tb.rundefers = 0
\t\tfor i, instr := range b.Instrs {\n-\t\t\tif alloc, ok := instr.(*Alloc); ok {\n-\t\t\t\tif liftAlloc(df, alloc, newPhis) {\n-\t\t\t\t\talloc.index = numAllocs
+\t\t\tswitch instr := instr.(type) {\n+\t\t\tcase *Alloc:\n+\t\t\t\tif liftAlloc(df, instr, newPhis) {\n+\t\t\t\t\tinstr.index = numAllocs
\t\t\t\t\tnumAllocs++\n \t\t\t\t\t// Delete the alloc.\n \t\t\t\t\tb.Instrs[i] = nil
\t\t\t\t\tb.gaps++
\t\t\t\t} else {\n-\t\t\t\t\talloc.index = -1
+\t\t\t\t\tinstr.index = -1
\t\t\t\t}\n+\t\t\tcase *Defer:\n+\t\t\t\tusesDefer = true\n+\t\t\tcase *RunDefers:\n+\t\t\t\tb.rundefers++
\t\t\t}\n \t\t}\n \t}\n@@ -202,22 +213,33 @@ func lift(fn *Function) {\n \t\t}\n \t\tnps = nps[:j]\n
-\t\tif j+b.gaps == 0 {\n-\t\t\tcontinue // fast path: no new phis and no gaps\n+\t\trundefersToKill := b.rundefers\n+\t\tif usesDefer {\n+\t\t\trundefersToKill = 0\n+\t\t}\n+\n+\t\tif j+b.gaps+rundefersToKill == 0 {\n+\t\t\tcontinue // fast path: no new phis or gaps\n \t\t}\n \n \t\t// Compact nps + non-nil Instrs into a new slice.\n \t\t// TODO(adonovan): opt: compact in situ if there is\n \t\t// sufficient space or slack in the slice.\n-\t\tdst := make([]Instruction, j+len(b.Instrs)-b.gaps)\n+\t\tdst := make([]Instruction, len(b.Instrs)+j-b.gaps-rundefersToKill)\n \t\tfor i, np := range nps {\n \t\t\tdst[i] = np.phi\n \t\t}\n \t\tfor _, instr := range b.Instrs {\n-\t\t\tif instr != nil {\n-\t\t\t\tdst[j] = instr\n-\t\t\t\tj++\n+\t\t\tif instr == nil {\n+\t\t\t\tcontinue\n \t\t\t}\n+\t\t\tif !usesDefer {\n+\t\t\t\tif _, ok := instr.(*RunDefers); ok {\n+\t\t\t\t\tcontinue\n+\t\t\t\t}\n+\t\t\t}\n+\t\t\tdst[j] = instr\n+\t\t\tj++\n \t\t}\n \t\tfor i, np := range nps {\n \t\t\tdst[i] = np.phi\n```
## コアとなるコードの解説
### `src/pkg/exp/ssa/builder.go` の変更
このファイルは、GoのAST(抽象構文木)からSSA中間表現を構築する役割を担っています。変更の核心は、`return`ステートメントや関数の暗黙的な終了点、`init`ブロックの終了点に`RunDefers`命令を挿入するロジックです。
* **`buildReturn` 関数内**:
* 以前は、戻り値の処理が単一の`switch`文で行われていましたが、名前付き戻り値の処理と`defer`の実行が明確に分離されました。
* `if fn.namedResults != nil`ブロックが追加され、名前付き戻り値が存在する場合の処理が定義されています。
1. まず、`return`ステートメントで指定された値が評価され、`results`スライスに格納されます。
2. 次に、`for i, r := range results { emitStore(fn, fn.namedResults[i], r) }`によって、これらの値が名前付き戻り値パラメータ(`fn.namedResults`)に並行代入されます。
3. **`fn.emit(new(RunDefers))`**: ここで新しい`RunDefers`命令が挿入されます。これにより、名前付き戻り値への代入が完了した直後に`defer`関数が実行されることが保証されます。
4. `if fn.namedResults != nil`ブロックの後半で、`results = results[:0]; for _, r := range fn.namedResults { results = append(results, emitLoad(fn, r)) }`というコードが追加されています。これは、`defer`関数が名前付き戻り値を変更する可能性があるため、`RunDefers`実行後に名前付き戻り値パラメータから最終的な値を再ロードして、`Ret`命令に渡すためのものです。
* **関数の暗黙的な終了点**: `buildFunction`関数内で、関数の本体ブロックの終端に到達した場合(明示的な`return`がない場合)にも、`fn.emit(new(RunDefers))`が挿入されるようになりました。これにより、暗黙的な`return`でも`defer`が正しく実行されます。
* **`init` ブロックの終了点**: `buildDecl`関数や`BuildPackage`関数内で、`init`ブロックの終端にも`init.emit(new(RunDefers))`が挿入されています。これは、`init`関数内でも`defer`が使用される可能性があり、その場合も正しく実行される必要があるためです。
### `src/pkg/exp/ssa/ssa.go` の変更
このファイルはSSA中間表現の基本的なデータ構造と命令を定義しています。
* **`Function` 構造体**:
* `results []*Alloc`フィールドが`namedResults []*Alloc`にリネームされました。これは、このフィールドが名前付き戻り値パラメータの割り当て(`Alloc`)を保持することをより明確に示しています。
* **`BasicBlock` 構造体**:
* `rundefers int`フィールドが追加されました。これは、この基本ブロック内に存在する`RunDefers`命令の数を一時的に追跡するために使用されます。最適化パス(`lift.go`)で利用されます。
* **`RunDefers` 構造体**:
* 新しいSSA命令`RunDefers`が定義されました。これは`anInstruction`を埋め込んでおり、SSA命令としての基本的な振る舞いを継承します。この構造体自体はフィールドを持ちません。これは、`RunDefers`が単に「`defer`スタックを実行する」というアクションを表すためです。
* 関連するメソッド(`ImplementsInstruction()`, `Operands()`)も追加されています。
### `src/pkg/exp/ssa/lift.go` の変更
このファイルは、SSA中間表現に対する最適化パス("lifting"パス)を実装しています。このコミットでは、このパスに`RunDefers`命令の最適化ロジックが追加されました。
* **`usesDefer` フラグ**:
* `lift`関数の冒頭で`usesDefer := false`が宣言されます。
* 基本ブロック内の命令を走査するループ内で、`instr := instr.(type)`の`switch`文に`case *Defer:`が追加され、`Defer`命令が見つかった場合に`usesDefer`が`true`に設定されます。これにより、関数全体で`Defer`命令が使用されているかどうかが追跡されます。
* **`b.rundefers` カウント**:
* `case *RunDefers:`が追加され、`RunDefers`命令が見つかるたびに`b.rundefers++`でカウントされます。
* **`RunDefers` 命令の削除ロジック**:
* 基本ブロックの命令を新しいスライス`dst`にコンパクト化する際に、`if !usesDefer { if _, ok := instr.(*RunDefers); ok { continue } }`という条件が追加されました。
* このロジックは、「もし関数全体で`Defer`命令が一つも使われていない(`usesDefer`が`false`)ならば、この`RunDefers`命令はスキップ(削除)する」というものです。これにより、`defer`を使用しない関数から不要な`RunDefers`命令が取り除かれ、生成されるコードの効率が向上します。
これらの変更により、Go言語の`defer`と名前付き戻り値の複雑な相互作用がSSA中間表現で正確にモデル化され、コンパイラがGo言語の仕様に完全に準拠したコードを生成できるようになりました。
## 関連リンク
* Go言語の`defer`ステートメントに関する公式ドキュメント: [https://go.dev/blog/defer-panic-and-recover](https://go.dev/blog/defer-panic-and-recover)
* Go言語の関数と戻り値に関する公式ドキュメント: [https://go.dev/ref/spec#Function_declarations](https://go.dev/ref/spec#Function_declarations)
* このコミットのGo Gerritレビューページ: [https://golang.org/cl/7411043](https://golang.org/cl/7411043)
## 参考にした情報源リンク
* Go言語の公式ドキュメント
* Go言語のソースコード(特に`src/cmd/compile/internal/ssa`パッケージ)
* SSA形式に関する一般的な情報源(コンパイラ最適化の教科書など)