【问题标题】:Weird optimization results for this multiply-add code这个乘加代码的奇怪优化结果
【发布时间】:2018-08-21 13:31:03
【问题描述】:

我正在编译这段代码:

#include <cstdint>

template <typename T>
struct vec{ T v[4]; };

template <typename T>
vec<T> foo (vec<T> x, vec<T> y, vec<T> z) {
    return {
        x.v[0] + y.v[0] * z.v[0],
        x.v[1] + y.v[1] * z.v[1],
        x.v[2] + y.v[2] * z.v[2],
        x.v[3] + y.v[3] * z.v[3]
    };
}

template vec<int64_t> foo ( vec<int64_t> x, vec<int64_t> y, vec<int64_t> z);
template vec<float> foo ( vec<float> x, vec<float> y, vec<float> z);

最大优化,使用 clang 6.0 和 gcc 7.3。但是results 很奇怪:

  • 没有编译器使用融合乘加 - 用于整数或浮点数,尽管这些似乎是显而易见的选择。为什么?
  • gcc 对 int64_t 情况(不适用于 float 情况)使用了无数条指令,这比在 -O2 处的 clang 和本身要多得多。真的更快吗?

clang 6.0:

vec<long> foo<long>(vec<long>, vec<long>, vec<long>):             # @vec<long> foo<long>(vec<long>, vec<long>, vec<long>)
        mov     rax, qword ptr [rsp + 72]
        imul    rax, qword ptr [rsp + 40]
        add     rax, qword ptr [rsp + 8]
        mov     qword ptr [rdi], rax
        mov     rax, qword ptr [rsp + 80]
        imul    rax, qword ptr [rsp + 48]
        add     rax, qword ptr [rsp + 16]
        mov     qword ptr [rdi + 8], rax
        mov     rax, qword ptr [rsp + 88]
        imul    rax, qword ptr [rsp + 56]
        add     rax, qword ptr [rsp + 24]
        mov     qword ptr [rdi + 16], rax
        mov     rax, qword ptr [rsp + 96]
        imul    rax, qword ptr [rsp + 64]
        add     rax, qword ptr [rsp + 32]
        mov     qword ptr [rdi + 24], rax
        mov     rax, rdi
        ret
vec<float> foo<float>(vec<float>, vec<float>, vec<float>):             # @vec<float> foo<float>(vec<float>, vec<float>, vec<float>)
        mulps   xmm2, xmm4
        addps   xmm0, xmm2
        mulps   xmm3, xmm5
        addps   xmm1, xmm3
        ret

GCC 7.3:

vec<long> foo<long>(vec<long>, vec<long>, vec<long>):
        movdqu  xmm3, XMMWORD PTR [rsp+56]
        mov     rax, rdi
        movdqu  xmm4, XMMWORD PTR [rsp+88]
        movdqa  xmm1, xmm3
        movdqa  xmm0, xmm3
        movdqa  xmm2, xmm4
        movdqu  xmm5, XMMWORD PTR [rsp+72]
        pmuludq xmm1, xmm4
        psrlq   xmm0, 32
        psrlq   xmm2, 32
        pmuludq xmm0, xmm4
        pmuludq xmm2, xmm3
        movdqu  xmm4, XMMWORD PTR [rsp+40]
        paddq   xmm0, xmm2
        psllq   xmm0, 32
        paddq   xmm0, xmm1
        movdqa  xmm3, xmm5
        movdqu  xmm1, XMMWORD PTR [rsp+24]
        movdqa  xmm2, xmm4
        psrlq   xmm3, 32
        pmuludq xmm3, xmm4
        paddq   xmm1, xmm0
        movdqu  xmm6, XMMWORD PTR [rsp+8]
        pmuludq xmm2, xmm5
        movdqa  xmm0, xmm4
        movups  XMMWORD PTR [rdi+16], xmm1
        psrlq   xmm0, 32
        pmuludq xmm0, xmm5
        paddq   xmm0, xmm3
        psllq   xmm0, 32
        paddq   xmm0, xmm2
        paddq   xmm0, xmm6
        movups  XMMWORD PTR [rdi], xmm0
        ret
