我看到人们默认使用-msse -msse2 -mfpmath=sse
标志,希望这样可以提高性能。我知道当在C代码中使用特殊向量类型时,SSE会被启用。但是这些标志对于常规的C代码有任何区别吗?编译器是否使用SSE来优化常规的C代码?
我看到人们默认使用-msse -msse2 -mfpmath=sse
标志,希望这样可以提高性能。我知道当在C代码中使用特殊向量类型时,SSE会被启用。但是这些标志对于常规的C代码有任何区别吗?编译器是否使用SSE来优化常规的C代码?
-O2
处向量化循环,gcc在-O3
处向量化循环。
(GCC12在-O2
时启用向量化,但仅当成本非常低廉时, 仍需要-O3
来向量化大多数带有运行时变量trip counts的循环。)-O1
或-Os
下,编译器也会使用SIMD加载/存储指令来复制或初始化比整数寄存器宽的结构体或其他对象。这并不真正算作自动向量化;它更像是默认内置的memset/memcpy策略的一部分,用于小型固定大小块。(如果没有-fno-builtin
,这也将适用于小常量长度的显式使用memcpy
。)它确实利用和需要支持SIMD指令并由内核启用,无论您是否称其为“向量化”。(内核使用-mgeneral-regs-only
,或在旧版GCC中使用-mno-mmx -mno-sse
来禁用此功能。)
SSE2是x86-64的基线/非可选项,因此编译器在针对x86-64时总是可以使用SSE1/SSE2指令。稍后的指令集(SSE4、AVX、AVX2、AVX512以及像BMI2、popcnt等非SIMD扩展)必须手动启用(例如-march=x86-64-v3
或-msse4.1
),告诉编译器可以生成不能在旧CPU上运行的代码。或者生成多个版本的代码并在运行时进行选择,但这会增加额外的开销,只有对于较大的函数才值得。
-msse -msse2 -mfpmath=sse
已经是x86-64的默认设置,但不适用于32位i386。一些32位调用约定在x87寄存器中返回FP值,因此使用SSE/SSE2进行计算然后必须将结果存储/重新加载到x87 st(0)
中可能不方便。使用-mfpmath=sse
,更智能的编译器可能仍会使用x87进行产生FP返回值的计算。
在32位x86上,默认情况下可能没有启用-msse2
,这取决于编译器的配置方式。如果您正在使用32位,因为您的目标CPU太旧而无法运行64位代码,则可能希望确保禁用它,或仅使用-msse
。
-O3 -march=native -mfpmath=sse
可以让二进制文件针对你编译的CPU进行调优,同时使用链接时优化和基于性能的优化。在测试数据上运行gcc -fprofile-generate
,然后运行gcc -fprofile-use
。使用-march=native
会使得二进制文件无法在旧的CPU上运行,因为编译器可能使用了新的指令。对于gcc来说,基于性能的优化非常有帮助:如果没有它,它永远不会展开循环。但是通过PGO,它知道哪些循环经常运行/迭代很多次,即哪些循环是“热点”,值得花费更多的代码大小进行优化。链接时优化允许在文件之间进行内联和常量传播。如果你拥有许多小函数却没有在头文件中定义,它非常有帮助。
请参考如何从GCC / clang汇编输出中去除“噪音”?,了解更多有关查看编译器输出并理解其含义的内容。
这里有一些关于x86-64的具体例子在Godbolt编译器浏览器上。Godbolt还支持其他几种架构的gcc,而且使用clang时可以添加-target mips
或其他选项,因此您还可以看到启用了正确编译器选项的ARM NEON的自动向量化。您可以使用-m32
与x86-64编译器一起生成32位代码。int sumint(int *arr) {
int sum = 0;
for (int i=0 ; i<2048 ; i++){
sum += arr[i];
}
return sum;
}
使用gcc8.1 -O3
进行内部循环(不使用-march=haswell
或任何启用AVX / AVX2的选项):
.L2: # do {
movdqu xmm2, XMMWORD PTR [rdi] # load 16 bytes
add rdi, 16
paddd xmm0, xmm2 # packed add of 4 x 32-bit integers
cmp rax, rdi
jne .L2 # } while(p != endp)
# then horizontal add and extract a single 32-bit sum
没有-ffast-math
,编译器无法重新排序FP操作,因此float
等效项不会自动矢量化(参见Godbolt链接:您将获得标量addss
)。(OpenMP可以在每个循环基础上启用它,或使用-ffast-math
)。
但是,一些FP内容可以安全地自动矢量化而不改变操作顺序。
// clang won't contract this into an FMA without -ffast-math :/
// but gcc will (if you compile with -march=haswell)
void scale_array(float *arr) {
for (int i=0 ; i<2048 ; i++){
arr[i] = arr[i] * 2.1f + 1.234f;
}
}
# load constants: xmm2 = {2.1, 2.1, 2.1, 2.1}
# xmm1 = (1.23, 1.23, 1.23, 1.23}
.L9: # gcc8.1 -O3 # do {
movups xmm0, XMMWORD PTR [rdi] # load unaligned packed floats
add rdi, 16
mulps xmm0, xmm2 # multiply Packed Single-precision
addps xmm0, xmm1 # add Packed Single-precision
movups XMMWORD PTR [rdi-16], xmm0 # store back to the array
cmp rax, rdi
jne .L9 # }while(p != endp)
当multiplier = 2.0f
时,使用addps
来进行加倍,会使Haswell / Broadwell的吞吐量减少一半!因为在SKL之前,FP加法只在一个执行端口上运行,但有两个FMA单元可以运行乘法。 SKL取消了加法器,并使用与mul和FMA相同的每个时钟吞吐量和延迟来运行add。(http://agner.org/optimize/,请参见the x86 tag wiki中的其他性能链接。)
使用-march=haswell
编译可以让编译器为scale + add使用单个FMA。(但是,除非使用-ffast-math
,否则clang不会将表达式缩减为FMA。我IRC还有一个选项可以启用FP缩减而不进行其他激进操作。)