Wann, wenn überhaupt, ist das Abrollen der Schleife noch sinnvoll?

93

Ich habe versucht, extrem leistungskritischen Code (einen schnellen Sortieralgorithmus, der in einer Monte-Carlo-Simulation millionenfach aufgerufen wird) durch Abrollen der Schleife zu optimieren. Hier ist die innere Schleife, die ich zu beschleunigen versuche:

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

Ich habe versucht, mich zu etwas abzuwickeln wie:

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
}

Das machte absolut keinen Unterschied, also habe ich es wieder in die besser lesbare Form geändert. Ich habe ähnliche Erfahrungen gemacht, als ich versucht habe, die Schleife abzuwickeln. Wann, wenn überhaupt, ist das Abrollen von Schleifen angesichts der Qualität von Verzweigungsvorhersagen auf moderner Hardware immer noch eine nützliche Optimierung?

Dsimcha
quelle
1
Darf ich fragen, warum Sie keine Standardbibliotheks-QuickSort-Routinen verwenden?
Peter Alexander
14
@Poita: Weil meine einige zusätzliche Funktionen haben, die ich für die statistischen Berechnungen benötige, die ich mache, und sehr gut auf meine Anwendungsfälle abgestimmt sind und daher weniger allgemein, aber messbar schneller als die Standardbibliothek. Ich verwende die Programmiersprache D, die einen alten beschissenen Optimierer hat, und für große Arrays von zufälligen Floats habe ich die C ++ STL-Sortierung von GCC immer noch um 10-20% übertroffen.
Dsimcha

Antworten:

122

Das Abrollen von Schleifen ist sinnvoll, wenn Sie Abhängigkeitsketten unterbrechen können. Dies gibt einer außer Betrieb befindlichen oder superskalaren CPU die Möglichkeit, die Dinge besser zu planen und somit schneller zu laufen.

Ein einfaches Beispiel:

for (int i=0; i<n; i++)
{
  sum += data[i];
}

Hier ist die Abhängigkeitskette der Argumente sehr kurz. Wenn Sie einen Stillstand bekommen, weil Sie einen Cache-Fehler im Datenarray haben, kann die CPU nichts anderes tun, als zu warten.

Auf der anderen Seite dieser Code:

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;

könnte schneller laufen. Wenn Sie in einer Berechnung einen Cache-Fehler oder einen anderen Stillstand erhalten, gibt es noch drei weitere Abhängigkeitsketten, die nicht vom Stillstand abhängen. Eine außer Betrieb befindliche CPU kann diese ausführen.

Nils Pipenbrinck
quelle
2
Vielen Dank. Ich habe versucht, die Schleife in diesem Stil an mehreren anderen Stellen in der Bibliothek abzuwickeln, an denen ich Summen und Dinge berechne, und an diesen Stellen wirkt es Wunder. Ich bin mir fast sicher, dass der Grund dafür ist, dass es die Parallelität auf Befehlsebene erhöht, wie Sie vorschlagen.
Dsimcha
2
Schöne Antwort und lehrreiches Beispiel. Obwohl ich nicht sehe, wie sich Verzögerungen bei Cache-Fehlern auf die Leistung in diesem Beispiel auswirken können . Ich kam, um mir die Leistungsunterschiede zwischen den beiden Codeteilen zu erklären (auf meinem Computer ist der zweite Codeteil 2-3 Mal schneller), indem ich feststellte, dass der erste jede Art von Parallelität auf Befehlsebene in den Gleitkommaspuren deaktiviert. Die zweite würde es einer superskalaren CPU ermöglichen, bis zu vier Gleitkomma-Adds gleichzeitig auszuführen.
Toby Brull
2
Beachten Sie, dass das Ergebnis bei der Berechnung einer Summe auf diese Weise nicht numerisch mit der ursprünglichen Schleife identisch ist.
Barabas
Die schleifengetragene Abhängigkeit ist ein Zyklus , die Addition. Ein OoO-Kern reicht aus. Hier könnte das Abrollen Gleitkomma-SIMD helfen, aber hier geht es nicht um OoO.
Veedrac
2
@Nils: Nicht sehr viel; Mainstream x86 OoO-CPUs sind Core2 / Nehalem / K10 immer noch ähnlich genug. Das Aufholen nach einem Cache-Fehler war immer noch recht gering, das Ausblenden der FP-Latenz war immer noch der Hauptvorteil. Im Jahr 2010 waren CPUs, die 2 Ladevorgänge pro Takt ausführen konnten, noch seltener (nur AMD, da SnB noch nicht freigegeben wurde), sodass mehrere Akkumulatoren für Ganzzahlcode definitiv weniger wertvoll waren als jetzt (dies ist natürlich skalarer Code, der automatisch vektorisiert werden sollte , also wer weiß, ob Compiler mehrere Akkumulatoren in Vektorelemente oder in mehrere Vektorakkumulatoren verwandeln ...)
Peter Cordes
25

