目的
インライン展開により、-gcflags
でmoved 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()
についての出力はない。
package main
func main() {
_ = f()
}
func f() *int {
y := 2
res := y * 2
return &res
}
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
がヒープにエスケープされていることがわかるだろう。
package main
func main() {
_ = f()
}
//go:noinline
func f() *int {
y := 2
res := y * 2
return &res
}
同上
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
そのものをスタック領域からヒープ領域にコピーする必要がある。
ヒープへの読み書き速度は、スタックへの読み書き速度よりもはるかに遅いため、このような時間差が生まれる。
ポインタを返せば、速度が速くなると安易に考えるのは非常に危険である。
参考