在循环条件之外为循环条件声明变量是否更快?
Is it faster to declare variables for loop criteria outside of the loop condition?
在下面的场景中,示例 1 是否比示例 2 快?为什么?
示例 1
int c = myArray.Count;
for (int i = 0; i < c; i++)
{
Console.WriteLine(myArray[i]);
}
示例 2
for (int i = 0; i < myArray.Count; i++)
{
Console.WriteLine(myArray[i]);
}
数组没有名为 Count
的 属性 只有方法 Count()
。使用示例 2,该方法将 运行 每次迭代,这比预定义变量花费的时间要多得多。
假设您使用 ICollection
中的 属性 Count
或 array
中的 Length
示例 [=20] 之间几乎没有显着差异=] 你最好选择最易读的解决方案。
让我们获取 IL 代码,看看 Release 配置中发生了什么。
/* 0x0000027B 6F0D00000A */ IL_001F: callvirt instance int32 class [System.Collections]System.Collections.Generic.List`1::get_Count()
/* 0x00000280 0B */ IL_0024: stloc.1
/* 0x00000281 16 */ IL_0025: ldc.i4.0
/* 0x00000282 0C */ IL_0026: stloc.2
/* 0x00000283 2B10 */ IL_0027: br.s IL_0039
// loop start (head: IL_0039)
/* 0x00000285 06 */ IL_0029: ldloc.0
/* 0x00000286 08 */ IL_002A: ldloc.2
/* 0x00000287 6F0E00000A */ IL_002B: callvirt instance !0 class [System.Collections]System.Collections.Generic.List`1::get_Item(int32)
/* 0x0000028C 280F00000A */ IL_0030: call void [System.Console]System.Console::WriteLine(char)
/* 0x00000291 08 */ IL_0035: ldloc.2
/* 0x00000292 17 */ IL_0036: ldc.i4.1
/* 0x00000293 58 */ IL_0037: add
/* 0x00000294 0C */ IL_0038: stloc.2
/* 0x00000295 08 */ IL_0039: ldloc.2
/* 0x00000296 07 */ IL_003A: ldloc.1
/* 0x00000297 32EC */ IL_003B: blt.s IL_0029
// end loop
/* 0x00000299 16 */ IL_003D: ldc.i4.0
/* 0x0000029A 0D */ IL_003E: stloc.3
/* 0x0000029B 2B10 */ IL_003F: br.s IL_0051
// loop start (head: IL_0051)
/* 0x0000029D 06 */ IL_0041: ldloc.0
/* 0x0000029E 09 */ IL_0042: ldloc.3
/* 0x0000029F 6F0E00000A */ IL_0043: callvirt instance !0 class [System.Collections]System.Collections.Generic.List`1::get_Item(int32)
/* 0x000002A4 280F00000A */ IL_0048: call void [System.Console]System.Console::WriteLine(char)
/* 0x000002A9 09 */ IL_004D: ldloc.3
/* 0x000002AA 17 */ IL_004E: ldc.i4.1
/* 0x000002AB 58 */ IL_004F: add
/* 0x000002AC 0D */ IL_0050: stloc.3
/* 0x000002AD 09 */ IL_0051: ldloc.3
/* 0x000002AE 06 */ IL_0052: ldloc.0
/* 0x000002AF 6F0D00000A */ IL_0053: callvirt instance int32 class [System.Collections]System.Collections.Generic.List`1::get_Count()
/* 0x000002B4 32E7 */ IL_0058: blt.s IL_0041
// end loop
两者之间有一个明显的区别,在后一种方法中,您在每次迭代中调用 virtual 实例方法,而在另一种方法中仅调用一次 before循环。
IL 指令的数量相对相同,所以除非你认为这个 callvirt
(为什么要为实例方法调用 virt?因为它有很好的 null 检查,编译器将它用于 non-virtual方法)指令会拖你后腿我建议你选择最佳实践,可能 小的性能调整不值得我保证,更不用说 JIT 也可以做到一些优化 - 我不会感到惊讶。
更新: 使用 BenchmarkDotNet 和 附加调试器的基准测试。
Method | Mean | Error | StdDev |
------------- |---------:|----------:|----------:|
OutsideCount | 25.04 ns | 0.3334 ns | 0.2955 ns |
InsideCount | 26.13 ns | 0.5295 ns | 0.6502 ns |
Foreach | 40.59 ns | 0.3848 ns | 0.3599 ns |
同样,这是非常特定于硬件的,但为了论证而展示它。
你可以自己测试一下。为此你需要一个 workbench:
class WorkBench
{
private static readonly Stopwatch S = new Stopwatch();
private static long[] RunOnce()
{
var results = new long[3];
var myArray = Enumerable.Range(0, 1000000).ToList();
int x = 1;
S.Restart();
for (int i = 0; i < myArray.Count; i++)
{
x = i + 1;
}
S.Stop();
results[0] = S.ElapsedTicks;
S.Restart();
int c = myArray.Count;
for (int i = 0; i < c; i++)
{
x = i - 1;
}
S.Stop();
results[1] = S.ElapsedTicks;
results[2] = x;
return results;
}
private static void Main(string[] args)
{
var results = new List<Tuple<long, long>>();
for (int i = 0; i < 1500; i++)
{
var workBenchResult = RunOnce();
results.Add(Tuple.Create(workBenchResult[0], workBenchResult[1]));
}
var average = Tuple.Create(results.Average(r => r.Item1), results.Average(r => r.Item2));
Console.WriteLine($"Average 1: {Math.Round(average.Item1, 4)}");
Console.WriteLine($"Average 2: {Math.Round(average.Item2, 4)}");
}
在我的机器上结果是:
调试:7852 和 6631(变量更快)
发布:1117 和 1127(几乎相同的东西)
大体思路是这样的:
在调试模式下调用数组的(集合)属性 未优化,因此会增加开销。
在发布模式下,此调用已优化,因为显式声明一个新的 int 变量将在堆栈中分配 space 等。它比优化代码花费更多时间,优化代码可能通过保存指向的指针来创建快捷方式Count
属性 并直接访问此 属性。
在下面的场景中,示例 1 是否比示例 2 快?为什么?
示例 1
int c = myArray.Count;
for (int i = 0; i < c; i++)
{
Console.WriteLine(myArray[i]);
}
示例 2
for (int i = 0; i < myArray.Count; i++)
{
Console.WriteLine(myArray[i]);
}
数组没有名为 Count
的 属性 只有方法 Count()
。使用示例 2,该方法将 运行 每次迭代,这比预定义变量花费的时间要多得多。
假设您使用 ICollection
中的 属性 Count
或 array
中的 Length
示例 [=20] 之间几乎没有显着差异=] 你最好选择最易读的解决方案。
让我们获取 IL 代码,看看 Release 配置中发生了什么。
/* 0x0000027B 6F0D00000A */ IL_001F: callvirt instance int32 class [System.Collections]System.Collections.Generic.List`1::get_Count() /* 0x00000280 0B */ IL_0024: stloc.1 /* 0x00000281 16 */ IL_0025: ldc.i4.0 /* 0x00000282 0C */ IL_0026: stloc.2 /* 0x00000283 2B10 */ IL_0027: br.s IL_0039 // loop start (head: IL_0039) /* 0x00000285 06 */ IL_0029: ldloc.0 /* 0x00000286 08 */ IL_002A: ldloc.2 /* 0x00000287 6F0E00000A */ IL_002B: callvirt instance !0 class [System.Collections]System.Collections.Generic.List`1::get_Item(int32) /* 0x0000028C 280F00000A */ IL_0030: call void [System.Console]System.Console::WriteLine(char) /* 0x00000291 08 */ IL_0035: ldloc.2 /* 0x00000292 17 */ IL_0036: ldc.i4.1 /* 0x00000293 58 */ IL_0037: add /* 0x00000294 0C */ IL_0038: stloc.2 /* 0x00000295 08 */ IL_0039: ldloc.2 /* 0x00000296 07 */ IL_003A: ldloc.1 /* 0x00000297 32EC */ IL_003B: blt.s IL_0029 // end loop /* 0x00000299 16 */ IL_003D: ldc.i4.0 /* 0x0000029A 0D */ IL_003E: stloc.3 /* 0x0000029B 2B10 */ IL_003F: br.s IL_0051 // loop start (head: IL_0051) /* 0x0000029D 06 */ IL_0041: ldloc.0 /* 0x0000029E 09 */ IL_0042: ldloc.3 /* 0x0000029F 6F0E00000A */ IL_0043: callvirt instance !0 class [System.Collections]System.Collections.Generic.List`1::get_Item(int32) /* 0x000002A4 280F00000A */ IL_0048: call void [System.Console]System.Console::WriteLine(char) /* 0x000002A9 09 */ IL_004D: ldloc.3 /* 0x000002AA 17 */ IL_004E: ldc.i4.1 /* 0x000002AB 58 */ IL_004F: add /* 0x000002AC 0D */ IL_0050: stloc.3 /* 0x000002AD 09 */ IL_0051: ldloc.3 /* 0x000002AE 06 */ IL_0052: ldloc.0 /* 0x000002AF 6F0D00000A */ IL_0053: callvirt instance int32 class [System.Collections]System.Collections.Generic.List`1::get_Count() /* 0x000002B4 32E7 */ IL_0058: blt.s IL_0041 // end loop
两者之间有一个明显的区别,在后一种方法中,您在每次迭代中调用 virtual 实例方法,而在另一种方法中仅调用一次 before循环。
IL 指令的数量相对相同,所以除非你认为这个 callvirt
(为什么要为实例方法调用 virt?因为它有很好的 null 检查,编译器将它用于 non-virtual方法)指令会拖你后腿我建议你选择最佳实践,可能 小的性能调整不值得我保证,更不用说 JIT 也可以做到一些优化 - 我不会感到惊讶。
更新: 使用 BenchmarkDotNet 和 附加调试器的基准测试。
Method | Mean | Error | StdDev |
------------- |---------:|----------:|----------:|
OutsideCount | 25.04 ns | 0.3334 ns | 0.2955 ns |
InsideCount | 26.13 ns | 0.5295 ns | 0.6502 ns |
Foreach | 40.59 ns | 0.3848 ns | 0.3599 ns |
同样,这是非常特定于硬件的,但为了论证而展示它。
你可以自己测试一下。为此你需要一个 workbench:
class WorkBench
{
private static readonly Stopwatch S = new Stopwatch();
private static long[] RunOnce()
{
var results = new long[3];
var myArray = Enumerable.Range(0, 1000000).ToList();
int x = 1;
S.Restart();
for (int i = 0; i < myArray.Count; i++)
{
x = i + 1;
}
S.Stop();
results[0] = S.ElapsedTicks;
S.Restart();
int c = myArray.Count;
for (int i = 0; i < c; i++)
{
x = i - 1;
}
S.Stop();
results[1] = S.ElapsedTicks;
results[2] = x;
return results;
}
private static void Main(string[] args)
{
var results = new List<Tuple<long, long>>();
for (int i = 0; i < 1500; i++)
{
var workBenchResult = RunOnce();
results.Add(Tuple.Create(workBenchResult[0], workBenchResult[1]));
}
var average = Tuple.Create(results.Average(r => r.Item1), results.Average(r => r.Item2));
Console.WriteLine($"Average 1: {Math.Round(average.Item1, 4)}");
Console.WriteLine($"Average 2: {Math.Round(average.Item2, 4)}");
}
在我的机器上结果是:
调试:7852 和 6631(变量更快)
发布:1117 和 1127(几乎相同的东西)
大体思路是这样的:
在调试模式下调用数组的(集合)属性 未优化,因此会增加开销。
在发布模式下,此调用已优化,因为显式声明一个新的 int 变量将在堆栈中分配 space 等。它比优化代码花费更多时间,优化代码可能通过保存指向的指针来创建快捷方式Count
属性 并直接访问此 属性。