Ответ 1
Скорость компиляции - это то, что действительно можно повысить, если вы знаете, как это сделать. Всегда разумно тщательно продумывать дизайн проекта (особенно в случае больших проектов, состоящих из нескольких модулей) и модифицировать его, чтобы компилятор мог эффективно выводить результаты.
1. Предварительно скомпилированные заголовки.
Предварительно скомпилированный заголовок - это обычный заголовок (файл .h
), который содержит наиболее распространенные объявления, typedefs и include. Во время компиляции он анализируется только один раз - перед компиляцией любого другого источника. Во время этого процесса компилятор генерирует данные некоторого внутреннего (наиболее вероятного, двоичного) формата. Затем он использует эти данные для ускорения генерации кода.
Это образец:
#pragma once
#ifndef __Asx_Core_Prerequisites_H__
#define __Asx_Core_Prerequisites_H__
//Include common headers
#include "BaseConfig.h"
#include "Atomic.h"
#include "Limits.h"
#include "DebugDefs.h"
#include "CommonApi.h"
#include "Algorithms.h"
#include "HashCode.h"
#include "MemoryOverride.h"
#include "Result.h"
#include "ThreadBase.h"
//Others...
namespace Asx
{
//Forward declare common types
class String;
class UnicodeString;
//Declare global constants
enum : Enum
{
ID_Auto = Limits<Enum>::Max_Value,
ID_None = 0
};
enum : Size_t
{
Max_Size = Limits<Size_t>::Max_Value,
Invalid_Position = Limits<Size_t>::Max_Value
};
enum : Uint
{
Timeout_Infinite = Limits<Uint>::Max_Value
};
//Other things...
}
#endif /* __Asx_Core_Prerequisites_H__ */
В проекте, когда используется PCH, каждый исходный файл обычно содержит #include
к этому файлу (я не знаю о других, но в VC++ это фактически требование - каждый источник, подключенный к проекту, настроен для использования PCH, должен начинаться с: #include PrecompiledHedareName.h
). Конфигурация предварительно скомпилированных заголовков очень зависит от платформы и выходит за рамки этого ответа.
Обратите внимание на один важный момент: вещи, которые определены/включены в PCH, следует менять только тогда, когда это абсолютно необходимо - каждый chnge может вызвать перекомпиляцию всего проекта (и других зависимых модулей)!
Подробнее о PCH:
Wiki
Док GCC
Документ Microsoft
2. Форвардные декларации.
Если вам не нужно полное определение класса, перешлите его, чтобы удалить ненужные зависимости в вашем коде. Это также подразумевает широкое использование указателей и ссылок, когда это возможно. Пример:
#include "BigDataType.h"
class Sample
{
protected:
BigDataType _data;
};
Вам действительно нужно хранить _data
как значение? Почему бы не так:
class BigDataType; //That enough, #include not required
class Sample
{
protected:
BigDataType* _data; //So much better now
};
Это особенно выгодно для крупных типов.
3. Не злоупотребляйте шаблонами.
Мета-программирование - очень мощный инструмент в наборе инструментов для разработчиков. Но не пытайтесь использовать их, когда они не нужны.
Они отлично подходят для таких вещей, как черты характера, оценка во время компиляции, статическое отражение и так далее. Но они приносят много неприятностей:
- Сообщения об ошибках - если вы когда-либо видели ошибки, вызванные неправильным использованием итераторов или контейнеров
std::
(особенно сложных, таких какstd::unordered_map
), чем вы знаете, что это такое. - Удобочитаемость - сложные шаблоны могут быть очень сложными для чтения/изменения/обслуживания.
- Причуды - многие методы, для которых используются шаблоны, не так хорошо известны, поэтому обслуживание такого кода может быть еще сложнее.
- Время компиляции - самое важное для нас сейчас:
Помните, если вы определяете функцию как:
template <class Tx, class Ty>
void sample(const Tx& xv, const Ty& yv)
{
//body
}
он будет скомпилирован для каждой исключительной комбинации Tx
и Ty
. Если такая функция используется часто (и для многих таких комбинаций), она действительно может замедлить процесс компиляции. Теперь представьте, что произойдет, если вы начнете чрезмерно использовать шаблоны для целых классов...
4. Использование идиомы PIMPL.
Это очень полезный метод, который позволяет нам:
- скрыть детали реализации
- ускорить генерацию кода
- простые обновления, не нарушая код клиента
Как это работает? Рассмотрим класс, который содержит много данных (например, представляющих человека). Это может выглядеть так:
class Person
{
protected:
string name;
string surname;
Date birth_date;
Date registration_date;
string email_address;
//and so on...
};
Наше приложение развивается, и нам нужно расширить/изменить определение Person
. Мы добавляем некоторые новые поля, удаляем другие... и все падает: размер Персоны меняется, имена полей меняются... Катаклизм. В частности, каждый клиентский код, который зависит от определения Person
, должен быть изменен/обновлен/исправлен. Не хорошо.
Но мы можем сделать это умным способом - скрыть детали Person:
class Person
{
protected:
class Details;
Details* details;
};
Теперь мы делаем несколько приятных вещей:
- клиент не может создать код, который зависит от того, как определен
Person
- перекомпиляция не требуется, если мы не изменяем открытый интерфейс, используемый клиентским кодом
- мы сокращаем время компиляции, потому что определения
string
иDate
больше не должны присутствовать (в предыдущей версии нам приходилось включать соответствующие заголовки для этих типов, что добавляет дополнительные зависимости).
Несмотря на то, что это не может повысить скорость, оно более четкое и менее подвержено ошибкам. По сути, это то же самое, что использование include guard:
#ifndef __Asx_Core_Prerequisites_H__
#define __Asx_Core_Prerequisites_H__
//Content
#endif /* __Asx_Core_Prerequisites_H__ */
Предотвращает многократный анализ одного и того же файла. Хотя #pragma once
не является стандартным (на самом деле, прагма отсутствует - прагмы зарезервированы для директив, специфичных для компилятора), он довольно широко поддерживается (примеры: VC++, GCC, CLang, ICC) и может использоваться, не беспокоясь - компиляторы должны игнорировать неизвестные прагмы (более или менее тихо).
6. Устранение ненужных зависимостей.
Очень важный момент! Когда код подвергается рефакторингу, зависимости часто меняются. Например, если вы решите провести некоторую оптимизацию и использовать указатели/ссылки вместо значений (см. пункт 2 и 4 этого ответа), некоторые включения могут стать ненужными. Рассмотрим:
#include "Time.h"
#include "Day.h"
#include "Month.h"
#include "Timezone.h"
class Date
{
protected:
Time time;
Day day;
Month month;
Uint16 year;
Timezone tz;
//...
};
Этот класс был изменен, чтобы скрыть детали реализации:
//These are no longer required!
//#include "Time.h"
//#include "Day.h"
//#include "Month.h"
//#include "Timezone.h"
class Date
{
protected:
class Details;
Details* details;
//...
};
Хорошо бы отслеживать такие избыточные включения, используя либо мозг, встроенные инструменты (такие как VS Dependency Visualizer) или внешние утилиты (например, GraphViz).
Visual Studio также имеет очень хороший вариант - если вы щелкнете по RMB по любому файлу, вы увидите опцию "Создать график включаемых файлов" - он сгенерирует хороший, читаемый график, который можно легко проанализировать и использовать для отслеживания ненужных зависимостей.
Пример графика, созданного в моем файле String.h
: