Perché la compilazione C ++ richiede così tanto tempo?


540

La compilazione di un file C ++ richiede molto tempo rispetto a C # e Java. Ci vuole molto più tempo per compilare un file C ++ rispetto a eseguire uno script Python di dimensioni normali. Attualmente sto usando VC ++ ma è lo stesso con qualsiasi compilatore. Perchè è questo?

I due motivi che mi sono venuti in mente sono stati il ​​caricamento dei file di intestazione e l'esecuzione del preprocessore, ma non sembra che dovrebbe spiegare il motivo per cui impiega così tanto tempo.


58
VC ++ supporta le intestazioni precompilate. Usarli aiuterà. Un sacco.
Brian,

1
Sì nel mio caso (principalmente C con poche classi - nessun modello) le intestazioni precompilate accelerano di circa 10 volte
Lothar

@Brian Non userei mai una testa pre-compilata in una libreria
Cole Johnson,

13
It takes significantly longer to compile a C++ file- vuoi dire 2 secondi rispetto a 1 secondo? Certamente è il doppio del tempo, ma difficilmente significativo. O intendi 10 minuti rispetto a 5 secondi? Si prega di quantificare.
Nick Gammon,

2
Ho scommesso sui moduli; Non mi aspetto che i progetti C ++ diventino più veloci da realizzare rispetto ad altri linguaggi di programmazione solo con i moduli, ma può avvicinarsi molto alla maggior parte dei progetti con una certa gestione. Spero di vedere un buon gestore di pacchetti con integrazione artificiale dopo i moduli
Abdurrahim

Risposte:


800

Diverse ragioni

File di intestazione

Ogni singola unità di compilazione richiede che centinaia o persino migliaia di intestazioni siano caricate (1) e (2) compilate. Ognuno di essi deve in genere essere ricompilato per ogni unità di compilazione, poiché il preprocessore garantisce che il risultato della compilazione di un'intestazione possa variare tra ogni unità di compilazione. (Una macro può essere definita in un'unità di compilazione che modifica il contenuto dell'intestazione).

Questo è probabilmente il motivo principale, poiché richiede la compilazione di enormi quantità di codice per ogni unità di compilazione e, inoltre, ogni intestazione deve essere compilata più volte (una volta per ogni unità di compilazione che lo include).

Collegamento

Una volta compilati, tutti i file degli oggetti devono essere collegati insieme. Questo è fondamentalmente un processo monolitico che non può essere ben parallelizzato e deve elaborare l'intero progetto.

parsing

La sintassi è estremamente complicata da analizzare, dipende fortemente dal contesto ed è molto difficile da chiarire. Questo richiede molto tempo.

Modelli

In C #, List<T>è l'unico tipo che viene compilato, indipendentemente da quante istanze di Elenco hai nel tuo programma. In C ++, vector<int>è un tipo completamente separato da vector<float>, e ognuno dovrà essere compilato separatamente.

Aggiungete a ciò che i template formano un "sotto-linguaggio" completo di Turing che il compilatore deve interpretare, e questo può diventare ridicolmente complicato. Anche il codice di metaprogrammazione dei modelli relativamente semplice può definire modelli ricorsivi che creano dozzine e dozzine di istanze di modelli. I modelli possono anche risultare in tipi estremamente complessi, con nomi ridicolmente lunghi, aggiungendo molto lavoro extra al linker. (Deve confrontare molti nomi di simboli, e se questi nomi possono crescere in molte migliaia di caratteri, ciò può diventare abbastanza costoso).

E, naturalmente, esacerbano i problemi con i file di intestazione, poiché i modelli devono generalmente essere definiti nelle intestazioni, il che significa che molto più codice deve essere analizzato e compilato per ogni unità di compilazione. Nel semplice codice C, un'intestazione in genere contiene solo dichiarazioni forward, ma pochissimo codice effettivo. In C ++, non è raro che quasi tutto il codice risieda nei file di intestazione.

Ottimizzazione

C ++ consente alcune ottimizzazioni molto drammatiche. C # o Java non consentono di eliminare completamente le classi (devono essere presenti a scopo di riflessione), ma anche un semplice metaprogramma di modello C ++ può facilmente generare dozzine o centinaia di classi, tutte incorporate ed eliminate di nuovo nell'ottimizzazione fase.

Inoltre, un programma C ++ deve essere completamente ottimizzato dal compilatore. Il programma AC # può fare affidamento sul compilatore JIT per eseguire ulteriori ottimizzazioni in fase di caricamento, C ++ non ottiene tali "seconde possibilità". Ciò che genera il compilatore è ottimizzato come otterrà.

Macchina

Il C ++ viene compilato in codice macchina che può essere leggermente più complicato dell'utilizzo del bytecode Java o .NET (specialmente nel caso di x86). (Questo è menzionato per completezza solo perché è stato menzionato nei commenti e simili. In pratica, è improbabile che questo passaggio richieda più di una piccola frazione del tempo totale di compilazione).

Conclusione

La maggior parte di questi fattori sono condivisi dal codice C, che in realtà viene compilato in modo abbastanza efficiente. Il passaggio di analisi è molto più complicato in C ++ e può richiedere molto più tempo, ma il principale offensore è probabilmente il template. Sono utili e rendono C ++ un linguaggio molto più potente, ma prendono anche il loro pedaggio in termini di velocità di compilazione.


38
Per quanto riguarda il punto 3: la compilazione in C è notevolmente più veloce di C ++. È sicuramente il frontend che causa il rallentamento e non la generazione del codice.
Tom,

72
Per quanto riguarda i template: non solo il vettore <int> deve essere compilato separatamente dal vettore <double>, ma il vettore <int> viene ricompilato in ogni unità di compilazione che lo utilizza. Le definizioni ridondanti vengono eliminate dal linker.
David Rodríguez - dribeas,

15
dribeas: True, ma non è specifico per i template. Le funzioni incorporate o qualsiasi altra cosa definita nelle intestazioni verrà ricompilata ovunque sia inclusa. Ma sì, è particolarmente doloroso con i modelli. :)
jalf

15
@configurator: Visual Studio e gcc consentono entrambe le intestazioni precompilate, il che può portare a serie accelerazioni della compilazione.
small_duck

5
Non sono sicuro che l'ottimizzazione sia il problema, poiché le nostre build DEBUG sono in realtà più lente delle build in modalità di rilascio. Anche la generazione di pdb è colpevole.
gast128,

40

Il rallentamento non è necessariamente lo stesso con qualsiasi compilatore.

Non ho usato Delphi o Kylix ma ai tempi di MS-DOS, un programma Turbo Pascal si sarebbe compilato quasi istantaneamente, mentre il programma equivalente Turbo C ++ sarebbe semplicemente strisciato.

Le due differenze principali erano un modulo molto potente e una sintassi che consentiva la compilazione a passaggio singolo.

È certamente possibile che la velocità di compilazione non sia stata una priorità per gli sviluppatori di compilatori C ++, ma ci sono anche alcune complicazioni intrinseche nella sintassi C / C ++ che rendono più difficile l'elaborazione. (Non sono un esperto di C, ma Walter Bright lo è, e dopo aver creato vari compilatori C / C ++ commerciali, ha creato il linguaggio D. Una delle sue modifiche è stata quella di applicare una grammatica senza contesto per rendere il linguaggio più facile da analizzare .)

Inoltre, noterai che generalmente i Makefile sono impostati in modo che ogni file sia compilato separatamente in C, quindi se 10 file sorgente usano tutti lo stesso file include, quel file include viene elaborato 10 volte.


38
È interessante confrontare Pascal, dal momento che Niklaus Wirth ha impiegato il tempo impiegato dal compilatore per compilarsi come punto di riferimento durante la progettazione dei suoi linguaggi e compilatori. C'è una storia che dopo aver scritto attentamente un modulo per una rapida ricerca dei simboli, lo ha sostituito con una semplice ricerca lineare perché le dimensioni ridotte del codice hanno reso il compilatore più veloce.
Dietrich Epp il

1
@DietrichEpp L'empirismo paga.
Tomas Zubiri,

40

L'analisi e la generazione del codice sono in realtà piuttosto veloci. Il vero problema è aprire e chiudere i file. Ricorda, anche con include guards, il compilatore ha ancora aperto il file .H e letto ogni riga (e quindi ignoralo).

Un amico una volta (mentre era annoiato al lavoro), prese l'applicazione della sua azienda e mise tutto - tutti i file di origine e di intestazione - in un unico grande file. Il tempo di compilazione è passato da 3 ore a 7 minuti.


14
Bene, l'accesso ai file ha sicuramente una mano in questo, ma come ha detto jalf, la ragione principale di ciò sarà qualcos'altro, vale a dire l'analisi ripetuta di molti, molti, molti file di testata (nidificati!) Che si interrompono completamente nel tuo caso.
Konrad Rudolph,

9
È a quel punto che il tuo amico deve impostare intestazioni precompilate, spezzare le dipendenze tra i diversi file di intestazione (cerca di evitare un'intestazione compresa un'altra, invece procedi in avanti) e ottieni un HDD più veloce. A parte questo, una metrica piuttosto sorprendente.
Tom Leys,

6
Se l'intero file di intestazione (tranne i possibili commenti e le righe vuote) è all'interno delle protezioni di intestazione, gcc è in grado di ricordare il file e saltarlo se viene definito il simbolo corretto.
CesarB,

11
L'analisi è un grosso problema. Per N coppie di file sorgente / intestazione di dimensioni simili con interdipendenze, ci sono O (N ^ 2) che passano attraverso i file di intestazione. Mettere tutto il testo in un singolo file sta tagliando quell'analisi duplicata.
Tom,

9
Piccola nota a margine: la guardia include include più analisi per unità di compilazione. Non contro più analisi complessive.
Marco van de Voort,

16

Un altro motivo è l'uso del pre-processore C per l'individuazione delle dichiarazioni. Anche con le protezioni per le intestazioni, .h devono ancora essere analizzate più volte, ogni volta che sono incluse. Alcuni compilatori supportano intestazioni precompilate che possono aiutare in questo, ma non vengono sempre utilizzati.

Vedi anche: C ++ Risposte a domande frequenti


Penso che dovresti audire il commento sulle intestazioni precompilate per sottolineare questa parte IMPORTANTE della tua risposta.
Kevin,

6
Se l'intero file di intestazione (tranne i possibili commenti e le righe vuote) è all'interno delle protezioni di intestazione, gcc è in grado di ricordare il file e saltarlo se viene definito il simbolo corretto.
CesarB,

5
@CesarB: deve ancora elaborarlo completamente una volta per unità di compilazione (file .cpp).
Sam Harwell,

16

C ++ viene compilato nel codice macchina. Quindi hai il pre-processore, il compilatore, l'ottimizzatore e infine l'assemblatore, che devono essere eseguiti tutti.

Java e C # vengono compilati in byte-code / IL e la macchina virtuale Java / .NET Framework vengono eseguiti (o compilati JIT in codice macchina) prima dell'esecuzione.

Python è un linguaggio interpretato che viene anche compilato in codice byte.

Sono sicuro che ci sono anche altri motivi per questo, ma in generale, non dover compilare in linguaggio macchina nativo consente di risparmiare tempo.


15
Il costo aggiunto dalla pre-elaborazione è banale. Il principale "altro motivo" per un rallentamento è che la compilazione è suddivisa in attività separate (una per file oggetto), quindi le intestazioni comuni vengono elaborate più volte. Quello è O (N ^ 2) nel caso peggiore, rispetto alla maggior parte delle altre lingue O (N) tempo di analisi.
Tom,

12
Dalla stessa argomentazione si potrebbe dire che i compilatori C, Pascal ecc. Sono lenti, il che non è mediamente vero. Ha più a che fare con la grammatica di C ++ e l'enorme stato che un compilatore C ++ deve mantenere.
Sebastian Mach,

2
C è lento. Soffre dello stesso problema di analisi dell'intestazione della soluzione accettata. Ad esempio, prendere un semplice programma GUI di Windows che include windows.h in alcune unità di compilazione e misurare le prestazioni di compilazione quando si aggiungono (brevi) unità di compilazione.
Marco van de Voort,

14

I maggiori problemi sono:

1) La replica infinita dell'intestazione. Già menzionato. Le mitigazioni (come #pragma una volta) di solito funzionano solo per unità di compilazione, non per build.

2) Il fatto che la toolchain sia spesso separata in più binari (make, preprocessore, compilatore, assemblatore, archiviatore, impdef, linker e dlltool in casi estremi) che tutti devono reinizializzare e ricaricare tutti gli stati continuamente per ogni invocazione ( compilatore, assemblatore) o ogni coppia di file (archiviatore, linker e dlltool).

Vedi anche questa discussione su comp.compilers: http://compilers.iecc.com/comparch/article/03-11-078 specialmente questo:

http://compilers.iecc.com/comparch/article/02-07-128

Nota che John, il moderatore di comp.compilers sembra essere d'accordo, e ciò significa che dovrebbe essere possibile raggiungere velocità simili anche per C, se uno integra completamente la toolchain e implementa le intestazioni precompilate. Molti compilatori C commerciali lo fanno in una certa misura.

Si noti che il modello Unix di fattorizzare tutto su un binario separato è una specie del modello peggiore per Windows (con la sua lenta creazione di processi). È molto evidente quando si confrontano i tempi di compilazione di GCC tra Windows e * nix, specialmente se il sistema make / configure chiama anche alcuni programmi solo per ottenere informazioni.


13

Costruire C / C ++: cosa succede veramente e perché ci vuole così tanto tempo

Una parte relativamente grande del tempo di sviluppo del software non è spesa per scrivere, eseguire, eseguire il debug o persino progettare il codice, ma aspettare che finisca la compilazione. Per rendere le cose veloci, dobbiamo prima capire cosa sta succedendo quando viene compilato il software C / C ++. I passaggi sono approssimativamente i seguenti:

  • Configurazione
  • Avvio dello strumento di creazione
  • Controllo delle dipendenze
  • Compilazione
  • Collegamento

Vedremo ora ogni passaggio in modo più dettagliato, concentrandoci su come possono essere fatti più velocemente.

Configurazione

Questo è il primo passo quando si inizia a costruire. Di solito significa eseguire uno script di configurazione o CMake, Gyp, SCons o qualche altro strumento. Questo può richiedere da un secondo a diversi minuti per script di configurazione basati su Autotools di grandi dimensioni.

Questo passaggio avviene relativamente raramente. Deve essere eseguito solo quando si cambiano le configurazioni o si modifica la configurazione della build. A parte cambiare i sistemi di compilazione, non c'è molto da fare per rendere questo passo più veloce.

Avvio dello strumento di creazione

Questo è ciò che accade quando esegui make o fai clic sull'icona di build su un IDE (che di solito è un alias per make). Il file binario dello strumento di compilazione si avvia e legge i suoi file di configurazione e la configurazione della build, che di solito sono la stessa cosa.

A seconda della complessità e delle dimensioni della build, ciò può richiedere da una frazione di secondo a diversi secondi. Di per sé questo non sarebbe così male. Sfortunatamente la maggior parte dei sistemi di build basati su make fa sì che make sia invocato da decine a centinaia di volte per ogni singolo build. Di solito questo è causato dall'uso ricorsivo di make (che è male).

Va notato che il motivo per cui Make è così lento non è un bug di implementazione. La sintassi di Makefiles ha alcune stranezze che rendono un'implementazione davvero veloce quasi impossibile. Questo problema è ancora più evidente se combinato con il passaggio successivo.

Controllo delle dipendenze

Una volta che lo strumento di compilazione ha letto la sua configurazione, deve determinare quali file sono stati modificati e quali devono essere ricompilati. I file di configurazione contengono un grafico aciclico diretto che descrive le dipendenze di compilazione. Questo grafico viene in genere creato durante la fase di configurazione. Il tempo di avvio dello strumento di compilazione e lo scanner delle dipendenze vengono eseguiti su ogni singola build. Il loro runtime combinato determina il limite inferiore nel ciclo di modifica-compilazione-debug. Per i piccoli progetti questa volta è di solito qualche secondo circa. Questo è tollerabile. Ci sono alternative a Make. Il più veloce di questi è Ninja, costruito dagli ingegneri di Google per Chromium. Se stai usando CMake o Gyp per costruire, passa ai loro backend Ninja. Non devi modificare nulla nei file di compilazione stessi, goditi semplicemente l'aumento di velocità. Ninja non è impacchettato nella maggior parte delle distribuzioni, tuttavia,

Compilazione

A questo punto finalmente invochiamo il compilatore. Tagliare alcuni angoli, ecco i passaggi approssimativi presi.

  • La fusione include
  • Analisi del codice
  • Generazione / ottimizzazione del codice

Contrariamente alla credenza popolare, la compilazione del C ++ non è poi così lenta. STL è lento e la maggior parte degli strumenti di compilazione utilizzati per compilare C ++ sono lenti. Tuttavia, esistono strumenti e modi più rapidi per mitigare le parti lente della lingua.

Usarli richiede un po 'di grasso al gomito, ma i vantaggi sono innegabili. Tempi di costruzione più rapidi portano a sviluppatori più felici, più agilità e, eventualmente, codice migliore.


9

Un linguaggio compilato richiederà sempre un sovraccarico iniziale maggiore di un linguaggio interpretato. Inoltre, forse non hai strutturato molto bene il tuo codice C ++. Per esempio:

#include "BigClass.h"

class SmallClass
{
   BigClass m_bigClass;
}

Compila molto più lentamente di:

class BigClass;

class SmallClass
{
   BigClass* m_bigClass;
}

3
Soprattutto se BigClass include altri 5 file che utilizza, eventualmente includendo tutto il codice nel programma.
Tom Leys,

7
Questo è forse un motivo. Ma Pascal, ad esempio, impiega solo un decimo del tempo di compilazione impiegato da un programma C ++ equivalente. Questo non perché l'ottimizzazione di gcc: s impiega più tempo, ma piuttosto che Pascal è più facile da analizzare e non ha a che fare con un preprocessore. Vedi anche il compilatore Digital Mars D.
Daniel O

2
Non è l'analisi più semplice, è la modularità che evita la reinterpretazione di windows.h e di altre mille intestazioni per ogni unità di compilazione. Sì, Pascal analizza più facilmente (anche se quelli maturi, come Delphi sono di nuovo più complicati), ma non è questo che fa la differenza.
Marco van de Voort,

1
La tecnica mostrata qui che offre un miglioramento della velocità di compilazione è nota come dichiarazione a termine .
DavidRR

scrivere classi in un solo file. non sarebbe un codice disordinato?
Fennekin,

8

Un modo semplice per ridurre i tempi di compilazione in progetti C ++ più grandi è quello di creare un file * .cpp che includa tutti i file cpp nel progetto e compilarlo. Ciò riduce il problema dell'esplosione dell'intestazione a una volta. Il vantaggio è che gli errori di compilazione faranno comunque riferimento al file corretto.

Ad esempio, supponiamo di avere a.cpp, b.cpp e c.cpp .. creare un file: everything.cpp:

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

Quindi compilare il progetto semplicemente facendo tutto.cpp


3
Non riesco a vedere l'obiezione a questo metodo. Supponendo di generare le inclusioni da uno script o Makefile, non si tratta di un problema di manutenzione. Accelera infatti la compilazione senza offuscare i problemi di compilazione. Si potrebbe sostenere il consumo di memoria durante la compilazione, ma questo è raramente un problema per le macchine moderne. Quindi qual è l'oggetto di questo approccio (a parte l'affermazione che è sbagliato)?
rileyberton

9
@rileyberton (dal momento che qualcuno ha votato il tuo commento) fammi precisare: no, non accelera la compilazione. Infatti, si assicura che qualsiasi compilazione prende il tempo massimo da non isolando unità di traduzione. La cosa grandiosa di loro è che non è necessario ricompilare tutti .cpp-s se non sono cambiati. (Questo trascurando gli argomenti stilistici). La corretta gestione delle dipendenze e forse le intestazioni precompilate sono molto meglio.
Vede il

7
Siamo spiacenti, ma questo può essere un metodo molto efficiente per accelerare la compilazione, perché (1) praticamente elimini i collegamenti e (2) devi elaborare le intestazioni di uso comune solo una volta. Inoltre, funziona in pratica , se ti preoccupi di provarlo. Sfortunatamente, rende impossibili le ricostruzioni incrementali, quindi ogni build è completamente da zero. Ma una ricostruzione completa con questo metodo è molto più veloce di quella che otterresti altrimenti
jalf

4
@BartekBanachewicz certo, ma quello che hai detto è che "non accelera la compilazione", senza qualificazioni. Come hai detto, ogni compilazione richiede il massimo tempo (nessuna ricostruzione parziale), ma allo stesso tempo riduce drasticamente il massimo rispetto a quello che altrimenti sarebbe. Sto solo dicendo che è un po 'più sfumato di "non farlo"
jalf

2
Divertiti con variabili e funzioni statiche. Se voglio una grande unità di compilazione, creerò un grande file .cpp.
gnasher729,

6

Alcuni motivi sono:

1) La grammatica C ++ è più complessa di C # o Java e richiede più tempo per l'analisi.

2) (Più importante) Il compilatore C ++ produce codice macchina ed esegue tutte le ottimizzazioni durante la compilazione. C # e Java vanno a metà strada e lasciano questi passaggi a JIT.


5

Il compromesso che stai ottenendo è che il programma funziona un po 'più veloce. Potrebbe esserti di grande conforto durante lo sviluppo, ma potrebbe importare molto una volta che lo sviluppo è completo e il programma è gestito dagli utenti.


4

La maggior parte delle risposte non è chiara nel menzionare che C # funzionerà sempre più lentamente a causa del costo di eseguire azioni che in C ++ vengono eseguite una sola volta al momento della compilazione, questo costo delle prestazioni è influenzato anche dalle dipendenze di runtime (più cose da caricare per essere in grado per l'esecuzione), per non parlare del fatto che i programmi C # avranno sempre un ingombro di memoria maggiore, il che si tradurrà in prestazioni più strettamente correlate alla capacità dell'hardware disponibile. Lo stesso vale per altre lingue interpretate o che dipendono da una macchina virtuale.


4

Ci sono due problemi che posso pensare che potrebbero influenzare la velocità con cui i tuoi programmi in C ++ stanno compilando.

POSSIBILE PROBLEMA N. 1 - COMPILARE IL TITOLO: (Questo potrebbe non essere già stato affrontato da un'altra risposta o commento.) Microsoft Visual C ++ (AKA VC ++) supporta intestazioni precompilate, che consiglio vivamente. Quando crei un nuovo progetto e selezioni il tipo di programma che stai realizzando, sullo schermo dovrebbe apparire una finestra di installazione guidata. Se premi il pulsante "Avanti>" nella parte inferiore, la finestra ti porterà a una pagina che ha diversi elenchi di funzioni; assicurarsi che la casella accanto all'opzione "Intestazione precompilata" sia selezionata. (NOTA: questa è stata la mia esperienza con le applicazioni console Win32 in C ++, ma potrebbe non essere il caso di tutti i tipi di programmi in C ++.)

POSSIBILE NUMERO 2 - LA POSIZIONE È STATA COMPILATA IN: Quest'estate, ho seguito un corso di programmazione e abbiamo dovuto archiviare tutti i nostri progetti su unità flash da 8 GB, poiché i computer nel laboratorio che stavamo usando venivano cancellati ogni notte a mezzanotte, che avrebbe cancellato tutto il nostro lavoro. Se si esegue la compilazione su un dispositivo di archiviazione esterno per motivi di portabilità / sicurezza / ecc., Può richiedere molto tempotempo (anche con le intestazioni precompilate che ho menzionato sopra) per la compilazione del programma, soprattutto se si tratta di un programma abbastanza grande. Il mio consiglio per te in questo caso sarebbe quello di creare e compilare programmi sul disco rigido del computer che stai utilizzando e ogni volta che vuoi / devi smettere di lavorare sui tuoi progetti per qualunque motivo, trasferiscili sul tuo dispositivo di archiviazione, quindi fare clic sull'icona “Rimozione sicura dell'hardware ed espulsione supporti”, che dovrebbe apparire come una piccola unità flash dietro un piccolo cerchio verde con un segno di spunta bianco su di esso, per disconnetterlo.

Spero che questo ti aiuta; fammi sapere se lo fa! :)

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.