Aus Linux-Magazin 05/2005

Mit CCache effizient C und C++ kompilieren

Bis zum Faktor 10 ohne Hardware-Upgrade: Die Compiler-Cache-Software CCache übersetzt C- und C++- Programme in Rekordzeit. Intelligentes Caching macht's möglich.

Für Software-Entwickler gilt das olympische Motto nicht nur alle vier Jahre, sondern immer: schneller, höher, weiter! Dabei stoßen sie häufig an die Leistungsgrenzen des Systems. Wer an größeren Projekten mit MBytes an Quellcode arbeitet, wartet auf einen Clean Build schon mal eine Stunde. Die einfachste Lösung ist es, die Hardware aufzurüsten. Tatsächlich entschärfen Prozessor-Upgrades, mehr RAM und schnellere Festplatten das Problem.

Aber nicht jeder kann und will ein Monatsgehalt dafür springen lassen. Gewöhnlich sehen sowohl der IT-Beauftragte in der Firma als auch der private Geldbeutel eine kostenneutrale Lösung lieber, zum Beispiel die Compiler-Cache-Software CCache[1]. Sie steigert die Geschwindigkeit von Kompiliervorgängen um einen Faktor 4 bis 10.

Zentral statt verteilt

In Ausgabe 11/2004 stellte das Linux-Magazin schon ein verteiltes Compilersystem vor[2]. Die Idee dabei war, voneinander unabhängige Übersetzungen eines Projekts parallel auf mehreren Rechnern auszuführen. Das bietet sich insbesondere für Firmen an, da dort in der Regel viele Rechner zur Verfügung stehen. Mit zehn Teilnehmern lässt sich beispielsweise der Linux-Kernel bis zu 7-mal schneller kompilieren. Zu Hause stehen aber statt einer Farm in den meisten Fällen nur einzelne Rechner. Damit scheiden verteilte Compilersysteme aus.

Es bleibt nur die Erinnerung – oder etwas technischer formuliert: Caching. “All optimization is an exercise in caching” lautet eine leicht abgewandelte Weisheit nicht nur unter CPU-Designern. Betrachtet man die Builds eines Projekts genauer, ist zu erkennen, dass sich meist nur wenige Dateien verändern; es würde genügen, ausschließlich sie zu übersetzen. Diesen Zweck erfüllt bereits Make, aber nach einem »make clean« fängt wieder das Warten an.

An dieser Stelle setzt ein Compiler Cache an. Der Clou besteht darin, Quelltext nur einmal zu übersetzen und, falls man demselben Code erneut begegnet, die zugehörige Objektdatei aus dem Cache zu kopieren. Der Geschwindigkeitsgewinn entspricht dann der Zeitdifferenz zwischen echter Übersetzung und Blick in den Cache plus Kopierzeit. Bei einem so genannten Cache Miss dauert der Vorgang nur marginal länger, nämlich die Dauer für das Erkennen des Cache Miss sowie das Kopieren der Objektdatei in den Cache.

Vor längerer Zeit schon implementierte Erik Thiele die Idee eines Compiler Cache als Shellskript[3],[4]. Eine zusätzliche Beschleunigung durch Neu- Implementierung in C und jede Menge wichtige Erweiterungen (siehe Kasten “Erweiterungen …”) bringt CCache von Andrew Tridgell, seines Zeichens Autor von Samba und Rsync. Die aktuelle CCache-Version 2.4 liegt auf der Projekt-Homepage zum Download bereit.

Funktionsweise

Das Prinzip des Compiler Caching ist recht einfach: Nachdem ich »hello.c« einmal übersetzt habe, kopiere ich das Resultat »hello.o« in einen Extraspeicher; beim nächsten Mal kopiere ich es nur noch vom Extraspeicher an die Stelle, an der es auch der Compiler erzeugt hätte. Doch wer genau analysiert, stellt fest, dass es ganz so einfach nicht funktioniert, denn der Dateiname reicht zur Identifikation nicht aus. Sonst würde CCache nach jeder Änderung an »hello.c« immer noch das alte »hello.o« kopieren. Auch die Länge von »hello.c« als zusätzlicher Schlüssel bietet keinen Ausweg, schließlich ändert sie sich nicht, wenn der Programmierer beispielsweise einen String »hello!« durch »Hallo!« ersetzt.

