Entwicklung der Hardware: Parallelisierung

English

Bis etwa einigen Jahren konnte man davon ausgehen, dass sich die Taktfrequenz von gängigen Mikroprozessoren regelmäßig erhöht und bei der Entwicklung neuer Software darauf vertrauen, dass die Hardware zumindest in naher Zukunft leistungsfähig genug sein würde, um gewisse Ineffizienzen zu kompensieren. Nun ist die Leistungsfähigkeit eines Rechners von sehr viel mehr Faktoren als der Taktfrequenz abhängig, vor allem auch davon, wie viele Operationen in einem Takt bzw. wieviele Takte für eine Operation benötigt werden. Wichtig ist erfahrungsgemäß aber auch ein genügend großer Hauptspeicher und natürlich gewisse Optimierungen auf der Platine und auf dem Chip, die man gar nicht so explizit wahrnimmt und auch nicht so leicht ändern kann. Grundsätzlich ist aber die Leistung einzelner CPU-Kerne nur noch langsam gestiegen, man bekommt aber ohne weiteres einen Chip, auf dem mehrere CPU-Kerne implementiert sind. Dualcore, Quadcore u.s.w. sind ja gut bekannt. Ein Link dazu: The Free Lunch is Over

Nun kann man diese mehrfachen Prozessoren mit einer einzelnen Ressourcen-hungrigen Applikation nutzen, wenn diese mehrere parallel verlaufende Threads oder Prozesse verwendet. Leider gibt es dabei ein paar Probleme. Das ärgerlichste ist, dass man zwar viele Entwickler findet, die sagen, das zu können, aber dass die Sache nachher tatsächlich stabil funktioniert, ist leider seltener. Häufig erlebt man Schönwetter-Software. Auf dem Rechner des Entwicklers funktioniert sie prima, vielleicht auch auf dem Testsystem. Aber im produktiven Einsatz treten dann obskure Fehler auf, die niemand nachvollziehen kann. Oder die verschiedenen Threads und Prozesse warten so oft gegenseitig aufeinander, daß in Wirklichkeit im Durchschnitt doch nur eine CPU genutzt wird. Oder das System blockiert sich selbst („Verklemmung“ oder engl. „dead-lock“). Natürlich nicht auf dem Rechner des Entwicklers, weil die spezielle Situation, die dazu führt, nur unter Last im produktiven Einsatz zum ersten Mal auftritt. Was lernt man daraus? Für diese Art von Architektur braucht man sehr gute Entwickler, die sich die parallelen Abläufe vorstellen können und die genug Erfahrung damit haben. Und man sollte sich nicht zu weit aus dem Fenster lehnen, sondern Dinge entwickeln, die zwar die Parallelität ausnutzen, aber doch robust genug sind, um nicht nur knapp zu funktionieren. Außerdem ist es wichtig, mir sehr realitätsnahen Daten und Datenmengen zu testen und auch Lasttests durchzuführen. Auf Testsystemen, die den späteren Produktivsystemen ähneln.

Ein anderer Ansatz löst das ganze Problem durch Frameworks. Es gibt sicher gute und leichtgewichtige Frameworks, aber bei gängigen Frameworks wie JEE (früher J2EE) brauchen diese schon selbst so viele Ressourcen und sie schränken die Möglichkeiten bei der Entwicklung so stark ein, dass man zumindest den Vorteil des einfacheren Multithreadings sofort wieder verliert, weil das Framework selber jetzt einen großen Teil der Rechenleistung und des Hauptspeichers in Anspruch nimmt. Es mag andere gute Gründe geben, solche Frameworks mit JEE-Applikationsservern einzusetzen, aber der Performance-Gewinn durch Parallelisierung hält sich normalerweise in Grenzen.

Das Problem ist immer wieder, daß man Datenstrukturen hat, die von mehreren Threads oder Prozessen manipuliert werden können und dass dadurch Probleme auftreten, die sich zwar handhaben lassen, die aber in der Praxis große Schwierigkeiten bereiten.

Zwei radikale Ansätze sind:

  • Verzicht auf gemeinsam genutzte Datenstrukturen
  • Verwendung von unveränderbaren Datenstrukturen

