为什么.NET使用SIMD而不是x87进行与SIMD无关的数学运算?

9

这只是一个好奇的问题。我在查看代码分解(C#,64位,发布模式,VS 2012 RC):

            double a = 10d * Math.Log(20d, 2d);
000000c8  movsd       xmm1,mmword ptr [00000138h] 
000000d0  movsd       xmm0,mmword ptr [00000140h] 
000000d8  call        000000005EDC7F50 
000000dd  movsd       mmword ptr [rsp+58h],xmm0 
000000e3  movsd       xmm0,mmword ptr [rsp+58h] 
000000e9  mulsd       xmm0,mmword ptr [00000148h] 
000000f1  movsd       mmword ptr [rsp+30h],xmm0 
            a = Math.Pow(a, 6d);
000000f7  movsd       xmm1,mmword ptr [00000150h] 
000000ff  movsd       xmm0,mmword ptr [rsp+30h] 
00000105  call        000000005F758220 
0000010a  movsd       mmword ptr [rsp+60h],xmm0 
00000110  movsd       xmm0,mmword ptr [rsp+60h] 
00000116  movsd       mmword ptr [rsp+30h],xmm0 

我发现编译器在这里没有使用x87指令(Power使用Logs),感到有些奇怪。当然,我不知道调用位置的代码是什么,但我知道SIMD没有日志功能,这使得这个选择更加奇怪。此外,这里没有任何并行化处理,那么为什么要使用SIMD而不是简单的x87呢?

另外,我还发现很奇怪的一点是,x87 FYL2X 指令没有被使用,而它专门设计用于第一行代码中展示的情况。

有人能解释一下吗?


1
没有源代码,所以我将把这留作评论:我相信这是出于性能原因,SIMD通常会稍微快一些,但代价是准确性,它还避免了在更复杂的代码中数字在x87和SIMD寄存器之间的尴尬移位。x64还有16个XMM寄存器,是FPU协处理器拥有空间的两倍,减少了寄存器压力,并且意味着可以应用正常的寄存器分配技术,而不是试图强制堆栈为基础的x87寄存器。 - Necrolis
使用传统的fyl2x等指令将是一个性能下降。实际上,现在这些函数的SSE版本更加精确和快速。英特尔已经对此进行了一些基准测试,我手头没有确切的来源。 - Gunther Piez
2
@IanC 嗯,也许不是这样。x87内置的超越函数非常慢,甚至可能需要多达一百个周期。实际上,也许有比泰勒级数更好的方法 - 例如,sin和cos可以通过平均一个二次函数及其平方来近似(在范围缩减之后,你必须做到这一点),并且你可以玩弄指数字段以获得对ln的良好初始近似(之后你可以改进它)。 - harold
1
@IanC 关于为什么在32位系统中没有多使用SSE,我有一个理论:起初,他们不想麻烦地进行SSE检测,但所有的64位CPU都有SSE2,所以他们可以在那里使用它而无需检测。后来他们发现强制转换非常慢,因此他们只为那些情况添加了SSE(和检测),但不想重写所有其他部分。这只是一个理论。 - harold
我对SSE的了解不足,无法知道它为什么能解决强制转换问题。我猜测它只是比x87更擅长处理这个问题(而不是避免它)。 - IamIC
显示剩余14条评论
1个回答

10
这里有两个独立的问题。首先,为什么编译器要使用SSE寄存器而不是x87浮点堆栈来处理函数参数,其次,为什么编译器不直接使用能够计算对数的单个指令。
不使用对数指令最容易解释,x86中的对数指令被定义为精确到80位,而您正在使用双精度浮点数,它只有64位。将对数计算到64位而不是80位的精度要快得多,速度提高超过了必须在软件中执行而不是在硅片上执行的代价。
使用SSE寄存器更难以用令人满意的方式解释。简单的答案是,x64调用约定要求将前四个浮点参数传递给一个函数,从xmm0到xmm3。
下一个问题当然是,为什么调用约定告诉你这样做而不使用浮点堆栈。答案是,本地x64代码很少使用x87 FPU,而是使用SSE替代。这是因为在SSE中乘法和除法更快(80位与64位再次涉及),而且SSE寄存器更容易操作(在FPU中,您只能访问堆栈的顶部,并且旋转FPU堆栈通常是现代处理器上最慢的操作之一,实际上有些处理器专门为此目的增加了一个额外的管线阶段)。

谢谢。出于兴趣,XMM寄存器的宽度为128(或现在使用YMM扩展到256)。是否可能仅将1个double推入低64位,并使处理器仅评估该值?如果不行,将浪费内存带宽和电力。 - IamIC
2
指令movsd正在做这件事,它加载到XMMn的低64位中。此外,mulsd仅乘以寄存器的低半部分。 - jleahy
有道理。有趣的是,32位编译器比64位编译器执行更精确的数学运算:) - IamIC
我假设所有操作都有一个仅在低64位上运行的版本?我还假设32位单精度浮点数会被转换为64位进行处理? - IamIC
3
第一个是正确的,第二个未必正确。像 mulss 这样的指令只会将 XMMn 的最低 32 位中的单精度浮点数相乘。我不确定编译器通常如何处理单精度数学运算。 - jleahy
1
C和C++编译器对于float变量和计算使用单精度指令;如果.NET编译器浪费指令在cvtss2sd和转换上,我会感到惊讶,因为与x87不同,它不是免费的。x87是FPUs跨不同架构中的奇怪的一个;SSE/SSE2更像ARM、MIPS等的FP指令集(平面寄存器集,没有隐式转换)。 - Peter Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接