Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 19391] ファイルの概要

このコミットは、Goランタイムにおけるdebug.FreeOSMemory関数の動作を修正し、メモリを即座にOSに解放するように改善するものです。これまでの実装では、ガベージコレクション(GC)のマーキングフェーズのみが実行され、スイープフェーズが実行されていなかったため、debug.FreeOSMemoryを呼び出してもメモリがすぐにOSに返還されない問題がありました。この変更により、GCのマーキングとスイープの両フェーズがdebug.FreeOSMemoryの呼び出し時に強制的に実行されるようになります。

コミット

  • コミットハッシュ: e893acf1848b96a922356eb753e8cea79f469afd
  • Author: Dmitriy Vyukov dvyukov@google.com
  • Date: Mon May 19 12:06:30 2014 +0400

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/e893acf1848b96a922356eb753e8cea79f469afd

元コミット内容

runtime: fix freeOSMemory to free memory immediately
Currently freeOSMemory makes only marking phase of GC, but not sweeping phase.
So recently memory is not released after freeOSMemory.
Do both marking and sweeping during freeOSMemory.
Fixes #8019.

LGTM=khr
R=golang-codereviews, khr
CC=golang-codereviews, rsc
https://golang.org/cl/97550043

変更の背景

Goのランタイムには、runtime/debugパッケージにFreeOSMemory()という関数が提供されています。この関数は、Goプログラムが使用しているメモリのうち、不要になったものをOSに返還することを試みるものです。しかし、このコミット以前のFreeOSMemory()の実装には問題がありました。

具体的には、FreeOSMemory()が内部でガベージコレクション(GC)をトリガーする際に、GCの「マーキングフェーズ」は実行されるものの、「スイープフェーズ」が適切に実行されていませんでした。GCのマーキングフェーズは、到達可能なオブジェクトを識別する役割を担いますが、実際にメモリを解放してOSに返還するためには、到達不能なオブジェクトが占めていたメモリ領域を回収するスイープフェーズが不可欠です。

このスイープフェーズが不足していたため、FreeOSMemory()を呼び出しても、期待通りにメモリがOSに即座に返還されず、アプリケーションのメモリ使用量が減少しないという問題(Issue #8019)が発生していました。特に、メモリ使用量が一時的に急増し、その後解放されるようなシナリオでは、OSレベルでのメモリフットプリントがなかなか減少しないことが課題となっていました。このコミットは、この問題を解決し、FreeOSMemory()がその名の通り、メモリを即座にOSに解放する機能を果たすようにするためのものです。

前提知識の解説

Goのガベージコレクション (GC) の基本

Goのガベージコレクションは、プログラムが動的に確保したメモリのうち、もはや到達不可能(参照されていない)になったオブジェクトを自動的に回収する仕組みです。GoのGCは、主に以下の2つのフェーズで構成されます。

  1. マーキングフェーズ (Marking Phase): このフェーズでは、GCはプログラムのルート(グローバル変数、スタック上の変数など)から開始し、そこから到達可能なすべてのオブジェクトを「生きている(live)」としてマークします。このプロセスは、オブジェクト間の参照をたどることで行われます。GoのGCは、このマーキングフェーズの一部をアプリケーションの実行と並行して(concurrently)行うことで、アプリケーションの一時停止時間(STW: Stop-The-World)を最小限に抑えるように設計されています。

  2. スイープフェーズ (Sweeping Phase): マーキングフェーズが完了した後、スイープフェーズが開始されます。このフェーズでは、マーキングされなかった(つまり、到達不可能と判断された)オブジェクトが占めていたメモリ領域が解放され、再利用可能な状態になります。GoのGCでは、このスイープフェーズも通常はアプリケーションの実行と並行して行われます。解放されたメモリは、Goランタイムのヒープに戻され、必要に応じてOSに返還される可能性があります。

runtime.GC()debug.FreeOSMemory() の役割

  • runtime.GC(): この関数は、Goランタイムにガベージコレクションの実行を明示的に要求します。通常、GoのGCは自動的にバックグラウンドで実行されますが、runtime.GC()を呼び出すことで、開発者は特定のタイミングでGCを強制的に実行させることができます。これは、例えばベンチマークの前にヒープをクリーンアップしたい場合や、メモリ使用量を一時的に削減したい場合などに利用されます。

  • runtime/debug.FreeOSMemory(): この関数は、Goランタイムが現在保持しているが、もはや使用していないメモリをオペレーティングシステムに返還することを試みます。Goランタイムは、一度OSから確保したメモリをすぐにOSに返還せず、将来の割り当てのために内部で保持しておくことがあります。これは、頻繁なOSコールを避けてパフォーマンスを向上させるためですが、場合によってはOSレベルでのメモリ使用量を減らしたいというニーズがあります。FreeOSMemory()は、そのような場合に、Goランタイムが保持しているアイドル状態のメモリをOSに積極的に返還するよう促すために使用されます。

メモリがOSに解放される仕組み

Goランタイムは、OSからメモリをページ単位(通常は4KBや8KB)で確保します。これらのページは、Goのヒープとして管理され、オブジェクトの割り当てに使用されます。オブジェクトがGCによって回収されると、そのオブジェクトが占めていたページは空きページとなります。Goランタイムは、これらの空きページをすぐにOSに返還するのではなく、将来の割り当てのために内部のフリーリストに保持しておくことが一般的です。

OSにメモリを返還するプロセスは「スカベンジング(scavenging)」と呼ばれます。これは、Goランタイムが一定期間使用されていない空きページを識別し、それらをOSに返還する操作です。debug.FreeOSMemory()は、このスカベンジングプロセスをトリガーし、Goランタイムが保持しているアイドルなメモリをOSに積極的に返還するように促します。しかし、このコミット以前は、GCのスイープフェーズが適切に完了しないと、スカベンジングの対象となるべきメモリが正確に識別されず、結果としてOSへのメモリ返還が遅延していました。

技術的詳細

このコミットの核心は、runtime·gc関数のforce引数に新しいセマンティクス(意味)を追加し、それを利用してruntime.GC()debug.FreeOSMemory()の動作を変更した点にあります。

runtime·gc 関数の force 引数の新しい意味

以前のruntime·gc関数は、force引数に1を渡すと、現在のヒープ使用量に関わらずGCを強制実行するという意味合いでした。このコミットでは、force引数に2という新しい値が導入されました。

  • force = 1: 現在のヒープ使用量に関わらずGCを強制実行します。
  • force = 2: GCを強制実行し、さらに「eager sweep(積極的なスイープ)」を実行します。

「eager sweep」とは、GCのスイープフェーズを即座に、かつ完全に実行することを意味します。通常、GoのGCのスイープフェーズは、アプリケーションの実行と並行して、バックグラウンドで徐々に実行されます。これにより、アプリケーションのレイテンシへの影響を最小限に抑えます。しかし、force=2が指定された場合、この並行スイープを待つことなく、スイープフェーズが直ちに完了するように強制されます。

eagersweep フラグの導入とその目的

src/pkg/runtime/mgc0.cstruct gc_argsbool eagersweep;という新しいフィールドが追加されました。このフラグは、runtime·gc関数が呼び出された際に、force引数が2以上である場合にtrueに設定されます(a.eagersweep = force >= 2;)。

このeagersweepフラグの目的は、GCのスイープフェーズを即座に完了させる必要があるかどうかをGCルーチンに伝えることです。特に、debug.FreeOSMemory()のように、メモリをOSに即座に返還したいという強い要求がある場合に、このフラグが利用されます。

並行スイープとEager Sweepの関係

src/pkg/runtime/mgc0.cgc関数内で、並行スイープを制御する条件が変更されました。

変更前:

if(ConcurrentSweep) {
    // ...
}

変更後:

if(ConcurrentSweep && !args->eagersweep) {
    // ...
}

この変更は非常に重要です。eagersweepフラグがtrueの場合(つまり、force=2でGCが呼び出された場合)、ConcurrentSweepが有効であっても、並行スイープのロジックがスキップされることを意味します。これは、eager sweepが要求された際には、並行スイープによる遅延を許容せず、スイープを即座に完了させることを優先するためです。並行スイープはバックグラウンドで徐々にメモリを解放しますが、eager sweepはGC呼び出しのコンテキスト内でスイープを完了させるため、両者は排他的な関係にあります。

runtime.GC()debug.FreeOSMemory()runtime·gc(2) を呼び出すようになったことの意味

  • runtime.GC() の変更: 以前のruntime.GC()は、runtime·gc(1)を2回呼び出していました。これは、「ユーザーはGCが戻ったときに未使用メモリが解放されることを期待している」という仮定に基づき、1回目のGCでコレクションを行い、2回目のGCで1回目のスイープが完了することを強制するという意図がありました。しかし、このアプローチは「2回目のコレクションはやりすぎ」とコメントされており、非効率的でした。 このコミットにより、runtime.GC()は単一のruntime·gc(2)呼び出しに簡素化されました。これにより、GCが強制実行され、かつeager sweepが実行されるため、runtime.GC()が戻る時点でメモリがより確実に解放されるようになります。

  • runtime/debug.FreeOSMemory() の変更: runtime/debug.FreeOSMemory()もまた、runtime·gc(1)を呼び出していました。これが、メモリが即座にOSに返還されない根本原因でした。このコミットにより、runtime/debug.FreeOSMemory()runtime·gc(2)を呼び出すように変更されました。これにより、FreeOSMemory()が呼び出された際に、GCのマーキングフェーズとスイープフェーズの両方が強制的に、かつ即座に実行され、その結果として不要なメモリがより迅速にOSに返還されるようになります。

コアとなるコードの変更箇所

このコミットでは、以下の3つのファイルが変更されています。

  1. src/pkg/runtime/malloc.goc
  2. src/pkg/runtime/mgc0.c
  3. src/pkg/runtime/mheap.c

コアとなるコードの解説

src/pkg/runtime/malloc.goc

--- a/src/pkg/runtime/malloc.goc
+++ b/src/pkg/runtime/malloc.goc
@@ -849,16 +849,7 @@ runtime·cnewarray(Type *typ, intgo n)
 }

 func GC() {
-	// We assume that the user expects unused memory to have
-	// been freed when GC returns. To ensure this, run gc(1) twice.
-	// The first will do a collection, and the second will force the
-	// first's sweeping to finish before doing a second collection.
-	// The second collection is overkill, but we assume the user
-	// has a good reason for calling runtime.GC and can stand the
-	// expense. At the least, this fixes all the calls to runtime.GC in
-	// tests that expect finalizers to start running when GC returns.
-	runtime·gc(1);
-	runtime·gc(1);
+	runtime·gc(2);  // force GC and do eager sweep
 }

 func SetFinalizer(obj Eface, finalizer Eface) {

runtime.GC()関数の実装が変更されました。以前はruntime·gc(1)を2回呼び出していましたが、新しいforce=2のセマンティクスを利用して、runtime·gc(2)を1回呼び出すように簡素化されました。これにより、GCが強制実行され、かつ積極的なスイープが行われることが保証されます。

src/pkg/runtime/mgc0.c

--- a/src/pkg/runtime/mgc0.c
+++ b/src/pkg/runtime/mgc0.c
@@ -2239,6 +2239,7 @@ runtime·updatememstats(GCStats *stats)
 struct gc_args
 {
 	int64 start_time; // start time of GC in ns (just before stoptheworld)
+	bool  eagersweep;
 };

 static void gc(struct gc_args *args);

@@ -2257,6 +2258,8 @@ readgogc(void)
 	return runtime·atoi(p);
 }

