69

Ho cercato di ottimizzare un codice estremamente critico per le prestazioni (un algoritmo di ordinamento rapido che viene 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:Quando, se mai, lo svolgimento del ciclo è ancora utile?

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

ho cercato srotolando a qualcosa di simile:

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 ha fatto assolutamente nessuna differenza quindi l'ho cambiato di nuovo alla 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?

+1

Posso chiederti perché non stai utilizzando le routine di quicksort della libreria standard? –

+8

@Poita: Perché il mio ha alcune caratteristiche extra di cui ho bisogno per i calcoli statistici che sto facendo e sono molto ottimizzato per i miei casi d'uso e quindi meno generale ma misurabilmente più veloce della lib standard. Sto usando il linguaggio di programmazione D, che ha un vecchio ottimizzatore crappy, e per grandi matrici di float casuali, continuo a battere l'ordinamento C++ STL di GCC del 10-20%. – dsimcha

risposta

90

Lo srotolamento del loop ha senso se è possibile interrompere 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<n; i++) 
{ 
    sum += data[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 di questo codice:

for (int i=0; i<n; i+=4) 
{ 
    sum1 += data[i+0]; 
    sum2 += data[i+1]; 
    sum3 += data[i+2]; 
    sum4 += data[i+3]; 
} 
sum = sum1 + sum2 + sum3 + sum4; 

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.

+2

Grazie. Ho provato lo srotolamento del loop in questo stile in molti altri punti della libreria in cui sto calcolando somme e cose, e in questi luoghi funziona a meraviglia. Sono quasi certo che la ragione è che aumenta il parallelismo a livello di istruzioni, come suggerisci tu. – dsimcha

+2

Bella risposta ed esempio istruttivo. Anche se non vedo come le bancarelle su cache-misses potrebbero influenzare le prestazioni * per questo particolare esempio *. Sono venuto per spiegare a me stesso le differenze di prestazioni tra i due pezzi di codice (sulla mia macchina il secondo pezzo di codice è 2-3 volte più veloce) notando che il primo disabilita qualsiasi tipo di parallelismo a livello di istruzione nelle corsie a virgola mobile. Il secondo consentirebbe a una CPU super scalare di eseguire fino a quattro aggiunte in virgola mobile contemporaneamente. –

+1

Ricordare che il risultato non sarà identicamente numericamente al loop originale quando si calcola una somma in questo modo. – Barabas

17

Questi 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(); 
} 

scrittura:

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

Anche allora quasi certamente non avrà importanza, ma ora si sta facendo 50 confronti, invece di 200 (immaginate il confronto è più complesso).

Manuale Lo svolgimento 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 anziché x *= 2. Devi solo scrivere x *= 2 e il compilatore lo ottimizzerà per te in base a ciò che è meglio.

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

+0

Sono d'accordo, quei giorni sono finiti dove è possibile modificare qualche anello qua e là e aspettarsi enormi benefici. I compilatori sono così avanzati. – fastcodejava

+0

Mi piace quando il compilatore ottimizza 'x * = 2' per me. Non mi piace quando tenta di riorganizzare il mio codice. Ciò include lo srotolamento del ciclo, il sollevamento del codice, l'eliminazione del codice che non verrà mai raggiunto, cose del genere. Sono perfettamente in grado di decidere quando o quando non fare quelle cose. –

+1

@ Mike Sicuramente disattivando l'ottimizzazione se una buona idea quando è perplessa, ma vale la pena leggere il link che Poita_ ha pubblicato. I compilatori stanno diventando * dolorosamente * bravi in ​​quel business. – dmckee

0

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 è possibile ruotare gruppi di lavoro separati. Per farlo funzionare dovresti ridurre il numero di iterazioni.

+0

Stavo srotolando un ordinamento rapido che viene chiamato dal ciclo interno della mia simulazione, non dal ciclo principale della simulazione. – dsimcha

13

Indipendentemente dalla previsione del ramo sull'hardware moderno, la maggior parte dei compilatori eseguono comunque lo srotolamento del ciclo.

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

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

+0

Buona lettura. Grazie. – dsimcha

+6

Questa è una buona lettura, ma l'unica parte che ho pensato fosse sul punto in cui parlava di mantenere semplice la struttura dei dati. Il resto è stato accurato ma si basa su un'ipotesi non dichiarata - che ciò che viene eseguito * deve * essere. Nel tuning che faccio, trovo le persone che si preoccupano dei registri e dei problemi di cache quando enormi quantità di tempo finiscono in inutili montagne di codice di astrazione. –

+0

"le ottimizzazioni delle mani non sono quasi mai efficaci" → Forse vero se sei completamente nuovo nell'attività. Semplicemente non vero altrimenti. – Veedrac

0

Lo srotolamento del ciclo è 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.

Nell'esempio, si utilizza una piccola quantità di variabili locali, senza sovrascrivere i registri.

Il confronto (all'estremità del loop) è anche uno svantaggio principale se il confronto è pesante (cioè non- test istruzione), soprattutto se dipende da una funzione esterna.

Lo srotolamento del loop aiuta ad aumentare la consapevolezza della CPU anche per la previsione dei rami, ma questi si verificano comunque.

2

Per quanto mi risulta, i compilatori moderni già srotolare loop se del caso - un esempio è gcc, se superato i flag di ottimizzazione che il manuale dice lo farà:

Srotolare loop il cui numero di iterazioni può essere determinato al momento della compilazione o dopo l'accesso al ciclo .

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

+0

I compilatori just in time non eseguono lo srotolamento del loop, le euristiche sono troppo costose. I compilatori statici possono impiegare più tempo su di esso, ma la differenza tra i due modi dominanti è importante. – Abel

2

Lo srotolamento del loop, sia che si svolga manualmente lo srotolamento o lo srotolamento del compilatore, può essere spesso 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.

+0

Perché in particolare su CPU x86 recinte? – JohnTortugo

+3

@JohnTortugo: le moderne CPU x86 hanno alcune ottimizzazioni per i loop piccoli - si veda ad es. Rilevatore Loop Stream sulle architetture Core e Nehalem: srotolando un ciclo in modo che non sia più abbastanza piccolo da adattarsi alla cache LSD, questa ottimizzazione viene annullata. Vedi per es. http://www.tomshardware.com/reviews/Intel-i7-nehalem-cpu,2041-3.html –

1

Provare senza saperlo non è il modo per farlo.
Questo tipo richiede un'alta percentuale del tempo complessivo?

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

Here's an example of how to get maximum performance.

1

Loop svolgimento può essere utile nei casi specifici. L'unico guadagno non è saltare alcuni test!

Può ad esempio consentire la sostituzione scalare, l'inserimento efficiente del precaricamento del software ... Sareste sorpresi in realtà quanto possa essere utile (potete ottenere facilmente il 10% di accelerazione sulla maggior parte dei cicli 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)