26、构建系统性能优化:并行编译、ccache与预编译头文件

说实话,嵌入式项目的构建速度,经常让人抓狂。

我见过不少团队,改一行代码,等编译等五分钟。喝杯咖啡回来,还没编完。这其实不是代码的问题,是构建系统没调好。今天咱们就聊聊怎么让编译飞起来。

并行编译:让CPU火力全开

先讲最直接的提速手段——并行编译。说白了,就是让CPU多核同时干活。

make -j 的用法

在Makefile里,你只需要加个 -j 参数:

# 让make自己决定并行任务数
make -j

# 指定并行数,比如4核
make -j4

# 更推荐的做法:用nproc获取CPU核心数
make -j $(nproc)

我个人习惯用 make -j$(nproc)。为什么?因为不同开发机配置不一样。你写死 -j4,别人16核的机器就浪费了。

注意: 并行数不是越大越好。我见过有人设 -j64,结果内存爆了,编译反而更慢。一般建议设为CPU物理核心数的1.5到2倍。

CMake中的并行控制

CMake这边更优雅。你可以通过环境变量来控制:

# 设置并行编译级别
export CMAKE_BUILD_PARALLEL_LEVEL=4
cmake --build .

也可以在CMakeLists.txt里设置:

# 全局设置,所有target都受影响
set(CMAKE_BUILD_PARALLEL_LEVEL 4)

# 或者只针对特定target
set_property(TARGET my_firmware PROPERTY JOB_POOL_COMPILE compile_job_pool)
set_property(TARGET my_firmware PROPERTY JOB_POOL_LINK link_job_pool)

嗯,这里要注意:链接阶段通常吃内存,我建议把链接的并行度设低一点。比如编译开8路,链接只开2路。

ccache:缓存编译结果

并行编译解决的是「一次编译」的速度。但实际开发中,我们经常改一行代码,然后重新编译。这时候ccache就派上用场了。

ccache的原理很简单:它缓存了编译器的输出。如果源文件没变,头文件没变,编译器参数没变,那就直接从缓存拿结果。

安装与配置

# Ubuntu/Debian
sudo apt install ccache

# macOS
brew install ccache

# 查看缓存状态
ccache -s

在CMake里启用ccache也很简单:

# 方法一:设置CMAKE_C_COMPILER_LAUNCHER
find_program(CCACHE_PROGRAM ccache)
if(CCACHE_PROGRAM)
    set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE "${CCACHE_PROGRAM}")
    message(STATUS "ccache enabled")
endif()

# 方法二:更推荐的做法
set(CMAKE_C_COMPILER_LAUNCHER ccache)
set(CMAKE_CXX_COMPILER_LAUNCHER ccache)
我的经验: 第一次启用ccache时,编译会变慢(因为要写缓存)。别慌,第二次就快了。我有个项目,启用ccache后,增量编译从3分钟降到了15秒。

ccache的坑

我曾经踩过一个坑:ccache缓存了错误的编译结果。原因是编译器版本变了,但ccache没意识到。解决方案:

# 设置编译器检查
export CCACHE_COMPILERCHECK=content

# 或者更狠一点,每次clean缓存
ccache -C  # 清理所有缓存

另外,嵌入式开发中经常用交叉编译器。ccache默认只缓存同一编译器的结果。如果你切换了工具链,记得清缓存。

预编译头文件(PCH)

这个技术有点年头了,但在大型嵌入式项目中依然好用。

预编译头文件的思路:把那些不怎么变的标准库头文件(比如 <stdio.h><stdlib.h>)预先编译成二进制格式。这样每个源文件就不用重复解析这些头文件了。

GCC中的PCH

# 生成预编译头文件
gcc -x c-header my_header.h -o my_header.h.gch

# 使用:编译器会自动查找.gch文件
gcc -include my_header.h main.c -o main

在Makefile里可以这样组织:

# 生成PCH
PCH_FILE = common.h.gch
$(PCH_FILE): common.h
    $(CC) -x c-header $< -o $@

# 依赖PCH
%.o: %.c $(PCH_FILE)
    $(CC) -include common.h -c $< -o $@

CMake中的PCH

CMake 3.16之后原生支持PCH,用起来方便多了:

# 为target添加预编译头
target_precompile_headers(my_target PRIVATE
    <vector>
    <string>
    <algorithm>
    "my_project_common.h"
)

# 也可以只对某个文件生效
target_precompile_headers(my_target PRIVATE
    $<$<COMPILE_LANGUAGE:CXX>:<vector>>
)
关键点: PCH只对「稳定」的头文件有效。如果你频繁修改预编译头里的内容,那每次改完都要重新生成PCH,反而更慢。

知识体系总览

下面这张图总结了本章的核心逻辑:

构建系统性能优化三大支柱 编译速度提升 并行编译 make -j / CMAKE_BUILD_PARALLEL_LEVEL ccache缓存 增量编译加速 5~20倍 预编译头文件 PCH / target_precompile_headers make -j$(nproc) JOB_POOL控制 CMAKE_C_COMPILER_LAUNCHER 缓存命中率监控 GCC .gch文件 CMake原生支持 三者结合:编译时间从分钟级降到秒级

实战建议

说了这么多,到底怎么用?我给出一个典型的嵌入式项目配置:

  1. 先上ccache:这是投入产出比最高的。配置简单,效果明显。
  2. 再开并行编译make -j$(nproc) 或者 CMake 里设 CMAKE_BUILD_PARALLEL_LEVEL
  3. 最后考虑PCH:如果项目头文件特别多(比如超过50个),PCH能再省30%的时间。
一个小技巧: 在CI/CD流水线里,可以把ccache的缓存目录设为共享存储。这样不同构建节点都能复用缓存。我之前的项目,团队10个人共享一个ccache缓存,每天能省下好几个小时的编译时间。

最后提醒一句:性能优化要「先测量,后优化」。别一上来就搞PCH,先看看编译时间到底花在哪。用 make -d 或者 cmake --build . --verbose 看看输出,心里就有数了。