Slide 1

Slide 1 text

Takuto Nagami @logica0419 歴史から学ぶ Goのメモリ管理基礎

Slide 2

Slide 2 text

メモリ管理を知らない / 理解に苦しんでいる人?

Slide 3

Slide 3 text

【通説】 メモリ管理は難しい 僕も結構苦しんでいました

Slide 4

Slide 4 text

メモリ管理ってなんだ? ● メモリ管理は広い概念 ○ マシン、OS、言語など領域によって意味が異なる ● プログラミング言語の領域で言うと… ○ どのようにデータ(変数)が メモリ内で管理されるかを 決める戦略

Slide 5

Slide 5 text

なぜメモリ管理を気にするのか? ● アプリケーションのパフォーマンス向上 ● Go自身のメンテナンスにはメモリ管理の知識が必要 ● 深堀りすると面白い! 中でも、特にパフォーマンス 向上が大きなモチベーション

Slide 6

Slide 6 text

メモリ周りのパフォーマンス向上例 ● たった一つのメモリ割り当ての変更だけで、CPU使用率 57%削減、メモリ使用量99%削減した例も!

Slide 7

Slide 7 text

メモリ管理のセッションは難しい!

Slide 8

Slide 8 text

メモリ管理のセッションは難しい!

Slide 9

Slide 9 text

メモリ管理のセッションは難しい!

Slide 10

Slide 10 text

メモリ管理のセッションは難しい!

Slide 11

Slide 11 text

メモリ管理のセッションは難しい!

Slide 12

Slide 12 text

メモリ管理のセッションは難しい! 正直わからん! ではなぜ難しいんだろう?

Slide 13

Slide 13 text

学習プロセス: Why, What and How アプリケーションに新機能を追加するとき

Slide 14

Slide 14 text

学習プロセス: Why, What and How アプリケーションに新機能を追加するとき Why なぜ必要 なのか

Slide 15

Slide 15 text

学習プロセス: Why, What and How Why なぜ必要 なのか What 何を追加 するのか アプリケーションに新機能を追加するとき

Slide 16

Slide 16 text

学習プロセス: Why, What and How Why なぜ必要 なのか What 何を追加 するのか How どのように 実装するのか アプリケーションに新機能を追加するとき

Slide 17

Slide 17 text

アプリケーションに新機能を追加するとき 学習プロセス: Why, What and How Why なぜ必要 なのか What 何を追加 するのか How どのように 実装するのか 既存セッションは メモリ管理がどのように 実装されているかに フォーカスしがち

Slide 18

Slide 18 text

学習プロセス: Why, What and How Why なぜ必要 なのか What 何を追加 するのか How どのように 実装するのか アプリケーションに新機能を追加するとき

Slide 19

Slide 19 text

今日お話するのは メモリ管理の 「なぜ」と「何」です! それぞれの戦略がなぜ生まれ、何をするのか学びましょう

Slide 20

Slide 20 text

前提: Goが提供するコンポーネント ● コンパイラ ○ ソースコードを実行ファイル (バイナリ) にする ● ランタイム ○ 実行中、書いたソースコードと一緒に動く

Slide 21

Slide 21 text

前提: Goが提供するコンポーネント ソース コード ● コンパイラ ○ ソースコードを実行ファイル (バイナリ) にする ● ランタイム ○ 実行中、書いたソースコードと一緒に動く go build

Slide 22

Slide 22 text

前提: Goが提供するコンポーネント ソース コード 実行 ファイル go build ● コンパイラ ○ ソースコードを実行ファイル (バイナリ) にする ● ランタイム ○ 実行中、書いたソースコードと一緒に動く

Slide 23

Slide 23 text

前提: Goが提供するコンポーネント コンパイラ の仕事 ● コンパイラ ○ ソースコードを実行ファイル (バイナリ) にする ● ランタイム ○ 実行中、書いたソースコードと一緒に動く ソース コード 実行 ファイル go build

Slide 24

Slide 24 text

前提: Goが提供するコンポーネント (バイナリを実行) ● コンパイラ ○ ソースコードを実行ファイル (バイナリ) にする ● ランタイム ○ 実行中、書いたソースコードと一緒に動く コンパイラ の仕事 ソース コード 実行 ファイル go build

Slide 25

Slide 25 text

前提: Goが提供するコンポーネント ランタイム の仕事 ● コンパイラ ○ ソースコードを実行ファイル (バイナリ) にする ● ランタイム ○ 実行中、書いたソースコードと一緒に動く コンパイラ の仕事 ソース コード 実行 ファイル go build (バイナリを実行)

Slide 26

Slide 26 text

前提: Goが提供するコンポーネント go run ● コンパイラ ○ ソースコードを実行ファイル (バイナリ) にする ● ランタイム ○ 実行中、書いたソースコードと一緒に動く コンパイラ の仕事 ソース コード 実行 ファイル go build (バイナリを実行) ランタイム の仕事

Slide 27

Slide 27 text

メモリとは何か / 古のメモリ管理

Slide 28

Slide 28 text

メモリとは何か メモリ = RAM (Random Access Memory)

Slide 29

Slide 29 text

メモリとは何か メモリ = RAM (Random Access Memory) ↑ ストレージ (HDD/SSD)

Slide 30

Slide 30 text

メモリとは何か メモリ = RAM (Random Access Memory) ↑ ストレージ (HDD/SSD)

Slide 31

Slide 31 text

メモリを図解する . . . ● メモリはアプリケーションから Key-Valueストアとして使用 ○ Key: メモリアドレス ○ Value: 各アドレスに1バイト ※ OSはRAMを分割し、それぞれの プロセスに「仮想メモリ」として割り当てる (詳しくは「仮想メモリ」で検索) 0x0000 0x0001 0x0002 0x0003

Slide 32

Slide 32 text

アセンブリにおけるメモリの読み書き . . . . . . 0x0000 0x0001 0x0002 0x0003 {address}

Slide 33

Slide 33 text

アセンブリにおけるメモリの読み書き . . . . . . 書き込み 0x0000 0x0001 0x0002 0x0003 {address} 1 1

Slide 34

Slide 34 text

アセンブリにおけるメモリの読み書き . . . . . . 読み込み 0x0000 0x0001 0x0002 0x0003 {address} 1 1 %eax (CPUレジスタ)

Slide 35

Slide 35 text

【つらみ】直感的だが、かなりつらい ● プログラマが、使うメモリアドレスを全て覚えておく 必要がある 🤯

Slide 36

Slide 36 text

変数の誕生 / スタックとヒープ

Slide 37

Slide 37 text

高級プログラミング言語の誕生 ● 1957: Fortranがリリース ○ 「最初」の高級プログラミング言語 ● COBOL, BASIC, PASCAL, C... からGoに至るまで、様々 な言語が開発されてきた

Slide 38

Slide 38 text

データの抽象化 - 変数 . . . (変数)

Slide 39

Slide 39 text

データの抽象化 - 変数 . . . . . . . . . . . {a addr} 12 a (変数) 12

Slide 40

Slide 40 text

データの抽象化 - 変数 a (変数) 12 {a addr} {b addr} b 34 . . . . . . . . . 12 34

Slide 41

Slide 41 text

データの抽象化 - 変数 a (変数) 12 b 34 {a addr} {b addr} . . . . . . . . . 12 34 プログラマは 変数の名前だけ 認識すれば良い

Slide 42

Slide 42 text

プログラマーはアドレスを 覚える必要ナシ🙌 代わりに言語側に 管理責任が生まれる😨

Slide 43

Slide 43 text

データを配置するためのルールが欲しい {a addr} ? {b addr} ? . . . . . . . . . 12 34

Slide 44

Slide 44 text

関数呼び出し

Slide 45

Slide 45 text

main() 関数呼び出し

Slide 46

Slide 46 text

呼び出し main() 関数呼び出し

Slide 47

Slide 47 text

呼び出し main() a() 関数呼び出し

Slide 48

Slide 48 text

関数呼び出し 呼び出し main() a() 呼び出し

Slide 49

Slide 49 text

関数呼び出し 呼び出し main() a() 呼び出し b()

Slide 50

Slide 50 text

関数呼び出し 呼び出し main() a() return

Slide 51

Slide 51 text

関数呼び出し 呼び出し main() a()

Slide 52

Slide 52 text

関数呼び出し return main()

Slide 53

Slide 53 text

関数呼び出し main()

Slide 54

Slide 54 text

関数呼び出し プログラム終了

Slide 55

Slide 55 text

関数呼び出しのデータ構造 Last In, First Out (LIFO)...? main() a() 呼び出し main() a() return

Slide 56

Slide 56 text

関数呼び出しのデータ構造 First In, First Out (FIFO)...? main() a() 呼び出し main() a() return そう、 スタックですね!

Slide 57

Slide 57 text

. . . スタック メモリ領域

Slide 58

Slide 58 text

. . . main() stack frame スタック メモリ領域 {main() s.f. addr}

Slide 59

Slide 59 text

スタックフレーム ● メモリ内における「関数」の表現方法 ○ それぞれのフレームがスコープになる ● 構造はABIによって定められている ○ ローカル変数 ○ (一部) 引数 ○ 自分を呼び出した関数のスタックフレームアドレス ○ 別関数を呼び出した場所 などが格納される a() stack frame ● 10行目で呼び出し ● main()にreturn 5 7

Slide 60

Slide 60 text

. . . main() stack frame スタック メモリ領域 {main() s.f. addr}

Slide 61

Slide 61 text

. . . main() stack frame 5 スタック メモリ領域 {main() s.f. addr} {v addr}

Slide 62

Slide 62 text

. . . main() stack frame ● 3行目で呼び出し 5 スタック メモリ領域 {main() s.f. addr} {v addr}

Slide 63

Slide 63 text

. . . main() stack frame ● 3行目で呼び出し 5 a() stack frame スタック メモリ領域 {a() s.f. addr} {main() s.f. addr} {v addr}

Slide 64

Slide 64 text

. . . main() stack frame ● 3行目で呼び出し 5 a() stack frame ● main()にreturn スタック メモリ領域 {a() s.f. addr} {main() s.f. addr} {v addr}

Slide 65

Slide 65 text

. . . main() stack frame ● 3行目で呼び出し 5 a() stack frame ● main()にreturn 5 スタック メモリ領域 {a() s.f. addr} {arg addr} {main() s.f. addr} {v addr}

Slide 66

Slide 66 text

. . . main() stack frame ● 3行目で呼び出し 5 a() stack frame ● main()にreturn 5 arg+2= 7 スタック メモリ領域 {a() s.f. addr} {arg addr} {a addr} {main() s.f. addr} {v addr}

Slide 67

Slide 67 text

. . . main() stack frame ● 3行目で呼び出し 5 a() stack frame ● main()にreturn 5 7 スタック メモリ領域 {a() s.f. addr} {arg addr} {a addr} {main() s.f. addr} {v addr}

Slide 68

Slide 68 text

. . . main() stack frame ● 3行目で呼び出し 5 a() stack frame ● main()にreturn 5 7 スタック メモリ領域 {a() s.f. addr} {arg addr} {a addr} {main() s.f. addr} {v addr}

Slide 69

Slide 69 text

. . . main() stack frame ● 3行目で呼び出し 5 a() stack frame ● 10行目で呼び出し ● main()にreturn 5 7 スタック メモリ領域 {a() s.f. addr} {arg addr} {a addr} {main() s.f. addr} {v addr}

Slide 70

Slide 70 text

. . スタック メモリ領域 main() stack frame ● 3行目で呼び出し 5 a() stack frame ● 10行目で呼び出し ● main()にreturn {b() s.f. addr} {a() s.f. addr} {arg addr} {a addr} {main() s.f. addr} {v addr} 5 7 b() stack frame

Slide 71

Slide 71 text

. . スタック メモリ領域 main() stack frame ● 3行目で呼び出し 5 a() stack frame ● 10行目で呼び出し ● main()にreturn {b() s.f. addr} {a() s.f. addr} {arg addr} {a addr} {main() s.f. addr} {v addr} 5 7 b() stack frame ● a()にreturn

Slide 72

Slide 72 text

. . スタック メモリ領域 main() stack frame ● 3行目で呼び出し 5 a() stack frame ● 10行目で呼び出し ● main()にreturn {b() s.f. addr} {arg addr} {a() s.f. addr} {arg addr} {a addr} {main() s.f. addr} {v addr} 5 7 b() stack frame ● a()にreturn 7

Slide 73

Slide 73 text

. . スタック メモリ領域 main() stack frame ● 3行目で呼び出し 5 a() stack frame ● 10行目で呼び出し ● main()にreturn {b() s.f. addr} {arg addr} {b addr} {a() s.f. addr} {arg addr} {a addr} {main() s.f. addr} {v addr} 5 7 b() stack frame ● a()にreturn 7 1

Slide 74

Slide 74 text

