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

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

このコミットは、Go言語のARMアーキテクチャ向けコンパイラである5gにおいて、構造体やインターフェース、スライス、文字列などの複合型データのレジスタ割り当て(registerization)を改善するためにcomponentgenという新しい関数を導入するものです。これにより、コンパイラがこれらの型をより効率的に処理し、生成されるアセンブリコードのパフォーマンスが向上することが期待されます。特に、6g(x86アーキテクチャ向けコンパイラ)のバージョンと本質的に同じロジックが導入されています。

コミット

commit ee3e2ac1a691f5e3d9a8fbbc9f6d79c606b81868
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date:   Sun Oct 28 20:11:21 2012 +0100

    cmd/5g: introduce componentgen for better registerization.
    
    It is essentially identical to the version in 6g.
    
    R=dave, minux.ma, rsc
    CC=golang-dev
    https://golang.org/cl/6710043

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

https://github.com/golang/go/commit/ee3e2ac1a691f5e3d9a8fbbc9f6d79c606b81868

元コミット内容

cmd/5g: introduce componentgen for better registerization.

It is essentially identical to the version in 6g.

R=dave, minux.ma, rsc
CC=golang-dev
https://golang.org/cl/6710043

変更の背景

Goコンパイラは、プログラムのパフォーマンスを最適化するために、変数をメモリではなくCPUレジスタに割り当てる「レジスタ割り当て(registerization)」という最適化を行います。レジスタはメモリよりもはるかに高速にアクセスできるため、レジスタに割り当てられた変数はプログラムの実行速度を向上させます。

このコミットの背景には、5g(ARMアーキテクチャ向けGoコンパイラ)が、構造体、インターフェース、スライス、文字列といった複合型データのレジスタ割り当てにおいて、6g(x86アーキテクチャ向けGoコンパイラ)と同等の効率性を持っていなかったという問題があります。複合型は複数のコンポーネント(フィールドや要素)から構成されており、これらのコンポーネントを個別にレジスタに割り当てることで、よりきめ細やかな最適化が可能になります。

componentgen関数の導入は、このギャップを埋め、5gコンパイラが複合型のコンポーネントレベルでのレジスタ割り当てをより効果的に行えるようにすることを目的としています。これにより、ARMアーキテクチャ上でのGoプログラムの実行効率が向上し、特にこれらの複合型を多用するコードにおいて顕著なパフォーマンス改善が期待されます。

前提知識の解説

Goコンパイラと5g

Go言語のコンパイラは、ソースコードを機械語に変換する役割を担っています。Goのツールチェインは、異なるCPUアーキテクチャ向けに複数のコンパイラを含んでいます。

  • 5g: ARMアーキテクチャ(例: Raspberry Pi, スマートフォン)向けのGoコンパイラです。
  • 6g: x86-64アーキテクチャ(一般的なデスクトップPCやサーバー)向けのGoコンパイラです。
  • 8g: x86-32アーキテクチャ向けのGoコンパイラです。

これらのコンパイラは、それぞれターゲットアーキテクチャの特性に合わせてコード生成を行います。

レジスタ割り当て (Registerization)

レジスタ割り当ては、コンパイラの重要な最適化手法の一つです。CPUには少数の高速な記憶領域である「レジスタ」があります。プログラムが変数を頻繁にアクセスする場合、その変数をメモリに置くよりもレジスタに置いた方が、アクセス速度が格段に向上します。レジスタ割り当ては、どの変数をどのレジスタに割り当てるかを決定するプロセスです。

Goの複合型の内部表現

Go言語の複合型は、内部的には以下のように表現されます。

  • インターフェース (interface{}):

    • itab (interface table): 型情報とメソッドセットへのポインタ。
    • data (data pointer): 実際の値へのポインタ。 これら2つのポインタで構成されます。
  • スライス ([]T):

    • array (data pointer): 基底配列へのポインタ。
    • len (length): スライスの現在の長さ。
    • cap (capacity): 基底配列の容量。 これら3つの要素で構成されます。
  • 文字列 (string):

    • array (data pointer): 文字列のバイトデータへのポインタ。
    • len (length): 文字列の長さ(バイト数)。 これら2つの要素で構成されます。

これらの複合型は、複数の「コンポーネント」から構成されており、それぞれが独立した値として扱われる可能性があります。componentgenは、これらのコンポーネントを個別にレジスタに割り当てることで、より効率的なコード生成を目指します。

技術的詳細

このコミットの主要な変更点は、src/cmd/5g/cgen.ccomponentgen関数を導入し、既存のコード生成ロジックを修正して、特定の複合型(インターフェース、スライス、文字列)の処理においてこの新しい関数を利用するようにしたことです。

componentgenの役割

componentgen関数は、構造体、インターフェース、スライス、文字列といった複合型のコピーやゼロクリア(初期化)を、そのコンポーネント(内部のフィールドやポインタ)ごとに処理することを可能にします。これにより、コンパイラは複合型全体を一度にメモリ間でコピーするのではなく、個々のコンポーネントをレジスタを介して効率的に移動させたり、ゼロで埋めたりすることができます。

具体的には、componentgenは以下のケースで呼び出されます。

  • sgen関数(構造体のコピーを生成する関数)内で、サイズが8バイトまたは12バイトの構造体(これはGoのインターフェース、スライス、文字列の典型的なサイズに相当します)をコピーする際に呼び出されます。
  • clearfat関数(大きな構造体をゼロクリアする関数)内で、サイズが8バイトまたは12バイトの構造体をゼロクリアする際に呼び出されます。

componentgenは、引数としてコピー元(nr)とコピー先(nl)のNodeを受け取ります。ゼロクリアの場合はnrN(nil)になります。関数内部では、nlの型(TARRAYTSTRINGTINTER)に基づいて、それぞれのコンポーネント(Array_arrayArray_nelArray_capなどのオフセットでアクセスされる内部ポインタや長さ情報)を個別に処理します。

cgen.cの変更点

cgen.cは、GoのAST(抽象構文木)ノードをアセンブリコードに変換する主要なファイルです。このコミットでは、特にOLENlen組み込み関数)、OCAPcap組み込み関数)、OITAB(インターフェースの型情報テーブルへのアクセス)の処理が変更されています。

変更前は、これらの操作で一時的なレジスタを複数割り当てていましたが、変更後はigen関数(アドレス生成)とgmove関数(汎用的なデータ移動)をより直接的に使用し、不要なレジスタ割り当てを削減しています。これは、componentgenの導入と合わせて、レジスタの利用効率を高めるための変更です。

例えば、OLENOCAPでスライスや文字列の長さを取得する際、以前はregallocで一時レジスタを確保し、gmoveでデータを移動していましたが、変更後はigenで直接アドレスを生成し、そのアドレスからgmoveres(結果を格納するノード)に直接データを移動するように簡略化されています。これにより、中間的なレジスタの利用が減り、コード生成がより効率的になります。

また、igen関数にONAMEOINDREGのケースが追加され、レジスタの参照カウント管理が改善されています。これにより、igenの呼び出し元がregfreeを呼び出す必要がある場合に、レジスタのライフサイクルが正しく管理されるようになります。

ggen.cの変更点

ggen.cは、汎用的なコード生成ルーチンを含むファイルです。clearfat関数にcomponentgenの呼び出しが追加されました。これにより、サイズが8バイトまたは12バイトの複合型をゼロクリアする際に、componentgenが利用され、コンポーネントごとのゼロクリアが可能になります。

gsubr.cの変更点

gsubr.cは、サブルーチンやユーティリティ関数を含むファイルです。

  • resvd配列にREGSP(スタックポインタレジスタ)が追加されました。これは、スタックポインタが予約済みレジスタとして扱われ、誤って割り当てられないようにするためです。
  • regfree関数(レジスタを解放する関数)のロジックが修正され、REGSPが解放対象から除外されるようになりました。また、reg[i] <= 0の場合のfatalエラーメッセージがより詳細になりました。

reg.cの変更点

reg.cは、レジスタ割り当てに関連する関数を含むファイルです。

  • mkvar関数(変数をレジスタに割り当てる準備をする関数)から、TARRAYTSTRINGgoto noneの対象から除外されました。これは、これらの型がcomponentgenによってコンポーネントレベルで処理されるようになったため、以前のようにレジスタ割り当ての対象外とする必要がなくなったことを示唆しています。

これらの変更は、5gコンパイラが複合型をより効率的に扱い、レジスタをより効果的に利用できるようにするための包括的な取り組みの一部です。

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

src/cmd/5g/cgen.cにおけるOITABOLENOCAPの変更

--- a/src/cmd/5g/cgen.c
+++ b/src/cmd/5g/cgen.c
@@ -307,17 +307,11 @@ cgen(Node *n, Node *res)
 		break;
 
 	case OITAB:
-		// itable of interface value
+		// interface table is first word of interface value
 		igen(nl, &n1, res);
-		n1.op = OREGISTER;	// was OINDREG
-		regalloc(&n2, n->type, &n1);
-		n1.op = OINDREG;
 		n1.type = n->type;
-		n1.xoffset += 0;
-		gmove(&n1, &n2);
-		gmove(&n2, res);
+		gmove(&n1, res);
 		regfree(&n1);
-		regfree(&n2);
 		break;
 
 	case OLEN:
@@ -345,15 +339,10 @@ cgen(Node *n, Node *res)
 		if(istype(nl->type, TSTRING) || isslice(nl->type)) {
 			// both slice and string have len one pointer into the struct.
 			igen(nl, &n1, res);
-			n1.op = OREGISTER;	// was OINDREG
-			regalloc(&n2, types[TUINT32], &n1);
-			n1.op = OINDREG;
 			n1.type = types[TUINT32];
 			n1.xoffset += Array_nel;
-			gmove(&n1, &n2);
-			gmove(&n2, res);
+			gmove(&n1, res);
 			regfree(&n1);
-			regfree(&n2);
 			break;
 		}
 		fatal("cgen: OLEN: unknown type %lT", nl->type);
@@ -383,11 +372,9 @@ cgen(Node *n, Node *res)
 			break;
 		}
 		if(isslice(nl->type)) {
-			regalloc(&n1, types[tptr], res);
-			agen(nl, &n1);
-			n1.op = OINDREG;
+			igen(nl, &n1, res);
 			n1.type = types[TUINT32];
-			n1.xoffset = Array_cap;
+			n1.xoffset += Array_cap;
 			gmove(&n1, res);
 			regfree(&n1);
 			break;

src/cmd/5g/cgen.cにおけるigenの変更

--- a/src/cmd/5g/cgen.c
+++ b/src/cmd/5g/cgen.c
@@ -898,6 +885,20 @@ igen(Node *n, Node *a, Node *res)
 		dump("\nigen-n", n);
 	}
 	switch(n->op) {
+	case ONAME:
+		if((n->class&PHEAP) || n->class == PPARAMREF)
+			break;
+		*a = *n;
+		return;
+
+	case OINDREG:
+		// Increase the refcount of the register so that igen's caller
+		// has to call regfree.
+		if(n->val.u.reg != REGSP)
+			reg[n->val.u.reg]++;
+		*a = *n;
+		return;
+
 	case ODOT:
 		igen(n->left, a, res);
 		a->xoffset += n->xoffset;

src/cmd/5g/cgen.cにおけるcomponentgenの追加

--- a/src/cmd/5g/cgen.c
+++ b/src/cmd/5g/cgen.c
@@ -1399,6 +1356,10 @@ sgen(Node *n, Node *res, int64 w)
 		return;
 	}
 
+	if(w == 8 || w == 12)
+		if(componentgen(n, res))
+			return;
+	
 	// determine alignment.
 	// want to avoid unaligned access, so have to use
 	// smaller operations for less aligned types.
@@ -1516,3 +1477,156 @@ sgen(Node *n, Node *res, int64 w)
 	regfree(&src);
 	regfree(&tmp);
 }
+
+static int
+cadable(Node *n)
+{
+	if(!n->addable) {
+		// dont know how it happens,
+		// but it does
+		return 0;
+	}
+
+	switch(n->op) {
+	case ONAME:
+		return 1;
+	}
+	return 0;
+}
+
+/*
+ * copy a structure component by component
+ * return 1 if can do, 0 if cant.
+ * nr is N for copy zero
+ */
+int
+componentgen(Node *nr, Node *nl)
+{
+	Node nodl, nodr, tmp;
+	int freel, freer;
+
+	freel = 0;
+	freer = 0;
+
+	switch(nl->type->etype) {
+	default:
+		goto no;
+
+	case TARRAY:
+		if(!isslice(nl->type))
+			goto no;
+	case TSTRING:
+	case TINTER:
+		break;
+	}
+
+	nodl = *nl;
+	if(!cadable(nl)) {
+		if(nr == N || !cadable(nr))
+			goto no;
+		igen(nl, &nodl, N);
+		freel = 1;
+	}
+
+	if(nr != N) {
+		nodr = *nr;
+		if(!cadable(nr)) {
+			igen(nr, &nodr, N);
+			freer = 1;
+		}
+	} else {
+		// When zeroing, prepare a register containing zero.
+		nodconst(&tmp, nl->type, 0);
+		regalloc(&nodr, types[TUINT], N);
+		gmove(&tmp, &nodr);
+		freer = 1;
+	}
+
+	switch(nl->type->etype) {
+	case TARRAY:
+		nodl.xoffset += Array_array;
+		nodl.type = ptrto(nl->type->type);
+
+		if(nr != N) {
+			nodr.xoffset += Array_array;
+			nodr.type = nodl.type;
+		}
+		gmove(&nodr, &nodl);
+
+		nodl.xoffset += Array_nel-Array_array;
+		nodl.type = types[simtype[TUINT]];
+
+		if(nr != N) {
+			nodr.xoffset += Array_nel-Array_array;
+			nodr.type = nodl.type;
+		}
+		gmove(&nodr, &nodl);
+
+		nodl.xoffset += Array_cap-Array_nel;
+		nodl.type = types[simtype[TUINT]];
+
+		if(nr != N) {
+			nodr.xoffset += Array_cap-Array_nel;
+			nodr.type = nodl.type;
+		}
+		gmove(&nodr, &nodl);
+
+		goto yes;
+
+	case TSTRING:
+		nodl.xoffset += Array_array;
+		nodl.type = ptrto(types[TUINT8]);
+
+		if(nr != N) {
+			nodr.xoffset += Array_array;
+			nodr.type = nodl.type;
+		}
+		gmove(&nodr, &nodl);
+
+		nodl.xoffset += Array_nel-Array_array;
+		nodl.type = types[simtype[TUINT]];
+
+		if(nr != N) {
+			nodr.xoffset += Array_nel-Array_array;
+			nodr.type = nodl.type;
+		}
+		gmove(&nodr, &nodl);
+
+		goto yes;
+
+	case TINTER:
+		nodl.xoffset += Array_array;
+		nodl.type = ptrto(types[TUINT8]);
+
+		if(nr != N) {
+			nodr.xoffset += Array_array;
+			nodr.type = nodl.type;
+		}
+		gmove(&nodr, &nodl);
+
+		nodl.xoffset += Array_nel-Array_array;
+		nodl.type = ptrto(types[TUINT8]);
+
+		if(nr != N) {
+			nodr.xoffset += Array_nel-Array_array;
+			nodr.type = nodl.type;
+		}
+		gmove(&nodr, &nodl);
+
+		goto yes;
+	}
+
+no:
+	if(freer)
+		regfree(&nodr);
+	if(freel)
+		regfree(&nodl);
+	return 0;
+
+yes:
+	if(freer)
+		regfree(&nodr);
+	if(freel)
+		regfree(&nodl);
+	return 1;
+}

src/cmd/5g/ggen.cにおけるclearfatの変更

--- a/src/cmd/5g/ggen.c
+++ b/src/cmd/5g/ggen.c
@@ -616,7 +616,12 @@ clearfat(Node *nl)
 	if(debug['g'])
 		dump("\nclearfat", nl);
 
+
 	w = nl->type->width;
+	if(w == 8 || w == 12)
+		if(componentgen(N, nl))
+			return;
+
 	c = w % 4;	// bytes
 	q = w / 4;	// quads

src/cmd/5g/gsubr.cにおけるresvdregfreeの変更

--- a/src/cmd/5g/gsubr.c
+++ b/src/cmd/5g/gsubr.c
@@ -255,8 +255,9 @@ afunclit(Addr *a)
 
 static	int	resvd[] =
 {
-	9,	// reserved for m
-	10,	// reserved for g
+	9,     // reserved for m
+	10,    // reserved for g
+	REGSP, // reserved for SP
 };
 
 void
@@ -400,15 +401,17 @@ regfree(Node *n)
 		print("regalloc fix %d float %d\n", fixfree, floatfree);
 	}
 
-	if(n->op == ONAME && iscomplex[n->type->etype])
+	if(n->op == ONAME)
 		return;
 	if(n->op != OREGISTER && n->op != OINDREG)
 		fatal("regfree: not a register");
 	i = n->val.u.reg;
