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合法化的整体过程。


什么是 SelectionDAG 合法化

LLVM默认的指令选择器是基于SelectionDAG实现的。SelectionDAG是一个有向无环图(Directed-Acyclic-Graph,DAG),其节点为SDNode。每个SDNode对应一条指令或一个操作数。从概念上,SelectionDAG分为legalillegal两种DAG。如果DAG中的所有节点都是目标机器原生支持的,那么称之为legal DAG。也就是说,legal DAG中既不存在目标机器不支持的操作,也不存在目标机器不支持的类型(包括指令的操作数类型及其操作结果类型)。

在指令选择(特指在<Target>DAGToDAGISel::Select()函数被调用)之前,我们需要将illegal DAG转换为legal DAG,该过程称为SelectionDAG合法化。它包括以下两种情况:


类型合法化

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::i32MVT::i64指定为目标机器AArch64的合法类型,并分别对应GPR32allGPR64all寄存器类。

需要注意地是, 目标机器中的每种合法类型只能对应一种寄存器类。这是因为,函数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文件):

方式 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文件)。常见的处理方式如下:

需要注意地是, 对于目标机器中的非法标量类型,LLVM不允许子类<Target>TargetLowering定制其LegalizeTypeAction。而对于目标机器中的非法向量类型,LLVM允许子类<Target>TargetLowering覆写getPreferredVectorAction()函数定制其LegalizeTypeAction

如何合法化目标机器的非法类型

类型合法化的实现为SelectionDAG::LegalizeTypes()函数(位于llvm/lib/CodeGen/SelectionDAG/LegalizeTypes.cpp文件)。它包括以下三种情况:

需要注意地是:

如何定制非法类型的合法化行为

step 1: 指定类型合法化行为是Custom

在子类<Target>TargetLowering的构造函数中调用setOperationAction()函数指明要对哪个节点的哪种非法类型(包括非法的操作数类型和非法的操作结果类型)进行类型合法化行为的定制。比如:

setOperationAction(ISD::ADDMVT::i8Custom);

上述代码表示,如果节点的操作码是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()需要注意地是:

step 3: 对于非法操作数,实现用户自定义的类型合法化行为

在子类<Target>TargetLowering中覆写成员函数LowerOperationWrapper()实现用户自定义的非法操作数的类型合法化行为。

对于成员函数LowerOperationWrapper()需要注意地是:


操作合法化

LLVM中的操作分为机器无关的和机器相关的。机器无关操作的操作码定义在ISD::NodeType中(位于llvm/include/llvm/CodeGen/ISDOpcodes.h文件)。机器相关操作的操作码通常定义在<Target>ISD::NodeType中(位于llvm/lib/Target/<Target>/<Target>ISelLowering.h文件)。

通常情况下,每种目标机器仅支持部分机器无关的操作。对于不支持的操作,我们需要将其转换为合法操作。而在操作合法化之前,我们需要指定目标机器中哪些操作是合法的以及如何处理非法操作。需要注意地是, 这里的操作是由操作码和类型构成的,而类型包括操作数类型和操作结果类型。比如:如果目标机器的合法类型为MVT::i32MVT::i64,但操作码为ISD::ADD的机器无关操作仅支持操作数类型为MVT::i32的加法运算,那么操作码为ISD::ADD且操作数类型为MVT::i64的操作就属于非法操作。

如何指定目标机器的合法操作

我们通过在子类<Target>TargetLowering的构造函数中调用setOperationAction()指定目标机器的合法操作。比如:

setOperationAction(ISD::SDIVMVT::i32Legal);

上述代码表示,如果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文件),包括如下几种:

我们通过在子类<Target>TargetLowering的构造函数中调用setOperationAction()指定目标机器中非法操作的处理方式。

指定目标机器中非法操作的处理方式为Promote

setOperationAction(ISD::SDIVMVT::i8Promote);

上述代码表示,如果SDNode的操作码为ISD::SDIV,并且操作数和操作结果的类型都是MVT::i8,那么该操作是非法的,其操作合法化的处理方式为Promote。对于这种未显式地指定提升到哪种类型的情况,LLVM会默认提升到下一个合法类型,并且要求该合法类型的操作合法化的处理方式不是Promote需要注意地是, LLVM的这种默认提升行为仅适用于整型标量。

除此之外,也可以显式地指定提升到哪种类型:

setOperationAction(ISD::SDIVMVT::i8Promote);
AddPromotedToType(ISD::SDIVMVT::i8MVT::i32);

setOperationPromotedToType(ISD::SDIVMVT::i8MVT::i32);

上述代码表示,如果SDNode的操作码为ISD::SDIV,并且操作数和操作结果的类型都是MVT::i8,那么该操作是非法的,其操作合法化的处理方式为Promote,并且操作数和操作结果的类型会提升为MVT::i32

指定目标机器中非法操作的处理方式为Expand

setOperationAction(ISD::SDIVMVT::i32Expand);

上述代码表示,如果SDNode的操作码为ISD::SDIV,并且操作数和操作结果的类型都是MVT::i32,那么该操作是非法的,其操作合法化的处理方式为Expand

如何定制非法操作的合法化行为

step 1: 指定操作合法化行为是Custom

在子类<Target>TargetLowering的构造函数中调用setOperationAction()函数指明要对非法操作进行操作合法化行为的定制。

setOperationAction(ISD::SDIVMVT::i8Custom);

上述代码表示,如果节点的操作码是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

References


下一篇:LLVM 之后端篇(6):如何基于 Pattern 实现指令选择

上一篇:LLVM 之后端篇(4):理解指令选择的 dump 输出

首页