[インデックス 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ランタイムのガベージコレクタがチャネルをより正確に処理できるようにするための複数の変更を含んでいます。
-
Hchan
構造体へのGCの認識の追加:src/pkg/runtime/chan.c
のHchan
構造体の定義に、GCがスタック内のポインタのみを想定しており、ヒープ内のポインタを含まないというコメントが追加されています。これは、Hchan
自体はポインタを含まないが、そのバッファはポインタを含む可能性があるという前提を示唆しています。runtime·Hchansize
というグローバル変数が追加され、Hchan
構造体のサイズをGCが利用できるようにしています。runtime·makechan_c
関数(チャネル作成の内部関数)で、新しく作成されたチャネルオブジェクトにTypeInfo_Chan
型情報を設定するruntime·settype
が呼び出されるようになりました。これにより、GCはオブジェクトがチャネルであることを認識できます。
-
TypeInfo_Chan
の導入:src/pkg/runtime/malloc.h
にTypeInfo_Chan = 3
という新しい列挙型が追加されました。これは、ヒープに割り当てられたオブジェクトの型情報をGCが識別するために使用されます。これにより、GCはチャネルオブジェクトを特別な方法で処理する必要があることを認識します。
-
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.
というコメントがあり、これはバッファが循環バッファとして機能する場合に、使用中の部分のみをスキャンすることで効率を改善できる可能性を示唆しています。ただし、チャネルルーチンが未使用部分をゼロクリアするため、メモリリークには繋がらないとされています。
-
runtime.h
の更新:src/pkg/runtime/runtime.h
にextern 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
関数内で、新しく作成されたチャネルオブジェクトc
にTypeInfo_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プログラムカウンタpc
をchanProg
に設定し、チャネル専用の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;
これがこのコミットの最も重要な部分です。
Hchan
構造体自体の無視:Hchan
構造体自体にはヒープポインタが含まれていないため、そのサイズ分(runtime·Hchansize
)はスキャンをスキップします。これは効率化のためです。- 要素がポインタを含むかどうかのチェック:
!(chantype->elem->kind & KindNoPointers)
は、チャネルの要素型がポインタを含まない型(例:int
,bool
)でないことを確認します。ポインタを含まない型であれば、GCはバッファをスキャンする必要がありません。 - バッファのサイズと位置の特定: チャネルのバッファは
Hchan
構造体の直後にメモリに配置されます。バッファのサイズ(チャネルの容量cap(c)
)は、Hchan
構造体の2番目のuintgo
フィールドから取得されます。 - バッファのスキャン対象への追加: バッファが空でなければ、そのバッファ領域をGCのスキャン対象として
objbuf
に追加します。(byte*)chan+runtime·Hchansize
: これはチャネルバッファの開始アドレスを計算しています。Hchan
構造体の直後からバッファが始まるため、Hchan
のアドレスにそのサイズを加算します。n*chantype->elem->size
: これはバッファ全体のバイトサイズを計算しています(容量n
× 各要素のサイズ)。(uintptr)chantype->elem->gc | PRECISE | LOOP
: これは、バッファ内の要素をスキャンするためのGCプログラム(要素の型に応じたGC情報)と、PRECISE
(正確なスキャン)およびLOOP
(バッファ内のすべての要素をループしてスキャン)フラグを組み合わせています。
- バッファのフラッシュ:
objbuf
が満杯になった場合、flushobjbuf
を呼び出して、キューに追加されたオブジェクトを実際にGCが処理できるようにします。
src/pkg/runtime/runtime.h
の変更
extern uint32 runtime·Hchansize;
の追加:runtime·Hchansize
が他のファイルから参照できるように、外部変数として宣言されています。
これらの変更により、Goのランタイムはチャネルオブジェクトのメモリレイアウトを正確に理解し、その内部バッファに格納されているポインタをGCが追跡できるようになりました。これにより、チャネルが不要になった際に、そのチャネルが保持していたデータも適切にGCの対象となり、メモリが解放されるようになり、メモリリークの問題が解決されます。
関連リンク
- Stack Overflow Issue: Memory consumption skyrocket
- Go Code Review Link: https://golang.org/cl/7307086
参考にした情報源リンク
- Go's garbage collector is a precise, concurrent, tri-color, mark-and-sweep collector that efficiently manages memory, including that used by channels (internally represented as
Hchan
). - Go's garbage collector is "precise" because it accurately distinguishes between pointers and non-pointers in memory.
- Channels in Go are heap-allocated data structures. The internal representation of a channel is
Hchan
. - The Stack Overflow question "Memory consumption skyrocket" discusses a memory leak issue, specifically in Go, related to
time.Ticker
. - One of the answers suggests that a fix for this issue was implemented in a Go tip version newer than February 25, 2013 (revision 1c50db40d078).