【问题标题】:Packing non-contiguous vector elements in AVX (and higher)在 AVX(及更高版本)中打包非连续向量元素
【发布时间】:2021-02-24 20:57:48
【问题描述】:

拥有这种性质的代码:

void foo(double *restrict A, double *restrict x,
                             double *restrict y) {
  y[5] += A[4] * x[5];
  y[5] += A[5] * x[1452];
  y[5] += A[6] * x[3373];
}

使用gcc 10.2 和标志-O3 -mfma -mavx2 -fvect-cost-model=unlimited (Compiler Explorer) 编译的结果是:

foo(double*, double*, double*):
        vmovsd  xmm1, QWORD PTR [rdx+40]
        vmovsd  xmm0, QWORD PTR [rdi+32]
        vfmadd132sd     xmm0, xmm1, QWORD PTR [rsi+40]
        vmovsd  xmm2, QWORD PTR [rdi+40]
        vfmadd231sd     xmm0, xmm2, QWORD PTR [rsi+11616]
        vmovsd  xmm3, QWORD PTR [rdi+48]
        vfmadd231sd     xmm0, xmm3, QWORD PTR [rsi+26984]
        vmovsd  QWORD PTR [rdx+40], xmm0
        ret

它不会将任何数据打包在一起(4 个vmovsd 用于加载数据,1 个用于存储),执行 3 个vfmaddXXXsd。尽管如此,我将其矢量化的动机是它可以只使用一个vfmadd231pd 来完成。我使用 AVX2 的内在函数编写此代码的“最干净”尝试是:

void foo_intrin(double *restrict A, double *restrict x,
                            double *restrict y) {
  __m256d __vop0, __vop1,__vop2;
  __m128d __lo256, __hi256;

  // THE ISSUE
  __vop0 = _mm256_maskload_pd(&A[4], _mm256_set_epi64x(0,-1,-1,-1));
  __vop1 = _mm256_mask_i64gather_pd(_mm256_setzero_pd(), &x[5], 
                                    _mm256_set_epi64x(0,3368, 1447, 0), 
                                    _mm256_set_pd(0,-1,-1,-1), 8);
  // 1 vs 3 FMADD, "the gain"
  __vop2 = _mm256_fmadd_pd(__vop0, __vop1, __vop2);

  // reducing 4 double elements: 
  // Peter Cordes' answer https://stackoverflow.com/a/49943540/2856041
  __lo256 = _mm256_castpd256_pd128(__vop2);
  __hi256 = _mm256_extractf128_pd(__vop2, 0x1);
  __lo256 = _mm_add_pd(__lo256, __hi256);

  // question:
  // could you use here shuffle instead?
  // __hi256 = _mm_shuffle_pd(__lo256, __lo256, 0x1);
  __hi256 = _mm_unpackhi_pd(__lo256, __lo256);


  __lo256 = _mm_add_pd(__lo256, __hi256);
  
  y[5] += __lo256[0];
}

生成以下 ASM:

foo_intrin(double*, double*, double*):
        vmovdqa ymm2, YMMWORD PTR .LC1[rip]
        vmovapd ymm3, YMMWORD PTR .LC2[rip]
        vmovdqa ymm0, YMMWORD PTR .LC0[rip]
        vmaskmovpd      ymm1, ymm0, YMMWORD PTR [rdi+32]
        vxorpd  xmm0, xmm0, xmm0
        vgatherqpd      ymm0, QWORD PTR [rsi+40+ymm2*8], ymm3
        vxorpd  xmm2, xmm2, xmm2
        vfmadd132pd     ymm0, ymm2, ymm1
        vmovapd xmm1, xmm0
        vextractf128    xmm0, ymm0, 0x1
        vaddpd  xmm0, xmm0, xmm1
        vunpckhpd       xmm1, xmm0, xmm0
        vaddpd  xmm0, xmm0, xmm1
        vaddsd  xmm0, xmm0, QWORD PTR [rdx+40]
        vmovsd  QWORD PTR [rdx+40], xmm0
        vzeroupper
        ret
.LC0:
        .quad   -1
        .quad   -1
        .quad   -1
        .quad   0
.LC1:
        .quad   0
        .quad   1447
        .quad   3368
        .quad   0
.LC2:
        .long   0
        .long   -1074790400
        .long   0
        .long   -1074790400
        .long   0
        .long   -1074790400
        .long   0
        .long   0

