sse/avx 相当于 neon vuzp
sse/avx equivalent for neon vuzp
Intel 的矢量扩展 SSE、AVX 等为每个元素大小提供两个解包操作,例如SSE 内在函数是 _mm_unpacklo_*
和 _mm_unpackhi_*
。对于向量中的 4 个元素,它会这样做:
inputs: (A0 A1 A2 A3) (B0 B1 B2 B3)
unpacklo/hi: (A0 B0 A1 B1) (A2 B2 A3 B3)
unpack 相当于 ARM 的 NEON 指令集中的 vzip
。但是,NEON 指令集还提供了 vuzp
运算,它是 vzip
的逆运算。对于向量中的 4 个元素,它会这样做:
inputs: (A0 A1 A2 A3) (B0 B1 B2 B3)
vuzp: (A0 A2 B0 B2) (A1 A3 B1 B3)
如何 vuzp
使用 SSE 或 AVX 内在函数有效地实现?似乎没有关于它的说明。对于 4 个元素,我假设可以使用随机播放和随后的解包移动 2 个元素来完成:
inputs: (A0 A1 A2 A3) (B0 B1 B2 B3)
shuffle: (A0 A2 A1 A3) (B0 B2 B1 B3)
unpacklo/hi 2: (A0 A2 B0 B2) (A1 A3 B1 B3)
是否有更高效的单指令解决方案? (也许首先是 SSE - 我知道对于 AVX,我们可能会遇到其他问题,即洗牌和解包不会跨车道。)
了解这一点可能对编写数据调配和解调代码很有用(只需根据解包操作反转调配代码的操作,就可以推导出解调代码)。
编辑:这是 8 元素版本:这是 NEON 的效果 vuzp
:
input: (A0 A1 A2 A3 A4 A5 A6 A7) (B0 B1 B2 B3 B4 B5 B6 B7)
vuzp: (A0 A2 A4 A6 B0 B2 B4 B6) (A1 A3 A5 A7 B1 B3 B5 B7)
这是我的版本,每个输出元素有一个 shuffle
和一个 unpack
(似乎可以推广到更大的元素编号):
input: (A0 A1 A2 A3 A4 A5 A6 A7) (B0 B1 B2 B3 B4 B5 B6 B7)
shuffle: (A0 A2 A4 A6 A1 A3 A5 A7) (B0 B2 B4 B6 B1 B3 B5 B7)
unpacklo/hi 4: (A0 A2 A4 A6 B0 B2 B4 B6) (A1 A3 A5 A7 B1 B3 B5 B7)
EOF 建议的方法是正确的,但每个输出需要 log2(8)=3
unpack
次操作:
input: (A0 A1 A2 A3 A4 A5 A6 A7) (B0 B1 B2 B3 B4 B5 B6 B7)
unpacklo/hi 1: (A0 B0 A1 B1 A2 B2 A3 B3) (A4 B4 A5 B5 A6 B6 A7 B7)
unpacklo/hi 1: (A0 A4 B0 B4 A1 A5 B1 B5) (A2 A6 B2 B6 A3 A7 B3 B7)
unpacklo/hi 1: (A0 A2 A4 A6 B0 B2 B4 B6) (A1 A3 A5 A7 B1 B3 B5 B7)
it should be possible to derive deswizzling code just by inverting the operations
习惯于对英特尔矢量洗牌的非正交性感到失望和沮丧。 punpck
没有正反函数。 SSE/AVX pack
指令用于缩小元素大小。 (所以一个 packusdw
is the inverse of punpck[lh]wd
对零,但与两个任意向量一起使用时则不然)。此外,pack
指令仅适用于 32->16(双字到字)和 16->8(字到字节)元素大小。没有packusqd
(64->32).
PACK 指令仅在饱和时可用,而不是 t运行cation(直到 AVX512 vpmovqd
),因此对于此用例,我们需要为 2 个 PACK 准备 4 个不同的输入向量指示。结果证明这是可怕的,比你的 3-shuffle 解决方案更糟糕(参见下面 Godbolt link 中的 unzip32_pack()
)。
但是有一个 2 输入随机播放可以为 32 位元素执行您想要的操作:shufps
。结果的低 2 元素可以是第一个向量的任意 2 个元素,高 2 元素可以是第二个向量的任意元素。我们想要的洗牌符合这些限制,所以我们可以使用它。
我们可以在 2 条指令中解决整个问题(为非 AVX 版本加上一个 movdqa
,因为 shufps
破坏了左输入寄存器):
inputs: a=(A0 A1 A2 A3) a=(B0 B1 B2 B3)
_mm_shuffle_ps(a,b,_MM_SHUFFLE(2,0,2,0)); // (A0 A2 B0 B2)
_mm_shuffle_ps(a,b,_MM_SHUFFLE(3,1,3,1)); // (A1 A3 B1 B3)
_MM_SHUFFLE()
uses most-significant-element first notation,就像英特尔的所有文档一样。你的符号是相反的。
shufps
的唯一内在函数使用 __m128
/ __m256
向量(float
不是整数),因此您必须强制转换才能使用它。 _mm_castsi128_ps
是一个 reinterpret_cast:它编译为零指令。
#include <immintrin.h>
static inline
__m128i unziplo(__m128i a, __m128i b) {
__m128 aps = _mm_castsi128_ps(a);
__m128 bps = _mm_castsi128_ps(b);
__m128 lo = _mm_shuffle_ps(aps, bps, _MM_SHUFFLE(2,0,2,0));
return _mm_castps_si128(lo);
}
static inline
__m128i unziphi(__m128i a, __m128i b) {
__m128 aps = _mm_castsi128_ps(a);
__m128 bps = _mm_castsi128_ps(b);
__m128 hi = _mm_shuffle_ps(aps, bps, _MM_SHUFFLE(3,1,3,1));
return _mm_castps_si128(hi);
}
gcc 会将它们分别内联到一条指令中。删除 static inline
后,我们可以看到它们如何编译为非内联函数。我把它们放在 the Godbolt compiler explorer
unziplo(long long __vector(2), long long __vector(2)):
shufps xmm0, xmm1, 136
ret
unziphi(long long __vector(2), long long __vector(2)):
shufps xmm0, xmm1, 221
ret
在最近 Intel/AMD CPU 秒内,对整数数据使用 FP 洗牌没问题。没有额外的旁路延迟延迟(参见 this answer which summarizes what Agner Fog's microarch guide says about it). It has extra latency on Intel Nehalem , but may still be the best choice there. FP loads/shuffles ,只有实际的 FP 数学指令关心这一点。
有趣的事实:在 AMD Bulldozer 系列 CPUs(和 Intel Core2)上,FP 洗牌如 shufps
仍然 运行 在 ivec 域中,因此它们实际上有额外的延迟当在 FP 指令之间使用时,而不是在整数指令之间使用!
与 ARM NEON / ARMv8 SIMD 不同,x86 SSE 没有任何 2 输出寄存器指令,它们在 x86 中很少见。 (它们存在,例如 mul r64
,但总是在当前 CPU 上解码为多个微指令)。
创建 2 个结果向量总是至少需要 2 条指令。如果他们不需要 运行 在洗牌端口上,那将是理想的,因为最近的英特尔 CPUs 的洗牌吞吐量仅为每个时钟 1 个。当您的所有指令都是随机播放时,指令级并行性没有太大帮助。
对于吞吐量,1 次随机播放 + 2 次非随机播放可能比 2 次随机播放更有效,并且具有相同的延迟。甚至 2 次洗牌和 2 次混合可能比 3 次洗牌更有效,具体取决于周围代码中的瓶颈。但我认为我们不能用那几条指令替换 2x shufps
。
没有SHUFPS
:
你的shuffle + unpacklo/hi很不错。总共有 4 次洗牌:2 pshufd
准备输入,然后 2 punpck
l/h。这可能比任何旁路延迟都更糟糕,除了在 Nehalem 上,在延迟很重要但吞吐量不重要的情况下。
任何其他选项似乎都需要准备 4 个输入向量,用于混合或 packss
。有关混合选项,请参阅 @Mysticial's answer to _mm_shuffle_ps() equivalent for integer vectors (__m128i)?。对于两个输出,总共需要 4 次洗牌才能完成输入,然后是 2x pblendw
(快)或 vpblendd
(甚至更快)。
对 16 位或 8 位元素使用 packsswd
或 wb
也可以。屏蔽 a 和 b 的奇数元素需要 2x pand
条指令,将奇数元素向下移动到偶数位置需要 2x psrld
条指令。这为您设置了 2x packsswd
来创建两个输出向量。总共 6 条指令,加上许多 movdqa
,因为这些指令都破坏了它们的输入(不像 pshufd
,后者是复制+洗牌)。
// don't use this, it's not optimal for any CPU
void unzip32_pack(__m128i &a, __m128i &b) {
__m128i a_even = _mm_and_si128(a, _mm_setr_epi32(-1, 0, -1, 0));
__m128i a_odd = _mm_srli_epi64(a, 32);
__m128i b_even = _mm_and_si128(b, _mm_setr_epi32(-1, 0, -1, 0));
__m128i b_odd = _mm_srli_epi64(b, 32);
__m128i lo = _mm_packs_epi16(a_even, b_even);
__m128i hi = _mm_packs_epi16(a_odd, b_odd);
a = lo;
b = hi;
}
Nehalem 是唯一一个 CPU 可能值得使用 2x shufps
以外的东西的地方,因为它具有高 (2c) 旁路延迟。它每个时钟有 2 个洗牌吞吐量,pshufd
是一个复制+洗牌,所以 2x pshufd
准备 a
和 b
的副本只需要一个额外的 movdqa
之后将 punpckldq
和 punpckhdq
结果放入单独的寄存器中。 (movdqa
不是免费的;它有 1c 延迟并且需要 Nehalem 上的向量执行端口。如果你在洗牌吞吐量上遇到瓶颈,而不是整体前端带宽(uop 吞吐量),它只比洗牌便宜或其他东西。)
我非常推荐只使用 2x shufps
。 平均来说会很好 CPU,而且在任何地方都不可怕。
AVX512
AVX512 引入了一个跨车道 pack-with-t运行cation 指令,它缩小了单个向量(而不是 2 输入混洗)。它是 pmovzx
的倒数,可以缩小 64b->8b 或任何其他组合,而不仅仅是缩小 2 倍。
对于这种情况,__m256i _mm512_cvtepi64_epi32 (__m512i a)
(vpmovqd
) 将从向量中取出偶数个 32 位元素并将它们打包在一起。 (即每个 64 位元素的低半部分)。不过,它仍然不是交错的好构建块,因为您需要其他东西来放置奇数元素。
它还有 signed/unsigned 饱和版本。这些指令甚至具有内存目标形式,内在函数公开该形式让您进行屏蔽存储。
但是对于这个问题,正如 Mysticial 指出的那样,AVX512 提供了 2 个输入的车道交叉洗牌,您可以像 shufps
一样使用它来在两个洗牌中解决整个问题:vpermi2d/vpermt2d
.
Intel 的矢量扩展 SSE、AVX 等为每个元素大小提供两个解包操作,例如SSE 内在函数是 _mm_unpacklo_*
和 _mm_unpackhi_*
。对于向量中的 4 个元素,它会这样做:
inputs: (A0 A1 A2 A3) (B0 B1 B2 B3)
unpacklo/hi: (A0 B0 A1 B1) (A2 B2 A3 B3)
unpack 相当于 ARM 的 NEON 指令集中的 vzip
。但是,NEON 指令集还提供了 vuzp
运算,它是 vzip
的逆运算。对于向量中的 4 个元素,它会这样做:
inputs: (A0 A1 A2 A3) (B0 B1 B2 B3)
vuzp: (A0 A2 B0 B2) (A1 A3 B1 B3)
如何 vuzp
使用 SSE 或 AVX 内在函数有效地实现?似乎没有关于它的说明。对于 4 个元素,我假设可以使用随机播放和随后的解包移动 2 个元素来完成:
inputs: (A0 A1 A2 A3) (B0 B1 B2 B3)
shuffle: (A0 A2 A1 A3) (B0 B2 B1 B3)
unpacklo/hi 2: (A0 A2 B0 B2) (A1 A3 B1 B3)
是否有更高效的单指令解决方案? (也许首先是 SSE - 我知道对于 AVX,我们可能会遇到其他问题,即洗牌和解包不会跨车道。)
了解这一点可能对编写数据调配和解调代码很有用(只需根据解包操作反转调配代码的操作,就可以推导出解调代码)。
编辑:这是 8 元素版本:这是 NEON 的效果 vuzp
:
input: (A0 A1 A2 A3 A4 A5 A6 A7) (B0 B1 B2 B3 B4 B5 B6 B7)
vuzp: (A0 A2 A4 A6 B0 B2 B4 B6) (A1 A3 A5 A7 B1 B3 B5 B7)
这是我的版本,每个输出元素有一个 shuffle
和一个 unpack
(似乎可以推广到更大的元素编号):
input: (A0 A1 A2 A3 A4 A5 A6 A7) (B0 B1 B2 B3 B4 B5 B6 B7)
shuffle: (A0 A2 A4 A6 A1 A3 A5 A7) (B0 B2 B4 B6 B1 B3 B5 B7)
unpacklo/hi 4: (A0 A2 A4 A6 B0 B2 B4 B6) (A1 A3 A5 A7 B1 B3 B5 B7)
EOF 建议的方法是正确的,但每个输出需要 log2(8)=3
unpack
次操作:
input: (A0 A1 A2 A3 A4 A5 A6 A7) (B0 B1 B2 B3 B4 B5 B6 B7)
unpacklo/hi 1: (A0 B0 A1 B1 A2 B2 A3 B3) (A4 B4 A5 B5 A6 B6 A7 B7)
unpacklo/hi 1: (A0 A4 B0 B4 A1 A5 B1 B5) (A2 A6 B2 B6 A3 A7 B3 B7)
unpacklo/hi 1: (A0 A2 A4 A6 B0 B2 B4 B6) (A1 A3 A5 A7 B1 B3 B5 B7)
it should be possible to derive deswizzling code just by inverting the operations
习惯于对英特尔矢量洗牌的非正交性感到失望和沮丧。 punpck
没有正反函数。 SSE/AVX pack
指令用于缩小元素大小。 (所以一个 packusdw
is the inverse of punpck[lh]wd
对零,但与两个任意向量一起使用时则不然)。此外,pack
指令仅适用于 32->16(双字到字)和 16->8(字到字节)元素大小。没有packusqd
(64->32).
PACK 指令仅在饱和时可用,而不是 t运行cation(直到 AVX512 vpmovqd
),因此对于此用例,我们需要为 2 个 PACK 准备 4 个不同的输入向量指示。结果证明这是可怕的,比你的 3-shuffle 解决方案更糟糕(参见下面 Godbolt link 中的 unzip32_pack()
)。
但是有一个 2 输入随机播放可以为 32 位元素执行您想要的操作:shufps
。结果的低 2 元素可以是第一个向量的任意 2 个元素,高 2 元素可以是第二个向量的任意元素。我们想要的洗牌符合这些限制,所以我们可以使用它。
我们可以在 2 条指令中解决整个问题(为非 AVX 版本加上一个 movdqa
,因为 shufps
破坏了左输入寄存器):
inputs: a=(A0 A1 A2 A3) a=(B0 B1 B2 B3)
_mm_shuffle_ps(a,b,_MM_SHUFFLE(2,0,2,0)); // (A0 A2 B0 B2)
_mm_shuffle_ps(a,b,_MM_SHUFFLE(3,1,3,1)); // (A1 A3 B1 B3)
_MM_SHUFFLE()
uses most-significant-element first notation,就像英特尔的所有文档一样。你的符号是相反的。
shufps
的唯一内在函数使用 __m128
/ __m256
向量(float
不是整数),因此您必须强制转换才能使用它。 _mm_castsi128_ps
是一个 reinterpret_cast:它编译为零指令。
#include <immintrin.h>
static inline
__m128i unziplo(__m128i a, __m128i b) {
__m128 aps = _mm_castsi128_ps(a);
__m128 bps = _mm_castsi128_ps(b);
__m128 lo = _mm_shuffle_ps(aps, bps, _MM_SHUFFLE(2,0,2,0));
return _mm_castps_si128(lo);
}
static inline
__m128i unziphi(__m128i a, __m128i b) {
__m128 aps = _mm_castsi128_ps(a);
__m128 bps = _mm_castsi128_ps(b);
__m128 hi = _mm_shuffle_ps(aps, bps, _MM_SHUFFLE(3,1,3,1));
return _mm_castps_si128(hi);
}
gcc 会将它们分别内联到一条指令中。删除 static inline
后,我们可以看到它们如何编译为非内联函数。我把它们放在 the Godbolt compiler explorer
unziplo(long long __vector(2), long long __vector(2)):
shufps xmm0, xmm1, 136
ret
unziphi(long long __vector(2), long long __vector(2)):
shufps xmm0, xmm1, 221
ret
在最近 Intel/AMD CPU 秒内,对整数数据使用 FP 洗牌没问题。没有额外的旁路延迟延迟(参见 this answer which summarizes what Agner Fog's microarch guide says about it). It has extra latency on Intel Nehalem , but may still be the best choice there. FP loads/shuffles
有趣的事实:在 AMD Bulldozer 系列 CPUs(和 Intel Core2)上,FP 洗牌如 shufps
仍然 运行 在 ivec 域中,因此它们实际上有额外的延迟当在 FP 指令之间使用时,而不是在整数指令之间使用!
与 ARM NEON / ARMv8 SIMD 不同,x86 SSE 没有任何 2 输出寄存器指令,它们在 x86 中很少见。 (它们存在,例如 mul r64
,但总是在当前 CPU 上解码为多个微指令)。
创建 2 个结果向量总是至少需要 2 条指令。如果他们不需要 运行 在洗牌端口上,那将是理想的,因为最近的英特尔 CPUs 的洗牌吞吐量仅为每个时钟 1 个。当您的所有指令都是随机播放时,指令级并行性没有太大帮助。
对于吞吐量,1 次随机播放 + 2 次非随机播放可能比 2 次随机播放更有效,并且具有相同的延迟。甚至 2 次洗牌和 2 次混合可能比 3 次洗牌更有效,具体取决于周围代码中的瓶颈。但我认为我们不能用那几条指令替换 2x shufps
。
没有SHUFPS
:
你的shuffle + unpacklo/hi很不错。总共有 4 次洗牌:2 pshufd
准备输入,然后 2 punpck
l/h。这可能比任何旁路延迟都更糟糕,除了在 Nehalem 上,在延迟很重要但吞吐量不重要的情况下。
任何其他选项似乎都需要准备 4 个输入向量,用于混合或 packss
。有关混合选项,请参阅 @Mysticial's answer to _mm_shuffle_ps() equivalent for integer vectors (__m128i)?。对于两个输出,总共需要 4 次洗牌才能完成输入,然后是 2x pblendw
(快)或 vpblendd
(甚至更快)。
对 16 位或 8 位元素使用 packsswd
或 wb
也可以。屏蔽 a 和 b 的奇数元素需要 2x pand
条指令,将奇数元素向下移动到偶数位置需要 2x psrld
条指令。这为您设置了 2x packsswd
来创建两个输出向量。总共 6 条指令,加上许多 movdqa
,因为这些指令都破坏了它们的输入(不像 pshufd
,后者是复制+洗牌)。
// don't use this, it's not optimal for any CPU
void unzip32_pack(__m128i &a, __m128i &b) {
__m128i a_even = _mm_and_si128(a, _mm_setr_epi32(-1, 0, -1, 0));
__m128i a_odd = _mm_srli_epi64(a, 32);
__m128i b_even = _mm_and_si128(b, _mm_setr_epi32(-1, 0, -1, 0));
__m128i b_odd = _mm_srli_epi64(b, 32);
__m128i lo = _mm_packs_epi16(a_even, b_even);
__m128i hi = _mm_packs_epi16(a_odd, b_odd);
a = lo;
b = hi;
}
Nehalem 是唯一一个 CPU 可能值得使用 2x shufps
以外的东西的地方,因为它具有高 (2c) 旁路延迟。它每个时钟有 2 个洗牌吞吐量,pshufd
是一个复制+洗牌,所以 2x pshufd
准备 a
和 b
的副本只需要一个额外的 movdqa
之后将 punpckldq
和 punpckhdq
结果放入单独的寄存器中。 (movdqa
不是免费的;它有 1c 延迟并且需要 Nehalem 上的向量执行端口。如果你在洗牌吞吐量上遇到瓶颈,而不是整体前端带宽(uop 吞吐量),它只比洗牌便宜或其他东西。)
我非常推荐只使用 2x shufps
。 平均来说会很好 CPU,而且在任何地方都不可怕。
AVX512
AVX512 引入了一个跨车道 pack-with-t运行cation 指令,它缩小了单个向量(而不是 2 输入混洗)。它是 pmovzx
的倒数,可以缩小 64b->8b 或任何其他组合,而不仅仅是缩小 2 倍。
对于这种情况,__m256i _mm512_cvtepi64_epi32 (__m512i a)
(vpmovqd
) 将从向量中取出偶数个 32 位元素并将它们打包在一起。 (即每个 64 位元素的低半部分)。不过,它仍然不是交错的好构建块,因为您需要其他东西来放置奇数元素。
它还有 signed/unsigned 饱和版本。这些指令甚至具有内存目标形式,内在函数公开该形式让您进行屏蔽存储。
但是对于这个问题,正如 Mysticial 指出的那样,AVX512 提供了 2 个输入的车道交叉洗牌,您可以像 shufps
一样使用它来在两个洗牌中解决整个问题:vpermi2d/vpermt2d
.