+// force = 1 - do GC regardless of current heap usage
+// force = 2 - go GC and eager sweep
 void
 runtime·gc(int32 force)
 {
@@ -2292,7 +2295,7 @@ runtime·gc(int32 force)
 		return;

 	runtime·semacquire(&runtime·worldsema, false);
-	if(!force && mstats.heap_alloc < mstats.next_gc) {
+	if(force==0 && mstats.heap_alloc < mstats.next_gc) {
 		// typically threads which lost the race to grab
 		// worldsema exit here when gc is done.
 		runtime·semrelease(&runtime·worldsema);
@@ -2301,6 +2304,7 @@ runtime·gc(int32 force)

 	// Ok, we're doing it!  Stop everybody else
 	a.start_time = runtime·nanotime();
+	a.eagersweep = force >= 2;
 	m->gcing = 1;
 	runtime·stoptheworld();
 	
@@ -2490,7 +2494,7 @@ gc(struct gc_args *args)
 	sweep.spanidx = 0;

 	// Temporary disable concurrent sweep, because we see failures on builders.
-	if(ConcurrentSweep) {
+	if(ConcurrentSweep && !args->eagersweep) {
 		runtime·lock(&gclock);
 		if(sweep.g == nil)
 			sweep.g = runtime·newproc1(&bgsweepv, nil, 0, 0, runtime·gc);
  • struct gc_argseagersweepフィールドが追加され、GCの引数として積極的なスイープが必要かどうかが渡されるようになりました。
  • runtime·gc関数のコメントが更新され、force=1force=2の意味が明確化されました。
  • runtime·gc内で、force >= 2の場合にa.eagersweeptrueに設定されるようになりました。
  • GCの実行条件をチェックするif文がif(!force && ...)からif(force==0 && ...)に変更されました。これは、force0の場合のみ、ヒープ使用量に基づくGCのスキップを考慮するという、より厳密な条件になりました。
  • 並行スイープの実行条件がif(ConcurrentSweep)からif(ConcurrentSweep && !args->eagersweep)に変更されました。これにより、eagersweeptrueの場合には並行スイープが抑制され、即座のスイープが優先されるようになります。

src/pkg/runtime/mheap.c

--- a/src/pkg/runtime/mheap.c
+++ b/src/pkg/runtime/mheap.c
@@ -555,7 +555,7 @@ runtime·MHeap_Scavenger(void)
 void
 runtime∕debug·freeOSMemory(void)
 {
-	runtime·gc(1);
+	runtime·gc(2);  // force GC and do eager sweep
 	runtime·lock(&runtime·mheap);
 	scavenge(-1, ~(uintptr)0, 0);
 	runtime·unlock(&runtime·mheap);

runtime/debug.FreeOSMemory()関数の実装が変更されました。以前はruntime·gc(1)を呼び出していましたが、runtime·gc(2)を呼び出すように変更されました。これにより、FreeOSMemory()が呼び出された際に、GCが強制実行され、かつ積極的なスイープが行われることで、メモリがより迅速にOSに返還されるようになります。その後のscavenge呼び出しは、GCによって解放されたメモリを実際にOSに返還する役割を担います。

関連リンク

  • Go Issue: Fixes #8019 - debug.FreeOSMemoryがメモリを即座に解放しない問題に関する元のIssue。
  • Go CL (Change List): https://golang.org/cl/97550043 - このコミットに対応するGoのコードレビューシステム上の変更リスト。

参考にした情報源リンク

  • Goのソースコード(上記コミットの差分)
  • Goのガベージコレクションに関する一般的なドキュメントと解説
  • Issue #8019の議論
  • runtime.GC() および runtime/debug.FreeOSMemory() のGoドキュメント
  • Goの並行GCに関する技術記事# [インデックス 19391] ファイルの概要

このコミットは、Goランタイムにおけるdebug.FreeOSMemory関数の動作を修正し、メモリを即座にOSに解放するように改善するものです。これまでの実装では、ガベージコレクション(GC)のマーキングフェーズのみが実行され、スイープフェーズが実行されていなかったため、debug.FreeOSMemoryを呼び出してもメモリがすぐにOSに返還されない問題がありました。この変更により、GCのマーキングとスイープの両フェーズがdebug.FreeOSMemoryの呼び出し時に強制的に実行されるようになります。

コミット

  • コミットハッシュ: e893acf1848b96a922356eb753e8cea79f469afd
  • Author: Dmitriy Vyukov dvyukov@google.com
  • Date: Mon May 19 12:06:30 2014 +0400

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/e893acf1848b96a922356eb753e8cea79f469afd

元コミット内容

runtime: fix freeOSMemory to free memory immediately
Currently freeOSMemory makes only marking phase of GC, but not sweeping phase.
So recently memory is not released after freeOSMemory.
Do both marking and sweeping during freeOSMemory.
Fixes #8019.

LGTM=khr
R=golang-codereviews, khr
CC=golang-codereviews, rsc
https://golang.org/cl/97550043

変更の背景

Goのランタイムには、runtime/debugパッケージにFreeOSMemory()という関数が提供されています。この関数は、Goプログラムが使用しているメモリのうち、不要になったものをOSに返還することを試みるものです。しかし、このコミット以前のFreeOSMemory()の実装には問題がありました。

具体的には、FreeOSMemory()が内部でガベージコレクション(GC)をトリガーする際に、GCの「マーキングフェーズ」は実行されるものの、「スイープフェーズ」が適切に実行されていませんでした。GCのマーキングフェーズは、到達可能なオブジェクトを識別する役割を担いますが、実際にメモリを解放してOSに返還するためには、到達不能なオブジェクトが占めていたメモリ領域を回収するスイープフェーズが不可欠です。

このスイープフェーズが不足していたため、FreeOSMemory()を呼び出しても、期待通りにメモリがOSに即座に返還されず、アプリケーションのメモリ使用量が減少しないという問題(Issue #8019)が発生していました。特に、メモリ使用量が一時的に急増し、その後解放されるようなシナリオでは、OSレベルでのメモリフットプリントがなかなか減少しないことが課題となっていました。このコミットは、この問題を解決し、FreeOSMemory()がその名の通り、メモリを即座にOSに解放する機能を果たすようにするためのものです。

前提知識の解説

Goのガベージコレクション (GC) の基本

Goのガベージコレクションは、プログラムが動的に確保したメモリのうち、もはや到達不可能(参照されていない)になったオブジェクトを自動的に回収する仕組みです。GoのGCは、主に以下の2つのフェーズで構成されます。

  1. マーキングフェーズ (Marking Phase): このフェーズでは、GCはプログラムのルート(グローバル変数、スタック上の変数など)から開始し、そこから到達可能なすべてのオブジェクトを「生きている(live)」としてマークします。このプロセスは、オブジェクト間の参照をたどることで行われます。GoのGCは、このマーキングフェーズの一部をアプリケーションの実行と並行して(concurrently)行うことで、アプリケーションの一時停止時間(STW: Stop-The-World)を最小限に抑えるように設計されています。

  2. スイープフェーズ (Sweeping Phase): マーキングフェーズが完了した後、スイープフェーズが開始されます。このフェーズでは、マーキングされなかった(つまり、到達不可能と判断された)オブジェクトが占めていたメモリ領域が解放され、再利用可能な状態になります。GoのGCでは、このスイープフェーズも通常はアプリケーションの実行と並行して行われます。解放されたメモリは、Goランタイムのヒープに戻され、必要に応じてOSに返還される可能性があります。

runtime.GC()debug.FreeOSMemory() の役割

  • runtime.GC(): この関数は、Goランタイムにガベージコレクションの実行を明示的に要求します。通常、GoのGCは自動的にバックグラウンドで実行されますが、runtime.GC()を呼び出すことで、開発者は特定のタイミングでGCを強制的に実行させることができます。これは、例えばベンチマークの前にヒープをクリーンアップしたい場合や、メモリ使用量を一時的に削減したい場合などに利用されます。

  • runtime/debug.FreeOSMemory(): この関数は、Goランタイムが現在保持しているが、もはや使用していないメモリをオペレーティングシステムに返還することを試みます。Goランタイムは、一度OSから確保したメモリをすぐにOSに返還せず、将来の割り当てのために内部で保持しておくことがあります。これは、頻繁なOSコールを避けてパフォーマンスを向上させるためですが、場合によってはOSレベルでのメモリ使用量を減らしたいというニーズがあります。FreeOSMemory()は、そのような場合に、Goランタイムが保持しているアイドル状態のメモリをOSに積極的に返還するよう促すために使用されます。

メモリがOSに解放される仕組み

Goランタイムは、OSからメモリをページ単位(通常は4KBや8KB)で確保します。これらのページは、Goのヒープとして管理され、オブジェクトの割り当てに使用されます。オブジェクトがGCによって回収されると、そのオブジェクトが占めていたページは空きページとなります。Goランタイムは、これらの空きページをすぐにOSに返還するのではなく、将来の割り当てのために内部のフリーリストに保持しておくことが一般的です。

OSにメモリを返還するプロセスは「スカベンジング(scavenging)」と呼ばれます。これは、Goランタイムが一定期間使用されていない空きページを識別し、それらをOSに返還する操作です。debug.FreeOSMemory()は、このスカベンジングプロセスをトリガーし、Goランタイムが保持しているアイドルなメモリをOSに積極的に返還するように促します。しかし、このコミット以前は、GCのスイープフェーズが適切に完了しないと、スカベンジングの対象となるべきメモリが正確に識別されず、結果としてOSへのメモリ返還が遅延していました。

技術的詳細

このコミットの核心は、runtime·gc関数のforce引数に新しいセマンティクス(意味)を追加し、それを利用してruntime.GC()debug.FreeOSMemory()の動作を変更した点にあります。

runtime·gc 関数の force 引数の新しい意味

以前のruntime·gc関数は、force引数に1を渡すと、現在のヒープ使用量に関わらずGCを強制実行するという意味合いでした。このコミットでは、force引数に2という新しい値が導入されました。

  • force = 1: 現在のヒープ使用量に関わらずGCを強制実行します。
  • force = 2: GCを強制実行し、さらに「eager sweep(積極的なスイープ)」を実行します。

「eager sweep」とは、GCのスイープフェーズを即座に、かつ完全に実行することを意味します。通常、GoのGCのスイープフェーズは、アプリケーションの実行と並行して、バックグラウンドで徐々に実行されます。これにより、アプリケーションのレイテンシへの影響を最小限に抑えます。しかし、force=2が指定された場合、この並行スイープを待つことなく、スイープフェーズが直ちに完了するように強制されます。

eagersweep フラグの導入とその目的

src/pkg/runtime/mgc0.cstruct gc_argsbool eagersweep;という新しいフィールドが追加されました。このフラグは、runtime·gc関数が呼び出された際に、force引数が2以上である場合にtrueに設定されます(a.eagersweep = force >= 2;)。

このeagersweepフラグの目的は、GCのスイープフェーズを即座に完了させる必要があるかどうかをGCルーチンに伝えることです。特に、debug.FreeOSMemory()のように、メモリをOSに即座に返還したいという強い要求がある場合に、このフラグが利用されます。

並行スイープとEager Sweepの関係

src/pkg/runtime/mgc0.cgc関数内で、並行スイープを制御する条件が変更されました。

変更前:

if(ConcurrentSweep) {
    // ...
}

変更後:

if(ConcurrentSweep && !args->eagersweep) {
    // ...
}

この変更は非常に重要です。eagersweepフラグがtrueの場合(つまり、force=2でGCが呼び出された場合)、ConcurrentSweepが有効であっても、並行スイープのロジックがスキップされることを意味します。これは、eager sweepが要求された際には、並行スイープによる遅延を許容せず、スイープを即座に完了させることを優先するためです。並行スイープはバックグラウンドで徐々にメモリを解放しますが、eager sweepはGC呼び出しのコンテキスト内でスイープを完了させるため、両者は排他的な関係にあります。

runtime.GC()debug.FreeOSMemory()runtime·gc(2) を呼び出すようになったことの意味

  • runtime.GC() の変更: 以前のruntime.GC()は、runtime·gc(1)を2回呼び出していました。これは、「ユーザーはGCが戻ったときに未使用メモリが解放されることを期待している」という仮定に基づき、1回目のGCでコレクションを行い、2回目のGCで1回目のスイープが完了することを強制するという意図がありました。しかし、このアプローチは「2回目のコレクションはやりすぎ」とコメントされており、非効率的でした。 このコミットにより、runtime.GC()は単一のruntime·gc(2)呼び出しに簡素化されました。これにより、GCが強制実行され、かつeager sweepが実行されるため、runtime.GC()が戻る時点でメモリがより確実に解放されるようになります。

  • runtime/debug.FreeOSMemory() の変更: runtime/debug.FreeOSMemory()もまた、runtime·gc(1)を呼び出していました。これが、メモリが即座にOSに返還されない根本原因でした。このコミットにより、runtime/debug.FreeOSMemory()runtime·gc(2)を呼び出すように変更されました。これにより、FreeOSMemory()が呼び出された際に、GCのマーキングフェーズとスイープフェーズの両方が強制的に、かつ即座に実行され、その結果として不要なメモリがより迅速にOSに返還されるようになります。

コアとなるコードの変更箇所

このコミットでは、以下の3つのファイルが変更されています。

  1. src/pkg/runtime/malloc.goc
  2. src/pkg/runtime/mgc0.c
  3. src/pkg/runtime/mheap.c

コアとなるコードの解説

src/pkg/runtime/malloc.goc

--- a/src/pkg/runtime/malloc.goc
+++ b/src/pkg/runtime/malloc.goc
@@ -849,16 +849,7 @@ runtime·cnewarray(Type *typ, intgo n)
 }

 func GC() {
-	// We assume that the user expects unused memory to have
-	// been freed when GC returns. To ensure this, run gc(1) twice.
-	// The first will do a collection, and the second will force the
-	// first's sweeping to finish before doing a second collection.
-	// The second collection is overkill, but we assume the user
-	// has a good reason for calling runtime.GC and can stand the
-	// expense. At the least, this fixes all the calls to runtime.GC in
-	// tests that expect finalizers to start running when GC returns.
-	runtime·gc(1);
-	runtime·gc(1);
+	runtime·gc(2);  // force GC and do eager sweep
 }

 func SetFinalizer(obj Eface, finalizer Eface) {

runtime.GC()関数の実装が変更されました。以前はruntime·gc(1)を2回呼び出していましたが、新しいforce=2のセマンティクスを利用して、runtime·gc(2)を1回呼び出すように簡素化されました。これにより、GCが強制実行され、かつ積極的なスイープが行われることが保証されます。

src/pkg/runtime/mgc0.c

--- a/src/pkg/runtime/mgc0.c
+++ b/src/pkg/runtime/mgc0.c
@@ -2239,6 +2239,7 @@ runtime·updatememstats(GCStats *stats)
 struct gc_args
 {
 	int64 start_time; // start time of GC in ns (just before stoptheworld)
+	bool  eagersweep;
 };

 static void gc(struct gc_args *args);

@@ -2257,6 +2258,8 @@ readgogc(void)
 	return runtime·atoi(p);
 }

+// force = 1 - do GC regardless of current heap usage
+// force = 2 - go GC and eager sweep
 void
 runtime·gc(int32 force)
 {
@@ -2292,7 +2295,7 @@ runtime·gc(int32 force)
 		return;

 	runtime·semacquire(&runtime·worldsema, false);
-	if(!force && mstats.heap_alloc < mstats.next_gc) {
+	if(force==0 && mstats.heap_alloc < mstats.next_gc) {
 		// typically threads which lost the race to grab
 		// worldsema exit here when gc is done.
 		runtime·semrelease(&runtime·worldsema);
@@ -2301,6 +2304,7 @@ runtime·gc(int32 force)

 	// Ok, we're doing it!  Stop everybody else
 	a.start_time = runtime·nanotime();
+	a.eagersweep = force >= 2;
 	m->gcing = 1;
 	runtime·stoptheworld();
 	
@@ -2490,7 +2494,7 @@ gc(struct gc_args *args)
 	sweep.spanidx = 0;

 	// Temporary disable concurrent sweep, because we see failures on builders.
-	if(ConcurrentSweep) {
+	if(ConcurrentSweep && !args->eagersweep) {
 		runtime·lock(&gclock);
 		if(sweep.g == nil)
 			sweep.g = runtime·newproc1(&bgsweepv, nil, 0, 0, runtime·gc);
  • struct gc_argseagersweepフィールドが追加され、GCの引数として積極的なスイープが必要かどうかが渡されるようになりました。
  • runtime·gc関数のコメントが更新され、force=1force=2の意味が明確化されました。
  • runtime·gc内で、force >= 2の場合にa.eagersweeptrueに設定されるようになりました。
  • GCの実行条件をチェックするif文がif(!force && ...)からif(force==0 && ...)に変更されました。これは、force0の場合のみ、ヒープ使用量に基づくGCのスキップを考慮するという、より厳密な条件になりました。
  • 並行スイープの実行条件がif(ConcurrentSweep)からif(ConcurrentSweep && !args->eagersweep)に変更されました。これにより、eagersweeptrueの場合には並行スイープが抑制され、即座のスイープが優先されるようになります。

src/pkg/runtime/mheap.c

--- a/src/pkg/runtime/mheap.c
+++ b/src/pkg/runtime/mheap.c
@@ -555,7 +555,7 @@ runtime·MHeap_Scavenger(void)
 void
 runtime∕debug·freeOSMemory(void)
 {
-	runtime·gc(1);
+	runtime·gc(2);  // force GC and do eager sweep
 	runtime·lock(&runtime·mheap);
 	scavenge(-1, ~(uintptr)0, 0);
 	runtime·unlock(&runtime·mheap);

runtime/debug.FreeOSMemory()関数の実装が変更されました。以前はruntime·gc(1)を呼び出していましたが、runtime·gc(2)を呼び出すように変更されました。これにより、FreeOSMemory()が呼び出された際に、GCが強制実行され、かつ積極的なスイープが行われることで、メモリがより迅速にOSに返還されるようになります。その後のscavenge呼び出しは、GCによって解放されたメモリを実際にOSに返還する役割を担います。

関連リンク

  • Go Issue: Fixes #8019 - debug.FreeOSMemoryがメモリを即座に解放しない問題に関する元のIssue。
  • Go CL (Change List): https://golang.org/cl/97550043 - このコミットに対応するGoのコードレビューシステム上の変更リスト。

参考にした情報源リンク

  • Goのソースコード(上記コミットの差分)
  • Goのガベージコレクションに関する一般的なドキュメントと解説
  • Issue #8019の議論
  • runtime.GC() および runtime/debug.FreeOSMemory() のGoドキュメント
  • Goの並行GCに関する技術記事