十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Rust 编译器特性选择缓存机制深度解析:rustc trait selection 的缓存设计与微妙考量

Rust 编译器特性选择缓存机制深度解析:rustc trait selection 的缓存设计与微妙考量 Rust 编译器特性选择缓存机制深度解析rustc trait selection 的缓存设计与微妙考量【免费下载链接】rustEmpowering everyone to build reliable and efficient software.项目地址: https://gitcode.com/GitHub_Trending/ru/rust导读特性选择trait selection是 Rust 编译器将每个特性引用trait reference与具体 impl 配对的核心过程而缓存是保证这一过程高效进行的关键。本文以 rustc-dev-guide 中《Caching and subtle considerations therewith》一章为基础结合当前仓库中rustc_trait_selection与rustc_middle的实际源码系统讲解 rustc 特性选择缓存的设计动机、占位符placeholder键变换机制、本地缓存与全局缓存的区分原则、候选缓存selection cache与求值缓存evaluation cache的职责划分以及临时缓存provisional cache如何处理递归求值中的环。读完本文你将理解 rustc 如何在类型变量尚未完全确定的情况下安全复用选择结果并能循着源码路径深入验证每一个设计决策。为什么特性选择结果需要缓存在 特性解析总览 中特性选择被定义为为每个特性引用配对 impl的过程例如对于泛型函数fn fooT: Clone(x: T)当实例化为T Veci32时编译器需要找到implT: Clone Clone for VecT这一条 impl 并解析其 where 子句。如果每遇到一个特性引用都从头遍历所有候选 impl、逐条评估编译开销会成倍放大——同一特性引用如i32: Copy在大型 crate 中可能出现成千上万次。因此 rustc 采用缓存策略将选择结果保存下来以便复用。然而缓存面临一个核心难点在类型变量尚未完全确定时我们也希望缓存生效。文档caching.md明确指出特性选择过程本身可能同时在对类型变量施加影响unification因此缓存不仅要保存选择过程的结果还要能在命中后重放replay其对类型变量的副作用。这正是缓存设计中最微妙的部分。核心机制用占位符替换未绑定推断变量缓存查找必须基于确定性的键。假设有一个特性引用usize : Foo$t其中$t是未绑定的推断变量inference variable。若直接以$t作为键由于推断变量的编号与状态变化无常两次看似相同的查询可能产生不同的键缓存将形同虚设。文档给出的方案是先将所有未绑定的推断变量替换为占位符placeholder版本。例如usize : Foo$t被替换为usize : Foo$0其中$0是占位类型。随后以usize : Foo$0为键查询缓存。命中cache hit缓存直接给出选择过程的下一步动作例如应用 impl #22或应用 where 子句X : FooY。未命中cache miss从零开始走完整的选择过程得到结论后把结果写入缓存。一个完整示例缓存命中与副作用重放沿用文档中的例子。假设我们从未命中开始通过选择过程得出结论唯一可能的 impl 是impl Fooisize for usize { ... } // Impl #22于是在缓存中记录usize : Foo$0 ImplCandidate(22)随后执行确认confirmation阶段ImplCandidate(22)被确认作为副作用把$t与isize统一。稍后编译器再次遇到usize : Foo$u。将其中的推断变量替换为占位符后得到usize : Foo$0与之前完全一致因此缓存命中直接返回ImplCandidate(22)。确认阶段再次作为副作用把$u与isize统一——缓存不仅复用了选择结论还重放了它对类型变量的约束效果这正是前文强调的可重放副作用在实践中的体现。本地缓存与全局缓存的区分一个微妙的交互在于特性查找的结果会随作用域内可见的 where 子句而变化。同样的T: Clone在fn fT: Clone(...)内部可以通过 where 子句满足而在没有该 where 子句的作用域内则必须依赖具体 impl。因此 rustc 维护了两个缓存本地缓存local cache附着于ParamEnv键中携带参数环境结果可能依赖作用域内的 where 子句。全局缓存global cache附着于tcx即TyCtxt全局上下文键中不携带完整参数环境结果与 where 子句无关。当结果可能受作用域内 where 子句影响时使用本地缓存反之优先使用全局缓存以获得跨作用域、跨 crate 的复用率。判据演进从pick_candidate_cache到can_use_global_caches文档中提到判据函数pick_candidate_cache并标注了 TODO看起来pick_candidate_cache已不存在本节是否仍然准确通过检索当前仓库源码可以确认该函数确实已被重构移除取而代之的是 select/mod.rs 中的can_use_global_caches方法。但文档描述的简单、保守的规则在精神上得到了继承fn can_use_global_caches( self, param_env: ty::ParamEnvtcx, pred: ty::PolyTraitClausetcx, ) - bool { // If there are any inference variables in the ParamEnv, then we // always use a cache local to this particular scope. Otherwise, we // switch to a global cache. if param_env.has_infer() || pred.has_infer() { return false; } ... }见 compiler/rustc_trait_selection/src/traits/select/mod.rs从源码结构看当前规则比文档描述的有 where 子句就用本地缓存更加精细只要参数环境或谓词中还存在推断变量has_infer()就强制使用本地缓存因为推断变量可能随后被统一导致全局缓存结果失效。只有当参数环境与谓词都不含推断变量时才允许进入全局缓存分支。此外还有两类显式避开全局缓存的场景见can_use_global_caches后续代码相干性检查TypingMode::Coherence避免相干性分析的结果污染全局主缓存由于相干性检查执行较快不值得为提高命中率冒此风险。定义 opaque 类型opaque type时opaque 的隐藏类型hidden type可能影响候选选择结果因此在定义 opaque 类型的过程中禁用全局缓存。这与文档所述的历史教训一脉相承rustc 曾试图做更细粒度的区分但引发了一连串令人头疼的 bug如 #18290、#22019最终回归简单且安全的规则。文档指出该简单规则在编译 rustc 自身时命中率高达约95%这一数据至今仍被视作该策略有效性的重要佐证注文档中该数据为历史观测值当前版本源码注释中未再重复给出。两类缓存的源码实现selection cache 与 evaluation cache深入源码可以发现所谓选择结果缓存在实现上被拆成了两个具体的数据结构二者都定义在 compiler/rustc_middle/src/traits/select.rspub type SelectionCachetcx, ENV WithDepNodeCache(ENV, ty::TraitClausetcx), SelectionResulttcx, SelectionCandidatetcx; pub type EvaluationCachetcx, ENV WithDepNodeCache(ENV, ty::PolyTraitClausetcx), EvaluationResult;SelectionCache键为(环境, TraitClause)值为SelectionResultSelectionCandidate。它缓存的是选择结果——即特性引用最终由哪类候选满足如ImplCandidate、ParamCandidate等这正是文档示例中usize : Foo$0 ImplCandidate(22)对应的数据结构。其底层容器WithDepNodeCache定义于 compiler/rustc_middle/src/traits/cache.rs在FxHashMap之上额外携带了DepNodeIndex用于增量编译的依赖追踪。EvaluationCache键为(环境, PolyTraitClause)值为EvaluationResult。它缓存的是求值结果——即某个谓词是否成立EvaluatedToOk/EvaluatedToErr/EvaluatedToAmbig等不关心具体由哪条 impl 满足。两个缓存在SelectionContext中的使用路径分别对应 select/mod.rs 中的check_candidate_cache/insert_candidate_cache围绕candidate_from_obligation与check_evaluation_cache/insert_evaluation_cache围绕evaluate_predicate_recursively。求值evaluation只判断义务是否成立而不约束推断变量因此其缓存值无需重放副作用而选择selection结果后续还要进入确认阶段确认过程的统一副作用必须按上文方式重放。缓存在GlobalCtxt中的挂载位置全局缓存实际存放在GlobalCtxt的GlobalCaches结构中见 compiler/rustc_middle/src/ty/context.rspub struct GlobalCachestcx { ... /// Caches the results of trait selection. This cache is used /// for things that do not have to do with the parameters in scope. pub selection_cache: traits::SelectionCachetcx, ty::TypingEnvtcx, /// Caches the results of trait evaluation. This cache is used /// for things that do not have to do with the parameters in scope. /// Merge this with selection_cache? pub evaluation_cache: traits::EvaluationCachetcx, ty::TypingEnvtcx, /// Caches the results of goal evaluation in the new solver. new_solver_evaluation_cache: Locksearch_graph::GlobalCacheTyCtxttcx, new_solver_canonical_param_env_cache: Lockty::CanonicalParamEnvCacheTyCtxttcx, ... }值得注意的两点源码注释中evaluation_cache旁留有疑问Merge this withselection_cache?说明 rustc 开发者自己也认为两个缓存有合并潜力但当前仍保持分离设计。GlobalCaches中同时存在**新求解器next-generation trait solver**专用的new_solver_evaluation_cache与new_solver_canonical_param_env_cache。这印证了 rustc 正在向新的特性求解器迁移新旧两套缓存体系在全局上下文中并存。缓存读写如何选择本地与全局在SelectionContext中check_candidate_cache与check_evaluation_cache都遵循同样的二分逻辑见 select/mod.rs若can_use_global_caches(param_env, trait_pred)为真则从tcx.caches.selection_cache/tcx.caches.evaluation_cache读取键为(typing_env(param_env), trait_pred)否则从infcx.evaluation_cache/infcx.selection_cache这一推断上下文InferCtxt本地的缓存读取。写入侧insert_candidate_cache、insert_evaluation_cache同样根据该判据决定写入哪个缓存。由此本地缓存在实现上即InferCtxt持有的缓存全局缓存即TyCtxt持有的缓存与文档local cache attached to ParamEnv、global cache attached to tcx的描述在语义上一一对应——只是如今ParamEnv的角色由can_use_global_caches的has_infer检查与键中的TypingEnv共同承担。求值缓存与递归环临时缓存Provisional Cache特性求值需要递归评估T: Trait时可能产生嵌套义务如 impl 的 where 子句而这些嵌套义务又可能回溯到正在评估的谓词形成环。若直接向全局缓存写入环参与者的结果可能得到错误结论若完全禁止缓存又会导致指数级重复计算。为此SelectionContext引入了临时缓存ProvisionalEvaluationCache其完整定义见 select/mod.rs。其设计要点每个缓存条目记录结果之外还保存深度优先编号DFNdepth-first number与达到的栈深度reached_depthstruct ProvisionalEvaluation { from_dfn: usize, reached_depth: usize, result: EvaluationResult, }若某个结果依赖栈中更浅的节点即参与递归环则标记为PROVISIONAL临时仅写入临时缓存不写入全局求值缓存见evaluate_predicate_recursively中CACHE MISS/PROVISIONAL分支select/mod.rs。当评估失败error时通过 DFN 清理对应子树写入的所有临时结果当栈弹出到reached_depth时剩余临时结果可提交commit为正式缓存条目。源码注释中还记录了一个边界情形当推断上下文已被错误污染tainted_by_errors时即使结果看起来依赖栈也允许写入全局缓存以避免在出错场景下出现指数级重复求值导致编译器挂起见注释中引用的 issue #150907——因为编译注定失败牺牲精度换取性能是安全的。这套 DFN reached_depth 的机制本质上是文档结果可能依赖栈这一微妙考量的工程化实现能脱离栈独立成立的结果才允许进入全局缓存环内结果只停留在临时缓存中直至环被消解。从缓存视角理解新的特性求解器前文多次出现的new_solver_*缓存字段指向 rustc 的下一代特性求解器next-generation trait solver即rustc_trait_selection/src/solve目录下的新实现。与旧求解器在SelectionContext中手工维护本地/全局二分不同新求解器采用规范化canonicalization 搜索图search graph全局缓存的策略目标首先被规范化为CanonicalInput见 compiler/rustc_middle/src/traits/solve.rs再以规范化后的输入为键查全局缓存。源码注释同文件还记录了一项重要收益对CanonicalInput做驻留interning后在求解器递归深度溢出的场景下编译 bevy_render 的内存占用从约 14GiB 降至约 4GiB对应 issue #161748 的修复。新求解器的出现并不否定本文介绍的缓存原理——替换推断变量为规范形式、缓存结果、处理递归环的三大支柱在新旧求解器中一脉相承只是实现载体从SelectionContext手工缓存演变为搜索图全局缓存。小结缓存设计的三个核心结论键必须稳定通过将未绑定推断变量替换为占位符使usize : Foo$t与usize : Foo$u映射到同一缓存键实现跨查询复用并在命中后重放确认阶段的统一副作用。结果必须与环境解耦或显式区分结果依赖 where 子句的查询走本地缓存与作用域无关的查询走全局缓存。当前判据can_use_global_caches要求参数环境与谓词均不含推断变量并在相干性检查与 opaque 类型定义阶段保守禁用全局缓存换取正确性与约 95% 的高命中率编译 rustc 自身时的历史观测值。递归环必须特殊处理环参与者的求值结果先进入带 DFN 与 reached_depth 的临时缓存失败时按 DFN 回滚环消解后提交避免指数级重复计算又不污染全局缓存。若要进一步验证上述内容建议按以下路径阅读源码缓存机制总入口compiler/rustc_trait_selection/src/traits/select/mod.rscandidate_from_obligation、evaluate_predicate_recursively、can_use_global_caches、ProvisionalEvaluationCache缓存数据结构定义compiler/rustc_middle/src/traits/select.rs 与 compiler/rustc_middle/src/traits/cache.rs全局缓存挂载位置compiler/rustc_middle/src/ty/context.rs 中的GlobalCaches新旧求解器缓存对比compiler/rustc_middle/src/traits/solve.rs 与 compiler/rustc_trait_selection/src/solve特性解析背景特性解析总览 与 参数环境说明【免费下载链接】rustEmpowering everyone to build reliable and efficient software.项目地址: https://gitcode.com/GitHub_Trending/ru/rust创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表