Writing an LLVM Pass

1.简介 - 什么是pass?
LLVMpass框架是LLVM系统的重要组成部分,因为LLVMpasses是编译器的大多数有趣部分存在的地方。通过执行构成编译器的转换和优化,它们构建这些转换所使用的分析结果,并且它们首先是编译器代码的结构化技术。

所有LLVM传递都是Pass类的子类,它通过重写从Pass继承的虚方法来实现功能。根据传递的工作方式,您应该继承ModulePass,CallGraphSCCPass,FunctionPass或LoopPass,或RegionPass或BasicBlockPass类,这样可以为系统提供有关传递操作的更多信息,以及如何将其与其他传递结合使用。 LLVM Pass Framework的一个主要特性是它根据传递遇到的约束(由它们派生自哪个类来指示)来调度传递以高效的方式运行。

我们首先向您展示如何构建传递,从设置代码到编译,加载和执行它。基础知识出现故障后,将讨论更多高级功能。

2.快速入门 - 写下hello world
在这里,我们描述如何编写“hello world”的pass。 “Hello”pass旨在简单地打印出正在编译的程序中存在的非外部函数的名称。 它根本不修改程序,它只是检查它。 此传递的源代码和文件位于lib/Transforms/Hello目录中的LLVM源代码树中。

3.设置构建环境
首先,配置和构建LLVM。 接下来,您需要在LLVM源代码库中的某个位置创建新目录。 对于这个例子,我们假设您创建了lib/ Transforms/Hello。 最后,您必须设置一个构建脚本,该脚本将编译新传递的源代码。 为此,请将以下内容复制到CMakeLists.txt中:

add_llvm_library( LLVMHello MODULE
  Hello.cpp

  PLUGIN_TOOL
  opt
  )

并将以下行放入lib / Transforms / CMakeLists.txt:

add_subdirectory(Hello)

(请注意,已经有一个名为Hello的目录带有示例“Hello”传递;您可以使用它 - 在这种情况下您不需要修改任何CMakeLists.txt文件 - 或者,如果您想从头开始创建所有内容 ,使用另一个名字。)

此构建脚本指定要编译当前目录中的Hello.cpp文件并将其链接到共享对象$(LEVEL)/lib/LLVMHello.so,该工具可由opt工具通过其-load选项动态加载。 如果您的操作系统使用.so之外的后缀(例如Windows或Mac OS X),则将使用相应的扩展名。

现在我们已经设置了构建脚本,我们只需要为pass本身编写代码。

4.Basic code required

现在我们有了编译新pass的方法,我们只需要编写它。 从以下开始:

#include "llvm/Pass.h"
#include "llvm/IR/Function.h"
#include "llvm/Support/raw_ostream.h"

这是必需的,因为我们正在编写Pass,我们正在使用函数,我们将进行一些打印。

接下来我们有:

namespace {

...开始一个匿名命名空间。 匿名命名空间是C ++的“静态”关键字是C(在全局范围内)。 它使在匿名命名空间内声明的内容仅对当前文件可见。 如果您不熟悉它们,请参阅一本体面的C ++书籍以获取更多信息。

接下来,我们声明我们的pass本身:

struct Hello : public FunctionPass {

这声明了一个“Hello”类,它是FunctionPass的子类。 稍后将详细描述不同的内置pass子类,但是现在知道FunctionPass一次对一个函数进行操作。

static char ID;
Hello() : FunctionPass(ID) {}

这声明了LLVM用于标识pass的pass标识符。 这允许LLVM避免使用昂贵的C ++运行时信息。

bool runOnFunction(Function &F) override {
    errs() << "Hello: ";
    errs().write_escaped(F.getName()) << '\n';
    return false;
  }
}; // end of struct Hello
}  // end of anonymous namespace

我们声明了一个runOnFunction方法,它覆盖了从FunctionPass继承的抽象虚方法。 这是我们应该做的事情,所以我们只用每个函数的名称打印出我们的消息。

char Hello::ID = 0;

我们在这里初始化passID。 LLVM使用ID的地址来标识pass,因此初始化值并不重要。

最后,我们注册我们的类Hello,给它一个命令行参数“hello”,并命名为“Hello World Pass”。 最后两个参数描述了它的行为:如果传递遍历CFG而不修改它,那么第三个参数设置为true; 如果pass是分析pass,例如支配树pass,则提供true作为第四个参数。

整体而言,.cpp文件如下所示:

#include "llvm/Pass.h"
#include "llvm/IR/Function.h"
#include "llvm/Support/raw_ostream.h"

using namespace llvm;

namespace {
struct Hello : public FunctionPass {
  static char ID;
  Hello() : FunctionPass(ID) {}

  bool runOnFunction(Function &F) override {
    errs() << "Hello: ";
    errs().write_escaped(F.getName()) << '\n';
    return false;
  }
}; // end of struct Hello
}  // end of anonymous namespace

char Hello::ID = 0;
static RegisterPass<Hello> X("hello", "Hello World Pass",
                             false /* Only looks at CFG */,
                             false /* Analysis Pass */);

现在您有了一个全新的闪亮共享对象文件,我们可以使用opt命令通过您的传递来运行LLVM程序。 因为您使用RegisterPass注册了您的pass,所以一旦加载,您就可以使用opt工具来访问它。

要测试它,请按照LLVM系统入门末尾的示例将“Hello World”编译为LLVM。 我们现在可以通过这样的转换运行程序的bitcode文件(hello.bc)(或者当然,任何bitcode文件都可以):

$ opt -load lib/LLVMHello.so -hello < hello.bc > /dev/null
Hello: __main
Hello: puts
Hello: main

现在您有了一个全新的闪亮共享对象文件,我们可以使用opt命令通过您的pass来运行LLVM程序。 因为您使用RegisterPass注册了您的pass,所以一旦加载,您就可以使用opt工具来访问它。

要测试它,请按照LLVM系统入门末尾的示例将“Hello World”编译为LLVM。 我们现在可以通过这样的转换运行程序的bitcode文件(hello.bc)(或者当然,任何bitcode文件都可以):

$ opt -load lib/LLVMHello.so -hello < hello.bc > /dev/null
Hello: __main
Hello: puts
Hello: main

-load选项指定opt应该将您的pass加载为共享对象,这使得“-hello”成为有效的命令行参数(这是您需要注册传递的一个原因)。 因为Hello pass不会以任何有趣的方式修改程序,所以我们只丢弃opt的结果(将其发送到/ dev / null)。

要查看您注册的其他字符串发生了什么,请尝试使用-help选项运行opt:

$ opt -load lib/LLVMHello.so -help
OVERVIEW: llvm .bc -> .bc modular optimizer and analysis printer

USAGE: opt [subcommand] [options] <input bitcode file>

OPTIONS:
  Optimizations available:
...
    -guard-widening           - Widen guards
    -gvn                      - Global Value Numbering
    -gvn-hoist                - Early GVN Hoisting of Expressions
    -hello                    - Hello World Pass
    -indvars                  - Induction Variable Simplification
    -inferattrs               - Infer set function attributes
...

pass名称将作为传递的信息字符串添加,为opt的用户提供一些文档。 既然你有一个工作pass,你就可以继续前进,让它做你想要的酷transformations。 一旦你完成所有的工作和测试,找出你的pass可能会很有用。 PassManager提供了一个很好的命令行选项(--time-passes),允许您获取有关传递执行时间的信息以及您排队的其他传递。 例如:

$ opt -load lib/LLVMHello.so -hello -time-passes < hello.bc > /dev/null
Hello: __main
Hello: puts
Hello: main
===-------------------------------------------------------------------------===
                      ... Pass execution timing report ...
===-------------------------------------------------------------------------===
  Total Execution Time: 0.0007 seconds (0.0005 wall clock)

   ---User Time---   --User+System--   ---Wall Time---  --- Name ---
   0.0004 ( 55.3%)   0.0004 ( 55.3%)   0.0004 ( 75.7%)  Bitcode Writer
   0.0003 ( 44.7%)   0.0003 ( 44.7%)   0.0001 ( 13.6%)  Hello World Pass
   0.0000 (  0.0%)   0.0000 (  0.0%)   0.0001 ( 10.7%)  Module Verifier
   0.0007 (100.0%)   0.0007 (100.0%)   0.0005 (100.0%)  Total

如您所见,我们上面的实现非常快。 opt工具会自动插入列出的其他pass,以验证pass发出的LLVM是否仍然有效并且格式良好的LLVM(尚未以某种方式被破坏)。

现在你已经看到了pass背后的机制的基础知识,我们可以讨论它们如何工作以及如何使用它们的更多细节。

5.Pass类和要求

在设计新pass时,您设计一个新的pass时应该做的第一件事就是确定你的pass需要子类化哪个pass。 Hello World示例使用FunctionPass类进行实现,但我们没有讨论为什么或何时发生这种情况。 在这里,我们讨论可用的类,从最一般到最具体。

为Pass选择超类时,您应该选择最具体的类,同时仍然能够满足列出的要求。 这为LLVM Pass 基础设施提供了优化pass运行所需的信息,因此生成的编译器不会非常慢。

6.ImmutablePass类

最简单和无聊的pass类型是“ImmutablePass”类。此pass类型用于不必运行的传递,不更改状态,永远不需要更新。这不是正常类型的转换或分析,但可以提供有关当前编译器配置的信息。

尽管这种传递类很少使用,但提供有关正在编译的当前目标机器的信息以及可能影响各种变换的其他静态信息非常重要。

ImmutablePasses永远不会使其他转换无效,永远不会失效,永远不会“运行”。

7.ModulePass类
ModulePass类是您可以使用的所有超类中最常用的类。从ModulePass派生表示您的pass使用整个程序作为一个单元,以无法预测的顺序引用函数体,或添加和删除函数。因为对ModulePass子类的行为一无所知,所以不能对它们的执行进行优化。

如果函数传递不需要任何模块或不可变传递,则模块传递可以使用getAnalysis接口getAnalysis <DominatorTree>(llvm :: Function *)来提供函数来检索分析结果。注意,这只能对分析运行的函数进行,例如,在支配者的情况下,你应该只要求DominatorTree进行函数定义,而不是声明。

要编写正确的ModulePass子类,请从ModulePass派生并使用以下签名重载runOnModule方法:

  1. runOnModule方法
    virtual bool runOnModule(Module&M)= 0;
    
    runOnModule方法执行pass的有趣工作。 如果模块被转换修改,它应返回true,否则返回false。

8.CallGraphSCCPass类

CallGraphSCCPass用于需要在调用图上自上而下遍历程序的传递(在调用者之前调用callees)。 从CallGraphSCCPass派生提供了一些构建和遍历CallGraph的机制,但也允许系统优化CallGraphSCCPasses的执行。 如果您的pass符合下面列出的要求,并且不符合FunctionPass或BasicBlockPass的要求,则应从CallGraphSCCPass派生。

TODO:简要解释一下SCC(强连通分量),Tarjan的算法和B-U的含义。

为了明确,CallGraphSCCPass子类是:

  1. ......不允许检查或修改除当前SCC以及SCC的直接呼叫者和直接被叫者之外的任何Functions。
  2. ...需要保留当前的CallGraph对象,更新它以反映对程序所做的任何更改。
  3. ...不允许在当前模块中添加或删除SCC,但它们可能会更改SCC的内容。
  4. ...允许在当前模块中添加或删除全局变量。
  5. ...允许在runOnSCC(包括全局数据)的调用中维护状态。

在某些情况下,实现CallGraphSCCPass有点棘手,因为它必须处理具有多个节点的SCC。 如果修改程序,下面描述的所有虚拟方法都应该返回true,否则应该返回false。

  • The doInitialization(CallGraph &) method
virtual bool doInitialization(CallGraph &CG);

doInitialization方法允许执行CallGraphSCCPasses不允许执行的大部分操作。 它们可以添加和删除函数,获取函数指针等.doInitialization方法旨在执行不依赖于正在处理的SCC的简单初始化类型的东西。 doInitialization方法调用未安排与任何其他pass执行重叠(因此它应该非常快)。

  • The runOnSCC method
virtual bool runOnSCC(CallGraphSCC &SCC) = 0;

 runOnSCC方法执行pass的有趣工作,如果模块被转换修改则返回true,否则返回false。

  • The doFinalization(CallGraph &) method

doFinalization方法是一种不经常使用的方法,当pass框架为正在编译的程序中的每个SCC调用runOnSCC时调用该方法。

9.FunctionPass类

与ModulePass子类相比,FunctionPass子类确实具有系统可以预期的可预测的本地行为。 所有FunctionPass都独立于程序中的所有其他功能执行程序中的每个功能。 FunctionPasses不要求它们按特定顺序执行,FunctionPasses不要修改外部函数。

显而易见,不允许FunctionPass子类:

  • 检查或修改当前正在处理的功能以外的功能。
  • 在当前模块中添加或删除功能。
  • 在当前模块中添加或删除全局变量。
  • 跨runOnFunction调用(包括全局数据)维护状态。

实现FunctionPass通常很简单(例如,请参阅Hello World传递)。 FunctionPasses可能会使三个虚拟方法超载以完成其工作。 如果修改程序,所有这些方法都应返回true,否则返回false。

10.doInitialization(Module&)方法

virtual bool doInitialization(Module&M);

doInitialization方法允许执行不允许FunctionPasses执行的大部分操作。 它们可以添加和删除函数,获取函数指针等.doInitialization方法旨在执行不依赖于正在处理的函数的简单初始化类型的东西。 doInitialization方法调用未安排与任何其他pass执行重叠(因此它应该非常快)。

如何使用此方法的一个很好的例子是LowerAllocations pass。 此过程将malloc和free指令转换为依赖于平台的malloc()和free()函数调用。 它使用doInitialization方法获取对其所需的malloc和free函数的引用,如有必要,可将原型添加到模块中。

  • runOnFunction方法
virtual bool runOnFunction(Function&F)= 0;

runOnFunction方法必须由您的子类实现,以执行pass转换或分析工作。 像往常一样,如果修改了函数,则应返回true值。

  • doFinalization(Module&)方法
virtual bool doFinalization(Module&M);

doFinalization方法是一种不经常使用的方法,当pass框架为正在编译的程序中的每个函数调用runOnFunction时调用该方法。

11.LoopPass类

所有LoopPass都在函数中的每个循环上执行,与函数中的所有其他循环无关。 LoopPass以循环嵌套顺序处理循环,以便最后处理最外层循环。

允许LoopPass子类使用LPPassManager接口更新循环嵌套。 实现循环传递通常很简单。 LoopPasses可能会使三个虚拟方法超载以完成其工作。 如果修改程序,所有这些方法都应返回true,否则返回false。

旨在作为主循环pass管道的一部分运行的LoopPass子类需要保留另一个循环在其管道要求中传递的所有相同的函数分析。 为了简化这一过程,LoopUtils.h提供了一个getLoopAnalysisUsage函数。 它可以在子类的getAnalysisUsage覆盖中调用,以获得一致和正确的行为。 类似地,INITIALIZE_PASS_DEPENDENCY(LoopPass)将初始化这组功能分析。

  • The doInitialization(Loop *, LPPassManager &) method
virtual bool doInitialization(Loop *, LPPassManager &LPM);

doInitialization方法旨在执行不依赖于正在处理的函数的简单初始化类型的东西。 doInitialization方法调用未安排与任何其他pass执行重叠(因此它应该非常快)。 LPPassManager接口应用于访问功能或模块级别的分析信息。

  • The runOnLoop method
virtual bool runOnLoop(Loop *,LPPassManager&LPM)= 0;

