Suchen

C++ für Echtzeit-Anwendungen

Seite: 2/2

Firma zum Thema

Indirekter Funktionsaufruf mit Kontext vs. virtueller Methodenaufruf

Die wichtigsten Prinzipien der OOP sind Vererbung und Polymorphie. Beide Prinzipien lassen sich in C ebenfalls nachprogrammieren. Da die Sprache C aber für SP entwickelt wurde, ist eine vollständige Nachbildung der OOP mit großem Aufwand verbunden.

Daher wird an dieser Stelle hierauf verzichtet. Für einen Vergleich wird wiederum auf eine Aufgabenstellung mit eingeschränktem Funktionsumfang zurückgegriffen.

Bildergalerie

Bildergalerie mit 10 Bildern

Ein Algorithmus soll zur Laufzeit austauschbar sein. In C kann dies über Funktionszeiger und ein Kontext Argument erreicht werden. In C++ würde man von einer Basisklasse erben und durch die Basisklasse auf den Algorithmus zugreifen.

In Bild 5 sowie Bild 6 sind beide Fälle im Pseudocode angegeben. Ebenfalls in den Bildern enthalten, ist auch der resultierende Assemblercode.

Wie an dem Assemblercode zu erkennen ist, ist der Compiler nun nicht mehr in der Lage direkt auf die Funktion/Methode zu verzweigen. Die Sprungadresse muss in beiden Fällen aus der Objektinstanz bzw. dem Kontext abgefragt werden. Dies benötigt zusätzliche Zyklen.

Im Vergleich zu dem Funktionsaufruf muss die Adresse der virtuellen Methode zusätzlich aus einer speziellen Tabelle - genannt virtual function table - abgefragt werden. Wie die virtual function table aufgebaut ist, kann [4] entnommen werden.

Letztendlich ist die Verwendung eines Funktionszeigers beim gegebenen Beispiel um durchschnittlich 2 Taktzyklen schneller. Allerdings muss hier gesagt werden, dass der Ansatz über virtuelle Methoden bei weitem flexibler ist. Insbesondere da dieses Sprachkonstrukt elementar für die gesamte OOP ist.

Der Fall der Mehrfachvererbung sowie die Verwendung von Vererbungshierachien hat den gleichen Performance Overhead wie die Einfachvererbung, da für jede vererbte Klasse eine eigene virtual function table erstellt wird.

Vergleich der Aufrufarten

Zusammenfassend sind die Ergebnisse in Tabelle 2 der Bildergalerie angegeben. Der maximale Unterschied für die gegebenen Testszenarien sind 2 Taktzyklen. Wenn man bedenkt, dass ein Zyklus typischerweise wenige Nanosekunden benötigt (100MHz = 10ns) ist der Overhead von C++ gegenüber C marginal.

Ein Zahlenbeispiel soll dies verdeutlichen: Angenommen man würde auf einem 100 MHz schnellem Cortex-M3 100 virtuelle Methoden indirekt aufrufen. Jede Methode schreibt eine Membervariable. Im Vergleich zu 100 indirekten Funktionsaufrufen mit Kontext, die jeweils eine Variable einer Struktur schreiben, wäre der zeitliche Unterschied ca. 2 us.

Für den Großteil der Anwendungen ist dies durchaus vertretbar, insbesondere im Hinblick auf die genannten Vorteile der OOP.

Embedded Programmierstil

Trotz der oben gemachten Erkenntnisse ist es ohne Weiteres möglich in C sowie C++ „langsamen“ Code zu schreiben. Allerdings fällt insbesondere bei C++ auf, dass die Literatur der letzten Jahre einen verständlichen aber eher ineffizienten Programmierstil fördert. Ein Vergleich beider Sprachen nutzt nur wenig, wenn in C++ ineffizienter programmiert wird.

Die nachfolgende Liste beschreibt die häufigsten Ursachen für ineffizienten Echtzeit-Code in C++:

1. Übermäßige Verwendung von Interfaces

Durch den zusätzlichen Overhead, welcher durch den Aufruf von virtuellen Methoden entsteht, sollten Interfaces (abstrakte Klassen) nur dort wo es unbedingt notwendig ist, verwendet werden. In folgenden Fällen lassen sich Interfaces nur schwer vermeiden:

  • Trennung von unterschiedlichen Schichten im System: Dies bietet Vorteile beim Testen und vermeidet zu viele Abhängigkeiten zu anderen Schichten. Man sollte aber darauf achten, dass die Schichten nicht zu feingranular werden.
  • Austausch von Algorithmen: Müssen Algorithmen zur Laufzeit ausgetauscht werden, so führt kein Weg an Interfaces vorbei (siehe Bild 7). Steht zur Compile-Zeit fest welcher Algorithmus verwendet werden soll, so bieten sich Class-Templates (z. B.: Policy-Pattern) zur Vermeidung von Interfaces an (siehe Bild 8).

