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] スコープ外で参照される可能性のある変数はヒープにエスケープされる

Last updated at Posted at 2024-11-12

目的

スコープ外で参照される可能性のある変数はヒープにエスケープされ、ローカル変数として完結する変数はヒープにエスケープされないことを理解する。

注意

  • 今回検証する値は、サイズを小さくしています。サイズが大きい場合、どんな状況でもヒープに配置されるので、今回の検証とは異なる結果になります。
  • 今回は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スライスが、このスタックフレーム以外でも使用される可能性があるからである。ただし、先ほども言ったように、呼び出し元でインライン展開されない前提である。

main.go
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スライスがエスケープしないことがわかる。

main.go
package main

func main() {
	f1()
	f2()
}

func f1() {
	_ = make([]int, 1)
}

func f2() []int {
	res := make([]int, 1)
	return res
}
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: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>が出力されている。

main.go
package main

func main() {
	f1()
	f2()
}

func f1() {
	_ = make([]int, 1)
}

//go:noinline
func f2() []int {
	res := make([]int, 1)
	return res
}
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: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によって、完全なコピーが返るので、ヒープにエスケープされることはないです。

main.go
package main

func main() {
	f1()
	f2()
}

func f1() {
	_ = make([]int, 1)
}

//go:noinline
func f2() int {
	res := 123456
	return res
}

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: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のポインタを受け取ることはあるけれど。

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?