本節主要介紹指令選擇的具體步驟(select). select是將基於ISD SDNode的DAG替換成基於機器指令節點的DAG的過程.
select基本流程
在完成combine與legalize之后SelectionDAGISel::CodeGenAndEmitDAG()會調用SelectionDAGISel::DoInstructionSelection()進行指令選擇.
void SelectionDAGISel::DoInstructionSelection() {
PreprocessISelDAG();
{
DAGSize = CurDAG->AssignTopologicalOrder();
HandleSDNode Dummy(CurDAG->getRoot());
SelectionDAG::allnodes_iterator ISelPosition (CurDAG->getRoot().getNode());
++ISelPosition;
ISelUpdater ISU(*CurDAG, ISelPosition);
while (ISelPosition != CurDAG->allnodes_begin()) {
SDNode *Node = &*--ISelPosition;
if (Node->use_empty())
continue;
Select(Node);
}
CurDAG->setRoot(Dummy.getValue());
}
PostprocessISelDAG();
}
PreprocessISelDAG()與PostprocessISelDAG()是SelectionDAGISel類提供的hook, 用於指令選擇前后做架構相關的優化. 在這之前幾個架構custom的時間點中DAG combine時不能保證opcode與operand的合法性, custom legalize聚焦於opcode的合法化以保證指令選擇時覆蓋整個DAG的節點, 而此處兩個接口最適合做基於(合法化后的)DAG的窺孔優化.
注意到在指令選擇前還會調用一次AssignTopologicalOrder()重新排序, 並且以逆序方式遍歷節點, 這種從上往下的遍歷方式與SelectCode代碼邏輯結合, 保證了最貪婪的tree pattern匹配.
Select()又是一個SelectionDAGISel類提供的hook, 每個后端架構都必須要實現該接口, 仍然以RISCV架構為例.
void RISCVDAGToDAGISel::Select(SDNode *Node) {
if (Node->isMachineOpcode()) {
Node->setNodeId(-1);
return;
}
// Instruction Selection not handled by the auto-generated tablegen selection
// should be handled here.
unsigned Opcode = Node->getOpcode();
MVT XLenVT = Subtarget->getXLenVT();
SDLoc DL(Node);
EVT VT = Node->getValueType(0);
switch (Opcode) {
case ISD::Constant: {
auto ConstNode = cast<ConstantSDNode>(Node);
if (VT == XLenVT && ConstNode->isNullValue()) {
SDValue New = CurDAG->getCopyFromReg(CurDAG->getEntryNode(), SDLoc(Node),
RISCV::X0, XLenVT);
ReplaceNode(Node, New.getNode());
return;
}
int64_t Imm = ConstNode->getSExtValue();
if (XLenVT == MVT::i64) {
ReplaceNode(Node, selectImm(CurDAG, SDLoc(Node), Imm, XLenVT));
return;
}
break;
}
......
}
// Select the default instruction.
SelectCode(Node);
}
static SDNode *selectImm(SelectionDAG *CurDAG, const SDLoc &DL, int64_t Imm,
MVT XLenVT) {
RISCVMatInt::InstSeq Seq;
RISCVMatInt::generateInstSeq(Imm, XLenVT == MVT::i64, Seq);
SDNode *Result = nullptr;
SDValue SrcReg = CurDAG->getRegister(RISCV::X0, XLenVT);
for (RISCVMatInt::Inst &Inst : Seq) {
SDValue SDImm = CurDAG->getTargetConstant(Inst.Imm, DL, XLenVT);
if (Inst.Opc == RISCV::LUI)
Result = CurDAG->getMachineNode(RISCV::LUI, DL, XLenVT, SDImm);
else
Result = CurDAG->getMachineNode(Inst.Opc, DL, XLenVT, SrcReg, SDImm);
// Only the first instruction has X0 as its source.
SrcReg = SDValue(Result, 0);
}
return Result;
}
通常一個后端架構的Select()接口由兩部分組成, 手工編碼指令選擇與通過td自動生成pattern match的匹配代碼, 通常不能通過編寫pattern獲取最佳選擇的指令會通過手工編碼的方式, 因此這塊代碼在pattern match代碼前執行. 只有在手工選擇失敗之后才會調用SelectCode()進行pattern match匹配, 如果還失敗則會調用SelectionDAGISel::CannotYetSelect()報錯(在移植一個新架構時常見情況).
我們以RISCV為例看下什么情況需要手工編碼指令選擇. 其常量賦值(給寄存器)的語義對應的硬件指令比較特殊, 原因是
- RISCV的通用寄存器包含像ARM一樣的zero寄存器X0, 立即數0可以直接lowering為X0寄存器.
- RISCV標准指令集沒有32bit立即數賦值指令, 對小於4K的立即數可以使用addi指令賦值, 對於低12bit全0的立即數可以單獨使用lui指令賦值, 否則需要lui + addi指令組合賦值.
可見Select()中首先對ISD::Constant節點判斷其值是否為0, 為0則直接替換為寄存器拷貝, 其次調用selectImm()中對第二種情況分類討論.
在選擇了最優的指令后調用SelectionDAG::getMachineNode()創建一個新節點, 注意該接口與SelectionDAG::getNode()的區別在於對glue節點處理的不同, 其它處理是一樣的.
MachineSDNode *SelectionDAG::getMachineNode(unsigned Opcode, const SDLoc &DL,
SDVTList VTs,
ArrayRef<SDValue> Ops) {
bool DoCSE = VTs.VTs[VTs.NumVTs-1] != MVT::Glue;
MachineSDNode *N;
void *IP = nullptr;
if (DoCSE) {
FoldingSetNodeID ID;
AddNodeIDNode(ID, ~Opcode, VTs, Ops);
IP = nullptr;
if (SDNode *E = FindNodeOrInsertPos(ID, DL, IP)) {
return cast<MachineSDNode>(UpdateSDLocOnMergeSDNode(E, DL));
}
}
// Allocate a new MachineSDNode.
N = newSDNode<MachineSDNode>(~Opcode, DL.getIROrder(), DL.getDebugLoc(), VTs);
createOperands(N, Ops);
if (DoCSE)
CSEMap.InsertNode(N, IP);
InsertNode(N);
NewSDValueDbgMsg(SDValue(N, 0), "Creating new machine node: ", this);
return N;
}
SDValue SelectionDAG::getNode(unsigned Opcode, const SDLoc &DL, EVT VT) {
FoldingSetNodeID ID;
AddNodeIDNode(ID, Opcode, getVTList(VT), None);
void *IP = nullptr;
if (SDNode *E = FindNodeOrInsertPos(ID, DL, IP))
return SDValue(E, 0);
auto *N = newSDNode<SDNode>(Opcode, DL.getIROrder(), DL.getDebugLoc(),
getVTList(VT));
CSEMap.InsertNode(N, IP);
InsertNode(N);
SDValue V = SDValue(N, 0);
NewSDValueDbgMsg(V, "Creating new node: ", this);
return V;
}
基於pattern match指令選擇實現
SelectCode()是tablegen自動生成的指令選擇器的實現, 其定義見llvm build目錄下lib/Target/[arch]/[arch]GenDAGISel.inc(其中arch為具體后端架構名). 以RISCV為例其實現如下
void DAGISEL_CLASS_COLONCOLON SelectCode(SDNode *N)
{
// Some target values are emitted as 2 bytes, TARGET_VAL handles
// this.
#define TARGET_VAL(X) X & 255, unsigned(X) >> 8
static const unsigned char MatcherTable[] = {
......
}; // Total Array size is 25735 bytes
#undef TARGET_VAL
SelectCodeCommon(N, MatcherTable,sizeof(MatcherTable));
}
注意到SelectCode()定義了一個非常大的靜態數組並將其傳給SelectCodeCommon, 該數組是tablegen根據td中定義的pattern生成的一個狀態跳轉表, 我們將在接下來解釋其具體構成.
在這之前我們首先思考一個問題, 給定指令的語義與輸入DAG, 如何將其映射到對應的指令? 舉個簡單的例子一個架構支持三條指令, MUL對應乘法運算, SUB對應減法運算, SRL對應邏輯右移, 那么對應下圖1中的DAG我們該如何將其映射為機器指令.
最簡單的方式是將指令描述為對應的DAG節點, 然后通過展開的方式一一匹配. 如圖2中我們將DAG中每個操作節點分割開來, 然后一一匹配到對應的機器指令.
但是這種方式帶來的效率的問題: 選擇的指令不一定是最優的. 如果這個架構又支持了一條MS指令, 其作用是將前兩個操作數相乘后再邏輯右移, 宏展開的方式無法選擇恰當的指令. 對此我們使用DAG covering的方式, 將DAG划分為一個個子圖, 對每個子圖做檢查, 查看指令的語義是否能匹配子圖, 如果可以則替換為機器指令, 否則將子圖划分為更小的子圖遞歸檢查.
如圖3所示, 首先從root節點開始搜索, 根據操作符確定候選的SUB/MS兩條指令, 再向下搜索第一個來源mul確定候選的MS指令, 依次類推. 再候選過程中可能存在一個DAG對應多個子圖划分的情況(比如上文也可以划分為mul一個子圖和另一個子圖add + sub, 如果架構支持一個指令將add的結果減去另一個值), 在權衡不同划分的優劣時可以使用動態規划來求解.
更進一步的研究建議閱讀Survey on Instruction Selection第四章, 本文將聚焦於LLVM的實現.
LLVM的實現與上文描述中稍微有些區別:
- 指令語義是以tree pattern的方式描述的. tree與DAG最大區別在於tree中的節點只有一個輸出邊, 這樣大大簡化了子圖的復雜度. 缺點是由於被選擇的DAG中存在一個節點被多個節點引用的情況(多條輸出邊), 對於這類情況需要將其轉換為tree形式(這步被稱為undagging).
- LLVM使用貪婪的匹配策略, 因此選擇結果不一定是全局最優的.
LLVM使用tablegen處理td中描述的pattern, 將其分類並設定優先級, 其結果轉化成一段特殊格式的數組(也被稱作匹配表). 我們截取一段RISCV的匹配表作為例子來解釋其含義與構成.
static const unsigned char MatcherTable[] = {
/* 0*/ OPC_SwitchOpcode /*81 cases */, 12|128,5/*652*/, TARGET_VAL(ISD::AND),// ->657
/* 5*/ OPC_Scope, 41|128,4/*553*/, /*->561*/ // 2 children in Scope
/* 8*/ OPC_CheckAndImm, 127|128,127|128,127|128,127|128,15/*4294967295*/,
/* 14*/ OPC_Scope, 75|128,3/*459*/, /*->476*/ // 2 children in Scope
/* 17*/ OPC_MoveChild0,
/* 18*/ OPC_SwitchOpcode /*2 cases */, 96|128,1/*224*/, TARGET_VAL(RISCVISD::DIVUW),// ->247
/* 23*/ OPC_MoveChild0,
/* 24*/ OPC_Scope, 110, /*->136*/ // 2 children in Scope
/* 26*/ OPC_CheckAndImm, 127|128,127|128,127|128,127|128,15/*4294967295*/,
/* 32*/ OPC_RecordChild0, // #0 = $rs1
/* 33*/ OPC_MoveParent,
/* 34*/ OPC_MoveChild1,
/* 35*/ OPC_Scope, 49, /*->86*/ // 2 children in Scope
/* 37*/ OPC_CheckAndImm, 127|128,127|128,127|128,127|128,15/*4294967295*/,
/* 43*/ OPC_RecordChild0, // #1 = $rs2
/* 44*/ OPC_MoveParent,
/* 45*/ OPC_MoveParent,
/* 46*/ OPC_SwitchType /*2 cases */, 24, MVT::i32,// ->73
/* 49*/ OPC_Scope, 10, /*->61*/ // 2 children in Scope
/* 51*/ OPC_CheckPatternPredicate, 0, // (Subtarget->hasStdExtM()) && (Subtarget->is64Bit()) && (MF->getSubtarget().checkFeatures("-64bit"))
/* 53*/ OPC_MorphNodeTo1, TARGET_VAL(RISCV::DIVU), 0,
MVT::i32, 2/*#Ops*/, 0, 1,
// Src: (and:{ *:[i32] } (riscv_divuw:{ *:[i32] } (and:{ *:[i32] } GPR:{ *:[i32] }:$rs1, 4294967295:{ *:[i32] }), (and:{ *:[i32] } GPR:{ *:[i32] }:$rs2, 4294967295:{ *:[i32] })), 4294967295:{ *:[i32] }) - Complexity = 27
// Dst: (DIVU:{ *:[i32] } GPR:{ *:[i32] }:$rs1, GPR:{ *:[i32] }:$rs2)
/* 61*/ /*Scope*/ 10, /*->72*/
/* 62*/ OPC_CheckPatternPredicate, 1, // (Subtarget->hasStdExtM()) && (Subtarget->is64Bit())
/* 64*/ OPC_MorphNodeTo1, TARGET_VAL(RISCV::DIVU), 0,
MVT::i32, 2/*#Ops*/, 0, 1,
// Src: (and:{ *:[i32] } (riscv_divuw:{ *:[i32] } (and:{ *:[i32] } GPR:{ *:[i32] }:$rs1, 4294967295:{ *:[i32] }), (and:{ *:[i32] } GPR:{ *:[i32] }:$rs2, 4294967295:{ *:[i32] })), 4294967295:{ *:[i32] }) - Complexity = 27
// Dst: (DIVU:{ *:[i32] } GPR:{ *:[i32] }:$rs1, GPR:{ *:[i32] }:$rs2)
/* 72*/ 0, /*End of Scope*/
......
};
首先注意到這個表是以unsigned char型數組方式編碼的, 每行起始的注釋指示行首元素在數組中的下標, 方便我們快速查找.
每行行首的元素是以OPC_起始的枚舉值(其值見SelectionDAGISel::BuiltinOpcodes定義), 指示了匹配表要跳轉的下一狀態. 跟在狀態枚舉之后的元素的含義由狀態機根據當前狀態進行解釋, 一個狀態所需的所有信息均記錄在同一行內. 關於狀態枚舉的具體含義我們結合代碼來具體介紹.
行首注釋與狀態枚舉之間的縮進長度指示了該狀態的所屬的層級. 舉例而言對於pattern (add a, (sub b, c)), 檢查操作數b的范圍與檢查操作數c的范圍兩個狀態是平級的, 檢查操作數字a的范圍肯定優先於檢查操作數b的范圍(先匹配樹的根節點, 再葉子節點). 利用縮進可以圖形化閱讀狀態跳轉表.
表中有些特殊的立即數編碼, 比如第一行中的下標1和下標2元素(12|128,5/652/). 這么編碼的原因是因為這個表是unsigned char型數組, 而許多枚舉/下標/立即數范圍大於1個byte的大小, 因此需要變長編碼(使用定長編碼數組會大大增加表的大小, 以X86為例當前大小為500K, 如果使用int數組需要2M空間). 變長編碼的方式是取最高位指示是否需要讀入后一字節, 即12|128,5的編碼值為(12 + 5 << 7 = 652), 正好等於注釋里的值. 其解碼接口見GetVBR()(defined in lib/CodeGen/SelectionDAG/SelectionDAGISel.cpp).
LLVM_ATTRIBUTE_ALWAYS_INLINE static inline uint64_t
GetVBR(uint64_t Val, const unsigned char *MatcherTable, unsigned &Idx) {
assert(Val >= 128 && "Not a VBR");
Val &= 127; // Remove first vbr bit.
unsigned Shift = 7;
uint64_t NextBits;
do {
NextBits = MatcherTable[Idx++];
Val |= (NextBits&127) << Shift;
Shift += 7;
} while (NextBits & 128);
return Val;
}
我們先回到SelectCodeCommon(), 它接受三個參數, 依次是將被選擇的節點, 根據tablegen生成的匹配表, 以及匹配表長度, 如果匹配成功則替換NodeToMatch, 否則調用CannotYetSelect()報錯退出. 代碼分為兩個部分, 第一部分是准備階段, 判斷節點的屬性, 加載匹配表, 第二部分是匹配階段, 根據載入的匹配表進行匹配.
void SelectionDAGISel::SelectCodeCommon(SDNode *NodeToMatch,
const unsigned char *MatcherTable,
unsigned TableSize) {
switch (NodeToMatch->getOpcode()) {
default:
break;
case ISD::EntryToken:
case ISD::BasicBlock:
case ISD::Register:
case ISD::RegisterMask:
case ISD::TargetConstant:
case ISD::TargetConstantFP:
case ISD::TargetConstantPool:
case ISD::TargetFrameIndex:
case ISD::TargetExternalSymbol:
case ISD::MCSymbol:
NodeToMatch->setNodeId(-1);
return;
......
}
SmallVector<SDValue, 8> NodeStack;
SDValue N = SDValue(NodeToMatch, 0);
NodeStack.push_back(N);
// 記錄當前匹配的pattern對應匹配表的位置, 如果當前匹配失敗則從對應位置繼續
SmallVector<MatchScope, 8> MatchScopes;
// 記錄已被匹配的節點及其對應的父節點, 根節點的父節點為空
SmallVector<std::pair<SDValue, SDNode*>, 8> RecordedNodes;
// 記錄已匹配的pattern的memory信息
SmallVector<MachineMemOperand*, 2> MatchedMemRefs;
// 記錄當前的chain與glue依賴信息
SDValue InputChain, InputGlue;
// 記錄被匹配的pattern的chain信息
SmallVector<SDNode*, 3> ChainNodesMatched;
// 記錄當前所在匹配表的位置
unsigned MatcherIndex = 0;
if (!OpcodeOffset.empty()) {
// OpcodeOffset非空, 即已經加載過整張匹配表
if (N.getOpcode() < OpcodeOffset.size())
MatcherIndex = OpcodeOffset[N.getOpcode()];
} else if (MatcherTable[0] == OPC_SwitchOpcode) {
// 第一次匹配, 從頭讀入匹配表
unsigned Idx = 1;
while (true) {
// CaseSize等於根節點為該Opc的pattern對應的匹配表的長度
unsigned CaseSize = MatcherTable[Idx++];
if (CaseSize & 128)
CaseSize = GetVBR(CaseSize, MatcherTable, Idx);
// 長度為0表示查找到達表的末尾
if (CaseSize == 0) break;
uint16_t Opc = MatcherTable[Idx++];
Opc |= (unsigned short)MatcherTable[Idx++] << 8;
if (Opc >= OpcodeOffset.size())
OpcodeOffset.resize((Opc+1)*2);
OpcodeOffset[Opc] = Idx;
Idx += CaseSize;
}
// 如果找到對應的opcode則設置下標到起始位置
if (N.getOpcode() < OpcodeOffset.size())
MatcherIndex = OpcodeOffset[N.getOpcode()];
}
while (true) {
BuiltinOpcodes Opcode = (BuiltinOpcodes)MatcherTable[MatcherIndex++];
// 狀態機處理
switch (Opcode) {
......
}
// 匹配成功時switch會直接continue
// 匹配失敗時才會到此處, 嘗試回溯之前的匹配
// 方式是首先檢查當前scope的下一元素, 直到遍歷所有child
// 如果仍然失敗再回溯該scope的父節點
++NumDAGIselRetries;
while (true) {
// scope棧為空, 匹配失敗
if (MatchScopes.empty()) {
CannotYetSelect(NodeToMatch);
return;
}
// 回溯最近的scope
MatchScope &LastScope = MatchScopes.back();
RecordedNodes.resize(LastScope.NumRecordedNodes);
NodeStack.clear();
NodeStack.append(LastScope.NodeStack.begin(), LastScope.NodeStack.end());
N = NodeStack.back();
if (LastScope.NumMatchedMemRefs != MatchedMemRefs.size())
MatchedMemRefs.resize(LastScope.NumMatchedMemRefs);
MatcherIndex = LastScope.FailIndex;
InputChain = LastScope.InputChain;
InputGlue = LastScope.InputGlue;
if (!LastScope.HasChainNodesMatched)
ChainNodesMatched.clear();
// 檢查該scope的長度, 為0代表是該scope中最后一個pattern
unsigned NumToSkip = MatcherTable[MatcherIndex++];
if (NumToSkip & 128)
NumToSkip = GetVBR(NumToSkip, MatcherTable, MatcherIndex);
if (NumToSkip != 0) {
LastScope.FailIndex = MatcherIndex+NumToSkip;
break;
}
// 該scope中所有匹配失敗, 回溯父節點
MatchScopes.pop_back();
}
}
}
第一步首先判斷節點類型決定是否選擇機器指令.
- 像AssertZext, AssertSext這類偽節點(用作編譯器內部檢測的節點)無需做指令選擇, 直接將其移除.
- Constant節點如果在legalize階段已被轉化為TargetConstant則表明對應指令支持立即數編碼也無需選擇.
- 像CopyToReg, CopyFromReg等公共框架的偽指令也無需選擇.
- EntryToken, TokenFactor等用做指示DAG約束關系的節點也無需選擇, 但為方便之后指令排序這里不會刪除.
以上這些指令(除第一類外)會直接設置NodeId為-1表明已被選擇.
第二步加載匹配表, 從上文介紹可以看到匹配表是一個非常大的數組, 為加速索引SelectionDAGISel類使用一個vector容器OpcodeOffset來緩存不同pattern的根節點在匹配表中的起始位置, 其下標為根節點Opcode的枚舉值. 利用這個緩存可以線性查找根節點的匹配.
在第一次調用SelectCodeCommon()時OpcodeOffset為空, 走else分支從頭加載匹配表, OPC_SwitchOpcode指示匹配表當前位置用來檢查Opcode. 以上文RISCV的匹配表為例, 匹配表第一個元素即OPC_SwitchOpcode, 其后的注釋/81 cases/代表與其同級的OPC_SwitchOpcode共有81個(即按根節點的Opcode來分類共有81種分類)(X86是428類). OPC_SwitchOpcode之后跟隨的兩個元素分別是casesize與opcode, 后者為這個OPC_SwitchOpcode對應的根節點的opcode, 前者為OPC_SwitchOpcode的管理范圍(根節點為opcode的pattern在匹配表里的長度), 用來指示當opcode匹配失敗時需要跳轉到下一個OPC_SwitchOpcode的數組下標.
通常情況下一條指令可能對應多個pattern, 一個DAG Node也可能對應多條指令, 比如(and reg, imm)與(and reg, reg)一定對應不同指令, 考慮到一些復雜pattern比如(and (mul reg, reg), imm)或者(and reg, (shl reg, imm)), 如果一一比較效率較低. 一個辦法是根據匹配的條件對這些pattern分類, 形成樹狀的結構來快速索引pattern, OPC_Scope的作用就是分類匹配條件. 假定我們當前匹配的是and節點, 接下來讀取的第一個OPC_Scope指示當前需要檢查一個匹配條件, 緊跟在它后面的值指示該scope的長度(即條件判斷失敗時下一個判斷條件的位置, 該值為41|128,4/553/), 再后面是OPC_CheckAndImm, 指示該scope中的pattern需要滿足輸入的節點的第二個操作數為立即數的條件.
case OPC_Scope: {
// 順序查找當前scope中滿足條件的匹配
// 只有在找到一個匹配后再將其加入MatchScopes中
unsigned FailIndex;
while (true) {
// 獲取該scope的長度(匹配失敗時跳轉到下一scope的數組下標)
unsigned NumToSkip = MatcherTable[MatcherIndex++];
if (NumToSkip & 128)
NumToSkip = GetVBR(NumToSkip, MatcherTable, MatcherIndex);
// 長度為0表明到達該scope的末尾
if (NumToSkip == 0) {
FailIndex = 0;
break;
}
FailIndex = MatcherIndex+NumToSkip;
unsigned MatcherIndexOfPredicate = MatcherIndex;
// 判斷是否匹配
bool Result;
MatcherIndex = IsPredicateKnownToFail(MatcherTable, MatcherIndex, N,
Result, *this, RecordedNodes);
// 成功則跳出循環
if (!Result)
break;
++NumDAGIselRetries;
// 否則檢查下一scope
MatcherIndex = FailIndex;
}
// If the whole scope failed to match, bail.
if (FailIndex == 0) break;
// 記錄當前scope的位置, 若子節點匹配失敗時需要從該位置回溯
MatchScope NewEntry;
NewEntry.FailIndex = FailIndex;
NewEntry.NodeStack.append(NodeStack.begin(), NodeStack.end());
NewEntry.NumRecordedNodes = RecordedNodes.size();
NewEntry.NumMatchedMemRefs = MatchedMemRefs.size();
NewEntry.InputChain = InputChain;
NewEntry.InputGlue = InputGlue;
NewEntry.HasChainNodesMatched = !ChainNodesMatched.empty();
MatchScopes.push_back(NewEntry);
continue;
}
由於匹配表是順序查找的, 一開始匹配到的pattern不一定是最終的pattern, 因此我們需要記錄每次匹配的位置, 當匹配失敗時可以從對應的位置重新開始匹配, 這個信息存放在MatchScopes中.
回到SelectCodeCommon(), 匹配成功時switch會continue跳過剩余代碼. 而匹配失敗時會檢查MatchScopes是否非空, 若非空則取出最后一個節點重新開始匹配. 如果scope對應的所有元素都匹配失敗則返回父節點重新開始匹配, 直到匹配成功或者MatchScopes為空.
到這匹配表的構成基本上已經解釋的七七八八了, 現在我們可以回頭看下OPC_*枚舉, 其大致分類如下.
- OPC_SwitchOpcode 指示匹配的opcode分類
- OPC_Scope 指示匹配的操作數的條件分類
- OPC_Record* 記錄當前匹配成功的節點, 在生成MachineNode時會被替換掉(如果節點沒有其它引用的話)
- OPC_Move* 記錄當前匹配的操作數(被機器指令使用的操作數, 復雜pattern中的中間結果不會記錄), 創建MachineNode時用到
- OPC_Check* 判斷是否滿足匹配條件
- OPC_Emit* 匹配成功, 創建MachineNode並替換原有的DAG Node
- OPC_Morph* 類似OPC_Emit*, 區別在於被morph的節點可能有其它引用, 需要額外檢查來保證是否刪除
遺留問題:
- 既然匹配表是順序檢索的, 那么一個DAG如果對應兩條pattern, 后面的pattern不是永遠選不到? 是的, 因此LLVM為pattern設置了優先級. 具體而言
a. 后端td中用於描述pattern的Pattern類(defined in include/llvm/Target/TargetSelectionDAG.td)有個成員AddedComplexity, 編譯器開發人員可以通過設置該值調整指令選擇的優先級. 類似的對於ComplexPattern類有個成員Complexity也可以設置ComplexPattern的優先級.
b. tablegen后端會根據pattern的形式計算其complexity, 再加上額外設置的值得到最終值, 生成的匹配表按最終值進行排序, 相同優先級指令根據cost(指令數)排序. pattern本身的complexity計算比價復雜, 簡要而言對於復雜pattern逐個計算然后計算總值, 如果沒有復雜pattern則根據操作數個數計算, 對於操作數是立即數的會有額外加成(否則需要額外指令將立即數移動到寄存器). 具體的計算方式以后有空再分析. - 兩類特殊的依賴節點chain與glue的處理.
TODO - 補張圖方便理解.