LLVM 中的指令调度器及其工作过程

LLVM 中的指令调度器及其工作过程

概述

LLVM 中实现了多种指令调度器,分别作用于后端流程的不同阶段,包括指令选择阶段的指令调度器、寄存器分配前的指令调度器和寄存器分配后的指令调度器

这三类调度器都有llc命令行选项可以控制其使能或禁用

在寄存器分配前,基本块中的操作数仍以虚拟寄存器表示,约束较少,指令调度的自由度较高,但要考虑调度结果对寄存器分配的影响。例如,如果虚拟寄存器的定值和使用相距较远,虚拟寄存器的生存期可能较长,这会增加寄存器分配的难度

如果能通过指令重排序,拉近虚拟寄存器的定值和使用之间的距离,可以使寄存器分配难度降低

调度方向一般分为三种,即自顶向下(top down)、自底向上(bottom up) 或 双向(bidirectioin) 调度

自底向上策略较为简单,并且这种策略已有很多成熟编译时优化。双向调度策略,即自顶向下、自底向上同时进行,再从中选出最好的候选指令

如果使用自顶向下调度策略,则以数据依赖图中的入口节点(entry node)为调度起始节点;如果使用自底向上调度策略,则以数据依赖图中的出口节点(exit node) 为调度起始节点

1. 指令选择阶段的调度器

所有调度器的实现类抖继承自ScheduleDAG类。ScheduleDAG类的两个子类分别是ScheduleDAGSDNodes类和ScheduleDAGInsts类中

其中, ScheduleDAGSDNodes 类是指令选择调度器实现类的基类,其调度对象是SDNode实例;ScheduleDAGInstrs 类是寄存器分配前和寄存器分配后的调度器实现类的基类,其调度对象是MachineInstr实例

指令选择通过拓扑排序将DAG转为MachineInstr列表,并结合其他启发式策略,决定MachineInstr列表的指令顺序。作为指令选择过程的一部分,指令选择阶段的指令调度器由ScheduleDAGRRList(其中的RR为Register Reduction 的缩写)类实现

ScheduleDAGRRList 类继承自 ScheduleDAGSDodes类,是LLVM中一种较传统的指令调度器,其目的是将SelectionDAG 中的 SDNode实例转换为MachineInstr 实例。因此,ScheduleDAGRRList 类实现的是一种DAG 调度器

ScheduleDAGRRList 类实现自底向上策略。ScheduleDAGRRList类采用启发式调度策略决定MachineInstr列表中的顺序。启发式调度策略的基本概念是通过结构上层的代价函数对调度候选指令排序,排序的策略包括源顺序(source order)、寄存器压力敏感、物理寄存器复制优先、延迟敏感

ScheduleDAGRRList 类进行调度的基本方法是使用优先级队列为就绪列表,并保存可用节点。然后按优先级顺序每次从次优先级队列中取出一个节点,并检查其调度合法性

如果节点合法则将该节点发出,针对不同策略,在ScheduleDAGRRList类的C++实现文件中注册了四种DAG调度(代码见<llvm_root>/livm/lib/CodeGen/SelectionDAG/ScheduleDAGRRList.cpp):

  • burrListDAGScheduler
  • sourceListDAGScheduler
  • hybridListDAGScheduler
  • ILPListDAGScheduler

其中,burrListDAGScheduler(其中的burr为bottom-up register reduction的缩写) 是一种减少寄存器用量的列表调度器

sourceListDAGScheduler 与 burrListDAGScheduler 类似,但是按源代码顺序调度的列表调度器

hybridListDAGScheduler 是寄存器压力敏感的列表调度器,且其力图在延迟和寄存器压力间保持平衡

ILPListDAGScheduler 也是寄存器压力敏感的列表调度器,但其力图在指令级并行度和寄存器压力间保持平衡

前己述及,LLVM中的指令选择功能在SelectionDAGISel 类中实现。该类继承自MachineFunctionPass类,是基于SelectionDAG的指令选择pass的公共基类

在SelectionDAGISel类的SelectBasicBlock()函数中,其最后一步是调用 CodeGenAndEmitDAG() 函数。该函数在调用 DoInstrucntionSelection()函数完成指令选择后, 首先调用 CreateScheduler()函数生成指令调度器,然后调用调度器的Run()函数,将降级后的DAG转换为机器指令。代码实现如下:

void SelectionDAGISel::CodeGenAndEmitDAG() {
    ...
    DoInstructionSelection();
    
    ...
    
    // Schedule machine code.
    ScheduleDAGSDNodes *Scheduler = CreateScheduler();
    
    {
    NamedRegionTimer T("sched", "Instruction Scheduling", GroupName,
                       GroupDescription, TimePassesIsEnabled);
    Scheduler->Run(CurDAG, FuncInfo->MBB);
   }
}

其中,CreateScheduler() 函数通过ISHeuristic 命令行选项决定使用何种指令调度器

ScheduleDAGSDNodes *SelectionDAGISel::CreateScheduler() {
  return ISHeuristic(this, OptLevel);
}

static cl::opt<RegisterScheduler::FunctionPassCtor, false,
               RegisterPassParser<RegisterScheduler>>
ISHeuristic("pre-RA-sched",
            cl::init(&createDefaultScheduler), cl::Hidden,
            cl::desc("Instruction schedulers available (before register"
                     " allocation):"));

如果llc的命令行选项pre-RA-sched 指定了调度器,则使用指定调度器;否则,调用createDefaultScheduler()函数,生成适合目标后端的指令调度器

createDefaultScheduler() 函数根据后端设置的调度偏好生成对应的调度器,这些调度偏好对应了调度时采用的不同启发式策略

ScheduleDAGSDNodes* createDefaultScheduler(SelectionDAGISel *IS,
                                             CodeGenOpt::Level OptLevel) {
    const TargetLowering *TLI = IS->TLI;
    const TargetSubtargetInfo &ST = IS->MF->getSubtarget();

    // Try first to see if the Target has its own way of selecting a scheduler
    if (auto *SchedulerCtor = ST.getDAGScheduler(OptLevel)) {
      return SchedulerCtor(IS, OptLevel);
    }

    if (OptLevel == CodeGenOpt::None ||
        (ST.enableMachineScheduler() && ST.enableMachineSchedDefaultSched()) ||
        TLI->getSchedulingPreference() == Sched::Source)
      return createSourceListDAGScheduler(IS, OptLevel);
    if (TLI->getSchedulingPreference() == Sched::RegPressure)
      return createBURRListDAGScheduler(IS, OptLevel);
    if (TLI->getSchedulingPreference() == Sched::Hybrid)
      return createHybridListDAGScheduler(IS, OptLevel);
    if (TLI->getSchedulingPreference() == Sched::VLIW)
      return createVLIWDAGScheduler(IS, OptLevel);
    if (TLI->getSchedulingPreference() == Sched::Fast)
      return createFastDAGScheduler(IS, OptLevel);
    if (TLI->getSchedulingPreference() == Sched::Linearize)
      return createDAGLinearizer(IS, OptLevel);
    assert(TLI->getSchedulingPreference() == Sched::ILP &&
           "Unknown sched type!");
    return createILPListDAGScheduler(IS, OptLevel);
  }