runOnLoop方法必须由子类实现,以执行pass的转换或分析工作。像往常一样,如果修改了函数,则应返回true值。 LPPassManager接口应该用于更新循环嵌套。

  • The doFinalization() method
virtual bool doFinalization();

doFinalization方法是一种不常用的方法,当pass框架为正在编译的程序中的每个循环调用runOnLoop时调用该方法。

12.RegionPass类
RegionPass类似于LoopPass,但在函数中的每个单个条目单个退出区域上执行。 RegionPass以嵌套顺序处理区域,以便最后处理最外层区域。

允许RegionPass子类使用RGPassManager接口更新区域树。 您可以重载RegionPass的三个虚拟方法来实现您自己的区域pass。 如果修改程序,所有这些方法都应返回true,否则返回false。

  • The doInitialization(Region *, RGPassManager &) method
virtual bool doInitialization(Region *, RGPassManager &RGM);

doInitialization方法旨在执行不依赖于正在处理的函数的简单初始化类型的东西。 doInitialization方法调用未安排与任何其他pass执行重叠(因此它应该非常快)。 RPPassManager接口应用于访问功能或模块级分析信息。

  • The runOnRegion method
virtual bool runOnRegion(Region *,RGPassManager&RGM)= 0;

runOnRegion方法必须由您的子类实现,以执行pass的转换或分析工作。像往常一样,如果修改了区域,则应返回真值。应使用RGPassManager接口更新区域树。

  • The doFinalization() method
virtual bool doFinalization();

doFinalization方法是一种不经常使用的方法,当pass框架为正在编译的程序中的每个区域调用runOnRegion时调用该方法。

13.The BasicBlockPass class

BasicBlockPasses就像FunctionPass一样,除了它们必须一次将它们的检查和修改范围限制为单个基本块。 因此,他们不允许执行以下任何操作:

  • 修改或检查当前基本块之外的任何基本块。
  • 在runOnBasicBlock的调用中维护状态。
  • 修改控制流图(通过更改终止符指令)
  • 任何禁止使用FunctionPasses的东西。

BasicBlockPasses对传统的本地和“peephole”优化很有用。 它们可能会覆盖FunctionPass所具有的相同的doInitialization(Module&)和doFinalization(Module&)方法,但也可以实现以下虚拟方法:

  • The doInitialization(Function &) method
virtual bool doInitialization(Function &F);

doInitialization方法允许执行BasicBlockPasses不允许执行的大部分操作,但FunctionPasses可以执行。 doInitialization方法旨在执行简单的初始化,该初始化不依赖于正在处理的BasicBlock。 doInitialization方法调用未安排与任何其他pass执行重叠(因此它应该非常快)。

  • runOnBasicBlock方法
virtual bool runOnBasicBlock(BasicBlock&BB)= 0;

重写此函数以执行BasicBlockPass的工作。 此功能不允许检查或修改参数以外的基本块,也不允许修改CFG。 如果修改了基本块,则必须返回true值。

  • doFinalization(Function&)方法
virtual bool doFinalization(Function &F);

doFinalization方法是一种不常用的方法,当pass框架为正在编译的程序中的每个BasicBlock调用runOnBasicBlock时调用该方法。 这可用于执行按功能完成。

14.The MachineFunctionPass class

MachineFunctionPass是LLVM代码生成器的一部分,它在程序中每个LLVM函数的机器相关表示上执行。

代码生成器pass由TargetMachine :: addPassesToEmitFile和类似的例程特别注册和初始化,因此通常不能从opt或bugpoint命令运行它们。

MachineFunctionPass也是一个FunctionPass,因此适用于FunctionPass的所有限制也适用于它。 MachineFunctionPasses还有其他限制。 特别是,不允许MachineFunctionPasses执行以下任何操作:

  1. 修改或创建任何LLVM IR指令,BasicBlock,参数,函数,GlobalVariables,GlobalAliases或 Modules.。
  2. 修改当前正在处理的MachineFunction以外的MachineFunction。
  3. 在runOnMachineFunction(包括全局数据)的调用中维护状态。
  • The runOnMachineFunction(MachineFunction &MF) method
virtual bool runOnMachineFunction(MachineFunction &MF) = 0;

runOnMachineFunction可以被认为是MachineFunctionPass的主要入口点; 也就是说,您应该覆盖此方法以执行MachineFunctionPass的工作。

在Module中的每个MachineFunction上调用runOnMachineFunction方法,以便MachineFunctionPass可以对函数的机器相关表示执行优化。 如果您想获得正在使用的MachineFunction的LLVM函数,请使用MachineFunction的getFunction()访问器方法 - 但请记住,您不能从MachineFunctionPass修改LLVM函数或其内容。

15.pass注册

在Hello World的示例pass中,我们说明了pass注册是如何工作的,并讨论了使用它的一些原因以及它的作用。这里我们讨论pass的注册方式和注册原因。

正如我们上面看到的,pass是用registerpass模板注册的。template参数是将在命令行上使用的过程的名称,用于指定应将该过程添加到程序(例如,使用opt或bugpoint)。第一个参数是pass的名称,它将用于程序的-help输出,以及-debug pass选项生成的调试输出。              

如果您希望您的pass易于转储,则应实现虚拟打印方法: 

  • The print method
virtual void print(llvm :: raw_ostream&O,const Module * M)const;

打印方法必须通过“分析”来实现,以便打印分析人类可读版本的结果。 这对于调试分析本身以及其他人来确定分析的工作方式非常有用。 使用opt -analyze参数来调用此方法。

llvm::raw_ostream参数指定要在其上写入结果的流,Module参数提供指向已分析程序的顶级模块的指针。 但请注意,在某些情况下(例如从调试器调用Pass :: dump()),此指针可能为NULL,因此它只应用于增强调试输出,不应该依赖它。

16.Specifying interactions between passes

PassManager的主要职责之一是确保pass正确地相互交互。 因为PassManager试图优化pass的执行,所以它必须知道pass如何相互交互以及各个传递之间存在哪些依赖关系。 为了跟踪这一点,每次pass都可以声明在当前pass之前需要执行的pass集,以及当前pass失效的传递。

通常,此功能用于要求在运行pass之前计算分析结果。 运行任意pass过程可以使计算的分析结果无效,这是失效集指定的。 如果pass未实现getAnalysisUsage方法,则默认为没有任何先决条件pass,并使所有其他pass无效。

  • The getAnalysisUsage method
virtual void getAnalysisUsage(AnalysisUsage &Info) const;

通过实现getAnalysisUsage方法,可以为转换指定必需和无效的集合。实现应该在AnalysisUsage对象中填入有关哪些pass是必需的而不是无效的信息。要执行此操作,pass可以在AnalysisUsage对象上调用以下任何方法:

  • The AnalysisUsage::addRequired<> and AnalysisUsage::addRequiredTransitive<> methods

如果您的pass需要执行上一个pass(例如分析),它可以使用这些方法之一来安排在pass之前运行它。 LLVM具有许多不同类型的分析和pass,可以满足从DominatorSet到BreakCriticalEdges的范围。例如,要求BreakCriticalEdges保证在运行pass时CFG中没有关键边缘。

  • The AnalysisUsage::addPreserved<> method

PassManager的一项工作是优化分析的运行方式和时间。特别是,它试图避免重新计算数据,除非它需要。出于这个原因,允许pass声明它们保留(即,它们不会使现有分析无效),如果它可用的话。例如,简单的常量折叠pass不会修改CFG,因此它不可能影响支配者分析的结果。默认情况下,假定所有pass使所有其他pass无效。

AnalysisUsage类提供了几种在与addPreserved相关的特定情况下有用的方法。特别地,可以被称为setPreservesAll方法来指示pass不修改在所有LLVM程序(其用于分析为真),并且setPreservesCFG方法可通过改变程序中的指令,但不修改的转化可以使用CFG或终止符指令(请注意,此属性是为BasicBlockPasses隐式设置的)。

addPreserved对于像BreakCriticalEdges这样的转换特别有用。这个pass知道如何更新一小组循环和支配者相关的分析(如果它们存在),所以它可以保留它们,尽管它会破坏CFG。getAnalysisUsage的示例实现
 

//此示例修改程序,但不修改CFG
void LICM::getAnalysisUsage(AnalysisUsage &AU) const {
  AU.setPreservesCFG();
  AU.addRequired<LoopInfoWrapperPass>();
}
  • The getAnalysis<> and getAnalysisIfAvailable<> methods

Pass :: getAnalysis <>方法由您的类自动继承,使您可以访问使用getAnalysisUsage方法声明的声明。 它需要一个模板参数来指定所需的传递类,并返回对该传递的引用。 例如:

bool LICM::runOnFunction(Function &F) {
  LoopInfo &LI = getAnalysis<LoopInfoWrapperPass>().getLoopInfo();
  //...
}

此方法调用返回对所需pass的引用。 如果尝试获取未在getAnalysisUsage实现中声明的分析,则可能会导致运行时断言失败。 此方法可以由run *方法实现调用,也可以由run *方法调用的任何其他本地方法调用。

模块级别pass可以使用此接口使用功能级别分析信息。 例如:

bool ModuleLevelPass::runOnModule(Module &M) {
  //...
  DominatorTree &DT = getAnalysis<DominatorTree>(Func);
  //...
}

在上面的示例中,在返回对所需pass的引用之前,传递管理器调用runOnFunction for DominatorTree。

如果您的pass能够更新分析(如上所述),则可以使用getAnalysisIfAvailable方法,该方法返回指向分析的指针(如果它处于活动状态)。 例如:

if (DominatorSet *DS = getAnalysisIfAvailable<DominatorSet>()) {
  // A DominatorSet is active.  This code will update it.
}

17.Implementing Analysis Groups

现在我们已经了解了如何定义pass,如何使用它们以及如何从其他pass中获得pass的基础知识,现在是时候让它变得更有趣了。到目前为止,我们看到的所有pass关系都非常简单:一次pass依赖于另一个特定pass,在它可以运行之前运行。对于许多应用来说,这很好,对于其他应用,需要更大的灵活性。

特别是,定义了一些分析,使得分析结果只有一个简单的界面,但有多种计算方法。例如,考虑别名分析。最琐碎的别名分析为任何别名查询返回“may alias”。最复杂的分析是流量敏感的,上下文敏感的过程间分析,可能需要花费大量的时间来执行(显然,这两个极端之间存在很大的空间用于其他实现)。为了干净地支持这种情况,LLVM Pass Infrastructure支持分析组的概念。

18.Analysis Group Concepts

