【问题标题】:Does compiler use SSE instructions for a regular C code?编译器是否对常规 C 代码使用 SSE 指令?
【发布时间】:2018-11-20 00:39:09
【问题描述】:

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

【问题讨论】:

  • 所有 x86-64(又名 AMD64)架构都有 SSE 和 SSE2。因此,如果您针对 x86-64 目标进行编译,编译器将使用 SSE 寄存器以及 SSE 和 SSE2 扩展。一般来说,C 编译器在向量化代码方面很差,所以大多数编译器会为 x86-64 上的“普通 C”发出非向量化的 SSE/SSE2 代码。 (我写这个是作为评论而不是作为答案,因为我不知道你在问什么。)
  • 我在问:将这些标志提供给 clang 或 gcc 是否合理,并期望不是根据矢量化类型编写的代码将获得性能。在这种情况下,编译器是否真的使用 SSE 指令来向量化某些东西?
  • 这取决于目标架构,以及使用的其他标志。它们肯定有助于 32 位 Intel (x86) 架构(代码可以在任何 x86-64 处理器(如果操作系统/内核支持 32 位代码)或任何支持 SSE2 的 x86 处理器上运行)。对于 x86-64 目标,它们是多余的(这些标志默认有效,基于目标)。如果编译器知道 SSE/SSE2 可用(基于架构,或从选项标志),它会尽力向量化甚至“常规 C”;总的来说,他们不是很擅长。

标签: c compilation compiler-optimization sse simd


【解决方案1】:

是的,如果您使用完全优化进行编译,现代编译器会使用 SSE2 自动矢量化。 clang 甚至在 -O2 处启用它,在 -O3 处启用 gcc。

即使在 -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 会生成可能无法在早期 CPU 上运行的二进制文件。 Profile-guided optimization 对 gcc 非常有帮助:没有它,它永远不会展开循环。但是对于 PGO,它知道哪些循环经常运行/进行大量迭代,即哪些循环是“热的”并且值得花更多的代码大小。链接时优化允许跨文件内联/恒定传播。 非常如果你的 C++ 中有很多你实际上没有在头文件中定义的小函数。


请参阅How to remove "noise" from GCC/clang assembly output?,了解有关查看编译器输出和理解它的更多信息。

这里有一些针对 x86-64 的具体示例 on the Godbolt compiler explorer。 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/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

没有-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 进行缩放 + 添加。 (但 clang 不会将表达式收缩为 FMA,除非您使用 -ffast-math。IIRC 可以选择启用 FP 收缩而无需其他激进操作。)

【讨论】:

  • 编译器还在其他不属于传统(循环)自动矢量化范围的地方使用 SSE。例如,即使在-O1-Osclang and gcc 也使用 SSE/AVX 指令进行结构复制。
  • " 以后的指令集(SSE4、AVX、AVX2、AVX512 和非 SIMD 扩展,如 BMI2、popcnt 等)必须手动启用才能告诉编译器可以编写不能在旧 CPU 上运行的代码。" -- "必须手动启用" 是什么意思?你的意思是我们必须传递所需的编译器标志?或者有一些系统级别的设置需要我们启用这些功能?我想是前者。
  • @Nawaz:我的意思是编译器标志使代码生成能够使用它们。所有支持 SSE、AVX 和 AVX512 的主流内核都会适当地设置 CPU 控制寄存器位以默认启用它们。
猜你喜欢
  • 2018-06-01
  • 2011-11-08
  • 1970-01-01
  • 2011-04-26
  • 2011-06-14
  • 2016-04-15
  • 1970-01-01
  • 2013-06-11
  • 1970-01-01
相关资源
最近更新 更多