Le meilleur livre pour répondre à votre question serait probablement: Cooper and Torczon, «Engineering a Compiler», 2003. Si vous avez accès à une bibliothèque universitaire, vous devriez pouvoir en emprunter un exemplaire.
Dans un compilateur de production tel que llvm ou gcc, les concepteurs mettent tout en œuvre pour conserver tous les algorithmes au-dessous de où est la taille de l'entrée. Pour certaines des analyses des phases "d'optimisation", cela signifie que vous devez utiliser des méthodes heuristiques plutôt que de produire un code vraiment optimal.nO(n2)n
Le lexer est une machine à états finis, donc la taille de l'entrée (en caractères) et produit un flux de jetons transmis à l'analyseur.O ( n )O(n)O(n)
Pour de nombreux compilateurs et pour de nombreuses langues, l'analyseur est LALR (1) et traite donc le flux de jetons en temps en nombre de jetons d'entrée. Lors de l'analyse, vous devez généralement garder trace d'une table de symboles, mais, pour de nombreuses langues, elle peut être gérée avec une pile de tables de hachage ("dictionnaires"). Chaque accès au dictionnaire est , mais vous pouvez parfois avoir à marcher dans la pile pour rechercher un symbole. La profondeur de la pile est où est la profondeur d'imbrication des étendues. (Ainsi, dans les langages C-like, combien de couches d'accolades vous êtes à l'intérieur.)O ( 1 ) O ( s ) sO(n)O(1)O(s)s
Ensuite, l'arbre d'analyse est généralement "aplati" dans un graphe de flux de contrôle. Les nœuds du graphe de flux de contrôle peuvent être des instructions à 3 adresses (similaires à un langage d'assemblage RISC), et la taille du graphe de flux de contrôle sera généralement linéaire par rapport à la taille de l'arbre d'analyse.
Ensuite, une série d'étapes d'élimination de la redondance est généralement appliquée (élimination de sous-expression commune, mouvement de code invariant de boucle, propagation constante, ...). (Ceci est souvent appelé "optimisation" bien qu'il y ait rarement quelque chose d'optimal dans le résultat, le but réel est d'améliorer le code autant que possible dans les contraintes de temps et d'espace que nous avons imposées au compilateur.) Chaque étape d'élimination de la redondance exigent généralement des preuves de certains faits concernant le graphe de flux de contrôle. Ces épreuves sont généralement effectuées à l'aide d' une analyse de flux de données . La plupart des analyses de flux de données sont conçues de manière à converger en passes sur le graphe de flux où est (en gros) la profondeur d'imbrication de la boucle et un passage sur le graphe de flux prend du tempsd O ( n ) nO(d)dO(n)où est le nombre d'instructions à 3 adresses.n
Pour des optimisations plus sophistiquées, vous pouvez effectuer des analyses plus sophistiquées. À ce stade, vous commencez à faire des compromis. Vous voulez que vos algorithmes d'analyse prennent beaucoup moins queO(n2)temps dans la taille du graphe de flux du programme entier, mais cela signifie que vous devez vous passer des informations (et des transformations améliorant les programmes) qui pourraient être coûteuses à prouver. L'analyse d'alias est un exemple classique. Dans le cas d'une paire d'écritures en mémoire, vous souhaitez prouver que les deux écritures ne peuvent jamais cibler le même emplacement de mémoire. (Vous voudrez peut-être effectuer une analyse d'alias pour voir si vous pouvez déplacer une instruction au-dessus de l'autre.) Mais pour obtenir des informations précises sur les alias, vous devrez peut-être analyser tous les chemins de contrôle possibles dans le programme, ce qui est exponentiel en nombre de branches. dans le programme (et donc exponentielle dans le nombre de nœuds dans le graphe de flux de contrôle.)
Ensuite, vous entrez dans l'allocation de registre. L'attribution de registre peut être formulée comme un problème de coloration de graphique, et il est connu que la coloration d'un graphique avec un nombre minimal de couleurs est NP-Hard. La plupart des compilateurs utilisent donc une sorte d’heuristique gloutonne combinée à des renversements de registres dans le but de réduire au mieux le nombre de renversements de registres dans des délais raisonnables.
Enfin, vous entrez dans la génération de code. La génération de code consiste généralement en un bloc de base maximal à un moment où un bloc de base est un ensemble de noeuds de graphe de flux de commande connectés de manière linéaire avec une seule entrée et une seule sortie. Cela peut être reformulé en un problème de couverture de graphe où le graphe que vous essayez de couvrir est le graphe de dépendance du jeu d'instructions à 3 adresses du bloc de base et que vous essayez de couvrir avec un jeu de graphes représentant la machine disponible. instructions. Ce problème est exponentiel dans la taille du bloc de base le plus grand (qui peut, en principe, être du même ordre que la taille de l'ensemble du programme). Il s'agit donc encore d'une fois d'une heuristique où seulement un petit sous-ensemble des couvertures possibles est examiné.