将缓冲区中的数据压缩为每个元素16位到12位

Pio*_*wak 7 c arm simd neon

我想知道是否有机会提高这种压缩的性能.想法是使高于4095的值饱和,并将每12位的每个值放在新的连续缓冲区中.就像那样:

概念:

兑换:

输入缓冲区:[0.0] [0.1] [0.2] ... [0.15] | [1.0] [1.1] [1.2] ...... [1.15] | [2.0] [2.1] [2.2] ...... [2.15]等......

至:

输出缓冲区:[0.0] [0.1] [0.2] ... [0.11] | [1.0] [1.1] [1.2] ...... [1.11] | [2.0] [2.1] [2.2] ...... [2.11]等......

输入和输出缓冲区定义为:

uint16_t input [76800](字节大小等于153600字节)

uint24_t输出[38400](它的字节大小等于115200字节)

所以我将数据大小减少了1/4.这种计算在Cortex-A9上的成本约为1ms,CPU速度为792 MHz,内核为2.我必须执行这样的"压缩",因为我通过以太网传输大约18MB/s,这给了我巨大的开销.我已经测试过各种压缩算法,例如Snappy,LZ4,并且在饱和度和位漂移的情况下,这些算法甚至都达不到1毫秒.

我写了以下代码:

#pragma pack(push, 1)
typedef struct {
        union {
                struct {
                        uint32_t value0_24x1:24;
                };
                struct {
                        uint32_t value0_12x1:12;
                        uint32_t value1_12x1:12;
                };
                struct {
                        uint32_t value0_8x1:8;
                        uint32_t value1_8x1:8;
                        uint32_t value3_8x1:8;
                };
        };
} uint24_t;
#pragma pack(pop)


static inline uint32_t __attribute__((always_inline)) saturate(uint32_t value)
{
        register uint32_t result;

        asm volatile("usat %0, %2, %1 \n\t"                     \
                : [result] "=r" (result)                        \
                : [value] "r" (value), [saturate] "I" (12)      \
                :                                               \
                );

        return result;
}

void __attribute__((noinline, used)) compact(const uint16_t *input, uint24_t *output, uint32_t elements)
{
#if 0
        /* More readable, but slower */
        for (uint32_t i = 0; i < elements; ++i) {
                output->value0_12x1 = saturate(*input++);
                (output++)->value1_12x1 = saturate(*input++);
        }
#else
        /* Alternative - less readable but faster */
        for (uint32_t i = 0; i < elements; ++i, input += 2)
                (output++)->value0_24x1 = saturate(*input) | ((uint32_t)saturate(*(input+1))) << 12;
#endif
}

static uint16_t buffer_in[76800] = {0};
static uint24_t buffer_out[38400] = {0};

int main()
{
    /* Dividing by 2 because we process two input values in a single loop inside compact() */
    compact(buffer_in, buffer_out, sizeof(buffer_in) / sizeof(buffer_in[0]) / 2);

    return 0;
}
Run Code Online (Sandbox Code Playgroud)

这是大会:

248 00008664 <compact>:
249     8664:   e92d4010    push    {r4, lr}
250     8668:   e3a03000    mov r3, #0
251     866c:   ea00000c    b   86a4 <compact+0x40>
252     8670:   e1d040b0    ldrh    r4, [r0]
253     8674:   e6ec4014    usat    r4, #12, r4
254     8678:   e1d0c0b2    ldrh    ip, [r0, #2]
255     867c:   e6ecc01c    usat    ip, #12, ip
256     8680:   e184c60c    orr ip, r4, ip, lsl #12
257     8684:   e2833001    add r3, r3, #1
258     8688:   e2800004    add r0, r0, #4
259     868c:   e5c1c000    strb    ip, [r1]
260     8690:   e7e7445c    ubfx    r4, ip, #8, #8
261     8694:   e7e7c85c    ubfx    ip, ip, #16, #8
262     8698:   e5c14001    strb    r4, [r1, #1]
263     869c:   e5c1c002    strb    ip, [r1, #2]
264     86a0:   e2811003    add r1, r1, #3
265     86a4:   e1530002    cmp r3, r2
266     86a8:   1afffff0    bne 8670 <compact+0xc>
267     86ac:   e8bd8010    pop {r4, pc}
Run Code Online (Sandbox Code Playgroud)

使用GCC 4.6.3编译以下CFLAGS:

-Os(-O2和-O3没有任何明显的改进)

-march = armv7-a -mcpu = cortex-a9 -mtune = cortex-a9

-marm -mfloat-abi = softfp -mfpu = neon funsafe-math-optimizations

基准测试表明,我们每1个数据转换使用~10.3个周期.

问题是:

  1. 我可以使用NEON来提高性能吗?
  2. 有人可以给我一些关于NEON的提示吗?我应该使用什么内在函数?

一些代码示例将非常受欢迎,因为在NEON方面我是完全的noob.

Jak*_*LEE 5

以下是答案:

  1. 是的,它会非常快.

  2. 你应该不惜一切代价避免使用内在函数.这不值得努力.去集会

我到家后会给你一个示例实施.

////////////////////////////////////////////////// //

好的,这就是:你想要将16位打包到12位.它的比例为4:3.

因此,加载数据4传播并存储3个传播是明智的:vld4.16 - > vst3.16

/*
*   void fanic_pack16to12(unsigned short * pDst, unsigned short * pSrc, unsigned int count);
*   assert :
*       count >= 64
*       count % 4 == 0
*
*   written by : Jake Lee
*   part of FANIC project - Fastest ARM NEON Implementation Challenge
*/
    pDst .req r0
    pSrc .req r1
    count .req r2

    .text
    .arm
    .global fanic_pack16to12:

    .func
    .align 5
fanic_pack16to12:
    pld     [pSrc]
    pld     [pSrc, #64]
    pld     [pSrc, #128]
    pld     [pSrc, #192]
    pld     [pSrc, #256]
    sub     count, count, #64

    .align 5
1:
    vld4.16     {d16, d18, d20, d22}, [pSrc]!
    vld4.16     {d17, d19, d21, d23}, [pSrc]!
    vld4.16     {d24, d26, d28, d30}, [pSrc]!
    vld4.16     {d25, d27, d29, d31}, [pSrc]!
    pld     [pSrc, #128]
    pld     [pSrc, #192]
    subs    count, count, #64

    vqshl.u16   q0, q8, #4
    vqshl.u16   q3, q9, #4
    vqshl.u16   q8, q10, #4
    vqshl.u16   q9, q11, #4
        vqshl.u16   q10, q12, #4
        vqshl.u16   q13, q13, #4
        vqshl.u16   q14, q14, #4
        vqshl.u16   q15, q15, #4
    vshl.u16    q1, q3, #4
    vshl.u16    q2, q8, #8
        vshl.u16    q11, q13, #4
        vshl.u16    q12, q14, #8
    vsri.16     q0, q3, #12
    vsri.16     q1, q8, #8
    vsri.16     q2, q9, #4
        vsri.16     q10, q13, #12
        vsri.16     q11, q14, #8
        vsri.16     q12, q15, #4

    vst3.16     {d0, d2, d4}, [pDst]!
    vst3.16     {d1, d3, d5}, [pDst]!
    vst3.16     {d20, d22, d24}, [pDst]!
    vst3.16     {d21, d23, d25}, [pDst]!
    bpl     1b

    cmp     count, #-64
    add     pDst, pDst, count

    bxle    lr

    add     pSrc, pSrc, count, lsl #1
    add     pDst, pDst, count, asr #1
    b       1b
     .endfunc
     .end
Run Code Online (Sandbox Code Playgroud)

请注意智能寄存器分配和循环控制可以节省多少周期和带宽 - 这些内核无法实现.

该实现将如此快速地运行,就像由专用硬件完成一样.

  • 绝对没有管道危险.
  • 大约50个循环/迭代=小于1个循环/数据

玩得开心!

////////////////////////////////////////////////// ////

好的,下面是解包功能:

/*
*   void fanic_unpack12to16(unsigned short *pDst, unsigned short *pSrc, unsigned int count);
*   assert :
*       count >=64
*       count % 4 == 0
*   
*   written by : Jake Lee
*   part of FANIC project - Fastest ARM NEON Implementation Challenge
*/
    pDst .req r0
    pSrc .req r1
    count .req r2

    .text
    .arm
    .global fanic_unpack12to16:

    .func
    .align 5
fanic_unpack12to16:

    pld [pSrc]
    pld [pSrc, #64*1]
    pld [pSrc, #64*2]
    vpush       {q4}
    pld [pSrc, #64*3]
    vmov.i16    q4, #0x0fff
    pld [pSrc, #64*4]
    sub count, count, #64

    .align 5
1:
    vld3.16     {d20, d22, d24}, [pSrc]!
    vld3.16     {d21, d23, d25}, [pSrc]!
    vld3.16     {d26, d28, d30}, [pSrc]!
    vld3.16     {d27, d29, d31}, [pSrc]!
    pld     [pSrc, #128]
    pld     [pSrc, #192]
    subs    count, count, #64

    vshr.u16    q1, q11, #8
    vshr.u16    q2, q12, #12
    vshr.u16    q0, q10, #4
    vand        q3, q12, q4
        vshr.u16    q9, q14, #8
    vsli.16     q1, q10, #8
    vsli.16     q2, q11, #4
        vshr.u16    q10, q15, #12
        vsli.16     q9, q13, #8
    vbic.i16    q1, q1, #0xf000
    vbic.i16    q2, q2, #0xf000
    vsli.16     q10, q14, #4
    vshr.u16    q8, q13, #4
    vbic.i16    q9, q9, #0xf000
    vand        q11, q15, q4
    vbic.i16    q10, q10, #0xf000

    vst4.16     {d0, d2, d4, d6}, [pDst]!
    vst4.16     {d1, d3, d5, d7}, [pDst]!
    vst4.16     {d16, d18, d20, d22}, [pDst]!
    vst4.16     {d17, d19, d21, d23}, [pDst]!
    bpl     1b

    cmp     count, #-64
    add     pSrc, pSrc, count

    vpople      {q4}
    bxle    lr

    add     pSrc, pSrc, count, asr #1
    add     pDst, pDst, count, lsl #1
    b       1b

    .endfunc
    .end
Run Code Online (Sandbox Code Playgroud)

调整点数:

  • 强制对齐src和dst为64字节以获得最大带宽效率
  • 然后保证所有与内存相关的指令对齐.4个传播的256位,3个传播的64位如下:

    vld4.16 {d16,d18,d20,d22},[pSrc,:256]!

    ..

    vst3.16 {d0,d2,d4},[pDst,:64]!

    ..

  • 使得计数为64的倍数.否则,您将不得不编写处理残留数据的额外代码(当前的代码会因对齐错误而崩溃)

  • 你可以增加/减少pld偏移64,以增加缓存命中率

如果不是很大的话,这将提高性能.