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


93

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) eseguendo il looping. 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 alcuna differenza, quindi l'ho cambiato di nuovo in una forma più leggibile. Ho avuto esperienze simili altre volte in cui ho provato lo srotolamento del loop. Data la qualità dei predittori di branch sull'hardware moderno, quando, se mai, lo srotolamento del loop è ancora un'ottimizzazione utile?


1
Posso chiederti perché non stai usando le routine quicksort della libreria standard?
Peter Alexander,

14
@ Poita: Perché i miei hanno alcune funzionalità extra di cui ho bisogno per i calcoli statistici che sto facendo e sono molto ottimizzati per i miei casi d'uso e quindi meno generali ma misurabilmente più veloci della libreria standard. Sto usando il linguaggio di programmazione D, che ha un vecchio ottimizzatore di merda, e per grandi array di float casuali, continuo a battere l'ordinamento C ++ STL di GCC del 10-20%.
dsimcha,

Risposte:


122

Lo srotolamento del ciclo ha senso se è possibile interrompere le catene di dipendenze. Questo dà a una CPU fuori servizio o super scalare la possibilità di programmare meglio le cose e quindi funzionare 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 si verifica uno stallo perché si ha una mancanza di cache sull'array di dati, la cpu non può fare altro che aspettare.

D'altra parte 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ù velocemente. Se si ottiene un errore di cache o un altro stallo in un calcolo, ci sono ancora altre tre catene di dipendenze che non dipendono dallo stallo. Una CPU fuori servizio può eseguirli.


2
Grazie. Ho provato a srotolare il ciclo in questo stile in molti altri punti della libreria dove sto calcolando le somme e cose del genere, e in questi posti funziona a meraviglia. Sono quasi sicuro che il motivo sia che aumenta il parallelismo a livello di istruzione, come suggerisci.
dsimcha

2
Bella risposta ed esempio istruttivo. Anche se non vedo come i blocchi sui mancati riscontri nella cache possano influire sulle prestazioni per questo particolare esempio . Sono arrivato a 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 in virgola mobile. Il secondo consentirebbe a una CPU super scalare di eseguire fino a quattro aggiunte in virgola mobile contemporaneamente.
Toby Brull

2
Tieni presente che il risultato non sarà numericamente identico al ciclo originale quando si calcola una somma in questo modo.
Barabas

La dipendenza portata dal ciclo è un ciclo , l'addizione. Un core OoO andrà bene. Qui lo srotolamento potrebbe aiutare la SIMD in virgola mobile, ma non si tratta di OoO.
Veedrac

2
@ Nils: non molto; le CPU x86 OoO tradizionali sono ancora abbastanza simili a Core2 / Nehalem / K10. Recuperare il ritardo dopo un errore nella cache era ancora piuttosto minore, nascondere la latenza FP era ancora il principale vantaggio. Nel 2010, le CPU che potevano eseguire 2 carichi per clock erano ancora più rare (solo AMD perché SnB non era ancora stato rilasciato), quindi più accumulatori erano decisamente meno preziosi per il codice intero rispetto ad ora (ovviamente questo è codice scalare che dovrebbe auto-vettorializzare , quindi chissà se i compilatori trasformeranno più accumulatori in elementi vettoriali o in più accumulatori vettoriali ...)
Peter Cordes

25

Quelle 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 srotolamento manuale del ciclo in generale è in gran parte un artefatto della storia. È un'altra della lista crescente di cose che un buon compilatore farà per te quando è importante. Ad esempio, la maggior parte delle persone non si preoccupa di scrivere x <<= 1o x += xinvece di x *= 2. Devi solo scrivere x *= 2e il compilatore lo ottimizzerà per te al meglio.

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


1
@ Mike Sicuramente disattivare l'ottimizzazione se una buona idea quando perplesso, ma vale la pena leggere il link che Poita_ ha pubblicato. I compilatori stanno diventando incredibilmente bravi in ​​questo settore.
dmckee --- gattino ex moderatore

16
@Mike "Sono perfettamente in grado di decidere quando o quando non fare quelle cose" ... ne dubito, a meno che tu non sia sovrumano.
Mr. Boy

5
@ John: non so perché dici così; la gente sembra pensare che l'ottimizzazione sia una sorta di arte nera che solo i compilatori e i bravi indovini sanno fare. Tutto si riduce alle istruzioni, ai cicli e ai motivi per cui vengono spesi. Come ho spiegato molte volte su SO, è facile dire come e perché vengono spesi. Se ho un ciclo che deve utilizzare una percentuale significativa di tempo e impiega troppi cicli nel sovraccarico del ciclo, rispetto al contenuto, posso vederlo e srotolarlo. Lo stesso per il sollevamento del codice. Non ci vuole un genio.
Mike Dunlavey

3
Sono sicuro che non sia così difficile, ma dubito ancora che tu possa farlo velocemente come fa il compilatore. Qual è il problema con il compilatore che lo fa per te comunque? Se non ti piace, disattiva le ottimizzazioni e brucia il tuo tempo come se fosse il 1990!
Mr. Boy

2
Il guadagno di prestazioni dovuto allo srotolamento del loop non ha nulla a che fare con i confronti che stai salvando. Niente di niente.
bobbogo

14

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

Sarebbe utile scoprire quante ottimizzazioni fa per te il tuo compilatore.

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


7
Questa è una buona lettura, ma l'unica parte che ho pensato fosse sul segno è stata quella in cui parla di mantenere semplice la struttura dei dati. Il resto è stato accurato, ma si basa su un presupposto enorme e non dichiarato: ciò che viene eseguito deve essere. Nella messa a punto che faccio, trovo le persone che si preoccupano dei registri e dei problemi di cache quando enormi quantità di tempo stanno andando in montagne inutili di codice di astrazione.
Mike Dunlavey

4
"le ottimizzazioni delle mani non sono quasi mai efficaci" → Forse è vero se sei completamente nuovo nel compito. Semplicemente non è vero altrimenti.
Veedrac

Nel 2019 ho ancora svolto operazioni manuali di srotolamento con notevoli guadagni rispetto ai tentativi automatici del compilatore ... quindi non è così affidabile lasciare che il compilatore faccia tutto. Sembra non srotolarsi così spesso. Almeno per c # non posso parlare a nome di tutte le lingue.
WDUK

2

Per quanto ne so, i compilatori moderni già srotolano i loop dove appropriato - un esempio è gcc, se vengono passati i flag di ottimizzazione, il manuale dice che:

Svolgi loop il cui numero di iterazioni può essere determinato in fase di compilazione o all'ingresso nel loop.

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


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

2

Lo srotolamento del loop, che si tratti di srotolamento manuale o di compilazione, può spesso essere controproducente, in particolare con le CPU x86 più recenti (Core 2, Core i7). In conclusione: confronta il tuo codice con e senza loop srotolato su qualunque CPU prevedi di distribuire questo codice.


Perché in particolare sulle CPU x86 recet?
JohnTortugo

7
@JohnTortugo: Le moderne CPU x86 hanno alcune ottimizzazioni per piccoli loop - vedi ad esempio Loop Stream Detector sulle architetture Core e Nehalem - lo srotolamento di un loop in modo che non sia più abbastanza piccolo da stare nella cache LSD vanifica questa ottimizzazione. Vedi ad esempio tomshardware.com/reviews/Intel-i7-nehalem-cpu,2041-3.html
Paul R

1

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

Tutto ciò che fa lo srotolamento del ciclo è ridurre il sovraccarico del ciclo di incremento / decremento, confronto per la condizione di arresto e salto. Se quello che stai facendo nel ciclo richiede più cicli di istruzione rispetto al sovraccarico del ciclo stesso, non vedrai molti miglioramenti in percentuale.

Ecco un esempio di come ottenere le massime prestazioni.


1

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

Ad esempio, può consentire la sostituzione scalare, l'inserimento efficiente del prefetch del software ... Saresti sorpreso di quanto possa essere utile (puoi facilmente ottenere il 10% di velocità sulla maggior parte dei loop anche con -O3) srotolando in modo aggressivo.

Come è stato detto prima, però, 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)


0

Lo svolgimento del ciclo dipende interamente dalla dimensione del problema. Dipende interamente dalla capacità del tuo algoritmo 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.

Un buon scenario per lo svolgimento del ciclo sarebbe la rotazione di un'immagine. Dal momento che potresti ruotare gruppi di lavoro separati. Per farlo funzionare, dovresti ridurre il numero di iterazioni.


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

0

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

Nel tuo esempio, usi una piccola quantità di variabili locali, senza esagerare con i registri.

Anche il confronto (alla fine del ciclo) è un grave inconveniente se il confronto è pesante (cioè nontest istruzioni), specialmente se dipende da una funzione esterna.

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

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.