【问题标题】:How can I make this loop run faster?我怎样才能让这个循环运行得更快?
【发布时间】:2021-02-17 23:17:03
【问题描述】:

我正在使用此代码查找热图像中温度最高的像素和像素的坐标。

void _findMax(uint16_t *image, int sz, sPixelData *returnPixel)
{
    int temp = 0;

    for (int i = sz; i > 0; i--)
    {
        if (returnPixel->temperature < *image)
        {
            returnPixel->temperature = *image;
            temp = i;
        }
        image++;
    }

    returnPixel->x_location = temp % IMAGE_HORIZONTAL_SIZE;
    returnPixel->y_location = temp / IMAGE_HORIZONTAL_SIZE;
}

图像大小为 640x480,运行此函数大约需要 35 毫秒,这对于我需要它来说太慢了(理想情况下不到 10 毫秒)。

这是在运行 Linux 的 ARM A9 处理器上执行的。

我使用的编译器是 ARM v8 32-Bit Linux gcc 编译器。

我正在使用优化 -O3 和以下编译选项:-march=armv7-a+neon -mcpu=cortex-a9 -mfpu=neon-fp16 -ftree-vectorize。

这是编译器的输出:

000127f4 <_findMax>:
    for(int i = sz; i > 0; i--)
   127f4:   e3510000    cmp r1, #0
{
   127f8:   e52de004    push    {lr}        ; (str lr, [sp, #-4]!)
    for(int i = sz; i > 0; i--)
   127fc:   da000014    ble 12854 <_findMax+0x60>
   12800:   e1d2c0b0    ldrh    ip, [r2]
   12804:   e2400002    sub r0, r0, #2
    int temp = 0;
   12808:   e3a0e000    mov lr, #0
        if(returnPixel->temperature < *image)
   1280c:   e1f030b2    ldrh    r3, [r0, #2]!
   12810:   e153000c    cmp r3, ip
            returnPixel->temperature = *image;
   12814:   81a0c003    movhi   ip, r3
   12818:   81a0e001    movhi   lr, r1
   1281c:   81c230b0    strhhi  r3, [r2]
    for(int i = sz; i > 0; i--)
   12820:   e2511001    subs    r1, r1, #1
   12824:   1afffff8    bne 1280c <_findMax+0x18>
   12828:   e30c3ccd    movw    r3, #52429  ; 0xcccd
   1282c:   e34c3ccc    movt    r3, #52428  ; 0xcccc
   12830:   e0831e93    umull   r1, r3, r3, lr
   12834:   e1a034a3    lsr r3, r3, #9
   12838:   e0831103    add r1, r3, r3, lsl #2
   1283c:   e6ff3073    uxth    r3, r3
   12840:   e04ee381    sub lr, lr, r1, lsl #7
   12844:   e6ffe07e    uxth    lr, lr
    returnPixel->x_location = temp % IMAGE_HORIZONTAL_SIZE;
   12848:   e1c2e0b4    strh    lr, [r2, #4]
    returnPixel->y_location = temp / IMAGE_HORIZONTAL_SIZE;
   1284c:   e1c230b6    strh    r3, [r2, #6]
}
   12850:   e49df004    pop {pc}        ; (ldr pc, [sp], #4)
    for(int i = sz; i > 0; i--)
   12854:   e3a03000    mov r3, #0
   12858:   e1a0e003    mov lr, r3
   1285c:   eafffff9    b   12848 <_findMax+0x54>

为了清楚起见,在 cmets 之后:

每个像素都是一个无符号的 16 位整数,image[0] 是坐标为 0,0 的像素,数组中最后一个像素的坐标为 639,479。

【问题讨论】:

  • 您可以每隔一行和一列检查一次,这将使其大​​约快 4 倍。您失去了精度,但它可能足以满足您的需求。
  • a) 稍微展开 for 循环。例如每次迭代处理几个像素。参见:duffs device -> en.wikipedia.org/wiki/Duff%27s_device, b) 使用一些梯度路径算法:en.wikipedia.org/wiki/Gradient_descent
  • 你如何读取像素?找到当时最热门的可能是微不足道的。
  • @pqans -我添加了选项 -funroll-loops 将其缩短到 13 毫秒
  • 是的,所以算法没有用,除非你能找到一种方法将图像分割成几个“热点方块”。如果你能做到这一点,那么梯度下降比当前算法的“蛮力”快得多。

标签: c gcc optimization arm neon


【解决方案1】:

这是在运行 Linux 的 ARM A9 处理器上执行的。

ARM Cortex-A9 支持 Neon。

考虑到这一点,目标应该是将 8 个值(128 位像素数据)加载到寄存器中,然后“与 8 个位置中每个位置的当前最大值进行比较”以获得掩码,然后使用掩码以及它的反转来掩盖“太小”的旧最大值和“太小”的新值;然后将结果或结果合并到“8 个位置中每个位置的当前最大值”中。

一旦对所有像素都完成了(使用循环);您想在“8 个地方的当前最大值”中找到最高值。

但是;要找到最热像素的位置(而不是它有多热),您需要将图像分割成图块(例如,可能是 8 像素宽和 8 像素高)。这使您可以找到最大值。每个瓷砖内的温度(使用霓虹灯);然后在最热的图块中找到像素。请注意,对于巨大的图像,这适用于“多层”方法 - 例如创建一个较小的图像,其中包含原始图像中每个图块的最大值;然后再次做同样的事情来创建一个更小的图像,其中包含每个“瓷砖组”中的最大值,然后......

用纯 C 语言进行这项工作意味着试图说服编译器进行自动向量化。替代方法是使用编译器内在函数或内联汇编。在任何这些情况下,使用 Neon 并行处理 8 个像素(没有任何分支)可以/应该显着提高性能(多少取决于 RAM 带宽)。

【讨论】:

  • 重新“获取掩码,然后使用掩码”:我相信 Cortex-A9 有vmax 指令可以更新最大值,而无需使用多条指令生成掩码并使用它。如果一个人正在做一个初始循环来找到最大值,它应该使用vmax
  • 重新“将图像分割成小块”:将图像二维平铺有什么好处?一维搜索对缓存或内存访问没有障碍。如果关心的是重新加载在第一个循环中被驱逐的行的成本,可以一维地剥离两个循环(找到最大值,然后找到它的位置)(将组合循环限制为使用小于 S 字节的 @987654323 @,其中 S 是总缓存大小)。
  • @EricPostpischil:“平铺”可以是任何矩形(包括“1 高,X 宽”矩形)。对于“多层”方法,方形瓷砖(例如 8 高,8 宽)更容易可视化(因为原始图像的一个位置的热点将在较小的图像中位于同一位置);这使原始询问者更容易理解。当然,一旦最初的提问者理解了基本概念并开始尝试实施它,他们可能会自己弄清楚什么是最优化的“图块”,因此“过早地去优化学习过程”毫无意义。跨度>
【解决方案2】:

您应该尽量减少内存访问,尤其是在循环中。
每个*-&gt; 都可能导致不必要的内存访问,从而导致严重的性能下降。
局部变量是你最好的朋友:

void _findMax(uint16_t *image, int sz, sPixelData *returnPixel)
{
    int temp = 0;
    uint16_t temperature = returnPixel->temperature;
    uint16_t pixel;

    for (int i = sz; i > 0; i--)
    {
        pixel = *image++;
        if (temperature < pixel)
        {
            temperature = pixel;
            temp = i;
        }
    }

    returnPixel->temperature = temperature;
    returnPixel->x_location = temp % IMAGE_HORIZONTAL_SIZE;
    returnPixel->y_location = temp / IMAGE_HORIZONTAL_SIZE;
}

以下是如何利用霓虹灯对其进行优化:

#include <stdint.h>
#include <arm_neon.h>
#include <assert.h>

static inline void findMax128_neon(uint16_t *pDst, uint16x8_t *pImage)
{
    uint16x8_t in0, in1, in2, in3, in4, in5, in6, in7, in8, in9, in10, in11, in12, in13, in14, in15;
    uint16x4_t dmax;
    in0 = vld1q_u16(pImage++);
    in1 = vld1q_u16(pImage++);
    in2 = vld1q_u16(pImage++);
    in3 = vld1q_u16(pImage++);
    in4 = vld1q_u16(pImage++);
    in5 = vld1q_u16(pImage++);
    in6 = vld1q_u16(pImage++);
    in7 = vld1q_u16(pImage++);
    in8 = vld1q_u16(pImage++);
    in9 = vld1q_u16(pImage++);
    in10 = vld1q_u16(pImage++);
    in11 = vld1q_u16(pImage++);
    in12 = vld1q_u16(pImage++);
    in13 = vld1q_u16(pImage++);
    in14 = vld1q_u16(pImage++);
    in15 = vld1q_u16(pImage);

    in0 = vmaxq_u16(in1, in0);
    in2 = vmaxq_u16(in3, in2);
    in4 = vmaxq_u16(in5, in4);
    in6 = vmaxq_u16(in7, in6);
    in8 = vmaxq_u16(in9, in8);
    in10 = vmaxq_u16(in11, in10);
    in12 = vmaxq_u16(in13, in12);
    in14 = vmaxq_u16(in15, in14);

    in0 = vmaxq_u16(in2, in0);
    in4 = vmaxq_u16(in6, in4);
    in8 = vmaxq_u16(in10, in8);
    in12 = vmaxq_u16(in14, in12);

    in0 = vmaxq_u16(in4, in0);
    in8 = vmaxq_u16(in12, in8);

    in0 = vmaxq_u16(in8, in0);

    dmax = vmax_u16(vget_high_u16(in0), vget_low_u16(in0));

    dmax = vpmax_u16(dmax, dmax);

    dmax = vpmax_u16(dmax, dmax);

    vst1_lane_u16(pDst, dmax, 0);
}

void _findMax_neon(uint16_t *image, int sz, sPixelData *returnPixel)
{
    assert((sz % 128) == 0);
    const uint32_t nSector = sz/128;
    uint16_t max[nSector];
    uint32_t i, s, nMax;
    uint16_t *pImage;

    for (i = 0; i < nSector; ++i)
    {
        findMax128_neon(&max[i], (uint16x8_t  *) &image[i*128]);
    }

    s = 0;
    nMax = max[0];

    for (i = 1; i < nSector; ++i)
    {
        if (max[i] > nMax)
        {
            s = i;
            nMax = max[i];
        }
    }

    if (nMax < returnPixel->temperature)
    {
        returnPixel->x_location = 0;
        returnPixel->y_location = 0;
        return;
    }

    pImage = &image[s];
    i = 0;

    while(1) {
        if (*pImage++ == nMax) break;
        i += 1;
    }

    i += 128 * s;

    returnPixel->temperature = nMax;
    returnPixel->x_location = i % IMAGE_HORIZONTAL_SIZE;
    returnPixel->y_location = i / IMAGE_HORIZONTAL_SIZE;
}

请注意,上面的函数假定 sz 是 128 的倍数。
是的,它将在不到 10 毫秒的时间内运行。

【讨论】:

    【解决方案3】:

    这里的罪魁祸首是对最高“温度”的缓慢线性搜索。如果可能的话,我不太确定如何使用给定的信息改进搜索算法(你能提前对数据进行排序吗?),但你可以从这个开始:

    uint16_t max = 0;
    size_t found_index = 0;
    
    for(size_t i=0; i<sz; i++)
    {
      if(max < image[i])
      {
        max = image[i];
        found_index = sz - i - 1; // or whatever makes sense here for the algorithm
      }
    }
    
    returnPixel->temperature = max;
    returnPixel->x_location = found_index % IMAGE_HORIZONTAL_SIZE;
    returnPixel->y_location = found_index / IMAGE_HORIZONTAL_SIZE;
    

    由于从上到下的迭代顺序,并且不会在循环中间触及不相关的内存returnPixel,这可能会带来非常轻微的性能提升。 max 应该被存储在一个寄存器中,如果幸运的话,你可能会获得更好的整体缓存性能。不过,这与原始代码一样带有一个分支,所以它是一个小的改进。

    另一个微优化是将参数更改为const uint16_t* image - 这可能会提供更好的指针别名,以防returnPixel 恰好包含uint16_t。无论性能如何,image 都应该是 const,因为 const 正确性始终是一种很好的做法。

    如果您一次读取 image 32 位或 64 位,然后想出一种快速查找方法来找到该 32/64 位块中的最大图像,则可能会使用更多更模糊的优化技巧。

    【讨论】:

      【解决方案4】:

      如果您必须找到图像中最热的像素,并且图像数据本身没有结构,那么我认为您坚持遍历像素。如果是这样,您有多种不同的方法可以加快速度:

      • 如上所述,尝试循环展开和其他微优化技巧,这可能会为您提供所需的性能提升
      • 并行,将数组分成 N 个块,找到每个块的 MAX[N],然后找到 MAX[N] 值中的最大值。您在此处必须小心,因为设置并行进程可能需要比完成工作更长的时间。

      如果您试图找到的图像有一些结构、大量冷像素和热点(大于 1 像素),那么您可以使用其他技术。

      一种方法可能是将图像分成 N 个框,然后对每个框进行采样,然后最热的框(也可能在相邻的框中)中的最热像素就是您的结果。但是,这取决于它们是您可以依赖的图像的某种结构。

      【讨论】:

        【解决方案5】:

        汇编语言显示编译器在每次找到新的最大值时存储在returnPixel-&gt;temperature 中,指令为strhhi r3, [r2]。通过在本地对象中缓存最大值并仅在循环结束后更新 returnPixel-&gt;temperature 来消除这种不必要的存储:

        uint16_t maximum = returnPixel->temperature;
        for (int i = sz; i > 0; i--)
            {
                if (maximum < *image)
                {
                    maximum = *image;
                    temp = i;
                }
                image++;
            }
        returnPixel->temperature = maximum;
        

        这不太可能尽可能多地减少执行时间,但如果发生一些糟糕的缓存或内存交互,则可能会发生这种情况。这是一个非常简单的更改,因此请在继续使用其他答案中建议的 SIMD 矢量化之前尝试一下。

        关于向量化,有两种方法:

        • 使用vmax 指令迭代图像,以更新迄今为止在每个SIMD 通道中看到的最大值。然后合并车道以找到整体最大值。然后再次遍历图像,寻找任何车道中的最大值。 (我忘记了该架构有什么指令可以帮助测试比较是否在任何通道中产生正确。)
        • 遍历图像,维护三个寄存器:一个是迄今为止在每个通道中看到的最大值,一个是图像中的位置计数器,另一个是每个通道中每个通道当时的计数器值的记录看到了新的最大值。第一个可以用vmax 更新,如上。第二个可以用vadd 更新。第三个可以用vcmpvbit 更新。循环结束后,找出哪个车道的最大值,并从记录的那个车道的计数器中获取最大值的位置。

        根据必要指令的性能,混合方法可能更快:

        • 设置一些条带大小 S。将图像划分为该大小的条带。对于图像中的每个条带,找到最大值(使用上述快速vmax 循环)。如果最大值大于以前的条带中看到的,请记住它和当前条带编号。处理完整个图像后,任务已简化为在特定条带中找到最大值的位置。为此,请使用上面第一种方法中的第二个循环。 (对于大图像,可能会进一步细化,可能会在找到确切位置之前使用更短的条带来细化位置,具体取决于缓存行为和其他因素。)

        【讨论】:

          【解决方案6】:

          在我看来,您无法改进该算法,因为任何数组元素都可以保持最大值,因此您至少需要对数据进行一次传递,我不相信您可以在不使用多线程的情况下改进这一点。您可以启动多个线程(与您可能拥有的内核/处理器一样多)并为每个线程提供图像的一个子集。完成后,您将拥有与您启动的线程数一样多的局部最大值。只需对这些 vaue 进行第二次传递即可获得最大的总值,您就完成了。但请考虑创建线程、为其分配堆栈内存和调度的额外工作量,因为如果值的数量低于在单个线程中运行所有线程的工作量,则这些工作量可能会更高。如果您在某处有一个线程池来提供准备运行的线程,而这是您可以继续使用的东西,那么您可能能够在 N 分之一的时间内完成在一个处理器中运行所有循环(其中N 是你机器上的核心数)

          注意: 使用 2 的幂次方的维度可以省去计算商和余数的工作,方法是使用位移位和位掩码解决除法问题。你只在你的函数中使用它一次,但无论如何它是一种改进。

          【讨论】:

            猜你喜欢
            • 2017-12-20
            • 2013-10-21
            • 2021-07-09
            • 2020-02-20
            • 1970-01-01
            • 2021-11-12
            • 2023-03-18
            • 2011-04-22
            • 1970-01-01
            相关资源
            最近更新 更多