LLVM笔记1 LLVM Pass Manager

目录

一、LegacyPM添加Pass

二、NewPM添加Pass

三、简短的PASS别名设置

四、LegacyPM和NewPM比较

4.1 易使用性(NewPM)

4.2 易维护性

4.3 开发者友好性

五、为什么提出NewPM来代替LegacyPM呢?

5.1 性能

5.2 扩展性


一、LegacyPM添加Pass

LegacyPM的流水线在PassManagerBuilder.cpp中。

二、NewPM添加Pass

 

NewPM的流水线在PassBuilder.cpp和PassBuilderPipeline.cpp中。

三、简短的PASS别名设置

四、LegacyPM和NewPM比较

4.1 易使用性(NewPM)

  • 不需要明确继承的父类,ModulePass, FunctionPass,只需要提供run语义的函数即认为是Pass。
  • 不需要为Transform类定义一个ID。
  • 不需要通过 getAnalysisUsage(AnalysisUsage &AU)指定依赖关系,尝试通过getResult获取结果及认为有依赖。
  • 不需要AnalysisUsage指定required, preserved 等依赖Pass。
  • 不需要定义全局的静态变量或者INITIALIZE_PASS宏注册Pass。
  • Pass结构显示区分Transform Pass(PassInfoMixin)和Analysis Pass(AnalysisInfoMixin)。

4.2 易维护性

从LegacyPM和NewPM的类图, 序列图,约束,我们可以很清楚的看到NewPM的复杂度明显降低。从类的扩展机制上,例如PassManager均有 ModulePassManager, FunctionPassManager,LegacyPM通过继承关系来实现各个SubPassManager,继承的关系比较深;NewPM可以有效的利用PassManager模板进行扩展(模板在代码体积上会较大)。 此外,LegacyPM继承关系深伴随着众多的虚函数,代码的维护性和静态分析都带来了困难。

4.3 开发者友好性

LLVM的开发者主要分为2种类型:

一类是使用LLVM作为二次开发,主要会编写对应功能的Transform Pass和Analysis Pass,在LegacyPassManager框架下开发者需要遵循较多的约束,并且Pass的执行真正的执行顺序是由PMTopLevelManager的schedule确定的。相对于LegacyPassManager体系,开发者可以利用NewPassManager提供的模板和Pass语义快速开发,并且由于依赖关系,执行顺序是通过getResult等接口调用显示指定的,对于代码的逻辑和可维护性较好。

第二类的开发者是对LLVM需要深度的改造,他们需要甚至需要扩展、定制自己PassManager,AnalysisManager等关键的数据结构,基于LegacyPassManager从技术门槛上会比较低,但是会引入非常复杂的依赖,调用关系,例如,各个PMDataManager子类的管理,特别是PMTopLevelManager对PMDataManager及其中的Pass的调度复杂度会很高,并且问题很难排查。如果基于NewPassManager来开发,由于通过Concept-Model设计模式,依赖关系和执行顺序通过接口调用显示指定,复杂度相对较低,但对于开发者的技术门槛相对较高。

五、为什么提出NewPM来代替LegacyPM呢?

5.1 性能

class ModulePass : public Pass {
public:
  /// runOnModule - Virtual method overriden by subclasses to process the module
  /// being operated on.
  virtual bool runOnModule(Module &M) = 0;
};
class MyPass : public ModulePass {
    bool runOnModule(Module &M) override {
    }
}

int main() {
    Module M;
    ModulePass* pass = New MyPass();
    pass->runOnModule(M);
}

在这种接口设计中,我们对于自定义的Pass可以通过继承抽象类并实现自定义的RunX方法,这种设计在现有的C++系统中是一种常态,这种设计的问题是什么呢?在上述代码片段中,在main函数中通过pass->runOnModule进行成员函数调用,该成员函数在虚表中,因此会带来虚表的查询和函数间接调用的开销,此外,由于虚表的查询需要运行时的运行时类型信息以及函数的间接调用,因此,无法在编译器间对runOnModule调用进行inline等优化,从而导致性能优化的限制。

为了解决这个问题LLVM采用了"static polymorphism"的设计思路(参考C++ CRTP介绍):如下的代码片段中,ModulePass作为基类,在runOnModule方法调用过程中可以通过静态的函数派发实现子类的函数调用,通过这种方式避免了虚表的查找和函数间接调用,同时也可以在编译期间将runOnModule做到inline优化。

template <typename PassT>
class ModulePass {
public:
  /// runOnModule - Virtual method overriden by subclasses to process the module
  /// being operated on.
  bool runOnModule(Module &M) {
      return (static_cast<PassT*>(this))->runOnModule(M);
  }
};

class MyPass : public ModulePass<MyPass> {
    bool runOnModule(Module &M) override {
    }
}

int main() {
    Module M;
    ModulePass<MyPass>* pass = New MyPass();
    pass->runOnModule(M);
}

虽然"static polymorphism"可以解决一部分子类的派生问题,但你会发现如何维护一个父类的列表,以完成运行时的批量派发是一个待解决的问题(提升性能的同时,牺牲了灵活性)。

std::vector<ModulePass*> ...

为了解决"static polymorphism"引入而无法定义如下的基类列表的问题,LLVM提出了2种设计。

1. 自定义类层次结构和RTTI

其核心的思想是为各类定义和赋予一个type-tag(类型id),并在编译时期完成类型检查并根据classof完成调用的静态派发,参考LLVM RTTI

class Shape {
 public:
   /// Discriminator for LLVM-style RTTI (dyn_cast<> et al.)
   enum ShapeKind {
     SK_Square,
     SK_Circle
   };
 private:
   const ShapeKind Kind;
 public:
   ShapeKind getKind() const { return Kind; }

   Shape(ShapeKind K) : Kind(K) {}
   double computeArea() {};
 };

 class Square : public Shape {
   double SideLength;
 public:
   Square(double S) : Shape(SK_Square), SideLength(S) {}
   double computeArea() {};
+
+  static bool classof(const Shape *S) {
+    return S->getKind() == SK_Square;
+  }
 };

 class Circle : public Shape {
   double Radius;
 public:
   Circle(double R) : Shape(SK_Circle), Radius(R) {}
   double computeArea() {};
+
+  static bool classof(const Shape *S) {
+    return S->getKind() == SK_Circle;
+  }
 };

int main() {
    Shape* pShape = new Circle();
    dyn_cast<Circle*>(pShape)->computeArea();
    // dyn_cast<Circle*> = Circle::classof(pShape) == SK_Circle ?
    //                     static_cast<Circle*>(pShape) :nullptr;
}

2. 基于Concept-Model的多态设计

由于性能的考量我们引入了CRTP的模板设计,但它实际上是通过模板技术为各个不同的子类引入了一层封装(但这层分装不需要client用户逐个实现,而是通过模板参数及模板实例化自动生成)。因此在编译后会生成各自不同的wrapper类型,为了能够满足通用的列表存储,我们需要引入抽象基类Concept来作为各个子类的Generic Base Class。

class Concept {
public: 
   virtual bool runOnModule(Module &M) = 0;
};

template <typename PassT>
class ModulePass : Concept {
public:
  /// runOnModule - Virtual method overriden by subclasses to process the module
  /// being operated on.
  bool runOnModule(Module &M) {
      return (static_cast<PassT*>(this))->runOnModule(M);
  }
};

class MyPassA : public ModulePass<MyPass> {
    bool runOnModule(Module &M) override {
    }
}
class MyPassB : public ModulePass<MyPass> {
    bool runOnModule(Module &M) override {
    }
}

int main()  {
    Module M;
    std::vector<Concept*> passVec;
    passVec.emplace_back(new MyPassA);
    passVec.emplace_back(new MyPassB);
    for(auto pass: passVec ) {
        pass->runOnModule(M);
    }
}

至此,那么为什么要引入Concepted-based Polymorphism而不是基于抽象接口的多态实现呢?这个问题我们从"扩展性"方面来详细阐述。

5.2 扩展性

如上节所阐述的基于抽象接口的多态和基于Concepted-based的多态都会引入RTTI和调用的动态派发,他们有什么区别,为何要引入Concepted-based Polymorphism机制呢?

// PartA
class ModulePass {
public:
  /// runOnModule - Virtual method overriden by subclasses to process the module
  /// being operated on.
  virtual bool runOnModule(Module &M) = 0;
};

class MyPass : public ModulePass {
    bool runOnModule(Module &M) override {
    }
}
// PartB : third-party
class ExternalPass {
     bool runOnModule(Module &M) {
     }
}

如下的示例存在2个独立的不同来源,如果要求我们的框架可以兼容类型与PartB部分的代码,那么可以有两种方案。

class ExternalPass : public ModulePass {
     bool runOnModule(Module &M) {
     }
}

1.改造PartB中的类,继承自抽象的接口ModulePass。这种改造往往是不可实施的,PartB一般是一个独立的子系统和子模块,改造量大,此外,ModulePass会成为所有子模块的依赖,对于外部项目的侵入是不可以接受的。

2.为不同的外部模块ExternalPass增加adaptor接口。

// PartB : third-party
class ExternalPassAdaptor : public ModulePass {
    ExternalPassAdaptor(ExternalPass* pass) : externPass(pass) {}
    bool runOnModule(Module &M) override {
        pass->runOnModule(M);
    }
    ExternalPass* externPass;
}

class ExternalPass {
     bool runOnModule(Module &M) {
     }
}

这种方式为每个外部的ExternalPass均需要增加一个Adaptor,每次外部类型集成均需要修改集成部分代码。我们从之前讨论的基于Concept-based Polymorphism的设计,可以为外部Pass增加一个Adaptor模板类,从而避免每次集成外部类的重复Adaptor实现。这种方式看起来对可扩展性非常的友好,但其中有一个限制,那就是说我们需要确保被集成的三方Pass和我们的抽象接口是"概念一致"的(或者称为语义一致性 Semantic Polymorphism),即我们的抽象接口提供了runOnModule函数的语义,那么我们可以集成的ExternalPass也有runOnModule语义。

template <typename PassT>
class PassAdaptor : public ModulePass {
    PassAdaptor(PassT* pass) : externPass(pass) {}
    bool runOnModule(Module &M) override {
        pass->runOnModule(M);
    }
    PassT* externPass;
}

class ExternalPass {
     bool runOnModule(Module &M) {
     }
}

int main() {
    Module M;
    ModulePass* pass = new PassAdaptor(ExternalPass);
    pass->runOnModule();
}

按照如上的定义,虽然在"物理"类继承关系上,三方类(ExternalPass)不在我们的类继承体系中,我们可以通过如上的PassAdaptor Concept-Based框架集成三方类,使得他们在语法表示和语义层是一致的。

从性能、扩展性角度看,简单的基于抽象接口的继承多态都不是最好的选择,Concept-Based Polymorphism的设计在扩展性上有明显的优势。

参考:LLVM PassManager对C++程序设计的思考

  • 23
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值