Jen*_* M. 5 c sse compilation simd compiler-optimization
我看到人们-msse -msse2 -mfpmath=sse默认使用标志,希望这会提高性能.我知道在C代码中使用特殊向量类型时SSE会参与进来.但是这些标志对常规C代码有什么影响吗?编译器是否使用SSE来优化常规C代码?
是的,如果使用完全优化进行编译,现代编译器将使用SSE2进行自动向量化.clang即使在-O2,gcc at -O3也可以使用它.
即使在-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如果编译器确实选择使用新指令,则使用make可能无法在早期CPU上运行的二进制文件.配置文件引导优化对gcc非常有用:它永远不会在没有它的情况下展开循环.但是对于PGO,它知道哪些循环经常运行/进行大量迭代,即哪些循环"热"并且值得花费更多的代码大小.链接时优化允许跨文件进行内联/常量传播.如果您的C++具有很多小函数,而这些函数实际上并未在头文件中定义,那将非常有用.
请参阅如何从GCC /铿锵声组件输出中删除"噪音"?有关查看编译器输出和理解它的更多信息.
以下是关于x86-64 的Godbolt编译器资源管理器的一些具体示例.Godbolt还有其他几种架构的gcc,并且你可以添加-target mips或者其他任何内容,所以你也可以看到ARM NEON的自动矢量化,并使用正确的编译器选项来启用它.您可以使用-m32x86-64编译器来获取32位代码.
int sumint(int *arr) {
int sum = 0;
for (int i=0 ; i<2048 ; i++){
sum += arr[i];
}
return sum;
}
Run Code Online (Sandbox Code Playgroud)
内循环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
Run Code Online (Sandbox Code Playgroud)
没有-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)
Run Code Online (Sandbox Code Playgroud)
乘数= 2.0f导致使用addps加倍,在Haswell/Broadwell上将吞吐量降低2倍!因为在SKL之前,FP add仅在一个执行端口上运行,但有两个FMA单元可以运行乘法.SKL放弃了加法器,运行时添加了与每个时钟吞吐量和延迟相同的2个mul和FMA.(http://agner.org/optimize/,并查看x86标签wiki中的其他性能链接.)
编译时-march=haswell允许编译器使用单个FMA进行缩放+添加.(但是除非你使用,否则clang不会将表达式收缩到FMA中-ffast-math.IIRC可以选择在没有其他激进操作的情况下启用FP收缩.)