Jun 242013
 

Die Parallelisierung von Algorithmen und Programmteilen hat mir schon immer Spaß gemacht und ich habe mir sogar zu Hause einen Rechencluster gebaut, mit dem ich experimentieren konnte. In der aktuellen dotnetpro Ausgabe (07/2013) beschreibt Bernd Marquardt, bei dem ich auf der Parallel 2012 Konferenz einen .NET TPL Workshop mitmachen durfte, in einem Artikel die Parallelisierung von Algorithmen mit AMP unter C++, die dann auf der Grafikkarte ausgeführt werden. Leider muss man hier immer noch den Umweg über C++ gehen, aber glücklicherweise gibt es für .NET mit Cudafy ein Framework, mit dem man diesen Umweg nicht gehen muss. Cudafy unterstützt, neben dem namensgebenden CUDA von Nvidia, auch OpenCL, sodass man damit plattformübergreifende Parallelisierungen vornehmen kann. Ich habe in den letzten Tage ein wenig mit Cudafy herumgespielt und möchte hier einmal ein recht simples Beispiel der Matrixmultiplikation beschreiben.

Um Cudafy zu verwenden, benötigen wir zuerst ein GPGPU Objekt, das unsere zu verwendende Hardware repräsentiert. Cudafy unterstützt dabei sowohl CUDA von Nvidia als auch OpenCL der Khronos Group. Ich habe mich in meinem Beispiel für OpenCL entschieden, denn so konnte ich sowohl die CPU als auch die beiden Grafikkarten in meinem System (Intel und Nvidia) zur Berechnung auswählen. Folgender Codeschnippsel zeigt die Instanziierung des GPGPU Objektes, ein ganz rudimentäres Exception Handling (jaja, ich weiss ;-)) und die Instanziierung meiner Matrixmultiplikationsklasse.

 

Wichtigster Grundstein meiner Entwicklungsumgebung ist eine abstrakte Basisklasse, von denen meine implementierten Algorithmenklassen erben. Dieser Schritt ist nicht unbedingt notwendig, macht aber in meinem Fall Sinn, da ich beim „Herumspielen“ nicht nur die Matrixmultiplikation implementiert habe, sondern auch andere Algorithmen.

Im Konstruktor muss ein GPGPU Objekt übergeben werden, das die ausgewählte Client Hardware repräsentiert (die Instanziierung wurde bereits weiter oben beschrieben). Die Execute Methode enthält nun den Code, der benötigt wird, m einen Kernel mit Cudafy für die zu verwendende Client Hardware vorzubereiten (Aufruf von Cudafy(…) und LoadModule(..)). Außerdem wird noch eine Zeitmessung mit der allseits bekannten Stopwatch() durchgeführt. In diesem Block wird die innerhalb der Algorithmenklasse zu implementierenden Methode OnExecute() aufgerufen, sodass nach Ausführung die Dauer der Berechnung (inklusive der Übertragungszeit der Daten zur berechnenden Hardware) in der Konsole ausgegeben wird.

Die Klasse zur Matrixmultiplikation sieht dann so aus.

Wie bereits erwähnt, ist diese Klasse während meiner Tests entstanden, sodass ich in diesem Beispiel Testdaten verwenden, die ich bei Aufruf des Konstruktors generiere. Dem Konstruktor wird in diesem Beispiel neben dem GPGPU Objekt auch noch die Dimension der Matrix mit übergeben. Die wichtigsten Teile des Codes stecken aber in der Methode OnExecute() und Multiply(…). Die Methode OnExecute() ist dafür verantwortlich, den Speicher auf der Client Hardware anzulegen und die Arrays vom Host auf den Client zu übertragen. Dies wird hier durch die Methode CopyToDevice(…) vorgenommen. Das Ergebnisarray wird nicht auf die Client Hardware kopiert, sondern nur der Speicher reserviert, da es zu Beginn sowieso leer ist und deshalb keine Daten benötigt werden. Die Launch(…) Methode startet dann die Berechnung auf der Client Hardware. Den Aufruf werde ich hier nicht weiter erläutern, mehr zur Launch(…) Methode findet man aber in der Cudafy Dokumentation. Nach dem Aufruf wird das Ergebnis von der Hardware wieder auf den Host übertragen und der belegte Speicher auf dem Client wieder freigegeben. Das war’s auch schon.

