Einige Iterationen für die vektorisierte Restschleife zurückspringen

8

Ich vektorisiere eine Schleife von Hand und bearbeite 4 Elemente gleichzeitig. Die Gesamtzahl der Elemente darf nicht ein Vielfaches von 4 sein, daher habe ich am Ende meiner Hauptschleife noch ein paar Dinge übrig. Ich dachte, dass ich die Restposten mit der gleichen vektorisierten Hauptschleife machen könnte, wenn der Zählwert größer als 4 ist und einige Artikel noch sicher sind. Zum Beispiel, wenn ich 10 Elemente verarbeiten muss, könnte ich 0123, 4567, 6789 in drei Iterationen verarbeiten. Ich konnte keine Hinweise auf diese Technik finden. Ist es dumm, aber ich sehe nicht wie?

%Vor%     
Bruno Martinez 17.11.2017, 14:52
quelle

1 Antwort

5

Wenn Ihre Eingabe und Ausgabe nicht überlappen und es sicher ist, das gleiche Element mehrmals neu zu verarbeiten, ist diese allgemeine Idee großartig. Dies ist häufig der Fall, wenn die Ausgabe schreibgeschützt ist. z.B. out[i] = pure_func(in[i]) ist idempotent, aber out[i] += func(in[i]) nicht. Reductions wie sum += in[i] sind auch weniger zugänglich.

Wenn es verwendbar ist, ist es oft besser als eine Skalar-Cleanup-Schleife.

Wenn es nicht ganz so einfach ist, sehen Sie sich @Paul Rs Kommentare und die damit verbundene Frage an: Vektorisieren mit nicht ausgerichteten Puffern: mit VMASKMOVPS: Erzeugen einer Maske aus einer Fehlausrichtung? Oder nicht mit dem insn überhaupt (TL: DR: eigentlich vmaskmovps zu benutzen ist normalerweise nicht gut, aber andere masking und unaligned-load tricks sind.)

Ihre spezifische Implementierung (die gleiche Schleife wiederverwendbar zu machen, um den letzten Vektor zu überlappen) endet damit, dass die innere Schleife ziemlich schlecht wird mit clang, wobei i+8 und i+4 innerhalb jeder inneren Schleifen-Iteration ausgeführt werden.

gcc schafft eine etwas weniger schlechte innere Schleife, aber es ist immer noch weniger effizient als es mit gcc7.2 -O3 -mtune=haswell (asm Ausgabe auf Godbolt) sein könnte. Die innere Schleife hat einen zusätzlichen Aufwand, weil sie die alte i jedes Mal speichert, wenn i += 4 ausgeführt wird, weil CSE zwischen dieser und der i+4 in der Schleifenbedingung und / oder i = count - 4 außerhalb der Schleife liegt. (gcc ist manchmal ziemlich dumm, zusätzliche Arbeit in eine innere Schleife zu legen, anstatt eine Operation danach neu zu berechnen oder rückgängig zu machen.)

Quelle + asm auf dem Godbolt Compiler Explorer (für das Original und eine verbesserte Version (siehe unten)) .

%Vor%

Die Verwendung eines indizierten Adressierungsmodus tut SSE2 nicht besonders weh, ist aber bei AVX nicht gut. AVX erlaubt nicht ausgerichtete Speicheroperanden für jede Anweisung (außer vmovdqa ), so dass der Compiler die Ladung in vpaddd xmm0, xmm1, [rdi+rax*4] falten kann, wenn Sie mit -march=haswell bauen. Aber das kann nicht sogar auf Haswell micro-fuse , so ist es immer noch 2 ups für das Front-End.

Wir können die inneren Schleifen von clang und gcc mit i <= count - 4 korrigieren. Wir wissen count >= 4 an diesem Punkt, count - 4 wird niemals zu einer großen Zahl springen. (Beachten Sie, dass i + 4 umbrechen und somit eine Endlosschleife erzeugen könnte, wenn count innerhalb von 4 des Maximalwerts für den Typ liegt. Das hat wahrscheinlich dazu geführt, dass der Klang so schwer ist und zu verpassten Optimierungen führt).

Nun erhalten wir eine identische innere Schleife von gcc7.2 und clang5.0 (beide mit -O3 -march=haswell ). (Wörtlich identisch, sogar mit den gleichen Registern, nur einen anderen Markennamen.)

%Vor%

Dies sind 5 Fused-Domain-Ups auf Haswell, die also höchstens eine Iteration pro 1.25 Clocks ausführen können, Engpässe am Front-End, nicht beim Laden, Speichern oder SIMD paddd -Durchsatz. (Es kann Engpässe bei der Speicherbandbreite über große Eingänge verursachen, aber das Abrollen mindestens ein bisschen ist in der Regel sogar für diesen Fall eine gute Sache.)

