Dieser Artikel wurde maschinell übersetzt.

C++ AMP

Einführung in Tiling in C++ AMP

Daniel Moth

Dieser Artikel behandelt eine Vorabversion Technologie namens C++ AMP, das ausgeliefert wird mit Visual Studio 11.Alle Angaben sind freibleibend.

Visual Studio 11 bringt Unterstützung für heterogene computing Mainstream durch C++ Accelerated Massive Parallelism (C++ AMP).Ein weiterer Artikel in dieser Ausgabe, die ich Voraussetzung lesen zu diesem Artikel halte C++ AMP eingeführt.Also wenn Sie noch nicht gelesen haben es noch nicht, tun Sie dies bitte, starten auf p.28.

Bevor ich Sie in die GPU-Performance-Optimierung Technik namens "Kacheln" Programmierung einführen, denken Sie daran, dass Sie im vorhergehenden Artikel über Index <N>, soweit <N>, restrict(amp) und Array_view < T, N > Parallel_for_each gelernt.Sie sind in der Lage, die C++ AMP-API verwenden, um eigene Daten paralleler Algorithmen implementieren, wie z. B. die Matrixmultiplikation im vorhergehenden Artikel gemeinsam genutzt und hier in wiederholt Abbildung 1.

Abbildung 1-Matrix-Multiplikation, einfaches Modell

1  void MatMul(vector<int>& vC, const vector<int>& vA,
     const vector<int>& vB, int M, int N, int W )
2  {
3    array_view<const int, 2> a(M, W, vA), b(W, N, vB);
4    array_view<int, 2> c(M, N, vC);
5    c.discard_data();
6    parallel_for_each(c.extent, [=](index<2> idx) restrict(amp)
7    {
8      int row = idx[0]; int col = idx[1];
9      int sum = 0;
10     for(int i = 0; i < b.extent[0]; i++)
11       sum += a(row, i) * b(i, col);
12     c[idx] = sum;
13   });
14   c.synchronize();
15 }

Wenn Sie nicht vertraut mit der Matrixmultiplikation, ist hier eine online-Referenz: Bit.ly/HiuP.

In diesem Artikel werde ich stellen Ihnen Fliesen, ist die am häufigsten verwendete Optimierungstechnik bei der Programmierung von GPUs. Der einzige Grund, Fliesen in Ihr Algorithmus einzuführen ist die zusätzliche Leistung, die Sie möglicherweise durch Wiederverwendung von Daten und besseren Speicherzugriff Muster erreichen. Fliesen können Sie besser nutzen den GPU-Speicher-Hierarchie auf einem niedrigeren Niveau als was Sie mit dem einfachen Modell tun können, die Sie aus den einleitenden Artikel kennen.

Es gibt zwei Schritte, um Fliesen. Zuerst müssen Sie explizit die Berechnung Fliese (dieser erste Schritt geschieht für Sie unter die Decke mit dem einfachen Modell); Zweitens müssen Sie die Vorteile der Tile_static Speicher (in diesem zweite Schritt geschieht nicht für Sie automatisch, so Sie es explizit tun müssen selbst).

Tiled_extent Klasse

Sie wissen, dass Parallel_for_each eine Umfang-Objekt als erstes Argument akzeptiert. Das Ausmaß beschreibt die Compute-Domäne – das heißt, wie viele threads (Größe) und welche Form (Dimensionalität) die Berechnung ausgeführt wird. Betrachten Sie die folgenden zwei Maße-Beispiele:

extent<1> e(12);   // 12 threads in a single dimension
extent<2> ee(2,6); // 12 threads in two-dimensional space

Es gibt eine gekachelte Überlastung Parallel_for_each, die eine Tiled_extent-Argument akzeptiert. Eine Tiled_extent beschreibt, wie das ursprüngliche Ausmaß in gleich große Fliesen brechen. Sie können eine Tiled_extent für bis zu Rang nur drei erhalten. Wenn Sie mehr Dimensionen als die haben, müssen Sie halten mit dem einfachen Modell oder umgestalten von Code. Die Gesamtzahl der Threads in einer Kachel darf 1.024 nicht überschreiten.

Der einfachste Weg, ein Tiled_extent-Objekt zu erhalten, wird durch Aufrufen der parameterlosen vorlagenbasierten Kachel-Methode auf, die eine Tiled_extent für das Ausmaß-Objekt zurückgibt. Für die beiden früheren Umfang Beispiele schreiben Sie etwas wie z. B. die folgenden entsprechenden Tiled_extent Objekte zu erhalten:

tiled_extent<6> t_e = e.tile<6>();        // e.rank==t_e.rank
tiled_extent<2,2> t_ee = ee.tile<2, 2>(); // ee.rank==t_ee.rank

Eine bildliche Darstellung finden Sie unter Abbildung 2, aus denen hervorgeht, wie Fliesen weit die Daten in kleinere Teilmengen partitioniert die in C++ AMP Fliesen bezeichnet werden.

tiled_extent Is an Extent Partitioned into Tiles
Abbildung 2 Tiled_extent ist ein Umfang in Kacheln aufgeteilt

Die Zahlen, die Sie wählen sich: als Vorlage-Argumente zum Zeitpunkt der Kompilierung bekannt sein müssen. Sie müssen die Dimensionen des globalen Umfang an die Parallel_for_each übergeben gleichmäßig aufteilen:

  • e [0] = 12 ist durch t_e.tile_extent[0]=6 teilbar
  • EE [0] = 2 ist teilbar durch t_ee.tile_extent[0]=2
  • EE [1] = 6 ist teilbar durch t_ee.tile_extent[1]=2

Aus Performance-Gründen die kleinste Fliese Größe in den am wenigsten -­wesentliche Dimension sollte 16. Die Kachelgröße Stimmung kann besser oder schlechter Leistung, je nach Hardware Ergebnissen, die Sie verwenden. Mein Rat ist, versuchen Sie nicht zu tun — wählen Sie stattdessen eine Zahl, die ebenso führt auch über eine breite Palette von Hardware, beginnend mit 16 und sogar ein Vielfaches davon.

Tiled_index Klasse

Sie wissen, dass in den Aufruf von Parallel_for_each Sie in einen Lambda-Ausdruck mit Ihrem Code als zweites Argument übergeben. Code erhält ein Index-Objekt, und Sie können sich vorstellen das Index-Objekt als Thread-ID. Zum Beispiel:

parallel_for_each(my_extent,[=](index<2> idx) restrict(amp){
  my_array_view[idx] = ...
});

Wenn Sie das Ausmaß, das Sie an die Parallel_for_each übergeben Ziegel, nimmt die Signatur des Lambda-Ausdrucks, die Sie übergeben ein Tiled_index. Ein Tiled_index besteht aus vier Index-Objekten. Beispielsweise können Sie noch auf das Index-Objekt erhalten, die Sie erwartet hatten, indem Sie eine Eigenschaft für das Tiled_index-Objekt wie folgt:

parallel_for_each(my_extent.tile<2,2>(),[=](tiled_index<2,2> t_idx) restrict(amp){
  my_array_view[t_idx.global] = ...
});

Wenn gekachelte Algorithmen zu schreiben, möchten Sie wahrscheinlich wissen, den lokalen Index in die Kachel (nicht nur die global Index in der gesamten Domäne Compute). Das Index-Objekt erhalten Sie über die lokalen Eigenschaft der Tiled_index. Für einige Algorithmen ist es nützlich zu wissen, der Index der Kachel in Bezug auf die anderen Fliesen in die Berechnung, und auch der global Index die Fliese Herkunft. Sie können diese Index-Objekte über die Fliesen und Tile_origin Eigenschaften des Tiled_index zugreifen.

Mithilfe den zweidimensionalen Umfang dem obigen Beispiel (Maße <2> (2,6) .tile <2,2> ()), können Sie Abbildung 3 die Werte der oben genannten Tiled_index Eigenschaften für das markierte Quadrat.

tiled_index Properties Returning Index Objects
Abbildung 3 Tiled_index Eigenschaften zurückgeben von Index-Objekten

