在 arm neon 中有效地重新洗牌和组合 16 个 3 位数字

Posted

技术标签:

【中文标题】在 arm neon 中有效地重新洗牌和组合 16 个 3 位数字【英文标题】:Efficiently reshuffle and combine 16 3-bit numbers in arm neon 【发布时间】:2018-05-06 19:24:01 【问题描述】:

我有一个包含 16 个无符号数字的数组,其中每个数字都小于 8(例如,它可以用 3 位表示)。这十六个数字被加载到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,函数结果存储在内存中。

My neon version:

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));

【问题讨论】:

有什么意义?坚持使用您的 C 版本,总字节数少于 128 字节。这是经验法则。 @Jake'Alquimista'LEE 我的测量结果表明,我对这个功能的霓虹灯尝试并没有带来太多好处,但我仍然看到它。尽管它是算法的叶函数,但该算法每几毫秒调用数万次。因此,如果我可以节省 10-20 个周期,我将对整体运行时间产生显着影响。基本上这 16 个 3 位数字来自霓虹灯,这个函数的结果进入内存。如果我在霓虹灯中做这一切,我可以避免失速,并且可能在霓虹灯中比在手臂本身做得更好。 只是因为你在 ooo 机器上运行它。它会比有序版本的纯 C 版本慢得多。除非性能至关重要,否则如果性能增益低于 100%,则根本不应该使用 NEON,因为功耗会急剧增加。 我在循环中运行我的算法 10K 次,所以这个特定的部分运行了大约 500K 次。我对总时间进行了概要分析,并且确实得到了改进,但是由于 ooo 并且因为这件作品很小,所以很难看到它有多好的细节。然而,我确实怀疑我的霓虹灯版本不是那么好;) @Jake'Alquimista'LEE 我添加了我的霓虹灯版本。它显然比 c 版本更短,并且避免存储 16 个字节并读取它们。 【参考方案1】:

好吧,不管它值多少钱,下面是根据我所知优化的 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

它应该比你的快得多,但同样,它在有序机器上没有多大意义。

【讨论】:

我现在正在重构/重写我的代码,前两个操作码正是我所拥有的

以上是关于在 arm neon 中有效地重新洗牌和组合 16 个 3 位数字的主要内容,如果未能解决你的问题,请参考以下文章

与 ARM Neon vtbx 的字节顺序混淆

有效计算 arm neon 中 16 字节缓冲区中不同值的数量

如何使用 Neon Extension 有效地反转汇编语言 ARM 中的数组?

ARM NEON 到 aarch64

我怎样才能有效地洗牌?

SAD 16*4 的 Arm-neon 优化版本未提供预期增益