ARM NEON:将每像素 8 位二进制图像(仅 0/1)转换为每像素 1 位?

ARM NEON: Convert a binary 8-bit-per-pixel image (only 0/1) to 1-bit-per-pixel?

我正在执行一项任务,将每个像素有 8 位 (uint8_t) 且每个像素只能为 0 或 1(或 255)的大型二进制标签图像转换为一个数组uint64_t个数字,uint64_t个数字中的每一位代表一个标签像素。

例如,

输入数组:0 1 1 0 ... (00000000 00000001 00000001 00000000 ...)

或输入数组:0 255 255 0 ... (00000000 11111111 11111111 00000000 ...)

输出数组(number):6(因为每个uint8_t转换成bit后变成0110

目前实现这个的C代码是:

 for (int j = 0; j < width >> 6; j++) {
        uint8_t* in_ptr= in + (j << 6);
        uint64_t out_bits = 0;
        if (in_ptr[0]) out_bits |= 0x0000000000000001;
        if (in_ptr[1]) out_bits |= 0x0000000000000002;
        .
        .
        .
        if (in_ptr[63]) out_bits |= 0x8000000000000000;
       *output = obits; output ++;
    }

ARM NEON 能否优化此功能?请帮忙。谢谢!

假设输入值为 0 或 255,下面是基本版本,非常简单,特别适合有英特尔 SSE/AVX 经验的人。

void foo_basic(uint8_t *pDst, uint8_t *pSrc, intptr_t length)
{
    //assert(length >= 64);
    //assert(length & 7 == 0);
    uint8x16_t in0, in1, in2, in3;
    uint8x8_t out;
    const uint8x16_t mask = {1, 2, 4, 8, 16, 32, 64, 128, 1, 2, 4, 8, 16, 32, 64, 128};

    length -= 64;

    do {
        do {
            in0 = vld1q_u8(pSrc); pSrc += 16;
            in1 = vld1q_u8(pSrc); pSrc += 16;
            in2 = vld1q_u8(pSrc); pSrc += 16;
            in3 = vld1q_u8(pSrc); pSrc += 16;

            in0 &= mask;
            in1 &= mask;
            in2 &= mask;
            in3 &= mask;

            in0 = vpaddq_u8(in0, in1);
            in2 = vpaddq_u8(in2, in3);

            in0 = vpaddq_u8(in0, in2);

            out = vpadd_u8(vget_low_u8(in0), vget_high_u8(in0));

            vst1_u8(pDst, out); pDst += 8;

            length -= 64;
        } while (length >=0);

        pSrc += length>>3;
        pDst += length;
    } while (length > -64);
}

然而,Neon 具有非常用户友好且高效的排列和位操作指令,允许“垂直”

void foo_advanced(uint8_t *pDst, uint8_t *pSrc, intptr_t length)
{
    //assert(length >= 128);
    //assert(length & 7 == 0);
    uint8x16x4_t in0, in1;
    uint8x16x2_t row04, row15, row26, row37;

    length -= 128;

    do {
        do {
            in0 = vld4q_u8(pSrc); pSrc += 64;
            in1 = vld4q_u8(pSrc); pSrc += 64;

            row04 = vuzpq_u8(in0.val[0], in1.val[0]);
            row15 = vuzpq_u8(in0.val[1], in1.val[1]);
            row26 = vuzpq_u8(in0.val[2], in1.val[2]);
            row37 = vuzpq_u8(in0.val[3], in1.val[3]);

            row04.val[0] = vsliq_n_u8(row04.val[0], row15.val[0], 1);
            row26.val[0] = vsliq_n_u8(row26.val[0], row37.val[0], 1);
            row04.val[1] = vsliq_n_u8(row04.val[1], row15.val[1], 1);
            row26.val[1] = vsliq_n_u8(row26.val[1], row37.val[1], 1);

            row04.val[0] = vsliq_n_u8(row04.val[0], row26.val[0], 2);
            row04.val[1] = vsliq_n_u8(row04.val[1], row26.val[1], 2);

            row04.val[0] = vsliq_n_u8(row04.val[0], row04.val[1], 4);

            vst1q_u8(pDst, row04.val[0]); pDst += 16;

            length -= 128;
        } while (length >=0);

        pSrc += length>>3;
        pDst += length;
    } while (length > -128);
}

仅限 Neon 的高级版本更短更快,但 GCC 在处理 Neon 特定排列指令(例如 vtrnvzip 和 [=16] 时非常糟糕=].

https://godbolt.org/z/bGdbohqKe

Clang 也好不到哪里去:它会发送不必要的垃圾邮件 vorr,其中 GCCvmov 相同。

    .syntax unified
    .arm
    .arch   armv7-a
    .fpu    neon
    .global foo_asm
    .text

.func
.balign 64
foo_asm:
    sub     r2, r2, #128

.balign 16
1:
    vld4.8      {d16, d18, d20, d22}, [r1]!
    vld4.8      {d17, d19, d21, d23}, [r1]!
    vld4.8      {d24, d26, d28, d30}, [r1]!
    vld4.8      {d25, d27, d29, d31}, [r1]!
    subs    r2, r2, #128

    vuzp.8      q8, q12
    vuzp.8      q9, q13
    vuzp.8      q10, q14
    vuzp.8      q11, q15

    vsli.8      q8, q9, #1
    vsli.8      q10, q11, #1
    vsli.8      q12, q13, #1
    vsli.8      q14, q15, #1

    vsli.8      q8, q10, #2
    vsli.8      q12, q14, #2

    vsli.8      q8, q12, #4

    vst1.8      {q8}, [r0]!
    bpl     1b

    add     r1, r1, r2
    cmp     r2, #-128
    add     r0, r0, r2, asr #3

    bgt     1b
.balign 8
    bx      lr

.endfunc
.end

最内层循环包括:
GCC:32 条指令
叮当声:30 条指令
汇编:18 条指令

不需要火箭科学就可以找出最快的速度和速度:如果您要进行排列,请不要相信编译器。

站在Jake 'Alquimista' LEE的肩膀上,我们可以通过改变zip和vlsi运算符的顺序来改进解压指令和算法:

#define interleave_nibbles(top) \
    top.val[0] = vsliq_n_u8(top.val[0], top.val[1],1);\
    top.val[2] = vsliq_n_u8(top.val[2], top.val[3],1);\
    top.val[0] = vsliq_n_u8(top.val[0], top.val[2],2); 

void transpose_bits(uint8_t const *src, uint8_t *dst) {
    uint8x16x4_t top = vld4q_u8(src);
    uint8x16x4_t bot = vld4q_u8(src + 64); src+=128;
    interleave_nibbles(top);
    interleave_nibbles(bot);
    // now we have 4 bits correct in each of the 32 bytes left
    // top = 0to3 4to7 8to11 12to15 ...
    // bot = 64to67 68to71 ...
    uint8x16x2_t top_bot = vuzpq_u8(top.val[0], bot.val[0]);
    uint8x16_t result = vsliq_n_u8(top_bot.val[0], top_bot.val[1], 4);
    vst1q_u8(dst, result); dst += 16;
}

clang 生成的汇编程序现在只有两个无关的 movs(通过 or),gcc 输出有四个 movs。

    vld4.8  {d16, d18, d20, d22}, [r0]!
    vld4.8  {d17, d19, d21, d23}, [r0]!
    vld4.8  {d24, d26, d28, d30}, [r0]!
    vsli.8  q10, q11, #1
    vorr    q0, q8, q8
    vld4.8  {d25, d27, d29, d31}, [r0]
    vsli.8  q0, q9, #1
    vorr    q2, q14, q14
    vsli.8  q12, q13, #1
    vsli.8  q2, q15, #1
    vsli.8  q0, q10, #2
    vsli.8  q12, q2, #2
    vuzp.8  q0, q12
    vsli.8  q0, q12, #4
    vst1.8  {d0, d1}, [r1]

而且 arm64 版本看起来很完美,只有 12 条指令。

    ld4     { v0.16b, v1.16b, v2.16b, v3.16b }, [x0], #64
    ld4     { v4.16b, v5.16b, v6.16b, v7.16b }, [x0]
    sli     v0.16b, v1.16b, #1
    sli     v2.16b, v3.16b, #1
    sli     v0.16b, v2.16b, #2
    sli     v4.16b, v5.16b, #1
    sli     v6.16b, v7.16b, #1
    sli     v4.16b, v6.16b, #2
    uzp1    v16.16b, v0.16b, v4.16b
    uzp2    v0.16b, v0.16b, v4.16b
    sli     v16.16b, v0.16b, #4
    str     q16, [x1]