LLVMバックエンドの記述¶
はじめに¶
このドキュメントでは、LLVM中間表現(IR)を指定されたマシンまたは他の言語のコードに変換するコンパイラバックエンドの記述手法について説明します。特定のマシン向けのコードは、アセンブリコードまたはバイナリコード(JITコンパイラで使用可能)のいずれかの形式をとることができます。
LLVMのバックエンドは、X86、PowerPC、ARM、SPARCなど、いくつかのタイプのターゲットCPUの出力を作成できるターゲット非依存のコードジェネレータを備えています。バックエンドは、CellプロセッサのSPUまたはGPUをターゲットとするコードを生成して、計算カーネルの実行をサポートするためにも使用できます。
このドキュメントは、ダウンロードしたLLVMリリースのllvm/lib/Target
のサブディレクトリにある既存の例に焦点を当てています。特に、このドキュメントでは、SPARCターゲット用の静的コンパイラ(テキストアセンブリを出力するコンパイラ)を作成する例に焦点を当てています。これは、SPARCがRISC命令セットや簡単な呼び出し規約など、かなり標準的な特性を持っているためです。
対象読者¶
このドキュメントの対象読者は、特定のハードウェアまたはソフトウェアタゲットのコードを生成するためにLLVMバックエンドを記述する必要があるすべての人です。
前提となる知識¶
このドキュメントを読む前に、以下の必須ドキュメントを読んでおく必要があります。
LLVM言語リファレンスマニュアル — LLVMアセンブリ言語のリファレンスマニュアル。
LLVMターゲット非依存コードジェネレータ — LLVM内部表現を指定されたターゲットのマシンコードに変換するためのコンポーネント(クラスとコード生成アルゴリズム)のガイド。コード生成ステージの説明(命令選択、スケジューリングと形成、SSAベースの最適化、レジスタ割り当て、プロローグ/エピローグコードの挿入、後期マシンコードの最適化、コードの出力)に特に注意してください。
TableGenの概要 — LLVMコード生成をサポートするためにドメイン固有情報を管理するTableGen(
tblgen
)アプリケーションについて説明したドキュメント。TableGenは、ターゲット記述ファイル(.td
サフィックス)からの入力を処理し、コード生成に使用できるC++コードを生成します。LLVMパスの記述(レガシーPMバージョン) — アセンブリプリンタは
FunctionPass
であり、いくつかのSelectionDAG
処理手順も同様です。
このドキュメントのSPARCの例に従うには、SPARCアーキテクチャマニュアル、バージョン8のコピーを参照用に用意してください。ARM命令セットの詳細については、ARMアーキテクチャリファレンスマニュアルを参照してください。GNUアセンブラ形式(GAS
)の詳細については、特にアセンブリプリンタについては、Using Asを参照してください。“Using As”には、ターゲットマシン依存の機能のリストが含まれています。
基本手順¶
LLVM IRを指定されたターゲット(マシンまたは他の言語)のコードに変換するLLVM用コンパイラバックエンドを記述するには、以下の手順に従います。
ターゲットマシンの特性を記述する
TargetMachine
クラスのサブクラスを作成します。特定のTargetMachine
クラスとヘッダーファイルの既存の例をコピーします。たとえば、SparcTargetMachine.cpp
とSparcTargetMachine.h
から始めますが、ターゲットのファイル名を変更します。同様に、「Sparc
」を参照するコードをターゲットを参照するように変更します。ターゲットのレジスタセットを記述します。TableGenを使用して、ターゲット固有の
RegisterInfo.td
入力ファイルからレジスタ定義、レジスタエイリアス、およびレジスタクラスのコードを生成します。また、レジスタ割り当てに使用されるクラスレジスタファイルデータを表し、レジスタ間の相互作用も記述するTargetRegisterInfo
クラスのサブクラスの追加コードも記述する必要があります。ターゲットの命令セットを記述します。
TargetInstrFormats.td
とTargetInstrInfo.td
のターゲット固有バージョンから、ターゲット固有の命令のコードを生成するためにTableGenを使用します。ターゲットマシンでサポートされているマシン命令を表すTargetInstrInfo
クラスのサブクラスの追加コードを記述する必要があります。命令の有向非巡回グラフ(DAG)表現からネイティブのターゲット固有の命令へのLLVM IRの選択と変換について説明します。TableGenを使用して、
TargetInstrInfo.td
のターゲット固有バージョンに追加情報に基づいてパターンを一致させ、命令を選択するコードを生成します。パターンマッチングとDAGからDAGへの命令選択を実行するために、XXXISelDAGToDAG.cpp
(XXX
は特定のターゲットを識別する)のコードを記述します。また、SelectionDAGでネイティブにサポートされていない操作とデータ型を置換または削除するために、XXXISelLowering.cpp
にコードを記述します。LLVM IRをターゲットマシンのGAS形式に変換するアセンブリプリンタのコードを記述します。ターゲット固有バージョンの
TargetInstrInfo.td
で定義されている命令にアセンブリ文字列を追加する必要があります。また、LLVMからアセンブリへの変換を実行するAsmPrinter
のサブクラスと、TargetAsmInfo
の簡単なサブクラスのコードも記述する必要があります。必要に応じて、サブターゲット(つまり、機能が異なるバリアント)のサポートを追加します。
-mcpu=
および-mattr=
コマンドラインオプションを使用できるようにするTargetSubtarget
クラスのサブクラスのコードも記述する必要があります。必要に応じて、JITサポートを追加し、バイナリコードをメモリに直接出力するために使用されるマシンコードエミッタ(
TargetJITInfo
のサブクラス)を作成します。
.cpp
および.h
ファイルでは、最初にこれらのメソッドをスタブアップし、後で実装します。最初は、クラスに必要なプライベートメンバーと、サブクラス化する必要があるコンポーネントがわからない場合があります。
準備¶
実際にコンパイラバックエンドを作成するには、いくつかのファイルを作成および変更する必要があります。ここでは、絶対的な最小限について説明します。ただし、実際にLLVMターゲット非依存コードジェネレータを使用するには、LLVMターゲット非依存コードジェネレータドキュメントに記載されている手順を実行する必要があります。
まず、ターゲットに関連するすべてのファイルを保持するために、lib/Target
の下にサブディレクトリを作成する必要があります。ターゲットが「Dummy」と呼ばれる場合は、ディレクトリlib/Target/Dummy
を作成します。
この新しいディレクトリに、CMakeLists.txt
を作成します。別のターゲットの CMakeLists.txt
をコピーして変更するのが最も簡単です。少なくとも LLVM_TARGET_DEFINITIONS
変数を含める必要があります。ライブラリは LLVMDummy
という名前を付けることができます(例として、MIPS ターゲットを参照)。あるいは、ライブラリを LLVMDummyCodeGen
と LLVMDummyAsmPrinter
に分割することもできます。後者は lib/Target/Dummy
の下のサブディレクトリに実装する必要があります(例として、PowerPC ターゲットを参照)。
これらの2つの命名スキームは llvm-config
にハードコードされていることに注意してください。他の命名スキームを使用すると、llvm-config
が混乱し、llc
をリンクするときに(一見無関係に見える)多くのリンカエラーが発生します。
ターゲットが実際に何かを実行するようにするには、TargetMachine
のサブクラスを実装する必要があります。この実装は通常、lib/Target/DummyTargetMachine.cpp
ファイルに配置する必要がありますが、lib/Target
ディレクトリのどのファイルでもビルドされ、動作するはずです。LLVMのターゲット非依存コードジェネレータを使用するには、現在のすべてのマシンバックエンドが行っていること、つまり LLVMTargetMachine
のサブクラスを作成する必要があります。(ターゲットを最初から作成するには、TargetMachine
のサブクラスを作成します。)
LLVMが実際にターゲットをビルドしてリンクするようにするには、cmake
を -DLLVM_EXPERIMENTAL_TARGETS_TO_BUILD=Dummy
オプションを付けて実行する必要があります。これにより、すべてのターゲットのリストにターゲットを追加することなく、ターゲットがビルドされます。
ターゲットが安定したら、メインの CMakeLists.txt
にある LLVM_ALL_TARGETS
変数に追加できます。
ターゲットマシン¶
LLVMTargetMachine
は、LLVMターゲット非依存コードジェネレータで実装されたターゲットの基底クラスとして設計されています。LLVMTargetMachine
クラスは、さまざまな仮想メソッドを実装する具体的なターゲットクラスによって特殊化される必要があります。LLVMTargetMachine
は、include/llvm/Target/TargetMachine.h
で TargetMachine
のサブクラスとして定義されています。TargetMachine
クラスの実装(TargetMachine.cpp
)は、多数のコマンドラインオプションも処理します。
LLVMTargetMachine
の具体的なターゲット固有のサブクラスを作成するには、既存の TargetMachine
クラスとヘッダーをコピーすることから始めます。作成するファイルの名前は、特定のターゲットを反映するようにする必要があります。たとえば、SPARCターゲットの場合、ファイルの名前を SparcTargetMachine.h
と SparcTargetMachine.cpp
にします。
ターゲットマシン XXX
の場合、XXXTargetMachine
の実装は、ターゲットコンポーネントを表すオブジェクトを取得するためのアクセス方法を持つ必要があります。これらのメソッドは get*Info
という名前で、命令セット(getInstrInfo
)、レジスタセット(getRegisterInfo
)、スタックフレームレイアウト(getFrameInfo
)、および同様の情報を取得するためのものです。XXXTargetMachine
は、データ型のサイズやアラインメント要件など、ターゲット固有のデータ特性を持つオブジェクトにアクセスするために、getDataLayout
メソッドも実装する必要があります。
たとえば、SPARCターゲットの場合、ヘッダーファイル SparcTargetMachine.h
は、クラスメンバーを返すだけのいくつかの get*Info
および getDataLayout
メソッドのプロトタイプを宣言します。
namespace llvm {
class Module;
class SparcTargetMachine : public LLVMTargetMachine {
const DataLayout DataLayout; // Calculates type size & alignment
SparcSubtarget Subtarget;
SparcInstrInfo InstrInfo;
TargetFrameInfo FrameInfo;
protected:
virtual const TargetAsmInfo *createTargetAsmInfo() const;
public:
SparcTargetMachine(const Module &M, const std::string &FS);
virtual const SparcInstrInfo *getInstrInfo() const {return &InstrInfo; }
virtual const TargetFrameInfo *getFrameInfo() const {return &FrameInfo; }
virtual const TargetSubtarget *getSubtargetImpl() const{return &Subtarget; }
virtual const TargetRegisterInfo *getRegisterInfo() const {
return &InstrInfo.getRegisterInfo();
}
virtual const DataLayout *getDataLayout() const { return &DataLayout; }
// Pass Pipeline Configuration
virtual bool addInstSelector(PassManagerBase &PM, bool Fast);
virtual bool addPreEmitPass(PassManagerBase &PM, bool Fast);
};
} // end namespace llvm
getInstrInfo()
getRegisterInfo()
getFrameInfo()
getDataLayout()
getSubtargetImpl()
一部のターゲットでは、以下のメソッドもサポートする必要があります
getTargetLowering()
getJITInfo()
GPU などの一部のアーキテクチャは、任意のプログラムロケーションへのジャンプをサポートしておらず、マスクされた実行を使用して分岐を実装し、ループ本体の周囲の特殊な命令を使用してループを実装します。このようなハードウェアで処理されない還元不可能な制御フローを導入する CFG の変更を避けるため、ターゲットは初期化時に setRequiresStructuredCFG(true) を呼び出す必要があります。
さらに、XXXTargetMachine
コンストラクタは、ポインタサイズ、アラインメント、エンディアンなどの特性を含む、ターゲットマシンのデータレイアウトを決定する TargetDescription
文字列を指定する必要があります。たとえば、SparcTargetMachine
のコンストラクタには次が含まれています
SparcTargetMachine::SparcTargetMachine(const Module &M, const std::string &FS)
: DataLayout("E-p:32:32-f128:128:128"),
Subtarget(M, FS), InstrInfo(Subtarget),
FrameInfo(TargetFrameInfo::StackGrowsDown, 8, 0) {
}
ハイフンは TargetDescription
文字列の部分を区切ります。
文字列の大文字「
E
」は、ビッグエンディアン ターゲットデータモデルを示します。小文字の「e
」はリトルエンディアンを示します。「
p:
」の後には、ポインタ情報(サイズ、ABIアラインメント、優先アラインメント)が続きます。「p:
」の後に2つの数値のみが続く場合、最初の値はポインタサイズ、2番目の値はABIアラインメントと優先アラインメントの両方です。次に、数値型のアラインメントを示す文字:「
i
」、「f
」、「v
」、または「a
」(それぞれ、整数、浮動小数点数、ベクトル、または集約に対応)。「i
」、「v
」、または「a
」の後には、ABIアラインメントと優先アラインメントが続きます。「f
」の後には3つの値が続きます。最初の値はlong doubleのサイズ、次にABIアラインメント、次にABI優先アラインメントを示します。
ターゲットの登録¶
また、TargetRegistry
にターゲットを登録する必要があります。これは、他の LLVM ツールが実行時にターゲットを検索して使用できるようにするために使用するものです。TargetRegistry
は直接使用できますが、ほとんどのターゲットでは、作業を処理するヘルパーテンプレートがあります。
すべてのターゲットは、登録中にターゲットを表すために使用されるグローバル Target
オブジェクトを宣言する必要があります。次に、ターゲットの TargetInfo
ライブラリで、ターゲットはそのオブジェクトを定義し、RegisterTarget
テンプレートを使用してターゲットを登録する必要があります。たとえば、Sparc 登録コードは次のようになります
Target llvm::getTheSparcTarget();
extern "C" void LLVMInitializeSparcTargetInfo() {
RegisterTarget<Triple::sparc, /*HasJIT=*/false>
X(getTheSparcTarget(), "sparc", "Sparc");
}
これにより、TargetRegistry
は名前またはターゲットトリプルでターゲットを検索できます。さらに、ほとんどのターゲットは、個別のライブラリで利用可能な追加機能も登録します。これらの登録手順は別々です。これは、一部のクライアントがターゲットの一部のみをリンクする場合があるためです。たとえば、JITコードジェネレータはアセンブラプリンタの使用を必要としません。Sparcアセンブリプリンタを登録する例を次に示します
extern "C" void LLVMInitializeSparcAsmPrinter() {
RegisterAsmPrinter<SparcAsmPrinter> X(getTheSparcTarget());
}
詳細については、「llvm/Target/TargetRegistry.h」を参照してください。
レジスタセットとレジスタクラス¶
ターゲットマシンのレジスタファイルを表現する具体的なターゲット固有のクラスを記述する必要があります。このクラスは XXXRegisterInfo
と呼ばれ(XXX
はターゲットを識別します)、レジスタ割り当てに使用されるクラスレジスタファイルデータを表します。また、レジスタ間の相互作用についても説明します。
関連するレジスタを分類するために、レジスタクラスも定義する必要があります。一部の命令で同じように扱われるすべてのレジスタのグループに、レジスタクラスを追加する必要があります。典型的な例は、整数レジスタ、浮動小数点レジスタ、またはベクトルレジスタのレジスタクラスです。レジスタアロケータを使用すると、命令は指定されたレジスタクラスの任意のレジスタを使用して、同様の方法で命令を実行できます。レジスタクラスは、これらのセットから命令に仮想レジスタを割り当て、レジスタクラスにより、ターゲット非依存レジスタアロケータが実際のレジスタを自動的に選択できます。
レジスタ定義、レジスタエイリアス、レジスタクラスなど、レジスタのコードの多くは、TableGen によって XXXRegisterInfo.td
入力ファイルから生成され、XXXGenRegisterInfo.h.inc
および XXXGenRegisterInfo.inc
出力ファイルに配置されます。XXXRegisterInfo
の実装の一部のコードは、手動でコーディングする必要があります。
レジスタの定義¶
XXXRegisterInfo.td
ファイルは、通常、ターゲットマシンのレジスタ定義で始まります。Register
クラス(Target.td
で指定)は、各レジスタのオブジェクトを定義するために使用されます。指定された文字列 n
は、レジスタの Name
になります。基本的な Register
オブジェクトにはサブレジスタがなく、エイリアスも指定されていません。
class Register<string n> {
string Namespace = "";
string AsmName = n;
string Name = n;
int SpillSize = 0;
int SpillAlignment = 0;
list<Register> Aliases = [];
list<Register> SubRegs = [];
list<int> DwarfNumbers = [];
}
たとえば、X86RegisterInfo.td
ファイルには、Register
クラスを利用したレジスタ定義があります。例:
def AL : Register<"AL">, DwarfRegNum<[0, 0, 0]>;
これは、レジスタ AL
を定義し、gcc
、gdb
、またはデバッグ情報ライターがレジスタを識別するために使用する値(DwarfRegNum
を使用)を割り当てます。レジスタ AL
の場合、DwarfRegNum
は3つの異なるモードを表す3つの値の配列を取ります。最初の要素はX86-64用、2番目はX86-32の例外処理(EH)用、3番目は汎用です。 -1はgcc番号が未定義であることを示す特別なDwarf番号であり、-2はこのモードでレジスタ番号が無効であることを示します。
X86RegisterInfo.td
ファイルの先に説明した行から、TableGenは X86GenRegisterInfo.inc
ファイルに以下のコードを生成します。
static const unsigned GR8[] = { X86::AL, ... };
const unsigned AL_AliasSet[] = { X86::AX, X86::EAX, X86::RAX, 0 };
const TargetRegisterDesc RegisterDescriptors[] = {
...
{ "AL", "AL", AL_AliasSet, Empty_SubRegsSet, Empty_SubRegsSet, AL_SuperRegsSet }, ...
レジスタ情報ファイルから、TableGenは各レジスタに対して TargetRegisterDesc
オブジェクトを生成します。 TargetRegisterDesc
は include/llvm/Target/TargetRegisterInfo.h
に以下のフィールドで定義されています。
struct TargetRegisterDesc {
const char *AsmName; // Assembly language name for the register
const char *Name; // Printable name for the reg (for debugging)
const unsigned *AliasSet; // Register Alias Set
const unsigned *SubRegs; // Sub-register set
const unsigned *ImmSubRegs; // Immediate sub-register set
const unsigned *SuperRegs; // Super-register set
};
TableGenはターゲット記述ファイル全体(.td
)を使用して、レジスタのテキスト名(TargetRegisterDesc
の AsmName
フィールドと Name
フィールド)と、定義されたレジスタに対する他のレジスタの関係(他の TargetRegisterDesc
フィールド)を決定します。この例では、他の定義によりレジスタ「AX
」、「EAX
」、および「RAX
」が互いにエイリアスとして確立されるため、TableGenはこのレジスタエイリアスセットにヌル終端配列(AL_AliasSet
)を生成します。
Register
クラスは、より複雑なクラスの基底クラスとしてよく使用されます。 Target.td
では、Register
クラスは、SubRegs
リストにサブレジスタを指定する必要があるレジスタを定義するために使用される RegisterWithSubRegs
クラスの基底です。次に示すように。
class RegisterWithSubRegs<string n, list<Register> subregs> : Register<n> {
let SubRegs = subregs;
}
SparcRegisterInfo.td
では、SPARC用に追加のレジスタクラスが定義されています。 Register
のサブクラスである SparcReg
と、さらにそのサブクラスである Ri
、Rf
、Rd
です。 SPARCレジスタは5ビットのID番号で識別されます。これはこれらのサブクラスに共通の機能です。「let
」式を使用して、スーパークラスで最初に定義された値(Rd
クラスの SubRegs
フィールドなど)をオーバーライドすることに注意してください。
class SparcReg<string n> : Register<n> {
field bits<5> Num;
let Namespace = "SP";
}
// Ri - 32-bit integer registers
class Ri<bits<5> num, string n> :
SparcReg<n> {
let Num = num;
}
// Rf - 32-bit floating-point registers
class Rf<bits<5> num, string n> :
SparcReg<n> {
let Num = num;
}
// Rd - Slots in the FP register file for 64-bit floating-point values.
class Rd<bits<5> num, string n, list<Register> subregs> : SparcReg<n> {
let Num = num;
let SubRegs = subregs;
}
SparcRegisterInfo.td
ファイルには、以下のように、Register
のこれらのサブクラスを利用するレジスタ定義があります。
def G0 : Ri< 0, "G0">, DwarfRegNum<[0]>;
def G1 : Ri< 1, "G1">, DwarfRegNum<[1]>;
...
def F0 : Rf< 0, "F0">, DwarfRegNum<[32]>;
def F1 : Rf< 1, "F1">, DwarfRegNum<[33]>;
...
def D0 : Rd< 0, "F0", [F0, F1]>, DwarfRegNum<[32]>;
def D1 : Rd< 2, "F2", [F2, F3]>, DwarfRegNum<[34]>;
上記の最後の2つのレジスタ(D0
と D1
)は、単精度浮動小数点サブレジスタのペアのエイリアスである倍精度浮動小数点レジスタです。エイリアスに加えて、定義されたレジスタのサブレジスタとスーパーレジスタの関係は、レジスタの TargetRegisterDesc
のフィールドにあります。
レジスタクラスの定義¶
RegisterClass
クラス(Target.td
で指定)は、関連するレジスタのグループを表すオブジェクトを定義し、レジスタのデフォルトの割り当て順序も定義するために使用されます。 Target.td
を使用するターゲット記述ファイル XXXRegisterInfo.td
は、次のクラスを使用してレジスタクラスを構築できます。
class RegisterClass<string namespace,
list<ValueType> regTypes, int alignment, dag regList> {
string Namespace = namespace;
list<ValueType> RegTypes = regTypes;
int Size = 0; // spill size, in bits; zero lets tblgen pick the size
int Alignment = alignment;
// CopyCost is the cost of copying a value between two registers
// default value 1 means a single instruction
// A negative value means copying is extremely expensive or impossible
int CopyCost = 1;
dag MemberList = regList;
// for register classes that are subregisters of this class
list<RegisterClass> SubRegClassList = [];
code MethodProtos = [{}]; // to insert arbitrary code
code MethodBodies = [{}];
}
RegisterClass
を定義するには、次の4つの引数を使用します。
定義の最初の引数は、名前空間の名前です。
2番目の引数は、
include/llvm/CodeGen/ValueTypes.td
で定義されているValueType
レジスタ型値のリストです。定義された値には、整数型(i16
、i32
、ブール値のi1
など)、浮動小数点型(f32
、f64
)、ベクトル型(たとえば、8 x i16
ベクトルのv8i16
)が含まれます。RegisterClass
内のすべてのレジスタは同じValueType
を持つ必要がありますが、一部のレジスタはベクトルデータを異なる構成で格納する場合があります。たとえば、128ビットベクトルを処理できるレジスタは、16個の8ビット整数要素、8個の16ビット整数、4個の32ビット整数などを処理できる場合があります。RegisterClass
定義の3番目の引数は、レジスタがメモリに格納またはロードされるときに必要なアライメントを指定します。最後の引数
regList
は、このクラスに含まれるレジスタを指定します。代替の割り当て順序方法が指定されていない場合、regList
はレジスタアロケータで使用される割り当て順序も定義します。(add R0, R1, ...)
でレジスタを単にリストするだけでなく、より高度な集合演算子も使用できます。詳細については、include/llvm/Target/Target.td
を参照してください。
SparcRegisterInfo.td
では、3つの RegisterClass
オブジェクトが定義されています。 FPRegs
、DFPRegs
、および IntRegs
です。3つすべてのレジスタクラスで、最初の引数は文字列「SP
」で名前空間を定義します。 FPRegs
は32個の単精度浮動小数点レジスタ(F0
から F31
)のグループを定義します。 DFPRegs
は16個の倍精度レジスタ(D0-D15
)のグループを定義します。
// F0, F1, F2, ..., F31
def FPRegs : RegisterClass<"SP", [f32], 32, (sequence "F%u", 0, 31)>;
def DFPRegs : RegisterClass<"SP", [f64], 64,
(add D0, D1, D2, D3, D4, D5, D6, D7, D8,
D9, D10, D11, D12, D13, D14, D15)>;
def IntRegs : RegisterClass<"SP", [i32], 32,
(add L0, L1, L2, L3, L4, L5, L6, L7,
I0, I1, I2, I3, I4, I5,
O0, O1, O2, O3, O4, O5, O7,
G1,
// Non-allocatable regs:
G2, G3, G4,
O6, // stack ptr
I6, // frame ptr
I7, // return address
G0, // constant zero
G5, G6, G7 // reserved for kernel
)>;
TableGenで SparcRegisterInfo.td
を使用すると、作成する他のソースコードに含めることを目的としたいくつかの出力ファイルが生成されます。 SparcRegisterInfo.td
は SparcGenRegisterInfo.h.inc
を生成します。これは、作成するSPARCレジスタ実装の実装のヘッダーファイル(SparcRegisterInfo.h
)に含める必要があります。 SparcGenRegisterInfo.h.inc
では、TargetRegisterInfo
を基底として使用する SparcGenRegisterInfo
と呼ばれる新しい構造体が定義されています。また、定義されたレジスタクラスに基づいて、DFPRegsClass
、FPRegsClass
、および IntRegsClass
という型を指定します。
SparcRegisterInfo.td
は SparcGenRegisterInfo.inc
も生成します。これは、SPARCレジスタ実装である SparcRegisterInfo.cpp
の下部に含まれています。以下のコードは、生成された整数レジスタと関連するレジスタクラスのみを示しています。 IntRegs
内のレジスタの順序は、ターゲット記述ファイルの IntRegs
の定義の順序を反映しています。
// IntRegs Register Class...
static const unsigned IntRegs[] = {
SP::L0, SP::L1, SP::L2, SP::L3, SP::L4, SP::L5,
SP::L6, SP::L7, SP::I0, SP::I1, SP::I2, SP::I3,
SP::I4, SP::I5, SP::O0, SP::O1, SP::O2, SP::O3,
SP::O4, SP::O5, SP::O7, SP::G1, SP::G2, SP::G3,
SP::G4, SP::O6, SP::I6, SP::I7, SP::G0, SP::G5,
SP::G6, SP::G7,
};
// IntRegsVTs Register Class Value Types...
static const MVT::ValueType IntRegsVTs[] = {
MVT::i32, MVT::Other
};
namespace SP { // Register class instances
DFPRegsClass DFPRegsRegClass;
FPRegsClass FPRegsRegClass;
IntRegsClass IntRegsRegClass;
...
// IntRegs Sub-register Classes...
static const TargetRegisterClass* const IntRegsSubRegClasses [] = {
NULL
};
...
// IntRegs Super-register Classes..
static const TargetRegisterClass* const IntRegsSuperRegClasses [] = {
NULL
};
...
// IntRegs Register Class sub-classes...
static const TargetRegisterClass* const IntRegsSubclasses [] = {
NULL
};
...
// IntRegs Register Class super-classes...
static const TargetRegisterClass* const IntRegsSuperclasses [] = {
NULL
};
IntRegsClass::IntRegsClass() : TargetRegisterClass(IntRegsRegClassID,
IntRegsVTs, IntRegsSubclasses, IntRegsSuperclasses, IntRegsSubRegClasses,
IntRegsSuperRegClasses, 4, 4, 1, IntRegs, IntRegs + 32) {}
}
レジスタアロケータは予約済みレジスタの使用を回避し、呼び出し先保存レジスタはすべての揮発性レジスタが使用されるまで使用されません。これは通常は十分ですが、場合によってはカスタム割り当て順序を提供する必要がある場合があります。
TargetRegisterInfo
のサブクラスを実装する¶
最後の手順は、TargetRegisterInfo.h
で説明されているインターフェースを実装する XXXRegisterInfo
の部分をハンドコーディングすることです(TargetRegisterInfoクラス を参照)。これらの関数は、オーバーライドされない限り、0
、NULL
、または false
を返します。 SparcRegisterInfo.cpp
のSPARC実装でオーバーライドされる関数のリストを以下に示します。
getCalleeSavedRegs
— 呼び出し先保存スタックフレームオフセットの順序で、呼び出し先保存レジスタのリストを返します。getReservedRegs
— 物理レジスタ番号でインデックス付けされたビットセットを返し、特定のレジスタが使用不可かどうかを示します。hasFP
— 関数が専用のフレームポインタレジスタを持つ必要があるかどうかを示すブール値を返します。eliminateCallFramePseudoInstr
— コールフレームのセットアップまたは破棄疑似命令が使用されている場合、これを呼び出して削除できます。eliminateFrameIndex
— 抽象フレームインデックスを、それらを使用する可能性のある命令から削除します。emitPrologue
— 関数にプロローグコードを挿入します。emitEpilogue
— 関数にエピローグコードを挿入します。
命令セット¶
コード生成の初期段階では、LLVM IRコードは、ターゲット命令を含む`SDNode`クラスのインスタンスであるノードを持つ`SelectionDAG`に変換されます。`SDNode`は、オペコード、オペランド、型要件、および操作プロパティを持ちます。たとえば、操作が可換であるか、操作がメモリからロードするかどうかなどです。さまざまな操作ノードタイプは、`include/llvm/CodeGen/SelectionDAGNodes.h`ファイル(`ISD`名前空間の`NodeType`列挙型の値)に記述されています。
TableGenは、以下のターゲット記述(`.td`)入力ファイルを使用して、命令定義のためのコードの大部分を生成します。
`Target.td` — `Instruction`、`Operand`、`InstrInfo`、およびその他の基本クラスが定義されている場所です。
`TargetSelectionDAG.td` — `SelectionDAG`命令選択ジェネレータによって使用され、`SDTC*`クラス(選択DAG型制約)、`SelectionDAG`ノードの定義(`imm`、`cond`、`bb`、`add`、`fadd`、`sub`など)、およびパターンサポート(`Pattern`、`Pat`、`PatFrag`、`PatLeaf`、`ComplexPattern`)が含まれています。
`XXXInstrFormats.td` — ターゲット固有の命令定義のためのパターン。
`XXXInstrInfo.td` — 命令テンプレート、条件コード、および命令セットの命令のターゲット固有の定義。アーキテクチャの変更に合わせて、異なるファイル名が使用される場合があります。たとえば、SSE命令を持つPentiumの場合、このファイルは`X86InstrSSE.td`であり、MMXを持つPentiumの場合、このファイルは`X86InstrMMX.td`です。
ターゲット固有の`XXX.td`ファイルもあります。ここで、`XXX`はターゲットの名前です。`XXX.td`ファイルには他の`.td`入力ファイルが含まれていますが、その内容はサブターゲットにとってのみ直接重要です。
ターゲットマシンでサポートされているマシン命令を表す具体的なターゲット固有クラス`XXXInstrInfo`を記述する必要があります。`XXXInstrInfo`には、それぞれが1つの命令を記述する`XXXInstrDescriptor`オブジェクトの配列が含まれています。命令記述子は以下を定義します。
オペコードニーモニック
オペランドの数
暗黙的なレジスタ定義と使用のリスト
ターゲットに依存しないプロパティ(メモリアクセス、可換性など)
ターゲット固有のフラグ
Instructionクラス(`Target.td`で定義)は、主に、より複雑な命令クラスのベースとして使用されます。
class Instruction {
string Namespace = "";
dag OutOperandList; // A dag containing the MI def operand list.
dag InOperandList; // A dag containing the MI use operand list.
string AsmString = ""; // The .s format to print the instruction with.
list<dag> Pattern; // Set to the DAG pattern for this instruction.
list<Register> Uses = [];
list<Register> Defs = [];
list<Predicate> Predicates = []; // predicates turned into isel match code
... remainder not shown for space ...
}
`SelectionDAG`ノード(`SDNode`)には、`XXXInstrInfo.td`で定義されているターゲット固有の命令を表すオブジェクトが含まれている必要があります。命令オブジェクトは、ターゲットマシンのアーキテクチャマニュアル(SPARCターゲットのSPARC Architecture Manualなど)の命令を表す必要があります。
アーキテクチャマニュアルの単一の命令は、多くの場合、そのオペランドに応じて、複数のターゲット命令としてモデル化されます。たとえば、マニュアルでは、レジスタまたは即値オペランドをとる加算命令について説明している場合があります。LLVMターゲットは、これを`ADDri`と`ADDrr`という2つの命令でモデル化できます。
各命令カテゴリのクラスを定義し、各オペコードを、オペコードと拡張オペコードの固定バイナリエンコーディングなどの適切なパラメータを持つカテゴリのサブクラスとして定義する必要があります。レジスタビットを、それらがエンコードされている命令のビットにマップする必要があります(JITの場合)。また、自動アセンブリプリンタが使用されたときに命令をどのように印刷するかを指定する必要があります。
SPARC Architecture Manual, Version 8で説明されているように、命令には3つの主要な32ビット形式があります。形式1は`CALL`命令専用です。形式2は、条件コードと`SETHI`(レジスタの上位ビットを設定)命令の分岐用です。形式3は他の命令用です。
これらの各形式は、`SparcInstrFormat.td`に対応するクラスを持っています。`InstSP`は、他の命令クラスの基本クラスです。より正確な形式のために追加の基本クラスが指定されています。たとえば、`SparcInstrFormat.td`では、`F2_1`は`SETHI`用であり、`F2_2`は分岐用です。他に3つの基本クラスがあります。レジスタ/レジスタ操作用の`F3_1`、レジスタ/即値操作用の`F3_2`、および浮動小数点操作用の`F3_3`です。`SparcInstrInfo.td`は、合成SPARC命令の基本クラス`Pseudo`も追加します。
`SparcInstrInfo.td`は、主にSPARCターゲットのオペランドと命令の定義で構成されています。`SparcInstrInfo.td`では、次のターゲット記述ファイルエントリ`LDrr`は、メモリ アドレスからレジスタへのWord(`LD` SPARC オペコード)のLoad Integer命令を定義します。最初のパラメータである値3(2進数で`11`)は、このカテゴリの操作の操作値です。2 番目のパラメータ(2進数で`000000`)は、`LD`/Load Word の特定の操作値です。3 番目のパラメータは出力先であり、レジスタ オペランドであり、ターゲット記述ファイル `Register`(`IntRegs`)で定義されています。
def LDrr : F3_1 <3, 0b000000, (outs IntRegs:$rd), (ins (MEMrr $rs1, $rs2):$addr),
"ld [$addr], $dst",
[(set i32:$dst, (load ADDRrr:$addr))]>;
4 番目のパラメータは入力ソースであり、`SparcInstrInfo.td` の前のほうで定義されているアドレス オペランド `MEMrr` を使用します。
def MEMrr : Operand<i32> {
let PrintMethod = "printMemOperand";
let MIOperandInfo = (ops IntRegs, IntRegs);
}
5 番目のパラメータは、アセンブリ プリンタによって使用される文字列であり、アセンブリ プリンタ インターフェイスが実装されるまでは空の文字列のままにすることができます。6 番目と最後のパラメータは、「LLVM ターゲット非依存コード ジェネレータ」で説明されている SelectionDAG 選択フェーズ中に命令を照合するために使用されるパターンです。このパラメータについては、次のセクション「命令セレクタ」で詳しく説明します。
命令クラスの定義は、異なるオペランド型に対してオーバーロードされないため、レジスタ、メモリ、または即値オペランドに対して、命令の別々のバージョンが必要です。たとえば、即値オペランドからレジスタにWordのLoad Integer命令を実行するには、次の命令クラスが定義されます。
def LDri : F3_2 <3, 0b000000, (outs IntRegs:$rd), (ins (MEMri $rs1, $simm13):$addr),
"ld [$addr], $dst",
[(set i32:$rd, (load ADDRri:$addr))]>;
これほど多くの類似した命令の定義を作成するには、多くのカットアンドペーストが必要になる場合があります。`.td`ファイルでは、`multiclass`ディレクティブを使用すると、テンプレートを作成して、一度に複数の命令クラスを定義できます(`defm`ディレクティブを使用)。たとえば、`SparcInstrInfo.td`では、`multiclass`パターン`F3_12`は、`F3_12`が呼び出されるたびに2つの命令クラスを作成するように定義されています。
multiclass F3_12 <string OpcStr, bits<6> Op3Val, SDNode OpNode> {
def rr : F3_1 <2, Op3Val,
(outs IntRegs:$rd), (ins IntRegs:$rs1, IntRegs:$rs1),
!strconcat(OpcStr, " $rs1, $rs2, $rd"),
[(set i32:$rd, (OpNode i32:$rs1, i32:$rs2))]>;
def ri : F3_2 <2, Op3Val,
(outs IntRegs:$rd), (ins IntRegs:$rs1, i32imm:$simm13),
!strconcat(OpcStr, " $rs1, $simm13, $rd"),
[(set i32:$rd, (OpNode i32:$rs1, simm13:$simm13))]>;
}
したがって、以下に示すように、`XOR`および`ADD`命令に対して`defm`ディレクティブが使用されると、4つの命令オブジェクト`XORrr`、`XORri`、`ADDrr`、および`ADDri`が作成されます。
defm XOR : F3_12<"xor", 0b000011, xor>;
defm ADD : F3_12<"add", 0b000000, add>;
`SparcInstrInfo.td`には、分岐命令によって参照される条件コードの定義も含まれています。`SparcInstrInfo.td`の次の定義は、SPARC条件コードのビット位置を示しています。たとえば、10番目のビットは整数の「より大きい」条件を表し、22番目のビットは浮動小数点数の「より大きい」条件を表します。
def ICC_NE : ICC_VAL< 9>; // Not Equal
def ICC_E : ICC_VAL< 1>; // Equal
def ICC_G : ICC_VAL<10>; // Greater
...
def FCC_U : FCC_VAL<23>; // Unordered
def FCC_G : FCC_VAL<22>; // Greater
def FCC_UG : FCC_VAL<21>; // Unordered or Greater
...
(`Sparc.h`は、同じSPARC条件コードに対応する列挙型も定義していることに注意してください。`Sparc.h`の値が`SparcInstrInfo.td`の値に対応していることを確認するように注意する必要があります。つまり、`SPCC::ICC_NE = 9`、`SPCC::FCC_U = 23`などです。)
命令オペランドのマッピング¶
コードジェネレータのバックエンドは、命令オペランドを命令のフィールドにマッピングします。命令エンコーディング Inst
のビットが具体的な値を持たないフィールドに割り当てられるときは常に、outs
または ins
リストのオペランドに一致する名前があることが期待されます。このオペランドは、未定義のフィールドを埋め込みます。たとえば、Sparcターゲットは、XNORrr
命令を、3つのオペランド(出力 $rd
、入力 $rs1
、および $rs2
)を持つ F3_1
形式の命令として定義します。
def XNORrr : F3_1<2, 0b000111,
(outs IntRegs:$rd), (ins IntRegs:$rs1, IntRegs:$rs2),
"xnor $rs1, $rs2, $rd",
[(set i32:$rd, (not (xor i32:$rs1, i32:$rs2)))]>;
SparcInstrFormats.td
の命令テンプレートは、F3_1
の基底クラスが InstSP
であることを示しています。
class InstSP<dag outs, dag ins, string asmstr, list<dag> pattern> : Instruction {
field bits<32> Inst;
let Namespace = "SP";
bits<2> op;
let Inst{31-30} = op;
dag OutOperandList = outs;
dag InOperandList = ins;
let AsmString = asmstr;
let Pattern = pattern;
}
InstSP
は op
フィールドを定義し、それを使用して命令のビット30と31を定義しますが、値を割り当てません。
class F3<dag outs, dag ins, string asmstr, list<dag> pattern>
: InstSP<outs, ins, asmstr, pattern> {
bits<5> rd;
bits<6> op3;
bits<5> rs1;
let op{1} = 1; // Op = 2 or 3
let Inst{29-25} = rd;
let Inst{24-19} = op3;
let Inst{18-14} = rs1;
}
F3
は rd
、op3
、および rs1
フィールドを定義し、それらを命令で使用しますが、やはり値を割り当てません。
class F3_1<bits<2> opVal, bits<6> op3val, dag outs, dag ins,
string asmstr, list<dag> pattern> : F3<outs, ins, asmstr, pattern> {
bits<8> asi = 0; // asi not currently used
bits<5> rs2;
let op = opVal;
let op3 = op3val;
let Inst{13} = 0; // i field = 0
let Inst{12-5} = asi; // address space identifier
let Inst{4-0} = rs2;
}
F3_1
は op
および op3
フィールドに値を割り当て、rs2
フィールドを定義します。したがって、F3_1
形式の命令は、命令エンコーディングを完全に指定するために、rd
、rs1
、および rs2
の定義を必要とします。
XNORrr
命令は、OutOperandListとInOperandListでこれら3つのオペランドを提供し、対応するフィールドにバインドして、命令エンコーディングを完成させます。
一部の命令では、単一のオペランドにサブオペランドが含まれている場合があります。前述のように、LDrr
命令は、MEMrr
型の入力オペランドを使用します。このオペランド型には、MIOperandInfo
値によって (ops IntRegs, IntRegs)
として定義された2つのレジスタサブオペランドが含まれています。
def LDrr : F3_1 <3, 0b000000, (outs IntRegs:$rd), (ins (MEMrr $rs1, $rs2):$addr),
"ld [$addr], $dst",
[(set i32:$dst, (load ADDRrr:$addr))]>;
この命令も F3_1
形式であるため、rd
、rs1
、および rs2
という名前のオペランドも期待されます。これを可能にするために、複合オペランドはオプションで各サブオペランドに名前を付けることができます。この例では、MEMrr
の最初のサブオペランドは $rs1
、2番目は $rs2
という名前が付けられ、オペランド全体にも $addr
という名前が付けられています。
特定の命令が命令形式で定義されているすべてのオペランドを使用しない場合、定数値が1つまたはすべてにバインドされる場合があります。たとえば、RDASR
命令は単一のレジスタオペランドのみをとるため、rs2
に定数ゼロを割り当てます
let rs2 = 0 in
def RDASR : F3_1<2, 0b101000,
(outs IntRegs:$rd), (ins ASRRegs:$rs1),
"rd $rs1, $rd", []>;
命令オペランド名マッピング¶
TableGenは、getNamedOperandIdx()という関数も生成します。これは、TableGen名に基づいてMachineInstr内のオペランドのインデックスを検索するために使用できます。命令のTableGen定義でUseNamedOperandTableビットを設定すると、すべてのオペランドがllvm :: XXX :: OpName名前空間の列挙型に追加され、OperandMapテーブルにもエントリが追加されます。これはgetNamedOperandIdx()を使用してクエリできます
int DstIndex = SP::getNamedOperandIdx(SP::XNORrr, SP::OpName::dst); // => 0
int BIndex = SP::getNamedOperandIdx(SP::XNORrr, SP::OpName::b); // => 1
int CIndex = SP::getNamedOperandIdx(SP::XNORrr, SP::OpName::c); // => 2
int DIndex = SP::getNamedOperandIdx(SP::XNORrr, SP::OpName::d); // => -1
...
OpName列挙型のエントリはTableGen定義から逐語的に取得されるため、小文字の名前のオペランドは列挙型に小文字のエントリを持ちます。
バックエンドにgetNamedOperandIdx()関数を含めるには、XXXInstrInfo.cppとXXXInstrInfo.hでいくつかのプリプロセッサマクロを定義する必要があります。例えば
XXXInstrInfo.cpp
#define GET_INSTRINFO_NAMED_OPS // For getNamedOperandIdx() function
#include "XXXGenInstrInfo.inc"
XXXInstrInfo.h
#define GET_INSTRINFO_OPERAND_ENUM // For OpName enum
#include "XXXGenInstrInfo.inc"
namespace XXX {
int16_t getNamedOperandIdx(uint16_t Opcode, uint16_t NamedIndex);
} // End namespace XXX
命令オペランドタイプ¶
TableGenは、llvm :: XXX :: OpTypes名前空間で、バックエンドで定義されたすべての名前付きオペランドタイプで構成される列挙型も生成します。いくつかの一般的な即値オペランドタイプ(たとえば、i8、i32、i64、f32、f64)は、include/llvm/Target/Target.td
ですべてのターゲットに対して定義されており、各ターゲットのOpTypes列挙型で使用できます。また、名前付きオペランドタイプのみが列挙型に表示されます。匿名タイプは無視されます。たとえば、X86バックエンドは、分岐ターゲットオペランドを表すTableGen Operand
クラスの両方のインスタンスであるbrtarget
とbrtarget8
を定義します
def brtarget : Operand<OtherVT>;
def brtarget8 : Operand<OtherVT>;
これは次のようになります
namespace X86 {
namespace OpTypes {
enum OperandType {
...
brtarget,
brtarget8,
...
i32imm,
i64imm,
...
OPERAND_TYPE_LIST_END
} // End namespace OpTypes
} // End namespace X86
典型的なTableGenの方法では、列挙型を使用するには、プリプロセッサマクロを定義する必要があります
#define GET_INSTRINFO_OPERAND_TYPES_ENUM // For OpTypes enum
#include "XXXGenInstrInfo.inc"
命令スケジューリング¶
命令イテラシーは、MCDesc :: getSchedClass()を使用してクエリできます。値は、XXXGenInstrInfo.incでTableGenによって生成されたllvm :: XXX :: Sched名前空間の列挙型によって名前を付けることができます。スケジュールクラスの名前は、XXXSchedule.tdで指定されている名前と同じで、デフォルトのNoItineraryクラスが追加されています。
スケジュールモデルは、CodeGenSchedModels
クラスを使用して、SubtargetEmitterによってTableGenによって生成されます。これは、マシンリソースの使用を指定するイテラシーメソッドとは異なります。ツールutils/schedcover.py
を使用して、スケジュールモデルの説明でどの命令がカバーされ、どの命令がカバーされていないかを判断できます。最初のステップは、以下の命令を使用して出力ファイルを作成することです。次に、出力ファイルでschedcover.py
を実行します
$ <src>/utils/schedcover.py <build>/lib/Target/AArch64/tblGenSubtarget.with
instruction, default, CortexA53Model, CortexA57Model, CycloneModel, ExynosM3Model, FalkorModel, KryoModel, ThunderX2T99Model, ThunderXT8XModel
ABSv16i8, WriteV, , , CyWriteV3, M3WriteNMISC1, FalkorWr_2VXVY_2cyc, KryoWrite_2cyc_XY_XY_150ln, ,
ABSv1i64, WriteV, , , CyWriteV3, M3WriteNMISC1, FalkorWr_1VXVY_2cyc, KryoWrite_2cyc_XY_noRSV_67ln, ,
...
スケジュールモデルの生成からのデバッグ出力をキャプチャするには、適切なターゲットディレクトリに変更し、次のコマンドを使用します。subtarget-emitter
デバッグオプション付きのコマンド
$ <build>/bin/llvm-tblgen -debug-only=subtarget-emitter -gen-subtarget \
-I <src>/lib/Target/<target> -I <src>/include \
-I <src>/lib/Target <src>/lib/Target/<target>/<target>.td \
-o <build>/lib/Target/<target>/<target>GenSubtargetInfo.inc.tmp \
> tblGenSubtarget.dbg 2>&1
ここで、<build>
はビルドディレクトリ、src
はソースディレクトリ、<target>
はターゲットの名前です。上記のコマンドが必要なものであることを再確認するには、次を使用してビルドから正確なTableGenコマンドをキャプチャできます。
$ VERBOSE=1 make ...
出力でllvm-tblgen
コマンドを検索します。
命令関係マッピング¶
このTableGen機能は、命令を相互に関連付けるために使用されます。複数の命令形式があり、命令選択後にそれらを切り替える必要がある場合に特に役立ちます。この機能全体は、ターゲット固有の命令セットに従ってXXXInstrInfo.td
ファイルで定義できる関係モデルによって駆動されます。関係モデルは、InstrMapping
クラスをベースとして定義されます。 TableGenはすべてのモデルを解析し、指定された情報を使用して命令関係マップを生成します。関係マップは、XXXGenInstrInfo.inc
ファイルにテーブルとして、それらをクエリする関数とともに出力されます。この機能の使用方法の詳細については、命令マッピングの使用方法を参照してください。
TargetInstrInfo
のサブクラスを実装する¶
最後のステップは、TargetInstrInfo.h
で説明されているインターフェースを実装するXXXInstrInfo
の部分を手動でコーディングすることです(TargetInstrInfoクラスを参照)。これらの関数は、オーバーライドされない限り、0
またはブール値を返すか、アサートします。 SparcInstrInfo.cpp
のSPARC実装でオーバーライドされる関数のリストを次に示します。
isLoadFromStackSlot
— 指定されたマシン命令がスタックスロットからの直接ロードである場合、宛先レジスタ番号とスタックスロットのFrameIndex
を返します。isStoreToStackSlot
— 指定されたマシン命令がスタックスロットへの直接ストアである場合、宛先レジスタ番号とスタックスロットのFrameIndex
を返します。copyPhysReg
— 物理レジスタのペア間で値をコピーします。storeRegToStackSlot
— レジスタ値をスタックスロットに保存します。loadRegFromStackSlot
— スタックスロットからレジスタ値をロードします。storeRegToAddr
— レジスタ値をメモリに保存します。loadRegFromAddr
— メモリからレジスタ値をロードします。foldMemoryOperand
— 指定されたオペランドのロードまたはストア命令のいずれかの命令を組み合わせようとします。
分岐の折りたたみとIf変換¶
命令を組み合わせたり、到達できない命令を削除したりすることで、パフォーマンスを向上させることができます。`XXXInstrInfo` の `analyzeBranch` メソッドを実装して、条件付き命令を調べ、不要な命令を削除することができます。`analyzeBranch` は、マシン基本ブロック (MBB) の末尾を見て、分岐フォールディングや if 変換などの改善の機会を探します。`BranchFolder` と `IfConverter` マシン関数パス (`lib/CodeGen` ディレクトリのソースファイル `BranchFolding.cpp` と `IfConversion.cpp` を参照) は、`analyzeBranch` を呼び出して、命令を表す制御フローグラフを改善します。
独自の `analyzeBranch` 実装のモデルとして、ARM、Alpha、および X86 の `analyzeBranch` の実装例をいくつか調べることができます。SPARC は有用な `analyzeBranch` を実装していないため、以下に ARM ターゲットの実装を示します。
`analyzeBranch` はブール値を返し、4 つのパラメーターを取ります。
`MachineBasicBlock &MBB` — 調べる対象の入力ブロック。
`MachineBasicBlock *&TBB` — 返される宛先ブロック。真と評価される条件分岐の場合、`TBB` は宛先です。
`MachineBasicBlock *&FBB` — 偽と評価される条件分岐の場合、`FBB` は宛先として返されます。
`std::vector<MachineOperand> &Cond` — 条件分岐の条件を評価するためのオペランドのリスト。
最も単純なケースでは、ブロックが分岐なしで終了する場合、後続のブロックにフォールスルーします。`TBB` または `FBB` のいずれにも宛先ブロックは指定されないため、両方のパラメーターは `NULL` を返します。`analyzeBranch` の開始部分 (ARM ターゲットの以下のコードを参照) は、関数パラメーターと最も単純なケースのコードを示しています。
bool ARMInstrInfo::analyzeBranch(MachineBasicBlock &MBB,
MachineBasicBlock *&TBB,
MachineBasicBlock *&FBB,
std::vector<MachineOperand> &Cond) const
{
MachineBasicBlock::iterator I = MBB.end();
if (I == MBB.begin() || !isUnpredicatedTerminator(--I))
return false;
ブロックが単一の無条件分岐命令で終了する場合、`analyzeBranch` (以下に示す) はその分岐の宛先を `TBB` パラメーターで返す必要があります。
if (LastOpc == ARM::B || LastOpc == ARM::tB) {
TBB = LastInst->getOperand(0).getMBB();
return false;
}
ブロックが 2 つの無条件分岐で終了する場合、2 番目の分岐には到達しません。この状況では、以下に示すように、最後の分岐命令を削除し、最後から 2 番目の分岐を `TBB` パラメーターで返します。
if ((SecondLastOpc == ARM::B || SecondLastOpc == ARM::tB) &&
(LastOpc == ARM::B || LastOpc == ARM::tB)) {
TBB = SecondLastInst->getOperand(0).getMBB();
I = LastInst;
I->eraseFromParent();
return false;
}
ブロックは、条件が偽と評価された場合に後続ブロックにフォールスルーする単一の条件分岐命令で終了する場合があります。この場合、`analyzeBranch` (以下に示す) は、その条件分岐の宛先を `TBB` パラメーターで返し、条件を評価するためのオペランドのリストを `Cond` パラメーターで返す必要があります。
if (LastOpc == ARM::Bcc || LastOpc == ARM::tBcc) {
// Block ends with fall-through condbranch.
TBB = LastInst->getOperand(0).getMBB();
Cond.push_back(LastInst->getOperand(1));
Cond.push_back(LastInst->getOperand(2));
return false;
}
ブロックが条件分岐とそれに続く無条件分岐の両方で終了する場合、`analyzeBranch` (以下に示す) は、条件分岐の宛先 (「`true`」の条件評価に対応すると仮定) を `TBB` パラメーターで返し、無条件分岐の宛先を `FBB` パラメーター (「`false`」の条件評価に対応) で返す必要があります。条件を評価するためのオペランドのリストは、`Cond` パラメーターで返す必要があります。
unsigned SecondLastOpc = SecondLastInst->getOpcode();
if ((SecondLastOpc == ARM::Bcc && LastOpc == ARM::B) ||
(SecondLastOpc == ARM::tBcc && LastOpc == ARM::tB)) {
TBB = SecondLastInst->getOperand(0).getMBB();
Cond.push_back(SecondLastInst->getOperand(1));
Cond.push_back(SecondLastInst->getOperand(2));
FBB = LastInst->getOperand(0).getMBB();
return false;
}
最後の 2 つのケース (単一の条件分岐で終了する場合、または 1 つの条件分岐と 1 つの無条件分岐で終了する場合)、`Cond` パラメーターで返されるオペランドは、他の命令のメソッドに渡して新しい分岐を作成したり、他の操作を実行したりできます。`analyzeBranch` の実装には、後続の操作を管理するためのヘルパーメソッド `removeBranch` と `insertBranch` が必要です。
`analyzeBranch` は、ほとんどの場合、成功を示す false を返す必要があります。`analyzeBranch` は、メソッドが何をすべきかわからない場合、たとえば、ブロックに 3 つの終端分岐がある場合にのみ true を返す必要があります。`analyzeBranch` は、間接分岐など、処理できないターミネーターに遭遇した場合に true を返す場合があります。
命令セレクタ¶
LLVM は `SelectionDAG` を使用して LLVM IR 命令を表し、`SelectionDAG` のノードは理想的にはネイティブターゲット命令を表します。コード生成中に、命令選択パスが実行され、ネイティブではない DAG 命令がネイティブターゲット固有の命令に変換されます。`XXXISelDAGToDAG.cpp` で記述されているパスは、パターンを照合し、DAG から DAG への命令選択を実行するために使用されます。必要に応じて、分岐命令に対して同様の DAG から DAG への操作を実行するパスを ( `XXXBranchSelector.cpp` に) 定義できます。その後、`XXXISelLowering.cpp` のコードは、`SelectionDAG` でネイティブでサポートされていない操作とデータ型を置き換えたり削除したりします (合法化)。
TableGen は、次のターゲット記述入力ファイルを使用して、命令選択のコードを生成します。
`XXXInstrInfo.td` — ターゲット固有の命令セットの命令の定義が含まれており、`XXXISelDAGToDAG.cpp` に含まれる `XXXGenDAGISel.inc` を生成します。
`XXXCallingConv.td` — ターゲットアーキテクチャの呼び出しと戻り値の規則が含まれており、`XXXISelLowering.cpp` に含まれる `XXXGenCallingConv.inc` を生成します。
命令選択パスの実装には、`FunctionPass` クラスまたは `FunctionPass` のサブクラスを宣言するヘッダーを含める必要があります。`XXXTargetMachine.cpp` では、Pass Manager (PM) は各命令選択パスを実行するパスのキューに追加する必要があります。
LLVM 静的コンパイラ (`llc`) は、DAG の内容を視覚化するのに最適なツールです。特定の処理フェーズの前または後に `SelectionDAG` を表示するには、SelectionDAG 命令選択プロセス で説明されている `llc` のコマンドラインオプションを使用します。
命令セレクタの動作を記述するには、LLVM コードを `SelectionDAG` に Lowering するためのパターンを、`XXXInstrInfo.td` の命令定義の最後のパラメーターとして追加する必要があります。たとえば、`SparcInstrInfo.td` では、このエントリはレジスタストア操作を定義し、最後のパラメーターはストア DAG 演算子を持つパターンを記述します。
def STrr : F3_1< 3, 0b000100, (outs), (ins MEMrr:$addr, IntRegs:$src),
"st $src, [$addr]", [(store i32:$src, ADDRrr:$addr)]>;
`ADDRrr` は、`SparcInstrInfo.td` でも定義されているメモリモードです。
def ADDRrr : ComplexPattern<i32, 2, "SelectADDRrr", [], []>;
`ADDRrr` の定義は、命令セレクタの実装 ( `SparcISelDAGToDAG.cpp` など) で定義されている関数 `SelectADDRrr` を参照します。
`lib/Target/TargetSelectionDAG.td` では、ストアの DAG 演算子は以下のように定義されています。
def store : PatFrag<(ops node:$val, node:$ptr),
(unindexedstore node:$val, node:$ptr)> {
let IsStore = true;
let IsTruncStore = false;
}
`XXXInstrInfo.td` は、命令に適切な処理メソッドを呼び出すために使用される `SelectCode` メソッドも ( `XXXGenDAGISel.inc` に) 生成します。この例では、`SelectCode` は `ISD::STORE` オペコードに対して `Select_ISD_STORE` を呼び出します。
SDNode *SelectCode(SDValue N) {
...
MVT::ValueType NVT = N.getNode()->getValueType(0);
switch (N.getOpcode()) {
case ISD::STORE: {
switch (NVT) {
default:
return Select_ISD_STORE(N);
break;
}
break;
}
...
`STrr` のパターンが一致するため、`XXXGenDAGISel.inc` の他の場所に、`STrr` のコードが `Select_ISD_STORE` 用に作成されます。`Emit_22` メソッドも `XXXGenDAGISel.inc` で生成され、この命令の処理を完了します。
SDNode *Select_ISD_STORE(const SDValue &N) {
SDValue Chain = N.getOperand(0);
if (Predicate_store(N.getNode())) {
SDValue N1 = N.getOperand(1);
SDValue N2 = N.getOperand(2);
SDValue CPTmp0;
SDValue CPTmp1;
// Pattern: (st:void i32:i32:$src,
// ADDRrr:i32:$addr)<<P:Predicate_store>>
// Emits: (STrr:void ADDRrr:i32:$addr, IntRegs:i32:$src)
// Pattern complexity = 13 cost = 1 size = 0
if (SelectADDRrr(N, N2, CPTmp0, CPTmp1) &&
N1.getNode()->getValueType(0) == MVT::i32 &&
N2.getNode()->getValueType(0) == MVT::i32) {
return Emit_22(N, SP::STrr, CPTmp0, CPTmp1);
}
...
SelectionDAG の合法化フェーズ¶
合法化フェーズは、ターゲットによってネイティブにサポートされている型と操作を使用するように DAG を変換します。ネイティブでサポートされていない型と操作については、サポートされていない型と操作をサポートされている型と操作に変換するために、ターゲット固有の `XXXTargetLowering` 実装にコードを追加する必要があります。
`XXXTargetLowering` クラスのコンストラクターでは、最初に `addRegisterClass` メソッドを使用して、どの型がサポートされ、どのレジスタクラスがそれらに関連付けられているかを指定します。レジスタクラスのコードは、TableGen によって `XXXRegisterInfo.td` から生成され、`XXXGenRegisterInfo.h.inc` に配置されます。たとえば、SparcTargetLowering クラスのコンストラクターの実装 ( `SparcISelLowering.cpp` 内) は、次のコードで始まります。
addRegisterClass(MVT::i32, SP::IntRegsRegisterClass);
addRegisterClass(MVT::f32, SP::FPRegsRegisterClass);
addRegisterClass(MVT::f64, SP::DFPRegsRegisterClass);
`ISD` 名前空間 (`include/llvm/CodeGen/SelectionDAGNodes.h`) のノードタイプを調べ、ターゲットがネイティブにサポートしている操作を判断する必要があります。ネイティブサポートのない操作については、`XXXTargetLowering` クラスのコンストラクターにコールバックを追加して、命令選択プロセスが何をすべきかを知ることができます。`TargetLowering` クラスのコールバックメソッド ( `llvm/Target/TargetLowering.h` で宣言) は次のとおりです。
`setOperationAction` — 一般的な操作。
`setLoadExtAction` — 拡張付きロード。
`setTruncStoreAction` — 切り捨てストア。
`setIndexedLoadAction` — インデックス付きロード。
setIndexedStoreAction
— インデックス付きストア。setConvertAction
— 型変換。setCondCodeAction
— 指定された条件コードのサポート。
注: 古いリリースでは、setLoadExtAction
の代わりに setLoadXAction
が使用されています。また、古いリリースでは、setCondCodeAction
がサポートされていない場合があります。使用しているリリースで具体的にどのメソッドがサポートされているかを確認してください。
これらのコールバックは、操作が指定された型(または複数の型)で動作するかどうかを判断するために使用されます。すべての場合において、3 番目のパラメータは LegalAction
型の列挙値です: Promote
、Expand
、Custom
、または Legal
。SparcISelLowering.cpp
には、4 つの LegalAction
値すべての例が含まれています。
昇格(Promote)¶
指定された型に対してネイティブサポートのない操作の場合、指定された型は、サポートされているより大きな型に昇格される場合があります。たとえば、SPARC はブール値(i1
型)の符号拡張ロードをサポートしていないため、SparcISelLowering.cpp
では、以下の 3 番目のパラメータ Promote
によって、i1
型の値がロード前に大きな型に変更されます。
setLoadExtAction(ISD::SEXTLOAD, MVT::i1, Promote);
展開(Expand)¶
ネイティブサポートのない型の場合、値は昇格ではなく、さらに分解する必要がある場合があります。ネイティブサポートのない操作の場合、他の操作の組み合わせを使用して同様の効果を得ることができます。SPARC では、浮動小数点のサインおよびコサインの三角関数演算は、setOperationAction
の 3 番目のパラメータ Expand
で示されるように、他の操作への展開によってサポートされます。
setOperationAction(ISD::FSIN, MVT::f32, Expand);
setOperationAction(ISD::FCOS, MVT::f32, Expand);
カスタム(Custom)¶
一部の操作では、単純な型の昇格または操作の展開では不十分な場合があります。場合によっては、特別な組み込み関数を実装する必要があります。
たとえば、定数値は特別な処理が必要になる場合や、操作でスタック内のレジスタのスピルとリストア、およびレジスタアロケータの操作が必要になる場合があります。
以下の SparcISelLowering.cpp
コードに示すように、浮動小数点値から符号付き整数への型変換を実行するには、最初に 3 番目のパラメータとして Custom
を指定して setOperationAction
を呼び出す必要があります。
setOperationAction(ISD::FP_TO_SINT, MVT::i32, Custom);
LowerOperation
メソッドでは、各 Custom
操作に対して、呼び出す関数を示す case 文を追加する必要があります。次のコードでは、FP_TO_SINT
オペコードは LowerFP_TO_SINT
メソッドを呼び出します。
SDValue SparcTargetLowering::LowerOperation(SDValue Op, SelectionDAG &DAG) {
switch (Op.getOpcode()) {
case ISD::FP_TO_SINT: return LowerFP_TO_SINT(Op, DAG);
...
}
}
最後に、FP レジスタを使用して浮動小数点値を整数に変換する LowerFP_TO_SINT
メソッドが実装されます。
static SDValue LowerFP_TO_SINT(SDValue Op, SelectionDAG &DAG) {
assert(Op.getValueType() == MVT::i32);
Op = DAG.getNode(SPISD::FTOI, MVT::f32, Op.getOperand(0));
return DAG.getNode(ISD::BITCAST, MVT::i32, Op);
}
適正(Legal)¶
Legal
LegalizeAction
列挙値は、操作がネイティブでサポートされていることを示します。Legal
はデフォルトの状態を表すため、めったに使用されません。SparcISelLowering.cpp
では、CTPOP
(整数に設定されているビットをカウントする操作)のアクションは、SPARC v9 でのみネイティブでサポートされています。次のコードは、v9 以外の SPARC 実装で Expand
変換手法を有効にします。
setOperationAction(ISD::CTPOP, MVT::i32, Expand);
...
if (TM.getSubtarget<SparcSubtarget>().isV9())
setOperationAction(ISD::CTPOP, MVT::i32, Legal);
呼び出し規約¶
ターゲット固有の呼び出し規約をサポートするために、XXXGenCallingConv.td
は、lib/Target/TargetCallingConv.td
で定義されているインターフェース(CCIfType
や CCAssignToReg
など)を使用します。TableGen はターゲット記述子ファイル XXXGenCallingConv.td
を使用して、通常は XXXISelLowering.cpp
に含まれるヘッダーファイル XXXGenCallingConv.inc
を生成できます。TargetCallingConv.td
のインターフェースを使用して、以下を指定できます。
パラメータ割り当ての順序。
パラメータと戻り値の配置場所(スタックまたはレジスタ)。
使用できるレジスタ。
呼び出し側または呼び出し先がスタックをアンワインドするかどうか。
次の例は、CCIfType
および CCAssignToReg
インターフェースの使用方法を示しています。CCIfType
述語が true の場合(つまり、現在の引数の型が f32
または f64
の場合)、アクションが実行されます。この場合、CCAssignToReg
アクションは、引数値を最初に使用可能なレジスタ(R0
または R1
)に割り当てます。
CCIfType<[f32,f64], CCAssignToReg<[R0, R1]>>
SparcCallingConv.td
には、ターゲット固有の戻り値の呼び出し規約(RetCC_Sparc32
)と基本的な 32 ビット C 呼び出し規約(CC_Sparc32
)の定義が含まれています。以下に示す RetCC_Sparc32
の定義は、指定されたスカラー戻り値の型に使用されるレジスタを示しています。単精度浮動小数点数はレジスタ F0
に返され、倍精度浮動小数点数はレジスタ D0
に返されます。32 ビット整数はレジスタ I0
または I1
に返されます。
def RetCC_Sparc32 : CallingConv<[
CCIfType<[i32], CCAssignToReg<[I0, I1]>>,
CCIfType<[f32], CCAssignToReg<[F0]>>,
CCIfType<[f64], CCAssignToReg<[D0]>>
]>;
SparcCallingConv.td
の CC_Sparc32
の定義では、指定されたサイズと配置で値をスタックスロットに割り当てる CCAssignToStack
が導入されています。以下の例では、最初のパラメータ 4 はスロットのサイズを示し、2 番目のパラメータ 4 は 4 バイト単位でのスタックの配置を示します。(特別な場合:サイズがゼロの場合、ABI サイズが使用されます。配置がゼロの場合、ABI 配置が使用されます。)
def CC_Sparc32 : CallingConv<[
// All arguments get passed in integer registers if there is space.
CCIfType<[i32, f32, f64], CCAssignToReg<[I0, I1, I2, I3, I4, I5]>>,
CCAssignToStack<4, 4>
]>;
CCDelegateTo
は、別のよく使用されるインターフェースであり、指定されたサブ呼び出し規約を見つけようとします。一致が見つかった場合は、それが呼び出されます。次の例(X86CallingConv.td
)では、RetCC_X86_32_C
の定義は CCDelegateTo
で終わっています。現在の値がレジスタ ST0
または ST1
に割り当てられた後、RetCC_X86Common
が呼び出されます。
def RetCC_X86_32_C : CallingConv<[
CCIfType<[f32], CCAssignToReg<[ST0, ST1]>>,
CCIfType<[f64], CCAssignToReg<[ST0, ST1]>>,
CCDelegateTo<RetCC_X86Common>
]>;
CCIfCC
は、指定された名前を現在の呼び出し規約と照合しようとするインターフェースです。名前が現在の呼び出し規約と一致する場合、指定されたアクションが呼び出されます。次の例(X86CallingConv.td
)では、Fast
呼び出し規約が使用されている場合、RetCC_X86_32_Fast
が呼び出されます。SSECall
呼び出し規約が使用されている場合、RetCC_X86_32_SSE
が呼び出されます。
def RetCC_X86_32 : CallingConv<[
CCIfCC<"CallingConv::Fast", CCDelegateTo<RetCC_X86_32_Fast>>,
CCIfCC<"CallingConv::X86_SSECall", CCDelegateTo<RetCC_X86_32_SSE>>,
CCDelegateTo<RetCC_X86_32_C>
]>;
CCAssignToRegAndStack
は CCAssignToReg
と同じですが、一部のレジスタが使用されている場合はスタックスロットも割り当てます。基本的には、CCIf<CCAssignToReg<regList>, CCAssignToStack<size, align>>
のように動作します。
class CCAssignToRegAndStack<list<Register> regList, int size, int align>
: CCAssignToReg<regList> {
int Size = size;
int Align = align;
}
その他の呼び出し規約インターフェースには、以下が含まれます。
CCIf <predicate, action>
— 述語が一致する場合、アクションを適用します。CCIfInReg <action>
— 引数に「inreg
」属性がマークされている場合、アクションを適用します。CCIfNest <action>
— 引数に「nest
」属性がマークされている場合、アクションを適用します。CCIfNotVarArg <action>
— 現在の関数が可変個の引数を取らない場合、アクションを適用します。CCAssignToRegWithShadow <registerList, shadowList>
—CCAssignToReg
と似ていますが、レジスタのシャドウリストがあります。CCPassByVal <size, align>
— 指定された最小サイズとアライメントでスタックスロットに値を割り当てます。CCPromoteToType <type>
— 現在の値を指定された型に昇格させます。CallingConv <[actions]>
— サポートされている各呼び出し規約を定義します。
アセンブリプリンタ¶
コード生成段階では、コードジェネレータはLLVMパスを利用してアセンブリ出力を生成する場合があります。これを行うには、LLVM IRをターゲットマシンのGAS形式アセンブリ言語に変換するプリンタのコードを、以下の手順で実装する必要があります。
ターゲットのすべてのアセンブリ文字列を定義し、
XXXInstrInfo.td
ファイルで定義されている命令に追加します。(命令セットを参照してください。)TableGenは、XXXAsmPrinter
クラスのprintInstruction
メソッドの実装を含む出力ファイル(XXXGenAsmWriter.inc
)を生成します。XXXTargetAsmInfo.h
を記述します。これは、XXXTargetAsmInfo
クラス(TargetAsmInfo
のサブクラス)の基本的な宣言を含みます。XXXTargetAsmInfo.cpp
を記述します。これは、TargetAsmInfo
プロパティのターゲット固有の値と、場合によってはメソッドの新しい実装を含みます。XXXAsmPrinter.cpp
を記述します。これは、LLVMからアセンブリへの変換を実行するAsmPrinter
クラスを実装します。
XXXTargetAsmInfo.h
のコードは、通常、XXXTargetAsmInfo.cpp
で使用するためのXXXTargetAsmInfo
クラスの簡単な宣言です。同様に、XXXTargetAsmInfo.cpp
には、通常、TargetAsmInfo.cpp
のデフォルト値をオーバーライドするXXXTargetAsmInfo
の置換値の宣言がいくつかあります。たとえば、SparcTargetAsmInfo.cpp
では
SparcTargetAsmInfo::SparcTargetAsmInfo(const SparcTargetMachine &TM) {
Data16bitsDirective = "\t.half\t";
Data32bitsDirective = "\t.word\t";
Data64bitsDirective = 0; // .xword is only supported by V9.
ZeroDirective = "\t.skip\t";
CommentString = "!";
ConstantPoolSection = "\t.section \".rodata\",#alloc\n";
}
X86アセンブリプリンタの実装(X86TargetAsmInfo
)は、ターゲット固有のTargetAsmInfo
クラスがオーバーライドされたメソッド:ExpandInlineAsm
を使用する例です。
AsmPrinter
のターゲット固有の実装は、XXXAsmPrinter.cpp
に記述されます。これは、LLVMを出力可能なアセンブリに変換するAsmPrinter
クラスを実装します。実装には、AsmPrinter
クラスとMachineFunctionPass
クラスの宣言を含む以下のヘッダーが含まれている必要があります。MachineFunctionPass
は、FunctionPass
のサブクラスです。
#include "llvm/CodeGen/AsmPrinter.h"
#include "llvm/CodeGen/MachineFunctionPass.h"
FunctionPass
として、AsmPrinter
は最初にdoInitialization
を呼び出してAsmPrinter
をセットアップします。SparcAsmPrinter
では、変数名を処理するためにMangler
オブジェクトがインスタンス化されます。
XXXAsmPrinter.cpp
では、runOnMachineFunction
メソッド(MachineFunctionPass
で宣言されています)をXXXAsmPrinter
に実装する必要があります。MachineFunctionPass
では、runOnFunction
メソッドはrunOnMachineFunction
を呼び出します。runOnMachineFunction
のターゲット固有の実装は異なりますが、一般的には各マシン関数を処理するために以下のことを行います。
初期化を実行するために
SetupMachineFunction
を呼び出します。メモリにスピルされた定数を出力ストリームに出力するために、
EmitConstantPool
を呼び出します。現在の関数で使用されるジャンプテーブルを出力するために、
EmitJumpTableInfo
を呼び出します。現在の関数のラベルを出力します。
基本ブロックラベルと命令のアセンブリ(
printInstruction
を使用)を含む、関数のコードを出力します。
XXXAsmPrinter
の実装には、TableGenによって生成され、XXXGenAsmWriter.inc
ファイルに出力されるコードも含まれている必要があります。XXXGenAsmWriter.inc
のコードには、以下のメソッドを呼び出す可能性のあるprintInstruction
メソッドの実装が含まれています。
printOperand
printMemOperand
printCCOperand
(条件文の場合)printDataDirective
printDeclare
printImplicitDef
printInlineAsm
AsmPrinter.cpp
のprintDeclare
、printImplicitDef
、printInlineAsm
、およびprintLabel
の実装は、一般的にアセンブリの出力には十分であり、オーバーライドする必要はありません。
printOperand
メソッドは、オペランドのタイプ(レジスタ、イミディエイト、基本ブロック、外部シンボル、グローバルアドレス、定数プールインデックス、またはジャンプテーブルインデックス)に応じた長いswitch
/case
文で実装されます。メモリアドレスオペランドを持つ命令の場合、適切な出力を生成するためにprintMemOperand
メソッドを実装する必要があります。同様に、条件オペランドを出力するには、printCCOperand
を使用する必要があります。
doFinalization
はXXXAsmPrinter
でオーバーライドする必要があり、アセンブリプリンタをシャットダウンするために呼び出す必要があります。doFinalization
中に、グローバル変数と定数が出力に出力されます。
サブターゲットのサポート¶
サブターゲットのサポートは、特定のチップセットの命令セットのバリエーションをコード生成プロセスに通知するために使用されます。たとえば、提供されているLLVM SPARC実装は、SPARCマイクロプロセッサアーキテクチャの3つの主要バージョン(バージョン8(V8、32ビットアーキテクチャ)、バージョン9(V9、64ビットアーキテクチャ)、およびUltraSPARCアーキテクチャ)をカバーしています。V8には、32個の単精度レジスタまたは8個の4倍精度レジスタとしても使用できる16個の倍精度浮動小数点レジスタがあります。また、V8は純粋なビッグエンディアンです。V9には、16個の4倍精度レジスタとしても使用できる32個の倍精度浮動小数点レジスタがありますが、単精度レジスタとして使用することはできません。UltraSPARCアーキテクチャは、V9とUltraSPARC Visual Instruction Set拡張機能を組み合わせたものです。
サブターゲットのサポートが必要な場合は、アーキテクチャ用のターゲット固有のXXXSubtarget
クラスを実装する必要があります。このクラスは、コマンドラインオプション-mcpu=
と-mattr=
を処理する必要があります。
TableGenは、Target.td
ファイルとSparc.td
ファイルの定義を使用して、SparcGenSubtarget.inc
にコードを生成します。以下に示すTarget.td
では、SubtargetFeature
インターフェースが定義されています。SubtargetFeature
インターフェースの最初の4つの文字列パラメータは、機能名、機能によって設定されるXXXSubtargetフィールド、XXXSubtargetフィールドの値、および機能の説明です。(5番目のパラメータは、存在が暗示されている機能のリストであり、デフォルト値は空の配列です。)
フィールドの値が文字列「true」または「false」の場合、フィールドはbool型と見なされ、1つのSubtargetFeatureのみがそれを参照します。それ以外の場合、整数型と見なされます。整数値は、列挙定数の名前です。複数の機能が同じ整数フィールドを使用する場合、フィールドは、そのフィールドを共有するすべての有効な機能の最大値に設定されます。
class SubtargetFeature<string n, string f, string v, string d,
list<SubtargetFeature> i = []> {
string Name = n;
string FieldName = f;
string Value = v;
string Desc = d;
list<SubtargetFeature> Implies = i;
}
Sparc.td
ファイルでは、SubtargetFeature
を使用して以下の機能が定義されています。
def FeatureV9 : SubtargetFeature<"v9", "IsV9", "true",
"Enable SPARC-V9 instructions">;
def FeatureV8Deprecated : SubtargetFeature<"deprecated-v8",
"UseV8DeprecatedInsts", "true",
"Enable deprecated V8 instructions in V9 mode">;
def FeatureVIS : SubtargetFeature<"vis", "IsVIS", "true",
"Enable UltraSPARC Visual Instruction Set extensions">;
Sparc.td
の他の場所では、Proc
クラスが定義され、次に、前述の機能を持つ可能性のある特定のSPARCプロセッサのサブタイプを定義するために使用されます。
class Proc<string Name, list<SubtargetFeature> Features>
: Processor<Name, NoItineraries, Features>;
def : Proc<"generic", []>;
def : Proc<"v8", []>;
def : Proc<"supersparc", []>;
def : Proc<"sparclite", []>;
def : Proc<"f934", []>;
def : Proc<"hypersparc", []>;
def : Proc<"sparclite86x", []>;
def : Proc<"sparclet", []>;
def : Proc<"tsc701", []>;
def : Proc<"v9", [FeatureV9]>;
def : Proc<"ultrasparc", [FeatureV9, FeatureV8Deprecated]>;
def : Proc<"ultrasparc3", [FeatureV9, FeatureV8Deprecated]>;
def : Proc<"ultrasparc3-vis", [FeatureV9, FeatureV8Deprecated, FeatureVIS]>;
Target.td
ファイルとSparc.td
ファイルから、結果のSparcGenSubtarget.inc
は、機能を識別するための列挙値、CPU機能とCPUサブタイプを表す定数の配列、および指定されたサブターゲットオプションを設定する機能文字列を解析するParseSubtargetFeatures
メソッドを指定します。生成されたSparcGenSubtarget.inc
ファイルは、SparcSubtarget.cpp
に含める必要があります。XXXSubtarget
メソッドのターゲット固有の実装は、この擬似コードに従う必要があります。
XXXSubtarget::XXXSubtarget(const Module &M, const std::string &FS) {
// Set the default features
// Determine default and user specified characteristics of the CPU
// Call ParseSubtargetFeatures(FS, CPU) to parse the features string
// Perform any additional operations
}
JITのサポート¶
ターゲットマシンの実装には、必要に応じて、マシンコードと補助構造を、メモリに直接書き込むことができるバイナリ出力として出力するJust-In-Time(JIT)コードジェネレータが含まれています。これを行うには、以下の手順を実行してJITコード生成を実装します。
ターゲットマシン命令を再配置可能なマシンコードに変換するマシン関数パスを含む
XXXCodeEmitter.cpp
ファイルを記述します。マシンコードとスタブの出力など、ターゲット固有のコード生成アクティビティのためのJITインターフェースを実装する
XXXJITInfo.cpp
ファイルを記述します。XXXTargetMachine
を、getJITInfo
メソッドを介してTargetJITInfo
オブジェクトを提供するように変更します。
JITサポートコードの記述には、いくつかの異なるアプローチがあります。たとえば、TableGenとターゲット記述子ファイルを使用してJITコードジェネレータを作成できますが、必須ではありません。AlphaおよびPowerPCターゲットマシンでは、TableGenを使用して、マシン命令のバイナリコーディングと、それらのコードにアクセスするための`getBinaryCodeForInstr`メソッドを含む`XXXGenCodeEmitter.inc`が生成されます。他のJIT実装ではそうではありません。
`XXXJITInfo.cpp`と`XXXCodeEmitter.cpp`の両方が、出力ストリームにデータ(バイト、ワード、文字列など)を書き込むためのいくつかのコールバック関数のコードを含む`MachineCodeEmitter`クラスを定義する`llvm/CodeGen/MachineCodeEmitter.h`ヘッダーファイルを含める必要があります。
マシンコードエミッタ¶
`XXXCodeEmitter.cpp`では、`Emitter`クラスのターゲット固有のものが関数パス(`MachineFunctionPass`のサブクラス)として実装されています。 `runOnMachineFunction`のターゲット固有の実装(`MachineFunctionPass`の`runOnFunction`によって呼び出される)は、`MachineBasicBlock`を反復処理し、`emitInstruction`を呼び出して各命令を処理し、バイナリコードを生成します。 `emitInstruction`は、主に`XXXInstrInfo.h`で定義されている命令タイプに関するcase文で実装されています。たとえば、`X86CodeEmitter.cpp`では、`emitInstruction`メソッドは、次の`switch`/`case`文を中心に構築されています。
switch (Desc->TSFlags & X86::FormMask) {
case X86II::Pseudo: // for not yet implemented instructions
... // or pseudo-instructions
break;
case X86II::RawFrm: // for instructions with a fixed opcode value
...
break;
case X86II::AddRegFrm: // for instructions that have one register operand
... // added to their opcode
break;
case X86II::MRMDestReg:// for instructions that use the Mod/RM byte
... // to specify a destination (register)
break;
case X86II::MRMDestMem:// for instructions that use the Mod/RM byte
... // to specify a destination (memory)
break;
case X86II::MRMSrcReg: // for instructions that use the Mod/RM byte
... // to specify a source (register)
break;
case X86II::MRMSrcMem: // for instructions that use the Mod/RM byte
... // to specify a source (memory)
break;
case X86II::MRM0r: case X86II::MRM1r: // for instructions that operate on
case X86II::MRM2r: case X86II::MRM3r: // a REGISTER r/m operand and
case X86II::MRM4r: case X86II::MRM5r: // use the Mod/RM byte and a field
case X86II::MRM6r: case X86II::MRM7r: // to hold extended opcode data
...
break;
case X86II::MRM0m: case X86II::MRM1m: // for instructions that operate on
case X86II::MRM2m: case X86II::MRM3m: // a MEMORY r/m operand and
case X86II::MRM4m: case X86II::MRM5m: // use the Mod/RM byte and a field
case X86II::MRM6m: case X86II::MRM7m: // to hold extended opcode data
...
break;
case X86II::MRMInitReg: // for instructions whose source and
... // destination are the same register
break;
}
これらのcase文の実装は、多くの場合、最初にオペコードを発行し、次にオペランドを取得します。次に、オペランドに応じて、ヘルパーメソッドが呼び出されてオペランドが処理される場合があります。たとえば、`X86CodeEmitter.cpp`では、`X86II::AddRegFrm`の場合、最初に発行されるデータ(`emitByte`による)は、レジスタオペランドに追加されたオペコードです。次に、マシンオペランドを表すオブジェクト`MO1`が抽出されます。 `isImmediate`、`isGlobalAddress`、`isExternalSymbol`、`isConstantPoolIndex`、`isJumpTableIndex`などのヘルパーメソッドは、オペランドのタイプを決定します。(`X86CodeEmitter.cpp`には、`emitConstant`、`emitGlobalAddress`、`emitExternalSymbolAddress`、`emitConstPoolAddress`、`emitJumpTableAddress`などのプライベートメソッドもあり、これらは出力ストリームにデータを発行します。)
case X86II::AddRegFrm:
MCE.emitByte(BaseOpcode + getX86RegNum(MI.getOperand(CurOp++).getReg()));
if (CurOp != NumOps) {
const MachineOperand &MO1 = MI.getOperand(CurOp++);
unsigned Size = X86InstrInfo::sizeOfImm(Desc);
if (MO1.isImmediate())
emitConstant(MO1.getImm(), Size);
else {
unsigned rt = Is64BitMode ? X86::reloc_pcrel_word
: (IsPIC ? X86::reloc_picrel_word : X86::reloc_absolute_word);
if (Opcode == X86::MOV64ri)
rt = X86::reloc_absolute_dword; // FIXME: add X86II flag?
if (MO1.isGlobalAddress()) {
bool NeedStub = isa<Function>(MO1.getGlobal());
bool isLazy = gvNeedsLazyPtr(MO1.getGlobal());
emitGlobalAddress(MO1.getGlobal(), rt, MO1.getOffset(), 0,
NeedStub, isLazy);
} else if (MO1.isExternalSymbol())
emitExternalSymbolAddress(MO1.getSymbolName(), rt);
else if (MO1.isConstantPoolIndex())
emitConstPoolAddress(MO1.getIndex(), rt);
else if (MO1.isJumpTableIndex())
emitJumpTableAddress(MO1.getIndex(), rt);
}
}
break;
前の例では、`XXXCodeEmitter.cpp`は、アドレス(たとえば、PICベースオフセットを持つグローバルアドレス)を再配置するために使用される可能性のある`RelocationType`列挙型である変数`rt`を使用しています。そのターゲットの`RelocationType`列挙型は、ターゲット固有の短い`XXXRelocations.h`ファイルで定義されています。 `RelocationType`は、`XXXJITInfo.cpp`で定義されている`relocate`メソッドによって、参照されるグローバルシンボルのアドレスを書き換えるために使用されます。
たとえば、`X86Relocations.h`は、X86アドレスに対して次の再配置タイプを指定します。4つのケースすべてで、再配置された値はメモリ内の既存の値に追加されます。 `reloc_pcrel_word`と`reloc_picrel_word`の場合、追加の初期調整があります。
enum RelocationType {
reloc_pcrel_word = 0, // add reloc value after adjusting for the PC loc
reloc_picrel_word = 1, // add reloc value after adjusting for the PIC base
reloc_absolute_word = 2, // absolute relocation; no additional adjustment
reloc_absolute_dword = 3 // absolute relocation; no additional adjustment
};
ターゲットJIT情報¶
`XXXJITInfo.cpp`は、マシンコードやスタブの発行など、ターゲット固有のコード生成アクティビティのためのJITインターフェースを実装します。少なくとも、`XXXJITInfo`のターゲット固有のバージョンは、以下を実装します。
`getLazyResolverFunction` — JITを初期化し、コンパイルに使用される関数をターゲットに提供します。
`emitFunctionStub` — コールバック関数に指定されたアドレスを持つネイティブ関数を返します。
`relocate` — 再配置タイプに基づいて、参照されるグローバルのアドレスを変更します。
実際のターゲットが最初にわからない場合に使用される関数スタブへのラッパーであるコールバック関数。
`getLazyResolverFunction`の実装は、一般的に簡単です。受信パラメータをグローバル`JITCompilerFunction`として設定し、関数ラッパーとして使用されるコールバック関数を返します。Alphaターゲット(`AlphaJITInfo.cpp`内)の場合、`getLazyResolverFunction`の実装は単純に次のようになります。
TargetJITInfo::LazyResolverFn AlphaJITInfo::getLazyResolverFunction(
JITCompilerFn F) {
JITCompilerFunction = F;
return AlphaCompilationCallback;
}
X86ターゲットの場合、`getLazyResolverFunction`の実装は、SSE命令とXMMレジスタを持つプロセッサに対して異なるコールバック関数を返すため、少し複雑です。
コールバック関数は、最初に呼び出し先レジスタ値、受信引数、フレーム、およびリターンアドレスを保存し、後で復元します。コールバック関数は、レジスタまたはスタックへの低レベルのアクセスを必要とするため、通常はアセンブラで実装されます。