Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Goのメモリ管理 / Memory management in Go

Goのメモリ管理 / Memory management in Go

ymotongpoo

June 02, 2023
Tweet

More Decks by ymotongpoo

Other Decks in Programming

Transcript

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

    View Slide

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

    View Slide

  3. メモリ関連セッション

    View Slide

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

    View Slide

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

    View Slide

  6. View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  11. プログラム
    コード
    初期化データ
    .bss
    ヒープ
    スタック
    スタック
    スタック
    OSとの対応
    共有ライブラリ
    mheap
    arena

    mcentral …
    8MB
    2KB
    プロセス
    Goランタイム

    View Slide

  12. プログラム
    コード
    初期化データ
    .bss
    ヒープ
    スタック
    スタック
    スタック
    OSとの対応
    共有ライブラリ
    mheap
    arena

    mcentral …
    8MB
    2KB
    プロセス
    Goランタイム
    ほぼヒープが問題

    View Slide

  13. mheap
    P1 (論理プロセッサー)
    Go内部のメモリ管理概要
    mcache

    arena
    mcentral mcentral mcentral
    ……


    span (non empty)
    span (empty)

    stack
    P2
    stack
    Pn
    stack
    mcache

    mspan

    mspan
    mspan

    View Slide

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

    View Slide

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

    View Slide

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



    mspan

    mspan
    mspan

    View Slide

  17. 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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  21. 問題: ヒープ 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 }

    View Slide

  22. 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
    最適化によりインライン展開している
    スタックにあります

    View Slide

  23. 例: ポインターを返してみる
    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 }
    ポインター型に変えてみる

    View Slide

  24. 例: ポインターを返してみる
    $ 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
    ※この例では実際にはインラインされているので影響はない
    関数の戻り値はヒープ
    に渡されます
    インライン展開されたもの
    はスタックにあります

    View Slide

  25. プリミティブ型のサイズ
    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)

    View Slide

  26. プリミティブ型のサイズ
    配列
    スライス (slice)
    len
    count
    buckets

    unsafe.Pointer
    int
    int
    マップ (map)
    []bmap
    tophash
    keys
    values

    bmap
    hmap

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  32. 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]

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide