歴史から学ぶ、Goのメモリ管理基礎
by
Takuto Nagami
×
Copy
Open
Link
Embed
Share
Beginning
This slide
Copy link URL
Copy link URL
Copy iframe embed code
Copy iframe embed code
Copy javascript embed code
Copy javascript embed code
Share
Tweet
Share
Tweet
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
ありがとう ございました カンファレンス、楽しみましょう!