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

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

このコミットは、Goランタイムにおけるチャネル(channels)のガベージコレクション(GC)をより「正確(precise)」にするための変更を導入しています。具体的には、チャネルがヒープ上に確保されたメモリを適切にGCが認識し、回収できるようにするための改善が含まれています。これにより、チャネルが原因で発生する可能性のあるメモリリーク問題が解決されます。

コミット

commit a656f82071c1631ed0aae5c403cf948fc06b52ce
Author: Jan Ziak <0xe2.0x9a.0x9b@gmail.com>
Date:   Mon Feb 25 15:58:23 2013 -0500

    runtime: precise garbage collection of channels
    
    This changeset adds a mostly-precise garbage collection of channels.
    The garbage collection support code in the linker isn't recognizing
    channel types yet.
    
    Fixes issue http://stackoverflow.com/questions/14712586/memory-consumption-skyrocket
    
    R=dvyukov, rsc, bradfitz
    CC=dave, golang-dev, minux.ma, remyoudompheng
    https://golang.org/cl/7307086
---
 src/pkg/runtime/chan.c    |  5 +++++
 src/pkg/runtime/malloc.h  |  1 +
 src/pkg/runtime/mgc0.c    | 33 +++++++++++++++++++++++++++++++++
 src/pkg/runtime/runtime.h |  1 +
 4 files changed, 40 insertions(+)

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

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

元コミット内容

runtime: precise garbage collection of channels

This changeset adds a mostly-precise garbage collection of channels.
The garbage collection support code in the linker isn't recognizing
channel types yet.

Fixes issue http://stackoverflow.com/questions/14712586/memory-consumption-skyrocket

変更の背景

この変更の主な背景には、Goのチャネルが適切にガベージコレクションされないことによるメモリ消費の増大問題がありました。コミットメッセージに記載されている Fixes issue http://stackoverflow.com/questions/14712586/memory-consumption-skyrocket は、Stack Overflowで報告された、time.Ticker のようなチャネルを内部的に使用するGoの機能が、適切に停止されない場合にメモリリークを引き起こすという問題を示唆しています。

Goのガベージコレクタは、到達不能になったオブジェクトのメモリを自動的に解放しますが、チャネルのような複雑なデータ構造の場合、その内部に含まれるポインタやデータがGCによって正確に追跡されないと、メモリが解放されずに残り続ける可能性があります。このコミットは、特にチャネルの内部構造(Hchan)がヒープ上のポインタを適切にGCに認識させることで、この問題を解決しようとしています。

当時のGoのGCは、チャネルの型を完全に認識していなかったため、チャネルが保持するデータがGCの対象外と見なされ、メモリが解放されない状況が発生していました。このコミットは、チャネルのGCサポートを改善し、メモリ消費の「急増」を防ぐことを目的としています。

前提知識の解説

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

Goのガベージコレクタは、正確(precise)並行(concurrent)トライカラー(tri-color)、**マーク&スイープ(mark-and-sweep)**方式を採用しています。

  • 正確(Precise)GC: GoのGCは、メモリ内の値がポインタであるか非ポインタであるかを正確に区別できます。これにより、GCは到達可能なオブジェクトを確実に識別し、誤って使用中のメモリを解放したり、到達不能なメモリを解放し損ねたりするリスクを最小限に抑えます。Go 1.3以降、ランタイムはポインタ型を持つ値がポインタを含むという基本的な仮定を置いています。
  • 並行(Concurrent)GC: GCの大部分のフェーズが、ユーザープログラムの実行と並行して動作します。これにより、GCによるアプリケーションの一時停止(ストップ・ザ・ワールド)時間を最小限に抑え、レイテンシを低減します。
  • トライカラー(Tri-color)マーク&スイープ: GCはオブジェクトを白、灰、黒の3色に分類します。
    • : まだGCによって訪問されていない、または到達不能と見なされるオブジェクト。最終的に解放される候補。
    • : GCによって訪問されたが、その参照先(子オブジェクト)がまだ訪問されていないオブジェクト。
    • : GCによって訪問され、その参照先もすべて訪問されたオブジェクト。到達可能と見なされる。 GCはルート(グローバル変数、スタック変数、ゴルーチンのスタックなど)から開始し、到達可能なオブジェクトをマーク(色付け)していきます。マークフェーズの完了後、スイープフェーズで白いオブジェクト(到達不能なオブジェクト)のメモリを解放します。

