﻿# Ускорение сборки C и C\+\+ проектов

Многие программисты не понаслышке знают о том, что программа на языке C и C\+\+ собирается очень долго\. Кто\-то решает эту проблему, сражаясь на мечах во время сборки, кто\-то \- походом на кухню "выпить кофе"\. Это статья для тех, кому это надоело, и он решил, что пора что\-то предпринять\. В этой статье разобраны различные способы ускорения сборки проекта, а также лечение болезни "поправил один заголовочный файл \- пересобралась половина проекта"\.

![0549_Reducing_build_time_ru/image1.png](https://import.viva64.com/docx/blog/0549_Reducing_build_time_ru/image1.png)

## Общие принципы

Прежде чем начать, давайте узнаем/вспомним об основных фазах трансляции кода C/C\+\+ в исполняемую программу\.

Согласно [п\.5\.1\.1\.2](http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1548.pdf) драфта N1548 "Programming languages — C" и [п\.5\.2](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/n4659.pdf) драфта N4659 "Working Draft, Standard for Programming Language C\+\+" \(опубликованные версии стандартов можно приобрести [здесь](https://www.iso.org/standard/57853.html) и [здесь](https://www.iso.org/standard/68564.html)\) определены 8 и 9 фаз трансляции соответственно\. Давайте опустим детали и рассмотрим абстрактно процесс трансляции:

![0549_Reducing_build_time_ru/image2.png](https://import.viva64.com/docx/blog/0549_Reducing_build_time_ru/image2.png)

* Фаза I \- исходный файл поступает на вход препроцессора\. Препроцессор делает подстановку содержимого указанных в \#include файлов и раскрывает макросы\. Соответствует фазам 1 \- 4 драфтов C11 и C\+\+17\.
* Фаза II \- препроцессированный файл поступает на вход компилятора и преобразуется в объектный\. Соответствует фазам 5 \- 7 драфта C11 и 5 \- 8 драфта C\+\+17\.
* Фаза III \- компоновщик связывает объектные файлы и предоставленные статические библиотеки, формируя исполняемую программу\. Соответствует фазе 8 и 9 драфтов C11 и C\+\+17 соответственно\.

Программа составляется из единиц трансляции \(\*\.c, \*\.cc, \*\.cpp, \*\.cxx\), каждая является самодостаточной и может препроцессироваться/компилироваться независимо от другой\. Из этого также следует, что каждая единица трансляции не имеет никакой информации о других единицах\. Если двум единицам трансляции надо обменяться какой\-либо информацией \(например, функцией\), то это решается путем связывания по имени: внешняя сущность объявляется с ключевым словом _extern_, и на фазе III компоновщик их связывает\. Простой пример\.

Файл TU1\.cpp:

```cpp
// TU1.cpp

#include <cstdint>

int64_t abs(int64_t num) 
{
  return num >= 0 ? num : -num;
}
```

Файл TU2\.cpp:

```cpp
// TU2.cpp

#include <cstdint>

extern int64_t abs(int64_t num);

int main()
{
  return abs(0);
}
```

Для упрощения согласования разных единиц трансляции был придуман механизм заголовочных файлов, заключающийся в объявлении четкого интерфейса\. Впоследствии каждая единица трансляции в случае надобности включает заголовочный файл через директиву препроцессора _\#include_\. 

Далее рассмотрим, как можно ускорить сборку на разных фазах\. Кроме самого принципа также будет полезно описать, как внедрить тот или иной способ в сборочную систему\. Примеры будут приводиться для следующих сборочных систем: _MSBuild_, _Make_, _CMake_\.

### Зависимости при компиляции

Зависимости при компиляции – это то, что в наибольшей степени влияет на скорость сборки C/C\+\+ проектов\. Они возникают всякий раз, когда вы включаете заголовочный файл через препроцессорную директиву _\#include_\. При этом создается впечатление, что существует лишь один источник объявления для какой\-то сущности\. Реальность же далека от идеала \- компилятору приходится многократно обрабатывать одни и те же объявления в разных единицах трансляции\. Еще сильнее картину портят макросы: стоит перед включением заголовка добавить объявление макроса, как его содержимое может в корне измениться\. 

Рассмотрим пару способов, как можно уменьшить число зависимостей\.

Способ N1: убирайте неиспользуемые включения\. Не надо платить за то, что вы не используете\. Так вы сокращаете работу как препроцессору, так и компилятору\. Можно как вручную "перелопатить" заголовки/исходные файлы, так и воспользоваться утилитами: [include\-what\-you\-use](https://include-what-you-use.org/), [ReSharper C\+\+](https://www.jetbrains.com/resharper-cpp/), [CppClean](https://code.google.com/archive/p/cppclean/), [Doxygen \+ Graphviz](https://www.doxygen.nl/manual/diagrams.html) \(для визуализации диаграммы включений\) и т\.д\.

Способ N2: используйте зависимость от объявления, а не от определения\. Выделим 2 главных аспекта:

1\) В заголовочных файлах не используйте объекты там, где можно воспользоваться ссылками или указателями\. Для ссылок и указателей достаточно опережающего объявления, поскольку компилятор знает размер ссылки/указателя \(4 или 8 байт в зависимости от платформы\), а размер передаваемых объектов не имеет значения\. Простой пример:

```cpp
// Foo.h
#pragma once
class Foo
{
  ....
};

// Bar.h
#pragma once
#include "Foo.h"
class Bar
{
  void foo(Foo obj);  // <= Передача по значению
  ....
};
```

Теперь, при изменении первого заголовка компилятору придется перекомпилировать единицы трансляции, зависимые как от _Foo\.h_, так и _Bar\.h_\. 

Чтобы разорвать подобную связь, достаточно отказаться от передачи объекта _obj_ по значению в пользу передачи по указателю или ссылке в заголовке _Bar\.h_:

```cpp
// Bar.h
#pragma once
class Foo;     // <= Опережающее объявление класса Foo
class Bar
{
  void foo(const Foo &obj);  // <= Передача по константной ссылке
  ....
};
```

Существует также еще один факт, о котором мало кто помнит или знает: вы даже можете объявлять функции, возвращающие и принимающие объекты по значению \(\!\!\!\), достаточно лишь добавить перед этим опережающее объявление\. Важно лишь помнить – перед вызовом такой функции вы обязаны предоставить определение класса, которое использует функция\. Такой пример скомпилируется:

```cpp
// Bar.h
#pragma once
class Foo;     // <= Опережающее объявление класса Foo
class Bar
{
  Foo foo(const Foo obj);  // <= Передача константного объекта
                           //    по значению
  ....
};
```

Что касается стандартных заголовков, то здесь можно волноваться поменьше и просто включать их в заголовочный файл при необходимости\. Исключением является разве что _iostream_\. Этот заголовочный файл настолько вырос в размерах, что к нему дополнительно поставляется заголовок [_iosfwd_](https://en.cppreference.com/w/cpp/header/iosfwd), содержащий только опережающие объявления нужных сущностей\. Именно его и стоит включать в ваши заголовочные файлы\.

2\) Используйте идиомы [Pimpl](https://en.cppreference.com/w/cpp/language/pimpl) или интерфейсного класса\. Pimpl убирает детали реализации, помещая их в отдельный класс, объект которого доступен через указатель\. Второй подход основан на создании абстрактного базового класса, детали реализации которого переносятся в производный класс, переопределяющем чистые виртуальные функции\. Оба варианта устраняют зависимости на этапе компиляции, но также вносят свои накладные расходы во время работы программы, а именно: создание и удаление динамического объекта, добавление уровня косвенной адресации \(из\-за указателя\); и отдельно в случае интерфейсного класса \- расходы на вызов виртуальных функций\.

Способ N3 \(опционально\): дополнительно можно создавать заголовки, содержащие только опережающие объявления \(аналог [_iosfwd_](https://en.cppreference.com/w/cpp/header/iosfwd)\)\. Эти "опережающие" заголовки затем можно включать в другие обычные заголовки\.

### Параллельная компиляция

При стандартном подходе компилятору раз за разом будет поступать новый файл для препроцессирования и компиляции\. Поскольку каждая единица трансляции самодостаточна, то хороший способ ускорения \- распараллелить фазы I\-II трансляции, обрабатывая одновременно N файлов за раз\.

В Visual Studio режим включается флагом _/MP\[processMax\]_ на уровне проекта, где _processMax_ \- опциональный аргумент, отвечающий за максимальное количество процессов компиляции\.

В make режим включается флагом _\-jN_, где _N_ \- число процессов компиляции\.

Если вы используете CMake \(к тому же и в кросс\-платформенной разработке\), то им можно сгенерировать файлы для [обширного списка сборочных систем](https://cmake.org/cmake/help/latest/manual/cmake-generators.7.html?highlight=generators) через флаг _\-G_\. Например, CMake генерирует для C\+\+ анализатора PVS\-Studio решение для Visual Studio под Windows, так и Unix Makefiles под Linux\. Чтобы CMake генерировал проекты в решении Visual Studio с флагом _/MP_, добавьте следующие строки в ваш _CMakeLists\.txt_:

```cpp
if (MSVC)
    target_compile_options(target_name /MP ...)
endif()
```

Также через CMake \(с версии 2\.8\.0\) можно позвать сборочную систему с флагами параллелизации\. Для MSVC \(_/MP_ указан в CMakeLists\.txt\) и Ninja \(параллелизм уже включен\):

```cpp
cmake --build /path/to/build-dir
```

Для Makefiles:

```cpp
cmake --build /path/to/build-dir -- -jN
```

### Распределенная компиляция

Воспользовавшись предыдущим советом, можно в разы снизить время сборки\. Однако, когда проект огромен, и этого может быть недостаточно\. Увеличивая число процессов компиляции, вы натыкаетесь на барьер в виде максимального числа одновременно компилируемых файлов из\-за процессора/оперативной памяти/дисковых операций\. Здесь и приходит на помощь распределенная компиляция, использующая свободные ресурсы товарища за спиной\. Идея проста: 

1\) препроцессируем исходные файлы на одной локальной машине или на всех доступных машинах;

2\) компилируем препроцессированные файлы на локальной и на удаленных машинах;

3\) ожидаем результата от других машин в виде объектных файлов;

4\) компонуем объектные файлы;

5\) ????

6\) PROFIT\!

Выделим основные особенности распределенной компиляции:

* Масштабируемость \- подцепляем машину, и теперь она может помогать в сборке\.
* Эффективность распределенной компиляции зависит от производительности сети и каждой машины\. Крайне рекомендуется схожая производительность каждой машины\.
* Необходимость в идентичности окружения на всех машинах \(версии компиляторов, библиотек и т\.д\.\)\. Это особенно необходимо, если препроцессирование происходит на всех машинах\.

Наиболее известными представителями являются:

* [Incredibuild](https://www.incredibuild.com/)
* [distcc](https://github.com/distcc/distcc)
* [Icecream](https://github.com/icecc/icecream)

В Linux можно достаточно легко интегрировать distcc и Icecream несколькими способами:

1\) Универсальный, через символическую ссылку \(symlink\)

```cpp
mkdir -p /opt/distcc/bin # или /opt/icecc/bin
ln -s /usr/bin/distcc /opt/distcc/bin/gcc
ln -s /usr/bin/distcc /opt/distcc/bin/g++
export PATH=/opt/distcc/bin:$PATH
```

2\) Для CMake, начиная с версии 3\.4

```cpp
cmake -DCMAKE_CXX_COMPILER_LAUNCHER=/usr/bin/distcc /path/to/CMakeDir
```

### Кэш компилятора

Другим способом уменьшить время сборки является применение кэша компилятора\. Немного изменим фазу II трансляции кода:

![0549_Reducing_build_time_ru/image3.png](https://import.viva64.com/docx/blog/0549_Reducing_build_time_ru/image3.png)

Теперь при компиляции препроцессированного файла на основе его содержимого, флагов компиляции, вывода компилятора вычисляется хэш\-значение \(учитывает флаги компиляции\)\. Впоследствии хэш\-значение и соответствующий ему объектный файл регистрируется в кэше компилятора\. При повторной компиляции с теми же флагами неизмененного файла из кэша будет взят уже готовый объектный файл и подан на вход компоновщика\.

Что можно использовать:

* Для Unix\-подобных: [ccache](https://ccache.dev/) \(GCC, Clang\), [cachecc1](http://cachecc1.sourceforge.net/) \(GCC\)\.
* Для Windows: [clcache](https://github.com/frerich/clcache) \(MSVC\), [cclash](https://github.com/inorton/cclash) \(MSVC\)\.

Регистрацию ccache для его последующего использования можно произвести несколькими способами:

1\) Универсальный, через символическую ссылку

```cpp
mkdir -p /opt/ccache/bin
ln -s /usr/bin/ccache /opt/ccache/bin/gcc
ln -s /usr/bin/ccache /opt/ccache/bin/g++
export PATH=/opt/ccache/bin:$PATH
```

2\) Для CMake, начиная с версии 3\.4

```cpp
cmake -DCMAKE_CXX_COMPILER_LAUNCHER=/usr/bin/ccache /path/to/CMakeDir
```

Кэш компилятора также можно интегрировать в распределенную компиляцию\. Например, для использования ccache с distcc/Icecream, выполните следующие действия:

1\) Установите переменную _CCACHE\_PREFIX_:

```cpp
export CCACHE_PREFIX=distcc # или icecc
```

2\) Воспользуйтесь одним из пунктов 1 \- 2 регистрации ccache\.

### Предварительно откомпилированные заголовочные файлы

При компиляции большого количества исходных файлов компилятор, по факту, выполняет множество раз одну и ту же работу по разбору тяжеловесных заголовков \(например, _iostream_\)\. Основная идея заключается в том, чтобы вынести эти тяжеловесные заголовки в отдельный файл \(обычно именуется префиксным заголовком\), который компилируется единожды и затем включается во все единицы трансляции **самым первым**\.

В MSVC для создания предкомпилированного заголовка по умолчанию генерируются 2 файла: _stdafx\.h_ и _stdafx\.cpp_ \(можно использовать и другие имена\)\. Первым шагом необходимо скомпилировать _stdafx\.cpp_ с флагом _/Yc"path\-to\-stdafx\.h"_\. По умолчанию создается файл с расширением _\.pch_\. Чтобы использовать предкомпилированный заголовок при компиляции исходного файла используем флаг _/Yu"path\-to\-stdafx\.h"_\. Совместно с флагами _/Yc_ и _/Yu_ также можно использовать _/Fp"path\-to\-pch"_ для указания пути к _\.pch_ файлу\. Теперь необходимо подключить в каждой единице трансляции префиксный заголовок самым первым: либо непосредственно через _\#include "path\-to\-stdafx\.h"_, либо принудительно через флаг _/FI"path\-to\-stdafx\.h"_\.

Подход в GCC/Clang отличается немногим: необходимо передать компилятору на вход непосредственно сам префиксный заголовок вместо обычного компилируемого файла\. Компилятор автоматически выполнит генерацию предкомпилированного заголовка с расширением _\.gch_ по умолчанию\. При помощи ключа _\-x_ можно дополнительно указать, рассматривать ли его как _c\-header_ или _c\+\+\-header_\. Теперь включите префиксный заголовок вручную через _\#include_, либо через флаг _\-include_\.

Подробно о предварительно откомпилированных заголовках вы можете прочитать [здесь](https://pvs-studio.ru/ru/blog/posts/cpp/0265/)\.

Если вы используете CMake, то рекомендуем попробовать модуль [cotire](https://github.com/sakra/cotire/blob/master/MANUAL.md): он может в автоматическом режиме проанализировать исходные файлы, сгенерировать префиксный и предкомпилированный заголовки и подключить их к единицам трансляции\. Есть также возможность указать свой префиксный заголовок \(например, _stdafx\.h_\)\.

### Single Compilation Unit

Суть данного метода \- создать единый компилируемый файл \(блок трансляции\), в который включаются другие единицы трансляции:

```cpp
// SCU.cpp
#include "translation_unit1.cpp"
....
#include "translation_unitN.cpp"
```

Если в единый компилируемый файл включаются все единицы трансляции, то такой способ иначе называют _Unity build_\. Выделим основные особенности Single Compilation Unit:

* Число компилируемых файлов заметно уменьшается, а значит, и число дисковых операций\. Компилятор гораздо меньше обрабатывает одни и те же файлы и инстанцирует шаблоны\. Это заметно отражается на времени сборки\.
* Компилятор теперь может выполнять оптимизации, доступные компоновщику \(Link time optimization/Whole program optimization\)\.
* Несколько ухудшается инкрементальная сборка, поскольку изменение одного файла в составе Single Compilation Unit приводит к его перекомпиляции\.
* При применении Unity Build становится невозможным использовать распределенную сборку\.

Отметим возможные проблемы при применении подхода:

* Нарушение ODR \(совпадение имен макросов, локальных статических функций, глобальных статических переменных, переменных в анонимных пространствах имен\)\.
* Коллизии имен вследствие применения _using namespace_\.

Максимальную выгоду на многоядерных системах будут давать схемы:

* параллельной компиляции нескольких Single Compilation Unit с применением предкомпилированного заголовка; 
* распределенной компиляции нескольких Single Compilation Unit с применением кэша компилятора\.

### Замена компонентов трансляции

Замена одного из компонентов трансляции на более быстрый аналог также может увеличить скорость сборки\. Однако, делать это стоит на свой страх и риск\.

В качестве более быстрого компилятора можно воспользоваться [Zapcc](https://github.com/yrnkrn/zapcc)\. Авторы обещают многократное ускорение перекомпиляции проектов\. Это можно проследить на примере перекомпиляции _Boost\.Math_ \(нажмите на анимацию для увеличения\):

![0549_Reducing_build_time_ru/image5.gif](https://import.viva64.com/docx/blog/0549_Reducing_build_time_ru/image5.gif)

Zapcc не жертвует производительностью программ, основан на Clang и полностью с ним совместим\. [Здесь](http://lists.llvm.org/pipermail/cfe-dev/2015-May/043155.html) можно ознакомиться с принципом работы Zapcc\. Если ваш проект основан на CMake, то заменить компилятор очень легко:

```cpp
export CC=/path/to/zapcc
export CXX=/path/to/zapcc++
cmake /path/to/CMakeDir
```

или так:

```cpp
cmake -DCMAKE_C_COMPILER=/path/to/zapcc \
      -DCMAKE_CXX_COMPILER=/path/to/zapcc++ \
      /path/to/CMakeDir
```

Если ваша ОС использует ELF\-формат объектных файлов \(Unix\-подобные системы\), то можно заменить компоновщик GNU ld на GNU gold\. GNU gold идет в составе binutils, начиная с версии 2\.19, и активируется флагом _\-fuse\-ld\=gold_\. В CMake его можно активировать, например, следующим кодом:

```cpp
if (UNIX AND NOT APPLE)
  execute_process(COMMAND ${CMAKE_CXX_COMPILER} 
                  -fuse-ld=gold -Wl,--version 
                  ERROR_QUIET OUTPUT_VARIABLE ld_version)
  if ("${ld_version}" MATCHES "GNU gold")
    message(STATUS "Found Gold linker, use faster linker")
    set(CMAKE_EXE_LINKER_FLAGS 
        "${CMAKE_EXE_LINKER_FLAGS} -fuse-ld=gold")
    set(CMAKE_SHARED_LINKER_FLAGS
        "${CMAKE_SHARED_LINKER_FLAGS} -fuse-ld=gold ")
  endif()
endif()
```

### Использование SSD/RAMDisk

Очевидным "бутылочным горлышком" в сборке является скорость дисковых операций \(в особенности случайного доступа\)\. Перенос временных файлов проекта или его самого на более быструю память \(HDD с повышенной скоростью случайного доступа, SSD, RAID из HDD/SSD, RAMDisk\) в некоторых ситуациях может сильно помочь\.

### Модульная система в C\+\+

Большинство вышеперечисленных способов исторически возникли из\-за выбора принципа трансляции C/C\+\+ языков\. Механизм заголовочных файлов, несмотря на кажущуюся простоту, доставляет много хлопот для C/C\+\+ программистов\.

Уже достаточно продолжительное время идет обсуждение о включении модулей в стандарт C\+\+ \(и, возможно, появится в C\+\+20\)\. _Модулем_ будет считаться связанный набор единиц трансляции \(_модульная единица_\) с определенным набором внешних \(экспортируемых\) имен, называемых _интерфейсом модуля_\. Модуль будет доступен для всех импортирующих его единиц трансляции **только** через его интерфейс\. Неэкспортируемые имена помещаются в _имплементацию модуля_\.

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

В данной статье не будет детально рассматриваться устройство будущих модулей\. Если вы хотите узнать о них больше, то рекомендуем к просмотру выступление Бориса Колпакова на _CppCon 2017_ о модулях C\+\+:

<https://www.youtube.com/watch?v=E8EbDcLQAoc&feature=youtu.be>

и оставим небольшую вырезку из его выступления, где показывается разница между новым и старым подходом сборки:

<https://www.youtube.com/embed/E8EbDcLQAoc?start=206&end=318>

На сегодняшний момент компиляторы [MSVC](https://devblogs.microsoft.com/cppblog/cpp-modules-in-visual-studio-2017/), [GCC](https://gcc.gnu.org/wiki/cxx-modules), [Clang](https://clang.llvm.org/docs/Modules.html) предлагают экспериментальную поддержку модулей\.

## А что\-нибудь про сборку PVS\-Studio будет?

В этом разделе давайте рассмотрим, насколько бывают эффективными и полезными описанные подходы\. 

За основу возьмем ядро анализатора PVS\-Studio для анализа C и C\+\+ кода\. Оно, конечно же, написано на C\+\+ и представляет собой консольное приложение\. Ядро является небольшим проектом по сравнению с такими гигантами, как LLVM/Clang, GCC, Chromium и т\.д\. Вот, например, что выдает CLOC на нашей кодовой базе:

```cpp
----------------------------------------------------------------
Language          files        blank        comment         code
----------------------------------------------------------------
C++                380         28556         17574        150222
C/C++ Header       221          9049          9847         46360
Assembly             1            13            22           298
----------------------------------------------------------------
SUM:               602         37618         27443        196880
----------------------------------------------------------------
```

Отметим, что до проведения всяких работ наш проект собирался за 1\.5 минуты \(использовались параллельная компиляция и один предкомпилированный заголовок\) на следующей конфигурации рабочей машины:

* Процессор Intel Core i7\-4770 3\.4 GHz \(8 CPU\)\.
* ОЗУ 16 Gb RAM DDR3\-1333 MHz\.
* Samsung SSD 840 EVO 250 Gb в качестве системного диска\.
* WDC WD20EZRX\-00D8PB0 2 Tb под рабочие нужды\.

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

* сборка на HDD, компиляция в 1 поток, без оптимизаций;
* сборка на SSD, компиляция в 1 поток, без оптимизаций;
* сборка на RAMDisk, компиляция в 1 поток, без оптимизаций\.

![0549_Reducing_build_time_ru/image6.png](https://import.viva64.com/docx/blog/0549_Reducing_build_time_ru/image6.png)

Рисунок 1\. Сборка анализатора PVS\-Studio, 1 поток, без оптимизаций\. Сверху \- сборка Debug версии, снизу \- Release\.

Как видно из диаграммы, за счет большей скорости произвольного доступа проект на RAMDisk без оптимизаций в 1 поток собирается немного быстрей\.

Второй этап замеров \- дорабатываем напильником исходный код: удаляем ненужные включения заголовков, устраняем зависимости от определения, улучшаем предкомпилированный заголовок \(убираем из него часто изменяемые заголовки\) \- и постепенно прикручиваем оптимизации:

* компиляция в 1 поток, проект на HDD, SSD и RAMDisk:
  * single compilation units \(SCU\);
  * предкомпилированный заголовок \(PCH\);
  * single compilation units \+ предкомпилированный заголовок \(SCU \+ PCH\)\.

![0549_Reducing_build_time_ru/image7.png](https://import.viva64.com/docx/blog/0549_Reducing_build_time_ru/image7.png)

Рисунок 2\. Компиляция в 1 поток после оптимизаций\.

* компиляция в 4 потока, проект на HDD, SSD, RAMDisk:
  * SCU;
  * PCH;
  * SCU \+ PCH\.

![0549_Reducing_build_time_ru/image8.png](https://import.viva64.com/docx/blog/0549_Reducing_build_time_ru/image8.png)

Рисунок 3\. Компиляция в 4 потока после оптимизаций\.

* компиляция в 8 потоков, проект на HDD, SSD, RAMDisk:
  * SCU;
  * PCH;
  * SCU \+ PCH\.

![0549_Reducing_build_time_ru/image9.png](https://import.viva64.com/docx/blog/0549_Reducing_build_time_ru/image9.png)

Рисунок 4\. Компиляция в 8 потоков после оптимизаций\.

Сделаем краткие выводы:

* Польза от применения SSD/RAMDisk может колебаться в зависимости от их модели, скорости произвольного доступа, условий запуска, фаз луны и т\.д\. Хоть они и являются более быстрыми аналогами HDD, **конкретно в нашем случае** они не дают значительный выигрыш\.
* Предкомпилированные заголовки \- очень эффективное средство\. Этот способ и ранее использовался в нашем анализаторе, и его использование даже при компиляции в 1 поток давало 7\-8 кратное ускорение\.
* При малом числе единых блоков трансляции \(SCU\) целесообразно не создавать предкомпилированные заголовки\. Используйте предкомпилированные заголовки, когда число единых блоков трансляции достаточно велико \(\> 10\)\.

## Заключение

Для многих программистов языки C/C\+\+ ассоциируются как нечто "долго компилирующееся"\. И на это есть свои причины: выбранный в свое время способ трансляции, метапрограммирование \(для C\+\+\), тысячи их\. Благодаря описанным методам оптимизации можно лишить себя подобных предрассудков о чрезмерно долгой компиляции\. В частности, время сборки нашего ядра анализатора PVS\-Studio для анализа C и C\+\+ кода удалось снизить с 1 минуты 30 секунд до 40 секунд путем интеграции Single Compilation Units и переработки заголовочных и исходных файлов\. Более того, если бы до начала оптимизаций не были использованы параллельная компиляция и предкомпилированные заголовки, нами было бы получено семикратное уменьшение времени сборки\!

В окончание хочется добавить, что об этой проблеме прекрасно помнят в комитете стандартизации и полный ходом идет решение данной проблемы: все мы ждем нового стандарта C\+\+20, который, возможно, одним из нововведений "завезёт" модули в любимый многими язык и сделает жизнь C\+\+ программистов гораздо проще\.