[インデックス 18467] ファイルの概要
このコミットは、Goランタイムのガベージコレクション(GC)におけるスイープフェーズを並行化する重要な変更を導入します。これにより、GCによるアプリケーションの一時停止(Stop-the-World: STW)時間を大幅に削減し、全体的なパフォーマンスとレイテンシを向上させることを目的としています。具体的には、バックグラウンドで動作するスイーパーゴルーチンと、必要に応じて実行されるオンデマンドスイープを導入することで、スイープ処理がSTWフェーズから切り離されます。
コミット
commit 3c3be622011747f6db4b4cf81ed3a975dfca2b51
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Wed Feb 12 22:16:42 2014 +0400
runtime: concurrent GC sweep
Moves sweep phase out of stoptheworld by adding
background sweeper goroutine and lazy on-demand sweeping.
It turned out to be somewhat trickier than I expected,
because there is no point in time when we know size of live heap
nor consistent number of mallocs and frees.
So everything related to next_gc, mprof, memstats, etc becomes trickier.
At the end of GC next_gc is conservatively set to heap_alloc*GOGC,
which is much larger than real value. But after every sweep
next_gc is decremented by freed*GOGC. So when everything is swept
next_gc becomes what it should be.
For mprof I had to introduce 3-generation scheme (allocs, revent_allocs, prev_allocs),
because by the end of GC we know number of frees for the *previous* GC.
Significant caution is required to not cross yet-unknown real value of next_gc.
This is achieved by 2 means:
1. Whenever I allocate a span from MCentral, I sweep a span in that MCentral.
2. Whenever I allocate N pages from MHeap, I sweep until at least N pages are
returned to heap.
This provides quite strong guarantees that heap does not grow when it should now.
http-1
allocated 7036 7033 -0.04%
allocs 60 60 +0.00%
cputime 51050 46700 -8.52%
gc-pause-one 34060569 1777993 -94.78%
gc-pause-total 2554 133 -94.79%
latency-50 178448 170926 -4.22%
latency-95 284350 198294 -30.26%
latency-99 345191 220652 -36.08%
rss 101564416 101007360 -0.55%
sys-gc 6606832 6541296 -0.99%
sys-heap 88801280 87752704 -1.18%
sys-other 7334208 7405928 +0.98%
sys-stack 524288 524288 +0.00%
sys-total 103266608 102224216 -1.01%
time 50339 46533 -7.56%
virtual-mem 292990976 293728256 +0.25%
garbage-1
allocated 2983818 2990889 +0.24%
allocs 62880 62902 +0.03%
cputime 16480000 16190000 -1.76%
gc-pause-one 828462467 487875135 -41.11%
gc-pause-total 4142312 2439375 -41.11%
rss 1151709184 1153712128 +0.17%
sys-gc 66068352 66068352 +0.00%
sys-heap 1039728640 1039728640 +0.00%
sys-other 37776064 40770176 +7.93%
sys-stack 8781824 8781824 +0.00%
sys-total 1152354880 1155348992 +0.26%
time 16496998 16199876 -1.80%
virtual-mem 1409564672 1402281984 -0.52%
LGTM=rsc
R=golang-codereviews, sameer, rsc, iant, jeremyjackins, gobot
CC=golang-codereviews, khr
https://golang.org/cl/46430043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/3c3be622011747f6db4b4cf81ed3a975dfca2b51
元コミット内容
このコミットは、Goランタイムのガベージコレクション(GC)におけるスイープフェーズを並行化するものです。具体的には、スイープフェーズをStop-the-World(STW)から外し、バックグラウンドスイーパーゴルーチンと遅延オンデマンドスイープを導入します。
この変更は、ライブヒープのサイズや、mallocとfreeの一貫した数を常に把握できるわけではないため、予想よりも複雑な実装となりました。next_gc
、mprof
、memstats
など、GCに関連するすべての要素がより複雑になります。
GCの終了時、next_gc
は保守的にheap_alloc*GOGC
という実際の値よりもはるかに大きな値に設定されます。しかし、各スイープの後、next_gc
はfreed*GOGC
だけ減らされます。これにより、すべてのスイープが完了すると、next_gc
は本来あるべき値になります。
mprof
に関しては、GCの終了時に前のGCで解放されたオブジェクトの数がわかるため、3世代スキーム(allocs
、recent_allocs
、prev_allocs
)を導入する必要がありました。
まだ不明なnext_gc
の実際の値を超えないように、細心の注意が必要です。これは以下の2つの方法で実現されます。
- MCentralからスパンを割り当てるたびに、そのMCentral内のスパンをスイープします。
- MHeapからNページを割り当てるたびに、少なくともNページがヒープに返されるまでスイープします。 これらの対策により、ヒープが不必要に成長しないという強力な保証が得られます。
コミットメッセージには、http-1
とgarbage-1
という2つのベンチマークの結果が含まれており、特にgc-pause-one
とgc-pause-total
が大幅に削減されていることが示されています。これは、STW時間の削減という目標が達成されたことを裏付けています。
変更の背景
Goの初期のガベージコレクションは、マークフェーズとスイープフェーズの両方がStop-the-World(STW)で行われていました。STWとは、GCが実行されている間、アプリケーションのすべてのゴルーチンが一時停止される状態を指します。これにより、GCの実行中はアプリケーションが応答しなくなり、特にレイテンシに敏感なアプリケーションではユーザー体験に悪影響を与える可能性がありました。
このコミットの主な目的は、GCのSTW時間を短縮することです。GCの3つの主要なフェーズ(マーク、スイープ、準備)のうち、スイープフェーズはヒープ上の到達不能なオブジェクトが占めていたメモリを解放し、再利用可能にする役割を担います。このスイープフェーズをSTWから切り離し、アプリケーションの実行と並行して(concurrently)または遅延して(lazily)実行できるようにすることで、GCによるアプリケーションの一時停止を最小限に抑えることが可能になります。
特に、GoのGCは「マーク&スイープ」方式を採用しており、オブジェクトの到達可能性をマークした後、到達不能なオブジェクトが占めるメモリ領域をスイープして解放します。このスイープ処理がヒープサイズに比例して時間がかかるため、大規模なヒープを持つアプリケーションではSTW時間が長くなる傾向がありました。スイープを並行化することで、このボトルネックを解消し、よりスムーズなアプリケーション実行を実現します。
前提知識の解説
このコミットの変更を理解するためには、Goランタイムのメモリ管理とガベージコレクションに関するいくつかの基本的な概念を理解しておく必要があります。
- ガベージコレクション (GC): プログラムが不要になったメモリを自動的に解放する仕組みです。GoのGCは「マーク&スイープ」アルゴリズムをベースにしています。
- マークフェーズ (Mark Phase): GCのルート(グローバル変数、スタック上の変数など)から到達可能なすべてのオブジェクトを「ライブ(生きている)」としてマークします。このフェーズは通常STWで行われます。
- スイープフェーズ (Sweep Phase): マークされなかったオブジェクト(到達不能なオブジェクト、つまり「ゴミ」)が占めるメモリ領域を解放し、再利用可能にします。
- Stop-the-World (STW): GCの特定のフェーズ中に、すべてのアプリケーションゴルーチンが一時停止される状態です。GCがヒープの一貫したスナップショットを取得し、安全にメモリを操作するために必要ですが、アプリケーションの応答性を低下させます。
- ヒープ (Heap): プログラムが動的にメモリを割り当てる領域です。Goのランタイムは、このヒープを効率的に管理するために独自のメモリ管理システムを持っています。
- スパン (MSpan): Goのメモリ管理における基本的なメモリ割り当て単位です。ヒープはページ(通常8KB)の集まりで構成され、複数のページが集まってスパンを形成します。スパンは特定のサイズのオブジェクト(例えば、8バイトオブジェクト用、16バイトオブジェクト用など)を格納するために使用されます。
- MCentral: 特定のサイズクラスのスパンを管理する中央キャッシュです。ゴルーチンはMCentralからスパンを取得し、そのスパンからオブジェクトを割り当てます。
- MHeap: すべてのスパンを管理するグローバルなヒープです。MCentralがスパンを使い果たした場合、MHeapから新しいスパンを取得します。
gcpercent
: 環境変数GOGC
で設定されるGCのトリガー閾値です。デフォルトは100です。gcpercent=100
の場合、現在使用中のヒープメモリが2倍になったときにGCがトリガーされます。例えば、現在4MB使用している場合、8MBに達するとGCが実行されます。next_gc
: 次のGCがトリガーされるヒープ割り当ての目標値を示すランタイム変数です。mprof
: メモリプロファイリングに関連する統計情報です。
技術的詳細
このコミットは、GoランタイムのGCスイープフェーズを並行化するために、いくつかの重要な技術的変更を導入しています。
-
バックグラウンドスイーパーゴルーチン (
bgsweep
):- GCのマークフェーズが完了した後、専用のバックグラウンドゴルーチンが起動され、ヒープ上のスパンを並行してスイープします。
- このゴルーチンは
runtime·sweepone()
を繰り返し呼び出し、スイープが必要なスパンを一つずつ処理します。 - スイープするスパンがなくなると、このゴルーチンは自身をパーク(一時停止)し、次のGCサイクルで再び起動されるのを待ちます。
- これにより、アプリケーションの実行と並行してメモリの解放が進み、STW時間が短縮されます。
-
遅延オンデマンドスイープ (
runtime·MSpan_EnsureSwept
,runtime·MCentral_AllocList
,MHeap_Reclaim
):- バックグラウンドスイーパーだけでは、ヒープの成長速度が速い場合にスイープが追いつかない可能性があります。
- このため、メモリ割り当て時に必要に応じてスパンをスイープする「オンデマンドスイープ」が導入されました。
runtime·malloc
(オブジェクト解放時)やaddspecial
/removespecial
(ファイナライザやプロファイル関連の特殊なレコード操作時)など、スパンのGCビットマップを読み書きする可能性のある操作の前に、runtime·MSpan_EnsureSwept(s)
が呼び出されます。これは、スパンがまだスイープされていない場合、その場でスイープを実行するか、バックグラウンドスイーパーが完了するのを待つことで、GCビットマップの破損を防ぎます。runtime·MCentral_AllocList
(MCentralからスパンを割り当てる際)では、スイープが必要なスパンを優先的にスイープし、再利用を試みます。runtime·MHeap_Alloc
(MHeapから新しいページを割り当てる際)では、MHeap_Reclaim
が呼び出されます。これは、新しいメモリを割り当てる前に、既存のヒープから少なくとも要求されたページ数分のメモリをスイープして解放しようとします。これにより、ヒープが不必要に成長するのを防ぎます。
-
sweepgen
(スイープ世代) の導入:MSpan
構造体にsweepgen
フィールドが追加されました。これは、各スパンがどのGC世代でスイープされたか、またはスイープが必要か、スイープ中かを示すために使用されます。mheap.sweepgen
はGCごとに2ずつインクリメントされます。s->sweepgen == h->sweepgen - 2
: スパンはスイープが必要な状態です。s->sweepgen == h->sweepgen - 1
: スパンは現在スイープ中です。s->sweepgen == h->sweepgen
: スパンはスイープ済みで、使用可能です。- この
sweepgen
を用いることで、並行スイープ中にスパンの一貫性を保ち、古いGC世代のスパンが誤って再利用されたり、GCビットマップが破損したりするのを防ぎます。
-
next_gc
の動的な調整:- 並行スイープでは、GC終了時に正確なライブヒープサイズが不明なため、
next_gc
の計算が複雑になります。 - GC終了時、
next_gc
は保守的にmstats.heap_alloc + mstats.heap_alloc * gcpercent / 100
という大きな値に設定されます。これは、すべてのメモリがライブであると仮定した値です。 - その後、
runtime·MSpan_Sweep
がスパンをスイープしてメモリを解放するたびに、mstats.next_gc
は-(uint64)(freed_size * (gcpercent + 100)/100)
だけ減らされます。 - これにより、スイープが進むにつれて
next_gc
が実際のライブヒープサイズに近づき、次のGCトリガーが適切に調整されます。
- 並行スイープでは、GC終了時に正確なライブヒープサイズが不明なため、
-
mprof
の3世代スキーム:- メモリプロファイリング(
mprof
)の統計情報(割り当てと解放の数、バイト数)を正確に収集するために、Bucket
構造体にprev_allocs
,prev_frees
などのフィールドが追加されました。 - これは、GC終了時に「前のGCで解放されたオブジェクトの数」がわかるという特性に対応するためです。
MProf_GC
関数が呼び出されるたびに、recent
統計がprev
統計に移動され、prev
統計がallocs
/frees
に加算されます。これにより、GCサイクルをまたいだ正確なプロファイリングが可能になります。
- メモリプロファイリング(
-
ロックの変更:
- ファイナライザのキューイングやバックグラウンドスイーパーの管理のために、
finlock
がgclock
に置き換えられ、GC関連のロックがより一元的に管理されるようになりました。
- ファイナライザのキューイングやバックグラウンドスイーパーの管理のために、
これらの変更により、GoのGCは「部分的に並行(partially concurrent)」なGCとなり、マークフェーズはSTWのままですが、スイープフェーズは並行して実行されるようになりました。
コアとなるコードの変更箇所
このコミットにおける主要なコード変更は以下のファイルに集中しています。
src/pkg/runtime/malloc.h
:MSpan
構造体へのsweepgen
フィールドの追加、新しい関数の宣言。src/pkg/runtime/malloc.goc
:runtime·free
におけるruntime·MSpan_EnsureSwept
の呼び出し。src/pkg/runtime/mcentral.c
:runtime·MCentral_AllocList
におけるスイープロジックの追加、runtime·MCentral_FreeSpan
の変更。src/pkg/runtime/mgc0.c
: GCのメインロジックが記述されているファイルで、最も広範な変更が行われています。runtime·MSpan_EnsureSwept
、runtime·MSpan_Sweep
、bgsweep
、runtime·sweepone
といった並行スイープの核心となる関数の実装。runtime·gc
におけるスイープフェーズの変更、next_gc
の計算ロジックの更新。gcstats
への新しい統計情報の追加。
src/pkg/runtime/mheap.c
:MHeap
構造体へのsweepgen
関連フィールドの追加、MHeap_Reclaim
関数の追加、runtime·MHeap_Alloc
におけるスイープの考慮。src/pkg/runtime/mprof.goc
: メモリプロファイリングの3世代スキームの実装。
コアとなるコードの解説
src/pkg/runtime/malloc.h
struct MSpan
{
// ...
uint32 sweepgen; // sweep generation, see comment in MSpan
// ...
};
void runtime·MSpan_EnsureSwept(MSpan *span);
bool runtime·MSpan_Sweep(MSpan *span);
// ...
struct MHeap
{
// ...
MSpan **sweepspans; // copy of allspans referenced by sweeper
uint32 sweepgen; // sweep generation, see comment in MSpan
uint32 sweepdone; // all spans are swept
// ...
};
MSpan
構造体にsweepgen
が追加され、各スパンのスイープ状態を追跡します。MHeap
にはグローバルなsweepgen
、スイーパーが参照するスパンのコピーsweepspans
、そしてスイープ完了を示すsweepdone
が追加されています。
src/pkg/runtime/malloc.goc
void runtime·free(void *v)
{
// ...
// Ensure that the span is swept.
// If we free into an unswept span, we will corrupt GC bitmaps.
runtime·MSpan_EnsureSwept(s);
// ...
}
オブジェクトを解放する際に、そのオブジェクトが属するスパンが確実にスイープされていることをruntime·MSpan_EnsureSwept
で確認します。これは、GCビットマップの破損を防ぐための重要な安全策です。
src/pkg/runtime/mcentral.c
int32 runtime·MCentral_AllocList(MCentral *c, MLink **pfirst)
{
// ...
sg = runtime·mheap.sweepgen;
retry:
for(s = c->nonempty.next; s != &c->nonempty; s = s->next) {
if(s->sweepgen == sg-2 && runtime·cas(&s->sweepgen, sg-2, sg-1)) {
runtime·unlock(c);
runtime·MSpan_Sweep(s); // On-demand sweep
runtime·lock(c);
goto retry;
}
if(s->sweepgen == sg-1) {
continue; // Being swept by background sweeper, skip
}
goto havespan; // Already swept, allocate from it
}
// ... (similar logic for empty list)
// ...
}
MCentral_AllocList
は、スパンを割り当てる際に、まずnonempty
リストとempty
リストを走査し、スイープが必要なスパン(s->sweepgen == sg-2
)を見つけると、runtime·MSpan_Sweep
を呼び出してその場でスイープを実行します。これにより、メモリ割り当て時に必要なメモリを即座に利用可能にすることができます。
src/pkg/runtime/mgc0.c
// Concurrent sweep.
// The sweep phase proceeds concurrently with normal program execution.
// The heap is swept span-by-span both lazily (when a goroutine needs another span)
// and concurrently in a background goroutine (this helps programs that are not CPU bound).
// ... (詳細な説明) ...
void runtime·MSpan_EnsureSwept(MSpan *s)
{
uint32 sg;
sg = runtime·mheap.sweepgen;
if(runtime·atomicload(&s->sweepgen) == sg)
return; // Already swept
if(runtime·cas(&s->sweepgen, sg-2, sg-1)) {
runtime·MSpan_Sweep(s); // Sweep it now
return;
}
// Unfortunate condition, wait for background sweeper
while(runtime·atomicload(&s->sweepgen) != sg)
runtime·osyield();
}
bool runtime·MSpan_Sweep(MSpan *s)
{
// ... (スイープのコアロジック) ...
// Decrement next_gc by freed memory
runtime·xadd64(&mstats.next_gc, -(uint64)(nfree * size * (gcpercent + 100)/100));
// ...
return res; // true if span returned to heap
}
static void bgsweep(void)
{
g->issystem = 1;
for(;;) {
while(runtime·sweepone() != -1) {
gcstats.nbgsweep++;
runtime·gosched(); // Yield to other goroutines
}
// ... (finalizer logic and park) ...
}
}
uintptr runtime·sweepone(void)
{
// ... (一つスパンをスイープするロジック) ...
if(!runtime·MSpan_Sweep(s))
npages = 0;
// ...
return npages; // pages returned to heap, or -1 if nothing to sweep
}
void gc(struct gc_args *args)
{
// ...
// Sweep what is not sweeped by bgsweep.
while(runtime·sweepone() != -1)
gcstats.npausesweep++; // Count sweeps during STW
// ...
// Conservatively set next_gc to high value assuming that everything is live
// concurrent/lazy sweep will reduce this number while discovering new garbage
mstats.next_gc = mstats.heap_alloc+mstats.heap_alloc*gcpercent/100;
// ...
// Start/wake up background sweeper goroutine
runtime·lock(&gclock);
if(sweep.g == nil)
sweep.g = runtime·newproc1(&bgsweepv, nil, 0, 0, runtime·gc);
else if(sweep.parked) {
sweep.parked = false;
runtime·ready(sweep.g);
}
runtime·unlock(&gclock);
// ...
}
mgc0.c
は並行スイープの心臓部です。
runtime·MSpan_EnsureSwept
は、スパンがスイープ済みであることを保証します。必要であればその場でスイープするか、バックグラウンドスイーパーの完了を待ちます。runtime·MSpan_Sweep
は、個々のスパンをスイープする実際の処理を行います。解放されたメモリ量に基づいてnext_gc
を減らします。bgsweep
は、バックグラウンドでruntime·sweepone
を繰り返し呼び出し、ヒープをスイープする専用のゴルーチンです。runtime·sweepone
は、スイープが必要なスパンを一つ見つけてスイープし、解放されたページ数を返します。gc
関数は、GCの開始時に残っている未スイープのスパンを強制的にスイープし(gcstats.npausesweep
でカウント)、next_gc
を保守的に設定します。また、bgsweep
ゴルーチンを起動または再開します。
src/pkg/runtime/mheap.c
static void MHeap_Reclaim(MHeap *h, uintptr npage)
{
// To prevent excessive heap growth, before allocating n pages
// we need to sweep and reclaim at least n pages.
if(!h->sweepdone) {
// Prioritize sweeping busy spans with large objects
// Then smaller objects
// Finally, sweep everything that is not yet swept
}
}
MSpan* runtime·MHeap_Alloc(MHeap *h, uintptr npage, int32 sizeclass, bool large, bool zeroed)
{
// ...
if(!h->sweepdone)
MHeap_Reclaim(h, npage); // Reclaim pages before allocation
// ...
}
MHeap_Reclaim
は、新しいメモリを割り当てる前に、ヒープが不必要に成長するのを防ぐために導入されました。これは、まず大きなオブジェクトのスパンからスイープを試み、次に小さなオブジェクトのスパン、そして最後にまだスイープされていないすべてのスパンをスイープして、要求されたページ数分のメモリを確保しようとします。runtime·MHeap_Alloc
はこのMHeap_Reclaim
を呼び出します。
src/pkg/runtime/mprof.goc
struct Bucket
{
// ...
struct // typ == MProf
{
// ...
uintptr prev_allocs; // since last but one till last gc
uintptr prev_frees;
uintptr prev_alloc_bytes;
uintptr prev_free_bytes;
uintptr recent_allocs; // since last gc till now
uintptr recent_frees;
uintptr recent_alloc_bytes;
uintptr recent_free_bytes;
// ...
};
// ...
};
void MProf_GC(void)
{
Bucket *b;
for(b=mbuckets; b; b=b->allnext) {
b->allocs += b->prev_allocs;
b->frees += b->prev_frees;
b->alloc_bytes += b->prev_alloc_bytes;
b->free_bytes += b->prev_free_bytes;
b->prev_allocs = b->recent_allocs;
b->prev_frees = b->recent_frees;
b->prev_alloc_bytes = b->recent_alloc_bytes;
b->prev_free_bytes = b->recent_free_bytes;
b->recent_allocs = 0;
b->recent_frees = 0;
b->recent_alloc_bytes = 0;
b->recent_free_bytes = 0;
}
}
void runtime·MProf_Free(Bucket *b, void *p, uintptr size, bool freed)
{
runtime·lock(&proflock);
if(freed) {
b->recent_frees++;
b->recent_free_bytes += size;
} else {
b->prev_frees++;
b->prev_free_bytes += size;
}
// ...
}
mprof.goc
では、メモリプロファイリングの統計を正確に収集するために、Bucket
構造体にprev_allocs
などのフィールドが追加され、3世代スキームが導入されました。MProf_GC
関数は、GCが実行されるたびに統計を更新し、recent
な統計をprev
に、prev
な統計を最終的なallocs
/frees
に加算します。runtime·MProf_Free
は、オブジェクトが実際に解放されたかどうか(freed
引数)に基づいて、recent
またはprev
の解放統計を更新します。
関連リンク
- Goのガベージコレクションに関する公式ドキュメントやブログ記事は、Goの進化とともに更新されるため、最新の情報を参照することが重要です。
- Goのソースコードリポジトリ: https://github.com/golang/go
- このコミットの変更リスト(Gerrit): https://golang.org/cl/46430043
参考にした情報源リンク
- Goのガベージコレクションの歴史と進化に関する記事(例: "Go's new GC: less pause time, more throughput" by Rick Hudson, "The Go garbage collector: Prioritizing low latency and simplicity" by Rick Hudson and Michael Knyszek)
- Goランタイムのソースコード(特に
src/runtime
ディレクトリ) - Goのメモリ管理に関する技術ブログやカンファレンス発表資料
これらの変更により、GoのGCはより効率的になり、特にレイテンシが重要なアプリケーションにおいて、GCによる一時停止が大幅に削減されました。