【问题标题】:Why does List.Sum() perform poorly as compared to a foreach?为什么 List.Sum() 与 foreach 相比表现不佳?
【发布时间】:2021-03-15 18:05:53
【问题描述】:

问题:为什么Sum()的执行时间比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

【问题讨论】:

  • 你错误地使用了new Random
  • 这是调试版本还是发布版本?你的程序怎么不会因为被零除而崩溃?
  • @Dai,Sum函数中不是除法而是加0。
  • “我发现 Sum() 与 foreach 相比实际上要慢一些”——为什么会让人感到意外?
  • 使用 Benchmark dotnet 对代码进行性能分析。 benchmarkdotnet.org/articles/overview.html

标签: c# performance foreach sum


【解决方案1】:

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

  • Linq 的SumIEnumerable&lt;T&gt; 上运行,而不是List&lt;T&gt;
    • CLR/JIT 确实foreach 使用List&lt;T&gt; 进行了特殊情况优化,但如果List&lt;T&gt; 作为IEnumerable&lt;T&gt; 传递则不会。
      • 这意味着它正在使用 IEnumerator&lt;T&gt; 并产生与此相关的所有虚拟呼叫的费用。
      • 而使用 List&lt;T&gt; 直接使用静态调用(实例方法调用仍然是“静态”调用,前提是它们不是虚拟的)。
  • Linq 的Sum 接受委托Func&lt;T,Int64&gt;
    • 作为委托 Func&lt;T,Int64&gt; 传递的函数未内联,即使使用 MethodImplOptions.AggressiveInline
    • 委托调用的成本比虚拟调用贵。

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

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

Approach Time (ms)
Test_Sum_Delegate 118ms
Test_MySum_DirectFunc_IEnum 112ms
Test_MySum_IndirectFunc_IEnum 114ms
Test_MySum_DirectCall_IEnum 89ms
Test_MySum_DirectFunc_List 58ms
Test_MySum_IndirectFunc_List 58ms
Test_MySum_DirectCall_List 37ms
Test_Sum_DelegateLambda 109ms
Test_For_Inline 4ms
Test_For_Delegate 3ms
Test_ForUnrolled_Inline 4ms
Test_ForUnrolled_Delegate 4ms
Test_ForEach_Inline 38ms
Test_ForEach_Delegate 37ms

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

System.Linq.Enumerable.Sum 方法 (Test_Sum_Delegate) 与Test_MySum_IndirectFunc_IEnum 相同(忽略它们之间的4ms 差异)。这两种方法都使用IEnumerable&lt;T&gt; 迭代List&lt;T&gt;

更改方法以将List&lt;T&gt; 传递为List&lt;T&gt; 而不是IEnumerable&lt;T&gt;(在Test_MySum_IndirectFunc_List 中)消除了来自foreach 使用IEnumerator&lt;T&gt; 的虚拟调用,这导致从~114ms 减少到58ms,时间已经减少了 50%。

然后将Func&lt;Int64,Int64&gt;(委托)调用更改为GetValue func 为“静态”调用GetValue(如Test_MySum_DirectCall_List)将时间缩短到37ms - 这是相同的作为Test_ForEach_Delegate。这个方法和你手写的foreach循环一样。

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-02-22
    • 2015-03-07
    • 1970-01-01
    相关资源
    最近更新 更多