C++ für Echtzeit-Anwendungen
Die Objektorientierte Programmierung in C++ hat unter vielen Embedded-Programmierern den Ruf, gegenüber der Strukturierten Programmierung in C weniger performant zu sein. Hierdurch wird C++ für zur Erreichung harter Echtzeit meist von vornherein ausgeschlossen. Aber ist das auch berechtigt?
Anbieter zum Thema

Die Objektorientierte Programmierung (OOP) in C++ hat unter vielen embedded Programmierern den Ruf nicht so performant zu sein wie die Strukturierte Programmierung (SP) in C. Insbesondere bei Programmteilen, die mit kleinen Abtastraten arbeiten (<1 ms), so die Argumentation, muss nach wie vor stark auf Rechenzeit-Overhead geachtet werden. Hierdurch wird C++ meist von vornherein ausgeschlossen.
Dennoch möchte man bei sinkenden Entwicklungszeiten und steigender Anzahl von Programmierern an einem Projekt die bekannten Vorteile der Objektorientierung nutzen. Das Ziel dieser Veröffentlichung ist es daher, aufzuzeigen mit welchen Rechenzeit-Einbußen bei Verwendung von OOP mit C++ im Vergleich zu SP in C zu rechnen ist. Weiterhin soll die Frage beantwortet werden, ob dies wirklich ein Ausschlusskriterium für den Einsatz von C++ in echtzeitkritischen Anwendungen bedeutet.
Echtzeit mit C++: Direkte Vergleiche mit C kaum möglich
Die SP in C unterscheidet sich stark von der OOP in C++. Ein direkter Rechenzeit-Vergleich beider Programmierstile ist hierdurch kaum möglich. Hier sollen daher die wichtigsten elementaren Bausteine beider Sprachen miteinander verglichen werden.
Untersucht werden die in Tabelle 1 angegebenen Szenarien. Für jedes Szenario wird jeweils ein Programmstück mit einem Compiler in Objektcode übersetzt und anschließend in Assemblercode konvertiert. Der Assemblercode erlaubt einen direkten Vergleich zwischen C und C++.
Weiterhin kann durch Zählen der benötigten CPU-Zyklen eine Abschätzung getroffen werden, mit wie viel Overhead bei C++ zu rechnen ist. Bei den Szenarien wird jeweils zwischen direkten und indirekten Aufrufen unterschieden. Unter einem direkten Aufruf versteht man den Zugriff auf ein Objekt / eine Funktion ohne Zwischenschritt.
Demgegenüber steht der indirekte Aufruf - ein Zugriff auf ein Objekt/Funktion über einen Zeiger oder Referenz. Für die Untersuchungen wird die Prozessorarchitektur „Cortex-M3“ der Firma ARM verwendet [1][2]. Das Übersetzen des C++ Codes und das Auslesen des Assembler Codes wird mit gnu gcc in der Version 4.9.3 durchgeführt.
Damit der Assemblercode die Realität möglichst genau wiedergibt, wird beim Aufruf des gcc stets die Optimierungseinstellung -o2 verwendet.
Direkter Funktionsaufruf (ohne Kontext) vs. Statischer Methodenaufruf
Zum Vergleich von statischen Methoden mit Funktionen werden die in Bild 1 sowie Bild 2 dargestellten Code-Fragmente ausgewählt. In beiden Fällen wird die Methode/Funktion über einen direkten Zugriff aufgerufen und anschließend eine globale Variable geschrieben. Diese Aufrufart spiegelt, stark vereinfacht, den mit C üblicherweise verwendeten Programmierstil im embedded Bereich wieder.
Nach Übersetzung folgt der ebenfalls in Bild 1 und Bild 2 unten angegebene Assembler-Code. In roter Schrift sind die Prozessorzyklen angegeben, die für jeweils eine Operation benötigt werden. Dabei ist zu bemerken, dass Branch-Instruktionen vom Zustand der Prozessor-Pipeline und von der Branch-Prediction abhängen und für diese Operationen daher kein exakter Wert angegeben werden kann [1].
Die benötigten CPU-Zyklen sind demnach 8 - 12 Prozessorzyklen für beide Fälle. Bemerkenswert ist, dass sowohl die statische Methode als auch die Funktion an einer festen Position im Speicher steht und daher direkt auf sie verzweigt werden kann. Aus Compiler- und Prozessor-Sicht sind beide Aufrufarten absolut identisch.
Direkter Funktionsaufruf mit Kontext vs. Indirekter virtueller Methodenaufruf
Wird in C gefordert, dass ein Algorithmus wiederverwendbar oder mehrfach-instanziierbar ist, so führt man üblicherweise einen Kontext mit, der einer C Funktion übergeben wird. In C++ passiert dies beim Zugriff auf Methoden implizit. In Bild 3 sowie Bild 4 sind beide Fälle im Pseudocode angegeben. Ebenfalls in den Bildern enthalten, ist auch der Assemblercode.
Bis auf marginale Unterschiede beim Zugriff auf den Speicher ist der Assemblercode beider Fälle identisch. Wie zuvor ist der Compiler in der Lage eine Verzweigung auf eine konstante Adresse durchzuführen. Hinzugekommen ist die Ermittlung der Adresse des this-Zeigers bzw. die des Kontexts.
Der Compiler realisiert beide Fälle mit den gleichen Operationen. Aus Sicht einer Rechenzeitbetrachtung ergeben sich somit auch identische Ergebnisse. Für beide Fälle beträgt die Aufrufdauer 9 - 13 Zyklen.
Problematisch an dem oben angegebenen Testszenario ist, dass in C++ Klassenmethoden üblicherweise nicht direkt aufgerufen werden. Vielmehr ist es so, dass nach heutigen Programmierkonzepten eine geringe Kopplung zwischen den Klassen erwünscht ist und somit mit Referenzen oder Zeigern auf eine Objektinstanz gearbeitet wird.
Der hierdurch resultierende indirekte Zugriff auf eine Methode benötigt zusätzliche Rechenzeit von 2 Taktzyklen (für das Laden des Zeigers), die im oben angegebenen Beispiel nicht enthalten sind. Diese zusätzliche Rechenzeit wird durch die Abfrage der Instanz Adresse aus dem Zeiger/Referenz hervorgerufen.
(ID:44600617)