[インデックス 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.c
にcomponentgen
関数を導入し、既存のコード生成ロジックを修正して、特定の複合型(インターフェース、スライス、文字列)の処理においてこの新しい関数を利用するようにしたことです。
componentgen
の役割
componentgen
関数は、構造体、インターフェース、スライス、文字列といった複合型のコピーやゼロクリア(初期化)を、そのコンポーネント(内部のフィールドやポインタ)ごとに処理することを可能にします。これにより、コンパイラは複合型全体を一度にメモリ間でコピーするのではなく、個々のコンポーネントをレジスタを介して効率的に移動させたり、ゼロで埋めたりすることができます。
具体的には、componentgen
は以下のケースで呼び出されます。
sgen
関数(構造体のコピーを生成する関数)内で、サイズが8バイトまたは12バイトの構造体(これはGoのインターフェース、スライス、文字列の典型的なサイズに相当します)をコピーする際に呼び出されます。clearfat
関数(大きな構造体をゼロクリアする関数)内で、サイズが8バイトまたは12バイトの構造体をゼロクリアする際に呼び出されます。
componentgen
は、引数としてコピー元(nr
)とコピー先(nl
)のNode
を受け取ります。ゼロクリアの場合はnr
がN
(nil)になります。関数内部では、nl
の型(TARRAY
、TSTRING
、TINTER
)に基づいて、それぞれのコンポーネント(Array_array
、Array_nel
、Array_cap
などのオフセットでアクセスされる内部ポインタや長さ情報)を個別に処理します。
cgen.c
の変更点
cgen.c
は、GoのAST(抽象構文木)ノードをアセンブリコードに変換する主要なファイルです。このコミットでは、特にOLEN
(len
組み込み関数)、OCAP
(cap
組み込み関数)、OITAB
(インターフェースの型情報テーブルへのアクセス)の処理が変更されています。
変更前は、これらの操作で一時的なレジスタを複数割り当てていましたが、変更後はigen
関数(アドレス生成)とgmove
関数(汎用的なデータ移動)をより直接的に使用し、不要なレジスタ割り当てを削減しています。これは、componentgen
の導入と合わせて、レジスタの利用効率を高めるための変更です。
例えば、OLEN
やOCAP
でスライスや文字列の長さを取得する際、以前はregalloc
で一時レジスタを確保し、gmove
でデータを移動していましたが、変更後はigen
で直接アドレスを生成し、そのアドレスからgmove
でres
(結果を格納するノード)に直接データを移動するように簡略化されています。これにより、中間的なレジスタの利用が減り、コード生成がより効率的になります。
また、igen
関数にONAME
とOINDREG
のケースが追加され、レジスタの参照カウント管理が改善されています。これにより、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
関数(変数をレジスタに割り当てる準備をする関数)から、TARRAY
とTSTRING
がgoto none
の対象から除外されました。これは、これらの型がcomponentgen
によってコンポーネントレベルで処理されるようになったため、以前のようにレジスタ割り当ての対象外とする必要がなくなったことを示唆しています。
これらの変更は、5g
コンパイラが複合型をより効率的に扱い、レジスタをより効果的に利用できるようにするための包括的な取り組みの一部です。
コアとなるコードの変更箇所
src/cmd/5g/cgen.c
におけるOITAB
、OLEN
、OCAP
の変更
--- 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
におけるresvd
とregfree
の変更
--- 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
(スライス)、TSTRING
、TINTER
の各ケースで、それぞれの内部構造(Array_array
、Array_nel
、Array_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
関数は、変数をレジスタに割り当てる準備をする関数です。以前はTARRAY
とTSTRING
型の変数をレジスタ割り当ての対象外としていましたが、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.c
にcomponentgen
関数を導入し、既存のコード生成ロジックを修正して、特定の複合型(インターフェース、スライス、文字列)の処理においてこの新しい関数を利用するようにしたことです。
componentgen
の役割
componentgen
関数は、構造体、インターフェース、スライス、文字列といった複合型のコピーやゼロクリア(初期化)を、そのコンポーネント(内部のフィールドやポインタ)ごとに処理することを可能にします。これにより、コンパイラは複合型全体を一度にメモリ間でコピーするのではなく、個々のコンポーネントをレジスタを介して効率的に移動させたり、ゼロで埋めたりすることができます。
具体的には、componentgen
は以下のケースで呼び出されます。
sgen
関数(構造体のコピーを生成する関数)内で、サイズが8バイトまたは12バイトの構造体(これはGoのインターフェース、スライス、文字列の典型的なサイズに相当します)をコピーする際に呼び出されます。clearfat
関数(大きな構造体をゼロクリアする関数)内で、サイズが8バイトまたは12バイトの構造体をゼロクリアする際に呼び出されます。
componentgen
は、引数としてコピー元(nr
)とコピー先(nl
)のNode
を受け取ります。ゼロクリアの場合はnr
がN
(nil)になります。関数内部では、nl
の型(TARRAY
、TSTRING
、TINTER
)に基づいて、それぞれのコンポーネント(Array_array
、Array_nel
、Array_cap
などのオフセットでアクセスされる内部ポインタや長さ情報)を個別に処理します。
cgen.c
の変更点
cgen.c
は、GoのAST(抽象構文木)ノードをアセンブリコードに変換する主要なファイルです。このコミットでは、特にOLEN
(len
組み込み関数)、OCAP
(cap
組み込み関数)、OITAB
(インターフェースの型情報テーブルへのアクセス)の処理が変更されています。
変更前は、これらの操作で一時的なレジスタを複数割り当てていましたが、変更後はigen
関数(アドレス生成)とgmove
関数(汎用的なデータ移動)をより直接的に使用し、不要なレジスタ割り当てを削減しています。これは、componentgen
の導入と合わせて、レジスタの利用効率を高めるための変更です。
例えば、OLEN
やOCAP
でスライスや文字列の長さを取得する際、以前はregalloc
で一時レジスタを確保し、gmove
でデータを移動していましたが、変更後はigen
で直接アドレスを生成し、そのアドレスからgmove
でres
(結果を格納するノード)に直接データを移動するように簡略化されています。これにより、中間的なレジスタの利用が減り、コード生成がより効率的になります。
また、igen
関数にONAME
とOINDREG
のケースが追加され、レジスタの参照カウント管理が改善されています。これにより、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
関数(変数をレジスタに割り当てる準備をする関数)から、TARRAY
とTSTRING
がgoto none
の対象から除外されました。これは、これらの型がcomponentgen
によってコンポーネントレベルで処理されるようになったため、以前のようにレジスタ割り当ての対象外とする必要がなくなったことを示唆しています。
これらの変更は全体として、Goの5g
コンパイラが複合型をより細かく、より効率的に処理できるようにするための重要なステップであり、ARMアーキテクチャ上でのGoプログラムのパフォーマンス向上に貢献します。
コアとなるコードの変更箇所
src/cmd/5g/cgen.c
におけるOITAB
、OLEN
、OCAP
の変更
--- 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
におけるresvd
とregfree
の変更
--- 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
(スライス)、TSTRING
、TINTER
の各ケースで、それぞれの内部構造(Array_array
、Array_nel
、Array_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
関数は、変数をレジスタに割り当てる準備をする関数です。以前はTARRAY
とTSTRING
型の変数をレジスタ割り当ての対象外としていましたが、componentgen
の導入により、これらの複合型もコンポーネントレベルでレジスタ割り当ての恩恵を受けられるようになったため、この制限が解除されました。これにより、コンパイラはこれらの型に対してもより積極的なレジスタ割り当てを試みることができるようになります。
これらの変更は全体として、Goの5g
コンパイラが複合型データをより細かく、より効率的に処理できるようにするための重要なステップであり、ARMアーキテクチャ上でのGoプログラムのパフォーマンス向上に貢献します。
関連リンク
- Go言語のコンパイラに関する公式ドキュメント(当時のものに近い情報源を見つけるのは難しい可能性がありますが、Goのコンパイラ設計に関する一般的な情報は役立ちます)
- Goのインターフェース、スライス、文字列の内部表現に関する記事やドキュメント
- コンパイラのレジスタ割り当てに関する一般的な情報
参考にした情報源リンク
- Go言語のコンパイラに関する一般的な知識
- コンパイラ最適化(特にレジスタ割り当て)に関する一般的な知識
- Go言語のインターフェース、スライス、文字列の内部構造に関する情報
- Gitのコミットログと差分分析
- Goのソースコード(
src/cmd/5g/
ディレクトリ内の関連ファイル)