. . スタック メモリ領域 main() stack frame ● 3行目で呼び出し 5 a() stack frame ● 10行目で呼び出し ● main()にreturn {b() s.f. addr} {arg addr} {b addr} {a() s.f. addr} {arg addr} {a addr} {main() s.f. addr} {v addr} 5 7 b() stack frame ● a()にreturn 7 1

Slide 75

Slide 75 text

. . スタック メモリ領域 main() stack frame ● 3行目で呼び出し 5 a() stack frame ● 10行目で呼び出し ● main()にreturn {b() s.f. addr} {arg addr} {b addr} {a() s.f. addr} {arg addr} {a addr} {main() s.f. addr} {v addr} 5 7 b() stack frame ● a()にreturn 7 1 arg+b= 8 (CPUレジスタ)

Slide 76

Slide 76 text

. . スタック メモリ領域 main() stack frame ● 3行目で呼び出し 5 a() stack frame ● 10行目で呼び出し ● main()にreturn {b() s.f. addr} {arg addr} {b addr} {a() s.f. addr} {arg addr} {a addr} {main() s.f. addr} {v addr} 5 7 b() stack frame ● a()にreturn 7 1 8 (CPUレジスタ)

Slide 77

Slide 77 text

. . . スタック メモリ領域 main() stack frame ● 3行目で呼び出し 5 a() stack frame ● main()にreturn {a() s.f. addr} {arg addr} {a addr} {main() s.f. addr} {v addr} 5 7 8 (CPUレジスタ)

Slide 78

Slide 78 text

. . . スタック メモリ領域 main() stack frame ● 3行目で呼び出し 5 a() stack frame ● main()にreturn {a() s.f. addr} {arg addr} {a addr} {main() s.f. addr} {v addr} 5 7 8 (CPUレジスタ)

Slide 79

Slide 79 text

. . . スタック メモリ領域 main() stack frame 5 {main() s.f. addr} {v addr} 8 (CPUレジスタ)

Slide 80

Slide 80 text

. . . スタック メモリ領域 main() stack frame 8 {main() s.f. addr} {v addr} 8 (CPUレジスタ)

Slide 81

Slide 81 text

. . . スタック メモリ領域 main() stack frame 8 {main() s.f. addr} {v addr}

Slide 82

Slide 82 text

. . . スタック メモリ領域 プログラム終了

Slide 83

Slide 83 text

スタックは銀の弾丸ではない ● ビルド時にサイズが不定の型 (map/slice) は扱えない ○ スタック関連の処理はコンパイラの仕事 ● 引数以外の方法で関数をまたいだ変数の共有ができない

Slide 84

Slide 84 text

新しい戦略: 動的メモリ確保 (ヒープ メモリ領域) スタックの問題を解決するためのメモリ割り当て方法

Slide 85

Slide 85 text

{main() s.f. addr} ヒープ メモリ領域 . . . . . . . main() stack frame

Slide 86

Slide 86 text

ヒープ メモリ領域 . . . . . . . [] {slice_h addr} {main() s.f. addr} main() stack frame

Slide 87

Slide 87 text

ヒープ メモリ領域 . . . . . . . [] {slice_h addr} {main() s.f. addr} main() stack frame ランタイムに よって アドレスが決まる

Slide 88

Slide 88 text

ヒープ メモリ領域 . . . . . . . [] {slice_h addr} {main() s.f. addr} main() stack frame main()はsliceが どこかわからない (スタックフレームの中 しか見られない)

Slide 89

Slide 89 text

. . . . . . . [] {slice_h addr} {main() s.f. addr} {slice addr} main() stack frame ヒープ メモリ領域 {slice_h addr}

Slide 90

Slide 90 text

. . . . . . . [] {slice_h addr} {main() s.f. addr} {slice addr} main() stack frame ヒープ メモリ領域 これが ポインタ {slice_h addr}

Slide 91

Slide 91 text

. . . . . . . [] {slice_h addr} {main() s.f. addr} {slice addr} {num addr} main() stack frame ヒープ メモリ領域 5 これが ポインタ {slice_h addr}

Slide 92

Slide 92 text

ポインタは大事なので、もう少し詳しく見ていきます もう一つ例を挙げて ポインタがどのように 振る舞うか見てみよう

Slide 93

Slide 93 text

値渡し と ポインタ渡し 違いを原理からきちんと説明できますか?

Slide 94

