defer
defer は Go の日常開発で非常に頻繁に出現するキーワードで、FIFO 方式で defer に関連付けられた関数を実行します。多くの場合、このメカニズムを利用してリソースの解放操作を行います。例えばファイルの閉じる操作などです。
fd, err := os.Open("/dev/stdin")
if err != nil{
return err
}
defer fd.Close()
...これほど頻繁に出現するキーワードであるため、その背後にある構造を理解する必要があるでしょう。
構造
defer キーワードは runtime._defer 構造体に対応し、その構造は複雑ではありません。
type _defer struct {
started bool
heap bool
openDefer bool
sp uintptr // sp at time of defer
pc uintptr // pc at time of defer
fn func() // can be nil for open-coded defers
_panic *_panic // panic that is running defer
link *_defer // next defer on G; can point to either heap or stack!
fd unsafe.Pointer // funcdata for the function associated with the frame
varp uintptr // value of varp for the stack frame
framepc uintptr
}fn フィールドは defer キーワードに対応する関数で、link は次のリンクされた defer を表し、sp と pc は呼び出し元の関数情報を記録し、defer がどの関数に属するかを判断するために使用されます。defer は実行時にリンクドリスト形式で存在し、リストのヘッダーはゴルーチン G 上にあるため、defer は実際にはゴルーチンと直接関連付けられています。
type g struct {
...
_panic *_panic // innermost panic - offset known to liblink
_defer *_defer // innermost defer
...
}関数を実行する際、関数内の defer をチェーンのヘッダーから順に追加していきます。
defer fn1()
defer fn2()
defer fn3()上記のコードは以下の図に対応します。

