6

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

14

Проблема с долгим временем компиляции C++

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

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

Каковы причины такой задержки? Есть ли способы оптимизировать процесс компиляции C++?

5 ответ(ов)

8

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

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

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

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

Линковка

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

Парсинг

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

Шаблоны

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

0

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

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

0

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

Java и C# компилируются в байт-код или IL, который затем выполняется виртуальной машиной Java или .NET Framework. При этом, возможно, также выполняется JIT-компиляция в машинный код перед выполнением программы.

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

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

0

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Компиляция

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

  • Объединение include-файлов
  • Парсинг кода
  • Генерация/оптимизация кода

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

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

0

Самые большие проблемы заключаются в следующем:

  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 делают это в той или иной степени.

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

Чтобы ответить на вопрос, пожалуйста, войдите или зарегистрируйтесь