|
| 1 | +--- |
| 2 | +title: "Go 1.26の新GC「Green Tea(緑茶)」解説" |
| 3 | +date: 2026/01/30 00:00:00 |
| 4 | +postid: a |
| 5 | +tag: |
| 6 | + - Go |
| 7 | + - Go1.26 |
| 8 | + - GC |
| 9 | +category: |
| 10 | + - Programming |
| 11 | +thumbnail: /images/2026/20260130a/thumbnail.png |
| 12 | +author: 棚井龍之介 |
| 13 | +lede: "Green Tea GCの開発タイムライン(出典: [Go公式ブログ])*[Go 1.26] がリリースされ、ガベージコレクタ(GC)に大きな変更が加わりました。その名も Green Tea GC(緑茶GC)です。" |
| 14 | +--- |
| 15 | +<img src="/images/2026/20260130a/Green_Tea_GC開発のタイムライン.png" alt="Green_Tea_GC開発のタイムライン" width="1200" height="563" loading="lazy"> |
| 16 | +*Green Tea GCの開発タイムライン(出典: [Go公式ブログ](https://go.dev/blog/greenteagc))* |
| 17 | + |
| 18 | +## はじめに |
| 19 | + |
| 20 | +[Go 1.26](https://go.dev/doc/go1.26) がリリースされ、ガベージコレクタ(GC)に大きな変更が加わりました。その名も **Green Tea GC**(緑茶GC)です。 |
| 21 | + |
| 22 | +公式ブログによると、この名前は2024年にGoランタイムチームのAustinが日本でカフェ巡りをしながら、大量の抹茶を飲みつつプロトタイプを開発したことに由来しているそうです。 |
| 23 | + |
| 24 | +> Green Tea got its name in 2024 when Austin worked out a prototype of an earlier version **while cafe crawling in Japan and drinking LOTS of matcha!** This prototype showed that the core idea of Green Tea was viable. And from there we were off to the races. |
| 25 | +
|
| 26 | +Green Tea GCは [Go 1.25](https://go.dev/doc/go1.25) で `GOEXPERIMENT=greenteagc` として実験的に導入され、Go 1.26 からはデフォルトで有効化されました。本記事では、この新しいGCの仕組みと、実際にどの程度の性能改善が得られるのかを解説します。 |
| 27 | + |
| 28 | +--- |
| 29 | + |
| 30 | +## TL;DR |
| 31 | + |
| 32 | +- **10〜40%のGCオーバーヘッド削減**を実現 |
| 33 | +- メモリアクセスの**空間的局所性**を大幅に改善 |
| 34 | +- Intel/AMDの最新CPUでは**AVX-512によるベクトル加速**も利用可能 |
| 35 | +- Go 1.26からデフォルト有効(オプトアウト: `GOEXPERIMENT=nogreenteagc`) |
| 36 | + |
| 37 | +--- |
| 38 | + |
| 39 | +## GoのGCの基本アルゴリズム |
| 40 | + |
| 41 | +GoのGCは **並行マークスイープ(Concurrent Mark-Sweep)** アルゴリズムを採用しています。 |
| 42 | +まず、従来から使われている基本的な仕組みを確認しましょう。 |
| 43 | + |
| 44 | +### マーキングの仕組み |
| 45 | + |
| 46 | +GoのGCは、プログラムが使用中のオブジェクトを特定するために「マーキング」を行います。 |
| 47 | + |
| 48 | +マーキングでは、「ルート」(グローバル変数やスタック上の変数)から参照をたどり、到達できるオブジェクトに「使用中」の印をつけていきます。印がつかなかったオブジェクトは、プログラムから到達不可能なのでGC対象となります。 |
| 49 | + |
| 50 | +処理の流れは次のとおりです: |
| 51 | + |
| 52 | +1. ルートから参照されているオブジェクトをワークリストに追加 |
| 53 | +2. ワークリストからオブジェクトを取り出し、その中のポインタを調べる(これを「スキャン」と呼ぶ) |
| 54 | +3. 見つけたポインタが指すオブジェクトをワークリストに追加 |
| 55 | +4. ワークリストが空になるまで繰り返す |
| 56 | + |
| 57 | +```txt |
| 58 | +【マーキング処理の流れ】 |
| 59 | +
|
| 60 | + ルート |
| 61 | + │ |
| 62 | + ▼ |
| 63 | + ┌───────┐ |
| 64 | + │ A │ ←── スタート |
| 65 | + └───────┘ |
| 66 | + │ │ |
| 67 | + ▼ ▼ |
| 68 | + ┌───────┐ ┌───────┐ |
| 69 | + │ B │ │ C │ |
| 70 | + └───────┘ └───────┘ |
| 71 | + │ |
| 72 | + ▼ |
| 73 | + ┌───────┐ |
| 74 | + │ D │ |
| 75 | + └───────┘ |
| 76 | +
|
| 77 | +ワークリストの変化: |
| 78 | + Step 1: [A] ← ルートからAを追加 |
| 79 | + Step 2: [B, C] ← Aをスキャン → B, Cを発見 |
| 80 | + Step 3: [C, D] ← Bをスキャン → Dを発見 |
| 81 | + Step 4: [D] ← Cをスキャン → 新規なし |
| 82 | + Step 5: [] ← Dをスキャン → 完了 |
| 83 | +``` |
| 84 | + |
| 85 | +### 従来GCの問題点 |
| 86 | + |
| 87 | +上記のマーキング処理は、公式ブログで「グラフフラッド(graph flood)」と呼ばれています。ポインタをたどってオブジェクトを次々と訪問していく処理です。 |
| 88 | + |
| 89 | +しかし、この方法には問題がありました。ワークリストからオブジェクトを取り出してスキャンするたびに、メモリ上のまったく異なる場所にジャンプしてしまうのです。 |
| 90 | + |
| 91 | +```txt |
| 92 | +メモリ空間(ページ単位で区切られている) |
| 93 | +
|
| 94 | +ページ1 ページ2 ページ3 ページ4 ページ5 |
| 95 | +┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ |
| 96 | +│ [A] │ │ [B] │ │ │ │ [D] │ │ [C] │ |
| 97 | +│ │ │ │ │ │ │ │ │ │ |
| 98 | +└───────┘ └───────┘ └───────┘ └───────┘ └───────┘ |
| 99 | +
|
| 100 | +ポインタの参照関係: A → C → B → D |
| 101 | +(AがCを参照、CがBを参照、BがDを参照) |
| 102 | + ↓ |
| 103 | +GCのアクセス先: ページ1 → 5 → 2 → 4 とジャンプ |
| 104 | +``` |
| 105 | + |
| 106 | +ポインタをたどる順序とメモリ上の配置は無関係なため、あちこちに飛び回ることになってしまいます。 |
| 107 | + |
| 108 | +CPUには「キャッシュ」という高速な一時メモリがあります。メインメモリ(RAM)へのアクセスは遅いため、よく使うデータをキャッシュに置いて高速化しています。近くのメモリを連続してアクセスすればキャッシュが効きますが、離れた場所をランダムにアクセスすると、毎回メインメモリまで取りに行く必要があります。 |
| 109 | + |
| 110 | +公式ブログでは、この状況を「高速道路ではなく市街地を走るようなもの」と表現しています。先が見えず次に何が起こるか予測できないため、CPUは本来の性能を発揮できません。 |
| 111 | + |
| 112 | +> Imagine the CPU driving down a road, where that road is your program. The CPU wants to ramp up to a high speed, and to do that it needs to be able to see far ahead of it, and the way needs to be clear. **But the graph flood algorithm is like driving through city streets for the CPU. The CPU can't see around corners and it can't predict what's going to happen next.** To make progress, it constantly has to slow down to make turns, stop at traffic lights, and avoid pedestrians. It hardly matters how fast your engine is because you never get a chance to get going. |
| 113 | +
|
| 114 | +具体的な数字で見ると([公式ブログ](https://go.dev/blog/greenteagc)より): |
| 115 | + |
| 116 | +- GC時間の **90%** がマーキングに費やされる |
| 117 | +- そのマーキング時間の **35%以上** がメモリアクセス待ち(ストール) |
| 118 | +- メインメモリへのアクセスはキャッシュの **最大100倍遅い** |
| 119 | + |
| 120 | +--- |
| 121 | + |
| 122 | +## Green Teaの仕組み |
| 123 | + |
| 124 | +Green Tea GCの核心アイデアは非常にシンプルです: |
| 125 | + |
| 126 | +> **「オブジェクト単位ではなく、ページ(スパン)単位で作業する」** |
| 127 | +
|
| 128 | +### ページ蓄積戦略 |
| 129 | + |
| 130 | +従来のGCは、ポインタを見つけるとすぐにそのオブジェクトをスキャンしていました。Green Teaでは異なるアプローチを取ります。 |
| 131 | + |
| 132 | +1. ポインタを発見したら、ターゲットオブジェクトが存在する**ページ全体**をワークリストに追加 |
| 133 | +2. ページがキューで待機している間に、同じページ内の**他のオブジェクトも蓄積** |
| 134 | +3. ページを処理する時に、蓄積されたすべてのオブジェクトを**メモリ順序で一括スキャン** |
| 135 | + |
| 136 | +```txt |
| 137 | +【従来のGC】オブジェクト単位でスキャン → ランダムアクセス |
| 138 | +
|
| 139 | + ページ1 ページ2 ページ3 ページ4 |
| 140 | +┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ |
| 141 | +│ ① │ │ ③ │ │ ② │ │ ④ │ |
| 142 | +└─────────┘ └─────────┘ └─────────┘ └─────────┘ |
| 143 | + │ ↑ ↑ ↑ |
| 144 | + └────────────┼────────────┘ │ |
| 145 | + └─────────────────────────┘ |
| 146 | + アクセス順: ① → ② → ③ → ④(ページ間をジャンプ) |
| 147 | +
|
| 148 | +
|
| 149 | +【Green Tea GC】ページ単位でスキャン → シーケンシャルアクセス |
| 150 | +
|
| 151 | + ページ1 ページ2 ページ3 ページ4 |
| 152 | +┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ |
| 153 | +│ ○ ○ ○ │→│ ○ ○ ○ │→│ ○ ○ ○ │→│ ○ ○ ○ │ |
| 154 | +└─────────┘ └─────────┘ └─────────┘ └─────────┘ |
| 155 | + アクセス順: ページ1内を全部 → ページ2内を全部 → ... |
| 156 | +``` |
| 157 | + |
| 158 | +内部的には、各オブジェクトに「発見済み/スキャン済み」のビットを持たせることで、同じページ内のオブジェクトを効率的に追跡しています。 |
| 159 | + |
| 160 | +### FIFOキュー |
| 161 | + |
| 162 | +従来のGCはLIFO(スタック)でワークリストを管理していましたが、Green TeaはFIFO(キュー)を採用しています。 |
| 163 | + |
| 164 | +- **LIFO(従来のGC)**: 最後に入れたものを最初に処理 → 蓄積する時間がない |
| 165 | +- **FIFO(Green Tea GC)**: 最初に入れたものを後で処理 → 待機中に同じページの他オブジェクトが蓄積される |
| 166 | + |
| 167 | +### ベクトル加速 |
| 168 | + |
| 169 | +2020年以降の新しいIntel/AMD CPUでは、CPU内蔵の高速演算機能(ベクトル命令)を活用して、追加で約10%の性能改善が得られます。 |
| 170 | + |
| 171 | +詳細は公式ドキュメントを参照してください: |
| 172 | + |
| 173 | +- [Go 1.26 Release Notes](https://go.dev/doc/go1.26) |
| 174 | +- [The Green Tea Garbage Collector](https://go.dev/blog/greenteagc) |
| 175 | +- [A Guide to the Go Garbage Collector](https://go.dev/doc/gc-guide) |
| 176 | + |
| 177 | +なお、Green Teaは512バイト以下の小オブジェクトに最適化されており、各プロセッサが独自のスパンキューを持つことで、多コア環境でも効率的にスケールします。 |
| 178 | + |
| 179 | +--- |
| 180 | + |
| 181 | +## 実際に動作させて、計測してみる |
| 182 | + |
| 183 | +実際にGreen Tea GCの効果を確認するため、GC負荷の高いベンチマークプログラムを作成して計測してみました。 |
| 184 | + |
| 185 | +### ベンチマークコード |
| 186 | + |
| 187 | +```go gc_bench.go |
| 188 | +package main |
| 189 | + |
| 190 | +import ( |
| 191 | + "fmt" |
| 192 | + "runtime" |
| 193 | + "time" |
| 194 | +) |
| 195 | + |
| 196 | +type SmallObject struct { |
| 197 | + next *SmallObject |
| 198 | + val [6]int64 // 48 bytes + pointer = ~64 bytes block |
| 199 | +} |
| 200 | + |
| 201 | +const ( |
| 202 | + numObjects = 10_000_000 // 1000万個のオブジェクト |
| 203 | + iterations = 100 |
| 204 | +) |
| 205 | + |
| 206 | +func main() { |
| 207 | + fmt.Println("Starting GC Benchmark...") |
| 208 | + |
| 209 | + var m runtime.MemStats |
| 210 | + runtime.ReadMemStats(&m) |
| 211 | + initialNumGC := m.NumGC |
| 212 | + |
| 213 | + start := time.Now() |
| 214 | + |
| 215 | + for i := 0; i < iterations; i++ { |
| 216 | + makeGarbage() |
| 217 | + } |
| 218 | + |
| 219 | + duration := time.Since(start) |
| 220 | + runtime.ReadMemStats(&m) |
| 221 | + |
| 222 | + fmt.Printf("\n--- Result ---\n") |
| 223 | + fmt.Printf("Total Duration: %v\n", duration) |
| 224 | + fmt.Printf("Average per iteration: %v\n", duration/time.Duration(iterations)) |
| 225 | + fmt.Printf("NumGC: %d (this run: %d)\n", m.NumGC, m.NumGC-initialNumGC) |
| 226 | + fmt.Printf("TotalPause: %v\n", time.Duration(m.PauseTotalNs)) |
| 227 | +} |
| 228 | + |
| 229 | +// 大量の小さなオブジェクトを生成しては破棄する(短寿命オブジェクトの掃き出し負荷テスト) |
| 230 | +func makeGarbage() { |
| 231 | + var head *SmallObject |
| 232 | + for i := 0; i < numObjects; i++ { |
| 233 | + // リンク構造を作ることでスキャナにポインタを追跡させる |
| 234 | + head = &SmallObject{next: head, val: [6]int64{int64(i)}} |
| 235 | + } |
| 236 | + _ = head // keep alive until here |
| 237 | +} |
| 238 | +``` |
| 239 | + |
| 240 | +このコードのポイント: |
| 241 | + |
| 242 | +- **64バイトの小オブジェクト**: Green Teaの最適化対象となるサイズ |
| 243 | +- **リンクリスト構造**: ポインタ追跡を強制し、GCスキャナに負荷をかける |
| 244 | +- **1000万個 × 100回**: 大量のオブジェクト生成と破棄を繰り返す |
| 245 | + |
| 246 | +### 計測結果 |
| 247 | + |
| 248 | +**Go 1.25** と **Go 1.26rc1** でそれぞれ5回ずつ実行し、平均を取りました。 |
| 249 | + |
| 250 | +```sh |
| 251 | +$ for i in {1..5}; do go run gc_bench.go 2>&1 | grep "Total Duration"; done |
| 252 | +Total Duration: 42.809022396s |
| 253 | +Total Duration: 41.469512574s |
| 254 | +Total Duration: 40.128681706s |
| 255 | +Total Duration: 41.512944959s |
| 256 | +Total Duration: 40.281574518s |
| 257 | + |
| 258 | +$ for i in {1..5}; do go1.26rc1 run gc_bench.go 2>&1 | grep "Total Duration"; done |
| 259 | +Total Duration: 36.808893283s |
| 260 | +Total Duration: 35.833063737s |
| 261 | +Total Duration: 36.843265014s |
| 262 | +Total Duration: 37.50556513s |
| 263 | +Total Duration: 36.551616285s |
| 264 | +``` |
| 265 | + |
| 266 | +| バージョン | Run 1 | Run 2 | Run 3 | Run 4 | Run 5 | **平均** | |
| 267 | +|-----------|-------|-------|-------|-------|-------|----------| |
| 268 | +| Go 1.25 | 42.81s | 41.47s | 40.13s | 41.51s | 40.28s | **41.24s** | |
| 269 | +| Go 1.26rc1 | 36.81s | 35.83s | 36.84s | 37.51s | 36.55s | **36.71s** | |
| 270 | + |
| 271 | +### 結果分析 |
| 272 | + |
| 273 | +```txt |
| 274 | +改善: 41.24s → 36.71s |
| 275 | +差分: 4.53秒の短縮 |
| 276 | +改善率: 約11%高速化 |
| 277 | +``` |
| 278 | + |
| 279 | +また、GC回数の減少も計測できました(ex: 129回 → 109回)。これはGreen TeaのFIFOキュー戦略により、GCがより効率的にメモリを回収できるようになったためだと思われます。 |
| 280 | + |
| 281 | +このベンチマークは小オブジェクトのリンクリストという、Green Teaの得意パターンを直撃するワークロードです。実際のアプリケーションでは、ワークロードの特性によって改善率は変動します(公式発表では10〜40%)。 |
| 282 | + |
| 283 | +--- |
| 284 | + |
| 285 | +## まとめ |
| 286 | + |
| 287 | +Go 1.26のGreen Tea GCについて、変更点を整理します。 |
| 288 | + |
| 289 | +| 観点 | 従来のGC | Green Tea GC | |
| 290 | +|------|---------|--------------| |
| 291 | +| 処理単位 | オブジェクト | ページ(スパン) | |
| 292 | +| メモリアクセス | ランダム | シーケンシャル | |
| 293 | +| キュー戦略 | LIFO | FIFO | |
| 294 | +| キャッシュ効率 | — | 大幅に改善 | |
| 295 | + |
| 296 | +### 導入方法 |
| 297 | + |
| 298 | +Go 1.26では何もしなくてもGreen Tea GCが有効です。もし問題が発生した場合は、以下でオプトアウトできます: |
| 299 | + |
| 300 | +```bash |
| 301 | +GOEXPERIMENT=nogreenteagc go build |
| 302 | +``` |
| 303 | + |
| 304 | +### GCトレースの確認 |
| 305 | + |
| 306 | +GCの動作を詳しく確認したい場合は: |
| 307 | + |
| 308 | +```bash |
| 309 | +GODEBUG=gctrace=1 ./your_program |
| 310 | +``` |
| 311 | + |
| 312 | +## おわりに |
| 313 | + |
| 314 | +今回のアイデアの種は、2018年まで遡るようです。公式ブログでは次のように述べられています: |
| 315 | + |
| 316 | +> The seeds of this idea go all the way back to 2018. What's funny is that everyone on the team thinks someone else thought of this initial idea. |
| 317 | +
|
| 318 | +「チームの全員が、このアイデアは他の誰かが考えたものだと思っている」というのも面白いエピソードですね。 |
| 319 | + |
| 320 | +Go 1.26へのアップグレードを検討している方は、ぜひ緑茶GCの効果を体感してみてください。 |
0 commit comments