C语言 压缩12位整数阵列的SIMD位重排

x6h2sr28  于 2023-08-03  发布在  其他
关注(0)|答案(2)|浏览(110)

我有一个12位整数的大型紧密封装数组,采用以下重复位封装模式:(其中An/Bn 中的 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..

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

|           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..


我已经让它在每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;
    }
}


但是使用字节粒度对性能不是很好。目标CPU是64位ARM Cortex-A72(树莓派计算模块4)。对于上下文,此代码将MIPI CSI-2位压缩的原始图像数据转换为Adobe DNG的位压缩。
我希望使用SIMD intrinsic可以获得相当大的性能改进,但我真的不知道从哪里开始。我有SIMDe头来翻译内部函数,所以AVX/AVX 2解决方案是受欢迎的。

kd3sttzy

kd3sttzy1#

neon ld3指令是理想的;它加载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;
    }
}

字符串
Try on godbolt的数据。
使用gcc,生成的程序集看起来与您所期望的一样。(有一个mov可以通过更好的寄存器分配来消除,但这很小。
不幸的是,clang有一个奇怪的优化缺失,它将4位右移分解为3位和1位的移位。I filed a bug
原则上,我们可以使用sli,Shift Left和Insert来做得更好,以有效地将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);


但是因为它覆盖了源操作数,所以我们需要额外增加几个mov。clang更巧妙地分配寄存器,只需要一个mov
另一个可能稍微快一点的选项是使用sra、Shift Right和Accumulate,它执行加法而不是插入。由于相关位在这里已经是零,所以这具有相同的效果。奇怪的是没有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);

hgc7kmma

hgc7kmma2#

我建议你从图表开始。
我不能说 neon ,所以我将描述我将如何使AVX 2代码做你想要的(但是,你应该用你的目标指令集实现它;如果你的目标是编写新代码,最好不要使用转换器)。x64 intrinsic有很好的文档; here is an example我用的。
AVX 2寄存器具有256位或32字节。也就是10个单位的24位数据。画一张图(最好是在纸上):画出如果从内存中读取,256位寄存器将包含哪些位。然后画出你想在变形后得到的比特。用线连接它们。识别具有相同相对位置的位块。
然后编写代码,隔离相关的位块(_mm256_and_si256),将它们移位(_mm256_slli_si256,可能是_mm256_bslli_epi128或其他)并组合它们(_mm256_or_si256)。AVX 2在移位方面特别特殊,所以我相信 neon 代码会更容易编写。
你的主循环应该包含阅读、处理和写入3个寄存器,或者768位。如果你只为第一个做了一个图,你也许可以类似地实现其他两个。当然,您需要对循环剩余部分(最后几个数据元素)进行特殊处理-使用常规C代码处理它们。

相关问题