编译器为内部函数生成程序集的问题
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 寄存器中传递浮点函数参数,因此应该删除那些 vmovss
和 vinsertps
指令。为什么提到的编译器仍然发出它们?是否可以不用内联汇编来摆脱它们?
我也尝试使用 _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
帮助 GCC 和 clang 消除额外的浮点特殊案例和 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/clang 和 Visual 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.
我正在使用英特尔 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 寄存器中传递浮点函数参数,因此应该删除那些 vmovss
和 vinsertps
指令。为什么提到的编译器仍然发出它们?是否可以不用内联汇编来摆脱它们?
我也尝试使用 _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
帮助 GCC 和 clang 消除额外的浮点特殊案例和 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/clang 和 Visual 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.