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

540

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

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

Дэн Гольдштейн
источник
58
VC ++ поддерживает предварительно скомпилированные заголовки. Использование их поможет. Много.
Брайан
1
Да, в моем случае (в основном C с несколькими классами - без шаблонов) предварительно скомпилированные заголовки увеличиваются примерно в 10 раз
Lothar
@ Брайан Я бы никогда не использовал предварительно скомпилированную голову в библиотеке
Коул Джонсон,
13
It takes significantly longer to compile a C++ file- Вы имеете в виду 2 секунды по сравнению с 1 секундой? Конечно, это вдвое больше, но вряд ли существенно. Или вы имеете в виду 10 минут по сравнению с 5 секундами? Пожалуйста, количественно.
Ник Гэммон
2
Я сделал ставку на модули; Я не ожидаю, что проекты на C ++ будут создаваться быстрее, чем на других языках программирования, только с модулями, но это может быть очень близко для большинства проектов с некоторым управлением. Я надеюсь увидеть хороший менеджер пакетов с артефактной интеграцией после модулей
Abdurrahim

Ответы:

800

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

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

Каждый отдельный модуль компиляции требует, чтобы (1) загружались и (2) компилировались сотни или даже тысячи заголовков. Каждый из них, как правило, должен быть перекомпилирован для каждого модуля компиляции, потому что препроцессор гарантирует, что результат компиляции заголовка может отличаться для каждого модуля компиляции. (Макрос может быть определен в одном модуле компиляции, который изменяет содержимое заголовка).

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

соединение

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

анализ

Синтаксис чрезвычайно сложен для синтаксического анализа, сильно зависит от контекста, и его очень сложно устранить. Это занимает много времени.

Шаблоны

В C # List<T>это единственный тип, который компилируется, независимо от того, сколько экземпляров List у вас есть в вашей программе. В C ++ vector<int>это совершенно отдельный тип vector<float>, и каждый из них должен быть скомпилирован отдельно.

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

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

оптимизация

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

Более того, программа на C ++ должна быть полностью оптимизирована компилятором. Программа AC # может полагаться на JIT-компилятор для выполнения дополнительных оптимизаций во время загрузки, C ++ не имеет таких «вторых шансов». То, что генерирует компилятор, так же оптимизировано, как и собирается.

Машина

C ++ компилируется в машинный код, который может быть несколько сложнее, чем использование байт-кода Java или .NET (особенно в случае x86). (Это упомянуто из-за полноты только потому, что это было упомянуто в комментариях и тому подобном. На практике этот шаг вряд ли займет более крошечной доли общего времени компиляции).

Вывод

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

jalf
источник
38
Что касается пункта 3: компиляция C заметно быстрее, чем C ++. Это определенно интерфейс, который вызывает замедление, а не генерацию кода.
Том
72
Относительно шаблонов: не только вектор <int> должен компилироваться отдельно от вектора <double>, но вектор <int> перекомпилируется в каждом модуле компиляции, который его использует. Избыточные определения устраняются компоновщиком.
Дэвид Родригес - dribeas
15
dribeas: Да, но это не относится к шаблонам. Встроенные функции или что-либо еще, определенное в заголовках, будут перекомпилированы везде, где они включены. Но да, это особенно больно с шаблонами. :)
Джалф
15
@configurator: Visual Studio и gcc позволяют предварительно скомпилированные заголовки, что может привести к серьезным ускорениям компиляции.
small_duck
5
Не уверен, что оптимизация является проблемой, так как наши сборки DEBUG на самом деле медленнее, чем сборки в режиме релиза. Pdb поколение также является виновником.
gast128
40

Замедление не обязательно то же самое с любым компилятором.

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

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

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

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

tangentstorm
источник
38
Интересно сравнить Паскаль, поскольку Никлаус Вирт использовал время, которое потребовалось компилятору, чтобы скомпилировать себя в качестве эталона при разработке своих языков и компиляторов. Существует история, что после тщательного написания модуля для быстрого поиска символов он заменил его простым линейным поиском, потому что уменьшенный размер кода заставил компилятор быстрее компилироваться.
Дитрих Эпп
1
@DietrichEpp Эмпиризм окупается.
Томас
40

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

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

Джеймс Керран
источник
14
Конечно, в этом есть доступ к файлам, но, как сказал Джальф, главной причиной этого будет нечто другое, а именно повторный анализ многих, многих, многих (вложенных!) Заголовочных файлов, которые полностью выпадают в вашем случае.
Конрад Рудольф
9
Именно в этот момент ваш друг должен установить предварительно скомпилированные заголовки, разорвать зависимости между различными заголовочными файлами (старайтесь избегать одного заголовка, включая другой, вместо прямого объявления) и получить более быстрый жесткий диск. Это в стороне, довольно удивительный показатель.
Том Лейс
6
Если весь заголовочный файл (за исключением возможных комментариев и пустых строк) находится в пределах защиты заголовка, gcc может запомнить файл и пропустить его, если задан правильный символ.
CesarB
11
Разбор это большое дело. Для N пар исходных / заголовочных файлов одинакового размера с взаимозависимостями существует O (N ^ 2) проходов через заголовочные файлы. Помещение всего текста в один файл сокращает этот повторный анализ.
Том
9
Небольшое примечание: Включает защиту от нескольких разборок на единицу компиляции. Не против нескольких разборов в целом.
Марко ван де Воорт
16

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

Смотрите также: C ++ часто задаваемые вопросы

Дэйв Рэй
источник
Я думаю, что вы должны выделить комментарий к предварительно скомпилированным заголовкам, чтобы указать на эту ВАЖНУЮ часть вашего ответа.
Кевин
6
Если весь заголовочный файл (за исключением возможных комментариев и пустых строк) находится в пределах защиты заголовка, gcc может запомнить файл и пропустить его, если задан правильный символ.
ЦезарьБ
5
@CesarB: он все еще должен обработать его полностью один раз на единицу компиляции (файл .cpp).
Сэм Харвелл
16

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

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

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

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

Алан
источник
15
Стоимость, добавленная предварительной обработкой, тривиальна. Основная «другая причина» замедления заключается в том, что компиляция разбивается на отдельные задачи (по одной на объектный файл), поэтому общие заголовки обрабатываются снова и снова. Это O (N ^ 2) наихудший случай, в отличие от времени анализа большинства других языков O (N).
Том
12
Из той же аргументации можно сказать, что компиляторы C, Pascal и т. Д. Работают медленно, что в среднем неверно. Это больше связано с грамматикой C ++ и огромным состоянием, которое должен поддерживать компилятор C ++.
Себастьян Мах
2
С медленно. Он страдает от той же проблемы разбора заголовка, что и принятое решение. Например, возьмите простую программу Windows GUI, которая включает в себя windows.h в несколько единиц компиляции, и измерьте производительность компиляции при добавлении (коротких) единиц компиляции.
Марко ван де Воорт
14

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

1) Бесконечный повторный заголовок. Уже упоминалось. Смягчения (например, #pragma один раз) обычно работают только на единицу компиляции, а не на сборку.

2) тот факт, что цепочка инструментов часто разделяется на несколько двоичных файлов (make, препроцессор, компилятор, ассемблер, архиватор, impdef, linker и 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, если кто-то полностью интегрирует цепочку инструментов и реализует предварительно скомпилированные заголовки. Многие коммерческие компиляторы Си делают это в некоторой степени.

Обратите внимание, что Unix-модель разделения всего на отдельный двоичный файл является своего рода худшей моделью для Windows (с ее медленным созданием процесса). Это очень заметно при сравнении времени сборки GCC между Windows и * nix, особенно если система make / configure также вызывает некоторые программы только для получения информации.

Марко ван де Воорт
источник
13

Сборка C / C ++: что на самом деле происходит и почему так долго

Относительно большая часть времени разработки программного обеспечения не тратится на написание, запуск, отладку или даже проектирование кода, а на ожидание завершения его компиляции. Чтобы ускорить процесс, мы сначала должны понять, что происходит, когда компилируется программное обеспечение C / C ++. Шаги примерно таковы:

  • конфигурация
  • Запуск инструмента сборки
  • Проверка зависимостей
  • компиляция
  • соединение

Теперь мы рассмотрим каждый шаг более подробно, сосредоточив внимание на том, как их можно сделать быстрее.

конфигурация

Это первый шаг при начале сборки. Обычно означает запуск скрипта конфигурирования или CMake, Gyp, SCons или другого инструмента. Для очень больших скриптов конфигурирования на основе Autotools это может занять от одной секунды до нескольких минут.

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

Запуск инструмента сборки

Это то, что происходит, когда вы запускаете make или нажимаете на значок сборки в IDE (обычно это псевдоним для make). Двоичный файл инструмента сборки запускается и считывает свои файлы конфигурации, а также конфигурацию сборки, что обычно является одним и тем же.

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

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

Проверка зависимостей

Как только инструмент сборки прочитает свою конфигурацию, он должен определить, какие файлы были изменены, а какие нужно перекомпилировать. Файлы конфигурации содержат ориентированный ациклический граф, описывающий зависимости сборки. Этот график обычно строится на этапе настройки. Время запуска инструмента сборки и сканер зависимостей запускаются при каждой сборке. Их объединенная среда выполнения определяет нижнюю границу цикла edit-compile-debug. Для небольших проектов это время обычно составляет несколько секунд или около того. Это терпимо. Есть альтернативы, чтобы сделать. Самым быстрым из них является Ninja, созданный инженерами Google для Chromium. Если вы используете CMake или Gyp для сборки, просто переключитесь на их бэкэнды Ninja. Вам не нужно ничего менять в самих файлах сборки, просто наслаждайтесь ускорением. Ninja не упакован в большинстве дистрибутивов, хотя,

компиляция

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

  • Слияние включает
  • Разбор кода
  • Генерация кода / оптимизация

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

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

Равиндра Ачарья
источник
9

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

#include "BigClass.h"

class SmallClass
{
   BigClass m_bigClass;
}

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

class BigClass;

class SmallClass
{
   BigClass* m_bigClass;
}
Энди Брайс
источник
3
Особенно верно, если BigClass включает в себя еще 5 файлов, которые он использует, в конечном итоге включая весь код в вашей программе.
Том Лейс
7
Это, возможно, одна из причин. Но Паскаль, например, просто занимает десятую часть времени компиляции, которую занимает эквивалентная программа C ++. Это не потому, что оптимизация gcc: s занимает больше времени, а в том, что Pascal легче анализировать и ему не нужно иметь дело с препроцессором. Также см. Digital Mars D компилятор.
Даниэль О
2
Это не простой синтаксический анализ, а модульность, позволяющая избежать повторной интерпретации windows.h и множества других заголовков для каждого модуля компиляции. Да, Pascal анализирует легче (хотя зрелые, такие как Delphi, снова стали более сложными), но это не то, что имеет большое значение.
Марко ван де Воорт
1
Методика, показанная здесь, которая предлагает улучшение скорости компиляции, называется предварительным объявлением .
DavidRR
написание классов в одном файле. не будет ли это грязным кодом?
Феннекин
8

Простой способ сократить время компиляции в больших проектах C ++ - сделать включаемый файл * .cpp, который включает все файлы cpp в вашем проекте, и скомпилировать его. Это уменьшает проблему взрыва заголовка до одного раза. Преимущество этого заключается в том, что ошибки компиляции будут по-прежнему ссылаться на правильный файл.

Например, предположим, что у вас есть a.cpp, b.cpp и c.cpp .. создайте файл: everything.cpp:

#include "a.cpp"
#include "b.cpp"
#include "c.cpp"

Затем скомпилируйте проект, просто сделав everything.cpp

rileyberton
источник
3
Я не вижу возражений против этого метода. Предполагая, что вы генерируете включения из скрипта или Makefile, это не проблема обслуживания. Фактически это ускоряет компиляцию, не запутывая проблемы компиляции. Вы могли бы поспорить о потреблении памяти при компиляции, но это редко является проблемой на современном компьютере. Так в чем же цель этого подхода (помимо утверждения, что это неправильно)?
Рилибертон
9
@rileyberton (так как кто-то проголосовал за ваш комментарий), позвольте мне изложить его: нет, это не ускоряет компиляцию. Фактически, он гарантирует, что любая компиляция займет максимальное количество времени , не изолируя единицы перевода. Самое замечательное в них то, что вам не нужно перекомпилировать все .cpp-ы, если они не изменились. (Это без учета стилистических аргументов). Правильное управление зависимостями и, возможно, предварительно скомпилированные заголовки намного лучше.
Сех
7
Извините, но это может быть очень эффективным методом для ускорения компиляции, потому что вы (1) в значительной степени исключаете ссылки, и (2) нужно обрабатывать обычно используемые заголовки только один раз. Кроме того, это работает на практике , если вы пытаетесь это попробовать. К сожалению, это делает невозможным постепенное перестроение, поэтому каждая сборка полностью с нуля. Но полное восстановление с помощью этого метода является намного быстрее , чем вы получили бы в противном случае
jalf
4
@BartekBanachewicz конечно, но вы сказали, что «это не ускоряет компиляцию», без квалификаторов. Как вы сказали, каждая компиляция занимает максимальное количество времени (без частичной перестройки), но в то же время значительно снижает максимальную по сравнению с тем, что было бы иначе. Я просто говорю, что это немного больше нюансов, чем «не делай этого»
13:04
2
Веселитесь со статическими переменными и функциями. Если я хочу большой модуль компиляции, я создам большой файл .cpp.
gnasher729
6

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

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

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

Неманья Трифунович
источник
5

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

ТЕД
источник
4

Большинство ответов немного неясно, когда упоминается, что C # всегда будет работать медленнее из-за затрат на выполнение действий, которые в C ++ выполняются только один раз во время компиляции, на эту производительность также влияют зависимости времени выполнения (больше вещей для загрузки, чтобы иметь возможность запускать), не говоря уже о том, что программы на C # всегда будут иметь больший объем памяти, и все это приведет к тому, что производительность будет более тесно связана с возможностями доступного оборудования. То же самое относится и к другим языкам, которые интерпретируются или зависят от виртуальной машины.

Паника
источник
4

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

ВОЗМОЖНЫЙ ВЫПУСК № 1 - СОСТАВЛЕНИЕ ЖАТКИ: (Это может или не может быть уже помощью другого ответа или комментария.) Microsoft Visual C ++ (AKA VC ++) поддерживает предварительно скомпилированные заголовки, которые я настоятельно рекомендую. Когда вы создаете новый проект и выбираете тип программы, которую вы делаете, на экране должно появиться окно мастера установки. Если вы нажмете кнопку «Далее>» в нижней части окна, откроется окно с несколькими списками функций; убедитесь, что флажок рядом с опцией «Precompiled header» установлен. (ПРИМЕЧАНИЕ: это мой опыт работы с консольными приложениями Win32 на C ++, но это может быть не так для всех видов программ на C ++.)

ВОЗМОЖНАЯ ВОПРОС № 2 - МЕСТО, КОТОРОЕ СОБИРАЕТСЯ: Этим летом я прошел курс программирования, и нам пришлось хранить все наши проекты на флэш-накопителях 8 ГБ, поскольку компьютеры в лаборатории, которую мы использовали, стирались каждую ночь в полночь, которая бы стерла всю нашу работу. Если вы компилируете на внешнее устройство хранения данных для переносимости / безопасности / и т. Д., Это может занять очень много времени. время (даже с предварительно скомпилированными заголовками, о которых я упоминал выше) для вашей программы для компиляции, особенно если это довольно большая программа. Мой совет для вас в этом случае будет состоять в том, чтобы создавать и компилировать программы на жестком диске компьютера, который вы используете, и всякий раз, когда вы захотите или по какой-либо причине прекратите работу над вашими проектами, перенесите их на ваш внешний устройства хранения, а затем щелкните значок «Безопасное извлечение устройства и извлечения носителя», который должен появиться в виде небольшой флэш-накопителя за небольшим зеленым кружком с белой галочкой на нем, чтобы отключить его.

Я надеюсь, это поможет вам; дайте мне знать, если это так! :)

cjor530
источник