Go: -gcflags=-m でエスケープ解析の結果を見て、ヒープ割り当てを減らす
Go コンパイラは変数をスタックに置くかヒープに置くか(エスケープ解析)を自動で決める。go build -gcflags=-m でその判断理由を表示でき、ホットパスの不要なヒープ割り当て(=GC 負荷)を見つけられる。
見方
sh
go build -gcflags='-m' ./... # 判断結果
go build -gcflags='-m -m' ./pkg/... # 理由まで詳しく(出力が多い)
go test -gcflags='-m' -run TestX ./pkg # テスト対象だけ見るときも出力例(意味):
moved to heap: x… 変数 x がヒープに移された&T{...} escapes to heap… 構造体リテラルがヒープ割り当てになったx does not escape… スタックで済んでいるcan inline f… インライン化された(インライン化でエスケープしなくなることもある)
ヒープに逃げやすい典型
- ローカル変数のポインタを返す(
return &v)。呼び出し元より長く生きるため。 interface{}/anyに値を入れる(fmt.Println(x)の引数など)。- クロージャが外側の変数をキャプチャし、そのクロージャが関数の外へ出る(goroutine に渡す、返す)。
- サイズがコンパイル時に分からないスライス(
make([]byte, n)の n が変数)や、大きすぎる配列。 - 値をポインタでチャネルやグローバル変数・フィールドに保存する。
使いどころ
- まずベンチマーク(
go test -bench . -benchmemのallocs/op)で割り当てが多い関数を見つけ、そこだけ-mで理由を確認する。全体を最適化しようとしない。 - 小さい構造体はポインタでなく値で返す・受け取ると、スタックで済むことが多い。
- バッファは呼び出し側で確保して渡す(
func Read(buf []byte)型の API)と割り当てを使い回せる。
注意
- 判断はコンパイラのバージョンで変わる。Go を上げたら測り直す。
- 「ヒープ=悪」ではない。読みやすさを犠牲にした最適化は、計測で効果が確認できた箇所に限る。
出典
- @func25 2026-10-12(解説動画の紹介): @func25 · 2026-10-12
- 動画: youtube.com (動画本編は未視聴。上記はコマンドと一般的な挙動を自分の言葉でまとめたもの)