|
Die Zeit des
Geschwindigkeitsrausches ist vorbei, zumindest was die
Prozessoren betrifft, die heutzutage in Workstations,
Servern und Laptops verbaut werden. Höhere Taktraten
sind aufgrund der geltenden physikalischen Gesetze nicht
mehr DIE Lösung für eine bessere Performance (neben der
Optimierung der Instruktionen und des Cash-Speichers).
Dafür werben die Hersteller mit neuen Produkten wie
Dual-Core, Quad-Core, etc.. In diesen Lösungen sind
mehrere (Haupt)Prozessoren auf einem einzigen Chip
untergebracht und das in einer symmetrischen Art und
Weise. Das heisst, die Kerne sind identisch und können
dieselben Aufgaben erfüllen. Die einzelnen Kerne
erhalten dabei einen eigenen Cash, der weitere
Möglichkeiten der Performanceoptimierung bietet. Okay,
das war die Hardware. Leider kann die Software diese
neuen Möglichkeiten nicht in jedem Fall per se nutzen;
sie muss darauf vorbereitet sein. Der Weg dahin ist
steinig und kompliziert und wird leider durch die
existierenden Compiler, Entwicklungsumgebungen und
Frameworks nicht genügend unterstützt. Hier muss man
zwischen den existierenden Programmiersprachen und
Laufzeitumgebungen (Java, .NET) unterscheiden, die
verschiedene Ansätze bieten. Dazu kommen Frameworks wie
OpenMP und MPI, die in der Welt des
Hochleistungsrechnens schon länger existieren.
Neuerdings denkt man in dem Kontext der Parallelisierung
auch wieder über funktionale Programmiersprachen nach.
Wie man leicht erkennen kann, ist die Lernkurve für
Entwickler hoch und die Fehlerquellen sind zahlreich.
Dazu kommt, dass es oft Unschärfen in den
Begrifflichkeiten gibt, vor allem wenn es um
Multi-Threading, Hyper-Threading und Multi/Many-Core
sowie Multi-Tasking (im Rahmen von Betriebssystemen)
geht. Oft wird die Parallelisierung in der
Softwareentwicklung mit dem Meilenstein
Objektorientierte Programmierung verglichen. Dieser
Vergleich ist durchaus realistisch. Ich befasse mich auf
meinem Blog mit dem Thema Parallelisierung und möchte
dabei Lösungen und aktuelle Trends aufzeigen.
Use-Cases und Problemstellungen im Bereich
Nebenläufigkeit und Parallelisierung
Ich habe die Thematik bereits mehrfach
angerissen – „Wann ist welche Art von Parallelisierung sinnvoll
und wann nicht?“. Diese Frage steht in engem Zusammenhang mit
vielen Unschärfen in den Begrifflichkeiten. Ich möchte
keinesfalls engagierte Entwickler davon abhalten, sich in dieser
Herausforderung zu stellen. Aber ähnlich zum
Lernkurven-Vergleich drängen sich auch hier Analogien mit dem
Meilenstein Objektorientierung in der Softwareentwicklung auf.
Es gibt Code der ist „dermaßen objektorientiert“ geschrieben,
dass er unübersichtlich, kaum wartbar und nicht mehr
weiterverwendbar ist. Ein typischer Fall, beim dem über das Ziel
hinausgeschossen und damit die Objektorientierung konterkariert
worden ist.
Was hat das mit Parallelisierung zu tun? Im
Prinzip gilt auch hier der Ansatz: was ist die konkrete
Anforderung und welche Use-Cases (Anwendungsfälle) lassen sich
daraus ableiten? Wobei man die Zielplattform (Systemarchitektur)
nicht vernachlässigen darf. Hier eine (nicht vollständige) Liste
von Fragen, mit Hilfe derer man einer Lösung näher kommen kann:
- Wie
sieht die HW-Architektur der Zielplattform aus
(Schlüsselworte: Hyprerthreading, symmetrische oder
asymmetrische MultiCore-CPU’s, Shared Memory, Distributed
Memory)?
- Welches
Betriebssystem mit welchen Laufzeitumgebungen und
Programmiersprachen werden auf der Zielplattform eingesetzt?
Dabei ist das Betriebssystem immer mehr zu vernachlässigen,
da präemptives Multitasking heutzutage Allgemeingut ist und
damit zumindest parallele Prozessausführung gegeben ist.
- Ist
Performance die wichtigste nicht-funktionale Anforderung?
Mit Performance sind in diesem Zusammenhang Datendurchsatz
und Verarbeitungsgeschwindigkeit gemeint. Hier geht es auch
um den Fakt, dass die Zeiten höherer Taktraten vorbei sind
und der Cache leider nicht die Lösung aller Probleme
darstellt.
- Haben
die betreffenden Use-Cases einen synchronen oder asynchronen
Charakter?
- Wie
soll der Datenaustausch zwischen Ausführungsströmen (ich
verwende diesen Begriff hier ganz bewusst) erfolgen und in
welchem Umfang? Benötige ich Zugriff auf „shared“ Ressourcen
und in welchem Umfang?
Ausgehend von der Beantwortung dieser Fragen
kann eine Architektur entwickelt werden, die den Anforderungen
genügt und die auf die richtigen Lösungen setzt. Richtig
bedeutet in diesem Zusammenhang auch, dass die passenden
Techniken verwendet werden um weitere nicht-funktionale
Anforderungen wir Wartbarkeit, Erweiterbarkeit, Testbarkeit und
Einfachheit (!) zu erfüllen. Ich möchte und kann an dieser
Stelle keine Entscheidungsmatrix liefern; werde aber kurz ein
paar Lösungsmöglichkeiten skizzieren.
- Wenn
Punkt 3 das entscheidende Kriterium ist, sollte eine gute
Auslastung der zur Verfügung stehenden Rechenkerne das Ziel
sein. Es gibt in diesem Fall immer noch die Möglichkeit,
eine Lastverteilung der Prozesse als Lösung zu wählen.
Sollte dies aufgrund der Problemstellung (Arithmetik, etc.)
nicht möglich sein, muss der Weg über eine Verteilung der
Threads gefunden werden. Hier bietet sich beispielsweise
OpenMP an. Natürlich kann sich Punkt 3 auch mit Punkt 5
überlagern, wenn auf viele „shared“ Variablen zugegriffen
werden muss. Es ist anzumerken, dass OpenMP uns nicht vor
Race-Conditions oder Dead-Locks bewahrt.
-
Bezüglich Punkt 4 gibt es sicherlich die am weitesten
entwickelten Lösungsmöglichkeiten und gute Chancen, diese
Problemstellung umfassend zu lösen. Eine gute Entkopplung
sowie asynchrone Kommunikationsmechanismen sind hier die
Erfolgkriterien. Konkrete Anwendungsfälle existieren vor
allem im Umfeld von Benutzerschnittstellen (UI’s) auf
Clientsystemen. Ausführungsströme (z.B. Threads) in der
Präsentationslogik sollten von der Geschäftslogik mithilfe
geeigneter asynchroner Kommunikationsmechanismen getrennt
werden, um gezielt eine lose Kopplung zu erreichen.
Ich möchte noch anmerken, dass die Aufgabenstellung in Punkt
5 (Asynchronität) für mich nicht originär zur Problemdomäne
Parallelisierung / Nebenläufigkeit gehört, in der Diskussion
aber dort oft eingeordnet wird.
- Punkt 5
ist sicherlich die komplizierteste Aufgabenstellung, da
bisher nur wenig Unterstützung durch Entwicklungsumgebungen,
Compilern und Frameworks vorliegt. Bis es diese gibt
(beispielsweise auf der Grundlage von Konzepten des
„Transactional Memory“) muss man noch mit all den
Schwierigkeiten des „Lock-Free Programming“ leben. Dazu
gehört natürlich ein ausgefeiltes Testkonzept, um Dead-Locks
und Race-Conditions auf die Spur zu kommen. Generell kann
ich hinsichtlich Punkt 5 nur empfehlen, Architektur und
Design hinsichtlich alternativer Lösungsmöglichkeiten des
Datenaustausches genau zu prüfen.
Natürlich ist anzumerken, dass es auch einen Mix
an Lösungen geben kann. Als konkretes Beispiel im
Hochleistungsrechnen sind Hybridanwendungen aus MPI (Message
Passing Interface) und OpenMP zu nennen.
|