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

[インデックス 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を強制的にトリガーします。このメカニズムが、今回のコミットで修正された問題の対象でした。

技術的詳細

このコミットの核心は、時間基準の不一致を解消することにあります。

  1. 問題の特定: src/pkg/runtime/mheap.c内のruntime·MHeap_Scavenger関数(GCの補助的な役割を担うゴルーチン)において、GCを強制的にトリガーするかどうかを判断するロジックがありました。

    if(now - mstats.last_gc > forcegc) {
    

    ここで、nowruntime·nanotime()によって取得されるモノトニック時間であり、mstats.last_gcはUnix時間でした。異なる時間基準の値を直接減算して比較することは、常に予期せぬ結果(この場合は、nowmstats.last_gcよりも常に大きく、かつその差が急速に開くため、forcegcの閾値をすぐに超えてしまう)を招きます。

  2. 解決策の導入:

    • src/pkg/runtime/runtime.hに新しい関数runtime·unixnanotime()が宣言されました。この関数は、Unix時間をナノ秒単位で返すことを明確に示しています。
    • src/pkg/runtime/time.gocruntime·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) {
      
      これにより、unixnowmstats.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(&note, 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);

コアとなるコードの解説

  1. src/pkg/runtime/runtime.h:

    • runtime·nanotime(void)のコメントが// monotonic timeに変更され、その性質が明確化されました。
    • 新しくint64 runtime·unixnanotime(void); // real time, can skipが追加されました。これは、Unix時間を返す新しい関数であり、システム時刻の変更によって「スキップ」(ジャンプ)する可能性がある「リアルタイム」であることを示しています。
  2. src/pkg/runtime/time.goc:

    • runtime·unixnanotime関数の実装が追加されました。この関数は、内部的にruntime·gc_unixnanotimeを呼び出し、その結果を返します。これにより、Goランタイム内でUnix時間を取得するための標準的なインターフェースが提供されます。
  3. 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の前方宣言が削除されました。
  4. src/pkg/runtime/mheap.c:

    • runtime·MHeap_Scavenger関数内で、強制GCのトリガーロジックが変更されました。
      • 新しいローカル変数unixnowが導入されました。
      • unixnow = runtime·unixnanotime();によって、現在のUnix時間が取得されます。
      • 強制GCの条件式がif(unixnow - mstats.last_gc > forcegc)に変更されました。
    • この変更が最も重要です。mstats.last_gcunixnowが両方ともUnix時間ベースになったことで、GCが最後に実行されてからの実際の経過時間を正確に計算できるようになりました。これにより、時間基準の不一致による不正確な強制GCのトリガーが解消され、GCが適切に制御されるようになります。

これらの変更により、GoランタイムはGCの強制トリガーをより正確に管理できるようになり、不必要なGCの実行が抑制され、全体的なパフォーマンスと安定性が向上します。

関連リンク

参考にした情報源リンク

  • 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のドキュメントなど)