在编译32位和64位时性能差异巨大(快26倍)

我试图衡量在访问值types和引用types列表时使用forforeach的区别。

我使用下面的类来进行分析。

 public static class Benchmarker { public static void Profile(string description, int iterations, Action func) { Console.Write(description); // Warm up func(); Stopwatch watch = new Stopwatch(); // Clean up GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); watch.Start(); for (int i = 0; i < iterations; i++) { func(); } watch.Stop(); Console.WriteLine(" average time: {0} ms", watch.Elapsed.TotalMilliseconds / iterations); } } 

我使用了double的价值types。 我创build了这个“假类”来testing引用types:

 class DoubleWrapper { public double Value { get; set; } public DoubleWrapper(double value) { Value = value; } } 

最后我跑了这段代码,比较了时间差异。

 static void Main(string[] args) { int size = 1000000; int iterationCount = 100; var valueList = new List<double>(size); for (int i = 0; i < size; i++) valueList.Add(i); var refList = new List<DoubleWrapper>(size); for (int i = 0; i < size; i++) refList.Add(new DoubleWrapper(i)); double dummy; Benchmarker.Profile("valueList for: ", iterationCount, () => { double result = 0; for (int i = 0; i < valueList.Count; i++) { unchecked { var temp = valueList[i]; result *= temp; result += temp; result /= temp; result -= temp; } } dummy = result; }); Benchmarker.Profile("valueList foreach: ", iterationCount, () => { double result = 0; foreach (var v in valueList) { var temp = v; result *= temp; result += temp; result /= temp; result -= temp; } dummy = result; }); Benchmarker.Profile("refList for: ", iterationCount, () => { double result = 0; for (int i = 0; i < refList.Count; i++) { unchecked { var temp = refList[i].Value; result *= temp; result += temp; result /= temp; result -= temp; } } dummy = result; }); Benchmarker.Profile("refList foreach: ", iterationCount, () => { double result = 0; foreach (var v in refList) { unchecked { var temp = v.Value; result *= temp; result += temp; result /= temp; result -= temp; } } dummy = result; }); SafeExit(); } 

我select了ReleaseAny CPU选项,运行该程序,并得到以下时间:

 valueList for: average time: 483,967938 ms valueList foreach: average time: 477,873079 ms refList for: average time: 490,524197 ms refList foreach: average time: 485,659557 ms Done! 

然后我select了Release和x64选项,运行该程序,得到以下时间:

 valueList for: average time: 16,720209 ms valueList foreach: average time: 15,953483 ms refList for: average time: 19,381077 ms refList foreach: average time: 18,636781 ms Done! 

为什么x64位版本如此之快? 我期望有一些差异,但不是这样大的。

我没有访问其他电脑。 你可以在你的机器上运行这个,告诉我结果吗? 我正在使用Visual Studio 2015,我有一个英特尔酷睿i7 930。

这里是SafeExit()方法,所以你可以自己编译/运行:

 private static void SafeExit() { Console.WriteLine("Done!"); Console.ReadLine(); System.Environment.Exit(1); } 

根据要求,使用double? 而不是我的DoubleWrapper

任何CPU

 valueList for: average time: 482,98116 ms valueList foreach: average time: 478,837701 ms refList for: average time: 491,075915 ms refList foreach: average time: 483,206072 ms Done! 

64位

 valueList for: average time: 16,393947 ms valueList foreach: average time: 15,87007 ms refList for: average time: 18,267736 ms refList foreach: average time: 16,496038 ms Done! 

最后但并非最不重要的:创build一个x86configuration文件给我几乎相同的结果使用Any CPU

我可以在4.5.2上重现这一点。 没有RyuJIT在这里。 x86和x64反汇编看起来都合理。 范围检查等等是一样的。 基本结构相同。 没有循环展开。

x86使用一组不同的浮点指令。 这些指令的执行似乎与除了除法之外的x64指令相当:

  1. 32位x87浮点指令在内部使用10个字节的精度。
  2. 扩展的精度划分非常慢。

除法操作使32位版本非常慢。 取消该部门的注释将使性能在很大程度上取得平衡 (从430毫秒降低到3.25毫秒)。

彼得·科德斯指出,两个浮点单位的指令延迟并不相同。 也许一些中间结果是非规范化的数字或NaN。 其中一个单位可能会引起缓慢的path。 或者,也许这两个值之间的差异,因为10字节与8字节的浮点精度。

Peter Cordes 也指出, 所有的中间结果都是NaN …解决这个问题( valueList.Add(i + 1) ,使得没有除数为零)大多均衡结果。 显然,32位代码根本不像NaN操作数。 让我们打印一些中间值: if (i % 1000 == 0) Console.WriteLine(result); 。 这证实了数据现在是理智的。

在进行基准testing时,您需要对实际工作量进行基准testing 但是谁会想到一个无辜的部门会搞砸你的基准呢?

尝试简单地总结数字来获得更好的基准。

分部和模数总是很慢。 如果将BCL Dictionary代码修改为不使用模运算符来计算可测量的桶索引性能。 这是多么缓慢的分工。

这是32位代码:

在这里输入图像说明

64位代码(结构相同,分区快):

在这里输入图像说明

尽pipe使用了SSE指令,但这并不是vector化的。

valueList[i] = i ,从i=0 ,所以第一个循环迭代的i=0 0.0 / 0.0因此,您的整个基准testing中的每个操作都是通过NaN完成的。

由于@usr在反汇编输出中显示 ,32位版本使用x87浮点,而64位使用SSE浮点。

我不是NaN的performance方面的专家,或者x87和SSE之间的差异,但我认为这解释了26x的差距。 我敢打赌,如果你初始化valueList[i] = i+1你的结果将会在32位和64位之间更接近。 (更新:usr证实这使得32位和64位性能相当接近。)

与其他业务相比,分工非常缓慢。 看看我对@ usr的回答的评论。 另请参阅http://agner.org/optimize/了解大量关于硬件的知识,并优化asm和C / C ++,其中一些与C#相关。 对于所有最近的x86 CPU,大多数指令都具有延迟和吞吐量的指令表。

但是,对于正常值,10B x87 fdiv并不比SSE2的8B双精度divsd慢得多。 IDK关于与NaN,无限或非规范性的性能差异。

不过,他们对NaN和其他FPUexception有不同的控制。 x87 FPU控制字独立于SSE舍入/exception控制寄存器(MXCSR)。 如果x87得到每个部门的CPUexception,但是SSE不是,那很容易解释26的因素。或者,当处理NaN时,性能差异可能很大。 NaN之后,通过NaN来搅动硬件并不是最佳的。

IDK,如果SSE控制避免与非正常减速将在这里发挥作用,因为我相信result将是NaN所有的时间。 IDK,如果C#在MXCSR中设置了denormals-are-zero标志,或者是flush-to-zero-flag(首先写入0,而不是在回读时将denormals作为零处理)。

我发现了一篇关于SSE浮点控制的英特尔文章 ,并将其与x87 FPU控制字进行了对比。 不过,关于NaN并没有太多的说法。 它以这个结尾:

结论

为了避免由于非正规化和下溢数字引起的序列化和性能问题,请使用SSE和SSE2指令在硬件内设置Flush-to-Zero和Denormals-Are-Zero模式,以实现浮点应用程序的最高性能。

IDK,如果这有助于除以零。

对于foreach

testing一个吞吐量受限的循环体,而不仅仅是一个循环携带的依赖链,可能会很有趣。 事实上,所有的工作都取决于以前的结果。 CPU没有什么可以并行执行的(除了边界检查,而mul / div链运行时检查下一个数组加载)。

如果“实际工作”占用了更多的CPU执行资源,则可能会在方法之间看到更多的差异。 另外,在英特尔Sandybridge之前,28uop环路缓冲区中的环路configuration与否有很大的区别。 如果不是,你会得到指令解码瓶颈,尤其是, 当平均指令长度较长时(发生在SSE)。 解码到多个uop的指令也会限制解码器的吞吐量,除非它们对解码器有好的模式(例如2-1-1)。 因此,一个更多循环开销指令的循环可以使循环符合28进入uopcaching的区别,这对Nehalem来说是一个大问题,有时在Sandybridge和以后也会有所帮助。

我们观察到所有浮点运算中有99.9%会涉及NaN,这至less非常不寻常(先由Peter Cordes发现)。 我们还有另一个usr的实验,发现删除除法指令会使时差几乎完全消失。

然而,事实是NaN只是由于第一个分数计算0.0 / 0.0而产生的,所以给出了初始的NaN。 如果不执行除法,结果将始终为0.0,我们将始终计算0.0 * temp – > 0.0,0.0 + temp – > temp,temp – temp = 0.0。 所以拆除师不仅拆除了师,还拆除了NaNs。 我认为NaN实际上是问题,一个实现处理NaN的速度很慢,而另一个则没有问题。

在i = 1处开始循环并再次测量是值得的。 四个运算结果* temp,+ temp,/ temp, – temp有效地加上(1 – temp),所以在大部分操作中我们不会有任何exception的数字(0,无穷大,NaN)。

唯一的问题可能是分区总是给出一个整数结果,并且当正确的结果不使用许多比特时,一些除法实现有捷径。 例如,除法310.0 / 31.0给出了10.0作为前四位,余数为0.0,并且一些实现可以停止评估其余的50位,而另一些则不能。 如果存在显着差异,那么以结果= 1.0 / 3.0开始循环将会有所作为。

为什么在您的机器上以64bit执行速度可能有几个原因。 我问你使用哪个CPU的原因是,当64位CPU首次出现时,AMD和Intel有不同的机制来处理64位代码。

处理器架构:

英特尔的CPU架构纯粹是64位。 为了执行32位代码,在执行之前需要将32位指令转换(在CPU内部)为64位指令。

AMD的CPU架构是在其32位架构之上构build64位的; 也就是说,它本质上是一个64位扩展的32位体系结构 – 没有代码转换过程。

这显然是几年前,所以我不知道是否/如何技术已经改变,但实质上,你会希望64位代码在64位机器上performance更好,因为CPU能够工作的倍数每个指令的位数。

.NET JIT

有人认为,由于JIT编译器能够根据您的处理器架构优化您的代码,因此.NET(以及Java等其他托pipe语言)能够胜过C ++等语言。 在这方面,您可能会发现JIT编译器正在使用64位体系结构中的某些东西,可能不可用,或者需要在32位中执行的解决方法。

注意:

你没有使用DoubleWrapper,而是考虑使用Nullable<double>或简写语法: double? – 我有兴趣看看是否对您的testing有任何影响。

注2:有些人似乎将我对64​​位体系结构的评论与IA-64相混淆。 只是为了澄清,在我的回答中,64位是指x86-64,32位是指x86-32。 这里没有提到IA-64!