Precompiled Headers w Visual C++

Uwaga! Informacje na tej stronie mają ponad 6 lat. Nadal je udostępniam, ale prawdopodobnie nie odzwierciedlają one mojej aktualnej wiedzy ani przekonań.

Wersja artykułu 1.2

Wstęp

Język C++, choć wyraźnie pozostaje w tyle za rozwojem nowoczesnych metod i języków programowania takich jak C# czy Java, w wielu dziedzinach wciąż jest najważniejszym językiem programowania. Jedną z jego wad jest konieczność stosowania plików nagłówkowych, a w konsekwencji bardzo długi czas kompilacji programów.

Precompiled Headers to mechanizm, który małym nakładem pracy pozwala wielokrotnie przyspieszyć kompilację. Posiada go wiele kompilatorów. Ja tutaj skupię się na Visual C++ 2005. W innych wersjach kompilatora Microsoftu powinno wyglądać to podobnie. Dla innych kompilatorów, np. g++, będziesz musiał poszukać innego artykułu.

Precompiled Headers to ogólny mechanizm i można używać go na różne sposoby. Ja zamiast opisywać go abstrakcyjnie postaram się pokazać, jak w praktyce można go wykorzystać w przykładowym projekcie w Visual C++.

W skrócie

Zastosowanie Precompiled Headers można streścić w następujących 6 krokach. Opiszę je dokładnie poniżej.

  1. Utwórz nowy plik nagłówkowy - PCH.h. Wpisz do niego #include nagłówków, które wszędzie chciałbyś mieć włączone.
  2. Utwórz nowy plik źródłowy - PCH.cpp. Wpisz do niego jedynie #include "PCH.h".
  3. W opcjach projektu, w gałęzi C/C++ > Precompiled Headers ustaw "Use Precompiled Header (/Yu)" i wpisz "PCH.h".
  4. We właściwościach pliku PCH.cpp w tej samej gałęzi przestaw opcję na "Create Precompiled Header (/Yc)".
  5. W każdym pliku cpp swojego projektu włącz na początku #include "PCH.h".
  6. Dla plików, w których nie chcesz tego zrobić, przestaw we właściwościach opcję na "Not Using Precompiled Headers".

W szczegółach

Załóżmy, że mamy projekt programu w C++ złożony z kilku plików źródłowych cpp i nagłówkowych h. Chcemy zastosować do niego mechanizm Precompiled Headers, żeby przyspieszyć jego kompilację. Oto co dokładnie musisz zrobić:

1. PCH.h

"Precompiled header", czyli nagłówek prekompilowany, jak sama nazwa wskazuje, musi być plikiem nagłówkowym. Moglibyśmy uczynić nim jeden z istniejących w naszym projekcie nagłówków, ale ja proponuję utworzyć nowy. W tym celu kliknij File > New > File..., wybierz "Header File (.h)" i jako nazwę wpisz "PCH.h". Nazwa jest oczywiście dowolna, to tylko moja propozycja.

W jego treści może się znaleźć cokolwiek - bezpośrednio deklaracje funkcji, zmiennych i klas, ale w szczególności include-y innych plików nagłówkowych. Ten plik będzie prekompilowany i każdy plik źródłowy cpp, który go użyje, będzie się kompilował dużo szybciej. Co powinniśmy do niego wpisać?

Należy w nim włączyć inne pliki nagłówkowe - wszystkie te, które chcesz mieć do dyspozycji we wszystkich lub w większości plików źródłowych swojego projektu, bo często z nich korzystasz w różnych miejscach i traktujesz je jako podstawę, do której potrzebujesz dostępu zawsze i wszędzie. Chodzi szczególnie o biblioteki systemowe, które nierzadko liczą sobie dziesiątki albo setki tysięcy linii kodu i dotychczas musiały przetwarzane od początku przy kompilacji każdego pliku cpp. Mogą to być na przykład:

Przykładowy plik PCH.h może wyglądać tak:

Przykładowy plik PCH.h

2. PCH.cpp

Sam plik nagłówkowy to za mało. Pliki h są tak naprawdę traktowane przez kompilator jak powietrze - jakby ich w ogóle nie było. Mogą mieć dowolne rozszerzenie, leżeć w dowolnych katalogach i nie należeć nawet do projektu - i tak zadziałają poprawnie, bo są po prostu dosłownie, tekstowo włączane do plików źródłowych cpp przez preprocesor podczas kompilacji.

Dlatego do naszego prekompilowanego nagłówka potrzebujemy odpowiadającego mu pliku źródłowego. W tym celu kliknij File > New > File... i wybierz "C++ File (.cpp)" oraz wpisz nazwę "PCH.cpp". Ten plik nie będzie robił nic szczególnego, a jedynie posłuży nam do wygenerowania prekompilowanego nagłówka, który potem zostanie użyty przez pozostałe pliki źródłowe projektu. Dlatego jako jego treść wpisz następującą, jedną jedyną linijkę:

