Ein Muss für Skalierung

Going Parallel with C++ (Teil 3)
Kommentare

Windows Developer

Der Artikel „Ein Muss für Skalierung“ von Thomas Trotzki und Christian Binder ist erstmalig erschienen im Windows Developer 10.2012
GPU-Programmierung mit C++ AMP
Das Prinzip der

Windows Developer

Der Artikel „Ein Muss für Skalierung“ von Thomas Trotzki und Christian Binder ist erstmalig erschienen im Windows Developer 10.2012

GPU-Programmierung mit C++ AMP

Das Prinzip der Parallelregister wurde nicht für CPUs erfunden, sondern eher von der GPU abgeschaut. Eine GPU ist nicht unbedingt direkt mit einer CPU zu vergleichen, sind doch der Aufbau und die Arbeitsweise recht unterschiedlich. So haben GPUs kein Stack-Konzept und daher gewisse Probleme mit rekursiven Methoden. Dafür sind sie aber ausgelegt, sehr schnell die gleichen Dinge auf unterschiedlichen Daten zu machen. Erreicht wird das durch schnellen Speicher und eine massive Anzahl einfacher Cores, die alle die gleichen Befehle ausführen. Die bei GPUs verwendete Abkürzung SIMD (Single Instruction Multiple Data) umschreibt diesen Sachverhalt recht gut. Zur Veranschaulichung: Heutige Grafikkarten kommen mit 0,5-4 GB Speicher daher und verfügen über 256-4096 Cores – da steht eine i7 CPU mit acht Cores weit hinten an. Bei einer richtigen Verteilung unserer Algorithmen auf CPU und GPU sind daher enorme Leistungsverbesserungen zu erzielen. In der Praxis wird das bislang aber nur selten umgesetzt: Shader-Code, andere Compiler und eine dürftige Integration in die Entwicklungsumgebung stellen oftmals eine zu große Hürde für den C++-Programmierer dar.

Mit C++-AMP fällt diese Hürde. Der Programmierer deklariert in seinem Code die Passagen, die auf der GPU ausgeführt werden sollen. Compiler und Linker sorgen für die gesamte Infrastrukur. Konkret werden die als C++-AMP-Code gekennzeichneten Codeteile durch den normalen C++-Compiler extrahiert und daraus HLSL (Shader Code) generiert. Dieser wird durch einen Shader Compiler übersetzt und vom Linker in das Executable integriert. Compiler und Linker sorgen auch dafür, dass entsprechende Stubs, die zur Laufzeit die Übertragung von Daten und Code zwischen CPU und GPU übernehmen, in die Anwendung integriert werden. Hierfür setzt Microsoft auf DirectX, die entsprechenden Mechanismen werden dort bereits seit Langem erfolgreich genutzt.

In Listing 5 wird eine einfache Matrixmultiplikation dargestellt, die für Matrizen der Größe 1024×1024 auf einer CPU ca. 10 s zur Berechnung benötigt. Eine Verteilung auf die Cores der CPU bringt hier natürlich auch bereits eine gewisse Verbesserung. Jedoch werden wir selten mehr als acht Cores antreffen, im Vergleich zur GPU eher dürftig.

Listing 5

