前言
本篇文章作为 Pass 编写的入门,主要介绍以下内容:
- 示例 Pass 编写
- Pass 编译构建和运行
- Pass 不同子类
- Pass 注册
- Pass 间依赖
示例
首先我们先结合一个小示例,尝试编写 Pass,之后再详细介绍涉及的内容。
#include "llvm/Pass.h" //添加头文件
#include "llvm/IR/Function.h"
#include "llvm/Support/raw_ostream.h"
#include "llvm/IR/LegacyPassManager.h"
#include "llvm/Transforms/IPO/PassManagerBuilder.h"
using namespace llvm; //添加 llvm 命名空间
namespace { // 定义匿名命名空间
struct SkeletonPass : public FunctionPass { // 定义 SkeletonPass 继承 FunctionPass
static char ID; // // ID 标识
SkeletonPass() : FunctionPass(ID) {}
virtual bool runOnFunction(Function &F) { //// 重载 runOnFunction 函数,接受Function类型参数
errs() << "I saw a function called " << F.getName() << "!\n"; // 输出函数名
return false; // 没有修改 F,因此返回 false
}
};
}
char SkeletonPass::ID = 0; //id ,随便指定一个数字就可以
// Register the pass so `opt -skeleton` runs it.
static RegisterPass<SkeletonPass> X("skeleton", "a useless pass"); //注册Pass
errs() 为LLVM 中 C++ 的输出流。
以上的示例 Pass 会在每个函数运行时输出函数名。
构建
编写好 Pass 后,需要进行编译构建,有两种构建方式:
- 将 Pass 放至 LLVM 源码目录,并修改已有的 CMakeLists 文件。可以直接查看 官方示例
- 单独建立目录,并编写好 CMakeLists文件。
单独构建目录结构大致如下:
pass-project
- CMakeLists.txt
- MyPass
- mypass.cpp
- CMakeLists.txt
- build
参考 llvm-pass-skeleton (该repo有不同的分支,用于展示不同的用法),不过由于 llvm-pass-skeleton 依赖的版本比较旧,对应的编译选项 C++11 已经不适合,因此需要修改下 CMakeLists 文件,在这里提供我使用的:
llvm-pass-skeleton
下的 CMakeList.txt (添加编译选项标准为 C++14):
cmake_minimum_required(VERSION 3.1)
project(Skeleton)
find_package(LLVM REQUIRED CONFIG)
add_definitions(${LLVM_DEFINITIONS})
include_directories(${LLVM_INCLUDE_DIRS})
link_directories(${LLVM_LIBRARY_DIRS})
add_compile_options(-std=c++14)
add_subdirectory(skeleton) # Use your pass name here.
skeleton 内的 CMakeList.txt(删除了 target_compile_features(SkeletonPass PRIVATE cxx_range_for cxx_auto_type
因为该语句使用C++11来编译Pass)
add_library(SkeletonPass MODULE
# List your source files here.
Skeleton.cpp
)
# LLVM is (typically) built with no C++ RTTI. We need to match that;
# otherwise, we'll get linker errors about missing RTTI data.
set_target_properties(SkeletonPass PROPERTIES
COMPILE_FLAGS "-fno-rtti"
)
# Get proper shared-library behavior (where symbols are not necessarily
# resolved when the shared library is linked) on OS X.
if(APPLE)
set_target_properties(SkeletonPass PROPERTIES
LINK_FLAGS "-undefined dynamic_lookup"
)
endif(APPLE)
这里我使用的 Skeleton 文件是 noauto 分支下的。
在 build 目录依次执行
cmake ..
make
编译成功:
# mm @ iZ2ze2gbki9vcb415a0z6rZ in ~/llvm-learn/llvm-pass-skeleton/build on git:master x [11:41:04]
$ make
Scanning dependencies of target SkeletonPass
[ 50%] Building CXX object skeleton/CMakeFiles/SkeletonPass.dir/Skeleton.cpp.o
[100%] Linking CXX shared module libSkeletonPass.so
[100%] Built target SkeletonPass
在 build/skeleton
目录下可以看到 libSkeletonPass.so
文件。
使用 opt 运行 Pass
获得 so 文件后,我们通过 opt 的 -load 选项动态加载此文件,选择 pass 执行。
通过 opt -help 命令也可以看到我们的 pass:
$ opt -load ./libSkeletonPass.so -help | grep skeleton
--skeleton - a useless pass
直接运行 opt -load ./libSkeletonPass.so -skeleton test.ll -o test.ll
就可对 test.ll 文件应用编写好的 Pass。
也可以使用C/C++ 编写文件,使用 clang 转化为 LLVM IR 再使用 opt 命令运行。
使用 Clang 自动化运行 Pass
每次使用 clang 编译获得LLVM IR,再通过 opt 运行 pass,再再使用 opt 执行一些优化,这个过程十分繁琐。
我们可以在Pass 文件将注册部分写成这样(MyPass 是自定义的Pass 类), llvm-pass-skeleton的master 分支下就是这样编写的,可以对比一下 noauto 分支下的不同。
static void registerMyPass(const PassManagerBuilder &,
PassManagerBase &PM) {
PM.add(new MyPass()); // PM 用于添加 Pass
}
static RegisterStandardPasses
RegisterMyPass(PassManagerBuilder::EP_EarlyAsPossible,
registerMyPass); // 第一个参数表示加载的时间,枚举值,第二个参数是函数指针
之后便可以通过以下命令直接运行了
$ clang -Xclang -load -Xclang mypass.so ...
这里我简单地编写了一些 Demo 进行测试(Pass 就是SkeletonPass)
#include <stdio.h>
void func2(){
printf("hello world");
}
void func(){
func2();
}
int main(){
func();
return 0;
}
执行结果如下:
$ clang -Xclang -load -Xclang ./libSkeletonPass.so test.c
I saw a function called func2!
I saw a function called func!
I saw a function called main!
Pass
通过以上内容,可以大致了解 Pass 的编写,构建和运行,接下来我们进一步了解 Pass 的相关内容。
推荐先阅读这篇文章,llvm学习(七):IR 的基础结构 了解基础概念。
LLVM 提供了多种Pass类, 均继承自 Pass class。都包含一些关键的虚函数,自定义 Pass 时需要重载这些函数。
介绍常用的 Pass 类及方法,其中比较常用的是 ModulePass 和 FunctionPass。
名称 | 解释 | 接口 |
---|---|---|
ImmutablePass | 不常用的,指不运行, 不改变状态也永不更新的pass,一般情况下用于显示编译器的配置信息。 | |
ModulePass | 最常用的,将整个程序看作一个单元进行处理,可以引用,添加和删除 Function | virtual bool runOnModule(Module &M) = 0; |
CallGraphSCCPass | 可以从底至上遍历程序的函数调用关系图。 | virtual bool doInitialization(CallGraph &CG); virtual bool runOnSCC(CallGraphSCC &SCC) = 0; virtual bool doFinalization(CallGraph &CG); |
FunctionPass | 可以修改和分析 Function 的行为,每个函数间是相互独立的, 相互之间无法影响。 | virtual bool doInitialization(Module &M); virtual bool runOnFunction(Function &F) = 0; virtual bool doFinalization(Module &M); |
LoopPass | 以循环为单位,以循环嵌套顺序处理,外层循环最后处理。可以使用 LPPassManager 接口更新循环嵌套。 | virtual bool doInitialization(Loop *, LPPassManager &LPM); virtual bool runOnLoop(Loop *, LPPassManager &LPM) = 0; virtual bool doFinalization(); |
RegionPass | Region是一个控制流图的连接的子图,单入口单出口。可以被用来分析和优化控制流图中的部分内容。。使用 RGPassManager 接口可以更新 region tree。 | virtual bool doInitialization(Region *, RGPassManager &RGM); virtual bool runOnRegion(Region *, RGPassManager &RGM) = 0; virtual bool doFinalization(); |
MachineFunctionPass | 类似FunctionPass, 但它属于LLVM code generator(后端), 生成架构相关代码, FunctionPass 属于,生成通用的IR。无法通过通用Pass 接口注册 | virtual bool runOnMachineFunction(MachineFunction &MF) = 0; |
- 不同的 Pass 运行在不同的 XX(Module/Function/Loop..) 之上
runOnxx
中通常为 Pass 的主要代码,在里面实现优化分析工作。doInitialization
顾名思义做一些初始化工作,doFinalization
做一些收尾工作。- 如果这些虚函数修改工作,则返回 true,否则返回 false。
Pass 注册
编写 Pass 后需要注册 Pass 到LLVM框架中。根据Pass作用域不同分为两种:
- 对于架构无关的,在 LLVM IR 层面的Pass
- 对于架构相关的优化Pass,在LLVM Backend层面的Pass,需要在目标后端目录下注册。
下面仅介绍常见的在LLVM IR 层面的注册。(在lib/Passes/PassRegistry.def
目录可以查看 LLVM IR 优化pass. )
通过 RegisterPass
template注册Pass。接口如下:
//其中第一参数 PassArg 通常代表Pass的名称,用于opt 工具识别 ,第二个参数是 Pass 的具体名称。
llvm::RegisterPass< passName >::RegisterPass ( StringRef PassArg,
StringRef Name,
bool CFGOnly = false,
bool is_analysis = false
)
如果想要方便输出自定义 Pass 的信息,用于调试和分析,可以重载 print 函数,原型如下
virtual void print(llvm::raw_ostream &O, const Module *M) const;
使用 opt -analyze
可以调用该方法。其中参数 llvm::raw_ostream
指定需要输出结果的流,Module
指定需要分析的上层 Module 的指针。
Pass 间交互/依赖
LLVM Pass之间的依赖由PassManager管理的,如果自定义的 Pass 依赖其他 Pass,则需要实现 getAnalysisUsage
方法,获得 AnalysisUsage
对象。这个对象包含了 Pass 依赖的信息。
virtual void getAnalysisUsage(AnalysisUsage &Info) const;
Pass 调用 AnalysisUsage 对象的以下方法之一解决依赖,安排指定Pass在自定义的Pass 之前运行。
-
AnalysisUsage::addRequired<>
方法 -
AnalysisUsage::addRequiredTransitive<>
方法 -
AnalysisUsage::addPreserved<>
方法
具体可以查看这篇文章: 【LLVM】Pass之间的依赖
getAnalysisUsage
实现示例如下:
// This example modifies the program, but does not modify the CFG
void LICM::getAnalysisUsage(AnalysisUsage &AU) const {
AU.setPreservesCFG(); // 保留 CFG
AU.addRequired<LoopInfoWrapperPass>(); //等LoopInfoWrapperPass执行后再执行
}
-debug-pass 选项可以 debugging pass execution, seeing how things work, and diagnosing when you should be preserving more analyses than you currently are.
可以查看 PASS 依赖:
opt sum-O0.ll -debug-pass=Structure -mem2reg -S -o sum-O1.ll
Pass Arguments 可以看到。
优化
介绍用于不同优化的目的Pass。
内存优化
合并指令
IR 层优化
-
Ox 指定优化级别,具体有:
-
O0 : 不优化,用于代码调试
-
O1 :介于 O0 和 O2之间
-
O2 :中等优化
-
Os :类似于 O2 ,体积更小
-
Oz :类似于 Os , 体积最小
-
O3 :类似于 O2 ,代码体积大于 O2,但是运行速度更快
-
O4 :类似于 O3 ,增加了链接时优化。
更多内置分析与优化Pass请查看文档 LLVM’s Analysis and Transform Passes
总结
以上内容介绍粗略地 Pass的编写/编译构建/运行等内容,内容比较浅显,想要真正地写一些有用的 Pass 还需要深入了解 LLVM 的接口。
PS:文章介绍的是传统的 Pass 编写方式,目前已有新版的PassManager,API 都有了变化,感兴趣的可以查看搜索 Writing LLVM Pass in 2018 系列的文章。