Folglich muss CCache den Inhalt von »hello.c« genauer betrachten. Es würde allerdings lange dauern, alle Quelltextdateien zu speichern und bei Bedarf komplett zu vergleichen. Stattdessen transferiert CCache den Inhalt von »hello.c« mit dem MD4-Algorithmus[5] in eine 128-Bit-Nummer (Hash) und stellt sie als 32-stellige Hexadezimalzahl dar. Mit dieser Hexadezimalzahl als Dateiname wird das kompilierte »hello.o« im Cache abgelegt. Ob man eine Quelldatei schon einmal übersetzt und das Resultat bereits im Cache hat, ergibt sich dann bei einer MD4-Berechnung aus dem Datei-Inhalt gefolgt von einem Blick ins Dateisystem.

Wichtigster Grundsatz für einen Compiler Cache ist, es muss immer dieselbe Datei herauskommen wie ohne Cache. Irren darf er sich nur auf der sicheren Seite. Lieber einen Cache Miss diagnostizieren und unnötig eine Datei ein zweites Mal übersetzen, als irrtümlich einen Cache Hit erkennen und die falsche Datei aus dem Cache holen.

Das führt zu Problem Nummer eins: Das Kompilat »hello.o« hängt nicht allein von »hello.c« ab, sondern noch von einigen anderen Faktoren. Zum Beispiel führt die Zeile »#include “hello.h”« im Quelltext dazu, dass »hello.o« auch von »hello.h« abhängt. Zudem beeinflussen Makrodefinitionen auf der Kommandozeile (»-DFOOBAR=42«) und Include-Pfade (»-I/usr/local/include«) das Kompilat. Mit anderen Worten: CCache muss den Quelltext erst durch den C-Präprozessor schicken. Erst dessen Ausgabe »hello.i« eignet sich zum Berechnen des MD4-Hash.

Alle Details beachten

Weiterhin hängt das Resultat der Übersetzung von »hello.i« von der Optimierungsstufe ab. Potenziell beeinflussen auch fast alle anderen Compiler-Optionen die Objektdatei. Also müssen sie in unveränderter Reihenfolge ebenfalls in den MD4-Hash eingehen. Last but not least, auch der Compiler selbst spielt eine Rolle. Verschiedene Compiler (C oder C++, unterschiedliche Versionen, Crosscompiler etc.) erzeugen aus ein und derselben Quelldatei unterschiedliche Objektdateien. Also müssen auch Pfad, Größe und wenigstens die Zeit der letzten Änderung des Compilers zum MD4-Hash beitragen.

Erweiterungen von
CCache

Gegenüber seinem Vorläufer Compiler Cache bietet CCache einige Vorteile:

  • In C implementiert, also schneller.
  • Führt Buch über Cache Hits und Misses sowie ein
    Dutzend andere Kenngrößen (siehe Kasten
    “Cache-Statistik”).
  • Automatisches Cache-Management, das heißt Entfernen von
    Dateien, wenn der Cache voll wird.
  • Die Compiler-Warnungen auf »stderr« werden
    ebenfalls gespeichert, somit identische Logs beim Kompilieren mit
    und ohne CCache.
  • Versteht eine umfangreichere Liste von Compiler-Optionen.
  • Merkt sich die präprozessierten Quellen und füttert
    das Resultat bei einem Cache Miss direkt an den Compiler.

Stellt CCache nach der Berechnung des Hash einen Cache Miss fest, kompiliert es übrigens nicht »hello.c«, denn diese Datei würde überflüssigerweise noch einmal durch den C-Präprozessor geschickt. Das Ergebnis liegt schon in »hello.i« vor, alle Compiler können bereits präprozessierten Quellcode direkt verarbeiten. In der Regel erkennen sie solchen Code an der Endung ».i«. Eine Zusammenfassung der Arbeitsweise von CCache zeigt Abbildung 1 in einem Ablaufdiagramm.

Cache as cache can

