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

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

このコミットは、Goランタイムのガベージコレクタ(GC)における文字列スキャン処理の軽微な最適化に関するものです。具体的には、GCがメモリ上のオブジェクトをスキャンし、到達可能なオブジェクトをマークするフェーズにおいて、文字列オブジェクトの処理方法を改善しています。これにより、不要な処理を削減し、GCの効率をわずかに向上させています。

コミット

commit 40f5e67571d6ce299140638e40bea6b00cc76330
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Wed Mar 26 15:03:58 2014 +0400

    runtime: minor improvement of string scanning
    If we set obj, then it will be enqueued for marking at the end of the scanning loop.
    This is not necessary, since we've already marked it.
    This can wait for 1.4 if you wish.
    
    LGTM=rsc
    R=rsc
    CC=golang-codereviews
    https://golang.org/cl/80030043

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

https://github.com/golang/go/commit/40f5e67571d6ce299140638e40bea6b00cc76330

元コミット内容

このコミットは、src/pkg/runtime/mgc0.c ファイル内の scanblock 関数における GC_STRING 型の処理を修正しています。

変更前のコードでは、文字列のデータポインタ (stringptr->str) を一時変数 obj に代入してから markonly(obj) を呼び出していました。

		case GC_STRING:
			stringptr = (String*)(stack_top.b + pc[1]);
			if(stringptr->len != 0) {
				obj = stringptr->str; // ここで一時変数 obj に代入
				markonly(obj);
			}
			pc += 2;
			continue;

変更の背景

Goのガベージコレクタは、プログラムが使用しているメモリを効率的に管理するために、到達可能なオブジェクトを特定し、不要になったオブジェクトを解放する役割を担っています。このプロセスには、メモリをスキャンしてポインタをたどり、到達可能なオブジェクトにマークを付ける「マークフェーズ」が含まれます。

scanblock 関数は、このマークフェーズにおいて、特定のメモリブロックをスキャンし、そこに含まれるポインタを解析して、参照先のオブジェクトをマークする役割を担っています。文字列 (GC_STRING) もGCの対象となるオブジェクトの一つです。

このコミットの背景にある問題は、変更前のコードが文字列のデータポインタを obj という一時変数に代入していた点にあります。コミットメッセージによると、「obj を設定すると、スキャンループの最後にマークのためにキューに入れられる」という動作がありました。しかし、markonly 関数が既にそのオブジェクトをマークしているため、この追加のキューイングは不要であり、冗長な処理となっていました。

つまり、既にマーク済みのオブジェクトに対して、さらにマーク処理のためのキューイングを行うという無駄なオーバーヘッドが発生していたため、これを排除することが目的でした。これはGCの効率をわずかに向上させるための、比較的小規模な最適化です。コミットメッセージに「This can wait for 1.4 if you wish.」とあるように、Go 1.4リリースに向けて検討された、緊急性の低い改善点であったことが伺えます。

前提知識の解説

このコミットを理解するためには、以下のGoランタイムとガベージコレクションに関する基本的な知識が必要です。

  1. Goのガベージコレクション (GC): GoのGCは、主に「マーク&スイープ」アルゴリズムに基づいています。これは、プログラムが現在使用しているオブジェクト(到達可能なオブジェクト)を特定し、それらにマークを付け、マークされなかったオブジェクト(到達不可能なオブジェクト、つまり不要なオブジェクト)をメモリから解放するプロセスです。

    • マークフェーズ (Mark Phase): GCのルート(グローバル変数、スタック上の変数など)から開始し、ポインタをたどって到達可能なすべてのオブジェクトにマークを付けます。
    • スキャン (Scanning): マークフェーズの一部として、GCはメモリブロックやオブジェクト内部をスキャンし、他のオブジェクトへのポインタを見つけ出します。見つかったポインタの参照先のオブジェクトもマークの対象となります。
    • スイープフェーズ (Sweep Phase): マークされなかったオブジェクトが占めていたメモリを解放し、再利用可能にします。
  2. src/pkg/runtime/mgc0.c (または mgc.go): これはGoランタイムのガベージコレクタのコア部分を実装しているC言語のファイル(Go 1.5以降はGo言語で再実装され mgc.go などに移行)です。GCのマークフェーズやスキャンロジックなど、低レベルなメモリ管理の処理が含まれています。

  3. scanblock 関数: mgc0.c 内の重要な関数の一つで、GCのマークフェーズ中にメモリブロックをスキャンする役割を担います。この関数は、与えられたメモリブロック内のデータ型を解析し、ポインタがあればその参照先のオブジェクトをマークします。

  4. GC_STRING: Goランタイム内部で定義されている、文字列型を示す定数または識別子です。scanblock 関数は、スキャン中にこの型に遭遇すると、文字列のデータ部分(実際の文字データが格納されているメモリ領域)をGCの対象として適切に処理します。

  5. String 構造体: Goの文字列は、ランタイム内部では通常、ポインタと長さを持つ構造体として表現されます。

    typedef struct String {
        byte* str; // 文字列データへのポインタ
        intgo len; // 文字列の長さ
    } String;
    

    stringptr->str は、この String 構造体の str フィールド、つまり実際の文字列データが格納されているメモリ領域へのポインタを指します。

  6. markonly 関数: GoランタイムのGCの一部であり、指定されたオブジェクトを「マーク済み」として記録する関数です。この関数が呼び出されると、GCはそのオブジェクトが到達可能であると認識し、スイープフェーズで解放されるのを防ぎます。

技術的詳細

このコミットの技術的詳細は、Goランタイムのガベージコレクタにおける「マークフェーズ」の効率化にあります。

scanblock 関数は、GCがメモリをスキャンする際に、様々な型のオブジェクトを処理します。GC_STRING 型に遭遇した場合、その文字列が指す実際のデータ領域もGCの対象としてマークする必要があります。

変更前のコードでは、以下のステップで処理が行われていました。

  1. stringptr = (String*)(stack_top.b + pc[1]);: 現在スキャンしているメモリ位置 (stack_top.b + pc[1]) から String 構造体へのポインタを取得します。
  2. if(stringptr->len != 0): 文字列の長さが0でないことを確認します。長さが0の文字列はデータを持たないため、マークする必要がありません。
  3. obj = stringptr->str;: ここが変更のポイントです。文字列のデータへのポインタ (stringptr->str) を一時変数 obj に代入していました。
  4. markonly(obj);: obj が指すオブジェクト(文字列データ)をマークします。

コミットメッセージが指摘するように、この obj = stringptr->str; の代入が問題でした。GoのGCの内部ロジックでは、scanblock のようなスキャンループ内で obj のようなローカル変数にポインタが代入されると、その obj 自体がスキャンループの終了時(または別のタイミング)に、再度マーク処理のためにキューに入れられる可能性がありました。

しかし、markonly(obj) が既に呼び出されているため、stringptr->str が指す文字列データは既にマーク済みです。したがって、obj を介して再度キューに入れる処理は冗長であり、CPUサイクルとメモリ帯域の無駄になります。

変更後のコードでは、この冗長性を排除するために、obj = stringptr->str; の行が削除されました。

		case GC_STRING:
			stringptr = (String*)(stack_top.b + pc[1]);
			if(stringptr->len != 0)
				markonly(stringptr->str); // 直接 markonly に渡す
			pc += 2;
			continue;

これにより、stringptr->str が指す文字列データは直接 markonly 関数に渡され、一度だけマークされます。一時変数 obj を介した不要なキューイング処理が回避され、GCの効率がわずかに向上します。これは、特に大量の文字列が生成・処理されるアプリケーションにおいて、GCの一時停止時間(stop-the-world時間)の短縮に寄与する可能性があります。

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

変更は src/pkg/runtime/mgc0.c ファイルの scanblock 関数内、GC_STRING のケースにあります。

--- a/src/pkg/runtime/mgc0.c
+++ b/src/pkg/runtime/mgc0.c
@@ -913,10 +913,8 @@ scanblock(Workbuf *wbuf, bool keepworking)
 
 		case GC_STRING:
 			stringptr = (String*)(stack_top.b + pc[1]);
-			if(stringptr->len != 0) {
-				obj = stringptr->str;
-				markonly(obj);
-			}
+			if(stringptr->len != 0)
+				markonly(stringptr->str);
 			pc += 2;
 			continue;
 

コアとなるコードの解説

変更されたコードは、scanblock 関数内の switch ステートメントの一部です。この switch は、GCがスキャンしているメモリ領域内のオブジェクトの型に基づいて異なる処理を行います。GC_STRING は、現在処理しているオブジェクトがGoの文字列であることを示します。

  • stringptr = (String*)(stack_top.b + pc[1]);: pc はプログラムカウンタのようなもので、stack_top.b + pc[1] は現在のスキャン位置から文字列オブジェクトのメタデータ(String 構造体)へのオフセットを示します。これにより、stringptr はメモリ上の String 構造体を指すポインタとなります。

  • if(stringptr->len != 0): 文字列の長さが0でない場合のみ、その文字列データが有効であるため、マーク処理に進みます。長さが0の文字列は、実際のデータ領域を持たないため、GCの対象としてマークする必要がありません。

  • markonly(stringptr->str); (変更後): この行が変更の核心です。変更前は obj = stringptr->str; markonly(obj); となっていましたが、変更後は stringptr->str を直接 markonly 関数に渡しています。 stringptr->str は、Goの文字列が実際に格納されているバイト配列の先頭へのポインタです。markonly 関数は、このポインタが指すメモリ領域を「到達可能」としてマークします。これにより、GCはこの文字列データを解放せず、プログラムが引き続き使用できるようにします。

この変更により、obj という一時変数を介した冗長な処理が排除され、GCのマークフェーズにおける文字列スキャンの効率が向上しました。これは、Goランタイムのガベージコレクタが、いかに細部にわたる最適化を追求しているかを示す良い例です。

関連リンク

参考にした情報源リンク

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

このコミットは、Goランタイムのガベージコレクタ(GC)における文字列スキャン処理の軽微な最適化に関するものです。具体的には、GCがメモリ上のオブジェクトをスキャンし、到達可能なオブジェクトをマークするフェーズにおいて、文字列オブジェクトの処理方法を改善しています。これにより、不要な処理を削減し、GCの効率をわずかに向上させています。

コミット

commit 40f5e67571d6ce299140638e40bea6b00cc76330
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Wed Mar 26 15:03:58 2014 +0400

    runtime: minor improvement of string scanning
    If we set obj, then it will be enqueued for marking at the end of the scanning loop.
    This is not necessary, since we've already marked it.
    This can wait for 1.4 if you wish.
    
    LGTM=rsc
    R=rsc
    CC=golang-codereviews
    https://golang.org/cl/80030043

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

https://github.com/golang/go/commit/40f5e67571d6ce299140638e40bea6b00cc76330

元コミット内容

このコミットは、src/pkg/runtime/mgc0.c ファイル内の scanblock 関数における GC_STRING 型の処理を修正しています。

変更前のコードでは、文字列のデータポインタ (stringptr->str) を一時変数 obj に代入してから markonly(obj) を呼び出していました。

		case GC_STRING:
			stringptr = (String*)(stack_top.b + pc[1]);
			if(stringptr->len != 0) {
				obj = stringptr->str; // ここで一時変数 obj に代入
				markonly(obj);
			}
			pc += 2;
			continue;

変更の背景

Goのガベージコレクタは、プログラムが使用しているメモリを効率的に管理するために、到達可能なオブジェクトを特定し、不要になったオブジェクトを解放する役割を担っています。このプロセスには、メモリをスキャンしてポインタをたどり、到達可能なオブジェクトにマークを付ける「マークフェーズ」が含まれます。

scanblock 関数は、このマークフェーズにおいて、特定のメモリブロックをスキャンし、そこに含まれるポインタを解析して、参照先のオブジェクトをマークする役割を担っています。文字列 (GC_STRING) もGCの対象となるオブジェクトの一つです。

