Ein Angebot von

Tipps und Tricks für die Laufzeitoptimierung

| Autor / Redakteur: Peter Gliwa* / Sebastian Gerstl

Bild 1: Timing auf verschiedenen Ebenen und in verschiedenen (Projekt-) Phasen
Bild 1: Timing auf verschiedenen Ebenen und in verschiedenen (Projekt-) Phasen (Bild: GLIWA)

Die Ressource „Rechenzeit“ wird in vielen Projekten im Verlauf der Entwicklung knapp. Im Folgenden werden einige praktische Ansätze beleuchtet, um in solchen Situationen Steuergerätesoftware hinsichtlich der Laufzeit zu optimieren. Zum anderen werden Maßnahmen angesprochen, um frühzeitig beim Design, bei der Konfiguration und Implementierung Laufzeitprobleme zu verhindern.

In den letzten 20 Jahren hat sich mit Blick auf das Timing bei der Entwicklung von Steuergerätesoftware viel getan. Wurde das Timing früher meist nur dann explizit betrachtet, wenn es Probleme verursachte, wird dem Timing heute oft systematisch und frühzeitig Aufmerksamkeit zuteil. Die Betriebssystemkonfiguration wird beispielsweise nicht einfach vom Vorgängerprojekt übernommen und hier und da angepasst. Stattdessen finden eingehende Überlegungen statt, welche Anforderungen an das Timing existieren und wie diese mit einer geeigneten Betriebssystemkonfiguration, Verteilung von Tasks auf die verschiedenen Kerne eines Multicoreprozessors, etc. begegnet werden können.

Immer öfter kommen dabei Techniken wie die Schedulingsimulation oder Schedulinganalyse zum Einsatz. Dennoch kommt praktisch jedes Projekt an einen Punkt, bei dem das Verhalten des realen Systems mitunter gravierend vom erwarteten Verhalten – dem simulierten oder modellierten Timing – abweicht. Hier hilft nur eine Analyse des realen Systems, falls erforderlich in der realen Umgebung, also im Fahrzeug.

Laufzeitanalyse: welche Technik für welche Situation?

Bevor wir uns den konkreten Timinganalysetechniken zuwenden, soll verdeutlicht werden, dass bei der Timinganalyse grundsätzlich zwei Fragen vorangestellt werden sollten. Erstens: In welcher Phase befindet sich das Projekt? Mögliche Phasen sind „früh“ oder „spät“ beziehungsweise „Design“, „Implementierung“ oder „Verifikation“, um es mit den Begriffen des V-Modells zu sagen. Zweitens: Auf welcher Abstraktionsebene soll Timinganalyse betrieben werden? Mögliche Ebenen sind „Codeebene“, „Schedulingebene“ oder „Netzwerkebene“. Es lässt sich feststellen, dass sich jede Auseinandersetzung mit einem konkreten Timingaspekt in einem Koordinatensystem mit den Achsen „Phase/Projektlaufzeit“ und „Ebene“ verorten lässt. Abbildung 1 stellt dies anhand von V-Modellen auf der jeweiligen Ebene dar.

Die gleichen Ebenen finden sich in Abbildung 2 unterhalb der Granularitätsachse wieder. Darüber sind die verschiedenen Timinganalysetechniken aufgetragen; es besteht eine gewisse Zuordnung anhand der horizontalen Position. Die „statische Codeanalyse“ beispielsweise findet auf der Codeebene statt und deckt den Bereich von „Opcode States“ bis hin zu „TASK/ISR“ ab.

Abbildung 3 ergänzt die Übersicht um die Aspekte „Art der Timinganalyse“ und die bereits angesprochene (Projekt-) Phase. Eine kurze Beschreibung der Funktionsweise der verschiedenen Analysetechniken findet sich auf dem Timingposter [1], dem auch die bisherigen Abbildungen entnommen sind.

Die folgende Liste zeigt Anwendungsfälle („Use-cases“) für die verschiedenen Timinganalysetechniken auf.

Statische Codeanalyse

  • Use-Case: Ermittlung der WCET (worst-case core execution time) unabhängig von der Verfügbarkeit von Hardware und unabhängig von Testvektoren
  • Anmerkungen: Die Auswirkung von Interrupts auf Cache und Pipeline wird ignoriert, ebenso diverse Multicoreeffekte wie zum Beispiel Zugriffskonflikte am Memory-Interface. Indirekte Funktionsaufrufe und Schleifenobergrenzen können unter Umständen nicht aufgelöst und müssen manuell „annotiert“ (von Hand ergänzt) werden, was fehleranfällig ist.

Codesimulation

  • Use-Case: Grobe Abschätzung der CET (core execution time) für das gegebene Testszenario
  • Anmerkung: Spielt in der Timinganalyse keine große Rolle.

Messen

  • Use-Ceses: Analyse des realen Systems (die Software läuft auf der Zielhardware) zwecks Profiling, Verifikation oder Überwachung.
  • Anmerkungen: Unter (Timing-) Profiling versteht man die Ermittlung von Timingparametern wie der CPU-Auslastung, der CET (core execution time), der RT (response time) etc. Die Ergebnisse hängen von den Testvektoren ab, die die Software während der Messung bearbeitet hat.

Tracing

  • Use-Ceses: Analyse des realen Systems (die Software läuft auf der Zielhardware) zwecks Visualisierung, Debugging, Optimierung, Profiling oder Verifikation.
  • Anmerkungen: Unter Tracing versteht man die Aufzeichnung von Ereignissen zur späteren Analyse und Visualisierung. Mit Blick auf das Timing eignen sich Schedulingtraces mit Ereignissen wie „Aktivierung“, „Start“, „Unterbrechung“, „Terminierung“ von Tasks besonders gut. Man unterscheidet Hardware-basiertes Tracing und Software-basiertes Tracing. Ersteres kann ohne Modifikation der Software auskommen, für letzteres wird die Software instrumentiert, was die Verwendung der serienidentischen Hardware im Fahrzeug erlaubt.

Schedulingsimulation

  • Use-Case: Design und Optimierung von Schedulingkonzepten und der Betriebssystemkonfiguration; Analyse des typischen Schedulingverhaltens

