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

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

このコミットは、Goコンパイラのガベージコレクション(GC)関連コードの一部である src/cmd/gc/reflect.c に変更を加えています。具体的には、GCが小さな配列型を処理する方法を最適化し、パフォーマンスを向上させることを目的としています。

コミット

cmd/gc: unroll small array types

R=golang-dev, rsc CC=golang-dev, nigeltao https://golang.org/cl/7812044

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

https://github.com/golang/go/commit/175c4015233272311d20dad8eb3e6f7d1acd941a

元コミット内容

commit 175c4015233272311d20dad8eb3e6f7d1acd941a
Author: Jan Ziak <0xe2.0x9a.0x9b@gmail.com>
Date:   Fri Mar 15 06:57:50 2013 +0100

    cmd/gc: unroll small array types
    
    R=golang-dev, rsc
    CC=golang-dev, nigeltao
    https://golang.org/cl/7812044
---
 src/cmd/gc/reflect.c | 20 +++++++++++++++++---\n 1 file changed, 17 insertions(+), 3 deletions(-)\n

変更の背景

Goのガベージコレクタは、プログラムが使用しなくなったメモリを自動的に解放する役割を担っています。このプロセスには、メモリ上のオブジェクトをスキャンし、到達可能なオブジェクトをマークするフェーズが含まれます。配列のような複合型をスキャンする際、特に小さな配列の場合、配列全体を一つの大きなブロックとして扱うよりも、個々の要素を直接処理する方が効率的な場合があります。

このコミットの背景には、Goのガベージコレクタが配列を処理する際のオーバーヘッドを削減し、全体的なGCのパフォーマンスを向上させるという目的があります。特に、要素数が少ない(またはサイズが小さい)配列に対して、より直接的なスキャンアプローチを適用することで、GCのレイテンシを短縮し、アプリケーションの実行速度を向上させることが期待されます。

前提知識の解説

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

Goのガベージコレクタは、並行マーク&スイープ方式を採用しています。これは、プログラムの実行と並行してGCが動作し、アプリケーションの一時停止(ストップ・ザ・ワールド)時間を最小限に抑えることを目指しています。GCの主要なフェーズには以下が含まれます。

  • マークフェーズ: ルート(グローバル変数、スタック上の変数など)から到達可能なすべてのオブジェクトをマークします。
  • スキャンフェーズ: マークされたオブジェクトをスキャンし、それらが参照する他のオブジェクトをマークします。このプロセスは再帰的に行われます。
  • スイープフェーズ: マークされなかった(到達不可能な)オブジェクトが占めるメモリを解放し、再利用可能にします。

配列や構造体のような複合型をスキャンする際、GCはそれらの内部にポインタが含まれているかどうかを判断し、含まれていればそのポインタが指すオブジェクトもスキャン対象とします。

src/cmd/gc/reflect.c の役割

src/cmd/gc/reflect.c は、Goコンパイラのバックエンドの一部であり、Goの型システムとガベージコレクタが連携するための重要な役割を担っています。このファイルは、Goのランタイムリフレクション(実行時に型情報を検査・操作する機能)に関連するコードを含んでいますが、同時にコンパイラが生成する型情報がGCによってどのように利用されるか、特にポインタを含む型や配列型がどのようにスキャンされるかを定義しています。

dgcsym1 のような関数は、特定の型がどのようにメモリ上に配置され、GCがその型をスキャンする際にどのオフセットにポインタが存在するか、あるいはポインタが存在しない場合はどのようにスキップするかを決定するために使用されます。

配列のアンローリング (Array Unrolling)

一般的に「ループアンローリング」という最適化手法がありますが、ここではGCの文脈で「配列のアンローリング」という概念が使われています。これは、配列の要素を一つずつ明示的に処理することで、配列全体を抽象的に扱うことによるオーバーヘッド(例えば、ループのセットアップ、インデックス計算、境界チェックなど)を削減する手法です。

GCの文脈では、小さな配列をスキャンする際に、配列全体を一つの単位として処理するのではなく、その個々の要素に対してGCスキャン関数を直接呼び出すことで、より効率的な処理を可能にします。これにより、GCが配列の内部構造をより詳細に「認識」し、ポインタの有無やその位置を正確に把握して、無駄な処理を省くことができます。

技術的詳細

このコミットの核心は、gcinline という新しいヘルパー関数の導入と、既存の dgcsym1 関数における配列処理ロジックの変更です。

gcinline 関数の導入

static int
gcinline(Type *t) {
	switch(t->etype) {
	case TARRAY:
		if(t->bound == 1)
			return 1;
		if(t->width <= 4*widthptr)
			return 1;
		break;
	}
	return 0;
}
  • gcinline 関数は、与えられた型 t がGCによって「インライン処理」(つまり、要素ごとにアンロールして処理)すべき小さな配列であるかどうかを判断します。
  • t->etypeTARRAY (配列型) である場合にのみ処理を行います。
  • 条件1: t->bound == 1: 配列の要素数 (bound) が1である場合、これは単一要素の配列であり、常にインライン処理の対象となります。これは、配列としてのオーバーヘッドが最も少ないケースです。
  • 条件2: t->width <= 4*widthptr: 配列の合計サイズ (width) が、ポインタのサイズ (widthptr) の4倍以下である場合も、インライン処理の対象となります。widthptr は通常、システムのポインタサイズ(32ビットシステムでは4バイト、64ビットシステムでは8バイト)を指します。この条件は、配列が非常に小さい(例えば、ポインタ4つ分以下のサイズ)場合に適用されます。これは、要素数が1でなくても、全体として非常にコンパクトな配列であれば、個別に処理する方が効率的であるという判断に基づいています。

この gcinline 関数は、GCが配列をスキャンする際に、配列のサイズに基づいて動的に最適化のパスを選択するためのゲートウェイとして機能します。

dgcsym1 関数の変更

dgcsym1 関数は、シンボル s、元の型 ot、現在の型 t、オフセット off、スタックサイズ stack_size を引数に取り、GCがメモリ上のオブジェクトをスキャンする際の型情報を処理します。この関数は再帰的に呼び出され、複合型(構造体や配列)の内部を深く探索します。

変更前は、配列 t の処理において、t->bound == 1 (単一要素配列) の場合にのみ、その要素に対して dgcsym1 を再帰的に呼び出していました。

変更後は、この条件が gcinline(t) に置き換えられました。

// 変更前
// else if(t->bound == 1) {
// 	ot = dgcsym1(s, ot, t->type, off, stack_size);  // recursive call of dgcsym1
// }

// 変更後
else if(gcinline(t)) {
	for(i=0; i<t->bound; i++)
		ot = dgcsym1(s, ot, t->type, off, stack_size);  // recursive call of dgcsym1
}
  • gcinline(t) が真の場合(つまり、小さな配列であると判断された場合)、for ループが導入されます。
  • このループは i=0 から t->bound-1 まで実行され、配列の各要素に対して dgcsym1 を再帰的に呼び出します。
  • t->type は配列の要素の型を指します。off は現在のメモリオフセットを追跡し、各要素の処理後に更新されます。

この変更により、GCは小さな配列の各要素を明示的にスキャンするようになります。これにより、配列全体を抽象的に処理するよりも、個々の要素のポインタの有無や値に基づいて、よりきめ細かく効率的なGCスキャンが可能になります。これは、特にポインタを含まない小さな配列の場合に、GCが不要なポインタチェックや複雑なロジックをスキップできるため、パフォーマンス上の利点があります。

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

--- a/src/cmd/gc/reflect.c
+++ b/src/cmd/gc/reflect.c
@@ -1025,11 +1025,24 @@ dalgsym(Type *t)
 	return s;
 }
 
