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

时间:2018-03-27 07:20:43

标签: c# generic-programming coding-efficiency

在以下场景中,示例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]);
}

3 个答案:

答案 0 :(得分:3)

让我们获取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

两者之间存在一个明显的区别,在后一种方法中,您在每次迭代时调用虚拟实例方法,而在循环之前只调用一次

IL指令是相对相同的数字,所以除非你认为这个callvirt(为什么callvirt为实例方法?因为它附带了很好的空检查,而编译器也将它用于非虚方法)指令会拖累你我建议你选择什么是潜在小型性能调整不值得的最佳实践我保证,更不用说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 |

这又是非常具体的硬件,但为了论证而显示它。

答案 1 :(得分:2)

您可以自己测试一下。为此,你需要一个工作台:

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)}");
    }

在我的机器上,结果是:

DEBUG:7852和6631(变速更快)

RELEASE:1117和1127(几乎相同的东西)

一般的想法是:

在调试模式下,对数组(集合)属性的调用未进行优化,因此会增加开销。

在Release模式下,此调用已经过优化,因为显式声明一个新的int变量将在堆栈中分配空间等。它比可能通过保存指向Count属性的指针创建快捷方式的优化代码花费更多时间并直接访问此属性。

答案 2 :(得分:0)

数组不具有名为Count的属性,只有方法Count()。使用示例2,该方法将在每次迭代时运行,这比预定义变量花费的时间要多得多。

假设您使用来自Count的{​​{1}}或来自ICollection的{​​{1}}的属性,那么这些示例在性能方面和您之间几乎没有显着差异#39;最好采用最易读的解决方案。