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

一些迭代器更快。 我发现了这一点,因为我从第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; } 

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

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

一些迭代器更快。

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

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

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

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

不,我真的不会。

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

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

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

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

就我们可能在C#中所做的代码而言,我们可能会进行一次展开的优化,尽管它很少是我们关心运行时间的地方。

假设我需要做x次。

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

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

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

然后我发现花费了实际代码时间的哪些部分。在应用程序的生命周期中,当用户单击按钮时,外部循环调用另一个需要400毫秒的部分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实现)。

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

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

我使用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_001e 。 但除此之外,还有很多事情要发生。

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

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

所以你可以看到循环比直接赋值更多。 正如其他人所说 – 这是循环展开的好处 – 不经常运行此循环开销,或者在您的示例中完全避免它。

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

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