(Weitere Informationen zur Code-Review Fragen ) des Kontexts dieser Schleife.)
Umgebung:
Ich schreibe nicht viel Assembler-Code, und wenn ich es tue, ist es entweder kurz genug oder einfach genug, dass ich mir keine Gedanken darüber machen muss, wie ich die maximale Menge an perfomieren kann. Mein komplexerer Code wird normalerweise in C geschrieben, und ich lasse die Optimierer des Compilers sich Sorgen über Latenz, Codeausrichtung usw. machen.
In meinem aktuellen Projekt macht der MSVC-Optimierer jedoch eine bemerkenswert schlechte Arbeit am Code in meinem kritischen Pfad. Also ...
Ich habe noch kein gutes Werkzeug gefunden, das entweder eine statische oder eine Laufzeitanalyse von x64-Assembler-Code mit dem Ziel durchführt, Verzögerungen zu beseitigen, die Latenz zu verbessern, usw. Alles, was ich habe, ist der VS-Profiler, der mir sagt ) welche Anweisungen die meiste Zeit benötigen. Und die Uhr an der Wand, die mir sagt, ob die letzte Veränderung die Dinge besser oder schlechter gemacht hat.
Als Alternative habe ich mich durch Agners Dokumente geschlichen, in der Hoffnung, etwas mehr aus meinem Code herauszuholen. Das Problem ist, dass es schwer ist, etwas von seiner Arbeit zu verstehen, bis man alles verstanden hat. Aber Teile davon machen Sinn, und ich versuche, das anzuwenden, was ich gelernt habe.
Was hier im Sinn ist, hier ist der Kern meiner innersten Schleife, die (wo nicht überraschend) der VS-Profiler sagt, dass meine Zeit damit verbracht wird:
%Vor%Ja, das ist so ziemlich ein Lehrbuchbeispiel für eine Abhängigkeitskette: Jede Anweisung in dieser engen kleinen Schleife hängt von den Ergebnissen der vorherigen Operation ab. Dies bedeutet, dass es keine Parallelität geben kann, was bedeutet, dass ich den Prozessor nicht voll ausnutzen kann.
Inspiriert von Agners "optimizing assembler" -Dokument habe ich einen Ansatz entwickelt, der es mir (hoffentlich) erlaubt, zwei Operationen gleichzeitig durchzuführen, so dass ich eine Pipeline aktualisieren kann, die ymm2 aktualisiert, und eine weitere Aktualisierung (etwa) ymm8.
Es ist jedoch eine nicht-triviale Veränderung, also frage ich mich, ob es wahrscheinlich helfen wird, bevor ich alles zerreiße. Wenn ich Agners "Anweisungstabellen" für Kaby Lake (mein Ziel) anschaue, sehe ich Folgendes:
%Vor%Vor diesem Hintergrund sieht es so aus, als ob eine Pipeline p0 + p5 für den vptest gegen ymm2 verwendet, eine andere p1, um vpminub und vpsubb für ymm8 zu verwenden. Ja, die Dinge werden immer noch hinter vptest gestapelt, aber es sollte helfen.
Oder würde es?
Ich führe diesen Code derzeit aus 8 Threads (Ja, 8 Threads geben mir einen besseren Gesamtdurchsatz als 4,5,6 oder 7). Da mein i7700k 4 Hyperthread-Cores hat, würde die Tatsache, dass auf jedem Core 2 Threads laufen, bedeuten, dass ich bereits die Ports ausgeschöpft habe? Ports sind "pro Kern", nicht "pro logischer CPU", oder?
Also.
Nach meinem derzeitigen Verständnis von Agners Arbeit scheint es, dass es keinen Weg gibt, diesen Code in seiner jetzigen Form weiter zu optimieren. Wenn ich eine bessere Leistung haben möchte, muss ich einen anderen Ansatz finden.
Und ja, ich bin mir sicher, wenn ich meine ganze asm-Routine hier veröffentlichen würde, könnte jemand einen alternativen Ansatz vorschlagen. Aber der Zweck dieser Frage ist nicht, dass jemand meinen Code für mich schreibt. Ich versuche zu sehen, ob ich anfangen zu verstehen, wie man über die Optimierung von asm-Code nachdenkt.
Ist das (in etwa) der richtige Weg, die Dinge zu betrachten? Fehle ich ein paar Stücke? Oder ist das Flat-out falsch?
TL: DR : Hyperthreading sollte alle Ihre Vektor-ALU-Ports mit 2 Threads pro Kern belasten.
vptest
schreibt kein Vektorregister, nur Flags. Die nächste Iteration muss nicht darauf warten, daher ist ihre Latenz meistens irrelevant.
Nur jnz
ist abhängig von vptest
und spekulative Ausführung + Verzweigungsvorhersage verbirgt die Latenz der Kontrollabhängigkeiten. vptest
Latency ist relevant dafür, wie schnell ein Branch Mispredict erkannt werden kann, aber nicht für den Durchsatz im korrekt vorhergesagten Fall.
Guter Punkt zum Hyperthreading. Das Verschachteln von zwei unabhängigen Dep-Ketten innerhalb eines einzelnen Threads kann hilfreich sein, aber es ist viel schwieriger, es richtig und effizient zu machen.
Sehen wir uns die Anweisungen in Ihrer Schleife an. prevised-take jnz
wird immer auf p6 laufen, also können wir es abwerten. (Abrollen könnte tatsächlich schaden: vorhergesagt-nicht-genommen jnz
kann auch auf p0 oder p6 laufen)
Auf einem Kern selbst sollte Ihre Schleife bei 2 Zyklen pro Iteration laufen, bei Latenz eine Engstelle. Es sind 5 Fused-Domain-Ups, so dass 1,25 Zyklen benötigt werden. (Im Gegensatz zu test
kann jnz
nicht mit vptest
verschmelzen). Beim Hyper-Threading ist das Front-End bereits ein schlimmerer Flaschenhals als die Latenz . Jeder Thread kann in jedem zweiten Zyklus 4 Ups ausgeben, was weniger ist als die 5 Ups in jedem anderen Zyklus des Abhängigkeitsketten-Engpasses.
(Dies ist typisch für aktuelle Intel, besonders SKL / KBL: viele Ups haben genügend Ports zur Auswahl, die 4 Ups pro Clock-Durchsatz realistisch sind, besonders mit SKL's verbessertem Durchsatz des Uop-Caches und Decoders zur Vermeidung von Bubbles aufgrund von Front-End-Einschränkungen und nicht die Auffüllung des Back-End.)
Jedes Mal, wenn ein Thread abstürzt (zB für einen Branch-Mispredict), kann das Front-End den anderen Thread einholen und viele zukünftige Iterationen in den Out-of-Order-Kern bekommen, um es bei einem Iter per durchzukauen 2 Zyklen. (Oder weniger wegen der Durchsatzlimits für den Ausführungsport, siehe unten).
Ausführungs-Port-Durchsatz (unfusioned-domain):
Nur eine von 5 Ups läuft auf p6 ( jnz
). Es kann kein Engpass sein, da die Ausgabequote des Frontends uns beim Ausführen dieser Schleife auf weniger als einen Zweig pro Takt begrenzt.
Die anderen 4 Vektor-ALUs pro Iteration müssen auf den 3 Ports mit Vektorausführungseinheiten laufen. Die P01- und P015-Ups verfügen über genügend Planungsflexibilität, sodass kein einzelner Port ein Engpass sein kann. Daher können wir nur den Gesamtdurchsatz der ALU betrachten. Das sind 4 Ups / Iter für 3 Ports, für einen maximalen durchschnittlichen Durchsatz für einen physischen Kern von einem Iter pro 1.333 Zyklen.
Für einen einzelnen Thread (kein HT) ist dies nicht der größte Engpass. Aber mit zwei Hyperthreads, das ist ein Iter pro 2.6666 Zyklen.
Hyperthreading sollte Ihre Ausführungseinheiten sättigen, wobei ein gewisser Frontend-Durchsatz übrig bleibt . Jeder Thread sollte einen Durchschnittswert von 2.666c haben, wobei das Frontend bei einem pro 2.5c ausgegeben werden kann. Da die Latenzzeit nur einen Wert pro Sekunde 2c erreicht, kann sie nach Verzögerungen auf dem kritischen Pfad aufgrund von Ressourcenkonflikten aufholen. (a vptest
uop stehlen einen Zyklus von einem der beiden anderen ups).
Wenn Sie die Schleife ändern können, um weniger häufig oder mit weniger Vektorups zu überprüfen, könnte das ein Gewinn sein. Aber alles was ich denke, ist more vector uops (zB vpand
anstelle von vptest
und dann vpor
ein paar dieser Ergebnisse zusammen vor dem Überprüfen ... Oder vpxor
zu produzieren ein All-Zero-Vektor, wenn vptest
würde). Vielleicht, wenn es einen Vektor XNOR oder etwas gab, aber es ist nicht.
Um zu überprüfen, was tatsächlich passiert, können Sie per Leistungsindikatoren Ihren aktuellen Code profilieren und sehen, welchen Durchsatz Sie für einen ganzen Kern erzielen (nicht nur für jeden einzelnen logischen Thread). Oder profile einen logischen Thread und sieh nach, ob er etwa die Hälfte von p015 sättigt.
Eine Teilantwort:
Intel bietet ein Tool namens Intel Architecture Code Analyzer (beschrieben hier ), die statische Analyse von Code durchführt und zeigt, (welche Art von Ports) in einem Abschnitt von asm-Code verwendet werden.
Leider:
Aber vielleicht am wichtigsten (für meine Bedürfnisse):
Wenn sich die Implementierungsdetails zwischen Prozessoren ändern, macht das die gesamte Ausgabe verdächtig. Die Daten in der PDF-Datei deuten darauf hin, dass v2.3 im Juli 2017 veröffentlicht wurde, was bedeutet, dass ich wahrscheinlich noch etwas auf die nächste Veröffentlichung warten muss.
Tags und Links assembly performance x86-64 micro-optimization avx2