In Situationen, in denen die Leistung von größter Bedeutung ist, erzeugt der C-Compiler höchstwahrscheinlich nicht den schnellsten Code im Vergleich zu dem, was Sie mit handgestimmter Assemblersprache tun können. Ich neige dazu, den Weg des geringsten Widerstands zu beschreiten - für kleine Routinen wie diese schreibe ich einfach asm-Code und habe eine gute Vorstellung davon, wie viele Zyklen für die Ausführung erforderlich sind. Möglicherweise können Sie mit dem C-Code herumspielen und den Compiler dazu bringen, eine gute Ausgabe zu generieren, aber Sie verschwenden möglicherweise viel Zeit damit, die Ausgabe auf diese Weise zu optimieren. Compiler (insbesondere von Microsoft) haben in den letzten Jahren einen langen Weg zurückgelegt, sind jedoch immer noch nicht so intelligent wie der Compiler zwischen Ihren Ohren, da Sie an Ihrer spezifischen Situation arbeiten und nicht nur an einem allgemeinen Fall. Der Compiler verwendet möglicherweise bestimmte Anweisungen (z. B. LDM) nicht, die dies beschleunigen können. Es ist unwahrscheinlich, dass es klug genug ist, um die Schleife abzuwickeln. Hier ist eine Möglichkeit, die die drei Ideen enthält, die ich in meinem Kommentar erwähnt habe: Schleifen-Abrollen, Cache-Prefetch und Verwenden der ldm-Anweisung (Multiple Load). Die Anzahl der Befehlszyklen beträgt ungefähr 3 Takte pro Array-Element, berücksichtigt jedoch keine Speicherverzögerungen.
Betriebstheorie: Das CPU-Design von ARM führt die meisten Befehle in einem Taktzyklus aus, die Befehle werden jedoch in einer Pipeline ausgeführt. C-Compiler versuchen, die Pipeline-Verzögerungen zu beseitigen, indem sie andere Anweisungen dazwischen verschachteln. Bei einer engen Schleife wie dem ursprünglichen C-Code fällt es dem Compiler schwer, die Verzögerungen zu verbergen, da der aus dem Speicher gelesene Wert sofort verglichen werden muss. Mein Code unten wechselt zwischen 2 Sätzen von 4 Registern, um die Verzögerungen des Speichers selbst und der Pipeline, die die Daten abruft, erheblich zu reduzieren. Wenn Sie mit großen Datenmengen arbeiten und Ihr Code nicht die meisten oder alle verfügbaren Register verwendet, erhalten Sie im Allgemeinen keine maximale Leistung.
; 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:
Es gibt viele Skeptiker in den Kommentaren, die meine Erfahrung für anekdotisch / wertlos halten und Beweise benötigen. Ich habe GCC 4.8 (vom Android NDK 9C) verwendet, um die folgende Ausgabe mit der Optimierung -O2 zu generieren (alle Optimierungen sind aktiviert, einschließlich des Abrollens der Schleife ). Ich habe den ursprünglichen C-Code zusammengestellt, der in der obigen Frage dargestellt ist. Folgendes hat GCC produziert:
.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
Die Ausgabe von GCC rollt nicht nur die Schleife nicht ab, sondern verschwendet auch einen Takt bei einem Stillstand nach dem LDR. Es sind mindestens 8 Takte pro Array-Element erforderlich. Es ist gut, die Adresse zu verwenden, um zu wissen, wann die Schleife verlassen werden muss, aber all die magischen Dinge, zu denen Compiler in der Lage sind, sind in diesem Code nirgends zu finden. Ich habe den Code nicht auf der Zielplattform ausgeführt (ich besitze keine), aber jeder, der Erfahrung mit der Leistung von ARM-Code hat, kann feststellen, dass mein Code schneller ist.
Update 2:
Ich habe Microsoft Visual Studio 2013 SP2 die Möglichkeit gegeben, den Code besser zu nutzen. Es war in der Lage, NEON-Anweisungen zu verwenden, um meine Array-Initialisierung zu vektorisieren, aber die vom OP geschriebene Suche nach linearen Werten verlief ähnlich wie die von GCC generierte (ich habe die Beschriftungen umbenannt, um sie besser lesbar zu machen):
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
Wie gesagt, ich besitze nicht die genaue Hardware des OP, aber ich werde die Leistung auf einem nVidia Tegra 3 und Tegra 4 der 3 verschiedenen Versionen testen und die Ergebnisse bald hier veröffentlichen.
Update 3:
Ich habe meinen Code und den kompilierten ARM-Code von Microsoft auf einem Tegra 3 und Tegra 4 (Surface RT, Surface RT 2) ausgeführt. Ich habe 1000000 Iterationen einer Schleife ausgeführt, die keine Übereinstimmung findet, sodass sich alles im Cache befindet und leicht zu messen ist.
My Code MS Code
Surface RT 297ns 562ns
Surface RT 2 172ns 296ns
In beiden Fällen läuft mein Code fast doppelt so schnell. Die meisten modernen ARM-CPUs werden wahrscheinlich ähnliche Ergebnisse liefern.