在AVX及更高版本中打包非连续矢量元素

问题描述

具有这种性质的代码:

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=unlimitedCompiler 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 y) {
  __m256d __vop0,__vop1,__vop2;
  __m128d __lo256,__hi256;

  // THE ISSUE
  __vop0 = _mm256_maskload_pd(&A[4],_mm256_set_epi64x(0,-1,-1));
  __vop1 = _mm256_mask_i64gather_pd(_mm256_setzero_pd(),&x[5],3368,1447,0),_mm256_set_pd(0,-1),8);
  // 1 vs 3 FMADD,"the gain"
  __vop2 = _mm256_fmadd_pd(__vop0,__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*):
        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
        vfmadd132pd     ymm0,ymm2,ymm1
        vmovapd xmm1,xmm0
        vextractf128    xmm0,0x1
        vaddpd  xmm0,xmm1
        vunpckhpd       xmm1,xmm0
        vaddpd  xmm0,xmm1
        vaddsd  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)报告说:
      li VMOVAPD(YMM,M256),例如_mm256_load_pd的延迟为[≤5;≤8],吞吐量为0.5。
  • VMASKMOVPD(YMM,YMM,M256),例如_mm256_maskload_pd的延迟为[1;≤9],吞吐量也为0.5,但解码后的分辨率为2而不是1。差异如此巨大吗?用其他方式打包会更好吗?
  • 关于mask_gather-时尚说明,据我对以上所有文档的了解,不管是否使用遮罩,它都具有相同的性能,这是正确的吗? uops.info和Intel Intrinsics Guide均报告相同的性能和ASM格式;我很可能错过了一些东西。
    • 在所有情况下,gather比“简单” set好吗?用内在术语说话。我知道set会根据数据类型生成vmov类型的指令(例如,如果数据是常量,它可能只加载地址,如.LC0.LC1.LC2)。
  • 根据英特尔内部技术,_mm256_shuffle_pd_mm256_unpackhi_pd具有相同的Lantecy和吞吐量。第一个生成vpermildp,第二个生成vunpckhpd,而uops.info也报告相同的值。有什么区别吗?
  • 最后但并非最不重要的是,这种临时矢量化值得吗?我并不是说我的内在代码,而是这样的向量化代码的概念。我怀疑通常比较干净的代码编译器产生的数据移动太多,因此我关心的是改进打包非连续数据的方式。

    解决方法

    vfmaddXXXsdpd指令是“便宜的”(单uop,2 /时钟吞吐量),甚至比shuffle(Intel CPU的1时钟吞吐量)或收集加载便宜。 https://uops.info/。加载操作也是2 / clock,因此很多标量加载(尤其是来自同一条缓存行)非常便宜,请注意其中3个可以折叠为FMA的内存源操作数。

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

    您的情况远没有最坏的情况;您从1个输入中有3个连续元素。如果您知道可以安全地加载4个元素,而没有接触未映射页面的风险,则可以解决该输入问题。 (并且您始终可以使用maskload)。但是另一个向量仍然是不连续的,可能会加速。

    如果通过改组比普通标量需要更多的总指令(实际上是uops)来完成操作,通常是不值得的。和/或如果改组吞吐量比任何其他方法都更糟糕的瓶颈,标量版本。

    ({vgatherdpd为此目的计算了很多指令,它们是多线程并且每次加载进行1次缓存访问。另外,您还必须加载索引的常数向量,而不是将偏移量硬编码为寻址模式。 / p>

    此外,AMD CPU甚至Zen2上的收集速度都非常慢。直到AVX512,我们才完全没有散射,即使在冰湖上,散射也很慢。但是,您的案例不需要分散,只需要水平和即可。这将涉及更多的洗牌和vaddpd / sd因此,即使使用maskload +收集输入信息,在单独的矢量元素中具有3个乘积对您来说也不是特别方便。


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

    相关问答

    错误1:Request method ‘DELETE‘ not supported 错误还原:...
    错误1:启动docker镜像时报错:Error response from daemon:...
    错误1:private field ‘xxx‘ is never assigned 按Alt...
    报错如下,通过源不能下载,最后警告pip需升级版本 Requirem...