目标后端可以为不同的子目标设置不同的调度偏好。例如,AMDGPU后端为其R600和SI 子目标分别设置调度偏好为Source和RegPressure:

R600ISelLowering.cpp

R600TargetLowering::R600TargetLowering(const TargetMachine &TM,
                                       const R600Subtarget &STI)
    : AMDGPUTargetLowering(TM, STI), Subtarget(&STI), Gen(STI.getGeneration()) {
    setSchedulingPreference(Sched::Source);
}

SITargetLowering::SITargetLowering(const TargetMachine &TM,
                                   const GCNSubtarget &STI)
    : AMDGPUTargetLowering(TM, STI),
      Subtarget(&STI) {
    setSchedulingPreference(Sched::RegPressure);      
}

在ScheduleDAGRRList.cpp 文件中,为不同启发式策略实现了不同调度器的生成函数,并在生成函数中生成了对应的优先级队列,而且将优先级队列作为 ScheduleDAGRRList 实例的初始化参数

例如,在上述createDefaultScheduler()函数中,针对RegPressure调度偏好,调用burrListDAGScheduler调度器生成函数 createBURRListDAGScheduler()

该函数实现代码如下:

ScheduleDAGSDNodes *
llvm::createBURRListDAGScheduler(SelectionDAGISel *IS,
                                 CodeGenOpt::Level OptLevel) {
  ...

  BURegReductionPriorityQueue *PQ =
    new BURegReductionPriorityQueue(*IS->MF, false, false, TII, TRI, nullptr);
  ScheduleDAGRRList *SD = new ScheduleDAGRRList(*IS->MF, false, PQ, OptLevel);
  PQ->setScheduleDAG(SD);
  return SD;
}

其中,BURegReductionPriorityQueue 为 RegReductionPriorityQueue 类模板别名。 RegReductionPriorityQueue 类通过模板参数自定义优先调度的排序标准,实现了调度器 burrListDAGScheduler 中使用的优先级队列

相应地,sourceListDAGScheduler、hybridListDAGScheduler 和 ILPListDAGScheduler 都有各自对应的优先级队列实现类

生成指令调度后,CodeGenAndEmitDAG() 函数中调用的调度器Run()函数将进一步调用目标后端调度器的 Schedule()函数,也就是ScheduleDAGRRList 类重写的 Schedule() 函数

SelectionDAGISel.cpp

void SelectionDAGISel::CodeGenAndEmitDAG() {
  ...
  // Schedule machine code.
  ScheduleDAGSDNodes *Scheduler = CreateScheduler();
  {
    NamedRegionTimer T("sched", "Instruction Scheduling", GroupName,
                       GroupDescription, TimePassesIsEnabled);
    Scheduler->Run(CurDAG, FuncInfo->MBB);
  }
  ...
}

ScheduleDAGSDNodes.cpp

/// Run - perform scheduling.
///
void ScheduleDAGSDNodes::Run(SelectionDAG *dag, MachineBasicBlock *bb) {
  BB = bb;
  DAG = dag;

  // Clear the scheduler's SUnit DAG.
  ScheduleDAG::clearDAG();
  Sequence.clear();

  // Invoke the target's selection of scheduler.
  Schedule();
}

ScheduleDAGRRList 类的主要功能可通过重写Schedule() 函数实现,包括建立指令调度所需的依赖图和执行列表调度两个部分。

建立依赖图由ScheduleDAGSDNodes 类的 BuildSchedGraph() 函数完成

这里依赖图是根据输入的SelectionDAG对象构建的SUnit(Scheduling Unit)图。SUnit 图与SelectionDAG相似,但不包括与调度无关的节点,而且,SUnit 图中的每个SUnit对象表示粘合在一起的SDNode 节点

ScheduleDAGRRList 类定义及其成员变量AvailableQueue(表示就绪队列)定义、成员函数Schedule() 声明如下:

/// Schedule - Schedule the DAG using list scheduling.
void ScheduleDAGRRList::Schedule() {
  ...
  
  // Build the scheduling graph.
  BuildSchedGraph(nullptr);

  ...
  
  AvailableQueue->initNodes(SUnits);

  HazardRec->Reset();

  // Execute the actual scheduling loop.
  ListScheduleBottomUp();

  AvailableQueue->releaseState();

  ....
}

BuildSchedGraph() 函数通过聚类、生成SUnit节点、添加调度边三个步骤建立SUnit图

/// BuildSchedGraph - Build the SUnit graph from the selection dag that we
/// are input.  This SUnit graph is similar to the SelectionDAG, but
/// excludes nodes that aren't interesting to scheduling, and represents
/// glued together nodes with a single SUnit.
void ScheduleDAGSDNodes::BuildSchedGraph(AAResults *AA) {
  // Cluster certain nodes which should be scheduled together.
  ClusterNodes();
  // Populate the SUnits array.
  BuildSchedUnits();
  // Compute all the scheduling dependencies between nodes.
  AddSchedEdges();
}

首先,ClusterNodes() 函数将某些需要放在一起调度的节点聚类在一起

  • 例如,对于多个基址相同但偏移量不同,且偏移量相距不远的加载(load) 操作节点
  • 可以通过在加载操作节点间增加粘合依赖,将这些加载操作节点聚类在一起,保证这些加载操作节点以地址升序调度,以此提高缓存局部性
  • 聚类中的加载节点基址是否相同,聚类中加载节点数量和聚类中不同加载节点间的偏移量距离由各后端通过areLoadsFromSameBasePtr() 和 shouldScheduleLoadsNear() 函数实现自行决定
  • 例如, AMDGPU 后端要求聚类的加载节点不超过16个,聚类在一起的加载节点间的偏移量距离不超过64字节

其次,BuildSchedUnits() 函数为SDNode 节点(包括粘合在一起的多个SDNode节点),建立对应的SUnit节点

  • 但是对于某些节点,如ConstantSDNode、RegisterSDNode、GlobalAddressSDNode等,因与调度无关,被称为被动节点(passive node), 被动节点不需要建立对应的SUnit节点
  • 此处还会为每个SUnit节点计算延迟值(Latency),该延迟值可用于设置后续的调度依赖延迟。如果当前SUnit 节点中包含了多个聚类的SDNode节点,则将聚类中所有SDNode 节点的延迟之和作为当前SUnit节点的延迟值
  • 各后端可通过实现各自的getInstrLatency()接口自行决定计算延迟值的方法
  • 例如, AMDGPU 后端的 R600子目标将延迟值固定设置为2,SI子目标则通过指令调度机器模型计算延迟值

最后,AddSchedEdges() 根据SUnit 节点间的调度依赖,在SUnit 节点间增加边

  • 这里涉及的调度依赖分为Barrier和Data两种
  • Barrier 依赖对应操作数的值类型为MVT::Other(表示两个SDNode 节点间通过非数据流链相连)
  • 除MVT::Other 以外的其他值类型对应的是Data 依赖
  • 对于Barrier 依赖,其延迟固定设置为1。对于Data依赖,其延迟为BuildSchedUnits()函数中计算得到的SUnit节点延迟值

建立依赖图, Schedule() 函数继续调用AvailableQueue的initNodes() 函数完成队列初始化

AvailableQueue 是由其父类SchedulingPriorityQueue 指针指向的子类对象。 SchedulingPriorityQueue 类可将不同的优先级计算算法插入列表调度程序,并实现标准优先级队列接口,确保Sunit 节点能以任意顺序插入,并按定义的优先级顺序返回

优先级的计算和队列的表示完全取决于子类实现

AvailableQueue 具体指向哪个子类对象,由ScheduleDAGRRList类提供的调度器生成函数决定。例如,对于burrListDAGScheduler 调度器,AvailableQueue指向的是 BURegReductionPriorityQueue(RegReductionPriorityQueue的别名)类对象

RegReductionPriorityQueue类可根据SUnit节点的Sethi-Ullman值作为优先级(Sethi-Ullman值越小,优先级越高)进行调度,以减少寄存器压力

RegReductionPriorityQueue 类的基类RegReductionPQBase中重写了initNodes()函数,其中调用CalcNodeSethiUllmanNumber()函数实现了SUnit节点的Sethi-Ullman值计算

Sethi-Ullman算法是一种最小化寄存器占用量的调度算法,并可减少中间值溢出及内存恢复的代价

Sethi-Ullman 值的计算方法是遍历当前SUnit节点的所有前驱节点(PreSU),如果发现某个前驱节点的Sethi-Ullman值大于当前SUnit节点的Sethi-Ullman值,则将当前SUnit节点的Sethi-Ullman值设置为该前驱节点的Sethi-Ullman值

否则,当前SUnit节点的Sethi-Ullman值增加1。随后,列表调度的执行由ScheduleDAGRRList 类实现的 ListScheduleBottomUp() 函数完成,代码实现如下:

void ScheduleDAGRRList::ListScheduleBottomUp() {
  // Release any predecessors of the special Exit node.
  ReleasePredecessors(&ExitSU);
    
  ...
  
  while (!AvailableQueue->empty() || !Interferences.empty()) {
    SUnit *SU = PickNodeToScheduleBottomUp();

    AdvancePastStalls(SU);

    ScheduleNodeBottomUp(SU);

    while (AvailableQueue->empty() && !PendingQueue.empty()) {
      // Advance the cycle to free resources. Skip ahead to the next ready SU.
      assert(MinAvailableCycle < std::numeric_limits<unsigned>::max() &&
             "MinAvailableCycle uninitialized");
      AdvanceToCycle(std::max(CurCycle + 1, MinAvailableCycle));
    }
  }

  // Reverse the order if it is bottom up.
  std::reverse(Sequence.begin(), Sequence.end());
}

ListScheduleBottomUp() 函数是自底向上列表调度的主循环。其中,ReleasePredecessors()函数遍历出口节点ExitSU(因为调度方向自底向上,所以从出口节点开始遍历)的每一前驱节点,并递减这些前驱节点的NumSucessLeft值,NumSuccsLeft值表示未调度的后继节点数量

如果某前驱节点的NumSuccsLeft 值到达零,表示该前驱节点的所有后继节点都被调用,则该前驱节点可以被添加到就绪队列AvailableQueue等待调度,并将ExitSU 节点的时钟周期界限(代码中称为 Height) 与 前驱节点的ExitSU 节点间的连接边延迟相加,以二者相加的和更新前驱节点的Height

Height 是在不导致流水线停顿的前提下,可以调度前驱节点的时钟周期。然后,ReleasePredecessors() 函数还会更新两个指针数组LiveRegDefs 和 LiveRegGens

LiveRegDefs 是物理寄存器定值集合,其中的每个元素记录了物理寄存器的定值。相应地,LiveRegGens是物理寄存器使用集合,其中的每个元素记录了物理寄存器的使用,例如,对下面节点序列

flags = (3) add
flags = (2) addc flags
flags = (1) addc flags

LiveRegDefs 中与物理寄存器flags对应的元素(即LiveRegDefs[flags])值为3,LiveRegGens中与物理寄存器flags对应的元素(即LiveRegGens[flags]) 值为1

在调度时,必须先调度LiveRegDefs中的节点,然后才能调度其他修改寄存器的节点。LiveRegDefs 和 LiveRegGens这两个数组可用于寄存器的干扰检查

/// Return a node that can be scheduled in this cycle. Requirements:
/// (1) Ready: latency has been satisfied
/// (2) No Hazards: resources are available
/// (3) No Interferences: may unschedule to break register interferences.
SUnit *ScheduleDAGRRList::PickNodeToScheduleBottomUp() {
      SUnit *CurSU = AvailableQueue->empty() ? nullptr : AvailableQueue->pop();
      auto FindAvailableNode = [&]() {
        while (CurSU) {
          SmallVector<unsigned, 4> LRegs;
          if (!DelayForLiveRegsBottomUp(CurSU, LRegs))
            break;
          LLVM_DEBUG(dbgs() << "    Interfering reg ";
                     if (LRegs[0] == TRI->getNumRegs()) dbgs() << "CallResource";
                     else dbgs() << printReg(LRegs[0], TRI);
                     dbgs() << " SU #" << CurSU->NodeNum << '\n');
          auto [LRegsIter, LRegsInserted] = LRegsMap.try_emplace(CurSU, LRegs);
          if (LRegsInserted) {
            CurSU->isPending = true;  // This SU is not in AvailableQueue right now.
            Interferences.push_back(CurSU);
          }
          else {
            assert(CurSU->isPending && "Interferences are pending");
            // Update the interference with current live regs.
            LRegsIter->second = LRegs;
          }
          CurSU = AvailableQueue->pop();
        }
      };
      ...
}

PickNodeToScheduleBottomUp() 函数可以从就绪队列 AvailableQueue 中取出当前 SUnit 节点,并检查其延迟、干扰是否满足调度要求

当前SUnit 节点可以调用需要满足的要求有三项: 第一,当前时钟周期满足当前SUnit 节点延迟要求;第二,有满足调度的可用资源;第三,不存在寄存器干扰

如果上述三项要求都满足,且其前驱节点的计数减少到零,则调用 ScheduleNodeBottomUp() 函数将当前SUnit 节点加入 AvailableQueue 中调度,并更新流水线、计分板、寄存器压力、LiveRegDefs、LiveRegGens等状态

ScheduleNodeBottomUp()函数通过 ScheduleHazardRecognizer 对象HazardRec 调用指令发射函数 EmitInstrunction(), ScheduleHazardRecognizer 对象决定是否应在当前时钟周期发射指令,并且在当前时钟周期发射指令一旦导致流水线停顿,是否发射其他就绪指令,或者插入空操作(nop)

2. 寄存器分配前的调度器

寄存器分配前指令调度器将DAG 中的节点拓扑结构排序。寄存器分配高度依赖于寄存器分配前指令调度器产生的指令顺序,该指令顺序决定了寄存器压力,即同时处于活动状态且必须分配给不同物理寄存器的虚拟寄存器的数量

因此,寄存器分配前指令调度必须最小化寄存器压力以优化性能

寄存器分配前调度器的实现类是 ScheduleDAGMILive 类(实现代码见<llvm_root>/llvm/lib/GodeGen/MachineScheduler.cpp)

ScheduleDAGMILive 类是ScheduleDAGMI 类的子类,而ScheduleDAGMI 类又是 ScheduleDAGInstrs 的子类

ScheduleDAGMILive 类中实现了寄存器分配前调度器和寄存器分配后调度器的共用功能,并可以根据给定的MachineSchedStrategy 策略接口调度机器指令,为配置调度器提供更多灵活性

和ScheduleDAGMI 相比,和ScheduleDAGMILive 类在构建DAG并驱动列表调度同时,还会更新指令流、寄存器压力和LiveInterval等信息。因此,ScheduleDAGMILive 类主要用在寄存器分配前

寄存器分配前的指令调度pass 由 MachineScheduler 类实现(代码可见 MachineSchedule.cpp)

MachineScheduler 类在 phi 消除之后调度机器指令,其入口函数 runOnMachineFunction() 首先初始化 pass 上下文,获得MachineLoopInfo、MachineDominatorTree、TargetPassConfig、AAResultsWrapperPass、LiveIntervals 等 pass 的分析结果

MachineScheduler 类会保留 LiveIntervals 分析结果,用于寄存器分配前指令调度。初始化后, runOnMachineFunction() 函数按顺序访问基本块,并将每个块划分为更小的的调度区域(scheduling region), 然后以调度区域为单位,对其中的指令进行调度

bool MachineScheduler::runOnMachineFunction(MachineFunction &mf) {
  ...

  if (EnableMachineSched.getNumOccurrences()) {
    if (!EnableMachineSched)
      return false;
  } else if (!mf.getSubtarget().enableMachineScheduler())
    return false;

  // 初始化pass 上下文
  AA = &getAnalysis<AAResultsWrapperPass>().getAAResults();
  LIS = &getAnalysis<LiveIntervals>();

  ... 
  
  std::unique_ptr<ScheduleDAGInstrs> Scheduler(createMachineScheduler());
  scheduleRegions(*Scheduler, false);

}

调度区域的划分由上述getSchedRegions() 函数完成. 默认的调度边界有三种: 1. 基本块的结束指令(如ADMGPU ISA中的S_ENDPGM) 指令,自然也是调度区域的边界。2. 指令调度不能跨函数调用,所以,函数调用也是调度区域的边界。3.修改堆栈指针是调度区域的边界

因此,一个调度区域可以被认为是一段直线(straight-line) 代码,即一个基本块或一个基本块的一部分。不同后端可根据各自的需求,通过调用isSchedulingBoundary()接口,设置自己的边界类型,并修改默认的边界类型设定

  • 例如,AMDGPU 后端将修改EXEC寄存器的指令也作为调度器边界
  • 为了与DAG构建器保持一致,runOnMachineFunction()函数采用自底向上的顺序访问调度区域

在执行主要功能前,runOnMachineFunction()函数首先通过选项变量 EnableMachineSched 检查llc 命令行选项enable-misched 是否启用寄存器分配前指令调度,如果未启用,则返回

MachineScheduler 类的 createMachineScheduler() 函数通过PassConfig指针调用目标自定义的 createMachineScheduler() 函数。对于AMDGPU 后端来说,如果llc命令行选项指令的目标是amdgcn,则调用GCNPassConfig::createMachineScheduler(), 其代码实现如下:

/// Instantiate a ScheduleDAGInstrs that will be owned by the caller.
ScheduleDAGInstrs *MachineScheduler::createMachineScheduler() {
  // Select the scheduler, or set the default.
  MachineSchedRegistry::ScheduleDAGCtor Ctor = MachineSchedOpt;
  if (Ctor != useDefaultMachineSched)
    return Ctor(this);

  // Get the default scheduler set by the target for this function.
  ScheduleDAGInstrs *Scheduler = PassConfig->createMachineScheduler(this);
  if (Scheduler)
    return Scheduler;

  // Default to GenericScheduler.
  return createGenericSchedLive(this);
}

MacineScheduler 的主要功能是在scheduleRegions() 函数中实现。该函数被寄存器分配前调度pass实现类MachineScheduler和寄存器分配后调度pass实现类PostMachineScheduler类共用

通过 scheduleRegions() 函数的第二个参数FixKillFlags 可以区分函数调用方。当MachineScheduler 类调用 scheduleRegions() 函数时,FixKillFlags 设为 false;当PostMachineScheduler 类调用 schedulRegions()函数时,FixKillFlags 设为true。

scheduleRegions()函数实现代码如下:

/// Main driver for both MachineScheduler and PostMachineScheduler.
void MachineSchedulerBase::scheduleRegions(ScheduleDAGInstrs &Scheduler,
                                           bool   ) {
  // Visit all machine basic blocks.
  //
  // TODO: Visit blocks in global postorder or postorder within the bottom-up
  // loop tree. Then we can optionally compute global RegPressure.
  for (MachineFunction::iterator MBB = MF->begin(), MBBEnd = MF->end();
       MBB != MBBEnd; ++MBB) {

    Scheduler.startBlock(&*MBB);

    // Break the block into scheduling regions [I, RegionEnd). RegionEnd
    // points to the scheduling boundary at the bottom of the region. The DAG
    // does not include RegionEnd, but the region does (i.e. the next
    // RegionEnd is above the previous RegionBegin). If the current block has
    // no terminator then RegionEnd == MBB->end() for the bottom region.
    //
    // All the regions of MBB are first found and stored in MBBRegions, which
    // will be processed (MBB) top-down if initialized with true.
    //
    // The Scheduler may insert instructions during either schedule() or
    // exitRegion(), even for empty regions. So the local iterators 'I' and
    // 'RegionEnd' are invalid across these calls. Instructions must not be
    // added to other regions than the current one without updating MBBRegions.

    MBBRegionsVector MBBRegions;
    getSchedRegions(&*MBB, MBBRegions, Scheduler.doMBBSchedRegionsTopDown());
    for (const SchedRegion &R : MBBRegions) {
      MachineBasicBlock::iterator I = R.RegionBegin;
      MachineBasicBlock::iterator RegionEnd = R.RegionEnd;
      unsigned NumRegionInstrs = R.NumRegionInstrs;

      // Notify the scheduler of the region, even if we may skip scheduling
      // it. Perhaps it still needs to be bundled.
      Scheduler.enterRegion(&*MBB, I, RegionEnd, NumRegionInstrs);

      // Skip empty scheduling regions (0 or 1 schedulable instructions).
      if (I == RegionEnd || I == std::prev(RegionEnd)) {
        // Close the current region. Bundle the terminator if needed.
        // This invalidates 'RegionEnd' and 'I'.
        Scheduler.exitRegion();
        continue;
      }
     
      if (DumpCriticalPathLength) {
        errs() << MF->getName();
        errs() << ":%bb. " << MBB->getNumber();
        errs() << " " << MBB->getName() << " \n";
      }

      // Schedule a region: possibly reorder instructions.
      // This invalidates the original region iterators.
      Scheduler.schedule();

      // Close the current region.
      Scheduler.exitRegion();
    }
    Scheduler.finishBlock();
    // FIXME: Ideally, no further passes should rely on kill flags. However,
    // thumb2 size reduction is currently an exception, so the PostMIScheduler
    // needs to do this.
    if (FixKillFlags)
      Scheduler.fixupKills(*MBB);
  }
  Scheduler.finalizeSchedule();
}

