我看到人们默认使用-msse -msse2 -mfpmath=sse
标志,希望这会提高性能。我知道在C代码中使用特殊向量类型时SSE会参与进来。但是这些标志对常规C代码有什么影响吗?编译器是否使用SSE来优化常规C代码?
答案 0 :(得分:6)
是的,如果使用完全优化进行编译,现代编译器会使用SSE2进行自动向量化。 clang即使在-O2,gcc at -O3也能启用它。
即使在-O1或-Os,编译器也会使用SIMD加载/存储指令来复制或初始化比整数寄存器更宽的结构或其他对象。这并不算作自动矢量化;对于小型固定大小的块,它更像是默认内置memset / memcpy策略的一部分。但它确实利用了并且要求支持SIMD指令。
对于x86-64,SSE2是基线/非可选的,因此编译器在定位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位代码,您可能需要确保它已被禁用,或者只是{{1} }。
为正在编译的CPU调整二进制文件的最佳方法是-msse
,并使用链接时优化+配置文件引导优化。 (gcc -O3 -march=native -mfpmath=sse
/运行某些测试数据/ -fprofile-generate
)。
如果编译器确实选择使用新指令,则使用gcc -fprofile-use
会生成可能无法在早期CPU上运行的二进制文件。配置文件引导优化对gcc非常有用:它永远不会在没有它的情况下展开循环。但是对于PGO,它知道哪些循环经常运行/进行大量迭代,即哪些循环“热”并且值得花费更多的代码大小。链接时优化允许跨文件进行内联/常量传播。如果您的C ++具有许多您未在头文件中实际定义的小函数,那么非常非常有用。
有关查看编译器输出并了解它的更多信息,请参阅How to remove "noise" from GCC/clang assembly output? 。
以下是x86-64 的一些具体示例on the Godbolt compiler explorer。 Godbolt还有其他几种架构的gcc,并且你可以添加-march=native
或其他任何内容,因此你也可以看到ARM NEON的自动矢量化,并使用正确的编译器选项来启用它。您可以将-target mips
与x86-64编译器一起使用以获得32位代码。
-m32
带有int sumint(int *arr) {
int sum = 0;
for (int i=0 ; i<2048 ; i++){
sum += arr[i];
}
return sum;
}
的内部循环(没有gcc8.1 -O3
或任何启用AVX / AVX2的内容):
-march=haswell
如果没有.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
,编译器就无法重新排序FP操作,因此-ffast-math
等效项不会自动向量化(请参阅Godbolt链接:你得到标量float
)。 (OpenMP可以基于每个循环启用它,或使用addss
)。
但是一些FP的东西可以安全地自动矢量化而不会改变操作顺序。
-ffast-math
乘数= // 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
加倍,在Haswell / Broadwell上将吞吐量降低2倍!因为在SKL之前,FP add仅在一个执行端口上运行,但有两个FMA单元可以运行乘法。 SKL放弃了加法器,运行时添加了与每个时钟吞吐量和延迟相同的2个mul和FMA。 (http://agner.org/optimize/,并在the x86 tag wiki中查看其他效果链接。)
使用addps
进行编译可让编译器使用单个FMA进行缩放+添加。 (但是除非你使用-march=haswell
,否则clang不会将表达式收缩到FMA中.IIRC可以选择在没有其他激进操作的情况下启用FP收缩。)
答案 1 :(得分:-1)
一般来说,这是不可能的。但是,对于某些特定的C源代码和编译器,您可以通过查看生成的程序集来回答这个问题。几乎所有编译器都应该有一个创建汇编文件的选项。然后,您可以搜索SSE指令。
对于大多数Unix C编译器,请使用-S
选项。有关详细信息阅读编译器的精细手册。