Ein Angebot von

C++ programmieren: Wer hat Angst vor dem bösen „++“?

| Autor / Redakteur: Matthias Bauer* / Christine Kremser

In der Embedded-Entwicklung wird das "klassische" C gegenüber C++ oft noch vorgezogen, da die "++"-Programmiersprache als vergleichsweise ressourcenhungrig betrachtet wird. Aber ist diese Annahme grundlegend richtig?
In der Embedded-Entwicklung wird das "klassische" C gegenüber C++ oft noch vorgezogen, da die "++"-Programmiersprache als vergleichsweise ressourcenhungrig betrachtet wird. Aber ist diese Annahme grundlegend richtig? (Bild: gemeinfrei/Pixabay / CC0)

Manche Vorurteile halten sich hartnäckig. Zum Beispiel dieses: C++ ist für extrem ressourcenarme Systeme nicht geeignet. Dabei stimmt das schlichtweg nicht! Vielmehr bringt der Einsatz der richtigen C++-Sprachmittel gerade für Systeme mit extrem begrenzten Ressourcen unschätzbare Vorteile.

Indem der Compiler als Codegenerator genutzt wird, lassen sich aus generischen, flexibel konfigurierbaren Softwarekomponenten Programme realisieren, die keinerlei Runtime-Overhead gegenüber einer Speziallösung enthalten. Das ist gerade für Anwendungen interessant, die auf Plattformen mit sehr begrenzten Ressourcen (z. B. Codespeicher, RAM, Rechengeschwindigkeit, Energie) integriert werden.

C++: Steht das „++“ für mehr Ressourcenbedarf?

Bei extrem oberflächlicher Betrachtung könnte man tatsächlich zu dem Schluss kommen, dass mit C++ entwickelte Software größeren Binärcode erzeugt, als das Pendant in C.

Baut man zum Beispiel ein einfaches Hello World-Programm in C und C++ mit der Entwicklungsumgebung Keil uVision für ein Cortex-M3-Target, dann sehen sich C++-Skeptiker zunächst bestätigt: Während das in C geschriebene Programm Codespeicher im niedrigen einstelligen Kilobyte-Bereich einnimmt, belegt das C++-Programm mehr als 30 kByte (siehe Bilder 1 und 2 in der Bildergalerie).

Für den Mehrverbrauch an Codespeicher ist jedoch nicht die Programmiersprache C++ verantwortlich. Durch die Verwendung mancher Teile der Standard Template Library handelt man sich Bibliotheks-Code ein, der nicht für Embedded-Anwendungen optimiert ist und zum Teil Features von C++ nutzt, die man in Anwendungen mit extrem begrenzten Ressourcen besser ausklammern sollte (z. B. Exception-Handling).

Bei der Verwendung der STL auf Embedded-Targets ist demnach aus mehreren Gründen Vorsicht geboten. Zum Beispiel machen die Collection-Klassen per Default von dynamischer Speicherallokierung Gebrauch, was auf Systemen ohne virtuellem Speichermanagement früher oder später zu Problemen durch Speicherfragmentierung führt.

Die Programmiersprache C++ an sich kann dagegen bedenkenlos eingesetzt werden, ohne dass dadurch mehr wertvolle Systemressourcen gebunden werden als durch den Einsatz von C. Bei C++ bezahlt man nämlich nur für die Features mit der Währung „Ressourcen“, die man auch tatsächlich verwendet. Dazu sollte man natürlich genau wissen, welche Sprachfeatures ggf. Ressourcenmehrkosten verursachen.

Ein Überblick über Ressourcenkosten einiger C++Sprachfeatures kann der Tabelle 1 in der Bildergalerie entnommen werden.

