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

时间:2016-11-04 06:28:03

标签: c++ assembly optimization sse intrinsics

我正在使用英特尔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生成的代码很好,但是有vxorps(将绝对未使用的xmm3寄存器设置为零)和movss中的_fma指令相比较std_fma(依赖于编译器内在的std :: fma)

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和多个调用约定,但没有改变。

1 个答案:

答案 0 :(得分:3)

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

正如Ross Ridge在评论中指出的那样,编译器不够聪明,无法识别只使用XMM寄存器中最低的浮点元素,因此它会将其他三个元素归零{{1} } vxorps说明。这绝对没必要,但你能做什么?

需要注意的是, clang 3.9 在为英特尔内部函数生成程序集时比 GCC 6.2(或7.0的当前快照)做得好得多,因为它只在{ {1}}在我的例子中。我测试了更多的内在函数,在大多数情况下, clang 完成了单个指令的完美工作。

你能做什么

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

这还不够

编译器,如 GCC ,通过特殊处理NaN和无穷大来实现这些功能。因此,除了内在函数之外,它们还可以进行一些比较,分支和可能的_mm_fmadd_ss标记处理。

编译器标志<cmath> errno帮助 GCC clang 消除额外的浮点特殊情况和errno处理,如果可能,他们可以发出单个指令:https://godbolt.org/g/LZJyaB

您可以使用-fno-math-errno实现相同的功能,因为它还包含上述标志,但它includes much more than that,并且可能不需要那些(如不安全的数学优化)。

不幸的是,这不是便携式解决方案。 它适用于大多数情况(参见godbolt链接),但仍然依赖于实现。

还有

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

需要考虑的事项:

第一个 GCC / clang Visual Studio 使用不同的内联汇编语法,而Visual Studio不会t允许它在x64模式下。

第二您需要为AVX目标和非VEX编码发出VEX编码指令(3个op变体,例如-fno-trapping-math)(2个op变体,例如-ffast-math )用于预AVX CPU的变体。 VEX编码指令是3个操作数指令,因此它们为编译器提供了更多的自由优化。为了发挥他们的优势,必须正确设置register input/output parameters。这样的工作就像下面这样。

vsqrtss xmm0 xmm1 xmm2

但以下是VEX的一项糟糕技术:

sqrtss xmm0 xmm1

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

第3次正如Peter Cordes所指出的,对于内联汇编函数,您可能会丢失common subexpression elimination (CSE)constant folding (constant propagation)。但是,如果内联asm未声明为# if __AVX__ asm("vsqrtss %1, %1, %0" :"=x"(x) : "x"(x)); # else asm("sqrtss %1, %0" :"=x"(x) : "x"(x)); # endif ,则编译器可以将其视为纯函数,它仅依赖于其输入并执行公共子表达式消除,这很好。

彼得说:

  

Don't use inline asm不是绝对的规则,它只是你的一部分   在使用之前应该注意并仔细考虑。如果   替代品不符合您的要求,您最终不会满足   这内联到无法优化的地方,然后走右边   进取。