为什么有些迭代器比C#中的其他迭代器更快?

时间:2014-01-30 01:52:22

标签: c# performance iterator

一些迭代器更快。我发现这一点是因为我在Channel 9上从Bob Tabor那里听说永远不会复制和粘贴。

我习惯做这样的事情来设置数组值:

testArray[0] = 0;
testArray[1] = 1;

这是一个简化的示例,但为了不复制和粘贴,或者不再输入内容,我想我应该使用循环。但我有这种唠叨的感觉,循环比简单地列出命令要慢,看起来我是对的:列出的东西要快得多。在我的大多数试验中,速度,最快到最慢,是列表,do循环,for循环,然后是while循环。

为什么列出内容比使用迭代器更快,为什么迭代器的速度不同?

如果我没有以最有效的方式使用这些迭代器,请帮助我。

以下是我的结果(对于2个int数组),我的代码在下面(对于4个int数组)。我在Windows 7 64位上尝试了几次。

enter image description here

要么我不擅长迭代,要么使用迭代器并不像它的那样伟大。请告诉我它是哪个。非常感谢。

int trials = 0;

TimeSpan listTimer = new TimeSpan(0, 0, 0, 0);
TimeSpan forTimer = new TimeSpan(0, 0, 0, 0);
TimeSpan doTimer = new TimeSpan(0, 0, 0, 0);
TimeSpan whileTimer = new TimeSpan(0, 0, 0, 0);
Stopwatch stopWatch = new Stopwatch();
long numberOfIterations = 100000000;

int numElements = 4;
int[] testArray = new int[numElements];
testArray[0] = 0;
testArray[1] = 1;
testArray[2] = 2;
testArray[3] = 3;

// List them
stopWatch.Start();
for (int x = 0; x < numberOfIterations; x++)
{
    testArray[0] = 0;
    testArray[1] = 1;
    testArray[2] = 2;
    testArray[3] = 3;
}
stopWatch.Stop();
listTimer += stopWatch.Elapsed;
Console.WriteLine(stopWatch.Elapsed);
stopWatch.Reset();

// for them
stopWatch.Start();
int q;
for (int x = 0; x < numberOfIterations; x++)
{
    for (q = 0; q < numElements; q++)
        testArray[q] = q;
}
stopWatch.Stop();
forTimer += stopWatch.Elapsed;
Console.WriteLine(stopWatch.Elapsed);
stopWatch.Reset();

// do them
stopWatch.Start();
int r;
for (int x = 0; x < numberOfIterations; x++)
{
    r = 0;
    do
    {
        testArray[r] = r;
        r++;
    } while (r < numElements);
}
stopWatch.Stop();
doTimer += stopWatch.Elapsed;
Console.WriteLine(stopWatch.Elapsed);
stopWatch.Reset();

// while
stopWatch.Start();
int s;
for (int x = 0; x < numberOfIterations; x++)
{
    s = 0;
    while (s < numElements)
    {
        testArray[s] = s;
        s++;
    }
}
stopWatch.Stop();
whileTimer += stopWatch.Elapsed;
Console.WriteLine(stopWatch.Elapsed);
stopWatch.Reset();
Console.WriteLine("listTimer");
Console.WriteLine(listTimer);
Console.WriteLine("forTimer");
Console.WriteLine(forTimer);
Console.WriteLine("doTimer");
Console.WriteLine(doTimer);
Console.WriteLine("whileTimer");
Console.WriteLine(whileTimer);

Console.WriteLine("Enter any key to try again the program");
Console.ReadLine();
trials++;

当我尝试使用4元素数组时,结果似乎变得更加明显。

我认为如果我通过像其他试验这样的变量分配listThem组的值,那将是公平的。它确实使listThem组稍慢,但它仍然是最快的。以下是几次尝试后的结果:

enter image description here

以下是我实施清单的方式:

int w = 0;
for (int x = 0; x < numberOfIterations; x++)
{
    testArray[w] = w;
    w++;
    testArray[w] = w;
    w++;
    testArray[w] = w;
    w++;
    testArray[w] = w;
    w = 0;
}

我知道这些结果可能是特定于实现的,但您会认为Microsoft会警告我们每个循环在速度方面的优缺点。你怎么看?感谢。

更新:根据我发布的注释代码和列表仍然比循环更快,但循环似乎更接近性能。循环从最快到最慢:for,while,然后执行。这有点不同,所以我的猜测是,虽然基本上是相同的速度,for循环比do和while循环快约半个百分点,至少在我的机器上。以下是一些试验的结果:enter image description here

2 个答案:

答案 0 :(得分:8)

  

有些迭代器更快。

当然,一些迭代器会做不同的事情。执行不同操作的不同代码将以不同的速度运行。

  

我习惯做这样的事情来设置数组值:

首先,这真的是你需要节省的时间吗?根据您的测量结果(如果它是调试版本,这是毫无意义的),您的额外代码似乎可以节省大约10纳秒。如果世界上的每个人都使用过您的应用程序一次,那么您保存所有用户的总时间仍然会少于刚刚输入的额外时间。他们中的任何一个都不会在任何一点思考&#34;好吧,那里有十纳秒我永远不会回来&#34;。

  

但你会认为微软会警告我们每个循环在速度方面的优缺点

不,我真的不会。

特别是当你进一步概括时。首先,使用更大的循环,等效的展开代码可能会更慢,因为循环可能适合指令行缓存,而解开的代码不会。

另一方面,迭代和枚举(平均而言往往比迭代更慢,但也不是很多)更灵活。它们将导致更小,更惯用的代码。这些适用于很多情况下,你所解决的那种情况要么适用,要么不适用(所以你因为不得不做一些令人费解的事情而失去你所期望的节省)。它们的误差范围较小,因为它们的范围较小。

首先,MS或其他任何人都不能建议总是用重复的复制粘贴语句填充您的代码,以节省几纳秒,因为它始终不是最快的方法,其次,由于其他代码优越的其他方式,他们不会这样做。

现在,确实存在节省几纳秒非常重要的情况,这就是我们做几十亿次的事情。如果一个芯片制造商敲了一个基本指令所需的几纳秒的时间,它就会真正赢得胜利。

就我们在C#中可能采用的代码类型而言,我们可能会进行一次展开的优化,尽管我们很少关注运行时间。

我们说我需要x次做一些事情。

首先,我做了显而易见的事情:

for(int i = 0; i != x; ++i)
  DoSomething();

让我们说我的整个申请并不像我需要的那么快。我做的第一件事就是考虑什么?#34;我需要的速度快#34;意思是,因为除非这是为了乐趣而编码(嘿,追求速度的荒谬努力可能很有趣),这是我想知道的第一件事。我得到了答案,或者更有可能是几个答案(最低可接受,最低目标,理想和市场营销 - 了解如何快速 - 这可能是不同的水平)。

然后我找到了实际代码时间的哪些部分。在应用程序的生命周期中,当需要400ms的另一个部分被外部循环调用1,000次时,无论何时用户都无需优化。单击一个按钮,导致4秒延迟。

然后我重新考虑我的整个方法 - 是&#34;这样做X次&#34; (这本身就是O(x)的时间复杂度),是达到我实际目标的唯一方法,或者我可以做一些完全不同的事情,也许是O(ln x)(也就是说,而不是花时间与X成正比时间与X的对数成正比。我是否可以缓存一些结果,以便在更长的初始运行时间内节省几毫秒的数毫秒?

然后我会看看我是否可以提高DoSomething()的速度。 99.9%的时间,我在那里做得比改变循环更好,因为它可能花费的时间比循环本身所花费的几纳秒还多。

我可能会在DoSomething()中做一些非常可怕的单一和令人困惑的事情,我通常认为它们是错误的代码,因为我知道这是它所在的地方。是值得的(而且我将评论不仅解释这个更混乱的代码如何工作,而且正是为什么它以这种方式完成)。我将测量这些变化,并且可能在几年后我会再次测量它们,因为当前CPU上使用当前框架的最快方法可能不是.NET 6.5上最快的方法,因为我们和#39;已将应用程序移至酷炫的新服务器上,使用英特尔于2017年推出的最新芯片。

很可能我会直接插入DoSomething()循环,因为调用函数的成本几乎肯定大于循环方法的成本(但不完全肯定,那里只有抖动内容以及它具有什么效果,这可能会令人惊讶。

也许,也许,我可以用以下内容替换实际循环:

if(x > 0)
  switch(x & 7)
  {
    case 0:
      DoSomething();
      goto case 7;
    case 7:
      DoSomething();
      goto case 6;
    case 6:
      DoSomething();
      goto case 5;
    case 5:
      DoSomething();
      goto case 4;
    case 4:
      DoSomething();
      goto case 3;
    case 3:
      DoSomething();
      goto case 2;
    case 2:
      DoSomething();
      goto case 1;
    case 1:
      DoSomething();
      if((x -= 8) > 0)
        goto case 0;
      break;
  }

因为这是一种结合循环在不占用大量指令内存方面的性能优势的一种方法,具有性能优势,您发现手动展开循环会带来短循环;它几乎将你的方法用于8个项目的组,并循环通过8个块。

为什么8?因为它是一个合理的起点;如果这对我的代码中的热点非常重要,我实际上会测量不同的大小。我唯一一次在真实(不仅仅是为了好玩)的.NET代码中做到这一点,我最终做了16块。

而且只有时间,每次迭代调用的指令都非常短(12条IL指令与C#代码*x++ = *y++对应)它是为代码设计的让其他代码快速执行某些操作的目的整个代码路径是我在大多数情况下避免遇到的问题,需要做更多的工作来确定何时我更好地使用或避免它,而不是尽可能快地完成这一点。

其余的时间,要么放松要么节省很多(如果有的话),要么它没有保存在重要的地方,或者在考虑它之前还有其他更迫切的优化要做。

我当然不会从这样的代码开始;这将是过早优化的定义。

通常,迭代很快。其他编码员也知道。抖动是已知的(在某些情况下可以应用一些优化)。这是可以理解的。它很短。它很灵活。使用foreach的规则也很快,尽管没有迭代那么快,而且更加灵活(有很多方法可以高效地使用IEnumerable实现)。

重复代码更脆弱,更容易隐藏一个愚蠢的错误(我们都写错误让我们思考&#34;这太愚蠢了,它几乎不足以算作一个错误&#34; ,这些很容易修复,只要你能找到它们)。维护起来比较困难,而且随着项目的进行,更容易变成更难维护的东西。从大局来看,更难以看到可以实现最大的性能提升。

总而言之,第九频道第一集的人并没有警告你,某些事情可能会让你的节目慢10ns,在某些情况下,他会被嘲笑。

答案 1 :(得分:2)

我使用ILDASM来查看for循环与直接赋值的IL。

用于直接赋值的IL,不使用循环,看起来像这样,每次赋值重复3次:

IL_0007:  ldloc.0
IL_0008:  ldc.i4.0
IL_0009:  ldc.i4.0
IL_000a:  stelem.i4

for循环的IL如下所示:

IL_0017:  ldc.i4.0
IL_0018:  stloc.1
IL_0019:  br.s       IL_0023
IL_001b:  ldloc.0
IL_001c:  ldloc.1
IL_001d:  ldloc.1
IL_001e:  stelem.i4
IL_001f:  ldloc.1
IL_0020:  ldc.i4.1
IL_0021:  add
IL_0022:  stloc.1
IL_0023:  ldloc.1
IL_0024:  ldc.i4.4
IL_0025:  blt.s      IL_001b
IL_0027:  ret

对阵列的分配是在IL_001bIL_001e的行上完成的。但除此之外,还有很多事情要发生。

循环中发生的第一件事不是赋值 - 它检查循环变量是否在范围内。所以它分支到IL_0023,然后返回到IL_001b以开始分配。

在赋值之后,它必须将循环计数器(IL_001f)递增到IL_0022)。然后它检查循环变量并再次分支。

所以你可以看到循环比直接赋值更多。正如其他人所说 - 这是循环展开的好处 - 不那么频繁地运行这个循环开销,或者在你的例子中完全避免它。

Jon关于JIT如何进行优化的观点也很重要。有了这样的微基准测试,CPU缓存和分支(这就是for循环正在做的事情)可能会对性能产生严重影响 - 因为你正在测量这么小的数字。

最终,如果循环的结构比循环中的操作更昂贵,那么来自循环的微小性能开销实际上很重要,那么您可能有一个循环展开的情况。但更有可能你有一个可以改进的设计。