Arbeit - Database Technology Group

Werbung
Fakultät Informatik Institut für Systemarchitektur, Professur für Datenbanken
Diplomarbeit
FRAMEWORK FÜR DIE
SPEZIFIKATION UND AUSFÜHRUNG
PARALLELER
CLUSTERING-ALGORITHMEN
Alexander Krause
Matr.-Nr.: 3411206
Betreut durch:
Prof. Dr.-Ing. Wolfang Lehner
und:
Dr.-Ing. Dirk Habich
Eingereicht am 28. Februar 2015
2
ERKLÄRUNG
Ich erkläre, dass ich die vorliegende Arbeit selbständig, unter Angabe aller Zitate und nur unter
Verwendung der angegebenen Literatur und Hilfsmittel angefertigt habe.
Dresden, 28. Februar 2015
3
4
ABSTRACT
Aufgrund der immer größeren Datenmengen, die durch immer größere Sensornetzwerke, Protokollierungsmaßnahmen oder andere Quellen aufgezeichnet werden, steigt die Menge von zu
bearbeitenden Daten immer weiter an. Um diese Informationen zu verarbeiten, wird der Prozess
des Data-Mining angewendet, wovon ein wichtiger Teil aus der Clusteranalyse besteht. Dadurch
sollen diejenigen Elemente einer Datenmenge gefunden werden, deren Merkmale einander ähnlich oder sogar identisch sind. Um bei der Analyse selbst so variabel wie möglich zu sein, werden
Ausführungsumgebungen benötigt, um die Clustering-Algorithmen verwenden zu können. Für
jede Implementierung gelten dabei in der Regel Abhängigkeiten, welche sie an eine Umgebung
binden. Diese Arbeit untersucht eine Möglichkeit, um basierend auf dem MapReduce Modell
eine plattformunabhängige Spezifikation von Clustering-Algorithmen zu ermöglichen und die
Parallelität der zu Grunde liegenden Hardware auszunutzen. Dabei wird ein mögliches Sprachkonzept aufgezeigt und darauf eingegangen, wie dessen Umsetzung in plattformunabhängigen
Programmcode funktionieren kann. Die Funktionalität des Ansatzes wird anhand einer MapReduce und OpenCL Implementierung bestätigt.
5
6
INHALTSVERZEICHNIS
1
2
Einführung
1.1
Zielsetzung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
10
1.2
Aufbau der Arbeit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
11
PIPE-basierte Spezifikation paralleler Clustering-Algorithmen
13
2.1
Vorbetrachtungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
13
2.1.1
Parallele Programmierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
13
2.1.2
MapReduce und PACT . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
15
2.1.3
Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
16
2.2
PIPE-Konzept . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
17
2.3
cPIPE-Ansatz für Clustering-Algorithmen . . . . . . . . . . . . . . . . . . . . . . . .
19
2.3.1
Modulare Beschreibung von Algorithmen . . . . . . . . . . . . . . . . . . . .
19
2.3.2
cPIPE Elemente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
20
2.3.3
Beispiele . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
21
Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
24
2.4
3
9
PIPE Compiler für effiziente Ausführung
27
3.1
Vorbetrachtungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
28
3.1.1
28
Source-to-Source Compiling . . . . . . . . . . . . . . . . . . . . . . . . . . . .
7
Inhaltsverzeichnis
3.1.2
Ausführungsumgebungen . . . . . . . . . . . . . . . . . . . . . . . . . . . .
29
3.1.3
MapReduce . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
30
3.1.4
OpenCL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
31
3.2
PIPE Compiler Ansatz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
32
3.3
Optimierungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
36
3.4
Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
37
4
Implementierung
39
5
Evaluation
43
5.1
PIPE Konzept . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
43
5.2
Source-to-Source Compiler und Ausführungsumgebungen . . . . . . . . . . . . . .
45
6
8
Zusammenfassung und Ausblick
49
1
EINFÜHRUNG
Im derzeitigen „Petabyte Zeitalter“, wie es in [Ott09] beschrieben wird, sind große Datenmengen
allgegenwärtig. Diese auch als „Big Data“ bezeichnete Datenmenge zu verarbeiten und hinsichtlich nützlicher Informationen zu analysieren, ist oft nicht nur ein ökonomisches, sondern auch
ein technisches Problem. „Data“ bedeutet im lateinischen „Gegebenes“. Big Data verweist also
auf eine enorme Menge von gegebenen Dingen, in diesem Fall Wissen. Um eine so große Menge
an unentdeckten Wissen zu verarbeiten, werden effiziente Analysealgorithmen gebraucht. Diese
stammen unter anderem aus dem Bereich des Data Mining, dessen Aufgabe es ist, unbekanntes
Wissen aus einer Menge von Quellen zu extrahieren. Um dabei kosteneffizient und gleichzeitig
schnell zu sein, müssen auch die verwendeten Algorithmen mit aktuellen Entwicklungen Schritt
halten. Eine Kategorie des Data Minings wird als Data Clustering, also Clusteranalyse, bezeichnet. Ihr Fokus liegt darauf, Häufungen von ähnlichen Elementen zu finden und diese gegen
andere Ballungsräume von ähnlichen Elementen abzugrenzen. Das Ergebnis einer solchen Analyse ist in Abbildung 1.1 zu sehen. In realen Anwendungsfällen werden weit mehr als nur sechs
Cluster betrachtet bzw. gefunden, sodass die Effizienz eines Algorithmus einen kritischen Aspekt
von Analysen darstellt. Auf diese Weise können, aus der enormen Fülle von teilweise unstrukturiertem Wissen, bisher unbekannte Informationen gewonnen werden.
2
1.5
1
0.5
0
0
0.5
1
1.5
2
Abbildung 1.1: Beispielcluster unterschiedlicher Form und Größe
9
Kapitel 1 Einführung
Neben den Datenquellen entwickelt sich auch die Hardware der zu Grunde liegenden Systeme
stetig weiter. Wo in früheren Workstations oder Servern lediglich Prozessoren mit einem Kern
verbaut waren, finden in aktuellen Systemen Multi- oder Manycore Prozessoren Anwendung.
Die Unterscheidung beider Varianten bezieht sich dabei lediglich auf die tatsächlich verbaute
Anzahl von Kernen auf einem Chip. Mit einer steigenden Anzahl von Prozessorkernen steigt
gleichermaßen auch die Anzahl von Berechnungsaufgaben, die zur selben Zeit von einem Prozessor ausgeführt werden können. Dieser Effekt wird auch als parallele Berechnung bezeichnet
und bedeutet, dass ein oder mehrere Programme zur gleichen Zeit auf mehreren Prozessoren oder
deren Kernen ausgeführt werden kann. Für einzelne Programme, wie etwa Analysealgorithmen,
bedeutet dies eine Umstrukturierung der internen Programmstruktur. Um diesem Mehraufwand
entgegen zu wirken, müssen die verwendeten Algorithmen effizient im parallelen Umfeld funktionieren. Bei optimaler Ausnutzung der zur Verfügung stehenden Hardware können dabei, je
nach Szenario, Geschwindigkeitsvorteile entstehen, die sogar bis an die Anzahl der zur Berechnung verwendeten Prozessorkerne heranreicht.
Um Algorithmen ausführen zu können, wird eine Ausführungsumgebung benötigt. Diese weist
bestimmte Eigenschaften auf, die an ihre zu Grunde liegende Hardware gebunden sind. Je nach
System werden mehrere Szenarien, wie beispielsweise „Scale Up“ oder „Scale Out“, beschrieben. Ersteres meint den Einsatz eines größeren, leistungsfähigeren Chips. Scale Out bedeutet
hingegen die Verwendung von mehreren Subsystemen zur Erstellung eines Gesamtsystems. Ein
Analysealgorithmus, der in solchen Szenarien eingesetzt werden soll, muss also gut skalierbar
und effizient im parallelen Umfeld arbeiten. Dabei muss berücksichtigt werden, dass eine Ausführungsumgebung aufgrund der jeweiligen Hardwareeigenschaften oftmals an ein ausführendes System gebunden ist. Wird also ein Algorithmus für eine bestimmte Ausführungsumgebung
programmiert, so kann dieselbe Implementierung in der Regel nicht ohne Anpassungen auf ein
anderes System portiert werden. Grundlegend wäre also eine Möglichkeit wünschenswert, mit
der ein Wechsel des verwendeten Systems und der damit verbundenen Ausführungsumgebung
so einfach wie möglich gestaltet wird. Dadurch könnte gleichermaßen der Aufwand zur Beschreibung von skalierbaren, effizienten Analysealgorithmen im parallelen Umfeld reduziert und die
Effektivität der durchgeführten Analysen gesteigert werden.
1.1
ZIELSETZUNG
Ziel dieser Arbeit ist es, einen Ansatz zur Formulierung von parallel ablaufenden Algorithmen
zu finden. Dabei soll eine uniforme Beschreibung solcher Algorithmen möglich sein, damit sie
auf einer Vielzahl von unterschiedlichen Plattformen ausführbar sind. Da die konkreten Implementierungen eines Algorithmus für spezifische Ausführugnsumgebungen voneinander abweichen, muss eine Abbildungsschicht zwischen der Spezifikation von Analysealgorithmen und den
angestrebten Ausführungsumgebungen geschaffen werden. Dieses Schema wird in Abbildung
1.2 verdeutlicht. Am Beispiel der Klasse von Clustering-Algorithmen soll gezeigt werden, wie
diese in parallelisierbare Segmente unterteilt und formuliert werden können. Die Abbildung
der plattformunabhängigen Beschreibung auf eine plattformabhängige Implementierung bedeutet lediglich, dass eine Programmiersprache in eine andere umgewandelt werden muss. Es wird
demnach ein Prozess benötigt, der eine Programmiersprache in eine andere umwandeln kann.
Für solche Zwecke wurde bereits in der Vergangenheit ein Source-to-Source Compiler eingesetzt.
Dieser soll prototypisch implementiert werden, sodass er die für die Spezifikation der Clustering-
10
1.2 Aufbau der Arbeit
Algorithmen entworfene Sprache verarbeiten und in eine der, möglicherweise mehreren, Zielsprachen übersetzen kann. Der gesamte Übersetzungs- und Ausführungsprozess soll am Beispiel
der beiden Ausführungsumgebungen MapReduce sowie OpenCL verdeutlicht werden, da diese
eine Vielzahl von homo- und heterogenen Systemen verwenden können. Als Testplattformen
stehen zwei Systeme zur Verfügung, die jeweils mit den 64 Bit Versionen der Betriebssysteme
Ubuntu 12.04 und Windows 7 betrieben werden.
Plattformunabhängige
Beschreibung
Abbildungsschicht
Plattform 1
Plattform ...
Plattform N
Abbildung 1.2: Schematischer Aufbau des Spezifikations- und Ausführungsvorgangs
1.2
AUFBAU DER ARBEIT
Diese Arbeit betrachtet die Entwicklung einer plattformunabhängigen Spezifikationssprache für
parallele Algorithmen am Beispiel des Themas Data Clustering. In Kapitel 2 wird das Thema Data
Clustering beleuchtet. Neben einer kurzen Einführung in den Bereich der parallelen Programmierung werden das klassische MapReduce Modell sowie dessen Verallgemeinerung, das PACT Modell, erläutert. Es folgt eine Beschreibung des abstrakten PIPE Modells, welches an seiner Spezialisierungsform den cPIPEs für den Bereich Data Clustering im Detail erläutert wird und den plattformunabhängigen Beschreibungsteil aus Abbildung 1.2 darstellt. In Kapitel 3 wird eine Übersicht zum Thema Source-to-Source Kompilierung, also der Abbildungsschicht aus Abbildung 1.2,
gegeben. Dabei wird außerdem auf die beiden zu Testzwecken ausgewählten Ausführungsumgebungen MapReduce und OpenCL eingegangen. Weiterhin wird die Funktionsweise des für diese
Arbeit notwendigen Source-to-Source Compilers erläutert sowie Optimierungsmöglichkeiten des
Programmcodes aufgezeigt. Das Kapitel 4 beschreibt konkrete Implementierungsdetails. Kapitel
5 zeigt mit Hilfe von Experimenten die Durchführbarkeit des erarbeiteten Konzepts. Das Kapitel
6 fasst diese Arbeit zusammen und gibt einen Überblick über mögliche zukünftige Entwicklungen.
11
Kapitel 1 Einführung
12
2
PIPE-BASIERTE SPEZIFIKATION
PARALLELER
CLUSTERING-ALGORITHMEN
Mit der steigenden Parallelität auf Hardwareebene entstehen für Programmierer neue Probleme
in Bezug auf die Spezifikation von Algorithmen. Im Folgenden werden die Grundlagen paralleler
Programmierung mit Hilfe von früheren und aktuellen Methoden erläutert. Es folgt die Darstellung eines neuen, abstrakten und plattformunabhängigen Konzeptes zur Beschreibung paralleler
Algorithmen. Dessen Funktionsweise wird an der Thematik des Data Clustering im Detail erläutert. Abschließend wird dieses Kapitel übersichtsgebend zusammengefasst.
2.1
VORBETRACHTUNGEN
Parallelität auf Hardwareebene kann durch die zwei Formen „Scale Up“ sowie „Scale Out“ beschrieben werden. Ersteres meint den Einsatz eines größeren, leistungsfähigeren Chips. Scale Out
bedeutet hingegen die Verwendung von mehreren Subsystemen zur Erstellung eines Gesamtsystems. Der folgende Abschnitt erläutert die Grundlagen von paralleler Programmierung und
die damit verbundenen Probleme. Zusätzlich wird eine Übersicht über aktuelle Methoden zur
Ausführung von parallelen Algorithmen gegeben und diese im Anschluss zusammengefasst.
2.1.1
Parallele Programmierung
In den frühen Formen der Programmierung galt es, einen Algorithmus auf einer einzigen Recheneinheit auszuführen. Lange Zeit ging bei der Entwicklung von Prozessoren der Trend in die
Richtung, dass einzelne Kerne immer höher getaktet werden. Als diese Entwicklung an physikalische Grenzen zu stoßen drohte, wurde das Hardwaredesign von „Scale Up“ auf „Scale Out“
13
Kapitel 2 PIPE-basierte Spezifikation paralleler Clustering-Algorithmen
umkonzipiert. Das bedeutet, dass nicht mehr nur ein einzelner, extrem leistungsfähiger Chip
verwendet wird, sondern eine Vielzahl von Systemen für die Erledigung anstehender Aufgaben
genutzt werden. Dieses Prinzip lässt sich auf einen Prozessor genauso wie auf Rechenzentren
anwenden. Im lokalen Kontext bedeutet es, anstatt einem Einzelkernprozessor mit hoher Taktrate einen Mehrkernprozessor zu verwenden, dessen Kerne mit geringerer Frequenz arbeiten.
Neben dem ökonomischen Aspekt ermöglicht diese Veränderung auch, dass mehrere Aufgaben
gleichzeitig abgearbeitet werden können. Davon können auch einzelne Algorithmen profitieren.
Mit einer angepassten Architektur können separate Abschnitte oder das gesamte Programm in
nebenläufige Aufgaben partitioniert werden. Um das zu realisieren, wurden zunächst Programmiermodelle eingeführt, welche das Beschreiben von parallelen Abschnitten ermöglichen. Ein
kritischer Aspekt ist dabei allerdings, dass sequentiell programmierte Algorithmen im parallelen
Kontext ihre Speicherbereiche teilen müssen. Dabei ist zu berücksichtigen, dass zu jeder Zeit von
anderen Programmteilen ein Zugriff auf den selben Speicherbereich oder eine Manipulation dessen erfolgen kann. Grundsätzlich muss immer beachtet werden, dass bei gleichzeitigem Zugriff
auf den selben Speicherbereich Probleme auftreten können. Für die folgenden Beispiele wird ein
System angenommen, dass mit zwei Prozessorkernen ausgestattet ist und deswegen mit zwei
Threads zur gleichen Zeit arbeiten kann. Ein Thread stellt dabei einen Prozessteil dar, welcher
Programmcode losgelöst von anderen Programmteilen ausführen kann. Weiterhin sollen keine
sonstigen Modifikationen des Speichers durch andere Programme möglich sein. Versuchen diese
zwei Threads den selben Speicherbereich zu lesen, besteht kein Problem. Problematisch wird es,
wenn ein Thread den Bereich beschreibt, während der andere ihn liest oder den selben Bereich beschreiben möchte. Ähnlich dem Transaktionsprinzip in Datenbankmanagementsystem (DBMS)
kommt es zu Datenabhängigkeiten, weswegen ein schreibender Zugriff immer vor lesenden Zugriffen geschützt werden muss. Andernfalls kann es bei nicht-atomaren Operationen dazu kommen, dass einer der Threads einen inkonsistenten Speicherzustand einliest, woraufhin dessen
Berechnungen mit fehlerhaften Daten durchgeführt würden. Beim Schreib-Schreib-Konflikt geht
dies soweit, dass die berechneten Daten von Thread1 in den Speicher geschrieben würden, wobei
sie unter Umständen vollständig von Thread2 überschrieben werden, ohne dass diese je hätten
verarbeitet werden können. Die standardisierte Lösung für diese Probleme stellt das Sperren von
kritischen Abschnitten dar, in denen nichtatomare Operationen ausgeführt werden und ein gemeinsamer Speicherbereich genutzt wird. Zwei Modelle zur Ermöglichung paralleler Programmierung werden durch Open Multi-Processing (OpenMP) sowie Message-Passing Interface (MPI)
realisiert. Ersteres ermöglicht die Parallelisierung von Programmteilen eines Prozesses. Dabei
werden vor allem Schleifen mittels Compilerdirektiven gekennzeichnet, wobei die jeweiligen Iterationsschritte von unabhängigen Threads ausgeführt werden. OpenMP wurde für die Verbesserung der Laufzeit von Programmen auf Basis von Non-Uniform Memory Access (NUMA) Systemen erdacht [Dag98]. Für solche Systeme waren die unterschiedlichen Zugriffszeiten innerhalb
der Speicherhierarchie namensgebend. Ein Zugriff auf den Cache des Prozessors ist am schnellsten; Daten aus dem Hauptspeicher abzurufen benötigt bereits deutlich mehr Zeit und die längste
Wartezeit entsteht bei Speicherzugriffen auf Archivspeicher wie Hard Disk Drives (HDDs) oder
Solid State Disks (SSDs). Anders funktioniert dies bei MPI. Dessen Intention ist es, gleich mehrere Prozesse die nebenläufige Abarbeitung zu ermöglichen. Diese werden allerdings durch ein
gemeinsames Interface, über das Nachrichten untereinander ausgetauscht werden können, synchronisiert [Pac98].
14
2.1 Vorbetrachtungen
2.1.2
MapReduce und PACT
Die nächste Evolutionsstufe paralleler Programmierung stellte die Entwicklung von MapReduce
durch Google dar. Dieses System wurde für das „Scale Out“ Szenario konzipiert und nahm eine
einheitliche Datengrundlage an, wobei die Ein- und Ausgabedaten aus Schlüssel-Wert-Paaren
bestehen. Es ermöglicht dem Programmierer das Einbringen von nutzerseitig spezifizierten Programmcode in ein Framework, welches sämtliche Aufgaben von Datenpartitionierung, Speichermanagement und Arbeitsverteilung selbst übernimmt. Die einzig zwingende Konvention war,
dass es zwei Funktionen zweiter Ordnung gibt, namentlich Map und Reduce, die immer in exakt
dieser Reihenfolge ausgeführt werden.
Beide arbeiten auf dem fest definierten
Eingabe
Datenschema und ohne Abweichung von
der Ausführungsreihenfolge. Abbildung
Map
2.1 verdeutlicht dieses Konzept. Die EinZwischenergebnis
Ausgabe
gabedaten werden von mehreren MapMap
pern bearbeitet und nach bestimmten InReduce
formationen durchsucht. Diese werden
im Schlüssel-Wert-Paar Schema als ZwiMap
Reduce
schenergebnisse abgelegt und anschließend nach den Schlüsseln gruppiert. Abschließend findet eine Reduktion aller
Map
Werte in eine gemeinsame Ergebnisliste mittels einem oder mehrerer Reducer
statt.
Abbildung 2.1: MapReduce Schema
Ebenfalls auf die Verarbeitung großer Datenmengen ausgelegt ist das von MapReduce abgeleitete Parallelization Contract
(PACT) Programmiermodell aus [Ale11]. PACT steht für Parellelization Contracts, also Vereinbarungen für die parallele Abarbeitung von Operatoren zur Datentransformation. Das System
arbeitet auf einer Datengrundlage von Schlüssel-Wert-Paaren, genau wie das MapReduce Konzept, mit dem es verglichen wird und das es generalisiert [Ale11]. Eine Innovation gegenüber
dem klassischen MapReduce Verfahren stellen die Programmiermöglichkeiten mittels PACTs dar.
Die zuvor fest definierte Abfolge von Map- und Reduce-Schritten bedeutet immer eine gewisse
Limitierung in der Anwendbarkeit dieses Modells, um mehrere Arten von Algorithmen auszu-
Eingabe B
Eingabe A
Unabhängige
Subsets
(a) Cross PACT
Eingabe B
Eingabe B
Eingabe A
Unabhängige
Subsets
(b) Match PACT
Eingabe A
Unabhängige
Subsets
(c) CoGroup PACT
Abbildung 2.2: PACT Operatoren, entnommen aus [Ale11]
15
Kapitel 2 PIPE-basierte Spezifikation paralleler Clustering-Algorithmen
drücken, wie in [Kal13] gezeigt wird. Um diese durch das MapReduce Framework gegebenen
Grenzen zu überwinden, führte [Ale11] unter Beibehaltung des Datenschemas zusätzlich zu Map
und Reduce drei weitere Operatoren ein. Diese sind namentlich „Cross“, „Match“ und „CoGroup“ und werden in Abbildung 2.2 illustriert. Jeder dieser Operatoren wird als Parallelization
Contract (PACT) bezeichnet. Der Cross PACT berechnet ein Kreuzprodukt aus allen Eingabedaten. Durch den Match PACT werden jene Datensätze miteinander kombiniert, deren Schlüssel
übereinstimmen. Dabei können zwei verschiedene Eingabedatenpaare auch von zwei verschiedenen Match-Instanzen bearbeitet werden. Der CoGroup PACT generiert Datensubsets aus allen
Schlüssel-Wert-Paaren mit dem selben Schlüssel. Anders als bei Match ist dabei garantiert, dass
diese so entstandenen Subsets von der selben Instanz der Nutzerfunktion bearbeitet werden. Dieses Modell hat den grundlegenden Vorteil, dass eine größere Anzahl von Algorithmen einfach als
Orchestrierung von PACTs erstellt werden kann. Das Schema des PACT Programmiermodells
wird in Abbildung 2.3 verdeutlicht. Ein PACT besteht immer aus genau einer Funktion zweiter
Ordnung, die als Eingabe eine Funktion erster Ordnung erwartet. Diese beinhaltet problemspezifischen Code. Für jeden PACT gilt, dass ihre Eingabedaten nicht mit denen anderer PACTs
übereinstimmen müssen. Optional kann auch ein Ausgabe-PACT spezifiziert werden, der Bedingungen für die Art der Ausgabedaten festlegt.
Schlüssel
Wert
Eingabe
PACT
...
Nutzer
Code
Ausgabe
PACT
...
Ausgabe
Eingabe
Unabhängige
Datengrundlagen
Abbildung 2.3: PACT Schema, übersetzt aus [Ale11]
2.1.3
Zusammenfassung
Unter Berücksichtigung der historischen Entwicklung kann festgestellt werden, dass die parallele
Programmierung ein relevantes Thema im Bereich der Informatik darstellt. Beginnend mit Compilerparadigmen im Low-Level Bereich entstanden mit der Zeit diverse Frameworks, um dem
Anwender eine Vielzahl an Werkzeugen bereit zu stellen, mit denen die Spezifikation von parallelisierbaren Algorithmen immer einfacher wird. Zwischen den jeweiligen Evolutionsschritten
wurde versucht, die Nachteile des Vorgängers zu beseitigen. Sobald ein Programmierer mittels
OpenMP oder MPI eine Implementierung vornimmt, ist diese an eine bestimmte Plattform gebunden. MapReduce behebt dieses Problem durch ein einheitliches Datenmodell gepaart mit
zwei Funktionen zweiter Ordnung, welche die Verwendung von Nutzercode erlauben. Dieses
Programmiermodell wurde allerdings ursprünglich für den Bereich Information Retrieval erdacht, um beispielsweise Wörter in Dokumenten zu zählen, was die konzeptionellen Schwächen
in Bezug auf die fehlende Vielseitigkeit für die Spezifikation anderer Algorithmen erklärt. Das
für den Bereich Datentransformation erdachte PACT revolutioniert den MapReduce Ansatz in
16
2.2 PIPE-Konzept
sofern, dass dem Benutzer weitere Elemente zur Verfügung gestellt werden, mit denen die Daten bearbeitet werden können. Obwohl das Datenschema im Schlüssel-Wert-Paar Stil beibehalten
wird, kann aufgrund der freien Kombinationsmöglichkeiten aller Elemente bereits ein Fortschritt
in Richtung Vielseitigkeit erreicht werden. Nachteilig wirkt sich dabei allerdings wieder die feste
Bindung an ein einzelnes Datenschema aus. Als nächsten Evolutionsschritt bietet sich demnach
ein Konzept an, welches eine vom zu Grunde liegenden Datenmodell unabhängige Spezifikation
von Algorithmen ermöglicht und zusätzlich unabhängig von der Zielplatform einsetzbar ist.
2.2
PIPE-KONZEPT
Die Effektivität und Ausführbarkeit von Algorithmen ist oft unmittelbar mit der angestrebten
Ausführungsumgebung und der verwendeten Hardware verknüpft. Abhängig von der verwendeten Programmiersprache können etwaige Implementierungen nicht zwischen einzelnen Systemen portiert werden, sondern müssen erst aufwändig aufgearbeitet und angepasst werden, um
den verschiedensten Anforderungen diverser homo- und heterogener Systeme gerecht werden
zu können. Es wird also eine Möglichkeit gesucht, Algorithmen auf generische Art und Weise
plattformunabhängig zu spezifizieren. Basierend auf dieser möglichst einfach zu haltenden Beschreibung soll eine große Anzahl unterschiedlicher Plattformen verwendet werden können. Eine
zwingend notwendige Eigenschaft einer solchen Beschreibung muss allerdings sein, dass sie sich
den hohen Grad an Parallelität der unterliegenden Hardwarestruktur zu Nutze machen kann.
Aus den Vorüberlegungen ergeben sich drei Herausforderungen, die es zu lösen gilt. Ein Framework für die Spezifikation und Ausführung von parallelen Algorithmen braucht also zunächst
eine Möglichkeit, Clustering-Algorithmen zu beschreiben, ohne auf hardwarebezogene oder systemspezifische Eigenschaften eingehen zu müssen. Wünschenswert ist in diesem Zusammenhang auch eine grundlegend einfache Syntax. Je einfacher die zur Formulierung der Algorithmen verwendeten Konstrukte sind, desto geringer ist im Endeffekt auch der Aufwand, diese in
plattformabhängigen Code zu übersetzen. Dies kann am Beispiel von bereits vorhandenem Programmcode verdeutlicht werden. Wenn eine problemspezifische Lösung bereits vorhanden ist
und diese allerdings nur in einer sequentiellen Umgebung lauffähig ist, gibt es zwei Möglichkeiten. Entweder der Code kann in einer Form annotiert werden, sodass er im parallelen Umfeld
lauffähig wird oder er muss gänzlich neu verfasst werden. Letzteres bedeutet immer einen großen
Mehraufwand, verglichen mit einer einfachen Übersetzung. Der zweite Schwerpunkt liegt bei
den Ausführungsumgebungen. Diese bestimmen die Form, in der die Algorithmen ausgedrückt
werden, in der Regel maßgeblich. Es wird also ein Programm benötigt, welches die Übersetzung
zwischen der allgemeinen Spezifikation der Algorithmen und der spezifischen Implementierung
für die Ausführungsumgebung realisieren kann. Als letzte Aufgabe gilt es die Problematik der
Optimierung zu lösen. Je allgemeingültiger die Formulierung von Code ist, desto schlechter ist
seine Leistung. Die Optimierung von Algorithmen ist für die Ökonomie bzw. Laufzeit von Programmen unentbehrlich.
Anders als bei den Vorgängern MapReduce und PACT wird eine Spezifikationsmethode aus Applikationssicht benötigt, um ein Höchstmaß an Flexibilität zu erreichen. Für die Spezifikation
von Algorithmen werden nach wie vor zwei Dinge benötigt. Diese sind ein Datenschema und
eine endliche Menge von Platform Independent Parallel PattErn (PIPE) Elementen, welche auf jenem Schema arbeiten. Diese Elemente bestehen, wie in Abbildung 2.4 illustriert, aus einer Menge
von Ein- und Ausgabeparametern. Die Struktur und Anzahl jener Parameter ist dabei lediglich
17
Kapitel 2 PIPE-basierte Spezifikation paralleler Clustering-Algorithmen
PIPE-Element
Beschreibung
Nutzercode
Beschreibung
Nutzercode
Eingabe 1
Eingabe n
Ausgabe 1
Ausgabe n
Nutzercode
Abbildung 2.4: PIPE Beschreibungsschema
vom Datenschema der Applikation determiniert. Weiterhin dienen die PIPE-Elemente zur Ausführung von nutzerseitig beschriebenen Funktionen zur letztendlichen Datenverarbeitung. Je
nach Domäne können sowohl das Datenschema, als auch die Form der PIPE-Elemente variieren.
Dieses Modell weicht vom MapReduce Ansatz insofern ab, dass nicht nur der Funktionsrumpf
spezifiziert wird, sondern auch die Position innerhalb der Verarbeitungskette. Es muss lediglich
darauf geachtet werden, dass auch nur miteinander solche PIPEs hintereinander ausgeführt werden können, deren jeweilige Aus- bzw. Eingabeparameter miteinander kompatibel sind. Unter
Berücksichtigung dieser Limitation ist eine freie Orchestrierung beliebig vieler PIPE Elemente
problemlos möglich, siehe Abbildung 2.5.
Die Verwendung einer Domain-specific Language (DSL) wurde zunächst ausgeschlossen. Ein
Hauptgrund dafür war, dass eine komplette Sprache hätte modelliert werden müssen, welche
alle Probleme einer entsprechenden Domäne abdeckt und darüber hinaus eine umfassende, aber
einfache Algorithmenbeschreibung ermöglicht. Begründet durch die begrenzte Zeit der Arbeit
schien dies also unmöglich. Diese Erkenntnis führte letztendlich zu dem Schluss, dass die Verwendung einer bereits bestehenden Programmiersprache die sinnvollste Variante wäre. Um die
Handhabung von Variablen, deren Initialisierung und Freigabe, weitestgehend zu vereinfachen,
fiel die Wahl auf die Klasse der funktionalen Programmiersprachen. Anders als bei imperativen
Eingabe 1
Ausgabe 1
Nutzer
Funktion
Eingabe n
Eingabe 1
Nutzer
Funktion
Ausgabe n
PIPE 1
Ausgabe 1
Eingabe n
Eingabe 1
Ausgabe 1
Nutzer
Funktion
Ausgabe n
Eingabe n
Ausgabe n
PIPE n
Abbildung 2.5: PIPE Verbindungsschema
18
2.3 cPIPE-Ansatz für Clustering-Algorithmen
Sprachen bestehen funktionale Programme lediglich aus der Definition von Funktionen. Es finden keine expliziten Speichermanipulationen statt, weswegen diese Sprachen auch ohne Nebeneffekte auskommen. Um all diese Anforderungen erfüllen zu können, wurde für die Spezifikation
der Algorithmen die „PIPE Based Algorithm SpEcification (PIPEBASE)“ eingeführt. Sie fußt auf
der Verwendung von PIPEs und soll durch eine möglichst einfach gehaltene Syntax auch für Programmierlaien problemlos anwendbar sein. Zur Beschreibung der Funktionen erster Ordnung,
also der Nutzerfunktionen, wird auf die Syntax der Programmiersprache Haskell zurückgegriffen.
Haskell ist eine nach dem US-amerikanischen Logiker und Mathematiker Haskell Brooks Curry
benannte, funktionale Programmiersprache. Die Sprache selbst basiert auf dem Lambda-Kalkül,
einer formalen Sprache für die Definition von Funktionen mittels gebundener Parameter. Neben
ihrem Bekanntheitsgrad führte auch die Einfachheit der Syntax zur Wahl dieser Sprache. Obgleich die Definition von Funktionen in Haskell relativ einfach ist, so besteht das Hauptproblem
in der Überführung von Haskell-Code in die Syntax der Sprachen C bzw. C++. Als erster Ansatz
wurde Haskell in seiner Rohform verwendet. Begründet durch das Ziel, mehrere Ausführungsumgebungen unterstützen zu können, ist keine Verwendung von Klassen zur Vererbung möglich.
Dadurch unterscheidet sich PIPEBASE von anderen Systemen wie dem von Apache entwickelten Hadoop [Dit12], welches für dessen Map- und Reduce-Funktionen vordefinierte Konstrukte
mittels Vererbung bereit stellt. Daher ist es notwendig, Kenntnis von der vorliegenden Schnittstelle innerhalb der bereitgestellten Ausführungsumgebungen zu haben, wie beispielsweise Anzahl und Typ von Eingabeparametern. Diese codeseitigen Informationen reichen allerdings nicht
aus, um vollständigen MapReduce- bzw. OpenCL-lauffähigen Quellcode zu generieren. Es fehlen
die Hinweise, welche PIPE dieser Operator repräsentiert. Um eine möglichst einfache Entwicklung zu ermöglichen, wurde die Verwendung von Annotationen innerhalb des Codes eingeführt.
Dabei wird dem Compiler, ähnlich wie bei JavaDoc [Ora04], mit Hilfe von als Kommentaren gekennzeichneten Hinweisen der Rest der notwendigen Informationen mitgeteilt. Diese sind unter
anderem die Kennzeichnung der zu verwendenden PIPE. Weiterhin wird dem Compiler mitgeteilt, welcher Codebereich gerade eingelesen wird.
2.3
CPIPE-ANSATZ FÜR CLUSTERING-ALGORITHMEN
Die Clustering Platform Independent Parallel PattErns (cPIPEs) stellen eine domänenspezifische
Implementierung des PIPE-Modells dar. Am Beispiel des Komplexes Data Clustering soll gezeigt
werden, wie die Spezifikation von parallelen Clustering-Algorithmen basierend auf PIPEBASE
funktioniert. Im folgenden wird beschrieben, wie sich die Wahl eines spezifischen Datenschemas
auf das Layout der benötigten cPIPE-Elemente auswirkt.
2.3.1
Modulare Beschreibung von Algorithmen
Neben vielen anderen Themen arbeitet der Lehrstuhl für Datenbanken an der Technischen Universität Dresden an einem eigenen DBMS. Dieses Projekt bietet die Möglichkeit für Innovationen
und damit verbundene Testläufe, welche in diesem Umfang in proprietären System nicht möglich
wären. Eine dieser Ideen stellt die Modulare Clusteranalyse dar, welche in [Hah14] veröffentlicht
19
Kapitel 2 PIPE-basierte Spezifikation paralleler Clustering-Algorithmen
wurde. Der Kerngedanke besteht darin, die Analysewerkzeuge in die namensgebende, modularisierte Form zu bringen. Ebenfalls zu Grunde liegt eine veränderte Datenhaltung, denn das
erdachte System arbeitet grundlegend auf Matrizen. Damit verbunden ist eine Klassifizierung
der Operatoren. Diese werden in die drei Kategorien „cross“, „row“ und „column“ eingeteilt,
also entsprechend der Art und Weise, wie sie Daten aus der Matrix entnehmen und weiterverarbeiten. Neben den modularisierten Operatoren können Clustering-Algorithmen laut [Hah14] in
mehrere Phasen unterteilt werden. Diese können in Tabelle 2.1 nachgelesen werden. Da sämtliche
Clustering-Algorithmen jede dieser Phasen durchlaufen, könnten bei gleichbleibenden Ein- und
Ausgabedatenströmen nicht nur einzelne Operatoren, sondern ganze Phasen ausgetauscht werden. Dies dient dem Zweck, eine vereinheitlichte Sichtweise auf Clustering-Algorithmen durch
ein kompaktes Set von Instruktionen zu generieren. Weiterhin wird das Ziel verfolgt, die Schwierigkeiten parallelisierter Ausführung dem Programmierer zu erleichtern. Durch die Datenrepräsentation in Form von Matrizen werden verwendete Algorithmen als Matrixoperatoren geschrieben. Dies vereinfacht das Verständnis und Programmieren von Clustering-Algorithmen immens
und macht es weiterhin möglich, verschiedene Algorithmen direkt miteinander zu vergleichen
[Hah14].
Tabelle 2.1: Phasen von Clustering-Algorithmen aus [Hah14] (übersetzt)
Phase
Evaluation
Eingabe
Punkte, Referenzen
Verarbeitung
Distanzmaß
Selektion
Punkt-Distanz-ReferenzTripel
Punkt-Distanz-ReferenzTripel
-
Filter
Assoziation
Optimierung
2.3.2
Ausgabe
Punkt-Distanz-ReferenzTripel
Punkt-Distanz-ReferenzTripel
Entfernungen
(Punkt-Referenz-Tupel)
-
Assoziationsfunktion
-
cPIPE Elemente
In Anlehnung an die aus [Hah14] definierten Operatoren und deren Datengrundlage können mit
Hilfe des PIPE-Konzepts drei cPIPEs definiert werden. Diese benötigen fest definiert zwei Eingabemengen und liefern genau einen Ausgabewert, der wiederum eine Menge repräsentiert. Der erste Operator ist dem PACT „Cross“ aus [Ale11] ähnlich und wird in der folgenden Abbildung 2.6a
Referenzen
1
2
Referenzen
3
4
1
2
Referenzen
3
4
1
3
4
A
A
1
A
2
A
3
A
4
A
A
1
A
2
A
3
A
4
A
A
1
A
2
A
3
A
4
B
B
1
B
2
B
3
B
4
B
B
1
B
2
B
3
B
4
B
B
1
B
2
B
3
B
4
C
C
1
C
2
C
3
C
4
C
C
1
C
2
C
3
C
4
C
C
1
C
2
C
3
C
4
D
D
1
D
2
D
3
D
4
D
D
1
D
2
D
3
D
4
D
D
1
D
2
D
3
D
4
Daten
(a) cross cPIPE Schema
Daten
(b) row cPIPE Schema
Daten
(c) col cPIPE Schema
Abbildung 2.6: cross, row und col cPIPE Schemata
20
2
2.3 cPIPE-Ansatz für Clustering-Algorithmen
dargestellt. Seine Eingabeparameter bestehen aus einer Datenmenge und einer Referenzmenge.
Cross stellt das aus anderen Anwendungen bekannte Kreuzprodukt dar und realisiert dieses im
parallelen Umfeld. Bei Initialisierung dieser PIPE wird sicher gestellt, dass jedes Element aus
der Datenmenge mit jedem einzelnen Element aus der Referenzmenge kombiniert wird. Er stellt
also grundlegend den aus dem SQL Bereich bekannten Join dar. Das Kreuzprodukt bildet die
Grundlage für einige von Clusteranalysen. Beispiele sind hierfür der in Abschnitt 2.1 beschriebene k-Means Algorithmus oder der DBSCAN Algorithmus, bei dem die Eingabedatenmenge
mit sich selbst verglichen wird, um Ähnlichkeiten zu finden. Die Ausgabeparameter von cross
bestehen also aus einer einzelnen Menge, welche aus sämtlichen Kombinationen aller Elemente
der Daten- und Referenzmenge besteht. Das zweite cPIPE-Konstrukt stellt „row“ dar. Dieser
Operator arbeitet, wie der Name impliziert, zeilenbasiert. Seine Eingabeparameter bestehen aus
einer einzelnen Menge. Diese beinhaltet sämtliche Tupel einer Matrix, wie sie beispielsweise vom
cross Operator generiert wird. Abbildung 2.6b stellt die jeweiligen Eingabemengen hellgrau bzw.
dunkelgrau hinterlegt dar. Dieser Operator realisiert, ausgehend von einem Matrixschema als
Datengrundlage, eine Partitionierung der Elemente nach der Datenmenge. Der dritte, neu eingeführte Operator ist „col“ und wird durch Abbildung 2.6c dargestellt. Der col Operator stellt das
vertikale Pendant zum row Operator dar. Die Eingabeparameter sind ebenfalls auf eine Menge
begrenzt, in der Abbildung hell- und dunkelgrau hervorgehoben. Analog zu row erwirkt die Verwendung des col Operators eine Partitionierung der Datengrundlage nach der Referenzmenge.
Für alle vorgestellten Operatoren gilt, dass diese sich an der Datengrundlage aus [Hah14] orientieren, also grundlegend zur Verarbeitung von Daten im Stile einer Matrix erdacht sind. Von
technischer Seite betrachtet bedeutet dies, dass sie mit Tripeln arbeiten. Diese sind von der Form
ZeilenID-SpaltenID-Wert. Jeder der Operatoren verwendet außerdem eine zuvor in PIPEBASE
spezifizierte Nutzerfunktion. Grundlegend kann also festgestellt werden, dass jede cPIPE als
Proxy zwischen der Ausführungsumgebung und der Nutzerfunktion arbeitet. Die Daten werden auf eine Art und Weise aufbereitet, wie die Nutzerfunktion sie zur Berechnung benötigt.
Anschließend wird deren Rechenergebnis wieder, sofern notwendig, in die vom Framework benötigte Form umgewandelt, sodass diese dann weiter verarbeitet werden können und gegebenenfalls weiteren cPIPEs als Eingabedaten dienen können.
2.3.3
Beispiele
Im Bereich des Data Clustering unterscheiden sich die bearbeiteten Datenmengen innerhalb eines
Algorithmus. Oft wird mit zwei Mengen, einer Daten- und einer Referenzmenge gestartet, aus
denen Zwischenergebnisse generiert werden. Basierend auf dieser Ergebnismenge muss gegebenenfalls auch auf die zugehörigen Daten- und Referenzpunkte zurückgegriffen werden, um deren
Koordinaten zu manipulieren. Dies ist beispielsweise beim k-Means Algorithmus der Fall. Um
k-Means in PIPEBASE zu formulieren, werden lediglich drei cPIPEs benötigt; jeweils eine Instanz
von cross, row und col. Der folgende Vorgang kann durch Abbildung 2.7 nachvollzogen werden.
Als erstes müsste die euklidische Distanz von allen Datenpunkten zu allen Referenzpunkten mit
Hilfe des cross Operators berechnet werden. Jedes andere Distanzmaß würde ebenfalls funktionieren. Anschließend wird durch eine Minimumfunktion von row die minimale Entfernung eines
Datenpuntkes zu einem Referenzpunkt ermittelt, wobei dessen Position in der Matrix mit „1“ ersetzt wird und der Rest der Zeile zu nullen ist. Abschließend würde durch einen col Operator
jeder Punkt aus der Datenmenge ausgewählt werden, dessen Minimum beim gleichen Referenz-
21
Kapitel 2 PIPE-basierte Spezifikation paralleler Clustering-Algorithmen
Re f 1

