LLVM笔记(13) - 指令选择(五) select
本节主要介绍指令选择的具体步骤(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 - 补张图方便理解.