Slide 94 text

. . . main() stack frame ● 3行目で呼び出し 5 値渡し {main() s.f. addr} {num addr}

Slide 95

Slide 95 text

. . . main() stack frame ● 3行目で呼び出し 5 値渡し f() stack frame {f() s.f. addr} {main() s.f. addr} {num addr}

Slide 96

Slide 96 text

. . . main() stack frame ● 3行目で呼び出し 5 値渡し f() stack frame 5 {f() s.f. addr} {num addr} {main() s.f. addr} {num addr}

Slide 97

Slide 97 text

{f() s.f. addr} {num addr} {main() s.f. addr} {num addr} . . . main() stack frame ● 3行目で呼び出し 5 値渡し f() stack frame 10

Slide 98

Slide 98 text

{f() s.f. addr} {num addr} {main() s.f. addr} {num addr} . . . main() stack frame ● 3行目で呼び出し 5 値渡し f() stack frame 10 main()関数の numには 影響しない

Slide 99

Slide 99 text

. . . ポインタ渡し main() stack frame ● 3行目で呼び出し 5 {main() s.f. addr} {num addr}

Slide 100

Slide 100 text

. . . ポインタ渡し main() stack frame ● 3行目で呼び出し 5 f() stack frame {f() s.f. addr} {main() s.f. addr} {num addr}

Slide 101

Slide 101 text

. . . main() stack frame ● 3行目で呼び出し f() stack frame ポインタ渡し 5 {num addr} {f() s.f. addr} {num addr} {main() s.f. addr} {num addr}

Slide 102

Slide 102 text

. . . main() stack frame ● 3行目で呼び出し f() stack frame ポインタ渡し {f() s.f. addr} {num addr} {main() s.f. addr} {num addr} 10 {num addr}

Slide 103

Slide 103 text

. . . {f() s.f. addr} {num addr} {main() s.f. addr} {num addr} main() stack frame ● 3行目で呼び出し 10 f() stack frame ポインタ渡し {num addr} main()関数の numに 影響を及ぼす

Slide 104

Slide 104 text

スタックとヒープを 組み合わせることで 汎用的なメモリ管理が 実現できる

Slide 105

Slide 105 text

古のヒープ管理 . . . . . . . {main() s.f. addr} main() stack frame C言語を例にとると ● func malloc(int size) pointer ● func free(pointer memory) (上記関数は疑似コード)

Slide 106

Slide 106 text

古のヒープ管理 C言語を例にとると ● func malloc(int size) pointer ● func free(pointer memory) (上記関数は疑似コード) . . . . . . . ~~~ {ヒープのどこか} {main() s.f. addr} main() stack frame {ヒープのどこか}

Slide 107

Slide 107 text

古のヒープ管理 . . . . . . . {main() s.f. addr} main() stack frame {ヒープのどこか} C言語を例にとると ● func malloc(int size) pointer ● func free(pointer memory) (上記関数は疑似コード)

Slide 108

Slide 108 text

【つらみ】もしもfree()を忘れたら? ● メモリ使用量が無限に増えていく ○ メモリリークと呼ばれている ● 最終的にはOOM (Out of Memory) killで強制終了

Slide 109

Slide 109 text

ヒープの自動管理 / ガベージコレクション

Slide 110

Slide 110 text

ガベージコレクション (GC) ● もう使われないヒープ内のデータを削除する ○ スタックは一切関係ない戦略 ● 基本的にランタイムの仕事

Slide 111

Slide 111 text

ヒープ領域 スタック領域 マーク & スイープ main() stack frame a() stack frame b() stack frame a b c e {a addr} d {b addr} {var1 addr} var1 {d addr}

Slide 112

Slide 112 text

ヒープ領域 スタック領域 マーク段階 main() stack frame a() stack frame b() stack frame a b c e {a addr} d {b addr} {var1 addr} var1 {d addr}

Slide 113

Slide 113 text

ヒープ領域 スタック領域 マーク段階 main() stack frame a() stack frame b() stack frame a b c e {a addr} d {b addr} {var1 addr} var1 {d addr}

Slide 114

Slide 114 text

ヒープ領域 スタック領域 マーク段階 main() stack frame a() stack frame b() stack frame a b c e {a addr} d {b addr} {var1 addr} var1 {d addr}

Slide 115

Slide 115 text

ヒープ領域 スタック領域 マーク段階 main() stack frame a() stack frame b() stack frame a b c e {a addr} d {b addr} {var1 addr} var1 {d addr}

Slide 116

Slide 116 text

ヒープ領域 スタック領域 マーク段階 main() stack frame a() stack frame b() stack frame a b c e {a addr} d {b addr} {var1 addr} var1 {d addr}

Slide 117

Slide 117 text

ヒープ領域 スタック領域 マーク段階 main() stack frame a() stack frame b() stack frame a b c e {a addr} d {b addr} {var1 addr} var1 {d addr}

Slide 118

Slide 118 text

ヒープ領域 スタック領域 マーク段階 main() stack frame a() stack frame b() stack frame a b c e {a addr} d {b addr} {var1 addr} var1 {d addr}

Slide 119

Slide 119 text

ヒープ領域 スタック領域 main() stack frame a() stack frame b() stack frame a b e {a addr} d {b addr} {var1 addr} var1 {d addr} スイープ段階 c

Slide 120

Slide 120 text

ヒープ領域 スタック領域 main() stack frame a() stack frame b() stack frame a b e {a addr} d {b addr} {var1 addr} var1 {d addr} スイープ段階

Slide 121

Slide 121 text

シンプルだが、最適化は結構難しい 例えばGo言語では、以下のような最適化が行われている ● 我々が書いたコードと並列で動かす ○ Tri-colorマーキングなどを使う ● GCのトリガーの調整 ○ GOGC ○ GOMEMLIMIT ● Go 1.25で実験的マークアルゴリズム Green Tea GCが導入

Slide 122

Slide 122 text

シンプルだが、最適化は結構難しい ● 複雑なアルゴリズムを使った最適化は可能 ● Goは↑をしていない ○ シンプルの哲学が、仕様だけでなくランタイム実装 にも根付いている

Slide 123

Slide 123 text

【つらみ】GCは重い ● ヒープに多くのデータを置くと、GCが重くなる ○ 特にマーク段階が重い

Slide 124

Slide 124 text

多くのGC言語は型でヒープを決める ● 昔のJava: オブジェクトは全てヒープ ● Python: オブジェクトは全てヒープ ● JS: non-primitive型の値は全てヒープ

Slide 125

Slide 125 text

多くのGC言語は型でヒープを決める ● 昔のJava: オブジェクトは全てヒープ ● Python: オブジェクトは全てヒープ ● JS: non-primitive型の値は全てヒープ Non-primitiveな型は ヒープに行きがち

Slide 126

Slide 126 text

ヒープエスケープ 【Go特有】

Slide 127

Slide 127 text

ヒープエスケープ ● できる限り多くのデータをスタックに置く ● 本当に必要なデータのみヒープに「逃げる」

Slide 128

Slide 128 text

どの変数がヒープに逃げるかわかる?

Slide 129

Slide 129 text

by Jacob Walker どの変数がヒープに逃げるかわかる?

Slide 130

Slide 130 text

典型的なパターンは存在するけど… ● 返り値がポインタの場合 ● インターフェース型として扱われる場合 ● map, channel, string, 大抵のslice

Slide 131

Slide 131 text

コンパイラとランタイムのみぞ知る ● 多くのヒープエスケープはコンパイラが決める ● 残りのヒープエスケープはランタイムが決める

Slide 132

Slide 132 text

エスケープ解析 ● コンパイラは変数を解析してヒープに行くものを決める ○ この解析ログを出力させることができる go {run/build/test} -gcflags="-m" **.go

Slide 133

Slide 133 text

ベンチマーク ● ランタイム担当分まで含め、実際にヒープに割り当て られたかはベンチマークで確かめる go test -bench {package} -benchmem

Slide 134

Slide 134 text

例 (エスケープあり)

Slide 135

Slide 135 text

例 (エスケープあり)

Slide 136

Slide 136 text

例 (エスケープあり)

Slide 137

Slide 137 text

例 (エスケープなし)

Slide 138

Slide 138 text

例 (エスケープなし)

Slide 139

Slide 139 text

例 (エスケープなし)

Slide 140

Slide 140 text

例 (エスケープなしに見えてもある場合)

Slide 141

Slide 141 text

例 (エスケープなしに見えてもある場合)

Slide 142

Slide 142 text

例 (エスケープなしに見えてもある場合)

Slide 143

Slide 143 text

