编译加速:ccache集成、并行构建、预编译头文件

说实话,嵌入式项目的编译速度,一直是让我头疼的问题。早期我做一个STM32项目,每次改一行代码,等编译完成要喝杯咖啡。后来项目规模越来越大,一个全量编译动辄十几分钟,甚至半小时。这谁受得了?

今天我们就来聊聊怎么给CMake项目装上「加速器」。我总结了三板斧:ccache缓存并行构建预编译头文件。这三招用好了,编译时间能砍掉一半以上。

核心观点:编译加速的本质是「避免重复劳动」。ccache缓存的是编译结果,并行构建是同时干多份活,预编译头文件是把公共部分提前准备好。三者不冲突,可以叠加使用。

CMake 编译加速三板斧 ccache 缓存 并行构建 预编译头文件 find_program + 全局包装 CMAKE_C_COMPILER_LAUNCHER -j 参数 + CPU 核心数 Ninja 生成器优势 target_precompile_headers REUSE_FROM 共享机制 三者叠加使用,效果最佳

一、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