Diese würden keinen Unterschied machen, da Sie die gleiche Anzahl von Vergleichen durchführen. Hier ist ein besseres Beispiel. Anstatt:

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

schreiben:

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

Selbst dann spielt es mit ziemlicher Sicherheit keine Rolle, aber Sie führen jetzt 50 statt 200 Vergleiche durch (stellen Sie sich vor, der Vergleich ist komplexer).

Das manuelle Abrollen von Schleifen im Allgemeinen ist jedoch weitgehend ein Artefakt der Geschichte. Es ist eine weitere wachsende Liste von Dingen, die ein guter Compiler für Sie erledigt, wenn es darauf ankommt. Zum Beispiel machen sich die meisten Leute nicht die Mühe zu schreiben x <<= 1oder x += xstattdessen x *= 2. Sie schreiben einfach x *= 2und der Compiler optimiert es für Sie auf das Beste.

Grundsätzlich müssen Sie Ihren Compiler immer weniger hinterfragen.

Cletus
quelle
1
@Mike Schalten Sie die Optimierung sicher aus, wenn Sie eine gute Idee haben, wenn Sie verwirrt sind, aber es lohnt sich, den Link zu lesen, den Poita_ gepostet hat. Compiler werden in diesem Geschäft schmerzlich gut.
dmckee --- Ex-Moderator Kätzchen
16
@Mike "Ich bin perfekt in der Lage zu entscheiden, wann oder wann ich diese Dinge nicht tun soll" ... Ich bezweifle es, es sei denn, du bist übermenschlich.
Mr. Boy
5
@ John: Ich weiß nicht warum du das sagst; Leute scheinen zu denken, dass Optimierung eine Art schwarze Kunst ist, die nur Compiler und gute Vermutungen zu tun wissen. Es kommt alles auf Anweisungen und Zyklen und die Gründe an, warum sie ausgegeben werden. Wie ich schon oft bei SO erklärt habe, ist es leicht zu sagen, wie und warum diese ausgegeben werden. Wenn ich eine Schleife habe, die einen erheblichen Prozentsatz der Zeit in Anspruch nehmen muss und im Vergleich zum Inhalt zu viele Zyklen im Schleifen-Overhead verbringt, kann ich das sehen und abrollen. Gleiches gilt für das Heben von Code. Es braucht kein Genie.
Mike Dunlavey
3
Ich bin mir sicher, dass es nicht so schwer ist, aber ich bezweifle immer noch, dass Sie es so schnell schaffen wie der Compiler. Was ist das Problem, wenn der Compiler das überhaupt für Sie erledigt? Wenn es dir nicht gefällt, schalte einfach die Optimierungen aus und verbrenne deine Zeit wie 1990!
Mr. Boy
2
Der Leistungsgewinn durch das Abrollen der Schleife hat nichts mit den Vergleichen zu tun, die Sie speichern. Gar nichts.
Bobbogo
14

Unabhängig von der Verzweigungsvorhersage auf moderner Hardware rollen die meisten Compiler die Schleife ohnehin für Sie ab.

Es lohnt sich herauszufinden, wie viele Optimierungen Ihr Compiler für Sie vornimmt.

Ich fand Felix von Leitners Präsentation zu diesem Thema sehr aufschlussreich. Ich empfehle Ihnen, es zu lesen. Zusammenfassung: Moderne Compiler sind SEHR clever, daher sind Handoptimierungen fast nie effektiv.

Peter Alexander
quelle
7
Das ist eine gute Lektüre, aber der einzige Teil, den ich für richtig hielt, war, wo er darüber spricht, die Datenstruktur einfach zu halten. Der Rest war korrekt, beruht aber auf einer riesigen unausgesprochenen Annahme - dass das, was ausgeführt wird , sein muss. Bei der Optimierung finde ich Leute, die sich Sorgen um Register und Cache-Fehler machen, wenn sehr viel Zeit in unnötige Berge von Abstraktionscode fließt.
Mike Dunlavey
3
"Handoptimierungen sind fast nie effektiv" → Vielleicht wahr, wenn Sie völlig neu in der Aufgabe sind. Sonst einfach nicht wahr.
Veedrac
Im Jahr 2019 habe ich immer noch manuelle Abrollvorgänge mit erheblichen Gewinnen gegenüber den automatischen Versuchen des Compilers durchgeführt. Es ist also nicht so zuverlässig, den Compiler alles machen zu lassen. Es scheint sich nicht allzu oft abzuwickeln. Zumindest für c # kann ich nicht für alle Sprachen sprechen.
WDUK
2

Soweit ich weiß, entrollen moderne Compiler gegebenenfalls bereits Schleifen - ein Beispiel ist gcc. Wenn die Optimierungsflags übergeben werden, heißt es im Handbuch:

