资讯详情

资讯详情

.NET Runtime 中的 OSR(栈上替换)机制详解与调试指南

.NET Runtime 中的 OSR栈上替换机制详解与调试指南【免费下载链接】runtime.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.项目地址: https://gitcode.com/GitHub_Trending/runtime6/runtime导读本文基于 .NET Runtime 仓库中 OsrDetailsAndDebugging.md 展开系统讲解 OSROn Stack Replacement栈上替换技术——它让运行时可以在方法栈帧仍然活跃时把执行流从无优化的 Tier0 原生代码无缝切换到经过优化的 OSR 代码版本。文章覆盖 OSR 的设计动机、分层编译Tiered Compilation与 PGO 的配合、JIT 侧 patchpoint 的插入原理、运行时侧的 OSR 方法创建流程、帧布局与 Prolog/Epilog 细节以及一整套可落地的调试手段环境变量、日志、ETW、调试器行为并附带仓库源码级佐证适合 JIT 开发者以及希望深入理解 .NET 性能调优机制的读者。背景为什么需要 OSROSROn Stack Replacement描述的是运行时在方法仍有活跃栈帧的情况下将控制流从一份原生代码版本转移到另一份原生代码版本的能力。它的核心目标是让运行时一开始以无优化的方式 JIT 大多数方法快速启动当这些方法的调用变得计算密集时再通过 OSR 把控制流转移过去。转移过程中活跃的调用会在栈上被改写为正确的帧形状与代码引用。从这个角度看OSR 与分层编译Tiering非常相似但二者有本质区别分层编译只能在方法被调用时切换原生代码版本。如果用户把全部代码都放在Main的循环里Main只会被调用一次分层编译将永远没有机会更新正在执行的代码。OSR 恰恰解决了这种长驻方法无法升级代码版本的问题。OSR 与EnCEdit and Continue编辑并继续也有相似之处但关键差异在于EnC 的转移由用户通过调试器发起的编辑触发且通常意味着方法的 IL 版本发生了变化而 OSR 的转移由运行时自主协调转移到的目标方法是对同一份 IL 代码版本的不同原生代码编译结果。在 .NET Runtime 的具体实现中OSR 用于从 Tier0快速 JIT、未优化原生代码版本切换到经过优化的 OSR 版本。OSR 版本的原生代码在很多方面都与众不同下文会详细展开。分层编译Tiered CompilationOSR 构建在分层编译之上。分层编译是运行时与 JIT 的一项联合特性方法首先被快速 JIT不优化如果被频繁调用之后在后台线程上重新 JIT 为优化版本。这样应用既能快速启动前期 JIT 时间少又能在稳定期获得完整的稳态性能。分层编译对方法采用不同的处理策略常规情况方法先被快速 JITTier0在被频繁调用后后台线程重新 JITTier1。预编译prejitted方法预编译代码直接充当 Tier0 代码。部分方法绕过分层编译首次 JIT 即优化标记了AggressiveOptimization特性的方法动态方法Dynamic methods以及来自可回收程序集collectible assemblies的方法在 .NET 3.0 / 5.0 / 6.0 中默认情况下包含循环的方法。最后一条策略由DOTNET_TC_QuickJitForLoops控制见下文 QJFL 小节。分层编译相关代码在仓库中位于 src/coreclr/vm例如 onstackreplacement.cpp 中的OnStackReplacementManager以及 JIT 侧的 importer.cpp 等文件中。动态 PGO 与 Full PGO动态 PGODynamic PGO在 .NET 6.0 中引入同样依赖分层编译。在动态 PGO 下任何在 Tier0 被 JIT 的方法其 Tier0 版本都会额外插入插桩代码来收集 profile 数据当方法在 Tier1 重新编译时这些数据可供 JIT 使用。Full PGO是动态 PGO 的一个变体它强制所有方法都经过 Tier0以便全部收集 profile 数据。Quick Jit For LoopsQJFL分层编译在 .NET Core 3.0 中引入。最初所有被 JIT 的方法都经过 Tier0但开发过程中收到了大量反馈性能敏感的用户代码可能被困在 Tier0 版本中造成不良的性能影响。作为回应默认行为被改为包含循环的方法首次即优化不参与分层 JIT。这种行为的速记写法是QJFL0。虽然QJFL0解决了用户当时遇到的即时问题但它也有副作用并非所有带循环的方法都性能敏感很多情况下启动 JIT 时间增加却没有任何稳态收益。在某些启动 JIT 时间占比显著的应用中影响可达 20% 左右。事实上过早优化方法反而可能降低稳态性能原因有二类构造器class initializer可能尚未执行早期优化的方法必须执行类构造器检查也无法读取只读静态字段动态 PGO 只能作用于经过 Tier0 的方法。可以通过将 QJFL 设为 1 来改变默认行为一些启动敏感型应用例如Powershell正是这样做的。启用 OSR 的理由OSR 允许运行时让大多数方法以 Tier0 JIT并在需要时从 Tier0 代码转移从而可以把默认行为改为QJFL1。启用 OSR 带来的收益包括普遍更好的启动性能某些场景提升可达 20% 以上改进的稳态性能动态 PGO 的收益提升缩小动态 PGO 与 Full PGO 之间的差距。OSR 如何工作启用 OSROSR 通过两个环境变量开启DOTNET_TC_QuickJitForLoops1 (即 QJFL1) DOTNET_TC_OnStackReplacement1 (即 OSR1)同时受以下配置影响DOTNET_TieredPGO1 使 Tier0 进入 BBINSTR1即插桩模式哪些方法具备 OSR 资格OSR Eligible运行时在 Tier0 发起 JIT 请求时会传入 QJFL、OSR、BBINSTR 的当前值。JIT 对方法做一次初始 IL 扫描检查以下几点见 importer.cpp 中的compHasBackwardsBranch逻辑是否存在任何词法上向后跳转的 IL 分支compHasBackwardsBranch是否存在位于 catch / filter / finally / fault 区域中的词法向后分支即handler 中的循环是否存在localloc该方法是否为 reverse PInvoke是否存在tail.前缀的调用。随后按如下规则决策QJFL0 compHasBackwardsBranchJIT 将优化级别改为完全优化并通知运行时该方法不再参与分层编译。这是旧版本中的默认行为。QJFL1 OSR0 !compHasBackwardsBranchJIT 以 Tier0 编译该方法。QJFL1 OSR1 compHasBackwardsBranchJIT 继续检查方法是否包含 handler 中的循环、localloc或为 reverse PInvoke。若都没有方法即OSR Eligible否则 JIT 将方法切换为优化编译。若存在tail.前缀调用且BBINSTR0JIT 会覆盖以上所有规则将方法切换为优化编译。Tier0 下 OSR Eligible 方法的 JIT 过程在导入importation阶段JIT 寻找作为词法回边目标的块并将其标记为需要 patchpoint。注意在合法 IL 中这些点必须是栈空点stack empty points。在 patchpoint 阶段如果存在被标记的块JIT 会为方法新增一个整型局部变量patchpoint 计数器在方法入口处添加 IR将计数器初始化为DOTNET_TC_OnStackReplacement_InitialCounter的值默认1000在每个被标记的块处插入计数器减 1若归零或为负则条件调用CORINFO_HELP_PATCHPOINT的代码。helper 的参数是块的 IL offset 以及计数器的地址。编译其余部分正常进行但方法完成后JIT 会创建一个patchpoint descriptor记录方法的帧大小、所有 IL 局部变量的虚拟偏移以及其他关键帧信息如泛型上下文等的偏移。该 descriptor 被传回运行时与方法的 debug info 一同存储。这里依赖一个关键事实Tier0 方法不做优化在栈空点不会把任何 IL 状态保存在寄存器中IL 状态与栈槽存在一一对应关系。因此单个 patchpoint descriptor 即可服务方法内所有 patchpoint无需做 liveness 分析来确定其内容。一个 Tier0 方法可能包含很多 patchpoint。当前实现中运行时可能为每个 patchpoint 各创建一个 OSR 方法而每个 OSR 方法可能涵盖原方法的大部分乃至全部代码。因此最坏情况下会产生大量 OSR 代码但目前认为这类情况很少见未来可能需要实现某种策略来避免。带 Patchpoint 的 Tier0 代码执行过程当带有 patchpoint 的 Tier0 方法被调用时每次到达 patchpoint 都会递减每次调用独立的patchpoint 计数器。计数器归零时代码调用CORINFO_HELP_PATCHPOINT运行时 helper。helper 以调用处的返回地址为键在 patchpoint info 表中查找第一次看到该 patchpoint 时运行时向表中添加一个条目并用DOTNET_OSR_CounterBump当前为0x1000决定的值重新装载局部计数器。若某 offset 的 patchpoint 命中次数超过DOTNET_OSR_HitLimit当前为0x10即 16运行时将为该 offset 创建 OSR 方法。这意味着单次调用中某 offset 在约 (0x1000 × 0x11) ≈ 69,000 次执行后即可触发 OSR 创建。这个数字是在继续执行 Tier0 代码的性能损失与创建优化 OSR 版本的一次性成本 后续在 OSR 代码中执行带来的收益之间权衡后选择的未来可能还需调整。OSR 方法由命中阈值的那个线程同步创建。若 OSR 方法创建期间有第二个线程到达其计数器会被重新装载并继续执行 Tier0 代码之后它可能还会再次回来。方法就绪后发起线程被转移到 OSR 方法随后其他活跃帧到达该 patchpoint 时也会陆续转移。需要强调的是OSR 方法不会把控制权交还给 Tier0 方法它是 Tier0 方法在该 patchpoint 处的完全延续。在运行时侧这一整套逻辑实现在 src/coreclr/vm/jithelpers.cppJIT_PatchpointHCIMPL见 L1714 附近负责查询 patchpoint 信息并决定是否转移JitPatchpointWorkerL1269负责为方法创建新的原生代码版本并调用 JIT 编译 OSR 版本。其中ilCodeVersion.AddNativeCodeVersion(pMD, NativeCodeVersion::OptimizationTier1OSR, ...)明确使用OptimizationTier1OSR标记 OSR 原生代码版本与 Tier1 同属优化层。若暂不转移如计数器未达阈值helper 会返回跳过跳转指令的地址x64 上为ip 2arm64/loongarch64/riscv64 上为ip 4让 Tier0 代码继续执行。OSR 方法的创建OSR 方法特定于某个 Tier0 方法在特定 IL offset 处的状态。运行时决定创建 OSR 方法时会调用 JIT 并传入特殊的 OSR 标志与该 IL offset。这次编译与普通优化编译相似但有几个特殊之处导入从指定 IL offset 开始拉入从该点可达的全部代码。通常但并非总是方法入口IL offset 0不可达因此 OSR 方法只导入完整方法的子集。控制流通常从第一个块fgFirstBB分支到 OSR IL offset 处的块即 OSR entry。当 OSR entry 位于 try 中间或嵌套 try 中时需要特别小心。若启用了动态 PGOOSR 方法会读取 Tier0 方法捕获的 PGO 数据同时也会对 OSR 方法插桩以确保之后生成的 Tier1 版本能看到在强制方法停留在 Tier0情况下本应看到的所有 PGO 数据。JIT 可能还需要导入入口块即使从 OSR IL offset 可达的 IL 中没有显式分支到 offset 0——例如尾递归可能形成回到方法入口的循环。这曾是若干隐蔽 bug 的来源最终可能循环回方法入口的调用点可能来自**内联函数inlinee或去虚拟化devirtualization**等场景。大多数情况下JIT 把 OSR 方法当作普通优化 JIT 方法来处理。但除了导入、帧布局、PGO、prolog/epilog 生成之外仍有少数地方需要特殊处理OSR 方法永远不需要对局部变量做零初始化也永远不会看到未定义的局部变量值——这些都交给 Tier0 帧处理。OSR 方法仍可能需要清零自己分配的临时变量。帧布局Frame LayoutOSR 方法的帧在逻辑上合并了 Tier0 方法的帧。由于 OSR 方法有不同的寄存器保存方式和不同的临时变量其帧在 Tier0 帧之外还有一个 OSR 专属的扩展部分。在x64上Tier0 帧总是 RBP 帧在arm64上Tier0 帧可以是 JIT 能创建的 5 种帧类型中的任意一种。x64 上帧指针如需要指向OSR 扩展部分的基址arm64 上帧指针指向Tier0 帧的基址。这种差异是历史原因造成的未来很可能应让 x64 对齐 arm64 的做法。与 Tier0 参数或局部变量对应的、在 OSR 方法中有栈归属的局部变量即lvaIsOSRLocal在 OSR 方法中会使用Tier0 的槽位作为其栈归属。因此 OSR 方法在整个执行过程中访问 Tier0 帧的某些部分是常见现象这些槽位按需向 GC 报告。不过在转移和 OSR 方法 prolog 执行之后Tier0 帧的很大部分实际上已经死亡——Tier0 的活跃状态已被 OSR 方法装入寄存器。OSR PrologOSR prolog 在概念上与普通方法 prolog 类似但有几点关键差异OSR 方法通过Tier0 方法的跳转进入因此 Tier0 方法用到的callee-saved 寄存器可能需要特殊处理在x64上OSR 方法把 callee-saved 寄存器的原值保留在 Tier0 帧中由 epilog 直接恢复不需要任何额外指令在其他目标平台上Tier0 用到的 callee-saved 寄存器在 prolog 中先被恢复然后按正常方式再次保存到 OSR 帧中。以上逻辑在genOSRHandleTier0CalleeSavedRegistersAndFrame中实现。OSR 方法还必须从 Tier0 帧的对应槽位初始化所有 live-in 的寄存器化参数或局部变量这由genEnregisterOSRArgsAndLocals完成。若 OSR 方法需要报告泛型上下文它使用Tier0 帧的槽位为保证这一点带 patchpoint 的 Tier0 方法被强制总是报告其泛型上下文。OSR 方法不需要注册函数入口回调因为它永远不会被调用也不需要获取同步方法监视器Tier0 帧已经做过。在 JIT 侧这两个关键函数的声明位于 codegen.hgenEnregisterOSRArgsAndLocals见 L362genOSRHandleTier0CalleeSavedRegistersAndFrame见 L471实现分布在各平台文件中例如 codegencommon.cpp 的genEnregisterOSRArgsAndLocalsL4321 附近、codegenarm64.cppL5557 附近与 codegenarm.cppL1791 附近的genOSRHandleTier0CalleeSavedRegistersAndFrame并在 prolog 生成路径codegencommon.cpp L5619、L5880 附近中被调用。OSR EpilogOSR epilog 执行以下操作撤销 OSR 对帧的贡献SP 增加部分恢复由 OSR prolog 保存的 callee-saved 寄存器撤销 Tier0 帧SP 增加部分恢复 Tier0 需要的 callee-saved 寄存器仅 x64恢复 RBP返回到 Tier0/OSR 方法的调用者。由于存在两次 SP 调整该 epilog 的格式并不标准——这在当时破坏了 x64 epilog 的 unwind。该问题已被修复修复方案记录在 OSRX64EpilogRedesign.md。而在 arm64 上由于有 epilog unwind 码第二次 SP 调整似乎不会引发问题。OSR 中的 FuncletsOSR 的 funcletsfunclets即 EH 处理块对应的独立代码段与普通 funclets 基本相同。OSR Unwind 信息prolog unwind 在 offset 0 处为 Tier0 帧包含一个幻影phantomSP 调整。如前所述x64 epilog 中的两次 SP 调整在 epilog 中 unwind 时会产生问题但在 prolog 和方法体中的 unwind 似乎工作正常unwind 码正确描述了所需操作。arm64 有 epilog 的 unwind 码这个问题不会出现。当 OSR 方法活跃时栈帧只显示该 OSR 方法而不显示 Tier0 方法。OSR GC 信息OSR 的 GC 信息是标准的唯一的特殊之处是某些特殊偏移如泛型上下文等可能引用Tier0 帧中的槽位。OSR 方法的执行OSR 方法永远不会被直接调用只能通过带 patchpoint 的 Tier0 方法跳转进入。在x64上为保证栈对齐prolog 会压入一个幻影返回地址x64 方法假定进入时 SP 满足 8 mod 16 对齐arm64 不需要这样做因为调用不压栈。OSR 方法返回时会同时清理自己的栈和 Tier0 方法的栈。值得注意的递归场景如果 Tier0 方法既递归又含循环会形成有趣的动态。大量循环后 OSR 方法被创建当前活跃的 Tier0 实例转移过去当 OSR 方法做递归调用时它调用的是 Tier0 方法后者随后会很快转移到刚创建的 OSR 版本。调试 OSR查看创建了哪些 OSR 方法设置DOTNET_JitDisasmSummary1OSR 方法会被特殊标记并附上启发式的 IL offset。例如用一些高强度的 OSR 设置在库测试上运行结果共 JIT 了 160,675 个方法其中 699 个是 OSR 方法。对 dump 输出 grep OSR最后几行形如Compiling 32408 System.Text.Json.Serialization.Converters.ObjectDefaultConverter1[WrapperForPoint_3D][System.Text.Json.Serialization.Tests.WrapperForPoint_3D]::OnTryRead, IL size 850, hash0x5a693818 Tier1-OSR 0x5f Compiling 32411 System.Text.Json.Serialization.Tests.ConverterForPoint3D::Read, IL size 40, hash0x294c33b5 Tier1-OSR 0xf Compiling 32412 System.Text.Json.Serialization.Converters.ObjectDefaultConverter1[WrapperForPoint_3D][System.Text.Json.Serialization.Tests.ConverterForPoint3D]::OnTryWrite, IL size 757, hash0x1ed8b727 Tier1-OSR 0x60 Compiling 32629 System.Text.Json.Serialization.Converters.ObjectWithParameterizedConstructorConverter1[KeyValuePair2][System.Collections.Generic.KeyValuePair2[System.__Canon,System.__Canon]]::ReadConstructorArgumentsWithContinuation, IL size 192, hash0x7ab2e686 Tier1-OSR 0x0 Compiling 32655 System.Text.Json.Serialization.Converters.ObjectWithParameterizedConstructorConverter1[Point_3D_Struct][System.Text.Json.Serialization.Tests.Point_3D_Struct]::ReadConstructorArgumentsWithContinuation, IL size 192, hash0xb37dcd36 Tier1-OSR 0x0其中0x5f之类的标注告诉你该 OSR 方法对应的 IL offset。顺带一提看到 offset 为0x0的 patchpoint很能说明示例是在随机 patchpoint开启的情况下运行的——因为 IL offset 0 很少是循环的起点。另外注意OSR 方法 JIT 完成后总是立即被执行所以上面每个 OSR 方法都至少执行过一次。控制哪些方法具备 OSR 资格DOTNET_TC_QuickJitForLoops0—— 没有方法具备 OSR 资格全部切换为优化编译DOTNET_TC_OnStackReplacement0—— 没有方法具备 OSR 资格全部以 Tier0 JITDOTNET_JitEnableOsrRange...—— 用方法哈希来限定哪些方法具备 OSR 资格DOTNET_JitEnablePatchpointRange...—— 用方法哈希来限定哪些 Tier0 方法被插入 patchpoint。使用Enable系列控制做二分搜索已被证明是追踪 OSR 代码生成问题的一种可靠方式。以上面同一个示例为例用DOTNET_JitEnableOsrRange00000000-7FFFFFFF运行时只创建了 249 个 OSR 方法。哈希在该范围之外、原本会由 OSR 处理的方法改为立即优化并绕过分层编译。因此你可以系统性缩小 OSR 方法集合来定位导致测试失败的那个或那几个方法。范围语法说明范围值用十六进制表示条目可以是区间如上例、单值也可以是区间与单值的并集例如DOTNET_JitEnableOsrRange00000000-3FFFFFFF,067a3f68,F0000000-FFFFFFFF实践中通常只需要单个区间。改变 OSR 方法的创建速率以下配置值可以改变运行时策略使 OSR 方法被更积极或更不积极地创建DOTNET_OSR_HitCountN—— 修改运行时在某个 offset 看到 helper 调用多少次后才创建 OSR 方法。值为 0 或 1 会急切地创建 OSR 方法。DOTNET_OSR_CounterBumpN—— 修改运行时执行的局部计数器重装载值。值越小带 patchpoint 的 Tier0 方法回调运行时的频率越高。DOTNET_TC_OnStackReplacement_InitialCounterN—— 修改 JIT 烘焙进 Tier0 方法的初始计数器值。值越小首次调用运行时越早发生。注意把这三个值都设为 0 或 1会在 patchpoint第一次命中时就创建 OSR 方法。如果方法有多个比如两个patchpoint可能需要调整这些设置以确保在给定运行中两个 OSR 版本都能被创建。追踪运行时策略设置以下环境变量DOTNET_LogEnable1 DOTNET_LogFacility0x00400000 DOTNET_LogLevel5 DOTNET_LogToConsole1即可记录 Tier0 方法调用CORINFO_HELP_PATCHPOINT的运行时行为。示例输出TID 4bdc2: Jit_Patchpoint: patchpoint [17] (0x0000FFFF1E5F3BB8) hit 1 in Method0x0000FFFF1EAD9130M (Xunit.JsonDeserializer::DeserializeInternal) [il offset 45] (limit 2) TID 4bdc2: Jit_Patchpoint: patchpoint [17] (0x0000FFFF1E5F3BB8) TRIGGER at count 2 TID 4bdc2: JitPatchpointWorker: creating OSR version of Method0x0000FFFF1EAD9130M (Xunit.JsonDeserializer::DeserializeInternal) at offset 45 TID 4bdc2: Jit_Patchpoint: patchpoint [17] (0x0000FFFF1E5F3BB8) TRANSITION to ip 0x0000FFFF1E5F6820 TID 4bdc2: Jit_Patchpoint: patchpoint [18] (0x0000FFFF1E5F6BE0) hit 1 in Method0x0000FFFF1EB03B58M (Xunit.JsonBoolean::.ctor) [il offset 0] (limit 2) TID 4bdc2: Jit_Patchpoint: patchpoint [18] (0x0000FFFF1E5F6BE0) TRIGGER at count 2 TID 4bdc2: JitPatchpointWorker: creating OSR version of Method0x0000FFFF1EB03B58M (Xunit.JsonBoolean::.ctor) at offset 0 TID 4bdc2: Jit_Patchpoint: patchpoint [18] (0x0000FFFF1E5F6BE0) TRANSITION to ip 0x0000FFFF1E5F6D00解读方括号中的编号[17]是已调用运行时 helper 的不同 patchpoint 的数量从运行时角度看它充当一种 patchpoint ID。hit只是 Tier0 方法对 helper 的一次调用TRIGGER表示这次命中使该 patchpoint 达到了DOTNET_OSR_HitCount限制此时开始创建 OSR 方法TRANSITION表示控制权从 Tier0 方法转移到 OSR 方法。这些日志输出与 jithelpers.cpp 中的实现一一对应JitPatchpointWorker在创建 OSR 版本时打印JitPatchpointWorker: creating OSR version of ...L1304helper 在返回 OSR 代码地址时打印Jit_Patchpoint: patchpoint [...] TRANSITION to ip ...L1704。还可以用以下配置进一步调整运行时策略DOTNET_OSR_LowIdDOTNET_OSR_HighId二者共同构成一个闭区间描述哪些 patchpoint ID 被允许TRIGGER从而TRANSITION。因此你也可以用它来控制在运行时侧创建哪些 OSR 方法而无需改动 JIT 行为。改变 Tier0 代码中 patchpoint 的放置位置DOTNET_JitOffsetOnStackReplacementoffset—— 只在给定 IL offset且为栈空点放置 patchpointDOTNET_JitRandomOnStackReplacementval—— 在正常 OSR patchpoint 之外额外在非 handler 块开头的栈空点随机放置 patchpoint。值表示概率百分比十六进制0不会添加任何额外 patchpoint0x64十进制 100会尽可能多地添加。后者被 OSR 压力测试stress配合低值的策略配置使用以创建大量 OSR 方法。控制 dump 内容通过名称或哈希启用 JIT dump会产生多次编译的 dumpTier0 一次、每个 OSR 方法各一次、Tier1 一次。可以通过以下配置控制DOTNET_JitDumpTier00—— 抑制所有 Tier0 JIT 请求的 dumpDOTNET_JitDumpAtOSROffsetN—— 只 dump 指定 IL offset 的 OSR JIT 请求。从 ETW 观察 OSROSR 方法在 ETW 的MethodJitting 事件中被特殊标记。目前该值尚未被 tracevent / PerfView / SOS 解析。在 SOS 中方法的 OSR 版本当前被标记为未知层级unknown tier。调试器下的 OSR在源码级单步调试带 patchpoint 的 Tier0 代码时到 OSR 的转移会在后台发生。由于 OSR 代码是优化过的而 Tier0 代码没有优化如果你恰好在 OSR 转移发生的那个点上单步可能会看到调试器显示的内容突然劣化。在 Tier0 上设置的断点会被重新应用到 OSR 方法但如果缺少 source→IL→native 的映射信息断点可能无法生效。在汇编级单步时可以看到 patchpoint helper 的调用等细节。栈上正常显示 OSR 方法除非执行恰好在 OSR epilog 中暂停该问题仍在解决中。对于一个已转移到 OSR 方法的 Tier0 方法栈上只会看到一个条目。OSR 与性能如前所述QJFL1与OSR1的组合通常能带来更好的启动性能以及可比或更好的稳态性能。但也要注意有可能在 OSR 方法中花费大量时间例如全部代码都在Main中的基准测试。总体而言OSR 方法的性能应与等效的 Tier1 方法相当。实践中观察到 ±20% 左右的波动原因主要有OSR 方法往往是完整 Tier1 方法的子集很多时候只包含一个循环。JIT 对孤立的单个循环生成的代码往往比对复杂方法中的单个循环更好。OSR 方法可能只看到部分的 PGO 数据因为 Tier0 方法的部分代码可能尚未执行。JIT 目前还不太擅长处理这种部分 PGO 覆盖的情况。对 BenchmarkDotNet 结果的影响BenchmarkDotNetBDN通常能很好地确保测量针对方法最优化版本通常是 Tier1 版本进行因此一般不预期 OSR 对 BDN 结果有太大影响。然而某些 BDN 配置方式会使其更可能测量到 OSR 方法或 OSR 与 Tier1 的组合——尤其是减少预热warmup迭代次数。默认情况下方法需被调用 30 次才会在 Tier1 重新 JIT如果 BDN 基准以少于 30 次迭代运行就可能出现并非所有方法都测量 Tier1 代码的情况而启用 OSR 时这往往意味着测量的是 OSR 方法的性能。这在以下两类基准中更常发生(a) 基准内部大量循环来拉长测量区间的耗时而非依赖 BDN 决定合适的迭代策略(b) 运行时间很长的基准BDN 判断无需很多迭代即可获得有意义时长的测量约 250 ms 左右。在性能仓库performance repo的配置中减少了预热迭代次数且部分基准同时属于上述两类。未来很可能需要调整这些基准以确保测量的是 Tier1 代码的性能。参考资料OSR 设计文档 —— 更完整的 OSR 设计说明部分内容可能略显过时。OSRX64EpilogRedesign.md —— x64 OSR epilog 重新设计的记录。OSR Next Steps Issuedotnet/runtime issue #33658—— 记录了 bring-up 期间遇到的问题、当前限制以及可回访的改进思路包含大量信息。延伸阅读仓库内相关实现JIT 侧 patchpoint 相关逻辑src/coreclr/jit/importer.cppcompHasBackwardsBranch检查、src/coreclr/jit/codegencommon.cppgenEnregisterOSRArgsAndLocals等。运行时侧 OSR 管理src/coreclr/vm/jithelpers.cppJIT_Patchpointhelper 与JitPatchpointWorker、src/coreclr/vm/onstackreplacement.cppOnStackReplacementManager。各平台的 OSR prolog 处理codegenarm64.cpp、codegenarm.cpp。【免费下载链接】runtime.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.项目地址: https://gitcode.com/GitHub_Trending/runtime6/runtime创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →