Kompilierungszeit mit Hilfe eines precompiled header (PCH) deutlich verringern


Wenn Projekte in C++ etwas größer werden, kommen relativ schnell unangenehme Eigenschaften zum tragen. Eine davon ist die immer mehr ansteigende Kompilierungszeit. Nach nur ein paar grundlegenden Klassen hatten wir in unserem Projekt auf meinem System mit Visual Studio 2013 für einen kompletten Rebuild 105 Sekunden zu warten. Das ist auf Dauer natürlich viel zu lange, daher musste eine Lösung her. Durch mehrere Verbesserungen konnte dieser Wert auf 15 Sekunden reduziert werden. Ein Teil davon wurde durch die Verwendung einer PCH (siehe unten) erreicht und diesen möchte ich hier genauer beschreiben.

Bevor ich auf die konkreten Schritte eingehe, verliere ich zuerst ein paar grundlegende Worte über den Kompilierungsvorgang. Dieser stellt sich nämlich nicht als sonderlich intelligent heraus.

Hintergründe zum allgemeinen Kompilierungsvorgang

Für den Kompilierungsvorgang wird jede .cpp Datei als eigenständige Kompilierungseinheit betrachtet. Für einen kompletten Rebuild müssen also alle .cpp Dateien gelesen und kompiliert werden. Werden in den .cpp Dateien nun Headerdateien eingebunden, so müssen auch diese eingelesen und kompiliert werden. Das Problem ist nun, dass diese Einheiten unabhängig voneinander agieren. Werden die gleichen Headerdateien in verschiedenen .cpp Dateien eingelesen, bedeutet das, dass diese für jede .cpp Datei einzeln eingelesen und kompiliert werden müssen. Bemerkbar macht sich dies insbesondere bei großen Headerdateien wie beispielsweise Header aus der Standardbibliothek.

Als Beispiel sei folgendes Bild gegeben:

Beispielhafte Include-Hierarchie

Wir haben es mit zwei Implementierungsdateien zu tun (A.cpp und B.cpp). A.cpp inkludiert A.h und common.h. B.cpp inkludiert B.h und ebenfalls die common.h. Da der Kompilierungsvorgang getrennt abläuft, wird die common.h zweimal eingelesen und kompiliert. Wenn die common.h nun folgendermaßen aussehe


#pragma once

#include <vector>
#include <map>
#include <list>
#include <string>
#include <sstream>
#include <memory>

kann man sich leicht vorstellen, warum die Kompilierungszeiten so schnell ansteigen können (die Headerdateien aus der Standardbibliothek können verdammt groß werden).

Zum Glück gibt es für dieses Problem eine Lösung und die heißt precompiled header (PCH). Damit wird der Compiler veranlasst, bestimmte Headerdateien nur einmal zu kompilieren und das Ergebnis dann für alle Kompilierungseinheiten (.cpp Dateien) wiederzuverwenden. Damit löst man genau das beschriebene Problem. Wäre die common.h im PCH, kompiliere sie im Beispiel nur einmal anstatt zweimal.

Die PCH ist letztendlich selbst nur eine (besondere) Headerdatei. In Visual Studio wird sie meisten bei neuen Projekten mit angelegt und heißt stdafx.h. Dort können nun alle Headerdateien eingetragen werden, welche für den kompletten Kompilierungsvorgang nur einmalig kompiliert werden sollen. Damit das ganze allerdings funktioniert muss diese Headerdatei in allen anderen Dateien (sowohl .h als auch in .cpp Dateien) als allererstes eingebunden werden. Zum Glück gibt es aber auch dafür eine automatisierbare Lösung.

Bevor ich auf die Schritte eingehe, welche für Visual Studio notwendig sind, vorweg noch eine Warnung. Dadurch, dass die PCH in allen anderen Dateien eingebunden werden muss, bedeutet eine Änderung der PCH immer einen kompletten Rebuild. Eine Änderung tritt auch auf, wenn eine der Headerdateien, welche die PCH einbindet, verändert wird. Aus diesem Grund sollten dort nur Headerdateien stehen, welche sich nur sehr selten ändern. Die Headerdateien der Standardbibliothek sind dafür ein gutes Beispiel.

Einrichtung in Visual Studio 2013

  1. Zuallererst müsst ihr dafür sorgen, dass es die entsprechenden stdafx.h und stdafx.cpp Dateien gibt. Meistens sind diese bereits vorhanden. Falls nicht, kann man sie ganz normal manuell anlegen.
  2. Das Projekt muss für die PCH Datei eingerichtet werden. Dazu unter Projekt -> Eigenschaften -> C/C++ -> Vorkompilierte Header wechseln. Unter "Vorkompilierter Header" sollte Verwenden (/Yu) und unter "Vorkompilierte Headerdatei" stdafx.h stehen.
  3. Es muss noch dafür gesorgt werden, dass die PCH in jeder Datei des Projektes als erstes eingebunden wird. Dazu auf den Reiter "Erweitert" wechseln und dort unter "Erzwungene Includedateien" stdafx.h eintragen. Anschließend die Projekteigenschaften wieder schließen.
  4. Nun muss man Visual Studio noch mitteilen, dass die entsprechende PCH erstellt werden soll. Dazu die Eigenschaften der Datei stdafx.cpp öffnen (Rechtsklick -> Eigenschaften) und unter C/C++ -> Vorkompilierte Header bei "Vorkompilierte Header" Erstellen (/Yc) auswählen.

Die PCH könnte dabei wie die folgende aussehen. Bis auf die letzte Zeile handelt es sich dabei nur um automatisch generierte Einträge.


// stdafx.h : Includedatei für Standardsystem-Includedateien
// oder häufig verwendete projektspezifische Includedateien,
// die nur in unregelmäßigen Abständen geändert werden.
//

#pragma once

#include "targetver.h"

#include <stdio.h>
#include <tchar.h>

// Hier auf zusätzliche Header, die das Programm erfordert, verweisen.
#include "common.h"

Es wurde dabei nur die common.h und nicht die einzelnen Headerdateien selbst innerhalb der common.h eingebunden. Das hat den Grund, dass das Projekt weiterhin kompilierbar sein sollte, auch wenn die PCH nicht eingerichtet ist. Das heißt, ein Projekt sollte nicht die Verwendung einer PCH voraussetzten, damit es sich kompilieren lässt (Eine Mehrfachinkludierung wird durch die entsprechenden include guards sichergestellt.

Das waren alle notwendigen Schritte. Nun könnt ihr das Projekt neu kompilieren. Wenn alles funktioniert hat, wird zuerst die stdafx.cpp und dann die restlichen Dateien kompiliert.

Da die Vekroria-Engine in ihren Headerdateien aktuell noch regen Gebrauch von der Anweisung using namespace std; macht, können diese Headerdateien noch nicht in die PCH eingebunden werden (obwohl sie sich eignen würden). Durch die genannte Anweisung kommt es leider (zumindest bei unserem Projekt) zu Namenskonflikten. Wenn spätere Versionen der Vektoria-Engine diese Anweisung jedoch nicht mehr enthalten, sollten auch diese Headerdateien in der PCH eingebunden werden können.