[インデックス 19346] ファイルの概要
このコミットは、Goランタイムにおける強制ガベージコレクション(GC)のトリガーロジックのバグを修正するものです。具体的には、mstats.last_gc
(最後のGC実行時刻)がUnix時間で記録されているにもかかわらず、GCの強制トリガーを判断する際にモノトニック時間と比較されていた問題を解決します。これにより、GCが意図せず頻繁に強制実行される状況が改善されます。
コミット
commit a12661329b81675267303602bf16493608ec7bed
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Tue May 13 09:53:03 2014 +0400
runtime: fix triggering of forced GC
mstats.last_gc is unix time now, it is compared with abstract monotonic time.
On my machine GC is forced every 5 mins regardless of last_gc.
LGTM=rsc
R=golang-codereviews
CC=golang-codereviews, iant, rsc
https://golang.org/cl/91350045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/a12661329b81675267303602bf16493608ec7bed
元コミット内容
runtime: fix triggering of forced GC
mstats.last_gc is unix time now, it is compared with abstract monotonic time.
On my machine GC is forced every 5 mins regardless of last_gc.
変更の背景
Goランタイムのガベージコレクション(GC)は、メモリ使用量に基づいて自動的に実行されますが、特定の条件下ではGCが強制的にトリガーされることがあります。このコミットが修正しようとしている問題は、この強制GCのトリガーロジックにありました。
コミットメッセージによると、作者の環境では「last_gc
に関わらず5分ごとにGCが強制されている」という現象が発生していました。これは、mstats.last_gc
という変数が最後のGCが実行された時刻を記録しているにもかかわらず、その値が正しく利用されていないことを示唆しています。
根本的な原因は、mstats.last_gc
がUnix時間(1970年1月1日からの経過時間)で記録されているのに対し、GCの強制トリガーを判断する際に使用されるruntime·nanotime()
が返す値がモノトニック時間(システム起動からの経過時間など、システム時刻の変更に影響されない時間)であったため、異なる時間基準の値を比較していたことにあります。この時間基準の不一致により、last_gc
からの経過時間が常に大きく見え、GCが不必要に頻繁に強制実行されるというバグが発生していました。
前提知識の解説
Goのガベージコレクション (GC)
Go言語は、自動メモリ管理のためにガベージコレクション(GC)を採用しています。GoのGCは、主に三色マーク&スイープアルゴリズムをベースにしており、並行処理と低レイテンシを重視して設計されています。GCは、ヒープメモリの使用量がある閾値を超えた場合や、特定の条件(例えば、一定時間GCが実行されていない場合)で自動的にトリガーされます。
mstats.last_gc
mstats
はGoランタイムのメモリ統計情報を含む構造体です。mstats.last_gc
は、この構造体の一部であり、最後にガベージコレクションが完了した時刻をナノ秒単位のUnix時間で記録しています。この値は、GCの実行頻度を制御したり、GCが長時間実行されていない場合に強制的にトリガーしたりするための基準として使用されます。
モノトニック時間とUnix時間
- モノトニック時間 (Monotonic Time): システムが起動してからの経過時間など、システム時刻の変更(NTP同期や手動での時刻変更など)に影響されずに単調に増加する時間です。時間の「流れ」を測定するのに適しており、経過時間の計算によく用いられます。Goランタイムの
runtime·nanotime()
関数は、このモノトニック時間をナノ秒単位で返します。 - Unix時間 (Unix Time / Epoch Time): 1970年1月1日00:00:00 UTC(Unixエポック)からの経過秒数(またはナノ秒数)で表される時間です。これは絶対的な時刻を表し、ファイルシステムやネットワークプロトコルなど、異なるシステム間で時刻を同期する際に広く使用されます。Goランタイムの
runtime·unixnanotime()
関数は、このUnix時間をナノ秒単位で返します。
強制GC (Forced GC)
Goランタイムは、メモリ使用量だけでなく、GCが最後に実行されてからの経過時間も考慮してGCをトリガーします。これは、プログラムがほとんどメモリを割り当てない場合でも、GCが全く実行されないことを防ぐためです。一定時間GCが実行されていない場合、ランタイムはGCを強制的にトリガーします。このメカニズムが、今回のコミットで修正された問題の対象でした。
技術的詳細
このコミットの核心は、時間基準の不一致を解消することにあります。
-
問題の特定:
src/pkg/runtime/mheap.c
内のruntime·MHeap_Scavenger
関数(GCの補助的な役割を担うゴルーチン)において、GCを強制的にトリガーするかどうかを判断するロジックがありました。if(now - mstats.last_gc > forcegc) {
ここで、
now
はruntime·nanotime()
によって取得されるモノトニック時間であり、mstats.last_gc
はUnix時間でした。異なる時間基準の値を直接減算して比較することは、常に予期せぬ結果(この場合は、now
がmstats.last_gc
よりも常に大きく、かつその差が急速に開くため、forcegc
の閾値をすぐに超えてしまう)を招きます。 -
解決策の導入:
src/pkg/runtime/runtime.h
に新しい関数runtime·unixnanotime()
が宣言されました。この関数は、Unix時間をナノ秒単位で返すことを明確に示しています。src/pkg/runtime/time.goc
にruntime·unixnanotime()
の実装が追加されました。これは内部的にruntime·gc_unixnanotime
を呼び出してUnix時間を取得します。src/pkg/runtime/mgc0.c
では、GC完了時にmstats.last_gc
を更新する際に、直接runtime·unixnanotime()
を呼び出すように変更されました。これにより、mstats.last_gc
が確実にUnix時間で設定されるようになります。- 最も重要な変更は
src/pkg/runtime/mheap.c
です。runtime·MHeap_Scavenger
内で、強制GCのトリガー判断に使用するnow
の代わりに、新しく導入されたunixnow = runtime·unixnanotime()
を使用するように変更されました。
これにより、unixnow = runtime·unixnanotime(); if(unixnow - mstats.last_gc > forcegc) {
unixnow
とmstats.last_gc
が両方ともUnix時間ベースとなり、正しい時間差の比較が可能になりました。
この修正により、GCの強制トリガーが意図した通りに、つまり最後のGCから実際に一定のUnix時間が経過した場合にのみ発生するようになり、不必要なGCの頻発が解消されました。
コアとなるコードの変更箇所
src/pkg/runtime/mgc0.c
--- a/src/pkg/runtime/mgc0.c
+++ b/src/pkg/runtime/mgc0.c
@@ -91,8 +91,6 @@ enum {
// Initialized from $GOGC. GOGC=off means no gc.
static int32 gcpercent = GcpercentUnknown;
-void runtime·gc_unixnanotime(int64 *now);
-
static FuncVal* poolcleanup;
void
@@ -2406,7 +2404,7 @@ gc(struct gc_args *args)\n mstats.next_gc = mstats.heap_alloc+mstats.heap_alloc*gcpercent/100;\n \n t4 = runtime·nanotime();\n- runtime·gc_unixnanotime((int64*)&mstats.last_gc); // must be Unix time to make sense to user\n+ mstats.last_gc = runtime·unixnanotime(); // must be Unix time to make sense to user\n mstats.pause_ns[mstats.numgc%nelem(mstats.pause_ns)] = t4 - t0;\n mstats.pause_total_ns += t4 - t0;\n mstats.numgc++;
src/pkg/runtime/mheap.c
--- a/src/pkg/runtime/mheap.c
+++ b/src/pkg/runtime/mheap.c
@@ -508,6 +508,7 @@ runtime·MHeap_Scavenger(void)\n {\n MHeap *h;\n uint64 tick, now, forcegc, limit;\n+ int64 unixnow;\n int32 k;\n Note note, *notep;\n
@@ -531,8 +532,8 @@ runtime·MHeap_Scavenger(void)\n \truntime·notetsleepg(¬e, tick);\n
\truntime·lock(h);\n-\t\tnow = runtime·nanotime();\n-\t\tif(now - mstats.last_gc > forcegc) {\n+\t\tunixnow = runtime·unixnanotime();\n+\t\tif(unixnow - mstats.last_gc > forcegc) {\n \t\truntime·unlock(h);\n \t\t// The scavenger can not block other goroutines,\n \t\t// otherwise deadlock detector can fire spuriously.\n@@ -544,8 +545,8 @@ runtime·MHeap_Scavenger(void)\n \t\tif(runtime·debug.gctrace > 0)\n \t\t\truntime·printf(\"scvg%d: GC forced\\n\", k);\n \t\truntime·lock(h);\n-\t\t\tnow = runtime·nanotime();\n \t\t}\n+\t\tnow = runtime·nanotime();\n \tscavenge(k, now, limit);\n \truntime·unlock(h);\n }
src/pkg/runtime/runtime.h
--- a/src/pkg/runtime/runtime.h
+++ b/src/pkg/runtime/runtime.h
@@ -920,7 +920,8 @@ void runtime·exitsyscall(void);\n G* runtime·newproc1(FuncVal*, byte*, int32, int32, void*);\n bool runtime·sigsend(int32 sig);\n int32 runtime·callers(int32, uintptr*, int32);\n-int64 runtime·nanotime(void);\n+int64 runtime·nanotime(void); // monotonic time\n+int64 runtime·unixnanotime(void); // real time, can skip\n void runtime·dopanic(int32);\n void runtime·startpanic(void);\n void runtime·freezetheworld(void);
src/pkg/runtime/time.goc
--- a/src/pkg/runtime/time.goc
+++ b/src/pkg/runtime/time.goc
@@ -54,6 +54,16 @@ func stopTimer(t *Timer) (stopped bool) {
// C runtime.
+void runtime·gc_unixnanotime(int64 *now);\n+
+int64 runtime·unixnanotime(void)\n+{\n+\tint64 now;\n+\n+\truntime·gc_unixnanotime(&now);\n+\treturn now;\n+}\n+\n static void timerproc(void);\n static void siftup(int32);\n static void siftdown(int32);
コアとなるコードの解説
-
src/pkg/runtime/runtime.h
:runtime·nanotime(void)
のコメントが// monotonic time
に変更され、その性質が明確化されました。- 新しく
int64 runtime·unixnanotime(void); // real time, can skip
が追加されました。これは、Unix時間を返す新しい関数であり、システム時刻の変更によって「スキップ」(ジャンプ)する可能性がある「リアルタイム」であることを示しています。
-
src/pkg/runtime/time.goc
:runtime·unixnanotime
関数の実装が追加されました。この関数は、内部的にruntime·gc_unixnanotime
を呼び出し、その結果を返します。これにより、Goランタイム内でUnix時間を取得するための標準的なインターフェースが提供されます。
-
src/pkg/runtime/mgc0.c
:gc
関数内で、mstats.last_gc
を更新する行が変更されました。- 変更前:
runtime·gc_unixnanotime((int64*)&mstats.last_gc);
- 変更後:
mstats.last_gc = runtime·unixnanotime();
- 変更前:
- これにより、
mstats.last_gc
には確実にUnix時間(ナノ秒単位)が代入されるようになります。また、不要になったruntime·gc_unixnanotime
の前方宣言が削除されました。
-
src/pkg/runtime/mheap.c
:runtime·MHeap_Scavenger
関数内で、強制GCのトリガーロジックが変更されました。- 新しいローカル変数
unixnow
が導入されました。 unixnow = runtime·unixnanotime();
によって、現在のUnix時間が取得されます。- 強制GCの条件式が
if(unixnow - mstats.last_gc > forcegc)
に変更されました。
- 新しいローカル変数
- この変更が最も重要です。
mstats.last_gc
とunixnow
が両方ともUnix時間ベースになったことで、GCが最後に実行されてからの実際の経過時間を正確に計算できるようになりました。これにより、時間基準の不一致による不正確な強制GCのトリガーが解消され、GCが適切に制御されるようになります。
これらの変更により、GoランタイムはGCの強制トリガーをより正確に管理できるようになり、不必要なGCの実行が抑制され、全体的なパフォーマンスと安定性が向上します。
関連リンク
- Go CL 91350045: https://golang.org/cl/91350045
参考にした情報源リンク
- Go runtime.MemStats.LastGC: https://pkg.go.dev/runtime#MemStats
- Go 1.3beta1 MemStats.LastGC bug: (Web検索結果より、Go 1.3beta1で
MemStats.LastGC
が正しくUnix epoch timeを報告しないバグがあったことが示唆されています。具体的なバグトラッカーのリンクは特定できませんでしたが、このコミットの背景と一致します。) - Goのガベージコレクションに関する一般的な情報源 (例: Go公式ブログ、Goのドキュメントなど)
- The Go Blog: Go's new GC: https://go.dev/blog/go15gc (これはGo 1.5のGCに関するものですが、GoのGCの進化を理解する上で参考になります。)
- Go runtime source code: https://github.com/golang/go/tree/master/src/runtime