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

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

このコミットは、Go言語のガベージコレクタ(GC)がチャネル型を適切にサポートするための変更を導入しています。具体的には、コンパイラがチャネル型に対してGC情報を生成する方法と、ランタイムのGCがチャネルオブジェクトをスキャンしてマークする方法が更新されています。これにより、チャネルが参照するメモリが正確に追跡され、不要になった際に適切に解放されるようになります。

コミット

commit 6e69df6102d167344a74d720b5ef080cdf04a8d7
Author: Jan Ziak <0xe2.0x9a.0x9b@gmail.com>
Date:   Tue Mar 19 19:51:03 2013 +0100

    cmd/gc: support channel types in the garbage collector
    
    R=golang-dev, dvyukov, rsc
    CC=golang-dev
    https://golang.org/cl/7473044

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

https://github.com/golang/go/commit/6e69df6102d167344a74d720b5ef080cdf04a8d7

元コミット内容

cmd/gc: support channel types in the garbage collector

R=golang-dev, dvyukov, rsc
CC=golang-dev
https://golang.org/cl/7473044

変更の背景

Go言語のガベージコレクタは、プログラムが使用しなくなったメモリを自動的に解放する役割を担っています。しかし、このコミット以前は、Goのチャネル型(chan)がガベージコレクタによって完全に適切に扱われていませんでした。特に、チャネル自体がヒープ上に割り当てられるオブジェクトであり、チャネルが内部的に保持する要素(バッファ内のデータや送受信待ちのゴルーチンへのポインタなど)もGCの対象となるべきです。

このコミットの背景には、チャネルが参照するメモリがGCによって正確に追跡されず、結果としてメモリリークが発生したり、GCが不要なチャネルオブジェクトを解放できなかったりする可能性があったという問題があります。Goの並行処理の根幹をなすチャネルが正しくGCされないことは、長期稼働するアプリケーションの安定性やメモリ効率に大きな影響を与えます。

この変更は、Goのガベージコレクタがチャネルの内部構造を理解し、チャネルが参照するすべてのポインタを適切にスキャンしてマークできるようにすることで、この問題を解決することを目的としています。これにより、チャネルに関連するメモリが不要になった際に確実に解放され、Goプログラムのメモリ管理がより堅牢になります。

前提知識の解説

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

Go言語は、自動メモリ管理のためにマーク&スイープ方式のガベージコレクタを採用しています。基本的な流れは以下の通りです。

  1. マークフェーズ (Mark Phase): GCは、プログラムが現在使用している(到達可能な)すべてのオブジェクトを特定し、マークします。これは、ルートセット(グローバル変数、スタック上の変数、レジスタなど)から開始し、それらが参照するオブジェクト、さらにそのオブジェクトが参照するオブジェクト…と辿っていくことで行われます。
  2. スイープフェーズ (Sweep Phase): マークされなかったオブジェクト(到達不可能なオブジェクト、つまり不要になったメモリ)は、解放されて再利用可能なメモリとしてマークされます。

GoのGCは、並行性(concurrent)と低遅延(low-latency)を重視して設計されており、プログラムの実行と並行してGC処理の一部を進めることができます。GCがオブジェクトをスキャンする際には、そのオブジェクトがどのような型であり、どのオフセットにポインタが含まれているかという情報(GCプログラムまたはGCビットマップ)が必要です。これにより、GCはポインタを辿って到達可能なオブジェクトを正確にマークできます。

Go言語のチャネル (Channels)

Goのチャネルは、ゴルーチン間で値を安全に送受信するための通信メカニズムです。チャネルはmake(chan Type, capacity)で作成され、ヒープ上にHchanという構造体として割り当てられます。Hchan構造体には、バッファ、送受信待ちのゴルーチンキュー、ロックなどの情報が含まれています。

チャネルは、その要素型(Type)がポインタを含む場合、そのポインタが指すオブジェクトもGCの対象となります。例えば、chan *MyStructのようなチャネルは、MyStructへのポインタを保持するため、GCはチャネルのバッファ内にあるこれらのポインタをスキャンし、参照先のMyStructオブジェクトをマークする必要があります。

src/cmd/gc/reflect.csrc/pkg/runtime/mgc0.c の役割

  • src/cmd/gc/reflect.c: これはGoコンパイラの一部であり、型のリフレクション情報、特にガベージコレクタが型をスキャンするために必要なGCプログラム(またはGCビットマップ)を生成する役割を担っています。コンパイラは、各型について、どの部分がポインタであるかをGCに伝えるためのメタデータを生成します。
  • src/pkg/runtime/mgc0.c: これはGoランタイムのガベージコレクタのコア部分です。scanblock関数などが含まれており、実際にヒープ上のオブジェクトをスキャンし、ポインタを辿って到達可能なオブジェクトをマークする処理を行います。GCプログラムやGCビットマップを解釈し、それに基づいてメモリを走査します。

このコミットは、これら2つのコンポーネントが連携してチャネル型を正しく処理できるようにするための変更です。

技術的詳細

このコミットの技術的詳細は、主にGoコンパイラがチャネル型に対するGC情報を生成する方法と、GoランタイムのGCがその情報を使用してチャネルをスキャンする方法の変更に集約されます。

src/cmd/gc/reflect.c の変更

以前のreflect.cでは、TCHAN(チャネル型)はTUNSAFEPTR(unsafe.Pointer)やTFUNC(関数型)と同じカテゴリで扱われていました。これは、これらの型が単なるポインタとして扱われ、その指す先の詳細なGCスキャンは行われないことを意味していました。

変更後、TCHANdgcsym1関数内で独立したcase TCHAN:ブロックを持つようになりました。この新しいブロックでは、チャネル型に対してGC_CHAN_PTRという新しいGC命令が生成されます。

// struct Hchan*
case TCHAN:
    if(*off % widthptr != 0)
        fatal("dgcsym1: invalid alignment, %T", t);
    ot = duintptr(s, ot, GC_CHAN_PTR); // GC_CHAN_PTR 命令を生成
    ot = duintptr(s, ot, *off);        // オフセットを記録
    ot = dsymptr(s, ot, dtypesym(t), 0); // チャネルの型情報を記録
    *off += t->width;
    break;

この変更により、コンパイラはチャネル型を単なるポインタとしてではなく、GCが特別に処理すべき構造体へのポインタとして認識し、そのための具体的なGC命令をランタイムに提供するようになります。GC_CHAN_PTR命令は、GCがチャネルオブジェクト自体をスキャンする必要があることを示します。

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

src/pkg/runtime/mgc0.hには、GCプログラムで使用される命令の列挙型が定義されています。このコミットでは、新たにGC_CHAN_PTRという命令が追加されました。

enum {
    // ...
    GC_MAP_PTR,     // Go map. Args: (off, MapType*)
    GC_CHAN_PTR,    // Go channel. Args: (off, ChanType*) // 新しく追加
    GC_STRING,      // Go string. Args: (off)
    // ...
};

この定義により、ランタイムのGCはGC_CHAN_PTR命令を認識し、それに対応する処理を実行できるようになります。

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