Caching von Objektdateien ist nur möglich, wenn auch eine Objektdatei erzeugt wird. Üblicherweise dient dazu die Compiler-Option »-c« (compile only). In folgenden Fällen übersetzt CCache, ohne das Ergebnis anschließend in den Cache aufzunehmen:

  • Kompilieren mit anschließendem Linken, etwa mit »cc
    -o hello hello.c«.
  • Kompilieren mehrerer Quelldateien auf einmal, etwa mit
    »cc -c hello.c world.c«.
  • Aufruf des Linkers, etwa mit »cc -o hello
    hello.o«.
  • Wenn der Exit-Status der Übersetzung nicht null ist.

Darüber, ob und wie häufig solche Situationen bereits aufgetreten sind, gibt die CCache-interne Buchführung Auskunft (siehe Kasten “CCache-Statistik”).

Wer kein fertiges Paket in seiner Distribution findet, übersetzt CCache mit dem üblichen Dreisatz »./configure; make; make install«. Dies installiert das ausführbare Programm im Verzeichnis »/usr/local/bin/ccache« sowie eine Manual Page. Grundsätzlich gibt es zwei Möglichkeiten, CCache aufzurufen. Zum einen lässt sich vor das Compiler-Kommando das Präfix »ccache« setzen:

ccache cc -o hello.o hello.c

Beim Übersetzen per Makefile erledigt folgende Zeile das für C++:

make CXX='ccache g++'

Aber auch schon beim Konfigurieren funktioniert:

./configure CC='ccache gcc40'

Dieses Beispiel nutzt die kommende GCC-Version 4.0.

Abbildung 1: CCache erstellt einen MD4-Hash aus mehreren Elementen um festzustellen, ob eine Datei bereits im Cache ist.

Abbildung 1: CCache erstellt einen MD4-Hash aus mehreren Elementen um festzustellen, ob eine Datei bereits im Cache ist.

Pro und Kontra

Diese Methoden bieten als gleichzeitigen Vor- und Nachteil die Möglichkeit, bei jedem Kompiliervorgang einzeln zu entscheiden, ob CCache zum Einsatz kommt. Wer CCache als Standard verwenden möchte, lässt symbolische Links von den Compilernamen auf die ausführbare Datei »ccache« zeigen. Eine alternative Lösung, die einfaches Ein- und Ausschalten von CCache ermöglicht, besteht aus einem eigenen Verzeichnis, das nichts als symbolische Links auf »ccache« enthält:

mkdir -p /usr/local/libexec/ccache
cd /usr/local/libexec/ccache
ln -s /usr/local/bin/ccache cc
ln -s /usr/local/bin/ccache gcc
ln -s /usr/local/bin/ccache gcc40
ln -s /usr/local/bin/ccache c++
ln -s /usr/local/bin/ccache g++
ln -s /usr/local/bin/ccache icc
ln -s /usr/local/bin/ccache icpc
[...]

Nun kommt CCache zum Einsatz, wenn »/usr/local/libexec/ccache« vor »/usr/bin/« und vor »/usr/local/bin/« in der Umgebungsvariablen »PATH« steht. Dies führt nicht zu einem rekursiven Aufruf, weil CCache stets nach dem ersten Compiler sucht, der kein symbolischer Link ist. Noch einen Hauch schneller geht es, wenn man CCache das Verzeichnis mit den echten Compilern über die Variable »CCACHE_PATH« mitteilt und damit die Suche erspart.

Es bleibt die bedeutende Frage, wie sehr CCache einen Build-Vorgang beschleunigt. Abbildung 2 zeigt die Werte für je zwei C- und C++-Projekte als Balkendiagramm. Die herkömmlichen Übersetzungszeiten ohne CCache sind auf 100 normiert, so lassen sich die Veränderungen direkt in Prozent ablesen. Man erkennt für die C-Projekte eine Reduktion auf 27 oder 23 Prozent, also ungefähr einen Faktor 4.

CCache-Statistik

CCache gibt mit interessanten Kenngrößen wie der Anzahl der Cache Hits Auskunft über seine Effizienz:

