Zeitverhalten automatisch analysieren und optimieren Kurzer Weg zum schnellen Code

In komplexen Embedded Systemen kann jeder noch so kleine Performance-Bug schnell zum Flaschenhals werden. Mit neuen Verfahren lassen sich Performance-Einbrüche bei der Codeabarbeitung und beim Datenzugriff automatisch finden, die Tools geben zudem Hinweise für die Optimierung des Codes.

Galt früher alleine Stabilität im Sinne von logischen Fehlern oder Programmierfehlern als Maß für die Qualität von Software, so sind heute auch die erzielte Rechenleistung, etwaige Performance-Einbrüche und eine optimale Ausnutzung der Hardware mit einzubeziehen. Performance-Bugs lassen sich zwar mit Hilfe üblicher Profiling-Methoden messen, diese erfordern allerdings eine Code-Instrumentierung, beeinflussen also selbst das Laufzeitverhalten der Applikation.

In typischen »Deeply Embedded«-Anwendungen zum Beispiel im Automobil- oder Industriebereich ist eine solche Veränderung des Laufzeitverhaltens nicht akzeptabel. Hier sind alle auszuführenden Tasks zyklisch abzuarbeiten, nicht selten ist die Länge des Zyklus' genau festgelegt und keine der Tasks darf das vorgegebene Zeitregime verletzen, auch wenn es zu Ausnahmesituationen kommt. Die Applikation muss also bestimmte Antwortzeiten zwingend einhalten und darf auch unter hoher Last die Spezifikation nicht verletzen.

Anders als bei universellen Prozessoren ist bei modernen »Systems-on-Chip« (SoC) wie den High-End-Mikrocontrollern »TC1797« von Infineon (Bild 1) nicht so sehr die – meist vergleichsweise niedrige – Taktfrequenz wichtig, sondern ein auf das jeweilige Anwendungsgebiet optimiertes, komplexes Systemdesign. Der Schlüssel für eine hohe Leistungsfähigkeit und einen großen Datendurchsatz liegt vor allem in den geringen Zugriffzeiten der internen Speicher, den spezialisierten Einheiten und der Aufgabenteilung der Busse. Die richtigen Werkzeuge vorausgesetzt, lässt sich die Performance meist noch deutlich steigern.

Codeoptimierung mittels Profiling

Moderne Compiler bieten dem Entwickler meist gleich eine ganze Reihe statischer, auf der Analyse des Programmcodes basierender Code-Optimierungsmöglichkeiten, die unabhängig von der Zielarchitektur anwendbar sind. Dazu gehören zum Beispiel »Loop Unrolling«, »Function Inlining« oder »Common Subexpression Elimination«, um nur einige zu nennen. Zusätzlich können optimierende Compiler spezielle Chip-Features ausnutzen, wie sie auch der TC1797 bietet. Weil hierfür das zeitliche Verhalten des Programms unter realen Bedingungen bekannt sein muss, versagt hier die statische Programmanalyse. Vor allem die Einflüsse des Speichersubsystems auf die Leistung lassen sich erst während der Laufzeit beobachten und messen. Einige typische Performance-Bugs, die in diesem Zusammenhang auftreten können, sind: Cache-Effekte: Code und Daten werden wiederholt aus den Caches verdrängt und müssen aufwändig aus den langsameren Speichern zurückgeholt werden, Kontrollfluss mit vielen Sprüngen: Sprungbefehle bremsen das Programm aus, zum Beispiel wenn der Befehlscache nicht den angesprungenen Code enthält oder die Prozessor-Pipelines leerlaufen, hohe Interruptlast: Häufige Interrupts unterbrechen den normalen Programmablauf. Damit verbunden sind wiederum Verdrängungseffekte in den Caches.

Um bei der Code-Optimierung auch solche Effekte berücksichtigen zu können, gilt es erst einmal, Informationen über das Laufzeitverhalten zu sammeln. Ein »Application Profiling« zeigt, welche Wege das Programm durch seinen Kontrollflussgraphen (CFG) bevorzugt. Letzterer beinhaltet alle möglichen Wege durch das Programm in Form von Kanten, die wiederum die Basisblöcke (Knoten) miteinander verbinden. Basisblöcke sind linear hintereinander ausgeführte Sequenzen von Maschinenbefehlen, die keine Verzweigung durch einen Sprung oder Call beinhalten. Die »Coverage-Analyse«, ein Verfahren, das beispielsweise für das Profiling im »GNU-Compiler« zur Anwendung kommt, instrumentiert nun alle Kanten, die für eine spätere Rekonstruktion des Kontrollflusses nötig sind (Bild 2). Die Instrumentierung fügt eine Codesequenz in die jeweilige Kante ein und allokiert gleichzeitig einen Speicherplatz im Target-Speicher, der einen Zähler enthält. Beim Beschreiten der Kante wird gleichzeitig der zugehörige Zähler inkrementiert.

Nach Beendigung des Profiling-Laufes wertet das Tool »gcov« die Zähler aus und ermittelt Programmteile, die einen hohen Anteil an der Gesamtlaufzeit haben. Zusätzlich lassen sich auch kritische Pfade im Programm finden, die durch Umsortieren und Zusammenfassen von Basisblöcken ein günstigeres Cache-Verhalten und eine bessere Auslastung der Prozessor-Pipelines ermöglichen. Wie schon erwähnt, ist die eben beschriebene Verfahrensweise für Deeply-Embedded-Applikationen allerdings nicht akzeptabel. Nicht nur verändert die Instrumentierung das Laufzeitverhalten, sondern die Zähler belegen auch den oft knappen Target-Speicher.