このコミットの背景にある問題は、変更前のコードが文字列のデータポインタを obj という一時変数に代入していた点にあります。コミットメッセージによると、「obj を設定すると、スキャンループの最後にマークのためにキューに入れられる」という動作がありました。しかし、markonly 関数が既にそのオブジェクトをマークしているため、この追加のキューイングは不要であり、冗長な処理となっていました。

つまり、既にマーク済みのオブジェクトに対して、さらにマーク処理のためのキューイングを行うという無駄なオーバーヘッドが発生していたため、これを排除することが目的でした。これはGCの効率をわずかに向上させるための、比較的小規模な最適化です。コミットメッセージに「This can wait for 1.4 if you wish.」とあるように、Go 1.4リリースに向けて検討された、緊急性の低い改善点であったことが伺えます。

前提知識の解説

このコミットを理解するためには、以下のGoランタイムとガベージコレクションに関する基本的な知識が必要です。

  1. Goのガベージコレクション (GC): GoのGCは、主に「マーク&スイープ」アルゴリズムに基づいています。これは、プログラムが現在使用しているオブジェクト(到達可能なオブジェクト)を特定し、それらにマークを付け、マークされなかったオブジェクト(到達不可能なオブジェクト、つまり不要なオブジェクト)をメモリから解放するプロセスです。

    • マークフェーズ (Mark Phase): GCのルート(グローバル変数、スタック上の変数など)から開始し、ポインタをたどって到達可能なすべてのオブジェクトにマークを付けます。
    • スキャン (Scanning): マークフェーズの一部として、GCはメモリブロックやオブジェクト内部をスキャンし、他のオブジェクトへのポインタを見つけ出します。見つかったポインタの参照先のオブジェクトもマークの対象となります。
    • スイープフェーズ (Sweep Phase): マークされなかったオブジェクトが占めていたメモリを解放し、再利用可能にします。
  2. src/pkg/runtime/mgc0.c (または mgc.go): これはGoランタイムのガベージコレクタのコア部分を実装しているC言語のファイル(Go 1.5以降はGo言語で再実装され mgc.go などに移行)です。GCのマークフェーズやスキャンロジックなど、低レベルなメモリ管理の処理が含まれています。

  3. scanblock 関数: mgc0.c 内の重要な関数の一つで、GCのマークフェーズ中にメモリブロックをスキャンする役割を担います。この関数は、与えられたメモリブロック内のデータ型を解析し、ポインタがあればその参照先のオブジェクトをマークします。

  4. GC_STRING: Goランタイム内部で定義されている、文字列型を示す定数または識別子です。scanblock 関数は、スキャン中にこの型に遭遇すると、文字列のデータ部分(実際の文字データが格納されているメモリ領域)をGCの対象として適切に処理します。

  5. String 構造体: Goの文字列は、ランタイム内部では通常、ポインタと長さを持つ構造体として表現されます。

    typedef struct String {
        byte* str; // 文字列データへのポインタ
        intgo len; // 文字列の長さ
    } String;
    

    stringptr->str は、この String 構造体の str フィールド、つまり実際の文字列データが格納されているメモリ領域へのポインタを指します。

  6. markonly 関数: GoランタイムのGCの一部であり、指定されたオブジェクトを「マーク済み」として記録する関数です。この関数が呼び出されると、GCはそのオブジェクトが到達可能であると認識し、スイープフェーズで解放されるのを防ぎます。

技術的詳細

このコミットの技術的詳細は、Goランタイムのガベージコレクタにおける「マークフェーズ」の効率化にあります。

scanblock 関数は、GCがメモリをスキャンする際に、様々な型のオブジェクトを処理します。GC_STRING 型に遭遇した場合、その文字列が指す実際のデータ領域もGCの対象としてマークする必要があります。

