如何在两个 AVX2 向量之间交换 128 位部分

How to swap 128-bit parts between two AVX2 vectors

问题:我有 4 个 256 位 AVX2 向量(A、B、C、D),我需要对它们各自的 128 位部分执行交换操作以及两个不同的向量之间。这是我需要做的转换。

             Original                      Transformed
    || Low Lane || High Lane||     || Low Lane || High Lane||
A = ||    L1    ||    H1    || = > ||    L1    ||    L2    ||
B = ||    L2    ||    H2    || = > ||    H1    ||    H2    ||
C = ||    L3    ||    H3    || = > ||    L3    ||    L4    ||
D = ||    L4    ||    H4    || = > ||    H3    ||    H4    ||

Visualization

基本上我需要按以下顺序将输出 L1, L2, L3, L4, H1, H2, H3, H4 存储到数组中。

我目前的解决方案是使用:
4x _mm256_blend_epi32(最坏情况:延迟 1,吞吐量 0.35)
4x _mm256_permute2x128_si256(最坏情况:延迟 3,吞吐量 1)

// (a, c) = block0, (b, d) = block1
a = Avx2.Permute2x128(a, a, 1);
var template = Avx2.Blend(a, b, 0b1111_0000); // H1 H2
a = Avx2.Blend(a, b, 0b0000_1111); // L2 l1
a = Avx2.Permute2x128(a, a, 1); // L1 l2
b = template;

c = Avx2.Permute2x128(c, c, 1);
template = Avx2.Blend(c, d, 0b1111_0000); // H3 H4
c = Avx2.Blend(c, d, 0b0000_1111);  // L4 L3
c = Avx2.Permute2x128(c, c, 1); // L3 l4
d = template;

// Store keystream into buffer (in corrected order = [block0, block1])
Avx2.Store(outputPtr, a);
Avx2.Store(outputPtr + Vector256<uint>.Count, c);
Avx2.Store(outputPtr + Vector256<uint>.Count * 2, b);
Avx2.Store(outputPtr + Vector256<uint>.Count * 3, d);

注意:如果你想知道,我正在使用 C#/NetCore 来做 AVX2!请随意使用 C/C++.

中的示例

有没有更好或更有效的方法?

编辑

接受的答案为 C#

var tmp = Avx2.Permute2x128(a, b, 0x20);
b = Avx2.Permute2x128(a, b, 0x31);
a = tmp;
tmp = Avx2.Permute2x128(c, d, 0x20);
d = Avx2.Permute2x128(c, d, 0x31);
c = tmp;

如果我没理解错的话,我想你可以在没有这个 2x4 转置的混合指令的情况下逃脱,创建新的变量来选择你想要的车道。类似于:

__m256i a;    // L1 H1
__m256i b;    // L2 H2
__m256i c;    // L3 H3
__m256i d;    // L4 H4

__m256i A = _mm256_permute2x128_si256(a, b, 0x20);  // L1 L2
__m256i B = _mm256_permute2x128_si256(a, b, 0x31);  // H1 H2
__m256i C = _mm256_permute2x128_si256(c, d, 0x20);  // L3 L4
__m256i D = _mm256_permute2x128_si256(c, d, 0x31);  // H3 H4

您仍然有 vperm2i128 指令的 3 个周期延迟,但是当您有数据跨越 128 位通道时,您总是有这种情况。这 4 次洗牌是独立的,因此它们可以流水线化(ILP); Intel 和 Zen 2 的吞吐量为 vperm2i128 (https://agner.org/optimize/, https://uops.info/).

的 1/时钟

如果幸运的话,编译器会将 L1、L2 和 L3、L4 洗牌优化为 vinserti128,AMD Zen 1 运行效率更高(1 uop 而不是 8;车道交叉洗牌得到拆分成多个 128 位微指令。)


这 4 次洗牌需要 4 微指令用于洗牌端口(英特尔端口 5); Intel 和 Zen2 对于这些洗牌只有 1/clock 洗牌吞吐量。如果这将成为您循环中的瓶颈,请考虑@chtz 的答案,它通过进行 2 次洗牌来排列需要移动的 4 条通道,从而增加前端吞吐量,为廉价混合做准备 (vpblendd)。相关:What considerations go into predicting latency for operations on modern superscalar processors and how can I calculate them by hand?

您可以使用两个排列和 4 个混合来进行操作,从而提供 2 个周期的绝对吞吐量:

void foo(
    __m256i a,    // L1 H1
    __m256i b,    // L2 H2
    __m256i c,    // L3 H3
    __m256i d,    // L4 H4
    __m256i* outputPtr
)
{
    // permute. Port usage: 1*p5, Latency 3 on both inputs
    __m256i BA = _mm256_permute2x128_si256(a, b, 0x21);  // H1 L2 
    __m256i DC = _mm256_permute2x128_si256(c, d, 0x21);  // H3 L4

    // blend. Port usage: 1*p015, Latency 1 on both inputs
    __m256i A = _mm256_blend_epi32(a, BA, 0xf0);  // L1 L2
    __m256i B = _mm256_blend_epi32(BA, b, 0xf0);  // H1 H2
    __m256i C = _mm256_blend_epi32(c, DC, 0xf0);  // L3 L4
    __m256i D = _mm256_blend_epi32(DC, d, 0xf0);  // H3 H4

    _mm256_store_si256(outputPtr+0, A);
    _mm256_store_si256(outputPtr+1, B);
    _mm256_store_si256(outputPtr+2, C);
    _mm256_store_si256(outputPtr+3, D);
}

然而,根据上下文(特别是如果 a, ..., d 最初是从内存中读取的),使用 vmovdqu and vinserti128 的序列也可能更好带有 m128 个内存操作数的指令。您将有两倍的负载,但没有通道间延迟,端口 5 上也没有瓶颈——关于延迟和端口使用,基于内存的 vinsert128 表现得像混合体。