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

483

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

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

  • 55
    VC ++ поддерживает предварительно скомпилированные заголовки. Использование их поможет. Много.
  • 1
    Да, в моем случае (в основном C с несколькими классами - без шаблонов) предварительно скомпилированные заголовки ускоряются примерно в 10 раз
Показать ещё 7 комментариев
Теги:
performance
compiler-construction
compilation

14 ответов

709
Лучший ответ

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

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

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

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

соединение

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

анализ

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

Шаблоны

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

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

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

оптимизация

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

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

Машина

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

Заключение

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

  • 33
    Что касается пункта 3: компиляция C заметно быстрее, чем C ++. Это определенно интерфейс, который вызывает замедление, а не генерацию кода.
  • 0
    Согласен, как я уже сказал, это очень маленький фактор. Я упомянул об этом только потому, что видел, что это упоминалось в некоторых других ответах, и, упомянув это здесь из-за полноты, я мог, по крайней мере, указать, что это не имело большого значения. :)
Показать ещё 27 комментариев
36

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

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

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

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

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

  • 35
    Интересно сравнить Паскаль, так как Никлаус Вирт использовал время, которое потребовалось компилятору, чтобы скомпилировать себя в качестве эталона при разработке своих языков и компиляторов. Существует история о том, что после тщательного написания модуля для быстрого поиска символов он заменил его простым линейным поиском, потому что уменьшенный размер кода заставил компилятор работать быстрее.
  • 0
    @DietrichEpp Эмпиризм окупается.
34

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

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

  • 13
    Конечно, в этом есть доступ к файлам, но, как сказал Джальф, главной причиной этого будет нечто иное, а именно повторный анализ многих, многих, многих (вложенных!) Заголовочных файлов, которые полностью выпадают в вашем случае.
  • 8
    Именно в этот момент ваш друг должен установить предварительно скомпилированные заголовки, разорвать зависимости между различными заголовочными файлами (старайтесь избегать одного заголовка, включая другой, вместо прямого объявления) и получить более быстрый жесткий диск. Это в стороне, довольно удивительный показатель.
Показать ещё 7 комментариев
16

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

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

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

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

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

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

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

  • 0
    Я думаю, что вы должны выделить комментарий к предварительно скомпилированным заголовкам, чтобы указать на эту ВАЖНУЮ часть вашего ответа.
  • 5
    Если весь заголовочный файл (за исключением возможных комментариев и пустых строк) находится внутри защиты заголовка, gcc может запомнить файл и пропустить его, если задан правильный символ.
Показать ещё 1 комментарий
11

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

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

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

9

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

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

  • Конфигурация
  • Сборка встроенного инструмента
  • Проверка зависимостей
  • Подборка
  • Связь

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

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

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

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

Сборка встроенного инструмента

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

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

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

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

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

Компиляция

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

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

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

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

7

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

#include "BigClass.h"

class SmallClass
{
   BigClass m_bigClass;
}

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

class BigClass;

class SmallClass
{
   BigClass* m_bigClass;
}
  • 2
    Особенно верно, если BigClass включает в себя еще 5 файлов, которые он использует, в конечном итоге включая весь код в вашей программе.
  • 7
    Это, возможно, одна из причин. Но Паскаль, например, просто занимает десятую часть времени компиляции, что эквивалентна программе на С ++. Это не потому, что оптимизация gcc: s занимает больше времени, а в том, что Pascal легче анализировать и ему не нужно иметь дело с препроцессором. Также см. Digital Mars D компилятор.
Показать ещё 3 комментария
5

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

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

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

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

  • 3
    Я не вижу возражений против этого метода. Предполагая, что вы генерируете включения из скрипта или Makefile, это не проблема обслуживания. Фактически это ускоряет компиляцию, не запутывая проблемы компиляции. Вы могли бы поспорить о потреблении памяти при компиляции, но это редко является проблемой на современном компьютере. Так в чем же цель этого подхода (помимо утверждения, что это неправильно)?
  • 0
    хорошо, если вы добавите ссылку на это: stackoverflow.com/questions/543697/… у вас, возможно, не будет так много отрицательных отзывов: P Конечно, это быстрее, но я очень не рекомендую это. Я ненавижу "#include спагетти"
Показать ещё 8 комментариев
4

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

4

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

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

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

2

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

1

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

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

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

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

0

Как уже отмечалось, компилятор тратит много времени на создание экземпляра и снова создает шаблоны. До такой степени, что есть проекты, которые сосредоточены на этом конкретном предмете, и требуют наблюдаемого 30-кратного ускорения в некоторых действительно благоприятных случаях. См. http://www.zapcc.com.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню