0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[golang] インライン展開により、`-gcflags` で`moved to heap` が出力されても、ヒープにエスケープしない場合もある

Posted at

目的

インライン展開により、-gcflagsmoved to heap が出力されても、ヒープにエスケープしない場合もあるということを理解する。

環境

$ go version
go version go1.23.2 linux/amd64

検証

(1) ヒープにエスケープしないパターン

moved to heap: res が出力されているが、f()はインライン展開されるので、resはヒープにエスケープしない。
moved to heap: resの部分の出力は、あくまでも、その関数のスコープしか見ていないことに注意しよう。つまり、f()関数だけを見ており、呼び出し元でインライン展開されるかどうかは全く気にしていない。
ベンチマークテストにおける0 allocs/opという結果や、<runtime.newobject> が出力されない結果を見れば、resがヒープにエスケープしてないことがわかるだろう。
後の(2)の例では、0000000000466c00 <main.f>の出力があるが、今回はf()が完全にmain()にインライン展開されているので、f()についての出力はない。

main.go
package main

func main() {
	_ = f()
}

func f() *int {
	y := 2
	res := y * 2
	return &res
}

main_test.go
package main

import "testing"

func Benchmark(b *testing.B) {
	for i := 0; i < b.N; i++ {
		_ = f()
	}
}

ubuntu@ip-172-31-33-158:~/gopractice$ go build -gcflags "-m=2" .
# gopractice
./main.go:7:6: can inline f with cost 15 as: func() *int { y := 2; res := y * 2; return &res }
./main.go:3:6: can inline main with cost 19 as: func() { _ = f() }
./main.go:4:7: inlining call to f
./main.go:9:2: res escapes to heap:
./main.go:9:2:   flow: ~r0 = &res:
./main.go:9:2:     from &res (address-of) at ./main.go:10:9
./main.go:9:2:     from return &res (return) at ./main.go:10:2
./main.go:9:2: moved to heap: res
$ go test -bench=. -benchmem
goos: linux
goarch: amd64
pkg: gopractice
cpu: Intel(R) Xeon(R) CPU E5-2686 v4 @ 2.30GHz
Benchmark-2     1000000000               0.3741 ns/op          0 B/op          0 allocs/op
PASS
ok      gopractice      0.422s
$ go build -o main_binary main.go
$ objdump -d main_binary
...
0000000000466be0 <main.main>:
  466be0:       c3                      ret
  466be1:       cc                      int3
  466be2:       cc                      int3
  466be3:       cc                      int3
  466be4:       cc                      int3
  466be5:       cc                      int3
  466be6:       cc                      int3
  466be7:       cc                      int3
  466be8:       cc                      int3
  466be9:       cc                      int3
  466bea:       cc                      int3
  466beb:       cc                      int3
  466bec:       cc                      int3
  466bed:       cc                      int3
  466bee:       cc                      int3
  466bef:       cc                      int3
  466bf0:       cc                      int3
  466bf1:       cc                      int3
  466bf2:       cc                      int3
  466bf3:       cc                      int3
  466bf4:       cc                      int3
  466bf5:       cc                      int3
  466bf6:       cc                      int3
  466bf7:       cc                      int3
  466bf8:       cc                      int3
  466bf9:       cc                      int3
  466bfa:       cc                      int3
  466bfb:       cc                      int3
  466bfc:       cc                      int3
  466bfd:       cc                      int3
  466bfe:       cc                      int3
  466bff:       cc                      int3
...

(2) ヒープにエスケープするパターン

//go:noinline があるので、f()はインライン展開されず、resはヒープにエスケープする。
ベンチマークテストの1 allocs/opという結果や、call 40b2e0 <runtime.newobject>という出力結果を見れば、resがヒープにエスケープされていることがわかるだろう。

main.go
package main

func main() {
	_ = f()
}

//go:noinline
func f() *int {
	y := 2
	res := y * 2
	return &res
}

