目的
グローバル変数は、スタックでもヒープでもなく、Data segment というメモリ静的メモリ領域に配置されることを理解する。
下記の書籍に、ヒープにエスケープされると書いてあるが、これは間違いで、ヒープに配置されるわけではない。
変数がヒープにエスケープされる他の場合として、次のようなものがあります。
- グローバル変数(複数のゴルーチンがアクセスできるので)
環境
$ go version
go version go1.23.2 linux/amd64
検証
(1)
myValue
というグローバル変数は、0x54a128
( 54a128
)というメモリ領域に配置されていることがわかる。
このメモリ領域は、スタックでもヒープでもなく、Data segmentという静的メモリ領域である。
lea 0xb9887(%rip),%rdx # 54a128 <main.myValue>
という記述は、rip
アドレスから、rdx
アドレスにデータをロードしており、rip
はグローバル変数のみに使われるアドレスの種類である。
ベンチマークテストでは、0x64e1e0
というアドレスになっていて、go run
とかの結果とは異なるが、Data segment に配置されることには変わりない。
package main
import "fmt"
var myValue = 64 * 1024
func main() {
fmt.Printf("%p\n", &myValue)
}
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:7:6: cannot inline main: function too complex: cost 81 exceeds budget 80
./main.go:8:12: inlining call to fmt.Printf
./main.go:8:12: ... argument does not escape
$ go test -bench=. -benchmem
...
0x64e1e0
0x64e1e0
...
301125 6292 ns/op 0 B/op 0 allocs/op
PASS
ok gopractice 1.942s
$ 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: 76 53 jbe 4908d9 <main.main+0x59>
490886: 55 push %rbp
490887: 48 89 e5 mov %rsp,%rbp
49088a: 48 83 ec 48 sub $0x48,%rsp
49088e: 48 8d 15 ab 66 00 00 lea 0x66ab(%rip),%rdx # 496f40 <type:*+0x5f40>
490895: 48 89 54 24 38 mov %rdx,0x38(%rsp)
49089a: 48 8d 15 87 98 0b 00 lea 0xb9887(%rip),%rdx # 54a128 <main.myValue>
4908a1: 48 89 54 24 40 mov %rdx,0x40(%rsp)
4908a6: 48 8b 1d bb 3b 0c 00 mov 0xc3bbb(%rip),%rbx # 554468 <os.Stdout>
4908ad: 48 8d 05 04 50 04 00 lea 0x45004(%rip),%rax # 4d58b8 <go:itab.*os.File,io.Writer>
4908b4: 48 8d 0d e9 f6 01 00 lea 0x1f6e9(%rip),%rcx # 4affa4 <go:string.*+0x4c>
4908bb: bf 03 00 00 00 mov $0x3,%edi
4908c0: 48 8d 74 24 38 lea 0x38(%rsp),%rsi
4908c5: 41 b8 01 00 00 00 mov $0x1,%r8d
4908cb: 4d 89 c1 mov %r8,%r9
4908ce: e8 6d 98 ff ff call 48a140 <fmt.Fprintf>
4908d3: 48 83 c4 48 add $0x48,%rsp
4908d7: 5d pop %rbp
4908d8: c3 ret
4908d9: e8 02 93 fd ff call 469be0 <runtime.morestack_noctxt.abi0>
4908de: 66 90 xchg %ax,%ax
4908e0: eb 9e jmp 490880 <main.main>
$ go run main.go
0x54a128
(2)
0x54fa00
(54fa00
)は、DataSegment 領域のアドレスである。これは、グローバル変数であるスライス構造体のアドレスを示している。
スライスは、容量を超えたappend
をすると、容量が2倍となるスライスのコピーを作り(スライス要素のみコピーする)、ヒープに配置する。
今回は、容量を超えたappendをしたので、容量二倍のスライス要素のコピーがヒープ領域に作成された。しかし、スライス構造体自体のアドレスは変わっていない。
package main
import "fmt"
var myValue = []int{1, 2, 3, 4, 5}
func main() {
fmt.Printf("Slice struct address: %p\n", &myValue) // スライス構造のアドレス
fmt.Printf("Data address before append: %p\n", &myValue[0]) // データ領域のアドレス
myValue = append(myValue, 6)
fmt.Printf("Slice struct address after append: %p\n", &myValue) // スライス構造のアドレス
fmt.Printf("Data address after append: %p\n", &myValue[0]) // 新しいデータ領域のアドレス
}
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:7:6: cannot inline main: function too complex: cost 333 exceeds budget 80
./main.go:8:12: inlining call to fmt.Printf
./main.go:9:12: inlining call to fmt.Printf
./main.go:11:12: inlining call to fmt.Printf
./main.go:12:12: inlining call to fmt.Printf
./main.go:8:12: ... argument does not escape
./main.go:9:12: ... argument does not escape
./main.go:11:12: ... argument does not escape
./main.go:12:12: ... argument does not escape
$ go test -bench=. -benchmem
...
Slice struct address: 0x661e80
Data address before append: 0xc000154000
Slice struct address after append: 0x661e80
Data address after append: 0xc000154000
Slice struct address: 0x661e80
Data address before append: 0xc000154000
Slice struct address after append: 0x661e80
Data address after append: 0xc000154000
...
10000 149283 ns/op 34 B/op 0 allocs/op
PASS
ok gopractice 1.503s
$ go build -o main_binary main.go
$ objdump -d main_binary > objdump.txt
0000000000490880 <main.main>:
490880: 4c 8d 64 24 f8 lea -0x8(%rsp),%r12
490885: 4d 3b 66 10 cmp 0x10(%r14),%r12
490889: 0f 86 b7 01 00 00 jbe 490a46 <main.main+0x1c6>
49088f: 55 push %rbp
490890: 48 89 e5 mov %rsp,%rbp
490893: 48 83 c4 80 add $0xffffffffffffff80,%rsp
490897: 48 8d 15 62 7c 00 00 lea 0x7c62(%rip),%rdx # 498500 <type:*+0x7500>
49089e: 48 89 54 24 70 mov %rdx,0x70(%rsp)
4908a3: 4c 8d 15 56 f1 0b 00 lea 0xbf156(%rip),%r10 # 54fa00 <main.myValue>
4908aa: 4c 89 54 24 78 mov %r10,0x78(%rsp)
4908af: 48 8b 1d 12 3c 0c 00 mov 0xc3c12(%rip),%rbx # 5544c8 <os.Stdout>
4908b6: 48 8d 05 3b 51 04 00 lea 0x4513b(%rip),%rax # 4d59f8 <go:itab.*os.File,io.Writer>
4908bd: 48 8d 0d 01 30 02 00 lea 0x23001(%rip),%rcx # 4b38c5 <go:string.*+0x392d>
4908c4: bf 19 00 00 00 mov $0x19,%edi
4908c9: 48 8d 74 24 70 lea 0x70(%rsp),%rsi
4908ce: 41 b8 01 00 00 00 mov $0x1,%r8d
4908d4: 4d 89 c1 mov %r8,%r9
4908d7: e8 64 98 ff ff call 48a140 <fmt.Fprintf>
4908dc: 48 83 3d 24 f1 0b 00 cmpq $0x0,0xbf124(%rip) # 54fa08 <main.myValue+0x8>
4908e3: 00
4908e4: 0f 86 51 01 00 00 jbe 490a3b <main.main+0x1bb>
4908ea: 48 8b 15 0f f1 0b 00 mov 0xbf10f(%rip),%rdx # 54fa00 <main.myValue>
4908f1: 4c 8d 15 48 66 00 00 lea 0x6648(%rip),%r10 # 496f40 <type:*+0x5f40>
4908f8: 4c 89 54 24 60 mov %r10,0x60(%rsp)
4908fd: 48 89 54 24 68 mov %rdx,0x68(%rsp)
490902: 48 8b 1d bf 3b 0c 00 mov 0xc3bbf(%rip),%rbx # 5544c8 <os.Stdout>
490909: 48 8d 05 e8 50 04 00 lea 0x450e8(%rip),%rax # 4d59f8 <go:itab.*os.File,io.Writer>
490910: 48 8d 0d 66 46 02 00 lea 0x24666(%rip),%rcx # 4b4f7d <go:string.*+0x4fe5>
490917: bf 1f 00 00 00 mov $0x1f,%edi
49091c: 48 8d 74 24 60 lea 0x60(%rsp),%rsi
490921: 41 b8 01 00 00 00 mov $0x1,%r8d
490927: 4d 89 c1 mov %r8,%r9
49092a: e8 11 98 ff ff call 48a140 <fmt.Fprintf>
49092f: 48 8b 0d da f0 0b 00 mov 0xbf0da(%rip),%rcx # 54fa10 <main.myValue+0x10>
490936: 48 8b 1d cb f0 0b 00 mov 0xbf0cb(%rip),%rbx # 54fa08 <main.myValue+0x8>
49093d: 48 ff c3 inc %rbx
490940: 48 8b 05 b9 f0 0b 00 mov 0xbf0b9(%rip),%rax # 54fa00 <main.myValue>
490947: 48 39 d9 cmp %rbx,%rcx
49094a: 73 3b jae 490987 <main.main+0x107>
49094c: bf 01 00 00 00 mov $0x1,%edi
490951: 48 8d 35 28 90 00 00 lea 0x9028(%rip),%rsi # 499980 <type:*+0x8980>
490958: e8 03 5b fd ff call 466460 <runtime.growslice>
49095d: 48 89 0d ac f0 0b 00 mov %rcx,0xbf0ac(%rip) # 54fa10 <main.myValue+0x10>
490964: 83 3d c5 3c 0e 00 00 cmpl $0x0,0xe3cc5(%rip) # 574630 <runtime.writeBarrier>
49096b: 74 13 je 490980 <main.main+0x100>
49096d: e8 ee af fd ff call 46b960 <runtime.gcWriteBarrier2>
490972: 49 89 03 mov %rax,(%r11)
490975: 48 8b 15 84 f0 0b 00 mov 0xbf084(%rip),%rdx # 54fa00 <main.myValue>
49097c: 49 89 53 08 mov %rdx,0x8(%r11)
490980: 48 89 05 79 f0 0b 00 mov %rax,0xbf079(%rip) # 54fa00 <main.myValue>
490987: 48 89 1d 7a f0 0b 00 mov %rbx,0xbf07a(%rip) # 54fa08 <main.myValue+0x8>
49098e: 48 c7 44 d8 f8 06 00 movq $0x6,-0x8(%rax,%rbx,8)
490995: 00 00
490997: 48 8d 15 62 7b 00 00 lea 0x7b62(%rip),%rdx # 498500 <type:*+0x7500>
49099e: 48 89 54 24 50 mov %rdx,0x50(%rsp)
4909a3: 48 8d 15 56 f0 0b 00 lea 0xbf056(%rip),%rdx # 54fa00 <main.myValue>
4909aa: 48 89 54 24 58 mov %rdx,0x58(%rsp)
4909af: 48 8b 1d 12 3b 0c 00 mov 0xc3b12(%rip),%rbx # 5544c8 <os.Stdout>
4909b6: 48 8d 05 3b 50 04 00 lea 0x4503b(%rip),%rax # 4d59f8 <go:itab.*os.File,io.Writer>
4909bd: 48 8d 0d 68 5d 02 00 lea 0x25d68(%rip),%rcx # 4b672c <go:string.*+0x6794>
4909c4: bf 26 00 00 00 mov $0x26,%edi
4909c9: 48 8d 74 24 50 lea 0x50(%rsp),%rsi
4909ce: 41 b8 01 00 00 00 mov $0x1,%r8d
4909d4: 4d 89 c1 mov %r8,%r9
4909d7: e8 64 97 ff ff call 48a140 <fmt.Fprintf>
4909dc: 48 83 3d 24 f0 0b 00 cmpq $0x0,0xbf024(%rip) # 54fa08 <main.myValue+0x8>
4909e3: 00
4909e4: 76 4b jbe 490a31 <main.main+0x1b1>
4909e6: 48 8b 15 13 f0 0b 00 mov 0xbf013(%rip),%rdx # 54fa00 <main.myValue>
4909ed: 4c 8d 15 4c 65 00 00 lea 0x654c(%rip),%r10 # 496f40 <type:*+0x5f40>
4909f4: 4c 89 54 24 40 mov %r10,0x40(%rsp)
4909f9: 48 89 54 24 48 mov %rdx,0x48(%rsp)
4909fe: 48 8b 1d c3 3a 0c 00 mov 0xc3ac3(%rip),%rbx # 5544c8 <os.Stdout>
490a05: 48 8d 05 ec 4f 04 00 lea 0x44fec(%rip),%rax # 4d59f8 <go:itab.*os.File,io.Writer>
490a0c: 48 8d 0d 32 41 02 00 lea 0x24132(%rip),%rcx # 4b4b45 <go:string.*+0x4bad>
490a13: bf 1e 00 00 00 mov $0x1e,%edi
490a18: 48 8d 74 24 40 lea 0x40(%rsp),%rsi
490a1d: 41 b8 01 00 00 00 mov $0x1,%r8d
490a23: 4d 89 c1 mov %r8,%r9
490a26: e8 15 97 ff ff call 48a140 <fmt.Fprintf>
490a2b: 48 83 ec 80 sub $0xffffffffffffff80,%rsp
490a2f: 5d pop %rbp
490a30: c3 ret
490a31: 31 c0 xor %eax,%eax
490a33: 48 89 c1 mov %rax,%rcx
490a36: e8 c5 b2 fd ff call 46bd00 <runtime.panicIndex>
490a3b: 31 c0 xor %eax,%eax
490a3d: 48 89 c1 mov %rax,%rcx
490a40: e8 bb b2 fd ff call 46bd00 <runtime.panicIndex>
490a45: 90 nop
490a46: e8 95 91 fd ff call 469be0 <runtime.morestack_noctxt.abi0>
490a4b: e9 30 fe ff ff jmp 490880 <main.main>
$ go run main.go
Slice struct address: 0x54fa00
Data address before append: 0x54a420
Slice struct address after append: 0x54fa00
Data address after append: 0xc00008c000
おまけ
下記資料からもわかるように、スライスは、スライス構造体であり、そのスライス構造体のフィールドの一つが、スライス要素のポインタを示すのである。
// []byte のイメージ。Go のコンパイラが実際にこういう構造体を管理しているわけではない
type sliceHeader struct {
Length int
Capacity int
ZerothElement *byte
}
参考
RIP addressing is always relative to RIP (64bit Instruction Pointer) register. So it can be use for global variables only. The 0 offset is equal to address of the following instruction after the RIP-addressed instruction. For example: