可能有人认为相比于 ForTest1,ForTest2 存储了数组的 Length,少了对于数组属性的频繁调用,会有更好的性能表现。[1]
1 | using System; |
以下 是 上段代码编译出的 IL code:(以下所述栈均为操作数栈 (Operand stack))[2]
1 | .method private hidebysig static void ForTest1() cil managed |
1 | .method private hidebysig static void ForTest2() cil managed |
对比上述的 IL code,确实临时存储数组长,能够少在 for 的比较进行中少进行一定的操作,无需将数组从局部变量表(Local Variable Table)入操作数栈 (Operand stack),并执行 ldlen 获取数组长。[3] 但要注意, JIT 编译器知道 Length 是 Array 类的属性,生成的代码中只会调用该属性一次,结果会存储到临时变量中,此后的检查中调用的都是此临时变量。[4]不需要自己用局部变量做缓存,这样既没有性能提升,还可能造成可读性下降。[5]
参阅
CLR via C# (第四版) 16.7 数组的内部工作原理
注释
一般,该工具位于 NETFX 4.7.2 Tools 中
C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.7.2 Tools\x64\ildasm.exe
Microsoft Learn, C# arrays 与 Array.Length Property 支撑数组实例长度是数组自身的稳定事实;因此这里的性能直觉应落在 JIT 是否识别数组循环形状,而不是把
Length当普通可变属性。 ↩︎ECMA-335 Common Language Infrastructure 定义 CIL 指令与运行时模型;Microsoft Learn Ildasm.exe 支撑用 ILDASM 查看程序集 IL。IL 能说明编译器输出,但不能直接等同最终机器码性能。 ↩︎
ECMA-335 定义
ldlen为读取数组长度的 CIL 指令;这支撑文中从 IL 层面对ForTest1与ForTest2的比较。 ↩︎Jeffrey Richter, CLR via C#, Fourth Edition 用数组内部工作原理解释数组长度和运行时模型;Microsoft .NET 5 performance post 也明确讨论
for (int i = 0; i < arr.Length; i++)这类循环中 JIT 可证明索引不会越界并消除 bounds check。 ↩︎Microsoft .NET 5、.NET 8、.NET 9 与 .NET 10 performance posts 对 bounds check elimination、range analysis、PGO 和 loop 优化的持续讨论说明:这类微优化依赖目标运行时、Release 构建、JIT 机器码和测量数据。普通数组循环默认保持
i < array.Length的清晰形状;只有上界昂贵、有副作用、需要快照语义或有测量证据时,才应手动缓存。 ↩︎