46 votes

Est-ce une bonne idée de compiler un langage en C ?

Partout sur le web, j'ai l'impression qu'écrire un backend C pour un compilateur n'est plus une si bonne idée. Le backend C de GHC n'est plus activement développé (c'est mon sentiment non étayé). Les compilateurs ciblent C-- ou LLVM.

Normalement, je penserais que GCC est un bon vieux compilateur mature qui optimise bien le code, donc la compilation en C utilisera la maturité de GCC pour produire un code meilleur et plus rapide. Est-ce que ce n'est pas vrai ?

Je suis conscient que la question dépend fortement de la nature du langage compilé et d'autres facteurs tels que l'obtention d'un code plus facile à maintenir. Je cherche une réponse plus générale (par rapport au langage compilé) qui se concentre uniquement sur la performance (sans tenir compte de la qualité du code, etc.). Je serais également très heureux si la réponse incluait une explication sur la raison pour laquelle GHC s'éloigne de C et pourquoi LLVM est plus performant en tant que backend ( voir ceci ) ou tout autre exemple de compilateur faisant la même chose dont je n'ai pas connaissance.

4 votes

L'une des réponses est que C ne peut pas vous donner un accès direct aux ressources clés de la machine. Essayez de manipuler le pointeur de pile en C vanille.

10 votes

Je ne suis pas d'accord pour clore cette question. Il y a des arguments clairs contre la compilation en C, mais je ne peux pas les donner maintenant.

0 votes

GCC est LE pire compilateur connu de l'humanité.

28voto

augustss Points 15750

Permettez-moi d'énumérer mes deux plus gros problèmes avec la compilation en C. Si c'est un problème pour votre langage, cela dépend du type de fonctionnalités dont vous disposez.

  • Collecte des ordures Lorsque vous disposez d'un ramasse-miettes, vous pouvez être amené à interrompre l'exécution normale à n'importe quel moment du programme et, à ce moment-là, vous devez accéder à tous les pointeurs qui pointent vers le tas. Si vous compilez en C, vous n'avez aucune idée de l'emplacement de ces pointeurs. Le langage C est responsable des variables locales, des arguments, etc. Les pointeurs sont probablement sur la pile (ou peut-être dans d'autres registres Windows sur un SPARC), mais il n'y a pas d'accès réel à la pile. Et même si vous parcourez la pile, quelles valeurs sont des pointeurs ? LLVM s'attaque en fait à ce problème (mais je ne sais pas dans quelle mesure, car je n'ai jamais utilisé LLVM avec GC).

  • Appels de queue De nombreux langages supposent que les appels de queue fonctionnent (c'est-à-dire qu'ils n'augmentent pas la pile) ; Scheme l'impose, Haskell le suppose. Ce n'est pas le cas en C. Dans certaines circonstances, il est possible de convaincre certains compilateurs C de faire des appels de queue. Mais vous voulez que les appels de queue soient fiables, par exemple lors de l'appel de queue d'une fonction inconnue. Il existe des solutions de contournement maladroites, comme le trampoline, mais rien de vraiment satisfaisant.

0 votes

Plusieurs langages = langages fonctionnels ? :-)

0 votes

Oui, en grande partie fonctionnelle, je crois.

9 votes

Lennart a raison ici - ce sont deux des principaux problèmes que nous avons rencontrés avec GHC lors de la compilation via C. Traiter correctement le garbage collection implique vraiment de gérer la pile soi-même - l'alternative est d'utiliser un GC conservateur, ce qui n'est pas vraiment viable dans un système de production. LLVM résout le problème du tail-call, mais sa solution au problème du GC n'est pas encore assez bonne pour GHC (et il n'est pas certain qu'elle le sera un jour - il y a eu une tentative sérieuse en C-- pour faire cela correctement, et même là, cela a impliqué quelques compromis).

27voto

Matt Points 6908

Bien que je ne sois pas un expert en compilateurs, je pense que cela se résume au fait que vous perdez quelque chose dans la traduction en C par rapport à la traduction dans le langage intermédiaire de LLVM par exemple.

