SSE/AVX:如何将一组 16 位像素(打包 RGB)拆分为位平面

SSE/AVX: How to split a set of 16-bit pixels (packed RGB) into bitplanes

提问人:John Smith 提问时间:10/26/2023 最后编辑:Peter CordesJohn Smith 更新时间:10/29/2023 访问量:136

问:

我有一些基本的 SSE 知识,并编写了一些加速函数。但是这个问题让我难住了,我想知道是否真的有一种加速的 SIMD 方法来处理它。

我有一张包含 3 个颜色通道的图像。每个颜色通道的宽度高达 16 位。数据类型始终为 ,但取决于配置的颜色深度,仅作为位的子集可能是有效的。现在,我想将图像拆分为组成位平面。uint16_t

这意味着我想要一个仅包含像素中每个通道的第一位的缓冲区。另一个包含第二个位的缓冲区。包含第三位等的缓冲区。

基本上,在简化的代码中,我有这个:

#include <inttypes.h>


// img_width is always divisable by 8
// img contains RGB pixels. Each channel is one uint16_t
// where color_depth contains how many bits are valid
// the bitplanes_* are outputs
void extract_bitplanes(
    uint16_t* img,
    uint16_t img_width,
    uint16_t img_height,
    uint8_t color_depth,
    uint8_t** bitplanes_r,
    uint8_t** bitplanes_g,
    uint8_t** bitplanes_b
)
{
    for (uint16_t y = 0; y < img_height; ++y)
    {
        for (uint16_t x = 0; x < img_width; x += 8)
        {
            uint16_t* img_start = img + 3 * (img_width * y + x);

            // Get 8 pixels to use. This is done since 8 pixels
            // means we can create a full byte in the color channel iamge
            uint16_t* p0 = img_start;
            uint16_t r0 = p0[0];
            uint16_t g0 = p0[1];
            uint16_t b0 = p0[2];
            uint16_t* p1 = img_start + 3;
            uint16_t r1 = p1[0];
            uint16_t g1 = p1[1];
            uint16_t b1 = p1[2];
            uint16_t* p2 = img_start + 6;
            uint16_t r2 = p2[0];
            uint16_t g2 = p2[1];
            uint16_t b2 = p2[2];
            uint16_t* p3 = img_start + 9;
            uint16_t r3 = p3[0];
            uint16_t g3 = p3[1];
            uint16_t b3 = p3[2];
            uint16_t* p4 = img_start + 12;
            uint16_t r4 = p4[0];
            uint16_t g4 = p4[1];
            uint16_t b4 = p4[2];
            uint16_t* p5 = img_start + 15;
            uint16_t r5 = p5[0];
            uint16_t g5 = p5[1];
            uint16_t b5 = p5[2];
            uint16_t* p6 = img_start + 18;
            uint16_t r6 = p6[0];
            uint16_t g6 = p6[1];
            uint16_t b6 = p6[2];
            uint16_t* p7 = img_start + 21;
            uint16_t r7 = p7[0];
            uint16_t g7 = p7[1];
            uint16_t b7 = p7[2];

            for (uint8_t c = 0; c < color_depth; ++c) {
                uint32_t plane_offset = (y * img_width + x) / 8; 

                bitplanes_r[c][plane_offset] = (((r0 >> c) & 1) << 0) | (((r1 >> c) & 1) << 1) | (((r2 >> c) & 1) << 2)
                                            | (((r3 >> c) & 1) << 3) | (((r4 >> c) & 1) << 4) | (((r5 >> c) & 1) << 5)
                                            | (((r6 >> c) & 1) << 6) | (((r7 >> c) & 1) << 7);

                bitplanes_g[c][plane_offset] = (((g0 >> c) & 1) << 0) | (((g1 >> c) & 1) << 1) | (((g2 >> c) & 1) << 2)
                                            | (((g3 >> c) & 1) << 3) | (((g4 >> c) & 1) << 4) | (((g5 >> c) & 1) << 5)
                                            | (((g6 >> c) & 1) << 6) | (((g7 >> c) & 1) << 7);
            
                bitplanes_b[c][plane_offset] = (((b0 >> c) & 1) << 0) | (((b1 >> c) & 1) << 1) | (((b2 >> c) & 1) << 2)
                                            | (((b3 >> c) & 1) << 3) | (((b4 >> c) & 1) << 4) | (((b5 >> c) & 1) << 5)
                                            | (((b6 >> c) & 1) << 6) | (((b7 >> c) & 1) << 7);
            }

        }
    }
}

这做了很多工作,理论上可以完全并行化。但我似乎无法弄清楚如何将其映射到 SIMD 内部函数。是否有可能使用内部函数来加速这一点?还是这个问题要专业化?

任何帮助都是值得赞赏的。

C 图像处理 SIMD SSE AVX

评论

2赞 Peter Cordes 10/26/2023
pmovmskb是从每个字节元素中提取位并打包到位掩码中的常用方法。或者从每个 32 位元素中获取一点。如果将一些未对齐的加载结果混合在一起,则可以从“绿色”像素分量的顶部获得一个包含 15 或 16 字节数据的向量,并可以将它们按正确的顺序排列。+ 序列可以提取连续位平面的块。(以不同的方式混合以获取其他组件的顶部字节和低字节。这需要大量的洗牌/混合;其他策略可能会更好movmskpspshufbpaddb same,samepmovmskb
0赞 Peter Cordes 10/26/2023
uint8_t** bitplanes_r- 指针到指针?为什么不是 2D 数组,或者在平面数组中手动进行 2D 索引?(或每个通道的平面阵列)。可能无关紧要,因为您总是希望从同一位平面累积至少 8 个连续位来存储在某个地方,但应该将一些标量开销转换为循环中的指针增量。
0赞 John Smith 10/26/2023
@PeterCordes 这也是一种可能性。它们是连续分配的,所以我怀疑额外的指针取消引用是否太重要了。
2赞 Peter Cordes 10/26/2023
它们是连续分配的,所以我怀疑额外的指针取消引用是否太重要了。- 额外的负载使用延迟通常很糟糕,使无序的 exec 更加努力地工作。添加一个环形不变常量以在同一组件内的位平面上跨步应使内部循环更便宜,并且可能使用更少的整数寄存器。
0赞 Peter Cordes 10/26/2023
另一种随机馈送策略可能是对每个 16 字节输入向量(或使用 AVX2 从中间拆分的 32 字节负载的 16 字节半部分),将 2 个红色高半部分、2 个红色低半部分等分组(从 12 字节 = 2 个像素)以从部分重叠的未对齐负载上的另一个结果馈送。或者也许从两个向量开始,每个向量都从 12 字节像素对的开头开始,然后做?这些都不是好事;AVX-512 可以通过车道交叉洗牌和面罩测试以及面罩拆包说明来更好地做到这一点。pmovmskbpshufbpunpcklwdpshufbunpcklbwpshufb

答: 暂无答案