Goのチャネル(Channels)

Goのチャネルは、ゴルーチン間で値を安全に送受信するための通信メカニズムです。チャネルは、Goの並行処理モデルの根幹をなす要素であり、共有メモリによる競合状態を避けるために推奨される「通信によってメモリを共有する」という哲学を体現しています。

チャネルには、バッファなしチャネルとバッファ付きチャネルがあります。

  • バッファなしチャネル: 送信操作と受信操作が同期的に行われます。送信側は受信側が値を受け取るまでブロックされ、受信側は送信側が値を送るまでブロックされます。
  • バッファ付きチャネル: 指定された数の値を格納できる内部バッファを持ちます。バッファが満杯になるまで送信操作はブロックされず、バッファが空になるまで受信操作はブロックされません。

Hchan 構造体

Goのランタイム内部では、チャネルは Hchan というC言語の構造体で表現されます。この構造体は、チャネルのキューのカウント、要素のサイズ、要素の型情報、送受信待ちのゴルーチンキュー、ミューテックスなどのチャネルの動作に必要なメタデータを含んでいます。

Hchan 構造体自体はヒープに割り当てられます。そして、バッファ付きチャネルの場合、そのバッファも Hchan 構造体の直後にメモリ上に配置されることが一般的です。このバッファには、チャネルを通じて送受信される実際のデータが格納されます。

このコミットの文脈では、Hchan 構造体とそのバッファがGCによってどのように扱われるかが重要になります。特に、チャネルのバッファ内にポインタが含まれる場合、GCがそのポインタを正確に追跡し、参照先のオブジェクトが到達可能であるかを判断できる必要があります。

技術的詳細

このコミットは、Goランタイムのガベージコレクタがチャネルをより正確に処理できるようにするための複数の変更を含んでいます。

  1. Hchan 構造体へのGCの認識の追加:

    • src/pkg/runtime/chan.cHchan 構造体の定義に、GCがスタック内のポインタのみを想定しており、ヒープ内のポインタを含まないというコメントが追加されています。これは、Hchan 自体はポインタを含まないが、そのバッファはポインタを含む可能性があるという前提を示唆しています。
    • runtime·Hchansize というグローバル変数が追加され、Hchan 構造体のサイズをGCが利用できるようにしています。
    • runtime·makechan_c 関数(チャネル作成の内部関数)で、新しく作成されたチャネルオブジェクトに TypeInfo_Chan 型情報を設定する runtime·settype が呼び出されるようになりました。これにより、GCはオブジェクトがチャネルであることを認識できます。
  2. TypeInfo_Chan の導入:

    • src/pkg/runtime/malloc.hTypeInfo_Chan = 3 という新しい列挙型が追加されました。これは、ヒープに割り当てられたオブジェクトの型情報をGCが識別するために使用されます。これにより、GCはチャネルオブジェクトを特別な方法で処理する必要があることを認識します。
  3. mgc0.c におけるGCロジックの更新:

    • src/pkg/runtime/mgc0.c はGoのGCのコアロジックを含むファイルです。
    • GC_CHAN という新しいGC命令が追加されました。これは、GCがチャネルオブジェクトをスキャンする際に使用する命令です。
    • chanProg という新しいGCプログラムが定義されました。これは GC_CHAN 命令を使用します。
    • scanblock 関数(GCがメモリブロックをスキャンする主要な関数)内で、TypeInfo_Chan 型のオブジェクトが検出された場合に、chanProg を使用してチャネルをスキャンするロジックが追加されました。
    • GC_CHAN 命令の処理ロジックが追加されました。このロジックは以下のことを行います。
      • Hchan 構造体自体にはヒープポインタがないため、その先頭の sizeof(Hchan) バイトは無視されます。
      • チャネルの要素がポインタを含まない型(KindNoPointers)でない場合、チャネルのバッファ(Hchan の直後にメモリに配置される)をスキャンします。
      • バッファのサイズ(cap(c))はチャネル構造体の2番目の uintgo フィールドから取得されます。
      • バッファが空でない場合、バッファの各要素をGCの対象としてキューに追加します。この際、チャネルの要素の型情報(chantype->elem->gc)と、PRECISE | LOOP フラグ(正確なスキャンとループ処理を示す)が使用されます。
      • TODO(atom): split into two chunks so that only the in-use part of the circular buffer is scanned. というコメントがあり、これはバッファが循環バッファとして機能する場合に、使用中の部分のみをスキャンすることで効率を改善できる可能性を示唆しています。ただし、チャネルルーチンが未使用部分をゼロクリアするため、メモリリークには繋がらないとされています。
  4. runtime.h の更新:

    • src/pkg/runtime/runtime.hextern uint32 runtime·Hchansize; が追加され、runtime·Hchansize が外部から参照可能になりました。

これらの変更により、GoのGCはチャネルオブジェクトをヒープ上で正確に識別し、その内部バッファに格納されているポインタを追跡できるようになります。これにより、チャネルが不要になった際に、そのチャネルが保持していたデータも適切にGCの対象となり、メモリが解放されるようになります。

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

src/pkg/runtime/chan.c

@@ -33,6 +33,8 @@ struct	WaitQ
 	SudoG*	last;
 };
 
+// The garbage collector is assuming that Hchan can only contain pointers into the stack
+// and cannot contain pointers into the heap.
 struct	Hchan
 {
 	uintgo	qcount;			// total data in the q
@@ -48,6 +50,8 @@ struct	Hchan
 	Lock;
 };
 
+uint32 runtime·Hchansize = sizeof(Hchan);
+
 // Buffer follows Hchan immediately in memory.
 // chanbuf(c, i) is pointer to the i'th slot in the buffer.
 #define chanbuf(c, i) ((byte*)((c)+1)+(uintptr)(c)->elemsize*(i))
@@ -112,6 +116,7 @@ runtime·makechan_c(ChanType *t, int64 hint)
 	c->elemalg = elem->alg;
 	c->elemalign = elem->align;
 	c->dataqsiz = hint;
+	runtime·settype(c, (uintptr)t | TypeInfo_Chan);
 
 	if(debug)
 		runtime·printf("makechan: chan=%p; elemsize=%D; elemalg=%p; elemalign=%d; dataqsiz=%D\n",

src/pkg/runtime/malloc.h

@@ -482,6 +482,7 @@ enum
 	TypeInfo_SingleObject = 0,
 	TypeInfo_Array = 1,
 	TypeInfo_Map = 2,
+	TypeInfo_Chan = 3,
 
 	// Enables type information at the end of blocks allocated from heap	
 	DebugTypeAtBlockEnd = 0,

src/pkg/runtime/mgc0.c

@@ -164,6 +164,7 @@ static struct {
 enum {
 	GC_DEFAULT_PTR = GC_NUM_INSTR,
 	GC_MAP_NEXT,
+	GC_CHAN,
 };
 
 // markonly marks an object. It returns true if the object
@@ -521,6 +522,9 @@ static uintptr defaultProg[2] = {PtrSize, GC_DEFAULT_PTR};\n // Hashmap iterator program
 static uintptr mapProg[2] = {0, GC_MAP_NEXT};\n 
+// Hchan program
+static uintptr chanProg[2] = {0, GC_CHAN};\n+\n // Local variables of a program fragment or loop
 typedef struct Frame Frame;\n struct Frame {\n@@ -560,6 +564,8 @@ scanblock(Workbuf *wbuf, Obj *wp, uintptr nobj, bool keepworking)\n 	bool didmark, mapkey_kind, mapval_kind;\n 	struct hash_gciter map_iter;\n 	struct hash_gciter_data d;\n+\tHchan *chan;\n+\tChanType *chantype;\n \n \tif(sizeof(Workbuf) % PageSize != 0)\n \t\truntime·throw("scanblock: size of Workbuf is suboptimal");\n@@ -601,6 +607,8 @@ scanblock(Workbuf *wbuf, Obj *wp, uintptr nobj, bool keepworking)\n 	mapkey_size = mapval_size = 0;\n 	mapkey_kind = mapval_kind = false;\n 	mapkey_ti = mapval_ti = 0;\n+\tchan = nil;\n+\tchantype = nil;\n \n \tgoto next_block;\n \n@@ -660,6 +668,11 @@ scanblock(Workbuf *wbuf, Obj *wp, uintptr nobj, bool keepworking)\n 					goto next_block;\n 					}\n 					break;\n+\t\t\t\tcase TypeInfo_Chan:\n+\t\t\t\t\tchan = (Hchan*)b;\n+\t\t\t\t\tchantype = (ChanType*)t;\n+\t\t\t\t\tpc = chanProg;\n+\t\t\t\t\tbreak;\n \t\t\t\tdefault:\n \t\t\t\t\truntime·throw("scanblock: invalid type");\n \t\t\t\t\treturn;\n@@ -897,6 +910,26 @@ scanblock(Workbuf *wbuf, Obj *wp, uintptr nobj, bool keepworking)\n 			pc += 4;\n 			break;\n \n+\t\tcase GC_CHAN:\n+\t\t\t// There are no heap pointers in struct Hchan,\n+\t\t\t// so we can ignore the leading sizeof(Hchan) bytes.\n+\t\t\tif(!(chantype->elem->kind & KindNoPointers)) {\n+\t\t\t\t// Channel's buffer follows Hchan immediately in memory.\n+\t\t\t\t// Size of buffer (cap(c)) is second int in the chan struct.\n+\t\t\t\tn = ((uintgo*)chan)[1];\n+\t\t\t\tif(n > 0) {\n+\t\t\t\t\t// TODO(atom): split into two chunks so that only the\n+\t\t\t\t\t// in-use part of the circular buffer is scanned.\n+\t\t\t\t\t// (Channel routines zero the unused part, so the current\n+\t\t\t\t\t// code does not lead to leaks, it's just a little inefficient.)\n+\t\t\t\t\t*objbufpos++ = (Obj){(byte*)chan+runtime·Hchansize, n*chantype->elem->size,\n+\t\t\t\t\t\t(uintptr)chantype->elem->gc | PRECISE | LOOP};\n+\t\t\t\t\tif(objbufpos == objbuf_end)\n+\t\t\t\t\t\tflushobjbuf(objbuf, &objbufpos, &wp, &wbuf, &nobj);\n+\t\t\t\t}\n+\t\t\t}\n+\t\t\tgoto next_block;\n+\n \t\tdefault:\n \t\t\truntime·throw("scanblock: invalid GC instruction");\n \t\t\treturn;\n```

### `src/pkg/runtime/runtime.h`

```c
@@ -609,6 +609,7 @@ extern	int32	runtime·ncpu;
 extern	bool	runtime·iscgo;
 extern 	void	(*runtime·sysargs)(int32, uint8**);
 extern	uint32	runtime·maxstring;
+extern	uint32	runtime·Hchansize;
 
 /*
  * common functions and data

コアとなるコードの解説

src/pkg/runtime/chan.c の変更

  • Hchan 構造体へのコメント追加:

    // The garbage collector is assuming that Hchan can only contain pointers into the stack
    // and cannot contain pointers into the heap.
    

    このコメントは、Hchan 構造体自体がヒープへのポインタを持たないというGCの仮定を明示しています。これは、Hchan の直後に続くチャネルバッファがヒープ上のオブジェクトへのポインタを持つ可能性があるため、GCが Hchan 自体とバッファを区別して処理する必要があることを示唆しています。

  • runtime·Hchansize の追加:

    uint32 runtime·Hchansize = sizeof(Hchan);
    

    Hchan 構造体のサイズを runtime·Hchansize というグローバル変数に格納しています。これはGCがチャネルオブジェクトのメモリレイアウトを正確に理解し、バッファ部分を特定するために使用されます。

  • runtime·settype(c, (uintptr)t | TypeInfo_Chan); の追加: runtime·makechan_c 関数内で、新しく作成されたチャネルオブジェクト cTypeInfo_Chan という型情報を設定しています。これにより、GCはヒープ上のこのメモリ領域がチャネルオブジェクトであることを認識し、チャネル専用のGCロジックを適用できるようになります。TypeInfo_Chan は、GCがオブジェクトの種類を識別するためのタグのようなものです。

src/pkg/runtime/malloc.h の変更

  • TypeInfo_Chan = 3, の追加:
    enum
    	TypeInfo_SingleObject = 0,
    	TypeInfo_Array = 1,
    	TypeInfo_Map = 2,
    	TypeInfo_Chan = 3,
    
    TypeInfo_Chan という新しい列挙値が追加されました。これは、GCがヒープ上のオブジェクトの型を識別するために使用される TypeInfo 列挙型の一部です。これにより、GCはチャネルオブジェクトを他の一般的なオブジェクト(単一オブジェクト、配列、マップ)とは異なる特別な方法で処理する必要があることを認識します。

src/pkg/runtime/mgc0.c の変更

  • GC_CHAN 命令の追加と chanProg の定義:

    enum {
    	GC_DEFAULT_PTR = GC_NUM_INSTR,
    	GC_MAP_NEXT,
    	GC_CHAN,
    };
    // ...
    static uintptr chanProg[2] = {0, GC_CHAN};
    

    GC_CHAN という新しいGC命令が導入されました。これは、GCがチャネルオブジェクトをスキャンする際に実行すべき特定の処理を示すものです。chanProg は、この GC_CHAN 命令を含むGCプログラム(GCがオブジェクトをスキャンする際の命令シーケンス)として定義されています。

  • scanblock 関数におけるチャネルの型認識と処理:

    				case TypeInfo_Chan:
    					chan = (Hchan*)b;
    					chantype = (ChanType*)t;
    					pc = chanProg;
    					break;
    

    scanblock 関数は、GCがメモリブロックを走査し、到達可能なオブジェクトをマークする主要なループです。ここで、オブジェクトの型情報が TypeInfo_Chan であると識別された場合、そのオブジェクトを Hchan 型としてキャストし、対応するチャネル型情報 ChanType を取得します。そして、GCプログラムカウンタ pcchanProg に設定し、チャネル専用のGCロジックを実行するように指示します。

  • GC_CHAN 命令の具体的な処理ロジック:

    		case GC_CHAN:
    			// There are no heap pointers in struct Hchan,
    			// so we can ignore the leading sizeof(Hchan) bytes.
    			if(!(chantype->elem->kind & KindNoPointers)) {
    				// Channel's buffer follows Hchan immediately in memory.
    				// Size of buffer (cap(c)) is second int in the chan struct.
    				n = ((uintgo*)chan)[1];
    				if(n > 0) {
    					// TODO(atom): split into two chunks so that only the
    					// in-use part of the circular buffer is scanned.
    					// (Channel routines zero the unused part, so the current
    					// code does not lead to leaks, it's just a little inefficient.)
    					*objbufpos++ = (Obj){(byte*)chan+runtime·Hchansize, n*chantype->elem->size,
    						(uintptr)chantype->elem->gc | PRECISE | LOOP};
    					if(objbufpos == objbuf_end)
    						flushobjbuf(objbuf, &objbufpos, &wp, &wbuf, &nobj);
    				}
    			}
    			goto next_block;
    

    これがこのコミットの最も重要な部分です。

    1. Hchan 構造体自体の無視: Hchan 構造体自体にはヒープポインタが含まれていないため、そのサイズ分(runtime·Hchansize)はスキャンをスキップします。これは効率化のためです。
    2. 要素がポインタを含むかどうかのチェック: !(chantype->elem->kind & KindNoPointers) は、チャネルの要素型がポインタを含まない型(例: int, bool)でないことを確認します。ポインタを含まない型であれば、GCはバッファをスキャンする必要がありません。
    3. バッファのサイズと位置の特定: チャネルのバッファは Hchan 構造体の直後にメモリに配置されます。バッファのサイズ(チャネルの容量 cap(c))は、Hchan 構造体の2番目の uintgo フィールドから取得されます。
    4. バッファのスキャン対象への追加: バッファが空でなければ、そのバッファ領域をGCのスキャン対象として objbuf に追加します。
      • (byte*)chan+runtime·Hchansize: これはチャネルバッファの開始アドレスを計算しています。Hchan 構造体の直後からバッファが始まるため、Hchan のアドレスにそのサイズを加算します。
      • n*chantype->elem->size: これはバッファ全体のバイトサイズを計算しています(容量 n × 各要素のサイズ)。
      • (uintptr)chantype->elem->gc | PRECISE | LOOP: これは、バッファ内の要素をスキャンするためのGCプログラム(要素の型に応じたGC情報)と、PRECISE(正確なスキャン)および LOOP(バッファ内のすべての要素をループしてスキャン)フラグを組み合わせています。
    5. バッファのフラッシュ: objbuf が満杯になった場合、flushobjbuf を呼び出して、キューに追加されたオブジェクトを実際にGCが処理できるようにします。

src/pkg/runtime/runtime.h の変更

  • extern uint32 runtime·Hchansize; の追加: runtime·Hchansize が他のファイルから参照できるように、外部変数として宣言されています。

これらの変更により、Goのランタイムはチャネルオブジェクトのメモリレイアウトを正確に理解し、その内部バッファに格納されているポインタをGCが追跡できるようになりました。これにより、チャネルが不要になった際に、そのチャネルが保持していたデータも適切にGCの対象となり、メモリが解放されるようになり、メモリリークの問題が解決されます。

関連リンク

参考にした情報源リンク