src/pkg/runtime/mgc0.cは、GCの主要なスキャンロジックが実装されているファイルです。このファイルでは、主にscanblock関数が変更されています。

  1. 変数の追加と初期化: chan_retという新しいuintptr*型の変数が追加され、nilで初期化されるようになりました。これは、チャネルのスキャン処理から戻るためのポインタとして使用されます。
  2. map_retnil: map_ret = 0という記述がmap_ret = nilに変更されました。これは機能的な変更ではなく、Goの慣用的なスタイルに合わせたものです。
  3. TypeInfo_Chanの処理: scanblock関数内でTypeInfo_Chan(チャネルの型情報)が検出された場合、chan_retnilに設定され、chanProgというGCプログラムが実行されるようになりました。
  4. GC_CHAN_PTR命令のハンドリング: scanblockのメインループに、新しいGC_CHAN_PTRケースが追加されました。
    • このケースは、スタック上のポインタからチャネルオブジェクト(Hchan*)を取得します。
    • チャネルがnilでない場合、markonly(chan)を呼び出してチャネルオブジェクト自体をマークします。
    • チャネルの要素型(chantype->elem)がポインタを含まない(KindNoPointersフラグが立っている)場合は、それ以上スキャンする必要がないため、次のGC命令に進みます。
    • 要素型がポインタを含む場合、chan_retに現在のGCプログラムカウンタ(pc+3)を保存し、chanProg(チャネルの内部構造をスキャンするためのGCプログラム)の先頭(chanProg+1)にジャンプします。これにより、チャネルのバッファや送受信キュー内のポインタがスキャンされます。
  5. GC_CHAN命令の修正: 既存のGC_CHANケースは、チャネルオブジェクト自体をスキャンする役割を担っていました。このコミットでは、chan_retnilでない場合に、chan_retに保存されたアドレスに戻るように修正されました。これにより、GC_CHAN_PTRからchanProgにジャンプした後、チャネルの内部スキャンが完了した際に、元のGCプログラムの実行を再開できるようになります。

これらの変更により、GoのGCはチャネルオブジェクトへのポインタを認識し、そのポインタが指すチャネルオブジェクト自体をマークし、さらにチャネルの内部構造(バッファ内の要素など)を再帰的にスキャンして、チャネルが参照するすべての到達可能なオブジェクトを正確にマークできるようになりました。

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

このコミットのコアとなる変更は、以下の3つのファイルにまたがっています。

  1. src/cmd/gc/reflect.c:

    • dgcsym1関数内のcase TCHAN:ブロックが、既存のTUNSAFEPTR, TFUNCのグループから分離され、独立した処理を持つようになりました。
    • この新しいTCHANブロック内で、GC_CHAN_PTRという新しいGC命令が生成されるようになりました。
    --- a/src/cmd/gc/reflect.c
    +++ b/src/cmd/gc/reflect.c
    @@ -1082,7 +1082,6 @@ dgcsym1(Sym *s, int ot, Type *t, vlong *off, int stack_size)
     		*off += t->width;
     		break;
    
    -	case TCHAN:
     	case TUNSAFEPTR:
     	case TFUNC:
     		if(*off % widthptr != 0)
    @@ -1092,6 +1091,16 @@ dgcsym1(Sym *s, int ot, Type *t, vlong *off, int stack_size)
     		*off += t->width;
     		break;
    
    +	// struct Hchan*
    +	case TCHAN:
    +		if(*off % widthptr != 0)
    +			fatal("dgcsym1: invalid alignment, %T", t);
    +		ot = duintptr(s, ot, GC_CHAN_PTR);
    +		ot = duintptr(s, ot, *off);
    +		ot = dsymptr(s, ot, dtypesym(t), 0);
    +		*off += t->width;
    +		break;
    +
     	// struct Hmap*
     	case TMAP:
     		if(*off % widthptr != 0)
    
  2. src/pkg/runtime/mgc0.h:

    • GC命令の列挙型にGC_CHAN_PTRが追加されました。
    --- a/src/pkg/runtime/mgc0.h
    +++ b/src/pkg/runtime/mgc0.h
    @@ -24,6 +24,7 @@ enum {
     	GC_ARRAY_NEXT,  // The next element of an array. Args: none
     	GC_CALL,        // Call a subroutine. Args: (off, objgcrel)
     	GC_MAP_PTR,     // Go map. Args: (off, MapType*)
    +	GC_CHAN_PTR,    // Go channel. Args: (off, ChanType*)
     	GC_STRING,      // Go string. Args: (off)
     	GC_EFACE,       // interface{}. Args: (off)
     	GC_IFACE,       // interface{...}. Args: (off)
    
  3. src/pkg/runtime/mgc0.c:

    • scanblock関数内でchan_ret変数が追加され、初期化されました。
    • GC_CHAN_PTRという新しいcasescanblockのGC命令処理ループに追加され、チャネルポインタのマークと、チャネル内部のスキャン(chanProgへのジャンプ)ロジックが実装されました。
    • GC_CHANケースが修正され、chan_retを使用してchanProgからの復帰を処理するようになりました。
    --- a/src/pkg/runtime/mgc0.c
    +++ b/src/pkg/runtime/mgc0.c
    @@ -566,7 +566,7 @@ scanblock(Workbuf *wbuf, Obj *wp, uintptr nobj, bool keepworking)
     	byte *b, *arena_start, *arena_used;
     	uintptr n, i, end_b, elemsize, size, ti, objti, count, type;
     	uintptr *pc, precise_type, nominal_size;
    -	uintptr *map_ret, mapkey_size, mapval_size, mapkey_ti, mapval_ti;
    +	uintptr *map_ret, mapkey_size, mapval_size, mapkey_ti, mapval_ti, *chan_ret;
     	void *obj;
     	Type *t;
     	Slice *sliceptr;
    @@ -627,6 +627,7 @@ scanblock(Workbuf *wbuf, Obj *wp, uintptr nobj, bool keepworking)
     	mapkey_ti = mapval_ti = 0;
     	chan = nil;
     	chantype = nil;
    +	chan_ret = nil; // chan_ret の初期化
     
     	goto next_block;
     
    @@ -692,7 +693,7 @@ scanblock(Workbuf *wbuf, Obj *wp, uintptr nobj, bool keepworking)
     					mapval_kind = maptype->elem->kind;
     					mapval_ti   = (uintptr)maptype->elem->gc | PRECISE;
     
    -					map_ret = 0;
    +					map_ret = nil; // 0 から nil へ変更
     					pc = mapProg;
     				} else {
     					goto next_block;
    @@ -701,6 +702,7 @@ scanblock(Workbuf *wbuf, Obj *wp, uintptr nobj, bool keepworking)
     				case TypeInfo_Chan:
     					chan = (Hchan*)b;
     					chantype = (ChanType*)t;
    +					chan_ret = nil; // chan_ret の初期化
     					pc = chanProg;
     					break;
     				default:
    @@ -941,7 +943,7 @@ scanblock(Workbuf *wbuf, Obj *wp, uintptr nobj, bool keepworking)
     					}
     				}
     			}
    -			if(map_ret == 0)
    +			if(map_ret == nil) // 0 から nil へ変更
     				goto next_block;
     			pc = map_ret;
     			continue;
    @@ -957,6 +959,25 @@ scanblock(Workbuf *wbuf, Obj *wp, uintptr nobj, bool keepworking)
     			flushobjbuf(objbuf, &objbufpos, &wp, &wbuf, &nobj);
     		continue;
     
    +	// GC_CHAN_PTR の新しいケース
    +	case GC_CHAN_PTR:
    +		// Similar to GC_MAP_PTR
    +		chan = *(Hchan**)(stack_top.b + pc[1]);
    +		if(chan == nil) {
    +			pc += 3;
    +			continue;
    +		}
    +		if(markonly(chan)) { // チャネルオブジェクト自体をマーク
    +			chantype = (ChanType*)pc[2];
    +			if(!(chantype->elem->kind & KindNoPointers)) {
    +				// Start chanProg.
    +				chan_ret = pc+3; // 戻りアドレスを保存
    +				pc = chanProg+1; // chanProg へジャンプ
    +				continue;
    +			}
    +		}
    +		pc += 3;
    +		continue;
    +
     	case GC_CHAN:
     		// There are no heap pointers in struct Hchan,
     		// so we can ignore the leading sizeof(Hchan) bytes.
    @@ -975,7 +996,10 @@ scanblock(Workbuf *wbuf, Obj *wp, uintptr nobj, bool keepworking)
     					flushobjbuf(objbuf, &objbufpos, &wp, &wbuf, &nobj);
     				}
     			}
    -			goto next_block;
    +			if(chan_ret == nil) // chan_ret が nil でない場合のみ戻る
    +				goto next_block;
    +			pc = chan_ret; // chanProg からの復帰
    +			continue;
     
     	default:
     		runtime·throw("scanblock: invalid GC instruction");
    

コアとなるコードの解説

このコミットの核心は、Goのガベージコレクタがチャネル型を「ポインタを含む可能性のある複合型」として認識し、その内部構造を再帰的にスキャンする能力を獲得した点にあります。

  1. コンパイラによるGC命令の生成 (reflect.c):

    • 以前は、チャネルは単なるポインタとして扱われ、その指す先の詳細なGCスキャンは行われませんでした。
    • 新しいcase TCHAN:ブロックでは、コンパイラはチャネル型に対してGC_CHAN_PTRという特別なGC命令を生成します。この命令は、ランタイムのGCに対して「ここにチャネルへのポインタがある。このチャネルオブジェクトをスキャンする必要がある」と伝えます。
    • この命令には、チャネルオブジェクトのオフセットと、そのチャネルの型情報(ChanType*)が引数として含まれます。これにより、GCはチャネルの具体的な型情報に基づいて、その内部構造を適切に解釈できます。
  2. ランタイムGCによるチャネルポインタの処理 (mgc0.cGC_CHAN_PTR ケース):

    • scanblock関数は、GCプログラムを順に実行しながらメモリをスキャンします。
    • GC_CHAN_PTR命令に遭遇すると、GCはまずそのポインタが指すチャネルオブジェクト自体をmarkonly(chan)でマークします。これにより、チャネルオブジェクトが到達可能であることがGCに認識されます。
    • 次に重要なのは、チャネルの要素型(chantype->elem)がポインタを含むかどうかをチェックする部分です。
      • もし要素型がポインタを含まない(例: chan int)場合、チャネルのバッファや送受信キューにはGCの対象となるポインタが含まれないため、それ以上のスキャンは不要です。GCは次の命令に進みます。
      • もし要素型がポインタを含む(例: chan *MyStruct)場合、チャネルのバッファや送受信キューにはGCの対象となるポインタが含まれる可能性があります。この場合、GCは現在のGCプログラムの実行を一時停止し、chan_retに現在の位置(戻りアドレス)を保存します。そして、chanProgという別のGCプログラムにジャンプします。chanProgは、チャネルの内部構造(バッファ、送受信キューなど)をスキャンし、そこに存在するポインタをマークするための専用のGCプログラムです。
  3. chanProgからの復帰 (mgc0.cGC_CHAN ケースの修正):

    • chanProgによるチャネル内部のスキャンが完了すると、GCはGC_CHAN命令の処理に戻ります。
    • 修正されたGC_CHANケースでは、chan_retnilでない場合(つまり、GC_CHAN_PTRからchanProgにジャンプしてきた場合)、chan_retに保存されたアドレスにジャンプして、元のGCプログラムの実行を再開します。これにより、チャネルの内部スキャンが完了した後も、GCは残りのオブジェクトのスキャンを継続できます。

この一連のメカニズムにより、GoのGCはチャネルオブジェクト自体だけでなく、チャネルが内部的に保持するすべてのポインタ(バッファ内の要素、送受信待ちのゴルーチンへのポインタなど)を正確に追跡し、マークできるようになりました。これにより、チャネルに関連するメモリが不要になった際に確実に解放され、メモリリークのリスクが低減されます。

関連リンク

  • Go言語の公式ドキュメント: Go言語のガベージコレクションやチャネルに関する詳細な情報は、Goの公式ドキュメントやブログ記事で確認できます。
  • Goのソースコード:
    • src/cmd/gc/reflect.c
    • src/pkg/runtime/mgc0.c
    • src/pkg/runtime/mgc0.h

参考にした情報源リンク