Какие методы можно использовать для ускорения компиляции C++?
Этот вопрос возник в некоторых комментариях к вопросу о переполнении стека Стиль программирования C++, и мне интересно услышать, какие есть идеи.
Я видел связанный вопрос, Почему компиляция C++ занимает так много времени?, но он не дает много решений.
Очень похоже на этот вопрос: stackoverflow.com/questions/364240/…
Общие ответы. У меня действительно большая база кода, написанная многими людьми. Идеи о том, как атаковать, были бы хороши. Кроме того, были бы интересны предложения по быстрой компиляции только что написанного кода.
Обратите внимание, что часто соответствующая часть времени сборки используется не компилятором, а сценариями сборки.
Я пролистал эту страницу и не увидел никаких упоминаний об измерениях. Я написал небольшой сценарий оболочки, который добавляет метку времени к каждой строке ввода, которую он получает, поэтому я могу просто выполнить вызов make. Это позволяет мне видеть, какие цели являются самыми дорогими, общее время компиляции или компоновки и т. д., Просто сравнивая временные метки. Если вы попробуете этот подход, помните, что временные метки будут неточными для параллельных сборок.
используйте distcc или incredibuild





Я имел представление о с использованием RAM-диска. Оказалось, что для моих проектов это не имеет большого значения. Но тогда они еще довольно маленькие. Попытайся! Мне было бы интересно услышать, насколько это помогло.
Хм. Почему кто-то проголосовал против? Я попробую завтра.
Я ожидаю, что голос против будет потому, что он никогда не будет иметь большого значения. Если у вас достаточно неиспользуемой оперативной памяти, ОС в любом случае разумно использует ее в качестве дискового кеша.
@MSalters - а сколько бы "хватило"? Я знаю, что это теория, но по какой-то причине использование RAMdrive делает действительно дает значительный импульс. Иди разбери ...
достаточно, чтобы скомпилировать ваш проект и по-прежнему кэшировать входные и временные файлы. Очевидно, что размер в ГБ будет напрямую зависеть от размера вашего проекта. Следует отметить, что на старых ОС (в частности, WinXP) кеширование файлов было довольно ленивым, и оперативная память оставалась неиспользованной.
Конечно, оперативная память быстрее, если файлы уже находятся в оперативной памяти, а не сначала выполняет целую кучу медленных операций ввода-вывода, а затем они уже находятся в оперативной памяти? (подъем-повтор для файлов, которые изменились - записать их обратно на диск и т. д.).
Я бы ожидал, что компоновщик будет быстрее, если библиотеки уже находятся на жестком диске
Файлы на "жестком диске", вероятно, уже находятся в ОЗУ из-за кэширования, которое у нас есть в любой приличной ОС уже много лет (десятилетия?)
Если у вас многоядерный процессор, Visual Studio (2005 и новее), а также GCC поддерживают многопроцессорные компиляции. Это то, что нужно включить, если у вас есть оборудование.
@Fellman, посмотрите другие ответы - используйте опцию -j #.
Я просто сделаю ссылку на свой другой ответ: Как ВЫ сокращаете время компиляции и время компоновки для проектов Visual C++ (собственный C++)?. Еще один момент, который я хочу добавить, но который часто вызывает проблемы, - это использование предварительно скомпилированных заголовков. Но, пожалуйста, используйте их только для частей, которые почти никогда не меняются (например, заголовки GUI toolkit). В противном случае они будут стоить вам больше времени, чем в конечном итоге сэкономят.
Другой вариант - при работе с GNU make включить опцию -j<N>:
-j [N], --jobs[=N] Allow N jobs at once; infinite jobs with no arg.
Обычно он у меня на 3, так как здесь у меня двухъядерный. Затем он будет запускать компиляторы параллельно для разных единиц перевода при условии, что между ними нет зависимостей. Связывание нельзя выполнять параллельно, поскольку существует только один процесс компоновщика, связывающий вместе все объектные файлы.
Но сам компоновщик может быть многопоточным, и именно это делает компоновщик GNU goldELF. Это оптимизированный многопоточный код C++, который, как говорят, связывает объектные файлы ELF намного быстрее, чем старый ld (и фактически был включен в binutils).
Да, хорошо. Извините, этот вопрос не возник, когда я искал.
тебе не нужно было извиняться. это было для Visual C++. ваш вопрос, похоже, относится к любому компилятору. так что нормально :)
Вот некоторые:
make -j2 - хороший пример).-O1, чем -O2 или -O3).К вашему сведению, я считаю, что обычно быстрее запускать больше процессов, чем ядер. Например, в четырехъядерной системе я обычно использую -j8, а не -j4. Причина этого в том, что когда один процесс блокируется при вводе-выводе, другой может компилироваться.
@MrFooz: Я тестировал это несколько лет назад, скомпилировав ядро Linux (из оперативной памяти) на i7-2700k (4 ядра, 8 потоков, я установил постоянный множитель). Я забыл точный лучший результат, но -j12 примерно до -j18 были значительно быстрее, чем -j8, как вы и предполагаете. Мне интересно, сколько ядер у вас может быть, прежде чем пропускная способность памяти станет ограничивающим фактором ...
@MarkKCowan, это зависит от множества факторов. Разные компьютеры имеют совершенно разную пропускную способность памяти. Современные высокопроизводительные процессоры требуют нескольких ядер, чтобы заполнить шину памяти. Кроме того, существует баланс между вводом-выводом и процессором. Некоторый код очень легко скомпилировать, другой код может быть медленным (например, с большим количеством шаблонов). Мое текущее практическое правило - -j с вдвое большим количеством ядер.
Обновите свой компьютер
Тогда у вас есть все другие типичные предложения
Использовать
#pragma once
в верхней части файлов заголовков, поэтому, если они включены в единицу перевода более одного раза, текст заголовка будет включен и проанализирован только один раз.
Несмотря на широкую поддержку, #pragma once является нестандартным. См. en.wikipedia.org/wiki/Pragma_once
И в наши дни такой же эффект имеют обычные охранники включения. Пока они находятся в верхней части файла, компилятор может обрабатывать их как #pragma once
Когда я закончил колледж, первый настоящий производственный код на C++, который я увидел, содержал эти загадочные директивы #ifndef ... #endif между ними, где были определены заголовки. Я очень наивно спросил парня, который писал код, об этих всеобъемлющих вещах, и меня познакомили с миром крупномасштабного программирования.
Возвращаясь к сути, использование директив для предотвращения дублирования определений заголовков было первым, что я узнал, когда дело дошло до сокращения времени компиляции.
Старый но золотой. иногда очевидное забывается.
'включить охранников'
Взгляните на Идиома Pimpl, здесь и здесь, также известные как непрозрачный указатель или классы дескрипторов. Это не только ускоряет компиляцию, но и повышает безопасность исключений в сочетании с функцией безбрасывающий своп. Идиома Pimpl позволяет уменьшить зависимости между заголовками и уменьшить объем перекомпиляции, которую необходимо выполнить.
По возможности используйте предварительные декларации. Если компилятору нужно только знать, что SomeIdentifier - это структура, указатель или что-то еще, не включайте определение целиком, заставляя компилятор выполнять больше работы, чем нужно. Это может иметь каскадный эффект, делая этот способ медленнее, чем нужно.
Потоки Ввод / вывод особенно известны тем, что замедляют сборку. Если они вам нужны в файле заголовка, попробуйте # включить <iosfwd> вместо <iostream> и # включить заголовок <iostream> только в файл реализации. Заголовок <iosfwd> содержит только предварительные объявления. К сожалению, другие стандартные заголовки не имеют соответствующего заголовка объявлений.
Предпочитайте передачу по ссылке перед передачей по значению в сигнатурах функций. Это избавит от необходимости # включать соответствующие определения типов в файл заголовка, и вам нужно будет только переадресовать объявление типа. Конечно, предпочитайте константные ссылки неконстантным ссылкам, чтобы избежать неясных ошибок, но это проблема для другого вопроса.
Используйте защитные условия, чтобы файлы заголовков не включались более одного раза в одну единицу перевода.
#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), вы также можете сократить время компоновки.
Больше оперативной памяти, более быстрые жесткие диски (включая твердотельные накопители) и большее количество процессоров / ядер повлияют на скорость компиляции.
Однако предварительно скомпилированные заголовки не идеальны. Побочным эффектом их использования является то, что вы включаете больше файлов, чем необходимо (потому что каждая единица компиляции использует один и тот же предварительно скомпилированный заголовок), что может вызывать полную перекомпиляцию чаще, чем необходимо. Просто нужно иметь в виду.
Кроме того, в VS2008 можно параллельно создавать несколько файлов .cpp, а не только проекты.
Я обычно помещаю такие вещи, как заголовки STL и заголовки библиотек (например, windows.h), которые не меняются, в предварительно скомпилированные заголовки. Но да, это плохая идея - добавлять что-то, что будет меняться хотя бы частично.
не лучше ли предпочесть #ifndef #pragma? по цене еще 2 loc вы получаете дополнительную поддержку компиляторов.
Как насчет того, чтобы «стать лучшим программистом», чтобы вам не приходилось перекомпилировать каждый новый модуль mod3 или mod5 loc? :)
В современных компиляторах #ifndef выполняется так же быстро, как и #pragma once (при условии, что защита включения находится в верхней части файла). Таким образом, #pragma Once бесполезен с точки зрения скорости компиляции.
Если современные компиляторы включают только gcc, то да. Насколько я могу судить, Visual C++ этого не делает.
Даже если у вас только VS 2005, а не 2008, вы можете добавить переключатель / MP в параметры компиляции, чтобы включить параллельную сборку на уровне .cpp.
Когда был написан этот ответ, SSD были непомерно дорогими, но сегодня они являются лучшим выбором при компиляции C++. При компиляции вы получаете доступ к большому количеству небольших файлов. Это требует большого количества операций ввода-вывода в секунду, которые обеспечивают SSD.
На комбинации #pragma и охранников вам следует изменить порядок. Некоторые компиляторы будут обрабатывать include guard как #pragma once, если это первая вещь в предварительно обработанном файле, но не смогут этого сделать, если вы добавите что-либо, кроме комментария вне #ifndef.
+1 за ccache. На установку потребовалось всего 15 минут, и теперь я получаю 10-кратное ускорение во многих распространенных сценариях.
Влияет ли объем ОЗУ на скорость компиляции только из-за файла подкачки или есть другая причина? Мне просто интересно, будет ли разница с 6 ГБ до 12 ГБ или более ....
@Dinaiz: иметь меньше ОЗУ, чем использует компилятор, очень плохо, потому что это вызывает разбиение на страницы, но наличие значительно большего объема ОЗУ, чем использует компилятор, также полезно, потому что избыток используется для дискового кеша (в большинстве современных ОС)
Большинство из этих предложений предполагают отдельную компиляцию, что может быть недостижимо, если используются шаблоны, а возможные аргументы шаблона неизвестны или их слишком много. Есть предложения, когда есть только одна единица перевода?
Много хороших моментов! К списку параллельных утилит я бы добавил Icecc, что намного лучше distcc (IMO). Я написал немного более ориентированную на Linux статью на эту тему: Более быстрые сборки C++. Другое дело, что сборки выпуска (без символов отладки) могут быть быстрее для некоторых проектов, так как связывание может быть быстрее.
Сделайте себе одолжение и используйте диски NVME на машинах разработчиков. Конечно, они дорогие, но что еще может превзойти 3200 чтений по цене и простоте использования. Я использую 6-ядерный процессор и 970 pro nvme-накопитель. Скорость замечательная.
По возможности используйте форвардные объявления. Если в объявлении класса используется только указатель или ссылка на тип, вы можете просто объявить его вперед и включить заголовок для типа в файл реализации.
Например:
// 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
}
Меньшее количество включений означает гораздо меньше работы для препроцессора, если вы сделаете это достаточно.
Разве это не имеет значения только в том случае, если один и тот же заголовок включен в несколько единиц перевода? Если есть только одна единица перевода (как это часто бывает при использовании шаблонов), это, по-видимому, не повлияет.
Если есть только одна единица перевода, зачем помещать ее в заголовок? Разве не имеет смысла просто поместить содержимое в исходный файл? Разве весь смысл заголовков не в том, что они могут быть включены более чем в один исходный файл?
Я бы порекомендовал эти статьи из "Игры изнутри, инди-дизайн игр и программирование":
Конечно, они довольно старые - вам придется повторно протестировать все с последними версиями (или версиями, доступными вам), чтобы получить реалистичные результаты. В любом случае, это хороший источник идей.
Вы можете использовать Сборки Unity.
Взаимодействие с другими людьми
@idbrii, ссылка мертвая. Вот снимок на archive.org
Просто для полноты: сборка может быть медленной из-за глупости системы сборки, а также из-за того, что компилятор долго выполняет свою работу.
Прочтите Рекурсивное объявление считается вредным (PDF) для обсуждения этой темы в средах Unix.
Где ты проводишь время? Вы привязаны к процессору? Ограничена память? Диск связан? Можно еще ядер использовать? Больше оперативной памяти? Вам нужен RAID? Вы просто хотите повысить эффективность своей нынешней системы?
Под gcc / g ++ вы смотрели ccache? Это может быть полезно, если вы много занимаетесь make clean; make.
После того, как вы применили все описанные выше приемы кода (предварительные объявления, сокращение включения заголовков до минимума в общедоступных заголовках, размещение большей части деталей внутри файла реализации с помощью Сутенер ...) и больше ничего не может быть получено с точки зрения языка, рассмотрите свою систему сборки . Если вы используете Linux, рассмотрите возможность использования distcc (распределенный компилятор) и ccache (компилятор кеша).
Первый, distcc, выполняет шаг препроцессора локально, а затем отправляет выходные данные первому доступному компилятору в сети. Для этого требуются одинаковые версии компилятора и библиотеки на всех настроенных узлах сети.
Последний, ccache, представляет собой кеш компилятора. Он снова запускает препроцессор, а затем проверяет внутреннюю базу данных (хранящуюся в локальном каталоге), если этот файл препроцессора уже был скомпилирован с теми же параметрами компилятора. Если это так, он просто выводит двоичный файл и выводит его при первом запуске компилятора.
Оба могут использоваться одновременно, так что, если ccache не имеет локальной копии, он может отправить ее по сети на другой узел с помощью distcc, иначе он может просто внедрить решение без дальнейшей обработки.
Я не думаю, что distcc требует одинаковых версий библиотека на всех настроенных узлах. distcc выполняет только удаленную компиляцию, но не компоновку. Он также отправляет код предварительно обработанный по сети, поэтому заголовки, доступные в удаленной системе, не имеют значения.
Динамическое связывание (.so) может быть намного быстрее, чем статическое связывание (.a). Особенно если у вас медленный сетевой диск. Это потому, что у вас есть весь код в файле .a, который необходимо обработать и записать. Кроме того, на диск необходимо записать исполняемый файл гораздо большего размера.
динамическое связывание предотвращает многие виды оптимизации времени компоновки, поэтому во многих случаях вывод может быть медленнее
Больше оперативной памяти.
Кто-то говорил о накопителях RAM в другом ответе. Я сделал это с 80286 и Турбо C++ (показывает возраст), и результаты были феноменальными. Как и потеря данных, когда машина разбилась.
в DOS у вас не может быть много памяти, хотя
и машина падала каждый раз, когда вы делали ошибку памяти
По этой теме есть целая книга под названием Разработка крупномасштабного программного обеспечения на C++ (написанная Джоном Лакосом).
Шаблоны книги предшествуют датам, поэтому к содержанию этой книги добавьте «использование шаблонов тоже может замедлить работу компилятора».
Книга часто упоминается в таких темах, но для меня она была скудной. Он в основном утверждает, что нужно как можно больше использовать форвардные объявления и разъединять зависимости. Это немного говорит об очевидном, помимо того, что использование идиомы pimpl имеет недостатки во время выполнения.
@ gast128 Я думаю, его смысл в том, чтобы использовать идиомы кодирования, которые разрешают инкрементную повторную компиляцию, то есть, чтобы, если вы немного измените источник где-то, вам не придется все перекомпилировать.
В Linux (и, возможно, в некоторых других * NIX) вы действительно можете ускорить компиляцию, НЕ СМОТРЕТЬ на выходе и переключившись на Другой TTY.
Вот эксперимент: printf замедляет мою программу
Один из приемов, который в прошлом работал у меня довольно хорошо: не компилируйте несколько исходных файлов 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 использовал ту же самую технику с 1999 года для создания оптимизированных двоичных файлов (возможно, для выпуска). Переключение на сценарий настройки сборки называлось --enable-final. Из археологических интересов я откопал сообщение, в котором сообщалось об этой функции: http://lists.kde.org/?l=kde-devel&m=92722836009368&w=2
Я не уверен, что это действительно то же самое, но я полагаю, что включение «Оптимизации всей программы» в VC++ (msdn.microsoft.com/en-us/library/0zza0de8%28VS.71%29.aspx) должно иметь такое же влияние на производительность во время выполнения, чем то, что вы предлагаете. Однако время компиляции определенно может быть лучше в вашем подходе!
@Frerich: Вы описываете сборки Unity, упомянутые в ответе OJ. Я также видел их, называемые массовыми сборками и мастер-сборками.
Так как же UB по сравнению с WPO / LTCG?
Это потенциально полезно только для одноразовых компиляций, а не во время разработки, когда вы циклически переключаетесь между редактированием, сборкой и тестированием. В современном мире четыре ядра - это норма, возможно, через пару лет количество ядер значительно увеличится. Если компилятор и компоновщик не могут использовать несколько потоков, то список файлов, возможно, можно разделить на подсписки <core-count> + N, которые компилируются параллельно, где N - некоторое подходящее целое число (в зависимости от системной памяти и того, как машина используется в противном случае).
Хотя это и не была «техника», я не мог понять, как проекты Win32 с большим количеством исходных файлов компилируются быстрее, чем мой пустой проект «Hello World». Таким образом, я надеюсь, что это поможет кому-то так же, как я.
В Visual Studio одним из вариантов увеличения времени компиляции является добавочное связывание (/ ДОПОЛНИТЕЛЬНЫЙ). Это несовместимо с генерацией кода во время компоновки (/ LTCG), поэтому не забудьте отключить инкрементное связывание при выполнении сборок выпуска.
отключение генерации кода во время компоновки не является хорошим предложением, так как это отключает многие оптимизации. Вам нужно включить /INCREMENTAL только в режиме отладки
Более быстрые жесткие диски.
Компиляторы записывают на диск много (и, возможно, огромных) файлов. Работайте с SSD вместо обычного жесткого диска, и время компиляции намного меньше.
Общие сетевые ресурсы резко замедлят вашу сборку, так как задержка поиска высока. Для чего-то вроде Boost это имело огромное значение для меня, хотя наш сетевой диск довольно быстрый. Когда я переключился с общего сетевого ресурса на локальный SSD, время на компиляцию игрушечной программы Boost уменьшилось с 1 минуты до 1 секунды.
Не о времени компиляции, а о времени сборки:
Используйте ccache, если вам нужно пересобрать те же файлы во время работы. в ваших файлах сборки
Используйте ниндзя-билд вместо make. Я сейчас составляю проект с ~ 100 исходными файлами, и все кэшируется ccache. делать нужды 5 минут, ниндзя меньше 1.
Вы можете сгенерировать свои файлы ниндзя из cmake с помощью -GNinja.
Начиная с Visual Studio 2017 у вас есть возможность получать некоторые метрики компилятора о том, что требует времени.
Добавьте эти параметры в C / C++ -> Командная строка (Дополнительные параметры) в окне свойств проекта:
/Bt+ /d2cgsummary /d1reportTime
Вы можете получить дополнительную информацию в этом посте.
Использование динамической компоновки вместо статической делает ваш компилятор более быстрым.
Если вы используете t Cmake, активируйте свойство:
set(BUILD_SHARED_LIBS ON)
Build Release, используя статическое связывание, можно оптимизировать.
Прежде всего, мы должны понять, что такого особенного в C++, что отличает его от других языков.
Некоторые говорят, что в C++ тоже много возможностей. Но послушайте, есть языки, в которых гораздо больше возможностей, и они далеко не так медленны.
Некоторые говорят, что важен размер файла. Нет, строки исходного кода не коррелируют со временем компиляции.
Но подождите, как это может быть? Больше строк кода должно означать более длительное время компиляции, в чем волшебство?
Хитрость в том, что в директивах препроцессора скрыто множество строк кода. Да. Всего один #include может испортить производительность компиляции вашего модуля.
Видите ли, в C++ нет модульной системы. Все файлы *.cpp скомпилированы с нуля. Таким образом, наличие 1000 файлов * .cpp означает тысячу компиляций вашего проекта. У вас есть больше? Печалька.
Вот почему разработчики C++ не решаются разбивать классы на несколько файлов. Все эти заголовки утомительно поддерживать.
Итак, что мы можем сделать, кроме использования предварительно скомпилированных заголовков, объединения всех файлов cpp в один и сохранения минимального количества заголовков?
C++ 20 предоставляет нам предварительную поддержку модули! В конце концов, вы сможете забыть о #include и ужасной производительности компиляции, которую приносят с собой файлы заголовков. Тронули один файл? Перекомпилируйте только этот файл! Нужно создать новую кассу? Компилируйте за секунды, а не за минуты и часы.
Сообщество C++ должно как можно скорее перейти на C++ 20. Разработчики компиляторов C++ должны уделять этому больше внимания, разработчики C++ должны начать тестирование предварительной поддержки в различных компиляторах и использовать те компиляторы, которые поддерживают модули. Это самый важный момент в истории C++!
Не могли бы вы дать нам какой-нибудь контекст? Или вы ищете очень общие ответы?