Компиляция файла C++ занимает очень много времени по сравнению с C# и Java. Компиляция файла C++ занимает значительно больше времени, чем запуск сценария Python обычного размера. В настоящее время я использую VC++, но то же самое и с любым компилятором. Почему это?
Две причины, о которых я мог подумать, - это загрузка файлов заголовков и запуск препроцессора, но это не похоже на объяснение того, почему это занимает так много времени.
Да, в моем случае (в основном C с несколькими классами - без шаблонов) предварительно скомпилированные заголовки ускоряются примерно в 10 раз
@Brian Я бы никогда не использовал предварительно скомпилированную голову в библиотеке, хотя
Попробуйте TinyCC, но он просто ОЧЕНЬ мало оптимизирован
Вы можете использовать предварительно скомпилированные заголовки и код C++. Когда вы компилируете свой код, он обновляет только файлы с изменениями. На это уйдет совсем немного времени. Перекомпиляция всего проекта может занять в 10 раз больше времени.
It takes significantly longer to compile a C++ file - вы имеете в виду 2 секунды по сравнению с 1 секундой? Конечно, вдвое длиннее, но вряд ли. Или вы имеете в виду 10 минут по сравнению с 5 секундами? Пожалуйста, дайте количественную оценку.
ОТ: используйте ccache для ускорения :-)
Ставлю на модули; Я не ожидаю, что проекты на C++ станут строиться быстрее, чем на других языках программирования, просто с модулями, но для большинства проектов он может быть очень близок при некотором управлении. Надеюсь увидеть хороший менеджер пакетов с артефакторной интеграцией после модулей
Я использовал g ++ в 1990-х, и тогда он был намного быстрее, чем сейчас. Должно быть наоборот. Я предполагаю, что он ужасно раздулся. «g ++ (GCC) 7.4.0» потребовалось всего 2 минуты, чтобы скомпилировать 23-строчную программу на C++, которая просто выполняет базовые операции со списками! Я использую Cygwin g ++ в Windows 10 с процессором Intel (R) Core (TM) i5-6400 @ 2,70 ГГц (4 процессора), ~ 2,7 ГГц.
@NickGammon Ну, если бы компиляция C++ длилась всего в два раза дольше, это не считалось бы значительно более длительным! С другой стороны, если бы он был в 120 раз медленнее, то его бы никто не использовал. Это больше похоже на 10 раз медленнее что-то вроде 10 файлов в секунду в C# или 1 в секунду в C++.





Вот несколько причин:
1) Грамматика C++ сложнее, чем C# или Java, и требует больше времени для анализа.
2) (что более важно) компилятор C++ создает машинный код и выполняет все оптимизации во время компиляции. C# и Java идут половиной пути и оставляют эти шаги JIT.
C++ скомпилирован в машинный код. Итак, у вас есть препроцессор, компилятор, оптимизатор и, наконец, ассемблер, и все они должны работать.
Java и C# компилируются в байт-код / IL, а виртуальная машина Java / .NET Framework выполняется (или JIT-компиляция в машинный код) перед выполнением.
Python - это интерпретируемый язык, который также компилируется в байт-код.
Я уверен, что для этого есть и другие причины, но в целом отсутствие необходимости компилировать на родной машинный язык экономит время.
Стоимость предварительной обработки незначительна. Основная «другая причина» замедления заключается в том, что компиляция разбита на отдельные задачи (по одной для каждого объектного файла), поэтому общие заголовки обрабатываются снова и снова. Это O (N ^ 2) в худшем случае, по сравнению с большинством других языков, время анализа O (N).
Из той же аргументации можно сказать, что компиляторы C, Pascal и т. д. Работают медленно, что в среднем неверно. Это больше связано с грамматикой C++ и огромным состоянием, которое компилятор C++ должен поддерживать.
C медленный. Он страдает той же проблемой синтаксического анализа заголовка, что и принятое решение. Например. возьмите простую программу с графическим интерфейсом Windows, которая включает windows.h в несколько единиц компиляции, и измерьте производительность компиляции по мере добавления (коротких) единиц компиляции.
Другая причина - использование препроцессора C для поиска объявлений. Даже с защитой заголовков .h все равно придется анализировать снова и снова, каждый раз, когда они включаются. Некоторые компиляторы поддерживают предварительно скомпилированные заголовки, которые могут помочь в этом, но они не всегда используются.
См. Также: C++ Часто задаваемые ответы
Я думаю, вам следует выделить комментарий к предварительно скомпилированным заголовкам жирным шрифтом, чтобы указать на эту ВАЖНУЮ часть вашего ответа.
Если весь файл заголовка (за исключением возможных комментариев и пустых строк) находится внутри защитных элементов заголовка, gcc может запомнить файл и пропустить его, если определен правильный символ.
@CesarB: ему все равно нужно обрабатывать его полностью один раз на единицу компиляции (файл .cpp).
Скомпилированный язык всегда требует больших начальных накладных расходов, чем интерпретируемый язык. Вдобавок, возможно, вы не очень хорошо структурировали свой код C++. Например:
#include "BigClass.h"
class SmallClass
{
BigClass m_bigClass;
}
Компилируется намного медленнее, чем:
class BigClass;
class SmallClass
{
BigClass* m_bigClass;
}
Особенно верно, если BigClass включает еще 5 файлов, которые он использует, в конечном итоге включая весь код вашей программы.
Возможно, это одна из причин. Но, например, Pascal занимает лишь десятую часть времени компиляции, необходимого для эквивалентной программы на C++. Это не потому, что оптимизация gcc: s занимает больше времени, а скорее потому, что Паскаль легче анализировать и ему не нужно иметь дело с препроцессором. См. Также компилятор Digital Mars D.
Это не простой синтаксический анализ, а модульность, позволяющая избежать переинтерпретации windows.h и множества других заголовков для каждой единицы компиляции. Да, Pascal разбирает проще (хотя зрелые, вроде Delphi, снова сложнее), но не это имеет большое значение.
Показанный здесь метод, который предлагает улучшение скорости компиляции, известен как предварительная декларация.
запись классов всего в один файл. Разве это не будет запутанный код?
Некоторые причины
Каждая единица компиляции требует, чтобы были (1) загружены и (2) скомпилированы сотни или даже тысячи заголовков. Каждый из них обычно должен быть перекомпилирован для каждой единицы компиляции, потому что препроцессор гарантирует, что результат компиляции заголовка мощь будет различаться для каждой единицы компиляции. (Макрос может быть определен в одном модуле компиляции, который изменяет содержимое заголовка).
Вероятно, это основная причина то, поскольку для каждой единицы компиляции требуется огромное количество кода, и, кроме того, каждый заголовок должен быть скомпилирован несколько раз (один раз для каждой единицы компиляции, которая его включает).
После компиляции все объектные файлы должны быть связаны вместе. По сути, это монолитный процесс, который нельзя хорошо распараллелить, и он должен обрабатывать весь ваш проект.
Синтаксис чрезвычайно сложен для анализа, сильно зависит от контекста и очень трудно устранить неоднозначность. На это уходит много времени.
В C# List<T> - единственный компилируемый тип, независимо от того, сколько экземпляров List у вас есть в вашей программе.
В C++ vector<int> - это полностью отдельный тип от vector<float>, и каждый из них должен быть скомпилирован отдельно.
Добавьте к этому, что шаблоны составляют полный по Тьюрингу «подъязык», который компилятор должен интерпретировать, и это может стать до смешного сложным. Даже относительно простой код метапрограммирования шаблонов может определять рекурсивные шаблоны, которые создают десятки и десятки экземпляров шаблонов. Шаблоны также могут приводить к очень сложным типам с смехотворно длинными именами, что добавляет компоновщику много дополнительной работы. (Приходится сравнивать множество имен символов, и если эти имена могут вырасти до многих тысяч символов, это может стать довольно дорогостоящим).
И, конечно же, они усугубляют проблемы с файлами заголовков, потому что шаблоны обычно должны быть определены в заголовках, что означает, что для каждой единицы компиляции необходимо анализировать и компилировать гораздо больше кода. В простом коде C заголовок обычно содержит только предварительные объявления, но очень мало фактического кода. В C++ нередко почти весь код находится в файлах заголовков.
C++ допускает некоторые очень существенные оптимизации. C# или Java не позволяют полностью исключить классы (они должны присутствовать для целей отражения), но даже простая метапрограмма шаблона C++ может легко генерировать десятки или сотни классов, все они встраиваются и снова удаляются на этапе оптимизации.
Более того, программа на C++ должна быть полностью оптимизирована компилятором. Программа на C# может полагаться на JIT-компилятор для выполнения дополнительных оптимизаций во время загрузки, У C++ нет таких «вторых шансов». То, что генерирует компилятор, оптимизировано настолько, насколько это возможно.
C++ компилируется в машинный код, который может быть несколько сложнее, чем использование байт-кода Java или .NET (особенно в случае x86). (Это упомянуто для полноты только потому, что это было упомянуто в комментариях и тому подобном. На практике этот шаг вряд ли займет больше, чем крошечную долю от общего времени компиляции).
Большинство из этих факторов разделяются кодом C, который на самом деле компилируется довольно эффективно. Этап синтаксического анализа намного сложнее в C++ и может занять значительно больше времени, но, вероятно, основным нарушителем являются шаблоны. Они полезны и делают C++ гораздо более мощным языком, но они также сказываются на скорости компиляции.
Что касается пункта 3: компиляция C заметно быстрее, чем C++. Определенно, замедление вызывает интерфейс, а не генерация кода.
Согласен, как я уже сказал, это очень маленький фактор. Я упомянул об этом только потому, что видел, как это упоминалось в некоторых других ответах, и, упомянув об этом здесь для полноты, я мог, по крайней мере, указать, что это не имеет большого значения. :)
Что касается шаблонов: не только vector <int> должен компилироваться отдельно от vector <double>, но vector <int> перекомпилируется в каждой единице компиляции, которая его использует. Избыточные определения удаляются компоновщиком.
dribeas: Верно, но это не относится к шаблонам. Встроенные функции или что-либо еще, определенное в заголовках, будет перекомпилировано везде, где это есть. Но да, это особенно больно с шаблонами. :)
Что касается пункта 1: нельзя ли кэшировать скомпилированные файлы заголовков, возможно, один раз для каждой конфигурации макроса?
@configurator: Да, их можно кешировать. Visual Studio делает это, но я не знаю подробностей. Я думаю, что gcc по умолчанию не кеширует, но это возможно.
@configurator: Visual Studio и gcc позволяют использовать предварительно скомпилированные заголовки, что может значительно ускорить компиляцию.
Томас: Есть для этого ссылка? Я не знал, что VS выполняет какую-либо форму кеширования заголовков. Хотя это кажется очевидной оптимизацией. (если вы не имели в виду предварительно скомпилированные заголовки. Я думал, что должно быть возможно что-то более общее)
По нашему опыту, особенно трудно (медленно) компилировать шаблоны - в нашем проекте заранее скомпилированные заголовки больше не имеют значения. Чем больше мы используем шаблоны и чем больше мы используем с ними сложные вещи (например, несколько уровней инкапсуляции, свойства, политики или даже метапрограммирование), тем больше времени занимает компиляция.
Я думаю, что первые 2 причины, которые вы перечислили, являются основными, и сборка единства просто решит их.
@lzprgmr: проблема с единой сборкой в том, что вам нужно перекомпилировать все, если у вас есть малейший шанс. Так что это тоже не серебряная пуля.
jalf: Я думал, что файлы .PCH означают «предварительно скомпилированный заголовок»
@MarcovandeVoort: да, это так. Я не уверен, к чему вы клоните. Я сказал, что это означало что-то еще?
@ColeJohnson: но это ничем не отличается от любого другого языка. Я попытался перечислить уникальные особенности C++
Использование жесткого диска значительно продлит процесс компиляции и компоновки. Использование SSD или RAM-диска может сократить время компиляции и компоновки.
Я думал, что C# будет генерировать специальный код для универсальных классов, экземпляров которых создаются с помощью типов значений, что уменьшает упаковку и использует инструкции IL, специфичные для этого типа. В отличие от дженериков Java, которые представляют собой просто конструкцию типа компиляции.
@GabrielGarcia, все это делается во время выполнения, даже для типов значений (источник)
@linquize Неправда; по крайней мере, по моему опыту. Я тестировал компиляцию большой базы кода C++ в MSVC на одном компьютере как с твердотельным накопителем, так и без него ... SSD улучшил производительность примерно на 5% ... вместо 12 минут на компиляцию потребовалось 11 и немного. Большую часть времени центральный процессор является узким местом, повторно компилируя все эти файлы заголовков снова и снова.
Но светодиод жесткого диска горит дольше (особенно на этапе связывания), а не просто мигает.
@StephenLin да, машинный код генерируется во время выполнения. Я имел в виду несколько неполную концепцию, представленную в исходном сообщении выше (что может привести к ряду заблуждений). «В C# List <T> - единственный тип, который компилируется, независимо от того, сколько экземпляров List у вас есть в вашей программе». Да, только один набор инструкций IL генерируется из кода C#. Нет, когда вы говорите об экземплярах (подразумевает время выполнения), другой машинный код делает компилируется из IL для каждого типа значения.
@GabrielGarcia Хорошо, это имеет смысл, и я согласен с тем, что разъяснение важно, но, чтобы быть педантичным, компилятор C# этого не делает; это часть создания экземпляра типа с помощью CLR, которая не зависит от языка :) Кроме того, я не уверен на 100%, действительно ли CLR проходит промежуточный этап генерации IL для конкретного типа из общего IL или просто генерирует тип -специфический собственный код непосредственно при JITing. Цитата Андерса, кажется, подразумевает последнее, но, возможно, он не был на 100% точен в своей формулировке.
Не уверен, является ли оптимизация проблемой, поскольку наши сборки DEBUG на самом деле медленнее, чем сборки в режиме выпуска. Генерация pdb также является виновником.
@GabrielGarcia: Возможно, стоит отметить, что универсальные типы и машинный код, который их использует, создаются в .NET по запросу, в то время как C++ должен создавать во время компиляции каждый общий тип, который программа могла бы использовать (независимо от того, любой код, использующий все типы, когда-либо выполняется, когда программа получает ввод, который она действительно получает).
@jalf: Я не понял, что вы имеете в виду, говоря, что «даже простая метапрограмма шаблона C++ может легко генерировать десятки или сотни классов, все из которых встраиваются и снова удаляются на этапе оптимизации». Спасибо
Кроме того, из-за шаблонов компоновщику предстоит еще много работы, дедуплицируя экземпляры шаблонов, которые были сгенерированы много раз.
Вот история из уст лошадей, то есть того, кто делает компиляторы C++ для жизни: drdobbs.com/cpp/c-compilation-speed/228701711
Я разработал практику размещения всех моих шаблонов в отдельном заголовке и создания специализаций шаблонов с использованием заголовков без шаблонов. Это позволяет компилировать шаблоны в предварительно скомпилированную библиотеку, а затем использовать их без компиляции. Больше не нужно долго компилировать библиотеку, только приложение. Это все еще может быть проблемой при создании подключаемых модулей DLL, но в остальном он работает безупречно.
Замедление не обязательно одинаково для любого компилятора.
Я не использовал Delphi или Kylix, но еще во времена MS-DOS программа Turbo Pascal компилировалась почти мгновенно, в то время как эквивалентная программа Turbo C++ просто сканировала.
Двумя основными отличиями были очень сильная модульная система и синтаксис, допускавший однопроходную компиляцию.
Конечно, возможно, что скорость компиляции просто не была приоритетом для разработчиков компилятора C++, но есть также некоторые присущие синтаксису C / C++ сложности, которые затрудняют его обработку. (Я не специалист по C, но Уолтер Брайт является экспертом, и после создания различных коммерческих компиляторов C / C++ он создал язык D. Одно из его изменений должен был обеспечить соблюдение контекстно-свободной грамматики, чтобы облегчить синтаксический анализ языка.)
Кроме того, вы заметите, что обычно Makefile настроен таким образом, что каждый файл компилируется отдельно на C, поэтому, если все 10 исходных файлов используют один и тот же включаемый файл, этот включаемый файл обрабатывается 10 раз.
Сравнивать Паскаль интересно, поскольку Никлаус Вирт использовал время, которое потребовалось компилятору для компиляции самого себя, в качестве эталона при разработке своих языков и компиляторов. Рассказывают, что после тщательного написания модуля для быстрого поиска символов он заменил его простым линейным поиском, потому что уменьшенный размер кода заставил компилятор скомпилировать себя быстрее.
@DietrichEpp Эмпиризм окупается.
Парсинг и генерация кода на самом деле довольно быстрые. Настоящая проблема - это открытие и закрытие файлов. Помните, что даже с включенной защитой компилятор все равно откроет файл .H и прочитает каждую строку (а затем проигнорирует ее).
Однажды друг (когда ему было скучно на работе) взял приложение своей компании и поместил все - все исходные файлы и файлы заголовков - в один большой файл. Время компиляции упало с 3 часов до 7 минут.
Что ж, доступ к файлам наверняка приложил руку к этому, но, как сказал jalf, основной причиной этого будет что-то еще, а именно повторный синтаксический анализ многих, многих, многих (вложенных!) Файлов заголовков, которые полностью выпадают в вашем случае.
Именно в этот момент вашему другу нужно настроить предварительно скомпилированные заголовки, разорвать зависимости между разными заголовочными файлами (постарайтесь избегать включения одного заголовка в другой, вместо этого пересылать объявление) и получить более быстрый жесткий диск. Помимо этого, довольно удивительный показатель.
Если весь файл заголовка (за исключением возможных комментариев и пустых строк) находится внутри защитных элементов заголовка, gcc может запомнить файл и пропустить его, если определен правильный символ.
Парсинг - это большое дело. Для N пар файлов исходного кода / заголовка одинакового размера с взаимозависимостями существует O (N ^ 2) проходов через файлы заголовков. Помещение всего текста в один файл сокращает дублирование синтаксического анализа.
Небольшое примечание: включение предохраняет от многократного синтаксического анализа на единицу компиляции. Не против нескольких парсеров в целом.
Добавляя к @MarcovandeVoort, некоторые компиляторы распознают охранников include, что они есть, что позволяет компилятору вообще избежать повторного открытия файла. Другие (на данный момент, большинство) используют #pragma once, чтобы явно делать то, что неявно делают охранники включения (помечая файл как включаемый только один раз), что позволяет избежать повторного открытия файлов. Но ничто из этого не решает проблему отдельных единиц компиляции; каждый исходный файл C / C++ будет перечитывать все эти заголовки один раз, включая guard и #pragma once, просто означает, что они не перечитывают их много раз каждый.
@Tom Ввод-вывод - это большое дело, и наличие файлов заголовков само по себе имеет большое значение. Не парсинг. В 1988 году у меня была секция компилятора, предназначенная только для синтаксического анализа, работающая со скоростью 50 000 строк в секунду на процессоре с тактовой частотой 2 МГц. Чуть позже я использовал давно известный компилятор Watcom C, который показывал количество строк, и он показал, что около 95% любой компиляции были заголовками. Вот почему мы предварительно компилируем заголовки.
Сколько файлов .cpp в сборке? В полной сборке компилятор должен проанализировать все заголовки для каждого файла .cpp. Вероятно, на это уходит больше времени, чем просто на чтение файлов, которые, вероятно, уже находятся в кеше ОС от предыдущей компиляции.
Вы получаете компромисс в том, что программа работает немного быстрее. Это может быть холодным утешением для вас во время разработки, но это может иметь большое значение, когда разработка завершена и программа просто запускается пользователями.
Самые большие проблемы:
1) Бесконечный повторный анализ заголовка. Уже упоминалось. Меры по устранению (например, #pragma once) обычно работают только для каждой единицы компиляции, а не для каждой сборки.
2) Тот факт, что инструментальная цепочка часто разделена на несколько двоичных файлов (make, препроцессор, компилятор, ассемблер, архиватор, impdef, компоновщик и dlltool в крайних случаях), которые все должны повторно инициализировать и перезагружать все состояние все время для каждого вызова ( компилятор, ассемблер) или каждую пару файлов (архиватор, компоновщик и dlltool).
См. Также это обсуждение на comp.compilers: http://compilers.iecc.com/comparch/article/03-11-078, особенно это:
http://compilers.iecc.com/comparch/article/02-07-128
Обратите внимание, что Джон, модератор comp.compilers, похоже, согласен, и это означает, что должно быть возможно достичь аналогичных скоростей и для C, если полностью интегрировать инструментальную цепочку и реализовать предварительно скомпилированные заголовки. Многие коммерческие компиляторы C в той или иной степени делают это.
Обратите внимание, что Unix-модель выделения всего в отдельный двоичный файл - это своего рода модель наихудшего случая для Windows (с ее медленным созданием процесса). Это очень заметно при сравнении времени сборки GCC между Windows и * nix, особенно если система make / configure также вызывает некоторые программы только для получения информации.
Другой фактор: во многих случаях методы и / или шаблонные функции, определенные в классах заголовков, избыточно компилируются в нескольких модулях компиляции, которые включают заголовок; линкер выбросит все, кроме одного.
Большинство ответов немного неясны при упоминании того, что C# всегда будет работать медленнее из-за затрат на выполнение действий, которые в C++ выполняются только один раз во время компиляции, эта стоимость производительности также влияет из-за зависимостей во время выполнения (больше вещей для загрузки, чтобы иметь возможность для запуска), не говоря уже о том, что программы на C# всегда будут иметь больший объем памяти, что приводит к тому, что производительность более тесно связана с возможностями доступного оборудования. То же самое верно и для других языков, которые интерпретируются или зависят от виртуальной машины.
Простой способ сократить время компиляции в больших проектах C++ - создать включаемый файл * .cpp, который включает все файлы cpp в вашем проекте, и скомпилировать его. Это сводит проблему взрыва заголовка до одного раза. Преимущество этого заключается в том, что ошибки компиляции по-прежнему будут ссылаться на правильный файл.
Например, предположим, что у вас есть a.cpp, b.cpp и c.cpp .. создайте файл: everything.cpp:
#include "a.cpp"
#include "b.cpp"
#include "c.cpp"
Затем скомпилируйте проект, просто сделав all.cpp
Я не вижу возражений против этого метода. Предполагая, что вы генерируете включения из скрипта или Makefile, это не проблема обслуживания. Фактически, это ускоряет компиляцию без запутывания проблем компиляции. Вы можете спорить о потреблении памяти при компиляции, но на современной машине это редко бывает проблемой. Так в чем же цель этого подхода (помимо утверждения о его неправильности)?
хорошо, если бы вы добавили ссылку на это: stackoverflow.com/questions/543697/…, возможно, у вас не было бы так много голосов против: P, конечно, это быстрее, но я очень не одобряю этого. Ненавижу "# включать спагетти"
@rileyberton (поскольку кто-то проголосовал за ваш комментарий) позвольте мне пояснить: нет, это не ускоряет компиляцию. Фактически, он гарантирует, что любая компиляция принимает максимальное количество времени, изолируя единицы трансляции нет. Самое замечательное в них то, что вам не нужно перекомпилировать все .cpp-ы, если они не изменились. (Это без учета стилистических аргументов). Правильное управление зависимостями и, возможно, предварительно скомпилированные заголовки намного лучше.
Что за объект? Это явно медленнее. Ага.
Извините, но этот может - очень эффективный метод для ускорения компиляции, потому что вы (1) в значительной степени исключаете связывание и (2) должны обрабатывать часто используемые заголовки только один раз. Кроме того, он работает на практике, если вы потрудитесь попробовать. К сожалению, это делает невозможными инкрементные перестройки, поэтому каждая сборка создается полностью с нуля. Но полная перестройка с помощью этого метода является намного быстрее, чем то, что вы получили бы в противном случае
@jalf Как часто происходит полная перестройка по сравнению с частичной? Когда я писал помедленнее, я имел в виду «время, затрачиваемое на компиляцию среднего проекта, будет больше». И мы обсуждали это некоторое время назад в Lounge, возможно, вы захотите покопаться в архивах.
@BartekBanachewicz конечно, но то, что вы сказал, было то, что "это не ускоряет компиляцию", без квалификаторов. Как вы сказали, это заставляет каждую компиляцию занимать максимальное количество времени (без частичной перестройки), но в то же время он резко снижает максимальное время по сравнению с тем, что было бы в противном случае. Я просто говорю, что это немного более тонко, чем "не делай этого"
@jalf Согласен. Я тщательно выбрал формулировку «это гарантирует, что любая компиляция займет максимальное количество времени». Если это может означать, что этот "максимум" несколько уменьшен по сравнению с полной модульной перестройкой ... Что ж. В последнее время я не работал ни над чем, кроме игрушек, где это было бы победой. В C++. В C баланс может быть другим.
Получайте удовольствие от статических переменных и функций. Если мне нужна большая единица компиляции, я создам большой файл .cpp.
@ gnasher729 Я включил все, и это сработало отлично. Таким образом, в моем небольшом проекте полная компиляция была быстрее, чем отдельная компиляция двух файлов .cpp, так что почти всегда это была явная победа. В то время предварительно скомпилированные заголовки еще были реализованы gcc, а заголовки STL занимали намного больше времени, чем мой код. Я также поддерживал классический make-файл, на всякий случай ...
Сборка C / C++: что происходит на самом деле и почему это занимает так много времени
Относительно большая часть времени разработки программного обеспечения тратится не на написание, запуск, отладку или даже разработку кода, а на ожидание завершения компиляции. Чтобы ускорить процесс, мы сначала должны понять, что происходит при компиляции программного обеспечения C / C++. Шаги примерно следующие:
Теперь мы рассмотрим каждый шаг более подробно, уделяя особое внимание тому, как их можно сделать быстрее.
Конфигурация
Это первый шаг при начале сборки. Обычно означает запуск скрипта настройки или CMake, Gyp, SCons или другого инструмента. Для очень больших скриптов настройки на основе Autotools это может занять от одной секунды до нескольких минут.
Этот шаг случается сравнительно редко. Его нужно запускать только при изменении конфигурации или изменении конфигурации сборки. Если не считать изменения систем сборки, мало что нужно сделать, чтобы ускорить этот шаг.
Запуск инструмента сборки
Вот что происходит, когда вы запускаете make или щелкаете значок сборки в IDE (который обычно является псевдонимом для make). Бинарный файл инструмента сборки запускается и считывает свои файлы конфигурации, а также конфигурацию сборки, которые обычно являются одним и тем же.
В зависимости от сложности сборки и размера это может занять от долей секунды до нескольких секунд. Само по себе это было бы не так уж и плохо. К сожалению, большинство систем сборки на основе make вызывают вызов make от десятков до сотен раз для каждой отдельной сборки. Обычно это вызвано рекурсивным использованием make (что плохо).
Следует отметить, что причина такой медленной работы Make не в ошибке реализации. В синтаксисе Makefiles есть некоторые особенности, которые делают действительно быструю реализацию практически невозможной. Эта проблема становится еще более заметной в сочетании со следующим шагом.
Проверка зависимости
После того, как инструмент сборки прочитал свою конфигурацию, он должен определить, какие файлы были изменены, а какие нужно перекомпилировать. Файлы конфигурации содержат ориентированный ациклический граф, описывающий зависимости сборки. Этот график обычно строится на этапе настройки. Время запуска инструмента сборки и сканер зависимостей запускаются при каждой сборке. Их объединенная среда выполнения определяет нижнюю границу цикла редактирования-компиляции-отладки. Для небольших проектов это время обычно составляет несколько секунд или около того. Это терпимо. Есть альтернативы Make. Самый быстрый из них - Ninja, созданный инженерами Google для Chromium. Если вы используете CMake или Gyp для сборки, просто переключитесь на их серверную часть Ninja. Вам не нужно ничего менять в самих файлах сборки, просто наслаждайтесь увеличением скорости. Однако Ninja не входит в состав большинства дистрибутивов, поэтому вам, возможно, придется установить его самостоятельно.
Компиляция
На этом этапе мы, наконец, вызываем компилятор. Срезая углы, вот примерные шаги.
Вопреки распространенному мнению, компиляция C++ на самом деле не так уж и медленна. STL медленный, и большинство инструментов сборки, используемых для компиляции C++, медленные. Однако есть более быстрые инструменты и способы смягчить медленные части языка.
Их использование требует небольшого количества смазки, но польза неоспорима. Более быстрое время сборки приводит к более счастливым разработчикам, большей гибкости и, в конечном итоге, к лучшему коду.
Я могу подумать о двух проблемах, которые могут повлиять на скорость компиляции ваших программ на C++.
ВОЗМОЖНЫЙ ВОПРОС №1 - СОСТАВЛЕНИЕ ЗАГОЛОВКИ: (Это может быть или не было уже рассмотрено в другом ответе или комментарии.) Microsoft Visual C++ (A.K.A. VC++) поддерживает предварительно скомпилированные заголовки, что я настоятельно рекомендую. Когда вы создаете новый проект и выбираете тип создаваемой программы, на вашем экране должно появиться окно мастера установки. Если вы нажмете кнопку «Далее>» внизу окна, вы попадете на страницу с несколькими списками функций; убедитесь, что установлен флажок рядом с параметром «Предварительно скомпилированный заголовок». (ПРИМЕЧАНИЕ. Я имел опыт работы с консольными приложениями Win32 на C++, но это может быть не так со всеми видами программ на C++.)
ВОЗМОЖНЫЙ ВОПРОС № 2 - МЕСТО, СОБИРАЮЩЕЕ: Этим летом я прошел курс программирования, и нам пришлось хранить все наши проекты на флеш-накопителях объемом 8 ГБ, поскольку компьютеры в лаборатории, которые мы использовали, стирались каждую ночь в полночь, что могло стереть всю нашу работу. Если вы компилируете на внешнее запоминающее устройство ради переносимости / безопасности / и т. д., Для компиляции вашей программы может потребоваться время очень долго (даже с предварительно скомпилированными заголовками, о которых я упоминал выше), особенно если это довольно большая программа. . Мой совет в этом случае - создавать и компилировать программы на жестком диске компьютера, который вы используете, и всякий раз, когда вы хотите / должны прекратить работу над своим проектом (проектами) по какой-либо причине, перенесите их на внешний запоминающее устройство, а затем щелкните значок «Безопасное извлечение оборудования и извлечение носителя», который должен появиться в виде небольшой флэш-накопителя за небольшим зеленым кружком с белой галочкой, чтобы отключить его.
Я надеюсь, это поможет вам; дайте мне знать, если это произойдет! :)
В больших объектно-ориентированных проектах существенная причина заключается в том, что C++ затрудняет ограничение зависимостей.
Частные функции должны быть перечислены в общедоступном заголовке соответствующего класса, что делает зависимости более транзитивными (заразительными), чем они должны быть:
// Ugly private dependencies
#include <map>
#include <list>
#include <chrono>
#include <stdio.h>
#include <Internal/SecretArea.h>
#include <ThirdParty/GodObjectFactory.h>
class ICantHelpButShowMyPrivatePartsSorry
{
public:
int facade(int);
private:
std::map<int, int> implementation_detail_1(std::list<int>);
std::chrono::years implementation_detail_2(FILE*);
Intern::SecretArea implementation_detail_3(const GodObjectFactory&);
};
Если этот шаблон блаженно повторяется в деревьях зависимостей заголовков, это имеет тенденцию создавать несколько «божественных заголовков», которые косвенно включают большие части всех заголовков в проекте. Они такие же всезнающие, как объекты бога, за исключением того, что это не очевидно, пока вы не нарисуете их деревья включения.
Это увеличивает время компиляции двумя способами:
Да, есть смягчения, такие как предварительное объявление, который ощутил недостатки или сутенер идиома, которое является абстракцией с ненулевой стоимостью. Несмотря на то, что C++ безграничен в том, что вы можете делать, вашим коллегам будет интересно, что вы курили, если вы слишком далеко отойдете от того, как это должно быть.
Худшая часть: если задуматься, необходимость объявлять частные функции в их общедоступных заголовках даже не требуется: моральный эквивалент функций-членов может быть и обычно имитируется в C, который не воссоздает эту проблему.
Чтобы просто ответить на этот вопрос, C++ - гораздо более сложный язык, чем другие языки, доступные на рынке. У него есть устаревшая модель включения, которая анализирует код несколько раз, а его шаблонные библиотеки не оптимизированы для скорости компиляции.
Грамматика и ADL
Давайте посмотрим на грамматическую сложность C++ на очень простом примере:
x*y;
Хотя вы, вероятно, скажете, что приведенное выше выражение является выражением с умножением, это не обязательно так в C++. Если x является типом, тогда оператор фактически является объявлением указателя. Это означает, что грамматика C++ контекстно-зависима.
Вот еще один пример:
foo<x> a;
Опять же, вы можете подумать, что это объявление переменной «a» типа foo, но это также можно интерпретировать как:
(foo < x) > a;
что сделало бы это выражением сравнения.
В C++ есть функция под названием Argument Dependent Lookup (ADL). ADL устанавливает правила, которые определяют, как компилятор ищет имя. Рассмотрим следующий пример:
namespace A{
struct Aa{};
void foo(Aa arg);
}
namespace B{
struct Bb{};
void foo(A::Aa arg, Bb arg2);
}
namespace C{
struct Cc{};
void foo(A::Aa arg, B::Bb arg2, C::Cc arg3);
}
foo(A::Aa{}, B::Bb{}, C::Cc{});
Правила ADL гласят, что мы будем искать имя «foo», учитывая все аргументы вызова функции. В этом случае все функции с именем «foo» будут рассматриваться для разрешения перегрузки. Этот процесс может занять время, особенно если имеется много перегрузок функций. В контексте шаблонов правила ADL становятся еще более сложными.
#включают
Эта команда может существенно повлиять на время компиляции. В зависимости от типа файла, который вы включаете, препроцессор может скопировать только пару строк кода или тысячи.
Кроме того, компилятор не может оптимизировать эту команду. Вы можете копировать различные фрагменты кода, которые можно изменять непосредственно перед включением, если файл заголовка зависит от макросов.
Есть несколько решений этих проблем. Вы можете использовать предварительно скомпилированные заголовки, которые являются внутренним представлением компилятора того, что было проанализировано в заголовке. Однако это невозможно сделать без усилий пользователя, поскольку предварительно скомпилированные заголовки предполагают, что заголовки не зависят от макроса.
Функция модулей обеспечивает решение этой проблемы на уровне языка. Он доступен начиная с версии C++ 20.
Шаблоны
Скорость компиляции шаблонов непростая. Каждая единица перевода, которая использует шаблоны, должна быть включена, и определения этих шаблонов должны быть доступны. Некоторые экземпляры шаблонов превращаются в экземпляры других шаблонов. В некоторых крайних случаях создание экземпляра шаблона может потреблять много ресурсов. Библиотека, использующая шаблоны и не предназначенная для скорости компиляции, может вызвать проблемы, как вы можете видеть при сравнении библиотек метапрограммирования, представленных по этой ссылке: http://metaben.ch/. Их различия в скорости компиляции значительны.
Если вы хотите понять, почему одни библиотеки метапрограммирования лучше подходят для компиляции, чем другие, посмотрите это видео о Правиле Хиля.
Заключение
C++ - это медленно компилируемый язык, потому что производительность компиляции не была наивысшим приоритетом при первоначальной разработке языка. В результате в C++ появились функции, которые могут быть эффективны во время выполнения, но не обязательно эффективны во время компиляции.
P.S - Я работаю в Incredibuild, компании по ускорению разработки программного обеспечения, специализирующейся на ускорении компиляции C++, добро пожаловать в попробуй бесплатно.
VC++ поддерживает предварительно скомпилированные заголовки. Их использование поможет. Много.