Ein Muss für Skalierung

Going Parallel with C++ (Teil 2)
Kommentare

Die Struktur des Codes bleibt erhalten, es wird lediglich etwas „Dekoration“ um die Schleife gebaut. Noch einfacher wird es durch die Verwendung der parallel_for-Schleife, diese ist speziell für das gezeigte

Die Struktur des Codes bleibt erhalten, es wird lediglich etwas „Dekoration“ um die Schleife gebaut. Noch einfacher wird es durch die Verwendung der parallel_for-Schleife, diese ist speziell für das gezeigte Szenario vorgesehen.

Listing 3

int _tmain(int argc, _TCHAR* argv[])
{
  vector v = get_sudoku_samples();
  const int nFields = v.size();

  critical_section csCout;
  parallel_for(0, nFields, [&csCout, v] (int field) 
  {
    int (* sudoku) [9] = v[field];
    int values = sudoku_getcount(sudoku);
    int solutions = sudoku_solve(sudoku, 0, 0);
    csCout.lock();
    cout << "(" << values << "," << solutions 
         << ")" << endl;
    csCout.unlock();
    delete [] sudoku;
  });

  return 0;
}  

Die Synchronisierung des Zugriffs auf gemeinsam genutzte Ressourcen - im Beispiel anhand cout gezeigt - wird ebenfalls durch Klassen der PPL unterstützt.

Windows Developer

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

Der Concurrency Visualizer

Der Concurrency Visualizer hat bereits in Visual Studio 2010 Einzug gehalten. Er arbeitet mit der PPL zusammen und bietet eine recht anschauliche grafische Darstellung der Parallelisierung einer Anwendung. In Visual Studio 2010 wird der Cuncurrency Analyzer über ANALYZE | LAUNCH PERFORMANCE WIZARD gestartet, die Optionen werden im Dialog "Performance Wizard" auf CONCURRENCY | VISUALIZE THE BEHAVIOR OF A MULTITHREDED APPLICATION festgelegt. In Visual Studio 2012 hat der Concurrency Visualizer einen etwas exponierteren Eintrittspunkt erhalten und wurde bezüglich Geschwindigkeit deutlich optimiert. Hier erfolgt der Start über ANALYZE | CONCURRENCY WIZARD | START WITH CURRENT PROJECT.

Die Arbeitsweise des Tools ist recht einfach: Während der Ausführung der Anwendung werden Profiling-Daten gesammelt, die anschließend ausgewertet und grafisch aufbereitet werden. Dabei liefert der Visualizer drei unterschiedliche Darstellungen, die im Folgenden anhand der parallelisierten Version des Sudoku-Solvers dargestellt werden.

Die CPU-Ausnutzung unterscheidet sich deutlich zwischen sequenzieller und paralleler Ausführung. In Abbildung 1 ist auf den ersten Blick ist zu erkennen, wie die parallelisierte Version sich sofort nach dem Start auf alle logischen Kerne verteilt, die Gesamtlaufzeit beträgt hier ca. 5 s. Das analoge Diagramm für die sequenzielle Ausführung weist hingegen nur einen, dafür um einiges längeren Balken auf (12 s).

Abb. 1: CPU-Ausnutzung, parallel
Abb. 1: CPU-Ausnutzung, parallel

In der Thread-Darstellung in Abbildung 2 wird deutlich, was die PPL dabei für uns leistet. Im gezeigten Beispiel werden 72 Sudoku-Rätsel mit jeweils 1 bis ca. 300 000 Lösungen parallel gelöst. Die PPL stellt fest, dass es nicht sinnvoll ist, tatsächlich 72 Threads zu erstellen, wo nur 8 logische Cores zur Verfügung stehen. Daneben vermittelt die Grafik auch einen raschen Eindruck vom Status der Threads und damit von der Qualität der Parallelisierung. Execution (grün), Synchronization (rot) und Preemption (gelb) wechseln sich ab. Dabei ist Preemption Overhead und daher unerwünscht, Synchronization ein Warten auf eine Systemressource. Im konkreten Beispiel haben wir recht wenig Preemption, und auch die Synchronization hält sich in Grenzen.

Abb. 2: Threads, parallel
Abb. 2: Threads, parallel

Anhand der Cores-Darstellung in Abbildung 3 ist recht schnell zu erkennen, ob Threads auch wiederholt auf demselben Core ausgeführt werden. Das bringt ebenfalls Performance, da die Wahrscheinlichkeit eines Cache-Hits steigt.

Abb. 3: Cores, parallel
Abb. 3: Cores, parallel
Paralleles Debugging