Matrix Multiplikation mit Tiled Parallel_for_each Revisited

In Abbildung 1 sah Sie eine Matrix-Multiplikation-Implementierung mit dem einfache Modell von C++ AMP. Wie würden Sie dieses Algorithmus ändern, so dass es explizit angeordnet werden kann, mit was Sie bisher gelernt haben (mit Tiled_extent und Tiled_index)?

Die Lösung erscheint Abbildung 4, mit den Änderungen aus der früheren Liste fettgedruckt.

Abbildung 4-Matrix-Multiplikation, Tiled, Schritt 1 ohne Verwendung von tile_static

3  array_view<const int, 2> a(M, W, vA), b(W, N, vB);
4  array_view<int, 2> c(M, N, vC);
5  c.discard_data();
6  parallel_for_each(c.extent.tile<16,16>(),
     [=](tiled_index<16,16> t_idx) restrict(amp)
7  {
8  int row = t_idx.global[0]; int col = t_idx.global[1];
9  int sum = 0;
10 for(int i = 0; i < b.extent[0]; i++)
11   sum += a(row, i) * b(i, col);
12 c[t_idx.global] = sum;
13 });
14 c.synchronize();

In Zeile 6 ich aufgerufen die Ziegel-Methode auf das Ausmaß, Kommissionierung eine Kachelgröße (16 x 16 in diesem Beispiel), und änderte ich den Lambda-Ausdruck einer Tiled_index mit passenden Ziegel Größe Vorlage-Argumente akzeptieren. In der Lambda-Nachrichtentext ersetzte ich alle Idx vorkommen mit t_idx.global (Linien 8 und 12). Diese mechanistische Konvertierung ist was sollten Sie zuerst für alle Ihre Algorithmen der C++ AMP tun wenn Sie sich entscheiden, um sie zu wiederholen. Es ist das erste – aber nicht die einzige — Schritt auf dem Weg aus dem einfachen Modell in das gekachelte Modell.

Eine Sache zu beachten, wie bereits erwähnt, dass mit dieser Änderung ist, müssen Sie sicherstellen, die Kachelgröße gleichmäßig hob der globale Umfang Maße teilt. Mein Beispiel wird davon ausgegangen, quadratische Matrizen wo jeder Dimension durch 16 teilbar ist. Außerdem ist es üblich, die Kachelgröße heraus in eine static const Int-Variable oder ein Vorlage-Argument zu hissen.

Der einfache Matrix Multiplikation Probe in Abbildung 1, das System die Berechnung in Ihrem Namen, hinter den Kulissen Fliesen. Damit es implizit gekachelt wird, anstatt explizit angeordnet, und Sie müssen nicht über die Teilbarkeit Anforderung sorgen. Was das einfache Modell nicht für Sie, und daher Sie selbst tun müssen, ist der notwendige Schritt 2 der Fliesen: ändern den Algorithmus, Tile_static Speicher und in der Regel die Verwendung von einem oder mehreren der anderen Index-Objekte verwenden. Bevor das geht, werfen wir einen Umweg, einige Hardware-Grundlagen zu verstehen.

Kurze Hintergrundinformationen über GPU-Speicher-Hierarchie

Es gibt eine Menge von online-Inhalten, die GPU Hardwaremerkmale erläutert. Diese Inhalte hängt davon ab welche Hardwarehersteller es konzentriert sich auf, und auch welche Hardware-Familie aus jeder Anbieter beschreibt. In diesem Abschnitt bieten eine hochrangige, Kreuz-Hardware und rau (jeder Hardwarehersteller hat seine eigene Terminologie und eigene Feinheiten) Bild von C++ AMP der Perspektive eines Entwicklers.

Grafikprozessoren haben globalen Speicher, in dem die Daten Array und Array_view befindet. Es gibt auch Register für jeden Thread, das ist, wo Ihre lokalen Variablen in der Regel gehen (sofern Ihre Hardware-Treiber andere Ideen hat — beispielsweise, wenn Sie zu viele Register für die Hardware den Code ausführt verwenden, auf, es wird verschütten ihren Inhalt in globalen Speicher). Zugriff auf globalen Speicher ist viel langsamer als der Zugriff auf Register — zum Beispiel dauert eine GPU-Zyklus für Register Zugriff im Vergleich zu 1.000 Zyklen für einen globalen Speicher-Zugriff.

