是的,如果您使用完全优化进行编译,现代编译器会使用 SSE2 自动矢量化。 clang 甚至在 -O2 处启用它,在 -O3 处启用 gcc。
即使在 -O1 或 -Os 下,编译器也会使用 SIMD 加载/存储指令来复制或初始化结构或其他比整数寄存器更宽的对象。这并不能真正算作自动矢量化。它更像是他们默认的内置 memset / memcpy 策略的一部分,用于固定大小的小块。但它确实利用并需要支持 SIMD 指令。
SSE2 是 x86-64 的基准/非可选,因此编译器在面向 x86-64 时始终可以使用 SSE1/SSE2 指令。必须手动启用以后的指令集(SSE4、AVX、AVX2、AVX512 和非 SIMD 扩展,如 BMI2、popcnt 等),以告诉编译器可以编写不能在旧 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。
为您正在编译的 CPU 制作二进制文件的最佳方法是 -O3 -march=native -mfpmath=sse,并使用链接时间优化 + 配置文件引导优化。 (gcc -fprofile-generate / 在一些测试数据上运行 / gcc -fprofile-use)。
如果编译器确实选择使用新指令,则使用-march=native 会生成可能无法在早期 CPU 上运行的二进制文件。 Profile-guided optimization 对 gcc 非常有帮助:没有它,它永远不会展开循环。但是对于 PGO,它知道哪些循环经常运行/进行大量迭代,即哪些循环是“热的”并且值得花更多的代码大小。链接时优化允许跨文件内联/恒定传播。 非常如果你的 C++ 中有很多你实际上没有在头文件中定义的小函数。
请参阅How to remove "noise" from GCC/clang assembly output?,了解有关查看编译器输出和理解它的更多信息。
这里有一些针对 x86-64 的具体示例 on the Godbolt compiler explorer。 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 上将吞吐量降低 2 倍!因为在 SKL 之前,FP add 只在一个执行端口上运行,但是有两个 FMA 单元可以运行乘法。 SKL 放弃了加法器,并以与 mul 和 FMA 相同的每时钟 2 次吞吐量和延迟运行 add。 (http://agner.org/optimize/,并在the x86 tag wiki 中查看其他性能链接。)
使用-march=haswell 编译允许编译器使用单个 FMA 进行缩放 + 添加。 (但 clang 不会将表达式收缩为 FMA,除非您使用 -ffast-math。IIRC 可以选择启用 FP 收缩而无需其他激进操作。)