从填充为 0 的数组加载到 256 位 AVX2 寄存器

Posted

技术标签:

【中文标题】从填充为 0 的数组加载到 256 位 AVX2 寄存器【英文标题】:Load to 256 bit AVX2 register from an array with 0 padding 【发布时间】:2020-01-22 03:01:32 【问题描述】:

如果数组大小小于 4,我想将 4 个 double 加载到 256 位寄存器中并用 0 填充。

register __m256d c = _mm256_loadu_pd(C);

现在假设 C 中只有三个元素,我想将寄存器 c 中的最后一个“条目”填充为 0。我怎样才能有效地做到这一点?

【问题讨论】:

总是有vmaskmovpd,如果最后一个元素恰好位于未映射的页面中,它甚至可以进行故障抑制。你需要那个吗?另请参阅Intel store instructions on delibrately overlapping memory regions 了解有关 vmaskmov 效率的一些信息(主要用于商店,但它对于 AMD 上的负载很有效,与商店不同)。 @PeterCordes 这需要我指定一个掩码.. 寻找一个更简单的选项 是的,您需要一个掩码,您可以从滑动窗口加载到-1, -1, -1, 0,0,0 数组中获得该掩码。 Vectorizing with unaligned buffers: using VMASKMOVPS: generating a mask from a misalignment count? Or not using that insn at all。或者您可以从长度动态生成它,例如 (1ULL<<(n*8)) - 1 和 vmovd 到 XMM + vpmovsxbq ymm, xmm。不幸的是,您没有很多好的选择,特别是如果您无法填充源数据以保证从指针 C 加载至少 32 个字节。 在 AVX512 将掩码变成一流的操作(甚至可能是这样)之前,您肯定会想要剥离处理大数组的尾部或整个小数组,与你的主循环。对于较大数组中的尾部处理,它可以执行在最后一个元素处结束的加载(如果数组大小不是向量宽度的倍数,则可能与早期加载重叠)。对于垂直 SIMD 复制到 dst 数组,您可以让存储重叠,但对于水平总和或其他东西,您需要避免重复计算重叠,所以这种方式不起作用。 更正:VPERM2F128 采用 256 位内存操作数,因此您实际上需要单独的 128 位加载。在这种情况下,您还可以使用VINSERTF128 将两半结合起来。这就是_mm256_set[r]_m128d 将为您生成的内容:godbolt.org/z/yifEK_ 【参考方案1】:

这是一种方法。与_mm256_maskload_pd不同的是,下面的函数不需要加载或创建掩码。

// Load 3 doubles from memory, zero out the 4-th one.
inline __m256d load3( const double* source )

    const __m128d low = _mm_loadu_pd( source );
    const __m128d high = _mm_load_sd( source + 2 );
    return _mm256_set_m128d( high, low );   // vinsertf128

为了完整起见,这里有 2 个其他变体。

// Zero out the high 2 double lanes.
inline __m256d zeroupper( __m128d low2 )

    const __m256d low = _mm256_castpd128_pd256( low2 ); // no instruction
    const __m256d zero = _mm256_setzero_pd();   // vxorpd
    // vblendpd is 4-5 times faster than vinsertf128
    return _mm256_blend_pd( zero, low, 3 ); // vblendpd

// Load 2 doubles from memory, zero out other 2
inline __m256d load2( const double* source )

    return zeroupper( _mm_loadu_pd( source ) );

// Load 1 double from memory, zero out the other 3
inline __m256d load1( const double* source )

    return zeroupper( _mm_load_sd( source ) );

【讨论】:

vblendpd 比 vinsertf128 快 4-5 倍 呃,什么?在 Zen 1 上,vinsertf128 实际上更快。在 Intel 上,两者都是单 uop,但 vinsertf128 是通道交叉 shuffle(3c 延迟,HSW/SKL 上 1c 吞吐量)而 vblendpd 是 1c 延迟,Haswell/Skylake 上 0.333c 吞吐量)。如果您尝试对这些 intrinsics 进行基准测试,那么希望一个好的编译器能够完全优化 _mm256_blend_pd,而只需使用 vmovupd xmm, [mem] 将零扩展到 256 位。但是一些编译器(如 MSVC 和 ICC)不会优化掉内在函数。 在实践中,clang 在实际加载的情况下优化了与零混合的情况(不使用已经在寄存器中的值)。但 GCC9.2 没有。 godbolt.org/z/5ZXsR7。使用 _mm256_set_pd(0,0,0, *source); 从 gcc 和 clang 获取高效的 asm:只需一个 vmovsdvmovq 加载。但是 MSVC 仍然对此感到困惑,在零扩展加载指令之后使用愚蠢的vshufpd + vinsertf128。 godbolt.org/z/ww4uLL @PeterCordes 确实,仅使用_mm256_set[r]_pd 已经在clang/gcc 上生成了最有效的代码,适用于1-3 的所有大小:godbolt.org/z/qEYrex

以上是关于从填充为 0 的数组加载到 256 位 AVX2 寄存器的主要内容,如果未能解决你的问题,请参考以下文章

SIMD (AVX2) - 将 uint8_t 值加载到多个浮点 __m256 寄存器

将 2x4 64b 结构的第一行加载到 AVX2 的 256b 寄存器中的最快方法是啥?

两个 16 位整数向量与 C++ 中的 AVX2 的内积

使用 AVX2 C++ 的选择性加载

AVX2 上的 256 位 CRC 计算

加载指令与 AVX 中的 AVX2 __m256i const* mem_addr [关闭]