Insbesondere Templates machen die Sprache C++ extrem interessant für Embedded-Anwendungen. Durch sie lässt sich der C++-Compiler als Codegenerator verwenden, der aus generischen und damit flexibel einsetzbaren Softwarekomponenten zur Compilezeit hocheffizienten Code erzeugt. Das Konzept der Codegenerierung lässt sich sogar bis auf Treiberebene gewinnbringend einsetzen. Dazu betrachten wir einen Treiber für Digitaleingänge, der z. B. bei jeder steigenden Pulsflanke aus dem Interrupt-Kontext eine Applikationsfunktion aufrufen soll.

Die ISR-Routine eines solchen Treibers besteht in der Regel aus zwei Teilen (siehe Bild 3):

  • 1. Ein hardwareabhängiger Teil prüft, ob der Interrupt durch den betreffenden Hardwarebaustein (in unserem Fall Input-Pin) ausgelöst wurde (falls mehrere Quellen den Interrupt auslösen können) und setzt den Interrupt ggf. zurück.
  • 2. Aufruf einer Callback-Routine, die nicht Teil des Treibers ist, sondern applikationsspezifische Funktionalität enthält.

Fallbeispiel: Ein Treiber mit ISR in C

Die ISRs von in C implementierten Treibern rufen die applikationsspezifischen Callbacks typischerweise über Funktionszeiger.

Die relevanten Codefragmente eines solchen Treibers können dem Bild 4 in der Bildergalerie entnommen werden.

Bei diesem Ansatz braucht die Callback-Routine dem Treiber erst zur Laufzeit bekannt gemacht werden, was oftmals überflüssige Flexibilität ermöglicht. In den meisten Fällen wird bereits zum Compile-Zeitpunkt festgelegt, welche Callback-Routine der Treiber rufen soll.

Derselbe Treiber mit C++

Realisiert man den Treiber mit der Programmiersprache C++, kann man auch völlig ohne Funktionszeiger beliebige Callback-Funktionen in den Treiber einhängen. Dazu nutzt man C++-Templates und das Konzept der sogenannten Template-Spezialisierung.

Ein in C++ realisierter Treiber kann demnach wie in Bild 5 abgebildet aussehen.

Dort wo der Treiber einen Callback ausführen möchte, ruft er einfach die statische Methode invokeCallback() des Klassentemplates TDriverIsrCallback auf. Durch Spezialisierung des Klassentemplates TDriverIsrCallback kann man nun für den jeweiligen Treiber festlegen, was in dieser statischen Methode getan werden soll, d. h. von dort aus die gewünschte applikationsspezifische Callback-Routine aufrufen. (siehe Bild 6 in der Bildergalerie)

Wenn der Compiler den Code der Treiberklasse parst muss ihm die Spezialisierung noch nicht zur Verfügung stehen. Die Spezialisierung kann demnach außerhalb des Treiber-Code-Moduls erfolgen, damit der Treiber generisch bleibt und keine Abhängigkeit zum Applikationscode hat. Als Bindeglied zwischen den Applikations-Modulen und Treibern dient das Code-Modul mit der gezeigten Templatespezialisierung.

Da der Compiler den Aufruf von invokeCallback() als Inline-Methode wegoptimiert, entsteht nach der Codegenerierung Binärcode als wäre die Callback-Routine direkt im Treiber aufgerufen worden, also wie in Bild 7 in der Bildergalerie gezeigt.

Diese Technik bringt folgende Vorteile gegenüber der Callback-Lösung per Funktionszeiger:

  • Weniger Datenspeicher, da kein Funktionszeiger gespeichert werden braucht.
  • Weniger Codespeicher, da die Adresse der Callback-Funktion im Lauf der Treiberinitialisierung nicht mittels setCallbackFct() gefüllt werden muss.
  • Der Compiler kann die Applikations-Callback-Funktionen sogar „inlinen“.

Der letzte Punkt ist besonders interessant: Für kurze Inline-Callback-Routinen platziert der Compiler deren Inhalt direkt in die Treiber-ISR, d. h. aus der ISR wird dann gar keine Funktion aufgerufen, d. h. das Sichern der Rücksprungadresse, Sprung und Rücksprung entfallen gänzlich. Das kann bei sehr häufig auftretenden Interrupts durchaus ins Gewicht fallen.

Die etwas gewöhnungsbedürftige Schreibweise, um Callback-Routinen mittels Templatespezialisierung einzuhängen, kann mittels eines Makros deutlich vereinfacht werden. Im Embedded-Software-Baukasten redBlocks, wo die hier vorgestellte Technik zum Einsatz kommt wird statt einer Template-Spezialisierung gemäß Bild 6 das Makro von Bild 8 in der Bildergalerie genutzt (der zweite Parameter DigitalInputA0:: CBK_ON_INPUT_CHANGED erlaubt die Auswahl des Callbacks, falls ein Treiber mehr als eine Callback-Routine besitzt ).

Die Quadratur des Kreises – wiederverwendbar und kein Overhead?

Der gezeigte Implementierungsansatz erlaubt eine saubere Trennung von Treibern in einen hardwareunabhängigen High-Level- und einen hardwareabhängigen Low-Level-Teil. Der High-Level-Teil (z. B. zuständig für die Datenpufferung in einem UART-Treiber) kann ohne jegliche Modifikationen zusammen mit unterschiedlichen Low-Level-Treibern auf verschiedenen Hardware-Targets zusammenarbeiten und ist damit portabel und wiederverwendbar. (siehe Bild 9)

Trotz der sauberen Modularisierung entsteht mit der in diesem Artikel vorgestellten Technik keinerlei Ressourcenoverhead. Beim Einsatz von Funktionszeigern hingegen wird durch die Aufteilung von Treibern in einen High-Level- und einen Low-Level-Teil eine unnötige Indirektion bei Callbacks eingeführt.

Zusammenfassung

Mit C++-Templates definiert der C++-Standard einen mächtigen Codegenerator, der ganz gezielt eingesetzt werden kann, um aus generisch wiederverwendbaren Softwarebausteinen hocheffizienten Code für ressourcenarme Embedded-Systeme zu erzeugen. Eine wartungsfreundliche, modulare Softwarearchitektur lässt sich so bis auf Treiberebene ohne zusätzliche Ressourcenkosten realisieren.

Die hier vorgestellte Technik kommt in der redBlocks-Komponentenbibliothek zum Einsatz und wird dort unter anderem dafür verwendet, um Embedded-Software in die SiL-Umgebung des redBlocks-WYSIWYG-Simulators zu integrieren und dort automatisiert testen zu können.

Mit Hilfe des redBlocks-Eval-Pakets (erhältlich via www.redblocks.de) kann die hier beschriebene Technik einfach nachvollzogen werden.

Objektorientierte Programmierung mit C

Objektorientierte Programmierung mit C

04.05.19 - Obwohl C keine objektorientierte Sprache ist, ist die objektorientierte Programmierung mit ihr durchaus machbar. Was ist möglich und wo stößt die Programmiersprache C an ihre Grenzen? lesen

Bugs und Defekte in Multitasking-Software eliminieren

Statische Analyse

Bugs und Defekte in Multitasking-Software eliminieren

28.06.17 - Werkzeuge für die statische Analyse spüren Multitasking-Fehler auf, die mit Softwaretests nur schwer gefunden werden. Diese Tools eignen sich auch für das riesige Spektrum an Fehlern, bei denen es zu Interaktionen zwischen mehreren Tasks sowie mit einem Echtzeitbetriebssystem (RTOS) kommt. lesen

* Matthias Bauer ist Geschäftsführer der redlogix Software & System Engineering GmbH. Er beschäftigt sich mit dem Einsatz moderner objektorientierter Entwicklungsmethoden für Embedded-Software auf Plattformen mit sehr begrenzten Ressourcen und begleitet Unternehmen beim Umstieg von C auf C++, sowie dem Einsatz des Software-Baukastens redBlocks.

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? Infos finden Sie unter www.mycontentfactory.de (ID: 44308562 / Implementierung)