In situazioni in cui le prestazioni sono della massima importanza, il compilatore C molto probabilmente non produrrà il codice più veloce rispetto a quello che si può fare con un linguaggio assembly assemblato a mano. Tendo a prendere la strada della minor resistenza - per piccole routine come questa, scrivo solo codice asm e ho una buona idea di quanti cicli ci vorranno per eseguire. Potresti essere in grado di giocherellare con il codice C e far sì che il compilatore generi un buon output, ma potresti finire per perdere un sacco di tempo a sintonizzare l'output in quel modo. I compilatori (soprattutto di Microsoft) hanno fatto molta strada negli ultimi anni, ma non sono ancora così intelligenti come il compilatore tra le orecchie perché stai lavorando sulla tua situazione specifica e non solo su un caso generale. Il compilatore potrebbe non utilizzare determinate istruzioni (ad es. LDM) che possono accelerare questo, ed è " È improbabile che sia abbastanza intelligente da srotolare il circuito. Ecco un modo per farlo che incorpora le 3 idee che ho citato nel mio commento: srotolamento del ciclo, prefetch della cache e utilizzo dell'istruzione di caricamento multiplo (ldm). Il conteggio del ciclo di istruzioni arriva a circa 3 clock per elemento dell'array, ma questo non tiene conto dei ritardi di memoria.
Teoria di funzionamento: il design della CPU ARM esegue la maggior parte delle istruzioni in un ciclo di clock, ma le istruzioni vengono eseguite in una pipeline. I compilatori C cercheranno di eliminare i ritardi della pipeline intercalando altre istruzioni in mezzo. Quando viene presentato con un ciclo stretto come il codice C originale, il compilatore avrà difficoltà a nascondere i ritardi perché il valore letto dalla memoria deve essere immediatamente confrontato. Il mio codice seguente alterna tra 2 set di 4 registri per ridurre significativamente i ritardi della memoria stessa e la pipeline che recupera i dati. In generale, quando si lavora con set di dati di grandi dimensioni e il codice non utilizza la maggior parte o tutti i registri disponibili, non si ottengono le massime prestazioni.
; 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
Aggiornare:
ci sono molti scettici nei commenti che pensano che la mia esperienza sia aneddotica / senza valore e richieda prove. Ho usato GCC 4.8 (da Android NDK 9C) per generare il seguente output con l'ottimizzazione -O2 (tutte le ottimizzazioni attivate incluso lo svolgimento di loop ). Ho compilato il codice C originale presentato nella domanda sopra. Ecco cosa ha prodotto 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
L'output di GCC non solo non srotola il loop, ma spreca anche un orologio su uno stallo dopo il LDR. Richiede almeno 8 clock per elemento dell'array. Fa un buon lavoro usando l'indirizzo per sapere quando uscire dal loop, ma tutte le cose magiche che i compilatori sono in grado di fare non si trovano da nessuna parte in questo codice. Non ho eseguito il codice sulla piattaforma di destinazione (non ne possiedo uno), ma chiunque abbia esperienza nelle prestazioni del codice ARM può vedere che il mio codice è più veloce.
Aggiornamento 2:
ho dato a Visual Studio 2013 SP2 di Microsoft la possibilità di fare meglio con il codice. È stato in grado di utilizzare le istruzioni NEON per vettorializzare l'inizializzazione del mio array, ma la ricerca del valore lineare come scritta dall'OP è risultata simile a quella generata da GCC (ho rinominato le etichette per renderlo più leggibile):
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
Come ho detto, non possiedo l'hardware esatto dell'OP, ma testerò le prestazioni su un nVidia Tegra 3 e Tegra 4 delle 3 diverse versioni e pubblicherò presto i risultati qui.
Aggiornamento 3:
ho eseguito il mio codice e il codice ARM compilato di Microsoft su un Tegra 3 e Tegra 4 (Surface RT, Surface RT 2). Ho eseguito 1000000 iterazioni di un ciclo che non riesce a trovare una corrispondenza in modo che tutto sia nella cache ed è facile da misurare.
My Code MS Code
Surface RT 297ns 562ns
Surface RT 2 172ns 296ns
In entrambi i casi il mio codice viene eseguito quasi il doppio della velocità. La maggior parte delle moderne CPU ARM darà probabilmente risultati simili.