Check inline expansion with Go

164fd510e92a1912155b869b2c333c1e?s=47 Tsuji Daishiro
December 04, 2019

Check inline expansion with Go

164fd510e92a1912155b869b2c333c1e?s=128

Tsuji Daishiro

December 04, 2019
Tweet

Transcript

  1. インライン展開をGoでのぞいてみる 2019/12/4(Wed) golang.tokyo #28 Future Architect Tsuji Daishiro

  2. Who are you? 辻 大志郎(つじ だいしろう) @d_tutuz 渋谷区役所(~2014/9) Future Architect(2014/10~)

    ✓ 所属 Technology Innovation Group 競技プログラミング部 2
  3. None
  4. 関数のインライン展開 やっていますか? 4

  5. 関数のインライン展開とは? ✓ コンパイラのコード最適化テクのひとつ ✓ 関数呼び出しのコードを呼び出し元のコードに展開 ✓ 関数呼び出しのオーバヘッドを削減し、高速化 5 package main

    func main() { a, b := 10, 20 res := add(a, b) println(res) } func add(a, b int) int { return a + b } package main func main() { // a, b := 10, 20 // res := a + b res := 30 //インライン展開 +定数畳み込み println(res) } func add(a, b int) int { return a + b }
  6. アセンブラをのぞいてみる 6

  7. Goの標準ツールのobjdumpで逆アセンブリ ✓ 実行ファイルのバイナリから逆アセンブリできるGoの標準ツール ✓ バイナリのTEXTセグメントを出力 7 go tool objdump [-s

    symregexp] binary
  8. インライン展開する関数としない関数 8 package hello //go:noinline func HelloNoInline() interface{} { return

    struct{}{} } func Hello() interface{} { return struct{}{} } hello.go pragmaで明示的にインライン展開を抑制 ✓ 関数自体は空のstructをreturnする何もしない関数
  9. それぞれビルドしてアセンブリを出力 9 package main func main() { hello.HelloNoInline() } main.go

    go build main.go go tool objdump main.exe > dump_noinline.txt インライン展開しない package main func main() { hello.Hello() } main.go go build main.go go tool objdump main.exe > dump.txt インライン展開する
  10. インライン展開しない場合 10 TEXT github.com/d-tsuji/go-sandbox/opt/hello.HelloNoInline(SB) C:/Users/tsuji/go/src/github.com/d-tsuji/go-sandbox/opt/hello/hello.go hello.go:5 0x451e30 488d0549ec0000 LEAQ runtime.rodata+60032(SB),

    AX hello.go:5 0x451e37 4889442408 MOVQ AX, 0x8(SP) hello.go:5 0x451e3c 488d058d6d0800 LEAQ runtime.zerobase(SB), AX hello.go:5 0x451e43 4889442410 MOVQ AX, 0x10(SP) hello.go:5 0x451e48 c3 RET :-1 0x451e49 cc INT $0x3 :-1 0x451e4a cc INT $0x3 :-1 0x451e4b cc INT $0x3 :-1 0x451e4c cc INT $0x3 :-1 0x451e4d cc INT $0x3 :-1 0x451e4e cc INT $0x3 :-1 0x451e4f cc INT $0x3 TEXT main.main(SB) C:/Users/tsuji/go/src/github.com/d-tsuji/go-sandbox/opt/hello/cmd/main.go main.go:5 0x451e50 65488b0c2528000000 MOVQ GS:0x28, CX main.go:5 0x451e59 488b8900000000 MOVQ 0(CX), CX main.go:5 0x451e60 483b6110 CMPQ 0x10(CX), SP main.go:5 0x451e64 761d JBE 0x451e83 main.go:5 0x451e66 4883ec18 SUBQ $0x18, SP main.go:5 0x451e6a 48896c2410 MOVQ BP, 0x10(SP) main.go:5 0x451e6f 488d6c2410 LEAQ 0x10(SP), BP main.go:6 0x451e74 e8b7ffffff CALL github.com/d-tsuji/go-sandbox/opt/hello.HelloNoInline(SB) main.go:7 0x451e79 488b6c2410 MOVQ 0x10(SP), BP main.go:7 0x451e7e 4883c418 ADDQ $0x18, SP main.go:7 0x451e82 c3 RET main.go:5 0x451e83 e8487effff CALL runtime.morestack_noctxt(SB) main.go:5 0x451e88 ebc6 JMP main.main(SB) main.goからHelloNoInlineが呼び出されている
  11. インライン展開する場合 11 TEXT main.main(SB) C:/Users/tsuji/go/src/github.com/d-tsuji/go-sandbox/opt/hello/cmd/main.go main.go:5 0x451e30 c3 RET :0

    0x451e31 cc INT $0x3 :0 0x451e32 cc INT $0x3 :0 0x451e33 cc INT $0x3 :0 0x451e34 cc INT $0x3 :0 0x451e35 cc INT $0x3 :0 0x451e36 cc INT $0x3 :0 0x451e37 cc INT $0x3 :0 0x451e38 cc INT $0x3 :0 0x451e39 cc INT $0x3 :0 0x451e3a cc INT $0x3 :0 0x451e3b cc INT $0x3 :0 0x451e3c cc INT $0x3 :0 0x451e3d cc INT $0x3 :0 0x451e3e cc INT $0x3 :0 0x451e3f cc INT $0x3 main.goからHelloの呼び出しがなくなっている
  12. 呼び出し元に展開されることがわかった 12

  13. 関数呼び出しのコストって? 13

  14. ベンチマークを取って関数呼び出しのコストを確認 14 package hello import "testing" func BenchmarkHello(b *testing.B) {

    for i := 0; i < b.N; i++ { Hello() } } func BenchmarkHelloNoInline(b *testing.B) { for i := 0; i < b.N; i++ { HelloNoInline() } } test_hello.go ✓ さきほどの関数を用いてベンチマークをとる
  15. ベンチマーク結果 15 go test -bench . –benchmem goos: windows goarch:

    amd64 pkg: github.com/d-tsuji/go-sandbox/opt/hello BenchmarkHello-8 2000000000 0.34 ns/op 0 B/op 0 allocs/op BenchmarkHelloNoInline-8 2000000000 1.51 ns/op 0 B/op 0 allocs/op PASS ok github.com/d-tsuji/go-sandbox/opt/hello 4.295s ✓ 関数呼び出しのコストだけ考えると5倍程度の違い(極端な例ですが) ✓ 関数自体のコストが小さい場合は、関数呼び出しのオーバヘッドが無視できない場合も
  16. 落ち葉拾い 16

  17. ビルド時に最適化の結果を確認 17 ✓ 今回は実行ファイルのアセンブリから確認したかったためobjdumpを用いた ✓ ビルド時に最適化の結果を出力できる(インライン展開以外の最適化結果も出力) ✓ go build –gcflags

    “-m” main.go ✓ go build -gcflags “-m -m” main.go go build -gcflags "-m -m" main.go # command-line-arguments .¥main.go:5:6: can inline main as: func() { hello.Hello() } .¥main.go:6:13: inlining call to hello.Hello func() interface {} { return (interface {})(struct {} literal) } .¥main.go:6:13: main (interface {})(struct {} literal) does not escape 今回のインライン展開する実装例の場合だと、以下のような結果が得られる
  18. 関数がインライン展開される条件 18 ✓ Compiler And Runtime Optimizations ✓ https://github.com/golang/go/wiki/CompilerOptimizations ✓

    関数に含まれる expressions (式?)が40個未満 ✓ 関数呼び出し・ループ・クロージャー・panic・recover・select・switch といった複雑なものを含ま ない Only short and simple functions are inlined. To be inlined a function must contain less than ~40 expressions and does not contain complex things like function calls, loops, labels, closures, panic's, recover's, select's, switch'es, etc expressionsの解釈がわからず…
  19. None