Skip to main content
 首页 » 编程设计

c#之为什么在 C# 中有些迭代器比其他迭代器快

2024年05月10日9lhb25

一些迭代器更快。我发现这一点是因为我在 Channel 9 上收到了 Bob Tabor 的来信永远不要复制和粘贴。

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

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

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

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

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

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



要么我不擅长迭代,要么使用迭代器并不像它想象的那么好。请让我知道它是哪个。非常感谢。

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 组变慢了一点,但它仍然是最快的。以下是几次尝试后的结果:



这是我如何实现列表:
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 和 do。这有点不同,所以我的猜测是 do 和 while 的速度基本上相同,for 循环比 do 和 while 循环快大约 0.5%,至少在我的机器上是这样。以下是一些试验的结果:

请您参考如下方法:

Some iterators are faster.



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

I was in the habit of doing something like this to set array values:



首先,这真的是您需要节省的时间吗?根据您的测量(如果是调试版本则毫无意义),您的额外代码似乎为您节省了大约 10 纳秒。如果世界上每个人都使用过您的应用程序,那么您为所有用户节省的总时间仍将少于仅用于输入的额外时间。他们在任何时候都不会想“好吧,有十纳秒我永远不会回来”。

but you would think Microsoft would warn us of the advantages and disadvantages of each loop when it comes to speed



不,我真的不会。

尤其是当你进一步概括时。一方面,对于较大的循环,等效的展开代码很可能会变慢,因为循环可能适合指令行缓存,而展开代码则不会。

另一方面,迭代和枚举(平均而言往往比迭代慢,但也慢得多)灵活得多。它们将导致更小、更惯用的代码。这些适用于许多情况,其中您的展开类型不适用或不容易适用(因此,由于不得不做一些令人费解的事情,您会失去预期的任何节省)。他们的错误范围更小,仅仅是因为他们对任何事情的范围都更小。

因此,首先 MS 或其他任何人不能建议总是用重复的复制粘贴语句页面填充您的代码以节省几纳秒,因为无论如何它并不总是最快的方法,其次他们不会这样做因此,由于所有其他方式,其他代码更胜一筹。

现在,确实在某些情况下,节省几纳秒真的很重要,这就是我们要做数十亿次的事情。如果芯片制造商将基本指令所需的时间缩短几纳秒,那么加起来就是真正的胜利。

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

假设我需要做一些事情 x次。

首先,我做了显而易见的事情:
for(int i = 0; i != x; ++i) 
  DoSomething(); 

假设我的整个应用程序没有我需要的那么快。我做的第一件事就是考虑“按我需要的速度”是什么意思,因为除非这是为了好玩而编码(嘿,追求速度的荒谬努力可能很有趣),这是我想知道的第一件事。我得到了一个答案,或者更有可能是几个答案(最低可接受、最低目标、理想和营销 - 开始吹嘘 - 这有多快 - 可能是不同的水平)。

然后我发现实际代码时间的哪些部分被花费了。 当用户点击一个按钮时,当另一个需要 400ms 的部分被外循环调用 1000 次时,优化在应用程序的生命周期中需要 10ns 的东西是没有意义的,导致 4 秒延迟。

然后我重新考虑我的整个方法 - 是“做 X 次”(时间复杂度本质上是 O(x)),这是实现我的实际目标的唯一方法,或者我可以做一些完全不同的事情,也许 O(ln x )(也就是说,时间不是与 X 成正比,而是与 X 的对数成正比)。我可以缓存一些结果,以便获得更长的初始运行时间,我可以节省几毫秒数千次吗?

那我看看能不能提高 DoSomething()的速度.在 99.9% 的情况下,我会比改变循环做得更好,因为它可能比循环本身花费的几纳秒花费更多的时间。

我可能会在 DoSomething() 中做一些非常可怕的单调和令人困惑的事情我通常会认为这是糟糕的代码,因为我知道这是值得的地方(我将评论不仅解释这个更令人困惑的代码是如何工作的,而且确切地解释为什么这样做)。我将衡量这些变化,可能几年后我会再次衡量它们,因为在当前 CPU 上使用当前框架的最快方法可能不是 .NET 6.5 上最快的方法,因为我们已经移动了应用程序安装到带有英特尔 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 实现)。

重复的代码更脆弱,更容易隐藏愚蠢的错误(我们都编写错误让我们认为“太愚蠢了,几乎不足以算作错误”,这些很容易修复,只要你可以找到它们)。随着项目的进行,它更难维护,并且更有可能变成更难维护的东西。更难看到大局,而在大局中可以实现最大的性能改进。

总之,第 9 channel 的那个人没有警告你在某些情况下可能会使你的程序慢 10ns 的原因是他会被 mock 。