Wie bekannt CPU ist Pipeline, und es funktioniert am effizientesten, wenn die Reihenfolge der Befehle unabhängig voneinander - dies als ILP (Instruction-Level Parallelism) bekannt: Ссылка
Aber gibt es ein wirklich funktionierendes Beispiel, das die Vorteile von ILP zeigt, zumindest synthetisches Beispiel für CPU x86_64 (aber für die gleiche Menge an cmp
/ jne
in beiden Fällen ) ?
Ich werde das folgende Beispiel schreiben - addiere alle Elemente des Arrays, aber es zeigt keine Vorteile von ILP: Ссылка
Ergebnis:
seq: 0,100000 s, Res: 1000000000, IPL: 0,110000 sec, schneller 0,909091 X, Res: 1000000000
seq: 0.100000 sec, Res: 1000000000, IPL: 0.100.000 sec, schneller 1.000000 X, Res: 1000000000
seq: 0,100000 s, Res: 1000000000, IPL: 0,110000 sec, schneller 0,909091 X, Res: 1000000000
seq: 0.100000 sec, Res: 1000000000, IPL: 0.100.000 sec, schneller 1.000000 X, Res: 1000000000
seq: 0,110000 Sekunden, Res: 1000000000, IPL: 0,110000 Sekunden, schneller 1,000000 X, Res: 1000000000
seq: 0,100000 s, Res: 1000000000, IPL: 0,110000 sec, schneller 0,909091 X, Res: 1000000000
seq: 0,100000 s, Res: 1000000000, IPL: 0,110000 sec, schneller 0,909091 X, Res: 1000000000
seq: 0,110000 Sekunden, Res: 1000000000, IPL: 0,100000 Sekunden, schneller 1.100000 X, Res: 1000000000
seq: 0,110000 Sekunden, Res: 1000000000, IPL: 0,100000 Sekunden, schneller 1.100000 X, Res: 1000000000
seq: 0,110000 Sekunden, Res: 1000000000, IPL: 0,120000 Sekunden, schneller 0,916667 X, Res: 1000000000
schneller AVG: 0.975303
ILP sogar ein bisschen langsamer als Sequential.
C-Code: Ссылка
%Vor%UPDATE:
Compiler-Befehlszeile:
%Vor%Zusätzliche Hinweise zur Antwort:
Das einfache Beispiel, das die Vorteile von ILP zwischen Unroll-loop und Unroll-loop + ILP für ein Array von 50000000 Doppelelementen zeigt: Ссылка
schneller AVG: 1.152778
xmm0
verwendet, das dann zum Ergebnis xmm6
beiträgt kann verwendet werden Umbenennen registrieren : xmm6
verwendet, dh es kann nicht Registrierung umbenennen : Bei den meisten Intel-Prozessoren dauert es drei Zyklen, um eine Gleitkomma-Addition durchzuführen. Aber es kann bis zu 1 / Zyklus aufrechterhalten, wenn sie unabhängig sind.
Wir können ILP leicht demonstrieren, indem wir einen Gleitkomma-Add auf den kritischen Pfad setzen.
Umgebung:
-O2
Stellen Sie sicher, dass der Compiler keine unsicheren Fließkomma-Optimierungen durchführt.
%Vor%Ausgabe:
%Vor%Beachten Sie, dass der Unterschied fast genau 3x ist. Das liegt an der 3-Zyklen-Latenz und dem 1-Zyklus-Durchsatz der Gleitkomma-Addition.
Die sequentielle Version hat sehr wenig ILP, weil alle Gleitkomma-Additionen auf dem kritischen Pfad sind. (Jede Erweiterung muss warten, bis die vorherige Erweiterung abgeschlossen ist.) Die nicht gerollte Version verfügt über 4 separate Abhängigkeitsketten mit bis zu 4 unabhängigen Adds, die alle parallel ausgeführt werden können. Nur 3 werden benötigt, um den Prozessorkern zu sättigen.
Der Unterschied kann auch für ganzzahligen Code sichtbar gemacht werden, zum Beispiel
%Vor%Die Ergebnisse auf meiner 4770K sind etwa 35,9 Millionen TSC-Ticks für die erste vs 11,9 Millionen für die zweite (Mindestzeit über 1k Läufe).
In der ersten ist die Abhängigkeitskette in eax
die langsamste Sache bei 4 Zyklen pro Iteration. Nichts anderes ist von Bedeutung, die Abhängigkeitskette von ecx
ist schneller und es gibt viel Durchsatz, um sie und den Kontrollfluss zu verbergen. 35,9 Millionen TSC-Ticks funktionieren übrigens auf 40 Millionen Zyklen, da der TSC bei der Basistaktrate von 3,5 GHz anschlägt, aber der maximale Turbo bei 3,9 GHz liegt, 3,9 / 3,5 · 35,9 bei etwa 40.
Die Version des zweiten, die ich in den Kommentaren erwähnt habe (4 Akkumulatoren, aber mit [rsp]
, um die Konstante 1 zu speichern), nimmt 17,9, was es 2 Zyklen pro Iteration macht. Das entspricht dem Durchsatz der Speicherlasten, der bei Haswell 2 / Zyklus ist. 4 lädt so 2 Zyklen. Der Schleifenoverhead kann sich immer noch verstecken.
Der zweite wie oben beschrieben benötigt 1.3333 Zyklen pro Iteration. Die ersten vier Adds können zu den Ports 0, 1, 5 und 6 gehen, das add/jnz
fusionierte Paar kann nur zu Port 6 gehen. Wenn man das fusionierte Paar in p6 setzt, bleiben 3 Ports für 4 μops übrig, also 1.3333 Zyklen.
Tags und Links optimization c++ assembly x86 performance