clang hätte sich bei der Auto-Vektorisierung für Sie ausgegraben und AVX2 verwendet, so dass Sie durch das manuelle Vektorisieren in diesem Fall, bei dem der Compiler es leicht machen kann, tatsächlich schlechter werden. (Es sei denn, Sie erstellen mit gcc -O2 , wodurch die automatische Vektorisierung nicht aktiviert wird).

Sie können tatsächlich ein Beispiel dafür sehen, wie sich clang automatisch vektorisiert, weil es die Bereinigungsschleife vektorisiert, aus irgendeinem Grund nicht realisierend, dass es niemals mit count >= 4 laufen kann. Ja, es prüft, ob count > 3 und springt zur manuell vektorisierten Schleife, dann prüft es, ob es 0 ist, prüft dann, ob es > 32 ist und springt zu einer automatisch vektorisierten Version der Aufräumschleife ... / facepalm. )

Das Zurückspringen in die Hauptschleife ist meistens nicht kompatibel mit dem Abrollen in der C-Quelle und wird wahrscheinlich das Abrollen des Compilers verhindern.

Wie oben erwähnt, ist das Abrollen der inneren Schleife normalerweise ein Gewinn.

In asm könntest du Dinge so einrichten, dass du nach einer abgerollten Schleife und dann vielleicht wieder für einen nicht ausgerichteten endgültigen Vektor in die innere Schleife für die letzten 1 oder 2 Vektoren zurückspringen kannst.

Dies kann jedoch für die Verzweigungsvorhersage schlecht sein. Der Loop-Zweig wird wahrscheinlich als stark vorhergesagt vorhergesagt, könnte also bei jedem Sprung in die Schleife falsch vorhersagen. Separater Bereinigungscode wird dieses Problem nicht haben, wenn es also ein echtes Problem ist, dann wird separater Bereinigungscode (Duplizieren des inneren Schleifenkörpers) besser sein.

Sie können die innere Schleifenlogik normalerweise in eine Inline-Funktion einfügen, die Sie mehrfach in einer nicht abgerollten Schleife und einmal in einem Vektorblock für die Bereinigung / nicht ausgerichteten Vektor verwenden können.

Obwohl Ihre Idee nicht die innere Schleife in diesem Fall verletzt, wenn Sie es sorgfältig tun, ist die Menge an zusätzlichen Anweisungen und zusätzliche Verzweigung vor / nach der inneren Schleife wahrscheinlich mehr als Sie mit einem einfacheren Ansatz erhalten würden Aufräumen. Also wieder, das könnte nützlich sein, wenn der Schleifenkörper sehr groß ist, aber in diesem Fall sind es nur ein paar Anweisungen und billiger zu duplizieren als um herum zu verzweigen.

Ein effizienter Weg zur Lösung des gleichen Problems besteht in einem möglicherweise überlappenden letzten Vektor . Daher gibt es keine bedingte Verzweigung, die davon abhängt, ob die Elementanzahl eine ganze Anzahl von vollständigen Vektoren ist oder nicht. Der letzte Vektor muss unausgerichtete Lade- / Speicheranweisungen verwenden, da Sie seine Ausrichtung nicht kennen.

Auf modernen x86 (Intel seit Nehalem, AMD seit Bulldozer, siehe Agner Fogs Guides ), unausgerichtete load / store-Anweisungen auf Zeigern sind zur Laufzeit tatsächlich ausgerichtet haben keine Strafe. (Anders als bei Core2, wo movdqu immer langsamer ist, selbst wenn die Daten tatsächlich ausgerichtet sind.) IDK, was die Situation auf ARM, MIPS oder anderen SIMD-Architekturen ist.

Sie können dieselbe Idee verwenden, um einen möglicherweise nicht ausgerichteten ersten Vektor (der sich nicht überschneidet, wenn er tatsächlich ausgerichtet ist) zu handhaben und dann die Hauptschleife dazu zu verwenden, ausgerichtete Vektoren zu verwenden. Mit zwei Zeigern könnte einer jedoch relativ zu dem anderen falsch ausgerichtet sein. Der übliche Hinweis (aus dem Intel Optimierungshandbuch) ist, den Ausgabezeiger (durch den Sie speichern) auszurichten.

Sie können und sollten __restrict für beide Zeiger verwenden. Kein Grund, es nicht zu tun, außer es kann etwas anderes aliasieren. Wenn ich nur eines machen würde, würde ich es für den Ausgabezeiger verwenden, aber es kann beides bedeuten.

    
Peter Cordes 17.11.2017 21:30
quelle

Tags und Links