Почему компиляция C++ занимает так много времени?

Компиляция файла C++ занимает очень много времени по сравнению с C# и Java. Компиляция файла C++ занимает значительно больше времени, чем запуск сценария Python обычного размера. В настоящее время я использую VC++, но то же самое и с любым компилятором. Почему это?

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

VC++ поддерживает предварительно скомпилированные заголовки. Их использование поможет. Много.

Brian 25.11.2008 22:21

Да, в моем случае (в основном C с несколькими классами - без шаблонов) предварительно скомпилированные заголовки ускоряются примерно в 10 раз

Lothar 08.09.2009 14:13

@Brian Я бы никогда не использовал предварительно скомпилированную голову в библиотеке, хотя

Cole Johnson 26.11.2012 04:05

Попробуйте TinyCC, но он просто ОЧЕНЬ мало оптимизирован

Christian Irwan Hadi Wicaksana 19.01.2015 12:49

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

Evan Carslake 03.02.2016 20:06
It takes significantly longer to compile a C++ file - вы имеете в виду 2 секунды по сравнению с 1 секундой? Конечно, вдвое длиннее, но вряд ли. Или вы имеете в виду 10 минут по сравнению с 5 секундами? Пожалуйста, дайте количественную оценку.
Nick Gammon 21.06.2016 13:38

ОТ: используйте ccache для ускорения :-)

pevik 23.07.2016 01:24
#pragma once
Sachin Joseph 05.03.2017 01:47

Ставлю на модули; Я не ожидаю, что проекты на C++ станут строиться быстрее, чем на других языках программирования, просто с модулями, но для большинства проектов он может быть очень близок при некотором управлении. Надеюсь увидеть хороший менеджер пакетов с артефакторной интеграцией после модулей

Abdurrahim 08.12.2018 06:50

Я использовал 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 ГГц.

Lyle Z 08.02.2020 20:41

@NickGammon Ну, если бы компиляция C++ длилась всего в два раза дольше, это не считалось бы значительно более длительным! С другой стороны, если бы он был в 120 раз медленнее, то его бы никто не использовал. Это больше похоже на 10 раз медленнее что-то вроде 10 файлов в секунду в C# или 1 в секунду в C++.

Phil1970 08.12.2020 04:36
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
566
11
132 896
15
Перейти к ответу Данный вопрос помечен как решенный

Ответы 15

Вот несколько причин:

1) Грамматика C++ сложнее, чем C# или Java, и требует больше времени для анализа.

2) (что более важно) компилятор C++ создает машинный код и выполняет все оптимизации во время компиляции. C# и Java идут половиной пути и оставляют эти шаги JIT.

C++ скомпилирован в машинный код. Итак, у вас есть препроцессор, компилятор, оптимизатор и, наконец, ассемблер, и все они должны работать.

Java и C# компилируются в байт-код / ​​IL, а виртуальная машина Java / .NET Framework выполняется (или JIT-компиляция в машинный код) перед выполнением.

Python - это интерпретируемый язык, который также компилируется в байт-код.

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

Стоимость предварительной обработки незначительна. Основная «другая причина» замедления заключается в том, что компиляция разбита на отдельные задачи (по одной для каждого объектного файла), поэтому общие заголовки обрабатываются снова и снова. Это O (N ^ 2) в худшем случае, по сравнению с большинством других языков, время анализа O (N).

Tom 07.12.2008 10:05

Из той же аргументации можно сказать, что компиляторы C, Pascal и т. д. Работают медленно, что в среднем неверно. Это больше связано с грамматикой C++ и огромным состоянием, которое компилятор C++ должен поддерживать.

Sebastian Mach 10.06.2011 12:40

C медленный. Он страдает той же проблемой синтаксического анализа заголовка, что и принятое решение. Например. возьмите простую программу с графическим интерфейсом Windows, которая включает windows.h в несколько единиц компиляции, и измерьте производительность компиляции по мере добавления (коротких) единиц компиляции.

Marco van de Voort 02.12.2014 15:18

Другая причина - использование препроцессора C для поиска объявлений. Даже с защитой заголовков .h все равно придется анализировать снова и снова, каждый раз, когда они включаются. Некоторые компиляторы поддерживают предварительно скомпилированные заголовки, которые могут помочь в этом, но они не всегда используются.

См. Также: C++ Часто задаваемые ответы

Я думаю, вам следует выделить комментарий к предварительно скомпилированным заголовкам жирным шрифтом, чтобы указать на эту ВАЖНУЮ часть вашего ответа.

Kevin 25.11.2008 21:37

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

CesarB 26.11.2008 04:02

@CesarB: ему все равно нужно обрабатывать его полностью один раз на единицу компиляции (файл .cpp).

Sam Harwell 25.03.2010 20:38

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

#include "BigClass.h"

class SmallClass
{
   BigClass m_bigClass;
}

Компилируется намного медленнее, чем:

class BigClass;

class SmallClass
{
   BigClass* m_bigClass;
}

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

Tom Leys 25.11.2008 22:50

Возможно, это одна из причин. Но, например, Pascal занимает лишь десятую часть времени компиляции, необходимого для эквивалентной программы на C++. Это не потому, что оптимизация gcc: s занимает больше времени, а скорее потому, что Паскаль легче анализировать и ему не нужно иметь дело с препроцессором. См. Также компилятор Digital Mars D.

Daniel O 27.03.2009 13:20

Это не простой синтаксический анализ, а модульность, позволяющая избежать переинтерпретации windows.h и множества других заголовков для каждой единицы компиляции. Да, Pascal разбирает проще (хотя зрелые, вроде Delphi, снова сложнее), но не это имеет большое значение.

Marco van de Voort 29.11.2013 19:34

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

DavidRR 08.04.2015 16:51

запись классов всего в один файл. Разве это не будет запутанный код?

Fennekin 22.10.2015 20:33
Ответ принят как подходящий

Некоторые причины

Заголовочные файлы

Каждая единица компиляции требует, чтобы были (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++. Определенно, замедление вызывает интерфейс, а не генерация кода.

Tom 07.12.2008 10:02

Согласен, как я уже сказал, это очень маленький фактор. Я упомянул об этом только потому, что видел, как это упоминалось в некоторых других ответах, и, упомянув об этом здесь для полноты, я мог, по крайней мере, указать, что это не имеет большого значения. :)

jalf 07.12.2008 20:43

Что касается шаблонов: не только vector <int> должен компилироваться отдельно от vector <double>, но vector <int> перекомпилируется в каждой единице компиляции, которая его использует. Избыточные определения удаляются компоновщиком.

David Rodríguez - dribeas 31.12.2008 17:16

dribeas: Верно, но это не относится к шаблонам. Встроенные функции или что-либо еще, определенное в заголовках, будет перекомпилировано везде, где это есть. Но да, это особенно больно с шаблонами. :)

jalf 31.12.2008 18:09

Что касается пункта 1: нельзя ли кэшировать скомпилированные файлы заголовков, возможно, один раз для каждой конфигурации макроса?

configurator 02.03.2009 01:17

@configurator: Да, их можно кешировать. Visual Studio делает это, но я не знаю подробностей. Я думаю, что gcc по умолчанию не кеширует, но это возможно.

Thomas 02.03.2009 01:45

@configurator: Visual Studio и gcc позволяют использовать предварительно скомпилированные заголовки, что может значительно ускорить компиляцию.

small_duck 02.03.2009 03:10

Томас: Есть для этого ссылка? Я не знал, что VS выполняет какую-либо форму кеширования заголовков. Хотя это кажется очевидной оптимизацией. (если вы не имели в виду предварительно скомпилированные заголовки. Я думал, что должно быть возможно что-то более общее)

jalf 02.03.2009 19:10

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

Suma 05.05.2009 23:51

