编译器是否对常规 C 代码使用 SSE 指令?



我看到人们默认使用-msse -msse2 -mfpmath=sse标志,希望这会提高性能。我知道当 C 代码中使用特殊向量类型时,SSE 会参与其中。但是这些标志对常规 C 代码有什么区别吗?编译器是否使用 SSE 来优化常规 C 代码?

是的,如果您使用完全优化进行编译,现代编译器会使用 SSE2 自动矢量化。 Clang在-O2处矢量化循环,gcc在-O3处矢量化循环。

(GCC12 支持-O2矢量化,但仅在"非常便宜"的情况下,仍然需要-O3来矢量化大多数具有运行时可变行程计数的循环。

即使在-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 和非 SIMD 扩展,如 BMI2、popcnt 等(必须手动启用(例如-march=x86-64-v3-msse4.1(告诉编译器可以编写无法在旧CPU上运行的代码。 或者让它生成多个版本的代码并在运行时进行选择,但这会产生额外的开销,并且只值得用于较大的函数。

-msse -msse2 -mfpmath=sse已经是 x86-64 的默认值,但不是 32 位 i386 的默认值。 一些 32 位调用约定在 x87 寄存器中返回 FP 值,因此使用 SSE/SSE2 进行计算,然后必须存储/重新加载结果才能在 x87st(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 上运行的二进制文件。 按配置文件优化对 gcc 非常有帮助:没有它,它永远不会展开循环。 但是使用 PGO,它知道哪些循环经常运行/进行大量迭代,即哪些循环是"热"的,值得花费更多的代码大小。 链接时间优化允许跨文件内联/常量传播。 如果您C++有很多实际上没有在头文件中定义的小函数,这将非常有用


请参阅如何从 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)

乘数 =2.0f导致使用addps加倍,将 Haswell/Broadwell 的吞吐量降低 2 倍! 因为在SKL之前,FP加法只在一个执行端口上运行,但有两个FMA单元可以运行乘法。 SKL 放弃了加法器,并以与 mul 和 FMA 相同的每时钟吞吐量和延迟运行 2 个。 (http://agner.org/optimize/,并查看 x86 标记 wiki 中的其他性能链接。

使用-march=haswell进行编译允许编译器使用单个 FMA 进行缩放 + 添加。 (但是 clang 不会将表达式收缩到 FMA 中,除非您使用-ffast-math。 IIRC有一个选项可以在没有其他激进操作的情况下启用FP收缩。

最新更新