Schnellere Build Zeiten
Schnellere Build-Zeiten mit diesen einfachen Tricks⌗
- Extra-Trick, der das Leben noch leichter macht! #void*
Die meisten C/C++-Programmierer kennen es: Aus Versehen hat man eine der zentralen Header-Dateien geändert und ärgern sich jetzt darüber, dass der Build mehr als eine halbe Stunde dauern wird. Grob verärgert über den eigenen Fehler schnappt man sich seinen Lieblingskollegen und verzieht sich mit ihm in die Kaffeeküche, um mit ihm über den Code zu lästern. Doch es kam anders.
Inklusion ist gut, aber nicht im Code⌗
In dieser emotionalen Situation wollte ich eigentlich nur Dampf ablassen. Selbstverständlich war mir bewusst, dass ich selbst schuld daran war – sowohl an der Struktur der #include
s, als auch an der Dateiänderung, die mir gerade eine Zwangspause aufoktroyierte. Mit meinem Lieblingskollegen Mattias[1] hatte ich den falschen Kollegen für die Kaffeepause erwischt. Checklistenartig fragte er einen konstruktiven Vorschlag nach dem anderen ab. Mir fiel zu jedem einzelnen Punkt nur noch eine Antwort ein: Da hab‘ ich nicht dran gedacht – warum bin ich so dumm?
#ifdefs helfen aus der #include-Hölle⌗
Mattias[1] fragte zunächst, ob ich konsequent die mehrfache Inklusion von Headern unterbunden habe. Selbstverständlich haben in meinem Code überall die #pragma once
gefehlt. Altmodisch, aber standardkonform lässt sich das auch mit einer Kombination aus #ifndef
und #define
umsetzen.
// h
#include <AMCE_base_types.h>
#include <AMCE_conditionals.h>
#include <AMCE_misra_workarounds.h>
#ifndef TIME_H
#define TIME_H
//[…]
#endif /* !TIME_H */
Aus Versehen waren die #includes
innerhalb der Header nicht von diesem Guard erfasst. Die referenzierten Header-Dateien wurden so mehrfach inkludiert. Selbst wenn in den referenzierten Headern das #ifndef
implementiert war, führte das Kompilieren zu weiteren kaskadierten unnötigen Dateizugriffen und zusätzlichem Parsing.
Zu viele exponierte Typen in den Headern⌗
Das nächste Problem: Die privaten Attribute müssen nicht notwendigerweise im Header exponiert werden. Statt die privaten Attribute zu exponieren, vorwärts-deklarieren wir ein privates Objekt:
// h
class pimpl;
class MySecretClass {
public:
MySecretClass();
private:
pimpl* _p;
};
Das private Objekt wird dann nur in der cpp-Datei ausimplementiert. Die verwendeten Typen und deren Includes werden nur noch für diese eine cpp-Datei nötig:
// cpp
#include <SecretShare.h>
class pimpl {
public:
SecretShare* aLittleSecret;
};
MySecretClass::MySecretClass() {
_p = new pimpl();
_p-> aLittleSecret = new SecretShare(42);
}
Mein Einwand, dass man sich durch die Pointer nur Scherereien einhandle und der Code langsamer und unsicher würde, entgegnete Mattias[1], es sei ja der Kunde, der sich später mit den vielen Crashes herumschlagen müsse. Hauptsache wir beiden hätten unsere Ruhe im Büro.
Weg mit den Includes für Pointer⌗
Mattias[1] hatte aber noch mehr Tipps parat: Sobald in einer Header-Datei ein Typ nur als Pointer verwendet wird, kümmert es den Compiler herzlich wenig, wie dieser Typ genau aussieht. Der Compiler reserviert immer die gleiche Größe eines Pointers beliebigen Typs.
In einem Header wie diesem:
// h
#include <Time.h>
#include <SecretShare.h>
#include <LogMessage.h>
Log {
public:
Log.Warn(Time* t, SecretShare* s, LogMessage* msg)
Log.Error(Time* t, SecretShare* s, LogMessage* msg)
}
braucht der Compiler folglich auch nicht wissen, wie der Typ konkret aussieht. Wir ersetzen die #include
-Direktiven durch Vorwärtsdeklarationen:
// h
class Time;
class SecretShare,
class LogMessage;
Durch diese Maßnahme sparen wir nicht nur die Dateizugriffe auf die ganze Kaskade der Include-Files, sondern auch deren Parsing. Compiler sind erschreckend ignorant. Wir hatten ja schon mehrfach auf die Gefahren im Umgang mit Pointern hingewiesen.
Was ich nicht auf dem Schirm hatte, war, dass das ja auch für die cpp-Dateien gilt. Clean-Code-konformer Code schiebt heute lediglich verpointerte Objekte von einem Methodenparameter in andere Methoden, ohne die Objekte kennen zu müssen. Den Compiler kümmert es auch hier nicht, was da hin- und hergereicht wird. Am Rande sei nur darauf hingewiesen, dass in Layered-Designs hier womöglich gegen ein striktes Layering verstoßen wird.
Weg mit den Typen!⌗
In einem älteren Blog-Beitrag[2] haben wir schon einmal Hinweise gegeben, wie man C++-Code sowohl leichter lesbar als auch universeller gestalten kann. Die oben genannten Tricks helfen zwar, die Build-Zeit im konkreten Fall von 40 Minuten auf 3 Minuten zu verkürzen. Doch in Sachen Lesbarkeit ist niemandem geholfen, wenn dutzende Zeilen #include
-Anweisungen durch ebenso viele Vorwärtsdeklarationen ersetzt werden – der Code bleibt aufgebläht.
Nachdem es dem Compiler sowieso egal ist, was der Code da herumreichen soll, und Pointer sowieso immer die gleiche Größe haben, greifen wir einfach konsequent auf void*
als Typ zurück. Mit void*
entfallen auch alle Vorwärtsdeklarationen und der Code ist endlich aufgeräumt.
// h
/* no #includes – speed up build - LOL */
Log {
public:
Warn(void* time, void* secret, void* msg)
Error(void* time, void* secret, void* msg)
}
Und was ist mit Methodenüberladung?⌗
Mancher Anhänger der Objektorientierung wird nun einwenden, dass er nun keine Methodenüberladung anhand von Parametern mehr machen kann. Nachdem der Compiler Objektorientierung genauso wenig versteht wie die meisten Programmierer, können wir die Typentscheidung auch direkt in die Methoden verschieben und die verschiedenen Implementierungen auch noch in das PIMPL-Objekt verschieben, um möglichst wenig der API sichtbar zu machen.
// cpp
#include <Time.h>
#include <DateTime.h>
//[… implementation of PIMPL…]
void* Log::Warn(void* time, void* secret, void* msg) {
return
dynamic_cast<Time*>(time) != nullptr
? _p->Warn((Time*)time, secret, msg)
: dynamic_cast<DateTime*>(time) != nullptr
? _p->Warn((DateTime*)time, secret, msg)
: null;
}
Fazit⌗
Die ganze Welt der Entwickler redet über Agile, SAFe, Clean Code, Gehaltsmaximierung und Sabaticals. Gleichzeitig wird in dieser unverbindlichen Welt die Compile-Pause als gern genutztes Instrument der Entspannung am Arbeitsplatz sowie als Ausrede im StandUp genutzt. Viele Entwickler arbeiten heute ohnehin empirisch, insbesondere die fachfremden Quereinsteiger aus der E-Technik, Medizintechnik, Physik, Wirtschaftsinformatik oder Mathematik. Sie probieren einfach so lange aus, bis der Code irgendwie funktioniert, ohne den Code verstanden zu haben. Die Produktivität geht dabei gegen Null, während keinem der Beteiligten bewusst ist, dass die Geduld der technikfernen Geldgeber nicht unendlich ist.
Nach dem konsequenten Aufräumen der Include-Hierarchie und flächendeckender Umstellung auf void*
-Pointer können auch empirische Programmierer dank kurzer Build-Zeiten wieder ein Mindestmaß an Produktivität erreichen. Der erfahrene Informatiker hingegen weiß ohnehin, was der Code tut. Compiliert wird also nur ein einziges Mal zum Release. Dabei ist es dann egal, wie lang der Build dauert.
[1] Name geändert zum Schutz des beteiligten Protagonisten.
[2] https://iccb.dev/posts/leicht_lesbar_cpp_bereit/