c++项目如何有效缩短编译时间? (预编译头与Unity Build)

13次阅读

预编译头(PCH)真正加速需满足:头文件稳定且被大量共用;#include “stdafx.h” 必须为首个非注释行;仅放入不变、全局、重型标准头;MSVC下用/showincludes验证是否生效;Clang/GCC不推荐依赖PCH。

c++项目如何有效缩短编译时间? (预编译头与Unity Build)

预编译头(PCH)怎么配才真正加速?

预编译头只有在头文件稳定、被大量源文件共用时才有效;盲目启用反而拖慢增量编译。关键不是“用了没”,而是“用对了没”。

  • #include 顺序必须严格:所有源文件中,#include "stdafx.h"(或你命名的 PCH 头)必须是第一个非注释行,否则 MSVC / Clang-cl 会直接放弃使用 PCH
  • 只把**不变的、全局的、重型的**头放进 PCH: 可以,"config.h" 或含宏定义的头要谨慎——一旦修改,整个 PCH 重编,所有依赖它的 .cpp 全部重编
  • MSVC 下检查是否生效:编译时加 /showIncludes,看输出里是否出现 Note: including file: ...stdafx.h 前有 ... precompiled header skipped 就说明没用上
  • Clang 和 GCC 不原生支持 PCH 加速多文件编译(GCC 的 .gch 是 per-file 的),实际项目中建议只在 MSVC 环境下投入 PCH 优化

unity Build(Jumbo Build)有哪些隐藏代价?

Unity Build 把多个 .cpp 合并为一个大 TU 编译,能显著减少模板实例化和头文件重复解析,但会破坏增量编译粒度,且容易暴露隐式依赖问题。

  • 合并策略必须可控:不要全项目开启。推荐按模块分组,例如 core/ 下的 12 个 .cpp 合成一个 core_unity.cpp,而不是把 renderer/network/ 强塞一起
  • 每个 Unity 文件顶部必须显式包含它所依赖的所有头,不能靠“前面某个 .cpp 已经 include 过”——因为合并后顺序不确定,#include 被挪到最前也不保险
  • 调试信息错位:GDB / LLDB 中断点可能跳转到错误的原始文件行号;MSVC 的 PDB 通常能保持映射,但需确认构建时开了 /Zi 且没禁用 /FS
  • CI 构建机内存压力陡增:一个 500MB 的 Unity TU 可能吃掉 2GB+ RAM;若机器只有 4 核 8GB,开 4 个并行 Unity 编译大概率 OOM

预编译头 + Unity Build 能叠加使用吗?

可以,但仅限 MSVC,且必须严格分层:Unity 文件本身要 #include PCH 头,而 PCH 内不能引用任何 Unity 文件中定义的符号(比如内联函数、Static 变量)。

// unity_core.cpp #include "stdafx.h"  // ← 必须第一行,且 stdafx.h 已预编译  #include "a.cpp" #include "b.cpp" #include "c.cpp"
  • 不能让 stdafx.h 包含 "core_unity.cpp" 或任何生成的 Unity 源文件路径
  • 如果 Unity 文件里用了 __declspec(dllexport) 或模块接口c++20 Modules),PCH 里不能有相关声明,否则链接阶段报 LNK2019:unresolved external symbol
  • 实践中更稳妥的做法是:PCH 服务常规编译,Unity Build 单独走一套不依赖 PCH 的构建逻辑(即 Unity 文件自己完整 include 所需头),避免耦合失效

比 PCH 和 Unity 更值得优先做的三件事

很多团队花两周调 PCH,却忽略更立竿见影的改进。真实项目中,以下三项往往带来 30%+ 编译时间下降,且无副作用:

立即学习C++免费学习笔记(深入)”;

  • #include 替代 #include "xxx.h":让编译器跳过当前目录查找,尤其在大型项目有深嵌套 include 路径时效果明显
  • 删除未使用的 #include:用 clang++ -MJ 生成 jsON 编译数据库,再配合 include-what-you-use(IWYU)自动检测冗余头
  • 把频繁变更的配置头(如 build_config.h)从公共头中剥离,改用 -D 宏传递给编译器,避免一次修改触发数百个 .cpp 重编

Unity 和 PCH 是“高阶技巧”,但它们解决的是“如何更快地做重复劳动”;而删冗余头、减路径查找、拆配置头,是在直接减少劳动本身。后者见效快、风险低、可自动化验证——别急着编译黑科技,先打开 compile_commands.json 看看你的项目里有多少 #include "base/String_util.h" 其实根本没用到 string_util.h 里的任何东西。

text=ZqhQzanResources