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

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

このコミットは、Goランタイムのガベージコレクション(GC)に関連するmgc0.cファイル内のrunfinq関数におけるメモリリークを修正するものです。特に、sync.Poolのファイナライザテストが失敗する原因となっていた、C言語で書かれた関数内の未初期化データが古いメモリ領域を指し続ける問題に対処しています。

コミット

commit b08156cd874d9534776cd9ece8f6f4ab092a68a5
Author: Russ Cox <rsc@golang.org>
Date:   Fri Mar 7 11:27:01 2014 -0500

    runtime: fix memory leak in runfinq
    
    One reason the sync.Pool finalizer test can fail is that
    this function's ef1 contains uninitialized data that just
    happens to point at some of the old pool. I've seen this cause
    retention of a single pool cache line (32 elements) on arm.
    
    Really we need liveness information for C functions, but
    for now we can be more careful about data in long-lived
    C functions that block.
    
    LGTM=bradfitz, dvyukov
    R=golang-codereviews, bradfitz, dvyukov
    CC=golang-codereviews, iant, khr
    https://golang.org/cl/72490043

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

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

元コミット内容

Goランタイムのrunfinq関数におけるメモリリークを修正します。sync.Poolのファイナライザテストが失敗する一因として、この関数のef1変数が未初期化データを含み、それがたまたま古いプールの一部を指してしまうことが挙げられます。特にARMアーキテクチャでは、これにより単一のプールキャッシュライン(32要素)が保持され続けてしまう現象が確認されていました。

本来であればC関数に対するライブネス情報(変数が今後使用されるかどうかを示す情報)が必要ですが、当面の間は、長時間ブロックするC関数内のデータに対してより注意を払うことで対応します。

変更の背景

Goのガベージコレクタは、到達不能になったオブジェクトを自動的に解放しますが、ファイナライザが設定されたオブジェクトは、GCによって到達不能と判断された後、ファイナライザが実行されるまでメモリが解放されません。sync.Poolはオブジェクトの再利用を目的としたもので、GCの負荷を軽減するために使用されます。しかし、sync.Poolにオブジェクトを戻すためにファイナライザを使用するパターンは、GCがオブジェクトを解放しようとするたびにファイナライザがオブジェクトをプールに戻し、結果的にオブジェクトが再び到達可能になるというアンチパターンを引き起こす可能性があります。これにより、GCがオブジェクトを解放できず、メモリリークが発生する可能性があります。

このコミットの背景には、sync.Poolのファイナライザテストが失敗するという具体的な問題がありました。調査の結果、GoランタイムのC言語で実装されたrunfinq関数内で使用されるローカル変数、特にEface型のef1が、適切に初期化またはゼロクリアされていない場合に、古いsync.Poolのデータへの参照を保持し続けることが判明しました。C言語の関数はGoのランタイムが持つような詳細なライブネス情報を持たないため、コンパイラが変数を最適化する際に、その変数が古いポインタを保持し続ける可能性がありました。特にARMのような特定のアーキテクチャでこの問題が顕在化したのは、メモリの配置やコンパイラの最適化の挙動が影響していると考えられます。

この問題は、本来解放されるべきメモリが解放されずに残り続けるため、メモリ使用量が増加し、最終的にはシステムのパフォーマンス低下やメモリ枯渇につながる可能性がありました。

前提知識の解説

  • Goランタイム (Go Runtime): Goプログラムの実行を管理する低レベルのシステム。ガベージコレクション、スケジューリング、メモリ管理など、Go言語の多くの重要な機能を提供します。Goランタイムの一部はC言語やアセンブリ言語で書かれています。
  • ガベージコレクション (Garbage Collection, GC): プログラムが動的に割り当てたメモリのうち、もはや使用されない(到達不能な)領域を自動的に解放するプロセス。GoのGCは並行・並行(concurrent and parallel)なマーク&スイープ方式を採用しています。
  • ファイナライザ (Finalizer): runtime.SetFinalizer関数を使ってオブジェクトに設定できる特殊な関数。オブジェクトがガベージコレクタによって到達不能と判断され、メモリが解放される直前に実行されます。主にファイルディスクリプタやネットワーク接続など、Goのメモリ管理外のリソースを解放するために使用されます。ただし、ファイナライザの実行タイミングは保証されず、メモリリークの原因となるアンチパターンも存在します。
  • sync.Pool: 一時的なオブジェクトの再利用を目的としたGoの標準ライブラリの機能。オブジェクトの生成と破棄のコストを削減し、GCの負荷を軽減します。使用済みのオブジェクトをプールに戻し、必要に応じてプールから取得することで、オブジェクトの再利用を促進します。
  • Eface (Empty Interface): Goの空インターフェース(interface{})の内部表現。Goのインターフェースは、内部的には型情報と値(または値へのポインタ)の2つのワードで構成されます。Efaceは、任意の型の値を保持できるため、Goの型システムにおける柔軟性の基盤となります。
  • ライブネス情報 (Liveness Information): プログラム解析の概念で、ある時点である変数が将来のどこかで参照される可能性があるかどうかを示す情報。ガベージコレクタは、この情報に基づいてオブジェクトが到達可能かどうかを判断します。C言語のコンパイラはGoランタイムのような詳細なライブネス情報を常に生成するわけではないため、GoのGCがC関数内のポインタを正確に追跡するのが難しい場合があります。
  • USEDマクロ: GoランタイムのC言語部分で使われるマクロ。これは、コンパイラが最適化によって「未使用」と判断し、レジスタから削除してしまう可能性のある変数を、意図的にメモリにフラッシュさせる(メモリ上に存在させる)ために使用されます。これにより、GCがその変数を正しくスキャンできるようになります。

技術的詳細

このコミットは、Goランタイムのsrc/pkg/runtime/mgc0.cファイル内のrunfinq関数に焦点を当てています。mgc0.cは、Goの初期のガベージコレクタの実装の一部であり、runfinq関数はファイナライザキューを処理し、登録されたファイナライザを実行する役割を担っていました。

問題の核心は、C言語で書かれたrunfinq関数が、GoのGCが持つような詳細なライブネス情報を持たない点にありました。これにより、関数内のローカル変数(特にEface型のef1)が、以前のファイナライザ実行で処理されたオブジェクトへの古いポインタを保持し続ける可能性がありました。これらのポインタがゼロクリアされないままだと、GCはそれらのポインタが指すメモリ領域がまだ「到達可能」であると誤って判断し、結果としてメモリが解放されずにリークが発生していました。

コミットメッセージで言及されている「sync.Pool finalizer test can fail」という点は重要です。sync.Poolはオブジェクトの再利用を促進しますが、もしファイナライザがオブジェクトをsync.Poolに戻すようなロジックを含んでいた場合、GCがオブジェクトを解放しようとするたびにファイナライザが実行され、オブジェクトがプールに戻されることで、GCがそのオブジェクトを解放できなくなるという循環参照のような状態が発生し、メモリリークを引き起こす可能性があります。このコミットは、そのようなアンチパターンによって引き起こされるメモリリークを、runfinq関数自体の内部的な問題として修正しています。

修正は、runfinq関数内の主要なローカル変数を明示的にnil(またはゼロ値)に初期化し、ループの各イテレーションの終わりに再度ゼロクリアすることで行われます。これにより、これらの変数が古いポインタを保持し続けることを防ぎ、GCが正しくメモリを解放できるようになります。

また、USED(&f);のようなUSEDマクロの使用は、Cコンパイラがこれらの変数を最適化によって削除しないようにするためのものです。これにより、変数がメモリ上に確実に存在し、GCがそれらをスキャンして到達可能性を判断できるようになります。

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

src/pkg/runtime/mgc0.cファイルのrunfinq関数に以下の変更が加えられました。

--- a/src/pkg/runtime/mgc0.c
+++ b/src/pkg/runtime/mgc0.c
@@ -2525,8 +2525,29 @@ runfinq(void)
 	uint32 framesz, framecap, i;
 	Eface *ef, ef1;
 
+	// This function blocks for long periods of time, and because it is written in C
+	// we have no liveness information. Zero everything so that uninitialized pointers
+	// do not cause memory leaks.
+	f = nil;
+	fb = nil;
+	next = nil;
 	frame = nil;
 	framecap = 0;
+	framesz = 0;
+	i = 0;
+	ef = nil;
+	ef1.type = nil;
+	ef1.data = nil;
+	
+	// force flush to memory
+	USED(&f);
+	USED(&fb);
+	USED(&next);
+	USED(&framesz);
+	USED(&i);
+	USED(&ef);
+	USED(&ef1);
+
 	for(;;) {
 		runtime·lock(&gclock);
 		fb = finq;
@@ -2581,6 +2602,16 @@ runfinq(void)
 			finc = fb;
 			runtime·unlock(&gclock);
 		}
+
+		// Zero everything that's dead, to avoid memory leaks.
+		// See comment at top of function.
+		f = nil;
+		fb = nil;
+		next = nil;
+		i = 0;
+		ef = nil;
+		ef1.type = nil;
+		ef1.data = nil;
 		runtime·gc(1);	// trigger another gc to clean up the finalized objects, if possible
 	}
 }

コアとなるコードの解説

変更は主にrunfinq関数の冒頭と、無限ループの各イテレーションの終わりに集中しています。

  1. 関数の冒頭での初期化とゼロクリア:

    • f = nil; fb = nil; next = nil;
    • frame = nil; framecap = 0; framesz = 0; i = 0;
    • ef = nil; ef1.type = nil; ef1.data = nil; これらの行は、runfinq関数が開始される際に、関数内で使用されるポインタ型変数(f, fb, next, frame, ef)およびEface型のef1の各フィールド(type, data)を明示的にnilまたはゼロ値に初期化しています。これにより、これらの変数が以前の実行や未初期化状態から古いメモリ参照を保持することを防ぎます。
  2. USEDマクロの適用:

    • USED(&f); USED(&fb); USED(&next); USED(&framesz); USED(&i); USED(&ef); USED(&ef1); これらのUSEDマクロは、Cコンパイラに対して、これらの変数が「使用されている」ことを示し、最適化によってレジスタから削除されたり、メモリからフラッシュされたりしないように指示します。これにより、GoのGCがこれらの変数をスキャンする際に、それらが指すメモリ領域の到達可能性を正しく判断できるようになります。これは、C関数がGoのGCのような詳細なライブネス情報を持たないことへの対処策です。
  3. ループ内でのゼロクリア:

    • f = nil; fb = nil; next = nil; i = 0; ef = nil; ef1.type = nil; ef1.data = nil; for(;;)ループの各イテレーションの終わりに、これらの変数が再度nilまたはゼロ値に設定されます。これは、ファイナライザの処理が完了し、次のGCがトリガーされる前に、これらの変数が古い参照を保持しないようにするためです。これにより、長期にわたるrunfinqの実行中にメモリリークが発生するリスクをさらに低減します。

これらの変更は、C言語で書かれたGoランタイムの低レベル部分におけるメモリ管理の堅牢性を向上させ、特にsync.PoolのようなGoの高度な機能と連携する際の潜在的なメモリリークを防ぐことを目的としています。

関連リンク

参考にした情報源リンク