Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
歴史から学ぶ、Goのメモリ管理基礎
Search
Takuto Nagami
January 09, 2026
Technology
3
270
歴史から学ぶ、Goのメモリ管理基礎
2026/1/9 BuriKaigi 2026にて登壇した際の資料です。
Takuto Nagami
January 09, 2026
Tweet
Share
More Decks by Takuto Nagami
See All by Takuto Nagami
【2025改訂版】ITエンジニアとして知っておいてほしい、電子メールという大きな穴
logica0419
2
120
Fundamentals of Memory Management in Go: Learning Through the History
logica0419
1
110
GopherCon Tourのつくりかた
logica0419
2
82
Go言語はstack overflowの夢を見るか?
logica0419
2
750
あなたの言葉に力を与える、演繹的なアプローチ
logica0419
1
250
GC25 Recap+: Advancing Go Garbage Collection with Green Tea
logica0419
3
880
GopherCon Tour 概略
logica0419
2
510
言葉の壁を越えて ~Gophers EXと歩む海外登壇への道~
logica0419
1
71
Maintainer Meetupで「生の声」を聞く ~講演だけじゃないKubeCon
logica0419
1
760
Other Decks in Technology
See All in Technology
Claude Codeを使った情報整理術
knishioka
16
11k
テストセンター受験、オンライン受験、どっちなんだい?
yama3133
0
200
「リリースファースト」の実感を届けるには 〜停滞するチームに変化を起こすアプローチ〜 #RSGT2026
kintotechdev
0
390
20251222_サンフランシスコサバイバル術
ponponmikankan
2
160
Digitization部 紹介資料
sansan33
PRO
1
6.4k
Oracle Cloud Infrastructure:2025年12月度サービス・アップデート
oracle4engineer
PRO
0
170
Authlete で実装する MCP OAuth 認可サーバー #CIMD の実装を添えて
watahani
0
350
マーケットプレイス版Oracle WebCenter Content For OCI
oracle4engineer
PRO
5
1.5k
Next.js 16の新機能 Cache Components について
sutetotanuki
0
210
Everything As Code
yosuke_ai
0
470
[2025-12-12]あの日僕が見た胡蝶の夢 〜人の夢は終わらねェ AIによるパフォーマンスチューニングのすゝめ〜
tosite
0
240
田舎で20年スクラム(後編):一個人が企業で長期戦アジャイルに挑む意味
chinmo
1
190
Featured
See All Featured
Keith and Marios Guide to Fast Websites
keithpitt
413
23k
The untapped power of vector embeddings
frankvandijk
1
1.5k
Measuring & Analyzing Core Web Vitals
bluesmoon
9
720
SEO for Brand Visibility & Recognition
aleyda
0
4.1k
I Don’t Have Time: Getting Over the Fear to Launch Your Podcast
jcasabona
34
2.6k
JavaScript: Past, Present, and Future - NDC Porto 2020
reverentgeek
52
5.8k
Heart Work Chapter 1 - Part 1
lfama
PRO
3
35k
Done Done
chrislema
186
16k
Deep Space Network (abreviated)
tonyrice
0
32
How to train your dragon (web standard)
notwaldorf
97
6.5k
How to Build an AI Search Optimization Roadmap - Criteria and Steps to Take #SEOIRL
aleyda
1
1.8k
How Software Deployment tools have changed in the past 20 years
geshan
0
30k
Transcript
Takuto Nagami @logica0419 歴史から学ぶ Goのメモリ管理基礎
メモリ管理を知らない / 理解に苦しんでいる人?
【通説】 メモリ管理は難しい 僕も結構苦しんでいました
メモリ管理ってなんだ? • メモリ管理は広い概念 ◦ マシン、OS、言語など領域によって意味が異なる • プログラミング言語の領域で言うと… ◦ どのようにデータ(変数)が メモリ内で管理されるかを
決める戦略
なぜメモリ管理を気にするのか? • アプリケーションのパフォーマンス向上 • Go自身のメンテナンスにはメモリ管理の知識が必要 • 深堀りすると面白い! 中でも、特にパフォーマンス 向上が大きなモチベーション
メモリ周りのパフォーマンス向上例 • たった一つのメモリ割り当ての変更だけで、CPU使用率 57%削減、メモリ使用量99%削減した例も!
メモリ管理のセッションは難しい!
メモリ管理のセッションは難しい!
メモリ管理のセッションは難しい!
メモリ管理のセッションは難しい!
メモリ管理のセッションは難しい!
メモリ管理のセッションは難しい! 正直わからん! ではなぜ難しいんだろう?
学習プロセス: Why, What and How アプリケーションに新機能を追加するとき
学習プロセス: Why, What and How アプリケーションに新機能を追加するとき Why なぜ必要 なのか
学習プロセス: Why, What and How Why なぜ必要 なのか What 何を追加
するのか アプリケーションに新機能を追加するとき
学習プロセス: Why, What and How Why なぜ必要 なのか What 何を追加
するのか How どのように 実装するのか アプリケーションに新機能を追加するとき
アプリケーションに新機能を追加するとき 学習プロセス: Why, What and How Why なぜ必要 なのか What
何を追加 するのか How どのように 実装するのか 既存セッションは メモリ管理がどのように 実装されているかに フォーカスしがち
学習プロセス: Why, What and How Why なぜ必要 なのか What 何を追加
するのか How どのように 実装するのか アプリケーションに新機能を追加するとき
今日お話するのは メモリ管理の 「なぜ」と「何」です! それぞれの戦略がなぜ生まれ、何をするのか学びましょう
前提: Goが提供するコンポーネント • コンパイラ ◦ ソースコードを実行ファイル (バイナリ) にする • ランタイム
◦ 実行中、書いたソースコードと一緒に動く
前提: Goが提供するコンポーネント ソース コード • コンパイラ ◦ ソースコードを実行ファイル (バイナリ) にする
• ランタイム ◦ 実行中、書いたソースコードと一緒に動く go build
前提: Goが提供するコンポーネント ソース コード 実行 ファイル go build • コンパイラ
◦ ソースコードを実行ファイル (バイナリ) にする • ランタイム ◦ 実行中、書いたソースコードと一緒に動く
前提: Goが提供するコンポーネント コンパイラ の仕事 • コンパイラ ◦ ソースコードを実行ファイル (バイナリ) にする
• ランタイム ◦ 実行中、書いたソースコードと一緒に動く ソース コード 実行 ファイル go build
前提: Goが提供するコンポーネント (バイナリを実行) • コンパイラ ◦ ソースコードを実行ファイル (バイナリ) にする •
ランタイム ◦ 実行中、書いたソースコードと一緒に動く コンパイラ の仕事 ソース コード 実行 ファイル go build
前提: Goが提供するコンポーネント ランタイム の仕事 • コンパイラ ◦ ソースコードを実行ファイル (バイナリ) にする
• ランタイム ◦ 実行中、書いたソースコードと一緒に動く コンパイラ の仕事 ソース コード 実行 ファイル go build (バイナリを実行)
前提: Goが提供するコンポーネント go run • コンパイラ ◦ ソースコードを実行ファイル (バイナリ) にする
• ランタイム ◦ 実行中、書いたソースコードと一緒に動く コンパイラ の仕事 ソース コード 実行 ファイル go build (バイナリを実行) ランタイム の仕事
メモリとは何か / 古のメモリ管理
メモリとは何か メモリ = RAM (Random Access Memory)
メモリとは何か メモリ = RAM (Random Access Memory) ↑ ストレージ (HDD/SSD)
メモリとは何か メモリ = RAM (Random Access Memory) ↑ ストレージ (HDD/SSD)
メモリを図解する . . . • メモリはアプリケーションから Key-Valueストアとして使用 ◦ Key: メモリアドレス
◦ Value: 各アドレスに1バイト ※ OSはRAMを分割し、それぞれの プロセスに「仮想メモリ」として割り当てる (詳しくは「仮想メモリ」で検索) 0x0000 0x0001 0x0002 0x0003
アセンブリにおけるメモリの読み書き . . . . . . 0x0000 0x0001 0x0002
0x0003 {address}
アセンブリにおけるメモリの読み書き . . . . . . 書き込み 0x0000 0x0001
0x0002 0x0003 {address} 1 1
アセンブリにおけるメモリの読み書き . . . . . . 読み込み 0x0000 0x0001
0x0002 0x0003 {address} 1 1 %eax (CPUレジスタ)
【つらみ】直感的だが、かなりつらい • プログラマが、使うメモリアドレスを全て覚えておく 必要がある 🤯
変数の誕生 / スタックとヒープ
高級プログラミング言語の誕生 • 1957: Fortranがリリース ◦ 「最初」の高級プログラミング言語 • COBOL, BASIC, PASCAL,
C... からGoに至るまで、様々 な言語が開発されてきた
データの抽象化 - 変数 . . . (変数)
データの抽象化 - 変数 . . . . . . .
. . . . {a addr} 12 a (変数) 12
データの抽象化 - 変数 a (変数) 12 {a addr} {b addr}
b 34 . . . . . . . . . 12 34
データの抽象化 - 変数 a (変数) 12 b 34 {a addr}
{b addr} . . . . . . . . . 12 34 プログラマは 変数の名前だけ 認識すれば良い
プログラマーはアドレスを 覚える必要ナシ🙌 代わりに言語側に 管理責任が生まれる😨
データを配置するためのルールが欲しい {a addr} ? {b addr} ? . . .
. . . . . . 12 34
関数呼び出し
main() 関数呼び出し
呼び出し main() 関数呼び出し
呼び出し main() a() 関数呼び出し
関数呼び出し 呼び出し main() a() 呼び出し
関数呼び出し 呼び出し main() a() 呼び出し b()
関数呼び出し 呼び出し main() a() return
関数呼び出し 呼び出し main() a()
関数呼び出し return main()
関数呼び出し main()
関数呼び出し プログラム終了
関数呼び出しのデータ構造 Last In, First Out (LIFO)...? main() a() 呼び出し main()
a() return
関数呼び出しのデータ構造 First In, First Out (FIFO)...? main() a() 呼び出し main()
a() return そう、 スタックですね!
. . . スタック メモリ領域
. . . main() stack frame スタック メモリ領域 {main() s.f.
addr}
スタックフレーム • メモリ内における「関数」の表現方法 ◦ それぞれのフレームがスコープになる • 構造はABIによって定められている ◦ ローカル変数 ◦
(一部) 引数 ◦ 自分を呼び出した関数のスタックフレームアドレス ◦ 別関数を呼び出した場所 などが格納される a() stack frame • 10行目で呼び出し • main()にreturn 5 7
. . . main() stack frame スタック メモリ領域 {main() s.f.
addr}
. . . main() stack frame 5 スタック メモリ領域 {main()
s.f. addr} {v addr}
. . . main() stack frame • 3行目で呼び出し 5 スタック
メモリ領域 {main() s.f. addr} {v addr}
. . . main() stack frame • 3行目で呼び出し 5 a()
stack frame スタック メモリ領域 {a() s.f. addr} {main() s.f. addr} {v addr}
. . . main() stack frame • 3行目で呼び出し 5 a()
stack frame • main()にreturn スタック メモリ領域 {a() s.f. addr} {main() s.f. addr} {v addr}
. . . main() stack frame • 3行目で呼び出し 5 a()
stack frame • main()にreturn 5 スタック メモリ領域 {a() s.f. addr} {arg addr} {main() s.f. addr} {v addr}
. . . 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}
. . . 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}
. . . 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}
. . . 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}
. . スタック メモリ領域 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
. . スタック メモリ領域 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
. . スタック メモリ領域 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
. . スタック メモリ領域 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
. . スタック メモリ領域 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
. . スタック メモリ領域 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レジスタ)
. . スタック メモリ領域 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レジスタ)
. . . スタック メモリ領域 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レジスタ)
. . . スタック メモリ領域 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レジスタ)
. . . スタック メモリ領域 main() stack frame 5 {main()
s.f. addr} {v addr} 8 (CPUレジスタ)
. . . スタック メモリ領域 main() stack frame 8 {main()
s.f. addr} {v addr} 8 (CPUレジスタ)
. . . スタック メモリ領域 main() stack frame 8 {main()
s.f. addr} {v addr}
. . . スタック メモリ領域 プログラム終了
スタックは銀の弾丸ではない • ビルド時にサイズが不定の型 (map/slice) は扱えない ◦ スタック関連の処理はコンパイラの仕事 • 引数以外の方法で関数をまたいだ変数の共有ができない
新しい戦略: 動的メモリ確保 (ヒープ メモリ領域) スタックの問題を解決するためのメモリ割り当て方法
{main() s.f. addr} ヒープ メモリ領域 . . . . .
. . main() stack frame
ヒープ メモリ領域 . . . . . . . []
{slice_h addr} {main() s.f. addr} main() stack frame
ヒープ メモリ領域 . . . . . . . []
{slice_h addr} {main() s.f. addr} main() stack frame ランタイムに よって アドレスが決まる
ヒープ メモリ領域 . . . . . . . []
{slice_h addr} {main() s.f. addr} main() stack frame main()はsliceが どこかわからない (スタックフレームの中 しか見られない)
. . . . . . . [] {slice_h addr}
{main() s.f. addr} {slice addr} main() stack frame ヒープ メモリ領域 {slice_h addr}
. . . . . . . [] {slice_h addr}
{main() s.f. addr} {slice addr} main() stack frame ヒープ メモリ領域 これが ポインタ {slice_h addr}
. . . . . . . [] {slice_h addr}
{main() s.f. addr} {slice addr} {num addr} main() stack frame ヒープ メモリ領域 5 これが ポインタ {slice_h addr}
ポインタは大事なので、もう少し詳しく見ていきます もう一つ例を挙げて ポインタがどのように 振る舞うか見てみよう
値渡し と ポインタ渡し 違いを原理からきちんと説明できますか?
. . . main() stack frame • 3行目で呼び出し 5 値渡し
{main() s.f. addr} {num addr}
. . . main() stack frame • 3行目で呼び出し 5 値渡し
f() stack frame {f() s.f. addr} {main() s.f. addr} {num addr}
. . . main() stack frame • 3行目で呼び出し 5 値渡し
f() stack frame 5 {f() s.f. addr} {num addr} {main() s.f. addr} {num addr}
{f() s.f. addr} {num addr} {main() s.f. addr} {num addr}
. . . main() stack frame • 3行目で呼び出し 5 値渡し f() stack frame 10
{f() s.f. addr} {num addr} {main() s.f. addr} {num addr}
. . . main() stack frame • 3行目で呼び出し 5 値渡し f() stack frame 10 main()関数の numには 影響しない
. . . ポインタ渡し main() stack frame • 3行目で呼び出し 5
{main() s.f. addr} {num addr}
. . . ポインタ渡し main() stack frame • 3行目で呼び出し 5
f() stack frame {f() s.f. addr} {main() s.f. addr} {num addr}
. . . main() stack frame • 3行目で呼び出し f() stack
frame ポインタ渡し 5 {num addr} {f() s.f. addr} {num addr} {main() s.f. addr} {num addr}
. . . main() stack frame • 3行目で呼び出し f() stack
frame ポインタ渡し {f() s.f. addr} {num addr} {main() s.f. addr} {num addr} 10 {num addr}
. . . {f() s.f. addr} {num addr} {main() s.f.
addr} {num addr} main() stack frame • 3行目で呼び出し 10 f() stack frame ポインタ渡し {num addr} main()関数の numに 影響を及ぼす
スタックとヒープを 組み合わせることで 汎用的なメモリ管理が 実現できる
古のヒープ管理 . . . . . . . {main() s.f.
addr} main() stack frame C言語を例にとると • func malloc(int size) pointer • func free(pointer memory) (上記関数は疑似コード)
古のヒープ管理 C言語を例にとると • func malloc(int size) pointer • func free(pointer
memory) (上記関数は疑似コード) . . . . . . . ~~~ {ヒープのどこか} {main() s.f. addr} main() stack frame {ヒープのどこか}
古のヒープ管理 . . . . . . . {main() s.f.
addr} main() stack frame {ヒープのどこか} C言語を例にとると • func malloc(int size) pointer • func free(pointer memory) (上記関数は疑似コード)
【つらみ】もしもfree()を忘れたら? • メモリ使用量が無限に増えていく ◦ メモリリークと呼ばれている • 最終的にはOOM (Out of Memory)
killで強制終了
ヒープの自動管理 / ガベージコレクション
ガベージコレクション (GC) • もう使われないヒープ内のデータを削除する ◦ スタックは一切関係ない戦略 • 基本的にランタイムの仕事
ヒープ領域 スタック領域 マーク & スイープ main() stack frame a() stack
frame b() stack frame a b c e {a addr} d {b addr} {var1 addr} var1 {d addr}
ヒープ領域 スタック領域 マーク段階 main() stack frame a() stack frame b()
stack frame a b c e {a addr} d {b addr} {var1 addr} var1 {d addr}
ヒープ領域 スタック領域 マーク段階 main() stack frame a() stack frame b()
stack frame a b c e {a addr} d {b addr} {var1 addr} var1 {d addr}
ヒープ領域 スタック領域 マーク段階 main() stack frame a() stack frame b()
stack frame a b c e {a addr} d {b addr} {var1 addr} var1 {d addr}
ヒープ領域 スタック領域 マーク段階 main() stack frame a() stack frame b()
stack frame a b c e {a addr} d {b addr} {var1 addr} var1 {d addr}
ヒープ領域 スタック領域 マーク段階 main() stack frame a() stack frame b()
stack frame a b c e {a addr} d {b addr} {var1 addr} var1 {d addr}
ヒープ領域 スタック領域 マーク段階 main() stack frame a() stack frame b()
stack frame a b c e {a addr} d {b addr} {var1 addr} var1 {d addr}
ヒープ領域 スタック領域 マーク段階 main() stack frame a() stack frame b()
stack frame a b c e {a addr} d {b addr} {var1 addr} var1 {d addr}
ヒープ領域 スタック領域 main() stack frame a() stack frame b() stack
frame a b e {a addr} d {b addr} {var1 addr} var1 {d addr} スイープ段階 c
ヒープ領域 スタック領域 main() stack frame a() stack frame b() stack
frame a b e {a addr} d {b addr} {var1 addr} var1 {d addr} スイープ段階
シンプルだが、最適化は結構難しい 例えばGo言語では、以下のような最適化が行われている • 我々が書いたコードと並列で動かす ◦ Tri-colorマーキングなどを使う • GCのトリガーの調整 ◦ GOGC
◦ GOMEMLIMIT • Go 1.25で実験的マークアルゴリズム Green Tea GCが導入
シンプルだが、最適化は結構難しい • 複雑なアルゴリズムを使った最適化は可能 • Goは↑をしていない ◦ シンプルの哲学が、仕様だけでなくランタイム実装 にも根付いている
【つらみ】GCは重い • ヒープに多くのデータを置くと、GCが重くなる ◦ 特にマーク段階が重い
多くのGC言語は型でヒープを決める • 昔のJava: オブジェクトは全てヒープ • Python: オブジェクトは全てヒープ • JS: non-primitive型の値は全てヒープ
多くのGC言語は型でヒープを決める • 昔のJava: オブジェクトは全てヒープ • Python: オブジェクトは全てヒープ • JS: non-primitive型の値は全てヒープ
Non-primitiveな型は ヒープに行きがち
ヒープエスケープ 【Go特有】
ヒープエスケープ • できる限り多くのデータをスタックに置く • 本当に必要なデータのみヒープに「逃げる」
どの変数がヒープに逃げるかわかる?
by Jacob Walker どの変数がヒープに逃げるかわかる?
典型的なパターンは存在するけど… • 返り値がポインタの場合 • インターフェース型として扱われる場合 • map, channel, string, 大抵のslice
コンパイラとランタイムのみぞ知る • 多くのヒープエスケープはコンパイラが決める • 残りのヒープエスケープはランタイムが決める
エスケープ解析 • コンパイラは変数を解析してヒープに行くものを決める ◦ この解析ログを出力させることができる go {run/build/test} -gcflags="-m" **.go
ベンチマーク • ランタイム担当分まで含め、実際にヒープに割り当て られたかはベンチマークで確かめる go test -bench {package} -benchmem
例 (エスケープあり)
例 (エスケープあり)
例 (エスケープあり)
例 (エスケープなし)
例 (エスケープなし)
例 (エスケープなし)
例 (エスケープなしに見えてもある場合)
例 (エスケープなしに見えてもある場合)
例 (エスケープなしに見えてもある場合)
ヒープ内でのオブジェクト管理 int (8 byte) • 色んなデータ (オブジェクト) がヒープに入る ◦ バラバラに入れていたら管理が面倒
struct (可変サイズ) ポインタ (8 byte)
… … ヒープ内でのオブジェクト管理 • 同サイズのオブジェクトをスパンでまとめる 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
ヒープ内でのオブジェクト管理 • スパン (mspan) はもっと大きな単位でまとめられる ◦ mcentral ▪ 同じサイズのスパンをグループ化 ◦
arena ▪ mheapが1回で引き出してくるメモリ量 ▪ mcentralを複数格納 ◦ mheap ▪ ヒープ本体
ゼロアロケーション • ヒープ割り当て (エスケープ) が無いという意味 ◦ スタックにはデータを入れるので「文字通りゼロ」 ではない • パフォーマンス向上のためによく使われる
ゼロアロケーションライブラリ zerolog: ゼロアロケーションログ
ゼロアロケーションライブラリ fasthttp: (一部) ゼロアロケーションhttp server
ゼロアロケーションライブラリ go-reflect: ゼロアロケーションリフレクション
ゼロアロケーションも銀の弾丸ではない • ヒープを使わないので、関数をまたいだ データ共有は全て引数のコピー ◦ 引数のサイズが大きいと逆に重くなる • ゼロアロケーション = 正義
ではない ◦ ベンチマークで効果を計測しよう main() Var foo() Arg bar() Arg
ヒープエスケープを理解し ゼロアロケーションを 賢く使いましょう
スタックオーバーフロー • ヒープとスタックの境目を決めるのは言語 ◦ 例: Linux上のC言語ならスタックは8MB . . . .
. ヒープ変数 main() stack frame
スタックオーバーフロー • 再帰関数などでスタックサイズの制限を超えてしまう
スタックオーバーフロー • 再帰関数などでスタックサイズの制限を超えてしまう → スタックオーバーフロー!! • !!
【つらみ】スタックが爆発する • ヒープエスケープはスタックの使用量を増やす ◦ スタックオーバーフローが起こりやすくなる
【つらみ】ヒープも爆発する • スタック領域を大きくするといいのでは…? • スタックが大きい = ヒープが小さい ◦ ✅ スタックオーバーフローは
起こりにくくなる ◦ ❌ OOM killの確率が上がる😭 • あちらを立てればこちらが立たず状態 . . . . . Heap var main() stack frame
可変サイズスタック 【Go特有】
Goのスタックは可変サイズ • Goはgoroutineごとに分離したスタックを持つ • それぞれのスタックが状況に合わせて伸び縮みする ◦ 4kBで初期化される
ざっくりしたイメージ 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
可変スタックの実装 • 昔はセグメント化スタックによって実装されていた • 今はスタックコピー (より効率が良い)
スタックコピー . . . main() stack frame m2() stack frame
main goroutine のスタック
スタックコピー . . . main() stack frame m2() stack frame
m3() stack frame m3()を実行する ためには サイズが足りない
スタックコピー . . . main() stack frame m2() stack frame
2倍のサイズ 新しい main goroutine のスタック
スタックコピー . . . main() stack frame m2() stack frame
main() stack frame m2() stack frame
スタックコピー . . . main() stack frame m2() stack frame
スタックコピー . . . main() stack frame m2() stack frame
m3() stack frame これでm3()が 実行できる
スタックコピー . . . main() stack frame m2() stack frame
スタックコピー . . . main() stack frame m2() stack frame
スタックが可変サイズなら スタックオーバーフローは 起きないのでは?
Goのスタックオーバーフロー 残念ながら実際は起こる
最大スタックサイズ • 各goroutineはデフォルトで1GBまでしかスタックを 拡大できない ◦ 制限を超えるとスタックオーバーフローは起こる
最大サイズの変更 • runtime/debug.SetMaxStack()で上書き可能
goroutineごとの 可変サイズスタックが Goのスタックの特殊さ goroutineが特殊だと言われる理由の一つでもある
冒頭で紹介した事例の解説
【おさらい】今回扱う事例 https://developers.cyberagent.co.jp/blog/archives/54653/ • たった一つのメモリ割り当ての変更だけで、CPU使用率 57%削減、メモリ使用量99%削減した例
異常なスパイク - 大量のエスケープ • CPU使用率に定期的なスパイクが見られた • 寿命の短いヒープオブジェクトが大量に生成されていた ◦ GCが高頻度でトリガーされていた •
↑ を引き起こしていたのはフィルター関数
改善前のフィルター関数
改善前のフィルター関数
改善前のフィルター関数
改善前のフィルター関数 たった一つの 割り当てが大量の GCを引き起こしていた
改善後のフィルター関数
改善後のフィルター関数 スライスを 再利用
ベンチマーク • 両方の関数でベンチマークを回してみた ◦ 1から100までのリストを元の配列とする ◦ 偶数のみを抜き出すようフィルタリング
2倍の速さ! ベンチマーク • 両方の関数でベンチマークを回してみた ◦ 1から100までのリストを元の配列とする ◦ 偶数のみを抜き出すようフィルタリング
ベンチマーク • 両方の関数でベンチマークを回してみた ◦ 1から100までのリストを元の配列とする ◦ 偶数のみを抜き出すようフィルタリング たった一つの割り当て で引き起こされた ことがわかる
2倍の速さ!
更なるリファクタリング (記事の内容) • イテレータを使うと、よりシンプルかつ安全に書ける
改善結果 • ワークロード全体で ◦ CPU使用率57%削減!! ◦ メモリ使用量99%削減!!
まとめ
まとめ • メモリ管理はプログラマーの需要を満たすために発展 してきた • メモリ管理に関する知識は強い力となる • 今回のセッションはあくまで出発点 ◦ 他のセッションで各機能を深堀りしてみよう!
おまけ: 次のステップは?
ガベージコレクションの実装 GoのGCについて理解する @ Go Conference 2022 Spring
ガベージコレクションの条件とその活用 Go1.19から始めるGCのチューニング方法 @ Go Conference 2023 Online
エスケープ解析とその活用 Escape Analysis in Go: Understanding and Optimizing Memory Allocation
@ Go Conference 2023 Online
Goのメモリ割り当てとOSの関係 Goのメモリ管理 @ Go Conference 2023 Online ⚠発展的
並列処理におけるメモリ管理 よくわかるThe Go Memory Model - 行間を 図解で埋め尽くす @ Go
Conference 2023 Online
ありがとう ございました カンファレンス、楽しみましょう!