【问题标题】:Counting elements in an array -Performance-计算数组中的元素 - 性能 -
【发布时间】:2020-01-31 10:12:43
【问题描述】:

我有这个数组:var arr = new int[] { 1, 1, 0, -1, -1 }; 我需要计算正数、负数和零数的数量。 我使用foreach 循环和Linq 完成了它,并尝试使用Stopwatch 比较两种方法之间的性能这是我的代码:

        int pos = 0, neg = 0, zeros = 0;
        int p = 0, n = 0, z = 0;


        Stopwatch sw = new Stopwatch();

        sw.Start();

        pos = arr.Sum(e => e > 0 ? 1 : 0);
        neg = arr.Sum(e => e < 0 ? 1 : 0);
        zeros = arr.Sum(e => e == 0 ? 1 : 0);

        sw.Stop();


        Stopwatch sw2 = new Stopwatch();

        sw2.Start();

        foreach (var item in arr)
        {
            if (item > 0) p++;
            else if (item < 0) n++;
            else z++;
        }

        sw2.Stop();

        Console.WriteLine("Elapsed={0}", sw.Elapsed); //Elapsed=00:00:00.0008311
        Console.WriteLine("Elapsed2={0}", sw2.Elapsed); //Elapsed2=00:00:00.0000028

结果显示foreach 循环比Linq 方法(8311 毫秒)好很多(28 毫秒),所以我的问题是为什么会有这么多性能差异?

我什至尝试了三个foreach循环,一个计算负数,一个数正数,第三个数零,但性能仍然比Linq方法好!

提前感谢您的帮助!

【问题讨论】:

  • 那么,您的问题是什么?
  • 我相信这是因为您的阵列非常小。 Linq 在内部创建枚举器,这需要一些时间。但是,如果您有包含数千个项目的数组,我认为,Linq 可能会更快。
  • @PavelAnikhouski 为什么会有这么多性能差异?
  • 您正在使用 linq 方法创建 3 个序列,而不是 1 个 foreach 循环
  • 你也可以稍微改变一下:pos = arr.Count(e => e > 0);否定 = arr.Count(e => e e == 0);

标签: c# .net performance linq


【解决方案1】:

让我们以不同的方式进行赛马

private static string UnderTest(int size) {
  int pos = 0, neg = 0, zeros = 0;
  int p = 0, n = 0, z = 0;

  Random random = new Random(0);
  int[] arr = Enumerable
    .Range(0, size)
    .Select(x => random.Next(-1, 2))
    .ToArray();
  GC.Collect(GC.MaxGeneration);

  Stopwatch sw = new Stopwatch();
  sw.Start();

  // Three Linq loops (expected to be 3 three times slower)
  pos = arr.Sum(e => e > 0 ? 1 : 0);    
  neg = arr.Sum(e => e < 0 ? 1 : 0);
  zeros = arr.Sum(e => e == 0 ? 1 : 0);

  sw.Stop();

  Stopwatch sw2 = new Stopwatch();
  sw2.Start();

  // Just 1 loop 
  foreach (var item in arr) {
    if (item > 0) p++;
    else if (item < 0) n++;
    else z++;
  }

  sw2.Stop();

  return $"{sw.Elapsed} vs. {sw2.Elapsed} ratio: {((double)sw.Elapsed.Ticks) / sw2.Elapsed.Ticks:f3}";
} 

不同数组sizes的比赛:

   int[] loops = new int[] {
    1000,
    10_000,
    100_000,
    1_000_000,
    10_000_000,
    100_000_000,
    1000,         // <- 1000 again
  };

  string report = string.Join(Environment.NewLine, loops
    .Select(loop => $"loops: {loop,10} : {UnderTest(loop)}"));

  Console.Write(report);

结果:

loops:       1000 : 00:00:00.0006471 vs. 00:00:00.0000114 ratio: 56.763 // <- Warming up
loops:      10000 : 00:00:00.0003195 vs. 00:00:00.0001074 ratio: 2.975
loops:     100000 : 00:00:00.0037131 vs. 00:00:00.0010910 ratio: 3.403
loops:    1000000 : 00:00:00.0351574 vs. 00:00:00.0118858 ratio: 2.958
loops:   10000000 : 00:00:00.3729617 vs. 00:00:00.1198276 ratio: 3.112
loops:  100000000 : 00:00:03.7002508 vs. 00:00:01.1808595 ratio: 3.134
loops:       1000 : 00:00:00.0000322 vs. 00:00:00.0000099 ratio: 3.253 // <- Expected

发生了什么:我们有基于 3 Linq 的循环(3 调用 Sum),所以 Linq 慢 3 倍(不足为奇)。 然而,当系统加载程序集编译 IL 代码等时,我们在第一次运行时有一个巨大的异常值。 所以你有一个所谓的热身效果。

【讨论】:

  • 非常好的答案。但是,请您解释一下为什么在创建数组loops 后收集垃圾?
  • @Cid:谢谢! GC.Collect(GC.MaxGeneration); 更好的地方是在比赛前(UnderTest 内):我们不希望 GC 偶尔启动并干扰测量
  • 这是有道理的,我不习惯手动调用 GC,所以直接在代码中收集可以防止垃圾在街道上堆积(堆,嗯)收集器被自动调用?
  • @Cid:我们尽量避免不必要的进程(尤其是垃圾收集,它阻止世界)干扰。如果我们收集所有垃圾(例如,包含数百万个项目的数组),我们可以希望接下来的运行不会被中断(例如,在foreach 的中间)。
【解决方案2】:

Foreach 是一个简单 for 的精美表示,它遍历数组一次并同时计算所有 3 个值。

sum 创建一个临时变量(我们称之为 sum),并遍历数组,调用您作为参数提供的函数,返回它的值并将其添加到我们的临时变量中。这意味着,您调用 n 个函数,其中 n 是数组的长度。

正如您可能从中猜到的那样,调用一个函数(就时钟周期而言)比简单地将它们相加要昂贵得多。你也这样做了 3 次,每个 arr.sum 一次。

除此之外,(以及任何)函数、库与否都有一些开销,而且由于数组大小不是很大,所以开销很重要

【讨论】:

  • 对于 foreach 中的缓存,也可能会有一些小的性能提升,但它们很可能太小,在此示例中不重要
猜你喜欢
  • 2016-03-24
  • 2016-07-09
  • 2019-08-21
  • 2021-09-26
  • 1970-01-01
  • 1970-01-01
  • 2016-05-19
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多