You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

开发基于Clang的工具时如何提升编译速度

基于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 s14 s2.5 s342 MB
g++ -O26.9 s14 s2.5 s345 MB
g++ -g8.6 s18 s4.1 s472 MB
g++ -g -O28.9 s18 s4.3 s478 MB

Clang+LLVM 14.0.0 测试结果

按照建议换用clang作为编译器测试,结果如下:

编译选项无PCH编译耗时生成PCH耗时使用PCH编译耗时PCH文件大小
clang5.6 s5.1 s2.3 s62 MB
clang -O25.6 s5.2 s2.5 s62 MB
clang -g5.6 s5.2 s2.4 s62 MB
clang -g -O25.7 s5.1 s2.6 s62 MB

这个结果相比g++确实有提升,是我后续可能采用的方案,但仍然没有达到亚秒级的目标。
补充测试说明:我也测试了GCC 12.1,所有测试项的耗时都比GCC 9.3高约2%。

测试环境配置

  • 使用官方发布的clang+llvm-14.0.0 x86_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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 23:09:18