Pourquoi la compilation C ++ prend-elle autant de temps?


540

La compilation d'un fichier C ++ prend beaucoup de temps par rapport à C # et Java. La compilation d'un fichier C ++ prend beaucoup plus de temps que l'exécution d'un script Python de taille normale. J'utilise actuellement VC ++ mais c'est la même chose avec n'importe quel compilateur. Pourquoi est-ce?

Les deux raisons auxquelles je pouvais penser étaient le chargement des fichiers d'en-tête et l'exécution du préprocesseur, mais cela ne semble pas expliquer pourquoi cela prend autant de temps.


58
VC ++ prend en charge les en-têtes précompilés. Les utiliser vous aidera. Beaucoup.
Brian

1
Oui dans mon cas (surtout C avec quelques classes - pas de modèles) les en-têtes précompilés accélèrent environ 10x
Lothar

@Brian Je n'utiliserais jamais une tête pré-compilée dans une bibliothèque
Cole Johnson

13
It takes significantly longer to compile a C++ file- tu veux dire 2 secondes contre 1 seconde? C'est certainement deux fois plus long, mais peu significatif. Ou voulez-vous dire 10 minutes contre 5 secondes? Veuillez quantifier.
Nick Gammon

2
J'ai misé sur les modules; Je ne m'attends pas à ce que les projets C ++ deviennent plus rapides à construire que sur d'autres langages de programmation, juste avec des modules, mais cela peut être très proche pour la plupart des projets avec une certaine gestion. J'espère voir un bon gestionnaire de paquets avec une intégration artificielle après les modules
Abdurrahim

Réponses:


800

Plusieurs raisons

Fichiers d'en-tête

Chaque unité de compilation nécessite des centaines, voire des milliers d'en-têtes à (1) charger et (2) compiler. Chacun d'entre eux doit généralement être recompilé pour chaque unité de compilation, car le préprocesseur garantit que le résultat de la compilation d'un en-tête peut varier entre chaque unité de compilation. (Une macro peut être définie dans une unité de compilation qui modifie le contenu de l'en-tête).

C'est probablement la raison principale, car cela nécessite de grandes quantités de code à compiler pour chaque unité de compilation, et en outre, chaque en-tête doit être compilé plusieurs fois (une fois pour chaque unité de compilation qui l'inclut).

Mise en relation

Une fois compilés, tous les fichiers objets doivent être liés ensemble. Il s'agit essentiellement d'un processus monolithique qui ne peut pas très bien être parallélisé et doit traiter l'ensemble de votre projet.

Analyse

La syntaxe est extrêmement compliquée à analyser, dépend fortement du contexte et est très difficile à lever. Cela prend beaucoup de temps.

Modèles

En C #, List<T>est le seul type compilé, quel que soit le nombre d'instanciations de List que vous avez dans votre programme. En C ++, vector<int>est un type complètement distinct de vector<float>, et chacun devra être compilé séparément.

Ajoutez à cela que les modèles constituent un «sous-langage» complet de Turing complet que le compilateur doit interpréter, et cela peut devenir ridiculement compliqué. Même un code de métaprogrammation de modèle relativement simple peut définir des modèles récursifs qui créent des dizaines et des dizaines d'instanciations de modèle. Les modèles peuvent également entraîner des types extrêmement complexes, avec des noms ridiculement longs, ce qui ajoute beaucoup de travail supplémentaire à l'éditeur de liens. (Il faut comparer beaucoup de noms de symboles, et si ces noms peuvent devenir des milliers de caractères, cela peut devenir assez cher).

Et bien sûr, ils aggravent les problèmes avec les fichiers d'en-tête, car les modèles doivent généralement être définis dans les en-têtes, ce qui signifie que beaucoup plus de code doit être analysé et compilé pour chaque unité de compilation. En code C ordinaire, un en-tête ne contient généralement que des déclarations avancées, mais très peu de code réel. En C ++, il n'est pas rare que presque tout le code réside dans des fichiers d'en-tête.

Optimisation

C ++ permet des optimisations très spectaculaires. C # ou Java ne permettent pas d'éliminer complètement les classes (elles doivent être là à des fins de réflexion), mais même un simple métaprogramme de modèle C ++ peut facilement générer des dizaines ou des centaines de classes, qui sont toutes intégrées et éliminées à nouveau dans l'optimisation phase.

De plus, un programme C ++ doit être entièrement optimisé par le compilateur. Le programme AC # peut s'appuyer sur le compilateur JIT pour effectuer des optimisations supplémentaires au moment du chargement, C ++ n'obtient pas de telles "secondes chances". Ce que le compilateur génère est aussi optimisé que possible.

Machine

C ++ est compilé en code machine qui peut être un peu plus compliqué que l'utilisation de Java ou .NET en bytecode (en particulier dans le cas de x86). (Ceci n'est mentionné par souci d'exhaustivité que parce qu'il a été mentionné dans les commentaires et autres. En pratique, cette étape ne prendra probablement pas plus qu'une infime fraction du temps de compilation total).

Conclusion

La plupart de ces facteurs sont partagés par le code C, qui se compile assez efficacement. L'étape d'analyse est beaucoup plus compliquée en C ++ et peut prendre beaucoup plus de temps, mais le principal délinquant est probablement les modèles. Ils sont utiles et font de C ++ un langage beaucoup plus puissant, mais ils pèsent également sur la vitesse de compilation.


38
Concernant le point 3: la compilation C est sensiblement plus rapide que C ++. C'est définitivement l'interface qui cause le ralentissement, et non la génération de code.
Tom

72
Concernant les modèles: non seulement le vecteur <int> doit être compilé séparément du vecteur <double>, mais le vecteur <int> est recompilé dans chaque unité de compilation qui l'utilise. Les définitions redondantes sont éliminées par l'éditeur de liens.
David Rodríguez - dribeas

15
dribeas: Vrai, mais ce n'est pas spécifique aux modèles. Les fonctions en ligne ou tout autre élément défini dans les en-têtes seront recompilés partout où ils sont inclus. Mais oui, c'est particulièrement pénible avec les modèles. :)
jalf

15
@configurator: Visual Studio et gcc autorisent tous les deux des en-têtes précompilés, ce qui peut accélérer considérablement la compilation.
small_duck

5
Je ne sais pas si l'optimisation est le problème, car nos builds DEBUG sont en fait plus lents que les builds en mode release. La génération pdb est également un coupable.
gast128

40

Le ralentissement n'est pas nécessairement le même avec n'importe quel compilateur.

Je n'ai pas utilisé Delphi ou Kylix mais à l'époque MS-DOS, un programme Turbo Pascal se compilait presque instantanément, tandis que le programme Turbo C ++ équivalent ne faisait qu'explorer.

Les deux principales différences étaient un système de modules très solide et une syntaxe qui permettait la compilation en un seul passage.

Il est certainement possible que la vitesse de compilation n'ait tout simplement pas été une priorité pour les développeurs de compilateurs C ++, mais il existe également des complications inhérentes à la syntaxe C / C ++ qui la rendent plus difficile à traiter. (Je ne suis pas un expert en C, mais Walter Bright l'est, et après avoir construit divers compilateurs commerciaux C / C ++, il a créé le langage D. L'une de ses modifications a consisté à appliquer une grammaire sans contexte pour rendre le langage plus facile à analyser. .)

De plus, vous remarquerez que généralement les Makefiles sont configurés de sorte que chaque fichier soit compilé séparément en C, donc si 10 fichiers source utilisent tous le même fichier include, ce fichier inclus est traité 10 fois.


38
Il est intéressant de comparer Pascal, car Niklaus Wirth a utilisé le temps qu'il a fallu au compilateur pour se compiler comme référence lors de la conception de ses langages et compilateurs. Il y a une histoire qui après avoir soigneusement écrit un module pour la recherche rapide de symboles, il l'a remplacé par une simple recherche linéaire car la taille réduite du code a rendu le compilateur plus rapide à compiler.
Dietrich Epp

1
@DietrichEpp L'empirisme est payant.
Tomas Zubiri

40

