首页 > 解决方案 > 为什么 List.Sum() 与 foreach 相比表现不佳?

问题描述

问题:为什么在以下场景中的执行时间Sum()比a长得多?foreach()

public void TestMethod4()
{
    List<int> numbers = new List<int>();
    for (int i = 0; i < 1000000000; i++)
    {
        numbers.Add(i);
    }

    Stopwatch sw = Stopwatch.StartNew();
    long totalCount = numbers.Sum(num => true ? 1 : 0); // simulating a dummy true condition
    sw.Stop();
    Console.WriteLine("Time taken Sum() : {0}ms", sw.Elapsed.TotalMilliseconds);

    sw = Stopwatch.StartNew();
    totalCount = 0;
    foreach (var num in numbers)
    {
        totalCount += true ? 1 : 0; // simulating a dummy true condition
    }
    sw.Stop();
    Console.WriteLine("Time taken foreach() : {0}ms", sw.Elapsed.TotalMilliseconds);
}

样品运行1

Time taken Sum()     : 21443.8093ms
Time taken foreach() : 4251.9795ms

标签: c#performanceforeachsum

解决方案


TL;DR:时间差异是由 CLR 在第二种情况下应用两个单独的优化引起的,但不是第一种情况:

  • LinqSum在 上运行IEnumerable<T>,而不是List<T>
    • CLR/JIT确实foreach对with进行了特殊情况优化List<T>,但如果 aList<T>作为 传递,则没有IEnumerable<T>
      • 这意味着它正在使用IEnumerator<T>并产生与此相关的所有虚拟呼叫的成本。
      • List<T>直接使用使用静态调用(实例方法调用仍然是“静态”调用,只要它们不是虚拟的)。
  • LinqSum接受委托Func<T,Int64>
    • 作为委托传递的函数Func<T,Int64>不是内联的,即使使用MethodImplOptions.AggressiveInline.
    • 委托调用的成本略高于虚拟调用。

我已经SUM使用您在此处访问的各种不同方法重新实现了您的程序:https ://gist.github.com/Jehoel/1a4fcd2e70374d3694c3a105061a6d1c

我的基准测试结果(发布版本、x64、.NET Core 5、i7-7700HQ):

方法 时间(毫秒)
Test_Sum_Delegate 118毫秒
Test_MySum_DirectFunc_IEnum 112毫秒
Test_MySum_IndirectFunc_IEnum 114 毫秒
Test_MySum_DirectCall_IEnum 89 毫秒
Test_MySum_DirectFunc_List 58毫秒
Test_MySum_IndirectFunc_List 58毫秒
Test_MySum_DirectCall_List 37毫秒
Test_Sum_DelegateLambda 109 毫秒
Test_For_Inline 4毫秒
Test_For_Delegate 3ms
Test_ForUnrolled_Inline 4毫秒
Test_ForUnrolled_Delegate 4毫秒
Test_ForEach_Inline 38ms
Test_ForEach_Delegate 37毫秒

我们可以通过一次更改一件事来隔离不同的行为(例如foreachvs forIEnumerable<T>vs List<T>Func<T>vs 直接函数调用)。

System.Linq.Enumerable.Sum方法(Test_Sum_Delegate)与Test_MySum_IndirectFunc_IEnum(忽略它们之间的差异4ms)相同。这两种方法都遍历List<T>using IEnumerable<T>

将方法更改为传递List<T>aList<T>而不是IEnumerable<T>(in Test_MySum_IndirectFunc_List) 可以消除使用虚拟调用,foreachIEnumerator<T>会导致从~114msto58ms减少,时间已经减少了 50%。

然后Func<Int64,Int64>将对 func 的(委托)调用更改为对(如)GetValue的“静态”调用将时间缩短到- 与. 这种方法与您的手写循环相同。GetValueTest_MySum_DirectCall_List37msTest_ForEach_Delegateforeach

获得更快性能的唯一方法是使用for没有任何虚拟调用的循环。(在调试版本中,Unrolled 循环甚至比正常for循环更快,但在发布版本中没有观察到差异)。


推荐阅读