Entrollen Sie Schleifen, deren Anzahl von Iterationen zur Kompilierungszeit oder beim Eintritt in die Schleife bestimmt werden kann.

In der Praxis ist es also wahrscheinlich, dass Ihr Compiler die trivialen Fälle für Sie erledigt. Es liegt daher an Ihnen, sicherzustellen, dass der Compiler so viele Ihrer Schleifen wie möglich leicht bestimmen kann, wie viele Iterationen benötigt werden.

Rich Bradshaw
quelle
Just-in-Time-Compiler führen normalerweise kein Loop-Unrolling durch, die Heuristiken sind zu teuer. Statische Compiler können mehr Zeit damit verbringen, aber der Unterschied zwischen den beiden dominanten Methoden ist wichtig.
Abel
2

Das Abrollen von Schleifen, sei es das Abrollen von Hand oder das Abrollen von Compilern, kann häufig kontraproduktiv sein, insbesondere bei neueren x86-CPUs (Core 2, Core i7). Fazit: Benchmarking Ihres Codes mit und ohne Schleifenabwicklung auf den CPUs, auf denen Sie diesen Code bereitstellen möchten.

Paul R.
quelle
Warum besonders bei Recet x86-CPUs?
JohnTortugo
7
@JohnTortugo: Moderne x86-CPUs haben bestimmte Optimierungen für kleine Schleifen - siehe z. B. Loop Stream Detector auf Core- und Nehalem-Architekturen - das Abrollen einer Schleife, sodass sie nicht mehr klein genug ist, um in den LSD-Cache zu passen, verhindert diese Optimierung. Siehe zB tomshardware.com/reviews/Intel-i7-nehalem-cpu,2041-3.html
Paul R
1

Versuchen ohne es zu wissen ist nicht der richtige Weg.
Nimmt diese Art einen hohen Prozentsatz der Gesamtzeit in Anspruch?

Alles, was das Abrollen der Schleife bewirkt, ist das Reduzieren des Schleifenaufwands durch Inkrementieren / Dekrementieren, Vergleichen für die Stoppbedingung und Springen. Wenn das, was Sie in der Schleife tun, mehr Befehlszyklen benötigt als der Schleifen-Overhead selbst, werden Sie prozentual keine große Verbesserung feststellen.

Hier ist ein Beispiel, wie Sie maximale Leistung erzielen.

Mike Dunlavey
quelle
1

Das Abrollen der Schleife kann in bestimmten Fällen hilfreich sein. Der einzige Vorteil ist, einige Tests nicht zu überspringen!

Es kann zum Beispiel das Ersetzen von Skalaren und das effiziente Einfügen von Software-Prefetching ermöglichen ... Sie wären überrascht, wie nützlich es sein kann (Sie können die meisten Loops sogar mit -O3 leicht um 10% beschleunigen), indem Sie es aggressiv abrollen.

Wie bereits gesagt, hängt es stark von der Schleife ab und der Compiler und das Experiment sind notwendig. Es ist schwer, eine Regel zu erstellen (oder die Compiler-Heuristik zum Abrollen wäre perfekt).

Kamtschatka
quelle
0

Das Abrollen der Schleife hängt vollständig von Ihrer Problemgröße ab. Es hängt ganz davon ab, ob Ihr Algorithmus die Größe in kleinere Arbeitsgruppen reduzieren kann. Was Sie oben getan haben, sieht nicht so aus. Ich bin mir nicht sicher, ob eine Monte-Carlo-Simulation überhaupt abgewickelt werden kann.

Ein gutes Szenario für das Abrollen der Schleife wäre das Drehen eines Bildes. Da könnte man separate Arbeitsgruppen drehen. Damit dies funktioniert, müssten Sie die Anzahl der Iterationen reduzieren.

jwendl
quelle
Ich habe eine schnelle Sortierung abgewickelt, die aus der inneren Schleife meiner Simulation aufgerufen wird, nicht aus der Hauptschleife der Simulation.
Dsimcha
0

Das Abrollen der Schleife ist immer noch nützlich, wenn sowohl in als auch mit der Schleife viele lokale Variablen vorhanden sind. Um diese Register mehr wiederzuverwenden, anstatt eines für den Schleifenindex zu speichern.

In Ihrem Beispiel verwenden Sie eine kleine Anzahl lokaler Variablen, ohne die Register zu überbeanspruchen.

Der Vergleich (zum Schleifenende) ist auch ein Hauptnachteil, wenn der Vergleich schwer ist (dh keine testAnweisung), insbesondere wenn er von einer externen Funktion abhängt.

Das Abrollen der Schleife erhöht auch das Bewusstsein der CPU für die Verzweigungsvorhersage, aber diese treten trotzdem auf.

LiraNuna
quelle