Darüber hinaus haben in der Nähe ihrer Verarbeitung Elemente, GPUs einen kleinen Scratchpad-Speicher (z. B. 16 KB, 48 KB, je nach Hardware). Dies ist viel schneller, als globaler Speicher zuzugreifen; Vielleicht Zyklen 10 pro Zugang, zum Beispiel. Dieser Speicherbereich, auch genannt den lokalen Datenspeicher ist ein programmierbarer Cache. CPU-Caches werden automatisch und transparent für Sie verwaltet, und daher Leistungsvorteile werden automatisch erteilt Ihnen. Im Gegensatz dazu müssen Sie diese GPU-Cache selbst verwalten durch Kopieren von Daten in und aus ihm heraus, daher müssen Sie den Leistungsvorteil abonnieren. Bei einigen Programmiermodellen nennen diese "shared Memory," andere nennen es "lokaler Speicher" und noch andere nennen es "Gruppe freigegebenen Speicher." In C++ AMP heißt dieser programmierbaren Cache "Tile_static Erinnerung" – dazu später mehr.

Als nächstes wollen wir die GPU Hardware-Modell, beginnend mit Speicher Gültigkeitsdauer und Geltungsbereich ein logisches Modell zuordnen.

Ein Stück des globalen Speichers ist zugänglich für alle Threads, und seine Lebensdauer überschreitet, die der Lebensdauer einer Parallel_for_each-Berechnung, so dass ein spätere Parallel_for_each-Aufruf auf denselben Daten arbeiten kann. Ein Register-Wert ist nur von einem Thread zugänglich, und seine Lebensdauer ist, dass der Thread. Eine Teilmenge aller Threads, die in C++ AMP ein Ziegel von Threads aufgerufen wird, ein Stück Tile_static Speicher freigegeben ist und seine Lebensdauer ist, dass der Fliese. Jetzt fangen Sie an, finden Sie unter Warum Sie benötigen, um Ihre Berechnung zu wiederholen: Ohne zu wissen, welche Fliese ein Thread gehört, haben Sie keine Möglichkeit mit dieser schnellen Tile_static Speicher.

Sie können den Tile_static Speicher als sein "gepachtet" die Fliese von Threads den Abschluss die Fliese Ausführung, an welcher Stelle noch ein weiteres Steinchen übernimmt vorstellen. Also, logisch gesehen, Threads aus verschiedenen Fliesen sind im Zusammenhang mit Tile_static Speicher voneinander isoliert und sie erscheinen zu jedem Besitz des Speichers Tile_static.

Mithilfe der neuen Tile_static Speicher-Klasse

Tile_static-Speicherzugriff verwenden Sie die Tile_static-Speicher-Klasse. Dies ist die zweite Erweiterung, die C++ AMP der Sprache C++ (der andere wird zu beschränken macht, die ich in meinem früheren Artikel behandelt).

Sie können nur diese neuen Speicherklasse in restrict(amp) Funktionen, und nur wenn die Parallel_for_each gekachelt wird, und nur für Variablen deklariert innerhalb dieser Funktionsblock. Zeiger und Verweise können nicht Tile_static markiert werden, und alle impliziten Konstruktor/Destruktor der Tile_static-Variablen werden nicht aufgerufen. In der Regel (aber nicht immer) Tile_static Variablen sind Arrays, und sie sind in der Regel proportional zu der Flächengröße, zum Beispiel:

tile_static float t[16][16]; // Access t to access tile static memory

