126

Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

  • Upload
    others

  • View
    0

  • Download
    0

Embed Size (px)

Citation preview

Page 1: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

Masterarbeit

Fakultät Informatik

Untersuchungen zur Abstraktionder GPU-Programmierung in Javaam Beispiel Fluid-Simulation

Prüfer Prof. Dr. Meixner

Start 16.05.2013Abgabe 18.11.2013

Matthias KlaÿKleiberweg 486199 Augsburg

Tel: 0821 431552E-Mail: matthias.klass

@hs-augsburg.deMat.-Nr.: 933580

Hochschule AugsburgAn der Hochschule 186161 Augsburg

Tel: 0821 5586-0E-Mail: [email protected]: 0821 5586-3222

http://www.hs-augsburg.de

Page 2: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

i

Abstract

For the past years the programming of graphics units has gained importance. While the perfor-mance of normal processors increases only with linear gain, it duplicates for graphic processingunits (GPU) with each new generation. As applications like real-time simulations or data eval-uations require more and more computing power, the requirements posed to the underylinghardware also grows signi�cantly. To use the available potential of GPUs, developers have beendependent on native APIs, provided by di�erent manufacturers like NVIDIA or AMD. However,not every program is written in native languages, which is why the demand for the ability to usethe GPU computing power in higher-level programming languages increases. Both the evaluationof available libraries to use GPUs in Java and their usage to implement a �uid simulation arepart of this work.

Kurzzusammenfassung

Die Programmierung von Gra�kkarten hat in den vergangenen Jahren zunehmend an Bedeu-tung gewonnen. Während die Rechenleistung von Prozessoren nur langsam linear steigt, so ver-doppelt sich diese für Gra�kkarten mit jeder Generation. Gleichzeitig steigen in verschiedens-ten Bereichen, wie z.B. Echtzeitsimulationen oder Auswertungen auf groÿen Datenmengen, dieAnforderungen an die Rechenleistung der unterliegenden Hardware. Um das zur Verfügung ste-hende Potential der Gra�kkarten zu nutzen waren Entwickler bisher stark auf herstellerspezi�scheSchnittstellen wie CUDA oder OpenCL angewiesen. Jedoch besteht der Bedarf an Rechenleis-tung nicht nur auf der Ebene nativer Sprachen, sondern ebenfalls unter Nutzung von höherenProgrammiersprachen. Insbesondere die Evaluierung verfügbarer Abstraktionsbibliotheken sowiederen Verwendung zur Implementierung einer Fluid-Simulation sind Thema dieser Arbeit.

Page 3: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

ii

MasterarbeitHochschule für angewandteWissenschaften, AugsburgFakultät für Informatik

Hiermit erkläre ich, dass ich die folgende Masterarbeit �Untersuchungen zur Abstraktion derGPU-Programmierung in Java am Beispiel Fluid-Simulation� selbstständig verfasst, noch nichtanderweitig für Prüfungszwecke vorgelegt, keine anderen Hilfsmittel als die angegebenen Quellenoder Hilfsmittel verwendet sowie wörtliche und sinngemäÿe Zitate als solche gekennzeichnet habe.

Matthias Klaÿ

Page 4: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

iii

Struktur der Arbeit

Kapitel 1 motiviert die Verwendung von Gra�kkarten zur Berechnung rechenintensiver Aufgaben.Dabei wird u.a. darauf eingegangen, warum die Gra�kkarte für bestimmte Berechnungen besserals der normale Prozessor geeignet ist.

Im Anschluss wird in Kapitel 2 beschrieben, wie die Programmierung der Gra�kkarten sichim Zeitverlauf entwickelt hat und warum eine Java-Abstraktion sinnvoll ist. Dabei werden u.a.auch Vor- und Nachteile gegenüber einer nativen Vorgehensweise aufgezeigt. Schlieÿlich gehtdas Kapitel kurz auf einige am Markt verfügbare Abstraktionsbibliotheken ein und beschreibtabschlieÿend das Ziel der Arbeit.

Als Nächstes führt Kapitel 3 in die eigentliche Gra�kkartenprogrammierung ein. Dies beinhaltetauf der einen Seite die Architektur einer NVIDIA Gra�kkarte, also deren Ausführungsmodellund Speicherstruktur, und auf der anderen Seite eine kurze Einführung in CUDA-C zur Pro-grammierung.

Kapitel 4 beschäftigt sich anschlieÿend mit der Beschreibung der Navier-Stokes-Flüssigkeits-gleichungen und deren numerische Lösung zur Implementierung eines Simulators. Dies beziehthauptsächlich die am Fraunhofer IWU implementierten Verfahren ein.

Das sich anschlieÿende Kapitel 5 trägt das Thema Abstraktion. Darin werden verschiedene An-forderungen an Abstraktionsbibliotheken diskutiert und die Bibliotheken im Detail vorgestellt.Abschlieÿend wird ein Geschwindigkeitsbenchmark vorgestellt, anhand dem die Bibliotheken ver-glichen werden.

Darauf aufbauend wird eine Bibliothek ausgewählt, unter deren Nutzung in Kapitel 6 der nativeSimulator des Fraunhofer IWU portiert wird. Zusätzlich wird die erstellte Implementierung, inkl.der unterliegenden Bibliothek, anhand den Aspekten Ausführungszeit und Objektorientierungweiter optimiert.

Kapitel 7 fasst schlieÿlich die erreichten Ergebnisse zusammen und bietet einen Überblick überzukünftige Anforderungen an die Bibliothek und die Portierung.

Fraunhofer IWU

Die Arbeit ist in der Projektgruppe RMV des Fraunhofer IWU in Augsburg entstanden. DasFraunhofer IWU beschäftigt sich mit der anwendungsorientierten Forschung auf dem Gebiet derWerkzeugmaschinen und Produktionstechnik. Die Projektgruppe RMV spezialisiert diese Gebietim Rahmen der Ressourcene�zienz. Dies deckt auf der einen Seite die Entwicklung von Werkzeu-gen und Maschinen ab, mit der die Produktion e�zienter gestaltet werden kann, und auf deranderen Seite auch Möglichkeiten, um verwendete Rohsto�e besser zu nutzen bzw. anschlieÿendwieder zu verwerten.

Die Rahmenbedingungen der Masterarbeit stellt dabei die Forschungstätigkeit des BetreuersHerrn M.Sc. Stefan Krotil, die sich mit der Echtzeitsimulation von Flüssigkeiten im industriellenUmfeld beschäftigt. Aktuelle Simulationssoftware ist verhältnismäÿig langsam und nur schwer zukon�gurieren. Diese wird deswegen nur selten in der Industrie eingesetzt. Durch die Verbindung

Page 5: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

iv

der Navier-Stokes-Gleichungen mit der Nutzung des Leistungspotentials der Gra�kkarten ergebensich deswegen Vorteile in Bezug auf die Entwicklungsgeschwindigkeit und -kosten neuer Produkte.

Danksagung

Der Autor möchte sich zuerst bei Prof. Dr. Meixner (Hochschule Augsburg) für die Betreuungwährend der Masterarbeit bedanken. Insbesondere das o�ene Feedback und Kommentare überresultierende Evaluationsgraphen waren während der Arbeit sehr hilfreich. Das Thema der GPU-Abstraktion ist zwar ein unheimlich spannendes und für die Zukunft relevantes Themengebiet,der Autor wäre aber selbst nie auf die entsprechende Themenstellung gekommen. Auch hiervielen Dank für den Themenvorschlag!

Auÿerdem geht ein Dankeschön an Herrn M.Sc. Stefan Krotil, der die Rahmenbedingungen fürdie Arbeit schuf und, obwohl die Arbeit selbst nur als Randthema seiner wissenschaftlichenTätigkeit zu betrachten ist, stets mit Rat und Tat zur Seite stand. Insbesondere auch vielenDank für die geduldigen Erklärungen zum Thema Navier-Stokes-Gleichungen!

Page 6: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

Inhaltsverzeichnis

1 Motivation 1

2 Stand der Technik und Ziel der Arbeit 3

2.1 Graphics Processing Unit, Gra�kprozessor (GPU)-Computing . . . . . . . . . . . 3

2.2 Native GPU-Programmierung vs. Java-Abstraktion . . . . . . . . . . . . . . . . . 4

2.3 CUDA-Abstraktionsbibliotheken für Java . . . . . . . . . . . . . . . . . . . . . . 5

3 Programmierung einer GPU 7

3.1 Architektur einer GPU mit CUDA Unterstützung . . . . . . . . . . . . . . . . . . 7

3.1.1 Thread-Organisation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7

3.1.2 Ausführung von Blöcken und Warps . . . . . . . . . . . . . . . . . . . . . 8

3.1.3 Speichermodell und -zugri� . . . . . . . . . . . . . . . . . . . . . . . . . . 9

3.2 CUDA Programmierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11

3.2.1 Beispiel Array-Inkrementierung . . . . . . . . . . . . . . . . . . . . . . . . 12

3.2.2 Synchronisation von Threads . . . . . . . . . . . . . . . . . . . . . . . . . 14

3.2.3 Kompilierung von CUDA-Code . . . . . . . . . . . . . . . . . . . . . . . . 15

3.2.4 OpenCL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16

4 Navier-Stokes-Gleichungen zur Flüssigkeitssimulation 18

4.1 Impuls- und Inkompressibilitätsgleichungen . . . . . . . . . . . . . . . . . . . . . 18

4.2 Diskretisierung des Simulationsraums . . . . . . . . . . . . . . . . . . . . . . . . . 19

4.3 Numerische Berechnung der Einzelterme der Navier-Stokes-Gleichungen . . . . . 20

4.4 Gröÿe der Simulationsschritte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22

4.5 Datenstrukturen im vorhandenen Simulator . . . . . . . . . . . . . . . . . . . . . 23

4.6 Ablauf eines Simulationsschrittes . . . . . . . . . . . . . . . . . . . . . . . . . . . 23

5 CUDA-Abstraktion in Java 25

5.1 Anforderungen an eine Java API . . . . . . . . . . . . . . . . . . . . . . . . . . . 25

5.2 Algorithmen zur Evaluation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29

5.3 Evaluation verfügbarer Abstraktionsbibliotheken . . . . . . . . . . . . . . . . . . 32

5.3.1 JCuda . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32

5.3.2 Java-GPU . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35

Page 7: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

Inhaltsverzeichnis vi

5.3.3 Aparapi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39

5.3.4 Rootbeer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45

5.3.5 Delite . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55

5.3.6 Projekt Sumatra . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64

5.4 Evaluation der Ausführungsgeschwindigkeiten . . . . . . . . . . . . . . . . . . . . 66

5.4.1 Randbedingungen der Messung . . . . . . . . . . . . . . . . . . . . . . . . 66

5.4.2 Aufbau des Evaluations-Projektes . . . . . . . . . . . . . . . . . . . . . . . 67

5.4.3 Ergebnisse der Zeitmessung . . . . . . . . . . . . . . . . . . . . . . . . . . 70

5.5 Fazit der Evaluation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78

6 Portierung des Simulators 80

6.1 Struktur des Simulators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80

6.2 Erweiterung und Verbesserung von Aparapi . . . . . . . . . . . . . . . . . . . . . 84

6.2.1 Implementierung objektorientierter Einsprungpunkte . . . . . . . . . . . . 84

6.2.2 Erhöhung der Geschwindigkeit durch Caching des besten OpenCL-Gerätes 88

6.2.3 Aufruf von OpenCL-Methoden von auÿerhalb der Kernel-Klasse . . . . . 90

6.2.4 Implementierung der Verwendung von Objekten in Kernels . . . . . . . . 90

6.3 Abbildung der neuen API auf den Fluid-Simulator . . . . . . . . . . . . . . . . . 93

6.4 Geschwindigkeitsvergleich zum nativen Simulator . . . . . . . . . . . . . . . . . . 95

7 Fazit 97

7.1 Erreichte Ziele . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97

7.2 Ansatzpunkte für zukünftige Verbesserungen . . . . . . . . . . . . . . . . . . . . 98

7.3 Ausblick zur GPU-Entwicklung . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99

A Java-Native-Interface (JNI) 101

B Feature-Vergleich der Bibliotheken 103

C Pro�ling im JNI- und GPU-Umfeld 106

C.1 Pro�ling in Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106

C.2 Pro�ling von JNI-C++-Quelltext . . . . . . . . . . . . . . . . . . . . . . . . . . . 107

C.3 Pro�ling in CUDA . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108

Abkürzungsverzeichnis 111

Abbildungsverzeichnis 113

Literaturverzeichnis 114

Page 8: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

Kapitel 1

Motivation

Während der letzten Jahre sind die Anforderungen an Simulationssoftware massiv gestiegen.Um schneller berechnete Ergebnisse auswerten zu können, werden bisher als Batch-Bearbeitungdurchgeführte Berechnungen heute als Echtzeit-Simulation umgesetzt. Auf diese Art und Weisekönnen o�ensichtlich unsinnige Parameter bereits vor Beendigung der Simulation verworfen unddamit schneller und e�zienter die bestmögliche Einstellung gefunden werden.

Ein Beispiel für eine rechenintensive Simulation ist die Berechnung des Verhaltens von Flüssigkei-ten. Das Problem liegt dabei darin, dass z.B. für ein Gitter mit den Dimensionen 1000× 1000×1000 eine Milliarde Zellen für einen einzigen Simulationsschritt bearbeitet werden müssen. Eshandelt sich dabei nicht um einfache Additionen beliebiger Werte sondern um die Lösung kom-plexer Di�erential-Gleichungen. Diese Berechnungen in Echtzeit durchzuführen ist mit Hilfe vonsequentiellen Programmen auf Grund der Rechenmenge nicht möglich. Heute werden hierfürmassiv parallele Ansätze verfolgt.

Traditionell wurden alle Berechnungen auf der Central Processing Unit (CPU) ausgeführt. Dieseist auf die Minimierung der sequentiellen Ausführungszeit einer Aufgabe optimiert. Dazu wurdenspezielle Architektur-Optimierungen wie Out-of-order-Ausführung, Schattenregister o.ä. einge-führt [KWH10][S. 3]. Eine Steigerung der Rechenleistung wurde durch immer höhere Taktratenermöglicht. Aufgrund physikalischer Limitierungen, wie z.B. Leckströme, und der exponentiellenZunahme des Stromverbrauchs für die Takterhöhung innerhalb einer CPU, ist dieser Weg in denletzten Jahren immer schwieriger und teurer geworden [KWH10][S. 1] [SK11][S. 3]. Stattdessensetzen die Hersteller vermehrt auf die Integration von mehreren CPU-Kernen sowie auf die op-timierte Nutzung der vorhandenen Ressourcen durch Hyperthreading / Simultaneous Multi-threading (SMT). Aktuelle Prozessoren besitzen dabei zwischen 6 und 16 Kerne und sind mitFrequenzen zwischen drei und vier GHz getaktet [int13] [SK11][S. 3].

Eine Alternative bietet die Nutzung von bereits auf die parallele Berechnung von Millionen vonPartikeln optimierten Gra�kkarten. Das Anfang 2013 vorgestelle NVIDIA Flagschi�, NVIDIATITAN, stellt dabei 2688 Rechenkerne zur Verfügung [TIT13]. Im Gegensatz zur langsamenSteigerung der Kern-Zahlen bei CPUs verdoppeln sich diese bei GPUs mit nahezu jeder Ge-neration [KWH10][S. 2]. Eine Übersicht der Entwicklung der Rechenleistungen von GPUs undCPUs �ndet sich in Abbildung 1.1. Die in GPUs verbauten Recheneinheiten sind dabei deut-lich schwächer als ihre in CPUs verbauten Verwandten. Während bei Letzteren Threads aufsequentielle Ausführungszeit optimiert sind, wird bei GPUs der Durchsatz maximiert. Dieswird durch Ausnutzung der Parallelität und nicht durch ausgefeilte Kontroll�usslogik erreicht[KWH10][S. 4]. In besonders für die GPU-Ausführung geeigneten Anwendungsfällen sind damitGeschwindigkeitssteigerungen um mehr als den Faktor 100 realistisch.

Page 9: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

2

Abbildung 1.1: Vergleich der Entwicklung der Rechenleistung von CPUs und GPUs [cud13][S. 2]

Page 10: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

Kapitel 2

Stand der Technik und Ziel der Arbeit

Der klassische Einsatzzweck von Gra�kkarten ist die Berechnung von Visualisierungen, insbeson-dere auch für Computerspiele. Die Verwendung von Gra�kkarten zur Berechnung komplexerAlgorithmen und Formeln, wie es z.B. für eine Fluid-Simulation notwendig ist, ist dagegen ver-hältnismäÿig neu. Um das Jahr 2000 wurde das Potential dieser Art der Berechnung erkannt.Zu diesem Zeitpunkt gab es allerdings noch keine für derartige Berechnungen ausgelegte Ap-plication Programming Interface (API). Stattdessen wurde auf existierende Schnittstellen wieOpen Graphics Library (OpenGL) und DirectX zurückgegri�en und die numerische Berechnungdurch die bereits verfügbaren Domänenobjekte der Gra�kberechnung abgebildet. Beispielsweisewurden für die Datenblöcke der Berechnung für die Speicherung von Farbblöcken gedachte Spei-cherstrukturen verwendet. Diese Art der numerischen Berechnung unter Verwendung von beste-henden Gra�k-Schnittstellen wird gemeinhin als General Purpose GPU (GPGPU)-Berechnungbezeichnet [SK11][S. 5] [fer09][S. 3].

2.1 GPU-Computing

Da diese Art der Nutzung der Gra�kkarte unübersichtlichen und schlecht wartbaren Quelltextproduziert, führte NVIDIA im November 2006 Compute Uni�ed Device Architecture (CUDA)

ein. CUDA wird näher in Kapitel 3 beschrieben. Durch das neue Programmiermodell wurdenLimitierungen durch die Nutzung von GPGPU-APIs aufgehoben und eine speziell für numerischeBerechnungen erstellte API eingeführt [SK11][S. 6f] [KWH10][S. 5]. Dadurch kann explizit spe-zi�ziert werden, ob ein Codeteil auf der für sequentiellen Code optimierten CPU oder auf derfür parallele Algorithmen optimierten GPU ausgeführt werden soll.

Die eigentliche Programmierung erfolgt dabei in C mit einer entsprechenden Erweiterung fürCUDA. Diese erlaubt die Spezi�kation unterschiedlicher Parameter, wie z.B. die Anzahl anThreads, die an der Ausführung beteiligt sind, deren Aufteilung, sowie die Nutzung verschiedenerSpeicher-Architekturen. Sämtliche Parameter zur Ausführung auf der Gra�kkarte werden dabeihändisch vom Entwickler spezi�ziert. Dieser wird dabei nur sehr wenig von eventuell vorhandenenBibliotheken oder der CUDA-API selbst unterstützt und muss durch grundlegendes Verständ-nis der zugrunde liegenden Hardware abschätzen, welche Optimierungsmöglichkeiten am Bestenzum jeweiligen Anwendungsfall passen.

Im Gegensatz zu GPGPU-Berechnungen steht damit eine explizite Schnittstelle für numerischeBerechnungen zur Verfügung. Diese Art der Berechnung bezeichnet man auch alsGPU-Computing[fer09][S. 3].

Page 11: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

2.2. Native GPU-Programmierung vs. Java-Abstraktion 4

2.2 Native GPU-Programmierung vs. Java-Abstraktion

Die Entwicklung von CUDA als Schnittstelle, die explizit die Ressourcen der GPU für numerischeBerechnungen bereitstellt, ist als Evolutionsschritt in der Nutzung der Gra�kkarte zu verstehen.Die native Art der Programmierung bringt dabei einige Nachteile mit sich.

Im Wesentlichen lassen sich dabei die Nachteile von C++ und weiterer nativer Sprachen bzw.die Gründe zur Einführung höherer Programmiersprachen, wie z.B. Java, übertragen.

C bzw. C++ erfordern, dass der Entwickler die Speicherverwaltung selbst übernimmt. Im Fallvon CUDA-C ist die Sachlage noch komplexer, da hier zusätzlich spezi�ziert werden muss, woder Speicher (CPU oder GPU) alloziert werden soll. Gegebenenfalls müssen entsprechende Datenmanuell in den Speicher der GPU transferiert werden. Ein erklärtes Ziel von Java ist es dagegen,diese explizite Speicherverwaltung an die Sprache zu delegieren [GM96][S. 14]. Im Wesentlichensind auch die Verwaltung des GPU-Speichers sowie der zugehörige Transfer-Vorgang kapselbar.Diese können entsprechend von einer Bibliothek übernommen werden.

Ein groÿer Vorteil von Java ist deren Objektorientierung, die es erlaubt, groÿe Programme inkleinere Strukturen aufzuspalten und damit den resultierenden Quelltext lesbar zu gestalten.C++ bietet entsprechende Möglichkeiten zur Objektorientierung ebenfalls, CUDA-C dagegennicht. Entsprechend entstehen Programme mit vielen globalen und speziell für CUDA annotiertenFunktionen und Variablen, die schnell in einer unübersichtlichen Architektur resultieren.Komplexität, die durch die Nutzung der Hardware-Architektur, also beispielsweise speziellerSpeicher-Architekturen oder Ausführungsmodelle, entsteht, kann so nicht gekapselt werden undfällt entsprechend mehr ins Gewicht. Die Beschreibung der Summation aller Elemente einesArrays ist z.B. eine Anfängeraufgabe. Dagegen benötigt eine e�ziente parallele Implementierungmehrere Schleifen, Synchronisation und komplexe Speicherarchitekturen (siehe dazu [Par07], Al-gorithmus von Blelloch).

Die Erstellung des Quelltextes ist mitunter ein wesentlicher Unterschied der Programmierumge-bungen. Durch die eingeschränkten Möglichkeiten in Java (z.B. keine Pointer) steht eine sehr guteIntegrierte Entwicklungsumgebung (IDE)-Unterstützung zur Verfügung. Refactoring-Optionen,statische Code-Analyse und unterschiedlichste Bibliotheken helfen dabei verhältnismäÿig schnellgut zu wartenden Quelltext zu erstellen.Dagegen ist die IDE-Unterstützung für C++ und CUDA-C bestenfalls rudimentär. FehlendeRefactoring-Optionen, wenig statische Code-Analyse, also z.B. keine Anzeige von unbenutztenVariablen usw., führen zu schlecht wartbarem und unübersichtlichem Quelltext. Die Ausführungvon in CUDA-C spezi�zierten Methoden auf der CPU ist in der Bibliothek nicht vorgesehen underschwert damit gleichzeitig die Testbarkeit.

Der Quelltext selbst ist in beiden Umgebungen schnell erstellt. Diesen allerdings anschlieÿendlau�ähig zu gestalten benötigt dagegen eine sehr unterschiedliche Zeitspanne. Nachfolgend sindbeispielhaft zwei auftretende Fehlermeldungen aufgelistet:

Der Thread 'Win64-Thread' (0x1564) hat mit Code 1 (0x1) geendet.

Eine Ausnahme (erste Chance) bei 0x000007fefe199e5d in Fluid_Solver.exe:

Microsoft C++-Ausnahme: cudaError_enum an Speicherposition 0x0014f590..

Page 12: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

2.3. CUDA-Abstraktionsbibliotheken für Java 5

Die Aufgabe von Fehlermeldungen ist es auf einen aufgetretenen Fehler hinzuweisen. Diese An-forderung erfüllen die beiden Beispielmeldungen. Allerdings wird kein Hinweis auf die Ursachegegeben. Irgendwann im Programm ist ein Thread abgestürzt. Gleichzeitig ist irgendwo im Pro-gramm auf eine fehlerhafte Speicherposition der GPU zugegri�en worden. Fraglich bleibt dabei,wo exakt der Fehler aufgetreten ist. Ein Java-Entwickler ist daran gewöhnt die letzten Anweisun-gen in Form eines Stack-Traces auf der Konsole angezeigt zu bekommen, natürlich inklusiveZeilennummern und Quelltext-Datei. Der Fehler kann damit oftmals auf den ersten Blick er-kannt und behoben werden. Im Fall von CUDA-C und C++ ist die Verwendung des Debuggersnotwendig, wobei so lange Instruktionen durchlaufen werden müssen, bis durch systematischesEingrenzen die fehlerhafte Anweisung gefunden wurde.Auch dies tri�t allerdings nicht auf alle Fehler zu. Wird beispielsweise vergessen Speicher freizu-geben oder die Referenz auf allozierte Speicherbereiche verloren, so entstehen Lecks, die innerhalbder Applikation zu Fehlern führen. Die Ursache ist dabei nicht trivial zu �nden, da jede einzelneZeile im Quelltext dazu beitragen kann. In Java ist dies durch die automatische Speicherverwal-tung durch den Java Garbage Collector nicht möglich, womit eine signi�kante Steigerung derEntwicklungsgeschwindigkeit erzielt werden kann.

Entsprechendes ist ebenfalls Ziel der Java-Abstraktion. Auf der einen Seite sollte die expliziteSpeicherverwaltung dem Entwickler abgenommen werden und nur optional zur Verfügung stehen.Auf der anderen Seite sollte eine Fehlerbehandlung so implementiert werden, sodass die Fehlerauf konkrete Quelltext Zeilen verweisen und nicht auf Speicherbereiche.

2.3 CUDA-Abstraktionsbibliotheken für Java

Java bietet durchaus viele Vorteile gegenüber C++ und CUDA-C, wie aus dem vorherigen Ab-schnitt hervorgeht. Ab dem Jahr 2010 startete deshalb auch die Entwicklung verschiedener Ab-straktionsmodelle zur Nutzung von CUDA in Java.

Einzelne Bibliotheken werden näher in Kapitel 5.3 beschrieben. Grundsätzlich sind fünf Biblio-theken verfügbar: JCuda, Java-GPU, Aparapi, Rootbeer und Delite.

Rootbeer, Delite und Java-gpu sind dabei Bibliotheken, die an Universitäten entstanden sind.Delite stammt aus dem Pervasive Parallelism Laboratory (PPL) der Stanford-Universität [BSL+11],Rootbeer von der Syracuse-Universität in New York [PSFW12] und Java-gpu vom Trinity Collegein Dublin [Cal10]. Delite ist dabei die einzige Bibliothek, die aktiv von einer ganzen Gruppe wei-ter entwickelt wird. Die anderen Bibliotheken wurden in Master- oder Doktorarbeiten entworfenund implementiert.

Hinter Aparapi und JCuda stehen dagegen keine Universitäten. Zu JCuda ist hier nichts näherbekannt. Aparapi wurde hingegen von AMD zur SuperComputing 2009 entworfen und vorgestellt.Ende 2012, Anfang 2013 nahm auch ein Projekt der virtuellen Maschine von Java die Arbeit zurnativen Integration von GPUs auf (Projekt Sumatra, [Sum13a]).

Die Bibliotheken nehmen für sich in Anspruch, dem Entwickler die vielen Einzelschritte zurAusführung von Programm-Code auf der GPU abzunehmen und den generierten Code trotz-dem performant auszuführen. Dabei soll es insbesondere möglich sein, sehr viel einfacher undschneller einfach verständliche und gut lesbare Algorithmen zu entwickeln, die gleichzeitig auch

Page 13: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

2.3. CUDA-Abstraktionsbibliotheken für Java 6

testbar bleiben. Durch die entsprechend geringere Implementierungszeit soll dabei die schnelleEvaluierung und Implementierung unterschiedlicher Algorithmen ermöglicht werden [PSFW12].Die Entwickler der Bibliotheken schlieÿen dabei aber nicht aus, dass anschlieÿend getestete undevaluierte Algorithmen wiederum in CUDA-C oder Open Computing Language (OpenCL), demvon AMD geprägten Standard zur Programmierung heterogener Architekturen, nachprogram-miert werden, um das dort zur Verfügung stehende Optimierungspotential zu nutzen und dieentwickelte Implementierung weiter zu beschleunigen [apa13b].

Neben der vereinfachten Entwicklung von Algorithmen zur Ausführung auf der Gra�k-Hardwaresteht durch die Verwendung derartiger Bibliotheken allen in Java programmierten Anwendungendie Nutzung der zusätzlichen, parallelen, Ressourcen o�en. Somit können auch Anwendungen,die groÿe Datenmengen verarbeiten, durch die stark nebenläu�ge Ausführung beschleunigt wer-den. Ein Beispiel hierfür ist die angedachte Beschleunigung der Apache Hadoop Bibliothek zurAuswertung groÿer Datenmengen durch die Verwendung von GPU-Berechnungen.

Ziel der Arbeit

Diese Arbeit verfolgt im Wesentlichen zwei Einzelziele:

Die oben genannten fünf Bibliotheken sind auf dem Markt verfügbar und können frei verwendetwerden. Einige Bibliotheken sind im Entwicklungsstand weiter fortgeschritten, einige sind mittenin der Entwicklung und manche werden bereits nicht mehr weiter entwickelt. Eine Übersicht überden momentanen Entwicklungsstand bzw. ein unvoreingenommener Vergleich existiert im Au-genblick nicht. Dieser stellt, einschlieÿlich der Beschreibung der Funktionsweise der Bibliothekensowie einem Funktions- und Geschwindigkeitsvergleich, eine Säule der Arbeit dar.

Als zweite Säule dient der Nachweis, dass eine ausgewählte Bibliothek in der Lage ist, einenkomplexen Anwendungsfall auf der GPU zu implementieren. Dabei soll gezeigt werden, dass dieFunktionalität der resultierenden Anwendung identisch ist, gleichzeitig der entstehende Quelltextaber besser zu warten ist. Insbesondere soll dabei Wert auf die Testbarkeit der nebenläu�genAlgorithmen sowie auf eine objektorientierte Programmstruktur gelegt werden. Unter Umständenist es dabei auch nötig eine für die Portierung ausgewählte Bibliothek weiter anzupassen, sodassder Quelltext besser lesbar, schneller ausführbar oder strukturierter ist.

Die beiden Ziele sind jeweils auf die bereits erwähnte Simulation von Flüssigkeiten zu beziehen.Das Fraunhofer IWU entwickelt derzeit einen auf CUDA basierenden Simulator, der unter Nutzungverschiedener, für die GPU optimierter, Algorithmen Fluide simulieren und anzeigen kann. DieBerechnung umfasst dabei die Lösung komplexer Poisson- und Di�erentialgleichungen. Der An-wendungsfall ist optimal, da auf der einen Seite komplexe und zeitaufwändige Berechnungendurchgeführt werden müssen, gleichzeitig aber auch jede Zelle unabhängig von jeder anderenZelle berechnet werden kann.

Page 14: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

Kapitel 3

Programmierung einer GPU

Im bisherigen Verlauf der Arbeit wurden CUDA und die GPU-Programmierung nur namentlicherwähnt. Um verschiedene Abstraktionsbibliotheken evaluieren zu können muss dagegen eingrundsätzliches Verständnis der Hardware-Architektur sowie des Programmiermodells vorliegen.Deswegen führt der nachfolgende Abschnitt näher in CUDA ein, beschreibt das Ausführungs-modell und die Speicherarchitektur der Gra�kkarte sowie, schlieÿlich, den CUDA-KonkurrentenOpenCL.

3.1 Architektur einer GPU mit CUDA Unterstützung

GPUs zeichnen sich durch die massiv parallele Ausführung von Aufgaben aus. Genau wie beiCPUs wird die nebenläu�ge Ausführung eines Programms auch hier durch mehrere Ausführungs-fäden (Threads) erreicht. Um sich allerdings nicht, wie auf CPUs, durch die Verwendung mehrerer1000 Threads, Probleme wie Race-Conditions oder Deadlocks einzuhandeln, stellen NVIDIA,bzw. die Khronos Group für OpenCL, eine spezielle Architektur zur Verfügung. Die nachfol-gende Beschreibung konzentriert sich dabei auf CUDA, da diese im Fraunhofer IWU verwendetwird. OpenCL funktioniert dabei, unter Verwendung anderer Begri�e, analog.

3.1.1 Thread-Organisation

Die Verwaltung der Menge an verfügbaren Threads folgt in CUDA einer speziellen Organisation.

Threads werden immer in sogenannte Blöcke partitioniert, wobei die maximale Blockgröÿe einehardwarespezi�sche Konstante darstellt. Bei modernen GPUs liegt diese bei 1024 Threads proBlock, bei Älteren nur bei 512 oder weniger. Alle Threads in einem Block werden auf der GPU inder gleichen Hardwareeinheit abgearbeitet. Ausgeführt werden die Threads nicht einzeln, sondernimmer als Gruppe von Threads, sogenannte Warps. Ein Warp bezeichnet eine Menge von 32aufeinander folgender Threads [KWH10][S. 71]. Der Begri� Warp ist NVIDIA spezi�sch. AMDbzeichnet in OpenCL 64 Threads, die zusammen ausgeführt werden, als Wavefronts [gcn12][S.3].

Threads auf CPUs unterscheiden sich stark von Threads auf GPUs. Während Threads auf CPUsjeweils unterschiedliche Instruktionen ausführen können, führen alle Threads innerhalb einesgerade auszuführenden Warps immer dieselbe Instruktion aus. NVIDIA bezeichnet das Ausfüh-rungsmodell als Single Instruction, Multiple Thread (SIMT) [cud13][S. 66]. Bevor jeweils dienächste Instruktion geladen und dekodiert wird, wird demnach die momentane Instruktion aufalle Threads angewendet [KWH10][S. 98]. Dieses Vorgehen resultiert in einer massiven Entlas-

Page 15: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

3.1. Architektur einer GPU mit CUDA Unterstützung 8

tung der Hardware zum Holen und zur Dekodierung neuer Instruktionen, da für alle 32 Threadsim aktiven Warp dieselbe Instruktion wiederverwendet werden kann.

Obwohl alle Threads in einem Warp demselben Ausführungspfad folgen, kann auch eine Verzwei-gung für einzelne Threads des Warps erfolgen (Branch Divergence, [cud13][S. 67], [KWH10][S.98]). Dies hat allerdings zur Folge, dass alle nicht vom Hauptpfad abweichenden Threads wartenmüssen, da auf Grund des SIMT-Ausführungsmodells eine Instruktion auf alle Threads des Warpsangewendet werden muss.

SIMT ist dabei nicht mit Single Instruction, Multiple Data (SIMD) zu verwechseln. Bei SIMDwird eine Instruktion auf viele Datensätze angewandt. Im Gegensatz dazu wird bei SIMT eineInstruktion an alle Threads des Warps verteilt. Die Instruktion beein�usst damit nicht nur dasErgebnis einer Rechnung, sondern den Kontroll�uss eines Threads [LNOM08][S. 44].

Die Organisation vorhandener Threads in Form eines Grids an Blöcken, die wiederum Threadsenthalten, ist in Abbildung 3.1 illustriert.

3.1.2 Ausführung von Blöcken und Warps

Hauptbestandteil der GPU sind die Streaming-Multiprozessoren (SMs). Im Wesentlichen handeltes sich dabei um die Kapselung eines Instruktions-Caches, einer Einheit zum Abholen neuerBefehle, einem speziellen Speicher, der näher in Abschnitt 3.1.3 erläutert wird, sowie um einebestimmte Anzahl an Streaming-Prozessoren (SPs) (alias CUDA-Core, die Fermi Architekturenthält beispielsweise 32 Stück [fer09][S. 8]) und Gleitkomma-Berechnungseinheiten.

Die auszuführenden Blöcke werden auf SMs mit freien Kapazitäten verteilt (siehe Abbildung 3.2)und von den SP des SM verarbeitet. Wie viele Blöcke parallel ausgeführt werden können hängtvon den benötigten Ressourcen eines Blocks ab. Sind nicht genügend Ressourcen vorhanden,um z.B. acht Blöcke parallel auszuführen, so werden automatisch weniger Blöcke an den SMverteilt [KWH10][S. 70f]. Da damit weniger Blöcke gleichzeitig ausgeführt werden können, sinktdamit auch die Nebenläu�gkeit der Gesamtausführung und letztendlich auch die Ausführungs-geschwindigkeit.

Beispiele für begrenzte Ressourcen beziehen sich auf die maximale Anzahl an Threads, die voneinem SM überwacht und ausgeführt werden können. Bei modernen GPUs liegt diese Konstantebei 2.048 Threads [kep12][S. 7], die beispielsweise in zwei Blöcke mit jeweils 1.024 Threads, vierBlöcke mit jeweils 512 Threads oder acht Blöcke mit 256 Threads aufgeteilt werden können. Wenndie Obergrenze an parallel abarbeitbaren Blöcken schon bei 8 Blöcken liegt, ist eine Aufteilungin mehr als 8 Blöcke u.U. ebenfalls nicht möglich [KWH10][S. 71].

Ein weiteres Beispiel für begrenzte Ressourcen ist die maximale Anzahl an Registern, die pro SMzur Verfügung stehen. Moderne GPUs stellen hier 65.536 Register zur Verfügung. Geht man voneiner maximalen Anzahl von 2.048 Threads im Block aus, so stehen jedem Thread 32 Registerzur Verfügung. Werden mehr Register pro Thread verwendet, so wird die Anzahl an Blöcken proSM reduziert und damit die Ausführungszeit u.U. deutlich erhöht [KWH10][S. 112f].

Schlieÿlich ist noch zu klären, warum Blöcke überhaupt in Warps aufgeteilt werden. Es handeltsich dabei um das Mittel der GPU zur Steigerung des Datendurchsatzes. Grund dafür ist, dassein Warp während einer längeren Warte-Operation, wie z.B. einem Speicherzugri�, blockiert ist.

Page 16: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

3.1. Architektur einer GPU mit CUDA Unterstützung 9

DeviceHost

Kernel1

Grid 1

Block(0,0)

Grid 2

Block(1,0)

Block(0,1) Block (1,1)

Block(1,1)

Thread(1,0,0)

Thread(2,0,0)

Thread(0,0,0)

Thread(1,1,0)

Thread(2,1,0)

Thread(0,1,0)

(0,0,1) (1,0,1) (2,0,1)

Kernel1

Abbildung 3.1: Thread-Organisation [KWH10][S. 63]

In diesem Fall kann der Warp-Scheduler, der dafür zuständig ist, welcher Warp als nächstesausgeführt wird, einen anderen Warp auswählen und ausführen. Beim Wechsel entsteht keinzusätzlicher Overhead (Zero-Overhead-Scheduling). Im Ende�ekt wird also, durch den schnellenWechsel, die Latenz, die durch die langen Wartezeiten entsteht, versteckt und die Ressourcenoptimal ausgenutzt (Latency-Hiding, [KWH10][S. 73]).

3.1.3 Speichermodell und -zugri�

Zur optimierten Ausführung muss der Entwickler die Hardware-Architektur der GPU kennen.Insbesondere die Verwendung spezieller Speicherarchitekturen ermöglicht dabei eine signi�kanteSteigerung der Ausführungsgeschwindigkeit. Allein die Verwendung des nachfolgend beschriebe-nen Shared-Memory ermöglicht im Anwendungsfall Matrix-Multiplikation eine weitere Steigerungder Geschwindigkeit um den Faktor 16 [KWH10][S. 90].

Host, Device und Kernel Vor der eigentlichen Beschreibung, welche Speichertypen Gra�k-karten zur Verfügung stehen, werden noch drei Begri�e eingeführt. NVIDIA unterscheidet dieBegri�e Host und Device. Device verweist dabei auf die GPU und den zugehörigen Speicher,Host auf die CPU und deren Speicher [SK11][S. 23]. Die Speicherbereiche sind normalerweisephysikalisch voneinander getrennt, sodass ein Aufruf auf dem falschen Gerät eine Fehlermeldungauslöst. Abschlieÿend wird als Kernel eine Funktion bezeichnet, die auf einem Device ausgeführtwird [SK11][S. 23].

Die Speichertypen unterscheiden sich v.a. in Hinblick auf Zugri�sgeschwindigkeit und Sicht-barkeit der Speicherinhalte.

Page 17: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

3.1. Architektur einer GPU mit CUDA Unterstützung 10

CUDA ProgrammBlock 0 Block 1 Block 2 Block 3

Block 4 Block 5 Block 6 Block 7

SM 0 SM 1GPU mit 2 SMs

Block 0

Block 2

Block 4

Block 6

Block 1

Block 3

Block 5

Block 7

SM 0GPU mit 4 SMs

SM 1 SM 2 SM 3

Block 0 Block 1 Block 2 Block 3

Block 4 Block 5 Block 6 Block 7

Abbildung 3.2: Aufteilung von Blöcken auf mehrere SM [cud13][S. 7]

Als Ziel für den initialen Kopiervorgang können Daten vom Host entweder in das Global-,Constant- oder Texture-Memory des Devices geladen werden.

Global Memory Der globale Speicher umfasst typischerweise mehrere GB an Speicherplatzund bietet damit Raum zum Speichern aller notwendigen Rechendaten. Die Menge an Speicher-platz wird dabei mit sehr hohen Zugri�szeiten erkauft, die leicht mehrere hundert Taktzyklenbetragen können. Damit wird ein entsprechender Zugri� schnell zum Flaschenhals einer App-likation. Im Ende�ekt entsteht schlieÿlich aus einem Zugri� auf das Global-Memory durch dienebenläu�ge Ausführung von Threads und Warps eine ganze Menge an Zugri�en [cud13][S. 74].

Constant Memory Elemente im bereits erwähnten Speicher für Konstanten werden im glo-balen Speicher abgelegt. Statt allerdings bei jedem Zugri� wiederum den langsamen Speicherzu-gri� auszuführen, wird der Inhalt der letzten Zugri�e in einem speziellen Cache auf dem SMzwischengespeichert (Constant-Cache). E�zient ist dieser Speicher damit nur nutzbar, wenn einSpeicherbereich mehrfach bzw. von vielen unterschiedlichen Threads abgerufen wird, da nur indiesem Fall die geringe Zugri�szeit greift. Ist ein Datum nicht im Cache vorhanden, so musstrotzdem auf den langsamen globalen Speicher zurückgegri�en werden. Um Inkonsistenzen zuvermeiden, ergibt sich durch die Zwischenspeicherung im Cache eines SM die Limitierung, dassnur der Host den Konstanten-Speicher verändern darf. Für das Device ist dieser Speicherbereichnur lesbar.

Schlieÿlich ist auch die Menge an Elementen, die im Speicherbereich für Konstanten zwischen-gespeichert werden können, sehr beschränkt. Eine aktuelle NVIDIA GTX 680 Gra�kkarte bietetdafür rund 65 kB an Speicher [KWH10][S. 82]. Die Begrenzung auf eine relativ geringe Mengean verfügbarem Speicher auf SMs ist charakteristisch für CUDA und �ndet sich teilweise auchin den weiteren speziellen Speichertypen wieder.

Texture Memory Ein weiterer, dem Constant Memory sehr ähnlicher, Speichertyp ist dasTexture Memory. Entsprechend dem Constant Memory werden auch hier Daten in einen Texture-

Page 18: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

3.2. CUDA Programmierung 11

Cache auf dem Device zwischengespeichert. Jedoch ist der Cache darauf optimiert gute Zugri�-szeiten auch auf direkt angrenzende Zellen zu bieten [cud13][S. 73].

Drei weitere Speichertypen sind ausschlieÿlich für das Device sicht- und schreibbar.

Register Zuerst gibt es die bereits erwähnten Register, die jedem Thread zur Verfügung stehen.Diese Art der Speicherung besitzt die schnellste Zugri�szeit, ist allerdings durch die zur Verfügungstehende Anzahl an Speicherplätzen sehr begrenzt. Moderne Devices stellen hier zwischen 63 und255 Register pro Thread zur Verfügung [kep12][S. 7].

Werden in einem Kernel lokale Arrays verwendet bzw. mehr Register initialisiert, als für einenThread zur Verfügung stehen, so wird dieser Speicher im globalen Speicher alloziert und stehtdementsprechend auch mit der damit einhergehenden langsamen Zugri�szeit zur Verfügung[cud13][S. 77]. Diese Art von Speicher nennt man lokalen Speicher (Local-Memory).

Shared-Memory Der letzte Speicher, der in diesem Rahmen erwähnt werden soll, ist dasShared-Memory. Der Speicherbereich steht, im Gegensatz zu Registern und dem lokalen Spei-cher, nicht pro Thread, sondern pro Block zur Verfügung. Genau wie bei Registern ist dieserSpeicherplatz auf dem Chip angesiedelt und damit sehr schnell im Zugri�. Allerdings ist derSpeicherplatz stark begrenzt. Ein modernes Device stellt hier rund 48 kB zur Verfügung. DerSpeicherplatz selbst ist in mehrere Bänke, typischerweise 16, aufgeteilt. Gleichzeitige Zugri�e aufdie selbe Bank werden serialisiert ausgeführt und als Bank-Kon�ikt (bank con�ict) bezeichnet[cud13][S. 77].

Shared-Memory eignet sich gut um in einem Block mehrere Threads kooperieren zu lassen[KWH10][S. 80]. Allerdings handelt man sich im Gegenzug eine stark erhöhte Komplexität ein,da mit falschen Zugri�en Bank-Kon�ikte auftreten können, die die Geschwindigkeitssteigerungwieder zunichte machen. Auÿerdem erfordert die Programmierung von Shared-Memory spezielleAlgorithmen, sodass nur Threads in einem Block kooperieren müssen und nicht auf Ergebnissevon anderen Blöcken angewiesen sind. Diese stehen auf Grund der lokalen Natur des Speichertypsnicht zur Verfügung.

Gerade im Hinblick auf die unterschiedlichen Speichertypen lassen sich bei einer Anwendunghohe Geschwindigkeitssteigerungen erzielen. Gleichzeitig entsteht, v.a. durch Shared-Memory,eine stark erhöhte Komplexität, da der Programm-Code unter dessen Nutzung nicht nur von denauszuführenden Threads, sondern zusätzlich von der zugrunde liegenden Blockstruktur abhängigist.

3.2 CUDA Programmierung

Um anschlieÿend verschiedene Frameworks zur CUDA-Abstraktion evaluieren zu können, stelltdieser Abschnitt die Kern-Anweisungen von CUDA-C vor. In keiner Weise sollen alle Konzepteund Anweisungen der Spracherweiterung abgedeckt werden. Dafür wird an dieser Stelle auf dieBücher [SK11] und [KWH10] verwiesen.

Page 19: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

3.2. CUDA Programmierung 12

(Device) Grid

Block (0,0)Shared Memory

Register

Thread (0,0)

Global Memory

Register

Thread (1,0)

Block (1,0)Shared Memory

Register

Thread (0,0)

Register

Thread (1,0)

Constant Memory

Host

Abbildung 3.3: Device Speichermodell [KWH10][S. 47]

3.2.1 Beispiel Array-Inkrementierung

Als Einstiegsprogramm sollen alle Elemente eines Arrays inkrementiert werden. Das Programmist nicht besonders komplex und weist eine sehr hohe Speichersättigung durch das geringe Ver-hältnis zwischen Berechnung und Speicherzugri� auf, dient aber gerade auf Grund der Unab-hängigkeit der Einzeloperationen als gutes Beispiel. Die Lösung ist in Abbildung 3.1 abgedruckt.

Wie bereits erwähnt ist CUDA-C eine Erweiterung der Programmiersprache C. Einstiegspunktdes Programms ist folglich eine implementierte main-Methode. Als Erstes wird der zu bearbei-tende Array initialisiert und in eine Variable h_content geschrieben. Das h_ steht dabei fürHost. Dies ist eine gern verwendete Konvention, um unterscheiden zu können, ob der zugehörigeSpeicher auf dem Device (d_) oder auf dem Host zu �nden ist.

Anschlieÿend wird globaler Speicher auf der Gra�kkarte alloziert. Das Kommando cudaMalloc

übernimmt dies nach Übergabe einer Variablenadresse und der erwarteten Speichergröÿe. Diesentspricht im Wesentlichen dem Verhalten des malloc-Kommandos in C.

Mit cudaMemcpy(Ziel, Quelle, Gröÿe, Richtung) wird der Inhalt einer Variablen auf das De-vice verschoben. Einzig erklärungsbedürftiger Parameter ist hier die Richtung. Diese gibt an, obInhalt vom Host auf das Device kopiert wird (cudaMemcpyHostToDevice), vom Device auf das De-vice (cudaMemcpyDeviceToDevice) oder vom Device auf den Host (cudaMemcpyDeviceToHost).

Nachdem der Speicher mit den zu bearbeitenden Werten gefüllt ist, kann ein Kernel-Aufruferfolgen. Dieser sieht wie ein normaler Methodenaufruf aus. Einziger Unterschied sind die beidenVariablen gridSize und blockSize, die in drei spitze Klammern gehüllt sind (<<< ... >>>).In Abschnitt 3.1.1 wurde beschrieben, dass Threads immer in Blöcke aufgeteilt werden. An derStelle der Variable gridSize wird hier die Anzahl an Blöcken eingetragen, die zur Ausführungbenötigt werden, während blockSize angibt, wie viele Threads in einem Block vorhanden sind[KWH10][S. 42]. Im Beispiel werden also zwei Blöcke mit jeweils 512 Threads initialisiert. Wie inAbbildung 3.1 angedeutet, ist auch die Übergabe mehrdimensionaler (bis zu drei Dimensionen)

Page 20: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

3.2. CUDA Programmierung 13

__global__ void incrementKernel(unsigned int *d_content ,

unsigned int count) {

int tid = blockIdx.x * blockDim.x + threadIdx.x;

if (tid > count) return;

d_content[tid] += 1;

}

int main(void) {

int count = 1000, size = sizeof(unsigned int) * count;

//... (Initialisierung des Arrays)

unsigned int *d_content;

cudaMalloc ((void **) &d_content , size);

cudaMemcpy(d_content , h_content , size , cudaMemcpyHostToDevice );

int blockSize = 512;

int gridSize = (count / blockSize) + 1;

incrementKernel <<<gridSize , blockSize >>>(d_content , count );

cudaMemcpy(h_content , d_content , size , cudaMemcpyDeviceToHost );

//... (Ausgabe des Inhalts und Freigabe der Speicherbereiche)

}

Quelltext 3.1: Map-Kernel zur Inkrementierung aller Array-Elemente

Page 21: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

3.2. CUDA Programmierung 14

Blöcke möglich. Für verschiedene Anwendungsfälle, z.B. bei der Implementierung eines Kernelszur Matrixmultiplikation, sind zwei Dimensionen sinnvoll, da dadurch nicht händisch die x-und y-Koordinaten aus einer globalen Thread-ID berechnet werden müssen. Damit wird derQuelltext übersichtlicher und durch eine fehlende, und sehr zeitaufwändige, Modulo-Operationzur Berechnung der x- und y- Koordinaten aus einer globalen ID, schneller. Gleichzeitig muss beider Spezi�kation der Blockgröÿen beachtet werden, dass auch hier nicht die maximale Blockgröÿevon 512 bzw. 1.024 Threads pro Block überschritten wird.

Nach Ausführung des Kernels wird der Inhalt von der Gra�kkarte wieder zurück kopiert undausgegeben. Schlieÿlich sollte nicht vergessen werden den Speicher auf der Gra�kkarte wiedermit cudaFree freizugeben. Speicherlecks wirken sich hier bis zum Neustart des Rechners aus.

Das eigentliche Kernstück des Programms ist der nebenläu�g ausgeführte Kernel. Dieser sieht imGroÿen und Ganzen wie eine C-Funktion aus. Besonders wichtig ist das Kürzel __global__ inder Signatur der Funktion. Dieses gibt an, dass die Funktion auf dem Device auszuführen ist.Neben __global__ existieren noch die Kürzel __device__ und __host__. Ersteres bezeichnetdabei Funktionen, die nur vom Device aufgerufen werden und auch auf dem Device wiederum aus-geführt werden. Letzteres bezeichnet Funktionen, die auf dem Host ausgeführt werden. __host__ist auch der Standardfall für alle nicht annotierten Funktionen. Die letzten beiden Kürzel könnenauch in Kombination verwendet werden. Damit wird angegeben, dass eine Funktion sowohl vonHost als auch von Device aufrufbar ist [KWH10][S. 52].

Die Grundidee an diesem Kernel ist, dass jeder Thread ein Element des Arrays inkrementiert.Um festzustellen, welcher Thread welches Element inkrementieren soll, ist eine Abbildung derThreads auf den Zahlenraum der natürlichen Zahlen notwendig. CUDA stellt hierfür die Varia-blen blockId, blockDim und threadId zur Verfügung. Theoretisch könnten hier, wie vorhererwähnt, alle Dimensionen x, y und z verwendet werden. Da in diesem Beispiel nur die x-Dimension verwendet wird, wird auch nur .x auf den Variablen aufgerufen. Die eindeutige IDeines Threads ergibt sich durch Multiplikation der Block Dimension (blockDim.x)mit der Block-ID (blockIdx.x) addiert mit der momentanen Thread-ID im aktuellen Block (threadIdx.x).

Da immer ganze Blöcke initialisiert werden, kann es vorkommen, dass Threads mit den höchstenzugewiesenen Zahlen keine Entsprechung im Array �nden, da dieser nicht genügend Elementeaufweist. Um Ausführungsfehler zu vermeiden wird dies vor der Ausführung der eigentlichenKernel-Anweisungen geprüft. Erst dann �nden Array-Zugri�e und Berechnungen statt.In diesem Fall werden beispielsweise 2 · 512 = 1024 Threads für eine Array-Gröÿe von 1000gestartet. Ohne die Prüfung fänden 24 Threads keine Entsprechung im Array. Bei der Ausführungwürde folglich eine unspecified launch failure Fehlermeldung auftreten.

3.2.2 Synchronisation von Threads

Zur Synchronisation von Threads steht eine Methode zur Verfügung, die es erlaubt durch eineBarriere darauf zu warten, dass alle Threads im aktuellen Block bis zur Barriere aufgeschlossenhaben. Erst dann wird die Ausführung fortgesetzt. Zur Erstellung einer derartigen Barriere wirddie Funktion __syncthreads() aufgerufen [cud13][S. 94].

Allerdings sind bei der Verwendung dieser Methode zwei Fallstricke zu beachten:

Page 22: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

3.2. CUDA Programmierung 15

Die Barriere gilt nur für alle Threads in einem Block. Eine Synchronisierung über mehrere Blöckehinweg ist generell nicht möglich. Durch diese Einschränkung können alle Blöcke unabhängigvoneinander ausgeführt werden. Allerdings ist darauf zu achten, dass alle zu berechnenden Pro-grammabschnitte entsprechend unabhängig voneinander sind, sodass trotz der fehlenden Syn-chronisation das korrekte Ergebnis ermittelt wird [KWH10][S. 69]. Die Aufteilung in unabhängigeBlöcke �ndet sich in der Literatur auch unter dem Stichwort tiling.

Ein zweiter Fallstrick bezieht sich auf Synchronisationspunkte, die nur unter vorheriger Auswer-tung einer if-Bedingung erreichbar ist. Wird die Bedingung von einigen Threads positiv und voneinigen Threads negativ ausgewertet, so blockiert die Ausführung, da nicht alle Ausführungspfa-de die Barriere erreichen. Die weitere Ausführung wird erst freigegeben, wenn alle Threads genaudiese erreicht haben. Auch das Setzen eines zusätzlichen Synchronisationspunktes in den else

Zweig der Bedingung scha�t hier keine Abhilfe, da dieser Punkt als neue Barriere angesehen wird[KWH10][S. 69].

Da die Ausführungsumgebung garantiert, dass ein Kernel-Aufruf vor einem nachfolgenden KernelAufruf abgeschlossen ist, ist auch eine Art Synchronisation über mehrere Kernel-Aufrufe hinwegmöglich. Ein Block übergreifender Synchronisationspunkt ist in diesem Fall das Ende eines Ker-nel. Diese Art der Synchronisation verursacht allerdings zusätzliche Kosten, da die verschiedenenKernel-Aufrufe verwaltet werden müssen [cud13][S. 70].

3.2.3 Kompilierung von CUDA-Code

Um CUDA-Code zu kompilieren stellt NVIDIA einen eigenen Werkzeugkasten zur Verfügung.Dieser beinhaltet verschiedene Werkzeuge und Formate zur Kompilierung und Ausführung.

Zu Beginn der Kompilierung werden der Host- und der Device-Code in der Quelltext-Dateigetrennt. Der Host-Code wird als normaler C oder C++ Code weiter kompiliert. Unter Unixkommt hier die GNU Compiler Collection (GCC) zum Einsatz, unter Windows der Visual StudioC Compiler [nvc13][S. 2]. Der Device-Code wird getrennt behandelt.

CUDA unterscheidet zwischen einer virtuellen Architektur (compute architecture) und einerrealen GPU-Architektur (sm architecture). Grund hierfür ist, dass NVIDIA die Binärkompatibili-tät der Gra�kkartengenerationen zugunsten zukünftiger Verbesserungen nicht zusichern möchte.Stattdessen wurde das Parallel Thread eXecution architecture (PTX)-Zwischenformat eingeführt,das in keiner Weise von den eigentlich auf der GPU zur Verfügung stehenden Instruktionen ab-hängt, sondern nur über die zur Verfügung stehenden GPU-Features, z.B. atomare Operationen,de�niert wird. Erst in einem zweiten Schritt wird von der GPU abhängiger binärer Code erzeugtund ggf. in einer binären cubin-Datei gespeichert.

Wie in Abbildung 3.4 illustriert werden beide Architekturen bei der Kompilierung angegeben. Dievirtuelle Architektur gibt also an, von welchen allgemeinen Features der Kernel-Code abhängt.Diese müssen von der eigentlichen Gra�kkarte später zur Verfügung gestellt werden. Die realeArchitektur muss zur virtuellen Architektur passen, muss aber nicht zwingend angegeben werden[nvc13][S. 29].

Stattdessen kann ein Just-In-Time (JIT)-Compiler verwendet werden. Zur Laufzeit wird vomAusführungssystem entschieden welche reale Architektur verwendet werden soll. Der Zwischen-

Page 23: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

3.2. CUDA Programmierung 16

*.cu (Device Code)

Phase 1 (nvopencc)

PTX

Phase 2(ptxas)

cubin

Virtuellecompute Arch.

Realesm Arch.

Ausführung(CUDA Gerätetreiber)

Abbildung 3.4: Kompilierung einer CUDAQuelltext-Datei über die PTX -Zwischenrepräsentationzu einer binären cubin-Datei [nvc13][S. 30]

code wird anschlieÿend für die reale Architektur kompiliert und ausgeführt [nvc13][S. 32].

Als Resultat der Kompilierung wird die cubin- bzw. PTX-Datei anschlieÿend in ein Fatbinary

geschrieben. Dieses kann auch mehrere bereits kompilierte cubin-Dateien für unterschiedlichereale Architekturen aufnehmen [nvc13][S. 32]. Schlieÿlich wird es mit der Host-Binärdatei vomHost-Linker zusammengefügt und als ausführbare Binärdatei abgelegt. Vor der Ausführung wirddie für die Ausführungsumgebung passende kompilierte bzw. die PTX-Datei gesucht und ausge-führt.

Nähere Informationen zur Kompilierung des CUDA-Quelltextes �nden sich in der Dokumentationzum NVIDIA Compiler [nvc13].

3.2.4 OpenCL

OpenCL ist eine standardisierte Sprache für heterogene, parallele Architekturen, wie z.B. GPUsvon NVIDIA und AMD. Die Programmierung erfolgt passend zu CUDA in C. Während CUDAsich allerdings auf die Programmierung von NVIDIA GPUs konzentriert, werden von OpenCLverschiedenste parallele Architekturen unterstützt. Beispielsweise kann auch eine Implemen-tierung von OpenCL für Field Programmable Arrays (FPGAs) erstellt werden [KWH10][S. 206].

Da der Standard damit sehr viele unterschiedliche Architekturen unterstützen muss, werdenspezielle Eigenschaften von Geräten nicht verwendet. Daraus folgt, dass OpenCL-Code zwangsläu-�g nicht so schnell ausführbar sein kann, wie es im Gegensatz dazu erstellte CUDA-Programmesein können, da diese nur auf NVIDIA Hardware hin optimiert werden [KWH10][S. 206].

Das Programmiermodell entspricht weitgehend dem Modell von CUDA. Quasi alle Begri�ich-keiten �nden in OpenCL ihre Entsprechung. Eine Übersicht, wie die Begri�e in OpenCL wieder-zu�nden sind, ist in Abbildung 3.5 dargestellt.

Wesentlich komplexer als in CUDA ist das Starten von Kernels sowie die Verwaltung von Geräten.

Page 24: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

3.2. CUDA Programmierung 17

OpenCL CUDAAusführung Kernel Kernel

Host HostNDrange GridWork Item ThreadWork Group Block

API-Aufrufe get_global_id(0) blockIdx.x * blockDim.x + threadIdx.x

get_local_id(0) threadIdx.x

get_global_size(0) gridDimx.x * blockDim.x

get_local_size(0) blockDim.x

Speicher Global-Memory Global-MemoryConstant-Memory Constant-MemoryLocal-Memory Shared-MemoryPrivate-Memory Local-Memory

Abbildung 3.5: Übersicht der Begri�ichkeiten in CUDA und OpenCL [KWH10][S. 207, 209, 210,231]

Während in CUDA standardmäÿig ein Gerät zur Ausführung ausgewählt wird, geschieht inOpenCL die Auswahl explizit. Der Start von Kernels erfolgt durch explizite Funktionsaufrufe, dievorher wiederum mit verschiedenen Funktionsaufrufen kon�guriert werden müssen [KWH10][S.212�]. Hintergrund ist hier, dass sehr unterschiedliche Hardware-Typen unterstützt werden, dieeine komplexere Geräteverwaltung erfordern.

Die Implementierung von NVIDIA für OpenCL kompiliert dabei zu PTX-Zwischencode. Ab hierkann die PTX-Zwischendatei genau wie jede beliebige, aus CUDA-Quelltext erzeugte, PTX-Dateibehandelt werden [ope09][S. 16].

Insgesamt ist OpenCL ein wichtiger Ansatz, um verschiedene Architekturen über eine abstrakteSchnittstelle ansteuern zu können. Für native, geschwindigkeitsabhängige, Anwendungen, istCUDA die bessere Wahl, da hier die Architekturspezi�ka besser ausgenutzt werden können. Füreine Java-Abstraktion ist u.U. OpenCL die bessere Wahl, da die aufwändige Implementierungeiner Abstraktion anschlieÿend alle zu OpenCL kompatiblen Geräte unterstützt.

Page 25: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

Kapitel 4

Navier-Stokes-Gleichungen zur

Flüssigkeitssimulation

Als Anwendungsfall zur Evaluation der Abstraktionsbibliotheken dient, wie schon erwähnt, dervom Fraunhofer IWU in CUDA-C implementierte Simulator. Der nachfolgende Abschnitt führtin die grundlegende mathematischen Formeln der Fluid-Simulation ein und zeigt auf, wie dieFormeln numerisch gelöst werden können. Da der Fokus der Arbeit auf der Abstraktion vonCUDA-C in Java liegt, konzentriert sich die Beschreibung auf die bereits im Simulator vorliegen-den Lösungsverfahren.

4.1 Impuls- und Inkompressibilitätsgleichungen

Die Gleichungen setzen sich aus der Impuls- und der Inkompressibilitätsgleichung zusammen,wobei die zweite Gleichung nur als Randbedingung der ersten Gleichung zu verstehen ist. DieImpuls-Gleichung errechnet dabei ein Geschwindigkeitsfeld ~u innerhalb des Simulationsraums.Durch Verschiebung von Einzelpartikeln im Vektorfeld ergibt sich die Flüssigkeitssimulation[CCE01][S. 1]. Im kompakter Form sehen die beiden Gleichungen wie folgt aus:

∂~u

∂t= − (~u · ∇) · ~u︸ ︷︷ ︸

Advektion

−1

ρ∇p︸ ︷︷ ︸

Druck

+ ν · ∇ · ∇ · ~u︸ ︷︷ ︸Diffusion

+ ~f︸︷︷︸Externe Krafte

(4.1)

∇ · ~u = 0 (4.2)

Die Impulsgleichung (Gleichung 4.1) berechnet den Impuls im Vektorfeld durch Anwendungverschiedener Kräfte: Advektion, Druck, Di�usion und externe Kräfte.

Der Gradienten-Operator (∇) innerhalb der Gleichungen bezeichnet dabei die Richtungsableitung[CCE01][S. 2]:

∇ =

(∂

∂x,∂

∂y,∂

∂z

)(4.3)

Wird der Operator auf ein Skalarfeld angewendet, so ergibt sich als Ergebnis ein Feld, das angibt,wie sich das ursprüngliche Feld räumlich verändert. Wendet man den Operator stattdessen aufein Vektorfeld an, so ergibt sich als Resultat die sog. Divergenz, die den Netto�uss von oder zueiner Zelle beschreibt.

Die Advektion (auch Transport) beschreibt die Verschiebung der Geschwindigkeitsvektoren überden Simulationsraum. Stellt man sich z.B. ein Partikel vor, so muss dieses mit einer bestimmten

Page 26: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

4.2. Diskretisierung des Simulationsraums 19

Geschwindigkeit durch den Raum bewegt werden. Im Ende�ekt werden die Partikel von denGeschwindigkeitsvektoren durch den Raum �gezogen� [CCE01][S. 2]. Zum Beispiel ist die Advek-tion auch die Kraft, die eine Feder, die in einem Fluss schwimmt, diesen hinab trägt.

Die zweite Kraft, der Druck, wird durch kollidierende Moleküle der Flüssigkeit in einem Bereichverursacht. Entsprechende Kollisionen führen zu Geschwindigkeitsänderungen und damit zu einerVeränderung des Impulses an der Position im Vektorfeld [FC04][S. 642]. Die Variable ρ beschreibtdie Dichte der zu simulierenden Flüssigkeit. Oft wird diese Variable mit dem konstanten Wert 1

belegt [CCE01][S. 2].

Eine weitere Kraft, die den Impuls im Vektorfeld beein�usst, ist die Di�usion. Passiert ein Par-tikel der Flüssigkeit ein Hindernis oder weitere Partikel mit einer anderen Geschwindigkeit, soverändert sich die Geschwindigkeit der passierenden Partikel und Wirbel entstehen [Rør04][S. 21].Die Änderung der Geschwindigkeiten erfolgt dabei durch Annäherung an das gemeinsame arith-metische Mittel. Die in der Gleichung enthaltene Variable ν beschreibt dabei die Viskosität, alsodie Zähigkeit, der Flüssigkeit. Mathematisch wird dieses Verhalten über den Laplace-Operator

ausgedrückt [NC08][S. 643]. Dieser beschreibt, inwieweit die Werte innerhalb einer Zelle desGeschwindigkeitsfelds vom Durchschnittswert der Nachbarzellen abweichen [CCE01][S. 2]:

∇2 = ∇ · ∇ =∂2

∂x2+

∂2

∂y2+

∂2

∂z2(4.4)

Der letzte Teil der Impulsgleichung, ~f , beschreibt den Ein�uss externer Kräfte. Hier könnensowohl lokale Kräfte, also nur einen Teil des Simulationsraums beein�ussende, oder Körperkräfte,also Kräfte, die den Gesamtraum beein�ussen, angelegt werden [FC04][S. 642].

Die zweite Gleichung beschreibt die Randbedingung der Nicht-Kompressibilität (Gleichung 4.2).Diese bringt zum Ausdruck, dass das Volumen der simulierten Flüssigkeit in Raum und Zeitkonstant bleibt, d.h. auch unter Einwirkung von Druck nicht verändert werden kann [FC04][S.641]. Es ist dabei sicher von dieser Randbedingung auszugehen, wenn die sog. Mach-Zahl klei-ner als 0, 3 ist. Die Mach-Zahl beschreibt dabei das Verhältnis der Geschwindigkeit zur Schall-Geschwindigkeit [KKS99][S. 4]. Generell ist die Möglichkeit der Anwendung der Randbedin-gung wünschenswert, da in diesem Fall die Gleichungen wesentlich vereinfacht werden können[BMF07][S. 7].

Um die Randbedingung zu erfüllen, muss die Gesamtströmung innerhalb der Flüssigkeit 0 sein,weswegen der resultierende Vektorraum auch als divergenzfrei bezeichnet wird [CCE01][S. 2]. DieDivergenzfreiheit wird dadurch erreicht, dass der Druck im Feld immer so berechnet wird, dassder Netto�uss 0 ergibt [BMF07][S. 8].

4.2 Diskretisierung des Simulationsraums

Vor einer eigentlichen Berechnung der Flüssigkeiten muss festgelegt werden, wie die Domäne derSimulation in Berechnungschritte diskretisiert werden kann. Grundsätzlich gibt es hierzu zweiMöglichkeiten [NC08][S. 636]:

Page 27: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

4.3. Numerische Berechnung der Einzelterme der Navier-Stokes-Gleichungen 20

Euler und Lagrange Wird die Diskretisierung nach Euler gewählt, so werden feste Netz-punkte in jedem Schritt berechnet. Im Wesentlichen wird also das kubische Volumen des Berech-nungsraums in Zellen aufgeteilt, an deren Positionen regelmäÿig das Geschwindigkeits- undDruckfeld aktualisiert werden. In jeder Zelle wird der aktuelle Wert der Felder gespeichert.

Wird hingegen die Diskretisierung nach Lagrange gewählt, so werden im Gegensatz zur Berech-nung fester Punkte Partikel durch den Raum bewegt. Die Implementierung fällt auf Grund derirregulären Struktur allerdings wesentlich schwerer.

Der aktuell bestehende Simulator berechnet die Einzelwerte der Impuls-Gleichung über einensog. Semi-Langrange-Ansatz [BMF07][S. 20]. Im Wesentlichen werden hier alle Elemente der Im-pulsgleichung mit Ausnahme der Advektion, nach Euler, also an festen Netzpunkten, berechnet.Die Advektion dagegen wird nach Lagrange berechnet, da hier einfachere Algorithmen verfügbarsind. Die Abbildung zwischen den beiden Ansätzen geschieht durch Interpolation der Werte aufdie Gitterzellen.

Netz-Struktur Ein weiterer wichtiger Punkt, der vor der Implementierung zu klären ist,ist welche Netzstruktur gewählt werden soll. Hier �nden sich in der Literatur zwei Ansätze:Collocated-Grids und Staggered-Grids.

Bei Collocated-Grids werden alle Simulationswerte, also Druck und Geschwindigkeit, im Zentrumeiner jeden Gitterzelle gespeichert. Das Verfahren zeichnet sich im Gegensatz zu Staggered-Grids v.a. über seine Einfachheit aus, leidet gleichzeitig aber unter numerischer Ungenauigkeit[Rør04][S. 37f].

Staggered-Grids speichern im Gegenzug dazu die einzelnen Werte an unterschiedlichen Stellen inder Zelle. So wird der Druck weiterhin im Zentrum gespeichert, die einzelnen Komponenten desGeschwindigkeitsvektors dagegen aber an den Grenzen zur jeweiligen Nachbarzelle. Auf dieseWeise kann der Satz der zentralen Di�erenzen an den Grenzen der Zellen ansetzen und mussnicht, wie bei Collocated-Grids, Werte an diskreten Gitterzellen verwenden. Nähere Details zuStaggered-Grids und dessen Vorteile �nden sich in [BMF07][S. 17].

Im vorliegenden Simulator wird ein Marker-and-Cell-Grid (MAC-Grid) eingesetzt, das einemStaggered-Grid entspricht [BMF07][S. 17]. Die Datenspeicherung erfolgt dabei nicht an den Zell-grenzen sondern in den Arrays, die auch die Zellen selbst repräsentieren. Erst bei der Berechnungder Einzelformeln werden die Zellgrenzen mit einbezogen.

4.3 Numerische Berechnung der Einzelterme der Navier-Stokes-

Gleichungen

Für den Ablauf eines Simulationsschrittes müssen die einzelnen Teile der Navier-Stokes-Gleichungenberechnet werden. Die Beschreibung der Lösungsverfahren beschränkt sich hier hauptsächlichauf die mathematischen Verfahren. Die Implementierungen können direkt aus den Lösungswegenabgeleitet werden und entsprechen auch den im existierenden Simulator verwendeten.

Page 28: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

4.3. Numerische Berechnung der Einzelterme der Navier-Stokes-Gleichungen 21

Berechnung der Di�usion

Unter Einbeziehung der Zeitscheibe ∆t berechnet sich die Di�usion unter Auswertung der fol-genden Formel [CCE01][S. 2]:

diffusion = ∆t · ν · ∇2 · ~u (4.5)

Die Schwierigkeit bei der Lösung dieser Gleichung liegt in der Lösung von ∇2. Dies kann unterVerwendung der beiden folgenden Gleichungen berechnet werden:

∇2~u =(∇2ux(x, y, z),∇2uy(x, y, z),∇2uz(x, y, z)

)(4.6)

∇2g(x, y, z) = g(x+ 1, y, z) + g(x− 1, y, z) + g(x, y + 1, z) + g(x, y − 1, z)+

g(x, y, z + 1) + g(x, y, z − 1)− 6g(x, y, z) (4.7)

Anschlieÿend ergibt sich die Di�usion durch schlichtes Einsetzen in Gleichung 4.5.

Berechnung der Advektion

Die Advektion wird mit der Methode der Charakteristiken nach Stam durchgeführt. Die Grund-idee der Methode ist wie folgt: In jedem Zeitschritt bewegen sich Partikel durch das Feld. Um dieGeschwindigkeit zur Zeit t+ ∆t zu berechnen wird das Partikel auf die Zeit t unter Anwendungdes letzten Geschwindigkeitsvektors zurückverfolgt und der aktuelle Wert auf diesen letzten Wertgesetzt. Dies führt zu einer numerisch sehr stabilen Berechnung der Advektion [Sta99][S. 3].

~u2(x) = ~u1(p(x,−∆t)) (4.8)

Im vorliegenden Simulator wird die Rückverfolgung für jeden Punkt x durchgeführt und dasErgebnis anschlieÿend auf einen Gitterpunkt interpoliert. Weitere mögliche Ansätze zur Lösungder partiellen Di�erentialgleichung des Advektions-Terms bestehen durch die Algorithmen nachMacCormack und Runga-Kutta [GH99].

Berechnung der Projektion

Di�usion und Advektion können unter Anwendung der obigen Verfahren recht einfach gelöstwerden. Die letzte Gleichung muss dagegen zwei Formeln erfüllen - den Druck-Anteil aus derImpulsgleichung und die Bedingung der Nicht-Kompressibilität. Beide Gleichungen werden zuerstmiteinander verbunden [FC04][S. 644f]:

Mittels der Helmholtz-Hodge-Dekomposition lässt sich ein Vektorfeld als

~w = ~u+∇~p (4.9)

schreiben. Dabei wird angenommen, dass ~u ein divergenzfreies Feld ist, das parallel zur Flüs-sigkeitsebene steht. Ein divergenzfreies Feld ergibt sich demnach zu

Page 29: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

4.4. Gröÿe der Simulationsschritte 22

~u = ~w −∇~p (4.10)

Wird der Nabla-Operator auf beide Seiten angewendet, so ergibt sich unter Verwendung von∇~u = 0 die Poisson-Druckgleichung zu

∇2~p = ∇w (4.11)

Hiermit lässt sich ein Projektionsoperator P de�nieren, der ein Vektorfeld ~w auf ein divergenz-freies Feld ~u abbildet:

P~w = P~u+ P(∇p) (4.12)

Wendet man den Projektionsoperator auf die Impulsgleichung an, so entfällt die Druckgleichungwegen P(∇p) = 0 und es ergibt sich die �nale Form der für den Simulator relevanten vereinigtenImpulsgleichung:

∂~u

∂t= P(−(~u · ∇) · ~u+ ν · ∇ · ∇ · ~u+ ~f) (4.13)

Letztlich ist somit der Ablauf der Simulation klar de�niert: Zuerst werden Di�usion, Advektionund externe Kräfte berechnet und addiert. Der benötigte Druckvektor ~p wird durch Lösungvon Gleichung 4.11 unter Verwendung des gerade berechneten nicht-divergenzfreien Feldes ~w

ermittelt. Schlieÿlich erhält man das divergenzfreie Druckfeld ~u durch Auswertung von Gleichung4.10.

Die sich im Zwischenschritt ergebende Poisson-Gleichung kann durch Jakobi-Iterationen gelöstwerden. Die zugehörige Gleichung für die (l + 1)-te Iteration ergibt sich zu [FC04][S. 649]:

~p(l+1)i,j,k =

~p(l)i−1,j,k + ~p

(l)i+1,j,k + ~p

(l)i,j+1,k + ~p

(l)i,j−1,k + ~p

(l)i,j,k+1 + ~p

(l)i,j,k−1 − (∂~p

(l)i,j,k)2 · ∇ · ~w

6(4.14)

Die Anzahl an Jakobi-Iterationen dürfen hier nicht zu klein sein, damit das Verfahren annäherndkonvergieren kann. Im vorliegenden Simulator werden 5 Iterationen eingesetzt.

Statt der Jakobi-Iterationen kann auch das iterative Verfahren der konjugierten Gradienten zurLösung der Matrix verwendet werden [She94][S. 30�]. Das Verfahren löst die Matrix e�zienterund ist im vorhandenen Simulator ebenfalls implementiert.

4.4 Gröÿe der Simulationsschritte

Für jeden Zeitschritt ist im Simulator eine konstante Zeitscheibe fest gesetzt. Dieser Wert istüber die folgende Formel approximiert:

∆t ≤ ∆x

|~umax|(4.15)

Page 30: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

4.5. Datenstrukturen im vorhandenen Simulator 23

Dabei wird zugrunde gelegt, dass die Zeitscheibe maximal so groÿ sein kann, dass der längsteGeschwindigkeitsvektor nicht gröÿer als die Breite einer Zelle ist [CCE01][S. 3]. Ist dieser bekanntund gleichzeitig die Gitterbreite statisch, so kann der Wert als konstant angenommen und stattder Formel eingesetzt werden.

4.5 Datenstrukturen im vorhandenen Simulator

Verschiedene Datenstrukturen sind nötig um die Simulationsschritte zu berechnen. Im Wesentli-chen beschränkt sich der existierende Simulator auf die Verwendung von eindimensionalen Arrays.Beispielsweise werden für das Halten des Geschwindigkeitsfeldes drei Arrays der entsprechen-den Feldgröÿe alloziert. Jedes der Arrays enthält eine Komponente des Geschwindigkeitsvektors(x, y, z) an einem Punkt des Feldes. Der gleiche E�ekt könnte auch durch Verwendung einesPunkt-structs erzielt werden. Die Verwendung von structs ist allerdings in Bezug auf die Aus-führungsgeschwindigkeit und auf die Cache-Hierarchien sehr umstritten [KWH10][S. 161].

Weitere Arrays werden für die Geschwindigkeiten an Zu�uss-Stellen im Netz, die Berechnung desGradienten und die Druck-Felder sowie für eventuell bestehende Hindernisse alloziert. Da vieleder Berechnungen nicht in-place durchgeführt werden können, werden für das Geschwindigkeits-und das Druckfeld jeweils weitere Arrays alloziert, die bei jedem Simulationsschritt getauschtwerden. Somit können alle Punkte im Geschwindigkeitsfeld getrennt voneinander bzw. nebenläu-�g zueinander berechnet werden.

4.6 Ablauf eines Simulationsschrittes

Die eigentliche Implementierung folgt dem obigen Lösungsverfahren. Der Ablauf im Einzelnenist in Abbildung 4.1 dargestellt.

Zuerst werden eventuell vorhandene Ein�üsse behandelt. Von diesen zeigt jeweils ein Geschwin-digkeitsvektor in den Simulationsraum. Damit diese Vektoren konstant bleiben und nicht durchdie Berechnung des Drucks o.ä. verändert werden, müssen sie in jedem Schritt auf den konstantenWert des Ein�usses gesetzt werden.

Anschlieÿend werden Di�usion, Advektion und die Projektion entsprechend der bereits beschriebe-nen Algorithmen berechnet. Zusätzlich werden dabei jeweils vorhandene Randbedingungen be-achtet. Diese beziehen sich z.B. darauf, dass kein Partikel in einen Festkörper verschoben werdendarf. Entsprechend darf auch kein Partikel den Simulationsraum verlassen.

Die Berechnungen der Navier-Stokes Gleichungen werden für jeden Simulationsschritt wieder-holt. Zusätzlich werden nach jedem Schritt u.U. verschiedene Darstellungen, z.B. die Partikel-Positionen oder deren Geschwindigkeitsvektoren, berechnet und angezeigt.

Page 31: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

4.6. Ablauf eines Simulationsschrittes 24

Aktualisierung der Geschwindigkeitsvektorendurch Einflüsse

Diffusion

Advektion des Geschwindigkeitsfelds

Projektion

Advektion der Partikel

simulierend

Abbildung 4.1: Ablauf eines Simulationsschrittes

Page 32: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

Kapitel 5

CUDA-Abstraktion in Java

Nach der theoretischen und praktischen Einführung in die Navier-Stokes-Flüssigkeitsgleichungenstellt sich die Frage, inwiefern sich die ersten beiden Kapitel, also die CUDA-Programmierung unddie Navier-Stokes-Gleichungen, sich mit Java, bzw. einer dazu Bytecode-kompatiblen Sprache,verbinden lassen. Dazu beschäftigt sich dieses Kapitel insbesondere mit den verfügbaren CUDA-Abstraktionsbibliotheken und deren Vor- und Nachteilen in Bezug auf die nachfolgend gestelltenAnforderungen.

5.1 Anforderungen an eine Java API

Aus den Navier-Stokes-Gleichungen lassen sich verschiedene Anforderungen ableiten, wovoneinige schon ein Vorgri� auf die im weiteren Verlauf beschriebenen Fähigkeiten der einzelnenBibliotheken sind. Eine Vergleichsmatrix über alle evaluierten Merkmale der Bibliotheken �ndetsich in Anhang B.

Plattform-Unterstützung Da die Entwicklung am Fraunhofer IWU auf Windows-Computernerfolgt, ist eine Unterstützung des entsprechenden Betriebssystems erforderlich. Eine Unter-stützung von Unix-basierenden Betriebssystemen ist dagegen unter Umständen in Zukunft sinn-voll, im Moment jedoch unnötig. Allerdings ist bereits hier darauf hinzuweisen, dass die Ent-wicklung der Bibliotheken stark auf die Unix-Umgebung fokussiert ist und Windows meist nurrudimentär unterstützt wird.

Code-Umwandlung In CUDA werden Kernels in CUDA-C geschrieben. Eine Java-Abstraktionsollte es hingegen ermöglichen diese in Java zu de�nieren. Durch eine für den Entwickler trans-parente Umwandlung in nativen Quelltext sollte dieser auch auf der Gra�kkarte ausführbar sein.Hierbei stellt sich die Frage, welche Java-Programmierkonstrukte, wie Klassen, Collections-API,Arrays o.ä., unterstützt werden. Gleichzeitig ist fraglich, ob eine objektorientierte Program-mierung von Kernel-Funktionen bzw. Konstrukte wie Verweise auf statische Funktionen o.ä.,möglich sind.

Dabei ist die Einbindung von nativem Code wünschenswert. Hiermit ist eine prototypische Ent-wicklung auf Java möglich, wobei der resultierende Code später auf die native Sprache abgebildetwerden kann. Der Vorteil dabei ist, dass relativ schnell in Java ein GPU-Kernel geschrieben wer-den kann. Dabei muss keine Rücksicht auf C- oder GPU-Spezi�ka genommen werden. Allerdingsist der generierte CUDA-Kernel mit ziemlicher Sicherheit auf Grund der automatischen Generie-rung nicht so performant, wie es ein nativ geschriebener Kernel sein könnte, da dieser sich direkt

Page 33: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

5.1. Anforderungen an eine Java API 26

an den Spezi�ka der Gra�kkarte orientieren kann und damit auch passende Optimierungen, wieLoop-Unrolling o.ä., anwenden kann. Deswegen kann in einem zweiten Schritt der prototypischeJava-Quelltext in nativen und optimierten performanten CUDA-C- oder OpenCL-Code übersetztund in die Ausführungsumgebung eingebunden werden.

Speicherverwaltung In CUDA muss jeder Speicherbereich händisch verwaltet, d.h. alloziertund wieder freigegeben, werden. Eine Java-Abstraktion sollte dies für den Entwickler übernehmen,um damit eigentlich über�üssigen, bzw. generierbaren, Code zu entfernen. Trotz der automatis-chen Speicherverwaltung sollten spezielle Speicherbereiche wie Constant- oder Shared-Memoryweiterhin nutzbar bleiben, sodass die GPU-Anwendung weiterhin durch Verwendung dieser schnellenSpeicherbausteine performant ausgeführt werden kann.

Einige der nachfolgend beschriebenen Bibliotheken

Alloziere CPU-Speicher

Alloziere GPU-Speicher

Kopiervorgang CPU => GPU

Kernel-Ausführung

Kopiervorgang GPU => CPU

Freigabe GPU-Speicher

weitere Aufrufe

Freigabe CPU-Speicher

Abbildung 5.1: Typische Abarbeitungmehrerer Kernel Aufrufe auf den gleichenDaten in Java

verfolgen den Ansatz, vor jeder Kernel-Ausführungden nötigen Speicherbereich auf der GPU zu al-lozieren, den nötigen Speicher aus dem CPU-Speicherin den GPU-Speicher zu kopieren, die Ausführungzu starten, nötigen Speicher zurück zu kopieren undden GPU-Speicher schlieÿlich wieder freizugeben. Die-ser Vorgang ist in Abbildung 5.1 illustriert. Proble-matisch wird dies sobald Kernels mehrfach ausge-führt werden, da vor und nach jedem Kernel-Startdie Daten kopiert werden müssen. CUDA erlaubt esdurch die explizite Spezi�kation von Kopiervorgän-gen diesen Flaschenhals zu vermeiden. Eine entsp-rechende Unterstützung ist für die Implementierungder Navier-Stokes-Gleichungen wichtig, da hier sehroft Kernels mehrfach ausgeführt werden müssen. EinBeispiel hierfür ist das Jakobi-Verfahren, das zur Lö-sung der Projektion eingesetzt wird.

Eine weitergehende Anforderung ist die Ausführungvon mehreren verschiedenen Kernels, wobei nur beimersten Kernel nötige Daten kopiert werden und an-schlieÿend, bei weiteren Kernels, die gleichen Da-ten wiederverwendet werden können. Dieses Musterlässt sich beim bestehenden Flüssigkeitssimulatorrecht häu�g �nden, da z.B. Advektion und Di�u-sion jeweils in eigenen Kernels ausgeführt werden,trotzdem aber in der Funktion jeweils dieselben Da-ten manipulieren.

Entwicklungsgeschwindigkeit und Lernkurve Zu dieser Kategorie gehören sehr unter-schiedliche Merkmale, insbesondere jedoch die Möglichkeit der Einbettung in ein bestehendesProjekt, die Entwicklungsgeschwindigkeit, die Testbarkeit des erstellten Codes, die zukünftige

Page 34: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

5.1. Anforderungen an eine Java API 27

Weiterentwicklung und die Möglichkeit Fehler, sowohl im selbst erstellten Quelltext als auch inder Bibliothek, zu �nden.

Zwar wird eine GPU-Abstraktionsumgebung nur ein einziges Mal in ein Projekt eingebunden,so ist doch die Komplexität der Einbindung ein wichtiges Kriterium. Müssen bei jeder Ak-tualisierung alle Bibliotheken händisch kompiliert, kopiert und eingebunden werden? Was mussdafür geändert werden? Wie kompliziert ist das entsprechende Build-Skript? Kann überhaupt aufQuelltext auÿerhalb der Bibliothek zugegri�en werden, oder muss sämtlicher Quelltext basierendauf einer Domain Speci�c Language (DSL) der Bibliothek spezi�ziert werden? Gleichzeitig stelltsich auch die Frage, wie schnell, und wann, die Umwandlung in GPU-Code erfolgt und ob dafürextra Kommandos im Build-Skript ausgeführt werden müssen.

Ist die Bibliothek eingebunden, so sollte der Entwickler schnell in der Lage sein, diese richtigeinzusetzen und Algorithmen zu implementieren. Dazu ist es wichtig, dass ein eventueller Staging-Zwischenschritt zur Code-Umwandlung die Entwicklungsgeschwindigkeit nur wenig beein�usstund gleichzeitig generierter Code ohne weitere Fehler lau�ähig ist.

Eventuell auftretende Fehler sollten bei der Ausführung des evtl. generierten Quelltextes nichtverschluckt bzw. ignoriert werden, sondern in geeigneter Form dem Entwickler präsentiert wer-den. Dies bezieht sich v.a. auf häu�g in der Entwicklung auftretende ArrayIndexOutOfBounds-Exceptions. Zur weiteren Fehlersuche ist die Möglichkeit des Debuggings ein wichtiges Instru-ment, das auch bei der GPU-Programmierung nicht fehlen darf. Dies bezieht sich v.a. darauf,Kernels, u.U. auch in Java, zur Fehlersuche in Einzelschritt-Auswertung betrachten zu kön-nen. Dabei sollten eventuell auftretende Race-Bedingungen zwischen nebenläu�g ausgeführtenThreads erkennbar sein.

Ist auch durch Debugging der Fehler nicht ersichtlich, so muss ein Blick in den Quelltext der un-terliegenden Bibliothek möglich sein. Diese sollte dazu eine nicht zu hohe Komplexität aufweisen,damit die Struktur und der Ablauf der Code-Umwandlung vom Entwickler in möglichst kurzerZeit verstanden werden kann. Komplexität entsteht hier auf der einen Seite durch verworreneArchitekturen, falsche Methodennamen, fehlende Dokumentation o.ä. und auf der anderen Seitedurch komplexe Ausführungsmodelle, eventuell also z.B. durch einen zusätzlich nötigen Staging-Schritt zur Code-Umwandlung. Abgesehen davon sollte die Möglichkeit zur Ausgabe des gene-rierten CUDA-C- oder OpenCL-Code in lesbarer Form bestehen. Damit lassen sich Fehler, dieim eigenen Algorithmus liegen, von Fehlern innerhalb der Code-Generierung unterscheiden.

Weiterhin sollte auch in Zukunft eine ausgewählte Bibliothek weiterentwickelt und unterstütztwerden, sodass Fehler schnell behoben werden und auch auf Fragen zügig geantwortet wird. Hierist insbesondere eine aktive Gemeinde zur Beantwortung auftretender Fragen und Probleme rundum die Bibliothek hervorzuheben. Auÿerdem ist wichtig, dass eine entsprechende Bibliothek nichtauf Einzelentwickler angewiesen ist, die das Projekt jederzeit aufgeben und damit die Einstellungder Entwicklung bewirken können.

Ausführungsmodi In CUDA-C ist die Ausführung hauptsächlich auf die Gra�kkarte beg-renzt. Zusätzliche Ausführungsmodi, wie z.B. die Ausführung mit OpenMP, sind nur mit Work-arounds möglich. Dies ist v.a. darauf zurückzuführen, dass für die Host-Ausführung zusätzlicheSchleifen notwendig sind, die im CUDA-Ausführungsmodell implizit vorhanden sind. Gleichzeitigmüssen zur Kompatibilität zu CUDA auch globale Variablen (z.B. threadIdx.x und blockIdx.x)

Page 35: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

5.1. Anforderungen an eine Java API 28

für die aktuelle Thread-Nummer bzw. den aktuellen Block emuliert werden.

Ein entsprechendes Abstraktionsframework sollte zumindest die Java-Thread-Pool-Ausführung,z.B. durch Implementierung des Master-Worker-Patterns, gleichwertig zur CUDA-C bzw. OpenCLAusführung unterstützen. Dies ist insbesondere ein Vorteil für Geschwindigkeitsvergleiche zwi-schen GPU und CPU und dient gleichzeitig der Entwicklungsgeschwindigkeit und Testbarkeitdes Codes, da nicht zur Ausführung jeder Code-Änderung der Entwickler-Quelltext in CUDA-Cumgewandelt werden muss, sondern direkt in Java ausgeführt werden kann. Abgesehen davon be-sitzt nicht jeder Entwicklungsserver eine leistungsfähige Gra�kkarte. Soll, z.B. durch Continuous-Integration, auf einem Server die Applikation oder entsprechende Tests, regelmäÿig ausgeführtwerden, so kann dies nebenläu�g auf den verfügbaren CPU-Kernen erfolgen.

Unterstützte Sprach-Features Bestimmte Features sind für die Implementierung von Algo-rithmen wichtig und müssen von den Bibliotheken zur Verfügung gestellt werden. Insbesonderesind hier eindimensionale Arrays sowie entsprechende Operationen darauf, OpenGL-Unterstüt-zung und eine API zur Zeitmessung erforderlich.

Da die Navier-Stokes-Gleichungen stark auf der Verwendung von primitiven Arrays basieren, diein verschiedensten Kernels abgearbeitet werden, ist deren Unterstützung, v.a. für den Daten-typ double, notwendig. Entsprechende Operationen darauf, d.h. Operationen zum Belegen undAuslesen von Array-Feldern, sind entsprechend zur Verfügung zu stellen. Eine Möglichkeit zumAuslesen der Länge des Arrays, die es in C nicht gibt, ist der Code-Leserlichkeit förderlich, abernicht zwingend notwendig.

Die Lesbarkeit eines Programms lässt sich zusätzlich stark über die Unterstützung und Ver-wendung von Objekten erhöhen, da damit die Implementierung eines Domänenmodells möglichwird. Insbesondere für Arrays sind deswegen Objekte ein wichtiger Evaluierungsparameter, auchin Bezug auf die Serialisierungsgeschwindigkeit und die Umwandlung in äquivalente C-Structs.Als Alternative zu Objekten wären u.U. auch mehrdimensionale Arrays ausreichend. Hier stelltsich ebenfalls die Frage nach der Unterstützung durch die Bibliotheken.

Damit verschiedene Lösungsansätze schnell miteinander verglichen werden können ist eine Schnitt-stelle zur Zeitmessung erforderlich. Diese sollte v.a. die GPU-Zeit mit einbeziehen. CUDA bietetdafür z.B. eine eigene Schnittstelle durch Events an, die den Vorteil haben, dass kein durch dasBetriebssystem entstehender Overhead, z.B. durch den Thread-Scheduler, mit einbezogen wird[SK11][S. 108].

Damit die berechnete Simulation ausgegeben werden kann verwendet der bestehende SimulatorOpenGL zur Visualisierung. Ein ähnliches Interface sollte auch die Java-Abstraktion bieten.Möglichst sollte dabei kein Overhead durch zusätzliche Kopiervorgänge (GPU → CPU → GPU)entstehen.

Ausführungsgeschwindigkeit Mit dem Flüssigkeitssimulator ist eine Echtzeit-Simulationdas Ziel des Gesamtprojektes. Eine entsprechende Java-Abstraktionsumgebung sollte daher mög-lichst wenig zusätzlichen Overhead mit einbringen, damit die eigentliche Ausführungszeit durchdie zusätzliche Bibliothek nicht wesentlich erhöht wird. Eventuell existierende Code-Optimierun-gen zur Steigerung der Geschwindigkeit sind ebenfalls wünschenswert.

Page 36: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

5.2. Algorithmen zur Evaluation 29

5.2 Algorithmen zur Evaluation

Zur Evaluation wurden unter Nutzung der Abstraktionsbibliotheken sechs verschiedene Algo-rithmen beschrieben und implementiert. Dabei wurden, soweit möglich, für alle Bibliotheken diegleichen Implementierungen der Algorithmen zugrunde gelegt.

Array-Inkrementierung Als sehr einfaches Beispiel für eine GPU-Parallelisierung dient dieInkrementierung eines Arrays um jeweils +1. Damit die Funktionalität korrekt implementiertwerden kann muss die Bibliothek die Verwendung primitiver Arrays erlauben. Die Idee des Al-gorithmus ist, dass jeder Thread ein Element aus dem Array in ein Register lädt, dieses inkre-mentiert und das Ergebnis zurück in den Array schreibt. Durch sehr groÿe zu inkrementierendeArrays werden damit sehr viele Threads gestartet. Die Bibliotheken müssen daher in der Lagesein die Threads in Blöcke aufzuteilen und nacheinander abzuarbeiten.

Matrix-Multiplikation Zwei quadratische Matrizen identischer Gröÿe werden für diese Eva-luationsaufgabe miteinander multipliziert. Dabei werden zuerst keinerlei Optimierungen wieShared- bzw. Local-Memory angewendet. Der implementierte Algorithmus ist wesentlich kom-plexer als der der Array-Inkrementierung, da im Kernel eine zusätzliche Schleife ausgeführt wer-den muss, die die Anzahl an Berechnungen deutlich erhöht. Die zugrunde liegende Idee ist, dassjeder Thread eine Zelle der resultierenden Matrix berechnet. Jeder Thread muss also insgesamtgenau so viele Multiplikationen und Additionen, wie die Matrix breit und hoch ist, ausführen.

Da die Matrix-Multiplikation als Paradefall für Shared-Memory gilt, wird unter Nutzung vonBlock-Tiling der Algorithmus weiter optimiert. Dabei macht man sich zunutze, dass viele Threadsin einem Block die gleichen Daten aus den Ausgangsmatrizen benötigen. Alle Threads einesBlocks kollaborieren nachfolgend jeweils darin, einen Teil der Ausgangsmatrizen in den schnellenSpeicher zu laden und nutzen diesen daraufhin zur eigentlichen Berechnung. Anschlieÿend wird,so lange weitere Blöcke vorhanden sind, der nächste Block verarbeitet [KWH10][S. 84�]. Dadie Gröÿe von Shared-Memory auf Gra�kkarten stark beschränkt ist, muss die Gröÿe des Sub-Blockes so gewählt werden, dass die Speichergröÿe nicht überschritten wird. Auf einer GeForceGTX 660TI Gra�kkarte stehen beispielsweise 48 kB pro Block zur Verfügung, womit 6144 Double-Zahlen speicherbar sind. Daraus ergibt sich ein Maximum von 78 für die Breite und Höhe desSub-Blocks.

Mandelbrot Als weiteres Beispiel zur GPU-Evaluation dient die Berechnung eines Mandelbrot-Fraktals. Alle Pixel des entstehenden Bildes sind dabei unabhängig voneinander berechenbar,sodass die Aufgabe ohne Verluste auf der GPU zu parallelisieren ist. Jeder Thread ist im entste-henden Algorithmus für die Berechnung eines Pixels zuständig.

Mandelbrot ist ein klassisches Beispiel für den Test der GPU-Parallelisierung, das sich bei nahezujeder Bibliothek als Testfall wieder�nden lässt. Eine wesentliche Neuerung gegenüber der Matrix-Multiplikation ist hingegen nicht vorhanden.

Page 37: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

5.2. Algorithmen zur Evaluation 30

Parallele Summation von Array-Elementen Noch komplexer ist der Algorithmus zurBerechnung der Reduktion eines Arrays unter Nutzung der Addition. Ziel ist die Summation allerElemente eines Arrays (z.B. reduce([0, 1, 2, 3, 4, 5, 6]) = 21). Ein zugehöriger serieller Algorithmusist sehr einfach zu implementieren. Dagegen ist die Nutzung der parallelen Ressourcen der GPUanspruchsvoll, da im naiven Ansatz jedes Zwischenergebnis von allen vorherigen Berechnungenabhängt. Eine Möglichkeit diesen Flaschenhals zu umgehen ist der Algorithmus nach Blelloch,der von NVIDIA in einer Ausarbeitung zusammengefasst wurde [Par07].

Der Ablauf des Algorithmus ist in Abbildung 5.2 für einen Beispielblock mit 8 Elementendargestellt. Zuerst wird der Block in zwei Hälften aufgeteilt. Für jedes Element in der erstenHälfte addiert ein Thread sein Element auf ein entsprechendes Element in der zweiten Hälfte.Anschlieÿend wird der Block halbiert und der Vorgang entsprechend dem ersten Schritt fortge-setzt. Der Algorithmus terminiert, wenn die Blockgröÿe 1 ist, da dann das erste Element desBlocks die Summe enthält. Damit keine Inkonsistenzen auftreten müssen die Threads zwischenden Halbierungen der Block-Gröÿen aufeinander warten. Die Summen der Einzelblöcke werdenauf die CPU zurück kopiert und dort �nal aufaddiert.

Wird die Block-Gröÿe entsprechend klein gewählt, so kann die Abarbeitung durch Shared-Memory stark beschleunigt werden. Wie bei der Matrix-Multiplikation werden dazu in einemersten Schritt die Daten in das Shared-Memory geladen und anschlieÿend die dort abgelegtenWerte zur Berechnung genutzt.

Insbesondere die notwendige Synchronisation zwischen zwei Abarbeitungsschritten zeichnet die-sen Testfall aus. Diese ist notwendig, da ein Read-After-Write (RAW)-Kon�ikt zwischen derBeendigung der Addition eines Threads und dem Lesen des Ergebnisses zur nächsten Additioneines anderen Threads besteht. Ist die Synchronisation inkorrekt implementiert, so ergeben sichwährend der Abarbeitung durch auftauchende Race-Conditions falsche Ergebnisse.

Jakobi-Verfahren zur Lösung von Poisson-Gleichungen Das Jakobi-Verfahren zur Lö-sung von linearen Gleichungssystemen kann analog zur allgemeinen Formel des Verfahren imple-mentiert werden [MV11][S. 76]:

1 2 3 4 5 6 70

6 8 10 4 5 6 74

+

16 8 10 4 5 6 712

16 8 10 4 5 6 726

+ + +

+ +

+

Abbildung 5.2: Parallele Reduktion (Beispiel für einen Block), vgl. [Par07][S. 8]

Page 38: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

5.2. Algorithmen zur Evaluation 31

x(m+1)i = 1

aii·

(bi −

∑j 6=i

aij · x(m)j

)Jeder Thread berechnet eine Zelle des Ergebnisvektors. Da die Ausführungsreihenfolge der Threadsunspezi�ziert ist, muss zur Sicherstellung der korrekten Abarbeitung der Gleichung eine Synchro-nisation zwischen dem Lesen von x(m)

j und dem Schreiben von x(m+1)i eingefügt werden. Problem

dabei ist, dass CUDA nur die Synchronisation von Blöcken unterstützt. Die beiden Werte xi undxj können dagegen auch in zwei verschiedenen Blöcken zu �nden sein.

In diesem Anwendungsfall spielt jedoch das Weglassen der Synchronisation keine Rolle. Tritt derFall ein, dass ein neuer Wert für x(m+1)

i geschrieben wird, bevor der in der gleichen Speicherzelle

zu �ndende Wert x(m)i innerhalb der Schleife aus der Zelle gelesen wurde, so wird der bereits

aktualisierte Wert verwendet. Dies resultiert in einer schnelleren Konvergenz des Verfahrens, dax(m+1)i bereits näher am Ergebniswert liegt. Insgesamt entsteht dadurch eine Mischung aus dem

Jakobi- und dem Gauÿ-Seidel-Verfahren.

Schlieÿlich wird die Abhängigkeit zwischen den Iterationen des Verfahrens durch vielfache Kernel-Aufrufe gelöst. Im Test wurden jeweils 200 Aufrufe ohne Prüfung auf ein Konvergenz-Kriteriumdurchgeführt.

Um diesen Testfall korrekt implementieren zu können, muss die mehrfache Ausführung von Ker-nels möglich sein. Im besten Fall kann für die Bibliothek spezi�ziert werden, dass zwischen denEinzeliterationen keine Daten zwischen GPU und CPU kopiert werden, was sich wiederum ineiner geringeren Ausführungszeit niederschlagen sollte.

Verfahren der konjugierten Gradienten zur Lösung von Poisson-Gleichungen Derletzte und gleichzeitig anspruchsvollstes Testfall ist das Verfahren der konjugierten Gradienten.Das zugehörige Verfahren zur Lösung des Gleichungssystems A · x = b wurde [Mic11][S. 3]entnommen. Voraussetzung für die Konvergenz des Lösungsverfahren ist eine positiv de�niteund symmetrische Matrix A. Das Verfahren wurde pro Iteration mit sieben Kernel-Aufrufenimplementiert. Enthalten sind darin Skalar- und Matrixprodukte sowie Vektoradditionen und-skalierungen.

Im Gegensatz zum Jakobi-Verfahren muss zur e�zienten Implementierung die Bibliothek diemehrfache Ausführung verschiedener Kernels unterstützen. Auch hier ist wiederum die Geschwin-digkeit stark davon davon abhängig, ob die Bibliothek die Speicherbereiche der Kernels trennt,womit ein zusätzlicher Kopiervorgang nötig ist, oder ob der gleiche Speicherbereich auf der GPUohne weitere Datentransfers nutzbar ist.

Page 39: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

5.3. Evaluation verfügbarer Abstraktionsbibliotheken 32

5.3 Evaluation verfügbarer Abstraktionsbibliotheken

Dieser Abschnitt beschreibt die aktuell verfügbaren Bibliotheken zur Java-Abstraktion. Zuerstwird jeweils die Architektur bzw. der Ablauf der Code-Umwandlung erklärt. Dies ist insbeson-dere wichtig, um ein Gefühl für die Komplexität der Bibliothek und den Ablauf der Code-Transformation, bzw. der Abbildung auf GPU-Code, zu bekommen.

Anschlieÿend werden die vorhandenen Features mit den Anforderungen aus Abschnitt 5.1 ver-glichen. Der Laufzeitvergleich wird, im Gegensatz zu allen anderen Anforderungen, nicht in jedemBibliotheksabschnitt einzeln behandelt, sondern aus Gründen der Übersichtlichkeit am Ende desKapitels in gesammelter Form.

Die nachfolgenden Abschnitte beschreiben dabei nicht die Erfüllung jeder Evaluationsanforderungim Einzelnen, da nicht jede Bibliothek auf Grund ihrer Architektur und ihrer Zielsetzung alleAnforderungen erfüllen kann. Ist dies klar ersichtlich, so werden derartige Anforderungen nichtnäher behandelt.

5.3.1 JCuda

JCuda [jcu13] ist ein unter der MIT-Lizenz stehendes Java-Binding für CUDA, das wichtigeAPI-Funktionen aus dem CUDA-Toolkit von NVIDIA abstrahiert und als Java-Funktionen zurVerfügung stellt. Die Bibliothek wird von einem Einzel-Entwickler, Marco Hutter, entwickelt.Weitere Informationen über die Bibliothek �nden sich leider nicht, da sich auf der zugehörigenWeb-Site kein Impressum oder ähnliche Daten �nden, die weiteren Aufschluss über den Herstellerbzw. die näheren Entwicklungsumstände geben. Verö�entlichungen oder ähnliche Quellen sindebenfalls nicht vorhanden. Die nachfolgende Beschreibung bezieht sich deswegen einzig auf denö�entlichen Quelltext der Bibliothek.

Der selbe Hersteller bietet zusätzlich ein Binding für OpenCL mit dem Namen JOCL an, dashier allerdings auf Grund der Nähe zu JCuda nicht näher betrachtet wird. Auÿerdem stehen ver-schiedene Bibliotheken, die auf JCuda bzw. JOCL basieren, zur Verfügung. Diese enthalten Funk-tionen für typische Basic Linear Algreba Subprograms (BLAS)-Operationen, wie z.B. Matrix-Operationen, die schnelle Fourier-Transformation o.ä.. Die Bibliothek stellt wohl einen Klon dervon NVIDIA in CUDA-C implementierten Bibliothek für BLAS-Operationen (cuBLAS) dar.

Ablauf der Ausführung Im Wesentlichen besteht JCuda aus einer einfachen Abstraktions-schicht, deren Funktionsnamen und Klassen der CUDA-Toolkit-API nachempfunden sind. Da dieTerminologie stark an die Terminologie von CUDA angelehnt ist, �ndet man sich als Entwicklerschnell zurecht.

Diese Abstraktionsschicht reicht die abzuarbeitenden Daten via Java Native Interface (JNI)an C-Funktionen weiter, die anschlieÿend wiederum Toolkit-Funktionen von CUDA aufrufen.Die verwendeten Kernel müssen direkt in CUDA-C geschrieben werden, da eine automatischeTransformation von Java nach CUDA-Code nicht erfolgt. Ausgeführt wird allerdings auch keinCUDA-Quelltext, sondern Quelltext in kompilierter Form, d.h. entweder PTX-Zwischencode odervon der Architektur abhängiger Code in Form einer cubin-Datei. Die nötige Infrastruktur zur

Page 40: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

5.3. Evaluation verfügbarer Abstraktionsbibliotheken 33

Kompilierung ist vom Entwickler zu entwerfen. Die Kompilierung selbst kann damit zu einembeliebigen Zeitpunkt erfolgen, d.h. entweder zur Laufzeit oder vor dem Programmstart.

Ein Beispiel für die stark an CUDA-C angelehnte API ist in Quelltext 5.1 dargestellt. Die In-frastruktur zur Code-Kompilierung ist hier nicht eingezeichnet und kann dem beiliegenden Eval-uationsprojekt entnommen werden. Insgesamt erinnert der entstehende Java-Quelltext stark annativen CUDA-Quelltext. Einzig der Kernel-Aufruf sowie die Instantiierung der Device-Pointerunterscheiden sich. Der Kernel selbst ist nicht eingezeichnet, da dieser exakt dem nativen Kernelin Quelltext 3.1 entspricht.

Evaluation Sämtliche Testfälle können mit der Bibliothek durch die Unterstützung von primi-tiven Arrays, mehrfachen Kernel-Aufrufen sowie Shared-Memory usw. ohne gröÿeren Aufwandimplementiert werden. Einige Fallstricke sind hierbei zu beachten:

Wie bereits erwähnt erfordert JCuda die explizite Speicherverwaltung durch den Entwickler.Dieser muss in Bytes vorgeben, wie groÿ der allozierte Speicherbereich sein soll, den Speicherhändisch auf die GPU kopieren und den Kernel anschlieÿend, wiederum manuell, starten. Einenahtlose Koppelung an die Datentypen von Java erfolgt nicht, womit die zu allozierende Spei-chergröÿe durch eine an die C sizeof-Funktion angelehnte Methode berechnet werden muss.Auftretende Fehler durch Zugri� auf Speicherbereiche, die auÿerhalb der allozierten Gröÿe liegen,werden verschluckt und nicht an Java zurück propagiert. Eine erwartete ArrayIndexOutOf-

BoundsException tritt z.B. nicht auf.

Auch weitere mögliche auftretende Fehler werden angezeigt. Wird beispielsweise versucht zu vielSpeicher auf die Gra�kkarte zu kopieren, so stürzt der Gra�kkartentreiber ab und ein CUDA_-

ERROR_UNKNOWN wird zurückgegeben. Dies entspricht dem Verhalten des CUDA-Toolkits, ist abertrotzdem der Fehlersuche nicht förderlich.Die explizite Speicherverwaltung bringt hingegen durchaus Vorteile mit sich. Dadurch, dasssämtliche den Speicher betre�ende Vorgänge vom Entwickler angestoÿen werden, ist ein mehrfa-cher Kernel-Aufruf ohne automatische Kopiervorgänge implizit gegeben. Entsprechender Over-head, wie er in anderen Bibliotheken auftritt, entfällt.

Gleichzeitig ist die Mächtigkeit der Bibliothek durch die starke Anlehnung an den CUDA-Toolkitzwar gegeben, dehnt sich aber nicht auf alle Bereiche aus. Insbesondere Referenz-Datentypen, wiesie in CUDA über Structs realisierbar sind, entfallen. Diese werden zwar im CUDA-C Quelltextunterstützt, können aber nicht aus Java heraus auf das Device kopiert werden. Hierfür existierenzwar in den einschlägigen Foren der Bibliothek bereits Vorschläge, eine Implementierung liegt je-doch nicht vor. Dafür werden, durch die explizite Spezi�kation der Kernel-Funktionen in nativemCUDA-C, spezielle Speicher wie Constant- oder Shared-Memory unterstützt.

Als Ausführungsmodi wird nur der GPU-Modus angeboten. Dies liegt darin begründet, dass vomEntwickler spezi�zierter nativer CUDA-Code ausgeführt wird. Allerdings liegt auch kein Wrappermit pthreads o.ä. vor. Ein Geschwindigkeitsvergleich Host-Device ist damit nicht möglich. Dernative Quelltext in CUDA-C führt auch dazu, dass die Kernel-Funktion nur unter Einbeziehungder Gra�kkarte getestet werden kann. Ein Aufruf von Java-Methoden aus der Kernel-Funktionist entsprechend nicht möglich.

Die für einen Vergleich erforderliche API zur Zeitmessung ist dagegen in Form von CUDA-Events

Page 41: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

5.3. Evaluation verfügbarer Abstraktionsbibliotheken 34

CUdeviceptr deviceValues = new CUdeviceptr ();

int valuesSize = values.length * Sizeof.INT;

cuMemAlloc(deviceValues , valuesSize );

cuMemcpyHtoD(deviceValues , Pointer.to(values), valuesSize );

Pointer kernelParameters = Pointer.to(

Pointer.to(deviceValues),

Pointer.to(new int[]{ values.length })

);

int gridSizeX = (values.length / getMaxThreadsPerBlock ()) + 1;

cuLaunchKernel(getEnvironment (). function ,

gridSizeX , 1, 1, // Groesse des Grids

1024, 1, 1, // Groesse der Bloecke

0, null , // Groesse des Shared -Memory

kernelParameters , null // Kernel -Parameter

);

cuCtxSynchronize ();

cuMemcpyDtoH(Pointer.to(values), deviceValues , valuesSize );

cuCtxSynchronize ();

cuMemFree(deviceValues );

Quelltext 5.1: Kernel-Aufruf in JCuda

implementiert. Dies entspricht aus den in den Anforderungen genannten Gründen der optimalenLösung.

Auch die unterstützten Plattformen entsprechen den Anforderungen. Mit Windows und Linuxwerden die beiden gröÿten Systeme unterstützt. Damit allerdings unter Linux eine Ausführungmöglich ist, muss die libcuda.so bereits vor Ausführung der Applikation geladen sein. Dies wirddurch Hinzufügen des nötigen Pfades in die LD_PRELOAD Umgebungsvariable erreicht.

Die Einbettung der Bibliothek erfordert hauptsächlich das Schreiben von Code zur Kompilierungder CUDA-Dateien in PTX oder Cubin. Soweit nicht alle Kernels bereits vor Ausführung kom-piliert werden sollen, ist ein explizites Build-Skript damit nicht zwingend notwendig.

Bei der eigentlichen Verwendung der Bibliothek ist stark darauf zu achten, dass der CuContext,der den JNI-Kontext kapselt, entweder wiederverwendet wird, oder aber nach der Kernel-Ausfüh-rung wieder freigegeben wird. Andernfalls sind bereits bei ≤ 100 Kernel-Aufrufen die Ressourcendes ausführenden Rechners erschöpft.

Insgesamt ist die Bibliothek auf Grund der dünnen Abstraktionsschicht leicht zu durchschauen,ist aber auf Grund der fehlenden Ausführungsmodi schlecht testbar.

JCUDA Die Bibliothek JCuda ist nicht mit der an der Rice-Universität in Houston entwickel-ten CUDA-Abstraktionsbibliothek JCUDA zu verwechseln. Diese Bibliothek konzentriert sichebenfalls darauf, native Funktionen der CUDA-Bibliothek durch JNI-Aufrufe für den Entwicklerverfügbar zu machen, erfordert dabei aber noch einen zusätzlichen Schritt zwischen Java-Code-Kompilierung und paralleler Ausführung. Dieser ist nötig, da Kernel-Aufrufe mit vier Kleiner-bzw. Gröÿer-Zeichen (<<<< ... >>>) im Java-Code kon�guriert werden. Die entsprechende

Page 42: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

5.3. Evaluation verfügbarer Abstraktionsbibliotheken 35

Compiler-Erweiterung ist proprietär. Die entstandene Bibliothek ist nur innerhalb der Rice-Universität verfügbar. Da letztendlich nur 2009 eine kurze Arbeit dazu verö�entlicht wurde,wird die Bibliothek nicht weiter in die Evaluierung einbezogen [YGS09].

5.3.2 Java-GPU

Eine weitere Bibliothek, mit dem Namen java-gpu, ist als Doktorarbeit am Trinity College inDublin entstanden. In der Ausarbeitung entwarf Peter Calvert den nötigen Code zur implizitenSchleifenparallelisierung auf der Gra�kkarte. Im Wesentlichen geht es dabei um die implizite undexplizite Erkennung und Parallelisierung von Schleifen in einem gegebenen Quellcode.

Die Parallelisierung erfolgt durch einen zusätzlichen Staging-Schritt zwischen der Kompilierungder Quelldateien und der eigentlichen Ausführung der generierten *.class-Dateien. In diesemZwischenschritt werden die generierten Dateien nochmals gelesen, parallelisiert und die paralleleVariante erneut geschrieben. Wird schlieÿlich die parallele Variante im Java-Classpath bei derAusführung angegeben, so wird die Applikation nebenläu�g auf der Gra�kkarte ausgeführt.

Code-Umwandlung Zur Parallelisierung sind verschiedene Schritte notwendig. Die Einzel-schritte sind in Abbildung 5.4 verdeutlicht.

Zuerst wird der von javac generierte Bytecode durch Verwendung des ASM-Frameworks [BLC02]importiert. Die Bibliothek erlaubt dabei die dynamische Transformation von Bytecode Objekten,also das Lesen und Einfügen von Instruktionen im existierenden Bytecode-Instruktionsgraphen.Um das Einlesen von Instruktionen über den Graph einfach zu gestalten, wird das Visitor-Patternangewendet [BLC02][S. 7].

Das Visitor-Pattern [GHJV94][S. 331] besteht aus zwei Arten von Elementen - Visitors undNodes.Visitors de�nieren visit-Methoden, die nach Aufruf der accept-Methode auf einem Node desObjektgraphen in der richtigen Reihenfolge aufgerufen werden. Im Beispiel von java-gpu wer-den visit-Methoden, z.B. für Methoden, Felder, Klassen oder auch Annotationen, de�niert.Diese kapseln den nötigen Code zum Aufbau des internen Objekt-Graphen zur Abbildung derInstruktionen in der kompilierten Class-Datei. Andere Visitors werden wiederum zur späterenCode-Generierung verwendet. Abbildung 5.3 illustriert den grundsätzlichen Aufbau zur Erstel-lung einer internen Bytecode-Repräsentation durch Nutzung des Entwurfsmusters.

Während der Abarbeitung der einzelnen Bytecode-Instruktionen durch Aufruf dedizierter visit-Methoden werden verschiedene Optimierungen angewendet. Einzelne Methoden werden in Ab-schnitte ohne ein- oder ausgehende Sprünge (Basic-Blocks) zerlegt [BLC02][S. 10]. Zur Erstellungeines Ablauf-Graphen werden Instruktionen den Blöcken zugeordnet, die anschlieÿend wiederumuntereinander verbunden werden. Um sicherzustellen, dass der Quellcode richtig typisiert ist,wird für Lese- und Schreiboperationen auf Felder, Variablen und Methodenparameter geprüft,ob deren Typen respektive zueinander passen. Beispielsweise darf kein Feld vom Typ Integer miteinem String beschrieben werden. Als Ergebnis ergibt sich ein typisierter Graph an Instruktionen,die aus dem Bytecode gelesen wurden, der anschlieÿend zur Schleifenerkennung weiterverwendetwerden kann. Der zugehörige Quelltext �ndet sich in bytecode.MethodImporter.

Page 43: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

5.3. Evaluation verfügbarer Abstraktionsbibliotheken 36

Node

accept(visitor: NodeVisitor)

MethodNode

visitor -> visitMethod(this)

ClassNode

visitor -> visitClass(this)

FieldNode

visitor -> visitField(this)

NodeVisitor

visitClass(node: ClassNode): voidvisitMethod(node: MethodNode): voidvisitField(node: FieldNode): void(...)

Abbildung 5.3: Lesen einer Klasse unter Nutzung des Visitor-Patterns, siehe [GHJV94][S. 331f]

Bytecode-Import(1)

Schleifenerkennung(2)

Kernel-Extraktion(3)

Datei-Export(4)

Abbildung 5.4: Schritte zur impliziten Schleifenparallelisierung in java-gpu

Page 44: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

5.3. Evaluation verfügbarer Abstraktionsbibliotheken 37

Als nächstes müssen existierende Schleifen erkannt werden. Diese sind im Bytecode nur nochdurch Sprünge bekannt und müssen aus den Basic-Blocks wiederhergestellt werden. Der entsp-rechende Vorgang erfolgt in drei Schritten:Zuerst werden natürliche Schleifen erkannt. Eine natürliche Schleife ist eine solche, die nur einenEintritts- und nur einen Austrittspunkt besitzt. Beispielsweise kann damit eine for-Schleife nichtmit einem return abgebrochen werden, da damit zwei Abbruchbedingungen gegeben wären.Entsprechend gefundene Schleifen werden als Menge an Loop-Objekten zurückgegeben (sieheanalysis.loops.LoopDetector, [Cal10][S. 15f]).Eine Schleife ist auf der GPU nur parallelisierbar, soweit ihre Indexe bekannt sind. Dazu wird imzweiten Schritt versucht, die Schleifen in sog. triviale Schleifen umzuwandeln. Diese de�nierensich dadurch, dass nur eine Bedingung die Schleife beendet, keine Schreiboperationen auf Va-riablen vor der Prüfung der Bedingung auftauchen und der Schleifen-Zähler um eine einfacheZahl in- oder dekrementiert wird, also keine Operationen auÿer Addition und Subtraktion dabeiauftreten.Schlieÿlich müssen die Indexe normalisiert werden. Dies bezieht z.B. die Anwendung einer >=

in eine <= Abbruchbedingung oder die Anpassung der Indexe für einen <-Operator mit ein.Die entstehende Menge an trivialen Schleifen wird an den letzten Schritt, die Erkennung vongeschachtelten Schleifen, weitergegeben (vgl. analysis.loops.LoopTrivialiser, [Cal10][S. 28,32f]).Im Wesentlichen werden hier die erkannten trivialen Schleifen in eine Baumstruktur übersetzt(loop nesting). Dies ist unabdingbar, da aus einem gerade ausgeführten Kernel keine weiterenKernels aufgerufen werden können. Dieser Fall könnte eintreten, wenn verschachtelte Schleifennicht erkannt und daraufhin alle Schleifen parallelisiert würden. Die eigentliche Implementierungder Erkennung ist straight-forward, da im Objektgraphen nur verglichen werden muss, ob alsKind-Element eine weitere Schleife auftritt (siehe analysis.loops.LoopNester#nest).

Für die erkannten Schleifen kann nun die eigentliche Extraktion durchgeführt werden. Die Paral-lelisierung erfolgt immer von auÿen nach innen. Nach und nach wird versucht mehrdimensionaleKernel-Aufrufe zu erkennen. Parallelisierbare Schleifen müssen dabei immer direkt geschachteltsein. Es darf also bis auf die Inkrementierung von Schleifenzählern keine Instruktion zwischen denSchleifenköpfen zu �nden sein. Dies ist nötig, da die mehrfach geschachtelten Schleifen in CUDAabstrahiert werden und die Einzelschleifen damit nicht mehr explizit ausgeschrieben werden,sondern über das Ausführungsmodell abgebildet werden.

Zuerst wird jeweils überprüft, ob eine Schleife überhaupt als CUDA-Kernel extrahiert werdenkann. Die Bibliothek stellt dabei zwei Möglichkeiten zur Verfügung: Spezi�kation einer Anno-tation mit Angabe des Namens der Inkrement-Variable, sowie die Durchführung einer automa-tischen Analyse. Letztere stellt sicher, dass keine direkten Schreibvorgänge auf lokale Variab-len auÿerhalb des Schleifenrumpfes geschehen und dass sich keine indirekten Schreibzugri�e,z.B. auf Arrays, überschneiden [Cal10][S. 36]. Dies ist zur parallelen Ausführung der einzelnenSchleifendurchläufe in CUDA und der damit einhergehenden unspezi�zierten Ausführungsrei-henfolge nötig. Von der Reihenfolge abhängige Schleifendurchläufe würden Race-Conditions unddamit unspezi�ziertes Verhalten hervorrufen.

Anschlieÿend wird die Anzahl der parallelisierbaren Dimensionen ermittelt. Dies wird durchÜberprüfung der direkten Kind-Elemente auf ihren Schleifentyp durch Anwendung des Resultatsdes vorhergehenden LoopNesters erreicht. Verwendete Variablen werden schlieÿlich insofern er-kannt, als deren Inhalte beim Kernel-Start und dessen Beendigung kopiert werden müssen. Dazu

Page 45: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

5.3. Evaluation verfügbarer Abstraktionsbibliotheken 38

wird eine sogenannte Live-Variable-Analyse durchgeführt. Eine Variable ist dabei live, wenn siegelesen wird, bevor ihr Inhalt geschrieben wurde [Cal10][S. 16]. Derartige Variablen müssen vonauÿerhalb befüllt worden sein und müssen deswegen auf die GPU kopiert werden. Zusätzlichwerden automatisch alle statische Variablen in den Konstanten-Speicher kopiert. Auf all diesencopy-in-Variablen wird abgefragt, ob auf sie während der Kernel-Abarbeitung ein Schreibzugri�statt�ndet. In diesem Fall wird die Variable nach der Kernel Ausführung zurück kopiert (sieheanalysis.KernelExtractor).

Basierend auf den gefunden Kernel-Instruktionen wird nun der eigentliche CUDA-C-Kernel durchAnwendung des Visitor-Patterns auf dem internen Objektgraphen erstellt. Der entsprechendgenerierte Kernel-Code, sowie nötiger Quelltext für den Aufruf des Kernels, wird direkt in eineDatei geschrieben. Der Name der Aufruf-Methode entspricht dabei der JNI-Konvention. Wirdwährend des Exportes eine Instruktion erkannt, die nicht in C umgesetzt werden kann, so erfolgtein Fallback auf die Java-Implementierung. Dies ist u.a. dann der Fall, wenn eine Java-API-Methode aufgerufen wird, für die kein C Äquivalent zur Verfügung steht (z.B. einige Java-Math-Methoden). Nach erfolgreichem Export des Kernels wird schlieÿlich der Basic-Block des Kernelsdurch einen JNI-Methodenaufruf ersetzt. Dazu muss letztendlich ein interner Methodenaufrufim Instruktionsgraph erzeugt und statt dem eigentlichen Basic-Block eingehängt werden.

Abschlieÿend werden geänderte Teile des Instruktions-Graph in Bytecode exportiert und diegenerierten Quelltext-Dateien kompiliert.

Evaluation Die Bibliothek ist insbesondere interessant, weil sie eine der ersten Bibliothekenzur GPU-Parallelisierung in Java darstellt und dabei eine sehr klare und schöne Struktur aufweist.

Allerdings haften ihr zwei Nachteile an, weswegen im Nachfolgenden nicht weiter auf unter-schiedliche Features eingegangen wird:

� Die Plattform-Unterstützung ist rein auf Linux begrenzt. Das in den Anforderungen geforderteWindows-Betriebssystem wird nicht unterstützt. Grund dafür sind die sich von Linux un-terscheidenden Header-Imports der Windows C-Quelltext-Dateien.

� Das Projekt ist für den ursprünglichen Entwickler mit der Dissertation abgeschlossen. Seitder Fertigstellung der Arbeit fanden keine Code-Änderungen mehr statt. Mittlerweile kom-piliert der generierte Code zwar noch, ausführbar ist er aber nicht mehr. Eine entsprechendeAnfrage an den Entwickler erhielt zwar eine Antwort, eine Lösung erfolgte jedoch nicht.

Gleichzeitig fehlen noch viele weitere geforderten Anforderungen:Verschiedene Ausführungsmodi sind nicht möglich, da nur entweder eine sequentielle Abar-beitung oder aber eine GPU-Abarbeitung möglich ist. Eine parallele Ausführung in Java istausgeschlossen bzw. wird nicht unterstützt.Abgesehen davon ziehen mehrere Aufrufe desselben Kernels zusätzliche Kopiervorgänge nachsich, da die Bibliothek nicht zwischenspeichert, welche Objekte bereits auf der GPU gespeichertsind.

Sehr positiv ist die Übersichtlichkeit des Quelltextes und insbesondere die Dokumentation. DerQuelltext kann durch Inline-Kommentare und aussagekräftige Methoden- und Feldnamen im Stil

Page 46: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

5.3. Evaluation verfügbarer Abstraktionsbibliotheken 39

von Clean-Code wie ein Buch gelesen werden. Mit der Dissertation als Dokumentation sind auchdie theoretischen Hintergründe verständlich dargestellt und geben einen guten Überblick überdie Architektur der Arbeit.

5.3.3 Aparapi

Aparapi (�A Parallel API�) ermöglicht die explizite Spezi�kation von OpenCL-Kernels in Java.Die Bibliothek stellt ein aktives Community-Projekt dar, das v.a. von drei AMD-Entwicklernweiterentwickelt wird. Ursprünglich wurde die Bibliothek unter dem Codenamen Barista für dieKonferenz SuperComputing 2009 entwickelt.

Besonders zeichnet sich die Bibliothek dadurch aus, dass die Parallelisierung nicht in einemzusätzlichen Schritt zwischen Kompilierung und Ausführung erfolgt, sondern dass ausführbarerOpenCL-Quellcode zur Laufzeit generiert, kompiliert und ausgeführt wird.

Kernel-Spezi�kation Ausführbare Kernels werden durch Ableitung von com.amd.aparapi.-

Kernel erstellt. Ein Beispiel für einen Kernel inkl. dessen Ausführung �ndet sich in Quelltext5.2.

Die zu implementierende #run Methode kapselt dabei die parallel auszuführenden Instruktionen.Felder werden automatisch in Kernel-Parameter umgewandelt. Dabei werden Annotationen fürSpeicherbereiche wie Constant- oder Shared-Memory unterstützt.

Instantiierte Kernels sind schwergewichtig, da diese im Hintergrund den nötigen OpenCL-Kontexthalten. Dementsprechend sollten auch nicht zu viele Kernel-Instanzen gleichzeitig vorgehaltenwerden, sondern besser bestehende Instanzen wiederverwendet oder aber als Zwischenschritt#dispose aufgerufen werden. Dadurch wird verwendeter Speicher wieder freigegeben.

Durch Ausführung der #execute Methode wird die Abarbeitung gestartet. Hierzu ist eine Rangeanzugeben, die die ein- oder zweidimensionale Dimensionsgröÿe bei der Ausführung kapselt.Auf dem Kernel-Objekt kann zusätzlich durch Aufruf von #setExecutionMode der Ausführungs-modus gesetzt werden. Aparapi stellt hier die parallele Ausführung auf GPU, CPU, in Formeines Java-Thread-Pools sowie in Form einer nativen CPU-Ausführung mit OpenCL, sowie einensequentiellen Ausführungsmodus zur Verfügung.

Kompilierung und Ausführung Die Kompilierung wird immer pro Kernel-Instanz beimersten Kernel-Start ausgeführt und bei mehrfacher Ausführung wiederverwendet. Tritt währendder Kompilierung ein Fehler auf, so fällt die Ausführung automatisch auf einen Java-Thread-Poolzurück. Gleichzeitig wird die Transformation und Generierung des OpenCL-Quelltextes nur fürdie native Ausführung durchgeführt. Da OpenCL, im Gegensatz zu CUDA, nicht auf die GPU-Ausführung festgelegt ist, kann der generierte Code sowohl für den nativen CPU- als auch denGPU-Ausführungsmodus verwendet werden.

Die Ausführung erfolgt in diversen Einzelschritten, die in Abbildung 5.5 verdeutlicht sind.

In einem ersten Schritt wird der Bytecode der Kernel-Klasse eingelesen und in eine interneRepräsentation überführt. Verantwortlich hierfür ist die ClassModel#parse-Methode, die zuerstden Pfad der entsprechenden Class-Datei sucht und den Inhalt anschlieÿend konvertiert. Dazu

Page 47: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

5.3. Evaluation verfügbarer Abstraktionsbibliotheken 40

public class ArrayIncrementKernel extends Kernel {

@Constant

private int[] in;

private int[] out;

private int length;

public ArrayIncrementKernel(int[] in, int[] out , int length) {

this.in = in;

this.out = out;

this.length = length;

}

@Override

public void run() {

int id = getGlobalId ();

if (id >= length) return;

out[id] = in[id] + 1;

}

public static void main(String [] args) {

int[] in = {1, 2, 3, 4, 5};

int[] out = new int[in.length ];

ArrayIncrementKernel kernel =

new ArrayIncrementKernel(in, out , in.length );

kernel.setExecutionMode(EXECUTION_MODE.GPU);

kernel.execute(in.length );

System.out.println(Arrays.toString(out ));

}

}

Quelltext 5.2: Array-Inkrementierung mit Aparapi

Page 48: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

5.3. Evaluation verfügbarer Abstraktionsbibliotheken 41

wird in Aparapi keine zusätzliche Bibliothek verwendet. Stattdessen entsteht die interne Repräsen-tation durch Anwendung der Java-Bytecode Spezi�kation [LYBB13], d.h. durch Byte-weises ein-lesen der Quelldatei. Im Wesentlichen wurde im Rahmen der Bibliothek also eine eigenständigeBytecode-Bibliothek zum Einlesen erstellt. Jede einzelne Instruktion wird mit einem internenInstructionSet verglichen, der sämtlichen möglichen Befehle innerhalb des Bytecodes enthält.Die sich ergebende Menge an Instruction-Objekten geben den Kontroll�uss der Methodenwieder.

In einem zweiten Schritt wird der Instruktionsgraph durch Setzen der Verzweigungsziele aufden einzelnen Instruktionen vervollständigt und das Ergebnis so angepasst, dass anschlieÿendOpenCL-Quelltext generiert werden kann. Dabei werden beispielsweise Bytecode DUP-Operationen[LYBB13][S. 412], die eine bestimmte Menge an vorhergehenden Operationen duplizieren, exp-lizit ausgeschrieben. Eine weitere Anpassung bezieht sich auf die Entfernung bzw. Erkennungvon Instruktionen mit Seitene�ekten. Dazu gehören u.a. Instruktionen wie return(a++) oder a= b++.

Daraufhin werden auf dieselbe Art und Weise die aus dem Kernel-Eintrittspunkt aufgerufenenMethoden eingelesen. Möglich sind dabei u.a. Aufrufe statischer Methoden. Wichtig ist dabeidie Prüfung auf eventuell auftretende Rekursion. Diese wird von OpenCL nicht unterstützt[Mun11][S. 232]. Da für jede Methode eine Liste an Methoden vorgehalten wird, die von derMethode selbst aufgerufen werden, kann rekursiv geprüft werden, ob unter Aufruf dieser Metho-den die eigene Methode nochmals aufgerufen wird (siehe MethodModel#checkForRecursion).Tritt der entsprechende Fall ein, so ist eine OpenCL-Generierung nicht möglich, und der Aus-führungsmodus wird auf die Java-Thread-Pool-Ausführung zurückgesetzt. Als Abschluss derErkennungs- bzw. Einlesephase wird nochmals über alle Methoden und deren Instruktionengelaufen und erkannt, welche Felder, Arrays und Objekte verwendet werden. Diese werden sowohlfür den Kopiervorgang als auch für das Schreiben des Kernels markiert.

Für das eigentliche Schreiben der Kernel-Methode ist die KernelWriter-Klasse zuständig. Hierwerden benötigte Referenzen in einem struct vorgehalten sowie Methoden-Implementierun-gen geschrieben. Das Schreiben der einzelnen Instruktionen erfolgt Instruktion für Instruktionin der BlockWriter#writeBlock-Methode. In if-Konstrukten über alle möglichen Bytecode-Instruktionstypen werden die passenden OpenCL-Instruktionen in den resultierenden Stringgeschrieben. Als Gesamtergebnis ergibt sich eine Zeichenkette mit dem Kernel, seinen Instruktio-nen, allen aufgerufenen Methoden und deren Inhalt, sowie, für die Abarbeitung nötige, structsund Felder.

Schlieÿlich wird in einem JNI-Aufruf durch Nutzung der OpenCL-API der generierte Code kom-piliert (#clCreateProgramWithSource und #clBuildProgram). Aus dem Kernel referenzierteFelder werden zur eigentlichen Ausführung in einen Array an KernelArg-Objekten übersetzt.Diese kapseln sowohl den Typ des Feldes, z.B. Float, Int, Array, usw., als auch den Speicher-typ, also Constant-, Local- oder Global-Memory, und den Feld-Inhalt. Entsprechend wird für dieArgumente Speicherplatz alloziert, die Inhalte auf die GPU kopiert und der Kernel ausgeführt.Nach der Ausführung wird nötiger Speicher wieder zurück auf die CPU kopiert und unter denpassenden Adressen Java zur Verfügung gestellt.

Page 49: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

5.3. Evaluation verfügbarer Abstraktionsbibliotheken 42

Lade und konvertiere *.class-Dateien

Setzen der Verzweigungen

Einlesen von aufgerufenen Methoden

Erkennung verwendeter Felder

Schreiben des Kernels

Kompilierung via JNI

noch nicht kompiliert

ja

OpenCL Ausführung

Java Ausführung

GPU- oder CPU-Ausführung?

ja nein

Abbildung 5.5: Ausführung eines Aparapi-Kernels

Page 50: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

5.3. Evaluation verfügbarer Abstraktionsbibliotheken 43

Evaluation Für Aparapi konnten alle gestellten Evaluationsalgorithmen vollständig implemen-tiert werden.

Insbesondere hervorzuheben ist die Unterstützung der expliziten Speicherverwaltung, die esermöglicht, mehrere Kernel-Aufrufe, ohne zwischengeschaltete Kopieroperationen, zu erstellen[apa13b][ExplicitBu�erHandling]. Dies funktioniert allerdings nur basierend auf einem Kernel,da Speicherbereiche mehrerer Kernels vollständig getrennt sind. Deswegen kann ein Kopiervor-gang bei Ausführung des zweiten Kernels nicht verhindert werden. Die Entwickler empfehlenmomentan aus dem Standard-Eintrittspunkt mit einem If-Konstrukt auf eine Klassen-Variablezu prüfen und darauf basierend weitere Methoden aufzurufen, wobei jede Bedingung eineneigenständigen Kernel repräsentiert [apa13b][EmulatingMultipleEntrypointsUsingCurrentAPI].Ein Vorschlag für die Verwendung verschiedener Eintrittspunkte in einem Kernel durch die Im-plementierung dedizierter Methoden liegt dabei bereits vor, ist aber im Augenblick nur halbfertigim Quellcode zu �nden.

Eine objektorientierte Programmierung ist dementsprechend nur sehr begrenzt möglich. Spe-zi�ziert man unterschiedliche Kernel-Objekte, so handelt man sich auf der einen Seite einenstark erhöhten Speicherbereich durch im Hintergrund vorgehaltenen OpenCL-Kontexte sowieeine verlangsamte Ausführung durch auf Grund der getrennten Speicherbereiche nötig werdendeKopiervorgänge ein.

Zum Thema Speicherverwaltung wurden zusätzlich weitere Speicherarten implementiert. So wer-den explizit Constant- und Shared-Memory über Annotationen unterstützt [apa13b][UsingCon-stantMemory, UsingLocalMemory]. Diese müssen vom Entwickler händisch an Attribute ange-bracht werden, damit der für OpenCL notwendige Quelltext für die Speichertypen richtig einge-fügt wird. Die Verwendung expliziter Annotationen ist durchaus sinnvoll, da nur an sehr aus-gewählten Stellen die Speichermodelle sinnvoll sind und meist angepasste Algorithmen benötigen.Basierend auf diesen Implementierungen können leicht weitere Speichertypen, wie z.B. Texture-Memory o.ä., implementiert werden.

All diese Speichermodelle müssen, soweit der Entwickler sie nicht verwenden möchte, nicht spe-zi�ziert werden. In diesem Fall übernimmt die Bibliothek die gesamte Speicherverwaltung, inkl.der Kopiervorgänge von und zur GPU, transparent für den Entwickler.

Eventuell auftretende Exceptions werden nicht gefangen. Tritt z.B. eine NullPointerExceptionwährend der GPU-Ausführung auf, so beendet die Ausführung mit einem OpenCL-Fehler undfällt auf die Ausführung in Java zurück. Tritt hier die Exception nochmals auf, so wird dieseentsprechend normalen Java-Exceptions weitergereicht. Leider entspricht es nur dem Idealfall,dass ein Fehler, der auf der GPU auftritt, auch in der Java-Ausführung in Erscheinung tritt.Während der Evaluation wurden mehrere Fehler angetro�en, die nur in der GPU-Ausführungauftraten.

In Hinblick auf die Verwendung von Arrays bietet Aparapi die Verwendung von eindimensiona-len Arrays an. Mehrdimensionale Arrays werden erst in der neuesten Version unterstützt. Daslength-Attribut auf Arrays ist ebenfalls implementiert, ist allerdings in der vorliegenden Ver-sion nicht funktionsfähig. Verwendete Arrays dürfen nicht nur primitive Datentypen, sondernauch komplexere Objekte enthalten. Diese werden in C-Structs zur Ausführung umgewandelt.Die Entwickler warnen allerdings vor Geschwindigkeitseinbuÿen aufgrund der struct-Befüllung[apa13b][NewFeatures].

Page 51: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

5.3. Evaluation verfügbarer Abstraktionsbibliotheken 44

Soweit der Ausführungsmodus entsprechend gesetzt ist wird der vorliegende Java-Kernel-Codeautomatisch in OpenCL umgesetzt. Dabei werden Methodenaufrufe für Methoden auÿerhalb dereigenen Klasse unterstützt. Allerdings sollte vom Aufruf allzu komplexer Methoden abgesehenwerden, da entsprechender Code auch auf der GPU ausgeführt werden muss.Durch Implementierung des OpenCL-Interfaces lässt sich die Code-Generierung so beein�ussen,dass selbst geschriebener nativer OpenCL-Code zur Ausführung verwendet wird [apa13b][New-OpenCLBinding]. Dadurch lässt sich die in den Anforderungen beschriebene prototypische Ent-wicklung umsetzen.

Als Ausführungsmodi stehen die bereits oben beschriebenen Varianten CPU, GPU, Java-Thread-Pool (JTP) sowie SEQ zur sequentiellen Ausführung zur Verfügung. Bis auf SEQ werden alleAusführungsmodi gleichwertig unterstützt. Dies bezieht sich v.a. auch darauf, dass API-Aufrufezum Heraus�nden der Thread-ID auch im JTP-Modus funktionieren. Da spezi�zierte Blöcke zurJTP-Ausführung sequentiell ausgeführt werden, ist auch die Verwendung von Shared-Memory imentsprechenden Modus möglich und liefert die richtigen Ergebnisse. SEQ wird nur mit Blöckender Gröÿe 1 unterstützt, da gröÿere Werte im Fall von Block-Barrieren durch die sequentielleAusführung einen Dead-Lock auslösen würden (siehe KernelRunner). Dies ist allerdings aufGrund der guten Unterstützung von JTP keine Einschränkung.

Zur Zeitmessung wird vor und nach der Kernel-Ausführung die momentane Systemzeit in Nano-sekunden gemessen und die Di�erenz ermittelt. Diese spiegelt die Ausführungszeit inklusive allerKopiervorgänge wieder. Über eine zusätzliche System-Property lässt sich auch explizites Pro�l-ing einschalten. Damit werden bei jeder Kernel-Ausführung ProfileInfo-Objekte generiert, dieeinen detaillierten Überblick über die Dauer von ausgeführten Lese-, Schreib- und Ausführungs-operationen geben.

Eine Schnittstelle zu einem OpenGL-Binding zur Anzeige von Ergebnissen gibt es momentannicht. Folglich muss der Speicherinhalt jeweils vor der Visualisierung in den CPU-Speicher zurückkopiert werden und kann nicht direkt auf der GPU erfolgen. Schön wäre hier eine direkte Integra-tion von z.B. Java Bindings für OpenGL (JOGL), so dass kein weiterer Kopiervorgang nötig ist.In den FAQ geben die Entwickler von Aparapi an, dass sie sich eher auf parallele Datenberech-nungen als auf die Anzeige von Gra�ken spezialisieren. Eine direkte Integration von JOGL istdeswegen nicht geplant [apa13b].

Als Plattformen zur Ausführung werden Linux und Windows gleichermaÿen unterstützt. Durchden JIT-Ansatz zur Kompilierung von OpenCL-Code ist die Bibliothek sehr leicht einzubinden,da keine etwaigen Staging-Schritte in einem Build-Skript nach der Kompilierung ausgeführt wer-den müssen. Damit muss nur das entsprechende jar-Archiv eingebunden werden und zusätzlichsichergestellt werden, dass die native OpenCL-Bibliothek von Aparapi au�ndbar ist.

Ein groÿes Manko ist die mangelhafte Dokumentation. DasWiki bietet zwar einige Informationen,eine Beschreibung der Architektur �ndet sich allerdings nirgends. Einige JavaDoc-Kommentarebieten Hinweise, was komplexe Methoden bewirken. Im Wesentlichen bleibt zur Dokumentationaber nur der Blick in den Quelltext. Dieser ist allerdings nicht immer klar geschrieben ist undweist mittlerweile viele Fragmente auf (siehe z.B. Code für mehrere Eintrittspunkte in einemKernel).

Da die Bibliothek von einer breiten Community und von drei AMD Entwicklern vorangetriebenwird, ist eine Einstellung sehr unwahrscheinlich. Momentan wird aktive Entwicklung zur Ein-

Page 52: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

5.3. Evaluation verfügbarer Abstraktionsbibliotheken 45

bindung der in Java 8 kommenden Lambda-Funktionen betrieben (siehe dazu auch Abschnitt5.3.6).

Insgesamt ist die Komplexität der Bibliothek überschaubar, da das Einlesen des Bytecodes nur dieJava-Spezi�kation abarbeitet und anschlieÿend auf einer internen Repräsentation Optimierungendurchgeführt werden. Tritt ein Fehler bei der Kompilierung bzw. Code-Generierung auf, so lässtsich dieser v.a. auf Grund der JIT-Kompilierung, die ein direktes Debugging in die Bibliothekerlaubt, mit verhältnismäÿig geringem Aufwand �nden. Schwierig wird die Fehlersuche v.a. wennein Fehler im nativen Teil der Bibliothek gesucht werden muss.

5.3.4 Rootbeer

Die Bibliothek Rootbeer wird von Philip Pratt-Szeliga an der Syracuse-Universität in New Yorkentwickelt. Die grundsätzliche Bibliothek (�java-autocuda�) entwarf er in seiner Masterarbeit zurimpliziten Parallelisierung von Java-Code in CUDA [PS10], die fast parallel zu java-gpu entstand.Zum Kopieren nötiger Daten und zum Starten von Kernels nützt diese das bereits beschriebeneFramework JCuda [PS10][S. 22].Die entstandene Bibliothek wurde weiter entwickelt und letztlich in Rootbeer umbenannt. Al-lerdings liegt mittlerweile der Fokus nicht mehr auf der impliziten Parallelisierung von Java,sondern, genau wie bei Aparapi, auf der expliziten Spezi�kation paralleler Kernels. Auch JCudawurde im Lauf der Zeit entfernt und durch eine selbst entwickelte Ausführungsumgebung ersetzt.Im Gegensatz zu Aparapi ist bei Rootbeer ein expliziter Staging-Schritt zur Code-Generierungnach der Kompilierung notwendig. Hier wird der Kernel-Code extrahiert und in die entsprechendeCUDA-Darstellung umgewandelt. Dies geschieht auf Basis eines Eingangs-Jar-Archivs und ergibtnach der Umwandlung ein neues Jar-Archiv, das alle parallelen Code-Bestandteile enthält.

Kernel-Spezi�kation Ein Kernel, wie z.B. zur Array-Inkrementierung in Quelltext 5.3, wirddurch Implementierung des Kernel-Interfaces und De�nition der entsprechenden #gpuMethod

de�niert. Um die eigentliche Ausführung kümmert sich die Rootbeer-Klasse. Diese kapselt dieMethoden zur Ausführung, zum Setzen der Grid-Kon�guration und der Gra�kkarte. Vorsicht istbei der parallelen Ausführung mehrerer Kernels geboten. Die Rootbeer-Klasse ist aufgrund vonKlassenattributen nicht thread-safe.

Der Ausführungsmodus kann zur Laufzeit geändert werden. Rootbeer bietet hier drei Modi an:MODE_GPU, MODE_JEMU (Java-Emulation mit Thread-Pools) und MODE_NEMU (Na-tive Emulation mit pthreads). Der Modus kann allerdings zur Ausführungszeit nicht beliebigverändert werden. Bei der Umwandlung wird entweder nativer CPU- oder GPU-Code generiert.Entsprechend muss bei einer Änderung des Ausführungsmodus die Umwandlung erneut aus-geführt werden. Dagegen kann ein Wechsel auf JEMU immer statt�nden, da hier kein nativerQuelltext benötigt wird.

Die eigentliche Ausführung beginnt mit Ausführung der #runAll-Methode. Normalerweise wirdeine Liste an Kernel-Objekten erwartet, die nacheinander abgearbeitet werden. Seit Neuestem istauch die Spezi�kation eines einzelnen Kernel-Objektes möglich (sog. Template), das entsprechendder Thread-Kon�guration ausgeführt wird.

Zur Laufzeit können innerhalb der Kernel-Methode verschiedene Rootbeer-Methoden zur Iden-

Page 53: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

5.3. Evaluation verfügbarer Abstraktionsbibliotheken 46

ti�zierung des aktuellen Threads auf dem RootbeerGpu-Objekt aufgerufen werden. Diese geben,analog zur nativen CUDA-Ausführung, z.B. den momentanen Block oder die Thread-ID im ak-tuellen Block zurück.

Staging Zur Generierung des CUDA-Codes ist ein expliziter Staging-Schritt nötig. Dieser istin Abbildung 5.6 illustriert und wird nachfolgend genauer erklärt. Auf Grund fehlender Architek-turdokumentation stammen die nachfolgenden Informationen direkt aus dem Quelltext.

Die zentrale Einstiegsklasse zur Kompilierung ist rootbeer.Main. Hier werden entsprechendeKommandozeilenargumente, die zur Kompilierung verwendet werden, erkannt und verarbeitet.Argumente sind z.B. für die verschiedenen Ausführungsmodi, die Ausführung von Tests, dieVerwendung von Doubles und Rekursion vorhanden. Direkt im Anschluss wird basierend aufdem verwendeten Betriebssystem (o�ziell unterstützt sind Windows, Linux und Mac OS X) inden entsprechenden Verzeichnissen nach der CUDA-Bibliothek gesucht und diese geladen.

Anschlieÿend wird die Bytecode-Bibliothek Soot initialisiert. Diese stellt die Funktionalitätenzum Einlesen, zur Manipulation und zur Ausgabe von Bytecode zur Verfügung [VKZ11][S. 1�].Als interne Intermediate Representation / Zwischenrepräsentation (IR), die auch für Manipu-lationen genutzt werden kann, stellt sie zwei Varianten zur Verfügung: Jimple und Shimple.Dabei ist Jimple die eigentliche Zwischensprache und Shimple eine Static Single Assignment(SSA)-optimierte Variante davon. SSA bedeutet, dass eine Zuweisung an eine Variable nur exakteinmal im Programm vorkommen kann. Dies wird über die Einführung zusätzlicher Variablenerreicht. Rootbeer verzichtet auf diese Art der Darstellung und verwendet Jimple als Zwischen-sprache [PSFW12][S. 3].Zur Initialisierung wird der Inhalt des Eingangsarchivs in einem temporären Verzeichnis abgelegtund anschlieÿend das Laden der Klassen angestoÿen. Dies erfolgt über eine für Rootbeer modi-�zierte Variante von Soot. Im Wesentlichen wurde eine RootbeerCompiler-Klasse aufgebaut,die die Einzelklassen des Jars einliest und Meta-Informationen daraus extrahiert. Dabei wer-den v.a. Eintrittspunkte für Kernels erkannt und der Aufrufgraph zwischen allen Methoden undKlassen des Jars aufgebaut. Jedem Eintrittspunkt wird dabei zusätzlich ein Tiefensuchen-Objekt(DfsInfo) zugeordnet, das in der Eintrittsmethode direkt oder indirekt referenzierte Typen sowieMethodenaufrufe enthält. Die einzelnen Instruktionen im Methodenkörper werden wiederum vonSoot eingelesen und in die interne Darstellung übersetzt.

Nachdem alle Klassen in das Zwischenformat konvertiert sind, werden die einzelnen Kernel-Methoden bearbeitet. Zuerst prüft dabei ein FieldReadWriteInspector auf Felder, die in derEintrittsmethode, bzw. in daraus aufgerufenen Methoden, verwendet werden. Entsprechend ge-schriebene oder gelesene Felder werden in passenden Datenstrukturen abgelegt. Diese sind späterwichtig, um heraus�nden zu können, welche Daten auf die GPU und respektive zurück kopiertwerden müssen.

Anschlieÿend wird der eigentliche Kernel-Code generiert. Der entsprechende Schritt hängt vonder zur Staging-Zeit angegebenen Ausführungskon�guration ab:Wird der GPU-Modus eingestellt, so wird CUDA-Code erzeugt. Alle anderen Kon�guratio-nen resultieren in nativem C-Code, der mit pthreads parallelisiert ist. Intern wird diese Artder Erzeugung in einer OpenCLScene#getOpenCLCode-Methode durchgeführt. Die Methode zurCUDA-Code Erzeugung be�ndet sich dabei ebenfalls in der OpenCLScene-Klasse. Der o�en-

Page 54: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

5.3. Evaluation verfügbarer Abstraktionsbibliotheken 47

public class ArrayIncrementKernel implements Kernel {

private int[] in;

private int[] out;

public ArrayIncrementKernel(int[] in, int[] out) {

this.in = in;

this.out = out;

}

@Override

public void gpuMethod () {

int index = RootbeerGpu.getThreadId ();

if (index >= in.length) return;

int value = in[index];

out[index] = value + 1;

}

public static void main(String [] args) {

int[] in = new int[] {1, 2, 3, 4, 5};

int[] out = new int[in.length ];

Rootbeer rootbeer = new Rootbeer ();

List <Kernel > kernels = new ArrayList <Kernel >();

for (int i = 0; i < in.length; i++)

kernels.add(new ArrayIncrementKernel(in , out);

rootbeer.runAll(kernels );

Configuration.runtimeInstance ()

.setMode(Configuration.MODE_JEMU );

System.out.println(Arrays.toString(out ));

}

}

Quelltext 5.3: Array-Inkrementierung mit Rootbeer

Page 55: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

5.3. Evaluation verfügbarer Abstraktionsbibliotheken 48

Initialisierung

Laden der Klassen im jar-Archiv

Prüfung von Feldern

Generierung des nativen Codes

Kompilierung

Generierung des nativen Codes

MODE_GPU

ja

Generiere Serialisierungs-Bytecode

für alle Kernels

Schreiben der Klassen

Packen der Klassen als neues jar-Archiv

Abbildung 5.6: Ablauf der Kompilierung in Rootbeer

Page 56: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

5.3. Evaluation verfügbarer Abstraktionsbibliotheken 49

sichtliche Widerspruch stammt wohl noch aus der Zeit der Masterarbeit, da damals versuchtwurde, OpenCL-Code zu erzeugen. Es blieb allerdings beim Versuch, da kein valider Quelltexterzeugt werden konnte [PS10][S. 52].Die Code-Generierung für die GPU- und die native Ausführung unterscheidet sich nur dadurch,dass der GPU-Code am Ende noch zusätzlich kompiliert wird. Die Spezi�ka, wie z.B. die Paral-lelisierung mit pthreads oder aber Kernel-Eintrittspunkte (z.B. das global-Attribut für CUDA)werden über Tweaks Klassen abgebildet, die global als Singleton zu Beginn des Staging-Schrittesgesetzt wurden und an passenden Stellen im Programm, wie z.B. zum Einfügen der Quelltext-Zeilen zum Kernel-Start, aufgerufen werden.

Die eigentliche Code-Generierung ist etwas schwieriger als in Aparapi. Grund dafür ist, dasssowohl Synchronisationsmonitore als auch Exceptions auf der GPU unterstützt werden. BeideFeatures �nden sich weit verstreut im Generierungscode wieder.Für Monitore wird bei jeder Methode, soweit diese synchronisiert ist, ein Intialisierungsblock fürdie Synchronisierung geschrieben. Diese ist über aktives Warten und ein atomares Compare-and-Swap (CAS)-Kommando realisiert.Exceptions müssen über try-catch-Blöcke abgebildet werden, die auch intern im Code-Segmentvorkommen dürfen. Dazu wird bei der Abbildung der einzelnen Instruktionen durch entsp-rechende Code-Generierung explizit auf Exceptions geachtet. Unterstützt werden dabei nurNullPointerExceptions und OutOfMemoryExceptions. ArrayIndexOutOfBounds-Exeptions wer-den nur indirekt unterstützt, da Rootbeer einen Fehler beim Lesen der Daten vom Heap meldet.Dabei gerät allerdings die Ausführung in einen Deadlock, da die Threads, die die Ergebnisseeinsammeln können, ewig auf die Ergebnisse in ihrer BlockingQueue warten.

Nach Schreiben des Initialisierungs-Codes, v.a. für die Monitore, fehlt noch ein letzter Schrittvor dem Schreiben der eigentlichen Instruktionen. Zur Unterstützung von Monitoren müsseneventuell vorhandene Monitor-Gruppen über EnterMonitorStmt- und ExitMonitorStmt-Instruk-tionen erkannt werden. Anschlieÿend werden die einzelnen Instruktionen den Gruppen zugeord-net und diese einzeln durch das bereits in java-gpu eingeführte Visitor-Pattern verarbeitet. Fürjeden Instruktionstyp ist dabei in der Klasse MethodStmtSwitch eine case.*-Methode imple-mentiert, die in einen StringBuilder den generierten C-Code schreibt. Hier ist auch für dieMonitor-Gruppe ein nochmaliges aktives Warten implementiert.

Das Ergebnis der Methodeninhalte wird zusammen mit nötigen Header-De�nitionen, Methoden-Prototypen und einem Kernel-Template in einen String geschrieben. Das Kernel-Template ent-hält den Eintritts-Quellcode, der schlieÿlich die eigentlichen, generierten, Kernels aufruft. DieserInhalt wird jeweils für Linux und für Windows vorgehalten, da die entsprechenden Header-De�nitionen im Fall von NEMU verschieden sind. Der Aufruf der generierten Kernel-Methodeergibt sich schlieÿlich durch String-Ersetzung eines in der Kernel-Template-Datei de�nierten Pa-rameters durch den generierten Methodennamen.

Der generierte Code wird im Fall der GPU-Ausführung anschlieÿend kompiliert. Da die Header-Dateien im Fall der CUDA-Ausführung für Windows und Linux gleich sind, wird nur der Linux-Code zur Kompilierung verwendet. Als Ergebnis ergeben sich zwei Byte-Arrays mit dem für32- und 64-Bit als Fat-Binary kompilierten Quellcode. Dieses enthält den für unterschiedlicheGPU-Architekturen kompilierten cubin-Code.

Nach der Kompilierung wird abschlieÿend Java Bytecode generiert, der nötige Methoden zum

Page 57: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

5.3. Evaluation verfügbarer Abstraktionsbibliotheken 50

Lesen- und Schreiben der auf der GPU benötigten und veränderten Daten zur Verfügung stellt.Die Idee dabei ist, dass der Inhalt sämtlicher verwendeter Felder und Variablen zuerst in einenSpeicherbereich auf den Heap geschrieben werden und dieser anschlieÿend mit nur einem, viaJNI aufgerufenen, cudaMemcpy-Aufruf auf die GPU transferiert werden kann. Rückwärts funk-tioniert der Ablauf analog. Die dazu nötigen Methoden werden mit Soot generiert und in eineSerializer-Klasse geschrieben, die in die Kernel-Klasse eingehängt wird (siehe VisitorGen).

Schlieÿlich werden alle Kernel-Klassen mit dem zusätzlichen Interface CompiledKernel versehen,das die notwendigen Methoden, zum Beispiel zum Abruf der erwähnten Serializer-Instanz,kapselt. Gleichzeitig wird das Interface bei der Ausführung zur Unterscheidung bereits kom-pilierter und noch nicht kompilierter Kernels verwendet. Wurde der Staging-Schritt beispielsweiseübersprungen, so fehlt das Interface an den Kernel-Klassen.

Da damit die Code-Generierung und -Kompilierung abgeschlossen ist, werden nun alle Klassenaus der Zwischendarstellung zurück in Bytecode geschrieben und die resultierende Dateien alsneues jar-Archiv gepackt.

Ausführung Nach der Kompilierung kann der generierte Code ausgeführt werden. Der grundle-gende Ablauf ist in Abbildung 5.7 illustriert.

Wird der Staging-Schritt übersprungen, so wird dies durch Prüfung auf das CompiledKernel-Interface erkannt und der Kernel n-mal durch Aufruf der #gpuMethod-Methode ausgeführt. Dadie JEMU-Ausführung näher an die GPU-Ausführung angelehnt ist, wäre diese hier die bessereWahl. Dies lässt sich in der Rootbeer#runAll Methode durch Änderung einer if-Bedingungumstellen.

Für die NEMU-Ausführung wird zuerst der generierte C-Code kompiliert und anschlieÿend aus-geführt. Die Kompilierung erfolgt dabei bei jedem Kernel-Aufruf. Anschlieÿend wird durch Aufrufder generierten writeToHeap-Methoden der Heap-Inhalt für den Kernel-Aufruf generiert und derInhalt an eine native #runOnCpu-Methode übergeben. Diese führt in C den eigentlichen Kernel mitkonstanten vier Threads aus. Die Ergebnisse werden unter Nutzung der readFromHeap-Methodengelesen und in den Objekten wieder zur Verfügung gestellt. Die für die GPU-Auführung nötigeAnzahl an Threads bzw. Blöcken wird vor der Ausführung von einem BlockShaper berechnet,der entweder eine zur Kompilierzeit gesetzte Thread-Kon�guration übernimmt oder eine Kon�-guration automatisch so zu bestimmen versucht, dass die Di�erenz der in der GPU gestartetenThreads zur Anzahl der geforderten Kernel-Aufrufe minimal ist. Für den NEMU-Modus spieltdas keine groÿe Rolle, da dieser in vier Threads das Verhalten nur emuliert.

Die JEMU-Ausführung greift auf ein selbst geschriebenes Master-Worker-Pattern zurück. Dazuwird eine Menge an Arbeitern, alias CpuCores, instantiiert und die auszuführenden Kernels aufdiese verteilt. Die Thread- sowie die Block-ID werden während der Abarbeitung einer Aufgabezur Kernel-Ausführung aktualisiert. Damit die nebenläu�g laufenden Threads dabei keine Race-Conditions verursachen, werden die Werte in Attribute des aktuellen Threads gesetzt. ÜberAbruf, welcher Thread die #getTheadIdxx bzw. #getBlockIdxx Methoden aufruft, einen Castauf den benutzerde�nierten Thread und Abruf des Attributes, wird der entsprechend richtigeWert für die momentane Abarbeitung zurückgegeben.

Die GPU-Ausführung entspricht bis auf wenige Erweiterungen dem NEMU-Modus. Entspre-

Page 58: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

5.3. Evaluation verfügbarer Abstraktionsbibliotheken 51

Kopiere Kernel-Blöcke in CPU-Heap

Kompiliere Kernel-Code

Ausführung derKernel-Funktion

Lesen der Blöcke

Parallele Abarbeitungauf CPU-Kernen

Kopiere Kernel-Blöcke in CPU-Heap

Bestimme Blockund Grid Größe

Lade Funktion auf GPU

Ausführung derKernel-Funktion

Lesen der Blöcke

Entfernen derKernel Funktion

JEMU_MODUS

ja nein

NEMU_MODUS

ja nein

Sequentieller Aufrufvon #gpuMethod aufallen Kernels

erster Kernel kompiliert?

ja nein

Abbildung 5.7: Ablauf der Ausführung in Rootbeer

Page 59: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

5.3. Evaluation verfügbarer Abstraktionsbibliotheken 52

chend werden zuerst nötige Daten im Heap gesammelt. Hier besteht bereits die Möglichkeit dieSerialisierung zu parallelisieren. Dies ist bisher noch deaktiviert, soll aber in einer der nächstenVersionen freigeschaltet werden. Anschlieÿend werden, analog zum NEMU-Modus, die Block-und Grid-Gröÿen berechnet und als Abschluss der Kon�gurationsphase der kompilierte cubin-Code in einer #compileCode-Methode über einen nativen Funktionsaufruf geladen.Durch einen Aufruf von #runBlocks wird ein nativer Aufruf gestartet, der zuerst die Heap-Datenauf die GPU kopiert, den Kernel Aufruf unter Verwendung von NVIDIA API-Methoden startetund das Resultat zurück kopiert.Analog zum NEMU-Modus werden die im Heap gesammelten Daten schlieÿlich gelesen und demaufrufenden Code zur Verfügung gestellt.

Evaluation Obwohl die Bibliothek im Vergleich zu den drei vorhergehenden Bibliothekenwesentlich komplexer ist, konnten mit Rootbeer nicht alle Algorithmen zur Evaluation imple-mentiert werden. Dies betri�t die Algorithmen zur Berechnung der Reduktion von Arrays sowiedas Verfahren der konjugierten Gradienten.

Beide Algorithmen benötigen eine Synchronisation von auf der GPU ausgeführten Thread-Blöcken. Im Test produzierte diese Synchronisation jedoch unter Verwendung des exakt gleichenAlgorithmus, der auch bei Aparapi und JCuda zum Einsatz kam, die falschen Resultate. Dievon Rootbeer berechneten Ergebnisse deuten darauf hin, dass die Synchronisation nicht funk-tionsfähig ist. Eine Anfrage an den Entwickler wurde zwar beantwortet, der Fehler jedoch nichtbehoben.

Weitere Schwierigkeiten in Bezug auf die Bibliothek betre�en beispielsweise die Code-Generierung:

Diese ist vom Entwickler vollständig gekapselt und funktioniert im Normalfall auch problemlos.Trotzdem lassen sich Fälle konstruieren, in dem der generierte Quelltext nicht ausführbar ist.Grund dafür ist, dass die Entfernung nicht verwendeter Methoden und Klassen während derUmwandlung nicht immer zuverlässig funktioniert. Unter expliziter Referenzierung der Metho-den und Klassen konnte dies während der Evaluation verhindert werden.Ebenfalls kann es vorkommen, dass nach gröÿeren Umbenennungen im Code der Staging-Vorgangabbricht bzw. keine Veränderung aufweist. In diesem Fall müssen händisch die als Cache dienen-den Verzeichnisse .rootbeer- und .soot im Benutzerverzeichnis gelöscht werden. Für Linuxbietet die Bibliothek dafür ein eigenes Skript mit dem Namen clear_cache an.Auch die Verwendung von Block- und Thread-Ids in Kernels funktioniert nicht zuverlässig. ImTest konnten Fälle reproduziert werden, in denen die Berechnung der globalen ID aus Thread-und Block-ID das falsche Resultat lieferte. Wird dagegen die ID als Kernel-Parameter bei derexpliziten Erstellung der Kernel-Objekte übergeben, so wird der generierte Code fehlerfrei aus-geführt.

Die Code-Umwandlung erfolgt bis auf oben genannte Einschränkungen zuverlässig. Verwen-dete Methoden, die aus der eigentlichen Kernel-Funktion aufgerufen werden, werden ebenfallskonvertiert und für die Ausführung mit einbezogen. Nativer Code, wie in den Anforderungenbeschrieben, kann dagegen nicht eingebunden werden. Sehr unschön ist die Dauer des Staging-Vorgangs. Diese kann gut und gerne fünf bis 10 Minuten betragen und muss nach jeder Code-Änderung neu ausgeführt werden. Mussten dabei die Cache-Verzeichnisse .soot und .rootbeer

zusätzlich gelöscht werden, so dauert die Umwandlung um so länger. Die lange Dauer ist al-

Page 60: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

5.3. Evaluation verfügbarer Abstraktionsbibliotheken 53

lerdings im Augenblick Schwerpunkt der Entwicklung und wurde in den letzten Beta-Versionenwesentlich verbessert (1.0.48-alpha4). Eine schnelle, prototypische Entwicklung, ist damit trotz-dem nicht möglich. Der Autor hat hier starke Zweifel, dass, wie in der Rootbeer-Verö�entlichungbeschrieben, eine deutliche Steigerung der Produktivität durch geringere Entwicklungszeitengegenüber der nativen CUDA-Entwicklung möglich ist [PSFW12][S. 1].

Rootbeer übernimmt die Verwaltung des GPU-Speichers inklusive der Kopiervorgänge auf dieGra�kkarte. Leider ist dabei der mehrfache Aufruf einzelner Kernels, ohne dass bei jedem Aufrufdie Daten erneut kopiert werden, nicht möglich. Nach einem Aufruf ist der Zustand des Rootbeer-Ausführungsobjektes wieder zurückgesetzt. Das Selbe gilt entsprechend auch für die Ausführungverschiedener Kernels. Bei jeder Ausführung wird der Code erneut geladen, nötige Daten kopiert,die Ausführung gestartet, Daten zurück kopiert und der Kontext aufgeräumt. Shared-Memorykann dabei durch eine API für alle Ausführungsmodi verwendet werden. Andere Speichertypenwerden dagegen nicht unterstützt.

In Sachen Fehlerbehandlung stellt Rootbeer eine eigene Infrastruktur für die Unterstützung vonExceptions zur Verfügung. Dies funktioniert einwandfrei für NullpointerExceptions und OutOf-MemoryExceptions. ArrayIndexOutOfBoundsExceptions werden, wie bereits beschrieben, nurindirekt unterstützt. Eine Rückmeldung erfolgt in jedem Fall. Bricht allerdings die Kompilierungwährend des Stagings ab, so bleibt quasi keine Möglichkeit den Fehler als normaler Entwick-ler zu �nden. Derartige Fehler treten beispielsweise bei der Verwendung des momentan nichtunterstützten JDK 7 auf. Auch das Windows-Betriebssystem wird im Augenblick nur o�ziellunterstützt, während im Test der Staging-Vorgang mit einer Fehlermeldung abbricht.

Rootbeer-Code zu testen ist verhältnismäÿig schwierig. Im Wesentlichen bleibt die Auswahl, ent-weder mit der seriellen bzw. JEMU-Ausführung vorlieb zu nehmen, wobei dann nicht alle Featuresunterstützt werden, oder aber vorher den langwierigen Staging-Schritt in Kauf zu nehmen, ohnedann aber die Möglichkeit des Debuggings zu haben, da der ausgeführte Code rein generiert istund auch nicht auf der CPU ausgeführt wird. Die Komplexität der Bibliothek trägt dabei nichtzum einfachen Au�nden von Ursachen für Fehlermeldungen bei. Ein normaler Staging-Vorgang,der mit dem bereitgestellten jar-Archiv vorgenommen wird, bietet nur die Möglichkeit des De-buggens, wenn der Bibliotheks-Quelltext in einer IDE aufgesetzt wird und von dort aus derStaging-Vorgang angestoÿen wird. Durch die mangelnde Architektur-Dokumentation und denunverständlichen, schlecht strukturierten und wenig dokumentierten Quelltext wird die Einar-beitung bzw. das Debuggen massiv erschwert.

Zur Zeitmessung steht eine sog. Stopwatch-Klasse zur Verfügung, die die CPU-Di�erenz in Mil-lisekunden misst. Dediziert kann damit abgefragt werden, wie lange für die Serialisierung, Aus-führung und Deserialisierung gebraucht wurde und wie insgesamt die Grid-Kon�guration bei derAusführung aussah. Allerdings funktioniert die Zeitmessung nur für die GPU-Ausführung. Fürdie NEMU- und die JEMU-Ausführungsmodi ist dies nicht implementiert.

Der Einbau in ein bestehendes Projekt ist verhältnismäÿig einfach. In einem Build-Skript kannder zusätzliche Schritt des Aufrufs des Rootbeer-Jars eingebaut werden, sodass automatisch dasparallelisierte jar erzeugt wird. Genau wie bei Aparapi muss natürlich das entsprechende jar-Archiv zur Programmierung im Classpath vorhanden sein.Schlieÿlich muss bei der Einrichtung eines Betriebssystems, auf der Rootbeer ausgeführt werdensoll, überprüft werden, wo die Bibliothek für CUDA genau abgelegt ist. Die von Rootbeer über-

Page 61: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

5.3. Evaluation verfügbarer Abstraktionsbibliotheken 54

prüften Pfade be�nden sich in der Klasse CudaLoader. Diese enthält allerdings einige Standard-Suchpfade, die nicht zwangsläu�g für alle Betriebssysteme zutre�en müssen. Entsprechend mussdie Klasse ggf. angepasst werden, da sonst die Ausführung des generierten Quelltextes auf Grundder nicht au�ndbaren CUDA-Bibliothek fehlschlägt. Für das Evaluationsbetriebssystem Ubuntuwurde dies entsprechend durchgeführt.

Insgesamt besitzt Rootbeer durchaus Potential zur Nutzung als GPU-Bibliothek. Insbesonderedie Unterstützung von Exceptions auf der GPU sind attraktiv. Allerdings ist der momentane En-twicklungsstand nicht annähernd für eine produktive Anwendung empfehlenswert. Hinzu kommt,dass der momentane Hauptentwickler seine Doktorarbeit aufbauend auf Rootbeer schreibt unddiese relativ bald beendet haben wird. Die entsprechende Information stammt aus Antworten desEntwicklers auf Fehlermeldungen auf der Github-Seite des Projekts. Dementsprechend wird sichder Entwickler nach Fertigstellung der Arbeit anderweitig bewerben und damit nicht zwingendZeit für Rootbeer erübrigen können.

Page 62: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

5.3. Evaluation verfügbarer Abstraktionsbibliotheken 55

5.3.5 Delite

Einen ganz anderen Weg als Rootbeer und Aparapi nimmt die von der Stanford-Universität ent-wickelte Bibliothek Delite. Genau wie java-gpu setzt die Bibliothek auf implizite Parallelisierungund erfordert damit keine explizite Spezi�kation von Kernels, die später auf der GPU ausge-führt werden. Das Grundprinzip der Bibliothek ist der, von den Entwicklern geprägte, Begri�der Sprach-Virtualisierung (language virtualization, [RO10][S. 2]).

Eine Programmiersprache ist dann virtualisierbar, wenn sie eine DSL-Umgebung beherbergenkann, die gegenüber einer Eigenimplementierung der DSL bzgl. folgender Kriterien identisch ist:

� AusdrucksfähigkeitDomänen-Spezialisten sollen in der Lage sein ihr Wissen in natürlicher Sprache abzulegen.

� GeschwindigkeitDas Domänen-Wissen kann zur Optimierung des generierten Codes genutzt werden, umdadurch eine höhere Geschwindigkeit zu erzielen.

� SicherheitDie Implementierung des DSL-Programms darf Garantien bzgl. Programmausführung nichtaufgeben.

� Vertretbarer AufwandStatt einer DSL kann eine neue Sprache implementiert werden. Dazu ist allerdings dieaufwändige Erstellung eines entsprechenden Compilers nötig. Dieser Aufwand soll bei derSprach-Virtualisierung entfallen.

Framework-Stack Vor der Beschreibung des Ablaufs der Kompilierung bzgl. der eigentlichenProgramm-Spezi�kation ist es nötig den grundsätzlichen Framework-Stack zu verstehen bzw.deren Einzelaufgaben näher zu erläutern. Im Wesentlichen werden verschiedene DSLs, Deliteselbst, Leightweight Modular Staging (LMS) sowie Scala-Virtualized verwendet.

Grundsätzlich bietet die Programmiersprache Scala bereits eine gute Ausgangsposition für DSLs.Grund dafür sind v.a. die hohe Flexibilität der Syntax der Sprache, sehr wenige Sprach-Features,die nicht als Methode realisiert und damit auch vom Entwickler überschreibbar sind, impliziteDe�nitionen und Parameter sowie Manifeste [MRHO12][S. 1].Die hohe Flexibilität erlaubt damit die Spezi�kation von Konstrukten, die auf der einen Seitewie natürliche Sprache und auf der anderen Seite wie native Scala-Konstrukte aussehen. Dieswird auch in Bibliotheken, z.B. zum Unit-Testing von Scala-Applikationen, aktiv verwendet. DerAusdruck (5+3) mustBe 8 ist z.B. in der Sprache valide. Scala selbst setzt ebenfalls auf sehrwenige integrierte Sprach-Features und de�niert die meisten Features über derartige Funktionen.Für domänenspezi�sche Sprachen, wie es die Sprach-Virtualisierung fordert, ist dies optimal, dadie Syntax an die Problem-Domäne angepasst werden kann.

Weitere Konstrukte erlauben es, Parameter und Funktionsaufrufe zu verstecken. Somit wer-den implizite Typ-Konvertierungen, Methodenaufrufe oder Parameter möglich, die bei Bedarfaufgerufen werden. Die entsprechenden Aufrufe bzw. Parameter-De�nitionen werden vom Com-piler eingefügt. Zum Beispiel ist es damit möglich, eine Funktion, die als def test(implicit a:

Page 63: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

5.3. Evaluation verfügbarer Abstraktionsbibliotheken 56

String) {...} de�niert ist, mit test() aufzurufen. Dazu muss aber im momentanen Scope eineimplizite Variable von Typ String de�niert sein: implicit val someVariable = "HalloWelt".Auf dem selben Weg lassen sich auch Methodenaufrufe einfügen [OSV10][S. 453�]. Im Hinblickauf domänenspezi�sche Sprachen scha�t dies die Möglichkeit eine klare Syntax durch Versteckenvon nötigen, aber für die Domäne semantisch verwirrenden, Methodenaufrufen zu scha�en.Eine spezielle Ausprägung von impliziten Parametern sind Manifest-Objekte. Diese geben zurLaufzeit Aufschluss über generische Typen. In einer generischen Methode kann damit z.B. geprüftwerden, ob der Typ-Parameter T vom Typ String ableitet. Sobald der Compiler einen implizitenParameter einer Methode vom entsprechenden Typ �ndet, wird automatisch ein entsprechendesObjekt generiert [MRHO12][S. 2]. Zur Laufzeit ist dies in Java nicht möglich, da durch dieErstellung des Bytecodes die generischen Typ-Informationen verloren gehen [OSV10][S. 546].

Bei Scala-Virtualized handelt es sich um eine Erweiterung des

DSLsOptiML

Delite

LMSLeightweight Modular

Staging

Scala-VirtualizedScala

LibraryScala

Compiler

...OptiLA

Anwendung

Abbildung 5.8: Framework-Stack in Delite

Scala-Compilers sowie der Scala-Bibliothek. Die davon für Delitewichtigste Erweiterung ist, dass noch mehr fest vorde�nierteSprachkonstrukte vom Entwickler überschreibbar sind. Ein Bei-spiel dafür ist, dass, unter Verwendung der Erweiterung, auchdas If-Konstrukt durch Überschreiben der __ifThenElse(c,a,b)Methode neu implementiert werden kann [MRHO12][S. 2f].

Das Delite zugrunde liegende Framework LMS nutzt diese Artder Überschreibung syntaktischer Konstrukte um eine IR derKontrollstrukturen des Programms aufzubauen. Im Wesentli-chen stellt es den Unterbau für einen als Bibliothek de�niertenCompiler bzw. Transformer zur Verfügung. Die Umwandlung ineine IR wird dabei durch Ausführung des Domänen-Quellcodeserstellt. Da Ergebnisse von Ausdrücken bei der Ausführung di-rekt bestimmt werden, unterscheidet LMS zwischen Ausdrü-cken, die zur Staging-Zeit bestimmt werden, und Ausdrücken, deren Resultat erst bei der ei-gentlichen Ausführung bestimmt werden kann. Dazu werden sog. abstrakte Typ-Konstruktoren

eingeführt. Deren Aussagekraft beschränkt sich darauf, dass ein gegebener Ausdruck zur Aus-führungszeit ein Ergebnis liefern wird, zur Stage-Zeit aber nur der Code-Transformation und-Generierung dient. Zum Beispiel wird ein Ausdruck der Form (3+4) im generierten Code zueiner konstanten Zahl 7, da kein abstrakter Typ-Konstruktor verwendet wird. Schreibt man dage-gen einen Ausdruck der Form val a: Rep[Int] = 3; val b = 3 + 4, so wird die Zahl 3 durcheinen impliziten Methodenaufruf in ein abstraktes Typ-Objekt umgewandelt. Der daran hän-gende Code kann zur Staging-Zeit nicht ausgewertet werden und �ieÿt in die Code-Generierungein [RO10][S. 1].

Die nötige Infrastruktur zur Code-Generierung sowie die IR für Basis-Typen in Scala stellt LMSebenfalls zur Verfügung. Dabei werden Scala, C++, CUDA und OpenCL als Zielsprachen für dieCode-Generierung unterstützt. Gleichzeitig werden durch LMS zur Staging-Zeit bereits diverseCode-Optimierungen durchgeführt. Diese beziehen sich beispielsweise auf sog. Loop-Fusion, alsodas Zusammenlegen mehrerer Schleifen, oder Inlining, d.h. die Entfernung von Funktionsaufru-fen durch direktes Einfügen in den aufrufenden Quellcode. Zusätzlich existiert eine einfach zunutzende Schnittstelle um eigene Optimierungen einzubauen bzw. neue IR-Nodes einzufügen.Dies bezieht sich wiederum auf das Grundkonzept der Sprach-Virtualisierung, das u.a. voraus-

Page 64: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

5.3. Evaluation verfügbarer Abstraktionsbibliotheken 57

setzt, dass das Domänen-Wissen zur Generierung von optimalem Code genutzt werden kann[RSL+11][S. 11�].

Auf LMS aufbauend stellt Delite die Infrastruktur für oft verwendete parallele Muster zur Verfü-gung. So können auf einem de�nierten DeliteArray z.B. Map-Reduce-Operationen ausgeführt, indie IR übersetzt, optimiert und für die Code-Generierung verwendet werden [RSL+11][S. 7]. Ent-wickler müssen dabei beim Schreiben ihrer DSL-Applikationen darauf achten, nur die de�niertenDSL-API Methoden zu verwenden. Die normalen Scala-Collection-Klassen sind hierfür nichtgeeignet. Auÿerdem ist Delite die eigentliche Schnittstelle, die den generierten Code in Dateienschreibt und diese im Anschluss einer Delite-Runtime zur Ausführung übergibt. LMS stellt nurdie grundlegenden Funktionalitäten zur Erstellung der IR, für Transformationen und zur Code-Generierung zur Verfügung. Delite setzt diese Funktionalitäten zu einer Gesamt-Bibliothek zurparallelen Ausführung zusammen.

Auf Delite aufbauend �nden sich verschiedene vorde�nierte DSLs, wovon die Meisten undoku-mentiert sind. Die wohl wichtigste prototypisch implementierte DSL ist OptiML, die Strukturenfür maschinelles Lernen zur Verfügung stellt. Zusätzlich können mathematische Konstrukte wieMatrizen, Vektoren oder Graphen verwendet werden [Suj][S. 1�]. Im Wesentlichen ist OptiMLals Alternative zu Matlab positioniert. OptiML wurde ebenfalls zur Evaluation von Delite ver-wendet.

Die letzte Ebene �ndet sich in der eigentlichen DSL-Anwendung, die vom Entwickler spezi�ziertwird. Eine Beschreibung inklusive Beispiel �ndet sich im folgenden Abschnitt.

Kernel-Spezi�kation Delite erfordert keine Spezi�kation von parallelen Kernels, sondern er-stellt diese basierend auf von der Bibliothek bereitgestellten Domänenobjekten. Eine Beispielan-wendung zur parallelen Array-Inkrementierung, die auf OptiML aufbaut, �ndet sich in in Quell-text 5.4.

Eine DSL ist immer in ein Interface und eine Implementierung aufgeteilt. Der eigentliche Codewird nur gegen das Interface geschrieben. Bei Ausführung der Transformation wird eine Imple-mentierung de�niert, die beschreibt, wie die Interface-Methoden auf die IR und die IR auf denzu generierenden Quelltext abgebildet werden.

Für OptiML besteht das Interface in der OptiMLApplication-Klasse. Dort werden alle, für dieDSL verfügbaren, Methoden angegeben. Durch ein benutzerde�niertes Mix-In können auch selbstimplementierte DSL-Methoden eingehängt werden. Mix-Ins erlauben es den Quellcode andererKlassen in die aktuelle Klasse einzumischen. Es ist eine Art Vererbung, die aber durch Lineari-sierung auch Mehrfachvererbung zulässt [OSV10][S. 645]. Benutzerde�nierte Methoden müssenanschlieÿend implementiert werden. Dabei muss angegeben werden, wie die DSL-Methoden aufz.B. generierten Scala- oder CUDA-Code abgebildet werden sollen.

Die Implementierung wird durch Ableitung von einem Runner-Objekt und durch Einmischungdes Interface-Traits de�niert. Ein Trait ist eine abstrakte Klasse, die in eine andere Klasse, durchNutzung des with-Schlüsselwortes, eingemischt werden kann [OSV10][S. 217]. In diesem Fall wirdder vorher bereits de�nierte Applikations-Trait de�niert, der die #main-Methode implementiert.Diese Methode kapselt den eigentlichen, auszuführenden, Quelltext.

Die Beispielimplementierung verwendet eine API-Methode um einen Nullvektor der Länge 5

Page 65: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

5.3. Evaluation verfügbarer Abstraktionsbibliotheken 58

object SingleDimensionArraySupportRunner

extends OptiMLApplicationRunner

with SingleDimensionArraySupport

trait SingleDimensionArraySupport extends OptiMLApplication {

def main() {

val length = 5

val vectorElements = Vector.zeros(length)

vectorElements.pprint ()

val newVectorElements = vectorElements.map(_ + 1)

newVectorElements.pprint ()

}

}

Quelltext 5.4: Array-Inkrementierung mit Delite

zu erstellen. Die verwendete Vektor-Klasse stammt aus der OptiLA-DSL, auf der OptiML auf-baut. Die Methode #zeros wird auf eine #densevector_obj_zeros Methode abgebildet, die mitParametern und Rückgabewerten auf den bereits beschriebenen abstrakten Typ-Konstruktorenbasiert. Nachfolgend wendet die Map-Operation auf alle Elemente des Vektors die in rundenKlammern angegebene Funktion an und gibt einen neuen Vektor, der alle Ergebnisse enthält,zurück. Die anzuwendende Funktion ist hier in Kurzform angegeben. Der Unterstrich referenziertden Wert des ersten Parameters der Funktion, der um +1 inkrementiert wird. Schlieÿlich wirdder neue Vektor auf die Konsole ausgegeben.

Staging Der Ablauf der Ausführung eines Delite- bzw. OptiML-Programms gliedert sich in zweiSchritte: das Staging, in dem der de�nierte Scala-Code transformiert wird, und die Ausführungdes Ergebnisses. Der Ablauf ist in Abbildung 5.9 dargestellt.

Vor dem Staging wird der Scala-Code mit dem erweiterten Scala-Compiler kompiliert. Damitdabei keine Typ-Informationen verloren gehen, verwendet der Code von Delite und LMS exzessivManifest-Objekte. Diese sind nötig, da, wie bereits erwähnt, generische Typen im Java-Bytecodeauf Object gecastet werden und damit verloren gehen [LYBB13][S. 66]. Manifest-Objekte werdenvom Compiler als entsprechender Ersatz eingefügt.

Die eigentliche Umwandlung beginnt mit Ausführung der de�nierten Runner-Klasse. Dabei wirdnicht die vom Entwickler de�nierte #main-Methode ausgeführt, sondern eine Methode in derKlasse DeliteApplication, die für das eigentliche Staging zuständig ist. Basierend auf deneingestellten Kon�gurationsoptionen wird ermittelt, welche Ziel-Sprachen generiert werden sollen.Entsprechend werden Instanzen der Generatoren erstellt. Wird eine benutzerde�nierte DSL ver-wendet, so muss auch die entsprechende #getCodeGenPkg-Methode überschrieben werden. Diesemuss basierend auf der übergebenen Zielsprache einen, um die neuen DSL-Methoden erweiterten,Generator zurückgeben.

Zur Initialisierung gehört ebenfalls das Setzen nötiger Transformatoren für die spätere Opti-mierung der generierten IR sowie das Kopieren der von den Ziel-Sprachen verwendeten Daten-

Page 66: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

5.3. Evaluation verfügbarer Abstraktionsbibliotheken 59

.scala .classKompilierungScala Compiler

.cu

.scala .cpp

.deg

StageDelite /OptiML

AusführungDelite Runtime

HelloWorld

Abbildung 5.9: Ablauf ausgehend von Scala-Quellcode-Dateien bis hin zum ausgeführten Pro-gramm

strukturen in den Build-Ordner. Delite generiert immer alle kon�gurierten Ziel-Sprachen und er-laubt es zur Laufzeit diese Sprachen zu wechseln bzw. teilweise auch zu kombinieren [RSL+11][S.8]. Die kopierten Datenstrukturen beziehen sich z.B. auf in den verschiedenen Ziel-Sprachenimplementierte Utility-Klassen wie eine Array-Liste o.ä..

Im nächsten Schritt wird die eigentliche IR generiert. Dies wird durch Ausführung der de�nierten#main-Methode erreicht, die um die zusätzlichen Programm-Argumente �geliftet� wurde. Da dieArgumente erst zur Laufzeit und nicht zur Stage-Zeit bekannt sind, werden diese durch einabstraktes Symbol (fresh[T], [RO10][S. 5]) ersetzt. Damit ist die externe Abhängigkeit gekapseltund der Funktionsinhalt o�en gelegt. Die Funktion wird nun ausgeführt (siehe Expressions-

#reifySubGraph, val r = b) und durch Aufruf der implementierten Interface-Methoden, sowieder überschriebenen Methoden der Scala-Kontrollstrukturen, in die IR übersetzt.

Eventuell auftretende Sub-Blöcke werden durch rekursive Methoden-Aufrufe ebenfalls eingelesen.Als Beispiel seien hier der then- und der else-Block von __ifThenElse genannt. Diese müssenextra eingelesen werden, weswegen eine zusätzliche reify-Aktion ausgeführt wird. Externe Ab-hängigkeiten bzw. Seitene�ekte werden gleichzeitig mit re�ect-Aktionen eingesammelt.

Beide Aktionen beziehen sich auf die monadische Charakterisierung von Seitene�ekten undBerechnungen in funktionalen Programmiersprachen bzw. dem λ-Kalkül [Fil10][S. 1]. Dazu wer-den Reify- und Reflect-Objekte in die IR eingehängt. Ersteres gibt dabei die Grenzen zwi-schen verschiedenen Monaden (dt. einwertiges Element, Einzeller) an. Letzteres bestimmt Ab-hängigkeiten bzw. Seitene�ekte des Monaden in einem Reflect-Element. LMS speichert dieseSeitene�ekte als Menge an Ausdrücken im eingehängten Re�ect-Element [RO10][S. 7] (sieheEffects#reflectEffectInternal).

Während des Aufbaus der IR können weitere Code-Optimierungen ein�ieÿen. Die empfohleneVorgehensweise ist das Überschreiben der Implementierung der Interface-Methode, Anwendender Optimierung und Aufruf der Basis-Methode. Durch Pattern-Matching lassen sich die Opti-mierungen elegant implementieren [RO10][S. 3f].

Im Anschluss werden kon�gurierte Transformationen ausgeführt. Delite ersetzt beispielsweise

Page 67: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

5.3. Evaluation verfügbarer Abstraktionsbibliotheken 60

einzelne Map- und Reduce-Objekte durch eine optimierte Kombination. Diese Transformationwird allerdings nur in der Optigraph-DSL verwendet.

LMS stellt zusätzlich die Infrastruktur zur Verschmelzung mehrerer Schleifen (loop fusion) bereit.Erstere werden explizit in einer eigenen Delite-Phase ausgeführt und die IR entsprechend umge-hängt. Die Verschmelzung der Schleifen wird dabei kurz vor der eigentlichen Code-Generierung,also nach Ausführung der Delite-Transformationen, beim Durchlaufen der Einzelblöcke zur Ge-nerierung, ausgeführt.

Zuletzt fehlt noch die Aufspaltung des Quelltextes in einzelne Kernel-Blöcke sowie die Ausgabedes generierten Codes. Für beide Operationen ist die Klasse DeliteGenTaskGraph zuständig, dieauch die Ausgabe der einzelnen Generator-Quelltext-Teile steuert. Da zur Laufzeit anschlieÿendverschiedene Code-Sprachen (Scala, OpenCL, CUDA) zur Verfügung stehen, wird gleichzeitigein Delite Execution Graph (DEG) generiert. Dieser enthält, in Form eines JavaScript ObjectNotation (JSON)-Objektes, für jeden generierten Kernel-Block Abhängigkeiten von Variablenund anderen Blöcken sowie die für den Block zur Verfügung stehenden Ziel-Sprachen. Dazupassend wird jeweils der Pfad zur generierten Kernel-Datei der Sprache ausgegeben. Der DEGwird während der Ausführung benötigt, um Kernels auf die Hardwareressourcen verteilen zukönnen.

Dabei wird einmal über alle gespeicherten Blöcke der IR gelaufen. Für jeden Block werdenanhand der gespeicherten Abhängigkeiten (Re�ect-E�ekte) gebundene bzw. geänderte Variab-len ermittelt (siehe DeliteGenTaskGraph#emitAnyNode). Zusätzlich ermöglichen es die bereitsermittelten Seitene�ekte eventuell auftretende Abhängigkeiten von anderen Kernels (im CodeantiDeps genannt) zu ermitteln. Basierend auf dem Block-Typ (z.B. While-Schleife oder If-Else)wird ein Eintrag im DEG geschrieben, der die antiDeps sowie Eingangs- und Ausgangsvariablenenthält. Durch Nutzung der Export-Generatoren wird für alle spezi�zierten Sprachen der zuge-hörige Block generiert und dessen Inhalt anschlieÿend zusammen mit dem von den Ein- und Aus-gangsvariablen abhängigen Header und Footer des Kernels emittiert. Der eigentlich Block-Inhaltwird Ausdruck für Ausdruck über Pattern-Matching und in Abhängigkeit von der Ziel-Sprachein den Methodenkörper geschrieben. Wenn nötig werden zuletzt nötige Methoden zum Kopierendes Inhaltes auf die Ziel-Architektur bzw. zurück sowie zur Allokation des Speichers ausgegeben.

Ausführung Nach dem Staging kann der DEG ausgeführt werden. Delite stellt dazu eineeigene Runtime zur Verfügung.

Die Ausführung teilt sich wiederum in drei Phasen auf: Die Erstellung eines Ausführungsplans,dessen Kompilierung und Ausführung. Für die Erstellung des Ausführungsplans ist ein Sched-

uler zuständig, für die Ausführung ein Executor. Beide Klassen stehen in unterschiedlichenAusprägungen zur Verfügung. Damit lässt sich kon�gurieren, ob der generierte Code nur aufder CPU (SMPStaticScheduler, SMPExecutor) oder auf CPU und GPU (SMP_GPU_Scheduler,SMP_GPU_Executor) ausgeführt werden soll. Die nachfolgende Beschreibung bezieht sich auf diekombinierte GPU- / CPU-Ausführung. Die reine CPU-Ausführung entspricht einem Subset derbeschriebenen Variante.

Nach dem Einlesen des generierten DEG und dessen Übersetzung in eine interne Repräsentationwird ein Ausführungsplan erstellt. Dieser gibt an, auf welcher Hardwareressource, also z.B. aufwelcher GPU oder auf welcher CPU, die Aufgabe ausgeführt wird und in welcher Reihenfolge die

Page 68: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

5.3. Evaluation verfügbarer Abstraktionsbibliotheken 61

Ausführung statt�nden muss, um alle Abhängigkeiten zu befriedigen. Ob eine Operation auf derGPU ausgeführt wird, hängt davon ab, ob die Operation eine Ausführung auf der GPU unter-stützt. Grundsätzlich wird die Code-Generierung für alle kon�gurierten Ziele und alle gefundenenOperationen während des Stagings durchgeführt. Wird allerdings eine Operation gefunden, dieauf Grund des Variablentyps oder auf Grund der Operation nicht generierbar ist, so wird eineException geworfen und die Generierung für dieses Ziel abgebrochen. Wenig überraschend sindSchleifen die einzigen Operationen, die auf der GPU unterstützt werden (siehe GPUCodegen).

Für die CPU-Ausführung werden parallele Operationen, wie z.B. Schleifen, aufgespalten undauf verfügbare CPU-Ressourcen verteilt. Gleichzeitig wird zur Reduktion von Kopiervorgängenversucht die Operationen auf den Hardwareressourcen auszuführen, auf denen bereits ihre Ab-hängigkeiten ausgeführt werden (clustering, SMP_GPU_StaticScheduler#cluster, [BSL+11][S.94]).

Nach der Erstellung des Ausführungsplans werden die Kernels zur späteren Ausführung kom-piliert. Auÿerdem werden Übergänge zwischen den Einzeloperationen eingefügt. Dies schlieÿtzum Beispiel das Kopieren der Daten zwischen Hardware-Ressourcen und Synchronisationspunk-te nach Kopiervorgängen ein. Damit ist gewährleistet, dass z.B. eine von einer GPU-Operationabhängige CPU-Operation so lange wartet, bis die Durchführung der GPU-Operation beendetund der Kopiervorgang auf die CPU abgeschlossen ist. Auch die eigentlichen Kernel-Aufrufewerden entsprechend eingefügt.

Abschlieÿend wird der generierte Ausführungsplan ausgeführt.

Evaluation Im Vergleich zu den anderen Bibliotheken mutet Delite in Verbindung mit dervorhandenen Toolchain als professionellstes Projekt an. Grund dafür ist die sehr durchdachte undgut dokumentierte Code-Architektur sowie die Features zur impliziten Parallelisierung. Trotzdemkonnten nicht alle Testfälle implementiert werden, da die Code-Generierung nicht vollständigzuverlässig funktioniert.

Kompiliert ein spezi�zierter Quelltext gegen die vorhandene DSL, so bedeutet dies nicht, dass dergenerierte Code anschlieÿend auch lau�ähig ist. Entsprechend ausgegebene Fehlermeldungen sindkryptisch zu lesen und dienen nicht der Fehlersuche. Zwei Beispiele für derartige Fehlermeldungensind in Abbildung 5.5 dargestellt. Ob nun der Wert x4440 in der Zeile 77 gefunden wurde odernicht - der Entwickler kann derartige Angaben nicht auf seinen selbst entwickelten Quelltextabbilden und einen eventuell vorhandenen Fehler beheben. Nur durch Ausprobieren ist es hiermöglich ausführbaren Quelltext zu produzieren. Derartige Fehlermeldungen stellen insgesamtden gröÿten Nachteil der Bibliothek dar.Auf Grund der zwischengeschalteten Code-Generierung ist auch eine Einzelschritt-Auswertungzur Fehlersuche nicht möglich. Selbst wenn dies im generierten Quelltext möglich wäre, so könnteder Entwickler mit den kryptischen Variablen nichts anfangen.

Die Code-Generierung ist für eine einzelne umzuwandelnde Klasse im Vergleich zu Rootbeerschneller. Müssen mehrere Klassen transformiert werden, so muss der Staging-Vorgang jeweilseinzeln ausgeführt werden und dauert insgesamt genauso lang. Gleichzeitig wird unter Nutzungder Standard-Kon�guration der bereits vorher transformierte Code überschrieben. Im Normalfallwird in einer Applikation nur eine Main-Methode, d.h. nur ein Runner, transformiert, wodurchdies nur für die Evaluierung eine Einschränkung darstellt.

Page 69: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

5.3. Evaluation verfügbarer Abstraktionsbibliotheken 62

error: illegal sharing of mutable objects Sym (509)

at Sym (510)= Reflect(NewVar(Sym (509)) ,

Summary(false ,false ,false ,false ,true ,List(

Sym (509)) , List(Sym (509)) , List(),List()),List(Sym (509)))

-----

/x4422x4446x4460x4505.scala :77: error: not found: value x4440

val x4504 = x4440 * x4440

Quelltext 5.5: Zwei Beispiele für Fehlermeldungen in Delite

Der Entwickler muss sich bei der Programmierung weder um die Spezi�kation von Kernels nochum Speicherverwaltung in irgendeiner Art kümmern. Basic-Blocks werden automatisch erkanntund im Stil von java-gpu automatisch parallelisiert. Im Gegensatz zu dem erwähnten Frameworkbietet Delite allerdings die Möglichkeit selbst Optimierungen einzubauen und damit im Stil derSprach-Virtualisierung optimalen Domänen-Code zu produzieren.Die Speicherverwaltung geht in Delite sogar soweit, dass mehrfach ausgeführte Kernels direkt aufder GPU ausgeführt werden und kein Kopiervorgang dazwischen mehr erfolgt. Die Bibliothekprüft für jeden Aufruf von Kernels explizit ob ein Kopiervorgang notwendig ist und kopiert nurin diesem Fall benötigte Daten.

Speicherstrukturen wie Shared- oder Constant-Memory können nur über bereits vorimplemen-tierte BLAS-Operationen, die jeweils eine optimierte GPU-Variante aufweisen, oder durch selbstde�nierte DSL-Methoden inkl. der Spezi�kation des Kernel-Inhaltes, verwendet werden. Im We-sentlichen sind die Speicherbereiche also vor dem Entwickler versteckt, was die Komplexität desDomänen-Quelltextes deutlich reduziert.

Als Exception wurde für Delite eine auf der GPU geworfene ArrayIndexOutOfBounds-Exceptionprovoziert. Der Fehler wird transparent an den Nutzer auf der CPU weitergereicht und als Ursachedie erwartete Kernel-Datei angegeben. Fehlersuche stellt allerdings im Allgemeinen ein Problembei Delite dar. Die Implementierung ist sehr komplex und springt v.a. durch implizite Metho-denaufrufe durch die Quellcode-Dateien. Der Staging-Schritt erleichtert dabei die Fehlersucheauch nicht unbedingt, wobei zumindest im Gegensatz zu Rootbeer keine externe Rootbeer.jarzum Staging notwendig ist, sondern der Runner direkt ausgeführt werden kann. Damit kann dasStaging selbst in der bereits eingerichteten IDE verfolgt und gedebugged werden. Allerdings istdazu Voraussetzung, dass die IDE sämtlichen Delite-Quellcode zur Verfügung gestellt bekommt.

Während der Evaluierung wurde die für gute Scala-Unterstützung bekannte IDE IntelliJ IDEAverwendet. Trotz dieser guten Unterstützung stürzt diese regelmäÿig ab. Dies liegt wohl u.a. ander Gröÿe der Bibliothek sowie an der angepassten Version des Scala-Compilers und der Scala-Bibliothek. Bereits beim Schreiben des Quellcodes werden Kompilierfehler angezeigt, die in derverwendeten Compiler-Version allerdings gar keinen Fehlern entsprechen. Diese Konstrukte wer-den nur eben nicht vom Standard Scala-Compiler sowie, insbesondere, von der IDE unterstützt.

Als Ausführungsmodi stehen sehr unterschiedliche Möglichkeiten zur Verfügung. Der generierteCode kann als CUDA, OpenCL, pures Scala und C++ ausgeführt werden, wobei OpenCL in deraktuellen Version nicht unterstützt wird und CUDA nur auf dem Entwickler-Branch funktioniert.

Page 70: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

5.3. Evaluation verfügbarer Abstraktionsbibliotheken 63

Für die CUDA- und Scala-Ausführung kann jeweils spezi�ziert werden wie viele Gra�kkerneoder Threads für die Ausführung verwendet werden. Eine Kombination von CUDA- und Scala-Ausführung ist nur beschränkt möglich. Sobald mehr als ein Scala Thread in Verbindung mit derCUDA-Ausführung verwendet wird, schlägt die Kompilierung des generierten Quelltextes fehl.Ein erneutes Staging ist für eine geänderte Ausführungsumgebung nicht notwendig. Beim Stagingwird der Quellcode für alle angegebenen Ziel-Sprachen generiert. Dieser kann entsprechend auchohne weitere Transformation bei der Ausführung verwendet werden.

Als komplette Ausführungsumgebung kümmert sich Delite auch um die Zeitmessung der gene-rierten Programme. Diese erfolgt automatisch beim Programmstart und endet bei Beendigungdes Programms. Einzelmessungen können ebenfalls über eine integrierte API durchgeführt wer-den. Allerdings wird die gemessene Zeit jeweils auf die Konsole geschrieben. Für normale Ent-wickler ist dies ausreichend. Für eine Evaluation stellt dies allerdings auf Grund der nötigenAnschlussauswertung eine Einschränkung dar. Deswegen wurde dafür eine eigene auf der inter-nen API basierende DSL entwickelt, die die Ergebnisse als Double zurück gibt. Dieser Wert kannanschlieÿend in eine Datei geschrieben werden, die auch zur Auswertung verwendet werden kann.

Entsprechend der eingebetteten Umgebung fallen Tests sehr schwer. Im Gegensatz zu Aparapikönnen in Tests keine Kernels einzeln gestartet und deren Ergebnis betrachtet und validiert wer-den. Stattdessen ist die integrierte Ausführungsumgebung zu nutzen. Dabei ist es ebenfalls nichtmöglich, normalen, nicht für Delite geschriebenen, Code, in einem Delite-Programm einzubinden.Die Nutzung einer Unit-Testing-Bibliothek ist damit ausgeschlossen.

Als Plattformen werden sowohl die geforderte Windows-Umgebung als auch die optionale Linux-Umgebung o�ziell unterstützt. Funktionsfähig ist allerdings im Augenblick nur die Ausführungauf UNIX-basierten Systemen, da im Fall von Windows eine Option von GCC vorausgesetztwird, die für den Windows Visual Studio Compiler nicht existiert. Während der Evaluierungwurde versucht diese Option zu entfernen. Als Ergebnis traten diverse weitere Fehler auf.

Im Allgemeinen begrenzt die integrierte Ausführungsumgebung auch die Integration in eigeneProjekte. Es muss schlicht die gesamte Anwendung parallelisiert und mit Delite ausgeführt wer-den. Dazu muss eigentlich auch die mitgelieferte Umgebung verwendet werden. Ein Verzeichnisim Delite Ordner enthält den Quellcode von LMS. Ein zweites Verzeichnis enthält die Dateienfür Delite, d.h. dessen Ausführungsumgebung (runtime), das Framework selbst, die DSLs sowievorhandene Apps. Eine eigene Anwendung sollte sich als Applikation eingliedern und dementsp-rechend auch im dazu passenden Ordner geschrieben werden. Entsprechend wird dann auch derClassPath zum Au�nden der Einzelklassen richtig generiert. Das project-Verzeichnis enthältdabei das eigentliche Scala Build Tool (SBT)-Build-Skript, das für den Aufbau des ClassPathund das Laden der Abhängigkeiten, z.B. auf Scala-Virtualized, zuständig ist. Diese Strukturentspricht nicht dem, was der Autor sich als Bibliotheksintegration erwartet.

Zum Aufbau einer Delite-Anwendung im typischen Maven- oder Gradle- Stil, sind einige wei-tere Zwischenschritte nötig. Ein src-Verzeichnis soll den Quellcode der Applikation enthalten.Notwendige Bibliotheken werden entweder vom Build-Skript nachgezogen oder in einem lib-Verzeichnis abgelegt. Dazu müssen die Bibliotheken allerdings als jar vorliegen. Diese lassen sichmit dem SBT-Build-Skript von Delite generieren. Entsprechend kann man für LMS vorgehen.Durch Analyse des Delite-Build-Skriptes kann der ClassPath analog zum SBT-Skript in Gra-dle mittels der Jar-Abhängigkeiten rekonstruiert werden. Die Transformation erfolgt dann nicht

Page 71: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

5.3. Evaluation verfügbarer Abstraktionsbibliotheken 64

mehr mit dem mitgelieferten delitec-Skript, sondern in der IDE durch Ausführung des Runnersbzw. durch Ausführung des Build-Skripts.Vorsicht ist während der Kompilierung der Delite-Artefakte immer in Bezug auf zur Verfügungstehenden Speicher geboten. Unter Umständen muss dieser für SBT zur erfolgreichen Kom-pilierung wesentlich erhöht werden. Die Schritte der Artefakt-Generierung müssen für jede Ak-tualisierung der Delite-Bibliothek jeweils wiederholt werden.Eine richtige Bibliotheksintegration ist nach Aussagen der Entwickler ebenfalls für eine der nächs-ten Versionen geplant.

Insgesamt stellt Delite eine sehr professionelle und ständig weiter entwickelte Bibliothek mit ak-tiver Community dar, deren Mitglieder auch gerne bei Problemen helfen. Die Community selbstscheint momentan noch verhältnismäÿig klein zu sein, was auf die Verbreitung der Bibliothek selb-st schlieÿen lässt. Der Einsatz der Bibliothek ist auf Grund mangelnder Tool-Unterstützung undteilweise auf Grund der verschiedenen impliziten Methodenaufrufe für Scala typischen verwirren-den Compiler- und Staging-Fehlermeldungen nicht einfach. Bis zu einem möglichen produktivenEinsatz dürfte entsprechend noch eine längere Zeit vergehen.

5.3.6 Projekt Sumatra

Als Abschluss der Beschreibung der GPU-Abstraktionsbibliotheken für Java soll noch eine letzteBibliothek angesprochen werden. Diese bezieht sich auf die Bemühungen der Sprache selbst, eineGPU-Abstraktion aufzunehmen und, z.B. durch die Verwendung von Java 8 Lambda-Funktionen,den Entwicklern zur Verfügung zu stellen. Das zugehörige Projekt hört auf den Namen Sumatra

[Sum13a].

Die nachfolgende Beschreibung bezieht sich hauptsächlich auf vereinzelte Präsentationen und v.a.Mailing-Listen der Projekte. Bisher existieren hauptsächlich Ideen und vereinzelte prototypischeTeilimplementierungen. Wissenschaftliche Verö�entlichungen sind (noch) nicht zu �nden. Dieo�zielle Integration wurde Ende September für Java 9 angekündigt.

Das Projekt teilt sich momentan in zwei Bestrebungen auf, wovon Erstere bereits gröÿtenteilsabgeschlossen ist:

Es wird ein Prototyp basierend auf dem bereits beschriebenen Framework Aparapi implemen-tiert, der unter Nutzung von Lambda-Funktionen eine parallele Variante des Methodeninhaltsgeneriert und diesen auf einer OpenCL kompatiblen GPU ausführt. Ein von den Entwicklernvorgestelltes Beispiel ist in Abbildung 5.10 zu �nden. Die nötige Parallelisierung �ndet dabei ineiner angepassten Variante der ForEachOps statt. Hier wird auch der Aufruf auf eine angepassteAparapi-Variante gestartet und der OpenCL-Quellcode damit ausgeführt [Sum13b][2013-April].

Der zweite Ansatz, der teilweise parallel bearbeitet und auch als Fortsetzung zum Aparapi-

Streams.intRange(0, in.length ). parallel (). forEach( id -> {

c[id] = a[id] + b[id];

});

Abbildung 5.10: Addition zweier Arrays mit unter Nutzung des Sumatra-Aparapi Prototyps

Page 72: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

5.3. Evaluation verfügbarer Abstraktionsbibliotheken 65

Prototyp angesehen wird, ist die Integration von Graal zur Kompilierung und Erstellung desGPU-Codes durch Erstellung eines PTX- bzw. Heterogeneous System Architecture IntermediateLanguage (HSAIL)-Backends. Sowohl PTX (siehe 3.2.3) als auch HSAIL sind Zwischensprachenfür parallele Architekturen. Der Ansatz von HSAIL ist allerdings noch wesentlich tiefgreifenderals der von PTX, da diese die Parallelisierung auf unterschiedlichen Gerätetypen in der Zwis-chensprache bereits abbildet [Kyr12][S. 3]. Zum Beispiel unterstützt HSAIL Instruktionen zumLaden von Kernel-Argumenten sowie zum Setzen von Ids für abzuarbeitende Kernel-Instanzen[hsa13]. PTX-Zwischencode bezieht sich nur auf den CUDA-Code, der auch auf Gra�kkartenausgeführt wird. Zur Ausführung von Java-Code mit dem entsprechenden Zwischencode wirdzuerst der Anfangscode durch einen High Level Compiler (HLC) in das HSAIL-Zwischenformatkompiliert. Dieses wird auf der Ziel-Maschine von einem Finalizer in die richtige Instruction SetArchitecture (ISA) konvertiert und ausgeführt [Kyr12][S. 5].

Graal ist eine Erweiterung des momentan in der Java Virtual Machine (JVM) eingesetztenHotSpot-Compilers. Das Ziel des Projektes ist es den Prozess der Kompilierung und deren Kon-�gurationsoptionen transparent für den Anwendungsentwickler zu machen. Beispielsweise ist esmöglich durch Callbacks während der Kompilierung Nodes in der internen Graph-Repräsentationdes Quellcodes (Sea-of-Nodes-Darstellung) Nodes auszutauschen und damit zu optimieren [Gra13a][Gra13b]. Dies erinnert stark an das Konzept von Delite zur Optimierung der internen Darstel-lung durch Callbacks auf den Domänen-Code.

Gleichzeitig bindet Graal den eigentlichen Compiler über ein Compiler Runtime Interface (CRI)an. Dadurch werden jetzt schon sowohl der HotSpot-Compiler als auch der neue, komplett in Javageschriebene, Maxine-Compiler unterstützt. Durch Implementierung eines eigenen Backends fürdie erwähnten IR-Darstellungen kann während dem Durchlaufen der ForEachOps entsprechenderGPU-Code erzeugt werden. Durch die reine Verwendung des HotSpot-Compilers ist dies nichtmöglich, da dieser auf die Erzeugung von Bytecode festgelegt ist und gleichzeitig nicht aus demQuellcode heraus beein�ussbar ist.

Das gesamte Projekt be�ndet sich aktuell in einem sehr frühen Stadium. Der Aparapi Prototypliegt dabei inkl. der nötigen Generierung von HSAIL in lau�ähiger Form im Aparapi Repositoryvor. Genau dieser bildet auch die Grundlage für die momentane Weiterentwicklung von Aparapi.Das Compiler-Backend für Graal ist im Graal-Repository zu �nden. Allerdings fehlt noch dieAnbindung an die JVM selbst. Insgesamt müssen alle Prototypen nacheinander integriert werden,bis ein Gesamt-Prototyp zur Verfügung steht.

Das Projekt besitzt in jedem Fall groÿes Potential und ist damit in Zusammenhang mit Graalauch in Zukunft relevant. Die Entwickler setzen mit ihren Ansätzen auf die bestehenden, teilweiseauch bereits hier beschriebenen, Bibliotheken und versuchen deren Limitierungen, z.B. eventuellvorhandene über�üssige Kopieroperationen, zu eliminieren und zu verbessern [Sum13c]. Im mo-mentanen Zustand repräsentiert der Ansatz jedoch noch keine verwendbare Alternative zu denbestehenden Bibliotheken.

Page 73: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

5.4. Evaluation der Ausführungsgeschwindigkeiten 66

5.4 Evaluation der Ausführungsgeschwindigkeiten

Abschlieÿend zur Evaluation der Abstraktionsbibliotheken folgt ein Vergleich der Ausführungs-geschwindigkeiten anhand den in Abschnitt 5.2 vorgestellten Algorithmen.

Dazu wird zuerst näher auf Randbedingungen der Geschwindigkeitsevaluation eingegangen. An-schlieÿend folgt eine kurze Beschreibung des Aufbaus des Projektes sowie abschlieÿend dieVorstellung der erzielten Ergebnisse der Messungen.

5.4.1 Randbedingungen der Messung

Evaluationsrechner Die Evaluation �ndet auf einem Ubuntu-Linux-Rechner statt. Dieser ent-hält neben einem AMD Opteron Prozessor mit 16 Kernen 16 GB Arbeitsspeicher und eineGeForce GTX660Ti Gra�kkarte mit 2 GB Speicher und 1344 Streaming-Prozessoren, die auf7 Multiprozessoren verteilt sind. Damit keine Beein�ussung der Laufzeiten auftritt wurde derRechner explizit für die Berechnung reserviert. Zum Starten bzw. Überprüfen des Fortschrittswurde zur geringen Beein�ussung ein SSH-Terminal verwendet.

Der Grund für die Verwendung eines Linux Betriebssystems liegt in der teilweise fehlendenWindows-Unterstützung der Bibliotheken. Da die Geschwindigkeiten der Bibliotheken, die Win-dows nicht unterstützen, trotzdem ein�ieÿen sollen, bleibt nur die Verwendung eines Unix-Betriebssystems. Der im weiteren Verlauf der Arbeit erstellte Vergleich der Ausführungszeitenzwischen dem nativen Simulator und einer ausgewählten Bibliothek erfolgt abschlieÿend unterWindows.

Zeitmessung Die Zeitmessung erfolgt jeweils ohne Staging-Zeiten, d.h. ohne Messung derUmwandlung von Java in GPU-Code. Dies ist v.a. der Fairness geschuldet, da jede Bibliothekihren auszuführende Quelltext anders generiert und kompiliert. Aparapi generiert sämtlichenCode zur Laufzeit, Rootbeer und Delite bereits zur Staging-Zeit. Im Fall von Delite wird auchdas Runtime-Scheduling beim Hochfahren des Delite-Kontextes nicht mit einbezogen. Für Root-beer wird die Vorbereitung der Ausführungsumgebung jeweils mit eingerechnet, da diese beijedem Kernel-Start erfolgt und nicht gekapselt werden kann.

Im GPU-Fall ist die volle Ausführungszeit mit einbezogen, d.h. inklusive aller Kopiervorgängevon und zur GPU. Diese sind für einen fairen Vergleich zwischen Java- und GPU-Ausführungzwingend mit einzubeziehen, da diese als Overhead bei jeder Auslagerung rechenintensiver Ope-rationen an die GPU anfallen.

Zur Berechnung der Geschwindigkeitssteigerung im Vergleich zu Referenzbibliotheken, wie dieserielle oder die native Ausführung, wurde möglichst ein über die Ausführungsparameter berech-netes arithmetisches Mittel verwendet. Eine Überschreitung der maximalen Ausführungszeit�ieÿt entsprechend mit dem Timeout von 180 Sekunden in die Berechnung ein. Fällt eine Bib-liothek auf die serielle Ausführung zurück, so wird, soweit die Berechnung noch innerhalb desZeit-Limits abgeschlossen werden kann, die resultierende Gesamtzeit zur Berechnung des arith-metischen Mittels verwendet.

Page 74: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

5.4. Evaluation der Ausführungsgeschwindigkeiten 67

Häu�gkeit der Ausführung sowie Timeout Die auszuführende Aufgabe wird jeweils ein-mal ohne Zeitmessung ausgeführt. Dies führt dazu, dass in jedem Fall, z.B. auch für Aparapi,der Code kompiliert ist. Auch der Hotspot-Compiler der JVM hat damit die Chance zur Kom-pilierung bzw. Optimierung häu�g durchlaufener Code-Abschnitte.

Nach dem Probedurchlauf wird der Test jeweils 50 Mal je Bibliothek durchgeführt und die Aus-führungszeiten aufgenommen. In den resultierenden Graphen ist jeweils das arithmetische Mitteldieser Ausführungszeiten eingezeichnet. Liefert die Bibliothek auf die auszuführende Operationnach 120 Sekunden pro Durchlauf keine Antwort, so wird die Ausführung abgebrochen und derTimeout, also die 180 Sekunden, als Ausführungszeit angenommen. Dies ist nicht vollständigkorrekt, da in den 180 Sekunden jeweils auch der Aufbau der Evaluationsumgebung inkl. derInitialisierung der Testdaten statt�nden muss und eben nicht nur die Ausführung des Testfallsinkl. Zeitmessung. Die Evaluationsfälle sind jedoch so gewählt, dass der Timeout für eine nor-male Berechnung ausreicht. Tritt eine Überschreitung der Ausführungszeit auf, so dauert dieBerechnung im Normalfall wesentlich länger.

5.4.2 Aufbau des Evaluations-Projektes

Architektur und Start der Evaluierungsschritte Das Projekt ist in mehrere, in Abbil-dung 5.11 dargestellte, Gradle-Module aufgespalten. Die Evaluation jeder Bibliothek ist dabei ineinem Modul enthalten, in dem alle Abhängigkeiten gekapselt sind. Zusätzlich stellt ein Modul(�Eval�) die Infrastruktur der Auswertung sowie den Quelltext zur Auswertung der Resultate zurVerfügung. Dies beinhaltet die Logik zum Starten der Testfälle, deren Parametergröÿen sowie dasSchreiben der Ergebnisse in CSV-Dateien. Schlussendlich enthält das Modul auch Funktionenum die CSV-Datei wieder einzulesen und die resultierenden Gra�ken bzw. weitere Statistiken zugenerieren. Schlieÿlich kapselt ein Modul alle Code-Bestandteile, die von allen anderen Modulenbenötigt werden (�Util�). Darin enthalten ist auf der einen Seite ein Interface, das für jedenTestfall eine Methode enthält. Als Rückgabe wird ein Evaluationsobjekt erwartet, das für ver-schiedene Parametergröÿen ausgeführt werden kann und entsprechende Test-Resultate zurückgibt. Jede evaluierte Bibliothek muss dieses Interface als zentrale Komponente implementieren.Damit kann die Evaluation vollständig automatisch durchlaufen werden, da eine Evaluation-sklasse die entsprechenden Objekte aufsammeln und für die verschiedenen Testfälle ausführenkann.Abgesehen davon enthält das Modul nötige Methoden zur Initialisierung der Testdaten bzw. zurVeri�kation resultierender Ergebnisse. Bis auf Delite, das auf Grund der Struktur keine externenJava-Methoden aufrufen kann, nutzen alle Bibliotheken diese Initialisierungsmethoden.

Die native Evaluation ist dagegen vollkommen unabhängig vom globalen Evaluationsprojekt. Einzusätzlicher JNI-Layer zur automatischen Ausführung wäre hier nur zusätzlicher Overhead ohneweiteren Nutzen gewesen.

Jeder Evaluationsschritt wird auf eine externe JVM abgebildet. Dies hat mehrere Vorteile:

� Jeder Schritt �ndet die gleiche Umgebung mit den gleichen Speicherbedingungen vor.

� Tritt ein Speicher-Leak in einer Bibliothek auf, so wird meist der GPU-Speicher nachBeendigung der JVM wieder freigegeben. Damit tritt der Fall nicht ein, dass wegen nicht

Page 75: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

5.4. Evaluation der Ausführungsgeschwindigkeiten 68

Rootbeer-GPU.jar

Rootbeer-GPU-Kernels

Inhalt von Rootbeer-GPU-Kernels als jargepackt und von Rootbeer transformiertund parallelisiert

Eval

Rootbeer-Eval Aparapi-Eval JCuda-Eval Delite-Eval

Cuda-Nativ-Eval

Util

Abbildung 5.11: Module der Evaluation

freigegebenem Speicher alle anderen Evaluationen keinen Speicher mehr vor�nden.

� Nach einer bestimmten Zeitspanne kann der Prozess abgebrochen werden.

Die Kommunikation der virtuellen Maschinen �ndet dabei über das Dateisystem statt. Die ge-starteten JVMs speichern ihre Ergebnisse in Dateien, die generisch von der Haupt-JVM wiedergelesen und verarbeitet werden können.

Einbindung der Bibliotheken JCuda und Aparapi können durch Einbindung der jar-Archivesowie durch Platzierung nötiger dynamischer Bibliotheken (.dll, .so) verwendet werden. FürRootbeer und Delite ist dagegen noch ein zusätzlicher Staging-Schritt notwendig, der sich auch imBuild-Skript wieder�nden muss. Für beide Bibliotheken wurde ein eigener Gradle-Task erstellt,der für die Umwandlung bzw. Generierung des notwendigen Quelltextes zuständig ist.

Im Fall von Rootbeer werden die Kernels in einem zusätzlichen Modul spezi�ziert (siehe �Rootbeer-GPU-Kernels� in Abbildung 5.11). Der Inhalt dieses Moduls wird unter Verwendung der Rootbeer-Bibliothek transformiert und als Abhängigkeit zu Rootbeer-Eval eingehängt. Theoretisch könn-ten die Kernels auch direkt in Rootbeer-Eval spezi�ziert werden. Auf Grund der Entfernung vonnicht verwendeten Methoden seitens Rootbeer sollte jedoch die Menge an transformiertem Codeso gering wie möglich gehalten werden. Gleichzeitig erhöht dies die Entwicklungsgeschwindigkeit,da damit weniger Quelltext eingelesen und transformiert werden muss.Bei der Einbindung ist strikt darauf zu achten, dass nur der Classpath der transformierten jar-Datei zur Ausführung verwendet wird. In Abbildung 5.11 referenziert deswegen Rootbeer-Evalauch direkt das transformierte jar-Archiv. Wird trotzdem das ursprüngliche Rootbeer-GPU-Kernels Modul im Classpath referenziert, so ist das Ergebnis abhängig von der Reihenfolge derEinbindung der Class-Dateien. Wird z.B. zuerst die nicht-transformierte Variante geladen, so er-folgt die Ausführung seriell, da nur der erste Vorgang des Ladens einer Klasse berücksichtigt wirdund alle weiteren Anfragen zum Laden einer Klasse mit dem selben voll-quali�zierten Namen ver-worfen werden [GJGB13][S. 316]. Zu erkennen ist dieses Verhalten daran, dass die resultierendenAusführungszeiten der GPU und der JEMU-Ausführung identisch sind.

Page 76: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

5.4. Evaluation der Ausführungsgeschwindigkeiten 69

Wie bereits in der Evaluation von Delite beschrieben, wurde die Bibliothek nicht nach demStandardverfahren von Delite durch Verwendung der delite- und delitec-Skripte sowie durchAblage der Quelltexte innerhalb der Bibliothek eingebunden, sondern über den Standard Maven-bzw. Gradle-Weg. Da entsprechend die mitgelieferten Skripte zur Transformation nicht mehrfunktionsfähig sind, wird durch Starten der Delite-Umwandlungsklasse die Transformation di-rekt gestartet. Dazu existiert für jeden Evaluationsalgorithmus ein eigener Gradle-Task, der dieKlasse aufruft und die Ergebnisse in ein eigenes Ziel-Verzeichnis umleitet. Dessen Name basiertauf dem Namen der umzuwandelnden Klasse. Ohne das Setzen eines individuellen Zielpfadeswürde jeder Start der Transformation das vorhergehende Ergebnis überschreiben.Zum eigentlichen Start eines Delite-Tasks wird die interne #main-Methode der Delite-Runtimeverwendet. Verschiedene Parameter, wie z.B. der Name und Ort des DEG, die Verwendung vonCUDA oder die Anzahl an zu verwendenden Threads, werden vorher in der Delite-Con�g bzw.u.U. in Umgebungsvariablen gesetzt und von Delite anschlieÿend zur Ausführung einbezogen. DieInitialisierung der Umgebungsvariablen muss dabei vor dem ersten Zugri� auf die Delite-Con�gerfolgen, da die entsprechende Kon�gurationsklasse bei Initialisierung die Werte aus den Umge-bungsvariablen liest und die Optionen setzt. Die Initialisierung erfolgt dabei vom Classloader beider ersten Referenzierung der Klasse.Da eine #main-Methode keine Ergebnisse zurück liefern kann, erfolgt die nötige Rückgabe überdas Dateisystem. Das Schreiben von Dateien mit benutzerde�niertem Inhalt wird von Delitenicht unterstützt. Hierzu wurde eine eigene DSL-Methode implementiert.

Messung der Ausführungszeiten Um die Zeitmessung zu vereinfachen und gleichartig zugestalten wird jeweils vor Start einer Kernel-Ausführung mit System#nanoTime die Zeit bestimmtund die Di�erenz zum Ende der Ausführung gespeichert. Auf die Verwendung von internen APIszur Zeitmessung wird dabei verzichtet, da nicht jede Bibliothek die gleichen Funktionalitäten zurZeitmessung aufweist.

Vorsicht ist bei der Zeitmessung mit Delite geboten. Die Bibliothek wendet eine sogenanntecommon subexpression elimination an [Suj][S. 23]. Diese bildet mehrfache ausgeführte Anweisun-gen, die keine weiteren Seitene�ekte haben, auf die erste Ausführung ab. Wird also z.B. einEvaluations-Task 50 Mal ausgeführt, so liefert nur die erste Ausführung die richtige Laufzeit.Alle anderen Durchläufe liefern ein Resultat nahe 0, da keine weitere Ausführung erfolgt. Umdies auszuschlieÿen ruft jeder Evaluationsdurchlauf die Delite-Runtime mit dem DEG erneut auf.Delite besitzt damit keine interne Schleife und muss die Funktion richtig ausführen.

Abgesehen davon legt die Bibliothek starkenWert auf den Abhängigkeitsgraphen. Nicht voneinan-der abhängige Blöcke können umgeordnet oder z.B. parallel auf unterschiedlichen Prozessorenausgeführt werden. Dieses Verhalten stellt ein Problem dar, da zwischen Start, Durchführungund Stop der Evaluation keine Abhängigkeit besteht. Dies führt dazu, dass Delite die Startzeitberechnet, direkt anschlieÿend die Evaluation stoppt und erst dann die Berechnung startet. Ent-sprechend ergeben sich bei der Messung Zeiten nahe 0.Um dies zu verhindern �ndet sich die eigentliche Evaluation in einer eigenen Funktion, die dieStart-Zeit übergeben bekommt. Als Rückgabewert der Funktion wird die Startzeit, die, in Ab-hängigkeit des in der Evaluation berechneten Ergebnisses, verändert wurde, verwendet. Die Ver-änderung kann dabei z.B. bei einem Array die Addition und, direkt nachfolgend, die Subtraktiondes ersten Array-Elements sein. Die entstehende Abhängigkeit verhindert damit die Vertauschung

Page 77: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

5.4. Evaluation der Ausführungsgeschwindigkeiten 70

der Berechnung der Startzeit und der eigentlichen Evaluation. Um noch eine Abhängigkeit zwi-schen Berechnung der Stoppzeit und der Evaluation zu scha�en, wurde die DSL-Methode zurBestimmung der aktuellen Zeit so de�niert, dass diese einen zusätzlichen Dummy-Parameterentgegen nimmt. Delite sieht diesen Parameter als Abhängigkeit an. Hier kann das Ergebnis derEvaluationsfunktion verwendet werden, um die benötigte Abhängigkeit zwischen Berechnungund Zeitmessung zu scha�en.

Die native Evaluation verwendet zur Messung der Geschwindigkeit die clock_gettime-Funktion[clo13]. Damit ist es u.a. möglich die aktuelle Zeit in Nanosekunden zu bestimmen.Ein Hindernis stellt sich bei der Initialisierung der Testdaten. Der verwendete Compiler verwen-det zur Allokation des Speichers eine optimistische Strategie [mal13]. Der zurückgegebene Spei-cher wird beim ersten Aufruf initialisiert und nicht sofort beim Aufruf der #malloc-Funktion.Die Allokation ist jedoch verhältnismäÿig zeitaufwändig und verfälscht, soweit sie während derZeitmessung erfolgt, die Ergebnisse deutlich.

Als Abhilfe kann entweder der Speicher alloziert und direkt anschlieÿend, z.B. per Schleife, ini-tialisiert oder, alternativ, via #cudaMallocHost alloziert werden. Die zweite Lösung verwendeteinen besonderen Speichertyp, sog. page locked memory. Dieser garantiert, dass die Seiten ausdem virtuellen Speicher nicht auf die Festplatte ausgelagert werden. Gleichzeitig wird der Spei-cher sofort alloziert. Für CUDA hat dies den zusätzlichen Vorteil, dass der Speicher über diedirekte Referenzierung des physikalischen Speichers ohne Umwege transferiert werden kann, waseinen zusätzlichen Durchsatz beim Kopiervorgang von rund 20% bedeutet.

Die zweite Speicherart wird in der nativen Simulation folgerichtig eingesetzt. Dies trägt u.a. demmöglichen Optimierungspotential innerhalb von CUDA Rechnung und setzt dabei die gleicheOptimierung ein, die auch von Aparapi angewendet wird. Andernfalls wäre Aparapi in Bezugauf Kopieroperationen deutlich im Vorteil.

5.4.3 Ergebnisse der Zeitmessung

Die bereits beschriebenen Algorithmen werden 50 Mal ausgeführt und deren Mittelwert als Ergeb-nis in den Graphen aufgenommen. Die Zahl 50 erweist sich als verhältnismäÿig zuverlässig. Esergibt sich dabei eine Abweichung von im Schnitt 3% um den Mittelwert. Die Scala-Ausführungvon Delite schwankt dabei am stärksten. In Ausnahmefällen steigt die Abweichung auf bis zu30%.

Array-Inkrementierung Die sehr einfache Operation weist ein sehr kleines Verhältnis zwi-schen Berechnung und Speicherzugri�en auf. Im Wesentlichen erfolgt nur eine Addition in Ver-bindung mit zwei sehr teuren Speicherzugri�en auf den globalen Speicher. Zusätzlich ist dieZeit der Kernel-Ausführung im Verhältnis der zum Kopieren notwendigen Zeit sehr niedrig. Derentstehende Overhead lässt daher die Erwartungshaltung entstehen, dass die serielle Ausführungschneller als die GPU-Ausführung ist. Selbst im Vergleich der seriellen Abarbeitung im Gegen-satz zu Java Thread-Pools sollte die serielle Ausführung schneller sein, da trotz der sehr parallelausführbaren Aufgabe der Overhead zur Erstellung und Verwaltung der Threads das möglicheEinsparpotential durch eine Verminderung der ohnehin sehr geringen Rechenzeit übersteigt.

Page 78: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

5.4. Evaluation der Ausführungsgeschwindigkeiten 71

Das Ergebnis der Zeitmessung ist in Abbildung 5.12 dargestellt. Gemessen wurden die Inkre-mentierung von Arrays mit einer Anzahl von 1 bis 2.000.000 Elementen.

Wie zu erwarten ergibt sich für alle Bibliotheken, dass deren Ausführung wesentlich langsamerals die serielle Ausführung ist. Die serielle Kurve �ndet sich erst im rechten Diagramm wieder,in der neben der seriellen Zeit nur GPU-Ausführungszeiten eingezeichnet sind (ohne Rootbeerund Delite). Würde man den Graph vergröÿern und als Parametergröÿen bis knapp 500.000betrachten, so ergäbe sich ein interpolierter Schnittpunkt zwischen der seriellen Ausführung undden GPU-Bibliotheken, insbesondere der nativen CUDA-Ausführung sowie JCuda, bei zwischen100.000 und 250.000. Bis dahin überwiegt der Geschwindigkeitsvorteil der seriellen Ausführung.

Au�ällig ist v.a. der hohe Unterschied zwischen den Einzelbibliotheken. In der Übersicht imunteren Graphen sind alle Bibliotheken eingezeichnet. Darin scheinen JCuda und die nativeEvaluation eine Ausführungszeit von nahe 0 zu besitzen. Der krasse Gegensatz dazu ist dieCUDA-Ausführung von Delite, die bereits bei einer Startzeit von mehr als 1.000 ms beginnt.

Dabei ist die Zeit der reinen CUDA-Ausführung in Delite minimal. Diese rangiert im Bereichzweistelliger µs. Auf die Steuerung der Ausführung, bzw. die Ausführung auf der CPU, ent-fällt dagegen ein dreistelliger Millisekunden-Betrag. Dies ist ein deutlicher Hinweis, warum dieAusführung so langsam ist. Vergleicht man die CUDA-Zeiten mit denen anderer Bibliotheken,so ergibt sich, dass diese eigentlich im zweistelligen Mikrosekundenbereich liegen sollten. Delitenutzt also nicht die Kapazität von CUDA aus, sondern berechnen den Hauptteil der Testfälle aufder CPU. Da gleichzeitig die Verwendung mehrerer Java-Threads in Verbindung mit CUDA nichtfunktionsfähig ist, wird der Groÿteil der Berechnung nur seriell ausgeführt. Dieses Verhalten lässtsich auch in den weiteren Testfällen wieder�nden.

Der Graph der GPU-Ausführung endet für Aparapi bei einer Parametergröÿe von rund 16.000.Ab diesem Wert fällt die Ausführung von der GPU- auf die JTP-Ausführung zurück. Grunddafür ist, dass die Anzahl an Threads im Verhältnis zu der Anzahl an Gruppen bzw. der Grup-pengröÿe in Aparapi falsch gesetzt wird. Die Gesamtanzahl an Threads muss ein Vielfaches derGruppengröÿen sein. Dies wurde nicht vollständig berücksichtigt, weswegen die Ausführung hiermit einem OpenCL-Fehler abbricht. Im weiteren Verlauf der Arbeit wurde der Fehler zwar be-hoben. Da in den anderen Bibliotheken jedoch ebenfalls keine Fehler beseitigt wurden, wurde diereparierte Version nicht zur Evaluation verwendet.

JCuda und die native Ausführung scheinen quasi gleichauf. Vergleicht man deren Ausführungszeit-en in einem Pro�ler, der angibt, an welchen Stellen wie viel Zeit verbraucht wird, so ergibtsich, dass die Kernel-Ausführungen der beiden Bibliotheken exakt die gleiche Zeitspanne benöti-gen. Der Unterschied zwischen den beiden Kurven ergibt sich durch den JNI-Overhead sowiedurch den, bei der nativen Evaluation verwendeten, schnelleren Speicher. Insgesamt benötigtdie native Ausführung trotz der gleichen Kernel-Ausführungszeiten nur rund 60% der JCuda-Ausführungszeit.

Rootbeer als Bibliothek weist dagegen eine sehr lange Ausführungszeit auf. Selbst die GPU-Aus-führung scha�t es nicht in die rechte Kurve der GPU-Ausführungszeiten und ist damit nicht mitden anderen Bibliotheken zu vergleichen. Als Ursache für diese hohen Ausführungszeiten lassensich zwei Gründe �nden: Auf der einen Seite de�niert Rootbeer eine maximale Anzahl an Threads,die gleichzeitig auf der Gra�kkarte ausgeführt werden können. Dieser Wert ist fest auf 16.384einprogrammiert und passt sich nicht an die Gra�kkarte an. Werden für Berechnungen Thread-

Page 79: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

5.4. Evaluation der Ausführungsgeschwindigkeiten 72

Gröÿen benötigt, die diese Grenze übersteigen, teilt Rootbeer die Berechnung auf mehrere Kernel-Ausführungen auf. Je mehr Kernel-Invokes benötigt werden, desto langsamer wird entsprechenddie Ausführungszeit. Rootbeer setzt den Wert der maximal parallel ausgeführten Blöcke auf64 fest. Moderne Gra�kkarten führen Block-Zahlen in Millionenhöhe parallel aus. Als zweitenGrund �ndet sich die Codequalität. Im Pro�ler fällt eine als BlockingQueue bezeichnete Klasseauf, die allerdings auf neu abzuarbeitende Aufgaben in einer while-Schleife aktiv wartet. Alleindiese Schleife zeichnet für 90% der Ausführungszeit verantwortlich.

Die Java-Ausführung von Rootbeer ist sogar noch langsamer. Dies äuÿert sich darin, dass abeiner Array-Gröÿe von 65.000 180 Sekunden zur Berechnung nicht mehr ausreichen und diesedeswegen vorzeitig abgebrochen wird. Die serielle Berechnung benötigt zur Inkrementierung einesentsprechend groÿen Arrays rund 0,3 ms.

Matrix-Multiplikation Die Matrix-Multiplikation weist eine wesentlich höhere Menge annötigen Berechnungen auf. Jeder Thread muss den Inhalt jeder Zelle in der zu berechnendenZeile bzw. Spalte laden. Jeweils zwei Zellen werden durch eine Multiplikation verknüpft und derenInhalt addiert. Zusätzlich müssen für die linearisierten Eingabematrizen die Indexe der Zellenberechnet werden, wodurch jeweils eine Multiplikation und eine Addition zusätzlich anfallen.Geht man von einer Matrix-Gröÿe und -Breite von M aus und bezieht dabei nur Multiplikationein, so ergibt sich ein Verhältnis zwischen Speicherzugri� und Berechnung zu M ·3

2·M = 1, 5. Jehöher dieses Verhältnis, desto besser ist ein Algorithmus für die GPU-Ausführung geeignet. DieArray-Inkrementierung kommt hier nur auf einen Wert von 0, 5.

Das Ergebnis ist in Abbildung 5.13 dargestellt.

Wie erwartet liegen bis auf Rootbeer und Delite alle Zeiten unter der sequentiellen Ausführungs-zeit. Aparapi ist dabei 80 Mal schneller, JCuda sogar 119 Mal. Die Kurve der nativen Evaluationhebt sich kaum von JCuda ab. Auch hier sind die Zeiten der Kernels identisch, wodurch nur einzusätzlicher Overhead v.a. durch den JNI-Layer auftritt. Bei der hohen Anzahl an Berechnungenfällt dieser allerdings nicht mehr ins Gewicht.

Für Delite wurde zur Ausführung nicht die in der Bibliothek verfügbare, optimierte Matrix-Multiplikation verwendet, sondern die unoptimierte Variante, die auch bei den anderen Bib-liotheken zum Einsatz kommt. Die Scala-Ausführung scha�t es dabei noch sich an die Java-Ausführungszeit von Aparapi anzunähern und liegt sogar noch unter der seriellen Zeit. Die GPU-Ausführung ist hingegen deutlich langsamer als die serielle Variante. Verwendet man statt demunoptimierten Algorithmus die in der Bibliothek zur Verfügung stehende, optimierte, Matrix-Multiplikation, so sinkt die Ausführungszeit wesentlich auf rund 1/8 der seriellen Zeit.

Auch die anderen Bibliotheken stellen über Shared-Memory Mittel zur Optimierung zur Verfü-gung. Dadurch benötigt Aparapi nur noch 1/604 und JCuda nur noch 1/648 der seriellen Zeit.Die native, optimierte, Ausführung weist mit 1/668 die höchste Beschleunigung auf. Unter diesemAspekt erscheint die BLAS optimierte Variante von Delite sehr langsam.

Parallele Summation von Array-Elementen Das Ergebnis der Evaluation �ndet sich inAbbildung 5.14. Als Erstes sticht ins Auge, dass keine der Abstraktionsbibliotheken die serielleAusführungszeit bei der Summation der Elemente unterschreiten kann. Dies gilt auch bei ver-

Page 80: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

5.4. Evaluation der Ausführungsgeschwindigkeiten 73

Abbildung 5.12: Ausführungszeiten zur Array-Inkrementierung bis 25.000 (links: GPU- undCPU-Ausführungszeiten, rechts: GPU-Ausführungszeiten bis 25.000, unten: Ausführungszeitenbis 2.000.000

Page 81: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

5.4. Evaluation der Ausführungsgeschwindigkeiten 74

Abbildung 5.13: Ausführungszeiten zur Matrix-Multiplikation (links: GPU und CPU-Ausführungszeiten, rechts: GPU-Ausführungszeiten)

hältnismäÿig groÿen Arrays, die summiert werden sollen. Zurückzuführen ist dies wohl auf diesehr einfache Implementierung auf der CPU, die mit nur einer Schleife auskommt. Der Overheaddes Algorithmus von Blelloch führt hier wesentlich zusätzlichen Overhead ein.

Rund 120% der seriellen Zeit benötigt im Schnitt die nächsthöhere Kurve der nativen Ausführung.Direkt gefolgt wird diese von der Bibliothek JCuda, die allerdings bei einer Array-Gröÿe von rund33 Millionen Einträgen abbricht. Ab dieser Grenze meldet die Bibliothek zu wenig Speicher. Dassdies nicht richtig ist folgt aus der durchgängigen Kurve der nativen Evaluation. Auch Aparapiführt den Anwendungsfall ohne Fehler aus. Letztendlich benötigt JCuda bis zu dieser Grenzebereits rund die doppelte Ausführungszeit der seriellen Ausführung.

Aparapi hat ebenfalls Probleme mit der Ausführung dieses Anwendungsfalls und benötigt aufder GPU 286% der seriellen Ausführungszeit. Die CPU-Ausführung der Bibliothek ist noch lang-samer. Hier konnte ein massiver Overhead festgestellt werden, der dazu führt, dass die Kurve mitmassiver Steigung exponentiell wächst. Die Zeiten betragen das 2.000-fache der seriellen Ausfüh-rungszeit. Ein Erklärungsversuch bezieht sich hier auf lange Wartezeiten bei Locks zwischen denEinzelrunden der Reduktion. Bei Beobachtung der CPU-Auslastung während der Berechnungwurde auch selten mehr als eine CPU in Anspruch genommen.

Delite liegt im Gegensatz dazu mit der CPU-Ausführung, die 146% der seriellen Ausführungszeitbenötigt, wesentlich besser. Der Abfall zu Beginn der Kurve ist wohl auf eine Kompilierung desauszuführenden Quelltextes durch den Hotspot-Compiler zurückzuführen. Im Gegensatz dazumutet die Kurve der Delite CUDA-Ausführung sehr seltsam an. Hier ist der Einbruch bei rund25.000.000 Elementen wesentlich deutlicher ausgeprägt. Dieser konnte auch bei mehrfacher Aus-führung des Evaluationsfalls reproduziert werden. Der Overhead liegt in jedem Fall so hoch, dasseine entsprechende Ausführung nicht lohnenswert ist.

Rootbeer �ndet sich nicht in der Gra�k wieder. Grund dafür ist, dass die sich ergebenden Ergeb-

Page 82: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

5.4. Evaluation der Ausführungsgeschwindigkeiten 75

nisse rechnerisch nur unter Annahme einer Fehlfunktion der Synchronisation erklärt werden kön-nen. Da der generierte Code nicht ausgegeben werden kann, wurde auf eine weitere Fehlersucheverzichtet.

Zusätzlich zum rudimentären Algorithmus wurde die Reduktion unter Nutzung von Shared-Memory für JCuda, Aparapi und nativem CUDA-C implementiert. Ohne das Ergebnis näher zuerläutern benötigt Aparapi hierdurch rund 68%, JCuda 91% und die native Ausführung 80% dernicht optimierten Ausführungszeit. Die Speicherarchitekturen haben also deutlichen Ein�uss aufdie Ausführungsgeschwindigkeit der Algorithmen.

Insgesamt ist das Verhältnis zwischen Berechnungen und Speichertransfer in diesem Anwen-dungsfall nicht hoch genug, um eine GPU-Ausführung zu rechtfertigen. Die Reduktion ist eherein Anwendungsfall, der innerhalb eines Algorithmus, der auf der GPU ausgeführt wird, zum Ein-satz kommt. Ein Beispiel ist das Verfahren der konjugierten Gradienten, in dem die Reduktionebenfalls eine Rolle spielt.

Mandelbrot Die resultierende Gra�k zur Berechnung der Mandelbrot-Fraktale �ndet sich inAbbildung 5.15.

Rootbeer ist, wie in den bereits vorhergehenden Fällen, die langsamste Bibliothek und ist dabeiim Gesamten rund 22 Mal (GPU) bzw. 28 Mal (JEMU) langsamer als die serielle Ausführung. DieCUDA-Ausführung von Delite beginnt mit einem hohen Overhead, gleicht sich aber insgesamtan die serielle Ausführung an und endet mit einer im Schnitt 1,25 Mal höheren Ausführungszeit.

Aparapi und JCuda schenken sich in diesem Anwendungsfall nichts. Diesmal ist Aparapi rund6% schneller. Gleichzeitig ist auch die Aparapi Thread-Pool-Ausführung wesentlich schneller alsdie serielle Ausführung (rund 9 Mal). Schnellste Kurve ist insgesamt die native Ausführung, dieim Verhältnis zur seriellen Ausführung eine Beschleunigung um den Faktor 143 aufweist.

Jakobi-Verfahren zur Lösung von Poisson-Gleichungen Durch die mehrfache Ausführungvon Kernels innerhalb des Jakobi-Algorithmus ist zu erwarten, dass die Ausführungsgeschwin-digkeiten von JCuda, Aparapi und Delite wesentlich niedriger als die der Rootbeer-Ausführungsind, da hier auf weitere Kopiervorgänge zwischen GPU und CPU bei jedem Kernel-Start und-Ende verzichtet werden kann.

An Abbildung 5.16 ist dieses Verhalten nicht vollständig zu erkennen, da Rootbeer, wie leiderauch bereits in den vorherigen Tests, eine wesentliche höhere Ausführungszeit aufzeigt. Aparapiund JCuda �nden sich unter der seriellen Ausführungszeit wieder, wobei Aparapi rund 54%und JCuda 70% der Zeit benötigen. Allerdings rechnet sich der GPU-Overhead erst ab einerMatrix-Gröÿe von rund 1.000× 1.000.

Dies gilt nicht für Rootbeer, Delite und Aparapi JTP. Im Fall von Aparapi JTP ist die typischeAnfangssteigung als Overhead zu erkennen. Damit beträgt die Ausführungszeit im Verhältnis zurseriellen Zeit das rund 13-fache. Delite kann für diesen Anwendungsfall keinerlei Optimierungenanwenden und erreicht die 85- (CUDA) bzw. 174-fache (Scala) Zeit. Diese Werte beziehen bereitsdie in 4 Evaluationsfällen aufgetauchten Zeitüberschreitungen mit ein. Aus diesem Grund ist diesteiler steigende Kurve von Rootbeer mit einer 10 (GPU) bzw. 19 (JEMU) Mal langsamerenAusführung schneller als Delite.

Page 83: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

5.4. Evaluation der Ausführungsgeschwindigkeiten 76

Abbildung 5.14: Ausführungszeiten zur Reduktion (links: GPU und CPU-Ausführungszeiten,rechts: GPU-Ausführungszeiten)

Abbildung 5.15: Ausführungszeiten zur Berechnung eines Mandelbrot-Fraktals (links: GPU undCPU-Ausführungszeiten, rechts: GPU-Ausführungszeiten)

Page 84: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

5.4. Evaluation der Ausführungsgeschwindigkeiten 77

Abbildung 5.16: Ausführungszeiten zur Lösung von Poisson-Gleichungen unter Nutzung desJakobi-Verfahrens (links: GPU und CPU-Ausführungszeiten, rechts: GPU-Ausführungszeiten)

Verfahren der konjugierten Gradienten zur Lösung von Poisson-Gleichungen Dasselbe Verhalten lässt sich auch beim Verfahren der konjugierten Gradienten mit seinen vielenKernel-Aufrufen feststellen.

Der Algorithmus wurde ausschlieÿlich für Aparapi und JCuda implementiert. Für Delite wurdezwar die Implementierung bis zum kompilierenden Code fertiggestellt, konnte aber auf Grundnicht zu debuggender Fehler bei der Code-Generierung nicht zur Ausführung gebracht werden.Der selbe Grund für die Nicht-Implementierung gilt auch für Rootbeer. Auch hier wurde einkompilierender Algorithmus fertiggestellt. Jedoch liefert dieser zur Ausführungszeit unter Ver-wendung desselben Algorithmus, der auch bei JCuda und Aparapi zum Einsatz kommt, diefalschen Ergebnisse. Da für kleinere Matrizen die richtigen Resultate berechnet werden, liegt derSchluss wiederum nahe, dass es sich wiederum um den selben Fehler in Bezug auf die Synchro-nisation von Threads in Blöcken handelt.

Für die anderen Bibliotheken sind die Ergebnisse in Abbildung 5.17 dargestellt.

Bis zu einer Matrix-Gröÿe von rund 1.000 Elementen scha�t es keine GPU-Ausführung die serielleAusführung zu unterschreiten. Erst ab dieser Gröÿe übersteigt die mögliche Beschleunigung durchNutzung der Gra�kkarte den Overhead der Nutzung der GPU. Im Gesamten ermöglicht dies fürdie GPU-Ausführung von Aparapi im Verhältnis zur seriellen Ausführungszeit eine Beschleuni-gung von 1,7 und für JCuda sowie die native Ausführung eine Beschleunigung von 1,8.

Page 85: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

5.5. Fazit der Evaluation 78

Abbildung 5.17: Ausführungszeiten zur Lösung von Poisson-Gleichungen unter Nutzung desVerfahrens der konjugierten Gradienten (links: GPU und CPU-Ausführungszeiten, rechts: Aus-führungszeiten bis zu einer Parametergröÿe von < 1.200)

5.5 Fazit der Evaluation

Alle Bibliotheken besitzen Stärken und Schwächen. Eine der Bibliotheken wird nachfolgend fürdie Implementierung des Simulators ausgewählt:

Rootbeer scheidet als erste Bibliothek aus. Groÿes Plus der Bibliothek ist die einfach zu verwen-dende API und die Unterstützung auch spezieller Konstrukte wie Shared-Memory. Allerdingsist der generierte Code sehr fehleranfällig. Dies kann v.a. daran festgemacht werden, dass nichtin allen Evaluationsfällen lau�ähiger bzw. richtiger Quelltext produziert werden konnte. Ins-besondere betri�t dies die Reduktion und das komplexe Verfahren der konjugierten Gradienten.Auÿerdem ist die langsame Ausführungszeit über alle Evaluationsfälle hinweg ein Ausschlusskri-terium.

Mit JCuda können alle Anwendungsfälle korrekt implementiert werden und auch die Ausfüh-rungszeit ist durch die direkte Abbildung auf native CUDA-Funktionen und einen sehr dünnenJNI-Layer sehr gut. Als Ausschlusskriterium fungiert hier die Art der Programmierung. EineJava-Abstraktion sollte es ermöglichen, GPU-Quelltext in Java zu spezi�zieren. Gleichzeitig sollteals Ausführungsmodus auch eine Java-Ausführung unterstützt werden, damit aus Java herausein Testen des Algorithmus möglich ist. Keine der beiden Anforderungen kann JCuda erfüllen,da Kernels nativ in CUDA-C spezi�ziert werden müssen.

Als vorletzte Bibliothek ist auch Delite nicht verwendbar. Diese Bibliothek entspricht eigentlichdem, was ein Entwickler sich als GPU-Abstraktion wünscht. Die domänenspezi�sche Program-mierung und Erstellung des Quelltextes resultiert in nahezu optimalem Quelltext, wobei zusätz-lich die Ausführung über Pattern-Matching noch vom Entwickler wesentlich verbesserbar ist.Leider be�ndet sich die Bibliothek noch nicht in einem Stadium, in dem diese produktiv einge-

Page 86: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

5.5. Fazit der Evaluation 79

setzt werden kann. Kennzeichen hierfür betre�en auf der einen Seite teilweise nicht lau�ähigengenerierten Quelltext, Fehlermeldungen, die nicht auf den Quelltext zurückzuführen sind, keineMöglichkeit zum Debuggen und zusätzlich die nicht besonders schnelle Ausführungszeit. Trotz-dem ist hervorzuheben, dass die Nutzung externer Bibliotheken für BLAS-Operationen und dieautomatische Abbildung von Domänenfunktionen in Delite auf diese Operationen eine Beson-derheit darstellt, die auch für andere Bibliotheken wünschenswert ist.

Schlieÿlich bleibt als letzte Bibliothek Aparapi. Diese überzeugt durch eine schnelle Ausführungs-zeit, eine zuverlässige Code-Generierung sowie mit einer für den Programmierer freundlichen API,die insbesondere durch den impliziten, zur Laufzeit erfolgenden, Staging-Vorgang besonders test-bar ist. Allerdings bleiben auch für diese Bibliothek verschiedene Nachteile, wie insbesondere diesehr hohe Fehleranfälligkeit des unterliegenden Quelltextes sowie die bisher nicht vorhandeneImplementierung von verschiedenen Kernel-Aufrufen, sodass mehrere verschiedene Kernels aufunterschiedliche Speicherbereiche abgebildet werden.

Trotzdem entspricht Aparapi der von allen evaluierten Bibliotheken besten Option und wird fol-gerichtig über den weiteren Verlauf der Arbeit zur Portierung des nativen Simulators eingesetzt.

Page 87: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

Kapitel 6

Portierung des Simulators

Im vorherigen Kapitel wurden verschiedene Bibliotheken evaluiert und �nal Aparapi als ambesten einsetzbare Bibliothek ausgewählt. Anhand dieser wurde der bestehende native Simulatordes Fraunhofer Instituts nach Java portiert. Dazu wurde zuerst eine Implementierung auf Basisder verfügbaren Bibliothek erstellt und diese anschlieÿend weiter verbessert.

Einige Spezi�ka werden im nachfolgenden Kapitel genauer erläutert.

6.1 Struktur des Simulators

Die grundsätzliche Struktur des Simulators ist in Abbildung 6.1 dargestellt. Der ursprünglicheKernel-Code aus dem nativen Simulator konnte fast unverändert übernommen werden, da Cund Java sich in der Syntax nicht wesentlich unterscheiden. Hauptsächlich wurden Pointer durchArrays und Unions jeweils durch mehrere Arrays ersetzt. Einige Hilfsmethoden, z.B. zur Berech-nung des Array-Indizes aus drei Dimensionen, wurden zur Übersichtlichkeit hinzugefügt.

Auÿerdem mussten, durch den Austausch des nativen CUDA-C durch Aparapi, die Kernel-Aufrufe durch Aparapi-Aufrufe ersetzt werden. Die Struktur der Kernels wird im nächsten Ab-schnitt beschrieben.

Kernel-Hierarchie unter Nutzung der Aparapi-API

Statt der Verwendung von globalen Funktionen, wie sie im ursprünglichen Simulator zu �nd-en sind, wurde eine Kernel-Hierarchie entworfen, die die einzelnen Kernel-Methoden kapselt.Um die explizite Speicherverwaltung von Aparapi nutzen zu können, darf dabei allerdings nureine einzige Kernel-Klasse verwendet werden. Entsprechend der empfohlenen Vorgehensweisefür mehrere Einsprungpunkte, übernimmt eine globale Variable im Kernel dabei die Unter-scheidung, welche Kernel-Aufrufe welche Kernel-Methoden starten sollen. Im Simulator erfolgtdie Aufteilung basierend auf einem If-Konstrukt, das über einen übergebenen Task-Parameterauf Gleichheit zu globalen Integer-Konstanten prüft. Jedem Kernel, bzw. der zugehörigen If-Bedingung, ist eine entsprechende Konstante zugewiesen.

Zur Strukturierung des entstehenden Quelltextes bietet Aparapi drei Möglichkeiten:

Kernel-Klasse Die Einzelmethoden werden alle in eine Kernel-Klasse geschrieben. Daraus re-sultiert sehr unübersichtlicher Quelltext, da in einer einzigen, mehrere 1000 Zeilen langen,Datei das Au�nden einer Einzelzeile sehr aufwändig ist.

Page 88: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

6.1. Struktur des Simulators 81

FluidSolver

public void init();public void step();public void cleanUp();

SimulationKernel

private static final int ADD_SOURCE = 0;private static final int SWAP_VELOCITIES = 1;private int task;

// (...)public void gpuMethod();

ModelSimulationData

Kernel

InterpolationKernel

BoundaryKernel

PressureKernel

AdvectionKernel

VisualizationKernel

AddSourceVelocity Diffusion Swap DefaultModel

RuntimeVariables

Main SimulationGUI

Abbildung 6.1: Struktur des mit Aparapi implementierten Simulators

Page 89: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

6.1. Struktur des Simulators 82

Statische Methoden Alternativ können die Methoden in andere Klassen ausgelagert wer-den und statisch aus dem Einstiegspunkt referenziert und aufgerufen werden. Vorsichtist geboten, soweit Methoden aus der Kernel-Klasse selbst benötigt werden. Beispielsweisemuss zur Ermittlung der aktuellen Thread-ID die Methode #getGlobalId in der Kernel-Klasse verwendet werden. Diese Methode ist jedoch nur innerhalb der Kernel-Klasse aufruf-bar und steht damit in der statischen, externen, Methode nicht zur Verfügung. WeitereBeispiele dafür betre�en u.a. die Berechnung von Wurzeln oder das Runden von Werten.Aparapi stellt für derartige Funktionen eigene Methoden zur Verfügung, die diese Opera-tionen auf OpenCL-Konstrukte abbilden.Für die Thread-ID ist eine Lösung durch Übergabe der ID als Parameter an die aufzu-rufende Funktion möglich. Andere Anwendungsfälle, wie z.B. die Wurzelfunktion, besitzenkeine derartige Lösung. Entsprechend kann Quelltext, der derartige Methoden verwendet,nicht ausgelagert werden.

Vererbungshierarchie Als dritte und letzte Möglichkeit kann eine Pseudo-Vererbungshierarchieeingesetzt werden. Mehrere verschiedene Kernel-Klassen werden implementiert und leitenvoneinander linear ab, wobei die oberste Kernel-Klasse von der Aparapi-Kernel-Klasseableiten muss. Jede dieser Unterklassen kann dabei Methoden für eine Untergruppe anMethoden halten. Im implementierten Simulator wird diese Aufteilung z.B. zur Berech-nung der Advektion bzw. zur Kapselung der Visualisierungsmethoden verwendet.

Für die Implementierung wurde eine Kombination der drei oben genannten Ansätze verwen-det. Die Auslagerung in statische Methoden wurde dabei auf Grund der etwas besseren Test-barkeit bevorzugt. Hier muss auf keine Aparapi-Konstrukte zurückgegri�en werden, da statischeFunktionen direkt getestet werden können. Die meisten Kernel-Methoden nutzen jedoch nativeKernel-Funktionen, v.a. zur Wurzelberechnung, womit die Auslagerung nur teilweise möglich ist.

Grundsätzlich sind alle drei Ansätze über die JTP-Ausführung testbar. Dabei hält sich auch derSpeicherverbrauch stark in Grenzen, da der teure OpenCL-Kontext erst bei der ersten nativenAusführung initialisiert wird. Ein Test kann z.B. über die normale JUnit-Infrastruktur spezi�-ziert und anschlieÿend ohne weitere Zusätze in einem Continuous-Integration-Server ausgeführtwerden.

Datenstrukturen

Als Datenstruktur zum Halten des Simulationszustands bzw. für die Berechnungen wurden die inder nativen Simulation verwendeten Arrays übernommen. Die Java-seitigen Arrays werden aller-dings im Gegensatz zur nativen Variante nicht nach dem Kopiervorgang auf die GPU freigegeben,sondern weiter vorgehalten. Ein nahtloser Wechsel zwischen JTP- und GPU-Abarbeitung istdamit durch einen Kopiervorgang ohne vorherige neue Allokation der Ressourcen möglich. Gleich-zeitig resultiert daraus ein erhöhter Speicherverbrauch, der aber auf Grund der physikalischenTrennung der Speicher von Device und Host keine Auswirkungen hat.

Die Daten selbst werden in einer SimulationData-Klasse vorgehalten. Im Gegensatz dazu hältein Model nur generelle Simulationsparameter, also z.B. die Breite, Höhe und Tiefe des Simulati-onsraums. In der nativen Simulation �ndet sich das Modell ebenfalls wieder, wobei das Halten der

Page 90: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

6.1. Struktur des Simulators 83

Simulationsdaten nicht nötig ist, da kein direkter Wechsel zwischen GPU und CPU-Ausführungmöglich ist.

Eine zusätzliche RuntimeVariables-Klasse hält auÿerdem alle Werte, die aus der Benutzerober-�äche veränderbar sind, namentlich z.B. der Ausführungsmodus oder die Kameraposition.

Abarbeitungssteuerung im Fluid-Solver

Die zentrale Abarbeitungssteuerung übernimmt die FluidSolver-Klasse. Diese ist für die Initia-lisierung eines einzelnen Kernel-Objektes zur Simulation verantwortlich. Dazu werden Set- undGet-Methoden auf dem Kernel-Objekt aufgerufen, die jeweils entsprechende Objekte von oderzur GPU kopieren.

Für einzelne Kernel Aufrufe wird jeweils der entsprechende Task auf dem Kernel gesetzt und dieAbarbeitung gestartet. Die #step-Methode kapselt die Abarbeitung eines vollständigen Simula-tionsschrittes. Insgesamt werden hier 34 Aufrufe gestartet, wobei der Groÿteil auf die Projektionentfällt, die unter Nutzung von 5 Jakobi Iterationen den Druck im Simulationsraum angleicht.

Die Steuerungslogik, also die Simulationsschritte und deren Reihenfolge, entspricht wiederumder nativen Implementierung.

Gra�sche Benutzerschnittstelle mit Swing und JOGL

Die native Simulation verwendet zur Visualisierung OpenGL. JOGL [jog13] stellt die Bibliothekunter Java zur Verfügung. Ein entsprechender Simulations-Canvas wird in Swing eingebundenund über APIs, die stark denen der nativen Implementierung gleichen, kon�guriert und ausge-führt.

Die Simulationsschleife wird ebenfalls von OpenGL vorgegeben. In JOGL entspricht dies einemAnimator-Objekt, der als Taktgeber fungiert und alle n Millisekunden eine #display-Methodeaufruft. Diese ruft entsprechend jeweils den Simulationsschritt im FluidSolver auf und stelltanschlieÿend unter Nutzung zurück kopierter Werte die Ergebnisse in verschiedenen Visualisie-rungen dar. Kopiert wird nur benötigter Inhalt, hauptsächlich also die Positionen darzustellenderPartikel sowie deren Farben.

Im Schnitt wird die Simulation im GPU-Modus dabei mit rund 50 Frames pro Sekunde ausge-führt. Für einen �üssigen Ablauf der Simulation ist dies ausreichend. Im Gegensatz dazu erreichtdie JTP-Darstellung gerade rund 5 Frames pro Sekunde.

Final ergibt sich eine Ober�äche wie in Abbildung 6.2 dargestellt. Der graue Kasten im Bildrepräsentiert den Simulationsraum. Links ist ein länglicher Balken zu sehen, der einen Ein�ussrepräsentiert. Dessen Geschwindigkeitsvektoren zeigen jeweils nach rechts in den Simulations-raum. Rechts ist ein Aus�uss abgebildet. Links unten ist ein weiteres Rechteck zu sehen, das einHindernis repräsentiert.

Page 91: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

6.2. Erweiterung und Verbesserung von Aparapi 84

Abbildung 6.2: Bildschirm-Foto des Simulators während der Simulation

6.2 Erweiterung und Verbesserung von Aparapi

Nachfolgend werden an der Bibliothek verschiedene Verbesserung zur Erhöhung der Geschwin-digkeit bzw. zur Verbesserung der Nutzbarkeit beschrieben.

6.2.1 Implementierung objektorientierter Einsprungpunkte

Die bedingte Unterstützung mehrfache Einsprungpunkte stellt eine deutliche Einschränkung imProgrammiermodell von Aparapi dar. Dabei ist v.a. die Verwendung unterschiedlicher Speicher-bereiche für verschiedene Kernels ein Problem bei der Erstellung objektorientierter Anwendun-gen.

Änderung der Java-API An der grundsätzlichen API sind einige Änderungen vorzunehmen.Im Wesentlichen betri�t dies die Tatsache, dass in der vorliegenden Version jeder Kernel eineneigenen KernelRunner besitzt, der wiederum auf einen eigenständigen OpenCL-Kontext abge-bildet wird. Ziel ist es auf einen einzigen OpenCL-Kontext mehrere Kernel-Aufrufe, in diesemFall also KernelRunners, abzubilden.

In Java führt dies zu der in Abbildung 6.3 angegeben Struktur. Wichtigste Änderung ist, dasseinzelne Kernels leichtgewichtig werden. Dies wird dadurch erreicht, dass Kernels selbst nichtmehr ausgeführt werden können, sondern einem KernelRunner zur Ausführung übergeben wer-den. Eine direkte Referenz auf einen KernelRunner innerhalb eines Kernels existiert nicht mehr.

Damit ein KernelRunner zwischen verschiedenen Kernel-Klassen zur Ausführung unterschei-den kann, wird zusätzlich eine Map von der Kernel-Klasse auf ein KernelMapping eingeführt.

Page 92: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

6.2. Erweiterung und Verbesserung von Aparapi 85

Dieses kapselt bisher als direkte Attribute der KernelRunner-Klasse zu �ndende Attribute. Diesemüssen pro Kernel einmal vorgehalten werden. Beispiele umfassen dabei die schon als Schlüsselder Map dienende Kernel-Klasse, die Argumente, die zur Ausführung an den Kernel übergebenwerden müssen, die umgewandelte Darstellung transformierte des Einsprung-Punktes sowie einjniContextHandle.

Die Hauptaufgabe der KernelArg-Klasse ist die Kapselung des Datentyps eines Kernel-Argumentsund dessen Länge. Ausprägungen der Klasse werden auf der nativen Seite zum Start der Ker-nels benötigt. Die EntryPoint-Klasse dient hauptsächlich der Code-Generierung und wird an-schlieÿend nur noch zur Generierung von Structs für die Konvertierung von Objekten verwen-det. Wichtigstes Element ist hingegen das Context-Handle. Auf der nativen Seite müssen ver-schiedene Daten zwischengespeichert werden, die nicht bei jedem Kernel-Aufruf erneut gesetztwerden sollen. Dazu gehört u.a. der Quelltext des Kernels sowie dessen Parameter. Um dies zugewährleisten, existiert in der nativen Umgebung eine Klasse JNIContext. Der korrekte Namender Klasse ist eigentlich KernelContext, da die Klasse alle für einen Kernel relevanten Attributekapselt, mit JNI dagegen nichts zu tun hat. Eine Instanz dieser Klasse wird jeweils beim erstenStart eines Kernels instanziiert und deren Speicheradresse als jniContextHandle zurückgegeben.Für jeden weiteren JNI-Methodenaufruf wird diese Adresse wiederum übergeben und zurück indie JNIContext-Klasse umgewandelt.

Auch die explizite Speicherverwaltung muss in der neuen API angepasst werden. Get- und Put-Operationen machen, im Gegensatz zur bisherigen API, innerhalb der Kernel-Klasse keinen Sinnmehr, da hier kein Zugri� auf den nativen Teil der Bibliothek mehr möglich ist. Die entsprechen-den Methoden wurden deswegen in den KernelRunner verschoben und werden nun unabhängigvon allen Kernel-Klassen auf den OpenCL-Kontext abgebildet.Die dabei verfolgte Idee ist, dass die native Implementierung die Abbildung gleicher Java-Referenzen auf den gleichen OpenCL-Speicherbereich übernimmt. Die Java-Seite bemerkt davonnichts, sondern übergibt die Referenzen schlicht weiterhin an die native Seite. Sobald Get- oderPut-Methoden aufgerufen werden, wird, basierend auf der Referenz, der OpenCL-Speicherinhaltaktualisiert oder ausgelesen und, falls nötig, der Inhalt des Java-Objektes durch Überschreibendes Adressinhalts aktualisiert.

Änderung der nativen Implementierung Die native Implementierung ist in sieben wesent-liche JNI-Methoden aufgeteilt, die den Lebenszyklus eines KernelRunners bzw. eines Kernelswieder spiegeln:

initJNI Die Methode wird einmal pro Kernel aufgerufen. Darin wird die Umgebung für die Aus-führung eines bestimmten Kernels gescha�en. Wesentliches Element ist dabei die Erstellungdes bereits erwähnten JNIContext-Objektes, das alle Attribute eines Kernels kapselt. JederKontext besitzt z.B. eine ID für ein OpenCL-Gerät, eine eigene Abarbeitungswarteschlangeund z.B. Referenzen auf Kernel-Objekte oder -Klassen.

buildProgramJNI Nachdem der CUDA-Quelltext Java seitig erstellt wurde, wird dieser, wiederumgemeinsam mit einem Kontext, an die Methode übergeben. Folgend wird der Quelltextkompiliert und die CommandQueue für den Kontext erstellt und gesetzt.

Page 93: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

6.2. Erweiterung und Verbesserung von Aparapi 86

Kernel

public abstract void run();public int getLocalId(int dim);public int getGroupId(int dim);public int getGlobalId(int dim);public static double sqrt(double value);public static double floor(double value);

KernelRunner

private Map<Class, KernelMapping> kernelMapping;

public void execute(Kernel kernel, Range range);public KernelRunner put(int[] array);public KernelRunner get(int[] array);public void dispose();

KernelMapping

public final Class<? extends Kernel> kernelClass;public final List<KernelArg> kernelArgs;public final Entrypoint entryPoint;public long jniContextHandle;

MandelbrotKernel

private int[] pixels;

public void run();

MatrixMultiplicationKernel

private int[] in1;private int[] in2;private int[] result;

public void run();

Launcher

public static void main(String[] args);

Abbildung 6.3: Geänderte Aparapi-API um verschiedene Kernels auf den selben KernelRunnerabbilden zu können.

Page 94: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

6.2. Erweiterung und Verbesserung von Aparapi 87

setArgsJNI Bisher wurden keine Argumente für den Kernel übergeben. Dies wird nun durchden Aufruf der Methode nachgeholt. Übergeben wird eine Menge an KernelArg-Objekten,die jeweils in ein natives KernelArg-Objekt umgewandelt werden. Dieses beinhaltet auchden Pu�er, der für das Halten der Referenz auf direkten GPU-Speicher zuständig ist.

runKernelJNI Vor Aufruf dieser Methode wird auf der Java-Seite jedes Kernel-Argumentgeprüft und, falls nötig, Referenzen von Arrays aktualisiert bzw. explizite Put-Operationenabgearbeitet. Wird dabei erkannt, dass eine Referenz verändert wurde, so wird ein Boolean-Flag übergeben, das als Indikator für eine auszuführende Aktualisierung dient. In diesemFall wird evtl. bereits allozierter Speicher freigegeben, neuer Speicher alloziert und dieserals Argument für den Kernel an der passenden Stelle gesetzt. Abschlieÿend werden dieDaten kopiert und der Kernel ausgeführt.

disposeJNI Pro Kontext kann hier allozierter Speicher wieder freigegeben werden.

getJNI Wird der explizite Speichermodus verwendet, so werden Get-Operationen auf dieseMethode abgebildet. Entsprechend wird der Speicher der GPU ausgelesen und der Inhaltim KernelArg gesetzt.

getExtensionsJNI Die Methode dient dem Au�nden der verfügbaren OpenCL-Erweiterungen.

Folgend auf die bisherige Implementierung wurden verschiedene Abläufe leicht verändert unddie Struktur optimiert. Dazu wurden nicht nur die neuen Funktionen eingebaut, sondern dieinterne Struktur durch Verschiebung globaler Funktionen in entsprechende Klassen, sowie durchdie Verwendung von Polymorphismus, objektorientiert gestaltet. Die sich ergebende Struktur istin Abbildung 6.4 dargestellt und wird nachfolgend näher erläutert.

Grundsätzlich existieren jetzt zwei Kontexte: Der erste Kontext ist für das Halten eines Kernelsund dessen Attribute zuständig. Diese Klasse entspricht der früheren JNIContext-Klasse undwurde entsprechend ihrer Verantwortlichkeit in KernelContext umbenannt.Als zweiter Kontext existiert eine neu gescha�ene KernelRunnerContext-Klasse. Diese hält proKernelRunner alle, für die OpenCL-Ausführung notwendigen, Konstrukte, z.B. also die früherim JNIContext vorgehaltene Warteschlange für OpenCL-Kommandos.

Die Instanziierung der entsprechenden Objekte folgt dem Schema der bisherigen JNIContext-Implementierung. Beim ersten Aufruf einer entsprechenden JNI-Methode wird das Objekt in-stanziiert und dessen Adresse als Kontext zurückgegeben. Für den KernelRunnerContext wurdeeine neue JNI-Methode gescha�en, die beim ersten Kernel-Aufruf innerhalb eines KernelRunneraufgerufen wird und das native Objekt instanziiert. Der zurückgegebene Kontext wird global imKernelRunner vorgehalten und bei den folgenden JNI-Aufrufen jeweils übergeben.

Statt wie bisher bei der Ausführung eines Kernels für einen Array pauschal Speicher zu al-lozieren und die notwendigen Daten zu kopieren, muss nun nach bereits erstellten OpenCL-Referenzen Ausschau gehalten werden. Ist Speicher für ein Java-Objekt bereits alloziert, so mussder OpenCL-Speicher wiederverwendet werden. Die Verwaltung des Speichers sowie dessen Ab-bildung auf Java-Objekte übernimmt eine neue Klasse mit dem Namen BufferManager. DieKlasse wird pro KernelRunnerContext einmal instanziiert. Durch entsprechende Methoden kannein GPU-Pu�er für eine Java-Referenz abgeholt werden. Intern wird die Menge an momentan

Page 95: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

6.2. Erweiterung und Verbesserung von Aparapi 88

vorhandenen Pu�ern durchlaufen. Wird die Java-Referenz durch Vergleich auf Adressgleichheitgefunden, so wird der entsprechende Pu�er zurückgegeben. Ansonsten wird ein neuer Pu�eralloziert. Die Prüfung erfolgt explizit auf Adressgleichheit und nicht auf Gleichheit der Objekt-inhalte. Arrays mit gleichem Inhalt, aber unterschiedlichen Adressen, werden voraussichtlichauch in Kernels unterschiedlich verwendet und besitzen u.U. nach der Kernel-Ausführung andereInhalte.

Für die Pu�er sind als Datenstruktur zwei Klassen relevant: AparapiBuffer und ArrayBuffer.Beide Klassen waren in der bisherigen Implementierung ebenfalls vorhanden. ArrayBuffer kapseltdabei die Referenz auf einen Java-Array und dessen Elemente sowie den zugehörigen GPU-Speicher. Die Klasse AparapiBuffer ist dagegen für das Halten von mehrdimensionalen Arrayszuständig, die linearisiert auf die GPU abgebildet werden. Um besser mit den Klassen arbeiten zukönnen, wurde eine dritte Klasse, GPUElement, eingefügt, von der beide Pu�er-Klassen abgeleitetsind. Diese ist für das Halten des GPU-Speichers zuständig.

Um heraus�nden zu können, wann ein Pu�er nicht mehr benötigt wird und deswegen freigegebenwerden kann, hält die GPUElement-Klasse einen Referenzzähler in Form einer simplen Integer-Variable. Verweist ein Kernel-Argument auf einen Pu�er, so wird dessen interner Zähler um +1

inkrementiert. Wird dagegen das Kernel-Argument nicht mehr benötigt oder ändert sich derreferenzierte Pu�er, so wird der Zähler des entsprechend nun veralteten Pu�ers um 1 dekremen-tiert. Vor jedem Aufruf eines Kernels wird auf dem BufferManager eine Methode zum Aufräumenüber�üssiger Pu�er aufgerufen. Zu diesem Zeitpunkt wurden bereits alle notwendigen Referen-zen aktualisiert, womit veraltete, nicht mehr referenzierte Pu�er gelöscht und deren OpenCL-Speicherinhalte freigegeben werden können. Der BufferManager durchläuft alle gespeichertenPu�er und prüft, ob die Elemente jeweils einen Referenzzähler mit Inhalt > 0 haben. Ist diesnicht der Fall, so wird der Speicher freigegeben.

In einer früheren Version wurde auf den Referenzzähler verzichtet und stattdessen über allegespeicherten KernelContext-Objekte und deren Argumente gelaufen. Sobald kein Pu�er mehrvon einem Kontext referenziert wurde wurde dieser freigegeben. Der Ansatz ist funktionsfähigund wesentlich zuverlässiger als der Referenzzähler, der darauf angewiesen ist, dass alle, dieKlasse verwendenden, Code-Abschnitte den Zähler richtig verwenden. Im Gegensatz dazu bringtdas Durchlaufen der Kernel-Argumente und -Kontexte eine signi�kant längere Ausführungszeitmit sich, weswegen der Referenzzähler als Mittel der Wahl eingesetzt wird.

Schlieÿlich wurde auch die Logik hinter der bisherigen dispose-Methode verändert. Bisher wur-den dadurch der eine, bisher im KernelRunner referenzierte, OpenCL-Kontext aufgeräumt. Inder neuen Implementierung werden alle gespeicherten Kernel-Kontexte eines Runner-Kontextsaufgeräumt. Damit ist sichergestellt, dass nach erfolgreicher Ausführung auch z.B. der OpenCL-Kontext sowie die CommandQueue freigegeben werden.

6.2.2 Erhöhung der Geschwindigkeit durch Caching des besten OpenCL-

Gerätes

Im Verlauf der Implementierung mehrfacher Einsprungpunkte wurde der Java-Teil von Aparapieinem Pro�ling unterzogen. Im Ergebnis fällt eine Methode auf, die die Ausführung um rund30% verlängert. Der entsprechende Auszug aus dem Pro�ler ist in Abbildung 6.5 dargestellt.

Page 96: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

6.2. Erweiterung und Verbesserung von Aparapi 89

GPUElement

jobject javaObject;cl_mem mem;int referenceCount;

AparapiBuffer

int numDims;void* data;

ArrayBuffer

int length;void* addr;

KernelContext

jobject kernelObject;jclass kernelClass;cl_program program;cl_kernel kernel;KernelArg** args;

KernelArg

KernelContext* kernelContext;jobject argObj;jobject javaArg;GPUElement* buffer;

BufferManager

std::vector aparapiBufferList;std::vector arrayBufferList;

void cleanUpNonReferenceBuffers();

KernelRunnerContext

cl_device_id deviceId;cl_device_type deviceType;cl_context context;cl_command_queue commandQueue;BufferManager* bufferManager;std::vector kernelContextList;

Abbildung 6.4: Struktur der nativen Klassen in Aparapi

Page 97: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

6.2. Erweiterung und Verbesserung von Aparapi 90

Abbildung 6.5: Auszug aus VisualVM zum Flaschenhals der Device#best-Methode

Die Methode ist dafür zuständig, basierend auf den zur Verfügung stehenden Recheneinheiten,das beste OpenCL-Gerät auszuwählen und zurückzugeben. Um an die verfügbaren Geräte zugelangen wird eine native Methode aufgerufen, die die Plattformen aus OpenCL extrahiert undzurück gibt. Dies ist verhältnismäÿig zeitaufwändig und stellt einen Flaschenhals dar.

Um diesen zu umgehen wurde vorausgesetzt, dass sich die Ausführungsumgebung, also dieMenge an zur Verfügung stehenden OpenCL-Geräten, während der Ausführung nicht ändert.Hot-Swapping wurde explizit auÿen vor gelassen.Unter dieser Annahme kann das beste Gerät des letzten Methodenaufrufs als Attribut in derDevice-Klasse zwischengespeichert werden. Ist das Attribut null, so wird die Methode tatsäch-lich ausgeführt und das Ergebnis im Attribut zwischengespeichert. Ansonsten wird jeweils nurder Inhalt des Attributs zurückgegeben und die Ausführungszeit damit entsprechend verkürzt.

6.2.3 Aufruf von OpenCL-Methoden von auÿerhalb der Kernel-Klasse

Um auch aus statischen Methoden heraus OpenCL-Methoden, wie z.B. die Methode zur Berech-nung der Quadratwurzel, aufrufen zu können, wurden diese zwar in der Kernel-Klasse belassen,dafür aber statisch und ö�entlich gemacht.

Da die Methoden anschlieÿend aus einem Einsprungpunkt indirekt referenziert werden, erkenntAparapi die Methoden als auf direkten OpenCL-Quelltext abzubildende Funktionen und gene-riert den Quelltext entsprechend. In eine eigene Klasse können die Methoden nicht ausgelagertwerden, da die zuständige OpenCLDelegate-Annotation im Augenblick nur in der Kernel-Klassegesucht wird.

6.2.4 Implementierung der Verwendung von Objekten in Kernels

In der aktuellen Version von Aparapi ist die Verwendung von Objekten relativ eingeschränkt.Zwar können Arrays, die Objekte enthalten, verwendet werden. Diese müssen jedoch einzeln innative Structs umgewandelt werden, womit der Array zeitaufwändig neu aufgebaut werden muss.

Eine Alternative besteht in der Verwendung von Objekten, die Arrays und zugehörige Methodenbeinhalten. Statt also Arrays zu nutzen, die komplexe Objekte beinhalten, wird stattdessen ein

Page 98: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

6.2. Erweiterung und Verbesserung von Aparapi 91

Objekt verwendet, dass für jedes Attribut des ursprünglichen Objektes einen primitiven Arrayverwendet, der anschlieÿend auch direkt auf der GPU verwendet werden kann. Dies ermöglichtzusätzlich die Verwendung von Methoden, die auf die Attribute zugreifen, manipulieren und diesekapseln.

Ein Anwendungsfall ist beispielsweise die im Simulator verwendete Linearisierung einer Matrixauf einen eindimensionalen Array. Im Augenblick müssen für jeden Zugri� zuerst die zugehörigenIndexe berechnet werden, um anschlieÿend das richtige Element extrahieren bzw. aktualisierenzu können. Durch die Nutzung einer kapselnden Klasse kann dieser Quelltext zum Zugri� auf denArray, bzw. der Berechnung des linearisierten Indexes, gekapselt und das entstehende Konstruktnach auÿen hin durch ein Matrix-Interface zur Verfügung gestellt werden.

Abgesehen davon ermöglicht dies die mehrfache Verwendung der Klasse. Im Simulator wird z.B.nicht nur eine linearisierte Matrix verwendet, sondern an sehr unterschiedlichen Stellen, z.B. fürdie Speicherung der Geschwindigkeitsvektoren und der Partikel-Positionen. Durch Verwendungeiner Klasse könnte der Quelltext maÿgeblich verkleinert und besser strukturiert werden.

Da auf der GPU keine Objekte unterstützt werden, muss diese Art der Objektorientierung vor derKernel-Ausführung rückgängig gemacht werden. Beide nachfolgend kurz vorgestellten Variantenversuchen deswegen dies, basierend auf dem Aufrufgraphen, durch Inlining durchzuführen. Wirdbeispielsweise ein Feld array in einem Objektattribut object innerhalb eines Kernels verwendet,so müsste das entsprechende Feld in array_object umbenannt, object selbst entfernt und alleZugri�e aktualisiert werden.

Methoden müssen, für jedes verwendete Objektattribut, dupliziert werden, damit diese auf dierichtigen, umbenannten, Felder zugreifen. Gleichzeitig werden die Methodennamen, genau wie beiden Attributen, unter Verwendung der Attribut-Namen, die zu der Methode führen, umbenannt.

Zur eigentlichen Implementierung der Funktionalität stehen zwei Varianten zur Auswahl:

Entfernung der Objektorientierung im Bytecode Die erste Variante bezieht sich darauf,den Bytecode unmittelbar nach dem Einlesen in Aparapi und noch vor der Code-Generie-rung so zu verändern, dass die Objekte in der resultierenden Struktur nicht mehr vorkom-men. Entsprechend müssen Objektfelder und Methoden verschoben, ursprüngliche Feldergelöscht und Referenzen innerhalb von Instruktionen aktualisiert werden.

Code-Generierung Statt den Bytecode zu verändern wird während der Code-Umwandlung derAufruf-Graph von Methoden und Klassen erstellt und dieser bei der Code-Generierung, zurEntfernung von Objekten berücksichtigt.

Die erste der beiden Varianten ist, aufgrund der generischen Struktur, der schönere der bei-den Ansätze. Allerdings benötigt dieser zur Umsetzung eine vollständige Bytecode-Bibliothek,die auch die Verschmelzung von Klassen ermöglicht. Dies ist händisch sehr aufwändig, da imBytecode sog. Constant-Pools für Klassen, Methoden und Felder verwendet werden [LYBB13][S.82�]. Derartige Unterstützung ist in Aparapi im Augenblick nicht zu �nden, da das Einlesen desBytecodes händisch implementiert wurde. Entsprechend scheidet diese Variante auf Grund desnötigen Zeitaufwands vorerst aus.

Der zweite Ansatz ist wesentlich weniger generisch und erfordert die Umstrukturierung des

Page 99: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

6.2. Erweiterung und Verbesserung von Aparapi 92

Aparapi Quelltextes. Standardmäÿig analysiert Aparapi nur den Einsprungpunkt, um referen-zierte Methoden und Attribute zu �nden. Bei diesem Ansatz muss jeder gefundene Aufruf vonObjektmethoden ebenfalls analysiert und in einer internen Struktur abgebildet werden. Dabeimuss gleichzeitig der Aufrufgraph, an dieser Stelle also die Hierarchie der verwendeten Feldna-men, gespeichert werden, um die Felder und Methoden anschlieÿend entsprechend umbenennenbzw. dupliziert ausgeben zu können. Der Graph wird anschlieÿend auch noch dazu gebraucht,um native Referenzen auf Kernel-Objekte abbilden zu können, da nach der Code-Generierungdie Struktur des generierten Codes vom Java-Code di�eriert.

Auf Grund der nicht benötigten Bytecode-Transformation wurde dieser Ansatz weiter verfolgt.Dies führt dazu, dass sehr einfache Objekte innerhalb von Kernels verwendet werden können.Hauptsächlich beschränkt sich dies auf einfache Plain Old Java Object (POJO)s mit Getter-und Setter-Methoden. Sobald komplexere Objekte verwendet werden, z.B. zur Kapselung einerlinearisierten Matrix, ist der Quelltext in der erstellten Implementierung nicht mehr valide. DieUrsache besteht darin, dass bei der Code-Generierung über die einzelnen Instruktionen gelaufenwird und diese zum Schreiben des Quelltextes verwendet werden. Wird nun ein Methoden-aufruf geschrieben, der innerhalb eines Objektes gestartet wird, so fehlt die Referenz auf denAufruf-Graphen. Es kann nicht ermittelt werden, wo die Objektmethode anzusiedeln ist, bzw.wie die umbenannte Methode, die aufgerufen werden soll, heiÿt. Entsprechend können keineFeld-Umbenennungen vorgenommen werden, womit der resultierende Quelltext nicht ausgeführtwerden kann.

Um hier Abhilfe zu scha�en müssten im Vorfeld der Generierung die Bytecode-Instruktionengeändert werden. Allerdings könnte in diesem Fall auch direkt der Bytecode verändert werden, umdie Objekte komplett zu entfernen und die bisherige Code-Generierung zu belassen. Entsprechendwurde die Implementierung an dieser Stelle gestoppt und als Aufgabe für zukünftige Arbeitenbelassen.

Page 100: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

6.3. Abbildung der neuen API auf den Fluid-Simulator 93

6.3 Abbildung der neuen API auf den Fluid-Simulator

Mit der neuen API kann der Quelltext wesentlich übersichtlicher gestaltet werden. Die Strukturder Kernels ist in Abbildung 6.6 dargestellt. Weitere Simulationsklassen, wie z.B. der FluidSolver,wurden intern verändert, zugunsten der Übersichtlichkeit aber nicht in die Abbildung aufgenom-men.

Kernels können nun vollständig voneinander getrennt in verschiedene Pakete und Vererbungs-hierarchien aufgeteilt werden. Eine bisher notwendige globale Variable zur Unterscheidung derKernel-Aufrufe entfällt, da die Entscheidung, welcher Einsprungpunkt aufgerufen wird, vomKernelRunner übernommen wird. Dieser wird im Rahmen der Simulation einmalig instanziiertund dient anschlieÿend als Proxy für alle Kernel-Aufrufe.

Die Instanziierung geschieht in der Klasse FluidSolver. Zusätzlich werden hier Instanzen der, fürdie Berechnung der Navier-Stokes-Gleichungen nötigen, Kernels gehalten. Ein Simulationsschrittwird, entsprechend der bisherigen Implementierung, über eine ö�entliche #step-Methode von derOber�äche bzw. von einem Benchmark, aufgerufen.

Ebenfalls im FluidSolver enthalten ist eine Methode, die einen beliebigen Kernel mit einergeg. Anzahl an Threads ausführt. Diese ist darauf zurückzuführen, dass die Kernels zur Visu-alisierung von einem Enum und nicht vom FluidSolver gehalten werden. Auch die Logik zurAusführung der entsprechenden Kernels �ndet sich in Attributen des Enums. Um Kernels aufdem KernelRunner auszuführen muss der FluidSolver deswegen die Ausführung von externenKernels unterstützen. Zusätzlich müssen für die Visualisierung Methoden zum Schreiben bzw.Lesen der Arrays in den FluidSolver eingefügt werden, da nur dieser den KernelRunner refe-renziert und damit indirekten Zugri� auf die Speicherbereiche der GPU besitzt.

Die Kernel-Klassen enthalten jeweils Referenzen auf alle benötigten Arrays. Diese sind als Klassen-Attribute dargestellt und werden entsprechend auf jeweils identische Speicherbereiche der GPUabgebildet. In einigen Fällen ist eine zusätzliche Vererbungshierarchie einzelner Kernels sinnvoll,da alle Unterklassen die gleichen Methoden oder Attribute verwenden. Teilweise enthält auchdie Basisklasse alle Attribute und den Kernel, wobei sich nur der Konstruktor und dessen Ar-gumente der einzelnen Implementierungen unterscheidet. Im Augenblick funktioniert dies nur,soweit alle Attribut der Basisklasse mindestens protected sind. Private Attribute werden vonAparapi nicht in den generierten OpenCL-Quelltext einbezogen, womit der resultierende Quell-text nicht kompilierbar ist.

Page 101: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

6.3. Abbildung der neuen API auf den Fluid-Simulator 94

aparapi

kernel.advection

kernel.pressure

kernel.visualization

kernel

KernelKernelRunner

AdvectParticlesKernel

AdvectVelocitiesSTAMKernel

PressureDivergenceKernel

PressureGradientKernel

PressureSTAMKernel

PressureVelocityUpdateKernel

SwapPressureKernel

DivergencesParticleFieldKernel

VerticesParticleFieldKernel

GradientVerticesKernelLengthBasedVerticesKernel

ParticleVerticesKernel

BaseVisualizationKernel

PressureParticleFieldKernel

VelocityVerticesKernel

AddSourceVelocityKernel

DiffusionKernel

SwapVelocitiesKernel

Abbildung 6.6: Struktur des mit der neuen API implementierten Simulators

Page 102: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

6.4. Geschwindigkeitsvergleich zum nativen Simulator 95

6.4 Geschwindigkeitsvergleich zum nativen Simulator

Um die Portierung abschlieÿend bewerten zu können wird ein Benchmark durchgeführt, der dienative Implementierung mit der Aparapi Implementierung anhand der Ausführungszeiten verg-leicht. Dazu wird jeweils ein Modell mit zwischen 1 und 320.000 Zellen initialisiert. Anschlieÿendwerden 1.000 Simulationsschritte berechnet. Für jeden Schritt wird dies 50 Mal durchgeführt,wodurch die Standardabweichung im Vergleich zum arithmetischen Mittel bei rund 1% liegt.

Evaluationsumgebung Als Evaluationsumgebung wird, wie in den Anforderungen gefordert,ein Windows-Rechner verwendet. Für alle nicht GPU-Berechnungen stehen zwei Intel XeonProzessoren mit jeweils 6 Kernen und je 12 Hardware-Threads zur Verfügung. Zusätzlich ste-hen zur Berechnung 32 GB RAM bereit.Für GPU-Berechnung enthält der Evaluationsrechner 2 NVIDIA Tesla K20c mit je 13 SMs sowieeine Quattro K5000 mit 9 SMs zur Verfügung. Zur Geschwindigkeitsvergleich wird jeweils eineTesla K20c verwendet.

Zeitmessung Um in der nativen Zeitmessung die Ausführungszeit in höchstmöglicher Präzi-sion messen zu können, kommt der sog. QueryPerformanceCounter zum Einsatz [Que13]. Dieclock_gettime-Funktion kann hier nicht verwendet werden, da diese in Windows nicht zur Ver-fügung steht. Der QueryPerformanceCounter wird basierend auf einer bestimmten Taktfrequenzpro Zeitschritt inkrementiert. Durch den geg. Umstand, dass die Taktfrequenz sich während derSystemausführung nicht ändert, kann die Ausführungszeit mit höchstmöglicher Genauigkeit be-stimmt werden.Für die Java-Zeitmessung kommt wiederum die bereits in der Bibliotheksevaluation verwendeteSystem#nanoTime-Methode zum Einsatz.

Ergebnisgraph Es ergibt sich der in Abbildung 6.7 dargestellte Graph. Dieser enthält dreiKurven, wobei die rote Kurve für die native Implementierung des Fraunhofer Instituts, die grüneKurve für die ö�entliche vorhandene Aparapi-Bibliothek und die blaue Kurve für die in derArbeit angepasste Version steht.

Alle drei Kurven verlaufen dabei fast parallel. Insgesamt ist der Wert der ursprünglichen AparapiVersion im Gegensatz zur nativen Ausführung rund 4 Mal so hoch, die angepasste Version dagegennur rund 3 Mal. Der Grund für die schnellere Ausführung der angepassten Version liegt v.a. inder Speicherung des erstmalig berechneten Device#best Wertes, was, wie bereits beschrieben,eine Senkung der Ausführungszeit um rund 30% ausmacht.

Im Vergleich zu den bisherigen Evaluationsfällen stellt die Flüssigkeitssimulation ein wesentlichkomplexeres Umfeld mit einer Kombination aus sehr unterschiedlichen Anforderungen dar. Zwarspiegelt das Ergebnis die Graphen der Evaluation ungefähr wieder, so ist doch ein wesentlichhöherer Overhead vorhanden. Vermutlich lässt sich dieser auf den Zusatzaufwand der JNI-Methodenaufrufe zurückführen, wobei dieser durch die hohen Anzahl an Kernel-Aufrufen (32.000Aufrufe pro Evaluationsfall) deutlich ins Gewicht fällt.

Page 103: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

6.4. Geschwindigkeitsvergleich zum nativen Simulator 96

Abbildung 6.7: Vergleich der Ausführungszeiten zwischen nativer und Aparapi Ausführung amBeispiel des Fluid-Simulators

Page 104: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

Kapitel 7

Fazit

Abschlieÿend werden die erreichten Ziele, noch zu implementierende, zukünftige Verbesserungenfür die Portierung als auch für Aparapi sowie zukünftige Entwicklungen in Bezug auf die GPU-Programmierung vorgestellt.

7.1 Erreichte Ziele

Im Rahmen der Arbeit wurden verschiedene Bibliotheken zur Java-GPU-Abstraktion vorgestellt.Dabei wurde aufgezeigt, dass diese funktionsfähig eingesetzt werden können. Allerdings sinddie meisten Bibliotheken von einem produktiven Einsatz noch weit entfernt. Hier machen sichFehler in Bezug auf die Code-Generierung, und deren Fehleranfälligkeit, sowie der Möglichkeitzur Fehlersuche, deutlich bemerkbar.

Gleichzeitig wurde nachgewiesen, dass die Ausführungsgeschwindigkeit, unter Auslassung derCode-Generierung und -Kompilierung, nicht zwangsläu�g langsamer als eine native Ausführungsein muss. Für Einzelaufgaben waren im Vergleich zur nativen Ausführung verschiedene Biblio-theken fast gleich schnell. Natürlich fällt hier weiterhin ein Overhead an, der u.a. auf die erhöhteAbstraktion zurückzuführen. Der Unterschied ist jedoch meist vergleichsweise gering. Allerdingstri�t diese Aussage nicht auf alle Bibliotheken zu. Vor dem Einsatz einer der verfügbaren Biblio-theken sollte genau evaluiert werden, wie schnell diese einen konkreten Anwendungsfall abarbei-ten kann. Ansonsten kann es, wie für Delite und Rootbeer nachgewiesen, sogar zu, im Vergleichzur seriellen Ausführung, langsameren Ausführungszeiten kommen.

Insgesamt steckt die Java-Abstraktion noch in den Kinderschuhen. Delite ist aufgrund der zurDomäne sehr nahen Programmierung der Wunschansatz der abstrakten GPU-Programmierung.Alle anderen Bibliotheken lehnen sich sehr stark an das GPU-Programmiermodell an, womit dieKomplexität der Programmierung nicht gekapselt wird und eine bessere Quelltext-Struktur nichtmöglich ist.

Aparapi wurde in dieser Hinsicht in der Arbeit um mehrfache Einsprungpunkte erweitert, womitder resultierende Java-Quelltext wesentlich besser strukturiert werden kann. Hier besteht auchin Zukunft noch weiteres Potential, insbesondere auch in Bezug auf die in dieser Arbeit ange-sprochene Implementierung der Verwendung von Java-Objekten auf der GPU.

Page 105: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

7.2. Ansatzpunkte für zukünftige Verbesserungen 98

7.2 Ansatzpunkte für zukünftige Verbesserungen

Aparapi ist, wie bereits beschrieben, die Bibliothek, die am ehesten als produktiv einsetzbarzu bezeichnen ist. Unter Verwendung der Bibliothek wurde nachgewiesen, dass auch ein kom-plexer Anwendungsfall, wie die Flüssigkeitssimulation, abstrakt beschrieben werden kann undgleichzeitig anschlieÿend besser les-, wart- und testbar ist. Trotz des Leistungsabfalls auf rund1/3 der Ausführungsgeschwindigkeit ist der Anwendungsfall Fluid-Simulation noch in Echtzeitberechenbar. Erst bei groÿen Grid-Gröÿen macht sich der Overhead bemerkbar.

Innerhalb der Bibliothek muss zukünftig stark an der Testabdeckung gearbeitet werden. Im Build-Skript der Bibliothek ist keine automatische Testausführung vorhanden, womit die wenigen, imAugenblick vorhandenen Tests, unbemerkt fehlschlagen können. Für die native Implementierungsind überhaupt keine Tests vorhanden, womit eine Erweiterung dieses Teils sehr schwierig ist,da jederzeit ein Feature beschädigt werden kann. Vorbild sollte hier die Bibliothek Rootbeer mitstarkem Fokus auf Unit-Tests sein. Auf Grund der OpenCL-Abhängigkeit dürften Tests, die nurfür die native Umgebung konzipiert sind, schwierig werden. Zur Sicherstellung des Funktion-ierens der nativen Features wären allerdings bereits automatisch ausgeführte Beispiele, z.B. dieFourier-Transformation oder die Matrix-Multiplikation, ausreichend. Als Integrationstest konzip-iert würden diese zumindest teilweise einzelne Unit-Tests ersetzen.

Gleichzeitig wäre eine direkte Einbindung von OpenGL wünschenswert. Aufgrund der Trennungder Speicherbereiche von Aparapi und OpenGL müssen die Daten jeweils als Umweg zurückauf den Host kopiert werden, um anschlieÿend wieder zur Darstellung zurück auf das Deviceverschoben zu werden. Eine direkte Integration würde die Laufzeit wesentlich verkürzen und diedarstellbare Frame-Rate deutlich erhöhen. Gleichzeitig steckt hinter diesem Feature durchausAufwand, da hierzu die OpenGL-Java-Bibliothek, also z.B. JOGL, ebenfalls angepasst werdenmuss.

Bereits angesprochen wurde die wünschenswerte Erweiterung von Aparapi für Objektorien-tierung. Der Aufwand steckt hier vor allem in der Evaluation verschiedener Bytecode Biblio-theken sowie in der Portierung der bisherigen Funktionalität zum Einlesen des Bytecodes auf dieneue Bibliothek. Die Erstellung bzw. Ausführung eines Benchmarks ist hier zwangsläu�g nötig,da die neue Bytecode-Bibliothek die Ausführungszeit nicht steigern sollte. Basierend darauf kanndas Inlining verhältnismäÿig einfach eingebaut werden. Als Gratis-E�ekt dabei ergibt sich eingröÿeres Refactoring des Aparapi Quelltextes, woraus sich eine erheblich klarere Struktur ergebendürfte.

Darüber hinaus besteht auch Potenzial im Einbau von optimierten Bibliotheken, wie z.B. cuBLASfür CUDA bzw. clMath für OpenCL. Der native Simulator verwendet diese Bibliothek z.B. zurBerechnung der Matrix-Multiplikationen beim Verfahrens der konjugierten Gradienten. Hier isteine deutliche Geschwindigkeitssteigerung möglich. Zwar bietet Aparapi die Möglichkeit der Ein-bindung von nativen Kernels, so bezieht sich dies nur auf reine OpenCL-Dateien. Die Möglichkeitzur Einbindung externer Bibliotheken ist nicht gegeben. Genauso fehlt eine mögliche Abstraktion,um die Bibliotheken elegant in eigenen Kernels nutzen zu können.

Schlieÿlich wäre eine Optimierung der JTP-Ausführung sinnvoll. Interessant wäre dabei, ob derOverhead eingeschränkt werden kann, sodass auch diese Art der Ausführung konkurrenzfähigwird. Zum Testen ist der Ausführungsmodus zwar ausreichend, zum produktiven Einsatz je-

Page 106: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

7.3. Ausblick zur GPU-Entwicklung 99

doch viel zu langsam. Ein erster Schritt wäre hier ein ausführliches Pro�ling, um für bestimmteAnwendungsfälle, wie z.B. dem beschriebenen Jakobi-Verfahren, die Ursache für den Overheadzu ermitteln. In diesem Testfall ist Aparapi deutlich langsamer als die Host-Ausführungen vonDelite und Rootbeer.

Ein weiterer Ansatzpunkt ist die Prüfung, warum der AMD-Pro�ler für Aparapi nicht funktion-iert. Pro�ling ist ein sehr sinnvolles Feature, um die Bibliothek weiter zu optimieren bzw. dengenerierten Quelltext anhand der resultierenden Ausführungsgeschwindigkeit zu testen. OhnePro�ling ist nur eine Sicht- bzw. Funktionalitätsprüfung möglich. Mit Pro�ling kann explizitangegeben werden, wo eventuell ein Flaschenhals vorhanden ist.

Auch für den portierten Simulator wäre eine erhöhte Testabdeckung wünschenswert. Zwar wurdeeine automatische Testausführung durch Verwendung von Gradle eingebaut und auch rudimentärerste Tests für die Portierung erstellt, so reichen diese bei weitem nicht für einen produktivenEinsatz aus. Dies liegt v.a. daran, dass der Fokus dieser Arbeit auf dem Aufzeigen der Möglichkeitder Erstellung von Tests und eben nicht auf einer vollständigen Testabdeckung liegt.

7.3 Ausblick zur GPU-Entwicklung

Die bereits in der Einführung genannten Gründe für eine Steigerung der Anforderungen anverfügbare Rechenleistung wird auch in Zukunft die Verwendung von GPUs weiter befeuern.Dies führt auch dazu, dass die Gra�kkarten immer schneller an die wachsenden Erwartungenangepasst werden.

Ein Beispiel hierfür ist die von AMD vorgestellte Heterogeneous UniformMemory Access (hUMA)Speicherarchitektur, die es der GPU ermöglicht, direkt, ohne weitere Kopiervorgänge, auf denSpeicher der GPU zuzugreifen. Auf diese Weise wird die gröÿte Einschränkung der Program-mierung von GPUs umgangen und ö�net weitere Einsatzgebiete der Verwendung von Gra�k-karten [hum13][S. 9]. Interessanterweise hat NVIDIA bisher keine Antwort auf die von AMDdadurch gestellte Herausforderung verö�entlicht.

Durch die mit hUMA in Kombination mit HSAIL einhergehende Vereinfachung der GPU-Pro-grammierung ist auch ein Schub in Richtung GPU-Abstraktion für weitere Sprachen auÿerhalbvon OpenCL und CUDA-C zu erwarten. Die Unterstützung für HSAIL in Aparapi ist bereitsein deutlicher Hinweis hierauf. Die Entwickler bei AMD verwenden dabei bereits Geräte mithUMA-Unterstützung [apa13a][Issue 128].

Gleichzeitig wird auch mit dem Projekt Sumatra die Java-GPU-Abstraktion in eine breite Öf-fentlichkeit getragen. Soweit die Implementierung fertiggestellt ist wird die Verwendung vonGPUs durch parallele Listen o.ä. auch Einzug in normale Applikation �nden, in denen bisher derOverhead der Verwendung von Gra�kkarten zu groÿ war.

Sumatra wird allerdings wohl nie Bibliotheken wie Aparapi oder Rootbeer komplett ablösenkönnen. Sumatra konzentriert sich augenscheinlich mehr auf die Parallelisierung von Operationenauf Listen als um die Abbildung komplexer GPU-Spezi�ka wie spezielle Speicherbereiche oderThread- und Blöckgröÿen. Diese sind jedoch für eine performante Ausführung auf der Gra�kkarteerforderlich. Dementsprechend wird Aparapi höchstens als unterliegende Bibliothek von Sumatrazum Einsatz kommen, wobei Sumatra nie als vollwertiger Ersatz bestehen kann. Gerade die

Page 107: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

7.3. Ausblick zur GPU-Entwicklung 100

Möglichkeit durch Callbacks den erzeugten Quelltext zu optimieren deutet eventuell auch aufdie Möglichkeit einer domänenspezi�schen GPU-Abstraktion an, wie sie ebenfalls bei Delite zumEinsatz kommt.

Page 108: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

Anhang A

Java-Native-Interface (JNI)

Viele der beschriebenen Abstraktions-Bibliotheken verwenden das JNI zum Zugri� auf die Gra-�kkarte durch die nativen CUDA- bzw. OpenCL-Bibliotheken.

Wie in Gra�k A.1 abgebildet ist das JNI Teil der virtuellen Maschine und ermöglicht den di-rekten Zugri� auf native Systembibliotheken bzw. selbst geschriebenen C oder C++ Quelltext.Gleichzeitig ist aber auch der umgekehrte Weg, also der Aufruf von Java-Funktionen aus nativenFunktionen heraus über ein Invocation-Interface möglich [Lia99][S. 5].

Zugri� auf native Funktionen

Um auf native Funktionen zugreifen zu können müssen mehrere Schritte befolgt werden:

Die Funktion wird zuerst mit dem Schlüsselwort native markiert. Mit javah -jni wird zuder zugehörigen Klasse eine C-Header-Datei generiert. Diese kann in einer zugehörigen C-Dateiimplementiert und zu einer Bibliothek kompiliert werden (*.so in Linux, *.dll in Windows).Anschlieÿend kann die Bibliothek mit System#loadLibrary() geladen werden [Lia99][S. 11,13].Java sucht nach der entsprechenden Bibliothek in der java.library.path System-Property.Diese enthält standardmäÿig auch den Inhalt der LD_LIBRARY_PATH (Linux) bzw. PATH Variable(Windows) [Lia99][S. 17].

JNI-Referenztypen

JNI unterscheidet verschiedene Referenztypen zum Zugri� auf Java-Objekte:

Lokale Referenzen Diese Art der Referenz ist nur innerhalb eines JNI-Aufrufs gültig. An-schlieÿend wird sie automatisch freigegeben. Entsprechend sollte die Referenz nicht inner-halb der nativen Umgebung vorgehalten werden. Wird als Parameter einer nativen Funktion

Java Applikation Native Applikation/Bibliothek

Java virtuelleMaschine

JNI

System-umgebung

Abbildung A.1: Grundsätzliche JNI-Struktur [Lia99][S. 5]

Page 109: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

102

ein Java-Objekttyp spezi�ziert, so wird immer eine lokale Referenz übergeben [Lia99][S.156].

Globale Referenzen Globale Referenzen müssen dagegen explizit vom Programmierer erstelltund anschlieÿend auch wieder freigegeben werden. Die JVM kümmert sich also nicht umdie entsprechende Verwaltung [Lia99][S. 156].

Schwache globale Referenzen Diese letzte Referenz-Typ entspricht der globalen Referenz, bisauf dem Umstand, dass das unterliegende Objekt durch den Garbage Collector aufgeräumtwerden darf. Der Wert der Referenz entspricht in diesem Fall NULL.

In Aparapi werden globale Referenzen auf die nötigen Kernel-Argumente vorgehalten, sodassdiese auch über Kernel-Aufrufe hinweg gültig sind.

Page 110: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

Anhang B

Feature-Vergleich der Bibliotheken

JCuda Java-GPU Rootbeer Aparapi DeliteSprache Java Java Java Java Scala

Plattform-UnterstützungLinux ja ja ja ja jaWindows ja nein ja (mit Fehlern) ja nein *3)

Parallelisierungexplizit (manuelle Kernels) ja nein ja ja neinimplizit (aut. Erkennung) nein ja nein nein jaAutomatische Code-Umwandlung nein ja ja ja jaStaging erforderlich nein ja ja nein ja

SpeicherverwaltungAut. Allokation und Freigabe nein ja ja ja jaMehrere Kernel Aufrufe ohne ern. Kopieren ja nein nein ja ja

EntwicklungDokumentation + - + ++ ++Beispiele ++ + + ++ +Lernkurve + *4) + ++ �Entwicklungs-Geschwindigkeit + *4) - ++ -Testbarkeit + � - ++ �Entwicklungsumgebung ++ ++ ++ ++ �Zukünftige Weiterentwicklung ja nein ?? ja jaAufruf von Java-Projektmethoden ja ja ja ja nein

Page 111: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

104

Native Implementierung der Kernels möglich ja nein nein ja neinIn bestehendes Projekt einbaubar ja ja ja ja nein

FehlersuchePro�ler CUDA ja *4) ja nein jaKomplexität der Bibliothek niedrig mittel hoch mittel sehr hochDokumentation der Bibliothek- JavaDoc ++ ++ - + -- Architektur-Dokumentation � ++ � - ++- Code-Leserlichkeit + ++ - + -Aktive Community + � + + +Leserlichkeit des generierten Codes *1) *2) *1) ++ �Verständlichkeit der Fehlermeldungen ++ *2) - ++ �

Ausführungs-ModiGPU- OpenCL nein nein nein ja ja- CUDA ja ja ja nein nein *3)Java-Thread-Pools nein nein ja ja ja

Unterstützung von . . .OpenGL ja nein nein nein neinPrimitive Datentypen ja ja ja ja jaEindimensionale Arrays ja ja ja ja jaMehrdimensionale Arrays nein ja ja nein *5) jaArray-Length Attribut nein ja ja ja *3) jaObjekte in äuÿeren Klassen nein *4) ja nein jaObjekte auf der GPU nein ja ja ja jaStrings auf der GPU nein nein ja nein jaExceptions auf der GPU nein nein ja nein / indirekt neinMehrdimensionale Grid-Gröÿen ja ja (implizit) nein ja *1)API zur Geschwindigkeits-Messung ja nein ja (nur GPU) ja jaBLAS-Operationen ja (externe Bib.) nein nein nein ja

Page 112: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

105

Shared-Memory auf der GPU ja nein ja ja *1)Shared-Memory Unterstützung in Java *1) *1) ja ja jaExplizite Spezi�kation der Grid-Gröÿen ja nein ja ja neinMehrere Gra�kkarten / -kerne ja nein nein ja ja

Geschwindigkeit ++ *1 � ++ �

Gesamteindruck + - � + -

Legende*1) tri�t nicht zu*2) Bibliothek funktioniert zur Zeit nicht*3) Zur Zeit nicht lau�ähig, o�ziell unterstützt*4) unbekannt*5) mittlerweile implementiert

Page 113: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

Anhang C

Pro�ling im JNI- und GPU-Umfeld

Pro�ling stellt das Mittel der Wahl dar, um in Anwendungen Flaschenhälse in Bezug auf sehrunterschiedliche Themen wie Ausführungszeiten von Einzelmethoden, Speicherverbrauch, Fest-plattenzugri� oder Speicher-Transferzeiten und -durchsatz zu �nden [Shi03][S. 3]. EntsprechendeWerkzeuge variieren auf den unterschiedlichen Plattformen, genau wie die zu betrachtenden li-mitierenden Faktoren der Ausführungsgeschwindigkeit. Deswegen ist das nachfolgende Kapitelin Abschnitte für die Java, die CUDA und die C++-Ausführung aufgeteilt.

Flaschenhälse entstehen dabei durch noch bestehende Applikationsfehler, falsche Datenstruk-turen oder durch fehlendes Verständnis von Operationen, wie z.B. Netzwerkzugri�e oder Hardware-Operationen. Durch einen Pro�ler lassen sich diese Flaschenhälse in übersichtlicher Form darstellenund verhältnismäÿig schnell beheben.

C.1 Pro�ling in Java

Die JVM stellt zur Geschwindigkeitsmessung bzw. zum Au�nden von Flaschenhälsen mit HPROFein Werkzeug zur Verfügung [hpr13]. Dieses ermöglicht es den Speicher und die Geschwindigkeitvon Methoden zu überwachen.

Im Anwendungsfall der Optimierung von Aparapi bzw. der Fluid-Simulation ist insbesonderedie Ausführungsgeschwindigkeit der entscheidende Faktor, da Aparapi in Bezug auf verfügbarenSpeicher im Normalfall nicht limitiert ist. Dagegen bestimmt die CPU-Geschwindigkeit wesentlichdas Ergebnis.

Bei der Ausführung muss zur Erstellung des Geschwindigkeitspro�ls die Kommandozeilenoption-J-agentlib:hprof=cpu=samples übergeben werden. Die nachfolgend generierte Datei enthältanschlieÿend u.a. die Ausführungszeit jeder ausgeführten Methode in Prozent, die Anzahl derMethodenaufrufe sowie den Methodennamen selbst.

Intern wird das Pro�l durch regelmäÿigen Abruf des Stack-Zustandes der ausführenden Threadserreicht. Die Traces werden analysiert, aufgespalten und die eingetragenen Methoden zur Er-stellung des Pro�ls verwendet. Da hierfür ein kohärenter Stack-Zustand Voraussetzung ist, mussder Zugri� synchronisiert erfolgen und u.U. sogar der Thread in seiner Ausführung angehaltenwerden. Dies führt dazu, dass die Ausführungszeiten innerhalb eines Pro�lers wesentlich von derdirekten Ausführung ohne Pro�ler abweichen [Shi03][S. 28]. Diese Methode des Pro�ling wirdauch Sampling genannt.

Ein anderer Ansatz ist die sog. Instrumentierung. Statt in regelmäÿigen Intervallen den Stackzu analysieren wird vor der Ausführung Bytecode eingefügt, der zur Messung der genauen

Page 114: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

C.2. Pro�ling von JNI-C++-Quelltext 107

Ausführungszeiten bzw. Methodenaufrufe dient. Im Fall des Sampling ist die Anzahl der Me-thodenaufrufe sowie deren Ausführungszeiten nur eine Schätzung, da nur in Intervallen dieThread-Zustände ausgelesen werden. Bei der Instrumentierung wird der tatsächliche, genaueWert gemessen, wobei die Ausführungszeit im Gegensatz zum Sampling deutlich sinkt [hpr13].

Zur Au�ndung der Device#best-Flaschenhalses war auf Grund des hohen Anteils der #best-Methode an der Ausführungszeit Sampling ausreichend.

Um die Kommandozeilenoptionen nicht händisch verwenden zu müssen und zusätzlich einegraphische Auswertung verwenden zu können, können zusätzlich graphische Werkzeuge, wie dasin dieser Arbeit verwendetet Tool VisualVM, genutzt werden.

C.2 Pro�ling von JNI-C++-Quelltext

Da die Erweiterung des nativen Anteils von Aparapi auf Grund der besseren Unterstützung vonC++ in Visual Studio stattfand, wurde auch für das Pro�ling die gleiche IDE verwendet. ImGegensatz zu Java besitzt Visual Studio hierfür keine eingebaute Funktionalität. Stattdessenkann eine Erweiterung, die sog. Visual Studio Pro�ling Tools, nachinstalliert werden, die dengewünschten Funktionsumfang enthält.

Pro�ling unterliegt in Windows einer strikten Gruppenrichtlinie. Bevor die Aktion entsprechenddurchgeführt werden kann muss der Pro�ling-Treiber, vsperfcmd, von einem Administrator ge-startet werden. Zu beachten sind dabei u.U. nötige Parameter, die Entwicklern die Verwendungdes Treibers ermöglichen.

Ist der Treiber geladen, so kann in Visual Studio ein Pro�ling-Lauf gestartet werden. Die Werte,die gemessen werden können, sind äquivalent zu den HPROF-Optionen. Entsprechend könnender Speicherverbrauch sowie die Ausführungszeiten von Funktionen mit Hilfe von Sampling undInstrumentierung gemessen werden. Eine zusätzliche Option, Concurrency, bezieht sich auf dieMessung von Wartezeiten von Threads [vis13].

Zur Auswertung steht wiederum eine graphische Ober�äche zur Verfügung, deren Werkzeuge denselben Umfang wie die Ober�äche von VisualVM haben. Eine zusätzliche, sehr nützliche, Optionbezieht sich auf die Ausblendung von Quelltext auÿerhalb des eigenen Projektes.

Zum Pro�ling muss die Applikation gestartet werden. Allerdings �ndet im Fall von Aparapikein Start einer C++-Methode statt, da der native Anteil nur von Java aus aufgerufen wird.Abhilfe scha�t hier das Feature, den Pro�ler an einen bestehenden Prozess anhängen zu können.Dementsprechend kann die Applikation zuerst im Debugging-Modus in Java gestartet werdenund vor dem Aufruf der zu messenden JNI-Methode ein Breakpoint gesetzt werden. Wird diesererreicht, so ist auch der Prozess au�ndbar und kann genutzt werden, um innerhalb von VisualStudio den Pro�ler an den Java-Prozess anzuhängen. Anschlieÿend wird die Ausführung in Javafortgesetzt, der C++-Code ausgeführt und die Statistik entsprechend erstellt.

Entsprechend funktioniert auch das Debuggen von JNI-Anwendungen in Visual Studio. Auchhier wird zuerst der Debugger in Java gestartet, auf einen Breakpoint gewartet, der Debuggerin Visual Studio an den Java-Prozess gehängt und die Ausführung in Java fortgesetzt. VisualStudio erkennt das Erreichen einer Breakpoints im C++-Code und stoppt die Ausführung an

Page 115: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

C.3. Pro�ling in CUDA 108

der entsprechend richtigen Stelle. Allerdings ist darauf hinzuweisen, dass dieser Ansatz nur unterVerwendung einer in Visual Studio generierten Bibliothek mit Debug-Symbolen funktioniert.Ansonsten wird der native Code als nicht äquivalent zum in Visual Studio geladenen Quelltexterkannt und der Debug-Modus ignoriert.

C.3 Pro�ling in CUDA

Im Gegensatz zum Pro�ling in Java und C++ interessieren bei der Bewertung der GPU-Ausführungmehrere Kriterien:

Natürlich ist weiterhin die Ausführungszeit ein wichtiges Kriterium. Allerdings ist hier nichtunbedingt die Bewertung auf Basis einzelner Funktionen interessant, sondern die Ausführungs-zeit einzelner Kernels sowie deren Kon�guration, d.h. die Anzahl an Blöcken und deren Gröÿe.Abgesehen davon ist der Speicher ein wichtiges Kriterium. Relevant für die Geschwindigkeit istdie Menge an zu kopierenden Daten sowie der entsprechende Durchsatz. Dieser hängt u.a. auchvon der Art des Quellspeichers ab. Im Abschnitt 5.4.3 wird genauer erklärt, warum die nativeSimulation z.B. Page-Locked-Memory nutzen sollte und nicht normalen Speicher, der u.a. aufdie Festplatte ausgelagert werden kann.

NVIDIA stellt für CUDA mit dem auf dem Kommandozeilenprogramm nvprof basierendenVisual Pro�ler das nötige Werkzeug zur Messung der oben genannten Werte zur Verfügung.Eine mögliche Anzeige ist mit dem Verfahren der konjugierten Gradienten in Abbildung C.1dargestellt. In der Abbildung ist der Kopiervorgang von der CPU auf die GPU markiert. Rechtswird zusätzlich der Kopierdurchsatz angezeigt, in diesem Fall 5,7 GB/s. Die zugehörige Zeit ist inder Detail-Anzeige im unteren Teil der Abbildung dargestellt. Da für das Verfahren mehrere Ker-nels ausgeführt werden, existiert für jeden Kernel eine eigene Spur, in der die einzelnen Kernel-Aufrufe abgebildet sind. Zusätzlich wird angezeigt, wie viel Prozent ein Kernel zur gesamtenAusführungszeit beiträgt.

Allein mit diesem Mittel können bereits verschiedene Speicherarten verglichen bzw. unnötigeKernel-Aufrufe, wie sie z.B. in Rootbeer bestehen, erkannt werden. Um auch weitere Flaschen-hälse zu �nden analysiert der Pro�ler das Verhalten des Programms und zeigt evtl. vorhan-dene Probleme an. Dies betri�t z.B. divergierende Ausführungspfade unterschiedlicher Threads,langsamen Kopierdurchsatz, fehlende Überlappung von Kopiervorgängen und Ausführung oderSpeicherbereiche, deren Kopiervorgänge den Durchsatz aufgrund ihrer kleinen Gröÿe signi�kantverlangsamen.

Der Visual Pro�ler kann ebenfalls zur Analyse der Java-Abstraktionsbibliotheken verwendetwerden. Dazu wird das Programm java mit den entsprechenden Kommandozeilenargumentenzum Start der Java-Anwendung kon�guriert und ausgeführt. Der Zugri� auf die GPU wird dabeierkannt und gemessen.

AMD stellt mit dem App Pro�ler für OpenCL ein ähnliches Programm zur Verfügung [amd13].Dies ist insbesondere wichtig, da der Visual Pro�ler von NVIDIA keine OpenCL-Anwendungenanalysieren kann. Eine visuelle Darstellung ist hier allerdings nur mit dem mitgelieferten Vi-sual Studio Plug-In für Windows möglich. Unter Linux bleibt nur die direkte Verwendung derKommandozeilenober�äche. Für Aparapi konnten beide Varianten weder unter Linux noch unter

Page 116: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

C.3. Pro�ling in CUDA 109

Windows produktiv zum Einsatz gebracht werden. Wird, genau wie bei NVIDIA, java als Aus-führungsprogramm spezi�ziert und entsprechende Argumente angegeben, so passiert schlichtnichts. Die einzige Ausgabe beschreibt, dass der Pro�ler initialisiert wurde. Zusätzlich steigt dieCPU-Auslastung auf 100%, wobei sich das Programm anschlieÿend nicht mehr beendet.

Abbildung C.1: Anzeige des NVIDIA Visual Pro�ler für die native Ausführung des Verfahrensder konjugierten Gradienten

Page 117: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

Abkürzungsverzeichnis

API Application Programming Interface. 2, 27, 30�32, 35, 37, 39, 41, 49, 50, 53, 55, 60, 66, 71,73, 74, 77, 78, 80�83

BLAS Basic Linear Algreba Subprograms. 30, 59, 69, 73, 84

CAS Compare-and-Swap. 46

CPU Central Processing Unit. 1�3, 6, 25�27, 29, 37�39, 41, 43, 49�51, 57�59, 67, 69, 84

CRI Compiler Runtime Interface. 62

CUDA Compute Uni�ed Device Architecture. 2�6, 9, 10, 12�16, 24�27, 30�33, 35, 38, 42�44,46, 50, 51, 53, 56, 59, 60, 62, 65, 82, 84, 90

DEG Delite Execution Graph. 56, 57, 65, 66

DSL Domain Speci�c Language. 51�55, 58, 69

FPGA Field Programmable Arrays. 14

GCC GNU Compiler Collection. 13

GPGPU General Purpose GPU. 2, 3, 5

GPU Graphics Processing Unit, Gra�kprozessor. 1�8, 13, 14, 25�31, 34�39, 41, 43, 44, 46, 47,49�51, 57�59, 61�64, 66, 67, 69, 71, 73, 76, 77, 79, 81, 82, 84

HLC High Level Compiler. 62

HSAIL Heterogeneous System Architecture Intermediate Language. 61, 62

IDE Integrierte Entwicklungsumgebung. 50, 59, 61

IR Intermediate Representation / Zwischenrepräsentation. 44, 53�57, 62

ISA Instruction Set Architecture. 62

JIT Just-In-Time. 14, 42

JNI Java Native Interface. 5, 31�33, 35, 37, 39, 47, 64, 71, 77, 78, 90

JOGL Java Bindings für OpenGL. 41, 42

JSON JavaScript Object Notation. 56

JTP Java-Thread-Pool. 76, 77

Page 118: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

Abkürzungsverzeichnis 111

JVM Java Virtual Machine. 62�64, 79

LMS Leightweight Modular Staging. 52, 53, 55, 56, 60

OpenCL Open Computing Language. 4, 6, 14�16, 27, 30, 36�39, 41, 42, 46, 53, 56, 59, 61,76�79, 81, 82, 90

OpenGL Open Graphics Library. 77

PTX Parallel Thread eXecution architecture. 14, 15, 31, 32, 61, 62

SBT Scala Build Tool. 60, 61

SIMD Single Instruction, Multiple Data. 7

SIMT Single Instruction, Multiple Thread. 6, 7

SM Streaming-Multiprozessor. 7, 9

SMT Simultaneous Multithreading. 1

SP Streaming-Prozessor. 7

SSA Static Single Assignment. 44

Page 119: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

Abbildungsverzeichnis

1.1 Vergleich der Entwicklung der Rechenleistung von CPUs und GPUs [cud13][S. 2] 2

3.1 Thread-Organisation [KWH10][S. 63] . . . . . . . . . . . . . . . . . . . . . . . . . 9

3.2 Aufteilung von Blöcken auf mehrere SM [cud13][S. 7] . . . . . . . . . . . . . . . . 10

3.3 Device Speichermodell [KWH10][S. 47] . . . . . . . . . . . . . . . . . . . . . . . . 12

3.4 Kompilierung einer CUDA Quelltext-Datei über die PTX -Zwischenrepräsentationzu einer binären cubin-Datei [nvc13][S. 30] . . . . . . . . . . . . . . . . . . . . . . 16

3.5 Übersicht der Begri�ichkeiten in CUDA und OpenCL [KWH10][S. 207, 209, 210,231] . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17

4.1 Ablauf eines Simulationsschrittes . . . . . . . . . . . . . . . . . . . . . . . . . . . 24

5.1 Typische Abarbeitung mehrerer Kernel Aufrufe auf den gleichen Daten in Java . 26

5.2 Parallele Reduktion (Beispiel für einen Block), vgl. [Par07][S. 8] . . . . . . . . . . 30

5.3 Lesen einer Klasse unter Nutzung des Visitor-Patterns, siehe [GHJV94][S. 331f] . 36

5.4 Schritte zur impliziten Schleifenparallelisierung in java-gpu . . . . . . . . . . . . . 36

5.5 Ausführung eines Aparapi-Kernels . . . . . . . . . . . . . . . . . . . . . . . . . . 42

5.6 Ablauf der Kompilierung in Rootbeer . . . . . . . . . . . . . . . . . . . . . . . . . 48

5.7 Ablauf der Ausführung in Rootbeer . . . . . . . . . . . . . . . . . . . . . . . . . . 51

5.8 Framework-Stack in Delite . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56

5.9 Ablauf ausgehend von Scala-Quellcode-Dateien bis hin zum ausgeführten Programm 59

5.10 Addition zweier Arrays mit unter Nutzung des Sumatra-Aparapi Prototyps . . . 64

5.11 Module der Evaluation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68

5.12 Ausführungszeiten zur Array-Inkrementierung bis 25.000 (links: GPU- und CPU-Ausführungszeiten, rechts: GPU-Ausführungszeiten bis 25.000, unten: Ausführungszeit-en bis 2.000.000 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73

5.13 Ausführungszeiten zur Matrix-Multiplikation (links: GPU und CPU-Ausführungszeiten,rechts: GPU-Ausführungszeiten) . . . . . . . . . . . . . . . . . . . . . . . . . . . 74

5.14 Ausführungszeiten zur Reduktion (links: GPU und CPU-Ausführungszeiten, rechts:GPU-Ausführungszeiten) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76

5.15 Ausführungszeiten zur Berechnung eines Mandelbrot-Fraktals (links: GPU undCPU-Ausführungszeiten, rechts: GPU-Ausführungszeiten) . . . . . . . . . . . . . 76

Page 120: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

Abbildungsverzeichnis 113

5.16 Ausführungszeiten zur Lösung von Poisson-Gleichungen unter Nutzung des Jakobi-Verfahrens (links: GPU und CPU-Ausführungszeiten, rechts: GPU-Ausführungszeiten) 77

5.17 Ausführungszeiten zur Lösung von Poisson-Gleichungen unter Nutzung des Ver-fahrens der konjugierten Gradienten (links: GPU und CPU-Ausführungszeiten,rechts: Ausführungszeiten bis zu einer Parametergröÿe von < 1.200) . . . . . . . . 78

6.1 Struktur des mit Aparapi implementierten Simulators . . . . . . . . . . . . . . . 81

6.2 Bildschirm-Foto des Simulators während der Simulation . . . . . . . . . . . . . . 84

6.3 Geänderte Aparapi-API um verschiedene Kernels auf den selben KernelRunnerabbilden zu können. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86

6.4 Struktur der nativen Klassen in Aparapi . . . . . . . . . . . . . . . . . . . . . . . 89

6.5 Auszug aus VisualVM zum Flaschenhals der Device#best-Methode . . . . . . . . 90

6.6 Struktur des mit der neuen API implementierten Simulators . . . . . . . . . . . . 94

6.7 Vergleich der Ausführungszeiten zwischen nativer und Aparapi Ausführung amBeispiel des Fluid-Simulators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96

A.1 Grundsätzliche JNI-Struktur [Lia99][S. 5] . . . . . . . . . . . . . . . . . . . . . . 101

C.1 Anzeige des NVIDIA Visual Pro�ler für die native Ausführung des Verfahrens derkonjugierten Gradienten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109

Page 121: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

Literaturverzeichnis

[amd13] AMD App Pro�ler. 07.2013. � http://developer.amd.com/tools-and-sdks/

heterogeneous-computing/archived-tools/amd-app-profiler/ (zitiert auf Seite108)

[apa13a] Aparapi. 06.05.2013. � https://code.google.com/p/aparapi (zitiert auf Seite 99)

[apa13b] Frequently Asked Questions Aparapi. 22.03.2013. � https://code.google.com/p/

aparapi/wiki/FrequentlyAskedQuestions (auf den Seiten 6, 43 und 44 zitiert)

[BLC02] Bruneton, Eric ; Lenglet, Romain ; Coupaye, Thierry: ASM: A code manipula-tion tool to implement adaptable systems. In: In Adaptable and extensible component

systems, 2002 (zitiert auf Seite 35)

[BMF07] Bridson, Robert ; Müller-Fischer, Matthias: Fluid simulation, SIGGRAPH

2007 course notes. ACM, 2007 (auf den Seiten 19 und 20 zitiert)

[BSL+11] Brown, Kevin J. ; Sujeeth, Arvind K. ; Lee, Hyouk J. ; Rompf, Tiark ; Chafi,Hassan ; Odersky, Martin ; Olukotun, Kunle: A Heterogeneous Parallel Frame-work for Domain-Speci�c Languages. In: Proceedings of the 2011 International Con-ference on Parallel Architectures and Compilation Techniques. Washington, DC,USA : IEEE Computer Society, 2011 (PACT '11). � ISBN 978�0�7695�4566�0, S.89�100 (auf den Seiten 5 und 61 zitiert)

[Cal10] Calvert, P.: Parallelisation of Java for Graphics Processors, Trinity College, Diss.,2010 (auf den Seiten 5, 37 und 38 zitiert)

[CCE01] Cline, David ; Cardon, David ; Egbert, Parris K.: Fluid Flow for the Rest of Us:Tutorial of the Marker and Cell Method in Computer Graphics / Brigham YoungUniversity. 2001. � Forschungsbericht (auf den Seiten 18, 19, 21 und 23 zitiert)

[clo13] clock_gettime(3) - Linux man page. 29.07.2013. � http://linux.die.net/man/3/

clock_gettime (zitiert auf Seite 70)

[cud13] NVIDIA CUDA C Programming Guide Version 5.5. 07.2013. � http://docs.

nvidia.com/cuda/pdf/CUDA_C_Programming_Guide.pdf (auf den Seiten 2, 7, 8,10, 11, 14, 15 und 112 zitiert)

[FC04] Fernando, R. ; Corporation, NVIDIA: GPU Gems: Programming Techniques,

Tips and Tricks for Real-Time Graphics. Addison Wesley Professional, 2004 (GPUgems / [NVIDIA]. Ed. by Randima Fernando). � ISBN 9780321228321 (auf denSeiten 19, 21 und 22 zitiert)

[fer09] Whitepaper - NVIDIA's Next Generation CUDA Compute Architecture. 2009 (aufden Seiten 3 und 8 zitiert)

Page 122: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

Literaturverzeichnis 115

[Fil10] Filinski, Andrzej: Monads in action. In: SIGPLAN Not. 45 (2010), Jan-uar, Nr. 1, 483�494. http://dx.doi.org/10.1145/1707801.1706354. � DOI10.1145/1707801.1706354. � ISSN 0362�1340 (zitiert auf Seite 59)

[gcn12] AMD GRAPHICS CORES NEXT (GCN) ARCHITECTURE. 06.2012. � http://

www.amd.com/br/Documents/GCN_Architecture_whitepaper.pdf (zitiert auf Seite7)

[GH99] Griffiths, David F. ; Higham, Desmond J.: MacCormack's Method for Advection-Reaction Equations. 1999. � Forschungsbericht (zitiert auf Seite 21)

[GHJV94] Gamma, E. ;Helm, R. ; Johnson, R. ;Vlissides, J.: Design Patterns - Elements of

Reusable Object-Oriented Software. Pearson Education, 1994. � ISBN 9780321700698(auf den Seiten 35, 36 und 112 zitiert)

[GJGB13] Gosling, J. ; Joy, B ; G., Steele ; Bracha, G.: The Java Language Speci�cation.Oracle, 2013 (zitiert auf Seite 68)

[GM96] Gosling, James ; McGilton, Henry: The Java Language Environment / SUNmicrosystems. 1996. � Forschungsbericht (zitiert auf Seite 4)

[Gra13a] Graal Project. 28.05.2013. � http://openjdk.java.net/projects/graal/ (zitiertauf Seite 65)

[Gra13b] Graal - A Bytecode Agnostic Compiler for the JVM. 28.05.2013. � http://

medianetwork.oracle.com/video/player/1113230360001 (zitiert auf Seite 65)

[hpr13] HPROF: A Heap/CPU Pro�ling Tool. 14.08.2013. � http://docs.oracle.com/

javase/7/docs/technotes/samples/hprof.html (auf den Seiten 106 und 107 zi-tiert)

[hsa13] HSAIL-based GPU o�oad: the Quest for Java Performance Be-

gins. 17.06.2013. � http://developer.amd.com/community/blog/

hsail-based-gpu-offload-the-quest-for-java-performance-begins/ (zi-tiert auf Seite 65)

[hum13] AMD Heterogeneous Uniform Memory Access. 30.04.2013. � http://de.

slideshare.net/AMD/amd-heterogeneous-uniform-memory-access (zitiert aufSeite 99)

[int13] Intel. 22.03.2013. � http://www.intel.com (zitiert auf Seite 1)

[jcu13] jcuda.org - Java bindings for CUDA. 30.04.2013. � http://www.jcuda.org/ (zitiertauf Seite 32)

[jog13] JOGL - Java Binding for the OpenGL API. 10.07.2013. � https://jogamp.org/

jogl/www/ (zitiert auf Seite 83)

[kep12] NVIDIA's Next Generation Compute Architecture: Kepler TM

GK110. 2012. � http://www.nvidia.com/content/PDF/kepler/

NVIDIA-Kepler-GK110-Architecture-Whitepaper.pdf (auf den Seiten 8 und 11zitiert)

Page 123: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

Literaturverzeichnis 116

[KKS99] Kaushik, DK ; Keyes, DE ; Smith, BF: Newton-Krylov-Schwarz methods for aero-dynamic problems: Compressible and incompressible �ows on unstructured grids. In:Proceedings of the 11th International Conference on Domain Decomposition Methods.

Domain Decomposition Press, Bergen, 1999 (zitiert auf Seite 19)

[KWH10] Kirk, David B. ; W. Hwu, Wen mei: Programming Massively Parallel Processors:

A Hands-on Approach. Elsevier, 2010. � ISBN 9780123814739 (auf den Seiten 1, 3,7, 8, 9, 10, 11, 12, 14, 15, 16, 17, 23, 29 und 112 zitiert)

[Kyr12] Kyriazis, George: Heterogeneous System Architecture: A Technical Review.30.08.2012. � http://developer.amd.com/wordpress/media/2012/10/hsa10.pdf

(zitiert auf Seite 65)

[Lia99] Liang, S.: The Java Native Interface: Programmer's Guide and Speci�cation. AD-DISON WESLEY Publishing Company Incorporated, 1999 (Addison-Wesley Javaseries). � ISBN 9780201325775 (auf den Seiten 101, 102 und 113 zitiert)

[LNOM08] Lindholm, E. ; Nickolls, J. ; Oberman, S. ; Montrym, J.: NVIDIA Tesla: AUni�ed Graphics and Computing Architecture. In: Micro, IEEE 28 (2008), Nr. 2,S. 39�55. http://dx.doi.org/10.1109/MM.2008.31. � DOI 10.1109/MM.2008.31.� ISSN 0272�1732 (zitiert auf Seite 8)

[LYBB13] Lindholm, T. ;Yellin, F. ; Bracha, G. ; Buckley, A.: The Java Virtual Machine

Speci�cation. Oracle, 2013 (auf den Seiten 41, 58 und 91 zitiert)

[mal13] malloc(3) - Linux man page. 05.08.2013. � http://linux.die.net/man/3/malloc

(zitiert auf Seite 70)

[Mic11] Michels, Dominik: Sparse-matrix-CG-solver in CUDA. In: Proceedings of the 15thCentral European Seminar on Computer Graphics, 2011 (zitiert auf Seite 31)

[MRHO12] Moors, Adriaan ; Rompf, Tiark ; Haller, Phillipp ; Odersky, Martin: ToolDemo: Scala-Virtualized. In: CM SIGPLAN Workshop on Partial Evaluation and

Program Manipulation (2012) (auf den Seiten 55 und 56 zitiert)

[Mun11] Munshi, Aaftab: The OpenCL Speci�cation 1.2 / Khronos OpenCLWorking Group.2011. � Forschungsbericht. � http://developer.amd.com/wordpress/media/2012/

10/opencl-1.2.pdf (zitiert auf Seite 41)

[MV11] Meister, A. ; Vömel, C.: Numerik linearer Gleichungssysteme:. Vieweg Verlag,Friedr, & Sohn Verlagsgesellschaft mbH, 2011. � ISBN 9783834881007 (zitiert aufSeite 30)

[NC08] Nguyuen, H. ; Corporation, NVIDIA: GPU gems 3. Addison Wesley PublishingCompany, 2008 (Lab Companion Series). � ISBN 9780321515261 (zitiert auf Seite19)

[nvc13] CUDA Compiler Driver NVCC. 26.03.2013. � http://docs.nvidia.com/cuda/pdf/

CUDA_Compiler_Driver_NVCC.pdf (auf den Seiten 15, 16 und 112 zitiert)

Page 124: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

Literaturverzeichnis 117

[ope09] OpenCL Programming Guide for the CUDA Architecture. 27.08.2009. �http://www.nvidia.com/content/cudazone/download/OpenCL/NVIDIA_OpenCL_

ProgrammingGuide.pdf (zitiert auf Seite 17)

[OSV10] Odersky, M. ; Spoon, L. ; Venners, B.: Programming in Scala, second edition.Artima, Incorporated, 2010 (Artima Series). � ISBN 9780981531649 (auf den Seiten56 und 57 zitiert)

[Par07] Parallel Pre�x Sum (Scan) with CUDA. 04.2007. � http://developer.download.

nvidia.com/compute/cuda/1.1-Beta/x86_website/projects/scan/doc/scan.

pdf (auf den Seiten 4, 30 und 112 zitiert)

[PS10] Pratt-Szeliga, Philip C.: Automatically Utilizing Graphics Processing Units fromJava ByteCode, Syracuse University, Diplomarbeit, 2010 (auf den Seiten 45 und 49zitiert)

[PSFW12] Pratt-Szeliga, Philip C. ; Fawcett, James W. ; Welch, Roy D.: Rootbeer:Seamlessly Using GPUs from Java. In: Proceedings of the 2012 IEEE 14th Inter-

national Conference on High Performance Computing and Communication & 2012

IEEE 9th International Conference on Embedded Software and Systems. Washington,DC, USA : IEEE Computer Society, 2012 (HPCC '12). � ISBN 978�0�7695�4749�7,S. 375�380 (auf den Seiten 5, 6, 46 und 53 zitiert)

[Que13] QueryPerformanceCounter function. 26.07.2013. � http://msdn.microsoft.com/

en-us/library/windows/desktop/ms644904(v=vs.85).aspx (zitiert auf Seite 95)

[RO10] Rompf, Tiark ; Odersky, Martin: Lightweight modular staging: a pragmatic ap-proach to runtime code generation and compiled DSLs. In: Proceedings of the ninthinternational conference on Generative programming and component engineering.New York, NY, USA : ACM, 2010 (GPCE '10). � ISBN 978�1�4503�0154�1, 127�136(auf den Seiten 55, 56 und 59 zitiert)

[Rør04] Rørbech, Marinus: Real-Time Simulation of 3D Fluid Using Graphics Hardware,University of Copenhagen, Diplomarbeit, 2004 (auf den Seiten 19 und 20 zitiert)

[RSL+11] Rompf, Tiark ; Sujeeth, Arvind K. ; Lee, HyoukJoong ; Brown, Kevin J. ; Chafi,Hassan ; Odersky, Martin ; Olukotun, Kunle: Building-blocks for performanceoriented DSLs. In: arXiv preprint arXiv:1109.0778 (2011) (auf den Seiten 57 und 59zitiert)

[She94] Shewchuk, Jonathan R.: An introduction to the conjugate gradient method without

the agonizing pain. 1994 (zitiert auf Seite 22)

[Shi03] Shirazi, J.: Java Performance Tuning. O'Reilly Media, Incorporated, 2003 (Javaseries). � ISBN 9780596003777 (zitiert auf Seite 106)

[SK11] Sanders, J. ; Kandrot, E.: CUDA by Example: An Introduction to General-

Purpose GPU Programming. Addison-Wesley, 2011. � ISBN 9780131387683 (aufden Seiten 1, 3, 9, 11 und 28 zitiert)

Page 125: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

Literaturverzeichnis 118

[Sta99] Stam, Jos: Stable Fluids. In: Proceedings of the 26th annual conference on Comput-

er graphics and interactive techniques ACM Press/Addison-Wesley Publishing Co.,1999, S. 121�128 (zitiert auf Seite 21)

[Suj] Sujeeth, Arvind K.: OptiML Language Speci�cation 0.2. � http://stanford-ppl.

github.io/Delite/optiml/downloads/optiml-spec.pdf (auf den Seiten 57 und 69zitiert)

[Sum13a] Project Sumatra. 28.05.2013. � http://openjdk.java.net/projects/sumatra (aufden Seiten 5 und 64 zitiert)

[Sum13b] Sumatra-dev Mailing-Liste. 28.05.2013. � http://mail.openjdk.java.net/

pipermail/sumatra-dev (zitiert auf Seite 64)

[Sum13c] Sumatra Wiki. 28.05.2013. � https://wikis.oracle.com/display/

HotSpotInternals/Sumatra (zitiert auf Seite 65)

[TIT13] TITAN - GeForce GTX Gra�kkarte mit Kepler Technologie | NVIDIA. 20.03.2013.� http://www.nvidia.de/object/geforce-gtx-titan-de.html (zitiert auf Seite1)

[vis13] Analyzing Application Performance by Using Pro�ling Tools. 14.08.2013. � http:

//msdn.microsoft.com/en-us/library/z9z62c29.aspx (zitiert auf Seite 107)

[VKZ11] Verbrugge, Clark ; Kielstra, Allan ; Zhang, Yi: There is nothing wrong without-of-thin-air: compiler optimization and memory models. In: Proceedings of the

2011 ACM SIGPLAN Workshop on Memory Systems Performance and Correctness.New York, NY, USA : ACM, 2011 (MSPC '11), S. 1�6 (zitiert auf Seite 46)

[YGS09] Yan, Yonghong ; Grossman, Max ; Sarkar, Vivek: JCUDA: A Programmer-Friendly Interface for Accelerating Java Programs with CUDA. In: Proceedings of the15th International Euro-Par Conference on Parallel Processing. Berlin, Heidelberg :Springer-Verlag, 2009 (Euro-Par '09). � ISBN 978�3�642�03868�6, S. 887�899 (zitiertauf Seite 35)

Page 126: Masterarbeit akultätF Informatik Untersuchungen zur ...meixner/masterarbeitKlass.pdf · the GPU computing power in higher-level programming languages increases. Both the evaluation

Literaturverzeichnis 119