[インデックス 14999] ファイルの概要
このコミットは、Goランタイムのメモリ管理、特にガベージコレクション(GC)とスカベンジャー(Scavenger)の連携を改善することを目的としています。未使用のメモリ領域(スパン)の検出を早め、スカベンジャーがOSにメモリを返却する効率を高めるための変更が含まれています。具体的には、スパンが未使用になった時刻を記録するunusedsince
フィールドの設定タイミングを前倒しし、関連するロジックをより適切なmheap
モジュールに集約しています。
コミット
commit 09ea3b518ee6fd45a7b09b7f34a4ef84c5159240
Author: Sébastien Paolacci <sebastien.paolacci@gmail.com>
Date: Mon Jan 28 12:53:35 2013 -0500
runtime: earlier detection of unused spans.
Mark candidate spans one GC pass earlier.
Move scavenger's code out from mgc0 and constrain it into mheap (where it belongs).
R=rsc, dvyukov, minux.ma
CC=golang-dev
https://golang.org/cl/7002049
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/09ea3b518ee6fd45a7b09b7f34a4ef84c5159240
元コミット内容
このコミットは、Goランタイムのメモリ管理における以下の主要な改善を目的としています。
- 未使用スパンの早期検出: ガベージコレクション(GC)のサイクルにおいて、未使用となったメモリ領域(スパン)をより早く識別できるようにします。
unusedsince
タイムスタンプの設定タイミング変更: スパンが未使用とマークされるタイミングを、GCのスイープフェーズから、スパンがヒープのフリーリストに返却される時点に前倒しします。これにより、スカベンジャーがメモリをOSに返却する機会を早めます。- スカベンジャー関連コードの再配置: スカベンジャーに関連するコードロジックを、
mgc0.c
(GCのメインロジック)からmheap.c
(ヒープとスパンの管理ロジック)へと移動させ、コードの責務を明確化し、保守性を向上させます。
変更の背景
Goランタイムは、効率的なメモリ管理とガベージコレクションを提供します。その一環として、使用されなくなったメモリをOSに返却する「スカベンジャー」というメカニズムが存在します。スカベンジャーは、一定期間未使用の状態が続いたメモリ領域(スパン)をOSに解放することで、システム全体のメモリ使用量を最適化します。
このコミット以前は、スパンが未使用になったことを示すunusedsince
タイムスタンプは、GCのスイープフェーズ中にmgc0.c
内のsweepspan
関数で設定されていました。しかし、スパンが実際に解放され、ヒープのフリーリストに戻されるのは、mheap.c
内のMHeap_FreeLocked
関数が呼び出された時点です。このタイムラグにより、スパンが未使用になってからunusedsince
が設定されるまでに遅延が生じ、結果としてスカベンジャーがメモリをOSに返却するまでの時間も長くなる可能性がありました。
このコミットの背景には、以下の課題意識があったと考えられます。
- メモリ返却の遅延:
unusedsince
の設定が遅れることで、OSへのメモリ返却が遅延し、システム全体のメモリフットプリントが増大する可能性。 - コードの責務の曖昧さ: スパンの解放と状態変更に関するロジックが、GCのメインロジック(
mgc0.c
)とヒープ管理ロジック(mheap.c
)に分散しており、コードの理解と保守が複雑になる。
これらの課題を解決し、Goランタイムのメモリ管理効率をさらに向上させるために、unusedsince
の設定タイミングの変更とコードの再配置が行われました。
前提知識の解説
このコミットを理解するためには、Goランタイムのメモリ管理とガベージコレクションに関する以下の概念を理解しておく必要があります。
- Goランタイム (Go Runtime): Goプログラムの実行を管理するシステム。メモリ割り当て、ガベージコレクション、ゴルーチン管理、スケジューリングなどを担当します。
- ヒープ (Heap): プログラムが動的にメモリを割り当てる領域。Goでは、
make
やnew
で作成されるオブジェクトはヒープに割り当てられます。 - スパン (MSpan): Goランタイムのメモリ管理における基本的な単位。連続したメモリページ(通常は8KB)のブロックを指します。ヒープは多数のスパンで構成されており、各スパンは特定のサイズのオブジェクトを格納するために使用されるか、フリー(未使用)の状態になります。
MSpanInUse
: スパンが現在使用中であることを示します。MSpanFree
: スパンが現在未使用であり、ヒープのフリーリストにあることを示します。
- ガベージコレクション (GC): プログラムが不要になったメモリを自動的に解放するプロセス。GoのGCは主に「マーク&スイープ」アルゴリズムを採用しています。
- マークフェーズ (Mark Phase): プログラムがアクセス可能なオブジェクト(到達可能なオブジェクト)をマークします。
- スイープフェーズ (Sweep Phase): マークされなかったオブジェクトが占めるメモリ領域を解放し、再利用可能にします。このフェーズで、未使用のスパンがヒープのフリーリストに戻されます。
- スカベンジャー (Scavenger): Goランタイムのコンポーネントの一つで、ヒープのフリーリストにあるスパンのうち、一定期間未使用の状態が続いているものをOSに返却する役割を担います。これにより、Goプロセスが占有する物理メモリ量を削減し、システム全体のメモリ効率を向上させます。
unusedsince
:MSpan
構造体に含まれるフィールドで、そのスパンが未使用(MSpanFree
)になった時刻を記録するタイムスタンプです。スカベンジャーはこのタイムスタンプを参照し、古くなった未使用スパンをOSに返却します。mheap.c
: Goランタイムのヒープ管理ロジックを実装しているファイル。スパンの割り当て、解放、フリーリストの管理など、ヒープ全体の操作を扱います。mgc0.c
: Goランタイムのガベージコレクションの主要なロジックを実装しているファイル。マークフェーズやスイープフェーズの処理が含まれます。MHeap_FreeLocked
:mheap.c
内で定義されている関数で、指定されたスパンをヒープのフリーリストに返却する際に呼び出されます。sweepspan
:mgc0.c
内で定義されている関数で、GCのスイープフェーズ中に各スパンを処理します。
技術的詳細
このコミットの技術的な核心は、MSpan
構造体のunusedsince
フィールドの更新ロジックの変更と、それに伴うスカベンジャーの動作の調整にあります。
変更前:
src/pkg/runtime/mgc0.c
のsweepspan
関数内で、スパンがMSpanFree
状態になった際にs->unusedsince = runtime·nanotime();
が設定されていました。これはGCのスイープフェーズ中に実行されます。
変更後:
src/pkg/runtime/mgc0.c
からsweepspan
内のunusedsince
設定ロジックが削除されました。src/pkg/runtime/mheap.c
のMHeap_FreeLocked
関数内に、s->unusedsince = runtime·nanotime();
が移動されました。MHeap_FreeLocked
は、スパンがヒープのフリーリストに返却される際に呼び出されるため、スパンが未使用になった瞬間にunusedsince
が設定されるようになります。- スパンの結合(coalescing)処理において、結合されるスパンの
unusedsince
値が新しいスパンに引き継がれるようにt->unusedsince = s->unusedsince;
が追加されました。これにより、結合後もスパンの「古さ」が正しく保持されます。 runtime·MHeap_Scavenger
関数内でのunusedsince
のチェック条件がif(s->unusedsince != 0 && (now - s->unusedsince) > limit)
からif((now - s->unusedsince) > limit)
に変更されました。これは、unusedsince
が常に有効なタイムスタンプを持つことが保証されるようになったため、s->unusedsince != 0
のチェックが不要になったことを示唆しています。
この変更により、スパンが未使用になったという情報がより早くunusedsince
に反映されるため、スカベンジャーはより迅速にそのスパンをOSに返却する候補として認識できるようになります。これは、メモリの再利用効率を高め、Goプログラムのメモリフットプリントを削減する効果が期待できます。
また、スカベンジャー関連のロジックがmheap.c
に集約されたことで、メモリヒープの管理とOSへのメモリ返却という一連の処理がmheap
モジュール内で完結するようになり、コードのモジュール性と保守性が向上しました。
コアとなるコードの変更箇所
src/pkg/runtime/mgc0.c
--- a/src/pkg/runtime/mgc0.c
+++ b/src/pkg/runtime/mgc0.c
@@ -1159,10 +1159,6 @@ sweepspan(ParFor *desc, uint32 idx)
USED(&desc);
s = runtime·mheap.allspans[idx];
- // Stamp newly unused spans. The scavenger will use that
- // info to potentially give back some pages to the OS.
- if(s->state == MSpanFree && s->unusedsince == 0)
- s->unusedsince = runtime·nanotime();
if(s->state != MSpanInUse)
return;
arena_start = runtime·mheap.arena_start;
src/pkg/runtime/mheap.c
--- a/src/pkg/runtime/mheap.c
+++ b/src/pkg/runtime/mheap.c
@@ -138,7 +138,9 @@ HaveSpan:
*(uintptr*)(t->start<<PageShift) = *(uintptr*)(s->start<<PageShift); // copy "needs zeroing" mark
t->state = MSpanInUse;
MHeap_FreeLocked(h, t);
+ t->unusedsince = s->unusedsince; // preserve age
}
+ s->unusedsince = 0;
// Record span info, because gc needs to be
// able to map interior pointer to containing span.
@@ -300,10 +302,12 @@ MHeap_FreeLocked(MHeap *h, MSpan *s)
}
mstats.heap_idle += s->npages<<PageShift;
s->state = MSpanFree;
- s->unusedsince = 0;
- s->npreleased = 0;
runtime·MSpanList_Remove(s);
sp = (uintptr*)(s->start<<PageShift);
+ // Stamp newly unused spans. The scavenger will use that
+ // info to potentially give back some pages to the OS.
+ s->unusedsince = runtime·nanotime();
+ s->npreleased = 0;
// Coalesce with earlier, later spans.
p = s->start;
@@ -401,10 +405,10 @@ runtime·MHeap_Scavenger(void)
runtime·entersyscall();
runtime·notesleep(¬e);
runtime·exitsyscall();
+ if(trace)
+ runtime·printf("scvg%d: GC forced\\n", k);
runtime·lock(h);
now = runtime·nanotime();
- if (trace)
- runtime·printf("scvg%d: GC forced\\n", k);
}
sumreleased = 0;
for(i=0; i < nelem(h->free)+1; i++) {
@@ -415,7 +419,7 @@ runtime·MHeap_Scavenger(void)
if(runtime·MSpanList_IsEmpty(list))
continue;
for(s=list->next; s != list; s=s->next) {
- if(s->unusedsince != 0 && (now - s->unusedsince) > limit) {
+ if((now - s->unusedsince) > limit) {
released = (s->npages - s->npreleased) << PageShift;
mstats.heap_released += released;
sumreleased += released;
コアとなるコードの解説
src/pkg/runtime/mgc0.c
の変更
sweepspan
関数からのunusedsince
設定ロジックの削除: 変更前は、sweepspan
関数内でスパンがMSpanFree
状態になった際にs->unusedsince = runtime·nanotime();
が設定されていました。このロジックが完全に削除されました。これは、スパンが実際に解放されるタイミング(MHeap_FreeLocked
が呼び出されるタイミング)でunusedsince
を設定する方が適切であるという判断に基づいています。sweepspan
はGCのスイープフェーズの一部であり、スパンの状態を更新しますが、必ずしもそのスパンが即座にOSに返却可能な状態になるわけではありませんでした。
src/pkg/runtime/mheap.c
の変更
-
MHeap_FreeLocked
関数内でのunusedsince
設定ロジックの追加と移動:t->unusedsince = s->unusedsince; // preserve age
の追加: これは、スパンが結合される際に、元のスパンs
のunusedsince
の値を新しいスパンt
に引き継ぐためのものです。これにより、スパンが結合されても、そのスパンが未使用になってからの経過時間が正しく保持され、スカベンジャーが適切に判断できるようになります。s->unusedsince = 0;
の追加: これは、結合された元のスパンs
のunusedsince
をリセットするためのものです。s->unusedsince = runtime·nanotime();
とs->npreleased = 0;
の移動/追加:MHeap_FreeLocked
関数は、スパンがヒープのフリーリストに返却される際に呼び出されます。この場所でs->unusedsince = runtime·nanotime();
を設定することで、スパンが実際に未使用になった瞬間にタイムスタンプが記録されるようになります。これにより、スカベンジャーが未使用スパンを検出するタイミングが早まり、OSへのメモリ返却が効率化されます。s->npreleased = 0;
も同時に設定され、解放されたページ数がリセットされます。
-
runtime·MHeap_Scavenger
関数内でのunusedsince
チェック条件の簡略化:if(s->unusedsince != 0 && (now - s->unusedsince) > limit)
からif((now - s->unusedsince) > limit)
への変更:MHeap_FreeLocked
でunusedsince
が常に設定されるようになったため、スカベンジャーがスパンを処理する際にはunusedsince
がゼロでないことが保証されます。したがって、s->unusedsince != 0
という冗長なチェックが不要になり、条件式が簡略化されました。これは、unusedsince
が常に有効な値を持つという新しい前提を反映しています。if (trace)
ブロックの移動: デバッグ出力に関するif (trace)
ブロックの移動は、機能的な変更ではなく、コードの整理と一貫性のためのものです。
これらの変更により、Goランタイムは未使用メモリの検出とOSへの返却をより迅速かつ効率的に行えるようになり、全体的なメモリフットプリントの管理が改善されます。
関連リンク
- Go言語の公式ドキュメント: https://go.dev/
- Goランタイムのソースコード(GitHub): https://github.com/golang/go/tree/master/src/runtime
- Goのガベージコレクションに関するブログ記事やドキュメント(一般的な情報源)
参考にした情報源リンク
- Goのガベージコレクションに関する公式ブログ記事や設計ドキュメント(例: Go 1.5 GC Pacing, Go's new GC strategyなど、当時のGoのGCに関する情報)
- Goのメモリ管理に関する技術記事や解説サイト
- Goのソースコード(特に
src/runtime/mheap.c
とsrc/runtime/mgc0.c
の当時のバージョン) - GoのCL (Change List) 7002049: https://golang.org/cl/7002049 (コミットメッセージに記載されているリンク)
- CLのレビューコメントや詳細な説明は、コミットの意図を深く理解する上で非常に有用です。
- Goの
runtime
パッケージのドキュメント: https://pkg.go.dev/runtime - Goのメモリ管理に関する書籍や論文