如何在两个 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
表现得像混合体。
问题:我有 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/).
如果幸运的话,编译器会将 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
表现得像混合体。