[インデックス 17153] ファイルの概要
このコミットは、Goコンパイラのcmd/6g
(64-bit x86アーキテクチャ向けコンパイラ)における命令デコードロジックを共通関数に集約し、最適化パスの堅牢性と保守性を向上させるものです。これまで複数の最適化パスで重複し、かつ不正確であった命令に関する情報を一元化することで、コードの品質と将来的な拡張性を高めています。
コミット
commit 24c8035fbe113dfe644f4419eadcb826e08788ee
Author: Russ Cox <rsc@golang.org>
Date: Sun Aug 11 21:46:38 2013 -0400
cmd/6g: move opt instruction decode into common function
Add new proginfo function that returns information about a
Prog*. The information includes various instruction
description bits as well as a list of required registers set
and used and indexing registers used.
Convert the large instruction switches to use proginfo.
This information was formerly duplicated in multiple
optimization passes, inconsistently. For example, the
information about which registers an instruction requires
appeared three times for most instructions.
Most of the switches were incomplete or incorrect in some way.
For example, the switch in copyu did not list cases for INCB,
JPS, MOVAPD, MOVBWSX, MOVBWZX, PCDATA, POPQ, PUSHQ, STD,
TESTB, TESTQ, and XCHGL. Those were all falling into the
"unknown instruction" default case and stopping the rewrite,
perhaps unnecessarily. Similarly, the switch in needc only
listed a handful of the instructions that use or set the carry bit.
We still need to decide whether to use proginfo to generalize
a few of the remaining smaller switches in peep.c.
If this goes well, we'll make similar changes in 8g and 5g.
R=ken2
CC=golang-dev
https://golang.org/cl/12637051
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/24c8035fbe113dfe644f4419eadcb826e08788ee
元コミット内容
cmd/6g
: 最適化命令デコードを共通関数に移動
Prog*
に関する情報(様々な命令記述ビット、使用・設定される必須レジスタ、インデックスレジスタ)を返す新しいproginfo
関数を追加。
大規模な命令スイッチをproginfo
を使用するように変換。
この情報は以前、複数の最適化パスで重複し、一貫性がなかった。例えば、命令が必要とするレジスタに関する情報は、ほとんどの命令で3回出現していた。
ほとんどのスイッチは、何らかの形で不完全または不正確だった。例えば、copyu
のスイッチはINCB
, JPS
, MOVAPD
, MOVBWSX
, MOVBWZX
, PCDATA
, POPQ
, PUSHQ
, STD
, TESTB
, TESTQ
, XCHGL
のケースをリストしておらず、これらはすべて「不明な命令」のデフォルトケースにフォールバックし、不必要に書き換えを停止させていた可能性がある。同様に、needc
のスイッチは、キャリービットを使用または設定する命令のごく一部しかリストしていなかった。
peep.c
に残っているいくつかの小さなスイッチをproginfo
で一般化するかどうかは、まだ決定する必要がある。
これがうまくいけば、8g
と5g
でも同様の変更を行う予定。
変更の背景
Goコンパイラの最適化フェーズにおいて、各命令(Prog*
構造体で表現される)がどのような特性を持つか(例:どのレジスタを使用・変更するか、キャリーフラグに影響するか、命令の種類など)に関する情報が、複数の異なる最適化パス(copyu
, needc
など)で個別に、かつswitch
文を用いてハードコードされていました。
このアプローチには以下の問題がありました。
- 情報の重複と不整合: 同じ命令に関する情報が複数の場所で定義されており、変更があった場合にすべての箇所を更新する必要がありました。これにより、情報が不整合になるリスクが高まりました。
- 不完全性・不正確性: 各
switch
文がすべての命令を網羅しているわけではなく、一部の命令が「不明な命令」として扱われ、最適化が早期に停止してしまう問題がありました。これは、本来最適化可能なコードが最適化されない原因となっていました。コミットメッセージでは、copyu
におけるINCB
,JPS
,MOVAPD
などの命令や、needc
におけるキャリービット関連の命令の例が挙げられています。 - 保守性の低下: 新しい命令が追加されたり、既存の命令の特性が変更されたりするたびに、関連するすべての最適化パスの
switch
文を更新する必要があり、保守コストが高く、エラーの温床となっていました。
このコミットは、これらの問題を解決するために、命令に関する情報を一元化されたproginfo
関数とprogtable
に集約し、各最適化パスがこの共通の情報源を参照するように変更することで、コードの堅牢性、正確性、保守性を大幅に向上させることを目的としています。
前提知識の解説
このコミットを理解するためには、以下のGoコンパイラの内部構造とx86アセンブリの基本的な知識が必要です。
- Goコンパイラ (
cmd/6g
,cmd/8g
,cmd/5g
):- Go言語のコンパイラは、ターゲットアーキテクチャごとに異なるバックエンドを持っています。
cmd/6g
: x86-64 (AMD64) アーキテクチャ向けのコンパイラバックエンドです。cmd/8g
: ARMアーキテクチャ向けのコンパイラバックエンドです。cmd/5g
: ARMv5/v6/v7アーキテクチャ向けのコンパイラバックエンドです。- これらのコンパイラは、Goのソースコードを中間表現に変換し、最終的にターゲットアーキテクチャの機械語に変換します。この過程で様々な最適化パスが適用されます。
Prog*
構造体:- Goコンパイラのバックエンドにおいて、
Prog
構造体は単一のアセンブリ命令を表す内部表現です。 - 各
Prog
インスタンスは、命令の種類(p->as
)、オペランド(p->from
,p->to
)、リンク(p->link
で次の命令へのポインタ)などの情報を含みます。 - 最適化パスは、この
Prog
のリストを走査し、命令の追加、削除、変更などを行います。
- Goコンパイラのバックエンドにおいて、
- 最適化パス:
- コンパイラは、生成されるコードの性能を向上させるために様々な最適化を行います。
peep.c
(Peephole Optimizer): 局所的な命令列をより効率的な命令列に置き換える最適化です。例えば、MOV AX, BX; MOV BX, CX
をMOV AX, CX
に変換するなど、ごく短い命令シーケンスを対象とします。reg.c
(Register Allocator): プログラム変数に物理レジスタを割り当てる処理を行います。レジスタの使用状況(どのレジスタが使用され、どのレジスタが変更されるか)を正確に把握することが重要です。copyu
関数:peep.c
内で使用される関数で、あるアドレス(レジスタやメモリ位置)が命令によってどのように使用または変更されるかを分析します。needc
関数:peep.c
内で使用される関数で、キャリーフラグ(CPUのステータスレジスタの一部)が特定の命令によって使用されるかどうかを判断します。
- レジスタとフラグ:
- レジスタ: CPU内部の高速な記憶領域で、演算の対象となるデータやアドレスを一時的に保持します。x86-64アーキテクチャでは、
AX
,BX
,CX
,DX
,DI
,SI
などの汎用レジスタがあります。 - キャリーフラグ (Carry Flag, CF): 算術演算の結果、最上位ビットからの桁上がり(または桁借り)が発生したことを示すフラグです。
ADC
(Add with Carry) やSBB
(Subtract with Borrow) などの命令で使用されます。 RtoB
マクロ: レジスタの型(D_AX
など)をビットマスクに変換するためのマクロです。これにより、複数のレジスタをビット単位のORで表現し、レジスタの集合を効率的に管理できます。
- レジスタ: CPU内部の高速な記憶領域で、演算の対象となるデータやアドレスを一時的に保持します。x86-64アーキテクチャでは、
技術的詳細
このコミットの核心は、命令に関するメタデータを一元化するためのProgInfo
構造体と、その情報を取得するためのproginfo
関数、そしてそのデータが格納されたprogtable
の導入です。
ProgInfo
構造体 (src/cmd/6g/opt.h
に追加)
typedef struct ProgInfo ProgInfo;
struct ProgInfo
{
uint32 flags; // 命令の特性を示すビットフラグ
uint32 reguse; // この命令で必須となる使用レジスタのビットマスク
uint32 regset; // この命令で必須となる設定レジスタのビットマスク
uint32 regindex; // アドレス指定モードで使用されるレジスタのビットマスク
};
flags
: 命令の様々な特性をビットフラグで表現します。Pseudo
: 擬似命令(TEXT
,GLOBL
など)。OK
: 特に言うべきことはないが、問題ない命令。SizeB
,SizeW
,SizeL
,SizeQ
,SizeF
,SizeD
: 命令が操作するデータのサイズ(バイト、ワード、ロング、クアッド、float32、float64)。LeftAddr
,LeftRead
,LeftWrite
: 左オペランド(p->from
)がアドレス、読み込み、書き込みのいずれかであるか。RightAddr
,RightRead
,RightWrite
: 右オペランド(p->to
)がアドレス、読み込み、書き込みのいずれかであるか。SetCarry
,UseCarry
,KillCarry
: キャリーフラグの設定、使用、または無効化。Move
,Conv
,Cjmp
,Break
,Call
,Jump
,Skip
: 命令の種類(移動、型変換、条件分岐、制御フロー中断、関数呼び出し、無条件ジャンプ、データ命令)。ShiftCX
,ImulAXDX
:CX
レジスタによるシフト、DX:AX
への乗算など、特定のレジスタ使用パターン。
reguse
: 命令が実行されるために読み込みが必要なレジスタのビットマスク。regset
: 命令が実行された結果、書き込みが行われるレジスタのビットマスク。regindex
: メモリアドレス指定モード(例:[BX+SI*4]
)において、アドレス計算に使用されるレジスタのビットマスク。
progtable
(src/cmd/6g/prog.c
に新規追加)
progtable
は、ALAST
(命令の最大値)までの各命令コードに対応するProgInfo
構造体の配列です。このテーブルは、各命令の静的な特性を定義します。
static ProgInfo progtable[ALAST] = {
[ATYPE]= {Pseudo | Skip},
[ATEXT]= {Pseudo},
// ... 多くの命令の定義 ...
[AADCL]= {SizeL | LeftRead | RightRdwr | SetCarry | UseCarry},
[AADDQ]= {SizeQ | LeftRead | RightRdwr | SetCarry | UseCarry},
// ...
};
このテーブルにより、各命令の特性が中央集権的に管理され、重複や不整合が解消されます。
proginfo
関数 (src/cmd/6g/prog.c
に新規追加)
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; // シフト量が定数でない場合、CXレジスタが使用される
if(info->flags & ImulAXDX) {
if(p->to.type == D_NONE) {
info->reguse |= AX;
info->regset |= AX | DX; // IMUL命令でDX:AXが使用される場合
} else {
info->flags |= RightRdwr;
}
}
// アドレス指定モードによるレジスタ使用の追加
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);
}
proginfo
関数は、与えられたProg*
(命令)から、その命令の特性を記述するProgInfo
構造体を生成します。これはprogtable
から基本情報を取得し、さらに命令のオペランド(p->from
, p->to
)に基づいて、CX
レジスタによるシフトやDX:AX
レジスタへの乗算、アドレス指定モードで使用されるレジスタなど、動的なレジスタ使用情報を追加します。
既存コードの変更
src/cmd/6g/peep.c
:needc
,prevl
,subprop
,copyu
などの関数で、巨大なswitch
文がproginfo
関数からの情報を使用するように置き換えられました。これにより、命令の特性判断が簡潔かつ正確になりました。- 例えば、
copyu
関数では、以前は命令の種類ごとに複雑なswitch
文でレジスタの使用・設定を判断していましたが、proginfo
から得られるinfo.reguse
,info.regset
,info.flags
(LeftRead
,RightWrite
など)を使って汎用的に処理できるようになりました。
src/cmd/6g/reg.c
:- レジスタアロケータの主要な関数である
regopt
内で、命令の特性(スキップすべき命令、制御フローを中断する命令など)を判断するためにproginfo
が使用されるようになりました。 - レジスタの使用(
r->use1
,r->use2
)と設定(r->set
)のビットマスクを計算する際にも、proginfo
から得られるinfo.reguse
,info.regset
,info.regindex
、およびinfo.flags
(LeftRead
,RightWrite
など)が直接利用されるようになりました。
- レジスタアロケータの主要な関数である
コアとなるコードの変更箇所
このコミットによる主要なコード変更は以下のファイルに集中しています。
src/cmd/6g/opt.h
:ProgInfo
構造体の定義が追加されました。ProgInfo
構造体で使用されるビットフラグのenum
定義が追加されました。proginfo
関数のプロトタイプ宣言が追加されました。
src/cmd/6g/peep.c
:needc
,peep
,prevl
,subprop
,copyu
などの関数内で、命令の特性を判断するために使用されていた大規模なswitch
文が削除または大幅に簡略化され、新しく導入されたproginfo
関数からの情報を使用するように変更されました。
src/cmd/6g/prog.c
: (新規ファイル)progtable
という静的なProgInfo
構造体の配列が定義され、各命令の静的な特性がここに一元的に記述されました。proginfo
関数が実装されました。この関数は、progtable
から基本情報を取得し、命令のオペランドに基づいて動的なレジスタ使用情報を追加します。
src/cmd/6g/reg.c
:regopt
関数内で、命令の特性(レジスタの使用、設定、インデックスレジスタ、制御フローの特性など)を判断するために、proginfo
関数が呼び出されるようになりました。これにより、レジスタアロケーションのロジックが簡潔かつ正確になりました。
コアとなるコードの解説
src/cmd/6g/prog.c
(新規ファイル)
このファイルは、命令に関するすべてのメタデータを集約する中心的な役割を担います。
progtable
:- Goコンパイラがサポートする各x86-64命令(
ALAST
までの列挙型で識別される)に対して、その命令の特性を定義するProgInfo
構造体が静的に初期化されています。 - 例えば、
AADCL
(Add with Carry Long) 命令は、SizeL
(32ビット操作)、LeftRead
(左オペランドを読み込む)、RightRdwr
(右オペランドを読み書きする)、SetCarry
(キャリーフラグを設定する)、UseCarry
(キャリーフラグを使用する) といった特性を持つことが明確に定義されています。 ADIVL
(Divide Long) のように、AX
やDX
レジスタを暗黙的に使用・設定する命令についても、reguse
とregset
フィールドでその情報が記述されています。
- Goコンパイラがサポートする各x86-64命令(
proginfo
関数:- この関数は、
Prog* p
(特定のアセンブリ命令)を受け取り、その命令の完全なProgInfo
を返します。 - まず、
progtable[p->as]
から命令の基本的な静的特性を取得します。 - 次に、命令のオペランド(
p->from
,p->to
)を検査し、ShiftCX
(シフト命令でCX
がシフト量として使われる場合)やImulAXDX
(IMUL
命令でDX:AX
が暗黙的に使われる場合)のような動的なレジスタ使用パターンをreguse
やregset
に追加します。 - さらに、メモリアドレス指定モード(例:
[base + index*scale + disp]
)で使用されるレジスタ(p->from.type-D_INDIR
,p->from.index
など)をregindex
に追加します。これにより、アドレス計算に必要なレジスタも正確に追跡できます。
- この関数は、
src/cmd/6g/peep.c
このファイルは、命令の特性を判断するためにproginfo
をどのように利用するかを示しています。
needc
関数:- 以前は、キャリーフラグを使用する命令を個別の
case
文で列挙していましたが、proginfo
のUseCarry
フラグとSetCarry
/KillCarry
フラグを使用するように変更されました。 - これにより、キャリーフラグの依存関係をより正確かつ簡潔に判断できるようになりました。
- 以前は、キャリーフラグを使用する命令を個別の
copyu
関数:- この関数は、あるアドレス
v
が命令p
によってどのように使用または変更されるかを分析します。 - 以前は、命令の種類ごとに非常に長い
switch
文があり、各命令のレジスタ使用・設定、アドレス指定モードなどを個別に処理していました。 - 変更後、
proginfo(&info, p)
を呼び出して命令の特性を取得し、info.flags
(LeftRead
,RightWrite
など)、info.reguse
,info.regset
などに基づいて汎用的なロジックで処理するようになりました。これにより、コードの重複が大幅に削減され、新しい命令が追加された際の保守が容易になりました。
- この関数は、あるアドレス
src/cmd/6g/reg.c
このファイルは、レジスタアロケータがproginfo
をどのように利用してレジスタの使用状況を追跡するかを示しています。
regopt
関数:- 各命令
p
に対してproginfo(&info, p)
を呼び出し、その命令の特性を取得します。 info.flags & Skip
を使用して、最適化の対象外となる擬似命令などをスキップします。info.flags & Break
を使用して、RET
やJMP
のような制御フローを中断する命令を識別し、レジスタフロー分析を適切に処理します。r->use1.b[0] |= info.reguse | info.regindex;
およびr->set.b[0] |= info.regset;
のように、proginfo
から得られたレジスタ使用・設定情報を直接レジスタアロケータの内部データ構造に反映させます。これにより、レジスタのライブネス分析(どのレジスタがどの時点で有効か)がより正確に行えるようになります。- オペランド(
p->from
,p->to
)のレジスタ使用・設定も、info.flags
(LeftAddr
,LeftRead
,LeftWrite
,RightAddr
,RightRead
,RightWrite
)に基づいて汎用的に処理されるようになりました。
- 各命令
これらの変更により、Goコンパイラのバックエンドにおける命令デコードと最適化のロジックが大幅に改善され、より堅牢で保守性の高いコードベースが実現されました。
関連リンク
- Go Gerrit Change: https://golang.org/cl/12637051
- GitHub Commit: https://github.com/golang/go/commit/24c8035fbe113dfe644f4419eadcb826e08788ee
参考にした情報源リンク
- Go言語のコンパイラに関する一般的な情報:
- Go Compiler Internals - The Go Programming Language (一般的なGoコンパイラの概要)
- x86-64アセンブリ命令セットに関する情報:
- コンパイラの最適化に関する一般的な情報:
- レジスタ割り当てに関する一般的な情報:
- Goコンパイラのソースコード (特に
src/cmd/6g
ディレクトリ): - Goコンパイラの開発に関する議論:
- golang-dev mailing list (コミットメッセージに
CC=golang-dev
とあるため)
- golang-dev mailing list (コミットメッセージに
- GoのCL (Change List) について:
- Go Code Review Comments - The Go Programming Language (GoのコードレビュープロセスとCLの役割について)
Prog
構造体やD_AX
などの定数に関する情報:- Goコンパイラのソースコード内の関連ファイル(例:
src/cmd/internal/obj/x86/x86.go
やsrc/cmd/internal/obj/x86/a.h
など、当時のC言語実装における対応するヘッダファイル)
- Goコンパイラのソースコード内の関連ファイル(例:
RtoB
マクロの概念:- ビットマスクを用いたレジスタ集合の表現は、コンパイラやOSカーネルなどの低レベルプログラミングで一般的に用いられる手法です。
fatal
関数:- Goコンパイラ内部で使用されるエラー報告関数。
u.h
,libc.h
,gg.h
,opt.h
などのヘッダファイル:- GoコンパイラのC言語部分で使用される内部ヘッダファイル。
D_INDIR
,D_AX
などの定数:- Goコンパイラの内部で、オペランドの種類やレジスタを識別するために使用される定数。