其中,getSchedRegions() 函数按上述调度边界标准完成调度区域划分,基本块被分解为一系列调度去区域

单个基本块中的所有调度区域保存在调度区域向量MBBRegions 中,调度区域由结构体SchedRegion表示。

结构体SchedRegion 中 的 RegionBegin 和 RegionEnd 字段分别指向调度区域的开始和结束指令,NumRegionInstrs 字段表示调度区域中机器指令的数量
在这里插入图片描述

scheduleRegions() 函数的主体是遍历调度区域向量MBBRegions, 并对其中的每个调度区域调用调度器的enterRegion()、schedule() 和 exitRegion() 函数

其中,enterRegion() 函数的作用有两个

  • 1,用当前调度区域的RegionBegin、RegionEnd和NumRegionInstrs 字段值设置调度器对应成员变量
  • 2,设置区域调度策略。区域调度策略分为寄存器压力和调度方向两部分

寄存器压力策略决定是否启用寄存器压力跟踪。如果启用压力跟踪,当检测压力过大时,调度器可以采用措施减小压力,改善性能

为了节省编译时间,默认的寄存器压力策略是避免在较小的区域设置寄存器压力监视器,只有当可调度指令的数量超过寄存器总量的一半时,才启动跟踪寄存器压力, 调度器完成当前区域调度后调用exitRegion() 函数,当前该函数为空

在这里插入图片描述

enterRegion() 函数执行完成后,scheduleRegions() 函数调用Scheduler.schedule() 对当前区域调度执行指令调度功能

schedule()函数根据各后端制定的调度策略,选择和调度符合要求的指令

  • 例如,AMDGPU 后端的GCN 子目标通过调用GCNScheduleDAGMILive::schedule() 函数完成寄存器分配前指令调度
  • 该函数调用流程如图4-12所示
void GCNScheduleDAGMILive::finalizeSchedule() {
  // Start actual scheduling here. This function is called by the base
  // MachineScheduler after all regions have been recorded by
  // GCNScheduleDAGMILive::schedule().
  LiveIns.resize(Regions.size());
  Pressure.resize(Regions.size());
  RescheduleRegions.resize(Regions.size());
  RegionsWithHighRP.resize(Regions.size());
  RegionsWithExcessRP.resize(Regions.size());
  RegionsWithMinOcc.resize(Regions.size());
  RegionsWithIGLPInstrs.resize(Regions.size());
  RescheduleRegions.set();
  RegionsWithHighRP.reset();
  RegionsWithExcessRP.reset();
  RegionsWithMinOcc.reset();
  RegionsWithIGLPInstrs.reset();

  runSchedStages();
}

void GCNScheduleDAGMILive::runSchedStages() {
  LLVM_DEBUG(dbgs() << "All regions recorded, starting actual scheduling.\n");

  if (!Regions.empty())
    BBLiveInMap = getBBLiveInMap();

  GCNSchedStrategy &S = static_cast<GCNSchedStrategy &>(*SchedImpl);
  while (S.advanceStage()) {
    auto Stage = createSchedStage(S.getCurrentStage());
    if (!Stage->initGCNSchedStage())
      continue;

    for (auto Region : Regions) {
      RegionBegin = Region.first;
      RegionEnd = Region.second;
      // Setup for scheduling the region and check whether it should be skipped.
      if (!Stage->initGCNRegion()) {
        Stage->advanceRegion();
        exitRegion();
        continue;
      }

      ScheduleDAGMILive::schedule();
      Stage->finalizeGCNRegion();
    }

    Stage->finalizeGCNSchedStage();
  }
}

最后会调用 ScheduleDAGMILive::schedule() 函数中的指令调度功能(<llvm_root>/llvm/lib/Target/AMDGPU/GCNSchedStrategy.h)

void ScheduleDAGMILive::schedule() {
  buildDAGWithRegPressure();
  postprocessDAG();

  SmallVector<SUnit*, 8> TopRoots, BotRoots;
  findRootsAndBiasEdges(TopRoots, BotRoots);

  // Initialize the strategy before modifying the DAG.
  // This may initialize a DFSResult to be used for queue priority.
  SchedImpl->initialize(this);

  ...

  // Initialize ready queues now that the DAG and priority data are finalized.
  initQueues(TopRoots, BotRoots);

  bool IsTopNode = false;
  while (true) {
    SUnit *SU = SchedImpl->pickNode(IsTopNode);
    if (!SU) break;
    ... 
   

    scheduleMI(SU, IsTopNode);

    ...
  }
  ...
}

其中,buildDAGWithRegPressure() 的作用是构造DAG,并初始化三个寄存器压力监视器RPTracker、TopRPTraker和BotRPTracker。 代码如下:

/// Build the DAG and setup three register pressure trackers.
void ScheduleDAGMILive::buildDAGWithRegPressure() {
  if (!ShouldTrackPressure) {
    RPTracker.reset();
    RegionCriticalPSets.clear();
    buildSchedGraph(AA);
    return;
  }

  // Initialize the register pressure tracker used by buildSchedGraph.
  RPTracker.init(&MF, RegClassInfo, LIS, BB, LiveRegionEnd,
                 ShouldTrackLaneMasks, /*TrackUntiedDefs=*/true);

  // Account for liveness generate by the region boundary.
  if (LiveRegionEnd != RegionEnd)
    RPTracker.recede();

  // Build the DAG, and compute current register pressure.
  buildSchedGraph(AA, &RPTracker, &SUPressureDiffs, LIS, ShouldTrackLaneMasks);

  // Initialize top/bottom trackers after computing region pressure.
  initRegPressure();
}

如果在区域调度策略中启用寄存器压力跟踪(即ShouldTrackerPressure变量为真), buildDAGWithRegPressure()函数调用RPTracker.init()函数[即RegPressureTraker类的init()函数]初始化寄存器压力监视器RPTraker

RegPressureTracker 类用于跟踪MachineBasicBlock 内的指令序列在某个指令位置的当前寄存器压力,并记录已遍历的区域内达到的最高寄存器压力值

RegPressureTraker 类的成员变量CurrSetPresusure(无符号整型向量类型)用于记录各寄存器类的寄存器压力值

RegPressureTracker 类的成员变量P(RegisterPressure) 结构体类型,保存了寄存器压力结果,其中的MaxSetPressure字段记录了当前调度区域中各寄存器迄今最大的寄存器压力值(或称峰值寄存器压力)

在指令调度中,表征某种类型寄存器在线程中使用量的指标是峰值寄存器压力。需要注意的是,由于寄存器分配是 NP-hard 问题,寄存器分配算法产生的可能是次优解,使其寄存器使用量超过峰值寄存器压力

RPTracker 初始化完成后,buildDAGWithRegPressure() 接着调用 buildSchedGraph() 函数,构造当前调度区域的DAG,并计算当前寄存器压力值

然后,buildSchedGraph() 函数调用 ScheduleDAGInstrs::addSchedBarrierDeps() 函数,从ExitSU节点开始,按自底向上的顺序扫描指令序列,并按照指令操作数的定值-使用链,建立SUnit节点之间的依赖关系

void ScheduleDAGInstrs::buildSchedGraph(...) {
  ...

  // Create an SUnit for each real instruction.
  initSUnits();
  
  if (PDiffs)
    PDiffs->init(SUnits.size());
    
  ... 
  
  // Model data dependencies between instructions being scheduled and the
  // ExitSU.
  addSchedBarrierDeps();
  
  ...
  // Walk the list of instructions, from bottom moving up.
  MachineInstr *DbgMI = nullptr;
  for (MachineBasicBlock::iterator MII = RegionEnd, MIE = RegionBegin;
       MII != MIE; --MII) {
    
    
    ....
    
    if (RPTracker) {
      ...
      
      if (PDiffs != nullptr)
        PDiffs->addInstruction(SU->NodeNum, RegOpers, MRI);

      ..
      RPTracker->recede(RegOpers);
    }

此后,buildSchedGraph() 函数遍历当前调度区域,并调用PDiffs->addInstrunction() 函数为其中的每一条机器指令计算寄存器压力变化值,保存在向量PDiffArray->addInstrunction() 函数为其中的每一条机器指令计算寄存器压力变化值,保存在向量PDiffArray中,并调用RPTracker->recode() 分析机器指令的每一个操作数

如果操作数是定值,因为定值将截断操作数寄存器生存期,并减轻寄存器压力,因此调用decreaseRegPressure()函数,将RPTracker的变量CurrSetPressure 中与操作数寄存器所属寄存器类相对应的寄存器压力值递减

如果操作数是使用,因为使用将延伸操作数寄存器的生存期,并增加寄存器压力,因此调用increaseRegPressure() 函数,将CurrSetPressure中与操作数寄存器所属寄存器类相对应的寄存器压力值递增

buildSchedGraph() 函数实现代码

void ScheduleDAGInstrs::buildSchedGraph(AAResults *AA,
                                        RegPressureTracker *RPTracker,
                                        PressureDiffs *PDiffs,
                                        LiveIntervals *LIS,
                                        bool TrackLaneMasks) {
  // Create an SUnit for each real instruction.
  initSUnits();
  
  if (PDiffs)
    PDiffs->init(SUnits.size());
    
  for (MachineBasicBlock::iterator MII = RegionEnd, MIE = RegionBegin;
       MII != MIE; --MII) {
       if (RPTracker) {
          ...
          if (TrackLaneMasks) {
            SlotIndex SlotIdx = LIS->getInstructionIndex(MI);
            RegOpers.adjustLaneLiveness(*LIS, MRI, SlotIdx);
          }
          
          if (PDiffs != nullptr)
            PDiffs->addInstruction(SU->NodeNum, RegOpers, MRI);

          ...
          
          RPTracker->recede(RegOpers);
    }
  }
}

buildSchedGraph() 函数结束后,buildDAGWithRegPressure() 函数继续调用initRegPressure() 函数,为自顶向下和自底向上调度分别初始化寄存器压力监视器TopRPTracker 和 BotBPTracker

RPTracker 和 TopRPTrakcer、BotRPTracker的不同之处在于,RPTracker中记录的压力值覆盖整个调度区域,而TopTracker 和 BottomTracker分别用于自顶向下和自底向上两个方向的指令调度过程中,其指令指针起始位置分别初始化为调度区域的顶部和底部,但不覆盖任何未被调度指令

与RPTracker监视器类似,TopTracker 和 BottomTracker 监视器也有各自的CurrSetPressure 和 MaxSetPressure 变量,分别记录自顶向下调度和自底向上调度过程中各个寄存器类的压力值变化

RPTrakcer 和 TopRPTracker、BotRPTracker 中维护的CurrSetPressure 和 MaxSetPressure 变量在后续的调度函数pickNode()中会用到

buildDAGWithRegPressure() 函数结束后,ScheduleDAGMILive::schedule() 函数继续调用postprocessDAG()函数处理ScheduleDAGMutation对象

然后,ScheduleDAGMILive::schedule() 函数调用 findRootsAndBiasEdges()函数,遍历调度区域内的所有Sunit节点,根据 SUnit节点的NumPredsLeft 和 NumSuccsLeft 变量值,决定将 SUnit 节点放在自顶向下调度根节点队列(TopRoots) 中,还是放在自底向上调度根节点队列(BotRoots)中

如果 NumPredsLeft 值为0,表示SUnit 节点没有前驱节点,则将 SUnit 节点放在自顶向下调度根节点队列;如果 NumSucessLeft 值为0,表示SUnit 节点没有后续节点,则将SUnit 节点放在自底向上调度根节点队列

即 TopRoots 保存调度器区域依赖图的最顶层节点,BotRoots 保存调度器区域依赖图的最底层节点

ScheduleDAGMILive::schedule() 函数接下来调用的 initQueues() 函数使用TopRoots 和 BotRoots 队列中的节点初始化指令调度就绪队列。然后,分别通过从入口节点EntrySU 开始遍历后继节点,和从出口节点ExitSU 开始遍历前驱节点,分别建立不同调度方向的就绪队列

上述准备工作完成后,ScheduleDAGMILive::schedule() 函数通过调度策略实现类对象SchedImpl,调用pickNode() 函数选择调度节点。各后端可以有自己的调度策略实现类

例如,对于 AMDGPU 后端,根据区域调度策略中的 OnlyTopDonw 和 OnlyBottomUp 字段值,其GCN子对象调度策略实现类 GCNMaxOccupancySchedStrategy 中 的 pickNode() 函数支持自顶向上、自底向上和双向三种方向的调度方法(该函数实现逻辑与G enericScheduler::pickNode() 函数相同)

单向调度功能在pickNodeFromQueue()函数中实现,双向调度功能在pickNodeBidirectional() 函数中实现。pickNodeFromQueue() 函数和 pickNodeBidirectional() 函数都可以调用GenericScheduler::tryCandidate()函数

该函数出于便利性和效率方面的考虑,没有采用代价模型,而是通过启发式策略,综合平衡物理寄存器生存期、寄存器压力、延迟、关键资源(critical resource)等因素,从而在挑选节点时确定最优的调度节点

pickNode() 函数实现代码如下

/// Pick the best node to balance the schedule. Implements MachineSchedStrategy.
SUnit *GenericScheduler::pickNode(bool &IsTopNode) {
  ...
  
  do {
    if (RegionPolicy.OnlyTopDown) {
      ...
        pickNodeFromQueue(Top, NoPolicy, DAG->getTopRPTracker(), TopCand);
        ... 
        SU = TopCand.SU;
     
    } else if (RegionPolicy.OnlyBottomUp) {
      ...
        pickNodeFromQueue(Bot, NoPolicy, DAG->getBotRPTracker(), BotCand);
        ...
        SU = BotCand.SU;
      
    } else {
      SU = pickNodeBidirectional(IsTopNode);
    }
  } while (SU->isScheduled);

  ...
  return SU;
}

3. 寄存器分配后的调度器

寄存器分配后调度器的实现类有两个。默认的寄存器分配后调度器由 SchedulePostRATDList(其中的TD 表示 Top Down) 类实现,另一个可选的调度器实现类是ScheduleDAGMI 类,二者都是SceduleDAGInstrs类的子类

寄存器分配后指令调度pass实现也有两个: PostRAScheduler 类 和 PostMachineScheduler类,二者都是 MachineFunctionPass 类的子类

默认的寄存器分配后指令调度pass实现类 PostRAScheduler. PostRAScheduler 类与调度器实现类 SchedulePostRATDList 配合完成调度功能。相应地,PostMachineScheduler 类与调度器实现类 ScheduleDAGMI 配合完成调度功能

如果后端希望采用PostMachineScheduler 类调度 pass 而不是默认的 PostRAScheduler类,则需要在后端代码生成器配置选项pass中调用 substituePass()接口,将PostRAScheduler调度 pass替换为 PostMachineScheduler 调度pass。如果一来,后端使用调度器实现类也改为了ScheduleDAGMI

  • 例如,PowerPC 后端在高于O0的优化级别下使用 PostMachineScheduler 类调用pass

代码实现如下:

/// PPC Code Generator Pass Configuration Options.
class PPCPassConfig : public TargetPassConfig {
public:
  PPCPassConfig(PPCTargetMachine &TM, PassManagerBase &PM)
    : TargetPassConfig(TM, PM) {
    // At any optimization level above -O0 we use the Machine Scheduler and not
    // the default Post RA List Scheduler.
    if (TM.getOptLevel() != CodeGenOpt::None)
      substitutePass(&PostRASchedulerID, &PostMachineSchedulerID);
  }

SchedulePostRATDList 类中重写了纯虚函数 schedule(), 并在调度pass类 PostRAScheduler 的 runOnMachineFunction() 函数中调用该函数

PostRAScheduler::runOnMachineFunction() 函数实现代码如下:

bool PostRAScheduler::runOnMachineFunction(MachineFunction &Fn) {
    ...
    // Check that post-RA scheduling is enabled for this target.
    // This may upgrade the AntiDepMode.
    if (!enablePostRAScheduler(Fn.getSubtarget(), PassConfig->getOptLevel(),
                             AntiDepMode, CriticalPathRCs))
    
        return false;
    
    ...
    
    SchedulePostRATDList Scheduler(Fn, MLI, AA, RegClassInfo, AntiDepMode,
                                 CriticalPathRCs);
    
    ...
    
    // Loop over all of the basic blocks
    for (auto &MBB : Fn) {
        ...
        // Initialize register live-range state for scheduling in this block.
        Scheduler.startBlock(&MBB);
        
        ...
        
        for (MachineBasicBlock::iterator I = Current; I != MBB.begin();) {
            ...
            Scheduler.schedule();
            
          }
          ...
        }
    }
}

当 llc 命令行选项 post-RA-scheduler为false时,enablePostRAScheduler() 函数返回值为false。此时,PostRAScheduler::runOnMachineFunction() 函数在生成 SchedulePostRATDList 对象前返回,不会执行调度pass

SchedulePostRATDList 类主要功能在schedule() 函数中实现,实现代码如下:

/// Schedule - Schedule the instruction range using list scheduling.
///
void SchedulePostRATDList::schedule() {
  // Build the scheduling graph.
  buildSchedGraph(AA);

  ...

  postprocessDAG();


  AvailableQueue.initNodes(SUnits);
  ListScheduleTopDown();
  AvailableQueue.releaseState();
}

其中的buildSchedGraph() 函数由与寄存器分配前调度器共用的基类 ScheduleDAGInstrs 中实现。但对于寄存器分配后调度器,已不需要考虑寄存器压力,因此和寄存器压力相关(如寄存器压力监视器、寄存器压力变化等)的逻辑都不在执行

buildSchedGraph() 函数的其他功能,如为机器指令构建对应的SUint 节点及其依赖图与ScheduleDAGMILive::postprocessDAG() 函数一样,SchedulePostRATDList::postprocessDAG() 函数功能是应用之前添加的 ScheduleDAGMutation 对象

SUnit 节点的调度在ListScheduleTopDown() 函数中完成。该函数是自顶向下列表调度的主循环。SchedulePostRATDList 类中维护了两个SUnit节点队列: AvaliableQueue 和 PendingQueue.

其中 AvaliableQueue 是就绪队列。PendingQueue 中包含操作数已发射,但由于操作的延迟,执行结果尚未准备好的指令。一旦这些指令的操作数已发射,但由于操作的延迟,执行结果尚未准备好的所有指令

一旦这些指令的操作数都可用,就会添加到AvailableQueue中。ListScheduleTopDown()函数调用 SchedulePostRATDList::ScheduleNodeTopDown()函数,依此调度AvailableQueue中的SUnit节点

如果某SUnit节点被调度,ReleaseSuccessors() 函数会从该节点开始遍历其每一个后续节点,并递减这些后继节点的NumPredsLeft计数

如果计数达到零,则将其添加到PeingQueue,等待调度。然后, AvailableQueue.scheduledNode()函数遍历该节点的所有后继节点,将其中只有一个前驱节点的后继节点的优先级调高,使其能提前被调度

ScheduleNodeTopDown()函数实现代码如下:

/// ScheduleNodeTopDown - Add the node to the schedule. Decrement the pending
/// count of its successors. If a successor pending count is zero, add it to
/// the Available queue.
void SchedulePostRATDList::ScheduleNodeTopDown(SUnit *SU, unsigned CurCycle) {
  LLVM_DEBUG(dbgs() << "*** Scheduling [" << CurCycle << "]: ");
  LLVM_DEBUG(dumpNode(*SU));

  Sequence.push_back(SU);
  assert(CurCycle >= SU->getDepth() &&
         "Node scheduled above its depth!");
  SU->setDepthToAtLeast(CurCycle);

  ReleaseSuccessors(SU);
  SU->isScheduled = true;
  AvailableQueue.scheduledNode(SU);
}

另一种寄存器分配后调度pass实现类PostMachineScheduler 的入口函数 runOnMachineFunctioin() 与寄存器分配前调度pass实现类 PostMachineScheduler的入口函数 runOnMachineFunction() 与 寄存器分配前调度 pass 实现类 MachineScheduler 类的入口函数 runOnMachineFunction() 类似,同样首先需要初始化pass的上下文

2者的不同之处在于,PostMachineScheduler 类用于寄存器分配后指令调度,因此不需要获得LiveIntervals分析结果

PostMachineScheduler::runOnMachineFunction()

bool PostMachineScheduler::runOnMachineFunction(MachineFunction &mf) {
  if (skipFunction(mf.getFunction()))
    return false;

  if (EnablePostRAMachineSched.getNumOccurrences()) {
    if (!EnablePostRAMachineSched)
      return false;
  } else if (!mf.getSubtarget().enablePostRAMachineScheduler()) {
    LLVM_DEBUG(dbgs() << "Subtarget disables post-MI-sched.\n");
    return false;
  }
  LLVM_DEBUG(dbgs() << "Before post-MI-sched:\n"; mf.print(dbgs()));

  // Initialize the context of the pass.
  MF = &mf;
  MLI = &getAnalysis<MachineLoopInfo>();
  PassConfig = &getAnalysis<TargetPassConfig>();
  AA = &getAnalysis<AAResultsWrapperPass>().getAAResults();

  if (VerifyScheduling)
    MF->verify(this, "Before post machine scheduling.");

  // Instantiate the selected scheduler for this target, function, and
  // optimization level.
  std::unique_ptr<ScheduleDAGInstrs> Scheduler(createPostMachineScheduler());
  scheduleRegions(*Scheduler, true);

  if (VerifyScheduling)
    MF->verify(this, "After post machine scheduling.");
  return true;
}

当 llc 命令行选项 enable-post-misched 为false 时,选项变量 EnablePostRAMachineSched 为 false, 函数返回,不会执行调度 pass

与 MachineScheduler 类的 createMachineScheduler() 函数类似, PostMachineScheduler 类的 createPostMachineScheduler() 函数也是通过 PassConfig指针调用目标自定义的 createPostMachineScheduler() 函数

如果后端(如AMDPGU 后端)没有实现自定义寄存器分配后的调度器,则PostMachineScheduler 类的 createPostMachineScheduler() 函数通过调用 createGenericSchedPostRA() 函数,生成和使用默认的调度器

相关代码实现如下:

/// Instantiate a ScheduleDAGInstrs for PostRA scheduling that will be owned by
/// the caller. We don't have a command line option to override the postRA
/// scheduler. The Target must configure it.
ScheduleDAGInstrs *PostMachineScheduler::createPostMachineScheduler() {
  // Get the postRA scheduler set by the target for this function.
  ScheduleDAGInstrs *Scheduler = PassConfig->createPostMachineScheduler(this);
  if (Scheduler)
    return Scheduler;

  // Default to GenericScheduler.
  return createGenericSchedPostRA(this);
}

ScheduleDAGMI *llvm::createGenericSchedPostRA(MachineSchedContext *C) {
  return new ScheduleDAGMI(C, std::make_unique<PostGenericScheduler>(C),
                           /*RemoveKillFlags=*/true);
}

上述 createGenericSchedPostRA() 函数在生成 ScheduleDAGMI 实例时,插入了用于寄存器分配后调度策略实现类 PostGenericScheduler

与寄存器分配前调度策略实现类 GenericScheduler类似,PostGenericScheduler 类也实现了 picnNode()、pickNodeFromQueue()、tryCandidate() 等接口,可通过一系列启发式策略,决定指令调度方式

例如,在调度期间跟踪寄存器压力值,如果寄存器压力值接近物理限制(可物理寄存器的数量),则优先选择、调度最小化寄存器压力值的指令。降低寄存器压力对CPU和GPU 有不同的意义

在CPU架构上降低寄存器压力的目的是为了避免溢出,而在GPU架构上降低寄存器压力的主要目的是增加占用率,因为占用率会显著影响GPU应用性能

LLVM 后端可以根据各自硬件架构特定定义自己的调度策略接口(MachineSchedStrategy),并根据自定义启发式策略选择合适的候选指令

PostMachineScheduler::runOnMacineFunction() 函数中还调用了scheduleRegions() 函数

综上所述,LLVM 的指令调度过程涉及的调度器类型和相关类定义较多,总结后可将这些类定义分为三种: 调用pass类、调度器实现类和调度策略类 根据调度发生的位置不同,这些类的基类各不相同

寄存器分配前和寄存器分配后调度 pass 类的基类都为 MacruneFunctionPass 类

其中 , 寄存器分配前调度 pass 由 MachineScheduler 类实现,寄存器分配后调度 pass 由 PostRAScheduler 类或 PostMachineScheduler 类实现

指令选择阶段没有单独的调度 pass, 调度功能在指令选择 pass 实现类 SelectionDAGISel 中完成。该类的基类也是 MachineFunctionPass 类

寄存器分配前和寄存器分配后调度器实现类的基类都为 ScheduleDAGlnstrs 类。 其中,寄存器分 配前调度器由 ScheduleDAGMILive类实现 ,寄存器分配后调度器由 SchedulePostRATDList 类或 ScheduleDAGMI 类实现

指令选择阶段的调度器实现类 ScheduleDAGRRList 继承自 ScheduleDAGSDNodes 类。

寄存器分配前和寄存器分配后调度策略实现类的基类都为 MachineSchedStrategy 类

其中,寄存器分配前调度策略由 GenericScheduler 类或其派生类实现,与由 ScheduleDAGMILive 类实现的寄存器分
配前调度器配合使用

寄存器分配后调度策略由 PostGenericScheduler 类或其派生类实现,与由 ScheduleDAGMI 类实现的寄存器分配后调度器配合使用 。 指令选择阶段不使用调度策略

调度 pass 类、调度器类和调度策略类的继承关系及其与后端流程各阶段的对应关系分别如下
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

  • 6
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
LLVM拓展控制流主要是通过引入新的指令来实现的。LLVM提供了多种控制流指令,如条件分支、无条件分支、switch语句等,但是在某些情况下,这些指令可能无法满足程序员的需要。例如,在一些高级语言,存在一些控制流结构,如异常处理、goto语句、try-catch语句等,这些结构无法直接转换为LLVM指令。 为了解决这个问题,LLVM引入了拓展控制流指令。拓展控制流指令可以模拟出高级语言的控制流结构,从而实现对高级语言的支持。例如,LLVM引入了invoke指令来实现函数调用的异常处理,引入了indirectbr指令来实现goto语句,引入了landingpad指令来实现异常处理等。 关于LLVM拓展控制流的编译实验过程,一般可以分为以下几个步骤: 1. 实现拓展控制流指令的前端语言支持。首先需要在前端语言支持相应的控制流结构,例如在C++支持异常处理、goto语句等。 2. 实现拓展控制流指令间表示(IR)支持。接下来需要在LLVM IR引入相应的拓展控制流指令,例如invoke、indirectbr、landingpad等。 3. 实现拓展控制流指令的后端支持。最后需要在LLVM后端实现相应的指令转换和代码生成,以便于将LLVM IR转换为目标代码。 在实际的编译实验,需要根据具体的拓展控制流指令来进行相应的实现,具体的实现细节可以参考LLVM官方文档。同时,也需要进行相应的测试和验证,以确保拓展控制流指令的正确性和可用性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值