Treść pliku PCH.cpp

3. Use Precompiled Header

Czas na pogrzebanie w opcjach projektu. Musimy powiedzieć kompilatorowi, że ma skorzystać z mechanizmu Precompiled Headers i wskazać odpowiedni plik. Kliknij prawym klawiszem na projekt i z menu kontekstowego wybierz Properties.

Opcje projektu

Pokaże się okienko opcji projektu. W nim, w drzewku po lewej stronie wybierz gałąź Configuration Properties > C/C++ > Precompiled Headers. Po prawej stronie przestaw:

Use Precompiled Header

Co tu się stało? Tak naprawdę nie ma czegoś takiego jak ustawienie Precompiled Headers dla całego projektu. Przestawiając te opcje w ustawieniach projektu przestawiliśmy opcje, które będą stosowane domyślnie dla każdego pliku źródłowego cpp w naszym projekcie - o to chodziło. Każdy plik źródłowy ma korzystać z tego nagłówka do przyspieszenia kompilacji i właśnie poinformowaliśmy o tym kompilator.

4. Create Precompiled Header

Nagłówek prekompilowany tak naprawdę przybierze postać ogromnego, wielomegabajtowego pliku o rozszerzeniu PCH umieszczonego pośród plików tymczasowych kompilacji obok plików ILK, PDB, RES, OBJ i innych tym podobnych. Żeby można było z niego korzystać, musi jednak najpierw powstać. Dlatego jednemu z plików źródłowych naszego projektu musimy nakazać utworzenie prekompilowanego nagłówka. Właśnie po to powstał PCH.cpp.

Podobnie jak wyżej, kliknij prawym klawiszem tym razem na plik PCH.cpp i z menu wybierz Properties.

Opcje pliku PCH.cpp

W oknie dialogowym, które się otworzy, w gałęzi Configuration Properties > C/C++ > Precompiled Headers przestaw opcję Create/Use Precompiled Header na: Create Precompiled Header (/Yc).

Create Precompiled Header

5. #include "PCH.h"

Przestawienie tych opcji nie załatwi jednak za nas w automagiczny sposób przyspieszenia kompilacji. To do ciebie - programisty - należy dokończenie dzieła poprzez prawdziwe włączenie tego prekompilowanego nagłówka do każdego pliku źródłowego cpp swojego projektu, w którym zdecydowałeś się go używać. W tym celu, W KAŻDYM PLIKU ŹRÓDŁOWYM CPP, NA SAMYM POCZĄTKU, MUSISZ WŁĄCZYĆ NAGLÓWEK PCH.h.

Włączenie PCH.h

Jeśli o tym zapomnisz, otrzymasz błąd kompilacji o następującej treści: fatal error C1010: unexpected end of file while looking for precompiled header. Did you forget to add '#include "PCH.h"' to your source?

6. Not Using Precompiled Headers

Być może z jakiś powodów nie chcesz albo nie możesz włączyć prekompilowanego nagłówka PCH.h na początku jednego ze swoich plików źródłowych cpp w projekcie. Na przykład kiedy jest to zewnętrzna biblioteka autorstwa innego programisty, która nie jest związana z resztą twojego projektu, ale zdecydowałeś się włączyć ją bezpośrednio do projektu i nie chcesz jej modyfikować. Możesz w takim przypadku zrezygnować z korzystania z mechanizmu Precompiled Headers podczas jego kompilacji. Musisz jednak poinformować o tym kompilator, by nie szukał tam włączenia PCH.h i nie wyświetlił błędu takiego jak zacytowany powyżej.

W tym celu kliknij prawym klawiszem na taki plik cpp i z menu kontekstowego wybierz Properties. Następnie, w oknie dialogowym które się pokaże w gałęzi Configuration Properties > C/C++ > Precompiled Headers przestaw opcję Create/Use Precompiled Header na: Not Using Precompiled Headers.

Not Using Precompiled Headers

Problemy

Jakie problemy mogą się pojawić podczas korzystania z mechanizmu Precompiled Headers? Po pierwsze, zmiana czegokolwiek w którymś z plików włączanych przez nasz nagłówek prekompilowany wymusza rekompilację wszystkich korzystających z niego plików, a więc praktycznie całego projektu. Jednak czy to na pewno wada? W końcu założeniem było, że do PCH.h wpisujemy te nagłówki, których i tak chcemy używać w całym projekcie. Poza tym, zaleca się wpisywać tam nagłówki modułów, które są już stabilne i nie zmienia się ich zbyt często (szczególnie bibliotek systemowych), a moduły, które są w takcie pisania lub modyfikacji włączać we własnym zakresie tam gdzie to potrzebne.

Po drugie, problemy mogą się pojawić jeśli pliki swojego projektu masz zorganizowane we więcej niż pojedynczy katalog. Kompilator nie jest świadomy położenia tych plików względem siebie i nie zrozumie, że włączony w jednym pliku cpp "PCH.h" to ten sam plik, co włączony w drugim "../PCH.h" i w trzecim "Common/PCH.h", nawet jeśli te ścieżki wskazują poprawną lokalizację względem danego pliku cpp. Kompilator w takim przypadku zgłosi błąd, że nie znalazł prekompilowanego nagłówka włączonego dosłownie taki sposób, jaki został zadeklarowany w opcjach projektu.

Co wtedy zrobić? Rozwiązaniem jest niekorzystanie ze ścieżek względnych podczas włączania plików w swoim projekcie, a jedynie specyfikowanie ich nazw i dodanie wszystkich podkatalogów projektu do ścieżek, w których kompilator ma poszukiwać włączanych plików. W tym celu kliknij prawym klawiszem na projekt, wybierz z menu Properties, w oknie właściwości projektu zaznacz gałąź Configuration Properties > C/C++ > General i wypełnij odpowiednio opcję Additional Include Directories.

Additional Include Directories

Dodatek

Za zwrócenie mi uwagi na ten problem, a raczej za uświadomienie, że jego rozwiązanie nie jest tak oczywiste jak mi się wydawało, podziękowania należą się koledze o pseudonimie Koshmaar (wątek forum).

Rodzi się pytanie: Jeśli w pliku nagłówkowym używam używamy czegoś z jakiejś biblioteki, np. klasy std::vector z nagłówka <vector>, to wypadałoby włączyć ten nagłówek wewnątrz naszego pliku nagłówkowego. Tak nakazywałyby zasady poprawnego programowania w C++. Wówczas i tak włączamy każdy używany nagłówek tam gdzie to potrzebne i cały mechanizm Precompiled Headers wydaje się niepotrzebny. Jak to rozwiązać?

Rozwiązaniem jest następujący tok myślenia: Skoro zakładamy, że PCH.h włącza <vector>, a my włączamy nasz PCH.h w każdym pliku cpp, to możemy w tych plikach, a nawet w naszych nagłówkach h używać klasy std::vector bez dodatkowego włączania <vector>. Na przykład:

// ---- PLIK: SrobkaDoSilnika.h:

class Strobka
{
private:
  std::vector<float> m_WektorFloatow;
public:
  void Wypelnij();
};

// ---- PLIK: SrobkaDoSilnika.cpp:

#include "pch.h"
#include "SrobkaDoSilnika.h"

void Srobka::Wypelnij()
{
  m_WektorFloatow.push_back(666);
}

// ---- PLIK: TlokDoSilnika.cpp:

#include "pch.h"
#include "SrobkaDoSilnika.h"
#include "TlokDoSilnika.h"

// ...

To zadziała, bo każdy plik, który włącza StobkaDoSilnika.h włącza wcześniej pch.h więc mamy pewność, że std::vector jest mu znany.

Podsumowanie

Efekt jest prosty. Kompiluje się najpierw ten plik, który tworzy prekompilowany nagłówek, a dopiero potem te, które z niego korzystają - już bardzo szybko.

Output - wynik kompilacji

Problem jest dużo szerszy, a mechanizm Precompiled Headers bardziej elastyczny, niż to pokazuje przedstawiony tutaj przypadek. Moim celem było jednak ukazanie, jak można wykorzystać go w praktyce przedstawiając przykład użycia. Jeśli chcesz, możesz w MSDN lub na Google poszukać więcej informacji, jak choćby przełączników pozwalających na korzystanie z Precompiled Headers podczas uruchamiania kompilatora z parametrami z poziomu wiersza poleceń.

Precompiled Headers to funkcja kompilatora, która, kiedy użyta poprawnie, nie stwarza żadnych problemów, a wielokrotnie przyspiesza kompilację. To bardzo ważne - tym ważniejsze, im większy jest twój projekt. Jest przy tym bardzo prosta do zastosowania. Wystarczy zastosować opisane wyżej 6 kroków. Z pewnością wielu programistów by ją stosowało, gdyby tylko wiedziało o jej istnieniu. Dlatego moim celem było przedstawienie w tym artykule w możliwie najbardziej przystępny sposób, jak zacząć używać Precompiled Headers.

Linki

Adam Sawicki
25 maja 2007
[Download] [Dropbox] [pub] [Mirror] [Privacy policy]
Copyright © 2004-2024