L'analyse et la génération de code sont en fait assez rapides. Le vrai problème est l'ouverture et la fermeture de fichiers. N'oubliez pas que même avec les gardes d'inclusion, le compilateur a toujours ouvert le fichier .H et lu chaque ligne (puis l'ignore).

Un ami une fois (alors qu'il s'ennuyait au travail), a pris la demande de son entreprise et a mis tout - tous les fichiers source et en-tête - dans un gros fichier. Le temps de compilation est passé de 3 heures à 7 minutes.


14
Eh bien, l'accès aux fichiers a bien sûr un rôle à jouer, mais comme l'a dit Jalf, la raison principale en sera autre, à savoir l'analyse répétée de nombreux, nombreux, nombreux (imbriqués!) Fichiers d'en-tête qui disparaissent complètement dans votre cas.
Konrad Rudolph

9
C'est à ce stade que votre ami doit configurer des en-têtes précompilés, briser les dépendances entre les différents fichiers d'en-tête (essayez d'éviter un en-tête en incluant un autre, au lieu de le déclarer) et obtenez un disque dur plus rapide. Cela mis à part, une métrique assez incroyable.
Tom Leys

6
Si l'intégralité du fichier d'en-tête (à l'exception des commentaires éventuels et des lignes vides) se trouve dans les protections d'en-tête, gcc est capable de se souvenir du fichier et de l'ignorer si le symbole correct est défini.
CesarB

11
L'analyse est un gros problème. Pour N paires de fichiers source / en-tête de taille similaire avec des interdépendances, O (N ^ 2) passe par des fichiers d'en-tête. Mettre tout le texte dans un seul fichier réduit cette analyse en double.
Tom

9
Petite note latérale: les gardes d'inclusion protègent contre plusieurs analyses par unité de compilation. Pas contre plusieurs analyses globales.
Marco van de Voort

16

Une autre raison est l'utilisation du pré-processeur C pour localiser les déclarations. Même avec des protège-têtes, .h doit encore être analysé encore et encore, chaque fois qu'ils sont inclus. Certains compilateurs prennent en charge les en-têtes précompilés qui peuvent vous aider, mais ils ne sont pas toujours utilisés.

Voir aussi: Réponses C ++ fréquemment posées


Je pense que vous devriez mettre en gras le commentaire sur les en-têtes précompilés pour souligner cette partie IMPORTANTE de votre réponse.
Kevin

6
Si l'intégralité du fichier d'en-tête (à l'exception des commentaires éventuels et des lignes vides) se trouve dans les protections d'en-tête, gcc est capable de se souvenir du fichier et de l'ignorer si le symbole correct est défini.
CesarB

5
@CesarB: Il doit encore le traiter en entier une fois par unité de compilation (fichier .cpp).
Sam Harwell

16

C ++ est compilé en code machine. Vous avez donc le préprocesseur, le compilateur, l'optimiseur et enfin l'assembleur, qui doivent tous fonctionner.

Java et C # sont compilés en octet-code / IL, et la machine virtuelle Java / .NET Framework s'exécutent (ou compilent JIT en code machine) avant l'exécution.

Python est un langage interprété qui est également compilé en code octet.

Je suis sûr qu'il y a aussi d'autres raisons à cela, mais en général, ne pas avoir à compiler en langage machine natif fait gagner du temps.


15
Le coût ajouté par le prétraitement est insignifiant. La «principale autre raison» d'un ralentissement est que la compilation est divisée en tâches distinctes (une par fichier objet), de sorte que les en-têtes communs sont traités encore et encore. C'est O (N ^ 2) le pire des cas, par rapport à la plupart des autres langues O (N) temps d'analyse.
Tom

12
Vous pourriez dire à partir de la même argumentation que les compilateurs C, Pascal etc. sont lents, ce qui n'est pas vrai en moyenne. Cela a plus à voir avec la grammaire de C ++ et l'énorme état qu'un compilateur C ++ doit maintenir.
Sebastian Mach

2
C est lent. Il souffre du même problème d'analyse de l'en-tête que la solution acceptée. Par exemple, prenez un programme GUI Windows simple qui inclut windows.h dans quelques unités de compilation et mesurez les performances de compilation lorsque vous ajoutez des unités de compilation (courtes).
Marco van de Voort

14

Les principaux problèmes sont les suivants:

1) L'analyse infinie de l'en-tête. Déjà mentionné. Les atténuations (comme #pragma une fois) ne fonctionnent généralement que par unité de compilation, pas par build.

2) Le fait que la chaîne d'outils est souvent séparée en plusieurs fichiers binaires (make, préprocesseur, compilateur, assembleur, archiveur, impdef, linker et dlltool dans les cas extrêmes) qui doivent tous réinitialiser et recharger tous les états tout le temps pour chaque appel ( compilateur, assembleur) ou tous les deux fichiers (archiveur, éditeur de liens et dlltool).

Voir aussi cette discussion sur comp.compilers: http://compilers.iecc.com/comparch/article/03-11-078 spécialement celle-ci:

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

Notez que John, le modérateur de comp.compilers semble être d'accord, et que cela signifie qu'il devrait être possible d'atteindre des vitesses similaires pour C aussi, si l'on intègre pleinement la chaîne d'outils et implémente des en-têtes précompilés. De nombreux compilateurs C commerciaux le font dans une certaine mesure.

Notez que le modèle Unix de factorisation de tout dans un binaire séparé est une sorte de pire modèle pour Windows (avec sa création de processus lente). Il est très visible lorsque l'on compare les temps de construction de GCC entre Windows et * nix, surtout si le système make / configure appelle également certains programmes juste pour obtenir des informations.


13

Construire C / C ++: ce qui se passe vraiment et pourquoi cela prend-il autant de temps

Une partie relativement importante du temps de développement logiciel n'est pas consacrée à l'écriture, à l'exécution, au débogage ou même à la conception de code, mais à l'attente de la fin de la compilation. Pour accélérer les choses, nous devons d'abord comprendre ce qui se passe lorsque le logiciel C / C ++ est compilé. Les étapes sont à peu près les suivantes:

  • Configuration
  • Création d'un outil de démarrage
  • Vérification des dépendances
  • Compilation
  • Mise en relation

Nous allons maintenant examiner chaque étape plus en détail en nous concentrant sur la façon dont elles peuvent être accélérées.

Configuration

Il s'agit de la première étape lors du démarrage de la construction. Signifie généralement l'exécution d'un script de configuration ou CMake, Gyp, SCons ou un autre outil. Cela peut prendre entre une seconde et plusieurs minutes pour les très gros scripts de configuration basés sur Autotools.

Cette étape se produit relativement rarement. Il doit uniquement être exécuté lors de la modification des configurations ou de la modification de la configuration de build. À moins de changer les systèmes de construction, il n'y a pas grand-chose à faire pour accélérer cette étape.

Création d'un outil de démarrage

C'est ce qui se produit lorsque vous exécutez make ou cliquez sur l'icône de génération sur un IDE (qui est généralement un alias pour make). L'outil de construction binaire démarre et lit ses fichiers de configuration ainsi que la configuration de construction, qui sont généralement la même chose.

Selon la complexité et la taille de la construction, cela peut prendre de une fraction de seconde à plusieurs secondes. En soi, ce ne serait pas si mal. Malheureusement, la plupart des systèmes de build basés sur la marque font invoquer make à des dizaines à des centaines de fois pour chaque build. Habituellement, cela est dû à une utilisation récursive de make (ce qui est mauvais).

Il convient de noter que la raison pour laquelle Make est si lent n'est pas un bogue d'implémentation. La syntaxe de Makefiles a quelques bizarreries qui rendent une implémentation très rapide presque impossible. Ce problème est encore plus visible lorsqu'il est combiné avec l'étape suivante.

Vérification des dépendances

Une fois que l'outil de construction a lu sa configuration, il doit déterminer quels fichiers ont changé et lesquels doivent être recompilés. Les fichiers de configuration contiennent un graphique acyclique dirigé décrivant les dépendances de construction. Ce graphique est généralement créé lors de l'étape de configuration. Le temps de démarrage de l'outil de génération et le scanner de dépendances sont exécutés sur chaque génération. Leur runtime combiné détermine la borne inférieure du cycle d'édition-compilation-débogage. Pour les petits projets, cette durée est généralement de quelques secondes environ. C'est tolérable. Il existe des alternatives à Make. Le plus rapide d'entre eux est Ninja, qui a été construit par les ingénieurs de Google pour Chromium. Si vous utilisez CMake ou Gyp pour construire, passez simplement à leurs backends Ninja. Vous n'avez rien à changer dans les fichiers de construction eux-mêmes, profitez simplement de l'augmentation de vitesse. Ninja n'est pas inclus dans la plupart des distributions, cependant,

Compilation

À ce stade, nous invoquons enfin le compilateur. En coupant quelques coins, voici les étapes approximatives prises.

  • La fusion comprend
  • Analyser le code
  • Génération / optimisation de code

Contrairement à la croyance populaire, la compilation de C ++ n'est pas si lente. La STL est lente et la plupart des outils de compilation utilisés pour compiler C ++ sont lents. Cependant, il existe des outils et des moyens plus rapides pour atténuer les parties lentes du langage.

Leur utilisation prend un peu d'huile de coude, mais les avantages sont indéniables. Des temps de construction plus rapides conduisent à des développeurs plus heureux, plus d'agilité et, finalement, un meilleur code.


9

Un langage compilé va toujours nécessiter une surcharge initiale plus importante qu'un langage interprété. De plus, vous n'avez peut-être pas très bien structuré votre code C ++. Par exemple:

#include "BigClass.h"

class SmallClass
{
   BigClass m_bigClass;
}

Compile beaucoup plus lentement que:

class BigClass;

class SmallClass
{
   BigClass* m_bigClass;
}

3
Particulièrement vrai si BigClass arrive à inclure 5 autres fichiers qu'il utilise, incluant éventuellement tout le code dans votre programme.
Tom Leys

7
C'est peut-être une des raisons. Mais Pascal, par exemple, ne prend qu'un dixième du temps de compilation d'un programme C ++ équivalent. Ce n'est pas parce que l'optimisation de gcc: s prend plus de temps mais plutôt que Pascal est plus facile à analyser et n'a pas à traiter avec un préprocesseur. Voir aussi le compilateur Digital Mars D.
Daniel O

2
Ce n'est pas l'analyse plus facile, c'est la modularité qui évite de réinterpréter windows.h et une multitude d'autres en-têtes pour chaque unité de compilation. Oui, Pascal analyse plus facilement (même si les versions matures, comme Delphi sont à nouveau plus compliquées), mais ce n'est pas ce qui fait la grande différence.
Marco van de Voort

1
La technique présentée ici qui offre une amélioration de la vitesse de compilation est connue sous le nom de déclaration directe .
DavidRR

écrire des classes dans un seul fichier. ne serait-ce pas un code désordonné?
Fennekin

8

Un moyen simple de réduire le temps de compilation dans les projets C ++ plus volumineux consiste à créer un fichier d'inclusion * .cpp qui inclut tous les fichiers cpp de votre projet et à le compiler. Cela réduit le problème d'explosion d'en-tête à une seule fois. L'avantage est que les erreurs de compilation feront toujours référence au fichier correct.

Par exemple, supposons que vous ayez a.cpp, b.cpp et c.cpp .. créez un fichier: everything.cpp:

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

Ensuite, compilez le projet en créant simplement everything.cpp


3
Je ne vois pas d'objection à cette méthode. En supposant que vous générez les inclusions à partir d'un script ou d'un Makefile, ce n'est pas un problème de maintenance. En fait, cela accélère la compilation sans obstruer les problèmes de compilation. Vous pourriez discuter de la consommation de mémoire lors de la compilation, mais c'est rarement un problème sur les machines modernes. Alors, quel est l'objet de cette approche (à part l'affirmation qu'elle est fausse)?
rileyberton

9
@rileyberton (puisque quelqu'un a voté pour votre commentaire) permettez-moi de le préciser: non, cela n'accélère pas la compilation. En fait, il s'assure que toute compilation prend le maximum de temps en n'isolant pas les unités de traduction. La grande chose à leur sujet est que vous n'avez pas besoin de recompiler tous les fichiers .cpp s'ils ne changent pas. (Cela ne tient pas compte des arguments stylistiques). Une bonne gestion des dépendances et peut-être des en- têtes précompilés sont bien meilleurs.
sehe

7
Désolé, mais cela peut être une méthode très efficace pour accélérer la compilation, car vous (1) éliminez à peu près la liaison et (2) ne devez traiter les en-têtes couramment utilisés qu'une seule fois. En outre, cela fonctionne dans la pratique , si vous prenez la peine de l'essayer. Malheureusement, cela rend les reconstructions incrémentielles impossibles, donc chaque génération est complètement à partir de zéro. Mais une reconstruction complète avec cette méthode est beaucoup plus rapide que ce que vous obtiendriez autrement
jalf

4
@BartekBanachewicz bien sûr, mais ce que vous avez dit, c'est que "cela n'accélère pas la compilation", sans qualificatif. Comme vous l'avez dit, chaque compilation prend le maximum de temps (pas de reconstructions partielles), mais en même temps, elle réduit considérablement le maximum par rapport à ce qu'elle serait autrement. Je dis juste que c'est un peu plus nuancé que "ne fais pas ça"
jalf

2
Amusez-vous avec les variables et fonctions statiques. Si je veux une grosse unité de compilation, je vais créer un gros fichier .cpp.
gnasher729

6

Quelques raisons:

1) La grammaire C ++ est plus complexe que C # ou Java et prend plus de temps à analyser.

2) (Plus important) Le compilateur C ++ produit du code machine et fait toutes les optimisations pendant la compilation. C # et Java vont à mi-chemin et laissent ces étapes à JIT.


5

Le compromis que vous obtenez est que le programme s'exécute un peu plus vite. Cela peut être un réconfort pour vous pendant le développement, mais cela pourrait être très important une fois le développement terminé et le programme est simplement exécuté par les utilisateurs.


4

La plupart des réponses ne sont pas claires en mentionnant que C # s'exécutera toujours plus lentement en raison du coût des actions qui, en C ++, ne sont effectuées qu'une seule fois au moment de la compilation, ce coût de performance est également impacté en raison des dépendances d'exécution (plus de choses à charger pour pouvoir pour s'exécuter), sans oublier que les programmes C # auront toujours une empreinte mémoire plus élevée, ce qui se traduira par des performances plus étroitement liées à la capacité du matériel disponible. Il en va de même pour les autres langues interprétées ou dépendantes d'une machine virtuelle.


4

Il y a deux problèmes auxquels je peux penser qui pourraient affecter la vitesse de compilation de vos programmes en C ++.

PROBLÈME POSSIBLE # 1 - COMPILATION DE L'EN-TÊTE: (Cela peut ou peut ne pas avoir déjà été résolu par une autre réponse ou un autre commentaire.) Microsoft Visual C ++ (AKA VC ++) prend en charge les en-têtes précompilés, ce que je recommande fortement. Lorsque vous créez un nouveau projet et sélectionnez le type de programme que vous créez, une fenêtre d'assistant de configuration doit apparaître sur votre écran. Si vous appuyez sur le bouton "Suivant>" au bas de celui-ci, la fenêtre vous amènera à une page qui a plusieurs listes de fonctionnalités; assurez-vous que la case à côté de l'option «En-tête précompilé» est cochée. (REMARQUE: c'est mon expérience avec les applications de console Win32 en C ++, mais cela peut ne pas être le cas avec toutes sortes de programmes en C ++.)

PROBLÈME POSSIBLE # 2 - L'EMPLACEMENT ÉTÉ COMPILÉ POUR: Cet été, j'ai suivi un cours de programmation, et nous avons dû stocker tous nos projets sur des lecteurs flash de 8 Go, car les ordinateurs du laboratoire que nous utilisions étaient effacés tous les soirs à minuit, qui aurait effacé tout notre travail. Si vous compilez vers un périphérique de stockage externe pour des raisons de portabilité / sécurité / etc., cela peut prendre très longtempstemps (même avec les en-têtes précompilés que j'ai mentionnés ci-dessus) pour que votre programme compile, surtout s'il s'agit d'un programme assez volumineux. Mon conseil pour vous dans ce cas serait de créer et de compiler des programmes sur le disque dur de l'ordinateur que vous utilisez, et chaque fois que vous voulez / devez arrêter de travailler sur vos projets pour quelque raison que ce soit, transférez-les sur votre externe périphérique de stockage, puis cliquez sur l'icône «Retirer le matériel en toute sécurité et éjecter le support», qui devrait apparaître comme un petit lecteur flash derrière un petit cercle vert avec une coche blanche dessus, pour le déconnecter.

J'espère que ceci vous aide; faites-moi savoir si c'est le cas! :)

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.