Die Client Implementierung des Algorithmus steckt in der mit dem „Cudafy“ Attribut markierten Methode Multiply(…). Die Methodenparameter sind GThread (wird standardmäßig von Cudafy hinzugefügt und enthält Statusinformationen), die Dimension der Matrix und die Matrizen für die Matrixmultiplikation. In dieser Methode wird zuerst die x und y Position innerhalb der Matrix errechnet. Die Berechnung orientiert sich hier an den Blöcken, die über die Launch Methode mit übergeben werden. Genauere Informationen dazu findet ihr in den Grundlagen zum Thema OpenCL und CUDA. Im nächsten Schritt wird vorsichtshalber noch überprüft, dass x und y auch wirklich innerhalb der Matrix liegen und anschließend die Matrixmultiplikation durchgeführt, die dann im Ergebnisarray gespeichert wird.

Die Methode CpuCalculation() habe ich implementiert, um die Geschwindigkeit einer einzelnen CPU mit der Ausführung auf einem Client zu vergleichen. Auf meinem Rechner war eine Nvidia GT650M bei einer Dimension von 256 bereits 3x so schnell wie ein Kern meiner i7 CPU. Bei einer Dimension von 512 war sie schon um den Faktor 7,5 schneller.

Abschließend kann man sagen, dass sich die Nutzung einer Grafikkarte als Client bei einem hinreichend großen Problem lohnt und man damit eine große Beschleunigung erreichen kann. Dank Cudafy funktioniert das alles auch in .NET ohne den Umweg über C++. In meinem Beispiel bleibt außerdem noch viel Platz für Optimierungen, da ich das Beispiel möglichst simpel halten wollte – und das gilt sowohl für die Berechnung der Matrixmultiplikation auf der CPU (hier wäre bspw. eine Implementierung mit der TPL sinnvoll, die dann alle Kerne nutzt) als auch auf der GPU.

Jun 062013
 

Heute stand ich vor dem Problem, dass ich ein Byte Array als Index in einem Dictionary in .NET verwenden wollte. Das hat natürlich nicht funktioniert, da der Hash-Code von zwei inhaltsgleichen Byte-Arrays ungleich ist und dadurch der Vergleich beim Zugriff fehl schlägt. Wenn ich also überprüfen möchte, ob ein Key bereits in der Datenstruktur vorhanden ist, wird es immer fehl schlagen, da das erzeugte Objekt mit in den Hashwert eingeht.

Das folgende kleine Beispiel zeigt das Problem:

Man erhält als Ergebnis „Hash Array 1 = ‚37121646‘, Hash Array 2 = ‚45592480‘”. Fügt man also das erste Array z.B. einem Dictionary hinzu, wird mit bei einer Abfrage mit ContainsKey(…) ein false erhalten.

Glücklicherweise gibt es seit .NET 3.5 die Möglichkeit, diesen Datentypen im Konstruktor einen IEqualityComparer<T> mitzugeben, der dann den Vergleich übernimmt.  Eine Klasse, die dieses Interface implementiert, muss die Methoden Equals(…) und GetHashCode(…) zur Verfügung stellen. Für meinen Anwendungsfall reicht die folgende Implementierung:

Wichtig ist hier speziell die GetHashCode(…) Methode. Hier wird der Inhalt des Arrays mit dem BitConverter in einen Hex-String konvertiert und von diesem String der HashCode zurückgegeben. Der Hash wird dann also über den Inhalt gebildet und ist dann bei beiden Arrays gleich. Unsere neue Implementierung wird dem Konstruktor dann mit übergeben und dann in der entsprechenden Datenstruktur verwendet. Das folgende Beispiel zeigt die Anwendung und ein „Vorher / Nachher“ Vergleich:

Startet man das Programm, erhält man folgende Ausgabe „Ohne IEqualityComparer – Contains = False. Mit IEqualityComparer – Contains = True“. Problem gelöst :-).

Durch die weitere Nutzung der Seite stimmst du der Verwendung von Cookies zu. Weitere Informationen

Die Cookie-Einstellungen auf dieser Website sind auf "Cookies zulassen" eingestellt, um das beste Surferlebnis zu ermöglichen. Wenn du diese Website ohne Änderung der Cookie-Einstellungen verwendest oder auf "Akzeptieren" klickst, erklärst du sich damit einverstanden.

Schließen