ゴルーチン以外にも、P も defer とある程度の関連があります。P の構造体には deferpool フィールドがあります。
type p struct {
...
deferpool []*_defer // pool of available defer structs (see panic.go)
deferpoolbuf [32]*_defer
...
}deferpool には事前に割り当てられた defer 構造が格納されており、P に関連付けられたゴルーチン G に新しい defer 構造を割り当てるために使用され、オーバーヘッドを削減できます。
割り当て
構文上で defer キーワードを使用すると、コンパイラはこれを runtime.deferproc 関数の呼び出しに変換します。例えば Go コードは以下のようになります。
defer fn1(x, y)コンパイル後の実際のコードは以下のようになります。
deferproc(func(){
fn1(x, y)
})したがって、実際には defer が渡す関数にはパラメータも戻り値もありません。deferproc 関数のコードは以下の通りです。
func deferproc(fn func()) {
gp := getg()
d := newdefer()
d.link = gp._defer
gp._defer = d
d.fn = fn
d.pc = getcallerpc()
d.sp = getcallersp()
return0()
}この関数は defer 構造を作成し、ゴルーチン G チェーンのヘッダーに追加する役割を果たします。runtime.newdefer 関数は P 内の deferpool から事前に割り当てられた defer 構造を取得しようとします。
if len(pp.deferpool) == 0 && sched.deferpool != nil {
lock(&sched.deferlock)
for len(pp.deferpool) < cap(pp.deferpool)/2 && sched.deferpool != nil {
d := sched.deferpool
sched.deferpool = d.link
d.link = nil
pp.deferpool = append(pp.deferpool, d)
}
unlock(&sched.deferlock)
}まずグローバルな sched.deferpool からローカルの deferpool に半分まで defer 構造を補充し、その後 P 内の deferpool から取得しようとします。
if n := len(pp.deferpool); n > 0 {
d = pp.deferpool[n-1]
pp.deferpool[n-1] = nil
pp.deferpool = pp.deferpool[:n-1]
}
if d == nil {
// Allocate new defer.
d = new(_defer)
}
d.heap = true最後に見つからない場合のみ、手動割り当て方式を使用します。最後に以下のコードがあります。
d.heap = trueこれは defer がヒープ上で割り当てられることを表し、対応して false の場合はスタック上で割り当てられます。スタック上で割り当てられたメモリは返却時に自動的に回収され、メモリ管理効率はヒープ上よりも高くなります。スタック上で割り当てるかどうかを決定する要因はループ層数です。この部分のロジックは cmd/compile/ssagen 内の escape.goDeferStmt メソッドの以下の小部分に遡れます。
func (e *escape) goDeferStmt(n *ir.GoDeferStmt) {
...
if n.Op() == ir.ODEFER && e.loopDepth == 1 {
k = e.later(e.discardHole())
n.SetEsc(ir.EscNever)
}
...
}e.loopDepth は現在の文のループ層数を表し、現在の defer 文がループ内にない場合、スタック上に割り当てられます。
case ir.ODEFER:
n := n.(*ir.GoDeferStmt)
if s.hasOpenDefers {
s.openDeferRecord(n.Call.(*ir.CallExpr))
} else {
d := callDefer
if n.Esc() == ir.EscNever {
d = callDeferStack
}
s.callResult(n.Call.(*ir.CallExpr), d)
}スタック上で割り当てる場合、スタック上で直接 defer 構造体を作成し、最終的に runtime.deferprocStack 関数によって defer 構造の作成を完了します。
if k == callDeferStack {
// Make a defer struct d on the stack.
if stksize != 0 {
s.Fatalf("deferprocStack with non-zero stack size %d: %v", stksize, n)
}
t := deferstruct()
...
// Call runtime.deferprocStack with pointer to _defer record.
ACArgs = append(ACArgs, types.Types[types.TUINTPTR])
aux := ssa.StaticAuxCall(ir.Syms.DeferprocStack, s.f.ABIDefault.ABIAnalyzeTypes(nil, ACArgs, ACResults))
call = s.newValue0A(ssa.OpStaticLECall, aux.LateExpansionResultType(), aux)
call.AddArgs(callArgs...)
call.AuxInt = int64(types.PtrSize) // deferprocStack takes a *_defer argdeferprocStack 関数のシグネチャは以下の通りです。
func deferprocStack(d *_defer)具体的な作成ロジックは deferproc と大きな違いはありません。主な違いは、スタック上で割り当てる場合、defer 構造のソースは直接作成された構造体であり、ヒープ上で割り当てられた defer のソースは new 関数であることです。
実行
関数が返却しようとするとき、または panic が発生したとき、runtime.deferreturn 関数に入ります。この関数はゴルーチンのチェーンから defer を取り出して実行する役割を果たします。
func deferreturn() {
gp := getg()
for {
d := gp._defer
sp := getcallersp()
if d.sp != sp {
return
}
fn := d.fn
d.fn = nil
gp._defer = d.link
freedefer(d)
fn()
}
}まず getcallersp() を通じて現在の関数のスタックフレームを取得し、defer 構造内の sp と比較して、defer が現在の関数に属するかどうかを判断します。その後、defer 構造をチェーンのヘッダーから取り出し、gp._defer = d.link を使用して次の defer を実行し、runtime.freedefer 関数を通じて defer 構造をプールに解放し、最後に fn を呼び出して実行します。現在の関数に属するすべての defer の実行が完了するまで、このようにループを続けます。
オープンコーディング
defer の使用にはコストがかかります。構文上は便利を提供しますが、直接関数呼び出しを行うわけではなく、一連のプロセスを経る必要があるため、パフォーマンスの損失が発生します。そのため、Go 公式はある最適化方式を設計しました——オープンコーディングです。これは defer に対する最適化方式で、元の英名は open-coded です。国内ではほぼ開放編碼と翻訳されていますが、ここでの open は展開を意味し、defer 関数のコードを現在の関数コード内に展開するもので、関数のインライン化に似ています。この最適化方式には以下の制限条件があります。
- 関数内の
defer数が 8 個を超えないこと deferとreturnの両方の数の積が 15 を超えないことdeferがループ内に出現しないこと- コンパイル最適化が無効化されていないこと
- 手動で
os.Exit()を呼び出していないこと - ヒープからパラメータをコピーする必要がないこと
この部分の判断ロジックは cmd/compile/ssagen.buildssa 関数の以下の部分のコードに遡れます。
s.hasOpenDefers = base.Flag.N == 0 && s.hasdefer && !s.curfn.OpenCodedDeferDisallowed()
if s.hasOpenDefers && len(s.curfn.Exit) > 0 {
s.hasOpenDefers = false
}
if s.hasOpenDefers {
for _, f := range s.curfn.Type().Results().FieldSlice() {
if !f.Nname.(*ir.Name).OnStack() {
s.hasOpenDefers = false
break
}
}
}
if s.hasOpenDefers && s.curfn.NumReturns*s.curfn.NumDefers > 15 {
s.hasOpenDefers = false
}その後、Go は現在の関数内に 8 ビット整数変数 deferBits を作成し、bitmap として使用して defer をマークします。各ビットが 1 つの defer をマークし、8 ビット整数 uint8 は最大 8 個を表せます。対応するビットが 1 の場合、対応するオープンコーディング最適化後の defer が関数の返却時に実行されます。
