从零开始教你写一个LLVM Pass

失业在家闲来无事,不如写些文章回馈下社区。

如果你已经有一定基础,这里是建议您直接看 LLVM的加Pass的 官方文档1 官方文档2。可能限制于自身经验以及专有名词语言的障碍,您可以看下我这篇文章,希望对您有所帮助,如果有不清楚的欢迎留言或查看官方文档。

本文以LLVM Release 15.0.7: Jan 2023 中的 AMDGPUResourceUsageAnalysisPass 为例,对应的commit id是8dfdcc7, 和你一起剖析他是怎么加进去的

一、什么是Pass

LLVM Pass是针对LLVM编译器基础设施的插件式组件,用于执行编译器优化或代码分析。

1、所有的LLVM Pass都是Pass的子类

我们 AMDGPUResourceUsageAnalysis 是这样定义的,llvm/lib/Target/AMDGPU/AMDGPUResourceUsageAnalysis.h:27,其继承于 ModulePass

struct AMDGPUResourceUsageAnalysis : public ModulePass {
  static char ID;

我们可以在 llvm/include/llvm/Pass.h:248 中看到 ModulePass 是基于 Pass 的子类

//===----------------------------------------------------------------------===//
/// ModulePass class - This class is used to implement unstructured
/// interprocedural optimizations and analyses.  ModulePasses may do anything
/// they want to the program.
///
class ModulePass : public Pass {
public:

AMDGPUResourceUsageAnalysis 这个Pass是为了完成栈、寄存器等相关信息的计算,待到指令打印的时候用的。

ModulePass 是用于在整个编译单元(Module)级别执行优化和分析操作,作用于编译单元的中间表示(IR),而不是单个函数或基本块。

2、Pass的目的各种各样

在LLVM中,编译过程可以分为多个阶段,每个阶段都可以通过插件式的方式添加和修改。其可以用于多种目的,包括但不限于:

  • 优化: 这些Pass用于改进生成的机器码的性能和效率,例如执行常量传播、循环优化、内联等。

  • 分析: 这些Pass用于对代码进行静态分析,例如查找未使用的变量、检测内存泄漏、执行指令计数等。

  • 代码转换: 这些Pass用于在中间表示的不同形式之间进行转换,例如将高级语言代码转换为中间表示,或者执行特定的代码重组。

除上文提到的 Pass子类 ModulePass 外,还有 CallGraphSCCPass 在用于在调用图(call graph)上从下至上的遍历程序,SCC是strongly connected(强连通分量,转换DAG用),LoopPass在每个每个循环上执行,RegionPass在函数中的每个单入口单出口region执行,知乎文章参考

官方文档还有 LLVM’s Analysis and Transform Passes 清单

3、绝大多数Pass的输入是IR

IR中间表示是什么呢,就是经过LLVM前端处理后生成的一种抽象的、与具体硬件无关的表示,且可以展示为ISD,经过后端处理后Node可能会降级为目标平台特有的ISD。IR分类和解释详见 官方文档,或者看看这个大佬总结的也挺好。IR中涉及的Module、Function、Basic Block(BB) 如下图所示,我们后端往往拿到的就是其中的一个结构,他们是一个嵌套关系。

二、基础代码

1、build环境的问题

官方文档使用的是opt -load 库文件来跑的,由于我们是改在LLVM后端的,所以仅需在你需要修改的Target增加需要编译的文件即可,比如 AMDGPUResourceUsageAnalysis.cpp 可以在 llvm/lib/Target/AMDGPU/CMakeLists.txt:93 看到

2、写Pass需要一些头文件来引入库

你需要引入 llvm/Pass.h 文件,AMDGPUResourceUsageAnalysis 引入了 CallGraphSCCPass.hCallGraphSCCPass.h 内 引入了 llvm/Pass.h
llvm/lib/Target/AMDGPU/AMDGPUResourceUsageAnalysis.h:18

#ifndef LLVM_LIB_TARGET_AMDGPU_AMDGPURESOURCEUSAGEANALYSIS_H
#define LLVM_LIB_TARGET_AMDGPU_AMDGPURESOURCEUSAGEANALYSIS_H

#include "llvm/Analysis/CallGraphSCCPass.h"
#include "llvm/CodeGen/MachineModuleInfo.h"

namespace llvm {

3、定义这个Pass

选好你需要继承的Pass类,我们这个Pass要知道全局的信息,所以使用的ModulePass

这个Pass需要一个ID,并而且是static的,即只有一份
构造函数也是需要的,AMDGPUResourceUsageAnalysis() : ModulePass(ID) {}

还有runOnModule函数需要重写,bool runOnModule(Module &M) override; ,这个往往具体实现在 .cpp 中

这里还重写了 doInitialization 去 做初始化

其他就是你需要定义的成员以及你抽象的接口了

//===- AMDGPUResourceUsageAnalysis.h ---- analysis of resources -*- C++ -*-===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
//
/// \file
/// \brief Analyzes how many registers and other resources are used by
/// functions.
///
//===----------------------------------------------------------------------===//

#ifndef LLVM_LIB_TARGET_AMDGPU_AMDGPURESOURCEUSAGEANALYSIS_H
#define LLVM_LIB_TARGET_AMDGPU_AMDGPURESOURCEUSAGEANALYSIS_H

#include "llvm/Analysis/CallGraphSCCPass.h"
#include "llvm/CodeGen/MachineModuleInfo.h"

namespace llvm {

class GCNSubtarget;
class MachineFunction;
class TargetMachine;

struct AMDGPUResourceUsageAnalysis : public ModulePass {
  static char ID;

public:
  // Track resource usage for callee functions.
  struct SIFunctionResourceInfo {
    // Track the number of explicitly used VGPRs. Special registers reserved at
    // the end are tracked separately.
    int32_t NumVGPR = 0;
    int32_t NumAGPR = 0;
    int32_t NumExplicitSGPR = 0;
    uint64_t PrivateSegmentSize = 0;
    bool UsesVCC = false;
    bool UsesFlatScratch = false;
    bool HasDynamicallySizedStack = false;
    bool HasRecursion = false;
    bool HasIndirectCall = false;

    int32_t getTotalNumSGPRs(const GCNSubtarget &ST) const;
    // Total number of VGPRs is actually a combination of AGPR and VGPR
    // depending on architecture - and some alignment constraints
    int32_t getTotalNumVGPRs(const GCNSubtarget &ST, int32_t NumAGPR,
                             int32_t NumVGPR) const;
    int32_t getTotalNumVGPRs(const GCNSubtarget &ST) const;
  };

  AMDGPUResourceUsageAnalysis() : ModulePass(ID) {}

  bool doInitialization(Module &M) override {
    CallGraphResourceInfo.clear();
    return ModulePass::doInitialization(M);
  }

  bool runOnModule(Module &M) override;

  void getAnalysisUsage(AnalysisUsage &AU) const override {
    AU.addRequired<MachineModuleInfoWrapperPass>();
    AU.setPreservesAll();
  }

  const SIFunctionResourceInfo &getResourceInfo(const Function *F) const {
    auto Info = CallGraphResourceInfo.find(F);
    assert(Info != CallGraphResourceInfo.end() &&
           "Failed to find resource info for function");
    return Info->getSecond();
  }

private:
  SIFunctionResourceInfo analyzeResourceUsage(const MachineFunction &MF,
                                              const TargetMachine &TM) const;
  void propagateIndirectCallRegisterUsage();

