OpenMP. Michael Westermann

Ähnliche Dokumente
OpenMP - Threading- Spracherweiterung für C/C++ Matthias Klein, Michael Pötz Systemprogrammierung 15. Juni 2009

Parallel Regions und Work-Sharing Konstrukte

OpenMP. Viktor Styrbul

Einige Grundlagen zu OpenMP

1. Einführung in OpenMP

Parallele Programmierung mit OpenMP

Praktikum: Paralleles Programmieren für Geowissenschaftler

Java 8. Elmar Fuchs Grundlagen Programmierung. 1. Ausgabe, Oktober 2014 JAV8

Threads und OpenMP. Frank Mietke Cluster- & Gridcomputing Frank Mietke 7/4/04

JavaScript. Dies ist normales HTML. Hallo Welt! Dies ist JavaScript. Wieder normales HTML.

Schachtelung der 2. Variante (Bedingungs-Kaskade): if (B1) A1 else if (B2) A2 else if (B3) A3 else if (B4) A4 else A

Modul Entscheidungsunterstützung in der Logistik. Einführung in die Programmierung mit C++ Übung 2

S. d. I.: Programieren in C Folie 4-1. im Gegensatz zu Pascal gibt es in C kein Schlüsselwort "then"

Webbasierte Programmierung

PHP 5.4 ISBN Stephan Heller, Andreas Dittfurth 1. Ausgabe, September Grundlagen zur Erstellung dynamischer Webseiten GPHP54

Schleifenanweisungen

Algorithmen & Programmierung. Steuerstrukturen im Detail Selektion und Iteration

Programmierung mit C Zeiger

Sequentielle Programm- / Funktionsausführung innerhalb eines Prozesses ( thread = Ausführungsfaden )

4. Parallelprogrammierung

Einfache Rechenstrukturen und Kontrollfluss II

GI Vektoren

3. Anweisungen und Kontrollstrukturen

CUDA. Jürgen Pröll. Multi-Core Architectures and Programming. Friedrich-Alexander-Universität Erlangen-Nürnberg Jürgen Pröll 1

Repetitorium Informatik (Java)

VORKURS INFORMATIK EINE EINFÜHRUNG IN JAVASCRIPT

Einführung in die Programmierung Wintersemester 2011/12

Universität Karlsruhe (TH)

Schleifen in C/C++/Java

Prozeduren und Funktionen

Parallele Programmierung mit OpenMP

Projektseminar Parallele Programmierung

Prof. W. Henrich Seite 1

Programmieren mit OpenMP

C.3 Funktionen und Prozeduren

Test (Lösungen) Betriebssysteme, Rechnernetze und verteilte Systeme

(a) Wie unterscheiden sich synchrone und asynchrone Unterbrechungen? (b) In welchen drei Schritten wird auf Unterbrechungen reagiert?

Erweiterung der Aufgabe. Die Notenberechnung soll nicht nur für einen Schüler, sondern für bis zu 35 Schüler gehen:

Anleitung für zwei C++ - Openmp - Beispiele auf der NWZSuperdome

Kapitel 8. Programmierkurs. Methoden. 8.1 Methoden

Objektorientierte Programmierung

Programmieren I + II Regeln der Code-Formatierung

Parallele Systeme. 1 Einführung Durchführung der erweiterten Übung OpenMP... 3

Einstieg in die Informatik mit Java

Kapitel 3: Variablen

Grundlagen der Programmiersprache C++

Inhaltsverzeichnis. Grundbegriffe der C-Programmierung Für den HI-TECH C-Compiler

Programmierkurs Python I

Kapitel 5. Programmierkurs. Kontrollstrukturen. Arten von Kontrollstrukturen. Kontrollstrukturen Die if-anweisung Die switch-anweisung

PROGRAMMIERUNG IN JAVA

Entwurf von Algorithmen - Kontrollstrukturen

OOP und Angewandte Mathematik. Eine Einführung in die Anwendung objektorientierter Konzepte in der angewandten Mathematik

Bei for-schleifen muss man nur immer bedenken, dass die letzte Anweisung immer erst nach der Ausführung der restlichen Anweisungen der Schleife

Programmieren für Wirtschaftswissenschaftler SS 2015

OpenMP am Beispiel der Matrizenmultiplikation

ModProg 15-16, Vorl. 5

Übungen zur Vorlesung Wissenschaftliches Rechnen I. Grundelemente von Java. Eine Anweisung. wird mit dem Wertzuweisungsoperator = geschrieben.

Vorlesung Programmieren

Programmiersprache 1 (C++) Prof. Dr. Stefan Enderle NTA Isny

Variablen in MATLAB. Unterschiede zur Mathematik: Symbolisches und numerisches Rechnen. Skriptdateien. for-schleifen.

1 Organisatorisches. 2 Compilezeit- und Laufzeitfehler. 3 Exceptions. 4 Try-Catch-Finally

Shared-Memory Parallelisierung von C++ Programmen

Aufbau von Klassen. class punkt {...

Einstieg in die Informatik mit Java

Dynamisches Huffman-Verfahren

C++ Grundlagen. ++ bedeutet Erweiterung zum Ansi C Standard. Hier wird eine Funktion eingeleitet

Erwin Grüner

Inhaltsüberblick. I. Grundbegriffe - Objekte und Klassen. Organisatorisches. I. Grundbegriffe - Objektorientierte Konzepte

2.5 Programmstrukturen Entscheidung / Alternative

Probeklausur: Programmierung WS04/05

Schleifen in Javascript

Parallele Programmierung mit OpenMP

MSDN Webcast: Parallelprogrammierung mit der Task Parallel Library für.net (Teil 1) Presenter: Bernd Marquardt

Bedienung von BlueJ. Klassenanzeige

Programmieren I. Kapitel 5. Kontrollfluss

Moderne Betriebssysteme. Kapitel 8. Kapitel 8. Folie: 1. Multiprozessorsysteme. Autor: Andrew S. Tanenbaum

Klausur Informatik Programmierung, Seite 1 von 8 HS OWL, FB 7, Malte Wattenberg

Modellierung und Programmierung 1

Klausurvorbereitung VS1 (Prof. Brecht) (B0rg Edition)

= 7 (In Binärdarstellung: = 0111; Unterlauf) = -8 (In Binärdarstellung: = 1000; Überlauf)

Funktionale Programmiersprachen

Technische Informatik II

Informatik. Studiengang Chemische Technologie. Michael Roth WS 2012/2013. Hochschule Darmstadt -Fachbereich Informatik-

PThreads. Pthreads. Jeder Hersteller hatte eine eigene Implementierung von Threads oder light weight processes

Vorlesung "Verteilte Systeme" Sommersemester Verteilte Systeme. Adreßraum. Rechner. Verteilte Systeme, Sommersemester 1999 Folie 19.

Universität Karlsruhe (TH)

C- Kurs 04 Anweisungen

Domänenmodell: Fadenkommunikation und -synchronisation

Übung zu Grundlagen der Betriebssysteme. 10. Übung

SQL. SQL SELECT Anweisung SQL-SELECT SQL-SELECT

Algorithmen & Programmierung. Ausdrücke & Operatoren (1)

Grundlagen der Programmentwicklung

Parallel Processing in a Nutshell OpenMP & MPI kurz vorgestellt

Objektorientierte Programmierung OOP Programmieren mit Java

3 Variablen. 3.1 Allgemeines. 3.2 Definition und Verwendung von Variablen

Der Aufruf von DM_in_Euro 1.40 sollte die Ausgabe 1.40 DM = Euro ergeben.

Informatik I Übung, Woche 40

Universität Karlsruhe (TH)

Microcontroller Praktikum SS2010 Dipl. Ing. R. Reisch

Transkript:

Westfälische Wilhelms-Universität Münster Ausarbeitung OpenMP im Rahmen des Seminars Parallele und verteilte Programmierung Michael Westermann Themensteller: Prof. Dr. Herbert Kuchen Betreuer: Dipl. Wirt.-Inform. Philipp Ciechanowicz Institut für Wirtschaftsinformatik Praktische Informatik in der Wirtschaft

Inhaltsverzeichnis 1 Einleitung 1 2 Grundlagen 2 2.1 Das OpenMP-Programmiermodell.................... 2 2.2 Modell des gemeinsamen Speichers................... 3 2.3 Vergleich von OpenMP und MPI.................... 4 3 OpenMP-Direktiven 5 3.1 Variablendeklarationen.......................... 5 3.2 Direktiven zur Parallelisierung von Programmbereichen........ 7 3.2.1 parallel-direktive als Basis für parallele Bereiche...... 8 3.2.2 for-direktive zur Parallelisierung von Schleifen........ 10 3.2.3 sections-direktive zur Parallelisierung von Abschnitten... 16 3.3 Anzahl Threads.............................. 17 3.4 Koordination und Synchronisation von Threads............ 18 3.4.1 Kritische Abschnitte....................... 18 3.4.2 Atomare Operationen....................... 19 3.4.3 Synchronisation.......................... 20 3.4.4 Ausführung ausschließlich durch den Master-Thread...... 20 4 Zusammenfassung und Ausblick 21 A Programm-Beispiele 22 A.1 Ausgabe Threadnummern........................ 22 A.2 Primzahlen-Ausgabe, seriell....................... 23 A.3 Primzahlen-Ausgabe, parallel...................... 24 A.4 Primzahlen-Ausgabe, parallel und geordnet............... 25 B Tabelle der Ausführungszeiten 26 Literaturverzeichnis 27 i

Kapitel 1: Einleitung 1 Einleitung Multiprozessor-Desktoprechner oder Rechner mit Multicore-Prozessoren, die im weiteren Verlauf dieser Arbeit einheitlich als Multiprozessor-System bezeichnet werden, werden immer kostengünstiger und häufiger eingesetzt. Solche Systeme stellen parallele Rechenleistung bereit, so dass sie mehrere Threads gleichzeitig bearbeiten können. Um derartige parallele Rechenleistung effizient ausnutzen und somit die Ausführungszeit eines Programms senken zu können, sind jedoch parallele Programmiertechniken erforderlich. Auf Systemen mit gemeinsamem Speicher bietet sich hierfür die Anwendung von OpenMP an. OpenMP, das für Open specifications for Multi Processing steht, ist eine Spezifikation für parallele Programmierung auf Multiprozessor-Systemen mit gemeinsa- mem Speicher. Seit 1997 gemeinschaftlich von verschiedenen Hard- und Softwareherstellern entwickelt, ist es schnell zum Standard für die Parallelisierung von Anwendungen auf Systemen mit gemeinsamem Speicher geworden [Ch01, S. xiii]. OpenMP hält Compiler-Direktiven, Bibliotheksfunktionen und Umgebungsvariablen bereit und stellt Bindings für die Programmiersprachen C, C++ und Fortran zur Verfügung [Qu04, Kap. 17.1]. Diese Sprachen werden um Konstrukte zur Ausführung eines Programms mit unterschiedlichen Daten durch mehrere Threads, zur Aufteilung von Arbeit auf mehrere Threads, zur Synchronisation von Threads und zur Deklaration von gemeinsamen und privaten Variablen der Threads erweitert [RR07, S. 355]. Einzelne Bereiche eines seriellen Programms können durch das Einfügen weniger Direktiven parallelisiert werden (inkrementelles Parallelisieren). Somit ist ein gleichzeitiges Abarbeiten auf mehreren Prozessoren möglich, wodurch die Ausführungszeit erheblich gesenkt werden kann. Die Spezifikationen von OpenMP sind in [Op05] zu finden. Im Rahmen dieser Seminararbeit wird der Fokus auf C und C++ gelegt, das Binding zu Fortran wird nicht weiter betrachtet. In dieser Arbeit werden zunächst in Kapitel 2 das Programmiermodell von Open- MP und das Modell des gemeinsamen Speichers, auf dem OpenMP beruht, erläutert und eine Abgrenzung von OpenMP gegenüber dem Message-Passing-Interfaces (MPI) vorgenommen. In Kapitel 3 werden die wichtigsten Direktiven von OpenMP erläutert. Zunächst wird in Abschnitt 3.1 auf die Variablendeklarationen eingegangen, die beispielsweise festlegen, ob eine Variable für alle Threads gemeinsam ist oder für jeden Thread neu angelegt wird. Im folgenden Abschnitt 3.2, der den Hauptteil dieser Arbeit darstellt, werden die verschiedenen Direktiven zur Parallelisierung eines Programmabschnitts vorgestellt, bevor in Abschnitt 3.3 darauf eingangen wird, wie die Anzahl Threads festgelegt wird. Dem folgen im Abschnitt 3.4 Direktiven, mit denen 1

Kapitel 2: Grundlagen Threads koordiniert und synchronisiert werden können. Zum Abschluss wird in Kapitel 4 die Arbeit zusammengefasst und auf die zukünftige Bedeutung von OpenMP eingegangen. 2 Grundlagen Im Folgenden wird zunächst das auf parallel arbeitende Threads basierende Programmiermodell von OpenMP vorgestellt, das auch als Fork-join-Prinzip bezeichnet wird. Anschließend wird auf das Modell des gemeinsamen Speichers eingegangen, auf dem OpenMP basiert. Im letzten Abschnitt des Grundlagen-Kapitels wird kurz in das Message-Passing-Interface (MPI) eingeführt, bevor auf dessen Unterschiede zu OpenMP näher eingegangen wird. 2.1 Das OpenMP-Programmiermodell Grundlage von OpenMP ist die nebenläufige Abarbeitung von parallelen Bereichen durch parallel arbeitende Threads, die nach einem Fork-join-Prinzip erzeugt und beendet werden [RR07, S. 355]. Hierbei wird das Programm zunächst von nur einem einzigen Thread alleine und sequentiell ausgeführt. Erreicht der Thread bei der Ausführung des Codes einen Programmabschnitt, der mittels OpenMP parallelisiert werden soll, erzeugt der Thread ein Team von Threads, dem er selber als Master-Thread und eine bestimmte Anzahl weiterer Threads angehören. Dieser Vorgang wird als fork bezeichnet, da der Master-Thread zu mehreren Threads aufgegabelt wird. Der parallele Bereich wird von dem gesamten Team von Threads einschließlich des Master-Threads nebenläufig ausgeführt. Hierbei kann die Arbeit explizit auf die einzelnen Threads verteilt werden oder alle Threads des Teams führen den gleichen Programmcode aus. Auf die Varianten der Ausführung wird in Abschnitt 3.2 näher eingegangen. In OpenMP ist es möglich, parallele Bereiche zu verschachteln, also innerhalb eines parallelen Bereiches einen weiteren parallelen Bereich zu starten. Der Thread, der auf den inneren parallelen Bereich trifft, wird zum Master-Thread dieses inneren Bereiches. Liegt keine Verschachtelung von parallelen Bereichen vor, so bleibt die Anzahl der Threads konstant. Am Ende eines parallelen Bereiches werden die dem Team zugehörigen Threads synchronisiert und mit Ausnahme des Master-Threads alle beendet. Nach diesem Vorgang, der als join bezeichnet wird, setzt der Master- Thread die Ausführung alleine und sequentiell fort. Das Fork-join-Prinzip ist in Abb. 1 dargestellt. 2

Kapitel 2: Grundlagen Abbildung 1: Das Fork-join-Prinzip Quelle: In Anlehnung an [Qu04, Figure 17.2] 2.2 Modell des gemeinsamen Speichers OpenMP wurde für die parallele Programmierung auf Multiprozessor-Systemen mit gemeinsamem Speicher entwickelt. Auf einem solchen System teilen sich zwei oder mehr Prozessoren einen unbeschränkten Zugriff auf einen gemeinsamen Arbeitsspeicher. Somit sehen die Threads eines Prozesses den gleichen Adressraum, unabhängig davon, von welchem Prozessor sie bearbeitet werden [Ta03, S. 541]. Eine typische Architektur mit gemeinsamem Speicher ist in Abb. 2 dargestellt. Abbildung 2: Architektur mit gemeinsamem Speicher Quelle: In Anlehnung an [Ta03, S. 540] Der verteilte gemeinsame Speicher (distributed shared memory, DSM) wird ebenfalls von OpenMP unterstützt. Er ist vergleichbar mit einem virtuellen Speichersystem, bei dem sich die Speicherseiten auf physikalisch entfernten Computern befinden. Ein solches System ist dann sinnvoll, wenn die Anzahl an Prozessoren so groß wird, dass die Bandbreite des gemeinsamen Speichers zum Engpass wird. Im weiteren Verlauf dieser Arbeit wird keine Unterscheidung zwischen gemeinsamem Speicher und verteiltem gemeinsamen Speicher gemacht und beides einheitlich als gemeinsamer Speicher bezeichnet. 3

Kapitel 2: Grundlagen 2.3 Vergleich von OpenMP und MPI Neben OpenMP gibt es noch weitere Spezifikationen für parallele Programmierung. Auf Computersystemen mit verteiltem nicht-gemeinsamen Speicher wird häufig das Message-Passing-Interface (MPI) eingesetzt. Dieses stellt einen Standard für den Nachrichtenaustausch zwischen parallel zueinander arbeitenden Prozessen auf verteilten Systemen bereit. Jeder Prozess hat hierbei seine eigenen lokalen Daten und kann sich durch das explizite Verschicken von Nachrichten mit anderen Prozessen austauschen [RR07, S. 207]. MPI und OpenMP haben beide Vor- und Nachteile, so dass situationsbedingt zu entscheiden ist, welches Verfahren sinnvoller einzusetzen ist. Im Folgenden werden MPI und OpenMP anhand einiger Kriterien verglichen: OpenMP ist auf Systeme mit gemeinsamem Speicher begrenzt, während MPI bevorzugt auf Systeme mit verteiltem Speicher angewendet wird, aber auch auf Systeme mit gemeinsamem Speicher einsetzbar ist. Ein OpenMP-Programm wird von einem Prozess mit mindestens einem Thread ausgeführt. Ein MPI-Programm hingegen besteht in der Regel aus mehreren Prozessen, die Nachrichten austauschen können. Die Anzahl der Prozesse entspricht bei MPI in der Regel der Anzahl der Prozessoren des verteilten Computersystems, bei OpenMP richtet sich in der Regel die Anzahl der Threads nach der Anzahl der Prozessoren. Die Kommunikation findet bei MPI mittels Message Passing statt, bei OpenMP hingegen geschieht dies über gemeinsame Variablen der Threads. Bei den unterstützten Programmiersprachen hingegen gibt es keine Unterschiede zwischen OpenMP und MPI, beide stellen Bindings für C, C++ und Fortran bereit. Besteht bereits eine serielle Version eines Programms, so kann dieses durch das Einfügen von OpenMP-Direktiven mit relativ geringem Aufwand parallelisiert werden (inkrementelle Parallelisierung). Im Vergleich hierzu ist die Parallelisierung eines Programms mittels MPI mit erheblichem Aufwand verbunden. Ein weiterer Vorteil von OpenMP ist, dass der Quellcode nicht geändert werden muss, wenn das Programm auf einem System ausgeführt wird, das OpenMP nicht unterstützt. In diesem Fall ignoriert der Compiler die OpenMP-Direktiven und führt das Programm seriell aus. Ein Vorteil von MPI hingegen ist, dass es auf sehr hohe Anzahl von Prozessoren skalierbar ist. MPI und OpenMP können auch kombiniert eingesetzt werden, indem auf einzelnen Multiprozessor-Systemen mit gemeinsamen Speicher OpenMP-Programme laufen, die sich mittels MPI austauschen. 4

3 OpenMP-Direktiven Mit OpenMP können Abschnitte eines Programms parallel von mehreren Threads ausgeführt werden. Die Steuerung der parallelen Abarbeitung von Programmabschnitten wird mittels Compiler-Direktiven umgesetzt. Damit das Programm die OpenMP-Direktiven übersetzen kann, ist zunächst die Datei omp.h durch den Befehl include <omp.h> in das Programm einzubinden. Compiler-Direktiven für OpenMP haben in C und C++ folgende Syntax: #pragma omp <Klausel> Unterstützt der entsprechende Compiler kein OpenMP, so wird diese Zeile ignoriert und das Programm wird mit nur einem Thread seriell ausgeführt. Dem Entwickler wird es somit einfach gemacht, da er den Quelltext eines parallelen Programms nicht modifizieren muss, falls es von einem Compiler ausgeführt werden soll, der OpenMP nicht unterstützt oder bei dem OpenMP deaktiviert ist. OpenMP unterstützt zwei generelle Arten von Parallelisierung: Zum einen ist es möglich, eine Gruppe von Threads zu erzeugen, die alle den gleichen Programmabschnitt nebenläufig zueinander ausführen. Zum anderen stellt OpenMP Direktiven bereit, um die Arbeit auf mehrere Threads zu verteilen. In beiden Fällen wird zu Beginn des parallelen Bereiches die Klausel parallel verwendet, die im Abschnitt 3.2.1 näher erläutert wird. Folgt keine weitere Direktive, führen alle Threads den gleichen Programmcode aus. Sollen die Iterationen einer for-schleife auf mehrere Threads verteilt werden, wird innerhalb des parallelen Bereichs die for-direktive eingesetzt, worauf im Abschnitt 3.2.2 näher eingegangen wird. Neben der Parallelisierung einer Schleife besteht auch die Möglichkeit, dass jeder Thread einen anderen Abschnitt eines Programms nebenläufig zu den anderen Threads ausführt. Dieses wird mit der sections-direktive umgesetzt, die im Abschnitt 3.2.3 ausführlich erklärt wird. 3.1 Variablendeklarationen In einem parallelen OpenMP-Programm benutzen mehrere Threads einen gemeinsamen Speicher. Auf diese Weise können alle Threads auf die gleichen gemeinsamen Variablen zugreifen, wodurch ein Austausch zwischen den Threads ermöglicht wird, indem ein Thread der Variable einen Wert zuweist und ein anderer Thread diesen Wert ausliest. Variablen können auch als private Variablen deklariert werden. In diesem Fall wird für jeden Thread eine eigene Instanz der Variable im Thread-eigenen Speicher angelegt, die alle vom gleichen Typ und der gleichen Größe sein müssen. Diese privaten Variablen können für jeden Thread einen anderen Wert annehmen 5

und nur der zugehörige Thread kann auf diese zugreifen. Ob eine Variable als gemeinsame oder als private Variable deklariert wird, hängt nicht nur davon ab, ob die Threads über diese Variable miteinander kommunizieren, sondern insbesondere auch vom Aufbau des parallelen Bereichs. Wird beispielsweise eine Variable als Indexvariable einer parallel ausgeführten Schleife eingesetzt, so muss diese Indexvariable privat sein, da sie in jedem Thread einen anderen Wert annimmt. Diese Entscheidung muss sorgfältig getroffen werden, da eine falsche Deklaration von Variablen zu den häufigsten Fehlern in parallelen Programmen mit gemeinsamem Speicher gehört [Ch01, S. 48]. Ob eine Variable in einem parallelen Bereich für alle Threads gemeinsam oder privat ist, wird zu Beginn des parallelen Bereiches festgelegt. Dieses wird mittels Parametern, die der entsprechenden Direktive übergeben werden, realisiert. Bei Angabe des Parameters shared(liste der Variablen ) werden die in der Liste enthaltenen Variablen als gemeinsame Variablen deklariert, so dass eine Referenz auf diese Variable in verschiedenen Threads auf die gleiche Instanz im gemeinsamen Speicher verweist. So wird auch eine Änderung der Variable auf der globalen Instanz im gemeinsamen Speicher ausgeführt, sodass der neue Wert für alle Threads verfügbar ist. Wenn für jeden Thread eine eigene Instanz einer Variable erzeugt werden soll, so wird der Direktive der Parameter private(liste der Variablen ) übergeben, wobei Liste der Variablen alle Variablen enthält, für die dies gelten soll. Auch wenn vor Erreichen des parallelen Bereichs womöglich eine gleichnamige Variable existiert hat, sind die privaten Variablen, im Gegensatz zu den gemeinsamen Variablen, zu Beginn des parallelen Bereiches nicht initialisiert und ihre Werte somit undefiniert. Von der fehlenden Initialisierung zu Beginn des parallelen Bereichs gibt es allerdings Ausnahmen: Zum Einen ist die Indexvariable einer parallelisierten for-schleife bereits initialisiert. In jedem Thread hat die Indexvariable zu Beginn einen anderen Wert, je nachdem welche Iteration dieser Thread ausführt (siehe Abschnitt 3.2.2). Zum Anderen werden in C++ private Variablen, die vom Typ einer Klasse oder Struktur sind und einen Konstruktor haben, beim Anlegen für die einzelnen Threads initialisiert, indem der Konstruktor aufgerufen wird. Ist hingegen explizit erwünscht, dass die privaten Variablen mit dem Wert initialisiert werden, den die Variablen beim Erreichen des parallelen Bereich hatten, so wird statt des private-parameters der Parameter firstprivate(liste der Variablen ) verwendet, in dessen Liste die Variablen einzutragen sind, für die eine derartige Initialisierung vorgenommen werden soll. Eine Variable, die während eines parallelen Bereichs als private Variable deklariert war, ist nach Beenden dieses Abschnitts undefiniert, um Konsistenz zwi- 6

schen serieller und paralleler Ausführung des Abschnitts zu erreichen. Allerdings ist es auch in diesem Fall möglich, dass eine im parallelen Bereich als privat deklarierte Variable nach Beenden dieses Bereichs ihren Wert behält. Dafür wird anstatt des private-parameters der Parameter lastprivate(liste der Variablen ) verwendet. Des Weiteren besteht die Möglichkeit, eine Variable als Reduktionsvariable zu deklarieren. In diesem Fall erhält jeder Thread zunächst eine private Kopie der Variable. Am Ende des Abschnitts, zu der die Direktive gehört, wird die ursprüngliche Variable mit den Kopien der entsprechenden Variablen aller Threads entsprechend der Reduktionsoperation verknüpft. Ist der Operator beispielsweise +, so werden alle Werte der Kopien auf den ursprünglichen Wert hinzu addiert und das Ergebnis dieser Addition der ursprünglichen Variable als Wert zugewiesen. Um Variablen als Reduktionsvariablen zu deklarieren, ist der Direktive der Parameter reduction(op : Liste der Variablen ) zu übergeben, wobei op {+,,, &,ˆ,, &&,. Die privaten Kopien der Variablen werden mit dem neutralen Element des angegebenen Operators initialisiert. Für die Operatoren +,, &,,ˆ und ist das neutrale Element der Wert 0, für die Operatoren und && der Wert 1. [Op05, S. 79f.] Neben den beschriebenen Varianten der Variablendeklaration besteht noch die Möglichkeit, Variablen als threadprivate oder copyin zu deklarieren, worauf in dieser Arbeit nicht näher eingegangen wird. Jeder Variable darf, mit Ausnahme einer gleichzeitigen Zuweisung von firstprivate und lastprivate, nur eine Variablen-Deklaration zugewiesen werden. Des Weiteren ist zu beachten, dass einzelnen Elementen eines Arrays oder einer Struktur keine Deklaration zugewiesen werden kann, sondern nur dem kompletten Konstrukt. Wird eine Variable in keiner der Deklarationen erwähnt, wird sie standardmäßig als gemeinsame Variable deklariert. Eine Ausnahme hiervon bildet die bei der Aufteilung einer Schleife auf mehrere Threads erzeugte Index-Variable der Schleife, die standardmäßig als private Variable deklariert wird. Soll ein explizites Deklarieren jeder im parallelen Bereich verwendeten Variable erzwungen werden, so wird der Direktive zusätzlich der Parameter default(none) hinzugefügt. 3.2 Direktiven zur Parallelisierung von Programmbereichen In diesem Abschnitt werden die Direktiven, mit denen einzelne Bereiche eines Programms parallelisiert werden können, erläutert. Zunächst wird auf die parallel- Direktive eingegangen, bei deren Aufruf Threads erzeugt werden, die den darauf folgenden Anweisungsblock parallel ausführen. Sie ist Grundlage für die anschließend 7

erklärten Direktiven, die die Arbeit auf mehrere Threads verteilen. Die for-direktive verteilt die Iterationen einer Schleife auf mehrere Threads (siehe Abschnitt 3.2.2), mit der sections-direktive können unabhängige Abschnitte von je einem Thread parallel zueinander ausgeführt werden (siehe Abschnitt 3.2.3). 3.2.1 parallel-direktive als Basis für parallele Bereiche Mit Hilfe der parallel-direktive wird ein Abschnitt eines Programms von mehreren Threads gleichzeitig ausgeführt. Folgt keine Direktive, die die Arbeit auf mehrere Threads verteilt, führen alle Threads den gleichen Programmtext in replizierter Art aus. Dieses wird auch als SPMD (single-program multiple-data) bezeichnet [RR07, S. 356]. 3.2.1.1 Syntax Die generelle Syntax des parallel-konstruktes sieht folgendermaßen aus: #pragma omp parallel [Parameter [Parameter]...] { Anweisungsblock Welche Ausdrücke als Parameter für die parallel-direktive einsetzbar sind, ist in Tabelle 1 abzulesen. Direktive shared(liste der Variablen) private(liste der Variablen) firstprivate(liste der Variablen) lastprivate(liste der Variablen) default(shared / none) copyin(liste der Variablen) reduction(operetor: Liste der Variablen) if(skalarer Ausdruck) ordered schedule(typ [, Blockgröße]) nowait num threads(positiver Integer-Ausdruck) parallel X X X X X X X X for X X X X X X X sections X X X X X Tabelle 1: Zuordnung der jeweils zulässigen Parameter zu den einzelnen Direktiven 8

3.2.1.2 Beschreibung Bei Erreichen der parallel-direktive werden nach dem in Abschnitt 2.1 vorgestelltem Fork-join-Prinzip eine bestimmte Anzahl Threads erzeugt. Auf die Ermittlung der Anzahl der Threads, unter anderem mit Hilfe des Parameters num threads, wird in Abschnitt 3.3 näher eingegangen. Jedem Thread wird vom System eine eindeutige Thread-Nummer zugewiesen, wobei der Master-Thread immer die Thread- Nummer 0 und die anderen Threads bei eins beginnend aufsteigend durchnummeriert werden. Die Thread-Nummer des aktuellen Threads kann ermittelt werden, indem der Thread die Funktion omp get thread num() aufruft. Die Parameter shared, private, firstprivate, default, reduction und copyin legen fest, ob die einzelnen Variablen für jeden Thread privat oder für alle Threads gemeinsam sind (siehe Abschnitt 3.1). Bei Angabe des if-parameters wird zunächst der skalare Ausdruck ausgewertet. Ergibt sich für den Ausdruck ein Wert ungleich Null oder ist kein if-parameter angegeben, so wird der auf die Direktive folgende Anweisungsblock parallel ausgeführt, andernfalls wird die Direktive nicht weiter betrachtet und der Anweisungsblock wird von nur einem Thread seriell ausgeführt. Sind die Threads erstellt, führt jeder Thread für sich den direkt auf die Direktive folgenden Anweisungsblock aus. Ist der Anweisungsblock dabei nicht von geschweiften Klammern umrundet, so gilt ausschließlich der direkt auf die parallel-direktive folgende Befehl als Anweisungsblock. 3.2.1.3 Restriktionen Eine Bedingung an den Anweisungsblock ist, dass alle Threads den ersten und letzten Befehl des Blockes durchlaufen. Es darf demnach innerhalb eines Blockes keinen Sprung geben, der den Block verlässt. Somit ist beispielsweise keine return- Anweisung innerhalb eines Blockes erlaubt. Sprünge, die innerhalb des Blockes verbleiben, sind hingegen erlaubt. Der Anweisungsblock darf ebenso nicht innerhalb eines in dem Block begonnen Konstruktes enden, wie zum Beispiel vor dem Ende eines in dem Block beginnenden if-konstruktes. Des Weiteren ist zu beachten, dass die optionalen Parameter if und num threads nicht mehrfach in der Direktive stehen dürfen. Wird innerhalb des parallelen Bereichs eine Exception geworfen (throw-klausel), so ist sicherzustellen, dass diese Exception innerhalb des parallelen Bereichs und von dem Thread, der die Exception geworfen hat, abgefangen wird [Op05, S. 28]. 3.2.1.4 Beispiel Anhand eines Beispiels, das im Anhang unter A.1 zu finden ist, wird der Ablauf näher erläutert: Zunächst wird die Datei omp.h in das Programm eingebun- 9

den. Anschließend wird die Integer-Variable nummer deklariert. Im Hauptteil des Programms wird die parallel-direktive benutzt. Der Parameter num threads(4) veranlasst, dass neben dem aktuellen Thread drei weitere Threads erzeugt werden, der parallele Bereich also von insgesamt vier Threads bearbeitet wird (siehe Abschnitt 3.3). Da die Variable nummer als private Variable definiert ist, besitzt jeder Thread eine eigene Instanz dieser Variable. Diese Threads werden im folgenden parallel zueinander ausgeführt. Zunächst wird der Variable nummer die Nummer des aktuellen Threads zugewiesen. Im nächsten Schritt wird diese Nummer ausgegeben. Eine mögliche Ausgabe ist unter dem Quellcode, der im Anhang unter A.1 zu finden ist, abgedruckt. Hierbei kann keine Aussage darüber getroffen werden, in welcher Reihenfolge die verschiedenen Threads diesen Befehl ausführen. Am Ende des Anweisungsblocks werden die Threads synchronisiert, d.h. das Programm wird erst fortgesetzt, wenn alle Threads das Ende des Anweisungsblocks erreicht haben. Sobald dieses der Fall ist, werden alle Threads außer des Master-Threads beendet. Dieser setzt die Programmausführung seriell fort. 3.2.2 for-direktive zur Parallelisierung von Schleifen Bei der Parallelisierung einer for-schleife mittels OpenMP werden die einzelnen Iterationen der Schleifenausführung auf mehrere Threads verteilt, die durch mehrere Prozessoren parallel abgearbeitet werden können. Voraussetzung hierfür ist, dass die Anzahl der Iterationen bereits vor dem Ausführen berechenbar sein muss. Außerdem darf kein Schleifendurchlauf ein Ergebnis eines anderes Durchlaufs verwenden, so dass die einzelnen Iterationen der Schleife unabhängig voneinander ausgeführt werden können. Des Weiteren darf die Ausführung des Programms nicht davon abhängen, welcher Thread welchen Schleifendurchlauf bearbeitet. Eine weitere Voraussetzung ist, dass die Abarbeitung der Schleife nicht durch eine break-anweisung abgebrochen werden darf. Durch die Verteilung der Iterationen auf mehrere Threads ist es auf Multiprozessorsystemen möglich, die Laufzeit der Schleife zu reduzieren. Die Parallelisierung von Schleifen gehört zu den bedeutendsten und am häufigsten angewendeten OpenMP-Direktiven [Ch01, S. 41 f.]. 3.2.2.1 Syntax Die Syntax zur Parallelisierung einer Schleife innerhalb eines parallelen Bereichs ist folgendermaßen aufgebaut: 10

#pragma omp for [Parameter [Parameter]...] for (Index = Startwert ; Test ; Inkrementierung ) { Schleifenrumpf Welche Parameter hier eingesetzt werden dürfen ist in Tabelle 1 abzulesen. Die for-schleife muss in folgender Form vorliegen, damit bereits vor Ausführung der Schleife die Anzahl der Durchläufe berechnet werden kann: Die Indexvariable muss vom Typ int sein. Ihr wird bei der Initialisierung der Schleife ein Startwert zugewiesen, der auch als schleifenunabhängiger Integer-Ausdruck vorliegen darf. Der Test-Ausdruck im Kopf der for-schleife, dessen Ergebnis wahr ist, wenn die Schleife ein weiteres Mal ausgeführt werden soll, vergleicht die Indexvariable mit einem Integer-Ausdruck, dessen Wert sich während der Schleifenausführung nicht verändern darf. Als Operatoren sind hier <, <=, > und >= erlaubt. Der Inkrementierungs-Ausdruck muss die Indexvariable nach jedem Schleifendurchlauf um einen gleichen Wert erhöhen oder erniedrigen. Wenn der Vergleichsoperator < oder <= ist, dann muss der Wert erhöht werden, ansonsten erniedrigt. Hier sind folgende Ausdrücke erlaubt, wobei i für die Indexvariable und incr für einen schleifenunabhängigen Integer-Ausdruck steht: ++i, i++, --i, i--, i+=incr, i-=incr, i=i+incr, i=incr+i, i=i-incr. Besteht der parallele Bereich ausschließlich aus einer zu parallelisierenden Schleife, so ist es möglich, die Direktiven zu Beginn des parallelen Bereichs und zur Parallelisierung der Schleife zu kombinieren: #pragma omp parallel for [Parameter [Parameter]...] for (Index = Startwert; Test ; Inkrementierung ) { Schleifenrumpf Mit Ausnahme des nowait-parameters, der nicht mehr zulässig ist und auf den später näher eingegangen wird, kann bei der Kombination der beiden Direktiven jeder Parameter verwendet werden, der bei mindestens einer der beiden Direktiven zulässig ist. 3.2.2.2 Beschreibung In diesem Teilabschnitt werden die Parameter der for-direktive näher erläutert. Der schedule-parameter Wie die einzelnen Schleifendurchläufe auf die Threads des Teams verteilt werden, wird mit dem schedule-parameter festgelegt. Diesem werden der Typ, nach dem die 11

Aufteilung der Schleifeniterationen erfolgen soll, und optional eine Blockgröße als Parameter übergeben. Wird eine Blockgröße übergeben, so muss dieses ein Integer- Ausdruck sein, dessen Wert positiv ist und während der Schleifenausführung nicht verändert werden darf. Folgende Typen der Verteilung werden unterstützt: schedule(static, Blockgröße ): Die Schleifeniterationen werden statisch auf die Threads verteilt. Die Iterationen werden in Blöcke der festen Größe Blockgröße aufgeteilt und nach aufsteigender Thread-Nummer nach der Round- Robin-Strategie auf die Threads verteilt. Der letzte Block kann hierbei kleiner sein als die vorherigen. Ist keine Blockgröße angegeben, wird sie so gesetzt, dass jeder Thread einen etwa gleich großen Block aufeinander folgender Iterationen zugeteilt bekommt. Bei statischer Verteilung steht also bereits vor Ausführung fest, welcher Thread welche Iterationen ausführen wird. schedule(dynamic, Blockgröße ): Bei dieser Variante werden die Iterationen dynamisch auf die Threads verteilt. Jedem Thread werden zunächst so viele Iterationen zugewiesen, dass ihre Anzahl der Blockgröße entspricht. Sobald ein Thread diese abgearbeitet hat, bekommt er einen neuen Block der Größe Blockgröße zugewiesen, bis alle Iterationen verteilt sind. Auch hier kann der letzte Block kleiner sein als die vorherigen. Ist keine Blockgröße angegeben, wird als Defaultwert 1 verwendet. In diesem Fall wird jede Iteration einzeln einem Thread zugeteilt. Benötigt ein Thread durchschnittlich pro Block weniger Zeit zur Abarbeitung seiner Blöcke als andere Threads (z.b. schnellerer Prozessor oder weniger rechenintensive Iterationen), führt dies bei dynamischer Verteilung dazu, dass er über die gesamte Abarbeitungszeit insgesamt mehr Blöcke zugewiesen bekommt als andere Threads. Es findet also eine bessere Lastverteilung auf die einzelnen Prozessoren statt als bei der statischen Verteilung. schedule(guided, Blockgröße ): Hier werden die Iterationen ebenfalls dynamisch auf die Threads verteilt. Sei a die Anzahl der noch nicht bearbeiteten Iterationen, n die Anzahl der Threads und k die Blockgröße. Dann werden dem nächsten Thread max(k, a ) Iterationen zugewiesen. Eine Ausnahme ist auch n hier der letzte Block, der erreicht ist wenn a < k ist und eine Größe von a hat. Die Anzahl zugeteilter Iterationen nimmt also stetig ab. Ist keine Blockgröße angegeben, wird sie auf 1 gesetzt. Auch hier findet eine Lastverteilung auf die einzelnen Prozessoren statt. Im Vergleich zur dynamischen Verteilung werden die Iterationen auf insgesamt weniger Blöcke verteilt, dafür ist die Berechnung der verschiedenen Größen der Blöcke aufwendiger. 12

schedule(runtime): Bei dieser Angabe wird das Verfahren zur Verteilung der Iterationen auf die Threads erst zur Laufzeit des Programms bestimmt. Das geschieht mit Hilfe der Umgebungsvariable run-sched-var. Wird diese Variable beispielsweise vor dem Start des Programms durch die Eingabe von setenv OMP_SCHEDULE "static, 2" gesetzt, so wird ein statisches Scheduling mit einer Blockgröße von 2 angewendet. Wird die Umgebungsvariable nicht explizit gesetzt, ist es systemabhängig, welches Scheduling-Verfahren gewählt wird. Ist kein schedule-parameter angegeben, wird das Default-Scheduling-Verfahren der entsprechenden OpenMP-Implementierung angewendet. Dieses ist in der Umgebungsvariable def-sched-var festgehalten und kann vom Programmierer nicht verändert werden. Der ordered-parameter Der ordered-parameter wird verwendet, wenn ein bestimmter Bereich innerhalb der Schleife erst ausgeführt werden darf, wenn alle vorherigen Iterationen diesen Bereich bereits ausgeführt haben. Zusätzlich zu der Angabe des ordered-parameters in der for-direktive wird dem entsprechende Bereich, für den die vorgeschriebene Reihenfolge gelten muss, noch eine ordered-direktive hinzugefügt: #pragma omp ordered { Anweisungsblock Diese ordered-direktive darf nur verwendet werden, wenn der for-direktive der ordered-parameter übergeben wurde. Eine mehrfache Verwendung innerhalb einer Schleife ist nicht zulässig. Erreicht ein Thread eine ordered-direktive, wartet er, bis alle vorherigen Iterationen den Anweisungsblock dieser Direktive ausgeführt haben und setzt erst dann seine Ausführung fort. Der nowait-parameter Am Ende der for-schleife werden alle Threads synchronisiert. Es warten also alle Threads, bis alle Iterationen der Schleife abgearbeitet sind. Soll auf diese Synchronisierung verzichtet werden, wird der for-direktive der Parameter nowait hinzugefügt. In einer for-direktive darf der nowait-parameter nicht mehrfach aufgeführt sein. In der kombinierten parallel for-direktive ist die Verwendung dieses Parameters allerdings nicht möglich, da am Ende der for-schleife alle Threads bis auf den Master-Thread beendet werden, bevor dieser die Ausführung fortsetzt. 13

Weitere Parameter der for-direktive Die übrigen Parameter (private, firstprivate, lastprivate, reduction) sind, wie in Abschnitt 3.1 erläutert, für die verschiedenen Varianten der Variablendeklarationen zuständig. Der Schleifenindex braucht hierbei nicht explizit einbezogen werden, da er automatisch als private Variable deklariert wird und mit dem aktuellen Index der entsprechenden Iteration initialisiert wird. 3.2.2.3 Beispiel Die for-direktive soll nun anhand eines Beispielprogramms, das alle Primzahlen von 2 bis 100.000 ausgibt, veranschaulicht werden. Der Quelltext dieses zunächst seriellen Programms ist im Anhang unter A.2 zu finden. Zunächst werden die Integer- Variablen zahl, teiler und treffer deklariert, bevor als Überschrift das Wort Primzahlen ausgegeben wird. Im Anschluss daran wird mit Hilfe einer for-schleife jede Zahl von 2 bis 100.000 überprüft, ob sie einen Teiler hat. Dafür wird die Variable treffer, die anzeigt, ob für den aktuellen Wert der Schleifenvariablen zahl ein Teiler gefunden wurde, zunächst auf 0 gesetzt. Innerhalb der for-schleife wird nun eine weitere for-schleife gestartet, die für alle Zahlen von 2 bis zahl überprüft, ob es sich um einen Teiler von zahl handelt und in diesem Fall der Variable treffer den Wert 1 zuweist. Nach Ausführung der inneren for-schleife wird zahl ausgegeben, wenn kein Teiler für den aktuellen Wert der Variable zahl gefunden wurde. Mit Hilfe von OpenMP soll dieses Programm, oder zumindest ein Teil von diesem, parallelisiert werden, um die Ausführungszeit zu senken. Hierfür ist zunächst die Datei omp.h durch den Befehl #include <omp.h> einzubinden. Im nächsten Schritt soll eine der beiden for-schleifen parallelisiert werden. Würde dafür die innere Schleife gewählt werden, so müssten bei jeder Iteration der äußeren Schleife zusätzliche Threads erzeugt (fork), am Ende der Schleife synchronisiert und wieder beendet werden (join). Um diesen Overhead zu vermeiden, wird die äußere Schleife parallelisiert, so dass insgesamt nur ein fork- und ein join-vorgang ausgeführt werden muss. Da der parallele Bereich nur aus der Schleife besteht, wird auf die kombinierte Schreibweise der parallel- und der for-direktive zurückgegriffen. Die Variable treffer muss für den parallelen Bereich als private Variable deklariert werden, da sie für jeden Wert der Indexvariable zahl einzeln bestimmt, ob diese einen Teiler hat oder nicht und somit nicht von einem anderen Thread geändert werden darf. Selbiges gilt für die Variable teiler, die Inderxvariable der inneren Schleife ist. Die Variable zahl wird automatisch als privat deklariert, da sie die Indexvariable der parallelisierten for-schleife ist. Eine Variante des parallelisierten Programms ist im Anhang unter A.3 dargestellt. Als Scheduling-Verfahren wurde die statische 14

Verteilung mit der Blockgröße 1 gewählt, um die mit steigender Indexvariable rechenlastiger werdenden Iterationen so auf die verschiedenen Threads zu verteilen, dass sich die Rechenlast möglichst gleichmäßig auf die Threads verteilt. Ist von gleich schnellen Prozessoren auszugehen, wird die statische gegenüber der dynamischen Verteilung bevorzugt, da bei dynamischer Verteilung nach Ausführung jeder Iteration zusätzlicher Overhead entsteht, um dem Thread die nächste Iteration zuzuweisen. Sind die Prozessoren des Systems unterschiedlich schnell, bietet sich hier eher eine dynamische Verteilung an. Die oben beschriebene Variante hat allerdings noch einen Nachteil: Die Primzahlen werden nicht mehr zwingend in strikt aufsteigender Reihenfolge ausgegeben. Das liegt daran, dass ein Thread, der gerade eine Iteration mit höherer Indexvariable als ein anderer Thread bearbeitet, möglicherweise früher die Anweisung die Zahl auszugeben erreicht als der andere Thread. Soll ein solches Verhalten unterbunden werden, muss die ordered-direktive hinzugefügt werden, die einen bestimmten Bereich einer Schleife erst dann ausführt, wenn alle vorherigen Iterationen diesen bereits ausgeführt haben. In diesem Falle ist also vor dem if -Konstrukt, das die Variable zahl ausgibt, wenn es sich um eine Primzahl handelt, die Direktive #pragma omp ordered einzufügen. Zusätzlich ist der parallel for-direktive am Beginn des parallelen Bereichs der Parameter ordered hinzuzufügen. Wird das Programm, dessen Quelltext im Anhang unter A.4 zu finden ist, nun ausgeführt, so werden die Primzahlen wie bei der seriellen Abarbeitung des Programms in aufsteigender Reihenfolge ausgegeben. Des Weiteren sollte beachtet werden, dass die Blockgröße für das Scheduling in diesem Fall 1 betragen sollte. Große Blöcke führen zu einer längeren Ausführungszeit, weil andere Threads warten müssen, bis ein Thread den ganzen Block abgearbeitet hat, bevor sie den synchronisierten Bereich betreten dürfen. Um die Verkürzung der Laufzeit durch die Parallelisierung des Programms mit OpenMP zu veranschaulichen, wurden die erläuterten drei Programmvarianten auf einem Personalcomputer mit einem Intel Core 2 Duo E6300-Prozessor ausgeführt. Da dieser Prozessor aus zwei Kernen besteht, kann er zwei Threads gleichzeitig ausführen. Jede der drei Programmvarianten wurde zehnmal ausgeführt und anschließend der arithmetische Mittelwert über die jeweiligen Ausführungszeiten gebildet, um Schwankungen bei der Ausführungszeit, die zum Beispiel durch gleichzeitig laufende Prozesse des Betriebssystems beeinflusst werden können, auszugleichen. Es ist zu erwarten, dass die Ausführungszeit der parallelen Version durch die Bearbeitung von zwei Prozessorkernen gegenüber der seriellen Version um fast die Hälfte sinkt. Nach Einfügen der ordered-direktive ist davon auszugehen, dass die Ausführungszeit im Vergleich zur Version mit der ungeordneten Ausgabe etwas 15

ansteigt, da der Synchronisationsaufwand steigt. Gegenüber der seriellen Abarbeitung ist jedoch auch hier eine Verkürzung der Ausführungszeit zu erwarten. Die Ausführungszeiten der einzelnen Durchläufe sind in Tabelle 2 im Anhang abzulesen. Die Ausführung der seriellen Version dauerte durchschnittlich 45,5 Sekunden, die parallele Version ohne ordered-direktive benötigte im Durchschnitt 23,8 Sekunden. Somit konnte durch die Verteilung der Abarbeitung der Iterationen auf zwei Threads, die gleichzeitig von zwei Prozessorkernen bearbeitet werden können, durchschnittlich über 47% der Ausführungszeit eingespart werden. Die Ausführungszeit der dritten Variation, bei der die Ausgabe der Primzahlen in strikt aufsteigender Reihenfolge erfolgt, betrug im Durchschnitt 25,9 Sekunden. Dass dieser Wert die durchschnittliche Ausführungszeit der zweiten Version um fast 9% übersteigt liegt darin begründet, dass durch die Synchronisation bei der Ausgabe Leerlaufzeiten der Threads entstehen, da sie erst weiter ausgeführt werden, wenn die anderen Threads den synchronisierten Bereich für die vorhergehenden Schleifeniterationen ausgeführt haben. Im Vergleich zur seriellen Abarbeitung liegt aber noch immer eine beträchtliche Verkürzung der Ausführungszeit um durchschnittlich 43% vor. 3.2.3 sections-direktive zur Parallelisierung von Abschnitten Mit OpenMP ist es möglich, verschiedene Programmabschnitte, die unabhängig voneinander sind, von mehreren Threads parallel abarbeiten zu lassen. Grundlage hierfür ist die sections-direktive. 3.2.3.1 Syntax Die Syntax der sections-direktive ist folgendermaßen aufgebaut: #pragma omp sections [Parameter [Parameter]...] { [#pragma omp section] Anweisungsblock [#pragma omp section Anweisungsblock. ] Die einsetzbaren Parameter sind in Tabelle 1 dargestellt. Besteht der Anweisungsblock des parallelen Bereichs ausschließlich aus den zu parallelisierenden Abschnitten, so können, analog zur Parallelisierung der for-schleife, die parallel- und die sections-direktive kombiniert werden: 16

#pragma omp parallel sections [Parameter [Parameter]...] 3.2.3.2 Beschreibung Ist ein Bereich eines seriellen Programms so in Abschnitte aufteilbar, dass kein Abschnitt von den Ergebnissen eines vorherigen Abschnitts abhängt, so können diese Abschnitte parallel zueinander ausgeführt werden. Dem Bereich mit den zu parallelisierenden Abschnitten, der innerhalb eines parallel-konstruktes liegen muss, ist zu Beginn die sections-direktive mit den optionalen Parametern, die analog zur for-direktive der Variablendeklaration und der Thread-Synchronisation am Ende des Programmbereichs dienen, hinzuzufügen. Vor jedem einzelnen Abschnitt des Bereichs wird zusätzlich die section-direktive eingefügt. Nur für den ersten Abschnitt ist diese Angabe optional. Die Abschnitte werden bei der Ausführung des Bereichs auf die einzelnen Threads verteilt. Jeder Abschnitt wird genau einmal ausgeführt und jeder Thread führt keine, einen oder mehrere Abschnitte aus (abhängig vom Verhältnis Anzahl Abschnitte zu Anzahl Threads). Wie die Abschnitte auf die verschiedenen Threads verteilt werden ist implementierungsabhängig, so dass hierüber keine Aussagen getroffen werden können. Nachdem die Threads die Abschnitte abgearbeitet haben, werden sie, insofern kein nowait-parameter angegeben ist, synchronisiert, bevor die Ausführung des Programms fortgesetzt wird. Dieses Verfahren der Parallelisierung bietet sich insbesondere dann an, wenn durch andere Parallelisierungsverfahren (zum Beispiel mittels for-direktive) keine oder nur geringe Verkürzungen der Laufzeit in dem entsprechenden Bereich erreicht werden können, was beispielsweise dann der Fall ist, wenn große Teile innerhalb der einzelnen Abschnitte seriell ausgeführt werden müssen. 3.3 Anzahl Threads Ein paralleler Bereich wird von einer bestimmten Anzahl Threads ausgeführt. Diese Anzahl wird von mehreren Parameteren beeinflusst. Im ersten Schritt wird eine eventuell vorhandene if -Klausel in der parallel-direktive überprüft. Ist diese vorhanden und ihre Bedingung falsch, wird der Bereich nicht parallel sondern von nur einem Thread sequentiell abgearbeitet. Selbiges gilt für den Fall, dass sich die Direktive bereits in einem parallelen Bereich befindet und verschachtelte Parallelisierung deaktiviert ist oder kein weiterer Level der Parallelisierung unterstützt wird. Ob verschachtelte Parallelisierung unterstützt wird, ist abhängig vom Laufzeitsystem und kann durch den Aufruf von omp get nested() abgefragt werden, der 0 zurückliefert, wenn keine Verschachtelung unterstützt wird. Dieses kann beeinflusst werden, indem der entsprechenden Umgebungsvariable durch den Aufruf 17

von omp set nested(nested ) der Integer-Wert nested zugewiesen wird. Spricht weder die if -Klausel noch die Verschachtelung gegen eine Parallelisierung, kann durch die Funktion omp set dynmic(int dynamic threads) Einfluss auf die Anzahl der Threads genommen werden. Diese Funktion, die nur außerhalb eines parallelen Bereichs aufgerufen werden darf, ändert die Umgebungsvariable dyn-var, deren Wert durch Aufruf von omp get dynamic() ermittelt werden kann. Hat sie den Wert 0, so wird die Anzahl der Threads unabhängig von den Systemgegebenheiten folgendermaßen festgelegt: Wurde der parallel-direktive der Parameter num threads(anzahl ) übergeben, so bestimmt anzahl die Anzahl der Threads für den parallelen Bereich, ansonsten die Umgebungsvariable nthreads-var, die vom System initialisiert wird und außerhalb des parallelen Bereichs vom Programmierer durch die Funktion omp set num threads(int anzahl) verändert werden kann. Ist eine dynamische Anpassung an die Systemgegebenheiten durch das Laufzeitsystem erlaubt, d.h. dyn-var 0, wird erst zur Laufzeit festgelegt, von wie vielen Threads der parallele Bereich bearbeitet wird. Ist der Parameter num threads(anzahl ) angegeben, sind es höchstens anzahl Threads, ansonsten höchstens nthreadsvar Threads. 3.4 Koordination und Synchronisation von Threads In der Regel werden parallele Bereiche in einem OpenMP-Programm von mehreren Threads ausgeführt. Da diese auf gemeinsame Variablen zugreifen können, ist der Zugriff zu koordinieren. Hierfür stellt OpenMP verschiedene Direktiven bereit, die in diesem Abschnitt näher erläutert werden. Des Weiteren wird auf Direktiven eingegangen, mit deren Hilfe die Threads synchronisiert werden können. 3.4.1 Kritische Abschnitte Bei paralleler Programmierung auf Systemen mit gemeinsamem Speicher muss darauf geachtet werden, dass keine sogenannten Race-Conditions auftreten. Hierbei handelt es sich um Situationen, bei denen das Endergebnis davon abhängt, in welcher zeitlichen Abfolge die Threads auf den Prozessoren laufen. Eine Methode Race- Conditions zu vermeiden, ist der wechselseitige Ausschluss (mutual exclusion), d.h. ein Thread kann eine gemeinsam genutzte Variable nutzen, ohne dass ein anderer Thread zeitgleich auf diese zugreifen kann. Ein Abschnitt eines Programms, in dem auf gemeinsame Variablen zugegriffen wird, nennt sich kritischen Abschnitt [Ta03, S. 119]. Um Race-Conditions zu vermeiden bietet OpenMP die Möglichkeit, dass niemals 18

zwei Threads gleichzeitig einen kritischen Abschnitt ausführen können. Kritische Abschnitte sind dafür mit folgender Direktive zu kennzeichnen: #pragma omp critical [(name)] { kritischer Abschnitt Die Angabe von name ist optional. Alle kritischen Abschnitte ohne Namen bekommen vom System einen einheitlichen Namen zugewiesen. Trifft ein Thread auf einen kritischen Abschnitt, betritt er diesen erst, sobald sich kein anderer Thread in einem kritischen Abschnitt mit gleichem Namen befindet. Hierbei spielt es keine Rolle, ob die Threads einem gleichen Team zugeordnet sind. Bei der Deklarierung von Abschnitten zu kritischen Abschnitten ist darauf zu achten, dass Race-Conditions nur ausgeschlossen werden können, wenn alle Zugriffe auf eine gemeinsame Variable in gleichnamigen kritischen Abschnitten stehen. Werden hingegen zu viele Abschnitte zu kritischen Abschnitten deklariert, kann dieses zu vermeidbaren Performance- Einbußen führen. 3.4.2 Atomare Operationen Sei x eine gemeinsame Variable mit dem Wert 5. Führen nun zwei Threads parallel den Befehl x = x + 1 aus, so ist das Ergebnis nicht vorhersagbar. Das liegt darin begründet, dass x zuerst ausgelesen wird, der Wert inkrementiert wird und dann wieder nach x geschrieben wird. Wird der zweite Thread zwischen der Lese- und der Schreiboperation des ersten Threads komplett ausgeführt, so hat die Variable x nach Ausführung beider Threads den Wert 6, obwohl sich bei serieller Ausführung der beiden Threads der Wert 7 ergibt. Das beschriebene Problem kann in OpenMP mit den im vorherigen Abschnitt beschriebenen kritischen Abschnitten gelöst werden. Dieses lässt sich in OpenMP allerdings effizienter lösen, wenn der kritische Abschnitt ausschließlich aus einer Zuweisung besteht, die eine der folgenden Formen annimmt: x++, ++x, x--, --x, x binop= skalarer Ausdruck, wobei binop {+,,, /, &,ˆ,, <<, >> und skalarer Ausdruck nicht auf x referenzieren darf. Diese Speicherzugriffe können als atomare Operation deklariert werden, so dass sie in einem Schritt ausgeführt werden ohne dabei unterbrochen zu werden. Die OpenMP-Direktive zur Deklarierung einer Zuweisung als atomare Operation hat folgende Syntax: 19

#pragma omp atomic Zuweisung Die Zuweisung muss einer der oben genannten Operationen entsprechen. Das Berechnen von skalarer Ausdruck ist nicht Teil der atomaren Operation. 3.4.3 Synchronisation Mit Hilfe des barrier-konstruktes ist es möglich, Threads zeitlich zu synchronisieren. Die zugehörige Syntax lautet: #pragma omp barrier Erreicht ein Thread in einem parallelen Bereich diese Direktive, beginnt er die Abarbeitung der nachfolgenden Anweisungen erst, wenn alle Threads des Teams diese Direktive erreicht haben. Die Direktive wird zum Beispiel dann eingesetzt, wenn ein Abschnitt auf die Ergebnisse aller Threads eines vorherigen Abschnittes aufbaut. Es ist sicherzustellen, dass diese Direktive entweder von allen oder von keinem Thread erreicht wird. Sie darf somit beispielsweise nicht in einem if -Konstrukt stehen, wenn einige Threads die Bedingung des if -Konstruktes erfüllen und andere nicht, da dann nicht alle Threads die Direktive erreichen können und somit eine Verklemmung (Deadlock) entsteht. Es werden nur alle Threads des jeweiligen parallelen Bereichs synchronisiert. Das gilt auch, wenn mehrere parallelen Bereiche vorhanden sind, was zum Beispiel bei verschachtelter Parallelisierung auftreten kann. 3.4.4 Ausführung ausschließlich durch den Master-Thread Die master-direktive ermöglicht, dass ein Programmabschnitt in einem parallelen Bereich ausschließlich durch den Master-Thread des Teams ausgeführt wird. Ihre Syntax ist folgendermaßen: #pragma omp master { Anweisungsblock Erreicht der Master-Thread die Direktive, bearbeitet er den Anweisungsblock. Liegt die Direktive in einem verschachtelten parallelen Bereich, so ist hiermit der Master-Thread des innersten parallelen Bereiches gemeint. Alle anderen Threads des Teams ignorieren den Anweisungsblock und setzen die Ausführung mit dem ersten Befehl nach dem Anweisungsblock fort. Am Ende des Blockes erfolgt keine Synchronisierung. Im Gegensatz zur barrier-direktive ist es bei der master-direktive nicht notwendig, dass diese von allen Threads erreicht wird. 20

Kapitel 4: Zusammenfassung und Ausblick 4 Zusammenfassung und Ausblick OpenMP ist ein Schnittstelle zur parallelen Programmierung auf Multiprozessorsystemen mit gemeinsamem Speicher. Sie stellt Compilerdirektiven, Bibliotheksfunktionen und Umgebungsvariablen für die Programmiersprachen C, C++ und Fortran zur Verfügung. Der Programmierer kann von den zugrunde liegenden Threads abstrahieren, da OpenMP das Erstellen und Beenden der Threads nach dem Fork-join-Prinzip und die Verwaltung der Threads übernimmt. Soll ein Programm-Abschnitt von mehreren Prozessoren parallel abgearbeitet werden, sind diesem Abschnitt eine parallel-direktive voranzustellen und gegebenfalls, in Abhängigkeit von der Art der Parallelisierung, wie zum Beispiel Mehrfachausführung des gleichen Codes oder arbeitsteilende Aufteilung von Schleifeniterationen, weitere Direktiven hinzuzufügen. Hierbei ist der Programmierer dafür verantwortlich, dass keine Konflikte, Deadlocks und Race Conditions auftreten. Für diesen Zweck stellt OpenMP ebenfalls Direktiven zur Verfügung, mit denen beispielsweise kritische Abschnitte oder atomare Operationen deklariert werden können. Wird das Programm von einem Compiler ausgeführt, der OpenMP nicht unterstützt, so werden die OpenMP-Direktiven ignoriert und das Programm wird seriell ausgeführt. In modernen Supercomputern ist es möglich, MPI und OpenMP zu kombinieren. Hierbei wird die Arbeit mittels MPI auf die verschiedenen Multiprozessorsysteme verteilt, die die Berechnungen dann durch Anwendung von OpenMP parallelisieren. Im Hinblick auf weitere Performance-Steigerungen wird die Parallelisierung von Programmen zukünftig weiter an Bedeutung gewinnen. Eine weitere Geschwindigkeitssteigerung durch schnellere Prozessoren wird bald an ihre physikalischen Grenzen stoßen, da der Abstand zwischen Prozessor und Cache nicht beliebig verkleinert werden kann und die Daten sich nicht schneller als mit Lichtgeschwindigkeit ausbreiten können. Somit kann die Performance insbesondere durch die Verteilung der Arbeit auf mehrere Prozessoren, beispielsweise durch Anwendung von OpenMP, gesteigert werden. 21