在性能非常重要的情况下,与手动调优汇编语言相比,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可能会给出类似的结果.