
2026/07/25 16:55
Go の新しいガベージコレクションがヒープを走査していく様子
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
Go の新しい Green Tea ゴー・コレクター(v1.26 以降のデフォルト)と C# を比較した評価から得られる主な示唆は、CPU キャッシュ効率とメモリアン断片化の間にある明確なトレードオフである。Green Tea は連続したスパンへの割り当てを最適化することでキャッシュミスが大幅に減少するが、オブジェクトを解放する際に移動やコンパクト化を行うことはできない。その結果、C# の移動式コレクターが積極的に関与してヒープをコンパクト化し OS に散在ページを返すのと異なり、Go は大量の解放後(例:90% のオブジェクトを解放)も未解放オブジェクトがばら撒かれたままとなる。
裸の金属 x86 システムでのテストにより、この断片化は介入なしに持続することが確認された。しかし、開発者は
unsafe ポインタを使用して残存するオブジェクトを手動でパッケージ化することでこの制限を回避し、実質的にコンパクト化を模倣してシステムリソースを取り戻すことができる。結論として、Go は真の CPU パフォーマンス向上を提供するものの、複雑な回避策なしにヒープをコンパクト化できないという Go の不具合は依然として大きな制約であり、長期的動作を必要とし効率的なメモリ回収を要求するアプリケーションにおいては、C# が優れたアーキテクチャ上の振る舞いを提供することが組織にとって認識すべき点である。本文
ソフトウェアインフラ:Go の新 Garbage Collector「Green Tea」とヒープの歩き方
著者:フィル・エイトン
日付:2026 年 7 月 19 日
本記事は購読者限定で先行公開いたします。ご支援により、このような記事の発表が可能となっています。ありがとうございます。
導入:Go のメモリ管理の変化
昨年にリリースされた Go 1.25 では、新しい garbage collector(GC)である「Green Tea」が導入されました。数ヶ月前にリリースされた Go 1.26 からは、これがデフォルトとなりました。
- 受容的なプログラム: Green Tea を採用することで恩恵を受けられるプログラムが存在します。
- 課題となるプログラム: Go の旧来型の「残存 garbage collector」が抱える非移動型コレクターによるスパーズ(不連続)なページの回収不能という問題を引き起こすケースについても考察します。
メモリ管理の基本:サイズ階層化とスパン
Go は、メモリの管理において以下の原則を採用しています。
- サイズ segregated アロケーション: 同じサイズクラスのオブジェクトをまとめます。
- ページ単位のアロケーション: オブジェクトのサイズを上 nearest なクラスへ丸め上げ、1 つまたは複数の 8KiB ページ(Go の用語では「スパン」)からなる連続領域内に配置します。
- この手法は Go のアロケータ由来である tcmalloc など、一般的な malloc インプリメンテーションでも見られます。
ヒープの可視化:Go vs C#
異なるサイズのオブジェクトをランダムにアロケートし、その後ヒープアドレスを確認することで、各言語のアロケーション戦略の違いを観察します。
- 方法: アドレス空間を 32 バイトずつ移動させ、そのタイミングで文字(
)を出力して視覚化します。.
インストール手順
Go と C# を環境にインストールします。
sudo apt update -y sudo apt-get install -y dotnet-sdk-10.0 curl -fsSL https://go.dev/dl/go1.26.0.linux-amd64.tar.gz | sudo tar -C /usr/local -xz export PATH=$PATH:/usr/local/go/bin
擬似コードの実装例
Go でヒープの可視化
Go の実装では、オブジェクトが移動しないため、GC を実行する前後でも配置は同一のままです。
実行コマンド:
go run heapwalk.go
出力結果(抜粋):
=== pass 0 (base 0xba4841580c0) === 0xba4841580c0 M-M-M-M-M-M-M-M-M-M-M-M-M-M-M-M-M-M-M-M-M-M-M-M-M-M-M-M-M-M- ... 0xba4841add40 ..........................L---L---L---L---L---L---L---L---L- === pass 1 (base 0xba4841580c0) === // GC を実行しても配置は変化せず、オブジェクトの移動が発生していません。
C# でヒープの可視化
C# では同じサイズのオブジェクトがグループ化されていないことが観察されます。
ビルドと実行:
dotnet run HeapWalk.cs
出力結果(抜粋):
=== pass 0 (base 0x7aea1080a1e0) === 0x7aea1080a1e0 L---L---L---M-M-SL---L---SM-L---M-L---L---L---M-L---SSL // ランダムにアロケートされたオブジェクトが混在しています。
比較結果:
- Go: 同じサイズクラスのオブジェクトは互いに隣接して配置され、ランダムなアロケーションでも秩序立っています。
- C#: オブジェクトは移動せず、ランダムにアロケートされたままです。(別ワークロードでは C# がメモリ内でもオブジェクトを移動させる場合もありますが、デフォルトでは非移動です。)
マーク&スイープ:キャッシュフレンドリーさの検証
Go の Green Tea GC は、従来の「ポインタを追跡する」というアプローチとは異なる動作をとります。
- Green Tea の仕組み: スパン全体を走査し、発見したポインタに基づいて後続のスパンをキューイングします。これにより、ランダムなメモリアccess を抑え、キャッシュフレンドリーな動作を実現しています。
検証ワークロード:パッキング vs スカータリング
- packed: オブジェクトの参照先も隣接したメモリ領域にある(キャッシュフレンドリー)。
- scattered: 参照先がランダムに配置されている(キャッシュ unfriendly)。
パフォーマンス比較結果(L3 キャッシュミス率など):
- Green Tea GC は、古い GC に比べて実行時間の改善が見られます。
- 意外な事実:
で測った L3 キャッシュミスの割合は、新しい GC で増加しているように見えますが、これはL1/L2 キャッシュレベルの動作についての直接情報ではないためです。perf
正規化指標(MPKI)による分析
命令数に対するキャッシュミス率(Misses Per Kilo Instructions: MPKI)で比較すると、以下の結果が出ました。
| バージョン | ワークロード | L1-MPKI | L3-MPKI |
|---|---|---|---|
| old_gc | packed | 2.23 | 1.61 |
| old_gc | scattered | 31.57 | 12.76 |
| greentea | packed | 1.98 (↓) | 1.81 (↑) |
| greentea | scattered | 14.06 (↓) | 15.39 (↑) |
結論:
- L1 MPKI(最も重要な指標)は顕著に減少しています。
- 多くの読み込みが L1/L2 キャッシュに収容され、L3 に到達する必要がなくなりました。
- Green Tea GC は、少なくともキャッシュ改善の分野で劇的な向上をもたらしています。
最悪ケースの片付け:スパーズなページの問題
Go の非移動型コレクションでは、オブジェクトを解放してもメモリ領域(スパン)自体は未使用のまま残ることがあります。
シナリオ
- ランダムにサイズが異なるオブジェクトをアロケート。
- その 90% を解放(
にする)。nil - GC を実行し、OS にメモリを返す (
)。FreeOSMemory
結果:Go (Green Tea) の挙動
- 生き残ったオブジェクトは移動しないため、解放された領域と混在したスパンが解放されません。
- メモリ使用量は期待通り減少せず、不連続なページが残ります。
出力(抜粋):
=== pass 1 (5000 live, ...) === spans pinned 463 (46 if the survivors were packed) // 理想値は 46 個だが、463 のスパンが使われています。 HeapReleased 5512.0 KiB
対策:手動でのパッキング(Go)
オブジェクトをコピーして同じ位置に再配置することでフラグメンテーションを解消できますが、これは Go ランタイムの機能ではありません。
出力(手動パッキング後):
=== pass 2 (5000 live, base shifted) === // オブジェクトが連続した領域にまとめられています。 spans pinned 48 (47 if the survivors were packed) // スパン数が大幅に減少。 HeapReleased 9040.0 KiB // OS に返されるメモリが増加。
C# の片付けとの比較
C# では、GC がデフォルトで**フルコンパクション(Full Compaction)**を行うため、解放されたオブジェクトの処理に大きな違いが出ます。
C# の挙動
を実行すると、解放可能なメモリは即座に整理され、OS に返されます。GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive)- 生き残ったオブジェクトが移動し、連続した領域になります。
出力(抜粋):
=== pass 1 (5000 live, ...) === 8KiB chunks 47 (47 if the survivors were packed) // パッキング済みとして最適化されています。 // C# の GC は自動的にスパーズなページの問題を解決しています。
結論
- Go: Green Tea GC はキャッシュパフォーマンスを劇的に向上させました。しかし、非移動型であるため、解放後のメモリ整理(Defragmentation)には開発者の介入や手動パッキングが必要です。
- C#: GC が自動的にオブジェクトを移動・整理するため、メモリの不連続な領域が残りやすく、OS に返せなくなります。ただし、フルコレクション実行後は常に整理された状態になります。
Go の Green Tea GC は、キャッシュ性能を最適化する一方で、メモリ配置の整理については依然として開発者の理解と管理が必要であることを示しています。