Help us understand the problem. What is going on with this article?

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

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 トークで次のようなスライドを目にしました。

image.png

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

image.png

後で調べてみたところ Proposal: Low-cost defers through inline code, and extra funcdata to manage the panic case という文書の 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 がある関数は、従来と同じコードが生成されます。

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

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() を生成します。

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

bash
go run gen.go 8 | go tool compile -d ssa/build/dump=main /dev/stdin
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 セット生成されます。この呼び出しのエラーチェックとリターンも行っているため、だいぶ長くなっています。

bash
go run gen.go 9 | go tool compile -d ssa/build/dump=main /dev/stdin
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 というものを初めて読みましたが、全くわからんものではないということがわかったのが大きな収穫でした。 これなら自分もなにか作れそうな気がしてきました。

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

yaegashi
Linux や Unix が得意で低レベルなことが好きなエンジニアです
https://l0w.dev
bandainamcostudios
バンダイナムコスタジオは、家庭用ゲームソフト、モバイルコンテンツ、の企画・開発・運営、ゲームに関する技術研究・開発を行っている会社です。
https://www.bandainamcostudios.com
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした