Какие методы можно использовать для ускорения времени компиляции C ++? - PullRequest
224 голосов
/ 17 декабря 2008

Какие методы могут быть использованы для ускорения времени компиляции C ++?

Этот вопрос возник в некоторых комментариях к вопросу переполнения стека Стиль программирования C ++ , и мне интересно узнать, какие есть идеи.

Я видел связанный вопрос, Почему компиляция C ++ занимает так много времени? , но это не дает много решений.


Проголосуйте, здесь есть поддержка Visual Studio для совместного использования предварительно скомпилированных заголовков между проектами

Ответы [ 24 ]

238 голосов
/ 17 декабря 2008

Языковые техники

Пимпл идиома

Взгляните на идиому Pimpl здесь и здесь , также известную как непрозрачный указатель или обрабатывать классы. Он не только ускоряет компиляцию, но и повышает безопасность исключений в сочетании с функцией non-throwing swap . Идиома Pimpl позволяет уменьшить зависимости между заголовками и уменьшить объем перекомпиляции, который необходимо выполнить.

Форвардные декларации

По возможности, используйте предварительные объявления . Если компилятору нужно только знать, что SomeIdentifier является структурой или указателем или чем-то еще, не включайте полное определение, заставляя компилятор выполнять больше работы, чем нужно. Это может иметь каскадный эффект, делая этот путь медленнее, чем нужно.

Потоки I / O особенно известны замедлением сборок. Если они вам нужны в заголовочном файле, попробуйте #include <iosfwd> вместо <iostream> и #include заголовочный файл <iostream> только в файле реализации. Заголовок <iosfwd> содержит только предварительные объявления. К сожалению, у других стандартных заголовков нет соответствующего заголовка объявлений.

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

Условия охраны

Используйте защитные условия, чтобы файлы заголовков не включались более одного раза в один блок перевода.

#pragma once
#ifndef filename_h
#define filename_h

// Header declarations / definitions

#endif

Используя как прагму, так и ifndef, вы получаете переносимость простого макро-решения, а также оптимизацию скорости компиляции, которую могут выполнять некоторые компиляторы при наличии директивы pragma once.

Уменьшить взаимозависимость

Чем более модульный и менее взаимозависимый дизайн вашего кода в целом, тем реже вам придется все перекомпилировать. Вы также можете сократить объем работы, которую компилятор должен выполнять над любым отдельным блоком одновременно, в силу того, что он меньше отслеживает.

Опции компилятора

предварительно скомпилированные заголовки

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

Будьте осторожны, чтобы в предварительно скомпилированные заголовки включались только редко изменяемые элементы, иначе вы можете в конечном итоге выполнять полную перестройку чаще, чем это необходимо. Это хорошее место для STL заголовков и других файлов библиотеки.

ccache - еще одна утилита, использующая преимущества методов кэширования для ускорения процесса.

Использовать параллелизм

Многие компиляторы / IDE поддерживают использование нескольких ядер / процессоров для одновременной компиляции. В GNU Make (обычно используется с GCC) используйте параметр -j [N]. В Visual Studio в настройках есть опция, позволяющая создавать несколько проектов параллельно. Вы также можете использовать опцию /MP для паралеллизма на уровне файлов вместо просто паралеллизма на уровне проекта.

Другие параллельные утилиты:

Использовать более низкий уровень оптимизации

Чем больше компилятор пытается оптимизировать, тем сложнее он должен работать.

Общие библиотеки

Перемещение менее часто модифицированного кода в библиотеки может сократить время компиляции. Используя общие библиотеки (.so или .dll), вы также можете сократить время компоновки.

Получить более быстрый компьютер

Больше оперативной памяти, более быстрые жесткие диски (включая твердотельные накопители) и большее количество процессоров / ядер будут влиять на скорость компиляции.

32 голосов
/ 13 ноября 2015

Я работаю над проектом STAPL, который представляет собой библиотеку C ++ с большим количеством шаблонов. Время от времени мы должны пересматривать все методы, чтобы сократить время компиляции. Здесь я кратко изложил методы, которые мы используем. Некоторые из этих методов уже перечислены выше:

Поиск наиболее трудоемких разделов

Хотя нет доказанной корреляции между длиной символов и временем компиляции, мы заметили, что меньшие средние размеры символов могут улучшить время компиляции на всех компиляторах. Итак, ваши первые цели - найти самые большие символы в вашем коде.

Метод 1 - сортировка символов по размеру

Вы можете использовать команду nm для вывода списка символов на основе их размеров:

nm --print-size --size-sort --radix=d YOUR_BINARY

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

Метод 2 - Сортировка символов по длине

Вы можете запустить обычную команду nm и направить ее в ваш любимый скрипт ( 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. Один из способов решить эту проблему - абстрагировать общую часть кода в другой класс , Другой метод заключается в использовании инверсии наследования (реверсирование иерархии классов), но перед использованием этого метода убедитесь, что ваши цели проектирования не поставлены под угрозу.

Рефакторинг нешаблонного кода для отдельных единиц перевода

Используя эту технику, вы можете скомпилировать общий раздел один раз и позже связать его с другими вашими TU (единицами перевода).

Использовать внешние экземпляры шаблонов (начиная с C ++ 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 игнорирует явное создание экземпляров в этой точке и всегда компилирует созданные экземпляры классов во всех единицах компиляции.

Использовать билды единства

Основная идея Unity builds заключается в том, чтобы включить все файлы .cc, которые вы используете, в один файл и скомпилировать этот файл только один раз. Используя этот метод, вы можете избежать повторного создания общих разделов различных файлов, и если ваш проект содержит много общих файлов, вы, вероятно, также сэкономите на доступе к диску.

В качестве примера давайте предположим, что у вас есть три файла foo1.cc, foo2.cc, foo3.cc, и все они включают tuple из STL . Вы можете создать foo-all.cc, который будет выглядеть так:

#include "foo1.cc"
#include "foo2.cc"
#include "foo3.cc"

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

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

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

Предварительно скомпилированные заголовки (PCH) могут сэкономить вам много времени при компиляции, скомпилировав ваши заголовочные файлы в промежуточное представление, распознаваемое компилятором. Чтобы сгенерировать предварительно скомпилированные заголовочные файлы, вам нужно только скомпилировать ваш заголовочный файл с помощью обычной команды компиляции. Например, в GCC:

$ g++ YOUR_HEADER.hpp

Это создаст YOUR_HEADER.hpp.gch file (.gch - расширение для файлов PCH в GCC) в той же папке. Это означает, что если вы включите YOUR_HEADER.hpp в какой-то другой файл, компилятор будет использовать ваш YOUR_HEADER.hpp.gch вместо YOUR_HEADER.hpp в той же папке ранее.

Есть две проблемы с этой техникой:

  1. Вы должны убедиться, что прекомпилированные файлы заголовков стабильны и не будут меняться ( вы всегда можете изменить свой make-файл )
  2. Вы можете включить только один PCH на единицу компиляции (на большинстве компиляторов). Это означает, что если у вас есть несколько файлов заголовков для предварительной компиляции, вы должны включить их в один файл (например, all-my-headers.hpp). Но это означает, что вы должны включить новый файл во всех местах. К счастью, у GCC есть решение этой проблемы. Используйте -include и дайте ему новый заголовочный файл. Используя эту технику, вы можете разделять запятые разных файлов.

Например:

g++ foo.cc -include all-my-headers.hpp

Использовать безымянные или анонимные пространства имен

Безымянные пространства имен (например, анонимные пространства имен) могут значительно уменьшить сгенерированные двоичные размеры. Неназванные пространства имен используют внутреннюю связь, то есть символы, сгенерированные в этих пространствах имен, не будут видны другим TU (единицам перевода или компиляции). Компиляторы обычно генерируют уникальные имена для безымянных пространств имен. Это означает, что если у вас есть файл foo.hpp:

namespace {

template <typename T>
struct foo { };
} // Anonymous namespace
using A = foo<int>;

И вы случайно включили этот файл в два TU (два .cc-файла и скомпилировали их отдельно). Два экземпляра шаблона foo не будут одинаковыми. Это нарушает правило One Definition (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 - это default (общедоступная), что означает, что если вы скомпилируете вышеупомянутый метод с использованием общей библиотеки (-shared), foo2 и класс foo3 не будут видны в других TU (foo1 и foo4 будут видны). Если вы скомпилируете с -visibility=hidden, тогда будет виден только foo1. Даже foo4 будет скрыто.

Подробнее о видимости можно прочитать на вики GCC .

31 голосов
/ 17 декабря 2008

Я бы порекомендовал эти статьи из "Игр изнутри, инди-дизайна игр и программирования":

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

16 голосов
/ 21 сентября 2010

Одна из техник, которая в прошлом работала довольно хорошо для меня: не компилируйте несколько исходных файлов C ++ независимо, а скорее генерируйте один файл C ++, который включает в себя все остальные файлы, например:

// 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 года для создания оптимизированных двоичных файлов (возможно, для выпуска). Переключение на сценарий настройки сборки было названо --enable-final. Из археологического интереса я выкопал объявление, которое объявило эту функцию: http://lists.kde.org/?l=kde-devel&m=92722836009368&w=2

15 голосов
/ 30 декабря 2008

На эту тему есть целая книга, озаглавленная Крупномасштабный программный дизайн C ++ (автор Джон Лакос).

Книга предшествует шаблонам, поэтому к содержанию этой книги добавьте "использование шаблонов также может замедлить работу компилятора".

15 голосов
/ 17 декабря 2008

Я просто сошлюсь на другой мой ответ: Как ВЫ сокращаете время компиляции и время компоновки для проектов Visual C ++ (нативный C ++)? . Еще один момент, который я хочу добавить, но который часто вызывает проблемы - это использование предварительно скомпилированных заголовков. Но, пожалуйста, используйте их только для частей, которые почти никогда не меняются (например, заголовки инструментария GUI). В противном случае они будут стоить вам больше времени, чем сэкономят.

Другой вариант, когда вы работаете с GNU make, включить опцию -j<N>:

  -j [N], --jobs[=N]          Allow N jobs at once; infinite jobs with no arg.

У меня обычно это в 3, так как у меня здесь есть двухъядерный процессор. Затем он будет запускать компиляторы параллельно для разных модулей перевода, если между ними нет никаких зависимостей. Связывание невозможно выполнить параллельно, поскольку существует только один процесс компоновщика, связывающий все объектные файлы.

Но сам компоновщик может быть резьбовым, и это то, что делает компоновщик GNU gold ELF . Это оптимизированный многопоточный код C ++, который, как говорят, связывает объектные файлы ELF на порядок быстрее, чем старый ld (и фактически был включен в binutils ).

11 голосов
/ 17 декабря 2008

Вот некоторые из них:

  • Используйте все ядра процессора, запустив задание множественной компиляции (make -j2 - хороший пример).
  • Отключить или уменьшить оптимизацию (например, GCC намного быстрее с -O1, чем -O2 или -O3).
  • Использовать предварительно скомпилированные заголовки .
10 голосов
/ 17 декабря 2008

Как только вы применили все вышеперечисленные трюки кода (предварительные объявления, сокращение включения заголовка до минимума в публичных заголовках, добавление большинства деталей в файл реализации с помощью Pimpl ...) и больше ничего не может быть приобретенный по языку, рассмотрите вашу систему сборки. Если вы используете Linux, рассмотрите возможность использования distcc (распределенный компилятор) и ccache (компилятор кэша).

Первый, distcc, выполняет шаг препроцессора локально, а затем отправляет вывод первому доступному компилятору в сети. Для него требуются одинаковые версии компилятора и библиотеки во всех настроенных узлах сети.

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

Оба могут использоваться одновременно, так что если ccache не имеет локальной копии, он может отправить ее через сеть на другой узел с distcc, или он может просто внедрить решение без дальнейшей обработки.

9 голосов
/ 17 декабря 2008

Когда я вышел из колледжа, первый настоящий производительный код C ++, который я увидел, содержал эти загадочные директивы #ifndef ... #endif между ними, где были определены заголовки. Я спросил парня, который очень наивно писал код об этих всеобъемлющих вещах и познакомился с миром крупномасштабного программирования.

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

8 голосов
/ 27 декабря 2008

больше оперативной памяти.

Кто-то говорил об ОЗУ в другом ответе. Я сделал это с 80286 и Turbo C ++ (показывает возраст), и результаты были феноменальными. Как и потеря данных при сбое машины.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...