分析组是一个简单的界面,可以通过多个不同的pass来实现。分析组可以像传球一样给出人类可读的名称,但与pass不同,它们不需要从Pass类派生。分析组可以具有一个或多个实现,其中之一是“默认”实现。

客户端pass使用分析组,就像其他pass一样:AnalysisUsage :: addRequired()和Pass :: getAnalysis()方法。为了解决此要求,PassManager会扫描可用的pass以查看是否有任何分析组的实现可用。如果没有,则为要使用的pass创建默认实现。pass之间相互作用的所有标准规则仍然适用。

虽然Pass Registration对于正常传递是可选的,但是必须注册所有分析组实现,并且必须使用INITIALIZE_AG_PASS模板来加入实现池。此外,必须使用RegisterAnalysisGroup注册接口的默认实现。

作为分析组的具体实例,请考虑AliasAnalysis分析组。别名分析界面(basicaa pass)的默认实现只做了一些简单的检查,不需要进行大量的计算分析(例如:两个不同的全局变量永远不能互为别名等)。使用AliasAnalysis接口的传递(例如gvn传递)不关心实际提供别名分析的实现,它们只使用指定的接口。

从用户的角度来看,命令就像正常一样工作。发出命令opt -gvn ...将导致basicaa类被实例化并添加到pass序列中。发出命令opt -somefancyaa -gvn ...将导致gvn传递使用somefancyaa别名分析(实际上并不存在,它只是一个假设的例子)。

19.Using RegisterAnalysisGroup
RegisterAnalysisGroup模板用于注册分析组本身,而INITIALIZE_AG_PASS用于将pass实现添加到分析组。 首先,应该注册一个分析组,并为其提供一个人类可读的名称。 与pass注册不同,没有为分析组接口本身指定命令行参数,因为它是“抽象的”:

static RegisterAnalysisGroup<AliasAnalysis> A("Alias Analysis");

注册分析后,pass可以使用以下代码声明它们是接口的有效实现:

namespace {
  // Declare that we implement the AliasAnalysis interface
  INITIALIZE_AG_PASS(FancyAA, AliasAnalysis , "somefancyaa",
      "A more complex alias analysis implementation",
      false,  // Is CFG Only?
      true,   // Is Analysis?
      false); // Is default Analysis Group implementation?
}

这只显示了一个类FancyAA,它使用INITIALIZE_AG_PASS宏来注册和“加入”AliasAnalysis分析组。 分析组的每个实现都应该使用此宏加入。

namespace {
  // Declare that we implement the AliasAnalysis interface
  INITIALIZE_AG_PASS(BasicAA, AliasAnalysis, "basicaa",
      "Basic Alias Analysis (default AA impl)",
      false, // Is CFG Only?
      true,  // Is Analysis?
      true); // Is default Analysis Group implementation?
}

这里我们展示如何指定默认实现(使用INITIALIZE_AG_PASS模板的最后一个参数)。 对于要使用的分析组,必须始终只有一个默认实现可用。 只有默认实现可以从ImmutablePass派生。 这里我们声明BasicAliasAnalysis传递是接口的默认实现。

20.Pass Statistics

Statistic类旨在通过pass方式公开各种成功指标。 在命令行上启用-stats命令行选项时,将在运行结束时打印这些统计信息。 有关详细信息,请参阅“程序员手册”中的“统计”部分。

What PassManager does
PassManager类获取pass列表,确保正确设置其先决条件,然后调度pass以高效运行。 所有运行pass的LLVM工具都使用PassManager来执行这些pass。

PassManager做了两件主要的事情来尝试减少一系列pass的执行时间:

  1. 分享分析结果。 PassManager尝试尽可能避免重新计算分析结果。 这意味着要跟踪哪些分析已经可用,哪些分析失效,以及需要为通过运行哪些分析。 工作的一个重要部分是PassManager跟踪所有分析结果的确切生命周期,允许它在不再需要时释放分配给保存分析结果的内存。
  2. 管道程序执行pass。 PassManager尝试通过将通道流水线化,从而在一系列通道中获得更好的缓存和内存使用行为。 这意味着,给定一系列连续的FunctionPass,它将执行第一个函数上的所有FunctionPass,然后执行第二个函数上的所有FunctionPasses等等,直到整个程序运行完遍。这改进了编译器的缓存行为,因为它一次只触及单个函数的LLVM程序表示,而不是遍历整个程序。它减少了编译器的内存消耗,因为,例如,一次只需要计算一个DominatorSet。这也使得将来可以实现一些有趣的增强功能。

PassManager的有效性直接受到它所调度的pass行为的信息量的影响。 例如,面对未实现的getAnalysisUsage方法,“保留”集合是故意保守的。 如果没有实施,将会产生不允许任何分析结果贯穿执行pass的效果。

PassManager类公开了一个--debug-pass命令行选项,这些选项对于调试传递执行,查看工作原理以及诊断何时应该保留比当前更多的分析非常有用。 (要获取有关--debug-pass选项的所有变体的信息,只需键入“opt -help-hidden”)。

例如,通过使用-debug-pass = Structure选项,我们可以看到Hello World pass如何与其他pass交互。 让我们尝试使用gvn和licm passes:

