Blog 6 min de lecture

Les bénéfices des projets bien conçus : GCC contre Clang

Share this article
Les bénéfices des projets bien conçus : GCC contre Clang

GCC (GNU Compiler Collection) et Clang comptent parmi les compilateurs C++ les plus importants du monde du développement logiciel. Chacun repose sur une philosophie de conception et une architecture qui lui sont propres, répondant à des besoins et des préférences différents. Cet article explore les différences fondamentales de conception entre GCC et Clang, en montrant comment elles influent sur leurs fonctionnalités, leurs performances et leur facilité d'utilisation.

Différences d'architecture

  • Clang: la conception de Clang est hautement modulaire. Il se compose d'une série de bibliothèques bien définies (front end, middle end et back end) qui peuvent être utilisées indépendamment ou ensemble. Cette modularité rend Clang facilement extensible et maintenable. LibClang fournit en outre une interface C stable vers les bibliothèques Clang, ce qui facilite le développement d'outils et l'intégration dans les IDE.
  • GCC: historiquement, GCC a été conçu comme un compilateur monolithique aux composants fortement couplés. Bien qu'il soit devenu plus modulaire avec le temps, il le reste moins que Clang. GCC prend en charge les plugins, mais son architecture le rend plus difficile à étendre que la conception plus souple de Clang.

Examinons de plus près la conception de Clang pour comprendre pourquoi son architecture facilite l'ajout de nouvelles fonctionnalités et de plugins.

Conception de Clang

Comme beaucoup d’autres conceptions de compilateurs, le compilateur Clang comporte trois phases :

  • Le front end analyse le code source, vérifie qu'il ne contient pas d'erreurs et construit un arbre syntaxique abstrait (AST) propre au langage pour représenter le code d'entrée.
  • L'optimiseur effectue des optimisations sur l'AST généré par le front end.
  • Le back end génère le code machine final pour l'architecture cible.

Qu'est-ce qui distingue Clang des autres compilateurs ?

La différence la plus importante de sa conception tient au fait que Clang repose sur LLVM. L'idée derrière LLVM est d'utiliser la représentation intermédiaire LLVM (IR) ; c'est l'équivalent du bytecode pour Java.
LLVM IR est conçue pour accueillir les analyses et transformations de niveau intermédiaire que l'on trouve dans la partie optimiseur d'un compilateur. Elle a été pensée avec de nombreux objectifs précis en tête, notamment la prise en charge d'optimisations légères à l'exécution, d'optimisations inter-fonctions/interprocédurales, d'analyses de programme complet et de transformations de restructuration agressives, etc. Son aspect le plus important, cependant, est qu'elle est elle-même définie comme un langage de premier ordre doté d'une sémantique bien définie.

Grâce à cette conception, une grande partie du compilateur peut être réutilisée pour créer d'autres compilateurs. Par exemple, il suffit de remplacer le front end pour traiter d'autres langages.

I - Front end

Clang est conçu pour être modulaire, et chaque phase de compilation est assurée par un module spécifique. Voici quelques-uns des projets impliqués dans la phase front end :

Comme tout analyseur de front end, nous avons besoin d'un lexeur et d'une analyse sémantique. Le front end Clang peut être exécuté en passant l'argument -cc1. Il offre plusieurs fonctionnalités, comme la génération de l'AST :

clang -cc1 -ast-dump test.c

Cette commande est prise en charge par la fonction cc1_main. Voici la séquence de quelques-unes des méthodes intéressantes qui sont exécutées :

clang11

La méthode ExecuteAction possède un paramètre de type FrontEndAction; l'objectif est de spécifier quelle action de front end exécuter. FrontEndAction est une classe abstraite dont il faut hériter pour implémenter une action de front end concrète.

Découvrons toutes les actions de front end implémentées par Clang à l'aide de CQLinq; pour cela, nous pouvons rechercher toutes les classes qui en héritent directement ou indirectement.

from t in Types
let depth0 = t.DepthOfDeriveFrom(“clang.FrontendAction”)
where depth0  >= 0 orderby depth0
select new { t, depth0 }

De nombreuses actions de front end sont disponibles. Par exemple, ASTDumpAction génère l'AST sans créer l'exécutable final. Presque toutes les actions de front end héritent d'ASTFrontEndAction, ce qui signifie qu'elles travaillent sur l'AST généré.

Ce qui est intéressant dans cette conception, c'est qu'on peut facilement brancher une FrontEndAction personnalisée : il suffit d'en implémenter une nouvelle.

Comment effectuer un traitement sur l'AST ?

Chaque ASTFrontEndAction crée une ou plusieurs instances d'ASTConsumer. La classe ASTConsumer est une classe abstraite, et nous devons implémenter notre propre consommateur d'AST pour nos besoins spécifiques.

La FrontEndAction invoquera le consommateur d'AST comme le spécifie le graphe de dépendances suivant.

Recherchons toutes les classes ASTConsumer à l'aide de CQLinq:

from t in Types
let depth0 = t.DepthOfDeriveFrom(“clang.ASTConsumer”)
where depth0  == 1
select new { t, depth0 }

CodeGenerator est un exemple de consommateur d'AST. Comme mentionné précédemment, l'un des points forts de LLVM est son utilisation de l'IR, dont la génération requiert l'analyse de l'AST. CodeGenerator est la classe héritant d'ASTConsumer chargée de générer l'IR, et ce qui est intéressant, c'est que ce traitement est isolé dans un autre projet nommé ClangCodeGen.

Voici quelques-unes des classes impliquées dans la génération de l'IR LLVM :

II - Optimiseur

Pour expliquer cette phase, je ne saurais faire mieux que Chris Lattner, le père de LLVM, dans ce billet:

« Pour donner une intuition du fonctionnement des optimisations, il est utile de parcourir quelques exemples. Il existe de très nombreux types d'optimisations de compilation, si bien qu'il est difficile de fournir une recette pour résoudre un problème arbitraire. Cela dit, la plupart des optimisations suivent une structure simple en trois parties :

  • Rechercher un motif à transformer.
  • Vérifier que la transformation est sûre/correcte pour l’instance correspondante.
  • Effectuer la transformation, en mettant à jour le code.

L'optimiseur lit l'IR LLVM en entrée, la « mâche » un peu, puis émet de l'IR LLVM qui, on l'espère, s'exécutera plus rapidement. Dans LLVM (comme dans beaucoup d'autres compilateurs), l'optimiseur est organisé comme un pipeline de passes d'optimisation distinctes, chacune étant exécutée sur l'entrée avec la possibilité d'agir. Parmi les exemples courants de passes figurent l'inline (qui substitue le corps d'une fonction aux sites d'appel), la réassociation d'expressions, le déplacement de code invariant de boucle, etc. Selon le niveau d'optimisation, différentes passes sont exécutées : par exemple, à -O0 (aucune optimisation), le compilateur Clang n'exécute aucune passe ; à -O3, il en exécute une série de 67 dans son optimiseur (à compter de LLVM 2.8).

Explorons les passes de LLVMCore en recherchant les classes qui héritent de la classe « pass ».

from t in Types
let depth0 = t.DepthOfDeriveFrom(“llvm.Pass”)
where t.ParentProject.Name==”LLVMCore” && depth0  >= 0 orderby depth0
select new { t, depth0 }

Bien sûr, de nombreuses autres passes existent dans d'autres modules LLVM.

III - Back end

Comme les autres phases, le back end est chargé de générer la sortie pour une cible spécifique. Dans le cas de Clang, cette phase est hautement modulaire. Prenons par exemple LLVMX86Target, qui génère du code pour la cible x86.

Voici un graphe montrant tous les modules impliqués dans la génération des binaires pour la cible x86.

De nombreux modules interviennent dans cette phase, chacun avec une responsabilité précise. Cela favorise la cohésion, des API propres et la séparation des préoccupations, rendant le système plus facile à comprendre pour les développeurs, qui peuvent se concentrer sur de petites parties d'un ensemble plus vaste.

Conclusion

Le duo LLVM/Clang n'est pas qu'un compilateur C/C++ : c'est aussi une infrastructure pour construire des outils, et il est facile d'en étendre le comportement. De nombreux outils sont inclus d'origine dans le code source de LLVM/Clang, et bien d'autres sont disponibles sur le web.

Si vous avez besoin d'un analyseur C/C++ pour construire un outil, Clang est un très bon candidat.

Share this article