+	if(i == REGSP)
+		return;
 	if(i < 0 || i >= nelem(reg) || i >= nelem(regpc))
 		fatal("regfree: reg out of range");
 	if(reg[i] <= 0)
-		fatal("regfree: reg not allocated");
+		fatal("regfree: reg %R not allocated", i);
 	reg[i]--;
 	if(reg[i] == 0)
 		regpc[i] = 0;

src/cmd/5g/reg.cにおけるmkvarの変更

--- a/src/cmd/5g/reg.c
+++ b/src/cmd/5g/reg.c
@@ -987,8 +987,6 @@ mkvar(Reg *r, Adr *a)
 	switch(et) {
 	case 0:
 	case TFUNC:
-#tcase TARRAY:
-#tcase TSTRING:
 		goto none;
 	}
 

コアとなるコードの解説

cgen.cの変更

  • OITAB, OLEN, OCAPの最適化: 変更前は、インターフェースのテーブルポインタ、スライスや文字列の長さ・容量を取得する際に、regallocで一時レジスタn2を割り当て、gmoveを2回呼び出してデータを移動していました。 変更後は、igenで直接目的のアドレス(n1)を生成し、gmove(&n1, res)で直接結果レジスタresにデータを移動するように簡略化されています。これにより、中間的なレジスタn2の割り当てと解放が不要になり、コード生成がより効率的になります。これは、レジスタの利用を最小限に抑え、より直接的なデータフローを可能にするための典型的な最適化です。

  • igen関数の拡張: igen関数は、ノードのアドレスを生成する役割を担います。

    • ONAMEケースが追加され、ヒープに割り当てられていない、またはパラメータ参照ではない名前付き変数の場合、そのノード自体をアドレスとして使用できるようになりました。
    • OINDREGケースが追加され、レジスタ間接参照の場合に、そのレジスタの参照カウントをインクリメントするようになりました。これにより、igenの呼び出し元がregfreeを呼び出す必要がある場合に、レジスタが誤って解放されることを防ぎ、レジスタのライフサイクル管理が改善されます。
  • componentgen関数の導入: sgen関数内で、コピー対象の幅が8バイトまたは12バイトの場合にcomponentgenが呼び出されるようになりました。これは、Goのインターフェース(2ポインタ = 8バイト)やスライス(3要素 = 12バイト)のサイズに合致します。 componentgenは、複合型をそのコンポーネントごとにコピーまたはゼロクリアするロジックを実装しています。

    • cadableヘルパー関数は、ノードがアドレス可能(レジスタに直接ロードできる)かどうかをチェックします。
    • componentgenは、コピー元とコピー先のノードが直接アドレス可能でない場合、igenを呼び出してアドレスを生成し、一時レジスタにロードします。
    • TARRAY(スライス)、TSTRINGTINTERの各ケースで、それぞれの内部構造(Array_arrayArray_nelArray_capなどのオフセット)にアクセスし、コンポーネントごとにgmoveを使ってデータを移動します。ゼロクリアの場合は、ゼロを含むレジスタを準備し、それをコピー元として使用します。 この関数により、コンパイラは複合型全体を不透明なバイトの塊として扱うのではなく、その内部構造を理解し、個々のコンポーネントを効率的に操作できるようになります。

ggen.cの変更

  • clearfatでのcomponentgenの利用: clearfat関数は、大きな構造体をゼロクリアする際に使用されます。この関数も、幅が8バイトまたは12バイトの複合型をゼロクリアする際にcomponentgen(N, nl)を呼び出すようになりました。Nはコピー元がないことを示し、componentgenがゼロクリアモードで動作することを意味します。これにより、複合型のゼロクリアもコンポーネントレベルで効率的に行われるようになります。

gsubr.cの変更

  • 予約済みレジスタの追加とregfreeの修正: resvd配列にREGSP(スタックポインタレジスタ)が追加されました。これは、スタックポインタがコンパイラによって予約され、他の用途に誤って割り当てられないようにするためです。 regfree関数は、レジスタを解放する際に、REGSPを解放対象から除外するようになりました。また、レジスタがすでに解放されている場合に発生するエラーメッセージが改善され、どのレジスタが問題であるかを特定しやすくなりました。

reg.cの変更

  • mkvarの修正: mkvar関数は、変数をレジスタに割り当てる準備をする関数です。以前はTARRAYTSTRING型の変数をレジスタ割り当ての対象外としていましたが、componentgenの導入により、これらの複合型もコンポーネントレベルでレジスタ割り当ての恩恵を受けられるようになったため、この制限が解除されました。これにより、コンパイラはこれらの型に対してもより積極的なレジスタ割り当てを試みることができるようになります。

これらの変更は全体として、Goの5gコンパイラが複合型データをより細かく、より効率的に処理できるようにするための重要なステップであり、ARMアーキテクチャ上でのGoプログラムのパフォーマンス向上に貢献します。

関連リンク

  • Go言語のコンパイラに関する公式ドキュメント(当時のものに近い情報源を見つけるのは難しい可能性がありますが、Goのコンパイラ設計に関する一般的な情報は役立ちます)
  • Goのインターフェース、スライス、文字列の内部表現に関する記事やドキュメント
  • コンパイラのレジスタ割り当てに関する一般的な情報

参考にした情報源リンク

  • Go言語のコンパイラに関する一般的な知識
  • コンパイラ最適化(特にレジスタ割り当て)に関する一般的な知識
  • Go言語のインターフェース、スライス、文字列の内部構造に関する情報
  • Gitのコミットログと差分分析
  • Goのソースコード(src/cmd/5g/ディレクトリ内の関連ファイル)# [インデックス 14227] ファイルの概要

このコミットは、Go言語のARMアーキテクチャ向けコンパイラである5gにおいて、構造体やインターフェース、スライス、文字列などの複合型データのレジスタ割り当て(registerization)を改善するためにcomponentgenという新しい関数を導入するものです。これにより、コンパイラがこれらの型をより効率的に処理し、生成されるアセンブリコードのパフォーマンスが向上することが期待されます。特に、6g(x86アーキテクチャ向けコンパイラ)のバージョンと本質的に同じロジックが導入されています。

コミット

commit ee3e2ac1a691f5e3d9a8fbbc9f6d79c606b81868
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date:   Sun Oct 28 20:11:21 2012 +0100

    cmd/5g: introduce componentgen for better registerization.
    
    It is essentially identical to the version in 6g.
    
    R=dave, minux.ma, rsc
    CC=golang-dev
    https://golang.org/cl/6710043

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

https://github.com/golang/go/commit/ee3e2ac1a691f5e3d9a8fbbc9f6d79c606b81868

元コミット内容

cmd/5g: introduce componentgen for better registerization.

It is essentially identical to the version in 6g.

R=dave, minux.ma, rsc
CC=golang-dev
https://golang.org/cl/6710043

変更の背景

Goコンパイラは、プログラムのパフォーマンスを最適化するために、変数をメモリではなくCPUレジスタに割り当てる「レジスタ割り当て(registerization)」という最適化を行います。レジスタはメモリよりもはるかに高速にアクセスできるため、レジスタに割り当てられた変数はプログラムの実行速度を向上させます。

このコミットの背景には、5g(ARMアーキテクチャ向けGoコンパイラ)が、構造体、インターフェース、スライス、文字列といった複合型データのレジスタ割り当てにおいて、6g(x86アーキテクチャ向けGoコンパイラ)と同等の効率性を持っていなかったという問題があります。複合型は複数のコンポーネント(フィールドや要素)から構成されており、これらのコンポーネントを個別にレジスタに割り当てることで、よりきめ細やかな最適化が可能になります。

componentgen関数の導入は、このギャップを埋め、5gコンパイラが複合型のコンポーネントレベルでのレジスタ割り当てをより効果的に行えるようにすることを目的としています。これにより、ARMアーキテクチャ上でのGoプログラムの実行効率が向上し、特にこれらの複合型を多用するコードにおいて顕著なパフォーマンス改善が期待されます。

前提知識の解説

Goコンパイラと5g

Go言語のコンパイラは、ソースコードを機械語に変換する役割を担っています。Goのツールチェインは、異なるCPUアーキテクチャ向けに複数のコンパイラを含んでいます。

  • 5g: ARMアーキテクチャ(例: Raspberry Pi, スマートフォン)向けのGoコンパイラです。
  • 6g: x86-64アーキテクチャ(一般的なデスクトップPCやサーバー)向けのGoコンパイラです。
  • 8g: x86-32アーキテクチャ向けのGoコンパイラです。

これらのコンパイラは、それぞれターゲットアーキテクチャの特性に合わせてコード生成を行います。

レジスタ割り当て (Registerization)

レジスタ割り当ては、コンパイラの重要な最適化手法の一つです。CPUには少数の高速な記憶領域である「レジスタ」があります。プログラムが変数を頻繁にアクセスする場合、その変数をメモリに置くよりもレジスタに置いた方が、アクセス速度が格段に向上します。レジスタ割り当ては、どの変数をどのレジスタに割り当てるかを決定するプロセスです。

Goの複合型の内部表現

Go言語の複合型は、内部的には以下のように表現されます。

  • インターフェース (interface{}):

    • itab (interface table): 型情報とメソッドセットへのポインタ。
    • data (data pointer): 実際の値へのポインタ。 これら2つのポインタで構成されます。
  • スライス ([]T):

    • array (data pointer): 基底配列へのポインタ。
    • len (length): スライスの現在の長さ。
    • cap (capacity): 基底配列の容量。 これら3つの要素で構成されます。
  • 文字列 (string):

    • array (data pointer): 文字列のバイトデータへのポインタ。
    • len (length): 文字列の長さ(バイト数)。 これら2つの要素で構成されます。

これらの複合型は、複数の「コンポーネント」から構成されており、それぞれが独立した値として扱われる可能性があります。componentgenは、これらのコンポーネントを個別にレジスタに割り当てることで、より効率的なコード生成を目指します。

技術的詳細

このコミットの主要な変更点は、src/cmd/5g/cgen.ccomponentgen関数を導入し、既存のコード生成ロジックを修正して、特定の複合型(インターフェース、スライス、文字列)の処理においてこの新しい関数を利用するようにしたことです。

componentgenの役割

componentgen関数は、構造体、インターフェース、スライス、文字列といった複合型のコピーやゼロクリア(初期化)を、そのコンポーネント(内部のフィールドやポインタ)ごとに処理することを可能にします。これにより、コンパイラは複合型全体を一度にメモリ間でコピーするのではなく、個々のコンポーネントをレジスタを介して効率的に移動させたり、ゼロで埋めたりすることができます。

具体的には、componentgenは以下のケースで呼び出されます。

  • sgen関数(構造体のコピーを生成する関数)内で、サイズが8バイトまたは12バイトの構造体(これはGoのインターフェース、スライス、文字列の典型的なサイズに相当します)をコピーする際に呼び出されます。
  • clearfat関数(大きな構造体をゼロクリアする関数)内で、サイズが8バイトまたは12バイトの構造体をゼロクリアする際に呼び出されます。

componentgenは、引数としてコピー元(nr)とコピー先(nl)のNodeを受け取ります。ゼロクリアの場合はnrN(nil)になります。関数内部では、nlの型(TARRAYTSTRINGTINTER)に基づいて、それぞれのコンポーネント(Array_arrayArray_nelArray_capなどのオフセットでアクセスされる内部ポインタや長さ情報)を個別に処理します。

cgen.cの変更点

cgen.cは、GoのAST(抽象構文木)ノードをアセンブリコードに変換する主要なファイルです。このコミットでは、特にOLENlen組み込み関数)、OCAPcap組み込み関数)、OITAB(インターフェースの型情報テーブルへのアクセス)の処理が変更されています。

変更前は、これらの操作で一時的なレジスタを複数割り当てていましたが、変更後はigen関数(アドレス生成)とgmove関数(汎用的なデータ移動)をより直接的に使用し、不要なレジスタ割り当てを削減しています。これは、componentgenの導入と合わせて、レジスタの利用効率を高めるための変更です。

例えば、OLENOCAPでスライスや文字列の長さを取得する際、以前はregallocで一時レジスタを確保し、gmoveでデータを移動していましたが、変更後はigenで直接アドレスを生成し、そのアドレスからgmoveres(結果を格納するノード)に直接データを移動するように簡略化されています。これにより、中間的なレジスタの利用が減り、コード生成がより効率的になります。

また、igen関数にONAMEOINDREGのケースが追加され、レジスタの参照カウント管理が改善されています。これにより、igenの呼び出し元がregfreeを呼び出す必要がある場合に、レジスタのライフサイクルが正しく管理されるようになります。

ggen.cの変更点

ggen.cは、汎用的なコード生成ルーチンを含むファイルです。clearfat関数にcomponentgenの呼び出しが追加されました。これにより、サイズが8バイトまたは12バイトの複合型をゼロクリアする際に、componentgenが利用され、コンポーネントごとのゼロクリアが可能になります。

gsubr.cの変更点

gsubr.cは、サブルーチンやユーティリティ関数を含むファイルです。

  • resvd配列にREGSP(スタックポインタレジスタ)が追加されました。これは、スタックポインタが予約済みレジスタとして扱われ、誤って割り当てられないようにするためです。
  • regfree関数(レジスタを解放する関数)のロジックが修正され、REGSPが解放対象から除外されるようになりました。また、reg[i] <= 0の場合のfatalエラーメッセージがより詳細になりました。

reg.cの変更点

reg.cは、レジスタ割り当てに関連する関数を含むファイルです。

  • mkvar関数(変数をレジスタに割り当てる準備をする関数)から、TARRAYTSTRINGgoto noneの対象から除外されました。これは、これらの型がcomponentgenによってコンポーネントレベルで処理されるようになったため、以前のようにレジスタ割り当ての対象外とする必要がなくなったことを示唆しています。

これらの変更は全体として、Goの5gコンパイラが複合型をより細かく、より効率的に処理できるようにするための重要なステップであり、ARMアーキテクチャ上でのGoプログラムのパフォーマンス向上に貢献します。

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

src/cmd/5g/cgen.cにおけるOITABOLENOCAPの変更

--- a/src/cmd/5g/cgen.c
+++ b/src/cmd/5g/cgen.c
@@ -307,17 +307,11 @@ cgen(Node *n, Node *res)
 		break;
 
 	case OITAB:
-		// itable of interface value
+		// interface table is first word of interface value
 		igen(nl, &n1, res);
-		n1.op = OREGISTER;	// was OINDREG
-		regalloc(&n2, n->type, &n1);
-		n1.op = OINDREG;
 		n1.type = n->type;
-		n1.xoffset += 0;
-		gmove(&n1, &n2);
-		gmove(&n2, res);
+		gmove(&n1, res);
 		regfree(&n1);
-		regfree(&n2);
 		break;
 
 	case OLEN:
@@ -345,15 +339,10 @@ cgen(Node *n, Node *res)
 		if(istype(nl->type, TSTRING) || isslice(nl->type)) {
 			// both slice and string have len one pointer into the struct.
 			igen(nl, &n1, res);
-			n1.op = OREGISTER;	// was OINDREG
-			regalloc(&n2, types[TUINT32], &n1);
-			n1.op = OINDREG;
 			n1.type = types[TUINT32];
 			n1.xoffset += Array_nel;
-			gmove(&n1, &n2);
-			gmove(&n2, res);
+			gmove(&n1, res);
 			regfree(&n1);
-			regfree(&n2);
 			break;
 		}
 		fatal("cgen: OLEN: unknown type %lT", nl->type);
@@ -383,11 +372,9 @@ cgen(Node *n, Node *res)
 			break;
 		}
 		if(isslice(nl->type)) {
-			regalloc(&n1, types[tptr], res);
-			agen(nl, &n1);
-			n1.op = OINDREG;
+			igen(nl, &n1, res);
 			n1.type = types[TUINT32];
-			n1.xoffset = Array_cap;
+			n1.xoffset += Array_cap;
 			gmove(&n1, res);
 			regfree(&n1);
 			break;

src/cmd/5g/cgen.cにおけるigenの変更

--- a/src/cmd/5g/cgen.c
+++ b/src/cmd/5g/cgen.c
@@ -898,6 +885,20 @@ igen(Node *n, Node *a, Node *res)
 		dump("\nigen-n", n);
 	}
 	switch(n->op) {
+	case ONAME:
+		if((n->class&PHEAP) || n->class == PPARAMREF)
+			break;
+		*a = *n;
+		return;
+
+	case OINDREG:
+		// Increase the refcount of the register so that igen's caller
+		// has to call regfree.
+		if(n->val.u.reg != REGSP)
+			reg[n->val.u.reg]++;
+		*a = *n;
+		return;
+
 	case ODOT:
 		igen(n->left, a, res);
 		a->xoffset += n->xoffset;

src/cmd/5g/cgen.cにおけるcomponentgenの追加

--- a/src/cmd/5g/cgen.c
+++ b/src/cmd/5g/cgen.c
@@ -1399,6 +1356,10 @@ sgen(Node *n, Node *res, int64 w)
 		return;
 	}
 
