编译器为内部函数生成程序集的问题

Issues of compiler generated assembly for intrinsics

本文关键字:程序集 问题 内部函数 编译器      更新时间:2023-10-16

我正在使用英特尔SSE/AVX/FMA内部函数来实现一些数学函数的完美内联SSE/AVX指令。

给定以下代码

#include <cmath>
#include <immintrin.h>
auto std_fma(float x, float y, float z)
{
    return std::fma(x, y, z);
}
float _fma(float x, float y, float z)
{
    _mm_store_ss(&x,
        _mm_fmadd_ss(_mm_load_ss(&x), _mm_load_ss(&y), _mm_load_ss(&z))
    );
    return x;
}
float _sqrt(float x)
{
    _mm_store_ss(&x,
        _mm_sqrt_ss(_mm_load_ss(&x))
    );
    return x;
}

clang 3.9生成的程序集使用-march=x86-64 -mfma -O3

std_fma(float, float, float):                          # @std_fma(float, float, float)
        vfmadd213ss     xmm0, xmm1, xmm2
        ret
_fma(float, float, float):                             # @_fma(float, float, float)
        vxorps  xmm3, xmm3, xmm3
        vmovss  xmm0, xmm3, xmm0        # xmm0 = xmm0[0],xmm3[1,2,3]
        vmovss  xmm1, xmm3, xmm1        # xmm1 = xmm1[0],xmm3[1,2,3]
        vmovss  xmm2, xmm3, xmm2        # xmm2 = xmm2[0],xmm3[1,2,3]
        vfmadd213ss     xmm0, xmm1, xmm2
        ret
_sqrt(float):                              # @_sqrt(float)
        vsqrtss xmm0, xmm0, xmm0
        ret

虽然为_sqrt生成的代码很好,但与std_fma(依赖于编译器固有的std::fma)相比,_fma中有不必要的vxorps(将绝对未使用的xmm3寄存器设置为零)和movss指令

GCC 6.2生成的程序集使用-march=x86-64 -mfma -O3

std_fma(float, float, float):
        vfmadd132ss     xmm0, xmm2, xmm1
        ret
_fma(float, float, float):
        vinsertps       xmm1, xmm1, xmm1, 0xe
        vinsertps       xmm2, xmm2, xmm2, 0xe
        vinsertps       xmm0, xmm0, xmm0, 0xe
        vfmadd132ss     xmm0, xmm2, xmm1
        ret
_sqrt(float):
        vinsertps       xmm0, xmm0, xmm0, 0xe
        vsqrtss xmm0, xmm0, xmm0
        ret

和这里有很多不必要的vinsertps指令

示例:https://godbolt.org/g/q1BQym

默认的x64调用约定在XMM寄存器中传递浮点函数参数,因此应该消除那些vmovssvinsertps指令。为什么上面提到的编译器仍然会发出它们呢?有没有可能摆脱他们没有内联汇编?

我还尝试使用_mm_cvtss_f32代替_mm_store_ss和多个调用约定,但没有任何改变。

我根据评论,一些讨论和我自己的经验写下这个答案。

正如Ross Ridge在注释中指出的那样,编译器不够聪明,无法识别只使用了XMM寄存器中最低的浮点元素,因此它使用vxorps vinsertps指令将其他三个元素归零。这完全没有必要,但是你能做什么呢?

需要注意的是,clang 3.9在为Intel intrinsic生成程序集方面比GCC 6.2(或7.0的当前快照)做得好得多,因为在我的示例中,它只在_mm_fmadd_ss处失败。我还测试了更多的intrinsic,在大多数情况下,clang在发出单个指令方面做得很好。

你能做什么

你可以使用标准的<cmath>函数,如果有合适的CPU指令,希望它们被定义为编译器的内在函数。

这还不够

编译器,如GCC通过对NaN和无穷大的特殊处理来实现这些函数。因此,除了内在的,他们可以做一些比较,分支,和可能的errno标志处理。

编译器标志-fno-math-errno -fno-trapping-math帮助GCCclang消除额外的浮点特殊情况和errno处理,因此它们可以在可能的情况下发出单个指令:https://godbolt.org/g/LZJyaB.

您可以使用-ffast-math实现相同的功能,因为它也包含上述标志,但它包含的内容远不止这些,而这些(如不安全的数学优化)可能是不需要的。

不幸的是,这不是一个可移植的解决方案。它在大多数情况下都可以工作(参见godbolt链接),但是,您仍然依赖于实现。

你还可以使用内联汇编,这也是不可移植的,更棘手,有更多的事情要考虑。尽管如此,对于这样简单的一行指令,它还是可以的。

注意事项:

1st GCC/clangVisual Studio使用不同的内联汇编语法,Visual Studio不允许在x64模式下使用。

2nd对于AVX目标,您需要发出VEX编码的指令(3 op变体,例如vsqrtss xmm0 xmm1 xmm2),对于AVX之前的cpu,您需要发出非VEX编码的指令(2 op变体,例如sqrtss xmm0 xmm1)变体。VEX编码指令是3个操作数指令,因此它们为编译器优化提供了更多的自由。为了利用它们的优势,必须正确设置寄存器输入/输出参数。所以像下面这样的内容就可以了。

#   if __AVX__
    asm("vsqrtss %1, %1, %0" :"=x"(x) : "x"(x));
#   else
    asm("sqrtss %1, %0" :"=x"(x) : "x"(x));
#   endif

但是下面是一个不好的VEX技术:

asm("vsqrtss %1, %1, %0" :"+x"(x));

它可以产生不必要的移动指令,查看https://godbolt.org/g/VtNMLL。

3rd正如Peter Cordes指出的那样,内联汇编函数可能会丢失公共子表达式消除(CSE)和常量折叠(常量传播)。但是,如果内联asm没有声明为volatile,编译器可以将其视为仅依赖其输入的纯函数,并执行公共子表达式消除,这很好。

正如Peter所说:

"不要使用内联asm"不是绝对的规则,它只是你的一些想法使用前应注意并慎重考虑。如果替代方案不能满足您的要求,并且您不会以内联到无法优化的地方,然后往右转领先。