的吞吐量优化设计)
深入解析 .NET RyuJIT 线性扫描寄存器分配器LSRA的吞吐量优化设计【免费下载链接】runtime.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.项目地址: https://gitcode.com/GitHub_Trending/runtime6/runtime导读本文基于 docs/design/coreclr/jit/lsra-throughput.md 设计文档系统梳理 .NET RuntimeRyuJIT 后端线性扫描寄存器分配器Linear Scan Register AllocatorLSRA在吞吐量编译速度方面已知的次优环节以及作者提出的重构路线如何用更干净的containedness表示、把寄存器需求规格化并入RefPosition构建流程、并最终消除gtLsraInfo这一临时通信结构。读者读完可以掌握 RyuJIT 寄存器分配管线的整体数据流Lowering→TreeNodeInfo→RefPosition→ 寄存器选择理解GTF_CONTAINED标志与IsContained()的真实语义并了解当前仓库源码中这些设计的落地情况。背景LSRA 在 RyuJIT 中的角色在 .NET Runtime 的 RyuJIT 后端中寄存器分配Register AllocationRA是Lowering之后、CodeGen之前的关键优化与规整阶段其实现集中在 src/coreclr/jit/lsra.cpp 与 src/coreclr/jit/lsra.h以及各架构的lsraarm64.cpp、lsraxarch.cpp、lsrariscv64.cpp等文件。LSRA 的核心工作可概括为为每个节点的定义/使用建立RefPositionLinearScan::buildRefPositionsForNode()定义于 src/coreclr/jit/lsra.h为每个树节点生成相应的引用位置为局部变量lclVar与树临时量tree temps维护生命周期区间interval线性扫描按程序点顺序为每个区间挑选物理寄存器必要时生成 spill/reload。该设计文档指出当前实现的吞吐量即编译器本身运行 LSRA 阶段所消耗的时间存在多处可优化空间并且其中多数优化点彼此关联牵一发而动全身。当前实现中的六个次优环节文档首先列出了 LSRA 当前实现中六类已知的次优之处1. 额外的节点枚举遍pre-pass在TreeNodeInfoInit遍之前存在一次独立枚举节点的额外遍历。文档作者并不确定这次预遍历是否必须与TreeNodeInfoInit分离需要进一步调查。从源码结构看各架构的TreeNodeInfoInit逻辑已被拆分为独立的lsraarm.cpp、lsraarm64.cpp、lsraxarch.cpp、lsrariscv64.cpp等文件见 src/coreclr/jit 目录这正对应文档中提取TreeNodeInfoInit方法到独立lsra{arch}.cpp文件的重构动作。2. containment 识别的双重表示与重复计算containment包含是指某个节点的结果计算可以被折叠进其父节点例如 load/store 的地址计算被折叠进访存指令。当前实现中识别发生在Lowering阶段通信通过节点上gtLsraInfo字段完成——该字段平时不被使用重复当为节点构建RefPosition时containment 信息实际上被重复表达了一次检查开销IsContained()至少在每个节点执行一次在CodeGen::genCodeForTreeNode()开头当判断当前节点操作数是否 contained 时还会再检查。文档提出两条改进方向用更高效的 containment 表示把分析留在Lowering那里已有父上下文、便于做既有变换同时简化检查或者在构建RefPosition的过程中完成 containment 分析但后面会说明为何最终没有走这条路。3. 寄存器需求的规格化时机寄存器需求source、destination 以及任何 internal register 的寄存器掩码是在Lowering的最后一趟中规格化的且本质上需要更多空间。文档指出关键洞察对新寄存器定义节点目的地或 internal register的需求是独立于父节点的因此这部分可以在LinearScan::buildRefPositionsForNode()中完成无需像 contained 节点识别那样做双重遍历。4. lastUse 位的单独遍历RefPosition构建完成后还需再遍历一遍以设置 lastUse 位。之所以单独做是因为当前gtNext/gtPrev链接与真实代码生成顺序之间存在不一致。文档建议一旦该问题解决lastUse 位应由活跃性分析liveness在寄存器分配之前设置对应 issue #7256。5. RefPosition 全部提前创建所有RefPosition都在寄存器分配遍开始前一次性创建但其中只有 lclVar 的RefPosition真正需要提前存在——因为 lclVar 与树临时量不同可能有多处定义、跨基本块存活。树临时量的RefPosition理论上可以按需即时创建on-the-fly从而节省内存并改善局部性对应 issue #7257。6. 候选寄存器循环缺少短路LinearScan::tryAllocateFreeReg()与LinearScan::allocateBusyReg()中对所有候选寄存器的循环可以在找到最优得分寄存器时提前短路退出。此外在 MinOpts最小优化模式下甚至可以一旦找到合适候选就短路——当然这需要在吞吐量收益与代码质量影响之间权衡。仓库佐证当前 src/coreclr/jit/lsra.cpp 的寄存器选择逻辑中已有// Well set this to short-circuit remaining heuristics when we have a single candidate的注释表明候选收敛后短路剩余启发式这一方向已部分落地启发式评分体系定义在 src/coreclr/jit/lsra_score.h如BUSY_REG_SEL_DEF(PREV_REG_OPT, ...)等。表示 Containedness核心重构提案针对上述问题 2/3文档给出了详细的重构方案。作者最初的计划是将TreeNodeInfoInit遍的功能与RefPosition构建合并并彻底消除gtLsraInfo。但在把TreeNodeInfoInit方法提取为独立lsra{arch}.cpp文件的过程中作者意识到如果先把 containment 分析放进LinearScan之后再把它拉回Lowering会产生大量返工throw-away work。同时containedness 的当前表示并不干净Lowering阶段通过对节点行为的隐含知识 gtLsraInfo.dstCount来传达CodeGen阶段又通过相似的隐含节点特征 寄存器有无来判断。于是文档提出如下改进提案 A引入树根标志GTF_TREE_ROOT为每个节点添加一个标志指示它是否为树根tree root为了腾出该标志位提议在非LEGACY_BACKEND上消除GTF_REG_VAL。这需要一些额外清理但可以顺带消除一批 hack——这些 hack 存在的原因在于emitter 原本是为动态分配寄存器的代码生成器设计的生成完代码后设置该标志表示已放入寄存器而 RyuJIT 后端是在生成代码之前就完成寄存器分配两者模型不匹配。提案 B定义新的寄存器值语义寄存器值由谁赋值语义REG_UNKLowering需要寄存器must have a registerREG_OPTLowering定义处与使用处寄存器均为可选REG_OPT_USELowering定义处需要寄存器使用处可选REG_OPT_DEF可能Lowering文档认为可能也需要可作为补充完成上述表示后IsContained()可以被大幅简化。备选方案直接用GTF_CONTAINED标志位文档也指出或许更有效的做法是直接用额外的一个位作为真正的GTF_CONTAINED标志这值得考虑但初期用GTF_TREE_ROOT来简化 containedness 检查更容易落地——因为无需改动所有当前标记节点为 contained 的代码点。仓库现状印证从当前 src/coreclr/jit/gentree.h 可以看到GTF_CONTAINED 0x00000040, // This node is contained (executed as part of its parent)并配套IsContained()gentree.h、SetContained()gentree.h、ClearContained()gentree.h等方法。可以推断最终实现选择了直接使用GTF_CONTAINED标志这条路线而非初期设想的GTF_TREE_ROOT间接方案——这也说明文档中的备选讨论确实影响了后续实现方向。把 Containedness 分析与 Lowering 合并一旦上述表示改造完成就可以把设置 containedness 的代码移入Lowering的第一趟first pass。文档坦诚地指出这里很可能存在一些阶段顺序phase ordering挑战但作者认为这些挑战并非不可逾越。这一步的价值在于containment 的判定本质上是子节点能否折叠进父节点的父上下文问题而Lowering正是拥有父上下文的阶段把判定留在Lowering既避免了在LinearScan中重复推导隐含的节点行为知识也让CodeGen侧的检查如genCodeForTreeNode()开头的IsContained()判断变得更加直白。消除 gtLsraInfoIssue #7225在 containedness 改造完成之后gtLsraInfo就只剩下一个职责传达寄存器需求register requirements。因此文档给出最终目标仍保留TreeNodeInfo数据结构与TreeNodeInfoInit()方法但改为在LinearScan::buildRefPositionsForNode()处理每个节点时按需调用它们这样就不再需要Lowering最后一趟写入gtLsraInfo、LSRA 再读出来重复构建的双重传递gtLsraInfo字段可以被彻底删除对应 issue #7225。这与文档前文寄存器需求新定义部分独立于父节点、可在构建RefPosition时完成的论点互相呼应寄存器需求规格化天然适配逐节点、按需的构建模型而 containedness 分析因为依赖父上下文则应留在Lowering中。仓库现状与后续脉络从当前仓库源码可以观察到这次设计讨论的部分落地痕迹架构文件拆分各架构的 LSRA 构建/初始化逻辑已分散在 src/coreclr/jit/lsraarm.cpp、src/coreclr/jit/lsraarm64.cpp、src/coreclr/jit/lsraxarch.cpp、src/coreclr/jit/lsrariscv64.cpp 等文件中另有共享的 src/coreclr/jit/lsrabuild.cpp 承担RefPosition构建contained 标志独立成位GTF_CONTAINED已成为独立的节点标志位并配齐IsContained()/SetContained()/ClearContained()接口src/coreclr/jit/gentree.h寄存器选择短回路候选寄存器启发式选择中存在单一候选即短路的机制src/coreclr/jit/lsra.cpplastUse 体系RefPosition的 lastUse 语义贯穿分配主循环见 src/coreclr/jit/lsra.cpp 中大量refPosition.lastUse的分支判断其设置方式仍在演进。需要说明的是本设计文档写作时提到的gtLsraInfo字段、issue 编号#7225/#7256/#7257对应当时的代码状态在当前代码库中搜索gtLsraInfo已无匹配可以推断该字段在后续迭代中已被移除文档描述的重构目标基本完成。若读者想要核对具体实现细节建议以 src/coreclr/jit/lsra.cpp、src/coreclr/jit/lsra.h 与 src/coreclr/jit/lsra_score.h 的当前内容为准。小结吞吐量优化的三条主线把整份设计文档提炼为三条主线便于快速把握减少遍历次数合并冗余的节点预遍历把 lastUse 位的设置并入活跃性分析#7256把树临时量的RefPosition改为按需构建#7257避免一次性全量构建带来的内存与局部性开销让信息各归其位containment 分析留在拥有父上下文的Lowering用独立标志位最终为GTF_CONTAINED表示寄存器需求则下沉到LinearScan::buildRefPositionsForNode()按需计算从而消除gtLsraInfo#7225降低选择成本在寄存器选择启发式src/coreclr/jit/lsra_score.h中引入短回路命中最优/合适候选即提前退出尤其让 MinOpts 路径受益。这三条主线共同指向同一个目标在不牺牲寄存器分配质量的前提下让 LSRA 在大型方法上的编译时间显著下降这正是该设计文档作为 RyuJIT 后端性能优化路线图的核心价值所在。【免费下载链接】runtime.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.项目地址: https://gitcode.com/GitHub_Trending/runtime6/runtime创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考