Der erste Ansatz ist zum Beispiel bei Entwicklung mit Ruby oder mit C gut möglich, weil die einzelnen Prozesse relativ wenig Hauptspeicher verbrauchen und man sich so leisten kann, mehr Prozesse gleichzeitig laufen zu lassen. Nun kann man mit verschiedenen Mechanismen Interprozeßkommunikation durchführen, z.B. unter Linux und Unix mit Posix-IPC oder mit TCP/IP. Das funktioniert vor allem dann gut, wenn man relativ unabhängige Prozesse hat, die nur wenig miteinander kommunizieren müssen. Und auch dafür braucht man ähnlich gute Entwickler wie beim Multithreading, die IPC gut beherrschen, außer man hat das Glück, dass die Prozesse so unabhängig voneinander laufen, daß sie gar nicht miteinander kommunizieren müssen. Vielleicht hat Erlang diese Idee in praktikabler Form umgesetzt. Dort kann man mit einer großen Anzahl von Prozessen arbeiten, die völlig voneinander getrennte Speicherbereiche für ihre Daten haben, während es ein Messaging-System für die Kommunikation zwischen diesen Prozessen gibt.

Die andere Idee, alle geteilten Datenstrukturen unveränderbar („immutable“) zu machen, wird z.B. von Scala und Clojure umgesetzt. Der Nachteil, daß man statt ein Objekt zu ändern, nur eine Kopie mit dieser Änderung erzeugen kann, wird durch Mechanismen abgemildert, die in gewissen Fällen intern so ein Kopieren durch teilweise Referenzierung des Eingabeobjekts ersetzen. Das gibt es auch in einem einfachen Fall in Java, wo der Zeichenketten-Type (String) immutable ist und wo Teile der Zeichenkette, die mittels substring() erzeugt werden, dieselbe interne Struktur wie die ursprüngliche Zeichenkette referenzieren.

In jedem Fall muss man sich aber über Abhängigkeiten der Prozesse und Threads untereinander Gedanken machen, damit man keine „dead-locks“ baut. Mit Scala und Clojure ist es aber möglich, leichtgewichtige Frameworks zu bauen, die die Ausführung von sehr vielen Threads gleichzeitig erlauben, weil das Versprechen der Unveränderbarkeit der geteilten Objekte viele Probleme eliminiert. Twitter benutzt zum Beispiel intern Scala und ist damit in der Lage, auch bei Ereignissen, die sehr viel Kommunikationsbedarf auslösen, mit der Last fertigzuwerden.

Ein Problem prinzipieller Natur bleibt natürlich, wenn der Kommunikationsbedarf zwischen den Prozessen sehr groß ist. In einem großen System können nicht alle Kommunikationspfade optimal schnell sein, denn bei n parallelen Prozessoren gibt es {n(n-1)\over2} Kommunikationspaare, was für große n mit dem Quadrat der Anzahl der Prozessoren wächst, also zu Kompromissen zwingt. Man bekommt entweder bei einem gemeinsamen Kanal Kapazitätsprobleme oder man muss bei getrennten Kanälen neben Pfaden zu den direkten Nachbarn auch auf zusammengesetzte Verbindungen setzen. Um es plastisch zu beschreiben: Eine wirklich große Applikation läuft verteilt über mehrere Rechenzentren, in denen es jeweils mehrere Racks gibt. Die Racks haben jeweils mehrere Rechner, die Rechner mehrere CPUs und die CPUs mehrere Kerne. Mit viel Geld kann man raffiniertere Hardware bauen, wo eine größere Anzahl von CPU-Kernen schnell miteinander kommunizieren kann, aber in unmittelbarer Nachbarschaft eines CPU-Kerns mit maximal schneller Kommunikation lassen sich nur begrenzt viele andere CPU-Kerne unterbringen.

Eine Idee war, eine große Anzahl von Rechnern topologisch in einem Hyperwürfel anzuordnen. Man hat also 2^n Teilrechner in Positionen, die den gedachten Ecken eines n-dimensionalen Würfels entsprechen und die Kanten dieses Hyperwürfels sind die leistungsfähigen Verbindungen dazwischen. So kommt man mit maximal n Schritten zu jeder anderen Teilrechner und kann auch in n Schritten ein Zwischenergebnis von allen Teilrechnern aggregieren und danach wieder verteilen. Weiß jemand, welche Ansätze man in heutigen Hochleistungsrechnern mit sehr vielen CPUs wählt?

Grundsätzlich kann eine Berechnung mit wenig Kommunikationsbedarf der Teilprozesse sehr gut parallel ablaufen, aber ein hoher Kommunikationsbedarf kann den Gewinn durch die Parallelisierung zunichte machen.

Share Button

Beteilige dich an der Unterhaltung

1 Kommentar

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

*