Simd 使用AVX-512模拟64字节的移位

Simd 使用AVX-512模拟64字节的移位,simd,avx512,Simd,Avx512,我的问题是前一个问题的延伸: 如何使用AVX-512在64字节上实现类似的移位?具体而言,我应该如何实施: \uuuum512i\umm512\uslli\usi512(\uuuum512i a,int imm8) \uuuum512i\umm512\usrli\usi512(\uuuum512i a,int imm8) 根据SSE2方法和。以下是使用临时阵列的工作解决方案: __m512i _mm512_slri_si512(__m512i a, size_t imm8) { //

我的问题是前一个问题的延伸:

如何使用AVX-512在64字节上实现类似的移位?具体而言,我应该如何实施:

  • \uuuum512i\umm512\uslli\usi512(\uuuum512i a,int imm8)
  • \uuuum512i\umm512\usrli\usi512(\uuuum512i a,int imm8)

根据SSE2方法和。

以下是使用临时阵列的工作解决方案:

__m512i _mm512_slri_si512(__m512i a, size_t imm8)
{
    // set up temporary array and set upper half to zero 
    // (this needs to happen outside any critical loop)
    alignas(64) char temp[128];
    _mm512_store_si512(temp+64, _mm512_setzero_si512());

    // store input into lower half
    _mm512_store_si512(temp, a);

    // load shifted register
    return _mm512_loadu_si512(temp+imm8);
}

__m512i _mm512_slli_si512(__m512i a, size_t imm8)
{
    // set up temporary array and set lower half to zero 
    // (this needs to happen outside any critical loop)
    alignas(64) char temp[128];
    _mm512_store_si512(temp, _mm512_setzero_si512());

    // store input into upper half
    _mm512_store_si512(temp+64, a);

    // load shifted register
    return _mm512_loadu_si512(temp+(64-imm8));
}
如果编译时不知道
imm8
,但它不执行任何越界检查,那么这也应该可以工作。 实际上,您可以使用一个
3*64
临时值,并在左移位和右移位方法之间共享它(两种方法都适用于负输入)

当然,如果在函数体外部共享临时线程,则必须确保多个线程不会同时访问它

带使用演示的锁销链接:


正如Peter所指出的,这个存储加载技巧将导致使用AVX512的所有CPU上的存储转发暂停。最有效的转发情况(~6个周期延迟)仅在所有加载字节来自一个存储时有效。如果加载超出了与它重叠的最新存储区,那么它将有额外的延迟(如~16个周期)来扫描存储区缓冲区,如果需要,还可以从L1d缓存中合并字节。有关更多详细信息,请参阅和。这个额外的扫描过程可能会并行地发生在多个负载上,并且至少不会暂停其他事情(如正常的存储转发或管道的其余部分),因此这可能不是吞吐量问题

如果需要相同数据的多个移位偏移,则在不同路线上进行一次存储和多次重新加载应该比较好

但是,如果延迟是您的主要问题,那么您应该尝试基于
valignd
的解决方案(同样,如果您希望以4字节的倍数移位,这显然是一个更容易的解决方案)。或者对于恒定移位计数,
vpermw
的矢量控制可以工作


为了完整起见,这里是一个基于
valignd
valignr
的版本,用于从0到64的移位,编译时就知道了(使用C++17——但是您可以很容易地避免
if constexpr
这只是因为
static_assert
)。您可以传递第二个寄存器(即,它的行为类似于
valignr
如果在车道上对齐,它的行为将类似于
valignr)

模板
__m512i向右移位(uuum512i a,uuum512i进位=mm512_setzero_si512())
{
静态断言(0(a,进位);//0,16,32,48
返回mm512对准器epi8(a0,a1,N%16);
}
}
模板
__m512i向左移位(uuum512i a,uuum512i进位=mm512_setzero_si512())
{
返回右移(进位,a);
}
这里是一个带一些示例组件的锁紧螺栓连杆,以及每个可能的
shift\u right
操作的输出:

GCC忠实地将其转换为
valignd
valignr
指令——但可能会执行不必要的
vpxor
指令(例如,在
shiftleft_49
示例中),Clang会执行一些疯狂的替换(但不确定它们是否真的起作用)


该代码可以扩展为移位任意序列的寄存器(总是携带前一个寄存器的字节)。

对于需要精确移位64位的寄存器,可以使用直接在寄存器中工作的permute指令。对于8位的倍数移位,可以使用字节混洗(请参见
VPSHUFB
,并查看cast函数,如果您处理的是浮点数,因为洗牌使用整数)

下面是一个移位64位的示例(“SHR zmm1,64”)。掩码用于清除顶部的64位。如果您想要
ROR
类似的功能,可以使用不带掩码的版本。请注意,也可以向左移位。只需根据需要更改索引即可

#include <immintrin.h>
#include <iostream>

void show(char const * msg, double *v)
{
    std::cout
            << msg
            << ": "
            << v[0]
            << " "
            << v[1]
            << " "
            << v[2]
            << " "
            << v[3]
            << " "
            << v[4]
            << " "
            << v[5]
            << " "
            << v[6]
            << " "
            << v[7]
            << "\n";
}


int main(int argc, char * argv[])
{
    double v[8] = { 1., 2., 3., 4., 5., 6., 7., 8. };
    double q[8] = {};
    alignas(64) std::uint64_t indexes[8] = { 1, 2, 3, 4, 5, 6, 7, 0 };

    show("init", v);
    show("q", q);

    // load
    __m512d a(_mm512_loadu_pd(v));
    __m512i i(_mm512_load_epi64(indexes));

    // shift
    //__m512d b(_mm512_permutex_pd(a, 0x39));   // can't cross between 4 low and 4 high with immediate
    //__m512d b(_mm512_permutexvar_pd(i, a));   // ROR
    __m512d b(_mm512_maskz_permutexvar_pd(0x7F, i, a));   // LSR on a double basis

    // store
    _mm512_storeu_pd(q, b);

    show("shifted", q);
    show("original", v);
}
结果是这样的一条指令:

 979:   62 f1 fd 48 6f 85 d0    vmovdqa64 -0x130(%rbp),%zmm0
 980:   fe ff ff 
 983:   48 8d 75 80             lea    -0x80(%rbp),%rsi
 987:   48 8d 3d 02 04 00 00    lea    0x402(%rip),%rdi        # d90 <_IO_stdin_used+0x10>
 98e:   62 f3 fd 48 03 c0 01    valignq $0x1,%zmm0,%zmm0,%zmm0
 995:   62 f1 fd 48 11 45 fd    vmovupd %zmm0,-0xc0(%rbp)
979:62 f1 fd 48 6f 85 d0 vmovdqa64-0x130(%rbp),%zmm0
980:fe ff
983:48 8d 75 80 lea-0x80(%rbp),%rsi
987:48 8d 3d 02 04 00 00 lea 0x402(%rip),%rdi#d90
98e:62 f3 fd 48 03 c0 01有效期$0x1,%zmm0,%zmm0,%zmm0
995:62 f1 fd 48 11 45 fd vmovupd%zmm0,-0xc0(%rbp)

重要的一点是,使用更少的寄存器也更好,因为它增加了我们在寄存器中获得100%完全优化的机会,而不必使用内存(512位在内存之间传输非常多)。

最简单的解决方案可能是存储和重新加载数据(如有必要,使用
\u mm512\u maskz\u loadu\u epi8
)。对于
imm8
是constexpr和8的倍数的情况,可以使用AVX512F进行整个向量的dword粒度移位。否则,可以将其与
\u mm512\u shrdi\u epi16
2寄存器移位(AVX512\u VBMI2 so IceLake和更新版本…)一起用作构建块您可以使用哪种AVX512扩展,或者您关心哪种CPU?@PeterCordes
valignd
应该实际适用于4的倍数(即32位),不是吗?(哪种类型留下了问题
valignq
的意义是什么…)@chtz您有没有可能添加您的建议作为答案?@chtz:是的,我本想写32位的倍数,但在记住valignd!=vpalignr之后,不知何故忘记了实际编辑该句子。无论如何,
valignq
的要点是对qword元素应用掩码位图。(同样的原因,我们有
vpxord
vpxorq
而不是仅仅扩展AVX2
vpxor
)您应该指出,这保证了存储转发暂停;~16个周期的延迟,而不是~6个周期。不过,这通常不是吞吐量问题,因此对于某些用例来说可能是好的。(这不是“暂停”就像暂停整个管道一样。)@PeterCordes谢谢你的提示(我必须先读一读)。我想带面具的货物(或商店)不会有多大帮助
 96a:   62 f1 fd 48 6f 85 10    vmovdqa64 -0xf0(%rbp),%zmm0
 971:   ff ff ff 
 974:   b8 7f 00 00 00          mov    $0x7f,%eax              # mask
 979:   48 8d 3d 10 04 00 00    lea    0x410(%rip),%rdi        # d90 <_IO_stdin_used+0x10>
 980:   c5 f9 92 c8             kmovb  %eax,%k1                # special k1 register
 984:   4c 89 e6                mov    %r12,%rsi
 987:   62 f2 fd c9 16 85 d0    vpermpd -0x130(%rbp),%zmm0,%zmm0{%k1}{z}   # "shift"
 98e:   fe ff ff 
 991:   62 f1 fd 48 11 45 fe    vmovupd %zmm0,-0x80(%rbp)
// this is in place of the permute, without the need for the indexes
__m512i b(_mm512_maskz_alignr_epi64(0xFF, _mm512_castpd_si512(a), _mm512_castpd_si512(a), 1));
 979:   62 f1 fd 48 6f 85 d0    vmovdqa64 -0x130(%rbp),%zmm0
 980:   fe ff ff 
 983:   48 8d 75 80             lea    -0x80(%rbp),%rsi
 987:   48 8d 3d 02 04 00 00    lea    0x402(%rip),%rdi        # d90 <_IO_stdin_used+0x10>
 98e:   62 f3 fd 48 03 c0 01    valignq $0x1,%zmm0,%zmm0,%zmm0
 995:   62 f1 fd 48 11 45 fd    vmovupd %zmm0,-0xc0(%rbp)