Sonntag, 20. Juli 2008

entwickler.com Magazine Konferenzen Akademie Entwickler-Forum Jobbörse Bücher
Software & Support Verlag




Juni 2007
Software-Visualisierungstools optimieren Multi-Core-Systeme
Arbeitsteilung im Blick
Bill Graham

Bei schlecht oder fehlerhaft laufenden Multi-Core-Systemen wird die Lokalisierung eines Problems schon allein durch die Vielzahl von Systeminteraktionen zu einer gewaltigen und frustrierenden Aufgabe. Threads können jederzeit zwischen den Cores migrieren, mit Threads auf anderen Cores kommunizieren oder mit diesen Ressourcen teilen. Wenn bei mehreren Hundert Threads einer plötzlich unerwartet blockiert, gleicht die Suche nach dem Auslöser der Suche nach einer Nadel im Heuhaufen.


Debugging-Tools für Uniprozessorsysteme wurden nicht entwickelt, um ein solch komplexes Systemverhalten zu analysieren. Es kann sogar passieren, dass sie ein Problem in einem Teil des Multi-Core-Systems anzeigen, während es sich tatsächlich an einer anderen Stelle befindet. Hersteller wie QNX Software Systems [1] haben daher eine neue Generation von System-Tracing- und Visualisierungswerkzeugen auf den Markt gebracht. Anders als herkömmliche Tools, die sich auf einzelne Programme und Prozessor-Cores konzentrieren, analysieren diese Werkzeuge das Multi-Core-System als Ganzes. Entwickler können damit nicht nur komplexe Interaktionen auf Systemebene schnell visualisieren, sondern auch an den Bereichen arbeiten, die bei Parallelverarbeitung und Performance den größten Fortschritt versprechen. Beispielsweise kann der Entwickler in kürzester Zeit herausfinden, welche Cores zu viel oder zu wenig belastet sind. Er kann übermäßige Interprozesskommunikation zwischen Cores beseitigen, unnötige Migrationen der Threads einschränken und bei Multi-Core häufig auftretende Ressourcenkonflikte eingrenzen.

Übermäßige IPC zwischen Cores reduzieren
Übermäßige Interprozesskommunikation, kurz IPC, zwischen Cores kann die Performance eines Multi-Core-Systems mit symmetrischem Multiprocessing reduzieren. Sendet ein Core eine Nachricht zu einem anderen Core, muss diese in den Speicher geschrieben und ein Interrupt an den Empfänger-Core geschickt werden. Der Empfänger-Core muss daraufhin den Interrupt bedienen, den Software-Prozess oder Thread bestimmen, der die Nachricht verarbeitet, und aus dem Speicher auslesen. Tauschen Prozesse häufig Nachrichten zwischen den Cores aus, wie zum Beispiel ein Prozess mit Services für Client-Applikationen, können sie eine erhebliche Menge an Systemkapazität verbrauchen. Trotzdem ist dieser Overhead kleiner als der IPC-Overhead bei asymmetrischen Multi-Core-Systemen, da diese für IPC typischerweise ein komplettes Netzwerkprotokoll nutzen. Mit einem System-Tracing-Tool lässt sich dieses Verhalten deutlich einfacher erkennen und analysieren. Der QNX System Profiler trägt Systeminformationen zusammen, wie Hardware-Interrupts, Kernel-Calls, Scheduling-Vorgänge, Veränderungen bei Thread-Zuständen sowie verschiedene andere Formen der IPC wie Signale und Nachrichten. Der Nutzen des Tools liegt darin, dass es sämtliche Informationen zur Intercore-Kommunikation liefert und alles andere herausfiltern kann. Auf diese Weise sieht der Entwickler schnell, welche Threads zwischen den Cores kommunizieren und welche Cores eine bestimmte Aufgabe zu welcher Zeit ausführen. Um das Problem weiter einzugrenzen, bietet das Tool Statistiken, etwa zu den Interrupts pro Thread. Sie helfen erkennen, welche Threads übermäßige Intercore-Kommunikation verursachen. Das Tool kann zudem die CPU-Nutzung aus der Systemperspektive, der Prozessperspektive oder anhand der Thread-Prioritäten anzeigen. Die Systemperspektive zeigt potenzielle Problembereiche auf, etwa ob ein Core zu schwach oder zu stark ausgelastet ist. Aus der Prozessperspektive lässt sich feststellen, wie sich die Threads in einem Problembereich verhalten. Über die Thread-Prioritäten ist ersichtlich, ob bestimmte Threads aufgrund der ihnen zugeordneten Priorität zu viel oder zu wenig CPU-Zeit erhalten. Mit diesen Informationen können Entwickler messen, wie häufig und effizient Intercore-Kommunikation stattfindet, und entscheiden, wie das Problem zu lösen ist. Eine Möglichkeit ist, enger gekoppelte Prozesse an denselben Core zu binden. Dadurch verringert sich der Overhead der Intercore-Kommunikation, und die leistungshemmende Cache-Überlastung ist aufgehoben. Mit demselben System-Tracing-Tool kann der Entwickler die neue Konfiguration untersuchen und die Effizienzsteigerung messen.

Möglichkeiten für Parallelverarbeitung finden
Um die Performance von Applikationen auf Multi-Core-Prozessoren zu maximieren, müssen Entwickler die von diesen Chipsets angebotene Hardware-Parallelverarbeitung voll nutzen. Dabei stellt sich jedoch als Erstes die Frage, wo die Parallelverarbeitung den meisten Gewinn bringt. Mit einem System-Tracing-Tool können Entwickler genau die Prozesse oder Threads in Prozessen herausgreifen, die große Mengen an CPU-Zeit verbrauchen. Anschließend können sie mit einem Application Profiler die Funktionsleistung einzelner Prozesse analysieren und so herausfinden, welcher Code in einem Prozess oder Thread die meiste CPU-Zeit verbraucht. fill_array() ist eine rechenintensive Funktion, die ein großes zweidimensionales Array bedient. Sie verbraucht in der Applikation die meiste CPU-Zeit. Da fill_array() ein einzelner Thread ist, würde sich die Performance auch dann nicht verbessern, wenn er von einem Uniprozessorsystem auf ein Multi-Core-System verschoben würde. Wird der Algorithmus in verschiedene Threads gesplittet (Abbildung 1), kann das Betriebssystem die Rechenlast auf alle verfügbaren Prozessoren verteilen und so die Rechenzeit wesentlich verkürzen. Dies ist ein wichtiger Vorteil der auf symmetrischem Multiprocessing, kurz SMP, basierenden Echtzeitbetriebssysteme. Der Einsatz von System-Tracing-Tools für die Parallelverarbeitung erleichtert den Wechsel zu Multi-Core-Plattformen und liefert Applikationen ein Plus an Durchsatz und Performance.


Abb. 1: Erfordernisse für eine erfolgreiche Migration auf Multi-Core

Ressourcenkonflikte entdecken und reduzieren
Wurde eine Software auf eine Multi-Core-Umgebung migriert, verbesserte sich häufig die Performance nicht so sehr wie erwartet. Der Grund dafür sind in aller Regel Ressourcenkonflikte. Da Multi-Core parallel verarbeitete Prozesse bedeutet, kann es anders als bei Uniprozessorsystemen zu Ressourcenkonflikten kommen. Teilen sich zwei oder mehr Threads eine Datenstruktur, die durch eine Sperre, auch Mutex genannt, mit gegenseitigem Ausschluss geschützt ist, kommt es zu einem auf SMP-Systemen typischen Engpass. Die Mutex verhindert, dass verschiedene Threads simultan dieselbe Ressource nutzen. Greifen zwei Threads verschiedener Cores auf die durch eine Mutex geschützte Ressource zu, entsteht eine Art Wettkampf um die Ressource, der Zeit kostet: Anstatt parallel zu laufen und damit den Hauptvorteil von Multi-Core zu nutzen, müssen sich die Threads bei der Ausführung abwechseln. Die Routingtabelle in einer Netzwerkapplikation ist ein typisches Beispiel für Ressourcenkonflikte. In einer Uniprozessorumgebung hat nur jeweils ein Prozess darauf Zugriff. In einer Multi-Core-Umgebung hingegen können Threads verschiedener Cores gleichzeitig auf die Tabelle zugreifen und miteinander konkurrieren. Je schneller der Entwickler die Ursache eines Konfliktes erkennt und behebt, desto mehr erspart er sich Zeit und Frustration. Interessanterweise wirkt sich diese Maßnahme selbst dann positiv auf die Performance aus, wenn ein System scheinbar bereits angemessen schnell läuft. Ein gutes System-Tracing-Tool hilft Ressourcenkonflikte entdecken, indem es Prozesse markiert, die zwar ausführbereit, aber blockiert sind. Es kann aufgrund von Ressourcenkonflikten blockierte Threads statistisch erfassen und Core-zu-Core Messaging grafisch abbilden. Hilfreich sind zudem Features wie Suchmöglichkeiten für spezielle Systemereignisse oder die grafische Darstellung des Ausführungsablaufs mit exakten Zeitstempeln. Anhand dieser Informationen kann der Entwickler entscheiden, wie er das System am besten optimiert, beispielsweise indem er die Applikation in mehrere Threads splittet, um die Parallelverarbeitung auszubauen oder indem er die Quelle des Konfliktes entfernt und die Ressource für die Cores repliziert. Als Beispiel möge eine Applikation dienen, bei der vier Threads auf ein großes Array gemeinsamer Daten zugreifen und dieses bedienen. Auf einem Uniprozessorsystem würde das Real-Time Operating System (RTOS) die Gleichzeitigkeit der Threads lediglich simulieren. Auf einem Multi-Core-System laufen die Threads tatsächlich zeitgleich. In einem Uniprozessorsystem benötigt die Applikation nur eine einzige Mutex, um die gemeinsamen Daten ausreichend zu schützen. In einem Multi-Core-System jedoch kann eine einzelne Mutex zu erheblichen Konflikten führen. Läuft diese Applikation mit einer einzigen Mutex auf einem System mit vier Cores, laufen alle vier Threads parallel und benötigen für die Ausführung 2,046 Sekunden. Offensichtlich verbringen sie viel Zeit im Wettkampf um die Mutex, die dieses Array schützt. Weitere Mutexe für den Zugriff auf dieses Array verringern diesen Konflikt. Da jede neue Mutex eine bestimmte Region schützt, kann jeder Thread auch nur auf einen bestimmten Bereich des Arrays zugreifen. Mit 16 Mutexen sinkt die Gesamtverarbeitungszeit von 2,046 Sekunden auf 800 Millisekunden. Interessanterweise brachte der Einsatz von noch mehr Mutexen keine weiteren Fortschritte. Im Gegenteil: Die Gesamtverarbeitungszeit stieg, sobald 40 Mutexe im Spiel waren. Mit anderen Worten: Zu viele Mutexe können die Performance verringern. Der springende Punkt ist aber, dass Software eben nur selten für Multi-Core-Umgebungen optimiert ist. Entwickler benötigen daher Visualisierungswerkzeuge, um das Systemverhalten zu verstehen und den besten Ansatz zur Verbesserung des vorhandenen Codes zu finden.

