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

Issues of compiler generated assembly for 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 生成的程序集 with -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 生成的代码很好,但在 _fma 中有不必要的 vxorps(将绝对未使用的 xmm3 寄存器设置为零)和 movss 指令与 std_fma(依赖于编译器内部 std::fma)

相比

GCC 6.2 生成程序集 with -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 在为英特尔生成程序集方面比 GCC 6.2(或 7.0 的当前快照) 做得更好内在函数,因为在我的示例中它仅在 _mm_fmadd_ss 处失败。我也测试了更多内在函数,在大多数情况下 clang 完美地发出了单个指令。

你能做什么

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

这还不够

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

编译器标志 -fno-math-errno -fno-trapping-math 帮助 GCCclang 消除额外的浮点特殊案例和 errno handling, so they can emit single instructions if possible: https://godbolt.org/g/LZJyaB.

您可以使用 -ffast-math 实现相同的效果,因为它也包含上述标志,但它 includes much more than that 和那些(如不安全的数学优化)可能是不需要的。

不幸的是,这不是一个可移植的解决方案。 它在大多数情况下都有效(参见 godbolt link),但仍然取决于实现。

还有什么

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

需要考虑的事项:

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

2nd 您需要为 AVX 目标发出 VEX 编码指令(3 op 变体,例如 vsqrtss xmm0 xmm1 xmm2),以及非 VEX 编码指令(2 op 变体,例如sqrtss xmm0 xmm1) AVX CPU 之前的变体。 VEX 编码指令是 3 个操作数指令,因此它们为编译器提供了更大的优化自由度。要利用它们,必须正确设置 register input/output parameters。所以像下面这样的东西就可以了。

#   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 指出的那样,您可以为内联汇编函数丢失 common subexpression elimination (CSE) and constant folding (constant propagation)。然而,如果内联 asm 没有声明为 volatile,编译器可以将其视为仅依赖于其输入的纯函数并执行公共子表达式消除,这很棒。

正如彼得所说:

"Don't use inline asm" isn't an absolute rule, it's just something you should be aware of and consider carefully before using. If the alternatives don't meet your requirements, and you don't end up with this inlining into places where it can't optimize, then go right ahead.