【问题标题】:Is it possible to optimize this nested loop?是否可以优化这个嵌套循环?
【发布时间】:2020-02-07 07:16:35
【问题描述】:

我正在做一个项目,我想将给定的视频输入流转换为块部分(以便硬件编解码器可以使用它)。该项目在运行 200Mhz 时钟的 STM32 微控制器上运行。

接收到的输入是一个 YCbCr 4:2:2 渐进流,这基本上意味着输入流对于每一行都是这样的:

Size:      32 bit word    32 bit word    32 bit word    ...
Component: Cr Y1 Cb Y0    Cr Y1 Cb Y0    Cr Y1 Cb Y0    ...
Bits:      8  8  8  8     8  8  8  8     8  8  8  8     ...

此流需要转换为硬件编解码器使用的块格式。编解码器以特定顺序接受字节数组。目前,我正在使用查找表并写入空数组的图像帧的每 1/8 使用嵌套循环来执行此操作:

定义:

#define ROWS_PER_MCU                    8
#define WORDS_PER_MCU                   8
#define HORIZONTAL_MCU_PER_INPUTBUFFER  40
#define VERTICAL_MCU_PER_INPUTBUFFER    8

全局变量声明如下:

typedef struct jpegInputbufferLUT
{
    uint8_t JPEG_Y_MCU_LUT[256];
    uint8_t JPEG_Cb_MCU_422_LUT[256];
    uint8_t JPEG_Cr_MCU_422_LUT[256];
}jpegIndexLUT;

jpegIndexLUT jpegInputLUT;

uint8_t jpegInBuffer[81920];
uint32_t rawBuffer[20480];

查找表是这样创建的:

void JPEG_Init_MCU_LUT(void)
{
    uint32_t offset;

    /*Y LUT */
    for(uint32_t i = 0; i < 16; i++)
    {
        for(j = 0; j < 16; j++)
        {
            offset =  j + (i*8);
            if((j>=8) && (i>=8)) offset+= 120;
            else  if((j>=8) && (i<8)) offset+= 56;
            else  if((j<8) && (i>=8)) offset+= 64;

            jpegInputLUT.JPEG_Y_MCU_LUT[i*16 + j] = offset;
        }
    }

    /*Cb Cr LUT*/
    for(uint32_t i = 0; i < 16; i++)
    {
        for(j = 0; j < 16; j++)
        {
            offset = i*16 + j;

            jpegInputLUT.JPEG_Cb_MCU_422_LUT[offset] = (j/2) + (i*8) + 128;

            jpegInputLUT.JPEG_Cr_MCU_422_LUT[offset] = (j/2) + (i*8) + 192;
        }
    }
}

转换代码:

/* Initialize variables for array conversion */
uint32_t currentMCU = 0;
uint32_t lutOffset = 0;
uint32_t inputOffset = 0;
uint32_t verticalOffset = 0;

/* Convert X rows into MCU blocks for JPEG encoding */
for(uint8_t k = 0; k < VERTICAL_MCU_PER_INPUTBUFFER; k++)
{
    for(uint8_t n = 0; n < HORIZONTAL_MCU_PER_INPUTBUFFER; n++)
    {
        inputOffset = verticalOffset + (n * 8);
        lutOffset = 0;

        for(uint8_t i = 0; i < ROWS_PER_MCU; i++)
        {
            for(uint8_t j = 0; j < WORDS_PER_MCU; j++)
            {
                /* Mask 32 bit according to DCMI input format */
                uint32_t rawBufferAddress = inputOffset+j; // Calculate rawBuffer address here so it only has to be calculated once
                jpegInBuffer[jpegInputLUT.JPEG_Y_MCU_LUT[lutOffset] + currentMCU]       = (rawBuffer[rawBufferAddress] & 0x7F);
                jpegInBuffer[jpegInputLUT.JPEG_Cb_MCU_422_LUT[lutOffset] + currentMCU]  = ((rawBuffer[rawBufferAddress] >> 7) & 0x7F);
                jpegInBuffer[jpegInputLUT.JPEG_Cr_MCU_422_LUT[lutOffset] + currentMCU]  = ((rawBuffer[rawBufferAddress] >> 23) & 0x7F);
                jpegInBuffer[jpegInputLUT.JPEG_Y_MCU_LUT[lutOffset+1] + currentMCU]     = ((rawBuffer[rawBufferAddress] >> 16) & 0x7F);

                lutOffset+=2;
            }
            inputOffset += 320;
        }
        currentMCU += 256;
    }
    verticalOffset += 2240;
}