+	if(w == 8 || w == 12)
+		if(componentgen(n, res))
+			return;
+	
 	// determine alignment.
 	// want to avoid unaligned access, so have to use
 	// smaller operations for less aligned types.
@@ -1516,3 +1477,156 @@ sgen(Node *n, Node *res, int64 w)
 	regfree(&src);
 	regfree(&tmp);
 }
+
+static int
+cadable(Node *n)
+{
+	if(!n->addable) {
+		// dont know how it happens,
+		// but it does
+		return 0;
+	}
+
+	switch(n->op) {
+	case ONAME:
+		return 1;
+	}
+	return 0;
+}
+
+/*
+ * copy a structure component by component
+ * return 1 if can do, 0 if cant.
+ * nr is N for copy zero
+ */
+int
+componentgen(Node *nr, Node *nl)
+{
+	Node nodl, nodr, tmp;
+	int freel, freer;
+
+	freel = 0;
+	freer = 0;
+
+	switch(nl->type->etype) {
+	default:
+		goto no;
+
+	case TARRAY:
+		if(!isslice(nl->type))
+			goto no;
+	case TSTRING:
+	case TINTER:
+		break;
+	}
+
+	nodl = *nl;
+	if(!cadable(nl)) {
+		if(nr == N || !cadable(nr))
+			goto no;
+		igen(nl, &nodl, N);
+		freel = 1;
+	}
+
+	if(nr != N) {
+		nodr = *nr;
+		if(!cadable(nr)) {
+			igen(nr, &nodr, N);
+			freer = 1;
+		}
+	} else {
+		// When zeroing, prepare a register containing zero.
+		nodconst(&tmp, nl->type, 0);
+		regalloc(&nodr, types[TUINT], N);
+		gmove(&tmp, &nodr);
+		freer = 1;
+	}
+
+	switch(nl->type->etype) {
+	case TARRAY:
+		nodl.xoffset += Array_array;
+		nodl.type = ptrto(nl->type->type);
+
+		if(nr != N) {
+			nodr.xoffset += Array_array;
+			nodr.type = nodl.type;
+		}
+		gmove(&nodr, &nodl);
+
+		nodl.xoffset += Array_nel-Array_array;
+		nodl.type = types[simtype[TUINT]];
+
+		if(nr != N) {
+			nodr.xoffset += Array_nel-Array_array;
+			nodr.type = nodl.type;
+		}
+		gmove(&nodr, &nodl);
+
+		nodl.xoffset += Array_cap-Array_nel;
+		nodl.type = types[simtype[TUINT]];
+
+		if(nr != N) {
+			nodr.xoffset += Array_cap-Array_nel;
+			nodr.type = nodl.type;
+		}
+		gmove(&nodr, &nodl);
+
+		goto yes;
+
+	case TSTRING:
+		nodl.xoffset += Array_array;
+		nodl.type = ptrto(types[TUINT8]);
+
+		if(nr != N) {
+			nodr.xoffset += Array_array;
+			nodr.type = nodl.type;
+		}
+		gmove(&nodr, &nodl);
+
+		nodl.xoffset += Array_nel-Array_array;
+		nodl.type = types[simtype[TUINT]];
+
+		if(nr != N) {
+			nodr.xoffset += Array_nel-Array_array;
+			nodr.type = nodl.type;
+		}
+		gmove(&nodr, &nodl);
+
+		goto yes;
+
+	case TINTER:
+		nodl.xoffset += Array_array;
+		nodl.type = ptrto(types[TUINT8]);
+
+		if(nr != N) {
+			nodr.xoffset += Array_array;
+			nodr.type = nodl.type;
+		}
+		gmove(&nodr, &nodl);
+
+		nodl.xoffset += Array_nel-Array_array;
+		nodl.type = ptrto(types[TUINT8]);
+
+		if(nr != N) {
+			nodr.xoffset += Array_nel-Array_array;
+			nodr.type = nodl.type;
+		}
+		gmove(&nodr, &nodl);
+
+		goto yes;
+	}
+
+no:
+	if(freer)
+		regfree(&nodr);
+	if(freel)
+		regfree(&nodl);
+	return 0;
+
+yes:
+	if(freer)
+		regfree(&nodr);
+	if(freel)
+		regfree(&nodl);
+	return 1;
+}

src/cmd/5g/ggen.cにおけるclearfatの変更

--- a/src/cmd/5g/ggen.c
+++ b/src/cmd/5g/ggen.c
@@ -616,7 +616,12 @@ clearfat(Node *nl)
 	if(debug['g'])
 		dump("\nclearfat", nl);
 
+
 	w = nl->type->width;
+	if(w == 8 || w == 12)
+		if(componentgen(N, nl))
+			return;
+
 	c = w % 4;	// bytes
 	q = w / 4;	// quads

src/cmd/5g/gsubr.cにおけるresvdregfreeの変更

--- a/src/cmd/5g/gsubr.c
+++ b/src/cmd/5g/gsubr.c
@@ -255,8 +255,9 @@ afunclit(Addr *a)
 
 static	int	resvd[] =
 {
-	9,	// reserved for m
-	10,	// reserved for g
+	9,     // reserved for m
+	10,    // reserved for g
+	REGSP, // reserved for SP
 };
 
 void
@@ -400,15 +401,17 @@ regfree(Node *n)
 		print("regalloc fix %d float %d\n", fixfree, floatfree);
 	}
 