Hinsichtlich der Unterstützung beim Debugging von parallelen Anwendung sind bereits in Visual Studio 2010 einige Merkmale in Richtung PPL integriert, Visual Studio 2012 liefert weitere Neuerungen. Breakpoints lassen sich jederzeit auch in parallel ausgeführten Code setzen, hier geht der Entwickler wie gewohnt vor. Wird ein Breakpoint von einem Thread angesprungen, werden alle Threads angehalten. Über das bereits seit Längerem vorhandene Threads Window werden die aktuellen Threads dargestellt, es kann hier auch der Kontext von einem Thread auf einen anderen umgesetzt werden. Seit Visual Studio 2010 wird in der Spalte Location auch der Call Stack dargestellt, das hilft ungemein beim Navigieren. Dabei genügt es auch, die Maus über eine Location zu bewegen, der Call Stack wird dann als Tooltip dargestellt.

Analog zum Threads Window existiert auch eine Parallel Task Window, in Abbildung 4 dargestellt. Dort werden anstelle von Threads PPL-Tasks dargestellt. Tasks sind nach dem Anlegen im Status Scheduled. Erst wenn sie einem Thread zugeordnet werden, erhalten sie tatsächlich Ausführzeit (Active), zusätzlich wird in dem Parellel Task Window dann der zugeordnete Thread dargestellt. Ein Task kann auch im Status Waiting (wartet auf eine Ressource) oder Deadlock (zwei Tasks blockieren sich gegenseitig) sein.

Abb. 4: Parallel Task Window
Abb. 4: Parallel Task Window

Das Parallel Stack Window bietet beim Debugging eine Orientierungshilfe und erlaubt ein schnelleres Navigieren zwischen den einzelnen Ausführeinheiten. In diesem Fenster kann über eine kleine Auswahlbox links oben zwischen Thread und Task gewechselt werden. Der Inhalt von Variablen wird beim Debuggen über Watch Window oder Locals visualisiert. Ebenfalls kann der Inhalt recht einfach über einen Tooltip ermittelt werden, es wird wie gewohnt einfach die Maus über die entsprechende Variable bewegt. Neu in Visual Studio 2012 ist das Parallel Watch Window. Dieses stellt beim Debugging die Variablen für alle Tasks dar, die die entsprechende Methode durchlaufen haben.

Im Kontext unseres Backtracking-Beispiels wird die Leistungsfähigkeit des Parallel Watch Windows erst richtig deutlich. Abbildung 5 veranschaulicht, dass es bei Rekursion die entsprechenden Variablen über den gesamten Call Stack hinweg darstellt - neben Thread, Task und Rekursionstiefe.

Abb. 5: Parallel Watch bei Rekursion
Abb. 5: Parallel Watch bei Rekursion
Automatische Parallelisierung

Hinsichtlich automatischer Parallelisierung bietet Visual C++ 2012 einige neue Features. Der Vorteil: Der Entwickler und seine Anwendungen profitiert hier ohne weiteres Zutun. So können z. B. Schleifen automatisch parallelisiert werden. Wird der dargestellte Code aus Listing 4 mit der Option /Qpar (Enable Parallel Code Generation) übersetzt, findet das Aufsummieren der Werte unter Nutzung von acht Threads statt. Mit #pragma loop(hint_parallel(n)) kann die Anzahl der gewünschten Threads festgelegt werden. Die Option /Qpar-report:1 veranlasst den Compiler darüber hinaus dazu, beim Übersetzen eine kleine Meldung auszugeben, ob eine Parallelisierung auch tatsächlich umgesetzt werden konnte.

Listing 4

int arr[0x10000];
#pragma loop(hint_parallel(8))
for(int i=1; i<0x10000; i++)
{
  int result = 0;
  for(int j=0; j<0x100000; j++)
    result = result + i + j;
  arr[i] = result;
}  
Automatische Nutzung von Vector Units

Heutige CPUs unterstützen in der Regel Vector Units mit einem erweiterten Befehlssatz, der die Ausführung einer Operation auf mehreren Registern gleichzeitig zulässt. Konkret existieren die Register der CPU dabei mehrfach. In einem Fetch-Zyklus werden aufeinanderfolgende Daten gleichzeitig in alle Parallelregister übertragen. Mit dieser Option kann das Abarbeiten von Arrays proportional zur Anzahl der Parallelregister beschleunigt werden. Die Erkennung von Kandidaten für diese Optimierung funktioniert so stabil, dass sie standardmäßig aktiviert ist und explizit über #pragma loop(no_vector) für einzelne Schleifen deaktiviert werden muss.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -