CSharp-不要为for建立属性的临时变量

可能有人认为相比于 ForTest1,ForTest2 存储了数组的 Length,少了对于数组属性的频繁调用,会有更好的性能表现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
using System;

namespace JITPropertyAccessInFor
{
internal static class Program
{
public static void Main(string[] args)
{
}

private static void Test1()
{
var a = new int[5];
}

private static void ForTest1()
{
var a = new int[5];
for (var i = 0; i < a.Length; i++)
{
Console.WriteLine(a[i]);
}
}

private static void ForTest2()
{
var a = new int[5];
int len = a.Length;
for (var i = 0; i < len; i++)
{
Console.WriteLine(a[i]);
}
}
}
}

以下 是 上段代码编译出的 IL code:(以下所述栈均为操作数栈 (Operand stack))

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
.method private hidebysig static void  ForTest1() cil managed
{
// Code size 30 (0x1e)
.maxstack 2 // 栈最大深度
.locals init ([0] int32[] a, // 变量声明 (局部变量表)
[1] int32 i)
IL_0000: ldc.i4.5 // int32 5 入栈 (声明的数组大小)
IL_0001: newarr [mscorlib]System.Int32 // 创建0基一维数组的对象引用入栈
IL_0006: stloc.0 // 出栈 置于局部变量表0位置 (初始化数组完毕)
IL_0007: ldc.i4.0 // int32 0 入栈 (i = 0)
IL_0008: stloc.1 // 出栈 置于局部变量表1位置 (i = 0)
IL_0009: br.s IL_0017 // 无条件地将控制转移到目标指令(短格式)(至 for 中判断开始位置)
IL_000b: ldloc.0 // 局部变量表0位置变量入栈 (数组元素入栈)
IL_000c: ldloc.1 // 局部变量表1位置变量入栈 (i 入栈)
IL_000d: ldelem.i4 // 按指令指定类型(i4),将指定数组索引中的元素入栈
IL_000e: call void [mscorlib]System.Console::WriteLine(int32) // 调用由传递的方法说明符指示的方法 (打印a[i])
IL_0013: ldloc.1 // 局部变量表1位置变量入计算栈 (i 入栈) (i++ 开始)
IL_0014: ldc.i4.1 // int32 1 入栈
IL_0015: add // 出栈两次,出栈值相加,结果入栈
IL_0016: stloc.1 // 出栈 置于局部变量表1位置 (i++ 结束)
IL_0017: ldloc.1 // 局部变量表1位置变量入栈 (i 入栈) (for 中判断开始位置)
IL_0018: ldloc.0 // 局部变量表0位置变量入栈 (a 入栈,准备获取数组长)
IL_0019: ldlen // 将0基一维数组的元素数目推送到计算栈上。(数组长入栈)
IL_001a: conv.i4 // 将栈顶元素转换为 int32 类型
IL_001b: blt.s IL_000b // 判断计算栈顶两值大小(计算栈出栈两次,后出栈的是第一个值)。若第一个值小于第二个值,将控制转移到目标指令 (短格式)。
IL_001d: ret // 从当前方法返回
} // end of method Program::ForTest1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
.method private hidebysig static void  ForTest2() cil managed
{
// Code size 32 (0x20)
.maxstack 2 // 栈最大深度
.locals init ([0] int32[] a, // 变量声明 (局部变量表)
[1] int32 len,
[2] int32 i)
IL_0000: ldc.i4.5 // int32 5 入栈 (声明的数组大小)
IL_0001: newarr [mscorlib]System.Int32 // 创建0基一维数组的对象引用入栈
IL_0006: stloc.0 // 出栈 置于局部变量表0位置 (初始化数组完毕)
IL_0007: ldloc.0 // 局部变量表1位置变量入栈 (a 入栈,准备获取数组长)
IL_0008: ldlen // 将0基一维数组的元素数目推送到计算栈上。(数组长入栈)
IL_0009: conv.i4 // 将栈顶元素转换为 int32 类型
IL_000a: stloc.1 // 出栈 置于局部变量表1位置 (len = a.Length)
IL_000b: ldc.i4.0 // int32 0 入栈 (声明的数组大小)
IL_000c: stloc.2 // 出栈 置于局部变量表2位置 (i = 0)
IL_000d: br.s IL_001b // 无条件地将控制转移到目标指令(短格式)(至 for 中判断开始位置)
IL_000f: ldloc.0 // 局部变量表0位置变量入栈 (a 入栈)
IL_0010: ldloc.2 // 局部变量表2位置变量入栈 (i 入栈)
IL_0011: ldelem.i4 // 按指令指定类型(i4),将指定数组索引中的元素入栈
IL_0012: call void [mscorlib]System.Console::WriteLine(int32) // 调用由传递的方法说明符指示的方法 (打印a[i])
IL_0017: ldloc.2 // 局部变量表2位置变量入栈 (i 入栈) (i++ 开始)
IL_0018: ldc.i4.1 // int32 1 入栈
IL_0019: add // 出栈两次,出栈值相加,结果入栈
IL_001a: stloc.2 // 出栈 置于局部变量表2位置 (i++ 结束)
IL_001b: ldloc.2 // 局部变量表2位置变量入栈 (i 入栈) (for 中判断开始位置)
IL_001c: ldloc.1 // 局部变量表1位置变量入栈 (len 入栈)
IL_001d: blt.s IL_000f // 判断计算栈顶两值大小(计算栈出栈两次,后出栈的是第一个值)。若第一个值小于第二个值,将控制转移到目标指令 (短格式)。
IL_001f: ret // 从当前方法返回
} // end of method Program::ForTest2

对比上述的 IL code,确实临时存储数组长,能够少在 for 的比较进行中少进行一定的操作,无需将数组从局部变量表(Local Variable Table)入操作数栈 (Operand stack),并执行 ldlen 获取数组长。 但要注意, JIT 编译器知道 Length 是 Array 类的属性,生成的代码中只会调用该属性一次,结果会存储到临时变量中,此后的检查中调用的都是此临时变量。不需要自己用局部变量做缓存,这样既没有性能提升,还可能造成可读性下降

参阅

CLR via C# (第四版) 16.7 数组的内部工作原理

注释

Ildasm.exe(IL 反汇编程序)

一般,该工具位于 NETFX 4.7.2 Tools 中

C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.7.2 Tools\x64\ildasm.exe