【问题标题】:Move an int64_t to the high quadwords of an AVX2 __m256i vector将 int64_t 移动到 AVX2 __m256i 向量的高位四字
【发布时间】:2019-05-31 13:46:00
【问题描述】:

这个问题类似于[1]。但是我不太明白它是如何解决使用 GPR 插入 ymm 的高四字的问题。此外,我希望该操作不使用任何中间内存访问。

可以用AVX2或更低版本(我没有AVX512)吗?

[1]How to move double in %rax into particular qword position on %ymm or %zmm? (Kaby Lake or later)

【问题讨论】:

  • 用内在函数更新了我的答案

标签: c++ x86-64 simd intrinsics avx2


【解决方案1】:

我的回答 on the linked question 没有显示出一种方法来做到这一点,因为如果没有 AVX512F 用于屏蔽广播 (vpbroadcastq zmm0{k1}, rax),它就无法非常有效地完成。但实际上使用暂存器并没有那么糟糕,与vpinsrq 的成本差不多 + 立即混合。

(在英特尔上,总共 3 微指令。端口 5(vmovq + 广播)为 2 微指令,并且可以在任何端口上运行的即时混合。 见https://agner.org/optimize/)。

为此,我用 asm 更新了我的答案。在具有 Intel 内在函数的 C++ 中,您可以执行以下操作:

#include <immintrin.h>
#include <stdint.h>

// integer version.  An FP version would still use _mm256_set1_epi64x, then a cast
template<unsigned elem>
static inline
__m256i merge_epi64(__m256i v, int64_t newval)
{
    static_assert(elem <= 3, "a __m256i only has 4 qword elements");

    __m256i splat = _mm256_set1_epi64x(newval);

    constexpr unsigned dword_blendmask = 0b11 << (elem*2);  // vpblendd uses 2 bits per qword
    return  _mm256_blend_epi32(v, splat, dword_blendmask);
}

Clang 为所有 4 个可能的元素位置几乎完美地编译了这个,这确实展示了它的 shuffle 优化器是多么的好。它利用了所有特殊情况。作为奖励,它通过汇编向您显示混合和随机播放中哪些元素来自哪里。

From the Godbolt compiler explorer,一些测试函数来看看 regs 中的 args 会发生什么。

__m256i merge3(__m256i v, int64_t newval) {
    return merge_epi64<3> (v, newval);
}
// and so on for 2..0

# clang7.0 -O3 -march=haswell
merge3(long long __vector(4), long):
    vmovq   xmm1, rdi
    vpbroadcastq    ymm1, xmm1
    vpblendd        ymm0, ymm0, ymm1, 192 # ymm0 = ymm0[0,1,2,3,4,5],ymm1[6,7]
                      # 192 = 0xC0 = 0b11000000
    ret

merge2(long long __vector(4), long):
    vmovq   xmm1, rdi
    vinserti128     ymm1, ymm0, xmm1, 1          # Runs on more ports than vbroadcast on AMD Ryzen
        #  But it introduced a dependency on  v (ymm0) before the blend for no reason, for the low half of ymm1.  Could have used xmm1, xmm1.
    vpblendd        ymm0, ymm0, ymm1, 48 # ymm0 = ymm0[0,1,2,3],ymm1[4,5],ymm0[6,7]
    ret

merge1(long long __vector(4), long):
    vmovq   xmm1, rdi
    vpbroadcastq    xmm1, xmm1           # only an *XMM* broadcast, 1c latency instead of 3.
    vpblendd        ymm0, ymm0, ymm1, 12 # ymm0 = ymm0[0,1],ymm1[2,3],ymm0[4,5,6,7]
    ret

merge0(long long __vector(4), long):
    vmovq   xmm1, rdi
           # broadcast optimized away, newval is already in the low element
    vpblendd        ymm0, ymm0, ymm1, 3 # ymm0 = ymm1[0,1],ymm0[2,3,4,5,6,7]
    ret

其他编译器盲目地广播到完整的 YMM,然后混合,即使 elem=0。 您可以专门化模板,或在模板中添加if() 条件以优化掉。 例如splat = (elem?) set1() : v; 保存 elem==0 的广播。如果需要,您也可以捕获其他优化。


GCC 8.x 和更早的版本使用一种通常很糟糕的整数广播方式:它们存储/重新加载。这避免了使用任何 ALU shuffle 端口,因为广播负载在 Intel CPU 上是免费的,但它会将存储转发延迟引入从整数到最终向量结果的链中。

这在 gcc9 的当前主干中已修复,但我不知道是否有解决方法可以使用早期的 gcc 获得非愚蠢的代码生成。通常-march=&lt;an intel uarch&gt; 支持 ALU 而不是整数 -> 向量的存储/重新加载,反之亦然,但在这种情况下,成本模型仍然选择使用 -march=haswell 进行存储/重新加载。

# gcc8.2 -O3 -march=haswell
merge0(long long __vector(4), long):
    push    rbp
    mov     rbp, rsp
    and     rsp, -32          # align the stack even though no YMM is spilled/loaded
    mov     QWORD PTR [rsp-8], rdi
    vpbroadcastq    ymm1, QWORD PTR [rsp-8]   # 1 uop on Intel
    vpblendd        ymm0, ymm0, ymm1, 3
    leave
    ret

; GCC trunk: g++ (GCC-Explorer-Build) 9.0.0 20190103 (experimental)
; MSVC and ICC do this, too.  (For MSVC, make sure to compile with -arch:AVX2)
merge0(long long __vector(4), long):
    vmovq   xmm2, rdi
    vpbroadcastq    ymm1, xmm2
    vpblendd        ymm0, ymm0, ymm1, 3
    ret

对于运行时可变元素位置,随机播放仍然有效,但您必须创建一个混合掩码向量,并在正确的元素中设置高位。例如在alignas(8) int8_t mask[] = { 0,0,0,-1,0,0,0 }; 中从mask[3-elem] 加载vpmovsxbq。但是vpblendvbvblendvpd 比直接混合要慢,尤其是在 Haswell 上,所以尽可能避免。

【讨论】:

  • 这正是我想要的。感谢您的详细回答。我有一个后续问题。对于低阶两个元素,使用这种方法比 pinrq 有什么优势?
  • @budchanchao:vpinsrq 会将高 2 个元素归零。 pinsrq 会导致 Haswell 上的 SSE/AVX 停止。用于 YMM 的元素 1 的非 VEX pinsrq 在 Skylake(以及 AMD 和 Xeon Phi)上可能是最佳的,但如果没有内联汇编,你永远不会让编译器发出它。对于元素 0,vmovq + vpblendd 具有更好的执行端口压力:1p5 + 1p015 而不是 pinsrq xmm0, rax, 0 的 2p5。
猜你喜欢
  • 1970-01-01
  • 2018-04-03
  • 1970-01-01
  • 2018-01-18
  • 1970-01-01
  • 2019-12-21
  • 2020-02-29
  • 2021-09-19
  • 1970-01-01
相关资源
最近更新 更多