[インデックス 17157] ファイルの概要
このコミットは、Goコンパイラのx86 32-bitアーキテクチャ向けバックエンドであるcmd/8g
において、アセンブラ命令に関する情報を一元管理するための大規模なリファクタリングを導入しています。具体的には、各命令の特性(レジスタの使用状況、キャリーフラグへの影響など)を記述するProgInfo
構造体と、その情報を取得するproginfo
関数を導入し、命令のプロパティに依存する最適化ロジックからこれらの情報を切り離しました。これにより、コードの重複が排除され、保守性と拡張性が向上しています。
変更されたファイルは以下の通りです。
src/cmd/6g/prog.c
: 既存の6g
コンパイラ(x86 64-bit)における命令情報定義の微修正。src/cmd/8g/opt.h
:8g
コンパイラにおける命令情報の新しい構造体ProgInfo
と関連フラグの定義。src/cmd/8g/peep.c
:8g
コンパイラのpeephole最適化パスにおいて、命令プロパティの取得方法を新しいproginfo
関数に移行。src/cmd/8g/prog.c
:8g
コンパイラ向けに、命令ごとの詳細なプロパティを定義したprogtable
と、それを参照するproginfo
関数を新規作成。src/cmd/8g/reg.c
:8g
コンパイラのレジスタ割り当てパスにおいて、命令プロパティの取得方法を新しいproginfo
関数に移行。
コミット
cmd/8g: factor out prog information
Like CL 12637051, but for 8g instead of 6g.
Fix a few minor 6g errors too.
R=ken2
CC=golang-dev
https://golang.org/cl/12778043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/ac0df6ce895c14216d5f57cc749e7080815860f9
元コミット内容
このコミットは、Goコンパイラのx86 32-bitアーキテクチャ向けバックエンドであるcmd/8g
において、アセンブラ命令に関する情報を一元化するリファクタリングを実施しています。
コミットメッセージによると、この変更は以前に6g
(x86 64-bitアーキテクチャ向けコンパイラ)で行われた同様の変更(CL 12637051)を8g
に適用するものです。これは、コンパイラの異なるバックエンド間で共通の設計パターンを適用し、コードの整合性を高める意図があることを示唆しています。
また、6g
におけるいくつかの小さなエラー修正も含まれており、これはおそらく、命令情報の共通化に伴う全体的なコード品質の向上の一環として行われたものと考えられます。
このリファクタリングの主な目的は、命令のプロパティ(例:どのレジスタが読み書きされるか、キャリーフラグが設定されるかなど)を、命令を処理する各最適化パス(peephole最適化やレジスタ割り当てなど)から切り離し、一元的なデータ構造と関数で管理することにあります。これにより、命令の特性に関するロジックが重複することなく、よりクリーンで保守しやすいコードベースが実現されます。
変更の背景
Goコンパイラのバックエンドは、特定のCPUアーキテクチャ向けにアセンブラ命令を生成し、最適化を行います。この最適化プロセスでは、各アセンブラ命令がレジスタやフラグにどのような影響を与えるかを正確に把握する必要があります。例えば、ある命令が特定のレジスタを読み込むのか、書き込むのか、あるいは両方を行うのか、また、キャリーフラグなどのステータスフラグを設定するのか、使用するのか、破壊するのかといった情報です。
従来のGoコンパイラでは、これらの命令に関するプロパティ情報が、peephole最適化(peep.c
)やレジスタ割り当て(reg.c
)といった複数の最適化パスのコード内に散在していました。これは、新しい命令が追加されたり、既存の命令の特性が変更されたりするたびに、複数の場所でコードを修正する必要があることを意味し、コードの重複、保守性の低下、そしてバグの温床となる可能性がありました。
この問題に対処するため、Goチームは命令のプロパティ情報を一元化するアプローチを採用しました。このコミットは、そのアプローチを8g
コンパイラ(x86 32-bit)に適用するものです。コミットメッセージにある「Like CL 12637051, but for 8g instead of 6g」という記述は、既に6g
コンパイラ(x86 64-bit)で同様のリファクタリングが成功裏に実施されており、その設計パターンを8g
にも展開していることを明確に示しています。これにより、コンパイラ全体のコードベースの一貫性と品質が向上します。
前提知識の解説
このコミットを理解するためには、以下の概念について基本的な知識があると役立ちます。
Goコンパイラのバックエンド (cmd/8g
, cmd/6g
)
Goコンパイラは、ソースコードを中間表現に変換した後、各ターゲットアーキテクチャ(例: x86, ARM)向けのアセンブラコードを生成します。このアセンブラコードの生成と最適化を担当するのがバックエンドです。
cmd/8g
: x86 32-bitアーキテクチャ向けのコンパイラバックエンドです。cmd/6g
: x86 64-bitアーキテクチャ向けのコンパイラバックエンドです。 これらのバックエンドは、それぞれのアセンブラ命令セットとレジスタセットに特化した最適化を行います。
アセンブラ命令の最適化
コンパイラは、生成されたアセンブラコードのパフォーマンスを向上させるために様々な最適化を行います。これには、不要な命令の削除、命令の並べ替え、レジスタの効率的な使用などが含まれます。これらの最適化を行う際、コンパイラは各アセンブラ命令がシステムの状態(特にレジスタやCPUフラグ)にどのような影響を与えるかを正確に知る必要があります。
Prog
構造体
Goコンパイラのバックエンドでは、アセンブラ命令は内部的にProg
という構造体で表現されます。この構造体には、命令の種類(例: AMOVL
、AADDL
)、オペランド(ソースとデスティネーション)、そして次の命令へのリンクなどが含まれます。
ProgInfo
構造体とフラグ
このコミットで導入されたProgInfo
構造体は、個々のアセンブラ命令の静的な特性を記述するためのものです。この構造体には、命令の動作を詳細に表現するビットフラグの集合が含まれています。これらのフラグは、命令がレジスタをどのように使用するか、キャリーフラグなどのCPUフラグにどのように影響するか、命令の種類(移動、変換、条件分岐など)を示すために使用されます。
主要なフラグとその意味は以下の通りです。
Pseudo
: 擬似命令。実際のCPU命令ではなく、コンパイラ内部でのみ意味を持つ命令(例:ATEXT
,AGLOBL
)。OK
: 特に特筆すべき情報はないが、有効な命令。SizeB
,SizeW
,SizeL
,SizeQ
,SizeF
,SizeD
: 命令が操作するデータのサイズ(バイト、ワード、ロング、クアッド、単精度浮動小数点、倍精度浮動小数点)。LeftAddr
,LeftRead
,LeftWrite
: 命令の左オペランド(通常はソース)がアドレスとして使われるか、読み込まれるか、書き込まれるか。RightAddr
,RightRead
,RightWrite
: 命令の右オペランド(通常はデスティネーション)がアドレスとして使われるか、読み込まれるか、書き込まれるか。LeftRdwr
(LeftRead | LeftWrite
)、RightRdwr
(RightRead | RightWrite
) のように、読み書き両方を示す複合フラグも定義されています。
SetCarry
,UseCarry
,KillCarry
: 命令がキャリーフラグを設定するか、使用するか、またはその後のキャリーフラグの状態を無効にする(破壊する)か。Move
: データ移動命令(例:MOV
)。Conv
: データ型変換命令(例:CVT
)。Cjmp
: 条件付きジャンプ命令。Break
: 制御フローを中断する命令(例:RET
,JMP
)。Call
: 関数呼び出し命令。Jump
: 無条件ジャンプ命令。Skip
: 最適化パスでスキップされるべき命令(例:Pseudo
命令の一部)。ShiftCX
: シフト命令などで、CX
レジスタがシフト量として使用される可能性があることを示す。ImulAXDX
:IMUL
命令などで、AX
とDX
レジスタが暗黙的に使用されることを示す。
peep.c
(Peephole Optimizer)
peephole最適化は、コンパイラが生成したアセンブラコードの小さなシーケンス(「peephole」と呼ばれる)を検査し、より効率的な同等のシーケンスに置き換える最適化手法です。例えば、MOV AX, BX; MOV BX, AX
のような冗長な命令シーケンスを削除したり、より短い命令に置き換えたりします。この最適化は、命令のレジスタ使用状況やフラグへの影響を詳細に知る必要があります。
reg.c
(Register Allocator)
レジスタ割り当ては、プログラムの変数をCPUのレジスタに割り当てるプロセスです。レジスタはCPUが直接アクセスできる高速なメモリであり、レジスタを効率的に使用することでプログラムの実行速度が大幅に向上します。レジスタ割り当て器は、各命令がどのレジスタを読み込み、どのレジスタに書き込むかを把握し、レジスタの競合を避けつつ最適な割り当てを行います。
技術的詳細
このコミットの核心は、Goコンパイラの8g
バックエンドにおけるアセンブラ命令のプロパティ管理を、分散型から集中型へと移行した点にあります。
-
ProgInfo
構造体の定義 (src/cmd/8g/opt.h
):- このヘッダーファイルに、新しい
ProgInfo
構造体が定義されました。この構造体は、命令の特性を表現するためのビットフラグflags
、命令が使用する必須レジスタreguse
、命令が設定する必須レジスタregset
、そしてアドレッシングモードで使用されるレジスタregindex
を含みます。 - また、命令の様々な特性を示すためのビットフラグ(
Pseudo
,SizeB
,LeftRead
,RightWrite
,SetCarry
,UseCarry
,Move
,Call
など)がenum
として定義されました。これらのフラグは、命令の動作を抽象化し、最適化パスが命令の種類に依存することなく、その特性に基づいて処理を行えるようにします。
- このヘッダーファイルに、新しい
-
prog.c
の新規作成とprogtable
の導入 (src/cmd/8g/prog.c
):- このコミットで新しく作成された
src/cmd/8g/prog.c
ファイルは、8g
コンパイラがサポートするすべてのアセンブラ命令(ALAST
までの範囲)について、そのProgInfo
を定義した静的なテーブルprogtable
を含んでいます。 progtable
は、各命令コード(例:AADCL
,AMOVL
,ACALL
)に対応するProgInfo
エントリを持ち、その命令のサイズ、オペランドの読み書き特性、キャリーフラグへの影響、命令の種類(移動、呼び出し、分岐など)をビットフラグで詳細に記述しています。- このファイルには、
proginfo(ProgInfo *info, Prog *p)
関数も実装されています。この関数は、与えられたProg
(アセンブラ命令)のオペコード(p->as
)に基づいてprogtable
から対応するProgInfo
エントリを取得し、さらに特定の命令(例: シフト命令でのCX
レジスタの使用、IMUL
命令でのAX
/DX
レジスタの使用)に関する動的なレジスタ使用情報を追加で設定します。
- このコミットで新しく作成された
-
最適化パスの変更 (
src/cmd/8g/peep.c
,src/cmd/8g/reg.c
):peep.c
(peephole最適化)とreg.c
(レジスタ割り当て)のファイルでは、これまで命令の種類ごとに個別のswitch
文で処理されていた命令のプロパティに関するロジックが、新しく導入されたproginfo
関数とProgInfo
構造体のフラグチェックに置き換えられました。- 例えば、
peep.c
のneedc
関数では、以前はキャリーフラグを使用する命令を個別に列挙していましたが、変更後はproginfo
を呼び出し、返されたinfo.flags
にUseCarry
フラグが立っているかをチェックするだけでよくなりました。同様に、subprop
関数やcopyu
関数でも、命令のレジスタ使用や読み書き特性の判定にinfo.flags
が活用されています。 reg.c
のregopt
関数では、命令のuse1
(ソースオペランドのレジスタ使用)、set
(デスティネーションオペランドのレジスタ設定)などのビットマップを構築する際に、proginfo
から得られるinfo.reguse
、info.regset
、info.regindex
が直接利用されるようになりました。これにより、レジスタ割り当てロジックが大幅に簡素化され、命令ごとの特殊なケースを個別にハンドリングする必要がなくなりました。
-
6g
の修正 (src/cmd/6g/prog.c
):- このファイルでは、
ANOTW
命令のサイズがSizeQ
からSizeW
に修正され、APUSHQ
命令のRightWrite
フラグがLeftRead
に修正されるなど、いくつかのマイナーなバグ修正が行われています。これらは、おそらく6g
での同様のリファクタリング時に見落とされた、またはこの共通化の過程で発見された不整合の修正と考えられます。
- このファイルでは、
このリファクタリングにより、命令のプロパティ定義が一元化され、コンパイラのバックエンドコードの重複が大幅に削減されました。これにより、コードの保守性が向上し、将来的な命令セットの拡張や最適化ルールの変更が容易になります。また、命令の特性に関する単一の信頼できる情報源が提供されることで、コンパイラの正確性と堅牢性も向上します。
コアとなるコードの変更箇所
src/cmd/8g/opt.h
+typedef struct ProgInfo ProgInfo;
+struct ProgInfo
+{
+ uint32 flags; // the bits below
+ uint32 reguse; // required registers used by this instruction
+ uint32 regset; // required registers set by this instruction
+ uint32 regindex; // registers used by addressing mode
+};
+
+enum
+{
+ // Pseudo-op, like TEXT, GLOBL, TYPE, PCDATA, FUNCDATA.
+ Pseudo = 1<<1,
+
+ // There's nothing to say about the instruction,
+ // but it's still okay to see.
+ OK = 1<<2,
+
+ // Size of right-side write, or right-side read if no write.
+ SizeB = 1<<3,
+ SizeW = 1<<4,
+ SizeL = 1<<5,
+ SizeQ = 1<<6,
+ SizeF = 1<<7, // float aka float32
+ SizeD = 1<<8, // double aka float64
+
+ // Left side: address taken, read, write.
+ LeftAddr = 1<<9,
+ LeftRead = 1<<10,
+ LeftWrite = 1<<11,
+
+ // Right side: address taken, read, write.
+ RightAddr = 1<<12,
+ RightRead = 1<<13,
+ RightWrite = 1<<14,
+
+ // Set, use, or kill of carry bit.
+ // Kill means we never look at the carry bit after this kind of instruction.
+ SetCarry = 1<<15,
+ UseCarry = 1<<16,
+ KillCarry = 1<<17,
+
+ // Instruction kinds
+ Move = 1<<18, // straight move
+ Conv = 1<<19, // size conversion
+ Cjmp = 1<<20, // conditional jump
+ Break = 1<<21, // breaks control flow (no fallthrough)
+ Call = 1<<22, // function call
+ Jump = 1<<23, // jump
+ Skip = 1<<24, // data instruction
+
+ // Special cases for register use.
+ ShiftCX = 1<<25, // possible shift by CX
+ ImulAXDX = 1<<26, // possible multiply into DX:AX
+};
+
+void proginfo(ProgInfo*, Prog*);
src/cmd/8g/prog.c
(新規作成)
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
#include <u.h>
#include <libc.h>
#include "gg.h"
#include "opt.h"
// Matches real RtoB but can be used in global initializer.
#define RtoB(r) (1<<((r)-D_AX))
enum {
AX = RtoB(D_AX),
BX = RtoB(D_BX),
CX = RtoB(D_CX),
DX = RtoB(D_DX),
DI = RtoB(D_DI),
SI = RtoB(D_SI),
LeftRdwr = LeftRead | LeftWrite,
RightRdwr = RightRead | RightWrite,
};
#undef RtoB
// This table gives the basic information about instruction
// generated by the compiler and processed in the optimizer.
// See opt.h for bit definitions.
//
// Instructions not generated need not be listed.
// As an exception to that rule, we typically write down all the
// size variants of an operation even if we just use a subset.
//
// The table is formatted for 8-space tabs.
static ProgInfo progtable[ALAST] = {
[ATYPE]= {Pseudo | Skip},
[ATEXT]= {Pseudo},
[AFUNCDATA]= {Pseudo},
[APCDATA]= {Pseudo},
// NOP is an internal no-op that also stands
// for USED and SET annotations, not the Intel opcode.
[ANOP]= {LeftRead | RightWrite},
[AADCL]= {SizeL | LeftRead | RightRdwr | SetCarry | UseCarry},
[AADCW]= {SizeW | LeftRead | RightRdwr | SetCarry | UseCarry},
// ... (他の命令の定義が続く) ...
[AXORB]= {SizeB | LeftRead | RightRdwr | SetCarry},
[AXORL]= {SizeL | LeftRead | RightRdwr | SetCarry},
[AXORW]= {SizeW | LeftRead | RightRdwr | SetCarry},
};
void
proginfo(ProgInfo *info, Prog *p)
{
*info = progtable[p->as];
if(info->flags == 0)
fatal("unknown instruction %P", p);
if((info->flags & ShiftCX) && p->from.type != D_CONST)
info->reguse |= CX;
if(info->flags & ImulAXDX) {
if(p->to.type == D_NONE) {
info->reguse |= AX;
info->regset |= AX | DX;
} else {
info->flags |= RightRdwr;
}
}
// Addressing makes some registers used.
if(p->from.type >= D_INDIR)
info->regindex |= RtoB(p->from.type-D_INDIR);
if(p->from.index != D_NONE)
info->regindex |= RtoB(p->from.index);
if(p->to.type >= D_INDIR)
info->regindex |= RtoB(p->to.type-D_INDIR);
if(p->to.index != D_NONE)
info->regindex |= RtoB(p->to.index);
}
src/cmd/8g/peep.c
needc
関数の変更例:
static int
needc(Prog *p)
{
+ ProgInfo info;
+
while(p != P) {
- switch(p->as) {
- case AADCL:
- case ASBBL:
- case ARCRB:
- case ARCRW:
- case ARCRL:
+ proginfo(&info, p);
+ if(info.flags & UseCarry)
return 1;
- case AADDB:
- case AADDW:
- case AADDL:
- case ASUBB:
- case ASUBW:
- case ASUBL:
- case AJMP:
- case ARET:
- case ACALL:
+ if(info.flags & (SetCarry|KillCarry))
return 0;
- default:
- if(p->to.type == D_BRANCH)
- return 0;
- }
p = p->link;
}
return 0;
copyu
関数の変更例(大幅なswitch
文の削除とproginfo
利用への移行):
int
copyu(Prog *p, Adr *v, Adr *s)
{
+ ProgInfo info;
+
switch(p->as) {
-
- default:
- // ... 多数のcase文が削除 ...
-
case AJMP: /* funny */
if(s != A) {
if(copysub(&p->to, v, s, 1))
return 1;
return copysub(&p->from, v, s, 1);
}
if(copyau(&p->to, v))
return 1;
if(copyau(&p->from, v))
return 1;
return 0;
case ARET: /* funny */
if(s != A)
return 1;
return 3;
case ACALL: /* funny */
if(REGEXT && v->type <= REGEXT && v->type > exregoffset)
return 2;
if(REGARG >= 0 && v->type == (uchar)REGARG)
return 2;
if(s != A) {
if(copysub(&p->to, v, s, 1))
return 1;
return copysub(&p->from, v, s, 1);
}
if(copyau(&p->to, v))
return 4;
if(copyau(&p->from, v))
return 4;
return 3;
case ATEXT: /* funny */
if(REGARG >= 0 && v->type == (uchar)REGARG)
return 3;
return 0;
}
+ proginfo(&info, p);
+
+ if((info.reguse|info.regset) & RtoB(v->type))
+ return 2;
+
+ if(info.flags & LeftAddr)
+ if(copyas(&p->from, v))
+ return 2;
+
+ if((info.flags & (RightRead|RightWrite)) == (RightRead|RightWrite))
+ if(copyas(&p->to, v))
+ return 2;
+
+ if(info.flags & RightWrite) {
+ if(copyas(&p->to, v)) {
+ if(s != A)
+ return copysub(&p->from, v, s, 1);
+ if(copyau(&p->from, v))
+ return 4;
+ return 3;
+ }
+ }
+
+ if(info.flags & (LeftAddr|LeftRead|LeftWrite|RightAddr|RightRead|RightWrite)) {
+ if(s != A) {
+ if(copysub(&p->from, v, s, 1))
+ return 1;
+ return copysub(&p->to, v, s, 1);
+ }
+ if(copyau(&p->from, v))
+ return 1;
+ if(copyau(&p->to, v))
+ return 1;
+ }
+
return 0;
}
src/cmd/8g/reg.c
regopt
関数の変更例:
for(p=firstp; p!=P; p=p->link) {
- switch(p->as) {
- case ADATA:
- case AGLOBL:
- case ANAME:
- case ASIGNAME:
- case ATYPE:
+ proginfo(&info, p);
+ if(info.flags & Skip)
continue;
- }
r = rega();
nr++;
if(firstr == R) {
if(p->as == ACALL && p->to.type == D_EXTERN)
continue;
- // Addressing makes some registers used.
- if(p->from.type >= D_INDIR)
- r->use1.b[0] |= RtoB(p->from.type-D_INDIR);
- if(p->from.index != D_NONE)
- r->use1.b[0] |= RtoB(p->from.index);
- if(p->to.type >= D_INDIR)
- r->use2.b[0] |= RtoB(p->to.type-D_INDIR);
- if(p->to.index != D_NONE)
- r->use2.b[0] |= RtoB(p->to.index);
+ r->use1.b[0] |= info.reguse | info.regindex;
+ r->set.b[0] |= info.regset;
bit = mkvar(r, &p->from);
- if(bany(&bit))
- switch(p->as) {
- /*
- * funny
- */
- case ALEAL:
- case AFMOVD:
- case AFMOVF:
- case AFMOVL:
- case AFMOVW:
- case AFMOVV:
- setaddrs(bit);
- break;
-
- /*
- * left side read
- */
- default:
- for(z=0; z<BITS; z++)
- r->use1.b[z] |= bit.b[z];
- break;
-
- /*
- * left side read+write
- */
- case AXCHGB:
- case AXCHGW:
- case AXCHGL:
- for(z=0; z<BITS; z++) {
- r->use1.b[z] |= bit.b[z];
- r->set.b[z] |= bit.b[z];
- }
- break;
+ if(bany(&bit)) {
+ if(info.flags & LeftAddr)
+ setaddrs(bit);
+ if(info.flags & LeftRead)
+ for(z=0; z<BITS; z++)
+ r->use1.b[z] |= bit.b[z];
+ if(info.flags & LeftWrite)
+ for(z=0; z<BITS; z++)
+ r->set.b[z] |= bit.b[z];
}
コアとなるコードの解説
このコミットの核となるのは、ProgInfo
構造体、progtable
、そしてproginfo
関数の導入です。
ProgInfo
構造体とフラグ
ProgInfo
構造体は、各アセンブラ命令の静的な特性をカプセル化します。
flags
: 命令の基本的な動作やカテゴリを示すビットフラグの集合です。例えば、LeftRead
は命令が左オペランドを読み込むことを示し、RightWrite
は右オペランドに書き込むことを示します。SetCarry
は命令がキャリーフラグを設定することを示し、Call
は関数呼び出し命令であることを示します。これらのフラグを組み合わせることで、命令の複雑な動作を簡潔に表現できます。reguse
: 命令が実行されるために必須となるレジスタのビットマスクです。例えば、DIV
命令は暗黙的にAX
やDX
レジスタを使用するため、これらのレジスタがreguse
に含まれます。regset
: 命令が結果を書き込むレジスタのビットマスクです。reguse
と同様に、暗黙的に結果が書き込まれるレジスタ(例:DIV
命令後のAX
やDX
)が含まれます。regindex
: アドレッシングモード(例:[BP+SI]
)で使用されるレジスタのビットマスクです。
これらのフラグとレジスタ情報は、コンパイラの最適化パスが命令の具体的なオペコードに依存することなく、その抽象的な特性に基づいて処理を決定できるようにします。
progtable
progtable
は、ALAST
(すべてのアセンブラ命令の列挙の最大値)までの各命令コードに対応するProgInfo
エントリを格納する静的な配列です。このテーブルは、src/cmd/8g/prog.c
で定義されており、Goコンパイラがサポートするすべてのアセンブラ命令の「真実の情報源」となります。
例えば、AADCL
(Add with Carry Long)命令のエントリは以下のようになっています。
[AADCL]= {SizeL | LeftRead | RightRdwr | SetCarry | UseCarry},
これは、AADCL
が:
SizeL
: 32ビット(Long)データを操作する。LeftRead
: 左オペランドを読み込む。RightRdwr
: 右オペランドを読み書きする。SetCarry
: キャリーフラグを設定する。UseCarry
: キャリーフラグを使用する。 という特性を持つことを示しています。
proginfo
関数
proginfo
関数は、Prog
構造体(アセンブラ命令の内部表現)を受け取り、その命令のProgInfo
を計算して返します。
- まず、
p->as
(命令のオペコード)をインデックスとしてprogtable
から基本的なProgInfo
エントリを取得します。 - 次に、特定の命令(例: シフト命令で
CX
レジスタが定数でない場合、IMUL
命令でAX
/DX
レジスタが暗黙的に使用される場合)に対して、動的なレジスタ使用情報をreguse
やregset
に追加します。 - 最後に、オペランドのアドレッシングモードで使用されるレジスタ(例:
[BP+SI]
のBP
やSI
)をregindex
に追加します。
この関数は、命令の静的な特性と、特定のオペランドの形式によって変化する動的な特性を組み合わせて、命令の完全なプロパティ情報を提供します。
最適化パスでの利用
peep.c
やreg.c
のような最適化パスでは、もはや命令の種類ごとに複雑なswitch
文を書く必要がなくなりました。代わりに、proginfo
関数を呼び出して命令のProgInfo
を取得し、そのflags
やreguse
/regset
ビットマスクをチェックするだけで、命令の動作を正確に判断できるようになりました。
例えば、peep.c
のcopyu
関数では、以前は多数のcase
文で各命令のレジスタ読み書き特性を個別に処理していましたが、変更後はproginfo
を呼び出し、info.flags
のLeftAddr
, LeftRead
, LeftWrite
, RightAddr
, RightRead
, RightWrite
などのフラグをチェックすることで、汎用的なロジックで処理できるようになりました。これにより、コードの行数が大幅に削減され、可読性と保守性が向上しています。
RtoB
マクロは、レジスタの列挙値(例: D_AX
)をビットマスク(例: AX
)に変換するために使用されます。これにより、レジスタの集合をビット単位で効率的に表現し、操作することが可能になります。
この一連の変更は、Goコンパイラのバックエンドにおける命令処理の基盤を強化し、将来的な機能追加や最適化の改善をより容易にするための重要なステップです。
関連リンク
- Go言語公式サイト: https://golang.org/
- Goのソースコード (GitHub): https://github.com/golang/go
- Goコンパイラの内部構造に関する一般的な情報源(公式ドキュメントやブログ記事など)
参考にした情報源リンク
- Goのソースコード(特に
src/cmd/8g/
およびsrc/cmd/6g/
ディレクトリ内のファイル) - Goのコードレビューシステム (Gerrit) のCL 12778043: https://golang.org/cl/12778043
- 参照されているCL 12637051 (Go Gerrit): https://golang.org/cl/12637051 (このCLは
6g
に対する同様の変更を示しており、今回のコミットの背景を理解する上で非常に重要です。) - Goコンパイラに関する一般的な知識(アセンブラ、最適化、レジスタ割り当てなど)# [インデックス 17157] ファイルの概要
このコミットは、Goコンパイラのx86 32-bitアーキテクチャ向けバックエンドであるcmd/8g
において、アセンブラ命令に関する情報を一元管理するための大規模なリファクタリングを導入しています。具体的には、各命令の特性(レジスタの使用状況、キャリーフラグへの影響など)を記述するProgInfo
構造体と、その情報を取得するproginfo
関数を導入し、命令のプロパティに依存する最適化ロジックからこれらの情報を切り離しました。これにより、コードの重複が排除され、保守性と拡張性が向上しています。
変更されたファイルは以下の通りです。
src/cmd/6g/prog.c
: 既存の6g
コンパイラ(x86 64-bit)における命令情報定義の微修正。src/cmd/8g/opt.h
:8g
コンパイラにおける命令情報の新しい構造体ProgInfo
と関連フラグの定義。src/cmd/8g/peep.c
:8g
コンパイラのpeephole最適化パスにおいて、命令プロパティの取得方法を新しいproginfo
関数に移行。src/cmd/8g/prog.c
:8g
コンパイラ向けに、命令ごとの詳細なプロパティを定義したprogtable
と、それを参照するproginfo
関数を新規作成。src/cmd/8g/reg.c
:8g
コンパイラのレジスタ割り当てパスにおいて、命令プロパティの取得方法を新しいproginfo
関数に移行。
コミット
cmd/8g: factor out prog information
Like CL 12637051, but for 8g instead of 6g.
Fix a few minor 6g errors too.
R=ken2
CC=golang-dev
https://golang.org/cl/12778043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/ac0df6ce895c14216d5f57cc749e7080815860f9
元コミット内容
このコミットは、Goコンパイラのx86 32-bitアーキテクチャ向けバックエンドであるcmd/8g
において、アセンブラ命令に関する情報を一元化するリファクタリングを実施しています。
コミットメッセージによると、この変更は以前に6g
(x86 64-bitアーキテクチャ向けコンパイラ)で行われた同様の変更(CL 12637051)を8g
に適用するものです。これは、コンパイラの異なるバックエンド間で共通の設計パターンを適用し、コードの整合性を高める意図があることを示唆しています。
また、6g
におけるいくつかの小さなエラー修正も含まれており、これはおそらく、命令情報の共通化に伴う全体的なコード品質の向上の一環として行われたものと考えられます。
このリファクタリングの主な目的は、命令のプロパティ(例:どのレジスタが読み書きされるか、キャリーフラグが設定されるかなど)を、命令を処理する各最適化パス(peephole最適化やレジスタ割り当てなど)から切り離し、一元的なデータ構造と関数で管理することにあります。これにより、命令の特性に関するロジックが重複することなく、よりクリーンで保守しやすいコードベースが実現されます。
変更の背景
Goコンパイラのバックエンドは、特定のCPUアーキテクチャ向けにアセンブラ命令を生成し、最適化を行います。この最適化プロセスでは、各アセンブラ命令がレジスタやフラグにどのような影響を与えるかを正確に把握する必要があります。例えば、ある命令が特定のレジスタを読み込むのか、書き込むのか、あるいは両方を行うのか、また、キャリーフラグなどのステータスフラグを設定するのか、使用するのか、破壊するのかといった情報です。
従来のGoコンパイラでは、これらの命令に関するプロパティ情報が、peephole最適化(peep.c
)やレジスタ割り当て(reg.c
)といった複数の最適化パスのコード内に散在していました。これは、新しい命令が追加されたり、既存の命令の特性が変更されたりするたびに、複数の場所でコードを修正する必要があることを意味し、コードの重複、保守性の低下、そしてバグの温床となる可能性がありました。
この問題に対処するため、Goチームは命令のプロパティ情報を一元化するアプローチを採用しました。このコミットは、そのアプローチを8g
コンパイラ(x86 32-bit)に適用するものです。コミットメッセージにある「Like CL 12637051, but for 8g instead of 6g」という記述は、既に6g
コンパイラ(x86 64-bit)で同様のリファクタリングが成功裏に実施されており、その設計パターンを8g
にも展開していることを明確に示しています。これにより、コンパイラ全体のコードベースの一貫性と品質が向上します。
前提知識の解説
このコミットを理解するためには、以下の概念について基本的な知識があると役立ちます。
Goコンパイラのバックエンド (cmd/8g
, cmd/6g
)
Goコンパイラは、ソースコードを中間表現に変換した後、各ターゲットアーキテクチャ(例: x86, ARM)向けのアセンブラコードを生成します。このアセンブラコードの生成と最適化を担当するのがバックエンドです。
cmd/8g
: x86 32-bitアーキテクチャ向けのコンパイラバックエンドです。cmd/6g
: x86 64-bitアーキテクチャ向けのコンパイラバックエンドです。 これらのバックエンドは、それぞれのアセンブラ命令セットとレジスタセットに特化した最適化を行います。
アセンブラ命令の最適化
コンパイラは、生成されたアセンブラコードのパフォーマンスを向上させるために様々な最適化を行います。これには、不要な命令の削除、命令の並べ替え、レジスタの効率的な使用などが含まれます。これらの最適化を行う際、コンパイラは各アセンブラ命令がシステムの状態(特にレジスタやCPUフラグ)にどのような影響を与えるかを正確に知る必要があります。
Prog
構造体
Goコンパイラのバックエンドでは、アセンブラ命令は内部的にProg
という構造体で表現されます。この構造体には、命令の種類(例: AMOVL
、AADDL
)、オペランド(ソースとデスティネーション)、そして次の命令へのリンクなどが含まれます。
ProgInfo
構造体とフラグ
このコミットで導入されたProgInfo
構造体は、個々のアセンブラ命令の静的な特性を記述するためのものです。この構造体には、命令の動作を詳細に表現するビットフラグの集合が含まれています。これらのフラグは、命令がレジスタをどのように使用するか、キャリーフラグなどのCPUフラグにどのように影響するか、命令の種類(移動、変換、条件分岐など)を示すために使用されます。
主要なフラグとその意味は以下の通りです。
Pseudo
: 擬似命令。実際のCPU命令ではなく、コンパイラ内部でのみ意味を持つ命令(例:ATEXT
,AGLOBL
)。OK
: 特に特筆すべき情報はないが、有効な命令。SizeB
,SizeW
,SizeL
,SizeQ
,SizeF
,SizeD
: 命令が操作するデータのサイズ(バイト、ワード、ロング、クアッド、単精度浮動小数点、倍精度浮動小数点)。LeftAddr
,LeftRead
,LeftWrite
: 命令の左オペランド(通常はソース)がアドレスとして使われるか、読み込まれるか、書き込まれるか。RightAddr
,RightRead
,RightWrite
: 命令の右オペランド(通常はデスティネーション)がアドレスとして使われるか、読み込まれるか、書き込まれるか。LeftRdwr
(LeftRead | LeftWrite
)、RightRdwr
(RightRead | RightWrite
) のように、読み書き両方を示す複合フラグも定義されています。
SetCarry
,UseCarry
,KillCarry
: 命令がキャリーフラグを設定するか、使用するか、またはその後のキャリーフラグの状態を無効にする(破壊する)か。Move
: データ移動命令(例:MOV
)。Conv
: データ型変換命令(例:CVT
)。Cjmp
: 条件付きジャンプ命令。Break
: 制御フローを中断する命令(例:RET
,JMP
)。Call
: 関数呼び出し命令。Jump
: 無条件ジャンプ命令。Skip
: 最適化パスでスキップされるべき命令(例:Pseudo
命令の一部)。ShiftCX
: シフト命令などで、CX
レジスタがシフト量として使用される可能性があることを示す。ImulAXDX
:IMUL
命令などで、AX
とDX
レジスタが暗黙的に使用されることを示す。
peep.c
(Peephole Optimizer)
peephole最適化は、コンパイラが生成したアセンブラコードの小さなシーケンス(「peephole」と呼ばれる)を検査し、より効率的な同等のシーケンスに置き換える最適化手法です。例えば、MOV AX, BX; MOV BX, AX
のような冗長な命令シーケンスを削除したり、より短い命令に置き換えたりします。この最適化は、命令のレジスタ使用状況やフラグへの影響を詳細に知る必要があります。
reg.c
(Register Allocator)
レジスタ割り当ては、プログラムの変数をCPUのレジスタに割り当てるプロセスです。レジスタはCPUが直接アクセスできる高速なメモリであり、レジスタを効率的に使用することでプログラムの実行速度が大幅に向上します。レジスタ割り当て器は、各命令がどのレジスタを読み込み、どのレジスタに書き込むかを把握し、レジスタの競合を避けつつ最適な割り当てを行います。
技術的詳細
このコミットの核心は、Goコンパイラの8g
バックエンドにおけるアセンブラ命令のプロパティ管理を、分散型から集中型へと移行した点にあります。
-
ProgInfo
構造体の定義 (src/cmd/8g/opt.h
):- このヘッダーファイルに、新しい
ProgInfo
構造体が定義されました。この構造体は、命令の特性を表現するためのビットフラグflags
、命令が使用する必須レジスタreguse
、命令が設定する必須レジスタregset
、そしてアドレッシングモードで使用されるレジスタregindex
を含みます。 - また、命令の様々な特性を示すためのビットフラグ(
Pseudo
,SizeB
,LeftRead
,RightWrite
,SetCarry
,UseCarry
,Move
,Call
など)がenum
として定義されました。これらのフラグは、命令の動作を抽象化し、最適化パスが命令の種類に依存することなく、その特性に基づいて処理を行えるようにします。
- このヘッダーファイルに、新しい
-
prog.c
の新規作成とprogtable
の導入 (src/cmd/8g/prog.c
):- このコミットで新しく作成された
src/cmd/8g/prog.c
ファイルは、8g
コンパイラがサポートするすべてのアセンブラ命令(ALAST
までの範囲)について、そのProgInfo
を定義した静的なテーブルprogtable
を含んでいます。 progtable
は、各命令コード(例:AADCL
,AMOVL
,ACALL
)に対応するProgInfo
エントリを持ち、その命令のサイズ、オペランドの読み書き特性、キャリーフラグへの影響、命令の種類(移動、呼び出し、分岐など)をビットフラグで詳細に記述しています。- このファイルには、
proginfo(ProgInfo *info, Prog *p)
関数も実装されています。この関数は、与えられたProg
(アセンブラ命令)のオペコード(p->as
)に基づいてprogtable
から対応するProgInfo
エントリを取得し、さらに特定の命令(例: シフト命令でのCX
レジスタの使用、IMUL
命令でのAX
/DX
レジスタの使用)に関する動的なレジスタ使用情報を追加で設定します。
- このコミットで新しく作成された
-
最適化パスの変更 (
src/cmd/8g/peep.c
,src/cmd/8g/reg.c
):peep.c
(peephole最適化)とreg.c
(レジスタ割り当て)のファイルでは、これまで命令の種類ごとに個別のswitch
文で処理されていた命令のプロパティに関するロジックが、新しく導入されたproginfo
関数とProgInfo
構造体のフラグチェックに置き換えられました。- 例えば、
peep.c
のneedc
関数では、以前はキャリーフラグを使用する命令を個別に列挙していましたが、変更後はproginfo
を呼び出し、返されたinfo.flags
にUseCarry
フラグが立っているかをチェックするだけでよくなりました。同様に、subprop
関数やcopyu
関数でも、命令のレジスタ使用や読み書き特性の判定にinfo.flags
が活用されています。 reg.c
のregopt
関数では、命令のuse1
(ソースオペランドのレジスタ使用)、set
(デスティネーションオペランドのレジスタ設定)などのビットマップを構築する際に、proginfo
から得られるinfo.reguse
、info.regset
、info.regindex
が直接利用されるようになりました。これにより、レジスタ割り当てロジックが大幅に簡素化され、命令ごとの特殊なケースを個別にハンドリングする必要がなくなりました。
-
6g
の修正 (src/cmd/6g/prog.c
):- このファイルでは、
ANOTW
命令のサイズがSizeQ
からSizeW
に修正され、APUSHQ
命令のRightWrite
フラグがLeftRead
に修正されるなど、いくつかのマイナーなバグ修正が行われています。これらは、おそらく6g
での同様のリファクタリング時に見落とされた、またはこの共通化の過程で発見された不整合の修正と考えられます。
- このファイルでは、
このリファクタリングにより、命令のプロパティ定義が一元化され、コンパイラのバックエンドコードの重複が大幅に削減されました。これにより、コードの保守性が向上し、将来的な命令セットの拡張や最適化の改善が容易になります。また、命令の特性に関する単一の信頼できる情報源が提供されることで、コンパイラの正確性と堅牢性も向上します。
コアとなるコードの変更箇所
src/cmd/8g/opt.h
+typedef struct ProgInfo ProgInfo;
+struct ProgInfo
+{
+ uint32 flags; // the bits below
+ uint32 reguse; // required registers used by this instruction
+ uint32 regset; // required registers set by this instruction
+ uint32 regindex; // registers used by addressing mode
+};
+
+enum
+{
+ // Pseudo-op, like TEXT, GLOBL, TYPE, PCDATA, FUNCDATA.
+ Pseudo = 1<<1,
+
+ // There's nothing to say about the instruction,
+ // but it's still okay to see.
+ OK = 1<<2,
+
+ // Size of right-side write, or right-side read if no write.
+ SizeB = 1<<3,
+ SizeW = 1<<4,
+ SizeL = 1<<5,
+ SizeQ = 1<<6,
+ SizeF = 1<<7, // float aka float32
+ SizeD = 1<<8, // double aka float64
+
+ // Left side: address taken, read, write.
+ LeftAddr = 1<<9,
+ LeftRead = 1<<10,
+ LeftWrite = 1<<11,
+
+ // Right side: address taken, read, write.
+ RightAddr = 1<<12,
+ RightRead = 1<<13,
+ RightWrite = 1<<14,
+
+ // Set, use, or kill of carry bit.
+ // Kill means we never look at the carry bit after this kind of instruction.
+ SetCarry = 1<<15,
+ UseCarry = 1<<16,
+ KillCarry = 1<<17,
+
+ // Instruction kinds
+ Move = 1<<18, // straight move
+ Conv = 1<<19, // size conversion
+ Cjmp = 1<<20, // conditional jump
+ Break = 1<<21, // breaks control flow (no fallthrough)
+ Call = 1<<22, // function call
+ Jump = 1<<23, // jump
+ Skip = 1<<24, // data instruction
+
+ // Special cases for register use.
+ ShiftCX = 1<<25, // possible shift by CX
+ ImulAXDX = 1<<26, // possible multiply into DX:AX
+};
+
+void proginfo(ProgInfo*, Prog*);
src/cmd/8g/prog.c
(新規作成)
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
#include <u.h>
#include <libc.h>
#include "gg.h"
#include "opt.h"
// Matches real RtoB but can be used in global initializer.
#define RtoB(r) (1<<((r)-D_AX))
enum {
AX = RtoB(D_AX),
BX = RtoB(D_BX),
CX = RtoB(D_CX),
DX = RtoB(D_DX),
DI = RtoB(D_DI),
SI = RtoB(D_SI),
LeftRdwr = LeftRead | LeftWrite,
RightRdwr = RightRead | RightWrite,
};
#undef RtoB
// This table gives the basic information about instruction
// generated by the compiler and processed in the optimizer.
// See opt.h for bit definitions.
//
// Instructions not generated need not be listed.
// As an exception to that rule, we typically write down all the
// size variants of an operation even if we just use a subset.
//
// The table is formatted for 8-space tabs.
static ProgInfo progtable[ALAST] = {
[ATYPE]= {Pseudo | Skip},
[ATEXT]= {Pseudo},
[AFUNCDATA]= {Pseudo},
[APCDATA]= {Pseudo},
// NOP is an internal no-op that also stands
// for USED and SET annotations, not the Intel opcode.
[ANOP]= {LeftRead | RightWrite},
[AADCL]= {SizeL | LeftRead | RightRdwr | SetCarry | UseCarry},
[AADCW]= {SizeW | LeftRead | RightRdwr | SetCarry | UseCarry},
// ... (他の命令の定義が続く) ...
[AXORB]= {SizeB | LeftRead | RightRdwr | SetCarry},
[AXORL]= {SizeL | LeftRead | RightRdwr | SetCarry},
[AXORW]= {SizeW | LeftRead | RightRdwr | SetCarry},
};
void
proginfo(ProgInfo *info, Prog *p)
{
*info = progtable[p->as];
if(info->flags == 0)
fatal("unknown instruction %P", p);
if((info->flags & ShiftCX) && p->from.type != D_CONST)
info->reguse |= CX;
if(info->flags & ImulAXDX) {
if(p->to.type == D_NONE) {
info->reguse |= AX;
info->regset |= AX | DX;
} else {
info->flags |= RightRdwr;
}
}
// Addressing makes some registers used.
if(p->from.type >= D_INDIR)
info->regindex |= RtoB(p->from.type-D_INDIR);
if(p->from.index != D_NONE)
info->regindex |= RtoB(p->from.index);
if(p->to.type >= D_INDIR)
info->regindex |= RtoB(p->to.type-D_INDIR);
if(p->to.index != D_NONE)
info->regindex |= RtoB(p->to.index);
}
src/cmd/8g/peep.c
needc
関数の変更例:
static int
needc(Prog *p)
{
+ ProgInfo info;
+
while(p != P) {
- switch(p->as) {
- case AADCL:
- case ASBBL:
- case ARCRB:
- case ARCRW:
- case ARCRL:
+ proginfo(&info, p);
+ if(info.flags & UseCarry)
return 1;
- case AADDB:
- case AADDW:
- case AADDL:
- case ASUBB:
- case ASUBW:
- case ASUBL:
- case AJMP:
- case ARET:
- case ACALL:
+ if(info.flags & (SetCarry|KillCarry))
return 0;
- default:
- if(p->to.type == D_BRANCH)
- return 0;
- }
p = p->link;
}
return 0;
copyu
関数の変更例(大幅なswitch
文の削除とproginfo
利用への移行):
int
copyu(Prog *p, Adr *v, Adr *s)
{
+ ProgInfo info;
+
switch(p->as) {
-
- default:
- // ... 多数のcase文が削除 ...
-
case AJMP: /* funny */
if(s != A) {
if(copysub(&p->to, v, s, 1))
return 1;
return copysub(&p->from, v, s, 1);
}
if(copyau(&p->to, v))
return 1;
if(copyau(&p->from, v))
return 1;
return 0;
case ARET: /* funny */
if(s != A)
return 1;
return 3;
case ACALL: /* funny */
if(REGEXT && v->type <= REGEXT && v->type > exregoffset)
return 2;
if(REGARG >= 0 && v->type == (uchar)REGARG)
return 2;
if(s != A) {
if(copysub(&p->to, v, s, 1))
return 1;
return copysub(&p->from, v, s, 1);
}
if(copyau(&p->to, v))
return 4;
if(copyau(&p->from, v))
return 4;
return 3;
case ATEXT: /* funny */
if(REGARG >= 0 && v->type == (uchar)REGARG)
return 3;
return 0;
}
+ proginfo(&info, p);
+
+ if((info.reguse|info.regset) & RtoB(v->type))
+ return 2;
+
+ if(info.flags & LeftAddr)
+ if(copyas(&p->from, v))
+ return 2;
+
+ if((info.flags & (RightRead|RightWrite)) == (RightRead|RightWrite))
+ if(copyas(&p->to, v))
+ return 2;
+
+ if(info.flags & RightWrite) {
+ if(copyas(&p->to, v)) {
+ if(s != A)
+ return copysub(&p->from, v, s, 1);
+ if(copyau(&p->from, v))
+ return 4;
+ return 3;
+ }
+ }
+
+ if(info.flags & (LeftAddr|LeftRead|LeftWrite|RightAddr|RightRead|RightWrite)) {
+ if(s != A) {
+ if(copysub(&p->from, v, s, 1))
+ return 1;
+ return copysub(&p->to, v, s, 1);
+ }
+ if(copyau(&p->from, v))
+ return 1;
+ if(copyau(&p->to, v))
+ return 1;
+ }
+
return 0;
}
src/cmd/8g/reg.c
regopt
関数の変更例:
for(p=firstp; p!=P; p=p->link) {
- switch(p->as) {
- case ADATA:
- case AGLOBL:
- case ANAME:
- case ASIGNAME:
- case ATYPE:
+ proginfo(&info, p);
+ if(info.flags & Skip)
continue;
- }
r = rega();
nr++;
if(firstr == R) {
if(p->as == ACALL && p->to.type == D_EXTERN)
continue;
- // Addressing makes some registers used.
- if(p->from.type >= D_INDIR)
- r->use1.b[0] |= RtoB(p->from.type-D_INDIR);
- if(p->from.index != D_NONE)
- r->use1.b[0] |= RtoB(p->from.index);
- if(p->to.type >= D_INDIR)
- r->use2.b[0] |= RtoB(p->to.type-D_INDIR);
- if(p->to.index != D_NONE)
- r->use2.b[0] |= RtoB(p->to.index);
+ r->use1.b[0] |= info.reguse | info.regindex;
+ r->set.b[0] |= info.regset;
bit = mkvar(r, &p->from);
- if(bany(&bit))
- switch(p->as) {
- /*
- * funny
- */
- case ALEAL:
- case AFMOVD:
- case AFMOVF:
- case AFMOVL:
- case AFMOVW:
- case AFMOVV:
- setaddrs(bit);
- break;
-
- /*
- * left side read
- */
- default:
- for(z=0; z<BITS; z++)
- r->use1.b[z] |= bit.b[z];
- break;
-
- /*
- * left side read+write
- */
- case AXCHGB:
- case AXCHGW:
- case AXCHGL:
- for(z=0; z<BITS; z++) {
- r->use1.b[z] |= bit.b[z];
- r->set.b[z] |= bit.b[z];
- }
- break;
+ if(bany(&bit)) {
+ if(info.flags & LeftAddr)
+ setaddrs(bit);
+ if(info.flags & LeftRead)
+ for(z=0; z<BITS; z++)
+ r->use1.b[z] |= bit.b[z];
+ if(info.flags & LeftWrite)
+ for(z=0; z<BITS; z++)
+ r->set.b[z] |= bit.b[z];
}
コアとなるコードの解説
このコミットの核となるのは、ProgInfo
構造体、progtable
、そしてproginfo
関数の導入です。
ProgInfo
構造体とフラグ
ProgInfo
構造体は、各アセンブラ命令の静的な特性をカプセル化します。
flags
: 命令の基本的な動作やカテゴリを示すビットフラグの集合です。例えば、LeftRead
は命令が左オペランドを読み込むことを示し、RightWrite
は右オペランドに書き込むことを示します。SetCarry
は命令がキャリーフラグを設定することを示し、Call
は関数呼び出し命令であることを示します。これらのフラグを組み合わせることで、命令の複雑な動作を簡潔に表現できます。reguse
: 命令が実行されるために必須となるレジスタのビットマスクです。例えば、DIV
命令は暗黙的にAX
やDX
レジスタを使用するため、これらのレジスタがreguse
に含まれます。regset
: 命令が結果を書き込むレジスタのビットマスクです。reguse
と同様に、暗黙的に結果が書き込まれるレジスタ(例:DIV
命令後のAX
やDX
)が含まれます。regindex
: アドレッシングモード(例:[BP+SI]
)で使用されるレジスタのビットマスクです。
これらのフラグとレジスタ情報は、コンパイラの最適化パスが命令の具体的なオペコードに依存することなく、その抽象的な特性に基づいて処理を決定できるようにします。
progtable
progtable
は、ALAST
(すべてのアセンブラ命令の列挙の最大値)までの各命令コードに対応するProgInfo
エントリを格納する静的な配列です。このテーブルは、src/cmd/8g/prog.c
で定義されており、Goコンパイラがサポートするすべてのアセンブラ命令の「真実の情報源」となります。
例えば、AADCL
(Add with Carry Long)命令のエントリは以下のようになっています。
[AADCL]= {SizeL | LeftRead | RightRdwr | SetCarry | UseCarry},
これは、AADCL
が:
SizeL
: 32ビット(Long)データを操作する。LeftRead
: 左オペランドを読み込む。RightRdwr
: 右オペランドを読み書きする。SetCarry
: キャリーフラグを設定する。UseCarry
: キャリーフラグを使用する。 という特性を持つことを示しています。
proginfo
関数
proginfo
関数は、Prog
構造体(アセンブラ命令の内部表現)を受け取り、その命令のProgInfo
を計算して返します。
- まず、
p->as
(命令のオペコード)をインデックスとしてprogtable
から基本的なProgInfo
エントリを取得します。 - 次に、特定の命令(例: シフト命令で
CX
レジスタが定数でない場合、IMUL
命令でAX
/DX
レジスタが暗黙的に使用される場合)に対して、動的なレジスタ使用情報をreguse
やregset
に追加します。 - 最後に、オペランドのアドレッシングモードで使用されるレジスタ(例:
[BP+SI]
のBP
やSI
)をregindex
に追加します。
この関数は、命令の静的な特性と、特定のオペランドの形式によって変化する動的な特性を組み合わせて、命令の完全なプロパティ情報を提供します。
最適化パスでの利用
peep.c
やreg.c
のような最適化パスでは、もはや命令の種類ごとに複雑なswitch
文を書く必要がなくなりました。代わりに、proginfo
関数を呼び出して命令のProgInfo
を取得し、そのflags
やreguse
/regset
ビットマスクをチェックするだけで、命令の動作を正確に判断できるようになりました。
例えば、peep.c
のcopyu
関数では、以前は多数のcase
文で各命令のレジスタ読み書き特性を個別に処理していましたが、変更後はproginfo
を呼び出し、info.flags
のLeftAddr
, LeftRead
, LeftWrite
, RightAddr
, RightRead
, RightWrite
などのフラグをチェックすることで、汎用的なロジックで処理できるようになりました。これにより、コードの行数が大幅に削減され、可読性と保守性が向上しています。
RtoB
マクロは、レジスタの列挙値(例: D_AX
)をビットマスク(例: AX
)に変換するために使用されます。これにより、レジスタの集合をビット単位で効率的に表現し、操作することが可能になります。
この一連の変更は、Goコンパイラのバックエンドにおける命令処理の基盤を強化し、将来的な機能追加や最適化の改善をより容易にするための重要なステップです。
関連リンク
- Go言語公式サイト: https://golang.org/
- Goのソースコード (GitHub): https://github.com/golang/go
- Goコンパイラの内部構造に関する一般的な情報源(公式ドキュメントやブログ記事など)
参考にした情報源リンク
- Goのソースコード(特に
src/cmd/8g/
およびsrc/cmd/6g/
ディレクトリ内のファイル) - Goのコードレビューシステム (Gerrit) のCL 12778043: https://golang.org/cl/12778043
- 参照されているCL 12637051 (Go Gerrit): https://golang.org/cl/12637051 (このCLは
6g
に対する同様の変更を示しており、今回のコミットの背景を理解する上で非常に重要です。) - Goコンパイラに関する一般的な知識(アセンブラ、最適化、レジスタ割り当てなど)