フューチャー技術ブログ

Go 1.26の新GC「Green Tea(緑茶)」解説

Green_Tea_GC開発のタイムライン *Green Tea GCの開発タイムライン(出典: [Go公式ブログ](https://go.dev/blog/greenteagc))*

はじめに

Go 1.26 がリリースされ、ガベージコレクタ(GC)に大きな変更が加わりました。その名も Green Tea GC(緑茶GC)です。

公式ブログによると、この名前は2024年にGoランタイムチームのAustinが日本でカフェ巡りをしながら、大量の抹茶を飲みつつプロトタイプを開発したことに由来しているそうです。

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.

Green Tea GCは Go 1.25GOEXPERIMENT=greenteagc として実験的に導入され、Go 1.26 からはデフォルトで有効化されました。本記事では、この新しいGCの仕組みと、実際にどの程度の性能改善が得られるのかを解説します。


TL;DR

  • 10〜40%のGCオーバーヘッド削減を実現
  • メモリアクセスの空間的局所性を大幅に改善
  • Intel/AMDの最新CPUではAVX-512によるベクトル加速も利用可能
  • Go 1.26からデフォルト有効(オプトアウト: GOEXPERIMENT=nogreenteagc

GoのGCの基本アルゴリズム

GoのGCは 並行マークスイープ(Concurrent Mark-Sweep) アルゴリズムを採用しています。
まず、従来から使われている基本的な仕組みを確認しましょう。

マーキングの仕組み

GoのGCは、プログラムが使用中のオブジェクトを特定するために「マーキング」を行います。

マーキングでは、「ルート」(グローバル変数やスタック上の変数)から参照をたどり、到達できるオブジェクトに「使用中」の印をつけていきます。印がつかなかったオブジェクトは、プログラムから到達不可能なのでGC対象となります。

処理の流れは次のとおりです:

  1. ルートから参照されているオブジェクトをワークリストに追加
  2. ワークリストからオブジェクトを取り出し、その中のポインタを調べる(これを「スキャン」と呼ぶ)
  3. 見つけたポインタが指すオブジェクトをワークリストに追加
  4. ワークリストが空になるまで繰り返す
【マーキング処理の流れ】

ルート


┌───────┐
│ A │ ←── スタート
└───────┘
│ │
▼ ▼
┌───────┐ ┌───────┐
│ B │ │ C │
└───────┘ └───────┘


┌───────┐
│ D │
└───────┘

ワークリストの変化:
Step 1: [A] ← ルートからAを追加
Step 2: [B, C] ← Aをスキャン → B, Cを発見
Step 3: [C, D] ← Bをスキャン → Dを発見
Step 4: [D] ← Cをスキャン → 新規なし
Step 5: [] ← Dをスキャン → 完了

従来GCの問題点

上記のマーキング処理は、公式ブログで「グラフフラッド(graph flood)」と呼ばれています。ポインタをたどってオブジェクトを次々と訪問していく処理です。

しかし、この方法には問題がありました。ワークリストからオブジェクトを取り出してスキャンするたびに、メモリ上のまったく異なる場所にジャンプしてしまうのです。

メモリ空間(ページ単位で区切られている)

ページ1 ページ2 ページ3 ページ4 ページ5
┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐
│ [A] │ │ [B] │ │ │ │ [D] │ │ [C] │
│ │ │ │ │ │ │ │ │ │
└───────┘ └───────┘ └───────┘ └───────┘ └───────┘

ポインタの参照関係: A → C → B → D
(AがCを参照、CがBを参照、BがDを参照)

GCのアクセス先: ページ1 → 5 → 2 → 4 とジャンプ

ポインタをたどる順序とメモリ上の配置は無関係なため、あちこちに飛び回ることになってしまいます。

CPUには「キャッシュ」という高速な一時メモリがあります。メインメモリ(RAM)へのアクセスは遅いため、よく使うデータをキャッシュに置いて高速化しています。近くのメモリを連続してアクセスすればキャッシュが効きますが、離れた場所をランダムにアクセスすると、毎回メインメモリまで取りに行く必要があります。

公式ブログでは、この状況を「高速道路ではなく市街地を走るようなもの」と表現しています。先が見えず次に何が起こるか予測できないため、CPUは本来の性能を発揮できません。

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.

具体的な数字で見ると(公式ブログより):

  • GC時間の 90% がマーキングに費やされる
  • そのマーキング時間の 35%以上 がメモリアクセス待ち(ストール)
  • メインメモリへのアクセスはキャッシュの 最大100倍遅い

Green Teaの仕組み

Green Tea GCの核心アイデアは非常にシンプルです:

「オブジェクト単位ではなく、ページ(スパン)単位で作業する」

ページ蓄積戦略

従来のGCは、ポインタを見つけるとすぐにそのオブジェクトをスキャンしていました。Green Teaでは異なるアプローチを取ります。

  1. ポインタを発見したら、ターゲットオブジェクトが存在するページ全体をワークリストに追加
  2. ページがキューで待機している間に、同じページ内の他のオブジェクトも蓄積
  3. ページを処理する時に、蓄積されたすべてのオブジェクトをメモリ順序で一括スキャン
【従来のGC】オブジェクト単位でスキャン → ランダムアクセス

ページ1 ページ2 ページ3 ページ4
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ ① │ │ ③ │ │ ② │ │ ④ │
└─────────┘ └─────────┘ └─────────┘ └─────────┘
│ ↑ ↑ ↑
└────────────┼────────────┘ │
└─────────────────────────┘
アクセス順: ① → ② → ③ → ④(ページ間をジャンプ)


【Green Tea GC】ページ単位でスキャン → シーケンシャルアクセス

ページ1 ページ2 ページ3 ページ4
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ ○ ○ ○ │→│ ○ ○ ○ │→│ ○ ○ ○ │→│ ○ ○ ○ │
└─────────┘ └─────────┘ └─────────┘ └─────────┘
アクセス順: ページ1内を全部 → ページ2内を全部 → ...

内部的には、各オブジェクトに「発見済み/スキャン済み」のビットを持たせることで、同じページ内のオブジェクトを効率的に追跡しています。

FIFOキュー

従来のGCはLIFO(スタック)でワークリストを管理していましたが、Green TeaはFIFO(キュー)を採用しています。

  • LIFO(従来のGC): 最後に入れたものを最初に処理 → 蓄積する時間がない
  • FIFO(Green Tea GC): 最初に入れたものを後で処理 → 待機中に同じページの他オブジェクトが蓄積される

ベクトル加速

2020年以降の新しいIntel/AMD CPUでは、CPU内蔵の高速演算機能(ベクトル命令)を活用して、追加で約10%の性能改善が得られます。

詳細は公式ドキュメントを参照してください:

なお、Green Teaは512バイト以下の小オブジェクトに最適化されており、各プロセッサが独自のスパンキューを持つことで、多コア環境でも効率的にスケールします。


実際に動作させて、計測してみる

実際にGreen Tea GCの効果を確認するため、GC負荷の高いベンチマークプログラムを作成して計測してみました。

ベンチマークコード

gc_bench.go
package main

import (
"fmt"
"runtime"
"time"
)

type SmallObject struct {
next *SmallObject
val [6]int64 // 48 bytes + pointer = ~64 bytes block
}

const (
numObjects = 10_000_000 // 1000万個のオブジェクト
iterations = 100
)

func main() {
fmt.Println("Starting GC Benchmark...")

var m runtime.MemStats
runtime.ReadMemStats(&m)
initialNumGC := m.NumGC

start := time.Now()

for i := 0; i < iterations; i++ {
makeGarbage()
}

duration := time.Since(start)
runtime.ReadMemStats(&m)

fmt.Printf("\n--- Result ---\n")
fmt.Printf("Total Duration: %v\n", duration)
fmt.Printf("Average per iteration: %v\n", duration/time.Duration(iterations))
fmt.Printf("NumGC: %d (this run: %d)\n", m.NumGC, m.NumGC-initialNumGC)
fmt.Printf("TotalPause: %v\n", time.Duration(m.PauseTotalNs))
}

// 大量の小さなオブジェクトを生成しては破棄する(短寿命オブジェクトの掃き出し負荷テスト)
func makeGarbage() {
var head *SmallObject
for i := 0; i < numObjects; i++ {
// リンク構造を作ることでスキャナにポインタを追跡させる
head = &SmallObject{next: head, val: [6]int64{int64(i)}}
}
_ = head // keep alive until here
}

このコードのポイント:

  • 64バイトの小オブジェクト: Green Teaの最適化対象となるサイズ
  • リンクリスト構造: ポインタ追跡を強制し、GCスキャナに負荷をかける
  • 1000万個 × 100回: 大量のオブジェクト生成と破棄を繰り返す

計測結果

Go 1.25Go 1.26rc1 でそれぞれ5回ずつ実行し、平均を取りました。

$ for i in {1..5}; do go run gc_bench.go 2>&1 | grep "Total Duration"; done
Total Duration: 42.809022396s
Total Duration: 41.469512574s
Total Duration: 40.128681706s
Total Duration: 41.512944959s
Total Duration: 40.281574518s

$ for i in {1..5}; do go1.26rc1 run gc_bench.go 2>&1 | grep "Total Duration"; done
Total Duration: 36.808893283s
Total Duration: 35.833063737s
Total Duration: 36.843265014s
Total Duration: 37.50556513s
Total Duration: 36.551616285s
バージョン Run 1 Run 2 Run 3 Run 4 Run 5 平均
Go 1.25 42.81s 41.47s 40.13s 41.51s 40.28s 41.24s
Go 1.26rc1 36.81s 35.83s 36.84s 37.51s 36.55s 36.71s

結果分析

改善: 41.24s → 36.71s
差分: 4.53秒の短縮
改善率: 約11%高速化

また、GC回数の減少も計測できました(ex: 129回 → 109回)。これはGreen TeaのFIFOキュー戦略により、GCがより効率的にメモリを回収できるようになったためだと思われます。

このベンチマークは小オブジェクトのリンクリストという、Green Teaの得意パターンを直撃するワークロードです。実際のアプリケーションでは、ワークロードの特性によって改善率は変動します(公式発表では10〜40%)。


まとめ

Go 1.26のGreen Tea GCについて、変更点を整理します。

観点 従来のGC Green Tea GC
処理単位 オブジェクト ページ(スパン)
メモリアクセス ランダム シーケンシャル
キュー戦略 LIFO FIFO
キャッシュ効率 大幅に改善

導入方法

Go 1.26では何もしなくてもGreen Tea GCが有効です。もし問題が発生した場合は、以下でオプトアウトできます:

GOEXPERIMENT=nogreenteagc go build

GCトレースの確認

GCの動作を詳しく確認したい場合は:

GODEBUG=gctrace=1 ./your_program

おわりに

今回のアイデアの種は、2018年まで遡るようです。公式ブログでは次のように述べられています:

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.

「チームの全員が、このアイデアは他の誰かが考えたものだと思っている」というのも面白いエピソードですね。

Go 1.26へのアップグレードを検討している方は、ぜひ緑茶GCの効果を体感してみてください。