Übermäßige Migration der Threads reduzieren
Manche Betriebssysteme unterstützen die so genannte Prozessor-Affinität mit dem Ziel, die Performance von Multi-Core-Systemen zu optimieren. Der OS-Scheduler versucht stets, einen Thread auf den Core zu legen, auf dem er zuletzt gelaufen ist. Auf diese Weise kann sich der Core die Befehle des Threads direkt aus dem L1 Cache holen, anstatt sie aus dem L2 Cache oder dem Hauptspeicher laden zu müssen. In manchen Fällen jedoch wandern die Threads von Core zu Core und überschreiben gegenseitig ihre im Cache gespeicherten Befehle. Dadurch muss der L1 Cache ständig neu geladen werden. Jedesmal, wenn ein Thread auf einem anderen Core ausgeführt wird, geht der Performance-Vorteil durch wieder verwendete Informationen aus dem L1 Cache verloren. Unnötige Migrationen von Threads entstehen auch dadurch, dass Threads mit höherer Priorität die Ausführung von Threads mit niedrigerer Priorität unterbrechen. Dabei besteht jedes Mal die Möglichkeit, dass das OS das Handling des Threads auf einem anderen Core einplant. Je mehr Cores auf einem Chip sind, desto wahrscheinlicher ist dieser Fall. Ein System Profiler liefert Statistiken über die Anzahl der Core-zu-Core-Migrationen für einen bestimmten Thread. Durch die grafische Darstellung dieser Statistiken erhält der Entwickler eine systemumfassende Perspektive. Und es ist für ihn einfacher, sich einen potenziellen Problembereich heranzuholen. Ein Beispiel wären 280 Thread-Migrationen für einen Thread in einem beobachteten Zeitfenster von 8,979 Sekunden. Diese Zahl ist nicht besonders hoch, kann aber als zu hoch für diese Applikation angesehen werden. Gebündeltes Multiprocessing, kurz BMP, hilft die Anzahl der Migrationen reduzieren. Mit BMP hat QNX eine neue Form des Multiprocessing eingeführt, die Threads während der Laufzeit mit einem einfachen Funktionsaufruf (Abbildung 2) an einen bestimmten Core oder mehrere Cores bindet. Dieses Vorgehen unterbindet wirkungsvoll die Thread-Migration der Applikation. Fest an einen Core gebundene Threads und BMP sollten nur dann genutzt werden, wenn es absolut notwendig ist. Schließlich ist die Migration von Threads sehr nützlich, da der Scheduler mit ihr alle verfügbaren CPU-Cores maximal ausnutzen kann. Prinzipiell notwendig ist BMP bei Applikationen mit eng gekoppelten Prozessen sowie bei Threads, die auf einem einzelnen Core mehr Leistung bringen.


Abb. 2: Binding Threads

Ein umfassender Ansatz
Die richtigen Tools vereinfachen zwar Troubleshooting und Optimierung von Multi-Core-Designs, funktionieren aber nicht isoliert. Auch ein korrekt aufgebautes Betriebssystem verringert die Komplexität beim Einsatz von Software auf Multi-Core-Chips. Dies trifft vor allem dann zu, wenn das OS die Zuordnung gemeinsamer Hardware-Ressourcen auf einem Multi-Core-Chip transparent verwalten kann. Zudem hat ein für Multi-Core entwickeltes OS die nötige Flexibilität, um die von den Tools vorgeschlagene Optimierungsstrategie umzusetzen. Das QNX Neutrino RTOS zum Beispiel liefert mit gebündeltem Multiprocessing die transparente Ressourcenverwaltung von traditionellem SMP, während der Entwickler gleichzeitig jeden Prozess auf einen bestimmten Core festlegen kann. Dadurch lassen sich Engpässe, wie übermäßiges Messaging zwischen Cores, deutlich einfacher beheben.


  1. www.qnx.com
    Hat Ihnen dieser Artikel gefallen? Dann abonnieren Sie das Entwickler Magazin direkt über unser Online-Formular.



zur vorherigen Seite
zurück
an den Anfang der Seite
nach oben
Diesen Artikel drucken
drucken
Diesen Artikel weiterempfehlen
empfehlen
Software & Support Verlag GmbH