template
void mxm_single_cpu(int M, int N, int W, 
  const std::vector<_type>& va, 
  const std::vector<_type>& vb, 
  std::vector<_type>& vresult)
{
  for(int k=0; k v_a(1024*1024);
  std::vector v_b(1024*1024);
  std::vector v_res(1024*1024);

  mxm_single_cpu(1024, 1024, 1024, v_a, v_b, v_res);
}  

Der gezeigte Code lässt sich mit C++-AMP wirklich sehr einfach auf einer GPU zur Ausführung bringen. Wie, zeigt die Codesequenz in Listing 6.

Listing 6

template
void mxm_amp(int M, int N, int W, 
  const std::vector<_type>& va, 
  const std::vector<_type>& vb, 
  std::vector<_type>& vresult)
{
    extent<2> e_a(M, N), e_b(N, W), e_c(M, W);
    array_view av_a(e_a, va); 
    array_view av_b(e_b, vb); 
    array_view<_type, 2> av_c(e_c, vresult);

    parallel_for_each(av_c.extent, [=](index<2> idx)
    restrict(amp)
    {
      _type result = 0.0f;

      for(int i = 0; i < av_a.extent[1]; ++i)
      {
          index<2> idx_a(idx[0], i);
          index<2> idx_b(i, idx[1]);

          result += av_a[idx_a] * av_b[idx_b];
      }

      av_c[idx] = result;
    });

    // explicitly about copying out data
    av_c.synchronize();
}

{
  std::vector v_a(1024*1024);
  std::vector v_b(1024*1024);
  std::vector v_res(1024*1024);

  mxm_amp(1024, 1024, 1024, v_a, v_b, v_res);
}  

Auch hier bleibt die Struktur des Codes erhalten, C++-AMP erfordert ebenfalls nur etwas „Dekoration“ um die Schleife herum. extent, array_view und parallel_for_each kommen aus einer kleinen Klassenbibliothek zu C++-AMP und sorgen für den notwendigen Transfer der Daten von und zur GPU. Restrict(amp) ist die Anweisung an der Compiler, den entsprechenden C++-Block in Shader-Code zu überführen.

Abbildung 6 zeigt das Ergebnis der Bemühung über den Concurrency Visualizer. Das Ergebnis ist so gravierend, dass ganz genau hingeschaut werden muss. Rechts unten im Diagramm sind zwei recht kleine Zacken zu erkennen, das ist die Ausführung auf der GPU. Der riesige, grüne Balken darüber stellt die Ausführung auf der CPU dar. In Zahlen: Aus 10 s Rechnenzeit auf der CPU werden weniger als 100 ms auf der GPU! Und die werden darüber hinaus hauptsächlich für den Transfer der Daten zur und von der GPU benötigt.

Abb. 6: Performancevergleich von CPU und GPU
Abb. 6: Performancevergleich von CPU und GPU

Dass auch hinsichtlich Debugging bei Nutzung von C++-AMP keine größeren Einschränkung in Kauf zu nehmen sind, dafür sorgt ein GPU-Emulator. Der Code wird zwar deutlich langsamer als auf einer physikalischen GPU ausgeführt, aber dafür können wie gewohnt Breakpoints genutzt werden, sogar das Parallel Watch Window funktioniert wird erwartet. Lediglich ein Mixed Debugging von CPU- und GPU-Code wird derzeit leider noch nicht unterstützt, es müssen zwei getrennte Debug-Sessions gestartet werden.

Fazit

Parallelisierung ist Pflicht! Und da ist es beruhigend zu sehen, dass Compiler-Hersteller mit Hochdruck daran arbeiten, dieses Thema für uns Entwickler einfacher zu gestalten. Microsoft hat in Visual Studio 2010 mit der Parallel Pattern Library und einer guten Werkzeugunterstützung bereits gute Vorarbeit geleistet. Diese wird mit Visual Studio 2012 noch weiter abgerundet. „Use Task No Threads“ – vorbei sind die Zeiten von CreateThread & Co, Lambdas machen das Leben leichter!

Mit C++-AMP ist Microsoft ein weiterer, recht großer Wurf gelungen, denn nun wird die GPU für den C++-Programmierer zugänglich. Insbesondere der Aufwand für die Portierung bestehenden C++-Codes auf eine GPU wird damit deutlich minimiert. Bleibt zu wünschen, dass eine große Schar an Programmierern nun endlich die Zeit finden wird, sich um Parallelisierung zu kümmern. Denn auf Ebene der Hardware stehen wir hier sicherlich erst am Anfang, und die aktuelle Lücke sollte geschlossen werden, bevor sie zu groß wird.

Thomas Trotzki ist ALM Consultant bei der artiso AG nahe Ulm und Microsoft-C++-Profi der ersten Stunde. Mit Microsoft C++ und den MFC beschäftigt er sich intensiv seit den ersten Betaversionen zu Microsoft C/C++ 7.0, also bereits vor der Geburtsstunde von Visual C++ und Visual Studio. Technologisch ist er neben C++ und den MFC auch mit COM/DCOM und der gesamten „Managed Welt“ vertraut und hat umfangreiche Expertise im Application Lifecycle Management.
Christian Binder arbeitet als ALM Architect in der Developer Platform & Strategy Group bei Microsoft Deutschland. Er arbeitet seit 1999 bei Microsoft, u. a. als Escalation Engineer, dann als Platform Strategy Manager, und kann so auf umfangreiche Erfahrungen im Bereich Application Development zurückgreifen. Auch war er im Product Development von Microsoft in Redmond tätig, was ihn 2005 zum Thema Application Lifcycle Management gebracht hat.
Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -