Чому компіляція на C ++ займає так довго?


540

Компіляція файлу C ++ займає дуже багато часу в порівнянні з C # та Java. Для збирання файлу C ++ потрібно значно більше часу, ніж для запуску сценарію Python звичайного розміру. Наразі я використовую VC ++, але це те саме, що і з будь-яким компілятором. Чому це?

Дві причини, про які я міг подумати, - це завантаження файлів заголовків та запуск препроцесора, але це, здається, не повинно пояснювати, чому це займає так довго.


58
VC ++ підтримує попередньо складені заголовки. Використання їх допоможе. Багато.
Брайан

1
Так, у моєму випадку (переважно C з кількома класами - без шаблонів) попередньо складені заголовки прискорюються приблизно в 10 разів
Lothar

@Brian Я ніколи не використовував би попередньо складену голову в бібліотеці
Коул Джонсон,

13
It takes significantly longer to compile a C++ file- ти маєш на увазі 2 секунди порівняно з 1 секундою? Звичайно, це вдвічі довше, але навряд чи значне. Або ви маєте на увазі 10 хвилин порівняно з 5 секундами? Будь ласка, уточнюйте.
Нік Гаммон

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

Відповіді:


800

Кілька причин

Файли заголовків

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

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

Зв’язування

Після компіляції всі об’єктні файли повинні бути пов'язані між собою. Це в основному монолітний процес, який не дуже добре можна паралелізувати і повинен обробляти весь ваш проект.

Розбір

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

Шаблони

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

Додайте до цього, що шаблони складають повну "підмову", яку повинен інтерпретувати компілятор, і це може стати смішно складним. Навіть порівняно простий код метапрограмування шаблону може визначати рекурсивні шаблони, які створюють десятки і десятки шаблонів шаблонів. Також шаблони можуть спричинити надзвичайно складні типи з смішно довгими назвами, що додасть багато додаткової роботи для лінкера. (Слід порівняти багато імен символів, і якщо ці імена можуть перерости у багато тисяч символів, це може стати досить дорогим).

І звичайно, вони посилюють проблеми з файлами заголовків, оскільки шаблони, як правило, мають бути визначені в заголовках, а це означає, що для кожного компіляційного блоку потрібно проаналізувати набагато більше коду. У звичайному коді С заголовок, як правило, містить лише прямі декларації, але фактично дуже мало коду. У C ++ не рідкість майже весь код розташований у файлах заголовків.

Оптимізація

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

Крім того, компілятор повинен повністю оптимізувати програму C ++. Програма AC # може розраховувати на компілятор JIT для виконання додаткових оптимізацій під час завантаження, C ++ не отримує жодних таких "друге шансів". Те, що створює компілятор, настільки ж оптимізовано, як і збирається отримати.

Машина

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

Висновок

Більшість цих факторів поділяється кодом С, який насправді збирається досить ефективно. Крок розбору набагато складніше в C ++ і може зайняти значно більше часу, але головний правопорушник, мабуть, шаблони. Вони корисні та роблять C ++ набагато більш потужною мовою, але вони також приймають свою плату за швидкістю компіляції.


38
Щодо пункту 3: Складання компіляції помітно швидше, ніж C ++. Безумовно, що інтерфейс викликає уповільнення, а не генерацію коду.
Том

72
Щодо шаблонів: не тільки вектор <int> повинен бути скомпільований окремо від вектора <double>, але вектор <int> перекомпілюється у кожну одиницю компіляції, яка ним користується. Надлишкові визначення усуваються лінкером.
Девід Родрігес - дрибес

15
dribeas: Це правда, але це не конкретно для шаблонів. Вбудовані функції або що-небудь інше, визначене в заголовках, буде перекомпільовано скрізь, де воно включено. Але так, це особливо боляче з шаблонами. :)
jalf

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. Одна з його змін полягала в застосуванні без контексту граматики для полегшення розбору мови. .)

Також ви помітите, що в основному Makefiles налаштовані так, що кожен файл збирається окремо в C, тому якщо 10 вихідних файлів використовують один і той же файл, включають файл, який включає файл, обробляється 10 разів.


38
Цікаво порівняти Паскаля, оскільки Ніклаус Вірт використав час, який потрібен компілятору, щоб скласти себе як орієнтир при розробці його мов та компіляторів. Існує історія, що після ретельного написання модуля для швидкого пошуку символів він замінив його простим лінійним пошуком, оскільки зменшений розмір коду змусив компілятор швидше збиратись.
Дітріх Епп

1
@DietrichEpp Емпіризм окупається.
Томаш

39

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

Друг одного разу (поки нудьгував на роботі), взяв заявку своєї компанії і поклав усе - всі вихідні та заголовкові файли - в один великий файл. Час компіляції впав з 3 годин до 7 хвилин.


14
Що ж, доступ до файлів впевнений, що це стосується цього, але, як зазначає jalf, основною причиною цього стане щось інше, а саме повторний аналіз багатьох, багатьох, багатьох (вкладених!) Файлів заголовків, які повністю випадають у вашому випадку.
Конрад Рудольф

9
Саме в цей момент вашому другові потрібно встановити попередньо складені заголовки, розбити залежності між різними файлами заголовків (намагайтеся уникати одного заголовка, включаючи інший, замість цього вперед оголосити) та отримати швидший жорсткий диск. Що вбік, досить дивовижна метрика.
Том Лейс

6
Якщо весь файл заголовка (крім можливих коментарів та порожніх рядків) знаходиться в межах заголовків заголовка, gcc може запам'ятати файл та пропустити його, якщо визначений правильний символ.
CesarB

11
Парсинг - велика справа. Для N пар пар файлів джерел / заголовків однакового розміру з взаємозалежністю є O (N ^ 2) проходів через файли заголовків. Якщо ввести весь текст в один файл, це скорочення аналізу дубліката.
Том

9
Невелика бічна примітка: включити захисні пристрої, що захищають від декількох розборів на одиницю компіляції. Не проти кількох розборів загалом.
Марко ван де Ворт

16

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

Дивіться також: C ++ Часто запитувані відповіді


Я думаю, що вам слід сміливо коментувати попередньо складені заголовки, щоб вказати на цю ВАЖЛИВУ частину вашої відповіді.
Кевін

6
Якщо весь файл заголовка (крім можливих коментарів та порожніх рядків) знаходиться в межах заголовків заголовка, gcc може запам'ятати файл та пропустити його, якщо визначений правильний символ.
CesarB

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
C повільно. Він страждає від тієї ж проблеми розбору заголовка, що і прийняте рішення. Наприклад, візьміть просту програму для графічного інтерфейсу Windows, яка включає windows.h в декілька компіляційних одиниць, і виміряйте продуктивність компіляції, додаючи (короткі) одиниці компіляції.
Марко ван де Ворт

13

Найбільші проблеми:

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, якщо повністю інтегрувати ланцюжок інструментів і реалізувати попередньо складені заголовки. Багато комерційних компіляторів C роблять це певною мірою.

Зауважте, що Unix-модель розбиття фактів на окремий двійковий код - це свого роду найгірша модель для Windows (з її повільним створенням процесів). Це дуже помітно при порівнянні часів збірки GCC між Windows і * nix, особливо якщо система make / config також викликає деякі програми лише для отримання інформації.


12

Побудова C / C ++: що насправді відбувається і чому це займає так довго

Відносно велика частина часу на розробку програмного забезпечення не витрачається на написання, запуск, налагодження або навіть розробку коду, а чекає його закінчення. Для того, щоб зробити все швидше, спершу ми повинні зрозуміти, що відбувається при складанні програмного забезпечення C / C ++. Етапи приблизно такі:

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

Зараз ми розглянемо кожен крок більш детально, зосередившись на тому, як їх можна зробити швидше.

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

