第一种方法比第二种方法更快

时间:2015-01-24 19:44:39

标签: c# performance linq benchmarking performance-testing

我查看了.NET Framework源代码,偶然发现了implementation of LINQ-Sum

int Sum(this IEnumerable<int> source)

我看到它是用foreach循环实现的,并且想知道为什么MS的人不会因为性能原因而使用正常的for循环(后来我才知道,for-之间的性能差异不再存在 - 循环和foreach循环 - 但直到现在我才知道。

所以我在我自己的项目中复制了MS实现并编写了一些基准:

var range = Enumerable.Range(1, 1000);
Stopwatch sw = new Stopwatch();

//Do sth unimportant for warming up

sw.Start();
for(int i = 0; i <= 10000; i++)
{
    long z = i + 3;
}
sw.Stop();

//Implementation 1

sw.Reset();
sw.Start();
for (int i = 0; i <= 1000000; i++)
{
    long i1 = range.Sum1();
}
sw.Stop();
Console.WriteLine("Sum1: " + sw.ElapsedTicks.ToString());

//Implementation 2

sw.Reset();
sw.Start();
for (int i = 0; i <= 1000000; i++)
{
    long i2 = range.Sum2();
}
sw.Stop();
Console.WriteLine("Sum2: " + sw.ElapsedTicks.ToString());

以下是Sum的两个实现(注意:两者都是相同的,我首先要检查测量是否正常工作):

public static class LinqExtension
{
    public static int Sum1(this IEnumerable<int> source)
    {
        int sum = 0;
        checked
        {
            foreach (int v in source) sum += v;
        }
        return sum;
    }

    public static int Sum2(this IEnumerable<int> source)
    {
        int sum = 0;
        checked
        {
            foreach (int v in source) sum += v;
        }
        return sum;
    }
}

令人惊讶的是我得到了两个不同的结果:Sum1 = 16043441 vs. Sum2 = 17480907

所以我稍微扩展了基准测试并且不仅仅调用了Sum1和Sum2一次,而是按以下顺序多次调用:

  1. Sum1:16035534
  2. Sum2:17381296
  3. Sum2:17441259
  4. Sum1:16021378
  5. Sum1:16000879
  6. Sum1:15989672
  7. Sum2:17342804
  8. Sum2:17347417 ...
  9. 因此Sum1总是比Sum2快近10%。当我首先调用Sum2时,结果是相反的。

    导致这些性能差异的原因是什么?为什么第一个被调用的方法比第二个更快?我的基准无效吗?

    我正在使用Visual Studio 2015 CTP4和.NET Framework 4.5.3

    编辑:

    结果以毫秒而不是刻度

    1. Sum1:7714 ms
    2. Sum2:8336 ms
    3. Sum2:8321 ms
    4. Sum1:7686 ms
    5. Sum1:7693 ms
    6. Sum1:7686 ms
    7. Sum2:8372 ms
    8. Sum2:8302 ms ...

    9. 感谢评论,我修正了一些错误,现在代码看起来像这样:

      sw.Start();
      for (int i = 0; i <= 1000000; i++)
      {
         i1 = range.Sum1();
      }
      sw.Stop();
      Console.WriteLine("Sum1: " + sw.ElapsedMilliseconds.ToString() + "\n" + i1.ToString());
      

      现在结果完全不同了:

      1. Sum1:8021 ms
      2. Sum2:7587 ms
      3. Sum2:7660 ms
      4. Sum1:7989 ms
      5. Sum1:8041 ms
      6. Sum1:8038 ms
      7. Sum2:7609 ms
      8. Sum2:7613 ms
      9. 但是仍然存在差异,但现在反过来了。


        另一个更新:

        当我使用

        int[] range = new int[1000];
        for (int m = 0; m < range.Length; m++)
                    range[m] = m+1;
        

        而不是

        var range = Enumerable.Range(1, 1000);
        

        这两种方法同样快。

        1. Sum1:6966 ms
        2. Sum2:6986 ms
        3. Sum2:7045 ms
        4. Sum1:7039 ms
        5. Sum1:6932 ms
        6. Sum1:7064 ms
        7. Sum2:7023 ms
        8. Sum2:7026 ms

        9. 更新: 用Mono(SharpDevelop)和VS2013测试它,我得到了完全一致的结果。所以我认为使用VS2015不是一个好主意,因为它仍然是一个测试版。因此,结果的重要性非常低。


          另一个更新:

          stakx评论道:

            

          尝试至少调用一次Sum1Sum2方法   在你开始测量时间之前,为了确保   方法的代码由JIT生成。否则你可能会   包括你的JIT代码生成所需的时间   基准

          所以我在测量前一次调用了Sum1Sum2,令人惊讶的是这解决了问题。但我不明白为什么。我知道JIT生成代码会花费一些时间,但这只是第一次。在我的测试中,我有20个for循环,每个循环分别调用Sum1和Sum2 1.000.000次。我对每个循环进行测量,并为Sum1和Sum2获得不同的值。 如果第一个循环较慢,那将是有意义的,但事实并非如此。

          我使用ngen.exe生成原生图像并得到以下结果:

          1. Sum1:6517 ms
          2. Sum2:6837 ms
          3. Sum2:6817 ms
          4. Sum1:6511 ms
          5. Sum1:6513 ms
          6. Sum1:6513 ms
          7. Sum2:6822 ms
          8. Sum2:6942 ms ...
          9. 所以仍然存在这种差异。

            非常重要:它并不总是第一种更快的方法!有时 它是第一个被调用的方法,有时是第二个。但是一旦组装完成,结果就是可重复的。 这对我来说很混乱,当发生这种情况时,我看不到任何模式。

            Enigmativity:

              

            您是否尝试过交换调用方法的顺序?   先拨打Sum2

            是的,但结果只是反过来。如果Sum1是“快速方法”,则在交换后,Sum2是快速的,Sum1是慢速的。

1 个答案:

答案 0 :(得分:1)

我稍微修改了你的测试代码,我发现有两个因素对性能有影响:你是否运行相同或两个不同的集合以及枚举的类型(我不知道为什么尚未)。

枚举List<int>似乎是最慢的情况。数组int[]是最快的数组,当你使用两个不同的范围时,两者之间实际上没有区别(但是当使用列表时总是存在差异):

static void Main(string[] args)
{
    // Try with .ToList() and .ToArray()
    var range1 = Enumerable.Range(1, 1000);
    var range2 = Enumerable.Range(1, 1000);

    int numberOfSums = 100000;
    int numberOfTests = 3;

    for (int i = 0; i < numberOfTests; i++)
    {
        SumBenchmark(range1, LinqExtension.Sum1, numberOfSums, "Sum1");
    }

    for (int i = 0; i < numberOfTests; i++)
    {
        // Also try with range1
        SumBenchmark(range2, LinqExtension.Sum2, numberOfSums, "Sum2");
    }

    Console.ReadKey();
}

static void SumBenchmark(IEnumerable<int> numbers, Func<IEnumerable<int>, int> sum, int numberOfSums, string name)
{
    Stopwatch sw = new Stopwatch();
    sw.Start();
    for (int i = 0; i < numberOfSums; i++)
    {
        long result = sum(numbers);
    }
    sw.Stop();
    Console.WriteLine("{2}: {0} ticks in {1} ms ", sw.ElapsedTicks.ToString(), sw.ElapsedMilliseconds.ToString(), name);
}

对于我来说,第二次通话总是更快。


编辑:如果您在构建设置中禁用Prefer 32-bit选项并将其编译为64-bit,则会产生巨大的性能影响 - 然后原始Sum运行得更快:

Prefer 32-bit

...但是它运行时的速度没有.ToList().ToArray()


EDIT-2:这是Sum2使用int[]而不是IEnumerable的另一个结果:

Sum1: in 878 ms 
Sum1: in 863 ms 
Sum1: in 875 ms 
Sum2: in 122 ms 
Sum2: in 122 ms 
Sum2: in 121 ms 
Linq: in 830 ms 
Linq: in 825 ms 
Linq: in 836 ms 

生成的IL也不同:

public static int Sum2(this int[] source)

它的

.method public hidebysig static int32  Sum2(int32[] source) cil managed
{
  .custom instance void [mscorlib]System.Runtime.CompilerServices.ExtensionAttribute::.ctor() = ( 01 00 00 00 ) 
  // Code size       28 (0x1c)
  .maxstack  2
  .locals init ([0] int32 sum,
           [1] int32 v,
           [2] int32[] CS$6$0000,
           [3] int32 CS$7$0001)
  IL_0000:  ldc.i4.0
  IL_0001:  stloc.0
  IL_0002:  ldarg.0
  IL_0003:  stloc.2
  IL_0004:  ldc.i4.0
  IL_0005:  stloc.3
  IL_0006:  br.s       IL_0014
  IL_0008:  ldloc.2
  IL_0009:  ldloc.3
  IL_000a:  ldelem.i4
  IL_000b:  stloc.1
  IL_000c:  ldloc.0
  IL_000d:  ldloc.1
  IL_000e:  add.ovf
  IL_000f:  stloc.0
  IL_0010:  ldloc.3
  IL_0011:  ldc.i4.1
  IL_0012:  add
  IL_0013:  stloc.3
  IL_0014:  ldloc.3
  IL_0015:  ldloc.2
  IL_0016:  ldlen
  IL_0017:  conv.i4
  IL_0018:  blt.s      IL_0008
  IL_001a:  ldloc.0
  IL_001b:  ret
} // end of method LinqExtension::Sum2

public static int Sum1(this IEnumerable<int> source)

它的

.method public hidebysig static int32  Sum1(class [mscorlib]System.Collections.Generic.IEnumerable`1<int32> source) cil managed
{
  .custom instance void [mscorlib]System.Runtime.CompilerServices.ExtensionAttribute::.ctor() = ( 01 00 00 00 ) 
  // Code size       44 (0x2c)
  .maxstack  2
  .locals init ([0] int32 sum,
           [1] int32 v,
           [2] class [mscorlib]System.Collections.Generic.IEnumerator`1<int32> CS$5$0000)
  IL_0000:  ldc.i4.0
  IL_0001:  stloc.0
  IL_0002:  ldarg.0
  IL_0003:  callvirt   instance class [mscorlib]System.Collections.Generic.IEnumerator`1<!0> class [mscorlib]System.Collections.Generic.IEnumerable`1<int32>::GetEnumerator()
  IL_0008:  stloc.2
  .try
  {
    IL_0009:  br.s       IL_0016
    IL_000b:  ldloc.2
    IL_000c:  callvirt   instance !0 class [mscorlib]System.Collections.Generic.IEnumerator`1<int32>::get_Current()
    IL_0011:  stloc.1
    IL_0012:  ldloc.0
    IL_0013:  ldloc.1
    IL_0014:  add.ovf
    IL_0015:  stloc.0
    IL_0016:  ldloc.2
    IL_0017:  callvirt   instance bool [mscorlib]System.Collections.IEnumerator::MoveNext()
    IL_001c:  brtrue.s   IL_000b
    IL_001e:  leave.s    IL_002a
  }  // end .try
  finally
  {
    IL_0020:  ldloc.2
    IL_0021:  brfalse.s  IL_0029
    IL_0023:  ldloc.2
    IL_0024:  callvirt   instance void [mscorlib]System.IDisposable::Dispose()
    IL_0029:  endfinally
  }  // end handler
  IL_002a:  ldloc.0
  IL_002b:  ret
} // end of method LinqExtension::Sum1

在所有迭代之后IEnumerable参数不会太快......并且仅当foreach循环的集合不是IEnumerable时才进行优化。 DEMO