Die gängigste Methode zum Tile_static Speicher nutzen soll im globalen Speicher Bereiche, auf die Sie mehr als einmal Ihre Algorithmus zugreifen. Sie dann kopieren diese Bereiche in den Tile_static Speicher (nur einmal zahlen den Preis des globalen Speicherzugriff) und dann ändern Ihren Algorithmus, die Tile_static Kopie der Daten (Zugriff sehr schnell, immer wieder), statt den Zugriff auf die Daten im globalen Speicher mehrmals zu verwenden. Der programmierbare Cache ist klein, so dass Sie alle Ihre Daten-Array und Array_view Tile_static Variablen nicht kopieren können. Ein gekachelter Algorithmus ist in der Regel komplexer, da es zu umgehen, indem Sie kopieren nur eine Kachel-Größe-Wert von Daten aus dem globalen Arbeitsspeicher.

Bevor Sie ein mehr Real-World Beispiel, die die Technik des vorstehenden Absatzes zeigt betrachten, sehen Sie sich bitte an eine vereinfachende, gekünstelt und fehlerhafte Beispiel konzentriert sich rein auf die Verwendung von Tile_static wo ich versuche, alle Elemente einer Matrix zusammenfassen:

1  static const int TS = 2;
2  array_view<int, 2> av(2, 6, my_vector);
3  parallel_for_each(av.extent.tile<TS,TS>(),
     [=](tiled_index<TS,TS> t_idx) restrict(amp)
4  {       
5    tile_static int t[TS][TS];   
6    t[t_idx.local[0]][t_idx.local[1]] = av[t_idx.global];
7
8    if (t_idx.local == index<2>(0,0)) {
9      t[0][0] = t[0][0] + t[0][1] + t[1][0] + t[1][1];             
10     av[t_idx.tile_origin] = t[0][0];
11   }
12 });
13 int sum = av(0,0) + av(0,2) + av(0,4); // The three tile_origins

Der Fachbegriff für den vorangehenden Code ist: schrecklich. Linien 9 und 13 nutzen die Tatsache, dass die Fliese Größe 2 x 2 = 4 ist und die gesamte Größe 2 x 6 = 12 (daher drei Fliesen), ist beim reale Implementierungen sollten so geschrieben werden, so dass die Kachelgröße ändern oder Gesamtausmaß bedeutet nicht, dass anderer Code ändern. Die Leistung dieses Codes ist auch schrecklich, denn es verfügt über einen naiven Algorithmus und seine Verzweigungen nicht einer Daten-Parallelisierung ist. Gibt es keine Wiederverwendung der Daten in der Algorithmus, also die Verwendung von Tile_static Speicher ist nicht sogar gerechtfertigt. Es gibt auch ein Korrektheit-Fehler, den ich später erwähnen werde. Aber so schrecklich, wie dieser Code ist, es ist einfach genug für jemand ganz neu in Tile_static Speicher um den Code verstehen zu können – das ist alles, was zählt.

In Zeile 6 kopiert jeder Thread in jede Kachel Daten in seinen Tile_static-Speicher aus dem globalen Arbeitsspeicher. In Zeile 9 wird nur der erste Thread für jede Kachel die Ergebnisse für diese Kachel in der ersten Position des Speichers für diesen Ziegel Tile_static Summe. Dann zurück in Zeile 10, dass derselbe Thread (in jede Kachel) das Ergebnis speichern in globalen Speicher. Dann schließt die Berechnung auf das Gaspedal und der CPU-Thread auf der Hostseite auf Linie 13, die drei Summen aus den drei Fliesen in die Variable Summe addiert.

Ort Sie vor einen Korrektheit Fehler, speziell eine Race Condition? Auf zum nächsten Abschnitt der letzte Teil der gefliesten API erfahren hilft die uns beschäftigen, dass der Fehler.

Tile_barrier Klasse

Die Racebedingung im vorherigen Beispiel wird zwischen den Linien 6 und 9. Linie 9, die der Thread mit lokalen Index (0,0) setzt voraus, dass alle Werte in der Variablen Tile_static-t bereits gespeichert sind, gilt die nur, wenn alle anderen Threads in der Fliese Linie 6 bereits durchgeführt haben. Da Threads unabhängig von einander in der Regel ausgeführt, ist das keine Annahme, die Sie vornehmen können.

