目的
スライスが作成されるとき、容量が未知の場合は、ヒープに配置されることを理解する。
環境
$ go version
go version go1.23.2 linux/amd64
検証
(1) スライスの容量が既知
スライスはヒープにエスケープされていない(スタックに配置されている)ことがわかる。
main.go
package main
func main() {
_ = make([]byte, 1, 2)
}
main_test.go
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:3:6: can inline main with cost 5 as: func() { _ = make([]byte, 1, 2) }
./main.go:4:10: make([]byte, 1, 2) 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 1000000000 0.3725 ns/op 0 B/op 0 allocs/op
PASS
ok gopractice 0.418s
$ 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) スライスの容量が未知
スライスはヒープにエスケープされていることがわかる。
main.go
package main
func main() {
n := 2
_ = make([]byte, 1, n)
}
main_test.go
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:3:6: can inline main with cost 10 as: func() { n := 2; _ = make([]byte, 1, n) }
./main.go:5:10: make([]byte, 1, n) escapes to heap:
./main.go:5:10: flow: {heap} = &{storage for make([]byte, 1, n)}:
./main.go:5:10: from make([]byte, 1, n) (non-constant size) at ./main.go:5:10
./main.go:5:10: make([]byte, 1, n) 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 88923357 12.78 ns/op 2 B/op 1 allocs/op
PASS
ok gopractice 1.156s
$ 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 25 jbe 466c0b <main.main+0x2b>
466be6: 55 push %rbp
466be7: 48 89 e5 mov %rsp,%rbp
466bea: 48 83 ec 18 sub $0x18,%rsp
466bee: 48 8d 05 4b 56 00 00 lea 0x564b(%rip),%rax # 46c240 <type:*+0x5240>
466bf5: bb 01 00 00 00 mov $0x1,%ebx
466bfa: b9 02 00 00 00 mov $0x2,%ecx
466bff: 90 nop
466c00: e8 5b 79 ff ff call 45e560 <runtime.makeslice>
466c05: 48 83 c4 18 add $0x18,%rsp
466c09: 5d pop %rbp
466c0a: c3 ret
466c0b: e8 10 b1 ff ff call 461d20 <runtime.morestack_noctxt.abi0>
466c10: eb ce jmp 466be0 <main.main>
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
(3)
スライスの容量が既知でも、そのスタックフレーム以外に引き渡されるならば、ヒープにエスケープされる。
2 allocs/op
を見ると、二回のヒープへのエスケープが発生している。fmt.Printf("%p\n", &s)
は、スライス構造体のアドレスを引き渡しており、これにより、スライス構造体のエスケープと、スライス要素のエスケープが発生するので、二回のエスケープが発生している。
main.go
package main
import "fmt"
func main() {
s := make([]byte, 1, 17)
fmt.Printf("%p\n", &s)
fmt.Printf("%p\n", &s[0])
}
main_test.go
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:5:6: cannot inline main: function too complex: cost 171 exceeds budget 80
./main.go:7:12: inlining call to fmt.Printf
./main.go:8:12: inlining call to fmt.Printf
./main.go:6:11: make([]byte, 1, 17) escapes to heap:
./main.go:6:11: flow: s = &{storage for make([]byte, 1, 17)}:
./main.go:6:11: from make([]byte, 1, 17) (spill) at ./main.go:6:11
./main.go:6:11: from s := make([]byte, 1, 17) (assign) at ./main.go:6:4
./main.go:6:11: flow: {storage for ... argument} = s:
./main.go:6:11: from s[0] (dot of pointer) at ./main.go:8:23
./main.go:6:11: from &s[0] (address-of) at ./main.go:8:21
./main.go:6:11: from &s[0] (interface-converted) at ./main.go:8:21
./main.go:6:11: from ... argument (slice-literal-element) at ./main.go:8:12
./main.go:6:11: flow: fmt.a = &{storage for ... argument}:
./main.go:6:11: from ... argument (spill) at ./main.go:8:12
./main.go:6:11: from fmt.format, fmt.a := "%p\n", ... argument (assign-pair) at ./main.go:8:12
./main.go:6:11: flow: {heap} = *fmt.a:
./main.go:6:11: from fmt.Fprintf(os.Stdout, fmt.format, fmt.a...) (call parameter) at ./main.go:8:12
./main.go:6:2: s escapes to heap:
./main.go:6:2: flow: {storage for ... argument} = &s:
./main.go:6:2: from &s (address-of) at ./main.go:7:21
./main.go:6:2: from &s (interface-converted) at ./main.go:7:21
./main.go:6:2: from ... argument (slice-literal-element) at ./main.go:7:12
./main.go:6:2: flow: fmt.a = &{storage for ... argument}:
./main.go:6:2: from ... argument (spill) at ./main.go:7:12
./main.go:6:2: from fmt.format, fmt.a := "%p\n", ... argument (assign-pair) at ./main.go:7:12
./main.go:6:2: flow: {heap} = *fmt.a:
./main.go:6:2: from fmt.Fprintf(os.Stdout, fmt.format, fmt.a...) (call parameter) at ./main.go:7:12
./main.go:6:2: moved to heap: s
./main.go:6:11: make([]byte, 1, 17) escapes to heap
./main.go:7:12: ... argument does not escape
./main.go:8:12: ... argument does not escape
$ go test -bench=. -benchmem
...
0xc000250d08
0xc00024ef18
0xc000250d20
0xc00024ef30
0xc000250d38
0xc00024ef48
62806 18745 ns/op 48 B/op 2 allocs/op
PASS
ok gopractice 1.376s
$ go build -o main_binary main.go
$ objdump -d main_binary > objdump.txt
0000000000490880 <main.main>:
490880: 49 3b 66 10 cmp 0x10(%r14),%rsp
490884: 0f 86 fc 00 00 00 jbe 490986 <main.main+0x106>
49088a: 55 push %rbp
49088b: 48 89 e5 mov %rsp,%rbp
49088e: 48 83 ec 60 sub $0x60,%rsp
490892: 48 8d 05 27 80 00 00 lea 0x8027(%rip),%rax # 4988c0 <type:*+0x78c0>
490899: e8 e2 bf f7 ff call 40c880 <runtime.newobject>
49089e: 48 89 44 24 58 mov %rax,0x58(%rsp)
4908a3: bb 01 00 00 00 mov $0x1,%ebx
4908a8: b9 11 00 00 00 mov $0x11,%ecx
4908ad: 48 8d 05 cc 8e 00 00 lea 0x8ecc(%rip),%rax # 499780 <type:*+0x8780>
4908b4: e8 c7 5a fd ff call 466380 <runtime.makeslice>
4908b9: 48 8b 4c 24 58 mov 0x58(%rsp),%rcx
4908be: 48 c7 41 08 01 00 00 movq $0x1,0x8(%rcx)
4908c5: 00
4908c6: 48 c7 41 10 11 00 00 movq $0x11,0x10(%rcx)
4908cd: 00
4908ce: 83 3d fb 3c 0e 00 00 cmpl $0x0,0xe3cfb(%rip) # 5745d0 <runtime.writeBarrier>
4908d5: 74 0f je 4908e6 <main.main+0x66>
4908d7: e8 84 b0 fd ff call 46b960 <runtime.gcWriteBarrier2>
4908dc: 49 89 03 mov %rax,(%r11)
4908df: 48 8b 11 mov (%rcx),%rdx
4908e2: 49 89 53 08 mov %rdx,0x8(%r11)
4908e6: 48 89 01 mov %rax,(%rcx)
4908e9: 48 8d 15 10 7c 00 00 lea 0x7c10(%rip),%rdx # 498500 <type:*+0x7500>
4908f0: 48 89 54 24 48 mov %rdx,0x48(%rsp)
4908f5: 48 89 4c 24 50 mov %rcx,0x50(%rsp)
4908fa: 48 8b 1d 67 3b 0c 00 mov 0xc3b67(%rip),%rbx # 554468 <os.Stdout>
490901: 48 8d 05 f0 4f 04 00 lea 0x44ff0(%rip),%rax # 4d58f8 <go:itab.*os.File,io.Writer>
490908: bf 03 00 00 00 mov $0x3,%edi
49090d: 48 8d 74 24 48 lea 0x48(%rsp),%rsi
490912: 41 b8 01 00 00 00 mov $0x1,%r8d
490918: 4d 89 c1 mov %r8,%r9
49091b: 48 8d 0d 82 f6 01 00 lea 0x1f682(%rip),%rcx # 4affa4 <go:string.*+0x4c>
490922: e8 19 98 ff ff call 48a140 <fmt.Fprintf>
490927: 48 8b 54 24 58 mov 0x58(%rsp),%rdx
49092c: 48 83 7a 08 00 cmpq $0x0,0x8(%rdx)
490931: 76 47 jbe 49097a <main.main+0xfa>
490933: 48 8b 12 mov (%rdx),%rdx
490936: 4c 8d 15 43 64 00 00 lea 0x6443(%rip),%r10 # 496d80 <type:*+0x5d80>
49093d: 4c 89 54 24 38 mov %r10,0x38(%rsp)
490942: 48 89 54 24 40 mov %rdx,0x40(%rsp)
490947: 48 8b 1d 1a 3b 0c 00 mov 0xc3b1a(%rip),%rbx # 554468 <os.Stdout>
49094e: 48 8d 05 a3 4f 04 00 lea 0x44fa3(%rip),%rax # 4d58f8 <go:itab.*os.File,io.Writer>
490955: 48 8d 0d 48 f6 01 00 lea 0x1f648(%rip),%rcx # 4affa4 <go:string.*+0x4c>
49095c: bf 03 00 00 00 mov $0x3,%edi
490961: 48 8d 74 24 38 lea 0x38(%rsp),%rsi
490966: 41 b8 01 00 00 00 mov $0x1,%r8d
49096c: 4d 89 c1 mov %r8,%r9
49096f: e8 cc 97 ff ff call 48a140 <fmt.Fprintf>
490974: 48 83 c4 60 add $0x60,%rsp
490978: 5d pop %rbp
490979: c3 ret
49097a: 31 c0 xor %eax,%eax
49097c: 48 89 c1 mov %rax,%rcx
49097f: 90 nop
490980: e8 7b b3 fd ff call 46bd00 <runtime.panicIndex>
490985: 90 nop
490986: e8 55 92 fd ff call 469be0 <runtime.morestack_noctxt.abi0>
49098b: e9 f0 fe ff ff jmp 490880 <main.main>
$ go run main.go
0xc00009e000
0xc0000a0000
参考
例えば、
s := make(int[], 10)
はヒープにエスケープされないかもしれませんが、s := make([]int, n)
はそのサイズが変数に基づいているのでヒープにエスケープされます。