Statische Schedulinganalyse

  • Use-Case: Design und Optimierung von Schedulingkonzepten und der Betriebssystemkonfiguration; Analyse des worst-case Schedulingverhaltens (zum Beispiel Ermittlung der WCRT (worst-case response time)

Laufzeitoptimierung

Eine komplette Liste aller Laufzeitoptimierungsansätze zu erstellen, ist unmöglich. Daher sollen im Folgenden lediglich ein paar grundsätzliche Aspekte behandelt und exemplarisch einige konkrete Maßnahmen vorgestellt werden.

Als allgemein gültige Regel lässt sich festhalten, dass Laufzeitoptimierung immer „top-down“ erfolgen sollte, also von den oberen zu den unteren Ebenen hin. Würde man direkt mit der Codeoptimierung beginnen, wüsste man nicht, ob der betreffende Code überhaupt zu einem Zeitpunkt ausgeführt wird, an dem das Timing kritisch ist.

Besser ist es, zunächst auf der Schedulingebene ein gutes Verständnis der aktuellen Situation herbeizuführen, Optimierungen auf dieser Ebene durchzuführen und anschließend mittels Codeoptimierung die verbleibenden „Hot-spots“ anzugehen.

Die Bedeutung des guten Verständnisses wird leider oft unterschätzt. In vielen Projekten konnte ich beobachten, wie Integratoren beim Blick auf den ersten heruntergeladenen Trace mit einer gewissen Fassungslosigkeit feststellen mussten, dass sich das System ganz anders verhält, als gedacht und zuvor simuliert.

Laufzeitoptimierung auf der RTOS (Scheduling-) Ebene

Die oberste Regel für Laufzeitoptimierung auf der Schedulingebene wird leider oft missachtet, obwohl sie denkbar einfach ist: „Keep it simple!“

Konkret heißt das beispielsweise, dass die Betriebssystemkonfiguration möglichst einfach gehalten wird. Am besten sollte BCC1 (Basic Conformance Class ohne Mehrfachaktivierungen) zum Einsatz kommen. Leider legen die meisten AUTOSAR RTE Generatoren die Verwendung von ECC nahe, indem sie eine nicht terminierende ECC Task anlegen und darin eine zweite Ebene des Schedulings einführen: die RTE Runnables werden in dieser ECC Task mittels Events getriggert. Die damit verbundene Komplexität wird von den meisten Projektverantwortlichen nicht mehr überklickt und die Analyse von Timingproblemen wird deutlich erschwert.

Ganz nebenbei lässt sich mit der Verwendung von BCC der Stackbedarf signifikant verringern.

Bild 4: Effizient, hoch ausgelastet, zuverlässig und sicher: Aktivlenkung des BMW X5
Bild 4: Effizient, hoch ausgelastet, zuverlässig und sicher: Aktivlenkung des BMW X5 (Bild: GLIWA)

Eine weitere Maßnahme, um typische Echtzeitprobleme zu vermeiden, den Laufzeitbedarf und gleichzeitig den Stackbedarf weiter zu minimieren ist die Verwendung von kooperativem Multitasking. „Kooperativ“ heißt hier, dass Taskwechsel nur zu bestimmten Zeitpunkten erfolgen dürfen – sinnigerweise dann, wenn gerade kein Runnable läuft. Die RTE oder vergleichbare Mechanismen stellen dann fest, dass zur Sicherung der Datenkonsistenz keine Kopien der Daten notwendig sind. Ergebnis: Einsparung bei RAM und Laufzeit. Es leuchtet ein, dass darüber hinaus der Stackbedarf typischerweise drastisch reduziert wird, da eine Verschachtelung von Runnables nicht mehr möglich ist.

Abbildung 4 zeigt den Trace eines sehr positiven Beispiels für ein gelungenes Timingdesign: die Aktivlenkung des BMW X5 (e70). Das System ist – je nach Zustand – bis über 93% ausgelastet aber niemals überlastet. Durch die Optimierungsmaßnahmen konnte ein günstigerer, weniger leistungsfähiger Prozessor als in der Vorgängergeneration zum Einsatz kommen – und das bei zusätzlicher Funktionalität.

Bild 5: Standardimplementierung der memcpy Funktion
Bild 5: Standardimplementierung der memcpy Funktion (Bild: GLIWA)

Laufzeitoptimierung auf der Codeebene

Als Beispiel für die Laufzeitoptimierung auf der Codeebene soll an dieser Stelle die wohlbekannte Funktion memcpy optimiert werden. memcpy kopiert eine definierte Anzahl von Bytes von einem Speicherbereich in einen anderen. Eine Standardimplementierung ist in Abbildung 5 ersichtlich.

Bild 6: Der generierte memcpy Assemblercode des Ausgangspunktes der Optimierung
Bild 6: Der generierte memcpy Assemblercode des Ausgangspunktes der Optimierung (Bild: GLIWA)

Wird diese Funktion zum Kopieren von einem Kibibyte (1024 Bytes) auf einem Infineon AURIX TC275 mit 200 MHz Taktfrequenz und unter Verwendung eines TASKING Compilers verwendet, so ergibt sich für die Default Memory Locations eine minimale CET (core execution time) von 114,395µs oder 111,7ns pro Byte. Dies ist der Ausgangspunkt für die nun folgenden Optimierungen. Der generierte Assemblercode ist in Abbildung 6 zu sehen.

Im ersten Schritt sollen nun andere Speicherorte verwendet werden. Abbildung 7 zeigt einen Auszug aus dem AURIX Handbuch, dass die Zugriffsdauer auf verschiedene Speicher in Taktzyklen angibt. Ein entscheidender Multicoreaspekt sei an dieser Stelle schon mal erwähnt: die in der Tabelle angegebenen Zahlen gelten für den Fall, dass am Memory-Interface kein Zugriffskonflikt auftritt, also nicht etwa, dass ein anderer Kern gerade mit höherer Priorität auf den Speicherbereich zugreift. Im Falle eines Konfliktes kann die Verzögerung wesentlich größer sein.

Bild 7: Zugriffszeiten auf verschiedene Speicher des Infineon AURIX TC275
Bild 7: Zugriffszeiten auf verschiedene Speicher des Infineon AURIX TC275 (Bild: Infineon)

Doch auch ohne Konflikte ist der Unterschied zwischen „schnellen“ und „langsamen“ Speichern erheblich. Die Default Memory Locations des Ausgangsstands waren: Cached Flash0 für den Code, LMU RAM für das Ziel der Kopie und Cached Flash0 für die Quelle. Wird das Ziel nun statt im LMU RAM im Local DSPR0 abgelegt, verringert sich die Kopiergeschwindigkeit auf 100,6ns pro Byte.

Im nächsten Schritt wird dem Compiler per #pragma oder per Compileroption –t0 vorgegeben, bei der Kompilierung möglichst schnellen Code zu erzeugen. Der Compiler generiert nun anderen Assemblercode, siehe Abbildung 8, unter der Verwendung von prinzipiell erstrebenswerten „post-increment“ Speicherzugriffen und unter der Verwendung von speziellen Befehlen des AURIX Befehlssatzes, in diesem Fall der loop Anweisung. Der so erzeugte Code läuft wesentlich schneller und benötigt nur noch 59,6ns pro Byte.

Bild 8: Der generierte memcpy Assemblercode mit Compileroptimierung
Bild 8: Der generierte memcpy Assemblercode mit Compileroptimierung (Bild: GLIWA)

Abschließend wird der Code von Hand optimiert. In den meisten Fällen ist dies auf der C-Code-Ebene möglich, wobei permanent a) der erzeugte Assemblercode überprüft werden und b) mittels Messung die tatsächliche Laufzeit ermittelt werden muss. Im Bereich der Codeoptimierung ist die Verlockung groß, sich Annahmen und Vermutungen hinzugeben. Selbst die Besten der Besten erleben Überraschungen und müssen sich eingestehen, dass das erwartete Laufzeitverhalten eines Optimierungsansatzes weit entfernt liegt vom realen Verhalten und nur die Messung am realen System dies offenbart.

