开发基于Clang的工具时如何提升编译速度
我正在开发基于ClangTool的Clang生态工具,但单源文件重编译耗时远超预期:仅引入最基础必要头文件的极简示例程序单文件编译就需要约7秒,引入AST matchers后编译耗时直接攀升到20秒以上,对开发流程效率阻碍极大,在学习新API、需要频繁修正语法错误的阶段影响尤其明显。
我之前参考过2015年EuroLLVM会议上Tilmann Scheller分享的《高效构建Clang/LLVM》幻灯片内容,其中提到换用gcc编译产出的clang作为编译期编译器,可以获得不到2倍的性能提升。因为我当前场景下链接速度尚可,暂时不考虑链接阶段的优化方案。
我也尝试过使用GCC预编译头(PCH)方案,但效果达不到预期:虽然PCH可以把编译耗时缩短约一半,但首次生成PCH的耗时是普通编译的两倍以上,生成的PCH文件体积高达300-500MB,而且使用流程非常繁琐——需要调整整个项目的#include结构、管理额外的依赖和文件,还要仔细监控流程保证PCH被正确命中。
我的理想目标是把编译耗时压缩到1秒以内,毕竟我的源文件仅100行左右。我清楚预处理器展开后代码量约20万行,其中template关键字出现超过5000次,编译器实际处理的工作量远大于表面代码量,但我自己编写的代码直接用到的内容只占其中极小一部分。
作为对比,同等复杂度的脚本用Python绑定运行仅需约0.05秒。我知道Clang提供了C语言绑定的libclang,但我不想用C语言开发工具;而且libclang(包括C和Python版本绑定)只暴露了C++ API中很小一部分信息——最开始遇到这个问题时我花了几个月尝试libclang方案,最终因为信息缺失无法满足需求放弃,比如获取二元表达式运算符类型这类需求,即便部分问题现在已有解决方案,仍然存在大量缺失的接口。
编译耗时测试数据
所有耗时数据均取5次测量的中位数。
GCC 9.3 测试结果
我最关注g++ -g配置下的表现,测试结果如下:
| 编译选项 | 无PCH编译耗时 | 生成PCH耗时 | 使用PCH编译耗时 | PCH文件大小 |
|---|---|---|---|---|
g++ | 6.8 s | 14 s | 2.5 s | 342 MB |
g++ -O2 | 6.9 s | 14 s | 2.5 s | 345 MB |
g++ -g | 8.6 s | 18 s | 4.1 s | 472 MB |
g++ -g -O2 | 8.9 s | 18 s | 4.3 s | 478 MB |
Clang+LLVM 14.0.0 测试结果
按照建议换用clang作为编译器测试,结果如下:
| 编译选项 | 无PCH编译耗时 | 生成PCH耗时 | 使用PCH编译耗时 | PCH文件大小 |
|---|---|---|---|---|
clang | 5.6 s | 5.1 s | 2.3 s | 62 MB |
clang -O2 | 5.6 s | 5.2 s | 2.5 s | 62 MB |
clang -g | 5.6 s | 5.2 s | 2.4 s | 62 MB |
clang -g -O2 | 5.7 s | 5.1 s | 2.6 s | 62 MB |
这个结果相比g++确实有提升,是我后续可能采用的方案,但仍然没有达到亚秒级的目标。
补充测试说明:我也测试了GCC 12.1,所有测试项的耗时都比GCC 9.3高约2%。
测试环境配置
- 使用官方发布的
clang+llvm-14.0.0x86_64 Linux二进制发行版 - 系统为x86_64架构Linux Mint 20.1,编译用GCC版本为9.3.0
- CPU为3.5GHz四核Intel i5-6600K
- 内存8GB
- Linux系统运行在VMware Workstation 12虚拟机中(虚拟机不是性能瓶颈:很多C++项目在该虚拟机中的编译速度很快,甚至快于Windows宿主机的编译速度)
- 宿主机为Windows 10,物理内存32GB
测试用源文件与编译命令
待编译的极简示例文件为print-tu.cc,功能为打印翻译单元内容,代码如下:
// print-tu.cc // Print contents of a Translation Unit. // clang #include "clang/AST/ASTConsumer.h" // clang::ASTConsumer #include "clang/AST/ASTContext.h" // clang::ASTContext #include "clang/AST/DeclBase.h" // clang::Decl #include "clang/AST/DeclGroup.h" // clang::DeclGroupRef #include "clang/Basic/SourceLocation.h" // clang::SourceLocation #include "clang/Frontend/CompilerInstance.h" // clang::CompilerInstance #include "clang/Frontend/CompilerInvocation.h" // clang::CompilerInvocation #include "clang/Frontend/FrontendAction.h" // clang::FrontendAction, clang::ASTFrontendAction #include "clang/Tooling/CommonOptionsParser.h" // clang::tooling::CommonOptionsParser #include "clang/Tooling/Tooling.h" // clang::tooling::ClangTool, clang::tooling::newFrontendActionFactory // llvm #include "llvm/ADT/StringRef.h" // llvm::StringRef #include "llvm/Support/CommandLine.h" // llvm::cl::extrahelp // libc++ #include <iostream> // std::cout, etc. // libc #include <assert.h> // assert using clang::tooling::CommonOptionsParser; using clang::tooling::ClangTool; using clang::tooling::newFrontendActionFactory; using clang::ASTConsumer; using clang::ASTContext; using clang::ASTFrontendAction; using clang::CompilerInstance; using clang::CompilerInvocation; using clang::Decl; using clang::DeclGroupRef; using clang::DiagnosticConsumer; using clang::FileManager; using clang::FrontendAction; using clang::PCHContainerOperations; using clang::SourceLocation; using clang::TargetOptions; using llvm::StringRef; using std::cout; // Apply a custom category to all command-line options so that they are the // only ones displayed. static llvm::cl::OptionCategory MyToolCategory("my-tool options"); // CommonOptionsParser declares HelpMessage with a description of the common // command-line options related to the compilation database and input files. // It's nice to have this help message in all tools. static llvm::cl::extrahelp CommonHelp(CommonOptionsParser::HelpMessage); // A help message for this specific tool can be added afterwards. static llvm::cl::extrahelp MoreHelp("\nMore help text...\n"); // Implement ASTConsumer class MyASTConsumer : public ASTConsumer { public: // data // The context for the TU, as established by 'Initialize'. ASTContext *m_astContext; public: // methods MyASTConsumer() : m_astContext(NULL) {} // This is called at the start of the TU. virtual void Initialize(ASTContext &ctx) override { m_astContext = &ctx; } // This is called at the end of the TU. virtual void HandleTranslationUnit(ASTContext &ctx) override { cout << "in HandleTranslationUnit\n"; assert(m_astContext == &ctx); } virtual bool HandleTopLevelDecl(DeclGroupRef declGroup) override { cout << "in HandleTopLevelDecl\n"; assert(m_astContext); for (Decl const *decl : declGroup) { SourceLocation loc = decl->getLocation(); cout << " decl at " << loc.printToString(m_astContext->getSourceManager()) << '\n'; cout << " kind name: " << decl->getDeclKindName() << '\n'; } return true; } }; // Implement the FrontendAction interface. // // Inheriting from ASTFrontendAction provides definitions for // 'ExecuteAction' and 'usesPreprocessorOnly'. class MyFrontendAction : public ASTFrontendAction { public: virtual std::unique_ptr<ASTConsumer> CreateASTConsumer( CompilerInstance &ci, StringRef inFile) override { cout << "in CreateASTConsumer\n"; TargetOptions const &targetOptions = ci.getTargetOpts(); cout << " target options triple: " << targetOptions.Triple << '\n'; cout << " inFile: " << inFile.str() << '\n'; return std::unique_ptr<ASTConsumer>(new MyASTConsumer()); } }; int main(int argc, const char **argv) { auto ExpectedParser = CommonOptionsParser::create(argc, argv, MyToolCategory); if (!ExpectedParser) { // Fail gracefully for unsupported options. llvm::errs() << ExpectedParser.takeError(); return 2; } CommonOptionsParser& OptionsParser = ExpectedParser.get(); ClangTool Tool(OptionsParser.getCompilations(), OptionsParser.getSourcePathList()); // Arrange to run 'MyFrontendAction' on each TU. return Tool.run(newFrontendActionFactory<MyFrontendAction>().get()); } // EOF
无-g、-O2参数的基础编译命令为:
g++ -c -o print-tu.o print-tu.cc $(llvm-config --cxxflags)
内容的提问来源于stack exchange,提问作者Scott McPeak

