Почему компиляция C ++ занимает так много времени? - PullRequest
499 голосов
/ 25 ноября 2008

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

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

Ответы [ 13 ]

762 голосов
/ 25 ноября 2008

Несколько причин

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

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

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

Linking

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

Синтаксический

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

Шаблоны

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

37 голосов
/ 25 ноября 2008

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

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

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

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

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

35 голосов
/ 25 ноября 2008

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

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

15 голосов
/ 25 ноября 2008

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

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

15 голосов
/ 25 ноября 2008

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

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

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

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

12 голосов
/ 02 мая 2009

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

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

11 голосов
/ 23 апреля 2015

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

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

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

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

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

Это первый шаг при начале сборки. Обычно означает запуск скрипта конфигурирования или 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 ++, работают медленно. Однако существуют более быстрые инструменты и способы смягчения медленных частей языка.

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

7 голосов
/ 25 ноября 2008

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

#include "BigClass.h"

class SmallClass
{
   BigClass m_bigClass;
}

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

class BigClass;

class SmallClass
{
   BigClass* m_bigClass;
}
6 голосов
/ 04 марта 2013

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

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

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

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

4 голосов
/ 31 декабря 2008

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...