Mythen der Blog-Leser
Mit welchen weiteren Mythen beim Einsatz von C++ haben nun die Leser von heise Developer zu kämpfen? Rainer Grimm fasst die Diskussion im Forum zu seinem Blog zusammen.
- Rainer Grimm
Ich war sehr neugierig, welche weiteren Mythen mir meine Leser schicken wĂĽrden. Insbesondere die Leser auf diesem Blog waren ziemlich aktiv. Ich erhielt ein paar E-Mails und beobachtete eine lebhafte Diskussion auf heise Developer.
Bevo ich zu den mir zugeschickten Mythen schreibe, möchte ich zuerst die letzte Regel der C++ Core Guidelines vorstellen. Hier ist der letzte Mythos:
NR.7: Don’t: Make all data members protected
Daten, die als "protected" deklariert sind, machen dein Programm anspruchsvoller und fehleranfälliger. Falls in der Basisklasse protected-Daten zum Einsatz kommen, lässt sich nicht mehr über abgeleitete Klassen in Isolation nachdenken. Damit brichst du die Kapselung. Du musst dir immer Gedanken zu ganzen Klassenhierarchien machen.
Das bedeutet, diese drei Fragen mĂĽssen in der Regel beantwortet werden.
- Muss ich einen Konstruktor implementieren, um das protected-Datum richtig zu initialisieren?
- Welchen Wert besitzen das protected-Datum, wenn ich es verwende?
- Welche Funktionalität wird in Mitleidenschaft gezogen, wenn ich das protected-Datum verändere?
Natürlich wird die Beantwortung dieser Fragen umso anspruchsvoller, je mehr die Klassenhierarchie in die Tiefe wächst.
Streng genommen ist ein protected-Datum eine globale Variable in der Klassenhierarchie. Das erste Gebot der Softwareentwicklung lautet aber: Vermeide globale Daten.
Nun zu den Mythen:
In C++ geschriebene Programme brauchen mehr Speicher und CPU-Zeit als in C geschriebenen Programme (Gunter Königsmann)
Ehrlich gesagt, gegen diesen Mythos zu argumentieren, ist ziemlich schwierig und im Allgemeinen nicht möglich, wenn es um die C++-Bibliothek geht. Dies ist der Grund, dass ich nur ein paar Beobachtungen am Ende dieses Kapitels präsentiere. Zuerst einmal kommen aber die harten Fakten auf den Tisch. Der "Technical Report on C++ Performance" hilft mir dabei sehr.
Technical report on C++ performance
2006 verfasst die Working Group WG 21 das Dokument ISO/IEC TR 18015. Der Titel hört sich sehr sperrig an. Tatsächlich ist das Dokument die erste Wahl, wenn es darum geht, die Performanz von C++ zu analysieren. Diesen folgenden Aspekt bringt das Dokument direkt auf den Punkt:
- to give the reader a model of time and space overheads implied by use of various C++ language and library features,
- to debunk widespread myths about performance problems,
- to present techniques for use of C++ in applications where performance matters, and
- to present techniques for implementing C++ Standard language and library facilties to yield efficient code.
Verfasser des gut 200-seitigen Reports sind so bekannte Namen wie Dave Abrahams, Howard Hinnand, Dietmar KĂĽhl, Dan Saks, Bill Seymour, Bjarne Stroustrup und Detlef Vollmann.
In dem Dokument selbst geht es um die C++-Sprachmerkmale, deren Kosten und Verwendung, dem Erzeugen effizienter Bibliotheken in C++, der Nutzung von C++ in Embedded-Systemen und einer Schnittstelle in C++, um mit Hardware zu kommunizieren. Insbesondere auf die C++-Sprachmerkmale, deren Kosten und Verwendung will ich genauer eingehen.
C++-Sprachmerkmale, Kosten und Verwendung
Bei ihrer Analyse greifen die Autoren auf drei Computerarchitekturen mit fĂĽnf verschiedenen C++-Compilern zurĂĽck. Die Compiler rufen sie mit unterschiedlichen Optimisierungsstufen auf. Die Ergebnisse, die ich deutlich vereinfachend darstelle, sind sehr aufschlussreich.
Namensräume
- Besitzen keinen signifikanten Einfluss auf die Größe des Programms oder sein Zeitverhalten.
Typkonvertierungs-Operatoren
- Die C++-Casts const_cast, static_cast und reinterpret_cast unterscheiden sich weder in Größe noch Zeitverhalten von ihrem C-Pendant.
- Der zur Laufzeit ausgefĂĽhrte dynamic_cast besitzt einigen Kosten. (Anmerkung: Diese Konvertierung besitzt kein Pendant in C.)
Klasse
- Eine Klasse (class) ohne virtuelle Funktionen ist genauso groĂź wie eine Struktur (struct).
- Eine Klasse mit virtuellen Funktionen besitzt die Kosten eines Zeigers und einer virtuellen Funktionstabelle (virtual function table). Dies sind typischerweise 2 bis 4 Byte.
Funktionsaufrufe auf Objekten
- Der Aufruf einer nichtvirtuellen, nichtstatischen, nicht-inline Funktion ist genauso teuer wie der Aufruf einer freien Funktion.
- Der Aufruf einer virtuellen Funktion ist so teuer wie der Aufruf einer freien Funktion mithilfe eines Zeigers, der in einem Array gespeichert ist.
- Virtuelle Funktionen eines Klasse-Templates können Kosten verursachen.
- Das Inlining von Funktionen bringt einen signifikanten Vorteil, reicht aber noch nicht ganz an die Performanz von C-Makros heran.
Mehrfachvererbung
- Kann Zeit- und/oder Größen-Kosten implizieren.
- Virtuelle Basisklassen besitzen gegenüber nichtvirtuellen Basisklassen zusätzlichen Kosten.
Run-Time Type Information (RTTI)
- Pro Klasse werden typischerweise zusätzlich 40 Bytes benötigt.
- Der
typeid
-Aufruf ist relativ langsam. Dies scheint aber ein Problem der GĂĽte der Implementierung zu sein. - Die Konvertierung zur Laufzeit mit
dynamic_cast
ist langsam. Der Grund scheint laut dem Report vor allem auch in der Qualität der Implementierung zu liegen.
Ausnahmebehandlung
- Zwei Strategien für den Umgang mit Ausnahmen haben sich etabliert. Das ist die Code- und die Tabellen-Strategie. Während bei der Code-Strategie zur Laufzeit zusätzliche Datenstrukturen für den Ausführungskontext verwaltet und verschoben werden müssen, wird bei der Tabellen-Strategie der Ausführungskontext in einer Tabelle vorgehalten.
- Die Code-Strategie besitzt ein Größen-Overhead für den Stack und die Laufzeit. Der Zeit-Overhead ist ca. 6 Prozent, selbst wenn die Ausnahme nicht auftritt.
- Die Tabellen-Strategie besitzt weder Kosten in der Programmgröße noch in der Laufzeit (Anmerkung: Für die Laufzeit gilt dies nur, wenn keine Ausnahme auftrat). Dafür ist diese Strategie schwieriger zu implementieren.
Templates
- Für jedes Template-Argument wird ein neues Funktions- oder Klassen-Template erzeugt. Naiver Umgang mit Templates kann daher zu einem deutlichen Anstieg der Codegröße führen. Moderne C++-Compiler können die Anzahl der Template-Instanziierung deutlich reduzieren. In der Standard Template Library wird teilweise oder vollständige Template-Spezialisierung angewandt, um die Anzahl der Template-Instanziierungen zu reduzieren.
Die genauen Details, die exakten Zahlen und alle weiteren Punkte lassen sich in dem Report TR18015.pdf schön nachlesen.
MISRA C++
MISRA C++ (Motor Industry Software Reliability Association) stellt Regeln für das Schreiben von Software in sicherheitskritischen System auf. MISRA C++ wurde ursprünglich für die Automobilindustrie entwickelt, ist aber mittlerweile Industriestandard im Flugzeugbau, Militär und auch im Medizinbereich.
MISRA C++ stellt heraus, warum die Bedeutung von C++ in sicherheitskritischen System immer weiter zunimmt (1.1 The use of C++ in critical systems) :
- C++ gives good support for high-speed, low-level, input/output operations, which are essential to many embedded systems.
- The increased complexity of applications make the use of a high-level language more appropriate than assembly language.
- C++ compilers generate code with similar size and RAM requirements to those of C.
Ein bitterer Wermutstropfen bleibt aber. MISRA C++ basiert auf klassischem C++. Dies ist vereinfachend gesprochen C++98. Modernes C++ hat aber deutlich mehr zu bieten fĂĽr Embedded-Systeme. Diese Beobachtung gilt leider auch fĂĽr den bereits vorgestellten Report Technical report on C++ performance.
Ohne ein paar Beobachtungen zu modernem C++ kann ich diesen Artikel nicht beenden. Modernes C++ ist ein Begriff, der meist fĂĽr die C++-Standards C++11, C++14 und C++17 eingesetzt wird.
Meine Beobachtungen
Die Schwierigkeit, die Features der C++ Bibliothek mit ihrem C Pendant zu vergleichen, besteht vor allem darin, dass ein Vergleich äquivalente Datenstrukturen, Algorithmen oder Programme voraussetzt. Das heißt, damit lässt sich ein C-String mit einem C++-String nicht vergleichen, denn die C++-Variante verwaltet zum Beispiel automatisch ihren Speicher. Dieselbe Beobachtung lässt sich auf einen C-Array und die Container der Standard Template Library anwenden. Diese Punkte halten mich aber nicht davon ab, ein paar Punkte aufzuzählen. Wenn du mehr Details wissen willst, lese den hinterlegten Artikel.
- Ein std::array ist per Design so schnell und benötigt so wenig Speicher wie ein C-Array. Zusätzlich kennt es seine Länge: std::vector und std::array sind die erste Wahl.
- Ein std::unique_ptr ist per Design so schnell und benötigt so wenig Speicher wie ein nackter Zeiger. Klar, der std::unique_ptr ist auch sicher per Design: Speicher- und Performanzoverhead von Smart Pointern.
- Die Type-Traits Bibliothek erlaubt es Code zu schreiben, der sich selbst optimiert: Type-Traits- Performanz zählt.
- Dank constexpr lassen sich aufwendige Berechnungen auf die Compilezeit verschieben: C++ Core Guidelines: Programmierung zur Compilezeit mit constexpr.
- Move-Semantik und Perfect Fowarding erlauben es, teures und unsicheres Kopieren durch billiges und sicheres Verschieben zu ersetzen. Vereinfachend gesprochen kann eine Copy-Operation im Gegensatz zu einer Move-Operation fehlschlagen: Copy- versus Move-Semantik: Ein paar Zahlen und Perfect Forwarding.
Wie geht's weiter?
Ich hätte nicht gedacht, dass ich fast einen ganzen Artikel benötige, um gegen die Mythen zur Performanz und zum Speicherverbrauch von C++ zu argumentieren. Du kannst dir vorstellen, dass ich weitere Mythen zugeschickt bekommen habe. Auf diese werde ich in meinem nächsten Artikel eingehen. Falls du noch einen Mythos kennst, kannst du ihn mir auch gerne schicken: rainer.grimm@modernescpp.de. ()