🍵
Go 1.26で導入されたGreen Tea GC:なぜGCは進化したのか
Go プログラミング パフォーマンス
目次
はじめに
Go 1.26でGreen Tea GCが導入され、GCのパフォーマンスが大幅に向上しました。
本記事では、なぜ改善が必要だったのか、何が変わったのかを、前提知識から解説します。
前提知識:スタックとヒープ
Goのメモリはスタックとヒープの2つの領域で管理されています。
| 特性 | スタック | ヒープ |
|---|---|---|
| メモリアクセス速度 | 超高速 | 遅い |
| ライフサイクル | 関数終了時に自動解放 | GCが回収するまで残る |
| 用途 | ローカル変数 | 関数をまたぐデータ |
エスケープ解析
Goのコンパイラは「スタックとヒープのどちらに置くか」を自動判断します(エスケープ解析)。
ヒープにエスケープする条件
1. ポインタを返す
func newUser() *User {
u := &User{Name: "Alice"}
return u // ヒープにエスケープ
}
2. インターフェースに格納
var i interface{} = 42 // 42はヒープに配置
3. サイズが大きすぎる、または実行時に決まる
// 実行時にサイズが決まる
s := make([]int, n) // ヒープにエスケープ
# エスケープ解析の確認方法
go build -gcflags="-m" main.go
GCの仕組み:マーク&スイープ
Goは並行マーク&スイープ方式のGCを採用。
フェーズ1: マーク
まだ使われているデータを特定するフェーズです。
📌 用語の整理
- オブジェクト: ヒープ上のデータ(構造体、スライス、マップなど)
- ルート: マーキングの起点(グローバル変数、スタック、レジスタ)
🔍 マーキングの流れ
💡 Go公式blog中のスライドが超わかりやすいので見てください。
フェーズ2: スイープ
マークされなかったオブジェクトのメモリを回収し、再利用可能にします。
従来のGCの問題点
GCコストの約90%がマーキングに費やされ、そのうち35%がメモリアクセスの停滞でした。
🐌 グラフフラッド方式の課題
従来の方式では、オブジェクト間の参照を追跡する際、メモリ上で離れた場所を行ったり来たりします。
問題点
- メモリが分散 → CPUキャッシュが効かない
- 毎回メインメモリにアクセス
- メインメモリはキャッシュより最大100倍遅い
結果、CPUはメモリフェッチを個別に待つ必要があり、マーキングのボトルネックに。
Green Tea GCの改善点
💡 GCの仕組みと改善点はGo公式blog中のスライドが超わかりやすいので見てほしいです。
発見したオブジェクトをすぐに処理するLIFO方式→FIFO(First In First Out)方式に変更
効果
- 最初に入れたオブジェクトを後で処理
- 待機中に同じメモリページの他のオブジェクトがCPUキャッシュに蓄積
- ページ間を飛び回らず、1ページ内で効率的にマーキング
→ CPUキャッシュヒット率が向上し、メモリアクセス待ちが削減
結果
Green Tea GCの導入により、メモリアクセスの停滞(従来35%)が大幅に削減され、マーキングフェーズのパフォーマンスが向上しました。
まとめ
- スタックとヒープ: スタックはメモリアクセスが高速でGC不要、ヒープは柔軟だがGCが必要
- マーク&スイープ: ルートから到達可能なオブジェクトをマーク→不要なものを回収
- 従来の問題: グラフフラッド方式ではCPUキャッシュが効かず、メモリアクセスがボトルネック
- Green Tea GCの改善: FIFO方式でCPUキャッシュの効率を向上
参考リソース
- Green Tea GC公式ブログ→より詳細に背景~改善結果が書かれています。絶対に一読してください!!
- CyberAgent技術ブログ→メモリ改善によるパフォーマンス・チューニングのいい例でしたので、併せて一読してほしいです!(Green Tea GCは関係ないです)
- ASCII.jp - メモリとレジスタの仕組み