Data1 3.699
Data2 
 2.327
Data3 
 1.056
Data4 
 2.113
Data5  4.180
Data6 2.694
Re f 2
3.422
2.725
4.614
0.376
1.986
4.310
Re f 3
2.591
1.245
2.571
4.085
4.543
2.953
Re f 4

0.545
3.821 

1.224 

4.343 

1.405 
2.902

Data1
Data2 

Data3 

Data4 

Data5 
Data6
Re f 1
0
0
1
0
0
1
Re f 2
0
0
0
1
0
0
Re f 3
0
1
0
0
0
0
Re f 4

1
0 

0 

0 

1 
0
(a) Berechnete Distanzen des Kreuzproduktoperators (b) Substituierte Minima des Zeilenoperators
Re f 10
Data1
0
Data2 
 0
Data3 
 0
Data4 
 0
Data5  0
Data6
0

Re f 20
0
0
0
0
0
0
Re f 30
0
0
0
0
0
0
Re f 40

0
0 

0 

0 

0 
0
(c) 0-initialisierte Matrix mit neuen Referenzpunkten
Abbildung 2.7: Schrittweise Veränderung der Matrizen durch eine k-Means Variante
punkt liegt. Diese Punkte sind durch die mit einer 1 gekennzeichneten Spalten zu wählen. Die
Nutzerfunktion des col Operators mittelt alle Koordinaten und generiert so die neue Referenzmenge. Dieser Ablauf müsste solange wiederholt werden, bis die ausgegebene Referenzmenge
keinen Unterschied mehr zur eingegebenen Referenzmenge aufweist. Die Spezifikation einer row
cPIPE wird in Code Beispiel 2.1 in der ursprünglichen Haskellfassung dargestellt.
Zu sehen sind drei relevante Codeabschnitte, um eine cPIPE zu beschreiben. Mit Hilfe der zuerst auftretenden Variablendeklaration kann der in Abschnitt 3.1 näher beschriebene Source-toSource Compiler einfacher die Datentypen von verwendeten Variablen erkennen. Ein mit „Start“
beginnender Kommentar bedeutet, dass der nachfolgende Code die eigentliche Funktionalität beschreibt, wobei der „FunctionCall“-Kommentar dem Compiler mitteilt, dass der folgende Block
die Aufrufmodalitäten des Operators beinhaltet. Jede Codeeinheit wird mit einer kommentierten Raute beendet. Diese Methode vereinfacht das Einlesen der jeweiligen Codebereiche für den
Compiler extrem. Es müssen keine aufwändigen Methoden geschaffen werden, um die Segmente
voneinander unterscheiden zu können. Wie Code Beispiel 2.1 ebenfalls zu entnehmen ist, fehlen
die für Haskell typischen Tabulatoren nach Zeilenumbrüchen. Das Vorhandensein oder Weglassen selbiger ist aufgrund der Komplexität der untersuchten Algorithmen unerheblich und wurde
aus diesem Grund nicht als obligatorisch eingestuft. Weiterhin ist der Umfang der unterstütz-
//VariableDefinition
inList1 :: [[Double]]
inList2 :: [[Double]]
//#
//StartMinimum
calcMin :: [Double] -> Double
calcMin inList = minimum inList
//#
//FunctionCallMinimum
//rows
//OperatorArgs:inList1,inList2
//InputType:Value
//Fusion:Euklid
[ calcMin inList2!!i
| i <- [ 0 .. (length inList1) ]
]
//#
Code Beispiel 2.1: Haskellbasierter Ansatz: Beispielcode eines zeilenweise arbeitenden Minimum Algorithmus
22
2.3 cPIPE-Ansatz für Clustering-Algorithmen
ten Funktionen seitens Haskell zunächst eingeschränkt und aus Gründen der Effizienz nur auf
ein Subset eingegrenzt, welches für die Realisierung der zu testenden Algorithmen relevant ist.
Eine weitere strukturelle Regel stellt die Reihenfolge der Spezifikation dar. Die Haskelldatei ist
so konzipiert, dass die darin enthaltenen Informationen für den Source-to-Source Compiler ausreichend sind, um einen Analysevorgang starten zu können. Eine solche Datei repräsentiert also
immer genau einen Clustering-Algorithmus. Da dieser aus mehreren, verketteten cPIPEs bestehen kann, muss auch eine Reihenfolge selbiger angegeben werden. Diese wird trivialer Weise
direkt aus der Reihenfolge extrahiert, in der die „//Start“ Blöcke eingelesen werden. Bei oberflächlicher Betrachtung von Code Beispiel 2.1 könnte der Eindruck entstehen, dass anstatt der
Programmkonstrukte eher Pragmas zur Anwendung kommen. Diese stellen compilerspezifische
Anweisungen dar, welche nur dann ausgeführt werden, wenn der aktive Compiler jene auch
kennt. Bei unbekannten Pragmas werden diese ohne die Ausgabe von Fehlern oder Warnungen
übergangen. Auf diese Weise wird beispielsweise in OpenMP die parallelisierte Verarbeitung von
sonst sequentiellem Code reguliert. Die Annotationen im Haskellprogramm sind allerdings als
Hinweise zu deuten, welche das Einlesen des Codes vereinfachen sollen und dürfen in keinem
Fall ignoriert werden, da sonst Inkonsistenzen auftreten. Eine semantische Ähnlichkeit mit Pragmas besteht nicht. Durch die Einführung der Annotationen im Code sollen die dem Reifegrad
der Sprache entsprechenden Mängel auf einfache Weise ausgeglichen werden können.
Der zuvor beschriebene k-Means Algorithmus wird in Code Beispiel 2.2 komplett in der weiterentwickelten Form von PIPEBASE dargestellt. Mit der reinen, auf Haskell basierten Fassung hat
diese Version gemein, dass die Reihenfolge der Operatoren durch deren Auftreten innerhalb der
Spezifikationsdatei bestimmt ist. Die Beschreibung der Nutzerfunktionen findet weiterhin durch
die an Haskell angelehnte Syntax statt. Die zweite Entwicklungsstufe von PIPEBASE arbeitet die
Modularität der cPIPEs besser heraus und zeigt, wie einzelne Elemente miteinander verknüpft
werden können.
Dass die neuere PIPEBASE Version abwärtskompatibel ist, wird durch Abbildung 2.8 gezeigt.
Auf der linken Seite ist zu sehen, wie die Variablendeklaration der neuen Version (oben) in den
haskellbasierten, ursprünglichen Ansatz überführt werden kann. Die Funktionsdefinition auf der
rechten Seite wird analog behandelt. Jedes Element der neuen PIPEBASE-Formulierung ist entsprechend seinem Vorgängerpendant eingefärbt. Einzig der „Fusion“ Kommentar, durch dessen
Anwesenheit Hinweise zur Optimierung gegeben werden, ist bisher nocht nicht abbildbar.
A = cross( "points", D, R, functionEuklid
B = row( "values", A, functionMinimum );
D = Vector< Vector< Double > >;
R = Vector< Vector< Double > >;
//VariableDefinition
inList1 :: [[Double]]
inList2 :: [[Double]]
//#
);
//FunctionCallMinimum
//rows
//OperatorArgs:inList1,inList2
//InputType:Value
//Fusion:Euklid
[ calcMin inList2!!i
| i <- [ 0 .. (length inList1) ]
]
//#
Abbildung 2.8: Transformation von PIPEBASE in den reinen Haskell Ansatz
23
Kapitel 2 PIPE-basierte Spezifikation paralleler Clustering-Algorithmen
-- Haskell Funktionsdefinition
functionEuklid = "euklid :: [Double] -> [Double] -> Double
euklid p1 p2 = (sqrt . sum) squaresOfDifferences
where
squaresOfDifferences = zipWith squareDist p1 p2
where
squareDist x1 x2 = dist * dist
where
dist = x1 - x2";
functionMinimum = "calcMin :: [Double] -> Double
calcMin inList = minimum inList";
functionKMeans = "kmeans :: [Double] -> Double
kmeans inList1 = [sum inList1] / [length inList1]";
-- Daten- und Referenzmengendefinition
D = Vector< Vector< Double > >; -- Datenliste mit 2-dimensionalen Punkten
R = Vector< Vector< Double > >; -- Referenzlistemit 2-dimensionalen Punkten
-- cPIPE Definition
for ( 50 ) { -- 50 Iterationen
-- Berechnet Distanzen
A = cross( "points", D, R, functionEuklid
);
-- Ersetzt Minima mit 1, rest der Zeile mit 0
B = row( "values", A, functionMinimum, "1" );
}
-- 1-markierte Punkte werden functionKMeans übergeben
R = col( "points", B, functionKMeans );
Code Beispiel 2.2: Alternative Spezifizierung, leichter lesbar
2.4
ZUSAMMENFASSUNG
Als maßgeblicher Trend für zukünftige Entwicklungen im Hard- und Softwarebereich ist die parallele Programmierung im Fokus aller Bereiche. Aufgrund der vielen verschiedenen Systemkombinationen und Komponenten ist die Spezifikation von parallelen Algorithmen nicht immer
trivial. Um die Nachteile der plattformnahen Entwicklung zu überdecken, hat Google mit MapReduce ein System entwickelt, welches genau dieses Problem beseitigen konnte. Durch dessen
Erweiterung mit dem PACT Programmiermodell wurde ein weiterer Freiheitsgrad gewonnen. Indem die PACTs nicht mehr an ein starres Gerüst gebunden sind, sondern frei verbunden werden
können. Als universelles Mittel zur plattformunabhängigen Spezifikation von parallelen Algorithmen bietet das abstrakte PIPE Modell ein System, mit dem es keine Limitierung mehr auf
eine einzelne Domäne gibt. Die durch das System zur modularen Clusteranalyse von [Hah14] inspirierten PIPEs sind ähnlich zu [Ale11]. Die Definitionen der spezialisierten cPIPEs und PACTs
funktionieren analog. Ebenfalls bieten beide Modelle eine Möglichkeit, vom MapReduce Standard abzuweichen und eine dynamische Abarbeitung von komplexeren Aufgaben zu ermöglichen. Genau wie bei PACT ist die Reihenfolge der cPIPEs unerheblich, sodass eine konkrete Algorithmenimplementierung als Orchestrierung der zu verwendenden PIPE-Elemente bzw. deren
24
2.4 Zusammenfassung
Ausprägungen, im Falle des Data Clusterings den cPIPEs, erstellt wird. Die beiden Ansätze unterscheiden sich allerdings in zwei wichtigen Punkten. PACT wird durch ein eigenes System realisiert und arbeitet auf Schlüssel-Wert Paaren. PIPE ist hingegen frei vom Datenschema, obgleich
jede Ausprägung sich auf ein Datenschema festlegen muss. Im Falle von cPIPE wäre die Basis für
die interne Datenhaltung ein Schema bestehend aus Tripeln mit der Form ZeilenID-SpaltenID-Wert. Weiterhin ist PACT an die eigene Ausführungsumgebung gebunden, wohingegegen PIPE
durch seine Abstraktion als absolut plattformunabhängig betrachtet werden kann.
25
Kapitel 2 PIPE-basierte Spezifikation paralleler Clustering-Algorithmen
26
3
PIPE COMPILER FÜR
EFFIZIENTE AUSFÜHRUNG
Das PIPE Konzept aus Kapitel 2 stellt einen abstrakten Ansatz zur Beschreibung von parallelen Algorithmen dar. Die cPIPE Ausprägung für den Bereich Data Clustering ist ebenfalls noch
plattformunabhängig und muss durch einen Übersetzer in plattformabhängigen Programmcode
übersetzt werden. Dafür bietet sich die Verwendung eines Source-to-Source Compilers an, wie
in Abbilung 3.1 dargestellt ist. Ziel ist es, die domänenspezifischen, aber plattformunabhängigen
cPIPEs in für eine Ausführungsumgebung spezifischen Code auf effektive Weise umzuwandeln.
Zu diesem Zweck werden die Umgebungen MapReduce und OpenCL im Detail betrachtet.
Eingabe 1
Beschreibung
Ausgabe 1
Eingabe n
func
Ausgabe n
Plattformunabhängig
Source-to-Source Compiler
MapReduce
OpenCL
Plattformabhängig
...
Abbildung 3.1: PIPEBASE Schema
27
Kapitel 3 PIPE Compiler für effiziente Ausführung
3.1
VORBETRACHTUNGEN
Im Folgenden Abschnitt werden theoretische Grundlagen zum Thema Source-to-Source Compiling sowie zu den angestrebten Ausführungsumgebungen MapReduce und OpenCL gegeben.
Neben historischen Daten werden auch ähnlich geartete Arbeiten vorgestellt.
3.1.1
Source-to-Source Compiling
Der Gedanke, Quellcode einer Sprache in den einer anderen umzuwandeln, ist keineswegs neu.
Bereits 1977 verfasste Loveman einen Artikel, welcher die Möglichkeiten der Programmoptimierung mittels Source-to-Source Transformation thematisiert [Lov77]. Programme seien schon immer an Zeit- oder Kapazitätsgrenzen gebunden, was entweder durch deren Ausführung auf Minicomputern, Echtzeitanforderungen oder ökonomische Betrachtungsweisen aufgrund mehrfacher
Ausführung des selbigen begründet ist [Lov77]. Die Optimierung selbst sei schlußendlich nur das
Ergebnis sukzessiv ausgeführter Transformationen, um einen Quellcode in anderen Quellcode zu
überführen. Dafür nimmt er an, dass von einem Programm eine baumstrukturierte Repräsentation erstellbar ist und dass Souce-to-Source Transformationen patterngesteuerte Überführungen
von Baumdarstellungen des Programmcodes sind. Zu seinen Erkenntnissen zählt, dass nicht
jede anwendbare Transformation einen Gewinn für die Leistungsfähigkeit des Programms bringen muss und dass in der Regel die Anwendung einer Transformation ihre logischen Nachfolger
automatisch impliziert [Lov77]. Loveman stellte fest, dass bei der Programmierung die Formulierung eines problemlösenden Algorithmus im Vergleich zu dessen Effizienz höher priorisiert
ist. Das aus dem Quellcode kompilierte Programm neige deswegen dazu, weniger effizient zu
sein. Zu seinen Erkenntnissen zählt, dass Performanz großes Gewicht hat. Um das volle Potenzial von Programmen auszuschöpfen, sei eine transparente Nutzung von Source-to-Source
Transformationen notwendig [Lov77]. Code Beispiel 3.1 zeigt den Nutzen von Source-to-Source
Transformation mittels des eingegebenen Codes und dem fertig optimierten Produkt. Es handelt
sich um einen Algorithmus zur Multiplikation diagonaler Matrizen. Aufgrund a priori vorhandener Informationen wie dieser können mehrere Transformationen durchgeführt werden, welche
den stark vereinfachten Code auf der rechten Seite generieren.
Einen aktuelleres Beispiel stellt die Arbeit von Vitaly Schreibmann dar. Er verwendet eine DSL,
um mit einem modellgesteuerten Softwareentwicklungsprozess eine Representational State Transfer (REST) konforme Programmierschnittstelle generieren zu lassen. Anders als eine Generalpurpose Language (GPL) ist eine DSL darauf ausgelegt, nur die Probleme der eigenen Domäne
loop for i = 1 to 10
loop for j = l to 20
c[i,j] = 0,
loop for k = 1 to 10
c[i,j] = a[i,k] * b[k,j] + c[i,j]
repeat
repeat
repeat
loop for i = 1 to 10
loop for j = 1 to 20
c[i,j] = a[i,j] * b[i,j]
repeat
repeat
Code Beispiel 3.1: Anfang und Ende der Source-to-Source Transformation eines Matrix-Multiplikationsalgorithmus, entnommen aus [Lov77]
28
3.1 Vorbetrachtungen
abzubilden. Dies dient dem Zweck, domänenspezifischen Experten die Programmierung solcher
Schnittstellen ohne externes Wissen zu ermöglichen und die Komplexität der Programmierung
auf ein Minimum zu reduzieren. Weiterhin soll dadurch die Qualität des Codes gesteigert, der
gesamte Entwicklungsprozess verkürzt und REST Konformität garantiert werden [Sch14].
Ähnlich zu dieser Arbeit verwendet [Das11] einen skeletonbasierten Ansatz. SkePU steht für die
zwei Begriffe „Skeleton“ und „PU“. Der Begriff des Skeletons wird von [Das11] als vordefinierte
Fuhnktion definiert, die vom Programmierer zur Umsetzung seines Algorithmus zu verwenden
sind. Sie stellen also zuvor verfasste Funktionsgerüste dar, welche lediglich mit den relevanten
Code für die aktuelle Problemstellung gefüllt werden müssen. „PU“ steht für Processing Unit
[Das14]. Der Name soll implizieren, dass die vordefinierten Skeletons mit einer Vielzahl von
möglichen Backends, also bereits für Zielplatformen optimierte Implementierungen, ausgelegt
sind und so ein breites Anwendungsspektrum bieten. [Das11] beschreibt die aktuelle Verfügbarkeit von Skeletons für „map“, „reduce“, „mapreduce“, „map-with-overlap“, „maparray“ sowie „scan“. Ausserdem sei das Framework so allgemein gehalten, dass es mehere Architekturen
unterstützt und auch für sequentiellen Code für Central Processing Units (CPUs) sowie parallelisierte OpenMP-Implementierungen bei der Verwendung von Mehrkern-CPUs ausgelegt ist
[Das11].
Das Compiler-Framework Cetus ist der Arbeit von Loveman ähnlich. Es stellt eine Softwaresuite
bereit, mit der ANSI C Programme Source-to-Source transformiert werden können. Seinen Ursprung hat Cetus in einem Kursprojekt einiger Compilerbau-Studenten. Deren Aufgabe war es,
einen Source-to-Source Compiler für C zu entwickeln. Geschrieben wurde das Projekt in Java,
beinhaltet keinen proprietären Code und basiert ausschließlich auf frei verfügbaren Werkzeugen. Anders als diese Arbeit setzt Cetus auf die Verwendung eines vorhandenen Präprozessors,
um dessen Ausgabe für die Erstellung seiner internen Repräsentation des Codes zu verwenden
[Lee04].
3.1.2
Ausführungsumgebungen
Verschiedene Systeme, seien es Hard- oder Software, bedeuten unterschiedliche Bedingungen.
Hardwareseitig betrachtet reicht die Vielfältigkeit von Plattformen beginnend bei Standardsystemen mit nur einem (Mehrkern-)Prozessor und gipfelt in kompletten, auch dezentral verwalteten, Rechenzentren, in denen Hochleistungsserver mit einer Vielzahl von Recheneinheiten stehen. Für den Programmierer ist es in der heutigen Zeit unerheblich, ob es sich um die klassische
CPU handelt, oder um einen Rechencluster bestehend aus Grafikkarten bzw. Graphics Processing
Units (GPUs). Für vielerlei solcher Systeme, seien sie homo- oder heterogen, wurden Frameworks
entwickelt, um effiziente, transparante und skalierbare Anwendungen zu entwickeln, welche die
volle Leistungsfähigkeit ihres Zielsystems ausnutzen können. Zu diesem Zweck existieren unterschiedlichste Ausführungsumgebungen. Sie beschreiben den Kontext, in dem eine Applikation funktioniert; eine Schnittstelle zur Kommunikation mit der zu Grunde liegenden Hardware.
Um aktuellen Applikationen ein Höchstmaß an Skalierbarkeit und Effizienz zu verleihen, setzen
Programmierer immer mehr auf eine verteilte Berechnung der internen Datenströme. Wie in Abschnitt 2.1.2 angedeutet, stellt MapReduce einen bekannten Vertreteter einer solchen verteilten
Ausführungsumgebung dar. Weitere Alternativen sind unter Anderen OpenMP, OpenCL und
CUDA, über die in [Yan11] sowie [Khr14] eine grundlegende Übersicht gegeben werden. Abhän-
29
Kapitel 3 PIPE Compiler für effiziente Ausführung
gig von der verwendeten Programmiersprache kann der selbe Code in diversen Umgebungen
verwendet werden. Je nach dem, wie unterschiedlich die gewählten Ansätze sind, müssen Adaptionen nur in geringfügigem Maße durchgeführt werden. Um effektive Tests durchführen zu
können, orientiert sich diese Arbeit an den zwei repräsentativen Umgebungen MapReduce sowie
OpenCL.
Sowohl MapReduce als auch OpenCL weisen die grundlegende Ähnlichkeit auf, dass eine spezifizierte Funktion auf unterschiedlichen Daten arbeitet, was auch als Single Instruction, Multiple
Data (SIMD) bezeichnet wird [Gro13]. Sie unterscheiden sich allerdings hinsichtlich der Art der
Partitionierung der Daten. Bei einem MapReduce-Job werden diese nicht zwangsweise in eine
Anzahl zerlegt, die der Menge an vorhandenen Mappern entspricht. Bei einer Vektormultiplikation mittels OpenCL ist dies allerdings notwendig. Weiterhin haben beide die Eigenschaft gemein,
dass durch sie ein Framework bereit gestellt wird, welches von Anwendern programmierten, sequentiellen Code parallelisiert ausführt, der auch auf Just-in-Time (JIT)-kompilierte Weise in das
System eingebracht werden kann. Eine Synthese dieser beiden Systeme stellt HadoopCL dar.
Dieses Framework ermöglicht es, ein MapReduce System innerhalb heterogener Rechencluster
mit Hilfe von OpenCL zu verbinden und so die Stärken beider Technologien zu vereinen. Programmcode von Nutzern soll auf diese Weise auf allen Zielplattformen ausführbar sein [Gro13].
3.1.3
MapReduce
MapReduce stellt ein von einem Master gesteuertes System dar, welches zur effizienten Prozessierung großer Datenmengen von der Firma Google entwickelt wurde. Zu Grunde liegt die Idee, aus
einer Vielzahl von potentiell fehleranfälligen Rechnern einen Cluster zu bauen, der fehlertolerant
Benutzerprogramm
(1) fork
(1) fork
(1) fork
Master
(2) Map Zuordnung
(2) Reduce Zuordnung
split 0
Arbeiter
Arbeiter
split 1
Ausgabedatei 0
(6) Schreiben
split 2
(3) Einlesen
Arbeiter
(4) Lokales
Schreiben
(5) Externes
Lesen
split 3
Arbeiter
Ausgabedatei 1
Reduce Phase
Ausgabedateien
Arbeiter
split 4
Daten
Partitionierung
Map-Phase
Temporäre Dateien
(Mapper-lokal)
Abbildung 3.2: MapReduce Prozess, entnommen aus [Dea08]
30
3.1 Vorbetrachtungen
arbeiten kann. Stellt ein Benutzer eine Anfrage an den MapReduce-Cluster, werden zunächst die
notwendigen Daten in einer vom Nutzer zu spezifizierende Größe partitioniert. Anschließend
werden Master-, Mapper- und Reducer-Instanzen auf den verschiedenen Rechnern innerhalb des
Clusters gestartet. Darauf folgt der namensgebende Map-Reduce-Zyklus. Jeder Mapper liest die
ihm vom Master zugewiesenen Daten ein und erstellt Schlüssel-Wert-Paare. Diese werden in
lokalen, temporären Dateien zwischengespeichert, bis sie von einem Reducer weiterverarbeitet
werden. Dieser kombiniert die jeweiligen Datenpakete in einer Ausgabedatei. Kommen mehrere
Reducer zum Einsatz, müssen diese Dateien abschließend kombiniert werden. Der gesamte Vorgang kann mit Hilfe von Abbildung 3.2 nachempfunden werden. Der Master steuert die Zuteilung der Daten sowie die Aufgabenverteilung, welcher Porzess Mapper- oder Reduceraufgaben
übernimmt. Der Master überwacht permanent die Lebendigkeit der aktiven Jobs. Sollte einer
jener Prozesse abstürzen, so wird dessen gesamte Aufgabe an einen neuen Mapper weiter gegeben und neu gestartet. Die bisher in den temporären Dateien abgelegten Rechenergebnisse
gehen so verloren. Es ist allerdings effektiver, die vollständige Arbeit von neuem zu starten, als
aufwändige Datensicherungsmaßnahmen wie beispielsweise Replikationen zu verwenden. Eine
gute Übersicht zum kompletten MapReduce-Prozess bietet [Dea08].
3.1.4
OpenCL
OpenCL steht für Open Computing Language und stellt den ersten, offenen und kostenlosen
Standard für parallele Programmierung von Mehrkernprozessoren dar [Khr14]. Bisher wurden
4 Versionen veröffentlich, von denen OpenCL 2.0 die aktuellste ist. Dem System liegt die Idee
zu Grunde, dass eine klassische Schleife durch eine N-dimensionale Berechnungsdomäne ausgetauscht werden kann. Abbildung 3.3 illustriert einen solchen Ablauf und Code Beispiel 3.2 zeigt
C++
Programm
func( p1, p2 )
func( p1, p3)
func( p1, p.. )
C++
Programm
OpenCL Kontext
Buffer Strukturen
Kernel Initialisierung
func( p1, pN )
Asynchroner Kernel
Aufruf
Synchronisation
Ergebnisverarbeitung
Abbildung 3.3: Illustrierter Ablauf eines OpenCL gestützten Programmes
31
Kapitel 3 PIPE Compiler für effiziente Ausführung
diese Idee in C Code. Bei der klassischen Variante, auf der linken Seite, wird die Schleife N mal
auf serielle Weise ausgeführt. Der OpenCL-Kernel rechterhand wird hingegen N mal parallel berechnet. Dadurch kann die Ausführungszeit deutlich vermindert werden, je stärker die Anzahl
verfügbarer Prozessoren gegen N konvergiert. Ein OpenCL-basiertes Programm muss dafür zunächst immer einen OpenCL-Kontext initialisieren. Dieser wird auf die Zielplattform, also den
zu verwendenden Prozessortyp, ausgelegt. Ist ein solcher Kontext einmal erstellt, können sämtliche verfügbaren OpenCL-Kernels zu jeder Zeit ausgeführt werden. Grundlegend kann eine
Vielzahl von Prozessortypen verwendet werden. Gängige Beispiele hierfür sind unter anderen
CPUs, GPUs oder Field Programmable Gate Arrays (FPGAs). Das System wurde so konzipiert,
dass derselbe Code auf allen genannten Prozessoren problemlos ausgeführt werden kann, was
wiederum durch den zuvor initialisierten OpenCL-Kontext realisiert wird. Programmiert wird in
einem Subset des ISO C99 Standards [Tra13], wobei mittlerweile auch Erweiterungen hinsichtlich
C++ Features wie Vererbung oder Templates vorhanden sind [Ros11]. Für grundlegende Informationen und weitergehende Lektüre wird auf [Tra13] und [Sto10] verwiesen.
void vectorMult(
const float* a,
const float* b,
float* c,
const unsigned int count)
{
for(int i=0; i<count; i++)
c[i] = a[i] *b[i];
}
kernel void vectorMult(
global const float* a,
global const float* b,
global float* c)
{
int id = get_global_id(0);
c[id] = a[id] * b[id];
}
Code Beispiel 3.2: Traditionelle Schleife vs. OpenCL Ansatz, entnommen aus [Tra13]
3.2
PIPE COMPILER ANSATZ
Eine fertige Algorithmenbeschreibung in PIPEBASE enthält eine Menge von PIPE Elementen in
einer domänenspezifischen Ausprägung. Im Falle des Data Clusterings werden die cPIPEs betrachtet. Die Aufgabe des Source-to-Source Compilers besteht darin, die von einem Entwickler
verfasste Haskelldatei in PIPEBASE-Syntax zu lesen und zu verarbeiten. Dabei gilt es, mehrere
Herausforderungen zu bewältigen. Zunächst müssen die Funktionsrümpfe erstellt werden, die
den cPIPEs selbst entsprechen. Diese können entweder direkt übersetzt werden oder als vorgefertigte Implementierung bereits vorhanden sein. Im Rahmen von MapReduce entsprächen diese
Rümpfe den namensgebenden Funktionen. Für OpenCL repräsentieren die cPIPEs die Kernels.
Der nächste Schritt beinhaltet die Transformation der Funktionen erster Ordnung in die jeweilige
Syntax der angestrebten Ausführungsumgebung. Dabei sollen, entsprechend der Definition von
PIPE, aus der selben Spezifikation sowohl die Inhalte der Map und Reduce Funktionen, als auch
die Funktionen innerhalb der OpenCL Kernels generierbar sein. Dafür muss der Source-to-Source
Compiler die Syntax der in Haskell spezifizierten Nutzerfunktionen sowie die Syntax der Zielsprachen kennen und über Regeln verfügen, um Haskell in die jeweilige Zielsprache übersetzen
zu können. Am Ende dieses Prozesses soll die zu verwendende Ausführungsumgebung gestartet
werden, um den Analysevorgang zu beginnen.
Das ganze System wurde so konzipiert, dass dem Compiler zwei obligatorische Kommandozei-
32
3.2 PIPE Compiler Ansatz
lenparameter zu übergeben sind, wobei auch ein optionales hinzugefügt werden kann. Notwendig ist der Pfad zur Datei mit dem Algorithmus sowie die Spezifikation, ob das MapReduce oder
OpenCL Framework verwendet werden soll. Das optionale Argument lautet „filter“. Es weist
den Compiler an, Annotationen der Form „//Fusion:Funktionsname“ zu beachten, wie sie in
Code Beispiel 2.1 zu sehen ist. Zunächst muss allerdings erst eine interne Darstellung der spezifizierten Algorithmen angelegt werden. Ähnlich dem Baukasten für Data Clustering aus [Hah14]
soll auch der Compiler modular aufgebaut sein. Dadurch wird gewährleistet, dass neben MapReduce und OpenCL durch zukünftige Entwicklung auch andere Ausführungsumgebungen ohne
erheblichen Aufwand unterstützt werden können. Wie in Abschnitt 2.2 angedeutet, wird für jede
Umgebung auch eine spezifische Implementierung der PIPEs vorhanden sein. Um eine neue
Umgebung zu unterstützen, muss dem Compiler also lediglich ein neues Ausgabemodul hinzugefügt werden. Dieses kennt die spezifische Syntax der neuen Ausführungsumgebung und kann
aus der internen Darstellung den Quellcode für die neue Plattform erzeugen.
Nach dem Einlesen der Codeblöcke werden diese mit jeweils eigenen Methoden verarbeitet und
deren Informationen extrahiert. Der Compiler arbeitet hierbei mit der LL(1) Methode. Das bedeutet, dass der Programmcode von links nach rechts gelesen wird und anschließend eine Linksableitung stattfindet. Fundierte Informationen zum Thema LL- bzw. LR-Parsing können [Par11]
entnommen werden. Bei der Generierung der internen Darstellung wird für jeden Operator ein
Objekt angelegt. Dieses beschreibt seine enthaltenen Symbole und ggf. Unterfunktionen, falls
diese sich aus der Implementierung ergeben. Dafür wurden Ableitungsregeln erstellt, welche der
Haskellsyntax nachempfunden sind. In Orientierung an Abbildung 3.6 besteht eine feste Reihenfolge, welche die einzelnen Schritte steuert. Um von Position (1) zu (2) zu gelangen, wird
zunächst die vollständige Datei gelesen und zeitglich markiert, an welcher Stelle die jeweiligen
Codeblöcke zu finden sind. Bei der Verarbeitung eines Funktionsblocks wird auf folgende Weise
vorgegangen: Befindet sich in dessen erster Zeile beispielsweise eine Zeichenkette mit n Parametern gefolgt von einem doppelten Doppelpunkt gefunden, so muss diese der Funktionsdefinition
entsprechen und kann daraufhin in ihre Bestandteile zerlegt werden. Anschließend erfolgt eine
zeilenweise Verarbeitung des Codes, gestützt von einer Art Mustererkennung. Bei der Transition
von Zustand (2) zu (3) findet die tatsächliche Abbildung vom plattformunabhängigen Algorithmus zur spezifischen Implementierung statt. Nachdem alle Blöcke auf diese Weise verarbeitet
wurden, beginnt die Übersetzung in die Zielsprache. Dies geschieht ebenfalls anhand einfacher
Regeln und wird durch die, so allgemeingültig wie möglich gehaltene, interne Darstellung vereinfacht. Ein solcher Ableitungsvorgang soll anhand des Codes zur Berechnung der euklidischen
Distanz aus Code Beispiel 2.2 durch Abbildung 3.4 schrittweise verdeutlicht werden.
Zu Beginn der Transformation wird der gesamte Körper des Algorithmus analysiert, um die von
einer Ersetzung oder Erweiterung betroffenen Bezeichner zu entdecken. Im Beispiel betrifft das
„squareOfDifferences“ sowie „squareDist“. Ersteres ist ein Platzhalter, um eine bessere Lesbar-
(1)
(2)
(3)
(4)
(5)
(6)
squareOfDifferences - Funktion: zipWith Argumente: squareDist, p1, p2
static double squareDist( double p1, double p2 ) {...}
sqrt( sum )
sqrt( haskell_sum( squareOfDifferences ) )
sqrt( haskell_sum( zipWith ) ) )
sqrt( haskell_sum( haskell_zipWith( squareDist, p1, p2 ) ) )
Abbildung 3.4: Schrittweise Generierung des Funktionsaufrufs für die Berechnung der euklidischen Distanz
33
Kapitel 3 PIPE Compiler für effiziente Ausführung
keit der Zeile zu garantieren. Zweiteres stellt ein Alias für eine eingebettete Funktion dar. Die
Symbole werden schrittweise in der Reihenfolge ihrer Erkennung verarbeitet und zwischengespeichert bzw. direkt in die Ausgabedatei geschrieben, siehe (1) und (2). Die Schritte (3) bis
(6) zeigen die schrittweise Ersetzung der Funktionsnamen, basierend auf der Erkennung ihrer
Parameter. Begründet durch die Haskellsyntax wurde eine linksorienterte Ersetzungsstrategie
gewählt. Ausgehend von der am weitesten links stehenden Funktion werden deren Parameter erschöpfend durch die resultierenden Funktionsaufrufe ersetzt, bis schließlich der komplette
Operatorkern entstanden ist. Damit dieser Prozess ohne externe Einwirkung funktionieren kann,
muss jedes Ausgabemodul des Source-to-Source Compilers die genaue Struktur der Datengrundlage einer Ausführungsumgebung kennen und wissen, wie die erwarteten Parameter der verwendeten cPIPEs definiert sind.
Während des Transformationsvorgangs konnten die Vorteile der zu Beginn von Kapitel 3 beschriebenen skeletonbasierten Programmierung nutzbar gemacht werden. Gerüste von OpenCL
Kernels bzw. von MapReduce verwendeter Code müssen lediglich befüllt werden und können so
in ihrer Struktur auch leichter angepasst werden, falls dies nötig werden sollte. Sobald die Transformation abgeschlossen ist, wird die angestrebte Ausführungsumgebung kompiliert und somit
zu Punkt (4) aus Abbildung 3.6 übergegangen. Nachdem die interne Darstellung vervollständigt
wurde, wird diese als Basis für die Codegeneration genutzt. Grundlegend gilt, dass der Compiler
abhängig von der Ausführungsumgebung eine Ausgabedatei erstellt. Im Fall von MapReduce
entsteht eine Headerdatei, welche eine Klasse enthält. Diese Klasse wird mit sämtlichen übersetzten cPIPEs und deren Unterfunktionen gefüllt. Für OpenCL wird eine Kerneldatei erstellt.
Diese enthält normalen C Code. Abbildung 3.5 zeigt vereinfacht, auf welche Weise der Minimumoperator aus Code Beispiel 2.1 in C++ Code für das MapReduce Framework überführt wird.
//StartMinimum
calcMin :: [Double] -> Double
calcMin inList = minimum inList
//#
static double calcMin( vector< double > inList ) {
return haskell_minimum( inList );
}
static double haskell_minimum( vector< double > inVec ) {
double minVal = std::numeric_limits<double>::max();
if ( !inVec.isEmpty() ) {
minVal = inVec.first();
for ( int i = 1; i < inVec.size(); ++i ) {
minVal = min( minVal, inVec.at( i ) );
}
}
return minVal;
}
//FunctionCallMinimum
//rows
//OperatorArgs:inList1,inList2
//InputType:Value
//Fusion:Euklid
[ calcMin inList2!!i
| i <- [ 0 .. (length inList1) ]
]
//#
static List< vector< double > > Minimum(
vector< Vector< double > > inList1,
vector< Vector< double > > inList2 )
{
List< vector< double > > out;
for ( int i = 0; i < inList1.size(); ++i ) {
vector< double > pair;
double val = HaskellTranslated::calcMin( inList2.at( i ) );
pair.push_back( inList1.at( i ).first() );
pair.push_back( inList2.at( i ).indexOf( val ) );
pair.push_back( val );
out.append( pair );
}
return out;
}
Abbildung 3.5: Parametrierung eines MapReduce Skeletons mit dem Code aus Beispiel 2.1
34
3.2 PIPE Compiler Ansatz
Auffällig ist, dass eine Zusatzfunktion „haskell_minimum“ entstanden ist, welche nicht spezifiziert war. Während der Erkennung von eingebetteten Funktionen untersucht der Compiler die
jeweiligen Funktionsnamen auf Übereinstimmung mit Standardfunktionen, welche von vielen
Programmiersprachen bereit gestellt werden. Für das von dieser Arbeit benötigte Subset solcher
Standards wurden Funktionstemplates vorgefertigt, welche bei Auftreten einer bekannten und
unterstützten Standardfunktion automatisch eingefügt werden. Nachdem alle Standardfunktionen gefunden wurden und sämtliche Funktionsnamen bekannt sind, werden je nach Ausführungsumgebung Funktionsprototypen in die Ausgabedatei geschrieben. Bei der verwendeten
OpenCL Version ist dies beispielsweise notwendig. Deren Kernels sind im C99 Standard verfasst und kennen deswegen keine Klassen, weshalb es auf die Reihenfolge der Spezifikation von
Funktionen ankommt. Um dem MapReduce Framework eine Überprüfungsmöglichkeit für das
Vorhandensein von Operatoren zu geben, wird weiterhin eine Liste mit den Namen der übersetzten Operatoren implementiert. Nachdem alle Skeletons parametriert wurden, wird die fertige
Ausgabedatei beim entsprechenden Framework abgelegt, sodass dieses kompiliert und gestartet
werden kann. Zur Erstellung des Maschinencodes aus dem C/C++ Code der Ausführungsumgebungen wird die weit verbreitete Gnu Compiler Collection (GCC) verwendet. Abhängig vom
Betriebssystem wurde diese auf Linux direkt eingesetzt, wobei für Microsoft Windows die Entscheidung auf die Entwicklungsumgebung Minimalist Gnu for Windows (MinGW) fiel, welche
die GCC für das Betriebssystem bereit stellt.
Abbildung 3.6 illustriert den vollständigen, vom Framework angestrebten Prozess. Die in PIPEBASE
verfasste Datei aus Schritt (1) beinhaltet die Spezifikation des zu übersetzenden Algorithmus.
Dieser wird hinsichtlicher aller enthaltenen Funktionen und Symbole analysiert. Anschließend
wird im Schritt (2) eine interne Darstellung generiert, welche eine Beschreibung aller gefundenen
Informationen sowie deren Beziehungen zueinander abbildet. Aus dieser Datenmenge können
im Folgenden die Funktionen zweiter Ordnung, also die cPIPE Rümpfe im Zielcode erstellt werden. Sind diese bereits als Implementierung vorhanden, müssen sie lediglich parametriert werden. Im zweiten Fall ist wird der plattformspezifische MapReduce bzw. OpenCL Kernel Code
erzeugt und während Schritt (4) in die bereits übersetzten cPIPE Rümpfe eingefügt, woraufhin
das komplette Analyseprogramm JIT-kompiliert wird. Abschließend startet der Source-to-Source
Compiler das fertige Programm für die Analyse einer bereits vorhandenen Datenmenge und ei-
Clustering
Algorithmus
(1)
Wird eingelesen
Source-to-Source
Compiler
(2)
(3)
(4)
Kompiliert für
Ausführungsumgebung 1
Ausführungsumgebung 2
MapReduce
Programm
OpenCL
Programm
Funktion1{…}, Funkion2{…},
Variable1, Variable2, ...
Interne
Darstellung
Plattformabhängiger
C/C++ Code
Abbildung 3.6: Schematischer Ablauf des Source-to-Source Compilers
35
Kapitel 3 PIPE Compiler für effiziente Ausführung
ner Referenzmenge. Dieser Ansatz bietet die Möglichkeit, ein universelles Framework bereit zu
stellen. Mit dessen Hilfe kann auf hochdynamische Art und Weise die Erstellung von Programmcode für eine Vielzahl von unterschiedlichen Ausführungsumgebungen generiert werden. Durch
die Verwendung von PIPEs muss lediglich eine domänenspezifische Implementierung der selbigen vorhanden sein, um diese aus Sicht des Source-to-Source Compilers zu unterstützen.
3.3
OPTIMIERUNGEN
Für jeden handgeschriebenen Code gilt in der Regel, dass dieser nicht der Laufzeit entspricht, die
unter optimalen Bedingungen erreicht werden könnte. Diese Arbeit untersucht die Möglichkeit,
einfache Optimierungsregeln für den gegebenen Nutzercode anzuwenden. Dabei gibt es eine
Einschränkung auf die Verkettung der cPIPEs. An dieser Stelle sei allerdings erwähnt, dass auch
die Nutzerfunktionen selbst optimiert werden könnten. Grundlegend wäre eine Optimierung
der Algorithmen anhand deren Datenflussgraphen denkbar. Es ist ebenfalls möglich, die nach
der Übersetzung entstandenen Algorithmen zu restrukturieren, um eine verbesserte Laufzeit zu
erreichen. Es werden an dieser Stelle allerdings lediglich logische Optimierungen betrachtet. Als
Beispiel dient hierfür der „//Fusion“ Kommentar, wie er in Code Beispiel 2.1 zu sehen ist. Anders als die restlichen Kommentare stellt dieser den einzigen dar, welcher nicht notwendig für die
Funktionalität des spezifizierten Algorithmus ist. Auf diese Weise gekennzeichnete Operatoren
können vom Compiler in einem neuen fusioniert werden. Ziel ist es dabei, den Aufwand innerhalb der Datenanalyse so gering wie möglich zu halten. Das beste Beispiel stellt cross dar. Beim
Kreuzprodukt von genügend großen Datenmengen entstehen Zwischenergebnisse von extremer
Größe, die gegebenenfalls nicht mehr im Hauptspeicher gehalten werden könnten. Dementsprechend würde der Source-to-Source Compiler versuchen, einen cross Operator immer mit einem
folgenden row bzw. col Operator zu verschmelzen, um so die Speicherung des enorm großen
Zwischenergebnisses von cross zu umgehen. Wie die Orchestrierung der cPIPEs dadurch beein-
Daten
Euklid
Referenzen
Distanzen
Distanzen
Minimum
Minima
cross
Minima
Mittelwert
row
col
Daten
Euklid +
Minimum
Referenzen
Distanzen
Minima
Mittelwert
Referenzen
row
col
Abbildung 3.7: Reduzierung von cPIPEs durch logische Optimierung
36
Referenzen
3.4 Zusammenfassung
flusst wird, ist in Abbildung 3.7 schematisiert dargestellt. Weiterhin können direkt aufeinander
folgende row bzw. col cPIPEs zusammengefasst werden. Durch die Verschmelzung von cPIPEs
kann die Laufzeit vor allem im MapReduce Framework drastisch gesenkt werden, da die Anzahl auszuführender MapReduce Jobs reduziert wird und so weniger Overhead bezüglich der
Reinitialisierung aller beteiligten Komponenten notwendig ist.
3.4
ZUSAMMENFASSUNG
Der Source-to-Source Compiler stellt das essentielle Bindeglied zwischen der plattformunabhängigen Algorithmenbeschreibung und der plattformabhängigen Implementierung dar. Durch das
Überführen einer Eingabesprache in eine interne Repräsentation und einen modularisierten Aufbau ist es möglich, eine Vielzahl von Ausführungsumgebungen anzusteuern, ohne die eigentliche
Spezifikation des Algorithmus zu verändern. Mittels MapReduce und OpenCL kann gezeigt werden, dass die cPIPE Ausprägungen von PIPE tatsächlich unabhängig von der eigentlichen Zielplattform sind. Durch den Einsatz einer MapReduce Implementierung ist es ausserdem möglich
zu zeigen, dass PIPE lediglich eine abstraktere Variante dessen ist und durch die Spezialisierung
als cPIPE äquivalent ausgeführt werden kann. Mit Hilfe von OpenCL ist es weiterhin machbar,
den selben Code sowohl auf einer CPU als auch einer GPU auszuführen.
37
Kapitel 3 PIPE Compiler für effiziente Ausführung
38
4
IMPLEMENTIERUNG
Der praktische Teil dieser Arbeit besteht daraus, mehrere Ausführungsumgebungen für Tests
und zu Validierung der entwickelten Konzepte zur Verfügung zu stellen. Gleichzeitig ist der
Source-to-Source Compiler zu implementieren. Um die cPIPE Ausprägung zu erproben, kommen die in Abschnitt 3.1, 3.1.3 und 3.1.4 beschriebenen Ausführungsumgebungen MapReduce
und OpenCL zum Einsatz. Die Applikationsbeschreibung der cPIPEs und deren benötigte Datengrundlage weichen von der des standardisierten MapReduce Konzeptes ab. Vorhandene MapReduce Frameworks wie Hadoop konnten unter anderem aus diesem Grund nicht genutzt werden.
Die Partitionierung der Daten könnte aufgrund der Zeilen- und Spaltenbasierten Verarbeitungsweise der cPIPEs nicht ohne erheblichen Aufwand realisiert werden. Aus diesem Grund war
die Implementierung eines eigenen MapReduce Frameworks notwendig. Abbildung 4.1 zeigt
den schematischen Aufbau und die Ablaufstruktur der eigens für diese Arbeit entworfenen Ausführungsumgebung. Da ein Clustering-Algorithmus aus mehreren cPIPEs besteht, wurde deren
Abarbeitung im MapReduce Framework als eine Art von Jobs angesehen. Diese werden vom JobHandler nach Ablauf des Transformationsvorgangs ausgeführt, wobei für jede cPIPE ein neuer
MapReduce Zyklus gestartet wird (1). Während der Ausführung einer cPIPE steuert der MapReduce Coordinator die Funktionen des Datenstreamers, der Mapper, und des Reducers (2).
Die Verwendung einer eigenen Instanz für die Bereitstellung der Daten in Form eines Streamers gewährleistet eine kontinuierliche Bereitstellung der Daten. Während Mapper und Reducer
ihre Datenpakete verarbeiten, kann der Streamer aus der Datengrundlage neue Arbeitspakete
erstellen und diese in einer synchronisierten Liste für die Mapper bereitstellen. Genauso wie
die Mapper und Reducer, arbeitet der Streamer in einem eigenen Thread, also in einer eigenen
Aktivitätsumgebung, die von anderen Threads unabhängig ist. In Mehrkernprozessorsystemen
können je Prozessor mehrere Threads gleichzeitig verarbeitet werden, dies wird auch Multithreading genannt und bildet eine Grundlage für die parallele Programmierung. Bei der Verteilung
der Pakete muss auch auf die in Abschnitt 2.1.1 erklärte Speicherproblematik geachtet werden.
Wenn mehrere Mapper gleichzeitig ein Paket entnehmen wollen, könnte das aktuelle Element aus
der Liste des Datenstreamers entweder von mehreren Mappern gleichzeitig verarbeitet werden
oder ein Mapper könnte auf einen ungeschützten Speicherbereich zugreifen. Um dies zu verhindern, werden die Zugriffe auf die Datenpakete innerhalb des Streamers mittels Semaphoren
geschützt. Begründet durch eine ungleiche Datenverteilung, kann die Verarbeitungsgeschwindigkeit der Mapper untereinander variieren. Würde immer gewartet, bis alle Mapper ihr aktu-
39
Kapitel 4 Implementierung
JobHandler
(1)
(4)
MapReduce Framework
Coordinator
(2)
(3)
Mapper
func( ... )
DataStreamer
Reducer
Mapper
func( … )
Abbildung 4.1: Schematische Darstellung des verwendeten MapReduce Frameworks
elles Datenpaket verarbeitet haben, käme es zu einer immensen Verschwendung von Rechenzeit, wie in Abbildung 4.2 illustriert wird. Dieser Nachteil wird durch die
Datenbereitstellung durch den Streamer beseitigt. Wann
Thread1
immer ein Mapper sein aktuelles Datenpaket verarbeitet
hat, kann ohne Verzögerung ein neues vom DatenstreThread2
Totzeit
amer abgerufen werden, wodurch die Gesamtbearbeitungszeit aus zeitlicher Sicht optimiert werden konnte.
ThreadX
Im Anschluss an die Verarbeitung und Zusammenführung der Daten seitens Mapper und Reducer werden
Sync
die Daten im Coordinator gesammelt (3) und abschließend an den JobHandler weitergereicht (4). Die so propagierten Informationen werden als Basis für die Einga- Abbildung 4.2: Totzeiten bei synchronisierter Verarbeitung
bedaten des nächsten Jobs verwendet. Als Implementie-
40
rungssprache wurde hierfür das Framework Qt in der Version 5.3 verwendet [Dig14]. Die Wahl
fiel auf diese bekannte Open Source Lösung, da sie Plattformunabhängigkeit garantiert, was für
diese Arbeit von fundamentaler Bedeutung ist. Weiterhin ermöglichen einige bereitgestellte Klassen eine triviale Handhabung von Threads und deren Kommunikation untereinander auf Basis
von Signalen und sogenannten Slots. Dies sind besondere Funktionen, welche zum einen zur
Synchronisation verwendet werden können und so fest definierte Blockaden innerhalb kritischer
Abschnitte darstellen oder aber auf asynchrone Weise miteinander verbunden werden können,
um eine nebenläufige Abarbeitung bestimmter Aufgaben zu realisieren. Ein weiterer Bonus ist
die Ermöglichung von Reflexion innerhalb eines C++ Programms, worauf auch Qt basiert. Dafür
verwendet Qt einen Meta-Object Compiler (moc), welcher während des Kompilierungsvorgangs
eine Liste der vorhandenen Funktionen und deren Parameter erstellt und diese mit in das Program einkompiliert. Solche Informationen sind für jede Klasse verfügbar, welche von der Standardklasse QObject erbt. Dieser Mechanismus wird vom Source-to-Source Compiler in Synergie
mit dem selbst verfassten MapReduce-Framework ausgenutzt, um die zur Übersetzungszeit, dem
MapReduce Framework, unbekannten cPIPEs ausführen zu können.
Neben der notwendigen Plattformunabhängigkeit war auch dies ein Grund, um den Source-toSource Compiler ebenfalls mit dem auf C++ basierten Qt zu implementieren. Neben der Bereitstellung von Klassen zur Handhabung von Threads besteht auch die Möglichkeit zur plattformunabhängigen Bearbeitung von Dateien. Da das komplette System zu einem ausgereifteren
Entwicklungszeitpunkt nach Möglichkeit eine Vielzahl von Plattformen unterstützen soll, ist eine
derart portable Lösung wünschenswert. Um eine Zeit-effektive Implementierung des Compilers
zu ermöglichen, wurde eine grundlegend sequentielle Abarbeitung aller Arbeitsschritte angestrebt. Anders als zu Beginn konzipiert, kann der Compiler noch nicht aus einer absolut dynamischen Menge von Ableitungsregeln schöpfen, sondern ist an ein relativ beschränktes, den
getesteten Algorithmen angepasstes, Set zur Erkennung der Syntax gebunden. Die interne Darstellung der Funktionsdaten wurde mit Hilfe eines dedizierten Objekts je cPIPE realisiert. Jeder
Bearbeitungsschritt, der zur Erkennung und Verarbeitung der eingegebenen PIPEBASE Spezifikation dient, wurde aus Gründen der Erweiterbarkeit in separate Funktionen ausgegliedert, um
eine möglichst einfache Erweiterbarkeit der Funktionalitäten zu ermöglichen. Im Anschluss an
die Transformation von PIPEBASE in den plattformabhängigen Programmcode startet der Compiler die jeweilige Ausführungsumgebung. Dafür wurde ein kommandozeilenbasierte Aufruf
erdacht, welcher den Programmen die übersetzte Datei sowie die Namen der auszuführenden
cPIPEs und deren Eingabetypen in der festgelegten Reihenfolge übergibt. Als Alternative wäre
auch die Verwendung einer weiteren Datei möglich, welche all diese Informationen beinhaltet.
Um die notwendigen Festplattenzugriffe so gering wie möglich zu halten, eignete sich die kommandozeilenbasierte Variante.
Die Entwicklung der OpenCL-Umgebung wurde ausschließlich durch Standard-C++ realisiert.
Als Ausgabe des Compilers erhält die OpenCL-Umgebung eine Datei, welche die im eingeschränkten C99 Standard verfassten Kernels enthält. Jeder Kernel repräsentiert eine cPIPE. Die
hier notwendige Erkennung der Kernels zur Laufzeit wird durch OpenCL allein durch den Namen der cPIPE gewährleistet, was der Reflexion in Java bzw. mit der Funktionalität von Qt’s
moc vergleichbar ist. Anders als im MapReduce Framework wurde Steuerung der Operatoren
und Bereitstellung der Daten nicht in dedizierte Threads ausgelagert. Als prototypische Implementierung der OpenCL-Umgebung erwies sich eine Parallelisierung zwischen den Operatoren
als unnötig. In der nebenstehenden Abbildung 4.3 wird illustriert, wie die interne Struktur nach
der Transformation eines in PIPEBASE spezifizierten Algorithmus funktioniert. Die Kerneldatei
41
Kapitel 4 Implementierung
wird als ein OpenCL Programm kompiliert und
kann dadurch die Aufrufe der jeweiligen Kernels
ausführen. Die OpenCL-Umgebung folgt dabei
mit Ausnahme der Kernels einem strikt sequenAusführungsumgebung
tiellem Ablauf. Jeder Kernelaufruf fungiert dabei als Synchronisationspunkt, nach dem die verarOpenCL
beiteten Daten vom verwendeten Prozessor abgerufen, gegebenenfalls nachbearbeitet und als Eingabedaten für den nächsten Arbeitsschritt verKernel
wendet werden. Dieser Vorgang ist analog zum
Schema aus Abbildung 3.3, wobei die gewählte
C++ Programm
Ausprägung allerdings auf jede Parallelisierung
abseits der Kernels verzichtet. Bei der Entwicklung
aller drei Komponenten, also dem MapReduceFramework, der OpenCL-Umgebung sowie des
Source-to-Source Compilers wurden auch Funktionen des C++11 Standards verwendet, sodass zur
Abbildung 4.3: Ablauf innerhalb der OpenCLKompilierung des Projekts aktuelle Versionen von
Umgebung
Qt bzw. MinGW oder GCC notwendig sind.
Das Konzept von PIPEBASE wurde bei der Implementierung noch nicht berücksichtigt. Während der Entwicklung des Source-to-Source Compilers wurde die Spezifikationssprache und deren Umfang angepasst und erreichte erst gegen Ende der Arbeit den Stand der prozeduralen
Spezifikation. Eine Anpassung des Compilers an die neue Syntax würde eine Umstellung der
zuvor genannten Ableitungsregeln zur Folge haben, wodurch das gesamte Programm in großem
Umfang hätte überarbeitet werden müssen. Wie in Abschnitt 2.3.3 und durch Abbildung 2.8 gezeigt wurde, kann PIPEBASE in den reinen Haskellansatz überführt werden. Aus diesem Grund
ist die Überarbeitung als aktuell unnötig eingestuft worden.
42
5
EVALUATION
Dieses Kapitel bewertet die Ansätze von PIPE und das Source-to-Source Compiler Konzept zur
Überführung von plattformunabhängigen Code zur plattformspezifischen Implementierung. Es
wird auf die Vollständigkeit der Spezifikationsmöglichkeiten mittels PIPEBASE eingegangen und
die Eignung von Haskell als Definitionssprache für die Funktionen erster Ordnung diskutiert.
Weiterhin wird gezeigt, auf welche Weise sich die Anzahl von cPIPE-Elementen auf die Kompilierungs- bzw. Ausführungszeiten von Algorithmen innerhalb der gewählten Ausführungsumgebungen auswirken können.
5.1
PIPE KONZEPT
Die cPIPE Ausprägung von PIPE wurde genutzt, um den k-Means Algorithmus zu beschreiben.
Dieser kann in Code Beispiel 2.2 nachgeschlagen werden. K-Means stellt grundlegend den Repräsentanten der Klasse der partitionierenden Clustering-Algorithmen dar. Sämtliche anderen
Algorithmen, die zur selben Klasse gehören, können als Variation von k-Means betrachtet werden und hätten deswegen eine ähnliche Spezifikation. Da k-Means mittels PIPEBASE spezifizierbar ist, kann angenommen werden, dass eine Spezifikation anderer partitionierender ClusteringAlgorithmen möglich ist.
Ein weiterer bekannter Clustering-Algorithmus ist Density-Based Spatial Clustering of Applications with Noise (DBSCAN), der in [Est96] vorgestellt wurde. Diese Arbeit betrachtet eine Version
von DBSCAN, deren Definition aus [Hah14] entnommen ist. Der Algorithmus sucht ausgehend
von einem als Cluster markierten Punkt innerhalb einer e-Umgebung nach Punkten. Wenn in dieser e-Umgebung eine Mindestanzahl von Punkten enthalten ist, gilt der Bereich als „dicht“ und
wird als Cluster betrachtet. Anschließend werden alle zu einem Cluster gehörenden Punkte hinsichtlich ihrer e-Umgebung untersucht und, falls die Dichtebedingung erfüllt wird, deren Inhalt
mit dem Elterncluster verschmolzen. Code Beispiel 5.1 zeigt eine mögliche PIPEBASE Implementierung von DBSCAN. Zur Berechnung des Algorithmus werden lediglich vier cPIPEs benötigt:
Eine Instanz von cross, zwei row cPIPEs sowie eine col cPIPE. Diese berechnen zunächst die
euklidische Distanz der Punkte und testen auf die Einhaltung der e-Umgebungsbedingung. An-
43
Kapitel 5 Evaluation
-- Haskell Funktionsdefinition
functionEuklid = "euklid :: [Double] -> [Double] -> Double
euklid p1 p2 = (sqrt . sum) squaresOfDifferences
where
squaresOfDifferences = zipWith squareDist p1 p2
where
squareDist x1 x2 = dist * dist
where
dist = x1 - x2";
functionEpsilon = "calcEps :: [Double] -> Double
calcEps inList =
if theDistance < __epsilon__
then theDistance
else -1
where
theDistance = minimum inList";
functionReplace = "let out = []
calcRep :: [Double] -> Double
calcRep inList =
[ if inList!!j < 0
then out:inList!!j
else out : minimum inList
| j <- [ 0 .. (length inList) ] ]"
-- Daten- und Referenzmengendefinition
D = Vector< Vector< Double > >; -- Datenliste mit 2-dimensionalen Punkten
-- cPIPE Definition
-- Berechnet Distanzen
A = cross( "points", D, D, functionEuklid
);
-- Ersetzt Minima kleiner epsilon mit der ZeilenID, rest der Zeile mit -1
B = row( "values", A, functionMinimum, "rowID" );
for ( 50 ) { -- 50 Iterationen
C = row( "values", B, funtionReplace );
B = col( "values", C, functionReplace );
}
Code Beispiel 5.1: In PIPEBASE beschriebene Version von DBSCAN
schließend werden in einer Schleife abwechseln zeilen- und spaltenweise die Cluster gesucht,
falls vorhanden, deren ID mit dem jeweiligen Minimum der Zeile bzw. Spalte ersetzt. Da dieser
Algorithmus stellvertretend für die Klasse der dichtebasierten Clustering-Algorithmen fungiert,
wird auch in diesem Fall angenommen, dass diese Klasse mittels PIPEBASE beschrieben werden
kann.
Die Spezifikation der Nutzerfunktionen mittels Haskell stellt einen kritischen Teil von PIPEBASE
und den zugehörigen cPIPEs dar. Die Spezifikation eines Clustering-Algorithmus in einer funktionalen Programmiersprache ist grundlegend einwandfrei, allerdings ist die Ausdrucksweise ein
Problem. Um eine möglichst einfache Spezifikation von Nutzerfunktionen zu ermöglichen, muss
die Erstellung einer DSL erwogen werden. Sowohl die Beschreibung von Teilen aus k-Means
44
5.2 Source-to-Source Compiler und Ausführungsumgebungen
als auch der Ersetzungsfunktion in DBSCAN sind eher kompliziert und unübersichtlich. Mit einer DSL könnten Direktiven geschaffen werden, welche nur die Probleme des Data Clusterings
addressieren und dadurch eine effizientere und einfachere Spezifikation ermöglichen.
5.2
SOURCE-TO-SOURCE COMPILER UND AUSFÜHRUNGSUMGEBUNGEN
Bei der Übersetzung von einer Eingabesprache in eine Zielsprache ist es immer relevant, wie viel
Zeit dieser Prozess in Anspruch nimmt. Das beste Beispiel stellt hierfür der Query-Optimierer
in einem DBMS dar. Es muss beachtet werden, dass der Prozess die Query zu optimieren nicht
länger dauern sollte, als sie unoptimiert auszuführen. Andernfalls ist der Kosten-Nutzen Faktor
zu hinterfragen. Für den Source-to-Source Compiler dieser Arbeit bedeutet dies, ob die Übersetzung der cPIPEs in einem guten Verhältnis zur letztendlichen Ausführungszeit eines Algorithmus
steht. Abbildung 5.1 zeigt das Verhältnis Kompilierungszeit und der Menge zu übersetzender
cPIPEs. Zu sehen sind drei Kurven. Wird ausschließlich die Übersetzungszeit von PIPEBASE
zu MapReduce bzw. OpenCL Code betrachtet, können die Kurven beider Übersetzungszeiten
als nahezu deckungsgleich bezeichnet werden. Wird die, in der aktuellen Implementierung als
notwendig erachtete, Verarbeitung der Eingabedatenmenge in die Betrachtung mit einbezogen,
zeigt die Übersetzung zu MapReduce Code einen deutlichen Geschwindigkeitsvorteil. Aufgrund
der Annahme, dass die Anzahl und Dimensionen der Daten- und Referenzmengen zur Kompilierungszeit nicht bekannt sind, müssen die Eingabedaten vor der OpenCL-Übersetzung verarbeitet werden, da dessen Kernels abhängig von den Nutzerfunktionen auch Speicherallokation
mit festen Werten benötigen. Dieser Umweg ist nötig, da innerhalb eines OpenCL Kernels keine
dynamische Speicherallokation erlaubt ist.
Kompilierungszeit in Abhängigkeit von der Anzahl zu übersetzender cPIPEs
Kompilierungszeit in Millisekunden
250
200
150
MapReduce Code
OpenCL Kernel
OpenCL Kernel mit CSV
100
50
0
0
20
40
60
80
100
120
140
160
180
200
Anzahl übersetzter cPIPEs
Abbildung 5.1: Kompilierungszeiten in Abhängigkeit der zu übersetzenden cPIPEs
45
Kapitel 5 Evaluation
Ein anderes Szenario stellt die Menge der auszuführenden cPIPE-Elemente dar. Es kann davon
ausgegangen werden, dass mit steigender Komplexität eines Clustering-Algorithmus auch die
Menge der zu verkettenden cPIPEs steigt. Abbildung 5.2 zeigt das Verhalten der Ausführungsumgebungen MapReduce und OpenCL bei der Verkettung mehrerer cross cPIPEs zur Berechnung
der euklidischen Distanz. Wie zu erwarten, zeigen beide Ausführungsumgebungen einen linearen Anstieg der Ausführungszeiten. Es besteht also ein direkt proportionaler Zusammenhang
zwischen der Anzahl auszuführender cPIPEs und der Dauer der notwendigen Berechnungen. Es
muss allerdings festgestellt werden, dass das für diese Arbeit implementierte MapReduce Framework im Vergleich zu OpenCL eine deutlich schlechtere Zeiteffizienz aufweist. Dieser Effekt
kann allerdings dadurch erklärt werden, dass MapReduce grundlegend für ein „Scale Out“ Szenario konzipiert wurde. Im Rahmen dieser Arbeit konnten allerdings lediglich Tests im „Scale
Up“ Bereich durchgeführt werden. Anders als OpenCL skaliert das MapReduce Framework auf
nur einer CPU nicht zufriedenstellend.
Laufzeit in Millisekunden für 15.000 Datenpunkte
Laufzeit in Abhängigkeit von der Anzahl ausgeführter cPIPEs
4000
3500
3000
2500
MapReduce
OpenCL
2000
1500
1000
500
0
1
2
3
4
5
6
7
8
9
10
Anzahl verketteter cPIPEs
Abbildung 5.2: Ausführungszeiten bei unterschiedlicher cPIPE Anzahl
Dieser Trend wird auch in Abbildung 5.3 widergespiegelt. Der obere Graph der Abbildung zeigt
die Ausführungszeit des aus Code Beispiel 2.2 übersetzten k-Means Algorithmus in den beiden Ausführungsumgebungen MapReduce sowie OpenCL. Als Referenz dient eine alternative
k-Means Implementierung, die im .NET Framework 4.0 als parallelisierte Variante für einen einzelnen Rechner verfasst wurde. Wie Abbildung 5.2 bereits vermuten ließ, zeigt MapReduce mit
steigender Anzahl zu verarbeitender Punkte ein immer schlechteres Laufzeitverhalten. Die übersetzten Kernels von OpenCL berechnen die Cluster der selben Datenmenge allerdings schneller,
als die .NET 4.0 Variante für Windows. Die Meßdaten wurden für eine Eingabedatenmenge zwischen 15.000 und 120.000 Datenpunkten erhoben, bei denen jeweils 15 Cluster zur Bestimmung
festgelegt waren. Die untere Grafik aus Abbildung 5.3 zeigt den Geschwindigkeitsbonus, auch
genannt Speedup, der jeweiligen Ausführungsumgebungen untereinander. Es kann abgelesen
werden, dass OpenCL relativ konstant den selben Geschwindigkeitsvorteil gegenüber .NET 4.0
zeigt. MapReduce wird verglichen mit der .NET 4.0 Implementierung immer langsamer. In Bezug auf Abbildung 5.1 kann festgestellt werden, dass die Übersetzungszeit nur einen geringen
Teil der tatsächlichen Ausführungszeit einnimmt. Aus diesem Grund kann die Transformation
vom plattformunabhängigen PIPEBASE in plattformspezifischen Code als sinnvoll erachtet werden.
46
Laufzeit in Sekunden
5.2 Source-to-Source Compiler und Ausführungsumgebungen
k-Means im Qt MapReduce Framework vs. .NET 4.0 Parallelisierung
90
70
50
30
10
.Net 4.0
MapReduce
OpenCL
20
40
60
80
100
120
100
120
Anzahl berechneter Punkte (x * 1000)
Speedup Faktor
Speedup der Technologien untereinander
10
8
6
4
2
.NET 4.0 vs MapReduce
OpenCL vs .NET 4.0
20
40
60
80
Anzahl berechneter Punkte (x * 1000)
Abbildung 5.3: Laufzeit von k-Means ins .NET, MapReduce und OpenCL
47
Kapitel 5 Evaluation
48
6
ZUSAMMENFASSUNG UND
AUSBLICK
Ziel dieser Arbeit war es, einen Ansatz zur plattformunabhängigen Spezifikation von parallelen
Clustering-Algorithmen zu erarbeiten. Basierend auf dieser Spezifikation sollen mehrere Plattformen sowohl für homogene, als auch für heterogene Systeme angesprochen werden können.
Kapitel 2 zeigt den PIPE Ansatz, der genau dieses Problem löst. Gemeinsam mit dem Source-toSource Compiler aus Kapitel 3 konnte ein Framework erarbeitet werden, dessen Funktionsweise
am Beispiel des Themenbereichs Data Clustering gezeigt werden konnte.
Abbildung 6.1 zeigt, dass auch Ähnlichkeiten zum klassischen DBMS Ansatz vorhanden sind.
PIPEs und deren spezialisierte cPIPEs zeigen ein analoges Verhalten zu logischen und physischen Datenbankoperatoren. Logische Datenbankoperatoren repräsentieren Funktionen mit zwei
Dateneingängen sowie einem Datenausgang. Für jeden logischen Operator existiert mindestens
eine, oftmals aber auch mehrere Implementierungen. Aus dieser Menge wird vom Query Optimierer diejenige ausgewählt, welche die beste Leistung verspricht. Die Implementierungen eines
Systems sind dabei allerdings platformabhängig. Kommen PIPEs zur Anwendung bedeutet dies
jedoch, von der Plattformabhängigkeit losgelöst zu sein. Die Spezifikation mittels PIPEBASE
funktioniert hierbei analog zu SQL. Bei der Definition des vom System abzuarbeitenden Codes
wird lediglich definiert, welche Gerüste zu verwenden sind. Das System selbst greift dabei auf
die den Bedingungen bestangepasstesten Implementierungen der cPIPEs bzw. logischen Datenbankoperatoren zurück.
Anders als bei SQL bietet Haskell keine Möglichkeit, als Data Definition Language (DDL) zu fungieren. In PIPEBASE spezifizierter Code beschreibt durch die Anordnung der Operatoren implizit
die Abarbeitungsreihenfolge, bei SQL ist einer strikten Syntax Folge zu leisten. Beide so erstellte
Pläne werden im weiteren Verlauf transformiert und optimiert, sodass am Ende ein lauffähiges
C/C++ Programm steht, welches das tatsächliche Clustering der Daten vornimmt. Gemeinsam
haben beide, dass die Art und Weise des Clusterings beschrieben wird. Es besteht ebenfalls die
Möglichkeit, beide Codes zu optimieren. Seitens PIPEBASE bestehende Möglichkeiten zur Optimierung wurden rudimentär getestet und können noch weiterführend untersucht werden.
49
benutzt
Logische
Datenbankoperatoren
Haskell
benutzt
SQL
beschriebener Algorithmus
als Ablaufplan
erstellter Query-Execution-Plan
wird ersetzt durch
Query
Optimierer
wird ersetzt durch
Source to Source
Compiler
erstellt
Physische
Datenbankoperatoren
benutzt
cPIPE
benutzt
transformiert zu
C/C++
Programm
Ausführung
Map/Reduce
OpenCL
Framework
benutzt
C/C++ Programm
benutzt
Abbildung 6.1: Vergleich des PIPE Modells mit dem klassischen DBMS Anstaz
Kapitel 6 Zusammenfassung und Ausblick
50
PIPE
Für jede der drei zu Beginn von Abschnitt 2.2 beschriebenen Herausforderungen konnte eine
funktionsfähige Lösung gefunden werden. Die Beschreibung von Algorithmen mittels PIPEs,
unter Verwendung von PIPEBASE, der Source-to-Source Compiler als Übersetzungsglied sowie
die ansatzweise Optimierung, können alle zuvor angsprochenen Probleme lösen.
Für die Weiterentwicklung des PIPE Konzeptes erscheint die Erstellung einer DSL als sinnvoll.
Dadurch könnte eine effektivere Formulierung und Übersetzung der Nutzerfunktionen ermöglicht werden, als es mit der Spezifikation in Haskell möglich ist. Für die Verbesserung des Sourceto-Source Compilers gibt es mehrere Möglichkeiten. Die Übersetzung der Eingabesprache basiert
in der aktuellen Form auf festen Regeln. Um in zukünftigen Versionen eine dynamsiche Unterstützung von mehreren Ausführungsumgebungen zu ermöglichen, wäre die Einführung von
generischen Regeln über ein zusätzliches Modul denkbar. Dadurch könnte für jede neue Ausführungsumgebung ein neuer Regelsatz hinzugefügt werden, der die interne Darstellung des
Compilers in die Zielsprache überführt. Diese Regeln könnten dadurch auch nutzerseitig spezifizierbar sein, sodass ein Anwender den Source-to-Source Compiler nach Belieben erweitern kann.
Dadurch würde ein weiterer Freiheitsgrad gewonnen werden, der den plattformunabhängigen,
dynamischen Charakter des PIPE Konzeptes unterstützt.
51
Kapitel 6 Zusammenfassung und Ausblick
52
ABKÜRZUNGSVERZEICHNIS
PIPEBASE PIPE Based Algorithm SpEcification
DBSCAN Density-Based Spatial Clustering of Applications with Noise
OpenMP
Open Multi-Processing
MinGW
Minimalist Gnu for Windows
NUMA
Non-Uniform Memory Access
DBMS
Datenbankmanagementsystem
SIMD
Single Instruction, Multiple Data
FPGA
Field Programmable Gate Array
PACT
Parallelization Contract
REST
Representational State Transfer
cPIPE
Clustering Platform Independent Parallel PattErn
PIPE
Platform Independent Parallel PattErn
MPI
Message-Passing Interface
CPU
Central Processing Unit
DDL
Data Definition Language
DSL
Domain-specific Language
GCC
Gnu Compiler Collection
GPL
General-purpose Language
GPU
Graphics Processing Unit
HDD
Hard Disk Drive
SSD
Solid State Disk
53
Kapitel 6 Zusammenfassung und Ausblick
JIT
Just-in-Time
moc
Meta-Object Compiler
54
ABBILDUNGSVERZEICHNIS
1.1
Beispielcluster unterschiedlicher Form und Größe . . . . . . . . . . . . . . . . . . .
9
1.2
Schematischer Aufbau des Spezifikations- und Ausführungsvorgangs . . . . . . .
11
2.1
MapReduce Schema . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
15
2.2
PACT Operatoren, entnommen aus [Ale11] . . . . . . . . . . . . . . . . . . . . . . .
15
2.3
PACT Schema, übersetzt aus [Ale11] . . . . . . . . . . . . . . . . . . . . . . . . . . .
16
2.4
PIPE Beschreibungsschema . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
18
2.5
PIPE Verbindungsschema . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
18
2.6
cross, row und col cPIPE Schemata . . . . . . . . . . . . . . . . . . . . . . . . . . . .
20
2.7
Schrittweise Veränderung der Matrizen durch eine k-Means Variante . . . . . . . .
22
2.8
Transformation von PIPEBASE in den reinen Haskell Ansatz . . . . . . . . . . . . .
23
3.1
PIPEBASE Schema . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
27
3.2
MapReduce Prozess, entnommen aus [Dea08] . . . . . . . . . . . . . . . . . . . . .
30
3.3
Illustrierter Ablauf eines OpenCL gestützten Programmes . . . . . . . . . . . . . .
31
3.4
Schrittweise Generierung des Funktionsaufrufs für die Berechnung der euklidischen Distanz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
33
3.5
Parametrierung eines MapReduce Skeletons mit dem Code aus Beispiel 2.1 . . . .
34
3.6
Schematischer Ablauf des Source-to-Source Compilers . . . . . . . . . . . . . . . .
35
55
Abbildungsverzeichnis
56
3.7
Reduzierung von cPIPEs durch logische Optimierung . . . . . . . . . . . . . . . . .
36
4.1
Schematische Darstellung des verwendeten MapReduce Frameworks . . . . . . . .
40
4.2
Totzeiten bei synchronisierter Verarbeitung . . . . . . . . . . . . . . . . . . . . . . .
40
4.3
Ablauf innerhalb der OpenCL-Umgebung . . . . . . . . . . . . . . . . . . . . . . . .
42
5.1
Kompilierungszeiten in Abhängigkeit der zu übersetzenden cPIPEs . . . . . . . . .
45
5.2
Ausführungszeiten bei unterschiedlicher cPIPE Anzahl . . . . . . . . . . . . . . . .
46
5.3
Laufzeit von k-Means ins .NET, MapReduce und OpenCL . . . . . . . . . . . . . .
47
6.1
Vergleich des PIPE Modells mit dem klassischen DBMS Anstaz . . . . . . . . . . .
50
Literaturverzeichnis
LITERATURVERZEICHNIS
[Ale11] Alexandrov, A.; Ewen, S.; Heimel, M.; Hueske, F.; Kao, O.; Markl, V.; Nijkamp, E.; Warneke, D.: MapReduce and PACT - Comparing Data Parallel Programming Models, in Datenbanksysteme für Business, Technologie und Web (BTW), 14. Fachtagung des GI-Fachbereichs
Datenbanken und Informationssysteme (DBIS), 2.-4.3.2011 in Kaiserslautern, Germany, 2011,
S. 25–44.
[Dag98] Dagum, L.; Menon, R.: OpenMP: an industry standard API for shared-memory programming,
Computational Science Engineering, IEEE, Bd. 5, Nr. 1, Jan 1998, S. 46–55.
[Das11] Dastgeer, U.; Enmyren, J.; Kessler, C. W.: Auto-tuning SkePU: A Multi-backend Skeleton Programming Framework for multi-GPU Systems, in Proceedings of the 4th International
Workshop on Multicore Software Engineering, IWMSE ’11, ACM, New York, NY, USA, 2011,
S. 25–32.
[Das14] Dastgeer, U.;
Kessler, C.:
SkePU Frequently Asked Questions,
http://www.ida.liu.se/labs/pelab/skepu/faq.html [Online, Zugriff 02.01.2015].
2014,
[Dea08] Dean, J.; Ghemawat, S.: MapReduce: Simplified Data Processing on Large Clusters, Commun.
ACM, Bd. 51, Nr. 1, Jan. 2008, S. 107–113.
[Dig14] Digia Plc: Qt Project, 2014, http://qt-project.org/ [Online, Zugriff 17.01.2015].
[Dit12] Dittrich, J.; Quiané-Ruiz, J.-A.: Efficient Big Data Processing in Hadoop MapReduce, Proc.
VLDB Endow., Bd. 5, Nr. 12, Aug. 2012, S. 2014–2015.
[Est96] Ester, M.; Kriegel, H.-P.; Sander, J.; Xu, X.: A density-based algorithm for discovering clusters
in large spatial databases with noise., in Kdd, Bd. 96, 1996, S. 226–231.
[Gro13] Grossman, M.; Breternitz, M.; Sarkar, V.: HadoopCL: MapReduce on Distributed Heterogeneous Platforms Through Seamless Integration of Hadoop and OpenCL, in Proceedings of the
2013 IEEE 27th International Symposium on Parallel and Distributed Processing Workshops
and PhD Forum, IPDPSW ’13, 2013, S. 1918–1927.
[Hah14] Hahmann, M.; Habich, D.; Lehner, W.: Modular Data Clustering - Algorithm Design beyond
MapReduce, Technische Universität Dresden, 2014.
57
Literaturverzeichnis
[Kal13] Kalavri, V.; Vlassov, V.: MapReduce: Limitations, Optimizations and Open Issues, in Trust,
Security and Privacy in Computing and Communications (TrustCom), 2013 12th IEEE International Conference on, July 2013, S. 1031–1038.
[Khr14] Khronos Group: OpenCL, 2014, https://www.khronos.org/opencl/ [Online, Zugriff
27.12.2014].
[Lee04] Lee, S.-I.; Johnson, T.; Eigenmann, R.: Cetus – An Extensible Compiler Infrastructure for
Source-to-Source Transformation, in Rauchwerger, L. (Hrsg.): Languages and Compilers for
Parallel Computing, Bd. 2958 von Lecture Notes in Computer Science, Springer Berlin Heidelberg, 2004, S. 539–553.
[Lov77] Loveman, D. B.: Program Improvement by Source-to-Source Transformation, J. ACM, Bd. 24,
Nr. 1, Jan. 1977, S. 121–145.
[Ora04] Oracle: Javadoc Tool, 2004, http://www.oracle.com/technetwork/articles/java/indexjsp-135444.html [Online, Zugriff 06.01.2014].
[Ott09] Ottchen, C.: The Future of Information Modelling and the End of Theory: Less is Limited, More
is Different, Architectural Design, Bd. 79, Nr. 2, 2009, S. 22–27.
[Pac98] Pacheco, P. S.: A User’s Guide to MPI, University of San Francisco, San Francisco, CA
94117, 1998.
[Par11] Parr, T.; Fisher, K.: LL(*): The Foundation of the ANTLR Parser Generator, SIGPLAN Not.,
Bd. 46, Nr. 6, Juni 2011, S. 425–436.
[Ros11] Rosenberg, O.; Gaster, B. R.; Zheng, B.; Lipo, I.:
OpenCL Static
C++ Kernel Language Extension, 2011,
http://amd-dev.wpengine.netdnacdn.com/wordpress/media/2012/10/CPP_kernel_language.pdf [Online,
Zugriff
01.01.2015].
[Sch14] Schreibmann, V.: Design and Implementation of a Model-Driven Approach for Restful APIs,
IEEE Germany Student Conference, 2014.
[Sto10] Stone, J. E.; Gohara, D.; Shi, G.: OpenCL: A Parallel Programming Standard for Heterogeneous
Computing Systems, Computing in Science & Engineering, Bd. 12, Mai 2010, S. 66–72.
[Tra13] Travet,
N.;
Khronos
Group:
OpenCL
Introduction,
2013,
https://www.khronos.org/assets/uploads/developers/library/overview/opencl_overview.pdf
[Online, Zugriff 31.12.2014].
[Yan11] Yang, C.-T.; Huang, C.-L.; Lin, C.-F.: Hybrid CUDA, OpenMP, and {MPI} parallel programming on multicore {GPU} clusters, Computer Physics Communications, Bd. 182, Nr. 1, 2011, S.
266 – 269, Computer Physics Communications Special Edition for Conference on Computational Physics Kaohsiung, Taiwan, Dec 15-19, 2009.
58
Herunterladen