3、ccache加速:从原理到实战

说到Buildroot加速,ccache绝对是个绕不开的话题。我刚开始用Buildroot那会儿,每次全量编译都要等上半小时,改一行代码就得重来一遍,说实话挺崩溃的。后来同事跟我说:「你试试ccache?」一试,真香。

这一章,我就把ccache怎么用、怎么调优,一次性讲清楚。

3.1 ccache原理:它到底干了什么?

ccache的全称是「Compiler Cache」——编译器缓存。说白了,它就是个中间人。

正常编译流程是这样的:
源文件 → 预处理器 → 编译器 → 汇编器 → 目标文件

加了ccache之后:
源文件 → ccache(检查缓存)→ 有缓存?直接返回目标文件 → 没缓存?调用真正的编译器 → 结果存入缓存

ccache怎么判断缓存是否命中?它会对源文件内容、编译选项、头文件路径、宏定义等所有影响输出的因素做哈希。只要有一个字节变了,哈希值就不同,缓存自然就不命中。

核心要点:ccache缓存的是编译后的目标文件(.o),不是整个可执行文件。所以链接阶段它帮不上忙,但编译阶段提速非常明显。

我在项目中遇到过最夸张的一次:一个包含200多个源文件的BSP包,第一次编译用了18分钟,第二次只用了4分钟——因为只改了3个文件,其余全部命中缓存。

3.2 在Buildroot中启用ccache

Buildroot对ccache的支持非常成熟,基本上就是改几个配置项的事。

3.2.1 配置步骤

  1. 运行 make menuconfig
  2. 进入 Build options → Compiler cache (ccache)
  3. 勾选 Enable compiler cache
  4. (可选)设置缓存目录,默认是 ~/.buildroot-ccache

对应的.config文件里是这样的:

BR2_CCACHE=y
BR2_CCACHE_DIR="$(HOME)/.buildroot-ccache"

嗯,就这么简单。但有个坑我必须提醒你——

我曾经踩过的坑:如果你同时用多个Buildroot工程,千万别让它们共享同一个ccache目录。不同架构的编译产物混在一起,轻则缓存反复失效,重则链接出奇怪错误。每个工程单独设置 BR2_CCACHE_DIR,或者用环境变量 CCACHE_DIR 区分。

3.2.2 验证ccache是否生效

编译完成后,跑一下这个命令:

ccache -s

你会看到类似这样的输出:

cache directory                     /home/user/.buildroot-ccache
primary config                      /home/user/.buildroot-ccache/ccache.conf
secondary config      (readonly)    /etc/ccache.conf
stats updated                       Sun Mar 10 14:22:35 2025
cache hit (direct)                  1423
cache hit (preprocessed)             287
cache miss                           156
cache hit rate                     91.6 %

看到 cache hit rate 超过90%,说明ccache工作正常。如果这个数字很低,别急,后面我会讲怎么优化。

3.3 ccache缓存管理与优化

ccache用久了,缓存会越来越大。默认情况下它不会自动清理,你得自己管。

3.3.1 缓存大小控制

我个人习惯把缓存上限设为5GB,够用又不占太多磁盘:

ccache -M 5G

这个命令会写入 ~/.ccache/ccache.conf,永久生效。你也可以在Buildroot的配置里直接设:

BR2_CCACHE_DIR_SIZE="5G"

小技巧:如果你在CI/CD环境里用ccache,建议把缓存上限设小一点,比如1GB。因为CI机器通常是临时实例,缓存太大反而浪费拉取时间。

3.3.2 缓存清理策略

ccache提供了几个清理命令:

命令 作用 我推荐的使用场景
ccache -c 清理所有缓存 换芯片架构时,一次性清空
ccache -C 清理所有缓存并重置统计 调试缓存问题时,从零开始
ccache -F 限制缓存文件数量 配合 -M 一起用,双重保险
ccache --evict-older-than 7d 删除7天前的缓存 长期项目,定期清理过期缓存

你想想看,如果项目持续迭代,每天编译几十次,一周下来缓存可能膨胀到十几GB。我一般会在每周的构建脚本里加一行:

ccache --evict-older-than 7d

这样既保留了最近一周的热缓存,又不会让磁盘爆掉。

3.3.3 提高缓存命中率的几个技巧

缓存命中率低,ccache就白开了。我总结了几条实战经验:

  • 保持编译选项一致:同一个源文件,用 -O2 编译和用 -Os 编译,哈希值不同,缓存不共享。所以Makefile里的CFLAGS别乱改。
  • 避免绝对路径:如果编译命令里带了绝对路径(比如 -I/home/user/project/include),换个用户或换个目录,缓存就失效了。用相对路径或者 -I$(TOPDIR)/include 这种形式。
  • 统一编译器版本:gcc 9.3.0 和 gcc 9.4.0 生成的哈希也不同。团队协作时,大家用同一个工具链版本。
  • 关闭调试信息中的时间戳:有些Makefile会在 -g 里嵌入时间戳,每次编译都不一样,缓存永远不命中。检查一下有没有 -Wdate-time 之类的选项。

一句话总结:ccache的哈希算法很严格,任何「看起来不影响结果但实际变了」的东西,都会导致缓存miss。所以,编译环境越「干净」、越「一致」,命中率越高。

3.4 ccache工作流程总览

为了让你更直观地理解ccache的完整工作流程,我画了一张图:

ccache工作流程图 ccache 工作流程 源文件 (.c/.cpp) ccache 拦截 哈希匹配? 命中 返回缓存 .o 未命中 真实编译器 (gcc/clang) 目标文件 (.o) 存入缓存 命中时直接返回 .o,跳过预处理+编译+汇编;未命中时才调用真实编译器

从这张图可以看得很清楚:ccache就像一个智能分流器。命中了,直接拿结果;没命中,才走完整编译流程,同时把结果存下来供下次使用。

3.5 实战建议

最后,给你几个可以直接用的建议:

  • 新项目第一次编译前:先设好 BR2_CCACHE_DIR_SIZE,别等缓存爆了才想起来。
  • 日常开发:每周跑一次 ccache --evict-older-than 7d,保持缓存新鲜。
  • CI/CD环境:把ccache目录做成持久化卷,否则每次构建都是冷启动,ccache等于没用。
  • 换工具链或架构时:直接 ccache -C 清空,别留着旧缓存占地方。

我的个人习惯:在Buildroot的 post-build.sh 脚本里加一行 ccache -s | grep "cache hit rate",每次编译完自动打印命中率。如果发现命中率低于80%,我就会去查是不是编译选项变了或者头文件路径出了问题。

ccache这东西,配置起来不费劲,但用好了能省下大把时间。尤其是大型项目,每次全量编译省下十几分钟,一天迭代几次,积累下来就是几个小时。这笔账,你算算看。