目的
スコープ外で参照される可能性のある変数はヒープにエスケープされ、ローカル変数として完結する変数はヒープにエスケープされないことを理解する。
注意
- 今回検証する値は、サイズを小さくしています。サイズが大きい場合、どんな状況でもヒープに配置されるので、今回の検証とは異なる結果になります。
- 今回はintスライスを返り値として扱っていますが、int では再現できません。
- intスライスが返り値の場合、intスライス構造体のコピーが返り、intスライス要素はヒープにエスケープされます。int スライス(構造体)は、内部にintスライス要素へのポインタを持っています。
- intが返り値の場合は、完全なコピーが返るので、ヒープにエスケープされる可能性はないです。詳しくは、下記の(4)の例を見てください。
- 返り値として、intではなくて
&int
(intのアドレス)を使った場合、intそのものがヒープにエスケープされる必要性があるので、intスライスと同じような検証結果になるでしょう。
環境
$ go version
go version go1.23.2 linux/amd64
検証
(1)
まず大前提として、go build -gcflags "-m=2" .
の出力結果は、その関数のみに焦点を当てたものであり、呼び出し元でインライン展開されることは全く考慮していない。
./main.go:6:10: make([]int, 1) does not escape
を見ると、f1()
で作成されたintスライスはヒープではなくてスタックに配置されることがわかる。
./main.go:10:13: make([]int, 1) escapes to heap:
を見ると、f2()
で作成されたintスライスはヒープに配置されることがわかる。このintスライスが、このスタックフレーム以外でも使用される可能性があるからである。ただし、先ほども言ったように、呼び出し元でインライン展開されない前提である。
package main
func main() {}
func f1() {
_ = make([]int, 1)
}
func f2() []int {
res := make([]int, 1)
return res
}
$ go build -gcflags "-m=2" .
# gopractice
./main.go:3:6: can inline main with cost 0 as: func() { }
./main.go:5:6: can inline f1 with cost 4 as: func() { _ = make([]int, 1) }
./main.go:9:6: can inline f2 with cost 8 as: func() []int { res := make([]int, 1); return res }
./main.go:6:10: make([]int, 1) does not escape
./main.go:10:13: make([]int, 1) escapes to heap:
./main.go:10:13: flow: res = &{storage for make([]int, 1)}:
./main.go:10:13: from make([]int, 1) (spill) at ./main.go:10:13
./main.go:10:13: from res := make([]int, 1) (assign) at ./main.go:10:6
./main.go:10:13: flow: ~r0 = res:
./main.go:10:13: from return res (return) at ./main.go:11:2
./main.go:10:13: make([]int, 1) escapes to heap
$ go test -bench=. -benchmem
今回はやらない
$ go build -o main_binary main.go
$ objdump -d main_binary > objdump.txt
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)
前の(1)の例と違って、今回は、main()
が呼び出し元である。
先ほども言ったように、go build -gcflags "-m=2" .
は、その関数しか見ておらず、インライン展開を考慮していないため、f2()
ではヒープにエスケープするという結果になっている。
しかし、実際は、呼び出し元でインライン展開されているので、f2()
のintスライスはヒープにエスケープしない。その証拠に、ベンチマークテストで0 allocs/op
であり、さらには、objdump -d main_binary
の出力結果にcall 45e560 <runtime.makeslice>
のような出力がない。また、./main.go:5:4: make([]int, 1) does not escape
は、f2()
がインライン展開された後の挙動を示しており、intスライスがエスケープしないことがわかる。
package main
func main() {
f1()
f2()
}
func f1() {
_ = make([]int, 1)
}
func f2() []int {
res := make([]int, 1)
return res
}
package main
import "testing"
func Benchmark(b *testing.B) {
for i := 0; i < b.N; i++ {
main()
}
}
$ go build -gcflags "-m=2" .
# gopractice
./main.go:8:6: can inline f1 with cost 4 as: func() { _ = make([]int, 1) }
./main.go:12:6: can inline f2 with cost 8 as: func() []int { res := make([]int, 1); return res }
./main.go:3:6: can inline main with cost 16 as: func() { f1(); f2() }
./main.go:4:4: inlining call to f1
./main.go:5:4: inlining call to f2
./main.go:4:4: make([]int, 1) does not escape
./main.go:5:4: make([]int, 1) does not escape
./main.go:9:10: make([]int, 1) does not escape
./main.go:13:13: make([]int, 1) escapes to heap:
./main.go:13:13: flow: res = &{storage for make([]int, 1)}:
./main.go:13:13: from make([]int, 1) (spill) at ./main.go:13:13
./main.go:13:13: from res := make([]int, 1) (assign) at ./main.go:13:6
./main.go:13:13: flow: ~r0 = res:
./main.go:13:13: from return res (return) at ./main.go:14:2
./main.go:13:13: make([]int, 1) escapes to heap
$ 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.3703 ns/op 0 B/op 0 allocs/op
PASS
ok gopractice 0.417s
$ go build -o main_binary main.go
$ objdump -d main_binary > objdump.txt
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
(3)
f2()
を//go:noinline
にすると、呼び出し元でインライン展開されない。
このとき、f2()
のintスライスは、ヒープにエスケープされる。その証拠に、ベンチマークテストで1 allocs/op
となっており、さらに、call 45e560 <runtime.makeslice>
が出力されている。
package main
func main() {
f1()
f2()
}
func f1() {
_ = make([]int, 1)
}
//go:noinline
func f2() []int {
res := make([]int, 1)
return res
}
package main
import "testing"
func Benchmark(b *testing.B) {
for i := 0; i < b.N; i++ {
main()
}
}
$ go build -gcflags "-m=2" .
# gopractice
./main.go:8:6: can inline f1 with cost 4 as: func() { _ = make([]int, 1) }
./main.go:13:6: cannot inline f2: marked go:noinline
./main.go:3:6: can inline main with cost 65 as: func() { f1(); f2() }
./main.go:4:4: inlining call to f1
./main.go:14:13: make([]int, 1) escapes to heap:
./main.go:14:13: flow: res = &{storage for make([]int, 1)}:
./main.go:14:13: from make([]int, 1) (spill) at ./main.go:14:13
./main.go:14:13: from res := make([]int, 1) (assign) at ./main.go:14:6
./main.go:14:13: flow: ~r0 = res:
./main.go:14:13: from return res (return) at ./main.go:15:2
./main.go:14:13: make([]int, 1) escapes to heap
./main.go:4:4: make([]int, 1) does not escape
./main.go:9:10: make([]int, 1) does not escape
$ go test -bench=. -benchmem
goos: linux
goarch: amd64
pkg: gopractice
cpu: Intel(R) Xeon(R) CPU E5-2686 v4 @ 2.30GHz
Benchmark-2 59310711 17.13 ns/op 8 B/op 1 allocs/op
PASS
ok gopractice 1.043s
$ go build -o main_binary main.go
$ objdump -d main_binary > objdump.txt
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.f2>
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.f2>:
466c00: 49 3b 66 10 cmp 0x10(%r14),%rsp
466c04: 76 2d jbe 466c33 <main.f2+0x33>
466c06: 55 push %rbp
466c07: 48 89 e5 mov %rsp,%rbp
466c0a: 48 83 ec 18 sub $0x18,%rsp
466c0e: 48 8d 05 eb 67 00 00 lea 0x67eb(%rip),%rax # 46d400 <type:*+0x5400>
466c15: bb 01 00 00 00 mov $0x1,%ebx
466c1a: 48 89 d9 mov %rbx,%rcx
466c1d: 0f 1f 00 nopl (%rax)
466c20: e8 3b 79 ff ff call 45e560 <runtime.makeslice>
466c25: bb 01 00 00 00 mov $0x1,%ebx
466c2a: 48 89 d9 mov %rbx,%rcx
466c2d: 48 83 c4 18 add $0x18,%rsp
466c31: 5d pop %rbp
466c32: c3 ret
466c33: e8 e8 b0 ff ff call 461d20 <runtime.morestack_noctxt.abi0>
466c38: eb c6 jmp 466c00 <main.f2>
466c3a: cc int3
466c3b: cc int3
466c3c: cc int3
466c3d: cc int3
466c3e: cc int3
466c3f: cc int3
(4)
res
がスライスでなくてintだった場合は、return int
によって、完全なコピーが返るので、ヒープにエスケープされることはないです。
package main
func main() {
f1()
f2()
}
func f1() {
_ = make([]int, 1)
}
//go:noinline
func f2() int {
res := 123456
return res
}
package main
import "testing"
func Benchmark(b *testing.B) {
for i := 0; i < b.N; i++ {
main()
}
}
$ go build -gcflags "-m=2" .
# gopractice
./main.go:8:6: can inline f1 with cost 4 as: func() { _ = make([]int, 1) }
./main.go:13:6: cannot inline f2: marked go:noinline
./main.go:3:6: can inline main with cost 65 as: func() { f1(); f2() }
./main.go:4:4: inlining call to f1
./main.go:4:4: make([]int, 1) does not escape
./main.go:9:10: make([]int, 1) does not escape
$ go test -bench=. -benchmem
goos: linux
goarch: amd64
pkg: gopractice
cpu: Intel(R) Xeon(R) CPU E5-2686 v4 @ 2.30GHz
Benchmark-2 794488742 1.481 ns/op 0 B/op 0 allocs/op
PASS
ok gopractice 1.336s
$ go build -o main_binary main.go
$ objdump -d main_binary > objdump.txt
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.f2>
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.f2>:
466c00: b8 40 e2 01 00 mov $0x1e240,%eax
466c05: c3 ret
466c06: cc int3
466c07: cc int3
466c08: cc int3
466c09: cc int3
466c0a: cc int3
466c0b: cc int3
466c0c: cc int3
466c0d: cc int3
466c0e: cc int3
466c0f: cc int3
466c10: cc int3
466c11: cc int3
466c12: cc int3
466c13: cc int3
466c14: cc int3
466c15: cc int3
466c16: cc int3
466c17: cc int3
466c18: cc int3
466c19: cc int3
466c1a: cc int3
466c1b: cc int3
466c1c: cc int3
466c1d: cc int3
466c1e: cc int3
466c1f: cc int3
参考
sliceは内部にデータへのポインタを持っており、sliceそのものをポインタで渡すのは冗長であると言われる。他人のコードを見てもsliceのポインタを戻り値で返すケースはあまり見かけない。引数でsliceのポインタを受け取ることはあるけれど。