我有一个具有时间关键型ISR的嵌入式应用程序,它需要迭代大小为256的数组(最好是1024,但256是最小值),并判断值是否与数组内容匹配.如果是这样,bool将设置为true.

The microcontroller is an NXP LPC4357, ARM Cortex M4 core, and the compiler is GCC. I already have combined optimisation level 2 (3 is slower) and placing the function in RAM instead of flash. I also use pointer arithmetic and a for loop, which does down-counting instead of up (checking if i!=0 is faster than checking if i<256). All in all, I end up with a duration of 12.5 µs which has to be reduced drastically to be feasible. This is the (pseudo) code I use now:

uint32_t i;
uint32_t *array_ptr = &theArray[0];
uint32_t compareVal = 0x1234ABCD;
bool validFlag = false;

for (i=256; i!=0; i--)
{
    if (compareVal == *array_ptr++)
    {
         validFlag = true;
         break;
     }
}

最快的方法是什么?允许使用内联汇编.其他"不那么优雅"的把戏也被允许.

推荐答案

在性能非常重要的情况下,与手动调优汇编语言相比,C编译器很可能不会生成最快的代码.我倾向于 Select 阻力最小的路径-对于像这样的小 routine ,我只需编写ASM代码,并且很清楚执行它需要多少个周期.您也许能够摆弄C代码并让编译器生成良好的输出,但这样做可能会浪费大量时间来调优输出.编译器(特别是来自Microsoft的编译器)在过go 几年中取得了长足的进步,但它们仍然没有您耳朵之间的编译器那么聪明,因为您正在处理您的特定情况,而不仅仅是一般情况.编译器可能不会使用某些指令(例如LDM)来加速这一过程,并且不太可能足够智能来展开循环.这里有一种方法,它结合了我在 comments 中提到的3个 idea :循环展开、缓存预取和利用多重加载(LDM)指令.每个数组元素的指令周期计数大约为3个时钟,但这还没有考虑内存延迟.

Theory of operation: ARM的CPU设计在一个时钟周期内执行大多数指令,但指令是在管道中执行的.C编译器将try 通过在其间交错其他指令来消除管道延迟.当出现与原始C代码类似的紧密循环时,编译器将很难隐藏延迟,因为必须立即比较从内存读取的值.我下面的代码在两组4个寄存器之间交替,以显著减少内存本身和获取数据的管道的延迟.一般来说,当处理大型数据集时,如果代码没有使用大部分或所有可用寄存器,则无法获得最佳性能.

; r0 = count, r1 = source ptr, r2 = comparison value

   stmfd sp!,{r4-r11}   ; save non-volatile registers
   mov r3,r0,LSR #3     ; loop count = total count / 8
   pld [r1,#128]
   ldmia r1!,{r4-r7}    ; pre load first set
loop_top:
   pld [r1,#128]
   ldmia r1!,{r8-r11}   ; pre load second set
   cmp r4,r2            ; search for match
   cmpne r5,r2          ; use conditional execution to avoid extra branch instructions
   cmpne r6,r2
   cmpne r7,r2
   beq found_it
   ldmia r1!,{r4-r7}    ; use 2 sets of registers to hide load delays
   cmp r8,r2
   cmpne r9,r2
   cmpne r10,r2
   cmpne r11,r2
   beq found_it
   subs r3,r3,#1        ; decrement loop count
   bne loop_top
   mov r0,#0            ; return value = false (not found)
   ldmia sp!,{r4-r11}   ; restore non-volatile registers
   bx lr                ; return
found_it:
   mov r0,#1            ; return true
   ldmia sp!,{r4-r11}
   bx lr

Update:

.L9: cmp r3, r0
     beq .L8
.L3: ldr r2, [r3, #4]!
     cmp r2, r1
     bne .L9
     mov r0, #1
.L2: add sp, sp, #1024
     bx  lr
.L8: mov r0, #0
     b .L2

GCC的输出不仅没有展开循环,而且在LDR之后浪费了一个时钟.它需要每个数组元素至少8个时钟.它在使用地址知道何时退出循环方面做得很好,但是编译器能够做的所有神奇的事情都在这段代码中找不到.我没有在目标平台上运行代码(我没有目标平台),但是任何有ARM代码性能经验的人都可以看到我的代码更快.

Update 2:

loop_top:
   ldr  r3,[r1],#4  
   cmp  r3,r2  
   beq  true_exit
   subs r0,r0,#1 
   bne  loop_top
false_exit: xxx
   bx   lr
true_exit: xxx
   bx   lr

正如我所说,我不拥有OP的确切硬件,但我将在nVidia Tegra 3和Tegra 4上测试3个不同版本的性能,并很快将结果发布在这里.

Update 3:

             My Code       MS Code
Surface RT    297ns         562ns
Surface RT 2  172ns         296ns  

在这两种情况下,我的代码运行速度几乎是原来的两倍.大多数现代ARM CPU可能会给出类似的结果.

C++相关问答推荐

librsvg rsvg_handle_get_dimensions获取像素大小与浏览器中的渲染大小没有不同

为什么已经设置的值在C中被重置为for循环条件中的新值?

C中的__attributor__((aligned(4),packed))与 struct 的用法

正在try 将文件/文件夹名从目录 struct 存储到链接列表

GCC创建应用于移动项的单独位掩码的目的是什么?

进程在写入管道时挂起

为什么此共享库没有预期的依赖项?

FRIDA-服务器成为端口扫描的目标?

一旦运行长度超过2,编译器是否会优化";strnlen(mystring,32)>;2";以停止循环?

ifdef __cplusplus中的整数文字单引号

#定义SSL_CONNECTION_NO_CONST

在C语言中,指针指向一个数组

C堆栈(使用动态数组)realloc内存泄漏问题

这段代码用于在C中以相反的顺序打印数组,但它不起作用

Go和C中的数据 struct 对齐差异

宏观;S C调深度

有没有办法减少C语言中线程的堆大小?

为什么孤儿进程在 Linux 中没有被 PID 1 采用,就像我读过的一本书中声称的那样?

如何修复数组数据与列标题未对齐的问题?

使用邻接表创建图