我有以下代码,它每 4 位 64 位 int 正确打包一次。这是天真的方法,我正在使用查找表和循环。我想知道是否有一种更快的比特摆动、swar/simd、并行方法来更快地做到这一点? (msb() 返回最高有效位)
def pack(X):
compact = [
0b0000, # 0
0b0001, # 1
0b0001, # 10
0b0011, # 11
0b0001, #100
0b0011, #101
0b0011, #110
0b0111, #111
0b0001, #1000
0b0011, #1001
0b0011, #1010
0b0111, #1011
0b0011, #1100
0b0111, #1101
0b0111, #1110
0b1111, #1111
]
K = 0
while X:
i = msb(X)
j = (i//4 )*4
a = (X & (0b1111 << j))>>j
K |= compact[a] << j
X = X & ~(0b1111 << j)
return K
大多数 SIMD ISA 都有一个字节随机,可用于实现具有 4 位索引的 16 个条目 LUT。 例如 x86 SSSE3pshufb
或 ARM/AArch64vtbl
/tbl
。
显然,msb()
只是跳过全零半字节的优化,而不是真正的数据依赖关系,这是跨半字节的纯垂直 SIMD。
因此,只需拆分为 4 位半字节并再次打包即可。 对于 x86,可能是一个奇数/偶数拆分,并且两次进行半字节 LUT 比将它们打包在一起更好(例如punpcklbw
或movlhps
)
; asm pseudocode; translate into intrinsics in a language of your choice
; constants:
XMM7 = _mm_set1_epi8(0x0f)
XMM6 = LUT
; input in XMM0, perhaps from vmovq xmm0, rdi or a load
vpsrld xmm1, xmm0, 4 ; v >> 4
vpand xmm0, xmm0, XMM7 ; v &= 0x0f
vpand xmm1, xmm1, XMM7
vpshufb xmm0, XMM6, xmm0 ; low nibbles
vpshufb xmm1, XMM6, xmm1 ; high nibbles
vpslld xmm1, xmm1, 4 ; high << 4 ; alternative: make a shifted copy of the LUT to avoid this
vpor xmm0, xmm0, xmm1
; result in low qword of XMM0; in C you might want _mm_cvtsi128_si64
; vmovq rax, xmm0 get it back into an integer registers if necessary
这实际上可以在 XMM0 的高半和低半并行执行两个 64 位整数,如果你在循环中执行此操作。
使用 AVX-512 VBMI 进行vpermb
,您无需在 LUT 查找之前删除高位。 (vpshufb
使用索引的高位有条件地将输出中的该元素归零,这意味着在大多数情况下将其用作LUT时,您需要将其为零。
只做一个vpshufb
可能涉及vpunpcklbw
复制每个字节,可能允许在vpackuswb
之前与具有常量(如set1_epi16(0x1001)
)的vpmaddubsw
重新组合以移动和添加字节对。 或者可能是广播负载来复制整个 64 位输入,然后 AVX2vpsrlvq
仅右移高半部分。 然后 AND/vpshufb 各一次,而不是两次。 然后 vpunpckhqdq + vpslld + vpor 让高半部分回落并组合。 所以这些似乎都不是很好。
不需要任何特殊 SIMD 指令的替代方法是分别考虑 4 位中的每一个:
def pack(x):
r = x & 0x11111111
r |= r + ((x >> 1) & 0x11111111)
r |= r + ((x >> 2) & 0x11111111)
r |= r + ((x >> 3) & 0x11111111)
return r