AVX2,如何有效地将四个整数加载到 256 位寄存器的偶数索引并复制到奇数索引?

AVX2, How to Efficiently Load Four Integers to Even Indices of a 256 Bit Register and Copy to Odd Indices?

我在内存中有一个对齐的整数数组,包含索引 I0、I1、I2、I3。我的目标是将它们放入包含 I0、I0 + 1、I1、I1 + 1、I2、I2 + 1、I3、I3 + 1 的 __m256i 寄存器中。困难的部分是将它们放入 256 位寄存器中作为 I0、I0、I1、I1、I2、I2、I3、I3,之后我可以添加一个包含 0、1、0、1、0、1、0、1 的寄存器。

我找到了内在函数 _mm256_castsi128_si256,它让我可以将 4 个整数加载到 256 位寄存器的低 128 位中,但我正在努力从那里找到最好的内在函数。

如有任何帮助,我们将不胜感激。我可以访问所有 SSE 版本、AVX 和 AVX2,并且只想使用内部函数来执行此操作。

编辑:

我认为这可行,但我不知道它的效率如何...正在测试它。

// _mm128_load_si128: Loads 4 integer values into a temporary 128bit register.
// _mm256_broadcastsi128_si256: Copies 4 integer values in the 128 bit register to the low and high 128 bits of the 256 bit register.
__m256i tmpStuff = _mm256_broadcastsi128_si256 ((_mm_load_si128((__m128i*) indicesArray)));

// _mm256_unpacklo_epi32: Interleaves the integer values of source0 and source1.
__m256i indices = _mm256_unpacklo_epi32(tmpStuff, tmpStuff);

__m256i regToAdd = _mm256_set_epi32 (0, 1, 0, 1, 0, 1, 0, 1);
indices = _mm256_add_epi32(indices, regToAdd);

Edit2:上面的代码不起作用,因为 _mm256_unpacklo_epi32 的行为与我想的不一样。上面的代码将导致 I0、I0+1、I1、I1+1、I0、I0+1、I1、I1+1。

Edit3:以下代码有效,但我再次不确定它是否最有效:

__m256i tmpStuff = _mm256_castsi128_si256(_mm_loadu_si128((__m128i*) indicesArray));
__m256i mask = _mm256_set_epi32 (3, 3, 2, 2, 1, 1, 0, 0);
__m256i indices= _mm256_permutevar8x32_epi32(tmpStuff, mask);
__m256i regToAdd = _mm256_set_epi32 (1, 0, 1, 0, 1, 0, 1, 0); // Set in reverse order.
indices= _mm256_add_epi32(indices, regToAdd);

您的 _mm256_permutevar8x32_epi32 版本看起来非常适合 Intel CPU,除非我缺少一种可以将 shuffle 折叠成 128b 负载的方法。这可能对融合域 uop 吞吐量略有帮助,但对非融合域没有帮助。

1 次加载 (vmovdqa)、1 次随机播放 (vpermd、又名 _mm256_permutevar8x32_epi32) 和 1 次添加 (vpaddd) 非常轻量级。在 Intel 上,跨车道洗牌有额外的延迟,但吞吐量没有变差。在 AMD Ryzen 上,跨车道洗牌的成本更高。 (http://agner.org/optimize/).

由于您可以使用 AVX2,如果为 vpermd 加载洗牌掩码不是问题,那么您的解决方案就很棒。 (注册压力/缓存未命中)。

请注意 _mm256_castsi128_si256 不能保证 __m256i 的高半部分全为零。但是你不依赖于此,所以你的代码完全没问题。


顺便说一句,您可以使用一个 256 位加载并使用 vpermd 以 2 种不同的方式解压缩它。 使用另一个 mask 所有元素都高 4 .


另一种选择是未对齐的 256b 负载,车道拆分位于 4 个元素的中间,因此您在高车道底部有 2 个元素,在高车道底部有 2 个元素低车道的顶部。然后,您可以使用通道内洗牌将数据放在需要的地方。但它在每个通道中都有不同的洗牌,因此您仍然需要一个洗牌,将控制操作数放入寄存器(而不是立即数)中以在单个操作中完成。 (vpshufdvpermilps imm8 为两条通道回收相同的立即数。)立即数的不同位分别影响上/下通道的唯一洗牌是 qword 粒度洗牌,如 vpermq_mm256_permutex_epi64,不是 permutexvar).

你可以使用 vpermilps ymm,ymm,ymm, or vpshufb (_mm256_shuffle_epi8) for this, which will run more efficiently on Ryzen than a lane-crossing vpermd (probably 3 uops / 1 per 4c throughput if it's the same as vpermps, according to Agner Fog

但是,当您的数据已经对齐时,使用未对齐的加载就没有吸引力了,它所获得的只是车道内与车道交叉的洗牌。如果您需要 16 位或 8 位粒度的洗牌,那可能是值得的(因为在 AVX512 之前没有交叉字节或字洗牌,而在 Skylake-AVX512 上 vpermw 是多个 uops。)


一种避免混洗掩码向量常数但性能较差的替代方法(因为它需要两倍的混洗次数):

vpmovzxdq 是另一种将上面两个元素放入上面 128 位通道的选项。

; slow, not recommended.  Avoids using a register for shuffle-control, though.
vpmovzxdq  ymm0, [src]
vpshufd    ymm1, ymm0, _MM_SHUFFLE(2,2, 0,0)   ; duplicate elements
vpaddd     ...

或者,如果洗牌端口是整个循环的瓶颈,吞吐量可能高于上面的 2-洗牌版本。 (虽然仍然比 vpermd 版本差。)

; slow, not recommended.
vpmovzxdq  ymm0, [src]
vpsllq     ymm1, ymm0,32          ; left shift by 32
vpor       ymm0, ymm0, odd_ones   ; OR with set1_epi64x(1ULL << 32)
vpaddd     ymm0, ymm0, ymm1       ; I_n+0 in even elements, 1+I_n in odd

这具有一些指令级并行性:OR 可以 运行 与移位并行。但是它仍然因为更多的 uops 而很糟糕;如果你没有向量 regs 可能仍然最好使用内存中的洗牌控制向量。