変更前のコードでは、以下のステップで処理が行われていました。

  1. stringptr = (String*)(stack_top.b + pc[1]);: 現在スキャンしているメモリ位置 (stack_top.b + pc[1]) から String 構造体へのポインタを取得します。
  2. if(stringptr->len != 0): 文字列の長さが0でないことを確認します。長さが0の文字列はデータを持たないため、マークする必要がありません。
  3. obj = stringptr->str;: ここが変更のポイントです。文字列のデータへのポインタ (stringptr->str) を一時変数 obj に代入していました。
  4. markonly(obj);: obj が指すオブジェクト(文字列データ)をマークします。

コミットメッセージが指摘するように、この obj = stringptr->str; の代入が問題でした。GoのGCの内部ロジックでは、scanblock のようなスキャンループ内で obj のようなローカル変数にポインタが代入されると、その obj 自体がスキャンループの終了時(または別のタイミング)に、再度マーク処理のためにキューに入れられる可能性がありました。

しかし、markonly(obj) が既に呼び出されているため、stringptr->str が指す文字列データは既にマーク済みです。したがって、obj を介して再度キューに入れる処理は冗長であり、CPUサイクルとメモリ帯域の無駄になります。

変更後のコードでは、この冗長性を排除するために、obj = stringptr->str; の行が削除されました。

		case GC_STRING:
			stringptr = (String*)(stack_top.b + pc[1]);
			if(stringptr->len != 0)
				markonly(stringptr->str); // 直接 markonly に渡す
			pc += 2;
			continue;

これにより、stringptr->str が指す文字列データは直接 markonly 関数に渡され、一度だけマークされます。一時変数 obj を介した不要なキューイング処理が回避され、GCの効率がわずかに向上します。これは、特に大量の文字列が生成・処理されるアプリケーションにおいて、GCの一時停止時間(stop-the-world時間)の短縮に寄与する可能性があります。

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

変更は src/pkg/runtime/mgc0.c ファイルの scanblock 関数内、GC_STRING のケースにあります。

--- a/src/pkg/runtime/mgc0.c
+++ b/src/pkg/runtime/mgc0.c
@@ -913,10 +913,8 @@ scanblock(Workbuf *wbuf, bool keepworking)
 
 		case GC_STRING:
 			stringptr = (String*)(stack_top.b + pc[1]);
-			if(stringptr->len != 0) {
-				obj = stringptr->str;
-				markonly(obj);
-			}
+			if(stringptr->len != 0)
+				markonly(stringptr->str);
 			pc += 2;
 			continue;
 

コアとなるコードの解説

変更されたコードは、scanblock 関数内の switch ステートメントの一部です。この switch は、GCがスキャンしているメモリ領域内のオブジェクトの型に基づいて異なる処理を行います。GC_STRING は、現在処理しているオブジェクトがGoの文字列であることを示します。

  • stringptr = (String*)(stack_top.b + pc[1]);: pc はプログラムカウンタのようなもので、stack_top.b + pc[1] は現在のスキャン位置から文字列オブジェクトのメタデータ(String 構造体)へのオフセットを示します。これにより、stringptr はメモリ上の String 構造体を指すポインタとなります。

  • if(stringptr->len != 0): 文字列の長さが0でない場合のみ、その文字列データが有効であるため、マーク処理に進みます。長さが0の文字列は、実際のデータ領域を持たないため、GCの対象としてマークする必要がありません。

  • markonly(stringptr->str); (変更後): この行が変更の核心です。変更前は obj = stringptr->str; markonly(obj); となっていましたが、変更後は stringptr->str を直接 markonly 関数に渡しています。 stringptr->str は、Goの文字列が実際に格納されているバイト配列の先頭へのポインタです。markonly 関数は、このポインタが指すメモリ領域を「到達可能」としてマークします。これにより、GCはこの文字列データを解放せず、プログラムが引き続き使用できるようにします。

この変更により、obj という一時変数を介した冗長な処理が排除され、GCのマークフェーズにおける文字列スキャンの効率が向上しました。これは、Goランタイムのガベージコレクタが、いかに細部にわたる最適化を追求しているかを示す良い例です。

関連リンク

参考にした情報源リンク