#include "llvm/ADT/Statistic.h"#include "llvm/IR/Function.h"#include "llvm/IR/PassManager.h"#include "llvm/Passes/PassBuilder.h"#include "llvm/Passes/PassPlugin.h"#include "llvm/Support/Debug.h"
为了缩短源代码,我们将告诉编译器我们正在使用llvm命名空间:using namespace llvm;
LLVM的内置调试基础设施要求我们定义一个调试类型,这是一个字符串这个字符串稍后会在打印的统计信息中显示:#define DEBUG_TYPE "ppprofiler"
接下来,我们使用ALWAYS_ENABLED_STATISTIC宏定义一个计数器变量第一个参数是计数器变量的名称,第二个参数是统计信息中将打印的文本:ALWAYS_ENABLED_STATISTIC( NumOfFunc, "Number of instrumented functions.");
注意可以使用两个宏来定义计数器变量如果您使用STATISTIC宏,那么只有在调试构建中启用了断言,或者在CMake命令行上将LLVM_FORCE_ENABLE_STATS设置为ON时,才会收集统计信息如果您改用ALWAYS_ENABLED_STATISTIC宏,则始终收集统计信息值但是,使用–stats命令行选项打印统计信息仅适用于前一种方法如果需要,您可以通过调用llvm::PrintStatistics(llvm::raw_ostream)函数打印收集的统计信息接下来,我们必须在一个匿名命名空间中声明通道类该类继承自PassInfoMixin模板此模板只添加一些样板代码,例如name()方法它不用于确定通道的类型当执行通道时,LLVM将调用run()方法我们还需要一个名为instrument()的辅助方法:namespace {class PPProfilerIRPass : public llvm::PassInfoMixin<PPProfilerIRPass> {public: llvm::PreservedAnalyses run(llvm::Module &M, llvm::ModuleAnalysisManager &AM);private: void instrument(llvm::Function &F, llvm::Function EnterFn, llvm::Function ExitFn);};}
现在,让我们定义如何对函数进行仪器化除了要仪器化的函数外,还传递了要调用的函数:void PPProfilerIRPass::instrument(llvm::Function &F, Function EnterFn, Function ExitFn) {
在函数内部,我们更新统计计数器: ++NumOfFunc;
为了轻松插入IR代码,我们需要一个IRBuilder类的实例我们将将其设置为函数的第一条基本块: IRBuilder<> Builder(&F.getEntryBlock().begin());
现在我们有了构建器,我们可以插入一个全局常量,该常量保存我们希望仪器化的函数的名称: GlobalVariable FnName = Builder.CreateGlobalString(F.getName());
接下来,我们将插入对__ppp_enter()函数的调用,将名称作为参数传递: Builder.CreateCall(EnterFn->getFunctionType(), EnterFn, {FnName});
要调用__ppp_exit()函数,我们必须定位所有的返回指令方便的是,由调用SetInsertionPoint()函数设置的插入点位于作为参数传递的指令之前,因此我们可以在该点插入调用: for (BasicBlock &BB : F) { for (Instruction &Inst : BB) { if (Inst.getOpcode() == Instruction::Ret) { Builder.SetInsertPoint(&Inst); Builder.CreateCall(ExitFn->getFunctionType(), ExitFn, {FnName}); } } }}
接下来,我们将实现run()方法LLVM将传递我们的通道工作的模块和分析管理器,如果需要,我们可以从分析管理器请求分析结果:PreservedAnalysesPPProfilerIRPass::run(Module &M, ModuleAnalysisManager &AM) {
这里有一个轻微的麻烦:如果包含__ppp_enter()和__ppp_exit()函数实现的运行时模块被仪器化,那么我们会陷入麻烦,因为我们创建了一个无限递归为了避免这种情况,如果定义了这些函数中的一个,我们必须简单地什么都不做: if (M.getFunction("__ppp_enter") || M.getFunction("__ppp_exit")) { return PreservedAnalyses::all(); }
现在,我们准备声明函数这里没有什么不寻常的:首先,创建函数类型,然后创建函数: Type VoidTy = Type::getVoidTy(M.getContext()); PointerType PtrTy = PointerType::getUnqual(M.getContext()); FunctionType EnterExitFty = FunctionType::get(VoidTy, {PtrTy}, false); Function EnterFn = Function::Create( EnterExitFty, GlobalValue::ExternalLinkage, "__ppp_enter", M); Function ExitFn = Function::Create( EnterExitFty, GlobalValue::ExternalLinkage, "__ppp_exit", M);
我们现在需要做的就是循环遍历模块中的所有函数,并通过调用我们的instrument()方法来仪器化找到的函数当然,我们需要忽略只是原型的函数声明还可以有不带名称的函数,这与我们的方法不兼容我们将过滤掉这些函数: for (auto &F : M.functions()) { if (!F.isDeclaration() && F.hasName()) instrument(F, EnterFn, ExitFn); }
最后,我们必须声明我们没有保留任何分析这可能过于悲观,但我们通过这样做保持安全: return PreservedAnalyses::none();}
我们新通道的功能现在已经实现要使用我们的通道,我们需要将其注册到PassBuilder对象中这可以通过两种方式完成:静态或动态如果插件静态链接,那么它需要提供一个名为get<Plugin-Name>PluginInfo()的函数要使用动态链接,需要提供llvmGetPassPluginInfo()函数在这两种情况下,都返回PassPluginLibraryInfo结构的实例,该结构提供了有关插件的一些基本信息最重要的是,这个结构包含指向注册通道的函数的指针让我们将这个添加到我们的源代码中在RegisterCB()函数中,我们注册一个Lambda函数,当解析通道管道字符串时调用如果通道的名称是ppprofiler,那么我们将我们的通道添加到模块通道管理器中这些回调将在下一节中进一步扩展:void RegisterCB(PassBuilder &PB) { PB.registerPipelineParsingCallback( [](StringRef Name, ModulePassManager &MPM, ArrayRef<PassBuilder::PipelineElement>) { if (Name == "ppprofiler") { MPM.addPass(PPProfilerIRPass()); return true; } return false; });}
getPPProfilerPluginInfo()函数在插件静态链接时调用它返回有关插件的一些基本信息:llvm::PassPluginLibraryInfo getPPProfilerPluginInfo() { return {LLVM_PLUGIN_API_VERSION, "PPProfiler", "v0.1", RegisterCB};}
最后,如果插件动态链接,那么在加载插件时将调用llvmGetPassPluginInfo()函数但是,当将此代码静态链接到工具时,您可能会遇到链接器错误,因为该函数可能在几个源文件中定义解决方案是用宏保护该函数:#ifndef LLVM_PPPROFILER_LINK_INTO_TOOLSextern "C" LLVM_ATTRIBUTE_WEAK ::llvm::PassPluginLibraryInfollvmGetPassPluginInfo() { return getPPProfilerPluginInfo();}#endif
有了这个,我们已经实现了通道插件在我们看看如何使用新插件之前,让我们检查一下如果我们想将通道插件添加到LLVM源树中需要做哪些更改将通道添加到LLVM源树 实现一个新的通道作为插件很有用,例如,如果您计划与预编译的clang一起使用另一方面,如果您编写自己的编译器,那么将新通道直接添加到LLVM源树中可能有充分的理由您可以以两种不同的方式做到这一点——作为插件和完全集成的通道插件方法需要的更改较少在LLVM源树内利用插件机制 执行LLVM IR转换的通道的源代码位于llvm-project/llvm/lib/Transforms目录中在该目录内,创建一个名为PPProfiler的新目录,并将源文件PPProfiler.cpp复制到其中您不需要进行任何源代码更改要将新插件集成到构建系统,请创建一个名为CMakeLists.txt的文件,内容如下:
add_llvm_pass_plugin(PPProfiler PPProfiler.cpp)
最后,在父目录的CmakeLists.txt文件中,您需要通过添加以下行来包含新源目录:add_subdirectory(PPProfiler)
现在,您已准备好使用PPProfiler添加构建LLVM更改到LLVM的构建目录,并手动运行Ninja:$ ninja install
CMake将检测到构建描述的更改,并重新运行配置步骤您将看到一个额外的行:-- Registering PPProfiler as a pass plugin (static build: OFF)
这告诉您插件已被检测到并已作为共享库构建安装步骤之后,您将在<install directory>/lib目录中找到共享库PPProfiler.so到目前为止,与上一节的通道插件的唯一区别是共享库作为LLVM的一部分安装但您也可以将新插件静态链接到LLVM工具为此,您需要重新运行CMake配置,并在命令行上添加-DLLVM_PPPROFILER_LINK_INTO_TOOLS=ON选项从CMake查找此信息以确认更改的构建选项:-- Registering PPProfiler as a pass plugin (static build: ON)
再次编译并安装LLVM后,发生了以下变化:插件被编译到静态库libPPProfiler.a中,该库安装在<install directory>/lib目录中LLVM工具(如opt)与该库链接插件被注册为扩展您可以检查<install directory>/include/llvm/Support/Extension.def文件现在包含以下行:HANDLE_EXTENSION(PPProfiler)
此外,所有支持此扩展机制的工具都会获取新通道在“创建优化管道”部分中,您将学习如何在您的编译器中执行此操作这种方法效果很好,因为新源文件位于单独的目录中,并且只有一个现有文件被更改这最小化了如果您尝试使修改后的LLVM源树与主存储库保持同步,合并冲突的可能性还有将新通道作为插件添加不是最佳方法的情况LLVM提供的通道使用不同的注册方式如果您开发了一个新的通道并提议将其添加到LLVM中,LLVM社区接受了您的贡献,那么您将希望使用相同的注册机制完全集成通道到通道注册表 要将新通道完全集成到LLVM中,插件的源代码需要稍微不同的结构主要原因是通道类的构造函数是从通道注册表调用的,这要求类接口被放入头文件中像以前一样,您必须将新通道放入LLVM的Transforms组件中首先创建llvm-project/llvm/include/llvm/Transforms/PPProfiler/PPProfiler.h头文件该文件的内容是类定义;将其放入llvm命名空间中不需要其他更改:#ifndef LLVM_TRANSFORMS_PPPROFILER_PPPROFILER_H#define LLVM_TRANSFORMS_PPPROFILER_PPPROFILER_H#include "llvm/IR/PassManager.h"namespace llvm {class PPProfilerIRPass : public llvm::PassInfoMixin<PPProfilerIRPass> {public: llvm::PreservedAnalyses run(llvm::Module &M, llvm::ModuleAnalysisManager &AM);private: void instrument(llvm::Function &F, llvm::Function EnterFn, llvm::Function ExitFn);};} // namespace llvm#endif
接下来,将通道插件的源文件PPProfiler.cpp复制到新的目录llvm-project/llvm/lib/Transforms/PPProfiler中需要以以下方式更新此文件:由于类定义现在在头文件中,您必须从此文件中删除类定义在顶部,添加头文件的#include指令:#include "llvm/Transforms/PPProfiler/PPProfiler.h"
必须删除llvmGetPassPluginInfo()函数,因为通道没有被构建为独立的共享库和以前一样,您还需要为构建提供一个CMakeLists.txt文件您必须声明新通道为一个新组件:add_llvm_component_library(LLVMPPProfiler PPProfiler.cpp LINK_COMPONENTS Core Support)
之后,像在上一节中一样,您需要通过向父目录的CMakeLists.txt文件添加以下行来包含新的源目录:add_subdirectory(PPProfiler)
在LLVM内部,可用的通道保存在llvm/lib/Passes/PassRegistry.def数据库文件中您需要更新此文件新通道是一个模块通道,因此我们需要在文件中搜索定义模块通道的部分,例如,通过搜索MODULE_PASS宏在该部分内,添加以下行:MODULE_PASS("ppprofiler", PPProfilerIRPass())
这个数据库文件在llvm/lib/Passes/PassBuilder.cpp类中使用此文件需要包含您的新头文件:#include "llvm/Transforms/PPProfiler/PPProfiler.h"
这些是基于插件版本的新通道所需的所有源代码更改由于您创建了一个新的LLVM组件,还需要在llvm/lib/Passes/CMakeLists.txt文件中添加一个链接依赖项在LINK_COMPONENTS关键字下,您需要添加一行,包含新组件的名称: PPProfiler
Et voilà - 您现在可以构建并安装LLVM了新通道ppprofiler现在对所有LLVM工具都可用它已被编译到libLLVMPPProfiler.a库中,并作为PPProfiler组件在构建系统中可用到目前为止,我们已经讨论了如何创建一个新的通道在下一节中,我们将检查如何使用ppprofiler通道使用LLVM工具中的ppprofiler通道 回想一下我们在“作为插件开发ppprofiler通道”一节中作为LLVM树外插件开发的ppprofiler通道在这里,我们将学习如何使用这个通道与LLVM工具,如opt和clang,因为它们可以加载插件先看看opt在opt中运行通道插件 要尝试新插件,您需要一个包含LLVM IR的文件最简单的方法是翻译一个C程序,例如一个基本的“Hello World”风格的程序:#include <stdio.h>int main(int argc, char argv[]) { puts("Hello"); return 0;}
使用clang编译此文件,hello.c:$ clang -S -emit-llvm -O1 hello.c
您将得到一个非常简单的IR文件hello.ll,包含以下代码:$ cat hello.ll@.str = private unnamed_addr constant [6 x i8] c"Hello\00", align 1define dso_local i32 @main( i32 noundef %0, ptr nocapture noundef readnone %1) { %3 = tail call i32 @puts( ptr noundef nonnull dereferenceable(1) @.str) ret i32 0}
这足以进行测试通道要运行通道,您需要提供几个参数首先,您需要通过--load-pass-plugin选项告诉opt加载共享库要运行单个通道,您必须指定--passes选项使用hello.ll文件作为输入,您可以运行以下命令:$ opt --load-pass-plugin=./PPProfile.so \ --passes="ppprofiler" --stats hello.ll -o hello_inst.bc
如果启用了统计信息生成,您将看到以下输出:===--------------------------------------------------------=== ... Statistics Collected ...===--------------------------------------------------------===1 ppprofiler - Number of instrumented functions.
否则,您将被告知统计信息收集未启用:Statistics are disabled. Build with asserts or with-DLLVM_FORCE_ENABLE_STATS
位码文件hello_inst.bc是结果您可以使用llvm-dis工具将此文件转换为可读的IR正如预期的那样,您将看到对__ppp_enter()和__ppp_exit()函数的调用,以及一个用于函数名称的新常量:$ llvm-dis hello_inst.bc -o –@.str = private unnamed_addr constant [6 x i8] c"Hello\00", align 1@0 = private unnamed_addr constant [5 x i8] c"main\00", align 1define dso_local i32 @main(i32 noundef %0, ptr nocapture noundef readnone %1) { call void @__ppp_enter(ptr @0) %3 = tail call i32 @puts( ptr noundef nonnull dereferenceable(1) @.str) call void @__ppp_exit(ptr @0) ret i32 0}
这看起来已经很好了如果我们能将此IR转换为可执行文件并运行它,那将更好为此,您需要为被调用的函数提供实现通常,功能运行时支持比向编译器本身添加该功能要复杂得多在这种情况下也是如此当调用__ppp_enter()和__ppp_exit()函数时,您可以将其视为事件为了稍后分析数据,需要保存事件您希望获得的基本数据是事件类型、函数名称及其地址和时间戳如果不使用技巧,这并不像看起来那么容易让我们尝试一下创建一个名为runtime.c的文件,内容如下:您需要文件I/O、标准函数和时间支持这是由以下包含提供的:
#include <stdio.h>#include <stdlib.h>#include <time.h>
对于文件,需要一个文件描述符此外,当程序完成时,应正确关闭该文件描述符:static FILE FileFD = NULL;static void cleanup() { if (FileFD == NULL) { fclose(FileFD); FileFD = NULL; }}
为了简化运行时,只使用一个固定名称的输出如果文件未打开,则打开文件并注册清理函数:static void init() { if (FileFD == NULL) { FileFD = fopen("ppprofile.csv", "w"); atexit(&cleanup); }}
您可以调用clock_gettime()函数来获取时间戳CLOCK_PROCESS_CPUTIME_ID参数返回此进程消耗的时间请注意,并非所有系统都支持此参数如果需要,您可以使用其他时钟,如CLOCK_REALTIME:typedef unsigned long long Time;static Time get_time() { struct timespec ts; clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &ts); return 1000000000L ts.tv_sec + ts.tv_nsec;}
现在,很容易定义__ppp_enter()函数只需确保文件已打开,获取时间戳并写入事件:void __ppp_enter(const char FnName) { init(); Time T = get_time(); void Frame = __builtin_frame_address(1); fprintf(FileFD, // "enter|name|clock|frame" „enter|%s|%llu|%p\n", FnName, T, Frame);}
__ppp_exit()函数只在事件类型方面有所不同:void __ppp_exit(const char FnName) { init(); Time T = get_time(); void Frame = __builtin_frame_address(1); fprintf(FileFD, // "exit|name|clock|frame" „exit|%s|%llu|%p\n", FnName, T, Frame);}
这结束了一个非常简单的运行时支持实现在我们尝试之前,应该对实现说一些话,因为很明显有几个问题部分首先,由于只有一个文件描述符,且对其的访问未受保护,因此实现不是线程安全的尝试将此运行时实现与多线程程序一起使用,很可能会导致输出文件中的数据混乱此外,我们省略了检查I/O相关函数的返回值,这可能导致数据丢失但最重要的是,事件的时间戳不精确调用函数已经增加了开销,但在该函数中执行I/O操作会使情况变得更糟原则上,您可以匹配函数的进入和退出事件并计算函数的运行时间然而,这个值本质上是有缺陷的,因为它可能包括了I/O所需的时间总之,不要相信这里记录的时间尽管存在所有缺陷,但这个小运行时文件允许我们产生一些输出编译经过仪器化的文件的位码,以及包含运行时代码的文件,并运行生成的可执行文件:$ clang hello_inst.bc runtime.c$ ./a.out
这将在目录中产生一个名为ppprofile.csv的新文件,包含以下内容:$ cat ppprofile.csventer|main|3300868|0x1exit|main|3760638|0x1
酷 - 新通道和运行时似乎可以工作指定通道管道使用--passes选项,您不仅可以指定单个通道,还可以描述整个管道例如,优化级别2的默认管道名为default<O2>您可以使用--passes="ppprofile,default<O2>"参数在默认管道之前运行ppprofile通道请注意,管道描述中的通道名称必须为同一类型现在,让我们看看如何使用clang中的新通道将新通行证插入到clang中 在上一节中,您学习了如何使用opt运行单个通行证如果您需要调试一个通行证,这很有用,但对于一个真正的编译器来说,步骤不应该那么复杂为了获得最佳结果,编译器需要以一定的顺序运行优化通行证LLVM通行证管理器有一个默认的通行证执行顺序这也被称为默认通行证流水线使用opt,您可以通过--passes选项指定不同的通行证流水线这是灵活的,但对用户来说也很复杂同时,事实证明,大多数时候,您只想在非常特定的点添加一个新通行证,例如在运行优化通行证之前或循环优化过程结束时这些点被称为扩展点PassBuilder类允许您在扩展点注册一个通行证例如,您可以调用registerPipelineStartEPCallback()方法将通行证添加到优化流水线的开始这正是我们需要的ppprofiler通行证的地方在优化期间,函数可能会被内联,而通行证会错过这些内联函数相反,在优化通行证之前运行通行证保证了所有函数都被插入了要使用这种方法,您需要在通行证插件中扩展RegisterCB()函数将以下代码添加到函数中:
PB.registerPipelineStartEPCallback( [](ModulePassManager &PM, OptimizationLevel Level) { PM.addPass(PPProfilerIRPass()); });
每当通行证管理器填充默认通行证流水线时,它会调用所有扩展点的回调我们在这里简单地添加了新通行证要将插件加载到clang中,您可以使用-fpass-plugin选项现在,创建hello.c文件的插入式可执行文件几乎变得微不足道:$ clang -fpass-plugin=./PPProfiler.so hello.c runtime.c
请运行可执行文件并验证运行是否创建了ppprofiler.csv文件注意runtime.c文件没有被插入,因为通行证检查了特殊函数尚未在模块中声明这看起来已经好多了,但它能扩展到更大的程序吗?假设您想为第5章的tinylang编译器构建一个插入式二进制文件您会怎么做呢?您可以在CMake命令行上传递编译器和链接器标志,这正是我们需要的C++编译器的标志在CMAKE_CXX_FLAGS变量中给出因此,在CMake命令行上指定以下内容将把所有编译器运行添加新通行证:-DCMAKE_CXX_FLAGS="-fpass-plugin=<PluginPath>/PPProfiler.so"
请将<PluginPath>替换为共享库的绝对路径类似地,指定以下内容将把runtime.o文件添加到每个链接器调用中再次,请将<RuntimePath>替换为编译后的runtime.c的绝对路径:-DCMAKE_EXE_LINKER_FLAGS="<RuntimePath>/runtime.o"
当然,这需要clang作为构建编译器确保使用clang作为构建编译器的最快方法是相应地设置CC和CXX环境变量:export CC=clangexport CXX=clang++
有了这些额外的选项,第5章的CMake配置应该像往常一样运行构建tinylang可执行文件后,您可以使用示例Gcd.mod文件运行它ppprofile.csv文件也将被写入,这次有超过44,000行当然,拥有这样的数据集引发了一个问题,即您是否可以从中获得有用的信息例如,获取一个最常调用的10个函数的列表,以及函数的调用计数和在函数中花费的时间,将是有用的信息幸运的是,在Unix系统上,您有几个工具可以帮助让我们构建一个简短的管道,将进入事件与退出事件匹配,计算函数,并显示前10个函数awk Unix工具有助于完成这些步骤的大部分要将进入事件与退出事件匹配,必须将进入事件存储在记录关联映射中当匹配到退出事件时,查找存储的进入事件,并写入新记录发出的行包含进入事件的时间戳,退出事件的时间戳,以及两者之间的差异我们必须将此内容放入join.awk文件中:
BEGIN { FS = "|"; OFS = "|" }/enter/ { record[$2] = $0 }/exit/ { split(record[$2],val,"|") print val[2], val[3], $3, $3-val[3], val[4] }
为了计算函数调用和执行时间,使用了两个关联映射,count和sum在count中,计算函数调用,而在sum中,添加执行时间最后,转储映射您可以将此内容放入avg.awk文件中:BEGIN { FS = "|"; count[""] = 0; sum[""] = 0 }{ count[$1]++; sum[$1] += $4 }END { for (i in count) { if (i != "") { print count[i], sum[i], sum[i]/count[i], I }} }
运行这两个脚本后,结果可以按降序排序,然后从文件中取出前10行然而,我们仍然可以改进函数名称,__ppp_enter()和__ppp_exit(),它们是混淆的,因此难以阅读使用llvm-cxxfilt工具,名称可以被解混淆demangle.awk脚本如下:{ cmd = "llvm-cxxfilt " $4 (cmd) | getline name close(cmd); $4 = name; print }
要获取前10个函数调用,您可以运行以下命令:$ cat ppprofile.csv | awk -f join.awk | awk -f avg.awk | \ sort -nr | head -15 | awk -f demangle.awk
以下是输出的一些示例行:446 1545581 3465.43 charinfo::isASCII(char)409 826261 2020.2 llvm::StringRef::StringRef()382 899471 2354.64 tinylang::Token::is(tinylang::tok::TokenKind) const171 1561532 9131.77 charinfo::isIdentifierHead(char)
第一个数字是函数的调用计数,第二个是累积执行时间,第三个是平均执行时间如前所述,尽管不应信任时间值,但调用计数应该是准确的到目前为止,我们已经实现了一个新的插入式通行证,无论是作为插件还是作为LLVM的补充,并在一些现实世界的场景中使用了它在下一节中,我们将探讨如何在编译器中设置优化流水线将优化流水线添加到您的编译器中 我们在前几章中开发的tinylang编译器对IR代码执行不进行任何优化在接下来的几个小节中,我们将向编译器添加一个优化流水线以实现这一点创建一个优化流水线 PassBuilder类是设置优化流水线的核心这个类知道所有注册的通行证,并且可以根据文本描述构建一个通行证流水线我们可以使用这个类来创建从命令行给定的描述的通行证流水线,或者根据请求的优化级别使用默认流水线我们还支持使用通行证插件,例如我们在上一节中讨论的ppprofiler通行证插件通过这种方式,我们可以模仿opt工具的部分功能,并且还可以为命令行选项使用类似的名称PassBuilder类填充了一个ModulePassManager类的实例,该类是持有构建的通行证流水线并运行它的通行证管理器代码生成通行证仍然使用旧的通行证管理器因此,我们必须为此目的保留旧的通行证管理器对于实现,我们将扩展我们的tinylang编译器的tools/driver/Driver.cpp文件:我们将使用新类,因此我们将从添加新包含文件开始llvm/Passes/PassBuilder.h文件定义了PassBuilder类llvm/Passes/PassPlugin.h文件是插件支持所需的最后,llvm/Analysis/TargetTransformInfo.h文件提供了一个通行证,该通行证将IR级转换与特定于目标的信息连接起来:#include "llvm/Passes/PassBuilder.h"#include "llvm/Passes/PassPlugin.h"#include "llvm/Analysis/TargetTransformInfo.h"
为了使用新通行证管理器的某些功能,我们必须添加三个命令行选项,使用与opt工具相同的名称--passes选项允许文本指定通行证流水线,而--load-pass-plugin选项允许使用通行证插件如果给出了--debug-pass-manager选项,那么通行证管理器会打印出有关执行的通行证的信息:static cl::opt<bool> DebugPM("debug-pass-manager", cl::Hidden, cl::desc("打印PM调试信息"));static cl::opt<std::string> PassPipeline( "passes", cl::desc("通行证流水线的描述"));static cl::list<std::string> PassPlugins( "load-pass-plugin", cl::desc("从插件库加载通行证"));
用户通过优化级别影响通行证流水线的构建PassBuilder类支持六种不同的优化级别:无优化、三个速度优化级别和两个减小尺寸的级别我们可以用一个命令行选项捕获所有级别:static cl::opt<signed char> OptLevel( cl::desc("设置优化级别:"), cl::ZeroOrMore, cl::values( clEnumValN(3, "O", "相当于-O3"), clEnumValN(0, "O0", "优化级别0"), clEnumValN(1, "O1", "优化级别1"), clEnumValN(2, "O2", "优化级别2"), clEnumValN(3, "O3", "优化级别3"), clEnumValN(-1, "Os", "像-O2一样,但有额外的大小优化"), clEnumValN( -2, "Oz", "像-Os一样,但进一步减少代码大小")), cl::init(0));
LLVM的插件机制支持静态链接插件的插件注册表,该注册表在项目配置期间创建为了使用这个注册表,我们必须包含llvm/Support/Extension.def数据库文件,以创建返回插件信息的函数的原型:#define HANDLE_EXTENSION(Ext) \ llvm::PassPluginLibraryInfo get##Ext##PluginInfo();#include "llvm/Support/Extension.def"
现在,我们必须用新版本替换现有的emit()函数此外,我们还必须在函数顶部声明所需的PassBuilder实例:bool emit(StringRef Argv0, llvm::Module M, llvm::TargetMachine TM, StringRef InputFilename) { PassBuilder PB(TM); // 实现对命令行指定的通行证插件的支持 for (auto &PluginFN : PassPlugins) { auto PassPlugin = PassPlugin::Load(PluginFN); if (!PassPlugin) { WithColor::error(errs(), Argv0) << "无法从'" << PluginFN << "'加载通行证请求被忽略\n"; continue; } PassPlugin->registerPassBuilderCallbacks(PB); } // 使用静态插件注册表中的信息以类似的方式注册这些插件 #define HANDLE_EXTENSION(Ext) \ get##Ext##PluginInfo().RegisterPassBuilderCallbacks(PB); #include "llvm/Support/Extension.def" // 声明不同分析管理器的变量唯一的参数是调试标志 LoopAnalysisManager LAM(DebugPM); FunctionAnalysisManager FAM(DebugPM); CGSCCAnalysisManager CGAM(DebugPM); ModuleAnalysisManager MAM(DebugPM); // 通过PassBuilder实例上的相应register方法调用来填充分析管理器 FAM.registerPass( [&](] { return PB.buildDefaultAAPipeline(); }); PB.registerModuleAnalyses(MAM); PB.registerCGSCCAnalyses(CGAM); PB.registerFunctionAnalyses(FAM); PB.registerLoopAnalyses(LAM); PB.crossRegisterProxies(LAM, FAM, CGAM, MAM); // MPM模块通行证管理器保存我们构建的通行证流水线 ModulePassManager MPM(DebugPM); // 我们需要实现两种不同的方法来填充模块通行证管理器的通行证流水线 if (!PassPipeline.empty()) { if (auto Err = PB.parsePassPipeline( MPM, PassPipeline)) { WithColor::error(errs(), Argv0) << toString(std::move(Err)) << "\n"; return false; } } else { // 使用所选的优化级别确定要构建的通行证流水线 // ...(省略部分代码) } // 建立IR代码上运行转换的通行证流水线后,我们需要一个打开的文件来写入结果 // ...(省略部分代码) // 对于代码生成过程,我们必须使用旧的通行证管理器 legacy::PassManager CodeGenPM; CodeGenPM.add(createTargetTransformInfoWrapperPass( TM->getTargetIRAnalysis())); // 输出LLVM IR,我们必须添加一个将IR打印到流中的通行证 // ...(省略部分代码) // 准备就绪后,我们现在准备执行通行证 MPM.run(M, MAM); CodeGenPM.run(M); Out->keep(); return true;}
这是一个代码量很大的过程,但过程是直接的当然,我们还必须更新tools/driver/CMakeLists.txt构建文件中的依赖项除了添加目标组件,我们还必须添加LLVM的所有转换和代码生成组件名称大致类似于源代码所在的目录名称组件名称在配置过程中转换为链接库名称:set(LLVM_LINK_COMPONENTS ${LLVM_TARGETS_TO_BUILD} AggressiveInstCombine Analysis AsmParser BitWriter CodeGen Core Coroutines IPO IRReader InstCombine Instrumentation MC ObjCARCOpts Remarks ScalarOpts Support Target TransformUtils Vectorize Passes)
我们的编译器驱动支持插件,我们必须宣布这种支持:add_tinylang_tool(tinylang Driver.cpp SUPPORT_PLUGINS)
和以前一样,我们必须链接我们自己的库:target_link_libraries(tinylang PRIVATE tinylangBasic tinylangCodeGen tinylangLexer tinylangParser tinylangSema)
这些是对源代码和构建系统的必要添加要构建扩展的编译器,您必须转到构建目录并输入以下命令:$ ninja
构建系统文件的更改会自动检测到,在编译和链接我们更改的源代码之前会运行cmake如果您需要重新运行配置步骤,请按照第1章“安装LLVM”中的“编译tinylang应用程序”部分的说明操作由于我们使用opt工具的选项作为蓝图,您应该尝试使用选项运行tinylang来加载通行证插件并运行通行证,就像我们在前几节中所做的那样通过当前实现,我们可以运行默认的通行证流水线,或者我们可以自己构建一个后者非常灵活,但在几乎所有情况下,这将是小题大做默认流水线对C类语言运行得非常好然而,缺少的是如何扩展通行证流水线我们将在下一节中探讨如何实现这一点扩展通行证流水线在上一节中,我们使用PassBuilder类创建了一个通行证流水线,无论是从用户提供的描述还是一个预定义的名称现在,让我们看看另一种定制通行证流水线的方法:使用扩展点在构建通行证流水线的过程中,通行证构建器允许添加用户贡献的通行证这些地方被称为扩展点存在几个扩展点,如下:流水线开始扩展点,允许我们在流水线开始处添加通行证窥孔扩展点,允许我们在指令组合器通行证的每个实例之后添加通行证也存在其他扩展点要使用扩展点,您必须注册一个回调在构建通行证流水线期间,您的回调在定义的扩展点处运行,并且可以向给定的通行证管理器添加通行证要为流水线开始扩展点注册回调,您必须调用PassBuilder类的registerPipelineStartEPCallback()方法例如,要将我们的PPProfiler通行证添加到流水线的开头,您将适应通行证,使其成为一个模块通行证,并使用createModuleToFunctionPassAdaptor()模板函数进行调用,然后将通行证添加到模块通行证管理器:PB.registerPipelineStartEPCallback( [](ModulePassManager &MPM) { MPM.addPass(PPProfilerIRPass()); });
您可以在任何管线创建之前的通行证流水线设置代码中添加此代码片段 - 即在调用parsePassPipeline()方法之前我们在上一节中所做的一个非常自然的扩展是让用户在命令行上传递一个扩展点的流水线描述opt工具也允许这样做让我们为流水线开始扩展点这样做将以下代码添加到tools/driver/Driver.cpp文件中:首先,我们必须为用户指定流水线描述添加一个新的命令行再次,我们从opt工具中获取选项名称:static cl::opt<std::string> PipelineStartEPPipeline( "passes-ep-pipeline-start", cl::desc("流水线开始扩展点"));
使用Lambda函数作为回调是最方便的方式要解析流水线描述,我们必须调用PassBuilder实例的parsePassPipeline()方法通行证被添加到PM通行证管理器中,并作为参数传递给Lambda函数如果发生错误,我们只打印错误消息而不停止应用程序您可以在调用crossRegisterProxies()方法后添加此代码片段:PB.registerPipelineStartEPCallback( [&PB, Argv0](ModulePassManager &PM) { if (auto Err = PB.parsePassPipeline( PM, PipelineStartEPPipeline)) { WithColor::error(errs(), Argv0) << "无法解析流水线 " << PipelineStartEPPipeline.ArgStr << ": " << toString(std::move(Err)) << "\n"; } });
提示要允许用户在每个扩展点添加通行证,您需要为每个扩展点添加上述代码片段现在是一个尝试不同的通行证管理器选项的好时机使用--debug-pass-manager选项,您可以跟踪执行的通行证的顺序您也可以使用--print-before-all和--print-after-all选项在每个通行证之前或之后打印IR如果您创建了自己的通行证流水线,那么您可以在感兴趣的点插入打印通行证例如,尝试使用--passes="print,inline,print"选项此外,为了确定哪个通行证更改了IR代码,您可以使用--print-changed选项,它只会在IR代码与之前的通行证结果相比发生变化时才打印IR大大减少的输出使得跟踪IR转换变得更加容易PassBuilder类有一个嵌套的OptimizationLevel类来表示六个不同的优化级别我们不仅可以使用"default<O?>"流水线描述作为parsePassPipeline()方法的参数,也可以调用buildPerModuleDefaultPipeline()方法,它构建了请求级别的默认优化流水线 - 除了O0级别这个优化级别意味着不执行优化因此,没有向通行证管理器添加任何通行证如果我们仍然想运行某个特定的通行证,那么我们可以手动将其添加到通行证管理器中在这一点上运行一个简单的通行证是AlwaysInliner通行证,它将标记有always_inline属性的函数内联到调用者中在将命令行选项值转换为OptimizationLevel类对应的成员之后,我们可以这样实现:PassBuilder::OptimizationLevel Olevel = …;if (OLevel == PassBuilder::OptimizationLevel::O0) MPM.addPass(AlwaysInlinerPass());else MPM = PB.buildPerModuleDefaultPipeline(OLevel, DebugPM);
当然,以这种方式向通行证管理器添加一个以上的通行证是可能的当构建通行证流水线时,PassBuilder也使用addPass()方法运行扩展点回调由于流水线没有为优化级别O0填充,因此注册的扩展点没有被调用如果您使用扩展点注册应该也在O0级别上运行的通行证,这将是个问题您可以调用runRegisteredEPCallbacks()方法来运行注册的扩展点回调,结果是一个只填充了通过扩展点注册的通行证的通行证管理器通过向tinylang添加优化流水线,您创建了一个类似于clang的优化编译器LLVM社区在每个版本中都在努力改进优化和优化流水线由于这个原因,很少使用默认流水线大多数情况下,新通行证被添加以实现编程语言的特定语义总结在本章中,您学习了如何为LLVM创建一个新的通行证您使用通行证流水线描述和一个扩展点运行了通行证您通过构建和执行类似于clang的通行证流水线来扩展了您的编译器,将tinylang变成了一个优化编译器通行证流水线允许在扩展点添加通行证,您了解了如何注册这些点的通行证这使您能够使用您开发的通行证或现有通行证扩展优化流水线在下一章中,您将学习TableGen语言的基础知识,该语言在LLVM和clang中广泛使用,大大减少了手动编程(图片来源网络,侵删)
0 评论