Предкомпилированные заголовки

Предкомпилированные заголовки

patrios

Возможно многие из вас создав стандартный C++ проект в Visual Studio 2017 и ранее замечали такой заголовочный файл как “stdafx.h”. Позднее он был переименован в “pch.h”. В этой статье я попытаюсь объяснить что в этом файле такого особенного и как он может ускорить время компиляции вашего проекта.


В предыдущей статье про препроцессинг я показал что включения любого заголовочного файла в единицу трансляции (или проще в .cc/.cpp файл) буквально копирует все содержимое этого заголовочного файла. При этом код вставленный таким образом в единицу трансляции будет компилироваться ровно так же как и непосредственно сам код .cc/.cpp файла.

Для наглядности давайте рассмотрим следующий простой пример:

main.cpp:

#include <iostream>
#include <vector>
#include <map>
#include <memory>
#include <queue>
#include <list>
#include <mutex>
#include <bitset>
#include <chrono>

int main()
{
    std::cout << "Hello World!\n";
}

В данном примере мы просто включаем набор весьма стандартных заголовочных файлов, которые часто бывают нужны в разных местах в проекте, и далее просто выводим Hello World!

Для начала давайте посмотрим во что превратит препроцессор этот, казалось бы, простой код. Для этого воспользуемся следующей командой:

clang++ -D COMPILE_FLAG -E main.cc -o main.i

Я не буду копировать весь код, так как там более 54000 строк, но выглядит он примерно так:

Результат препроцессора

Препроцессор буквально выполнил команду которую мы его попросили выполнить – скопировал содержимое заголовочных файлов в модуль main.cc. Однако теперь компилятору понадобится обработать не 1 строку вывода текста на экран (+ код из библиотеки вывода), а целых 54 000 строк кода. Более того в случае инкриминтальной сборки, то есть когда мы просто меняем пару строк исключительно в модуле main.cc – компилятору опять же придется перекомпилировать все эти 54 000 строк кода.

Давайте посмотрим сколько времени занимает процесс компиляции такого модуля. Для это воспользуемся командой:

time clang++ main.cc

Результат:

real    0m0.425s

Можно перезапустить данную команду несколько раз, результат будет примерно таким же +-10%.

Почти полминуты, большая часть которой уходит на перекомпиляцию никогда не изменяющегося кода, ведь код в заголовках стандартных библиотек постоянен, по крайней мере до тех пор пока мы не решим обновить версию стандартных библиотек. Но что если бы мы могли один раз скомпилировать весь этот неизменный код и далее использовать уже готовый предкомпилированный модуль в процессе сборки. Собственно для этого и существуют предкомпилированные заголовки.

Для начала давайте вынесем все наши заголовки в один и назовем его pch.h.

#ifndef __PCH_H
#define __PCH_H

#include <iostream>
#include <vector>
#include <map>
#include <memory>
#include <queue>
#include <list>
#include <mutex>
#include <bitset>
#include <chrono>

#endif // __PCH_H

Далее мы включим только этот заголовок в модуле main.cc:

#include "pch.h"

int main()
{
    std::cout << "Hello World!\n";
}

Теперь нам остается непосредственно скомпилировать эти заголовки. Данный процесс немного отличается в Visual Studio и clang/gcc. Я покажу как это делается в clang, но если вам интересно настроить процесс в Visual Studio можете глянуть это видео. Мы же воспользуемся следующей командой:

time clang++ -x c++-header -o pch.h.pch -c pch.h

Результат:

real    0m0.522s

Как видно процесс предкомпиляции заголовков занимает даже чуть больше времени чем процесс полной пересборки. Однако в случае модификации кода в модуле main.cc нам теперь не нужно перекомпилировать весь код заголовочных файлов. Давайте глянем сколько времени займет сборка одного только модуля main.cc теперь:

time clang++ -include-pch pch.h.pch main.cc

Результат:

real    0m0.160s

Как видно компиляция стала почти в 3 раза быстрее.


Report Page