Si vous pensez au processus de compilation en C, vous créez un compilateur qui traduit en code C, puis le compilateur C traduit en une représentation intermédiaire (l'AST en mémoire), puis traduit en code machine. Les créateurs du compilateur C ont probablement passé beaucoup de temps à optimiser certains modèles créés par l'homme dans le langage, mais il est peu probable que vous puissiez créer un compilateur suffisamment sophistiqué à partir d'un langage source vers le C pour émuler la manière dont les humains écrivent le code. Le passage au C entraîne une perte de fidélité : le compilateur C n'a aucune connaissance de la structure de votre code d'origine. Pour obtenir ces optimisations, vous devez essentiellement adapter votre compilateur pour essayer de générer du code C que le compilateur C sait comment optimiser lorsqu'il construit son AST. C'est compliqué.

Cependant, si vous traduisez directement vers le langage intermédiaire de LLVM, cela revient à compiler votre code vers un bytecode de haut niveau indépendant de la machine, ce qui revient à ce que le compilateur C vous donne accès à la spécification exacte de ce que son AST doit contenir. Essentiellement, vous supprimez l'intermédiaire qui analyse le code C et passez directement à la représentation de haut niveau, qui préserve davantage les caractéristiques de votre code en nécessitant moins de traduction.

Également lié à la performance, LLVM peut faire des choses vraiment délicates pour les langages dynamiques, comme générer du code binaire au moment de l'exécution. C'est la partie "cool" de la compilation juste à temps : il s'agit d'écrire du code binaire à exécuter au moment de l'exécution, au lieu d'être coincé avec ce qui a été créé au moment de la compilation.

0 votes

La compilation JIT est bien sûr un atout majeur. Je suis d'accord avec ce que vous dites dans une certaine mesure Je ne vois pas ce que l'on "perd" en compilant en C mais que l'on ne perd pas en compilant en LLVm. Je veux dire que je suis d'accord pour dire que l'on "perd" une partie de la structure avec la transformation du langage, mais n'en va-t-il pas de même avec LLVM ou tout autre langage de base ?

2 votes

OK, imaginez en C l'énoncé x++ - il pourrait être compilé pour copier x dans un autre registre, puis incrémenter la valeur de x, puis retourner la valeur copiée (précédente) de x. Une optimisation très évidente est de compiler ceci en utilisant une fonction test-et-set si le processeur le supporte, qui fait exactement cela, mais plus rapidement et de manière atomique. Si vous représentez la même instruction en C par x = x + 1 Il se peut que ce ne soit pas optimisé, parce que ce n'est pas exactement la même chose - vous n'avez jamais besoin de renvoyer la valeur précédente, n'est-ce pas ?

3 votes

Pour obtenir cette optimisation, il faut donc construire votre le compilateur - celui qui génère le c - pour faire la différence entre les deux et produire un code c différent en fonction de la situation. Si vous compilez en bytecode LLVM, LLVM peut déduire cela de votre bytecode généré en vérifiant par exemple si vous regardez la valeur de retour et en décidant de l'optimiser. GCC peut ne sont pas assez intelligents pour le faire, puisqu'il s'agit d'un exemple trivial, mais il est tout simplement plus facile pour un optimiseur de trouver ce genre de fruit à portée de main lorsqu'il s'agit d'un langage de niveau inférieur, comme LLVM par

8voto

Daniel Fischer Points 114146

Si GHC s'est éloigné de l'ancien backend C, c'est en partie parce que le code produit par GHC n'était pas le code que gcc pouvait particulièrement bien optimiser. Ainsi, le générateur de code natif de GHC s'améliorant, il y avait moins de retour pour beaucoup de travail. À partir de la version 6.12, le code du NCG n'était que très rarement plus lent que le code compilé en C. Le NCG devenant encore meilleur dans ghc-7, il n'y avait pas d'incitation suffisante pour maintenir le backend de gcc en vie. LLVM est une meilleure cible parce qu'il est plus modulaire et que l'on peut effectuer de nombreuses optimisations sur sa représentation intermédiaire avant de lui transmettre le résultat.

D'un autre côté, la dernière fois que j'ai regardé, JHC produisait toujours du C et le binaire final à partir de celui-ci, typiquement (exclusivement ?) par gcc. Et les binaires de JHC ont tendance à être assez rapides.

Ainsi, si vous pouvez produire du code que le compilateur C gère bien, c'est toujours une bonne option, mais il ne vaut probablement pas la peine de faire trop d'efforts pour produire un bon code C si vous pouvez plus facilement produire de bons exécutables par une autre voie.

0 votes

I croire le backend GCC est supposé rester pour le bootstrapping (c'est juste un vague souvenir). Pour LLVM, l'une des raisons principales est qu'il n'est pas modulaire et il existe déjà de nombreux modules de qualité . Plus précisément, l'utilisation de LLVM vous offre (en paraphrasant Don Steward) 25 ans d'optimisations de langages impératifs gratuitement, et plus d'architectures cibles que la plupart des équipes de compilateurs ne pourront jamais espérer mettre en œuvre (et encore moins maintenir).

0 votes

Oui, pour le bootstrapping, il est là pour rester. Il n'est pas aussi important de produire du code adapté à l'optimiseur de gcc, et il est possible de le maintenir avec peu de travail. Mais -fvia-C a déjà disparu dans la version 7.2. Deuxième point : Stewart, avec "t". Je ne sais pas s'il l'a dit, mais c'est très vrai.

0 votes

Source : donsbot.wordpress.com/2010/03/01/ . A propos du t : Oui, désolé - malheureusement, je ne peux plus éditer.

8voto

Marco van de Voort Points 15378

Outre toutes les raisons liées à la qualité du générateur de code, il existe d'autres problèmes :

  1. Les compilateurs C libres (gcc, clang) sont un peu centrés sur Unix.
  2. La prise en charge de plusieurs compilateurs (par exemple, gcc sous Unix et MSVC sous Windows) nécessite une duplication des efforts.
  3. Les compilateurs peuvent intégrer des bibliothèques d'exécution (ou même des émulations *nix) sous Windows qui sont douloureuses. Deux runtimes C différents (par exemple linux libc et msvcrt) sur lesquels se baser compliquent votre propre runtime et sa maintenance.
  4. Vous obtenez un gros blob de versions externes dans votre projet, ce qui signifie qu'une transition de version majeure (par exemple, un changement de mangulation pourrait nuire à votre bibliothèque d'exécution, des changements d'ABI comme un changement d'alignement) pourrait nécessiter un certain travail. Notez que cela s'applique au compilateur ET à la bibliothèque d'exécution versionnée en externe (parties de la bibliothèque d'exécution). Et les compilateurs multiples multiplient ce phénomène. Ce n'est pas si grave pour le C en tant que backend car dans le cas où vous vous connectez directement à (lire : pariez sur) un backend, comme être un frontend gcc/llvm.
  5. Dans de nombreuses langues qui suivent cette voie, on voit les cismes s'infiltrer dans la langue principale. Bien sûr, vous n'y trouverez pas votre compte, mais vous pouvez vous en passer. se se laisser tenter :-)
  6. Fonctionnalité du langage qui ne correspond pas directement au langage C standard (comme les procédures imbriquées), et d'autres choses qui nécessitent une manipulation de la pile) sont difficiles.
  7. En cas de problème, les utilisateurs seront confrontés à des erreurs de compilateur ou d'éditeur de liens au niveau C qui sortent de leur champ d'expérience. Les analyser et se les approprier est pénible, surtout avec de multiples compilateurs et versions.

Notez que le point 4 signifie également que vous devrez investir du temps pour que les choses continuent à fonctionner lorsque les projets externes évolueront. C'est du temps qui n'est généralement pas consacré à votre projet, et comme le projet est plus dynamique, les versions multiplateformes nécessiteront beaucoup d'ingénierie de version supplémentaire pour tenir compte des changements.

En bref, d'après ce que j'ai vu, bien qu'une telle démarche permette un démarrage rapide (obtenir un générateur de code raisonnable gratuitement pour de nombreuses architectures), il y a des inconvénients. La plupart d'entre eux sont liés à la perte de contrôle et au mauvais support Windows de projets centrés sur *nix comme gcc (LLVM est trop récent pour en dire plus sur le long terme, mais leur rhétorique ressemble beaucoup à celle de gcc il y a dix ans). Si un projet dont vous dépendez fortement garde une certaine orientation (comme GCC qui passe à win64 terriblement lentement), alors vous êtes coincé avec lui.

Tout d'abord, décidez si vous voulez un support sérieux pour les systèmes non *nix (OS X étant plus unixy), ou seulement un compilateur Linux avec un palliatif mingw pour Windows ? Beaucoup de compilateurs ont besoin d'un support Windows de premier ordre.

Deuxièmement, quel doit être le degré de finition du produit ? Quel est le public visé ? S'agit-il d'un outil pour le développeur open source qui peut gérer une chaîne d'outils de bricolage, ou voulez-vous cibler un marché de débutants (comme de nombreux produits tiers, par exemple RealBasic) ?

Ou voulez-vous vraiment fournir un produit complet pour les professionnels avec une intégration profonde et des chaînes d'outils complètes ?

Ces trois orientations sont valables pour un projet de compilateur. Demandez-vous quelle est votre orientation principale, et ne supposez pas que d'autres options seront disponibles avec le temps. Par exemple, évaluez où en sont les projets qui ont choisi d'être un frontend GCC au début des années quatre-vingt-dix.

Essentiellement, la méthode Unix consiste à élargir le champ d'application (maximiser les plates-formes).

Les suites complètes (comme VS et Delphi, ce dernier ayant récemment commencé à prendre en charge OS X et ayant pris en charge Linux dans le passé) vont en profondeur et tentent de maximiser la productivité. Les suites complètes (comme VS et Delphi, qui ont récemment commencé à supporter OS X et ont supporté linux dans le passé) vont en profondeur et tentent de maximiser la productivité.

Les projets de tiers sont moins clairs. Ils s'adressent davantage aux programmeurs indépendants et aux boutiques de niche. Ils disposent de moins de ressources en matière de développement, mais les gèrent et les concentrent mieux.

7voto

nominolo Points 3895

Comme vous l'avez mentionné, la question de savoir si le C est une bonne langue cible dépend beaucoup de votre langue source. Voici donc quelques raisons pour lesquelles le C présente des inconvénients par rapport à LLVM ou à un langage cible personnalisé :

  • Collecte des ordures : Un langage qui veut supporter un garbage collection efficace doit connaître des informations supplémentaires qui interfèrent avec C. Si une allocation échoue, le GC doit trouver quelles valeurs sur la pile et dans les registres sont des pointeurs et lesquelles ne le sont pas. Comme l'allocateur de registres n'est pas sous notre contrôle, nous devons utiliser des techniques plus coûteuses telles que l'écriture de tous les pointeurs sur une pile séparée. Ce n'est qu'un des nombreux problèmes rencontrés lorsqu'on essaie de supporter un GC moderne au-dessus du C. (Notez que LLVM a également quelques problèmes dans ce domaine, mais j'ai entendu dire que l'on y travaillait).

  • Cartographie des caractéristiques et optimisations spécifiques à la langue : Certains langages s'appuient sur certaines optimisations, par exemple Scheme qui s'appuie sur l'optimisation tail-call. Les compilateurs C modernes peuvent le faire mais ne sont pas garantis de le faire, ce qui pourrait causer des problèmes si un programme s'appuie sur cette optimisation pour être correct. Les co-routines sont une autre caractéristique qu'il pourrait être difficile de prendre en charge en plus du langage C.

    La plupart des langages à typage dynamique ne peuvent pas non plus être optimisés correctement par les compilateurs C. Par exemple, Cython compile Python en C, mais le C généré utilise des appels à de nombreuses fonctions génériques qui ne seront probablement pas bien optimisées, même par les dernières versions de GCC. La compilation juste à temps telle que PyPy/LuaJIT/TraceMonkey/V8 est beaucoup plus apte à fournir de bonnes performances pour les langages dynamiques (au prix d'un effort d'implémentation beaucoup plus important).

  • Expérience en matière de développement : Le fait de disposer d'un interpréteur ou d'une JIT peut également permettre aux développeurs de bénéficier d'une expérience beaucoup plus pratique - générer du code C, puis le compiler et le lier, sera certainement plus lent et moins pratique.

Cela dit, je continue de penser qu'il est raisonnable d'utiliser le C comme cible de compilation pour le prototypage de nouveaux langages. Étant donné que LLVM a été explicitement conçu comme un compilateur dorsal, je n'envisagerais le C que s'il existe de bonnes raisons de ne pas utiliser LLVM. Si le langage source est de très haut niveau, il est fort probable que vous ayez besoin d'une passe d'optimisation de plus haut niveau car LLVM est en effet de très bas niveau (par exemple, GHC effectue la plupart de ses optimisations intéressantes avant de générer l'appel à LLVM). Oh, et si vous êtes en train de prototyper un langage, l'utilisation d'un interpréteur est probablement plus facile -- essayez simplement d'éviter les fonctionnalités qui dépendent trop de l'implémentation par un interpréteur.

0 votes

Le dernier point est en fait de l'acharnement. Les mêmes arguments pourraient être utilisés contre l'utilisation de LLVM

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X