Est-ce une bonne idée de comstackr 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 du GHC n’est plus développé activement (c’est mon sentiment non supporté). Les compilateurs ciblent C– ou LLVM.

Normalement, je pense que GCC est un bon vieux compilateur qui fonctionne bien lors de l’optimisation du code, par conséquent la compilation en C utilisera la maturité de GCC pour produire du code meilleur et plus rapide. N’est-ce pas vrai?

Je me rends compte que la question dépend beaucoup 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 un peu 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 comprenait une explication de la raison pour laquelle GHC s’éloigne de C et pourquoi LLVM fonctionne mieux en tant que backend ( voir ceci ) ou tout autre exemple de compilateur faisant la même chose que je ne suis pas au courant.

Bien que je ne sois pas un expert du compilateur, je crois que cela se résume au fait que vous perdez quelque chose dans la traduction vers C plutôt que vers le langage intermédiaire de LLVM.

Si vous pensez au processus de compilation en C, vous créez un compilateur qui se traduit en code C, puis le compilateur C se traduit par une représentation intermédiaire (l’AST en mémoire), puis le 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 vous ne pourrez probablement pas créer un compilateur suffisamment sophistiqué à partir d’un langage source pour émuler la manière dont les humains écrivent. code. Il y a une perte de fidélité à C – 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 qu’il génère un code C que le compilateur C sait optimiser lors de la construction de son AST. Désordonné.

Cependant, si vous traduisez directement dans le langage intermédiaire de LLVM, c’est comme si vous compiliez votre code dans un bytecode de haut niveau indépendant de la machine, semblable au compilateur C vous donnant access à spécifier exactement ce que devrait contenir son AST. Essentiellement, vous supprimez l’intermédiaire qui parsing le code C et accède directement à la représentation de haut niveau, ce qui préserve davantage les caractéristiques de votre code en nécessitant moins de traduction.

Aussi lié à la performance, LLVM peut faire des choses vraiment délicates pour les langages dynamics comme la génération de code binary à l’exécution. C’est la partie “cool” de la compilation juste-à-temps: il s’agit d’écrire du code binary à exécuter à l’exécution, au lieu d’être bloqué avec ce qui a été créé au moment de la compilation.

Permettez-moi d’énumérer mes deux plus gros problèmes avec la compilation en C. Si cela pose un problème, votre langue dépend du type de fonctionnalités que vous avez.

  • Récupération des ordures Lorsque vous disposez d’une récupération de mémoire, vous devrez peut-être interrompre l’exécution régulière à peu près n’importe quel point du programme, et à ce stade, vous devez accéder à tous les pointeurs qui pointent vers le tas. Si vous comstackz en C, vous n’avez aucune idée de la position de ces indicateurs. C est responsable des variables locales, des arguments, etc. Les pointeurs sont probablement sur la stack (ou peut-être dans d’autres fenêtres de registre sur un SPARC), mais il n’y a pas de véritable access à la stack. Et même si vous parsingz la stack, quelles valeurs sont des pointeurs? LLVM répond en fait à ce problème (je ne sais pas si je n’ai jamais utilisé LLVM avec GC).

  • Appels de queue De nombreuses langues supposent que les appels de queue fonctionnent (c’est-à-dire qu’ils ne développent pas la stack); Scheme le commande, Haskell l’assume. Ce n’est pas le cas avec C. Dans certaines circonstances, vous pouvez convaincre certains compilateurs C de faire des appels de queue. Mais vous voulez que les appels de queue soient fiables, par exemple lorsque tail appelle une fonction inconnue. Il existe des solutions de rechange maladroites, comme le trampoline, mais rien de tout à fait satisfaisant.

Une partie de la raison pour laquelle GHC s’éloigne de l’ancien backend C est que le code produit par GHC n’est pas le code que gcc pourrait particulièrement optimiser. Avec l’amélioration du générateur de code natif de GHC, il y avait moins de retour pour beaucoup de travail. À partir de la version 6.12, le code du NCG n’était que plus lent que le code C compilé dans très peu de cas. LLVM est une meilleure cible car elle est plus modulaire et on peut faire 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 encore C et le binary final de celui-ci, généralement (exclusivement?) Par gcc. Et les binarys de JHC ont tendance à être assez rapides.

Donc, si vous pouvez produire du code, le compilateur C se comporte bien, cela rest une bonne option, mais il ne vaut probablement pas la peine de parcourir trop de cerceaux pour produire un bon C si vous pouvez produire plus facilement de bons exécutables via une autre route.

Comme vous l’avez mentionné, le fait que C soit une bonne langue cible dépend beaucoup de votre langue source. Voici donc quelques raisons pour lesquelles C présente des inconvénients par rapport à LLVM ou à une langue cible personnalisée:

  • Garbage Collection : un langage qui souhaite prendre en charge une récupération de place efficace doit connaître les informations supplémentaires qui interfèrent avec C. Si une allocation échoue, le GC doit trouver quelles valeurs sur la stack et dans les registres sont des pointeurs et celles qui ne le sont pas. Comme l’allocateur de registre 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 stack séparée. Ceci n’est que l’un des nombreux problèmes rencontrés lors de la tentative de prise en charge de la GC moderne par-dessus C. (Notez que LLVM a encore quelques problèmes dans ce domaine, mais j’entends dire que cela est en cours.)

  • Mappage de fonctions et optimisations spécifiques au langage: Certains langages s’appuient sur certaines optimisations, par exemple, Scheme s’appuie sur une optimisation des appels de queue. Les compilateurs C modernes peuvent le faire, mais ils ne sont pas certains de le faire, ce qui pourrait causer des problèmes si un programme en dépendait. Une autre caractéristique qui pourrait être difficile à prendre en charge en plus de C est la co-routine.

    La plupart des langages typés dynamicment ne peuvent pas être optimisés par les compilateurs C. Par exemple, Cython comstack Python en C, mais le C généré utilise des appels à de nombreuses fonctions génériques qui ne seront probablement pas optimisées même par les dernières versions de GCC. La compilation juste à temps, telle que PyPy / LuaJIT / TraceMonkey / V8, est bien plus adaptée aux performances des langages dynamics (au prix d’une implémentation beaucoup plus importante).

  • Expérience de développement : Avoir un interprète ou JIT peut également vous offrir une expérience beaucoup plus pratique pour les développeurs: générer du code C, puis le comstackr et le lier sera certainement plus lent et moins pratique.

Cela dit, je pense toujours que c’est un choix raisonnable d’utiliser C comme cible de compilation pour le prototypage de nouveaux langages. Étant donné que LLVM était explicitement conçu comme un backend de compilateur, je ne considérerais que C s’il y a de bonnes raisons de ne pas utiliser LLVM. Si le langage de niveau source est de très haut niveau, vous aurez probablement besoin d’un passage d’optimisation plus avancé, car LLVM est en effet de très bas niveau (par exemple, GHC réalise la plupart de ses optimisations intéressantes avant de générer des appels dans LLVM). Oh, et si vous faites du prototypage d’un langage, il est probablement plus facile d’utiliser un interprète – essayez simplement d’éviter les fonctionnalités trop implémentées par un interprète.

Mis à part toutes les raisons de qualité du générateur de code, il y a aussi d’autres problèmes:

  1. Les compilateurs C gratuits (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 glisser dans les bibliothèques d’exécution (ou même les émulations * nix) sous Windows qui sont douloureuses. Deux C runtimes différents (par exemple linux libc et msvcrt) sur lesquels se basent compliquent votre propre runtime et sa maintenance
  4. Vous obtenez un gros blob avec version externe dans votre projet, ce qui signifie qu’une transition de version majeure (par exemple, un changement de manœuvre peut nuire à votre runtime, les changements ABI comme le changement d’alignement) peuvent nécessiter un certain travail. Notez que cela vaut pour le compilateur ET la version externe (parties de la bibliothèque d’exécution). Et plusieurs compilateurs multiplient cela. Ce n’est pas si grave pour C que pour le backend, comme dans le cas où vous vous connectez directement à un serveur principal (read: bet on), comme si vous étiez un client gcc / llvm.
  5. Dans de nombreuses langues qui suivent ce chemin, vous pouvez voir Cisms se répandre dans la langue principale. Bien sûr, cela ne vous plaira pas, mais vous serez tenté 🙂
  6. Les fonctionnalités linguistiques qui ne correspondent pas directement à la norme C (comme les procédures nestedes et les autres tâches nécessitant un mélange de stacks) sont difficiles.

Notez que le point 4 signifie également que vous devrez investir du temps pour continuer à faire fonctionner les projets externes. C’est le moment qui n’entre généralement pas dans votre projet, et puisque le projet est plus dynamic, les versions multiplateformes nécessiteront beaucoup d’ingénierie de version supplémentaire pour répondre aux changements.

Donc, en bref, d’après ce que j’ai vu, alors qu’un tel mouvement permet 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 à la mauvaise prise en charge de Windows par des projets centrés sur nix tels que gcc. (LLVM est trop nouveau pour en dire long sur le long terme, mais leur rhétorique ressemble beaucoup à celle de gcc il ya dix ans). Si un projet dont vous dépendez énormément garde un certain cap (comme GCC passe à Win64 très lentement), alors vous êtes coincé avec.

Tout d’abord, décidez si vous voulez avoir un support sérieux non-nix (OS X étant plus unixy), ou seulement un compilateur Linux avec un mingw stopgap pour Windows? Beaucoup de compilateurs ont besoin d’un support Windows de premier ordre.

Deuxièmement, comment le produit doit-il être fini? Quel est le public principal? Est-ce un outil pour le développeur open source qui peut gérer une chaîne d’outils DIY ou souhaitez-vous cibler un marché débutant (comme de nombreux produits tiers, par exemple RealBasic)?

Ou souhaitez-vous vraiment fournir un produit complet aux professionnels ayant une intégration profonde et des chaînes d’outils complètes?

Les trois sont des instructions valides pour un projet de compilateur. Demandez-vous quelle est votre direction principale et ne présumez pas que plus d’options seront disponibles à temps. Par exemple, évaluer où sont les projets qui ont choisi d’être une interface GCC au début des années quatre-vingt-dix.

Essentiellement, la méthode unix consiste à aller plus loin (maximiser les plates-formes)

Les suites complètes (comme VS et Delphi, ce dernier qui a récemment commencé à prendre en charge OS X et a déjà pris en charge Linux), vont en profondeur et essaient de maximiser la productivité. (supporte spécialement la plate-forme Windows presque complètement avec des niveaux d’intégration profonds)

Les projets tiers sont moins clairs. Ils vont plus après les programmeurs indépendants et les magasins de niche. Ils ont moins de ressources de développement, mais les gèrent et les concentrent mieux.

Un point qui n’a pas encore été soulevé est la suivante: à quel point votre langage est-il proche de C? Si vous comstackz un langage impératif de bas niveau, la sémantique de C peut être très proche du langage que vous implémentez. Si c’est le cas, c’est probablement une victoire, car le code écrit dans votre langue est susceptible de ressembler au type de code que quelqu’un pourrait écrire en C à la main. Ce n’était certainement pas le cas avec le backend C de Haskell, ce qui est l’une des raisons pour lesquelles le backend C s’est si peu optimisé.

Un autre point contre l’utilisation d’un backend C est que la sémantique de C n’est en fait pas aussi simple qu’elle en a l’air . Si votre langage diffère significativement de C, l’utilisation d’un backend C signifie que vous devrez suivre toutes ces complexités exaspérantes, ainsi que les différences possibles entre les compilateurs C. Il peut être plus facile d’utiliser LLVM, avec sa sémantique plus simple, ou de concevoir votre propre backend, que de suivre tout cela.

Personnellement, je comstackrais en C. De cette façon, vous avez un langage d’intermédiaire universel et vous n’avez pas à vous soucier de savoir si votre compilateur prend en charge chaque plate-forme. L’utilisation de LLVM peut générer des gains de performances (même si je pense que la même chose pourrait probablement être obtenue en optimisant la génération de code C afin de la rendre plus conviviale), mais elle ne prend en charge que les objectives pris en charge par LLVM. LLVM pour append une cible lorsque vous souhaitez prendre en charge quelque chose de nouveau, d’ancien, de différent ou d’obscur.

Pour autant que je sache, C ne peut ni interroger ni manipuler les indicateurs de processeur.

Cette réponse est une réfutation de certains des points opposés à C en tant que langue cible.

  1. Optimisation des appels de queue

    Toute fonction qui peut être un appel de queue optimisé est en réalité équivalente à une itération (c’est un processus itératif, dans la terminologie SICP). De plus, de nombreuses fonctions récursives peuvent et doivent être récursives, pour des raisons de performances, en utilisant des accumulateurs, etc.

    Ainsi, pour que votre langage garantisse l’optimisation de l’appel de la queue, vous devrez le détecter et ne pas simplement mapper ces fonctions avec les fonctions C normales, mais créer des itérations à partir de celles-ci.

  2. Collecte des ordures

    Il peut être réellement implémenté en C. Vous pouvez créer un système d’exécution pour votre langue qui consiste en quelques abstractions de base sur le modèle de mémoire C – en utilisant par exemple vos propres allocateurs de mémoire, constructeurs, pointeurs spéciaux pour les objects dans le langage source. etc.

    Par exemple, au lieu d’utiliser des pointeurs C standard pour les objects dans le langage source , une structure spéciale pourrait être créée, sur laquelle un algorithme de récupération de place pourrait être implémenté. Les objects dans votre langue (plus précisément, les références) – pourraient se comporter comme en Java, mais en C, ils pourraient être représentés avec les méta-informations (que vous n’auriez pas au cas où vous travailliez uniquement avec des pointeurs).

    Bien entendu, un tel système pourrait avoir des problèmes d’intégration avec les outils C existants – cela dépend de votre implémentation et des compromis que vous êtes prêt à faire.

  3. Manque d’opérations

    hippietrail a noté que C manque d’opérateurs de rotation (par lesquels je suppose qu’il signifiait un décalage circulaire) pris en charge par les processeurs. Si de telles opérations sont disponibles dans le jeu d’instructions, elles peuvent être ajoutées à l’aide d’un assemblage en ligne .

    Dans ce cas, le frontend devrait détecter l’architecture pour laquelle il est exécuté et fournir les extraits appropriés. Une sorte de solution de rechange sous la forme d’une fonction régulière devrait également être fournie.

Cette réponse semble aborder sérieusement certaines questions fondamentales. J’aimerais voir plus de détails sur les problèmes qui sont causés par la sémantique de C.

Il y a un cas particulier où, si vous écrivez un langage de programmation avec des exigences de sécurité * ou de fiabilité ssortingctes.

D’une part, il vous faudrait des années pour connaître un sous-ensemble de C suffisamment important pour que vous sachiez que toutes les opérations C que vous choisirez d’utiliser dans votre compilation sont sûres et n’évoquent pas un comportement indéfini. Deuxièmement, vous devrez alors trouver une implémentation de C à laquelle vous pouvez faire confiance (ce qui signifierait une base de code de confiance minuscule et ne sera probablement pas très efficace). Sans oublier que vous devrez trouver un éditeur de liens de confiance, un système d’exploitation capable d’exécuter du code C compilé et certaines bibliothèques de base, qui devront toutes être bien définies et fiables.

Donc, dans ce cas, vous pourriez aussi bien utiliser le langage d’assemblage, si vous vous souciez de l’indépendance de la machine, d’une représentation intermédiaire.

* S’il vous plaît noter que “sécurité forte” ici n’est pas du tout liée à ce que les banques et les entresockets informatiques prétendent avoir