编译器是否将SSE指令用于常规C代码?

时间:2018-06-10 17:25:46

标签: c compilation compiler-optimization sse simd

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

2 个答案:

答案 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选项。有关详细信息阅读编译器的精细手册