这个转换目前需要我大约 8 毫秒,并且需要完成 8 次。目前这几乎占用了我所有可用的执行时间,因为我试图从我的系统中获得 15 fps。

有没有办法加快速度?我在想也许对输入数组进行排序而不是仅仅写入一个新的缓冲区,但是交换一个数组中的 2 个元素会比将值复制到另一个数组中更快的执行时间吗?

很想听听您对此的想法/想法,

提前致谢!

【问题讨论】:

  • 您有一个想法,尝试它,衡量它 - 没有人可以明确告诉您它是否会更快。因此,您真正要寻找的是其他建议。你在什么编译器优化设置下达到了 8ms?确实是什么编译器?这两件事都可能产生重大影响。
  • 当我看到像rawBuffer[rawBufferAddress] 这样的东西重复了很多次时,这是设置变量并多次使用该变量的候选项。您是否正在编译完全优化的构建?与往常一样,检查代码的汇编输出,看看有哪些可以简化或改进的地方。
  • @tadman : 是的,但它也是优化器会发现并为您完成工作的那种不变表达式。
  • @tadman : STM32 是 ARM-Cortex,因此受到 GCC 的支持,而 ARMCC - 版本 6 基于 CLANG,v5 是 ARM 专有的 - 都可以胜任。
  • 8 ms * 200M = 1.6M 个周期,除以 (8*8*8*40) 得到 78 个周期。似乎太慢了。检查生产的组件类型。

标签: c optimization embedded microcontroller stm32


【解决方案1】:
  1. 您的程序似乎比 STM32 的预期运行速度慢。您可能需要查看生成的程序集、编译器优化设置、MCU 频率是否正确、内存是否太慢等。我们没有足够的信息来给出明确的答案。您的代码似乎花费了 8 ms * 200M / (8*8*8*40) = 78 个周期来进行每个内部循环迭代。作为参考,一个 stm32f723 只需要大约 15 个周期,一个 stm32f103 大约需要 28 个周期(代码被调整为在后一种情况下访问更小的数组)。

  2. 不需要 LUT 表,因为它的内容非常规则。读取 LUT 值会增加更多的内存读取,这可能是一个重要的贡献。如果我正确获取了您的 LUT 生成代码,它会在内部循环中生成以下数字:

Y1  Cb  Cr  Y2
0   128 192 1
2   129 193 3
4   130 194 5
6   131 195 7
64  132 196 65
66  133 197 67
68  134 198 69
70  135 199 71
8   136 200 9
etc

第二列和第三列只是连续的数字。第四列等于第一列加一。第一个数字需要翻转一下。你可以试试下面的代码(请检查是否正确):

uint32_t lutOffset = 0;
for(uint8_t i = 0; i < ROWS_PER_MCU; i++)
{
    for(uint8_t j = 0; j < WORDS_PER_MCU; j++)
    {
        uint32_t rawBufferAddress = (inputOffset+j) /* % 2048 */;
#if 0
        unsigned y_lut1 = jpegInputLUT.JPEG_Y_MCU_LUT[lutOffset];
        unsigned Cb_lut = jpegInputLUT.JPEG_Cb_MCU_422_LUT[lutOffset];
        unsigned Cr_lut = jpegInputLUT.JPEG_Cr_MCU_422_LUT[lutOffset];
        unsigned y_lut2 = jpegInputLUT.JPEG_Y_MCU_LUT[lutOffset+1];
#else
        unsigned y_lut1 = lutOffset | (j / 4) << 6 | (j % 4) << 1;
        unsigned Cb_lut = 128 + lutOffset + j;
        unsigned Cr_lut = 192 + lutOffset + j;
        unsigned y_lut2 = y_lut1 + 1;
#endif
        jpegInBuffer[y_lut1 + currentMCU] = (rawBuffer[rawBufferAddress] & 0x7F);
        jpegInBuffer[Cb_lut + currentMCU] = ((rawBuffer[rawBufferAddress] >> 7) & 0x7F);
        jpegInBuffer[Cr_lut + currentMCU] = ((rawBuffer[rawBufferAddress] >> 23) & 0x7F);
        jpegInBuffer[y_lut2 + currentMCU] = ((rawBuffer[rawBufferAddress] >> 16) & 0x7F);
    }
    lutOffset += 8;
    inputOffset += 320;
}

