教程LLVMIR(通道通行证函数流水线插件)「通道数据」

LLVM使用一系列通道来优化IR
一个通道对IR的单元(如函数或模块)进行操作
该操作可以是转换,以定义的方式改变IR,或者是分析,收集依赖关系等信息
这个通道系列称为通道管道
通道管理器在IR上执行通道管道,这是我们的编译器产生的
因此,您需要知道通道管理器的作用以及如何构建通道管道
如果编程语言的语义需要开发新的通道,我们必须将这些通道添加到管道中
在本章中,您将了解以下内容:如何利用LLVM通道管理器在LLVM中实现通道如何在LLVM项目中实现一个仪器通道,以及作为一个单独的插件通过使用LLVM工具中的ppprofiler通道,您将学习如何使用opt和clang的通道插件在为您的编译器添加优化管道时,您将基于新的通道管理器扩展tinylang编译器的优化管道到本章结束时,您将知道如何开发一个新的通道以及如何将其添加到通道管道中
您还将能够在您的编译器中设置通道管道
技术要求 本章的源代码可在 https://github.com/PacktPublishing/Learn-LLVM-17/tree/main/Chapter07 找到
LLVM通道管理器 LLVM核心库优化您的编译器创建的IR,并将其转化为目标代码
这个巨大的任务被分解为称为通道的单独步骤
这些通道需要按正确的顺序执行,这是通道管理器的目标
为什么不硬编码通道的顺序呢?您的编译器的用户通常期望您的编译器提供不同级别的优化
开发者在开发期间更喜欢快速编译速度而不是优化
最终应用程序的运行速度应尽可能快,您的编译器应能够执行复杂的优化,接受更长的编译时间
不同的优化级别意味着需要执行的不同数量的优化通道
因此,作为编译器编写者,您可能希望提供自己的通道,以利用您对源语言的了解
例如,您可能希望用内联IR或甚至用预计算的结果替换众所周知的库函数
对于C,这样的通道是LLVM库的一部分,但对于其他语言,您需要自己提供
在引入自己的通道后,您可能需要重新排序或添加一些通道
例如,如果您知道您的通道的操作使一些IR代码无法到达,那么您希望在您的通道之后额外运行死代码移除通道
通道管理器有助于组织这些要求
通道通常按其工作范围分类:模块通道以整个模块作为输入
这样的通道在给定的模块上执行其工作,并且可以用于此模块内的程序内操作
调用图通道在调用图的强连通分量(SCCs)上操作
它以自底向上的顺序遍历分量
函数通道以单个函数作为输入,并仅在该函数上执行其工作
循环通道在函数内的循环上工作
除了IR代码,通道还可能需要、更新或使某些分析结果失效
进行了许多不同的分析,例如别名分析或构造支配树
如果通道需要这样的分析,那么它可以从分析管理器请求它
如果信息已经被计算,那么将返回缓存的结果
否则,将计算信息
如果通道更改了IR代码,那么它需要宣布哪些分析结果被保留,以便在必要时使缓存的分析信息失效
在幕后,通道管理器确保以下内容:分析结果在通道之间共享
这需要跟踪哪个通道需要哪个分析以及每个分析的状态
目标是避免不必要的预先计算分析,并尽快释放由分析结果持有的内存
通道以管道方式执行
例如,如果应该顺序执行几个函数通道,那么通道管理器将在第一个函数上运行这些函数通道中的每一个
然后,它将在第二个函数上运行所有函数通道,依此类推
这里的基本思想是改善缓存行为,因为编译器只对有限的数据集(一个IR函数)执行转换,然后继续下一个有限的数据集
让我们实现一个新的IR转换通道,并探索如何将其添加到优化管道中
实现一个新的通道 一个通道可以对LLVM IR执行任意复杂的转换
为了说明添加一个新通道的机制,我们添加一个执行简单仪器的通道
为了研究程序的性能,了解函数被调用的频率以及它们的运行时间是很有趣的
收集这些数据的一种方法是将计数器插入每个函数中
这个过程称为仪器
我们将编写一个简单的仪器通道,该通道在每个函数的入口和每个出口点插入一个特殊函数调用
这些函数收集计时信息并将其写入文件
结果,我们可以创建一个非常基本的分析器,我们将命名为穷人的分析器,或简称为ppprofiler
我们将开发新的通道,使其可以作为独立插件使用,也可以作为插件添加到LLVM源树中
之后,我们将看看LLVM自带的通道是如何集成到框架中的
作为插件开发ppprofiler通道 在本节中,我们将查看在LLVM树外创建新通道作为插件
新通道的目标是在函数的入口处插入对__ppp_enter()函数的调用,并在每个返回指令之前插入对__ppp_exit()函数的调用
只将当前函数的名称作为参数传递
这些函数的实现可以计算调用次数并测量经过的时间
我们将在本章末尾实现此运行时支持
我们将研究如何开发通道
我们将源代码存储在PPProfiler.cpp文件中
按照这些步骤:首先,让我们包含一些文件:#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 installCMake将检测到构建描述的更改,并重新运行配置步骤
您将看到一个额外的行:-- 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关键字下,您需要添加一行,包含新组件的名称: PPProfilerEt 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中广泛使用,大大减少了手动编程
教程LLVMIR(通道通行证函数流水线插件)
(图片来源网络,侵删)

联系我们

在线咨询:点击这里给我发消息