Go 1.14 で defer が速くなるのは 8 個まで

## TL;DR

[2020/02/25 にリリースされた Go 1.14](https://blog.golang.org/go1.14) では defer のインライン展開というランタイム高速化の改善が盛り込まれましたが、これは関数内の defer が 8 個までの場合に限り有効です。 8 個より多くの defer やループ内の defer を含む関数では従来と同じコードが生成され、パフォーマンスの改善はありません。

## 解説

2020/02/25 に開催された [Go 1.14 Release Party in Japan](https://gocon.connpass.com/event/167301/) の YouTube Live を視聴していて、 @tenntenn さんの defer トークで次のようなスライドを目にしました。

[![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61759/9292a07c-8990-303f-e32d-7cee55174d1c.png)](https://youtu.be/IWyDR08pUU4?t=2024)

そこで思わず次のような質問をしてしまったのですが、

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61759/483d3ca1-feff-27be-48e5-aa4173bbcca9.png)

後で調べてみたところ [Proposal: Low-cost defers through inline code, and extra funcdata to manage the panic case](https://github.com/golang/proposal/blob/master/design/34481-opencoded-defers.md) という文書の Implementation のところにずばり答えが書いてありました。

> 1. 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 がある関数は、従来と同じコードが生成されます。

実際に次のようなプログラムでコードを生成して試してみました。

```go:gen.go
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() を生成します。

```console:コマンドライン
$ 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 セット生成されます。

```console:bash
go run gen.go 8 | go tool compile -d ssa/build/dump=main /dev/stdin
```

```text:main_01__build.dump(抜粋)
  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 セット生成されます。この呼び出しのエラーチェックとリターンも行っているため、だいぶ長くなっています。

```console:bash
go run gen.go 9 | go tool compile -d ssa/build/dump=main /dev/stdin
```

```text:main_01__build.dump(抜粋)
  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 というものを初めて読みましたが、全くわからんものではないということがわかったのが大きな収穫でした。 これなら自分もなにか作れそうな気がしてきました。

新型コロナウイルスの影響でイベント開催中止の決定が相次いでいるのは残念ですが、多くのコミュニティでオンライン開催に切り替えられ、急造ながらも質問や感想をその場で伝えられる双方向なイベントが実現できているのは大変素晴らしいことだと思います。イベントを主催される方々の熱意と努力に敬意を表します。また、今後の技術の洗練やノウハウの蓄積によるオンラインイベントの進化にも期待したいと思います。