英特尔的矢量扩展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)
在ARM的NEON指令集中,unpack的等效值为vzip
。但是,NEON指令集还提供操作vuzp
,它是vzip
的反转。对于向量中的4个元素,它执行此操作:
inputs: (A0 A1 A2 A3) (B0 B1 B2 B3)
vuzp: (A0 A2 B0 B2) (A1 A3 B1 B3)
如何使用SSE或AVX内在函数有效地实现vuzp
?似乎没有针对它的指示。对于4个元素,我假设它可以使用shuffle和随后的unpack移动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我们可能有另外的问题,洗牌和解包不会跨越车道。)
知道这可能对编写数据调配和deswizzling的代码很有用(应该可以通过基于解包操作反转调配代码的操作来导出deswizzling代码)。
编辑:这是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)
答案 0 :(得分:4)
应该可以通过反转操作来导出deswizzling代码
习惯于对英特尔矢量改组的非正交性感到失望和沮丧。 punpck
没有直接反转。 SSE / AVX pack
指令用于缩小元素大小。 (因此一个packusdw
是punpck[lh]wd
与零的倒数,但与两个任意向量一起使用时则不然。此外,pack
指令仅适用于32-> 16(字到字)和16-> 8(字到字)元素大小。没有packusqd
(64-> 32)。
PACK指令仅适用于饱和,而不是截断(直到AVX512 vpmovqd
),因此对于这种用例,我们需要为2个PACK指令准备4个不同的输入向量。事实证明这是可怕的,比你的3-shuffle解决方案更糟糕(见下面Godbolt链接中的unzip32_pack()
)。
有一个2输入的shuffle可以为32位元素执行所需的操作,但是:shufps
。结果的低2个元素可以是第一个向量的任何2个元素,高2个元素可以是第二个向量的任何元素。我们想要的洗牌符合这些约束条件,因此我们可以使用它。
我们可以解决2条指令中的整个问题(加上movdqa
非AVX版本,因为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 shuffle很好。没有额外的旁路延迟延迟(请参阅this answer,其中总结了Agner Fog's microarch guide对此的说法)。它在Intel Nehalem上具有额外的延迟,但可能仍然是那里的最佳选择。 FP加载/随机播放won't fault or corrupt integer bit-patterns that represent a NaN,只有实际的FP数学指令关心它。
有趣的事实:在AMD Bulldozer系列CPU(和Intel Core2)上,像shufps
这样的FP shuffle仍然在ivec域中运行,所以它们在FP指令之间使用时实际上有额外的延迟,但在整数指令之间没有!
与ARM NEON / ARMv8 SIMD不同, x86 SSE没有任何2输出寄存器指令,并且它们在x86中很少见。 (它们存在,例如mul r64
,但总是在当前CPU上解码为多个uops。)
它总是至少需要2条指令来创建2个结果向量。如果它们都不需要在shuffle端口上运行,那将是理想的,因为最近的Intel CPU具有每时钟仅1的shuffle吞吐量。当所有指令都是随机播放时,指令级并行性并没有多大帮助。
对于吞吐量,1次shuffle + 2次非shuffle可能比2次shuffle更有效,并且具有相同的延迟。或者甚至2次shuffle和2次混合比3次shuffle更有效,这取决于周围代码中的瓶颈。但我认为我们不能用少量指令替换2x shufps
。
SHUFPS
:你的shuffle + unpacklo / hi非常好。共计4次洗牌:2 pshufd
准备输入,然后2 punpck
l / h。这可能比任何旁路延迟更糟,除了Nehalem,在延迟很重要但吞吐量不高的情况下。
任何其他选项似乎都需要为混合或packss
准备4个输入向量。有关混合选项,请参阅@Mysticial's answer to _mm_shuffle_ps() equivalent for integer vectors (__m128i)?。对于两个输出,输入总共需要4次,然后是2次pblendw
(快速)或vpblendd
(甚至更快)。
对16位或8位元素使用packsswd
或wb
也可以。需要2x pand
指令来屏蔽a和b的奇数元素,并且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是唯一可能值得使用2x shufps
以外的东西的CPU,因为它的高(2c)旁路延迟。它有2个时钟随机播放吞吐量,而pshufd
是副本+随机播放,因此2x pshufd
准备a
和b
的副本只需要一个额外的{{1}之后,将movdqa
和punpckldq
结果放入单独的寄存器中。 (punpckhdq
不是免费的;它有1c延迟并且需要Nehalem上的矢量执行端口。如果您在随机播放吞吐量方面遇到瓶颈,而不是总体而言,它只比洗牌更便宜前端带宽(uop吞吐量)或其他东西。)
我非常推荐使用2x movdqa
。这对普通CPU来说会很好,而且在任何地方都不会太糟糕。
AVX512引入了一个带有截断通道的带有截断的指令,该指令缩小了单个向量(而不是2输入的shuffle)。它是shufps
的倒数,可以缩小64b-> 8b或任何其他组合,而不是仅为2倍。
对于这种情况,__m256i _mm512_cvtepi64_epi32 (__m512i a)
(pmovzx
)将从向量中取出偶数32位元素并将它们打包在一起。 (即每个64位元素的低半部分)。但是,它仍然不是交错的良好构建块,因为你需要其他东西才能将奇数元素放到位。
它还有签名/无符号饱和版本。这些指令甚至还有一个内存目标表单,内在函数暴露给你,让你做一个蒙面存储。
但是对于这个问题,正如Mysticial指出的那样, AVX512提供了2输入交叉的shuffle,您可以像vpmovqd
一样使用它来解决整个问题:vpermi2d/vpermt2d
强>