c++ - 跨AVX channel 进行改组的最佳方法?

标签 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);

最佳答案

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

  • 3x vinsertf128,通过串联2个xmm寄存器来创建y0,y2和y4。
  • 在这些结果之间使用2x vshufpd(车道内混洗)来创建y1和y3。

  • 请注意,y0和y2的低 channel 包含a1和a2,这是y1的低 channel 所需的元素。相同的混洗也适用于高车道。
    #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上非常好,因为vinsertf128是 super 便宜的(因为256位寄存器实现为2x 128位一半,因此它只是128位拷贝,不需要特殊的洗牌端口。)256位 channel 交叉在AMD上,随机播放速度很慢,但是像vshufpd这样的车道内256位随机播放只有2微秒。

    在Intel上,这是相当不错的,但是带有AVX的主流Intel CPU在256位或FP混洗中每个时钟的混洗吞吐量仅为1。 (Sandybridge和更早版本的整数128位洗牌具有更高的吞吐量,但是AVX2 CPU放弃了多余的洗牌单元,无论如何,它们都无济于事。)

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

    但是尤其是如果周围的代码也出现洗牌瓶颈,值得考虑只有4个存储和5个重叠 vector 加载的存储/重载策略。或者也许是2x vinsertf128来构造y0y4,然后是2x 256位存储+ 3个重叠的重载。这可能会使乱序的exec仅使用y0y4来开始依赖指令的处理,而将存储转发停顿解析为y1..3。

    尤其是如果您不太在意英特尔第一代Sandybridge,那么未对齐的256位 vector 加载效率会降低。 (请注意,如果您使用的是GCC,则希望使用gcc -mtune=haswell进行编译以关闭-mavx256-split-unaligned-load的默认设置/sandybridge调整。无论使用哪种编译器,如果使二进制文件在要编译的计算机上运行,​​-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的指南是更深入的指南,它解释了吞吐量与延迟之间的关系。)

    I do not even need to save, as data is also already available in contiguous memory.



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

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

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

    根据周围代码中的瓶颈,4个载荷+ 1个vshufpd(y0,y2)可能是平衡载荷端口压力和ALU压力的一种好方法。如果周围的代码在混洗端口压力低的情况下,甚至3个载荷+ 2个混洗。

    they are in registers from previous calculations which required them to be loaded.



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

    对y0,y2和y4进行256位加载,并将它们的下半部分用作x0,x1和x2。 (稍后用未对齐的负载或混洗构造y1和y3)。

    仅x3还不是您还想要的256位 vector 的低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字节对齐,则不会发生这种情况。

    关于c++ - 跨AVX channel 进行改组的最佳方法?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/52982211/

    相关文章:

    c++ - 读取文件和未定义的行为

    c++ - 模板和 typedef 错误

    c++ - 函数指针调用

    assembly - 累加器寄存器 8086 微处理器系列

    c - SSE 寄存器中的 "Extend"数据类型大小

    c++ - 使用递归打印节号

    assembly - 在没有分页的32位模式下,如何计算物理地址?

    assembly - OR 指令汇编到 ECX 寄存器中

    x86 - _mm_set_epi8- "set"是什么意思?

    c++ - 如何使用内在函数对 double 执行绝对值?