Slide 1

Slide 1 text

Confidential & Proprietary Goのメモリ管理 Go Conference 2023 Track B 16:00-16:20 June 2nd, 2023 山口能迪 (@ymotongpoo) bit.ly/20230602-gocon

Slide 2

Slide 2 text

⼭⼝ 能迪(やまぐちよしふみ) デベロッパーリレーションズエンジニア @ Google オブザーバビリティ、SRE、Go ⾃⼰紹介 @ymotongpoo @ymotongpoo

Slide 3

Slide 3 text

メモリ関連セッション

Slide 4

Slide 4 text

最初に伝えたいこと メモリ管理がすべて

Slide 5

Slide 5 text

最初に伝えたいこと Goのコンパイラーとランタイムはたいていの人類より賢いので、むやみ にメモリ管理を改善しようとすると失敗します それでも改善する必要があるなら ● 「推測するな、計測せよ」 ● Goのメモリ管理を理解しよう 今⽇はGoのメモリ管理を⼤まかに説明します

Slide 6

Slide 6 text

No content

Slide 7

Slide 7 text

想定環境 OS: Linux CPU: x86-64 or arm64 cgo 不使用

Slide 8

Slide 8 text

TLB MMU CPU DRAM プロセス (仮想メモリ) A A B B ディスク B C C 0x1000 0x8000 0x2000 0x7000 メモリマップトファイル ページフォルト 0x3000 スワップ ページ (4KB) ページ テーブル

Slide 9

Slide 9 text

s := make([]byte, 500 * 1024 * 1024) 考えてください この500MBの[]byteはどう確保されているのか

Slide 10

Slide 10 text

0x0 0xFFFFFFFFFFFFFFFF プログラムコード 初期化データ .bss ヒープ スタック スタック スタック 仮想メモリ NOTE: プログラムからは仮想メモリしか触れない 共有ライブラリ 8MB

Slide 11

Slide 11 text

プログラム コード 初期化データ .bss ヒープ スタック スタック スタック OSとの対応 共有ライブラリ mheap arena … mcentral … 8MB 2KB プロセス Goランタイム

Slide 12

Slide 12 text

プログラム コード 初期化データ .bss ヒープ スタック スタック スタック OSとの対応 共有ライブラリ mheap arena … mcentral … 8MB 2KB プロセス Goランタイム ほぼヒープが問題

Slide 13

Slide 13 text

mheap P1 (論理プロセッサー) Go内部のメモリ管理概要 mcache … arena mcentral mcentral mcentral …… … … span (non empty) span (empty) … stack P2 stack Pn stack mcache … mspan … mspan mspan

Slide 14

Slide 14 text

c.f. Goランタイム https://zenn.dev/hsaki/books/golang-concurrency/viewer/gointernal image by @saki_engineer

Slide 15

Slide 15 text

arena? Arena? arena 8KB …… …………… × 8000 64MB このarenaは Go 1.21 に⼊るArenaとは別

Slide 16

Slide 16 text

mspan と mcentral mspan (non empty) mspan (empty) ● mspan は事前にさまざまなサイズクラスでアロケートした オブジェクト保存⽤のページを管理している ● サイズクラスは 8B 〜 32KB のサイズのオブジェクトを合計 で32KBのサイズになる個数で保持している arena mcentral mcentral mcentral …… … … … mspan … mspan mspan

Slide 17

Slide 17 text

src/runtime/sizeclasses.go // class bytes/obj bytes/span objects tail waste max waste min align // 1 8 8192 1024 0 87.50% 8 // 2 16 8192 512 0 43.75% 16 // 3 24 8192 341 8 29.24% 8 // 4 32 8192 256 0 21.88% 32 // 5 48 8192 170 32 31.52% 16 // 6 64 8192 128 0 23.44% 64 // 7 80 8192 102 32 19.07% 16 // 8 96 8192 85 32 15.95% 32 // 9 112 8192 73 16 13.56% 16 // 10 128 8192 64 0 11.72% 128 // 11 144 8192 56 128 11.82% 16 // 12 160 8192 51 32 9.73% 32 // 13 176 8192 46 96 9.59% 16 // 14 192 8192 42 128 9.25% 64 // 15 208 8192 39 80 8.12% 16 // 16 224 8192 36 128 8.15% 32 // 17 240 8192 34 32 6.62% 16 // 18 256 8192 32 0 5.86% 256 // 19 288 8192 28 128 12.16% 32 // 20 320 8192 25 192 11.80% 64 // 21 352 8192 23 96 9.88% 32 // 22 384 8192 21 128 9.51% 128 // 23 416 8192 19 288 10.71% 32

Slide 18

Slide 18 text

32KB以上のオブジェクトは? ● arenaの中に直接 mspan を作ってそこにオブジェクトをアロ ケートする ● mcache はこの mspan を参照する mheap arena mcentral …… mcentral mspan 10MB

Slide 19

Slide 19 text

スタックとヒープ スタック ● レキシカルスコープ内 ○ ローカル変数 ○ 関数の引数と戻り値 ● ポインターでない ● LIFO ヒープ ● レキシカルスコープ外 ○ グローバル変数 ○ 巨⼤なデータ ● ポインター ● ライフサイクルが予測不能 c.f. エスケープ解析

Slide 20

Slide 20 text

スタックとヒープの実⽤上の特徴 スタック ● 割当と解放が速い ○ コンパイル時に決まる ● ⼀時的なもの ● サイズが⼩さい ヒープ ● 割当と解放が遅い ○ GCに頼らないといけない ● ライフサイクルが予測不能 ● サイズが⼤きい スタックで良いならスタックを使うようにする

Slide 21

Slide 21 text

問題: ヒープ or スタック? 5 func main() { 6 for i := 0; i < 100; i++ { 7 s := NewRectangle(i, 2*i) 8 fmt.Println(s.Area()) 9 } 10 } … 17 func NewRectangle(w, h int) Rectangle { 18 return Rectangle{ 19 Width: w, 20 Height: h, 21 } 22 } 23 24 func (r *Rectangle) Area() int { 25 return r.Width * r.Height 26 }

Slide 22

Slide 22 text

go build gcflags -m $ go build -gcflags -m main.go # command-line-arguments ./main.go:17:6: can inline NewRectangle ./main.go:24:6: can inline (*Rectangle).Area ./main.go:7:20: inlining call to NewRectangle ./main.go:8:21: inlining call to (*Rectangle).Area ./main.go:8:14: inlining call to fmt.Println ./main.go:8:14: ... argument does not escape ./main.go:8:21: ~R0 escapes to heap ./main.go:24:7: r does not escape 最適化によりインライン展開している スタックにあります

Slide 23

Slide 23 text

例: ポインターを返してみる 5 func main() { 6 for i := 0; i < 100; i++ { 7 s := NewRectangle(i, 2*i) 8 fmt.Println(s.Area()) 9 } 10 } … 17 func NewRectangle(w, h int) *Rectangle { 18 return &Rectangle{ 19 Width: w, 20 Height: h, 21 } 22 } 23 24 func (r *Rectangle) Area() int { 25 return r.Width * r.Height 26 } ポインター型に変えてみる

Slide 24

Slide 24 text

例: ポインターを返してみる $ go build -gcflags "-m" main.go # command-line-arguments ./main.go:17:6: can inline NewRectangle ./main.go:24:6: can inline (*Rectangle).Area ./main.go:7:20: inlining call to NewRectangle ./main.go:8:21: inlining call to (*Rectangle).Area ./main.go:8:14: inlining call to fmt.Println ./main.go:7:20: &Rectangle{...} does not escape ./main.go:8:14: ... argument does not escape ./main.go:8:21: ~R0 escapes to heap ./main.go:18:9: &Rectangle{...} escapes to heap ./main.go:24:7: r does not escape ※この例では実際にはインラインされているので影響はない 関数の戻り値はヒープ に渡されます インライン展開されたもの はスタックにあります

Slide 25

Slide 25 text

プリミティブ型のサイズ bool 1 byte int8 1 byte int16 2 byte int32 4 byte int64 8 byte uint8 1 byte uint16 2 byte uint32 4 byte uint64 8 byte float32 4 byte float64 8 byte complex64 8 byte complex128 16 byte byte (=uint8) 1 byte rune (=int32) 4 byte uintptr 8 byte str len int unsafe.Pointer ⽂字列 (string)

Slide 26

Slide 26 text

プリミティブ型のサイズ 配列 スライス (slice) len count buckets … unsafe.Pointer int int マップ (map) []bmap tophash keys values … bmap hmap

Slide 27

Slide 27 text

