我正在使用英特尔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寄存器中传递浮点函数参数,因此应删除那些vmovss
和vinsertps
指令。为什么提到的编译器仍会发出它们?没有内联汇编就可以摆脱它们吗?
我还尝试使用_mm_cvtss_f32
代替_mm_store_ss
和多个调用约定,但没有改变。
答案 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”不是绝对的规则,它只是你的一部分 在使用之前应该注意并仔细考虑。如果 替代品不符合您的要求,您最终不会满足 这内联到无法优化的地方,然后走右边 进取。