Vor der eigentlichen manuellen Optimierung ein paar Überlegungen zu memcpy. Laut Spezifikation kann sie Daten mit der Granularität von einem Byte kopieren. Der AURIX kann als 32bit Prozessor sehr effizient mit vier Byte großen Wörtern umgehen. Analysiert man die Datenobjekte einer typischen Anwendung im Automobilbereich, stellt man fest, dass die Größe der meisten Datenobjekte einem ganzzahligen Vielfachen von vier entspricht und sie auch vier Byte aligned sind – also eine Speicheradresse aufweisen, die ebenfalls ein ganzzahliges Vielfaches von vier ist. Die in Abbildung 9 dargestellte Implementierung der entscheidenden Bereiche von memcpy macht sich diese Erkenntnisse zu Nutzen und überprüft, ob Quelle und Ziel vier Byte aligned sind und ob auch die Anzahl der zu kopierenden Bytes ein ganzzahliges Vielfaches von vier ist. Ist dies der Fall, werden immer vier Bytes in einem einzelnen Schleifendurchlauf kopiert.

Bild 9: Von Hand optimierte Version von memcpy (Auszug)
Bild 9: Von Hand optimierte Version von memcpy (Auszug) (Bild: GLIWA)

Diese manuelle Optimierung ergibt zusammen mit allen zuvor genannten Optimierungen einen Laufzeitbedarf von 14,7ns pro Byte. Immerhin eine Laufzeitersparnis von rund 87% gegenüber der Ausgangsversion.

Multicore-spezifische Aspekte

Für Multicoresysteme hat es sich bewährt, die Funktionalität so aufzuteilen, dass intensive Berechnungen und die Bearbeitung vieler Interrupts auf verschiedene Kerne aufgeteilt werden. Als Folge werden bei den komplexen Berechnungen Pipeline und Cache besser genutzt, was den Durchsatz erhöht.

„Busy-Spinning“, also das Warten eines Kerns auf die Freigabe einer Ressource auf einen anderen Kern, sollte wann immer möglich konzeptionell vermieden werden. In vielen Fällen lässt sich der Einsatz von Spin-locks durch geeignetes Design der Software komplett vermeiden. Bei der Portierung von Singlecore Software auf Multicore ist das blinde Suchen/Ersetzen von DisableInterrupts()/EnableInterrupts() mit GetSpinlock()/ ReleaseSpinlock() eine schlechte Idee. Der beste Mechanismus zum Schutz von Daten ist der, den man nicht braucht.

Zusammenfassung, Ausblick

Laufzeitoptimierung ist komplex und vielschichtig. Sie kann und sollte auf verschiedenen Ebenen stattfinden: auf der Schedulingebene und auf der Codeebene und zwar in dieser Reihenfolge, um zu verhindern, dass (Codeoptimierungs-) Aufwand betrieben wird, der die Gesamtsituation gar nicht verbessert.