+static int
+gcinline(Type *t) {
+	switch(t->etype) {
+	case TARRAY:
+		if(t->bound == 1)
+			return 1;
+		if(t->width <= 4*widthptr)
+			return 1;
+		break;
+	}
+	return 0;
+}
+
 static int
 dgcsym1(Sym *s, int ot, Type *t, vlong *off, int stack_size)
 {
 	Type *t1;
-\tvlong o, off2, fieldoffset;\n+\tvlong o, off2, fieldoffset, i;\n 
 \tif(t->align > 0 && (*off % t->align) != 0)\n \t\tfatal(\"dgcsym1: invalid initial alignment, %T\", t);\n@@ -1132,8 +1145,9 @@ dgcsym1(Sym *s, int ot, Type *t, vlong *off, int stack_size)\n \t\t} else {\n \t\t\tif(t->bound < 1 || !haspointers(t->type)) {\n \t\t\t\t*off += t->width;\n-\t\t\t} else if(t->bound == 1) {\n-\t\t\t\tot = dgcsym1(s, ot, t->type, off, stack_size);  // recursive call of dgcsym1\n+\t\t\t} else if(gcinline(t)) {\n+\t\t\t\tfor(i=0; i<t->bound; i++)\n+\t\t\t\t\tot = dgcsym1(s, ot, t->type, off, stack_size);  // recursive call of dgcsym1\n \t\t\t} else {\n \t\t\t\tif(stack_size < GC_STACK_CAPACITY) {\n \t\t\t\t\tot = duintptr(s, ot, GC_ARRAY_START);\n```

## コアとなるコードの解説

1.  **`gcinline` 関数の追加**:
    *   この新しい関数は、Goの型システムにおける `Type` 構造体を受け取ります。
    *   `TARRAY` (配列型) の場合、その `bound` (要素数) が1であるか、または `width` (バイト単位のサイズ) が `4 * widthptr` (ポインタサイズの4倍) 以下である場合に `1` を返します。これは、GCが個々の要素を直接処理する(アンロールする)べき「小さな配列」を識別するためのロジックです。
    *   それ以外の場合は `0` を返します。

2.  **`dgcsym1` 関数の変更**:
    *   `dgcsym1` は、GCが型情報を走査し、メモリ上のポインタを識別するための主要な関数です。
    *   変更前は、配列の `bound` が1の場合にのみ、その要素に対して再帰的に `dgcsym1` を呼び出していました。
    *   変更後は、この条件が `gcinline(t)` の呼び出しに置き換えられました。これにより、単一要素の配列だけでなく、`gcinline` が「小さい」と判断したすべての配列に対して、以下のループが適用されます。
    *   `for(i=0; i<t->bound; i++) ot = dgcsym1(s, ot, t->type, off, stack_size);`
        *   このループは、配列の各要素に対して `dgcsym1` を再帰的に呼び出します。これにより、GCは配列全体を一つの塊としてではなく、個々の要素として処理するようになります。
        *   `t->type` は配列の要素の型を表します。
        *   `off` は、現在の要素のメモリ上のオフセットを追跡するために使用されます。

この変更により、Goのガベージコレクタは、小さな配列に対してより粒度の細かい、最適化されたスキャン戦略を採用するようになります。これにより、GCのオーバーヘッドが削減され、特にポインタを含まない小さな配列の処理が効率化されることで、全体的なアプリケーションのパフォーマンスが向上する可能性があります。

## 関連リンク

*   Go CL 7812044: [https://golang.org/cl/7812044](https://golang.org/cl/7812044)

## 参考にした情報源リンク

*   Goのガベージコレクションに関する公式ドキュメントやブログ記事 (一般的なGo GCの理解のため)
*   Goのコンパイラソースコード (`src/cmd/gc/`) (具体的な関数の役割理解のため)
*   ループアンローリングに関する一般的な最適化の概念 (「unroll」の理解のため)
*   `widthptr` の定義に関するGoのソースコードまたはドキュメント (Goの内部型表現の理解のため)
*   `TARRAY` や `etype` など、Goの型システムに関する情報 (Goの型表現の理解のため)