在 arm neon 中高效地重新洗牌和组合 16 个 3 位数字
Efficiently reshuffle and combine 16 3-bit numbers in arm neon
我有一个由 16 个无符号数组成的数组,其中每个数字都小于 8(例如,它可以用 3 位表示)。这 16 个数字被加载到 uint8x16_t
q 寄存器中。我需要重新洗牌并组合它们,类似于这个伪代码:
void reshuffleCombine(uint8_t src[16], uint64_t* dst)
{
uint64 d = 0;
d |= uint64(src[0]) << 45;
d |= uint64(src[4]) << 42;
d |= uint64(src[8]) << 39;
d |= uint64(src[12]) << 36;
d |= uint64(src[1]) << 33;
d |= uint64(src[5]) << 30;
d |= uint64(src[9]) << 27;
d |= uint64(src[13]) << 24;
d |= uint64(src[2]) << 21;
d |= uint64(src[6]) << 18;
d |= uint64(src[10]) << 15;
d |= uint64(src[14]) << 12;
d |= uint64(src[3]) << 9;
d |= uint64(src[7]) << 6;
d |= uint64(src[11]) << 3;
d |= uint64(src[15]) << 0;
*dst = d;
}
void reshuffleCombineNeon(uint8x16_t src, uint64_t* dst)
{
uint64x1_t res;
// ??
vst1_u64(dst, res);
}
我可以先用 1 个 vld 再用 1 个 vtbl 重新洗牌,但是,这整个操作是最后的步骤之一,不会重复很多次(例如,1 个 vld 不能在多个 reshuffleCombines 之间共享),因此它可能是如果可能,最好使用 vtrn/vzip,并且可能比 vld+vtbl 更有效。但是,问题的要点是:如何将所有这 16 个 3 位数字合并为一个 48 位值(存储在 64 位 uint 中)。该函数在算法结束时运行,16个3位数字来自neon,函数结果存储在内存中。
void reshuffleCombineNeon(uint8x16_t src, uint32_t* dst)
{
static const uint8_t idx0[] = { 15, 7, 14, 6, 13, 5, 12, 4 };
static const uint8_t idx1[] = { 11, 3, 10, 2, 9, 1, 8, 0 };
uint8x8x2_t y;
y.val[0] = vget_low_u8(src);
y.val[1] = vget_high_u8(src);
uint8x8_t vidx0 = vld1_u8(idx0);
uint8x8_t vidx1 = vld1_u8(idx1);
uint8x8_t x0 = vtbl2_u8(y, vidx0);
uint8x8_t x1 = vtbl2_u8(y, vidx1);
uint8x8_t x01 = vsli_n_u8(x0, x1, 3);
uint16x8_t x01L = vmovl_u8(x01);
uint32x4_t x01LL = vsraq_n_u32(vreinterpretq_u32_u16(x01L), vreinterpretq_u32_u16(x01L), 10);
x01LL = vmovl_u16(vmovn_u32(x01LL));
uint64x2_t x01X = vsraq_n_u64(vreinterpretq_u64_u32(x01LL), vreinterpretq_u64_u32(x01LL), 20);
x01X = vmovl_u32(vmovn_u64(x01X));
uint64x1_t X0 = vget_low_u64(x01X);
uint64x1_t X1 = vget_high_u64(x01X);
X0 = vsli_n_u64(X0, X1, 24);
vst1_u32(dst, vreinterpret_u32_u64(X0));
}
好吧,不管它值多少钱,下面是据我所知优化的 NEON 版本:
rev64 v16.16b, v16.16b
usra v16.2d, v16.2d, #32-3 // 8 elements (8bit)
ushll v17.8h, v16.8b, #6
uaddw2 v16.8h, v17.8h, v16.16b // 4 elements (16bit)
uxtl v16.4s, v16.4h
usra v16.2d, v16.2d, #32-12 // 2 elements (32bit)
ushll2 v17.2d, v16.4s, #24
uaddw v16.2d, v17.2d, v16.2s // 1 element (64bit), d16 contains the result
它应该比你的快得多,但同样,它在顺序机器上没有多大意义。
我有一个由 16 个无符号数组成的数组,其中每个数字都小于 8(例如,它可以用 3 位表示)。这 16 个数字被加载到 uint8x16_t
q 寄存器中。我需要重新洗牌并组合它们,类似于这个伪代码:
void reshuffleCombine(uint8_t src[16], uint64_t* dst)
{
uint64 d = 0;
d |= uint64(src[0]) << 45;
d |= uint64(src[4]) << 42;
d |= uint64(src[8]) << 39;
d |= uint64(src[12]) << 36;
d |= uint64(src[1]) << 33;
d |= uint64(src[5]) << 30;
d |= uint64(src[9]) << 27;
d |= uint64(src[13]) << 24;
d |= uint64(src[2]) << 21;
d |= uint64(src[6]) << 18;
d |= uint64(src[10]) << 15;
d |= uint64(src[14]) << 12;
d |= uint64(src[3]) << 9;
d |= uint64(src[7]) << 6;
d |= uint64(src[11]) << 3;
d |= uint64(src[15]) << 0;
*dst = d;
}
void reshuffleCombineNeon(uint8x16_t src, uint64_t* dst)
{
uint64x1_t res;
// ??
vst1_u64(dst, res);
}
我可以先用 1 个 vld 再用 1 个 vtbl 重新洗牌,但是,这整个操作是最后的步骤之一,不会重复很多次(例如,1 个 vld 不能在多个 reshuffleCombines 之间共享),因此它可能是如果可能,最好使用 vtrn/vzip,并且可能比 vld+vtbl 更有效。但是,问题的要点是:如何将所有这 16 个 3 位数字合并为一个 48 位值(存储在 64 位 uint 中)。该函数在算法结束时运行,16个3位数字来自neon,函数结果存储在内存中。
void reshuffleCombineNeon(uint8x16_t src, uint32_t* dst)
{
static const uint8_t idx0[] = { 15, 7, 14, 6, 13, 5, 12, 4 };
static const uint8_t idx1[] = { 11, 3, 10, 2, 9, 1, 8, 0 };
uint8x8x2_t y;
y.val[0] = vget_low_u8(src);
y.val[1] = vget_high_u8(src);
uint8x8_t vidx0 = vld1_u8(idx0);
uint8x8_t vidx1 = vld1_u8(idx1);
uint8x8_t x0 = vtbl2_u8(y, vidx0);
uint8x8_t x1 = vtbl2_u8(y, vidx1);
uint8x8_t x01 = vsli_n_u8(x0, x1, 3);
uint16x8_t x01L = vmovl_u8(x01);
uint32x4_t x01LL = vsraq_n_u32(vreinterpretq_u32_u16(x01L), vreinterpretq_u32_u16(x01L), 10);
x01LL = vmovl_u16(vmovn_u32(x01LL));
uint64x2_t x01X = vsraq_n_u64(vreinterpretq_u64_u32(x01LL), vreinterpretq_u64_u32(x01LL), 20);
x01X = vmovl_u32(vmovn_u64(x01X));
uint64x1_t X0 = vget_low_u64(x01X);
uint64x1_t X1 = vget_high_u64(x01X);
X0 = vsli_n_u64(X0, X1, 24);
vst1_u32(dst, vreinterpret_u32_u64(X0));
}
好吧,不管它值多少钱,下面是据我所知优化的 NEON 版本:
rev64 v16.16b, v16.16b
usra v16.2d, v16.2d, #32-3 // 8 elements (8bit)
ushll v17.8h, v16.8b, #6
uaddw2 v16.8h, v17.8h, v16.16b // 4 elements (16bit)
uxtl v16.4s, v16.4h
usra v16.2d, v16.2d, #32-12 // 2 elements (32bit)
ushll2 v17.2d, v16.4s, #24
uaddw v16.2d, v17.2d, v16.2s // 1 element (64bit), d16 contains the result
它应该比你的快得多,但同样,它在顺序机器上没有多大意义。