main_test.go
同上
ubuntu@ip-172-31-33-158:~/gopractice$ go build -gcflags "-m=2" .
# gopractice
./main.go:8:6: cannot inline f: marked go:noinline
./main.go:3:6: can inline main with cost 61 as: func() { _ = f() }
./main.go:10:2: res escapes to heap:
./main.go:10:2:   flow: ~r0 = &res:
./main.go:10:2:     from &res (address-of) at ./main.go:11:9
./main.go:10:2:     from return &res (return) at ./main.go:11:2
./main.go:10:2: moved to heap: res
$ go test -bench=. -benchmem
goos: linux
goarch: amd64
pkg: gopractice
cpu: Intel(R) Xeon(R) CPU E5-2686 v4 @ 2.30GHz
Benchmark-2     61113674                16.46 ns/op            8 B/op          1 allocs/op
PASS
ok      gopractice      1.031s
$ go build -o main_binary main.go
$ objdump -d main_binary
...
0000000000466be0 <main.main>:
  466be0:       49 3b 66 10             cmp    0x10(%r14),%rsp
  466be4:       76 0b                   jbe    466bf1 <main.main+0x11>
  466be6:       55                      push   %rbp
  466be7:       48 89 e5                mov    %rsp,%rbp
  466bea:       e8 11 00 00 00          call   466c00 <main.f>
  466bef:       5d                      pop    %rbp
  466bf0:       c3                      ret
  466bf1:       e8 2a b1 ff ff          call   461d20 <runtime.morestack_noctxt.abi0>
  466bf6:       eb e8                   jmp    466be0 <main.main>
  466bf8:       cc                      int3
  466bf9:       cc                      int3
  466bfa:       cc                      int3
  466bfb:       cc                      int3
  466bfc:       cc                      int3
  466bfd:       cc                      int3
  466bfe:       cc                      int3
  466bff:       cc                      int3

0000000000466c00 <main.f>:
  466c00:       49 3b 66 10             cmp    0x10(%r14),%rsp
  466c04:       76 21                   jbe    466c27 <main.f+0x27>
  466c06:       55                      push   %rbp
  466c07:       48 89 e5                mov    %rsp,%rbp
  466c0a:       48 83 ec 10             sub    $0x10,%rsp
  466c0e:       48 8d 05 eb 67 00 00    lea    0x67eb(%rip),%rax        # 46d400 <type:*+0x5400>
  466c15:       e8 c6 46 fa ff          call   40b2e0 <runtime.newobject>
  466c1a:       48 c7 00 04 00 00 00    movq   $0x4,(%rax)
  466c21:       48 83 c4 10             add    $0x10,%rsp
  466c25:       5d                      pop    %rbp
  466c26:       c3                      ret
  466c27:       e8 f4 b0 ff ff          call   461d20 <runtime.morestack_noctxt.abi0>
  466c2c:       eb d2                   jmp    466c00 <main.f>
  466c2e:       cc                      int3
  466c2f:       cc                      int3
  466c30:       cc                      int3
  466c31:       cc                      int3
  466c32:       cc                      int3
  466c33:       cc                      int3
  466c34:       cc                      int3
  466c35:       cc                      int3
  466c36:       cc                      int3
  466c37:       cc                      int3
  466c38:       cc                      int3
  466c39:       cc                      int3
  466c3a:       cc                      int3
  466c3b:       cc                      int3
  466c3c:       cc                      int3
  466c3d:       cc                      int3
  466c3e:       cc                      int3
  466c3f:       cc                      int3
...

おまけ

速度

ヒープにエスケープしない場合よりも、ヒープにエスケープした場合の方が明らかに時間がかかっている。具体的には、前者が0.3741 ns/opであり、後者が16.46 ns/opである。
ヒープにエスケープしない場合は、resのコピーを呼び出し元に返す。
ヒープにエスケープする場合は、resのアドレスのコピーを呼び出し元に返し、さらに、resそのものをスタック領域からヒープ領域にコピーする必要がある。
ヒープへの読み書き速度は、スタックへの読み書き速度よりもはるかに遅いため、このような時間差が生まれる。
ポインタを返せば、速度が速くなると安易に考えるのは非常に危険である。

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?