Automatische Vektorisierung
Automatisch Vektorisation, in Parallele Computing, ist ein Sonderfall von automatisch Parallelisierung, wo ein Computer Programm wird aus a konvertiert Skalar Implementierung, die ein einzelnes Paar von verarbeitet Operanden zu einer Zeit zu a Vektor Implementierung, die einen Vorgang auf mehreren Operanden gleichzeitig verarbeitet. Zum Beispiel moderne konventionelle Computer, einschließlich Spezialisation Supercomputer, normalerweise haben Vektoroperationen das gleichzeitig Operationen wie die folgenden vier Ergänzungen ausführt (über Simd oder SPMD Hardware):
Jedoch in den meisten Programmiersprachen Man schreibt typischerweise Schleifen, die nacheinander Ergänzungen vieler Zahlen durchführen. Hier ist ein Beispiel für eine solche Schleife, geschrieben in C:
zum (i = 0; i < n; i++) c[i] = a[i] + b[i];
Eine vektorisierende Compiler verwandelt solche Schleifen in Sequenzen von Vektoroperationen. Diese Vektoroperationen führen Ergänzungen für Elementblöcke aus den Arrays durch a
, b
und c
. Die automatische Vektorisierung ist ein wichtiges Forschungsthema in der Informatik.
Hintergrund
Frühe Computer hatten normalerweise eine Logikeinheit, die jeweils jeweils eine Anweisung auf einem Operanden ausführte. Computersprachen und Programme wurden daher so konzipiert, dass sie nacheinander ausgeführt wurden. Moderne Computer können jedoch viele Dinge gleichzeitig tun. Viele optimierende Compiler führen also eine automatische Vektorisierung durch, bei der Teile von sequentiellen Programmen in parallele Operationen umgewandelt werden.
Schleifenvektorisierung Transformiert prozedurale Schleifen, indem Sie jedem Operandenpaar eine Verarbeitungseinheit zuweisen. Programme verbringen die meiste Zeit in solchen Schleifen. Daher kann die Vektorisierung sie erheblich beschleunigen, insbesondere über große Datensätze. Schleifenvektorisierung wird in implementiert Intel's MMX, Sse, und Avx, in Macht ISA's Altivec, und in ARM's NEON, Sve und SVE2 -Anweisungssätze.
Viele Einschränkungen verhindern oder behindern die Vektorisierung. Manchmal kann die Vektorisierung die Ausführung verlangsamen, beispielsweise wegen von Pipeline Synchronisation oder Datenbewegungszeitung. Schleifenabhängigkeitsanalyse identifiziert Schleifen, die vektorisiert werden können, und stützt sich auf die Datenabhängigkeit der Anweisungen in Schleifen.
Garantien
Automatische Vektorisierung, wie alle Schleifenoptimierung oder andere Kompilierungszeitoptimierung muss das Programmverhalten genau bewahren.
Datenabhängigkeiten
Alle Abhängigkeiten müssen während der Ausführung respektiert werden, um falsche Ergebnisse zu verhindern.
Im Allgemeinen sind Schleife invariante Abhängigkeiten und lexikalisch vorwärts abhängige Kann leicht vektorisiert werden, und lexikalisch rückständige Abhängigkeiten können in lexikalisch vorwärts abhängige Abhängigkeiten umgewandelt werden. Diese Transformationen müssen jedoch sicher durchgeführt werden, um sicherzustellen, dass die Abhängigkeit zwischen Alle Aussagen Bleiben Sie dem Original treu.
Zyklische Abhängigkeiten müssen unabhängig von den vektorisierten Anweisungen verarbeitet werden.
Datenpräzision
Ganze Zahl Präzision (Bitgröße) muss während der Ausführung der Vektoranweisung aufbewahrt werden. Die korrekte Vektoranweisung muss basierend auf der Größe und dem Verhalten der inneren Ganzzahlen ausgewählt werden. Außerdem muss bei gemischten Ganzzahltypen zusätzliche Vorsicht darauf geachtet werden, sie korrekt zu fördern/herabzusetzen, ohne Präzision zu verlieren. Besonderes Sorgfalt muss bei Zeichenerweiterung (Weil mehrere Ganzzahlen in demselben Register gepackt sind) und während des Schaltvorgangs oder während des Operationen mit Teile tragen Das würde sonst berücksichtigt werden.
Schwimmpunkt Präzision muss ebenfalls aufbewahrt werden, es sei denn IEEE-754 Die Einhaltung wird ausgeschaltet, in diesem Fall wird die Operationen schneller sein, aber die Ergebnisse können geringfügig variieren. Große Variationen, auch wenn IEEE-754 ignoriert wird, bedeuten normalerweise den Programmiererfehler.
Theorie
Um ein Programm zu vectorisieren, muss der Optimierer des Compiler die Abhängigkeiten zwischen Aussagen zuerst verstehen und gegebenenfalls erneut ausgerichtet werden. Sobald die Abhängigkeiten abgebildet sind, muss der Optimierer die implementierenden Anweisungen ordnungsgemäß ordnen, um geeignete Kandidaten in Vektoranweisungen zu ändern, die mit mehreren Datenelementen arbeiten.
Aufbau der Abhängigkeitsgrafik
Der erste Schritt besteht darin, die zu bauen Abhängigkeitsgrafik, identifizieren, welche Aussagen von anderen Aussagen abhängen. Dies beinhaltet die Untersuchung jeder Anweisung und die Identifizierung jedes Datenelements, auf die die Anweisung zugreift, die Aufgaben zu den Aufgaben zu den Array -Zugriffsmodifikatoren zugeordnet und die Abhängigkeit von jeder Zugriff in allen anderen in allen Aussagen überprüft. Alias -Analyse Kann verwendet werden, um zu zertifizieren, dass die verschiedenen Variablen zugreifen (oder sich überschneiden). Die gleiche Region im Speicher.
Das Abhängigkeitsgraphen enthält alle lokalen Abhängigkeiten mit einer Entfernung, die nicht größer als die Vektorgröße ist. Wenn das Vektorregister 128 Bit beträgt und der Array-Typ 32 Bit beträgt, beträgt die Vektorgröße 128/32 = 4. Alle anderen nicht-cyklischen Abhängigkeiten sollten die Vektorisierung nicht ungültig machen, da es keinen gleichzeitigen Zugriff auf den gleichzeitigen Zugriff gibt Gleiche Vektoranweisung.
Angenommen, die Vektorgröße ist die gleiche wie 4 INTs:
zum (i = 0; i < 128; i++) { a[i] = a[i-16]; // 16> 4, sicher zu ignorieren a[i] = a[i-1]; // 1 <4, bleibt im Abhängigkeitsgraf }
Clustering
Mit dem Diagramm kann der Optimierer dann das gruppieren stark verbundene Komponenten (SCC) und separate vektorisierbare Aussagen aus dem Rest.
Betrachten Sie beispielsweise ein Programmfragment, das drei Anweisungsgruppen in einer Schleife enthält: (SCC1+SCC2), SCC3 und SCC4, in dieser Reihenfolge, in der nur die zweite Gruppe (SCC3) vektorisiert werden kann. Das endgültige Programm enthält dann drei Schleifen, eine für jede Gruppe, wobei nur das mittlere eins vektorisiert ist. Der Optimierer kann sich dem ersten nicht mit dem letzten anschließen, ohne die Ausführungsauftrag zu verletzen, was die erforderlichen Garantien ungültig machen würde.
Redewendungen erkennen
Einige nicht offene Abhängigkeiten können auf der Grundlage bestimmter Redewendungen weiter optimiert werden.
Beispielsweise können die folgenden Selbstdatenabhängigkeiten aufgrund des Wertes der rechten Handwerte vektorisiert werden (Werte (rechte Hand) (RHS) werden abgerufen und dann auf dem linken Wert gespeichert, sodass sich die Daten in der Zuordnung nicht ändern.
a[i] = a[i] + a[i+1];
Selbstabhängigkeit durch Skalare kann durch vektorisiert werden durch Variable Eliminierung.
Rahmenbedingungen
Der allgemeine Rahmen für die Schleifenvektorisierung ist in vier Stufen aufgeteilt:
- Auftakt: Wo die Loop-unabhängigen Variablen vorbereitet sind, um innerhalb der Schleife verwendet zu werden. Dies beinhaltet normalerweise, sie in Vektorregister mit spezifischen Mustern zu verschieben, die in Vektoranweisungen verwendet werden. Dies ist auch der Ort, an dem die Abhängigkeitsprüfung für Laufzeit eingesetzt werden kann. Wenn der Scheck entscheidet, dass die Vektorisierung nicht möglich ist, verzweigen Sie zu Aufräumen.
- Schleife (en): Alle vektorisierten (oder nicht) Loops, getrennt durch SCCS -Cluster in der Reihenfolge des Erscheinens im ursprünglichen Code.
- Postlude: Geben Sie alle schleifenunabhängigen Variablen, Induktionen und Reduktionen zurück.
- Aufräumen: Implementieren Sie einfache (nicht geprüfte) Schleifen für Iterationen am Ende einer Schleife, die nicht ein Vielfaches der Vektorgröße oder für die Laufzeitprüfungen der Vektorverarbeitung sind.
Laufzeit vs. Compile-Zeit
Einige Vektorisierungen können nicht zur Kompilierungszeit vollständig überprüft werden. Beispielsweise können Bibliotheksfunktionen die Optimierung besiegen, wenn die von ihnen verarbeitenden Daten vom Anrufer geliefert werden. Selbst in diesen Fällen kann die Laufzeitoptimierung noch Schleifen im Fliege vektorisieren.
Diese Laufzeitprüfung wird in der durchgeführt Auftakt Stage und lenkt den Fluss nach Möglichkeit auf vektorisierte Anweisungen, andernfalls kehrt abhängig von den Variablen, die an die Register oder Skalarvariablen weitergegeben werden, in die Standardverarbeitung zurück.
Der folgende Code kann leicht zum Kompilierungszeit vektorisiert werden, da er keine Abhängigkeit von externen Parametern hat. Außerdem garantiert die Sprache, dass keiner von beiden dieselbe Region im Gedächtnis wie jede andere Variable einnimmt, da sie lokale Variablen sind und nur in der Ausführung leben Stapel.
int a[128]; int b[128]; // initialisieren b zum (i = 0; i<128; i++) a[i] = b[i] + 5;
Andererseits enthält der folgende Code keine Informationen zu Speicherpositionen, da die Referenzen lauten Zeiger und das Gedächtnis, auf den sie zeigen, kann sich überlappen.
Leere berechnen(int *a, int *b) { int i; zum (i = 0; i < 128; i++, a++, b++) *a = *b + 5; }
Eine schnelle Laufzeitprüfung der die Anschrift von beiden a und b, plus der Schleifen -Iterationsraum (128) reicht aus, um festzustellen, ob sich die Arrays überlappen oder nicht, wodurch Abhängigkeiten enthüllt.
Es gibt einige Tools, mit denen vorhandene Anwendungen dynamisch analysiert werden können, um das inhärente latente Potenzial für SIMD -Parallelität zu bewerten, die durch weitere COMPILER -Fortschritte und/oder durch manuelle Codeänderungen ausgenutzt werden können.[1]
Techniken
Ein Beispiel wäre ein Programm, um zwei Vektoren numerischer Daten zu multiplizieren. Ein skalarer Ansatz wäre so etwas wie:
zum (i = 0; i < 1024; i++) c[i] = a[i] * b[i];
Dies könnte vektorisiert werden, um ungefähr so auszusehen:
zum (i = 0; i < 1024; i += 4) c[i:i+3] = a[i:i+3] * b[i:i+3];
Hier repräsentiert C [i: i+3] die vier Array -Elemente von C [i] bis c [i+3], und der Vektorprozessor kann vier Vorgänge für einen einzelnen Vektoranweisungen ausführen. Da die vier Vektoroperationen ungefähr zur gleichen Zeit wie eine Skalaranweisung abgeschlossen sind, kann der Vektoransatz bis zu viermal schneller laufen als der ursprüngliche Code.
Es gibt zwei unterschiedliche Compiler -Ansätze: eine basierend auf der konventionellen Vektorisierungstechnik und der andere basierend auf Schlaufe abrollen.
Automatische Vektorisierung auf Schleifenebene
Diese Technik, die für herkömmliche Vektormaschinen verwendet wird, versucht, SIMD -Parallelität auf Schleifenebene zu finden und zu nutzen. Es besteht aus zwei wichtigen Schritten wie folgt.
- Finden Sie eine innerste Schleife, die vektorisiert werden kann
- Transformieren Sie die Schleife und generieren Sie Vektorcodes
Im ersten Schritt sucht der Compiler nach Hindernissen, die die Vektorisierung verhindern können. Ein großes Hindernis für die Vektorisierung ist wahre Datenabhängigkeit kürzer als die Vektorlänge. Andere Hindernisse sind Funktionsaufrufe und kurze Iterationszahlen.
Sobald die Schleife als vektorisierbar bestimmt ist, wird die Schleife durch die Vektorlänge gestreift und jede Skalaranweisung innerhalb des Schleifenkörpers durch die entsprechende Vektoranweisung ersetzt. Im Folgenden werden die Komponententransformationen für diesen Schritt unter Verwendung des obigen Beispiels angezeigt.
- Nach dem Streifen
zum (i = 0; i < 1024; i += 4) zum (j = 0; j < 4; j++) c[i+j] = a[i+j] * b[i+j];
- Nach der Schleifenverteilung unter Verwendung von temporären Arrays
zum (i = 0; i < 1024; i += 4) { zum (j = 0; j < 4; j++) ta[j] = A[i+j]; zum (j = 0; j < 4; j++) TB[j] = B[i+j]; zum (j = 0; j < 4; j++) TC[j] = ta[j] * TB[j]; zum (j = 0; j < 4; j++) C[i+j] = TC[j]; }
- Nach dem Ersetzen durch Vektorcodes
zum (i = 0; i < 1024; i += 4) { Va = vec_ld(&A[i]); VB = vec_ld(&B[i]); VC = vec_mul(Va, VB); vec_st(VC, &C[i]); }
Automatische Vektorisierung der Blockebene auf Blockebene
Diese relativ neue Technik richtet sich speziell an moderne SIMD -Architekturen mit kurzen Vektorlängen.[2] Obwohl Loops abgerollt werden können, um die SIMD -Parallelität in Grundblöcken zu erhöhen, nutzt diese Technik die SIMD -Parallelität innerhalb grundlegender Blöcke und nicht in Schleifen. Die beiden Hauptschritte sind wie folgt.
- Die innerste Schleife wird durch einen Faktor der Vektorlänge abgerollt, um einen großen Schleifenkörper zu bilden.
- Isomorphe skalare Anweisungen (die dieselbe Operation ausführen) werden in einen Vektorunterricht verpackt, wenn Abhängigkeiten dies nicht verhindern.
Um schrittweise Transformationen für diesen Ansatz zu zeigen, wird das gleiche Beispiel erneut verwendet.
- Nach dem Abschluss der Schleife (durch die Vektorlänge, die in diesem Fall 4 angenommen wurde)
zum (i = 0; i < 1024; i += 4) { SA0 = ld(&A[i+0]); SB0 = ld(&B[i+0]); SC0 = SA0 * SB0; st(SC0, &C[i+0]); ... sa3 = ld(&A[i+3]); SB3 = ld(&B[i+3]); SC3 = sa3 * SB3; st(SC3, &C[i+3]); }
- Nach dem Packen
zum (i = 0; i < 1024; i += 4) { (SA0, Sa1, Sa2, sa3) = ld(&A[i+0:i+3]); (SB0, SB1, SB2, SB3) = ld(&B[i+0:i+3]); (SC0, SC1, SC2, SC3) = (SA0, Sa1, Sa2, sa3) * (SB0, SB1, SB2, SB3); st((SC0, SC1, SC2, SC3), &C[i+0:i+3]); }
- Nach Codegenerierung
zum (i = 0; i < 1024; i += 4) { Va = vec_ld(&A[i]); VB = vec_ld(&B[i]); VC = vec_mul(Va, VB); vec_st(VC, &C[i]); }
Hier repräsentieren SA1, SB1, ... skalare Variablen und VA, VB und VC repräsentieren Vektorvariablen.
Die meisten vektorisierenden kommerziellen Compiler verwenden den herkömmlichen Ansatz auf Loop-Ebene mit Ausnahme des IBM XL-Compilers.[3] was beides verwendet.
In Gegenwart des Kontrollflusss
Das Vorhandensein von If-Statements in der Schleifenkörper erfordert die Ausführung von Anweisungen in allen Kontrollpfaden, um die mehreren Werte einer Variablen zu verschmelzen. Ein allgemeiner Ansatz besteht darin, eine Folge von Code -Transformationen zu durchlaufen: Prädikation → Vektorisation (unter Verwendung einer der oben genannten Methoden) → Vektorprädikate entfernen → Skalarprädikate entfernen.[4] Wenn der folgende Code als Beispiel verwendet wird, um diese Transformationen anzuzeigen;
zum (i = 0; i < 1024; i++) wenn (A[i] > 0) C[i] = B[i]; anders D[i] = D[i-1];
- Nach der Prädikation
zum (i = 0; i < 1024; i++) { P = A[i] > 0; Np = !P; C[i] = B[i]; (P) D[i] = D[i-1]; (Np) }
wobei (p) ein Prädikat bezeichnet, das die Aussage bewacht.
- Nach der Vektorisierung
zum (i = 0; i < 1024; i += 4) { VP = A[i:i+3] > (0, 0, 0, 0); VNP = vec_not(VP); C[i:i+3] = B[i:i+3]; (VP) (NP1, NP2, NP3, NP4) = VNP; D[i+3] = D[i+2]; (NP4) D[i+2] = D[i+1]; (NP3) D[i+1] = D[i]; (NP2) D[i] = D[i-1]; (NP1) }
- Nach dem Entfernen von Vektorprädikaten
zum (i = 0; i < 1024; i += 4) { VP = A[i:i+3] > (0, 0, 0, 0); VNP = vec_not(VP); C[i:i+3] = vec_sel(C[i:i+3], B[i:i+3], VP); (NP1, NP2, NP3, NP4) = VNP; D[i+3] = D[i+2]; (NP4) D[i+2] = D[i+1]; (NP3) D[i+1] = D[i]; (NP2) D[i] = D[i-1]; (NP1) }
- Nach dem Entfernen von Skalarprädikaten
zum (i = 0; i < 1024; i += 4) { VP = A[i:i+3] > (0, 0, 0, 0); VNP = vec_not(VP); C[i:i+3] = vec_sel(C[i:i+3], B[i:i+3], VP); (NP1, NP2, NP3, NP4) = VNP; wenn (NP4) D[i+3] = D[i+2]; wenn (NP3) D[i+2] = D[i+1]; wenn (NP2) D[i+1] = D[i]; wenn (NP1) D[i] = D[i-1]; }
Reduzierung der Vektorisierungsaufwand in Gegenwart des Kontrollflusss
Die Ausführung der Anweisungen in allen Kontrollpfaden im Vektorcode war einer der Hauptfaktoren, die den Vektorcode in Bezug auf die Skalarbasis verlangsamen. Je komplexer der Kontrollfluss wird und je mehr Anweisungen im Skalarcode umgangen werden, desto größer wird der Vektorisierungsaufwand. Um diesen Vektorisierungsaufwand zu verringern, können Vektorzweige eingesetzt werden, um Vektoranweisungen zu umgehen, ähnlich wie die Skalaranweisungen von Skalaren umgehen.[5] Im Folgenden werden Altivec -Prädikate verwendet, um zu zeigen, wie dies erreicht werden kann.
- Skalarbasis (Originalcode)
zum (i = 0; i < 1024; i++) { wenn (A[i] > 0) { C[i] = B[i]; wenn (B[i] < 0) D[i] = E[i]; } }
- Nach der Vektorisierung in Gegenwart des Kontrollflusss
zum (i = 0; i < 1024; i += 4) { VPA = A[i:i+3] > (0, 0, 0, 0); C[i:i+3] = vec_sel(C[i:i+3], B[i:i+3], VPA); vt = B[i:i+3] < (0,0,0,0); VPB = vec_sel((0, 0, 0, 0), vt, VPA); D[i:i+3] = vec_sel(D[i:i+3], E[i:i+3], VPB); }
- Nach dem Einsetzen von Vektorzweigen
zum (i = 0; i < 1024; i += 4) { wenn (vec_any_gt(A[i:i+3], (0, 0, 0, 0))) { VPA = A[i:i+3] > (0,0,0,0); C[i:i+3] = vec_sel(C[i:i+3], B[i:i+3], VPA); vt = B[i:i+3] < (0, 0, 0, 0); VPB = vec_sel((0, 0, 0, 0), vt, VPA); wenn (vec_any_ne(VPB, (0, 0, 0, 0))) D[i:i+3] = vec_sel(D[i:i+3], E[i:i+3], VPB); } }
Im endgültigen Code mit Vektorzweigen sind zwei Dinge zu beachten. Zunächst ist die Prädikatdefinitionsanweisung für VPA auch im Körper des äußeren Vektorzweigs mit Vec_any_gt enthalten. Zweitens hängt die Rentabilität des inneren Vektorzweigs für VPB von der bedingten Wahrscheinlichkeit von VPB mit falschen Werten in allen Feldern ab, da VPA in allen Feldern falsche Werte aufweist.
Betrachten Sie ein Beispiel, bei dem der äußere Zweig in der Skalarbasis immer aufgenommen wird, wobei die meisten Anweisungen im Schleifenkörper umgehen. Der obige Zwischenfall ohne Vektorzweige führt alle Vektoranweisungen aus. Der endgültige Code mit Vektorzweigen führt sowohl den Vergleich als auch den Zweig im Vektormodus aus und gewinnt möglicherweise die Leistung über die Skalarbasis.
Manuelle Vektorisierung
In den meisten C und C ++ Compiler können verwendet werden Intrinsische Funktionen manuell vektorisieren, auf Kosten der Programmiererbemühungen und -wartbarkeit.
Siehe auch
Verweise
- ^ Holewinski, Justin; Ramamurthi, Ragavendar; Ravishankar, Mahesh; Fauzia, Naznin; Pouchet, Louis-Noël; Rountev, Atanas; Sadayappan, P. (6. August 2012). "Dynamische Spurenanalyse des Vektorisierungspotentials von Anwendungen". ACM Sigplan nennt. 47 (6): 371–382. doi:10.1145/2345156.2254108.
- ^ Larsen, S.; Amarasinghe, S. (2000). "Parallelität der Superwortebene mit Multimedia -Anweisungssätzen ausnutzen". Proceedings der ACM Sigplan -Konferenz zum Entwurf und der Implementierung von Programmiersprache. ACM Sigplan nennt. 35 (5): 145–156. doi:10.1145/358438.349320. HDL:1721.1/86445.
- ^ "Codeoptimierung mit IBM XL -Compilern" (PDF). Juni 2004. archiviert von das Original (PDF) Am 2010-06-10.
- ^ Shin, J.; Hall, M. W.; Chame, J. (2005). "Parallelität auf Superwortebene in Gegenwart des Kontrollflusss". Verfahren des internationalen Symposiums zur Erzeugung und Optimierung von Code. S. 165–175. doi:10.1109/cgo.2005.33. ISBN 0-7695-2298-x.
- ^ Shin, J. (2007). "Einführung des Kontrollflusss in den vektorisierten Code". Verfahren der 16. Internationalen Konferenz über parallele Architektur- und Zusammenstellungstechniken.S. 280–291. doi:10.1109/pact.2007.41.