Dans les situations où les performances sont de la plus haute importance, le compilateur C ne produira probablement pas le code le plus rapide par rapport à ce que vous pouvez faire avec le langage d'assemblage réglé manuellement. J'ai tendance à prendre le chemin de la moindre résistance - pour les petites routines comme celle-ci, j'écris juste du code asm et j'ai une bonne idée du nombre de cycles qu'il faudra pour exécuter. Vous pourrez peut-être manipuler le code C et faire en sorte que le compilateur génère une bonne sortie, mais vous risquez de perdre beaucoup de temps à régler la sortie de cette façon. Les compilateurs (en particulier de Microsoft) ont parcouru un long chemin ces dernières années, mais ils ne sont toujours pas aussi intelligents que le compilateur entre vos oreilles car vous travaillez sur votre situation spécifique et pas seulement sur un cas général. Le compilateur peut ne pas utiliser certaines instructions (par exemple LDM) qui peuvent accélérer cela, et il ' Il est peu probable que ce soit assez intelligent pour dérouler la boucle. Voici une façon de le faire qui intègre les 3 idées que j'ai mentionnées dans mon commentaire: déroulement de boucle, pré-extraction du cache et utilisation de l'instruction de chargement multiple (ldm). Le nombre de cycles d'instructions est d'environ 3 horloges par élément de tableau, mais cela ne prend pas en compte les retards de mémoire.
Théorie de fonctionnement: la conception du processeur d'ARM exécute la plupart des instructions en un seul cycle d'horloge, mais les instructions sont exécutées dans un pipeline. Les compilateurs C essaieront d'éliminer les retards du pipeline en entrelaçant d'autres instructions entre les deux. Lorsqu'il est présenté avec une boucle serrée comme le code C d'origine, le compilateur aura du mal à cacher les retards car la valeur lue en mémoire doit être immédiatement comparée. Mon code ci-dessous alterne entre 2 ensembles de 4 registres pour réduire considérablement les délais de la mémoire elle-même et du pipeline de récupération des données. En général, lorsque vous travaillez avec de grands ensembles de données et que votre code n'utilise pas la plupart ou tous les registres disponibles, vous n'obtenez pas des performances maximales.
; 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
Mise à jour:
Il y a beaucoup de sceptiques dans les commentaires qui pensent que mon expérience est anecdotique / sans valeur et nécessite une preuve. J'ai utilisé GCC 4.8 (de l'Android NDK 9C) pour générer la sortie suivante avec l'optimisation -O2 (toutes les optimisations sont activées, y compris le déroulement de la boucle ). J'ai compilé le code C original présenté dans la question ci-dessus. Voici ce que GCC a produit:
.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 sortie de GCC non seulement ne déroule pas la boucle, mais gaspille également une horloge sur un décrochage après le LDR. Il nécessite au moins 8 horloges par élément de tableau. Il fait un bon travail en utilisant l'adresse pour savoir quand sortir de la boucle, mais toutes les choses magiques que les compilateurs sont capables de faire sont introuvables dans ce code. Je n'ai pas exécuté le code sur la plate-forme cible (je n'en possède pas), mais toute personne expérimentée dans les performances du code ARM peut voir que mon code est plus rapide.
Mise à jour 2:
j'ai donné à Visual Studio 2013 SP2 de Microsoft une chance de faire mieux avec le code. Il a pu utiliser les instructions NEON pour vectoriser l'initialisation de mon tableau, mais la recherche de valeur linéaire écrite par l'OP est sortie similaire à ce que GCC a généré (j'ai renommé les étiquettes pour la rendre plus lisible):
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
Comme je l'ai dit, je ne possède pas le matériel exact de l'OP, mais je vais tester les performances sur un nVidia Tegra 3 et Tegra 4 des 3 versions différentes et publier les résultats ici bientôt.
Mise à jour 3:
J'ai exécuté mon code et le code ARM compilé de Microsoft sur un Tegra 3 et un Tegra 4 (Surface RT, Surface RT 2). J'ai exécuté 1000000 itérations d'une boucle qui ne parvient pas à trouver une correspondance afin que tout soit en cache et qu'il soit facile à mesurer.
My Code MS Code
Surface RT 297ns 562ns
Surface RT 2 172ns 296ns
Dans les deux cas, mon code s'exécute presque deux fois plus vite. La plupart des processeurs ARM modernes donneront probablement des résultats similaires.