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

ix0qys7i  于 2023-01-01  发布在  其他
关注(0)|答案(1)|浏览(180)

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

oug3syen

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进行计算,然后必须在x87 st(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位代码生成。

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的功能):

.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)

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压缩,而不使用其他攻击性操作)。

相关问题