sccache 缓存机制详解:哈希键的生成原理与预处理器缓存模式
【免费下载链接】sccacheSccache is a ccache-like tool. It is used as a compiler wrapper and avoids compilation when possible. Sccache has the capability to utilize caching in remote storage environments, including various cloud storage options, or alternatively, in local storage.项目地址: https://gitcode.com/GitHub_Trending/sc/sccache
sccache 是一款类似 ccache 的编译缓存工具,通过包装编译器来避免不必要的重复编译。其核心工作方式是:在访问存储(本地磁盘或各类云存储)之前,先对本次编译的输入计算一组哈希,只有当哈希完全一致时才判定缓存命中。本文基于 docs/Caching.md 展开,结合仓库源码(src/compiler/c.rs、src/compiler/rust.rs、src/compiler/preprocessor_cache.rs)与 docs/Local.md 中的本地缓存配置,系统讲解 Rust 与 C/C++ 两类编译的哈希键构成、多架构编译的处理方式,以及可大幅提速的预处理器缓存模式(preprocessor cache mode),帮助读者理解缓存命中率背后的决定因素,并学会通过环境变量与配置文件精确控制缓存行为。
缓存命中的前提:计算一致的哈希键
sccache 判断"存储中是否已有我们需要的产物"的唯一依据是哈希键(hash key)。由于编译配置(编译器版本、参数)和环境(环境变量、头文件搜索路径)都会影响编译结果,哈希计算必须把这些要素全部纳入,否则可能错误复用产物导致编译结果失真。因此 sccache 对不同语言分别设计了独立的哈希键生成逻辑,核心实现在CompilerHashertrait 的generate_hash_key方法中:
- Rust:见 src/compiler/rust.rs 中
RustHasher的实现; - C/C++:见 src/compiler/c.rs 中
CCompilerHasher的实现。
下面分别说明两类哈希键的构成。
Rust 编译的哈希键
对每个 Rust 文件的编译,sccache 会为每个被编译的文件生成一个 blake3 摘要(digest),同时并行地把以下要素计入哈希:
- rustc 可执行文件的路径(注意是解析 rustup 代理之后真正的 rustc 路径,见 src/compiler/rust.rs);
- 该 rustc 的 host triple(例如
x86_64-unknown-linux-gnu),它从rustc -vV输出中解析得到; - rustc 的 sysroot 路径(通过
rustc --print=sysroot获取); - rustc 的
$sysroot/lib下所有共享库的摘要(Windows 上为$sysroot/bin),这些共享库的 digest 在Rust::new中通过hash_all一次性计算并缓存,见 src/compiler/rust.rs; - 一个共享的、带缓存的 rlib 依赖读取器(
rlib_dep_reader,仅dist-client特性下启用),用于分布式编译场景; - 本次 rustc 调用的已解析参数(parsed arguments)。
完整哈希输入列表定义在 src/compiler/rust.rs,其顺序为:
- 一个缓存版本号
CACHE_VERSION(当前为b"6",见 src/compiler/rust.rs),改动哈希输入时必须同步递增; - 所有
compiler_shlibs_digests(sysroot 共享库摘要); - 完整命令行(其中
--extern、-L、--check-cfg、--out-dir、--diagnostic-width等包含无关路径或不确定顺序的参数会被剔除;--cfg被排序后追加;指向 JSON 文件的--target参数内容单独参与哈希); - 所有源文件的摘要(通过
rustc --emit=dep-info输出的依赖清单获得,见get_source_files_and_env_deps); - 命令行列出的所有
--extern依赖库的摘要; - 命令行列出的所有静态库(staticlib)的摘要;
--target指向的 JSON 文件内容的摘要;- 环境变量:包括 rustc dep-info 输出中列出的所有
# env-dep:变量,以及所有CARGO_开头的环境变量(但会剔除CARGO_MAKEFLAGS、CARGO_REGISTRIES_*token、CARGO_BUILD_JOBS、CARGO_ENCODED_RUSTFLAGS等不可缓存或已冗余的项); - 编译时的当前工作目录(cwd);
- rustc 的版本字符串(完整
rustc -vV输出,防止版本升级后复用旧产物)。
此外,sccache 还会额外计算一个weak_toolchain_key(由编译器可执行文件路径 + 其摘要构成),用于规避"编译器二进制是符号链接、摘要相同但真实路径不同"带来的工具链定位错误,见 src/compiler/c.rs 与 src/compiler/c.rs 中的注释说明。
C/C++ 编译的哈希键
对于 C/C++,sccache 的做法是先对源文件做预处理(preprocess),再对预处理结果(-E的输出)计算 blake3 摘要。预处理是 C/C++ 编译中最昂贵的环节之一,把它作为哈希键的主体意味着:只要预处理结果一致,编译产物就可复用。
除预处理输出外,C/C++ 哈希键还计入以下要素(对应CCompilerHasher::generate_hash_key的实现,见 src/compiler/c.rs):
- 编译器二进制的哈希:实际计算时还会混入编译器报告的版本号(如果编译器能报告版本的话),见
CCompiler::new中的executable_digest计算(src/compiler/c.rs); - 汇编器二进制的哈希及其报告的版本(如果编译器会把汇编工作交给外部汇编器):GCC 总是如此,clang 仅在指定
-fno-integrated-as时如此。汇编器摘要由CCompilerImpl::assembler_digest提供; - 编程语言(C/C++/Objective-C 等)及编译该语言所需的标志(如
-x c++); - 依赖生成目标文件(
depfile); - 依赖生成相关的命令行参数(
dependency_args); - 预处理器相关命令行参数(
preprocessor_args与common_args); - 指定编译架构的命令行参数(
arch_args); - 需要哈希内容的额外文件(
extra_hash_files,通过hash_all并行计算); - 是否生成 profiling 或 coverage 数据(
profile_generate); - 颜色模式(
color_mode); - 环境变量(
env_vars,在HashKeyParams::with_env_vars中计入)。
这些参数在解析阶段被分类存储于ParsedArguments结构体(src/compiler/c.rs),生成哈希键时,预处理器参数、架构参数与通用参数会被合并为preprocessor_and_arch_args,再通过HashKeyParams与预处理输出共同计算最终键值,见 src/compiler/c.rs。
多架构编译与SCCACHE_CACHE_MULTIARCH
当一条编译命令指定了多个-arch标志时(常见于 macOS 的通用二进制编译),sccache 需要把这些-arch标志重写为对应的预处理器宏,才能对文件做统一的预处理,例如把-arch x86_64重写为-D__X86_64__=1。
该行为由环境变量SCCACHE_CACHE_MULTIARCH控制,默认关闭(未设置即视为关闭),因为重写并不能保证在所有情况下都正确:
- 未设置
SCCACHE_CACHE_MULTIARCH时,遇到多个不同的-arch会直接判定为不可缓存,报错信息为"multiple different -arch, and SCCACHE_CACHE_MULTIARCH not set",见 src/compiler/gcc.rs 与 src/compiler/gcc.rs; - 设置后,架构参数会参与预处理并进入哈希键;仓库测试 src/compiler/gcc.rs 验证了开启该变量时多
-arch参数可被正确重写与缓存,src/compiler/gcc.rs 则验证了未开启时的禁用行为。
辅助语言(CUDA/HIP 等)的特殊处理
在 src/compiler/c.rs 中可以看到,对 HIP 等依赖位码库的编译,sccache 还会根据--rocm-path、--hip-device-lib-path参数以及ROCM_PATH、HIP_DEVICE_LIB_PATH环境变量解析 HIP 设备库路径,并将相关文件计入哈希,确保依赖的位码库变化时缓存能正确失效。
C/C++ 预处理器缓存模式:额外的缓存键
在"预处理器缓存模式"(preprocessor cache mode)下,sccache 会把预处理器的输出本身也缓存起来。这样当源码与所有头文件都未变化时,可以直接跳过预处理这一最耗时的环节,从缓存中拿到编译产物,大幅缩短缓存命中的响应时间。该模式详细介绍见 docs/Local.md 的 "Preprocessor cache mode" 一节,其设计灵感来自 ccache 的 direct mode。
预处理器缓存键与常规 C/C++ 编译器哈希键非常接近,但额外加入两个要素(对应preprocessor_cache_entry_hash_key的实现,见 src/compiler/preprocessor_cache.rs):
- 输入文件的路径(并依据配置做 basedir 剥离后再哈希,避免不同检出目录间误命中);
- 输入文件的哈希(内容摘要)。
该函数还会把一组与头文件搜索相关的环境变量计入缓存键,定义在CACHED_ENV_VARS(src/compiler/preprocessor_cache.rs),包括:
| 环境变量 | 作用 |
|---|---|
SCCACHE_C_CUSTOM_CACHE_BUSTER | 用户自定义的"缓存破坏器",设置为不同值即可阻止不同调用之间复用缓存 |
CPATH | 头文件搜索路径(影响包含解析结果) |
C_INCLUDE_PATH | C 头文件搜索路径 |
CPLUS_INCLUDE_PATH | C++ 头文件搜索路径 |
OBJC_INCLUDE_PATH | Objective-C 头文件搜索路径 |
OBJCPLUS_INCLUDE_PATH | Objective-C++ 头文件搜索路径 |
预处理器缓存的数据结构
预处理器缓存条目PreprocessorCacheEntry(src/compiler/preprocessor_cache.rs)本质上是一张"结果键 → 包含文件列表"的映射表,磁盘格式为 1 字节版本号(FORMAT_VERSION = 1)+ bincode 序列化。每个IncludeEntry记录被包含文件的绝对路径、内容摘要、文件大小以及(满足条件时记录的)mtime/ctime。
命中判定lookup_result_digest会从最新的结果开始倒序遍历,逐一核对所有被包含文件是否仍然存在且未变化(见result_matches,src/compiler/preprocessor_cache.rs)。为了防止条目无限膨胀,源码还设置了两个上限:单个条目最多保留MAX_PREPROCESSOR_CACHE_ENTRIES = 100个结果,全部被包含文件条目上限为 10000,超限时直接清空重建。
预处理器缓存模式何时被禁用
预处理器缓存模式在以下任一情况下会被自动关闭:
- 编译的不是 C 或 C++(以及无需 C 预处理的语言,见 src/compiler/c.rs);
- 配置项
use_preprocessor_cache_mode为false; - 使用的不是 GCC 或 Clang;
- 缓存存储不是本地磁盘;
- 命令行中存在
-MP、-Xpreprocessor或-Wp,中任何一个选项(在参数解析阶段被标记为too_hard_for_preprocessor_cache_mode,见 src/compiler/c.rs;-Xpreprocessor与-Wp,*也会让键值计算直接返回None禁用,见 src/compiler/preprocessor_cache.rs); - 某个头文件的修改时间过新(晚于编译开始时间,为避免竞争条件,见
add_result中对 mtime/ctime 的判定,src/compiler/preprocessor_cache.rs); - 源码中出现
__DATE__、__TIME__、__TIMESTAMP__等时间宏——这些宏的展开值取决于外部时间因素,可能使预处理器结果不稳定(检测逻辑见Digest::reader_sync_time_macros,在 src/compiler/preprocessor_cache.rs 中调用;__TIME__命中时会直接返回None禁用该模式)。
可能静默产生过期结果的情况
以下场景下预处理器缓存可能静默返回过期的结果,需要使用者知晓:
- 某次编译时一个头文件尚不存在,但如果它存在就会被包含;sccache 无从知晓这类"潜在包含",因此之后该头文件被创建时缓存不会失效;
- 源码中使用了
__TIME__等时间宏,同时开启了ignore_time_macros; - 存在其他影响预处理结果、但 sccache 无法感知的外部因素。
预处理器缓存模式的配置项
以下配置项定义于PreprocessorCacheModeConfig(src/config.rs),默认值如下:
| 配置项 | 默认值 | 说明 |
|---|---|---|
use_preprocessor_cache_mode | true | 是否启用预处理器缓存模式。可通过环境变量SCCACHE_DIRECT对单次调用覆盖(true/on/1或false/off/0),覆盖逻辑见 src/config.rs 与 src/compiler/c.rs |
file_stat_matches | false | 为false时仅通过哈希文件内容比较头文件是否变化;为true时改用"文件大小 + ctime + mtime"快速比对(配合下面的开关做更细粒度控制) |
use_ctime_for_stat | true | 为true时使用 ctime(UNIX 上为状态变更时间,Windows 上为创建时间)判断文件是否变化;在需要受控回拨修改时间(backdate)的场景可考虑关闭 |
ignore_time_macros | false | 为true时忽略源码中的__DATE__、__TIME__、__TIMESTAMP__。能提升预处理器缓存模式的命中速度,但可能产生过期结果 |
skip_system_headers | false | 为true时,预处理器缓存只为系统头文件记录路径(不缓存其内容),减少缓存体积与比对开销 |
hash_working_directory | true | 为true时将当前工作目录加入缓存键,以区分不同目录下的编译。对应实现见 src/compiler/c.rs |
注意:源码中PreprocessorCacheModeConfig::default()的use_preprocessor_cache_mode字段为false,但实际启用时使用的是PreprocessorCacheModeConfig::activated()(将该项置为true),本地磁盘缓存的默认配置即调用activated(),因此本地缓存默认开启预处理器缓存模式,与文档描述一致。配置文件的书写位置见 docs/Configuration.md。
本地缓存的其它实用配置
结合 docs/Local.md,围绕缓存键之外的本地缓存行为还有以下要点:
- 缓存目录:sccache 默认使用本地磁盘存储,可通过环境变量
SCCACHE_DIR改变缓存位置。各平台默认位置为:Linux~/.cache/sccache、Windows%LOCALAPPDATA%\Mozilla\sccache、macOS~/Library/Caches/Mozilla.sccache。 - 缓存大小:默认 10 GB,可通过
SCCACHE_CACHE_SIZE调整,例如SCCACHE_CACHE_SIZE="1G"。 - 单服务器约束:本地存储同一时间只支持一个 sccache 服务器,多个并发服务器会互相竞争并导致随机的构建失败。
- 只读缓存模式:本地缓存默认以读/写模式运行,设置环境变量
SCCACHE_LOCAL_RW_MODE=READ_ONLY(或显式READ_WRITE)可禁止 sccache 向磁盘写入新的缓存条目。适合"只消费已有缓存、不新增缓存"的场景;需要注意的是,该模式仅在缓存中已有条目时才有意义,空缓存下使用它只会徒增开销而不产生任何收益。
小结:如何用好 sccache 的缓存键
理解哈希键的构成,是排查缓存命中率问题的起点:
- Rust 项目:哈希键几乎覆盖了 rustc 的全部影响面(编译器本体、sysroot 共享库、所有源文件与依赖库、环境变量、cwd、版本号),因此任何环境或工具链变动都会自然导致缓存失效;要主动隔离不同调用之间的缓存,可通过
CARGO_*相关机制或改造命令行参数实现。 - C/C++ 项目:哈希键以预处理输出为主体,额外叠加编译器/汇编器摘要、语言与架构参数、环境变量等。多架构编译需显式设置
SCCACHE_CACHE_MULTIARCH;启用预处理器缓存模式可以跳过最耗时的预处理环节,但在使用__DATE__/__TIME__/__TIMESTAMP__宏、-Wp,/-Xpreprocessor参数以及非本地存储时会被自动禁用,相关权衡(如file_stat_matches、ignore_time_macros、skip_system_headers)可通过 docs/Configuration.md 描述的配置文件按需调整。 - 环境变量是灵活的开关:
SCCACHE_DIRECT(预处理器缓存开关)、SCCACHE_CACHE_MULTIARCH(多架构缓存)、SCCACHE_LOCAL_RW_MODE(只读模式)均可在不改动构建脚本的前提下即时生效。
更详细的本地存储行为可继续阅读 docs/Local.md,完整配置项说明见 docs/Configuration.md,分布式缓存场景下的键值利用方式见 docs/Distributed.md。
【免费下载链接】sccacheSccache is a ccache-like tool. It is used as a compiler wrapper and avoids compilation when possible. Sccache has the capability to utilize caching in remote storage environments, including various cloud storage options, or alternatively, in local storage.项目地址: https://gitcode.com/GitHub_Trending/sc/sccache
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考