深入解析 .NET CoreCLR JIT 的 Perf Score:用动态执行成本度量替代代码大小
发布时间:2026/9/17 7:40:36来源:尧图网络
深入解析 .NET CoreCLR JIT 的 Perf Score用动态执行成本度量替代代码大小【免费下载链接】runtime.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.项目地址: https://gitcode.com/GitHub_Trending/runtime6/runtimePerf Score 是 .NET CoreCLR 运行时 JITRyuJIT引入的一项评估特性它不再依赖方法生成的总代码字节数这一静态指标而是估算方法在目标 CPU 上的动态执行成本从而更准确地衡量 JIT 优化改动对含循环等复杂方法的影响。本文以 Perf-Score.md 设计文档 为核心结合 src/coreclr/jit 下的实际实现说明 Perf Score 的动机、简化假设、指令成本模型、计算方法与查看方式帮助 JIT 开发者在做 Asm Diff 对比时用更合理的指标评估改动优劣。Perf Score 要解决的问题为什么代码大小不足以衡量 JIT 改动质量JITJust-In-Time编译器的开发者修改 JIT 代码后通常需要评估这个改动是否让生成的机器码更快。为此社区已有成熟的工具链jit-diffs快速找出两个不同 JIT 版本改动前/后生成汇编不同的方法jit-analyze对 jit-diffs 的结果排序给出代码大小code size增加/减少最多的方法列表。对很多方法而言代码更小 ≈ 执行更快是成立的更少的指令通常意味着更少的取指、解码与执行开销。但对于含有循环的复杂方法代码大小是一个很差的度量JIT 通常希望为**最内层循环innermost loop**生成最优代码并愿意为循环外部的代码付出额外大小代价例如循环不变量外提、循环展开后的额外设置代码因此循环内部的一处改动即使让循环体执行更快也可能表现为方法总代码大小变大从而被 jit-analyze 误判为回归反之把循环内某条昂贵指令换成循环外的多条廉价指令总大小可能变小却被误判为改进。原设计文档给出的结论非常直接与其用总代码大小作为度量不如让 JIT 直接产出一个动态执行成本的估算值这个值就是方法的 Perf ScorePerformance Score。jit-analyze 之后也应改为基于 Perf Score 来挑选回归/改进最大的方法列表而不是基于代码字节数。目标用户Perf Score 面向的是JIT 代码库的开发者Who is the customer一节明确写明用于日常 JIT 改动后的对比评估而不是面向普通应用开发者的运行时指标。当一次性能改动影响某个 Benchmark 时原文档建议同时验证两个方向的一致性计算出的 Perf Score 变化方向应与 Benchmark 实测执行时间的变化方向一致。也就是说Perf Score 是静态评估工具Benchmark 才是最终裁判二者应互相印证。为什么精确建模动态执行成本非常困难要精确得到方法在某一颗具体 CPU 上的真实动态执行成本本质上需要模拟现代处理器的超标量superscalar执行模型每条指令被拆分为一系列步骤取指、译码、发射、执行、写回等构成一条流水线pipelineCPU 同时维护多条指令流水线并发执行当代硬件上同时处于执行状态的指令可以有二十条甚至更多指令之间存在数据依赖def/use 延迟、端口争用pipeline port 竞争、乱序执行out-of-order execution与投机执行speculation等复杂交互。要在 JIT 内部实现这样一个软件模型工程量巨大且极其复杂。Perf Score 的定位从一开始就非常克制——原文档明确强调Perf Score 不打算精确建模真实动态执行成本而是返回一个估算值通过若干合理的简化假设快速得到一个可用于排序对比的近似结果。硬件厂商文档中的三个基础参数Intel、ARM 等硬件厂商为汇编程序员和编译器编写者提供指令级文档通常包含以下三类信息它们也是 Perf Score 模型的输入基础参数含义Latency延迟指令的所有输入就绪后执行完成并产出结果所需的时钟周期数Throughput吞吐量在发射端口issue port空闲、可以接受下一条同型指令之前需要等待的时钟周期数该值常常小于 1 个周期即每周期可发射多条Pipeline Ports流水线端口每条指令使用的特定执行单元如 Load Memory、Store Memory、Integer ALU、Floating Point Multiply 等Perf Score 实现中JIT 对每条指令调用getInsExecutionCharacteristics拿到一个insExecutionCharacteristics结构再结合基本块的权重BasicBlock weight计算该指令的加权执行成本。注意当前实现不建模真实的 def/use 延迟链而是使用简化模型估算指令延迟原文档提到若未来 JIT 增加指令调度instruction scheduler阶段届时可以再回头细化这一部分且扩展成本不高。三条简化假设为了让估算又快又合理Perf Score 采用以下三条简化假设见原文档Simplifying Assumptions一节存储操作不支付延迟成本假设不会出现存完立刻把该值读回来的依赖场景因此内存写store操作无需计入其完整延迟硬件投机执行可隐藏每指令 1 个周期的延迟即每条指令的延迟可减去 1 个周期视为被处理器投机/乱序执行所掩盖不建模精确的端口占用而是按最坏情况建模例如连续发射两条除法指令除法的延迟/吞吐通常远高于 ALU 操作。这三条假设在源码中有直接对应。emitter::insEvaluateExecutionCostsrc/coreclr/jit/emit.cpp 中实现正是按此逻辑修正延迟当内存访问类型是Write或ReadWrite时从延迟中减去PERFSCORE_LATENCY_WR_GENERAL对应假设 1写不读回不付延迟否则只要延迟 ≥ 1.0就减 1.0对应假设 2投机隐藏 1 周期最终取max(throughput, latency)作为该指令的估算执行成本。对应实现片段if ((memAccessKind PerfScoreMemoryAccessKind::Write) || (memAccessKind PerfScoreMemoryAccessKind::ReadWrite)) { // We assume that we wont read back from memory for the next WR_GENERAL cycles // Thus we normally wont pay latency costs for writes. latency max(0.0f, latency - PERFSCORE_LATENCY_WR_GENERAL); } else if (latency 1.0) // Otherwise, If we arent performing a memory write { // We assume that the processors speculation will typically eliminate one cycle of latency latency - 1.0; } return max(throughput, latency);关于按最坏情况建模端口假设 3从源码结构看getInsExecutionCharacteristics对除法等昂贵指令直接使用很高的吞吐/延迟常量例如PERFSCORE_THROUGHPUT_140C相当于按背靠背发射的最坏情形计费而未区分指令间端口冲突的具体情况。实现细节从指令特征到方法级 Perf Score核心数据结构insExecutionCharacteristics在 src/coreclr/jit/emit.h 中定义了指令执行特征结构struct insExecutionCharacteristics { float insThroughput 0; float insLatency 0; PerfScoreMemoryAccessKind insMemoryAccessKind PerfScoreMemoryAccessKind::None; }; float insEvaluateExecutionCost(instrDesc* id); insExecutionCharacteristics getInsExecutionCharacteristics(instrDesc* id); void perfScoreUnhandledInstruction(instrDesc* id, insExecutionCharacteristics* result);其中PerfScoreMemoryAccessKind是一个枚举用于区分内存访问类型enum class PerfScoreMemoryAccessKind : unsigned { None 0, Read 1, Write 2, ReadWrite 3, };这些声明位于#if defined(DEBUG) || defined(LATE_DISASM)保护块内——Perf Score 的计算只在 DEBUG 构建或启用 LATE_DISASM 时编译不影响 release 版 JIT 的性能。指令成本常量表emit.h 中维护了一套按目标架构区分的延迟/吞吐常量宏例如PERFSCORE_THROUGHPUT_1C单发射1 周期到PERFSCORE_THROUGHPUT_140C以及PERFSCORE_LATENCY_1C到PERFSCORE_LATENCY_400C400C 注释为 Intel microcode issue with these instructions即某些受 Intel 微码问题影响的指令。此外还有按内存寻址方式细分的延迟常量栈位置访问PERFSCORE_LATENCY_RD_STACK/WR_STACK/RD_WR_STACK假设可命中 L0 cache常量地址访问PERFSCORE_LATENCY_RD_CONST_ADDR/WR_CONST_ADDR/RD_WR_CONST_ADDR同样假设命中 L0 cache一般内存访问PERFSCORE_LATENCY_RD_GENERAL/WR_GENERAL/RD_WR_GENERAL假设命中 L0 或 L1 cache并额外加上 1.0 的成本以反映更高的 cache miss 概率。这些常量的取值随目标架构TARGET_XARCH / TARGET_ARM64 / TARGET_LOONGARCH64 / TARGET_RISCV64 / TARGET_WASM不同而不同反映了不同 CPU 微架构在栈/常量/通用内存访问上的延迟差异。每个目标平台的 getInsExecutionCharacteristicsgetInsExecutionCharacteristics按目标平台分别实现x86/x64src/coreclr/jit/emitxarch.cppgetInsExecutionCharacteristics入口处先通过getMemoryOperation提取内存访问格式再按IF_SRD栈读、IF_SWR栈写、IF_SRW栈读写、IF_MRD常量地址读、IF_MWR、IF_MRW、IF_ARD一般内存读等指令格式分类设定吞吐、延迟与内存访问类型ARM/ARM64src/coreclr/jit/emitarm.cpp、src/coreclr/jit/emitarm64.cpp其中 ARM64 的getMemoryOperation直接输出PerfScoreMemoryAccessKind与是否局部访问的标志LoongArch64、RISC-V64、Wasm分别见 emitloongarch64.cpp、emitriscv64.cpp、emitwasm.cppWasm 使用平均物理目标的延迟假设。对于实现者未覆盖到的指令perfScoreUnhandledInstructionemit.cpp会打印指令名与格式并在 DEBUG 构建下触发断言assert(!PerfScore: unhandled instruction)在非 DEBUG 构建则回退为 1 周期的默认延迟/吞吐保证任何指令都有估值可用。按基本块权重加权求和Perf Score 不是简单地把指令成本累加而是按执行频率加权。在发射阶段emitIssue1Instremit.cpp中每条指令发出后float insExeCost insEvaluateExecutionCost(id); // All compPerfScore calculations must be performed using doubles double insPerfScore (double)(ig-igWeight / (double)BB_UNITY_WEIGHT) * insExeCost; m_compiler-Metrics.PerfScore insPerfScore; ig-igPerfScore insPerfScore;要点每个指令组insGroup通常对应一个基本块携带权重igWeightBB_UNITY_WEIGHT是权重归一化基准执行一次的单位权重指令成本 ×块权重 / 单位权重即为该指令的加权动态执行成本累加到块级igPerfScore定义于 emit.h 的insGroupdouble igPerfScore和方法级Metrics.PerfScore全部计算使用 double 以避免累加误差块权重来源于 PGOProfile-Guided Optimization配置或 JIT 对执行频率的重建因此含循环的方法中循环体内指令会被高权重放大这正是设计文档想要的——优化最内层循环带来的收益会在 Perf Score 上被显著反映出来而循环外一次性代码的成本则被权重稀释。igPerfScore可在 JIT dump 中逐块查看;; size... bbWeight... PerfScore ...方法级Metrics.PerfScore则是全局累计值。额外的代码大小成本仅看动态执行成本还不够Perf Score 还希望兼顾 JIT 生成的代码量原文档给出了明确的成本叠加规则每字节热代码hot code增加0.1每字节冷代码cold code增加0.01。即 Perf Score 是动态执行成本估算 代码大小惩罚项的组合指标冷代码如异常处理路径、罕见分支每字节仅计 0.01几乎不影响总分而热代码每字节 0.1使得明显的代码膨胀也会在总分中有所体现。这保证了循环优化但总体代码膨胀的改动不会被无脑加分兼顾了速度与体积。需要说明文档描述的是该特性的设计意图实际权重是否随版本演进调整请以仓库最新实现为准。如何查看与使用 Perf ScorePerf Score 是 JIT 调试/分析期的产物查看途径主要有以下几种1. 编译输出汇总行在 src/coreclr/jit/codegencommon.cpp 的代码生成汇总输出中包含 Perf Score 字段; Total bytes of code %d, prolog size %d, PerfScore %.2f, instruction count %d, allocated bytes for ...对应源码片段printf(; Total bytes of code %d, prolog size %d, PerfScore %.2f, instruction count %d, allocated bytes for code %d, codeSize, prologSize, m_compiler-Metrics.PerfScore, instrCount, GetEmitter()-emitTotalHotCodeSize GetEmitter()-emitTotalColdCodeSize);2. JitMetrics 配置开关Perf Score 作为一项 JIT 指标受JitMetrics配置控制src/coreclr/jit/jitconfigvalues.hCONFIG_INTEGER(JitMetrics, JitMetrics, 0);即默认关闭0打开后 JIT 会在编译时记录并汇报包括PerfScore在内的一批指标。指标定义于 src/coreclr/jit/jitmetadatalist.hJITMETADATAMETRIC(PerfScore, double, JIT_METADATA_LOWER_IS_BETTER)注意其 flag 是JIT_METADATA_LOWER_IS_BETTER——Perf Score 越低越好这与估算动态执行成本的语义一致。指标的采集、合并inlinee 合并到根方法与输出逻辑见 src/coreclr/jit/jitmetadata.h 与 src/coreclr/jit/jitmetadata.cpp。在 compiler.cpp 中编译结束日志会附带perfScore%.2fsprintf_s(metricPart, 128, , perfScore%.2f, numCse%u, Metrics.PerfScore, optCSEcount);同时方法表格 dump 中也有 PerfScore 列Metrics.PerfScore 9999.995时输出到小数点后两位。3. 逐块insGroup查看在 JIT dump 中每个指令组会附带PerfScore见 emit.cpp 中;; size... bbWeight... PerfScore %.2f可用于定位哪个基本块贡献了主要成本从而聚焦优化热点块。4. CSV / 指标导出在 src/coreclr/jit/lsra.cpp 的指标导出逻辑中也包含PerfScore列fprintf(file, ,\PerfScore\\n)与fprintf(file, ,%.2f\n, m_compiler-Metrics.PerfScore)便于做批量数据对比。后续工作jit-analyze 的 Perf Score 化原文档明确列出了 Follow-on work修改jit-analyze工具使其不再依据代码大小而是使用 Perf Score 识别回归/改进最大的方法。这意味着 JIT 开发者未来的典型工作流将是用 jit-diffs 找出两个 JIT 版本汇编有差异的方法用改造后的jit-analyze 按Perf Score 变化排序聚焦动态执行成本恶化/改善最明显的方法对涉及 Benchmark 的改动交叉验证 Perf Score 变化方向与 Benchmark 实测耗时的变化方向一致。实践建议与限制作为排序指标而非绝对真理Perf Score 是基于三条简化假设的估算值不建模真实 def/use 延迟链与端口争用只适合在 diff 对比中做相对排序不能当作精确的 cycle 数重视循环权重因为它按基本块权重加权含循环方法中循环体的改动会被正确放大——这正是它优于总代码大小的原因与 Benchmark 双向验证改动若影响 Benchmark应同时检查 Perf Score 与实测耗时的变化方向是否一致架构相关延迟/吞吐常量表按目标架构分别配置x86/x64、ARM64、LoongArch64、RISC-V64、Wasm 各不相同跨架构对比时要留意这一点仅调试/分析期可用Perf Score 计算仅在DEBUG || LATE_DISASM条件下编译不影响生产 JIT 路径。总结Perf Score 是 CoreCLR JIT 为评估 JIT 改动质量而设计的一个务实方案它承认精确建模超标量 CPU 执行成本不现实转而通过 Latency / Throughput / 内存访问类型三个维度配合基本块权重加权与少量代码大小惩罚产出一个可复现、可排序的方法级动态执行成本估算。配合 jit-diffs / jit-analyze 工具链它有望取代代码大小成为 JIT 开发者评估循环密集型方法优化效果的首选度量核心实现可从 emit.h特征结构与常量表、emit.cpp成本计算与加权累加、emitxarch.cpp 等平台实现以及 jitmetadatalist.h指标定义继续深入研读。【免费下载链接】runtime.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.项目地址: https://gitcode.com/GitHub_Trending/runtime6/runtime创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
网站建设高端定制企业官网