LLVM 之后端篇(5):理解 SelectionDAG 合法化
Author: stormQ
Created: Friday, 15. April 2022 11:29PM
Last Modified: Friday, 01. July 2022 00:36AM
本文基于release/13.x
版本的 LLVM 源码,介绍了如何指定目标机器的合法类型和合法操作,以及如何定制非法类型和非法操作的合法化行为。从而,初步了解SelectionDAG
合法化的整体过程。
LLVM
默认的指令选择器是基于SelectionDAG
实现的。SelectionDAG
是一个有向无环图(Directed-Acyclic-Graph,DAG),其节点为SDNode
。每个SDNode
对应一条指令或一个操作数。从概念上,SelectionDAG
分为legal
和illegal
两种DAG
。如果DAG
中的所有节点都是目标机器原生支持的,那么称之为legal DAG
。也就是说,legal DAG
中既不存在目标机器不支持的操作,也不存在目标机器不支持的类型(包括指令的操作数类型及其操作结果类型)。
在指令选择(特指在<Target>DAGToDAGISel::Select()
函数被调用)之前,我们需要将illegal DAG
转换为legal DAG
,该过程称为SelectionDAG
合法化。它包括以下两种情况:
类型合法化(Type Legalization
),即将SDNode
的操作数以及操作结果中的非法类型转换为目标机器支持的类型。
操作合法化(Operation Legalization
),即将SDNode
所表示的非法操作转换为目标机器支持的操作。
LLVM
中,MVT::SimpleValueType
定义了所有特定于目标机器的类型(位于llvm/include/llvm/Support/MachineValueType.h
文件)。通常情况下,每种目标机器仅支持其中部分类型。对于不支持的类型(illegal type
),我们需要将其转换为合法的类型(legal type
)。而在类型合法化之前,我们需要指定目标机器中哪些类型是合法的以及如何处理非法类型。
我们通过在子类<Target>TargetLowering
的构造函数中调用addRegisterClass()
指定目标机器的合法类型,以及这些合法类型各自对应的寄存器类。比如,函数AArch64TargetLowering::AArch64TargetLowering()
的部分实现如下:
addRegisterClass(MVT::i32, &AArch64::GPR32allRegClass);
addRegisterClass(MVT::i64, &AArch64::GPR64allRegClass);
上述代码将MVT::i32
和MVT::i64
指定为目标机器AArch64
的合法类型,并分别对应GPR32all
、GPR64all
寄存器类。
需要注意地是, 目标机器中的每种合法类型只能对应一种寄存器类。这是因为,函数addRegisterClass()
的实现如下(位于llvm/include/llvm/CodeGen/TargetLowering.h
文件):
void addRegisterClass(MVT VT, const TargetRegisterClass *RC) {
assert((unsigned)VT.SimpleTy < array_lengthof(RegClassForVT));
RegClassForVT[VT.SimpleTy] = RC;
}
LLVM
在类TargetLoweringBase
中提供了如下 3 个接口,可用于判断是否为目标机器的合法类型(位于llvm/include/llvm/CodeGen/TargetLowering.h
文件):
bool isTypeLegal(EVT VT) const
。该函数的参数VT
表示要判断的类型;返回值为true
时表示该类型为合法类型,否则为非法类型。
LegalizeTypeAction getTypeAction(LLVMContext &Context, EVT VT) const
。该函数的参数Context
表示上下文信息,参数VT
表示要判断的类型;返回值为TargetLoweringBase::TypeLegal
时表示该类型为合法类型,否则为非法类型。
LegalizeTypeAction getTypeAction(MVT VT) const
。该函数的参数VT
表示要判断的类型;返回值为TargetLoweringBase::TypeLegal
时表示该类型为合法类型,否则为非法类型。
方式 1(推荐用法):
在函数<Target>TargetLoweringBase:ReplaceNodeResults()
中判断MVT::i64
是否为目标机器的合法类型:
void <Target>TargetLowering::ReplaceNodeResults(SDNode *N,
SmallVectorImpl<SDValue> &Results,
SelectionDAG &DAG) const {
if (isTypeLegal(MVT::i64)) {
// ...
}
}
在函数<Target>DAGToDAGISel::Select()
中判断节点N
第一个定义的值是否为目标机器的合法类型:
void <Target>DAGToDAGISel::Select(SDNode *N) {
auto RetVT = N->getValueType(0);
if (TLI->isTypeLegal(RetVT)) {
// ...
}
}
在所有调用addRegisterClass()
地方的后面,我们通过调用computeRegisterProperties()
函数指定目标机器中非法类型的处理方式。比如,函数AArch64TargetLowering::AArch64TargetLowering()
的部分实现如下:
addRegisterClass(MVT::i32, &AArch64::GPR32allRegClass);
addRegisterClass(MVT::i64, &AArch64::GPR64allRegClass);
computeRegisterProperties(Subtarget->getRegisterInfo());
LLVM
中,TargetLoweringBase::LegalizeTypeAction
定义了目标机器中非法类型的所有处理方式(位于llvm/include/llvm/CodeGen/TargetLowering.h
文件)。常见的处理方式如下:
TypePromoteInteger
,即将小的非法整型提升为大的合法整型(称为promoting
)。整型提升默认遵循就近原则
。比如,如果目标机器的合法整型为i32
和i64
,那么i1
、i8
和i16
等非法整型会默认提升到i32
而不是i64
。
TypeExpandInteger
,即将大的非法整型扩展为多个小的合法整型(称为expanding
)。整型扩展也默认遵循就近原则
。比如,如果目标机器的合法整型为i32
和i64
,那么非法整型i128
会默认扩展成 2 个i64
而不是 4 个i32
。
TypeSplitVector
,即将大的非法向量类型拆分为多个小的合法向量类型(称为splitting
)。向量拆分也默认遵循就近原则
。特殊地,如果不存在小的合法向量类型,那么非法向量类型的所有元素就会被转换为标量(称为scalarizing
)。
TypeWidenVector
,即将小的非法向量类型扩展为大的合法向量类型(称为widening
)。向量扩展也默认遵循就近原则
。
需要注意地是, 对于目标机器中的非法标量类型,LLVM
不允许子类<Target>TargetLowering
定制其LegalizeTypeAction
。而对于目标机器中的非法向量类型,LLVM
允许子类<Target>TargetLowering
覆写getPreferredVectorAction()
函数定制其LegalizeTypeAction
。
类型合法化的实现为SelectionDAG::LegalizeTypes()
函数(位于llvm/lib/CodeGen/SelectionDAG/LegalizeTypes.cpp
文件)。它包括以下三种情况:
非法整型合法化。相关函数如下(位于llvm/lib/CodeGen/SelectionDAG/LegalizeIntegerTypes.cpp
文件):
DAGTypeLegalizer::PromoteIntegerResult()
,用于将操作结果中的非法整型提升为合法整型。
DAGTypeLegalizer::ExpandIntegerResult()
,用于将操作结果中的非法整型扩展为多个合法整型。
DAGTypeLegalizer::PromoteIntegerOperand()
,用于将操作数中的非法整型提升为合法整型。
DAGTypeLegalizer::ExpandIntegerOperand()
,用于将操作数中的非法整型扩展为多个合法整型。
非法浮点型合法化(位于llvm/lib/CodeGen/SelectionDAG/LegalizeFloatTypes.cpp
文件)。
非法向量类型合法化。相关函数如下(位于llvm/lib/CodeGen/SelectionDAG/LegalizeVectorTypes.cpp
文件):
DAGTypeLegalizer::ScalarizeVectorResult()
,用于将操作结果中的非法向量类型标量化。
DAGTypeLegalizer::SplitVectorResult()
,用于将操作结果中的非法向量类型拆分为多个合法向量类型。
DAGTypeLegalizer::WidenVectorResult()
,用于将操作结果中的非法向量类型扩展为合法向量类型。
DAGTypeLegalizer::ScalarizeVectorOperand()
,用于将操作数中的非法向量类型标量化。
DAGTypeLegalizer::SplitVectorOperand()
,用于将操作数中的非法向量类型拆分为多个合法向量类型。
DAGTypeLegalizer::WidenVectorOperand()
,用于将操作数中的非法向量类型扩展为合法向量类型。
需要注意地是:
关于非法向量类型标量化,LLVM 13.0
仅支持对元素数量为 1 的非法向量类型进行标量化。对于元素数量大于 1 的非法向量类型,如果目标机器不支持任何向量类型,那么该非法向量类型会经过一系列的widening
、splitting
处理转化为元素数量为 1 的非法向量类型后再进行标量化。
LLVM
允许定制目标机器中非法类型(包括非法的操作数类型和非法的操作结果类型)的合法化行为。
step 1: 指定类型合法化行为是Custom
在子类<Target>TargetLowering
的构造函数中调用setOperationAction()
函数指明要对哪个节点的哪种非法类型(包括非法的操作数类型和非法的操作结果类型)进行类型合法化行为的定制。比如:
setOperationAction(ISD::ADD, MVT::i8, Custom);
上述代码表示,如果节点的操作码是ISD::ADD
,并且该节点的操作数或操作结果的类型为MVT::i8
,那么该节点的类型合法化行为是用户自定义的。
step 2: 对于非法操作结果,实现用户自定义的类型合法化行为
在子类<Target>TargetLowering
中覆写成员函数ReplaceNodeResults()
实现用户自定义的非法操作结果的类型合法化行为。比如:
void <Target>TargetLowering::ReplaceNodeResults(SDNode *N,
SmallVectorImpl<SDValue> &Results,
SelectionDAG &DAG) const {
SDLoc DL(N);
switch (N->getOpcode()) {
case ISD::ADD: {
assert(N->getValueType(0) == MVT::i8 && "Unexpected custom legalisation!");
auto NewOp0 = DAG.getNode(ISD::SIGN_EXTEND, DL, MVT::i32, N->getOperand(0));
auto NewOp1 = DAG.getNode(ISD::SIGN_EXTEND, DL, MVT::i32, N->getOperand(1));
auto NewRes = DAG.getNode(ISD::ADD, DL, MVT::i32, NewOp0, NewOp1);
Results.push_back(NewRes);
break;
}
default:
LLVM_DEBUG({ dbgs() << "ReplaceNodeResults: "; N->dump(&DAG); });
llvm_unreachable("Don't know how to custom type legalize this operation!");
}
}
对于成员函数ReplaceNodeResults()
,需要注意地是:
函数返回时,如果第二个参数Results
的大小不为 0,那么其大小必须等于节点N
所定义值的个数。
函数返回时,如果第二个参数Results
的大小为 0,那么用户自定义的类型合法化行为实际上不会对节点N
生效。
step 3: 对于非法操作数,实现用户自定义的类型合法化行为
在子类<Target>TargetLowering
中覆写成员函数LowerOperationWrapper()
实现用户自定义的非法操作数的类型合法化行为。
对于成员函数LowerOperationWrapper()
,需要注意地是:
函数返回时,如果第二个参数Results
的大小不为 0,那么其大小必须等于节点N
所定义值的个数。
函数返回时,如果第二个参数Results
的大小为 0,那么用户自定义的类型合法化行为实际上不会对节点N
生效。
当用户自定义类型合法化行为时,函数ReplaceNodeResults()
先于LowerOperationWrapper()
被调用。这意味着,如果函数ReplaceNodeResults()
中除了合法化非法的操作结果类型以外,还进行了非法操作数类型的合法化;那么函数LowerOperationWrapper()
中的用户自定义的类型合法化行为一定不会生效。
函数TargetLowering::LowerOperationWrapper()
的默认实现会调用LowerOperation()
虚函数。
LLVM
中的操作分为机器无关的和机器相关的。机器无关操作的操作码定义在ISD::NodeType
中(位于llvm/include/llvm/CodeGen/ISDOpcodes.h
文件)。机器相关操作的操作码通常定义在<Target>ISD::NodeType
中(位于llvm/lib/Target/<Target>/<Target>ISelLowering.h
文件)。
通常情况下,每种目标机器仅支持部分机器无关的操作。对于不支持的操作,我们需要将其转换为合法操作。而在操作合法化之前,我们需要指定目标机器中哪些操作是合法的以及如何处理非法操作。需要注意地是, 这里的操作
是由操作码和类型构成的,而类型包括操作数类型和操作结果类型。比如:如果目标机器的合法类型为MVT::i32
和MVT::i64
,但操作码为ISD::ADD
的机器无关操作仅支持操作数类型为MVT::i32
的加法运算,那么操作码为ISD::ADD
且操作数类型为MVT::i64
的操作就属于非法操作。
我们通过在子类<Target>TargetLowering
的构造函数中调用setOperationAction()
指定目标机器的合法操作。比如:
setOperationAction(ISD::SDIV, MVT::i32, Legal);
上述代码表示,如果SDNode
的操作码为ISD::SDIV
,并且操作数和操作结果的类型都是MVT::i32
,那么该操作是合法的(Legal
)。
需要注意地是, 对于机器无关的操作,函数TargetLoweringBase::initActions()
初始化了其默认值(位于llvm/lib/CodeGen/TargetLoweringBase.cpp
文件)。因此,只有目标机器所支持的机器无关操作的默认值不是Legal
时,我们才需要显式地将其指定为目标机器的合法操作。
LLVM
在类TargetLoweringBase
中提供了多个接口,可用于判断是否为目标机器的合法操作。推荐使用:bool isOperationLegal(unsigned Op, EVT VT) const
(位于llvm/include/llvm/CodeGen/TargetLowering.h
文件)。该函数的参数Op
表示操作码,参数VT
表示操作数和操作结果类型;返回值为true
时表示该操作为合法操作,否则为非法操作。
LLVM
中,TargetLoweringBase::LegalizeAction
定义了目标机器中非法操作的所有处理方式(位于llvm/include/llvm/CodeGen/TargetLowering.h
文件),包括如下几种:
Promote
,即将非法操作的操作数和操作结果类型进行提升。实现函数为SelectionDAGLegalize::PromoteNode()
(位于llvm/lib/CodeGen/SelectionDAG/LegalizeDAG.cpp
文件)。
Expand
,即将非法操作尝试转换成其它操作,如果转换失败,则作为LibCall
进行处理。至于要转换成哪种操作,取决于函数SelectionDAGLegalize::ExpandNode()
的实现(位于llvm/lib/CodeGen/SelectionDAG/LegalizeDAG.cpp
文件)。需要注意地是, 如果转换成功,那么转换后的操作不一定是合法的。
LibCall
,即非法操作是函数调用。
Custom
,即定制非法操作的合法化行为。
我们通过在子类<Target>TargetLowering
的构造函数中调用setOperationAction()
指定目标机器中非法操作的处理方式。
指定目标机器中非法操作的处理方式为Promote
:
setOperationAction(ISD::SDIV, MVT::i8, Promote);
上述代码表示,如果SDNode
的操作码为ISD::SDIV
,并且操作数和操作结果的类型都是MVT::i8
,那么该操作是非法的,其操作合法化的处理方式为Promote
。对于这种未显式地指定提升到哪种类型的情况,LLVM
会默认提升到下一个合法类型,并且要求该合法类型的操作合法化的处理方式不是Promote
。需要注意地是, LLVM
的这种默认提升行为仅适用于整型标量。
除此之外,也可以显式地指定提升到哪种类型:
setOperationAction(ISD::SDIV, MVT::i8, Promote);
AddPromotedToType(ISD::SDIV, MVT::i8, MVT::i32);
或
setOperationPromotedToType(ISD::SDIV, MVT::i8, MVT::i32);
上述代码表示,如果SDNode
的操作码为ISD::SDIV
,并且操作数和操作结果的类型都是MVT::i8
,那么该操作是非法的,其操作合法化的处理方式为Promote
,并且操作数和操作结果的类型会提升为MVT::i32
。
指定目标机器中非法操作的处理方式为Expand
:
setOperationAction(ISD::SDIV, MVT::i32, Expand);
上述代码表示,如果SDNode
的操作码为ISD::SDIV
,并且操作数和操作结果的类型都是MVT::i32
,那么该操作是非法的,其操作合法化的处理方式为Expand
。
step 1: 指定操作合法化行为是Custom
在子类<Target>TargetLowering
的构造函数中调用setOperationAction()
函数指明要对非法操作进行操作合法化行为的定制。
setOperationAction(ISD::SDIV, MVT::i8, Custom);
上述代码表示,如果节点的操作码是ISD::SDIV
,并且该节点的操作数和操作结果的类型为MVT::i8
,那么该非法操作的操作合法化行为是用户自定义的。
step 2: 实现用户自定义的操作合法化行为
在子类<Target>TargetLowering
中覆写成员函数LowerOperation()
实现用户自定义的操作合法化行为。比如:
SDValue <Target>TargetLowering::LowerOperation(SDValue Op,
SelectionDAG &DAG) const {
SDLoc DL(Op);
switch (Op.getOpcode()) {
case ISD::ADD: {
assert(Op.getValueType() == MVT::i8 && "Unexpected custom legalisation!");
auto NewOp0 = DAG.getNode(ISD::SIGN_EXTEND, DL, MVT::i32, Op.getOperand(0));
auto NewOp1 = DAG.getNode(ISD::SIGN_EXTEND, DL, MVT::i32, Op.getOperand(1));
return DAG.getNode(ISD::ADD, DL, MVT::i32, NewOp0, NewOp1);
}
default:
LLVM_DEBUG({ dbgs() << "LowerOperation: "; Op.dump(&DAG); });
llvm_unreachable("Don't know how to custom lower this!");
}
return SDValue();
}
上述代码表示,将非法操作(操作码为ISD::ADD
,操作数和操作结果类型为MVT::i8
)合法化为如下 DAG(示意):
t1: i8 = ...
t3: i32 = sign_extend t1
t2: i8 = ...
t4: i32 = sign_extend t2
t5: i32 = add t3, t4
下一篇:LLVM 之后端篇(6):如何基于 Pattern 实现指令选择