ヒープ内でのオブジェクト管理 int (8 byte) ● 色んなデータ (オブジェクト) がヒープに入る ○ バラバラに入れていたら管理が面倒 struct (可変サイズ) ポインタ (8 byte)

Slide 144

Slide 144 text

… … ヒープ内でのオブジェクト管理 ● 同サイズのオブジェクトをスパンでまとめる int (8 byte) struct (8 byte) struct struct (32 byte) struct struct int int スパン1 (8 byte用) スパン2 (8 byte用) スパン3 (32 byte用) int int

Slide 145

Slide 145 text

ヒープ内でのオブジェクト管理 ● スパン (mspan) はもっと大きな単位でまとめられる ○ mcentral ■ 同じサイズのスパンをグループ化 ○ arena ■ mheapが1回で引き出してくるメモリ量 ■ mcentralを複数格納 ○ mheap ■ ヒープ本体

Slide 146

Slide 146 text

ゼロアロケーション ● ヒープ割り当て (エスケープ) が無いという意味 ○ スタックにはデータを入れるので「文字通りゼロ」 ではない ● パフォーマンス向上のためによく使われる

Slide 147

Slide 147 text

ゼロアロケーションライブラリ zerolog: ゼロアロケーションログ

Slide 148

Slide 148 text

ゼロアロケーションライブラリ fasthttp: (一部) ゼロアロケーションhttp server

Slide 149

Slide 149 text

ゼロアロケーションライブラリ go-reflect: ゼロアロケーションリフレクション

Slide 150

Slide 150 text

ゼロアロケーションも銀の弾丸ではない ● ヒープを使わないので、関数をまたいだ データ共有は全て引数のコピー ○ 引数のサイズが大きいと逆に重くなる ● ゼロアロケーション = 正義 ではない ○ ベンチマークで効果を計測しよう main() Var foo() Arg bar() Arg

Slide 151

Slide 151 text

ヒープエスケープを理解し ゼロアロケーションを 賢く使いましょう

Slide 152

Slide 152 text

スタックオーバーフロー ● ヒープとスタックの境目を決めるのは言語 ○ 例: Linux上のC言語ならスタックは8MB . . . . . ヒープ変数 main() stack frame

Slide 153

Slide 153 text

スタックオーバーフロー ● 再帰関数などでスタックサイズの制限を超えてしまう

Slide 154

Slide 154 text

スタックオーバーフロー ● 再帰関数などでスタックサイズの制限を超えてしまう → スタックオーバーフロー!! ● !!

Slide 155

Slide 155 text

【つらみ】スタックが爆発する ● ヒープエスケープはスタックの使用量を増やす ○ スタックオーバーフローが起こりやすくなる

Slide 156

Slide 156 text

【つらみ】ヒープも爆発する ● スタック領域を大きくするといいのでは…? ● スタックが大きい = ヒープが小さい ○ ✅ スタックオーバーフローは 起こりにくくなる ○ ❌ OOM killの確率が上がる😭 ● あちらを立てればこちらが立たず状態 . . . . . Heap var main() stack frame 󰷺

Slide 157

Slide 157 text

可変サイズスタック 【Go特有】

Slide 158

Slide 158 text

Goのスタックは可変サイズ ● Goはgoroutineごとに分離したスタックを持つ ● それぞれのスタックが状況に合わせて伸び縮みする ○ 4kBで初期化される

Slide 159

Slide 159 text

ざっくりしたイメージ main goroutine main() stack frame a() stack frame a2() stack frame a3() stack frame a4() stack frame m2() stack frame b() stack frame a goroutine b goroutine

Slide 160

Slide 160 text

可変スタックの実装 ● 昔はセグメント化スタックによって実装されていた ● 今はスタックコピー (より効率が良い)

Slide 161

Slide 161 text

スタックコピー . . . main() stack frame m2() stack frame main goroutine のスタック

Slide 162

Slide 162 text

スタックコピー . . . main() stack frame m2() stack frame m3() stack frame m3()を実行する ためには サイズが足りない

Slide 163

Slide 163 text

スタックコピー . . . main() stack frame m2() stack frame 2倍のサイズ 新しい main goroutine のスタック

Slide 164

Slide 164 text

スタックコピー . . . main() stack frame m2() stack frame main() stack frame m2() stack frame

Slide 165

Slide 165 text

スタックコピー . . . main() stack frame m2() stack frame

Slide 166

Slide 166 text

スタックコピー . . . main() stack frame m2() stack frame m3() stack frame これでm3()が 実行できる

Slide 167

Slide 167 text

スタックコピー . . . main() stack frame m2() stack frame

Slide 168

Slide 168 text

スタックコピー . . . main() stack frame m2() stack frame

Slide 169

Slide 169 text

スタックが可変サイズなら スタックオーバーフローは 起きないのでは?

Slide 170

Slide 170 text

Goのスタックオーバーフロー 残念ながら実際は起こる

Slide 171

Slide 171 text

最大スタックサイズ ● 各goroutineはデフォルトで1GBまでしかスタックを 拡大できない ○ 制限を超えるとスタックオーバーフローは起こる

Slide 172

Slide 172 text

最大サイズの変更 ● runtime/debug.SetMaxStack()で上書き可能

Slide 173

Slide 173 text

goroutineごとの 可変サイズスタックが Goのスタックの特殊さ goroutineが特殊だと言われる理由の一つでもある

Slide 174

Slide 174 text

冒頭で紹介した事例の解説

Slide 175

Slide 175 text

【おさらい】今回扱う事例 https://developers.cyberagent.co.jp/blog/archives/54653/ ● たった一つのメモリ割り当ての変更だけで、CPU使用率 57%削減、メモリ使用量99%削減した例

Slide 176

Slide 176 text

異常なスパイク - 大量のエスケープ ● CPU使用率に定期的なスパイクが見られた ● 寿命の短いヒープオブジェクトが大量に生成されていた ○ GCが高頻度でトリガーされていた ● ↑ を引き起こしていたのはフィルター関数

Slide 177

Slide 177 text

改善前のフィルター関数

Slide 178

Slide 178 text

改善前のフィルター関数

Slide 179

Slide 179 text

改善前のフィルター関数

Slide 180

Slide 180 text

改善前のフィルター関数 たった一つの 割り当てが大量の GCを引き起こしていた

Slide 181

Slide 181 text

改善後のフィルター関数

Slide 182

Slide 182 text

改善後のフィルター関数 スライスを 再利用

Slide 183

Slide 183 text

ベンチマーク ● 両方の関数でベンチマークを回してみた ○ 1から100までのリストを元の配列とする ○ 偶数のみを抜き出すようフィルタリング

Slide 184

Slide 184 text

2倍の速さ! ベンチマーク ● 両方の関数でベンチマークを回してみた ○ 1から100までのリストを元の配列とする ○ 偶数のみを抜き出すようフィルタリング

Slide 185

Slide 185 text

ベンチマーク ● 両方の関数でベンチマークを回してみた ○ 1から100までのリストを元の配列とする ○ 偶数のみを抜き出すようフィルタリング たった一つの割り当て で引き起こされた ことがわかる 2倍の速さ!

Slide 186

Slide 186 text

更なるリファクタリング (記事の内容) ● イテレータを使うと、よりシンプルかつ安全に書ける

Slide 187

Slide 187 text

改善結果 ● ワークロード全体で ○ CPU使用率57%削減!! ○ メモリ使用量99%削減!!

Slide 188

Slide 188 text

まとめ

Slide 189

Slide 189 text

まとめ ● メモリ管理はプログラマーの需要を満たすために発展 してきた ● メモリ管理に関する知識は強い力となる ● 今回のセッションはあくまで出発点 ○ 他のセッションで各機能を深堀りしてみよう!

Slide 190

Slide 190 text

おまけ: 次のステップは?

Slide 191

Slide 191 text

ガベージコレクションの実装 GoのGCについて理解する @ Go Conference 2022 Spring

Slide 192

Slide 192 text

ガベージコレクションの条件とその活用 Go1.19から始めるGCのチューニング方法 @ Go Conference 2023 Online

Slide 193

Slide 193 text

エスケープ解析とその活用 Escape Analysis in Go: Understanding and Optimizing Memory Allocation @ Go Conference 2023 Online

Slide 194

Slide 194 text

Goのメモリ割り当てとOSの関係 Goのメモリ管理 @ Go Conference 2023 Online ⚠発展的

Slide 195

Slide 195 text

並列処理におけるメモリ管理 よくわかるThe Go Memory Model - 行間を 図解で埋め尽くす @ Go Conference 2023 Online

Slide 196

Slide 196 text

ありがとう ございました カンファレンス、楽しみましょう!