同一个项目全量 Cook 几十分钟,日常迭代 Cook 几分钟,差距几乎全来自 DDC(Derived Data Cache,派生数据缓存)。但 DDC 不是"开了就灵"的开关:很多人遇到过"清一下缓存全好了"和"清了缓存反而更慢"两种截然相反的体验,根源都在键的构造上。接口主体在 Engine/Source/Runtime/DerivedDataCache,UE5.3 还有 Zen 后端(见第 9050 篇)。
成立前提:确定性重计算
DDC 缓存的是"派生数据"——纹理压缩后的 BC7 块、shader 编译出的平台字节码、导航网格这类可以从源数据重算的产物。它能当缓存用,靠的是这个性质:
$$\text{同一份源数据} + \text{同一套配置} \;\Longrightarrow\; \text{逐字节相同的输出}$$
注意"配置"三个字也在前提里。压缩格式、压缩级别、目标平台、shader 宏集合,全是输入的一部分。很多人 DDC miss 查了半天发现是 .ini 里改过一个参数——参数变了,键就变了,旧缓存和新键对不上,这不是 bug,是前提的自然推论。
键的三段拼法
一次 DDC 查询的键由三段拼成(FDerivedDataCacheInterface 系列接口,键最终是一个字符串):
- 类名:谁负责生成,比如
TEXTURE段对应纹理压缩器。同名数据的生成器换了实现,必须换键,否则拿旧实现的结果当新实现的用; - 版本号:生成器内部的格式版本。实现没变但输出格式变了(比如加了一个 mip 的元数据字段),靠它 invalidate;
- 对象后缀:具体到哪个对象、哪个变体。纹理就是"哪个包里的哪个纹理 + 目标格式 + mip 设置"的组合,shader 是"shader 源哈希 + 宏组合 + 平台"。
三段拼成一个字符串再哈希。前两段保证"生成逻辑变了必然换键",第三段保证"不同对象不串味"。写自定义 DDC 数据时(FDerivedDataCacheInterface::GetSynchronous / Put),最容易犯的错是后缀里漏了影响输出的参数——比如自己加了个压缩级别参数却拼进键里,结果就是缓存互相污染,症状是"改了参数结果没变"。
后端链:逐级短路
DDC 后端按序组成一条链,查询逐级下钻,命中即返回:
- Memory:进程内哈希表,同一次 Cook 会话内重复查询秒回;
- Local:本地文件系统(项目
DerivedDataCache目录),跨会话有效,换台机器就没了; - Remote / Zen:HTTP 共享缓存,CI 和整个团队共用,见第 9050 篇;
- 全 miss:现场重算,然后按
ECachePolicy决定回写哪些层(QueryLocal/QueryRemote/StoreLocal/StoreRemote)。
写路径同理:重算结果默认回写本地,配了远端就一并回写共享层——这也是为什么 CI 跑过一次全量 Cook 之后,开发机的增量 Cook 会明显变快:纹理和 shader 的派生数据早被 CI 灌进了共享层。
命中率为什么是线性的
把 Cook 时间拆成两部分:与缓存无关的固定骨架 $T_{base}$(包遍历、元数据、写出),和可被缓存的重计算 $T_{derived}$(纹理压缩、shader 编译、光照构建的中间产物)。设命中率为 $h$,则
$$T(h) = T_{base} + (1 - h)\, T_{derived}$$
这是个线性模型,有两个直接推论。第一,边际收益恒定:命中率从 50% 提到 60% 和从 90% 提到 100%(后者还做不到)省的时间一样多,不存在"越往后越赚"的复利。第二,收益上限被 $T_{derived}$ 钉死:如果项目里纹理和 shader 占比不大,DDC 调得再好也快不了多少;反过来纹理重的项目,首次 Cook 和命中 Cook 的差距能到 10 倍。想判断 DDC 值不值得折腾,先看 LogDerivedDataCache 里 miss 的分布,而不是盲目清缓存。
什么时候会集体失效
几条常见的"全库 miss"触发路径:
- 引擎升级:生成器版本号 bump,所有键换血,等于缓存清零——这是设计行为;
- 切平台 / 切 Cook 配置:平台名和配置项在键里,跨平台 Cook 天然不共享;
- 分支差异:CustomVersion 和引擎版本号不同,键不同,两个分支的缓存各是一套;
- 手动清缓存:
DerivedDataCache目录删掉重来,第一次 Cook 回到全量。
所以"共享 DDC 给全团队用"的前提是团队在同一条 release 分支上干活;多分支并行时,要么给每个分支配独立缓存命名空间,要么接受频繁的全量 miss。Zen 时代的共享缓存也是同样的规则,只是存储从"文件夹"换成了"内容寻址服务"(第 9050 篇展开)。
调试手段
- Cook 日志里开
LogDerivedDataCache Verbose,能看到每次查询的键和命中层级;miss 异常多时先怀疑键拼接,而不是怀疑后端坏了; - 自己做派生数据的,把"拼键用的全部参数"打进键里这一条当铁律,漏一个参数就是潜伏的缓存污染;
- 磁盘占用爆炸时查本地缓存目录大小,纹理重的项目几个 TB 不稀奇,定期清或搬到共享 Zen 上是常规运维。