【问题标题】:Vectorizing image processing矢量化图像处理
【发布时间】:2026-02-10 00:55:02
【问题描述】:

我有一些 C 代码在带有 ARM-Cortex-A9 的开发板上运行,它执行我需要加速的图像处理。我所拥有的是读取 8 个 RGB 像素的代码,其中每种颜色都表示为 uint8_t。这些像素需要进行颜色校正,因此使用查找表来查找像素的单个通道的颜色校正值。颜色校正通道使用 16 位类型,但实际使用的位可能因output_color_depth 参数而异。

在此预处理步骤之后,我需要提取每个像素的每个通道的每个有效位并将其存储在输出缓冲区中。

下面的代码是有问题的函数:


struct Pixel {
    uint8_t r;
    uint8_t g;
    uint8_t b;
};


static const uint16_t colorLookup[256] = { ... };

void postProcessImage(const struct Pixel* img, const uint16_t imgWidth, const uint16_t imgHeight,
                      uint8_t** output, const uint8_t output_color_depth)
{
    const uint8_t input_color_depth = 8;

    for (uint16_t y = 0; y < imgHeight; ++y)
    {
        const uint16_t top_offset = y * imgWidth;

        for (uint16_t x = 0; x < imgWidth; x += 8)
        {
            const uint16_t offset = top_offset + x;

            // Get 8 pixels to use. This is done since 8 pixels
            // means 24 color channels which can fit exactly into
            // 3 bytes
            const uint16_t r0 = colorLookup[img[offset + 0].r];
            const uint16_t g0 = colorLookup[img[offset + 0].g];
            const uint16_t b0 = colorLookup[img[offset + 0].b];

            const uint16_t r1 = colorLookup[img[offset + 1].r];
            const uint16_t g1 = colorLookup[img[offset + 1].g];
            const uint16_t b1 = colorLookup[img[offset + 1].b];

            const uint16_t r2 = colorLookup[img[offset + 2].r];
            const uint16_t g2 = colorLookup[img[offset + 2].g];
            const uint16_t b2 = colorLookup[img[offset + 2].b];

            const uint16_t r3 = colorLookup[img[offset + 3].r];
            const uint16_t g3 = colorLookup[img[offset + 3].g];
            const uint16_t b3 = colorLookup[img[offset + 3].b];

            const uint16_t r4 = colorLookup[img[offset + 4].r];
            const uint16_t g4 = colorLookup[img[offset + 4].g];
            const uint16_t b4 = colorLookup[img[offset + 4].b];

            const uint16_t r5 = colorLookup[img[offset + 5].r];
            const uint16_t g5 = colorLookup[img[offset + 5].g];
            const uint16_t b5 = colorLookup[img[offset + 5].b];

            const uint16_t r6 = colorLookup[img[offset + 6].r];
            const uint16_t g6 = colorLookup[img[offset + 6].g];
            const uint16_t b6 = colorLookup[img[offset + 6].b];

            const uint16_t r7 = colorLookup[img[offset + 7].r];
            const uint16_t g7 = colorLookup[img[offset + 7].g];
            const uint16_t b7 = colorLookup[img[offset + 7].b];

            for (uint8_t c = 0; c < output_color_depth; ++c)
            {
                // For each significant bit we create the resulting byte
                // and store it into the output buffer.
                output[c][offset + 0] = (((g2 >> c) & 1) << 7) | (((r2 >> c) & 1) << 6)
                                      | (((b1 >> c) & 1) << 5) | (((g1 >> c) & 1) << 4)
                                      | (((r1 >> c) & 1) << 3) | (((b0 >> c) & 1) << 2)
                                      | (((g0 >> c) & 1) << 1) |  ((r0 >> c) & 1);

                output[c][offset + 1] = (((r5 >> c) & 1) << 7) | (((b4 >> c) & 1) << 6)
                                      | (((g4 >> c) & 1) << 5) | (((r4 >> c) & 1) << 4)
                                      | (((b3 >> c) & 1) << 3) | (((g3 >> c) & 1) << 2)
                                      | (((r3 >> c) & 1) << 1) |  ((b2 >> c) & 1);

                output[c][offset + 2] = (((b7 >> c) & 1) << 7) | (((g7 >> c) & 1) << 6)
                                      | (((r7 >> c) & 1) << 5) | (((b6 >> c) & 1) << 4)
                                      | (((g6 >> c) & 1) << 3) | (((r6 >> c) & 1) << 2)
                                      | (((b5 >> c) & 1) << 1) |  ((g5 >> c) & 1);
            }
        }
    }
}

现在这个函数执行太慢了,我需要优化它。我正在考虑使用 NEON 指令来优化它。我很难在网上找到示例,但直观地说,这是我认为应该能够被矢量化的东西。有人可以给我一些指示,我怎样才能实现这样的目标?我也愿意接受有关如何优化此代码的其他建议!

感谢任何帮助。

【问题讨论】:

  • 强制性问题:您是否在启用编译器优化的情况下编译代码?如果没有,请按照步骤 1 进行操作。
  • 优化 #1:使用单个数组,而不是指向数组的指针数组。索引为img[y*imgWidth+x]。因为您遍历整个数组,您可以简单地将索引为img[index]index 从 0 运行到 imgWidth*imgHeight。不仅索引更便宜,而且您将拥有更少的内存碎片和更好的缓存局部性。
  • 为什么要连续分配给output[c][offset] 3次?
  • 请显示您使用的编译/链接命令以及 GCC 版本。
  • 尝试添加-march=native

