TL;DR
2020/02/25 にリリースされた Go 1.14 では defer のインライン展開というランタイム高速化の改善が盛り込まれましたが、これは関数内の defer が 8 個までの場合に限り有効です。 8 個より多くの defer やループ内の defer を含む関数では従来と同じコードが生成され、パフォーマンスの改善はありません。
解説
2020/02/25 に開催された Go 1.14 Release Party in Japan の YouTube Live を視聴していて、 @tenntenn さんの defer トークで次のようなスライドを目にしました。
そこで思わず次のような質問をしてしまったのですが、
後で調べてみたところ Proposal: Low-cost defers through inline code, and extra funcdata to manage the panic case という文書の Implementation のところにずばり答えが書いてありました。
- We need to restrict the number of defers in a function to the size of the deferBits bitmask. To minimize code size, we currently make deferBits to be 8 bits, and don’t do open-coded defers if there are more than 8 defers in a function. If there are more than 8 defers in a function, we revert to the standard defer chain implementation.
要するに、ひとつの関数の中でインラインに展開される defer の数は 8 個までということです。その数を越える defer がある関数は、従来と同じコードが生成されます。
実際に次のようなプログラムでコードを生成して試してみました。
package main
import (
"fmt"
"os"
"strconv"
)
func main() {
n := 0
if len(os.Args) > 1 {
n, _ = strconv.Atoi(os.Args[1])
}
fmt.Print("package main\nimport \"os\"\nfunc main() {\n")
for i := 0; i < n; i++ {
fmt.Printf("if len(os.Args) == %d { defer func() {}() }\n", i)
}
fmt.Print("}\n")
}
このプログラムは、コマンドライン引数に渡した数字だけの条件分岐と defer を含む main() を生成します。
$ go run gen.go 4 | gofmt
package main
import "os"
func main() {
if len(os.Args) == 0 {
defer func() {}()
}
if len(os.Args) == 1 {
defer func() {}()
}
if len(os.Args) == 2 {
defer func() {}()
}
if len(os.Args) == 3 {
defer func() {}()
}
}
defer が 8 個のとき、 SSA では Or8
を含む条件分岐のブロックが 8 セット生成されます。
go run gen.go 8 | go tool compile -d ssa/build/dump=main /dev/stdin
b2: <- b3 b4
v24 = Phi <mem> v22 v127
v224 = Phi <uint8> v21 v4
v25 = Load <[]string> v23 v24
v26 = SliceLen <int> v25
v28 = Eq64 <bool> v26 v27
If v28 -> b6 b7
b6: <- b2
v33 = Copy <mem> v24
v34 = Store <mem> {func()} v32 v29 v33
v36 = Copy <uint8> v224
v37 = Or8 <uint8> v36 v35
v38 = Store <mem> {uint8} v5 v37 v34
Plain -> b5
b7: <- b2
Plain -> b5
defer が 9 個のとき、SSA では次のような runtime.deferprocStack
の呼び出しが 9 セット生成されます。この呼び出しのエラーチェックとリターンも行っているため、だいぶ長くなっています。
go run gen.go 9 | go tool compile -d ssa/build/dump=main /dev/stdin
b2: <- b5 b4
v24 = Phi <mem> v20 v1
v25 = Load <[]string> v23 v24
v26 = SliceLen <int> v25
v28 = Eq64 <bool> v26 v27
If v28 -> b8 b9
b8: <- b2
v30 = Copy <mem> v24
v31 = VarDef <mem> {.autotmp_11} v30
v32 = LocalAddr <*struct { siz uint32; started bool; heap bool; openDefer bool; sp uintptr; pc uintptr; fn uintptr; _panic uintptr; link uintptr; framepc uintptr; varp uintptr; fd uintptr; args [0]uint8 }> {.autotmp_11} v2 v31
v33 = OffPtr <*uint32> [0] v32
v34 = Store <mem> {uint32} v33 v14 v31
v35 = OffPtr <**func()> [24] v32
v36 = Store <mem> {*func()} v35 v29 v34
v37 = Store <mem> {uintptr} v18 v32 v36
v38 = StaticCall <mem> {runtime.deferprocStack} [8] v37
Defer v38 -> b10 b11 (likely)
b9: <- b2
Plain -> b7
b10: <- b8
Plain -> b7
b11: <- b8
v39 = Copy <mem> v38
v40 = StaticCall <mem> {runtime.deferreturn} v39
Ret v40
ということで、 defer が 8 個と 9 個とで異なる SSA が生成されていました。また擬似コードにおける deferBits
には uint8
が使われていることがわかりました。
感想
今回のイベントのトークはどれも興味深く、またためになる内容でよかったです。 Go の SSA というものを初めて読みましたが、全くわからんものではないということがわかったのが大きな収穫でした。 これなら自分もなにか作れそうな気がしてきました。
新型コロナウイルスの影響でイベント開催中止の決定が相次いでいるのは残念ですが、多くのコミュニティでオンライン開催に切り替えられ、急造ながらも質問や感想をその場で伝えられる双方向なイベントが実現できているのは大変素晴らしいことだと思います。イベントを主催される方々の熱意と努力に敬意を表します。また、今後の技術の洗練やノウハウの蓄積によるオンラインイベントの進化にも期待したいと思います。