Quando, se mai, lo svolgimento del ciclo è ancora utile?

Ho cercato di ottimizzare un codice estremamente critico per le prestazioni (un algoritmo di ordinamento rapido chiamato milioni e milioni di volte all’interno di una simulazione di monte carlo) per lo srotolamento del ciclo. Ecco il ciclo interno che sto cercando di accelerare:

// Search for elements to swap. while(myArray[++index1] < pivot) {} while(pivot < myArray[--index2]) {} 

Ho provato a srotolare qualcosa come:

 while(true) { if(myArray[++index1] < pivot) break; if(myArray[++index1] < pivot) break; // More unrolling } while(true) { if(pivot < myArray[--index2]) break; if(pivot < myArray[--index2]) break; // More unrolling } 

Questo non ha fatto assolutamente alcuna differenza, quindi l’ho cambiato di nuovo nella forma più leggibile. Ho avuto esperienze simili altre volte ho provato a srotolare il ciclo. Data la qualità dei predittori di ramo sull’hardware moderno, quando, se mai, lo srotolamento del ciclo è ancora un’ottimizzazione utile?

Lo srotolamento del loop ha senso se riesci a rompere le catene di dipendenza. Ciò dà a una CPU fuori scala o super-scalare la possibilità di pianificare le cose meglio e quindi correre più velocemente.

Un semplice esempio:

 for (int i=0; i 

Qui la catena di dipendenza degli argomenti è molto breve. Se ottieni uno stallo perché hai una cache-miss sull'array di dati, la CPU non può fare altro che aspettare.

D'altra parte questo codice:

 for (int i=0; i 

potrebbe correre più veloce. Se ricevi un errore di cache o un altro stallo in un calcolo ci sono ancora tre altre catene di dipendenze che non dipendono dallo stallo. Una CPU guasta può eseguirle.

Quelli non farebbero alcuna differenza perché stai facendo lo stesso numero di confronti. Ecco un esempio migliore. Invece di:

 for (int i=0; i<200; i++) { doStuff(); } 

Scrivi:

 for (int i=0; i<50; i++) { doStuff(); doStuff(); doStuff(); doStuff(); } 

Anche allora quasi certamente non avrà importanza, ma ora stai facendo 50 confronti invece di 200 (immagina che il confronto sia più complesso).

Tuttavia, lo svolgimento manuale del ciclo in generale è in gran parte un artefatto della storia. È un'altra delle crescenti liste di cose che un buon compilatore farà per te quando è importante. Ad esempio, la maggior parte delle persone non si preoccupa di scrivere x <<= 1 o x += x invece di x *= 2 . Scrivi semplicemente x *= 2 e il compilatore lo ottimizzerà per te in base a ciò che è meglio.

Fondamentalmente c'è sempre meno bisogno di indovinare il tuo compilatore.

Indipendentemente dalla previsione dei rami sull’hardware moderno, la maggior parte dei compilatori esegue comunque lo srotolamento del loop.

Vale la pena scoprire quante ottimizzazioni il tuo compilatore fa per te.

Ho trovato la presentazione di Felix von Leitner molto illuminante sull’argomento. Ti raccomando di leggerlo. Riepilogo: I compilatori moderni sono MOLTO intelligenti, quindi le ottimizzazioni manuali non sono quasi mai efficaci.

Per quanto ho capito, i compilatori moderni già srotolano i loop dove appropriato – un esempio è gcc, se ha superato i flag di ottimizzazione il manuale dice che lo farà:

Srotolare i loop il cui numero di iterazioni può essere determinato al momento della compilazione o all’entrata del ciclo.

Quindi, in pratica è probabile che il tuo compilatore faccia i casi banali per te. Sta a te quindi assicurarti che il maggior numero ansible dei tuoi loop sia facile per il compilatore per determinare quante iterazioni saranno necessarie.

Lo srotolamento del loop, che si tratti di srotolare le mani o di srotolare il compilatore, può spesso essere controproducente, in particolare con le CPU x86 più recenti (Core 2, Core i7). Bottom line: confronta il tuo codice con e senza ciclo di svolgimento su qualsiasi CPU hai intenzione di implementare questo codice.

Provare senza saperlo non è il modo di farlo.
Questo tipo prende un’alta percentuale del tempo complessivo?

Lo svolgimento di tutti gli arresti del ciclo riduce il sovraccarico del ciclo di incremento / decremento, il confronto per la condizione di arresto e il salto. Se ciò che stai facendo nel ciclo richiede più cicli di istruzioni rispetto al sovraccarico del loop stesso, non vedrai molti miglioramenti percentuali.

Ecco un esempio di come ottenere il massimo delle prestazioni.

Lo srotolamento del loop può essere utile in casi specifici. L’unico guadagno non è saltare alcuni test!

Può ad esempio consentire la sostituzione scalare, l’inserimento efficiente del prefetching del software … Sareste sorpresi in realtà quanto possa essere utile (potete ottenere facilmente il 10% di accelerazione su molti loop anche con -O3) srotolando in modo aggressivo.

Come è stato detto prima, dipende molto dal ciclo e il compilatore e l’esperimento sono necessari. È difficile stabilire una regola (o l’euristica del compilatore per lo srotolamento sarebbe perfetta)

Lo srotolamento del loop dipende interamente dalla dimensione del problema. Dipende interamente dal fatto che l’algoritmo è in grado di ridurre le dimensioni in gruppi di lavoro più piccoli. Quello che hai fatto sopra non sembra così. Non sono sicuro che una simulazione di monte carlo possa essere srotolata.

I buoni scenari per lo srotolamento del loop farebbero ruotare un’immagine. Dal momento che è ansible ruotare gruppi di lavoro separati. Per farlo funzionare dovresti ridurre il numero di iterazioni.

Lo srotolamento del loop è ancora utile se ci sono molte variabili locali sia dentro che con il ciclo. Riutilizzare quei registri più invece di salvarne uno per l’indice del ciclo.

Nel tuo esempio, utilizzi una piccola quantità di variabili locali, non esagerando con i registri.

Anche il confronto (all’estremità del loop) è un grosso svantaggio se il confronto è pesante (cioè un’istruzione non di test ), specialmente se dipende da una funzione esterna.

Lo srotolamento del loop aiuta ad aumentare la consapevolezza della CPU anche per la previsione delle diramazioni, ma queste si verificano comunque.