吐槽一下我们底层核心通信库的编译速度。项目本身有几十万行 C++ 代码,每次只要动一个底层的公共结构体,整个增量编译就直接失效,CI/CD 跑一次要足足等十五分钟,简直就是带薪摸鱼的绝佳借口。但天天这么折腾,研发效率实在顶不住。为了解决编译耗时过长的大坑,我们对 CMake 构建脚本进行了全面重构,主要通过引入 ccache 编译缓存和预编译头文件(Precompiled Headers, PCH)这两个大招来完成极限加速。
首先是 ccache,它的核心原理非常直接:拦截编译器命令,把编译好的 .o 目标文件缓存起来。只要源文件内容、编译选项和编译器版本没变,就直接拿缓存拼装,根本不走真正的编译流程。这对于 CI/CD 机器上频繁的 clean build 简直是救命稻草。
在 CMake 中配置 ccache 其实非常简单,不需要在系统里搞各种软链接污染环境变量。现代 CMake(3.4+)提供了 CMAKE_CXX_COMPILER_LAUNCHER 变量,我们可以直接在 CMakeLists.txt 里声明。
接着是预编译头文件。在 C++ 里,每次 #include 一个头文件,编译器都会把它展开并解析一遍。如果每个 .cpp 都引入了 <vector>、<string>、<unordered_map> 以及像 boost 或 nlohmann/json 这种超级庞大的模板库,编译器的前端解析开销会非常恐怖。通过预编译头文件,编译器只会编译一次这些不经常变动的头文件,并保存为一个二进制状态,后续编译其他源文件时直接秒读。
下面是我们重构后的核心 CMakeLists.txt 配置片段:
cmake_minimum_required(VERSION 3.16)
project(EngineCore CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 自动寻找并配置 ccache
find_program(CCACHE_PROGRAM ccache)
if(CCACHE_PROGRAM)
message(STATUS "Found ccache: ${CCACHE_PROGRAM}")
# 设置 C 和 CXX 的编译器启动器
set(CMAKE_C_COMPILER_LAUNCHER ${CCACHE_PROGRAM})
set(CMAKE_CXX_COMPILER_LAUNCHER ${CCACHE_PROGRAM})
else()
message(WARNING "ccache not found! Build times will not be cached.")
endif()
# 核心动态库目标
add_library(EngineCore SHARED
src/core.cpp
src/network.cpp
src/protocol.cpp
src/serializer.cpp
src/utils.cpp
)
target_include_directories(EngineCore PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/include
)
# 开启预编译头文件加速(CMake 3.16+ 官方原生支持)
# 我们把不常改动的标准库和第三方库头文件塞进去
target_precompile_headers(EngineCore PRIVATE
<iostream>
<vector>
<string>
<memory>
<unordered_map>
<algorithm>
<functional>
<thread>
<chrono>
"include/thirdparty/nlohmann/json.hpp"
)
# 如果是外部目标要使用我们,也可以指定预编译头文件的传递行为
# 这里演示如何单独为某个文件取消 PCH(例如某些会产生冲突的遗留代码)
set_source_files_properties(src/legacy_code.cpp PROPERTIES
SKIP_PRECOMPILE_HEADERS ON
)
引入这套配置后,本地二次编译几乎是秒开。CI 系统里通过将 ~/.ccache 目录加入缓存列表,冷启动编译时间从 14 分钟骤降到 2.5 分钟左右,编译效率提升了将近 5 倍,心智负担小了太多。
不过使用 PCH 也有个潜在的弊端:它会隐式地往你的源文件里注入那些预编译的头文件。如果哪天你想把某个 .cpp 拆到另一个没有配置 PCH 的子模块里,会突然暴出一堆 undefined 错误,因为你可能漏写了基础头文件的 #include。所以,建议团队成员即使在享受 PCH 加速时,依然在源文件里老老实实写上自己所需的 #include。
大家在大型 C++ 项目中,还有没有配合使用过其他更激进的编译优化手段,比如 Distcc 分布式编译或者 Gold/LLD 链接器优化?效果如何?