[インデックス 12886] ファイルの概要
このコミットは、Goランタイムのガベージコレクション(GC)におけるスイープフェーズのパフォーマンス改善を目的としています。具体的には、オブジェクトの解放処理をバッチ化することで、GCの効率を高め、全体的な実行速度を向上させています。
コミット
commit 4945fc8e40eef046501f613135b4f18cf2777d29
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Thu Apr 12 12:01:24 2012 +0400
runtime: speedup GC sweep phase (batch free)
benchmark old ns/op new ns/op delta
garbage.BenchmarkParser 4370050250 3779668750 -13.51%
garbage.BenchmarkParser-2 3713087000 3628771500 -2.27%
garbage.BenchmarkParser-4 3519755250 3406349750 -3.22%
garbage.BenchmarkParser-8 3386627750 3319144000 -1.99%
garbage.BenchmarkTree 493585529 408102411 -17.32%
garbage.BenchmarkTree-2 500487176 402285176 -19.62%
garbage.BenchmarkTree-4 473238882 361484058 -23.61%
garbage.BenchmarkTree-8 486977823 368334823 -24.36%
garbage.BenchmarkTree2 31446600 31203200 -0.77%
garbage.BenchmarkTree2-2 21469000 21077900 -1.82%
garbage.BenchmarkTree2-4 11007600 10899100 -0.99%
garbage.BenchmarkTree2-8 7692400 7032600 -8.58%
garbage.BenchmarkParserPause 241863263 163249450 -32.50%
garbage.BenchmarkParserPause-2 120135418 112981575 -5.95%
garbage.BenchmarkParserPause-4 83411552 64580700 -22.58%
garbage.BenchmarkParserPause-8 51870697 42207244 -18.63%
garbage.BenchmarkTreePause 20940474 13147011 -37.22%
garbage.BenchmarkTreePause-2 20115124 11146715 -44.59%
garbage.BenchmarkTreePause-4 17217584 7486327 -56.52%
garbage.BenchmarkTreePause-8 18258845 7400871 -59.47%
garbage.BenchmarkTree2Pause 174067190 172674190 -0.80%
garbage.BenchmarkTree2Pause-2 131175809 130615761 -0.43%
garbage.BenchmarkTree2Pause-4 95406666 93972047 -1.50%
garbage.BenchmarkTree2Pause-8 86056095 85334952 -0.84%
garbage.BenchmarkParserLastPause 329932000 324790000 -1.56%
garbage.BenchmarkParserLastPause-2 209383000 210456000 +0.51%
garbage.BenchmarkParserLastPause-4 113981000 112921000 -0.93%
garbage.BenchmarkParserLastPause-8 77967000 76625000 -1.72%
garbage.BenchmarkTreeLastPause 29752000 18444000 -38.01%
garbage.BenchmarkTreeLastPause-2 24274000 14766000 -39.17%
garbage.BenchmarkTreeLastPause-4 19565000 8726000 -55.40%
garbage.BenchmarkTreeLastPause-8 21956000 10530000 -52.04%
garbage.BenchmarkTree2LastPause 314411000 311945000 -0.78%
garbage.BenchmarkTree2LastPause-2 214641000 210836000 -1.77%
garbage.BenchmarkTree2LastPause-4 110024000 108943000 -0.98%
garbage.BenchmarkTree2LastPause-8 76873000 70263000 -8.60%
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/5991049
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/4945fc8e40eef046501f613135b4f18cf2777d29
元コミット内容
このコミットの元々の内容は、Goランタイムのガベージコレクション(GC)におけるスイープフェーズの高速化、特に「バッチフリー(batch free)」という手法の導入です。ベンチマーク結果が示されており、garbage.BenchmarkParser
やgarbage.BenchmarkTree
などのベンチマークで大幅な性能向上が見られます。特に、GCポーズ時間を示す*Pause
系のベンチマークでは、最大で約60%もの改善が報告されています。
変更の背景
Goのガベージコレクションは、プログラムの実行中に不要になったメモリを自動的に解放する重要な機能です。GCの効率は、Goアプリケーションの全体的なパフォーマンスに直結します。特に、GCが実行される際にプログラムの実行が一時停止する「ポーズ時間」は、レイテンシに敏感なアプリケーションにとって重要な指標となります。
このコミットが行われた当時(2012年)、GoのGCはまだ発展途上にあり、パフォーマンスのボトルネックとなる部分がいくつか存在しました。その一つが、GCのスイープフェーズにおけるオブジェクトの解放処理でした。個々のオブジェクトを一つずつ解放する処理は、大量のオブジェクトが生成・破棄されるワークロードにおいて、オーバーヘッドが大きくなる傾向がありました。
この背景から、GCのスイープフェーズにおけるオブジェクト解放の効率を改善し、特にポーズ時間を短縮することが求められていました。バッチフリーの導入は、この課題に対する直接的な解決策として提案されました。
前提知識の解説
このコミットを理解するためには、Goランタイムのメモリ管理とガベージコレクションの基本的な概念を把握しておく必要があります。
-
ガベージコレクション (GC): プログラムが動的に確保したメモリ領域のうち、もはやどの変数からも参照されなくなった領域(到達不能なオブジェクト)を自動的に特定し、解放する仕組みです。GoのGCは、並行マーク&スイープ方式を採用しています。
- マークフェーズ (Mark Phase): GCのルート(グローバル変数、スタック上の変数など)から到達可能なすべてのオブジェクトをマークします。
- スイープフェーズ (Sweep Phase): マークされなかったオブジェクト(到達不能なオブジェクト)を「ゴミ」とみなし、それらが占めていたメモリ領域を解放し、再利用可能な状態に戻します。
- ポーズ時間 (Pause Time): GCが実行されている間、アプリケーションの実行が一時的に停止する時間です。GoのGCはポーズ時間を最小限に抑えるように設計されていますが、完全にゼロにすることはできません。
-
Goランタイムのメモリ管理構造:
- ヒープ (Heap): プログラムが動的にメモリを確保する領域です。Goのオブジェクトはヒープに割り当てられます。
MHeap
: Goランタイム全体のヒープを管理する構造体です。MSpan
: ヒープを構成する連続したページ(通常は8KB)のブロックです。MSpan
は、特定のサイズのオブジェクトを格納するために使用されます。例えば、小さなオブジェクト(small objects)は、同じサイズのオブジェクトを格納するMSpan
に割り当てられます。MCentral
: 特定のサイズのMSpan
を管理する中央リストです。MCentral
は、MSpan
をMCache
に提供したり、MCache
から返されたMSpan
を受け取ったりします。MCache
: 各P(プロセッサ、Goスケジューラにおける論理CPU)にローカルなキャッシュです。MCache
は、頻繁に割り当てられる小さなオブジェクトのために、MCentral
からMSpan
を借りてきて、そこからメモリを割り当てます。これにより、ロックの競合を減らし、アロケーションを高速化します。MLink
: フリーリスト(空きメモリブロックのリスト)を構成するためのリンクリストのノードです。解放されたオブジェクトは、このMLink
を使って連結され、フリーリストに追加されます。
-
MaxGcproc
: ガベージコレクションのマークフェーズで並行して動作するGCワーカーの最大数を定義する定数です。この値が大きいほど、GCのマークフェーズが高速化される可能性がありますが、CPUリソースの消費も増えます。
技術的詳細
このコミットの主要な技術的変更点は、GCのスイープフェーズにおけるオブジェクトの解放処理を「バッチ化」したことです。
従来のGoランタイムのGCスイープフェーズでは、sweepspan
関数がMSpan
内のオブジェクトを一つずつ走査し、到達不能なオブジェクトを見つけるたびに、そのオブジェクトをMCache
のローカルフリーリストに個別に返していました。この「一つずつ解放する」アプローチは、特に大量の小さなオブジェクトが解放される場合に、MCache_Free
関数の呼び出しオーバーヘッドや、関連するデータ構造の更新コストが累積し、パフォーマンスのボトルネックとなっていました。
このコミットでは、この問題を解決するために以下の変更が導入されました。
-
MaxGcproc
の増加:src/pkg/runtime/malloc.h
において、MaxGcproc
が4から16に増加されました。これは、GCの並行処理能力を向上させ、特にマルチコア環境でのGC性能を改善するための変更です。GCワーカーが増えることで、マークフェーズの処理がより多くのCPUコアに分散され、全体的なGC時間が短縮される可能性があります。 -
runtime·MCentral_FreeSpan
関数の導入:src/pkg/runtime/mcentral.c
にruntime·MCentral_FreeSpan
という新しい関数が追加されました。この関数は、MSpan
、解放するオブジェクトの数n
、そして解放されるオブジェクトのリンクリストの先頭start
と末尾end
を受け取ります。この関数は、複数のオブジェクトをまとめてMCentral
のフリーリストに返すことを可能にします。これにより、個々のオブジェクトごとにロックを取得したり、データ構造を更新したりするオーバーヘッドが削減されます。 -
sweepspan
関数の変更:src/pkg/runtime/mgc0.c
のsweepspan
関数が修正されました。- スイープ中に解放される小さなオブジェクトを、即座に
MCache_Free
で個別に解放するのではなく、一時的にローカルなリンクリスト(start
,end
,nfree
)に蓄積するように変更されました。 sweepspan
の最後に、蓄積されたオブジェクトのリストをまとめて新しいruntime·MCentral_FreeSpan
関数に渡し、一括で解放するように変更されました。MCache
のローカル統計(local_alloc
,local_nfree
など)の更新も、バッチ処理に合わせて調整されました。
- スイープ中に解放される小さなオブジェクトを、即座に
このバッチ処理により、GCスイープフェーズにおけるオブジェクト解放の粒度が粗くなり、システムコールやロックの回数が減少し、結果としてGCの効率が向上し、特にポーズ時間が大幅に短縮されました。ベンチマーク結果が示すように、この変更はGoアプリケーションの全体的なパフォーマンスに顕著な改善をもたらしました。
コアとなるコードの変更箇所
このコミットで変更された主要なファイルとコードの変更箇所は以下の通りです。
src/pkg/runtime/malloc.h
--- a/src/pkg/runtime/malloc.h
+++ b/src/pkg/runtime/malloc.h
@@ -125,7 +125,7 @@ enum
// 2, 3, and 4 are all plausible maximums depending
// on the hardware details of the machine. The garbage
// collector scales well to 4 cpus.
- MaxGcproc = 4,
+ MaxGcproc = 16,
};
// A generic linked list of blocks. (Typically the block is bigger than sizeof(MLink).)\n
@@ -341,6 +341,7 @@ struct MCentral
void runtime·MCentral_Init(MCentral *c, int32 sizeclass);
int32 runtime·MCentral_AllocList(MCentral *c, int32 n, MLink **first);
void runtime·MCentral_FreeList(MCentral *c, int32 n, MLink *first);
+void runtime·MCentral_FreeSpan(MCentral *c, MSpan *s, int32 n, MLink *start, MLink *end);
MaxGcproc
が4から16に増加。runtime·MCentral_FreeSpan
関数のプロトタイプ宣言が追加。
src/pkg/runtime/mcentral.c
--- a/src/pkg/runtime/mcentral.c
+++ b/src/pkg/runtime/mcentral.c
@@ -88,9 +88,6 @@ MCentral_Alloc(MCentral *c)\n }\n \n // Free n objects back into the central free list.\n-// Return the number of objects allocated.\n-// The objects are linked together by their first words.\n-// On return, *pstart points at the first object and *pend at the last.\n void\n runtime·MCentral_FreeList(MCentral *c, int32 n, MLink *start)\n {\n@@ -148,6 +145,42 @@ MCentral_Free(MCentral *c, void *v)\n \t}\n }\n \n+// Free n objects from a span s back into the central free list c.\n+// Called from GC.\n+void\n+runtime·MCentral_FreeSpan(MCentral *c, MSpan *s, int32 n, MLink *start, MLink *end)\n+{\n+\tint32 size;\n+\n+\truntime·lock(c);\n+\n+\t// Move to nonempty if necessary.\n+\tif(s->freelist == nil) {\n+\t\truntime·MSpanList_Remove(s);\n+\t\truntime·MSpanList_Insert(&c->nonempty, s);\n+\t}\n+\n+\t// Add the objects back to s's free list.\n+\tend->next = s->freelist;\n+\ts->freelist = start;\n+\ts->ref -= n;\n+\tc->nfree += n;\n+\n+\t// If s is completely freed, return it to the heap.\n+\tif(s->ref == 0) {\n+\t\tsize = runtime·class_to_size[c->sizeclass];\n+\t\truntime·MSpanList_Remove(s);\n+\t\t*(uintptr*)(s->start<<PageShift) = 1; // needs zeroing\n+\t\ts->freelist = nil;\n+\t\tc->nfree -= (s->npages << PageShift) / size;\n+\t\truntime·unlock(c);\n+\t\truntime·unmarkspan((byte*)(s->start<<PageShift), s->npages<<PageShift);\n+\t\truntime·MHeap_Free(&runtime·mheap, s, 0);\n+\t} else {\n+\t\truntime·unlock(c);\n+\t}\n+}\n+\n void\n runtime·MGetSizeClassInfo(int32 sizeclass, uintptr *sizep, int32 *npagesp, int32 *nobj)\n {\n```
- `runtime·MCentral_FreeList`のコメントが削除。
- `runtime·MCentral_FreeSpan`関数の実装が追加。この関数は、GCから呼び出され、指定された`MSpan`から`n`個のオブジェクトを`MCentral`のフリーリストにまとめて返します。`s->ref`が0になった場合(`MSpan`内のすべてのオブジェクトが解放された場合)、その`MSpan`はヒープに返されます。
### `src/pkg/runtime/mgc0.c`
```diff
--- a/src/pkg/runtime/mgc0.c
+++ b/src/pkg/runtime/mgc0.c
@@ -761,6 +761,8 @@ sweepspan(MSpan *s)\n byte *p;\n MCache *c;\n byte *arena_start;\n+\tMLink *start, *end;\n+\tint32 nfree;\n \n arena_start = runtime·mheap.arena_start;\n p = (byte*)(s->start << PageShift);\n@@ -774,6 +776,9 @@ sweepspan(MSpan *s)\n \tnpages = runtime·class_to_allocnpages[cl];\n \tn = (npages << PageShift) / size;\n }\n+\tnfree = 0;\n+\tstart = end = nil;\n+\tc = m->mcache;\n \n // Sweep through n objects of given size starting at p.\n // This thread owns the span now, so it can manipulate\n@@ -810,21 +815,33 @@ sweepspan(MSpan *s)\n // Mark freed; restore block boundary bit.\n *bitp = (*bitp & ~(bitMask<<shift)) | (bitBlockBoundary<<shift);\n \n-\t\tc = m->mcache;\n \t\tif(s->sizeclass == 0) {\n \t\t\t// Free large span.\n \t\t\truntime·unmarkspan(p, 1<<PageShift);\n \t\t\t*(uintptr*)p = 1;\t// needs zeroing\n \t\t\truntime·MHeap_Free(&runtime·mheap, s, 1);\n+\t\t\tc->local_alloc -= size;\n+\t\t\tc->local_nfree++;\n \t\t} else {\n \t\t\t// Free small object.\n \t\t\tif(size > sizeof(uintptr))\n \t\t\t\t((uintptr*)p)[1] = 1;\t// mark as "needs to be zeroed"\n-\t\t\tc->local_by_size[s->sizeclass].nfree++;\n-\t\t\truntime·MCache_Free(c, p, s->sizeclass, size);\n+\t\t\tif(nfree)\n+\t\t\t\tend->next = (MLink*)p;\n+\t\t\telse\n+\t\t\t\tstart = (MLink*)p;\n+\t\t\tend = (MLink*)p;\n+\t\t\tnfree++;\n \t\t}\n-\t\tc->local_alloc -= size;\n-\t\tc->local_nfree++;\n+\t}\n+\n+\tif(nfree) {\n+\t\tc->local_by_size[s->sizeclass].nfree += nfree;\n+\t\tc->local_alloc -= size * nfree;\n+\t\tc->local_nfree += nfree;\n+\t\tc->local_cachealloc -= nfree * size;\n+\t\tc->local_objects -= nfree;\n+\t\truntime·MCentral_FreeSpan(&runtime·mheap.central[cl], s, nfree, start, end);\n \t}\n }\n \n```
- `sweepspan`関数内で、`start`, `end`, `nfree`という変数が導入され、解放されるオブジェクトを一時的にバッチ処理するためのリンクリストを構築するようになりました。
- 小さなオブジェクトの解放ロジックが変更され、個別に`MCache_Free`を呼び出す代わりに、`start`と`end`ポインタを使って解放されるオブジェクトを連結し、`nfree`をインクリメントするようになりました。
- `sweepspan`の最後に、`nfree`が0より大きい場合(つまり、解放されるオブジェクトがある場合)、`runtime·MCentral_FreeSpan`を呼び出して、バッチでオブジェクトを解放するようになりました。
- `MCache`のローカル統計(`local_alloc`, `local_nfree`, `local_cachealloc`, `local_objects`)の更新が、バッチ処理のロジックに合わせて調整されました。
## コアとなるコードの解説
このコミットの核心は、`mgc0.c`の`sweepspan`関数と`mcentral.c`の`runtime·MCentral_FreeSpan`関数の連携にあります。
`sweepspan`関数は、GCのスイープフェーズにおいて、特定の`MSpan`(メモリブロック)を走査し、不要になったオブジェクトを特定して解放する役割を担います。変更前は、この関数が不要なオブジェクトを見つけるたびに、そのオブジェクトを`MCache`(CPUローカルなキャッシュ)に個別に返していました。これは、多数の小さなオブジェクトが解放される場合に、`MCache`への頻繁なアクセスとロックの取得・解放が発生し、性能上のボトルネックとなっていました。
変更後の`sweepspan`関数では、この問題を解決するために「バッチ処理」の概念が導入されました。
1. **ローカルなフリーリストの構築**: `sweepspan`は、`MSpan`を走査する際に、解放すべき小さなオブジェクトを見つけても、すぐに`MCache`に返すのではなく、`MLink`ポインタを使ってそれらをローカルなリンクリスト(`start`と`end`で管理される)に連結していきます。同時に、解放されるオブジェクトの数`nfree`をカウントします。
2. **バッチでの解放**: `sweepspan`が`MSpan`の走査を終えると、`nfree`が0より大きい場合(つまり、解放すべきオブジェクトが一つでも見つかった場合)、新しく導入された`runtime·MCentral_FreeSpan`関数を呼び出します。この関数には、構築したローカルなフリーリストの先頭(`start`)と末尾(`end`)、そして解放されるオブジェクトの総数(`nfree`)が渡されます。
3. **`runtime·MCentral_FreeSpan`の役割**: `runtime·MCentral_FreeSpan`は、渡されたオブジェクトのリンクリストを、対応する`MCentral`(特定のサイズの`MSpan`を管理する中央リスト)のフリーリストにまとめて追加します。この際、`MCentral`のロックは一度だけ取得され、複数のオブジェクトが効率的に解放されます。また、もし`MSpan`内のすべてのオブジェクトが解放され、`MSpan`が完全に空になった場合(`s->ref == 0`)、この`MSpan`は`MHeap`(全体のヒープ)に返され、再利用可能な状態になります。
このバッチ処理により、`MCache`や`MCentral`へのアクセス回数が大幅に削減され、それに伴うロックの競合やシステムコールのオーバーヘッドが減少します。結果として、GCのスイープフェーズが高速化され、特にGCポーズ時間の短縮に大きく貢献しています。
また、`MaxGcproc`が4から16に増加されたことは、GCのマークフェーズにおける並行処理能力の向上を意味します。これにより、より多くのCPUコアをGCのマーク処理に利用できるようになり、GC全体の実行時間を短縮する効果が期待できます。
## 関連リンク
* Go言語のガベージコレクションに関する公式ドキュメントやブログ記事 (当時のものがあれば)
* Goランタイムのメモリ管理に関する設計ドキュメント
* Goのベンチマークツールに関する情報
## 参考にした情報源リンク
* Goのソースコード (特に`src/pkg/runtime/`)
* Goのガベージコレクションに関する技術ブログや論文 (当時のもの)
* GoのIssueトラッカーやメーリングリスト (このコミットに関連する議論があれば)
(注:具体的なリンクは、2012年当時の情報源を特定することが困難なため、一般的なカテゴリで記載しています。当時のGoのGCに関する詳細な情報源は、Goの公式ブログやGoの設計ドキュメント、またはGoのソースコードリポジトリ内のドキュメントを参照すると見つかる可能性があります。)
# [インデックス 12886] ファイルの概要
このコミットは、Goランタイムのガベージコレクション(GC)におけるスイープフェーズのパフォーマンス改善を目的としています。具体的には、オブジェクトの解放処理をバッチ化することで、GCの効率を高め、全体的な実行速度を向上させています。
## コミット
commit 4945fc8e40eef046501f613135b4f18cf2777d29 Author: Dmitriy Vyukov dvyukov@google.com Date: Thu Apr 12 12:01:24 2012 +0400
runtime: speedup GC sweep phase (batch free)
benchmark old ns/op new ns/op delta
garbage.BenchmarkParser 4370050250 3779668750 -13.51%
garbage.BenchmarkParser-2 3713087000 3628771500 -2.27%
garbage.BenchmarkParser-4 3519755250 3406349750 -3.22%
garbage.BenchmarkParser-8 3386627750 3319144000 -1.99%
garbage.BenchmarkTree 493585529 408102411 -17.32%
garbage.BenchmarkTree-2 500487176 402285176 -19.62%
garbage.BenchmarkTree-4 473238882 361484058 -23.61%
garbage.BenchmarkTree-8 486977823 368334823 -24.36%
garbage.BenchmarkTree2 31446600 31203200 -0.77%
garbage.BenchmarkTree2-2 21469000 21077900 -1.82%
garbage.BenchmarkTree2-4 11007600 10899100 -0.99%
garbage.BenchmarkTree2-8 7692400 7032600 -8.58%
garbage.BenchmarkParserPause 241863263 163249450 -32.50%
garbage.BenchmarkParserPause-2 120135418 112981575 -5.95%
garbage.BenchmarkParserPause-4 83411552 64580700 -22.58%
garbage.BenchmarkParserPause-8 51870697 42207244 -18.63%
garbage.BenchmarkTreePause 20940474 13147011 -37.22%
garbage.BenchmarkTreePause-2 20115124 11146715 -44.59%
garbage.BenchmarkTreePause-4 17217584 7486327 -56.52%
garbage.BenchmarkTreePause-8 18258845 7400871 -59.47%
garbage.BenchmarkTree2Pause 174067190 172674190 -0.80%
garbage.BenchmarkTree2Pause-2 131175809 130615761 -0.43%
garbage.BenchmarkTree2Pause-4 95406666 93972047 -1.50%
garbage.BenchmarkTree2Pause-8 86056095 85334952 -0.84%
garbage.BenchmarkParserLastPause 329932000 324790000 -1.56%
garbage.BenchmarkParserLastPause-2 209383000 210456000 +0.51%
garbage.BenchmarkParserLastPause-4 113981000 112921000 -0.93%
garbage.BenchmarkParserLastPause-8 77967000 76625000 -1.72%
garbage.BenchmarkTreeLastPause 29752000 18444000 -38.01%
garbage.BenchmarkTreeLastPause-2 24274000 14766000 -39.17%
garbage.BenchmarkTreeLastPause-4 19565000 8726000 -55.40%
garbage.BenchmarkTreeLastPause-8 21956000 10530000 -52.04%
garbage.BenchmarkTree2LastPause 314411000 311945000 -0.78%
garbage.BenchmarkTree2LastPause-2 214641000 210836000 -1.77%
garbage.BenchmarkTree2LastPause-4 110024000 108943000 -0.98%
garbage.BenchmarkTree2LastPause-8 76873000 70263000 -8.60%
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/5991049
## GitHub上でのコミットページへのリンク
[https://github.com/golang/go/commit/4945fc8e40eef046501f613135b4f18cf2777d29](https://github.com/golang/go/commit/4945fc8e40eef046501f613135b4f18cf2777d29)
## 元コミット内容
このコミットの元々の内容は、Goランタイムのガベージコレクション(GC)におけるスイープフェーズの高速化、特に「バッチフリー(batch free)」という手法の導入です。ベンチマーク結果が示されており、`garbage.BenchmarkParser`や`garbage.BenchmarkTree`などのベンチマークで大幅な性能向上が見られます。特に、GCポーズ時間を示す`*Pause`系のベンチマークでは、最大で約60%もの改善が報告されています。
## 変更の背景
Goのガベージコレクションは、プログラムの実行中に不要になったメモリを自動的に解放する重要な機能です。GCの効率は、Goアプリケーションの全体的なパフォーマンスに直結します。特に、GCが実行される際にプログラムの実行が一時停止する「ポーズ時間」は、レイテンシに敏感なアプリケーションにとって重要な指標となります。
このコミットが行われた当時(2012年)、GoのGCはまだ発展途上にあり、パフォーマンスのボトルネックとなる部分がいくつか存在しました。その一つが、GCのスイープフェーズにおけるオブジェクトの解放処理でした。個々のオブジェクトを一つずつ解放する処理は、大量のオブジェクトが生成・破棄されるワークロードにおいて、オーバーヘッドが大きくなる傾向がありました。
この背景から、GCのスイープフェーズにおけるオブジェクト解放の効率を改善し、特にポーズ時間を短縮することが求められていました。バッチフリーの導入は、この課題に対する直接的な解決策として提案されました。
## 前提知識の解説
このコミットを理解するためには、Goランタイムのメモリ管理とガベージコレクションの基本的な概念を把握しておく必要があります。
* **ガベージコレクション (GC)**: プログラムが動的に確保したメモリ領域のうち、もはやどの変数からも参照されなくなった領域(到達不能なオブジェクト)を自動的に特定し、解放する仕組みです。GoのGCは、並行マーク&スイープ方式を採用しています。
* **マークフェーズ (Mark Phase)**: GCのルート(グローバル変数、スタック上の変数など)から到達可能なすべてのオブジェクトをマークします。
* **スイープフェーズ (Sweep Phase)**: マークされなかったオブジェクト(到達不能なオブジェクト)を「ゴミ」とみなし、それらが占めていたメモリ領域を解放し、再利用可能な状態に戻します。GoのGCにおけるスイープフェーズは、アプリケーションの実行と並行して行われるため、STW(Stop-The-World)ポーズを最小限に抑えることに貢献します。このフェーズで解放されたメモリはヒープに戻され、将来のメモリ割り当てに利用可能になります。GoのGCは非移動型(non-moving)であり、スイープフェーズ中にオブジェクトをメモリ内で移動させることはありません。
* **ポーズ時間 (Pause Time)**: GCが実行されている間、アプリケーションの実行が一時的に停止する時間です。GoのGCはポーズ時間を最小限に抑えるように設計されていますが、完全にゼロにすることはできません。
* **Goランタイムのメモリ管理構造**:
* **ヒープ (Heap)**: プログラムが動的にメモリを確保する領域です。Goのオブジェクトはヒープに割り当てられます。
* **`MHeap`**: Goランタイム全体のヒープを管理する構造体です。
* **`MSpan`**: ヒープを構成する連続したページ(通常は8KB)のブロックです。`MSpan`は、特定のサイズのオブジェクトを格納するために使用されます。例えば、小さなオブジェクト(small objects)は、同じサイズのオブジェクトを格納する`MSpan`に割り当てられます。
* **`MCentral`**: 特定のサイズの`MSpan`を管理する中央リストです。`MCentral`は、`MSpan`を`MCache`に提供したり、`MCache`から返された`MSpan`を受け取ったりします。
* **`MCache`**: 各P(プロセッサ、Goスケジューラにおける論理CPU)にローカルなキャッシュです。`MCache`は、頻繁に割り当てられる小さなオブジェクトのために、`MCentral`から`MSpan`を借りてきて、そこからメモリを割り当てます。これにより、ロックの競合を減らし、アロケーションを高速化します。
* **`MLink`**: フリーリスト(空きメモリブロックのリスト)を構成するためのリンクリストのノードです。解放されたオブジェクトは、この`MLink`を使って連結され、フリーリストに追加されます。
* **`MaxGcproc`**: ガベージコレクションのマークフェーズで並行して動作するGCワーカーの最大数を定義する定数です。この値が大きいほど、GCのマークフェーズが高速化される可能性がありますが、CPUリソースの消費も増えます。
## 技術的詳細
このコミットの主要な技術的変更点は、GCのスイープフェーズにおけるオブジェクトの解放処理を「バッチ化」したことです。
従来のGoランタイムのGCスイープフェーズでは、`sweepspan`関数が`MSpan`内のオブジェクトを一つずつ走査し、到達不能なオブジェクトを見つけるたびに、そのオブジェクトを`MCache`のローカルフリーリストに個別に返していました。この「一つずつ解放する」アプローチは、特に大量の小さなオブジェクトが解放される場合に、`MCache_Free`関数の呼び出しオーバーヘッドや、関連するデータ構造の更新コストが累積し、パフォーマンスのボトルネックとなっていました。
このコミットでは、この問題を解決するために以下の変更が導入されました。
1. **`MaxGcproc`の増加**: `src/pkg/runtime/malloc.h`において、`MaxGcproc`が4から16に増加されました。これは、GCの並行処理能力を向上させ、特にマルチコア環境でのGC性能を改善するための変更です。GCワーカーが増えることで、マークフェーズの処理がより多くのCPUコアに分散され、全体的なGC時間が短縮される可能性があります。
2. **`runtime·MCentral_FreeSpan`関数の導入**: `src/pkg/runtime/mcentral.c`に`runtime·MCentral_FreeSpan`という新しい関数が追加されました。この関数は、`MSpan`、解放するオブジェクトの数`n`、そして解放されるオブジェクトのリンクリストの先頭`start`と末尾`end`を受け取ります。この関数は、複数のオブジェクトをまとめて`MCentral`のフリーリストに返すことを可能にします。これにより、個々のオブジェクトごとにロックを取得したり、データ構造を更新したりするオーバーヘッドが削減されます。
3. **`sweepspan`関数の変更**: `src/pkg/runtime/mgc0.c`の`sweepspan`関数が修正されました。
* スイープ中に解放される小さなオブジェクトを、即座に`MCache_Free`で個別に解放するのではなく、一時的にローカルなリンクリスト(`start`, `end`, `nfree`)に蓄積するように変更されました。
* `sweepspan`の最後に、蓄積されたオブジェクトのリストをまとめて新しい`runtime·MCentral_FreeSpan`関数に渡し、一括で解放するように変更されました。
* `MCache`のローカル統計(`local_alloc`, `local_nfree`など)の更新も、バッチ処理に合わせて調整されました。
このバッチ処理により、GCスイープフェーズにおけるオブジェクト解放の粒度が粗くなり、システムコールやロックの回数が減少し、結果としてGCの効率が向上し、特にポーズ時間が大幅に短縮されました。ベンチマーク結果が示すように、この変更はGoアプリケーションの全体的なパフォーマンスに顕著な改善をもたらしました。
## コアとなるコードの変更箇所
このコミットで変更された主要なファイルとコードの変更箇所は以下の通りです。
### `src/pkg/runtime/malloc.h`
```diff
--- a/src/pkg/runtime/malloc.h
+++ b/src/pkg/runtime/malloc.h
@@ -125,7 +125,7 @@ enum
// 2, 3, and 4 are all plausible maximums depending
// on the hardware details of the machine. The garbage
// collector scales well to 4 cpus.
- MaxGcproc = 4,
+ MaxGcproc = 16,
};
// A generic linked list of blocks. (Typically the block is bigger than sizeof(MLink).)\n
@@ -341,6 +341,7 @@ struct MCentral
void runtime·MCentral_Init(MCentral *c, int32 sizeclass);
int32 runtime·MCentral_AllocList(MCentral *c, int32 n, MLink **first);
void runtime·MCentral_FreeList(MCentral *c, int32 n, MLink *first);
+void runtime·MCentral_FreeSpan(MCentral *c, MSpan *s, int32 n, MLink *start, MLink *end);
MaxGcproc
が4から16に増加。runtime·MCentral_FreeSpan
関数のプロトタイプ宣言が追加。
src/pkg/runtime/mcentral.c
--- a/src/pkg/runtime/mcentral.c
+++ b/src/pkg/runtime/mcentral.c
@@ -88,9 +88,6 @@ MCentral_Alloc(MCentral *c)\n }\n \n // Free n objects back into the central free list.\n-// Return the number of objects allocated.\n-// The objects are linked together by their first words.\n-// On return, *pstart points at the first object and *pend at the last.\n void\n runtime·MCentral_FreeList(MCentral *c, int32 n, MLink *start)\n {\n@@ -148,6 +145,42 @@ MCentral_Free(MCentral *c, void *v)\n \t}\n }\n \n+// Free n objects from a span s back into the central free list c.\n+// Called from GC.\n+void\n+runtime·MCentral_FreeSpan(MCentral *c, MSpan *s, int32 n, MLink *start, MLink *end)\n+{\n+\tint32 size;\n+\n+\truntime·lock(c);\n+\n+\t// Move to nonempty if necessary.\n+\tif(s->freelist == nil) {\n+\t\truntime·MSpanList_Remove(s);\n+\t\truntime·MSpanList_Insert(&c->nonempty, s);\n+\t}\n+\n+\t// Add the objects back to s's free list.\n+\tend->next = s->freelist;\n+\ts->freelist = start;\n+\ts->ref -= n;\n+\tc->nfree += n;\n+\n+\t// If s is completely freed, return it to the heap.\n+\tif(s->ref == 0) {\n+\t\tsize = runtime·class_to_size[c->sizeclass];\n+\t\truntime·MSpanList_Remove(s);\n+\t\t*(uintptr*)(s->start<<PageShift) = 1; // needs zeroing\n+\t\ts->freelist = nil;\n+\t\tc->nfree -= (s->npages << PageShift) / size;\n+\t\truntime·unlock(c);\n+\t\truntime·unmarkspan((byte*)(s->start<<PageShift), s->npages<<PageShift);\n+\t\truntime·MHeap_Free(&runtime·mheap, s, 0);\n+\t} else {\n+\t\truntime·unlock(c);\n+\t}\n+}\n+\n void\n runtime·MGetSizeClassInfo(int32 sizeclass, uintptr *sizep, int32 *npagesp, int32 *nobj)\n {\n```
- `runtime·MCentral_FreeList`のコメントが削除。
- `runtime·MCentral_FreeSpan`関数の実装が追加。この関数は、GCから呼び出され、指定された`MSpan`から`n`個のオブジェクトを`MCentral`のフリーリストにまとめて返します。`s->ref`が0になった場合(`MSpan`内のすべてのオブジェクトが解放された場合)、その`MSpan`はヒープに返されます。
### `src/pkg/runtime/mgc0.c`
```diff
--- a/src/pkg/runtime/mgc0.c
+++ b/src/pkg/runtime/mgc0.c
@@ -761,6 +761,8 @@ sweepspan(MSpan *s)\n byte *p;\n MCache *c;\n byte *arena_start;\n+\tMLink *start, *end;\n+\tint32 nfree;\n \n arena_start = runtime·mheap.arena_start;\n p = (byte*)(s->start << PageShift);\n@@ -774,6 +776,9 @@ sweepspan(MSpan *s)\n \tnpages = runtime·class_to_allocnpages[cl];\n \tn = (npages << PageShift) / size;\n }\n+\tnfree = 0;\n+\tstart = end = nil;\n+\tc = m->mcache;\n \n // Sweep through n objects of given size starting at p.\n // This thread owns the span now, so it can manipulate\n@@ -810,21 +815,33 @@ sweepspan(MSpan *s)\n // Mark freed; restore block boundary bit.\n *bitp = (*bitp & ~(bitMask<<shift)) | (bitBlockBoundary<<shift);\n \n-\t\tc = m->mcache;\n \t\tif(s->sizeclass == 0) {\n \t\t\t// Free large span.\n \t\t\truntime·unmarkspan(p, 1<<PageShift);\n \t\t\t*(uintptr*)p = 1;\t// needs zeroing\n \t\t\truntime·MHeap_Free(&runtime·mheap, s, 1);\n+\t\t\tc->local_alloc -= size;\n+\t\t\tc->local_nfree++;\n \t\t} else {\n \t\t\t// Free small object.\n \t\t\tif(size > sizeof(uintptr))\n \t\t\t\t((uintptr*)p)[1] = 1;\t// mark as "needs to be zeroed"\n-\t\t\tc->local_by_size[s->sizeclass].nfree++;\n-\t\t\truntime·MCache_Free(c, p, s->sizeclass, size);\n+\t\t\tif(nfree)\n+\t\t\t\tend->next = (MLink*)p;\n+\t\t\telse\n+\t\t\t\tstart = (MLink*)p;\n+\t\t\tend = (MLink*)p;\n+\t\t\tnfree++;\n \t\t}\n-\t\tc->local_alloc -= size;\n-\t\tc->local_nfree++;\n+\t}\n+\n+\tif(nfree) {\n+\t\tc->local_by_size[s->sizeclass].nfree += nfree;\n+\t\tc->local_alloc -= size * nfree;\n+\t\tc->local_nfree += nfree;\n+\t\tc->local_cachealloc -= nfree * size;\n+\t\tc->local_objects -= nfree;\n+\t\truntime·MCentral_FreeSpan(&runtime·mheap.central[cl], s, nfree, start, end);\n \t}\n }\n \n```
- `sweepspan`関数内で、`start`, `end`, `nfree`という変数が導入され、解放されるオブジェクトを一時的にバッチ処理するためのリンクリストを構築するようになりました。
- 小さなオブジェクトの解放ロジックが変更され、個別に`MCache_Free`を呼び出す代わりに、`start`と`end`ポインタを使って解放されるオブジェクトを連結し、`nfree`をインクリメントするようになりました。
- `sweepspan`の最後に、`nfree`が0より大きい場合(つまり、解放されるオブジェクトがある場合)、新しく導入された`runtime·MCentral_FreeSpan`を呼び出して、バッチでオブジェクトを解放するようになりました。
- `MCache`のローカル統計(`local_alloc`, `local_nfree`, `local_cachealloc`, `local_objects`)の更新が、バッチ処理のロジックに合わせて調整されました。
## コアとなるコードの解説
このコミットの核心は、`mgc0.c`の`sweepspan`関数と`mcentral.c`の`runtime·MCentral_FreeSpan`関数の連携にあります。
`sweepspan`関数は、GCのスイープフェーズにおいて、特定の`MSpan`(メモリブロック)を走査し、不要になったオブジェクトを特定して解放する役割を担います。変更前は、この関数が不要なオブジェクトを見つけるたびに、そのオブジェクトを`MCache`(CPUローカルなキャッシュ)に個別に返していました。これは、多数の小さなオブジェクトが解放される場合に、`MCache`への頻繁なアクセスとロックの取得・解放が発生し、性能上のボトルネックとなっていました。
変更後の`sweepspan`関数では、この問題を解決するために「バッチ処理」の概念が導入されました。
1. **ローカルなフリーリストの構築**: `sweepspan`は、`MSpan`を走査する際に、解放すべき小さなオブジェクトを見つけても、すぐに`MCache`に返すのではなく、`MLink`ポインタを使ってそれらをローカルなリンクリスト(`start`と`end`で管理される)に連結していきます。同時に、解放されるオブジェクトの数`nfree`をカウントします。
2. **バッチでの解放**: `sweepspan`が`MSpan`の走査を終えると、`nfree`が0より大きい場合(つまり、解放すべきオブジェクトが一つでも見つかった場合)、新しく導入された`runtime·MCentral_FreeSpan`関数を呼び出します。この関数には、構築したローカルなフリーリストの先頭(`start`)と末尾(`end`)、そして解放されるオブジェクトの総数(`nfree`)が渡されます。
3. **`runtime·MCentral_FreeSpan`の役割**: `runtime·MCentral_FreeSpan`は、渡されたオブジェクトのリンクリストを、対応する`MCentral`(特定のサイズの`MSpan`を管理する中央リスト)のフリーリストにまとめて追加します。この際、`MCentral`のロックは一度だけ取得され、複数のオブジェクトが効率的に解放されます。また、もし`MSpan`内のすべてのオブジェクトが解放され、`MSpan`が完全に空になった場合(`s->ref == 0`)、この`MSpan`は`MHeap`(全体のヒープ)に返され、再利用可能な状態になります。
このバッチ処理により、`MCache`や`MCentral`へのアクセス回数が大幅に削減され、それに伴うロックの競合やシステムコールのオーバーヘッドが減少します。結果として、GCのスイープフェーズが高速化され、特にGCポーズ時間の短縮に大きく貢献しています。
また、`MaxGcproc`が4から16に増加されたことは、GCのマークフェーズにおける並行処理能力の向上を意味します。これにより、より多くのCPUコアをGCのマーク処理に利用できるようになり、GC全体の実行時間を短縮する効果が期待できます。
## 関連リンク
* Go言語のガベージコレクションに関する公式ドキュメントやブログ記事 (当時のものがあれば)
* Goランタイムのメモリ管理に関する設計ドキュメント
* Goのベンチマークツールに関する情報
## 参考にした情報源リンク
* Goのソースコード (特に`src/pkg/runtime/`)
* Goのガベージコレクションに関する技術ブログや論文 (当時のもの)
* GoのIssueトラッカーやメーリングリスト (このコミットに関連する議論があれば)
* Go runtime garbage collection sweep phase: [https://dev.to/](https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQFZSzrQlp1pk9hmsUNASSoy_IUoCqYOBlJUk6I20IC5kyfrOwf2kXCNvidYFj2_LCnfzD28fPSDLPjuN6jc2CoGu08JaYxiWkL2Y6pNfaqzrFV1xNJJM5VxrbYSR5enKUDUyoNQGJT1W36XsTr-t6NfBWuzRBdVdeFse8MKUSfujPl1k8eIDcEl3Amw)
* Go's Garbage Collector: [https://medium.com/](https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQFbDyear64eOrxuJJ1DtVzbvrF9_Bl-r_sWaafDY39bpTC8wDDMvQq9HGFsiRvLPq_nBBMUfOVsRtfP3hZGE51j3HThqHKmye7ChB5141YxfoZ7hk7Hlqqb5XwGlHCeSLNVMSkCGwRIlOBhngmctL-_-LtXLMRUH-XNkQTXL1vXH-W9Sbbq)
* Understanding Go's Garbage Collector: [https://leapcell.io/](https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQF793wLk2_ZjouMajMsOLaOq_Gus3nyzoVUC3J_Bz8rjWD6eSGFziXld3aKrxrGtDY0VrhV3mBhHnZjPBTGBluUPTWB6XcBlKGDJMwEAGGKdmwE3R8okk2E9W6OvBBhqBzzKmZGXo71l49F5WSrdEfZh86u5KxLMV23O9kt)
* Go's runtime: [https://golang.org/](https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQGCLSIUVxl6PB6wF-aPi5q4lU_BxukcOlKcMVsw01cWouqVf110U1dOzs0Mmm1XPXmjgQIc4TEpvw4MWPw8UMY_IOdvUNR2-H1EP6G10B0-sxgCJ-6-StDV6DYzbeUu)