这个版本在我的 stm32f103 上每次迭代大约需要 20 个周期,即使在 72 MHz 下也不到 6 ms。

UPD。另一种选择是使用一个小型查找表而不是位计算:

static const unsigned x[8] = { 0, 2, 4, 6, 64, 66, 68, 70 };

//  unsigned y_lut1 = lutOffset | (j / 4) << 6 | (j % 4) << 1;
  unsigned y_lut1 = lutOffset + x[j];

这将内循环时序提高到 18 (f103) / 7.5 (f723) 周期。出于某种原因,为 F723 优化此表达式效果不佳。我希望这些选项能给出相同的结果,因为内部循环已展开,但谁知道呢。

  1. 作为额外的优化,可能没有必要,输出值可以组合成 32 位字并一次写入一个字。这似乎是可能的,因为 LUT 值以四个连续的块的形式出现。为此,可以将内部循环转换为 2 x 4 次迭代的嵌套循环。最内层循环的每 4 次迭代将为 Cb 生成一个 uint32_t,为 Cr 生成一个 uint32_t,为 Y 生成两个 uint32_t。但不值得这样做。

我用 SysTick 测量运行时间:

SysTick->LOAD = SysTick_LOAD_RELOAD_Msk;
SysTick->VAL = 0;
SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk | SysTick_CTRL_ENABLE_Msk;

volatile unsigned t0 = SysTick->VAL;
f();
volatile unsigned t1 = t0 - SysTick->VAL;

我有时也使用输出引脚,因为连接调试器不实用。严格来说,这两种方法都不能保证有效,因为编译器可能会跨测量点移动代码,但它已经按照我的预期工作(使用 gcc)。需要进行装配检查以确保没有任何可疑之处。

【讨论】:

  • 哇,感谢您的时间和努力。您对转换的理解是正确的。我已经尝试了您的代码,并且使用 gcc 和 -O3 的执行时间从 ~8ms 变为约 ~4.2ms。我已经使用 gpio 引脚和示波器测量了这一点。如果您不介意我问,您是如何衡量执行时间的?我也会看看我的汇编代码是如何产生的。
【解决方案2】:

可以在此处执行任何数量的微优化,以提供改进。有些人可能会在没有编译器优化的情况下在调试构建方面表现出改进,但在优化方面没有优势。甚至有可能一些“聪明”的技巧在调试中更快,如果非惯用的,可能会导致优化器生成更糟糕的代码,它可能会让你更喜欢清晰度而不是性能。

所有明显的微优化(例如循环展开编译器优化器)都可能会为您执行,而不会使代码复杂化或有引入错误的风险。

一个相当明显的改进(不管它是否更快)是改变:

        for( uint8_t j = 0; j < WORDS_PER_MCU; j++ )
        {
            /* Mask 32 bit according to DCMI input format */
            uint32_t rawBufferAddress = inputOffset+j; // Calculate rawBuffer address here so it only has to be calculated once
            ...

到:

        uint32_t rawBufferAddress = inputOffset ;
        for( uint8_t j = 0; j < WORDS_PER_MCU; rawBufferAddress++, j++)
        {
            /* Mask 32 bit according to DCMI input format */
            ...

您的“只需要计算一次”实际上是WORDS_PER_MCU 计算,增量可能比加法和赋值更快。在最坏的情况下也没有什么不同。

我同样建议将所有其他“循环结束增量,例如 lutOffset+=2 也移动到相应的 for 第三个表达式中。不是为了性能,而是为了清晰。

【讨论】:

  • 感谢您抽出宝贵时间查看我的帖子和您的回复。我不知道你可以在第三个表达式中更改多个变量,肯定会实现这个。
  • @JustQiix :您可以在for 中的所有三个分号分隔的表达式中使用任何有效表达式,并且由于C 具有"comma operator",您可以使用一系列子表达式。
  • @JustQiix :此外,如果您避免对自己施加的不必要的类型约束,可以这样做:for( int j = 0, rawBufferAddress = inputOffset; j &lt; WORDS_PER_MCU; rawBufferAddress++, j++) - 不是优化,而是将rawBufferAddress 的范围缩小到循环。
  • 感谢您的提示,我绝对可以看到这将如何使各个循环更加清晰。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-01-21
  • 2018-09-16
  • 1970-01-01
  • 2017-11-05
  • 1970-01-01
相关资源
最近更新 更多