Share
Embed
Transcript
37 slides
-
Confidential & Proprietary Goのメモリ管理 Go Conference 2023 Track B 16:00-16:20
June 2nd, 2023 山口能迪 (@ymotongpoo) bit.ly/20230602-gocon -
TLB MMU CPU DRAM プロセス (仮想メモリ) A A B B
ディスク B C C 0x1000 0x8000 0x2000 0x7000 メモリマップトファイル ページフォルト 0x3000 スワップ ページ (4KB) ページ テーブル -
0x0 0xFFFFFFFFFFFFFFFF プログラムコード 初期化データ .bss ヒープ スタック スタック スタック 仮想メモリ
NOTE: プログラムからは仮想メモリしか触れない 共有ライブラリ 8MB -
プログラム コード 初期化データ .bss ヒープ スタック スタック スタック OSとの対応 共有ライブラリ
mheap arena … mcentral … 8MB 2KB プロセス Goランタイム -
プログラム コード 初期化データ .bss ヒープ スタック スタック スタック OSとの対応 共有ライブラリ
mheap arena … mcentral … 8MB 2KB プロセス Goランタイム ほぼヒープが問題 -
mheap P1 (論理プロセッサー) Go内部のメモリ管理概要 mcache … arena mcentral mcentral mcentral
…… … … span (non empty) span (empty) … stack P2 stack Pn stack mcache … mspan … mspan mspan -
arena? Arena? arena 8KB …… …………… × 8000 64MB このarenaは
Go 1.21 に⼊るArenaとは別 -
mspan と mcentral mspan (non empty) mspan (empty) • mspan
は事前にさまざまなサイズクラスでアロケートした オブジェクト保存⽤のページを管理している • サイズクラスは 8B 〜 32KB のサイズのオブジェクトを合計 で32KBのサイズになる個数で保持している arena mcentral mcentral mcentral …… … … … mspan … mspan mspan -
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 -
32KB以上のオブジェクトは? • arenaの中に直接 mspan を作ってそこにオブジェクトをアロ ケートする • mcache はこの mspan
を参照する mheap arena mcentral …… mcentral mspan 10MB -
スタックとヒープ スタック • レキシカルスコープ内 ◦ ローカル変数 ◦ 関数の引数と戻り値 • ポインターでない
• LIFO ヒープ • レキシカルスコープ外 ◦ グローバル変数 ◦ 巨⼤なデータ • ポインター • ライフサイクルが予測不能 c.f. エスケープ解析 -
スタックとヒープの実⽤上の特徴 スタック • 割当と解放が速い ◦ コンパイル時に決まる • ⼀時的なもの • サイズが⼩さい
ヒープ • 割当と解放が遅い ◦ GCに頼らないといけない • ライフサイクルが予測不能 • サイズが⼤きい スタックで良いならスタックを使うようにする -
問題: ヒープ 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 } -
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 最適化によりインライン展開している スタックにあります -
例: ポインターを返してみる 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 } ポインター型に変えてみる -
例: ポインターを返してみる $ 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 ※この例では実際にはインラインされているので影響はない 関数の戻り値はヒープ に渡されます インライン展開されたもの はスタックにあります -
プリミティブ型のサイズ 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) -
プリミティブ型のサイズ 配列 スライス (slice) len count buckets … unsafe.Pointer int
int マップ (map) []bmap tophash keys values … bmap hmap -
コピーコスト type Rectangle struct { Width int // 8 byte
Height int // 8 byte } var r *Rectangle // 8 byte https://go.dev/play/p/gSisu8NMYUH 16 byte データのサイズと実際にメモリに確保される場所とのトレードオフ -
構造体 type Rectangle struct { Width uint32 Height uint16 }
https://go.dev/play/p/0PrgLF9ChCK 6 byte? -
メモリアラインメント type Rectangle struct { Width uint32 Height uint16 }
https://go.dev/play/p/RLBp2N_4tEo 6 byte? 8 byte パディング ランタイムがメモリにアクセスする単位がある -
type Rectangle struct { Width uint32 Height uint16 Init bool
} メモリアラインメント type Rectangle struct { Init bool Width uint32 Height uint16 } https://go.dev/play/p/qkQeVPC3cjA -
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] -
ガベージコレクションのタイミング ⼤きく分けて3つ(1.19以降) 1. ヒープサイズが直近のGCサイクルの終了時のサイズの $GOGC %分拡 張されたとき(デフォルトは100%) 2. ヒープサイズが $GOMEMLIMIT
に達したらGoランタイムにGCを⾛ら せるように促せる(デフォルトは math.IntMax64 ) 3. ⾃分で runtime.GC() を実⾏する -
ガベージコレクションのあと GCによって解放されたメモリはどうなるのか 1. 解放されたメモリは空きバケットとしてGoランタイムが保持する ◦ OSにはすぐには返さない 2. 空きバケットの数が増えるとGoランタイムが madvise システムコールを読
んでOSにメモリを解放可能と知らせる ◦ ref. MADV_DONTNEED