Це перший крок, коли починають будувати. Зазвичай означає запуск сценарію налаштування або CMake, Gyp, SCons або якогось іншого інструменту. Для дуже великих сценаріїв налаштування, заснованих на Autotools, це може зайняти що-небудь від однієї секунди до декількох хвилин.

Цей крок трапляється відносно рідко. Її потрібно запускати лише при зміні конфігурацій або зміні конфігурації збірки. За винятком зміни систем побудови, не дуже багато потрібно зробити, щоб зробити цей крок швидше.

Побудувати інструмент запуску

Це те, що відбувається, коли ви запускаєте make або натискаєте на піктограму build в IDE (що зазвичай є псевдонімом для make). Бінарний інструмент збирання запускає і зчитує його конфігураційні файли, а також конфігурацію збірки, які, як правило, те саме.

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

Слід зазначити, що причина Make tako повільна - це не помилка впровадження. У синтаксисі Makefiles є кілька химерностей, які роблять дійсно швидку реалізацію майже неможливою. Ця проблема стає ще більш помітною у поєднанні з наступним кроком.

Перевірка залежності

Після того, як інструмент збірки прочитав його конфігурацію, він повинен визначити, які файли змінилися та які потрібно перекомпілювати. Файли конфігурації містять спрямований ациклічний графік, що описує залежності побудови. Цей графік зазвичай будується під час кроку налаштування. Час запуску інструменту збирання та сканер залежності запускаються при кожній збірці. Їх комбінований час виконання визначає нижню межу циклу редагування-збирання-налагодження. Для невеликих проектів цей час зазвичай складає кілька секунд. Це допустимо. Є альтернативи Make. Найшвидший з них - це Ninja, яку побудували інженери Google для Chromium. Якщо ви використовуєте CMake або Gyp для побудови, просто переключіться на їхні програми 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
Це, мабуть, одна з причин. Але Pascal, наприклад, займає десяту частину часу компіляції, який займає еквівалентна програма C ++. Це не тому, що оптимізація gcc: s займає більше часу, а швидше, що Pascal простіше розбирати і не потрібно мати справу з препроцесором. Також дивіться компілятор Digital Mars D.
Даніель О

2
Це не простіший аналіз, це модульність, яка дозволяє уникнути повторної інтерпретації windows.h та незрозумілих інших заголовків для кожного блоку компіляції. Так, Паскаль розбирає простіше (хоча зрілі, як Delphi, знову складніші), але це не є великим значенням.
Marco van de Voort

1
Метод, показаний тут, що пропонує покращення швидкості компіляції, відомий як попереднє оголошення .
DavidRR

заняття з письма лише в одному файлі. чи не буде це безладним кодом?
Fennekin

7

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

Наприклад, припустимо, що у вас є a.cpp, b.cpp і c.cpp .. створити файл: everything.cpp:

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

Потім складіть проект, просто зробивши все.cpp


3
Я не бачу заперечень проти цього методу. Якщо припустити, що ви генеруєте включає в себе скрипт або Makefile, це не проблема обслуговування. Це насправді прискорює компіляцію, не замислюючи проблеми компіляції. Ви можете заперечити споживання пам'яті на компіляцію, але це рідко є проблемою на сучасній машині. Отже, що є об'єктом такого підходу (окрім твердження, що це неправильно)?
rileyberton

9
@rileyberton (оскільки хтось підтримав ваш коментар), дозвольте мені прописати це: ні, це не прискорює компіляцію. Фактично, це гарантує, що будь-яка компіляція займає максимум часу , не виділяючи перекладацькі одиниці. Чудова річ у них полягає в тому, що вам не потрібно перекомпілювати всі .cpp-s, якщо вони не змінилися. (Це ігнорування стилістичних аргументів). Правильне управління залежністю та, можливо, попередньо складені заголовки набагато краще.
sehe

7
Вибачте, але це може бути дуже ефективним методом для прискорення компіляції, оскільки ви (1) в значній мірі виключаєте посилання, і (2) потрібно обробляти загальновживані заголовки лише один раз. Крім того, це працює на практиці , якщо ви намагаєтеся спробувати. На жаль, це робить можливими додаткові перебудови, тому кожна збірка повністю з нуля. Але повне відновлення з допомогою цього методу є набагато швидше , ніж ви отримали б в іншому випадку
jalf

4
@BartekBanachewicz впевнений, але те, що ви сказали, це те, що "це не прискорює компіляцію", без кваліфікаторів. Як ви вже сказали, це робить кожну компіляцію забирати максимум часу (без часткових перебудов), але, в той же час, це значно зменшує максимум порівняно з тим, що було б інакше. Я просто кажу, що це трохи більш нюансовано, ніж "не роби цього"
jalf

2
Розважайтеся статичними змінними та функціями. Якщо я хочу великий блок компіляції, я створю великий .cpp файл.
gnasher729

6

Деякі причини:

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

2) (Більш важливо) компілятор C ++ виробляє машинний код і робить усі оптимізації під час компіляції. C # і Java проходять лише наполовину і залишають ці кроки JIT.


5

Ви можете скористатись тим, що програма працює трохи швидше. Це може бути для вас холодним комфортом під час розробки, але це може мати велике значення, коли розробка завершиться, і програму просто запустять користувачі.


3

Більшість відповідей трохи незрозумілі, згадуючи, що C # завжди працюватиме повільніше через витрати на виконання дій, які в C ++ виконуються лише один раз у час компіляції, на цю вартість виконання також впливає залежність від часу виконання (більше речей, які потрібно завантажувати, щоб мати змогу запустити), не кажучи вже про те, що програми C # завжди матимуть більший слід пам’яті, що призводить до того, що продуктивність буде тісніше пов’язана з можливостями наявного обладнання. Те саме стосується інших мов, які інтерпретуються або залежать від VM.


3

Думаю, є два питання, які можуть вплинути на швидкість збирання ваших програм на C ++.

МОЖЛИВЕ ПИТАННЯ №1 - СКЛАДУВАННЯ ГОЛОВНИКА: (Це може бути, а може, вже не було вирішено іншою відповіддю чи коментарем.) Microsoft Visual C ++ (AKA VC ++) підтримує попередньо складені заголовки, що я настійно рекомендую. Коли ви створюєте новий проект і вибираєте тип програми, яку ви створюєте, на екрані повинно з’явитися вікно майстра налаштування. Якщо ви натиснете кнопку «Далі>» внизу, вікно перенесе вас на сторінку, яка має кілька списків функцій; переконайтеся, що прапорець біля параметра "Попередньо складений заголовок" встановлений. (ПРИМІТКА. Це був мій досвід роботи із консольними програмами Win32 в C ++, але це може бути не так у всіх видах програм на C ++.)

МОЖЛИВО ВІДПОВІДЬ №2 - МІСЦЕ, ЯКІ СТАЄТЬСЯ: Цього літа я пройшов курс програмування, і нам довелося зберігати всі наші проекти на флеш-накопичувачах 8 ГБ, оскільки комп'ютери в лабораторії, яку ми використовували, стиралися щовечора опівночі, що б стерло всю нашу роботу. Якщо ви компілюєте на зовнішній запам'ятовуючий пристрій заради переносимості / безпеки / тощо., Це може зайняти дуже багато часучас (навіть з попередньо складеними заголовками, про які я згадував вище) для складання вашої програми, особливо якщо це досить велика програма. Моєю порадою для вас у цьому випадку буде створення та компіляція програм на жорсткому диску комп’ютера, який ви використовуєте, і коли завгодно / з будь-якої причини ви хочете / потрібно припинити роботу над своїми проектами, перенесіть їх на ваш зовнішній пристрою зберігання даних, а потім натисніть на значок «Безпечно видаліть апаратне та витягнення медіа», який повинен з’являтися як невеличка флешка за маленьким зеленим колом з білою галочкою, щоб відключити її.

Я сподіваюся, що це вам допоможе; дайте мені знати, якщо це станеться! :)

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.