Skip to content

Commit 80fa025

Browse files
authored
Merge pull request #1753 from future-architect/feature
Go1.26 GC
2 parents 996870e + 457bea7 commit 80fa025

3 files changed

Lines changed: 320 additions & 0 deletions

File tree

Lines changed: 320 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,320 @@
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の効果を体感してみてください。
498 KB
Loading
42.1 KB
Loading

0 commit comments

Comments
 (0)