编译器为内部函数生成程序集的问题
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生成的程序集使用-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寄存器中传递浮点函数参数,因此应该消除那些vmovss
和vinsertps
指令。为什么上面提到的编译器仍然会发出它们呢?有没有可能摆脱他们没有内联汇编?
我还尝试使用_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
帮助GCC和clang消除额外的浮点特殊情况和errno
处理,因此它们可以在可能的情况下发出单个指令:https://godbolt.org/g/LZJyaB.
您可以使用-ffast-math
实现相同的功能,因为它也包含上述标志,但它包含的内容远不止这些,而这些(如不安全的数学优化)可能是不需要的。
不幸的是,这不是一个可移植的解决方案。它在大多数情况下都可以工作(参见godbolt链接),但是,您仍然依赖于实现。
你还可以使用内联汇编,这也是不可移植的,更棘手,有更多的事情要考虑。尽管如此,对于这样简单的一行指令,它还是可以的。
注意事项:
1st GCC/clang和Visual 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"不是绝对的规则,它只是你的一些想法使用前应注意并慎重考虑。如果替代方案不能满足您的要求,并且您不会以内联到无法优化的地方,然后往右转领先。
- C++问题:用户认为数字1-100,程序提出问题不超过6次即可得到答案。无法正确
- 内联程序集printf将整数解释为地址
- 正在解码MSVC 32位版本的程序集(作业).没有手术做什么
- 具有外部"c"和程序集的未定义函数
- 用于将C++代码转换为 Web 程序集的脚本未终止
- 为什么我的C++程序的程序集输出充满了 .ascii,没有汇编代码?
- CoreCLR 中的检测探查器 - 将帮助程序程序集加载到 dotnet 进程的方法
- 不同于按值传递和常量引用传递的程序集
- 为什么在堆栈和堆上创建变量会产生相同的程序集代码?
- C++变量在调用 x64 程序集函数后重置为 0
- 如何将C++子例程链接到 x86 程序集程序?
- Qt 网页程序集缓存
- 测试操作系统时执行程序集"sti"时虚拟框崩溃
- 为什么从 constexpr 引用生成的程序集代码与从 constexpr 指针生成的程序集代码不同?
- 将内联程序集尾调用函数尾声替换为用于x86/x64 msvc的Intrinsics
- Visual Studio:C++\CLI 包装程序集路径依赖性问题
- visual studio c++中的内联程序集问题
- 以下内联程序集有什么问题
- 编译器为内部函数生成程序集的问题
- 调用windows API内联程序集时出现问题