问题描述
我正在使用此代码查找热图像中温度最高的像素以及该像素的坐标。
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位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,lr
12834: e1a034a3 lsr r3,#9
12838: e0831103 add r1,lsl #2
1283c: e6ff3073 uxth r3,r3
12840: e04ee381 sub lr,lr,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,#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>
为清楚起见,在评论后:
每个像素是一个无符号的16位整数,image [0]将是坐标为0,0的像素,数组的最后一个像素将是639,479。
解决方法
这是在运行Linux的ARM A9处理器上执行的。
ARM Cortex-A9支持Neon。
考虑到这一点,目标应该是将8个值(128位像素数据)加载到寄存器中,然后执行“与8个位置中的每个位置的当前最大值进行比较”以获得掩码,然后使用掩码反之则掩盖了“太小”的旧最大值和“太小”的新值;然后对结果进行“或”运算,以将新的较高值合并为“ 8个位置中的每个位置的当前最大值”。
针对所有像素完成一次操作(使用循环);您想要在“ 8个位置中的每个位置的当前最大值”中找到最大值。
但是;要找到最热像素的位置(而不是仅发现它的热度),您需要将图像拆分为图块(例如,可能宽8像素,高8像素)。这使您可以找到最大值。每个砖内的温度(使用氖);然后在最热的图块中找到像素。请注意,对于大图像,这适合“多层”方法-例如创建一个较小的图像,其中包含原始图像中每个图块的最大值;然后再次执行相同的操作,以创建甚至更小的图像,其中包含每个“图块组”中的最大值,然后...
使用纯C语言进行这项工作意味着试图说服编译器自动向量化。替代方法是使用编译器内部函数或内联汇编。在任何一种情况下,使用Neon并行处理8个像素(无分支)可以/应该显着提高性能(多少取决于RAM带宽)。
,您应该最大程度地减少内存访问,尤其是在循环中。
每个*
或->
都可能导致不必要的内存访问,从而严重影响性能。
局部变量是您最好的朋友:
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,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毫秒的时间内运行。
这里的罪魁祸首是对最高“温度”的缓慢线性搜索。我不太确定如何使用给出的信息改进搜索算法(如果可以的话)(可以预先对数据进行排序吗?),但是您可以从以下开始:
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位块中最大的图像,则可能还有更多晦涩的优化技巧。
如果您必须在图像中找到最热的像素,并且图像数据本身没有任何结构,那么我认为您不愿对像素进行迭代。如果是这样,您可以通过多种方法来加快速度:
- 按照上面的建议,尝试展开循环和其他微优化技巧,这可能会给您带来所需的性能提升
- 并行执行,将数组拆分为N个块,并为每个块找到MAX [N],然后找到MAX [N]值中的最大值。您在这里必须小心,因为设置并行流程可能比完成工作要花费更长的时间。
如果图像中存在某种结构,您试图找到很多冷像素和一个热点(大于1个像素),那么您可以使用其他技术。
一种方法可能是将图像分成N个框,然后对每个框进行采样,然后,最热的框(也可能是相邻框)中的最热像素将成为您的结果。但是,这取决于它们是您可以依赖的图像结构的一部分。
,汇编语言显示每次找到新的最大值时,编译器将使用指令returnPixel->temperature
将其存储在strhhi r3,[r2]
中。通过在本地对象中缓存最大值并在循环结束后仅更新returnPixel->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
更新。可以使用vcmp
和vbit
更新第三个。循环之后,找出哪个通道最大,并从记录的计数器中获得该通道最大的位置。
根据必要指令的执行情况,混合方法可能会更快:
- 设置一些条带大小S。将图像划分为该大小的条带。对于图像中的每个条带,找到最大值(使用上述快速
vmax
循环)。如果最大值大于先前试条中看到的最大值,请记住该最大值和当前试条编号。处理完整个图像后,任务已减少到在特定条带中找到最大值的位置。为此使用上面第一种方法的第二个循环。 (对于大图像,可能会进一步细化,可能会根据缓存行为和其他因素,在找到确切位置之前使用更小的条带大小来细化位置。)
我认为您无法改进该算法,因为任何数组元素都可以容纳最大值,因此您至少需要对数据进行一次遍历,我不相信您可以在不使用多线程的情况下进行改进。您可以启动多个线程(您可以拥有尽可能多的内核/处理器),并为每个线程分配一个映像的子集。完成后,您将拥有与启动的线程数一样多的本地最大值。只需对这些变量进行第二遍以获得最大的总价值,就可以完成。但是,请考虑创建线程,为线程分配堆栈内存和进行调度的额外工作量,因为如果值的数量少于在单个线程中全部运行的工作量,则可能会更高。如果您在某个地方有一个线程池来提供可以运行的线程,而您可以继续这样做,那么您大概可以在十分之一的时间内完成,以在一个处理器中运行所有循环(其中N是您在计算机上拥有的内核数)
注意:使用2的幂的维将通过解决带位移和位掩码的除法问题而节省您计算商和余数的工作。您只能在函数中使用它一次,但是无论如何它都是一种改进。