编译加速:ccache集成、并行构建、预编译头文件
说实话,嵌入式项目的编译速度,一直是让我头疼的问题。早期我做一个STM32项目,每次改一行代码,等编译完成要喝杯咖啡。后来项目规模越来越大,一个全量编译动辄十几分钟,甚至半小时。这谁受得了?
今天我们就来聊聊怎么给CMake项目装上「加速器」。我总结了三板斧:ccache缓存、并行构建、预编译头文件。这三招用好了,编译时间能砍掉一半以上。
核心观点:编译加速的本质是「避免重复劳动」。ccache缓存的是编译结果,并行构建是同时干多份活,预编译头文件是把公共部分提前准备好。三者不冲突,可以叠加使用。
一、ccache:让重复编译不再发生
ccache 是什么?说白了就是一个编译缓存工具。它会把每次编译的结果存起来,下次编译同样的源文件时,如果输入没变,就直接从缓存里拿结果。我习惯把它叫做「编译界的备忘录」。
我在一个大型物联网项目里用过ccache。那个项目有300多个源文件,每次全量编译要8分钟。集成ccache后,第二次编译直接降到2分钟。你想想看,一天要编译几十次,省下来的时间相当可观。
1.1 在CMake中集成ccache
集成方式很简单,我一般这样写:
# 查找ccache
find_program(CCACHE_PROGRAM ccache)
if(CCACHE_PROGRAM)
message(STATUS "Found ccache: ${CCACHE_PROGRAM}")
set(CMAKE_C_COMPILER_LAUNCHER "${CCACHE_PROGRAM}")
set(CMAKE_CXX_COMPILER_LAUNCHER "${CCACHE_PROGRAM}")
endif()
这段代码放在 CMakeLists.txt 的开头就行。它会先找系统里有没有 ccache,找到了就把它包装在编译器外面。
小技巧:我建议把这段代码放到一个单独的 cmake/ccache.cmake 文件中,然后用 include() 引入。这样不同项目都能复用,也方便统一管理。
1.2 验证ccache是否生效
编译完成后,跑一下这个命令:
ccache -s
你会看到类似这样的输出:
cache directory /home/user/.ccache
primary cache size 256.0 MB
cache hit rate 78.5 %
cache miss 21.5 %
命中率越高,说明加速效果越好。我见过优化得好的项目,命中率能到90%以上。
注意:ccache 对编译选项敏感。如果你改了编译选项(比如从 -O2 改成 -O0),之前的缓存就失效了。这是设计如此,不是bug。我曾经因为这个排查了半天,以为ccache坏了。
二、并行构建:让CPU满负荷运转
现在的电脑动不动就8核16线程,但默认情况下CMake只用一个核编译。这不是浪费吗?
并行构建的原理很简单:把不依赖的源文件同时编译。比如文件A和文件B没有互相引用,那就可以同时编译它们。
2.1 设置并行编译参数
最直接的方式是在构建时指定 -j 参数:
cmake --build build -j $(nproc) # Linux/Mac
cmake --build build -j 8 # 手动指定8个并行任务
$(nproc) 会自动获取CPU核心数。我个人习惯用这个,省心。
也可以在CMake中预设并行数:
set(CMAKE_BUILD_PARALLEL_LEVEL 8)
不过我不太推荐在CMakeLists.txt里写死这个值。因为不同开发者的机器配置不一样。你写8,人家只有4核怎么办?
2.2 生成器的选择也很重要
CMake支持多种生成器。我用下来,Ninja 的并行效率比 Make 高不少。原因在于Ninja天生就是为并行设计的,而Make的依赖解析相对保守。
cmake -B build -G Ninja
cmake --build build -j $(nproc)
我做过对比测试:同一个项目,用Make编译需要120秒,换成Ninja只要95秒。差距就在依赖图的解析效率上。
我的建议:新项目直接用Ninja生成器。老项目如果迁移成本高,至少把 -j 参数加上。别让CPU闲着。
三、预编译头文件:把公共头文件提前搞定
预编译头文件(PCH)是什么?简单说,就是把那些经常被include的、内容稳定的头文件,提前编译成二进制格式。这样每次编译源文件时,这部分就不用重新解析了。
嵌入式项目里,像 <stdint.h>、<string.h>、芯片厂商的HAL库头文件,这些几乎每个源文件都会include。它们就是预编译头的最佳候选。
3.1 CMake中配置预编译头
CMake 3.16 之后提供了 target_precompile_headers 命令,用起来很方便:
# 创建一个公共头文件列表
target_precompile_headers(my_target PUBLIC
<stdint.h>
<string.h>
<stdlib.h>
"stm32f4xx_hal.h"
"FreeRTOS.h"
)
这里 PUBLIC 表示这个预编译头对目标本身和所有链接它的目标都生效。PRIVATE 则只对当前目标生效。
3.2 多个目标共享预编译头
如果项目里有多个目标(比如 bootloader、app、test),它们用的头文件差不多。可以用 REUSE_FROM 来共享:
# 先给一个目标生成预编译头
target_precompile_headers(app PUBLIC
<stdint.h>
"common.h"
)
# 其他目标直接复用
target_precompile_headers(bootloader REUSE_FROM app)
target_precompile_headers(test REUSE_FROM app)
这样只有 app 会真正生成预编译头,其他目标直接拿过来用。编译时间能省不少。
踩坑提醒:我曾经在一个项目里把整个项目的头文件都塞进预编译头,结果编译反而变慢了。为什么?因为预编译头本身也要编译,如果头文件太大,生成预编译头的时间就得不偿失。一般建议只放那些稳定且高频使用的头文件。
四、三板斧组合使用
这三招不冲突,可以一起用。我一般这样配置:
# 1. ccache
find_program(CCACHE_PROGRAM ccache)
if(CCACHE_PROGRAM)
set(CMAKE_C_COMPILER_LAUNCHER "${CCACHE_PROGRAM}")
set(CMAKE_CXX_COMPILER_LAUNCHER "${CCACHE_PROGRAM}")
endif()
# 2. 生成器选择(在命令行指定 -G Ninja)
# 3. 预编译头
target_precompile_headers(my_app PUBLIC
<stdint.h>
<string.h>
"chip_hal.h"
"os_kernel.h"
)
构建时这样跑:
cmake -B build -G Ninja
cmake --build build -j $(nproc)
我实测过一个项目:
| 优化方式 | 首次编译 | 二次编译(改1个文件) |
|---|---|---|
| 无优化 | 320秒 | 310秒 |
| + 并行构建 | 95秒 | 90秒 |
| + ccache | 95秒 | 8秒 |
| + 预编译头 | 78秒 | 6秒 |
看到没?从320秒降到6秒,50倍的提升。这就是组合拳的威力。
最后说一句:编译加速这件事,投入产出比非常高。花半小时配置好,以后每天省半小时。这笔账怎么算都划算。赶紧去给你的项目加上吧。
公众号:蓝海资料掘金营,微信deep3321