我看到人们在默认情况下使用-msse -msse2 -mfpmath=sse标志,希望这能提高性能。我知道当C代码中使用特殊向量类型时,SSE会参与进来。但是这些标志对常规C代码有什么影响吗?编译器使用SSE来优化常规C代码吗?
-msse -msse2 -mfpmath=sse
oug3syen1#
是的,如果你编译的时候进行了完全优化,现代编译器会自动用SSE 2进行向量化。Clang在-O2向量化循环,gcc在-O3向量化。(GCC 12支持在-O2上进行矢量化,但只有当它“非常便宜”时才能实现,大多数具有运行时可变行程计数的循环仍然需要-O3进行矢量化。)即使在-O1或-Os上,编译器也会使用SIMD加载/存储指令来复制或初始化结构体或其他比整数寄存器更宽的对象。它更像是它们默认内置memset / memcpy策略的一部分,用于较小的固定大小的块。(如果没有-fno-builtin,这也适用于显式使用具有小常量长度的memcpy。)它确实利用了SIMD指令,并要求and enabled by the kernel支持SIMD指令,无论您是否称之为“矢量化”。(内核使用-mgeneral-regs-only,或在旧版GCC -mno-mmx -mno-sse中禁用此功能。)
-O2
-O3
-O1
-Os
-fno-builtin
memcpy
-mgeneral-regs-only
-mno-mmx -mno-sse
SSE 2是x86-64的基准/非可选指令,因此编译器在以x86-64为目标时始终可以使用SSE 1/SSE 2指令。(SSE 4、AVX、AVX 2、AVX 512和非SIMD扩展,如BMI 2、popcnt、等等)必须手动启用(例如-march=x86-64-v3或-msse4.1),以告知编译器可以生成无法在旧CPU上运行的代码。或者让它生成多个版本的代码并在运行时进行选择,但这会有额外的开销,而且只对较大的函数才值得。-msse -msse2 -mfpmath=sse已经是x86-64的默认值,但不是32位i386的默认值。某些32位调用约定返回x87寄存器中的FP值,因此使用SSE/SSE 2进行计算,然后必须在x87 st(0)中存储/重新加载结果可能会很不方便。对于-mfpmath=sse,更聪明的编译器可能仍然使用x87进行生成FP返回值的计算。
-march=x86-64-v3
-msse4.1
st(0)
-mfpmath=sse
在32位x86上,-msse2可能不是默认打开的,这取决于你的编译器是如何配置的。如果你使用32位是因为你的目标CPU太老了,它们 * 不能 * 运行64位代码,你可能需要确保它是禁用的,或者只启用-msse。
-msse2
-msse
针对您正在编译的CPU调优二进制文件的最佳方法是-O3 -march=native -mfpmath=sse,并使用链接时优化+配置文件导引优化.(gcc -fprofile-generate/ run on some test data /gcc -fprofile-use)。
-O3 -march=native -mfpmath=sse
-fprofile-generate
gcc -fprofile-use
使用-march=native生成的二进制文件可能无法在早期的CPU上运行,如果编译器选择使用新指令的话。没有它,它永远不会展开循环。但是有了PGO,它知道哪些循环经常运行/迭代很多,也就是说,哪些循环是“热”的,值得在上面花费更多的代码。链接时间优化允许跨文件的内联/常量传播。如果你有很多你实际上没有在头文件中定义的小函数的C++,这是非常有帮助的。
-march=native
**请参阅How to remove "noise" from GCC/clang assembly output?**了解有关查看编译器输出并理解其意义的更多信息。以下是Godbolt x86-64编译器资源管理器上的一些具体示例。Godbolt也有针对其他几种架构的gcc,使用clang可以添加-target mips或其他内容,因此您还可以看到ARM neon 的自动矢量化功能,并使用正确的编译器选项来启用它。您可以将-m32与x86-64编译器一起使用,以获得32位代码生成。
-target mips
-m32
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/AVX 2的功能):
gcc8.1 -O3
-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
如果没有-ffast-math,编译器就不能对FP操作重新排序,所以float的等价物不能自动向量化(参见Godbolt链接:您将获得标量addss)。(OpenMP可以在每个循环的基础上启用它,或者使用-ffast-math)。但是一些FP的东西可以安全地自动矢量化,而不改变操作的顺序。
-ffast-math
float
addss
// 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进行scale + add(但是clang不会将表达式压缩成FMA,除非使用-ffast-math。IIRC有一个选项可以启用FP压缩,而不使用其他攻击性操作)。
2.0f
addps
1条答案
按热度按时间oug3syen1#
是的,如果你编译的时候进行了完全优化,现代编译器会自动用SSE 2进行向量化。Clang在
-O2
向量化循环,gcc在-O3
向量化。(GCC 12支持在
-O2
上进行矢量化,但只有当它“非常便宜”时才能实现,大多数具有运行时可变行程计数的循环仍然需要-O3
进行矢量化。)即使在
-O1
或-Os
上,编译器也会使用SIMD加载/存储指令来复制或初始化结构体或其他比整数寄存器更宽的对象。它更像是它们默认内置memset / memcpy策略的一部分,用于较小的固定大小的块。(如果没有-fno-builtin
,这也适用于显式使用具有小常量长度的memcpy
。)它确实利用了SIMD指令,并要求and enabled by the kernel支持SIMD指令,无论您是否称之为“矢量化”。(内核使用-mgeneral-regs-only
,或在旧版GCC-mno-mmx -mno-sse
中禁用此功能。)SSE 2是x86-64的基准/非可选指令,因此编译器在以x86-64为目标时始终可以使用SSE 1/SSE 2指令。(SSE 4、AVX、AVX 2、AVX 512和非SIMD扩展,如BMI 2、popcnt、等等)必须手动启用(例如
-march=x86-64-v3
或-msse4.1
),以告知编译器可以生成无法在旧CPU上运行的代码。或者让它生成多个版本的代码并在运行时进行选择,但这会有额外的开销,而且只对较大的函数才值得。-msse -msse2 -mfpmath=sse
已经是x86-64的默认值,但不是32位i386的默认值。某些32位调用约定返回x87寄存器中的FP值,因此使用SSE/SSE 2进行计算,然后必须在x87st(0)
中存储/重新加载结果可能会很不方便。对于-mfpmath=sse
,更聪明的编译器可能仍然使用x87进行生成FP返回值的计算。在32位x86上,
-msse2
可能不是默认打开的,这取决于你的编译器是如何配置的。如果你使用32位是因为你的目标CPU太老了,它们 * 不能 * 运行64位代码,你可能需要确保它是禁用的,或者只启用-msse
。针对您正在编译的CPU调优二进制文件的最佳方法是
-O3 -march=native -mfpmath=sse
,并使用链接时优化+配置文件导引优化.(gcc-fprofile-generate
/ run on some test data /gcc -fprofile-use
)。使用
-march=native
生成的二进制文件可能无法在早期的CPU上运行,如果编译器选择使用新指令的话。没有它,它永远不会展开循环。但是有了PGO,它知道哪些循环经常运行/迭代很多,也就是说,哪些循环是“热”的,值得在上面花费更多的代码。链接时间优化允许跨文件的内联/常量传播。如果你有很多你实际上没有在头文件中定义的小函数的C++,这是非常有帮助的。**请参阅How to remove "noise" from GCC/clang assembly output?**了解有关查看编译器输出并理解其意义的更多信息。
以下是Godbolt x86-64编译器资源管理器上的一些具体示例。Godbolt也有针对其他几种架构的gcc,使用clang可以添加
-target mips
或其他内容,因此您还可以看到ARM neon 的自动矢量化功能,并使用正确的编译器选项来启用它。您可以将-m32
与x86-64编译器一起使用,以获得32位代码生成。带
gcc8.1 -O3
的内部循环(不带-march=haswell
或任何使能AVX/AVX 2的功能):如果没有
-ffast-math
,编译器就不能对FP操作重新排序,所以float
的等价物不能自动向量化(参见Godbolt链接:您将获得标量addss
)。(OpenMP可以在每个循环的基础上启用它,或者使用-ffast-math
)。但是一些FP的东西可以安全地自动矢量化,而不改变操作的顺序。
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进行scale + add(但是clang不会将表达式压缩成FMA,除非使用-ffast-math
。IIRC有一个选项可以启用FP压缩,而不使用其他攻击性操作)。