压缩 12 位整数数组的 SIMD 位重新排序

Rus*_*man 3 c simd pixelformat neon avx2

我有一个大的紧密封装的 12 位整数数组,采用以下重复位封装模式:(其中A n /B n中的n表示位数,A 和 B 是数组中的前两个 12 位整数)

|           byte0           |            byte1          |           byte2         | etc..
| A11 A10 A9 A8 A7 A6 A5 A4 | B11 B10 B9 B8 B7 B6 B5 B4 | B3 B2 B1 B0 A3 A2 A1 A0 | etc..
Run Code Online (Sandbox Code Playgroud)

我将其重新排序为以下模式:

|           byte0           |            byte1          |           byte2         | etc..
| A11 A10 A9 A8 A7 A6 A5 A4 | A3 A2 A1 A0 B11 B10 B9 B8 | B7 B6 B5 B4 B3 B2 B1 B0 | etc..
Run Code Online (Sandbox Code Playgroud)

我使用以下代码使其在每 3 字节循环中工作:

void CSI2toBE12(uint8_t* pCSI2, uint8_t* pBE, uint8_t* pCSI2LineEnd)
{
    while (pCSI2 < pCSI2LineEnd) {
        pBE[0] = pCSI2[0];
        pBE[1] = ((pCSI2[2] & 0xf) << 4) | (pCSI2[1] >> 4);
        pBE[2] = ((pCSI2[1] & 0xf) << 4) | (pCSI2[2] >> 4);
        
        // Go to next 12-bit pixel pair (3 bytes)
        pCSI2 += 3;
        pBE += 3;
    }
}
Run Code Online (Sandbox Code Playgroud)

但使用字节粒度对于性能来说并不是很好。目标 CPU 是 64 位 ARM Cortex-A72(Raspberry Pi 计算模块 4)。对于上下文,此代码将 MIPI CSI-2 位打包原始图像数据转换为 Adob​​e DNG 的位打包。

我希望使用 SIMD 内在函数能够获得相当大的性能提升,但我不太确定从哪里开始。我有 SIMDe 标头来转换内在函数,因此欢迎 AVX/AVX2 解决方案。

Nat*_*dge 5

NEONld3指令非常适合此目的;它加载 48 个字节并将它们解压缩到三个 NEON 寄存器中。那么你只需要几个轮班和手术室。

我想出了以下几点:

void vectorized(const uint8_t* pCSI2, uint8_t* pBE, const uint8_t* pCSI2LineEnd)
{
    while (pCSI2 < pCSI2LineEnd) {
        uint8x16x3_t in = vld3q_u8(pCSI2);
        uint8x16x3_t out;
        out.val[0] = in.val[0];
        out.val[1] = vorrq_u8(vshlq_n_u8(in.val[2], 4), vshrq_n_u8(in.val[1], 4));
        out.val[2] = vorrq_u8(vshlq_n_u8(in.val[1], 4), vshrq_n_u8(in.val[2], 4));
        vst3q_u8(pBE, out);
        pCSI2 += 48;
        pBE += 48;
    }
}
Run Code Online (Sandbox Code Playgroud)

试试神螺栓

使用 gcc,生成的程序集看起来就像您所期望的那样。(有一个mov可以通过更好的寄存器分配来消除,但这是相当小的。)

不幸的是,clang 有一个看起来奇怪的错过优化,它将 4 位右移分解为 3 位和 1 位移位。 我提交了一个错误

原则上,我们可以使用 、 Shift Left 和 Insert 做得更好一点sli,以有效地将 OR 与其中一个移位合并:

out.val[1] = vsliq_n_u8(vshrq_n_u8(in.val[1], 4), in.val[2], 4);
out.val[2] = vsliq_n_u8(vshrq_n_u8(in.val[2], 4), in.val[1], 4);
Run Code Online (Sandbox Code Playgroud)

但由于它会覆盖其源操作数,因此我们需要支付几个额外的费用movhttps://godbolt.org/z/TbzEEd1Pn。clang 分配寄存器更加巧妙,只需要一个mov

另一种可能稍微快一些的选项是使用sra、右移和累加,它执行添加而不是插入。由于此处相关位已经为零,因此具有相同的效果。奇怪的是没有sla

out.val[1] = vsraq_n_u8(vshlq_n_u8(in.val[2], 4), in.val[1], 4);
out.val[2] = vsraq_n_u8(vshlq_n_u8(in.val[1], 4), in.val[2], 4);
Run Code Online (Sandbox Code Playgroud)