vec<float> foo<float>(vec<float>, vec<float>, vec<float>):
        movq    QWORD PTR [rsp-40], xmm2
        movq    QWORD PTR [rsp-32], xmm3
        movq    QWORD PTR [rsp-56], xmm0
        movq    QWORD PTR [rsp-24], xmm4
        movq    QWORD PTR [rsp-16], xmm5
        movq    QWORD PTR [rsp-48], xmm1
        movaps  xmm0, XMMWORD PTR [rsp-40]
        mulps   xmm0, XMMWORD PTR [rsp-24]
        addps   xmm0, XMMWORD PTR [rsp-56]
        movaps  XMMWORD PTR [rsp-56], xmm0
        mov     rax, QWORD PTR [rsp-48]
        movq    xmm0, QWORD PTR [rsp-56]
        mov     QWORD PTR [rsp-56], rax
        movq    xmm1, QWORD PTR [rsp-56]
        ret

【问题讨论】:

  • 看起来 gcc 使用压缩 32x32 => 64 位乘法自动矢量化 64 位整数乘法,每对乘法使用 3 pmuludq。这看起来不像是一场胜利,尽管请注意 Skylake 对于pmuludq 的每个时钟吞吐量为 2,但对于 64 位整数乘法,每个时钟只有 1 个。这可能是 AVX2 的胜利,使用 4 个 int64_t 的向量完成整个操作,并使用 3 操作数 VEX 指令避免大部分 movdqa
  • TL:DR:看起来过于激进的自动矢量化。您是否对吞吐量和/或延迟进行了微基准测试?它可能在这两个方面都更糟,但可能不会超过 2 倍,甚至可能比这更糟糕。
  • 没有编译器使用融合乘加:您编写了整数代码。唯一的整数 mul+add 指令是水平加法,如github.com/HJLebbink/asm-dude/wiki/PMADDWD,直到 AVX512IFMA,而VPMADD52LUQ 仅对整数元素的低 52 位进行操作。 (这是double 的尾数宽度并非巧合:AVX512-IFMA 的重点显然是暴露 FMA 单元以供整数使用,而无需实际将其构建得更宽。
  • @PeterCordes:见编辑;花车也不会发生这种情况。另外,不,我想了解其基本原理是什么。另外 - 我只有一个平台可以进行微基准测试。
  • FMA 不是 x86-64 的基准!如果 gcc 默认使用 FMA 指令,它会在某些机器上生成带有 SIGILL 错误的代码。您必须使用-mfma-march=haswell-march=bdver2 或其他任何方式进行编译以启用 FMA + 更多功能,并设置-mtune=haswell。 (gcc -O3 -march=native 适合本地使用。)

标签: gcc clang compiler-optimization simd fma


【解决方案1】:

首先您需要启用 FMA 硬件,例如使用-mfma,然后使用Clang,您需要告诉它与-ffp-contract=fast(GCC和ICC默认这样做)或添加#pragma STDC FP_CONTRACT ONhttps://stackoverflow.com/a/34461738/2542702。使用 Clang 这会产生

    vfmadd213ps     xmm2, xmm4, xmm0
    vfmadd213ps     xmm3, xmm5, xmm1
    vmovaps xmm0, xmm2
    vmovaps xmm1, xmm3

为了使用 GCC 获得最佳结果,请使用向量扩展

typedef float float4 __attribute__((vector_size(sizeof(float)*4)));

float4 foof(float4 z, float4 y, float4 x) {
    return x + y*z;
}

使用 GCC 和 Clang,这很简单

vfmadd132ps     xmm0, xmm2, xmm1

https://godbolt.org/g/CrffNR

根据我的经验,Clang 在数组和循环矢量扩展方面似乎比 GCC 做得更好,但 GCC 在 GCC 中的矢量扩展得到了最好的支持。

【讨论】:

  • -ffp-contract=on 还不够吗?另外,vector_size 属性为什么有用?最后,用 C++17,我们不能做[[vector_size(sizeof(float)*4)]]吗?
  • @einpoklum,我不知道-ffp-contract=on。试一试就知道了。但我希望你需要fast。矢量扩展很有用,因为 GCC 当前使用它们生成更好的代码。我不知道 C++17 语法糖。
猜你喜欢
  • 2014-08-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-03-09
  • 1970-01-01
  • 1970-01-01
  • 2010-12-14
相关资源
最近更新 更多