跨AVX通道进行洗牌的最佳方法?

时间:2018-10-25 05:54:29

标签: c++ x86 sse simd avx

有些问题的标题相似,但是我的问题与一个非常特殊的用例有关,而其他地方没有涉及。

我有4个__128d寄存器(x0,x1,x2,x3),我想按如下方式在5个__256d寄存器(y0,y1,y2,y3,y4)中重新组合它们的内容,以准备进行其他计算:

on entry:
    x0 contains {a0, a1}
    x1 contains {a2, a3}
    x2 contains {a4, a5}
    x3 contains {a6, a7}
on exit:
    y0 contains {a0, a1, a2, a3}
    y1 contains {a1, a2, a3, a4}
    y2 contains {a2, a3, a4, a5}
    y3 contains {a3, a4, a5, a6}
    y4 contains {a4, a5, a6, a7}

我在下面的实现非常缓慢。有更好的方法吗?

y0 = _mm256_set_m128d(x1, x0);

__m128d lo = _mm_shuffle_pd(x0, x1, 1);
__m128d hi = _mm_shuffle_pd(x1, x2, 1);
y1 = _mm256_set_m128d(hi, lo);

y2 = _mm256_set_m128d(x2, x1);

lo = hi;
hi = _mm_shuffle_pd(x2, x3, 1);
y3 = _mm256_set_m128d(hi, lo);

y4 = _mm256_set_m128d(x3, x2);

1 个答案:

答案 0 :(得分:6)

使用寄存器中的输入,您可以按照5个随机播放指令进行操作:

  • 3x vinsertf128通过分别连接2个xmm寄存器来创建y0,y2和y4。
  • 在这些结果之间
  • 2x vshufpd(行内混洗)以创建y1和y3。

请注意,y0和y2的低通道包含a1和a2,这是y1的低通道所需的元素。同样的洗牌也适用于高车道。

#include <immintrin.h>

void merge(__m128d x0, __m128d x1, __m128d x2, __m128d x3,
     __m256d *__restrict y0, __m256d *__restrict y1,
     __m256d *__restrict y2, __m256d *__restrict y3, __m256d *__restrict y4)
{
    *y0 = _mm256_set_m128d(x1, x0);
    *y2 = _mm256_set_m128d(x2, x1);
    *y4 = _mm256_set_m128d(x3, x2);

    // take the high element from the first vector, low element from the 2nd.
    *y1 = _mm256_shuffle_pd(*y0, *y2, 0b0101);
    *y3 = _mm256_shuffle_pd(*y2, *y4, 0b0101);
}

很好地(with gcc and clang -O3 -march=haswell on Godbolt)编译为:

merge(double __vector(2), double __vector(2), double __vector(2), double __vector(2), double __vector(4)*, double __vector(4)*, double __vector(4)*, double __vector(4)*, double __vector(4)*):
    vinsertf128     ymm0, ymm0, xmm1, 0x1
    vinsertf128     ymm3, ymm2, xmm3, 0x1
    vinsertf128     ymm1, ymm1, xmm2, 0x1
    # vmovapd YMMWORD PTR [rdi], ymm0
    vshufpd ymm0, ymm0, ymm1, 5
    # vmovapd YMMWORD PTR [rdx], ymm1
    vshufpd ymm1, ymm1, ymm3, 5
    # vmovapd YMMWORD PTR [r8], ymm3
    # vmovapd YMMWORD PTR [rsi], ymm0
    # vmovapd YMMWORD PTR [rcx], ymm1
    # vzeroupper
    # ret

我注释掉了内联会消失的商店和东西,所以我们确实只用了5条随机指令,而您问题中的代码只有9条随机指令。 (也包含在Godbolt编译器资源管理器链接中。)

这在AMD上非常好[em] ,其中vinsertf128非常便宜(因为256位寄存器被实现为2个128位的一半,所以它只是128位的副本不需要特殊的shuffle端口。)在AMD上,256位穿越交叉的速度很慢,但是像vshufpd这样的车道内256位shuffle仅为2 oups。

在Intel上,这是相当不错的,但是带有AVX的主流Intel CPU在256位或FP随机播放中每个时钟的随机播放吞吐量仅为1。 (Sandybridge和更早版本的整数128位洗牌具有更高的吞吐量,但是AVX2 CPU放弃了多余的洗牌单元,反正它们对此毫无帮助。)

因此,英特尔CPU根本无法利用指令级并行性,但是总共只有5微妙。那是最小的,因为您需要5个结果。


但是特别是如果周围的代码也出现洗牌瓶颈时,值得考虑的存储/重装策略只有4个存储和5个重叠的矢量加载。或者也许2x vinsertf128来构造y0y4,然后2x 256位存储+ 3个重叠的重载。这样一来,杂乱无章的执行者就可以仅使用y0y4来执行相关指令,而将存储转发停顿解析为y1.3。

尤其是如果您不太在意英特尔第一代Sandybridge,那么未对齐的256位矢量加载效率会降低。 (请注意,如果您使用的是GCC,则希望使用gcc -mtune=haswell进行编译以关闭-mavx256-split-unaligned-load的默认/沙桥调整。无论使用哪种编译器,-march=native都是一个好主意如果要让二进制文件在编译它的机器上运行,则可以充分利用指令集和设置调整选项。)

但是如果前端的总uop吞吐量更多地位于瓶颈所在,那么改组实现最好。

(有关性能调优的更多信息,请参见https://agner.org/optimize/x86 tag wiki中的其他性能链接。还有What considerations go into predicting latency for operations on modern superscalar processors and how can I calculate them by hand?,但实际上Agner Fog的指南是更深入的指南,解释了什么吞吐量与延迟之间的关系实际上是大约。)


  

我什至不需要保存,因为连续内存中也已经有数据。

然后简单地将其加载5次重叠加载几乎可以肯定是您可以执行的最有效的操作。

Haswell的每个时钟从L1d可以执行2次加载,如果跨越缓存行边界,则可以进行更少的加载。 因此,如果您可以将块对齐64,则完全没有缓存行拆分就可以了。缓存未命中的速度很慢,但是从L1d缓存中重新加载热数据的成本非常低,并且现代CPU具有通常,AVX支持具有有效的不对齐负载支持。

(就像我之前说的,如果使用gcc,请确保您使用-march=haswell-mtune=haswell而不是-mavx进行编译,以避免gcc的-mavx256-split-unaligned-load。)

4个负载+ 1 vshufpd(y0,y2)可能是一种平衡负载端口压力和ALU压力的好方法,具体取决于周围代码中的瓶颈。如果周围代码对随机端口压力较低,甚至3个负载+ 2次随机播放。


  

它们在以前计算的寄存器中,要求将它们加载。

如果先前的计算仍将源数据保存在寄存器中,那么您可以首先进行256位加载,而仅将其128位低半用于较早的计算。( XMM寄存器是相应YMM寄存器的低128,读取它们不会干扰高通道,因此_mm256_castpd256_pd128会编译为0个asm指令。)

对y0,y2和y4进行256位加载,并将它们的下半部分用作x0,x1和x2。 (稍后将y1和y3构造成没有对齐的负载或随机播放)。

仅x3还不是您想要的256位向量的低128位。

理想情况下,当您从同一地址执行_mm_loadu_pd_mm256_loadu_pd时,编译器已经注意到了这种优化,但是您可能需要通过这样做来保持

__m256d y0 = _mm256_loadu_pd(base);
__m128d x0 = _mm256_castpd256_pd128(y0);

等,并根据周围的代码提取ALU内部(_mm256_extractf128_pd)或x3的128位负载。如果只需要一次,则最好将其折叠为用于任何指令的内存操作数。

潜在的缺点:128位计算开始之前的等待时间会稍长一些;如果256位负载是高速缓存行交叉而没有128位负载,则延迟几个周期。但是,如果您的数据块按64字节对齐,则不会发生这种情况。