復習: なぜポインターを使わない⽅が良いか https://qiita.com/ruiu/items/e60aa707e16f8f6dccd8

Slide 28

Slide 28 text

コピーコスト type Rectangle struct { Width int // 8 byte Height int // 8 byte } var r *Rectangle // 8 byte https://go.dev/play/p/gSisu8NMYUH 16 byte データのサイズと実際にメモリに確保される場所とのトレードオフ

Slide 29

Slide 29 text

構造体 type Rectangle struct { Width uint32 Height uint16 } https://go.dev/play/p/0PrgLF9ChCK 6 byte?

Slide 30

Slide 30 text

メモリアラインメント type Rectangle struct { Width uint32 Height uint16 } https://go.dev/play/p/RLBp2N_4tEo 6 byte? 8 byte パディング ランタイムがメモリにアクセスする単位がある

Slide 31

Slide 31 text

type Rectangle struct { Width uint32 Height uint16 Init bool } メモリアラインメント type Rectangle struct { Init bool Width uint32 Height uint16 } https://go.dev/play/p/qkQeVPC3cjA

Slide 32

Slide 32 text

go tool objdump $ go tool objdump -s main.main main TEXT main.main(SB) /Users/yoshifumi/personal/tmp/main.go main.go:20 0x10008ce00 f9400b90 MOVD 16(R28), R16 main.go:20 0x10008ce04 d10143f1 SUB $80, RSP, R17 main.go:20 0x10008ce08 eb10023f CMP R16, R17 main.go:20 0x10008ce0c 54000ce9 BLS 103(PC) main.go:20 0x10008ce10 f8130ffe MOVD.W R30, -208(RSP) main.go:20 0x10008ce14 f81f83fd MOVD R29, -8(RSP) main.go:20 0x10008ce18 d10023fd SUB $8, RSP, R29 main.go:24 0x10008ce1c a905ffff STP (ZR, ZR), 88(RSP) main.go:24 0x10008ce20 f0000147 ADRP 176128(PC), R7 main.go:24 0x10008ce24 913f00e7 ADD $4032, R7, R7 main.go:24 0x10008ce28 f9002fe7 MOVD R7, 88(RSP) main.go:24 0x10008ce2c 90000108 ADRP 131072(PC), R8 main.go:24 0x10008ce30 910c0108 ADD $768, R8, R8 main.go:24 0x10008ce34 f90033e8 MOVD R8, 96(RSP) https://pkg.go.dev/cmd/internal/obj/[email protected]

Slide 33

Slide 33 text

Goのメモリアロケーター Goランタイム内にあるメモリ確保に関するコード ● TCMallocベースのアロケーター ● ページサイズは8KB(OSの仮想メモリのページサイズとは別) ● スパンという単位でメモリブロックを割り当てて管理している

Slide 34

Slide 34 text

ガベージコレクター Goはガベージコレクターがヒープ内の不要な変数が確保したメモリを解 放してくれる ● 頻度が低いとOSからのメモリ確保の頻度が上がる ● 頻度が高いとプログラムが止められすぎて遅くなる よいタイミングで解放しメモリをいい具合に再利⽤したい

Slide 35

Slide 35 text

ガベージコレクションのタイミング ⼤きく分けて3つ(1.19以降) 1. ヒープサイズが直近のGCサイクルの終了時のサイズの $GOGC %分拡 張されたとき(デフォルトは100%) 2. ヒープサイズが $GOMEMLIMIT に達したらGoランタイムにGCを⾛ら せるように促せる(デフォルトは math.IntMax64 ) 3. ⾃分で runtime.GC() を実⾏する

Slide 36

Slide 36 text

ガベージコレクションのあと GCによって解放されたメモリはどうなるのか 1. 解放されたメモリは空きバケットとしてGoランタイムが保持する ○ OSにはすぐには返さない 2. 空きバケットの数が増えるとGoランタイムが madvise システムコールを読 んでOSにメモリを解放可能と知らせる ○ ref. MADV_DONTNEED

Slide 37

Slide 37 text

今⽇のまとめ Goのコンパイラーとランタイムはたいていの人類より賢いので、むやみ にメモリ管理を改善しようとすると失敗します それでも改善する必要があるなら ● 「推測するな、計測せよ」 ● Goのメモリ管理を理解しよう 必要になったときに今日知ったことを思い出してください