C vs C++

Aus der Mikrocontroller.net Artikelsammlung, mit Beiträgen verschiedener Autoren (siehe Versionsgeschichte)
Wechseln zu: Navigation, Suche

Speziell bei der Verwendung höherer Programmiersprachen (HLLs) im Bereich der Programmierung von Mikrocontrollern taucht häufig die Frage auf, ob eine Sprache wie C++ für "kleinere" Mikroprozessoren nicht einen zu großen Overhead darstelle und ob man nicht besser bei C (oder gar Assembler) bleiben solle.

Beziehung zwischen C und C++

Ursprünglich entstand C++ als Erweiterung zu C (mit minimalen Ausnahmen - Leitlinie: "As close to C as possible, but no closer"). Damit ist C als Teilmenge in C++ enthalten und jedes C-Programm ist zugleich ein C++-Programm, denn es kann auch mit einem C++-Compiler übersetzt werden (nicht aber umgekehrt!). Da ein weiterer Grundsatz bei der C++-Entwicklung lautete "You only pay for what you use", entsteht in einem Programm, das ausschließlich C-Features benutzt aber mit einem C++-Compiler übersetzt wird, kein prinzipiell bedingter Overhead.

So weit die Theorie.

Der Artikel wurde ursprünglich im Jahr 2004 verfasst. Einige der im weiteren Verlauf genannten Punkte, insbesondere bzgl. Optimierungen, Performance und unnötige Funktionsaufrufe, treffen auf aktuelle Compiler nicht mehr zu. Es gibt verschiedene (Web-basierte) Tools, um das Compiler-Ergebnis von C++ Code zu untersuchen:

  • Compiler Explorer (https://godbolt.org) - Hilft zu zeigen, wie der vom C++ Compiler erzeugt Assembler Code aussieht.
  • C++ Insights (https://cppinsights.io) - Hiermit lässt sich nachvollziehen, was ein C++ Konstrukt auf Code Ebene erzielt.

C++-Performance-Fußangeln

Allerdings waren die Zielsysteme bei der Weiterentwicklung von C++ in den letzten 20 Jahren in der Regel PCs und leistungsfähige Workstations, was in der Praxis zwei wichtige Konsequenzen hatte:

  • Die Sprachsyntax von C++ verschleiert mehr als die von C, womit man sich in einem Programm möglicherweise einen hohen, in der Programmierung von Mikrocontrollern unerwünschten Overhead ohne konkreten Nutzen einhandelt.
  • Die tatsächlichen Implementierungen von C++ bemühen sich weniger als die von C, ausführbare Programme (Executables) in der sparsamsten möglichen Form zu erzeugen.

Fußangel-Beispiel 1

Mitunter werden beim Einsatz von C++ Programmierrichtlinien angewendet, die im Bereich der Mikrocontrollern-Programmierung unsinnig sind oder zumindest nach anderen Kriterien bewertet werden müssen als im PC- und Workstation-Umfeld. So führen etwa "vorsorglich angelegte", aber leere Konstruktoren und Destruktoren in der Regel zu vielen Unterprogramm-Sprungbefehlen, an deren Ziel sofort wieder ein Rücksprung steht.

Oder es werden - dem Beispiel von Java oder bestimmten Entwurfsmustern folgend - Objekte in jedem Fall dynamisch angelegt, was das Einbinden komplexer Unterstützungsroutinen für die Speicherverwaltung auslöst und in keinem Fall zu Ergebnissen führt, deren Performance vergleichbar zu einfachen Variablen auf dem Stack oder an festen Adressen ist.

Fußangel-Beispiel 2

Manchmal wird ein einfaches "hello, world"-Programm in C und eines in C++ auf einem PC kompiliert und die Größe des erzeugten Executables als Kriterium für den Ressourcen-Bedarf der jeweiligen Programmiersprache herangezogen. Dieser Vergleich kann sehr in die Irre führen, da die typischen Ausgabe-Anweisungen der Sprachen C und C++ von Bibliotheksfunktionen abgewickelt werden, die aber mit Sicherheit bei der Kompilierung für einen Mikrocontroller ganz anders aussehen (wenn sie dort überhaupt vorhanden sind).

Bei der Strukturierung von unterstützenden Bibliotheksfunktionen wird im PC- und Workstationbereich kaum darauf geachtet, ob einem Minimal-Programm möglicherweise unnötiger Overhead mit hinzugebunden wird. So könnte beispielsweise - obwohl nur eine einfache Zeichenkette ausgegeben wird - durch eine Ausgabeanweisung die Unterstützung für die Ausgabe aller Datentypen inklusive Gleitpunktzahlen angesprochen werden, dies könnte wiederum das Hinzubinden der mathematischen Bibliothek auslösen und - bei Kompilierung für einen Prozessor ohne Hardware-Support für Gleitpunkt-Operationen - einer weiteren Bibliothek zu deren Emulation. Das alles, wie gesagt, obwohl überhaupt keine Gleitpunktzahl in der konkreten Ausgabe vorkommt.

Vererbung und Aggregation

Beide Mechanismen sind aus Sicht der Umsetzung als geschachtelte Strukturen zu verstehen. Eventuelle kleinere Performance-Einbußen könnten durch die fehlende Möglichkeit der Register-Optimierung entstehen, die ein Compiler bei mehreren, nicht zu einer Struktur zusammengefassten Variablen prinzipiell hat. Allerdings sind die Vor- und Nachteile hier in C und C++ die selben, und insofern ist das Thema für einen Vergleich "C vs. C++" nicht relevant.

In größeren Programmen überwiegt in aller Regel der Vorteil der besseren Strukturierung durch die Zusammenfassung von Einzelvariablen, insbesondere wenn auch die in C++ vorhandenen Mechanismen zum Zugriffsschutz eingesetzt werden. In kleinen Programmabschnitten mit extrem kritischer Performance kann ggf. eine "Hand-Optimierung" und Auflösung von Strukturen in Einzelvariablen erfolgen (oder gleich eine Re-Implementierung in Assembler).

Virtuelle Methoden

Virtuelle Methoden sind im Grunde genommen Funktionszeiger und insofern überall dort sinnvoll, wo man in einem C-Programm zur Lösung des Problems einen Funktionszeiger eingesetzt hätte (oder in Assembler eine Einsprung-Adresse in Maschinencode hinterlegt oder übergeben hätte).

Werden Methoden in C++ unnötigerweise "virtual" deklariert (mit anderen Worten: steht die Einsprungadresse immer fest und hätte insofern auch der Compiler oder Linker sie bestimmen können), so ist der Overhead natürlich unnötig - aber auch leicht vermeidbar, indem man solche Methoden eben nicht als "virtual" deklariert.

Dort, wo sinnvoll, also wo virtuelle Methoden tatsächlich ein Problem lösen helfen, sind sie in der Regel eleganter und programmiertechnisch einfacher zu handhaben als explizite Funktionszeiger.

Interfaces (Abstrakte Basisklassen)

Abstrakte Basisklassen sind im Grunde genommen Bündel von Funktionszeiger und ebenso wie virtuelle Methoden genau dort sinnvoll, wo sie ein Problem lösen helfen. Hierzu ein Beispiel: Ein Embedded-System hat eine Verbindung zu einem Host-Rechner zwecks Datenaustausch. Es besteht entweder ein LAN-Zugang oder an der seriellen Schnittstelle hängt ein Modem. Möglicherweise kommt es im laufenden Betrieb auch zu einer Umkonfiguration oder die Hostverbindung fällt komplett aus, dann sollen die Daten in einem lokalen, nicht-flüchtigen Speicher abgelegt werden.

Damit die Applikation nicht alle Eventualitäten an zahlreichen Stellen des Programms verteilt berücksichtigen muss, abstrahiert man den Datenaustausch z. B.

  • Verbindung initiieren
  • Daten übergeben
  • Übernahme bestätigen
  • Verbindung abbauen

in einem Interface. Jede der genannten Operationen entspricht dabei einem Zeiger auf ein entsprechendes Unterprogramm (also dessen Einsprungadresse). Tatsächlich implementiert werden diese Unterprogramme mehrfach (einmal für LAN, einmal für Modem, einmal für lokalen Speicher) und der eigentlichen Applikation wird - mehr oder weniger transparent - einfach das passende Funktionszeigerbündel übergeben, je nachdem, welche Art von Host-Verbindung derzeit möglich ist.

Auch hier ist es wieder so, dass ein Interface - realisiert durch eine abstrakte Basisklasse - im Vergleich zu expliziten Funktionszeigern eleganter und programmiertechnisch einfacher ausfällt (sofern man einmal das Prinzip verstanden hat). Ebenso gilt, dass abstrakte Basisklassen zu unnötigem Overhead führen, falls man sie "nur mal so" einsetzt, also ohne dass sie, wie oben dargestellt, ein konkretes Problem lösen.

Exceptions (Ausnahmebehandlung)

Mit Exceptions lässt sich ein "alternativer Kontrollfluss" in einem Programm definieren, vorzugsweise zur Handhabung von Fehlersituationen oder anderer "außergewöhnlicher Ereignisse". Mit der Einführung von Exceptions musste in C++ aber auch die Leitlinie "You only pay for what you use" verletzt werden. Das heißt es entsteht stets etwas Overhead, wenn der C++-Compiler Exceptions unterstützt, auch für Programme, die Execptions überhaupt nicht benutzen. Aus diesem Grund lassen sich Execptions über Kommandozeilen-Argumente des Compilers oder in den Projekt-Optionen einer Tool-Chain häufig deaktivieren.

Laufzeit-Typinformation

Laufzeit-Typinformation (auch Runtime Type Information oder RTTI genannt), verursacht unter Umständen ebenfalls einen geringen Overhead in C++, auch wenn ein Programm RTTI nicht benutzt. Insofern gilt wie für Exceptions, dass man ggf. nach einer Möglichkeit suchen sollte, RTTI ganz abzuschalten, wenn man das Feature nicht braucht. Auf der anderen Seite ist der RTTI-Overhead im Vergleich zu Exceptions sehr gering und entsteht vor allem im Hinblick auf ein wenig zusätzlich benötigten Speicherplatz, nicht in Form unnötig ausgeführter Anweisungen. (Nach Abschalten der RTTI-Unterstützung darf man also kein "schnelleres" Programm erwarten.)

Templates (Schablonen)

Templates sind ein Mechanismus, der zwischen Implementierungs- und Kompilierungs-Zeitpunkt greift. Im Grunde genommen steht mit einer C++-Template "ein bisschen Programmcode" stellvertretend für "möglicherweise sehr viel Programmcode", der aber noch in einem bestimmten Aspekt - einem Datentyp oder eine Konstante - variieren kann. Der allgemeine Teil (Template) wird nur einmalig implementiert und später in mehr oder weniger vielen Varianten kompiliert. (Das am ehesten vergleichbare äquivalent zu C++-Templates sind in C die Präprozessor-Makros.)

Unter diesem Aspekt betrachtet sind Templates keine Verschwendung (wie manchmal behauptet) sondern sehr sparsam und ideal für Mikrocontroller, da sie im Vergleich zu einer Bibliotheksfunktionen, die quasi "auf Vorrat" viele Varianten berücksichtigen, von denen konkret aber nicht alle genutzt werden, einem Programm nur das hinzufügt, was wirklich benötigt wird.

Auf der anderen Seite sind Templates ein neueres Feature von C++ zu dem bis vor wenigen Jahren auf Seite der Compiler-Lieferanten kaum Erfahrung hinsichtlich der optimalen Umsetzung bestand. Auch in der Fachliteratur werden Templates meist mit viel geringerer Tiefe und weniger Praxisbezug behandelt als diejenigen Features, die schon sehr lange von C++ unterstützt werden (Vererbung usw.). Eine generelle Empfehlung, Templates bei der Verwendung von C++ im Bereich der Mikrocontroller-Programmierung nicht einzusetzen, ist daraus aber keinesfalls abzuleiten - ganz im Gegenteil: Zur Lösung vieler Probleme sind Templates besser geeignet als etwa Präprozessor-Makros oder gar Copy-und-Paste-Programmierung.