标签: c++ c image-processing optimization simd


【解决方案1】:

我可以提示您可以改进的地方。最大的改进是改变问题并避免奇怪的位摆弄(看起来你将RGB图像转换为位平面图像?)。

可能一种方法是一步完成所有事情(我相信这会类似于上述 Pete D. 的答案)。

如果您坚持使用两部分解决方案(首先查找,然后重新分配位): 第一部分你不能真正优化太多,因为你需要为每个输入字节加载 16 位。 您可以做的是将查找表更改为 32 位,因为 Cortex-A9 是一个奇怪的 CPU:32 位加载需要 1 个周期,但其他任何内容(8 位/16 位)需要 2 个周期。 此外,如果你能做到:重新安排 RGB 图像的加载,使其也使用 32 位加载(4 个字节)并一个接一个地提取 4 次 8 位。 或者简而言之:查找部分的限制因素只是负载,因此您必须减少这些,最好使用尽可能多的 32 位(对齐)负载。

如果您的查找表值只需几个算术运算即可计算,那么避免查找并进行矢量化计算(8 位输入 --> 16 位输出)会更快。

第二部分: 如果您深入研究 NEON,您可能可以将查找中的 16 位值排列到 NEON 向量中,并一次对多个值进行并行处理。 也许并行化为 3(对于循环中的三个组件)。 或者以可以避免大量位摆弄的方式重新排列查找值(在 32 位查找表中存储更多,因为无论如何 32 位对于 Cortex-A9 来说更好)。

一般避免使用 8 位或 16 位循环计数器。使用本机大小(32 位),如果编译器不够聪明(通常不是这样),这有时可以避免开销。

【讨论】:

    【解决方案2】:

    我不知道 NEON,但这可以在标准 C++ 中进行优化,方法是使用 64 位无符号整数作为 8 * 8 位字节的等效值,并通过将其设为 8 个字符的联合来访问它。我认为 NEON 有 64 位和 128 位向量。 以下是给出解决方案要点的伪代码。 OP可以解决 详情。

    OP 代码可以通过观察它做同样的事情 3 次来简化。将输入视为宽 * 高 * 3 字节的流,并分块为 8 字节的块。

    返回 8 * 8 位的 64 位向量,而不是返回 16 位的查找表 (LUT)。该问题需要八个块的每个字节的第一位,然后是同一块的每个字节的第二位,最多 8 个颜色深度。深度不能超过 8,因为输入只有 8 位。所以 LUT 返回 8 个深度所需的 8 个字节。

    如果 colorLookup[ImageByte] == 11111111 那么 LUT[ImageBtye] ==

    1000000010000000100000001000000010000000100000001000000010000000

    如果 colorLookup[ImageByte] == 01111110 那么 LUT[ImageBtye] ==

    0000000010000000100000001000000010000000100000001000000000000000

    因此,对于每个 8 字节的块,循环遍历该块并进行 OR 和移位(或展开):

    union {
      unsigned long long   Sum;     // 64 bit
      char                 Byte[8]; // Need to address each byte of Sum later
    };
    
    for ( int I=0; I<8; ++I ) {
      Sum |= LUT[Stream[S]] >> I;
      ++S;
    }
    

    并行计算 8 个深度。现在只需剥离深度数 从联合(或展开),或使用 NEON 指令直接寻址 向量寄存器的 8 个字节。

    for ( int c=0; c<depth; ++c ) {
      output[?+c][?] = Byte[c];     
    }
    

    原始代码大约有。每 24 个字节有 24 个 LUT,对于每个深度,每 24 个字节有 24 个 > 和 24 个 |。

    上面有大约。每 24 个字节有 24 个 LUT、24 个 | 和 24 个 >>。深度是并行计算的。可能会有更少的寄存器溢出。

    还可以添加另一个LUT,其中LUT1 == LUT >> 1,那么“展开”可能是:-

      Sum0 = LUT[Stream[S  ]] | LUT1[Stream[S+1]];
      Sum1 = LUT[Stream[S+2]] | LUT1[Stream[S+3]];
      SumA = Sum0 | Sum1 >> 2;
      Sum0 = LUT[Stream[S+4]] | LUT1[Stream[S+5]];
      Sum1 = LUT[Stream[S+6]] | LUT1[Stream[S+7]];
      SumB = Sum0 | Sum1 >> 2;
      Sum  = SumA | SumB >> 4;
    

    可以添加 LUT2 和 3,但 ARM 只有 8K 或 16K L1 缓存(据我所知),并且 每个 LUT 为 2K。

    【讨论】:

    • @MichaelNastenko 是的。见最后一句。 LUT是2K。 ARM 有 8K L1 缓存。取决于 OP 图像的大小。图像可能是 Megs,因此考虑到 OP 有一个 512 字节的表,一两个 2K LUT 相比之下没什么。
    • @MichaelNastenko 如果图像输入或 colorLookup 绑定,则否。如果计算界限会更快 * output_color_depth。如果是前者,它是一个硬件或规范,约束。