Я думаю, что первые 2 причины, которые вы перечислили, являются основными, и сборка единства просто решит их.

baye 12.01.2010 17:04

@lzprgmr: проблема с единой сборкой в ​​том, что вам нужно перекомпилировать все, если у вас есть малейший шанс. Так что это тоже не серебряная пуля.

jalf 09.02.2011 12:28

jalf: Я думал, что файлы .PCH означают «предварительно скомпилированный заголовок»

Marco van de Voort 12.01.2012 15:57

@MarcovandeVoort: да, это так. Я не уверен, к чему вы клоните. Я сказал, что это означало что-то еще?

jalf 12.01.2012 16:15

@ColeJohnson: но это ничем не отличается от любого другого языка. Я попытался перечислить уникальные особенности C++

jalf 26.11.2012 13:27

Использование жесткого диска значительно продлит процесс компиляции и компоновки. Использование SSD или RAM-диска может сократить время компиляции и компоновки.

linquize 26.01.2013 10:33

Я думал, что C# будет генерировать специальный код для универсальных классов, экземпляров которых создаются с помощью типов значений, что уменьшает упаковку и использует инструкции IL, специфичные для этого типа. В отличие от дженериков Java, которые представляют собой просто конструкцию типа компиляции.

Gabriel Garcia 04.03.2013 07:41

@GabrielGarcia, все это делается во время выполнения, даже для типов значений (источник)

Stephen Lin 04.03.2013 07:52

@linquize Неправда; по крайней мере, по моему опыту. Я тестировал компиляцию большой базы кода C++ в MSVC на одном компьютере как с твердотельным накопителем, так и без него ... SSD улучшил производительность примерно на 5% ... вместо 12 минут на компиляцию потребовалось 11 и немного. Большую часть времени центральный процессор является узким местом, повторно компилируя все эти файлы заголовков снова и снова.

Orion Edwards 04.03.2013 08:13

Но светодиод жесткого диска горит дольше (особенно на этапе связывания), а не просто мигает.

linquize 04.03.2013 10:42

@StephenLin да, машинный код генерируется во время выполнения. Я имел в виду несколько неполную концепцию, представленную в исходном сообщении выше (что может привести к ряду заблуждений). «В C# List <T> - единственный тип, который компилируется, независимо от того, сколько экземпляров List у вас есть в вашей программе». Да, только один набор инструкций IL генерируется из кода C#. Нет, когда вы говорите об экземплярах (подразумевает время выполнения), другой машинный код делает компилируется из IL для каждого типа значения.

Gabriel Garcia 01.04.2013 06:38

@GabrielGarcia Хорошо, это имеет смысл, и я согласен с тем, что разъяснение важно, но, чтобы быть педантичным, компилятор C# этого не делает; это часть создания экземпляра типа с помощью CLR, которая не зависит от языка :) Кроме того, я не уверен на 100%, действительно ли CLR проходит промежуточный этап генерации IL для конкретного типа из общего IL или просто генерирует тип -специфический собственный код непосредственно при JITing. Цитата Андерса, кажется, подразумевает последнее, но, возможно, он не был на 100% точен в своей формулировке.

Stephen Lin 02.04.2013 06:34

Не уверен, является ли оптимизация проблемой, поскольку наши сборки DEBUG на самом деле медленнее, чем сборки в режиме выпуска. Генерация pdb также является виновником.

gast128 24.12.2013 15:15

@GabrielGarcia: Возможно, стоит отметить, что универсальные типы и машинный код, который их использует, создаются в .NET по запросу, в то время как C++ должен создавать во время компиляции каждый общий тип, который программа могла бы использовать (независимо от того, любой код, использующий все типы, когда-либо выполняется, когда программа получает ввод, который она действительно получает).

supercat 27.05.2015 18:59

@jalf: Я не понял, что вы имеете в виду, говоря, что «даже простая метапрограмма шаблона C++ может легко генерировать десятки или сотни классов, все из которых встраиваются и снова удаляются на этапе оптимизации». Спасибо

Destructor 03.06.2015 12:32

Кроме того, из-за шаблонов компоновщику предстоит еще много работы, дедуплицируя экземпляры шаблонов, которые были сгенерированы много раз.

plugwash 01.09.2017 15:53

Вот история из уст лошадей, то есть того, кто делает компиляторы C++ для жизни: drdobbs.com/cpp/c-compilation-speed/228701711

nyholku 11.06.2018 17:55

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

user2356685 19.06.2018 23:45

Замедление не обязательно одинаково для любого компилятора.

Я не использовал Delphi или Kylix, но еще во времена MS-DOS программа Turbo Pascal компилировалась почти мгновенно, в то время как эквивалентная программа Turbo C++ просто сканировала.

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

Конечно, возможно, что скорость компиляции просто не была приоритетом для разработчиков компилятора C++, но есть также некоторые присущие синтаксису C / C++ сложности, которые затрудняют его обработку. (Я не специалист по C, но Уолтер Брайт является экспертом, и после создания различных коммерческих компиляторов C / C++ он создал язык D. Одно из его изменений должен был обеспечить соблюдение контекстно-свободной грамматики, чтобы облегчить синтаксический анализ языка.)

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

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

Dietrich Epp 17.12.2012 08:15

@DietrichEpp Эмпиризм окупается.

Tomas Zubiri 15.02.2019 19:46

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

Однажды друг (когда ему было скучно на работе) взял приложение своей компании и поместил все - все исходные файлы и файлы заголовков - в один большой файл. Время компиляции упало с 3 часов до 7 минут.

Что ж, доступ к файлам наверняка приложил руку к этому, но, как сказал jalf, основной причиной этого будет что-то еще, а именно повторный синтаксический анализ многих, многих, многих (вложенных!) Файлов заголовков, которые полностью выпадают в вашем случае.

Konrad Rudolph 25.11.2008 22:06

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

Tom Leys 25.11.2008 22:49

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

CesarB 26.11.2008 04:03

Парсинг - это большое дело. Для N пар файлов исходного кода / заголовка одинакового размера с взаимозависимостями существует O (N ^ 2) проходов через файлы заголовков. Помещение всего текста в один файл сокращает дублирование синтаксического анализа.

Tom 07.12.2008 10:07

Небольшое примечание: включение предохраняет от многократного синтаксического анализа на единицу компиляции. Не против нескольких парсеров в целом.

Marco van de Voort 12.01.2012 15:52

Добавляя к @MarcovandeVoort, некоторые компиляторы распознают охранников include, что они есть, что позволяет компилятору вообще избежать повторного открытия файла. Другие (на данный момент, большинство) используют #pragma once, чтобы явно делать то, что неявно делают охранники включения (помечая файл как включаемый только один раз), что позволяет избежать повторного открытия файлов. Но ничто из этого не решает проблему отдельных единиц компиляции; каждый исходный файл C / C++ будет перечитывать все эти заголовки один раз, включая guard и #pragma once, просто означает, что они не перечитывают их много раз каждый.

ShadowRanger 15.06.2016 17:06

@Tom Ввод-вывод - это большое дело, и наличие файлов заголовков само по себе имеет большое значение. Не парсинг. В 1988 году у меня была секция компилятора, предназначенная только для синтаксического анализа, работающая со скоростью 50 000 строк в секунду на процессоре с тактовой частотой 2 МГц. Чуть позже я использовал давно известный компилятор Watcom C, который показывал количество строк, и он показал, что около 95% любой компиляции были заголовками. Вот почему мы предварительно компилируем заголовки.

user207421 28.01.2018 09:03

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

greggo 12.10.2020 20:31

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

Самые большие проблемы:

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 также вызывает некоторые программы только для получения информации.

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

greggo 12.10.2020 20:38

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

rileyberton 04.03.2013 05:33

хорошо, если бы вы добавили ссылку на это: stackoverflow.com/questions/543697/…, возможно, у вас не было бы так много голосов против: P, конечно, это быстрее, но я очень не одобряю этого. Ненавижу "# включать спагетти"

user1182183 04.03.2013 11:35

@rileyberton (поскольку кто-то проголосовал за ваш комментарий) позвольте мне пояснить: нет, это не ускоряет компиляцию. Фактически, он гарантирует, что любая компиляция принимает максимальное количество времени, изолируя единицы трансляции нет. Самое замечательное в них то, что вам не нужно перекомпилировать все .cpp-ы, если они не изменились. (Это без учета стилистических аргументов). Правильное управление зависимостями и, возможно, предварительно скомпилированные заголовки намного лучше.

sehe 04.03.2013 12:55

Что за объект? Это явно медленнее. Ага.

Bartek Banachewicz 04.03.2013 12:57

Извините, но этот может - очень эффективный метод для ускорения компиляции, потому что вы (1) в значительной степени исключаете связывание и (2) должны обрабатывать часто используемые заголовки только один раз. Кроме того, он работает на практике, если вы потрудитесь попробовать. К сожалению, это делает невозможными инкрементные перестройки, поэтому каждая сборка создается полностью с нуля. Но полная перестройка с помощью этого метода является намного быстрее, чем то, что вы получили бы в противном случае

jalf 04.03.2013 13:00

@jalf Как часто происходит полная перестройка по сравнению с частичной? Когда я писал помедленнее, я имел в виду «время, затрачиваемое на компиляцию среднего проекта, будет больше». И мы обсуждали это некоторое время назад в Lounge, возможно, вы захотите покопаться в архивах.

Bartek Banachewicz 04.03.2013 13:02

@BartekBanachewicz конечно, но то, что вы сказал, было то, что "это не ускоряет компиляцию", без квалификаторов. Как вы сказали, это заставляет каждую компиляцию занимать максимальное количество времени (без частичной перестройки), но в то же время он резко снижает максимальное время по сравнению с тем, что было бы в противном случае. Я просто говорю, что это немного более тонко, чем "не делай этого"

jalf 04.03.2013 13:04

@jalf Согласен. Я тщательно выбрал формулировку «это гарантирует, что любая компиляция займет максимальное количество времени». Если это может означать, что этот "максимум" несколько уменьшен по сравнению с полной модульной перестройкой ... Что ж. В последнее время я не работал ни над чем, кроме игрушек, где это было бы победой. В C++. В C баланс может быть другим.

sehe 04.03.2013 13:14

Получайте удовольствие от статических переменных и функций. Если мне нужна большая единица компиляции, я создам большой файл .cpp.

gnasher729 27.03.2014 13:17

@ gnasher729 Я включил все, и это сработало отлично. Таким образом, в моем небольшом проекте полная компиляция была быстрее, чем отдельная компиляция двух файлов .cpp, так что почти всегда это была явная победа. В то время предварительно скомпилированные заголовки еще были реализованы gcc, а заголовки STL занимали намного больше времени, чем мой код. Я также поддерживал классический make-файл, на всякий случай ...

maaartinus 18.07.2018 20:42

Сборка 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&);
};

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

Это увеличивает время компиляции двумя способами:

  1. Объем кода, который они добавляют в каждую единицу компиляции (файл .cpp), которая включает их, легко во много раз больше, чем сами файлы cpp. Чтобы представить это в перспективе, catch2.hpp составляет 18000 строк, тогда как большинство людей (даже IDE) начинают бороться с редактированием файлов размером более 1000-10000 строк.
  2. Количество файлов, которые необходимо перекомпилировать при редактировании заголовка, не входит в истинный набор файлов, которые от него зависят.

Да, есть смягчения, такие как предварительное объявление, который ощутил недостатки или сутенер идиома, которое является абстракцией с ненулевой стоимостью. Несмотря на то, что 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++, добро пожаловать в попробуй бесплатно.

Другие вопросы по теме