$ opt -load lib/LLVMHello.so -gvn -licm --debug-pass=Structure < hello.bc > /dev/null
ModulePass Manager
  FunctionPass Manager
    Dominator Tree Construction
    Basic Alias Analysis (stateless AA impl)
    Function Alias Analysis Results
    Memory Dependence Analysis
    Global Value Numbering
    Natural Loop Information
    Canonicalize natural loops
    Loop-Closed SSA Form Pass
    Basic Alias Analysis (stateless AA impl)
    Function Alias Analysis Results
    Scalar Evolution Analysis
    Loop Pass Manager
      Loop Invariant Code Motion
    Module Verifi

这个输出显示了构造pass的时间。这里我们看到GVN使用dominator树信息来完成它的工作。LICMpass使用自然循环信息,自然循环信息也使用支配树。在LICM通过之后,模块验证器运行(由opt工具自动添加),它使用支配树检查生成的LLVM代码是否格式良好。注意,支配树只计算一次,并由三个pass共享。让我们看看当我们运行Hello World pass时,在两次pass之间会发生什么变化:

$ opt -load lib/LLVMHello.so -gvn -hello -licm --debug-pass=Structure < hello.bc > /dev/null
ModulePass Manager
  FunctionPass Manager
    Dominator Tree Construction
    Basic Alias Analysis (stateless AA impl)
    Function Alias Analysis Results
    Memory Dependence Analysis
    Global Value Numbering
    Hello World Pass
    Dominator Tree Construction //后面不会对此做改动
    Natural Loop Information
    Canonicalize natural loops
    Loop-Closed SSA Form Pass
    Basic Alias Analysis (stateless AA impl)
    Function Alias Analysis Results
    Scalar Evolution Analysis
    Loop Pass Manager
      Loop Invariant Code Motion
    Module Verifier
  Bitcode Writer
Hello: __main
Hello: puts
Hello: main

在这里,我们看到Hello World pass杀死了支配树pass,即使它根本不修改代码!为了解决这个问题,我们需要在我们的pass中添加以下getAnalysisUsage方法:

// We don't modify the program, so we preserve all analyses
void getAnalysisUsage(AnalysisUsage &AU) const override {
  AU.setPreservesAll();
}

现在,当我们运行我们的pass,我们得到这样的输出:

Now when we run our pass, we get this output:
$ opt -load lib/LLVMHello.so -gvn -hello -licm --debug-pass=Structure < hello.bc > /dev/null
Pass Arguments:  -gvn -hello -licm
ModulePass Manager
  FunctionPass Manager
    Dominator Tree Construction
    Basic Alias Analysis (stateless AA impl)
    Function Alias Analysis Results
    Memory Dependence Analysis
    Global Value Numbering
    Hello World Pass
    Natural Loop Information
    Canonicalize natural loops
    Loop-Closed SSA Form Pass
    Basic Alias Analysis (stateless AA impl)
    Function Alias Analysis Results
    Scalar Evolution Analysis
    Loop Pass Manager
      Loop Invariant Code Motion
    Module Verifier
  Bitcode Writer
Hello: __main
Hello: puts
Hello: main

这表明我们不再意外地使主导者信息失效,因此不必计算两次。

21.The releaseMemory method

virtual void releaseMemory();

PassManager自动决定何时计算分析结果,以及将它们保存多久。因为pass对象本身的生命周期实际上是编译过程的整个持续时间,所以我们需要某种方法在分析结果不再有用时释放它们。releaseMemory虚拟方法就是这样做的。如果您正在编写一个分析或任何其他保留大量状态的传递(用于“需要”您的传递并使用getAnalysis方法的另一个传递),那么您应该实现releaseMemory来释放分配给维护这个内部状态的内存。此方法在类的run*方法之后调用,在pass中的run*的下一个调用之前调用。

22.Registering dynamically loaded passes

在使用LLVM构建生产质量工具时,大小很重要,这既是为了分发,也是为了在目标系统上运行时调节驻留代码大小。因此,我们希望有选择地使用一些pass,而忽略其他pass,并保持稍后更改配置的灵活性。您希望能够做到这一切,并向用户提供反馈。这就是pass registration发挥作用的地方。

pass registration的基本机制是MachinePassRegistry类和MachinePassRegistryNode的子类。

MachinePassRegistry的一个实例用于维护MachinePassRegistryNode对象的列表。此实例维护列表,并与命令行接口通信添加和删除操作。

MachinePassRegistryNode子类的一个实例用于维护关于特定pass的信息。此信息包括命令行名称、命令帮助字符串和用于创建pass实例的函数的地址。这些实例之一的全局静态构造函数使用相应的MachinePassRegistry注册,即静态析构函数注销注册。因此,在工具中静态链接的pass将在启动时注册。动态加载的pass将在加载时注册,在卸载时注销。

23.Using existing registries

有预定义的寄存器来跟踪指令调度(RegisterScheduler)和寄存器分配(RegisterRegAlloc)机器pass。在这里,我们将描述如何注册寄存器分配器机器pass。

实现寄存器分配器机器pass。在注册分配器.cpp文件中添加以下内容:

#include "llvm/CodeGen/RegAllocRegistry.h"

同样在注册分配器.cpp文件中,定义一个creator函数:

FunctionPass *createMyRegisterAllocator() {
  return new MyRegisterAllocator();
}

注意,这个函数的签名应该匹配RegisterRegAlloc::FunctionPassCtor的类型。在同一文件内加入“installing”声明,格式如下:

static RegisterRegAlloc myRegAlloc("myregalloc",
                                   "my register allocator help string",
                                   createMyRegisterAllocator);

注意,帮助字符串前面的两个空格会在-help查询上产生一个整洁的结果。

$ llc -help
  ...
  -regalloc                    - Register allocator to use (default=linearscan)
    =linearscan                -   linear scan register allocator
    =local                     -   local register allocator
    =simple                    -   simple register allocator
    =myregalloc                -   my register allocator help string
  ...

就是这样。用户现在可以自由地使用-regalloc=myregalloc作为选项。除了使用RegisterScheduler类外,注册指令调度程序与此类似。注意,RegisterScheduler::FunctionPassCtor与RegisterRegAlloc::FunctionPassCtor有很大的不同。

要强制将寄存器分配器加载/链接到llc/lli工具中,请将creator函数的全局声明添加到pass.h中,并将“伪”调用行添加到llvm/Codegen/LinkAllCodegenComponents.h中。

24.Creating new registries

最简单的方法是克隆一个现有注册中心;我们建议llvm / CodeGen / RegAllocRegistry.h。要修改的关键内容是类名和FunctionPassCtor类型。

然后需要声明注册表。例子:如果你的pass注册表是registermypass,那么定义:

MachinePassRegistry RegisterMyPasses::Registry;

最后,声明pass的命令行选项。例子:

cl::opt<RegisterMyPasses::FunctionPassCtor, false,
        RegisterPassParser<RegisterMyPasses> >
MyPassOpt("mypass",
          cl::init(&createDefaultMyPass),
          cl::desc("my pass option help"));

这里的命令选项是“mypass”,createDefaultMyPass是默认的创建者。

25.Using GDB with dynamically loaded passes

不幸的是,使用动态加载传递的GDB并不像它应该的那样容易。首先,您不能在尚未加载的共享对象中设置断点,其次,共享对象中的内联函数存在问题。这里有一些建议来调试您的通过GDB。

为了便于讨论,我将假设您正在调试opt调用的转换,尽管这里的描述并不依赖于此

26.Setting a breakpoint in your pass

你要做的第一件事是在opt进程上启动gdb:

$ gdb opt
GNU gdb 5.0
Copyright 2000 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB.  Type "show warranty" for details.
This GDB was configured as "sparc-sun-solaris2.6"...
(gdb)

注意,opt包含很多调试信息,因此加载它需要时间。要有耐心。因为我们还不能在pass中设置断点(共享对象直到运行时才加载),所以我们必须执行流程,并在它调用我们的pass之前让它停止,但是在它加载了共享对象之后。最简单的方法是在PassManager::run中设置断点,然后用需要的参数运行进程:

$ (gdb) break llvm::PassManager::run
Breakpoint 1 at 0x2413bc: file Pass.cpp, line 70.
(gdb) run test.bc -load $(LLVMTOP)/llvm/Debug+Asserts/lib/[libname].so -[passoption]
Starting program: opt test.bc -load $(LLVMTOP)/llvm/Debug+Asserts/lib/[libname].so -[passoption]
Breakpoint 1, PassManager::run (this=0xffbef174, M=@0x70b298) at Pass.cpp:70
70      bool PassManager::run(Module &M) { return PM->run(M); }
(gdb)

一旦opt在PassManager::run方法中停止,现在就可以自由地在pass中设置断点,以便跟踪执行过程或执行其他标准调试工作。

27.Miscellaneous Problems

一旦您掌握了基本知识,GDB就会遇到一些问题,有些有解决方案,有些没有。

内联函数有伪堆栈信息。一般来说,GDB在获取堆栈跟踪和遍历内联函数方面做得相当好。然而,当一个传递被动态加载时,它就完全丧失了这种能力。我所知道的惟一解决方案是反内联一个函数(将它从类的主体移动到.cpp文件)。

重新启动程序会中断断点。根据上面的信息,您已经成功地在您的传递中植入了一些断点。接下来你要做的就是重启程序。,再次键入“run”),然后开始收到关于断点不可设置的错误。我发现“修复”这个问题的唯一方法是删除已经在pass中设置的断点,运行程序,并在PassManager::run中的执行停止后重新设置断点。

希望这些技巧对常见的用例调试情况有所帮助。如果你想提供一些自己的建议,请联系Chris。

28.未来的扩展计划

尽管LLVM Pass基础设施目前非常强大,并且做了一些漂亮的事情,但是我们希望在将来添加一些东西。我们的目标是:多线程LLVM

多CPU机器正变得越来越普遍,编译永远都不够快:显然,我们应该允许使用多线程编译器。因为通过上面定义的语义(尤其是他们不能跨不同的调用维护状态运行*方法),干净的方法来实现多线程PassManager类编译器会创建每个传递对象的多个实例,并允许独立实例黑客程序的不同部分在同一时间。

这个实现将避免每次传递都必须实现多线程结构,只需要LLVM核心在几个地方(对于全局资源)进行锁定。虽然这是一个简单的扩展,但是我们还没有时间(或者多处理器机器,这是一个原因)来实现它。尽管如此,我们已经准备好了LLVM pass SMP,您也应该这样做。

猜你喜欢

转载自blog.csdn.net/zhang14916/article/details/89295573