Im dynamischen Fall, muss die Methode update() zwingend virtuell sein. Dies ist im statischen Fall nicht erforderlich. Somit ergibt sich ein erheblicher Performance Vorteil.

2. Zugriff auf Member

Im embedded Bereich ist es heutzutage noch üblich, direkt auf globale Variablen zuzugreifen. Aus Compiler Sicht ist dieses Vorgehen sehr performant. Moderne Softwareentwicklungsmethodiken verzichten auf derartige Konstrukte, da sie sehr fehleranfällig sind. Stattdessen sind Getter/Setter Methoden die Regel.

Diese meist sehr elementaren Methoden sollten stets mit dem Attribut inline versehen werden. Dies erlaubt dem Compiler die Methode direkt einzufügen. Berücksichtigen muss man hierbei nur, dass inline Methoden nicht mehr virtuell sein können.

3. Dynamisches Speichermanagement

Bei C++ ist die gängige Lehrmeinung, möglichst alle Objekte dynamisch anzulegen. Problematisch hieran ist, dass die Allokation und Freigabe von Speicher große Mengen Rechenzeit benötigt und nicht mehr deterministisch ist. Der Allokator muss eine Lücke im Speicher suchen, diese Lücke mit dem Inhalt füllen, Fragmentierung möglichst vermeiden, usw... .

Entsprechend darf new und delete in echtzeitkritischem Code nicht verwendet werden. Besser ist die „einmalige“ Allokation aller echtzeitkritischen Objekte zum Programmstart. Die meisten Allokatoren sperren darüber hinaus die Interrupts. Wenn eine minimale Interrupt-Latenz gefordert ist, müssen daher selbst nicht echtzeitkritische Programmteile ihren Speicher vor Beginn des echtzeitkritischen Betriebs allozieren.

4. Verwendung der Standard Template Library (STL)

Die STL ist eine Template Bibliothek mit Schwerpunkt auf Algorithmen und Datenstrukturen. Sie ist Teil des aktuellen C++ Sprachstandards. Die STL benötigt hochgradig das zuvor beschriebene dynamische Speichermanagement. Insbesondere Container sollten in echtzeitkritischem Code nicht verwendet werden.

5. C-Casts vs. C++-Casts

Für die Umwandlung eine Types existieren in C++ unterschiedliche Cast-Funktionen. Der „reinterpret_cast<>“ verhält sich zu dem C-Cast identisch hinsichtlich Funktion und Rechenzeit. Der „static_cast<>“ hat die gleiche Rechenzeit, führt aber zur Compile-Zeit eine Prüfung durch.

Er lässt sich nur für verwandte Typen oder fundamentale Typen benutzen. Auf den „dynamic_cast<>“ sollte man aus Rechenzeit, wie auch aus Speicherverbrauchs Gründen verzichten.

Zusammenfassung

C++ ist für die heutigen Anforderungen der Entwicklung wesentlich besser geeignet als C. Die Aussage, dass der Einsatz von C++ hierbei auf Kosten der Performance geht, konnte nicht bestätigt werden.

Vielmehr ist es so, dass beide Sprachen sich in einem vergleichbaren Rahmen bewegen. Unter Berücksichtigung elementarer Regeln, wie z.B., dass Vererbung in rechenzeitkritischen Code nur sehr begrenzt eingesetzt werden sollte, kann C++ für Echtzeitanwendungen bedenkenlos eingesetzt werden.

Literatur

[1] ARM, ARM Cortex-M3 Processor Technical Reference Manual, Revision r2p1, http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.100165_0201_00_en/index.html

[2] ARM, Cortex-M3 Devices Generic User Guide, http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0552a/index.html

[3] ISO/IEC TR 18015:2006(E), February 2006, Technical Report on C++ Performance

[4] Scott Meyers, Effective C++ in an Embedded Environment, Presentation Materials


* Dr.- Ing. Christian Gröling beschäftigte sich während seiner Promotion an der TU-Braunschweig mit embedded Software mit Fokus auf die elektrische Antriebstechnik. Diese Themenschwerpunkte baute er während seiner nachfolgenden Tätigkeit als Softwareentwickler bei der Firma LTi Drives GmbH, sowie anschließend bei Festo AG & Co. KG weiter aus. Über mehrere Jahre konnte er so Erfahrung in der embedded Softwareentwicklung insbesondere im Bereich kostensensitiver Applikationen sammeln.

(ID:44600617)