  DenseMap<const Function *, SIFunctionResourceInfo> CallGraphResourceInfo;
};
} // namespace llvm
#endif // LLVM_LIB_TARGET_AMDGPU_AMDGPURESOURCEUSAGEANALYSIS_H

4、runOnModulerunOnFunction 实现

后端的所有要做的事情其实就是一个个的Pass,由 PassManager 进行管理,可能是各种类型的Pass,ModulePass是通过 llvm/lib/IR/LegacyPassManager.cpp:1545 LocalChanged |= MP->runOnModule(M); 来运行的,runOnModule 就是这个 Pass 的入口;FunctionPass 是通过 llvm/lib/IR/LegacyPassManager.cpp:1430 LocalChanged |= FP->runOnFunction(F); 来运行的。

llvm/lib/Target/AMDGPU/AMDGPUResourceUsageAnalysis.cpp:100 代码中 可以看到了其拿到了这个Module 的 CallGraph,然后进行后序遍历,也就是左右根,然后进行了analyzeResourceUsage,这个函数里我们可以看到其会判断子call然后求大,即 CalleeFrameSize = std::max(I->second.PrivateSegmentSize, CalleeFrameSize); ,当前函数所用栈 = callee中的最大栈 + 当前函数的PrivateSegmentSize,而且你是后序遍历这个图的,也就是把子调用求完再求父的,不需要递归

bool AMDGPUResourceUsageAnalysis::runOnModule(Module &M) {
  auto *TPC = getAnalysisIfAvailable<TargetPassConfig>();
  if (!TPC)
    return false;

  MachineModuleInfo &MMI = getAnalysis<MachineModuleInfoWrapperPass>().getMMI();
  const TargetMachine &TM = TPC->getTM<TargetMachine>();
  bool HasIndirectCall = false;

  CallGraph CG = CallGraph(M);
  auto End = po_end(&CG);

  for (auto IT = po_begin(&CG); IT != End; ++IT) {
    Function *F = IT->getFunction();
    if (!F || F->isDeclaration())
      continue;

    MachineFunction *MF = MMI.getMachineFunction(*F);
    assert(MF && "function must have been generated already");

    auto CI = CallGraphResourceInfo.insert(
        std::make_pair(F, SIFunctionResourceInfo()));
    SIFunctionResourceInfo &Info = CI.first->second;
    assert(CI.second && "should only be called once per function");
    Info = analyzeResourceUsage(*MF, TM);
    HasIndirectCall |= Info.HasIndirectCall;
  }

  if (HasIndirectCall)
    propagateIndirectCallRegisterUsage();

  return false;
}

三、注入到后端中

1、初始化Pass

在 llvm/lib/Target/AMDGPU/AMDGPU.h:262 声明

void initializeAMDGPUResourceUsageAnalysisPass(PassRegistry &);
extern char &AMDGPUResourceUsageAnalysisID;

在 llvm/lib/Target/AMDGPU/AMDGPUTargetMachine.cpp:401 LLVMInitializeAMDGPUTarget 函数中调用

  initializeAMDGPUResourceUsageAnalysisPass(*PR);
  initializeGCNNSAReassignPass(*PR);
  initializeGCNPreRAOptimizationsPass(*PR);
}

2、Pass 执行和信息使用

这个Pass是在指令打印时使用的,其通过 &getAnalysis() 取到了这个Pass的信息

bool AMDGPUAsmPrinter::runOnMachineFunction(MachineFunction &MF) {
  // Init target streamer lazily on the first function so that previous passes
  // can set metadata.
  if (!IsTargetStreamerInitialized)
    initTargetStreamer(*MF.getFunction().getParent());

  ResourceUsage = &getAnalysis<AMDGPUResourceUsageAnalysis>();
  CurrentProgramInfo = SIProgramInfo();

需要注意的是其重写了 getAnalysisUsage 确保能拿到数据并执行正确

void AMDGPUAsmPrinter::getAnalysisUsage(AnalysisUsage &AU) const {
  AU.addRequired<AMDGPUResourceUsageAnalysis>();
  AU.addPreserved<AMDGPUResourceUsageAnalysis>();
  AsmPrinter::getAnalysisUsage(AU);
}

其他情况呢,比如 AMDGPUAlwaysInlinePass 总是强制内联Pass 他是在 void AMDGPUPassConfig::addIRPasses() { 加入的,addIRPasses 表示他的阶段,当然这也是 重写的函数

还可以加在不同的阶段,当然你想知道当前Target用了哪些Pass 查看 TargetPassConfig 和 XXPassConfig 即可

class AMDGPUPassConfig : public TargetPassConfig {
public:
  AMDGPUPassConfig(LLVMTargetMachine &TM, PassManagerBase &PM);

  AMDGPUTargetMachine &getAMDGPUTargetMachine() const {
    return getTM<AMDGPUTargetMachine>();
  }

  ScheduleDAGInstrs *
  createMachineScheduler(MachineSchedContext *C) const override;

  void addEarlyCSEOrGVNPass();
  void addStraightLineScalarOptimizationPasses();
  void addIRPasses() override;
  void addCodeGenPrepare() override;
  bool addPreISel() override;
  bool addInstSelector() override;
  bool addGCPasses() override;

另外可以根据用户不同的编译选项添加不同的Pass

  if (getOptLevel() == CodeGenOpt::Aggressive)
    addPass(createGVNPass());

llvm/lib/Transforms 文件夹下更是Pass集中地,标量优化在Scalar目录,过程间优化在 IPO 目录。比如 死代码消除 ADCELegacyPass在 llvm/lib/Passes/PassBuilderPipelines.cpp:561 我们可以看到其是默认启用的。

posted @ 2023-08-18 14:41  暴力都不会的蒟蒻  阅读(2204)  评论(2编辑  收藏  举报