-	if(n->op == ONAME && iscomplex[n->type->etype])
+	if(n->op == ONAME)
 		return;
 	if(n->op != OREGISTER && n->op != OINDREG)
 		fatal("regfree: not a register");
 	i = n->val.u.reg;
+	if(i == REGSP)
+		return;
 	if(i < 0 || i >= nelem(reg) || i >= nelem(regpc))
 		fatal("regfree: reg out of range");
 	if(reg[i] <= 0)
-		fatal("regfree: reg not allocated");
+		fatal("regfree: reg %R not allocated", i);
 	reg[i]--;
 	if(reg[i] == 0)
 		regpc[i] = 0;

src/cmd/5g/reg.cにおけるmkvarの変更

--- a/src/cmd/5g/reg.c
+++ b/src/cmd/5g/reg.c
@@ -987,8 +987,6 @@ mkvar(Reg *r, Adr *a)
 	switch(et) {
 	case 0:
 	case TFUNC:
-#tcase TARRAY:
-#tcase TSTRING:
 		goto none;
 	}
 

コアとなるコードの解説

cgen.cの変更

  • OITAB, OLEN, OCAPの最適化: 変更前は、インターフェースのテーブルポインタ、スライスや文字列の長さ・容量を取得する際に、regallocで一時レジスタn2を割り当て、gmoveを2回呼び出してデータを移動していました。 変更後は、igenで直接目的のアドレス(n1)を生成し、gmove(&n1, res)で直接結果レジスタresにデータを移動するように簡略化されています。これにより、中間的なレジスタn2の割り当てと解放が不要になり、コード生成がより効率的になります。これは、レジスタの利用を最小限に抑え、より直接的なデータフローを可能にするための典型的な最適化です。

  • igen関数の拡張: igen関数は、ノードのアドレスを生成する役割を担います。

    • ONAMEケースが追加され、ヒープに割り当てられていない、またはパラメータ参照ではない名前付き変数の場合、そのノード自体をアドレスとして使用できるようになりました。
    • OINDREGケースが追加され、レジスタ間接参照の場合に、そのレジスタの参照カウントをインクリメントするようになりました。これにより、igenの呼び出し元がregfreeを呼び出す必要がある場合に、レジスタが誤って解放されることを防ぎ、レジスタのライフサイクル管理が改善されます。
  • componentgen関数の導入: sgen関数内で、コピー対象の幅が8バイトまたは12バイトの場合にcomponentgenが呼び出されるようになりました。これは、Goのインターフェース(2ポインタ = 8バイト)やスライス(3要素 = 12バイト)のサイズに合致します。 componentgenは、複合型をそのコンポーネントごとにコピーまたはゼロクリアするロジックを実装しています。

    • cadableヘルパー関数は、ノードがアドレス可能(レジスタに直接ロードできる)かどうかをチェックします。
    • componentgenは、コピー元とコピー先のノードが直接アドレス可能でない場合、igenを呼び出してアドレスを生成し、一時レジスタにロードします。
    • TARRAY(スライス)、TSTRINGTINTERの各ケースで、それぞれの内部構造(Array_arrayArray_nelArray_capなどのオフセット)にアクセスし、コンポーネントごとにgmoveを使ってデータを移動します。ゼロクリアの場合は、ゼロを含むレジスタを準備し、それをコピー元として使用します。 この関数により、コンパイラは複合型全体を不透明なバイトの塊として扱うのではなく、その内部構造を理解し、個々のコンポーネントを効率的に操作できるようになります。

ggen.cの変更

  • clearfatでのcomponentgenの利用: clearfat関数は、大きな構造体をゼロクリアする際に使用されます。この関数も、幅が8バイトまたは12バイトの複合型をゼロクリアする際にcomponentgen(N, nl)を呼び出すようになりました。Nはコピー元がないことを示し、componentgenがゼロクリアモードで動作することを意味します。これにより、複合型のゼロクリアもコンポーネントレベルで効率的に行われるようになります。

gsubr.cの変更

  • 予約済みレジスタの追加とregfreeの修正: resvd配列にREGSP(スタックポインタレジスタ)が追加されました。これは、スタックポインタがコンパイラによって予約され、他の用途に誤って割り当てられないようにするためです。 regfree関数は、レジスタを解放する際に、REGSPを解放対象から除外するようになりました。また、レジスタがすでに解放されている場合に発生するエラーメッセージが改善され、どのレジスタが問題であるかを特定しやすくなりました。

reg.cの変更

  • mkvarの修正: mkvar関数は、変数をレジスタに割り当てる準備をする関数です。以前はTARRAYTSTRING型の変数をレジスタ割り当ての対象外としていましたが、componentgenの導入により、これらの複合型もコンポーネントレベルでレジスタ割り当ての恩恵を受けられるようになったため、この制限が解除されました。これにより、コンパイラはこれらの型に対してもより積極的なレジスタ割り当てを試みることができるようになります。

これらの変更は全体として、Goの5gコンパイラが複合型データをより細かく、より効率的に処理できるようにするための重要なステップであり、ARMアーキテクチャ上でのGoプログラムのパフォーマンス向上に貢献します。

関連リンク

  • Go言語のコンパイラに関する公式ドキュメント(当時のものに近い情報源を見つけるのは難しい可能性がありますが、Goのコンパイラ設計に関する一般的な情報は役立ちます)
  • Goのインターフェース、スライス、文字列の内部表現に関する記事やドキュメント
  • コンパイラのレジスタ割り当てに関する一般的な情報

参考にした情報源リンク

  • Go言語のコンパイラに関する一般的な知識
  • コンパイラ最適化(特にレジスタ割り当て)に関する一般的な知識
  • Go言語のインターフェース、スライス、文字列の内部構造に関する情報
  • Gitのコミットログと差分分析
  • Goのソースコード(src/cmd/5g/ディレクトリ内の関連ファイル)