对不起,如果有人现在有焦虑症,我深表歉意。让我们分解一下:

  • 我猜那些vxorpd 是用来清理寄存器的,但icc 只生成一个,而不是两个。
  • 根据Agner Fog,VCL 在AVX2 中不使用maskload,因为“屏蔽指令在AVX512 之前的指令集中非常慢”。然而,在uops.info 中,据报道,对于 Skylake(“常规”,无 AVX-512),:
    • VMOVAPD (YMM, M256),例如_mm256_load_pd 的延迟为 [≤5;≤8],吞吐量为 0.5。
    • VMASKMOVPD(YMM、YMM、M256),例如_mm256_maskload_pd 具有延迟 [1;≤9] 和 0.5 的吞吐量,但在两个微指令中解码而不是一个。这个差距有这么大吗?以不同的方式打包会更好吗?
  • 关于mask_gather-fashion 说明,据我了解,上面所有文档中,无论是否使用掩码,它都提供相同的性能,这是正确的吗? uops.info 和Intel Intrinsics Guide 都报告了相同的性能和 ASM 形式;我很可能遗漏了一些东西。
    • 在所有情况下,gather 是否比“简单”set 更好?用内在术语说话。我知道set 会根据数据类型生成vmov 类型的指令(例如,如果数据是常量,它可能只加载一个地址,如.LC0.LC1.LC2)。李>
  • 根据 Intel Intrinsics,_mm256_shuffle_pd_mm256_unpackhi_pd 具有相同的 lantecy 和吞吐量;第一个生成vpermildp,第二个生成vunpckhpd,并且uops.info 也报告相同的值。有什么区别吗?

最后但同样重要的是,这种特殊矢量化值得吗?我的意思不是我的内在代码,而是像这样矢量化代码的概念。我怀疑有太多的数据移动来执行比较干净的代码编译器,一般来说,产生,所以我关心的是改进打包非连续数据的方式。

【问题讨论】:

    标签: x86 simd intrinsics avx avx2


    【解决方案1】:

    vfmaddXXXsdpd 指令“便宜”(单 uop,2/时钟吞吐量),甚至比 shuffle(英特尔 CPU 上的 1/时钟吞吐量)或收集负载便宜。 https://uops.info/。加载操作也是 2 次/时钟,因此大量标量加载(尤其是来自同一缓存行)非常便宜,请注意其中 3 个如何折叠到 FMA 的内存源操作数中。

    在最坏的情况下,打包 4 (x2) 个完全不连续的输入,然后手动分散输出,这与仅使用标量负载和标量 FMA 相比绝对不值得(尤其是当 FMA 允许内存源操作数时)。

    您的情况远非最坏的情况;您有来自 1 个输入的 3 个连续元素。如果你知道你可以安全地加载 4 个元素而不会有接触未映射页面的风险,那么就可以处理该输入。 (而且你总是可以使用 maskload)。但另一个向量仍然是不连续的,可能会成为加速的阻碍。

    如果通过 shuffle 比普通标量需要更多的总指令(实际上是 uops)来完成它,通常是不值得的。 并且/或者如果 shuffle 吞吐量将是比任何东西更糟糕的瓶颈标量版本。

    vgatherdpd 为此计算了尽可能多的指令,它是多微指令并且每次加载执行 1 次缓存访问。此外,您还必须将索引的常量向量而不是硬编码偏移量加载到寻址模式中。

    此外,AMD CPU 上的采集速度非常慢,甚至 Zen2 也是如此。在 AVX512 之前我们根本没有散射,即使在冰湖上也很慢。但是,您的案例不需要散点图,只需要水平总和。这将涉及更多的洗牌和vaddpd / sd因此,即使使用 maskload + collect 作为输入,在单独的向量元素中包含 3 个产品对您来说并不是特别方便。


    一点点 SIMD(不是整个数组,只是几个操作)可能会有所帮助,但这看起来不像是一种重大胜利的情况。也许有一些值得做的事情,比如用一个负载 + 一个随机播放替换 2 个负载。或者,可以通过将 3 个乘积 加到输出中来缩短 y[5] 的延迟链,而不是 3 个 FMA 链。在累加器可以容纳大量数字的情况下,这甚至可能在数值上更好;将多个小数字添加到一个大总数中会失去精度。当然,这将花费 1 mul、2 FMA 和 1 add。

    【讨论】:

    • 感谢彼得,您的回答总是很有见地。关于标量与矢量代码,您是否建议一个好的指标是在管道中始终具有相同或更少数量的微指令以提高性能?
    • 这是我们必须担心的三个主要瓶颈之一。另外两个是 1. 长依赖链的延迟(通常是循环携带,或者太长以至于乱序 exec 无法隐藏) 2. 后端执行端口瓶颈,例如端口 5 的 shuffle 吞吐量(在 Intel CPU 上)。因此,如果您所有的微指令都是随机播放,那么您正在运行 1 微指令/时钟,而不是 4 微指令/时钟的前端瓶颈。但通常你有很好的代码组合,特别是如果它不是循环的一部分。那么通常延迟或前端 uop 计数是最重要的。
    猜你喜欢
    • 1970-01-01
    • 2021-09-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-07-18
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多