OpenMP 4.5: Eine kompakte Übersicht zu den Neuerungen

Know-how  –  0 Kommentare

OpenMP 4.5 ist der nächste Schritt in der Entwicklung von OpenMP, dem Standard für die Shared-Memory-Programmierung. Die neue Version stärkt vor allem die Programmierung von Beschleunigern mit dem Ziel, zu OpenACC aufzuschließen, bringt aber auch einige allgemeine neue Funktionen.

OpenMP 4.0 erschien im Juli 2013 und brachte als größte Neuerung Konstrukte für die Programmierung von Beschleunigern und Coprozessoren. Deren rasante Verbreitung vor allem im Hochleistungsrechnen hat dem OpenMP Language Committee keine Pause gegönnt, verlocken diese Geräte doch mit mehr Rechenleistung bei geringerem Energieverbrauch im Vergleich zu herkömmlichen Prozessoren. Dafür fordern sie aber auch eine spezielle Programmierung ihrer Mikroarchitektur und Berücksichtigung eines getrennten Speichers.

Sowohl auf diesen Geräten als auch auf herkömmlichen Multicore-Prozessoren muss somit immer mehr darum gekämpft werden, Parallelität auszudrücken, um deren Leistung auszunutzen. Um all das zu unterstützen, bringt OpenMP 4.5 eine Reihe neuer Funktionen und Verbesserungen für parallele Programmierung.

Schleifen verteilen leicht gemacht

Parallele Schleifen sind ein, wenn nicht das wichtigste Konstrukt in OpenMP. Mit den Worksharing-Konstrukten for für C/C++ und do für Fortran bietet OpenMP einen denkbar einfachen Weg, eine Schleife in Stücke zu hacken und die einzelnen Teile von den OpenMP-Threads bearbeiten zu lassen. Dennoch gibt es ein paar nervige Einschränkungen, die das Programmieren paralleler Schleifen verkomplizieren. Die vielleicht gravierendste ist, dass Worksharing-Konstrukte nicht in anderen derselben Art enthalten sein dürfen. Man kann also keine parallele Schleife innerhalb einer anderen einbauen, ohne zusätzliche parallele Regionen samt neuen Teams von Threads zu erzeugen.

Das neue Konstrukt taskloop schafft hier Abhilfe, indem es OpenMP-Tasks zur Ausführung nutzt und damit einige dieser Einschränkungen aufhebt.

Als kleiner Einschub sei an dieser Stelle erwähnt, dass OpenMP einen Task als Einheit von Code nebst Datenumgebung versteht. Wird ein task-Konstrukt erreicht, lässt sich der Task entweder direkt ausführen oder aber in eine Warteschlange einreihen und später abarbeiten. Dazu gesellen sich dann noch passende Synchronisationskonstrukte zur Steuerung der Reihenfolge in der Abarbeitung.

Der folgende Code zeigt ein Beispiel, wie sich normale Tasks und eine parallele Schleife mittels des taskloop-Kontruktes verschränkt lassen:

#pragma omp taskgroup
{
#pragma omp task
long_running_task() // kann nebenher ablaufen

#pragma omp taskloop collapse(2) grainsize(500) nogroup
for (int i = 0; i < N; i++)
for (int j = 0; j < M; j++)
loop_body();
}

Der Funktionsaufruf long_running_task() ist ein asynchron gestarteter Task, der sich somit nebenläufig zu der darauf folgenden Schleife ausführen lässt. Die Schachtelbarkeit von Tasks erlaubt es weiterhin, dass in der Funktion loop_body() erneut Tasks erzeugt werden, insbesondere auch mit dem taskloop-Konstrukt. Die collapse-Klausel, die ebenfalls beim for/do-Konstrukt verfügbar ist, instruiert den Compiler, die beiden nachfolgenden Schleifen zu einer zusammenzufassen. Die OpenMP-Implementierung ist für das Verteilen von Tasks auf die Threads zuständig und kann so für Lastverteilung sorgen, falls die Bearbeitung unterschiedlicher Aufgaben unterschiedlich lange dauern. Im Beispiel kann also der Thread, der für die Task-Ausführung des Funktionsaufrufs long_running_task() zuständig war, nach dem Abschluss an der Abarbeitung der Tasks der Schleife teilhaben.

Wie bei OpenMP üblich unterstützt das taskloop-Konstrukt die gewohnten Mittel, die Sichtbarkeit von Daten zu definieren. Das wird in OpenMP Scoping genannt, also das explizite Aufführen von Variablen in den Klauseln shared, private, firstprivate oder lastprivate. Weiterhin versteht das Konstrukt die Klausel nogroup, welche die implizit vorhandene taskgroup um das Konstrukt abschaltet und somit die automatische Synchronisation mit den erzeugten Tasks vermeidet. Die Größe dieser Tasks (Anzahl Iterationen) lässt sich mittels grainsize einstellen. Wer lieber die Anzahl der zu startenden Tasks kontrollieren möchte, kann die num_tasks-Klausel verwenden.

Soll mehr Einfluss auf das Abarbeiten der Tasks genommen werden, wird die mit OpenMP 4.5 neue priority-Klausel interessant: Je höher der angegebene Wert ist, desto höher die Priorität eines Tasks. Dieser Hinweis dient als Empfehlung an die OpenMP-Laufzeitumgebung, Tasks auszuwählen und deren Bearbeitung vorzuziehen, falls zu einem Zeitpunkt mehrere Tasks zur Ausführung bereitstehen. Eine Garantie, aus der Angabe von Prioritäten eine exakte Ausführungsreihenfolge abzuleiten, gibt es aber nicht. Ohne Angabe der Klausel haben alle Tasks die Priorität 0, sie sind also gleichwertig.