从填充为 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:只需一个 vmovsd
或 vmovq
加载。但是 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 寄存器