在循环条件之外为循环条件声明变量是否更快?

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 中的 属性 Countarray 中的 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 属性 并直接访问此 属性。