LLVM笔记(3) - PASS
1. pass的概念
在LLVM中优化以pass形式实现, 每一个pass代表一种优化. pass分为两类, 一类是分析(analysis)pass, 负责收集信息共其它pass使用, 辅助调试或使程序可视化; 另一类是变换(transform)pass, 改变程序的dataflow / controlflow. LLVM中实现了几十种优化pass, 其中许多pass运行不止一次. analysis pass存放在lib/Analysis下, transform pass存放在lib/Transforms下. 本文简要介绍其概念, 后文将逐个详细分析.
2. 跟踪与调试pass
LLVM提供了完善的日志系统来跟踪各个pass的运行.
1 [13:38:49] hansy@hansy:~$ cat test.c 2 int sigma(int cnt) 3 { 4 int sum = 0, i = 0; 5 for (i = 0; i < cnt; i++) 6 sum += i; 7 return sum; 8 } 9 10 [13:38:55] hansy@hansy:~$ clang test.c -O2 -mllvm -debug -S 2>test.ll 11 [13:39:02] hansy@hansy:~$ clang test.c -O2 -mllvm -debug-only=early-cse -S 2>test.ll 12 [13:39:05] hansy@hansy:~$ clang test.c -O2 -mllvm -print-before-all -S 2>test.ll 13 [13:39:16] hansy@hansy:~$ clang test.c -O2 -mllvm -print-after-all -S 2>test.ll 14 [13:39:25] hansy@hansy:~$ clang test.c -O2 -mllvm -print-after-all -mllvm -filter-print-funcs=sigma -S 2>test.ll
-mllvm指定将后一个选项传递给llvm. 因为默认执行的clang其实是一个driver, 在运行中调用对应的程序执行前端分析(clang), 中端优化指令选择(llvm)以及汇编链接(assembler & linker). -debug选项即将llvm中DEBUG宏输出到stderr. 如果我们只想需要某个特定pass的打印可以使用-debug-only选项(后接pass的DEBUG_TYPE).
通过-debug我们可以快速定位程序运行的路径, 但如果我们想要知道某个pass运行完后的结果可以使用后面两条命令. -print-before-all会在每个pass执行之前(根据pass的scope(function / loop / basicblock))打印IR, 类似的-print-after-all会在每个pass执行完毕后打印IR. 如果源文件中存在多个函数, 打印较多, 我们还可以使用-filter-print-funcs指定打印的函数名(对-debug选项无效).
3. DEBUG in LLVM
LLVM代码中使用LLVM_DEBUG()宏(defined in include/llvm/Support/Debug.h)控制打印信息, 该宏调用DEBUG_WITH_TYPE()宏, 后者根据NDEBUG是否定义决定是否执行括号内的内容.
1 #ifndef NDEBUG 2 #define DEBUG_WITH_TYPE(TYPE, X) \ 3 do { if (::llvm::DebugFlag && ::llvm::isCurrentDebugType(TYPE)) { X; } \ 4 } while (false) 5 #else 6 #define DEBUG_WITH_TYPE(TYPE, X) do { } while (false) 7 #endif 8 9 #define LLVM_DEBUG(X) DEBUG_WITH_TYPE(DEBUG_TYPE, X) 10 11 #ifndef NDEBUG 12 static cl::opt<bool, true> 13 Debug("debug", cl::desc("Enable debug output"), cl::Hidden, 14 cl::location(DebugFlag)); 15 static cl::opt<DebugOnlyOpt, true, cl::parser<std::string> > 16 DebugOnly("debug-only", cl::desc("Enable a specific type of debug output (comma separated list of types)"), 17 cl::Hidden, cl::ZeroOrMore, cl::value_desc("debug string"), 18 cl::location(DebugOnlyOptLoc), cl::ValueRequired); 19 #endif 20 21 bool isCurrentDebugType(const char *DebugType) { 22 if (CurrentDebugType->empty()) 23 return true; 24 for (auto &d : *CurrentDebugType) { 25 if (d == DebugType) 26 return true; 27 } 28 return false; 29 } 30 31 raw_ostream &llvm::dbgs() { 32 static struct dbgstream { 33 circular_raw_ostream strm; 34 35 dbgstream() : 36 strm(errs(), "*** Debug Log Output ***\n", 37 (!EnableDebugBuffering || !DebugFlag) ? 0 : DebugBufferSize) { 38 if (EnableDebugBuffering && DebugFlag && DebugBufferSize != 0) 39 sys::AddSignalHandler(&debug_user_sig_handler, nullptr); 40 } 41 } thestrm; 42 43 return thestrm.strm; 44 }
DebugType的设置见lib/Support/Debug.cpp文件, 其中DebugFlag(defined in lib/Support/Debug.cpp)是llvm选项-debug设置的. isCurrentDebugType()在不设置DebugType时返回true, 如果使用-debug-only选项那么容器CurrentDebugType非空即根据比较字符串结果决定打印. LLVM_DEBUG()宏固定使用DEBUG_TYPE作为传入的字符串, 所以不同pass下需要定义不同的DEBUG_TYPE宏, 同理如要打印某个pass就要找到对应的DEBUG_TYPE而不是直接写pass名. 注意输出时指定dbgs()为ostream, 它会将输出重定向到stderr(), 避免与正常的输出冲突.
最后说下NDEBUG宏, 该宏在Debug编译时不起效, 因此Debug编译时DEBUG()宏正常输出. 而Release编译时NDEBUG起效, 若也想输入调试信息需要在编译时增加-DLLVM_ENABLE_ASSERTIONS=On选项. 具体代码见cmake/modules/HandleLLVMOptions.cmake文件. 如下所示:
1 if( LLVM_ENABLE_ASSERTIONS ) 2 # MSVC doesn't like _DEBUG on release builds. See PR 4379. 3 if( NOT MSVC ) 4 add_definitions( -D_DEBUG ) 5 endif() 6 # On non-Debug builds cmake automatically defines NDEBUG, so we 7 # explicitly undefine it: 8 if( NOT uppercase_CMAKE_BUILD_TYPE STREQUAL "DEBUG" ) 9 add_definitions( -UNDEBUG ) 10 # Also remove /D NDEBUG to avoid MSVC warnings about conflicting defines. 11 foreach (flags_var_to_scrub 12 CMAKE_CXX_FLAGS_RELEASE 13 CMAKE_CXX_FLAGS_RELWITHDEBINFO 14 CMAKE_CXX_FLAGS_MINSIZEREL 15 CMAKE_C_FLAGS_RELEASE 16 CMAKE_C_FLAGS_RELWITHDEBINFO 17 CMAKE_C_FLAGS_MINSIZEREL) 18 string (REGEX REPLACE "(^| )[/-]D *NDEBUG($| )" " " 19 "${flags_var_to_scrub}" "${${flags_var_to_scrub}}") 20 endforeach() 21 endif() 22 endif()
4. 编写一个简单pass
LLVM文档介绍了如何编写一个hello world的pass, 具体见docs/WritingAnLLVMPass, 此文不再详述.
5. pass的分类
pass分为ImmutablePass, ModulePass, CallGraphSCCPass, FunctionPass, LoopPass, RegionPass, BasicBlockPass与MachineFunctionPass.
ImmutablePass指的是不运行, 不改变状态也永不更新的pass, 一般情况下用于显示编译器的配置信息.
ModulePass是将整个程序视作一个单元处理的pass, 它是最通用的pass类型. ModulePass可以调用函数级别的pass. ModulePass需要实现runOnModule(Module &).
CallGraphSCCPass用于被那些需要从底向上(bottom-up)遍历call graph的pass调用. 关于更多SCC内容在后文分析. CallGraphSCCPass需要实现doInitialization(CallGraph &), runOnSCC(CallGraphSCC &)和doFinalization(CallGraph &).
FunctionPass是以单个函数为作用域的pass, 每个函数间是相互独立的, 相互之间无法影响. FunctionPass需要实现doInitialization(Module &), runOnFunction(Function &)和doFinalization(Module &).
LoopPass是以单个loop为作用域的pass, 每个loop间相互独立. LoopPass以嵌套方式处理循环, 外层循环最后处理. LoopPass需要实现doInitialization(Loop *, LPPassManager &), runOnLoop(Loop *, LPPassManager &)和doFinalization().
RegionPass类似与LoopPass. RegionPass需要实现doInitialization(Region *, RGPassManager &), runOnRegion(Region *, RGPassManager &)和doFinalization().
BasicBlockPass类似FunctionPass, 但是以BasicBlock为scope. BasicBlockPass需要实现同样的接口, 但Initlialization与Finalization可以以Module或Function为单位.
MachineFunctionPass类似FunctionPass, 区别在于前者属于LLVM code generator(后端), 生成架构相关代码, 后者属于LLVM optimizer(中端), 生成通用的IR. 因此MachineFunctionPass无法通过通用pass接口注册(因此也无法使用opt调用优化), 而是通过TargetMachine::addPassesToEmitFile及其类似接口注册.
6. 注册pass
在编写完一个pass后并不会立即生效, 还需要将他注册到LLVM框架中. 根据pass作用域不同分为两处注册. 对于架构无关的中端优化pass, LLVM提供了一套pass pipleine. 在lib/Passes/PassRegistry.def中包含了LLVM中端优化pass. 对于架构相关的优化pass, 需要在目标后端目录下注册. 以X86为例, 在lib/Target/X86/X86TargetMachine.cpp中存放相关代码. 注意对特定架构的做的中端优化也放在该文件下, 但是在使用-emit-llvm生成IR时并不会调用这些优化.
7. 略