Какие методы можно использовать для ускорения времени компиляции C ++?
Этот вопрос возник в некоторых комментариях к стилю программирования C ++ в вопросе переполнения стека , и мне интересно узнать, какие есть идеи.
Я видел связанный вопрос, почему компиляция C ++ занимает так много времени? , но это не дает много решений.
Ответы:
Языковые техники
Пимпл идиом
Взгляните на идиому Pimpl здесь и здесь , также известную как непрозрачный указатель или дескриптор классов. Это не только ускоряет компиляцию, но и повышает безопасность исключений в сочетании с функцией без броска обмена . Идиома Pimpl позволяет вам уменьшить зависимости между заголовками и уменьшить объем перекомпиляции, который необходимо выполнить.
Форвардные декларации
По возможности используйте предварительные декларации . Если компилятору нужно только знать, что
SomeIdentifier
это структура или указатель или что-то еще, не включайте полное определение, заставляя компилятор выполнять больше работы, чем нужно. Это может иметь каскадный эффект, делая этот путь медленнее, чем нужно.В ввода / вывода потоков особенно известны для замедления сборки. Если они вам нужны в заголовочном файле, попробуйте #include
<iosfwd>
вместо<iostream>
#include и<iostream>
заголовок только в файле реализации.<iosfwd>
Заголовок содержит только вперед декларации. К сожалению, у других стандартных заголовков нет соответствующего заголовка объявлений.Предпочитайте передачу по ссылке передаче по значению в сигнатурах функций. Это избавит от необходимости #include соответствующие определения типа в заголовочном файле, и вам нужно будет только объявить тип вперед. Конечно, предпочитайте константные ссылки неконстантным ссылкам, чтобы избежать неясных ошибок, но это проблема другого вопроса.
Условия охраны
Используйте защитные условия, чтобы файлы заголовков не включались более одного раза в одну единицу перевода.
Используя как прагму, так и ifndef, вы получаете переносимость простого макро-решения, а также оптимизацию скорости компиляции, которую могут выполнять некоторые компиляторы при наличии
pragma once
директивы.Уменьшить взаимозависимость
Чем более модульный и менее взаимозависимый дизайн вашего кода в целом, тем реже вам придется все перекомпилировать. Вы также можете сократить объем работы, которую компилятор должен выполнять над любым отдельным блоком одновременно, в силу того, что он меньше отслеживает.
Опции компилятора
Предварительно скомпилированные заголовки
Они используются для составления общего раздела включенных заголовков один раз для многих блоков перевода. Компилятор компилирует его один раз и сохраняет его внутреннее состояние. Затем это состояние можно быстро загрузить, чтобы получить преимущество при компиляции другого файла с таким же набором заголовков.
Будьте осторожны, чтобы в предварительно скомпилированные заголовки включались только редко изменяемые элементы, иначе полные перестройки могут оказаться чаще, чем необходимо. Это хорошее место для заголовков STL и других файлов библиотеки.
ccache - еще одна утилита, которая использует методы кэширования для ускорения работы.
Используйте параллелизм
Многие компиляторы / IDE поддерживают использование нескольких ядер / процессоров для одновременной компиляции. В GNU Make (обычно используется с GCC) используйте
-j [N]
опцию. В Visual Studio в настройках есть опция, позволяющая строить несколько проектов параллельно. Вы также можете использовать/MP
опцию для паралеллизма на уровне файлов вместо просто паралеллизма на уровне проекта.Другие параллельные утилиты:
Используйте более низкий уровень оптимизации
Чем больше компилятор пытается оптимизировать, тем сложнее он должен работать.
Общие библиотеки
Перемещение менее часто модифицированного кода в библиотеки может сократить время компиляции. Используя общие библиотеки (
.so
или.dll
), вы также можете сократить время компоновки.Получите более быстрый компьютер
Больше оперативной памяти, более быстрые жесткие диски (включая твердотельные накопители) и большее количество процессоров / ядер будут влиять на скорость компиляции.
источник
Я работаю над проектом STAPL, который представляет собой библиотеку C ++ с большим количеством шаблонов. Время от времени мы должны пересматривать все методы, чтобы сократить время компиляции. Здесь я кратко изложил методы, которые мы используем. Некоторые из этих методов уже перечислены выше:
Поиск наиболее трудоемких разделов
Хотя нет доказанной корреляции между длиной символов и временем компиляции, мы заметили, что меньшие средние размеры символов могут улучшить время компиляции на всех компиляторах. Итак, ваши первые цели - найти самые большие символы в вашем коде.
Метод 1 - сортировка символов по размеру
Вы можете использовать
nm
команду для вывода списка символов на основе их размеров:В этой команде
--radix=d
вы можете увидеть размеры в десятичных числах (по умолчанию шестнадцатеричное). Теперь, взглянув на самый большой символ, определите, можете ли вы разбить соответствующий класс, и попытайтесь изменить его, разложив не шаблонные части в базовом классе или разделив класс на несколько классов.Метод 2 - сортировка символов по длине
Вы можете запустить обычную
nm
команду и направить ее в ваш любимый скрипт ( AWK , Python и т. Д.), Чтобы отсортировать символы по их длине . Основываясь на нашем опыте, этот метод определяет самые большие проблемы, делая кандидатов лучше, чем метод 1.Способ 3 - использовать Templight
« Templight - это инструмент, основанный на Clang, который позволяет профилировать время и потребление памяти при создании экземпляров шаблонов и выполнять интерактивные сеансы отладки, чтобы получить интроспекцию процесса создания шаблонов».
Вы можете установить Templight, проверив LLVM и Clang ( инструкции ) и применив к нему патч Templight. Настройка по умолчанию для LLVM и Clang - при отладке и утверждениях, и они могут значительно повлиять на время компиляции. Кажется, что Templight нуждается в обоих, поэтому вы должны использовать настройки по умолчанию. Процесс установки LLVM и Clang должен занять около часа или около того.
После применения патча вы можете использовать
templight++
находящуюся в папке сборки, которую вы указали при установке, для компиляции вашего кода.Убедитесь, что
templight++
это в вашем ПУТИ. Теперь для компиляции добавьте следующие ключиCXXFLAGS
в ваш Makefile или в параметры командной строки:Или
После завершения компиляции вы получите файлы .trace.memory.pbf и .trace.pbf, созданные в одной папке. Чтобы визуализировать эти следы, вы можете использовать инструменты Templight, которые могут конвертировать их в другие форматы. Следуйте этим инструкциям для установки templight-convert. Мы обычно используем вывод callgrind. Вы также можете использовать вывод GraphViz, если ваш проект небольшой:
Сгенерированный файл callgrind может быть открыт с помощью kcachegrind, в котором вы можете отследить наиболее инстанцирование, которое занимает больше времени / памяти.
Сокращение количества шаблонов
Хотя нет точного решения для сокращения количества экземпляров шаблона, есть несколько рекомендаций, которые могут помочь:
Рефакторинг классов с более чем одним аргументом шаблона
Например, если у вас есть класс,
и оба из
T
иU
могут иметь 10 различных опций, вы увеличили возможные экземпляры шаблонов этого класса до 100. Один из способов решить эту проблему - абстрагировать общую часть кода в другой класс. Другой метод заключается в использовании инверсии наследования (реверсирование иерархии классов), но перед использованием этого метода убедитесь, что ваши цели проектирования не поставлены под угрозу.Рефакторинг не шаблонного кода для отдельных единиц перевода
Используя эту технику, вы можете один раз скомпилировать общий раздел и позже связать его с другими вашими TU (единицами перевода).
Использовать внешние шаблоны (начиная с C ++ 11)
Если вы знаете все возможные экземпляры класса, вы можете использовать эту технику для компиляции всех случаев в другой единице перевода.
Например, в:
Мы знаем, что этот класс может иметь три возможных варианта:
Поместите вышесказанное в единицу перевода и используйте ключевое слово extern в заголовочном файле под определением класса:
Этот метод может сэкономить ваше время, если вы компилируете различные тесты с общим набором реализаций.
Используйте единство
Основная идея Unity builds состоит в том, чтобы включить все файлы .cc, которые вы используете, в один файл и скомпилировать этот файл только один раз. Используя этот метод, вы можете избежать повторного создания общих разделов различных файлов, и если ваш проект содержит много общих файлов, вы, вероятно, также сэкономите на доступе к диску.
В качестве примера, давайте предположим , что у вас есть три файла
foo1.cc
,foo2.cc
,foo3.cc
и все они включают в себяtuple
от STL . Вы можете создатьfoo-all.cc
что выглядит так:Вы компилируете этот файл только один раз и потенциально уменьшаете общие экземпляры среди трех файлов. Трудно вообще предсказать, может ли улучшение быть значительным или нет. Но одним очевидным фактом является то, что вы потеряете параллелизм в ваших сборках (вы больше не сможете компилировать три файла одновременно).
Кроме того, если какой-либо из этих файлов занимает много памяти, вам может фактически не хватить памяти до завершения компиляции. На некоторых компиляторах, таких как GCC , это может привести к ICE (внутренней ошибке компилятора) вашего компилятора из-за нехватки памяти. Так что не используйте эту технику, если вы не знаете все плюсы и минусы.
Предварительно скомпилированные заголовки
Предварительно скомпилированные заголовки (PCH) могут сэкономить вам много времени при компиляции, скомпилировав заголовочные файлы в промежуточное представление, распознаваемое компилятором. Чтобы сгенерировать предварительно скомпилированные файлы заголовков, вам нужно только скомпилировать файл заголовка с помощью обычной команды компиляции. Например, на GCC:
Это создаст
YOUR_HEADER.hpp.gch file
(.gch
это расширение для файлов PCH в GCC) в той же папке. Это означает, что если вы включитеYOUR_HEADER.hpp
в какой-то другой файл, компилятор будет использовать вашYOUR_HEADER.hpp.gch
вместоYOUR_HEADER.hpp
той же папки ранее.Есть две проблемы с этой техникой:
all-my-headers.hpp
). Но это означает, что вы должны включить новый файл во всех местах. К счастью, у GCC есть решение этой проблемы. Используйте-include
и дайте ему новый заголовочный файл. Вы можете разделить запятыми разные файлы, используя эту технику.Например:
Используйте безымянные или анонимные пространства имен
Безымянные пространства имен (также известные как анонимные пространства имен) могут значительно уменьшить сгенерированные двоичные размеры. Неназванные пространства имен используют внутреннюю связь, то есть символы, сгенерированные в этих пространствах имен, не будут видны другим TU (единицам перевода или компиляции). Компиляторы обычно генерируют уникальные имена для безымянных пространств имен. Это означает, что если у вас есть файл foo.hpp:
И вы случайно включили этот файл в два TU (два .cc-файла и скомпилировали их отдельно). Два экземпляра шаблона foo не будут одинаковыми. Это нарушает правило единого определения (ODR). По той же причине использование безымянных пространств имен не рекомендуется в заголовочных файлах. Не стесняйтесь использовать их в своих
.cc
файлах, чтобы избежать появления символов в ваших двоичных файлах. В некоторых случаях изменение всех внутренних деталей для.cc
файла показало уменьшение сгенерированных двоичных размеров на 10%.Изменение параметров видимости
В новых компиляторах вы можете выбрать ваши символы, которые будут либо видимыми, либо невидимыми в динамических общих объектах (DSO). В идеале, изменение видимости может улучшить производительность компилятора, оптимизировать время соединения (LTO) и сгенерированные двоичные размеры. Если вы посмотрите на заголовочные файлы STL в GCC, то увидите, что они широко используются. Чтобы включить выбор видимости, вам нужно изменить свой код для каждой функции, для каждого класса, для каждой переменной и, что более важно, для каждого компилятора.
С помощью видимости вы можете скрыть символы, которые вы считаете их закрытыми, от созданных общих объектов. В GCC вы можете управлять видимостью символов, передавая значение по умолчанию или скрытое для
-visibility
опции вашего компилятора. В некотором смысле это похоже на безымянное пространство имен, но более сложным и навязчивым способом.Если вы хотите указать видимости для каждого случая, вы должны добавить следующие атрибуты в свои функции, переменные и классы:
Видимость по умолчанию в GCC - это default (public), что означает, что если вы скомпилируете вышеупомянутое как
-shared
метод shared library ( ),foo2
и классfoo3
не будет виден в других TU (foo1
иfoo4
будет виден). Если вы скомпилируете-visibility=hidden
то толькоfoo1
будет видно. Дажеfoo4
будет скрыт.Вы можете прочитать больше о видимости на вики GCC .
источник
Я бы порекомендовал эти статьи из «Игр изнутри, инди-дизайна игр и программирования»:
Конечно, они довольно старые - вам придется заново протестировать все с последними версиями (или версиями, доступными вам), чтобы получить реалистичные результаты. В любом случае, это хороший источник идей.
источник
Одна из техник, которая работала для меня довольно хорошо в прошлом: не компилируйте несколько исходных файлов C ++ независимо, а скорее генерируйте один файл C ++, который включает в себя все остальные файлы, например так:
Конечно, это означает, что вы должны перекомпилировать весь включенный исходный код на случай, если какой-либо из источников изменится, поэтому дерево зависимостей ухудшится. Тем не менее, компиляция нескольких исходных файлов за одну единицу перевода происходит быстрее (по крайней мере, в моих экспериментах с MSVC и GCC) и создает меньшие двоичные файлы. Я также подозреваю, что компилятору предоставляется больше возможностей для оптимизации (поскольку он может видеть больше кода одновременно).
Эта техника ломается в разных случаях; например, компилятор выручит в случае, если два или более исходных файла объявят глобальную функцию с тем же именем. Я не мог найти эту технику, описанную ни в одном из других ответов, поэтому я упоминаю об этом здесь.
Что бы это ни стоило, проект KDE использовал эту же технику с 1999 года для создания оптимизированных двоичных файлов (возможно, для выпуска). Переключатель на сценарий конфигурации сборки был вызван
--enable-final
. Из археологического интереса я выкопал сообщение, в котором объявили об этой функции: http://lists.kde.org/?l=kde-devel&m=92722836009368&w=2источник
<core-count> + N
разбит на подсписки, которые компилируются параллельно, гдеN
есть какое-то подходящее целое число (в зависимости от системной памяти и того, как машина используется в противном случае).На эту тему есть целая книга, которая называется « Разработка больших программ на С ++» (написана Джоном Лакосом).
Книга предшествует шаблонам, поэтому к содержанию этой книги добавьте «использование шаблонов также может замедлить работу компилятора».
источник
Я просто сошлюсь на другой мой ответ: как ВЫ сокращаете время компиляции и время компоновки для проектов Visual C ++ (нативный C ++)? , Еще один момент, который я хочу добавить, но который часто вызывает проблемы - это использование предварительно скомпилированных заголовков. Но, пожалуйста, используйте их только для частей, которые почти никогда не меняются (например, заголовки инструментария GUI). В противном случае они будут стоить вам больше времени, чем сэкономят.
Другой вариант, когда вы работаете с GNU make, включить
-j<N>
опцию:Я обычно это
3
делаю, так как у меня здесь есть два ядра. Затем он будет запускать компиляторы параллельно для разных модулей перевода при условии, что между ними нет зависимостей. Связывание не может быть выполнено параллельно, поскольку существует только один процесс компоновщика, связывающий все объектные файлы.Но сам линкер может быть многопоточным, и именно это делает ELF линкер. Это оптимизированный многопоточный код C ++, который, как говорят, связывает объектные файлы ELF на порядок быстрее, чем старый (и фактически был включен в binutils ).
GNU gold
ld
источник
Вот некоторые:
make -j2
хороший пример).-O1
чем-O2
или-O3
).источник
-j12
до примерно-j18
значительно быстрее , чем был-j8
, так же , как вы предлагаете. Мне интересно, сколько ядер вы можете иметь до того, как пропускная способность памяти станет ограничивающим фактором ...-j
из 2-х кратного количества реальных ядер.После того, как вы применили все вышеперечисленные трюки кода (предварительные объявления, сокращение включения заголовков до минимума в публичных заголовках, добавление большинства деталей в файл реализации с помощью Pimpl ...) и ничего больше не может быть получено в зависимости от языка, рассмотрите вашу систему сборки , Если вы используете Linux, рассмотрите возможность использования distcc (распределенный компилятор) и ccache (кеш-компилятор).
Первый, distcc, выполняет шаг препроцессора локально, а затем отправляет вывод первому доступному компилятору в сети. Для него требуются одинаковые версии компилятора и библиотеки во всех настроенных узлах сети.
Последний, ccache, является кэшем компилятора. Он снова выполняет препроцессор и затем проверяет с внутренней базой данных (хранящейся в локальном каталоге), был ли этот файл препроцессора уже скомпилирован с теми же параметрами компилятора. Если это так, он просто выдает двоичный файл и выводит результаты первого запуска компилятора.
Оба могут использоваться одновременно, так что, если ccache не имеет локальной копии, он может отправить его через сеть на другой узел с distcc, или он может просто внедрить решение без дальнейшей обработки.
источник
Когда я заканчивал колледж, первый настоящий производительный код C ++, который я видел, содержал эти загадочные директивы #ifndef ... #endif между ними, где были определены заголовки. Я спросил парня, который очень наивно писал код об этих всеобъемлющих вещах и познакомился с миром крупномасштабного программирования.
Возвращаясь к делу, использование директив для предотвращения дублирования определений заголовков было первым, что я узнал, когда дело дошло до сокращения времени компиляции.
источник
Больше оперативной памяти
Кто-то говорил об ОЗУ в другом ответе. Я сделал это с 80286 и Turbo C ++ (показывает возраст), и результаты были феноменальными. Как была потеря данных при сбое машины.
источник
Используйте предварительные декларации, где вы можете. Если объявление класса использует только указатель или ссылку на тип, вы можете просто объявить его и включить заголовок для типа в файл реализации.
Например:
Меньшее количество включает в себя гораздо меньше работы для препроцессора, если вы делаете это достаточно.
источник
Вы можете использовать Unity Builds .
источник
использование
в верхней части заголовочных файлов, поэтому, если они включены более одного раза в модуль перевода, текст заголовка будет включен и проанализирован только один раз.
источник
Просто для полноты: сборка может быть медленной, потому что система сборки глупа, а также потому, что компилятору требуется много времени, чтобы выполнить свою работу.
Прочитайте « Рекурсивный вред для здоровья» (PDF) для обсуждения этой темы в среде Unix.
источник
Обнови свой компьютер
Тогда у вас есть все ваши другие типичные предложения
источник
У меня была идея об использовании привода RAM . Оказалось, что для моих проектов это не так уж важно. Но тогда они еще довольно маленькие. Попытайся! Мне было бы интересно услышать, насколько это помогло.
источник
Динамическое связывание (.so) может быть намного быстрее, чем статическое связывание (.a). Особенно, когда у вас медленный сетевой диск. Это потому, что у вас есть весь код в .a файле, который необходимо обработать и записать. Кроме того, гораздо больший исполняемый файл должен быть записан на диск.
источник
Не о времени компиляции, а о времени сборки:
Используйте ccache, если вам нужно пересобрать те же файлы, когда вы работаете с вашими файлами сборки
Используйте ниндзя-билд вместо make. В настоящее время я компилирую проект с ~ 100 исходными файлами, и все кэшируется ccache. сделать нужно 5 минут, ниндзя меньше 1.
Вы можете создавать свои файлы ниндзя из cmake с помощью
-GNinja
.источник
Где ты проводишь время? Вы связаны с процессором? Память связана? Диск связан? Можете ли вы использовать больше ядер? Больше оперативной памяти? Вам нужен RAID? Вы просто хотите повысить эффективность вашей нынешней системы?
Под gcc / g ++ вы смотрели на ccache ? Это может быть полезно, если вы делаете
make clean; make
много.источник
Быстрее жестких дисков.
Компиляторы записывают много (и, возможно, огромных) файлов на диск. Работа с SSD вместо обычного жесткого диска и время компиляции значительно ниже.
источник
В Linux (и, возможно, в некоторых других * NIX) вы действительно можете ускорить компиляцию, НЕ НАЗЫВАЯ на выходе и переходя на другой TTY.
Вот эксперимент: printf замедляет мою программу
источник
Совместное использование сетевых ресурсов резко замедлит вашу сборку, так как задержка поиска высока. Для чего-то вроде Boost, это имело огромное значение для меня, даже если наш сетевой диск довольно быстрый. Время компиляции игрушечной программы Boost сократилось с 1 минуты до 1 секунды, когда я переключился с общего сетевого ресурса на локальный SSD.
источник
Если у вас многоядерный процессор, Visual Studio (2005 и более поздние версии), а также GCC поддерживают многопроцессорные компиляции. Это что-то, чтобы включить, если у вас есть оборудование, конечно.
источник
Хотя это и не «техника», я не мог понять, как проекты Win32 с множеством исходных файлов компилируются быстрее, чем мой пустой проект «Hello World». Таким образом, я надеюсь, что это помогает кому-то, как это сделал я.
В Visual Studio одним из вариантов увеличения времени компиляции является добавочное связывание ( / INCREMENTAL ). Это несовместимо с генерацией кода времени компоновки ( / LTCG ), поэтому не забывайте отключать инкрементное связывание при сборке релизов.
источник
/INCREMENTAL
в режиме отладкиНачиная с Visual Studio 2017 у вас есть возможность иметь некоторые метрики компилятора о том, что занимает время.
Добавьте эти параметры в C / C ++ -> Командная строка (Дополнительные параметры) в окне свойств проекта:
/Bt+ /d2cgsummary /d1reportTime
Вы можете получить больше информации в этом посте .
источник
Использование динамической компоновки вместо статической заставит компилятор работать быстрее.
Если вы используете t Cmake, активируйте свойство:
Build Release, используя статические ссылки, может стать более оптимизированным.
источник