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

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

このコミットは、Go言語のランタイムにおけるガベージコレクタ(GC)の挙動を改善するものです。具体的には、GCがメモリブロックをスキャンする際に、そのブロックに型情報(typeinfo)が明示的に付与されていない場合でも、実際の型を動的に特定しようとするロジックが追加されています。これにより、GCの精度と堅牢性が向上し、より正確なメモリ管理が可能になります。

コミット

commit 059fed3dfb6f480598c72ab18fe28b322151ba38
Author: Jan Ziak <0xe2.0x9a.0x9b@gmail.com>
Date:   Fri Jan 18 16:56:17 2013 -0500

    runtime: try to determine the actual type during garbage collection
    
    If the scanned block has no typeinfo the garbage collector will attempt
    to get the actual type of the block.
    
    R=golang-dev, bradfitz, rsc
    CC=golang-dev
    https://golang.org/cl/7093045

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

https://github.com/golang/go/commit/059fed3dfb6f480598c72ab18fe28b322151ba38

元コミット内容

ランタイム:ガベージコレクション中に実際の型を特定しようと試みる

スキャンされたブロックに型情報がない場合、ガベージコレクタはそのブロックの実際の型を取得しようと試みます。

変更の背景

Go言語のガベージコレクタは、メモリ上のオブジェクトがまだ使用されているかどうか(「ライブ」であるか)を判断し、不要になったメモリを解放する役割を担っています。このプロセスにおいて、GCはメモリブロック内にポインタが含まれているかどうか、そしてそのポインタがどこを指しているかを正確に知る必要があります。この情報が不正確だと、誤って使用中のメモリを解放してしまったり(use-after-free)、逆に不要なメモリを解放し損ねてメモリリークを引き起こしたりする可能性があります。

従来のGCでは、メモリブロックに明示的な型情報(typeinfo)が付与されていることを前提としていました。しかし、特定のシナリオや内部的なメモリ割り当てにおいて、このtypeinfoが欠落している場合がありました。このような状況では、GCはブロックの内容を正確に解釈できず、効率的かつ安全なメモリ回収が困難になるという課題がありました。

このコミットは、この課題に対処するために導入されました。typeinfoが欠落している場合でも、runtime·gettypeのような内部関数を用いてブロックの実際の型を動的に推論するメカニズムを追加することで、GCの堅牢性と正確性を向上させ、Goプログラムの安定性を高めることを目的としています。

前提知識の解説

Go言語のガベージコレクション (2013年時点)

2013年時点のGo言語のガベージコレクタは、主に「マーク&スイープ」方式を採用していました。この方式は以下の2つのフェーズで構成されます。

  1. マークフェーズ (Mark Phase): プログラムのルート(グローバル変数、スタック上の変数など)から到達可能なすべてのオブジェクトを「ライブ」としてマークします。ポインタをたどって、到達可能なオブジェクトを再帰的にマークしていきます。
  2. スイープフェーズ (Sweep Phase): マークされなかったすべてのオブジェクト(「デッド」なオブジェクト)が不要なメモリとして識別され、解放されます。

GCが正確に動作するためには、メモリブロック内のどの部分がポインタであり、どの部分が非ポインタデータであるかを正確に識別する必要があります。この情報が「型情報(typeinfo)」としてランタイムに保持されています。

型情報 (typeinfo)

Go言語は静的型付け言語ですが、ランタイム時にも型情報を保持しています。これは、リフレクション(reflectパッケージ)や、特にガベージコレクタがメモリを正確にスキャンするために不可欠です。typeinfoは、特定のデータ構造のメモリレイアウトに関するメタデータを含んでおり、例えば、構造体内のどのオフセットにポインタが存在するか、配列の要素サイズはいくつか、といった情報を提供します。GCはこれを利用して、メモリブロックをスキャンする際に、ポインタを正確に識別し、そのポインタが指す先のオブジェクトもマーク対象とすることができます。

UseSpanType

UseSpanTypeは、Goランタイムの内部的なメカニズムに関連する概念です。Goのメモリ管理では、メモリは「スパン(span)」と呼ばれる連続したブロック単位で割り当てられます。UseSpanTypeは、GCがこれらのスパンを効率的にスキャンし、ライブオブジェクトを識別するために、スパンに関連付けられた型情報を利用するプロセスを指していると考えられます。これにより、GCはメモリ内のポインタの配置を正確に把握し、到達可能なメモリをすべてマークすることができます。これは、GCがメモリを効率的に走査し、すべての到達可能なメモリを正しく識別するために不可欠な内部フラグまたは設定であったと推測されます。

runtime·gettype

runtime·gettype·はGoランタイムの内部関数を示す慣例的な表記)は、Goランタイム内部の非公開関数です。この関数は、特定のメモリアドレスにあるオブジェクトの型情報を取得する役割を担っていました。GCがメモリブロックをスキャンする際に、明示的なtypeinfoがない場合でも、このruntime·gettypeを呼び出すことで、そのメモリブロックが実際にどのような型のオブジェクトを格納しているかを動的に判断しようとします。これにより、GCはより柔軟に、そして正確にメモリをスキャンし、ポインタを識別することが可能になります。

技術的詳細

このコミットの核心は、scanblock関数におけるtypeinfoの処理ロジックの拡張にあります。scanblockは、GCが特定のメモリブロックをスキャンしてポインタを識別し、到達可能なオブジェクトをマークするための主要な関数です。

変更前は、ti(型情報インデックス)が0でない場合(つまり、明示的な型情報が存在する場合)にのみ、その型情報に基づいてスキャンが行われていました。しかし、tiが0の場合(型情報が欠落している場合)は、defaultProgという汎用的なプログラム(ポインタの配置に関する情報が少ない、または全くない)を使用してスキャンが行われていました。これは、型が不明なブロックに対しては、GCが保守的かつ非効率的なスキャンを行うことを意味していました。

このコミットでは、tiが0の場合に、UseSpanTypeが有効であれば、runtime·gettype(b)を呼び出して、メモリブロックbの実際の型情報を動的に取得しようとします。

runtime·gettypeが型情報を正常に取得した場合、その型情報に基づいて以下のいずれかのケースに分岐します。

  • TypeInfo_SingleObject: ブロックが単一のオブジェクトを表す場合。t->gc(ガベージコレクションプログラム)からポインタ情報を取得し、precise_typetrueに設定して、正確な型情報に基づいてスキャンを行います。
  • TypeInfo_Array: ブロックが配列を表す場合。配列の要素サイズ(pc[0])を取得し、stack_top.countを0(無限反復)に設定して、配列のすべての要素をスキャンします。ここでもprecise_typetrueです。
  • TypeInfo_Map: ブロックがマップを表す場合。この時点ではTODOコメントがあり、将来的な拡張が示唆されていますが、defaultProgが使用されます。
  • default: その他の無効な型情報の場合、ランタイムエラーを発生させます。

もしruntime·gettypeが型情報を取得できなかった場合(type == 0)、またはUseSpanTypeが有効でない場合は、引き続きdefaultProgが使用されます。

この変更により、GCは型情報が明示的に付与されていないメモリブロックに対しても、よりインテリジェントに、そして正確にポインタを識別できるようになり、メモリ管理の精度と効率が向上しました。

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

--- a/src/pkg/runtime/mgc0.c
+++ b/src/pkg/runtime/mgc0.c
@@ -410,7 +410,7 @@ static void
 scanblock(Workbuf *wbuf, Obj *wp, uintptr nobj, bool keepworking)
 {
 	byte *b, *arena_start, *arena_used;
-	uintptr n, i, end_b, elemsize, ti, objti, count;
+	uintptr n, i, end_b, elemsize, ti, objti, count, type;
 	uintptr *pc, precise_type, nominal_size;
 	void *obj;
 	Type *t;
@@ -463,7 +463,6 @@ scanblock(Workbuf *wbuf, Obj *wp, uintptr nobj, bool keepworking)
 		runtime·printf("scanblock %p %D\n", b, (int64)n);
 		}
 
-		// TODO(atom): to be expanded in a next CL
 		if(ti != 0) {
 			pc = (uintptr*)(ti & ~(uintptr)PC_BITS);
 			precise_type = (ti & PRECISE);
@@ -476,6 +475,37 @@ scanblock(Workbuf *wbuf, Obj *wp, uintptr nobj, bool keepworking)
 			} else {
 				stack_top.count = 1;
 			}
+		} else if(UseSpanType) {
+			type = runtime·gettype(b);
+			if(type != 0) {
+				t = (Type*)(type & ~(uintptr)(PtrSize-1));
+				switch(type & (PtrSize-1)) {
+				case TypeInfo_SingleObject:
+					pc = (uintptr*)t->gc;
+					precise_type = true;  // type information about 'b' is precise
+					stack_top.count = 1;
+					stack_top.elemsize = pc[0];
+					break;
+				case TypeInfo_Array:
+					pc = (uintptr*)t->gc;
+					if(pc[0] == 0)
+						goto next_block;
+					precise_type = true;  // type information about 'b' is precise
+					stack_top.count = 0;  // 0 means an infinite number of iterations
+					stack_top.elemsize = pc[0];
+					stack_top.loop_or_ret = pc+1;
+					break;
+				case TypeInfo_Map:
+					// TODO(atom): to be expanded in a next CL
+					pc = defaultProg;
+					break;
+				default:
+					runtime·throw("scanblock: invalid type");
+					return;
+				}
+			} else {
+				pc = defaultProg;
+			}
 		} else {
 			pc = defaultProg;
 		}

コアとなるコードの解説

変更は主にsrc/pkg/runtime/mgc0.cファイルのscanblock関数内で行われています。

  1. 変数の追加: uintptr n, i, end_b, elemsize, ti, objti, count; の行に , type; が追加され、type変数が導入されました。これはruntime·gettypeの戻り値を格納するために使用されます。

  2. 既存のTODOコメントの削除: // TODO(atom): to be expanded in a next CL というコメントが削除されました。これは、このコミットがそのTODOの拡張の一部であることを示唆しています。

  3. 新しい型推論ロジックの追加: if(ti != 0)elseブロックに、新しいelse if(UseSpanType)ブロックが追加されました。

    • このブロックは、ti(明示的な型情報)が0である(つまり、型情報が欠落している)場合に実行されます。
    • UseSpanTypetrueの場合、type = runtime·gettype(b); が呼び出され、メモリブロックbの実際の型情報を取得しようとします。
    • runtime·gettypeが型情報を正常に取得した場合(type != 0)、その型情報に基づいてswitch文で処理が分岐します。
      • TypeInfo_SingleObject: 単一オブジェクトの場合の処理。t->gcからGCプログラムを取得し、precise_typetrueに設定します。
      • TypeInfo_Array: 配列の場合の処理。配列の要素サイズを取得し、stack_top.countを0(無限反復)に設定して、配列のすべての要素をスキャンします。ここでもprecise_typetrueです。
      • TypeInfo_Map: マップの場合の処理。この時点ではTODOであり、defaultProgが使用されます。
      • default: 無効な型情報の場合、ランタイムエラーを発生させます。
    • runtime·gettypeが型情報を取得できなかった場合(type == 0)、またはUseSpanTypefalseの場合は、引き続きpc = defaultProg;が実行され、汎用的なスキャンが行われます。

この変更により、GCは型情報が不明なメモリブロックに対しても、より詳細な型推論を試み、その結果に基づいてより正確なスキャンを実行できるようになりました。

関連リンク

参考にした情報源リンク