$ ccache -s
cache directory/share/ccache
cache hit19373
cache miss41451
called for link2600
multiple source files23
compile failed222
preprocessor error54
not a C/C++ file1598
autoconf compile/link3283
unsupported compiler option70
no input file210
files in cache82902
cache size961.3 MBytes
max cache size5.0 GBytes

Bei C++-Projekten entsteht ein noch größerer Gewinn: Auf 16 oder 10 Prozent sinkt der Zeitaufwand, also ein Faktor 6 für Firefox und sogar Faktor 10 für DDD (Data Display Debugger). Der stärkere Effekt bei C++ erklärt sich durch dessen Komplexität verglichen mit C. Pro KByte erzeugtem Objektcode arbeitet ein C++-Compiler im Durchschnitt deutlich länger als ein C-Compiler. Beim Kopieren aus dem Cache wirkt sich die Verarbeitungszeit nicht mehr aus.

Genau hier entsteht der Geschwindigkeitsgewinn. Allgemein gilt: Je länger die Verarbeitung zur Objektdatei dauert, desto größer fällt die Beschleunigung durch CCache aus. Das bedeutet, dass sich bei steigendem Aufwand für Code-Optimierung (»gcc -O0«, »-O1«, »-O2«, »-O3«) zunehmende Beschleunigungsfaktoren entwickeln. Die Praxis bestätigt dies, wie Abbildung 3 zeigt. Sie gibt die Übersetzungszeiten in Sekunden bei vier verschiedenen Optimierungsstufen am Beispiel des Editors Vim wieder. Je höher die Optimierung, desto länger dauert das Kompilieren. Die Zeit mit 100 Prozent Cache Hits bleibt dagegen praktisch gleich, weil höhere Optimierung das immer notwendige Präprozessieren des Quellcode nicht beeinflusst; ebenso wenig wie das Kopieren der Objektdatei aus dem Cache.

Abbildung 2: Benchmark-Resultate von CCache für je zwei C- und C++-Projekte. Die Werte auf der Zeitachse (nach oben) wurden auf 100 für eine herkömmliche Übersetzung normiert. Der größte Vorteil entsteht bei C++-Programmen.

Abbildung 2: Benchmark-Resultate von CCache für je zwei C- und C++-Projekte. Die Werte auf der Zeitachse (nach oben) wurden auf 100 für eine herkömmliche Übersetzung normiert. Der größte Vorteil entsteht bei C++-Programmen.

Abbildung 3: Benchmark-Resultate von CCache bei verschiedenen Optimierungsstufen des Compilers. Die Balken geben jeweils die Übersetzungszeiten in Sekunden wieder. Bei hoher Compiler-Optimierung entsteht ein größerer Vorteil.

Abbildung 3: Benchmark-Resultate von CCache bei verschiedenen Optimierungsstufen des Compilers. Die Balken geben jeweils die Übersetzungszeiten in Sekunden wieder. Bei hoher Compiler-Optimierung entsteht ein größerer Vorteil.

Zusammenfassung

Wer wiederholt Projekte mit »make clean; make« übersetzt, kann dies mit CCache deutlich beschleunigen, vor allem für C++-Quellen bei hoher Optimierungsstufe. Die Installation erfordert keine Änderungen an bestehenden Make-Dateien oder anderen Build-Systemen. Bei Bedarf lässt sich der Cache der Objektdateien auch von Arbeitsgruppen gemeinsam benutzen. CCache kooperiert auch mit dem verteilten Compilersystem Distcc[2] und ermöglicht damit zusätzliche Beschleunigung. (csc)

Infos

[1] CCache:[http://ccache.samba.org]

[2] Daniel Molkentin, “Ausgeteilt”: Linux-Magazin 11/04, S. 96

[3] Compiler Cache: [http://www.erikyyy.de/compilercache/]

[4] Erik Thiele, “Abkürzungen durch den Header-Dschungel”: Linux-Magazin 09/01 [https://www.linux-magazin.de/Artikel/ausgabe/2001/09/Compilercache/compilercache.html]

[5] “The MD4 Message-Digest Algorithm”, R. Rivest, RFC 1320: [http://www.ietf.org/rfc/rfc1320.txt]

LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben