En situaciones en las que el rendimiento es de suma importancia, el compilador de C probablemente no producirá el código más rápido en comparación con lo que puede hacer con el lenguaje ensamblador ajustado a mano. Tiendo a tomar el camino de menor resistencia: para pequeñas rutinas como esta, solo escribo un código asm y tengo una buena idea de cuántos ciclos tomará ejecutar. Es posible que pueda jugar con el código C y hacer que el compilador genere una buena salida, pero puede terminar perdiendo mucho tiempo ajustando la salida de esa manera. Los compiladores (especialmente de Microsoft) han recorrido un largo camino en los últimos años, pero aún no son tan inteligentes como el compilador entre tus oídos porque estás trabajando en tu situación específica y no solo en un caso general. El compilador no puede hacer uso de ciertas instrucciones (por ejemplo, LDM) que pueden acelerar esto, y ' Es poco probable que sea lo suficientemente inteligente como para desenrollar el bucle. Aquí hay una manera de hacerlo que incorpora las 3 ideas que mencioné en mi comentario: Desenrollo de bucle, captación previa de caché y haciendo uso de la instrucción de carga múltiple (ldm). El recuento del ciclo de instrucciones es de aproximadamente 3 relojes por elemento de matriz, pero esto no tiene en cuenta los retrasos de memoria.
Teoría de operación: el diseño de CPU de ARM ejecuta la mayoría de las instrucciones en un ciclo de reloj, pero las instrucciones se ejecutan en una tubería. Los compiladores de C intentarán eliminar los retrasos en la canalización intercalando otras instrucciones intermedias. Cuando se presenta un bucle cerrado como el código C original, el compilador tendrá dificultades para ocultar los retrasos porque el valor leído de la memoria debe compararse de inmediato. Mi código a continuación alterna entre 2 conjuntos de 4 registros para reducir significativamente los retrasos de la memoria en sí y la tubería que busca los datos. En general, cuando trabaja con grandes conjuntos de datos y su código no hace uso de la mayoría o de todos los registros disponibles, no obtiene el máximo rendimiento.
; 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
Actualización:
Hay muchos escépticos en los comentarios que piensan que mi experiencia es anecdótica / sin valor y requieren pruebas. Utilicé GCC 4.8 (del Android NDK 9C) para generar el siguiente resultado con la optimización -O2 (todas las optimizaciones activadas, incluido el desenrollado del bucle ). Compilé el código C original presentado en la pregunta anterior. Esto es lo que produjo 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
La salida de GCC no solo no desenrolla el bucle, sino que también desperdicia un reloj en una parada después del LDR. Requiere al menos 8 relojes por elemento de matriz. Hace un buen trabajo al usar la dirección para saber cuándo salir del bucle, pero todas las cosas mágicas que los compiladores son capaces de hacer no se encuentran en este código. No ejecuté el código en la plataforma de destino (no tengo uno), pero cualquier persona con experiencia en el rendimiento del código ARM puede ver que mi código es más rápido.
Actualización 2:
le di a Microsoft Visual Studio 2013 SP2 la oportunidad de mejorar con el código. Fue capaz de usar instrucciones NEON para vectorizar mi inicialización de matriz, pero la búsqueda de valor lineal tal como está escrita por el OP resultó similar a lo que generó GCC (cambié el nombre de las etiquetas para que sea más legible):
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
Como dije, no soy dueño del hardware exacto del OP, pero probaré el rendimiento en un nVidia Tegra 3 y Tegra 4 de las 3 versiones diferentes y publicaré los resultados aquí pronto.
Actualización 3:
ejecuté mi código y el código ARM compilado de Microsoft en un Tegra 3 y Tegra 4 (Surface RT, Surface RT 2). Ejecuté 1000000 iteraciones de un bucle que no puede encontrar una coincidencia para que todo esté en caché y sea fácil de medir.
My Code MS Code
Surface RT 297ns 562ns
Surface RT 2 172ns 296ns
En ambos casos, mi código se ejecuta casi el doble de rápido. La mayoría de las CPU ARM modernas probablemente darán resultados similares.