Programmierer kennen das Problem mit der Wartezeit beim Kompilieren nur zu gut: Sie ist zu kurz für eine andere Tätigkeit und dauert zu lange, um sie zu ignorieren. Dieser Artikel stellt ein Verfahren vor, das die Kompilierzeiten drastisch reduziert.
Angenommen Sie entwickeln ein Projekt in C oder C++ mit 50 Sourcefiles ( .cpp) und 50 Includefiles ( .hpp). Außerdem haben Sie ein Makefile entweder von Hand oder über Automake und Autoconf oder auf andere Weise erstellt. Sie starten Make und das Projekt kompiliert komplett durch. Wenn Sie in einem Sourcefile irgendeine Subroutine ändern, kompiliert Make erwartungsgemäß lediglich das geänderte Sourcefile und linkt das Programm danach neu.
Nun korrigieren Sie jedoch in einem Headerfile einen Tippfehler in einem Kommentar. Unglücklicherweise benötigen 45 der 50 Sourcefiles dieses Headerfile. Daher wird Make alle 45 Files neu kompilieren. Sie schauen zu und wissen genau, dass das Ergebnis identisch mit der vorigen Kompilierung sein wird – aber Make kann das nicht wissen.
Für Make ist ein Projekt ein Graph aus Dateien und ihren Abhängigkeiten. Make sieht lediglich, dass alle 45 Files von dem Includefile abhängen und dass es geändert wurde. Ergo müssen alle neu kompiliert werden. Der Compilercache erkennt diese unnötigen Kompilierungen und holt die Ergebnisse aus dem Cache, was den gesamten Vorgang wesentlich beschleunigt.
Weggabelungen
Der Compilercache ist ein Bash-Skript, das statt des bisherigen Compilers aufgerufen wird. Es entscheidet, ob eine Kompilierung wirklich nötig oder ob das Ergebnis schon im Cache ist. Falls eine Kompilierung notwendig ist, wird der Compiler aufgerufen, falls nicht, wird das Ergebnis aus dem Cache kopiert.
Die Entscheidung, ob ein File neu kompiliert werden muss, hängt jedoch nicht wie bei Make davon ab, ob der Quelltext neuer ist als das zugehörige Objectfile, der Compilercache führt stattdessen eine Analyse des zu übersetzenden Quelltextes durch. Das funktioniert zwar langsamer als Make, erfordert dafür aber wesentlich weniger Neukompilierungen. Im Ergebnis ist mit Compilercache ein deutlicher Geschwindigkeitsvorteil zu erzielen.
Die Vorteile von Compilercache schildert die README-Datei des Pakets im Einzelnen [1]. Im Kern wird ein Quelltext nicht neu übersetzt, wenn lediglich Reformatierungen wie das Einfügen von Leerzeichen zur besseren Lesbarkeit vorgenommen oder Kommentare abgeändert oder hinzugefügt wurden.
Installation |
|
Holen Sie sich zunächst den Compilercache von [1]. Die Installation erfolgt als Benutzer und beeinträchtigt die anderen User nicht (eine systemweite Installation wird im README beschrieben). Tippen Sie dann:
DUMMY="compilercache-1.0.9"
CURDIR="$(pwd)"
tar xzvf ${DUMMY}.tar.gz
cd ${DUMMY}/src
./compile.sh
cd $HOME
echo COMPILERCACHEBINDIR=${CURDIR}/ ${DUMMY}/bin > .compilercacherc
Vergessen Sie nicht die Version 1.0.9 durch die jeweils aktuelle zu ersetzen. Um sicher zu sein, können Sie die Installationsanleitung auch aus dem derzeit aktuellen README lesen. Nun müssen Sie, um den Compilercache zu nutzen, noch Ihre PATH-Umgebungsvariable anpassen, von Hand oder per Login-Skript:
export PATH=${HOME}/compilercache- 1.0.9/bin:${PATH}
Ab jetzt verwenden Sie automatisch den Compilercache. Die vielen Konfigurationsmöglichkeiten und weitere Feinheiten lesen Sie im README, dessen aktuelle Version auch auf der Homepage steht. Wenn Sie nun den Compiler aufrufen, erhalten Sie automatisch Debug-Meldungen, die über Cache-Hits und Cache-Misses informieren. Man kann die Meldungen auch abschalten. |
Problemfall
Wenn nirgendwo benutzte Makrodefinitionen verändert wurden, entfällt das Kompilieren. Das mag zunächst unsinnig erscheinen, denn wer ändert schon unbenutzte Makros? Im Linux-Kernel gibt es aber ein Paradebeispiel für dieses Problem: Die Datei include /linux/autoconf.h im Kernel-Source enthält alle Konfigurationsdaten des Kernels. Fast jedes C-File im Kernel bindet dieses Includefile ein. Macht man nun eine winzige Änderung an der Konfiguration eines Treibers, muss der komplette Kernel neu übersetzt werden. Mit Compilercache wird hier nur der Treiber selbst neu kompiliert.
Wird ein Projekt gelöscht und neu entpackt, so läuft die Kompilierung rasant schnell ab, da die Ergebnisse alle noch im Cache sind. Das nützt bei neuen CVS-Checkouts oder dem Aufspielen neuer Versionen solcher Pakete, bei denen sich lediglich ein paar Sourcen verändert haben.
Ist der Cache via NFS gemountet, kompiliert in einer Arbeitsgruppe von zehn Leuten fortan jeweils nur ein einziger die Quelltexte und spart damit die Zeit der anderen. Das wirkt sich gerade beim verteilten Entwickeln mit CVS sehr positiv aus – wenn das NFS schneller ist als die Neukompilierung.
Noch mehr Vor-, aber auch ein Nachteil
Während der Entwicklungsphase eines Programms wird öfter zwischen -O2 (optimierter Code) und -g (debuggbarer Code) umgeschaltet. Jedes Mal sind ein make clean– und ein erneuter make-Lauf mit den geänderten Optionen notwendig. Das ist ärgerlich, denn der Computer hat die Dinge früher schon mit denselben Optionen kompiliert. Compilercache dagegen holt die Files aus dem Cache statt sie neu zu übersetzen.
Wer ein Projekt von Anfang an entwickelt, beginnt oft mit einem einfachen Shellskript, das die Compileraufrufe durchführt. Mit wachsender Größe des Programms ist die Wartezeit für eine komplette Neukompilierung kaum mehr erträglich. Der gängige Weg führt normalerweise über das komplexe Automake. Makefiles sind fehleranfällig, was tagelanges Debuggen von Dateien nach sich ziehen kann, die nur aufgrund falscher Abhängigkeiten existieren. Mit Compilercache kann man noch lange beim einfachen Shellskripts bleiben.
Der Maintainer eines RPM-Pakets berichtete, dass beim Bauen eines Pakets der Quellcode mehrfach kompiliert werden muss, falls etwas schief gegangen ist. Bei entsprechender Größe der Quellen (in seinem Fall Mozilla) ist das ein schwer wiegendes Problem, das durch den Compilercache lösbar ist.
Fragt sich, wodurch die Vorteile des Cache erkauft werden. Die Antwort: durch Plattenplatz. Der jedoch ist im Gegensatz zur eingesparten Zeit käuflich und außerdem erzeugt der Cache nicht viele Daten. Wenn es dennoch einmal zu viele werden, kann man einfach die ältesten löschen. Das README-File nennt Details.
Eines der wichtigen Designkriterien beim Entwickeln von Compilercache war die Forderung, dass er keinen Schaden anrichten darf. Er muss sich unter allen Umständen genau so verhalten wie der wirkliche Compiler. Lediglich das Zeitverhalten darf anders sein.
Weg durch den Dschungel
Zunächst überprüft der Compilercache, ob eine -c-Option auf der Kommandozeile angegeben wurde. Falls der Compiler stattdessen beispielsweise als Linker aufgerufen wird, dann leitet der Cache direkt an den Compiler weiter.
Nun wird der Sourcecode zuerst durch den C-Präprozessor geschickt. Hierbei werden alle Makros ausgewertet, Kommentare entfernt und vor allem sämtliche Includefiles eingelesen. Als Ergebnis kommt ein einzelnes C-Sourcefile heraus. Das wird in seine lexikalischen Tokens zerlegt und jedes davon wird auf eine einzelne Zeile geschrieben. Das entstehende Sourcefile sieht immer gleich aus, egal wie viele Leerzeichen, Kommentare, unbenutzte Makros oder ähnliches Sie in Ihrem Quellcode eingebaut haben.
An diese Sourcedatei werden noch die Version des Compilers und die verwendeten (und relevanten) Kommandozeilenoptionen sowie der Name der Quelldatei angehängt. Dann ist die Berechnung des Hashwerts dieser Datei mit Hilfe von Md5sum an der Reihe. Der Hashwert ist der Dateiname des entstehenden Objektfiles im Cache-Verzeichnis. Ist also eine solche Datei bereits vorhanden, wird sie kopiert. Andernfalls ruft Compilercache den Compiler auf und kopiert das Ergebnis zusätzlich noch in den Cache.
Detailfragen
Der Compilercache ist nicht als Ersatz von Make gedacht, auch wenn man aufgrund der gestiegenen Geschwindigkeit oft auf Make verzichten kann. Make ist ein sehr schnelles Programm. Wenn es in der Lage ist, Kompilierungen wegzulassen, so tut es dies wesentlich schneller als der Compilercache. Make vergleicht lediglich die Zeit der letzten Änderung (Modification Time) der Dateien.
Der Compilercache hingegen führt komplexe Operationen mit den Dateien aus, was natürlich langsamer ist als das Make-Verfahren. Am besten lassen sich daher beide Tools in Kombination anwenden. Make verhindert trivial unnötige Kompilierungen und der Compilercache eliminiert die trickreicheren Fälle.
Ausblicke
Der Compilercache hat derzeit keine bekannten Bugs zu verzeichnen und ist vielerorts stabil im Einsatz. Es wäre zu begrüßen, wenn seine Funktionalität in den Gcc integriert würde. Der Compiler könnte dann die Kompilate funktionsbezogen und nicht – wie Compilercache – dateibezogen speichern. Doch bis dahin bleibt nur – Compilercache. ( mdö/tfr)
Infos |
|
[1] Homepage des Compilercaches: http://www.erikyyy.dercompilercache/ |
Der Autor |
|
Erik Thiele studiert Informatik an der Uni Stuttgart, interessiert sich für alles und nichts ab 1.0.6 und seine Homepage. Die URL mit korrektem Trailing Slash ist: http://www.erikyyy.der |