Voraussetzung für eine zielgerichtete Analyse und Optimierung ist die Kenntnis der verschiedenen Analysetechniken. Nur so kann man für den jeweiligen Anwendungsfall das richtige Werkzeug bemühen.

Der wichtigste erste Schritt, eine existierende Anwendung auf dem Weg zu einer effizienten und sicheren Software zu bringen, ist das Verständnis darüber, wie sich das System tatsächlich verhält. Vermutungen und Annahmen sind hier fehl am Platz; der Einblick und die Analyse des realen Systems unter realen Bedingungen (also auch im Fahrzeug) sind entscheidend.

Seit einigen Jahren machen sogenannte C-to-C Compiler von sich reden, die gegebenen C Code in parallelisierbaren C Code übersetzen sollen. Ich bin diesem Ansatz gegenüber skeptisch. Meine Vermutung (oder soll ich sagen: „Hoffnung“?) geht eher in die Richtung, dass eine bessere Ausnutzung von Multicore dadurch erzielt werden wird, dass die Codegeneratoren grundsätzlich anders arbeiten werden. Zukünftige Generatoren werden es erlauben, aus einem generischen Modell („generisch“ im Sinne von nicht auf Singlecore oder Multicore festgelegt) entweder Code für Singlecore (bzw. Single-thread) oder aber gut parallelisierbaren Code für Multicore zu erzeugen.

Leichteres Auffinden und Beheben von Timing-Fehlern

Leichteres Auffinden und Beheben von Timing-Fehlern

22.10.19 - PLS und Inchron haben eine gemeinsame Toolbox für die effiziente Visualisierung und Analyse von Echtzeitsystemen vorgestellt. Das abgestimmte Hard- und Software-Paket ermöglicht Entwicklern komplexer echtzeitkritischer Anwendungen im Automotive- und Industrie-Umfeld eine umfassende, exakte Beurteilung von Abhängigkeiten verschiedenster Ereignisse und deren Zeitverhalten. lesen

Automatische Multi-Core-Echtzeitvalidierung

Automatische Multi-Core-Echtzeitvalidierung

09.08.17 - Dieser Beitrag zeigt am Beispiel des freien Echtzeitbetriebssystemes Erika Enterprise einen modellbasierten Ansatz, um die Einhaltung von sicherheitskritischen Echtzeitanforderungen mittels Tracing über den kompletten Entwicklungsprozess zu überwachen. lesen

Quellenverzeichnis

[1] Timingposter, Peter Gliwa, Februar 2013

(Der Beitrag wurde mit freundlicher Genehmigung des Autors dem Embedded Software Engineering Kongress Tagungsband 2018 entnommen.)

Seit der Gründung 2003 steht Peter Gliwa als geschäftsführender Gesellschafter an der Spitze von GLIWA GmbH embedded systems.
Seit der Gründung 2003 steht Peter Gliwa als geschäftsführender Gesellschafter an der Spitze von GLIWA GmbH embedded systems. (Bild: GLIWA)

Autor

* Peter Gliwa hat Elektrotechnik an der Berufsakademie Stuttgart studiert. Über viele Jahre hinweg entwickelte er die Timingsuite T1 und berät heute international Kunden in Timingfragen und bei Themen rund um Echtzeitbetriebssysteme. Zuvor war er bei ETAS zunächst Entwickler, danach Produktmanager des Echtzeitbetriebssystems ERCOSEK. Zwischen 2001 und 2006 war er darüber hinaus als Dozent für das Fach „Mikrocomputertechnik” tätig.

Kommentar zu diesem Artikel abgeben

Schreiben Sie uns hier Ihre Meinung ...
(nicht registrierter User)

Zur Wahrung unserer Interessen speichern wir zusätzlich zu den o.g. Informationen die IP-Adresse. Dies dient ausschließlich dem Zweck, dass Sie als Urheber des Kommentars identifiziert werden können. Rechtliche Grundlage ist die Wahrung berechtigter Interessen gem. Art 6 Abs 1 lit. f) DSGVO.
Kommentar abschicken
copyright

Dieser Beitrag ist urheberrechtlich geschützt. Sie wollen ihn für Ihre Zwecke verwenden? Kontaktieren Sie uns über: support.vogel.de/ (ID: 46231426 / Echtzeit)