ICC是否满足C99规范的复数乘法?

时间:2017-02-04 20:32:42

标签: c assembly complex-numbers avx icc

考虑这个简单的代码:

#include <complex.h>
complex float f(complex float x) {
  return x*x;
}

如果使用英特尔编译器使用-O3 -march=core-avx2 -fp-model strict编译它,则会得到:

f:
        vmovsldup xmm1, xmm0                                    #3.12
        vmovshdup xmm2, xmm0                                    #3.12
        vshufps   xmm3, xmm0, xmm0, 177                         #3.12
        vmulps    xmm4, xmm1, xmm0                              #3.12
        vmulps    xmm5, xmm2, xmm3                              #3.12
        vaddsubps xmm0, xmm4, xmm5                              #3.12
        ret 

这比从gccclang得到的代码简单得多,而且比在线复制数字的代码简单得多。例如,它没有明确地用于处理复杂的NaN或无穷大。

  

这个程序集是否符合C99复数乘法的规范?

2 个答案:

答案 0 :(得分:20)

代码不符合。

附件G第5.1节第4段内容

  

*/运算符满足所有实数,虚数和复数操作数的以下无穷大属性:

     

- 如果一个操作数是无穷大而另一个操作数是非零有限数或无穷大,那么*运算符的结果是无穷大;

因此,如果 z = a * i b是无限的且 w = c * i d是无限的,数字 z * w 必须是无限的。

同一附件第3节第1段定义了复数无限的含义:

  

具有至少一个无限部分的复数或虚数值被视为无穷大(即使其另一部分是NaN)。

所以如果a或b都是 z 是无限的 这确实是一个明智的选择,因为它反映了数学框架 1

但是,如果我们让 z =∞+ i ∞(无限值)和 w = i ∞(和无限值)英特尔代码的结果是 z * w = NaN + i NaN由于∞·0中间体< SUP> 2

这足以将其标记为不合格。

我们可以通过查看第一个引用的脚注(此处未报告脚注)进一步确认这一点,它提到了CX_LIMITED_RANGE pragma指令。

第7.3.4节,第1段内容

  

复数乘法,除法和绝对值的通常数学公式是有问题的,因为它们对无穷大的处理以及由于不适当的溢出和下溢。 CX_LIMITED_RANGE编译指示可用于通知实现(状态为''on'')通常的数学公式[产生NaN]是可接受的。

在这里,标准委员会正试图减轻复杂乘法(和除法)的巨大工作量 In fact GCC has a flag to control this behaviour

  

-fcx-limited-range
  启用时,此选项表示执行复杂除法时不需要范围缩减步骤。

     

此外,没有检查复数乘法或除法的结果是否为NaN + I * NaN,试图在这种情况下挽救这种情况。

     

默认值为-fno-cx-limited-range,但-ffast-math 启用。
  此选项控制ISO C99 CX_LIMITED_RANGE编译指示的默认设置。

单独使用makes GCC generate slow code and additional checks这个选项,没有它生成的代码与英特尔的代码有相同的缺陷(我将源代码翻译成C ++)

f(std::complex<float>):
        movq    QWORD PTR [rsp-8], xmm0
        movss   xmm0, DWORD PTR [rsp-8]
        movss   xmm2, DWORD PTR [rsp-4]
        movaps  xmm1, xmm0
        movaps  xmm3, xmm2
        mulss   xmm1, xmm0
        mulss   xmm3, xmm2
        mulss   xmm0, xmm2
        subss   xmm1, xmm3
        addss   xmm0, xmm0
        movss   DWORD PTR [rsp-16], xmm1
        movss   DWORD PTR [rsp-12], xmm0
        movq    xmm0, QWORD PTR [rsp-16]
        ret

没有它,代码就是

f(std::complex<float>):
        sub     rsp, 40
        movq    QWORD PTR [rsp+24], xmm0
        movss   xmm3, DWORD PTR [rsp+28]
        movss   xmm2, DWORD PTR [rsp+24]
        movaps  xmm1, xmm3
        movaps  xmm0, xmm2
        call    __mulsc3
        movq    QWORD PTR [rsp+16], xmm0
        movss   xmm0, DWORD PTR [rsp+16]
        movss   DWORD PTR [rsp+8], xmm0
        movss   xmm0, DWORD PTR [rsp+20]
        movss   DWORD PTR [rsp+12], xmm0
        movq    xmm0, QWORD PTR [rsp+8]
        add     rsp, 40
        ret

__mulsc3 function几乎与标准C99推荐的复数乘法相同 它包括上述检查。

1 其中数字的模数从实例扩展| z |对于复杂的“‖z”,保持无限的定义是无限制的结果。简单地说,在复平面中存在无限值的整个圆周,并且只需要一个“坐标”就可以获得无限模数。

2 如果我们记得 z = NaN + i ∞或 z =∞,情况会变得更糟+ i NaN是有效的无限值

答案 1 :(得分:10)

我从-O2 -march=core-avx2 -ffast-math处的clang 3.8获得了类似但不完全相同的代码:我对最近的x86浮点功能并不十分熟悉,但我认为它正在进行相同的计算但是使用不同的指令来重排寄存器中的值。

f:
        vmovshdup       %xmm0, %xmm1    # xmm1 = xmm0[1,1,3,3]
        vaddss  %xmm0, %xmm0, %xmm2
        vmulss  %xmm2, %xmm1, %xmm2
        vmulss  %xmm1, %xmm1, %xmm1
        vfmsub231ss     %xmm0, %xmm0, %xmm1
        vinsertps       $16, %xmm2, %xmm1, %xmm0 # xmm0 = xmm1[0],xmm2[0],xmm1[2,3]
        retq

GCC 6.3,具有相同的选项,似乎再次进行相同的计算,但以第三种方式改变值:

f:
        vmovq   %xmm0, -8(%rsp)
        vmovss  -4(%rsp), %xmm2
        vmovss  -8(%rsp), %xmm0
        vmulss  %xmm2, %xmm2, %xmm1
        vfmsub231ss     %xmm0, %xmm0, %xmm1
        vmulss  %xmm2, %xmm0, %xmm0
        vmovss  %xmm1, -16(%rsp)
        vaddss  %xmm0, %xmm0, %xmm0
        vmovss  %xmm0, -12(%rsp)
        vmovq   -16(%rsp), %xmm0
        ret

如果没有-ffast-math,两个编译器都会生成截然不同的代码 来检查NaN,至少。

我从中得出结论,即使使用-fp-model strict,英特尔的编译器也生成完全符合IEEE标准的复数乘法。可能还有一些其他命令行开关可以生成完全符合IEEE标准的代码。

这是否有资格违反 C99 取决于英特尔的编译器是否符合附件F和G(其中规定了C的实现对提供IEEE兼容性的意义)真实的和复杂的算术),如果是,你必须给出什么命令行选项来获得符合模式。