Какие методы можно использовать для ускорения времени компиляции С++?
Какие методы можно использовать для ускорения времени компиляции С++?
Этот вопрос возник в некоторых комментариях к вопросу о переполнении стека стиль программирования на С++, и мне интересно узнать, какие идеи существуют.
Я видел связанный с этим вопрос, Почему компиляция С++ занимает так много времени?, но это не дает много решений.
Голосовать здесь есть поддержка Visual Studio для совместного использования предварительно скомпилированных заголовков между проектами
Ответы
Ответ 1
Языковые методы
Идиома Pimpl
Взгляните на идиома Pimpl здесь, и здесь, также известный как непрозрачный указатель или обрабатывающие классы, Это не только ускоряет компиляцию, но и повышает безопасность исключений в сочетании с функцией не-бросания swap. Идиома Pimpl позволяет уменьшить зависимость между заголовками и уменьшить количество перекомпиляции, которая должна быть выполнена.
Передовые декларации
По возможности используйте передовые объявления. Если компилятор должен знать только, что SomeIdentifier
- это структура или указатель или что-то еще, не включайте полное определение, заставляя компилятор делать больше работы, чем нужно. Это может иметь каскадный эффект, делая это медленнее, чем они должны быть.
Потоки I/O особенно известны замедлением сборки. Если они вам нужны в файле заголовка, попробуйте #include <iosfwd>
вместо <iostream>
и # включить заголовок <iostream>
только в файле реализации. Заголовок <iosfwd>
содержит только декларации вперед. К сожалению, другие стандартные заголовки не имеют соответствующего заголовка объявлений.
Предпочитайте передачу по ссылке на значение pass-by-value в сигнатурах функций. Это устранит необходимость # включить соответствующие определения типов в файл заголовка, и вам нужно будет только переслать-объявить тип. Конечно, предпочитайте ссылки const на неконстантные ссылки, чтобы избежать неясных ошибок, но это проблема для другого вопроса.
Условия охраны
Используйте условия защиты, чтобы файлы заголовков не включались более одного раза в единый блок перевода.
#pragma once
#ifndef filename_h
#define filename_h
// Header declarations / definitions
#endif
Используя как прагму, так и ifndef, вы получаете переносимость простого решения макросов, а также оптимизацию скорости компиляции, которую могут выполнять некоторые компиляторы в присутствии директивы pragma once
.
Уменьшить взаимозависимость
Чем более модульным и менее взаимозависимым будет ваш дизайн кода, тем реже вам придется перекомпилировать все. Вы также можете сократить объем работы, которую должен выполнять компилятор на любом отдельном блоке, в силу того, что он имеет меньше возможностей отслеживать.
Параметры компилятора
Предварительно скомпилированные заголовки
Они используются для компиляции общего раздела включенных заголовков один раз для многих единиц перевода. Компилятор компилирует его один раз и сохраняет его внутреннее состояние. Затем это состояние можно быстро загрузить, чтобы начать работу с компиляцией другого файла с тем же набором заголовков.
Будьте внимательны, если вы включите в предварительно скомпилированные заголовки редко изменяемые вещи, или вы можете сделать полные перестройки чаще, чем это необходимо. Это хорошее место для заголовков STL и других библиотек.
ccache - еще одна утилита, которая использует технологии кэширования для ускорения работы.
Использовать Parallelism
Многие компиляторы /IDE поддерживают использование нескольких ядер/процессоров для компиляции одновременно. В GNU Make (обычно используется с GCC) используйте параметр -j [N]
. В Visual Studio есть опция в настройках, позволяющая ей параллельно строить несколько проектов. Вы также можете использовать параметр /MP
для паралеллизма на уровне файлов, а не только паралеллизм на уровне проекта.
Другие параллельные утилиты:
Используйте более низкий уровень оптимизации
Чем больше пытается оптимизировать компилятор, тем труднее он должен работать.
Общие библиотеки
Перемещение вашего менее часто модифицированного кода в библиотеки может сократить время компиляции. Используя общие библиотеки (.so
или .dll
), вы также можете сократить время связывания.
Получить более быстрый компьютер
Больше оперативной памяти, более быстрых жестких дисков (включая SSD), и больше процессоров/ядер все будут иметь значение в скорости компиляции.
Ответ 2
Я бы рекомендовал эти статьи из "Игры изнутри, разработки игр и программирования инди":
Конечно, они довольно старые - вам придется перепроверить все с помощью последних версий (или доступных вам версий), чтобы получить реалистичные результаты. В любом случае, это хороший источник идей.
Ответ 3
Я работаю над проектом STAPL, который является сильно затененной библиотекой С++. Время от времени мы должны пересмотреть все методы, чтобы сократить время компиляции. Здесь я обобщил методы, которые мы используем. Некоторые из этих методов уже перечислены выше:
Поиск наиболее трудоемких разделов
Несмотря на отсутствие доказанной корреляции между длинами символов и временем компиляции, мы заметили, что меньшие средние размеры символов могут улучшить время компиляции для всех компиляторов. Итак, ваши первые цели - найти самые большие символы в вашем коде.
Способ 1 - Сортировка символов на основе размера
Вы можете использовать команду nm
для отображения символов на основе их размеров:
nm --print-size --size-sort --radix=d YOUR_BINARY
В этой команде --radix=d
позволяет видеть размеры в десятичных числах (по умолчанию - hex). Теперь, посмотрев на самый большой символ, определите, можете ли вы сломать соответствующий класс и попытаться его переделать, факторизуя не templated части в базовом классе или разделив класс на несколько классов.
Способ 2 - Сортировка символов на основе длины
Вы можете запустить обычную команду nm
и передать ее в ваш любимый script (AWK, Python и т.д.) для сортировки символов на основе их длины. Основываясь на нашем опыте, этот метод определяет наибольшие проблемы с кандидатами лучше, чем метод 1.
Способ 3 - Использовать Templight
" Templight является Clang инструмент для профилирования потребления времени и памяти для экземпляров шаблонов и для выполнения интерактивных сеансов отладки, чтобы получить интроспекцию в процессе создания шаблона ".
Вы можете установить Templight, проверив LLVM и Clang ( инструкции) и применения патча Templight на нем. Значение по умолчанию для LLVM и Clang - это отладка и утверждения, и это может значительно повлиять на время вашей компиляции. Кажется, что Templight нуждается в обоим, поэтому вам нужно использовать настройки по умолчанию. Процесс установки LLVM и Clang должен занимать около часа или около того.
После применения патча вы можете использовать templight++
, который находится в папке сборки, указанной вами при установке, для компиляции вашего кода.
Убедитесь, что templight++
находится в вашем PATH. Теперь для компиляции добавьте следующие ключи к вашему CXXFLAGS
в Makefile или в параметры командной строки:
CXXFLAGS+=-Xtemplight -profiler -Xtemplight -memory -Xtemplight -ignore-system
или
templight++ -Xtemplight -profiler -Xtemplight -memory -Xtemplight -ignore-system
После компиляции вы будете иметь .trace.memory.pbf и .trace.pbf, сгенерированные в той же папке. Чтобы визуализировать эти трассы, вы можете использовать Templight Tools, которые могут конвертировать их в другие форматы. Следуйте этим инструкциям для установки templight-convert. Обычно мы используем вывод callgrind. Вы также можете использовать вывод GraphViz, если ваш проект невелик:
$ templight-convert --format callgrind YOUR_BINARY --output YOUR_BINARY.trace
$ templight-convert --format graphviz YOUR_BINARY --output YOUR_BINARY.dot
Сгенерированный файл callgrind можно открыть с помощью kcachegrind, в котором вы можете трассировать наибольшее количество времени и памяти.
Уменьшение количества экземпляров шаблона
Хотя нет точного решения для уменьшения количества экземпляров шаблонов, существует несколько рекомендаций, которые могут помочь:
Классы рефракторов с более чем одним аргументом шаблона
Например, если у вас есть класс,
template <typename T, typename U>
struct foo { };
и оба из T
и U
могут иметь 10 различных параметров, вы увеличили возможные экземпляры экземпляра этого класса до 100. Один из способов решения этой проблемы - отделить общую часть кода от другого класса, Другим методом является использование инверсии наследования (изменение иерархии классов), но перед использованием этой техники убедитесь, что ваши цели дизайна не скомпрометированы.
Неформованный код Refactor для отдельных единиц перевода
Используя эту технику, вы можете скомпилировать общий раздел один раз и позже связать его с другими вашими модулями (единицами перевода).
Использовать экземпляры шаблонов extern (начиная с С++ 11)
Если вы знаете все возможные экземпляры класса, вы можете использовать этот метод для компиляции всех случаев в другой единицы перевода.
Например, в:
enum class PossibleChoices = {Option1, Option2, Option3}
template <PossibleChoices pc>
struct foo { };
Мы знаем, что этот класс может иметь три возможных экземпляра:
template class foo<PossibleChoices::Option1>;
template class foo<PossibleChoices::Option2>;
template class foo<PossibleChoices::Option3>;
Поместите это в блок переводов и используйте ключевое слово extern в своем файле заголовка ниже определения класса:
extern template class foo<PossibleChoices::Option1>;
extern template class foo<PossibleChoices::Option2>;
extern template class foo<PossibleChoices::Option3>;
Этот метод может сэкономить ваше время, если вы компилируете различные тесты с помощью общего набора экземпляров.
ПРИМЕЧАНИЕ. MPICH2 игнорирует явное инстанцирование в этой точке и всегда компилирует инстанцированные классы во всех единицах компиляции.
Использовать единичные сборки
Вся идея создания единства состоит в том, чтобы включить все файлы .cc, которые вы используете в одном файле, и скомпилировать этот файл только один раз. Используя этот метод, вы можете избежать повторного использования общих разделов разных файлов, и если ваш проект включает в себя множество распространенных файлов, вы, вероятно, сохранили бы и на обращении к диску.
В качестве примера предположим, что у вас есть три файла foo1.cc
, foo2.cc
, foo3.cc
, и все они включают tuple
из STL. Вы можете создать foo-all.cc
, который выглядит так:
#include "foo1.cc"
#include "foo2.cc"
#include "foo3.cc"
Вы компилируете этот файл только один раз и потенциально уменьшаете общие экземпляры этих трех файлов. Трудно вообще предсказать, может ли улучшение быть значительным или нет. Но один очевидный факт заключается в том, что вы потеряете parallelism в своих сборках (вы больше не можете скомпилировать три файла одновременно).
Кроме того, если какой-либо из этих файлов занимает много памяти, у вас может закончиться нехватка памяти до завершения компиляции. В некоторых компиляторах, таких как GCC, это может привести к нарушению компилятора ICE (Internal Compiler Error) из-за нехватки памяти. Поэтому не используйте эту технику, если вы не знаете все плюсы и минусы.
Предварительно скомпилированные заголовки
Предварительно скомпилированные заголовки (PCH) могут сэкономить вам много времени при компиляции, скомпилировав ваши файлы заголовков в промежуточное представление, распознаваемое компилятором. Чтобы генерировать предварительно скомпилированные файлы заголовков, вам нужно только скомпилировать ваш файл заголовка с помощью вашей обычной команды компиляции. Например, в GCC:
$ g++ YOUR_HEADER.hpp
Это приведет к созданию YOUR_HEADER.hpp.gch file
(.gch
- расширение для файлов PCH в GCC) в той же папке. Это означает, что если вы включите YOUR_HEADER.hpp
в какой-либо другой файл, компилятор будет использовать ваш YOUR_HEADER.hpp.gch
вместо YOUR_HEADER.hpp
в той же папке раньше.
В этой технике есть две проблемы:
- Вы должны убедиться, что файлы заголовков, предварительно скомпилированные, являются стабильными и не будут меняться (вы всегда можете изменить свой файл) <
- Вы можете включить только один PCH на блок компиляции (на большинстве компиляторов). Это означает, что если у вас есть несколько файлов заголовков, которые должны быть предварительно скомпилированы, вы должны включить их в один файл (например,
all-my-headers.hpp
). Но это означает, что вы должны включать новый файл во все места. К счастью, у GCC есть решение этой проблемы. Используйте -include
и дайте ему новый заголовочный файл. Вы можете запятовать отдельные файлы, используя эту технику.
Например:
g++ foo.cc -include all-my-headers.hpp
Использовать неназванные или анонимные пространства имен
пространства имен имен (анонимные пространства имен a.k.a.) могут значительно уменьшить генерируемые двоичные размеры. В пространствах имен имен используется внутренняя связь, что означает, что символы, сгенерированные в этих пространствах имен, не будут видны другим TU (единицам перевода или компиляции). Компиляторы обычно генерируют уникальные имена для неназванных пространств имен. Это означает, что если у вас есть файл foo.hpp:
namespace {
template <typename T>
struct foo { };
} // Anonymous namespace
using A = foo<int>;
И вы включаете этот файл в два TU (два .cc файла и компилируете их отдельно). Два экземпляра шаблона foo не будут одинаковыми. Это нарушает Одно правило определения (ODR). По той же причине использование неименованных пространств имен не рекомендуется в файлах заголовков. Не стесняйтесь использовать их в своих файлах .cc
, чтобы избежать появления символов в ваших двоичных файлах. В некоторых случаях изменение всех внутренних деталей файла .cc
показало уменьшение на 10% генерируемых двоичных размеров.
Изменение параметров видимости
В новых компиляторах вы можете выбрать, чтобы ваши символы были либо видимыми, либо невидимыми в динамических общих объектах (DSO). В идеале изменение видимости может улучшить производительность компилятора, оптимизировать время соединения (LTO) и порождать двоичные размеры. Если вы посмотрите на заголовочные файлы STL в GCC, вы увидите, что он широко используется. Чтобы включить видимость, вам нужно изменить свой код на каждую функцию, на класс, на переменную и, что более важно, на каждого компилятора.
С помощью видимости вы можете скрыть символы, которые считаете их конфиденциальными, из сгенерированных общих объектов. В GCC вы можете контролировать видимость символов, передавая по умолчанию или скрытые параметры -visibility
вашего компилятора. Это в некотором смысле похоже на неназванное пространство имен, но более сложным и навязчивым способом.
Если вы хотите указать видимость для каждого случая, вы должны добавить следующие атрибуты к своим функциям, переменным и классам:
__attribute__((visibility("default"))) void foo1() { }
__attribute__((visibility("hidden"))) void foo2() { }
__attribute__((visibility("hidden"))) class foo3 { };
void foo4() { }
Видимость по умолчанию в GCC по умолчанию (общедоступная), что означает, что если вы скомпилируете вышеописанный метод совместно используемой библиотеки (-shared
), foo2
и class foo3
не будут отображаться в других TU (foo1
и foo4
будут видны). Если вы компилируете с помощью -visibility=hidden
, тогда будет отображаться только foo1
. Даже foo4
будет скрыто.
Подробнее о видимости можно узнать в GCC wiki.
Ответ 4
Здесь есть целая книга по этой теме, которая называется крупномасштабная разработка программного обеспечения на C++ (написанная Джоном Лакосом).
Шаблоны предварительных дат книги, поэтому к содержанию этой книги добавить "использование шаблонов тоже может сделать компилятор медленнее".
Ответ 5
Я просто свяжусь с моим другим ответом: Как вы сокращаете время компиляции и связываете время для проектов Visual С++ (собственный С++)?. Еще один момент, который я хочу добавить, но часто вызывает проблемы с использованием предварительно скомпилированных заголовков. Но, пожалуйста, используйте их только для частей, которые вряд ли когда-либо меняются (например, заголовки инструментов GUI). В противном случае, они будут стоить вам больше времени, чем они спасут вас в конце.
Другим вариантом является, когда вы работаете с GNU make, включить параметр -j<N>
:
-j [N], --jobs[=N] Allow N jobs at once; infinite jobs with no arg.
Я обычно имею его в 3
, так как здесь у меня есть двойное ядро. Затем он будет запускать компиляторы параллельно для разных единиц перевода, если между ними нет зависимостей. Связывание не может выполняться параллельно, так как существует только один процесс компоновщика, связывающий все объектные файлы.
Но сам компоновщик может быть потоковым, и это то, что GNU gold
ELF. Он оптимизировал потоковый код на С++, который, как говорят, связывает файлы объектов ELF на величину быстрее старой ld
(и фактически был включен в binutils).
Ответ 6
Один метод, который работал у меня в прошлом: не скомпилируйте несколько исходных файлов С++ самостоятельно, а скорее создайте один файл С++, который включает в себя все остальные файлы, например:
// myproject_all.cpp
// Automatically generated file - don't edit this by hand!
#include "main.cpp"
#include "mainwindow.cpp"
#include "filterdialog.cpp"
#include "database.cpp"
Конечно, это означает, что вам нужно перекомпилировать весь исходный код, если какой-либо из источников изменится, поэтому дерево зависимостей ухудшается. Однако компиляция нескольких исходных файлов как одной единицы перевода выполняется быстрее (по крайней мере, в моих экспериментах с MSVC и GCC) и генерирует более мелкие двоичные файлы. Я также подозреваю, что компилятор получает больше возможностей для оптимизации (поскольку он может видеть больше кода сразу).
Эта техника ломается в различных случаях; например, компилятор выйдет из строя, если два или более исходных файла объявят глобальную функцию с тем же именем. Я не мог найти эту технику, описанную в любом из других ответов, но почему я упоминаю ее здесь.
Для того, что стоит, KDE Project использовал этот точный метод с 1999 года для создания оптимизированных двоичных файлов (возможно, для выпуска). Переключение на конфигурацию сборки script было вызвано --enable-final
. Из археологических интересов я выкопал сообщение, в котором была анонсирована эта функция: http://lists.kde.org/?l=kde-devel&m=92722836009368&w=2
Ответ 7
Вот некоторые из них:
- Используйте все процессорные ядра, запустив задание с несколькими компиляторами (
make -j2
- хороший пример).
- Выключите или уменьшите оптимизацию (например, GCC намного быстрее с
-O1
, чем -O2
или -O3
).
- Используйте предварительно скомпилированные заголовки.
Ответ 8
Как только вы применили все трюки кода выше (forward declarations, уменьшив включение заголовка до минимума в публичных заголовках, нажав большинство деталей внутри файла реализации с Pimpl...), и ничто другое не может быть получено по-разному, подумайте о своей системе сборки. Если вы используете Linux, рассмотрите возможность использования distcc (распределенный компилятор) и ccache (кэш-компилятор).
Первый, distcc, выполняет шаг препроцессора локально, а затем отправляет вывод в первый доступный компилятор в сети. Он требует одинаковых версий компилятора и библиотеки во всех настроенных узлах в сети.
Последний, ccache, является кешем компилятора. Он снова выполняет препроцессор, а затем проверяет внутреннюю базу данных (хранящуюся в локальном каталоге), если этот файл препроцессора уже скомпилирован с теми же параметрами компилятора. Если это так, оно просто выводит двоичный файл и выводится с первого запуска компилятора.
Оба могут использоваться одновременно, поэтому, если ccache не имеет локальной копии, он может отправить его через сеть в другой node с помощью distcc, иначе он может просто ввести решение без дальнейшей обработки.
Ответ 9
Когда я вышел из колледжа, первый реальный, достойный производства код С++, который я видел, имел эти архангельные директивы #ifndef... #endif между ними, где были определены заголовки. Я спросил парня, который писал код об этих всеобъемлющих вещах очень наивным образом и познакомился с миром крупномасштабного программирования.
Возвращаясь к точке, использование директив для предотвращения повторных описаний заголовков было первым, что я узнал, когда дело доходило до сокращения времени компиляции.
Ответ 10
Больше оперативной памяти.
Кто-то говорил о RAM-дисках в другом ответе. Я сделал это с помощью 80286 и Turbo С++ (показывает возраст), и результаты были феноменальными. Как и потеря данных при сбое машины.
Ответ 11
Использование
#pragma once
в верхней части заголовочных файлов, поэтому, если они включены более одного раза в блок трансляции, текст заголовка будет только включаться и анализироваться один раз.
Ответ 12
Вы можете использовать Unity Builds.
Ответ 13
Ответ 14
Используйте форвардные объявления, где сможете. Если в объявлении класса используется только указатель или ссылка на тип, вы можете просто переслать объявление и включить заголовок для типа в файле реализации.
Например:
// T.h
class Class2; // Forward declaration
class T {
public:
void doSomething(Class2 &c2);
private:
Class2 *m_Class2Ptr;
};
// T.cpp
#include "Class2.h"
void Class2::doSomething(Class2 &c2) {
// Whatever you want here
}
Меньшее число включает в себя гораздо меньше работы для препроцессора, если вы делаете это достаточно.
Ответ 15
Просто для полноты: сборка может быть медленной, потому что система сборки глупа, а также потому, что компилятор занимает много времени, чтобы выполнить свою работу.
Прочитайте Рекурсивный анализ считается вредоносным (PDF) для обсуждения этой темы в средах Unix.
Ответ 16
У меня была идея о с использованием RAM-диска. Оказалось, что для моих проектов это не так уж и важно. Но тогда они довольно маленькие. Попробуй! Мне было бы интересно узнать, насколько это помогло.
Ответ 17
Где вы проводите время? Вы связаны с процессором? Память связана? Диск связан? Можете ли вы использовать больше ядер? Больше ОЗУ? Вам нужен RAID? Вы просто хотите повысить эффективность своей текущей системы?
.
В gcc/g++ вы посмотрели ccache? Это может быть полезно, если вы делаете make_clean _; _ делаете много.
Ответ 18
Динамическое связывание (.so) может быть намного быстрее, чем статическое связывание (.a). Особенно, если у вас медленный сетевой диск. Это происходит потому, что у вас есть весь код в файле .a, который необходимо обработать и выписать. Кроме того, на диск должен быть выписан гораздо более большой исполняемый файл.
Ответ 19
Если у вас многоядерный процессор, как Visual Studio (2005, так и позже), так и GCC поддерживают многопроцессорные компиляции. Это то, что можно включить, если у вас есть оборудование, конечно.
Ответ 20
Акций сети резко сократит вашу сборку, так как латентность поиска высока. Для чего-то вроде Boost для меня это имело огромное значение, хотя наш сетевой накопитель довольно быстро. Время скомпилировать программу Boost для игрушек прошло от 1 минуты до 1 секунды, когда я переключился с сетевого ресурса на локальный SSD.
Ответ 21
Не о времени компиляции, а о времени сборки:
-
Используйте ccache, если вам нужно перестроить те же файлы, когда вы работаете
на ваших сборках
-
Используйте ninja-build вместо make. В настоящее время я составляю проект
с ~ 100 исходными файлами, и все кэшируется ccache. делать потребности
5 минут, ниндзя меньше 1.
Вы можете сгенерировать свои файлы ниндзя с cmake с помощью -GNinja
.
Ответ 22
В Linux (и, возможно, в некоторых других * NIX) вы действительно можете ускорить компиляцию, НЕ ЗАПУСКАЙТЕ на выходе и изменив на другой TTY.
Вот эксперимент: printf замедляет мою программу
Ответ 23
Хотя это не "метод", я не мог понять, как проекты Win32 со многими исходными файлами скомпилированы быстрее моего пустого проекта "Hello World". Таким образом, я надеюсь, что это поможет кому-то, как это сделал я.
В Visual Studio одним из вариантов увеличения времени компиляции является инкрементное связывание (/INCREMENTAL). Это несовместимо с генерацией кода времени линии (/LTCG), поэтому не забудьте отключить инкрементную привязку при создании релизов.
Ответ 24
Более быстрые жесткие диски.
Составители записывают на диск много (и, возможно, огромных) файлов. Работа с SSD вместо типичного жесткого диска и времени компиляции значительно ниже.