Was Sie brauchen, hier ist eine Möglichkeit, kodifiziert die Bedingung, die Linie 9 nicht ausgeführt werden muss, bis alle Threads in der Fliese Linie 6 ausgeführt haben. Die Art und Weise Sie diese Einschränkung Ausdrücken wird mithilfe eines Tile_barrier und eine der Wartemethoden aufrufen. Sie können nicht selbst ein Tile_barrier-Objekt erstellen, aber erhalten Sie durch die Barriere-Eigenschaft von der Tiled_index an Ihren Lambda übergeben. Sie können also die Racebedingung beheben, durch Einfügen der folgenden Zeile, nach Zeile 6:

7 t_idx.barrier.wait();

Beachten Sie, dass Sie diese Zeile kurz vor Linie 9 und innerhalb des Bedingungsblockes platziert haben, konnte nicht. Ein Aufruf von tile_barrier::wait muss an einer Stelle erscheint wo alle Threads in einer Kachel erreichen diesen Speicherort oder alle von ihnen werden es umgehen. Wenn die Barriere in solchen Positionen gelegt werden durfte, würde dann wenn ein Thread in einer Kachel das warten, führen Sie nicht das Programm hängen auf diesem Thread warten.

Der Compiler ist in der Lage, viele dieser Rennen Bedingungen für Sie, zu kennzeichnen und denen dies nicht der Fall, der Debugger kann für Sie finden, wenn Sie in Visual Studio 11 GPU Memory Access Ausnahmen unter das Dialogfeld Ausnahmen aus der Debuggen aktivieren | Ausnahmen im Menü.

Beispiel: Gekachelte Matrix-Multiplikation

Erinnern Sie sich das Matrix Multiplikation Beispiel in Abbildung 4? Es ist jetzt Zeit für Teil 2 der Fliesen-Übung, wo Sie Tile_static Speicher auch nutzen werde und zwangsläufig verwenden lokale Indizierung und das Tile_barrier-Objekt.

Vor dem Anzeigen der Lösung, werde ich Sie erinnern, dass auf meinem Laptop-Rechner die einfache C++ AMP-Matrix-Multiplikation ergibt sich eine Leistungssteigerung von mehr als 40-Mal im Vergleich zu den CPU-Seriencode für M = N = W = 1024. Die gekachelten Lösung, wo Sie über zu sehen TS = 16 (also pro Stück 16 x 16 = 256 threads), ist eine zusätzliche zwei Mal schneller als das einfache Modell! Dieser Messung enthält die Datenübertragung, so dass wenn Sie die Daten bereits übertragen und eine Reihe von Berechnungen für die Daten ausgeführt, dann die Kernel-Ausführungszeit viel mehr als zwei Mal schneller als die Variante wäre nicht die Tile_static Speicher verwenden. Also was Komplexität Preis bezahlen Sie für diese Art der Leistungssteigerung?

Der vollständig gekachelte Matrix Multiplikation Code ist dargestellt Abbildung 5.

Abbildung 5-Matrix-Multiplikation, gekachelt Plus verwenden Tile_static Speicher

1  void MatMul(vector<int>& vC, const vector<int>& vA,
     const vector<int>& vB, int M, int N, int W )
2  {
3    array_view<const int,2> a(M, W, vA), b(W, N, vB);
4    array_view<int,2> c(M, N, vC);  
5    c.discard_data();
6
7    parallel_for_each(c.extent.tile<TS,TS>(),
       [=](tiled_index<TS,TS> t_idx) restrict(amp) 
8    {
9      int row = t_idx.local[0]; int col = t_idx.local[1];
10     tile_static int locA[TS][TS], locB[TS][TS];
11     int sum = 0;
12     for (int i = 0; i < a.extent[1]; i += TS) {
13       locA[row][col] = a(t_idx.global[0], col + i);
14       locB[row][col] = b(row + i, t_idx.global[1]);
15       t_idx.barrier.wait();
16
17       for (int k = 0; k < TS; k++)
18         sum += locA[row][k] * locB[k][col];           
19       t_idx.barrier.wait();
20     }
21     c[t_idx.global] = sum;
22   });
23   c.synchronize();
24 }

Während den Code in Abbildung 5Werke für jede Fliese Größe und alle total Extent-Größe (entspricht den Regeln wie oben beschrieben), die am einfachsten zu verstehen, was es tut ist, kleine Zahlen zu verwenden.Kleine Zahlen nicht gut zur Laufzeit ausführen, aber sie helfen uns bekommen einen Griff auf das geschehen auf.Nehmen wir also an Fliesen, die 2-von-2 (TS = 2) und M = 2, N = 6 und W = 4.Diese Konfiguration ist abgebildet Abbildung 6.

Matrix Multiplication Example with 12 Threads in 2-by-2 TilesAbbildung 6 Beispiel für Matrix-Multiplikation mit 12 Fäden 2-von-2 Fliesen

Um den Code zu verstehen, nur eine Fliese, die zweite Kachel (der drei) folgen und nur einem bestimmten Thread: der Thread, der das Ergebnis 160 berechnet (Dies wird hervorgehoben, Abbildung 6— die Hervorhebung enthält die Zeile der Spalte A und die B, dass dieser Thread Zugriff zu benötigt um die Ergebnisse in die Zelle des c zu berechnen).

Betrachten wir den Code der Abbildung 5 und dabei ein Auge auf das hilfreiche Bild der Abbildung 6.

Wenn ich = 0, parallele Überwachungsfenster in Abbildung 7 zeigt die Werte der betreffenden Variablen bis die innere Schleife in Zeile 16.

When i=0, the Values of Other Variables Are Shown in Each Column, Where Each Row Is a ThreadAbbildung 7 Wann ich = 0, werden die Werte der anderen Variablen in jeder Spalte, wo jede Zeile ein Thread ist angezeigt

Wie Sie sehen können, kooperierte die vier Threads der Fliese zum Kopieren von Daten in die beiden Tile_static Arrays LocA und LocB aus Matrizen A und b.Dann, nachdem sie alle an der Barriere auf Zeile 15 trafen, sie die innere Schleife in Zeile 17 eingegeben.Finden Sie unter Abbildung 8 für die relevanten Variablen Werte für beide k = 0 und k = 1.

Still i=0, the Values of Other Variables Are Shown for k=0 and k=1Abbildung 8 noch i = 0, werden die Werte der anderen Variablen angezeigt, für k = 0 und k = 1

An dieser Stelle der Thread Sie sind folgende berechnet eine teilweise Summe von 1 x 4 und in der zweiten Iteration der Variablen k fügt es hinzu 2 x 10, also insgesamt 24 (siehe Abbildung 8).Es trifft dann die andere Threads auf die Barriere in Zeile 19.Sie sind jetzt bereit, eine zweite Iteration auf die äußere Schleife eingehen.Beachten Sie, daß ich die Variable den Wert 2, und Abbildung 9 zeigt die relevanten neue Werte für die Variablen bis die Barriere.

Abbildung 9 wann ich = 2, die Werte der anderen Variablen sind in dieser Tabelle dargestellt

Wieder arbeiteten die vier Threads der Fliese zum Kopieren von Daten in die beiden Tile_static Arrays LocA und LocB aus Matrizen A und B, für Matrix b rechts Matrix a und nach unten verschiebenNachdem sie alle in die Barriere in Zeile 15 treffen, geben sie wieder die innere Schleife in Zeile 17; finden Sie unter Abbildung 10 für die relevanten Variablen Werte für beide k = 0 und k = 1.

An dieser Stelle laut Abbildung 10, Sie sind folgende, Threads hinzugefügt 24 das Produkt 3 x 16 und fügt es weitere Iteration auf das zweite k 4 x 22, machen eine Grand-Gesamt-Summe von 160.Es trifft dann die andere Threads auf die Barriere in Zeile 19.Die Fäden sind nun bereit, eine dritte Iteration auf die äußere Schleife eingehen, aber sie erkennen die Schleifenbedingung nicht mehr erfüllt ist für Variable ich, so sie in Zeile 21 überspringen.Der Thread, dem Sie nach verwendet seiner globalen Index, um den globalen Speicher in C. aktualisieren

Still i=2, the Values of Other Variables Are Shown for k=0 and k=1Abbildung 10 noch i = 2, sind die Werte der anderen Variablen angezeigt, für k = 0 und k = 1

Sie sehen jetzt wie jedes Element der Matrix einmal aus dem globalen Arbeitsspeicher von jeder Thread kopiert und dann wiederverwendet wurde von jedem der anderen Threads in der Fliese.Wie ich bereits erwähnt, kommt die Leistungssteigerung von Daten einmal aus dem globalen Arbeitsspeicher in Tile_static Speicher zu kopieren, und dann das Stück von Daten mehrere Male im Code wiederverwenden.

Damit Sie den Code verstehen können, wählte ich eine Kachelgröße von 4 (TS = 2) sowie kleine Blöcke, aber in echten Code beide von denen größer wäre.Die Performance-Zahlen, die ich zuvor gebaut auf 16 x 16 = 256 gemeinsam Fäden pro Stück, so dass ich war ein Stück von Daten aus dem schnellen Speicher statt Zugriff jedes Mal von den globalen Speicher 256 mal wiederverwenden.Die gefliesten Matrix Multiplikation Implementierung zusätzlich profitiert von besseren Speicherzugriff Muster in der Matrix A (während Matrizen B und c in allen Implementierungen effizient zugegriffen werden), aber Speicher Koaleszenz ist würde den Rahmen dieses Artikels sprengen.

Das ist die Schlussfolgerung der gefliesten Matrixmultiplikation, die Tile_static Speicher nutzt.Wenn Sie mehr Übung gekachelte Algorithmen arbeiten möchten, siehe die Beispiele auf den "Parallele Programmierung in systemeigenem Code"-Teamblog unter bit.ly/xZt05R.

Der Nachteil

In diesem Artikel habe ich Sie auf Fliesen, die häufigste Optimierungstechnik zum Optimieren Ihres C++ AMP-Codes eingeführt.

Sie gelernt, drei neue Klassen von C++ AMP (Tiled_extent, Tiled_index und Tile_barrier), sowie die Tile_static-Speicher-Klasse.Sie wissen, dass Sie mit einer einfachen (nicht explizit gefliesten) Implementierung beginnen können.Dann Sie die Implementierung ändern können, durch Aufrufen der Fliese-Funktion auf dem Umfang Objekt (Kommissionierung eine Kachelgröße) und ändern Ihre Lambda-Signatur, ein Tiled_index (mit der gleichen Fliese Größe Vorlage-Argumente) akzeptieren.

Dass den mechanistischen Schritt 1 abgeschlossen.Für den zweiten und letzten Schritt schrieb Sie Ihr Algorithmus Tile_static Speicher mit entsprechenden Verwendung von Synchronisierung mithilfe eines Tile_barrier zu verwenden.Das ist der kreative Schritt, wo für jeden Algorithmus müssen Sie kommen mit einem brandneuen Lösung.Die einfachen und gefliesten Implementierungen der Matrix-Multiplikation veranschaulichen die Komplexität mit Fliesen eingeführt, und auch warum seine Verwendung führen kann gewinnt.Der Nachteil ist verkaufen, oder nicht abonnieren.

Es ist erwähnenswert, dass diese Art von Cache-fähige Programmierung Geschwindigkeit Gewinne für Ihre CPU-Code führen kann, auch, weil Sie würde helfen, die automatische transparent Cache-Management-System machen einen besseren Job.Auch wird von Compilern für intelligente gekachelte Code zugänglicher für Vektorisierung.

Neben meiner zwei Artikeln, gibt es eine Fülle von C++ AMP Inhalten (Dokumentation, Tipps, Muster, Links zu Videos und Beispiele) auf die parallele Programmierung Teamblog (bit.ly/bWfC5Y), dass ich Sie empfehlen zu lesen.

Daniel Moth ist leitender Program Manager der Microsoft Developer-Abteilung.

Dank der folgenden technischen Experten für die Überprüfung dieses Artikels: Steve Deitz, Yossi Levanoni, Robin Reynolds-Haertle, Stephen Toub und Weirong Zhu