在对性能要求极高的情况下,与手写汇编相比,C编译器可能无法生成最快的代码。我倾向于采用最简易的方法 - 对于像这样的小程序,我只编写汇编代码,并且对执行所需的周期数有很好的了解。您可以尝试调整C代码以获得编译器生成良好输出的效果,但是您可能会浪费大量时间来调整输出。编译器(特别是来自Microsoft的编译器)在过去几年中已经取得了长足进步,但它们仍然不如你的头脑中的编译器聪明,因为你正在处理你的特定情况而不仅仅是一般情况。编译器可能不会使用某些指令(例如LDM),以加速操作,也不太可能聪明到展开循环。以下是一种方法,其中包含我在评论中提到的3个想法:循环展开、缓存预取和利用多个加载(ldm)指令。指令周期数约为每个数组元素3个时钟周期,但这并未考虑内存延迟。
工作原理: ARM的CPU设计在一个时钟周期内执行大多数指令,但指令是在一个流水线中执行的。C编译器将尝试通过在其他指令之间交错来消除流水线延迟。当遇到像原始的C代码这样的紧密循环时,编译器将难以隐藏延迟,因为必须立即比较从内存中读取的值。下面的代码在2组4个寄存器之间交替,以显着减少内存本身和提取数据的流水线的延迟。通常,在处理大型数据集且您的代码未使用大多数或全部可用寄存器时,您无法获得最大性能。
stmfd sp!,{r4-r11}
mov r3,r0,LSR #3
pld [r1,#128]
ldmia r1!,{r4-r7}
loop_top:
pld [r1,#128]
ldmia r1!,{r8-r11}
cmp r4,r2
cmpne r5,r2
cmpne r6,r2
cmpne r7,r2
beq found_it
ldmia r1!,{r4-r7}
cmp r8,r2
cmpne r9,r2
cmpne r10,r2
cmpne r11,r2
beq found_it
subs r3,r3,#1
bne loop_top
mov r0,#0
ldmia sp!,{r4-r11}
bx lr
found_it:
mov r0,#1
ldmia sp!,{r4-r11}
bx lr
更新:评论中有很多怀疑者认为我的经验只是个案/毫无价值并要求证明。我使用GCC 4.8(来自Android NDK 9C)使用优化-O2(包括循环展开在内的所有优化)生成了以下输出。我编译了上面提出问题中的原始C代码。这是GCC生成的内容:
.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代码性能专家都可以看出我的代码更快。
更新2:
我给微软的Visual Studio 2013 SP2一个机会来改进代码。它能够使用NEON指令向量化我的数组初始化,但由OP编写的线性值搜索与GCC生成的类似(我重命名了标签以使其更可读)。
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:我在Tegra 3和Tegra 4(Surface RT,Surface RT 2)上运行了我的代码和Microsoft编译的ARM代码。我运行了1000000次迭代,但却无法找到匹配项,以便所有内容都在缓存中,并且很容易进行测量。
My Code MS Code
Surface RT 297ns 562ns
Surface RT 2 172ns 296ns
在这两种情况下,我的代码运行速度几乎快了一倍。大多数现代ARM处理器可能会给出类似的结果。
O(1)
或O(logN)
,相对于O(N)
),并且2)已经进行了性能分析,发现它是瓶颈时,才应该求助于汇编优化。 - vgru