Wikilivres frwikibooks https://fr.wikibooks.org/wiki/Accueil MediaWiki 1.46.0-wmf.24 first-letter Média Spécial Discussion Utilisateur Discussion utilisateur Wikilivres Discussion Wikilivres Fichier Discussion fichier MediaWiki Discussion MediaWiki Modèle Discussion modèle Aide Discussion aide Catégorie Discussion catégorie Transwiki Discussion Transwiki Wikijunior Discussion Wikijunior TimedText TimedText talk Module Discussion module Event Event talk Livre de cuisine/Recettes de la Bourgogne 0 23539 764076 760248 2026-04-20T08:01:21Z Xhungab 23827 764076 wikitext text/x-wiki {{livre de cuisine}} * [[Livre de cuisine/Escargot de Bourgogne|Escargot de Bourgogne]] * [[Livre de cuisine/Bœuf bourguignon|Bœuf bourguignon]] * [[Livre de cuisine/Fondue bourguignonne|Fondue bourguignonne]] * [[Livre de cuisine/Gaudes|Gaudes]] * [[Livre de cuisine/Œufs en meurette|Œufs en meurette]] * [[Livre de cuisine/Poulet à la Gaston Gérard|Poulet à la Gaston Gérard]] * [[Livre de cuisine/Flamusse bourguignonne|Flamusse bourguignonne]] {{Sur Wikipédia|Cuisine bourguignonne}} [[Catégorie:Cuisine bourguignonne|*]] 9se6gbomll69jmzvn0ho1suvncqeymy Pour lire Platon 0 29731 764011 763992 2026-04-19T15:55:51Z PandaMystique 119061 /* Table des matières */ 764011 wikitext text/x-wiki == Introduction == {{version imprimable}} Ce livre propose de fournir au lecteur les connaissances nécessaires à la compréhension des œuvres de Platon. Il s'adresse aux débutants (par exemple, des élèves de terminale, mais également tous ceux qui souhaitent acquérir une culture générale ou découvrir ce philosophe sans avoir de culture philosophique particulière) et aux lecteurs intermédiaires ou avancés, c'est-à-dire à des lecteurs qui ont lu quelques dialogues parmi les moins difficiles et qui sont éventuellement familiers de quelques-unes des notions principales de Platon. Ce livre ne s'adresse pas aux spécialistes de Platon qui ne trouveront ici rien qu'ils ne connaissent déjà par la fréquentation assidue des dialogues et des commentateurs. == Table des matières == === '''[[/Premiers pas/]]''' === * [[Pour lire Platon/Premiers pas#Qui est Platon ?|1 Qui est Platon ?]] * [[Pour lire Platon/Premiers pas#Pourquoi Platon a-t-il écrit ?|2 Pourquoi Platon a-t-il écrit ?]] * [[Pour lire Platon/Premiers pas#Qu'a-t-il écrit ?|3 Qu'a-t-il écrit ?]] * [[Pour lire Platon/Premiers pas#Dans quel ordre Platon a-t-il écrit ses dialogues ?|4 Dans quel ordre Platon a-t-il écrit ses dialogues ?]] * [[Pour lire Platon/Premiers pas#Pourquoi Platon a-t-il écrit des dialogues et pas des traités ?|5 Pourquoi Platon a-t-il écrit des dialogues et pas des traités ?]] * [[Pour lire Platon/Premiers pas#Qui sont les personnages des dialogues de Platon ?|6 Qui sont les personnages des dialogues de Platon ?]] * [[Pour lire Platon/Premiers pas#Pourquoi Socrate est-il le personnage principal des dialogues de Platon ?|7 Pourquoi Socrate est-il le personnage principal des dialogues de Platon ?]] * [[Pour lire Platon/Premiers pas#De quoi Platon parle-t-il dans ses dialogues ?|8 De quoi Platon parle-t-il dans ses dialogues ?]] * [[Pour lire Platon/Premiers pas#Les dialogues de Platon forment-ils un système philosophique ?|9 Les dialogues de Platon forment-ils un système philosophique ?]] * [[Pour lire Platon/Premiers pas#Pourquoi lire un auteur qui est mort il y a 24 siècles ?|10 Pourquoi lire un auteur qui est mort il y a 24 siècles ?]] === '''[[/Conseils pour la lecture|Conseils pour la lecture]]''' === *<small>Quelques conseils élémentaires avant de se lancer</small> * [[Pour lire Platon/Conseils pour la lecture#Quelles traductions choisir ?|1 Quelles traductions choisir ?]] * [[Pour lire Platon/Conseils pour la lecture#Faut-il apprendre le grec pour bien comprendre Platon ?|2 Faut-il apprendre le grec pour bien comprendre Platon ?]] * [[Pour lire Platon/Conseils pour la lecture#Par quels dialogues commencer ?|3 Par quels dialogues commencer ?]] * [[Pour lire Platon/Conseils pour la lecture#Comment tirer profit de la lecture des dialogues ?|4 Comment tirer profit de la lecture des dialogues ?]] * [[Pour lire Platon/Conseils pour la lecture#Quels outils utiliser au cours de la lecture ?|5 Quels outils utiliser au cours de la lecture ?]] * [[Pour lire Platon/Conseils pour la lecture#Quels livres lire sur Platon ?|6 Quels livres lire sur Platon ?]] === [[Pour lire Platon/Introduction par les dialogues|Introduction par les dialogues]] === === [[Pour lire Platon/Introduction par les mythes|Introduction par les mythes]] === === Lecture des dialogues === * [[Pour lire Platon/Guide des dialogues/Introduction|Introduction]] * [[Pour lire Platon/Guide des dialogues|Guide des dialogues]] === [[Pour lire Platon/Vocabulaire|Vocabulaire]] === [[Catégorie:Histoire de la philosophie]][[Catégorie:Philosophe]] {{AutoCat}} __NOTOC__ rlcl7rptllofzz0ob3mk9dcstdnuy2b Pour lire Platon/Introduction par les dialogues 0 29778 764002 763975 2026-04-19T14:08:13Z PandaMystique 119061 764002 wikitext text/x-wiki {{Sous-pages}} Le premier chapitre nous a permis de faire connaissance avec Platon. Nous allons à présent faire connaissance avec sa pensée. Voici comment nous organiserons ce chapitre. Chaque section abordera un domaine à travers une série de questions. Pour chacune de ces questions, nous donnerons un exposé succinct du contexte, une réponse détaillée et parfois une discussion argumentée des thèses de Platon et de leur actualité. Enfin, chaque section proposera la lecture de quelques extraits des dialogues. Nous prenons le parti de faire une première présentation de la pensée de Platon d'une manière qui pourra paraître trop systématique et artificielle. Cette manière n'a cependant pour objectif que d'éviter au débutant d'être noyé sous la multiplicité des sujets traités dans les dialogues. Selon les cas, les réponses pourront être très détaillées, mais nous privilégierons toujours une rédaction utilisant des mots familiers, plutôt qu'un vocabulaire philosophique spécifique. En ce sens, nous ne rechercherons pas l'exactitude absolue, mais la compréhension des problèmes soulevés. Les discussions des thèses de Platon auront pour but d'élargir la réflexion. Comme pour le premier chapitre, la liste des questions proposées n'est pas close. == Le philosophe == === Qu'est-ce que l'amour de la sagesse ? === '''Lecture conseillée''' : ''Phèdre'', ''Le Banquet'' '''Contexte'''<br/> L'étymologie du mot ''philosophie'' est sans doute connue par un très grande nombre de personnes : la philosophie est l'amour de la sagesse ou du savoir. Mais, quoique connue, cette signification n'étonne peut-être pas autant qu'elle le devrait. La lecture de Platon va nous montrer de quelle manière cet amour peut nous paraître déroutant, voir nous choquer. Tout d'abord, être sage, ou connaître, semble se rapporter à un état intellectuel. Pour Platon, il s'agit de la contemplation des réalités vraies. Or, il ne va pas de soi que l'on puisse dire qu'une réalité qui à avoir avec nos facultés intellectuelles puisse susciter une affection, et encore moins une affection tel qu'un désir. On peut ressentir de la joie à trouver une vérité, mais ce sentiment n'est pas ce qui fait la vérité. À plus forte raison, il est étrange que le désir amoureux soit non pas seulement dirigé vers le savoir, mais que ce savoir ne soit pas possible sans lui. Mais ce n'est pas la seule raison de s'étonner. Le désir dont parle Platon, c'est l'amour, terme qui comprend le désir sexuel. Or Platon, d'une part, fait référence de manière explicite à l'attirance sexuelle, mais, de plus, ne l'utilise pas comme une métaphore. L'amour de la sagesse n'est pas, ou pas seulement, une expression destinée à nous faire comprendre l'aspiration du philosophe à l'image du désir : la philosophie est véritablement ce désir sexuel, mais dont l'objet a été changé. On trouvera donc dans les dialogues des descriptions du désir philosophique en des termes sexuels des plus explicites. Ces descriptions concernent tant la jouissance sexuelle que la sexualité en tant que fonction de reproduction, ce qui inclut en particulier la fécondation et l'accouchement. Comment Platon conçoit-il ce rapport de la sexualité et de la philosophie et pourquoi fait-il ce rapprochement ? === Qu'est-ce que savoir ? === === Quelle est la place du philosophe dans la cité ? === '''Lecture conseillée''' : ''Apologie de Socrate'' '''Contexte'''<br/> Socrate prononce plusieurs discours devant les juges d'Athènes. Il est en effet accusé de corrompre la jeunesse et d'introduire de nouvelles divinités. Pourtant le philosophe passait son temps à interroger les gens et n'enseignait aucune doctrine en particulier, mais pensait avoir une activité politique de première importance en soumettant ces concitoyens à l'épreuve de ses objections. Qu'est-ce qui fait que le philosophe est malvenu dans la cité et y a-t-il une place s'il risque la mort en exerçant son activité ? == La vie éthique == === Qu'est-ce que l'âme ? === === Qu'est-ce que se connaître soi-même ? === === Qu'est-ce que la vertu ? === === Quel est le bien suprême de la vie ? === ''Philèbe'' === Doit-on craindre la mort ? === '''Lecture conseillée''' : ''Phédon'' '''Contexte'''<br/> Socrate s'entretient avec ses proches le jour de sa mort. L'agitation de ceux-ci contraste avec le calme du philosophe ; ils sont en effet bouleversés à l'idée que la mort est l'anéantissement de la personne et est donc une perte inestimable que rien ne peut consoler. Pourquoi Socrate pense-t-il que la mort n'est pas à craindre ? L'attitude de Socrate à l'égard de la mort est tout aussi essentielle pour la philosophie que son refus de désobéir aux lois : si Socrate se mettait à se lamenter de sa mort prochaine, quelle serait donc le sérieux d'une pratique qui prétend apprendre à vivre en prenant soin de la destinée de l'âme ? La vie philosophique ne serait qu'une grande hypocrisie. == La politique == === Qu'est-ce que la politique ? === === Qu'est-ce qu'une cité juste ? === ''La République'' === Pourquoi agir selon la justice, si l'on peut tirer profit de l'injustice ? === ''La République'' === Pourquoi obéir aux lois ? === '''Lecture conseillée''' : ''Criton'' '''Contexte'''<br/> Quelques jours avant son exécution, Socrate reçoit la visite en prison d'un vieil ami. Celui-ci lui propose de s'évader. Socrate refuse, et lui explique pourquoi il se doit de se soumettre à sa condamnation. Pourquoi Socrate choisit-il d'obéir à la loi, alors que cette obéissance le conduit à une mort injuste ? == L'art == === [[Pour lire Platon/Faut-il contrôler l'art ?|Faut-il contrôler l'art ?]] === '''Lecture conseillée''' : ''La République'' '''Contexte'''<br/> Dans ''La République'', Platon donne une très large place à l'examen du rôle que doit être celui du théâtre dans la cité juste. Cet examen est parallèle à l'institution de la philosophie comme principe et comme fin de la cité. Cette place suffit à montrer que Platon fait de l'art, et en particulier de la tragédie, un objet philosophique à part entière. Cela conduit à se demander pourquoi Platon pose, dans un texte sur les rapports entre philosophie et politique, le problème du rôle civique de l'art, d'autant plus que ce texte est ouvertement hostile au théâtre et le présente comme une menace. Si cette hostilité de Platon se manifeste au cours de réflexions portant sur les institutions de la cité, c'est donc que l'art comporte à ses yeux un risque politique. Quelle est donc cette menace ? ([[Pour lire Platon/Faut-il contrôler l'art ?|lire la suite...]]) == Erreurs fréquentes == Afin d'aider le débutant, nous donnons ici une liste d'erreurs d'interprétation fréquentes, erreurs commises également par les philosophes ! Nous ne donnons que des erreurs vraiment évidentes, que le texte même de Platon réfute. === Platon diviserait le monde en deux === Platon diviserait le monde en deux : d'un côté des réalités éternelles et de l'autre des réalités sensibles. Cette interprétation vient sans doute de certaines écoles platoniciennes et du christianisme, du moins dans les hérésies gnostiques. Au XIXeme siècle, Nietzsche l'a reprise à son compte, accusant Platon de dévaloriser le monde sensible. En réalité, pour Platon, il n'y a qu'une seule et unique réalité : les réalités intelligibles. Le monde sensible est formé d'après leur modèle et n'a de réalité qu'en tant qu'il a un rapport avec ces réalités. Autrement dit, la réalité du monde sensible est aussi intelligible. === Platon aurait inventé l'amour platonique === Platon n'a jamais défendu l'abstinence sexuelle : au contraire, le {{VocPlat|Corps|corps}} doit être en bonne santé pour ne pas être un obstacle à la pensée. Cela implique de ne pas se priver des plaisirs sensibles, nourriture, boisson et sexualité. Mais, dans chacun de ces plaisirs, il faut faire preuve de {{VocPlat|Tempérance|tempérance}}, pour éviter l'excès inverse de l'ascétisme. Lorsque Platon évoque le renoncement, dans le cadre de certaines relations, aux plaisirs charnels, il ne parle pas de supprimer le désir sexuel, mais de le reporter sur une réalité plus élevée, objet de l'{{VocPlat|Amour|amour}} philosophique. Cet amour n'est donc pas purement spirituel. L'amour pour un autre être apparaît dans cette conception comme un moyen. En ce sens, il y a bien là quelque chose de l'amour platonique qui est apparu pendant la Renaissance ; mais on ne peut les confondre, car Platon conçoit toujours l'amour comme désir sexuel, jamais comme une pure spiritualité. === L'âme serait composée de parties === Pour Platon, l'âme serait composée d'une partie intellectuelle, d'une partie ardente et d'une partie désirante. En réalité, bien que les traducteurs emploient parfois le mot ''partie'', l'âme est composée de puissances (de capacités ou de facultés) qui sont des activités de l'âme qui se définissent d'après les objets sur lesquels elles s'exercent. === Chaque groupe de la cité juste possèderait une vertu === On représente souvent la cité juste décrite par Platon comme une cité composée de trois classes possédant chacune une vertu : les gouvernants sont sages ; les gardiens sont courageux ; les producteurs sont tempérants. Cette représentation est fausse puisque les gouvernants sont sages, courageux et tempérants, les gardiens sont courageux et tempérants et ils reçoivent une éducation dans le but de devenir sages, et les producteurs tempérants. === La voix intérieure de Socrate serait celle d'un démon === L'expression ''démon de Socrate'' est utilisée par Plutarque. Mais elle n'apparaît nulle part dans les dialogues. Il est donc faux d'affirmer que Socrate entendrait un démon intérieur. Il reste que nous ne savons pas quel statut donner à cette voix qui est qualifiée de signe démonique : il peut tout aussi bien s'agir d'un dialogue de l'âme de Socrate avec elle-même. gyjwdkqz9cjyufi1678n7l83pc0955o Pour lire Platon/Conseils pour la lecture 0 29968 764001 642541 2026-04-19T14:07:12Z PandaMystique 119061 764001 wikitext text/x-wiki <noinclude>{{Sous-pages}}</noinclude> Ce chapitre propose des conseils pour aborder la lecture de Platon dans les meilleures conditions. Ces conseils ne sont pas des directives sur la manière de ''comprendre'' Platon (hormis les cas où Platon formule explicitement des exigences), mais des remarques qui ont pour but d'éviter des déconvenues au débutant. Par exemple, il serait peu judicieux de commencer à lire Platon par le ''Parménide'', dialogue très abstrait dont la compréhension pose de gros problèmes à tous les commentateurs. Bien entendu, ces conseils ne sont pas des règles absolues, mais il peut être bon de les avoir à l'esprit, même si on ne veut pas les suivre. == Quelles traductions choisir ? == On peut d'emblée écarter toutes les traductions anciennes, c'est-à-dire les traductions qui ont plus de 100 ans. Parmi les traductions en français, les plus connues sont celles de Chambry, de Robin, de Diès, de la Pléiade (Gallimard) et la dernière édition de Platon en poche. Les traductions de Chambry ne sont pas vraiment appréciées des spécialistes, à cause de leur manque de précision. C'est un jugement général qui peut cependant être contredit dans le détail : la traduction du ''Banquet'' est ainsi jugée excellente par Léon Robin. Pour commencer, si vous n'avez pas d'autres traductions sous la main, elles pourront faire l'affaire provisoirement. Les traductions de Robin et Diès ont une réputation excellente, mais elles peuvent être parfois difficiles à lire, du fait de leur fidélité au texte grec. Elles ne sont pas non plus les plus accessibles (surtout à cause du prix), mais il est vivement conseillé de les emprunter à la bibliothèque si cela vous est possible. Certaines de ces traductions ont été éditées également dans la collection Tel, à des prix plus abordables. Pour un usage quotidien, on peut conseiller la dernière édition des dialogues de Platon, qui présente en un seul volume la totalité des textes. En revanche, cette édition ne possède pas un appareil critique très riche. Pour l'étude d'un dialogue, on pourra donc préférer les éditions séparées en poche, qui comportent des introductions et des notes souvent de bonne qualité. == Faut-il apprendre le grec pour bien comprendre Platon ? == Non, mais vous pourrez difficilement échapper à la nécessité d'apprendre au moins le vocabulaire essentiel de Platon. Voici deux exemples qui permettront de comprendre cette nécessité. Le mot grec ''ousia'' est traduit par les mots français ''réalité'', ''réalité vraie'', ''essence'', etc, car ce mot est une notion philosophique qui, tout en étant en général précise, est difficile à traduire avec exactitude ; et, de fait, les traductions varient. Mieux vaut donc savoir ce que traduit le traducteur, pour éviter les contre-sens ou pour éviter de passer à côté du sens d'un texte. Un autre exemple concerne les traductions qui sont devenues des habitudes, mais qui sont ambiguës à cause des sens usuels du mot français. C'est le cas pour le mot ''participation'' ; en Français, ''participer'' a un sens actif que l'on pourrait rendre par ''prendre part à''. Or, ce sens est un contre-sens, puisque le terme grec est plus proche du mot ''recevoir'', qui a un sens passif. Ce dernier sens correspond en fait à l'expression française ''participer de''. Cette connaissance du vocabulaire grec n'est pas à surestimer : certains mots se traduisent d'une seule manière, comme le mot ''âme''. Aussi la connaissance des mots grecs n'est pas toujours un élément indispensable pour la compréhension de la pensée de Platon, bien que, si l'on souhaite avoir une connaissance réellement approfondie de sa pensée, il soit indispensable d'en passer, à un moment ou à un autre, par l'apprentissage du vocabulaire grec. Ce que nous conseillons, c'est de ne pas trop se soucier de ce problème pour le moment, mais d'être attentif aux notes de traduction, quand il y en a, surtout pour en comprendre le sens. Ensuite, à la longue, l'habitude vous aura fait retenir un petit vocabulaire que vous pourrez travailler et enrichir si cela s'avère nécessaire. == Par quels dialogues commencer ? == La réponse dépend de plusieurs facteurs. Pour un débutant, sans culture philosophique particulière, on peut conseiller de commencer par des dialogues qui ont par eux-mêmes un caractère introductif, comme l'''Apologie de Socrate'', le ''Banquet'' ou le ''Premier Alcibiade''. Mais il est aussi possible de commencer par des dialogues qui abordent de manière assez courte des questions précises, comme le ''Lachès'', le ''Lysis'', le ''Charmide'', ''Criton'' et ''Ion''. Si l'on conseille ces dialogues pour commencer, cela ne veut toutefois pas dire qu'ils ne comportent aucune difficulté. Bien au contraire, puisqu'ils comportent quelques difficultés philosophiques assez redoutables. Mais ces difficultés sont moins un obstacle à la lecture que les difficultés qui se présentent dans d'autres dialogues. De plus, ces dialogues se concentrent en général sur une seule question, ce qui permet au lecteur débutant de ne pas être submergé par la multiplication des problèmes et de se représenter assez facilement la discussion dans son ensemble. Avec un certain bagage philosophique, on pourra lire, outre les dialogues précédents, le ''Phédon'', le ''Phèdre'' et le premier livre de la ''République''. Ces dialogues présentent des difficultés philosophiques assez ardues, mais on peut en retirer un profit certain, même si l'on ne comprend pas tout tout de suite. Les dialogues que nous conseillons d'éviter pour commencer sont les dialogues les plus longs et les plus abstraits : le ''Sophiste'', le ''Philèbe'' et le ''Parménide'' demandent des connaissances philosophiques solides et une certaine familiarité avec la pensée de Platon. On peut aussi donner des conseils de lecture selon les thèmes que le lecteur préférerait aborder en premier. Si votre intérêt est surtout d'ordre éthique, nous conseillerons ''Hippias mineur'', ''Criton'', ''[[Pour lire Platon/Guide des dialogues/Ménon|Ménon]]'', ''Euthydème''. Pour ce qui touche à la philosophie, on peut commencer par l'''Apologie'' et ''Le Banquet''. == Comment tirer profit de la lecture des dialogues ? == Dans le chapitre précédent, nous avons expliqué qu'un certain nombre de dialogues se terminent par un échec à trouver la réponse à une question (on parle alors de dialogues ''aporétiques''), et nous avons dit que cet échec n'est pas un résultat négatif, puisqu'il aura au moins été possible de réfuter certaines opinions. Ce point donne de manière évidente une indication sur l'état d'esprit nécessaire pour tirer un profit intellectuel de la lecture de Platon : il faut être ouvert à la réfutation, accepter le dialogue, savoir reconnaître quand on a tort. De telles exigences sont formulées par Socrate à plusieurs reprises. Mais l'absence de réponse évidente conduit souvent le lecteur à tomber dans un piège : si Socrate déclare que la conversation n'a mené à rien, il s'en suivrait qu'il n'y aurait strictement aucune thèse à retenir de ces dialogues. La fausseté de ce raisonnement doit être impérativement présente à l'esprit du lecteur, sous peine de passer à côté de théories importantes qui se trouvent bel et bien dans les dialogues aporétiques et qui sont développées dans d'autres dialogues. Par exemple, si Socrate et Hippias ne parviennent pas à définir le beau (dans l’''Hippias majeur''), Socrate a tout de même formulé un cadre de pensée dans lequel la définition devait s'inscrire, cadre qui n'est pas anodin puisqu'il donne une esquisse de la théorie majeure de Platon, la théorie des formes. De plus, la réfutation d'une opinion ne signifie pas toujours qu'elle soit absolument fausse. Dans certains cas, en effet, la réfutation ne porte pas tant sur l'opinion examinée que sur le point de vue d'après lequel elle est énoncée. Par exemple, dans le ''Charmide'', plusieurs réponses de Critias à la question de savoir ce qu'est la sagesse ne sont pas très éloignées de la pensée de Socrate ; elles sont pourtant réfutées par ce dernier. Si Critias est réfuté, ce n'est pas parce que ses réponses sont fausses (pas toutes en tout cas), mais parce qu'il oublie de les rapporter à une réalité dont Socrate a souligné l'importance au début du dialogue : l'âme. Dès lors, la réfutation peut suggérer que Critias manque tout simplement le sujet essentiel de la sagesse, à savoir le soin de l'âme, ce qui veut dire que même en formulant des réponses correctes, il n'a pas compris ces réponses. Il est donc fortement conseillé d'être attentif à la manière dont Socrate pose ses questions et à certaines de ses affirmations, surtout celles qui se trouvent dans les prologues des dialogues, car elles donnent souvent une esquisse de réponse, même si le dialogue ne donne à la fin aucune formulation définitive. Cette exigence est rendue particulièrement nécessaire par le fait que Socrate utilise souvent la feinte et ne s'exprime donc pas toujours de manière directe. == Quels outils utiliser au cours de la lecture ? == Dans l'idéal, la lecture d'un philosophe se suffit à elle-même. Il est d'ailleurs évident que la connaissance d'un auteur passe par la lecture de ses textes et qu'aucun commentaire ne saurait s'y substituer. Il y a néanmoins plusieurs raisons de modérer fortement un tel point de vue. Tout d'abord, le contexte historique des dialogues n'est pas une évidence. Il est difficile de comprendre pourquoi Platon met tel personnage en scène de telle façon, ou pourquoi telle de ses thèses est originale par rapport à son temps, sans connaître leur contexte. On ne peut donc lire Platon comme un auteur contemporain. Ensuite, il est utile d'étudier Platon par des moyens qui vous permettront de structurer votre connaissance de ses dialogues plus rapidement et avec plus de précision que par la seule lecture des textes. Ces moyens sont en particulier les articles sur un certain sujet et les entrées d'un dictionnaire sur Platon. Ces moyens ont une valeur pragmatique que nous conseillons de ne pas négliger : si vous apprenez par exemple ce que c'est que la cité pour Platon dans un livre sur le vocabulaire de Platon, vous augmenterez votre compréhension de tous les textes où il sera question de la cité, alors même que vous n'avez pas encore lu tous les dialogues et que vous n'en avez pas encore fait une synthèse pour vous-même. Pour ces raisons, nous donnerons le conseil suivant, car il permet de gagner du temps tout en gagnant en compréhension : lire les introductions et les notes d'une édition, et utiliser un vocabulaire ou lire des recueils d'articles sur un sujet donné. À ce titre, les introductions de la dernière édition en poche sont à recommander. En ce qui concerne le vocabulaire, ce livre fournira bientôt en annexe un vocabulaire de base. == Quels livres lire sur Platon ? == *Voir la [[Pour lire Platon/Bibliographie|bibliographie]] commentée. Sur la lancée de la question précédente, nous allons conseiller, sur la base de notre propre expérience, quelques livres qui nous paraissent répondre aux critères suivants : clarté, simplicité relative et une certaine exhaustivité dans les sujets traités. *''Lire Platon'', sous la direction de Luc Brisson et Francesco Fronterotta *''Le Vocabulaire de Platon'', de Luc Brisson et Jean-François Pradeau *''Lectures de Platon'', sous la direction de Monique Dixsaut et Gilles Kévorkian {{AutoCat}} bdhtzw46urdgw295dutxynq3uqnf7vi Pour lire Platon/Guide des dialogues 0 30540 764072 651866 2026-04-20T05:47:43Z PandaMystique 119061 764072 wikitext text/x-wiki <noinclude>{{Pour lire Platon}}</noinclude> <div style="text-align: center; font-size: 1.4em; font-weight: bold; margin-bottom: 1em; padding-bottom: 0.5em; border-bottom: 2px solid #8b7355; color: #3a3530;">※ Guide des dialogues platoniciens ※</div> <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.5em; width: 100%;"> <div style="padding: 1.5em; background: linear-gradient(135deg, #ffffff40 0%, #e0e4e840 100%); border-radius: 10px; box-shadow: 0 3px 10px rgba(0,0,0,0.1);"> : [[/Premier Alcibiade/]] : [[/Second Alcibiade/]] : [[/Hippias majeur/]] : [[/Hippias mineur/]] : [[/Ion/]] : [[/Lachès/]] : [[/Charmide/]] : [[/Lysis/]] </div> <div style="padding: 1.5em; background: linear-gradient(135deg, #ffffff40 0%, #e0e4e840 100%); border-radius: 10px; box-shadow: 0 3px 10px rgba(0,0,0,0.1);"> : [[/Protagoras/]] : [[/Euthyphron/]] : [[/Gorgias/]] : [[/Ménon/]] : [[/Apologie de Socrate/]] : [[/Criton/]] : [[/Euthydème/]] </div> <div style="padding: 1.5em; background: linear-gradient(135deg, #ffffff40 0%, #e0e4e840 100%); border-radius: 10px; box-shadow: 0 3px 10px rgba(0,0,0,0.1);"> : [[/Ménexène/]] : [[/Cratyle/]] : [[/Phédon/]] : [[/Le Banquet/]] : [[/La République/]] : [[/Phèdre/]] : [[/Théétète/]] </div> <div style="padding: 1.5em; background: linear-gradient(135deg, #ffffff40 0%, #e0e4e840 100%); border-radius: 10px; box-shadow: 0 3px 10px rgba(0,0,0,0.1);"> : [[/Parménide/]] : [[/Le Sophiste/]] : [[/Le Politique/]] : [[/Philèbe/]] : [[/Timée/]] : [[/Critias/]] : [[/Les Lois/]] </div> </div> [[Catégorie:Pour lire Platon (livre)]] 3txwbdms326l4ml2ifohnj2fk6swsar Pour lire Platon/Premiers pas 0 66394 763998 763982 2026-04-19T13:44:03Z PandaMystique 119061 763998 wikitext text/x-wiki {{Pour lire Platon}} Ce chapitre est une foire aux questions destinée aux grands débutants. Il a pour but de donner une première culture générale sur Platon. On ne propose ici que des réponses brèves, en termes simples, sur des questions générales, sans entrer dans les détails. La présentation sous forme de questions doit permettre au lecteur de trouver facilement une réponse aux interrogations qu’il peut se poser au moment de commencer l’étude de Platon. La liste des questions n’est pas close, et chacun peut en proposer d’autres afin d’améliorer l’utilité du chapitre. == Qui est Platon ? == Platon est un philosophe grec qui a vécu aux Ve et IVe siècles av. J.-C., il y a environ vingt-quatre siècles. Il appartenait à une famille aristocratique d’Athènes, qui était alors l’une des plus puissantes cités et l’un des grands centres culturels de la Grèce antique. Platon est l’un des tout premiers, et l’un des plus importants, philosophes de la tradition occidentale. Il a joué un rôle décisif dans la définition classique de ce que l’on appelle la philosophie. == Pourquoi Platon a-t-il écrit ? == Platon est issu d’une famille aristocratique, et son éducation le destinait à la vie politique. Plusieurs facteurs l’ont toutefois détourné de cette voie : sa rencontre avec Socrate, l’exécution de celui-ci, ainsi que la corruption d’Athènes et, en particulier, les régimes arbitraires qui ont suivi la défaite contre Sparte. Platon a alors jugé nécessaire de réfléchir à ce que doit être une cité juste, et il a cherché à concilier la pensée philosophique et l’action politique. == Qu’a-t-il écrit ? == Sous le nom de Platon, nous possédons plus d’une trentaine de dialogues, un recueil de définitions et quelques lettres. Tous ces textes ne sont pas attribués avec certitude à Platon. On classe généralement ces dialogues en trois groupes selon le degré de certitude de leur authenticité : les dialogues reconnus comme authentiques, les dialogues douteux, et les dialogues dont il est certain qu’ils ne sont pas de lui. Il peut naturellement y avoir des désaccords entre les spécialistes sur le classement de certains dialogues. == Dans quel ordre Platon a-t-il écrit ses dialogues ? == Nous ne le savons pas avec certitude. Le classement qui fait l’objet du plus large accord distingue trois groupes : les dialogues de jeunesse, de maturité et de vieillesse. Ce classement possède une certaine objectivité, dans la mesure où il repose en partie sur l’analyse du style et du vocabulaire. À l’intérieur de chacun de ces groupes, en revanche, il est impossible d’établir un ordre chronologique sûr. De plus, certains dialogues peuvent être placés par certains commentateurs dans un groupe plutôt que dans un autre, selon l’interprétation qu’ils donnent de leur contenu. Il arrive par exemple qu’un dialogue de jeunesse paraisse contenir des thèses qui le rapprochent davantage des dialogues de maturité. Dans ce type d’argumentation, les certitudes restent faibles. La question de la chronologie ne paraît d’ailleurs pas globalement essentielle pour commencer à lire Platon. == Pourquoi Platon a-t-il écrit des dialogues et non des traités ? == On peut en donner plusieurs raisons. Les dialogues de Platon ne sont ni des cours ni des exposés systématiques de sa pensée, mais des conversations inscrites dans le cadre de la vie quotidienne de l’Athènes de la fin du Ve siècle av. J.-C. La conversation permet aux personnages de confronter leurs opinions et de voir s’ils sont capables de répondre aux objections qui surgissent au cours de l’examen. Elle constitue donc une manière de philosopher qui suppose que les interlocuteurs acceptent de prendre en compte des avis différents pour progresser dans la recherche de la vérité. La forme dialoguée illustre ainsi l’effort sur soi que doivent accomplir les interlocuteurs, alors qu’un traité ne permet pas un tel échange avec le lecteur. L’importance que Platon accorde au dialogue se voit aussi au fait qu’un dialogue ne s’achève pas nécessairement sur une réussite. Même lorsqu’elle échoue, la recherche a permis de mettre des opinions à l’épreuve de la réfutation, ce qui représente déjà un gain réel : les personnages se sont délivrés de certains préjugés, même s’ils n’ont pas encore découvert la vérité. On peut ajouter une autre raison : l’attitude de Socrate à l’égard de certains interlocuteurs peu aptes à ce genre de discussion. Socrate feint alors l’ignorance afin de les pousser à parler avec trop d’assurance, ce qui permet de les mettre plus facilement face à leurs contradictions et à leur ignorance. Une telle dramatisation serait difficile à rendre dans un traité. == Qui sont les personnages des dialogues de Platon ? == Les personnages des dialogues sont des jeunes Athéniens, des savants étrangers, des artistes, des devins, des amis de Socrate ou des proches de Platon. Mais le personnage qui revient dans presque tous les dialogues est son maître, Socrate. Les jeunes gens présents dans les dialogues rappellent le lien privilégié que Socrate entretient avec la jeunesse. Cette relation s’inscrit dans un contexte culturel grec bien particulier, mais Socrate la transforme en relation de maître à élève, dans laquelle le souci de l’âme prend le pas sur l’attirance pour la beauté corporelle. Lorsque les personnages sont des savants, des artistes, des devins ou des sophistes, Socrate cherche généralement à mettre au jour leur ignorance et la vanité de leurs prétentions, ce qui donne parfois lieu à des échanges très vifs. Dans certains cas, les personnages sont aussi des disciples célèbres de Socrate, connus pour le rôle funeste qu’ils ont joué dans l’histoire grecque. C’est le cas de Critias, de Charmide, qui était l’oncle de Platon, et d’Alcibiade. La conversation montre alors que Socrate les a encouragés dans la voie de la philosophie, mais qu’ils ont échoué à comprendre véritablement ce qu’il leur enseignait, par exemple lorsque la discussion porte sur la sagesse ou sur l’art politique. Ces dialogues ont donc également une portée apologétique. == Pourquoi Socrate est-il le personnage principal des dialogues de Platon ? == Le dialogue socratique n’est pas un genre propre à Platon, et celui-ci ne l’a sans doute pas inventé. Il s’est développé tout au long du IVe siècle av. J.-C. La mise en scène de Socrate constitue une forme d’apologie d’un philosophe dont la mort a profondément marqué ses disciples, soucieux de défendre sa mémoire. À partir des grands dialogues de vieillesse, cependant, le personnage de Socrate tend à s’effacer, et il n’apparaît plus dans les ''Lois''. L’une des raisons avancées pour expliquer cet effacement est que les thèses de Platon seraient alors devenues trop éloignées de ce que ses contemporains savaient du Socrate historique. == Le personnage de Socrate est-il une image fidèle du Socrate historique ? == Comme cette mise en scène de Socrate est aussi une création littéraire et philosophique, il est difficile de distinguer ce qui revient à Platon de ce qui devrait être attribué à Socrate. Ce problème n’a pas reçu de solution pleinement satisfaisante parmi les commentateurs. Certaines thèses présentes dans les dialogues sont néanmoins souvent rapportées à Socrate, comme l’importance accordée à la définition des concepts et le rôle central de l’enquête morale, que l’on observe notamment dans les dialogues dits socratiques. == De quoi Platon parle-t-il dans ses dialogues ? == Les sujets sont très nombreux, et il ne saurait être question d’en proposer ici une liste complète ni un résumé détaillé. On peut toutefois s’en faire une idée générale de la manière suivante. Un certain nombre de dialogues, souvent les plus courts, que l’on regroupe sous le nom de dialogues socratiques, portent sur des questions morales : le courage, la vertu, le mensonge, l’amitié, la piété ou encore la justice. Dans l’ensemble, ces dialogues s’organisent autour de l’idée que la vertu est une forme de connaissance. Ces recherches éthiques culminent notamment dans le ''Ménon'' et le ''Protagoras''. D’autres dialogues ont pour objet la politique, comme la ''République'', le ''Politique'' et les ''Lois''. D’autres encore abordent longuement des questions métaphysiques, telles que l’être, le non-être et la connaissance, en particulier le problème de la connaissance des réalités vraies. Par souci de clarté, on peut soutenir que tous ces sujets sont liés entre eux par ce que l’on appelle la théorie des Formes. Cette théorie pose l’hypothèse de réalités immuables, qui servent de modèles au monde sensible et changeant. Pour le philosophe, la connaissance de ces Formes permet de fonder la morale et la politique, deux domaines dont le but est le soin de l’âme, qui constitue l’un des soucis majeurs de Platon. == Les dialogues de Platon forment-ils un système philosophique ? == La réponse à cette question varie selon les interprètes, et il est difficile d’en donner une idée précise sans entrer dans des détails qui dépasseraient le cadre de cette introduction. On se contentera donc ici d’une réponse schématique en deux points. Si l’on considère que Platon n’a pas exposé sa pensée dans ses dialogues, mais dans un enseignement oral aujourd’hui perdu pour nous, ou si l’on estime simplement que les dialogues sont d’abord écrits pour mettre à l’épreuve la réflexion du lecteur sans lui imposer de doctrine, alors les textes ne nous mettent pas vraiment en présence d’un ensemble ordonné et cohérent de thèses philosophiques que l’on pourrait appeler un système. Si, au contraire, on pense que Platon a bien exposé ses idées dans les dialogues, alors l’ensemble des textes forme un tout, même si ce tout ne présente pas de manière évidente un système achevé. Cette interprétation, qui est la plus répandue, s’accompagne généralement de l’idée qu’il faut distinguer plusieurs groupes de dialogues correspondant à différents moments de la pensée de Platon. On aurait ainsi moins affaire à un système constitué d’un seul bloc qu’à une pensée cohérente et systématique en évolution, comme en témoigne le fait que les dialogues se présentent toujours sous la forme de recherches portant sur une question déterminée. == Pourquoi lire un auteur mort il y a vingt-quatre siècles ? == La civilisation de Platon et ses valeurs ne sont pas les nôtres, et elles nous sont très éloignées dans le temps. Même si la Grèce ancienne est pour nous l’un des lieux d’origine de la science et de la philosophie, et même si la civilisation européenne n’aurait pas été ce qu’elle est sans l’Antiquité, ses manières de voir le monde, la nature, l’être humain et la vie peuvent sembler trop lointaines pour nous intéresser encore. Que peut donc nous apporter la lecture de Platon ? Les questions posées par Platon dans ses dialogues ne sont pas différentes de celles que l’on retrouve dans toutes les cultures et à toutes les époques : qu’est-ce que l’univers ? que peut-on connaître ? qu’est-ce que l’être humain ? qu’est-ce que la mort ? Platon écrit donc sur des sujets qui demeurent toujours actuels : le sens de l’existence, la meilleure manière de vivre, le mal, la justice, l’amitié, l’amour, la sexualité, le plaisir ou l’art. Sur toutes ces questions qui continuent de nous concerner, Platon apporte des réponses depuis une perspective culturelle différente de la nôtre. Cette différence de point de vue peut nous aider à prendre du recul par rapport à certaines opinions qui nous paraissent évidentes simplement parce que notre culture nous les a inculquées très tôt. Bien sûr, certaines de ses réponses sont aujourd’hui scientifiquement dépassées ou ne sont plus philosophiquement admises. Il vaut mieux lire un ouvrage d’astrophysique qu’un dialogue de Platon pour apprendre quelque chose sur l’univers. Il reste pourtant utile d’être attentif à la manière dont Platon aborde ces questions. Même si sa représentation de l’univers est fausse, et même si certains de ses textes peuvent paraître très ennuyeux sur ce point, les cadres de sa pensée ne sont pas pour autant automatiquement disqualifiés. Enfin, toutes les réponses de Platon ne nous sont pas devenues étrangères. Sur certains sujets, nous continuons encore aujourd’hui à réfléchir dans le cadre intellectuel qu’il a contribué à définir. Ce point sera illustré dans le chapitre suivant. 8119cy4xjv0dz5mrwogyyj8kb4ya4ei 764000 763998 2026-04-19T14:05:53Z PandaMystique 119061 764000 wikitext text/x-wiki {{Sous-pages}} Ce chapitre est une foire aux questions destinée aux grands débutants. Il a pour but de donner une première culture générale sur Platon. On ne propose ici que des réponses brèves, en termes simples, sur des questions générales, sans entrer dans les détails. La présentation sous forme de questions doit permettre au lecteur de trouver facilement une réponse aux interrogations qu’il peut se poser au moment de commencer l’étude de Platon. La liste des questions n’est pas close, et chacun peut en proposer d’autres afin d’améliorer l’utilité du chapitre. == Qui est Platon ? == Platon est un philosophe grec qui a vécu aux Ve et IVe siècles av. J.-C., il y a environ vingt-quatre siècles. Il appartenait à une famille aristocratique d’Athènes, qui était alors l’une des plus puissantes cités et l’un des grands centres culturels de la Grèce antique. Platon est l’un des tout premiers, et l’un des plus importants, philosophes de la tradition occidentale. Il a joué un rôle décisif dans la définition classique de ce que l’on appelle la philosophie. == Pourquoi Platon a-t-il écrit ? == Platon est issu d’une famille aristocratique, et son éducation le destinait à la vie politique. Plusieurs facteurs l’ont toutefois détourné de cette voie : sa rencontre avec Socrate, l’exécution de celui-ci, ainsi que la corruption d’Athènes et, en particulier, les régimes arbitraires qui ont suivi la défaite contre Sparte. Platon a alors jugé nécessaire de réfléchir à ce que doit être une cité juste, et il a cherché à concilier la pensée philosophique et l’action politique. == Qu’a-t-il écrit ? == Sous le nom de Platon, nous possédons plus d’une trentaine de dialogues, un recueil de définitions et quelques lettres. Tous ces textes ne sont pas attribués avec certitude à Platon. On classe généralement ces dialogues en trois groupes selon le degré de certitude de leur authenticité : les dialogues reconnus comme authentiques, les dialogues douteux, et les dialogues dont il est certain qu’ils ne sont pas de lui. Il peut naturellement y avoir des désaccords entre les spécialistes sur le classement de certains dialogues. == Dans quel ordre Platon a-t-il écrit ses dialogues ? == Nous ne le savons pas avec certitude. Le classement qui fait l’objet du plus large accord distingue trois groupes : les dialogues de jeunesse, de maturité et de vieillesse. Ce classement possède une certaine objectivité, dans la mesure où il repose en partie sur l’analyse du style et du vocabulaire. À l’intérieur de chacun de ces groupes, en revanche, il est impossible d’établir un ordre chronologique sûr. De plus, certains dialogues peuvent être placés par certains commentateurs dans un groupe plutôt que dans un autre, selon l’interprétation qu’ils donnent de leur contenu. Il arrive par exemple qu’un dialogue de jeunesse paraisse contenir des thèses qui le rapprochent davantage des dialogues de maturité. Dans ce type d’argumentation, les certitudes restent faibles. La question de la chronologie ne paraît d’ailleurs pas globalement essentielle pour commencer à lire Platon. == Pourquoi Platon a-t-il écrit des dialogues et non des traités ? == On peut en donner plusieurs raisons. Les dialogues de Platon ne sont ni des cours ni des exposés systématiques de sa pensée, mais des conversations inscrites dans le cadre de la vie quotidienne de l’Athènes de la fin du Ve siècle av. J.-C. La conversation permet aux personnages de confronter leurs opinions et de voir s’ils sont capables de répondre aux objections qui surgissent au cours de l’examen. Elle constitue donc une manière de philosopher qui suppose que les interlocuteurs acceptent de prendre en compte des avis différents pour progresser dans la recherche de la vérité. La forme dialoguée illustre ainsi l’effort sur soi que doivent accomplir les interlocuteurs, alors qu’un traité ne permet pas un tel échange avec le lecteur. L’importance que Platon accorde au dialogue se voit aussi au fait qu’un dialogue ne s’achève pas nécessairement sur une réussite. Même lorsqu’elle échoue, la recherche a permis de mettre des opinions à l’épreuve de la réfutation, ce qui représente déjà un gain réel : les personnages se sont délivrés de certains préjugés, même s’ils n’ont pas encore découvert la vérité. On peut ajouter une autre raison : l’attitude de Socrate à l’égard de certains interlocuteurs peu aptes à ce genre de discussion. Socrate feint alors l’ignorance afin de les pousser à parler avec trop d’assurance, ce qui permet de les mettre plus facilement face à leurs contradictions et à leur ignorance. Une telle dramatisation serait difficile à rendre dans un traité. == Qui sont les personnages des dialogues de Platon ? == Les personnages des dialogues sont des jeunes Athéniens, des savants étrangers, des artistes, des devins, des amis de Socrate ou des proches de Platon. Mais le personnage qui revient dans presque tous les dialogues est son maître, Socrate. Les jeunes gens présents dans les dialogues rappellent le lien privilégié que Socrate entretient avec la jeunesse. Cette relation s’inscrit dans un contexte culturel grec bien particulier, mais Socrate la transforme en relation de maître à élève, dans laquelle le souci de l’âme prend le pas sur l’attirance pour la beauté corporelle. Lorsque les personnages sont des savants, des artistes, des devins ou des sophistes, Socrate cherche généralement à mettre au jour leur ignorance et la vanité de leurs prétentions, ce qui donne parfois lieu à des échanges très vifs. Dans certains cas, les personnages sont aussi des disciples célèbres de Socrate, connus pour le rôle funeste qu’ils ont joué dans l’histoire grecque. C’est le cas de Critias, de Charmide, qui était l’oncle de Platon, et d’Alcibiade. La conversation montre alors que Socrate les a encouragés dans la voie de la philosophie, mais qu’ils ont échoué à comprendre véritablement ce qu’il leur enseignait, par exemple lorsque la discussion porte sur la sagesse ou sur l’art politique. Ces dialogues ont donc également une portée apologétique. == Pourquoi Socrate est-il le personnage principal des dialogues de Platon ? == Le dialogue socratique n’est pas un genre propre à Platon, et celui-ci ne l’a sans doute pas inventé. Il s’est développé tout au long du IVe siècle av. J.-C. La mise en scène de Socrate constitue une forme d’apologie d’un philosophe dont la mort a profondément marqué ses disciples, soucieux de défendre sa mémoire. À partir des grands dialogues de vieillesse, cependant, le personnage de Socrate tend à s’effacer, et il n’apparaît plus dans les ''Lois''. L’une des raisons avancées pour expliquer cet effacement est que les thèses de Platon seraient alors devenues trop éloignées de ce que ses contemporains savaient du Socrate historique. == Le personnage de Socrate est-il une image fidèle du Socrate historique ? == Comme cette mise en scène de Socrate est aussi une création littéraire et philosophique, il est difficile de distinguer ce qui revient à Platon de ce qui devrait être attribué à Socrate. Ce problème n’a pas reçu de solution pleinement satisfaisante parmi les commentateurs. Certaines thèses présentes dans les dialogues sont néanmoins souvent rapportées à Socrate, comme l’importance accordée à la définition des concepts et le rôle central de l’enquête morale, que l’on observe notamment dans les dialogues dits socratiques. == De quoi Platon parle-t-il dans ses dialogues ? == Les sujets sont très nombreux, et il ne saurait être question d’en proposer ici une liste complète ni un résumé détaillé. On peut toutefois s’en faire une idée générale de la manière suivante. Un certain nombre de dialogues, souvent les plus courts, que l’on regroupe sous le nom de dialogues socratiques, portent sur des questions morales : le courage, la vertu, le mensonge, l’amitié, la piété ou encore la justice. Dans l’ensemble, ces dialogues s’organisent autour de l’idée que la vertu est une forme de connaissance. Ces recherches éthiques culminent notamment dans le ''Ménon'' et le ''Protagoras''. D’autres dialogues ont pour objet la politique, comme la ''République'', le ''Politique'' et les ''Lois''. D’autres encore abordent longuement des questions métaphysiques, telles que l’être, le non-être et la connaissance, en particulier le problème de la connaissance des réalités vraies. Par souci de clarté, on peut soutenir que tous ces sujets sont liés entre eux par ce que l’on appelle la théorie des Formes. Cette théorie pose l’hypothèse de réalités immuables, qui servent de modèles au monde sensible et changeant. Pour le philosophe, la connaissance de ces Formes permet de fonder la morale et la politique, deux domaines dont le but est le soin de l’âme, qui constitue l’un des soucis majeurs de Platon. == Les dialogues de Platon forment-ils un système philosophique ? == La réponse à cette question varie selon les interprètes, et il est difficile d’en donner une idée précise sans entrer dans des détails qui dépasseraient le cadre de cette introduction. On se contentera donc ici d’une réponse schématique en deux points. Si l’on considère que Platon n’a pas exposé sa pensée dans ses dialogues, mais dans un enseignement oral aujourd’hui perdu pour nous, ou si l’on estime simplement que les dialogues sont d’abord écrits pour mettre à l’épreuve la réflexion du lecteur sans lui imposer de doctrine, alors les textes ne nous mettent pas vraiment en présence d’un ensemble ordonné et cohérent de thèses philosophiques que l’on pourrait appeler un système. Si, au contraire, on pense que Platon a bien exposé ses idées dans les dialogues, alors l’ensemble des textes forme un tout, même si ce tout ne présente pas de manière évidente un système achevé. Cette interprétation, qui est la plus répandue, s’accompagne généralement de l’idée qu’il faut distinguer plusieurs groupes de dialogues correspondant à différents moments de la pensée de Platon. On aurait ainsi moins affaire à un système constitué d’un seul bloc qu’à une pensée cohérente et systématique en évolution, comme en témoigne le fait que les dialogues se présentent toujours sous la forme de recherches portant sur une question déterminée. == Pourquoi lire un auteur mort il y a vingt-quatre siècles ? == La civilisation de Platon et ses valeurs ne sont pas les nôtres, et elles nous sont très éloignées dans le temps. Même si la Grèce ancienne est pour nous l’un des lieux d’origine de la science et de la philosophie, et même si la civilisation européenne n’aurait pas été ce qu’elle est sans l’Antiquité, ses manières de voir le monde, la nature, l’être humain et la vie peuvent sembler trop lointaines pour nous intéresser encore. Que peut donc nous apporter la lecture de Platon ? Les questions posées par Platon dans ses dialogues ne sont pas différentes de celles que l’on retrouve dans toutes les cultures et à toutes les époques : qu’est-ce que l’univers ? que peut-on connaître ? qu’est-ce que l’être humain ? qu’est-ce que la mort ? Platon écrit donc sur des sujets qui demeurent toujours actuels : le sens de l’existence, la meilleure manière de vivre, le mal, la justice, l’amitié, l’amour, la sexualité, le plaisir ou l’art. Sur toutes ces questions qui continuent de nous concerner, Platon apporte des réponses depuis une perspective culturelle différente de la nôtre. Cette différence de point de vue peut nous aider à prendre du recul par rapport à certaines opinions qui nous paraissent évidentes simplement parce que notre culture nous les a inculquées très tôt. Bien sûr, certaines de ses réponses sont aujourd’hui scientifiquement dépassées ou ne sont plus philosophiquement admises. Il vaut mieux lire un ouvrage d’astrophysique qu’un dialogue de Platon pour apprendre quelque chose sur l’univers. Il reste pourtant utile d’être attentif à la manière dont Platon aborde ces questions. Même si sa représentation de l’univers est fausse, et même si certains de ses textes peuvent paraître très ennuyeux sur ce point, les cadres de sa pensée ne sont pas pour autant automatiquement disqualifiés. Enfin, toutes les réponses de Platon ne nous sont pas devenues étrangères. Sur certains sujets, nous continuons encore aujourd’hui à réfléchir dans le cadre intellectuel qu’il a contribué à définir. Ce point sera illustré dans le chapitre suivant. 1zf60vbbgh6sf16f5yvbhl9xvmtcie7 Fonctionnement d'un ordinateur/L'unité de contrôle 0 69026 764021 763777 2026-04-19T17:05:27Z Mewtow 31375 /* Les microcodes réinscriptibles */ 764021 wikitext text/x-wiki Pour rappel, les instructions se font en plusieurs étapes, appelées micro-opérations. Pour chaque instruction, il faut déduire quelles sont les micro-opérations à exécuter et dans quel ordre. Mais l'instruction chargée depuis la mémoire ne précise pas les micro-opérations à faire, elle se contente juste de dire quelle opération effectuer et sur quels opérandes. Le processeur doit donc traduire l'instruction en une séquence de micro-opérations, en une séquence de signaux de commandes adéquats. C'est le rôle de l''''unité de décodage d'instruction''', une portion du processeur qui « décode » l'instruction. [[File:Unité de décodage d'instruction.png|centre|vignette|upright=2|Unité de décodage d'instruction]] Une micro-opération configure le chemin de donnée d'une manière bien précise, afin de faire une opération de base : copie entre registres, accès mémoire, opération sur l'ALU. Pour cela, il faut configurer l'ALU pour qu'elle fasse l'opération adéquate, configurer le banc de registre pour lire /écrire les bons registres, etc. La micro-opération envoie des '''signaux de commande''' adéquats au chemin de données. Pour simplifier, une micro-opération est encodée en concaténant les signaux de commande pour l'ALU, ceux pour les registres, pour l'unité mémoire, etc. Chaque micro-opération encode les signaux de commande à destination du chemin de données. {|class="wikitable" |- ! colspan="4" | Micro-opération, encodage en binaire |- | Signaux de commande pour l'ALU | Signaux de commande pour les registres | Signaux de commande pour l'unité d'accès mémoire | Autres signaux de commande |} Il existe des processeurs assez rares où chaque instruction machine est une micro-opération. Son encodage précise directement les signaux de commande, pas besoin d'une unité de décodage d'instruction. De telles architectures sont appelées des ''architectures actionnées par déplacement''. Elles feront l'objet d'un chapitre dédié, nous allons les mettre de côté pour le moment et nous concentrer sur des architectures plus courantes. ==Les séquenceurs câblés et microcodés== Pour un même jeu d'instruction, des processeurs de marque différente peuvent avoir des séquenceurs différents. Les différences entre séquenceurs sont nombreuses, une partie étant liée à des optimisations plus ou moins sophistiquées du décodage. Mais l'une d'entre elle permet de distinguer deux types purs de séquenceurs, sur un critère assez pertinent. La distinction se fait sur la nature du séquenceur, sur le circuit de décodage utilisé. Le séquenceur est un circuit séquentiel, c’est-à-dire qu'il contient un circuit combinatoire et des registres. Or, nous avons vu dans les chapitres précédents que tout circuit combinatoire peut être remplacé ainsi par une ROM avec le contenu adéquat. Et le circuit combinatoire dans le séquenceur ne fait pas exception à cette règle. Le circuit combinatoire peut être implémenté de trois grandes manières différentes. * La première méthode est d'utiliser un circuit combinatoire proprement dit, construit avec des portes logiques, en utilisant les méthodes du chapitre sur les portes logiques. * La seconde remplace ce circuit par une mémoire ROM dans laquelle on écrit la table de vérité du circuit. * La troisième solution est une solution intermédiaire qui utilise un circuit dit PLA (''Programmable Logic Array''). Il y a donc un choix à faire : est-ce le séquenceur incorpore un circuit combinatoire ou une mémoire ROM ? Cela permet de distinguer les séquenceurs câblés, basés sur un circuit combinatoire/séquentiel, et les séquenceurs microcodés, basés sur une mémoire ROM. Les deux ont évidemment des avantages et des inconvénients différents, comme nous allons le voir. ==Les séquenceurs câblés== Si les instructions sont décodées par un assemblage de portes logiques et de registres, on parle de '''séquenceur câblé'''. Plus le nombre d'instructions est important, plus un séquenceur câblé est compliqué à concevoir par rapport à ses alternatives. La complexité du séquenceur dépend aussi de la complexité des instructions machine. Autant dire que les processeurs CISC n'utilisent pas trop ce genre de séquenceurs et préfèrent utiliser des séquenceurs microcodés ou hybrides, alors que les séquenceurs câblés sont préférés sur les processeurs RISC. ===L'implémentation du séquenceur=== Sur certains processeurs assez rares, toute instruction s’exécute en une seule micro-opération, ce qui fait que le séquenceur se résume alors à un simple circuit combinatoire. C'est très rare, car cela implique que toutes les instructions doivent se faire en moins d'un cycle d'horloge. Pour cela, la durée d'un cycle d'horloge doit se caler sur l'instruction la plus lente : un accès mémoire prendra autant de temps qu'une addition, ou qu'une multiplication, etc. Ensuite, il faut que le processeur soit une architecture Harvard, afin de charge l'instruction tout en accédant aux données en parallèle, le tout en un seul cycle d'horloge processeur. [[File:Séquenceur combinatoire 01.png|centre|vignette|upright=2.5|Séquenceur combinatoire]] Sur les autres processeurs, il y a des instructions qui demandent d’exécuter une suite de micro-opérations. Pour cela, le séquenceur devient un circuit séquentiel, qui intègre un registre/compteur. La présence de ce registre s’explique par le fait que le séquenceur a besoin de savoir à quelle micro-opération il en est, information qui est mémorisée dans un registre. [[File:Séquenceur séquentiel.png|centre|vignette|upright=2|Séquenceur séquentiel]] Dans le cas le plus simple, le séquenceur est basé sur un simple compteur couplé à un circuit combinatoire. Le compteur mémorise à quelle micro-opération il en est, en lui attribuant un numéro : s'il en est à la première, seconde, troisième micro-opération, etc. Le compteur est incrémenté à chaque micro-opération réussie (les accès mémoires peuvent prendre plusieurs cycles pour une seule micro-opération, si le CPU doit attendre la RAM). Il est réinitialisé quand l'instruction se termine, à savoir quand le compteur a atteint le nombre de micro-opérations adéquat pour exécuter l'instruction. Le compteur n'est pas forcément un compteur normal, qui stocke une valeur en binaire. Il s'agit souvent d'un compteur basé un registre à décalage, appelé un '''compteur ''one-hot''''', ou encore un compteur en anneau. La raison est que les compteurs en anneau sont très rapides et utilisent peu de circuits, sans compter qu'ils permettent de se passer de comparateur pour déterminer la valeur du compteur. Leur seul défaut est que les économies en portes logiques sont contrebalancées par un plus grand nombre de bascules, qui est cependant acceptable si le compteur encode peu de valeurs. Si on veut un séquenceur qui fonctionne rapidement, en moins d'un cycle d'horloge, c'est la meilleure solution qui soit. En combinant le compteur avec l'opcode, le séquenceur détermine quel est la micro-opération à effectuer. Pour être plus précis, un circuit combinatoire intégré au séquenceur prend en entrée le compteur et l'opcode de l'instruction machine, puis fournit en sortie la micro-opération adéquate. Dans son implémentation la plus simple, ce circuit combinatoire est composé de deux sous-circuits : un décodeur et une "matrice" de portes logiques. Le décodeur prend en entrée l'opcode et a une sortie pour chaque instruction possible, ce qui fait qu'on l'appelle le '''décodeur d'instruction'''. La matrice de portes prend en entrée les sorties du décodeur et le compteur, et sort les signaux de commande adéquats. Pour chaque instruction et chaque valeur de compteur, elle sort les signaux de commande correspondant à la micro-opération adéquate. Un exemple est illustré ci-dessous. L'exemple est celui de l'exécution d'une instruction qui charge une donnée dans le registre dit accumulateur d'un processeur à accumulateur (qui n'a qu'un seul registre, le dit accumulateur). Le tout se fait en 6 cycles, dont 4 servent à gérer le chargement de l'instruction et le ''program counter''. * Le premier cycle copie le ''program counter'' dans le registre d’interfaçage pour les adresses. * Le second cycle lance une lecture, la donnée lue est sur le bus de données à la fin du cycle. * Le troisième copie l'instruction lue dans le registre d’interfaçage pour les données et dans le registre d'instruction, et incrémente le ''program counter'' en parallèle. * Le quatrième copie l'adresse à lire dans le registre d’interfaçage d'adresse. * Le cinquième lit la donnée à lire depuis la mémoire. * Le sixième copie la donnée lue du registre d’interfaçage dans l'accumulateur. [[File:Animation of an LDA instruction performed by the control matrix of a simple hardwired control unit.gif|centre|vignette|upright=2.5|Implémentation de la matrice de portes d'un séquenceur câblé. Les sorties du décodeur sont à gauche, le compteur (''one hot'') est en haut, les signaux de commandes sont émis vers le bas.]] Pour résumer, un séquenceur câblé est composé d'un compteur de micro-opération, d'un décodeur d'instruction et d'une matrice de portes logiques. Dans le schéma précédent, vous voyez que l'usage d'un compteur ''one hot'' facilite l'implémentation de la matrice de portes logiques. ===La détermination de la fin d'une instruction=== Notons que le compteur interne au séquenceur est aussi utilisé pour déterminer quand une instruction se termine. Quand une instruction se termine, le processeur doit faire deux choses : réinitialiser le compteur du séquenceur, et surtout : incrémenter le ''program counter'' pour passer à l'instruction suivante. Pour cela, on ajoute un circuit combinatoire qui détermine si l'instruction en cours est terminée. Une instruction se termine quand la dernière micro-opération est atteinte, à savoir qu'une instruction qui se termine à la énième micro-opération se termine quand le compteur atteint N. Par exemple, pour une instruction de multiplication de 6 cycles d'horloge, le décodeur sait que l'instruction est terminée le compteur atteint 5 (signe qu'il en est à sa sixième micro-opération, soit la dernière). Le circuit combinatoire qui détermine si l'instruction est terminée est donc trivial : il associe une table qui attribue pour chaque opcode le numéro de la dernière micro-opération, et un comparateur qui vérifier si le compteur a atteint cette valeur. Une manière de faire plus simple est d'utiliser un décompteur, qui est décrémenté à chaque micro-opération exécutée, et de l'initialiser avec le nombre de micro-opérations de l'instruction exécutée. L’instruction est alors terminée quand le compteur atteint zéro. Ce faisant, le circuit qui détecte la fin d'une instruction est terriblement simple, sans compter qu'il gère naturellement le cas où les instructions n'ont qu'une seule micro-opération. Mais cela n'élimine pas le circuit qui détermine le nombre de cycles d'une instruction, car celui-ci sert pour initialiser le compteur. Cette solution n'est pas toujours utilisée, pour des raisons assez diverses, notamment le fait qu'elle se marie assez mal avec diverses techniques d'optimisation. Les deux techniques précédentes fonctionnent bien à condition qu'une instruction machine corresponde toujours à la même séquence de micro-opérations. Mais ce n'est pas toujours le cas et la séquence exacte peut différer selon l'état du processeur. Le cas classique est celui des accès mémoires, où le processeur doit attendre que la donnée demandée soit lue ou écrite. Comme autre exemple, certaines étapes/micro-opérations peuvent être facultatives et ne s’exécuter que sous certaines conditions. Pensez par exemple au cas des instructions à prédicats ou des branchements. Mais on peut avoir la même chose avec des instructions de multiplication ou de division, pour lesquelles le calcul peut être plus rapide avec certains opérandes. Dans ce cas, le compteur doit pouvoir sauter certaines micro-opérations et passer par exemple de la deuxième micro-opération à la dixième directement. Et cela demande d'ajouter quelques circuits combinatoires pour cela. Par exemple, le décodeur peut incorporer une sortie pour préciser le numéro de la micro-opération suivante, ce numéro servant à réinitialiser le registre du compteur. Le séquenceur prend en entrée le compteur, l'opcode de l'instruction, éventuellement d'autres entrées, et fournit en sortie : les signaux de commande, et le prochain état du compteur. Ou alors, le décodeur d'instruction dit de combien il faut sauter de micro-opération, de combien il faut augmenter le compteur. ==Les séquenceurs microcodés== Pour limiter la complexité du séquenceur, les concepteurs de processeurs ont inventé les '''''séquenceurs microcodés'''''. L'idée derrière ces séquenceurs microcodés est que, pour chaque instruction, la suite de micro-opérations à exécuter est pré-calculée et mémorisée dans une mémoire ROM, au lieu d'être déterminée à l’exécution par un circuit combinatoire. La mémoire ROM qui stocke la suite de micro-opérations équivalente pour chaque instruction microcodée s'appelle le '''''control store''''', tandis que son contenu s'appelle le '''microcode'''. : Par abus de langage, nous parlerons parfois de microcode pour désigner la suite de microinstructions correspondant à une instruction machine. Nous parlerons alors de microcode de l'addition pour désigner la suite de microinstructions correspondant à l'instruction machine de l'addition. Faire cette petite erreur rendra la lecture de cette section beaucoup plus fluide. Les séquenceurs microcodés étaient surtout utilisés sur les architectures CISC, celles avec un jeu d'instruction étoffé et beaucoup de modes d'adressages différents. Leur grand nombre d'instructions favorisait un microcode. De plus, le budget en transistor de ces processeur était assez limité, ce qui fait que ces opérations aujourd'hui banales n'avaient pas leur propre circuit et étaient émulées en microcode. Les premiers microprocesseurs 16 bits utilisaient souvent le microcode pour implémenter des instructions comme la multiplication et la division. Un exemple est le 8086 d'Intel, qui n'avait pas de circuit multiplieur/diviseur. A la place, il émulait la multiplication avec une série d'additions et de décalages, et la division avec des soustractions/décalages. Les processeurs de ce type utilisaient un microcode pour beaucoup d'instructions, pas seulement la multiplication et la division. En conséquence, ajouter des instructions dans un microcode "existant" coutait moins cher que d'ajouter un circuit multiplieur. Un autre exemple d'utilisation du microcode est celui des premiers processeurs capables d'effectuer des calculs flottants. Sur les premiers processeurs de ce type, il n'y avait pas de FPU, pas de circuits pour les calculs flottants. Les instructions flottantes étaient en réalité émulées par des calculs entiers : chaque instruction flottante était convertie en interne en une suite d'instructions entières qui émulaient l'instruction voulue. Pour cela, les instructions flottantes étaient microcodées. De nos jours, les processeurs contiennent des circuits de calcul flottant, ce qui fait que les instructions ne sont plus émulées sauf pour quelques-unes. Les séquenceurs micro-codés sont plus simples à concevoir et simplifient beaucoup le travail des concepteurs de processeurs. L'usage du microcode permet aussi d'ajouter des instructions facilement, en modifiant le microcode, sans pour autant modifier en profondeur le processeur. En contrepartie, un séquenceur microcodé utilise plus de portes logiques, vu qu'une ROM est un circuit gourmand en portes logique. En théorie, les instructions microcodées peuvent être plus rapides que leur équivalent logiciel, à savoir une instruction émulée par une suite d'instructions machines. Le microcode peut être optimisé de manière à mieux utiliser les ressources internes au processeur. Mais force est de constater que ces opportunités d’optimisation étaient rares dans la réalité. Mais cela n'était pas l'intérêt principal, car les architectures CISC qui privilégiaient la taille du programme - la ''code size''. L'usage d'un microcode n’a plus trop d'intérêt de nos jours, et surtout pas sur les architectures RISC qui se contentent d'un séquenceur câblé. ===Le ''control store''=== La caractéristique principale du ''control store'' est sa capacité, qui est souvent assez petite. La capacité du ''control store'' dépend non seulement du nombre de micro-instructions qu'il contient, mais aussi de la taille de ces dernières. Un byte du ''control store'' correspond à une micro-instruction, les exceptions étant très très rares. Et la taille des micro-instructions varie grandement d'un processeur à l'autre. Dans les grandes lignes, la différence principale tient beaucoup la manière dont sont encodées les micro-instructions. Il existe plusieurs sous-types de séquenceurs microcodés, qui se distinguent par la façon dont sont codées les micro-opérations. * Avec le '''microcode horizontal''', chaque instruction du microcode encode directement les signaux de commande à envoyer aux unités de calcul. Vu Le grand nombre de signaux de commande, il n'est pas rare que les micro-opérations d'un microcode horizontal fassent plus d'une centaine de bits ! * Avec un '''microcode vertical''', les instructions du microcode sont traduites en signaux de commande par un séquenceur câblé qui suit le ''control store''. Son avantage est que les micro-opérations sont plus compactes, elles font moins de bits. Cela permet d'utiliser un ''control store'' plus petit ou d'avoir un microcode plus important, au détriment de la complexité du séquenceur. Un exemple de microcode vertical est le microcode du 8086, encore lui ! Pour ce qui est de la commande de l'ALU, le microcode envoie une commande abstraite qui est décodée par un circuit combinatoire (un PLA), pour obtenir les signaux de commande de l'ALU. L'implémentation interne du ''control store'' ne suit pas forcément à la lettre l'organisation en byte. Pour faire comprendre ce que je veux dire, prenons l'exemple de l'Intel 8086, dont le ''control store'' contenait 512 bytes/microinstructions de 21 bits chacune. Le ''control store'' n'était pas une ROM de 512 lignes et de 21 colonnes, comme on pourrait s'y attendre. Les dimensions 512 par 21 donneraient une ROM très allongée, rendant son placement sur la puce de silicium peu pratique. A la place, elle regroupait 4 bytes par ligne, ce qui donnait 84 lignes et 128 colonnes. ===L'optimisation du microcode=== Le ''control store'' a souvent une capacité très faible, même pour une mémoire ROM. Une ROM prend de la place, ce qui fait que les concepteurs de processeurs préfèrent utiliser une ROM assez petite. Néanmoins, malgré la petitesse des ROM de l'époque, il arrivait souvent que le ''control store'' contienne des vides, des bytes inoccupés. Cela arrive si le microcode n'a pas une taille égale à une puissance de deux. Par exemple, si l'on a un microcode qui occupe 120 bytes, on doit utiliser un ''control store'' de 128 bytes, ce qui laisse 8 bytes vides. On pourrait croire que les vides sont placés à la fin du ''control store'', mais il est parfois préférable de disperser les vides dans le ''control store'', afin de simplifier les circuits adossés au microcode, ce que nous allons voir dans ce qui suit. Pour les concepteurs de processeurs, une difficulté majeure est de faire rentrer le microcode dans le ''control store''. C'est encore un problème à l'heure actuelle, mais ce l'était encore plus sur les architectures anciennes, qui devaient faire avec des ROM limitées qu'actuellement. De plus, sur les anciennes architectures CISC, le grand nombre d'instructions recherchait se mariait mal à la petite capacité des mémoires ROM de l'époque. Les concepteurs de processeurs devaient ruser pour faire rentrer un microcode souvent complexe dans une petite ROM. Diverses optimisations étaient possibles. La première optimisation de ce genre consiste à gérer des fonctions/sous-programmes/routines logicielles dans le microcode. Pour cela, les circuits en charge du microcode géraient l’exécution de fonctions dans le microcode, avec des registres pour l'appel de retour, des microinstructions pour faire des branchements dans le microcode et tout ce qui va avec. Mais le tout était généralement simplifié et rares étaient les processeurs qui incorporaient une pile d'appel complète pour le microcode. Beaucoup se limitaient à ajouter un registre pour l'adresse de retour, quelques instructions de branchement interne au microcode, et guère plus. Un exemple assez intéressant est celui du processeur Intel 8086, dont le microcode contient une sous-routine pour gérer chaque mode d'adressage. Sans optimisations, il faudrait un microcode par instruction et par mode d'adressage. Par exemple, le microcode pour une addition en mode d'adressage immédiat n'est pas la même que pour une instruction d'addition en mode d'adressage direct. Cependant, elles partagent un même cœur qui s'occupe de l'addition et de la gestion de l'accumulateur, même si la gestion des opérandes est totalement différente suivant le mode d'adressage. Pour éliminer cette redondance, le microcode du 8086 délègue la gestion des modes d'adressages et des opérandes à des sous-programmes spécialisés, une par mode d'adressage. La seconde optimisation est de réduire la taille des micro-instructions en jouant sur leur encodage. L'usage d'un microcode vertical est une première solution. Décoder certaines instructions simples sans passer par le microcode en est une autre, et elle donne les séquenceurs hybrides dont nous parlerons dans la suite du chapitre. Mais d'autres techniques sont possibles, comme le fait de déporter une partie du décodage en-dehors du ''control store'', dans des circuits logiques séparés. Un bon exemple de cela est celui de l'Intel 8086, encore lui, sur lequel beaucoup d'instructions existaient en deux exemplaires : une version 8 bits et une version 16 bits. Il n'y avait pas de microcode séparé pour les deux versions, mais un seul microcode qui s'occupait autant de la version 8 bits que de la version 16 bits de l'instruction. La différence entre les deux se faisait au niveau du bus interne du processeur. Un bit de l'instruction machine indiquait s'il s'agissait d'une version 8 ou 16 bits et ce bit était transmis à la machinerie du bus interne, sans passer par le microcode. ===Les circuits d’exécution du microcode=== Le processeur doit trouver un moyen de dérouler les micro-instructions les unes après les autres, ce qui est la même chose qu'avec des instructions machines. Le micro-code est donc couplé à un circuit qui de l’exécution des micro-opérations les unes après les autres, dans l'ordre. Ce circuit est l'équivalent du circuit de chargement, mais pour les micro-opérations. Pour cela, il y a deux méthodes, que voici. La première méthode fait que chaque micro-instruction contient l'adresse de la micro-instruction suivante. Avec cette méthode, on peut disperser une suite de microinstructions dans le ''control store'', au lieu de garder des microinstructions consécutives. L'utilité de cette méthode n'est pas évidente, mais elle deviendra plus claire dans la section suivante. [[File:Microcode sans microséquenceur.gif|centre|vignette|upright=1.5|Microcode sans microséquenceur.]] La seconde méthode fait que le séquenceur contient un équivalent du ''program counter'' pour le microcode. On trouve ainsi un '''micro-séquenceur''' qui regroupe un '''registre d’adresse de micro-opération''' et un '''micro-compteur ordinal'''. Le registre d’adresse de micro-opération est initialisé avec l'opcode de l'instruction à exécuter, qui pointe vers la première micro-instruction. Le micro-compteur ordinal se charge d'incrémenter ce registre à chaque fois qu'une micro-instruction est exécutée, afin de pointer sur la suivante. [[File:Microcode avec un microséquenceur.gif|centre|vignette|upright=2|Microcode avec un microséquenceur.]] Un séquenceur microcodé peut même gérer des micro-instructions de branchement, qui précisent la prochaine micro-instruction à exécuter. Grâce à cela, on peut faire des boucles de micro-opérations, par exemple. Pour gérer les micro-branchements, il faut rajouter la destination d'un éventuel branchement dans les micro-instructions de branchement. La taille des micro-instructions augmente alors, vu que toutes les micro-opérations ont la même taille. Voici ce que cela donne pour les microcodes avec un microcompteur ordinal. On voit que l'ajout des branchements modifie le microcompteur ordinal de façon à permettre les branchements entre micro-opérations, d'une manière identique à celle vue pour l'unité de chargement. [[File:Branchements avec microcode horizontal avec microséquenceur.gif|centre|vignette|upright=2|Branchements avec microcode horizontal avec microséquenceur.]] Voici ce que cela donne pour les microcodes où chaque micro-instruction contient l'adresse de la suivante : [[File:Branchements avec microcode horizontal sans microséquenceur.gif|centre|vignette|upright=2|Branchements avec microcode horizontal sans microséquenceur.]] Il est possible de créer des fonctions/sous-programmes/sous-routines dans le microcode, grâce à ces micro-branchements et en ajoutant un registre pour gérer l'adresse de retour. ===Localiser la première microinstruction à exécuter dans le ''control store''=== Un premier problème à résoudre avec un microcode, est de localiser la suite de micro-instructions à exécuter. Si l'on veut exécuter une instruction machine, le microcode doit trouver le début de la suite de microinstruction dans le microcode et démarrer l’exécution des microinstructions à partir de là. Pour le dire autrement, le séquenceur doit déterminer, à partir de l'opcode, quelle est l'adresse de départ dans le ''control store''. Pour cela, il y a plusieurs solutions. La première solution fait une traduction de l'opcode vers l'adresse de départ, en utilisant un circuit combinatoire et/ou une mémoire ROM. Elle a l'inconvénient de complexifier le processeur, dans le sens où on doit ajouter des circuits en plus. De plus, le circuit ou la ROM ajoutés mettent un certain temps avant de donner leur résultat, ce qui ralentit quelque peu le décodage des instructions. L'avantage principal est que l'on peut utiliser facilement un microséquenceur basique et placer les microinstructions les unes à la suite des autres dans le ''control store''. Cette technique s'utilise aussi bien avec un micro-séquenceur que sans. Dans les faits, elle s'utilise de préférence avec un micro-compteur ordinal. L'usage de ce dernier réduit fortement la taille du ''control store'', ce qui compense le fait de devoir ajouter des circuits pour faire la traduction opcode -> adresse. [[File:Control store adressé par predecodage de l'opcode.png|centre|vignette|upright=2|Control store adressé par predecodage de l'opcode]] L'autre solution considère l'opcode de l'instruction microcodée comme une adresse : le ''control store'' est conçu pour que cette adresse pointe directement sur le début de la suite de micro-opérations correspondante, la première micro-instruction de cette suite. Du moins, c'est le principe général, mais un détail vient mettre son grain de sel : un ''control store'' utilise systématiquement des adresses plus grandes que l'opcode. Ce qui fait qu'il faut rajouter des bits à l'opcode pour obtenir l'adresse, on doit concaténer des zéros à l'opcode pour obtenir l'adresse finale. On fait alors face à deux choix : soit on met l'opcode dans les bits de poids faible de l'adresse, soit on la place dans les bits de poids fort. Les deux solutions ont des avantages et inconvénients différents. [[File:Control store.gif|centre|vignette|upright=2|Control store d'un microcode horizontal.]] La première méthode place les opcodes dans les bits de poids faible et les zéros dans les bits de poids fort. Le défaut principal de cette méthode vient du fait que de nombreux opcodes ont des représentations binaires proches, ce qui fait que leurs adresses de départs sont proches dans le ''control store''. Il n'y a alors pas assez d'espace entre les deux adresses de départ pour y placer une suite de microninstructions. En clair, cette méthode ne peut pas s'utiliser avec un micro-séquenceur. Par contre, elle se marie très bien avec un ''control store'' où chaque microinstruction contient l'adresse de la suivante. En faisant cela, l'opcode pointe vers l'adresse de départ, mais le reste de la suite de microinstructions est placé ailleurs dans le ''control store'', dans des adresses qui ne correspondent pas à des opcodes. Les adresses de départ occupent donc le bas de la ROM du ''control store'', alors que le haut de la ROM contient les suites de microinstructions et éventuellement des vides. [[File:Control store adressé par l'opcode - opcode sur bits de poids faible.png|centre|vignette|upright=2|Control store adressé par l'opcode - opcode sur bits de poids faible]] La seconde méthode met l'opcode dans les bits de poids fort de l'adresse et les zéros dans les bits de poids faible. En faisant cela, les adresses de départ sont dispersées dans le ''control store'', elles sont séparées par des intervalles de taille de fixe. Cela garantit qu'il y a un espace fixe entre deux adresses de départ, dans lequel on peut placer une suite de microinstructions. Un bon exemple est celui du 8086, dont le microcode, très complexe, espace chaque instruction/opcode tous les 16 bytes, ce qui permet d'avoir 16 microinstructions par instruction machine. Son ''control store'' contenait 512 micro-instructions, 512 bytes, ce qui donne des adresses de 13 bits. Mais l'opcode occupait les 9 bits de poids fort de l'adresse de microcode, ce qui laissait 4 bits de poids faible libres. En conséquence, chaque instruction machine disposait de maximum 16 microinstructions consécutives. L'avantage de cette méthode est que l'on peut utiliser un microséquenceur plus petit, avec un incrémenteur de plus petite taille. De plus, les adresses utilisées pour les branchements dans le microcode sont plus petites. Par exemple, le microcode du 8086, qui espacait ses microinstructions toutes les 16 bytes, avait un microséquenceur de 4 bits. Ce dernier contenait un incrémenteur de micro-''program counter'' de 4 bits et non 13. De plus, les adresses utilisées pour les branchements dans le microcode ne faisaient que 4 bits, à savoir qu'il s'agissait de branchements relatifs. Tout cela rendait le microséquenceur beaucoup plus économe en circuits. Cette solution a cependant pour défaut de laisser beaucoup de vides dans le ''control store''. Le microcode de certaines instructions était assez court, d'autres avaient un microcode plus long. L'espace entre deux opcodes, entre deux adresses de départ, est fixe et se cale sur le microcode le plus long. En conséquence, le microcode de certaines instructions laisse des vides à sa suite. Si on sépare les adresses de départ par un espace assez court, alors les suites d'instructions trop longues ne rentrent pas, sauf en trichant. Par tricher, on veut dire que le microcode de ces instruction est découpé en morceaux et dispersé dans les vides du ''control store''. L’exécution d'un microcode dispersé ainsi se fait normalement grâce aux microinstructions de branchement. [[File:Control store adressé par l'opcode - opcode sur bits de poids fort 01.png|centre|vignette|upright=2|Control store adressé par l'opcode - opcode sur bits de poids fort]] Pour comparer les trois méthodes, on peut comparer ce qu'il en est pour le remplissage du ''control store''. Les deux premières méthodes remplissent le ''control store'' au mieux, alors que la dernière laisse des vides et disperse les suites de microinstructions dans le ''control store''. Par contre, il faut aussi tenir compte d'autres paramètres. La première solution demande d'ajouter des circuits de traduction opcode -> adresse qui prennent de la place, pas les deux dernières solutions. Enfin, la deuxième solution impose de rallonger les bytes du ''control store'', car on se prive de micro-séquenceur, ce qui n'est pas le cas des deux autres. Au final, comparer les trois solutions ne donne pas de gagnant absolu : tout dépend de l'implémentation du jeu d'instruction choisit, de son encodage, etc. ===La mise à jour du microcode=== Parfois, le processeur permet une mise à jour du ''control store'', ce qui permet de modifier le microcode pour corriger des bugs ou ajouter des instructions. L'utilisation principale est de corriger des bugs ou des problèmes de sécurité assez tordus. Il est fréquent que les processeurs aient des bugs matériels, présents à cause de défauts de conception parfois subtils. Les grands fabricants comme Intel et AMD documentent ces bugs dans leur documentation officielle. Une petite partie de ces bugs peuvent se corriger avec une mise à jour du microcode, et ils ne sont pas forcément dans le microcode lui-même. Un exemple serait la désactivation des instructions TSX sur les processeurs x86 Haswell, en 2014, qui ont été désactivées par une mise à jour du microcode, après qu'un bug de sécurité ait été découvert. La mise à jour du microcode est rarement permanente. Une mise à jour permanente du microcode implique que le ''control store'' est une EEPROM ou une mémoire ROM reprogrammable, donc des mémoire très difficiles à mettre en œuvre dans les processeurs. Or, le ''control store'' doit être une mémoire extrêmement performante, capable de fonctionner à très haute fréquence, avec des temps d'accès minuscules, aux performances proches d'une SRAM. En réalité, le ''control store'' est mis à jour temporairement, et est réinitialisé à chaque boot de l'ordinateur, à chaque boot du processeur. Pour cela, le ''control store'' est implémenté avec deux mémoires : une ROM qui contient le microcode originel, et une SRAM. Pour simplifier les explications, nous allons appeler ces deux mémoires la micro-ROM et la micro-RAM. Au démarrage de l'ordinateur, le microcode contenu dans la micro-ROM est copié dans la micro-RAM. Peu après l'allumage du processeur, le contenu de la micro-RAM peut être remplacé par un microcode mis à jours. Typiquement, le microcode corrigé est fourni soit par le BIOS, soit par le système d'exploitation. Les mises à jour de microcode sont généralement soumises à des mesures de sécurité drastiques intégrées au processeur. Par exemple, le microcode fournit par le fabricant est chiffré avec des clés connues seulement des fabricants de CPU, autres), et un microcode n'est chargé par le processeur que si la clé correspond. ===Les microcodes réinscriptibles=== Il existe des processeurs dont le microcode est directement reprogrammable, accessible par le programmeur. Le programmeur peut écrire ce qu'il veut dans la micro-RAM, à sa guise. On peut ainsi changer le jeu d'instruction du processeur au besoin, afin d'ajouter des instructions utiles. Il s'agit de processeurs destinés à l'embarqué, qui doivent être conçus sur mesure, on ne trouve pas de systèmes de ce genre dans les PCs. Ils sont appelés des '''processeurs à microcode réinscriptible'''. L'utilité est que les programmes peuvent disposer des instructions les plus adéquates pour leur fonction, ce qui réduit la taille du code (la mémoire prise par le programme exécutable) et facilite la programmation en assembleur. Ces deux avantages n'ont pas grand intérêt de nos jours. De plus, l'utilisation de cette technique demande un ''control store'' assez imposant, de grande taille, rarement rapide. Par contre, cette fonctionnalité a de nombreux défauts. Si chaque programme peut changer à la volée le jeu d'instruction du processeur, cela peut mettre le bazar. Si un programme change le microcode, les programmes qui passent après lui doivent réinitialiser le microcode pour ne pas exécuter des instructions incorrectes. Cela peut aussi poser des problèmes de sécurité, les hackers étant doués pour utiliser ce genre de fonctionnalités à des fins malveillantes. Cependant, les processeurs de ce type sont utilisés pour l'embarqué, sur des systèmes où un seul programme s'exécute, sans accès à un réseau local, il n'y a donc pas de problèmes liés à des programmes malveillants ou des interactions entre programmes. Un processeur de ce type est le GP1000 d'Imsys, qui a été décrit entres autres dans l'article "GP1000 Has Rewritable Microcode" du magazine ''Microprocessor report''. Il disposait d'une micro-ROM de 36 kibioctets, couplées à une micro-RAM de 18 kibioctets. Une partie du microcode était donc réinscriptible : un tiers l'était, les deux autres tiers étaient dans une micro-ROM impossible à modifier. Il y avait donc un microcode permanent en micro-ROM et un microcode variable en en micro-RAM. Le microcode permanent contient un ''chargeur de microcode'', qui recopie le microcode variable dans la micro-RAM, à l'allumage de l'ordinateur. Par la suite, le microcode variable est chargé avec une instruction machine dédiée. Un point très original est que le GP1000 supporte l'exécution de plusieurs programmes à tour de rôle. Le microcode permanent contient de quoi gérer des interruptions, émuler des périphériques virtuels, mais aussi gérer la présence de plusieurs programmes en cours d'exécution. Les programmes s'exécutent à tour de rôle et le microcode décide qui s’exécute et pour combien de temps. Le microcode permanent contient donc une sorte de mini-système d'exploitation rudimentaire ! Au-delà ce ça, Imsys fournissait des microcodes près à l'emploi, qui pouvaient être chargés à la demande. L'un d'entre eux permettait d'implémenter la machine virtuelle Java directement dans le processeur. Pour information, la machine virtuelle Java standardise le langage machine d'un processeur fictif, précisément une machine à pile simple, qui peut en théorie être implémentée en matériel. C'est exactement ce que faisait l'un des microcodes près à l'emploi d'Imsys. D'autres exemples de processeurs à microcode réinscriptible sont les Burroughs B1700, ainsi que le LSI-11 de l'entreprise Digital. Pour ce dernier, son ''control store'' est décrit dans l'article "TheLSI-11/23 Control Store Microarchitecture", qui n'est malheureusement pas accesible gratuitement. Pour le Burroughs B1700, on en parlera dans un futur chapitre, car ce processeur avait des tas d'optimisations liées à son microcode réinscriptible. Par exemple, il avait des optimisations pour rendre son microcode automodifiant ! Oui, vous avez bien lu : le microcode pouvait se modifier lui-même pour gérer des modes d'adressage complexe. Et il avait des optimisations de son registre de micro-opération pour cela ! Mais nous verrons cela dans le chapitre sur "Les ISA optimisés pour la compilation/interprétation". ==Les séquenceurs hybrides== Les séquenceurs hybrides sont un compromis entre séquenceurs câblés et microcodés. Ils permettent de profiter des avantages et inconvénients des deux types de séquenceurs. Sur le principe, une partie des instructions est décodée par une partie câblée, et l'autre passe par le microcode. Typiquement, de tels séquenceurs sont très fréquents sur les architectures CISC, où ils permettent un décodage rapide pour les instructions simples, alors que les instructions complexes le sont par le microcode, plus lent. L'organisation interne d'un séquenceur hybride varie grandement selon le processeur et le jeu d'instruction. Dans le cas le plus simple, on a un séquenceur câblé secondé par un séquenceur microcodé, les deux étant précédés par un '''circuit de prédécodage'''. Le circuit de prédécodage reçoit les instructions et les redirige soit vers le séquenceur câblé, soit vers le séquenceur microcodé. Les instructions les plus simples sont dirigées vers le séquenceur câblé, alors que les instructions complexes vont vers le microcode (généralement les instructions avec des modes d'adressage exotiques). Une solution intéressante est de décoder les instructions qui prennent un seul cycle dans un séquenceur câblé, alors que les instructions multicycles sont décodées par un séquenceur microcodé séparé. Mais dans le cas général, la séparation en deux séquenceurs n'est pas évidente et on trouve un ''control store'' entouré de circuits câblés, avec certaines instructions qui n'ont pas besoin du microcode pour être décodées, d'autres qui passent par le microcode, d'autres qui sont décodé partiellement par microcode et partiellement par des circuits câblés. Notons que le microcode vertical n'est pas un séquenceur hybride, car toutes les instructions passent par le microcode. Par contre, un séquenceur hybride peut utiliser un microcode vertical, ce qui rend le séquenceur assez compliqué. Sur les processeurs x86 modernes, on trouve plusieurs séquenceurs : plusieurs décodeurs câblés spécialisés, et un microcode séparé. ===Le séquenceur hybride du 8086=== Un bon exemple de séquenceur de ce type est celui du processeur x86 8086 d'Intel, ainsi que ceux qui ont suivi. Le jeu d'instruction x86 est tellement complexe qu'il utilise un séquenceur hybride. Le séquenceur de l'Intel 8086 est organisé comme suit : un ''control store'' de 512 microinstructions (512 bytes) couplé à de nombreux circuit câblés, et une ''Group Decode ROM'' qui décide pour chaque instruction si elle est décodée par le séquenceur câblé ou le microcodé. La mal-nommée ''Group Decode ROM'' est en réalité un petit circuit combinatoire un peu particulier (basé sur un PAL, composant proche d'une ROM), qui commande le séquenceur proprement dit. Il fournit 15 signaux qui configurent le séquenceur et disent si le décodage doit utiliser ou non le microcode. Il permet aussi de configurer le microcode pour gérer les différents modes d'adressage, ou encore de configurer les circuits câblés en aval du microcode. Sur ce processeur, les instructions qui s’exécutent en un seul cycle d'horloge sont décodés sans utiliser le microcode. Le 8086 utilise une sorte de micro-code vertical pour commander l'ALU. Entre l'ALU et le microcode, on trouve un mini-décodeur, qui décode l'opération envoyée par le microcode en signaux de commande. C'est un simple circuit combinatoire (un PLA pour être précis). La raison est que l'ALU du 8086 est quelque peu complexe. Nous l'avions vu dans le chapitre sur les unités de calcul, l'ALU du 8086 est basée sur un additionneur à propagation de retenue, où deux portes logiques sont remplacées par une porte logique universelle. Il faut commander ces deux portes universelles pour obtenir l'opération voulue, ce qui demande pas mal de signaux de commande. Et pour économiser de la place dans le microcode, l'opération à faire est encodée sur plusieurs bits, qui sont décodés pour générer les signaux de commande. Ce qui vient d'être dit est une simplification. En vérité, certaines instructions ne sont pas encodées dans le microcode. Pour ces instructions, l'opération est récupérée directement dans l'opcode de l'instruction. C'est le cas des opérations d'addition, soustraction, les opérations logiques, les décalages et autres opérations gérées naturellement par l'ALU. Pour elles, l'opération à faire est extraite de l'opcode, pas du microcode. Pour ces opérations, le microcode encode l'opération à exécuter sur l'ALU par une micro-instruction généraliste nommée XI. La micro-opération XI indique qu'il faut activer le multiplexeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'unité de chargement et le program counter | prevText=L'unité de chargement et le program counter | next=L'implémentation matérielle des branchements | nextText=L'implémentation matérielle des branchements }} </noinclude> 9p3jqn7kq763vvbg1h1ymxpy3064y44 764061 764021 2026-04-19T20:46:04Z Mewtow 31375 /* Les microcodes réinscriptibles */ 764061 wikitext text/x-wiki Pour rappel, les instructions se font en plusieurs étapes, appelées micro-opérations. Pour chaque instruction, il faut déduire quelles sont les micro-opérations à exécuter et dans quel ordre. Mais l'instruction chargée depuis la mémoire ne précise pas les micro-opérations à faire, elle se contente juste de dire quelle opération effectuer et sur quels opérandes. Le processeur doit donc traduire l'instruction en une séquence de micro-opérations, en une séquence de signaux de commandes adéquats. C'est le rôle de l''''unité de décodage d'instruction''', une portion du processeur qui « décode » l'instruction. [[File:Unité de décodage d'instruction.png|centre|vignette|upright=2|Unité de décodage d'instruction]] Une micro-opération configure le chemin de donnée d'une manière bien précise, afin de faire une opération de base : copie entre registres, accès mémoire, opération sur l'ALU. Pour cela, il faut configurer l'ALU pour qu'elle fasse l'opération adéquate, configurer le banc de registre pour lire /écrire les bons registres, etc. La micro-opération envoie des '''signaux de commande''' adéquats au chemin de données. Pour simplifier, une micro-opération est encodée en concaténant les signaux de commande pour l'ALU, ceux pour les registres, pour l'unité mémoire, etc. Chaque micro-opération encode les signaux de commande à destination du chemin de données. {|class="wikitable" |- ! colspan="4" | Micro-opération, encodage en binaire |- | Signaux de commande pour l'ALU | Signaux de commande pour les registres | Signaux de commande pour l'unité d'accès mémoire | Autres signaux de commande |} Il existe des processeurs assez rares où chaque instruction machine est une micro-opération. Son encodage précise directement les signaux de commande, pas besoin d'une unité de décodage d'instruction. De telles architectures sont appelées des ''architectures actionnées par déplacement''. Elles feront l'objet d'un chapitre dédié, nous allons les mettre de côté pour le moment et nous concentrer sur des architectures plus courantes. ==Les séquenceurs câblés et microcodés== Pour un même jeu d'instruction, des processeurs de marque différente peuvent avoir des séquenceurs différents. Les différences entre séquenceurs sont nombreuses, une partie étant liée à des optimisations plus ou moins sophistiquées du décodage. Mais l'une d'entre elle permet de distinguer deux types purs de séquenceurs, sur un critère assez pertinent. La distinction se fait sur la nature du séquenceur, sur le circuit de décodage utilisé. Le séquenceur est un circuit séquentiel, c’est-à-dire qu'il contient un circuit combinatoire et des registres. Or, nous avons vu dans les chapitres précédents que tout circuit combinatoire peut être remplacé ainsi par une ROM avec le contenu adéquat. Et le circuit combinatoire dans le séquenceur ne fait pas exception à cette règle. Le circuit combinatoire peut être implémenté de trois grandes manières différentes. * La première méthode est d'utiliser un circuit combinatoire proprement dit, construit avec des portes logiques, en utilisant les méthodes du chapitre sur les portes logiques. * La seconde remplace ce circuit par une mémoire ROM dans laquelle on écrit la table de vérité du circuit. * La troisième solution est une solution intermédiaire qui utilise un circuit dit PLA (''Programmable Logic Array''). Il y a donc un choix à faire : est-ce le séquenceur incorpore un circuit combinatoire ou une mémoire ROM ? Cela permet de distinguer les séquenceurs câblés, basés sur un circuit combinatoire/séquentiel, et les séquenceurs microcodés, basés sur une mémoire ROM. Les deux ont évidemment des avantages et des inconvénients différents, comme nous allons le voir. ==Les séquenceurs câblés== Si les instructions sont décodées par un assemblage de portes logiques et de registres, on parle de '''séquenceur câblé'''. Plus le nombre d'instructions est important, plus un séquenceur câblé est compliqué à concevoir par rapport à ses alternatives. La complexité du séquenceur dépend aussi de la complexité des instructions machine. Autant dire que les processeurs CISC n'utilisent pas trop ce genre de séquenceurs et préfèrent utiliser des séquenceurs microcodés ou hybrides, alors que les séquenceurs câblés sont préférés sur les processeurs RISC. ===L'implémentation du séquenceur=== Sur certains processeurs assez rares, toute instruction s’exécute en une seule micro-opération, ce qui fait que le séquenceur se résume alors à un simple circuit combinatoire. C'est très rare, car cela implique que toutes les instructions doivent se faire en moins d'un cycle d'horloge. Pour cela, la durée d'un cycle d'horloge doit se caler sur l'instruction la plus lente : un accès mémoire prendra autant de temps qu'une addition, ou qu'une multiplication, etc. Ensuite, il faut que le processeur soit une architecture Harvard, afin de charge l'instruction tout en accédant aux données en parallèle, le tout en un seul cycle d'horloge processeur. [[File:Séquenceur combinatoire 01.png|centre|vignette|upright=2.5|Séquenceur combinatoire]] Sur les autres processeurs, il y a des instructions qui demandent d’exécuter une suite de micro-opérations. Pour cela, le séquenceur devient un circuit séquentiel, qui intègre un registre/compteur. La présence de ce registre s’explique par le fait que le séquenceur a besoin de savoir à quelle micro-opération il en est, information qui est mémorisée dans un registre. [[File:Séquenceur séquentiel.png|centre|vignette|upright=2|Séquenceur séquentiel]] Dans le cas le plus simple, le séquenceur est basé sur un simple compteur couplé à un circuit combinatoire. Le compteur mémorise à quelle micro-opération il en est, en lui attribuant un numéro : s'il en est à la première, seconde, troisième micro-opération, etc. Le compteur est incrémenté à chaque micro-opération réussie (les accès mémoires peuvent prendre plusieurs cycles pour une seule micro-opération, si le CPU doit attendre la RAM). Il est réinitialisé quand l'instruction se termine, à savoir quand le compteur a atteint le nombre de micro-opérations adéquat pour exécuter l'instruction. Le compteur n'est pas forcément un compteur normal, qui stocke une valeur en binaire. Il s'agit souvent d'un compteur basé un registre à décalage, appelé un '''compteur ''one-hot''''', ou encore un compteur en anneau. La raison est que les compteurs en anneau sont très rapides et utilisent peu de circuits, sans compter qu'ils permettent de se passer de comparateur pour déterminer la valeur du compteur. Leur seul défaut est que les économies en portes logiques sont contrebalancées par un plus grand nombre de bascules, qui est cependant acceptable si le compteur encode peu de valeurs. Si on veut un séquenceur qui fonctionne rapidement, en moins d'un cycle d'horloge, c'est la meilleure solution qui soit. En combinant le compteur avec l'opcode, le séquenceur détermine quel est la micro-opération à effectuer. Pour être plus précis, un circuit combinatoire intégré au séquenceur prend en entrée le compteur et l'opcode de l'instruction machine, puis fournit en sortie la micro-opération adéquate. Dans son implémentation la plus simple, ce circuit combinatoire est composé de deux sous-circuits : un décodeur et une "matrice" de portes logiques. Le décodeur prend en entrée l'opcode et a une sortie pour chaque instruction possible, ce qui fait qu'on l'appelle le '''décodeur d'instruction'''. La matrice de portes prend en entrée les sorties du décodeur et le compteur, et sort les signaux de commande adéquats. Pour chaque instruction et chaque valeur de compteur, elle sort les signaux de commande correspondant à la micro-opération adéquate. Un exemple est illustré ci-dessous. L'exemple est celui de l'exécution d'une instruction qui charge une donnée dans le registre dit accumulateur d'un processeur à accumulateur (qui n'a qu'un seul registre, le dit accumulateur). Le tout se fait en 6 cycles, dont 4 servent à gérer le chargement de l'instruction et le ''program counter''. * Le premier cycle copie le ''program counter'' dans le registre d’interfaçage pour les adresses. * Le second cycle lance une lecture, la donnée lue est sur le bus de données à la fin du cycle. * Le troisième copie l'instruction lue dans le registre d’interfaçage pour les données et dans le registre d'instruction, et incrémente le ''program counter'' en parallèle. * Le quatrième copie l'adresse à lire dans le registre d’interfaçage d'adresse. * Le cinquième lit la donnée à lire depuis la mémoire. * Le sixième copie la donnée lue du registre d’interfaçage dans l'accumulateur. [[File:Animation of an LDA instruction performed by the control matrix of a simple hardwired control unit.gif|centre|vignette|upright=2.5|Implémentation de la matrice de portes d'un séquenceur câblé. Les sorties du décodeur sont à gauche, le compteur (''one hot'') est en haut, les signaux de commandes sont émis vers le bas.]] Pour résumer, un séquenceur câblé est composé d'un compteur de micro-opération, d'un décodeur d'instruction et d'une matrice de portes logiques. Dans le schéma précédent, vous voyez que l'usage d'un compteur ''one hot'' facilite l'implémentation de la matrice de portes logiques. ===La détermination de la fin d'une instruction=== Notons que le compteur interne au séquenceur est aussi utilisé pour déterminer quand une instruction se termine. Quand une instruction se termine, le processeur doit faire deux choses : réinitialiser le compteur du séquenceur, et surtout : incrémenter le ''program counter'' pour passer à l'instruction suivante. Pour cela, on ajoute un circuit combinatoire qui détermine si l'instruction en cours est terminée. Une instruction se termine quand la dernière micro-opération est atteinte, à savoir qu'une instruction qui se termine à la énième micro-opération se termine quand le compteur atteint N. Par exemple, pour une instruction de multiplication de 6 cycles d'horloge, le décodeur sait que l'instruction est terminée le compteur atteint 5 (signe qu'il en est à sa sixième micro-opération, soit la dernière). Le circuit combinatoire qui détermine si l'instruction est terminée est donc trivial : il associe une table qui attribue pour chaque opcode le numéro de la dernière micro-opération, et un comparateur qui vérifier si le compteur a atteint cette valeur. Une manière de faire plus simple est d'utiliser un décompteur, qui est décrémenté à chaque micro-opération exécutée, et de l'initialiser avec le nombre de micro-opérations de l'instruction exécutée. L’instruction est alors terminée quand le compteur atteint zéro. Ce faisant, le circuit qui détecte la fin d'une instruction est terriblement simple, sans compter qu'il gère naturellement le cas où les instructions n'ont qu'une seule micro-opération. Mais cela n'élimine pas le circuit qui détermine le nombre de cycles d'une instruction, car celui-ci sert pour initialiser le compteur. Cette solution n'est pas toujours utilisée, pour des raisons assez diverses, notamment le fait qu'elle se marie assez mal avec diverses techniques d'optimisation. Les deux techniques précédentes fonctionnent bien à condition qu'une instruction machine corresponde toujours à la même séquence de micro-opérations. Mais ce n'est pas toujours le cas et la séquence exacte peut différer selon l'état du processeur. Le cas classique est celui des accès mémoires, où le processeur doit attendre que la donnée demandée soit lue ou écrite. Comme autre exemple, certaines étapes/micro-opérations peuvent être facultatives et ne s’exécuter que sous certaines conditions. Pensez par exemple au cas des instructions à prédicats ou des branchements. Mais on peut avoir la même chose avec des instructions de multiplication ou de division, pour lesquelles le calcul peut être plus rapide avec certains opérandes. Dans ce cas, le compteur doit pouvoir sauter certaines micro-opérations et passer par exemple de la deuxième micro-opération à la dixième directement. Et cela demande d'ajouter quelques circuits combinatoires pour cela. Par exemple, le décodeur peut incorporer une sortie pour préciser le numéro de la micro-opération suivante, ce numéro servant à réinitialiser le registre du compteur. Le séquenceur prend en entrée le compteur, l'opcode de l'instruction, éventuellement d'autres entrées, et fournit en sortie : les signaux de commande, et le prochain état du compteur. Ou alors, le décodeur d'instruction dit de combien il faut sauter de micro-opération, de combien il faut augmenter le compteur. ==Les séquenceurs microcodés== Pour limiter la complexité du séquenceur, les concepteurs de processeurs ont inventé les '''''séquenceurs microcodés'''''. L'idée derrière ces séquenceurs microcodés est que, pour chaque instruction, la suite de micro-opérations à exécuter est pré-calculée et mémorisée dans une mémoire ROM, au lieu d'être déterminée à l’exécution par un circuit combinatoire. La mémoire ROM qui stocke la suite de micro-opérations équivalente pour chaque instruction microcodée s'appelle le '''''control store''''', tandis que son contenu s'appelle le '''microcode'''. : Par abus de langage, nous parlerons parfois de microcode pour désigner la suite de microinstructions correspondant à une instruction machine. Nous parlerons alors de microcode de l'addition pour désigner la suite de microinstructions correspondant à l'instruction machine de l'addition. Faire cette petite erreur rendra la lecture de cette section beaucoup plus fluide. Les séquenceurs microcodés étaient surtout utilisés sur les architectures CISC, celles avec un jeu d'instruction étoffé et beaucoup de modes d'adressages différents. Leur grand nombre d'instructions favorisait un microcode. De plus, le budget en transistor de ces processeur était assez limité, ce qui fait que ces opérations aujourd'hui banales n'avaient pas leur propre circuit et étaient émulées en microcode. Les premiers microprocesseurs 16 bits utilisaient souvent le microcode pour implémenter des instructions comme la multiplication et la division. Un exemple est le 8086 d'Intel, qui n'avait pas de circuit multiplieur/diviseur. A la place, il émulait la multiplication avec une série d'additions et de décalages, et la division avec des soustractions/décalages. Les processeurs de ce type utilisaient un microcode pour beaucoup d'instructions, pas seulement la multiplication et la division. En conséquence, ajouter des instructions dans un microcode "existant" coutait moins cher que d'ajouter un circuit multiplieur. Un autre exemple d'utilisation du microcode est celui des premiers processeurs capables d'effectuer des calculs flottants. Sur les premiers processeurs de ce type, il n'y avait pas de FPU, pas de circuits pour les calculs flottants. Les instructions flottantes étaient en réalité émulées par des calculs entiers : chaque instruction flottante était convertie en interne en une suite d'instructions entières qui émulaient l'instruction voulue. Pour cela, les instructions flottantes étaient microcodées. De nos jours, les processeurs contiennent des circuits de calcul flottant, ce qui fait que les instructions ne sont plus émulées sauf pour quelques-unes. Les séquenceurs micro-codés sont plus simples à concevoir et simplifient beaucoup le travail des concepteurs de processeurs. L'usage du microcode permet aussi d'ajouter des instructions facilement, en modifiant le microcode, sans pour autant modifier en profondeur le processeur. En contrepartie, un séquenceur microcodé utilise plus de portes logiques, vu qu'une ROM est un circuit gourmand en portes logique. En théorie, les instructions microcodées peuvent être plus rapides que leur équivalent logiciel, à savoir une instruction émulée par une suite d'instructions machines. Le microcode peut être optimisé de manière à mieux utiliser les ressources internes au processeur. Mais force est de constater que ces opportunités d’optimisation étaient rares dans la réalité. Mais cela n'était pas l'intérêt principal, car les architectures CISC qui privilégiaient la taille du programme - la ''code size''. L'usage d'un microcode n’a plus trop d'intérêt de nos jours, et surtout pas sur les architectures RISC qui se contentent d'un séquenceur câblé. ===Le ''control store''=== La caractéristique principale du ''control store'' est sa capacité, qui est souvent assez petite. La capacité du ''control store'' dépend non seulement du nombre de micro-instructions qu'il contient, mais aussi de la taille de ces dernières. Un byte du ''control store'' correspond à une micro-instruction, les exceptions étant très très rares. Et la taille des micro-instructions varie grandement d'un processeur à l'autre. Dans les grandes lignes, la différence principale tient beaucoup la manière dont sont encodées les micro-instructions. Il existe plusieurs sous-types de séquenceurs microcodés, qui se distinguent par la façon dont sont codées les micro-opérations. * Avec le '''microcode horizontal''', chaque instruction du microcode encode directement les signaux de commande à envoyer aux unités de calcul. Vu Le grand nombre de signaux de commande, il n'est pas rare que les micro-opérations d'un microcode horizontal fassent plus d'une centaine de bits ! * Avec un '''microcode vertical''', les instructions du microcode sont traduites en signaux de commande par un séquenceur câblé qui suit le ''control store''. Son avantage est que les micro-opérations sont plus compactes, elles font moins de bits. Cela permet d'utiliser un ''control store'' plus petit ou d'avoir un microcode plus important, au détriment de la complexité du séquenceur. Un exemple de microcode vertical est le microcode du 8086, encore lui ! Pour ce qui est de la commande de l'ALU, le microcode envoie une commande abstraite qui est décodée par un circuit combinatoire (un PLA), pour obtenir les signaux de commande de l'ALU. L'implémentation interne du ''control store'' ne suit pas forcément à la lettre l'organisation en byte. Pour faire comprendre ce que je veux dire, prenons l'exemple de l'Intel 8086, dont le ''control store'' contenait 512 bytes/microinstructions de 21 bits chacune. Le ''control store'' n'était pas une ROM de 512 lignes et de 21 colonnes, comme on pourrait s'y attendre. Les dimensions 512 par 21 donneraient une ROM très allongée, rendant son placement sur la puce de silicium peu pratique. A la place, elle regroupait 4 bytes par ligne, ce qui donnait 84 lignes et 128 colonnes. ===L'optimisation du microcode=== Le ''control store'' a souvent une capacité très faible, même pour une mémoire ROM. Une ROM prend de la place, ce qui fait que les concepteurs de processeurs préfèrent utiliser une ROM assez petite. Néanmoins, malgré la petitesse des ROM de l'époque, il arrivait souvent que le ''control store'' contienne des vides, des bytes inoccupés. Cela arrive si le microcode n'a pas une taille égale à une puissance de deux. Par exemple, si l'on a un microcode qui occupe 120 bytes, on doit utiliser un ''control store'' de 128 bytes, ce qui laisse 8 bytes vides. On pourrait croire que les vides sont placés à la fin du ''control store'', mais il est parfois préférable de disperser les vides dans le ''control store'', afin de simplifier les circuits adossés au microcode, ce que nous allons voir dans ce qui suit. Pour les concepteurs de processeurs, une difficulté majeure est de faire rentrer le microcode dans le ''control store''. C'est encore un problème à l'heure actuelle, mais ce l'était encore plus sur les architectures anciennes, qui devaient faire avec des ROM limitées qu'actuellement. De plus, sur les anciennes architectures CISC, le grand nombre d'instructions recherchait se mariait mal à la petite capacité des mémoires ROM de l'époque. Les concepteurs de processeurs devaient ruser pour faire rentrer un microcode souvent complexe dans une petite ROM. Diverses optimisations étaient possibles. La première optimisation de ce genre consiste à gérer des fonctions/sous-programmes/routines logicielles dans le microcode. Pour cela, les circuits en charge du microcode géraient l’exécution de fonctions dans le microcode, avec des registres pour l'appel de retour, des microinstructions pour faire des branchements dans le microcode et tout ce qui va avec. Mais le tout était généralement simplifié et rares étaient les processeurs qui incorporaient une pile d'appel complète pour le microcode. Beaucoup se limitaient à ajouter un registre pour l'adresse de retour, quelques instructions de branchement interne au microcode, et guère plus. Un exemple assez intéressant est celui du processeur Intel 8086, dont le microcode contient une sous-routine pour gérer chaque mode d'adressage. Sans optimisations, il faudrait un microcode par instruction et par mode d'adressage. Par exemple, le microcode pour une addition en mode d'adressage immédiat n'est pas la même que pour une instruction d'addition en mode d'adressage direct. Cependant, elles partagent un même cœur qui s'occupe de l'addition et de la gestion de l'accumulateur, même si la gestion des opérandes est totalement différente suivant le mode d'adressage. Pour éliminer cette redondance, le microcode du 8086 délègue la gestion des modes d'adressages et des opérandes à des sous-programmes spécialisés, une par mode d'adressage. La seconde optimisation est de réduire la taille des micro-instructions en jouant sur leur encodage. L'usage d'un microcode vertical est une première solution. Décoder certaines instructions simples sans passer par le microcode en est une autre, et elle donne les séquenceurs hybrides dont nous parlerons dans la suite du chapitre. Mais d'autres techniques sont possibles, comme le fait de déporter une partie du décodage en-dehors du ''control store'', dans des circuits logiques séparés. Un bon exemple de cela est celui de l'Intel 8086, encore lui, sur lequel beaucoup d'instructions existaient en deux exemplaires : une version 8 bits et une version 16 bits. Il n'y avait pas de microcode séparé pour les deux versions, mais un seul microcode qui s'occupait autant de la version 8 bits que de la version 16 bits de l'instruction. La différence entre les deux se faisait au niveau du bus interne du processeur. Un bit de l'instruction machine indiquait s'il s'agissait d'une version 8 ou 16 bits et ce bit était transmis à la machinerie du bus interne, sans passer par le microcode. ===Les circuits d’exécution du microcode=== Le processeur doit trouver un moyen de dérouler les micro-instructions les unes après les autres, ce qui est la même chose qu'avec des instructions machines. Le micro-code est donc couplé à un circuit qui de l’exécution des micro-opérations les unes après les autres, dans l'ordre. Ce circuit est l'équivalent du circuit de chargement, mais pour les micro-opérations. Pour cela, il y a deux méthodes, que voici. La première méthode fait que chaque micro-instruction contient l'adresse de la micro-instruction suivante. Avec cette méthode, on peut disperser une suite de microinstructions dans le ''control store'', au lieu de garder des microinstructions consécutives. L'utilité de cette méthode n'est pas évidente, mais elle deviendra plus claire dans la section suivante. [[File:Microcode sans microséquenceur.gif|centre|vignette|upright=1.5|Microcode sans microséquenceur.]] La seconde méthode fait que le séquenceur contient un équivalent du ''program counter'' pour le microcode. On trouve ainsi un '''micro-séquenceur''' qui regroupe un '''registre d’adresse de micro-opération''' et un '''micro-compteur ordinal'''. Le registre d’adresse de micro-opération est initialisé avec l'opcode de l'instruction à exécuter, qui pointe vers la première micro-instruction. Le micro-compteur ordinal se charge d'incrémenter ce registre à chaque fois qu'une micro-instruction est exécutée, afin de pointer sur la suivante. [[File:Microcode avec un microséquenceur.gif|centre|vignette|upright=2|Microcode avec un microséquenceur.]] Un séquenceur microcodé peut même gérer des micro-instructions de branchement, qui précisent la prochaine micro-instruction à exécuter. Grâce à cela, on peut faire des boucles de micro-opérations, par exemple. Pour gérer les micro-branchements, il faut rajouter la destination d'un éventuel branchement dans les micro-instructions de branchement. La taille des micro-instructions augmente alors, vu que toutes les micro-opérations ont la même taille. Voici ce que cela donne pour les microcodes avec un microcompteur ordinal. On voit que l'ajout des branchements modifie le microcompteur ordinal de façon à permettre les branchements entre micro-opérations, d'une manière identique à celle vue pour l'unité de chargement. [[File:Branchements avec microcode horizontal avec microséquenceur.gif|centre|vignette|upright=2|Branchements avec microcode horizontal avec microséquenceur.]] Voici ce que cela donne pour les microcodes où chaque micro-instruction contient l'adresse de la suivante : [[File:Branchements avec microcode horizontal sans microséquenceur.gif|centre|vignette|upright=2|Branchements avec microcode horizontal sans microséquenceur.]] Il est possible de créer des fonctions/sous-programmes/sous-routines dans le microcode, grâce à ces micro-branchements et en ajoutant un registre pour gérer l'adresse de retour. ===Localiser la première microinstruction à exécuter dans le ''control store''=== Un premier problème à résoudre avec un microcode, est de localiser la suite de micro-instructions à exécuter. Si l'on veut exécuter une instruction machine, le microcode doit trouver le début de la suite de microinstruction dans le microcode et démarrer l’exécution des microinstructions à partir de là. Pour le dire autrement, le séquenceur doit déterminer, à partir de l'opcode, quelle est l'adresse de départ dans le ''control store''. Pour cela, il y a plusieurs solutions. La première solution fait une traduction de l'opcode vers l'adresse de départ, en utilisant un circuit combinatoire et/ou une mémoire ROM. Elle a l'inconvénient de complexifier le processeur, dans le sens où on doit ajouter des circuits en plus. De plus, le circuit ou la ROM ajoutés mettent un certain temps avant de donner leur résultat, ce qui ralentit quelque peu le décodage des instructions. L'avantage principal est que l'on peut utiliser facilement un microséquenceur basique et placer les microinstructions les unes à la suite des autres dans le ''control store''. Cette technique s'utilise aussi bien avec un micro-séquenceur que sans. Dans les faits, elle s'utilise de préférence avec un micro-compteur ordinal. L'usage de ce dernier réduit fortement la taille du ''control store'', ce qui compense le fait de devoir ajouter des circuits pour faire la traduction opcode -> adresse. [[File:Control store adressé par predecodage de l'opcode.png|centre|vignette|upright=2|Control store adressé par predecodage de l'opcode]] L'autre solution considère l'opcode de l'instruction microcodée comme une adresse : le ''control store'' est conçu pour que cette adresse pointe directement sur le début de la suite de micro-opérations correspondante, la première micro-instruction de cette suite. Du moins, c'est le principe général, mais un détail vient mettre son grain de sel : un ''control store'' utilise systématiquement des adresses plus grandes que l'opcode. Ce qui fait qu'il faut rajouter des bits à l'opcode pour obtenir l'adresse, on doit concaténer des zéros à l'opcode pour obtenir l'adresse finale. On fait alors face à deux choix : soit on met l'opcode dans les bits de poids faible de l'adresse, soit on la place dans les bits de poids fort. Les deux solutions ont des avantages et inconvénients différents. [[File:Control store.gif|centre|vignette|upright=2|Control store d'un microcode horizontal.]] La première méthode place les opcodes dans les bits de poids faible et les zéros dans les bits de poids fort. Le défaut principal de cette méthode vient du fait que de nombreux opcodes ont des représentations binaires proches, ce qui fait que leurs adresses de départs sont proches dans le ''control store''. Il n'y a alors pas assez d'espace entre les deux adresses de départ pour y placer une suite de microninstructions. En clair, cette méthode ne peut pas s'utiliser avec un micro-séquenceur. Par contre, elle se marie très bien avec un ''control store'' où chaque microinstruction contient l'adresse de la suivante. En faisant cela, l'opcode pointe vers l'adresse de départ, mais le reste de la suite de microinstructions est placé ailleurs dans le ''control store'', dans des adresses qui ne correspondent pas à des opcodes. Les adresses de départ occupent donc le bas de la ROM du ''control store'', alors que le haut de la ROM contient les suites de microinstructions et éventuellement des vides. [[File:Control store adressé par l'opcode - opcode sur bits de poids faible.png|centre|vignette|upright=2|Control store adressé par l'opcode - opcode sur bits de poids faible]] La seconde méthode met l'opcode dans les bits de poids fort de l'adresse et les zéros dans les bits de poids faible. En faisant cela, les adresses de départ sont dispersées dans le ''control store'', elles sont séparées par des intervalles de taille de fixe. Cela garantit qu'il y a un espace fixe entre deux adresses de départ, dans lequel on peut placer une suite de microinstructions. Un bon exemple est celui du 8086, dont le microcode, très complexe, espace chaque instruction/opcode tous les 16 bytes, ce qui permet d'avoir 16 microinstructions par instruction machine. Son ''control store'' contenait 512 micro-instructions, 512 bytes, ce qui donne des adresses de 13 bits. Mais l'opcode occupait les 9 bits de poids fort de l'adresse de microcode, ce qui laissait 4 bits de poids faible libres. En conséquence, chaque instruction machine disposait de maximum 16 microinstructions consécutives. L'avantage de cette méthode est que l'on peut utiliser un microséquenceur plus petit, avec un incrémenteur de plus petite taille. De plus, les adresses utilisées pour les branchements dans le microcode sont plus petites. Par exemple, le microcode du 8086, qui espacait ses microinstructions toutes les 16 bytes, avait un microséquenceur de 4 bits. Ce dernier contenait un incrémenteur de micro-''program counter'' de 4 bits et non 13. De plus, les adresses utilisées pour les branchements dans le microcode ne faisaient que 4 bits, à savoir qu'il s'agissait de branchements relatifs. Tout cela rendait le microséquenceur beaucoup plus économe en circuits. Cette solution a cependant pour défaut de laisser beaucoup de vides dans le ''control store''. Le microcode de certaines instructions était assez court, d'autres avaient un microcode plus long. L'espace entre deux opcodes, entre deux adresses de départ, est fixe et se cale sur le microcode le plus long. En conséquence, le microcode de certaines instructions laisse des vides à sa suite. Si on sépare les adresses de départ par un espace assez court, alors les suites d'instructions trop longues ne rentrent pas, sauf en trichant. Par tricher, on veut dire que le microcode de ces instruction est découpé en morceaux et dispersé dans les vides du ''control store''. L’exécution d'un microcode dispersé ainsi se fait normalement grâce aux microinstructions de branchement. [[File:Control store adressé par l'opcode - opcode sur bits de poids fort 01.png|centre|vignette|upright=2|Control store adressé par l'opcode - opcode sur bits de poids fort]] Pour comparer les trois méthodes, on peut comparer ce qu'il en est pour le remplissage du ''control store''. Les deux premières méthodes remplissent le ''control store'' au mieux, alors que la dernière laisse des vides et disperse les suites de microinstructions dans le ''control store''. Par contre, il faut aussi tenir compte d'autres paramètres. La première solution demande d'ajouter des circuits de traduction opcode -> adresse qui prennent de la place, pas les deux dernières solutions. Enfin, la deuxième solution impose de rallonger les bytes du ''control store'', car on se prive de micro-séquenceur, ce qui n'est pas le cas des deux autres. Au final, comparer les trois solutions ne donne pas de gagnant absolu : tout dépend de l'implémentation du jeu d'instruction choisit, de son encodage, etc. ===La mise à jour du microcode=== Parfois, le processeur permet une mise à jour du ''control store'', ce qui permet de modifier le microcode pour corriger des bugs ou ajouter des instructions. L'utilisation principale est de corriger des bugs ou des problèmes de sécurité assez tordus. Il est fréquent que les processeurs aient des bugs matériels, présents à cause de défauts de conception parfois subtils. Les grands fabricants comme Intel et AMD documentent ces bugs dans leur documentation officielle. Une petite partie de ces bugs peuvent se corriger avec une mise à jour du microcode, et ils ne sont pas forcément dans le microcode lui-même. Un exemple serait la désactivation des instructions TSX sur les processeurs x86 Haswell, en 2014, qui ont été désactivées par une mise à jour du microcode, après qu'un bug de sécurité ait été découvert. La mise à jour du microcode est rarement permanente. Une mise à jour permanente du microcode implique que le ''control store'' est une EEPROM ou une mémoire ROM reprogrammable, donc des mémoire très difficiles à mettre en œuvre dans les processeurs. Or, le ''control store'' doit être une mémoire extrêmement performante, capable de fonctionner à très haute fréquence, avec des temps d'accès minuscules, aux performances proches d'une SRAM. En réalité, le ''control store'' est mis à jour temporairement, et est réinitialisé à chaque boot de l'ordinateur, à chaque boot du processeur. Pour cela, le ''control store'' est implémenté avec deux mémoires : une ROM qui contient le microcode originel, et une SRAM. Pour simplifier les explications, nous allons appeler ces deux mémoires la micro-ROM et la micro-RAM. Au démarrage de l'ordinateur, le microcode contenu dans la micro-ROM est copié dans la micro-RAM. Peu après l'allumage du processeur, le contenu de la micro-RAM peut être remplacé par un microcode mis à jours. Typiquement, le microcode corrigé est fourni soit par le BIOS, soit par le système d'exploitation. Les mises à jour de microcode sont généralement soumises à des mesures de sécurité drastiques intégrées au processeur. Par exemple, le microcode fournit par le fabricant est chiffré avec des clés connues seulement des fabricants de CPU, autres), et un microcode n'est chargé par le processeur que si la clé correspond. ===Les microcodes réinscriptibles=== Il existe des processeurs dont le microcode est directement reprogrammable, accessible par le programmeur. Le programmeur peut écrire ce qu'il veut dans la micro-RAM, à sa guise. On peut ainsi changer le jeu d'instruction du processeur au besoin, afin d'ajouter des instructions utiles. Il s'agit de processeurs destinés à l'embarqué, qui doivent être conçus sur mesure, on ne trouve pas de systèmes de ce genre dans les PCs. Ils sont appelés des '''processeurs à microcode réinscriptible'''. L'utilité est que les programmes peuvent disposer des instructions les plus adéquates pour leur fonction, ce qui réduit la taille du code (la mémoire prise par le programme exécutable) et facilite la programmation en assembleur. Ces deux avantages n'ont pas grand intérêt de nos jours. De plus, l'utilisation de cette technique demande un ''control store'' assez imposant, de grande taille, rarement rapide. Par contre, cette fonctionnalité a de nombreux défauts. Si chaque programme peut changer à la volée le jeu d'instruction du processeur, cela peut mettre le bazar. Si un programme change le microcode, les programmes qui passent après lui doivent réinitialiser le microcode pour ne pas exécuter des instructions incorrectes. Cela peut aussi poser des problèmes de sécurité, les hackers étant doués pour utiliser ce genre de fonctionnalités à des fins malveillantes. Cependant, les processeurs de ce type sont utilisés pour l'embarqué, sur des systèmes où un seul programme s'exécute, sans accès à un réseau local, il n'y a donc pas de problèmes liés à des programmes malveillants ou des interactions entre programmes. Un processeur de ce type est le GP1000 d'Imsys, qui a été décrit entres autres dans l'article "GP1000 Has Rewritable Microcode" du magazine ''Microprocessor report''. Il disposait d'une micro-ROM de 36 kibioctets, couplées à une micro-RAM de 18 kibioctets. Une partie du microcode était donc réinscriptible : un tiers l'était, les deux autres tiers étaient dans une micro-ROM impossible à modifier. Il y avait donc un microcode permanent en micro-ROM et un microcode variable en en micro-RAM. Le microcode permanent contient un ''chargeur de microcode'', qui recopie le microcode variable dans la micro-RAM, à l'allumage de l'ordinateur. Par la suite, le microcode variable est chargé avec une instruction machine dédiée. Un point très original est que le GP1000 supporte l'exécution de plusieurs programmes à tour de rôle. Le microcode permanent contient de quoi gérer des interruptions, émuler des périphériques virtuels, mais aussi gérer la présence de plusieurs programmes en cours d'exécution. Les programmes s'exécutent à tour de rôle et le microcode décide qui s’exécute et pour combien de temps. Le microcode permanent contient donc une sorte de mini-système d'exploitation rudimentaire ! Au-delà ce ça, Imsys fournissait des microcodes près à l'emploi, qui pouvaient être chargés à la demande. L'un d'entre eux permettait d'implémenter la machine virtuelle Java directement dans le processeur. Pour information, la machine virtuelle Java standardise le langage machine d'un processeur fictif, précisément une machine à pile simple, qui peut en théorie être implémentée en matériel. C'est exactement ce que faisait l'un des microcodes près à l'emploi d'Imsys. ==Les séquenceurs hybrides== Les séquenceurs hybrides sont un compromis entre séquenceurs câblés et microcodés. Ils permettent de profiter des avantages et inconvénients des deux types de séquenceurs. Sur le principe, une partie des instructions est décodée par une partie câblée, et l'autre passe par le microcode. Typiquement, de tels séquenceurs sont très fréquents sur les architectures CISC, où ils permettent un décodage rapide pour les instructions simples, alors que les instructions complexes le sont par le microcode, plus lent. L'organisation interne d'un séquenceur hybride varie grandement selon le processeur et le jeu d'instruction. Dans le cas le plus simple, on a un séquenceur câblé secondé par un séquenceur microcodé, les deux étant précédés par un '''circuit de prédécodage'''. Le circuit de prédécodage reçoit les instructions et les redirige soit vers le séquenceur câblé, soit vers le séquenceur microcodé. Les instructions les plus simples sont dirigées vers le séquenceur câblé, alors que les instructions complexes vont vers le microcode (généralement les instructions avec des modes d'adressage exotiques). Une solution intéressante est de décoder les instructions qui prennent un seul cycle dans un séquenceur câblé, alors que les instructions multicycles sont décodées par un séquenceur microcodé séparé. Mais dans le cas général, la séparation en deux séquenceurs n'est pas évidente et on trouve un ''control store'' entouré de circuits câblés, avec certaines instructions qui n'ont pas besoin du microcode pour être décodées, d'autres qui passent par le microcode, d'autres qui sont décodé partiellement par microcode et partiellement par des circuits câblés. Notons que le microcode vertical n'est pas un séquenceur hybride, car toutes les instructions passent par le microcode. Par contre, un séquenceur hybride peut utiliser un microcode vertical, ce qui rend le séquenceur assez compliqué. Sur les processeurs x86 modernes, on trouve plusieurs séquenceurs : plusieurs décodeurs câblés spécialisés, et un microcode séparé. ===Le séquenceur hybride du 8086=== Un bon exemple de séquenceur de ce type est celui du processeur x86 8086 d'Intel, ainsi que ceux qui ont suivi. Le jeu d'instruction x86 est tellement complexe qu'il utilise un séquenceur hybride. Le séquenceur de l'Intel 8086 est organisé comme suit : un ''control store'' de 512 microinstructions (512 bytes) couplé à de nombreux circuit câblés, et une ''Group Decode ROM'' qui décide pour chaque instruction si elle est décodée par le séquenceur câblé ou le microcodé. La mal-nommée ''Group Decode ROM'' est en réalité un petit circuit combinatoire un peu particulier (basé sur un PAL, composant proche d'une ROM), qui commande le séquenceur proprement dit. Il fournit 15 signaux qui configurent le séquenceur et disent si le décodage doit utiliser ou non le microcode. Il permet aussi de configurer le microcode pour gérer les différents modes d'adressage, ou encore de configurer les circuits câblés en aval du microcode. Sur ce processeur, les instructions qui s’exécutent en un seul cycle d'horloge sont décodés sans utiliser le microcode. Le 8086 utilise une sorte de micro-code vertical pour commander l'ALU. Entre l'ALU et le microcode, on trouve un mini-décodeur, qui décode l'opération envoyée par le microcode en signaux de commande. C'est un simple circuit combinatoire (un PLA pour être précis). La raison est que l'ALU du 8086 est quelque peu complexe. Nous l'avions vu dans le chapitre sur les unités de calcul, l'ALU du 8086 est basée sur un additionneur à propagation de retenue, où deux portes logiques sont remplacées par une porte logique universelle. Il faut commander ces deux portes universelles pour obtenir l'opération voulue, ce qui demande pas mal de signaux de commande. Et pour économiser de la place dans le microcode, l'opération à faire est encodée sur plusieurs bits, qui sont décodés pour générer les signaux de commande. Ce qui vient d'être dit est une simplification. En vérité, certaines instructions ne sont pas encodées dans le microcode. Pour ces instructions, l'opération est récupérée directement dans l'opcode de l'instruction. C'est le cas des opérations d'addition, soustraction, les opérations logiques, les décalages et autres opérations gérées naturellement par l'ALU. Pour elles, l'opération à faire est extraite de l'opcode, pas du microcode. Pour ces opérations, le microcode encode l'opération à exécuter sur l'ALU par une micro-instruction généraliste nommée XI. La micro-opération XI indique qu'il faut activer le multiplexeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'unité de chargement et le program counter | prevText=L'unité de chargement et le program counter | next=L'implémentation matérielle des branchements | nextText=L'implémentation matérielle des branchements }} </noinclude> 3zlr97m18pa11cs6unmo2ugeh7s9ax5 Les bases de données/Le vocabulaire de base des BDD 0 70594 764068 763896 2026-04-19T21:55:01Z Mewtow 31375 Annulation de la modification [[Special:Diff/763896|763896]] de [[Special:Contributions/~2026-23573-30|~2026-23573-30]] ([[User talk:~2026-23573-30|discussion]]) 764068 wikitext text/x-wiki Pour faire simple, la gestion des données mémorisées sur un support quelconque (disque dur, DVD, CD-ROM, …) demande de gérer deux choses bien différentes : le stockage des données et leur manipulation. Pour commencer, nous allons nous intéresser au stockage des données, comment celles-ci sont organisées pour être mémorisées durablement. Les concepts que nous allons voir dans cette page sont les bases de ce cours et les connaître est très important. La plupart des termes que nous allons voir forment le vocabulaire minimal qu'il faut connaître pour aborder ce domaine. == Abréviations == ;BDD ''(ou BD)'':'''B'''ase '''d'''e '''d'''onnées. ;SGBD:'''S'''ystème de '''g'''estion de '''b'''ase de '''d'''onnées. ;SQL:'''''S'''tructured '''q'''uery '''l'''anguage'' : Langage de requête structuré. == Tables, enregistrements et attributs == Que ce soit dans un fichier ou dans une base de données, les données à mémoriser sont ce qu'on appelle des '''enregistrements'''. Ces enregistrements mémorisent toutes les informations liées à un objet, une personne, une entité, une chose. Prenons le cas d'une entreprise qui veut mémoriser la liste des commandes qu'elle a passé à ses fournisseurs : chaque commande sera mémorisée dans l'ordinateur dans un enregistrement, qui contiendra toutes les informations liées à la commande. Prenons maintenant l'exemple d'un établissement scolaire qui veut mémoriser la liste de ses élèves : chaque élève sera mémorisé par un enregistrement, qui contiendra toutes les informations sur l'élève en question. Et enfin, prenons comme dernier exemple un biologiste qui veut établir la liste de toutes les espèces animales découvertes à ce jour : chaque espèce découverte se verra attribuer un enregistrement, qui contiendra toutes les informations sur l'espèce. Les enregistrements sont des équivalents des structures dans les langages de programmation comme le C ou le C++. === Enregistrements et attributs === Comme les exemples plus haut vous l'ont certainement fait remarquer, chaque entité, chaque enregistrement, est décrite par plusieurs informations : on a souvent besoin de mémoriser plusieurs informations distinctes pour chaque enregistrement. Par exemple, une entreprise aura besoin de stocker, pour chaque client : * son nom ; * son prénom ; * son adresse ; * ses commandes en cours ; * etc. Comme autre exemple, un établissement scolaire devra mémoriser pour chaque élève : * son nom ; * son prénom ; * son adresse ; * son âge ; * sa classe ; * sa filière d'étude ; * etc. Comme dernier exemple, un garage aura besoin de mémoriser, pour chaque voiture : * son numéro d'immatriculation ; * sa marque ; * son modèle ; * etc. Toutes ces informations sont des informations distinctes, mais qui appartiennent à la même personne, au même objet, à la même entité : on doit les regrouper dans un seul enregistrement. Pour cela, chaque enregistrement est un regroupement d''''attributs''', chaque attribut étant une information élémentaire, qui ne peut pas être décomposée en informations plus simples. Ces attributs sont équivalents aux variables des langages de programmation. Définir un enregistrement, c'est simplement définir l'ensemble des attributs qu'il contient : il faut préciser son type et sa valeur. Mais il faudra aussi lui donner un '''nom''', pour pouvoir le récupérer. === Type d'un attribut === Ces attributs peuvent prendre des valeurs bien précises, définies à l'avance : les valeurs que peuvent prendre chaque attribut sont déterminés par ce qu'on appelle le '''type''' de l'attribut. Les types autorisés par les systèmes de gestion de données sont ceux couramment utilisés en informatique, à savoir : les nombres entiers et à virgule, les caractères et autres formes de texte, les booléens, etc. Reprenons les exemples vus plus haut, pour voir quel est le type adéquat pour mémoriser chaque attribut. Pour l'exemple d'une entreprise qui a besoin de mémoriser la liste de ses clients, voici le type de chaque attribut : * son nom : chaîne de caractère ; * son prénom : chaîne de caractère ; * son adresse : chaîne de caractère ; * le numéro de la commande : nombre entier ; * etc. Prenons maintenant l'exemple d'une infirmerie scolaire qui veut mémoriser des informations médicales obtenues lors des visites médicales. Celui-ci doit mémoriser, pour chaque élève : * son nom : chaîne de caractère ; * son prénom : chaîne de caractère ; * son numéro de sécurité sociale : nombre entier ; * son adresse : chaîne de caractère ; * sa taille : nombre à virgule ; * son poids : nombre à virgule ; * son âge : nombre entier ; * son groupe sanguin : chaîne caractère ; * son groupe rhésus : booléen ; * sa tension artérielle : nombre à virgule ; * etc. Comme l'exemple de l'établissement scolaire qui doit mémoriser pour chaque élève : * son nom : chaîne de caractère ; * son prénom : chaîne de caractère ; * son adresse : chaîne de caractère ; * son âge : nombre entier ; * sa classe : chaîne de caractère ; * sa filière d'étude : chaîne de caractère ; * etc. Comme dernier exemple, un garage aura besoin de mémoriser, pour chaque voiture : * son numéro d'immatriculation : chaîne de caractère ; * sa marque : chaîne de caractère ; * son modèle : chaîne de caractère ; * etc. ==== Valeur nulle ==== Dans certains cas, il arrive que certaines informations ne soient pas connues lorsqu'on crée l'enregistrement. Par exemple, quand un élève rentre dans une école, on peut très bien ne pas connaître son établissement de l'année précédente. Et des cas similaires sont légions : soit l'information est inconnue, soit elle n'existe pas. On doit alors remplir l'attribut correspondant par une valeur qui indique que l'attribut a une valeur inconnue, que l'attribut n'a pas été rempli. Cette valeur est appelée la valeur NULL. Par exemple, l'exemple suivant, qui correspond aux informations d'une personne, est parfaitement possible : * nom de famille : Braguier ; * premier prénom : Jean-Paul ; * second prénom : NULL ; * troisième prénom : NULL ; * etc. ==Modèles relationnel et de fichiers== D'ordinaire, les entreprises et autres utilisateurs ont besoin de mémoriser de grandes quantités d'informations basées sur un même moule. Par exemple, les informations à mémoriser pour chaque élève d'un établissement scolaire seront identiques : tous les élèves ont un âge, un nom, un prénom, etc. Dans une liste de commandes passées par des clients, toutes les commandes sont définies par les mêmes informations : quel est le client, le produit commandé, la quantité à fournir, etc. En clair, tous les enregistrements d'une même liste ont des attributs semblables : ils ont le même type, le même nom et la même signification, seul leur valeur change. Elles ont besoin de mémoriser des '''listes d'enregistrements'''. Généralement, les informations sont classées dans plusieurs listes, par souci de simplification. Par exemple, si on prend le cas d'une librairie, celle-ci rangera les informations liées aux commandes de livres dans une liste séparée des informations sur les clients, et ainsi de suite : on aura une liste de client, une liste de commande de livre, une liste pour les livres en rayon, etc. ===Les modèles de données=== [[File:Traditional View of Data SVG.svg|vignette|Illustration du stockage par fichiers.]] Dans les cas les plus simples, on peut mémoriser chaque liste dans un fichier : les enregistrements y sont placés les uns à la suite des autres. Mais l'utilisation de fichiers a un défaut : le traitement de fichiers doit être programmé de zéro. En effet, les langages de programmation ne permettent que de récupérer les informations d'un fichier unes par unes, ou d'en ajouter à la fin du fichier, mais sans plus : on ne peut pas dire au langage de programmation qu'on ne veut récupérer que les données qui respectent une certaine condition, ni fusionner le contenu de deux fichiers, etc. Un autre défaut est que ce modèle ne permet pas de représenter les relations entre listes d'enregistrements. Et ces relations peuvent être très importantes pour interpréter les données ou permettre de les manipuler efficacement. Le modèle à base de fichier se contente de placer chaque liste dans un fichier, le programme devant connaître de lui-même les relations entre données. D'autres systèmes de gestion des données permettent cependant de représenter ces liens avec une grande efficacité. {|class="wikitable" |[[File:Flat File Model.svg|class=transparent|Modèle de stockage par fichier.]] |} Pour pouvoir faire cela nativement, sans avoir à programmer un morceau de programme qui le fasse à notre place, on peut utiliser ce qu'on appelle des '''bases de données'''. Ces bases de données sont en quelque sorte des fichiers, mais qui sont gérées par un logiciel qui fait le café : ces logiciels sont appelés des systèmes de gestion de base de données, ou SGBD. Avec ces SGBD, le rangement des informations dans un fichier n'est pas spécifié par le programmeur : le SGBD s'occupe de gérer tout ce qui a rapport avec la manière dont les données sont rangées sur le disque dur. En plus, les bases de données effectuent automatiquement des opérations assez puissantes et peuvent faire autre chose que lire les informations unes par unes dans un fichier, les modifier, ou y ajouter des informations : on peut décider de récupérer les données qui respectent une certaine condition, de fusionner des données, etc. Mais leur intérêt principal est de modéliser les relations entre chaque liste d'enregistrement efficacement, d'une manière qui permet de traiter les données rapidement. Ces bases de données utilisent des méthodes assez distinctes pour représenter les relations entre informations, méthodes qui permettent de distinguer les BDD arborescentes, en réseau, relationnelles, déductives et orientées-objet. Les plus simples sont de loin les BDD en réseau et arborescentes. Celles-ci codent les relations entre listes par des pointeurs, chaque enregistrement pouvant faire référence à un autre, situé dans une autre liste. La différence entre BDD en réseau et arborescentes tient dans le fait que certaines relations sont interdites dans les BDD arborescentes, qui ont une forme d'arbre (non-binaire, le plus souvent). Le modèle relationnel sera vu plus loin : c'est lui que nous étudierons dans ce cours. Le fait est que toutes les BDDs actuelles utilisent le modèle relationnel. Les modèles orientés-objet et déductifs sont plus difficiles à décrire, du moins pour qui ne sait pas ce que sont les langages de programmation orientés-objet ou logiques. {|class="wikitable" |+'''BDD à pointeurs''' |[[File:Hierarchical Model.svg|class=transparent|Modèle de stockage hiérarchique.]] |[[File:Network Model.svg|class=transparent|Modèle de stockage en réseau.]] |} {|class="wikitable" |+'''BDD sans pointeurs''' |[[File:Object-Oriented Model.svg|class=transparent|Modèle de stockage orienté-objet.]] |[[File:Relational Model.svg|class=transparent|Modèle de stockage relationnel.]] |} ===Le modèle relationnel=== Une base de donnée relationnelle permet de gérer des listes de données, qui sont appelées des '''tables''', sont organisées en lignes et en colonnes. Chaque ligne mémorise un enregistrement, la succession des lignes permettant de mémoriser plusieurs enregistrements les uns à la suite des autres, en liste. Comme dit plus haut, les enregistrements d'une liste ont des attributs de même nom et de même type, seul leur valeur changeant : les attributs de même nom et type sont placés sur une même colonne, par simplicité. Évidemment, tous les enregistrements d'une table ont exactement les mêmes attributs, ce qui fait que toutes les lignes d'une table ont le même nombre de colonnes. {|class="wikitable" |+ Exemple de table simplifiée qui mémorise une liste de personnes |- !Nom !Prénom !Age !Ville |- |Dupont |Jean |25 |Lille |- |Marais |Sophie |45 |Paris |- |Dupond |Mathieu |35 |Paris |- |Salomé |Saphia |24 |Lyon |- |Queneau |Raymond |53 |Marseille |- |Tepes |Vlad |29 |Lille |} Il existe un vocabulaire assez précis pour nommer les enregistrements, colonnes et autres particularités d'une table. Ceux-ci sont illustrés dans les deux schémas ci-dessous. [[File:Figure structure relationnelle.png|centre|Figure structure relationnelle.]] Une formulation équivalente à la précédente utilise les termes suivants : [[File:Table relationnel.png|centre|Table relationnel]] Évidemment, ces tables ne serviraient strictement à rien si on ne pouvait pas récupérer leur contenu : on peut récupérer une ligne dans la table sans que cela pose problème. On peut aussi ajouter des lignes dans une table, ainsi qu'en supprimer : si on prend l'exemple d'une liste de commandes tenue par une entreprise, on peut ajouter des commandes quand un client passe un contrat, ou en supprimer une fois la commande honorée. Et enfin, on peut modifier certaines lignes, quand celles-ci contiennent des données erronées ou dont la valeur a changé : si on prend une liste d'employé d'une entreprise, on peut vouloir changer le nom d'une employée suite à un mariage, par exemple. Ainsi, gérer des données dans une table demande de pouvoir faire quatre opérations de base : * CREATE (créer) : créer une nouvelle ligne ; * READ (lecture) : récupérer une ligne dans la table ; * UPDATE (modifier) : modifier le contenu d'une ligne ou d'un attribut ; * DELETE (supprimer) : supprimer une ligne devenue inutile. Ces quatre opérations sont regroupées sous l'acronyme CRUD. ===Remarque de fin=== Il est évident qu'il vaut mieux éviter de mémoriser des informations inutiles dans une base de données. Par données inutiles, on veut dire : données qui peuvent se déduire à partir des données déjà présentes dans l'enregistrement. Par exemple, le prix d'une commande client peut se déduire à partir du prix unitaire d'un produit et des quantités commandées. Dans ces conditions, mieux vaut éviter de mémoriser ces données facilement déductibles : le programme qui manipulera les informations de la base de donnée les calculera lui-même, ce qui est très rapide (plus que de lire les données depuis une BDD ou un fichier). Une fois cela fait, les données de la table forment un ensemble minimal de données. Mais toutes n'ont pas le même poids, le même intérêt. ==Conclusion== Ce chapitre s'est limité au vocabulaire de base, le vocabulaire des bases de données étant assez important. Pour vous en rendre compte, vous pouvez avoir une petite idée des termes du domaine en allant en voir la liste, disponible sur wiktionnaire, via ce lien : * [https://fr.wiktionary.org/wiki/Cat%C3%A9gorie:Lexique_en_fran%C3%A7ais_des_bases_de_donn%C3%A9es Lexique en français des bases de données]. Une partie des termes de cette liste seront vus dans les chapitres suivants. {{NavChapitre | book=Les bases de données | next=Les clés primaires et secondaires | nextText=Les clés primaires et secondaires }} {{autocat}} d6a48foni9fcodm339atr078lu0sop1 Jeu de rôle sur table — Jouer, créer/Au cœur du jeu de rôle, la partie 0 74517 764022 758524 2026-04-19T17:26:12Z Cdang 1202 /* La partie au cœur des préoccupations */ voc : conception par l'intention 764022 wikitext text/x-wiki <noinclude>{{NavTitre|book={{BASEPAGENAME}}|prev=Préparer une partie|next=Et en dehors des parties…}}</noinclude> == Des parties de jeu de rôle == De manière générale, un jeu se fait en parties. C'est également le cas du jeu de rôle. Par une « partie », on entend une session de jeu, c'est-à-dire une ou plusieurs personnes qui décident de participer au jeu pendant une durée donnée, et dans un environnement donné. Dans le cas « classique », cet environnement est un lieu — une pièce, un terrain de jeu plus ou moins grand. Cela peut aussi être un environnement « virtuel » : des échanges de lettre pour un jeu épistolaire ou des parties par correspondance, des appels téléphoniques ou des outils de communication en ligne — par exemple [[w:Jitsi|Jitsi]], [[w:Discord (logiciel)|Discord]] (et les générateurs de jets de dés DiceParser ou Dice Maiden), [[w:Skype|Skype]], [[w:Microsoft Teams|Microsoft Teams]], [[w:Google Hangouts|Google Hangouts]], [[w:Zoom Video Communications|Zoom]], [[w:Facebook Messenger|Facebook Messenger]]… —, souvent couplé à d'autres outils de partage d'informations — comme Miro<ref>{{lien web | url = https://miro.com/ | titre = Miro, free online collaborative whiteboard plateform | lang = en | consulté le = 2020-04-20 }}.</ref>, [[w:Framapad|Framapad]] ou Padlet<ref>{{lien web | url = https://padlet.com/ | titre = Padlet | consulté le = 2020-04-27 }}.</ref>, Notion<ref>{{lien web | url = https://www.notion.so/ | titre = Notion | consulté le = 2023-03-06 }}.</ref>, Obsidian<ref>{{lien web | url = https://obsidian.md/ | titre = Obsidian | consulté le = 2023-03-06 }}.</ref> — et à un générateur de jets de dés — Rolladie<ref>{{lien web | url = https://rolladie.net/ | titre = Roll a Die | consulté le = 2020-04-27 }}/</ref> ou DnDdiceroller<ref>{{lien web | url = https://www.dnddiceroller.com/ | titre = DnD Dice Roller | consulté le = 2020-04-27 }}.</ref>. Il peut aussi s'agir d'un réseau informatique : jeux par forum, jeux vidéo en ligne, plateformes de jeu de société — pour le jeu de rôle, en particulier Roll20<ref>{{lien web | url = https://roll20.net/ | titre = Roll20 : table de jeu virtuelle en ligne pour JdR papier et jeux de société | consulté le = 2020-04-20 }}.</ref>, Fantasy Grounds<ref>{{lien web | url = http://www.fantasygrounds.com/ | titre = Fantasy Grounds | lang = en | consulté le = 2020-04-20 }}.</ref>, Rolisteam<ref>{{lien web | url = https://rolisteam.org/ | titre = Rolisteam | lang = en | consulté le = 2020-04-20 }}.</ref> ou Let's Role<ref>{{lien web | url = https://lets-role.com/ | titre = Let's Role | consulté le = 2020-04-29 }}.</ref>. La participation est volontaire ; on y participe librement. Le consentement peut être implicite, on se laisse entraîner dans le jeu, mais à tout moment il y a la possibilité de dire « pouce, je ne joue plus ». La notion de consentement est importante, c'est ce qui fait la différence entre, par exemple, un match de boxe et une agression, entre des railleries entre potes et du harcèlement. Le jeu donc est limité dans l'espace et dans le temps. Cette activité se partage avec d'autres activités de la vie comme dormir, se nourrir, se laver, apprendre, travailler… {{Boîte déroulante début|Pourquoi le jeu de rôle devrait se dérouler « par parties » ?}} Le jeu est une activité qui est relativement isolée d'autres activités de la vie et ne doit pas avoir de répercussion trop importante sur elles, en particulier le travail et la santé<ref>On pourra notamment se référer à [[w:Johan Huizinga|Johan Huizinga]], ''[[w:Homo ludens|Homo ludens]], Gallimard (1938).</ref>. Pour donner quelques contre-exemples, lorsque des joueurs et joueuses deviennent professionnels (sportifs et sportives, poker, e-sport), on parle plus d'un métier que d'un jeu — activité « autotélique », pratiquée pour le plaisir seul. Et lorsqu'un jeu devient dangereux, comme le jeu du foulard ou des sports à risque, on sort du domaine du jeu. La notion de frontière entre le jeu et le reste de la vie humaine est floue et remise en question avec l'évolution des pratiques et des technologies, mais il n'en reste pas moins que « l'activité jeu de rôle » a un début et une fin, ce qui permet d'identifier une « partie », une « session de jeu ». Qui ne se limite pas à la rencontre des joueurs et joueuse (autour d'une table ou ''via'' un réseau informatique) : * l'activité jeu de rôle commence en amont, lorsque l'on prépare la partie : le cas échéant lorsque le meneur ou la meneuse de jeu prépare le scénario, lorsque l'on crée des personnages, lorsque les participant·es règlent les détails de la partie — à quoi on joue, comment on joue, où on se réunit (lieu physique ou moyen de partage informatique), qui amène quoi… ; * l'activité peut se poursuivre après la partie, entre les parties : quand on règle un problème survenu lors d'une partie — désaccord sur les règles ou sur la manière de gérer une situation, problème relationnel entre joueur·euses —, que l'on gère l'évolution de la situation entre les parties formelles — ce que font les personnages entre les aventures par exemple. Bien évidemment, le jeu n'est pas totalement étanche au reste de la vie. On arrive à une session de jeu avec ses préoccupations qui vont déteindre sur la partie, et des situations de jeu peuvent déteindre sur le reste de la vie. En particulier, on peut éprouver de la joie à l'idée de faire une partie et une certaine tristesse lorsque l'on quitte son groupe d'amis. À l'inverse, on peut ne jouer que pour faire plaisir à des proches ou juste pour partager un moment avec des personnes, mais éprouver un certain ennui lors de la pratique elle-même. On peut développer des compétences en jeu et les réutiliser, les réinvestir, dans d'autres activités, le jeu étant par essence apprentissage<ref>On pourra lire à ce propos [[w:Jean Piaget|Jean Piaget]], ''La formation du symbole chez l'enfant : imitation, jeu et rêve, image et représentation'', Delachaux et Niestlé (1945), http://www.fondationjeanpiaget.ch/fjp/site/textes/index.php</ref>. Les émotions éprouvées réellement par le joueur ou la joueuse peut infuser dans le jeu (la joueuse fait éprouver à son personnage ce qu'elle ressent elle-même, par exemple le personnage est triste parce que la joueuse a vécu un événement extérieur qui l'a rendue triste) ; à l'inverse, des émotions provoquées par ce que vit le personnage peuvent infuser dans le joueur ou la joueuse, ce phénomène étant en particulier étudié dans le domaine du jeu de rôle grandeur nature/semi-réel ''({{lang|en|bleed in, bleed out}})''. Les moyens de connexion numérique permettent de continuer à jouer en tout temps et en tous lieux, y compris sur le lieu de travail ou dans la rue, parfois au détriment de ces activités-là : baisse de productivité au travail, risque d'accident dans la rue<ref>On se souviendra par exemple des accidents liés au jeu ''[[w:Pokémon Go|Pokémon Go]]'', lire par exemple {{lien web | url = https://www.sudouest.fr/2016/07/18/coups-de-feu-chutes-accidents-quand-les-joueurs-de-pokemon-go-vont-trop-loin-2438556-6046.php | titre = Coups de feu, chutes, accidents : quand les joueurs de Pokémon Go vont trop loin | site = Sud-Ouest | auteur = Vincent Romain | date = 2016-07-18 | consulté le = 2020-04-17 }}.</ref>. Pour autant, le jeu de rôle reste une activité essentiellement bornée dans le temps — début et fin d'une partie — et dans l'espace — lieu de réunion ou devant son écran pour être connecté à réseau informatique sur lequel se déroule le jeu. {{Boîte déroulante fin}} == La partie au cœur des préoccupations == Puisque le jeu de rôle se déroule essentiellement en parties, c’est ce qui se déroule lors des parties qui doit être la préoccupation principale des joueur·euses et des concepteur·trices de jeu. Cela peut sembler une évidence mais ce n’est pas toujours clair pour tout le monde. En particulier, certains jeux peuvent être conçus avec des idées ''a priori'' sans vraiment correspondre à ce qui est censé se dérouler à la table. {{citation bloc|En gros voilà ce qui se passe dans les 90’s, d’où je me tiens : le style de “jeu par défaut” — la ''doxa'', la norme tacite, l’hypothèse de base, bref : la représentation que s’en font la plupart des gens qui pratiquent quand on leur dit “faire du jeu de rôle” — semble glisser lentement du défi de groupe avec objectif collectif (“on va descendre dans le donj’ avec ce qu’on a et si on joue finement on va le meuler ce dragon”) vers une envie de faire du tourisme dans un monde fictif en jouant le perso de nos rêves, pour voir ce que ça fait. En gros : on passe du sport d’équipe à la fanfiction collective. Or faut bien le dire, des règles prévues pour la première façon ne permettent pas la seconde sans heurts. Mais gros souci : on n’a jamais fait autrement donc on n’a pas ''vraiment'' conscience du problème. On se dit pas qu’il faut changer les fondations et reconstruire vers le haut. On se dit juste qu’on va prendre ce qu’on fait d’habitude et ''raboter en partant de la surface'' pour enlever ce qui gène, en espérant qu’on va pas devoir tout raboter, puis ''recoller dessus'' des bouts qui collent mieux. Du coup tu te retrouves avec une base bien solide, souvent caraque + compète, dont on va presque pas se servir parce qu’on en n’a pas tellement besoin, en tout cas pas comme elles tournent. Puisqu’elles tournent toujours en répondant à la question “c’est qui le plus fort” parfaitement appropriée au défi de groupe face aux éléments, mais pas du tout à de la fanfic collective, qui demande elle, et sans cesse, “comment je me démarque”. Si chaque mécanique est une façon de mettre en scène un certain type d’événements dans le cadre de ton jeu, dans les 90’s on filme ''Hamlet'' comme une retransmission de l’Eurocup. |2=Gregory Pogorzelsky (2016)<ref>{{lien web | site = Du bruit derrière le paravent | titre = Cthulhutech - Ep. 1: un nouveau costard | auteur = Gregory Pogorzelsky | date = 2016-04-21 | consulté le = 2020-04-16 | url=https://awarestudios.blogspot.com/2016/04/cthulhutech-ep-1-un-nouveau-costard.html }}.</ref> |3=Cthulhutech - Ep. 1: un nouveau costard}} Lorsqu’un auteur ou une autrice crée un jeu, il ou elle veut créer une expérience partagées par les joueurs et joueuses ; on parle de « conception par l'intention » ''({{lang|en|intentional design}})''. Ambiance, choix à faire, suspense créé par une attente ou du hasard, dilemmes moraux… Et cela est transmis notamment par les règles du jeu<ref>Lire par exemple {{lien web | titre = Une règle, ça sert à quoi ? | url = https://www.lapinmarteau.com/une-regle-ca-sert-a-quoi/ | auteur = Jérôme « Brand » Larré | site = Lapin Marteau (anciennement Tartofrez) | date = 2013-01-24 | consulté le = 2020-04-16 }}.</ref>. Mais aussi, dans le cas du jeu de rôle, par la description de l’univers fictionnel (''{{lang|en|background}}'', cadre de jeu, diégèse), les éléments de l’univers créés par les joueurs et joueuses, la préparation de la partie (scénario le cas échéant), le matériel de jeu (dés, paravent, illustrations et aides de jeu, figurines, feuilles de personnage, fonctionnalités du réseau informatique utilisé)… Tout cela influe sur ce que vont se représenter les joueurs et joueuses et les décisions qu’elles vont prendre, sur ce qu’elles vont éprouver, et sur le déroulement de l’histoire. == Quelques outils == Lorsque nous prenons une décision en dehors du jeu — choix de lecture, achat de matériel, conception d’une règle, d’un scénario, préparation d’une partie, création d’un personnage, développement d’une région géographique… —, il faut donc s’intéresser à la répercussion qu’elle va avoir au cours de la partie. Nous présentons ci-dessous quelques outils qui permettent de bien considérer cet impact. === Le système selon Baker-Care === On peut ici introduire la notion de « système » définie par le principe de Baker-Care, ou principe de Lumpley (D. Vincent Baker, fondateur de Lumpley Games, et Emily Care Boss) : {{citation bloc | Le ''système'' (comprenant, mais pas limité aux “règles” [dans le sens traditionnel]) est défini comme étant le moyen par lequel les joueurs [y compris le MJ] se mettent d’accord sur les événements imaginés au cours de la partie. | 2 = Ron Edwards (2004)<ref>{{lien web | url = http://indie-rpgs.com/_articles/glossary.html | titre = The Provisional Glossary | auteur = Ron Edwards | site = The Forge | date = 2004-05-08 | consulté le = 2020-04-17 }}. Pour la traduction : {{lien web | url = https://ptgptb.fr/theorie-101-1ere-partie-le-systeme-et-l-espace-imaginaire-commun | titre = Théorie 101 - 1re partie : le système et l’espace imaginaire commun | auteur = M. J. Young (trad. Pierre Carmody) | site = PTGPTB | date = 2005 | consulté le = 2020-04-17 }}.</ref> | 3 = The Provisional Glossary }} Le jeu en tant que produit, diffusé sous la forme de livre papier, de document électronique, pourquoi pas de vidéo ou de bande son (probablement radio Web ou fichier MP3), est donc destiné à créer un système permettant de reproduire l’expérience de jeu telle qu’elle est conçue par son créateur ou créatrice. Libre aux joueuses de se l’approprier et d’en faire autre chose ensuite, mais au moins le produit doit permettre de reproduire cette expérience. Dans le jeu de rôle « traditionnel » le système regroupe donc ce qui a été évoqué ci-dessus : * le « livre de base » du jeu : texte de règles, illustrations, textes d’ambiance… * les éventuels suppléments, en général la description d’une partie du cadre de jeu : région de l’univers, bestiaire… * le scénario ; * le matériel : fiches de personnages, crayons et gommes, dés, figurines, paravent… * mais aussi les règles explicites et implicites au sein du groupe (par exemple la politesse et le respect), les interventions hors-jeu (commentaires, reproches, compliments, plaisanteries)… * le caractère de chacun·e des participant·es ; l’humeur de chacun·e, ce qui s’est passé avant la partie ; * les connaissances en dehors du jeu et notamment la connaissance des univers de science-fiction et de merveilleux ''(fantasy)'', ce qui forme une culture commune entre les joueur·euses, facilitant l’immersion dans le monde imaginaire ; * … Voir aussi : * {{lien web | url = https://awarestudios.blogspot.com/2012/05/le-principe-de-lumpley-redux.html | titre = Le principe de Lumpley, redux | site = Du bruit derrière le paravent | auteur = Grégory Pogorzelsky | date = 2012-05-14 | consulté le = 2020-05-01 }} '''Exemple : jet de perception''' Le jet de perception est une règle que l’on trouve couramment. Les ouvrages contiennent souvent des conseil pour indiquer à la MJ dans quelles circonstances elle doit la mettre en œuvre. Un scénario peut aussi mentionner d’y avoir recours à tel ou tel moment. La MJ peut aussi décider de faire faire un jet au débotté, ''ad hoc''. Règles, conseils, scénario, décision ''ad hoc'', le jet de perception se retrouve dans plusieurs niveau du système. '''Exemple : enquête policière''' Considérons un scénario de type enquête policière. Les personnages doivent collecter des indices puis les analyser pour avancer dans l’histoire (typiquement trouver le coupable). Mais que se passe-t-il s’ils ne trouvent pas les indices ? Cela pourrait mener à un blocage de l’histoire, à un échec de la mission. Ce problème peut être réglé par différents éléments du système : cadre de jeu, scénario, règles ou décisions ''ad hoc'' du meneur ou de la meneuse de jeu. Par le cadre de jeu : supposons que les personnages soient des policiers dans un commissariat miteux et manquant de moyens, ambiance ''[[w:Capitaine Furillo|Hill Street Blues]]'' ou bien ''[[w:RoboCop|RoboCop]]''. Ils sont en permanence pressé par la hiérarchie et les crimes pleuvent. Le jeu est plus centré sur les problèmes humains, sociaux et politiques que par le fait de résoudre l’enquête en soi. S’ils échouent, c’est un peu plus de rancœur, de dénigrement par la population, de réprimandes par les chefs et le maire. Sans être forcément du « jouer pour échouer » ''({{lang|en|play to lose}})'', rater un indice et donc une enquête n’est pas forcément en problème vis-à-vis du plaisir de jeu. Par le scénario : un méthode classique consiste à prévoir trois manières de progresser<ref>{{lien web |url = http://ptgptb.fr/regle-des-trois-indices |titre = règle des trois indices |auteur = Justin Alexander |date = 2008 |consulté le = 2020-04-29 |site = PTGPTB }}.</ref>. On peut par exemple connaître le nom d’un suspect soit en interrogeant un témoin, soit en demandant à un indic, soit en trouvant un pochette d’allumettes portant le sigle du bar qu’il fréquente. On divise ainsi par trois les risques de passer à côté de l’indice. Par les règles : certain jeux prévoient que l’on fasse des jets de dés pour interroger un témoin, un complice ou trouver un objet caché. Il est toujours possible de rater un jet de dés, faut-il laisser reposer tout l’intérêt de l’histoire sur le hasard ? On peut proposer deux règles du jeu pour éviter ce problème : * un échec au jet ne signifie pas que l’action échoue mais qu’il survient une complication : les enquêteurs perdent du temps, ils éveillent l’attention d’une faction rivale, l’interrogatoire dérape et ils blessent le complice, ils se blessent en fouillant ou bien perdent leur téléphone dans la nature… * on ne fait pas de jet pour trouver un indice ; soit il suffit de demander car il se trouve facilement — témoins coopératif, objet en évidence —, soit il faut dépenser un « point d’enquête », représentant le fait que les moyens d’enquête sont limités, que la concentrations s’émousse au fur et à mesure ; on passe ainsi d’un jeu de hasard à un jeu de gestion de moyens : faut-il dépenser les points d’enquête tout de suite pour progresser ou bien les garder pour plus tard, pour d’autres indices ? On pourra se référer aux jeu utilisant le système ''[[w:Gumshoe (jeu de rôle)|Gumshoe]]'' de Robin D. Laws (2007). Par les décisions du meneur ou de la meneuse de jeu : il ou elle donne un coup de pouce discret aux joueuses pour les mettre sur la voie. '''Exemple : passé d’un personnage {{lang|en|''(background)''}}''' On joue un personnage adulte, parfois enfant, mais qui a déjà vécu avant la partie. Ce passé va servir de support à la joueuse : il donne une logique à la manière dont la joueuse va jouer ce personnage, les actions qu’il va faire, la manière dont il va interagir avec les autres personnages et l'environnement<ref name="EAFRM1989">Voir par exemple {{chapitre | prénom1=Coleman |nom1=Charlton |prénom2=Pete |nom2=Fenelon | titre chapitre=Expériences et antécédents familiaux |titre ouvrage=Manuel des personnages & des campagnes | traducteur=Michel Serrat | éditeur=Hexagonal |année=1989 |isbn=2-84188-000-1 | passage=68 }}.</ref>. C’est donc bien un élément du système. Parfois, on crée un historique sans y être contraint par les règles mais, par exemple, pour donner de l’épaisseur, justifier le métier, une capacité donnée, la raison pour laquelle il va à l’aventure. La mention de ce passé peut être totalement absent du livre de règles ou bien figurer sous forme de conseils aux joueuses<ref name="EAFRM1989" />. Parfois, l’historique est géré par des règles. Il peut s’agir de déterminer certaines capacités justifiées par un élément du passé, par un tirage aléatoire ou par un choix volontaire. Par exemple, dans ''Rolemaster'' 2{{e}} éd. (Coleman Charlton et Pete Fenelon, 1982), on tire des capacités spéciales justifiées par le passé du personnage<ref>{{chapitre | prénom1=Coleman |nom1=Charlton |prénom2=Pete |nom2=Fenelon | titre chapitre=Table des options historiques |titre ouvrage=Manuel des personnages & des campagnes | traducteur=Michel Serrat |éditeur = Hexagonal |année=1989 |isbn=2-84188-000-1 | passage=90-91 }}.</ref> ; dans ''Empire galactique''<ref>{{ouvrage | prénom1 = François | nom1 = Nedelec | prénom2 = Sylvie | nom2 = Barc | prénom3 = Jean-Charles | nom3 = Rodriguez | titre = Empire galactique | éditeur = Robert Laffont | année = 1987 }}.</ref>, l’histoire préliminaire (les études et le métier exercé) détermine les compétences initiales et dans ''SteamShadows''<ref>{{ouvrage | prénom1 = Camille | nom1 = Claben | prénom2 = Delphine | nom2 = Denocq | prénom3 = Christophe | nom3 = Dénouveaux | prénom4 = Frédéric | nom4 = Dorne | prénom5 = Nicolas | nom5 = Pierre | prénom6 = Laura | nom6 = Pierre | titre = SteamShadows | éditeur = JdR Éditions | année = 2014 | isbn = 978-2-9540908-5-6 }}.</ref>, le personnage se construit en faisant des choix d’historique des arbres narratifs, chaque choix ayant un coût ; ou encore le système d’avantages et désavantages de ''[[w:GURPS|GURPS]]'' (Steve Jackson, 1986) prévoit des éléments comme « allégeance » ou « personne à charge » qui impliquent des relations sociales passées. Parfois, l’historique est le cœur de la création du personnage. Par exemple, dans ''[[w:Fiasco (jeu)|Fiasco]]'' (Jason Morningstar, 2009), la création de la partie consiste à définir des liens entre les différents personnages. Certains jeux prévoient la création du groupe de PJ en tant que tel ; on crée ainsi un historique commun qui fait du lien. Par ailleurs, cela répond à un élément fondamental de la partie : comment les PJ se sont rencontré, que font-ils ensemble ? C’est le cas par exemple de ''[[w:Tenga (jeu de rôle)|Tenga]]'' (Jérôme Larré, 2011)<ref>{{ouvrage | prénom1 = Jérôme | nom1 = Larré | titre = Tenga | éditeur = John Doe | année = 2011 | passage = 34-42 }}.</ref> et de ''Homeka'' (Célia Chen et coll., 2017)<ref>{{ouvrage | prénom1 = Célia | nom1 = Chen | prénom2 = Delphine | nom2 = Denocq | prénom3 = Frédéric | nom3 = Dorne | prénom4 = Gabriel | nom4 = Glachant | titre = Homeka | éditeur = JdR éditions | année = 2017 | isbn = 979-10-94445-19-8 }}.</ref>. On peut considérer deux situations « extrêmes » : * une joueuse choisit un personnage archétypal et n’envisage rien quant à son passé ; cette manière de faire permet de jouer très rapidement, le personnage est tout à fait fonctionnel, il est défini de manière « existentielle<ref>Sur cette notion, voir la vidéo de {{lien web | url=https://www.youtube.com/watch?v=LoRUdun9h1s | titre=<nowiki>[</nowiki>Colloque<nowiki>]</nowiki> Personnage et Personnalité | auteur=Frederic Ferro | site=Youtube |date=2015-11-18 |consulté le=2020-05-02 }}, de 15 min 03 s à 20 min 22 s.</ref> » c’est-à-dire que c’est ce qu’il fait qui le définit ; la joueuse peut définir des éléments de son passé en cours de partie ; c’est la situation qui est décrite, caricaturée dans la saga MP3 ''Le Donjon de Naheulbeuk''<ref>{{lien web |url=http://www.penofchaos.com/warham/donjon.htm |titre = Le Donjon de Naheulbeuk |site=Pen of Chaos |auteur=John Lang |consulté le=2020-05-02 }}.</ref> ; * la joueuse imagine un passé riche et écrit cinq pages d’historique ; le personnage a une épaisseur, des raisons d’agir ; cependant, il est probable que la table — la meneuse de jeu et les autres joueuses — n’assimileront que peu d’éléments et donc seule une faible partie de l’historique sera utilisée concrètement en jeu ; il s’agit donc pour une grande partie d’un plaisir d’écrivain plus que d’élément utiles concrètement en jeu. Les deux situations sont légitimes et on peut envisager toutes les nuances entre ces deux situations. Une manière de faire consiste à se concentrer sur des éléments qui vont être facilement investis dans la partie, typiquement des relations avec des PJ ou des PNJ, des secrets. On pourra lire par exemple ''Avant la partie'' de Coralie David<ref>{{harvsp|David|2016|p=236-240}}.</ref> et ''Rendre les choses personnelles'' de Gregory Pogorzelski<ref>{{harvsp|Pogorzelski|2016}}.</ref>. En conclusion : en tant que manière dont la joueuse imagine son personnage, l’historique fait partie du système mais il peut en faire « encore plus partie » si des éléments de l’historique sont pris en compte par les autres joueuses. C’est plus facile si : * l’élément historique s’accompagne d’un effet « mécanique » sur le jeu comme une capacité spéciale ou un objet utile au cours de la partie ; * l’élément historique est un lien avec un PNJ ou une faction ; en particulier, la MJ devrait prévoir l’intervention de ces éléments en cours de partie, en particulier s’il s’agit d’un lien « désavantage » (allégeance, personne à charge) — dont le choix s’est accompagné d’une contrepartie, faute de quoi cette contrepartie serait gratuite — ou d’un lien « avantage » (ressource) — faut de quoi la joueuse se sentirait « trahie » d’avoir choisi un avantage qui n’en est pas un ; * il y a peu d’éléments dans l’historique ; certains éléments peuvent même être inventés en cours de partie ou bien être élaborés en commun. Voir aussi : * {{lien web | url = https://awarestudios.blogspot.com/2012/05/les-backgrounds-rallonge.html | titre = Les backgrounds à rallonge | site = Du bruit derrière le paravent | auteur = Grégory Pogorzelsky | date = 2012-05-10 | consulté le = 2020-05-01 }} {{note|Le mot « système » peut être utilisé avec un sens plus restreint, y compris au sein de la communauté de ''The Forge'' dont font partie Vincent D. Baker et Emily Care Boss. Par exemple, dans l'article de Ron Edwards ''{{lang|en|System does matter}}'' (1998)<ref>{{lien web | url = http://ptgptb.fr/le-systeme-est-important | titre = Le système ''est'' important | auteur = Ron Edwards | site = Places to go, people to be | date = 1998 | consulté le = 2018-04-13 }}</ref>, le mot « système » désigne la façon dont les joueurs et joueuses résolvent ce qui se passe dans la partie ; la résolution de situations (sens de Edwards) n'étant qu'une partie de la construction de la fiction (sens de Baker-Care). Et pour un certain nombre de joueurs et joueuses, le mot « système » désigne uniquement les règles écrites (RAW, ''{{lang|en|rules as written}}''). Lorsque l'on utilise le mot « système de jeu de rôle », il convient donc de bien définir ce que l'on entend par là.}} === SHID === Le jeu de rôle c’est plein de choses, et on pourra se référer au chapitre précédent ''[[../Qu'est-ce que le jeu de rôle ?/]]'', mais c’est en particulier : un jeu de société consistant à vivre une histoire en s’immergeant dans un monde imaginaire et en relevant des défis. * S, un jeu de société : on en a touché quelques mots ci-dessus, la notion de jeu n’est pas évidente à définir mais on pourra retenir en particulier qu’il s’agit d’une activité faite ''volontairement'' et ''pour le plaisir'' ; en dehors des jeux de rôle solo, il s’agit d’un jeu pratiqué à plusieurs et les interaction entre les joueurs et joueuses, les interactions sociales, sont une part importante ; on peut par exemple retenir les notions de coopération et de compétition, le partage de la parole, le fait que chacun et chacune contribue à la partie et y retire un plaisir… * H, histoire : on attend des événements de jeu qu’ils soient liés entre eux par une certaine logique, qu’il y ait des relations de cause à effet ; à la fin, le récit des événements fictif constituent une histoire, ce qui ne veut pas dire que cela ferait un livre ou un scénario de BD ou de film exploitable ; * I, immersion dans le monde imaginaire : « imaginaire » signifie qu’il est imaginé, qu’il est présent essentiellement dans l’imagination des joueurs et joueuses même si l’histoire se déroule dans le « monde réel » ; * D, défis : il s’agit de jeux dans le jeu : jeux de hasard, résolutions d’énigmes, jeux tactiques (par exemple combat), jeu d’improvisation théâtral… Partant de là, le système sert principalement à quatre choses : permettre à chacun et chacune de participer, faire avancer l’histoire (comment évolue une situation et quelle sera la situation suivante), créer une immersion dans le monde imaginaire (s’y croire), proposer des défis à relever avec des enjeux et une opposition (créer du jeu). Cette description SHID du jeu de rôle ressemble à la « théorie LNS » (ou GNS) et en est évidemment inspirée mais elle diffère dans sa finalité : il ne s’agit pas de comprendre les attentes des joueurs et joueuses mais d’avoir un guide de réflexion sur les différents éléments du jeu. Nous avons utilisé des dénominations différentes à la fois pour qu’il n’y ait pas de confusion avec la théorie LNS mais aussi pour lever quelques ambiguïtés sur certains termes (en particulier « narrativisme » et « simulationnisme »). '''Exemple : jet de perception''' Regardons la notion de jet de perception avec la loupe SHID : * S (jeu de société) : il s’agit d’un jeu de hasard ; il n’y a ici pas de notion de collaboration ou de compétition, la composante S n’est ''a priori'' pas impliquée ; * H (histoire) : comment va évoluer l’histoire si l’on réussit, si l’on échoue ? Les deux possibilités ont-elles leur intérêt — si l’on détecte une embuscade, on est content d’y échapper, si on ne la détecte pas, cela rajoute du piment au combat — ou bien l’une d’elle est-elle nuisible — si l’on passe à côté d’un indice et que cela bloque l’histoire par exemple (voir ci-dessus) ? * I (immersion, monde imaginaire) : d’un point de vue vraisemblance, remarquer ou pas un détail a une composante chance ; demander un jet peut faire sortir de l’immersion — puisque la joueuse est rappelée dans le monde réel pour se saisir des dés — mais peut aussi la renforcer — en reproduisant la notion de chance présente dans le monde imaginaire ; * D (défis) : il s’agit d’un jeu de hasard, cela peut être apprécié s’il y a un enjeu connu, on attend alors fébrilement que le dé s’arrête de rouler pour savoir si l’on réussit ou si l’on échoue ; mais cela s’applique-t-il dans le cas d’un jet de perception ? Rien n’est moins sûr, surtout s’il est caché des joueuses. Ainsi, en considérant un élément du système — règle du jeu, élément du cadre de jeu (monde imaginaire), élément du scénario, décision de la meneuse de jeu… — sous les aspects SHID, on s’oblige à considérer cet élément au cœur de la partie et cela permet d’évaluer sa pertinence. Cela peut mener à des situations très différentes. Considérons par exemple une meneuse de jeu qui a recours abondamment aux jets de perception. Elle peut justifier cela par : * le fait que ça maintient la tension nerveuse et donc favorise l’identification de la joueuse avec son personnage (I) puisqu’elle éprouve le même sentiment que son PJ ; par exemple, s’il s’agit d’apercevoir un bâtiment dans le lointain alors que l’on s’approche, un jet réussi permettrait de voir le bâtiment plus tôt ; même si cela n’a pas d’importance, les joueuses peuvent toujours se demander si elles réussissent le jet : que ce serait-il passé si j’avais raté le jet ? Et à l’inverse, si elles le ratent, elles se demandent ce qu’elles ont pu ne pas remarquer ; * cela oblige les joueuses à faire des choix lorsqu’elles créent et font monter l’expérience de leur personnage : la compétence de perception entre en concurrence avec d’autres, la joueuse doit donc faire un pari sur l’avenir (D), la perception sera-t-elle plus utile que d’autres compétences ? À l’inverse, une MJ peut bannir les jets de perception. En effet, chaque jet interrompt la narration (H) puisque l’on met en œuvre une procédure (lire une valeur sur la fiche de personnage, jeter les dés, comparer le résultats des dés à la valeur, décider des conséquences du jet) ; cette interruption peut aussi rappeler la joueuse à la réalité (I). '''Exemple : enquête policière''' * S : la recherche et l’interprétation des indices est une activité collaborative ; * H : l’enquête est le cœur de l’histoire ; * I : la joueuse doit réfléchir comme le ferait son personnage en prenant en compte les éléments du monde imaginaire ; elle suit des procédures réglementaires internes à ce monde ; * D : la résolution d'une énigme. '''Exemple : passé d’un personnage {{lang|en|''(background)''}}''' * S : l’historique d’un personnage crée du jeu « inter-joueuses » s’il contient des relations entre PJ ou des éléments communs au groupe ; * H : l’historique contribue à l’histoire s’il contient des liens avec des PNJ ou des factions ; il peut s’agir du moteur d’un arc narratif — une allégeance ou un ennemi est à l’origine de l’intrigue, une personne à charge ou une personne proche est en danger — ou d’éléments adjuvants — un indicateur, un contact dans la pègre ou une personne redevable donne un coup de pouce ; * I : l’historique ancre le PJ dans le monde imaginaire ; * D : des liens forts peuvent constituer un enjeu fort pour des défis ; ils peuvent eux-même constituer des défis, comme par exemple la nécessité de protéger une personne ou bien progresser socialement au sein d’un groupe comme les sociétés secrètes de ''Paranoïa'' (Greg Costikyan et coll., 1984), en particulier si cela est en concurrence avec d’autres objectifs. === Le jeu de rôle au petit déjeuner === Ron Edwards a énoncé un principe connu sous le sobriquet du « truc impossible avant le petit-déj’ »<ref>{{lien web | auteur = M. Joseph Young | titre = Théorie 101 - 2<sup>e</sup> partie : Le Truc impossible avant le petit-déj’. Qui a le contrôle de l'histoire ? | url = http://ptgptb.free.fr/0027/th101-2.htm | site = Places to go, people to be | date = 2005 | consulté le = 2020-05-02 }}</ref> : si les PJ sont les personnages principaux de l’histoire, la narration devrait être entre les mains des joueuses les incarnant. Même dans les jeux avec MJ, des éléments importants devraient être de la responsabilité des joueuses. On peut concevoir cela de deux manières, non exclusives. En premier, la préparation de la partie, le scénario, devrait laisser un large marge de manœuvre aux initiatives des joueuses ; la MJ ne devrait pas tenter de ramener le groupe « sur les rails », et même accepter que les actions des PJ puissent bouleverser le monde servant au cadre de jeu (faire et défaire des rois et pourquoi pas déboucher sur une apocalypse). En second, les joueuses devraient pouvoir injecter dans le jeu des éléments qui leur plaisent. Concernant le premier point, lorsque la MJ conçoit des situations, elle doit bien sûr prévoir au moins une solution pour ces situations, à moins de vouloir volontairement mettre les PJ dans une impasse ; mais elle doit accepter que les joueuses fassent autrement, qu’elles trouvent une autre solution que celles prévues. Pour passer de l’étape A à l’étape B, les PJ peuvent combattre l’adversité, mais elles peuvent aussi la contourner, la tromper, négocier avec elle, négocier avec une faction rivale de l’adversité… Concernant le second point, si les joueuses décident de se regrouper pour jouer, cela implique qu’elles aient des envies communes sur ce qui va se passer au cours de la partie (cela recroise le S de SHID). Cela concerne le choix de l’univers — plutôt réaliste ou fantastique, époque médiévale, contemporaine ou futuriste, licence (univers d’un autre média comme ''Star Wars'') — mais aussi le choix du type d’histoire — aventure, enquête, environnement dur (''{{lang|en|gritty}}'', « réaliste ») voire désespéré (survie, horreur) — et les thèmes abordés — légèreté ou gravité. Mais les joueuses peuvent aussi contribuer à l’élaboration du cadre de jeu ; certains jeux prévoient une première étape de création du monde, comme ''Apocalypse World'' (D. Vincent Baker et Meguey Baker, 2010) ou ''Sombre'' (Johan Scipion, 2011). Un des avantages de cette méthode est que comme les éléments viennent des joueuses, ils sont déjà assimilés, il n’y a pas besoin de les leur présenter. Cela ne doit pour autant pas émousser d’autres sources de plaisir d’une partie de jeu de rôle comme l’effet de surprise et le fait de surmonter une adversité, comme l’énonce le principe de Paul Czege : le joueur qui définit un obstacle ou un adversaire n'est pas celui qui est chargé de le surmonter. On peut aussi considérer les bacs à sable : un cadre de jeu dans lequel rien ne contraint les personnages de faire ci ou ça — pas de contrainte par une hiérarchie, pas de mission divine ni de quête, pas de recrutement par un commanditaire dans une auberge, pas d’appât, pas d’appel à l’aide d’un lointain cousin —, ce sont les joueuses qui sont totalement motrices de l’action, qui choisissent ce que les PJ vont faire. C’est un mode de jeu appelé « simulationniste »<ref>Lire la section ''Création d’aventure'' sur {{lien web | titre = Clarification du simulationnisme dans le modèle à trois volets | auteur = John H. Kim | url = http://ptgptb.fr/clarification-du-simulationnisme | site = PTGPTB | date = 2004 |consulté le = 2020-05-09 }}.</ref>. Pour autant, à moins d’avoir des joueuses très actives — on utilise parfois le terme abusif « proactives » —, il y a un risque réel que la partie s’enlise. Les PJ doivent avoir quelque chose à faire en temps normal — par exemple surveiller et explorer un territoire dans ''Oltréé !''<ref>{{ouvrage | prénom1 = John | nom1 = Grümph | titre = Oltréé ! | éditeur = Les XII Singes | année = 2013 | isbn = 978-2-91689819-3 }}.</ref> — et le cadre doit receler des intrigues, libre aux joueuses de suivre telle ou telle intrigue. Elles doivent avoir des éléments à disposition pour savoir ce qui se trame<ref>{{lien web | titre = Les Bacs à sable | url = https://ptgptb.fr/les-bacs-a-sable | auteur = Joseph Bloch | site = PTGPTB | date = 2014 | consulté le = 2020-05-09 }}.</ref>{{,}}<ref>{{lien web | titre = Lancer ou relancer un bac-à-sable | auteur = Acritarche | url = https://acritarche.tumblr.com/post/143889335628/lancer-ou-relancer-un-bac-%C3%A0-sable | site = Contes des ères abyssales | date = 2016-05-05 | consulté le = 2020-05-09 }}.</ref>. '''Exemple : jet de perception''' Ce point-là ne nous semble pas pertinent à étudier à la lumière du « truc impossible avant le petit-déj’ ». '''Exemple : enquête policière''' Le mode « jeu de piste » où les indices s’enchaînent dans un ordre donné A → B → C… est contraignant pour les joueuses. Celles-ci devraient avoir le choix de la manière de résoudre l’affaire et dans l’ordre d’aborder les situations. '''Exemple : passé d’un personnage (background)''' Les joueuses devraient avoir une large responsabilité dans la conception du passé et ce passé devrait avoir un réel effet en jeu, faute de quoi leurs choix auraient été vains. {{note|Le principe de Czege — séparer la définition de la résolution de l'adversité — n'est pas un principe absolu et a d'ailleurs été remis en question par son auteur depuis sa conception en 2000. Par exemple, dans les jeux propulsé par l’''Apocalypse'' (''powered by the'' Apocalypse, pbtA), les joueurs et joueuses choisissent les complications — et donc les obstacles — qui surviennent lorsqu’un jet est défavorable.}} === Mimétisme et fonctionnalité === Les PJ sont les « outils » qui permettent aux joueuses de vivre l’aventure, de percevoir l’environnement et d’agir sur lui. Isabelle Périer relève qu'un personnage a deux dimensions<ref>{{lien web | url = https://www.youtube.com/watch?v=_7fNHVYTPts | titre = Le jeu de rôle : une autre forme de littérature de jeunesse ? | auteur = Isabelle Périer | date = 2014-09-24 | consulté le = 2020-04-02 | site = Youtube }}, de 20 min 42 s à 22 min 10 s. }}</ref> : une dimension mimétique, c’est-à-dire qu’il « imite » une personne réelle, il a un nom, un métier, un statut social, une apparence, une manière de s’exprimer ; et une dimension fonctionnelle, c’est-à-dire qu’il « fonctionne » dans le monde, qu’il a des capacités d’action en général cadrées par des règles comme les « caractéristique » et les « compétences ». Ceci est en fait valable pour tous les éléments du monde imaginaire. Une arme, une pièce d'or, un mur, ont une dimension mimétique — une apparence, un poids, une texture au toucher, une odeur… — et une dimension fonctionnelle, des règles y sont attachées — combien de dégâts je fais avec une épée, combien de points d'expérience me rapporte la récolte d'une PO, quelles probabilités j’ai d’escalader le mur. '''Exemple : jet de perception''' La partie mimétique est importante pour ce qui est de l’identification au personnage et de l’immersion dans le monde imaginaire. Cela va passer par l’utilisation de tous les sens et pas seulement de la vue ; en particulier, on commence à percevoir des sons et des odeurs d’un phénomène avant de le voir, la peau est sensible à la chaleur et à l’humidité, les surfaces ont une texture (lisse, rugueuse, collante, humide, chaude ou froide, spongieuse…). On va d’abord percevoir des changements rapides comme un mouvement dans le coin du champ de vision ou un éclat ; on va d’abord percevoir un apparence générale — carrure et démarche d’un personnage, vague forme et couleur d’un objet ou d’un bâtiment, [[w:perspective atmosphérique|perspective atmosphérique]] et première impression. La partie fonctionnelle est la règle du jeu. '''Exemple : enquête policière''' Les éléments de l’enquête — indices, témoins, indics — sont mimétiques : ce sont des éléments « réels » à l’intérieur du monde imaginaire et pour que les joueuses sachent comment gérer l’affaire, il serait intéressant que ces éléments soient proches de ce que connaissent les joueuses (de la réalité ou des fictions : romans, film, séries policières). Faut-ils que ces éléments soient fonctionnels ? Pas obligatoirement, progresser dans l’enquête, c’est essentiellement une démarche intellectuelle, des réflexions et déductions dans la tête des joueuses. Mais les éléments peuvent avoir une fonction dans l’univers : par exemple, posséder tel élément fournit un bonus pour résoudre telle situation. Si le groupe enquête pour préparer une infiltration, les éléments collectés peuvent constituer une réserve de points permettant d’affronter l’adversité, ces points représentant de manière abstraite la préparation (penser à emporter tel ou tel matériel). '''Exemple : passé d’un personnage (background) ''' Le passé du personnel est évidemment mimétique. Il peut aussi être fonctionnel : par exemple, telle expérience permet de mieux gérer une situation (procurant ainsi un bonus), tel lien permet d’avoir accès à une ressource (ami pouvant rendre un service ou prêter du matériel, PNJ ayant une dette envers un PJ). On peut aussi à l’inverse « mimétiser » une fonctionnalité : le PJ possède une compétence et la joueuse invente un élément historique pour le justifier. Certains jeux mêlent mimétisme et fonctionnalité, comme par exemple dans ''Les Errants d'ukiyo''<ref>{{ouvrage | prénom1 = Vivien « Faesson » | nom1 = Féasson | titre = Les Errants d'ukiyo | éditeur = Icare | année = 2012 | isbn = 978-2-917475-79-9 }}.</ref> : les PJ ont des « techniques » qui sont une phrase descriptive associée à une valeur ; du point de vue mécanique, c’est l’équivalent de compétences mais s’appliquant à toutes les situations en rapport avec la phrase descriptive. Dans ''[[w:SimulacreS|Simulacres]]''<ref>Pierre Rosenthal, ''Simulacres'', auto-édité, 1986. Plusieurs versions imprimées, par exemple la version 7 du jeu dans le hors-série n<sup>o</sup> 10 de ''Casus Belli'', Excelsior éditions, mars 1994. Le jeu ''Capitaine Vaudou'' utilisera la version 8 du système et devrait être publié en 2020 par Monolith Board Games et Black Book Éditions.</ref>, les PJ ont un métier et un hobby qui donnent un bonus à toutes les situations entrant dans ces périmètres. == Bibliographie == * {{chapitre | id=David2016 | libellé = David 2016 | titre chapitre = Rassembler et diviser | prénom1 = Coralie | nom1 = David | titre ouvrage = Mener des parties de jeu de rôle | collection = Sortir de l'auberge | éditeur = Lapin Marteau | lieu = Saint-Orens-de-Gameville | année = 2016 | isbn = 978-2-9545811-4-9 | passage = 235-259 }} * {{chapitre | id=Pogorzelski2016 | libellé = Pogorzelski 2016 | titre chapitre = Rendre les choses personnelles | prénom1 = Gregory | nom1 = Pogorzelski | titre ouvrage = Mener des parties de jeu de rôle | collection = Sortir de l'auberge | éditeur = Lapin Marteau | lieu = Saint-Orens-de-Gameville | année = 2016 | isbn = 978-2-9545811-4-9 | passage = 261-275 }} * {{lien web |id=Gilbert2022 |libellé = Gilbert 2022 |prénom1=Levi |nom1=Gilbert |titre=Situational Design for TTRPG Adventures |site=levigilbert.dev |url=https://levigilbert.dev/posts/situationalDesignForTTRPGAdventures/ |date=2022-05-25 |consulté le=2025-11-25 }} == Notes == {{références|colonnes=2}} ---- ''[[../Préparer une partie/]]'' &lt; [[Jeu de rôle sur table — Jouer, créer|↑]] &gt; ''[[../Et en dehors des parties…/]]'' [[Catégorie:Jeu de rôle sur table — Jouer, créer (livre)|Au coeur du jeu de role la partie]] odrh25fup64h12x1t9geq5tg94hjr8a Le mouvement Wikimédia/L'utopie Wikimédia 0 79253 764074 763851 2026-04-20T07:57:31Z Lionel Scheepmans 20012 764074 wikitext text/x-wiki <noinclude>{{Le mouvement Wikimédia}}</noinclude> Pour mieux comprendre pourquoi Wikipédia et le mouvement Wikimédia sont perçus comme une utopie et même par certain comme « la dernière utopie collective du Web »<ref>{{Lien web|langue=fr-FR|nom1=Dupont-Besnard|prénom1=Marcus|titre=Wikipédia, la dernière utopie collective du web ?|url=https://web.archive.org/web/20250516050814/https://www.numerama.com/tech/805447-wikipedia-la-derniere-utopie-collective-du-web.html|site=Numerama|date=2021-12-29|consulté le=2025-12-21}}</ref>, voici une métaphore dans laquelle la partie numérique du mouvement Wikimédia est imaginée tel un immense quartier au sein de l'espace erpésenté en tant que ville. Une ville dans laquelle Internet serait le réseau routier, tandis que les serveurs informatiques qui hébergent les sites web constitueraient les batiments. Dans cette ville numérique le quartier Wikimédia rassembleraint alors près d’un millier d’édifices que l’on peut visiter librement et gratuitement. A l’exception de quelques bâtiments administratifs, non seulement on peut explorer tout cela librement et gratuitement, mais en plus, on peut aussi y modifier presque tout ce qui s’y trouve. On peut y apporter de nouvelles informations, sous forme de texte, photo, vidéo et document sonore, ou encore, si l’on le veut, ranger les informations apportées par d’autres personnes, afin de rendre leurs présentations plus esthétiques ou plus compréhensibles. D’une manière plus incroyable encore, on peut même faire disparaitre l’entièreté de ce qui est présent dans une pièce. Suite à quoi, un programme informatique remettra tout en place, avant de demander gentiment d’éviter ce genre de vandalisme. Concernant les actions plus discrètes et non détectées par les robots, l'une des personnes qui ont enrichi ou enjolivé la pièce, viendra certainement annuler les modifications malveillantes avant de contacter l'auteur. Et si c'est un multirécidiviste, il sera alors privé du droit de modification du bâtiment qu'il a vandalisé, et même dans tout le quartier si cela se justifie. Une décision qui, par ailleurs, sera toujours mise en application par un des volontaires administrateurs choisis par la communauté des bénévoles actifs au sein des projets. [[Fichier:The Digital City, Riyadh 191957.jpg|vignette|<small>Figure 3. Photo de la ''Digital City'' de [[w:Riyad|Riyadh]] et son aspect visuel en lien avec la métaphore du quartier Wikimédia.</small>|300x300px]] On comprend donc que tout le monde peut enrichir, mais aussi surveiller et protéger les richesses partagées dans le quartier Wikimédia. Il suffit pour cela de rejoindre le mouvement en se créant un compte et de profiter, entre autres, d'un système de notification qui poste un avertissement à chaque fois qu'une des pièces que l'on désire surveiller au sein des bâtiments Wikimédia est modifiée. Pour la création de ce compte, pas besoin de fournir une adresse ou un numéro de téléphone. Les seules informations personnelles indispensables au bon fonctionnement du quartier Wikimédia sont les [[w:Adresses_IP|adresses IP]] des ordinateurs connectés. Car contrairement à ce qui se passe dans les quartiers commerciaux de la grande ville numérique, tels que les [[w:GAFAM|GAFAM]], [[w:NATU_(Netflix,_Airbnb,_Tesla_et_Uber)|NATU]], [[w:BATX|BATX]] ou autres, aucune des informations récoltées lors des visites des bâtiments Wikimédia n’est exploitée par des services de marketing. Même les adresses IP enregistrées par le système ne sont pas visibles par les autres visiteurs. Elles sont remplacées par les noms et les pseudonymes fournis lors de la création des comptes, ou masquées par des comptes temporaires en cas de non-connexion. Seules quelques personnes accréditées par la communauté pour effectuer des contrôles d’usurpation d’identité ont accès à ces informations<ref>{{Lien web|auteur=MédiaWiki|titre=Produit de confiance et de sécurité/Comptes temporaires|url=https://web.archive.org/web/20250813140004/https://www.mediawiki.org/wiki/Trust_and_Safety_Product/Temporary_Accounts/fr}}.</ref>. C’est là une précaution nécessaire au bon déroulement des prises de décisions par recherche de [[w:Consensus|consensus]] organisées au sein du quartier Wikimédia. Dans la ville numérique que constitue le Web, Wikimédia apparait ainsi comme le plus grand quartier dédié au partage de la connaissance. La partie la plus connue du quartier, [[w:Wikipédia:Accueil_principal|Wikipédia]], est composée de plus de 350 bâtiments encyclopédiques répertoriés par langues. À côté de ceux-ci, et toujours séparés en versions linguistiques, se trouvent les bibliothèques [[:en:fr:accueil|Wikilivres]] et [[s:fr:Wikisource:Accueil|Wikisource]], les bâtiments lexicaux multilingues [[wikt:fr:Wiktionnaire:Page_d’accueil|Wiktionnaire]], ce centre journalistique [[n:fr:accueil|Wikinews]], le centre pédagogique et de recherche [[v:fr:accueil|Wikiversité]], le centre d'informations sur les voyages [[voy:fr:accueil|Wikivoyage]], le répertoire des êtres vivants [[species:main page|Wikispecies]] et l'institut des citations d’auteurs [[q:fr:accueil|Wikiquote]]. Cela sans oublier [[commons:main page|Wikimedia Commons]], un lieu de collecte pour tous les fichiers médiatiques, et [[wikidata:wikidata:main_page|Wikidata]], cette énorme banque d’informations structurées, dont la fonction, au même titre que Wikimedi Commons, est d’enrichir le contenu des autres buildings. 50 % de ces bâtiments sont constitués d'étages dédiés à la libre organisation technique et politique des projets. Tandis que plusieurs immeubles du quartier, tels que [[mw:main page|MediaWiki]], [[wikitech:Main_Page|Wikitech]], [[w:fr:phabricator|Phabricator]], sont entièrement dédiés à sa maintenance technique. Vient ensuite [[metawiki:main page|Méta-Wiki]], le centre administratif dédié à l’organisation et la gouvernance générale de l'ensemble du quartier. Puis le bâtiment [[otrswiki:Main page|Wikimedia VRT]], où l'on traite les courriers adressés au quartier, et finalement [[outreach:main page|''Wikimedia outreach'']], le centre de sensibilisation et de recrutement de nouveaux bénévoles. En découvrant l’existence de ce vaste quartier numérique libre d’accès, de participation et modification, on visualise mieux, à nouveau, comment les activités du mouvement Wikimédia dépassent largement ce qui se passe au sein du projet Wikipédia. Même si à lui seul, ce projet est déjà perçu comme « une utopie en marche »<ref>{{Article|langue=|prénom1=Christian|nom1=Vandendorpe|titre=Le phénomène Wikipédia: une utopie en marche|périodique=Le Débat|volume=148|numéro=1|éditeur=Gallimard|date=2008|issn=0246-2346|pages=17}}.</ref> ou « réalisée »<ref>{{Ouvrage|prénom1=Théo|nom1=Henri|directeur1=|titre=Wikipédia : une utopie réalisée ?|lieu=Université de Poitier|date=juillet 2013|pages totales=98|lire en ligne=https://web.archive.org/web/20211103122912/https://www.seies.net/sites/theo/doc/HENRI_theo_-_master_1_-_memoire.pdf}}.</ref>, il fait cependant partie d'une organisation mondiale bien plus vaste, quasi gérée uniquement par des bénévoles et de manière non lucrative. Une utopie bien plus grande donc, dont il est bon de comprendre les origines. Comme première explication, il y a cette synchronicité entre le chamboulement culturel provoqué par la [[w:Contre-culture_des_années_1960|contre-culture des années 1960]] et les débuts de la révolution numérique. Les chercheurs et étudiants en informatique, pionniers des réseaux et de leurs applications, étaient effectivement fortement influencés par ce changement de paradigme, qui fut symbolisé en France par les événements de [[w:mai_68|mai 68]]. De cette impulsion naîtront ainsi une philosophie et un mode d’organisation des activités tout à fait spécifiques, dont le mouvement Wikimédia deviendra l'héritier. Aujourd’hui, et à l'image du projet Wikipédia en français<ref>{{Lien web|auteur=Wikipédia|titre=Wikipédia:Règles et recommandations|url=https://web.archive.org/web/20251008105309/https://fr.wikipedia.org/wiki/Wikip%C3%A9dia:R%C3%A8gles_et_recommandations|date=|consulté le=}}.</ref>, les sites web hébergés par la Fondation Wikimédia contiennent de nombreuses [[w:Wikipédia:Règles_et_recommandations|règles et recommandations]] qui s'apparentent fortement à des lignes éditoriales. Parallèlement à cela, un [[foundation:Policy:Universal_Code_of_Conduct/fr|code de conduite universel]] fut adopté par la Fondation Wikimédia en 2022, dans le but d’établir un « référentiel minimum des comportements acceptables et inacceptables »<ref>{{Lien web|titre=Policy:Universal Code of Conduct/fr|url=https://web.archive.org/web/20251007061014/https://foundation.wikimedia.org/wiki/Policy:Universal_Code_of_Conduct/fr|site=|date=|consulté le=|auteur=Wikimedia Foundation Governance Wiki}}.</ref>. Cependant, dès la création de l'encyclopédie libre qui fut un jour qualifiée de « bazar libertaire »<ref>{{Lien web|langue=|auteur=Frédéric Joignot|titre=Wikipédia, bazar libertaire|url=https://web.archive.org/web/20170630065818/http://www.lemonde.fr/technologies/article/2012/01/14/wikipedia-bazar-libertaire_1629135_651865.html|site=Le Monde|lieu=|date=2012|consulté le=}}.</ref>, un principe fondateur intitulé [[w:Wikipédia:Interprétation_créative_des_règles|interprétation créative des règles]]<ref>{{Lien web|langue=|auteur=Wikipédia|titre=Wikipédia: Interprétation créative des règles|url=https://fr.wikipedia.org/wiki/Wikip&#233;dia:Interpr&#233;tation_cr&#233;ative_des_r&#232;gles|consulté le=}}.</ref>, toujours présent à ce jour, illustre clairement l’orientation politique et culturelle du projet : <blockquote> N’hésitez pas à contribuer, même si vous ne connaissez pas l’ensemble des règles, et si vous en rencontrez une qui, dans votre situation, semble gêner à l’élaboration de l’encyclopédie, ignorez-la ou, mieux, corrigez-la. </blockquote> Cette invitation illustre à elle seule les valeurs d’universalité, de liberté, de décentralisation, de partage, de collaboration et de mérite décrites par [[w:fr: Steven Levy|Steven Levy]] dans son ouvrage ''[[w:L'Éthique des hackers|L’Éthique des hackers]]''<ref>{{Ouvrage|langue=|prénom1=Steven|nom1=Levy|prénom2=Gilles|nom2=Tordjman|titre=L'éthique des hackers|éditeur=Globe|date=2013|isbn=978-2-211-20410-1|oclc=844898302}}.</ref>. Bien avant l’apparition du mouvement Wikimédia, une nouvelle perception du monde a donc rendu possible le développement d'un environnement numérique, ouvert, libre et gratuit, propice à la création d'une encyclopédie, écrite et gérée par des millions de contributeurs et contributrices situés aux quatre coins du monde. Cela commence par [[w:Internet|Internet]], un réseau mondial de communication en libre accès, puis le développement du ''[[w:World_Wide_Web|World Wide Web]]'', qui simplifie les interactions humaines à l’échelle planétaire, jusqu'à l'avènement des logiciels qui ont rendu possible la modification de sites web à l'aide d’un simple navigateur, pour former ce que l'on appelle aujourd’hui le [[w:Web_2.0|Web 2.0]]. Or, parmi ces logiciels se trouvent les [[w:fr:Moteur de Wiki|moteurs de Wiki]], dont le plus puissant d’entre eux, [[w:MediaWiki|MediaWiki]], est un [[Logiciels libres|logiciel libre]] développé par la Fondation Wikimédia, devenue par ce fait actrice au sein du [[w:Mouvement_du_logiciel_libre|mouvement du logiciel libre]].{{AutoCat}} n1bvy4j46ms8n5dasphintqjbymm4bu 764078 764074 2026-04-20T08:35:05Z Lionel Scheepmans 20012 764078 wikitext text/x-wiki <noinclude>{{Le mouvement Wikimédia}}</noinclude> Pour mieux comprendre pourquoi Wikipédia et le mouvement Wikimédia sont perçus comme une utopie et même comme « la dernière utopie collective du Web » par certains<ref>{{Lien web|langue=fr-FR|nom1=Dupont-Besnard|prénom1=Marcus|titre=Wikipédia, la dernière utopie collective du web ?|url=https://web.archive.org/web/20250516050814/https://www.numerama.com/tech/805447-wikipedia-la-derniere-utopie-collective-du-web.html|site=Numerama|date=2021-12-29|consulté le=2025-12-21}}</ref>, voici une métaphore dans laquelle la partie numérique du mouvement Wikimédia est imaginée tel un quartier au sein d"une ville que serait l'espace web. Une ville, dans laquelle le réseau routier serait Internet, les batiments, des serveurs informatiques et les pièces de ces batiments, des pages les web. Dès lors, dans cette ville, le quartier Wikimédia rassemble près d’un millier d’édifices, dont on peut visiter chaque pèce librement et gratuitement et même modifier presque tout ce qui s’y trouve, ceci à l’exception de quelques pièces situées dans des bâtiments administratifs. Dans chacun de ces espaces publics, on peut ainsi y placer de nouvelles choses, telles que du texte, des photos, des vidéos ou des documents sonores. On peut aussi y ranger ou modifier ce qui a été apporté par d’autres personnes, afin de rendre la pièce plus esthétiques ou plus autentique. Et lorsque pulieurs personnes ont des idéés différentes concernant l'aménagement de la pière, pas de problème. Chaque pièce possède une pièce annexe dédiée au débats et à la discussion pour que l'on puisse s'y rassembler et décider ensemble comment organiser les choses. Un personne mal intentionnée peut d'ailleurs faire disparaitre tout ce qui se trouve dans une pièce. Cependant, un robot aura vite fait de tout remettre en place, avant de transmettre un avertissement concernant la manière dont sera traitée ce genre de vandalisme. Si une actions plus discrètes n'est pas détectées par un robot dans la seconde qui suit, c'est alors une des personnes qui ont enrichi ou enjolivé la pièce, qui prendra certainement le relais pour annuler les changements malveillantes avant de contacter la personne qui en est l'auteur. En cas de multirécidive, sa capacité de modifier le bâtiment vandalisé peut alors lui être retirée, ce qui peut être appliqué à tout le quartier quand cela se justifie. Ce mise en application se faut toujours après discussion et par l'intermédiaire d'un administrateurs ou d'une administratrice bénévole choisi par l'ensemble autres personnes qui prennent soin bénévolelement des batiments. [[Fichier:The Digital City, Riyadh 191957.jpg|vignette|<small>Figure 3. Photo de la ''Digital City'' de [[w:Riyad|Riyadh]] et son aspect visuel en lien avec la métaphore du quartier Wikimédia.</small>|300x300px]] On comprend donc que tout le monde peut enrichir, mais aussi surveiller et protéger les richesses partagées dans le quartier Wikimédia. Il suffit pour cela de rejoindre le mouvement en se créant un compte et de profiter, entre autres, d'un système de notification qui poste un avertissement à chaque fois qu'une des pièces que l'on désire surveiller au sein des bâtiments Wikimédia est modifiée. Pour la création de ce compte, pas besoin de fournir une adresse ou un numéro de téléphone. Les seules informations personnelles indispensables au bon fonctionnement du quartier Wikimédia sont les [[w:Adresses_IP|adresses IP]] des ordinateurs connectés. Car contrairement à ce qui se passe dans les quartiers commerciaux de la grande ville numérique, tels que les [[w:GAFAM|GAFAM]], [[w:NATU_(Netflix,_Airbnb,_Tesla_et_Uber)|NATU]], [[w:BATX|BATX]] ou autres, aucune des informations récoltées lors des visites des bâtiments Wikimédia n’est exploitée par des services de marketing. Même les adresses IP enregistrées par le système ne sont pas visibles par les autres visiteurs. Elles sont remplacées par les noms et les pseudonymes fournis lors de la création des comptes, ou masquées par des comptes temporaires en cas de non-connexion. Seules quelques personnes accréditées par la communauté pour effectuer des contrôles d’usurpation d’identité ont accès à ces informations<ref>{{Lien web|auteur=MédiaWiki|titre=Produit de confiance et de sécurité/Comptes temporaires|url=https://web.archive.org/web/20250813140004/https://www.mediawiki.org/wiki/Trust_and_Safety_Product/Temporary_Accounts/fr}}.</ref>. C’est là une précaution nécessaire au bon déroulement des prises de décisions par recherche de [[w:Consensus|consensus]] organisées au sein du quartier Wikimédia. Dans la ville numérique que constitue le Web, Wikimédia apparait ainsi comme le plus grand quartier dédié au partage de la connaissance. La partie la plus connue du quartier, [[w:Wikipédia:Accueil_principal|Wikipédia]], est composée de plus de 350 bâtiments encyclopédiques répertoriés par langues. À côté de ceux-ci, et toujours séparés en versions linguistiques, se trouvent les bibliothèques [[:en:fr:accueil|Wikilivres]] et [[s:fr:Wikisource:Accueil|Wikisource]], les bâtiments lexicaux multilingues [[wikt:fr:Wiktionnaire:Page_d’accueil|Wiktionnaire]], ce centre journalistique [[n:fr:accueil|Wikinews]], le centre pédagogique et de recherche [[v:fr:accueil|Wikiversité]], le centre d'informations sur les voyages [[voy:fr:accueil|Wikivoyage]], le répertoire des êtres vivants [[species:main page|Wikispecies]] et l'institut des citations d’auteurs [[q:fr:accueil|Wikiquote]]. Cela sans oublier [[commons:main page|Wikimedia Commons]], un lieu de collecte pour tous les fichiers médiatiques, et [[wikidata:wikidata:main_page|Wikidata]], cette énorme banque d’informations structurées, dont la fonction, au même titre que Wikimedi Commons, est d’enrichir le contenu des autres buildings. 50 % de ces bâtiments sont constitués d'étages dédiés à la libre organisation technique et politique des projets. Tandis que plusieurs immeubles du quartier, tels que [[mw:main page|MediaWiki]], [[wikitech:Main_Page|Wikitech]], [[w:fr:phabricator|Phabricator]], sont entièrement dédiés à sa maintenance technique. Vient ensuite [[metawiki:main page|Méta-Wiki]], le centre administratif dédié à l’organisation et la gouvernance générale de l'ensemble du quartier. Puis le bâtiment [[otrswiki:Main page|Wikimedia VRT]], où l'on traite les courriers adressés au quartier, et finalement [[outreach:main page|''Wikimedia outreach'']], le centre de sensibilisation et de recrutement de nouveaux bénévoles. En découvrant l’existence de ce vaste quartier numérique libre d’accès, de participation et modification, on visualise mieux, à nouveau, comment les activités du mouvement Wikimédia dépassent largement ce qui se passe au sein du projet Wikipédia. Même si à lui seul, ce projet est déjà perçu comme « une utopie en marche »<ref>{{Article|langue=|prénom1=Christian|nom1=Vandendorpe|titre=Le phénomène Wikipédia: une utopie en marche|périodique=Le Débat|volume=148|numéro=1|éditeur=Gallimard|date=2008|issn=0246-2346|pages=17}}.</ref> ou « réalisée »<ref>{{Ouvrage|prénom1=Théo|nom1=Henri|directeur1=|titre=Wikipédia : une utopie réalisée ?|lieu=Université de Poitier|date=juillet 2013|pages totales=98|lire en ligne=https://web.archive.org/web/20211103122912/https://www.seies.net/sites/theo/doc/HENRI_theo_-_master_1_-_memoire.pdf}}.</ref>, il fait cependant partie d'une organisation mondiale bien plus vaste, quasi gérée uniquement par des bénévoles et de manière non lucrative. Une utopie bien plus grande donc, dont il est bon de comprendre les origines. Comme première explication, il y a cette synchronicité entre le chamboulement culturel provoqué par la [[w:Contre-culture_des_années_1960|contre-culture des années 1960]] et les débuts de la révolution numérique. Les chercheurs et étudiants en informatique, pionniers des réseaux et de leurs applications, étaient effectivement fortement influencés par ce changement de paradigme, qui fut symbolisé en France par les événements de [[w:mai_68|mai 68]]. De cette impulsion naîtront ainsi une philosophie et un mode d’organisation des activités tout à fait spécifiques, dont le mouvement Wikimédia deviendra l'héritier. Aujourd’hui, et à l'image du projet Wikipédia en français<ref>{{Lien web|auteur=Wikipédia|titre=Wikipédia:Règles et recommandations|url=https://web.archive.org/web/20251008105309/https://fr.wikipedia.org/wiki/Wikip%C3%A9dia:R%C3%A8gles_et_recommandations|date=|consulté le=}}.</ref>, les sites web hébergés par la Fondation Wikimédia contiennent de nombreuses [[w:Wikipédia:Règles_et_recommandations|règles et recommandations]] qui s'apparentent fortement à des lignes éditoriales. Parallèlement à cela, un [[foundation:Policy:Universal_Code_of_Conduct/fr|code de conduite universel]] fut adopté par la Fondation Wikimédia en 2022, dans le but d’établir un « référentiel minimum des comportements acceptables et inacceptables »<ref>{{Lien web|titre=Policy:Universal Code of Conduct/fr|url=https://web.archive.org/web/20251007061014/https://foundation.wikimedia.org/wiki/Policy:Universal_Code_of_Conduct/fr|site=|date=|consulté le=|auteur=Wikimedia Foundation Governance Wiki}}.</ref>. Cependant, dès la création de l'encyclopédie libre qui fut un jour qualifiée de « bazar libertaire »<ref>{{Lien web|langue=|auteur=Frédéric Joignot|titre=Wikipédia, bazar libertaire|url=https://web.archive.org/web/20170630065818/http://www.lemonde.fr/technologies/article/2012/01/14/wikipedia-bazar-libertaire_1629135_651865.html|site=Le Monde|lieu=|date=2012|consulté le=}}.</ref>, un principe fondateur intitulé [[w:Wikipédia:Interprétation_créative_des_règles|interprétation créative des règles]]<ref>{{Lien web|langue=|auteur=Wikipédia|titre=Wikipédia: Interprétation créative des règles|url=https://fr.wikipedia.org/wiki/Wikip&#233;dia:Interpr&#233;tation_cr&#233;ative_des_r&#232;gles|consulté le=}}.</ref>, toujours présent à ce jour, illustre clairement l’orientation politique et culturelle du projet : <blockquote> N’hésitez pas à contribuer, même si vous ne connaissez pas l’ensemble des règles, et si vous en rencontrez une qui, dans votre situation, semble gêner à l’élaboration de l’encyclopédie, ignorez-la ou, mieux, corrigez-la. </blockquote> Cette invitation illustre à elle seule les valeurs d’universalité, de liberté, de décentralisation, de partage, de collaboration et de mérite décrites par [[w:fr: Steven Levy|Steven Levy]] dans son ouvrage ''[[w:L'Éthique des hackers|L’Éthique des hackers]]''<ref>{{Ouvrage|langue=|prénom1=Steven|nom1=Levy|prénom2=Gilles|nom2=Tordjman|titre=L'éthique des hackers|éditeur=Globe|date=2013|isbn=978-2-211-20410-1|oclc=844898302}}.</ref>. Bien avant l’apparition du mouvement Wikimédia, une nouvelle perception du monde a donc rendu possible le développement d'un environnement numérique, ouvert, libre et gratuit, propice à la création d'une encyclopédie, écrite et gérée par des millions de contributeurs et contributrices situés aux quatre coins du monde. Cela commence par [[w:Internet|Internet]], un réseau mondial de communication en libre accès, puis le développement du ''[[w:World_Wide_Web|World Wide Web]]'', qui simplifie les interactions humaines à l’échelle planétaire, jusqu'à l'avènement des logiciels qui ont rendu possible la modification de sites web à l'aide d’un simple navigateur, pour former ce que l'on appelle aujourd’hui le [[w:Web_2.0|Web 2.0]]. Or, parmi ces logiciels se trouvent les [[w:fr:Moteur de Wiki|moteurs de Wiki]], dont le plus puissant d’entre eux, [[w:MediaWiki|MediaWiki]], est un [[Logiciels libres|logiciel libre]] développé par la Fondation Wikimédia, devenue par ce fait actrice au sein du [[w:Mouvement_du_logiciel_libre|mouvement du logiciel libre]].{{AutoCat}} a9e3frxnha0xjqhxbdoxcy9molcae63 764079 764078 2026-04-20T08:39:18Z Lionel Scheepmans 20012 764079 wikitext text/x-wiki <noinclude>{{Le mouvement Wikimédia}}</noinclude> Wikipédia et le mouvement Wikimédia sont parfois perçus comme une utopie et même comme « la dernière utopie collective du Web » par certains<ref>{{Lien web|langue=fr-FR|nom1=Dupont-Besnard|prénom1=Marcus|titre=Wikipédia, la dernière utopie collective du web ?|url=https://web.archive.org/web/20250516050814/https://www.numerama.com/tech/805447-wikipedia-la-derniere-utopie-collective-du-web.html|site=Numerama|date=2021-12-29|consulté le=2025-12-21}}</ref>. Pour nous aidez à visualiser en quoi consiste cette utopie, voici une métaphore dans laquelle la partie numérique du mouvement Wikimédia apparait tel un quartier au sein d"une ville que serait l'espace web. Dans cette ville, le réseau routier, c'est le réseau Internet, les batiments, des serveurs informatiques et les pièces de ces batiments, des pages les web. Dès lors, dans cette ville, le quartier Wikimédia rassemble près d’un millier d’édifices, dont on peut visiter chaque pèce librement et gratuitement et même modifier presque tout ce qui s’y trouve, ceci à l’exception de quelques pièces situées dans des bâtiments administratifs. Dans chacun de ces espaces publics, on peut ainsi y placer de nouvelles choses, telles que du texte, des photos, des vidéos ou des documents sonores. On peut aussi y ranger ou modifier ce qui a été apporté par d’autres personnes, afin de rendre la pièce plus esthétiques ou plus autentique. Et lorsque pulieurs personnes ont des idéés différentes concernant l'aménagement de la pière, pas de problème. Chaque pièce possède une pièce annexe dédiée au débats et à la discussion pour que l'on puisse s'y rassembler et décider ensemble comment organiser les choses. Un personne mal intentionnée peut d'ailleurs faire disparaitre tout ce qui se trouve dans une pièce. Cependant, un robot aura vite fait de tout remettre en place, avant de transmettre un avertissement concernant la manière dont sera traitée ce genre de vandalisme. Si une actions plus discrètes n'est pas détectées par un robot dans la seconde qui suit, c'est alors une des personnes qui ont enrichi ou enjolivé la pièce, qui prendra certainement le relais pour annuler les changements malveillantes avant de contacter la personne qui en est l'auteur. En cas de multirécidive, sa capacité de modifier le bâtiment vandalisé peut alors lui être retirée, ce qui peut être appliqué à tout le quartier quand cela se justifie. Ce mise en application se faut toujours après discussion et par l'intermédiaire d'un administrateurs ou d'une administratrice bénévole choisi par l'ensemble autres personnes qui prennent soin bénévolelement des batiments. [[Fichier:The Digital City, Riyadh 191957.jpg|vignette|<small>Figure 3. Photo de la ''Digital City'' de [[w:Riyad|Riyadh]] et son aspect visuel en lien avec la métaphore du quartier Wikimédia.</small>|300x300px]] On comprend donc que tout le monde peut enrichir, mais aussi surveiller et protéger les richesses partagées dans le quartier Wikimédia. Il suffit pour cela de rejoindre le mouvement en se créant un compte et de profiter, entre autres, d'un système de notification qui poste un avertissement à chaque fois qu'une des pièces que l'on désire surveiller au sein des bâtiments Wikimédia est modifiée. Pour la création de ce compte, pas besoin de fournir une adresse ou un numéro de téléphone. Les seules informations personnelles indispensables au bon fonctionnement du quartier Wikimédia sont les [[w:Adresses_IP|adresses IP]] des ordinateurs connectés. Car contrairement à ce qui se passe dans les quartiers commerciaux de la grande ville numérique, tels que les [[w:GAFAM|GAFAM]], [[w:NATU_(Netflix,_Airbnb,_Tesla_et_Uber)|NATU]], [[w:BATX|BATX]] ou autres, aucune des informations récoltées lors des visites des bâtiments Wikimédia n’est exploitée par des services de marketing. Même les adresses IP enregistrées par le système ne sont pas visibles par les autres visiteurs. Elles sont remplacées par les noms et les pseudonymes fournis lors de la création des comptes, ou masquées par des comptes temporaires en cas de non-connexion. Seules quelques personnes accréditées par la communauté pour effectuer des contrôles d’usurpation d’identité ont accès à ces informations<ref>{{Lien web|auteur=MédiaWiki|titre=Produit de confiance et de sécurité/Comptes temporaires|url=https://web.archive.org/web/20250813140004/https://www.mediawiki.org/wiki/Trust_and_Safety_Product/Temporary_Accounts/fr}}.</ref>. C’est là une précaution nécessaire au bon déroulement des prises de décisions par recherche de [[w:Consensus|consensus]] organisées au sein du quartier Wikimédia. Dans la ville numérique que constitue le Web, Wikimédia apparait ainsi comme le plus grand quartier dédié au partage de la connaissance. La partie la plus connue du quartier, [[w:Wikipédia:Accueil_principal|Wikipédia]], est composée de plus de 350 bâtiments encyclopédiques répertoriés par langues. À côté de ceux-ci, et toujours séparés en versions linguistiques, se trouvent les bibliothèques [[:en:fr:accueil|Wikilivres]] et [[s:fr:Wikisource:Accueil|Wikisource]], les bâtiments lexicaux multilingues [[wikt:fr:Wiktionnaire:Page_d’accueil|Wiktionnaire]], ce centre journalistique [[n:fr:accueil|Wikinews]], le centre pédagogique et de recherche [[v:fr:accueil|Wikiversité]], le centre d'informations sur les voyages [[voy:fr:accueil|Wikivoyage]], le répertoire des êtres vivants [[species:main page|Wikispecies]] et l'institut des citations d’auteurs [[q:fr:accueil|Wikiquote]]. Cela sans oublier [[commons:main page|Wikimedia Commons]], un lieu de collecte pour tous les fichiers médiatiques, et [[wikidata:wikidata:main_page|Wikidata]], cette énorme banque d’informations structurées, dont la fonction, au même titre que Wikimedi Commons, est d’enrichir le contenu des autres buildings. 50 % de ces bâtiments sont constitués d'étages dédiés à la libre organisation technique et politique des projets. Tandis que plusieurs immeubles du quartier, tels que [[mw:main page|MediaWiki]], [[wikitech:Main_Page|Wikitech]], [[w:fr:phabricator|Phabricator]], sont entièrement dédiés à sa maintenance technique. Vient ensuite [[metawiki:main page|Méta-Wiki]], le centre administratif dédié à l’organisation et la gouvernance générale de l'ensemble du quartier. Puis le bâtiment [[otrswiki:Main page|Wikimedia VRT]], où l'on traite les courriers adressés au quartier, et finalement [[outreach:main page|''Wikimedia outreach'']], le centre de sensibilisation et de recrutement de nouveaux bénévoles. En découvrant l’existence de ce vaste quartier numérique libre d’accès, de participation et modification, on visualise mieux, à nouveau, comment les activités du mouvement Wikimédia dépassent largement ce qui se passe au sein du projet Wikipédia. Même si à lui seul, ce projet est déjà perçu comme « une utopie en marche »<ref>{{Article|langue=|prénom1=Christian|nom1=Vandendorpe|titre=Le phénomène Wikipédia: une utopie en marche|périodique=Le Débat|volume=148|numéro=1|éditeur=Gallimard|date=2008|issn=0246-2346|pages=17}}.</ref> ou « réalisée »<ref>{{Ouvrage|prénom1=Théo|nom1=Henri|directeur1=|titre=Wikipédia : une utopie réalisée ?|lieu=Université de Poitier|date=juillet 2013|pages totales=98|lire en ligne=https://web.archive.org/web/20211103122912/https://www.seies.net/sites/theo/doc/HENRI_theo_-_master_1_-_memoire.pdf}}.</ref>, il fait cependant partie d'une organisation mondiale bien plus vaste, quasi gérée uniquement par des bénévoles et de manière non lucrative. Une utopie bien plus grande donc, dont il est bon de comprendre les origines. Comme première explication, il y a cette synchronicité entre le chamboulement culturel provoqué par la [[w:Contre-culture_des_années_1960|contre-culture des années 1960]] et les débuts de la révolution numérique. Les chercheurs et étudiants en informatique, pionniers des réseaux et de leurs applications, étaient effectivement fortement influencés par ce changement de paradigme, qui fut symbolisé en France par les événements de [[w:mai_68|mai 68]]. De cette impulsion naîtront ainsi une philosophie et un mode d’organisation des activités tout à fait spécifiques, dont le mouvement Wikimédia deviendra l'héritier. Aujourd’hui, et à l'image du projet Wikipédia en français<ref>{{Lien web|auteur=Wikipédia|titre=Wikipédia:Règles et recommandations|url=https://web.archive.org/web/20251008105309/https://fr.wikipedia.org/wiki/Wikip%C3%A9dia:R%C3%A8gles_et_recommandations|date=|consulté le=}}.</ref>, les sites web hébergés par la Fondation Wikimédia contiennent de nombreuses [[w:Wikipédia:Règles_et_recommandations|règles et recommandations]] qui s'apparentent fortement à des lignes éditoriales. Parallèlement à cela, un [[foundation:Policy:Universal_Code_of_Conduct/fr|code de conduite universel]] fut adopté par la Fondation Wikimédia en 2022, dans le but d’établir un « référentiel minimum des comportements acceptables et inacceptables »<ref>{{Lien web|titre=Policy:Universal Code of Conduct/fr|url=https://web.archive.org/web/20251007061014/https://foundation.wikimedia.org/wiki/Policy:Universal_Code_of_Conduct/fr|site=|date=|consulté le=|auteur=Wikimedia Foundation Governance Wiki}}.</ref>. Cependant, dès la création de l'encyclopédie libre qui fut un jour qualifiée de « bazar libertaire »<ref>{{Lien web|langue=|auteur=Frédéric Joignot|titre=Wikipédia, bazar libertaire|url=https://web.archive.org/web/20170630065818/http://www.lemonde.fr/technologies/article/2012/01/14/wikipedia-bazar-libertaire_1629135_651865.html|site=Le Monde|lieu=|date=2012|consulté le=}}.</ref>, un principe fondateur intitulé [[w:Wikipédia:Interprétation_créative_des_règles|interprétation créative des règles]]<ref>{{Lien web|langue=|auteur=Wikipédia|titre=Wikipédia: Interprétation créative des règles|url=https://fr.wikipedia.org/wiki/Wikip&#233;dia:Interpr&#233;tation_cr&#233;ative_des_r&#232;gles|consulté le=}}.</ref>, toujours présent à ce jour, illustre clairement l’orientation politique et culturelle du projet : <blockquote> N’hésitez pas à contribuer, même si vous ne connaissez pas l’ensemble des règles, et si vous en rencontrez une qui, dans votre situation, semble gêner à l’élaboration de l’encyclopédie, ignorez-la ou, mieux, corrigez-la. </blockquote> Cette invitation illustre à elle seule les valeurs d’universalité, de liberté, de décentralisation, de partage, de collaboration et de mérite décrites par [[w:fr: Steven Levy|Steven Levy]] dans son ouvrage ''[[w:L'Éthique des hackers|L’Éthique des hackers]]''<ref>{{Ouvrage|langue=|prénom1=Steven|nom1=Levy|prénom2=Gilles|nom2=Tordjman|titre=L'éthique des hackers|éditeur=Globe|date=2013|isbn=978-2-211-20410-1|oclc=844898302}}.</ref>. Bien avant l’apparition du mouvement Wikimédia, une nouvelle perception du monde a donc rendu possible le développement d'un environnement numérique, ouvert, libre et gratuit, propice à la création d'une encyclopédie, écrite et gérée par des millions de contributeurs et contributrices situés aux quatre coins du monde. Cela commence par [[w:Internet|Internet]], un réseau mondial de communication en libre accès, puis le développement du ''[[w:World_Wide_Web|World Wide Web]]'', qui simplifie les interactions humaines à l’échelle planétaire, jusqu'à l'avènement des logiciels qui ont rendu possible la modification de sites web à l'aide d’un simple navigateur, pour former ce que l'on appelle aujourd’hui le [[w:Web_2.0|Web 2.0]]. Or, parmi ces logiciels se trouvent les [[w:fr:Moteur de Wiki|moteurs de Wiki]], dont le plus puissant d’entre eux, [[w:MediaWiki|MediaWiki]], est un [[Logiciels libres|logiciel libre]] développé par la Fondation Wikimédia, devenue par ce fait actrice au sein du [[w:Mouvement_du_logiciel_libre|mouvement du logiciel libre]].{{AutoCat}} o6xy7am15msab2egv0tref0dxqnnkmx 764080 764079 2026-04-20T08:44:00Z Lionel Scheepmans 20012 764080 wikitext text/x-wiki <noinclude>{{Le mouvement Wikimédia}}</noinclude> Wikipédia et le mouvement Wikimédia sont parfois perçus comme une utopie et même comme « la dernière utopie collective du Web » par certains<ref>{{Lien web|langue=fr-FR|nom1=Dupont-Besnard|prénom1=Marcus|titre=Wikipédia, la dernière utopie collective du web ?|url=https://web.archive.org/web/20250516050814/https://www.numerama.com/tech/805447-wikipedia-la-derniere-utopie-collective-du-web.html|site=Numerama|date=2021-12-29|consulté le=2025-12-21}}</ref>. Pour nous aidez à visualiser en quoi consiste cette utopie, voici une métaphore dans laquelle la partie numérique du mouvement Wikimédia apparait tel un quartier au sein d"une ville que serait l'espace web. Dans cette ville, les routes, Internet représenterait le réseau routier, pendant que les serveurs informatiques constitueraient les édifices et les pages web, les pièces présentes au sein de ces édifices. Dès lors, dans cette ville, le quartier Wikimédia rassemble près d’un millier d’édifices, dont on peut visiter chaque pèce librement et gratuitement et même modifier presque tout ce qui s’y trouve, ceci à l’exception de quelques pièces situées dans des bâtiments administratifs. Dans chacun de ces espaces publics, on peut ainsi y placer de nouvelles choses, telles que du texte, des photos, des vidéos ou des documents sonores. On peut aussi y ranger ou modifier ce qui a été apporté par d’autres personnes, afin de rendre la pièce plus esthétiques ou plus autentique. Et lorsque pulieurs personnes ont des idéés différentes concernant l'aménagement de la pière, pas de problème. Chaque pièce possède une pièce annexe dédiée au débats et à la discussion pour que l'on puisse s'y rassembler et décider ensemble comment organiser les choses. Un personne mal intentionnée peut d'ailleurs faire disparaitre tout ce qui se trouve dans une pièce. Cependant, un robot aura vite fait de tout remettre en place, avant de transmettre un avertissement concernant la manière dont sera traitée ce genre de vandalisme. Si une actions plus discrètes n'est pas détectées par un robot dans la seconde qui suit, c'est alors une des personnes qui ont enrichi ou enjolivé la pièce, qui prendra certainement le relais pour annuler les changements malveillantes avant de contacter la personne qui en est l'auteur. En cas de multirécidive, sa capacité de modifier le bâtiment vandalisé peut alors lui être retirée, ce qui peut être appliqué à tout le quartier quand cela se justifie. Ce mise en application se faut toujours après discussion et par l'intermédiaire d'un administrateurs ou d'une administratrice bénévole choisi par l'ensemble autres personnes qui prennent soin bénévolelement des batiments. [[Fichier:The Digital City, Riyadh 191957.jpg|vignette|<small>Figure 3. Photo de la ''Digital City'' de [[w:Riyad|Riyadh]] et son aspect visuel en lien avec la métaphore du quartier Wikimédia.</small>|300x300px]] On comprend donc que tout le monde peut enrichir, mais aussi surveiller et protéger les richesses partagées dans le quartier Wikimédia. Il suffit pour cela de rejoindre le mouvement en se créant un compte et de profiter, entre autres, d'un système de notification qui poste un avertissement à chaque fois qu'une des pièces que l'on désire surveiller au sein des bâtiments Wikimédia est modifiée. Pour la création de ce compte, pas besoin de fournir une adresse ou un numéro de téléphone. Les seules informations personnelles indispensables au bon fonctionnement du quartier Wikimédia sont les [[w:Adresses_IP|adresses IP]] des ordinateurs connectés. Car contrairement à ce qui se passe dans les quartiers commerciaux de la grande ville numérique, tels que les [[w:GAFAM|GAFAM]], [[w:NATU_(Netflix,_Airbnb,_Tesla_et_Uber)|NATU]], [[w:BATX|BATX]] ou autres, aucune des informations récoltées lors des visites des bâtiments Wikimédia n’est exploitée par des services de marketing. Même les adresses IP enregistrées par le système ne sont pas visibles par les autres visiteurs. Elles sont remplacées par les noms et les pseudonymes fournis lors de la création des comptes, ou masquées par des comptes temporaires en cas de non-connexion. Seules quelques personnes accréditées par la communauté pour effectuer des contrôles d’usurpation d’identité ont accès à ces informations<ref>{{Lien web|auteur=MédiaWiki|titre=Produit de confiance et de sécurité/Comptes temporaires|url=https://web.archive.org/web/20250813140004/https://www.mediawiki.org/wiki/Trust_and_Safety_Product/Temporary_Accounts/fr}}.</ref>. C’est là une précaution nécessaire au bon déroulement des prises de décisions par recherche de [[w:Consensus|consensus]] organisées au sein du quartier Wikimédia. Dans la ville numérique que constitue le Web, Wikimédia apparait ainsi comme le plus grand quartier dédié au partage de la connaissance. La partie la plus connue du quartier, [[w:Wikipédia:Accueil_principal|Wikipédia]], est composée de plus de 350 bâtiments encyclopédiques répertoriés par langues. À côté de ceux-ci, et toujours séparés en versions linguistiques, se trouvent les bibliothèques [[:en:fr:accueil|Wikilivres]] et [[s:fr:Wikisource:Accueil|Wikisource]], les bâtiments lexicaux multilingues [[wikt:fr:Wiktionnaire:Page_d’accueil|Wiktionnaire]], ce centre journalistique [[n:fr:accueil|Wikinews]], le centre pédagogique et de recherche [[v:fr:accueil|Wikiversité]], le centre d'informations sur les voyages [[voy:fr:accueil|Wikivoyage]], le répertoire des êtres vivants [[species:main page|Wikispecies]] et l'institut des citations d’auteurs [[q:fr:accueil|Wikiquote]]. Cela sans oublier [[commons:main page|Wikimedia Commons]], un lieu de collecte pour tous les fichiers médiatiques, et [[wikidata:wikidata:main_page|Wikidata]], cette énorme banque d’informations structurées, dont la fonction, au même titre que Wikimedi Commons, est d’enrichir le contenu des autres buildings. 50 % de ces bâtiments sont constitués d'étages dédiés à la libre organisation technique et politique des projets. Tandis que plusieurs immeubles du quartier, tels que [[mw:main page|MediaWiki]], [[wikitech:Main_Page|Wikitech]], [[w:fr:phabricator|Phabricator]], sont entièrement dédiés à sa maintenance technique. Vient ensuite [[metawiki:main page|Méta-Wiki]], le centre administratif dédié à l’organisation et la gouvernance générale de l'ensemble du quartier. Puis le bâtiment [[otrswiki:Main page|Wikimedia VRT]], où l'on traite les courriers adressés au quartier, et finalement [[outreach:main page|''Wikimedia outreach'']], le centre de sensibilisation et de recrutement de nouveaux bénévoles. En découvrant l’existence de ce vaste quartier numérique libre d’accès, de participation et modification, on visualise mieux, à nouveau, comment les activités du mouvement Wikimédia dépassent largement ce qui se passe au sein du projet Wikipédia. Même si à lui seul, ce projet est déjà perçu comme « une utopie en marche »<ref>{{Article|langue=|prénom1=Christian|nom1=Vandendorpe|titre=Le phénomène Wikipédia: une utopie en marche|périodique=Le Débat|volume=148|numéro=1|éditeur=Gallimard|date=2008|issn=0246-2346|pages=17}}.</ref> ou « réalisée »<ref>{{Ouvrage|prénom1=Théo|nom1=Henri|directeur1=|titre=Wikipédia : une utopie réalisée ?|lieu=Université de Poitier|date=juillet 2013|pages totales=98|lire en ligne=https://web.archive.org/web/20211103122912/https://www.seies.net/sites/theo/doc/HENRI_theo_-_master_1_-_memoire.pdf}}.</ref>, il fait cependant partie d'une organisation mondiale bien plus vaste, quasi gérée uniquement par des bénévoles et de manière non lucrative. Une utopie bien plus grande donc, dont il est bon de comprendre les origines. Comme première explication, il y a cette synchronicité entre le chamboulement culturel provoqué par la [[w:Contre-culture_des_années_1960|contre-culture des années 1960]] et les débuts de la révolution numérique. Les chercheurs et étudiants en informatique, pionniers des réseaux et de leurs applications, étaient effectivement fortement influencés par ce changement de paradigme, qui fut symbolisé en France par les événements de [[w:mai_68|mai 68]]. De cette impulsion naîtront ainsi une philosophie et un mode d’organisation des activités tout à fait spécifiques, dont le mouvement Wikimédia deviendra l'héritier. Aujourd’hui, et à l'image du projet Wikipédia en français<ref>{{Lien web|auteur=Wikipédia|titre=Wikipédia:Règles et recommandations|url=https://web.archive.org/web/20251008105309/https://fr.wikipedia.org/wiki/Wikip%C3%A9dia:R%C3%A8gles_et_recommandations|date=|consulté le=}}.</ref>, les sites web hébergés par la Fondation Wikimédia contiennent de nombreuses [[w:Wikipédia:Règles_et_recommandations|règles et recommandations]] qui s'apparentent fortement à des lignes éditoriales. Parallèlement à cela, un [[foundation:Policy:Universal_Code_of_Conduct/fr|code de conduite universel]] fut adopté par la Fondation Wikimédia en 2022, dans le but d’établir un « référentiel minimum des comportements acceptables et inacceptables »<ref>{{Lien web|titre=Policy:Universal Code of Conduct/fr|url=https://web.archive.org/web/20251007061014/https://foundation.wikimedia.org/wiki/Policy:Universal_Code_of_Conduct/fr|site=|date=|consulté le=|auteur=Wikimedia Foundation Governance Wiki}}.</ref>. Cependant, dès la création de l'encyclopédie libre qui fut un jour qualifiée de « bazar libertaire »<ref>{{Lien web|langue=|auteur=Frédéric Joignot|titre=Wikipédia, bazar libertaire|url=https://web.archive.org/web/20170630065818/http://www.lemonde.fr/technologies/article/2012/01/14/wikipedia-bazar-libertaire_1629135_651865.html|site=Le Monde|lieu=|date=2012|consulté le=}}.</ref>, un principe fondateur intitulé [[w:Wikipédia:Interprétation_créative_des_règles|interprétation créative des règles]]<ref>{{Lien web|langue=|auteur=Wikipédia|titre=Wikipédia: Interprétation créative des règles|url=https://fr.wikipedia.org/wiki/Wikip&#233;dia:Interpr&#233;tation_cr&#233;ative_des_r&#232;gles|consulté le=}}.</ref>, toujours présent à ce jour, illustre clairement l’orientation politique et culturelle du projet : <blockquote> N’hésitez pas à contribuer, même si vous ne connaissez pas l’ensemble des règles, et si vous en rencontrez une qui, dans votre situation, semble gêner à l’élaboration de l’encyclopédie, ignorez-la ou, mieux, corrigez-la. </blockquote> Cette invitation illustre à elle seule les valeurs d’universalité, de liberté, de décentralisation, de partage, de collaboration et de mérite décrites par [[w:fr: Steven Levy|Steven Levy]] dans son ouvrage ''[[w:L'Éthique des hackers|L’Éthique des hackers]]''<ref>{{Ouvrage|langue=|prénom1=Steven|nom1=Levy|prénom2=Gilles|nom2=Tordjman|titre=L'éthique des hackers|éditeur=Globe|date=2013|isbn=978-2-211-20410-1|oclc=844898302}}.</ref>. Bien avant l’apparition du mouvement Wikimédia, une nouvelle perception du monde a donc rendu possible le développement d'un environnement numérique, ouvert, libre et gratuit, propice à la création d'une encyclopédie, écrite et gérée par des millions de contributeurs et contributrices situés aux quatre coins du monde. Cela commence par [[w:Internet|Internet]], un réseau mondial de communication en libre accès, puis le développement du ''[[w:World_Wide_Web|World Wide Web]]'', qui simplifie les interactions humaines à l’échelle planétaire, jusqu'à l'avènement des logiciels qui ont rendu possible la modification de sites web à l'aide d’un simple navigateur, pour former ce que l'on appelle aujourd’hui le [[w:Web_2.0|Web 2.0]]. Or, parmi ces logiciels se trouvent les [[w:fr:Moteur de Wiki|moteurs de Wiki]], dont le plus puissant d’entre eux, [[w:MediaWiki|MediaWiki]], est un [[Logiciels libres|logiciel libre]] développé par la Fondation Wikimédia, devenue par ce fait actrice au sein du [[w:Mouvement_du_logiciel_libre|mouvement du logiciel libre]].{{AutoCat}} gzpkshfi3b4mygt9ep8o55ii8l185t8 764082 764080 2026-04-20T10:51:51Z Lionel Scheepmans 20012 764082 wikitext text/x-wiki <noinclude>{{Le mouvement Wikimédia}}</noinclude> Wikipédia est parfois perçus comme « une utopie en marche »<ref>{{Article|langue=|prénom1=Christian|nom1=Vandendorpe|titre=Le phénomène Wikipédia: une utopie en marche|périodique=Le Débat|volume=148|numéro=1|éditeur=Gallimard|date=2008|issn=0246-2346|pages=17}}.</ref> ou « réalisée »<ref>{{Ouvrage|prénom1=Théo|nom1=Henri|directeur1=|titre=Wikipédia : une utopie réalisée ?|lieu=Université de Poitier|date=juillet 2013|pages totales=98|lire en ligne=https://web.archive.org/web/20211103122912/https://www.seies.net/sites/theo/doc/HENRI_theo_-_master_1_-_memoire.pdf}}.</ref> et même par certains, comme « la dernière utopie collective du Web »<ref>{{Lien web|langue=fr-FR|nom1=Dupont-Besnard|prénom1=Marcus|titre=Wikipédia, la dernière utopie collective du web ?|url=https://web.archive.org/web/20250516050814/https://www.numerama.com/tech/805447-wikipedia-la-derniere-utopie-collective-du-web.html|site=Numerama|date=2021-12-29|consulté le=2025-12-21}}</ref>. Mais qu'en est-il alors de l'ensemble du mouvement Wikimédia ? Pour nous aidez à visualiser ce qui s'y passe au niveau numérique, voici une métaphore dans laquelle le mouvement Wikimédia apparait tel un quartier au sein d"une ville que serait l'espace web. Dans cette ville, Internet serait le réseau routier, les serveurs informatiques, les batiments, et les pages web, les différentes pièces présentes au sein de ces édifices. Le quartier Wikimédia rassemblerait ainsi plus d’un millier d’édifices. Au sein de ceux-ci, chaque pièce peut être visitée gratuitement et même, à l’exception de quelques pièces situées dans des bâtiments administratifs, il est possible d'en modifier le contenu. On peut ajouter de nouvelles choses, tels que du texte, des photos, des vidéos ou des documents sonores, mais on peut aussi changer ou supprimer ce qui a été modifié par d’autres, dans le but de rendre les choses plus esthétiques ou plus autentique. Et lorsque plusieurs personnes ont des idéés différentes concernant l'aménagement d'une pière, pas de problème. Chaque pièce possède un espace annexe dédié au débats et à la discussion. Le quartier Wikimédia est tellement libre qu'une personne mal intentionnée peut faire disparaitre tout le contenu d'une pièce. Sauf que dans la seconde qui suit, un robot aura remis tout en place, juste avant de transmettre un message concernant le traitement du vandalisme. Lorsqu'une action plus discrète n'est pas détectées par un robot, c'est alors une des personnes qui ont enrichi la pièce qui prendra le relais, pour annuler les changements malveillantes et contacter la personne responsable. En cas de multirécidive, le ou la vandal se verra privé de sa capacité de modifier les pièces, soit, uniquement dans le bâtiment vandalisé, soit, dans tout le quartier quand cela se justifie. La sanction est mise en application toujours après discussion et par les soins d'un administrateurs ou d'une administratrice bénévole, choisi ou choisie par l'ensemble des autres bénévoles qui prennent soin des batiments. [[Fichier:The Digital City, Riyadh 191957.jpg|vignette|<small>Figure 3. Photo de la ''Digital City'' de [[w:Riyad|Riyadh]] et son aspect visuel en lien avec la métaphore du quartier Wikimédia.</small>|300x300px]] On comprend donc que tout le monde peut enrichir, mais aussi surveiller et protéger les richesses partagées dans le quartier Wikimédia. Il suffit pour cela de rejoindre le mouvement en se créant un compte et de profiter, entre autres, d'un système de notification qui permet de reçevoir un message d'avertissement à chaque modification d'une pièce que l'on veut surveiller. Pour la création de ce compte, pas besoin de fournir une adresse ou un numéro de téléphone. Les seules informations personnelles indispensables au bon fonctionnement du quartier Wikimédia sont les [[w:Adresses_IP|adresses IP]] des ordinateurs connectés. Car contrairement à ce qui se passe dans les quartiers commerciaux de la grande ville numérique, tels que les [[w:GAFAM|GAFAM]], [[w:NATU_(Netflix,_Airbnb,_Tesla_et_Uber)|NATU]], [[w:BATX|BATX]] ou autres, aucune des informations récoltées lors des visites des bâtiments Wikimédia n’est collectée pour être ensuite revendue à des personnes qui pourront en faire l'exploitation. Même les adresses IP enregistrées par le système ne sont pas visibles par les autres visiteurs. Elles sont remplacées par les noms et les pseudonymes fournis lors de la création des comptes, ou masquées par des comptes temporaires en cas de non-connexion. Seules quelques personnes accréditées par la communauté pour effectuer des contrôles d’usurpation d’identité ont accès à ces informations<ref>{{Lien web|auteur=MédiaWiki|titre=Produit de confiance et de sécurité/Comptes temporaires|url=https://web.archive.org/web/20250813140004/https://www.mediawiki.org/wiki/Trust_and_Safety_Product/Temporary_Accounts/fr}}.</ref>. C’est là une précaution nécessaire au bon déroulement des prises de décisions, qui se basent sur des recherches de [[w:Consensus|consensus]] au sujet de l'aménagement du quartier Wikimédia. Dans cette ville numérique que constituerait l'espace web, Wikimédia apparait ainsi comme le plus grand quartier dédié au partage de la connaissance. Tout d'abord, il y a la plus connue du quartier, constituée des plus de 350 bâtiments [[w:Wikipédia:Accueil_principal|Wikipédia]], dont chacun d'entre eux est dédié à une version linguistique de encyclopédie. Juste à côté et toujours séparés en versions linguistiques, se trouvent ensuite les bibliothèques [[:en:fr:accueil|Wikilivres]] et [[s:fr:Wikisource:Accueil|Wikisource]], les bâtiments lexicaux multilingues [[wikt:fr:Wiktionnaire:Page_d’accueil|Wiktionnaire]], le centre journalistique [[n:fr:accueil|Wikinews]], le centre pédagogique et de recherche [[v:fr:accueil|Wikiversité]], le centre d'informations touristique [[voy:fr:accueil|Wikivoyage]], le répertoire des êtres vivants [[species:main page|Wikispecies]] et enfin l'institut des citations d’auteurs [[q:fr:accueil|Wikiquote]]. Cela sans oublier qu'à deux pas de là se trouve [[commons:main page|Wikimedia Commons]], un musée médiatique, et puis [[wikidata:wikidata:main_page|Wikidata]], la plus grande banque d’informations structurées au monde, dont l'une des fonctions, au même titre que Wikimedi Commons, est d’enrichir les pièces situées des les autres buildings du quartier. Dans tous ces immeubles, il arrive souvent que plus de la moitié de leurs étages soient uniquement attribuer à l'organisation des projets. Une organisation soutenue d'ailleurs par d'autres édifices tels que [[mw:main page|MediaWiki]], [[wikitech:Main_Page|Wikitech]], [[w:fr:phabricator|Phabricator]], qui sont entièrement dédiés à la maintenance technique de l'ensemble du quartier. Quand aux aspects administratifs du quartier, c'est dans le bâtiment [[metawiki:main page|Méta-Wiki]] que ce situe le siège de la gouvernance, le bâtiment [[otrswiki:Main page|Wikimedia VRT]], celui du traitement des courriers, pendant que le bâtiment ''[[outreach:main page|Wikimedia outreach]]'' se concentre sur des actions de sensibilisations et de recrutement de nouveaux bénévoles. Ce qu'il y a de plus utopique dans le quartier Wikimédia est certainement le fait que, en dehors de certains aspects techniques, tous les bâtiments qui viennent d'être présentés sont excusivement gérés par des communautés bénévoles ouvertes à l'accueil de nouveaux membres. Les seuls immeubles du quartier qui diffèrent de ce principe sont les bâtiments vitrines de la Fondation Wikimédia et des autres associations Wikimédia qui engage du personnel pour le faire, ainsi que le bâtiment wiki du conseil d'administration de la fondation, dont la modification est réservée aux élus et certains employé, pour pouvoir garantir l'autenticité de leurs discours officiels. Tant d'aspects incroyables de part leur utopie, suscite donc au final cette simple question : « Comment tout cela a-t-il été rendu possible ? ». La réponse n'est pas courte et demande de parcourir toute une période de l'histoire allant de l'apparition de la [[w:Contre-culture_des_années_1960|contre-culture des années 1960]] jusqu'à nos jour, en passant par toutes les étapes de la révolution numérique. On y découvre alors que les chercheurs et étudiants en informatique, pionniers des réseaux et de leurs applications, étaient fortement influencés par le mouvement hippie et autres changements idéologiques symbolisé en France par les événements de [[w:mai_68|mai 68]]. Car c'est en effet au départ de ces impulsions que naîtra une philosophie du partage décentralisé et un mode d’organisation tout à fait spécifiques, dont le mouvement Wikimédia deviendra l'héritier. Cela commence par [[w:Internet|Internet]], un réseau mondial de communication en libre accès, puis le développement du ''[[w:World_Wide_Web|World Wide Web]]'', qui simplifie les interactions humaines à l’échelle planétaire, jusqu'à l'avènement des logiciels qui ont rendu possible la modification de sites web à l'aide d’un simple navigateur, pour former ce que l'on appelle aujourd’hui le [[w:Web_2.0|Web 2.0]]. Or, parmi ces logiciels se trouvent les [[w:fr:Moteur de Wiki|moteurs de Wiki]], dont le plus puissant d’entre eux, [[w:MediaWiki|MediaWiki]], est un [[Logiciels libres|logiciel libre]] développé par la Fondation Wikimédia, devenue par ce fait actrice au sein du [[w:Mouvement_du_logiciel_libre|mouvement du logiciel libre]].{{AutoCat}} lse4gydz4yi161ink34bf3lslarrot6 764086 764082 2026-04-20T11:54:04Z Lionel Scheepmans 20012 764086 wikitext text/x-wiki <noinclude>{{Le mouvement Wikimédia}}</noinclude> Wikipédia est parfois perçus comme « une utopie en marche »<ref>{{Article|langue=|prénom1=Christian|nom1=Vandendorpe|titre=Le phénomène Wikipédia: une utopie en marche|périodique=Le Débat|volume=148|numéro=1|éditeur=Gallimard|date=2008|issn=0246-2346|pages=17}}.</ref> ou « réalisée »<ref>{{Ouvrage|prénom1=Théo|nom1=Henri|directeur1=|titre=Wikipédia : une utopie réalisée ?|lieu=Université de Poitier|date=juillet 2013|pages totales=98|lire en ligne=https://web.archive.org/web/20211103122912/https://www.seies.net/sites/theo/doc/HENRI_theo_-_master_1_-_memoire.pdf}}.</ref> et même par certains, comme « la dernière utopie collective du Web »<ref>{{Lien web|langue=fr-FR|nom1=Dupont-Besnard|prénom1=Marcus|titre=Wikipédia, la dernière utopie collective du web ?|url=https://web.archive.org/web/20250516050814/https://www.numerama.com/tech/805447-wikipedia-la-derniere-utopie-collective-du-web.html|site=Numerama|date=2021-12-29|consulté le=2025-12-21}}</ref>. Mais qu'en est-il de l'ensemble du mouvement Wikimédia ? Pour nous aidez à visualiser ce qui s'y passe dans sa dimenssion numérique, voici une métaphore dans laquelle le mouvement Wikimédia apparait tel un quartier au sein d"une ville que serait l'espace web. Dans cette ville, Internet serait le réseau routier, les serveurs informatiques, les batiments, et les pages web, les différentes pièces présentes au sein de ces édifices. Le quartier Wikimédia rassemblerait ainsi plus d’un millier d’édifices. Au sein de ceux-ci, chaque pièce peut être visitée gratuitement et même, à l’exception de quelques pièces situées dans des bâtiments administratifs, il est possible d'en modifier le contenu. On peut ajouter de nouvelles choses, tels que du texte, des photos, des vidéos ou des documents sonores, mais on peut aussi changer ou supprimer ce qui a été modifié par d’autres, dans le but de rendre les choses plus esthétiques ou plus autentique. Et lorsque plusieurs personnes ont des idéés différentes concernant l'aménagement d'une pière, pas de problème. Chaque pièce possède un espace annexe dédié au débats et à la discussion. Le quartier Wikimédia est tellement libre qu'une personne mal intentionnée peut faire disparaitre tout le contenu d'une pièce. Sauf que dans la seconde qui suit, un robot aura remis tout en place, avant de transmettre un message concernant le traitement du vandalisme. Lorsqu'une action plus discrète n'est pas détectées par un robot, c'est alors une des personnes qui ont enrichi la pièce qui prendra le relais pour annuler les changements malveillantes et contacter la personne responsable. En cas de multirécidive, le ou la vandal se verra privé de sa capacité de modifier les pièces, soit, dans le bâtiment vandalisé, soit, dans tout le quartier quand cela se justifie. La sanction est toujours mise en application après discussion et par un administrateur ou une administratrice bénévole, choisi ou choisie par l'ensemble des autres bénévoles qui prennent soin des batiments. [[Fichier:The Digital City, Riyadh 191957.jpg|vignette|<small>Figure 3. Photo de la ''Digital City'' de [[w:Riyad|Riyadh]] et son aspect visuel en lien avec la métaphore du quartier Wikimédia.</small>|300x300px]] On comprend donc que tout le monde peut enrichir, mais aussi surveiller et protéger les richesses partagées dans le quartier Wikimédia. Il suffit pour cela de rejoindre le mouvement en se créant un compte et de profiter, entre autres, d'un système de notification qui permet de reçevoir un message d'avertissement à chaque modification d'une pièce que l'on veut surveiller. Pour la création de ce compte, pas besoin de fournir une adresse ou un numéro de téléphone. Les seules informations personnelles indispensables au bon fonctionnement du quartier Wikimédia sont les [[w:Adresses_IP|adresses IP]] des ordinateurs connectés. Car contrairement à ce qui se passe dans les quartiers commerciaux de la grande ville numérique, tels que les [[w:GAFAM|GAFAM]], [[w:NATU_(Netflix,_Airbnb,_Tesla_et_Uber)|NATU]], [[w:BATX|BATX]] ou autres, aucune des informations récoltées lors des visites des bâtiments Wikimédia n’est collectée pour être vendue à des personnes qui en feront l'exploitation. Même les adresses IP enregistrées par le système ne sont pas visibles par les autres visiteurs. Elles sont remplacées par les noms et les pseudonymes fournis lors de la création des comptes, ou masquées par des comptes temporaires en cas de non-connexion. Seules quelques personnes accréditées par la communauté pour effectuer des contrôles d’usurpation d’identité ont accès à ces informations<ref>{{Lien web|auteur=MédiaWiki|titre=Produit de confiance et de sécurité/Comptes temporaires|url=https://web.archive.org/web/20250813140004/https://www.mediawiki.org/wiki/Trust_and_Safety_Product/Temporary_Accounts/fr}}.</ref>. C’est là une précaution nécessaire au bon déroulement des votes pouvant faire suite aux recherches de [[w:Consensus|consensus]] concernant l'aménagement du quartier Wikimédia. Dans cette ville numérique que constituerait l'espace web, Wikimédia est ainsi comme le plus grand quartier dédié au partage de la connaissance. Tout d'abord, il y a les plus de 350 bâtiments [[w:Wikipédia:Accueil_principal|Wikipédia]], chacun dédié à une version linguistique de encyclopédie. Juste à côté et toujours séparés en versions linguistiques, se trouvent ensuite les bibliothèques [[:en:fr:accueil|Wikilivres]] et [[s:fr:Wikisource:Accueil|Wikisource]], puis, les bâtiments lexicaux multilingues [[wikt:fr:Wiktionnaire:Page_d’accueil|Wiktionnaire]], le centre journalistique [[n:fr:accueil|Wikinews]], le centre pédagogique et de recherche [[v:fr:accueil|Wikiversité]], le centre d'informations touristique [[voy:fr:accueil|Wikivoyage]], le répertoire des êtres vivants [[species:main page|Wikispecies]] et enfin l'institut des citations d’auteurs [[q:fr:accueil|Wikiquote]]. Cela sans oublier qu'à deux pas se situe [[commons:main page|Wikimedia Commons]], un musée médiatique, et enuite [[wikidata:wikidata:main_page|Wikidata]], la plus grande banque d’informations structurées au monde, dont l'une des fonctions, au même titre que Wikimedi Commons, est d’enrichir les pièces situées des les autres buildings du quartier. Dans tous ces immeubles, il arrive souvent que plus de la moitié des étages soient uniquement attribuer à l'organisation des projets. Une organisation qui reçois le soutien d'autres édifices tels que [[mw:main page|MediaWiki]], [[wikitech:Main_Page|Wikitech]], [[w:fr:phabricator|Phabricator]], qui sont trois lieu entièrement dédiés à la maintenance technique sur l'ensemble du quartier. Quand aux aspects administratifs, on retrouve le siège de la gouvernance dans le bâtiment [[metawiki:main page|Méta-Wiki]], traitement des courriers dans le bâtiment [[otrswiki:Main page|Wikimedia VRT]] et des initiatives de sensibilisations et de recrutements de nouveaux bénévoles dans le bâtiment ''[[outreach:main page|Wikimedia outreach]]''. Le plus utopique dans tout cela est finalement le fait que, en dehors de certains aspects techniques, tous ces bâtiments sont excusivement gérés par des communautés bénévoles, entièrement ouvertes à l'accueil de nouveaux membres. Les seuls immeubles du quartier qui diffèrent de ce principe sont les bâtiments vitrines de la Fondation Wikimédia et des autres associations Wikimédia qui engagent du personnel. Quand au conseil d'administration de la fondation, il possède aussi son propre bâtiment dont la modification des pièces est réservée aux élus et certains employés, pour pouvoir garantir l'autenticité de leurs discours officiels. Tant d'aspects incroyables de part leur utopie, suscite donc au final cette simple question : « Comment tout cela a-t-il été rendu possible ? ». La réponse n'est pas courte et demande de parcourir toute une période de l'histoire allant de l'apparition de la [[w:Contre-culture_des_années_1960|contre-culture des années 1960]] jusqu'à nos jour, en passant par toutes les étapes de la révolution numérique. On y découvre alors que les chercheurs et étudiants en informatique, pionniers des réseaux et de leurs applications, étaient fortement influencés par le mouvement hippie et autres changements idéologiques symbolisé en France par les événements de [[w:mai_68|mai 68]]. Car c'est en effet au départ de ces impulsions que naîtra une philosophie du partage décentralisé et un mode d’organisation tout à fait spécifiques, dont le mouvement Wikimédia deviendra l'héritier. Cela commence par [[w:Internet|Internet]], un réseau mondial de communication en libre accès, puis le développement du ''[[w:World_Wide_Web|World Wide Web]]'', qui simplifie les interactions humaines à l’échelle planétaire, jusqu'à l'avènement des logiciels qui ont rendu possible la modification de sites web à l'aide d’un simple navigateur, pour former ce que l'on appelle aujourd’hui le [[w:Web_2.0|Web 2.0]]. Or, parmi ces logiciels se trouvent les [[w:fr:Moteur de Wiki|moteurs de Wiki]], dont le plus puissant d’entre eux, [[w:MediaWiki|MediaWiki]], est un [[Logiciels libres|logiciel libre]] développé par la Fondation Wikimédia, devenue par ce fait actrice au sein du [[w:Mouvement_du_logiciel_libre|mouvement du logiciel libre]].{{AutoCat}} gxrhkqv9f6rz4z91ud9y8zwn5kh2xg6 Le mouvement Wikimédia/L'héritage d'une contre-culture 0 79277 764084 763748 2026-04-20T11:09:18Z Lionel Scheepmans 20012 764084 wikitext text/x-wiki <noinclude>{{Le mouvement Wikimédia}}</noinclude> [[Fichier:Wikimedia-logo black.svg|vignette|150x150px|<small>Figure 21. Logos du mouvement Wikimédia et de sa Fondation.</small>]] [[Fichier:Peace sign.svg|vignette|150x150px|<small>Figure 22. Logo du mouvement hippie et de la contre-culture.</small>]] Au terme de cette première partie d’ouvrage, il apparait évident que la révolution numérique, que l’on considère généralement comme une révolution technique, fut aussi, et peut-être avant tout, une révolution sociale et culturelle. Au sein de celle-ci et sur base du lancement du projet Wikipédia en ce début de XI<sup>ème</sup> siecle, nous venons de voir comment le mouvement Wikimédia représent l'une des manifestations les plus visible, si pas la plus visible, de ce qu'il reste de la [[w:Contre-culture_des_années_1960|contre-culture des années 1960]], apparue en opposition à une hégémonie culturelle paternaliste favorable à la marchandisation du monde et la [[w:Guerre_du_Viêt_Nam|guerre du Viêt Nam]]. Est-ce un hasard d'ailleurs si le renversement du logo du mouvement Wikimédia a une certaine familiarité avec celui du mouvement [[w:fr:Hippie|Hippie]] ? Sans compter que Richard Stallman, celui qui à créer le concept d'encyclopédie libre, fut reconnu en tant que gourou de la contre-culture hacker<ref>{{Ouvrage|auteur=|prénom1=Divers|nom1=auteurs|titre=L'Éthique Hacker|passage=11|éditeur=U.C.H Pour la Liberté|date=Version 9.3|pages totales=56|lire en ligne=https://web.archive.org/web/20211031170831/https://repo.zenk-security.com/Others/L%20Ethique%20Hacker.pdf}}.</ref> et père du système d’exploitation hippie<ref>{{Lien web|langue=|auteur=Gavin Clarke|titre=Stallman's GNU at 30: The hippie OS that foresaw the rise of Apple — and is now trying to take it on|url=https://web.archive.org/web/20230602214539/https://www.theregister.com/2013/10/07/stallman_thiry_years_gnu/|site=Theregister|date=7 Oct 2013|consulté le=}}.</ref>. Incontestablement, le mouvement Wikimédia a hérité des enjeux et valeurs transmis au mouvement des logiciels libres par une contre-culture dont le phylosophe [[w:fr: André Gorz|André Gorz]], père de la [[w:fr: Décroissance|décroissance]]<ref>{{Ouvrage|langue=|prénom1=David|nom1=Murray|prénom2=Cédric|nom2=Biagini|prénom3=Pierre|nom3=Thiesset|prénom4=Cyberlibris|nom4=ScholarVox International|titre=Aux origines de la décroissance: cinquante penseurs|date=2017|isbn=978-2-89719-329-4|isbn2=978-2-89719-330-0|isbn3=978-2-89719-331-7|oclc=1248948596}}.</ref> et théoricien de l’[[w:fr: Écologie politique|écologie politique]]<ref>{{Ouvrage|langue=|prénom1=André|nom1=Gorz|titre=Ecologie et politique: nouv. ed. et remaniee.|éditeur=Éditions du Seuil|date=1978|isbn=978-2-02-004771-5|oclc=796186896}}.</ref>, nous dit ceci<ref>{{Lien web|langue=|auteur=André Gorz|titre=Le travail dans la sortie du capitalisme|url=https://web.archive.org/web/20200921155055/http://ecorev.org/spip.php?article641|site=Revue Critique d'Écologie Politique|lieu=|date=7 janvier 2008}}.</ref> : <blockquote> La lutte engagée entre les "logiciels propriétaires" et les "logiciels libres" (libre, "free", est aussi l’équivalent anglais de "gratuit") a été le coup d’envoi du conflit central de l’époque. Il s’étend et se prolonge dans la lutte contre la marchandisation de richesses premières – la terre, les semences, le génome, les biens culturels, les savoirs et compétences communs, constitutifs de la culture du quotidien et qui sont les préalables de l’existence d’une société. De la tournure que prendra cette lutte dépend la forme civilisée ou barbare que prendra la sortie du capitalisme. </blockquote> [[Fichier:Wikimania_stallman_keynote2.jpg|alt=Photo de Richard Stallman lors du premier rassemblement Wikimania de 2005|vignette|<small>Figure 23. Photo de Richard Stallman lors du premier rassemblement internationnal du mouvement Wikimédia en 2005.</small>|gauche|300x300px]] En possédant le seul [[w:fr: Nom de domaine|nom de domaine]] non commercial du top 50 des sites les plus fréquentés du Web<ref>{{Lien web|auteur=Alexa|titre=Top sites|url=https://www.alexa.com/topsites|consulté le=}}.</ref>, le mouvement Wikimédia apparaît donc comme l’une des pierres angulaires de cette lutte entre la libèrté d'usage et la propriété marchande. Car au-delà du code informatique, s’il y a bien une chose qui se marchandise au sein de l’[[v:fr:L'écoumène numérique|écoumène numérique]], c’est sans aucun doute le savoir. Et un savoir qui se décline en information, lorsqu'il s'agit de récolter tout type de données relatives à l’identité et aux comportements des utilisateurs et utilisatrices d'Internet. Un « nouvel or noir » diront certains, alors que d’autres préfèrent parler de « capitalisme 3.0<ref>{{Ouvrage|langue=|prénom1=Philippe|nom1=Escande|prénom2=Sandrine|nom2=Cassini|titre=Bienvenue dans le capitalisme 3.0|éditeur=Albin Michel|date=2015|isbn=978-2-226-31914-2|oclc=954080043}}.</ref> » ou encore de « capitalisme de surveillance<ref>{{Ouvrage|langue=|prénom1=Shoshana|nom1=Zuboff|titre=L'âge du capitalisme de surveillance|éditeur=Zulma|date=2020|isbn=978-2-84304-926-2|oclc=1199962619}}.</ref><ref>{{Ouvrage|langue=|prénom1=Christophe|nom1=Masutti|prénom2=Francesca|nom2=Musiani|titre=Affaires privées : aux sources du capitalisme de surveillance|éditeur=Caen : C&F éditions|collection=Société numérique|date=2020|isbn=978-2-37662-004-4|oclc=1159990604|consulté le=}}.</ref> ». Il est évident que les enjeux de cette lutte ne sont pas faciles à comprendre, en raison notamment de la complexité de l’infrastructure informatique, mais aussi parce que ce combat s’inscrit dans une révolution que [[w:fr: Rémy Rieffel|Rémy Rieffel]] décrit à juste titre, comme : « instable et ambivalente, simultanément porteuse de promesse et lourde de menaces ». Et cela est d'autant plus vrai qu’elle prend place « dans un contexte où s’affrontent des valeurs d’émancipation et d’ouverture d’un côté et des stratégies de contrôle et de domination de l’autre<ref>{{Ouvrage|langue=|auteur=|prénom1=Rémy|nom1=Rieffel|titre=Révolution numérique, révolution culturelle ?|passage=20|lieu=|éditeur=Folio|date=2014|pages totales=|isbn=978-2-07-045172-2|oclc=953333541|lire en ligne=|consulté le=}}.</ref> ». En fait d’ambivalence, certain faits sont significatifs. Il est surprenant d'apprendre par exemple que Jimmy Wales par exemple, alors qu'il finança le projet Wikipédia avant de transfèrer les avoirs de sa société à la fondation Wikimédia, est un adepte de l’[[w:fr:Objectivisme (Ayn Rand)|objectivisme]] ? Soit une philosophie politique dans laquelle le capitalisme est perçu comme la forme idéale d’organisation de la société<ref>{{Ouvrage|langue=|prénom1=Ayn|nom1=Rand|prénom2=Nathaniel|nom2=Branden|prénom3=Alan|nom3=Greenspan|prénom4=Robert|nom4=Hessen|titre=Capitalism: the unknown ideal|date=2013|isbn=978-0-451-14795-0|oclc=1052843511|consulté le=}}.</ref> et pour laquelle, l’intention morale de l’existence est la poursuite de l’égoïsme rationnel<ref>{{Ouvrage|langue=|prénom1=Ayn|nom1=Rand|titre=La vertu d'égoïsme|éditeur=Les Belles lettres|date=2011|isbn=978-2-251-39046-8|oclc=937494401|consulté le=}}.</ref>. Comme preuve d'instabilité apparait ensuite les appels répètés de [[w:fr:Tim Berners-Lee|Tim Berners-Lee]] au sujet de la « [[w:fr:Redécentralisation d'Internet|redécentralisation]]<ref>{{Lien web|langue=|auteur=Liat Clark|titre=Tim Berners-Lee : we need to re-decentralise the web|url=https://web.archive.org/web/20201111164058/https://www.wired.co.uk/article/tim-berners-lee-reclaim-the-web|site=Wired UK|éditeur=|date=6 February 2014|consulté le=}}.</ref> » et la « régulation<ref>{{Lien web|auteur=Elsa Trujillo|titre=Tim Berners-Lee, inventeur du Web, appelle à la régulation de Facebook, Google et Twitter|url=https://web.archive.org/web/20201129111413/https://www.lefigaro.fr/secteur/high-tech/2018/03/12/32001-20180312ARTFIG00179-tim-berners-lee-inventeur-du-web-appelle-a-la-regulation-de-facebook-google-et-twitter.php|site=Le figaro|éditeur=|date=12/03/2018|consulté le=}}.</ref> » de l’espace web dont il fut le concepteur. Cela pendant que des milliards d’[[w:Internet des objets|objets connectés]] à Internet nourricent un marché qui dépasserait déjà les 2.6 milliards d’euros rien qu’en France et pour l’année 2020<ref>{{Lien web|langue=|auteur=Tristan Gaudiaut|titre=Infographie: L'essor de l'Internet des objets|url=https://web.archive.org/web/20211004110619/https://fr.statista.com/infographie/24353/chiffre-affaires-marche-iot-objets-connectes-france/|site=Statista Infographies|date=30 sept. 2021|consulté le=}}.</ref>. Au niveau du contôle et au-delà de ce qui est oppéré par les [[w:GAFAM|GAFAM]], c'est bien sûr au niveau des états et et des gouvernements que l'attention se porte. Dans le cadre du mouvement Wikimédia, le désir de s’émanciper des contrôles étatiques entraine parfois la censure des projets Wikimédia par des gouvernements. Ce fut ainsi le cas temporairement en Turquie, en Russie, en Iran et même au Royaume-Uni, ou encore en en Chine, avec un blocage permanent depuis depuis 2004<ref>{{Lien web|langue=|auteur=Christine Siméone|titre=Censurée en Turquie et en Chine, remise en cause en Russie, ces pays qui en veulent à Wikipédia|url=https://web.archive.org/web/20200225091639/https://www.franceinter.fr/societe/censuree-en-turquie-et-en-chine-remise-en-cause-en-russe-ces-pays-qui-remettent-wikipedia-en-cause|site=France Inter|lieu=|date=2019-12-26|consulté le=}}.</ref>. Dans d'autres contextes, des procédures juridiques peuvent être utilisées pour intimider les membres du mouvement. Ce fut le cas en France, lorsque le directeur de l’association locale fut menacé de poursuites pénales par la Direction Centrale du Renseignement Intérieures, dans le cadre d’une affaire liée à un article Wikipédia crée au sujet d’une station militaire<ref>{{Lien web|langue=|auteur=Stéphane Moccozet|titre=Une station hertzienne militaire du Puy-de-Dôme au cœur d'un désaccord entre Wikipédia et la DCRI|url=https://web.archive.org/web/20201124101244/https://france3-regions.francetvinfo.fr/auvergne-rhone-alpes/2013/04/06/un-station-hertzienne-militaire-du-puy-de-dome-au-coeur-d-un-desaccord-entre-wipikedia-et-la-dcri-229791.html|site=France 3 Auvergne-Rhône-Alpes|lieu=|date=06/04/2013|consulté le=}}.</ref>. En France, cette intimidation se limita à des menaces, mais en Biélorussie, l’éditeur [[w:Mark_Bernstein|Mark Bernstein]] fut réelement condamné à quinze jours de prison assorti de trois ans d’assignation à résidence, en raison de propos tenus sur la guerre en Ukraine<ref>{{Lien web|titre=Entrepreneur, Activist Mark Bernstein Detained In Minsk - Charter'97 :: News from Belarus - Belarusian News - Republic of Belarus - Minsk|url=https://web.archive.org/web/20220312011414/https://charter97.org/en/news/2022/3/11/458592/|site=Charter97|date=2022-03-11|consulté le=|auteur=Charter97}}.</ref>. D'un côté, l’espace Web a permis le développement de monopoles lucratif, à l’image des [[w:fr:GAFAM|GAFAM]], [[w:fr:BATX|BATX]], [[w:fr:NATU (Netflix, Airbnb, Tesla et Uber)|NATU]] et autres [[w:fr:Géants du web|géants du web]] accusés d'[[w:fr:Abus de position dominante|abus de position dominante]], alors que de l'autre, certains projets à prétentions commerciales, peuvent étonament donner naissance à des projets de partage autonomes. Comme vu précédement, ce fut le cas du projet commercial Nupedia qui abouti à la création de Wikipédia et de la fondation Wikimédia. Un scénariot très similaire, par ailleurs, à ce qui permis le développement du navigateur [[Firefox]] et la création de la [[w:Mozilla_Foundation|fondation Mozilla]], suite à la faillite de la société commerciale [[w:Netscape_Communications|Netscape Communications]]. Dans un autre contexte, un succès commercial tel que la messagerie instantanée [[w:fr:MSN Messenger|MSN Messenger]], a servi d'inpiration pour d’autres succès commerciaux, à l'images des nombreux réseaux sociaux apparus sur le web. Cela alors qu'au niveau de la sphère du partage autonome, un succès non commercial tel que le projet Wikipédia, a inspiré la création d’autres projets collaboratifs et sans but lucratif, parmi lesquels figure le projet [[w:fr:OpenStreetMap|OpenStreetMap]] dédié à la cartographie du monde sous licence libre. [[Fichier:Davide_Dormino_-_Anything_to_say.jpg|alt=Davide Dormino prenant place sur sa sculpture debout sur une chaise à côté de trois lanceurs d'alertes|vignette|<small>Figure 24. Sculpture en bronze de Davide Dormino intitulée ''[[w:Anything_to_say?|Anything to say?]]'' à l’honneur des trois lanceurs d’alertes que sont de gauche à droite : Edward Snowden, Julian Assange et Chelsea Manning.</small>|350x350px]] Tout se passe donc comme si au sein de l’espace numérique se perpétuait une opposition perpétuelle entre d’un côté, une recherche de pouvoir économique et politique centralisé au profit de quelques acteurs privilégiés, et de l’autre, un désir de partage et d’autonomie défendus par d'autres acteurs de la population humaine. Dans ce cadre, nous avons ainsi découvert que certains courants sociaux que l’on pourrait croire entièrement disparus continuent à influencer la manière dont fonctionnent nos sociétés. Car cinquante ans plus tard, et même si les termes et appelations ne sont plus les mêmes, il est évident que la figure emblématique contemporaine du [[w:fr:Lanceur d'alerte|lanceur ou de la lanceuse d’alerte]], est idéologiquement proche des figures contestataires apparues au sein de la contre-culture des années 1960. Certains Wikimédiens tels que [[w:Aaron Swartz|Aaron Swartz]], [[w:Bassel Khartabil|Bassel Khartabil]], [[w:Pavel_Pernikov|Pavel Pernikov]], [[w:Ihor_Kostenko|Ihor Kostenko]] et [[w:Mark_Bernstein|Mark Bernstein]] ont en effet perdu leur vie ou leur liberté pour défendre les valeurs présentées tout au long de cette première partie d’ouvrage. De manière similaire à [[w:Julian Assange|Julian Assange]], [[w:Edward Snowden|Edward Snowden]] et [[w:Chelsea Manning|Chelsea Manning]], on peut dire d’eux, qu’ils « ont perdu leur liberté pour défendre la nôtre »<ref>{{Lien web|titre=Berlin: Des statues à l'effigie des lanceurs d'alerte Snowden, Manning et Assange|url=https://web.archive.org/web/20230326124921/https://www.20minutes.fr/insolite/1601039-20150504-berlin-statues-effigie-lanceurs-alerte-snowden-manning-assange|site=20minutes.fr|date=04/05/2015|consulté le=|auteur=B.D.}}.</ref>. Même au sein du mouvement Wikimédia et comme cela fut présenté dans l'introduction de cet ouvrage, une alerte peut être lancée sous la forme d'un appel à commentaires pour réagir à une décision prise par la Fondation Wikimédia. En raison d'une organisation et de structures plus proches de ce qui se développe dans le système économique classique, les organisations hors ligne activent au sein du mouvement Wikimédia ne partagent effectivement pas toujours la même vision, ni parfois les mêmes valeurs, que celles dévelloppées au sein des communautés de contributeurs bénévoles actifs au sein des projets<ref name=":0">{{Ouvrage|langue=fr|prénom1=Lionel|nom1=Scheepmans|lien auteur1=user:Lionel Scheepmans|titre=Imagine un monde : quand le mouvement Wikimédia nous aide à penser de manière prospective la société globale et numérique de demain|éditeur=UCL - Université Catholique de Louvain|année=2022|date=17/06/2022|lire en ligne=https://dial.uclouvain.be/pr/boreal/object/boreal:264603|consulté le=2024-03-10|nature article=Thèse de doctorat}}</ref>. Au même titre que les autres sites web hébergés par le mouvement, cela n'a pas pour autant empêché Wikipédia de développer des nombreuses règles et recommandations qui s'apparentent fortement à des lignes éditoriales. Et même au sein du mouvement tout entier d'adopter un code de conduite universeldans le but d’établir un « référentiel minimum des comportements acceptables et inacceptables »<ref>{{Lien web|titre=Policy:Universal Code of Conduct/fr|url=https://web.archive.org/web/20251007061014/https://foundation.wikimedia.org/wiki/Policy:Universal_Code_of_Conduct/fr|site=|date=|consulté le=|auteur=Wikimedia Foundation Governance Wiki}}.</ref>. Cependant et à l'image du mouvement Wikimédia, reste perçu tel un « bazar libertaire »<ref>{{Lien web|langue=|auteur=Frédéric Joignot|titre=Wikipédia, bazar libertaire|url=https://web.archive.org/web/20170630065818/http://www.lemonde.fr/technologies/article/2012/01/14/wikipedia-bazar-libertaire_1629135_651865.html|site=Le Monde|lieu=|date=2012|consulté le=}}.</ref>, dont un principe fondateur d'interprétation créative des règles<ref>{{Lien web|langue=|auteur=Wikipédia|titre=Wikipédia: Interprétation créative des règles|url=https://fr.wikipedia.org/wiki/Wikip&#233;dia:Interpr&#233;tation_cr&#233;ative_des_r&#232;gles|consulté le=}}.</ref>, toujours actif à ce jour, illustre clairement l’orientation politique :<blockquote>N’hésitez pas à contribuer, même si vous ne connaissez pas l’ensemble des règles, et si vous en rencontrez une qui, dans votre situation, semble gêner à l’élaboration de l’encyclopédie, ignorez-la ou, mieux, corrigez-la.</blockquote>Une telle invitation semble, à elle seule, illustrer les valeurs d’universalité, de liberté, de décentralisation, de partage, de collaboration et de mérite décrites par Steven Levy dans son ouvrage ''L’Éthique des hackers''<ref>{{Ouvrage|langue=|prénom1=Steven|nom1=Levy|prénom2=Gilles|nom2=Tordjman|titre=L'éthique des hackers|éditeur=Globe|date=2013|isbn=978-2-211-20410-1|oclc=844898302}}.</ref>. En ce sens, Wikipédia ne faisait qu'intégrer une nouvelle perception du monde basée sur l'ouverte, la transparence et la liberté apparue des années avant la création d'une encyclopédie, écrite et gérée par des millions de contributeurs et contributrices situés aux quatre coins du monde. Ce que démontre cette première partie d'ouvrage est donc qu'il semble se maintenir, au sein de la communauté Wikimédia comme dans d'autres endoits dans le monde, une posture réfractaire à la marchandisation du monde d'une part. Mais aussi d'autre part,des philosophie pronant la liberté d’information et le secret de la communication face à « l’interférence du gouvernement et des grandes sociétés<ref>{{Lien web|auteur=[[w:Timothy C. May|]]|titre=Manifeste Crypto-Anarchiste|url=https://web.archive.org/web/20221208203642/https://www.larevuedesressources.org/manifeste-crypto-anarchiste,2316.html|site=La Revue des Ressources|date=4 mai 2012|consulté le=}}.</ref> ». Ces deux positionnement idéologiques s'opposent ainsi à une [[w:fr:Hégémonie culturelle|hégémonie culturelle]]<ref>{{Ouvrage|langue=|prénom1=Antonio|nom1=Gramsci|titre=Textes|passage=210|éditeur=Editions Sociales|date=1983|isbn=978-2-209-05518-0|oclc=12842792}}.</ref> capitalo-élitiste qui n'est pas nouvelle, mais qui ne cesse d'évoluer dans un monde toujours plus global et numérique. {{AutoCat}} c6ah8z31eeghwuid88d4o0hosrzvfp9 764085 764084 2026-04-20T11:37:31Z Lionel Scheepmans 20012 764085 wikitext text/x-wiki <noinclude>{{Le mouvement Wikimédia}}</noinclude> [[Fichier:Wikimedia-logo black.svg|vignette|150x150px|<small>Figure 21. Logos du mouvement Wikimédia et de sa Fondation.</small>]] [[Fichier:Peace sign.svg|vignette|150x150px|<small>Figure 22. Logo du mouvement hippie et de la contre-culture.</small>]] Au terme de cette première partie d’ouvrage, il apparait évident que la révolution numérique, que l’on considère généralement comme une révolution technique, fut aussi, et peut-être avant tout, une révolution sociale et culturelle. Au sein de celle-ci et sur base du lancement du projet Wikipédia en ce début de XI<sup>ème</sup> siecle, nous venons de voir comment le mouvement Wikimédia représent l'une des manifestations les plus visible, si pas la plus visible, de ce qu'il reste de la [[w:Contre-culture_des_années_1960|contre-culture des années 1960]], apparue en opposition à une hégémonie culturelle paternaliste favorable à la marchandisation du monde et la [[w:Guerre_du_Viêt_Nam|guerre du Viêt Nam]]. Est-ce un hasard d'ailleurs si le renversement du logo du mouvement Wikimédia a une certaine familiarité avec celui du mouvement [[w:fr:Hippie|Hippie]] ? Sans compter que Richard Stallman, celui qui à créer le concept d'encyclopédie libre, fut reconnu en tant que gourou de la contre-culture hacker<ref>{{Ouvrage|auteur=|prénom1=Divers|nom1=auteurs|titre=L'Éthique Hacker|passage=11|éditeur=U.C.H Pour la Liberté|date=Version 9.3|pages totales=56|lire en ligne=https://web.archive.org/web/20211031170831/https://repo.zenk-security.com/Others/L%20Ethique%20Hacker.pdf}}.</ref> et père du système d’exploitation hippie<ref>{{Lien web|langue=|auteur=Gavin Clarke|titre=Stallman's GNU at 30: The hippie OS that foresaw the rise of Apple — and is now trying to take it on|url=https://web.archive.org/web/20230602214539/https://www.theregister.com/2013/10/07/stallman_thiry_years_gnu/|site=Theregister|date=7 Oct 2013|consulté le=}}.</ref>. Incontestablement, le mouvement Wikimédia a hérité des enjeux et valeurs transmis au mouvement des logiciels libres par une contre-culture dont le phylosophe [[w:fr: André Gorz|André Gorz]], père de la [[w:fr: Décroissance|décroissance]]<ref>{{Ouvrage|langue=|prénom1=David|nom1=Murray|prénom2=Cédric|nom2=Biagini|prénom3=Pierre|nom3=Thiesset|prénom4=Cyberlibris|nom4=ScholarVox International|titre=Aux origines de la décroissance: cinquante penseurs|date=2017|isbn=978-2-89719-329-4|isbn2=978-2-89719-330-0|isbn3=978-2-89719-331-7|oclc=1248948596}}.</ref> et théoricien de l’[[w:fr: Écologie politique|écologie politique]]<ref>{{Ouvrage|langue=|prénom1=André|nom1=Gorz|titre=Ecologie et politique: nouv. ed. et remaniee.|éditeur=Éditions du Seuil|date=1978|isbn=978-2-02-004771-5|oclc=796186896}}.</ref>, nous dit ceci<ref>{{Lien web|langue=|auteur=André Gorz|titre=Le travail dans la sortie du capitalisme|url=https://web.archive.org/web/20200921155055/http://ecorev.org/spip.php?article641|site=Revue Critique d'Écologie Politique|lieu=|date=7 janvier 2008}}.</ref> : <blockquote> La lutte engagée entre les "logiciels propriétaires" et les "logiciels libres" (libre, "free", est aussi l’équivalent anglais de "gratuit") a été le coup d’envoi du conflit central de l’époque. Il s’étend et se prolonge dans la lutte contre la marchandisation de richesses premières – la terre, les semences, le génome, les biens culturels, les savoirs et compétences communs, constitutifs de la culture du quotidien et qui sont les préalables de l’existence d’une société. De la tournure que prendra cette lutte dépend la forme civilisée ou barbare que prendra la sortie du capitalisme. </blockquote> [[Fichier:Wikimania_stallman_keynote2.jpg|alt=Photo de Richard Stallman lors du premier rassemblement Wikimania de 2005|vignette|<small>Figure 23. Photo de Richard Stallman lors du premier rassemblement internationnal du mouvement Wikimédia en 2005.</small>|gauche|300x300px]] En possédant le seul [[w:fr: Nom de domaine|nom de domaine]] non commercial du top 50 des sites les plus fréquentés du Web<ref>{{Lien web|auteur=Alexa|titre=Top sites|url=https://www.alexa.com/topsites|consulté le=}}.</ref>, le mouvement Wikimédia apparaît donc comme l’une des pierres angulaires de cette lutte entre la libèrté d'usage et la propriété marchande. Car au-delà du code informatique, s’il y a bien une chose qui se marchandise au sein de l’[[v:fr:L'écoumène numérique|écoumène numérique]], c’est sans aucun doute le savoir. Et un savoir qui se décline en information, lorsqu'il s'agit de récolter tout type de données relatives à l’identité et aux comportements des utilisateurs et utilisatrices d'Internet. Un « nouvel or noir » diront certains, alors que d’autres préfèrent parler de « capitalisme 3.0<ref>{{Ouvrage|langue=|prénom1=Philippe|nom1=Escande|prénom2=Sandrine|nom2=Cassini|titre=Bienvenue dans le capitalisme 3.0|éditeur=Albin Michel|date=2015|isbn=978-2-226-31914-2|oclc=954080043}}.</ref> » ou encore de « capitalisme de surveillance<ref>{{Ouvrage|langue=|prénom1=Shoshana|nom1=Zuboff|titre=L'âge du capitalisme de surveillance|éditeur=Zulma|date=2020|isbn=978-2-84304-926-2|oclc=1199962619}}.</ref><ref>{{Ouvrage|langue=|prénom1=Christophe|nom1=Masutti|prénom2=Francesca|nom2=Musiani|titre=Affaires privées : aux sources du capitalisme de surveillance|éditeur=Caen : C&F éditions|collection=Société numérique|date=2020|isbn=978-2-37662-004-4|oclc=1159990604|consulté le=}}.</ref> ». Il est évident que les enjeux de cette lutte ne sont pas faciles à comprendre, en raison notamment de la complexité de l’infrastructure informatique, mais aussi parce que ce combat s’inscrit dans une révolution que [[w:fr: Rémy Rieffel|Rémy Rieffel]] décrit à juste titre, comme : « instable et ambivalente, simultanément porteuse de promesse et lourde de menaces ». Et cela est d'autant plus vrai qu’elle prend place « dans un contexte où s’affrontent des valeurs d’émancipation et d’ouverture d’un côté et des stratégies de contrôle et de domination de l’autre<ref>{{Ouvrage|langue=|auteur=|prénom1=Rémy|nom1=Rieffel|titre=Révolution numérique, révolution culturelle ?|passage=20|lieu=|éditeur=Folio|date=2014|pages totales=|isbn=978-2-07-045172-2|oclc=953333541|lire en ligne=|consulté le=}}.</ref> ». En fait d’ambivalence, certain faits sont significatifs. Il est surprenant d'apprendre par exemple que Jimmy Wales par exemple, alors qu'il finança le projet Wikipédia avant de transfèrer les avoirs de sa société à la fondation Wikimédia, est un adepte de l’[[w:fr:Objectivisme (Ayn Rand)|objectivisme]] ? Soit une philosophie politique dans laquelle le capitalisme est perçu comme la forme idéale d’organisation de la société<ref>{{Ouvrage|langue=|prénom1=Ayn|nom1=Rand|prénom2=Nathaniel|nom2=Branden|prénom3=Alan|nom3=Greenspan|prénom4=Robert|nom4=Hessen|titre=Capitalism: the unknown ideal|date=2013|isbn=978-0-451-14795-0|oclc=1052843511|consulté le=}}.</ref> et pour laquelle, l’intention morale de l’existence est la poursuite de l’égoïsme rationnel<ref>{{Ouvrage|langue=|prénom1=Ayn|nom1=Rand|titre=La vertu d'égoïsme|éditeur=Les Belles lettres|date=2011|isbn=978-2-251-39046-8|oclc=937494401|consulté le=}}.</ref>. Comme preuve d'instabilité apparait ensuite les appels répètés de [[w:fr:Tim Berners-Lee|Tim Berners-Lee]] au sujet de la « [[w:fr:Redécentralisation d'Internet|redécentralisation]]<ref>{{Lien web|langue=|auteur=Liat Clark|titre=Tim Berners-Lee : we need to re-decentralise the web|url=https://web.archive.org/web/20201111164058/https://www.wired.co.uk/article/tim-berners-lee-reclaim-the-web|site=Wired UK|éditeur=|date=6 February 2014|consulté le=}}.</ref> » et la « régulation<ref>{{Lien web|auteur=Elsa Trujillo|titre=Tim Berners-Lee, inventeur du Web, appelle à la régulation de Facebook, Google et Twitter|url=https://web.archive.org/web/20201129111413/https://www.lefigaro.fr/secteur/high-tech/2018/03/12/32001-20180312ARTFIG00179-tim-berners-lee-inventeur-du-web-appelle-a-la-regulation-de-facebook-google-et-twitter.php|site=Le figaro|éditeur=|date=12/03/2018|consulté le=}}.</ref> » de l’espace web dont il fut le concepteur. Cela pendant que des milliards d’[[w:Internet des objets|objets connectés]] à Internet nourricent un marché qui dépasserait déjà les 2.6 milliards d’euros rien qu’en France et pour l’année 2020<ref>{{Lien web|langue=|auteur=Tristan Gaudiaut|titre=Infographie: L'essor de l'Internet des objets|url=https://web.archive.org/web/20211004110619/https://fr.statista.com/infographie/24353/chiffre-affaires-marche-iot-objets-connectes-france/|site=Statista Infographies|date=30 sept. 2021|consulté le=}}.</ref>. Au niveau du contôle et au-delà de ce qui est oppéré par les [[w:GAFAM|GAFAM]], c'est bien sûr au niveau des états et et des gouvernements que l'attention se porte. Dans le cadre du mouvement Wikimédia, le désir de s’émanciper des contrôles étatiques entraine parfois la censure des projets Wikimédia par des gouvernements. Ce fut ainsi le cas temporairement en Turquie, en Russie, en Iran et même au Royaume-Uni, ou encore en en Chine, avec un blocage permanent depuis depuis 2004<ref>{{Lien web|langue=|auteur=Christine Siméone|titre=Censurée en Turquie et en Chine, remise en cause en Russie, ces pays qui en veulent à Wikipédia|url=https://web.archive.org/web/20200225091639/https://www.franceinter.fr/societe/censuree-en-turquie-et-en-chine-remise-en-cause-en-russe-ces-pays-qui-remettent-wikipedia-en-cause|site=France Inter|lieu=|date=2019-12-26|consulté le=}}.</ref>. Dans d'autres contextes, des procédures juridiques peuvent être utilisées pour intimider les membres du mouvement. Ce fut le cas en France, lorsque le directeur de l’association locale fut menacé de poursuites pénales par la Direction Centrale du Renseignement Intérieures, dans le cadre d’une affaire liée à un article Wikipédia crée au sujet d’une station militaire<ref>{{Lien web|langue=|auteur=Stéphane Moccozet|titre=Une station hertzienne militaire du Puy-de-Dôme au cœur d'un désaccord entre Wikipédia et la DCRI|url=https://web.archive.org/web/20201124101244/https://france3-regions.francetvinfo.fr/auvergne-rhone-alpes/2013/04/06/un-station-hertzienne-militaire-du-puy-de-dome-au-coeur-d-un-desaccord-entre-wipikedia-et-la-dcri-229791.html|site=France 3 Auvergne-Rhône-Alpes|lieu=|date=06/04/2013|consulté le=}}.</ref>. En France, cette intimidation se limita à des menaces, mais en Biélorussie, l’éditeur [[w:Mark_Bernstein|Mark Bernstein]] fut réelement condamné à quinze jours de prison assorti de trois ans d’assignation à résidence, en raison de propos tenus sur la guerre en Ukraine<ref>{{Lien web|titre=Entrepreneur, Activist Mark Bernstein Detained In Minsk - Charter'97 :: News from Belarus - Belarusian News - Republic of Belarus - Minsk|url=https://web.archive.org/web/20220312011414/https://charter97.org/en/news/2022/3/11/458592/|site=Charter97|date=2022-03-11|consulté le=|auteur=Charter97}}.</ref>. D'un côté, l’espace Web a permis le développement de monopoles lucratif, à l’image des [[w:fr:GAFAM|GAFAM]], [[w:fr:BATX|BATX]], [[w:fr:NATU (Netflix, Airbnb, Tesla et Uber)|NATU]] et autres [[w:fr:Géants du web|géants du web]] accusés d'[[w:fr:Abus de position dominante|abus de position dominante]], alors que de l'autre, certains projets à prétentions commerciales, peuvent étonament donner naissance à des projets de partage autonomes. Comme vu précédement, ce fut le cas du projet commercial Nupedia qui abouti à la création de Wikipédia et de la fondation Wikimédia. Un scénariot très similaire, par ailleurs, à ce qui permis le développement du navigateur [[Firefox]] et la création de la [[w:Mozilla_Foundation|fondation Mozilla]], suite à la faillite de la société commerciale [[w:Netscape_Communications|Netscape Communications]]. Dans un autre contexte, un succès commercial tel que la messagerie instantanée [[w:fr:MSN Messenger|MSN Messenger]], a servi d'inpiration pour d’autres succès commerciaux, à l'images des nombreux réseaux sociaux apparus sur le web. Cela alors qu'au niveau de la sphère du partage autonome, un succès non commercial tel que le projet Wikipédia, a inspiré la création d’autres projets collaboratifs et sans but lucratif, parmi lesquels figure le projet [[w:fr:OpenStreetMap|OpenStreetMap]] dédié à la cartographie du monde sous licence libre. [[Fichier:Davide_Dormino_-_Anything_to_say.jpg|alt=Davide Dormino prenant place sur sa sculpture debout sur une chaise à côté de trois lanceurs d'alertes|vignette|<small>Figure 24. Sculpture en bronze de Davide Dormino intitulée ''[[w:Anything_to_say?|Anything to say?]]'' à l’honneur des trois lanceurs d’alertes que sont de gauche à droite : Edward Snowden, Julian Assange et Chelsea Manning.</small>|350x350px]] Tout se passe donc comme si au sein de l’espace numérique se perpétuait une opposition perpétuelle entre d’un côté, une recherche de pouvoir économique et politique centralisé au profit de quelques acteurs privilégiés, et de l’autre, un désir de partage et d’autonomie défendus par d'autres acteurs de la population humaine. Dans ce cadre, nous avons ainsi découvert que certains courants sociaux que l’on pourrait croire entièrement disparus continuent à influencer la manière dont fonctionnent nos sociétés. Car cinquante ans plus tard, et même si les termes et appelations ne sont plus les mêmes, il est évident que la figure emblématique contemporaine du [[w:fr:Lanceur d'alerte|lanceur ou de la lanceuse d’alerte]], est idéologiquement proche des figures contestataires apparues au sein de la contre-culture des années 1960. Certains Wikimédiens tels que [[w:Aaron Swartz|Aaron Swartz]], [[w:Bassel Khartabil|Bassel Khartabil]], [[w:Pavel_Pernikov|Pavel Pernikov]], [[w:Ihor_Kostenko|Ihor Kostenko]] et [[w:Mark_Bernstein|Mark Bernstein]] ont en effet perdu leur vie ou leur liberté pour défendre les valeurs présentées tout au long de cette première partie d’ouvrage. De manière similaire à [[w:Julian Assange|Julian Assange]], [[w:Edward Snowden|Edward Snowden]] et [[w:Chelsea Manning|Chelsea Manning]], on peut dire d’eux, qu’ils « ont perdu leur liberté pour défendre la nôtre »<ref>{{Lien web|titre=Berlin: Des statues à l'effigie des lanceurs d'alerte Snowden, Manning et Assange|url=https://web.archive.org/web/20230326124921/https://www.20minutes.fr/insolite/1601039-20150504-berlin-statues-effigie-lanceurs-alerte-snowden-manning-assange|site=20minutes.fr|date=04/05/2015|consulté le=|auteur=B.D.}}.</ref>. Même au sein du mouvement Wikimédia et comme cela fut présenté dans l'introduction de cet ouvrage, une alerte peut être lancée sous la forme d'un appel à commentaires pour réagir à une décision prise par la Fondation Wikimédia. En raison d'une organisation et de structures plus proches de ce qui se développe dans le système économique classique, les organisations hors ligne activent au sein du mouvement Wikimédia ne partagent effectivement pas toujours la même vision, ni parfois les mêmes valeurs, que celles dévelloppées au sein des communautés de contributeurs bénévoles actifs au sein des projets<ref name=":0">{{Ouvrage|langue=fr|prénom1=Lionel|nom1=Scheepmans|lien auteur1=user:Lionel Scheepmans|titre=Imagine un monde : quand le mouvement Wikimédia nous aide à penser de manière prospective la société globale et numérique de demain|éditeur=UCL - Université Catholique de Louvain|année=2022|date=17/06/2022|lire en ligne=https://dial.uclouvain.be/pr/boreal/object/boreal:264603|consulté le=2024-03-10|nature article=Thèse de doctorat}}</ref>. Au même titre que les autres sites web hébergés par le mouvement, cela n'a pas pour autant empêché Wikipédia de développer des nombreuses règles et recommandations qui s'apparentent fortement à des lignes éditoriales, ni le mouvement d'adopter un code de conduite universeldans le but d’établir un « référentiel minimum des comportements acceptables et inacceptables »<ref>{{Lien web|titre=Policy:Universal Code of Conduct/fr|url=https://web.archive.org/web/20251007061014/https://foundation.wikimedia.org/wiki/Policy:Universal_Code_of_Conduct/fr|site=|date=|consulté le=|auteur=Wikimedia Foundation Governance Wiki}}.</ref>. Mais tout cela au sein d'une sorte de « bazar libertaire »<ref>{{Lien web|langue=|auteur=Frédéric Joignot|titre=Wikipédia, bazar libertaire|url=https://web.archive.org/web/20170630065818/http://www.lemonde.fr/technologies/article/2012/01/14/wikipedia-bazar-libertaire_1629135_651865.html|site=Le Monde|lieu=|date=2012|consulté le=}}.</ref>, dont le principe fondateur d'interprétation créative des règles<ref>{{Lien web|langue=|auteur=Wikipédia|titre=Wikipédia: Interprétation créative des règles|url=https://fr.wikipedia.org/wiki/Wikip&#233;dia:Interpr&#233;tation_cr&#233;ative_des_r&#232;gles|consulté le=}}.</ref> reste affiché jusqu'à ce jour :<blockquote>N’hésitez pas à contribuer, même si vous ne connaissez pas l’ensemble des règles, et si vous en rencontrez une qui, dans votre situation, semble gêner à l’élaboration de l’encyclopédie, ignorez-la ou, mieux, corrigez-la.</blockquote>Une simple phrase, qui a elle seule témoigne de l'héritage de toute une idéologie décrites par Steven Levy dans son ouvrage ''L’Éthique des hackers''<ref>{{Ouvrage|langue=|prénom1=Steven|nom1=Levy|prénom2=Gilles|nom2=Tordjman|titre=L'éthique des hackers|éditeur=Globe|date=2013|isbn=978-2-211-20410-1|oclc=844898302}}.</ref>. Dès sa création en 2001 Wikipédia intégrerait ainsi toute une panoplie d'idées préexistant concernant la conception d'un monde basée sur l'ouverte, la transparence et la liberté. Ce que démontre cette première partie d'ouvrage, c'est donc que perdure, au sein Wikimédia, comme dans d'autres endroits du monde, une posture réfractaire à la marchandisation du monde combiné à refus de « l’interférence du gouvernement et des grandes sociétés<ref>{{Lien web|auteur=[[w:Timothy C. May|]]|titre=Manifeste Crypto-Anarchiste|url=https://web.archive.org/web/20221208203642/https://www.larevuedesressources.org/manifeste-crypto-anarchiste,2316.html|site=La Revue des Ressources|date=4 mai 2012|consulté le=}}.</ref> ». Cette lutte du partage et de la liberté, contre l'enrichissement et de contrôle n'est pas nouvelle, mais comme cela vient d'être vu, ne cesse d'évoluer dans un monde toujours plus global et numérique. {{AutoCat}} 7ldgzdhju3swcpxolbs4j98856xna16 Fonctionnement d'un ordinateur/L'unité de chargement et le program counter 0 80691 764017 763879 2026-04-19T16:45:43Z Mewtow 31375 /* Les optimisations du code automodifiant */ 764017 wikitext text/x-wiki L'unité de contrôle s'occupe du chargement des instructions et de leur interprétation, leur décodage. Elle contient deux circuits : l''''unité de chargement''' qui charge l'instruction depuis la mémoire, et le '''séquenceur''' qui commande le chemin de données. Les deux circuits sont séparés, mais ils communiquent entre eux, notamment pour gérer les branchements. Dans ce chapitre, nous allons nous intéresser au chargement d'une instruction depuis la RAM/ROM, nous verrons l'étape de décodage de l'instruction au prochain chapitre. L'unité de chargement a pour cœur le ''program counter'', le registre qui mémorise l'adresse de l'instruction à charger. Il s'agit précisément d'un compteur, vu que le contenu du registre est incrémenté à chaque fois qu'une instruction est chargée. Le ''program counter'' est remis à zéro lors du démarrage du processeur, mais est aussi modifié par les instructions de branchement qui écrivent une adresse dedans. Son interface est la suivante : une entrée d'horloge, une entrée de ''Reset'', une entrée ''Enable'' qui indique quand l'incrémenter (renommée ''Instruction Fetch''), et deux entrées pour les branchements. Pour les branchements, il y a une entrée pour l'adresse de destination, une autre pour autoriser l'écriture de celle-ci dans le ''program counter''. La sortie du ''program counter'' est reliée au bus d'adresse mémoire, plus ou moins directement. [[File:Program counter.jpg|centre|vignette|upright=2|Program counter]] ==Les interconnexions entre séquenceur et bus mémoire== Pour lire une instruction, le processeur envoie le ''program counter'' sur le bus d'adresse et récupère l'instruction sur le bus de données. L'instruction lue est alors envoyée au séquenceur. Et cela demande de connecter le séquenceur au bus mémoire, à la fois sur le bus d'adresse et le bus de données. Et sur ce point, la situation est différente selon que l'on parler d'une architecture Harvard ou Von Neumann. ===Les architectures Harvard : des bus mémoire séparés pour la RAM et la ROM=== Sur les architectures Harvard, le séquenceur et le chemin de donnée utilisent des interfaces mémoire séparées. Le séquenceur est directement relié au bus mémoire de la ROM, alors que le chemin de données est connecté à la RAM. [[File:Microarchitecture de l'interface mémoire d'une architecture Harvard.png|centre|vignette|upright=2|Microarchitecture de l'interface mémoire d'une architecture Harvard]] Le ''program counter'' est envoyé sur le bus d'adresse de la ROM, l'instruction est récupérée sur le bus de données de la ROM. Pour la mémoire RAM, elle échange des données avec le chemin de données, notamment avec les registres généraux. Les adresses utilisées pour la RAM viennent elles soit du chemin de données, soit de l'unité de contrôle, tout dépend du mode d'adressage. Mais le ''program counter'' n'est pas impliqué. [[File:Architecture Harvard - échanges de données.png|centre|vignette|upright=2|Architecture Harvard - échanges de données]] Les architectures Harvard modifiées doivent cependant rajouter une connexion entre le bus ROM et les registres généraux. C'est nécessaire pour charger une donnée constante depuis la mémoire ROM. Rappelons que la donnée constante est copiée dans un registre général, donc dans le chemin de données. [[File:Architecture Harvard modifiée - implémentation du processeur.png|centre|vignette|upright=2|Architecture Harvard modifiée - implémentation du processeur]] ===Les architectures Von-Neumann : un bus mémoire unique=== Sur les architectures Von-Neumann et affiliées, le séquenceur et le chemin de donnée partagent la même interface mémoire. Et cela pose deux problèmes. Le premier problème est que le bus mémoire doit être libéré une fois l'instruction chargée, pour un éventuel accès mémoire. Et le séquenceur doit conserver une copie de l'instruction chargée, sans quoi il ne peut pas décoder l'instruction correctement. Par exemple, si l'instruction met plusieurs cycles à s'exécuter, le séquenceur doit conserver une copie de l'instruction durant ces plusieurs cycles. Pour ça, le processeur intègre un '''registre d'instruction''' situé juste avant le séquenceur, qui mémorise l'instruction chargée. [[File:Registre d'instruction.png|centre|vignette|upright=2|Registre d'instruction.]] Le second problème est de gérer le flot des instructions/données entre le bus mémoire, le chemin de données et le séquenceur. Si le processeur lit une instruction, le bus doit être relié à l'unité de contrôle. Par contre, s'il accède à une donnée, il doit être relié au chemin de données. Le processeur doit connecter l'interface mémoire soit au séquenceur, soit au chemin de données et cela complique le réseau d'interconnexion interne au processeur. [[File:Architecture Von Neumann - implémentation du processeur.png|centre|vignette|upright=2|Architecture Von Neumann - implémentation du processeur]] Une première solution utilise un bus unique qui relie l'interface mémoire, le séquenceur et le chemin de données. Pour charger une instruction, le séquenceur copie le ''program counter'' sur le bus d'adresse, attend que l'instruction lue soit disponible sur le bus de données, puis la copie dans le registre d'instruction. Le bus mémoire est alors libre et peut être utilisé pour lire ou écrire des données, si le besoin s'en fait sentir. : Il faut noter que cette solution implique d'utiliser des registres d’interfaçage avec la mémoire. [[File:Microarchitecture de l'interface mémoire d'une architecture von neumann.png|centre|vignette|upright=2|Microarchitecture de l'interface mémoire d'une architecture von neumann]] Une autre solution utilise deux bus interne séparés : un connecté au bus d'adresse, l'autre au bus de données. Le ''program counter'' est alors connecté au bus interne d'adresse, le séquenceur est relié au bus interne de données. Notons que la technique marche bien si le ''program counter'' est dans le banc de registre : les interconnexions utilisées pour gérer l'adressage indirect permettent d'envoyer le ''program counter'' sur le bus d'adresse sans ajout de circuit. Le tout peut être amélioré en remplaçant les deux bus par des multiplexeurs et démultiplexeurs. Le bus d'adresse est précédé par un multiplexeur, qui permet de faire le choix entre ''Program Counter'', adresse venant du chemin de données, ou adresse provenant du séquenceur (adressage absolu). De même, le bus de données est suivi par un démultiplexeur qui envoie la donnée/instruction lue soit au registre d'instruction, soit au chemin de données. Le tout se marie très bien avec les chemins de donnée vu dans le chapitre précédent. Au passage, il faut noter que cette solution nécessite un banc de registre multi-port. [[File:Connexion du program counter sur les bus avec PC isolé.png|centre|vignette|upright=2|Connexion du program counter sur les bus avec PC isolé]] ==Le chargement d'une instruction== [[File:Étape de chargement.png|vignette|upright=1|Étape de chargement.]] L'étape de chargement (ou ''fetch'') doit faire deux choses : mettre à jour le ''program counter'' et lire l'instruction en mémoire RAM ou en ROM. Les deux étapes peuvent être faites en parallèle, dans des circuits séparés. Pendant que l'instruction est lue depuis la mémoire RAM/ROM, le ''program counter'' est incrémenté et/ou altéré par un branchement. Pour lire l'instruction, on envoie le ''program counter'' sur le bus d'adresse et on récupère l'instruction sur le bus de données pour l'envoyer sur l'entrée du séquenceur. ===Le chargement des instructions de longueur variable=== Le chargement des instructions de longueur variable pose de nombreux problèmes. Le premier est que mettre à jour le ''program counter'' demande de connaitre la longueur de l'instruction chargée, pour l'ajouter au ''program counter''. Si la longueur d'une instruction est variable, on doit connaitre cette longueur d'une manière où d'une autre. La solution la plus simple indique la longueur de l'instruction dans une partie de l'opcode ou de la représentation en binaire de l'instruction. Mais les processeurs haute performance de nos PC utilisent d'autres solutions, qui n'encodent pas explicitement la taille de l'instruction. Les processeurs récents utilisent un registre d'instruction de grande taille, capable de mémoriser l'instruction la plus longue du processeur. Par exemple, si un processeur supporte des instructions allant de 1 à 8 octets, comme les CPU x86, le registre d'instruction fera 8 octets. Le décodeur d'instruction vérifie à chaque cycle le contenu du registre d'instruction. Si une instruction est détectée dedans, son décodage commence, peu importe la taille de l'instruction. Une fois l'instruction exécutée, elle est retirée du registre d'instruction. Reste à alimenter le registre d'instruction, à charger des instructions dedans. Et pour cela deux solutions : soit on charge l'instruction en plusieurs fois, soit d'un seul coup. La solution la plus simple charge les instructions par mots de 8, 16, 32, 64 bits. Ils sont accumulés dans le registre d'instruction jusqu'à détecter une instruction complète. La technique marche nettement mieux si les instructions sont très longues. Par exemple, les CPU x86 ont des instructions qui vont de 1 à 15 octets, ce qui fait qu'ils utilisent cette technique. Précisément, elle marche si les instructions les plus longues sont plus grandes que le bus mémoire. Une autre solution charge un bloc de mots mémoire aussi grand que la plus grande instruction. Par exemple, si le processeur gère des instructions allant de 1 à 4 octets, il charge 4 octets d'un seul coup dans le registre d'instruction. Il va de soit que cette solution ne marche que si les instructions du processeur sont assez courtes, au plus aussi courtes que le bus mémoire. Ainsi, on est sûr de charger obligatoirement au moins une instruction complète, et peut-être même plusieurs. Le contenu du registre d'instruction est alors découpé en instructions au fil de l'eau. Utiliser un registre d'instruction large pose problème avec des instructions à cheval entre deux blocs. Il se peut qu'on n'ait pas une instruction complète lorsque l'on arrive à la fin du bloc, mais seulement un morceau. Le problème se manifeste peu importe que l'instruction soit chargée d'un seul coup ou bien en plusieurs fois. [[File:Instructions non alignées.png|centre|vignette|upright=2|Instructions non alignées.]] Dans ce cas, on doit décaler le morceau de bloc pour le mettre au bon endroit (au début du registre d'instruction), et charger le prochain bloc juste à côté. On a donc besoin d'un circuit qui décale tous les bits du registre d'instruction, couplé à un circuit qui décide où placer dans le registre d'instruction ce qui a été chargé, avec quelques circuits autour pour configurer les deux circuits précédents. [[File:Décaleur d’instruction.png|centre|vignette|upright=2|Décaleur d’instruction.]] Une autre solution se passe de registre d'instruction en utilisant à la place l'état interne du séquenceur. Là encore, elle charge l'instruction morceau par morceau, typiquement par blocs de 8, 16 ou 32 bits. Les morceaux ne sont pas accumulés dans le registre d'instruction, la solution utilise à la place l'état interne du séquenceur. Les mots mémoire de l'instruction sont chargés à chaque cycle, faisant passer le séquenceur d'un état interne à un autre à chaque mot mémoire. Tant qu'il n'a pas reçu tous les mots mémoire de l'instruction, chaque mot mémoire non terminal le fera passer d'un état interne à un autre, chaque état interne encodant les mots mémoire chargés auparavant. S'il tombe sur le dernier mot mémoire d'une instruction, il rentre dans un état de décodage. ===Le chargement avec les jeux d'instruction compacts=== Pour finir, il faut parler du cas particulier des jeux d'instruction compacts. Pour rappel, ces jeux d'instruction sont présents sur certains CPU RISC 32 bits. De tels processeurs gèrent un jeu d'instruction 32 bits normal, qui encode ses instructions sur 32 bits, et un jeu d'instruction plus compact qui encode ses instructions sur 16 bits. Nous allons prendre l'exemple du jeu d'instruction emblématique de cette catégorie : l'ARM ''thumb'', qui est le jeu d'instruction compact associé au jeu d'instruction ARM 32 bits. Les deux jeux d'instruction ont un mode d'exécution chacun, ce qui veut dire que le processeur est configuré pour fonctionner en mode ''thumb'' ou en mode ARM 32 bits. En clair, on ne peut pas mélanger les deux jeu d'instructions en même temps. La raison est qu'une même suite de bit correspondait soit à une instruction ARM normale, soit à deux instructions thumbs. Pour faire la différence, il faut préciser dans quel mode d'exécution le CPU est : mode ARM ou mode thumb. Le processeur interpréte les instructions correctement en tenant compte de ce mode. Intuitivement, on se dirait qu'il y a deux décodeurs séparés : un utilisé en mode ARM, un autre utilisé en mode ''thumb''. Sauf que ce n'est pas la solution retenue. A la place, les ingénieurs d'ARM profitent d'une propriété du ''thumb'' : ''thumb'' est un sous-ensemble du jeu d’instruction 32 bits. Par exemple, une instruction ''thumb'' a un équivalent en RAM 32 bits, bien que l'encodage entre les deux est différent. Et cette propriété est utilisée pour faciliter l'implémentation au niveau du décodeur. Le processeur contient simplement un circuit qui convertit les instructions ''thumb'' en leur équivalent ARM 32 bits. Le circuit de conversion se situe en aval de l'unité de chargement, juste avant le décodeur d'instruction. Le circuit est désactivé quand le processeur fonctionne en mode ARM, mais est active en mode ''thumb''. ===Les optimisations du code automodifiant=== Précédemment dans ce cours, nous avons parlé du code automodifiant, à savoir le fait qu'un programme modifie ses propres instructions à la volée. Il était utilisé pour avoir des tableaux, sur des processeurs qui ne supportaient que le mode d'adressage direct. L'idée était que les instructions d'accès mémoire incorporaient une adresse, qui était incrémentée/décrémentée par code auto-modifiant. Les branchements indirects étaient eux aussi gérés de la même manière : l'adresse de destination, incorporée dans l'instruction via adressage absolu, était changée via code automodifiant. Quelques rares processeurs ont incorporé des optimisations pour simplifier l'usage du code automodifiant. Elles agissaient toutes sur le registre d'instruction, en permettant de modifier son contenu. Les instructions étaient chargées dans le registre d'instruction et étaient modifiées dans ce registre, avant d'être décodées. La modification était généralement assez simple : appliquer un masque, additionner une constante, guère plus. Les processeurs de ce type sont très rare, j'en connais deux exemples : le Burroughs B1700 et l'Apollo Guidance Computer. Un exemple est celui de l'Apollo Guidance Computer, qui permettait d'altérer le registre d'instruction avec une instruction dédiée, l'instruction ''Index next instruction'', que nous appellerons INI. Nous avons déjà vu cette instruction INI, dans le chapitre sur les modes d'adressage, mais nous pouvons maintenant expliquer comment elle est implémentée. Elle additionne une constante à l'instruction suivante. Elle était utilisée pour ajouter un décalage à une adresse, dans une instruction LOAD/STORE, ou un branchement direct. L'addition était vraisemblablement réalisée par l'ALU du processeur. La conséquence est que le registre d'instruction était un registre architectural, qui était adressé implicitement, avec un mode d'adressage implicite. ==L'incrémentation du ''program counter''== À chaque chargement, le ''program counter'' est mis à jour afin de pointer sur la prochaine instruction à charger. Sur la quasi-totalité des processeurs, les instructions sont placées dans l'ordre d’exécution dans la RAM. En faisant ainsi, on peut mettre à jour le ''program counter'' en lui ajoutant la longueur de l'instruction courante. Déterminer la longueur de l'instruction est simple quand les instructions ont toutes la même taille, mais certains processeurs ont des instructions de taille variable, ce qui complique le calcul. Dans ce qui va suivre, nous allons supposer que les instructions sont de taille fixe, ce qui fait que le ''program counter'' est toujours incrémenté de la même valeur. Il existe deux méthodes principales pour incrémenter le ''program counter'' : soit le ''program counter'' a son propre additionneur, soit le ''program counter'' est mis à jour par l'ALU. Pour ce qui est de l'organisation des registres, soit le ''program counter'' est un registre séparé des autres, soit il est regroupé avec d'autres registres dans un banc de registre. En tout, cela donne quatre organisations possibles. * Un ''program counter'' séparé relié à un incrémenteur séparé. * Un ''program counter'' séparé des autres registres, incrémenté par l'ALU. * Un ''program counter'' intégré au banc de registre, relié à un incrémenteur séparé. * Un ''program counter'' intégré au banc de registre, incrémenté par l'ALU. [[File:Calcul du program counter.png|centre|vignette|upright=2|les différentes méthodes de calcul du program counter.]] Les processeurs haute performance modernes utilisent tous la première méthode. Les autres méthodes étaient surtout utilisées sur les processeurs 8 ou 16 bits, elles sont encore utilisées sur quelques microcontrôleurs de faible puissance. Nous auront l'occasion de donner des exemples quand nous parlerons des processeurs 8 bits anciens, dans le chapitre sur les architectures à accumulateur et affiliées. ===Le ''program counter'' mis à jour par l'ALU=== Sur certains processeurs, le calcul de l'adresse de la prochaine instruction est effectué par l'ALU. L'avantage de cette méthode est qu'elle économise un incrémenteur dédié au ''program counter''. Ce qui explique que cette méthode était utilisée sur les processeurs assez anciens, à une époque où un additionneur pouvait facilement prendre 10 à 20% des transistors disponibles. Le désavantage principal est une augmentation de la complexité du séquenceur, qui doit gérer la mise à jour du ''program counter''. De plus, la mise à jour du ''program counter'' ne peut pas se faire en même temps qu'une opération arithmétique, ce qui réduit les performances. Si on incrémente le ''program counter'' avec l'ALU, il est intéressant de le placer dans le banc de registres. Un désavantage est qu'on perd un registre. Par exemple, avec un banc de registre de 16 registres, on ne peut adresser que 15 registres généraux. De plus ce n'est pas l'idéal pour le décodage des instructions et pour le séquenceur. Si le processeur dispose de plusieurs bancs de registres, le ''program counter'' est généralement placé dans le banc de registre dédié aux adresses. Sinon, il est placé avec les nombres entiers/adresses. D'anciens processeurs incrémentaient le ''program counter'' avec l'ALU, mais utilisaient bien un registre séparé pour le ''program counter''. Cette méthode est relativement simple à implémenter : il suffit de connecter/déconnecter le ''program counter'' du bus interne suivant les besoins. Le ''program counter'' est déconnecté pendant l’exécution d'une instruction, il est connecté au bus interne pour l'incrémenter. C'est le séquenceur qui gère le tout. L'avantage de cette séparation est qu'elle est plus facile à gérer pour le séquenceur. De plus, on gagne un registre général/entier dans le banc de registre. ===Le compteur programme=== Dans la quasi-totalité des processeurs modernes, le ''program counter'' est un vulgaire compteur comme on en a vu dans les chapitres sur les circuits, ce qui fait qu'il passe automatiquement d'une adresse à la suivante. Dans ce qui suit, nous allons appeler ce registre compteur : le '''compteur ordinal'''. L'usage d'un compteur simplifie fortement la conception du séquenceur. Le séquenceur n'a pas à gérer la mise à jour du ''program counter'', sauf en cas de branchements. De plus, il est possible d'incrémenter le ''program counter'' pendant que l'unité de calcul effectue une opération arithmétique, ce qui améliore grandement les performances. Le seul désavantage est qu'elle demande d'ajouter un incrémenteur dédié au ''program counter''. Sur les processeurs très anciens, les instructions faisaient toutes un seul mot mémoire et le compteur ordinal était un circuit incrémenteur des plus basique. Sur les processeurs modernes, le compteur est incrémenté par pas de 4 si les instructions sont codées sur 4 octets, 8 si les instructions sont codées sur 8 octets, etc. Concrètement, le compteur est un circuit incrémenteur auquel on aurait retiré quelques bits de poids faible. Par exemple, si les instructions sont codées sur 4 octets, on coupe les 2 derniers bits du compteur. Si les instructions sont codées sur 8 octets, on coupe les 3 derniers bits du compteur. Et ainsi de suite. Une amélioration de la solution précédente utilise un circuit incrémenteur partagé entre le ''program counter'' et d'autres registres. L'incrémenteur est utilisé pour incrémenter le ''program counter'', mais aussi pour effectuer d'autres calculs d'adresse. Par exemple, sur les architectures avec une pile d'adresse de retour, il est possible de partager l'incrémenteur/décrémenteur avec le pointeur de pile ( la technique ne marche pas avec une pile d'appel). Un autre exemple est celui des processeurs qui gèrent automatiquement le rafraichissement mémoire, grâce à, un compteur intégré dans le processeur. Il est possible de partager l'incrémenteur du ''program counter'' avec le compteur de rafraichissement mémoire, qui mémorise la prochaine adresse mémoire à rafraichir. Nous avions déjà abordé ce genre de partage dans le chapitre sur le chemin de données, dans l'annexe sur le pointeur de pile, mais nous verrons d'autres exemples dans le chapitre sur les architectures hybrides accumulateur-registre 8/16 bits. Il a existé quelques processeurs pour lesquelles le ''program counter'' était non pas un compteur binaire classique, mais un ''linear feedback shift register''. L'avantage est que le circuit d'incrémentation utilisait bien moins de portes logiques, l'économie était substantielle. Mais un défaut est que des instructions consécutives dans le programme n'étaient pas consécutives en mémoire. Il existait cependant une table de correspondance pour dire : la première instruction est à telle adresse, la seconde à telle autre, etc. Un exemple de processeur de ce type est le TMS 1000 de Texas Instrument, un des premiers microcontrôleur 4 bits datant des années 70. ===Quand est mis à jour le ''program counter'' ?=== Le ''program counter'' est mis à jour quand il faut charger une nouvelle instruction. Reste qu'il faut savoir quand le mettre à jour. Incrémenter le ''program counter'' à intervalle régulier, par exemple à chaque cycle d’horloge, fonctionne sur les processeurs où toutes les instructions mettent le même temps pour s’exécuter. Mais de tels processeurs sont très rares. Sur la quasi-totalité des processeurs, les instructions ont une durée d’exécution variable, elles ne prennent pas le même nombre de cycle d'horloge pour s’exécuter. Le temps d’exécution d'une instruction varie selon l'instruction, certaines sont plus longues que d'autres. Tout cela amène un problème : comment incrémenter le ''program counter'' avec des instructions avec des temps d’exécution variables ? La réponse est que la mise à jour du ''program counter'' démarre quand l'instruction précédente a terminée de s’exécuter, plus précisément un cycle avant. C'est un cycle avant pour que l'instruction chargée soit disponible au prochain cycle pour le décodeur. Pour cela, le séquenceur doit déterminer quand l'instruction est terminée et prévenir le ''program counter'' au bon moment. Il faut donc une interaction entre séquenceur et circuit de chargement. Le circuit de chargement contient une entrée Enable, qui autorise la mise à jour du ''program counter''. Le séquenceur met à 1 cette entrée pour prévenir au ''program counter'' qu'il doit être incrémenté au cycle suivant ou lors du cycle courant. Cela permet de gérer simplement le cas des instructions multicycles. [[File:Commande de la mise à jour du program counter.png|centre|vignette|upright=2|Commande de la mise à jour du program counter.]] Toute la difficulté est alors reportée dans le séquenceur, qui doit déterminer la durée d'une instruction. Pour gérer la mise à jour du ''program counter'', le séquenceur doit déterminer la durée d'une instruction. Cela est simple pour les instructions arithmétiques et logiques, qui ont un nombre de cycles fixe, toujours le même. Une fois l'instruction identifiée par le séquenceur, il connait son nombre de cycles et peut programmer un compteur interne au séquenceur, qui compte le nombre de cycles de l'instruction, et fournit le signal Enable au ''program counter''. Mais cette technique ne marche pas vraiment pour les accès mémoire, dont la durée n'est pas connue à l'avance, surtout sur les processeurs avec des mémoires caches. Pour cela, le séquenceur détermine quand l'instruction mémoire est finie et prévient le ''program counter'' quand c'est le cas. Il existe quelques processeurs pour lesquels le temps de calcul dépend des opérandes de l'instruction. On peut par exemple avoir une division qui prend 10 cycles avec certaines opérandes, mais 40 cycles avec d'autres opérandes. Mais ces processeurs sont rares et cela est surtout valable pour les opérations de multiplication/division, guère plus. Le problème est alors le même qu'avec les accès mémoire et la solution est la même : l'ALU prévient le séquenceur quand le résultat est disponible. ==Les branchements et le ''program counter''== Un branchement consiste juste à écrire l'adresse de destination dans le ''program counter''. L'implémentation des branchements demande d'extraire l'adresse de destination et d'altérer le ''program counter'' quand un branchement est détecté. Pour cela, le décodeur d'instruction détecte si l'instruction exécutée est un branchement ou non, et il en déduit où trouver l'adresse de destination. L'adresse de destination se trouve à un endroit qui dépend du mode d'adressage du branchement. Pour rappel, on distingue quatre modes d'adressage pour les branchements : direct, indirect, relatif et implicite. Pour les branchements directs, l'adresse est intégrée dans l'instruction de branchement elle-même et est extraite par le séquenceur. Pour les branchements indirects, l'adresse de destination est dans le banc de registre. Pour les branchements relatifs, il faut ajouter un décalage au ''program counter'', décalage extrait de l'instruction par le séquenceur. Enfin, les instructions de retour de fonction lisent l'adresse de destination depuis la pile. L'implémentation de ces quatre types de branchements est très différente. L'altération du ''program counter'' dépend de si le ''program counter'' est dans un compteur séparé ou s'il est dans le banc de registre. Nous avons vu plus haut qu'il y a quatre cas différents, mais nous n'allons voir que deux cas : celui où le ''program counter'' est dans le banc de registre et est incrémenté par l'unité de calcul, celui avec un ''program counter'' séparé avec son propre incrémenteur. Les deux autres cas ne sont utilisés que sur des architectures à accumulateur ou à pile spécifiques, que nous n'avons pas encore vu à ce stade du cours et que nous verrons en temps voulu dans le chapitre sur ces architectures. ===L’implémentation des branchements avec un ''program counter'' intégré au banc de registres=== Le cas le plus simple est celui où le ''program counter'' est intégré au banc de registre, avec une incrémentation faite par l'unité de calcul. Charger une instruction revient alors à effectuer une instruction LOAD en adressage indirect, à deux différences près : le registre sélectionné est le ''program counter'', l’instruction est copiée dans le registre d'instruction et non dans le banc de registre. L'incrémentation du ''porgram counter'' est implémenté avec des micro-opérations qui agissent sur le chemin de données, au même titre que les branchements indirects, relatifs et directs. * Les branchements indirects copient un registre dans le ''program counter'', ce qui revient simplement à faire une opération MOV entre deux registres, dont le ''program counter'' est la destination. * L'opération inverse d'un branchement indirect, qui copie le ''program counter'' dans un registre général, est utile pour sauvegarder l'adresse de retour d'une fonction. * Les branchements directs copient une adresse dans le ''program counter'', ce qui les rend équivalents à une opération MOV avec une constante immédiate, dont le ''program counter'' est la destination. * Les branchements relatifs sont une opération arithmétique entre le ''program counter'' et un opérande en adressage immédiat. En clair, à partir du moment où le chemin de données supporte les instructions MOV et l'addition, avec les modes d'adressage adéquat, il supporte les branchements. Aucune modification du chemin de données n'est nécessaire, le séquenceur gére le chargement d'une instruction avec les micro-instructions adéquates : une micro-opération ADD pour incrémenter le ''program counter'', une micro-opération LOAD pour le chargement de l'instruction. Notons que sur certaines architectures, le ''program counter'' est adressable au même titre que les autres registres du banc de registre. Les instructions de branchement sont alors remplacées par des instructions MOV ou des instructions arithmétique équivalentes, comme décrit plus haut. ===L’implémentation des branchements avec un ''program counter'' séparé=== Étudions maintenant le cas où le ''program counter'' est dans un registre/compteur séparé. Rappelons que tout compteur digne de ce nom possède une entrée de réinitialisation, qui remet le compteur à zéro. De plus, on peut préciser à quelle valeur il doit être réinitialisé. Ici, la valeur à laquelle on veut réinitialiser le compteur n'est autre que l'adresse de destination du branchement. Implémenter un branchement est donc simple : l'entrée de réinitialisation est commandée par le circuit de détection des branchements, alors que la valeur de réinitialisation est envoyée sur l'entrée adéquate. En clair, nous partons du principe que le ''program counter'' est implémenté avec ce type de compteur : [[File:Fonctionnement d'un compteur (décompteur), schématique.jpg|centre|vignette|upright=1.5|Fonctionnement d'un compteur (décompteur), schématique]] Toute la difficulté est de présenter l'adresse de destination au ''program counter''. Pour les branchements directs, l'adresse de destination est fournie par le séquenceur. L'adresse de destination du branchement sort du séquenceur et est présentée au ''program counter'' sur l'entrée adéquate. Voici donc comment sont implémentés les branchements directs avec un ''program counter'' séparé du banc de registres. Le schéma ci-dessous marche peu importe que le ''program counter'' soit incrémenté par l'ALU ou par un additionneur dédié. [[File:Unité de détection des branchements dans le décodeur.png|centre|vignette|upright=2|Unité de détection des branchements dans le décodeur]] Les branchements relatifs sont ceux qui font sauter X instructions plus loin dans le programme. Leur implémentation demande d'ajouter une constante au ''program counter'', la constante étant fournie dans l’instruction. Là encore, deux solutions sont possibles : réutiliser l'ALU pour calculer l'adresse, ou utiliser un additionneur séparé. L'additionneur séparé peut être fusionné avec l'additionneur qui incrémente le ''program counter'' pour passer à l’instruction suivante. [[File:Unité de chargement qui gère les branchements relatifs.png|centre|vignette|upright=2|Unité de chargement qui gère les branchements relatifs.]] Pour les branchements indirects, il suffit de lire le registre voulu et d'envoyer le tout sur l'entrée adéquate du ''program counter''. Il faut alors rajouter un multiplexeur pour que l'entrée de réinitialisationr ecoive la bonne adresse de destination. [[File:Unité de chargement qui gère les branchements directs.png|centre|vignette|upright=2|Unité de chargement qui gère les branchements directs et indirects.]] Toute la difficulté de l'implémentation des branchements est de configurer les multiplexeurs, ce qui est réalisé par le séquenceur en fonction du mode d'adressage du branchement. ==Les optimisations du chargement des instructions== Charger une instruction est techniquement une forme d'accès mémoire un peu particulier. En clair, charger une instruction prend au minimum un cycle d'horloge, et cela peut rapidement monter à 3-5 cycles, si ce n'est plus. L'exécution des instructions est alors fortement ralentit par la mémoire. Par exemple, imaginez que la mémoire mette 3 cycles d'horloges pour charger une instruction, alors qu'une instruction s’exécute en 1 cycle (si les opérandes sont dans les registres). La perte de performance liée au chargement des instructions est alors substantielle. Heureusement, il est possible de limiter la casse en utilisant des mémoires caches, ainsi que d'autres optimisations, que nous allons voir dans ce qui suit. ===Le tampon de préchargement=== La technique dite du '''préchargement''' est utilisée dans le cas où la mémoire a un temps d'accès important. Mais si la latence de la RAM est un problème, le débit ne l'est pas. Il est possible d'avoir une RAM lente, mais à fort débit. Par exemple, supposons que la mémoire puisse charger 4 instructions (de taille fixe) en 3 cycles. Le processeur peut alors charger 4 instructions en un seul accès mémoire, et les exécuter l'une après l'autre, une par cycle d'horloge. Les temps d'attente sont éliminés : le processeur peut décoder une nouvelle instruction à chaque cycle. Et quand la dernière instruction préchargée est exécutée, la mémoire est de nouveau libre, ce qui masque la latence des accès mémoire. [[File:Tampon de préchargement d'instruction.png|vignette|Tampon de préchargement d'instruction]] La seule contrainte est de mettre les instructions préchargées en attente. La solution pour cela est d'utiliser un registre d'instruction très large, capable de mémoriser plusieurs instructions à la fois. Ce registre est de plus connecté à un multiplexeur qui permet de sélectionner l'instruction adéquate dans ce registre. Ce multiplexeur est commandé par le ''program counter'' et quelques circuits annexes. Ce super-registre d'instruction est appelé un '''tampon de préchargement'''. La méthode a une implémentation nettement plus simple avec des instructions de taille fixe, alignées en mémoire. La commande du multiplexeur de sélection de l'instruction est alors beaucoup plus simple : il suffit d'utiliser les bits de poids fort du ''program counter''. Par exemple, prenons le cas d'un registre d'instruction de 32 octets pour des instructions de 4 octets, soit 8 instructions. Le choix de l'instruction à sélectionner se fait en utilisant les 3 bits de poids faible du ''program counter''. Mais cette méthode ajoute de nouvelles contraintes d'alignement, similaires à celles vues dans le chapitre sur l'alignement et le boutisme, sauf que l'alignement porte ici sur des blocs d'instructions de même taille que le tampon de préchargement. Si on prend l'exemple d'un tampon de préchargement de 128 bits, les instructions devront être alignées par blocs de 128 bits. C'est à dire qu'idéalement, les fonctions et autres blocs de code isolés doivent commencer à des adresses multiples de 128, pour pouvoir charger un bloc d'instruction en une seule fois. Sans cela, les performances seront sous-optimales. : Il arrive que le tampon de préchargement ait la même taille qu'une ligne de cache. Lorsque l'on passe d'un bloc d'instruction à un autre, le tampon de préchargement est mis à jour. Par exemple, si on prend un tampon de préchargement de 4 instructions, on doit changer son contenu toutes les 4 instructions. La seule exception est l'exécution d'un branchement. En effet, lors d'un branchement, la destination du branchement n'est pas dans le tampon de préchargement et elle doit être chargée dedans (sauf si le branchement pointe vers une instruction très proche, ce qui est improbable). Pour cela, le tampon de préchargement est mis à jour précocement quand le processeur détecte un branchement. Le même problème survient avec du code auto-modifiant. Un code auto-modifiant est un programme qui se modifie lui-même, en remplaçant certaines instructions par d'autres, en en retirant, en en ajoutant, lors de sa propre exécution. De tels programmes sont rares, mais la technique était utilisée dans quelques cas au tout début de l'informatique sur des ordinateurs rudimentaires. Ceux-ci avaient des modes d'adressages tellement limités que gérer des tableaux de taille variable demandait d'utiliser du code auto-modifiant pour écrire des boucles. Le problème survient quand des instructions sont modifiées immédiatement après avoir été préchargées. Les instructions dans le tampon de préchargement sont l'ancienne version, alors que la mémoire RAM contient les instructions modifiées. Gérer ce genre de cas demande de vider le tampon de préchargement si le cas arrive, mais ça ne vaut que très rarement le coup, aussi les ingénieurs ne s’embêtent pas à mettre ce correctif en place. Le code automodifiant est alors buggé. ===La ''prefetch input queue''=== La ''prefetch input queue'' est une sorte de cousine du tampon de préchargement, qui a été utilisée sur les processeurs Intel 8 et 16 bits, ainsi que sur le 286 et le 386. L'idée est là encore de précharger des instructions en avance. Sauf que cette fois-ci, on ne charge pas plusieurs instructions à la fois. Au contraire, les instructions sont préchargées séquentiellement, un ou deux octet à la fois. En conséquence, le tampon de préchargement est remplacé par une mémoire FIFO appelée la '''''Prefetch Input Queue''''', terme abrévié en PIQ. Les instructions sont accumulées une par une dans la PIQ, elles en sortent une par une au fur et à mesure pour alimenter le décodeur d'instruction. Pendant que le processeur exécute une instruction, on précharge la suivante. Sans ''prefetch input queue'', le processeur chargeait une instruction, la décodait et l'exécutait, puis chargeait la suivante, et ainsi de suite. Avec un tampon de préchargement, le processeur chargeait plusieurs instruction en une seule fois, puis les exécutait les unes après les autres. Avec la ''prefetch input queue'', pendant qu'une instruction est en cours de décodage/exécution, le processeur précharge l'instruction suivante. Si une instruction prend vraiment beaucoup de temps pour s'exécuter, le processeur peut en profiter pour précharger l'instruction encore suivante, et ainsi de suite, jusqu'à une limite de 4/6 instructions (la limite dépend du processeur). La ''prefetch input queue'' permet de précharger l'instruction suivante à condition qu'aucun autre accès mémoire n'ait lieu. Si l'instruction en cours d'exécution effectue des accès mémoire, ceux-ci sont prioritaires sur tout le reste, le préchargement de l'instruction suivante est alors mis en pause ou inhibé. Par contre, si la mémoire n'est pas utilisée par l'instruction en cours d'exécution, le processeur lit l'instruction suivante en RAM et la copie dans la ''prefetch input queue''. En conséquence, il arrive que la ''prefetch input queue'' n'ait pas préchargé l'instruction suivante, alors que le décodeur d'instruction est prêt pour la décoder. Dans ce cas, le décodeur doit attendre que l'instruction soit chargée. Les instructions préchargées dans la ''Prefetch input queue'' y attendent que le processeur les lise et les décode. Les instructions préchargées sont conservées dans leur ordre d'arrivée, afin qu'elles soient exécutées dans le bon ordre, ce qui fait que la ''Prefetch input queue'' est une mémoire de type FIFO. L'étape de décodage pioche l'instruction à décoder dans la ''Prefetch input queue'', cette instruction étant par définition la plus ancienne, puis la retire de la file. Le premier processeur commercial grand public à utiliser une ''prefetch input queue'' était le 8086. Il pouvait précharger à l'avance 6 octets d’instruction. L'Intel 8088 avait lui aussi une ''Prefetch input queue'', mais qui ne permettait que de précharger 4 instructions. Le 386 avait une ''prefetch input queue'' de 16 octets. La taille idéale de la FIFO dépend de nombreux paramètres. On pourrait croire que plus elle peut contenir d'instructions, mieux c'est, mais ce n'est pas le cas, pour des raisons que nous allons expliquer dans quelques paragraphes. [[File:80186 arch.png|centre|vignette|700px|upright=2|Architecture du 8086, du 80186 et de ses variantes.]] Vous remarquerez que j'ai exprimé la capacité de la ''prefetch input queue'' en octets et non en instructions. La raison est que sur les processeurs x86, les instructions sont de taille variable, avec une taille qui varie entre un octet et 6 octets. Cependant, le décodage des instructions se fait un octet à la fois : le décodeur lit un octet, puis éventuellement le suivant, et ainsi de suite jusqu'à atteindre la fin de l'instruction. En clair, le décodeur lit les instructions longues octet par octet dans la ''Prefetch input queue''. Les instructions varient entre 1 et 6 octets, mais tous ne sont pas utiles au décodeur d'instruction. Par exemple, le décodeur d'instruction n'a pas besoin d'analyser les constantes immédiates intégrées dans une instruction, ni les adresses mémoires en adressage absolu. Il n'a besoin que de deux octets : l'opcode et l'octet Mod/RM qui précise le mode d'adressage. Le second est facultatif. En clair, le décodeur a besoin de lire deux octets maximum depuis la ''prefetch input queue'', avant de passer à l’instruction suivante. Les autres octets étaient envoyés ailleurs, typiquement dans le chemin de données. Par exemple, prenons le cas d'une addition entre le registre AX et une constante immédiate. L'instruction fait trois octets : un opcode suivie par une constante immédiate codée sur deux octets. L'opcode était envoyé au décodeur, mais pas la constante immédiate. Elle était lue octet par octet et mémorisée dans un registre temporaire placé en entrée de l'ALU. Idem avec les adresses immédiates, qui étaient envoyées dans un registre d’interfaçage mémoire sans passer par le décodeur d'instruction. Pour cela, la ''prefetch input queue'' était connectée au bus interne du processeur ! Le décodeur dispose d'une micro-opération pour lire un octet depuis la ''prefetch input queue'' et le copier ailleurs dans le chemin de données. Par exemple, l'instruction d'addition entre le registre AX et une constante immédiate était composée de quatre micro-opérations : une qui lisait le premier octet de la constante immédiate, une seconde pour le second, une troisième micro-opération qui commande l'ALU et fait le calcul. Le décodeur devait attendre que qu'un moins un octet soit disponible dans la ''Prefetch input queue'', pour le lire. Il lisait alors cet octet et déterminait s'il contenait une instruction complète ou non. Si c'est une instruction complète, il la décodait et l''exécutait, puis passait à l'instruction suivante. Sinon, il lit un second octet depuis la ''Prefetch input queue'' et relance le décodage. Là encore, le décodeur vérifie s'il a une instruction complète, et lit un troisième octet si besoin, puis rebelote avec un quatrième octet lu, etc. Un circuit appelé le ''loader'' synchronisait le décodeur d'instruction et la ''Prefetch input queue''. Il fournissait deux bits : un premier bit pour indiquer que le premier octet d'une instruction était disponible dans la ''Prefetch input queue'', un second bit pour le second octet. Le ''loader'' recevait aussi des signaux de la part du décodeur d'instruction. Le signal ''Run Next Instruction'' entrainait la lecture d'une nouvelle instruction, d'un premier octet, qui était alors dépilé de la ''Prefetch input queue''. Le décodeur d'instruction pouvait aussi envoyer un signal ''Next-to-last (NXT)'', un cycle avant que l'instruction en cours ne termine. Le ''loader'' réagissait en préchargeant l'octet suivant. En clair, ce signal permettait de précharger l'instruction suivante avec un cycle d'avance, si celle-ci n'était pas déjà dans la ''Prefetch input queue''. Le bus de données du 8086 faisait 16 bits, alors que celui du 8088 ne permettait que de lire un octet à la fois. Et cela avait un impact sur la ''prefetch input queue''. La ''prefetch input queue'' du 8088 était composée de 4 registres de 8 bits, avec un port d'écriture de 8 bits pour précharger les instructions octet par octet, et un port de lecture de 8 bits pour alimenter le décodeur. En clair, tout faisait 8 bits. Le 8086, lui utilisait des registres de 16 bits et un port d'écriture de 16 bits. le port de lecture restait de 8 bits, grâce à un multiplexeur sélectionnait l'octet adéquat dans un registre 16 bits. Sa ''prefetch input queue'' préchargeait les instructions par paquets de 2 octets, non octet par octet, alors que le décodeur consommait les instructions octet par octet. Il s’agissait donc d'une sorte d'intermédiaire entre ''prefetch input queue'' à la 8088 et tampon de préchargement. Le 386 était dans un cas à part. C'était un processeur 32 bits, sa ''prefetch input queue'' contenait 4 registres de 32 bits, un port d'écriture de 32 bits. Mais il ne lisait pas les instructions octet par cotet. A la place, son décodeur d'instruction avait une entrée de 32 bits. Cependant, il gérait des instructions de 8 et 16 bits. Il fallait alors dépiler des instructions de 8, 16 et 32 bits dans la ''prefetch input queue''. De plus, les instructions préchargées n'étaient pas parfaitement alignées sur 32 bits : une instruction pouvait être à cheval sur deux registres 32 bits. Le processeur incorporait donc des circuits d'alignement, semblables à ceux utilisés pour gérer des instructions de longueur variable avec un registre d'instruction. Les branchements posent des problèmes avec la ''prefetch input queue'' : à cause d'eux, on peut charger à l'avance des instructions qui sont zappées par un branchement et ne sont pas censées être exécutées. Si un branchement est chargé, toutes les instructions préchargées après sont potentiellement invalides. Si le branchement est non-pris, les instructions chargées sont valides, elles sont censées s’exécuter. Mais si le branchement est pris, elles ont été chargées à tort et ne doivent pas s’exécuter. Pour éviter cela, la ''prefetch input queue'' est vidée quand le processeur détecte un branchement. Pour cela, le décodeur d'instruction dispose d'une micro-opération qui vide la ''Prefetch input queue'', elle invalide les instructions préchargées. Cette détection ne peut se faire qu'une fois le branchement décodé, qu'une fois que l'on sait que l'instruction décodée est un branchement. En clair, le décodeur invalide la ''Prefetch input queue'' quand il détecte un branchement. Les interruptions posent le même genre de problèmes. Il faut impérativement vider la ''Prefetch input queue'' quand une interruption survient, avant de la traiter. Il faut noter qu'une ''Prefetch input queue'' interagit assez mal avec le ''program counter''. En effet, la ''Prefetch input queue'' précharge les instructions séquentiellement. Pour savoir où elle en est rendue, elle mémorise l'adresse de la prochaine instruction à charger dans un '''registre de préchargement''' dédié. Il serait possible d'utiliser un ''program counter'' en plus du registre de préchargement, mais ce n'est pas la solution qui a été utilisée. Les processeurs Intel anciens ont préféré n'utiliser qu'un seul registre de préchargement en remplacement du ''program counter''. Après tout, le registre de préchargement n'est qu'un ''program counter'' ayant pris une avance de quelques cycles d'horloge, qui a été incrémenté en avance. Et cela ne pose pas de problèmes, sauf avec certains branchements. Par exemple, certains branchements relatifs demandent de connaitre la véritable valeur du ''program counter'', pas celle calculée en avance. Idem avec les instructions d'appel de fonction, qui demandent de sauvegarder l'adresse de retour exacte, donc le vrai ''program counter''. De telles situations demandent de connaitre la valeur réelle du ''program counter'', celle sans préchargement. Pour cela, le décodeur d'instruction dispose d'une instruction pour reconstituer le ''program counter'', à savoir corriger le ''program coutner'' et éliminer l'effet du préchargement. La micro-opération de correction se contente de soustraire le nombre d'octets préchargés au registre de préchargement. Le nombre d'octets préchargés est déduit à partir des deux pointeurs intégré à la FIFO, qui indiquent la position du premier et du dernier octet préchargé. Le bit qui indique si la FIFO est vide était aussi utilisé. Les deux pointeurs sont lus depuis la FIFO, et sont envoyés à un circuit qui détermine le nombre d'octets préchargés. Sur le 8086, ce circuit était implémenté non pas par un circuit combinatoire, mais par une mémoire ROM équivalente. La petite taille de la FIFO faisait que les pointeurs étaient très petits et la ROM l'était aussi. Un autre défaut est que la ''Prefetch input queue'' se marie assez mal avec du code auto-modifiant. Un code auto-modifiant est un programme qui se modifie lui-même, en remplaçant certaines instructions par d'autres, en en retirant, en en ajoutant, lors de sa propre exécution. De tels programmes sont rares, mais la technique était utilisée dans quelques cas au tout début de l'informatique sur des ordinateurs rudimentaires. Ceux-ci avaient des modes d'adressages tellement limités que gérer des tableaux de taille variable demandait d'utiliser du code auto-modifiant pour écrire des boucles. Le problème avec la ''Prefetch input queue'' survient quand des instructions sont modifiées immédiatement après avoir été préchargées. Les instructions dans la ''Prefetch input queue'' sont l'ancienne version, alors que la mémoire RAM contient les instructions modifiées. Gérer ce genre de cas est quelque peu complexe. Il faut en effet vider la ''Prefetch input queue'' si le cas arrive, ce qui demande d'identifier les écritures qui écrasent des instructions préchargées. C'est parfaitement possible, mais demande de transformer la ''Prefetch input queue'' en une mémoire hybride, à la fois mémoire associative et mémoire FIFO. Cela ne vaut que très rarement le coup, aussi les ingénieurs ne s’embêtent pas à mettre ce correctif en place, le code automodifiant est alors buggé. Le décodeur d'instruction dispose aussi d'une micro-opération qui stoppe le préchargement des instructions dans la ''Prefetch input queue''. : Pour ceux qui ont déjà lu un cours d'architecture des ordinateurs, la relation entre ''prefetch input queue'' et pipeline est quelque peu complexe. En apparence, la ''prefetch input queue'' permet une certaines forme de pipeline, en chargeant une instruction pendant que la précédente s'exécute. Les problématiques liées aux branchements ressemblent beaucoup à celles de la prédiction de branchement. Cependant, il est possible de dire qu'il s'agit plus d'une technique de préchargement que de pipeline. La différence entre les deux est assez subtile et les frontières sont assez floues, mais le fait est que l'instruction en cours d'exécution et le préchargement peuvent tout deux faire des accès mémoire et que l'instruction en cours a la priorité. Un vrai pipeline se débrouillerait avec une architecture Harvard, non avec un bus mémoire unique pour le chargement des instruction et la lecture/écriture des données. ===Le ''Zero-overhead looping''=== Nous avions vu dans le chapitre sur les instructions machines, qu'il existe des instructions qui permettent de grandement faciliter l'implémentation des boucles. Il s'agit de l'instruction REPEAT, utilisée sur certains processeurs de traitement de signal. Elle répète l'instruction suivante, voire une série d'instructions, un certain nombre de fois. Le nombre de répétitions est mémorisé dans un registre décrémenté à chaque itération de la boucle. Elle permet donc d'implémenter une boucle FOR facilement, sans recourir à des branchements ou des conditions. Une technique en lien avec cette instruction permet de grandement améliorer les performances et la consommation d'énergie. L'idée est de mémoriser la boucle, les instructions à répéter, dans une petite mémoire cache située entre l'unité de chargement et le séquenceur. Le cache en question s'appelle un '''tampon de boucle''', ou encore un ''hardware loop buffer''. Grâce à lui, il n'y a pas besoin de charger les instructions à chaque fois qu'on les exécute, on les charge une bonne fois pour toute : c'est plus rapide. Bien sûr, il ne fonctionne que pour de petites boucles, mais il en est de même avec l'instruction REPEAT. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le chemin de données | prevText=Le chemin de données | next=L'unité de contrôle | nextText=L'unité de contrôle }} </noinclude> sfmkyz7dj0d6bi6fliidal0m1rdn7tk 764020 764017 2026-04-19T17:03:48Z Mewtow 31375 /* Les optimisations du code automodifiant */ 764020 wikitext text/x-wiki L'unité de contrôle s'occupe du chargement des instructions et de leur interprétation, leur décodage. Elle contient deux circuits : l''''unité de chargement''' qui charge l'instruction depuis la mémoire, et le '''séquenceur''' qui commande le chemin de données. Les deux circuits sont séparés, mais ils communiquent entre eux, notamment pour gérer les branchements. Dans ce chapitre, nous allons nous intéresser au chargement d'une instruction depuis la RAM/ROM, nous verrons l'étape de décodage de l'instruction au prochain chapitre. L'unité de chargement a pour cœur le ''program counter'', le registre qui mémorise l'adresse de l'instruction à charger. Il s'agit précisément d'un compteur, vu que le contenu du registre est incrémenté à chaque fois qu'une instruction est chargée. Le ''program counter'' est remis à zéro lors du démarrage du processeur, mais est aussi modifié par les instructions de branchement qui écrivent une adresse dedans. Son interface est la suivante : une entrée d'horloge, une entrée de ''Reset'', une entrée ''Enable'' qui indique quand l'incrémenter (renommée ''Instruction Fetch''), et deux entrées pour les branchements. Pour les branchements, il y a une entrée pour l'adresse de destination, une autre pour autoriser l'écriture de celle-ci dans le ''program counter''. La sortie du ''program counter'' est reliée au bus d'adresse mémoire, plus ou moins directement. [[File:Program counter.jpg|centre|vignette|upright=2|Program counter]] ==Les interconnexions entre séquenceur et bus mémoire== Pour lire une instruction, le processeur envoie le ''program counter'' sur le bus d'adresse et récupère l'instruction sur le bus de données. L'instruction lue est alors envoyée au séquenceur. Et cela demande de connecter le séquenceur au bus mémoire, à la fois sur le bus d'adresse et le bus de données. Et sur ce point, la situation est différente selon que l'on parler d'une architecture Harvard ou Von Neumann. ===Les architectures Harvard : des bus mémoire séparés pour la RAM et la ROM=== Sur les architectures Harvard, le séquenceur et le chemin de donnée utilisent des interfaces mémoire séparées. Le séquenceur est directement relié au bus mémoire de la ROM, alors que le chemin de données est connecté à la RAM. [[File:Microarchitecture de l'interface mémoire d'une architecture Harvard.png|centre|vignette|upright=2|Microarchitecture de l'interface mémoire d'une architecture Harvard]] Le ''program counter'' est envoyé sur le bus d'adresse de la ROM, l'instruction est récupérée sur le bus de données de la ROM. Pour la mémoire RAM, elle échange des données avec le chemin de données, notamment avec les registres généraux. Les adresses utilisées pour la RAM viennent elles soit du chemin de données, soit de l'unité de contrôle, tout dépend du mode d'adressage. Mais le ''program counter'' n'est pas impliqué. [[File:Architecture Harvard - échanges de données.png|centre|vignette|upright=2|Architecture Harvard - échanges de données]] Les architectures Harvard modifiées doivent cependant rajouter une connexion entre le bus ROM et les registres généraux. C'est nécessaire pour charger une donnée constante depuis la mémoire ROM. Rappelons que la donnée constante est copiée dans un registre général, donc dans le chemin de données. [[File:Architecture Harvard modifiée - implémentation du processeur.png|centre|vignette|upright=2|Architecture Harvard modifiée - implémentation du processeur]] ===Les architectures Von-Neumann : un bus mémoire unique=== Sur les architectures Von-Neumann et affiliées, le séquenceur et le chemin de donnée partagent la même interface mémoire. Et cela pose deux problèmes. Le premier problème est que le bus mémoire doit être libéré une fois l'instruction chargée, pour un éventuel accès mémoire. Et le séquenceur doit conserver une copie de l'instruction chargée, sans quoi il ne peut pas décoder l'instruction correctement. Par exemple, si l'instruction met plusieurs cycles à s'exécuter, le séquenceur doit conserver une copie de l'instruction durant ces plusieurs cycles. Pour ça, le processeur intègre un '''registre d'instruction''' situé juste avant le séquenceur, qui mémorise l'instruction chargée. [[File:Registre d'instruction.png|centre|vignette|upright=2|Registre d'instruction.]] Le second problème est de gérer le flot des instructions/données entre le bus mémoire, le chemin de données et le séquenceur. Si le processeur lit une instruction, le bus doit être relié à l'unité de contrôle. Par contre, s'il accède à une donnée, il doit être relié au chemin de données. Le processeur doit connecter l'interface mémoire soit au séquenceur, soit au chemin de données et cela complique le réseau d'interconnexion interne au processeur. [[File:Architecture Von Neumann - implémentation du processeur.png|centre|vignette|upright=2|Architecture Von Neumann - implémentation du processeur]] Une première solution utilise un bus unique qui relie l'interface mémoire, le séquenceur et le chemin de données. Pour charger une instruction, le séquenceur copie le ''program counter'' sur le bus d'adresse, attend que l'instruction lue soit disponible sur le bus de données, puis la copie dans le registre d'instruction. Le bus mémoire est alors libre et peut être utilisé pour lire ou écrire des données, si le besoin s'en fait sentir. : Il faut noter que cette solution implique d'utiliser des registres d’interfaçage avec la mémoire. [[File:Microarchitecture de l'interface mémoire d'une architecture von neumann.png|centre|vignette|upright=2|Microarchitecture de l'interface mémoire d'une architecture von neumann]] Une autre solution utilise deux bus interne séparés : un connecté au bus d'adresse, l'autre au bus de données. Le ''program counter'' est alors connecté au bus interne d'adresse, le séquenceur est relié au bus interne de données. Notons que la technique marche bien si le ''program counter'' est dans le banc de registre : les interconnexions utilisées pour gérer l'adressage indirect permettent d'envoyer le ''program counter'' sur le bus d'adresse sans ajout de circuit. Le tout peut être amélioré en remplaçant les deux bus par des multiplexeurs et démultiplexeurs. Le bus d'adresse est précédé par un multiplexeur, qui permet de faire le choix entre ''Program Counter'', adresse venant du chemin de données, ou adresse provenant du séquenceur (adressage absolu). De même, le bus de données est suivi par un démultiplexeur qui envoie la donnée/instruction lue soit au registre d'instruction, soit au chemin de données. Le tout se marie très bien avec les chemins de donnée vu dans le chapitre précédent. Au passage, il faut noter que cette solution nécessite un banc de registre multi-port. [[File:Connexion du program counter sur les bus avec PC isolé.png|centre|vignette|upright=2|Connexion du program counter sur les bus avec PC isolé]] ==Le chargement d'une instruction== [[File:Étape de chargement.png|vignette|upright=1|Étape de chargement.]] L'étape de chargement (ou ''fetch'') doit faire deux choses : mettre à jour le ''program counter'' et lire l'instruction en mémoire RAM ou en ROM. Les deux étapes peuvent être faites en parallèle, dans des circuits séparés. Pendant que l'instruction est lue depuis la mémoire RAM/ROM, le ''program counter'' est incrémenté et/ou altéré par un branchement. Pour lire l'instruction, on envoie le ''program counter'' sur le bus d'adresse et on récupère l'instruction sur le bus de données pour l'envoyer sur l'entrée du séquenceur. ===Le chargement des instructions de longueur variable=== Le chargement des instructions de longueur variable pose de nombreux problèmes. Le premier est que mettre à jour le ''program counter'' demande de connaitre la longueur de l'instruction chargée, pour l'ajouter au ''program counter''. Si la longueur d'une instruction est variable, on doit connaitre cette longueur d'une manière où d'une autre. La solution la plus simple indique la longueur de l'instruction dans une partie de l'opcode ou de la représentation en binaire de l'instruction. Mais les processeurs haute performance de nos PC utilisent d'autres solutions, qui n'encodent pas explicitement la taille de l'instruction. Les processeurs récents utilisent un registre d'instruction de grande taille, capable de mémoriser l'instruction la plus longue du processeur. Par exemple, si un processeur supporte des instructions allant de 1 à 8 octets, comme les CPU x86, le registre d'instruction fera 8 octets. Le décodeur d'instruction vérifie à chaque cycle le contenu du registre d'instruction. Si une instruction est détectée dedans, son décodage commence, peu importe la taille de l'instruction. Une fois l'instruction exécutée, elle est retirée du registre d'instruction. Reste à alimenter le registre d'instruction, à charger des instructions dedans. Et pour cela deux solutions : soit on charge l'instruction en plusieurs fois, soit d'un seul coup. La solution la plus simple charge les instructions par mots de 8, 16, 32, 64 bits. Ils sont accumulés dans le registre d'instruction jusqu'à détecter une instruction complète. La technique marche nettement mieux si les instructions sont très longues. Par exemple, les CPU x86 ont des instructions qui vont de 1 à 15 octets, ce qui fait qu'ils utilisent cette technique. Précisément, elle marche si les instructions les plus longues sont plus grandes que le bus mémoire. Une autre solution charge un bloc de mots mémoire aussi grand que la plus grande instruction. Par exemple, si le processeur gère des instructions allant de 1 à 4 octets, il charge 4 octets d'un seul coup dans le registre d'instruction. Il va de soit que cette solution ne marche que si les instructions du processeur sont assez courtes, au plus aussi courtes que le bus mémoire. Ainsi, on est sûr de charger obligatoirement au moins une instruction complète, et peut-être même plusieurs. Le contenu du registre d'instruction est alors découpé en instructions au fil de l'eau. Utiliser un registre d'instruction large pose problème avec des instructions à cheval entre deux blocs. Il se peut qu'on n'ait pas une instruction complète lorsque l'on arrive à la fin du bloc, mais seulement un morceau. Le problème se manifeste peu importe que l'instruction soit chargée d'un seul coup ou bien en plusieurs fois. [[File:Instructions non alignées.png|centre|vignette|upright=2|Instructions non alignées.]] Dans ce cas, on doit décaler le morceau de bloc pour le mettre au bon endroit (au début du registre d'instruction), et charger le prochain bloc juste à côté. On a donc besoin d'un circuit qui décale tous les bits du registre d'instruction, couplé à un circuit qui décide où placer dans le registre d'instruction ce qui a été chargé, avec quelques circuits autour pour configurer les deux circuits précédents. [[File:Décaleur d’instruction.png|centre|vignette|upright=2|Décaleur d’instruction.]] Une autre solution se passe de registre d'instruction en utilisant à la place l'état interne du séquenceur. Là encore, elle charge l'instruction morceau par morceau, typiquement par blocs de 8, 16 ou 32 bits. Les morceaux ne sont pas accumulés dans le registre d'instruction, la solution utilise à la place l'état interne du séquenceur. Les mots mémoire de l'instruction sont chargés à chaque cycle, faisant passer le séquenceur d'un état interne à un autre à chaque mot mémoire. Tant qu'il n'a pas reçu tous les mots mémoire de l'instruction, chaque mot mémoire non terminal le fera passer d'un état interne à un autre, chaque état interne encodant les mots mémoire chargés auparavant. S'il tombe sur le dernier mot mémoire d'une instruction, il rentre dans un état de décodage. ===Le chargement avec les jeux d'instruction compacts=== Pour finir, il faut parler du cas particulier des jeux d'instruction compacts. Pour rappel, ces jeux d'instruction sont présents sur certains CPU RISC 32 bits. De tels processeurs gèrent un jeu d'instruction 32 bits normal, qui encode ses instructions sur 32 bits, et un jeu d'instruction plus compact qui encode ses instructions sur 16 bits. Nous allons prendre l'exemple du jeu d'instruction emblématique de cette catégorie : l'ARM ''thumb'', qui est le jeu d'instruction compact associé au jeu d'instruction ARM 32 bits. Les deux jeux d'instruction ont un mode d'exécution chacun, ce qui veut dire que le processeur est configuré pour fonctionner en mode ''thumb'' ou en mode ARM 32 bits. En clair, on ne peut pas mélanger les deux jeu d'instructions en même temps. La raison est qu'une même suite de bit correspondait soit à une instruction ARM normale, soit à deux instructions thumbs. Pour faire la différence, il faut préciser dans quel mode d'exécution le CPU est : mode ARM ou mode thumb. Le processeur interpréte les instructions correctement en tenant compte de ce mode. Intuitivement, on se dirait qu'il y a deux décodeurs séparés : un utilisé en mode ARM, un autre utilisé en mode ''thumb''. Sauf que ce n'est pas la solution retenue. A la place, les ingénieurs d'ARM profitent d'une propriété du ''thumb'' : ''thumb'' est un sous-ensemble du jeu d’instruction 32 bits. Par exemple, une instruction ''thumb'' a un équivalent en RAM 32 bits, bien que l'encodage entre les deux est différent. Et cette propriété est utilisée pour faciliter l'implémentation au niveau du décodeur. Le processeur contient simplement un circuit qui convertit les instructions ''thumb'' en leur équivalent ARM 32 bits. Le circuit de conversion se situe en aval de l'unité de chargement, juste avant le décodeur d'instruction. Le circuit est désactivé quand le processeur fonctionne en mode ARM, mais est active en mode ''thumb''. ===Les optimisations du code automodifiant=== Précédemment dans ce cours, nous avons parlé du code automodifiant, à savoir le fait qu'un programme modifie ses propres instructions à la volée. Il était utilisé pour avoir des tableaux, sur des processeurs qui ne supportaient que le mode d'adressage direct. L'idée était que les instructions d'accès mémoire incorporaient une adresse, qui était incrémentée/décrémentée par code auto-modifiant. Les branchements indirects étaient eux aussi gérés de la même manière : l'adresse de destination, incorporée dans l'instruction via adressage absolu, était changée via code automodifiant. Quelques rares processeurs ont incorporé des optimisations pour simplifier l'usage du code automodifiant. Elles agissaient toutes sur le registre d'instruction, en permettant de modifier son contenu. Les instructions étaient chargées dans le registre d'instruction et étaient modifiées dans ce registre, avant d'être décodées. La modification était généralement assez simple : appliquer un masque, additionner une constante, guère plus. Les processeurs de ce type sont très rare, j'en connais deux exemples : le Burroughs B1700 et l'Apollo Guidance Computer. Une autre possibilité serait de faire un OU logique entre le registre d'instruction et un autre opérande. Cela permettait par exemple de remplacer une adresse dans une instruction. L'adresse est alors surimposée avec un OU dans le champ d'adresse, alors que le reste de l'instruction n'est pas modifié. L'adresse peut venir d'un registre général, par exemple. Mais cela implique d'avoir calculé l'adresse finale, ce qui n'est pas très pratique. Les Burroughs B1700 utilisaient une technique similaire, mais pour leur microcode, ce qui fait qu'on détaillera cela dans un futur chapitre. Un exemple est celui de l'Apollo Guidance Computer, qui permettait d'altérer le registre d'instruction avec une instruction dédiée, l'instruction ''Index next instruction'', que nous appellerons INI. Nous avons déjà vu cette instruction INI, dans le chapitre sur les modes d'adressage, mais nous pouvons maintenant expliquer comment elle est implémentée. Elle additionne une constante à l'instruction suivante. Elle était utilisée pour ajouter un décalage à une adresse, dans une instruction LOAD/STORE, ou un branchement direct. L'addition était vraisemblablement réalisée par l'ALU du processeur. La conséquence est que le registre d'instruction était un registre architectural, qui était adressé implicitement, avec un mode d'adressage implicite. ==L'incrémentation du ''program counter''== À chaque chargement, le ''program counter'' est mis à jour afin de pointer sur la prochaine instruction à charger. Sur la quasi-totalité des processeurs, les instructions sont placées dans l'ordre d’exécution dans la RAM. En faisant ainsi, on peut mettre à jour le ''program counter'' en lui ajoutant la longueur de l'instruction courante. Déterminer la longueur de l'instruction est simple quand les instructions ont toutes la même taille, mais certains processeurs ont des instructions de taille variable, ce qui complique le calcul. Dans ce qui va suivre, nous allons supposer que les instructions sont de taille fixe, ce qui fait que le ''program counter'' est toujours incrémenté de la même valeur. Il existe deux méthodes principales pour incrémenter le ''program counter'' : soit le ''program counter'' a son propre additionneur, soit le ''program counter'' est mis à jour par l'ALU. Pour ce qui est de l'organisation des registres, soit le ''program counter'' est un registre séparé des autres, soit il est regroupé avec d'autres registres dans un banc de registre. En tout, cela donne quatre organisations possibles. * Un ''program counter'' séparé relié à un incrémenteur séparé. * Un ''program counter'' séparé des autres registres, incrémenté par l'ALU. * Un ''program counter'' intégré au banc de registre, relié à un incrémenteur séparé. * Un ''program counter'' intégré au banc de registre, incrémenté par l'ALU. [[File:Calcul du program counter.png|centre|vignette|upright=2|les différentes méthodes de calcul du program counter.]] Les processeurs haute performance modernes utilisent tous la première méthode. Les autres méthodes étaient surtout utilisées sur les processeurs 8 ou 16 bits, elles sont encore utilisées sur quelques microcontrôleurs de faible puissance. Nous auront l'occasion de donner des exemples quand nous parlerons des processeurs 8 bits anciens, dans le chapitre sur les architectures à accumulateur et affiliées. ===Le ''program counter'' mis à jour par l'ALU=== Sur certains processeurs, le calcul de l'adresse de la prochaine instruction est effectué par l'ALU. L'avantage de cette méthode est qu'elle économise un incrémenteur dédié au ''program counter''. Ce qui explique que cette méthode était utilisée sur les processeurs assez anciens, à une époque où un additionneur pouvait facilement prendre 10 à 20% des transistors disponibles. Le désavantage principal est une augmentation de la complexité du séquenceur, qui doit gérer la mise à jour du ''program counter''. De plus, la mise à jour du ''program counter'' ne peut pas se faire en même temps qu'une opération arithmétique, ce qui réduit les performances. Si on incrémente le ''program counter'' avec l'ALU, il est intéressant de le placer dans le banc de registres. Un désavantage est qu'on perd un registre. Par exemple, avec un banc de registre de 16 registres, on ne peut adresser que 15 registres généraux. De plus ce n'est pas l'idéal pour le décodage des instructions et pour le séquenceur. Si le processeur dispose de plusieurs bancs de registres, le ''program counter'' est généralement placé dans le banc de registre dédié aux adresses. Sinon, il est placé avec les nombres entiers/adresses. D'anciens processeurs incrémentaient le ''program counter'' avec l'ALU, mais utilisaient bien un registre séparé pour le ''program counter''. Cette méthode est relativement simple à implémenter : il suffit de connecter/déconnecter le ''program counter'' du bus interne suivant les besoins. Le ''program counter'' est déconnecté pendant l’exécution d'une instruction, il est connecté au bus interne pour l'incrémenter. C'est le séquenceur qui gère le tout. L'avantage de cette séparation est qu'elle est plus facile à gérer pour le séquenceur. De plus, on gagne un registre général/entier dans le banc de registre. ===Le compteur programme=== Dans la quasi-totalité des processeurs modernes, le ''program counter'' est un vulgaire compteur comme on en a vu dans les chapitres sur les circuits, ce qui fait qu'il passe automatiquement d'une adresse à la suivante. Dans ce qui suit, nous allons appeler ce registre compteur : le '''compteur ordinal'''. L'usage d'un compteur simplifie fortement la conception du séquenceur. Le séquenceur n'a pas à gérer la mise à jour du ''program counter'', sauf en cas de branchements. De plus, il est possible d'incrémenter le ''program counter'' pendant que l'unité de calcul effectue une opération arithmétique, ce qui améliore grandement les performances. Le seul désavantage est qu'elle demande d'ajouter un incrémenteur dédié au ''program counter''. Sur les processeurs très anciens, les instructions faisaient toutes un seul mot mémoire et le compteur ordinal était un circuit incrémenteur des plus basique. Sur les processeurs modernes, le compteur est incrémenté par pas de 4 si les instructions sont codées sur 4 octets, 8 si les instructions sont codées sur 8 octets, etc. Concrètement, le compteur est un circuit incrémenteur auquel on aurait retiré quelques bits de poids faible. Par exemple, si les instructions sont codées sur 4 octets, on coupe les 2 derniers bits du compteur. Si les instructions sont codées sur 8 octets, on coupe les 3 derniers bits du compteur. Et ainsi de suite. Une amélioration de la solution précédente utilise un circuit incrémenteur partagé entre le ''program counter'' et d'autres registres. L'incrémenteur est utilisé pour incrémenter le ''program counter'', mais aussi pour effectuer d'autres calculs d'adresse. Par exemple, sur les architectures avec une pile d'adresse de retour, il est possible de partager l'incrémenteur/décrémenteur avec le pointeur de pile ( la technique ne marche pas avec une pile d'appel). Un autre exemple est celui des processeurs qui gèrent automatiquement le rafraichissement mémoire, grâce à, un compteur intégré dans le processeur. Il est possible de partager l'incrémenteur du ''program counter'' avec le compteur de rafraichissement mémoire, qui mémorise la prochaine adresse mémoire à rafraichir. Nous avions déjà abordé ce genre de partage dans le chapitre sur le chemin de données, dans l'annexe sur le pointeur de pile, mais nous verrons d'autres exemples dans le chapitre sur les architectures hybrides accumulateur-registre 8/16 bits. Il a existé quelques processeurs pour lesquelles le ''program counter'' était non pas un compteur binaire classique, mais un ''linear feedback shift register''. L'avantage est que le circuit d'incrémentation utilisait bien moins de portes logiques, l'économie était substantielle. Mais un défaut est que des instructions consécutives dans le programme n'étaient pas consécutives en mémoire. Il existait cependant une table de correspondance pour dire : la première instruction est à telle adresse, la seconde à telle autre, etc. Un exemple de processeur de ce type est le TMS 1000 de Texas Instrument, un des premiers microcontrôleur 4 bits datant des années 70. ===Quand est mis à jour le ''program counter'' ?=== Le ''program counter'' est mis à jour quand il faut charger une nouvelle instruction. Reste qu'il faut savoir quand le mettre à jour. Incrémenter le ''program counter'' à intervalle régulier, par exemple à chaque cycle d’horloge, fonctionne sur les processeurs où toutes les instructions mettent le même temps pour s’exécuter. Mais de tels processeurs sont très rares. Sur la quasi-totalité des processeurs, les instructions ont une durée d’exécution variable, elles ne prennent pas le même nombre de cycle d'horloge pour s’exécuter. Le temps d’exécution d'une instruction varie selon l'instruction, certaines sont plus longues que d'autres. Tout cela amène un problème : comment incrémenter le ''program counter'' avec des instructions avec des temps d’exécution variables ? La réponse est que la mise à jour du ''program counter'' démarre quand l'instruction précédente a terminée de s’exécuter, plus précisément un cycle avant. C'est un cycle avant pour que l'instruction chargée soit disponible au prochain cycle pour le décodeur. Pour cela, le séquenceur doit déterminer quand l'instruction est terminée et prévenir le ''program counter'' au bon moment. Il faut donc une interaction entre séquenceur et circuit de chargement. Le circuit de chargement contient une entrée Enable, qui autorise la mise à jour du ''program counter''. Le séquenceur met à 1 cette entrée pour prévenir au ''program counter'' qu'il doit être incrémenté au cycle suivant ou lors du cycle courant. Cela permet de gérer simplement le cas des instructions multicycles. [[File:Commande de la mise à jour du program counter.png|centre|vignette|upright=2|Commande de la mise à jour du program counter.]] Toute la difficulté est alors reportée dans le séquenceur, qui doit déterminer la durée d'une instruction. Pour gérer la mise à jour du ''program counter'', le séquenceur doit déterminer la durée d'une instruction. Cela est simple pour les instructions arithmétiques et logiques, qui ont un nombre de cycles fixe, toujours le même. Une fois l'instruction identifiée par le séquenceur, il connait son nombre de cycles et peut programmer un compteur interne au séquenceur, qui compte le nombre de cycles de l'instruction, et fournit le signal Enable au ''program counter''. Mais cette technique ne marche pas vraiment pour les accès mémoire, dont la durée n'est pas connue à l'avance, surtout sur les processeurs avec des mémoires caches. Pour cela, le séquenceur détermine quand l'instruction mémoire est finie et prévient le ''program counter'' quand c'est le cas. Il existe quelques processeurs pour lesquels le temps de calcul dépend des opérandes de l'instruction. On peut par exemple avoir une division qui prend 10 cycles avec certaines opérandes, mais 40 cycles avec d'autres opérandes. Mais ces processeurs sont rares et cela est surtout valable pour les opérations de multiplication/division, guère plus. Le problème est alors le même qu'avec les accès mémoire et la solution est la même : l'ALU prévient le séquenceur quand le résultat est disponible. ==Les branchements et le ''program counter''== Un branchement consiste juste à écrire l'adresse de destination dans le ''program counter''. L'implémentation des branchements demande d'extraire l'adresse de destination et d'altérer le ''program counter'' quand un branchement est détecté. Pour cela, le décodeur d'instruction détecte si l'instruction exécutée est un branchement ou non, et il en déduit où trouver l'adresse de destination. L'adresse de destination se trouve à un endroit qui dépend du mode d'adressage du branchement. Pour rappel, on distingue quatre modes d'adressage pour les branchements : direct, indirect, relatif et implicite. Pour les branchements directs, l'adresse est intégrée dans l'instruction de branchement elle-même et est extraite par le séquenceur. Pour les branchements indirects, l'adresse de destination est dans le banc de registre. Pour les branchements relatifs, il faut ajouter un décalage au ''program counter'', décalage extrait de l'instruction par le séquenceur. Enfin, les instructions de retour de fonction lisent l'adresse de destination depuis la pile. L'implémentation de ces quatre types de branchements est très différente. L'altération du ''program counter'' dépend de si le ''program counter'' est dans un compteur séparé ou s'il est dans le banc de registre. Nous avons vu plus haut qu'il y a quatre cas différents, mais nous n'allons voir que deux cas : celui où le ''program counter'' est dans le banc de registre et est incrémenté par l'unité de calcul, celui avec un ''program counter'' séparé avec son propre incrémenteur. Les deux autres cas ne sont utilisés que sur des architectures à accumulateur ou à pile spécifiques, que nous n'avons pas encore vu à ce stade du cours et que nous verrons en temps voulu dans le chapitre sur ces architectures. ===L’implémentation des branchements avec un ''program counter'' intégré au banc de registres=== Le cas le plus simple est celui où le ''program counter'' est intégré au banc de registre, avec une incrémentation faite par l'unité de calcul. Charger une instruction revient alors à effectuer une instruction LOAD en adressage indirect, à deux différences près : le registre sélectionné est le ''program counter'', l’instruction est copiée dans le registre d'instruction et non dans le banc de registre. L'incrémentation du ''porgram counter'' est implémenté avec des micro-opérations qui agissent sur le chemin de données, au même titre que les branchements indirects, relatifs et directs. * Les branchements indirects copient un registre dans le ''program counter'', ce qui revient simplement à faire une opération MOV entre deux registres, dont le ''program counter'' est la destination. * L'opération inverse d'un branchement indirect, qui copie le ''program counter'' dans un registre général, est utile pour sauvegarder l'adresse de retour d'une fonction. * Les branchements directs copient une adresse dans le ''program counter'', ce qui les rend équivalents à une opération MOV avec une constante immédiate, dont le ''program counter'' est la destination. * Les branchements relatifs sont une opération arithmétique entre le ''program counter'' et un opérande en adressage immédiat. En clair, à partir du moment où le chemin de données supporte les instructions MOV et l'addition, avec les modes d'adressage adéquat, il supporte les branchements. Aucune modification du chemin de données n'est nécessaire, le séquenceur gére le chargement d'une instruction avec les micro-instructions adéquates : une micro-opération ADD pour incrémenter le ''program counter'', une micro-opération LOAD pour le chargement de l'instruction. Notons que sur certaines architectures, le ''program counter'' est adressable au même titre que les autres registres du banc de registre. Les instructions de branchement sont alors remplacées par des instructions MOV ou des instructions arithmétique équivalentes, comme décrit plus haut. ===L’implémentation des branchements avec un ''program counter'' séparé=== Étudions maintenant le cas où le ''program counter'' est dans un registre/compteur séparé. Rappelons que tout compteur digne de ce nom possède une entrée de réinitialisation, qui remet le compteur à zéro. De plus, on peut préciser à quelle valeur il doit être réinitialisé. Ici, la valeur à laquelle on veut réinitialiser le compteur n'est autre que l'adresse de destination du branchement. Implémenter un branchement est donc simple : l'entrée de réinitialisation est commandée par le circuit de détection des branchements, alors que la valeur de réinitialisation est envoyée sur l'entrée adéquate. En clair, nous partons du principe que le ''program counter'' est implémenté avec ce type de compteur : [[File:Fonctionnement d'un compteur (décompteur), schématique.jpg|centre|vignette|upright=1.5|Fonctionnement d'un compteur (décompteur), schématique]] Toute la difficulté est de présenter l'adresse de destination au ''program counter''. Pour les branchements directs, l'adresse de destination est fournie par le séquenceur. L'adresse de destination du branchement sort du séquenceur et est présentée au ''program counter'' sur l'entrée adéquate. Voici donc comment sont implémentés les branchements directs avec un ''program counter'' séparé du banc de registres. Le schéma ci-dessous marche peu importe que le ''program counter'' soit incrémenté par l'ALU ou par un additionneur dédié. [[File:Unité de détection des branchements dans le décodeur.png|centre|vignette|upright=2|Unité de détection des branchements dans le décodeur]] Les branchements relatifs sont ceux qui font sauter X instructions plus loin dans le programme. Leur implémentation demande d'ajouter une constante au ''program counter'', la constante étant fournie dans l’instruction. Là encore, deux solutions sont possibles : réutiliser l'ALU pour calculer l'adresse, ou utiliser un additionneur séparé. L'additionneur séparé peut être fusionné avec l'additionneur qui incrémente le ''program counter'' pour passer à l’instruction suivante. [[File:Unité de chargement qui gère les branchements relatifs.png|centre|vignette|upright=2|Unité de chargement qui gère les branchements relatifs.]] Pour les branchements indirects, il suffit de lire le registre voulu et d'envoyer le tout sur l'entrée adéquate du ''program counter''. Il faut alors rajouter un multiplexeur pour que l'entrée de réinitialisationr ecoive la bonne adresse de destination. [[File:Unité de chargement qui gère les branchements directs.png|centre|vignette|upright=2|Unité de chargement qui gère les branchements directs et indirects.]] Toute la difficulté de l'implémentation des branchements est de configurer les multiplexeurs, ce qui est réalisé par le séquenceur en fonction du mode d'adressage du branchement. ==Les optimisations du chargement des instructions== Charger une instruction est techniquement une forme d'accès mémoire un peu particulier. En clair, charger une instruction prend au minimum un cycle d'horloge, et cela peut rapidement monter à 3-5 cycles, si ce n'est plus. L'exécution des instructions est alors fortement ralentit par la mémoire. Par exemple, imaginez que la mémoire mette 3 cycles d'horloges pour charger une instruction, alors qu'une instruction s’exécute en 1 cycle (si les opérandes sont dans les registres). La perte de performance liée au chargement des instructions est alors substantielle. Heureusement, il est possible de limiter la casse en utilisant des mémoires caches, ainsi que d'autres optimisations, que nous allons voir dans ce qui suit. ===Le tampon de préchargement=== La technique dite du '''préchargement''' est utilisée dans le cas où la mémoire a un temps d'accès important. Mais si la latence de la RAM est un problème, le débit ne l'est pas. Il est possible d'avoir une RAM lente, mais à fort débit. Par exemple, supposons que la mémoire puisse charger 4 instructions (de taille fixe) en 3 cycles. Le processeur peut alors charger 4 instructions en un seul accès mémoire, et les exécuter l'une après l'autre, une par cycle d'horloge. Les temps d'attente sont éliminés : le processeur peut décoder une nouvelle instruction à chaque cycle. Et quand la dernière instruction préchargée est exécutée, la mémoire est de nouveau libre, ce qui masque la latence des accès mémoire. [[File:Tampon de préchargement d'instruction.png|vignette|Tampon de préchargement d'instruction]] La seule contrainte est de mettre les instructions préchargées en attente. La solution pour cela est d'utiliser un registre d'instruction très large, capable de mémoriser plusieurs instructions à la fois. Ce registre est de plus connecté à un multiplexeur qui permet de sélectionner l'instruction adéquate dans ce registre. Ce multiplexeur est commandé par le ''program counter'' et quelques circuits annexes. Ce super-registre d'instruction est appelé un '''tampon de préchargement'''. La méthode a une implémentation nettement plus simple avec des instructions de taille fixe, alignées en mémoire. La commande du multiplexeur de sélection de l'instruction est alors beaucoup plus simple : il suffit d'utiliser les bits de poids fort du ''program counter''. Par exemple, prenons le cas d'un registre d'instruction de 32 octets pour des instructions de 4 octets, soit 8 instructions. Le choix de l'instruction à sélectionner se fait en utilisant les 3 bits de poids faible du ''program counter''. Mais cette méthode ajoute de nouvelles contraintes d'alignement, similaires à celles vues dans le chapitre sur l'alignement et le boutisme, sauf que l'alignement porte ici sur des blocs d'instructions de même taille que le tampon de préchargement. Si on prend l'exemple d'un tampon de préchargement de 128 bits, les instructions devront être alignées par blocs de 128 bits. C'est à dire qu'idéalement, les fonctions et autres blocs de code isolés doivent commencer à des adresses multiples de 128, pour pouvoir charger un bloc d'instruction en une seule fois. Sans cela, les performances seront sous-optimales. : Il arrive que le tampon de préchargement ait la même taille qu'une ligne de cache. Lorsque l'on passe d'un bloc d'instruction à un autre, le tampon de préchargement est mis à jour. Par exemple, si on prend un tampon de préchargement de 4 instructions, on doit changer son contenu toutes les 4 instructions. La seule exception est l'exécution d'un branchement. En effet, lors d'un branchement, la destination du branchement n'est pas dans le tampon de préchargement et elle doit être chargée dedans (sauf si le branchement pointe vers une instruction très proche, ce qui est improbable). Pour cela, le tampon de préchargement est mis à jour précocement quand le processeur détecte un branchement. Le même problème survient avec du code auto-modifiant. Un code auto-modifiant est un programme qui se modifie lui-même, en remplaçant certaines instructions par d'autres, en en retirant, en en ajoutant, lors de sa propre exécution. De tels programmes sont rares, mais la technique était utilisée dans quelques cas au tout début de l'informatique sur des ordinateurs rudimentaires. Ceux-ci avaient des modes d'adressages tellement limités que gérer des tableaux de taille variable demandait d'utiliser du code auto-modifiant pour écrire des boucles. Le problème survient quand des instructions sont modifiées immédiatement après avoir été préchargées. Les instructions dans le tampon de préchargement sont l'ancienne version, alors que la mémoire RAM contient les instructions modifiées. Gérer ce genre de cas demande de vider le tampon de préchargement si le cas arrive, mais ça ne vaut que très rarement le coup, aussi les ingénieurs ne s’embêtent pas à mettre ce correctif en place. Le code automodifiant est alors buggé. ===La ''prefetch input queue''=== La ''prefetch input queue'' est une sorte de cousine du tampon de préchargement, qui a été utilisée sur les processeurs Intel 8 et 16 bits, ainsi que sur le 286 et le 386. L'idée est là encore de précharger des instructions en avance. Sauf que cette fois-ci, on ne charge pas plusieurs instructions à la fois. Au contraire, les instructions sont préchargées séquentiellement, un ou deux octet à la fois. En conséquence, le tampon de préchargement est remplacé par une mémoire FIFO appelée la '''''Prefetch Input Queue''''', terme abrévié en PIQ. Les instructions sont accumulées une par une dans la PIQ, elles en sortent une par une au fur et à mesure pour alimenter le décodeur d'instruction. Pendant que le processeur exécute une instruction, on précharge la suivante. Sans ''prefetch input queue'', le processeur chargeait une instruction, la décodait et l'exécutait, puis chargeait la suivante, et ainsi de suite. Avec un tampon de préchargement, le processeur chargeait plusieurs instruction en une seule fois, puis les exécutait les unes après les autres. Avec la ''prefetch input queue'', pendant qu'une instruction est en cours de décodage/exécution, le processeur précharge l'instruction suivante. Si une instruction prend vraiment beaucoup de temps pour s'exécuter, le processeur peut en profiter pour précharger l'instruction encore suivante, et ainsi de suite, jusqu'à une limite de 4/6 instructions (la limite dépend du processeur). La ''prefetch input queue'' permet de précharger l'instruction suivante à condition qu'aucun autre accès mémoire n'ait lieu. Si l'instruction en cours d'exécution effectue des accès mémoire, ceux-ci sont prioritaires sur tout le reste, le préchargement de l'instruction suivante est alors mis en pause ou inhibé. Par contre, si la mémoire n'est pas utilisée par l'instruction en cours d'exécution, le processeur lit l'instruction suivante en RAM et la copie dans la ''prefetch input queue''. En conséquence, il arrive que la ''prefetch input queue'' n'ait pas préchargé l'instruction suivante, alors que le décodeur d'instruction est prêt pour la décoder. Dans ce cas, le décodeur doit attendre que l'instruction soit chargée. Les instructions préchargées dans la ''Prefetch input queue'' y attendent que le processeur les lise et les décode. Les instructions préchargées sont conservées dans leur ordre d'arrivée, afin qu'elles soient exécutées dans le bon ordre, ce qui fait que la ''Prefetch input queue'' est une mémoire de type FIFO. L'étape de décodage pioche l'instruction à décoder dans la ''Prefetch input queue'', cette instruction étant par définition la plus ancienne, puis la retire de la file. Le premier processeur commercial grand public à utiliser une ''prefetch input queue'' était le 8086. Il pouvait précharger à l'avance 6 octets d’instruction. L'Intel 8088 avait lui aussi une ''Prefetch input queue'', mais qui ne permettait que de précharger 4 instructions. Le 386 avait une ''prefetch input queue'' de 16 octets. La taille idéale de la FIFO dépend de nombreux paramètres. On pourrait croire que plus elle peut contenir d'instructions, mieux c'est, mais ce n'est pas le cas, pour des raisons que nous allons expliquer dans quelques paragraphes. [[File:80186 arch.png|centre|vignette|700px|upright=2|Architecture du 8086, du 80186 et de ses variantes.]] Vous remarquerez que j'ai exprimé la capacité de la ''prefetch input queue'' en octets et non en instructions. La raison est que sur les processeurs x86, les instructions sont de taille variable, avec une taille qui varie entre un octet et 6 octets. Cependant, le décodage des instructions se fait un octet à la fois : le décodeur lit un octet, puis éventuellement le suivant, et ainsi de suite jusqu'à atteindre la fin de l'instruction. En clair, le décodeur lit les instructions longues octet par octet dans la ''Prefetch input queue''. Les instructions varient entre 1 et 6 octets, mais tous ne sont pas utiles au décodeur d'instruction. Par exemple, le décodeur d'instruction n'a pas besoin d'analyser les constantes immédiates intégrées dans une instruction, ni les adresses mémoires en adressage absolu. Il n'a besoin que de deux octets : l'opcode et l'octet Mod/RM qui précise le mode d'adressage. Le second est facultatif. En clair, le décodeur a besoin de lire deux octets maximum depuis la ''prefetch input queue'', avant de passer à l’instruction suivante. Les autres octets étaient envoyés ailleurs, typiquement dans le chemin de données. Par exemple, prenons le cas d'une addition entre le registre AX et une constante immédiate. L'instruction fait trois octets : un opcode suivie par une constante immédiate codée sur deux octets. L'opcode était envoyé au décodeur, mais pas la constante immédiate. Elle était lue octet par octet et mémorisée dans un registre temporaire placé en entrée de l'ALU. Idem avec les adresses immédiates, qui étaient envoyées dans un registre d’interfaçage mémoire sans passer par le décodeur d'instruction. Pour cela, la ''prefetch input queue'' était connectée au bus interne du processeur ! Le décodeur dispose d'une micro-opération pour lire un octet depuis la ''prefetch input queue'' et le copier ailleurs dans le chemin de données. Par exemple, l'instruction d'addition entre le registre AX et une constante immédiate était composée de quatre micro-opérations : une qui lisait le premier octet de la constante immédiate, une seconde pour le second, une troisième micro-opération qui commande l'ALU et fait le calcul. Le décodeur devait attendre que qu'un moins un octet soit disponible dans la ''Prefetch input queue'', pour le lire. Il lisait alors cet octet et déterminait s'il contenait une instruction complète ou non. Si c'est une instruction complète, il la décodait et l''exécutait, puis passait à l'instruction suivante. Sinon, il lit un second octet depuis la ''Prefetch input queue'' et relance le décodage. Là encore, le décodeur vérifie s'il a une instruction complète, et lit un troisième octet si besoin, puis rebelote avec un quatrième octet lu, etc. Un circuit appelé le ''loader'' synchronisait le décodeur d'instruction et la ''Prefetch input queue''. Il fournissait deux bits : un premier bit pour indiquer que le premier octet d'une instruction était disponible dans la ''Prefetch input queue'', un second bit pour le second octet. Le ''loader'' recevait aussi des signaux de la part du décodeur d'instruction. Le signal ''Run Next Instruction'' entrainait la lecture d'une nouvelle instruction, d'un premier octet, qui était alors dépilé de la ''Prefetch input queue''. Le décodeur d'instruction pouvait aussi envoyer un signal ''Next-to-last (NXT)'', un cycle avant que l'instruction en cours ne termine. Le ''loader'' réagissait en préchargeant l'octet suivant. En clair, ce signal permettait de précharger l'instruction suivante avec un cycle d'avance, si celle-ci n'était pas déjà dans la ''Prefetch input queue''. Le bus de données du 8086 faisait 16 bits, alors que celui du 8088 ne permettait que de lire un octet à la fois. Et cela avait un impact sur la ''prefetch input queue''. La ''prefetch input queue'' du 8088 était composée de 4 registres de 8 bits, avec un port d'écriture de 8 bits pour précharger les instructions octet par octet, et un port de lecture de 8 bits pour alimenter le décodeur. En clair, tout faisait 8 bits. Le 8086, lui utilisait des registres de 16 bits et un port d'écriture de 16 bits. le port de lecture restait de 8 bits, grâce à un multiplexeur sélectionnait l'octet adéquat dans un registre 16 bits. Sa ''prefetch input queue'' préchargeait les instructions par paquets de 2 octets, non octet par octet, alors que le décodeur consommait les instructions octet par octet. Il s’agissait donc d'une sorte d'intermédiaire entre ''prefetch input queue'' à la 8088 et tampon de préchargement. Le 386 était dans un cas à part. C'était un processeur 32 bits, sa ''prefetch input queue'' contenait 4 registres de 32 bits, un port d'écriture de 32 bits. Mais il ne lisait pas les instructions octet par cotet. A la place, son décodeur d'instruction avait une entrée de 32 bits. Cependant, il gérait des instructions de 8 et 16 bits. Il fallait alors dépiler des instructions de 8, 16 et 32 bits dans la ''prefetch input queue''. De plus, les instructions préchargées n'étaient pas parfaitement alignées sur 32 bits : une instruction pouvait être à cheval sur deux registres 32 bits. Le processeur incorporait donc des circuits d'alignement, semblables à ceux utilisés pour gérer des instructions de longueur variable avec un registre d'instruction. Les branchements posent des problèmes avec la ''prefetch input queue'' : à cause d'eux, on peut charger à l'avance des instructions qui sont zappées par un branchement et ne sont pas censées être exécutées. Si un branchement est chargé, toutes les instructions préchargées après sont potentiellement invalides. Si le branchement est non-pris, les instructions chargées sont valides, elles sont censées s’exécuter. Mais si le branchement est pris, elles ont été chargées à tort et ne doivent pas s’exécuter. Pour éviter cela, la ''prefetch input queue'' est vidée quand le processeur détecte un branchement. Pour cela, le décodeur d'instruction dispose d'une micro-opération qui vide la ''Prefetch input queue'', elle invalide les instructions préchargées. Cette détection ne peut se faire qu'une fois le branchement décodé, qu'une fois que l'on sait que l'instruction décodée est un branchement. En clair, le décodeur invalide la ''Prefetch input queue'' quand il détecte un branchement. Les interruptions posent le même genre de problèmes. Il faut impérativement vider la ''Prefetch input queue'' quand une interruption survient, avant de la traiter. Il faut noter qu'une ''Prefetch input queue'' interagit assez mal avec le ''program counter''. En effet, la ''Prefetch input queue'' précharge les instructions séquentiellement. Pour savoir où elle en est rendue, elle mémorise l'adresse de la prochaine instruction à charger dans un '''registre de préchargement''' dédié. Il serait possible d'utiliser un ''program counter'' en plus du registre de préchargement, mais ce n'est pas la solution qui a été utilisée. Les processeurs Intel anciens ont préféré n'utiliser qu'un seul registre de préchargement en remplacement du ''program counter''. Après tout, le registre de préchargement n'est qu'un ''program counter'' ayant pris une avance de quelques cycles d'horloge, qui a été incrémenté en avance. Et cela ne pose pas de problèmes, sauf avec certains branchements. Par exemple, certains branchements relatifs demandent de connaitre la véritable valeur du ''program counter'', pas celle calculée en avance. Idem avec les instructions d'appel de fonction, qui demandent de sauvegarder l'adresse de retour exacte, donc le vrai ''program counter''. De telles situations demandent de connaitre la valeur réelle du ''program counter'', celle sans préchargement. Pour cela, le décodeur d'instruction dispose d'une instruction pour reconstituer le ''program counter'', à savoir corriger le ''program coutner'' et éliminer l'effet du préchargement. La micro-opération de correction se contente de soustraire le nombre d'octets préchargés au registre de préchargement. Le nombre d'octets préchargés est déduit à partir des deux pointeurs intégré à la FIFO, qui indiquent la position du premier et du dernier octet préchargé. Le bit qui indique si la FIFO est vide était aussi utilisé. Les deux pointeurs sont lus depuis la FIFO, et sont envoyés à un circuit qui détermine le nombre d'octets préchargés. Sur le 8086, ce circuit était implémenté non pas par un circuit combinatoire, mais par une mémoire ROM équivalente. La petite taille de la FIFO faisait que les pointeurs étaient très petits et la ROM l'était aussi. Un autre défaut est que la ''Prefetch input queue'' se marie assez mal avec du code auto-modifiant. Un code auto-modifiant est un programme qui se modifie lui-même, en remplaçant certaines instructions par d'autres, en en retirant, en en ajoutant, lors de sa propre exécution. De tels programmes sont rares, mais la technique était utilisée dans quelques cas au tout début de l'informatique sur des ordinateurs rudimentaires. Ceux-ci avaient des modes d'adressages tellement limités que gérer des tableaux de taille variable demandait d'utiliser du code auto-modifiant pour écrire des boucles. Le problème avec la ''Prefetch input queue'' survient quand des instructions sont modifiées immédiatement après avoir été préchargées. Les instructions dans la ''Prefetch input queue'' sont l'ancienne version, alors que la mémoire RAM contient les instructions modifiées. Gérer ce genre de cas est quelque peu complexe. Il faut en effet vider la ''Prefetch input queue'' si le cas arrive, ce qui demande d'identifier les écritures qui écrasent des instructions préchargées. C'est parfaitement possible, mais demande de transformer la ''Prefetch input queue'' en une mémoire hybride, à la fois mémoire associative et mémoire FIFO. Cela ne vaut que très rarement le coup, aussi les ingénieurs ne s’embêtent pas à mettre ce correctif en place, le code automodifiant est alors buggé. Le décodeur d'instruction dispose aussi d'une micro-opération qui stoppe le préchargement des instructions dans la ''Prefetch input queue''. : Pour ceux qui ont déjà lu un cours d'architecture des ordinateurs, la relation entre ''prefetch input queue'' et pipeline est quelque peu complexe. En apparence, la ''prefetch input queue'' permet une certaines forme de pipeline, en chargeant une instruction pendant que la précédente s'exécute. Les problématiques liées aux branchements ressemblent beaucoup à celles de la prédiction de branchement. Cependant, il est possible de dire qu'il s'agit plus d'une technique de préchargement que de pipeline. La différence entre les deux est assez subtile et les frontières sont assez floues, mais le fait est que l'instruction en cours d'exécution et le préchargement peuvent tout deux faire des accès mémoire et que l'instruction en cours a la priorité. Un vrai pipeline se débrouillerait avec une architecture Harvard, non avec un bus mémoire unique pour le chargement des instruction et la lecture/écriture des données. ===Le ''Zero-overhead looping''=== Nous avions vu dans le chapitre sur les instructions machines, qu'il existe des instructions qui permettent de grandement faciliter l'implémentation des boucles. Il s'agit de l'instruction REPEAT, utilisée sur certains processeurs de traitement de signal. Elle répète l'instruction suivante, voire une série d'instructions, un certain nombre de fois. Le nombre de répétitions est mémorisé dans un registre décrémenté à chaque itération de la boucle. Elle permet donc d'implémenter une boucle FOR facilement, sans recourir à des branchements ou des conditions. Une technique en lien avec cette instruction permet de grandement améliorer les performances et la consommation d'énergie. L'idée est de mémoriser la boucle, les instructions à répéter, dans une petite mémoire cache située entre l'unité de chargement et le séquenceur. Le cache en question s'appelle un '''tampon de boucle''', ou encore un ''hardware loop buffer''. Grâce à lui, il n'y a pas besoin de charger les instructions à chaque fois qu'on les exécute, on les charge une bonne fois pour toute : c'est plus rapide. Bien sûr, il ne fonctionne que pour de petites boucles, mais il en est de même avec l'instruction REPEAT. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le chemin de données | prevText=Le chemin de données | next=L'unité de contrôle | nextText=L'unité de contrôle }} </noinclude> n2jqnk9x9a4oaok3rqsu8432zm4synz 764039 764020 2026-04-19T19:58:03Z Mewtow 31375 /* Les optimisations du code automodifiant */ 764039 wikitext text/x-wiki L'unité de contrôle s'occupe du chargement des instructions et de leur interprétation, leur décodage. Elle contient deux circuits : l''''unité de chargement''' qui charge l'instruction depuis la mémoire, et le '''séquenceur''' qui commande le chemin de données. Les deux circuits sont séparés, mais ils communiquent entre eux, notamment pour gérer les branchements. Dans ce chapitre, nous allons nous intéresser au chargement d'une instruction depuis la RAM/ROM, nous verrons l'étape de décodage de l'instruction au prochain chapitre. L'unité de chargement a pour cœur le ''program counter'', le registre qui mémorise l'adresse de l'instruction à charger. Il s'agit précisément d'un compteur, vu que le contenu du registre est incrémenté à chaque fois qu'une instruction est chargée. Le ''program counter'' est remis à zéro lors du démarrage du processeur, mais est aussi modifié par les instructions de branchement qui écrivent une adresse dedans. Son interface est la suivante : une entrée d'horloge, une entrée de ''Reset'', une entrée ''Enable'' qui indique quand l'incrémenter (renommée ''Instruction Fetch''), et deux entrées pour les branchements. Pour les branchements, il y a une entrée pour l'adresse de destination, une autre pour autoriser l'écriture de celle-ci dans le ''program counter''. La sortie du ''program counter'' est reliée au bus d'adresse mémoire, plus ou moins directement. [[File:Program counter.jpg|centre|vignette|upright=2|Program counter]] ==Les interconnexions entre séquenceur et bus mémoire== Pour lire une instruction, le processeur envoie le ''program counter'' sur le bus d'adresse et récupère l'instruction sur le bus de données. L'instruction lue est alors envoyée au séquenceur. Et cela demande de connecter le séquenceur au bus mémoire, à la fois sur le bus d'adresse et le bus de données. Et sur ce point, la situation est différente selon que l'on parler d'une architecture Harvard ou Von Neumann. ===Les architectures Harvard : des bus mémoire séparés pour la RAM et la ROM=== Sur les architectures Harvard, le séquenceur et le chemin de donnée utilisent des interfaces mémoire séparées. Le séquenceur est directement relié au bus mémoire de la ROM, alors que le chemin de données est connecté à la RAM. [[File:Microarchitecture de l'interface mémoire d'une architecture Harvard.png|centre|vignette|upright=2|Microarchitecture de l'interface mémoire d'une architecture Harvard]] Le ''program counter'' est envoyé sur le bus d'adresse de la ROM, l'instruction est récupérée sur le bus de données de la ROM. Pour la mémoire RAM, elle échange des données avec le chemin de données, notamment avec les registres généraux. Les adresses utilisées pour la RAM viennent elles soit du chemin de données, soit de l'unité de contrôle, tout dépend du mode d'adressage. Mais le ''program counter'' n'est pas impliqué. [[File:Architecture Harvard - échanges de données.png|centre|vignette|upright=2|Architecture Harvard - échanges de données]] Les architectures Harvard modifiées doivent cependant rajouter une connexion entre le bus ROM et les registres généraux. C'est nécessaire pour charger une donnée constante depuis la mémoire ROM. Rappelons que la donnée constante est copiée dans un registre général, donc dans le chemin de données. [[File:Architecture Harvard modifiée - implémentation du processeur.png|centre|vignette|upright=2|Architecture Harvard modifiée - implémentation du processeur]] ===Les architectures Von-Neumann : un bus mémoire unique=== Sur les architectures Von-Neumann et affiliées, le séquenceur et le chemin de donnée partagent la même interface mémoire. Et cela pose deux problèmes. Le premier problème est que le bus mémoire doit être libéré une fois l'instruction chargée, pour un éventuel accès mémoire. Et le séquenceur doit conserver une copie de l'instruction chargée, sans quoi il ne peut pas décoder l'instruction correctement. Par exemple, si l'instruction met plusieurs cycles à s'exécuter, le séquenceur doit conserver une copie de l'instruction durant ces plusieurs cycles. Pour ça, le processeur intègre un '''registre d'instruction''' situé juste avant le séquenceur, qui mémorise l'instruction chargée. [[File:Registre d'instruction.png|centre|vignette|upright=2|Registre d'instruction.]] Le second problème est de gérer le flot des instructions/données entre le bus mémoire, le chemin de données et le séquenceur. Si le processeur lit une instruction, le bus doit être relié à l'unité de contrôle. Par contre, s'il accède à une donnée, il doit être relié au chemin de données. Le processeur doit connecter l'interface mémoire soit au séquenceur, soit au chemin de données et cela complique le réseau d'interconnexion interne au processeur. [[File:Architecture Von Neumann - implémentation du processeur.png|centre|vignette|upright=2|Architecture Von Neumann - implémentation du processeur]] Une première solution utilise un bus unique qui relie l'interface mémoire, le séquenceur et le chemin de données. Pour charger une instruction, le séquenceur copie le ''program counter'' sur le bus d'adresse, attend que l'instruction lue soit disponible sur le bus de données, puis la copie dans le registre d'instruction. Le bus mémoire est alors libre et peut être utilisé pour lire ou écrire des données, si le besoin s'en fait sentir. : Il faut noter que cette solution implique d'utiliser des registres d’interfaçage avec la mémoire. [[File:Microarchitecture de l'interface mémoire d'une architecture von neumann.png|centre|vignette|upright=2|Microarchitecture de l'interface mémoire d'une architecture von neumann]] Une autre solution utilise deux bus interne séparés : un connecté au bus d'adresse, l'autre au bus de données. Le ''program counter'' est alors connecté au bus interne d'adresse, le séquenceur est relié au bus interne de données. Notons que la technique marche bien si le ''program counter'' est dans le banc de registre : les interconnexions utilisées pour gérer l'adressage indirect permettent d'envoyer le ''program counter'' sur le bus d'adresse sans ajout de circuit. Le tout peut être amélioré en remplaçant les deux bus par des multiplexeurs et démultiplexeurs. Le bus d'adresse est précédé par un multiplexeur, qui permet de faire le choix entre ''Program Counter'', adresse venant du chemin de données, ou adresse provenant du séquenceur (adressage absolu). De même, le bus de données est suivi par un démultiplexeur qui envoie la donnée/instruction lue soit au registre d'instruction, soit au chemin de données. Le tout se marie très bien avec les chemins de donnée vu dans le chapitre précédent. Au passage, il faut noter que cette solution nécessite un banc de registre multi-port. [[File:Connexion du program counter sur les bus avec PC isolé.png|centre|vignette|upright=2|Connexion du program counter sur les bus avec PC isolé]] ==Le chargement d'une instruction== [[File:Étape de chargement.png|vignette|upright=1|Étape de chargement.]] L'étape de chargement (ou ''fetch'') doit faire deux choses : mettre à jour le ''program counter'' et lire l'instruction en mémoire RAM ou en ROM. Les deux étapes peuvent être faites en parallèle, dans des circuits séparés. Pendant que l'instruction est lue depuis la mémoire RAM/ROM, le ''program counter'' est incrémenté et/ou altéré par un branchement. Pour lire l'instruction, on envoie le ''program counter'' sur le bus d'adresse et on récupère l'instruction sur le bus de données pour l'envoyer sur l'entrée du séquenceur. ===Le chargement des instructions de longueur variable=== Le chargement des instructions de longueur variable pose de nombreux problèmes. Le premier est que mettre à jour le ''program counter'' demande de connaitre la longueur de l'instruction chargée, pour l'ajouter au ''program counter''. Si la longueur d'une instruction est variable, on doit connaitre cette longueur d'une manière où d'une autre. La solution la plus simple indique la longueur de l'instruction dans une partie de l'opcode ou de la représentation en binaire de l'instruction. Mais les processeurs haute performance de nos PC utilisent d'autres solutions, qui n'encodent pas explicitement la taille de l'instruction. Les processeurs récents utilisent un registre d'instruction de grande taille, capable de mémoriser l'instruction la plus longue du processeur. Par exemple, si un processeur supporte des instructions allant de 1 à 8 octets, comme les CPU x86, le registre d'instruction fera 8 octets. Le décodeur d'instruction vérifie à chaque cycle le contenu du registre d'instruction. Si une instruction est détectée dedans, son décodage commence, peu importe la taille de l'instruction. Une fois l'instruction exécutée, elle est retirée du registre d'instruction. Reste à alimenter le registre d'instruction, à charger des instructions dedans. Et pour cela deux solutions : soit on charge l'instruction en plusieurs fois, soit d'un seul coup. La solution la plus simple charge les instructions par mots de 8, 16, 32, 64 bits. Ils sont accumulés dans le registre d'instruction jusqu'à détecter une instruction complète. La technique marche nettement mieux si les instructions sont très longues. Par exemple, les CPU x86 ont des instructions qui vont de 1 à 15 octets, ce qui fait qu'ils utilisent cette technique. Précisément, elle marche si les instructions les plus longues sont plus grandes que le bus mémoire. Une autre solution charge un bloc de mots mémoire aussi grand que la plus grande instruction. Par exemple, si le processeur gère des instructions allant de 1 à 4 octets, il charge 4 octets d'un seul coup dans le registre d'instruction. Il va de soit que cette solution ne marche que si les instructions du processeur sont assez courtes, au plus aussi courtes que le bus mémoire. Ainsi, on est sûr de charger obligatoirement au moins une instruction complète, et peut-être même plusieurs. Le contenu du registre d'instruction est alors découpé en instructions au fil de l'eau. Utiliser un registre d'instruction large pose problème avec des instructions à cheval entre deux blocs. Il se peut qu'on n'ait pas une instruction complète lorsque l'on arrive à la fin du bloc, mais seulement un morceau. Le problème se manifeste peu importe que l'instruction soit chargée d'un seul coup ou bien en plusieurs fois. [[File:Instructions non alignées.png|centre|vignette|upright=2|Instructions non alignées.]] Dans ce cas, on doit décaler le morceau de bloc pour le mettre au bon endroit (au début du registre d'instruction), et charger le prochain bloc juste à côté. On a donc besoin d'un circuit qui décale tous les bits du registre d'instruction, couplé à un circuit qui décide où placer dans le registre d'instruction ce qui a été chargé, avec quelques circuits autour pour configurer les deux circuits précédents. [[File:Décaleur d’instruction.png|centre|vignette|upright=2|Décaleur d’instruction.]] Une autre solution se passe de registre d'instruction en utilisant à la place l'état interne du séquenceur. Là encore, elle charge l'instruction morceau par morceau, typiquement par blocs de 8, 16 ou 32 bits. Les morceaux ne sont pas accumulés dans le registre d'instruction, la solution utilise à la place l'état interne du séquenceur. Les mots mémoire de l'instruction sont chargés à chaque cycle, faisant passer le séquenceur d'un état interne à un autre à chaque mot mémoire. Tant qu'il n'a pas reçu tous les mots mémoire de l'instruction, chaque mot mémoire non terminal le fera passer d'un état interne à un autre, chaque état interne encodant les mots mémoire chargés auparavant. S'il tombe sur le dernier mot mémoire d'une instruction, il rentre dans un état de décodage. ===Le chargement avec les jeux d'instruction compacts=== Pour finir, il faut parler du cas particulier des jeux d'instruction compacts. Pour rappel, ces jeux d'instruction sont présents sur certains CPU RISC 32 bits. De tels processeurs gèrent un jeu d'instruction 32 bits normal, qui encode ses instructions sur 32 bits, et un jeu d'instruction plus compact qui encode ses instructions sur 16 bits. Nous allons prendre l'exemple du jeu d'instruction emblématique de cette catégorie : l'ARM ''thumb'', qui est le jeu d'instruction compact associé au jeu d'instruction ARM 32 bits. Les deux jeux d'instruction ont un mode d'exécution chacun, ce qui veut dire que le processeur est configuré pour fonctionner en mode ''thumb'' ou en mode ARM 32 bits. En clair, on ne peut pas mélanger les deux jeu d'instructions en même temps. La raison est qu'une même suite de bit correspondait soit à une instruction ARM normale, soit à deux instructions thumbs. Pour faire la différence, il faut préciser dans quel mode d'exécution le CPU est : mode ARM ou mode thumb. Le processeur interpréte les instructions correctement en tenant compte de ce mode. Intuitivement, on se dirait qu'il y a deux décodeurs séparés : un utilisé en mode ARM, un autre utilisé en mode ''thumb''. Sauf que ce n'est pas la solution retenue. A la place, les ingénieurs d'ARM profitent d'une propriété du ''thumb'' : ''thumb'' est un sous-ensemble du jeu d’instruction 32 bits. Par exemple, une instruction ''thumb'' a un équivalent en RAM 32 bits, bien que l'encodage entre les deux est différent. Et cette propriété est utilisée pour faciliter l'implémentation au niveau du décodeur. Le processeur contient simplement un circuit qui convertit les instructions ''thumb'' en leur équivalent ARM 32 bits. Le circuit de conversion se situe en aval de l'unité de chargement, juste avant le décodeur d'instruction. Le circuit est désactivé quand le processeur fonctionne en mode ARM, mais est active en mode ''thumb''. ===Les optimisations du code automodifiant=== Précédemment dans ce cours, nous avons parlé du code automodifiant, à savoir le fait qu'un programme modifie ses propres instructions à la volée. Il était utilisé pour avoir des tableaux, sur des processeurs qui ne supportaient que le mode d'adressage direct. L'idée était que les instructions d'accès mémoire incorporaient une adresse, qui était incrémentée/décrémentée par code auto-modifiant. Les branchements indirects étaient eux aussi gérés de la même manière : l'adresse de destination, incorporée dans l'instruction via adressage absolu, était changée via code automodifiant. Quelques rares processeurs ont incorporé des optimisations pour simplifier l'usage du code automodifiant. Elles agissaient toutes sur le registre d'instruction, en permettant de modifier son contenu. Les instructions étaient chargées dans le registre d'instruction et étaient modifiées dans ce registre, avant d'être décodées. La modification était généralement assez simple : appliquer un masque, additionner une constante, guère plus. Les processeurs de ce type sont très rare, j'en connais deux exemples : le Burroughs B1700 et l'Apollo Guidance Computer. Le Burroughs B1700 permettait d'appliquer un masque sur l'instruction lue. Pour cela, il permettait de faire un OU logique entre l'instruction lue et un registre du processeur, le résultat étant mémorisé dans le registre d'instruction. Cela permettait par exemple de remplacer une adresse dans une micro-opération. L'adresse est alors surimposée avec un OU dans le champ d'adresse, alors que le reste de l'instruction n'est pas modifié. Un autre exemple est celui de l'Apollo Guidance Computer, qui permettait d'altérer le registre d'instruction avec une instruction dédiée, l'instruction ''Index next instruction'', que nous appellerons INI. Nous avons déjà vu cette instruction INI, dans le chapitre sur les modes d'adressage, mais nous pouvons maintenant expliquer comment elle est implémentée. Elle additionne une constante à l'instruction suivante. Elle était utilisée pour ajouter un décalage à une adresse, dans une instruction LOAD/STORE, ou un branchement direct. L'addition était vraisemblablement réalisée par l'ALU du processeur. La conséquence est que le registre d'instruction était un registre architectural, qui était adressé implicitement, avec un mode d'adressage implicite. ==L'incrémentation du ''program counter''== À chaque chargement, le ''program counter'' est mis à jour afin de pointer sur la prochaine instruction à charger. Sur la quasi-totalité des processeurs, les instructions sont placées dans l'ordre d’exécution dans la RAM. En faisant ainsi, on peut mettre à jour le ''program counter'' en lui ajoutant la longueur de l'instruction courante. Déterminer la longueur de l'instruction est simple quand les instructions ont toutes la même taille, mais certains processeurs ont des instructions de taille variable, ce qui complique le calcul. Dans ce qui va suivre, nous allons supposer que les instructions sont de taille fixe, ce qui fait que le ''program counter'' est toujours incrémenté de la même valeur. Il existe deux méthodes principales pour incrémenter le ''program counter'' : soit le ''program counter'' a son propre additionneur, soit le ''program counter'' est mis à jour par l'ALU. Pour ce qui est de l'organisation des registres, soit le ''program counter'' est un registre séparé des autres, soit il est regroupé avec d'autres registres dans un banc de registre. En tout, cela donne quatre organisations possibles. * Un ''program counter'' séparé relié à un incrémenteur séparé. * Un ''program counter'' séparé des autres registres, incrémenté par l'ALU. * Un ''program counter'' intégré au banc de registre, relié à un incrémenteur séparé. * Un ''program counter'' intégré au banc de registre, incrémenté par l'ALU. [[File:Calcul du program counter.png|centre|vignette|upright=2|les différentes méthodes de calcul du program counter.]] Les processeurs haute performance modernes utilisent tous la première méthode. Les autres méthodes étaient surtout utilisées sur les processeurs 8 ou 16 bits, elles sont encore utilisées sur quelques microcontrôleurs de faible puissance. Nous auront l'occasion de donner des exemples quand nous parlerons des processeurs 8 bits anciens, dans le chapitre sur les architectures à accumulateur et affiliées. ===Le ''program counter'' mis à jour par l'ALU=== Sur certains processeurs, le calcul de l'adresse de la prochaine instruction est effectué par l'ALU. L'avantage de cette méthode est qu'elle économise un incrémenteur dédié au ''program counter''. Ce qui explique que cette méthode était utilisée sur les processeurs assez anciens, à une époque où un additionneur pouvait facilement prendre 10 à 20% des transistors disponibles. Le désavantage principal est une augmentation de la complexité du séquenceur, qui doit gérer la mise à jour du ''program counter''. De plus, la mise à jour du ''program counter'' ne peut pas se faire en même temps qu'une opération arithmétique, ce qui réduit les performances. Si on incrémente le ''program counter'' avec l'ALU, il est intéressant de le placer dans le banc de registres. Un désavantage est qu'on perd un registre. Par exemple, avec un banc de registre de 16 registres, on ne peut adresser que 15 registres généraux. De plus ce n'est pas l'idéal pour le décodage des instructions et pour le séquenceur. Si le processeur dispose de plusieurs bancs de registres, le ''program counter'' est généralement placé dans le banc de registre dédié aux adresses. Sinon, il est placé avec les nombres entiers/adresses. D'anciens processeurs incrémentaient le ''program counter'' avec l'ALU, mais utilisaient bien un registre séparé pour le ''program counter''. Cette méthode est relativement simple à implémenter : il suffit de connecter/déconnecter le ''program counter'' du bus interne suivant les besoins. Le ''program counter'' est déconnecté pendant l’exécution d'une instruction, il est connecté au bus interne pour l'incrémenter. C'est le séquenceur qui gère le tout. L'avantage de cette séparation est qu'elle est plus facile à gérer pour le séquenceur. De plus, on gagne un registre général/entier dans le banc de registre. ===Le compteur programme=== Dans la quasi-totalité des processeurs modernes, le ''program counter'' est un vulgaire compteur comme on en a vu dans les chapitres sur les circuits, ce qui fait qu'il passe automatiquement d'une adresse à la suivante. Dans ce qui suit, nous allons appeler ce registre compteur : le '''compteur ordinal'''. L'usage d'un compteur simplifie fortement la conception du séquenceur. Le séquenceur n'a pas à gérer la mise à jour du ''program counter'', sauf en cas de branchements. De plus, il est possible d'incrémenter le ''program counter'' pendant que l'unité de calcul effectue une opération arithmétique, ce qui améliore grandement les performances. Le seul désavantage est qu'elle demande d'ajouter un incrémenteur dédié au ''program counter''. Sur les processeurs très anciens, les instructions faisaient toutes un seul mot mémoire et le compteur ordinal était un circuit incrémenteur des plus basique. Sur les processeurs modernes, le compteur est incrémenté par pas de 4 si les instructions sont codées sur 4 octets, 8 si les instructions sont codées sur 8 octets, etc. Concrètement, le compteur est un circuit incrémenteur auquel on aurait retiré quelques bits de poids faible. Par exemple, si les instructions sont codées sur 4 octets, on coupe les 2 derniers bits du compteur. Si les instructions sont codées sur 8 octets, on coupe les 3 derniers bits du compteur. Et ainsi de suite. Une amélioration de la solution précédente utilise un circuit incrémenteur partagé entre le ''program counter'' et d'autres registres. L'incrémenteur est utilisé pour incrémenter le ''program counter'', mais aussi pour effectuer d'autres calculs d'adresse. Par exemple, sur les architectures avec une pile d'adresse de retour, il est possible de partager l'incrémenteur/décrémenteur avec le pointeur de pile ( la technique ne marche pas avec une pile d'appel). Un autre exemple est celui des processeurs qui gèrent automatiquement le rafraichissement mémoire, grâce à, un compteur intégré dans le processeur. Il est possible de partager l'incrémenteur du ''program counter'' avec le compteur de rafraichissement mémoire, qui mémorise la prochaine adresse mémoire à rafraichir. Nous avions déjà abordé ce genre de partage dans le chapitre sur le chemin de données, dans l'annexe sur le pointeur de pile, mais nous verrons d'autres exemples dans le chapitre sur les architectures hybrides accumulateur-registre 8/16 bits. Il a existé quelques processeurs pour lesquelles le ''program counter'' était non pas un compteur binaire classique, mais un ''linear feedback shift register''. L'avantage est que le circuit d'incrémentation utilisait bien moins de portes logiques, l'économie était substantielle. Mais un défaut est que des instructions consécutives dans le programme n'étaient pas consécutives en mémoire. Il existait cependant une table de correspondance pour dire : la première instruction est à telle adresse, la seconde à telle autre, etc. Un exemple de processeur de ce type est le TMS 1000 de Texas Instrument, un des premiers microcontrôleur 4 bits datant des années 70. ===Quand est mis à jour le ''program counter'' ?=== Le ''program counter'' est mis à jour quand il faut charger une nouvelle instruction. Reste qu'il faut savoir quand le mettre à jour. Incrémenter le ''program counter'' à intervalle régulier, par exemple à chaque cycle d’horloge, fonctionne sur les processeurs où toutes les instructions mettent le même temps pour s’exécuter. Mais de tels processeurs sont très rares. Sur la quasi-totalité des processeurs, les instructions ont une durée d’exécution variable, elles ne prennent pas le même nombre de cycle d'horloge pour s’exécuter. Le temps d’exécution d'une instruction varie selon l'instruction, certaines sont plus longues que d'autres. Tout cela amène un problème : comment incrémenter le ''program counter'' avec des instructions avec des temps d’exécution variables ? La réponse est que la mise à jour du ''program counter'' démarre quand l'instruction précédente a terminée de s’exécuter, plus précisément un cycle avant. C'est un cycle avant pour que l'instruction chargée soit disponible au prochain cycle pour le décodeur. Pour cela, le séquenceur doit déterminer quand l'instruction est terminée et prévenir le ''program counter'' au bon moment. Il faut donc une interaction entre séquenceur et circuit de chargement. Le circuit de chargement contient une entrée Enable, qui autorise la mise à jour du ''program counter''. Le séquenceur met à 1 cette entrée pour prévenir au ''program counter'' qu'il doit être incrémenté au cycle suivant ou lors du cycle courant. Cela permet de gérer simplement le cas des instructions multicycles. [[File:Commande de la mise à jour du program counter.png|centre|vignette|upright=2|Commande de la mise à jour du program counter.]] Toute la difficulté est alors reportée dans le séquenceur, qui doit déterminer la durée d'une instruction. Pour gérer la mise à jour du ''program counter'', le séquenceur doit déterminer la durée d'une instruction. Cela est simple pour les instructions arithmétiques et logiques, qui ont un nombre de cycles fixe, toujours le même. Une fois l'instruction identifiée par le séquenceur, il connait son nombre de cycles et peut programmer un compteur interne au séquenceur, qui compte le nombre de cycles de l'instruction, et fournit le signal Enable au ''program counter''. Mais cette technique ne marche pas vraiment pour les accès mémoire, dont la durée n'est pas connue à l'avance, surtout sur les processeurs avec des mémoires caches. Pour cela, le séquenceur détermine quand l'instruction mémoire est finie et prévient le ''program counter'' quand c'est le cas. Il existe quelques processeurs pour lesquels le temps de calcul dépend des opérandes de l'instruction. On peut par exemple avoir une division qui prend 10 cycles avec certaines opérandes, mais 40 cycles avec d'autres opérandes. Mais ces processeurs sont rares et cela est surtout valable pour les opérations de multiplication/division, guère plus. Le problème est alors le même qu'avec les accès mémoire et la solution est la même : l'ALU prévient le séquenceur quand le résultat est disponible. ==Les branchements et le ''program counter''== Un branchement consiste juste à écrire l'adresse de destination dans le ''program counter''. L'implémentation des branchements demande d'extraire l'adresse de destination et d'altérer le ''program counter'' quand un branchement est détecté. Pour cela, le décodeur d'instruction détecte si l'instruction exécutée est un branchement ou non, et il en déduit où trouver l'adresse de destination. L'adresse de destination se trouve à un endroit qui dépend du mode d'adressage du branchement. Pour rappel, on distingue quatre modes d'adressage pour les branchements : direct, indirect, relatif et implicite. Pour les branchements directs, l'adresse est intégrée dans l'instruction de branchement elle-même et est extraite par le séquenceur. Pour les branchements indirects, l'adresse de destination est dans le banc de registre. Pour les branchements relatifs, il faut ajouter un décalage au ''program counter'', décalage extrait de l'instruction par le séquenceur. Enfin, les instructions de retour de fonction lisent l'adresse de destination depuis la pile. L'implémentation de ces quatre types de branchements est très différente. L'altération du ''program counter'' dépend de si le ''program counter'' est dans un compteur séparé ou s'il est dans le banc de registre. Nous avons vu plus haut qu'il y a quatre cas différents, mais nous n'allons voir que deux cas : celui où le ''program counter'' est dans le banc de registre et est incrémenté par l'unité de calcul, celui avec un ''program counter'' séparé avec son propre incrémenteur. Les deux autres cas ne sont utilisés que sur des architectures à accumulateur ou à pile spécifiques, que nous n'avons pas encore vu à ce stade du cours et que nous verrons en temps voulu dans le chapitre sur ces architectures. ===L’implémentation des branchements avec un ''program counter'' intégré au banc de registres=== Le cas le plus simple est celui où le ''program counter'' est intégré au banc de registre, avec une incrémentation faite par l'unité de calcul. Charger une instruction revient alors à effectuer une instruction LOAD en adressage indirect, à deux différences près : le registre sélectionné est le ''program counter'', l’instruction est copiée dans le registre d'instruction et non dans le banc de registre. L'incrémentation du ''porgram counter'' est implémenté avec des micro-opérations qui agissent sur le chemin de données, au même titre que les branchements indirects, relatifs et directs. * Les branchements indirects copient un registre dans le ''program counter'', ce qui revient simplement à faire une opération MOV entre deux registres, dont le ''program counter'' est la destination. * L'opération inverse d'un branchement indirect, qui copie le ''program counter'' dans un registre général, est utile pour sauvegarder l'adresse de retour d'une fonction. * Les branchements directs copient une adresse dans le ''program counter'', ce qui les rend équivalents à une opération MOV avec une constante immédiate, dont le ''program counter'' est la destination. * Les branchements relatifs sont une opération arithmétique entre le ''program counter'' et un opérande en adressage immédiat. En clair, à partir du moment où le chemin de données supporte les instructions MOV et l'addition, avec les modes d'adressage adéquat, il supporte les branchements. Aucune modification du chemin de données n'est nécessaire, le séquenceur gére le chargement d'une instruction avec les micro-instructions adéquates : une micro-opération ADD pour incrémenter le ''program counter'', une micro-opération LOAD pour le chargement de l'instruction. Notons que sur certaines architectures, le ''program counter'' est adressable au même titre que les autres registres du banc de registre. Les instructions de branchement sont alors remplacées par des instructions MOV ou des instructions arithmétique équivalentes, comme décrit plus haut. ===L’implémentation des branchements avec un ''program counter'' séparé=== Étudions maintenant le cas où le ''program counter'' est dans un registre/compteur séparé. Rappelons que tout compteur digne de ce nom possède une entrée de réinitialisation, qui remet le compteur à zéro. De plus, on peut préciser à quelle valeur il doit être réinitialisé. Ici, la valeur à laquelle on veut réinitialiser le compteur n'est autre que l'adresse de destination du branchement. Implémenter un branchement est donc simple : l'entrée de réinitialisation est commandée par le circuit de détection des branchements, alors que la valeur de réinitialisation est envoyée sur l'entrée adéquate. En clair, nous partons du principe que le ''program counter'' est implémenté avec ce type de compteur : [[File:Fonctionnement d'un compteur (décompteur), schématique.jpg|centre|vignette|upright=1.5|Fonctionnement d'un compteur (décompteur), schématique]] Toute la difficulté est de présenter l'adresse de destination au ''program counter''. Pour les branchements directs, l'adresse de destination est fournie par le séquenceur. L'adresse de destination du branchement sort du séquenceur et est présentée au ''program counter'' sur l'entrée adéquate. Voici donc comment sont implémentés les branchements directs avec un ''program counter'' séparé du banc de registres. Le schéma ci-dessous marche peu importe que le ''program counter'' soit incrémenté par l'ALU ou par un additionneur dédié. [[File:Unité de détection des branchements dans le décodeur.png|centre|vignette|upright=2|Unité de détection des branchements dans le décodeur]] Les branchements relatifs sont ceux qui font sauter X instructions plus loin dans le programme. Leur implémentation demande d'ajouter une constante au ''program counter'', la constante étant fournie dans l’instruction. Là encore, deux solutions sont possibles : réutiliser l'ALU pour calculer l'adresse, ou utiliser un additionneur séparé. L'additionneur séparé peut être fusionné avec l'additionneur qui incrémente le ''program counter'' pour passer à l’instruction suivante. [[File:Unité de chargement qui gère les branchements relatifs.png|centre|vignette|upright=2|Unité de chargement qui gère les branchements relatifs.]] Pour les branchements indirects, il suffit de lire le registre voulu et d'envoyer le tout sur l'entrée adéquate du ''program counter''. Il faut alors rajouter un multiplexeur pour que l'entrée de réinitialisationr ecoive la bonne adresse de destination. [[File:Unité de chargement qui gère les branchements directs.png|centre|vignette|upright=2|Unité de chargement qui gère les branchements directs et indirects.]] Toute la difficulté de l'implémentation des branchements est de configurer les multiplexeurs, ce qui est réalisé par le séquenceur en fonction du mode d'adressage du branchement. ==Les optimisations du chargement des instructions== Charger une instruction est techniquement une forme d'accès mémoire un peu particulier. En clair, charger une instruction prend au minimum un cycle d'horloge, et cela peut rapidement monter à 3-5 cycles, si ce n'est plus. L'exécution des instructions est alors fortement ralentit par la mémoire. Par exemple, imaginez que la mémoire mette 3 cycles d'horloges pour charger une instruction, alors qu'une instruction s’exécute en 1 cycle (si les opérandes sont dans les registres). La perte de performance liée au chargement des instructions est alors substantielle. Heureusement, il est possible de limiter la casse en utilisant des mémoires caches, ainsi que d'autres optimisations, que nous allons voir dans ce qui suit. ===Le tampon de préchargement=== La technique dite du '''préchargement''' est utilisée dans le cas où la mémoire a un temps d'accès important. Mais si la latence de la RAM est un problème, le débit ne l'est pas. Il est possible d'avoir une RAM lente, mais à fort débit. Par exemple, supposons que la mémoire puisse charger 4 instructions (de taille fixe) en 3 cycles. Le processeur peut alors charger 4 instructions en un seul accès mémoire, et les exécuter l'une après l'autre, une par cycle d'horloge. Les temps d'attente sont éliminés : le processeur peut décoder une nouvelle instruction à chaque cycle. Et quand la dernière instruction préchargée est exécutée, la mémoire est de nouveau libre, ce qui masque la latence des accès mémoire. [[File:Tampon de préchargement d'instruction.png|vignette|Tampon de préchargement d'instruction]] La seule contrainte est de mettre les instructions préchargées en attente. La solution pour cela est d'utiliser un registre d'instruction très large, capable de mémoriser plusieurs instructions à la fois. Ce registre est de plus connecté à un multiplexeur qui permet de sélectionner l'instruction adéquate dans ce registre. Ce multiplexeur est commandé par le ''program counter'' et quelques circuits annexes. Ce super-registre d'instruction est appelé un '''tampon de préchargement'''. La méthode a une implémentation nettement plus simple avec des instructions de taille fixe, alignées en mémoire. La commande du multiplexeur de sélection de l'instruction est alors beaucoup plus simple : il suffit d'utiliser les bits de poids fort du ''program counter''. Par exemple, prenons le cas d'un registre d'instruction de 32 octets pour des instructions de 4 octets, soit 8 instructions. Le choix de l'instruction à sélectionner se fait en utilisant les 3 bits de poids faible du ''program counter''. Mais cette méthode ajoute de nouvelles contraintes d'alignement, similaires à celles vues dans le chapitre sur l'alignement et le boutisme, sauf que l'alignement porte ici sur des blocs d'instructions de même taille que le tampon de préchargement. Si on prend l'exemple d'un tampon de préchargement de 128 bits, les instructions devront être alignées par blocs de 128 bits. C'est à dire qu'idéalement, les fonctions et autres blocs de code isolés doivent commencer à des adresses multiples de 128, pour pouvoir charger un bloc d'instruction en une seule fois. Sans cela, les performances seront sous-optimales. : Il arrive que le tampon de préchargement ait la même taille qu'une ligne de cache. Lorsque l'on passe d'un bloc d'instruction à un autre, le tampon de préchargement est mis à jour. Par exemple, si on prend un tampon de préchargement de 4 instructions, on doit changer son contenu toutes les 4 instructions. La seule exception est l'exécution d'un branchement. En effet, lors d'un branchement, la destination du branchement n'est pas dans le tampon de préchargement et elle doit être chargée dedans (sauf si le branchement pointe vers une instruction très proche, ce qui est improbable). Pour cela, le tampon de préchargement est mis à jour précocement quand le processeur détecte un branchement. Le même problème survient avec du code auto-modifiant. Un code auto-modifiant est un programme qui se modifie lui-même, en remplaçant certaines instructions par d'autres, en en retirant, en en ajoutant, lors de sa propre exécution. De tels programmes sont rares, mais la technique était utilisée dans quelques cas au tout début de l'informatique sur des ordinateurs rudimentaires. Ceux-ci avaient des modes d'adressages tellement limités que gérer des tableaux de taille variable demandait d'utiliser du code auto-modifiant pour écrire des boucles. Le problème survient quand des instructions sont modifiées immédiatement après avoir été préchargées. Les instructions dans le tampon de préchargement sont l'ancienne version, alors que la mémoire RAM contient les instructions modifiées. Gérer ce genre de cas demande de vider le tampon de préchargement si le cas arrive, mais ça ne vaut que très rarement le coup, aussi les ingénieurs ne s’embêtent pas à mettre ce correctif en place. Le code automodifiant est alors buggé. ===La ''prefetch input queue''=== La ''prefetch input queue'' est une sorte de cousine du tampon de préchargement, qui a été utilisée sur les processeurs Intel 8 et 16 bits, ainsi que sur le 286 et le 386. L'idée est là encore de précharger des instructions en avance. Sauf que cette fois-ci, on ne charge pas plusieurs instructions à la fois. Au contraire, les instructions sont préchargées séquentiellement, un ou deux octet à la fois. En conséquence, le tampon de préchargement est remplacé par une mémoire FIFO appelée la '''''Prefetch Input Queue''''', terme abrévié en PIQ. Les instructions sont accumulées une par une dans la PIQ, elles en sortent une par une au fur et à mesure pour alimenter le décodeur d'instruction. Pendant que le processeur exécute une instruction, on précharge la suivante. Sans ''prefetch input queue'', le processeur chargeait une instruction, la décodait et l'exécutait, puis chargeait la suivante, et ainsi de suite. Avec un tampon de préchargement, le processeur chargeait plusieurs instruction en une seule fois, puis les exécutait les unes après les autres. Avec la ''prefetch input queue'', pendant qu'une instruction est en cours de décodage/exécution, le processeur précharge l'instruction suivante. Si une instruction prend vraiment beaucoup de temps pour s'exécuter, le processeur peut en profiter pour précharger l'instruction encore suivante, et ainsi de suite, jusqu'à une limite de 4/6 instructions (la limite dépend du processeur). La ''prefetch input queue'' permet de précharger l'instruction suivante à condition qu'aucun autre accès mémoire n'ait lieu. Si l'instruction en cours d'exécution effectue des accès mémoire, ceux-ci sont prioritaires sur tout le reste, le préchargement de l'instruction suivante est alors mis en pause ou inhibé. Par contre, si la mémoire n'est pas utilisée par l'instruction en cours d'exécution, le processeur lit l'instruction suivante en RAM et la copie dans la ''prefetch input queue''. En conséquence, il arrive que la ''prefetch input queue'' n'ait pas préchargé l'instruction suivante, alors que le décodeur d'instruction est prêt pour la décoder. Dans ce cas, le décodeur doit attendre que l'instruction soit chargée. Les instructions préchargées dans la ''Prefetch input queue'' y attendent que le processeur les lise et les décode. Les instructions préchargées sont conservées dans leur ordre d'arrivée, afin qu'elles soient exécutées dans le bon ordre, ce qui fait que la ''Prefetch input queue'' est une mémoire de type FIFO. L'étape de décodage pioche l'instruction à décoder dans la ''Prefetch input queue'', cette instruction étant par définition la plus ancienne, puis la retire de la file. Le premier processeur commercial grand public à utiliser une ''prefetch input queue'' était le 8086. Il pouvait précharger à l'avance 6 octets d’instruction. L'Intel 8088 avait lui aussi une ''Prefetch input queue'', mais qui ne permettait que de précharger 4 instructions. Le 386 avait une ''prefetch input queue'' de 16 octets. La taille idéale de la FIFO dépend de nombreux paramètres. On pourrait croire que plus elle peut contenir d'instructions, mieux c'est, mais ce n'est pas le cas, pour des raisons que nous allons expliquer dans quelques paragraphes. [[File:80186 arch.png|centre|vignette|700px|upright=2|Architecture du 8086, du 80186 et de ses variantes.]] Vous remarquerez que j'ai exprimé la capacité de la ''prefetch input queue'' en octets et non en instructions. La raison est que sur les processeurs x86, les instructions sont de taille variable, avec une taille qui varie entre un octet et 6 octets. Cependant, le décodage des instructions se fait un octet à la fois : le décodeur lit un octet, puis éventuellement le suivant, et ainsi de suite jusqu'à atteindre la fin de l'instruction. En clair, le décodeur lit les instructions longues octet par octet dans la ''Prefetch input queue''. Les instructions varient entre 1 et 6 octets, mais tous ne sont pas utiles au décodeur d'instruction. Par exemple, le décodeur d'instruction n'a pas besoin d'analyser les constantes immédiates intégrées dans une instruction, ni les adresses mémoires en adressage absolu. Il n'a besoin que de deux octets : l'opcode et l'octet Mod/RM qui précise le mode d'adressage. Le second est facultatif. En clair, le décodeur a besoin de lire deux octets maximum depuis la ''prefetch input queue'', avant de passer à l’instruction suivante. Les autres octets étaient envoyés ailleurs, typiquement dans le chemin de données. Par exemple, prenons le cas d'une addition entre le registre AX et une constante immédiate. L'instruction fait trois octets : un opcode suivie par une constante immédiate codée sur deux octets. L'opcode était envoyé au décodeur, mais pas la constante immédiate. Elle était lue octet par octet et mémorisée dans un registre temporaire placé en entrée de l'ALU. Idem avec les adresses immédiates, qui étaient envoyées dans un registre d’interfaçage mémoire sans passer par le décodeur d'instruction. Pour cela, la ''prefetch input queue'' était connectée au bus interne du processeur ! Le décodeur dispose d'une micro-opération pour lire un octet depuis la ''prefetch input queue'' et le copier ailleurs dans le chemin de données. Par exemple, l'instruction d'addition entre le registre AX et une constante immédiate était composée de quatre micro-opérations : une qui lisait le premier octet de la constante immédiate, une seconde pour le second, une troisième micro-opération qui commande l'ALU et fait le calcul. Le décodeur devait attendre que qu'un moins un octet soit disponible dans la ''Prefetch input queue'', pour le lire. Il lisait alors cet octet et déterminait s'il contenait une instruction complète ou non. Si c'est une instruction complète, il la décodait et l''exécutait, puis passait à l'instruction suivante. Sinon, il lit un second octet depuis la ''Prefetch input queue'' et relance le décodage. Là encore, le décodeur vérifie s'il a une instruction complète, et lit un troisième octet si besoin, puis rebelote avec un quatrième octet lu, etc. Un circuit appelé le ''loader'' synchronisait le décodeur d'instruction et la ''Prefetch input queue''. Il fournissait deux bits : un premier bit pour indiquer que le premier octet d'une instruction était disponible dans la ''Prefetch input queue'', un second bit pour le second octet. Le ''loader'' recevait aussi des signaux de la part du décodeur d'instruction. Le signal ''Run Next Instruction'' entrainait la lecture d'une nouvelle instruction, d'un premier octet, qui était alors dépilé de la ''Prefetch input queue''. Le décodeur d'instruction pouvait aussi envoyer un signal ''Next-to-last (NXT)'', un cycle avant que l'instruction en cours ne termine. Le ''loader'' réagissait en préchargeant l'octet suivant. En clair, ce signal permettait de précharger l'instruction suivante avec un cycle d'avance, si celle-ci n'était pas déjà dans la ''Prefetch input queue''. Le bus de données du 8086 faisait 16 bits, alors que celui du 8088 ne permettait que de lire un octet à la fois. Et cela avait un impact sur la ''prefetch input queue''. La ''prefetch input queue'' du 8088 était composée de 4 registres de 8 bits, avec un port d'écriture de 8 bits pour précharger les instructions octet par octet, et un port de lecture de 8 bits pour alimenter le décodeur. En clair, tout faisait 8 bits. Le 8086, lui utilisait des registres de 16 bits et un port d'écriture de 16 bits. le port de lecture restait de 8 bits, grâce à un multiplexeur sélectionnait l'octet adéquat dans un registre 16 bits. Sa ''prefetch input queue'' préchargeait les instructions par paquets de 2 octets, non octet par octet, alors que le décodeur consommait les instructions octet par octet. Il s’agissait donc d'une sorte d'intermédiaire entre ''prefetch input queue'' à la 8088 et tampon de préchargement. Le 386 était dans un cas à part. C'était un processeur 32 bits, sa ''prefetch input queue'' contenait 4 registres de 32 bits, un port d'écriture de 32 bits. Mais il ne lisait pas les instructions octet par cotet. A la place, son décodeur d'instruction avait une entrée de 32 bits. Cependant, il gérait des instructions de 8 et 16 bits. Il fallait alors dépiler des instructions de 8, 16 et 32 bits dans la ''prefetch input queue''. De plus, les instructions préchargées n'étaient pas parfaitement alignées sur 32 bits : une instruction pouvait être à cheval sur deux registres 32 bits. Le processeur incorporait donc des circuits d'alignement, semblables à ceux utilisés pour gérer des instructions de longueur variable avec un registre d'instruction. Les branchements posent des problèmes avec la ''prefetch input queue'' : à cause d'eux, on peut charger à l'avance des instructions qui sont zappées par un branchement et ne sont pas censées être exécutées. Si un branchement est chargé, toutes les instructions préchargées après sont potentiellement invalides. Si le branchement est non-pris, les instructions chargées sont valides, elles sont censées s’exécuter. Mais si le branchement est pris, elles ont été chargées à tort et ne doivent pas s’exécuter. Pour éviter cela, la ''prefetch input queue'' est vidée quand le processeur détecte un branchement. Pour cela, le décodeur d'instruction dispose d'une micro-opération qui vide la ''Prefetch input queue'', elle invalide les instructions préchargées. Cette détection ne peut se faire qu'une fois le branchement décodé, qu'une fois que l'on sait que l'instruction décodée est un branchement. En clair, le décodeur invalide la ''Prefetch input queue'' quand il détecte un branchement. Les interruptions posent le même genre de problèmes. Il faut impérativement vider la ''Prefetch input queue'' quand une interruption survient, avant de la traiter. Il faut noter qu'une ''Prefetch input queue'' interagit assez mal avec le ''program counter''. En effet, la ''Prefetch input queue'' précharge les instructions séquentiellement. Pour savoir où elle en est rendue, elle mémorise l'adresse de la prochaine instruction à charger dans un '''registre de préchargement''' dédié. Il serait possible d'utiliser un ''program counter'' en plus du registre de préchargement, mais ce n'est pas la solution qui a été utilisée. Les processeurs Intel anciens ont préféré n'utiliser qu'un seul registre de préchargement en remplacement du ''program counter''. Après tout, le registre de préchargement n'est qu'un ''program counter'' ayant pris une avance de quelques cycles d'horloge, qui a été incrémenté en avance. Et cela ne pose pas de problèmes, sauf avec certains branchements. Par exemple, certains branchements relatifs demandent de connaitre la véritable valeur du ''program counter'', pas celle calculée en avance. Idem avec les instructions d'appel de fonction, qui demandent de sauvegarder l'adresse de retour exacte, donc le vrai ''program counter''. De telles situations demandent de connaitre la valeur réelle du ''program counter'', celle sans préchargement. Pour cela, le décodeur d'instruction dispose d'une instruction pour reconstituer le ''program counter'', à savoir corriger le ''program coutner'' et éliminer l'effet du préchargement. La micro-opération de correction se contente de soustraire le nombre d'octets préchargés au registre de préchargement. Le nombre d'octets préchargés est déduit à partir des deux pointeurs intégré à la FIFO, qui indiquent la position du premier et du dernier octet préchargé. Le bit qui indique si la FIFO est vide était aussi utilisé. Les deux pointeurs sont lus depuis la FIFO, et sont envoyés à un circuit qui détermine le nombre d'octets préchargés. Sur le 8086, ce circuit était implémenté non pas par un circuit combinatoire, mais par une mémoire ROM équivalente. La petite taille de la FIFO faisait que les pointeurs étaient très petits et la ROM l'était aussi. Un autre défaut est que la ''Prefetch input queue'' se marie assez mal avec du code auto-modifiant. Un code auto-modifiant est un programme qui se modifie lui-même, en remplaçant certaines instructions par d'autres, en en retirant, en en ajoutant, lors de sa propre exécution. De tels programmes sont rares, mais la technique était utilisée dans quelques cas au tout début de l'informatique sur des ordinateurs rudimentaires. Ceux-ci avaient des modes d'adressages tellement limités que gérer des tableaux de taille variable demandait d'utiliser du code auto-modifiant pour écrire des boucles. Le problème avec la ''Prefetch input queue'' survient quand des instructions sont modifiées immédiatement après avoir été préchargées. Les instructions dans la ''Prefetch input queue'' sont l'ancienne version, alors que la mémoire RAM contient les instructions modifiées. Gérer ce genre de cas est quelque peu complexe. Il faut en effet vider la ''Prefetch input queue'' si le cas arrive, ce qui demande d'identifier les écritures qui écrasent des instructions préchargées. C'est parfaitement possible, mais demande de transformer la ''Prefetch input queue'' en une mémoire hybride, à la fois mémoire associative et mémoire FIFO. Cela ne vaut que très rarement le coup, aussi les ingénieurs ne s’embêtent pas à mettre ce correctif en place, le code automodifiant est alors buggé. Le décodeur d'instruction dispose aussi d'une micro-opération qui stoppe le préchargement des instructions dans la ''Prefetch input queue''. : Pour ceux qui ont déjà lu un cours d'architecture des ordinateurs, la relation entre ''prefetch input queue'' et pipeline est quelque peu complexe. En apparence, la ''prefetch input queue'' permet une certaines forme de pipeline, en chargeant une instruction pendant que la précédente s'exécute. Les problématiques liées aux branchements ressemblent beaucoup à celles de la prédiction de branchement. Cependant, il est possible de dire qu'il s'agit plus d'une technique de préchargement que de pipeline. La différence entre les deux est assez subtile et les frontières sont assez floues, mais le fait est que l'instruction en cours d'exécution et le préchargement peuvent tout deux faire des accès mémoire et que l'instruction en cours a la priorité. Un vrai pipeline se débrouillerait avec une architecture Harvard, non avec un bus mémoire unique pour le chargement des instruction et la lecture/écriture des données. ===Le ''Zero-overhead looping''=== Nous avions vu dans le chapitre sur les instructions machines, qu'il existe des instructions qui permettent de grandement faciliter l'implémentation des boucles. Il s'agit de l'instruction REPEAT, utilisée sur certains processeurs de traitement de signal. Elle répète l'instruction suivante, voire une série d'instructions, un certain nombre de fois. Le nombre de répétitions est mémorisé dans un registre décrémenté à chaque itération de la boucle. Elle permet donc d'implémenter une boucle FOR facilement, sans recourir à des branchements ou des conditions. Une technique en lien avec cette instruction permet de grandement améliorer les performances et la consommation d'énergie. L'idée est de mémoriser la boucle, les instructions à répéter, dans une petite mémoire cache située entre l'unité de chargement et le séquenceur. Le cache en question s'appelle un '''tampon de boucle''', ou encore un ''hardware loop buffer''. Grâce à lui, il n'y a pas besoin de charger les instructions à chaque fois qu'on les exécute, on les charge une bonne fois pour toute : c'est plus rapide. Bien sûr, il ne fonctionne que pour de petites boucles, mais il en est de même avec l'instruction REPEAT. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le chemin de données | prevText=Le chemin de données | next=L'unité de contrôle | nextText=L'unité de contrôle }} </noinclude> nn5mptgpw5xxratsuo1f562chet9ktf 764069 764039 2026-04-19T22:29:00Z Mewtow 31375 /* Les optimisations du code automodifiant */ 764069 wikitext text/x-wiki L'unité de contrôle s'occupe du chargement des instructions et de leur interprétation, leur décodage. Elle contient deux circuits : l''''unité de chargement''' qui charge l'instruction depuis la mémoire, et le '''séquenceur''' qui commande le chemin de données. Les deux circuits sont séparés, mais ils communiquent entre eux, notamment pour gérer les branchements. Dans ce chapitre, nous allons nous intéresser au chargement d'une instruction depuis la RAM/ROM, nous verrons l'étape de décodage de l'instruction au prochain chapitre. L'unité de chargement a pour cœur le ''program counter'', le registre qui mémorise l'adresse de l'instruction à charger. Il s'agit précisément d'un compteur, vu que le contenu du registre est incrémenté à chaque fois qu'une instruction est chargée. Le ''program counter'' est remis à zéro lors du démarrage du processeur, mais est aussi modifié par les instructions de branchement qui écrivent une adresse dedans. Son interface est la suivante : une entrée d'horloge, une entrée de ''Reset'', une entrée ''Enable'' qui indique quand l'incrémenter (renommée ''Instruction Fetch''), et deux entrées pour les branchements. Pour les branchements, il y a une entrée pour l'adresse de destination, une autre pour autoriser l'écriture de celle-ci dans le ''program counter''. La sortie du ''program counter'' est reliée au bus d'adresse mémoire, plus ou moins directement. [[File:Program counter.jpg|centre|vignette|upright=2|Program counter]] ==Les interconnexions entre séquenceur et bus mémoire== Pour lire une instruction, le processeur envoie le ''program counter'' sur le bus d'adresse et récupère l'instruction sur le bus de données. L'instruction lue est alors envoyée au séquenceur. Et cela demande de connecter le séquenceur au bus mémoire, à la fois sur le bus d'adresse et le bus de données. Et sur ce point, la situation est différente selon que l'on parler d'une architecture Harvard ou Von Neumann. ===Les architectures Harvard : des bus mémoire séparés pour la RAM et la ROM=== Sur les architectures Harvard, le séquenceur et le chemin de donnée utilisent des interfaces mémoire séparées. Le séquenceur est directement relié au bus mémoire de la ROM, alors que le chemin de données est connecté à la RAM. [[File:Microarchitecture de l'interface mémoire d'une architecture Harvard.png|centre|vignette|upright=2|Microarchitecture de l'interface mémoire d'une architecture Harvard]] Le ''program counter'' est envoyé sur le bus d'adresse de la ROM, l'instruction est récupérée sur le bus de données de la ROM. Pour la mémoire RAM, elle échange des données avec le chemin de données, notamment avec les registres généraux. Les adresses utilisées pour la RAM viennent elles soit du chemin de données, soit de l'unité de contrôle, tout dépend du mode d'adressage. Mais le ''program counter'' n'est pas impliqué. [[File:Architecture Harvard - échanges de données.png|centre|vignette|upright=2|Architecture Harvard - échanges de données]] Les architectures Harvard modifiées doivent cependant rajouter une connexion entre le bus ROM et les registres généraux. C'est nécessaire pour charger une donnée constante depuis la mémoire ROM. Rappelons que la donnée constante est copiée dans un registre général, donc dans le chemin de données. [[File:Architecture Harvard modifiée - implémentation du processeur.png|centre|vignette|upright=2|Architecture Harvard modifiée - implémentation du processeur]] ===Les architectures Von-Neumann : un bus mémoire unique=== Sur les architectures Von-Neumann et affiliées, le séquenceur et le chemin de donnée partagent la même interface mémoire. Et cela pose deux problèmes. Le premier problème est que le bus mémoire doit être libéré une fois l'instruction chargée, pour un éventuel accès mémoire. Et le séquenceur doit conserver une copie de l'instruction chargée, sans quoi il ne peut pas décoder l'instruction correctement. Par exemple, si l'instruction met plusieurs cycles à s'exécuter, le séquenceur doit conserver une copie de l'instruction durant ces plusieurs cycles. Pour ça, le processeur intègre un '''registre d'instruction''' situé juste avant le séquenceur, qui mémorise l'instruction chargée. [[File:Registre d'instruction.png|centre|vignette|upright=2|Registre d'instruction.]] Le second problème est de gérer le flot des instructions/données entre le bus mémoire, le chemin de données et le séquenceur. Si le processeur lit une instruction, le bus doit être relié à l'unité de contrôle. Par contre, s'il accède à une donnée, il doit être relié au chemin de données. Le processeur doit connecter l'interface mémoire soit au séquenceur, soit au chemin de données et cela complique le réseau d'interconnexion interne au processeur. [[File:Architecture Von Neumann - implémentation du processeur.png|centre|vignette|upright=2|Architecture Von Neumann - implémentation du processeur]] Une première solution utilise un bus unique qui relie l'interface mémoire, le séquenceur et le chemin de données. Pour charger une instruction, le séquenceur copie le ''program counter'' sur le bus d'adresse, attend que l'instruction lue soit disponible sur le bus de données, puis la copie dans le registre d'instruction. Le bus mémoire est alors libre et peut être utilisé pour lire ou écrire des données, si le besoin s'en fait sentir. : Il faut noter que cette solution implique d'utiliser des registres d’interfaçage avec la mémoire. [[File:Microarchitecture de l'interface mémoire d'une architecture von neumann.png|centre|vignette|upright=2|Microarchitecture de l'interface mémoire d'une architecture von neumann]] Une autre solution utilise deux bus interne séparés : un connecté au bus d'adresse, l'autre au bus de données. Le ''program counter'' est alors connecté au bus interne d'adresse, le séquenceur est relié au bus interne de données. Notons que la technique marche bien si le ''program counter'' est dans le banc de registre : les interconnexions utilisées pour gérer l'adressage indirect permettent d'envoyer le ''program counter'' sur le bus d'adresse sans ajout de circuit. Le tout peut être amélioré en remplaçant les deux bus par des multiplexeurs et démultiplexeurs. Le bus d'adresse est précédé par un multiplexeur, qui permet de faire le choix entre ''Program Counter'', adresse venant du chemin de données, ou adresse provenant du séquenceur (adressage absolu). De même, le bus de données est suivi par un démultiplexeur qui envoie la donnée/instruction lue soit au registre d'instruction, soit au chemin de données. Le tout se marie très bien avec les chemins de donnée vu dans le chapitre précédent. Au passage, il faut noter que cette solution nécessite un banc de registre multi-port. [[File:Connexion du program counter sur les bus avec PC isolé.png|centre|vignette|upright=2|Connexion du program counter sur les bus avec PC isolé]] ==Le chargement d'une instruction== [[File:Étape de chargement.png|vignette|upright=1|Étape de chargement.]] L'étape de chargement (ou ''fetch'') doit faire deux choses : mettre à jour le ''program counter'' et lire l'instruction en mémoire RAM ou en ROM. Les deux étapes peuvent être faites en parallèle, dans des circuits séparés. Pendant que l'instruction est lue depuis la mémoire RAM/ROM, le ''program counter'' est incrémenté et/ou altéré par un branchement. Pour lire l'instruction, on envoie le ''program counter'' sur le bus d'adresse et on récupère l'instruction sur le bus de données pour l'envoyer sur l'entrée du séquenceur. ===Le chargement des instructions de longueur variable=== Le chargement des instructions de longueur variable pose de nombreux problèmes. Le premier est que mettre à jour le ''program counter'' demande de connaitre la longueur de l'instruction chargée, pour l'ajouter au ''program counter''. Si la longueur d'une instruction est variable, on doit connaitre cette longueur d'une manière où d'une autre. La solution la plus simple indique la longueur de l'instruction dans une partie de l'opcode ou de la représentation en binaire de l'instruction. Mais les processeurs haute performance de nos PC utilisent d'autres solutions, qui n'encodent pas explicitement la taille de l'instruction. Les processeurs récents utilisent un registre d'instruction de grande taille, capable de mémoriser l'instruction la plus longue du processeur. Par exemple, si un processeur supporte des instructions allant de 1 à 8 octets, comme les CPU x86, le registre d'instruction fera 8 octets. Le décodeur d'instruction vérifie à chaque cycle le contenu du registre d'instruction. Si une instruction est détectée dedans, son décodage commence, peu importe la taille de l'instruction. Une fois l'instruction exécutée, elle est retirée du registre d'instruction. Reste à alimenter le registre d'instruction, à charger des instructions dedans. Et pour cela deux solutions : soit on charge l'instruction en plusieurs fois, soit d'un seul coup. La solution la plus simple charge les instructions par mots de 8, 16, 32, 64 bits. Ils sont accumulés dans le registre d'instruction jusqu'à détecter une instruction complète. La technique marche nettement mieux si les instructions sont très longues. Par exemple, les CPU x86 ont des instructions qui vont de 1 à 15 octets, ce qui fait qu'ils utilisent cette technique. Précisément, elle marche si les instructions les plus longues sont plus grandes que le bus mémoire. Une autre solution charge un bloc de mots mémoire aussi grand que la plus grande instruction. Par exemple, si le processeur gère des instructions allant de 1 à 4 octets, il charge 4 octets d'un seul coup dans le registre d'instruction. Il va de soit que cette solution ne marche que si les instructions du processeur sont assez courtes, au plus aussi courtes que le bus mémoire. Ainsi, on est sûr de charger obligatoirement au moins une instruction complète, et peut-être même plusieurs. Le contenu du registre d'instruction est alors découpé en instructions au fil de l'eau. Utiliser un registre d'instruction large pose problème avec des instructions à cheval entre deux blocs. Il se peut qu'on n'ait pas une instruction complète lorsque l'on arrive à la fin du bloc, mais seulement un morceau. Le problème se manifeste peu importe que l'instruction soit chargée d'un seul coup ou bien en plusieurs fois. [[File:Instructions non alignées.png|centre|vignette|upright=2|Instructions non alignées.]] Dans ce cas, on doit décaler le morceau de bloc pour le mettre au bon endroit (au début du registre d'instruction), et charger le prochain bloc juste à côté. On a donc besoin d'un circuit qui décale tous les bits du registre d'instruction, couplé à un circuit qui décide où placer dans le registre d'instruction ce qui a été chargé, avec quelques circuits autour pour configurer les deux circuits précédents. [[File:Décaleur d’instruction.png|centre|vignette|upright=2|Décaleur d’instruction.]] Une autre solution se passe de registre d'instruction en utilisant à la place l'état interne du séquenceur. Là encore, elle charge l'instruction morceau par morceau, typiquement par blocs de 8, 16 ou 32 bits. Les morceaux ne sont pas accumulés dans le registre d'instruction, la solution utilise à la place l'état interne du séquenceur. Les mots mémoire de l'instruction sont chargés à chaque cycle, faisant passer le séquenceur d'un état interne à un autre à chaque mot mémoire. Tant qu'il n'a pas reçu tous les mots mémoire de l'instruction, chaque mot mémoire non terminal le fera passer d'un état interne à un autre, chaque état interne encodant les mots mémoire chargés auparavant. S'il tombe sur le dernier mot mémoire d'une instruction, il rentre dans un état de décodage. ===Le chargement avec les jeux d'instruction compacts=== Pour finir, il faut parler du cas particulier des jeux d'instruction compacts. Pour rappel, ces jeux d'instruction sont présents sur certains CPU RISC 32 bits. De tels processeurs gèrent un jeu d'instruction 32 bits normal, qui encode ses instructions sur 32 bits, et un jeu d'instruction plus compact qui encode ses instructions sur 16 bits. Nous allons prendre l'exemple du jeu d'instruction emblématique de cette catégorie : l'ARM ''thumb'', qui est le jeu d'instruction compact associé au jeu d'instruction ARM 32 bits. Les deux jeux d'instruction ont un mode d'exécution chacun, ce qui veut dire que le processeur est configuré pour fonctionner en mode ''thumb'' ou en mode ARM 32 bits. En clair, on ne peut pas mélanger les deux jeu d'instructions en même temps. La raison est qu'une même suite de bit correspondait soit à une instruction ARM normale, soit à deux instructions thumbs. Pour faire la différence, il faut préciser dans quel mode d'exécution le CPU est : mode ARM ou mode thumb. Le processeur interpréte les instructions correctement en tenant compte de ce mode. Intuitivement, on se dirait qu'il y a deux décodeurs séparés : un utilisé en mode ARM, un autre utilisé en mode ''thumb''. Sauf que ce n'est pas la solution retenue. A la place, les ingénieurs d'ARM profitent d'une propriété du ''thumb'' : ''thumb'' est un sous-ensemble du jeu d’instruction 32 bits. Par exemple, une instruction ''thumb'' a un équivalent en RAM 32 bits, bien que l'encodage entre les deux est différent. Et cette propriété est utilisée pour faciliter l'implémentation au niveau du décodeur. Le processeur contient simplement un circuit qui convertit les instructions ''thumb'' en leur équivalent ARM 32 bits. Le circuit de conversion se situe en aval de l'unité de chargement, juste avant le décodeur d'instruction. Le circuit est désactivé quand le processeur fonctionne en mode ARM, mais est active en mode ''thumb''. ===Les optimisations du code automodifiant=== Précédemment dans ce cours, nous avons parlé du code automodifiant, à savoir le fait qu'un programme modifie ses propres instructions à la volée. Il était utilisé pour avoir des tableaux, sur des processeurs qui ne supportaient que le mode d'adressage direct. L'idée était que les instructions d'accès mémoire incorporaient une adresse, qui était incrémentée/décrémentée par code auto-modifiant. Les branchements indirects étaient eux aussi gérés de la même manière : l'adresse de destination, incorporée dans l'instruction via adressage absolu, était changée via code automodifiant. Quelques rares processeurs ont incorporé des optimisations pour simplifier l'usage du code automodifiant. Elles agissaient toutes sur le registre d'instruction, en permettant de modifier son contenu. Les instructions étaient chargées dans le registre d'instruction et étaient modifiées dans ce registre, avant d'être décodées. La modification était généralement assez simple : appliquer un masque, additionner une constante, guère plus. Les processeurs de ce type sont très rares, j'en connais deux exemples : le Burroughs B1700 et l'Apollo Guidance Computer. Le Burroughs B1700 permettait d'appliquer un masque sur l'instruction lue. Pour cela, il permettait de faire un OU logique entre l'instruction lue et un registre du processeur, le résultat étant mémorisé dans le registre d'instruction. Cela permettait par exemple de remplacer une adresse dans une micro-opération. L'adresse est alors surimposée avec un OU dans le champ d'adresse, alors que le reste de l'instruction n'est pas modifié. Un autre exemple est celui de l'Apollo Guidance Computer, qui permettait d'altérer le registre d'instruction avec une instruction dédiée, l'instruction ''Index next instruction'', que nous appellerons INI. Nous avons déjà vu cette instruction INI, dans le chapitre sur les modes d'adressage, mais nous pouvons maintenant expliquer comment elle est implémentée. Elle additionne une constante à l'instruction suivante. Elle était utilisée pour ajouter un décalage à une adresse, dans une instruction LOAD/STORE, ou un branchement direct. L'addition était vraisemblablement réalisée par l'ALU du processeur. La conséquence est que le registre d'instruction était un registre architectural, qui était adressé implicitement, avec un mode d'adressage implicite. ==L'incrémentation du ''program counter''== À chaque chargement, le ''program counter'' est mis à jour afin de pointer sur la prochaine instruction à charger. Sur la quasi-totalité des processeurs, les instructions sont placées dans l'ordre d’exécution dans la RAM. En faisant ainsi, on peut mettre à jour le ''program counter'' en lui ajoutant la longueur de l'instruction courante. Déterminer la longueur de l'instruction est simple quand les instructions ont toutes la même taille, mais certains processeurs ont des instructions de taille variable, ce qui complique le calcul. Dans ce qui va suivre, nous allons supposer que les instructions sont de taille fixe, ce qui fait que le ''program counter'' est toujours incrémenté de la même valeur. Il existe deux méthodes principales pour incrémenter le ''program counter'' : soit le ''program counter'' a son propre additionneur, soit le ''program counter'' est mis à jour par l'ALU. Pour ce qui est de l'organisation des registres, soit le ''program counter'' est un registre séparé des autres, soit il est regroupé avec d'autres registres dans un banc de registre. En tout, cela donne quatre organisations possibles. * Un ''program counter'' séparé relié à un incrémenteur séparé. * Un ''program counter'' séparé des autres registres, incrémenté par l'ALU. * Un ''program counter'' intégré au banc de registre, relié à un incrémenteur séparé. * Un ''program counter'' intégré au banc de registre, incrémenté par l'ALU. [[File:Calcul du program counter.png|centre|vignette|upright=2|les différentes méthodes de calcul du program counter.]] Les processeurs haute performance modernes utilisent tous la première méthode. Les autres méthodes étaient surtout utilisées sur les processeurs 8 ou 16 bits, elles sont encore utilisées sur quelques microcontrôleurs de faible puissance. Nous auront l'occasion de donner des exemples quand nous parlerons des processeurs 8 bits anciens, dans le chapitre sur les architectures à accumulateur et affiliées. ===Le ''program counter'' mis à jour par l'ALU=== Sur certains processeurs, le calcul de l'adresse de la prochaine instruction est effectué par l'ALU. L'avantage de cette méthode est qu'elle économise un incrémenteur dédié au ''program counter''. Ce qui explique que cette méthode était utilisée sur les processeurs assez anciens, à une époque où un additionneur pouvait facilement prendre 10 à 20% des transistors disponibles. Le désavantage principal est une augmentation de la complexité du séquenceur, qui doit gérer la mise à jour du ''program counter''. De plus, la mise à jour du ''program counter'' ne peut pas se faire en même temps qu'une opération arithmétique, ce qui réduit les performances. Si on incrémente le ''program counter'' avec l'ALU, il est intéressant de le placer dans le banc de registres. Un désavantage est qu'on perd un registre. Par exemple, avec un banc de registre de 16 registres, on ne peut adresser que 15 registres généraux. De plus ce n'est pas l'idéal pour le décodage des instructions et pour le séquenceur. Si le processeur dispose de plusieurs bancs de registres, le ''program counter'' est généralement placé dans le banc de registre dédié aux adresses. Sinon, il est placé avec les nombres entiers/adresses. D'anciens processeurs incrémentaient le ''program counter'' avec l'ALU, mais utilisaient bien un registre séparé pour le ''program counter''. Cette méthode est relativement simple à implémenter : il suffit de connecter/déconnecter le ''program counter'' du bus interne suivant les besoins. Le ''program counter'' est déconnecté pendant l’exécution d'une instruction, il est connecté au bus interne pour l'incrémenter. C'est le séquenceur qui gère le tout. L'avantage de cette séparation est qu'elle est plus facile à gérer pour le séquenceur. De plus, on gagne un registre général/entier dans le banc de registre. ===Le compteur programme=== Dans la quasi-totalité des processeurs modernes, le ''program counter'' est un vulgaire compteur comme on en a vu dans les chapitres sur les circuits, ce qui fait qu'il passe automatiquement d'une adresse à la suivante. Dans ce qui suit, nous allons appeler ce registre compteur : le '''compteur ordinal'''. L'usage d'un compteur simplifie fortement la conception du séquenceur. Le séquenceur n'a pas à gérer la mise à jour du ''program counter'', sauf en cas de branchements. De plus, il est possible d'incrémenter le ''program counter'' pendant que l'unité de calcul effectue une opération arithmétique, ce qui améliore grandement les performances. Le seul désavantage est qu'elle demande d'ajouter un incrémenteur dédié au ''program counter''. Sur les processeurs très anciens, les instructions faisaient toutes un seul mot mémoire et le compteur ordinal était un circuit incrémenteur des plus basique. Sur les processeurs modernes, le compteur est incrémenté par pas de 4 si les instructions sont codées sur 4 octets, 8 si les instructions sont codées sur 8 octets, etc. Concrètement, le compteur est un circuit incrémenteur auquel on aurait retiré quelques bits de poids faible. Par exemple, si les instructions sont codées sur 4 octets, on coupe les 2 derniers bits du compteur. Si les instructions sont codées sur 8 octets, on coupe les 3 derniers bits du compteur. Et ainsi de suite. Une amélioration de la solution précédente utilise un circuit incrémenteur partagé entre le ''program counter'' et d'autres registres. L'incrémenteur est utilisé pour incrémenter le ''program counter'', mais aussi pour effectuer d'autres calculs d'adresse. Par exemple, sur les architectures avec une pile d'adresse de retour, il est possible de partager l'incrémenteur/décrémenteur avec le pointeur de pile ( la technique ne marche pas avec une pile d'appel). Un autre exemple est celui des processeurs qui gèrent automatiquement le rafraichissement mémoire, grâce à, un compteur intégré dans le processeur. Il est possible de partager l'incrémenteur du ''program counter'' avec le compteur de rafraichissement mémoire, qui mémorise la prochaine adresse mémoire à rafraichir. Nous avions déjà abordé ce genre de partage dans le chapitre sur le chemin de données, dans l'annexe sur le pointeur de pile, mais nous verrons d'autres exemples dans le chapitre sur les architectures hybrides accumulateur-registre 8/16 bits. Il a existé quelques processeurs pour lesquelles le ''program counter'' était non pas un compteur binaire classique, mais un ''linear feedback shift register''. L'avantage est que le circuit d'incrémentation utilisait bien moins de portes logiques, l'économie était substantielle. Mais un défaut est que des instructions consécutives dans le programme n'étaient pas consécutives en mémoire. Il existait cependant une table de correspondance pour dire : la première instruction est à telle adresse, la seconde à telle autre, etc. Un exemple de processeur de ce type est le TMS 1000 de Texas Instrument, un des premiers microcontrôleur 4 bits datant des années 70. ===Quand est mis à jour le ''program counter'' ?=== Le ''program counter'' est mis à jour quand il faut charger une nouvelle instruction. Reste qu'il faut savoir quand le mettre à jour. Incrémenter le ''program counter'' à intervalle régulier, par exemple à chaque cycle d’horloge, fonctionne sur les processeurs où toutes les instructions mettent le même temps pour s’exécuter. Mais de tels processeurs sont très rares. Sur la quasi-totalité des processeurs, les instructions ont une durée d’exécution variable, elles ne prennent pas le même nombre de cycle d'horloge pour s’exécuter. Le temps d’exécution d'une instruction varie selon l'instruction, certaines sont plus longues que d'autres. Tout cela amène un problème : comment incrémenter le ''program counter'' avec des instructions avec des temps d’exécution variables ? La réponse est que la mise à jour du ''program counter'' démarre quand l'instruction précédente a terminée de s’exécuter, plus précisément un cycle avant. C'est un cycle avant pour que l'instruction chargée soit disponible au prochain cycle pour le décodeur. Pour cela, le séquenceur doit déterminer quand l'instruction est terminée et prévenir le ''program counter'' au bon moment. Il faut donc une interaction entre séquenceur et circuit de chargement. Le circuit de chargement contient une entrée Enable, qui autorise la mise à jour du ''program counter''. Le séquenceur met à 1 cette entrée pour prévenir au ''program counter'' qu'il doit être incrémenté au cycle suivant ou lors du cycle courant. Cela permet de gérer simplement le cas des instructions multicycles. [[File:Commande de la mise à jour du program counter.png|centre|vignette|upright=2|Commande de la mise à jour du program counter.]] Toute la difficulté est alors reportée dans le séquenceur, qui doit déterminer la durée d'une instruction. Pour gérer la mise à jour du ''program counter'', le séquenceur doit déterminer la durée d'une instruction. Cela est simple pour les instructions arithmétiques et logiques, qui ont un nombre de cycles fixe, toujours le même. Une fois l'instruction identifiée par le séquenceur, il connait son nombre de cycles et peut programmer un compteur interne au séquenceur, qui compte le nombre de cycles de l'instruction, et fournit le signal Enable au ''program counter''. Mais cette technique ne marche pas vraiment pour les accès mémoire, dont la durée n'est pas connue à l'avance, surtout sur les processeurs avec des mémoires caches. Pour cela, le séquenceur détermine quand l'instruction mémoire est finie et prévient le ''program counter'' quand c'est le cas. Il existe quelques processeurs pour lesquels le temps de calcul dépend des opérandes de l'instruction. On peut par exemple avoir une division qui prend 10 cycles avec certaines opérandes, mais 40 cycles avec d'autres opérandes. Mais ces processeurs sont rares et cela est surtout valable pour les opérations de multiplication/division, guère plus. Le problème est alors le même qu'avec les accès mémoire et la solution est la même : l'ALU prévient le séquenceur quand le résultat est disponible. ==Les branchements et le ''program counter''== Un branchement consiste juste à écrire l'adresse de destination dans le ''program counter''. L'implémentation des branchements demande d'extraire l'adresse de destination et d'altérer le ''program counter'' quand un branchement est détecté. Pour cela, le décodeur d'instruction détecte si l'instruction exécutée est un branchement ou non, et il en déduit où trouver l'adresse de destination. L'adresse de destination se trouve à un endroit qui dépend du mode d'adressage du branchement. Pour rappel, on distingue quatre modes d'adressage pour les branchements : direct, indirect, relatif et implicite. Pour les branchements directs, l'adresse est intégrée dans l'instruction de branchement elle-même et est extraite par le séquenceur. Pour les branchements indirects, l'adresse de destination est dans le banc de registre. Pour les branchements relatifs, il faut ajouter un décalage au ''program counter'', décalage extrait de l'instruction par le séquenceur. Enfin, les instructions de retour de fonction lisent l'adresse de destination depuis la pile. L'implémentation de ces quatre types de branchements est très différente. L'altération du ''program counter'' dépend de si le ''program counter'' est dans un compteur séparé ou s'il est dans le banc de registre. Nous avons vu plus haut qu'il y a quatre cas différents, mais nous n'allons voir que deux cas : celui où le ''program counter'' est dans le banc de registre et est incrémenté par l'unité de calcul, celui avec un ''program counter'' séparé avec son propre incrémenteur. Les deux autres cas ne sont utilisés que sur des architectures à accumulateur ou à pile spécifiques, que nous n'avons pas encore vu à ce stade du cours et que nous verrons en temps voulu dans le chapitre sur ces architectures. ===L’implémentation des branchements avec un ''program counter'' intégré au banc de registres=== Le cas le plus simple est celui où le ''program counter'' est intégré au banc de registre, avec une incrémentation faite par l'unité de calcul. Charger une instruction revient alors à effectuer une instruction LOAD en adressage indirect, à deux différences près : le registre sélectionné est le ''program counter'', l’instruction est copiée dans le registre d'instruction et non dans le banc de registre. L'incrémentation du ''porgram counter'' est implémenté avec des micro-opérations qui agissent sur le chemin de données, au même titre que les branchements indirects, relatifs et directs. * Les branchements indirects copient un registre dans le ''program counter'', ce qui revient simplement à faire une opération MOV entre deux registres, dont le ''program counter'' est la destination. * L'opération inverse d'un branchement indirect, qui copie le ''program counter'' dans un registre général, est utile pour sauvegarder l'adresse de retour d'une fonction. * Les branchements directs copient une adresse dans le ''program counter'', ce qui les rend équivalents à une opération MOV avec une constante immédiate, dont le ''program counter'' est la destination. * Les branchements relatifs sont une opération arithmétique entre le ''program counter'' et un opérande en adressage immédiat. En clair, à partir du moment où le chemin de données supporte les instructions MOV et l'addition, avec les modes d'adressage adéquat, il supporte les branchements. Aucune modification du chemin de données n'est nécessaire, le séquenceur gére le chargement d'une instruction avec les micro-instructions adéquates : une micro-opération ADD pour incrémenter le ''program counter'', une micro-opération LOAD pour le chargement de l'instruction. Notons que sur certaines architectures, le ''program counter'' est adressable au même titre que les autres registres du banc de registre. Les instructions de branchement sont alors remplacées par des instructions MOV ou des instructions arithmétique équivalentes, comme décrit plus haut. ===L’implémentation des branchements avec un ''program counter'' séparé=== Étudions maintenant le cas où le ''program counter'' est dans un registre/compteur séparé. Rappelons que tout compteur digne de ce nom possède une entrée de réinitialisation, qui remet le compteur à zéro. De plus, on peut préciser à quelle valeur il doit être réinitialisé. Ici, la valeur à laquelle on veut réinitialiser le compteur n'est autre que l'adresse de destination du branchement. Implémenter un branchement est donc simple : l'entrée de réinitialisation est commandée par le circuit de détection des branchements, alors que la valeur de réinitialisation est envoyée sur l'entrée adéquate. En clair, nous partons du principe que le ''program counter'' est implémenté avec ce type de compteur : [[File:Fonctionnement d'un compteur (décompteur), schématique.jpg|centre|vignette|upright=1.5|Fonctionnement d'un compteur (décompteur), schématique]] Toute la difficulté est de présenter l'adresse de destination au ''program counter''. Pour les branchements directs, l'adresse de destination est fournie par le séquenceur. L'adresse de destination du branchement sort du séquenceur et est présentée au ''program counter'' sur l'entrée adéquate. Voici donc comment sont implémentés les branchements directs avec un ''program counter'' séparé du banc de registres. Le schéma ci-dessous marche peu importe que le ''program counter'' soit incrémenté par l'ALU ou par un additionneur dédié. [[File:Unité de détection des branchements dans le décodeur.png|centre|vignette|upright=2|Unité de détection des branchements dans le décodeur]] Les branchements relatifs sont ceux qui font sauter X instructions plus loin dans le programme. Leur implémentation demande d'ajouter une constante au ''program counter'', la constante étant fournie dans l’instruction. Là encore, deux solutions sont possibles : réutiliser l'ALU pour calculer l'adresse, ou utiliser un additionneur séparé. L'additionneur séparé peut être fusionné avec l'additionneur qui incrémente le ''program counter'' pour passer à l’instruction suivante. [[File:Unité de chargement qui gère les branchements relatifs.png|centre|vignette|upright=2|Unité de chargement qui gère les branchements relatifs.]] Pour les branchements indirects, il suffit de lire le registre voulu et d'envoyer le tout sur l'entrée adéquate du ''program counter''. Il faut alors rajouter un multiplexeur pour que l'entrée de réinitialisationr ecoive la bonne adresse de destination. [[File:Unité de chargement qui gère les branchements directs.png|centre|vignette|upright=2|Unité de chargement qui gère les branchements directs et indirects.]] Toute la difficulté de l'implémentation des branchements est de configurer les multiplexeurs, ce qui est réalisé par le séquenceur en fonction du mode d'adressage du branchement. ==Les optimisations du chargement des instructions== Charger une instruction est techniquement une forme d'accès mémoire un peu particulier. En clair, charger une instruction prend au minimum un cycle d'horloge, et cela peut rapidement monter à 3-5 cycles, si ce n'est plus. L'exécution des instructions est alors fortement ralentit par la mémoire. Par exemple, imaginez que la mémoire mette 3 cycles d'horloges pour charger une instruction, alors qu'une instruction s’exécute en 1 cycle (si les opérandes sont dans les registres). La perte de performance liée au chargement des instructions est alors substantielle. Heureusement, il est possible de limiter la casse en utilisant des mémoires caches, ainsi que d'autres optimisations, que nous allons voir dans ce qui suit. ===Le tampon de préchargement=== La technique dite du '''préchargement''' est utilisée dans le cas où la mémoire a un temps d'accès important. Mais si la latence de la RAM est un problème, le débit ne l'est pas. Il est possible d'avoir une RAM lente, mais à fort débit. Par exemple, supposons que la mémoire puisse charger 4 instructions (de taille fixe) en 3 cycles. Le processeur peut alors charger 4 instructions en un seul accès mémoire, et les exécuter l'une après l'autre, une par cycle d'horloge. Les temps d'attente sont éliminés : le processeur peut décoder une nouvelle instruction à chaque cycle. Et quand la dernière instruction préchargée est exécutée, la mémoire est de nouveau libre, ce qui masque la latence des accès mémoire. [[File:Tampon de préchargement d'instruction.png|vignette|Tampon de préchargement d'instruction]] La seule contrainte est de mettre les instructions préchargées en attente. La solution pour cela est d'utiliser un registre d'instruction très large, capable de mémoriser plusieurs instructions à la fois. Ce registre est de plus connecté à un multiplexeur qui permet de sélectionner l'instruction adéquate dans ce registre. Ce multiplexeur est commandé par le ''program counter'' et quelques circuits annexes. Ce super-registre d'instruction est appelé un '''tampon de préchargement'''. La méthode a une implémentation nettement plus simple avec des instructions de taille fixe, alignées en mémoire. La commande du multiplexeur de sélection de l'instruction est alors beaucoup plus simple : il suffit d'utiliser les bits de poids fort du ''program counter''. Par exemple, prenons le cas d'un registre d'instruction de 32 octets pour des instructions de 4 octets, soit 8 instructions. Le choix de l'instruction à sélectionner se fait en utilisant les 3 bits de poids faible du ''program counter''. Mais cette méthode ajoute de nouvelles contraintes d'alignement, similaires à celles vues dans le chapitre sur l'alignement et le boutisme, sauf que l'alignement porte ici sur des blocs d'instructions de même taille que le tampon de préchargement. Si on prend l'exemple d'un tampon de préchargement de 128 bits, les instructions devront être alignées par blocs de 128 bits. C'est à dire qu'idéalement, les fonctions et autres blocs de code isolés doivent commencer à des adresses multiples de 128, pour pouvoir charger un bloc d'instruction en une seule fois. Sans cela, les performances seront sous-optimales. : Il arrive que le tampon de préchargement ait la même taille qu'une ligne de cache. Lorsque l'on passe d'un bloc d'instruction à un autre, le tampon de préchargement est mis à jour. Par exemple, si on prend un tampon de préchargement de 4 instructions, on doit changer son contenu toutes les 4 instructions. La seule exception est l'exécution d'un branchement. En effet, lors d'un branchement, la destination du branchement n'est pas dans le tampon de préchargement et elle doit être chargée dedans (sauf si le branchement pointe vers une instruction très proche, ce qui est improbable). Pour cela, le tampon de préchargement est mis à jour précocement quand le processeur détecte un branchement. Le même problème survient avec du code auto-modifiant. Un code auto-modifiant est un programme qui se modifie lui-même, en remplaçant certaines instructions par d'autres, en en retirant, en en ajoutant, lors de sa propre exécution. De tels programmes sont rares, mais la technique était utilisée dans quelques cas au tout début de l'informatique sur des ordinateurs rudimentaires. Ceux-ci avaient des modes d'adressages tellement limités que gérer des tableaux de taille variable demandait d'utiliser du code auto-modifiant pour écrire des boucles. Le problème survient quand des instructions sont modifiées immédiatement après avoir été préchargées. Les instructions dans le tampon de préchargement sont l'ancienne version, alors que la mémoire RAM contient les instructions modifiées. Gérer ce genre de cas demande de vider le tampon de préchargement si le cas arrive, mais ça ne vaut que très rarement le coup, aussi les ingénieurs ne s’embêtent pas à mettre ce correctif en place. Le code automodifiant est alors buggé. ===La ''prefetch input queue''=== La ''prefetch input queue'' est une sorte de cousine du tampon de préchargement, qui a été utilisée sur les processeurs Intel 8 et 16 bits, ainsi que sur le 286 et le 386. L'idée est là encore de précharger des instructions en avance. Sauf que cette fois-ci, on ne charge pas plusieurs instructions à la fois. Au contraire, les instructions sont préchargées séquentiellement, un ou deux octet à la fois. En conséquence, le tampon de préchargement est remplacé par une mémoire FIFO appelée la '''''Prefetch Input Queue''''', terme abrévié en PIQ. Les instructions sont accumulées une par une dans la PIQ, elles en sortent une par une au fur et à mesure pour alimenter le décodeur d'instruction. Pendant que le processeur exécute une instruction, on précharge la suivante. Sans ''prefetch input queue'', le processeur chargeait une instruction, la décodait et l'exécutait, puis chargeait la suivante, et ainsi de suite. Avec un tampon de préchargement, le processeur chargeait plusieurs instruction en une seule fois, puis les exécutait les unes après les autres. Avec la ''prefetch input queue'', pendant qu'une instruction est en cours de décodage/exécution, le processeur précharge l'instruction suivante. Si une instruction prend vraiment beaucoup de temps pour s'exécuter, le processeur peut en profiter pour précharger l'instruction encore suivante, et ainsi de suite, jusqu'à une limite de 4/6 instructions (la limite dépend du processeur). La ''prefetch input queue'' permet de précharger l'instruction suivante à condition qu'aucun autre accès mémoire n'ait lieu. Si l'instruction en cours d'exécution effectue des accès mémoire, ceux-ci sont prioritaires sur tout le reste, le préchargement de l'instruction suivante est alors mis en pause ou inhibé. Par contre, si la mémoire n'est pas utilisée par l'instruction en cours d'exécution, le processeur lit l'instruction suivante en RAM et la copie dans la ''prefetch input queue''. En conséquence, il arrive que la ''prefetch input queue'' n'ait pas préchargé l'instruction suivante, alors que le décodeur d'instruction est prêt pour la décoder. Dans ce cas, le décodeur doit attendre que l'instruction soit chargée. Les instructions préchargées dans la ''Prefetch input queue'' y attendent que le processeur les lise et les décode. Les instructions préchargées sont conservées dans leur ordre d'arrivée, afin qu'elles soient exécutées dans le bon ordre, ce qui fait que la ''Prefetch input queue'' est une mémoire de type FIFO. L'étape de décodage pioche l'instruction à décoder dans la ''Prefetch input queue'', cette instruction étant par définition la plus ancienne, puis la retire de la file. Le premier processeur commercial grand public à utiliser une ''prefetch input queue'' était le 8086. Il pouvait précharger à l'avance 6 octets d’instruction. L'Intel 8088 avait lui aussi une ''Prefetch input queue'', mais qui ne permettait que de précharger 4 instructions. Le 386 avait une ''prefetch input queue'' de 16 octets. La taille idéale de la FIFO dépend de nombreux paramètres. On pourrait croire que plus elle peut contenir d'instructions, mieux c'est, mais ce n'est pas le cas, pour des raisons que nous allons expliquer dans quelques paragraphes. [[File:80186 arch.png|centre|vignette|700px|upright=2|Architecture du 8086, du 80186 et de ses variantes.]] Vous remarquerez que j'ai exprimé la capacité de la ''prefetch input queue'' en octets et non en instructions. La raison est que sur les processeurs x86, les instructions sont de taille variable, avec une taille qui varie entre un octet et 6 octets. Cependant, le décodage des instructions se fait un octet à la fois : le décodeur lit un octet, puis éventuellement le suivant, et ainsi de suite jusqu'à atteindre la fin de l'instruction. En clair, le décodeur lit les instructions longues octet par octet dans la ''Prefetch input queue''. Les instructions varient entre 1 et 6 octets, mais tous ne sont pas utiles au décodeur d'instruction. Par exemple, le décodeur d'instruction n'a pas besoin d'analyser les constantes immédiates intégrées dans une instruction, ni les adresses mémoires en adressage absolu. Il n'a besoin que de deux octets : l'opcode et l'octet Mod/RM qui précise le mode d'adressage. Le second est facultatif. En clair, le décodeur a besoin de lire deux octets maximum depuis la ''prefetch input queue'', avant de passer à l’instruction suivante. Les autres octets étaient envoyés ailleurs, typiquement dans le chemin de données. Par exemple, prenons le cas d'une addition entre le registre AX et une constante immédiate. L'instruction fait trois octets : un opcode suivie par une constante immédiate codée sur deux octets. L'opcode était envoyé au décodeur, mais pas la constante immédiate. Elle était lue octet par octet et mémorisée dans un registre temporaire placé en entrée de l'ALU. Idem avec les adresses immédiates, qui étaient envoyées dans un registre d’interfaçage mémoire sans passer par le décodeur d'instruction. Pour cela, la ''prefetch input queue'' était connectée au bus interne du processeur ! Le décodeur dispose d'une micro-opération pour lire un octet depuis la ''prefetch input queue'' et le copier ailleurs dans le chemin de données. Par exemple, l'instruction d'addition entre le registre AX et une constante immédiate était composée de quatre micro-opérations : une qui lisait le premier octet de la constante immédiate, une seconde pour le second, une troisième micro-opération qui commande l'ALU et fait le calcul. Le décodeur devait attendre que qu'un moins un octet soit disponible dans la ''Prefetch input queue'', pour le lire. Il lisait alors cet octet et déterminait s'il contenait une instruction complète ou non. Si c'est une instruction complète, il la décodait et l''exécutait, puis passait à l'instruction suivante. Sinon, il lit un second octet depuis la ''Prefetch input queue'' et relance le décodage. Là encore, le décodeur vérifie s'il a une instruction complète, et lit un troisième octet si besoin, puis rebelote avec un quatrième octet lu, etc. Un circuit appelé le ''loader'' synchronisait le décodeur d'instruction et la ''Prefetch input queue''. Il fournissait deux bits : un premier bit pour indiquer que le premier octet d'une instruction était disponible dans la ''Prefetch input queue'', un second bit pour le second octet. Le ''loader'' recevait aussi des signaux de la part du décodeur d'instruction. Le signal ''Run Next Instruction'' entrainait la lecture d'une nouvelle instruction, d'un premier octet, qui était alors dépilé de la ''Prefetch input queue''. Le décodeur d'instruction pouvait aussi envoyer un signal ''Next-to-last (NXT)'', un cycle avant que l'instruction en cours ne termine. Le ''loader'' réagissait en préchargeant l'octet suivant. En clair, ce signal permettait de précharger l'instruction suivante avec un cycle d'avance, si celle-ci n'était pas déjà dans la ''Prefetch input queue''. Le bus de données du 8086 faisait 16 bits, alors que celui du 8088 ne permettait que de lire un octet à la fois. Et cela avait un impact sur la ''prefetch input queue''. La ''prefetch input queue'' du 8088 était composée de 4 registres de 8 bits, avec un port d'écriture de 8 bits pour précharger les instructions octet par octet, et un port de lecture de 8 bits pour alimenter le décodeur. En clair, tout faisait 8 bits. Le 8086, lui utilisait des registres de 16 bits et un port d'écriture de 16 bits. le port de lecture restait de 8 bits, grâce à un multiplexeur sélectionnait l'octet adéquat dans un registre 16 bits. Sa ''prefetch input queue'' préchargeait les instructions par paquets de 2 octets, non octet par octet, alors que le décodeur consommait les instructions octet par octet. Il s’agissait donc d'une sorte d'intermédiaire entre ''prefetch input queue'' à la 8088 et tampon de préchargement. Le 386 était dans un cas à part. C'était un processeur 32 bits, sa ''prefetch input queue'' contenait 4 registres de 32 bits, un port d'écriture de 32 bits. Mais il ne lisait pas les instructions octet par cotet. A la place, son décodeur d'instruction avait une entrée de 32 bits. Cependant, il gérait des instructions de 8 et 16 bits. Il fallait alors dépiler des instructions de 8, 16 et 32 bits dans la ''prefetch input queue''. De plus, les instructions préchargées n'étaient pas parfaitement alignées sur 32 bits : une instruction pouvait être à cheval sur deux registres 32 bits. Le processeur incorporait donc des circuits d'alignement, semblables à ceux utilisés pour gérer des instructions de longueur variable avec un registre d'instruction. Les branchements posent des problèmes avec la ''prefetch input queue'' : à cause d'eux, on peut charger à l'avance des instructions qui sont zappées par un branchement et ne sont pas censées être exécutées. Si un branchement est chargé, toutes les instructions préchargées après sont potentiellement invalides. Si le branchement est non-pris, les instructions chargées sont valides, elles sont censées s’exécuter. Mais si le branchement est pris, elles ont été chargées à tort et ne doivent pas s’exécuter. Pour éviter cela, la ''prefetch input queue'' est vidée quand le processeur détecte un branchement. Pour cela, le décodeur d'instruction dispose d'une micro-opération qui vide la ''Prefetch input queue'', elle invalide les instructions préchargées. Cette détection ne peut se faire qu'une fois le branchement décodé, qu'une fois que l'on sait que l'instruction décodée est un branchement. En clair, le décodeur invalide la ''Prefetch input queue'' quand il détecte un branchement. Les interruptions posent le même genre de problèmes. Il faut impérativement vider la ''Prefetch input queue'' quand une interruption survient, avant de la traiter. Il faut noter qu'une ''Prefetch input queue'' interagit assez mal avec le ''program counter''. En effet, la ''Prefetch input queue'' précharge les instructions séquentiellement. Pour savoir où elle en est rendue, elle mémorise l'adresse de la prochaine instruction à charger dans un '''registre de préchargement''' dédié. Il serait possible d'utiliser un ''program counter'' en plus du registre de préchargement, mais ce n'est pas la solution qui a été utilisée. Les processeurs Intel anciens ont préféré n'utiliser qu'un seul registre de préchargement en remplacement du ''program counter''. Après tout, le registre de préchargement n'est qu'un ''program counter'' ayant pris une avance de quelques cycles d'horloge, qui a été incrémenté en avance. Et cela ne pose pas de problèmes, sauf avec certains branchements. Par exemple, certains branchements relatifs demandent de connaitre la véritable valeur du ''program counter'', pas celle calculée en avance. Idem avec les instructions d'appel de fonction, qui demandent de sauvegarder l'adresse de retour exacte, donc le vrai ''program counter''. De telles situations demandent de connaitre la valeur réelle du ''program counter'', celle sans préchargement. Pour cela, le décodeur d'instruction dispose d'une instruction pour reconstituer le ''program counter'', à savoir corriger le ''program coutner'' et éliminer l'effet du préchargement. La micro-opération de correction se contente de soustraire le nombre d'octets préchargés au registre de préchargement. Le nombre d'octets préchargés est déduit à partir des deux pointeurs intégré à la FIFO, qui indiquent la position du premier et du dernier octet préchargé. Le bit qui indique si la FIFO est vide était aussi utilisé. Les deux pointeurs sont lus depuis la FIFO, et sont envoyés à un circuit qui détermine le nombre d'octets préchargés. Sur le 8086, ce circuit était implémenté non pas par un circuit combinatoire, mais par une mémoire ROM équivalente. La petite taille de la FIFO faisait que les pointeurs étaient très petits et la ROM l'était aussi. Un autre défaut est que la ''Prefetch input queue'' se marie assez mal avec du code auto-modifiant. Un code auto-modifiant est un programme qui se modifie lui-même, en remplaçant certaines instructions par d'autres, en en retirant, en en ajoutant, lors de sa propre exécution. De tels programmes sont rares, mais la technique était utilisée dans quelques cas au tout début de l'informatique sur des ordinateurs rudimentaires. Ceux-ci avaient des modes d'adressages tellement limités que gérer des tableaux de taille variable demandait d'utiliser du code auto-modifiant pour écrire des boucles. Le problème avec la ''Prefetch input queue'' survient quand des instructions sont modifiées immédiatement après avoir été préchargées. Les instructions dans la ''Prefetch input queue'' sont l'ancienne version, alors que la mémoire RAM contient les instructions modifiées. Gérer ce genre de cas est quelque peu complexe. Il faut en effet vider la ''Prefetch input queue'' si le cas arrive, ce qui demande d'identifier les écritures qui écrasent des instructions préchargées. C'est parfaitement possible, mais demande de transformer la ''Prefetch input queue'' en une mémoire hybride, à la fois mémoire associative et mémoire FIFO. Cela ne vaut que très rarement le coup, aussi les ingénieurs ne s’embêtent pas à mettre ce correctif en place, le code automodifiant est alors buggé. Le décodeur d'instruction dispose aussi d'une micro-opération qui stoppe le préchargement des instructions dans la ''Prefetch input queue''. : Pour ceux qui ont déjà lu un cours d'architecture des ordinateurs, la relation entre ''prefetch input queue'' et pipeline est quelque peu complexe. En apparence, la ''prefetch input queue'' permet une certaines forme de pipeline, en chargeant une instruction pendant que la précédente s'exécute. Les problématiques liées aux branchements ressemblent beaucoup à celles de la prédiction de branchement. Cependant, il est possible de dire qu'il s'agit plus d'une technique de préchargement que de pipeline. La différence entre les deux est assez subtile et les frontières sont assez floues, mais le fait est que l'instruction en cours d'exécution et le préchargement peuvent tout deux faire des accès mémoire et que l'instruction en cours a la priorité. Un vrai pipeline se débrouillerait avec une architecture Harvard, non avec un bus mémoire unique pour le chargement des instruction et la lecture/écriture des données. ===Le ''Zero-overhead looping''=== Nous avions vu dans le chapitre sur les instructions machines, qu'il existe des instructions qui permettent de grandement faciliter l'implémentation des boucles. Il s'agit de l'instruction REPEAT, utilisée sur certains processeurs de traitement de signal. Elle répète l'instruction suivante, voire une série d'instructions, un certain nombre de fois. Le nombre de répétitions est mémorisé dans un registre décrémenté à chaque itération de la boucle. Elle permet donc d'implémenter une boucle FOR facilement, sans recourir à des branchements ou des conditions. Une technique en lien avec cette instruction permet de grandement améliorer les performances et la consommation d'énergie. L'idée est de mémoriser la boucle, les instructions à répéter, dans une petite mémoire cache située entre l'unité de chargement et le séquenceur. Le cache en question s'appelle un '''tampon de boucle''', ou encore un ''hardware loop buffer''. Grâce à lui, il n'y a pas besoin de charger les instructions à chaque fois qu'on les exécute, on les charge une bonne fois pour toute : c'est plus rapide. Bien sûr, il ne fonctionne que pour de petites boucles, mais il en est de même avec l'instruction REPEAT. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le chemin de données | prevText=Le chemin de données | next=L'unité de contrôle | nextText=L'unité de contrôle }} </noinclude> 05gugnt46kxn69z745cpfub093ytyxm Feuilles dupliquées orphelines 0 83624 764077 761056 2026-04-20T08:10:22Z Xhungab 23827 764077 wikitext text/x-wiki [[Catégorie:Feuilles volantes]] '''Je propose de créer un livre feuilles dupliquées orphelines pour sortir ces pages de la catégorie page orpheline. Ces pages orphelines ont été recopiées ou regroupé dans le texte d'un livre. Elles font donc doublons. A cause de leurs historiques, les supprimer est très complexes. En attendant de les supprimer elles se reposeront ici.''' * 01) Page orpheline du livre : '''États généraux du multilinguisme dans les outre-mer''' * 02) Page orpheline du livre : '''Les Pôles d'éco-citoyenneté dans le Nord-Pas-de-Calais''' * 03) Page orpheline du livre : '''Ville30''' * 04) Page orpheline du livre : '''Construire des communs''' * 05) Page orpheline du livre : '''Construire sa maison''' * 06) Page orpheline du livre : '''Enseignement de l'indonésien''' * 07) Page orpheline du livre : '''Fonctionnement d'un ordinateur''' * 08) Page orpheline du livre : '''GNU Health''' * 09) Page orpheline du livre : '''Informatique et Sciences du Numérique au lycée : un pas plus loin''' * 10) Page orpheline du livre : '''Peeragogy Handbook V1.3''' * 11) Page orpheline du livre : '''Physique atomique''' * 12) Page orpheline du livre : '''Calcul tensoriel''' * 13) Page orpheline du livre : '''Les moteurs de rendu des FPS en 2.5 D''' * 14) Page orpheline du livre : '''LispWorks CAPI''' * 15) Page orpheline du livre : '''Jardin au naturel''' * 16) Page orpheline du livre : '''Programmation Bash''' * 17) Page orpheline du livre : '''Tribologie''' * 18) Page orpheline du livre : '''Recherches sur les naissances « physiologique » et « naturelle »''' * 19) Page orpheline du livre : '''L'éco-construction en Nord-Pas-de-Calais''' * 20) Page orpheline du livre : '''Initiatives éco-citoyennes''' * 21) Page orpheline du livre : '''Pôles d'éco-citoyenneté''' * 22) Page orpheline du livre : '''Inventer les territoires culturels de demain''' * 23) Page orpheline du livre : '''Manuel de psychopathologie et psychothérapie intégrative''' * 24) Page orpheline du livre : '''Pratique médicale en Afrique francophone''' * 25) Page orpheline du livre : '''Les suites et séries''' * 26) Page orpheline du livre : '''Les opérations bit à bit''' * 27) Page orpheline du livre : '''Systèmes sensoriels''' * 28) Page orpheline du livre : '''Wikigreen''' * 29) Page orpheline du livre : '''Néerlandais''' * 30) Page orpheline du livre : '''Photographie''' * 31) Page orpheline du livre : '''Livre de cuisine''' * 32) Page orpheline du livre : '''Guide du MOOC''' ---- ---- ---- ---- Page orpheline du livre : États généraux du multilinguisme dans les outre-mer Bonjour, Le livre "États généraux du multilinguisme dans les outre-mer" a laissé quelques pages orphelines. Ces pages orphelines ont été recopiées ou regroupé dans le texte du livre. Elles font donc doublons. 1) page orpheline * [[États généraux du multilinguisme dans les outre-mer/Annexes/Contexte]] * Copié ici * [[États généraux du multilinguisme dans les outre-mer/Annexes]] 2) page orpheline * [[États généraux du multilinguisme dans les outre-mer/Annexes/Références]] * Copié ici * [[États généraux du multilinguisme dans les outre-mer/Annexes]] 3) page orpheline * [[États généraux du multilinguisme dans les outre-mer/Introduction]] * Copié ici * [[États généraux du multilinguisme dans les outre-mer/Présentation/Introduction]] 4) page orpheline * [[États généraux du multilinguisme dans les outre-mer/Synthèse et débats]] * Copié ici * [[États généraux du multilinguisme dans les outre-mer/Synthèse : restitution des ateliers]] 5) page orpheline * [[États généraux du multilinguisme dans les outre-mer/Thématiques/La transmission des langues : la prise en compte des langues d'origine et des acquis culturels dans l'apprentissage du français, leur place dans le système éducatif]] * Copié ici * [[États généraux du multilinguisme dans les outre-mer/Thématiques/La transmission des langues : la prise en compte des langues d'origine et des acquis culturels dans l'apprentissage du français ; leur place dans le système éducatif]] 6) page orpheline * [[États généraux du multilinguisme dans les outre-mer/Thématiques/Langues et création artistique/Table ronde plénière : transcription thématique 6]] * Copié ici * [[États généraux du multilinguisme dans les outre-mer/Thématiques/Langues et création artistique]] 7) page orpheline * [[États généraux du multilinguisme dans les outre-mer/Thématiques/Le rôle des langues dans la construction d'une identité commune/Table ronde plénière : transcription thématique 5]] * Copié ici * [[États généraux du multilinguisme dans les outre-mer/Thématiques/Le rôle des langues dans la construction d'une identité commune]] 8) page orpheline * [[États généraux du multilinguisme dans les outre-mer/Thématiques/Les technologies de la langue, la présence des langues sur la toile et sur les réseaux sociaux/Table ronde plénière : transcription thématique 4]] * Copié ici * [[États généraux du multilinguisme dans les outre-mer/Thématiques/Les technologies de la langue, la présence des langues sur la toile et sur les réseaux sociaux]] 9) page orpheline * [[États généraux du multilinguisme dans les outre-mer/Thématiques/L’emploi des langues : plurilinguisme, pratiques individuelles et pratiques sociales/Table ronde plénière : transcription]] * Copié ici * [[États généraux du multilinguisme dans les outre-mer/Thématiques/L’emploi des langues : plurilinguisme, pratiques individuelles et pratiques sociales]] 10) page orpheline * [[États généraux du multilinguisme dans les outre-mer/Thématiques/« L’équipement » des langues : de l’oral à l’écrit, description et outillage linguistique/Table ronde plénière : transcription thématique 2]] * Copié ici * [[États généraux du multilinguisme dans les outre-mer/Thématiques/« L’équipement » des langues : de l’oral à l’écrit, description et outillage linguistique]] 11) page orpheline * [[États généraux du multilinguisme dans les outre-mer/Thématiques/L’emploi des langues : plurilinguisme, pratiques individuelles et pratiques sociales/Table ronde plénière : transcription thématique 1]] * Copié ici * [[États généraux du multilinguisme dans les outre-mer/Thématiques/L’emploi des langues : plurilinguisme, pratiques individuelles et pratiques sociales]] 12) page orpheline * [[États généraux du multilinguisme dans les outre-mer/Thématiques/L’emploi des langues : plurilinguisme, pratiques individuelles et pratiques sociales/Table ronde plénière : transcription thématique 2]] * Copié ici * [[États généraux du multilinguisme dans les outre-mer/Thématiques/« L’équipement » des langues : de l’oral à l’écrit, description et outillage linguistique]] 13) page orpheline * [[États généraux du multilinguisme dans les outre-mer/Thématiques/L’emploi des langues : plurilinguisme, pratiques individuelles et pratiques sociales/Table ronde plénière : transcription thématique 3]] * Copié ici * [[États généraux du multilinguisme dans les outre-mer/Thématiques/La transmission des langues : la prise en compte des langues d'origine et des acquis culturels dans l'apprentissage du français ; leur place dans le système éducatif]] 14) page orpheline * [[États généraux du multilinguisme dans les outre-mer/Thématiques/L’emploi des langues : plurilinguisme, pratiques individuelles et pratiques sociales/Table ronde plénière : transcription thématique 4]] * Copié ici * [[États généraux du multilinguisme dans les outre-mer/Thématiques/Les technologies de la langue, la présence des langues sur la toile et sur les réseaux sociaux]] 15) page orpheline * [[États généraux du multilinguisme dans les outre-mer/Thématiques/L’emploi des langues : plurilinguisme, pratiques individuelles et pratiques sociales/Table ronde plénière : transcription thématique 5]] * Copié ici * [[États généraux du multilinguisme dans les outre-mer/Thématiques/Le rôle des langues dans la construction d'une identité commune]] 16) page orpheline * [[États généraux du multilinguisme dans les outre-mer/Thématiques/L’emploi des langues : plurilinguisme, pratiques individuelles et pratiques sociales/Table ronde plénière : transcription thématique 6]] * Copié ici * [[États généraux du multilinguisme dans les outre-mer/Thématiques/Langues et création artistique]] 17) page orpheline * [[États généraux du multilinguisme dans les outre-mer/Thématiques/L’emploi des langues : plurilinguisme, pratiques individuelles et pratiques sociales/Transcription de séance plénière]] * Copié ici voir plus loin * [[États généraux du multilinguisme dans les outre-mer/Thématiques/L’emploi des langues : plurilinguisme, pratiques individuelles et pratiques sociales]] ---- ---- ---- ---- Page orpheline du livre : Les Pôles d'éco-citoyenneté dans le Nord-Pas-de-Calais Bonjour, Le livre "Les Pôles d'éco-citoyenneté dans le Nord-Pas-de-Calais" a laissé quelques pages orphelines. Ces pages orphelines ont été recopiées ou regroupé dans le texte du livre. Elles font donc doublons. a) La page orpheline : * [[Les Pôles d'éco-citoyenneté dans le Nord-Pas-de-Calais/Des pôles d'éco-citoyennetés remarquables en-dehors du Nord-Pas-de-Calais]] * A été copier ici en fin de fichier. * [[Pôles d'éco-citoyenneté/Des pôles d'éco-citoyennetés remarquables en-dehors du Nord-Pas-de-Calais]] b) La page orpheline : * [[Les Pôles d'éco-citoyenneté dans le Nord-Pas-de-Calais/Les principaux Pôles d'éco-citoyenneté dans le Nord-Pas-de-Calais/Les Parcs Naturels Régionaux/Le PNR Scarpe-Escaut]] * A été copier ici en fin de fichier. * [[Pôles d'éco-citoyenneté/Les principaux Pôles d'éco-citoyenneté dans le Nord-Pas-de-Calais/Les Parcs Naturels Régionaux/Le PNR Scarpe-Escaut]] c) La page orpheline : * [[Les Pôles d'éco-citoyenneté dans le Nord-Pas-de-Calais/Les principaux Pôles d'éco-citoyenneté dans le Nord-Pas-de-Calais/Les Parcs Naturels Régionaux/Le PNR Scarpe-Escaut/Centre d'Amaury]] * A été copier ici. * [[Pôles d'éco-citoyenneté/Les principaux Pôles d'éco-citoyenneté dans le Nord-Pas-de-Calais/Les Parcs Naturels Régionaux/Le PNR Scarpe-Escaut/Centre d'Amaury]] d) La page orpheline : * [[Les Pôles d'éco-citoyenneté dans le Nord-Pas-de-Calais/Les principaux Pôles d'éco-citoyenneté dans le Nord-Pas-de-Calais/Les Parcs Naturels Régionaux/Le PNR Scarpe-Escaut/Maison de la Forêt]] * A été copier ici. * [[Pôles d'éco-citoyenneté/Les principaux Pôles d'éco-citoyenneté dans le Nord-Pas-de-Calais/Les Parcs Naturels Régionaux/Le PNR Scarpe-Escaut/Maison de la Forêt]] e) La page orpheline : * [[Les Pôles d'éco-citoyenneté dans le Nord-Pas-de-Calais/Sources]] * A été copier ici fin de fichier. * [[Pôles d'éco-citoyenneté/Sources]] f) La page orpheline : * [[Les Pôles d'éco-citoyenneté dans le Nord-Pas-de-Calais]] * Je crois que ce travail a déjà été effectué Merci Xhungab (discussion) 10 février 2026 à 12:19 (CET) ---- ---- ---- ---- Page orpheline du livre : Ville30 Le livre "Ville30" a laissé quelques pages orphelines. Ces pages orphelines ont été recopiées ou regroupé dans le texte du livre. Elles font donc doublons. Ces pages orphelines : *[[Ville30/Les espaces partagés|Les espaces partagés]] *[[Ville30/Les zones 30 et le concept de "ville 30"|Les zones 30 et le concept de "ville 30"]] *[[Ville30/Les zones piétonnes|Les zones piétonnes]] *[[Ville30/Les zones résidentielles et zones de rencontre|Les zones résidentielles et zones de rencontre]] *[[Ville30/Les zones à trafic limité|Les zones à trafic limité]] Ont été copié ici : *[[Ville30/Les concepts d’aménagements renforçant la fonction sociale|Les concepts d’aménagements renforçant la fonction sociale]] ---- ---- ---- ---- Page orpheline du livre : Construire des communs Bonjour, Le livre "Construire des communs" a laissé quelques pages orphelines. Ces pages orphelines ont été recopiées ou regroupé dans le texte du livre. Elles font donc doublons. La page orpheline : * [[Construire des communs/Public/Appels à projets|Appels à projets]] a été copiée ici: * [[Construire des communs/Public|Public]] Cette page semble abandonnée: * [[Construire des communs/Elinor Ostrom|Elinor Ostrom]] ---- ---- ---- ---- Page orpheline du livre : Construire sa maison Bonjour, Le livre "Construire sa maison" a laissé quelques pages orphelines. Ces pages orphelines ont été recopiées ou regroupé dans le texte du livre. Elles font donc doublons. La page orpheline : * [[Construire sa maison/Étude architecturale2|Étude architecturale2]] a été copiée ici: * [[Construire sa maison/Étude architecturale|L'étude architecturale]] La page orpheline : * [[Construire sa maison/Prospection immobilière|Prospection immobilière]] a été copiée ici: * [[Construire sa maison/Avant-Projet|Avant-Projet]] ---- ---- ---- ---- Page orpheline du livre : Enseignement de l'indonésien Bonjour, Le livre "Enseignement de l'indonésien" a laissé quelques pages orphelines. Ces pages orphelines ont été recopiées ou regroupé dans le texte du livre. Elles font donc doublons. La page orpheline : * [[Enseignement de l'indonésien/Contenu|Enseignement de l'indonésien/Contenu]] a été copiée ici: * [[Enseignement de l'indonésien/Contenus|Enseignement de l'indonésien/Contenus]] La page orpheline : * [[Enseignement de l'indonésien/Grammaire/Tenses|Enseignement de l'indonésien/Grammaire/Tenses]] a été copiée ici: * [[Enseignement de l'indonésien/Grammaire/Temps|Enseignement de l'indonésien/Grammaire/Temps]] ---- ---- ---- ---- Page orpheline du livre : Fonctionnement d'un ordinateur Bonjour, Le livre "Fonctionnement d'un ordinateur" a laissé quelques pages orphelines. Ces pages orphelines ont été recopiées ou regroupé dans le texte du livre. Elles font donc doublons. La page orpheline : * [[Fonctionnement d'un ordinateur/Version imprimable|Fonctionnement d'un ordinateur/Version imprimable]] a été copiée ici: * [[Fonctionnement d'un ordinateur/Version imprimable 2|Fonctionnement d'un ordinateur/Version imprimable 2]] La page orpheline : * [[Fonctionnement d'un ordinateur/Les circuits pour la population count|Fonctionnement d'un ordinateur/Les circuits pour la population count]] a été copiée ici: * [[Fonctionnement d'un ordinateur/Les circuits de calcul logique et bit à bit|Fonctionnement d'un ordinateur/Les circuits de calcul logique et bit à bit]] La page orpheline : * [[Fonctionnement d'un ordinateur/La mémoire virtuelle des périphériques|Fonctionnement d'un ordinateur/La mémoire virtuelle des périphériques]] a été copiée ici: Profondement modifiée. * [[Fonctionnement d'un ordinateur/Les méthodes de synchronisation entre processeur et périphériques|Les méthodes de synchronisation entre processeur et périphériques]] * [[Fonctionnement d'un ordinateur/L'adressage des périphériques|L'adressage des périphériques]] * [[Fonctionnement d'un ordinateur/Les périphériques et les cartes d'extension|Les périphériques et les cartes d'extension]] ---- ---- ---- ---- Page orpheline du livre : GNU Health Bonjour, Le livre "GNU Health" a laissé quelques pages orphelines. Ces pages orphelines ont été recopiées ou regroupé dans le texte du livre. Elles font donc doublons. La page orpheline : * [[GNU Health/Gestion des Droits dAcces|Gestion des Droits dAcces]] a été copiée ici: * [[GNU Health/Gestion des Droits d'Accès|Gestion des Droits d'Accès]] ---- ---- ---- ---- Page orpheline du livre : Informatique et Sciences du Numérique au lycée : un pas plus loin Bonjour, Le livre "Informatique et Sciences du Numérique au lycée : un pas plus loin" a laissé quelques pages orphelines. Ces pages orphelines ont été recopiées ou regroupé dans le texte du livre. Elles font donc doublons. La page orpheline : * [[Informatique et Sciences du Numérique au lycée : un pas plus loin/LANGAGES/Eléments de syntaxe et de sémantique/Représentation des données|Informatique et Sciences du Numérique au lycée : un pas plus loin/LANGAGES/Eléments de syntaxe et de sémantique/Représentation des données]] a été copiée ici: * [[Informatique et Sciences du Numérique au lycée : un pas plus loin/LANGAGES/Éléments de syntaxe et de sémantique/Données et types|Informatique et Sciences du Numérique au lycée : un pas plus loin/LANGAGES/Éléments de syntaxe et de sémantique/Données et types]] La page orpheline : * [[Informatique et Sciences du Numérique au lycée : un pas plus loin/LANGAGES/Eléments de syntaxe et de sémantique/Structuration des programmes|Informatique et Sciences du Numérique au lycée : un pas plus loin/LANGAGES/Eléments de syntaxe et de sémantique/Structuration des programmes]] a été copiée ici: * [[Informatique et Sciences du Numérique au lycée : un pas plus loin/LANGAGES/Éléments de syntaxe et de sémantique/Structuration des programmes|Informatique et Sciences du Numérique au lycée : un pas plus loin/LANGAGES/Éléments de syntaxe et de sémantique/Structuration des programmes]] La page orpheline : * [[Informatique et Sciences du Numérique au lycée : un pas plus loin/LANGAGES/Eléments de syntaxe et de sémantique/Styles de Programmation|Informatique et Sciences du Numérique au lycée : un pas plus loin/LANGAGES/Eléments de syntaxe et de sémantique/Styles de Programmation]] a été copiée ici: * [[Informatique et Sciences du Numérique au lycée : un pas plus loin/LANGAGES/Éléments de syntaxe et de sémantique/Styles de Programmation|Informatique et Sciences du Numérique au lycée : un pas plus loin/LANGAGES/Éléments de syntaxe et de sémantique/Styles de Programmation]] ---- ---- ---- ---- Page orpheline du livre : Peeragogy Handbook V1.3 Bonjour, Le livre "Peeragogy Handbook V1.3" a laissé quelques pages orphelines. Ces pages orphelines ont été recopiées ou regroupé dans le texte du livre. Elles font donc doublons. La page orpheline : * [[Peeragogy Handbook V1.3/Ajouter un cadre|Peeragogy Handbook V1.3/Ajouter un cadre]] a été copiée ici: * [[Peeragogy Handbook V1.3/Ajouter un cadre avec des activités|Peeragogy Handbook V1.3/Ajouter un cadre avec des activités]] ---- ---- ---- ---- Page orpheline du livre : Physique atomique Bonjour, Le livre "Physique atomique" a laissé quelques pages orphelines. Ces pages orphelines ont été recopiées ou regroupé dans le texte du livre. Elles font donc doublons. La page orpheline : * [[Physique atomique/Quantification de l'énergie|Quantification de l'énergie]] Voir les pages qui ont été «wikifiées»: * [[Physique atomique/Loi de Planck|Loi de Planck]] * [[Physique atomique/Effet photoélectrique|Effet photoélectrique]] * [[Physique atomique/Quantité de mouvement du rayonnement|Quantité de mouvement du rayonnement]] * [[Physique atomique/Spectres optiques|Spectres optiques]] * [[Physique atomique/Excitation électronique d’une vapeur atomique|Structure de l'atome]] La page orpheline : * [[Physique atomique/Structure de l'atome|Structure de l'atome]] Voir les pages qui ont été «wikifiées»: * [[Physique atomique/Les modèles classiques|Les modèles classiques]] * [[Physique atomique/Spectre des rayons X|Spectre des rayons X]] ---- ---- ---- ---- Page orpheline du livre : Calcul tensoriel Bonjour, Le livre "Calcul tensoriel" a laissé quelques pages orphelines. Ces pages orphelines ont été recopiées ou regroupé dans le texte du livre. Elles font donc doublons. La page orpheline : * [[Calcul tensoriel/Notions élémentaires/Tenseur métrique/Pseudo-contraction de sa dérivée partielle|Pseudo-contraction de sa dérivée partielle]] a été copiée ici: * [[Calcul tensoriel/Notions élémentaires/Tenseur métrique/Pseudo-contraction de la dérivée partielle|Pseudo-contraction de la dérivée partielle]] ---- ---- ---- ---- Page orpheline du livre : Les moteurs de rendu des FPS en 2.5 D Bonjour, Le livre "Les moteurs de rendu des FPS en 2.5 D" a laissé quelques pages orphelines. Ces pages orphelines ont été recopiées ou regroupé dans le texte du livre. Elles font donc doublons. La page orpheline : * [[Les moteurs de rendu des FPS en 2.5 D\Sommaire|Les moteurs de rendu des FPS en 2.5 D Sommaire ]] a été copiée ici: * [[Les moteurs de rendu des FPS en 2.5 D|Les moteurs de rendu des FPS en 2.5 D]] ---- ---- ---- ---- Page orpheline du livre : LispWorks CAPI Bonjour, Le livre "LispWorks CAPI" a laissé quelques pages orphelines. Ces pages orphelines ont été recopiées ou regroupé dans le texte du livre. Elles font donc doublons. La page orpheline : * [[LispWorks CAPI/Créer une simple fenêtre/Propriétés génériques|Propriétés génériques ]] a été copiée ici: * [[LispWorks/Créer une simple fenêtre/Propriétés génériques|Propriétés génériques]] ---- ---- ---- ---- Page orpheline du livre : Jardin au naturel Bonjour, Le livre "Jardin au naturel" a laissé quelques pages orphelines. Ces pages orphelines ont été recopiées ou regroupé dans le texte du livre. Elles font donc doublons. La page orpheline : * [[Jardin au naturel/Comment aménager un jardin au naturel ?/Faire vivre le sol du jardin|Faire vivre le sol du jardin ]] a été copiée ici: * [[Jardin au naturel/Comment entretenir un jardin au naturel ?/Faire vivre le sol du jardin| Faire vivre le sol du jardin]] ---- ---- ---- ---- Page orpheline du livre : Programmation Bash Bonjour, Le livre "Programmation Bash" a laissé quelques pages orphelines. Ces pages orphelines ont été recopiées ou regroupé dans le texte du livre. Elles font donc doublons. La page orpheline : * [[Programmation Bash/Redirections|Programmation Bash/Redirections ]] a été copiée ici: * [[Programmation Bash/Flux et redirections|Programmation Bash/Flux et redirections]] ---- ---- ---- ---- Page orpheline du livre : Tribologie Bonjour, Le livre "Tribologie" a laissé quelques pages orphelines. Ces pages orphelines ont été recopiées ou regroupé dans le texte du livre. Elles font donc doublons. La page orpheline : * [[Tribologie/Applications pratiques/Soudage par friction|Soudage par friction ]] a été copiée ici: * [[Soudage/Soudage par friction|Soudage par friction]] ---- ---- ---- ---- Page orpheline du livre : Recherches sur les naissances « physiologique » et « naturelle » Bonjour, Le livre "Recherches sur les naissances « physiologique » et « naturelle »" a laissé quelques pages orphelines. Ces pages orphelines ont été recopiées ou regroupé dans le texte du livre. Elles font donc doublons. La page orpheline : * [[Recherches sur les naissances "physiologique" et "naturelle"/Sommaire/Questions sur les hormones|Questions sur les hormones ]] a été copiée ici: * [[Recherches sur les naissances « physiologique » et « naturelle »/Questions sur les hormones|Questions sur les hormones]] ---- ---- ---- ---- Page orpheline du livre : L'éco-construction en Nord-Pas-de-Calais Bonjour, Le livre "L'éco-construction en Nord-Pas-de-Calais" a laissé quelques pages orphelines. Ces pages orphelines ont été recopiées ou regroupé dans le texte du livre. Elles font donc doublons. La page orpheline : * [[L'éco-construction en Nord-Pas-de-Calais/Partie 1|Partie 1 ]] a été copiée ici: * [[L'éco-construction en Nord-Pas-de-Calais/Partie1|Partie1]] La page orpheline : * [[L'éco-construction en Nord-Pas-de-Calais/Partie 2/L'habitat individuel|L'habitat individuel]] a été copiée ici: * [[L'éco-construction en Nord-Pas-de-Calais/Partie2/L'habitat individuel|L'habitat individuel]] La page orpheline : * [[L'éco-construction en Nord-Pas-de-Calais/Partie 3|Partie 3]] a été copiée ici: * [[L'éco-construction en Nord-Pas-de-Calais/Partie3/Des systèmes constructifs innovants|Partie3 Des systèmes constructifs innovants]] La page orpheline : * [[L'éco-construction en Nord-Pas-de-Calais/Partie 5|Partie 5]] a été copiée ici: * [[L'éco-construction en Nord-Pas-de-Calais/Partie1/L'architecture vernaculaire|L'architecture vernaculaire]] ---- ---- ---- ---- Page orpheline du livre : Initiatives éco-citoyennes Bonjour, Le livre "Initiatives éco-citoyennes" a laissé quelques pages orphelines. Ces pages orphelines ont été recopiées ou regroupé dans le texte du livre. Elles font donc doublons. La page orpheline : * [[Initiatives éco-citoyennes/Initiatives|Initiatives ]] a été copiée ici: * [[Initiatives éco-citoyennes/Enercoop|Enercoop]] La page orpheline : * [[Initiatives éco-citoyennes/Initiatives sur l'énergie|Initiatives sur l'énergie]] a été copiée ici: * [[Initiatives éco-citoyennes/Enercoop|Enercoop]] La page orpheline : * [[Initiatives éco-citoyennes/Enerpartagé|Enerpartagé]] a été copiée ici: * [[Initiatives éco-citoyennes/Les Coopératives solaires : Zoom sur "Énergies partagées"|Les Coopératives solaires : Zoom sur "Énergies partagées"]] La page orpheline : * [[Initiatives éco-citoyennes/Contexte et enjeux en matière d'énergie et de déchets|Contexte et enjeux en matière d'énergie et de déchets]] a été copiée ici: * [[Initiatives éco-citoyennes/Contexte et enjeux en matière d'énergie|Contexte et enjeux en matière d'énergie]] * [[Initiatives éco-citoyennes/Contexte et enjeux en matière de déchets|Contexte et enjeux en matière de déchets]] ---- ---- ---- ---- Page orpheline du livre : Pôles d'éco-citoyenneté/ Bonjour, Le livre "Pôles d'éco-citoyenneté/" a laissé quelques pages orphelines. Ces pages orphelines ont été recopiées ou regroupé dans le texte du livre. Elles font donc doublons. La page orpheline : * [[Pôles d'éco-citoyenneté/D'autres lieux de conseil sur l'environnement dans le Nord-Pas-de-Calais|D'autres lieux de conseil sur l'environnement dans le Nord-Pas-de-Calais ]] a été copiée ici: * [[Pôles d'éco-citoyenneté/Les principaux Pôles d'éco-citoyenneté dans le Nord-Pas-de-Calais/D'autres lieux de conseil sur l'environnement dans le Nord-Pas-de-Calais|D'autres lieux de conseil sur l'environnement dans le Nord-Pas-de-Calais]] La page orpheline : * [[Pôles d'éco-citoyenneté/Les principaux Pôles d'éco-citoyenneté dans le Nord-Pas-de-Calais/Les Maisons de l'environnement|Les Maisons de l'environnement ]] a été copiée ici: * [[Pôles d'éco-citoyenneté/Les principaux Pôles d'éco-citoyenneté dans le Nord-Pas-de-Calais/Les Maisons de l'Environnement/La Maison de l'Environnement de Dunkerque|La Maison de l'Environnement de Dunkerque]] La page orpheline : * [[Pôles d'éco-citoyenneté/Les principaux Pôles d'éco-citoyenneté dans le Nord-Pas-de-Calais/Eden 62 et la réserve naturelle du Romelaëre|Eden 62 et la réserve naturelle du Romelaëre ]] a été copiée ici: * [[Pôles d'éco-citoyenneté/Les principaux Pôles d'éco-citoyenneté dans le Nord-Pas-de-Calais/EDEN 62 et la Réserve Naturelle du Romelaëre|EDEN 62 et la Réserve Naturelle du Romelaëre]] La page orpheline : * [[Pôles d'éco-citoyenneté/Les principaux Pôles d'éco-citoyenneté dans le Nord-Pas-de-Calais/La Maison du Terril|La Maison du Terril]] a été copiée ici: * [[Pôles d'éco-citoyenneté/Les principaux Pôles d'éco-citoyenneté dans le Nord-Pas-de-Calais/La maison du Terril|La maison du Terri]] ---- ---- ---- ---- Page orpheline du livre : Inventer les territoires culturels de demain Bonjour, Le livre "Inventer les territoires culturels de demain" a laissé quelques pages orphelines. Ces pages orphelines ont été recopiées ou regroupé dans le texte du livre. Elles font donc doublons. La page orpheline : * [[Inventer les territoires culturels de demain/Les publics|Les publics ]] a été copiée ici: * [[Inventer les territoires culturels de demain|Inventer les territoires culturels de demain]] (voir enjeux des publics) ---- ---- ---- ---- Page orpheline du livre : Manuel de psychopathologie et psychothérapie intégrative Bonjour, Le livre "Manuel de psychopathologie et psychothérapie intégrative" a laissé quelques pages orphelines. Ces pages orphelines ont été recopiées ou regroupé dans le texte du livre. Elles font donc doublons. Les pages orphelines : * [[Manuel de psychopathologie et psychothérapie intégrative/Dictionnaire des outils conceptuels en psychothérapie intégrative|Dictionnaire des outils conceptuels en psychothérapie intégrative ]] * [[Manuel de psychopathologie et psychothérapie intégrative/ Dictionnaire des outils conceptuels en psychothérapie intégrative|Dictionnaire des outils conceptuels en psychothérapie intégrative]] a été copiée ici: * [[Manuel de psychopathologie et psychothérapie intégrative/ Dictionnaire des outils et concepts en psychothérapie intégrative|Dictionnaire des outils et concepts en psychothérapie intégrative]] ---- ---- ---- ---- Page orpheline du livre : Pratique médicale en Afrique francophone Bonjour, Le livre "Pratique médicale en Afrique francophone" a laissé quelques pages orphelines. Ces pages orphelines ont été recopiées ou regroupé dans le texte du livre. Elles font donc doublons. La page orpheline : * [[Pratique médicale en Afrique francophone/Niger|Niger]] est un commentaire sur la page suivante a été copiée ici: * [[Pratique médicale en Afrique francophone/Les pratiques médicales au Niger et télémédecine|Les pratiques médicales au Niger et télémédecine]] ---- ---- ---- ---- Page orpheline du livre : Les suites et séries Bonjour, Le livre "Les suites et séries" a laissé quelques pages orphelines. Ces pages orphelines ont été recopiées ou regroupé dans le texte du livre. Elles font donc doublons. La page orpheline : * [[Les suites et séries/Les suites arithmético-géométriques|Les suites arithmético-géométriques]] a été copiée ici: * [[Les suites et séries/Les suites numériques|Les suites numériques]] ---- ---- ---- ---- Page orpheline du livre : Les opérations bit à bit Bonjour, Le livre "Les opérations bit à bit" a laissé quelques pages orphelines. Ces pages orphelines ont été recopiées ou regroupé dans le texte du livre. Elles font donc doublons. La page orpheline : Les opérations bit à bit * [[Les opérations bit à bit/Calculs flottants|Calculs flottants]] a été copiée ici: * [[Les opérations bit à bit/Les opérations arithmétiques sur des flottants|Les opérations arithmétiques sur des flottants]] ---- ---- ---- ---- Page orpheline du livre : Systèmes sensoriels Bonjour, Le livre "Systèmes sensoriels" a laissé quelques pages orphelines. Ces pages orphelines ont été recopiées ou regroupé dans le texte du livre. Elles font donc doublons. La page orpheline : * [[Systèmes sensoriels/Système Gustatif|Système Gustatif]] a été copiée ici: * [[Systèmes sensoriels/Système gustatif|Système gustatif]] La page orpheline : * [[Systèmes sensoriels/Système Olfactif|Système Olfactif]] a été copiée ici: * [[Systèmes sensoriels/Système olfactif|Système olfactif]] La page orpheline : * [[Systèmes sensoriels/modèle:navigation|modèle:navigation]] Ancienne version a été copiée ici: * [[Systèmes sensoriels|Systèmes sensoriels]] La page orpheline : * [[Systèmes sensoriels/Table des matières|Table des matières]] Ancienne version a été copiée ici: * [[Modèle:Systèmes sensoriels/Table des matières|Modèle:Systèmes sensoriels/Table des matières]] ---- ---- ---- ---- Page orpheline du livre : Wikigreen Bonjour, Le livre "Wikigreen" a laissé quelques pages orphelines. Ces pages orphelines ont été recopiées ou regroupé dans le texte du livre. Elles font donc doublons. La page orpheline : * [[Wikigreen/Le compost|Le compost]] a été copiée ici: * [[Wikigreen/le-compost|le-compost]] La page orpheline : * [[Wikigreen/vehicule-electrique|vehicule-electrique]] a été copiée ici: * [[Wikigreen/Véhicule électrique|Véhicule électrique]] ---- ---- ---- ---- Page orpheline du livre : Néerlandais Bonjour, Le livre "Néerlandais" a laissé quelques pages orphelines. Ces pages orphelines ont été recopiées ou regroupé dans le texte du livre. Elles font donc doublons. La page orpheline: * [[Néerlandais/Pour adultes/Leçon 3 : Être et avoir/quiz|Leçon 3 : Être et avoir quiz]] a été copié ; * [[Néerlandais/Pour adultes/Progressons pas à pas/Leçon 3 : Être et avoir/quiz|Leçon 3 : Être et avoir quiz]] La page orpheline: * [[Néerlandais : la maison : vocabulaire illustrée : les pièces (correction)|la maison : vocabulaire illustrée : les pièces (correction)]] a été copié ; * [[Néerlandais : la maison : vocabulaire illustré : les pièces (correction)|la maison : vocabulaire illustré : les pièces (correction)]] La page orpheline: * [[Néerlandais dans le secondaire/les vacances : vocabulaire|les vacances : vocabulaire]] a été copié ; * [[Néerlandais/Dans le secondaire/les vacances/Vocabulaire|les vacances Vocabulaire]] ---- ---- ---- ---- Page orpheline du livre : Photographie Bonjour, Le livre "Photographie" a laissé quelques pages orphelines. Ces pages orphelines ont été recopiées ou regroupé dans le texte du livre. Elles font donc doublons. La page orpheline: * [[Photographie/Fabricants/Sony/Sony DT 16-105 mm f/3,5-5,6 SAL|Sony DT 16-105 mm f/3,5-5,6 SAL]] a été copié ; * [[Photographie/Fabricants/Sony/Sony DT 16-105 mm f/3,5-5,6 SAL-16105|Sony DT 16-105 mm f/3,5-5,6 SAL-16105]] La page orpheline: * [[Photographie/Fabricants/Zeiss Ikon/Zeiss Ikon Contessa Matic|Zeiss Ikon Contessa Matic]] a été copié ; * [[Photographie/Fabricants/Zeiss Ikon/Zeiss Ikon Contessamatic|Zeiss Ikon Contessamatic]] ---- ---- ---- ---- Page orpheline du livre : Livre de cuisine Bonjour, Le livre "Livre de cuisine" a laissé quelques pages orphelines. Ces pages orphelines ont été recopiées ou regroupé dans le texte du livre. Elles font donc doublons. La page orpheline: * [[Livre de cuisine/Singapour sling|Singapour sling]] * [[Flamusse bourguignonne]] a été copié ; * [[Livre de cuisine/Boissons/Cocktails/Singapour sling|Singapour sling]] * [[Livre de cuisine/Flamusse bourguignonne|Flamusse bourguignonne]] ---- ---- ---- ---- Page orpheline du livre : Guide du MOOC Bonjour, Le livre "Guide du MOOC" a laissé quelques pages orphelines. Ces pages orphelines ont été recopiées ou regroupé dans le texte du livre. Elles font donc doublons. La page orpheline: * [[Comment rendre un MOOC accessible pour les mobiles|Comment rendre un MOOC accessible pour les mobiles]] a été copié ; * [[Guide du MOOC/Accessible|Accessible]] ---- ---- ---- ---- [[Catégorie:Feuilles volantes|*]] ask8mbzkb0htvytiynynacosqfnk7zk Fonctionnement d'un ordinateur/Les ISA optimisés pour la compilation/interprétation 0 83651 764004 763898 2026-04-19T15:02:53Z Mewtow 31375 /* L'accélération de l'interprétation/JIT */ 764004 wikitext text/x-wiki De nos jours, la majorité des programmeurs programment dans des langages de haut niveau. Il est très rare pour eux d'avoir à utiliser de l'assembleur, et encore moins un langage machine. Les programmes écrits dans un langage de haut niveau sont traduits en langage machine par un logiciel appelé le compilateur. [[File:Compilation.PNG|centre|vignette|upright=2.5|Compilateur : principe.]] Il se trouve que quelques rares processeurs sont conçus pour faciliter le travail du compilateur. Et ce sont ces processeurs que nous allons voir dans ce qui suit. Ils représentent une catégorie de jeux d'instruction à part, qui n'a pas vraiment de nom. ==La compilation et l’interprétation== Un compilateur traduit donc du code source, écrit dans un langage de haut niveau, vers du langage machine. La traduction est rarement directe. En général, le compilateur traduit le code source en assembleur, qui est lui-même traduit en langage machine. L'assembleur est une représentation textuelle du langage machine alors que le code machine est du binaire exécutable par le processeur. La traduction se fait donc en deux étapes, la '''compilation''' proprement dite et l''''assemblage''', réalisées respectivement par un compilateur et un assembleur. Il faut parfois rajouter une troisième phase d'édition des liens, que nous passons volontairement sous silence. {| |[[File:Code C.png|vignette|200px|Le code source, ici en C.]] |[[File:Assem.png|vignette|250px|Le code assembleur.]] |[[File:Binary file - hello world (C programming).png|vignette|150px|Le code machine.]] |} ===L'interprétation et la compilation à la volée=== Une alternative à la compilation est l''''interprétation''', qui transforme le code source en code machine à la volée. Avec l'interprétation, le code source est passé à un logiciel appelé l''''interpréteur''', qui exécute le code source ligne de code par ligne de code. Une ligne de code est traduite en code machine, qui est exécutée, puis l'interpréteur passe à la ligne suivante. [[File:Kaantotulkkaus.png|centre|vignette|upright=3|Compilation versus interprétation.]] Un défaut de l'interprétation est son cout en performance, qui est assez important. Aussi, l'interprétation stricte a évolué vers un hybride entre interprétation et compilation. L'idée est qu'une partie peu importante du code source est interprétée, alors que le code important est compilé juste avant d'être exécuté. Pour le dire autrement, le code source est partiellement compilé à la volée, juste avant son exécution. Aussi, on parle de '''compilation à la volée'''. Le terme en anglais est ''Just In Time compilation'', abrévié en JIT. En soi, le JIT est associé à de l'interprétation, les JIT sont en réalité des hybrides interpréteurs-compilateurs. Un point important est que compiler du code à la volée est assez lourd, cela a un cout en performance. Par contre, le code compilé s'exécute plus vite que du code interprété. Si on exécute le code compilé une seule fois, le cout de la compilation l'emporte sur le gain à l'exécution. Mais le code compilé est réutilisé autant de fois que nécessaire. Le gain à l'exécution est donc multiplié par le nombre d'exécutions, alors que le cout de la compilation est répartit sur plusieurs exécutions, il est amorti. Les portions du code sont choisies de manière à avoir un gain en performance maximal. L'idée générale est que le code exécuté très souvent est compilé, alors que du code exécuté pas souvent est interprété. Typiquement, les boucles les plus souvent exécutées sont compilées, le code exécuté une seule fois est interprété. ===Les langages intermédiaires=== Les compilateurs modernes passent par un '''langage intermédiaire''' pour faire la transformation en code machine. Le compilateur traduit le langage de haut niveau en langage intermédiaire, puis traduit le langage intermédiaire en code machine cible. Le langage intermédiaire est parfois appelé le '''''bytecode''''', et ce terme recouvre aussi le code écrit avec du ''bytecode''. L'interprétation aussi peut passer par l'intermédiaire d'un un ''bytecode'', ce n'est pas limité aux compilateurs proprement dit. {| |[[File:Intermediate representation scheme.png|centre|vignette|upright=1.5|Représentation intermédiaire d'un compilateur.]] |[[File:Code interpreter scheme.png|centre|vignette|upright=1.5|Fonctionnement d'un interpréteur.]] |} Faire ainsi a de nombreux avantages, le principal étant d'avoir un compilateur capable de traduire un langage de haut niveau vers plusieurs jeux d'instructions différents. Cela permet d'avoir, par exemple, un compilateur qui traduit du C soit en code machine x86, soit en code machine pour un CPU ARM, soit pour un CPU POWERPC, etc. [[File:Compiler design.svg|centre|vignette|upright=3|Compiler design]] Pour l'interprétation, l'usage d'un ''bytecode'' a d'autres avantages. Pour l'exploiter, il faut distribuer non pas le code source, mais le ''bytecode''. En clair, l'interpréteur ne prend pas en entrée le code source, mais du ''bytecode'' déjà compilé. La traduction est alors beaucoup plus simple, car le code source a déjà été partiellement compilé, par la traduction en ''bytecode''. Interpréter du code source est en effet assez compliqué : il faut effectuer des étapes d'analyse lexicale, sémantique, et bien d'autres. Avec du ''bytecode'', ces étapes ont été réalisées lors de la compilation du ''bytecode'', l'interpréteur a alors peut de choses à faire. L'interprétation du ''bytecode'' se fait instruction par instruction, au niveau du ''bytecode''. Une instruction du ''bytecode'' est traduite en une instruction machine équivalente, qui est exécutée. Si l'instruction est un peu complexe, il exécute une fonction/procédure qui fait la même chose. Pour résumer, le ''bytecode'' est ensuite traduit à la volée et exécuté instruction par instruction par un logiciel appelé l'interpréteur. [[File:Control table.png|centre|vignette|upright=2|Control table]] Le langage intermédiaire peut être vu comme l'assembleur d'un processeur, n'existe pas forcément dans la réalité, mais dont le jeu d'instruction est décrit en détail. Le processeur en question est appelé une '''machine abstraite''', ou encore une ''machine virtuelle''. Nous utiliserons le terme de machine abstraite dans ce qui suit. La machine abstraite n'est pas la même suivant que l'on cible la compilation ou l'interprétation/JIT. Pour l'interprétation/JIT, la machine abstraite est souvent une machine à pile, car cela simplifie grandement la traduction en code machine final. L'interprétation demande que la traduction du ''bytecode'' en code machine soit la plus simple possible. Et cela demande de prendre en compte pas mal de détails. Par exemple, les différents jeux d'instruction existants n'ont pas le même nombre de registres, ce qui pose problème lors de la traduction du ''bytecode''. Lors de la transformation en code machine, un algorithme d'allocation de registres se débrouille pour traduire le code intermédiaire en code qui utilise un nombre limité de registres. Pour améliorer cette allocation de registres, il y a deux solutions niveau ''bytecode'' : soit utiliser un nombre illimité de registres, soit utiliser une machine à pile. Pour les machines à pile, il existe un algorithme simple et rapide pour traduire un code écrit pour une machine à pile en un code écrit pour un processeur avec des registres, qui se débrouille pas trop mal pour allouer efficacement les registres. C'est un avantage assez important pour les langages interprétés. ==Les processeurs qui exécutent du bytecode== Le ''bytecode'' est un code machine, ce qui signifie qu'il peut en théorie s'exécuter sur un processeur qui implémente le jeu d’instruction associé. Si le jeu d'instruction d'un ''bytecode'' est souvent une description censée être fictive, elle n'en reste pas moins un jeu d'instruction et des caractéristiques précises, qu'on peut l'implémenter en matériel ! Néanmoins, tous les ''bytecode'' ne sont pas égaux de ce point de vue. Certains sont faciles à implémenter en matériel, d'autres non. Les ''bytecodes'' implémentés en matériel sont ceux dont les machines abstraites sont des machines à pile. Un premier exemple est celui des processeurs Pascal MicroEngine, qui exécutaient directement le bytecode du langage Pascal, le fameux UCSD P-code. Un second exemple est la '''machine SECD''', qui sert de langage intermédiaire pour certains compilateurs de langages fonctionnels. Elle a été implémentée en matériel par plusieurs équipes, notamment par les chercheurs de l'université de Calgary en 1989. Dans le même genre, quelques processeurs simples étaient capables d’exécuter directement le ''bytecode'' du langage FORTH. Le FORTH, un des premiers langages à pile de haut niveau, possède de nombreuses implémentations hardware et est un des rares langages de haut niveau dont le ''bytecode'' a été utilisé comme langage machine sur certains processeurs. Par exemple, on peut citer le processeur FC16, capable d’exécuter nativement du ''bytecode'' FORTH. ===Les processeurs Java=== Le cas le plus impressionnant est celui de la machine virtuelle Java, qui est un design de processeur comme un autre. En temps normal, le ''bytecode'' Java est compilé ou interprété, mais certains processeurs exécutaient du ''bytecode'' Java directement, sans interprétation ni compilation. Ils sont appelés des '''processeurs Java'''. On peut citer les processeurs ARM disposant de l'extension Jazelle, du JEMCore et du aJ-100 de aJile Systems, le picoJava II de Sun, et quelques autres. Il y a aussi eu quelques projets de recherche ou processeurs open source, comme Komodo, jamuth, le ''Java Optimized Processor'' et quelques autres projets du même genre. L'implémentation de la machine virtuelle Java n'était cependant pas complète. Quelques instructions complexes n'étaient pas gérées par le processeur et devaient être émulées en logiciel. Mais la grosse majorité des instructions l'était. Les instructions non-supportées étaient émulées avec la méthode ''trap and emulate'', à savoir qu'un opcode inconnu déclenchait une exception matérielle ''opcode invalide'', dont la routine pouvait émuler les instructions qui doivent l'être. Le processeur aJ-100 de aJile est capable d'exécuter la quasi-totalité des instructions de la JVM Java, seules deux instructions faisant exception. Les '''accélérateurs Java''' sont une solution intermédiaire, qui permet de traduire à la volée du ''bytecode'' Java en instructions RISC, exécutées sur un autre processeur. Il s'agit formellement de traduction binaire, mais je préfère en parler ici. L'idée est de combiner un processeur RISC avec un coprocesseur Java. Le coprocesseur lit le ''bytecode'' Java et le traduit en instructions RISC, ces dernières étant exécutées sur le processeur RISC. Le JA108 de Nozomi était un coprocesseur de ce genre, au même titre que le coprocesseur JSTAR de JEDI. L'extension Jazelle de certains CPU ARM fonctionnait vraisemblablement sur un principe similaire, sauf que le coprocesseur était intégré dans le processeur ARM, entre le cache d'instruction et le décodeur d'instructions ARM. L'intérêt des processeurs Java n'est pas qu'une question de performance. Certes, ils permettent d’exécuter plus vite les programmes compilés en ''bytecode'', comme des programmes Java pour la JVM Java. Mais l'intérêt est aussi de faire des économies de mémoire RAM et ROM. Je rappelle que la machine virtuelle Java est une machine à pile, ce qui fait que sa densité de code est excellente. Les programmes en ''bytecode'' Java sont donc très petits, et prennent peu de place en mémoire ROM. De plus, utiliser un processeur Java permet de se passer d'interpréteur ou de JIT Java, ce qui économise encore plus de mémoire. ===Transformer un processeur RISC en machine à pile=== La plupart des processeurs Java utilisent en interne une architecture à registre généraux, qui émule une machine à registre en microcode. Quelques registres sont réservés pour l'état de la machine virtuelle, avec au minimum un registre réservé pour le pointeur de pile, un autre pour un pointeur vers un ''pool'' de constantes, et quelques autres. D'autres registres servent à mémoriser le haut de la pile, à savoir le sommet de la pile et quelques opérandes situées juste en-dessous. Par exemple, le processeur picoJava II gardait les 64 opérandes au sommet de la pile dans 64 registres généraux, séparés des registres pour les pointeurs et l'état de la JVM. Une optimisation possible, envisagée sur les processeurs picoJava, était une sorte de macro-fusion sous stéroïde. L'idée était de fusionner certaines séries d'instructions Java en une seule instruction machine. Typiquement, pour faire une opération arithmétique, une machine à pile empile deux opérandes et exécute une opération, ce qui prend trois instructions machine. La spécification picoJava 1 a proposé d'envoyer directement la seconde opérande en entrée de l'ALU, ce qui fusionnait l'empilement de la seconde opérande et l'opération arithmétique. Le processeur picoJava II va plus loin et fusionne les trois instructions en une seule instruction RISC, agissant sur des registres. Mais la majorité des processeurs Java ne faisaient pas cela, car cela complexifie grandement le décodeur. Les processeurs ''bigfoot'' de DCT utilisaient un système différent pour émuler une pile avec des registres. Leur banc de registre de 64 bits était coupé en deux : les 16 premiers registres étaient utilisés pour la pile, les 48 suivants étaient utilisés pour autre chose. L'idée est que le registre 0 mémorise un emplacement vide, le registre 1 contient le sommet de la pile, le registre 2 l'opérande sous le sommet de la pile, et ainsi de suite. Les 15 premières opérandes de la pile sont donc mémorisées dans les registres. Les instructions écrivent leur résultat dans le registre adéquat, puis tous les registres sont décalés pour que le résultat passe dans le registre R1. Par exemple, les instructions PUSH chargent une opérande dans le registre 0, puis tous les registres sont décalés de un rang, pour que l'opérande chargée soit dans le registre 1. Le registre 0 devient alors le registre numéro 1, le registre 1 devient le 2, et ainsi de suite. Par contre, les opérations arithmétiques font autrement. Une instruction d'addition, par exemple, lit les opérandes dans le registre 1 et 2, puis place son résultat dans le registre 2 (les deux opérandes sont dépilées). Puis, les registres sont décalés d'un rang pour que ce registre 2 devienne le 1. Les registres ne sont en réalité pas décalés, il n'y a pas de transferts entre registres. A la place, les 16 premiers registres utilisaient une forme limitée de renommage de registres, similaire au banc de registre tournant utilisé sur les processeurs EPIC comme l'Itanium. Les numéros de registres sont renommés, décalés, à chaque fois qu'on empile ou dépile une opérande. Pour faire le renommage de registres, processeur contient un ''register stack counter'' de 4 bits, qui est incrémenté à chaque fois qu'on empile une donnée, et est décrémenté quand on dépile une donnée. Quand on veut accéder à un opérande, le numéro de registre architectural associé est traduit en numéro de registre physique, en additionnant le ''register stack counter'' (modulo 16, pour rester sur 4 bits). ==L'accélération de l'interprétation/JIT : le ''Thumb-EE'' d'ARM== Quelques processeurs ont ajouté des instructions pour faciliter le travail des interpréteurs/JIT. Par exemple, en 2005, ARM a ajouté le mode ''Thumb Execution Environment'', qui faisait cela. Il reprenait le jeu d'instruction compact ''thumb'' et ajoutait quelques instructions et modifiait le comportement d'instructions existantes. Le jeu d'instruction ''Thumb-EE'' a été ajouté sur les CPU ARM en 2005, mais a été déprécié en 2011, par manque d'utilité. Mais la tentative mérite qu'on s'attarde dessus. Les CPU de gamme M, déstinés à l'embarqué, n'ont jamais supporté ''Thumb-EE'', vu que de tels processeurs ne sont pas conçus pour exécuter du code interprété/JIT. Le mode ''Thumb-EE'' est un mode d'exécution, séparé de l'ARM normal. Ainsi, une instruction ''Thumb-EE'' et une instruction ARM peuvent avoir le même encodage en binaire, mais se comporter différemment. Le processeur est à tout moment soit en mode ''Thumb-EE'', soit en mode ARM normal, ce qui précise comment ces instructions doivent se comporter. Des instructions ont été ajoutées par ''Thumb-EE'', pour qu'il fasse son travail. L'entrée et la sortie du mode ''Thumb-EE'' se fait avec deux instructions : ENTERX et LEAVEX. Une fois en mode ''Thumb-EE'', le décodage des instructions ''thumb'' se fait avec les règles du ''Thumb-EE'', et non celles du ''thumb'' normal. Une première différence entre ''thumb'' normal et ''Thumb-EE'' est l'ajout de ce qui s'appelle un ''null check'' pour les instructions mémoire. Le ''null check'' est utilisé pour les instructions en mode d'adressage "base + indice" ou "Base + décalage", ou tout autre mode d'adressage avec un registre de base utilisés dans des calculs d'adresse. L'idée est de vérifier si l'adresse de base vaut zéro ou non. Si c'est le cas, le processeur lève une exception matérielle, qui est traitée par l'interprétation ou le compilateur JIT. Une telle situation est en effet signe d'une erreur d'adressage mémoire, qui doit être traitée par l'interpréteur. Typiquement, c'est signe qu'un pointeur n'a pas bien été initialisé, ce qui peut arriver avec du code interprété ou JIT. Les interpréteurs/JIT traitent généralement la situation en ajoutant un test avant toute instruction mémoire, pour vérifier si le pointeur accédé vaut zéro ou non. Avec ''Thumb-EE'', pas besoin d'ajouter les tests en questions : ils sont réalisés automatiquement lors de chaque instruction mémoire. : Les registres pour la pile, comme le pointeur de pile et le pointeur de ''frame'', sont aussi concernés. L'instruction CHKA vérifie que les accès à un tableau ne débordent pas en dehors du tableau en question. Par exemple, pour un tableau de 1024 éléments, elle vérifie si l'indice est compris entre 0 et 1024. Pour cela, elle vérifie si l'indice de tableau est bien dans l'intervalle adéquat et lève une exception matérielle si ce n'est pas le cas. Son utilité se comprend quand on sait que tous les accès à un tableau sont vérifiés par l'interpréteur. L'interpréteur est censé ajouter des instructions pour vérifier les indices, à savoir deux ou trois branchements. Avec CHKA, le test se fait en une seule instruction. D'autres modifications mineures de l'encodage des instructions sont aussi présentes, ainsi que des modifications pour les instructions LOAD/STORE. Précisément, dans les modes "base + indice" et "base + décalage", le décalage et l'indice subissent maintenant des décalages. L'idée est de simplifier le calcul d'adresse. De quoi économiser quelques instructions lors des calculs d'adresse, ce qui facilite le travail de l'interpréteur/JIT. Et des instructions LOAD/STORE précises voient leur comportement modifié pour faciliter la gestion de la pile de Java ou l'exécution des méthodes locales. Pour faciliter le travail de l'interpréteur, deux instructions HBP et HBLP ont été ajoutées. Elles branchent vers une fonction qui gère les exceptions logicielles (une fonctionnalité présente dans de nombreux langages, comme Java). La première effectue un branchement vers la fonction en question, la seconde sauvegarde l'adresse de retour avant de faire ce branchement. Les deux peuvent brancher vers 256 fonctions pré-déterminées, sans avoir à présenter leur adresse. ==La traduction binaire accélérée par le matériel== Plus haut, nous avons surtout parlé des compilateurs et des interpréteurs. Cependant, nous devons aussi parler de la '''traduction binaire'''. Elle traduit un programme écrit dans un code machine vers un autre code machine. Par exemple, elle traduit un programme compilé pour un CPU x86 vers un code machine ARM. En général, le système d'exploitation est généralement compilé pour le jeu d'instruction natif, mais il exécute des applications prévues pour le x86. Les applications sont traduites par le système d'exploitation, avant d'exécuter le code traduit. : Nous parlerons dans la suite de code machine source et de code machine cible, pour parler respectivement du code à traduire et du code obtenu après traduction. Idem avec d'autres termes comme architecture cible/source, ou jeu d'instruction cible/source. La traduction binaire est surtout utilisée pour des questions d'émulation ou de compatibilité. Par exemple, lorsque les Macintosh sont passés de processeurs Power PC vers des processeurs x86, le système d'exploitation Mac OS utilisait la traduction binaire pour convertir les anciennes applications Power PC vers du code x86. Les utilisateurs n'y ont vu que du feu. Le système de traduction binaire était appelé Rosetta 1. Par la suite, lors de la transition de processeurs x86 vers des processeurs Apple, Rosetta 2 a vu le jour. La traduction binaire a surtout été utilisée pour traduire du code x86 vers un autre jeu d'instruction. Il faut dire que le x86 est le jeu d'instruction dominant. De nombreuses entreprises ont eu pour ambition de briser l'hégémonie du x86 sur PC, en remplaçant le x86 par un jeu d'instruction plus performant, tout en gardant une compatibilité maximale. La traduction binaire était la seule solution pratique. Elle était le plus souvent intégralement réalisée en logiciel, comme c'était le cas sur les architectures Itanium avec le ''IA-32 Execution Layer''. ===Les généralités sur la traduction binaire assistée en matériel=== L'architecture source est presque tout le temps une architecture CISC, assez ancienne, qu'on souhaite émuler. Le choix de l'architecture cible se porte souvent sur une architecture VLIW, fort différente de l'architecture source, et ce pour obtenir une économie de matériel conséquente. Pour s'exécuter rapidement, le code traduit doit exploiter le parallélisme d'instruction, à savoir exécuter plusieurs instructions en même temps dans des unités de calcul séparées. Il est possible d'utiliser un processeur superscalaire avec exécution dans le désordre pour cela, mais au prix d'un cout important en transistors. Alors qu'en utilisant un CPU VLIW, c'est le traducteur binaire qui fait tout le travail d'extraction du parallélisme d'instruction. Et après tout, quitte à avoir un traducteur binaire, autant lui refiler le boulot d'optimisation. Compiler du code à la volée est certes assez lourd, mais qu'il existe des algorithmes efficaces pour regrouper des instructions indépendantes dans une seule instruction VLIW. Au passage, le processeur VLIW a plus de registres que le processeur source. Cela permet de faire du renommage de registres directement en logiciel. Le traducteur binaire n'hésite pas à changer les noms de registres entre instructions source et instruction VLIW cible, afin de supprimer des dépendances de données. En conséquence, cela ouvre des opportunités de parallélisme, qui sont exploitées lors du regroupement des opérations en instructions VLIW. Vous pourriez penser que le choix d'un CPU VLIW pour émuler du CISC est tout sauf optimal. Vous devez penser que la traduction binaire est d'autant plus simple que l'architecture source et cibles sont semblables. Dans les faits, ce n'est pas tellement traduire les instructions qui pose problème, mais plus la gestion du registre d'état, des exceptions matérielles, de la mémoire virtuelle, la différence entre gros-boutisme et petit-boutisme, de même que des différences d'adressage pour les périphériques. Le premier problème est la gestion des conditions, notamment en présence d'un registre d'état. En pratique, la traduction binaire s'utilise pour traduire du code CISC vers du code RISC ou VLIW. Les architectures source ont donc un registre d'état, qui est mis à jour non seulement par des instructions de test, mais aussi des instructions arithmétiques. L'architecture cible n'a elle pas de registre d'état, mais des registres à prédicats. La traduction de l'un vers l'autre est alors quelque peu compliquée. Et elle est d'autant plus compliquée que le registre d'état est la source de dépendances d'instruction implicites, qui réduisent les performances. Par exemple, si une instruction arithmétique modifie le registre d'état, cela peut impacter l'exécution d'une instruction ultérieure, qui lit ce registre d'état. Pour éliminer ces fausses dépendances, le traducteur binaire doit renommer ce registre en logiciel et détecter les dépendances utiles. Et le compilateur doit gérer les cas où il y a beaucoup de distance entre les deux, voire les cas où l'instruction dépendante n'a pas encore été analysée par le compilateur. Un autre problème est lié à la gestion des exceptions matérielles, et précisément des exceptions précises. Pour rappel, une instruction VLIW regroupe plusieurs opérations, opérations qui correspondraient à une instruction machine sur un CPU pas VLIW. En conséquence, plusieurs instructions du langage machine "source" sont regroupées en une seule instruction VLIW. Et il faut tenir compte du cas où une instruction source lève une exception. Dans le code VLIW, cela signifie qu'une opération lève une exception, et il faut annuler partiellement l'instruction VLIW associée. Et par partiellement, on veut dire que seules les opérations suivantes dans l'ordre du programme source doivent être annulées, puis ré-exécutées. Et c'est un sacré casse-tête ! Les processeurs VLIW utilisent comme solution le mécanisme d'exceptions différées des processeurs EPIC, vu il y a quelques chapitres. Pour résumer, le code est exécuté par blocs d'instruction, délimités par des branchements ou tout autre limite/barrière pertinente dans le code. Un bloc de code est compilé en deux versions : une version rapide sans exceptions matérielles, une version lente qui gère les exceptions précises. La version rapide s'exécute sans exécuter les exceptions. Cependant, les exceptions matérielles sont enregistrées, pour être prises en compte à la toute fin du bloc de code. La version rapide est exécutée en premier et elle mémorise si une exception matérielle a eu lieu. Puis une instruction vérifie si une exception a eu lieu et décide quoi faire. Si aucune exception n'a eu lieu, elle passe à la suite du programme, les résultats du bloc de code sont définitivement acceptés. Mais si une exception a eu lieu, tout est annulé. Le processeur est remis dans l'état initial, puis le code est ré-exécuté instruction par instruction de manière à gérer l'exception correctement. ===La traduction par pré-décodage=== Une première solution serait de faire la traduction binaire dans le cache d'instruction. Lors d'un défaut de cache, le code chargé depuis la RAM est traduit en code machine cible. Et c'est ce code machine cible qui est mémorisé dans le cache d'instruction et exécuté par le processeur. La technique marche sur le papier et n'est qu'une amélioration des techniques de pré-décodage vues il y a quelques chapitres. Cependant, le code obtenu est une traduction assez basique, qui n'incorpore pas d'optimisations dignes de ce nom. De plus, elle gère mal le cas où la taille du code source et cible sont potentiellement très différentes. Concrètement, elle est surtout utile pour traduire du code d'un processeur RISC vers un autre processeur RISC, dont les tailles d'instructions sont similaires. Elle ne permet pas d'émuler plusieurs jeux d'instructions différents, le cout en matériel (un circuit de pré-décodage par jeu d'instruction) serait trop important. ===Le projet DAISY d'IBM=== De très rares processeurs étaient conçus pour accélérer cette traduction binaire, afin de garder de bonnes performances. Les premiers à avoir étudié l'idée étaient IBM, avec leur projet DAISY (''Dynamically Architected Instruction Set from Yorktown''). Le projet de base était de convertir à la volée du code compilé pour des CPU Power PC, vers du code VLIW. Les chercheurs d'IBM avaient développé un algorithme de traduction dynamique efficace, ainsi qu'un processeur VLIW disposant d'optimisations spécifiques à la traduction binaire dynamique, à la volée. De nombreuses idées de ce projet ont été reprises ou re-découvertes par la société Transmetta, avec ses processeurs Crusoe et Efficieon, puis par NVIDIA avec son projet Denver. Les ingénieurs de ce projet ont étudié la possibilité d'émuler plusieurs jeux d'instructions différents sur un même processeur, notamment le s390 d'IBM et le x86. En théorie, cela demande juste d'avoir plusieurs programmes de traduction binaire : un pour le x86, un autre pour le s390, éventuellement un autre pour le Power PC. Mais cela ne s'est pas concrétisé. Mais les ingénieurs ont étudié quelle pourrait être l'architecture VLIW idéale pour ça. Par exemple, il fallait des additions 3-opérandes pour simplifier les calculs d'adresse. Une difficulté était la gestion du registre d'état, dont les bits ne sont pas mis à jour de la même manière sur le x86 et le s390 ou le Power PC. La différence entre gros-boutisme et petit-boutisme était aussi un problème, de même que des différences d'adressage pour les périphériques. Pour que la traduction binaire soit efficace, le processeur VLIW intègre diverses optimisations, comme les exceptions différées et les branchements multi-voies. La principale est la suivante : seul le code des boucles ou fonctions exécutées fréquemment est traduit en code VLIW, le reste du code est interprété. En effet, traduire du code binaire prend plus de temps qu'une simple interprétation. Pour du code qui ne sera exécutée qu'une seule fois, il est plus rapide d'utiliser l'interprétation. Par contre, pour du code exécuté beaucoup de fois, le cout de la traduction binaire est amorti, dilué sur N exécutions, compensé par le gain en temps d'exécution de ce code traduit. Au final, cela permet de ne traduire que le code qui le mérite. La détection du code fréquemment exécuté est réalisée dans le cœur VLIW. L'unité de branchement mémorise les derniers branchements rencontrés et le nombre de fois qu'ils ont été exécutés. Si ils ont été exécutés un certain nombre de fois, l'unité de branchement lève une exception matérielle, qui invoque le traducteur binaire. La routine de cette exception est le traducteur binaire proprement dit. En clair, la traduction binaire est démarrée quand le processeur détecte qu'une fonction a été exécutée plus de N fois, via une exception matérielle dédiée. La mesure du nombre d'exécution d'un branchement se fait dans l'unité de calcul dédiée aux branchements, avec l'aide d'une sorte de ''branch adress buffer'' modifié. Pour rappel, le ''branch adress buffer'' mémorise le ''Program counter'' de chaque branchement récemment rencontré. Ici, chaque entrée du ''branch adress buffer'' est associée à un compteur incrémenté à chaque exécution du branchement. ===Les processeurs Crusoe et Efficieon de Transmetta=== Les processeurs Crusoe Et Efficeron sont deux processeurs VLIW produits par la société Transmetta, une société californienne rachetée par NVIDIA. Ils étaient conçus pour exécuter spécifiquement des programmes x86, système d'exploitation inclus. L'idée derrière ce projet était d'exécuter du code x86 sans que les applications, ni même le système d'exploitation et le BIOS soient au courant ! Les processeurs Transmetta étaient en réalité conçus pour exécuter un programme unique, le ''Code Morphing Software'' (CMS), qui traduisait le code x86 en code VLIW. Transmetta ne voulait pas que son processeur VLIW soit exploité directement, sans le CMS. Il n'avait pas rendu public de compilateur pour traduire du C vers du code VLIW, il n'avait pas donné la documentation du jeu d'instruction VLIW. Des efforts de rétro-ingénieurie, documentés sur le site realworldtech, ont cependant permis de comprendre comment étaient encodées les instructions du processeur Crusoe. Comme pour le projet Daisy, le ''Code Morphing Software'' utilisait à la fois interprétation et traduction binaire, selon les besoins. Les instructions sont interprétées lors de leurs premières exécutions, mais le CMS bascule sur de la traduction en code VLIW après un certain nombre d'exécution. Ainsi, les boucles souvent exécutées sont traduites en code VLIW, alors que le reste du code est interprété. Le choix du code à traduire est le fait du CMS, il utilise des heuristiques complexes pour, qui ne sont pas connues dans le détail. Le ''Code Morphing Software'' est mémorisé dans une EEPROM, ce qui en fait un ''firmware'' situé en dessous du BIOS. Le CMS démarre ensuite le BIOS, qui lui-même démarre le système d'exploitation, qui lui-même démarre les pilotes de périphériques et les programmes. Le CMS se réserve les 16 premiers mébioctets de l'espace d'adressage. L'EEPROM du CMS est mappée dedans, mais n'en utilise que la moitié. Le reste est utilisé comme cache, pour mémoriser le code VLIW traduit par le CMS. Les deux processeurs intégraient aussi deux mémoires SRAM, utilisées par le CMS, appelées la ''local program memory'' (LPM) et la ''local data memory'' (LDM). La première contient du code qui gére les interruptions, la mémoire virtuelle, les exceptions matérielles, les problèmes d'alignement mémoire, et quelques fonctions très fréquemment utilisées par le CMS. La seconde est de la mémoire RAM utilisée par ces fonctions de la ''local program memory''. Les mémoires caches L1 et L2 sont séparées de ces deux mémoires. Le processeur Crusoe et Efficieon étaient des processeurs VLIW très simples. Ils n'avaient même pas de MMU, ni de fonctionnalités importantes sur les CPU x86. Pour Crusoe, les instructions VLIW étaient encodées sur 64 ou 128 bits, et regroupaient entre 2 à 4 opérations. Les instructions LOAD/STORE ne supportaient pas d'adressage indicé, juste de l'adressage indirect à registre. Crusoe avait 5 unités de calcul : deux ALU entières, une unité LOAD/STORE, une unité de branchement, une FPU. Efficieon doublait le nombre d'ALU entière et d'unité LOAD/STORE, les instructions VLIW passaient à 256 bits. Le processeur Crusoe contient 160 registres, dont 64 registres généraux, 32 registres flottants. Les registres généraux font 32 bits, ce qui est cohérent avec le fait que les CPU x86 de l'époque étaient des processeurs 32 bits. Quant aux registres flottants, ils faisaient 80 bits, ce qui colle avec la taille des registres flottants de la FPU x87 utilisée à l'époque. Sur les 64 registres généraux, seuls 48 étaient réellement utilisable pour mémoriser des opérandes. Une partie des registres généraux étaient utilisés pour la gestion de la pile, d'autres pour mémoriser l'état du CPU x86 émulé, un registre était un registre zéro non accesible en écriture. La gestion des exceptions est optimisée avec un système d'exceptions différées, le même que celui décrit plus haut. Le processeur utilise un système similaire, pour effectuer des lectures anticipées, qui a été expliqué dans le chapitre sur les processeurs VLIW/EPIC. Sauf qu'il s'agit là d'une optimisation pour gagner en performances en général, pas une aide utile pour gérer des exceptions précises ou toute autre subtilité de la traduction binaire. Pour gérer les exceptions différées et les lectures anticipées, le processeur mémorise l'état du processeur avant de démarrer un bloc de code. Pour cela, le processeur contient des ''shadow registers'', 48 registres entiers et 16 registres flottants. Vous remarquerez qu'il y a autant de registres entiers que de ''shadow registers'' entiers. Les registres entiers/flottants sont mémorisés dans les ''shadow registers'' avant d'exécuter un bloc de code, afin que le processeur puisse revenir à l'état de base. Une autre fonctionnalité liée est le ''gated store buffer''. Le principe est d'éviter toute écriture en mémoire RAM, tant que le code VLIW n'a pas émis d'instruction ''commit''. L'idée est que les écritures sont conservées dans la ''store queue'' de l'unité LOAD/STORE. Pour rappel, la ''store queue'' met en attente les écritures soit tant que les écritures ne sont pas terminées, soit tant que la RAM est occupée. L'idée est que les écritures sont accumulées dans la ''store queue'' tant que l'instruction ''commit'' n'a pas eu lieu. Le ''store queue'' est "déversé dans la RAM" seulement quand l'instruction ''commit'' s'exécute. ===Les CPU NVIDIA Denver=== Nvidia a tenté quelque chose de similaire avec son projet Denver. NVIDIA a racheté l'entreprise Transmetta, ses brevets, et a tenté de refaire la même chose. Le projet initial était de traduire du code x86 vers un jeu d'instruction VLIW propriétaire, comme l'a fait Transmetta. Mais NVIDIA n'a pas réussi à acquérir la licence du jeu d'instruction x86 et s'est rabattu sur le jeu d'instruction ARM. Le processeur né de ce projet est le Tegra K1-64 CPU. Il s'agit d'un processeur composé d'un cœur VLIW, couplé à deux décodeurs d'instructions ARM. Les deux décodeurs ARM traduisent une paire d'instructions ARM consécutives en instructions VLIW, pour les exécuter sur le cœur VLIW. Le CPU peut fonctionner selon deux modes : ARM et VLIW. En somme, le processeur supporte deux jeux d'instruction : le jeu d'instruction ARM, et un jeu d'instruction VLIW. En mode ARM, les instructions ARM sont chargées depuis le cache, traduites en VLIW par les décodeurs ARM, puis exécutées par le cœur VLIW. En mode optimisé, le CPU exécute des instructions VLIW chargées depuis le cache d'instruction, les décodeurs ne sont pas utilisés. : Le CPU supporte en réalité deux jeux d'instruction ARM : ARM8 et 7. Mais ils correspondent au même mode pour le CPU. Un point important est que le processeur peut changer très rapidement de mode, en à peine quelques cycles d'horloges. On n'est clairement pas dans le cas des CPU x86, qui mettent des plombes pour passer du mode 32 à 64 bits et inversement. La commutation est tellement rapide qu'on peut considérer que le processeur supporte deux jeux d'instruction simultanément. Et le support simultané de deux jeux d'instruction facilite l'implémentation de la traduction binaire. Encore une fois, inutile de traduire du code qui ne sera exécutée qu'une seule fois, mieux vaut l'exécuter directement dans le mode ARM. Surtout que le processeur supporte un mode ARM qui permet d'exécuter du code ARM sans perte de performance. Par contre, le code fréquemment utilisé, notamment dans des boucles critiques, est traduit en VLIW. Au final, les performances sont optimisées, en limitant le travail du logiciel traducteur binaire. La détection du code fréquemment exécuté se fait différemment que pour les processeurs Transmetta et Daisy. L'unité de branchement ne détecte pas directement le code beaucoup exécuté, même si elle a un rôle à jouer. A la place, la détection est réalisée en logiciel, par un ''thread'' dédié, qui s'exécute sur un cœur séparé. Il détecte le code fréquemment utilisé en regardent les branchements exécutés récemment. Pour cela, le processeur mémorise l'historique des branchements pris récemment et copie cette historique dans une mémoire tampon dédiée, partagée entre tous les cœurs. Vu qu'elle est partagée, le ''thread'' de détection a accès à l'historique sans pertes de performances, seule la copie de l'historique dans ce tampon a un cout en performance, pas son partage. Le code VLIW obtenu après traduction binaire est mémorisé dans la mémoire RAM, dans une portion spécialement réservée pour. Elle fait 128 mébioctets, et est appelée le '''cache d'optimisation''' par NVIDIA. Le cache d'optimisation est protégé en écriture et seul le traducteur binaire peut écrire dedans. Au passage, le traducteur binaire est du code VLIW, ce qui fait qu'il est placé dans le cache d'optimisation. Le processeur détecte automatiquement quand une fonction pour laquelle le code compilé équivalent est disponible. Pour cela, l'unité de chargement contient une table de correspondance entre l'adresse de la fonction ARM, et l'adresse de son équivalent compilé. Quand une fonction est appelée, l'unité de chargement regarde l'adresse de destination du branchement. Si l'adresse est dans cette table, elle récupère l'adresse de la fonction compilée et branche vers celle-ci. Le processeur dispose d'un cache d'instruction de 128 kibioctets, ce qui est très important, mais nécessaire vu la taille des instructions VLIW. Le coeur VLIW dispose de ses propres décodeurs, d'un ''scoreboard'', de 7 unités de calcul et de registres. Pour les unités de calcul, il a deux FPU, deux ALU, une unité de branchement, et deux unités LOAD-STORE faisant aussi ALU. Un point important est que le CPU dispose de 64 registres entiers et de 64 registres flottants. C'est deux fois plus que supporte le jeu d'instruction ARM. En mode ARM, seule la moitié des registres est utilisée. Mais en mode optimisé, le traducteur binaire utilise bien les 64 registres, grâce à une sorte de renommage de registres logicielle. Pour simplifier la traduction binaire, le processeur supporte les techniques vues précédemment. Le processeur intègre un ''gated store buffer'' similaire à celui des processeurs Transmetta. Les techniques d'exceptions différées sont aussi supportées, comme sur les processeurs Transmetta. Il a aussi une unité de préchargement décrite par NVIDIA comme agressive, avec support du préchargement de type anticipé (''runahead''). ===L'émulation de la mémoire virtuelle=== Pour finir, il faut parler de la mémoire virtuelle avec la traduction binaire. Sur le processeur source, le processeur gère à la fois des adresses physiques et virtuelles et sa MMU fait la conversion entre les deux. Sur le processeur VLIW, la MMU est simulée par le programme de traduction binaire, partiellement ou totalement. L'implémentation exacte varie suivant que l'on parle du projet Daisy, des processeurs Transmetta ou de Denver. Sur les processeurs Transmetta, il n'y a pas de MMU ni de mémoire virtuelle. A la place, le programme de traduction binaire émule la mémoire virtuelle du processeur source. Le processeur VLIW ne gère que des adresses physiques, rien d'autre. L'espace d'adressage physique du processeur VLIW a la même taille que celui du processeur émulé, ici des processeurs x86. Il est vraisemblable que les adresses physiques utilisées par le processeur x86 sont les mêmes que celles du processeur VLIW. Sur le projet Daisy, le processeur VLIW gère la mémoire virtuelle via pagination, comme les processeurs émulés. Ce qui fait qu'il gère un espace d'adressage virtuel et un espace d'adressage physique. Pour éviter toute confusion, nous parlerons d'adresse physique/virtuelle VLIW pour les adresses physiques/virtuelles du processeur VLIW, d'adresse virtuelle/physique source pour celle du jeu d'instruction traduit, à savoir du Power PC ou du s390, éventuellement de l'x86. Il faut alors faire le lien entre adresses physiques source et adresses virtuelles VLIW. Pour cela, rien de plus simple : il y a correspondance parfaite. L'adresse physique source numéro N correspond à l'adresse virtuelle VLIW numéro N. Cependant, cela signifie que tout l'espace d'adressage virtuel VLIW serait utilisé par le code à traduire. En réalité, il faut ajouter de la place pour le code traduit et le programme de traduction binaire. La conséquence est que l'espace d'adressage virtuel VLIW est plus large que l'espace d'adressage source. L'espace d'adressage virtuel VLIW est découpé en trois sections : une section pour le code Power PC à traduire, une autre pour le code traduit en VLIW, et une dernière réservée au traducteur binaire. Le programme de traduction est placé dans une ROM mappée en mémoire dans la seconde section. Le reste de la seconde section est réservé à une mémoire RAM utilisée par le programme de traduction. [[File:Mémoire virtuelle sur le projet Daisy.png|centre|vignette|upright=1.5|Mémoire virtuelle sur le projet Daisy]] Il faut noter que chaque page physique source, une fois traduite, correspond à N pages virtuelles VLIW. La raison est que le code traduit prend plus de place que le code machine originel. En conséquence, on doit utiliser une page finale plus grande. Pour le reste, des pages contiguës en mémoire physique source sont elles aussi contiguës en mémoire virtuelle VLIW. Le calcul d'adresse est donc simple : l'adresse physique source est multipliée par N, puis on ajoute l'adresse de base à laquelle commence la section pour le code traduit ==Les jeux d'instructions dédiés à un langage de programmation== De rares processeurs sont conçus pour un langage de programmation en particulier. On appelle ces processeurs, conçus pour des besoins particuliers, des '''processeurs dédiés'''. Par exemple, les fameux ''Burrough E-mode'' B5000/B6000/B7000 étaient spécialement conçus pour exécuter de l'ALGOL-60. Leurs cousins B2000/B3000/B4000 étaient eux conçus pour le COBOL. Des langages fonctionnels ont aussi eu droit à leurs processeurs dédiés. Le prolog en est un bel exemple, avec les superordinateurs de 5ème génération qui lui étaient dédié. On peut aussi citer les machines LISP, dédiés au langage LISP, qui datent des années 1970. Elles étaient capables d’exécuter certaines fonctions de base du langage directement dans leurs circuits : elles possédaient notamment un ''garbage collector'' câblé dans ses circuits ainsi que des instructions machines supportant un typage déterminé à l’exécution. Les processeurs dédiés ont eu leur heure de gloire au début de l'informatique, à une époque où les langages de haut niveau venaient d'être inventés. À cette époque, les compilateurs n'étaient pas performants et ne savaient pas bien optimiser le code machine. Il était alors rationnel, pour l'époque, de rapprocher le code machine cible et le langage de programmation de haut niveau. De nombreuses architectures dédiés ont ainsi été inventées, avant que les concepteurs se rendent compte des défauts de cette approche. Les défauts en question ne sont pas nombreux, mais assez simples à comprendre. Premièrement, elles sont très rapides pour un langage de programmation en particulier, mais sont assez mauvaises pour les autres, d'où un problème de "compatibilité". Ajoutons à cela que les langages de programmation peuvent évoluer, devenir de moins en moins populaires/utilisés, ce qui rend la création d'architectures généralistes plus pertinente. Enfin, les architectures dédiées sont évidemment des processeurs CISC, pour implémenter les nombreuses fonctionnalités des langages évolués. Et les défauts des CISC sont assez rédhibitoires à l'heure actuelle. À l'heure actuelle, les algorithmes des compilateurs se sont améliorés et savent nettement mieux utiliser le matériel. Ils produisent du code machine efficace, ce qui rend les architecture dédiées bien moins intéressantes. Si on ajoute les défauts de ces architectures dédiées, par étonnant que les architectures dédiées aient presque disparues. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'accélération matérielle de la virtualisation | prevText=L'accélération matérielle de la virtualisation | next=Le matériel réseau | nextText=Le matériel réseau }} </noinclude> c6adrmy5djdyczuv53ug9lqppraozfx 764006 764004 2026-04-19T15:37:21Z Mewtow 31375 /* L'émulation de la mémoire virtuelle */ 764006 wikitext text/x-wiki De nos jours, la majorité des programmeurs programment dans des langages de haut niveau. Il est très rare pour eux d'avoir à utiliser de l'assembleur, et encore moins un langage machine. Les programmes écrits dans un langage de haut niveau sont traduits en langage machine par un logiciel appelé le compilateur. [[File:Compilation.PNG|centre|vignette|upright=2.5|Compilateur : principe.]] Il se trouve que quelques rares processeurs sont conçus pour faciliter le travail du compilateur. Et ce sont ces processeurs que nous allons voir dans ce qui suit. Ils représentent une catégorie de jeux d'instruction à part, qui n'a pas vraiment de nom. ==La compilation et l’interprétation== Un compilateur traduit donc du code source, écrit dans un langage de haut niveau, vers du langage machine. La traduction est rarement directe. En général, le compilateur traduit le code source en assembleur, qui est lui-même traduit en langage machine. L'assembleur est une représentation textuelle du langage machine alors que le code machine est du binaire exécutable par le processeur. La traduction se fait donc en deux étapes, la '''compilation''' proprement dite et l''''assemblage''', réalisées respectivement par un compilateur et un assembleur. Il faut parfois rajouter une troisième phase d'édition des liens, que nous passons volontairement sous silence. {| |[[File:Code C.png|vignette|200px|Le code source, ici en C.]] |[[File:Assem.png|vignette|250px|Le code assembleur.]] |[[File:Binary file - hello world (C programming).png|vignette|150px|Le code machine.]] |} ===L'interprétation et la compilation à la volée=== Une alternative à la compilation est l''''interprétation''', qui transforme le code source en code machine à la volée. Avec l'interprétation, le code source est passé à un logiciel appelé l''''interpréteur''', qui exécute le code source ligne de code par ligne de code. Une ligne de code est traduite en code machine, qui est exécutée, puis l'interpréteur passe à la ligne suivante. [[File:Kaantotulkkaus.png|centre|vignette|upright=3|Compilation versus interprétation.]] Un défaut de l'interprétation est son cout en performance, qui est assez important. Aussi, l'interprétation stricte a évolué vers un hybride entre interprétation et compilation. L'idée est qu'une partie peu importante du code source est interprétée, alors que le code important est compilé juste avant d'être exécuté. Pour le dire autrement, le code source est partiellement compilé à la volée, juste avant son exécution. Aussi, on parle de '''compilation à la volée'''. Le terme en anglais est ''Just In Time compilation'', abrévié en JIT. En soi, le JIT est associé à de l'interprétation, les JIT sont en réalité des hybrides interpréteurs-compilateurs. Un point important est que compiler du code à la volée est assez lourd, cela a un cout en performance. Par contre, le code compilé s'exécute plus vite que du code interprété. Si on exécute le code compilé une seule fois, le cout de la compilation l'emporte sur le gain à l'exécution. Mais le code compilé est réutilisé autant de fois que nécessaire. Le gain à l'exécution est donc multiplié par le nombre d'exécutions, alors que le cout de la compilation est répartit sur plusieurs exécutions, il est amorti. Les portions du code sont choisies de manière à avoir un gain en performance maximal. L'idée générale est que le code exécuté très souvent est compilé, alors que du code exécuté pas souvent est interprété. Typiquement, les boucles les plus souvent exécutées sont compilées, le code exécuté une seule fois est interprété. ===Les langages intermédiaires=== Les compilateurs modernes passent par un '''langage intermédiaire''' pour faire la transformation en code machine. Le compilateur traduit le langage de haut niveau en langage intermédiaire, puis traduit le langage intermédiaire en code machine cible. Le langage intermédiaire est parfois appelé le '''''bytecode''''', et ce terme recouvre aussi le code écrit avec du ''bytecode''. L'interprétation aussi peut passer par l'intermédiaire d'un un ''bytecode'', ce n'est pas limité aux compilateurs proprement dit. {| |[[File:Intermediate representation scheme.png|centre|vignette|upright=1.5|Représentation intermédiaire d'un compilateur.]] |[[File:Code interpreter scheme.png|centre|vignette|upright=1.5|Fonctionnement d'un interpréteur.]] |} Faire ainsi a de nombreux avantages, le principal étant d'avoir un compilateur capable de traduire un langage de haut niveau vers plusieurs jeux d'instructions différents. Cela permet d'avoir, par exemple, un compilateur qui traduit du C soit en code machine x86, soit en code machine pour un CPU ARM, soit pour un CPU POWERPC, etc. [[File:Compiler design.svg|centre|vignette|upright=3|Compiler design]] Pour l'interprétation, l'usage d'un ''bytecode'' a d'autres avantages. Pour l'exploiter, il faut distribuer non pas le code source, mais le ''bytecode''. En clair, l'interpréteur ne prend pas en entrée le code source, mais du ''bytecode'' déjà compilé. La traduction est alors beaucoup plus simple, car le code source a déjà été partiellement compilé, par la traduction en ''bytecode''. Interpréter du code source est en effet assez compliqué : il faut effectuer des étapes d'analyse lexicale, sémantique, et bien d'autres. Avec du ''bytecode'', ces étapes ont été réalisées lors de la compilation du ''bytecode'', l'interpréteur a alors peut de choses à faire. L'interprétation du ''bytecode'' se fait instruction par instruction, au niveau du ''bytecode''. Une instruction du ''bytecode'' est traduite en une instruction machine équivalente, qui est exécutée. Si l'instruction est un peu complexe, il exécute une fonction/procédure qui fait la même chose. Pour résumer, le ''bytecode'' est ensuite traduit à la volée et exécuté instruction par instruction par un logiciel appelé l'interpréteur. [[File:Control table.png|centre|vignette|upright=2|Control table]] Le langage intermédiaire peut être vu comme l'assembleur d'un processeur, n'existe pas forcément dans la réalité, mais dont le jeu d'instruction est décrit en détail. Le processeur en question est appelé une '''machine abstraite''', ou encore une ''machine virtuelle''. Nous utiliserons le terme de machine abstraite dans ce qui suit. La machine abstraite n'est pas la même suivant que l'on cible la compilation ou l'interprétation/JIT. Pour l'interprétation/JIT, la machine abstraite est souvent une machine à pile, car cela simplifie grandement la traduction en code machine final. L'interprétation demande que la traduction du ''bytecode'' en code machine soit la plus simple possible. Et cela demande de prendre en compte pas mal de détails. Par exemple, les différents jeux d'instruction existants n'ont pas le même nombre de registres, ce qui pose problème lors de la traduction du ''bytecode''. Lors de la transformation en code machine, un algorithme d'allocation de registres se débrouille pour traduire le code intermédiaire en code qui utilise un nombre limité de registres. Pour améliorer cette allocation de registres, il y a deux solutions niveau ''bytecode'' : soit utiliser un nombre illimité de registres, soit utiliser une machine à pile. Pour les machines à pile, il existe un algorithme simple et rapide pour traduire un code écrit pour une machine à pile en un code écrit pour un processeur avec des registres, qui se débrouille pas trop mal pour allouer efficacement les registres. C'est un avantage assez important pour les langages interprétés. ==Les processeurs qui exécutent du bytecode== Le ''bytecode'' est un code machine, ce qui signifie qu'il peut en théorie s'exécuter sur un processeur qui implémente le jeu d’instruction associé. Si le jeu d'instruction d'un ''bytecode'' est souvent une description censée être fictive, elle n'en reste pas moins un jeu d'instruction et des caractéristiques précises, qu'on peut l'implémenter en matériel ! Néanmoins, tous les ''bytecode'' ne sont pas égaux de ce point de vue. Certains sont faciles à implémenter en matériel, d'autres non. Les ''bytecodes'' implémentés en matériel sont ceux dont les machines abstraites sont des machines à pile. Un premier exemple est celui des processeurs Pascal MicroEngine, qui exécutaient directement le bytecode du langage Pascal, le fameux UCSD P-code. Un second exemple est la '''machine SECD''', qui sert de langage intermédiaire pour certains compilateurs de langages fonctionnels. Elle a été implémentée en matériel par plusieurs équipes, notamment par les chercheurs de l'université de Calgary en 1989. Dans le même genre, quelques processeurs simples étaient capables d’exécuter directement le ''bytecode'' du langage FORTH. Le FORTH, un des premiers langages à pile de haut niveau, possède de nombreuses implémentations hardware et est un des rares langages de haut niveau dont le ''bytecode'' a été utilisé comme langage machine sur certains processeurs. Par exemple, on peut citer le processeur FC16, capable d’exécuter nativement du ''bytecode'' FORTH. ===Les processeurs Java=== Le cas le plus impressionnant est celui de la machine virtuelle Java, qui est un design de processeur comme un autre. En temps normal, le ''bytecode'' Java est compilé ou interprété, mais certains processeurs exécutaient du ''bytecode'' Java directement, sans interprétation ni compilation. Ils sont appelés des '''processeurs Java'''. On peut citer les processeurs ARM disposant de l'extension Jazelle, du JEMCore et du aJ-100 de aJile Systems, le picoJava II de Sun, et quelques autres. Il y a aussi eu quelques projets de recherche ou processeurs open source, comme Komodo, jamuth, le ''Java Optimized Processor'' et quelques autres projets du même genre. L'implémentation de la machine virtuelle Java n'était cependant pas complète. Quelques instructions complexes n'étaient pas gérées par le processeur et devaient être émulées en logiciel. Mais la grosse majorité des instructions l'était. Les instructions non-supportées étaient émulées avec la méthode ''trap and emulate'', à savoir qu'un opcode inconnu déclenchait une exception matérielle ''opcode invalide'', dont la routine pouvait émuler les instructions qui doivent l'être. Le processeur aJ-100 de aJile est capable d'exécuter la quasi-totalité des instructions de la JVM Java, seules deux instructions faisant exception. Les '''accélérateurs Java''' sont une solution intermédiaire, qui permet de traduire à la volée du ''bytecode'' Java en instructions RISC, exécutées sur un autre processeur. Il s'agit formellement de traduction binaire, mais je préfère en parler ici. L'idée est de combiner un processeur RISC avec un coprocesseur Java. Le coprocesseur lit le ''bytecode'' Java et le traduit en instructions RISC, ces dernières étant exécutées sur le processeur RISC. Le JA108 de Nozomi était un coprocesseur de ce genre, au même titre que le coprocesseur JSTAR de JEDI. L'extension Jazelle de certains CPU ARM fonctionnait vraisemblablement sur un principe similaire, sauf que le coprocesseur était intégré dans le processeur ARM, entre le cache d'instruction et le décodeur d'instructions ARM. L'intérêt des processeurs Java n'est pas qu'une question de performance. Certes, ils permettent d’exécuter plus vite les programmes compilés en ''bytecode'', comme des programmes Java pour la JVM Java. Mais l'intérêt est aussi de faire des économies de mémoire RAM et ROM. Je rappelle que la machine virtuelle Java est une machine à pile, ce qui fait que sa densité de code est excellente. Les programmes en ''bytecode'' Java sont donc très petits, et prennent peu de place en mémoire ROM. De plus, utiliser un processeur Java permet de se passer d'interpréteur ou de JIT Java, ce qui économise encore plus de mémoire. ===Transformer un processeur RISC en machine à pile=== La plupart des processeurs Java utilisent en interne une architecture à registre généraux, qui émule une machine à registre en microcode. Quelques registres sont réservés pour l'état de la machine virtuelle, avec au minimum un registre réservé pour le pointeur de pile, un autre pour un pointeur vers un ''pool'' de constantes, et quelques autres. D'autres registres servent à mémoriser le haut de la pile, à savoir le sommet de la pile et quelques opérandes situées juste en-dessous. Par exemple, le processeur picoJava II gardait les 64 opérandes au sommet de la pile dans 64 registres généraux, séparés des registres pour les pointeurs et l'état de la JVM. Une optimisation possible, envisagée sur les processeurs picoJava, était une sorte de macro-fusion sous stéroïde. L'idée était de fusionner certaines séries d'instructions Java en une seule instruction machine. Typiquement, pour faire une opération arithmétique, une machine à pile empile deux opérandes et exécute une opération, ce qui prend trois instructions machine. La spécification picoJava 1 a proposé d'envoyer directement la seconde opérande en entrée de l'ALU, ce qui fusionnait l'empilement de la seconde opérande et l'opération arithmétique. Le processeur picoJava II va plus loin et fusionne les trois instructions en une seule instruction RISC, agissant sur des registres. Mais la majorité des processeurs Java ne faisaient pas cela, car cela complexifie grandement le décodeur. Les processeurs ''bigfoot'' de DCT utilisaient un système différent pour émuler une pile avec des registres. Leur banc de registre de 64 bits était coupé en deux : les 16 premiers registres étaient utilisés pour la pile, les 48 suivants étaient utilisés pour autre chose. L'idée est que le registre 0 mémorise un emplacement vide, le registre 1 contient le sommet de la pile, le registre 2 l'opérande sous le sommet de la pile, et ainsi de suite. Les 15 premières opérandes de la pile sont donc mémorisées dans les registres. Les instructions écrivent leur résultat dans le registre adéquat, puis tous les registres sont décalés pour que le résultat passe dans le registre R1. Par exemple, les instructions PUSH chargent une opérande dans le registre 0, puis tous les registres sont décalés de un rang, pour que l'opérande chargée soit dans le registre 1. Le registre 0 devient alors le registre numéro 1, le registre 1 devient le 2, et ainsi de suite. Par contre, les opérations arithmétiques font autrement. Une instruction d'addition, par exemple, lit les opérandes dans le registre 1 et 2, puis place son résultat dans le registre 2 (les deux opérandes sont dépilées). Puis, les registres sont décalés d'un rang pour que ce registre 2 devienne le 1. Les registres ne sont en réalité pas décalés, il n'y a pas de transferts entre registres. A la place, les 16 premiers registres utilisaient une forme limitée de renommage de registres, similaire au banc de registre tournant utilisé sur les processeurs EPIC comme l'Itanium. Les numéros de registres sont renommés, décalés, à chaque fois qu'on empile ou dépile une opérande. Pour faire le renommage de registres, processeur contient un ''register stack counter'' de 4 bits, qui est incrémenté à chaque fois qu'on empile une donnée, et est décrémenté quand on dépile une donnée. Quand on veut accéder à un opérande, le numéro de registre architectural associé est traduit en numéro de registre physique, en additionnant le ''register stack counter'' (modulo 16, pour rester sur 4 bits). ==L'accélération de l'interprétation/JIT : le ''Thumb-EE'' d'ARM== Quelques processeurs ont ajouté des instructions pour faciliter le travail des interpréteurs/JIT. Par exemple, en 2005, ARM a ajouté le mode ''Thumb Execution Environment'', qui faisait cela. Il reprenait le jeu d'instruction compact ''thumb'' et ajoutait quelques instructions et modifiait le comportement d'instructions existantes. Le jeu d'instruction ''Thumb-EE'' a été ajouté sur les CPU ARM en 2005, mais a été déprécié en 2011, par manque d'utilité. Mais la tentative mérite qu'on s'attarde dessus. Les CPU de gamme M, déstinés à l'embarqué, n'ont jamais supporté ''Thumb-EE'', vu que de tels processeurs ne sont pas conçus pour exécuter du code interprété/JIT. Le mode ''Thumb-EE'' est un mode d'exécution, séparé de l'ARM normal. Ainsi, une instruction ''Thumb-EE'' et une instruction ARM peuvent avoir le même encodage en binaire, mais se comporter différemment. Le processeur est à tout moment soit en mode ''Thumb-EE'', soit en mode ARM normal, ce qui précise comment ces instructions doivent se comporter. Des instructions ont été ajoutées par ''Thumb-EE'', pour qu'il fasse son travail. L'entrée et la sortie du mode ''Thumb-EE'' se fait avec deux instructions : ENTERX et LEAVEX. Une fois en mode ''Thumb-EE'', le décodage des instructions ''thumb'' se fait avec les règles du ''Thumb-EE'', et non celles du ''thumb'' normal. Une première différence entre ''thumb'' normal et ''Thumb-EE'' est l'ajout de ce qui s'appelle un ''null check'' pour les instructions mémoire. Le ''null check'' est utilisé pour les instructions en mode d'adressage "base + indice" ou "Base + décalage", ou tout autre mode d'adressage avec un registre de base utilisés dans des calculs d'adresse. L'idée est de vérifier si l'adresse de base vaut zéro ou non. Si c'est le cas, le processeur lève une exception matérielle, qui est traitée par l'interprétation ou le compilateur JIT. Une telle situation est en effet signe d'une erreur d'adressage mémoire, qui doit être traitée par l'interpréteur. Typiquement, c'est signe qu'un pointeur n'a pas bien été initialisé, ce qui peut arriver avec du code interprété ou JIT. Les interpréteurs/JIT traitent généralement la situation en ajoutant un test avant toute instruction mémoire, pour vérifier si le pointeur accédé vaut zéro ou non. Avec ''Thumb-EE'', pas besoin d'ajouter les tests en questions : ils sont réalisés automatiquement lors de chaque instruction mémoire. : Les registres pour la pile, comme le pointeur de pile et le pointeur de ''frame'', sont aussi concernés. L'instruction CHKA vérifie que les accès à un tableau ne débordent pas en dehors du tableau en question. Par exemple, pour un tableau de 1024 éléments, elle vérifie si l'indice est compris entre 0 et 1024. Pour cela, elle vérifie si l'indice de tableau est bien dans l'intervalle adéquat et lève une exception matérielle si ce n'est pas le cas. Son utilité se comprend quand on sait que tous les accès à un tableau sont vérifiés par l'interpréteur. L'interpréteur est censé ajouter des instructions pour vérifier les indices, à savoir deux ou trois branchements. Avec CHKA, le test se fait en une seule instruction. D'autres modifications mineures de l'encodage des instructions sont aussi présentes, ainsi que des modifications pour les instructions LOAD/STORE. Précisément, dans les modes "base + indice" et "base + décalage", le décalage et l'indice subissent maintenant des décalages. L'idée est de simplifier le calcul d'adresse. De quoi économiser quelques instructions lors des calculs d'adresse, ce qui facilite le travail de l'interpréteur/JIT. Et des instructions LOAD/STORE précises voient leur comportement modifié pour faciliter la gestion de la pile de Java ou l'exécution des méthodes locales. Pour faciliter le travail de l'interpréteur, deux instructions HBP et HBLP ont été ajoutées. Elles branchent vers une fonction qui gère les exceptions logicielles (une fonctionnalité présente dans de nombreux langages, comme Java). La première effectue un branchement vers la fonction en question, la seconde sauvegarde l'adresse de retour avant de faire ce branchement. Les deux peuvent brancher vers 256 fonctions pré-déterminées, sans avoir à présenter leur adresse. ==La traduction binaire accélérée par le matériel== Plus haut, nous avons surtout parlé des compilateurs et des interpréteurs. Cependant, nous devons aussi parler de la '''traduction binaire'''. Elle traduit un programme écrit dans un code machine vers un autre code machine. Par exemple, elle traduit un programme compilé pour un CPU x86 vers un code machine ARM. En général, le système d'exploitation est généralement compilé pour le jeu d'instruction natif, mais il exécute des applications prévues pour le x86. Les applications sont traduites par le système d'exploitation, avant d'exécuter le code traduit. : Nous parlerons dans la suite de code machine source et de code machine cible, pour parler respectivement du code à traduire et du code obtenu après traduction. Idem avec d'autres termes comme architecture cible/source, ou jeu d'instruction cible/source. La traduction binaire est surtout utilisée pour des questions d'émulation ou de compatibilité. Par exemple, lorsque les Macintosh sont passés de processeurs Power PC vers des processeurs x86, le système d'exploitation Mac OS utilisait la traduction binaire pour convertir les anciennes applications Power PC vers du code x86. Les utilisateurs n'y ont vu que du feu. Le système de traduction binaire était appelé Rosetta 1. Par la suite, lors de la transition de processeurs x86 vers des processeurs Apple, Rosetta 2 a vu le jour. La traduction binaire a surtout été utilisée pour traduire du code x86 vers un autre jeu d'instruction. Il faut dire que le x86 est le jeu d'instruction dominant. De nombreuses entreprises ont eu pour ambition de briser l'hégémonie du x86 sur PC, en remplaçant le x86 par un jeu d'instruction plus performant, tout en gardant une compatibilité maximale. La traduction binaire était la seule solution pratique. Elle était le plus souvent intégralement réalisée en logiciel, comme c'était le cas sur les architectures Itanium avec le ''IA-32 Execution Layer''. ===Les généralités sur la traduction binaire assistée en matériel=== L'architecture source est presque tout le temps une architecture CISC, assez ancienne, qu'on souhaite émuler. Le choix de l'architecture cible se porte souvent sur une architecture VLIW, fort différente de l'architecture source, et ce pour obtenir une économie de matériel conséquente. Pour s'exécuter rapidement, le code traduit doit exploiter le parallélisme d'instruction, à savoir exécuter plusieurs instructions en même temps dans des unités de calcul séparées. Il est possible d'utiliser un processeur superscalaire avec exécution dans le désordre pour cela, mais au prix d'un cout important en transistors. Alors qu'en utilisant un CPU VLIW, c'est le traducteur binaire qui fait tout le travail d'extraction du parallélisme d'instruction. Et après tout, quitte à avoir un traducteur binaire, autant lui refiler le boulot d'optimisation. Compiler du code à la volée est certes assez lourd, mais qu'il existe des algorithmes efficaces pour regrouper des instructions indépendantes dans une seule instruction VLIW. Au passage, le processeur VLIW a plus de registres que le processeur source. Cela permet de faire du renommage de registres directement en logiciel. Le traducteur binaire n'hésite pas à changer les noms de registres entre instructions source et instruction VLIW cible, afin de supprimer des dépendances de données. En conséquence, cela ouvre des opportunités de parallélisme, qui sont exploitées lors du regroupement des opérations en instructions VLIW. Vous pourriez penser que le choix d'un CPU VLIW pour émuler du CISC est tout sauf optimal. Vous devez penser que la traduction binaire est d'autant plus simple que l'architecture source et cibles sont semblables. Dans les faits, ce n'est pas tellement traduire les instructions qui pose problème, mais plus la gestion du registre d'état, des exceptions matérielles, de la mémoire virtuelle, la différence entre gros-boutisme et petit-boutisme, de même que des différences d'adressage pour les périphériques. Le premier problème est la gestion des conditions, notamment en présence d'un registre d'état. En pratique, la traduction binaire s'utilise pour traduire du code CISC vers du code RISC ou VLIW. Les architectures source ont donc un registre d'état, qui est mis à jour non seulement par des instructions de test, mais aussi des instructions arithmétiques. L'architecture cible n'a elle pas de registre d'état, mais des registres à prédicats. La traduction de l'un vers l'autre est alors quelque peu compliquée. Et elle est d'autant plus compliquée que le registre d'état est la source de dépendances d'instruction implicites, qui réduisent les performances. Par exemple, si une instruction arithmétique modifie le registre d'état, cela peut impacter l'exécution d'une instruction ultérieure, qui lit ce registre d'état. Pour éliminer ces fausses dépendances, le traducteur binaire doit renommer ce registre en logiciel et détecter les dépendances utiles. Et le compilateur doit gérer les cas où il y a beaucoup de distance entre les deux, voire les cas où l'instruction dépendante n'a pas encore été analysée par le compilateur. Un autre problème est lié à la gestion des exceptions matérielles, et précisément des exceptions précises. Pour rappel, une instruction VLIW regroupe plusieurs opérations, opérations qui correspondraient à une instruction machine sur un CPU pas VLIW. En conséquence, plusieurs instructions du langage machine "source" sont regroupées en une seule instruction VLIW. Et il faut tenir compte du cas où une instruction source lève une exception. Dans le code VLIW, cela signifie qu'une opération lève une exception, et il faut annuler partiellement l'instruction VLIW associée. Et par partiellement, on veut dire que seules les opérations suivantes dans l'ordre du programme source doivent être annulées, puis ré-exécutées. Et c'est un sacré casse-tête ! Les processeurs VLIW utilisent comme solution le mécanisme d'exceptions différées des processeurs EPIC, vu il y a quelques chapitres. Pour résumer, le code est exécuté par blocs d'instruction, délimités par des branchements ou tout autre limite/barrière pertinente dans le code. Un bloc de code est compilé en deux versions : une version rapide sans exceptions matérielles, une version lente qui gère les exceptions précises. La version rapide s'exécute sans exécuter les exceptions. Cependant, les exceptions matérielles sont enregistrées, pour être prises en compte à la toute fin du bloc de code. La version rapide est exécutée en premier et elle mémorise si une exception matérielle a eu lieu. Puis une instruction vérifie si une exception a eu lieu et décide quoi faire. Si aucune exception n'a eu lieu, elle passe à la suite du programme, les résultats du bloc de code sont définitivement acceptés. Mais si une exception a eu lieu, tout est annulé. Le processeur est remis dans l'état initial, puis le code est ré-exécuté instruction par instruction de manière à gérer l'exception correctement. ===La traduction par pré-décodage=== Une première solution serait de faire la traduction binaire dans le cache d'instruction. Lors d'un défaut de cache, le code chargé depuis la RAM est traduit en code machine cible. Et c'est ce code machine cible qui est mémorisé dans le cache d'instruction et exécuté par le processeur. La technique marche sur le papier et n'est qu'une amélioration des techniques de pré-décodage vues il y a quelques chapitres. Cependant, le code obtenu est une traduction assez basique, qui n'incorpore pas d'optimisations dignes de ce nom. De plus, elle gère mal le cas où la taille du code source et cible sont potentiellement très différentes. Concrètement, elle est surtout utile pour traduire du code d'un processeur RISC vers un autre processeur RISC, dont les tailles d'instructions sont similaires. Elle ne permet pas d'émuler plusieurs jeux d'instructions différents, le cout en matériel (un circuit de pré-décodage par jeu d'instruction) serait trop important. ===Le projet DAISY d'IBM=== De très rares processeurs étaient conçus pour accélérer cette traduction binaire, afin de garder de bonnes performances. Les premiers à avoir étudié l'idée étaient IBM, avec leur projet DAISY (''Dynamically Architected Instruction Set from Yorktown''). Le projet de base était de convertir à la volée du code compilé pour des CPU Power PC, vers du code VLIW. Les chercheurs d'IBM avaient développé un algorithme de traduction dynamique efficace, ainsi qu'un processeur VLIW disposant d'optimisations spécifiques à la traduction binaire dynamique, à la volée. De nombreuses idées de ce projet ont été reprises ou re-découvertes par la société Transmetta, avec ses processeurs Crusoe et Efficieon, puis par NVIDIA avec son projet Denver. Les ingénieurs de ce projet ont étudié la possibilité d'émuler plusieurs jeux d'instructions différents sur un même processeur, notamment le s390 d'IBM et le x86. En théorie, cela demande juste d'avoir plusieurs programmes de traduction binaire : un pour le x86, un autre pour le s390, éventuellement un autre pour le Power PC. Mais cela ne s'est pas concrétisé. Mais les ingénieurs ont étudié quelle pourrait être l'architecture VLIW idéale pour ça. Par exemple, il fallait des additions 3-opérandes pour simplifier les calculs d'adresse. Une difficulté était la gestion du registre d'état, dont les bits ne sont pas mis à jour de la même manière sur le x86 et le s390 ou le Power PC. La différence entre gros-boutisme et petit-boutisme était aussi un problème, de même que des différences d'adressage pour les périphériques. Pour que la traduction binaire soit efficace, le processeur VLIW intègre diverses optimisations, comme les exceptions différées et les branchements multi-voies. La principale est la suivante : seul le code des boucles ou fonctions exécutées fréquemment est traduit en code VLIW, le reste du code est interprété. En effet, traduire du code binaire prend plus de temps qu'une simple interprétation. Pour du code qui ne sera exécutée qu'une seule fois, il est plus rapide d'utiliser l'interprétation. Par contre, pour du code exécuté beaucoup de fois, le cout de la traduction binaire est amorti, dilué sur N exécutions, compensé par le gain en temps d'exécution de ce code traduit. Au final, cela permet de ne traduire que le code qui le mérite. La détection du code fréquemment exécuté est réalisée dans le cœur VLIW. L'unité de branchement mémorise les derniers branchements rencontrés et le nombre de fois qu'ils ont été exécutés. Si ils ont été exécutés un certain nombre de fois, l'unité de branchement lève une exception matérielle, qui invoque le traducteur binaire. La routine de cette exception est le traducteur binaire proprement dit. En clair, la traduction binaire est démarrée quand le processeur détecte qu'une fonction a été exécutée plus de N fois, via une exception matérielle dédiée. La mesure du nombre d'exécution d'un branchement se fait dans l'unité de calcul dédiée aux branchements, avec l'aide d'une sorte de ''branch adress buffer'' modifié. Pour rappel, le ''branch adress buffer'' mémorise le ''Program counter'' de chaque branchement récemment rencontré. Ici, chaque entrée du ''branch adress buffer'' est associée à un compteur incrémenté à chaque exécution du branchement. ===Les processeurs Crusoe et Efficieon de Transmetta=== Les processeurs Crusoe Et Efficeron sont deux processeurs VLIW produits par la société Transmetta, une société californienne rachetée par NVIDIA. Ils étaient conçus pour exécuter spécifiquement des programmes x86, système d'exploitation inclus. L'idée derrière ce projet était d'exécuter du code x86 sans que les applications, ni même le système d'exploitation et le BIOS soient au courant ! Les processeurs Transmetta étaient en réalité conçus pour exécuter un programme unique, le ''Code Morphing Software'' (CMS), qui traduisait le code x86 en code VLIW. Transmetta ne voulait pas que son processeur VLIW soit exploité directement, sans le CMS. Il n'avait pas rendu public de compilateur pour traduire du C vers du code VLIW, il n'avait pas donné la documentation du jeu d'instruction VLIW. Des efforts de rétro-ingénieurie, documentés sur le site realworldtech, ont cependant permis de comprendre comment étaient encodées les instructions du processeur Crusoe. Comme pour le projet Daisy, le ''Code Morphing Software'' utilisait à la fois interprétation et traduction binaire, selon les besoins. Les instructions sont interprétées lors de leurs premières exécutions, mais le CMS bascule sur de la traduction en code VLIW après un certain nombre d'exécution. Ainsi, les boucles souvent exécutées sont traduites en code VLIW, alors que le reste du code est interprété. Le choix du code à traduire est le fait du CMS, il utilise des heuristiques complexes pour, qui ne sont pas connues dans le détail. Le ''Code Morphing Software'' est mémorisé dans une EEPROM, ce qui en fait un ''firmware'' situé en dessous du BIOS. Le CMS démarre ensuite le BIOS, qui lui-même démarre le système d'exploitation, qui lui-même démarre les pilotes de périphériques et les programmes. Le CMS se réserve les 16 premiers mébioctets de l'espace d'adressage. L'EEPROM du CMS est mappée dedans, mais n'en utilise que la moitié. Le reste est utilisé comme cache, pour mémoriser le code VLIW traduit par le CMS. Les deux processeurs intégraient aussi deux mémoires SRAM, utilisées par le CMS, appelées la ''local program memory'' (LPM) et la ''local data memory'' (LDM). La première contient du code qui gére les interruptions, la mémoire virtuelle, les exceptions matérielles, les problèmes d'alignement mémoire, et quelques fonctions très fréquemment utilisées par le CMS. La seconde est de la mémoire RAM utilisée par ces fonctions de la ''local program memory''. Les mémoires caches L1 et L2 sont séparées de ces deux mémoires. Le processeur Crusoe et Efficieon étaient des processeurs VLIW très simples. Ils n'avaient même pas de MMU, ni de fonctionnalités importantes sur les CPU x86. Pour Crusoe, les instructions VLIW étaient encodées sur 64 ou 128 bits, et regroupaient entre 2 à 4 opérations. Les instructions LOAD/STORE ne supportaient pas d'adressage indicé, juste de l'adressage indirect à registre. Crusoe avait 5 unités de calcul : deux ALU entières, une unité LOAD/STORE, une unité de branchement, une FPU. Efficieon doublait le nombre d'ALU entière et d'unité LOAD/STORE, les instructions VLIW passaient à 256 bits. Le processeur Crusoe contient 160 registres, dont 64 registres généraux, 32 registres flottants. Les registres généraux font 32 bits, ce qui est cohérent avec le fait que les CPU x86 de l'époque étaient des processeurs 32 bits. Quant aux registres flottants, ils faisaient 80 bits, ce qui colle avec la taille des registres flottants de la FPU x87 utilisée à l'époque. Sur les 64 registres généraux, seuls 48 étaient réellement utilisable pour mémoriser des opérandes. Une partie des registres généraux étaient utilisés pour la gestion de la pile, d'autres pour mémoriser l'état du CPU x86 émulé, un registre était un registre zéro non accesible en écriture. La gestion des exceptions est optimisée avec un système d'exceptions différées, le même que celui décrit plus haut. Le processeur utilise un système similaire, pour effectuer des lectures anticipées, qui a été expliqué dans le chapitre sur les processeurs VLIW/EPIC. Sauf qu'il s'agit là d'une optimisation pour gagner en performances en général, pas une aide utile pour gérer des exceptions précises ou toute autre subtilité de la traduction binaire. Pour gérer les exceptions différées et les lectures anticipées, le processeur mémorise l'état du processeur avant de démarrer un bloc de code. Pour cela, le processeur contient des ''shadow registers'', 48 registres entiers et 16 registres flottants. Vous remarquerez qu'il y a autant de registres entiers que de ''shadow registers'' entiers. Les registres entiers/flottants sont mémorisés dans les ''shadow registers'' avant d'exécuter un bloc de code, afin que le processeur puisse revenir à l'état de base. Une autre fonctionnalité liée est le ''gated store buffer''. Le principe est d'éviter toute écriture en mémoire RAM, tant que le code VLIW n'a pas émis d'instruction ''commit''. L'idée est que les écritures sont conservées dans la ''store queue'' de l'unité LOAD/STORE. Pour rappel, la ''store queue'' met en attente les écritures soit tant que les écritures ne sont pas terminées, soit tant que la RAM est occupée. L'idée est que les écritures sont accumulées dans la ''store queue'' tant que l'instruction ''commit'' n'a pas eu lieu. Le ''store queue'' est "déversé dans la RAM" seulement quand l'instruction ''commit'' s'exécute. ===Les CPU NVIDIA Denver=== Nvidia a tenté quelque chose de similaire avec son projet Denver. NVIDIA a racheté l'entreprise Transmetta, ses brevets, et a tenté de refaire la même chose. Le projet initial était de traduire du code x86 vers un jeu d'instruction VLIW propriétaire, comme l'a fait Transmetta. Mais NVIDIA n'a pas réussi à acquérir la licence du jeu d'instruction x86 et s'est rabattu sur le jeu d'instruction ARM. Le processeur né de ce projet est le Tegra K1-64 CPU. Il s'agit d'un processeur composé d'un cœur VLIW, couplé à deux décodeurs d'instructions ARM. Les deux décodeurs ARM traduisent une paire d'instructions ARM consécutives en instructions VLIW, pour les exécuter sur le cœur VLIW. Le CPU peut fonctionner selon deux modes : ARM et VLIW. En somme, le processeur supporte deux jeux d'instruction : le jeu d'instruction ARM, et un jeu d'instruction VLIW. En mode ARM, les instructions ARM sont chargées depuis le cache, traduites en VLIW par les décodeurs ARM, puis exécutées par le cœur VLIW. En mode optimisé, le CPU exécute des instructions VLIW chargées depuis le cache d'instruction, les décodeurs ne sont pas utilisés. : Le CPU supporte en réalité deux jeux d'instruction ARM : ARM8 et 7. Mais ils correspondent au même mode pour le CPU. Un point important est que le processeur peut changer très rapidement de mode, en à peine quelques cycles d'horloges. On n'est clairement pas dans le cas des CPU x86, qui mettent des plombes pour passer du mode 32 à 64 bits et inversement. La commutation est tellement rapide qu'on peut considérer que le processeur supporte deux jeux d'instruction simultanément. Et le support simultané de deux jeux d'instruction facilite l'implémentation de la traduction binaire. Encore une fois, inutile de traduire du code qui ne sera exécutée qu'une seule fois, mieux vaut l'exécuter directement dans le mode ARM. Surtout que le processeur supporte un mode ARM qui permet d'exécuter du code ARM sans perte de performance. Par contre, le code fréquemment utilisé, notamment dans des boucles critiques, est traduit en VLIW. Au final, les performances sont optimisées, en limitant le travail du logiciel traducteur binaire. La détection du code fréquemment exécuté se fait différemment que pour les processeurs Transmetta et Daisy. L'unité de branchement ne détecte pas directement le code beaucoup exécuté, même si elle a un rôle à jouer. A la place, la détection est réalisée en logiciel, par un ''thread'' dédié, qui s'exécute sur un cœur séparé. Il détecte le code fréquemment utilisé en regardent les branchements exécutés récemment. Pour cela, le processeur mémorise l'historique des branchements pris récemment et copie cette historique dans une mémoire tampon dédiée, partagée entre tous les cœurs. Vu qu'elle est partagée, le ''thread'' de détection a accès à l'historique sans pertes de performances, seule la copie de l'historique dans ce tampon a un cout en performance, pas son partage. Le code VLIW obtenu après traduction binaire est mémorisé dans la mémoire RAM, dans une portion spécialement réservée pour. Elle fait 128 mébioctets, et est appelée le '''cache d'optimisation''' par NVIDIA. Le cache d'optimisation est protégé en écriture et seul le traducteur binaire peut écrire dedans. Au passage, le traducteur binaire est du code VLIW, ce qui fait qu'il est placé dans le cache d'optimisation. Le processeur détecte automatiquement quand une fonction pour laquelle le code compilé équivalent est disponible. Pour cela, l'unité de chargement contient une table de correspondance entre l'adresse de la fonction ARM, et l'adresse de son équivalent compilé. Quand une fonction est appelée, l'unité de chargement regarde l'adresse de destination du branchement. Si l'adresse est dans cette table, elle récupère l'adresse de la fonction compilée et branche vers celle-ci. Le processeur dispose d'un cache d'instruction de 128 kibioctets, ce qui est très important, mais nécessaire vu la taille des instructions VLIW. Le coeur VLIW dispose de ses propres décodeurs, d'un ''scoreboard'', de 7 unités de calcul et de registres. Pour les unités de calcul, il a deux FPU, deux ALU, une unité de branchement, et deux unités LOAD-STORE faisant aussi ALU. Un point important est que le CPU dispose de 64 registres entiers et de 64 registres flottants. C'est deux fois plus que supporte le jeu d'instruction ARM. En mode ARM, seule la moitié des registres est utilisée. Mais en mode optimisé, le traducteur binaire utilise bien les 64 registres, grâce à une sorte de renommage de registres logicielle. Pour simplifier la traduction binaire, le processeur supporte les techniques vues précédemment. Le processeur intègre un ''gated store buffer'' similaire à celui des processeurs Transmetta. Les techniques d'exceptions différées sont aussi supportées, comme sur les processeurs Transmetta. Il a aussi une unité de préchargement décrite par NVIDIA comme agressive, avec support du préchargement de type anticipé (''runahead''). ===L'émulation de la mémoire virtuelle=== Pour finir, il faut parler de la mémoire virtuelle avec la traduction binaire. Sur le processeur source, le processeur gère à la fois des adresses physiques et virtuelles et sa MMU fait la conversion entre les deux. Sur le processeur VLIW, la MMU est simulée par le programme de traduction binaire, partiellement ou totalement. L'implémentation exacte varie suivant que l'on parle du projet Daisy, des processeurs Transmetta ou de Denver. Sur les processeurs Transmetta, il n'y a pas de MMU ni de mémoire virtuelle. A la place, le programme de traduction binaire émule la mémoire virtuelle du processeur source. Le processeur VLIW ne gère que des adresses physiques, rien d'autre. L'espace d'adressage physique du processeur VLIW a la même taille que celui du processeur émulé, ici des processeurs x86. Il est vraisemblable que les adresses physiques utilisées par le processeur x86 sont les mêmes que celles du processeur VLIW. Sur le projet Daisy, le processeur VLIW gère la mémoire virtuelle via pagination, comme les processeurs émulés. Ce qui fait qu'il gère un espace d'adressage virtuel et un espace d'adressage physique. Pour éviter toute confusion, nous parlerons d'adresse physique/virtuelle VLIW pour les adresses physiques/virtuelles du processeur VLIW, d'adresse virtuelle/physique source pour celle du jeu d'instruction traduit, à savoir du Power PC ou du s390, éventuellement de l'x86. Il faut alors faire le lien entre adresses physiques source et adresses virtuelles VLIW. Pour cela, rien de plus simple : il y a correspondance parfaite. L'adresse physique source numéro N correspond à l'adresse virtuelle VLIW numéro N. Cependant, cela signifie que tout l'espace d'adressage virtuel VLIW serait utilisé par le code à traduire. En réalité, il faut ajouter de la place pour le code traduit et le programme de traduction binaire. La conséquence est que l'espace d'adressage virtuel VLIW est plus large que l'espace d'adressage source. L'espace d'adressage virtuel VLIW est découpé en trois sections : une section pour le code Power PC à traduire, une autre pour le code traduit en VLIW, et une dernière réservée au traducteur binaire. Le programme de traduction est placé dans une ROM mappée en mémoire dans la seconde section. Le reste de la seconde section est réservé à une mémoire RAM utilisée par le programme de traduction. [[File:Mémoire virtuelle sur le projet Daisy.png|centre|vignette|upright=1.5|Mémoire virtuelle sur le projet Daisy]] Il faut noter que chaque page physique source, une fois traduite, correspond à N pages virtuelles VLIW. La raison est que le code traduit prend plus de place que le code machine originel. En conséquence, on doit utiliser une page finale plus grande. Pour le reste, des pages contiguës en mémoire physique source sont elles aussi contiguës en mémoire virtuelle VLIW. Le calcul d'adresse est donc simple : l'adresse physique source est multipliée par N, puis on ajoute l'adresse de base à laquelle commence la section pour le code traduit. ==Les processeurs à jeu d'instruction reconfigurable== Nous venons de voir comment accélérer en matériel la traduction binaire. Mais il existe une alternative qui permet de se passer de traduction binaire, complétement. L'idée est d'avoir un processeur avec un '''jeu d'instruction reconfigurable'''. Le nom est assez parlent : on peut configurer le processeur de manière à supporter le jeu d'instruction de son choix ! Par exemple, on pouvait reconfigurer un CPU de manière à ce qu'il supporte le jeu d'instruction x86, ou qu'il exécute du ''bytecode'' Java, ou du code ARMv8, etc. De tels processeurs sont encore plus rare que ceux vu avant dans ce chapitre, mais ils ont existé ! Un exemple est celui du Burroughs B1700. Les processeurs de ce type avaient un ''microcode réinscriptible'', à savoir que c'était des processeurs microcodés pour lesquels on pouvait modifier le microcode. L'idée était qu'en changeant le microcode, on changeait le jeu d'instruction du processeur. Pour cela, le microcode était associé à une mémoire SRAM dans laquelle le microcode était copié au démarrage du CPU. Il y avait donc une micro-ROM et une micro-RAM. Les instructions étaient décodées en utilisant le microcode dans la micro-SRAM, la micro-ROM ne servait qu'au démarrage. En modifiant le contenu de la micro-SRAM, on pouvait altérer le microcode, voire le remplacer totalement. ===Une architecture bit-adressable=== Si avoir un microcode réinscriptible est suffisant, quelques choix de ''design'' facilitent l'émulation de jeux d'instructions variés. Par exemple, le CPU Burroughs B1700 avait ajouté une fonctionnalité très importante : il était bit-adressable. En clair, il pouvait adresser la mémoire bit par bit, si nécessaire. L'avantage est que cela permettait d'émuler des architectures dont la taille des registres/''byte''/mots était très différente. Un autre avantage est que cela facilitait la gestion des instructions de taille variable. Détaillons rapidement le premier avantage. Le processeur était un CPU 24 bits, mais il pouvait émuler des processeurs 16 bits, 8 bits, 4 bits, 1 bit, 9 bits, ou toute autre valeur. Pour cela, le processeur lisait des mots de 24 bits et utilisant un ''barrel shifter'' pour sélectionner les bits adéquats. Par exemple, pour un accès de 16 bits, la donnée lue en mémoire était masquée de manière à ne garder que les 16 bits de poids faible. Le ''barrel shifter''/circuit de masquage était placé directement avant le bus mémoire, sur le trajet des données/instructions. ===La gestion de la taille des instructions=== Les instructions à décoder n'ont pas de longueur fixe, à savoir qu'elles peuvent faire 16 bit avec tel jeu d'instruction, 8 bits avec un autre, 32 bits sur un autre, etc. Elles peuvent aussi être de longueur fixe ou variable ! Le B1700 intégrait donc un support des instructions de longueur variable, qui était "détourné" pour gérer le fait que des jeux d'instruction différents ont des instructions de taille différentes. Notons que vu que l'architecture est bit-adressable, les contraintes d'alignement des instructions disparaissent, ce qui a des avantages qu'on va détailler dans ce qui suit ! L'implémentation des instructions de taille variable ajoutait un registre avant le registre d'instruction. Le registre d'instruction proprement dit était un registre M de 24 bits. Il était précédé par un registre T, dans lequel l'instruction était assemblée, en combinant plusieurs morceaux. Le registre T est relié à un ''barrel shifter'', différent au précédent, qui permettait de masquer, décaler et extraire une donnée de ce registre. Si l'instruction est codée est codée sur moins de 24 bits, le registre T se contente de masquer les bits en trop. Si l'instruction chargée dépasser 24 bits, le chargement se faisait en plusieurs passes, avec plusieurs lectures en mémoire. Chaque passe découpait l'instruction pour extraire des '''champs''' : l'opcode, les opérandes, les adresses absolues, les constantes immédiates, les numéros de registre, etc. Et ils étaient copiés dans des registres interne au processeur. Par exemple, il servait à extraire l'opcode de l'instruction. Ou encore, pour une instruction de branchement direct, il pouvait extraire l'adresse à laquelle brancher de l'instruction. Le chargement de l'instruction se faisait alors champ par champ. Notons que vu que l'architecture est bit-adressable, les contraintes d'alignement des instructions disparaissent ! Quand on charge une instruction, peut importe qu'elle soit ou non alignée sur 24 bits, elle sera placée dans les bits de poids faible, il suffira de masquer les bits de poids fort inutiles. Par exemple, si on charge une instruction de 16 bits, les 16 bits seront copiés dans les 16 bits de poids faible du registre T, il suffira de masquer l'octet de poids fort. Pas besoin de faire de décalages pour gérer des instructions mal alignées... Pour charger une instruction, il faut connaitre plusieurs informations : l'adresse de son premier bit, sa taille. Pour cela, le B1700 contient deux registres, nommés ''Field Adress'' et ''Field Length'', qui indiquent l'adresse du premier bit et la taille de l'instruction à charger. Leur nom commence par ''field'', car le processeur peut gérer des instructions plus longues que 24 bits. Dans ce cas, le chargement de l'instruction se fait champ par champ. C'est alors l'adresse et la taille d'un champ qui est utilisée si l'instruction dépasse 24 bits. Si l'instruction est de taille variable, et que la taille d'un champ n'est pas connue à l'avance, le processeur chargeait un champ de la même taille que le précédent et corrigeait en fonction du résultat lu. L'assemblage des morceaux d'un chamlp ou d'une instruction se faisait dans le registre T. ==Les jeux d'instructions dédiés à un langage de programmation== De rares processeurs sont conçus pour un langage de programmation en particulier. On appelle ces processeurs, conçus pour des besoins particuliers, des '''processeurs dédiés'''. Par exemple, les fameux ''Burrough E-mode'' B5000/B6000/B7000 étaient spécialement conçus pour exécuter de l'ALGOL-60. Leurs cousins B2000/B3000/B4000 étaient eux conçus pour le COBOL. Des langages fonctionnels ont aussi eu droit à leurs processeurs dédiés. Le prolog en est un bel exemple, avec les superordinateurs de 5ème génération qui lui étaient dédié. On peut aussi citer les machines LISP, dédiés au langage LISP, qui datent des années 1970. Elles étaient capables d’exécuter certaines fonctions de base du langage directement dans leurs circuits : elles possédaient notamment un ''garbage collector'' câblé dans ses circuits ainsi que des instructions machines supportant un typage déterminé à l’exécution. Les processeurs dédiés ont eu leur heure de gloire au début de l'informatique, à une époque où les langages de haut niveau venaient d'être inventés. À cette époque, les compilateurs n'étaient pas performants et ne savaient pas bien optimiser le code machine. Il était alors rationnel, pour l'époque, de rapprocher le code machine cible et le langage de programmation de haut niveau. De nombreuses architectures dédiés ont ainsi été inventées, avant que les concepteurs se rendent compte des défauts de cette approche. Les défauts en question ne sont pas nombreux, mais assez simples à comprendre. Premièrement, elles sont très rapides pour un langage de programmation en particulier, mais sont assez mauvaises pour les autres, d'où un problème de "compatibilité". Ajoutons à cela que les langages de programmation peuvent évoluer, devenir de moins en moins populaires/utilisés, ce qui rend la création d'architectures généralistes plus pertinente. Enfin, les architectures dédiées sont évidemment des processeurs CISC, pour implémenter les nombreuses fonctionnalités des langages évolués. Et les défauts des CISC sont assez rédhibitoires à l'heure actuelle. À l'heure actuelle, les algorithmes des compilateurs se sont améliorés et savent nettement mieux utiliser le matériel. Ils produisent du code machine efficace, ce qui rend les architecture dédiées bien moins intéressantes. Si on ajoute les défauts de ces architectures dédiées, par étonnant que les architectures dédiées aient presque disparues. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'accélération matérielle de la virtualisation | prevText=L'accélération matérielle de la virtualisation | next=Le matériel réseau | nextText=Le matériel réseau }} </noinclude> cxelcbdo6pkler910myfmevcacc3ren 764007 764006 2026-04-19T15:47:22Z Mewtow 31375 /* La gestion de la taille des instructions */ 764007 wikitext text/x-wiki De nos jours, la majorité des programmeurs programment dans des langages de haut niveau. Il est très rare pour eux d'avoir à utiliser de l'assembleur, et encore moins un langage machine. Les programmes écrits dans un langage de haut niveau sont traduits en langage machine par un logiciel appelé le compilateur. [[File:Compilation.PNG|centre|vignette|upright=2.5|Compilateur : principe.]] Il se trouve que quelques rares processeurs sont conçus pour faciliter le travail du compilateur. Et ce sont ces processeurs que nous allons voir dans ce qui suit. Ils représentent une catégorie de jeux d'instruction à part, qui n'a pas vraiment de nom. ==La compilation et l’interprétation== Un compilateur traduit donc du code source, écrit dans un langage de haut niveau, vers du langage machine. La traduction est rarement directe. En général, le compilateur traduit le code source en assembleur, qui est lui-même traduit en langage machine. L'assembleur est une représentation textuelle du langage machine alors que le code machine est du binaire exécutable par le processeur. La traduction se fait donc en deux étapes, la '''compilation''' proprement dite et l''''assemblage''', réalisées respectivement par un compilateur et un assembleur. Il faut parfois rajouter une troisième phase d'édition des liens, que nous passons volontairement sous silence. {| |[[File:Code C.png|vignette|200px|Le code source, ici en C.]] |[[File:Assem.png|vignette|250px|Le code assembleur.]] |[[File:Binary file - hello world (C programming).png|vignette|150px|Le code machine.]] |} ===L'interprétation et la compilation à la volée=== Une alternative à la compilation est l''''interprétation''', qui transforme le code source en code machine à la volée. Avec l'interprétation, le code source est passé à un logiciel appelé l''''interpréteur''', qui exécute le code source ligne de code par ligne de code. Une ligne de code est traduite en code machine, qui est exécutée, puis l'interpréteur passe à la ligne suivante. [[File:Kaantotulkkaus.png|centre|vignette|upright=3|Compilation versus interprétation.]] Un défaut de l'interprétation est son cout en performance, qui est assez important. Aussi, l'interprétation stricte a évolué vers un hybride entre interprétation et compilation. L'idée est qu'une partie peu importante du code source est interprétée, alors que le code important est compilé juste avant d'être exécuté. Pour le dire autrement, le code source est partiellement compilé à la volée, juste avant son exécution. Aussi, on parle de '''compilation à la volée'''. Le terme en anglais est ''Just In Time compilation'', abrévié en JIT. En soi, le JIT est associé à de l'interprétation, les JIT sont en réalité des hybrides interpréteurs-compilateurs. Un point important est que compiler du code à la volée est assez lourd, cela a un cout en performance. Par contre, le code compilé s'exécute plus vite que du code interprété. Si on exécute le code compilé une seule fois, le cout de la compilation l'emporte sur le gain à l'exécution. Mais le code compilé est réutilisé autant de fois que nécessaire. Le gain à l'exécution est donc multiplié par le nombre d'exécutions, alors que le cout de la compilation est répartit sur plusieurs exécutions, il est amorti. Les portions du code sont choisies de manière à avoir un gain en performance maximal. L'idée générale est que le code exécuté très souvent est compilé, alors que du code exécuté pas souvent est interprété. Typiquement, les boucles les plus souvent exécutées sont compilées, le code exécuté une seule fois est interprété. ===Les langages intermédiaires=== Les compilateurs modernes passent par un '''langage intermédiaire''' pour faire la transformation en code machine. Le compilateur traduit le langage de haut niveau en langage intermédiaire, puis traduit le langage intermédiaire en code machine cible. Le langage intermédiaire est parfois appelé le '''''bytecode''''', et ce terme recouvre aussi le code écrit avec du ''bytecode''. L'interprétation aussi peut passer par l'intermédiaire d'un un ''bytecode'', ce n'est pas limité aux compilateurs proprement dit. {| |[[File:Intermediate representation scheme.png|centre|vignette|upright=1.5|Représentation intermédiaire d'un compilateur.]] |[[File:Code interpreter scheme.png|centre|vignette|upright=1.5|Fonctionnement d'un interpréteur.]] |} Faire ainsi a de nombreux avantages, le principal étant d'avoir un compilateur capable de traduire un langage de haut niveau vers plusieurs jeux d'instructions différents. Cela permet d'avoir, par exemple, un compilateur qui traduit du C soit en code machine x86, soit en code machine pour un CPU ARM, soit pour un CPU POWERPC, etc. [[File:Compiler design.svg|centre|vignette|upright=3|Compiler design]] Pour l'interprétation, l'usage d'un ''bytecode'' a d'autres avantages. Pour l'exploiter, il faut distribuer non pas le code source, mais le ''bytecode''. En clair, l'interpréteur ne prend pas en entrée le code source, mais du ''bytecode'' déjà compilé. La traduction est alors beaucoup plus simple, car le code source a déjà été partiellement compilé, par la traduction en ''bytecode''. Interpréter du code source est en effet assez compliqué : il faut effectuer des étapes d'analyse lexicale, sémantique, et bien d'autres. Avec du ''bytecode'', ces étapes ont été réalisées lors de la compilation du ''bytecode'', l'interpréteur a alors peut de choses à faire. L'interprétation du ''bytecode'' se fait instruction par instruction, au niveau du ''bytecode''. Une instruction du ''bytecode'' est traduite en une instruction machine équivalente, qui est exécutée. Si l'instruction est un peu complexe, il exécute une fonction/procédure qui fait la même chose. Pour résumer, le ''bytecode'' est ensuite traduit à la volée et exécuté instruction par instruction par un logiciel appelé l'interpréteur. [[File:Control table.png|centre|vignette|upright=2|Control table]] Le langage intermédiaire peut être vu comme l'assembleur d'un processeur, n'existe pas forcément dans la réalité, mais dont le jeu d'instruction est décrit en détail. Le processeur en question est appelé une '''machine abstraite''', ou encore une ''machine virtuelle''. Nous utiliserons le terme de machine abstraite dans ce qui suit. La machine abstraite n'est pas la même suivant que l'on cible la compilation ou l'interprétation/JIT. Pour l'interprétation/JIT, la machine abstraite est souvent une machine à pile, car cela simplifie grandement la traduction en code machine final. L'interprétation demande que la traduction du ''bytecode'' en code machine soit la plus simple possible. Et cela demande de prendre en compte pas mal de détails. Par exemple, les différents jeux d'instruction existants n'ont pas le même nombre de registres, ce qui pose problème lors de la traduction du ''bytecode''. Lors de la transformation en code machine, un algorithme d'allocation de registres se débrouille pour traduire le code intermédiaire en code qui utilise un nombre limité de registres. Pour améliorer cette allocation de registres, il y a deux solutions niveau ''bytecode'' : soit utiliser un nombre illimité de registres, soit utiliser une machine à pile. Pour les machines à pile, il existe un algorithme simple et rapide pour traduire un code écrit pour une machine à pile en un code écrit pour un processeur avec des registres, qui se débrouille pas trop mal pour allouer efficacement les registres. C'est un avantage assez important pour les langages interprétés. ==Les processeurs qui exécutent du bytecode== Le ''bytecode'' est un code machine, ce qui signifie qu'il peut en théorie s'exécuter sur un processeur qui implémente le jeu d’instruction associé. Si le jeu d'instruction d'un ''bytecode'' est souvent une description censée être fictive, elle n'en reste pas moins un jeu d'instruction et des caractéristiques précises, qu'on peut l'implémenter en matériel ! Néanmoins, tous les ''bytecode'' ne sont pas égaux de ce point de vue. Certains sont faciles à implémenter en matériel, d'autres non. Les ''bytecodes'' implémentés en matériel sont ceux dont les machines abstraites sont des machines à pile. Un premier exemple est celui des processeurs Pascal MicroEngine, qui exécutaient directement le bytecode du langage Pascal, le fameux UCSD P-code. Un second exemple est la '''machine SECD''', qui sert de langage intermédiaire pour certains compilateurs de langages fonctionnels. Elle a été implémentée en matériel par plusieurs équipes, notamment par les chercheurs de l'université de Calgary en 1989. Dans le même genre, quelques processeurs simples étaient capables d’exécuter directement le ''bytecode'' du langage FORTH. Le FORTH, un des premiers langages à pile de haut niveau, possède de nombreuses implémentations hardware et est un des rares langages de haut niveau dont le ''bytecode'' a été utilisé comme langage machine sur certains processeurs. Par exemple, on peut citer le processeur FC16, capable d’exécuter nativement du ''bytecode'' FORTH. ===Les processeurs Java=== Le cas le plus impressionnant est celui de la machine virtuelle Java, qui est un design de processeur comme un autre. En temps normal, le ''bytecode'' Java est compilé ou interprété, mais certains processeurs exécutaient du ''bytecode'' Java directement, sans interprétation ni compilation. Ils sont appelés des '''processeurs Java'''. On peut citer les processeurs ARM disposant de l'extension Jazelle, du JEMCore et du aJ-100 de aJile Systems, le picoJava II de Sun, et quelques autres. Il y a aussi eu quelques projets de recherche ou processeurs open source, comme Komodo, jamuth, le ''Java Optimized Processor'' et quelques autres projets du même genre. L'implémentation de la machine virtuelle Java n'était cependant pas complète. Quelques instructions complexes n'étaient pas gérées par le processeur et devaient être émulées en logiciel. Mais la grosse majorité des instructions l'était. Les instructions non-supportées étaient émulées avec la méthode ''trap and emulate'', à savoir qu'un opcode inconnu déclenchait une exception matérielle ''opcode invalide'', dont la routine pouvait émuler les instructions qui doivent l'être. Le processeur aJ-100 de aJile est capable d'exécuter la quasi-totalité des instructions de la JVM Java, seules deux instructions faisant exception. Les '''accélérateurs Java''' sont une solution intermédiaire, qui permet de traduire à la volée du ''bytecode'' Java en instructions RISC, exécutées sur un autre processeur. Il s'agit formellement de traduction binaire, mais je préfère en parler ici. L'idée est de combiner un processeur RISC avec un coprocesseur Java. Le coprocesseur lit le ''bytecode'' Java et le traduit en instructions RISC, ces dernières étant exécutées sur le processeur RISC. Le JA108 de Nozomi était un coprocesseur de ce genre, au même titre que le coprocesseur JSTAR de JEDI. L'extension Jazelle de certains CPU ARM fonctionnait vraisemblablement sur un principe similaire, sauf que le coprocesseur était intégré dans le processeur ARM, entre le cache d'instruction et le décodeur d'instructions ARM. L'intérêt des processeurs Java n'est pas qu'une question de performance. Certes, ils permettent d’exécuter plus vite les programmes compilés en ''bytecode'', comme des programmes Java pour la JVM Java. Mais l'intérêt est aussi de faire des économies de mémoire RAM et ROM. Je rappelle que la machine virtuelle Java est une machine à pile, ce qui fait que sa densité de code est excellente. Les programmes en ''bytecode'' Java sont donc très petits, et prennent peu de place en mémoire ROM. De plus, utiliser un processeur Java permet de se passer d'interpréteur ou de JIT Java, ce qui économise encore plus de mémoire. ===Transformer un processeur RISC en machine à pile=== La plupart des processeurs Java utilisent en interne une architecture à registre généraux, qui émule une machine à registre en microcode. Quelques registres sont réservés pour l'état de la machine virtuelle, avec au minimum un registre réservé pour le pointeur de pile, un autre pour un pointeur vers un ''pool'' de constantes, et quelques autres. D'autres registres servent à mémoriser le haut de la pile, à savoir le sommet de la pile et quelques opérandes situées juste en-dessous. Par exemple, le processeur picoJava II gardait les 64 opérandes au sommet de la pile dans 64 registres généraux, séparés des registres pour les pointeurs et l'état de la JVM. Une optimisation possible, envisagée sur les processeurs picoJava, était une sorte de macro-fusion sous stéroïde. L'idée était de fusionner certaines séries d'instructions Java en une seule instruction machine. Typiquement, pour faire une opération arithmétique, une machine à pile empile deux opérandes et exécute une opération, ce qui prend trois instructions machine. La spécification picoJava 1 a proposé d'envoyer directement la seconde opérande en entrée de l'ALU, ce qui fusionnait l'empilement de la seconde opérande et l'opération arithmétique. Le processeur picoJava II va plus loin et fusionne les trois instructions en une seule instruction RISC, agissant sur des registres. Mais la majorité des processeurs Java ne faisaient pas cela, car cela complexifie grandement le décodeur. Les processeurs ''bigfoot'' de DCT utilisaient un système différent pour émuler une pile avec des registres. Leur banc de registre de 64 bits était coupé en deux : les 16 premiers registres étaient utilisés pour la pile, les 48 suivants étaient utilisés pour autre chose. L'idée est que le registre 0 mémorise un emplacement vide, le registre 1 contient le sommet de la pile, le registre 2 l'opérande sous le sommet de la pile, et ainsi de suite. Les 15 premières opérandes de la pile sont donc mémorisées dans les registres. Les instructions écrivent leur résultat dans le registre adéquat, puis tous les registres sont décalés pour que le résultat passe dans le registre R1. Par exemple, les instructions PUSH chargent une opérande dans le registre 0, puis tous les registres sont décalés de un rang, pour que l'opérande chargée soit dans le registre 1. Le registre 0 devient alors le registre numéro 1, le registre 1 devient le 2, et ainsi de suite. Par contre, les opérations arithmétiques font autrement. Une instruction d'addition, par exemple, lit les opérandes dans le registre 1 et 2, puis place son résultat dans le registre 2 (les deux opérandes sont dépilées). Puis, les registres sont décalés d'un rang pour que ce registre 2 devienne le 1. Les registres ne sont en réalité pas décalés, il n'y a pas de transferts entre registres. A la place, les 16 premiers registres utilisaient une forme limitée de renommage de registres, similaire au banc de registre tournant utilisé sur les processeurs EPIC comme l'Itanium. Les numéros de registres sont renommés, décalés, à chaque fois qu'on empile ou dépile une opérande. Pour faire le renommage de registres, processeur contient un ''register stack counter'' de 4 bits, qui est incrémenté à chaque fois qu'on empile une donnée, et est décrémenté quand on dépile une donnée. Quand on veut accéder à un opérande, le numéro de registre architectural associé est traduit en numéro de registre physique, en additionnant le ''register stack counter'' (modulo 16, pour rester sur 4 bits). ==L'accélération de l'interprétation/JIT : le ''Thumb-EE'' d'ARM== Quelques processeurs ont ajouté des instructions pour faciliter le travail des interpréteurs/JIT. Par exemple, en 2005, ARM a ajouté le mode ''Thumb Execution Environment'', qui faisait cela. Il reprenait le jeu d'instruction compact ''thumb'' et ajoutait quelques instructions et modifiait le comportement d'instructions existantes. Le jeu d'instruction ''Thumb-EE'' a été ajouté sur les CPU ARM en 2005, mais a été déprécié en 2011, par manque d'utilité. Mais la tentative mérite qu'on s'attarde dessus. Les CPU de gamme M, déstinés à l'embarqué, n'ont jamais supporté ''Thumb-EE'', vu que de tels processeurs ne sont pas conçus pour exécuter du code interprété/JIT. Le mode ''Thumb-EE'' est un mode d'exécution, séparé de l'ARM normal. Ainsi, une instruction ''Thumb-EE'' et une instruction ARM peuvent avoir le même encodage en binaire, mais se comporter différemment. Le processeur est à tout moment soit en mode ''Thumb-EE'', soit en mode ARM normal, ce qui précise comment ces instructions doivent se comporter. Des instructions ont été ajoutées par ''Thumb-EE'', pour qu'il fasse son travail. L'entrée et la sortie du mode ''Thumb-EE'' se fait avec deux instructions : ENTERX et LEAVEX. Une fois en mode ''Thumb-EE'', le décodage des instructions ''thumb'' se fait avec les règles du ''Thumb-EE'', et non celles du ''thumb'' normal. Une première différence entre ''thumb'' normal et ''Thumb-EE'' est l'ajout de ce qui s'appelle un ''null check'' pour les instructions mémoire. Le ''null check'' est utilisé pour les instructions en mode d'adressage "base + indice" ou "Base + décalage", ou tout autre mode d'adressage avec un registre de base utilisés dans des calculs d'adresse. L'idée est de vérifier si l'adresse de base vaut zéro ou non. Si c'est le cas, le processeur lève une exception matérielle, qui est traitée par l'interprétation ou le compilateur JIT. Une telle situation est en effet signe d'une erreur d'adressage mémoire, qui doit être traitée par l'interpréteur. Typiquement, c'est signe qu'un pointeur n'a pas bien été initialisé, ce qui peut arriver avec du code interprété ou JIT. Les interpréteurs/JIT traitent généralement la situation en ajoutant un test avant toute instruction mémoire, pour vérifier si le pointeur accédé vaut zéro ou non. Avec ''Thumb-EE'', pas besoin d'ajouter les tests en questions : ils sont réalisés automatiquement lors de chaque instruction mémoire. : Les registres pour la pile, comme le pointeur de pile et le pointeur de ''frame'', sont aussi concernés. L'instruction CHKA vérifie que les accès à un tableau ne débordent pas en dehors du tableau en question. Par exemple, pour un tableau de 1024 éléments, elle vérifie si l'indice est compris entre 0 et 1024. Pour cela, elle vérifie si l'indice de tableau est bien dans l'intervalle adéquat et lève une exception matérielle si ce n'est pas le cas. Son utilité se comprend quand on sait que tous les accès à un tableau sont vérifiés par l'interpréteur. L'interpréteur est censé ajouter des instructions pour vérifier les indices, à savoir deux ou trois branchements. Avec CHKA, le test se fait en une seule instruction. D'autres modifications mineures de l'encodage des instructions sont aussi présentes, ainsi que des modifications pour les instructions LOAD/STORE. Précisément, dans les modes "base + indice" et "base + décalage", le décalage et l'indice subissent maintenant des décalages. L'idée est de simplifier le calcul d'adresse. De quoi économiser quelques instructions lors des calculs d'adresse, ce qui facilite le travail de l'interpréteur/JIT. Et des instructions LOAD/STORE précises voient leur comportement modifié pour faciliter la gestion de la pile de Java ou l'exécution des méthodes locales. Pour faciliter le travail de l'interpréteur, deux instructions HBP et HBLP ont été ajoutées. Elles branchent vers une fonction qui gère les exceptions logicielles (une fonctionnalité présente dans de nombreux langages, comme Java). La première effectue un branchement vers la fonction en question, la seconde sauvegarde l'adresse de retour avant de faire ce branchement. Les deux peuvent brancher vers 256 fonctions pré-déterminées, sans avoir à présenter leur adresse. ==La traduction binaire accélérée par le matériel== Plus haut, nous avons surtout parlé des compilateurs et des interpréteurs. Cependant, nous devons aussi parler de la '''traduction binaire'''. Elle traduit un programme écrit dans un code machine vers un autre code machine. Par exemple, elle traduit un programme compilé pour un CPU x86 vers un code machine ARM. En général, le système d'exploitation est généralement compilé pour le jeu d'instruction natif, mais il exécute des applications prévues pour le x86. Les applications sont traduites par le système d'exploitation, avant d'exécuter le code traduit. : Nous parlerons dans la suite de code machine source et de code machine cible, pour parler respectivement du code à traduire et du code obtenu après traduction. Idem avec d'autres termes comme architecture cible/source, ou jeu d'instruction cible/source. La traduction binaire est surtout utilisée pour des questions d'émulation ou de compatibilité. Par exemple, lorsque les Macintosh sont passés de processeurs Power PC vers des processeurs x86, le système d'exploitation Mac OS utilisait la traduction binaire pour convertir les anciennes applications Power PC vers du code x86. Les utilisateurs n'y ont vu que du feu. Le système de traduction binaire était appelé Rosetta 1. Par la suite, lors de la transition de processeurs x86 vers des processeurs Apple, Rosetta 2 a vu le jour. La traduction binaire a surtout été utilisée pour traduire du code x86 vers un autre jeu d'instruction. Il faut dire que le x86 est le jeu d'instruction dominant. De nombreuses entreprises ont eu pour ambition de briser l'hégémonie du x86 sur PC, en remplaçant le x86 par un jeu d'instruction plus performant, tout en gardant une compatibilité maximale. La traduction binaire était la seule solution pratique. Elle était le plus souvent intégralement réalisée en logiciel, comme c'était le cas sur les architectures Itanium avec le ''IA-32 Execution Layer''. ===Les généralités sur la traduction binaire assistée en matériel=== L'architecture source est presque tout le temps une architecture CISC, assez ancienne, qu'on souhaite émuler. Le choix de l'architecture cible se porte souvent sur une architecture VLIW, fort différente de l'architecture source, et ce pour obtenir une économie de matériel conséquente. Pour s'exécuter rapidement, le code traduit doit exploiter le parallélisme d'instruction, à savoir exécuter plusieurs instructions en même temps dans des unités de calcul séparées. Il est possible d'utiliser un processeur superscalaire avec exécution dans le désordre pour cela, mais au prix d'un cout important en transistors. Alors qu'en utilisant un CPU VLIW, c'est le traducteur binaire qui fait tout le travail d'extraction du parallélisme d'instruction. Et après tout, quitte à avoir un traducteur binaire, autant lui refiler le boulot d'optimisation. Compiler du code à la volée est certes assez lourd, mais qu'il existe des algorithmes efficaces pour regrouper des instructions indépendantes dans une seule instruction VLIW. Au passage, le processeur VLIW a plus de registres que le processeur source. Cela permet de faire du renommage de registres directement en logiciel. Le traducteur binaire n'hésite pas à changer les noms de registres entre instructions source et instruction VLIW cible, afin de supprimer des dépendances de données. En conséquence, cela ouvre des opportunités de parallélisme, qui sont exploitées lors du regroupement des opérations en instructions VLIW. Vous pourriez penser que le choix d'un CPU VLIW pour émuler du CISC est tout sauf optimal. Vous devez penser que la traduction binaire est d'autant plus simple que l'architecture source et cibles sont semblables. Dans les faits, ce n'est pas tellement traduire les instructions qui pose problème, mais plus la gestion du registre d'état, des exceptions matérielles, de la mémoire virtuelle, la différence entre gros-boutisme et petit-boutisme, de même que des différences d'adressage pour les périphériques. Le premier problème est la gestion des conditions, notamment en présence d'un registre d'état. En pratique, la traduction binaire s'utilise pour traduire du code CISC vers du code RISC ou VLIW. Les architectures source ont donc un registre d'état, qui est mis à jour non seulement par des instructions de test, mais aussi des instructions arithmétiques. L'architecture cible n'a elle pas de registre d'état, mais des registres à prédicats. La traduction de l'un vers l'autre est alors quelque peu compliquée. Et elle est d'autant plus compliquée que le registre d'état est la source de dépendances d'instruction implicites, qui réduisent les performances. Par exemple, si une instruction arithmétique modifie le registre d'état, cela peut impacter l'exécution d'une instruction ultérieure, qui lit ce registre d'état. Pour éliminer ces fausses dépendances, le traducteur binaire doit renommer ce registre en logiciel et détecter les dépendances utiles. Et le compilateur doit gérer les cas où il y a beaucoup de distance entre les deux, voire les cas où l'instruction dépendante n'a pas encore été analysée par le compilateur. Un autre problème est lié à la gestion des exceptions matérielles, et précisément des exceptions précises. Pour rappel, une instruction VLIW regroupe plusieurs opérations, opérations qui correspondraient à une instruction machine sur un CPU pas VLIW. En conséquence, plusieurs instructions du langage machine "source" sont regroupées en une seule instruction VLIW. Et il faut tenir compte du cas où une instruction source lève une exception. Dans le code VLIW, cela signifie qu'une opération lève une exception, et il faut annuler partiellement l'instruction VLIW associée. Et par partiellement, on veut dire que seules les opérations suivantes dans l'ordre du programme source doivent être annulées, puis ré-exécutées. Et c'est un sacré casse-tête ! Les processeurs VLIW utilisent comme solution le mécanisme d'exceptions différées des processeurs EPIC, vu il y a quelques chapitres. Pour résumer, le code est exécuté par blocs d'instruction, délimités par des branchements ou tout autre limite/barrière pertinente dans le code. Un bloc de code est compilé en deux versions : une version rapide sans exceptions matérielles, une version lente qui gère les exceptions précises. La version rapide s'exécute sans exécuter les exceptions. Cependant, les exceptions matérielles sont enregistrées, pour être prises en compte à la toute fin du bloc de code. La version rapide est exécutée en premier et elle mémorise si une exception matérielle a eu lieu. Puis une instruction vérifie si une exception a eu lieu et décide quoi faire. Si aucune exception n'a eu lieu, elle passe à la suite du programme, les résultats du bloc de code sont définitivement acceptés. Mais si une exception a eu lieu, tout est annulé. Le processeur est remis dans l'état initial, puis le code est ré-exécuté instruction par instruction de manière à gérer l'exception correctement. ===La traduction par pré-décodage=== Une première solution serait de faire la traduction binaire dans le cache d'instruction. Lors d'un défaut de cache, le code chargé depuis la RAM est traduit en code machine cible. Et c'est ce code machine cible qui est mémorisé dans le cache d'instruction et exécuté par le processeur. La technique marche sur le papier et n'est qu'une amélioration des techniques de pré-décodage vues il y a quelques chapitres. Cependant, le code obtenu est une traduction assez basique, qui n'incorpore pas d'optimisations dignes de ce nom. De plus, elle gère mal le cas où la taille du code source et cible sont potentiellement très différentes. Concrètement, elle est surtout utile pour traduire du code d'un processeur RISC vers un autre processeur RISC, dont les tailles d'instructions sont similaires. Elle ne permet pas d'émuler plusieurs jeux d'instructions différents, le cout en matériel (un circuit de pré-décodage par jeu d'instruction) serait trop important. ===Le projet DAISY d'IBM=== De très rares processeurs étaient conçus pour accélérer cette traduction binaire, afin de garder de bonnes performances. Les premiers à avoir étudié l'idée étaient IBM, avec leur projet DAISY (''Dynamically Architected Instruction Set from Yorktown''). Le projet de base était de convertir à la volée du code compilé pour des CPU Power PC, vers du code VLIW. Les chercheurs d'IBM avaient développé un algorithme de traduction dynamique efficace, ainsi qu'un processeur VLIW disposant d'optimisations spécifiques à la traduction binaire dynamique, à la volée. De nombreuses idées de ce projet ont été reprises ou re-découvertes par la société Transmetta, avec ses processeurs Crusoe et Efficieon, puis par NVIDIA avec son projet Denver. Les ingénieurs de ce projet ont étudié la possibilité d'émuler plusieurs jeux d'instructions différents sur un même processeur, notamment le s390 d'IBM et le x86. En théorie, cela demande juste d'avoir plusieurs programmes de traduction binaire : un pour le x86, un autre pour le s390, éventuellement un autre pour le Power PC. Mais cela ne s'est pas concrétisé. Mais les ingénieurs ont étudié quelle pourrait être l'architecture VLIW idéale pour ça. Par exemple, il fallait des additions 3-opérandes pour simplifier les calculs d'adresse. Une difficulté était la gestion du registre d'état, dont les bits ne sont pas mis à jour de la même manière sur le x86 et le s390 ou le Power PC. La différence entre gros-boutisme et petit-boutisme était aussi un problème, de même que des différences d'adressage pour les périphériques. Pour que la traduction binaire soit efficace, le processeur VLIW intègre diverses optimisations, comme les exceptions différées et les branchements multi-voies. La principale est la suivante : seul le code des boucles ou fonctions exécutées fréquemment est traduit en code VLIW, le reste du code est interprété. En effet, traduire du code binaire prend plus de temps qu'une simple interprétation. Pour du code qui ne sera exécutée qu'une seule fois, il est plus rapide d'utiliser l'interprétation. Par contre, pour du code exécuté beaucoup de fois, le cout de la traduction binaire est amorti, dilué sur N exécutions, compensé par le gain en temps d'exécution de ce code traduit. Au final, cela permet de ne traduire que le code qui le mérite. La détection du code fréquemment exécuté est réalisée dans le cœur VLIW. L'unité de branchement mémorise les derniers branchements rencontrés et le nombre de fois qu'ils ont été exécutés. Si ils ont été exécutés un certain nombre de fois, l'unité de branchement lève une exception matérielle, qui invoque le traducteur binaire. La routine de cette exception est le traducteur binaire proprement dit. En clair, la traduction binaire est démarrée quand le processeur détecte qu'une fonction a été exécutée plus de N fois, via une exception matérielle dédiée. La mesure du nombre d'exécution d'un branchement se fait dans l'unité de calcul dédiée aux branchements, avec l'aide d'une sorte de ''branch adress buffer'' modifié. Pour rappel, le ''branch adress buffer'' mémorise le ''Program counter'' de chaque branchement récemment rencontré. Ici, chaque entrée du ''branch adress buffer'' est associée à un compteur incrémenté à chaque exécution du branchement. ===Les processeurs Crusoe et Efficieon de Transmetta=== Les processeurs Crusoe Et Efficeron sont deux processeurs VLIW produits par la société Transmetta, une société californienne rachetée par NVIDIA. Ils étaient conçus pour exécuter spécifiquement des programmes x86, système d'exploitation inclus. L'idée derrière ce projet était d'exécuter du code x86 sans que les applications, ni même le système d'exploitation et le BIOS soient au courant ! Les processeurs Transmetta étaient en réalité conçus pour exécuter un programme unique, le ''Code Morphing Software'' (CMS), qui traduisait le code x86 en code VLIW. Transmetta ne voulait pas que son processeur VLIW soit exploité directement, sans le CMS. Il n'avait pas rendu public de compilateur pour traduire du C vers du code VLIW, il n'avait pas donné la documentation du jeu d'instruction VLIW. Des efforts de rétro-ingénieurie, documentés sur le site realworldtech, ont cependant permis de comprendre comment étaient encodées les instructions du processeur Crusoe. Comme pour le projet Daisy, le ''Code Morphing Software'' utilisait à la fois interprétation et traduction binaire, selon les besoins. Les instructions sont interprétées lors de leurs premières exécutions, mais le CMS bascule sur de la traduction en code VLIW après un certain nombre d'exécution. Ainsi, les boucles souvent exécutées sont traduites en code VLIW, alors que le reste du code est interprété. Le choix du code à traduire est le fait du CMS, il utilise des heuristiques complexes pour, qui ne sont pas connues dans le détail. Le ''Code Morphing Software'' est mémorisé dans une EEPROM, ce qui en fait un ''firmware'' situé en dessous du BIOS. Le CMS démarre ensuite le BIOS, qui lui-même démarre le système d'exploitation, qui lui-même démarre les pilotes de périphériques et les programmes. Le CMS se réserve les 16 premiers mébioctets de l'espace d'adressage. L'EEPROM du CMS est mappée dedans, mais n'en utilise que la moitié. Le reste est utilisé comme cache, pour mémoriser le code VLIW traduit par le CMS. Les deux processeurs intégraient aussi deux mémoires SRAM, utilisées par le CMS, appelées la ''local program memory'' (LPM) et la ''local data memory'' (LDM). La première contient du code qui gére les interruptions, la mémoire virtuelle, les exceptions matérielles, les problèmes d'alignement mémoire, et quelques fonctions très fréquemment utilisées par le CMS. La seconde est de la mémoire RAM utilisée par ces fonctions de la ''local program memory''. Les mémoires caches L1 et L2 sont séparées de ces deux mémoires. Le processeur Crusoe et Efficieon étaient des processeurs VLIW très simples. Ils n'avaient même pas de MMU, ni de fonctionnalités importantes sur les CPU x86. Pour Crusoe, les instructions VLIW étaient encodées sur 64 ou 128 bits, et regroupaient entre 2 à 4 opérations. Les instructions LOAD/STORE ne supportaient pas d'adressage indicé, juste de l'adressage indirect à registre. Crusoe avait 5 unités de calcul : deux ALU entières, une unité LOAD/STORE, une unité de branchement, une FPU. Efficieon doublait le nombre d'ALU entière et d'unité LOAD/STORE, les instructions VLIW passaient à 256 bits. Le processeur Crusoe contient 160 registres, dont 64 registres généraux, 32 registres flottants. Les registres généraux font 32 bits, ce qui est cohérent avec le fait que les CPU x86 de l'époque étaient des processeurs 32 bits. Quant aux registres flottants, ils faisaient 80 bits, ce qui colle avec la taille des registres flottants de la FPU x87 utilisée à l'époque. Sur les 64 registres généraux, seuls 48 étaient réellement utilisable pour mémoriser des opérandes. Une partie des registres généraux étaient utilisés pour la gestion de la pile, d'autres pour mémoriser l'état du CPU x86 émulé, un registre était un registre zéro non accesible en écriture. La gestion des exceptions est optimisée avec un système d'exceptions différées, le même que celui décrit plus haut. Le processeur utilise un système similaire, pour effectuer des lectures anticipées, qui a été expliqué dans le chapitre sur les processeurs VLIW/EPIC. Sauf qu'il s'agit là d'une optimisation pour gagner en performances en général, pas une aide utile pour gérer des exceptions précises ou toute autre subtilité de la traduction binaire. Pour gérer les exceptions différées et les lectures anticipées, le processeur mémorise l'état du processeur avant de démarrer un bloc de code. Pour cela, le processeur contient des ''shadow registers'', 48 registres entiers et 16 registres flottants. Vous remarquerez qu'il y a autant de registres entiers que de ''shadow registers'' entiers. Les registres entiers/flottants sont mémorisés dans les ''shadow registers'' avant d'exécuter un bloc de code, afin que le processeur puisse revenir à l'état de base. Une autre fonctionnalité liée est le ''gated store buffer''. Le principe est d'éviter toute écriture en mémoire RAM, tant que le code VLIW n'a pas émis d'instruction ''commit''. L'idée est que les écritures sont conservées dans la ''store queue'' de l'unité LOAD/STORE. Pour rappel, la ''store queue'' met en attente les écritures soit tant que les écritures ne sont pas terminées, soit tant que la RAM est occupée. L'idée est que les écritures sont accumulées dans la ''store queue'' tant que l'instruction ''commit'' n'a pas eu lieu. Le ''store queue'' est "déversé dans la RAM" seulement quand l'instruction ''commit'' s'exécute. ===Les CPU NVIDIA Denver=== Nvidia a tenté quelque chose de similaire avec son projet Denver. NVIDIA a racheté l'entreprise Transmetta, ses brevets, et a tenté de refaire la même chose. Le projet initial était de traduire du code x86 vers un jeu d'instruction VLIW propriétaire, comme l'a fait Transmetta. Mais NVIDIA n'a pas réussi à acquérir la licence du jeu d'instruction x86 et s'est rabattu sur le jeu d'instruction ARM. Le processeur né de ce projet est le Tegra K1-64 CPU. Il s'agit d'un processeur composé d'un cœur VLIW, couplé à deux décodeurs d'instructions ARM. Les deux décodeurs ARM traduisent une paire d'instructions ARM consécutives en instructions VLIW, pour les exécuter sur le cœur VLIW. Le CPU peut fonctionner selon deux modes : ARM et VLIW. En somme, le processeur supporte deux jeux d'instruction : le jeu d'instruction ARM, et un jeu d'instruction VLIW. En mode ARM, les instructions ARM sont chargées depuis le cache, traduites en VLIW par les décodeurs ARM, puis exécutées par le cœur VLIW. En mode optimisé, le CPU exécute des instructions VLIW chargées depuis le cache d'instruction, les décodeurs ne sont pas utilisés. : Le CPU supporte en réalité deux jeux d'instruction ARM : ARM8 et 7. Mais ils correspondent au même mode pour le CPU. Un point important est que le processeur peut changer très rapidement de mode, en à peine quelques cycles d'horloges. On n'est clairement pas dans le cas des CPU x86, qui mettent des plombes pour passer du mode 32 à 64 bits et inversement. La commutation est tellement rapide qu'on peut considérer que le processeur supporte deux jeux d'instruction simultanément. Et le support simultané de deux jeux d'instruction facilite l'implémentation de la traduction binaire. Encore une fois, inutile de traduire du code qui ne sera exécutée qu'une seule fois, mieux vaut l'exécuter directement dans le mode ARM. Surtout que le processeur supporte un mode ARM qui permet d'exécuter du code ARM sans perte de performance. Par contre, le code fréquemment utilisé, notamment dans des boucles critiques, est traduit en VLIW. Au final, les performances sont optimisées, en limitant le travail du logiciel traducteur binaire. La détection du code fréquemment exécuté se fait différemment que pour les processeurs Transmetta et Daisy. L'unité de branchement ne détecte pas directement le code beaucoup exécuté, même si elle a un rôle à jouer. A la place, la détection est réalisée en logiciel, par un ''thread'' dédié, qui s'exécute sur un cœur séparé. Il détecte le code fréquemment utilisé en regardent les branchements exécutés récemment. Pour cela, le processeur mémorise l'historique des branchements pris récemment et copie cette historique dans une mémoire tampon dédiée, partagée entre tous les cœurs. Vu qu'elle est partagée, le ''thread'' de détection a accès à l'historique sans pertes de performances, seule la copie de l'historique dans ce tampon a un cout en performance, pas son partage. Le code VLIW obtenu après traduction binaire est mémorisé dans la mémoire RAM, dans une portion spécialement réservée pour. Elle fait 128 mébioctets, et est appelée le '''cache d'optimisation''' par NVIDIA. Le cache d'optimisation est protégé en écriture et seul le traducteur binaire peut écrire dedans. Au passage, le traducteur binaire est du code VLIW, ce qui fait qu'il est placé dans le cache d'optimisation. Le processeur détecte automatiquement quand une fonction pour laquelle le code compilé équivalent est disponible. Pour cela, l'unité de chargement contient une table de correspondance entre l'adresse de la fonction ARM, et l'adresse de son équivalent compilé. Quand une fonction est appelée, l'unité de chargement regarde l'adresse de destination du branchement. Si l'adresse est dans cette table, elle récupère l'adresse de la fonction compilée et branche vers celle-ci. Le processeur dispose d'un cache d'instruction de 128 kibioctets, ce qui est très important, mais nécessaire vu la taille des instructions VLIW. Le coeur VLIW dispose de ses propres décodeurs, d'un ''scoreboard'', de 7 unités de calcul et de registres. Pour les unités de calcul, il a deux FPU, deux ALU, une unité de branchement, et deux unités LOAD-STORE faisant aussi ALU. Un point important est que le CPU dispose de 64 registres entiers et de 64 registres flottants. C'est deux fois plus que supporte le jeu d'instruction ARM. En mode ARM, seule la moitié des registres est utilisée. Mais en mode optimisé, le traducteur binaire utilise bien les 64 registres, grâce à une sorte de renommage de registres logicielle. Pour simplifier la traduction binaire, le processeur supporte les techniques vues précédemment. Le processeur intègre un ''gated store buffer'' similaire à celui des processeurs Transmetta. Les techniques d'exceptions différées sont aussi supportées, comme sur les processeurs Transmetta. Il a aussi une unité de préchargement décrite par NVIDIA comme agressive, avec support du préchargement de type anticipé (''runahead''). ===L'émulation de la mémoire virtuelle=== Pour finir, il faut parler de la mémoire virtuelle avec la traduction binaire. Sur le processeur source, le processeur gère à la fois des adresses physiques et virtuelles et sa MMU fait la conversion entre les deux. Sur le processeur VLIW, la MMU est simulée par le programme de traduction binaire, partiellement ou totalement. L'implémentation exacte varie suivant que l'on parle du projet Daisy, des processeurs Transmetta ou de Denver. Sur les processeurs Transmetta, il n'y a pas de MMU ni de mémoire virtuelle. A la place, le programme de traduction binaire émule la mémoire virtuelle du processeur source. Le processeur VLIW ne gère que des adresses physiques, rien d'autre. L'espace d'adressage physique du processeur VLIW a la même taille que celui du processeur émulé, ici des processeurs x86. Il est vraisemblable que les adresses physiques utilisées par le processeur x86 sont les mêmes que celles du processeur VLIW. Sur le projet Daisy, le processeur VLIW gère la mémoire virtuelle via pagination, comme les processeurs émulés. Ce qui fait qu'il gère un espace d'adressage virtuel et un espace d'adressage physique. Pour éviter toute confusion, nous parlerons d'adresse physique/virtuelle VLIW pour les adresses physiques/virtuelles du processeur VLIW, d'adresse virtuelle/physique source pour celle du jeu d'instruction traduit, à savoir du Power PC ou du s390, éventuellement de l'x86. Il faut alors faire le lien entre adresses physiques source et adresses virtuelles VLIW. Pour cela, rien de plus simple : il y a correspondance parfaite. L'adresse physique source numéro N correspond à l'adresse virtuelle VLIW numéro N. Cependant, cela signifie que tout l'espace d'adressage virtuel VLIW serait utilisé par le code à traduire. En réalité, il faut ajouter de la place pour le code traduit et le programme de traduction binaire. La conséquence est que l'espace d'adressage virtuel VLIW est plus large que l'espace d'adressage source. L'espace d'adressage virtuel VLIW est découpé en trois sections : une section pour le code Power PC à traduire, une autre pour le code traduit en VLIW, et une dernière réservée au traducteur binaire. Le programme de traduction est placé dans une ROM mappée en mémoire dans la seconde section. Le reste de la seconde section est réservé à une mémoire RAM utilisée par le programme de traduction. [[File:Mémoire virtuelle sur le projet Daisy.png|centre|vignette|upright=1.5|Mémoire virtuelle sur le projet Daisy]] Il faut noter que chaque page physique source, une fois traduite, correspond à N pages virtuelles VLIW. La raison est que le code traduit prend plus de place que le code machine originel. En conséquence, on doit utiliser une page finale plus grande. Pour le reste, des pages contiguës en mémoire physique source sont elles aussi contiguës en mémoire virtuelle VLIW. Le calcul d'adresse est donc simple : l'adresse physique source est multipliée par N, puis on ajoute l'adresse de base à laquelle commence la section pour le code traduit. ==Les processeurs à jeu d'instruction reconfigurable== Nous venons de voir comment accélérer en matériel la traduction binaire. Mais il existe une alternative qui permet de se passer de traduction binaire, complétement. L'idée est d'avoir un processeur avec un '''jeu d'instruction reconfigurable'''. Le nom est assez parlent : on peut configurer le processeur de manière à supporter le jeu d'instruction de son choix ! Par exemple, on pouvait reconfigurer un CPU de manière à ce qu'il supporte le jeu d'instruction x86, ou qu'il exécute du ''bytecode'' Java, ou du code ARMv8, etc. De tels processeurs sont encore plus rare que ceux vu avant dans ce chapitre, mais ils ont existé ! Un exemple est celui du Burroughs B1700. Les processeurs de ce type avaient un ''microcode réinscriptible'', à savoir que c'était des processeurs microcodés pour lesquels on pouvait modifier le microcode. L'idée était qu'en changeant le microcode, on changeait le jeu d'instruction du processeur. Pour cela, le microcode était associé à une mémoire SRAM dans laquelle le microcode était copié au démarrage du CPU. Il y avait donc une micro-ROM et une micro-RAM. Les instructions étaient décodées en utilisant le microcode dans la micro-SRAM, la micro-ROM ne servait qu'au démarrage. En modifiant le contenu de la micro-SRAM, on pouvait altérer le microcode, voire le remplacer totalement. ===Une architecture bit-adressable=== Si avoir un microcode réinscriptible est suffisant, quelques choix de ''design'' facilitent l'émulation de jeux d'instructions variés. Par exemple, le CPU Burroughs B1700 avait ajouté une fonctionnalité très importante : il était bit-adressable. En clair, il pouvait adresser la mémoire bit par bit, si nécessaire. L'avantage est que cela permettait d'émuler des architectures dont la taille des registres/''byte''/mots était très différente. Un autre avantage est que cela facilitait la gestion des instructions de taille variable. Détaillons rapidement le premier avantage. Le processeur était un CPU 24 bits, mais il pouvait émuler des processeurs 16 bits, 8 bits, 4 bits, 1 bit, 9 bits, ou toute autre valeur. Pour cela, le processeur lisait des mots de 24 bits et utilisant un ''barrel shifter'' pour sélectionner les bits adéquats. Par exemple, pour un accès de 16 bits, la donnée lue en mémoire était masquée de manière à ne garder que les 16 bits de poids faible. Le ''barrel shifter''/circuit de masquage était placé directement avant le bus mémoire, sur le trajet des données/instructions. ===La gestion de la taille des instructions=== Les instructions à décoder n'ont pas de longueur fixe, à savoir qu'elles peuvent faire 16 bit avec tel jeu d'instruction, 8 bits avec un autre, 32 bits sur un autre, etc. Elles peuvent aussi être de longueur fixe ou variable ! Le B1700 intégrait donc un support des instructions de longueur variable, qui était "détourné" pour gérer le fait que des jeux d'instruction différents ont des instructions de taille différentes. Notons que vu que l'architecture est bit-adressable, les contraintes d'alignement des instructions disparaissent, ce qui a des avantages qu'on va détailler dans ce qui suit ! L'implémentation des instructions de taille variable ajoutait un registre avant le registre d'instruction. Le registre d'instruction proprement dit était un registre M de 24 bits. Il était précédé par un registre T, dans lequel l'instruction était assemblée, en combinant plusieurs morceaux. Le registre T est relié à un ''barrel shifter'', différent au précédent, qui permettait de masquer, décaler et extraire une donnée de ce registre. Si l'instruction est codée est codée sur moins de 24 bits, le registre T se contente de masquer les bits en trop. Si l'instruction chargée dépasser 24 bits, le chargement se faisait en plusieurs passes, avec plusieurs lectures en mémoire. Chaque passe découpait l'instruction pour extraire des '''champs''' : l'opcode, les opérandes, les adresses absolues, les constantes immédiates, les numéros de registre, etc. Et ils étaient copiés dans des registres interne au processeur. Par exemple, il servait à extraire l'opcode de l'instruction. Ou encore, pour une instruction de branchement direct, il pouvait extraire l'adresse à laquelle brancher de l'instruction. Le chargement de l'instruction se faisait alors champ par champ. Notons que vu que l'architecture est bit-adressable, les contraintes d'alignement des instructions disparaissent ! Quand on charge une instruction, peut importe qu'elle soit ou non alignée sur 24 bits, elle sera placée dans les bits de poids faible, il suffira de masquer les bits de poids fort inutiles. Par exemple, si on charge une instruction de 16 bits, les 16 bits seront copiés dans les 16 bits de poids faible du registre T, il suffira de masquer l'octet de poids fort. Pas besoin de faire de décalages pour gérer des instructions mal alignées... Pour charger une instruction, il faut connaitre plusieurs informations : l'adresse de son premier bit, sa taille. Pour cela, le B1700 contient deux registres, nommés ''Field Adress'' (FA) et ''Field Length'' (FL), qui indiquent l'adresse du premier bit et la taille de l'instruction à charger. Le premier fait 24 bits, ce qui permet de gérer des adresses de 24 bits. N'oubliez pas que le processeur est bit-adressable, ce qui permet d'adresser 16 Mébi-bits, soit 2 mébioctets. Le registre pour la taille fait lui 16 bits, ce qui permet de gérer des instructions de 65536 bits, soit 8192 octets ! Leur nom commence par ''field'', car le processeur peut gérer des instructions plus longues que 24 bits. Dans ce cas, le chargement de l'instruction se fait champ par champ. Pour donner un exemple, prenons une instruction composée de 20 champs de 12 bits chacun. Les champs sont chargés un par un dans le registre T. Le registre FA est alors initialisé avec l'adresse de l'instruction, il est incrémenté de 12 à chaque cycle. Le registre FL est initialisé avec la taille de l'instruction, soit 20 * 12 = 240 bits, et il est décrémenté de 16 à chaque cycle. Si l'instruction est de taille variable, et que la taille d'un champ n'est pas connue à l'avance, le processeur chargeait un champ de la même taille que le précédent et corrigeait en fonction du résultat lu. L'assemblage des morceaux d'un chamlp ou d'une instruction se faisait dans le registre T. ==Les jeux d'instructions dédiés à un langage de programmation== De rares processeurs sont conçus pour un langage de programmation en particulier. On appelle ces processeurs, conçus pour des besoins particuliers, des '''processeurs dédiés'''. Par exemple, les fameux ''Burrough E-mode'' B5000/B6000/B7000 étaient spécialement conçus pour exécuter de l'ALGOL-60. Leurs cousins B2000/B3000/B4000 étaient eux conçus pour le COBOL. Des langages fonctionnels ont aussi eu droit à leurs processeurs dédiés. Le prolog en est un bel exemple, avec les superordinateurs de 5ème génération qui lui étaient dédié. On peut aussi citer les machines LISP, dédiés au langage LISP, qui datent des années 1970. Elles étaient capables d’exécuter certaines fonctions de base du langage directement dans leurs circuits : elles possédaient notamment un ''garbage collector'' câblé dans ses circuits ainsi que des instructions machines supportant un typage déterminé à l’exécution. Les processeurs dédiés ont eu leur heure de gloire au début de l'informatique, à une époque où les langages de haut niveau venaient d'être inventés. À cette époque, les compilateurs n'étaient pas performants et ne savaient pas bien optimiser le code machine. Il était alors rationnel, pour l'époque, de rapprocher le code machine cible et le langage de programmation de haut niveau. De nombreuses architectures dédiés ont ainsi été inventées, avant que les concepteurs se rendent compte des défauts de cette approche. Les défauts en question ne sont pas nombreux, mais assez simples à comprendre. Premièrement, elles sont très rapides pour un langage de programmation en particulier, mais sont assez mauvaises pour les autres, d'où un problème de "compatibilité". Ajoutons à cela que les langages de programmation peuvent évoluer, devenir de moins en moins populaires/utilisés, ce qui rend la création d'architectures généralistes plus pertinente. Enfin, les architectures dédiées sont évidemment des processeurs CISC, pour implémenter les nombreuses fonctionnalités des langages évolués. Et les défauts des CISC sont assez rédhibitoires à l'heure actuelle. À l'heure actuelle, les algorithmes des compilateurs se sont améliorés et savent nettement mieux utiliser le matériel. Ils produisent du code machine efficace, ce qui rend les architecture dédiées bien moins intéressantes. Si on ajoute les défauts de ces architectures dédiées, par étonnant que les architectures dédiées aient presque disparues. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'accélération matérielle de la virtualisation | prevText=L'accélération matérielle de la virtualisation | next=Le matériel réseau | nextText=Le matériel réseau }} </noinclude> d3bjntu2a3xrnfzfcpg4ghu0vv8hahv 764008 764007 2026-04-19T15:52:17Z Mewtow 31375 /* Les processeurs à jeu d'instruction reconfigurable */ 764008 wikitext text/x-wiki De nos jours, la majorité des programmeurs programment dans des langages de haut niveau. Il est très rare pour eux d'avoir à utiliser de l'assembleur, et encore moins un langage machine. Les programmes écrits dans un langage de haut niveau sont traduits en langage machine par un logiciel appelé le compilateur. [[File:Compilation.PNG|centre|vignette|upright=2.5|Compilateur : principe.]] Il se trouve que quelques rares processeurs sont conçus pour faciliter le travail du compilateur. Et ce sont ces processeurs que nous allons voir dans ce qui suit. Ils représentent une catégorie de jeux d'instruction à part, qui n'a pas vraiment de nom. ==La compilation et l’interprétation== Un compilateur traduit donc du code source, écrit dans un langage de haut niveau, vers du langage machine. La traduction est rarement directe. En général, le compilateur traduit le code source en assembleur, qui est lui-même traduit en langage machine. L'assembleur est une représentation textuelle du langage machine alors que le code machine est du binaire exécutable par le processeur. La traduction se fait donc en deux étapes, la '''compilation''' proprement dite et l''''assemblage''', réalisées respectivement par un compilateur et un assembleur. Il faut parfois rajouter une troisième phase d'édition des liens, que nous passons volontairement sous silence. {| |[[File:Code C.png|vignette|200px|Le code source, ici en C.]] |[[File:Assem.png|vignette|250px|Le code assembleur.]] |[[File:Binary file - hello world (C programming).png|vignette|150px|Le code machine.]] |} ===L'interprétation et la compilation à la volée=== Une alternative à la compilation est l''''interprétation''', qui transforme le code source en code machine à la volée. Avec l'interprétation, le code source est passé à un logiciel appelé l''''interpréteur''', qui exécute le code source ligne de code par ligne de code. Une ligne de code est traduite en code machine, qui est exécutée, puis l'interpréteur passe à la ligne suivante. [[File:Kaantotulkkaus.png|centre|vignette|upright=3|Compilation versus interprétation.]] Un défaut de l'interprétation est son cout en performance, qui est assez important. Aussi, l'interprétation stricte a évolué vers un hybride entre interprétation et compilation. L'idée est qu'une partie peu importante du code source est interprétée, alors que le code important est compilé juste avant d'être exécuté. Pour le dire autrement, le code source est partiellement compilé à la volée, juste avant son exécution. Aussi, on parle de '''compilation à la volée'''. Le terme en anglais est ''Just In Time compilation'', abrévié en JIT. En soi, le JIT est associé à de l'interprétation, les JIT sont en réalité des hybrides interpréteurs-compilateurs. Un point important est que compiler du code à la volée est assez lourd, cela a un cout en performance. Par contre, le code compilé s'exécute plus vite que du code interprété. Si on exécute le code compilé une seule fois, le cout de la compilation l'emporte sur le gain à l'exécution. Mais le code compilé est réutilisé autant de fois que nécessaire. Le gain à l'exécution est donc multiplié par le nombre d'exécutions, alors que le cout de la compilation est répartit sur plusieurs exécutions, il est amorti. Les portions du code sont choisies de manière à avoir un gain en performance maximal. L'idée générale est que le code exécuté très souvent est compilé, alors que du code exécuté pas souvent est interprété. Typiquement, les boucles les plus souvent exécutées sont compilées, le code exécuté une seule fois est interprété. ===Les langages intermédiaires=== Les compilateurs modernes passent par un '''langage intermédiaire''' pour faire la transformation en code machine. Le compilateur traduit le langage de haut niveau en langage intermédiaire, puis traduit le langage intermédiaire en code machine cible. Le langage intermédiaire est parfois appelé le '''''bytecode''''', et ce terme recouvre aussi le code écrit avec du ''bytecode''. L'interprétation aussi peut passer par l'intermédiaire d'un un ''bytecode'', ce n'est pas limité aux compilateurs proprement dit. {| |[[File:Intermediate representation scheme.png|centre|vignette|upright=1.5|Représentation intermédiaire d'un compilateur.]] |[[File:Code interpreter scheme.png|centre|vignette|upright=1.5|Fonctionnement d'un interpréteur.]] |} Faire ainsi a de nombreux avantages, le principal étant d'avoir un compilateur capable de traduire un langage de haut niveau vers plusieurs jeux d'instructions différents. Cela permet d'avoir, par exemple, un compilateur qui traduit du C soit en code machine x86, soit en code machine pour un CPU ARM, soit pour un CPU POWERPC, etc. [[File:Compiler design.svg|centre|vignette|upright=3|Compiler design]] Pour l'interprétation, l'usage d'un ''bytecode'' a d'autres avantages. Pour l'exploiter, il faut distribuer non pas le code source, mais le ''bytecode''. En clair, l'interpréteur ne prend pas en entrée le code source, mais du ''bytecode'' déjà compilé. La traduction est alors beaucoup plus simple, car le code source a déjà été partiellement compilé, par la traduction en ''bytecode''. Interpréter du code source est en effet assez compliqué : il faut effectuer des étapes d'analyse lexicale, sémantique, et bien d'autres. Avec du ''bytecode'', ces étapes ont été réalisées lors de la compilation du ''bytecode'', l'interpréteur a alors peut de choses à faire. L'interprétation du ''bytecode'' se fait instruction par instruction, au niveau du ''bytecode''. Une instruction du ''bytecode'' est traduite en une instruction machine équivalente, qui est exécutée. Si l'instruction est un peu complexe, il exécute une fonction/procédure qui fait la même chose. Pour résumer, le ''bytecode'' est ensuite traduit à la volée et exécuté instruction par instruction par un logiciel appelé l'interpréteur. [[File:Control table.png|centre|vignette|upright=2|Control table]] Le langage intermédiaire peut être vu comme l'assembleur d'un processeur, n'existe pas forcément dans la réalité, mais dont le jeu d'instruction est décrit en détail. Le processeur en question est appelé une '''machine abstraite''', ou encore une ''machine virtuelle''. Nous utiliserons le terme de machine abstraite dans ce qui suit. La machine abstraite n'est pas la même suivant que l'on cible la compilation ou l'interprétation/JIT. Pour l'interprétation/JIT, la machine abstraite est souvent une machine à pile, car cela simplifie grandement la traduction en code machine final. L'interprétation demande que la traduction du ''bytecode'' en code machine soit la plus simple possible. Et cela demande de prendre en compte pas mal de détails. Par exemple, les différents jeux d'instruction existants n'ont pas le même nombre de registres, ce qui pose problème lors de la traduction du ''bytecode''. Lors de la transformation en code machine, un algorithme d'allocation de registres se débrouille pour traduire le code intermédiaire en code qui utilise un nombre limité de registres. Pour améliorer cette allocation de registres, il y a deux solutions niveau ''bytecode'' : soit utiliser un nombre illimité de registres, soit utiliser une machine à pile. Pour les machines à pile, il existe un algorithme simple et rapide pour traduire un code écrit pour une machine à pile en un code écrit pour un processeur avec des registres, qui se débrouille pas trop mal pour allouer efficacement les registres. C'est un avantage assez important pour les langages interprétés. ==Les processeurs qui exécutent du bytecode== Le ''bytecode'' est un code machine, ce qui signifie qu'il peut en théorie s'exécuter sur un processeur qui implémente le jeu d’instruction associé. Si le jeu d'instruction d'un ''bytecode'' est souvent une description censée être fictive, elle n'en reste pas moins un jeu d'instruction et des caractéristiques précises, qu'on peut l'implémenter en matériel ! Néanmoins, tous les ''bytecode'' ne sont pas égaux de ce point de vue. Certains sont faciles à implémenter en matériel, d'autres non. Les ''bytecodes'' implémentés en matériel sont ceux dont les machines abstraites sont des machines à pile. Un premier exemple est celui des processeurs Pascal MicroEngine, qui exécutaient directement le bytecode du langage Pascal, le fameux UCSD P-code. Un second exemple est la '''machine SECD''', qui sert de langage intermédiaire pour certains compilateurs de langages fonctionnels. Elle a été implémentée en matériel par plusieurs équipes, notamment par les chercheurs de l'université de Calgary en 1989. Dans le même genre, quelques processeurs simples étaient capables d’exécuter directement le ''bytecode'' du langage FORTH. Le FORTH, un des premiers langages à pile de haut niveau, possède de nombreuses implémentations hardware et est un des rares langages de haut niveau dont le ''bytecode'' a été utilisé comme langage machine sur certains processeurs. Par exemple, on peut citer le processeur FC16, capable d’exécuter nativement du ''bytecode'' FORTH. ===Les processeurs Java=== Le cas le plus impressionnant est celui de la machine virtuelle Java, qui est un design de processeur comme un autre. En temps normal, le ''bytecode'' Java est compilé ou interprété, mais certains processeurs exécutaient du ''bytecode'' Java directement, sans interprétation ni compilation. Ils sont appelés des '''processeurs Java'''. On peut citer les processeurs ARM disposant de l'extension Jazelle, du JEMCore et du aJ-100 de aJile Systems, le picoJava II de Sun, et quelques autres. Il y a aussi eu quelques projets de recherche ou processeurs open source, comme Komodo, jamuth, le ''Java Optimized Processor'' et quelques autres projets du même genre. L'implémentation de la machine virtuelle Java n'était cependant pas complète. Quelques instructions complexes n'étaient pas gérées par le processeur et devaient être émulées en logiciel. Mais la grosse majorité des instructions l'était. Les instructions non-supportées étaient émulées avec la méthode ''trap and emulate'', à savoir qu'un opcode inconnu déclenchait une exception matérielle ''opcode invalide'', dont la routine pouvait émuler les instructions qui doivent l'être. Le processeur aJ-100 de aJile est capable d'exécuter la quasi-totalité des instructions de la JVM Java, seules deux instructions faisant exception. Les '''accélérateurs Java''' sont une solution intermédiaire, qui permet de traduire à la volée du ''bytecode'' Java en instructions RISC, exécutées sur un autre processeur. Il s'agit formellement de traduction binaire, mais je préfère en parler ici. L'idée est de combiner un processeur RISC avec un coprocesseur Java. Le coprocesseur lit le ''bytecode'' Java et le traduit en instructions RISC, ces dernières étant exécutées sur le processeur RISC. Le JA108 de Nozomi était un coprocesseur de ce genre, au même titre que le coprocesseur JSTAR de JEDI. L'extension Jazelle de certains CPU ARM fonctionnait vraisemblablement sur un principe similaire, sauf que le coprocesseur était intégré dans le processeur ARM, entre le cache d'instruction et le décodeur d'instructions ARM. L'intérêt des processeurs Java n'est pas qu'une question de performance. Certes, ils permettent d’exécuter plus vite les programmes compilés en ''bytecode'', comme des programmes Java pour la JVM Java. Mais l'intérêt est aussi de faire des économies de mémoire RAM et ROM. Je rappelle que la machine virtuelle Java est une machine à pile, ce qui fait que sa densité de code est excellente. Les programmes en ''bytecode'' Java sont donc très petits, et prennent peu de place en mémoire ROM. De plus, utiliser un processeur Java permet de se passer d'interpréteur ou de JIT Java, ce qui économise encore plus de mémoire. ===Transformer un processeur RISC en machine à pile=== La plupart des processeurs Java utilisent en interne une architecture à registre généraux, qui émule une machine à registre en microcode. Quelques registres sont réservés pour l'état de la machine virtuelle, avec au minimum un registre réservé pour le pointeur de pile, un autre pour un pointeur vers un ''pool'' de constantes, et quelques autres. D'autres registres servent à mémoriser le haut de la pile, à savoir le sommet de la pile et quelques opérandes situées juste en-dessous. Par exemple, le processeur picoJava II gardait les 64 opérandes au sommet de la pile dans 64 registres généraux, séparés des registres pour les pointeurs et l'état de la JVM. Une optimisation possible, envisagée sur les processeurs picoJava, était une sorte de macro-fusion sous stéroïde. L'idée était de fusionner certaines séries d'instructions Java en une seule instruction machine. Typiquement, pour faire une opération arithmétique, une machine à pile empile deux opérandes et exécute une opération, ce qui prend trois instructions machine. La spécification picoJava 1 a proposé d'envoyer directement la seconde opérande en entrée de l'ALU, ce qui fusionnait l'empilement de la seconde opérande et l'opération arithmétique. Le processeur picoJava II va plus loin et fusionne les trois instructions en une seule instruction RISC, agissant sur des registres. Mais la majorité des processeurs Java ne faisaient pas cela, car cela complexifie grandement le décodeur. Les processeurs ''bigfoot'' de DCT utilisaient un système différent pour émuler une pile avec des registres. Leur banc de registre de 64 bits était coupé en deux : les 16 premiers registres étaient utilisés pour la pile, les 48 suivants étaient utilisés pour autre chose. L'idée est que le registre 0 mémorise un emplacement vide, le registre 1 contient le sommet de la pile, le registre 2 l'opérande sous le sommet de la pile, et ainsi de suite. Les 15 premières opérandes de la pile sont donc mémorisées dans les registres. Les instructions écrivent leur résultat dans le registre adéquat, puis tous les registres sont décalés pour que le résultat passe dans le registre R1. Par exemple, les instructions PUSH chargent une opérande dans le registre 0, puis tous les registres sont décalés de un rang, pour que l'opérande chargée soit dans le registre 1. Le registre 0 devient alors le registre numéro 1, le registre 1 devient le 2, et ainsi de suite. Par contre, les opérations arithmétiques font autrement. Une instruction d'addition, par exemple, lit les opérandes dans le registre 1 et 2, puis place son résultat dans le registre 2 (les deux opérandes sont dépilées). Puis, les registres sont décalés d'un rang pour que ce registre 2 devienne le 1. Les registres ne sont en réalité pas décalés, il n'y a pas de transferts entre registres. A la place, les 16 premiers registres utilisaient une forme limitée de renommage de registres, similaire au banc de registre tournant utilisé sur les processeurs EPIC comme l'Itanium. Les numéros de registres sont renommés, décalés, à chaque fois qu'on empile ou dépile une opérande. Pour faire le renommage de registres, processeur contient un ''register stack counter'' de 4 bits, qui est incrémenté à chaque fois qu'on empile une donnée, et est décrémenté quand on dépile une donnée. Quand on veut accéder à un opérande, le numéro de registre architectural associé est traduit en numéro de registre physique, en additionnant le ''register stack counter'' (modulo 16, pour rester sur 4 bits). ==L'accélération de l'interprétation/JIT : le ''Thumb-EE'' d'ARM== Quelques processeurs ont ajouté des instructions pour faciliter le travail des interpréteurs/JIT. Par exemple, en 2005, ARM a ajouté le mode ''Thumb Execution Environment'', qui faisait cela. Il reprenait le jeu d'instruction compact ''thumb'' et ajoutait quelques instructions et modifiait le comportement d'instructions existantes. Le jeu d'instruction ''Thumb-EE'' a été ajouté sur les CPU ARM en 2005, mais a été déprécié en 2011, par manque d'utilité. Mais la tentative mérite qu'on s'attarde dessus. Les CPU de gamme M, déstinés à l'embarqué, n'ont jamais supporté ''Thumb-EE'', vu que de tels processeurs ne sont pas conçus pour exécuter du code interprété/JIT. Le mode ''Thumb-EE'' est un mode d'exécution, séparé de l'ARM normal. Ainsi, une instruction ''Thumb-EE'' et une instruction ARM peuvent avoir le même encodage en binaire, mais se comporter différemment. Le processeur est à tout moment soit en mode ''Thumb-EE'', soit en mode ARM normal, ce qui précise comment ces instructions doivent se comporter. Des instructions ont été ajoutées par ''Thumb-EE'', pour qu'il fasse son travail. L'entrée et la sortie du mode ''Thumb-EE'' se fait avec deux instructions : ENTERX et LEAVEX. Une fois en mode ''Thumb-EE'', le décodage des instructions ''thumb'' se fait avec les règles du ''Thumb-EE'', et non celles du ''thumb'' normal. Une première différence entre ''thumb'' normal et ''Thumb-EE'' est l'ajout de ce qui s'appelle un ''null check'' pour les instructions mémoire. Le ''null check'' est utilisé pour les instructions en mode d'adressage "base + indice" ou "Base + décalage", ou tout autre mode d'adressage avec un registre de base utilisés dans des calculs d'adresse. L'idée est de vérifier si l'adresse de base vaut zéro ou non. Si c'est le cas, le processeur lève une exception matérielle, qui est traitée par l'interprétation ou le compilateur JIT. Une telle situation est en effet signe d'une erreur d'adressage mémoire, qui doit être traitée par l'interpréteur. Typiquement, c'est signe qu'un pointeur n'a pas bien été initialisé, ce qui peut arriver avec du code interprété ou JIT. Les interpréteurs/JIT traitent généralement la situation en ajoutant un test avant toute instruction mémoire, pour vérifier si le pointeur accédé vaut zéro ou non. Avec ''Thumb-EE'', pas besoin d'ajouter les tests en questions : ils sont réalisés automatiquement lors de chaque instruction mémoire. : Les registres pour la pile, comme le pointeur de pile et le pointeur de ''frame'', sont aussi concernés. L'instruction CHKA vérifie que les accès à un tableau ne débordent pas en dehors du tableau en question. Par exemple, pour un tableau de 1024 éléments, elle vérifie si l'indice est compris entre 0 et 1024. Pour cela, elle vérifie si l'indice de tableau est bien dans l'intervalle adéquat et lève une exception matérielle si ce n'est pas le cas. Son utilité se comprend quand on sait que tous les accès à un tableau sont vérifiés par l'interpréteur. L'interpréteur est censé ajouter des instructions pour vérifier les indices, à savoir deux ou trois branchements. Avec CHKA, le test se fait en une seule instruction. D'autres modifications mineures de l'encodage des instructions sont aussi présentes, ainsi que des modifications pour les instructions LOAD/STORE. Précisément, dans les modes "base + indice" et "base + décalage", le décalage et l'indice subissent maintenant des décalages. L'idée est de simplifier le calcul d'adresse. De quoi économiser quelques instructions lors des calculs d'adresse, ce qui facilite le travail de l'interpréteur/JIT. Et des instructions LOAD/STORE précises voient leur comportement modifié pour faciliter la gestion de la pile de Java ou l'exécution des méthodes locales. Pour faciliter le travail de l'interpréteur, deux instructions HBP et HBLP ont été ajoutées. Elles branchent vers une fonction qui gère les exceptions logicielles (une fonctionnalité présente dans de nombreux langages, comme Java). La première effectue un branchement vers la fonction en question, la seconde sauvegarde l'adresse de retour avant de faire ce branchement. Les deux peuvent brancher vers 256 fonctions pré-déterminées, sans avoir à présenter leur adresse. ==La traduction binaire accélérée par le matériel== Plus haut, nous avons surtout parlé des compilateurs et des interpréteurs. Cependant, nous devons aussi parler de la '''traduction binaire'''. Elle traduit un programme écrit dans un code machine vers un autre code machine. Par exemple, elle traduit un programme compilé pour un CPU x86 vers un code machine ARM. En général, le système d'exploitation est généralement compilé pour le jeu d'instruction natif, mais il exécute des applications prévues pour le x86. Les applications sont traduites par le système d'exploitation, avant d'exécuter le code traduit. : Nous parlerons dans la suite de code machine source et de code machine cible, pour parler respectivement du code à traduire et du code obtenu après traduction. Idem avec d'autres termes comme architecture cible/source, ou jeu d'instruction cible/source. La traduction binaire est surtout utilisée pour des questions d'émulation ou de compatibilité. Par exemple, lorsque les Macintosh sont passés de processeurs Power PC vers des processeurs x86, le système d'exploitation Mac OS utilisait la traduction binaire pour convertir les anciennes applications Power PC vers du code x86. Les utilisateurs n'y ont vu que du feu. Le système de traduction binaire était appelé Rosetta 1. Par la suite, lors de la transition de processeurs x86 vers des processeurs Apple, Rosetta 2 a vu le jour. La traduction binaire a surtout été utilisée pour traduire du code x86 vers un autre jeu d'instruction. Il faut dire que le x86 est le jeu d'instruction dominant. De nombreuses entreprises ont eu pour ambition de briser l'hégémonie du x86 sur PC, en remplaçant le x86 par un jeu d'instruction plus performant, tout en gardant une compatibilité maximale. La traduction binaire était la seule solution pratique. Elle était le plus souvent intégralement réalisée en logiciel, comme c'était le cas sur les architectures Itanium avec le ''IA-32 Execution Layer''. ===Les généralités sur la traduction binaire assistée en matériel=== L'architecture source est presque tout le temps une architecture CISC, assez ancienne, qu'on souhaite émuler. Le choix de l'architecture cible se porte souvent sur une architecture VLIW, fort différente de l'architecture source, et ce pour obtenir une économie de matériel conséquente. Pour s'exécuter rapidement, le code traduit doit exploiter le parallélisme d'instruction, à savoir exécuter plusieurs instructions en même temps dans des unités de calcul séparées. Il est possible d'utiliser un processeur superscalaire avec exécution dans le désordre pour cela, mais au prix d'un cout important en transistors. Alors qu'en utilisant un CPU VLIW, c'est le traducteur binaire qui fait tout le travail d'extraction du parallélisme d'instruction. Et après tout, quitte à avoir un traducteur binaire, autant lui refiler le boulot d'optimisation. Compiler du code à la volée est certes assez lourd, mais qu'il existe des algorithmes efficaces pour regrouper des instructions indépendantes dans une seule instruction VLIW. Au passage, le processeur VLIW a plus de registres que le processeur source. Cela permet de faire du renommage de registres directement en logiciel. Le traducteur binaire n'hésite pas à changer les noms de registres entre instructions source et instruction VLIW cible, afin de supprimer des dépendances de données. En conséquence, cela ouvre des opportunités de parallélisme, qui sont exploitées lors du regroupement des opérations en instructions VLIW. Vous pourriez penser que le choix d'un CPU VLIW pour émuler du CISC est tout sauf optimal. Vous devez penser que la traduction binaire est d'autant plus simple que l'architecture source et cibles sont semblables. Dans les faits, ce n'est pas tellement traduire les instructions qui pose problème, mais plus la gestion du registre d'état, des exceptions matérielles, de la mémoire virtuelle, la différence entre gros-boutisme et petit-boutisme, de même que des différences d'adressage pour les périphériques. Le premier problème est la gestion des conditions, notamment en présence d'un registre d'état. En pratique, la traduction binaire s'utilise pour traduire du code CISC vers du code RISC ou VLIW. Les architectures source ont donc un registre d'état, qui est mis à jour non seulement par des instructions de test, mais aussi des instructions arithmétiques. L'architecture cible n'a elle pas de registre d'état, mais des registres à prédicats. La traduction de l'un vers l'autre est alors quelque peu compliquée. Et elle est d'autant plus compliquée que le registre d'état est la source de dépendances d'instruction implicites, qui réduisent les performances. Par exemple, si une instruction arithmétique modifie le registre d'état, cela peut impacter l'exécution d'une instruction ultérieure, qui lit ce registre d'état. Pour éliminer ces fausses dépendances, le traducteur binaire doit renommer ce registre en logiciel et détecter les dépendances utiles. Et le compilateur doit gérer les cas où il y a beaucoup de distance entre les deux, voire les cas où l'instruction dépendante n'a pas encore été analysée par le compilateur. Un autre problème est lié à la gestion des exceptions matérielles, et précisément des exceptions précises. Pour rappel, une instruction VLIW regroupe plusieurs opérations, opérations qui correspondraient à une instruction machine sur un CPU pas VLIW. En conséquence, plusieurs instructions du langage machine "source" sont regroupées en une seule instruction VLIW. Et il faut tenir compte du cas où une instruction source lève une exception. Dans le code VLIW, cela signifie qu'une opération lève une exception, et il faut annuler partiellement l'instruction VLIW associée. Et par partiellement, on veut dire que seules les opérations suivantes dans l'ordre du programme source doivent être annulées, puis ré-exécutées. Et c'est un sacré casse-tête ! Les processeurs VLIW utilisent comme solution le mécanisme d'exceptions différées des processeurs EPIC, vu il y a quelques chapitres. Pour résumer, le code est exécuté par blocs d'instruction, délimités par des branchements ou tout autre limite/barrière pertinente dans le code. Un bloc de code est compilé en deux versions : une version rapide sans exceptions matérielles, une version lente qui gère les exceptions précises. La version rapide s'exécute sans exécuter les exceptions. Cependant, les exceptions matérielles sont enregistrées, pour être prises en compte à la toute fin du bloc de code. La version rapide est exécutée en premier et elle mémorise si une exception matérielle a eu lieu. Puis une instruction vérifie si une exception a eu lieu et décide quoi faire. Si aucune exception n'a eu lieu, elle passe à la suite du programme, les résultats du bloc de code sont définitivement acceptés. Mais si une exception a eu lieu, tout est annulé. Le processeur est remis dans l'état initial, puis le code est ré-exécuté instruction par instruction de manière à gérer l'exception correctement. ===La traduction par pré-décodage=== Une première solution serait de faire la traduction binaire dans le cache d'instruction. Lors d'un défaut de cache, le code chargé depuis la RAM est traduit en code machine cible. Et c'est ce code machine cible qui est mémorisé dans le cache d'instruction et exécuté par le processeur. La technique marche sur le papier et n'est qu'une amélioration des techniques de pré-décodage vues il y a quelques chapitres. Cependant, le code obtenu est une traduction assez basique, qui n'incorpore pas d'optimisations dignes de ce nom. De plus, elle gère mal le cas où la taille du code source et cible sont potentiellement très différentes. Concrètement, elle est surtout utile pour traduire du code d'un processeur RISC vers un autre processeur RISC, dont les tailles d'instructions sont similaires. Elle ne permet pas d'émuler plusieurs jeux d'instructions différents, le cout en matériel (un circuit de pré-décodage par jeu d'instruction) serait trop important. ===Le projet DAISY d'IBM=== De très rares processeurs étaient conçus pour accélérer cette traduction binaire, afin de garder de bonnes performances. Les premiers à avoir étudié l'idée étaient IBM, avec leur projet DAISY (''Dynamically Architected Instruction Set from Yorktown''). Le projet de base était de convertir à la volée du code compilé pour des CPU Power PC, vers du code VLIW. Les chercheurs d'IBM avaient développé un algorithme de traduction dynamique efficace, ainsi qu'un processeur VLIW disposant d'optimisations spécifiques à la traduction binaire dynamique, à la volée. De nombreuses idées de ce projet ont été reprises ou re-découvertes par la société Transmetta, avec ses processeurs Crusoe et Efficieon, puis par NVIDIA avec son projet Denver. Les ingénieurs de ce projet ont étudié la possibilité d'émuler plusieurs jeux d'instructions différents sur un même processeur, notamment le s390 d'IBM et le x86. En théorie, cela demande juste d'avoir plusieurs programmes de traduction binaire : un pour le x86, un autre pour le s390, éventuellement un autre pour le Power PC. Mais cela ne s'est pas concrétisé. Mais les ingénieurs ont étudié quelle pourrait être l'architecture VLIW idéale pour ça. Par exemple, il fallait des additions 3-opérandes pour simplifier les calculs d'adresse. Une difficulté était la gestion du registre d'état, dont les bits ne sont pas mis à jour de la même manière sur le x86 et le s390 ou le Power PC. La différence entre gros-boutisme et petit-boutisme était aussi un problème, de même que des différences d'adressage pour les périphériques. Pour que la traduction binaire soit efficace, le processeur VLIW intègre diverses optimisations, comme les exceptions différées et les branchements multi-voies. La principale est la suivante : seul le code des boucles ou fonctions exécutées fréquemment est traduit en code VLIW, le reste du code est interprété. En effet, traduire du code binaire prend plus de temps qu'une simple interprétation. Pour du code qui ne sera exécutée qu'une seule fois, il est plus rapide d'utiliser l'interprétation. Par contre, pour du code exécuté beaucoup de fois, le cout de la traduction binaire est amorti, dilué sur N exécutions, compensé par le gain en temps d'exécution de ce code traduit. Au final, cela permet de ne traduire que le code qui le mérite. La détection du code fréquemment exécuté est réalisée dans le cœur VLIW. L'unité de branchement mémorise les derniers branchements rencontrés et le nombre de fois qu'ils ont été exécutés. Si ils ont été exécutés un certain nombre de fois, l'unité de branchement lève une exception matérielle, qui invoque le traducteur binaire. La routine de cette exception est le traducteur binaire proprement dit. En clair, la traduction binaire est démarrée quand le processeur détecte qu'une fonction a été exécutée plus de N fois, via une exception matérielle dédiée. La mesure du nombre d'exécution d'un branchement se fait dans l'unité de calcul dédiée aux branchements, avec l'aide d'une sorte de ''branch adress buffer'' modifié. Pour rappel, le ''branch adress buffer'' mémorise le ''Program counter'' de chaque branchement récemment rencontré. Ici, chaque entrée du ''branch adress buffer'' est associée à un compteur incrémenté à chaque exécution du branchement. ===Les processeurs Crusoe et Efficieon de Transmetta=== Les processeurs Crusoe Et Efficeron sont deux processeurs VLIW produits par la société Transmetta, une société californienne rachetée par NVIDIA. Ils étaient conçus pour exécuter spécifiquement des programmes x86, système d'exploitation inclus. L'idée derrière ce projet était d'exécuter du code x86 sans que les applications, ni même le système d'exploitation et le BIOS soient au courant ! Les processeurs Transmetta étaient en réalité conçus pour exécuter un programme unique, le ''Code Morphing Software'' (CMS), qui traduisait le code x86 en code VLIW. Transmetta ne voulait pas que son processeur VLIW soit exploité directement, sans le CMS. Il n'avait pas rendu public de compilateur pour traduire du C vers du code VLIW, il n'avait pas donné la documentation du jeu d'instruction VLIW. Des efforts de rétro-ingénieurie, documentés sur le site realworldtech, ont cependant permis de comprendre comment étaient encodées les instructions du processeur Crusoe. Comme pour le projet Daisy, le ''Code Morphing Software'' utilisait à la fois interprétation et traduction binaire, selon les besoins. Les instructions sont interprétées lors de leurs premières exécutions, mais le CMS bascule sur de la traduction en code VLIW après un certain nombre d'exécution. Ainsi, les boucles souvent exécutées sont traduites en code VLIW, alors que le reste du code est interprété. Le choix du code à traduire est le fait du CMS, il utilise des heuristiques complexes pour, qui ne sont pas connues dans le détail. Le ''Code Morphing Software'' est mémorisé dans une EEPROM, ce qui en fait un ''firmware'' situé en dessous du BIOS. Le CMS démarre ensuite le BIOS, qui lui-même démarre le système d'exploitation, qui lui-même démarre les pilotes de périphériques et les programmes. Le CMS se réserve les 16 premiers mébioctets de l'espace d'adressage. L'EEPROM du CMS est mappée dedans, mais n'en utilise que la moitié. Le reste est utilisé comme cache, pour mémoriser le code VLIW traduit par le CMS. Les deux processeurs intégraient aussi deux mémoires SRAM, utilisées par le CMS, appelées la ''local program memory'' (LPM) et la ''local data memory'' (LDM). La première contient du code qui gére les interruptions, la mémoire virtuelle, les exceptions matérielles, les problèmes d'alignement mémoire, et quelques fonctions très fréquemment utilisées par le CMS. La seconde est de la mémoire RAM utilisée par ces fonctions de la ''local program memory''. Les mémoires caches L1 et L2 sont séparées de ces deux mémoires. Le processeur Crusoe et Efficieon étaient des processeurs VLIW très simples. Ils n'avaient même pas de MMU, ni de fonctionnalités importantes sur les CPU x86. Pour Crusoe, les instructions VLIW étaient encodées sur 64 ou 128 bits, et regroupaient entre 2 à 4 opérations. Les instructions LOAD/STORE ne supportaient pas d'adressage indicé, juste de l'adressage indirect à registre. Crusoe avait 5 unités de calcul : deux ALU entières, une unité LOAD/STORE, une unité de branchement, une FPU. Efficieon doublait le nombre d'ALU entière et d'unité LOAD/STORE, les instructions VLIW passaient à 256 bits. Le processeur Crusoe contient 160 registres, dont 64 registres généraux, 32 registres flottants. Les registres généraux font 32 bits, ce qui est cohérent avec le fait que les CPU x86 de l'époque étaient des processeurs 32 bits. Quant aux registres flottants, ils faisaient 80 bits, ce qui colle avec la taille des registres flottants de la FPU x87 utilisée à l'époque. Sur les 64 registres généraux, seuls 48 étaient réellement utilisable pour mémoriser des opérandes. Une partie des registres généraux étaient utilisés pour la gestion de la pile, d'autres pour mémoriser l'état du CPU x86 émulé, un registre était un registre zéro non accesible en écriture. La gestion des exceptions est optimisée avec un système d'exceptions différées, le même que celui décrit plus haut. Le processeur utilise un système similaire, pour effectuer des lectures anticipées, qui a été expliqué dans le chapitre sur les processeurs VLIW/EPIC. Sauf qu'il s'agit là d'une optimisation pour gagner en performances en général, pas une aide utile pour gérer des exceptions précises ou toute autre subtilité de la traduction binaire. Pour gérer les exceptions différées et les lectures anticipées, le processeur mémorise l'état du processeur avant de démarrer un bloc de code. Pour cela, le processeur contient des ''shadow registers'', 48 registres entiers et 16 registres flottants. Vous remarquerez qu'il y a autant de registres entiers que de ''shadow registers'' entiers. Les registres entiers/flottants sont mémorisés dans les ''shadow registers'' avant d'exécuter un bloc de code, afin que le processeur puisse revenir à l'état de base. Une autre fonctionnalité liée est le ''gated store buffer''. Le principe est d'éviter toute écriture en mémoire RAM, tant que le code VLIW n'a pas émis d'instruction ''commit''. L'idée est que les écritures sont conservées dans la ''store queue'' de l'unité LOAD/STORE. Pour rappel, la ''store queue'' met en attente les écritures soit tant que les écritures ne sont pas terminées, soit tant que la RAM est occupée. L'idée est que les écritures sont accumulées dans la ''store queue'' tant que l'instruction ''commit'' n'a pas eu lieu. Le ''store queue'' est "déversé dans la RAM" seulement quand l'instruction ''commit'' s'exécute. ===Les CPU NVIDIA Denver=== Nvidia a tenté quelque chose de similaire avec son projet Denver. NVIDIA a racheté l'entreprise Transmetta, ses brevets, et a tenté de refaire la même chose. Le projet initial était de traduire du code x86 vers un jeu d'instruction VLIW propriétaire, comme l'a fait Transmetta. Mais NVIDIA n'a pas réussi à acquérir la licence du jeu d'instruction x86 et s'est rabattu sur le jeu d'instruction ARM. Le processeur né de ce projet est le Tegra K1-64 CPU. Il s'agit d'un processeur composé d'un cœur VLIW, couplé à deux décodeurs d'instructions ARM. Les deux décodeurs ARM traduisent une paire d'instructions ARM consécutives en instructions VLIW, pour les exécuter sur le cœur VLIW. Le CPU peut fonctionner selon deux modes : ARM et VLIW. En somme, le processeur supporte deux jeux d'instruction : le jeu d'instruction ARM, et un jeu d'instruction VLIW. En mode ARM, les instructions ARM sont chargées depuis le cache, traduites en VLIW par les décodeurs ARM, puis exécutées par le cœur VLIW. En mode optimisé, le CPU exécute des instructions VLIW chargées depuis le cache d'instruction, les décodeurs ne sont pas utilisés. : Le CPU supporte en réalité deux jeux d'instruction ARM : ARM8 et 7. Mais ils correspondent au même mode pour le CPU. Un point important est que le processeur peut changer très rapidement de mode, en à peine quelques cycles d'horloges. On n'est clairement pas dans le cas des CPU x86, qui mettent des plombes pour passer du mode 32 à 64 bits et inversement. La commutation est tellement rapide qu'on peut considérer que le processeur supporte deux jeux d'instruction simultanément. Et le support simultané de deux jeux d'instruction facilite l'implémentation de la traduction binaire. Encore une fois, inutile de traduire du code qui ne sera exécutée qu'une seule fois, mieux vaut l'exécuter directement dans le mode ARM. Surtout que le processeur supporte un mode ARM qui permet d'exécuter du code ARM sans perte de performance. Par contre, le code fréquemment utilisé, notamment dans des boucles critiques, est traduit en VLIW. Au final, les performances sont optimisées, en limitant le travail du logiciel traducteur binaire. La détection du code fréquemment exécuté se fait différemment que pour les processeurs Transmetta et Daisy. L'unité de branchement ne détecte pas directement le code beaucoup exécuté, même si elle a un rôle à jouer. A la place, la détection est réalisée en logiciel, par un ''thread'' dédié, qui s'exécute sur un cœur séparé. Il détecte le code fréquemment utilisé en regardent les branchements exécutés récemment. Pour cela, le processeur mémorise l'historique des branchements pris récemment et copie cette historique dans une mémoire tampon dédiée, partagée entre tous les cœurs. Vu qu'elle est partagée, le ''thread'' de détection a accès à l'historique sans pertes de performances, seule la copie de l'historique dans ce tampon a un cout en performance, pas son partage. Le code VLIW obtenu après traduction binaire est mémorisé dans la mémoire RAM, dans une portion spécialement réservée pour. Elle fait 128 mébioctets, et est appelée le '''cache d'optimisation''' par NVIDIA. Le cache d'optimisation est protégé en écriture et seul le traducteur binaire peut écrire dedans. Au passage, le traducteur binaire est du code VLIW, ce qui fait qu'il est placé dans le cache d'optimisation. Le processeur détecte automatiquement quand une fonction pour laquelle le code compilé équivalent est disponible. Pour cela, l'unité de chargement contient une table de correspondance entre l'adresse de la fonction ARM, et l'adresse de son équivalent compilé. Quand une fonction est appelée, l'unité de chargement regarde l'adresse de destination du branchement. Si l'adresse est dans cette table, elle récupère l'adresse de la fonction compilée et branche vers celle-ci. Le processeur dispose d'un cache d'instruction de 128 kibioctets, ce qui est très important, mais nécessaire vu la taille des instructions VLIW. Le coeur VLIW dispose de ses propres décodeurs, d'un ''scoreboard'', de 7 unités de calcul et de registres. Pour les unités de calcul, il a deux FPU, deux ALU, une unité de branchement, et deux unités LOAD-STORE faisant aussi ALU. Un point important est que le CPU dispose de 64 registres entiers et de 64 registres flottants. C'est deux fois plus que supporte le jeu d'instruction ARM. En mode ARM, seule la moitié des registres est utilisée. Mais en mode optimisé, le traducteur binaire utilise bien les 64 registres, grâce à une sorte de renommage de registres logicielle. Pour simplifier la traduction binaire, le processeur supporte les techniques vues précédemment. Le processeur intègre un ''gated store buffer'' similaire à celui des processeurs Transmetta. Les techniques d'exceptions différées sont aussi supportées, comme sur les processeurs Transmetta. Il a aussi une unité de préchargement décrite par NVIDIA comme agressive, avec support du préchargement de type anticipé (''runahead''). ===L'émulation de la mémoire virtuelle=== Pour finir, il faut parler de la mémoire virtuelle avec la traduction binaire. Sur le processeur source, le processeur gère à la fois des adresses physiques et virtuelles et sa MMU fait la conversion entre les deux. Sur le processeur VLIW, la MMU est simulée par le programme de traduction binaire, partiellement ou totalement. L'implémentation exacte varie suivant que l'on parle du projet Daisy, des processeurs Transmetta ou de Denver. Sur les processeurs Transmetta, il n'y a pas de MMU ni de mémoire virtuelle. A la place, le programme de traduction binaire émule la mémoire virtuelle du processeur source. Le processeur VLIW ne gère que des adresses physiques, rien d'autre. L'espace d'adressage physique du processeur VLIW a la même taille que celui du processeur émulé, ici des processeurs x86. Il est vraisemblable que les adresses physiques utilisées par le processeur x86 sont les mêmes que celles du processeur VLIW. Sur le projet Daisy, le processeur VLIW gère la mémoire virtuelle via pagination, comme les processeurs émulés. Ce qui fait qu'il gère un espace d'adressage virtuel et un espace d'adressage physique. Pour éviter toute confusion, nous parlerons d'adresse physique/virtuelle VLIW pour les adresses physiques/virtuelles du processeur VLIW, d'adresse virtuelle/physique source pour celle du jeu d'instruction traduit, à savoir du Power PC ou du s390, éventuellement de l'x86. Il faut alors faire le lien entre adresses physiques source et adresses virtuelles VLIW. Pour cela, rien de plus simple : il y a correspondance parfaite. L'adresse physique source numéro N correspond à l'adresse virtuelle VLIW numéro N. Cependant, cela signifie que tout l'espace d'adressage virtuel VLIW serait utilisé par le code à traduire. En réalité, il faut ajouter de la place pour le code traduit et le programme de traduction binaire. La conséquence est que l'espace d'adressage virtuel VLIW est plus large que l'espace d'adressage source. L'espace d'adressage virtuel VLIW est découpé en trois sections : une section pour le code Power PC à traduire, une autre pour le code traduit en VLIW, et une dernière réservée au traducteur binaire. Le programme de traduction est placé dans une ROM mappée en mémoire dans la seconde section. Le reste de la seconde section est réservé à une mémoire RAM utilisée par le programme de traduction. [[File:Mémoire virtuelle sur le projet Daisy.png|centre|vignette|upright=1.5|Mémoire virtuelle sur le projet Daisy]] Il faut noter que chaque page physique source, une fois traduite, correspond à N pages virtuelles VLIW. La raison est que le code traduit prend plus de place que le code machine originel. En conséquence, on doit utiliser une page finale plus grande. Pour le reste, des pages contiguës en mémoire physique source sont elles aussi contiguës en mémoire virtuelle VLIW. Le calcul d'adresse est donc simple : l'adresse physique source est multipliée par N, puis on ajoute l'adresse de base à laquelle commence la section pour le code traduit. ==Les processeurs à jeu d'instruction reconfigurable== Nous venons de voir comment accélérer en matériel la traduction binaire. Mais il existe une alternative qui permet de se passer de traduction binaire, complétement. L'idée est d'avoir un processeur avec un '''jeu d'instruction reconfigurable'''. Le nom est assez parlent : on peut configurer le processeur de manière à supporter le jeu d'instruction de son choix ! Par exemple, on pouvait reconfigurer un CPU de manière à ce qu'il supporte le jeu d'instruction x86, ou qu'il exécute du ''bytecode'' Java, ou du code ARMv8, etc. De tels processeurs sont encore plus rare que ceux vu avant dans ce chapitre, mais ils ont existé ! Les processeurs de ce type avaient un ''microcode réinscriptible'', à savoir que c'était des processeurs microcodés pour lesquels on pouvait modifier le microcode. L'idée était qu'en changeant le microcode, on changeait le jeu d'instruction du processeur. Pour cela, le microcode était associé à une mémoire SRAM dans laquelle le microcode était copié au démarrage du CPU. Il y avait donc une micro-ROM et une micro-RAM. Les instructions étaient décodées en utilisant le microcode dans la micro-SRAM, la micro-ROM ne servait qu'au démarrage. En modifiant le contenu de la micro-SRAM, on pouvait altérer le microcode, voire le remplacer totalement. Un exemple est celui du Burroughs B1700, et il est intéressant d'étudier en détail son architecture. Son architecture est décrite dans le livre "Interpreting Machines : Architecture and Programming of the Bl700/Bl800 Series", écrit par Elliott I. Organick et James A. Hinds. Une partie de ce qui vfa suivre est un très court résumé de ce livre. ===Une architecture bit-adressable=== Si avoir un microcode réinscriptible est suffisant, quelques choix de ''design'' facilitent l'émulation de jeux d'instructions variés. Par exemple, le CPU Burroughs B1700 avait ajouté une fonctionnalité très importante : il était bit-adressable. En clair, il pouvait adresser la mémoire bit par bit, si nécessaire. L'avantage est que cela permettait d'émuler des architectures dont la taille des registres/''byte''/mots était très différente. Un autre avantage est que cela facilitait la gestion des instructions de taille variable. Détaillons rapidement le premier avantage. Le processeur était un CPU 24 bits, mais il pouvait émuler des processeurs 16 bits, 8 bits, 4 bits, 1 bit, 9 bits, ou toute autre valeur. Pour cela, le processeur lisait des mots de 24 bits et utilisant un ''barrel shifter'' pour sélectionner les bits adéquats. Par exemple, pour un accès de 16 bits, la donnée lue en mémoire était masquée de manière à ne garder que les 16 bits de poids faible. Le ''barrel shifter''/circuit de masquage était placé directement avant le bus mémoire, sur le trajet des données/instructions. ===La gestion de la taille des instructions=== Les instructions à décoder n'ont pas de longueur fixe, à savoir qu'elles peuvent faire 16 bit avec tel jeu d'instruction, 8 bits avec un autre, 32 bits sur un autre, etc. Elles peuvent aussi être de longueur fixe ou variable ! Le B1700 intégrait donc un support des instructions de longueur variable, qui était "détourné" pour gérer le fait que des jeux d'instruction différents ont des instructions de taille différentes. Notons que vu que l'architecture est bit-adressable, les contraintes d'alignement des instructions disparaissent, ce qui a des avantages qu'on va détailler dans ce qui suit ! L'implémentation des instructions de taille variable ajoutait un registre avant le registre d'instruction. Le registre d'instruction proprement dit était un registre M de 24 bits. Il était précédé par un registre T, dans lequel l'instruction était assemblée, en combinant plusieurs morceaux. Le registre T est relié à un ''barrel shifter'', différent au précédent, qui permettait de masquer, décaler et extraire une donnée de ce registre. Si l'instruction est codée est codée sur moins de 24 bits, le registre T se contente de masquer les bits en trop. Si l'instruction chargée dépasser 24 bits, le chargement se faisait en plusieurs passes, avec plusieurs lectures en mémoire. Chaque passe découpait l'instruction pour extraire des '''champs''' : l'opcode, les opérandes, les adresses absolues, les constantes immédiates, les numéros de registre, etc. Et ils étaient copiés dans des registres interne au processeur. Par exemple, il servait à extraire l'opcode de l'instruction. Ou encore, pour une instruction de branchement direct, il pouvait extraire l'adresse à laquelle brancher de l'instruction. Le chargement de l'instruction se faisait alors champ par champ. Notons que vu que l'architecture est bit-adressable, les contraintes d'alignement des instructions disparaissent ! Quand on charge une instruction, peut importe qu'elle soit ou non alignée sur 24 bits, elle sera placée dans les bits de poids faible, il suffira de masquer les bits de poids fort inutiles. Par exemple, si on charge une instruction de 16 bits, les 16 bits seront copiés dans les 16 bits de poids faible du registre T, il suffira de masquer l'octet de poids fort. Pas besoin de faire de décalages pour gérer des instructions mal alignées... Pour charger une instruction, il faut connaitre plusieurs informations : l'adresse de son premier bit, sa taille. Pour cela, le B1700 contient deux registres, nommés ''Field Adress'' (FA) et ''Field Length'' (FL), qui indiquent l'adresse du premier bit et la taille de l'instruction à charger. Le premier fait 24 bits, ce qui permet de gérer des adresses de 24 bits. N'oubliez pas que le processeur est bit-adressable, ce qui permet d'adresser 16 Mébi-bits, soit 2 mébioctets. Le registre pour la taille fait lui 16 bits, ce qui permet de gérer des instructions de 65536 bits, soit 8192 octets ! Leur nom commence par ''field'', car le processeur peut gérer des instructions de taille variable et/ou plus longues que 24 bits. Dans ce cas, le chargement de l'instruction se fait champ par champ. Pour donner un exemple, prenons une instruction composée de 20 champs de 12 bits chacun. Les champs sont chargés un par un dans le registre T. Le registre FA est alors initialisé avec l'adresse de l'instruction, il est incrémenté de 12 à chaque cycle. Le registre FL est initialisé avec la taille de l'instruction, soit 20 * 12 = 240 bits, et il est décrémenté de 16 à chaque cycle. Précisons cependant que la taille d'un champ peut être changée d'un cycle à l'autre. Par exemple, le processeur peut charger un opcode de 8 bits, l'interpréter, détecter que l'instruction demande ensuite de charger une adresse de 16 bits. Dans ce cas, le registre FL est immédiatement altéré pour charger 16 bits, au lieu des 8 bits de l'opcode immédiatement précédent. ==Les jeux d'instructions dédiés à un langage de programmation== De rares processeurs sont conçus pour un langage de programmation en particulier. On appelle ces processeurs, conçus pour des besoins particuliers, des '''processeurs dédiés'''. Par exemple, les fameux ''Burrough E-mode'' B5000/B6000/B7000 étaient spécialement conçus pour exécuter de l'ALGOL-60. Leurs cousins B2000/B3000/B4000 étaient eux conçus pour le COBOL. Des langages fonctionnels ont aussi eu droit à leurs processeurs dédiés. Le prolog en est un bel exemple, avec les superordinateurs de 5ème génération qui lui étaient dédié. On peut aussi citer les machines LISP, dédiés au langage LISP, qui datent des années 1970. Elles étaient capables d’exécuter certaines fonctions de base du langage directement dans leurs circuits : elles possédaient notamment un ''garbage collector'' câblé dans ses circuits ainsi que des instructions machines supportant un typage déterminé à l’exécution. Les processeurs dédiés ont eu leur heure de gloire au début de l'informatique, à une époque où les langages de haut niveau venaient d'être inventés. À cette époque, les compilateurs n'étaient pas performants et ne savaient pas bien optimiser le code machine. Il était alors rationnel, pour l'époque, de rapprocher le code machine cible et le langage de programmation de haut niveau. De nombreuses architectures dédiés ont ainsi été inventées, avant que les concepteurs se rendent compte des défauts de cette approche. Les défauts en question ne sont pas nombreux, mais assez simples à comprendre. Premièrement, elles sont très rapides pour un langage de programmation en particulier, mais sont assez mauvaises pour les autres, d'où un problème de "compatibilité". Ajoutons à cela que les langages de programmation peuvent évoluer, devenir de moins en moins populaires/utilisés, ce qui rend la création d'architectures généralistes plus pertinente. Enfin, les architectures dédiées sont évidemment des processeurs CISC, pour implémenter les nombreuses fonctionnalités des langages évolués. Et les défauts des CISC sont assez rédhibitoires à l'heure actuelle. À l'heure actuelle, les algorithmes des compilateurs se sont améliorés et savent nettement mieux utiliser le matériel. Ils produisent du code machine efficace, ce qui rend les architecture dédiées bien moins intéressantes. Si on ajoute les défauts de ces architectures dédiées, par étonnant que les architectures dédiées aient presque disparues. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'accélération matérielle de la virtualisation | prevText=L'accélération matérielle de la virtualisation | next=Le matériel réseau | nextText=Le matériel réseau }} </noinclude> 256uno1d2y32fqzmocxfj226w4lvl63 764010 764008 2026-04-19T15:54:41Z Mewtow 31375 /* Une architecture bit-adressable */ 764010 wikitext text/x-wiki De nos jours, la majorité des programmeurs programment dans des langages de haut niveau. Il est très rare pour eux d'avoir à utiliser de l'assembleur, et encore moins un langage machine. Les programmes écrits dans un langage de haut niveau sont traduits en langage machine par un logiciel appelé le compilateur. [[File:Compilation.PNG|centre|vignette|upright=2.5|Compilateur : principe.]] Il se trouve que quelques rares processeurs sont conçus pour faciliter le travail du compilateur. Et ce sont ces processeurs que nous allons voir dans ce qui suit. Ils représentent une catégorie de jeux d'instruction à part, qui n'a pas vraiment de nom. ==La compilation et l’interprétation== Un compilateur traduit donc du code source, écrit dans un langage de haut niveau, vers du langage machine. La traduction est rarement directe. En général, le compilateur traduit le code source en assembleur, qui est lui-même traduit en langage machine. L'assembleur est une représentation textuelle du langage machine alors que le code machine est du binaire exécutable par le processeur. La traduction se fait donc en deux étapes, la '''compilation''' proprement dite et l''''assemblage''', réalisées respectivement par un compilateur et un assembleur. Il faut parfois rajouter une troisième phase d'édition des liens, que nous passons volontairement sous silence. {| |[[File:Code C.png|vignette|200px|Le code source, ici en C.]] |[[File:Assem.png|vignette|250px|Le code assembleur.]] |[[File:Binary file - hello world (C programming).png|vignette|150px|Le code machine.]] |} ===L'interprétation et la compilation à la volée=== Une alternative à la compilation est l''''interprétation''', qui transforme le code source en code machine à la volée. Avec l'interprétation, le code source est passé à un logiciel appelé l''''interpréteur''', qui exécute le code source ligne de code par ligne de code. Une ligne de code est traduite en code machine, qui est exécutée, puis l'interpréteur passe à la ligne suivante. [[File:Kaantotulkkaus.png|centre|vignette|upright=3|Compilation versus interprétation.]] Un défaut de l'interprétation est son cout en performance, qui est assez important. Aussi, l'interprétation stricte a évolué vers un hybride entre interprétation et compilation. L'idée est qu'une partie peu importante du code source est interprétée, alors que le code important est compilé juste avant d'être exécuté. Pour le dire autrement, le code source est partiellement compilé à la volée, juste avant son exécution. Aussi, on parle de '''compilation à la volée'''. Le terme en anglais est ''Just In Time compilation'', abrévié en JIT. En soi, le JIT est associé à de l'interprétation, les JIT sont en réalité des hybrides interpréteurs-compilateurs. Un point important est que compiler du code à la volée est assez lourd, cela a un cout en performance. Par contre, le code compilé s'exécute plus vite que du code interprété. Si on exécute le code compilé une seule fois, le cout de la compilation l'emporte sur le gain à l'exécution. Mais le code compilé est réutilisé autant de fois que nécessaire. Le gain à l'exécution est donc multiplié par le nombre d'exécutions, alors que le cout de la compilation est répartit sur plusieurs exécutions, il est amorti. Les portions du code sont choisies de manière à avoir un gain en performance maximal. L'idée générale est que le code exécuté très souvent est compilé, alors que du code exécuté pas souvent est interprété. Typiquement, les boucles les plus souvent exécutées sont compilées, le code exécuté une seule fois est interprété. ===Les langages intermédiaires=== Les compilateurs modernes passent par un '''langage intermédiaire''' pour faire la transformation en code machine. Le compilateur traduit le langage de haut niveau en langage intermédiaire, puis traduit le langage intermédiaire en code machine cible. Le langage intermédiaire est parfois appelé le '''''bytecode''''', et ce terme recouvre aussi le code écrit avec du ''bytecode''. L'interprétation aussi peut passer par l'intermédiaire d'un un ''bytecode'', ce n'est pas limité aux compilateurs proprement dit. {| |[[File:Intermediate representation scheme.png|centre|vignette|upright=1.5|Représentation intermédiaire d'un compilateur.]] |[[File:Code interpreter scheme.png|centre|vignette|upright=1.5|Fonctionnement d'un interpréteur.]] |} Faire ainsi a de nombreux avantages, le principal étant d'avoir un compilateur capable de traduire un langage de haut niveau vers plusieurs jeux d'instructions différents. Cela permet d'avoir, par exemple, un compilateur qui traduit du C soit en code machine x86, soit en code machine pour un CPU ARM, soit pour un CPU POWERPC, etc. [[File:Compiler design.svg|centre|vignette|upright=3|Compiler design]] Pour l'interprétation, l'usage d'un ''bytecode'' a d'autres avantages. Pour l'exploiter, il faut distribuer non pas le code source, mais le ''bytecode''. En clair, l'interpréteur ne prend pas en entrée le code source, mais du ''bytecode'' déjà compilé. La traduction est alors beaucoup plus simple, car le code source a déjà été partiellement compilé, par la traduction en ''bytecode''. Interpréter du code source est en effet assez compliqué : il faut effectuer des étapes d'analyse lexicale, sémantique, et bien d'autres. Avec du ''bytecode'', ces étapes ont été réalisées lors de la compilation du ''bytecode'', l'interpréteur a alors peut de choses à faire. L'interprétation du ''bytecode'' se fait instruction par instruction, au niveau du ''bytecode''. Une instruction du ''bytecode'' est traduite en une instruction machine équivalente, qui est exécutée. Si l'instruction est un peu complexe, il exécute une fonction/procédure qui fait la même chose. Pour résumer, le ''bytecode'' est ensuite traduit à la volée et exécuté instruction par instruction par un logiciel appelé l'interpréteur. [[File:Control table.png|centre|vignette|upright=2|Control table]] Le langage intermédiaire peut être vu comme l'assembleur d'un processeur, n'existe pas forcément dans la réalité, mais dont le jeu d'instruction est décrit en détail. Le processeur en question est appelé une '''machine abstraite''', ou encore une ''machine virtuelle''. Nous utiliserons le terme de machine abstraite dans ce qui suit. La machine abstraite n'est pas la même suivant que l'on cible la compilation ou l'interprétation/JIT. Pour l'interprétation/JIT, la machine abstraite est souvent une machine à pile, car cela simplifie grandement la traduction en code machine final. L'interprétation demande que la traduction du ''bytecode'' en code machine soit la plus simple possible. Et cela demande de prendre en compte pas mal de détails. Par exemple, les différents jeux d'instruction existants n'ont pas le même nombre de registres, ce qui pose problème lors de la traduction du ''bytecode''. Lors de la transformation en code machine, un algorithme d'allocation de registres se débrouille pour traduire le code intermédiaire en code qui utilise un nombre limité de registres. Pour améliorer cette allocation de registres, il y a deux solutions niveau ''bytecode'' : soit utiliser un nombre illimité de registres, soit utiliser une machine à pile. Pour les machines à pile, il existe un algorithme simple et rapide pour traduire un code écrit pour une machine à pile en un code écrit pour un processeur avec des registres, qui se débrouille pas trop mal pour allouer efficacement les registres. C'est un avantage assez important pour les langages interprétés. ==Les processeurs qui exécutent du bytecode== Le ''bytecode'' est un code machine, ce qui signifie qu'il peut en théorie s'exécuter sur un processeur qui implémente le jeu d’instruction associé. Si le jeu d'instruction d'un ''bytecode'' est souvent une description censée être fictive, elle n'en reste pas moins un jeu d'instruction et des caractéristiques précises, qu'on peut l'implémenter en matériel ! Néanmoins, tous les ''bytecode'' ne sont pas égaux de ce point de vue. Certains sont faciles à implémenter en matériel, d'autres non. Les ''bytecodes'' implémentés en matériel sont ceux dont les machines abstraites sont des machines à pile. Un premier exemple est celui des processeurs Pascal MicroEngine, qui exécutaient directement le bytecode du langage Pascal, le fameux UCSD P-code. Un second exemple est la '''machine SECD''', qui sert de langage intermédiaire pour certains compilateurs de langages fonctionnels. Elle a été implémentée en matériel par plusieurs équipes, notamment par les chercheurs de l'université de Calgary en 1989. Dans le même genre, quelques processeurs simples étaient capables d’exécuter directement le ''bytecode'' du langage FORTH. Le FORTH, un des premiers langages à pile de haut niveau, possède de nombreuses implémentations hardware et est un des rares langages de haut niveau dont le ''bytecode'' a été utilisé comme langage machine sur certains processeurs. Par exemple, on peut citer le processeur FC16, capable d’exécuter nativement du ''bytecode'' FORTH. ===Les processeurs Java=== Le cas le plus impressionnant est celui de la machine virtuelle Java, qui est un design de processeur comme un autre. En temps normal, le ''bytecode'' Java est compilé ou interprété, mais certains processeurs exécutaient du ''bytecode'' Java directement, sans interprétation ni compilation. Ils sont appelés des '''processeurs Java'''. On peut citer les processeurs ARM disposant de l'extension Jazelle, du JEMCore et du aJ-100 de aJile Systems, le picoJava II de Sun, et quelques autres. Il y a aussi eu quelques projets de recherche ou processeurs open source, comme Komodo, jamuth, le ''Java Optimized Processor'' et quelques autres projets du même genre. L'implémentation de la machine virtuelle Java n'était cependant pas complète. Quelques instructions complexes n'étaient pas gérées par le processeur et devaient être émulées en logiciel. Mais la grosse majorité des instructions l'était. Les instructions non-supportées étaient émulées avec la méthode ''trap and emulate'', à savoir qu'un opcode inconnu déclenchait une exception matérielle ''opcode invalide'', dont la routine pouvait émuler les instructions qui doivent l'être. Le processeur aJ-100 de aJile est capable d'exécuter la quasi-totalité des instructions de la JVM Java, seules deux instructions faisant exception. Les '''accélérateurs Java''' sont une solution intermédiaire, qui permet de traduire à la volée du ''bytecode'' Java en instructions RISC, exécutées sur un autre processeur. Il s'agit formellement de traduction binaire, mais je préfère en parler ici. L'idée est de combiner un processeur RISC avec un coprocesseur Java. Le coprocesseur lit le ''bytecode'' Java et le traduit en instructions RISC, ces dernières étant exécutées sur le processeur RISC. Le JA108 de Nozomi était un coprocesseur de ce genre, au même titre que le coprocesseur JSTAR de JEDI. L'extension Jazelle de certains CPU ARM fonctionnait vraisemblablement sur un principe similaire, sauf que le coprocesseur était intégré dans le processeur ARM, entre le cache d'instruction et le décodeur d'instructions ARM. L'intérêt des processeurs Java n'est pas qu'une question de performance. Certes, ils permettent d’exécuter plus vite les programmes compilés en ''bytecode'', comme des programmes Java pour la JVM Java. Mais l'intérêt est aussi de faire des économies de mémoire RAM et ROM. Je rappelle que la machine virtuelle Java est une machine à pile, ce qui fait que sa densité de code est excellente. Les programmes en ''bytecode'' Java sont donc très petits, et prennent peu de place en mémoire ROM. De plus, utiliser un processeur Java permet de se passer d'interpréteur ou de JIT Java, ce qui économise encore plus de mémoire. ===Transformer un processeur RISC en machine à pile=== La plupart des processeurs Java utilisent en interne une architecture à registre généraux, qui émule une machine à registre en microcode. Quelques registres sont réservés pour l'état de la machine virtuelle, avec au minimum un registre réservé pour le pointeur de pile, un autre pour un pointeur vers un ''pool'' de constantes, et quelques autres. D'autres registres servent à mémoriser le haut de la pile, à savoir le sommet de la pile et quelques opérandes situées juste en-dessous. Par exemple, le processeur picoJava II gardait les 64 opérandes au sommet de la pile dans 64 registres généraux, séparés des registres pour les pointeurs et l'état de la JVM. Une optimisation possible, envisagée sur les processeurs picoJava, était une sorte de macro-fusion sous stéroïde. L'idée était de fusionner certaines séries d'instructions Java en une seule instruction machine. Typiquement, pour faire une opération arithmétique, une machine à pile empile deux opérandes et exécute une opération, ce qui prend trois instructions machine. La spécification picoJava 1 a proposé d'envoyer directement la seconde opérande en entrée de l'ALU, ce qui fusionnait l'empilement de la seconde opérande et l'opération arithmétique. Le processeur picoJava II va plus loin et fusionne les trois instructions en une seule instruction RISC, agissant sur des registres. Mais la majorité des processeurs Java ne faisaient pas cela, car cela complexifie grandement le décodeur. Les processeurs ''bigfoot'' de DCT utilisaient un système différent pour émuler une pile avec des registres. Leur banc de registre de 64 bits était coupé en deux : les 16 premiers registres étaient utilisés pour la pile, les 48 suivants étaient utilisés pour autre chose. L'idée est que le registre 0 mémorise un emplacement vide, le registre 1 contient le sommet de la pile, le registre 2 l'opérande sous le sommet de la pile, et ainsi de suite. Les 15 premières opérandes de la pile sont donc mémorisées dans les registres. Les instructions écrivent leur résultat dans le registre adéquat, puis tous les registres sont décalés pour que le résultat passe dans le registre R1. Par exemple, les instructions PUSH chargent une opérande dans le registre 0, puis tous les registres sont décalés de un rang, pour que l'opérande chargée soit dans le registre 1. Le registre 0 devient alors le registre numéro 1, le registre 1 devient le 2, et ainsi de suite. Par contre, les opérations arithmétiques font autrement. Une instruction d'addition, par exemple, lit les opérandes dans le registre 1 et 2, puis place son résultat dans le registre 2 (les deux opérandes sont dépilées). Puis, les registres sont décalés d'un rang pour que ce registre 2 devienne le 1. Les registres ne sont en réalité pas décalés, il n'y a pas de transferts entre registres. A la place, les 16 premiers registres utilisaient une forme limitée de renommage de registres, similaire au banc de registre tournant utilisé sur les processeurs EPIC comme l'Itanium. Les numéros de registres sont renommés, décalés, à chaque fois qu'on empile ou dépile une opérande. Pour faire le renommage de registres, processeur contient un ''register stack counter'' de 4 bits, qui est incrémenté à chaque fois qu'on empile une donnée, et est décrémenté quand on dépile une donnée. Quand on veut accéder à un opérande, le numéro de registre architectural associé est traduit en numéro de registre physique, en additionnant le ''register stack counter'' (modulo 16, pour rester sur 4 bits). ==L'accélération de l'interprétation/JIT : le ''Thumb-EE'' d'ARM== Quelques processeurs ont ajouté des instructions pour faciliter le travail des interpréteurs/JIT. Par exemple, en 2005, ARM a ajouté le mode ''Thumb Execution Environment'', qui faisait cela. Il reprenait le jeu d'instruction compact ''thumb'' et ajoutait quelques instructions et modifiait le comportement d'instructions existantes. Le jeu d'instruction ''Thumb-EE'' a été ajouté sur les CPU ARM en 2005, mais a été déprécié en 2011, par manque d'utilité. Mais la tentative mérite qu'on s'attarde dessus. Les CPU de gamme M, déstinés à l'embarqué, n'ont jamais supporté ''Thumb-EE'', vu que de tels processeurs ne sont pas conçus pour exécuter du code interprété/JIT. Le mode ''Thumb-EE'' est un mode d'exécution, séparé de l'ARM normal. Ainsi, une instruction ''Thumb-EE'' et une instruction ARM peuvent avoir le même encodage en binaire, mais se comporter différemment. Le processeur est à tout moment soit en mode ''Thumb-EE'', soit en mode ARM normal, ce qui précise comment ces instructions doivent se comporter. Des instructions ont été ajoutées par ''Thumb-EE'', pour qu'il fasse son travail. L'entrée et la sortie du mode ''Thumb-EE'' se fait avec deux instructions : ENTERX et LEAVEX. Une fois en mode ''Thumb-EE'', le décodage des instructions ''thumb'' se fait avec les règles du ''Thumb-EE'', et non celles du ''thumb'' normal. Une première différence entre ''thumb'' normal et ''Thumb-EE'' est l'ajout de ce qui s'appelle un ''null check'' pour les instructions mémoire. Le ''null check'' est utilisé pour les instructions en mode d'adressage "base + indice" ou "Base + décalage", ou tout autre mode d'adressage avec un registre de base utilisés dans des calculs d'adresse. L'idée est de vérifier si l'adresse de base vaut zéro ou non. Si c'est le cas, le processeur lève une exception matérielle, qui est traitée par l'interprétation ou le compilateur JIT. Une telle situation est en effet signe d'une erreur d'adressage mémoire, qui doit être traitée par l'interpréteur. Typiquement, c'est signe qu'un pointeur n'a pas bien été initialisé, ce qui peut arriver avec du code interprété ou JIT. Les interpréteurs/JIT traitent généralement la situation en ajoutant un test avant toute instruction mémoire, pour vérifier si le pointeur accédé vaut zéro ou non. Avec ''Thumb-EE'', pas besoin d'ajouter les tests en questions : ils sont réalisés automatiquement lors de chaque instruction mémoire. : Les registres pour la pile, comme le pointeur de pile et le pointeur de ''frame'', sont aussi concernés. L'instruction CHKA vérifie que les accès à un tableau ne débordent pas en dehors du tableau en question. Par exemple, pour un tableau de 1024 éléments, elle vérifie si l'indice est compris entre 0 et 1024. Pour cela, elle vérifie si l'indice de tableau est bien dans l'intervalle adéquat et lève une exception matérielle si ce n'est pas le cas. Son utilité se comprend quand on sait que tous les accès à un tableau sont vérifiés par l'interpréteur. L'interpréteur est censé ajouter des instructions pour vérifier les indices, à savoir deux ou trois branchements. Avec CHKA, le test se fait en une seule instruction. D'autres modifications mineures de l'encodage des instructions sont aussi présentes, ainsi que des modifications pour les instructions LOAD/STORE. Précisément, dans les modes "base + indice" et "base + décalage", le décalage et l'indice subissent maintenant des décalages. L'idée est de simplifier le calcul d'adresse. De quoi économiser quelques instructions lors des calculs d'adresse, ce qui facilite le travail de l'interpréteur/JIT. Et des instructions LOAD/STORE précises voient leur comportement modifié pour faciliter la gestion de la pile de Java ou l'exécution des méthodes locales. Pour faciliter le travail de l'interpréteur, deux instructions HBP et HBLP ont été ajoutées. Elles branchent vers une fonction qui gère les exceptions logicielles (une fonctionnalité présente dans de nombreux langages, comme Java). La première effectue un branchement vers la fonction en question, la seconde sauvegarde l'adresse de retour avant de faire ce branchement. Les deux peuvent brancher vers 256 fonctions pré-déterminées, sans avoir à présenter leur adresse. ==La traduction binaire accélérée par le matériel== Plus haut, nous avons surtout parlé des compilateurs et des interpréteurs. Cependant, nous devons aussi parler de la '''traduction binaire'''. Elle traduit un programme écrit dans un code machine vers un autre code machine. Par exemple, elle traduit un programme compilé pour un CPU x86 vers un code machine ARM. En général, le système d'exploitation est généralement compilé pour le jeu d'instruction natif, mais il exécute des applications prévues pour le x86. Les applications sont traduites par le système d'exploitation, avant d'exécuter le code traduit. : Nous parlerons dans la suite de code machine source et de code machine cible, pour parler respectivement du code à traduire et du code obtenu après traduction. Idem avec d'autres termes comme architecture cible/source, ou jeu d'instruction cible/source. La traduction binaire est surtout utilisée pour des questions d'émulation ou de compatibilité. Par exemple, lorsque les Macintosh sont passés de processeurs Power PC vers des processeurs x86, le système d'exploitation Mac OS utilisait la traduction binaire pour convertir les anciennes applications Power PC vers du code x86. Les utilisateurs n'y ont vu que du feu. Le système de traduction binaire était appelé Rosetta 1. Par la suite, lors de la transition de processeurs x86 vers des processeurs Apple, Rosetta 2 a vu le jour. La traduction binaire a surtout été utilisée pour traduire du code x86 vers un autre jeu d'instruction. Il faut dire que le x86 est le jeu d'instruction dominant. De nombreuses entreprises ont eu pour ambition de briser l'hégémonie du x86 sur PC, en remplaçant le x86 par un jeu d'instruction plus performant, tout en gardant une compatibilité maximale. La traduction binaire était la seule solution pratique. Elle était le plus souvent intégralement réalisée en logiciel, comme c'était le cas sur les architectures Itanium avec le ''IA-32 Execution Layer''. ===Les généralités sur la traduction binaire assistée en matériel=== L'architecture source est presque tout le temps une architecture CISC, assez ancienne, qu'on souhaite émuler. Le choix de l'architecture cible se porte souvent sur une architecture VLIW, fort différente de l'architecture source, et ce pour obtenir une économie de matériel conséquente. Pour s'exécuter rapidement, le code traduit doit exploiter le parallélisme d'instruction, à savoir exécuter plusieurs instructions en même temps dans des unités de calcul séparées. Il est possible d'utiliser un processeur superscalaire avec exécution dans le désordre pour cela, mais au prix d'un cout important en transistors. Alors qu'en utilisant un CPU VLIW, c'est le traducteur binaire qui fait tout le travail d'extraction du parallélisme d'instruction. Et après tout, quitte à avoir un traducteur binaire, autant lui refiler le boulot d'optimisation. Compiler du code à la volée est certes assez lourd, mais qu'il existe des algorithmes efficaces pour regrouper des instructions indépendantes dans une seule instruction VLIW. Au passage, le processeur VLIW a plus de registres que le processeur source. Cela permet de faire du renommage de registres directement en logiciel. Le traducteur binaire n'hésite pas à changer les noms de registres entre instructions source et instruction VLIW cible, afin de supprimer des dépendances de données. En conséquence, cela ouvre des opportunités de parallélisme, qui sont exploitées lors du regroupement des opérations en instructions VLIW. Vous pourriez penser que le choix d'un CPU VLIW pour émuler du CISC est tout sauf optimal. Vous devez penser que la traduction binaire est d'autant plus simple que l'architecture source et cibles sont semblables. Dans les faits, ce n'est pas tellement traduire les instructions qui pose problème, mais plus la gestion du registre d'état, des exceptions matérielles, de la mémoire virtuelle, la différence entre gros-boutisme et petit-boutisme, de même que des différences d'adressage pour les périphériques. Le premier problème est la gestion des conditions, notamment en présence d'un registre d'état. En pratique, la traduction binaire s'utilise pour traduire du code CISC vers du code RISC ou VLIW. Les architectures source ont donc un registre d'état, qui est mis à jour non seulement par des instructions de test, mais aussi des instructions arithmétiques. L'architecture cible n'a elle pas de registre d'état, mais des registres à prédicats. La traduction de l'un vers l'autre est alors quelque peu compliquée. Et elle est d'autant plus compliquée que le registre d'état est la source de dépendances d'instruction implicites, qui réduisent les performances. Par exemple, si une instruction arithmétique modifie le registre d'état, cela peut impacter l'exécution d'une instruction ultérieure, qui lit ce registre d'état. Pour éliminer ces fausses dépendances, le traducteur binaire doit renommer ce registre en logiciel et détecter les dépendances utiles. Et le compilateur doit gérer les cas où il y a beaucoup de distance entre les deux, voire les cas où l'instruction dépendante n'a pas encore été analysée par le compilateur. Un autre problème est lié à la gestion des exceptions matérielles, et précisément des exceptions précises. Pour rappel, une instruction VLIW regroupe plusieurs opérations, opérations qui correspondraient à une instruction machine sur un CPU pas VLIW. En conséquence, plusieurs instructions du langage machine "source" sont regroupées en une seule instruction VLIW. Et il faut tenir compte du cas où une instruction source lève une exception. Dans le code VLIW, cela signifie qu'une opération lève une exception, et il faut annuler partiellement l'instruction VLIW associée. Et par partiellement, on veut dire que seules les opérations suivantes dans l'ordre du programme source doivent être annulées, puis ré-exécutées. Et c'est un sacré casse-tête ! Les processeurs VLIW utilisent comme solution le mécanisme d'exceptions différées des processeurs EPIC, vu il y a quelques chapitres. Pour résumer, le code est exécuté par blocs d'instruction, délimités par des branchements ou tout autre limite/barrière pertinente dans le code. Un bloc de code est compilé en deux versions : une version rapide sans exceptions matérielles, une version lente qui gère les exceptions précises. La version rapide s'exécute sans exécuter les exceptions. Cependant, les exceptions matérielles sont enregistrées, pour être prises en compte à la toute fin du bloc de code. La version rapide est exécutée en premier et elle mémorise si une exception matérielle a eu lieu. Puis une instruction vérifie si une exception a eu lieu et décide quoi faire. Si aucune exception n'a eu lieu, elle passe à la suite du programme, les résultats du bloc de code sont définitivement acceptés. Mais si une exception a eu lieu, tout est annulé. Le processeur est remis dans l'état initial, puis le code est ré-exécuté instruction par instruction de manière à gérer l'exception correctement. ===La traduction par pré-décodage=== Une première solution serait de faire la traduction binaire dans le cache d'instruction. Lors d'un défaut de cache, le code chargé depuis la RAM est traduit en code machine cible. Et c'est ce code machine cible qui est mémorisé dans le cache d'instruction et exécuté par le processeur. La technique marche sur le papier et n'est qu'une amélioration des techniques de pré-décodage vues il y a quelques chapitres. Cependant, le code obtenu est une traduction assez basique, qui n'incorpore pas d'optimisations dignes de ce nom. De plus, elle gère mal le cas où la taille du code source et cible sont potentiellement très différentes. Concrètement, elle est surtout utile pour traduire du code d'un processeur RISC vers un autre processeur RISC, dont les tailles d'instructions sont similaires. Elle ne permet pas d'émuler plusieurs jeux d'instructions différents, le cout en matériel (un circuit de pré-décodage par jeu d'instruction) serait trop important. ===Le projet DAISY d'IBM=== De très rares processeurs étaient conçus pour accélérer cette traduction binaire, afin de garder de bonnes performances. Les premiers à avoir étudié l'idée étaient IBM, avec leur projet DAISY (''Dynamically Architected Instruction Set from Yorktown''). Le projet de base était de convertir à la volée du code compilé pour des CPU Power PC, vers du code VLIW. Les chercheurs d'IBM avaient développé un algorithme de traduction dynamique efficace, ainsi qu'un processeur VLIW disposant d'optimisations spécifiques à la traduction binaire dynamique, à la volée. De nombreuses idées de ce projet ont été reprises ou re-découvertes par la société Transmetta, avec ses processeurs Crusoe et Efficieon, puis par NVIDIA avec son projet Denver. Les ingénieurs de ce projet ont étudié la possibilité d'émuler plusieurs jeux d'instructions différents sur un même processeur, notamment le s390 d'IBM et le x86. En théorie, cela demande juste d'avoir plusieurs programmes de traduction binaire : un pour le x86, un autre pour le s390, éventuellement un autre pour le Power PC. Mais cela ne s'est pas concrétisé. Mais les ingénieurs ont étudié quelle pourrait être l'architecture VLIW idéale pour ça. Par exemple, il fallait des additions 3-opérandes pour simplifier les calculs d'adresse. Une difficulté était la gestion du registre d'état, dont les bits ne sont pas mis à jour de la même manière sur le x86 et le s390 ou le Power PC. La différence entre gros-boutisme et petit-boutisme était aussi un problème, de même que des différences d'adressage pour les périphériques. Pour que la traduction binaire soit efficace, le processeur VLIW intègre diverses optimisations, comme les exceptions différées et les branchements multi-voies. La principale est la suivante : seul le code des boucles ou fonctions exécutées fréquemment est traduit en code VLIW, le reste du code est interprété. En effet, traduire du code binaire prend plus de temps qu'une simple interprétation. Pour du code qui ne sera exécutée qu'une seule fois, il est plus rapide d'utiliser l'interprétation. Par contre, pour du code exécuté beaucoup de fois, le cout de la traduction binaire est amorti, dilué sur N exécutions, compensé par le gain en temps d'exécution de ce code traduit. Au final, cela permet de ne traduire que le code qui le mérite. La détection du code fréquemment exécuté est réalisée dans le cœur VLIW. L'unité de branchement mémorise les derniers branchements rencontrés et le nombre de fois qu'ils ont été exécutés. Si ils ont été exécutés un certain nombre de fois, l'unité de branchement lève une exception matérielle, qui invoque le traducteur binaire. La routine de cette exception est le traducteur binaire proprement dit. En clair, la traduction binaire est démarrée quand le processeur détecte qu'une fonction a été exécutée plus de N fois, via une exception matérielle dédiée. La mesure du nombre d'exécution d'un branchement se fait dans l'unité de calcul dédiée aux branchements, avec l'aide d'une sorte de ''branch adress buffer'' modifié. Pour rappel, le ''branch adress buffer'' mémorise le ''Program counter'' de chaque branchement récemment rencontré. Ici, chaque entrée du ''branch adress buffer'' est associée à un compteur incrémenté à chaque exécution du branchement. ===Les processeurs Crusoe et Efficieon de Transmetta=== Les processeurs Crusoe Et Efficeron sont deux processeurs VLIW produits par la société Transmetta, une société californienne rachetée par NVIDIA. Ils étaient conçus pour exécuter spécifiquement des programmes x86, système d'exploitation inclus. L'idée derrière ce projet était d'exécuter du code x86 sans que les applications, ni même le système d'exploitation et le BIOS soient au courant ! Les processeurs Transmetta étaient en réalité conçus pour exécuter un programme unique, le ''Code Morphing Software'' (CMS), qui traduisait le code x86 en code VLIW. Transmetta ne voulait pas que son processeur VLIW soit exploité directement, sans le CMS. Il n'avait pas rendu public de compilateur pour traduire du C vers du code VLIW, il n'avait pas donné la documentation du jeu d'instruction VLIW. Des efforts de rétro-ingénieurie, documentés sur le site realworldtech, ont cependant permis de comprendre comment étaient encodées les instructions du processeur Crusoe. Comme pour le projet Daisy, le ''Code Morphing Software'' utilisait à la fois interprétation et traduction binaire, selon les besoins. Les instructions sont interprétées lors de leurs premières exécutions, mais le CMS bascule sur de la traduction en code VLIW après un certain nombre d'exécution. Ainsi, les boucles souvent exécutées sont traduites en code VLIW, alors que le reste du code est interprété. Le choix du code à traduire est le fait du CMS, il utilise des heuristiques complexes pour, qui ne sont pas connues dans le détail. Le ''Code Morphing Software'' est mémorisé dans une EEPROM, ce qui en fait un ''firmware'' situé en dessous du BIOS. Le CMS démarre ensuite le BIOS, qui lui-même démarre le système d'exploitation, qui lui-même démarre les pilotes de périphériques et les programmes. Le CMS se réserve les 16 premiers mébioctets de l'espace d'adressage. L'EEPROM du CMS est mappée dedans, mais n'en utilise que la moitié. Le reste est utilisé comme cache, pour mémoriser le code VLIW traduit par le CMS. Les deux processeurs intégraient aussi deux mémoires SRAM, utilisées par le CMS, appelées la ''local program memory'' (LPM) et la ''local data memory'' (LDM). La première contient du code qui gére les interruptions, la mémoire virtuelle, les exceptions matérielles, les problèmes d'alignement mémoire, et quelques fonctions très fréquemment utilisées par le CMS. La seconde est de la mémoire RAM utilisée par ces fonctions de la ''local program memory''. Les mémoires caches L1 et L2 sont séparées de ces deux mémoires. Le processeur Crusoe et Efficieon étaient des processeurs VLIW très simples. Ils n'avaient même pas de MMU, ni de fonctionnalités importantes sur les CPU x86. Pour Crusoe, les instructions VLIW étaient encodées sur 64 ou 128 bits, et regroupaient entre 2 à 4 opérations. Les instructions LOAD/STORE ne supportaient pas d'adressage indicé, juste de l'adressage indirect à registre. Crusoe avait 5 unités de calcul : deux ALU entières, une unité LOAD/STORE, une unité de branchement, une FPU. Efficieon doublait le nombre d'ALU entière et d'unité LOAD/STORE, les instructions VLIW passaient à 256 bits. Le processeur Crusoe contient 160 registres, dont 64 registres généraux, 32 registres flottants. Les registres généraux font 32 bits, ce qui est cohérent avec le fait que les CPU x86 de l'époque étaient des processeurs 32 bits. Quant aux registres flottants, ils faisaient 80 bits, ce qui colle avec la taille des registres flottants de la FPU x87 utilisée à l'époque. Sur les 64 registres généraux, seuls 48 étaient réellement utilisable pour mémoriser des opérandes. Une partie des registres généraux étaient utilisés pour la gestion de la pile, d'autres pour mémoriser l'état du CPU x86 émulé, un registre était un registre zéro non accesible en écriture. La gestion des exceptions est optimisée avec un système d'exceptions différées, le même que celui décrit plus haut. Le processeur utilise un système similaire, pour effectuer des lectures anticipées, qui a été expliqué dans le chapitre sur les processeurs VLIW/EPIC. Sauf qu'il s'agit là d'une optimisation pour gagner en performances en général, pas une aide utile pour gérer des exceptions précises ou toute autre subtilité de la traduction binaire. Pour gérer les exceptions différées et les lectures anticipées, le processeur mémorise l'état du processeur avant de démarrer un bloc de code. Pour cela, le processeur contient des ''shadow registers'', 48 registres entiers et 16 registres flottants. Vous remarquerez qu'il y a autant de registres entiers que de ''shadow registers'' entiers. Les registres entiers/flottants sont mémorisés dans les ''shadow registers'' avant d'exécuter un bloc de code, afin que le processeur puisse revenir à l'état de base. Une autre fonctionnalité liée est le ''gated store buffer''. Le principe est d'éviter toute écriture en mémoire RAM, tant que le code VLIW n'a pas émis d'instruction ''commit''. L'idée est que les écritures sont conservées dans la ''store queue'' de l'unité LOAD/STORE. Pour rappel, la ''store queue'' met en attente les écritures soit tant que les écritures ne sont pas terminées, soit tant que la RAM est occupée. L'idée est que les écritures sont accumulées dans la ''store queue'' tant que l'instruction ''commit'' n'a pas eu lieu. Le ''store queue'' est "déversé dans la RAM" seulement quand l'instruction ''commit'' s'exécute. ===Les CPU NVIDIA Denver=== Nvidia a tenté quelque chose de similaire avec son projet Denver. NVIDIA a racheté l'entreprise Transmetta, ses brevets, et a tenté de refaire la même chose. Le projet initial était de traduire du code x86 vers un jeu d'instruction VLIW propriétaire, comme l'a fait Transmetta. Mais NVIDIA n'a pas réussi à acquérir la licence du jeu d'instruction x86 et s'est rabattu sur le jeu d'instruction ARM. Le processeur né de ce projet est le Tegra K1-64 CPU. Il s'agit d'un processeur composé d'un cœur VLIW, couplé à deux décodeurs d'instructions ARM. Les deux décodeurs ARM traduisent une paire d'instructions ARM consécutives en instructions VLIW, pour les exécuter sur le cœur VLIW. Le CPU peut fonctionner selon deux modes : ARM et VLIW. En somme, le processeur supporte deux jeux d'instruction : le jeu d'instruction ARM, et un jeu d'instruction VLIW. En mode ARM, les instructions ARM sont chargées depuis le cache, traduites en VLIW par les décodeurs ARM, puis exécutées par le cœur VLIW. En mode optimisé, le CPU exécute des instructions VLIW chargées depuis le cache d'instruction, les décodeurs ne sont pas utilisés. : Le CPU supporte en réalité deux jeux d'instruction ARM : ARM8 et 7. Mais ils correspondent au même mode pour le CPU. Un point important est que le processeur peut changer très rapidement de mode, en à peine quelques cycles d'horloges. On n'est clairement pas dans le cas des CPU x86, qui mettent des plombes pour passer du mode 32 à 64 bits et inversement. La commutation est tellement rapide qu'on peut considérer que le processeur supporte deux jeux d'instruction simultanément. Et le support simultané de deux jeux d'instruction facilite l'implémentation de la traduction binaire. Encore une fois, inutile de traduire du code qui ne sera exécutée qu'une seule fois, mieux vaut l'exécuter directement dans le mode ARM. Surtout que le processeur supporte un mode ARM qui permet d'exécuter du code ARM sans perte de performance. Par contre, le code fréquemment utilisé, notamment dans des boucles critiques, est traduit en VLIW. Au final, les performances sont optimisées, en limitant le travail du logiciel traducteur binaire. La détection du code fréquemment exécuté se fait différemment que pour les processeurs Transmetta et Daisy. L'unité de branchement ne détecte pas directement le code beaucoup exécuté, même si elle a un rôle à jouer. A la place, la détection est réalisée en logiciel, par un ''thread'' dédié, qui s'exécute sur un cœur séparé. Il détecte le code fréquemment utilisé en regardent les branchements exécutés récemment. Pour cela, le processeur mémorise l'historique des branchements pris récemment et copie cette historique dans une mémoire tampon dédiée, partagée entre tous les cœurs. Vu qu'elle est partagée, le ''thread'' de détection a accès à l'historique sans pertes de performances, seule la copie de l'historique dans ce tampon a un cout en performance, pas son partage. Le code VLIW obtenu après traduction binaire est mémorisé dans la mémoire RAM, dans une portion spécialement réservée pour. Elle fait 128 mébioctets, et est appelée le '''cache d'optimisation''' par NVIDIA. Le cache d'optimisation est protégé en écriture et seul le traducteur binaire peut écrire dedans. Au passage, le traducteur binaire est du code VLIW, ce qui fait qu'il est placé dans le cache d'optimisation. Le processeur détecte automatiquement quand une fonction pour laquelle le code compilé équivalent est disponible. Pour cela, l'unité de chargement contient une table de correspondance entre l'adresse de la fonction ARM, et l'adresse de son équivalent compilé. Quand une fonction est appelée, l'unité de chargement regarde l'adresse de destination du branchement. Si l'adresse est dans cette table, elle récupère l'adresse de la fonction compilée et branche vers celle-ci. Le processeur dispose d'un cache d'instruction de 128 kibioctets, ce qui est très important, mais nécessaire vu la taille des instructions VLIW. Le coeur VLIW dispose de ses propres décodeurs, d'un ''scoreboard'', de 7 unités de calcul et de registres. Pour les unités de calcul, il a deux FPU, deux ALU, une unité de branchement, et deux unités LOAD-STORE faisant aussi ALU. Un point important est que le CPU dispose de 64 registres entiers et de 64 registres flottants. C'est deux fois plus que supporte le jeu d'instruction ARM. En mode ARM, seule la moitié des registres est utilisée. Mais en mode optimisé, le traducteur binaire utilise bien les 64 registres, grâce à une sorte de renommage de registres logicielle. Pour simplifier la traduction binaire, le processeur supporte les techniques vues précédemment. Le processeur intègre un ''gated store buffer'' similaire à celui des processeurs Transmetta. Les techniques d'exceptions différées sont aussi supportées, comme sur les processeurs Transmetta. Il a aussi une unité de préchargement décrite par NVIDIA comme agressive, avec support du préchargement de type anticipé (''runahead''). ===L'émulation de la mémoire virtuelle=== Pour finir, il faut parler de la mémoire virtuelle avec la traduction binaire. Sur le processeur source, le processeur gère à la fois des adresses physiques et virtuelles et sa MMU fait la conversion entre les deux. Sur le processeur VLIW, la MMU est simulée par le programme de traduction binaire, partiellement ou totalement. L'implémentation exacte varie suivant que l'on parle du projet Daisy, des processeurs Transmetta ou de Denver. Sur les processeurs Transmetta, il n'y a pas de MMU ni de mémoire virtuelle. A la place, le programme de traduction binaire émule la mémoire virtuelle du processeur source. Le processeur VLIW ne gère que des adresses physiques, rien d'autre. L'espace d'adressage physique du processeur VLIW a la même taille que celui du processeur émulé, ici des processeurs x86. Il est vraisemblable que les adresses physiques utilisées par le processeur x86 sont les mêmes que celles du processeur VLIW. Sur le projet Daisy, le processeur VLIW gère la mémoire virtuelle via pagination, comme les processeurs émulés. Ce qui fait qu'il gère un espace d'adressage virtuel et un espace d'adressage physique. Pour éviter toute confusion, nous parlerons d'adresse physique/virtuelle VLIW pour les adresses physiques/virtuelles du processeur VLIW, d'adresse virtuelle/physique source pour celle du jeu d'instruction traduit, à savoir du Power PC ou du s390, éventuellement de l'x86. Il faut alors faire le lien entre adresses physiques source et adresses virtuelles VLIW. Pour cela, rien de plus simple : il y a correspondance parfaite. L'adresse physique source numéro N correspond à l'adresse virtuelle VLIW numéro N. Cependant, cela signifie que tout l'espace d'adressage virtuel VLIW serait utilisé par le code à traduire. En réalité, il faut ajouter de la place pour le code traduit et le programme de traduction binaire. La conséquence est que l'espace d'adressage virtuel VLIW est plus large que l'espace d'adressage source. L'espace d'adressage virtuel VLIW est découpé en trois sections : une section pour le code Power PC à traduire, une autre pour le code traduit en VLIW, et une dernière réservée au traducteur binaire. Le programme de traduction est placé dans une ROM mappée en mémoire dans la seconde section. Le reste de la seconde section est réservé à une mémoire RAM utilisée par le programme de traduction. [[File:Mémoire virtuelle sur le projet Daisy.png|centre|vignette|upright=1.5|Mémoire virtuelle sur le projet Daisy]] Il faut noter que chaque page physique source, une fois traduite, correspond à N pages virtuelles VLIW. La raison est que le code traduit prend plus de place que le code machine originel. En conséquence, on doit utiliser une page finale plus grande. Pour le reste, des pages contiguës en mémoire physique source sont elles aussi contiguës en mémoire virtuelle VLIW. Le calcul d'adresse est donc simple : l'adresse physique source est multipliée par N, puis on ajoute l'adresse de base à laquelle commence la section pour le code traduit. ==Les processeurs à jeu d'instruction reconfigurable== Nous venons de voir comment accélérer en matériel la traduction binaire. Mais il existe une alternative qui permet de se passer de traduction binaire, complétement. L'idée est d'avoir un processeur avec un '''jeu d'instruction reconfigurable'''. Le nom est assez parlent : on peut configurer le processeur de manière à supporter le jeu d'instruction de son choix ! Par exemple, on pouvait reconfigurer un CPU de manière à ce qu'il supporte le jeu d'instruction x86, ou qu'il exécute du ''bytecode'' Java, ou du code ARMv8, etc. De tels processeurs sont encore plus rare que ceux vu avant dans ce chapitre, mais ils ont existé ! Les processeurs de ce type avaient un ''microcode réinscriptible'', à savoir que c'était des processeurs microcodés pour lesquels on pouvait modifier le microcode. L'idée était qu'en changeant le microcode, on changeait le jeu d'instruction du processeur. Pour cela, le microcode était associé à une mémoire SRAM dans laquelle le microcode était copié au démarrage du CPU. Il y avait donc une micro-ROM et une micro-RAM. Les instructions étaient décodées en utilisant le microcode dans la micro-SRAM, la micro-ROM ne servait qu'au démarrage. En modifiant le contenu de la micro-SRAM, on pouvait altérer le microcode, voire le remplacer totalement. Un exemple est celui du Burroughs B1700, et il est intéressant d'étudier en détail son architecture. Son architecture est décrite dans le livre "Interpreting Machines : Architecture and Programming of the Bl700/Bl800 Series", écrit par Elliott I. Organick et James A. Hinds. Une partie de ce qui vfa suivre est un très court résumé de ce livre. ===Une architecture bit-adressable=== Si avoir un microcode réinscriptible est suffisant, quelques choix de ''design'' facilitent l'émulation de jeux d'instructions variés. Par exemple, le CPU Burroughs B1700 avait ajouté une fonctionnalité très importante : il était bit-adressable. En clair, il pouvait adresser la mémoire bit par bit, si nécessaire. L'avantage est que cela permettait d'émuler des architectures dont la taille des registres/''byte''/mots était très différente. Un autre avantage est que cela facilitait la gestion des instructions de taille variable. Détaillons rapidement le premier avantage. Le processeur était un CPU 24 bits, mais il pouvait émuler des processeurs 16 bits, 8 bits, 4 bits, 1 bit, 9 bits, ou toute autre valeur. Pour cela, le processeur lisait des mots de 24 bits et utilisant un ''barrel shifter'' pour sélectionner les bits adéquats. Par exemple, pour un accès de 16 bits, la donnée lue en mémoire était masquée de manière à ne garder que les 16 bits de poids faible. Le ''barrel shifter''/circuit de masquage était placé directement avant le bus mémoire, sur le trajet des données/instructions. Le ''barrel shifter'' était commandé par un registre ''Field Unit'', qui précisait quelle était la taille des données à charger. ===La gestion de la taille des instructions=== Les instructions à décoder n'ont pas de longueur fixe, à savoir qu'elles peuvent faire 16 bit avec tel jeu d'instruction, 8 bits avec un autre, 32 bits sur un autre, etc. Elles peuvent aussi être de longueur fixe ou variable ! Le B1700 intégrait donc un support des instructions de longueur variable, qui était "détourné" pour gérer le fait que des jeux d'instruction différents ont des instructions de taille différentes. Notons que vu que l'architecture est bit-adressable, les contraintes d'alignement des instructions disparaissent, ce qui a des avantages qu'on va détailler dans ce qui suit ! L'implémentation des instructions de taille variable ajoutait un registre avant le registre d'instruction. Le registre d'instruction proprement dit était un registre M de 24 bits. Il était précédé par un registre T, dans lequel l'instruction était assemblée, en combinant plusieurs morceaux. Le registre T est relié à un ''barrel shifter'', différent au précédent, qui permettait de masquer, décaler et extraire une donnée de ce registre. Si l'instruction est codée est codée sur moins de 24 bits, le registre T se contente de masquer les bits en trop. Si l'instruction chargée dépasser 24 bits, le chargement se faisait en plusieurs passes, avec plusieurs lectures en mémoire. Chaque passe découpait l'instruction pour extraire des '''champs''' : l'opcode, les opérandes, les adresses absolues, les constantes immédiates, les numéros de registre, etc. Et ils étaient copiés dans des registres interne au processeur. Par exemple, il servait à extraire l'opcode de l'instruction. Ou encore, pour une instruction de branchement direct, il pouvait extraire l'adresse à laquelle brancher de l'instruction. Le chargement de l'instruction se faisait alors champ par champ. Notons que vu que l'architecture est bit-adressable, les contraintes d'alignement des instructions disparaissent ! Quand on charge une instruction, peut importe qu'elle soit ou non alignée sur 24 bits, elle sera placée dans les bits de poids faible, il suffira de masquer les bits de poids fort inutiles. Par exemple, si on charge une instruction de 16 bits, les 16 bits seront copiés dans les 16 bits de poids faible du registre T, il suffira de masquer l'octet de poids fort. Pas besoin de faire de décalages pour gérer des instructions mal alignées... Pour charger une instruction, il faut connaitre plusieurs informations : l'adresse de son premier bit, sa taille. Pour cela, le B1700 contient deux registres, nommés ''Field Adress'' (FA) et ''Field Length'' (FL), qui indiquent l'adresse du premier bit et la taille de l'instruction à charger. Le premier fait 24 bits, ce qui permet de gérer des adresses de 24 bits. N'oubliez pas que le processeur est bit-adressable, ce qui permet d'adresser 16 Mébi-bits, soit 2 mébioctets. Le registre pour la taille fait lui 16 bits, ce qui permet de gérer des instructions de 65536 bits, soit 8192 octets ! Leur nom commence par ''field'', car le processeur peut gérer des instructions de taille variable et/ou plus longues que 24 bits. Dans ce cas, le chargement de l'instruction se fait champ par champ. Pour donner un exemple, prenons une instruction composée de 20 champs de 12 bits chacun. Les champs sont chargés un par un dans le registre T. Le registre FA est alors initialisé avec l'adresse de l'instruction, il est incrémenté de 12 à chaque cycle. Le registre FL est initialisé avec la taille de l'instruction, soit 20 * 12 = 240 bits, et il est décrémenté de 16 à chaque cycle. Précisons cependant que la taille d'un champ peut être changée d'un cycle à l'autre. Par exemple, le processeur peut charger un opcode de 8 bits, l'interpréter, détecter que l'instruction demande ensuite de charger une adresse de 16 bits. Dans ce cas, le registre FL est immédiatement altéré pour charger 16 bits, au lieu des 8 bits de l'opcode immédiatement précédent. ==Les jeux d'instructions dédiés à un langage de programmation== De rares processeurs sont conçus pour un langage de programmation en particulier. On appelle ces processeurs, conçus pour des besoins particuliers, des '''processeurs dédiés'''. Par exemple, les fameux ''Burrough E-mode'' B5000/B6000/B7000 étaient spécialement conçus pour exécuter de l'ALGOL-60. Leurs cousins B2000/B3000/B4000 étaient eux conçus pour le COBOL. Des langages fonctionnels ont aussi eu droit à leurs processeurs dédiés. Le prolog en est un bel exemple, avec les superordinateurs de 5ème génération qui lui étaient dédié. On peut aussi citer les machines LISP, dédiés au langage LISP, qui datent des années 1970. Elles étaient capables d’exécuter certaines fonctions de base du langage directement dans leurs circuits : elles possédaient notamment un ''garbage collector'' câblé dans ses circuits ainsi que des instructions machines supportant un typage déterminé à l’exécution. Les processeurs dédiés ont eu leur heure de gloire au début de l'informatique, à une époque où les langages de haut niveau venaient d'être inventés. À cette époque, les compilateurs n'étaient pas performants et ne savaient pas bien optimiser le code machine. Il était alors rationnel, pour l'époque, de rapprocher le code machine cible et le langage de programmation de haut niveau. De nombreuses architectures dédiés ont ainsi été inventées, avant que les concepteurs se rendent compte des défauts de cette approche. Les défauts en question ne sont pas nombreux, mais assez simples à comprendre. Premièrement, elles sont très rapides pour un langage de programmation en particulier, mais sont assez mauvaises pour les autres, d'où un problème de "compatibilité". Ajoutons à cela que les langages de programmation peuvent évoluer, devenir de moins en moins populaires/utilisés, ce qui rend la création d'architectures généralistes plus pertinente. Enfin, les architectures dédiées sont évidemment des processeurs CISC, pour implémenter les nombreuses fonctionnalités des langages évolués. Et les défauts des CISC sont assez rédhibitoires à l'heure actuelle. À l'heure actuelle, les algorithmes des compilateurs se sont améliorés et savent nettement mieux utiliser le matériel. Ils produisent du code machine efficace, ce qui rend les architecture dédiées bien moins intéressantes. Si on ajoute les défauts de ces architectures dédiées, par étonnant que les architectures dédiées aient presque disparues. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'accélération matérielle de la virtualisation | prevText=L'accélération matérielle de la virtualisation | next=Le matériel réseau | nextText=Le matériel réseau }} </noinclude> cy54lowx9jr4d7ot04qw8v0c88h6dqp 764012 764010 2026-04-19T16:04:46Z Mewtow 31375 /* Les processeurs à jeu d'instruction reconfigurable */ 764012 wikitext text/x-wiki De nos jours, la majorité des programmeurs programment dans des langages de haut niveau. Il est très rare pour eux d'avoir à utiliser de l'assembleur, et encore moins un langage machine. Les programmes écrits dans un langage de haut niveau sont traduits en langage machine par un logiciel appelé le compilateur. [[File:Compilation.PNG|centre|vignette|upright=2.5|Compilateur : principe.]] Il se trouve que quelques rares processeurs sont conçus pour faciliter le travail du compilateur. Et ce sont ces processeurs que nous allons voir dans ce qui suit. Ils représentent une catégorie de jeux d'instruction à part, qui n'a pas vraiment de nom. ==La compilation et l’interprétation== Un compilateur traduit donc du code source, écrit dans un langage de haut niveau, vers du langage machine. La traduction est rarement directe. En général, le compilateur traduit le code source en assembleur, qui est lui-même traduit en langage machine. L'assembleur est une représentation textuelle du langage machine alors que le code machine est du binaire exécutable par le processeur. La traduction se fait donc en deux étapes, la '''compilation''' proprement dite et l''''assemblage''', réalisées respectivement par un compilateur et un assembleur. Il faut parfois rajouter une troisième phase d'édition des liens, que nous passons volontairement sous silence. {| |[[File:Code C.png|vignette|200px|Le code source, ici en C.]] |[[File:Assem.png|vignette|250px|Le code assembleur.]] |[[File:Binary file - hello world (C programming).png|vignette|150px|Le code machine.]] |} ===L'interprétation et la compilation à la volée=== Une alternative à la compilation est l''''interprétation''', qui transforme le code source en code machine à la volée. Avec l'interprétation, le code source est passé à un logiciel appelé l''''interpréteur''', qui exécute le code source ligne de code par ligne de code. Une ligne de code est traduite en code machine, qui est exécutée, puis l'interpréteur passe à la ligne suivante. [[File:Kaantotulkkaus.png|centre|vignette|upright=3|Compilation versus interprétation.]] Un défaut de l'interprétation est son cout en performance, qui est assez important. Aussi, l'interprétation stricte a évolué vers un hybride entre interprétation et compilation. L'idée est qu'une partie peu importante du code source est interprétée, alors que le code important est compilé juste avant d'être exécuté. Pour le dire autrement, le code source est partiellement compilé à la volée, juste avant son exécution. Aussi, on parle de '''compilation à la volée'''. Le terme en anglais est ''Just In Time compilation'', abrévié en JIT. En soi, le JIT est associé à de l'interprétation, les JIT sont en réalité des hybrides interpréteurs-compilateurs. Un point important est que compiler du code à la volée est assez lourd, cela a un cout en performance. Par contre, le code compilé s'exécute plus vite que du code interprété. Si on exécute le code compilé une seule fois, le cout de la compilation l'emporte sur le gain à l'exécution. Mais le code compilé est réutilisé autant de fois que nécessaire. Le gain à l'exécution est donc multiplié par le nombre d'exécutions, alors que le cout de la compilation est répartit sur plusieurs exécutions, il est amorti. Les portions du code sont choisies de manière à avoir un gain en performance maximal. L'idée générale est que le code exécuté très souvent est compilé, alors que du code exécuté pas souvent est interprété. Typiquement, les boucles les plus souvent exécutées sont compilées, le code exécuté une seule fois est interprété. ===Les langages intermédiaires=== Les compilateurs modernes passent par un '''langage intermédiaire''' pour faire la transformation en code machine. Le compilateur traduit le langage de haut niveau en langage intermédiaire, puis traduit le langage intermédiaire en code machine cible. Le langage intermédiaire est parfois appelé le '''''bytecode''''', et ce terme recouvre aussi le code écrit avec du ''bytecode''. L'interprétation aussi peut passer par l'intermédiaire d'un un ''bytecode'', ce n'est pas limité aux compilateurs proprement dit. {| |[[File:Intermediate representation scheme.png|centre|vignette|upright=1.5|Représentation intermédiaire d'un compilateur.]] |[[File:Code interpreter scheme.png|centre|vignette|upright=1.5|Fonctionnement d'un interpréteur.]] |} Faire ainsi a de nombreux avantages, le principal étant d'avoir un compilateur capable de traduire un langage de haut niveau vers plusieurs jeux d'instructions différents. Cela permet d'avoir, par exemple, un compilateur qui traduit du C soit en code machine x86, soit en code machine pour un CPU ARM, soit pour un CPU POWERPC, etc. [[File:Compiler design.svg|centre|vignette|upright=3|Compiler design]] Pour l'interprétation, l'usage d'un ''bytecode'' a d'autres avantages. Pour l'exploiter, il faut distribuer non pas le code source, mais le ''bytecode''. En clair, l'interpréteur ne prend pas en entrée le code source, mais du ''bytecode'' déjà compilé. La traduction est alors beaucoup plus simple, car le code source a déjà été partiellement compilé, par la traduction en ''bytecode''. Interpréter du code source est en effet assez compliqué : il faut effectuer des étapes d'analyse lexicale, sémantique, et bien d'autres. Avec du ''bytecode'', ces étapes ont été réalisées lors de la compilation du ''bytecode'', l'interpréteur a alors peut de choses à faire. L'interprétation du ''bytecode'' se fait instruction par instruction, au niveau du ''bytecode''. Une instruction du ''bytecode'' est traduite en une instruction machine équivalente, qui est exécutée. Si l'instruction est un peu complexe, il exécute une fonction/procédure qui fait la même chose. Pour résumer, le ''bytecode'' est ensuite traduit à la volée et exécuté instruction par instruction par un logiciel appelé l'interpréteur. [[File:Control table.png|centre|vignette|upright=2|Control table]] Le langage intermédiaire peut être vu comme l'assembleur d'un processeur, n'existe pas forcément dans la réalité, mais dont le jeu d'instruction est décrit en détail. Le processeur en question est appelé une '''machine abstraite''', ou encore une ''machine virtuelle''. Nous utiliserons le terme de machine abstraite dans ce qui suit. La machine abstraite n'est pas la même suivant que l'on cible la compilation ou l'interprétation/JIT. Pour l'interprétation/JIT, la machine abstraite est souvent une machine à pile, car cela simplifie grandement la traduction en code machine final. L'interprétation demande que la traduction du ''bytecode'' en code machine soit la plus simple possible. Et cela demande de prendre en compte pas mal de détails. Par exemple, les différents jeux d'instruction existants n'ont pas le même nombre de registres, ce qui pose problème lors de la traduction du ''bytecode''. Lors de la transformation en code machine, un algorithme d'allocation de registres se débrouille pour traduire le code intermédiaire en code qui utilise un nombre limité de registres. Pour améliorer cette allocation de registres, il y a deux solutions niveau ''bytecode'' : soit utiliser un nombre illimité de registres, soit utiliser une machine à pile. Pour les machines à pile, il existe un algorithme simple et rapide pour traduire un code écrit pour une machine à pile en un code écrit pour un processeur avec des registres, qui se débrouille pas trop mal pour allouer efficacement les registres. C'est un avantage assez important pour les langages interprétés. ==Les processeurs qui exécutent du bytecode== Le ''bytecode'' est un code machine, ce qui signifie qu'il peut en théorie s'exécuter sur un processeur qui implémente le jeu d’instruction associé. Si le jeu d'instruction d'un ''bytecode'' est souvent une description censée être fictive, elle n'en reste pas moins un jeu d'instruction et des caractéristiques précises, qu'on peut l'implémenter en matériel ! Néanmoins, tous les ''bytecode'' ne sont pas égaux de ce point de vue. Certains sont faciles à implémenter en matériel, d'autres non. Les ''bytecodes'' implémentés en matériel sont ceux dont les machines abstraites sont des machines à pile. Un premier exemple est celui des processeurs Pascal MicroEngine, qui exécutaient directement le bytecode du langage Pascal, le fameux UCSD P-code. Un second exemple est la '''machine SECD''', qui sert de langage intermédiaire pour certains compilateurs de langages fonctionnels. Elle a été implémentée en matériel par plusieurs équipes, notamment par les chercheurs de l'université de Calgary en 1989. Dans le même genre, quelques processeurs simples étaient capables d’exécuter directement le ''bytecode'' du langage FORTH. Le FORTH, un des premiers langages à pile de haut niveau, possède de nombreuses implémentations hardware et est un des rares langages de haut niveau dont le ''bytecode'' a été utilisé comme langage machine sur certains processeurs. Par exemple, on peut citer le processeur FC16, capable d’exécuter nativement du ''bytecode'' FORTH. ===Les processeurs Java=== Le cas le plus impressionnant est celui de la machine virtuelle Java, qui est un design de processeur comme un autre. En temps normal, le ''bytecode'' Java est compilé ou interprété, mais certains processeurs exécutaient du ''bytecode'' Java directement, sans interprétation ni compilation. Ils sont appelés des '''processeurs Java'''. On peut citer les processeurs ARM disposant de l'extension Jazelle, du JEMCore et du aJ-100 de aJile Systems, le picoJava II de Sun, et quelques autres. Il y a aussi eu quelques projets de recherche ou processeurs open source, comme Komodo, jamuth, le ''Java Optimized Processor'' et quelques autres projets du même genre. L'implémentation de la machine virtuelle Java n'était cependant pas complète. Quelques instructions complexes n'étaient pas gérées par le processeur et devaient être émulées en logiciel. Mais la grosse majorité des instructions l'était. Les instructions non-supportées étaient émulées avec la méthode ''trap and emulate'', à savoir qu'un opcode inconnu déclenchait une exception matérielle ''opcode invalide'', dont la routine pouvait émuler les instructions qui doivent l'être. Le processeur aJ-100 de aJile est capable d'exécuter la quasi-totalité des instructions de la JVM Java, seules deux instructions faisant exception. Les '''accélérateurs Java''' sont une solution intermédiaire, qui permet de traduire à la volée du ''bytecode'' Java en instructions RISC, exécutées sur un autre processeur. Il s'agit formellement de traduction binaire, mais je préfère en parler ici. L'idée est de combiner un processeur RISC avec un coprocesseur Java. Le coprocesseur lit le ''bytecode'' Java et le traduit en instructions RISC, ces dernières étant exécutées sur le processeur RISC. Le JA108 de Nozomi était un coprocesseur de ce genre, au même titre que le coprocesseur JSTAR de JEDI. L'extension Jazelle de certains CPU ARM fonctionnait vraisemblablement sur un principe similaire, sauf que le coprocesseur était intégré dans le processeur ARM, entre le cache d'instruction et le décodeur d'instructions ARM. L'intérêt des processeurs Java n'est pas qu'une question de performance. Certes, ils permettent d’exécuter plus vite les programmes compilés en ''bytecode'', comme des programmes Java pour la JVM Java. Mais l'intérêt est aussi de faire des économies de mémoire RAM et ROM. Je rappelle que la machine virtuelle Java est une machine à pile, ce qui fait que sa densité de code est excellente. Les programmes en ''bytecode'' Java sont donc très petits, et prennent peu de place en mémoire ROM. De plus, utiliser un processeur Java permet de se passer d'interpréteur ou de JIT Java, ce qui économise encore plus de mémoire. ===Transformer un processeur RISC en machine à pile=== La plupart des processeurs Java utilisent en interne une architecture à registre généraux, qui émule une machine à registre en microcode. Quelques registres sont réservés pour l'état de la machine virtuelle, avec au minimum un registre réservé pour le pointeur de pile, un autre pour un pointeur vers un ''pool'' de constantes, et quelques autres. D'autres registres servent à mémoriser le haut de la pile, à savoir le sommet de la pile et quelques opérandes situées juste en-dessous. Par exemple, le processeur picoJava II gardait les 64 opérandes au sommet de la pile dans 64 registres généraux, séparés des registres pour les pointeurs et l'état de la JVM. Une optimisation possible, envisagée sur les processeurs picoJava, était une sorte de macro-fusion sous stéroïde. L'idée était de fusionner certaines séries d'instructions Java en une seule instruction machine. Typiquement, pour faire une opération arithmétique, une machine à pile empile deux opérandes et exécute une opération, ce qui prend trois instructions machine. La spécification picoJava 1 a proposé d'envoyer directement la seconde opérande en entrée de l'ALU, ce qui fusionnait l'empilement de la seconde opérande et l'opération arithmétique. Le processeur picoJava II va plus loin et fusionne les trois instructions en une seule instruction RISC, agissant sur des registres. Mais la majorité des processeurs Java ne faisaient pas cela, car cela complexifie grandement le décodeur. Les processeurs ''bigfoot'' de DCT utilisaient un système différent pour émuler une pile avec des registres. Leur banc de registre de 64 bits était coupé en deux : les 16 premiers registres étaient utilisés pour la pile, les 48 suivants étaient utilisés pour autre chose. L'idée est que le registre 0 mémorise un emplacement vide, le registre 1 contient le sommet de la pile, le registre 2 l'opérande sous le sommet de la pile, et ainsi de suite. Les 15 premières opérandes de la pile sont donc mémorisées dans les registres. Les instructions écrivent leur résultat dans le registre adéquat, puis tous les registres sont décalés pour que le résultat passe dans le registre R1. Par exemple, les instructions PUSH chargent une opérande dans le registre 0, puis tous les registres sont décalés de un rang, pour que l'opérande chargée soit dans le registre 1. Le registre 0 devient alors le registre numéro 1, le registre 1 devient le 2, et ainsi de suite. Par contre, les opérations arithmétiques font autrement. Une instruction d'addition, par exemple, lit les opérandes dans le registre 1 et 2, puis place son résultat dans le registre 2 (les deux opérandes sont dépilées). Puis, les registres sont décalés d'un rang pour que ce registre 2 devienne le 1. Les registres ne sont en réalité pas décalés, il n'y a pas de transferts entre registres. A la place, les 16 premiers registres utilisaient une forme limitée de renommage de registres, similaire au banc de registre tournant utilisé sur les processeurs EPIC comme l'Itanium. Les numéros de registres sont renommés, décalés, à chaque fois qu'on empile ou dépile une opérande. Pour faire le renommage de registres, processeur contient un ''register stack counter'' de 4 bits, qui est incrémenté à chaque fois qu'on empile une donnée, et est décrémenté quand on dépile une donnée. Quand on veut accéder à un opérande, le numéro de registre architectural associé est traduit en numéro de registre physique, en additionnant le ''register stack counter'' (modulo 16, pour rester sur 4 bits). ==L'accélération de l'interprétation/JIT : le ''Thumb-EE'' d'ARM== Quelques processeurs ont ajouté des instructions pour faciliter le travail des interpréteurs/JIT. Par exemple, en 2005, ARM a ajouté le mode ''Thumb Execution Environment'', qui faisait cela. Il reprenait le jeu d'instruction compact ''thumb'' et ajoutait quelques instructions et modifiait le comportement d'instructions existantes. Le jeu d'instruction ''Thumb-EE'' a été ajouté sur les CPU ARM en 2005, mais a été déprécié en 2011, par manque d'utilité. Mais la tentative mérite qu'on s'attarde dessus. Les CPU de gamme M, déstinés à l'embarqué, n'ont jamais supporté ''Thumb-EE'', vu que de tels processeurs ne sont pas conçus pour exécuter du code interprété/JIT. Le mode ''Thumb-EE'' est un mode d'exécution, séparé de l'ARM normal. Ainsi, une instruction ''Thumb-EE'' et une instruction ARM peuvent avoir le même encodage en binaire, mais se comporter différemment. Le processeur est à tout moment soit en mode ''Thumb-EE'', soit en mode ARM normal, ce qui précise comment ces instructions doivent se comporter. Des instructions ont été ajoutées par ''Thumb-EE'', pour qu'il fasse son travail. L'entrée et la sortie du mode ''Thumb-EE'' se fait avec deux instructions : ENTERX et LEAVEX. Une fois en mode ''Thumb-EE'', le décodage des instructions ''thumb'' se fait avec les règles du ''Thumb-EE'', et non celles du ''thumb'' normal. Une première différence entre ''thumb'' normal et ''Thumb-EE'' est l'ajout de ce qui s'appelle un ''null check'' pour les instructions mémoire. Le ''null check'' est utilisé pour les instructions en mode d'adressage "base + indice" ou "Base + décalage", ou tout autre mode d'adressage avec un registre de base utilisés dans des calculs d'adresse. L'idée est de vérifier si l'adresse de base vaut zéro ou non. Si c'est le cas, le processeur lève une exception matérielle, qui est traitée par l'interprétation ou le compilateur JIT. Une telle situation est en effet signe d'une erreur d'adressage mémoire, qui doit être traitée par l'interpréteur. Typiquement, c'est signe qu'un pointeur n'a pas bien été initialisé, ce qui peut arriver avec du code interprété ou JIT. Les interpréteurs/JIT traitent généralement la situation en ajoutant un test avant toute instruction mémoire, pour vérifier si le pointeur accédé vaut zéro ou non. Avec ''Thumb-EE'', pas besoin d'ajouter les tests en questions : ils sont réalisés automatiquement lors de chaque instruction mémoire. : Les registres pour la pile, comme le pointeur de pile et le pointeur de ''frame'', sont aussi concernés. L'instruction CHKA vérifie que les accès à un tableau ne débordent pas en dehors du tableau en question. Par exemple, pour un tableau de 1024 éléments, elle vérifie si l'indice est compris entre 0 et 1024. Pour cela, elle vérifie si l'indice de tableau est bien dans l'intervalle adéquat et lève une exception matérielle si ce n'est pas le cas. Son utilité se comprend quand on sait que tous les accès à un tableau sont vérifiés par l'interpréteur. L'interpréteur est censé ajouter des instructions pour vérifier les indices, à savoir deux ou trois branchements. Avec CHKA, le test se fait en une seule instruction. D'autres modifications mineures de l'encodage des instructions sont aussi présentes, ainsi que des modifications pour les instructions LOAD/STORE. Précisément, dans les modes "base + indice" et "base + décalage", le décalage et l'indice subissent maintenant des décalages. L'idée est de simplifier le calcul d'adresse. De quoi économiser quelques instructions lors des calculs d'adresse, ce qui facilite le travail de l'interpréteur/JIT. Et des instructions LOAD/STORE précises voient leur comportement modifié pour faciliter la gestion de la pile de Java ou l'exécution des méthodes locales. Pour faciliter le travail de l'interpréteur, deux instructions HBP et HBLP ont été ajoutées. Elles branchent vers une fonction qui gère les exceptions logicielles (une fonctionnalité présente dans de nombreux langages, comme Java). La première effectue un branchement vers la fonction en question, la seconde sauvegarde l'adresse de retour avant de faire ce branchement. Les deux peuvent brancher vers 256 fonctions pré-déterminées, sans avoir à présenter leur adresse. ==La traduction binaire accélérée par le matériel== Plus haut, nous avons surtout parlé des compilateurs et des interpréteurs. Cependant, nous devons aussi parler de la '''traduction binaire'''. Elle traduit un programme écrit dans un code machine vers un autre code machine. Par exemple, elle traduit un programme compilé pour un CPU x86 vers un code machine ARM. En général, le système d'exploitation est généralement compilé pour le jeu d'instruction natif, mais il exécute des applications prévues pour le x86. Les applications sont traduites par le système d'exploitation, avant d'exécuter le code traduit. : Nous parlerons dans la suite de code machine source et de code machine cible, pour parler respectivement du code à traduire et du code obtenu après traduction. Idem avec d'autres termes comme architecture cible/source, ou jeu d'instruction cible/source. La traduction binaire est surtout utilisée pour des questions d'émulation ou de compatibilité. Par exemple, lorsque les Macintosh sont passés de processeurs Power PC vers des processeurs x86, le système d'exploitation Mac OS utilisait la traduction binaire pour convertir les anciennes applications Power PC vers du code x86. Les utilisateurs n'y ont vu que du feu. Le système de traduction binaire était appelé Rosetta 1. Par la suite, lors de la transition de processeurs x86 vers des processeurs Apple, Rosetta 2 a vu le jour. La traduction binaire a surtout été utilisée pour traduire du code x86 vers un autre jeu d'instruction. Il faut dire que le x86 est le jeu d'instruction dominant. De nombreuses entreprises ont eu pour ambition de briser l'hégémonie du x86 sur PC, en remplaçant le x86 par un jeu d'instruction plus performant, tout en gardant une compatibilité maximale. La traduction binaire était la seule solution pratique. Elle était le plus souvent intégralement réalisée en logiciel, comme c'était le cas sur les architectures Itanium avec le ''IA-32 Execution Layer''. ===Les généralités sur la traduction binaire assistée en matériel=== L'architecture source est presque tout le temps une architecture CISC, assez ancienne, qu'on souhaite émuler. Le choix de l'architecture cible se porte souvent sur une architecture VLIW, fort différente de l'architecture source, et ce pour obtenir une économie de matériel conséquente. Pour s'exécuter rapidement, le code traduit doit exploiter le parallélisme d'instruction, à savoir exécuter plusieurs instructions en même temps dans des unités de calcul séparées. Il est possible d'utiliser un processeur superscalaire avec exécution dans le désordre pour cela, mais au prix d'un cout important en transistors. Alors qu'en utilisant un CPU VLIW, c'est le traducteur binaire qui fait tout le travail d'extraction du parallélisme d'instruction. Et après tout, quitte à avoir un traducteur binaire, autant lui refiler le boulot d'optimisation. Compiler du code à la volée est certes assez lourd, mais qu'il existe des algorithmes efficaces pour regrouper des instructions indépendantes dans une seule instruction VLIW. Au passage, le processeur VLIW a plus de registres que le processeur source. Cela permet de faire du renommage de registres directement en logiciel. Le traducteur binaire n'hésite pas à changer les noms de registres entre instructions source et instruction VLIW cible, afin de supprimer des dépendances de données. En conséquence, cela ouvre des opportunités de parallélisme, qui sont exploitées lors du regroupement des opérations en instructions VLIW. Vous pourriez penser que le choix d'un CPU VLIW pour émuler du CISC est tout sauf optimal. Vous devez penser que la traduction binaire est d'autant plus simple que l'architecture source et cibles sont semblables. Dans les faits, ce n'est pas tellement traduire les instructions qui pose problème, mais plus la gestion du registre d'état, des exceptions matérielles, de la mémoire virtuelle, la différence entre gros-boutisme et petit-boutisme, de même que des différences d'adressage pour les périphériques. Le premier problème est la gestion des conditions, notamment en présence d'un registre d'état. En pratique, la traduction binaire s'utilise pour traduire du code CISC vers du code RISC ou VLIW. Les architectures source ont donc un registre d'état, qui est mis à jour non seulement par des instructions de test, mais aussi des instructions arithmétiques. L'architecture cible n'a elle pas de registre d'état, mais des registres à prédicats. La traduction de l'un vers l'autre est alors quelque peu compliquée. Et elle est d'autant plus compliquée que le registre d'état est la source de dépendances d'instruction implicites, qui réduisent les performances. Par exemple, si une instruction arithmétique modifie le registre d'état, cela peut impacter l'exécution d'une instruction ultérieure, qui lit ce registre d'état. Pour éliminer ces fausses dépendances, le traducteur binaire doit renommer ce registre en logiciel et détecter les dépendances utiles. Et le compilateur doit gérer les cas où il y a beaucoup de distance entre les deux, voire les cas où l'instruction dépendante n'a pas encore été analysée par le compilateur. Un autre problème est lié à la gestion des exceptions matérielles, et précisément des exceptions précises. Pour rappel, une instruction VLIW regroupe plusieurs opérations, opérations qui correspondraient à une instruction machine sur un CPU pas VLIW. En conséquence, plusieurs instructions du langage machine "source" sont regroupées en une seule instruction VLIW. Et il faut tenir compte du cas où une instruction source lève une exception. Dans le code VLIW, cela signifie qu'une opération lève une exception, et il faut annuler partiellement l'instruction VLIW associée. Et par partiellement, on veut dire que seules les opérations suivantes dans l'ordre du programme source doivent être annulées, puis ré-exécutées. Et c'est un sacré casse-tête ! Les processeurs VLIW utilisent comme solution le mécanisme d'exceptions différées des processeurs EPIC, vu il y a quelques chapitres. Pour résumer, le code est exécuté par blocs d'instruction, délimités par des branchements ou tout autre limite/barrière pertinente dans le code. Un bloc de code est compilé en deux versions : une version rapide sans exceptions matérielles, une version lente qui gère les exceptions précises. La version rapide s'exécute sans exécuter les exceptions. Cependant, les exceptions matérielles sont enregistrées, pour être prises en compte à la toute fin du bloc de code. La version rapide est exécutée en premier et elle mémorise si une exception matérielle a eu lieu. Puis une instruction vérifie si une exception a eu lieu et décide quoi faire. Si aucune exception n'a eu lieu, elle passe à la suite du programme, les résultats du bloc de code sont définitivement acceptés. Mais si une exception a eu lieu, tout est annulé. Le processeur est remis dans l'état initial, puis le code est ré-exécuté instruction par instruction de manière à gérer l'exception correctement. ===La traduction par pré-décodage=== Une première solution serait de faire la traduction binaire dans le cache d'instruction. Lors d'un défaut de cache, le code chargé depuis la RAM est traduit en code machine cible. Et c'est ce code machine cible qui est mémorisé dans le cache d'instruction et exécuté par le processeur. La technique marche sur le papier et n'est qu'une amélioration des techniques de pré-décodage vues il y a quelques chapitres. Cependant, le code obtenu est une traduction assez basique, qui n'incorpore pas d'optimisations dignes de ce nom. De plus, elle gère mal le cas où la taille du code source et cible sont potentiellement très différentes. Concrètement, elle est surtout utile pour traduire du code d'un processeur RISC vers un autre processeur RISC, dont les tailles d'instructions sont similaires. Elle ne permet pas d'émuler plusieurs jeux d'instructions différents, le cout en matériel (un circuit de pré-décodage par jeu d'instruction) serait trop important. ===Le projet DAISY d'IBM=== De très rares processeurs étaient conçus pour accélérer cette traduction binaire, afin de garder de bonnes performances. Les premiers à avoir étudié l'idée étaient IBM, avec leur projet DAISY (''Dynamically Architected Instruction Set from Yorktown''). Le projet de base était de convertir à la volée du code compilé pour des CPU Power PC, vers du code VLIW. Les chercheurs d'IBM avaient développé un algorithme de traduction dynamique efficace, ainsi qu'un processeur VLIW disposant d'optimisations spécifiques à la traduction binaire dynamique, à la volée. De nombreuses idées de ce projet ont été reprises ou re-découvertes par la société Transmetta, avec ses processeurs Crusoe et Efficieon, puis par NVIDIA avec son projet Denver. Les ingénieurs de ce projet ont étudié la possibilité d'émuler plusieurs jeux d'instructions différents sur un même processeur, notamment le s390 d'IBM et le x86. En théorie, cela demande juste d'avoir plusieurs programmes de traduction binaire : un pour le x86, un autre pour le s390, éventuellement un autre pour le Power PC. Mais cela ne s'est pas concrétisé. Mais les ingénieurs ont étudié quelle pourrait être l'architecture VLIW idéale pour ça. Par exemple, il fallait des additions 3-opérandes pour simplifier les calculs d'adresse. Une difficulté était la gestion du registre d'état, dont les bits ne sont pas mis à jour de la même manière sur le x86 et le s390 ou le Power PC. La différence entre gros-boutisme et petit-boutisme était aussi un problème, de même que des différences d'adressage pour les périphériques. Pour que la traduction binaire soit efficace, le processeur VLIW intègre diverses optimisations, comme les exceptions différées et les branchements multi-voies. La principale est la suivante : seul le code des boucles ou fonctions exécutées fréquemment est traduit en code VLIW, le reste du code est interprété. En effet, traduire du code binaire prend plus de temps qu'une simple interprétation. Pour du code qui ne sera exécutée qu'une seule fois, il est plus rapide d'utiliser l'interprétation. Par contre, pour du code exécuté beaucoup de fois, le cout de la traduction binaire est amorti, dilué sur N exécutions, compensé par le gain en temps d'exécution de ce code traduit. Au final, cela permet de ne traduire que le code qui le mérite. La détection du code fréquemment exécuté est réalisée dans le cœur VLIW. L'unité de branchement mémorise les derniers branchements rencontrés et le nombre de fois qu'ils ont été exécutés. Si ils ont été exécutés un certain nombre de fois, l'unité de branchement lève une exception matérielle, qui invoque le traducteur binaire. La routine de cette exception est le traducteur binaire proprement dit. En clair, la traduction binaire est démarrée quand le processeur détecte qu'une fonction a été exécutée plus de N fois, via une exception matérielle dédiée. La mesure du nombre d'exécution d'un branchement se fait dans l'unité de calcul dédiée aux branchements, avec l'aide d'une sorte de ''branch adress buffer'' modifié. Pour rappel, le ''branch adress buffer'' mémorise le ''Program counter'' de chaque branchement récemment rencontré. Ici, chaque entrée du ''branch adress buffer'' est associée à un compteur incrémenté à chaque exécution du branchement. ===Les processeurs Crusoe et Efficieon de Transmetta=== Les processeurs Crusoe Et Efficeron sont deux processeurs VLIW produits par la société Transmetta, une société californienne rachetée par NVIDIA. Ils étaient conçus pour exécuter spécifiquement des programmes x86, système d'exploitation inclus. L'idée derrière ce projet était d'exécuter du code x86 sans que les applications, ni même le système d'exploitation et le BIOS soient au courant ! Les processeurs Transmetta étaient en réalité conçus pour exécuter un programme unique, le ''Code Morphing Software'' (CMS), qui traduisait le code x86 en code VLIW. Transmetta ne voulait pas que son processeur VLIW soit exploité directement, sans le CMS. Il n'avait pas rendu public de compilateur pour traduire du C vers du code VLIW, il n'avait pas donné la documentation du jeu d'instruction VLIW. Des efforts de rétro-ingénieurie, documentés sur le site realworldtech, ont cependant permis de comprendre comment étaient encodées les instructions du processeur Crusoe. Comme pour le projet Daisy, le ''Code Morphing Software'' utilisait à la fois interprétation et traduction binaire, selon les besoins. Les instructions sont interprétées lors de leurs premières exécutions, mais le CMS bascule sur de la traduction en code VLIW après un certain nombre d'exécution. Ainsi, les boucles souvent exécutées sont traduites en code VLIW, alors que le reste du code est interprété. Le choix du code à traduire est le fait du CMS, il utilise des heuristiques complexes pour, qui ne sont pas connues dans le détail. Le ''Code Morphing Software'' est mémorisé dans une EEPROM, ce qui en fait un ''firmware'' situé en dessous du BIOS. Le CMS démarre ensuite le BIOS, qui lui-même démarre le système d'exploitation, qui lui-même démarre les pilotes de périphériques et les programmes. Le CMS se réserve les 16 premiers mébioctets de l'espace d'adressage. L'EEPROM du CMS est mappée dedans, mais n'en utilise que la moitié. Le reste est utilisé comme cache, pour mémoriser le code VLIW traduit par le CMS. Les deux processeurs intégraient aussi deux mémoires SRAM, utilisées par le CMS, appelées la ''local program memory'' (LPM) et la ''local data memory'' (LDM). La première contient du code qui gére les interruptions, la mémoire virtuelle, les exceptions matérielles, les problèmes d'alignement mémoire, et quelques fonctions très fréquemment utilisées par le CMS. La seconde est de la mémoire RAM utilisée par ces fonctions de la ''local program memory''. Les mémoires caches L1 et L2 sont séparées de ces deux mémoires. Le processeur Crusoe et Efficieon étaient des processeurs VLIW très simples. Ils n'avaient même pas de MMU, ni de fonctionnalités importantes sur les CPU x86. Pour Crusoe, les instructions VLIW étaient encodées sur 64 ou 128 bits, et regroupaient entre 2 à 4 opérations. Les instructions LOAD/STORE ne supportaient pas d'adressage indicé, juste de l'adressage indirect à registre. Crusoe avait 5 unités de calcul : deux ALU entières, une unité LOAD/STORE, une unité de branchement, une FPU. Efficieon doublait le nombre d'ALU entière et d'unité LOAD/STORE, les instructions VLIW passaient à 256 bits. Le processeur Crusoe contient 160 registres, dont 64 registres généraux, 32 registres flottants. Les registres généraux font 32 bits, ce qui est cohérent avec le fait que les CPU x86 de l'époque étaient des processeurs 32 bits. Quant aux registres flottants, ils faisaient 80 bits, ce qui colle avec la taille des registres flottants de la FPU x87 utilisée à l'époque. Sur les 64 registres généraux, seuls 48 étaient réellement utilisable pour mémoriser des opérandes. Une partie des registres généraux étaient utilisés pour la gestion de la pile, d'autres pour mémoriser l'état du CPU x86 émulé, un registre était un registre zéro non accesible en écriture. La gestion des exceptions est optimisée avec un système d'exceptions différées, le même que celui décrit plus haut. Le processeur utilise un système similaire, pour effectuer des lectures anticipées, qui a été expliqué dans le chapitre sur les processeurs VLIW/EPIC. Sauf qu'il s'agit là d'une optimisation pour gagner en performances en général, pas une aide utile pour gérer des exceptions précises ou toute autre subtilité de la traduction binaire. Pour gérer les exceptions différées et les lectures anticipées, le processeur mémorise l'état du processeur avant de démarrer un bloc de code. Pour cela, le processeur contient des ''shadow registers'', 48 registres entiers et 16 registres flottants. Vous remarquerez qu'il y a autant de registres entiers que de ''shadow registers'' entiers. Les registres entiers/flottants sont mémorisés dans les ''shadow registers'' avant d'exécuter un bloc de code, afin que le processeur puisse revenir à l'état de base. Une autre fonctionnalité liée est le ''gated store buffer''. Le principe est d'éviter toute écriture en mémoire RAM, tant que le code VLIW n'a pas émis d'instruction ''commit''. L'idée est que les écritures sont conservées dans la ''store queue'' de l'unité LOAD/STORE. Pour rappel, la ''store queue'' met en attente les écritures soit tant que les écritures ne sont pas terminées, soit tant que la RAM est occupée. L'idée est que les écritures sont accumulées dans la ''store queue'' tant que l'instruction ''commit'' n'a pas eu lieu. Le ''store queue'' est "déversé dans la RAM" seulement quand l'instruction ''commit'' s'exécute. ===Les CPU NVIDIA Denver=== Nvidia a tenté quelque chose de similaire avec son projet Denver. NVIDIA a racheté l'entreprise Transmetta, ses brevets, et a tenté de refaire la même chose. Le projet initial était de traduire du code x86 vers un jeu d'instruction VLIW propriétaire, comme l'a fait Transmetta. Mais NVIDIA n'a pas réussi à acquérir la licence du jeu d'instruction x86 et s'est rabattu sur le jeu d'instruction ARM. Le processeur né de ce projet est le Tegra K1-64 CPU. Il s'agit d'un processeur composé d'un cœur VLIW, couplé à deux décodeurs d'instructions ARM. Les deux décodeurs ARM traduisent une paire d'instructions ARM consécutives en instructions VLIW, pour les exécuter sur le cœur VLIW. Le CPU peut fonctionner selon deux modes : ARM et VLIW. En somme, le processeur supporte deux jeux d'instruction : le jeu d'instruction ARM, et un jeu d'instruction VLIW. En mode ARM, les instructions ARM sont chargées depuis le cache, traduites en VLIW par les décodeurs ARM, puis exécutées par le cœur VLIW. En mode optimisé, le CPU exécute des instructions VLIW chargées depuis le cache d'instruction, les décodeurs ne sont pas utilisés. : Le CPU supporte en réalité deux jeux d'instruction ARM : ARM8 et 7. Mais ils correspondent au même mode pour le CPU. Un point important est que le processeur peut changer très rapidement de mode, en à peine quelques cycles d'horloges. On n'est clairement pas dans le cas des CPU x86, qui mettent des plombes pour passer du mode 32 à 64 bits et inversement. La commutation est tellement rapide qu'on peut considérer que le processeur supporte deux jeux d'instruction simultanément. Et le support simultané de deux jeux d'instruction facilite l'implémentation de la traduction binaire. Encore une fois, inutile de traduire du code qui ne sera exécutée qu'une seule fois, mieux vaut l'exécuter directement dans le mode ARM. Surtout que le processeur supporte un mode ARM qui permet d'exécuter du code ARM sans perte de performance. Par contre, le code fréquemment utilisé, notamment dans des boucles critiques, est traduit en VLIW. Au final, les performances sont optimisées, en limitant le travail du logiciel traducteur binaire. La détection du code fréquemment exécuté se fait différemment que pour les processeurs Transmetta et Daisy. L'unité de branchement ne détecte pas directement le code beaucoup exécuté, même si elle a un rôle à jouer. A la place, la détection est réalisée en logiciel, par un ''thread'' dédié, qui s'exécute sur un cœur séparé. Il détecte le code fréquemment utilisé en regardent les branchements exécutés récemment. Pour cela, le processeur mémorise l'historique des branchements pris récemment et copie cette historique dans une mémoire tampon dédiée, partagée entre tous les cœurs. Vu qu'elle est partagée, le ''thread'' de détection a accès à l'historique sans pertes de performances, seule la copie de l'historique dans ce tampon a un cout en performance, pas son partage. Le code VLIW obtenu après traduction binaire est mémorisé dans la mémoire RAM, dans une portion spécialement réservée pour. Elle fait 128 mébioctets, et est appelée le '''cache d'optimisation''' par NVIDIA. Le cache d'optimisation est protégé en écriture et seul le traducteur binaire peut écrire dedans. Au passage, le traducteur binaire est du code VLIW, ce qui fait qu'il est placé dans le cache d'optimisation. Le processeur détecte automatiquement quand une fonction pour laquelle le code compilé équivalent est disponible. Pour cela, l'unité de chargement contient une table de correspondance entre l'adresse de la fonction ARM, et l'adresse de son équivalent compilé. Quand une fonction est appelée, l'unité de chargement regarde l'adresse de destination du branchement. Si l'adresse est dans cette table, elle récupère l'adresse de la fonction compilée et branche vers celle-ci. Le processeur dispose d'un cache d'instruction de 128 kibioctets, ce qui est très important, mais nécessaire vu la taille des instructions VLIW. Le coeur VLIW dispose de ses propres décodeurs, d'un ''scoreboard'', de 7 unités de calcul et de registres. Pour les unités de calcul, il a deux FPU, deux ALU, une unité de branchement, et deux unités LOAD-STORE faisant aussi ALU. Un point important est que le CPU dispose de 64 registres entiers et de 64 registres flottants. C'est deux fois plus que supporte le jeu d'instruction ARM. En mode ARM, seule la moitié des registres est utilisée. Mais en mode optimisé, le traducteur binaire utilise bien les 64 registres, grâce à une sorte de renommage de registres logicielle. Pour simplifier la traduction binaire, le processeur supporte les techniques vues précédemment. Le processeur intègre un ''gated store buffer'' similaire à celui des processeurs Transmetta. Les techniques d'exceptions différées sont aussi supportées, comme sur les processeurs Transmetta. Il a aussi une unité de préchargement décrite par NVIDIA comme agressive, avec support du préchargement de type anticipé (''runahead''). ===L'émulation de la mémoire virtuelle=== Pour finir, il faut parler de la mémoire virtuelle avec la traduction binaire. Sur le processeur source, le processeur gère à la fois des adresses physiques et virtuelles et sa MMU fait la conversion entre les deux. Sur le processeur VLIW, la MMU est simulée par le programme de traduction binaire, partiellement ou totalement. L'implémentation exacte varie suivant que l'on parle du projet Daisy, des processeurs Transmetta ou de Denver. Sur les processeurs Transmetta, il n'y a pas de MMU ni de mémoire virtuelle. A la place, le programme de traduction binaire émule la mémoire virtuelle du processeur source. Le processeur VLIW ne gère que des adresses physiques, rien d'autre. L'espace d'adressage physique du processeur VLIW a la même taille que celui du processeur émulé, ici des processeurs x86. Il est vraisemblable que les adresses physiques utilisées par le processeur x86 sont les mêmes que celles du processeur VLIW. Sur le projet Daisy, le processeur VLIW gère la mémoire virtuelle via pagination, comme les processeurs émulés. Ce qui fait qu'il gère un espace d'adressage virtuel et un espace d'adressage physique. Pour éviter toute confusion, nous parlerons d'adresse physique/virtuelle VLIW pour les adresses physiques/virtuelles du processeur VLIW, d'adresse virtuelle/physique source pour celle du jeu d'instruction traduit, à savoir du Power PC ou du s390, éventuellement de l'x86. Il faut alors faire le lien entre adresses physiques source et adresses virtuelles VLIW. Pour cela, rien de plus simple : il y a correspondance parfaite. L'adresse physique source numéro N correspond à l'adresse virtuelle VLIW numéro N. Cependant, cela signifie que tout l'espace d'adressage virtuel VLIW serait utilisé par le code à traduire. En réalité, il faut ajouter de la place pour le code traduit et le programme de traduction binaire. La conséquence est que l'espace d'adressage virtuel VLIW est plus large que l'espace d'adressage source. L'espace d'adressage virtuel VLIW est découpé en trois sections : une section pour le code Power PC à traduire, une autre pour le code traduit en VLIW, et une dernière réservée au traducteur binaire. Le programme de traduction est placé dans une ROM mappée en mémoire dans la seconde section. Le reste de la seconde section est réservé à une mémoire RAM utilisée par le programme de traduction. [[File:Mémoire virtuelle sur le projet Daisy.png|centre|vignette|upright=1.5|Mémoire virtuelle sur le projet Daisy]] Il faut noter que chaque page physique source, une fois traduite, correspond à N pages virtuelles VLIW. La raison est que le code traduit prend plus de place que le code machine originel. En conséquence, on doit utiliser une page finale plus grande. Pour le reste, des pages contiguës en mémoire physique source sont elles aussi contiguës en mémoire virtuelle VLIW. Le calcul d'adresse est donc simple : l'adresse physique source est multipliée par N, puis on ajoute l'adresse de base à laquelle commence la section pour le code traduit. ==Les processeurs à jeu d'instruction reconfigurable== Nous venons de voir comment accélérer en matériel la traduction binaire. Mais il existe une alternative qui permet de se passer de traduction binaire, complétement. L'idée est d'avoir un processeur avec un '''jeu d'instruction reconfigurable'''. Le nom est assez parlent : on peut configurer le processeur de manière à supporter le jeu d'instruction de son choix ! Par exemple, on pouvait reconfigurer un CPU de manière à ce qu'il supporte le jeu d'instruction x86, ou qu'il exécute du ''bytecode'' Java, ou du code ARMv8, etc. De tels processeurs sont encore plus rare que ceux vu avant dans ce chapitre, mais ils ont existé ! Les processeurs de ce type avaient un ''microcode réinscriptible'', à savoir que c'était des processeurs microcodés pour lesquels on pouvait modifier le microcode. L'idée était qu'en changeant le microcode, on changeait le jeu d'instruction du processeur. Pour cela, le microcode était associé à une mémoire SRAM dans laquelle le microcode était copié au démarrage du CPU. Il y avait donc une micro-ROM et une micro-RAM. Les instructions étaient décodées en utilisant le microcode dans la micro-SRAM, la micro-ROM ne servait qu'au démarrage. En modifiant le contenu de la micro-SRAM, on pouvait altérer le microcode, voire le remplacer totalement. ===Le Burroughs B1700 : introduction=== Un exemple est celui du Burroughs B1700, et il est intéressant d'étudier en détail son architecture. Son architecture est décrite dans le livre "Interpreting Machines : Architecture and Programming of the Bl700/Bl800 Series", écrit par Elliott I. Organick et James A. Hinds. Une partie de ce qui vfa suivre est un très court résumé de ce livre. Il disposait de 4 registres généraux, nommés X, Y, T et L, tous de 24 bits. les deux premiers étaient reliés à une ALU de 24 bits capable d'effectuer des opérations arithmétiques basiques. Fait étonnant, l'ALU faisait tous les calculs en même temps et fournissait ses résultats dans 7 registres : un registre SUM pour l'addition, un registre DIFFERENCE pour la soustraction, et un registre par opération bit à bit. Les registres pour les opérations bit à bit sont : CMPX et CMPY pour l'opération NOT sur chaque opérande, XANY pour le ET bit à bit, XEOY pour le XOR, XORY pour le OU logique A cela, il fallait ajouter deux registres de "résultat" MSKX et MSKY. Ils fournissaient un masque dépendant de la taille du résultat en bits. Cela permettait de gérer des données de 8 ou 16 bits, alors que l'ALU faisait 24 bits. Et cela n'était pas limité à 16 ou 8 bits, mais n'importe quelle taille inférieure ou égale à 24 bits était supportée, même des tailles non-conventionnelles comme des résultats codés sur 3 bits, 9 bits, 12 bits, etc. L'ALU fournissait 12 conditions, qui alimentait un registre d'état. Le registre d'état était segmenté en trois registres appelés XYST, XYCN et BICN. ===Une architecture bit-adressable=== Si avoir un microcode réinscriptible est suffisant, quelques choix de ''design'' facilitent l'émulation de jeux d'instructions variés. Par exemple, le CPU Burroughs B1700 avait ajouté une fonctionnalité très importante : il était bit-adressable. En clair, il pouvait adresser la mémoire bit par bit, si nécessaire. L'avantage est que cela permettait d'émuler des architectures dont la taille des registres/''byte''/mots était très différente. Un autre avantage est que cela facilitait la gestion des instructions de taille variable. Détaillons rapidement le premier avantage. Le processeur était un CPU 24 bits, mais il pouvait émuler des processeurs 16 bits, 8 bits, 4 bits, 1 bit, 9 bits, ou toute autre valeur. Pour cela, le processeur lisait des mots de 24 bits et utilisant un ''barrel shifter'' pour sélectionner les bits adéquats. Par exemple, pour un accès de 16 bits, la donnée lue en mémoire était masquée de manière à ne garder que les 16 bits de poids faible. Le ''barrel shifter''/circuit de masquage était placé directement avant le bus mémoire, sur le trajet des données/instructions. Le ''barrel shifter'' était commandé par un registre ''Field Unit'', qui précisait quelle était la taille des données à charger. ===La gestion de la taille des instructions=== Les instructions à décoder n'ont pas de longueur fixe, à savoir qu'elles peuvent faire 16 bit avec tel jeu d'instruction, 8 bits avec un autre, 32 bits sur un autre, etc. Elles peuvent aussi être de longueur fixe ou variable ! Le B1700 intégrait donc un support des instructions de longueur variable, qui était "détourné" pour gérer le fait que des jeux d'instruction différents ont des instructions de taille différentes. Notons que vu que l'architecture est bit-adressable, les contraintes d'alignement des instructions disparaissent, ce qui a des avantages qu'on va détailler dans ce qui suit ! L'implémentation des instructions de taille variable ajoutait un registre avant le registre d'instruction. Le registre d'instruction proprement dit était un registre M de 24 bits. Il était précédé par un registre T, dans lequel l'instruction était assemblée, en combinant plusieurs morceaux. Le registre T est relié à un ''barrel shifter'', différent au précédent, qui permettait de masquer, décaler et extraire une donnée de ce registre. Si l'instruction est codée est codée sur moins de 24 bits, le registre T se contente de masquer les bits en trop. Si l'instruction chargée dépasser 24 bits, le chargement se faisait en plusieurs passes, avec plusieurs lectures en mémoire. Chaque passe découpait l'instruction pour extraire des '''champs''' : l'opcode, les opérandes, les adresses absolues, les constantes immédiates, les numéros de registre, etc. Et ils étaient copiés dans des registres interne au processeur. Par exemple, il servait à extraire l'opcode de l'instruction. Ou encore, pour une instruction de branchement direct, il pouvait extraire l'adresse à laquelle brancher de l'instruction. Le chargement de l'instruction se faisait alors champ par champ. Notons que vu que l'architecture est bit-adressable, les contraintes d'alignement des instructions disparaissent ! Quand on charge une instruction, peut importe qu'elle soit ou non alignée sur 24 bits, elle sera placée dans les bits de poids faible, il suffira de masquer les bits de poids fort inutiles. Par exemple, si on charge une instruction de 16 bits, les 16 bits seront copiés dans les 16 bits de poids faible du registre T, il suffira de masquer l'octet de poids fort. Pas besoin de faire de décalages pour gérer des instructions mal alignées... Pour charger une instruction, il faut connaitre plusieurs informations : l'adresse de son premier bit, sa taille. Pour cela, le B1700 contient deux registres, nommés ''Field Adress'' (FA) et ''Field Length'' (FL), qui indiquent l'adresse du premier bit et la taille de l'instruction à charger. Le premier fait 24 bits, ce qui permet de gérer des adresses de 24 bits. N'oubliez pas que le processeur est bit-adressable, ce qui permet d'adresser 16 Mébi-bits, soit 2 mébioctets. Le registre pour la taille fait lui 16 bits, ce qui permet de gérer des instructions de 65536 bits, soit 8192 octets ! Leur nom commence par ''field'', car le processeur peut gérer des instructions de taille variable et/ou plus longues que 24 bits. Dans ce cas, le chargement de l'instruction se fait champ par champ. Pour donner un exemple, prenons une instruction composée de 20 champs de 12 bits chacun. Les champs sont chargés un par un dans le registre T. Le registre FA est alors initialisé avec l'adresse de l'instruction, il est incrémenté de 12 à chaque cycle. Le registre FL est initialisé avec la taille de l'instruction, soit 20 * 12 = 240 bits, et il est décrémenté de 16 à chaque cycle. Précisons cependant que la taille d'un champ peut être changée d'un cycle à l'autre. Par exemple, le processeur peut charger un opcode de 8 bits, l'interpréter, détecter que l'instruction demande ensuite de charger une adresse de 16 bits. Dans ce cas, le registre FL est immédiatement altéré pour charger 16 bits, au lieu des 8 bits de l'opcode immédiatement précédent. ==Les jeux d'instructions dédiés à un langage de programmation== De rares processeurs sont conçus pour un langage de programmation en particulier. On appelle ces processeurs, conçus pour des besoins particuliers, des '''processeurs dédiés'''. Par exemple, les fameux ''Burrough E-mode'' B5000/B6000/B7000 étaient spécialement conçus pour exécuter de l'ALGOL-60. Leurs cousins B2000/B3000/B4000 étaient eux conçus pour le COBOL. Des langages fonctionnels ont aussi eu droit à leurs processeurs dédiés. Le prolog en est un bel exemple, avec les superordinateurs de 5ème génération qui lui étaient dédié. On peut aussi citer les machines LISP, dédiés au langage LISP, qui datent des années 1970. Elles étaient capables d’exécuter certaines fonctions de base du langage directement dans leurs circuits : elles possédaient notamment un ''garbage collector'' câblé dans ses circuits ainsi que des instructions machines supportant un typage déterminé à l’exécution. Les processeurs dédiés ont eu leur heure de gloire au début de l'informatique, à une époque où les langages de haut niveau venaient d'être inventés. À cette époque, les compilateurs n'étaient pas performants et ne savaient pas bien optimiser le code machine. Il était alors rationnel, pour l'époque, de rapprocher le code machine cible et le langage de programmation de haut niveau. De nombreuses architectures dédiés ont ainsi été inventées, avant que les concepteurs se rendent compte des défauts de cette approche. Les défauts en question ne sont pas nombreux, mais assez simples à comprendre. Premièrement, elles sont très rapides pour un langage de programmation en particulier, mais sont assez mauvaises pour les autres, d'où un problème de "compatibilité". Ajoutons à cela que les langages de programmation peuvent évoluer, devenir de moins en moins populaires/utilisés, ce qui rend la création d'architectures généralistes plus pertinente. Enfin, les architectures dédiées sont évidemment des processeurs CISC, pour implémenter les nombreuses fonctionnalités des langages évolués. Et les défauts des CISC sont assez rédhibitoires à l'heure actuelle. À l'heure actuelle, les algorithmes des compilateurs se sont améliorés et savent nettement mieux utiliser le matériel. Ils produisent du code machine efficace, ce qui rend les architecture dédiées bien moins intéressantes. Si on ajoute les défauts de ces architectures dédiées, par étonnant que les architectures dédiées aient presque disparues. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'accélération matérielle de la virtualisation | prevText=L'accélération matérielle de la virtualisation | next=Le matériel réseau | nextText=Le matériel réseau }} </noinclude> cs7oc1ctrcjl9reu24m5uxi7lrfex18 764013 764012 2026-04-19T16:07:46Z Mewtow 31375 /* Le Burroughs B1700 : introduction */ 764013 wikitext text/x-wiki De nos jours, la majorité des programmeurs programment dans des langages de haut niveau. Il est très rare pour eux d'avoir à utiliser de l'assembleur, et encore moins un langage machine. Les programmes écrits dans un langage de haut niveau sont traduits en langage machine par un logiciel appelé le compilateur. [[File:Compilation.PNG|centre|vignette|upright=2.5|Compilateur : principe.]] Il se trouve que quelques rares processeurs sont conçus pour faciliter le travail du compilateur. Et ce sont ces processeurs que nous allons voir dans ce qui suit. Ils représentent une catégorie de jeux d'instruction à part, qui n'a pas vraiment de nom. ==La compilation et l’interprétation== Un compilateur traduit donc du code source, écrit dans un langage de haut niveau, vers du langage machine. La traduction est rarement directe. En général, le compilateur traduit le code source en assembleur, qui est lui-même traduit en langage machine. L'assembleur est une représentation textuelle du langage machine alors que le code machine est du binaire exécutable par le processeur. La traduction se fait donc en deux étapes, la '''compilation''' proprement dite et l''''assemblage''', réalisées respectivement par un compilateur et un assembleur. Il faut parfois rajouter une troisième phase d'édition des liens, que nous passons volontairement sous silence. {| |[[File:Code C.png|vignette|200px|Le code source, ici en C.]] |[[File:Assem.png|vignette|250px|Le code assembleur.]] |[[File:Binary file - hello world (C programming).png|vignette|150px|Le code machine.]] |} ===L'interprétation et la compilation à la volée=== Une alternative à la compilation est l''''interprétation''', qui transforme le code source en code machine à la volée. Avec l'interprétation, le code source est passé à un logiciel appelé l''''interpréteur''', qui exécute le code source ligne de code par ligne de code. Une ligne de code est traduite en code machine, qui est exécutée, puis l'interpréteur passe à la ligne suivante. [[File:Kaantotulkkaus.png|centre|vignette|upright=3|Compilation versus interprétation.]] Un défaut de l'interprétation est son cout en performance, qui est assez important. Aussi, l'interprétation stricte a évolué vers un hybride entre interprétation et compilation. L'idée est qu'une partie peu importante du code source est interprétée, alors que le code important est compilé juste avant d'être exécuté. Pour le dire autrement, le code source est partiellement compilé à la volée, juste avant son exécution. Aussi, on parle de '''compilation à la volée'''. Le terme en anglais est ''Just In Time compilation'', abrévié en JIT. En soi, le JIT est associé à de l'interprétation, les JIT sont en réalité des hybrides interpréteurs-compilateurs. Un point important est que compiler du code à la volée est assez lourd, cela a un cout en performance. Par contre, le code compilé s'exécute plus vite que du code interprété. Si on exécute le code compilé une seule fois, le cout de la compilation l'emporte sur le gain à l'exécution. Mais le code compilé est réutilisé autant de fois que nécessaire. Le gain à l'exécution est donc multiplié par le nombre d'exécutions, alors que le cout de la compilation est répartit sur plusieurs exécutions, il est amorti. Les portions du code sont choisies de manière à avoir un gain en performance maximal. L'idée générale est que le code exécuté très souvent est compilé, alors que du code exécuté pas souvent est interprété. Typiquement, les boucles les plus souvent exécutées sont compilées, le code exécuté une seule fois est interprété. ===Les langages intermédiaires=== Les compilateurs modernes passent par un '''langage intermédiaire''' pour faire la transformation en code machine. Le compilateur traduit le langage de haut niveau en langage intermédiaire, puis traduit le langage intermédiaire en code machine cible. Le langage intermédiaire est parfois appelé le '''''bytecode''''', et ce terme recouvre aussi le code écrit avec du ''bytecode''. L'interprétation aussi peut passer par l'intermédiaire d'un un ''bytecode'', ce n'est pas limité aux compilateurs proprement dit. {| |[[File:Intermediate representation scheme.png|centre|vignette|upright=1.5|Représentation intermédiaire d'un compilateur.]] |[[File:Code interpreter scheme.png|centre|vignette|upright=1.5|Fonctionnement d'un interpréteur.]] |} Faire ainsi a de nombreux avantages, le principal étant d'avoir un compilateur capable de traduire un langage de haut niveau vers plusieurs jeux d'instructions différents. Cela permet d'avoir, par exemple, un compilateur qui traduit du C soit en code machine x86, soit en code machine pour un CPU ARM, soit pour un CPU POWERPC, etc. [[File:Compiler design.svg|centre|vignette|upright=3|Compiler design]] Pour l'interprétation, l'usage d'un ''bytecode'' a d'autres avantages. Pour l'exploiter, il faut distribuer non pas le code source, mais le ''bytecode''. En clair, l'interpréteur ne prend pas en entrée le code source, mais du ''bytecode'' déjà compilé. La traduction est alors beaucoup plus simple, car le code source a déjà été partiellement compilé, par la traduction en ''bytecode''. Interpréter du code source est en effet assez compliqué : il faut effectuer des étapes d'analyse lexicale, sémantique, et bien d'autres. Avec du ''bytecode'', ces étapes ont été réalisées lors de la compilation du ''bytecode'', l'interpréteur a alors peut de choses à faire. L'interprétation du ''bytecode'' se fait instruction par instruction, au niveau du ''bytecode''. Une instruction du ''bytecode'' est traduite en une instruction machine équivalente, qui est exécutée. Si l'instruction est un peu complexe, il exécute une fonction/procédure qui fait la même chose. Pour résumer, le ''bytecode'' est ensuite traduit à la volée et exécuté instruction par instruction par un logiciel appelé l'interpréteur. [[File:Control table.png|centre|vignette|upright=2|Control table]] Le langage intermédiaire peut être vu comme l'assembleur d'un processeur, n'existe pas forcément dans la réalité, mais dont le jeu d'instruction est décrit en détail. Le processeur en question est appelé une '''machine abstraite''', ou encore une ''machine virtuelle''. Nous utiliserons le terme de machine abstraite dans ce qui suit. La machine abstraite n'est pas la même suivant que l'on cible la compilation ou l'interprétation/JIT. Pour l'interprétation/JIT, la machine abstraite est souvent une machine à pile, car cela simplifie grandement la traduction en code machine final. L'interprétation demande que la traduction du ''bytecode'' en code machine soit la plus simple possible. Et cela demande de prendre en compte pas mal de détails. Par exemple, les différents jeux d'instruction existants n'ont pas le même nombre de registres, ce qui pose problème lors de la traduction du ''bytecode''. Lors de la transformation en code machine, un algorithme d'allocation de registres se débrouille pour traduire le code intermédiaire en code qui utilise un nombre limité de registres. Pour améliorer cette allocation de registres, il y a deux solutions niveau ''bytecode'' : soit utiliser un nombre illimité de registres, soit utiliser une machine à pile. Pour les machines à pile, il existe un algorithme simple et rapide pour traduire un code écrit pour une machine à pile en un code écrit pour un processeur avec des registres, qui se débrouille pas trop mal pour allouer efficacement les registres. C'est un avantage assez important pour les langages interprétés. ==Les processeurs qui exécutent du bytecode== Le ''bytecode'' est un code machine, ce qui signifie qu'il peut en théorie s'exécuter sur un processeur qui implémente le jeu d’instruction associé. Si le jeu d'instruction d'un ''bytecode'' est souvent une description censée être fictive, elle n'en reste pas moins un jeu d'instruction et des caractéristiques précises, qu'on peut l'implémenter en matériel ! Néanmoins, tous les ''bytecode'' ne sont pas égaux de ce point de vue. Certains sont faciles à implémenter en matériel, d'autres non. Les ''bytecodes'' implémentés en matériel sont ceux dont les machines abstraites sont des machines à pile. Un premier exemple est celui des processeurs Pascal MicroEngine, qui exécutaient directement le bytecode du langage Pascal, le fameux UCSD P-code. Un second exemple est la '''machine SECD''', qui sert de langage intermédiaire pour certains compilateurs de langages fonctionnels. Elle a été implémentée en matériel par plusieurs équipes, notamment par les chercheurs de l'université de Calgary en 1989. Dans le même genre, quelques processeurs simples étaient capables d’exécuter directement le ''bytecode'' du langage FORTH. Le FORTH, un des premiers langages à pile de haut niveau, possède de nombreuses implémentations hardware et est un des rares langages de haut niveau dont le ''bytecode'' a été utilisé comme langage machine sur certains processeurs. Par exemple, on peut citer le processeur FC16, capable d’exécuter nativement du ''bytecode'' FORTH. ===Les processeurs Java=== Le cas le plus impressionnant est celui de la machine virtuelle Java, qui est un design de processeur comme un autre. En temps normal, le ''bytecode'' Java est compilé ou interprété, mais certains processeurs exécutaient du ''bytecode'' Java directement, sans interprétation ni compilation. Ils sont appelés des '''processeurs Java'''. On peut citer les processeurs ARM disposant de l'extension Jazelle, du JEMCore et du aJ-100 de aJile Systems, le picoJava II de Sun, et quelques autres. Il y a aussi eu quelques projets de recherche ou processeurs open source, comme Komodo, jamuth, le ''Java Optimized Processor'' et quelques autres projets du même genre. L'implémentation de la machine virtuelle Java n'était cependant pas complète. Quelques instructions complexes n'étaient pas gérées par le processeur et devaient être émulées en logiciel. Mais la grosse majorité des instructions l'était. Les instructions non-supportées étaient émulées avec la méthode ''trap and emulate'', à savoir qu'un opcode inconnu déclenchait une exception matérielle ''opcode invalide'', dont la routine pouvait émuler les instructions qui doivent l'être. Le processeur aJ-100 de aJile est capable d'exécuter la quasi-totalité des instructions de la JVM Java, seules deux instructions faisant exception. Les '''accélérateurs Java''' sont une solution intermédiaire, qui permet de traduire à la volée du ''bytecode'' Java en instructions RISC, exécutées sur un autre processeur. Il s'agit formellement de traduction binaire, mais je préfère en parler ici. L'idée est de combiner un processeur RISC avec un coprocesseur Java. Le coprocesseur lit le ''bytecode'' Java et le traduit en instructions RISC, ces dernières étant exécutées sur le processeur RISC. Le JA108 de Nozomi était un coprocesseur de ce genre, au même titre que le coprocesseur JSTAR de JEDI. L'extension Jazelle de certains CPU ARM fonctionnait vraisemblablement sur un principe similaire, sauf que le coprocesseur était intégré dans le processeur ARM, entre le cache d'instruction et le décodeur d'instructions ARM. L'intérêt des processeurs Java n'est pas qu'une question de performance. Certes, ils permettent d’exécuter plus vite les programmes compilés en ''bytecode'', comme des programmes Java pour la JVM Java. Mais l'intérêt est aussi de faire des économies de mémoire RAM et ROM. Je rappelle que la machine virtuelle Java est une machine à pile, ce qui fait que sa densité de code est excellente. Les programmes en ''bytecode'' Java sont donc très petits, et prennent peu de place en mémoire ROM. De plus, utiliser un processeur Java permet de se passer d'interpréteur ou de JIT Java, ce qui économise encore plus de mémoire. ===Transformer un processeur RISC en machine à pile=== La plupart des processeurs Java utilisent en interne une architecture à registre généraux, qui émule une machine à registre en microcode. Quelques registres sont réservés pour l'état de la machine virtuelle, avec au minimum un registre réservé pour le pointeur de pile, un autre pour un pointeur vers un ''pool'' de constantes, et quelques autres. D'autres registres servent à mémoriser le haut de la pile, à savoir le sommet de la pile et quelques opérandes situées juste en-dessous. Par exemple, le processeur picoJava II gardait les 64 opérandes au sommet de la pile dans 64 registres généraux, séparés des registres pour les pointeurs et l'état de la JVM. Une optimisation possible, envisagée sur les processeurs picoJava, était une sorte de macro-fusion sous stéroïde. L'idée était de fusionner certaines séries d'instructions Java en une seule instruction machine. Typiquement, pour faire une opération arithmétique, une machine à pile empile deux opérandes et exécute une opération, ce qui prend trois instructions machine. La spécification picoJava 1 a proposé d'envoyer directement la seconde opérande en entrée de l'ALU, ce qui fusionnait l'empilement de la seconde opérande et l'opération arithmétique. Le processeur picoJava II va plus loin et fusionne les trois instructions en une seule instruction RISC, agissant sur des registres. Mais la majorité des processeurs Java ne faisaient pas cela, car cela complexifie grandement le décodeur. Les processeurs ''bigfoot'' de DCT utilisaient un système différent pour émuler une pile avec des registres. Leur banc de registre de 64 bits était coupé en deux : les 16 premiers registres étaient utilisés pour la pile, les 48 suivants étaient utilisés pour autre chose. L'idée est que le registre 0 mémorise un emplacement vide, le registre 1 contient le sommet de la pile, le registre 2 l'opérande sous le sommet de la pile, et ainsi de suite. Les 15 premières opérandes de la pile sont donc mémorisées dans les registres. Les instructions écrivent leur résultat dans le registre adéquat, puis tous les registres sont décalés pour que le résultat passe dans le registre R1. Par exemple, les instructions PUSH chargent une opérande dans le registre 0, puis tous les registres sont décalés de un rang, pour que l'opérande chargée soit dans le registre 1. Le registre 0 devient alors le registre numéro 1, le registre 1 devient le 2, et ainsi de suite. Par contre, les opérations arithmétiques font autrement. Une instruction d'addition, par exemple, lit les opérandes dans le registre 1 et 2, puis place son résultat dans le registre 2 (les deux opérandes sont dépilées). Puis, les registres sont décalés d'un rang pour que ce registre 2 devienne le 1. Les registres ne sont en réalité pas décalés, il n'y a pas de transferts entre registres. A la place, les 16 premiers registres utilisaient une forme limitée de renommage de registres, similaire au banc de registre tournant utilisé sur les processeurs EPIC comme l'Itanium. Les numéros de registres sont renommés, décalés, à chaque fois qu'on empile ou dépile une opérande. Pour faire le renommage de registres, processeur contient un ''register stack counter'' de 4 bits, qui est incrémenté à chaque fois qu'on empile une donnée, et est décrémenté quand on dépile une donnée. Quand on veut accéder à un opérande, le numéro de registre architectural associé est traduit en numéro de registre physique, en additionnant le ''register stack counter'' (modulo 16, pour rester sur 4 bits). ==L'accélération de l'interprétation/JIT : le ''Thumb-EE'' d'ARM== Quelques processeurs ont ajouté des instructions pour faciliter le travail des interpréteurs/JIT. Par exemple, en 2005, ARM a ajouté le mode ''Thumb Execution Environment'', qui faisait cela. Il reprenait le jeu d'instruction compact ''thumb'' et ajoutait quelques instructions et modifiait le comportement d'instructions existantes. Le jeu d'instruction ''Thumb-EE'' a été ajouté sur les CPU ARM en 2005, mais a été déprécié en 2011, par manque d'utilité. Mais la tentative mérite qu'on s'attarde dessus. Les CPU de gamme M, déstinés à l'embarqué, n'ont jamais supporté ''Thumb-EE'', vu que de tels processeurs ne sont pas conçus pour exécuter du code interprété/JIT. Le mode ''Thumb-EE'' est un mode d'exécution, séparé de l'ARM normal. Ainsi, une instruction ''Thumb-EE'' et une instruction ARM peuvent avoir le même encodage en binaire, mais se comporter différemment. Le processeur est à tout moment soit en mode ''Thumb-EE'', soit en mode ARM normal, ce qui précise comment ces instructions doivent se comporter. Des instructions ont été ajoutées par ''Thumb-EE'', pour qu'il fasse son travail. L'entrée et la sortie du mode ''Thumb-EE'' se fait avec deux instructions : ENTERX et LEAVEX. Une fois en mode ''Thumb-EE'', le décodage des instructions ''thumb'' se fait avec les règles du ''Thumb-EE'', et non celles du ''thumb'' normal. Une première différence entre ''thumb'' normal et ''Thumb-EE'' est l'ajout de ce qui s'appelle un ''null check'' pour les instructions mémoire. Le ''null check'' est utilisé pour les instructions en mode d'adressage "base + indice" ou "Base + décalage", ou tout autre mode d'adressage avec un registre de base utilisés dans des calculs d'adresse. L'idée est de vérifier si l'adresse de base vaut zéro ou non. Si c'est le cas, le processeur lève une exception matérielle, qui est traitée par l'interprétation ou le compilateur JIT. Une telle situation est en effet signe d'une erreur d'adressage mémoire, qui doit être traitée par l'interpréteur. Typiquement, c'est signe qu'un pointeur n'a pas bien été initialisé, ce qui peut arriver avec du code interprété ou JIT. Les interpréteurs/JIT traitent généralement la situation en ajoutant un test avant toute instruction mémoire, pour vérifier si le pointeur accédé vaut zéro ou non. Avec ''Thumb-EE'', pas besoin d'ajouter les tests en questions : ils sont réalisés automatiquement lors de chaque instruction mémoire. : Les registres pour la pile, comme le pointeur de pile et le pointeur de ''frame'', sont aussi concernés. L'instruction CHKA vérifie que les accès à un tableau ne débordent pas en dehors du tableau en question. Par exemple, pour un tableau de 1024 éléments, elle vérifie si l'indice est compris entre 0 et 1024. Pour cela, elle vérifie si l'indice de tableau est bien dans l'intervalle adéquat et lève une exception matérielle si ce n'est pas le cas. Son utilité se comprend quand on sait que tous les accès à un tableau sont vérifiés par l'interpréteur. L'interpréteur est censé ajouter des instructions pour vérifier les indices, à savoir deux ou trois branchements. Avec CHKA, le test se fait en une seule instruction. D'autres modifications mineures de l'encodage des instructions sont aussi présentes, ainsi que des modifications pour les instructions LOAD/STORE. Précisément, dans les modes "base + indice" et "base + décalage", le décalage et l'indice subissent maintenant des décalages. L'idée est de simplifier le calcul d'adresse. De quoi économiser quelques instructions lors des calculs d'adresse, ce qui facilite le travail de l'interpréteur/JIT. Et des instructions LOAD/STORE précises voient leur comportement modifié pour faciliter la gestion de la pile de Java ou l'exécution des méthodes locales. Pour faciliter le travail de l'interpréteur, deux instructions HBP et HBLP ont été ajoutées. Elles branchent vers une fonction qui gère les exceptions logicielles (une fonctionnalité présente dans de nombreux langages, comme Java). La première effectue un branchement vers la fonction en question, la seconde sauvegarde l'adresse de retour avant de faire ce branchement. Les deux peuvent brancher vers 256 fonctions pré-déterminées, sans avoir à présenter leur adresse. ==La traduction binaire accélérée par le matériel== Plus haut, nous avons surtout parlé des compilateurs et des interpréteurs. Cependant, nous devons aussi parler de la '''traduction binaire'''. Elle traduit un programme écrit dans un code machine vers un autre code machine. Par exemple, elle traduit un programme compilé pour un CPU x86 vers un code machine ARM. En général, le système d'exploitation est généralement compilé pour le jeu d'instruction natif, mais il exécute des applications prévues pour le x86. Les applications sont traduites par le système d'exploitation, avant d'exécuter le code traduit. : Nous parlerons dans la suite de code machine source et de code machine cible, pour parler respectivement du code à traduire et du code obtenu après traduction. Idem avec d'autres termes comme architecture cible/source, ou jeu d'instruction cible/source. La traduction binaire est surtout utilisée pour des questions d'émulation ou de compatibilité. Par exemple, lorsque les Macintosh sont passés de processeurs Power PC vers des processeurs x86, le système d'exploitation Mac OS utilisait la traduction binaire pour convertir les anciennes applications Power PC vers du code x86. Les utilisateurs n'y ont vu que du feu. Le système de traduction binaire était appelé Rosetta 1. Par la suite, lors de la transition de processeurs x86 vers des processeurs Apple, Rosetta 2 a vu le jour. La traduction binaire a surtout été utilisée pour traduire du code x86 vers un autre jeu d'instruction. Il faut dire que le x86 est le jeu d'instruction dominant. De nombreuses entreprises ont eu pour ambition de briser l'hégémonie du x86 sur PC, en remplaçant le x86 par un jeu d'instruction plus performant, tout en gardant une compatibilité maximale. La traduction binaire était la seule solution pratique. Elle était le plus souvent intégralement réalisée en logiciel, comme c'était le cas sur les architectures Itanium avec le ''IA-32 Execution Layer''. ===Les généralités sur la traduction binaire assistée en matériel=== L'architecture source est presque tout le temps une architecture CISC, assez ancienne, qu'on souhaite émuler. Le choix de l'architecture cible se porte souvent sur une architecture VLIW, fort différente de l'architecture source, et ce pour obtenir une économie de matériel conséquente. Pour s'exécuter rapidement, le code traduit doit exploiter le parallélisme d'instruction, à savoir exécuter plusieurs instructions en même temps dans des unités de calcul séparées. Il est possible d'utiliser un processeur superscalaire avec exécution dans le désordre pour cela, mais au prix d'un cout important en transistors. Alors qu'en utilisant un CPU VLIW, c'est le traducteur binaire qui fait tout le travail d'extraction du parallélisme d'instruction. Et après tout, quitte à avoir un traducteur binaire, autant lui refiler le boulot d'optimisation. Compiler du code à la volée est certes assez lourd, mais qu'il existe des algorithmes efficaces pour regrouper des instructions indépendantes dans une seule instruction VLIW. Au passage, le processeur VLIW a plus de registres que le processeur source. Cela permet de faire du renommage de registres directement en logiciel. Le traducteur binaire n'hésite pas à changer les noms de registres entre instructions source et instruction VLIW cible, afin de supprimer des dépendances de données. En conséquence, cela ouvre des opportunités de parallélisme, qui sont exploitées lors du regroupement des opérations en instructions VLIW. Vous pourriez penser que le choix d'un CPU VLIW pour émuler du CISC est tout sauf optimal. Vous devez penser que la traduction binaire est d'autant plus simple que l'architecture source et cibles sont semblables. Dans les faits, ce n'est pas tellement traduire les instructions qui pose problème, mais plus la gestion du registre d'état, des exceptions matérielles, de la mémoire virtuelle, la différence entre gros-boutisme et petit-boutisme, de même que des différences d'adressage pour les périphériques. Le premier problème est la gestion des conditions, notamment en présence d'un registre d'état. En pratique, la traduction binaire s'utilise pour traduire du code CISC vers du code RISC ou VLIW. Les architectures source ont donc un registre d'état, qui est mis à jour non seulement par des instructions de test, mais aussi des instructions arithmétiques. L'architecture cible n'a elle pas de registre d'état, mais des registres à prédicats. La traduction de l'un vers l'autre est alors quelque peu compliquée. Et elle est d'autant plus compliquée que le registre d'état est la source de dépendances d'instruction implicites, qui réduisent les performances. Par exemple, si une instruction arithmétique modifie le registre d'état, cela peut impacter l'exécution d'une instruction ultérieure, qui lit ce registre d'état. Pour éliminer ces fausses dépendances, le traducteur binaire doit renommer ce registre en logiciel et détecter les dépendances utiles. Et le compilateur doit gérer les cas où il y a beaucoup de distance entre les deux, voire les cas où l'instruction dépendante n'a pas encore été analysée par le compilateur. Un autre problème est lié à la gestion des exceptions matérielles, et précisément des exceptions précises. Pour rappel, une instruction VLIW regroupe plusieurs opérations, opérations qui correspondraient à une instruction machine sur un CPU pas VLIW. En conséquence, plusieurs instructions du langage machine "source" sont regroupées en une seule instruction VLIW. Et il faut tenir compte du cas où une instruction source lève une exception. Dans le code VLIW, cela signifie qu'une opération lève une exception, et il faut annuler partiellement l'instruction VLIW associée. Et par partiellement, on veut dire que seules les opérations suivantes dans l'ordre du programme source doivent être annulées, puis ré-exécutées. Et c'est un sacré casse-tête ! Les processeurs VLIW utilisent comme solution le mécanisme d'exceptions différées des processeurs EPIC, vu il y a quelques chapitres. Pour résumer, le code est exécuté par blocs d'instruction, délimités par des branchements ou tout autre limite/barrière pertinente dans le code. Un bloc de code est compilé en deux versions : une version rapide sans exceptions matérielles, une version lente qui gère les exceptions précises. La version rapide s'exécute sans exécuter les exceptions. Cependant, les exceptions matérielles sont enregistrées, pour être prises en compte à la toute fin du bloc de code. La version rapide est exécutée en premier et elle mémorise si une exception matérielle a eu lieu. Puis une instruction vérifie si une exception a eu lieu et décide quoi faire. Si aucune exception n'a eu lieu, elle passe à la suite du programme, les résultats du bloc de code sont définitivement acceptés. Mais si une exception a eu lieu, tout est annulé. Le processeur est remis dans l'état initial, puis le code est ré-exécuté instruction par instruction de manière à gérer l'exception correctement. ===La traduction par pré-décodage=== Une première solution serait de faire la traduction binaire dans le cache d'instruction. Lors d'un défaut de cache, le code chargé depuis la RAM est traduit en code machine cible. Et c'est ce code machine cible qui est mémorisé dans le cache d'instruction et exécuté par le processeur. La technique marche sur le papier et n'est qu'une amélioration des techniques de pré-décodage vues il y a quelques chapitres. Cependant, le code obtenu est une traduction assez basique, qui n'incorpore pas d'optimisations dignes de ce nom. De plus, elle gère mal le cas où la taille du code source et cible sont potentiellement très différentes. Concrètement, elle est surtout utile pour traduire du code d'un processeur RISC vers un autre processeur RISC, dont les tailles d'instructions sont similaires. Elle ne permet pas d'émuler plusieurs jeux d'instructions différents, le cout en matériel (un circuit de pré-décodage par jeu d'instruction) serait trop important. ===Le projet DAISY d'IBM=== De très rares processeurs étaient conçus pour accélérer cette traduction binaire, afin de garder de bonnes performances. Les premiers à avoir étudié l'idée étaient IBM, avec leur projet DAISY (''Dynamically Architected Instruction Set from Yorktown''). Le projet de base était de convertir à la volée du code compilé pour des CPU Power PC, vers du code VLIW. Les chercheurs d'IBM avaient développé un algorithme de traduction dynamique efficace, ainsi qu'un processeur VLIW disposant d'optimisations spécifiques à la traduction binaire dynamique, à la volée. De nombreuses idées de ce projet ont été reprises ou re-découvertes par la société Transmetta, avec ses processeurs Crusoe et Efficieon, puis par NVIDIA avec son projet Denver. Les ingénieurs de ce projet ont étudié la possibilité d'émuler plusieurs jeux d'instructions différents sur un même processeur, notamment le s390 d'IBM et le x86. En théorie, cela demande juste d'avoir plusieurs programmes de traduction binaire : un pour le x86, un autre pour le s390, éventuellement un autre pour le Power PC. Mais cela ne s'est pas concrétisé. Mais les ingénieurs ont étudié quelle pourrait être l'architecture VLIW idéale pour ça. Par exemple, il fallait des additions 3-opérandes pour simplifier les calculs d'adresse. Une difficulté était la gestion du registre d'état, dont les bits ne sont pas mis à jour de la même manière sur le x86 et le s390 ou le Power PC. La différence entre gros-boutisme et petit-boutisme était aussi un problème, de même que des différences d'adressage pour les périphériques. Pour que la traduction binaire soit efficace, le processeur VLIW intègre diverses optimisations, comme les exceptions différées et les branchements multi-voies. La principale est la suivante : seul le code des boucles ou fonctions exécutées fréquemment est traduit en code VLIW, le reste du code est interprété. En effet, traduire du code binaire prend plus de temps qu'une simple interprétation. Pour du code qui ne sera exécutée qu'une seule fois, il est plus rapide d'utiliser l'interprétation. Par contre, pour du code exécuté beaucoup de fois, le cout de la traduction binaire est amorti, dilué sur N exécutions, compensé par le gain en temps d'exécution de ce code traduit. Au final, cela permet de ne traduire que le code qui le mérite. La détection du code fréquemment exécuté est réalisée dans le cœur VLIW. L'unité de branchement mémorise les derniers branchements rencontrés et le nombre de fois qu'ils ont été exécutés. Si ils ont été exécutés un certain nombre de fois, l'unité de branchement lève une exception matérielle, qui invoque le traducteur binaire. La routine de cette exception est le traducteur binaire proprement dit. En clair, la traduction binaire est démarrée quand le processeur détecte qu'une fonction a été exécutée plus de N fois, via une exception matérielle dédiée. La mesure du nombre d'exécution d'un branchement se fait dans l'unité de calcul dédiée aux branchements, avec l'aide d'une sorte de ''branch adress buffer'' modifié. Pour rappel, le ''branch adress buffer'' mémorise le ''Program counter'' de chaque branchement récemment rencontré. Ici, chaque entrée du ''branch adress buffer'' est associée à un compteur incrémenté à chaque exécution du branchement. ===Les processeurs Crusoe et Efficieon de Transmetta=== Les processeurs Crusoe Et Efficeron sont deux processeurs VLIW produits par la société Transmetta, une société californienne rachetée par NVIDIA. Ils étaient conçus pour exécuter spécifiquement des programmes x86, système d'exploitation inclus. L'idée derrière ce projet était d'exécuter du code x86 sans que les applications, ni même le système d'exploitation et le BIOS soient au courant ! Les processeurs Transmetta étaient en réalité conçus pour exécuter un programme unique, le ''Code Morphing Software'' (CMS), qui traduisait le code x86 en code VLIW. Transmetta ne voulait pas que son processeur VLIW soit exploité directement, sans le CMS. Il n'avait pas rendu public de compilateur pour traduire du C vers du code VLIW, il n'avait pas donné la documentation du jeu d'instruction VLIW. Des efforts de rétro-ingénieurie, documentés sur le site realworldtech, ont cependant permis de comprendre comment étaient encodées les instructions du processeur Crusoe. Comme pour le projet Daisy, le ''Code Morphing Software'' utilisait à la fois interprétation et traduction binaire, selon les besoins. Les instructions sont interprétées lors de leurs premières exécutions, mais le CMS bascule sur de la traduction en code VLIW après un certain nombre d'exécution. Ainsi, les boucles souvent exécutées sont traduites en code VLIW, alors que le reste du code est interprété. Le choix du code à traduire est le fait du CMS, il utilise des heuristiques complexes pour, qui ne sont pas connues dans le détail. Le ''Code Morphing Software'' est mémorisé dans une EEPROM, ce qui en fait un ''firmware'' situé en dessous du BIOS. Le CMS démarre ensuite le BIOS, qui lui-même démarre le système d'exploitation, qui lui-même démarre les pilotes de périphériques et les programmes. Le CMS se réserve les 16 premiers mébioctets de l'espace d'adressage. L'EEPROM du CMS est mappée dedans, mais n'en utilise que la moitié. Le reste est utilisé comme cache, pour mémoriser le code VLIW traduit par le CMS. Les deux processeurs intégraient aussi deux mémoires SRAM, utilisées par le CMS, appelées la ''local program memory'' (LPM) et la ''local data memory'' (LDM). La première contient du code qui gére les interruptions, la mémoire virtuelle, les exceptions matérielles, les problèmes d'alignement mémoire, et quelques fonctions très fréquemment utilisées par le CMS. La seconde est de la mémoire RAM utilisée par ces fonctions de la ''local program memory''. Les mémoires caches L1 et L2 sont séparées de ces deux mémoires. Le processeur Crusoe et Efficieon étaient des processeurs VLIW très simples. Ils n'avaient même pas de MMU, ni de fonctionnalités importantes sur les CPU x86. Pour Crusoe, les instructions VLIW étaient encodées sur 64 ou 128 bits, et regroupaient entre 2 à 4 opérations. Les instructions LOAD/STORE ne supportaient pas d'adressage indicé, juste de l'adressage indirect à registre. Crusoe avait 5 unités de calcul : deux ALU entières, une unité LOAD/STORE, une unité de branchement, une FPU. Efficieon doublait le nombre d'ALU entière et d'unité LOAD/STORE, les instructions VLIW passaient à 256 bits. Le processeur Crusoe contient 160 registres, dont 64 registres généraux, 32 registres flottants. Les registres généraux font 32 bits, ce qui est cohérent avec le fait que les CPU x86 de l'époque étaient des processeurs 32 bits. Quant aux registres flottants, ils faisaient 80 bits, ce qui colle avec la taille des registres flottants de la FPU x87 utilisée à l'époque. Sur les 64 registres généraux, seuls 48 étaient réellement utilisable pour mémoriser des opérandes. Une partie des registres généraux étaient utilisés pour la gestion de la pile, d'autres pour mémoriser l'état du CPU x86 émulé, un registre était un registre zéro non accesible en écriture. La gestion des exceptions est optimisée avec un système d'exceptions différées, le même que celui décrit plus haut. Le processeur utilise un système similaire, pour effectuer des lectures anticipées, qui a été expliqué dans le chapitre sur les processeurs VLIW/EPIC. Sauf qu'il s'agit là d'une optimisation pour gagner en performances en général, pas une aide utile pour gérer des exceptions précises ou toute autre subtilité de la traduction binaire. Pour gérer les exceptions différées et les lectures anticipées, le processeur mémorise l'état du processeur avant de démarrer un bloc de code. Pour cela, le processeur contient des ''shadow registers'', 48 registres entiers et 16 registres flottants. Vous remarquerez qu'il y a autant de registres entiers que de ''shadow registers'' entiers. Les registres entiers/flottants sont mémorisés dans les ''shadow registers'' avant d'exécuter un bloc de code, afin que le processeur puisse revenir à l'état de base. Une autre fonctionnalité liée est le ''gated store buffer''. Le principe est d'éviter toute écriture en mémoire RAM, tant que le code VLIW n'a pas émis d'instruction ''commit''. L'idée est que les écritures sont conservées dans la ''store queue'' de l'unité LOAD/STORE. Pour rappel, la ''store queue'' met en attente les écritures soit tant que les écritures ne sont pas terminées, soit tant que la RAM est occupée. L'idée est que les écritures sont accumulées dans la ''store queue'' tant que l'instruction ''commit'' n'a pas eu lieu. Le ''store queue'' est "déversé dans la RAM" seulement quand l'instruction ''commit'' s'exécute. ===Les CPU NVIDIA Denver=== Nvidia a tenté quelque chose de similaire avec son projet Denver. NVIDIA a racheté l'entreprise Transmetta, ses brevets, et a tenté de refaire la même chose. Le projet initial était de traduire du code x86 vers un jeu d'instruction VLIW propriétaire, comme l'a fait Transmetta. Mais NVIDIA n'a pas réussi à acquérir la licence du jeu d'instruction x86 et s'est rabattu sur le jeu d'instruction ARM. Le processeur né de ce projet est le Tegra K1-64 CPU. Il s'agit d'un processeur composé d'un cœur VLIW, couplé à deux décodeurs d'instructions ARM. Les deux décodeurs ARM traduisent une paire d'instructions ARM consécutives en instructions VLIW, pour les exécuter sur le cœur VLIW. Le CPU peut fonctionner selon deux modes : ARM et VLIW. En somme, le processeur supporte deux jeux d'instruction : le jeu d'instruction ARM, et un jeu d'instruction VLIW. En mode ARM, les instructions ARM sont chargées depuis le cache, traduites en VLIW par les décodeurs ARM, puis exécutées par le cœur VLIW. En mode optimisé, le CPU exécute des instructions VLIW chargées depuis le cache d'instruction, les décodeurs ne sont pas utilisés. : Le CPU supporte en réalité deux jeux d'instruction ARM : ARM8 et 7. Mais ils correspondent au même mode pour le CPU. Un point important est que le processeur peut changer très rapidement de mode, en à peine quelques cycles d'horloges. On n'est clairement pas dans le cas des CPU x86, qui mettent des plombes pour passer du mode 32 à 64 bits et inversement. La commutation est tellement rapide qu'on peut considérer que le processeur supporte deux jeux d'instruction simultanément. Et le support simultané de deux jeux d'instruction facilite l'implémentation de la traduction binaire. Encore une fois, inutile de traduire du code qui ne sera exécutée qu'une seule fois, mieux vaut l'exécuter directement dans le mode ARM. Surtout que le processeur supporte un mode ARM qui permet d'exécuter du code ARM sans perte de performance. Par contre, le code fréquemment utilisé, notamment dans des boucles critiques, est traduit en VLIW. Au final, les performances sont optimisées, en limitant le travail du logiciel traducteur binaire. La détection du code fréquemment exécuté se fait différemment que pour les processeurs Transmetta et Daisy. L'unité de branchement ne détecte pas directement le code beaucoup exécuté, même si elle a un rôle à jouer. A la place, la détection est réalisée en logiciel, par un ''thread'' dédié, qui s'exécute sur un cœur séparé. Il détecte le code fréquemment utilisé en regardent les branchements exécutés récemment. Pour cela, le processeur mémorise l'historique des branchements pris récemment et copie cette historique dans une mémoire tampon dédiée, partagée entre tous les cœurs. Vu qu'elle est partagée, le ''thread'' de détection a accès à l'historique sans pertes de performances, seule la copie de l'historique dans ce tampon a un cout en performance, pas son partage. Le code VLIW obtenu après traduction binaire est mémorisé dans la mémoire RAM, dans une portion spécialement réservée pour. Elle fait 128 mébioctets, et est appelée le '''cache d'optimisation''' par NVIDIA. Le cache d'optimisation est protégé en écriture et seul le traducteur binaire peut écrire dedans. Au passage, le traducteur binaire est du code VLIW, ce qui fait qu'il est placé dans le cache d'optimisation. Le processeur détecte automatiquement quand une fonction pour laquelle le code compilé équivalent est disponible. Pour cela, l'unité de chargement contient une table de correspondance entre l'adresse de la fonction ARM, et l'adresse de son équivalent compilé. Quand une fonction est appelée, l'unité de chargement regarde l'adresse de destination du branchement. Si l'adresse est dans cette table, elle récupère l'adresse de la fonction compilée et branche vers celle-ci. Le processeur dispose d'un cache d'instruction de 128 kibioctets, ce qui est très important, mais nécessaire vu la taille des instructions VLIW. Le coeur VLIW dispose de ses propres décodeurs, d'un ''scoreboard'', de 7 unités de calcul et de registres. Pour les unités de calcul, il a deux FPU, deux ALU, une unité de branchement, et deux unités LOAD-STORE faisant aussi ALU. Un point important est que le CPU dispose de 64 registres entiers et de 64 registres flottants. C'est deux fois plus que supporte le jeu d'instruction ARM. En mode ARM, seule la moitié des registres est utilisée. Mais en mode optimisé, le traducteur binaire utilise bien les 64 registres, grâce à une sorte de renommage de registres logicielle. Pour simplifier la traduction binaire, le processeur supporte les techniques vues précédemment. Le processeur intègre un ''gated store buffer'' similaire à celui des processeurs Transmetta. Les techniques d'exceptions différées sont aussi supportées, comme sur les processeurs Transmetta. Il a aussi une unité de préchargement décrite par NVIDIA comme agressive, avec support du préchargement de type anticipé (''runahead''). ===L'émulation de la mémoire virtuelle=== Pour finir, il faut parler de la mémoire virtuelle avec la traduction binaire. Sur le processeur source, le processeur gère à la fois des adresses physiques et virtuelles et sa MMU fait la conversion entre les deux. Sur le processeur VLIW, la MMU est simulée par le programme de traduction binaire, partiellement ou totalement. L'implémentation exacte varie suivant que l'on parle du projet Daisy, des processeurs Transmetta ou de Denver. Sur les processeurs Transmetta, il n'y a pas de MMU ni de mémoire virtuelle. A la place, le programme de traduction binaire émule la mémoire virtuelle du processeur source. Le processeur VLIW ne gère que des adresses physiques, rien d'autre. L'espace d'adressage physique du processeur VLIW a la même taille que celui du processeur émulé, ici des processeurs x86. Il est vraisemblable que les adresses physiques utilisées par le processeur x86 sont les mêmes que celles du processeur VLIW. Sur le projet Daisy, le processeur VLIW gère la mémoire virtuelle via pagination, comme les processeurs émulés. Ce qui fait qu'il gère un espace d'adressage virtuel et un espace d'adressage physique. Pour éviter toute confusion, nous parlerons d'adresse physique/virtuelle VLIW pour les adresses physiques/virtuelles du processeur VLIW, d'adresse virtuelle/physique source pour celle du jeu d'instruction traduit, à savoir du Power PC ou du s390, éventuellement de l'x86. Il faut alors faire le lien entre adresses physiques source et adresses virtuelles VLIW. Pour cela, rien de plus simple : il y a correspondance parfaite. L'adresse physique source numéro N correspond à l'adresse virtuelle VLIW numéro N. Cependant, cela signifie que tout l'espace d'adressage virtuel VLIW serait utilisé par le code à traduire. En réalité, il faut ajouter de la place pour le code traduit et le programme de traduction binaire. La conséquence est que l'espace d'adressage virtuel VLIW est plus large que l'espace d'adressage source. L'espace d'adressage virtuel VLIW est découpé en trois sections : une section pour le code Power PC à traduire, une autre pour le code traduit en VLIW, et une dernière réservée au traducteur binaire. Le programme de traduction est placé dans une ROM mappée en mémoire dans la seconde section. Le reste de la seconde section est réservé à une mémoire RAM utilisée par le programme de traduction. [[File:Mémoire virtuelle sur le projet Daisy.png|centre|vignette|upright=1.5|Mémoire virtuelle sur le projet Daisy]] Il faut noter que chaque page physique source, une fois traduite, correspond à N pages virtuelles VLIW. La raison est que le code traduit prend plus de place que le code machine originel. En conséquence, on doit utiliser une page finale plus grande. Pour le reste, des pages contiguës en mémoire physique source sont elles aussi contiguës en mémoire virtuelle VLIW. Le calcul d'adresse est donc simple : l'adresse physique source est multipliée par N, puis on ajoute l'adresse de base à laquelle commence la section pour le code traduit. ==Les processeurs à jeu d'instruction reconfigurable== Nous venons de voir comment accélérer en matériel la traduction binaire. Mais il existe une alternative qui permet de se passer de traduction binaire, complétement. L'idée est d'avoir un processeur avec un '''jeu d'instruction reconfigurable'''. Le nom est assez parlent : on peut configurer le processeur de manière à supporter le jeu d'instruction de son choix ! Par exemple, on pouvait reconfigurer un CPU de manière à ce qu'il supporte le jeu d'instruction x86, ou qu'il exécute du ''bytecode'' Java, ou du code ARMv8, etc. De tels processeurs sont encore plus rare que ceux vu avant dans ce chapitre, mais ils ont existé ! Les processeurs de ce type avaient un ''microcode réinscriptible'', à savoir que c'était des processeurs microcodés pour lesquels on pouvait modifier le microcode. L'idée était qu'en changeant le microcode, on changeait le jeu d'instruction du processeur. Pour cela, le microcode était associé à une mémoire SRAM dans laquelle le microcode était copié au démarrage du CPU. Il y avait donc une micro-ROM et une micro-RAM. Les instructions étaient décodées en utilisant le microcode dans la micro-SRAM, la micro-ROM ne servait qu'au démarrage. En modifiant le contenu de la micro-SRAM, on pouvait altérer le microcode, voire le remplacer totalement. ===Le Burroughs B1700 : introduction=== Un exemple est celui du Burroughs B1700, et il est intéressant d'étudier en détail son architecture. Son architecture est décrite dans le livre "Interpreting Machines : Architecture and Programming of the Bl700/Bl800 Series", écrit par Elliott I. Organick et James A. Hinds. Une partie de ce qui vfa suivre est un très court résumé de ce livre. Il disposait de 4 registres généraux, nommés X, Y, T et L, tous de 24 bits. les deux premiers étaient reliés à une ALU de 24 bits capable d'effectuer des opérations arithmétiques basiques. Fait étonnant, l'ALU faisait tous les calculs en même temps et fournissait ses résultats dans 7 registres : un registre SUM pour l'addition, un registre DIFFERENCE pour la soustraction, et un registre par opération bit à bit. Les registres pour les opérations bit à bit sont : CMPX et CMPY pour l'opération NOT sur chaque opérande, XANY pour le ET bit à bit, XEOY pour le XOR, XORY pour le OU logique A cela, il fallait ajouter deux registres de "résultat" MSKX et MSKY. Ils fournissaient un masque dépendant de la taille du résultat en bits. Cela permettait de gérer des données de 8 ou 16 bits, alors que l'ALU faisait 24 bits. Et cela n'était pas limité à 16 ou 8 bits, mais n'importe quelle taille inférieure ou égale à 24 bits était supportée, même des tailles non-conventionnelles comme des résultats codés sur 3 bits, 9 bits, 12 bits, etc. L'ALU fournissait 12 conditions, qui alimentait un registre d'état. Le registre d'état était segmenté en trois registres appelés XYST, XYCN et BICN. Le processeur contenait de nombreux autres registres de 4 bits qu'on détaillera plus tard, qui au nombre de 27 ! Et le processeur disposait d'une ALU 4 bits pour manipuler ces registres de 4 bits ! Elle est capable de faire des MOV entre registres de 4 bits, des opérations bit à bit, et peut aussi tester si un bit vaut 0 ou 1. La dernière possibilité est très utile pour implémenter les branchements. ===Une architecture bit-adressable=== Si avoir un microcode réinscriptible est suffisant, quelques choix de ''design'' facilitent l'émulation de jeux d'instructions variés. Par exemple, le CPU Burroughs B1700 avait ajouté une fonctionnalité très importante : il était bit-adressable. En clair, il pouvait adresser la mémoire bit par bit, si nécessaire. L'avantage est que cela permettait d'émuler des architectures dont la taille des registres/''byte''/mots était très différente. Un autre avantage est que cela facilitait la gestion des instructions de taille variable. Détaillons rapidement le premier avantage. Le processeur était un CPU 24 bits, mais il pouvait émuler des processeurs 16 bits, 8 bits, 4 bits, 1 bit, 9 bits, ou toute autre valeur. Pour cela, le processeur lisait des mots de 24 bits et utilisant un ''barrel shifter'' pour sélectionner les bits adéquats. Par exemple, pour un accès de 16 bits, la donnée lue en mémoire était masquée de manière à ne garder que les 16 bits de poids faible. Le ''barrel shifter''/circuit de masquage était placé directement avant le bus mémoire, sur le trajet des données/instructions. Le ''barrel shifter'' était commandé par un registre ''Field Unit'', qui précisait quelle était la taille des données à charger. ===La gestion de la taille des instructions=== Les instructions à décoder n'ont pas de longueur fixe, à savoir qu'elles peuvent faire 16 bit avec tel jeu d'instruction, 8 bits avec un autre, 32 bits sur un autre, etc. Elles peuvent aussi être de longueur fixe ou variable ! Le B1700 intégrait donc un support des instructions de longueur variable, qui était "détourné" pour gérer le fait que des jeux d'instruction différents ont des instructions de taille différentes. Notons que vu que l'architecture est bit-adressable, les contraintes d'alignement des instructions disparaissent, ce qui a des avantages qu'on va détailler dans ce qui suit ! L'implémentation des instructions de taille variable ajoutait un registre avant le registre d'instruction. Le registre d'instruction proprement dit était un registre M de 24 bits. Il était précédé par un registre T, dans lequel l'instruction était assemblée, en combinant plusieurs morceaux. Le registre T est relié à un ''barrel shifter'', différent au précédent, qui permettait de masquer, décaler et extraire une donnée de ce registre. Si l'instruction est codée est codée sur moins de 24 bits, le registre T se contente de masquer les bits en trop. Si l'instruction chargée dépasser 24 bits, le chargement se faisait en plusieurs passes, avec plusieurs lectures en mémoire. Chaque passe découpait l'instruction pour extraire des '''champs''' : l'opcode, les opérandes, les adresses absolues, les constantes immédiates, les numéros de registre, etc. Et ils étaient copiés dans des registres interne au processeur. Par exemple, il servait à extraire l'opcode de l'instruction. Ou encore, pour une instruction de branchement direct, il pouvait extraire l'adresse à laquelle brancher de l'instruction. Le chargement de l'instruction se faisait alors champ par champ. Notons que vu que l'architecture est bit-adressable, les contraintes d'alignement des instructions disparaissent ! Quand on charge une instruction, peut importe qu'elle soit ou non alignée sur 24 bits, elle sera placée dans les bits de poids faible, il suffira de masquer les bits de poids fort inutiles. Par exemple, si on charge une instruction de 16 bits, les 16 bits seront copiés dans les 16 bits de poids faible du registre T, il suffira de masquer l'octet de poids fort. Pas besoin de faire de décalages pour gérer des instructions mal alignées... Pour charger une instruction, il faut connaitre plusieurs informations : l'adresse de son premier bit, sa taille. Pour cela, le B1700 contient deux registres, nommés ''Field Adress'' (FA) et ''Field Length'' (FL), qui indiquent l'adresse du premier bit et la taille de l'instruction à charger. Le premier fait 24 bits, ce qui permet de gérer des adresses de 24 bits. N'oubliez pas que le processeur est bit-adressable, ce qui permet d'adresser 16 Mébi-bits, soit 2 mébioctets. Le registre pour la taille fait lui 16 bits, ce qui permet de gérer des instructions de 65536 bits, soit 8192 octets ! Leur nom commence par ''field'', car le processeur peut gérer des instructions de taille variable et/ou plus longues que 24 bits. Dans ce cas, le chargement de l'instruction se fait champ par champ. Pour donner un exemple, prenons une instruction composée de 20 champs de 12 bits chacun. Les champs sont chargés un par un dans le registre T. Le registre FA est alors initialisé avec l'adresse de l'instruction, il est incrémenté de 12 à chaque cycle. Le registre FL est initialisé avec la taille de l'instruction, soit 20 * 12 = 240 bits, et il est décrémenté de 16 à chaque cycle. Précisons cependant que la taille d'un champ peut être changée d'un cycle à l'autre. Par exemple, le processeur peut charger un opcode de 8 bits, l'interpréter, détecter que l'instruction demande ensuite de charger une adresse de 16 bits. Dans ce cas, le registre FL est immédiatement altéré pour charger 16 bits, au lieu des 8 bits de l'opcode immédiatement précédent. ==Les jeux d'instructions dédiés à un langage de programmation== De rares processeurs sont conçus pour un langage de programmation en particulier. On appelle ces processeurs, conçus pour des besoins particuliers, des '''processeurs dédiés'''. Par exemple, les fameux ''Burrough E-mode'' B5000/B6000/B7000 étaient spécialement conçus pour exécuter de l'ALGOL-60. Leurs cousins B2000/B3000/B4000 étaient eux conçus pour le COBOL. Des langages fonctionnels ont aussi eu droit à leurs processeurs dédiés. Le prolog en est un bel exemple, avec les superordinateurs de 5ème génération qui lui étaient dédié. On peut aussi citer les machines LISP, dédiés au langage LISP, qui datent des années 1970. Elles étaient capables d’exécuter certaines fonctions de base du langage directement dans leurs circuits : elles possédaient notamment un ''garbage collector'' câblé dans ses circuits ainsi que des instructions machines supportant un typage déterminé à l’exécution. Les processeurs dédiés ont eu leur heure de gloire au début de l'informatique, à une époque où les langages de haut niveau venaient d'être inventés. À cette époque, les compilateurs n'étaient pas performants et ne savaient pas bien optimiser le code machine. Il était alors rationnel, pour l'époque, de rapprocher le code machine cible et le langage de programmation de haut niveau. De nombreuses architectures dédiés ont ainsi été inventées, avant que les concepteurs se rendent compte des défauts de cette approche. Les défauts en question ne sont pas nombreux, mais assez simples à comprendre. Premièrement, elles sont très rapides pour un langage de programmation en particulier, mais sont assez mauvaises pour les autres, d'où un problème de "compatibilité". Ajoutons à cela que les langages de programmation peuvent évoluer, devenir de moins en moins populaires/utilisés, ce qui rend la création d'architectures généralistes plus pertinente. Enfin, les architectures dédiées sont évidemment des processeurs CISC, pour implémenter les nombreuses fonctionnalités des langages évolués. Et les défauts des CISC sont assez rédhibitoires à l'heure actuelle. À l'heure actuelle, les algorithmes des compilateurs se sont améliorés et savent nettement mieux utiliser le matériel. Ils produisent du code machine efficace, ce qui rend les architecture dédiées bien moins intéressantes. Si on ajoute les défauts de ces architectures dédiées, par étonnant que les architectures dédiées aient presque disparues. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'accélération matérielle de la virtualisation | prevText=L'accélération matérielle de la virtualisation | next=Le matériel réseau | nextText=Le matériel réseau }} </noinclude> ngt5z3sk2oalqgmpn4inv89np32heid 764014 764013 2026-04-19T16:11:56Z Mewtow 31375 /* Le Burroughs B1700 : introduction */ 764014 wikitext text/x-wiki De nos jours, la majorité des programmeurs programment dans des langages de haut niveau. Il est très rare pour eux d'avoir à utiliser de l'assembleur, et encore moins un langage machine. Les programmes écrits dans un langage de haut niveau sont traduits en langage machine par un logiciel appelé le compilateur. [[File:Compilation.PNG|centre|vignette|upright=2.5|Compilateur : principe.]] Il se trouve que quelques rares processeurs sont conçus pour faciliter le travail du compilateur. Et ce sont ces processeurs que nous allons voir dans ce qui suit. Ils représentent une catégorie de jeux d'instruction à part, qui n'a pas vraiment de nom. ==La compilation et l’interprétation== Un compilateur traduit donc du code source, écrit dans un langage de haut niveau, vers du langage machine. La traduction est rarement directe. En général, le compilateur traduit le code source en assembleur, qui est lui-même traduit en langage machine. L'assembleur est une représentation textuelle du langage machine alors que le code machine est du binaire exécutable par le processeur. La traduction se fait donc en deux étapes, la '''compilation''' proprement dite et l''''assemblage''', réalisées respectivement par un compilateur et un assembleur. Il faut parfois rajouter une troisième phase d'édition des liens, que nous passons volontairement sous silence. {| |[[File:Code C.png|vignette|200px|Le code source, ici en C.]] |[[File:Assem.png|vignette|250px|Le code assembleur.]] |[[File:Binary file - hello world (C programming).png|vignette|150px|Le code machine.]] |} ===L'interprétation et la compilation à la volée=== Une alternative à la compilation est l''''interprétation''', qui transforme le code source en code machine à la volée. Avec l'interprétation, le code source est passé à un logiciel appelé l''''interpréteur''', qui exécute le code source ligne de code par ligne de code. Une ligne de code est traduite en code machine, qui est exécutée, puis l'interpréteur passe à la ligne suivante. [[File:Kaantotulkkaus.png|centre|vignette|upright=3|Compilation versus interprétation.]] Un défaut de l'interprétation est son cout en performance, qui est assez important. Aussi, l'interprétation stricte a évolué vers un hybride entre interprétation et compilation. L'idée est qu'une partie peu importante du code source est interprétée, alors que le code important est compilé juste avant d'être exécuté. Pour le dire autrement, le code source est partiellement compilé à la volée, juste avant son exécution. Aussi, on parle de '''compilation à la volée'''. Le terme en anglais est ''Just In Time compilation'', abrévié en JIT. En soi, le JIT est associé à de l'interprétation, les JIT sont en réalité des hybrides interpréteurs-compilateurs. Un point important est que compiler du code à la volée est assez lourd, cela a un cout en performance. Par contre, le code compilé s'exécute plus vite que du code interprété. Si on exécute le code compilé une seule fois, le cout de la compilation l'emporte sur le gain à l'exécution. Mais le code compilé est réutilisé autant de fois que nécessaire. Le gain à l'exécution est donc multiplié par le nombre d'exécutions, alors que le cout de la compilation est répartit sur plusieurs exécutions, il est amorti. Les portions du code sont choisies de manière à avoir un gain en performance maximal. L'idée générale est que le code exécuté très souvent est compilé, alors que du code exécuté pas souvent est interprété. Typiquement, les boucles les plus souvent exécutées sont compilées, le code exécuté une seule fois est interprété. ===Les langages intermédiaires=== Les compilateurs modernes passent par un '''langage intermédiaire''' pour faire la transformation en code machine. Le compilateur traduit le langage de haut niveau en langage intermédiaire, puis traduit le langage intermédiaire en code machine cible. Le langage intermédiaire est parfois appelé le '''''bytecode''''', et ce terme recouvre aussi le code écrit avec du ''bytecode''. L'interprétation aussi peut passer par l'intermédiaire d'un un ''bytecode'', ce n'est pas limité aux compilateurs proprement dit. {| |[[File:Intermediate representation scheme.png|centre|vignette|upright=1.5|Représentation intermédiaire d'un compilateur.]] |[[File:Code interpreter scheme.png|centre|vignette|upright=1.5|Fonctionnement d'un interpréteur.]] |} Faire ainsi a de nombreux avantages, le principal étant d'avoir un compilateur capable de traduire un langage de haut niveau vers plusieurs jeux d'instructions différents. Cela permet d'avoir, par exemple, un compilateur qui traduit du C soit en code machine x86, soit en code machine pour un CPU ARM, soit pour un CPU POWERPC, etc. [[File:Compiler design.svg|centre|vignette|upright=3|Compiler design]] Pour l'interprétation, l'usage d'un ''bytecode'' a d'autres avantages. Pour l'exploiter, il faut distribuer non pas le code source, mais le ''bytecode''. En clair, l'interpréteur ne prend pas en entrée le code source, mais du ''bytecode'' déjà compilé. La traduction est alors beaucoup plus simple, car le code source a déjà été partiellement compilé, par la traduction en ''bytecode''. Interpréter du code source est en effet assez compliqué : il faut effectuer des étapes d'analyse lexicale, sémantique, et bien d'autres. Avec du ''bytecode'', ces étapes ont été réalisées lors de la compilation du ''bytecode'', l'interpréteur a alors peut de choses à faire. L'interprétation du ''bytecode'' se fait instruction par instruction, au niveau du ''bytecode''. Une instruction du ''bytecode'' est traduite en une instruction machine équivalente, qui est exécutée. Si l'instruction est un peu complexe, il exécute une fonction/procédure qui fait la même chose. Pour résumer, le ''bytecode'' est ensuite traduit à la volée et exécuté instruction par instruction par un logiciel appelé l'interpréteur. [[File:Control table.png|centre|vignette|upright=2|Control table]] Le langage intermédiaire peut être vu comme l'assembleur d'un processeur, n'existe pas forcément dans la réalité, mais dont le jeu d'instruction est décrit en détail. Le processeur en question est appelé une '''machine abstraite''', ou encore une ''machine virtuelle''. Nous utiliserons le terme de machine abstraite dans ce qui suit. La machine abstraite n'est pas la même suivant que l'on cible la compilation ou l'interprétation/JIT. Pour l'interprétation/JIT, la machine abstraite est souvent une machine à pile, car cela simplifie grandement la traduction en code machine final. L'interprétation demande que la traduction du ''bytecode'' en code machine soit la plus simple possible. Et cela demande de prendre en compte pas mal de détails. Par exemple, les différents jeux d'instruction existants n'ont pas le même nombre de registres, ce qui pose problème lors de la traduction du ''bytecode''. Lors de la transformation en code machine, un algorithme d'allocation de registres se débrouille pour traduire le code intermédiaire en code qui utilise un nombre limité de registres. Pour améliorer cette allocation de registres, il y a deux solutions niveau ''bytecode'' : soit utiliser un nombre illimité de registres, soit utiliser une machine à pile. Pour les machines à pile, il existe un algorithme simple et rapide pour traduire un code écrit pour une machine à pile en un code écrit pour un processeur avec des registres, qui se débrouille pas trop mal pour allouer efficacement les registres. C'est un avantage assez important pour les langages interprétés. ==Les processeurs qui exécutent du bytecode== Le ''bytecode'' est un code machine, ce qui signifie qu'il peut en théorie s'exécuter sur un processeur qui implémente le jeu d’instruction associé. Si le jeu d'instruction d'un ''bytecode'' est souvent une description censée être fictive, elle n'en reste pas moins un jeu d'instruction et des caractéristiques précises, qu'on peut l'implémenter en matériel ! Néanmoins, tous les ''bytecode'' ne sont pas égaux de ce point de vue. Certains sont faciles à implémenter en matériel, d'autres non. Les ''bytecodes'' implémentés en matériel sont ceux dont les machines abstraites sont des machines à pile. Un premier exemple est celui des processeurs Pascal MicroEngine, qui exécutaient directement le bytecode du langage Pascal, le fameux UCSD P-code. Un second exemple est la '''machine SECD''', qui sert de langage intermédiaire pour certains compilateurs de langages fonctionnels. Elle a été implémentée en matériel par plusieurs équipes, notamment par les chercheurs de l'université de Calgary en 1989. Dans le même genre, quelques processeurs simples étaient capables d’exécuter directement le ''bytecode'' du langage FORTH. Le FORTH, un des premiers langages à pile de haut niveau, possède de nombreuses implémentations hardware et est un des rares langages de haut niveau dont le ''bytecode'' a été utilisé comme langage machine sur certains processeurs. Par exemple, on peut citer le processeur FC16, capable d’exécuter nativement du ''bytecode'' FORTH. ===Les processeurs Java=== Le cas le plus impressionnant est celui de la machine virtuelle Java, qui est un design de processeur comme un autre. En temps normal, le ''bytecode'' Java est compilé ou interprété, mais certains processeurs exécutaient du ''bytecode'' Java directement, sans interprétation ni compilation. Ils sont appelés des '''processeurs Java'''. On peut citer les processeurs ARM disposant de l'extension Jazelle, du JEMCore et du aJ-100 de aJile Systems, le picoJava II de Sun, et quelques autres. Il y a aussi eu quelques projets de recherche ou processeurs open source, comme Komodo, jamuth, le ''Java Optimized Processor'' et quelques autres projets du même genre. L'implémentation de la machine virtuelle Java n'était cependant pas complète. Quelques instructions complexes n'étaient pas gérées par le processeur et devaient être émulées en logiciel. Mais la grosse majorité des instructions l'était. Les instructions non-supportées étaient émulées avec la méthode ''trap and emulate'', à savoir qu'un opcode inconnu déclenchait une exception matérielle ''opcode invalide'', dont la routine pouvait émuler les instructions qui doivent l'être. Le processeur aJ-100 de aJile est capable d'exécuter la quasi-totalité des instructions de la JVM Java, seules deux instructions faisant exception. Les '''accélérateurs Java''' sont une solution intermédiaire, qui permet de traduire à la volée du ''bytecode'' Java en instructions RISC, exécutées sur un autre processeur. Il s'agit formellement de traduction binaire, mais je préfère en parler ici. L'idée est de combiner un processeur RISC avec un coprocesseur Java. Le coprocesseur lit le ''bytecode'' Java et le traduit en instructions RISC, ces dernières étant exécutées sur le processeur RISC. Le JA108 de Nozomi était un coprocesseur de ce genre, au même titre que le coprocesseur JSTAR de JEDI. L'extension Jazelle de certains CPU ARM fonctionnait vraisemblablement sur un principe similaire, sauf que le coprocesseur était intégré dans le processeur ARM, entre le cache d'instruction et le décodeur d'instructions ARM. L'intérêt des processeurs Java n'est pas qu'une question de performance. Certes, ils permettent d’exécuter plus vite les programmes compilés en ''bytecode'', comme des programmes Java pour la JVM Java. Mais l'intérêt est aussi de faire des économies de mémoire RAM et ROM. Je rappelle que la machine virtuelle Java est une machine à pile, ce qui fait que sa densité de code est excellente. Les programmes en ''bytecode'' Java sont donc très petits, et prennent peu de place en mémoire ROM. De plus, utiliser un processeur Java permet de se passer d'interpréteur ou de JIT Java, ce qui économise encore plus de mémoire. ===Transformer un processeur RISC en machine à pile=== La plupart des processeurs Java utilisent en interne une architecture à registre généraux, qui émule une machine à registre en microcode. Quelques registres sont réservés pour l'état de la machine virtuelle, avec au minimum un registre réservé pour le pointeur de pile, un autre pour un pointeur vers un ''pool'' de constantes, et quelques autres. D'autres registres servent à mémoriser le haut de la pile, à savoir le sommet de la pile et quelques opérandes situées juste en-dessous. Par exemple, le processeur picoJava II gardait les 64 opérandes au sommet de la pile dans 64 registres généraux, séparés des registres pour les pointeurs et l'état de la JVM. Une optimisation possible, envisagée sur les processeurs picoJava, était une sorte de macro-fusion sous stéroïde. L'idée était de fusionner certaines séries d'instructions Java en une seule instruction machine. Typiquement, pour faire une opération arithmétique, une machine à pile empile deux opérandes et exécute une opération, ce qui prend trois instructions machine. La spécification picoJava 1 a proposé d'envoyer directement la seconde opérande en entrée de l'ALU, ce qui fusionnait l'empilement de la seconde opérande et l'opération arithmétique. Le processeur picoJava II va plus loin et fusionne les trois instructions en une seule instruction RISC, agissant sur des registres. Mais la majorité des processeurs Java ne faisaient pas cela, car cela complexifie grandement le décodeur. Les processeurs ''bigfoot'' de DCT utilisaient un système différent pour émuler une pile avec des registres. Leur banc de registre de 64 bits était coupé en deux : les 16 premiers registres étaient utilisés pour la pile, les 48 suivants étaient utilisés pour autre chose. L'idée est que le registre 0 mémorise un emplacement vide, le registre 1 contient le sommet de la pile, le registre 2 l'opérande sous le sommet de la pile, et ainsi de suite. Les 15 premières opérandes de la pile sont donc mémorisées dans les registres. Les instructions écrivent leur résultat dans le registre adéquat, puis tous les registres sont décalés pour que le résultat passe dans le registre R1. Par exemple, les instructions PUSH chargent une opérande dans le registre 0, puis tous les registres sont décalés de un rang, pour que l'opérande chargée soit dans le registre 1. Le registre 0 devient alors le registre numéro 1, le registre 1 devient le 2, et ainsi de suite. Par contre, les opérations arithmétiques font autrement. Une instruction d'addition, par exemple, lit les opérandes dans le registre 1 et 2, puis place son résultat dans le registre 2 (les deux opérandes sont dépilées). Puis, les registres sont décalés d'un rang pour que ce registre 2 devienne le 1. Les registres ne sont en réalité pas décalés, il n'y a pas de transferts entre registres. A la place, les 16 premiers registres utilisaient une forme limitée de renommage de registres, similaire au banc de registre tournant utilisé sur les processeurs EPIC comme l'Itanium. Les numéros de registres sont renommés, décalés, à chaque fois qu'on empile ou dépile une opérande. Pour faire le renommage de registres, processeur contient un ''register stack counter'' de 4 bits, qui est incrémenté à chaque fois qu'on empile une donnée, et est décrémenté quand on dépile une donnée. Quand on veut accéder à un opérande, le numéro de registre architectural associé est traduit en numéro de registre physique, en additionnant le ''register stack counter'' (modulo 16, pour rester sur 4 bits). ==L'accélération de l'interprétation/JIT : le ''Thumb-EE'' d'ARM== Quelques processeurs ont ajouté des instructions pour faciliter le travail des interpréteurs/JIT. Par exemple, en 2005, ARM a ajouté le mode ''Thumb Execution Environment'', qui faisait cela. Il reprenait le jeu d'instruction compact ''thumb'' et ajoutait quelques instructions et modifiait le comportement d'instructions existantes. Le jeu d'instruction ''Thumb-EE'' a été ajouté sur les CPU ARM en 2005, mais a été déprécié en 2011, par manque d'utilité. Mais la tentative mérite qu'on s'attarde dessus. Les CPU de gamme M, déstinés à l'embarqué, n'ont jamais supporté ''Thumb-EE'', vu que de tels processeurs ne sont pas conçus pour exécuter du code interprété/JIT. Le mode ''Thumb-EE'' est un mode d'exécution, séparé de l'ARM normal. Ainsi, une instruction ''Thumb-EE'' et une instruction ARM peuvent avoir le même encodage en binaire, mais se comporter différemment. Le processeur est à tout moment soit en mode ''Thumb-EE'', soit en mode ARM normal, ce qui précise comment ces instructions doivent se comporter. Des instructions ont été ajoutées par ''Thumb-EE'', pour qu'il fasse son travail. L'entrée et la sortie du mode ''Thumb-EE'' se fait avec deux instructions : ENTERX et LEAVEX. Une fois en mode ''Thumb-EE'', le décodage des instructions ''thumb'' se fait avec les règles du ''Thumb-EE'', et non celles du ''thumb'' normal. Une première différence entre ''thumb'' normal et ''Thumb-EE'' est l'ajout de ce qui s'appelle un ''null check'' pour les instructions mémoire. Le ''null check'' est utilisé pour les instructions en mode d'adressage "base + indice" ou "Base + décalage", ou tout autre mode d'adressage avec un registre de base utilisés dans des calculs d'adresse. L'idée est de vérifier si l'adresse de base vaut zéro ou non. Si c'est le cas, le processeur lève une exception matérielle, qui est traitée par l'interprétation ou le compilateur JIT. Une telle situation est en effet signe d'une erreur d'adressage mémoire, qui doit être traitée par l'interpréteur. Typiquement, c'est signe qu'un pointeur n'a pas bien été initialisé, ce qui peut arriver avec du code interprété ou JIT. Les interpréteurs/JIT traitent généralement la situation en ajoutant un test avant toute instruction mémoire, pour vérifier si le pointeur accédé vaut zéro ou non. Avec ''Thumb-EE'', pas besoin d'ajouter les tests en questions : ils sont réalisés automatiquement lors de chaque instruction mémoire. : Les registres pour la pile, comme le pointeur de pile et le pointeur de ''frame'', sont aussi concernés. L'instruction CHKA vérifie que les accès à un tableau ne débordent pas en dehors du tableau en question. Par exemple, pour un tableau de 1024 éléments, elle vérifie si l'indice est compris entre 0 et 1024. Pour cela, elle vérifie si l'indice de tableau est bien dans l'intervalle adéquat et lève une exception matérielle si ce n'est pas le cas. Son utilité se comprend quand on sait que tous les accès à un tableau sont vérifiés par l'interpréteur. L'interpréteur est censé ajouter des instructions pour vérifier les indices, à savoir deux ou trois branchements. Avec CHKA, le test se fait en une seule instruction. D'autres modifications mineures de l'encodage des instructions sont aussi présentes, ainsi que des modifications pour les instructions LOAD/STORE. Précisément, dans les modes "base + indice" et "base + décalage", le décalage et l'indice subissent maintenant des décalages. L'idée est de simplifier le calcul d'adresse. De quoi économiser quelques instructions lors des calculs d'adresse, ce qui facilite le travail de l'interpréteur/JIT. Et des instructions LOAD/STORE précises voient leur comportement modifié pour faciliter la gestion de la pile de Java ou l'exécution des méthodes locales. Pour faciliter le travail de l'interpréteur, deux instructions HBP et HBLP ont été ajoutées. Elles branchent vers une fonction qui gère les exceptions logicielles (une fonctionnalité présente dans de nombreux langages, comme Java). La première effectue un branchement vers la fonction en question, la seconde sauvegarde l'adresse de retour avant de faire ce branchement. Les deux peuvent brancher vers 256 fonctions pré-déterminées, sans avoir à présenter leur adresse. ==La traduction binaire accélérée par le matériel== Plus haut, nous avons surtout parlé des compilateurs et des interpréteurs. Cependant, nous devons aussi parler de la '''traduction binaire'''. Elle traduit un programme écrit dans un code machine vers un autre code machine. Par exemple, elle traduit un programme compilé pour un CPU x86 vers un code machine ARM. En général, le système d'exploitation est généralement compilé pour le jeu d'instruction natif, mais il exécute des applications prévues pour le x86. Les applications sont traduites par le système d'exploitation, avant d'exécuter le code traduit. : Nous parlerons dans la suite de code machine source et de code machine cible, pour parler respectivement du code à traduire et du code obtenu après traduction. Idem avec d'autres termes comme architecture cible/source, ou jeu d'instruction cible/source. La traduction binaire est surtout utilisée pour des questions d'émulation ou de compatibilité. Par exemple, lorsque les Macintosh sont passés de processeurs Power PC vers des processeurs x86, le système d'exploitation Mac OS utilisait la traduction binaire pour convertir les anciennes applications Power PC vers du code x86. Les utilisateurs n'y ont vu que du feu. Le système de traduction binaire était appelé Rosetta 1. Par la suite, lors de la transition de processeurs x86 vers des processeurs Apple, Rosetta 2 a vu le jour. La traduction binaire a surtout été utilisée pour traduire du code x86 vers un autre jeu d'instruction. Il faut dire que le x86 est le jeu d'instruction dominant. De nombreuses entreprises ont eu pour ambition de briser l'hégémonie du x86 sur PC, en remplaçant le x86 par un jeu d'instruction plus performant, tout en gardant une compatibilité maximale. La traduction binaire était la seule solution pratique. Elle était le plus souvent intégralement réalisée en logiciel, comme c'était le cas sur les architectures Itanium avec le ''IA-32 Execution Layer''. ===Les généralités sur la traduction binaire assistée en matériel=== L'architecture source est presque tout le temps une architecture CISC, assez ancienne, qu'on souhaite émuler. Le choix de l'architecture cible se porte souvent sur une architecture VLIW, fort différente de l'architecture source, et ce pour obtenir une économie de matériel conséquente. Pour s'exécuter rapidement, le code traduit doit exploiter le parallélisme d'instruction, à savoir exécuter plusieurs instructions en même temps dans des unités de calcul séparées. Il est possible d'utiliser un processeur superscalaire avec exécution dans le désordre pour cela, mais au prix d'un cout important en transistors. Alors qu'en utilisant un CPU VLIW, c'est le traducteur binaire qui fait tout le travail d'extraction du parallélisme d'instruction. Et après tout, quitte à avoir un traducteur binaire, autant lui refiler le boulot d'optimisation. Compiler du code à la volée est certes assez lourd, mais qu'il existe des algorithmes efficaces pour regrouper des instructions indépendantes dans une seule instruction VLIW. Au passage, le processeur VLIW a plus de registres que le processeur source. Cela permet de faire du renommage de registres directement en logiciel. Le traducteur binaire n'hésite pas à changer les noms de registres entre instructions source et instruction VLIW cible, afin de supprimer des dépendances de données. En conséquence, cela ouvre des opportunités de parallélisme, qui sont exploitées lors du regroupement des opérations en instructions VLIW. Vous pourriez penser que le choix d'un CPU VLIW pour émuler du CISC est tout sauf optimal. Vous devez penser que la traduction binaire est d'autant plus simple que l'architecture source et cibles sont semblables. Dans les faits, ce n'est pas tellement traduire les instructions qui pose problème, mais plus la gestion du registre d'état, des exceptions matérielles, de la mémoire virtuelle, la différence entre gros-boutisme et petit-boutisme, de même que des différences d'adressage pour les périphériques. Le premier problème est la gestion des conditions, notamment en présence d'un registre d'état. En pratique, la traduction binaire s'utilise pour traduire du code CISC vers du code RISC ou VLIW. Les architectures source ont donc un registre d'état, qui est mis à jour non seulement par des instructions de test, mais aussi des instructions arithmétiques. L'architecture cible n'a elle pas de registre d'état, mais des registres à prédicats. La traduction de l'un vers l'autre est alors quelque peu compliquée. Et elle est d'autant plus compliquée que le registre d'état est la source de dépendances d'instruction implicites, qui réduisent les performances. Par exemple, si une instruction arithmétique modifie le registre d'état, cela peut impacter l'exécution d'une instruction ultérieure, qui lit ce registre d'état. Pour éliminer ces fausses dépendances, le traducteur binaire doit renommer ce registre en logiciel et détecter les dépendances utiles. Et le compilateur doit gérer les cas où il y a beaucoup de distance entre les deux, voire les cas où l'instruction dépendante n'a pas encore été analysée par le compilateur. Un autre problème est lié à la gestion des exceptions matérielles, et précisément des exceptions précises. Pour rappel, une instruction VLIW regroupe plusieurs opérations, opérations qui correspondraient à une instruction machine sur un CPU pas VLIW. En conséquence, plusieurs instructions du langage machine "source" sont regroupées en une seule instruction VLIW. Et il faut tenir compte du cas où une instruction source lève une exception. Dans le code VLIW, cela signifie qu'une opération lève une exception, et il faut annuler partiellement l'instruction VLIW associée. Et par partiellement, on veut dire que seules les opérations suivantes dans l'ordre du programme source doivent être annulées, puis ré-exécutées. Et c'est un sacré casse-tête ! Les processeurs VLIW utilisent comme solution le mécanisme d'exceptions différées des processeurs EPIC, vu il y a quelques chapitres. Pour résumer, le code est exécuté par blocs d'instruction, délimités par des branchements ou tout autre limite/barrière pertinente dans le code. Un bloc de code est compilé en deux versions : une version rapide sans exceptions matérielles, une version lente qui gère les exceptions précises. La version rapide s'exécute sans exécuter les exceptions. Cependant, les exceptions matérielles sont enregistrées, pour être prises en compte à la toute fin du bloc de code. La version rapide est exécutée en premier et elle mémorise si une exception matérielle a eu lieu. Puis une instruction vérifie si une exception a eu lieu et décide quoi faire. Si aucune exception n'a eu lieu, elle passe à la suite du programme, les résultats du bloc de code sont définitivement acceptés. Mais si une exception a eu lieu, tout est annulé. Le processeur est remis dans l'état initial, puis le code est ré-exécuté instruction par instruction de manière à gérer l'exception correctement. ===La traduction par pré-décodage=== Une première solution serait de faire la traduction binaire dans le cache d'instruction. Lors d'un défaut de cache, le code chargé depuis la RAM est traduit en code machine cible. Et c'est ce code machine cible qui est mémorisé dans le cache d'instruction et exécuté par le processeur. La technique marche sur le papier et n'est qu'une amélioration des techniques de pré-décodage vues il y a quelques chapitres. Cependant, le code obtenu est une traduction assez basique, qui n'incorpore pas d'optimisations dignes de ce nom. De plus, elle gère mal le cas où la taille du code source et cible sont potentiellement très différentes. Concrètement, elle est surtout utile pour traduire du code d'un processeur RISC vers un autre processeur RISC, dont les tailles d'instructions sont similaires. Elle ne permet pas d'émuler plusieurs jeux d'instructions différents, le cout en matériel (un circuit de pré-décodage par jeu d'instruction) serait trop important. ===Le projet DAISY d'IBM=== De très rares processeurs étaient conçus pour accélérer cette traduction binaire, afin de garder de bonnes performances. Les premiers à avoir étudié l'idée étaient IBM, avec leur projet DAISY (''Dynamically Architected Instruction Set from Yorktown''). Le projet de base était de convertir à la volée du code compilé pour des CPU Power PC, vers du code VLIW. Les chercheurs d'IBM avaient développé un algorithme de traduction dynamique efficace, ainsi qu'un processeur VLIW disposant d'optimisations spécifiques à la traduction binaire dynamique, à la volée. De nombreuses idées de ce projet ont été reprises ou re-découvertes par la société Transmetta, avec ses processeurs Crusoe et Efficieon, puis par NVIDIA avec son projet Denver. Les ingénieurs de ce projet ont étudié la possibilité d'émuler plusieurs jeux d'instructions différents sur un même processeur, notamment le s390 d'IBM et le x86. En théorie, cela demande juste d'avoir plusieurs programmes de traduction binaire : un pour le x86, un autre pour le s390, éventuellement un autre pour le Power PC. Mais cela ne s'est pas concrétisé. Mais les ingénieurs ont étudié quelle pourrait être l'architecture VLIW idéale pour ça. Par exemple, il fallait des additions 3-opérandes pour simplifier les calculs d'adresse. Une difficulté était la gestion du registre d'état, dont les bits ne sont pas mis à jour de la même manière sur le x86 et le s390 ou le Power PC. La différence entre gros-boutisme et petit-boutisme était aussi un problème, de même que des différences d'adressage pour les périphériques. Pour que la traduction binaire soit efficace, le processeur VLIW intègre diverses optimisations, comme les exceptions différées et les branchements multi-voies. La principale est la suivante : seul le code des boucles ou fonctions exécutées fréquemment est traduit en code VLIW, le reste du code est interprété. En effet, traduire du code binaire prend plus de temps qu'une simple interprétation. Pour du code qui ne sera exécutée qu'une seule fois, il est plus rapide d'utiliser l'interprétation. Par contre, pour du code exécuté beaucoup de fois, le cout de la traduction binaire est amorti, dilué sur N exécutions, compensé par le gain en temps d'exécution de ce code traduit. Au final, cela permet de ne traduire que le code qui le mérite. La détection du code fréquemment exécuté est réalisée dans le cœur VLIW. L'unité de branchement mémorise les derniers branchements rencontrés et le nombre de fois qu'ils ont été exécutés. Si ils ont été exécutés un certain nombre de fois, l'unité de branchement lève une exception matérielle, qui invoque le traducteur binaire. La routine de cette exception est le traducteur binaire proprement dit. En clair, la traduction binaire est démarrée quand le processeur détecte qu'une fonction a été exécutée plus de N fois, via une exception matérielle dédiée. La mesure du nombre d'exécution d'un branchement se fait dans l'unité de calcul dédiée aux branchements, avec l'aide d'une sorte de ''branch adress buffer'' modifié. Pour rappel, le ''branch adress buffer'' mémorise le ''Program counter'' de chaque branchement récemment rencontré. Ici, chaque entrée du ''branch adress buffer'' est associée à un compteur incrémenté à chaque exécution du branchement. ===Les processeurs Crusoe et Efficieon de Transmetta=== Les processeurs Crusoe Et Efficeron sont deux processeurs VLIW produits par la société Transmetta, une société californienne rachetée par NVIDIA. Ils étaient conçus pour exécuter spécifiquement des programmes x86, système d'exploitation inclus. L'idée derrière ce projet était d'exécuter du code x86 sans que les applications, ni même le système d'exploitation et le BIOS soient au courant ! Les processeurs Transmetta étaient en réalité conçus pour exécuter un programme unique, le ''Code Morphing Software'' (CMS), qui traduisait le code x86 en code VLIW. Transmetta ne voulait pas que son processeur VLIW soit exploité directement, sans le CMS. Il n'avait pas rendu public de compilateur pour traduire du C vers du code VLIW, il n'avait pas donné la documentation du jeu d'instruction VLIW. Des efforts de rétro-ingénieurie, documentés sur le site realworldtech, ont cependant permis de comprendre comment étaient encodées les instructions du processeur Crusoe. Comme pour le projet Daisy, le ''Code Morphing Software'' utilisait à la fois interprétation et traduction binaire, selon les besoins. Les instructions sont interprétées lors de leurs premières exécutions, mais le CMS bascule sur de la traduction en code VLIW après un certain nombre d'exécution. Ainsi, les boucles souvent exécutées sont traduites en code VLIW, alors que le reste du code est interprété. Le choix du code à traduire est le fait du CMS, il utilise des heuristiques complexes pour, qui ne sont pas connues dans le détail. Le ''Code Morphing Software'' est mémorisé dans une EEPROM, ce qui en fait un ''firmware'' situé en dessous du BIOS. Le CMS démarre ensuite le BIOS, qui lui-même démarre le système d'exploitation, qui lui-même démarre les pilotes de périphériques et les programmes. Le CMS se réserve les 16 premiers mébioctets de l'espace d'adressage. L'EEPROM du CMS est mappée dedans, mais n'en utilise que la moitié. Le reste est utilisé comme cache, pour mémoriser le code VLIW traduit par le CMS. Les deux processeurs intégraient aussi deux mémoires SRAM, utilisées par le CMS, appelées la ''local program memory'' (LPM) et la ''local data memory'' (LDM). La première contient du code qui gére les interruptions, la mémoire virtuelle, les exceptions matérielles, les problèmes d'alignement mémoire, et quelques fonctions très fréquemment utilisées par le CMS. La seconde est de la mémoire RAM utilisée par ces fonctions de la ''local program memory''. Les mémoires caches L1 et L2 sont séparées de ces deux mémoires. Le processeur Crusoe et Efficieon étaient des processeurs VLIW très simples. Ils n'avaient même pas de MMU, ni de fonctionnalités importantes sur les CPU x86. Pour Crusoe, les instructions VLIW étaient encodées sur 64 ou 128 bits, et regroupaient entre 2 à 4 opérations. Les instructions LOAD/STORE ne supportaient pas d'adressage indicé, juste de l'adressage indirect à registre. Crusoe avait 5 unités de calcul : deux ALU entières, une unité LOAD/STORE, une unité de branchement, une FPU. Efficieon doublait le nombre d'ALU entière et d'unité LOAD/STORE, les instructions VLIW passaient à 256 bits. Le processeur Crusoe contient 160 registres, dont 64 registres généraux, 32 registres flottants. Les registres généraux font 32 bits, ce qui est cohérent avec le fait que les CPU x86 de l'époque étaient des processeurs 32 bits. Quant aux registres flottants, ils faisaient 80 bits, ce qui colle avec la taille des registres flottants de la FPU x87 utilisée à l'époque. Sur les 64 registres généraux, seuls 48 étaient réellement utilisable pour mémoriser des opérandes. Une partie des registres généraux étaient utilisés pour la gestion de la pile, d'autres pour mémoriser l'état du CPU x86 émulé, un registre était un registre zéro non accesible en écriture. La gestion des exceptions est optimisée avec un système d'exceptions différées, le même que celui décrit plus haut. Le processeur utilise un système similaire, pour effectuer des lectures anticipées, qui a été expliqué dans le chapitre sur les processeurs VLIW/EPIC. Sauf qu'il s'agit là d'une optimisation pour gagner en performances en général, pas une aide utile pour gérer des exceptions précises ou toute autre subtilité de la traduction binaire. Pour gérer les exceptions différées et les lectures anticipées, le processeur mémorise l'état du processeur avant de démarrer un bloc de code. Pour cela, le processeur contient des ''shadow registers'', 48 registres entiers et 16 registres flottants. Vous remarquerez qu'il y a autant de registres entiers que de ''shadow registers'' entiers. Les registres entiers/flottants sont mémorisés dans les ''shadow registers'' avant d'exécuter un bloc de code, afin que le processeur puisse revenir à l'état de base. Une autre fonctionnalité liée est le ''gated store buffer''. Le principe est d'éviter toute écriture en mémoire RAM, tant que le code VLIW n'a pas émis d'instruction ''commit''. L'idée est que les écritures sont conservées dans la ''store queue'' de l'unité LOAD/STORE. Pour rappel, la ''store queue'' met en attente les écritures soit tant que les écritures ne sont pas terminées, soit tant que la RAM est occupée. L'idée est que les écritures sont accumulées dans la ''store queue'' tant que l'instruction ''commit'' n'a pas eu lieu. Le ''store queue'' est "déversé dans la RAM" seulement quand l'instruction ''commit'' s'exécute. ===Les CPU NVIDIA Denver=== Nvidia a tenté quelque chose de similaire avec son projet Denver. NVIDIA a racheté l'entreprise Transmetta, ses brevets, et a tenté de refaire la même chose. Le projet initial était de traduire du code x86 vers un jeu d'instruction VLIW propriétaire, comme l'a fait Transmetta. Mais NVIDIA n'a pas réussi à acquérir la licence du jeu d'instruction x86 et s'est rabattu sur le jeu d'instruction ARM. Le processeur né de ce projet est le Tegra K1-64 CPU. Il s'agit d'un processeur composé d'un cœur VLIW, couplé à deux décodeurs d'instructions ARM. Les deux décodeurs ARM traduisent une paire d'instructions ARM consécutives en instructions VLIW, pour les exécuter sur le cœur VLIW. Le CPU peut fonctionner selon deux modes : ARM et VLIW. En somme, le processeur supporte deux jeux d'instruction : le jeu d'instruction ARM, et un jeu d'instruction VLIW. En mode ARM, les instructions ARM sont chargées depuis le cache, traduites en VLIW par les décodeurs ARM, puis exécutées par le cœur VLIW. En mode optimisé, le CPU exécute des instructions VLIW chargées depuis le cache d'instruction, les décodeurs ne sont pas utilisés. : Le CPU supporte en réalité deux jeux d'instruction ARM : ARM8 et 7. Mais ils correspondent au même mode pour le CPU. Un point important est que le processeur peut changer très rapidement de mode, en à peine quelques cycles d'horloges. On n'est clairement pas dans le cas des CPU x86, qui mettent des plombes pour passer du mode 32 à 64 bits et inversement. La commutation est tellement rapide qu'on peut considérer que le processeur supporte deux jeux d'instruction simultanément. Et le support simultané de deux jeux d'instruction facilite l'implémentation de la traduction binaire. Encore une fois, inutile de traduire du code qui ne sera exécutée qu'une seule fois, mieux vaut l'exécuter directement dans le mode ARM. Surtout que le processeur supporte un mode ARM qui permet d'exécuter du code ARM sans perte de performance. Par contre, le code fréquemment utilisé, notamment dans des boucles critiques, est traduit en VLIW. Au final, les performances sont optimisées, en limitant le travail du logiciel traducteur binaire. La détection du code fréquemment exécuté se fait différemment que pour les processeurs Transmetta et Daisy. L'unité de branchement ne détecte pas directement le code beaucoup exécuté, même si elle a un rôle à jouer. A la place, la détection est réalisée en logiciel, par un ''thread'' dédié, qui s'exécute sur un cœur séparé. Il détecte le code fréquemment utilisé en regardent les branchements exécutés récemment. Pour cela, le processeur mémorise l'historique des branchements pris récemment et copie cette historique dans une mémoire tampon dédiée, partagée entre tous les cœurs. Vu qu'elle est partagée, le ''thread'' de détection a accès à l'historique sans pertes de performances, seule la copie de l'historique dans ce tampon a un cout en performance, pas son partage. Le code VLIW obtenu après traduction binaire est mémorisé dans la mémoire RAM, dans une portion spécialement réservée pour. Elle fait 128 mébioctets, et est appelée le '''cache d'optimisation''' par NVIDIA. Le cache d'optimisation est protégé en écriture et seul le traducteur binaire peut écrire dedans. Au passage, le traducteur binaire est du code VLIW, ce qui fait qu'il est placé dans le cache d'optimisation. Le processeur détecte automatiquement quand une fonction pour laquelle le code compilé équivalent est disponible. Pour cela, l'unité de chargement contient une table de correspondance entre l'adresse de la fonction ARM, et l'adresse de son équivalent compilé. Quand une fonction est appelée, l'unité de chargement regarde l'adresse de destination du branchement. Si l'adresse est dans cette table, elle récupère l'adresse de la fonction compilée et branche vers celle-ci. Le processeur dispose d'un cache d'instruction de 128 kibioctets, ce qui est très important, mais nécessaire vu la taille des instructions VLIW. Le coeur VLIW dispose de ses propres décodeurs, d'un ''scoreboard'', de 7 unités de calcul et de registres. Pour les unités de calcul, il a deux FPU, deux ALU, une unité de branchement, et deux unités LOAD-STORE faisant aussi ALU. Un point important est que le CPU dispose de 64 registres entiers et de 64 registres flottants. C'est deux fois plus que supporte le jeu d'instruction ARM. En mode ARM, seule la moitié des registres est utilisée. Mais en mode optimisé, le traducteur binaire utilise bien les 64 registres, grâce à une sorte de renommage de registres logicielle. Pour simplifier la traduction binaire, le processeur supporte les techniques vues précédemment. Le processeur intègre un ''gated store buffer'' similaire à celui des processeurs Transmetta. Les techniques d'exceptions différées sont aussi supportées, comme sur les processeurs Transmetta. Il a aussi une unité de préchargement décrite par NVIDIA comme agressive, avec support du préchargement de type anticipé (''runahead''). ===L'émulation de la mémoire virtuelle=== Pour finir, il faut parler de la mémoire virtuelle avec la traduction binaire. Sur le processeur source, le processeur gère à la fois des adresses physiques et virtuelles et sa MMU fait la conversion entre les deux. Sur le processeur VLIW, la MMU est simulée par le programme de traduction binaire, partiellement ou totalement. L'implémentation exacte varie suivant que l'on parle du projet Daisy, des processeurs Transmetta ou de Denver. Sur les processeurs Transmetta, il n'y a pas de MMU ni de mémoire virtuelle. A la place, le programme de traduction binaire émule la mémoire virtuelle du processeur source. Le processeur VLIW ne gère que des adresses physiques, rien d'autre. L'espace d'adressage physique du processeur VLIW a la même taille que celui du processeur émulé, ici des processeurs x86. Il est vraisemblable que les adresses physiques utilisées par le processeur x86 sont les mêmes que celles du processeur VLIW. Sur le projet Daisy, le processeur VLIW gère la mémoire virtuelle via pagination, comme les processeurs émulés. Ce qui fait qu'il gère un espace d'adressage virtuel et un espace d'adressage physique. Pour éviter toute confusion, nous parlerons d'adresse physique/virtuelle VLIW pour les adresses physiques/virtuelles du processeur VLIW, d'adresse virtuelle/physique source pour celle du jeu d'instruction traduit, à savoir du Power PC ou du s390, éventuellement de l'x86. Il faut alors faire le lien entre adresses physiques source et adresses virtuelles VLIW. Pour cela, rien de plus simple : il y a correspondance parfaite. L'adresse physique source numéro N correspond à l'adresse virtuelle VLIW numéro N. Cependant, cela signifie que tout l'espace d'adressage virtuel VLIW serait utilisé par le code à traduire. En réalité, il faut ajouter de la place pour le code traduit et le programme de traduction binaire. La conséquence est que l'espace d'adressage virtuel VLIW est plus large que l'espace d'adressage source. L'espace d'adressage virtuel VLIW est découpé en trois sections : une section pour le code Power PC à traduire, une autre pour le code traduit en VLIW, et une dernière réservée au traducteur binaire. Le programme de traduction est placé dans une ROM mappée en mémoire dans la seconde section. Le reste de la seconde section est réservé à une mémoire RAM utilisée par le programme de traduction. [[File:Mémoire virtuelle sur le projet Daisy.png|centre|vignette|upright=1.5|Mémoire virtuelle sur le projet Daisy]] Il faut noter que chaque page physique source, une fois traduite, correspond à N pages virtuelles VLIW. La raison est que le code traduit prend plus de place que le code machine originel. En conséquence, on doit utiliser une page finale plus grande. Pour le reste, des pages contiguës en mémoire physique source sont elles aussi contiguës en mémoire virtuelle VLIW. Le calcul d'adresse est donc simple : l'adresse physique source est multipliée par N, puis on ajoute l'adresse de base à laquelle commence la section pour le code traduit. ==Les processeurs à jeu d'instruction reconfigurable== Nous venons de voir comment accélérer en matériel la traduction binaire. Mais il existe une alternative qui permet de se passer de traduction binaire, complétement. L'idée est d'avoir un processeur avec un '''jeu d'instruction reconfigurable'''. Le nom est assez parlent : on peut configurer le processeur de manière à supporter le jeu d'instruction de son choix ! Par exemple, on pouvait reconfigurer un CPU de manière à ce qu'il supporte le jeu d'instruction x86, ou qu'il exécute du ''bytecode'' Java, ou du code ARMv8, etc. De tels processeurs sont encore plus rare que ceux vu avant dans ce chapitre, mais ils ont existé ! Les processeurs de ce type avaient un ''microcode réinscriptible'', à savoir que c'était des processeurs microcodés pour lesquels on pouvait modifier le microcode. L'idée était qu'en changeant le microcode, on changeait le jeu d'instruction du processeur. Pour cela, le microcode était associé à une mémoire SRAM dans laquelle le microcode était copié au démarrage du CPU. Il y avait donc une micro-ROM et une micro-RAM. Les instructions étaient décodées en utilisant le microcode dans la micro-SRAM, la micro-ROM ne servait qu'au démarrage. En modifiant le contenu de la micro-SRAM, on pouvait altérer le microcode, voire le remplacer totalement. ===Le Burroughs B1700 : introduction=== Un exemple est celui du Burroughs B1700, et il est intéressant d'étudier en détail son architecture. Son architecture est décrite dans le livre "Interpreting Machines : Architecture and Programming of the Bl700/Bl800 Series", écrit par Elliott I. Organick et James A. Hinds. Une partie de ce qui vfa suivre est un très court résumé de ce livre. Il disposait de 4 registres généraux, nommés X, Y, T et L, tous de 24 bits. les deux premiers étaient reliés à une ALU de 24 bits capable d'effectuer des opérations arithmétiques basiques. Fait étonnant, l'ALU faisait tous les calculs en même temps et fournissait ses résultats dans 7 registres : un registre SUM pour l'addition, un registre DIFFERENCE pour la soustraction, et un registre par opération bit à bit. Les registres pour les opérations bit à bit sont : CMPX et CMPY pour l'opération NOT sur chaque opérande, XANY pour le ET bit à bit, XEOY pour le XOR, XORY pour le OU logique A cela, il fallait ajouter deux registres de "résultat" MSKX et MSKY. Ils fournissaient un masque dépendant de la taille du résultat en bits. Cela permettait de gérer des données de 8 ou 16 bits, alors que l'ALU faisait 24 bits. Et cela n'était pas limité à 16 ou 8 bits, mais n'importe quelle taille inférieure ou égale à 24 bits était supportée, même des tailles non-conventionnelles comme des résultats codés sur 3 bits, 9 bits, 12 bits, etc. L'ALU fournissait 12 conditions, qui alimentait un registre d'état. Le registre d'état était segmenté en trois registres appelés XYST, XYCN et BICN. Le processeur contenait de nombreux autres registres de 4 bits qu'on détaillera plus tard, qui au nombre de 27 ! Et le processeur disposait d'une ALU 4 bits pour manipuler ces registres de 4 bits ! Elle est capable de faire des MOV entre registres de 4 bits, des opérations bit à bit, et peut aussi tester si un bit vaut 0 ou 1. La dernière possibilité est très utile pour implémenter les branchements. Pour communiquer avec les entrées-sorties, il disposait de deux registres CMND et DATA : CMND pour envoyer une commande à une entrée-sortie, DATA pour lire ou écrire une donnée. Pour la mémoire, il avait deux registres séparés : READ et WRITE. READ contient la donnée lue lors d'une lecture,; WRITE est pour une donnée à écrire lors d’une écriture. Il s'agit de registres d’interfaçage mémoire, qui ont été promus au rang de registres architecturaux. Les deux registres sont gérés par le microcode, qui est le langage principal de la machine. Les instructions machines configurées font sans, mais le microcode doit y avoir accès facilement. Et vu que le microcode est le langage principal de ce processeur, ces deux registres deviennent architecturaux. ===Une architecture bit-adressable=== Si avoir un microcode réinscriptible est suffisant, quelques choix de ''design'' facilitent l'émulation de jeux d'instructions variés. Par exemple, le CPU Burroughs B1700 avait ajouté une fonctionnalité très importante : il était bit-adressable. En clair, il pouvait adresser la mémoire bit par bit, si nécessaire. L'avantage est que cela permettait d'émuler des architectures dont la taille des registres/''byte''/mots était très différente. Un autre avantage est que cela facilitait la gestion des instructions de taille variable. Détaillons rapidement le premier avantage. Le processeur était un CPU 24 bits, mais il pouvait émuler des processeurs 16 bits, 8 bits, 4 bits, 1 bit, 9 bits, ou toute autre valeur. Pour cela, le processeur lisait des mots de 24 bits et utilisant un ''barrel shifter'' pour sélectionner les bits adéquats. Par exemple, pour un accès de 16 bits, la donnée lue en mémoire était masquée de manière à ne garder que les 16 bits de poids faible. Le ''barrel shifter''/circuit de masquage était placé directement avant le bus mémoire, sur le trajet des données/instructions. Le ''barrel shifter'' était commandé par un registre ''Field Unit'', qui précisait quelle était la taille des données à charger. ===La gestion de la taille des instructions=== Les instructions à décoder n'ont pas de longueur fixe, à savoir qu'elles peuvent faire 16 bit avec tel jeu d'instruction, 8 bits avec un autre, 32 bits sur un autre, etc. Elles peuvent aussi être de longueur fixe ou variable ! Le B1700 intégrait donc un support des instructions de longueur variable, qui était "détourné" pour gérer le fait que des jeux d'instruction différents ont des instructions de taille différentes. Notons que vu que l'architecture est bit-adressable, les contraintes d'alignement des instructions disparaissent, ce qui a des avantages qu'on va détailler dans ce qui suit ! L'implémentation des instructions de taille variable ajoutait un registre avant le registre d'instruction. Le registre d'instruction proprement dit était un registre M de 24 bits. Il était précédé par un registre T, dans lequel l'instruction était assemblée, en combinant plusieurs morceaux. Le registre T est relié à un ''barrel shifter'', différent au précédent, qui permettait de masquer, décaler et extraire une donnée de ce registre. Si l'instruction est codée est codée sur moins de 24 bits, le registre T se contente de masquer les bits en trop. Si l'instruction chargée dépasser 24 bits, le chargement se faisait en plusieurs passes, avec plusieurs lectures en mémoire. Chaque passe découpait l'instruction pour extraire des '''champs''' : l'opcode, les opérandes, les adresses absolues, les constantes immédiates, les numéros de registre, etc. Et ils étaient copiés dans des registres interne au processeur. Par exemple, il servait à extraire l'opcode de l'instruction. Ou encore, pour une instruction de branchement direct, il pouvait extraire l'adresse à laquelle brancher de l'instruction. Le chargement de l'instruction se faisait alors champ par champ. Notons que vu que l'architecture est bit-adressable, les contraintes d'alignement des instructions disparaissent ! Quand on charge une instruction, peut importe qu'elle soit ou non alignée sur 24 bits, elle sera placée dans les bits de poids faible, il suffira de masquer les bits de poids fort inutiles. Par exemple, si on charge une instruction de 16 bits, les 16 bits seront copiés dans les 16 bits de poids faible du registre T, il suffira de masquer l'octet de poids fort. Pas besoin de faire de décalages pour gérer des instructions mal alignées... Pour charger une instruction, il faut connaitre plusieurs informations : l'adresse de son premier bit, sa taille. Pour cela, le B1700 contient deux registres, nommés ''Field Adress'' (FA) et ''Field Length'' (FL), qui indiquent l'adresse du premier bit et la taille de l'instruction à charger. Le premier fait 24 bits, ce qui permet de gérer des adresses de 24 bits. N'oubliez pas que le processeur est bit-adressable, ce qui permet d'adresser 16 Mébi-bits, soit 2 mébioctets. Le registre pour la taille fait lui 16 bits, ce qui permet de gérer des instructions de 65536 bits, soit 8192 octets ! Leur nom commence par ''field'', car le processeur peut gérer des instructions de taille variable et/ou plus longues que 24 bits. Dans ce cas, le chargement de l'instruction se fait champ par champ. Pour donner un exemple, prenons une instruction composée de 20 champs de 12 bits chacun. Les champs sont chargés un par un dans le registre T. Le registre FA est alors initialisé avec l'adresse de l'instruction, il est incrémenté de 12 à chaque cycle. Le registre FL est initialisé avec la taille de l'instruction, soit 20 * 12 = 240 bits, et il est décrémenté de 16 à chaque cycle. Précisons cependant que la taille d'un champ peut être changée d'un cycle à l'autre. Par exemple, le processeur peut charger un opcode de 8 bits, l'interpréter, détecter que l'instruction demande ensuite de charger une adresse de 16 bits. Dans ce cas, le registre FL est immédiatement altéré pour charger 16 bits, au lieu des 8 bits de l'opcode immédiatement précédent. ==Les jeux d'instructions dédiés à un langage de programmation== De rares processeurs sont conçus pour un langage de programmation en particulier. On appelle ces processeurs, conçus pour des besoins particuliers, des '''processeurs dédiés'''. Par exemple, les fameux ''Burrough E-mode'' B5000/B6000/B7000 étaient spécialement conçus pour exécuter de l'ALGOL-60. Leurs cousins B2000/B3000/B4000 étaient eux conçus pour le COBOL. Des langages fonctionnels ont aussi eu droit à leurs processeurs dédiés. Le prolog en est un bel exemple, avec les superordinateurs de 5ème génération qui lui étaient dédié. On peut aussi citer les machines LISP, dédiés au langage LISP, qui datent des années 1970. Elles étaient capables d’exécuter certaines fonctions de base du langage directement dans leurs circuits : elles possédaient notamment un ''garbage collector'' câblé dans ses circuits ainsi que des instructions machines supportant un typage déterminé à l’exécution. Les processeurs dédiés ont eu leur heure de gloire au début de l'informatique, à une époque où les langages de haut niveau venaient d'être inventés. À cette époque, les compilateurs n'étaient pas performants et ne savaient pas bien optimiser le code machine. Il était alors rationnel, pour l'époque, de rapprocher le code machine cible et le langage de programmation de haut niveau. De nombreuses architectures dédiés ont ainsi été inventées, avant que les concepteurs se rendent compte des défauts de cette approche. Les défauts en question ne sont pas nombreux, mais assez simples à comprendre. Premièrement, elles sont très rapides pour un langage de programmation en particulier, mais sont assez mauvaises pour les autres, d'où un problème de "compatibilité". Ajoutons à cela que les langages de programmation peuvent évoluer, devenir de moins en moins populaires/utilisés, ce qui rend la création d'architectures généralistes plus pertinente. Enfin, les architectures dédiées sont évidemment des processeurs CISC, pour implémenter les nombreuses fonctionnalités des langages évolués. Et les défauts des CISC sont assez rédhibitoires à l'heure actuelle. À l'heure actuelle, les algorithmes des compilateurs se sont améliorés et savent nettement mieux utiliser le matériel. Ils produisent du code machine efficace, ce qui rend les architecture dédiées bien moins intéressantes. Si on ajoute les défauts de ces architectures dédiées, par étonnant que les architectures dédiées aient presque disparues. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'accélération matérielle de la virtualisation | prevText=L'accélération matérielle de la virtualisation | next=Le matériel réseau | nextText=Le matériel réseau }} </noinclude> gurfi0zhlo9mmc39lmt7nx2mvlxzkhu 764015 764014 2026-04-19T16:20:31Z Mewtow 31375 /* Le Burroughs B1700 : introduction */ 764015 wikitext text/x-wiki De nos jours, la majorité des programmeurs programment dans des langages de haut niveau. Il est très rare pour eux d'avoir à utiliser de l'assembleur, et encore moins un langage machine. Les programmes écrits dans un langage de haut niveau sont traduits en langage machine par un logiciel appelé le compilateur. [[File:Compilation.PNG|centre|vignette|upright=2.5|Compilateur : principe.]] Il se trouve que quelques rares processeurs sont conçus pour faciliter le travail du compilateur. Et ce sont ces processeurs que nous allons voir dans ce qui suit. Ils représentent une catégorie de jeux d'instruction à part, qui n'a pas vraiment de nom. ==La compilation et l’interprétation== Un compilateur traduit donc du code source, écrit dans un langage de haut niveau, vers du langage machine. La traduction est rarement directe. En général, le compilateur traduit le code source en assembleur, qui est lui-même traduit en langage machine. L'assembleur est une représentation textuelle du langage machine alors que le code machine est du binaire exécutable par le processeur. La traduction se fait donc en deux étapes, la '''compilation''' proprement dite et l''''assemblage''', réalisées respectivement par un compilateur et un assembleur. Il faut parfois rajouter une troisième phase d'édition des liens, que nous passons volontairement sous silence. {| |[[File:Code C.png|vignette|200px|Le code source, ici en C.]] |[[File:Assem.png|vignette|250px|Le code assembleur.]] |[[File:Binary file - hello world (C programming).png|vignette|150px|Le code machine.]] |} ===L'interprétation et la compilation à la volée=== Une alternative à la compilation est l''''interprétation''', qui transforme le code source en code machine à la volée. Avec l'interprétation, le code source est passé à un logiciel appelé l''''interpréteur''', qui exécute le code source ligne de code par ligne de code. Une ligne de code est traduite en code machine, qui est exécutée, puis l'interpréteur passe à la ligne suivante. [[File:Kaantotulkkaus.png|centre|vignette|upright=3|Compilation versus interprétation.]] Un défaut de l'interprétation est son cout en performance, qui est assez important. Aussi, l'interprétation stricte a évolué vers un hybride entre interprétation et compilation. L'idée est qu'une partie peu importante du code source est interprétée, alors que le code important est compilé juste avant d'être exécuté. Pour le dire autrement, le code source est partiellement compilé à la volée, juste avant son exécution. Aussi, on parle de '''compilation à la volée'''. Le terme en anglais est ''Just In Time compilation'', abrévié en JIT. En soi, le JIT est associé à de l'interprétation, les JIT sont en réalité des hybrides interpréteurs-compilateurs. Un point important est que compiler du code à la volée est assez lourd, cela a un cout en performance. Par contre, le code compilé s'exécute plus vite que du code interprété. Si on exécute le code compilé une seule fois, le cout de la compilation l'emporte sur le gain à l'exécution. Mais le code compilé est réutilisé autant de fois que nécessaire. Le gain à l'exécution est donc multiplié par le nombre d'exécutions, alors que le cout de la compilation est répartit sur plusieurs exécutions, il est amorti. Les portions du code sont choisies de manière à avoir un gain en performance maximal. L'idée générale est que le code exécuté très souvent est compilé, alors que du code exécuté pas souvent est interprété. Typiquement, les boucles les plus souvent exécutées sont compilées, le code exécuté une seule fois est interprété. ===Les langages intermédiaires=== Les compilateurs modernes passent par un '''langage intermédiaire''' pour faire la transformation en code machine. Le compilateur traduit le langage de haut niveau en langage intermédiaire, puis traduit le langage intermédiaire en code machine cible. Le langage intermédiaire est parfois appelé le '''''bytecode''''', et ce terme recouvre aussi le code écrit avec du ''bytecode''. L'interprétation aussi peut passer par l'intermédiaire d'un un ''bytecode'', ce n'est pas limité aux compilateurs proprement dit. {| |[[File:Intermediate representation scheme.png|centre|vignette|upright=1.5|Représentation intermédiaire d'un compilateur.]] |[[File:Code interpreter scheme.png|centre|vignette|upright=1.5|Fonctionnement d'un interpréteur.]] |} Faire ainsi a de nombreux avantages, le principal étant d'avoir un compilateur capable de traduire un langage de haut niveau vers plusieurs jeux d'instructions différents. Cela permet d'avoir, par exemple, un compilateur qui traduit du C soit en code machine x86, soit en code machine pour un CPU ARM, soit pour un CPU POWERPC, etc. [[File:Compiler design.svg|centre|vignette|upright=3|Compiler design]] Pour l'interprétation, l'usage d'un ''bytecode'' a d'autres avantages. Pour l'exploiter, il faut distribuer non pas le code source, mais le ''bytecode''. En clair, l'interpréteur ne prend pas en entrée le code source, mais du ''bytecode'' déjà compilé. La traduction est alors beaucoup plus simple, car le code source a déjà été partiellement compilé, par la traduction en ''bytecode''. Interpréter du code source est en effet assez compliqué : il faut effectuer des étapes d'analyse lexicale, sémantique, et bien d'autres. Avec du ''bytecode'', ces étapes ont été réalisées lors de la compilation du ''bytecode'', l'interpréteur a alors peut de choses à faire. L'interprétation du ''bytecode'' se fait instruction par instruction, au niveau du ''bytecode''. Une instruction du ''bytecode'' est traduite en une instruction machine équivalente, qui est exécutée. Si l'instruction est un peu complexe, il exécute une fonction/procédure qui fait la même chose. Pour résumer, le ''bytecode'' est ensuite traduit à la volée et exécuté instruction par instruction par un logiciel appelé l'interpréteur. [[File:Control table.png|centre|vignette|upright=2|Control table]] Le langage intermédiaire peut être vu comme l'assembleur d'un processeur, n'existe pas forcément dans la réalité, mais dont le jeu d'instruction est décrit en détail. Le processeur en question est appelé une '''machine abstraite''', ou encore une ''machine virtuelle''. Nous utiliserons le terme de machine abstraite dans ce qui suit. La machine abstraite n'est pas la même suivant que l'on cible la compilation ou l'interprétation/JIT. Pour l'interprétation/JIT, la machine abstraite est souvent une machine à pile, car cela simplifie grandement la traduction en code machine final. L'interprétation demande que la traduction du ''bytecode'' en code machine soit la plus simple possible. Et cela demande de prendre en compte pas mal de détails. Par exemple, les différents jeux d'instruction existants n'ont pas le même nombre de registres, ce qui pose problème lors de la traduction du ''bytecode''. Lors de la transformation en code machine, un algorithme d'allocation de registres se débrouille pour traduire le code intermédiaire en code qui utilise un nombre limité de registres. Pour améliorer cette allocation de registres, il y a deux solutions niveau ''bytecode'' : soit utiliser un nombre illimité de registres, soit utiliser une machine à pile. Pour les machines à pile, il existe un algorithme simple et rapide pour traduire un code écrit pour une machine à pile en un code écrit pour un processeur avec des registres, qui se débrouille pas trop mal pour allouer efficacement les registres. C'est un avantage assez important pour les langages interprétés. ==Les processeurs qui exécutent du bytecode== Le ''bytecode'' est un code machine, ce qui signifie qu'il peut en théorie s'exécuter sur un processeur qui implémente le jeu d’instruction associé. Si le jeu d'instruction d'un ''bytecode'' est souvent une description censée être fictive, elle n'en reste pas moins un jeu d'instruction et des caractéristiques précises, qu'on peut l'implémenter en matériel ! Néanmoins, tous les ''bytecode'' ne sont pas égaux de ce point de vue. Certains sont faciles à implémenter en matériel, d'autres non. Les ''bytecodes'' implémentés en matériel sont ceux dont les machines abstraites sont des machines à pile. Un premier exemple est celui des processeurs Pascal MicroEngine, qui exécutaient directement le bytecode du langage Pascal, le fameux UCSD P-code. Un second exemple est la '''machine SECD''', qui sert de langage intermédiaire pour certains compilateurs de langages fonctionnels. Elle a été implémentée en matériel par plusieurs équipes, notamment par les chercheurs de l'université de Calgary en 1989. Dans le même genre, quelques processeurs simples étaient capables d’exécuter directement le ''bytecode'' du langage FORTH. Le FORTH, un des premiers langages à pile de haut niveau, possède de nombreuses implémentations hardware et est un des rares langages de haut niveau dont le ''bytecode'' a été utilisé comme langage machine sur certains processeurs. Par exemple, on peut citer le processeur FC16, capable d’exécuter nativement du ''bytecode'' FORTH. ===Les processeurs Java=== Le cas le plus impressionnant est celui de la machine virtuelle Java, qui est un design de processeur comme un autre. En temps normal, le ''bytecode'' Java est compilé ou interprété, mais certains processeurs exécutaient du ''bytecode'' Java directement, sans interprétation ni compilation. Ils sont appelés des '''processeurs Java'''. On peut citer les processeurs ARM disposant de l'extension Jazelle, du JEMCore et du aJ-100 de aJile Systems, le picoJava II de Sun, et quelques autres. Il y a aussi eu quelques projets de recherche ou processeurs open source, comme Komodo, jamuth, le ''Java Optimized Processor'' et quelques autres projets du même genre. L'implémentation de la machine virtuelle Java n'était cependant pas complète. Quelques instructions complexes n'étaient pas gérées par le processeur et devaient être émulées en logiciel. Mais la grosse majorité des instructions l'était. Les instructions non-supportées étaient émulées avec la méthode ''trap and emulate'', à savoir qu'un opcode inconnu déclenchait une exception matérielle ''opcode invalide'', dont la routine pouvait émuler les instructions qui doivent l'être. Le processeur aJ-100 de aJile est capable d'exécuter la quasi-totalité des instructions de la JVM Java, seules deux instructions faisant exception. Les '''accélérateurs Java''' sont une solution intermédiaire, qui permet de traduire à la volée du ''bytecode'' Java en instructions RISC, exécutées sur un autre processeur. Il s'agit formellement de traduction binaire, mais je préfère en parler ici. L'idée est de combiner un processeur RISC avec un coprocesseur Java. Le coprocesseur lit le ''bytecode'' Java et le traduit en instructions RISC, ces dernières étant exécutées sur le processeur RISC. Le JA108 de Nozomi était un coprocesseur de ce genre, au même titre que le coprocesseur JSTAR de JEDI. L'extension Jazelle de certains CPU ARM fonctionnait vraisemblablement sur un principe similaire, sauf que le coprocesseur était intégré dans le processeur ARM, entre le cache d'instruction et le décodeur d'instructions ARM. L'intérêt des processeurs Java n'est pas qu'une question de performance. Certes, ils permettent d’exécuter plus vite les programmes compilés en ''bytecode'', comme des programmes Java pour la JVM Java. Mais l'intérêt est aussi de faire des économies de mémoire RAM et ROM. Je rappelle que la machine virtuelle Java est une machine à pile, ce qui fait que sa densité de code est excellente. Les programmes en ''bytecode'' Java sont donc très petits, et prennent peu de place en mémoire ROM. De plus, utiliser un processeur Java permet de se passer d'interpréteur ou de JIT Java, ce qui économise encore plus de mémoire. ===Transformer un processeur RISC en machine à pile=== La plupart des processeurs Java utilisent en interne une architecture à registre généraux, qui émule une machine à registre en microcode. Quelques registres sont réservés pour l'état de la machine virtuelle, avec au minimum un registre réservé pour le pointeur de pile, un autre pour un pointeur vers un ''pool'' de constantes, et quelques autres. D'autres registres servent à mémoriser le haut de la pile, à savoir le sommet de la pile et quelques opérandes situées juste en-dessous. Par exemple, le processeur picoJava II gardait les 64 opérandes au sommet de la pile dans 64 registres généraux, séparés des registres pour les pointeurs et l'état de la JVM. Une optimisation possible, envisagée sur les processeurs picoJava, était une sorte de macro-fusion sous stéroïde. L'idée était de fusionner certaines séries d'instructions Java en une seule instruction machine. Typiquement, pour faire une opération arithmétique, une machine à pile empile deux opérandes et exécute une opération, ce qui prend trois instructions machine. La spécification picoJava 1 a proposé d'envoyer directement la seconde opérande en entrée de l'ALU, ce qui fusionnait l'empilement de la seconde opérande et l'opération arithmétique. Le processeur picoJava II va plus loin et fusionne les trois instructions en une seule instruction RISC, agissant sur des registres. Mais la majorité des processeurs Java ne faisaient pas cela, car cela complexifie grandement le décodeur. Les processeurs ''bigfoot'' de DCT utilisaient un système différent pour émuler une pile avec des registres. Leur banc de registre de 64 bits était coupé en deux : les 16 premiers registres étaient utilisés pour la pile, les 48 suivants étaient utilisés pour autre chose. L'idée est que le registre 0 mémorise un emplacement vide, le registre 1 contient le sommet de la pile, le registre 2 l'opérande sous le sommet de la pile, et ainsi de suite. Les 15 premières opérandes de la pile sont donc mémorisées dans les registres. Les instructions écrivent leur résultat dans le registre adéquat, puis tous les registres sont décalés pour que le résultat passe dans le registre R1. Par exemple, les instructions PUSH chargent une opérande dans le registre 0, puis tous les registres sont décalés de un rang, pour que l'opérande chargée soit dans le registre 1. Le registre 0 devient alors le registre numéro 1, le registre 1 devient le 2, et ainsi de suite. Par contre, les opérations arithmétiques font autrement. Une instruction d'addition, par exemple, lit les opérandes dans le registre 1 et 2, puis place son résultat dans le registre 2 (les deux opérandes sont dépilées). Puis, les registres sont décalés d'un rang pour que ce registre 2 devienne le 1. Les registres ne sont en réalité pas décalés, il n'y a pas de transferts entre registres. A la place, les 16 premiers registres utilisaient une forme limitée de renommage de registres, similaire au banc de registre tournant utilisé sur les processeurs EPIC comme l'Itanium. Les numéros de registres sont renommés, décalés, à chaque fois qu'on empile ou dépile une opérande. Pour faire le renommage de registres, processeur contient un ''register stack counter'' de 4 bits, qui est incrémenté à chaque fois qu'on empile une donnée, et est décrémenté quand on dépile une donnée. Quand on veut accéder à un opérande, le numéro de registre architectural associé est traduit en numéro de registre physique, en additionnant le ''register stack counter'' (modulo 16, pour rester sur 4 bits). ==L'accélération de l'interprétation/JIT : le ''Thumb-EE'' d'ARM== Quelques processeurs ont ajouté des instructions pour faciliter le travail des interpréteurs/JIT. Par exemple, en 2005, ARM a ajouté le mode ''Thumb Execution Environment'', qui faisait cela. Il reprenait le jeu d'instruction compact ''thumb'' et ajoutait quelques instructions et modifiait le comportement d'instructions existantes. Le jeu d'instruction ''Thumb-EE'' a été ajouté sur les CPU ARM en 2005, mais a été déprécié en 2011, par manque d'utilité. Mais la tentative mérite qu'on s'attarde dessus. Les CPU de gamme M, déstinés à l'embarqué, n'ont jamais supporté ''Thumb-EE'', vu que de tels processeurs ne sont pas conçus pour exécuter du code interprété/JIT. Le mode ''Thumb-EE'' est un mode d'exécution, séparé de l'ARM normal. Ainsi, une instruction ''Thumb-EE'' et une instruction ARM peuvent avoir le même encodage en binaire, mais se comporter différemment. Le processeur est à tout moment soit en mode ''Thumb-EE'', soit en mode ARM normal, ce qui précise comment ces instructions doivent se comporter. Des instructions ont été ajoutées par ''Thumb-EE'', pour qu'il fasse son travail. L'entrée et la sortie du mode ''Thumb-EE'' se fait avec deux instructions : ENTERX et LEAVEX. Une fois en mode ''Thumb-EE'', le décodage des instructions ''thumb'' se fait avec les règles du ''Thumb-EE'', et non celles du ''thumb'' normal. Une première différence entre ''thumb'' normal et ''Thumb-EE'' est l'ajout de ce qui s'appelle un ''null check'' pour les instructions mémoire. Le ''null check'' est utilisé pour les instructions en mode d'adressage "base + indice" ou "Base + décalage", ou tout autre mode d'adressage avec un registre de base utilisés dans des calculs d'adresse. L'idée est de vérifier si l'adresse de base vaut zéro ou non. Si c'est le cas, le processeur lève une exception matérielle, qui est traitée par l'interprétation ou le compilateur JIT. Une telle situation est en effet signe d'une erreur d'adressage mémoire, qui doit être traitée par l'interpréteur. Typiquement, c'est signe qu'un pointeur n'a pas bien été initialisé, ce qui peut arriver avec du code interprété ou JIT. Les interpréteurs/JIT traitent généralement la situation en ajoutant un test avant toute instruction mémoire, pour vérifier si le pointeur accédé vaut zéro ou non. Avec ''Thumb-EE'', pas besoin d'ajouter les tests en questions : ils sont réalisés automatiquement lors de chaque instruction mémoire. : Les registres pour la pile, comme le pointeur de pile et le pointeur de ''frame'', sont aussi concernés. L'instruction CHKA vérifie que les accès à un tableau ne débordent pas en dehors du tableau en question. Par exemple, pour un tableau de 1024 éléments, elle vérifie si l'indice est compris entre 0 et 1024. Pour cela, elle vérifie si l'indice de tableau est bien dans l'intervalle adéquat et lève une exception matérielle si ce n'est pas le cas. Son utilité se comprend quand on sait que tous les accès à un tableau sont vérifiés par l'interpréteur. L'interpréteur est censé ajouter des instructions pour vérifier les indices, à savoir deux ou trois branchements. Avec CHKA, le test se fait en une seule instruction. D'autres modifications mineures de l'encodage des instructions sont aussi présentes, ainsi que des modifications pour les instructions LOAD/STORE. Précisément, dans les modes "base + indice" et "base + décalage", le décalage et l'indice subissent maintenant des décalages. L'idée est de simplifier le calcul d'adresse. De quoi économiser quelques instructions lors des calculs d'adresse, ce qui facilite le travail de l'interpréteur/JIT. Et des instructions LOAD/STORE précises voient leur comportement modifié pour faciliter la gestion de la pile de Java ou l'exécution des méthodes locales. Pour faciliter le travail de l'interpréteur, deux instructions HBP et HBLP ont été ajoutées. Elles branchent vers une fonction qui gère les exceptions logicielles (une fonctionnalité présente dans de nombreux langages, comme Java). La première effectue un branchement vers la fonction en question, la seconde sauvegarde l'adresse de retour avant de faire ce branchement. Les deux peuvent brancher vers 256 fonctions pré-déterminées, sans avoir à présenter leur adresse. ==La traduction binaire accélérée par le matériel== Plus haut, nous avons surtout parlé des compilateurs et des interpréteurs. Cependant, nous devons aussi parler de la '''traduction binaire'''. Elle traduit un programme écrit dans un code machine vers un autre code machine. Par exemple, elle traduit un programme compilé pour un CPU x86 vers un code machine ARM. En général, le système d'exploitation est généralement compilé pour le jeu d'instruction natif, mais il exécute des applications prévues pour le x86. Les applications sont traduites par le système d'exploitation, avant d'exécuter le code traduit. : Nous parlerons dans la suite de code machine source et de code machine cible, pour parler respectivement du code à traduire et du code obtenu après traduction. Idem avec d'autres termes comme architecture cible/source, ou jeu d'instruction cible/source. La traduction binaire est surtout utilisée pour des questions d'émulation ou de compatibilité. Par exemple, lorsque les Macintosh sont passés de processeurs Power PC vers des processeurs x86, le système d'exploitation Mac OS utilisait la traduction binaire pour convertir les anciennes applications Power PC vers du code x86. Les utilisateurs n'y ont vu que du feu. Le système de traduction binaire était appelé Rosetta 1. Par la suite, lors de la transition de processeurs x86 vers des processeurs Apple, Rosetta 2 a vu le jour. La traduction binaire a surtout été utilisée pour traduire du code x86 vers un autre jeu d'instruction. Il faut dire que le x86 est le jeu d'instruction dominant. De nombreuses entreprises ont eu pour ambition de briser l'hégémonie du x86 sur PC, en remplaçant le x86 par un jeu d'instruction plus performant, tout en gardant une compatibilité maximale. La traduction binaire était la seule solution pratique. Elle était le plus souvent intégralement réalisée en logiciel, comme c'était le cas sur les architectures Itanium avec le ''IA-32 Execution Layer''. ===Les généralités sur la traduction binaire assistée en matériel=== L'architecture source est presque tout le temps une architecture CISC, assez ancienne, qu'on souhaite émuler. Le choix de l'architecture cible se porte souvent sur une architecture VLIW, fort différente de l'architecture source, et ce pour obtenir une économie de matériel conséquente. Pour s'exécuter rapidement, le code traduit doit exploiter le parallélisme d'instruction, à savoir exécuter plusieurs instructions en même temps dans des unités de calcul séparées. Il est possible d'utiliser un processeur superscalaire avec exécution dans le désordre pour cela, mais au prix d'un cout important en transistors. Alors qu'en utilisant un CPU VLIW, c'est le traducteur binaire qui fait tout le travail d'extraction du parallélisme d'instruction. Et après tout, quitte à avoir un traducteur binaire, autant lui refiler le boulot d'optimisation. Compiler du code à la volée est certes assez lourd, mais qu'il existe des algorithmes efficaces pour regrouper des instructions indépendantes dans une seule instruction VLIW. Au passage, le processeur VLIW a plus de registres que le processeur source. Cela permet de faire du renommage de registres directement en logiciel. Le traducteur binaire n'hésite pas à changer les noms de registres entre instructions source et instruction VLIW cible, afin de supprimer des dépendances de données. En conséquence, cela ouvre des opportunités de parallélisme, qui sont exploitées lors du regroupement des opérations en instructions VLIW. Vous pourriez penser que le choix d'un CPU VLIW pour émuler du CISC est tout sauf optimal. Vous devez penser que la traduction binaire est d'autant plus simple que l'architecture source et cibles sont semblables. Dans les faits, ce n'est pas tellement traduire les instructions qui pose problème, mais plus la gestion du registre d'état, des exceptions matérielles, de la mémoire virtuelle, la différence entre gros-boutisme et petit-boutisme, de même que des différences d'adressage pour les périphériques. Le premier problème est la gestion des conditions, notamment en présence d'un registre d'état. En pratique, la traduction binaire s'utilise pour traduire du code CISC vers du code RISC ou VLIW. Les architectures source ont donc un registre d'état, qui est mis à jour non seulement par des instructions de test, mais aussi des instructions arithmétiques. L'architecture cible n'a elle pas de registre d'état, mais des registres à prédicats. La traduction de l'un vers l'autre est alors quelque peu compliquée. Et elle est d'autant plus compliquée que le registre d'état est la source de dépendances d'instruction implicites, qui réduisent les performances. Par exemple, si une instruction arithmétique modifie le registre d'état, cela peut impacter l'exécution d'une instruction ultérieure, qui lit ce registre d'état. Pour éliminer ces fausses dépendances, le traducteur binaire doit renommer ce registre en logiciel et détecter les dépendances utiles. Et le compilateur doit gérer les cas où il y a beaucoup de distance entre les deux, voire les cas où l'instruction dépendante n'a pas encore été analysée par le compilateur. Un autre problème est lié à la gestion des exceptions matérielles, et précisément des exceptions précises. Pour rappel, une instruction VLIW regroupe plusieurs opérations, opérations qui correspondraient à une instruction machine sur un CPU pas VLIW. En conséquence, plusieurs instructions du langage machine "source" sont regroupées en une seule instruction VLIW. Et il faut tenir compte du cas où une instruction source lève une exception. Dans le code VLIW, cela signifie qu'une opération lève une exception, et il faut annuler partiellement l'instruction VLIW associée. Et par partiellement, on veut dire que seules les opérations suivantes dans l'ordre du programme source doivent être annulées, puis ré-exécutées. Et c'est un sacré casse-tête ! Les processeurs VLIW utilisent comme solution le mécanisme d'exceptions différées des processeurs EPIC, vu il y a quelques chapitres. Pour résumer, le code est exécuté par blocs d'instruction, délimités par des branchements ou tout autre limite/barrière pertinente dans le code. Un bloc de code est compilé en deux versions : une version rapide sans exceptions matérielles, une version lente qui gère les exceptions précises. La version rapide s'exécute sans exécuter les exceptions. Cependant, les exceptions matérielles sont enregistrées, pour être prises en compte à la toute fin du bloc de code. La version rapide est exécutée en premier et elle mémorise si une exception matérielle a eu lieu. Puis une instruction vérifie si une exception a eu lieu et décide quoi faire. Si aucune exception n'a eu lieu, elle passe à la suite du programme, les résultats du bloc de code sont définitivement acceptés. Mais si une exception a eu lieu, tout est annulé. Le processeur est remis dans l'état initial, puis le code est ré-exécuté instruction par instruction de manière à gérer l'exception correctement. ===La traduction par pré-décodage=== Une première solution serait de faire la traduction binaire dans le cache d'instruction. Lors d'un défaut de cache, le code chargé depuis la RAM est traduit en code machine cible. Et c'est ce code machine cible qui est mémorisé dans le cache d'instruction et exécuté par le processeur. La technique marche sur le papier et n'est qu'une amélioration des techniques de pré-décodage vues il y a quelques chapitres. Cependant, le code obtenu est une traduction assez basique, qui n'incorpore pas d'optimisations dignes de ce nom. De plus, elle gère mal le cas où la taille du code source et cible sont potentiellement très différentes. Concrètement, elle est surtout utile pour traduire du code d'un processeur RISC vers un autre processeur RISC, dont les tailles d'instructions sont similaires. Elle ne permet pas d'émuler plusieurs jeux d'instructions différents, le cout en matériel (un circuit de pré-décodage par jeu d'instruction) serait trop important. ===Le projet DAISY d'IBM=== De très rares processeurs étaient conçus pour accélérer cette traduction binaire, afin de garder de bonnes performances. Les premiers à avoir étudié l'idée étaient IBM, avec leur projet DAISY (''Dynamically Architected Instruction Set from Yorktown''). Le projet de base était de convertir à la volée du code compilé pour des CPU Power PC, vers du code VLIW. Les chercheurs d'IBM avaient développé un algorithme de traduction dynamique efficace, ainsi qu'un processeur VLIW disposant d'optimisations spécifiques à la traduction binaire dynamique, à la volée. De nombreuses idées de ce projet ont été reprises ou re-découvertes par la société Transmetta, avec ses processeurs Crusoe et Efficieon, puis par NVIDIA avec son projet Denver. Les ingénieurs de ce projet ont étudié la possibilité d'émuler plusieurs jeux d'instructions différents sur un même processeur, notamment le s390 d'IBM et le x86. En théorie, cela demande juste d'avoir plusieurs programmes de traduction binaire : un pour le x86, un autre pour le s390, éventuellement un autre pour le Power PC. Mais cela ne s'est pas concrétisé. Mais les ingénieurs ont étudié quelle pourrait être l'architecture VLIW idéale pour ça. Par exemple, il fallait des additions 3-opérandes pour simplifier les calculs d'adresse. Une difficulté était la gestion du registre d'état, dont les bits ne sont pas mis à jour de la même manière sur le x86 et le s390 ou le Power PC. La différence entre gros-boutisme et petit-boutisme était aussi un problème, de même que des différences d'adressage pour les périphériques. Pour que la traduction binaire soit efficace, le processeur VLIW intègre diverses optimisations, comme les exceptions différées et les branchements multi-voies. La principale est la suivante : seul le code des boucles ou fonctions exécutées fréquemment est traduit en code VLIW, le reste du code est interprété. En effet, traduire du code binaire prend plus de temps qu'une simple interprétation. Pour du code qui ne sera exécutée qu'une seule fois, il est plus rapide d'utiliser l'interprétation. Par contre, pour du code exécuté beaucoup de fois, le cout de la traduction binaire est amorti, dilué sur N exécutions, compensé par le gain en temps d'exécution de ce code traduit. Au final, cela permet de ne traduire que le code qui le mérite. La détection du code fréquemment exécuté est réalisée dans le cœur VLIW. L'unité de branchement mémorise les derniers branchements rencontrés et le nombre de fois qu'ils ont été exécutés. Si ils ont été exécutés un certain nombre de fois, l'unité de branchement lève une exception matérielle, qui invoque le traducteur binaire. La routine de cette exception est le traducteur binaire proprement dit. En clair, la traduction binaire est démarrée quand le processeur détecte qu'une fonction a été exécutée plus de N fois, via une exception matérielle dédiée. La mesure du nombre d'exécution d'un branchement se fait dans l'unité de calcul dédiée aux branchements, avec l'aide d'une sorte de ''branch adress buffer'' modifié. Pour rappel, le ''branch adress buffer'' mémorise le ''Program counter'' de chaque branchement récemment rencontré. Ici, chaque entrée du ''branch adress buffer'' est associée à un compteur incrémenté à chaque exécution du branchement. ===Les processeurs Crusoe et Efficieon de Transmetta=== Les processeurs Crusoe Et Efficeron sont deux processeurs VLIW produits par la société Transmetta, une société californienne rachetée par NVIDIA. Ils étaient conçus pour exécuter spécifiquement des programmes x86, système d'exploitation inclus. L'idée derrière ce projet était d'exécuter du code x86 sans que les applications, ni même le système d'exploitation et le BIOS soient au courant ! Les processeurs Transmetta étaient en réalité conçus pour exécuter un programme unique, le ''Code Morphing Software'' (CMS), qui traduisait le code x86 en code VLIW. Transmetta ne voulait pas que son processeur VLIW soit exploité directement, sans le CMS. Il n'avait pas rendu public de compilateur pour traduire du C vers du code VLIW, il n'avait pas donné la documentation du jeu d'instruction VLIW. Des efforts de rétro-ingénieurie, documentés sur le site realworldtech, ont cependant permis de comprendre comment étaient encodées les instructions du processeur Crusoe. Comme pour le projet Daisy, le ''Code Morphing Software'' utilisait à la fois interprétation et traduction binaire, selon les besoins. Les instructions sont interprétées lors de leurs premières exécutions, mais le CMS bascule sur de la traduction en code VLIW après un certain nombre d'exécution. Ainsi, les boucles souvent exécutées sont traduites en code VLIW, alors que le reste du code est interprété. Le choix du code à traduire est le fait du CMS, il utilise des heuristiques complexes pour, qui ne sont pas connues dans le détail. Le ''Code Morphing Software'' est mémorisé dans une EEPROM, ce qui en fait un ''firmware'' situé en dessous du BIOS. Le CMS démarre ensuite le BIOS, qui lui-même démarre le système d'exploitation, qui lui-même démarre les pilotes de périphériques et les programmes. Le CMS se réserve les 16 premiers mébioctets de l'espace d'adressage. L'EEPROM du CMS est mappée dedans, mais n'en utilise que la moitié. Le reste est utilisé comme cache, pour mémoriser le code VLIW traduit par le CMS. Les deux processeurs intégraient aussi deux mémoires SRAM, utilisées par le CMS, appelées la ''local program memory'' (LPM) et la ''local data memory'' (LDM). La première contient du code qui gére les interruptions, la mémoire virtuelle, les exceptions matérielles, les problèmes d'alignement mémoire, et quelques fonctions très fréquemment utilisées par le CMS. La seconde est de la mémoire RAM utilisée par ces fonctions de la ''local program memory''. Les mémoires caches L1 et L2 sont séparées de ces deux mémoires. Le processeur Crusoe et Efficieon étaient des processeurs VLIW très simples. Ils n'avaient même pas de MMU, ni de fonctionnalités importantes sur les CPU x86. Pour Crusoe, les instructions VLIW étaient encodées sur 64 ou 128 bits, et regroupaient entre 2 à 4 opérations. Les instructions LOAD/STORE ne supportaient pas d'adressage indicé, juste de l'adressage indirect à registre. Crusoe avait 5 unités de calcul : deux ALU entières, une unité LOAD/STORE, une unité de branchement, une FPU. Efficieon doublait le nombre d'ALU entière et d'unité LOAD/STORE, les instructions VLIW passaient à 256 bits. Le processeur Crusoe contient 160 registres, dont 64 registres généraux, 32 registres flottants. Les registres généraux font 32 bits, ce qui est cohérent avec le fait que les CPU x86 de l'époque étaient des processeurs 32 bits. Quant aux registres flottants, ils faisaient 80 bits, ce qui colle avec la taille des registres flottants de la FPU x87 utilisée à l'époque. Sur les 64 registres généraux, seuls 48 étaient réellement utilisable pour mémoriser des opérandes. Une partie des registres généraux étaient utilisés pour la gestion de la pile, d'autres pour mémoriser l'état du CPU x86 émulé, un registre était un registre zéro non accesible en écriture. La gestion des exceptions est optimisée avec un système d'exceptions différées, le même que celui décrit plus haut. Le processeur utilise un système similaire, pour effectuer des lectures anticipées, qui a été expliqué dans le chapitre sur les processeurs VLIW/EPIC. Sauf qu'il s'agit là d'une optimisation pour gagner en performances en général, pas une aide utile pour gérer des exceptions précises ou toute autre subtilité de la traduction binaire. Pour gérer les exceptions différées et les lectures anticipées, le processeur mémorise l'état du processeur avant de démarrer un bloc de code. Pour cela, le processeur contient des ''shadow registers'', 48 registres entiers et 16 registres flottants. Vous remarquerez qu'il y a autant de registres entiers que de ''shadow registers'' entiers. Les registres entiers/flottants sont mémorisés dans les ''shadow registers'' avant d'exécuter un bloc de code, afin que le processeur puisse revenir à l'état de base. Une autre fonctionnalité liée est le ''gated store buffer''. Le principe est d'éviter toute écriture en mémoire RAM, tant que le code VLIW n'a pas émis d'instruction ''commit''. L'idée est que les écritures sont conservées dans la ''store queue'' de l'unité LOAD/STORE. Pour rappel, la ''store queue'' met en attente les écritures soit tant que les écritures ne sont pas terminées, soit tant que la RAM est occupée. L'idée est que les écritures sont accumulées dans la ''store queue'' tant que l'instruction ''commit'' n'a pas eu lieu. Le ''store queue'' est "déversé dans la RAM" seulement quand l'instruction ''commit'' s'exécute. ===Les CPU NVIDIA Denver=== Nvidia a tenté quelque chose de similaire avec son projet Denver. NVIDIA a racheté l'entreprise Transmetta, ses brevets, et a tenté de refaire la même chose. Le projet initial était de traduire du code x86 vers un jeu d'instruction VLIW propriétaire, comme l'a fait Transmetta. Mais NVIDIA n'a pas réussi à acquérir la licence du jeu d'instruction x86 et s'est rabattu sur le jeu d'instruction ARM. Le processeur né de ce projet est le Tegra K1-64 CPU. Il s'agit d'un processeur composé d'un cœur VLIW, couplé à deux décodeurs d'instructions ARM. Les deux décodeurs ARM traduisent une paire d'instructions ARM consécutives en instructions VLIW, pour les exécuter sur le cœur VLIW. Le CPU peut fonctionner selon deux modes : ARM et VLIW. En somme, le processeur supporte deux jeux d'instruction : le jeu d'instruction ARM, et un jeu d'instruction VLIW. En mode ARM, les instructions ARM sont chargées depuis le cache, traduites en VLIW par les décodeurs ARM, puis exécutées par le cœur VLIW. En mode optimisé, le CPU exécute des instructions VLIW chargées depuis le cache d'instruction, les décodeurs ne sont pas utilisés. : Le CPU supporte en réalité deux jeux d'instruction ARM : ARM8 et 7. Mais ils correspondent au même mode pour le CPU. Un point important est que le processeur peut changer très rapidement de mode, en à peine quelques cycles d'horloges. On n'est clairement pas dans le cas des CPU x86, qui mettent des plombes pour passer du mode 32 à 64 bits et inversement. La commutation est tellement rapide qu'on peut considérer que le processeur supporte deux jeux d'instruction simultanément. Et le support simultané de deux jeux d'instruction facilite l'implémentation de la traduction binaire. Encore une fois, inutile de traduire du code qui ne sera exécutée qu'une seule fois, mieux vaut l'exécuter directement dans le mode ARM. Surtout que le processeur supporte un mode ARM qui permet d'exécuter du code ARM sans perte de performance. Par contre, le code fréquemment utilisé, notamment dans des boucles critiques, est traduit en VLIW. Au final, les performances sont optimisées, en limitant le travail du logiciel traducteur binaire. La détection du code fréquemment exécuté se fait différemment que pour les processeurs Transmetta et Daisy. L'unité de branchement ne détecte pas directement le code beaucoup exécuté, même si elle a un rôle à jouer. A la place, la détection est réalisée en logiciel, par un ''thread'' dédié, qui s'exécute sur un cœur séparé. Il détecte le code fréquemment utilisé en regardent les branchements exécutés récemment. Pour cela, le processeur mémorise l'historique des branchements pris récemment et copie cette historique dans une mémoire tampon dédiée, partagée entre tous les cœurs. Vu qu'elle est partagée, le ''thread'' de détection a accès à l'historique sans pertes de performances, seule la copie de l'historique dans ce tampon a un cout en performance, pas son partage. Le code VLIW obtenu après traduction binaire est mémorisé dans la mémoire RAM, dans une portion spécialement réservée pour. Elle fait 128 mébioctets, et est appelée le '''cache d'optimisation''' par NVIDIA. Le cache d'optimisation est protégé en écriture et seul le traducteur binaire peut écrire dedans. Au passage, le traducteur binaire est du code VLIW, ce qui fait qu'il est placé dans le cache d'optimisation. Le processeur détecte automatiquement quand une fonction pour laquelle le code compilé équivalent est disponible. Pour cela, l'unité de chargement contient une table de correspondance entre l'adresse de la fonction ARM, et l'adresse de son équivalent compilé. Quand une fonction est appelée, l'unité de chargement regarde l'adresse de destination du branchement. Si l'adresse est dans cette table, elle récupère l'adresse de la fonction compilée et branche vers celle-ci. Le processeur dispose d'un cache d'instruction de 128 kibioctets, ce qui est très important, mais nécessaire vu la taille des instructions VLIW. Le coeur VLIW dispose de ses propres décodeurs, d'un ''scoreboard'', de 7 unités de calcul et de registres. Pour les unités de calcul, il a deux FPU, deux ALU, une unité de branchement, et deux unités LOAD-STORE faisant aussi ALU. Un point important est que le CPU dispose de 64 registres entiers et de 64 registres flottants. C'est deux fois plus que supporte le jeu d'instruction ARM. En mode ARM, seule la moitié des registres est utilisée. Mais en mode optimisé, le traducteur binaire utilise bien les 64 registres, grâce à une sorte de renommage de registres logicielle. Pour simplifier la traduction binaire, le processeur supporte les techniques vues précédemment. Le processeur intègre un ''gated store buffer'' similaire à celui des processeurs Transmetta. Les techniques d'exceptions différées sont aussi supportées, comme sur les processeurs Transmetta. Il a aussi une unité de préchargement décrite par NVIDIA comme agressive, avec support du préchargement de type anticipé (''runahead''). ===L'émulation de la mémoire virtuelle=== Pour finir, il faut parler de la mémoire virtuelle avec la traduction binaire. Sur le processeur source, le processeur gère à la fois des adresses physiques et virtuelles et sa MMU fait la conversion entre les deux. Sur le processeur VLIW, la MMU est simulée par le programme de traduction binaire, partiellement ou totalement. L'implémentation exacte varie suivant que l'on parle du projet Daisy, des processeurs Transmetta ou de Denver. Sur les processeurs Transmetta, il n'y a pas de MMU ni de mémoire virtuelle. A la place, le programme de traduction binaire émule la mémoire virtuelle du processeur source. Le processeur VLIW ne gère que des adresses physiques, rien d'autre. L'espace d'adressage physique du processeur VLIW a la même taille que celui du processeur émulé, ici des processeurs x86. Il est vraisemblable que les adresses physiques utilisées par le processeur x86 sont les mêmes que celles du processeur VLIW. Sur le projet Daisy, le processeur VLIW gère la mémoire virtuelle via pagination, comme les processeurs émulés. Ce qui fait qu'il gère un espace d'adressage virtuel et un espace d'adressage physique. Pour éviter toute confusion, nous parlerons d'adresse physique/virtuelle VLIW pour les adresses physiques/virtuelles du processeur VLIW, d'adresse virtuelle/physique source pour celle du jeu d'instruction traduit, à savoir du Power PC ou du s390, éventuellement de l'x86. Il faut alors faire le lien entre adresses physiques source et adresses virtuelles VLIW. Pour cela, rien de plus simple : il y a correspondance parfaite. L'adresse physique source numéro N correspond à l'adresse virtuelle VLIW numéro N. Cependant, cela signifie que tout l'espace d'adressage virtuel VLIW serait utilisé par le code à traduire. En réalité, il faut ajouter de la place pour le code traduit et le programme de traduction binaire. La conséquence est que l'espace d'adressage virtuel VLIW est plus large que l'espace d'adressage source. L'espace d'adressage virtuel VLIW est découpé en trois sections : une section pour le code Power PC à traduire, une autre pour le code traduit en VLIW, et une dernière réservée au traducteur binaire. Le programme de traduction est placé dans une ROM mappée en mémoire dans la seconde section. Le reste de la seconde section est réservé à une mémoire RAM utilisée par le programme de traduction. [[File:Mémoire virtuelle sur le projet Daisy.png|centre|vignette|upright=1.5|Mémoire virtuelle sur le projet Daisy]] Il faut noter que chaque page physique source, une fois traduite, correspond à N pages virtuelles VLIW. La raison est que le code traduit prend plus de place que le code machine originel. En conséquence, on doit utiliser une page finale plus grande. Pour le reste, des pages contiguës en mémoire physique source sont elles aussi contiguës en mémoire virtuelle VLIW. Le calcul d'adresse est donc simple : l'adresse physique source est multipliée par N, puis on ajoute l'adresse de base à laquelle commence la section pour le code traduit. ==Les processeurs à jeu d'instruction reconfigurable== Nous venons de voir comment accélérer en matériel la traduction binaire. Mais il existe une alternative qui permet de se passer de traduction binaire, complétement. L'idée est d'avoir un processeur avec un '''jeu d'instruction reconfigurable'''. Le nom est assez parlent : on peut configurer le processeur de manière à supporter le jeu d'instruction de son choix ! Par exemple, on pouvait reconfigurer un CPU de manière à ce qu'il supporte le jeu d'instruction x86, ou qu'il exécute du ''bytecode'' Java, ou du code ARMv8, etc. De tels processeurs sont encore plus rare que ceux vu avant dans ce chapitre, mais ils ont existé ! Les processeurs de ce type avaient un ''microcode réinscriptible'', à savoir que c'était des processeurs microcodés pour lesquels on pouvait modifier le microcode. L'idée était qu'en changeant le microcode, on changeait le jeu d'instruction du processeur. Pour cela, le microcode était associé à une mémoire SRAM dans laquelle le microcode était copié au démarrage du CPU. Il y avait donc une micro-ROM et une micro-RAM. Les instructions étaient décodées en utilisant le microcode dans la micro-SRAM, la micro-ROM ne servait qu'au démarrage. En modifiant le contenu de la micro-SRAM, on pouvait altérer le microcode, voire le remplacer totalement. ===Le Burroughs B1700 : introduction=== Un exemple est celui du Burroughs B1700, et il est intéressant d'étudier en détail son architecture. Son architecture est décrite dans le livre "Interpreting Machines : Architecture and Programming of the Bl700/Bl800 Series", écrit par Elliott I. Organick et James A. Hinds. Une partie de ce qui vfa suivre est un très court résumé de ce livre. Il disposait de 4 registres généraux, nommés X, Y, T et L, tous de 24 bits. Les registres T et L étaient découpés en 6 sous-registres de 4 bits, nommés LA LB LC LD LE et LF pour le registre L, et TA TB TC TD TE et TF pour le registre T. Ils étaient adressables comme n'importe quel registre. Un point important est que le registre T avait des possibilités d’extraction spécifiques. Il était possible d'extrait N bits, placés n'importe où dans ce registre, et de les copier dans un autre registre. Nous verrons pourquoi cette possibilité est intéressante et ne sert que pour le registre T. Les registres X et Y étaient reliés à une ALU de 24 bits capable d'effectuer des opérations arithmétiques basiques. Fait étonnant, l'ALU faisait tous les calculs en même temps et fournissait ses résultats dans 7 registres : un registre SUM pour l'addition, un registre DIFFERENCE pour la soustraction, et un registre par opération bit à bit. Les registres pour les opérations bit à bit sont : CMPX et CMPY pour l'opération NOT sur chaque opérande, XANY pour le ET bit à bit, XEOY pour le XOR, XORY pour le OU logique. Les additions et soustractions pouvaient se faire sur des opérandes codés en binaire ou en BCD, suivant le code opération utilisé. A cela, il fallait ajouter deux registres de "résultat" MSKX et MSKY. Ils fournissaient un masque dépendant de la taille du résultat en bits. Cela permettait de gérer des données de 8 ou 16 bits, alors que l'ALU faisait 24 bits. Et cela n'était pas limité à 16 ou 8 bits, mais n'importe quelle taille inférieure ou égale à 24 bits était supportée, même des tailles non-conventionnelles comme des résultats codés sur 3 bits, 9 bits, 12 bits, etc. L'ALU fournissait 12 conditions, qui alimentait un registre d'état. Le registre d'état était segmenté en trois registres appelés XYST, XYCN et BICN. Le processeur contenait de nombreux autres registres de 4 bits, dont les sous-registres de T et L. En tout, il y a 27 registres de 4 bits adressables ! Et le processeur disposait d'une ALU 4 bits pour manipuler ces registres de 4 bits ! Elle est capable de faire des MOV entre registres de 4 bits, des opérations bit à bit, et peut aussi tester si un bit vaut 0 ou 1. La dernière possibilité est très utile pour implémenter les branchements. Pour communiquer avec les entrées-sorties, il disposait de deux registres CMND et DATA : CMND pour envoyer une commande à une entrée-sortie, DATA pour lire ou écrire une donnée. Pour la mémoire, il avait deux registres séparés : READ et WRITE. READ contient la donnée lue lors d'une lecture,; WRITE est pour une donnée à écrire lors d’une écriture. Il s'agit de registres d’interfaçage mémoire, qui ont été promus au rang de registres architecturaux. Les deux registres sont gérés par le microcode, qui est le langage principal de la machine. Les instructions machines configurées font sans, mais le microcode doit y avoir accès facilement. Et vu que le microcode est le langage principal de ce processeur, ces deux registres deviennent architecturaux. ===Une architecture bit-adressable=== Si avoir un microcode réinscriptible est suffisant, quelques choix de ''design'' facilitent l'émulation de jeux d'instructions variés. Par exemple, le CPU Burroughs B1700 avait ajouté une fonctionnalité très importante : il était bit-adressable. En clair, il pouvait adresser la mémoire bit par bit, si nécessaire. L'avantage est que cela permettait d'émuler des architectures dont la taille des registres/''byte''/mots était très différente. Un autre avantage est que cela facilitait la gestion des instructions de taille variable. Détaillons rapidement le premier avantage. Le processeur était un CPU 24 bits, mais il pouvait émuler des processeurs 16 bits, 8 bits, 4 bits, 1 bit, 9 bits, ou toute autre valeur. Pour cela, le processeur lisait des mots de 24 bits et utilisant un ''barrel shifter'' pour sélectionner les bits adéquats. Par exemple, pour un accès de 16 bits, la donnée lue en mémoire était masquée de manière à ne garder que les 16 bits de poids faible. Le ''barrel shifter''/circuit de masquage était placé directement avant le bus mémoire, sur le trajet des données/instructions. Le ''barrel shifter'' était commandé par un registre ''Field Unit'', qui précisait quelle était la taille des données à charger. ===La gestion de la taille des instructions=== Les instructions à décoder n'ont pas de longueur fixe, à savoir qu'elles peuvent faire 16 bit avec tel jeu d'instruction, 8 bits avec un autre, 32 bits sur un autre, etc. Elles peuvent aussi être de longueur fixe ou variable ! Le B1700 intégrait donc un support des instructions de longueur variable, qui était "détourné" pour gérer le fait que des jeux d'instruction différents ont des instructions de taille différentes. Notons que vu que l'architecture est bit-adressable, les contraintes d'alignement des instructions disparaissent, ce qui a des avantages qu'on va détailler dans ce qui suit ! L'implémentation des instructions de taille variable ajoutait un registre avant le registre d'instruction. Le registre d'instruction proprement dit était un registre M de 24 bits. Il était précédé par un registre T, dans lequel l'instruction était assemblée, en combinant plusieurs morceaux. Le registre T est relié à un ''barrel shifter'', différent au précédent, qui permettait de masquer, décaler et extraire une donnée de ce registre. Si l'instruction est codée est codée sur moins de 24 bits, le registre T se contente de masquer les bits en trop. Si l'instruction chargée dépasser 24 bits, le chargement se faisait en plusieurs passes, avec plusieurs lectures en mémoire. Chaque passe découpait l'instruction pour extraire des '''champs''' : l'opcode, les opérandes, les adresses absolues, les constantes immédiates, les numéros de registre, etc. Et ils étaient copiés dans des registres interne au processeur. Par exemple, il servait à extraire l'opcode de l'instruction. Ou encore, pour une instruction de branchement direct, il pouvait extraire l'adresse à laquelle brancher de l'instruction. Le chargement de l'instruction se faisait alors champ par champ. Notons que vu que l'architecture est bit-adressable, les contraintes d'alignement des instructions disparaissent ! Quand on charge une instruction, peut importe qu'elle soit ou non alignée sur 24 bits, elle sera placée dans les bits de poids faible, il suffira de masquer les bits de poids fort inutiles. Par exemple, si on charge une instruction de 16 bits, les 16 bits seront copiés dans les 16 bits de poids faible du registre T, il suffira de masquer l'octet de poids fort. Pas besoin de faire de décalages pour gérer des instructions mal alignées... Pour charger une instruction, il faut connaitre plusieurs informations : l'adresse de son premier bit, sa taille. Pour cela, le B1700 contient deux registres, nommés ''Field Adress'' (FA) et ''Field Length'' (FL), qui indiquent l'adresse du premier bit et la taille de l'instruction à charger. Le premier fait 24 bits, ce qui permet de gérer des adresses de 24 bits. N'oubliez pas que le processeur est bit-adressable, ce qui permet d'adresser 16 Mébi-bits, soit 2 mébioctets. Le registre pour la taille fait lui 16 bits, ce qui permet de gérer des instructions de 65536 bits, soit 8192 octets ! Leur nom commence par ''field'', car le processeur peut gérer des instructions de taille variable et/ou plus longues que 24 bits. Dans ce cas, le chargement de l'instruction se fait champ par champ. Pour donner un exemple, prenons une instruction composée de 20 champs de 12 bits chacun. Les champs sont chargés un par un dans le registre T. Le registre FA est alors initialisé avec l'adresse de l'instruction, il est incrémenté de 12 à chaque cycle. Le registre FL est initialisé avec la taille de l'instruction, soit 20 * 12 = 240 bits, et il est décrémenté de 16 à chaque cycle. Précisons cependant que la taille d'un champ peut être changée d'un cycle à l'autre. Par exemple, le processeur peut charger un opcode de 8 bits, l'interpréter, détecter que l'instruction demande ensuite de charger une adresse de 16 bits. Dans ce cas, le registre FL est immédiatement altéré pour charger 16 bits, au lieu des 8 bits de l'opcode immédiatement précédent. ==Les jeux d'instructions dédiés à un langage de programmation== De rares processeurs sont conçus pour un langage de programmation en particulier. On appelle ces processeurs, conçus pour des besoins particuliers, des '''processeurs dédiés'''. Par exemple, les fameux ''Burrough E-mode'' B5000/B6000/B7000 étaient spécialement conçus pour exécuter de l'ALGOL-60. Leurs cousins B2000/B3000/B4000 étaient eux conçus pour le COBOL. Des langages fonctionnels ont aussi eu droit à leurs processeurs dédiés. Le prolog en est un bel exemple, avec les superordinateurs de 5ème génération qui lui étaient dédié. On peut aussi citer les machines LISP, dédiés au langage LISP, qui datent des années 1970. Elles étaient capables d’exécuter certaines fonctions de base du langage directement dans leurs circuits : elles possédaient notamment un ''garbage collector'' câblé dans ses circuits ainsi que des instructions machines supportant un typage déterminé à l’exécution. Les processeurs dédiés ont eu leur heure de gloire au début de l'informatique, à une époque où les langages de haut niveau venaient d'être inventés. À cette époque, les compilateurs n'étaient pas performants et ne savaient pas bien optimiser le code machine. Il était alors rationnel, pour l'époque, de rapprocher le code machine cible et le langage de programmation de haut niveau. De nombreuses architectures dédiés ont ainsi été inventées, avant que les concepteurs se rendent compte des défauts de cette approche. Les défauts en question ne sont pas nombreux, mais assez simples à comprendre. Premièrement, elles sont très rapides pour un langage de programmation en particulier, mais sont assez mauvaises pour les autres, d'où un problème de "compatibilité". Ajoutons à cela que les langages de programmation peuvent évoluer, devenir de moins en moins populaires/utilisés, ce qui rend la création d'architectures généralistes plus pertinente. Enfin, les architectures dédiées sont évidemment des processeurs CISC, pour implémenter les nombreuses fonctionnalités des langages évolués. Et les défauts des CISC sont assez rédhibitoires à l'heure actuelle. À l'heure actuelle, les algorithmes des compilateurs se sont améliorés et savent nettement mieux utiliser le matériel. Ils produisent du code machine efficace, ce qui rend les architecture dédiées bien moins intéressantes. Si on ajoute les défauts de ces architectures dédiées, par étonnant que les architectures dédiées aient presque disparues. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'accélération matérielle de la virtualisation | prevText=L'accélération matérielle de la virtualisation | next=Le matériel réseau | nextText=Le matériel réseau }} </noinclude> 4obvlg91vu9iakca1f3so1ld8ho56es 764016 764015 2026-04-19T16:37:53Z Mewtow 31375 /* La gestion de la taille des instructions */ 764016 wikitext text/x-wiki De nos jours, la majorité des programmeurs programment dans des langages de haut niveau. Il est très rare pour eux d'avoir à utiliser de l'assembleur, et encore moins un langage machine. Les programmes écrits dans un langage de haut niveau sont traduits en langage machine par un logiciel appelé le compilateur. [[File:Compilation.PNG|centre|vignette|upright=2.5|Compilateur : principe.]] Il se trouve que quelques rares processeurs sont conçus pour faciliter le travail du compilateur. Et ce sont ces processeurs que nous allons voir dans ce qui suit. Ils représentent une catégorie de jeux d'instruction à part, qui n'a pas vraiment de nom. ==La compilation et l’interprétation== Un compilateur traduit donc du code source, écrit dans un langage de haut niveau, vers du langage machine. La traduction est rarement directe. En général, le compilateur traduit le code source en assembleur, qui est lui-même traduit en langage machine. L'assembleur est une représentation textuelle du langage machine alors que le code machine est du binaire exécutable par le processeur. La traduction se fait donc en deux étapes, la '''compilation''' proprement dite et l''''assemblage''', réalisées respectivement par un compilateur et un assembleur. Il faut parfois rajouter une troisième phase d'édition des liens, que nous passons volontairement sous silence. {| |[[File:Code C.png|vignette|200px|Le code source, ici en C.]] |[[File:Assem.png|vignette|250px|Le code assembleur.]] |[[File:Binary file - hello world (C programming).png|vignette|150px|Le code machine.]] |} ===L'interprétation et la compilation à la volée=== Une alternative à la compilation est l''''interprétation''', qui transforme le code source en code machine à la volée. Avec l'interprétation, le code source est passé à un logiciel appelé l''''interpréteur''', qui exécute le code source ligne de code par ligne de code. Une ligne de code est traduite en code machine, qui est exécutée, puis l'interpréteur passe à la ligne suivante. [[File:Kaantotulkkaus.png|centre|vignette|upright=3|Compilation versus interprétation.]] Un défaut de l'interprétation est son cout en performance, qui est assez important. Aussi, l'interprétation stricte a évolué vers un hybride entre interprétation et compilation. L'idée est qu'une partie peu importante du code source est interprétée, alors que le code important est compilé juste avant d'être exécuté. Pour le dire autrement, le code source est partiellement compilé à la volée, juste avant son exécution. Aussi, on parle de '''compilation à la volée'''. Le terme en anglais est ''Just In Time compilation'', abrévié en JIT. En soi, le JIT est associé à de l'interprétation, les JIT sont en réalité des hybrides interpréteurs-compilateurs. Un point important est que compiler du code à la volée est assez lourd, cela a un cout en performance. Par contre, le code compilé s'exécute plus vite que du code interprété. Si on exécute le code compilé une seule fois, le cout de la compilation l'emporte sur le gain à l'exécution. Mais le code compilé est réutilisé autant de fois que nécessaire. Le gain à l'exécution est donc multiplié par le nombre d'exécutions, alors que le cout de la compilation est répartit sur plusieurs exécutions, il est amorti. Les portions du code sont choisies de manière à avoir un gain en performance maximal. L'idée générale est que le code exécuté très souvent est compilé, alors que du code exécuté pas souvent est interprété. Typiquement, les boucles les plus souvent exécutées sont compilées, le code exécuté une seule fois est interprété. ===Les langages intermédiaires=== Les compilateurs modernes passent par un '''langage intermédiaire''' pour faire la transformation en code machine. Le compilateur traduit le langage de haut niveau en langage intermédiaire, puis traduit le langage intermédiaire en code machine cible. Le langage intermédiaire est parfois appelé le '''''bytecode''''', et ce terme recouvre aussi le code écrit avec du ''bytecode''. L'interprétation aussi peut passer par l'intermédiaire d'un un ''bytecode'', ce n'est pas limité aux compilateurs proprement dit. {| |[[File:Intermediate representation scheme.png|centre|vignette|upright=1.5|Représentation intermédiaire d'un compilateur.]] |[[File:Code interpreter scheme.png|centre|vignette|upright=1.5|Fonctionnement d'un interpréteur.]] |} Faire ainsi a de nombreux avantages, le principal étant d'avoir un compilateur capable de traduire un langage de haut niveau vers plusieurs jeux d'instructions différents. Cela permet d'avoir, par exemple, un compilateur qui traduit du C soit en code machine x86, soit en code machine pour un CPU ARM, soit pour un CPU POWERPC, etc. [[File:Compiler design.svg|centre|vignette|upright=3|Compiler design]] Pour l'interprétation, l'usage d'un ''bytecode'' a d'autres avantages. Pour l'exploiter, il faut distribuer non pas le code source, mais le ''bytecode''. En clair, l'interpréteur ne prend pas en entrée le code source, mais du ''bytecode'' déjà compilé. La traduction est alors beaucoup plus simple, car le code source a déjà été partiellement compilé, par la traduction en ''bytecode''. Interpréter du code source est en effet assez compliqué : il faut effectuer des étapes d'analyse lexicale, sémantique, et bien d'autres. Avec du ''bytecode'', ces étapes ont été réalisées lors de la compilation du ''bytecode'', l'interpréteur a alors peut de choses à faire. L'interprétation du ''bytecode'' se fait instruction par instruction, au niveau du ''bytecode''. Une instruction du ''bytecode'' est traduite en une instruction machine équivalente, qui est exécutée. Si l'instruction est un peu complexe, il exécute une fonction/procédure qui fait la même chose. Pour résumer, le ''bytecode'' est ensuite traduit à la volée et exécuté instruction par instruction par un logiciel appelé l'interpréteur. [[File:Control table.png|centre|vignette|upright=2|Control table]] Le langage intermédiaire peut être vu comme l'assembleur d'un processeur, n'existe pas forcément dans la réalité, mais dont le jeu d'instruction est décrit en détail. Le processeur en question est appelé une '''machine abstraite''', ou encore une ''machine virtuelle''. Nous utiliserons le terme de machine abstraite dans ce qui suit. La machine abstraite n'est pas la même suivant que l'on cible la compilation ou l'interprétation/JIT. Pour l'interprétation/JIT, la machine abstraite est souvent une machine à pile, car cela simplifie grandement la traduction en code machine final. L'interprétation demande que la traduction du ''bytecode'' en code machine soit la plus simple possible. Et cela demande de prendre en compte pas mal de détails. Par exemple, les différents jeux d'instruction existants n'ont pas le même nombre de registres, ce qui pose problème lors de la traduction du ''bytecode''. Lors de la transformation en code machine, un algorithme d'allocation de registres se débrouille pour traduire le code intermédiaire en code qui utilise un nombre limité de registres. Pour améliorer cette allocation de registres, il y a deux solutions niveau ''bytecode'' : soit utiliser un nombre illimité de registres, soit utiliser une machine à pile. Pour les machines à pile, il existe un algorithme simple et rapide pour traduire un code écrit pour une machine à pile en un code écrit pour un processeur avec des registres, qui se débrouille pas trop mal pour allouer efficacement les registres. C'est un avantage assez important pour les langages interprétés. ==Les processeurs qui exécutent du bytecode== Le ''bytecode'' est un code machine, ce qui signifie qu'il peut en théorie s'exécuter sur un processeur qui implémente le jeu d’instruction associé. Si le jeu d'instruction d'un ''bytecode'' est souvent une description censée être fictive, elle n'en reste pas moins un jeu d'instruction et des caractéristiques précises, qu'on peut l'implémenter en matériel ! Néanmoins, tous les ''bytecode'' ne sont pas égaux de ce point de vue. Certains sont faciles à implémenter en matériel, d'autres non. Les ''bytecodes'' implémentés en matériel sont ceux dont les machines abstraites sont des machines à pile. Un premier exemple est celui des processeurs Pascal MicroEngine, qui exécutaient directement le bytecode du langage Pascal, le fameux UCSD P-code. Un second exemple est la '''machine SECD''', qui sert de langage intermédiaire pour certains compilateurs de langages fonctionnels. Elle a été implémentée en matériel par plusieurs équipes, notamment par les chercheurs de l'université de Calgary en 1989. Dans le même genre, quelques processeurs simples étaient capables d’exécuter directement le ''bytecode'' du langage FORTH. Le FORTH, un des premiers langages à pile de haut niveau, possède de nombreuses implémentations hardware et est un des rares langages de haut niveau dont le ''bytecode'' a été utilisé comme langage machine sur certains processeurs. Par exemple, on peut citer le processeur FC16, capable d’exécuter nativement du ''bytecode'' FORTH. ===Les processeurs Java=== Le cas le plus impressionnant est celui de la machine virtuelle Java, qui est un design de processeur comme un autre. En temps normal, le ''bytecode'' Java est compilé ou interprété, mais certains processeurs exécutaient du ''bytecode'' Java directement, sans interprétation ni compilation. Ils sont appelés des '''processeurs Java'''. On peut citer les processeurs ARM disposant de l'extension Jazelle, du JEMCore et du aJ-100 de aJile Systems, le picoJava II de Sun, et quelques autres. Il y a aussi eu quelques projets de recherche ou processeurs open source, comme Komodo, jamuth, le ''Java Optimized Processor'' et quelques autres projets du même genre. L'implémentation de la machine virtuelle Java n'était cependant pas complète. Quelques instructions complexes n'étaient pas gérées par le processeur et devaient être émulées en logiciel. Mais la grosse majorité des instructions l'était. Les instructions non-supportées étaient émulées avec la méthode ''trap and emulate'', à savoir qu'un opcode inconnu déclenchait une exception matérielle ''opcode invalide'', dont la routine pouvait émuler les instructions qui doivent l'être. Le processeur aJ-100 de aJile est capable d'exécuter la quasi-totalité des instructions de la JVM Java, seules deux instructions faisant exception. Les '''accélérateurs Java''' sont une solution intermédiaire, qui permet de traduire à la volée du ''bytecode'' Java en instructions RISC, exécutées sur un autre processeur. Il s'agit formellement de traduction binaire, mais je préfère en parler ici. L'idée est de combiner un processeur RISC avec un coprocesseur Java. Le coprocesseur lit le ''bytecode'' Java et le traduit en instructions RISC, ces dernières étant exécutées sur le processeur RISC. Le JA108 de Nozomi était un coprocesseur de ce genre, au même titre que le coprocesseur JSTAR de JEDI. L'extension Jazelle de certains CPU ARM fonctionnait vraisemblablement sur un principe similaire, sauf que le coprocesseur était intégré dans le processeur ARM, entre le cache d'instruction et le décodeur d'instructions ARM. L'intérêt des processeurs Java n'est pas qu'une question de performance. Certes, ils permettent d’exécuter plus vite les programmes compilés en ''bytecode'', comme des programmes Java pour la JVM Java. Mais l'intérêt est aussi de faire des économies de mémoire RAM et ROM. Je rappelle que la machine virtuelle Java est une machine à pile, ce qui fait que sa densité de code est excellente. Les programmes en ''bytecode'' Java sont donc très petits, et prennent peu de place en mémoire ROM. De plus, utiliser un processeur Java permet de se passer d'interpréteur ou de JIT Java, ce qui économise encore plus de mémoire. ===Transformer un processeur RISC en machine à pile=== La plupart des processeurs Java utilisent en interne une architecture à registre généraux, qui émule une machine à registre en microcode. Quelques registres sont réservés pour l'état de la machine virtuelle, avec au minimum un registre réservé pour le pointeur de pile, un autre pour un pointeur vers un ''pool'' de constantes, et quelques autres. D'autres registres servent à mémoriser le haut de la pile, à savoir le sommet de la pile et quelques opérandes situées juste en-dessous. Par exemple, le processeur picoJava II gardait les 64 opérandes au sommet de la pile dans 64 registres généraux, séparés des registres pour les pointeurs et l'état de la JVM. Une optimisation possible, envisagée sur les processeurs picoJava, était une sorte de macro-fusion sous stéroïde. L'idée était de fusionner certaines séries d'instructions Java en une seule instruction machine. Typiquement, pour faire une opération arithmétique, une machine à pile empile deux opérandes et exécute une opération, ce qui prend trois instructions machine. La spécification picoJava 1 a proposé d'envoyer directement la seconde opérande en entrée de l'ALU, ce qui fusionnait l'empilement de la seconde opérande et l'opération arithmétique. Le processeur picoJava II va plus loin et fusionne les trois instructions en une seule instruction RISC, agissant sur des registres. Mais la majorité des processeurs Java ne faisaient pas cela, car cela complexifie grandement le décodeur. Les processeurs ''bigfoot'' de DCT utilisaient un système différent pour émuler une pile avec des registres. Leur banc de registre de 64 bits était coupé en deux : les 16 premiers registres étaient utilisés pour la pile, les 48 suivants étaient utilisés pour autre chose. L'idée est que le registre 0 mémorise un emplacement vide, le registre 1 contient le sommet de la pile, le registre 2 l'opérande sous le sommet de la pile, et ainsi de suite. Les 15 premières opérandes de la pile sont donc mémorisées dans les registres. Les instructions écrivent leur résultat dans le registre adéquat, puis tous les registres sont décalés pour que le résultat passe dans le registre R1. Par exemple, les instructions PUSH chargent une opérande dans le registre 0, puis tous les registres sont décalés de un rang, pour que l'opérande chargée soit dans le registre 1. Le registre 0 devient alors le registre numéro 1, le registre 1 devient le 2, et ainsi de suite. Par contre, les opérations arithmétiques font autrement. Une instruction d'addition, par exemple, lit les opérandes dans le registre 1 et 2, puis place son résultat dans le registre 2 (les deux opérandes sont dépilées). Puis, les registres sont décalés d'un rang pour que ce registre 2 devienne le 1. Les registres ne sont en réalité pas décalés, il n'y a pas de transferts entre registres. A la place, les 16 premiers registres utilisaient une forme limitée de renommage de registres, similaire au banc de registre tournant utilisé sur les processeurs EPIC comme l'Itanium. Les numéros de registres sont renommés, décalés, à chaque fois qu'on empile ou dépile une opérande. Pour faire le renommage de registres, processeur contient un ''register stack counter'' de 4 bits, qui est incrémenté à chaque fois qu'on empile une donnée, et est décrémenté quand on dépile une donnée. Quand on veut accéder à un opérande, le numéro de registre architectural associé est traduit en numéro de registre physique, en additionnant le ''register stack counter'' (modulo 16, pour rester sur 4 bits). ==L'accélération de l'interprétation/JIT : le ''Thumb-EE'' d'ARM== Quelques processeurs ont ajouté des instructions pour faciliter le travail des interpréteurs/JIT. Par exemple, en 2005, ARM a ajouté le mode ''Thumb Execution Environment'', qui faisait cela. Il reprenait le jeu d'instruction compact ''thumb'' et ajoutait quelques instructions et modifiait le comportement d'instructions existantes. Le jeu d'instruction ''Thumb-EE'' a été ajouté sur les CPU ARM en 2005, mais a été déprécié en 2011, par manque d'utilité. Mais la tentative mérite qu'on s'attarde dessus. Les CPU de gamme M, déstinés à l'embarqué, n'ont jamais supporté ''Thumb-EE'', vu que de tels processeurs ne sont pas conçus pour exécuter du code interprété/JIT. Le mode ''Thumb-EE'' est un mode d'exécution, séparé de l'ARM normal. Ainsi, une instruction ''Thumb-EE'' et une instruction ARM peuvent avoir le même encodage en binaire, mais se comporter différemment. Le processeur est à tout moment soit en mode ''Thumb-EE'', soit en mode ARM normal, ce qui précise comment ces instructions doivent se comporter. Des instructions ont été ajoutées par ''Thumb-EE'', pour qu'il fasse son travail. L'entrée et la sortie du mode ''Thumb-EE'' se fait avec deux instructions : ENTERX et LEAVEX. Une fois en mode ''Thumb-EE'', le décodage des instructions ''thumb'' se fait avec les règles du ''Thumb-EE'', et non celles du ''thumb'' normal. Une première différence entre ''thumb'' normal et ''Thumb-EE'' est l'ajout de ce qui s'appelle un ''null check'' pour les instructions mémoire. Le ''null check'' est utilisé pour les instructions en mode d'adressage "base + indice" ou "Base + décalage", ou tout autre mode d'adressage avec un registre de base utilisés dans des calculs d'adresse. L'idée est de vérifier si l'adresse de base vaut zéro ou non. Si c'est le cas, le processeur lève une exception matérielle, qui est traitée par l'interprétation ou le compilateur JIT. Une telle situation est en effet signe d'une erreur d'adressage mémoire, qui doit être traitée par l'interpréteur. Typiquement, c'est signe qu'un pointeur n'a pas bien été initialisé, ce qui peut arriver avec du code interprété ou JIT. Les interpréteurs/JIT traitent généralement la situation en ajoutant un test avant toute instruction mémoire, pour vérifier si le pointeur accédé vaut zéro ou non. Avec ''Thumb-EE'', pas besoin d'ajouter les tests en questions : ils sont réalisés automatiquement lors de chaque instruction mémoire. : Les registres pour la pile, comme le pointeur de pile et le pointeur de ''frame'', sont aussi concernés. L'instruction CHKA vérifie que les accès à un tableau ne débordent pas en dehors du tableau en question. Par exemple, pour un tableau de 1024 éléments, elle vérifie si l'indice est compris entre 0 et 1024. Pour cela, elle vérifie si l'indice de tableau est bien dans l'intervalle adéquat et lève une exception matérielle si ce n'est pas le cas. Son utilité se comprend quand on sait que tous les accès à un tableau sont vérifiés par l'interpréteur. L'interpréteur est censé ajouter des instructions pour vérifier les indices, à savoir deux ou trois branchements. Avec CHKA, le test se fait en une seule instruction. D'autres modifications mineures de l'encodage des instructions sont aussi présentes, ainsi que des modifications pour les instructions LOAD/STORE. Précisément, dans les modes "base + indice" et "base + décalage", le décalage et l'indice subissent maintenant des décalages. L'idée est de simplifier le calcul d'adresse. De quoi économiser quelques instructions lors des calculs d'adresse, ce qui facilite le travail de l'interpréteur/JIT. Et des instructions LOAD/STORE précises voient leur comportement modifié pour faciliter la gestion de la pile de Java ou l'exécution des méthodes locales. Pour faciliter le travail de l'interpréteur, deux instructions HBP et HBLP ont été ajoutées. Elles branchent vers une fonction qui gère les exceptions logicielles (une fonctionnalité présente dans de nombreux langages, comme Java). La première effectue un branchement vers la fonction en question, la seconde sauvegarde l'adresse de retour avant de faire ce branchement. Les deux peuvent brancher vers 256 fonctions pré-déterminées, sans avoir à présenter leur adresse. ==La traduction binaire accélérée par le matériel== Plus haut, nous avons surtout parlé des compilateurs et des interpréteurs. Cependant, nous devons aussi parler de la '''traduction binaire'''. Elle traduit un programme écrit dans un code machine vers un autre code machine. Par exemple, elle traduit un programme compilé pour un CPU x86 vers un code machine ARM. En général, le système d'exploitation est généralement compilé pour le jeu d'instruction natif, mais il exécute des applications prévues pour le x86. Les applications sont traduites par le système d'exploitation, avant d'exécuter le code traduit. : Nous parlerons dans la suite de code machine source et de code machine cible, pour parler respectivement du code à traduire et du code obtenu après traduction. Idem avec d'autres termes comme architecture cible/source, ou jeu d'instruction cible/source. La traduction binaire est surtout utilisée pour des questions d'émulation ou de compatibilité. Par exemple, lorsque les Macintosh sont passés de processeurs Power PC vers des processeurs x86, le système d'exploitation Mac OS utilisait la traduction binaire pour convertir les anciennes applications Power PC vers du code x86. Les utilisateurs n'y ont vu que du feu. Le système de traduction binaire était appelé Rosetta 1. Par la suite, lors de la transition de processeurs x86 vers des processeurs Apple, Rosetta 2 a vu le jour. La traduction binaire a surtout été utilisée pour traduire du code x86 vers un autre jeu d'instruction. Il faut dire que le x86 est le jeu d'instruction dominant. De nombreuses entreprises ont eu pour ambition de briser l'hégémonie du x86 sur PC, en remplaçant le x86 par un jeu d'instruction plus performant, tout en gardant une compatibilité maximale. La traduction binaire était la seule solution pratique. Elle était le plus souvent intégralement réalisée en logiciel, comme c'était le cas sur les architectures Itanium avec le ''IA-32 Execution Layer''. ===Les généralités sur la traduction binaire assistée en matériel=== L'architecture source est presque tout le temps une architecture CISC, assez ancienne, qu'on souhaite émuler. Le choix de l'architecture cible se porte souvent sur une architecture VLIW, fort différente de l'architecture source, et ce pour obtenir une économie de matériel conséquente. Pour s'exécuter rapidement, le code traduit doit exploiter le parallélisme d'instruction, à savoir exécuter plusieurs instructions en même temps dans des unités de calcul séparées. Il est possible d'utiliser un processeur superscalaire avec exécution dans le désordre pour cela, mais au prix d'un cout important en transistors. Alors qu'en utilisant un CPU VLIW, c'est le traducteur binaire qui fait tout le travail d'extraction du parallélisme d'instruction. Et après tout, quitte à avoir un traducteur binaire, autant lui refiler le boulot d'optimisation. Compiler du code à la volée est certes assez lourd, mais qu'il existe des algorithmes efficaces pour regrouper des instructions indépendantes dans une seule instruction VLIW. Au passage, le processeur VLIW a plus de registres que le processeur source. Cela permet de faire du renommage de registres directement en logiciel. Le traducteur binaire n'hésite pas à changer les noms de registres entre instructions source et instruction VLIW cible, afin de supprimer des dépendances de données. En conséquence, cela ouvre des opportunités de parallélisme, qui sont exploitées lors du regroupement des opérations en instructions VLIW. Vous pourriez penser que le choix d'un CPU VLIW pour émuler du CISC est tout sauf optimal. Vous devez penser que la traduction binaire est d'autant plus simple que l'architecture source et cibles sont semblables. Dans les faits, ce n'est pas tellement traduire les instructions qui pose problème, mais plus la gestion du registre d'état, des exceptions matérielles, de la mémoire virtuelle, la différence entre gros-boutisme et petit-boutisme, de même que des différences d'adressage pour les périphériques. Le premier problème est la gestion des conditions, notamment en présence d'un registre d'état. En pratique, la traduction binaire s'utilise pour traduire du code CISC vers du code RISC ou VLIW. Les architectures source ont donc un registre d'état, qui est mis à jour non seulement par des instructions de test, mais aussi des instructions arithmétiques. L'architecture cible n'a elle pas de registre d'état, mais des registres à prédicats. La traduction de l'un vers l'autre est alors quelque peu compliquée. Et elle est d'autant plus compliquée que le registre d'état est la source de dépendances d'instruction implicites, qui réduisent les performances. Par exemple, si une instruction arithmétique modifie le registre d'état, cela peut impacter l'exécution d'une instruction ultérieure, qui lit ce registre d'état. Pour éliminer ces fausses dépendances, le traducteur binaire doit renommer ce registre en logiciel et détecter les dépendances utiles. Et le compilateur doit gérer les cas où il y a beaucoup de distance entre les deux, voire les cas où l'instruction dépendante n'a pas encore été analysée par le compilateur. Un autre problème est lié à la gestion des exceptions matérielles, et précisément des exceptions précises. Pour rappel, une instruction VLIW regroupe plusieurs opérations, opérations qui correspondraient à une instruction machine sur un CPU pas VLIW. En conséquence, plusieurs instructions du langage machine "source" sont regroupées en une seule instruction VLIW. Et il faut tenir compte du cas où une instruction source lève une exception. Dans le code VLIW, cela signifie qu'une opération lève une exception, et il faut annuler partiellement l'instruction VLIW associée. Et par partiellement, on veut dire que seules les opérations suivantes dans l'ordre du programme source doivent être annulées, puis ré-exécutées. Et c'est un sacré casse-tête ! Les processeurs VLIW utilisent comme solution le mécanisme d'exceptions différées des processeurs EPIC, vu il y a quelques chapitres. Pour résumer, le code est exécuté par blocs d'instruction, délimités par des branchements ou tout autre limite/barrière pertinente dans le code. Un bloc de code est compilé en deux versions : une version rapide sans exceptions matérielles, une version lente qui gère les exceptions précises. La version rapide s'exécute sans exécuter les exceptions. Cependant, les exceptions matérielles sont enregistrées, pour être prises en compte à la toute fin du bloc de code. La version rapide est exécutée en premier et elle mémorise si une exception matérielle a eu lieu. Puis une instruction vérifie si une exception a eu lieu et décide quoi faire. Si aucune exception n'a eu lieu, elle passe à la suite du programme, les résultats du bloc de code sont définitivement acceptés. Mais si une exception a eu lieu, tout est annulé. Le processeur est remis dans l'état initial, puis le code est ré-exécuté instruction par instruction de manière à gérer l'exception correctement. ===La traduction par pré-décodage=== Une première solution serait de faire la traduction binaire dans le cache d'instruction. Lors d'un défaut de cache, le code chargé depuis la RAM est traduit en code machine cible. Et c'est ce code machine cible qui est mémorisé dans le cache d'instruction et exécuté par le processeur. La technique marche sur le papier et n'est qu'une amélioration des techniques de pré-décodage vues il y a quelques chapitres. Cependant, le code obtenu est une traduction assez basique, qui n'incorpore pas d'optimisations dignes de ce nom. De plus, elle gère mal le cas où la taille du code source et cible sont potentiellement très différentes. Concrètement, elle est surtout utile pour traduire du code d'un processeur RISC vers un autre processeur RISC, dont les tailles d'instructions sont similaires. Elle ne permet pas d'émuler plusieurs jeux d'instructions différents, le cout en matériel (un circuit de pré-décodage par jeu d'instruction) serait trop important. ===Le projet DAISY d'IBM=== De très rares processeurs étaient conçus pour accélérer cette traduction binaire, afin de garder de bonnes performances. Les premiers à avoir étudié l'idée étaient IBM, avec leur projet DAISY (''Dynamically Architected Instruction Set from Yorktown''). Le projet de base était de convertir à la volée du code compilé pour des CPU Power PC, vers du code VLIW. Les chercheurs d'IBM avaient développé un algorithme de traduction dynamique efficace, ainsi qu'un processeur VLIW disposant d'optimisations spécifiques à la traduction binaire dynamique, à la volée. De nombreuses idées de ce projet ont été reprises ou re-découvertes par la société Transmetta, avec ses processeurs Crusoe et Efficieon, puis par NVIDIA avec son projet Denver. Les ingénieurs de ce projet ont étudié la possibilité d'émuler plusieurs jeux d'instructions différents sur un même processeur, notamment le s390 d'IBM et le x86. En théorie, cela demande juste d'avoir plusieurs programmes de traduction binaire : un pour le x86, un autre pour le s390, éventuellement un autre pour le Power PC. Mais cela ne s'est pas concrétisé. Mais les ingénieurs ont étudié quelle pourrait être l'architecture VLIW idéale pour ça. Par exemple, il fallait des additions 3-opérandes pour simplifier les calculs d'adresse. Une difficulté était la gestion du registre d'état, dont les bits ne sont pas mis à jour de la même manière sur le x86 et le s390 ou le Power PC. La différence entre gros-boutisme et petit-boutisme était aussi un problème, de même que des différences d'adressage pour les périphériques. Pour que la traduction binaire soit efficace, le processeur VLIW intègre diverses optimisations, comme les exceptions différées et les branchements multi-voies. La principale est la suivante : seul le code des boucles ou fonctions exécutées fréquemment est traduit en code VLIW, le reste du code est interprété. En effet, traduire du code binaire prend plus de temps qu'une simple interprétation. Pour du code qui ne sera exécutée qu'une seule fois, il est plus rapide d'utiliser l'interprétation. Par contre, pour du code exécuté beaucoup de fois, le cout de la traduction binaire est amorti, dilué sur N exécutions, compensé par le gain en temps d'exécution de ce code traduit. Au final, cela permet de ne traduire que le code qui le mérite. La détection du code fréquemment exécuté est réalisée dans le cœur VLIW. L'unité de branchement mémorise les derniers branchements rencontrés et le nombre de fois qu'ils ont été exécutés. Si ils ont été exécutés un certain nombre de fois, l'unité de branchement lève une exception matérielle, qui invoque le traducteur binaire. La routine de cette exception est le traducteur binaire proprement dit. En clair, la traduction binaire est démarrée quand le processeur détecte qu'une fonction a été exécutée plus de N fois, via une exception matérielle dédiée. La mesure du nombre d'exécution d'un branchement se fait dans l'unité de calcul dédiée aux branchements, avec l'aide d'une sorte de ''branch adress buffer'' modifié. Pour rappel, le ''branch adress buffer'' mémorise le ''Program counter'' de chaque branchement récemment rencontré. Ici, chaque entrée du ''branch adress buffer'' est associée à un compteur incrémenté à chaque exécution du branchement. ===Les processeurs Crusoe et Efficieon de Transmetta=== Les processeurs Crusoe Et Efficeron sont deux processeurs VLIW produits par la société Transmetta, une société californienne rachetée par NVIDIA. Ils étaient conçus pour exécuter spécifiquement des programmes x86, système d'exploitation inclus. L'idée derrière ce projet était d'exécuter du code x86 sans que les applications, ni même le système d'exploitation et le BIOS soient au courant ! Les processeurs Transmetta étaient en réalité conçus pour exécuter un programme unique, le ''Code Morphing Software'' (CMS), qui traduisait le code x86 en code VLIW. Transmetta ne voulait pas que son processeur VLIW soit exploité directement, sans le CMS. Il n'avait pas rendu public de compilateur pour traduire du C vers du code VLIW, il n'avait pas donné la documentation du jeu d'instruction VLIW. Des efforts de rétro-ingénieurie, documentés sur le site realworldtech, ont cependant permis de comprendre comment étaient encodées les instructions du processeur Crusoe. Comme pour le projet Daisy, le ''Code Morphing Software'' utilisait à la fois interprétation et traduction binaire, selon les besoins. Les instructions sont interprétées lors de leurs premières exécutions, mais le CMS bascule sur de la traduction en code VLIW après un certain nombre d'exécution. Ainsi, les boucles souvent exécutées sont traduites en code VLIW, alors que le reste du code est interprété. Le choix du code à traduire est le fait du CMS, il utilise des heuristiques complexes pour, qui ne sont pas connues dans le détail. Le ''Code Morphing Software'' est mémorisé dans une EEPROM, ce qui en fait un ''firmware'' situé en dessous du BIOS. Le CMS démarre ensuite le BIOS, qui lui-même démarre le système d'exploitation, qui lui-même démarre les pilotes de périphériques et les programmes. Le CMS se réserve les 16 premiers mébioctets de l'espace d'adressage. L'EEPROM du CMS est mappée dedans, mais n'en utilise que la moitié. Le reste est utilisé comme cache, pour mémoriser le code VLIW traduit par le CMS. Les deux processeurs intégraient aussi deux mémoires SRAM, utilisées par le CMS, appelées la ''local program memory'' (LPM) et la ''local data memory'' (LDM). La première contient du code qui gére les interruptions, la mémoire virtuelle, les exceptions matérielles, les problèmes d'alignement mémoire, et quelques fonctions très fréquemment utilisées par le CMS. La seconde est de la mémoire RAM utilisée par ces fonctions de la ''local program memory''. Les mémoires caches L1 et L2 sont séparées de ces deux mémoires. Le processeur Crusoe et Efficieon étaient des processeurs VLIW très simples. Ils n'avaient même pas de MMU, ni de fonctionnalités importantes sur les CPU x86. Pour Crusoe, les instructions VLIW étaient encodées sur 64 ou 128 bits, et regroupaient entre 2 à 4 opérations. Les instructions LOAD/STORE ne supportaient pas d'adressage indicé, juste de l'adressage indirect à registre. Crusoe avait 5 unités de calcul : deux ALU entières, une unité LOAD/STORE, une unité de branchement, une FPU. Efficieon doublait le nombre d'ALU entière et d'unité LOAD/STORE, les instructions VLIW passaient à 256 bits. Le processeur Crusoe contient 160 registres, dont 64 registres généraux, 32 registres flottants. Les registres généraux font 32 bits, ce qui est cohérent avec le fait que les CPU x86 de l'époque étaient des processeurs 32 bits. Quant aux registres flottants, ils faisaient 80 bits, ce qui colle avec la taille des registres flottants de la FPU x87 utilisée à l'époque. Sur les 64 registres généraux, seuls 48 étaient réellement utilisable pour mémoriser des opérandes. Une partie des registres généraux étaient utilisés pour la gestion de la pile, d'autres pour mémoriser l'état du CPU x86 émulé, un registre était un registre zéro non accesible en écriture. La gestion des exceptions est optimisée avec un système d'exceptions différées, le même que celui décrit plus haut. Le processeur utilise un système similaire, pour effectuer des lectures anticipées, qui a été expliqué dans le chapitre sur les processeurs VLIW/EPIC. Sauf qu'il s'agit là d'une optimisation pour gagner en performances en général, pas une aide utile pour gérer des exceptions précises ou toute autre subtilité de la traduction binaire. Pour gérer les exceptions différées et les lectures anticipées, le processeur mémorise l'état du processeur avant de démarrer un bloc de code. Pour cela, le processeur contient des ''shadow registers'', 48 registres entiers et 16 registres flottants. Vous remarquerez qu'il y a autant de registres entiers que de ''shadow registers'' entiers. Les registres entiers/flottants sont mémorisés dans les ''shadow registers'' avant d'exécuter un bloc de code, afin que le processeur puisse revenir à l'état de base. Une autre fonctionnalité liée est le ''gated store buffer''. Le principe est d'éviter toute écriture en mémoire RAM, tant que le code VLIW n'a pas émis d'instruction ''commit''. L'idée est que les écritures sont conservées dans la ''store queue'' de l'unité LOAD/STORE. Pour rappel, la ''store queue'' met en attente les écritures soit tant que les écritures ne sont pas terminées, soit tant que la RAM est occupée. L'idée est que les écritures sont accumulées dans la ''store queue'' tant que l'instruction ''commit'' n'a pas eu lieu. Le ''store queue'' est "déversé dans la RAM" seulement quand l'instruction ''commit'' s'exécute. ===Les CPU NVIDIA Denver=== Nvidia a tenté quelque chose de similaire avec son projet Denver. NVIDIA a racheté l'entreprise Transmetta, ses brevets, et a tenté de refaire la même chose. Le projet initial était de traduire du code x86 vers un jeu d'instruction VLIW propriétaire, comme l'a fait Transmetta. Mais NVIDIA n'a pas réussi à acquérir la licence du jeu d'instruction x86 et s'est rabattu sur le jeu d'instruction ARM. Le processeur né de ce projet est le Tegra K1-64 CPU. Il s'agit d'un processeur composé d'un cœur VLIW, couplé à deux décodeurs d'instructions ARM. Les deux décodeurs ARM traduisent une paire d'instructions ARM consécutives en instructions VLIW, pour les exécuter sur le cœur VLIW. Le CPU peut fonctionner selon deux modes : ARM et VLIW. En somme, le processeur supporte deux jeux d'instruction : le jeu d'instruction ARM, et un jeu d'instruction VLIW. En mode ARM, les instructions ARM sont chargées depuis le cache, traduites en VLIW par les décodeurs ARM, puis exécutées par le cœur VLIW. En mode optimisé, le CPU exécute des instructions VLIW chargées depuis le cache d'instruction, les décodeurs ne sont pas utilisés. : Le CPU supporte en réalité deux jeux d'instruction ARM : ARM8 et 7. Mais ils correspondent au même mode pour le CPU. Un point important est que le processeur peut changer très rapidement de mode, en à peine quelques cycles d'horloges. On n'est clairement pas dans le cas des CPU x86, qui mettent des plombes pour passer du mode 32 à 64 bits et inversement. La commutation est tellement rapide qu'on peut considérer que le processeur supporte deux jeux d'instruction simultanément. Et le support simultané de deux jeux d'instruction facilite l'implémentation de la traduction binaire. Encore une fois, inutile de traduire du code qui ne sera exécutée qu'une seule fois, mieux vaut l'exécuter directement dans le mode ARM. Surtout que le processeur supporte un mode ARM qui permet d'exécuter du code ARM sans perte de performance. Par contre, le code fréquemment utilisé, notamment dans des boucles critiques, est traduit en VLIW. Au final, les performances sont optimisées, en limitant le travail du logiciel traducteur binaire. La détection du code fréquemment exécuté se fait différemment que pour les processeurs Transmetta et Daisy. L'unité de branchement ne détecte pas directement le code beaucoup exécuté, même si elle a un rôle à jouer. A la place, la détection est réalisée en logiciel, par un ''thread'' dédié, qui s'exécute sur un cœur séparé. Il détecte le code fréquemment utilisé en regardent les branchements exécutés récemment. Pour cela, le processeur mémorise l'historique des branchements pris récemment et copie cette historique dans une mémoire tampon dédiée, partagée entre tous les cœurs. Vu qu'elle est partagée, le ''thread'' de détection a accès à l'historique sans pertes de performances, seule la copie de l'historique dans ce tampon a un cout en performance, pas son partage. Le code VLIW obtenu après traduction binaire est mémorisé dans la mémoire RAM, dans une portion spécialement réservée pour. Elle fait 128 mébioctets, et est appelée le '''cache d'optimisation''' par NVIDIA. Le cache d'optimisation est protégé en écriture et seul le traducteur binaire peut écrire dedans. Au passage, le traducteur binaire est du code VLIW, ce qui fait qu'il est placé dans le cache d'optimisation. Le processeur détecte automatiquement quand une fonction pour laquelle le code compilé équivalent est disponible. Pour cela, l'unité de chargement contient une table de correspondance entre l'adresse de la fonction ARM, et l'adresse de son équivalent compilé. Quand une fonction est appelée, l'unité de chargement regarde l'adresse de destination du branchement. Si l'adresse est dans cette table, elle récupère l'adresse de la fonction compilée et branche vers celle-ci. Le processeur dispose d'un cache d'instruction de 128 kibioctets, ce qui est très important, mais nécessaire vu la taille des instructions VLIW. Le coeur VLIW dispose de ses propres décodeurs, d'un ''scoreboard'', de 7 unités de calcul et de registres. Pour les unités de calcul, il a deux FPU, deux ALU, une unité de branchement, et deux unités LOAD-STORE faisant aussi ALU. Un point important est que le CPU dispose de 64 registres entiers et de 64 registres flottants. C'est deux fois plus que supporte le jeu d'instruction ARM. En mode ARM, seule la moitié des registres est utilisée. Mais en mode optimisé, le traducteur binaire utilise bien les 64 registres, grâce à une sorte de renommage de registres logicielle. Pour simplifier la traduction binaire, le processeur supporte les techniques vues précédemment. Le processeur intègre un ''gated store buffer'' similaire à celui des processeurs Transmetta. Les techniques d'exceptions différées sont aussi supportées, comme sur les processeurs Transmetta. Il a aussi une unité de préchargement décrite par NVIDIA comme agressive, avec support du préchargement de type anticipé (''runahead''). ===L'émulation de la mémoire virtuelle=== Pour finir, il faut parler de la mémoire virtuelle avec la traduction binaire. Sur le processeur source, le processeur gère à la fois des adresses physiques et virtuelles et sa MMU fait la conversion entre les deux. Sur le processeur VLIW, la MMU est simulée par le programme de traduction binaire, partiellement ou totalement. L'implémentation exacte varie suivant que l'on parle du projet Daisy, des processeurs Transmetta ou de Denver. Sur les processeurs Transmetta, il n'y a pas de MMU ni de mémoire virtuelle. A la place, le programme de traduction binaire émule la mémoire virtuelle du processeur source. Le processeur VLIW ne gère que des adresses physiques, rien d'autre. L'espace d'adressage physique du processeur VLIW a la même taille que celui du processeur émulé, ici des processeurs x86. Il est vraisemblable que les adresses physiques utilisées par le processeur x86 sont les mêmes que celles du processeur VLIW. Sur le projet Daisy, le processeur VLIW gère la mémoire virtuelle via pagination, comme les processeurs émulés. Ce qui fait qu'il gère un espace d'adressage virtuel et un espace d'adressage physique. Pour éviter toute confusion, nous parlerons d'adresse physique/virtuelle VLIW pour les adresses physiques/virtuelles du processeur VLIW, d'adresse virtuelle/physique source pour celle du jeu d'instruction traduit, à savoir du Power PC ou du s390, éventuellement de l'x86. Il faut alors faire le lien entre adresses physiques source et adresses virtuelles VLIW. Pour cela, rien de plus simple : il y a correspondance parfaite. L'adresse physique source numéro N correspond à l'adresse virtuelle VLIW numéro N. Cependant, cela signifie que tout l'espace d'adressage virtuel VLIW serait utilisé par le code à traduire. En réalité, il faut ajouter de la place pour le code traduit et le programme de traduction binaire. La conséquence est que l'espace d'adressage virtuel VLIW est plus large que l'espace d'adressage source. L'espace d'adressage virtuel VLIW est découpé en trois sections : une section pour le code Power PC à traduire, une autre pour le code traduit en VLIW, et une dernière réservée au traducteur binaire. Le programme de traduction est placé dans une ROM mappée en mémoire dans la seconde section. Le reste de la seconde section est réservé à une mémoire RAM utilisée par le programme de traduction. [[File:Mémoire virtuelle sur le projet Daisy.png|centre|vignette|upright=1.5|Mémoire virtuelle sur le projet Daisy]] Il faut noter que chaque page physique source, une fois traduite, correspond à N pages virtuelles VLIW. La raison est que le code traduit prend plus de place que le code machine originel. En conséquence, on doit utiliser une page finale plus grande. Pour le reste, des pages contiguës en mémoire physique source sont elles aussi contiguës en mémoire virtuelle VLIW. Le calcul d'adresse est donc simple : l'adresse physique source est multipliée par N, puis on ajoute l'adresse de base à laquelle commence la section pour le code traduit. ==Les processeurs à jeu d'instruction reconfigurable== Nous venons de voir comment accélérer en matériel la traduction binaire. Mais il existe une alternative qui permet de se passer de traduction binaire, complétement. L'idée est d'avoir un processeur avec un '''jeu d'instruction reconfigurable'''. Le nom est assez parlent : on peut configurer le processeur de manière à supporter le jeu d'instruction de son choix ! Par exemple, on pouvait reconfigurer un CPU de manière à ce qu'il supporte le jeu d'instruction x86, ou qu'il exécute du ''bytecode'' Java, ou du code ARMv8, etc. De tels processeurs sont encore plus rare que ceux vu avant dans ce chapitre, mais ils ont existé ! Les processeurs de ce type avaient un ''microcode réinscriptible'', à savoir que c'était des processeurs microcodés pour lesquels on pouvait modifier le microcode. L'idée était qu'en changeant le microcode, on changeait le jeu d'instruction du processeur. Pour cela, le microcode était associé à une mémoire SRAM dans laquelle le microcode était copié au démarrage du CPU. Il y avait donc une micro-ROM et une micro-RAM. Les instructions étaient décodées en utilisant le microcode dans la micro-SRAM, la micro-ROM ne servait qu'au démarrage. En modifiant le contenu de la micro-SRAM, on pouvait altérer le microcode, voire le remplacer totalement. ===Le Burroughs B1700 : introduction=== Un exemple est celui du Burroughs B1700, et il est intéressant d'étudier en détail son architecture. Son architecture est décrite dans le livre "Interpreting Machines : Architecture and Programming of the Bl700/Bl800 Series", écrit par Elliott I. Organick et James A. Hinds. Une partie de ce qui vfa suivre est un très court résumé de ce livre. Il disposait de 4 registres généraux, nommés X, Y, T et L, tous de 24 bits. Les registres T et L étaient découpés en 6 sous-registres de 4 bits, nommés LA LB LC LD LE et LF pour le registre L, et TA TB TC TD TE et TF pour le registre T. Ils étaient adressables comme n'importe quel registre. Un point important est que le registre T avait des possibilités d’extraction spécifiques. Il était possible d'extrait N bits, placés n'importe où dans ce registre, et de les copier dans un autre registre. Nous verrons pourquoi cette possibilité est intéressante et ne sert que pour le registre T. Les registres X et Y étaient reliés à une ALU de 24 bits capable d'effectuer des opérations arithmétiques basiques. Fait étonnant, l'ALU faisait tous les calculs en même temps et fournissait ses résultats dans 7 registres : un registre SUM pour l'addition, un registre DIFFERENCE pour la soustraction, et un registre par opération bit à bit. Les registres pour les opérations bit à bit sont : CMPX et CMPY pour l'opération NOT sur chaque opérande, XANY pour le ET bit à bit, XEOY pour le XOR, XORY pour le OU logique. Les additions et soustractions pouvaient se faire sur des opérandes codés en binaire ou en BCD, suivant le code opération utilisé. A cela, il fallait ajouter deux registres de "résultat" MSKX et MSKY. Ils fournissaient un masque dépendant de la taille du résultat en bits. Cela permettait de gérer des données de 8 ou 16 bits, alors que l'ALU faisait 24 bits. Et cela n'était pas limité à 16 ou 8 bits, mais n'importe quelle taille inférieure ou égale à 24 bits était supportée, même des tailles non-conventionnelles comme des résultats codés sur 3 bits, 9 bits, 12 bits, etc. L'ALU fournissait 12 conditions, qui alimentait un registre d'état. Le registre d'état était segmenté en trois registres appelés XYST, XYCN et BICN. Le processeur contenait de nombreux autres registres de 4 bits, dont les sous-registres de T et L. En tout, il y a 27 registres de 4 bits adressables ! Et le processeur disposait d'une ALU 4 bits pour manipuler ces registres de 4 bits ! Elle est capable de faire des MOV entre registres de 4 bits, des opérations bit à bit, et peut aussi tester si un bit vaut 0 ou 1. La dernière possibilité est très utile pour implémenter les branchements. Pour communiquer avec les entrées-sorties, il disposait de deux registres CMND et DATA : CMND pour envoyer une commande à une entrée-sortie, DATA pour lire ou écrire une donnée. Pour la mémoire, il avait deux registres séparés : READ et WRITE. READ contient la donnée lue lors d'une lecture,; WRITE est pour une donnée à écrire lors d’une écriture. Il s'agit de registres d’interfaçage mémoire, qui ont été promus au rang de registres architecturaux. Les deux registres sont gérés par le microcode, qui est le langage principal de la machine. Les instructions machines configurées font sans, mais le microcode doit y avoir accès facilement. Et vu que le microcode est le langage principal de ce processeur, ces deux registres deviennent architecturaux. ===Une architecture bit-adressable=== Si avoir un microcode réinscriptible est suffisant, quelques choix de ''design'' facilitent l'émulation de jeux d'instructions variés. Par exemple, le CPU Burroughs B1700 avait ajouté une fonctionnalité très importante : il était bit-adressable. En clair, il pouvait adresser la mémoire bit par bit, si nécessaire. L'avantage est que cela permettait d'émuler des architectures dont la taille des registres/''byte''/mots était très différente. Un autre avantage est que cela facilitait la gestion des instructions de taille variable. Détaillons rapidement le premier avantage. Le processeur était un CPU 24 bits, mais il pouvait émuler des processeurs 16 bits, 8 bits, 4 bits, 1 bit, 9 bits, ou toute autre valeur. Pour cela, le processeur lisait des mots de 24 bits et utilisant un ''barrel shifter'' pour sélectionner les bits adéquats. Par exemple, pour un accès de 16 bits, la donnée lue en mémoire était masquée de manière à ne garder que les 16 bits de poids faible. Le ''barrel shifter''/circuit de masquage était placé directement avant le bus mémoire, sur le trajet des données/instructions. Le ''barrel shifter'' était commandé par un registre ''Field Unit'', qui précisait quelle était la taille des données à charger. ===La gestion de la taille des instructions=== Les instructions à décoder n'ont pas de longueur fixe, à savoir qu'elles peuvent faire 16 bit avec tel jeu d'instruction, 8 bits avec un autre, 32 bits sur un autre, etc. Elles peuvent aussi être de longueur fixe ou variable ! Le B1700 intégrait donc un support des instructions de longueur variable, qui était "détourné" pour gérer des jeux d'instruction différents. Notons que vu que l'architecture est bit-adressable, les contraintes d'alignement des instructions disparaissent, ce qui a des avantages qu'on va détailler dans ce qui suit ! L'implémentation des instructions de taille variable ajoutait un registre avant le registre d'instruction. Le registre d'instruction proprement dit était un registre M de 24 bits. Il était précédé par un registre T, dans lequel l'instruction était assemblée, en combinant plusieurs morceaux. Si l'instruction est codée est codée sur moins de 24 bits, le registre T se contente de masquer les bits en trop. Si l'instruction chargée dépasser 24 bits, le chargement se faisait en plusieurs passes, avec plusieurs lectures en mémoire. Dans ce qui suit, on suppose que l'instruction est composée de '''champs''' : l'opcode, les opérandes, les adresses absolues, les constantes immédiates, les numéros de registre, etc. L'idée est que l'instruction est chargée dans le registre T, puis découpée en champs, qui sont copiés dans les registres du processeur ou envoyés au microcode. Je rappelle que le registre T est relié à un ''barrel shifter'', différent au précédent, qui permettait de masquer, décaler et extraire une donnée de ce registre. Et il y a la possibilité d'extraire une suite de bit de ce registre T, pour la copier dans un autre registre. Par exemple, prenons une instruction composée d'un opcode, d'une constante immédiate et d'une adresse immédiate. L'opcode est lu et interprété par le microcode. Le microcode extrait la constante immédiate du registre T et la copie dans le registre X. Ensuite, l'adresse est extraite et envoyée au registre FA, une lecture est démarrée, et l'opérande est copiée dans le registre Y. Enfin, l'instruction est exécutée par le microcode. Le tout prend plusieurs micro-opérations : une première pour lire l'opcode, une seconde pour lire la première opérande, une troisième pour lire la seconde opérande, una quatrième pour tout exécuter. Notons que suivant la taille des champs, on peut en charger plusieurs à la fois. Par exemple, imaginons qu'on ait chargé 24 bits, contenant un opcode de 8 bits et une constante immédiate de 16 bits : pas besoin de faire deux accès mémoire, le registre T est juste découpé en un opcode et une constante. Le registre T est fait pour ce genre d'extraction. ===Le chargement d'un champ=== L'architecture est bit-adressable car elle gére des champs qui ne font pas forcément 8 ou 16 bits. Par exemple, certains jeux d'instructions ont des opcodes codés sur 6 ou 7 bits, voire 9-10 bits. Par exemple, prenons un opcode de 7 bits, suivi par une adresse de 12 bits et une constante immédiate de 15 bits : aucun de ces champs ne sera aligné sur 8/16/24 bits. Utiliser une architecture bit-adressable règle le problème d'alignement. Et en général, les contraintes d'alignement des instructions disparaissent ! Quand on charge une instruction, peut importe qu'elle soit ou non alignée sur 24 bits, elle sera placée dans les bits de poids faible, il suffira de masquer les bits de poids fort inutiles. Par exemple, si on charge une instruction de 16 bits, les 16 bits seront copiés dans les 16 bits de poids faible du registre T, il suffira de masquer l'octet de poids fort. Pas besoin de faire de décalages pour gérer des instructions mal alignées... Pour charger une instruction, il faut connaitre plusieurs informations : l'adresse de son premier bit, sa taille. Pour cela, le B1700 contient deux registres, nommés ''Field Adress'' (FA) et ''Field Length'' (FL), qui indiquent l'adresse du premier bit et la taille de l'instruction à charger. Le premier fait 24 bits, ce qui permet de gérer des adresses de 24 bits. N'oubliez pas que le processeur est bit-adressable, ce qui permet d'adresser 16 Mébi-bits, soit 2 mébioctets. Le registre pour la taille fait lui 16 bits, ce qui permet de gérer des instructions de 65536 bits, soit 8192 octets ! Leur nom commence par ''field'', car le processeur peut gérer des instructions de taille variable et/ou plus longues que 24 bits. Dans ce cas, le chargement de l'instruction se fait champ par champ. Pour donner un exemple, prenons une instruction composée de 20 champs de 12 bits chacun. Les champs sont chargés un par un dans le registre T. Le registre FA est alors initialisé avec l'adresse de l'instruction, il est incrémenté de 12 à chaque cycle. Le registre FL est initialisé avec la taille de l'instruction, soit 20 * 12 = 240 bits, et il est décrémenté de 16 à chaque cycle. Précisons cependant que la taille d'un champ peut être changée d'un cycle à l'autre. Par exemple, le processeur peut charger un opcode de 8 bits, l'interpréter, détecter que l'instruction demande ensuite de charger une adresse de 16 bits. Dans ce cas, le registre FL est immédiatement altéré pour charger 16 bits, au lieu des 8 bits de l'opcode immédiatement précédent. ==Les jeux d'instructions dédiés à un langage de programmation== De rares processeurs sont conçus pour un langage de programmation en particulier. On appelle ces processeurs, conçus pour des besoins particuliers, des '''processeurs dédiés'''. Par exemple, les fameux ''Burrough E-mode'' B5000/B6000/B7000 étaient spécialement conçus pour exécuter de l'ALGOL-60. Leurs cousins B2000/B3000/B4000 étaient eux conçus pour le COBOL. Des langages fonctionnels ont aussi eu droit à leurs processeurs dédiés. Le prolog en est un bel exemple, avec les superordinateurs de 5ème génération qui lui étaient dédié. On peut aussi citer les machines LISP, dédiés au langage LISP, qui datent des années 1970. Elles étaient capables d’exécuter certaines fonctions de base du langage directement dans leurs circuits : elles possédaient notamment un ''garbage collector'' câblé dans ses circuits ainsi que des instructions machines supportant un typage déterminé à l’exécution. Les processeurs dédiés ont eu leur heure de gloire au début de l'informatique, à une époque où les langages de haut niveau venaient d'être inventés. À cette époque, les compilateurs n'étaient pas performants et ne savaient pas bien optimiser le code machine. Il était alors rationnel, pour l'époque, de rapprocher le code machine cible et le langage de programmation de haut niveau. De nombreuses architectures dédiés ont ainsi été inventées, avant que les concepteurs se rendent compte des défauts de cette approche. Les défauts en question ne sont pas nombreux, mais assez simples à comprendre. Premièrement, elles sont très rapides pour un langage de programmation en particulier, mais sont assez mauvaises pour les autres, d'où un problème de "compatibilité". Ajoutons à cela que les langages de programmation peuvent évoluer, devenir de moins en moins populaires/utilisés, ce qui rend la création d'architectures généralistes plus pertinente. Enfin, les architectures dédiées sont évidemment des processeurs CISC, pour implémenter les nombreuses fonctionnalités des langages évolués. Et les défauts des CISC sont assez rédhibitoires à l'heure actuelle. À l'heure actuelle, les algorithmes des compilateurs se sont améliorés et savent nettement mieux utiliser le matériel. Ils produisent du code machine efficace, ce qui rend les architecture dédiées bien moins intéressantes. Si on ajoute les défauts de ces architectures dédiées, par étonnant que les architectures dédiées aient presque disparues. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'accélération matérielle de la virtualisation | prevText=L'accélération matérielle de la virtualisation | next=Le matériel réseau | nextText=Le matériel réseau }} </noinclude> 66uxb6tau6qxlzap3e2fj0iiy62g3tq 764018 764016 2026-04-19T16:52:21Z Mewtow 31375 /* Le Burroughs B1700 : introduction */ 764018 wikitext text/x-wiki De nos jours, la majorité des programmeurs programment dans des langages de haut niveau. Il est très rare pour eux d'avoir à utiliser de l'assembleur, et encore moins un langage machine. Les programmes écrits dans un langage de haut niveau sont traduits en langage machine par un logiciel appelé le compilateur. [[File:Compilation.PNG|centre|vignette|upright=2.5|Compilateur : principe.]] Il se trouve que quelques rares processeurs sont conçus pour faciliter le travail du compilateur. Et ce sont ces processeurs que nous allons voir dans ce qui suit. Ils représentent une catégorie de jeux d'instruction à part, qui n'a pas vraiment de nom. ==La compilation et l’interprétation== Un compilateur traduit donc du code source, écrit dans un langage de haut niveau, vers du langage machine. La traduction est rarement directe. En général, le compilateur traduit le code source en assembleur, qui est lui-même traduit en langage machine. L'assembleur est une représentation textuelle du langage machine alors que le code machine est du binaire exécutable par le processeur. La traduction se fait donc en deux étapes, la '''compilation''' proprement dite et l''''assemblage''', réalisées respectivement par un compilateur et un assembleur. Il faut parfois rajouter une troisième phase d'édition des liens, que nous passons volontairement sous silence. {| |[[File:Code C.png|vignette|200px|Le code source, ici en C.]] |[[File:Assem.png|vignette|250px|Le code assembleur.]] |[[File:Binary file - hello world (C programming).png|vignette|150px|Le code machine.]] |} ===L'interprétation et la compilation à la volée=== Une alternative à la compilation est l''''interprétation''', qui transforme le code source en code machine à la volée. Avec l'interprétation, le code source est passé à un logiciel appelé l''''interpréteur''', qui exécute le code source ligne de code par ligne de code. Une ligne de code est traduite en code machine, qui est exécutée, puis l'interpréteur passe à la ligne suivante. [[File:Kaantotulkkaus.png|centre|vignette|upright=3|Compilation versus interprétation.]] Un défaut de l'interprétation est son cout en performance, qui est assez important. Aussi, l'interprétation stricte a évolué vers un hybride entre interprétation et compilation. L'idée est qu'une partie peu importante du code source est interprétée, alors que le code important est compilé juste avant d'être exécuté. Pour le dire autrement, le code source est partiellement compilé à la volée, juste avant son exécution. Aussi, on parle de '''compilation à la volée'''. Le terme en anglais est ''Just In Time compilation'', abrévié en JIT. En soi, le JIT est associé à de l'interprétation, les JIT sont en réalité des hybrides interpréteurs-compilateurs. Un point important est que compiler du code à la volée est assez lourd, cela a un cout en performance. Par contre, le code compilé s'exécute plus vite que du code interprété. Si on exécute le code compilé une seule fois, le cout de la compilation l'emporte sur le gain à l'exécution. Mais le code compilé est réutilisé autant de fois que nécessaire. Le gain à l'exécution est donc multiplié par le nombre d'exécutions, alors que le cout de la compilation est répartit sur plusieurs exécutions, il est amorti. Les portions du code sont choisies de manière à avoir un gain en performance maximal. L'idée générale est que le code exécuté très souvent est compilé, alors que du code exécuté pas souvent est interprété. Typiquement, les boucles les plus souvent exécutées sont compilées, le code exécuté une seule fois est interprété. ===Les langages intermédiaires=== Les compilateurs modernes passent par un '''langage intermédiaire''' pour faire la transformation en code machine. Le compilateur traduit le langage de haut niveau en langage intermédiaire, puis traduit le langage intermédiaire en code machine cible. Le langage intermédiaire est parfois appelé le '''''bytecode''''', et ce terme recouvre aussi le code écrit avec du ''bytecode''. L'interprétation aussi peut passer par l'intermédiaire d'un un ''bytecode'', ce n'est pas limité aux compilateurs proprement dit. {| |[[File:Intermediate representation scheme.png|centre|vignette|upright=1.5|Représentation intermédiaire d'un compilateur.]] |[[File:Code interpreter scheme.png|centre|vignette|upright=1.5|Fonctionnement d'un interpréteur.]] |} Faire ainsi a de nombreux avantages, le principal étant d'avoir un compilateur capable de traduire un langage de haut niveau vers plusieurs jeux d'instructions différents. Cela permet d'avoir, par exemple, un compilateur qui traduit du C soit en code machine x86, soit en code machine pour un CPU ARM, soit pour un CPU POWERPC, etc. [[File:Compiler design.svg|centre|vignette|upright=3|Compiler design]] Pour l'interprétation, l'usage d'un ''bytecode'' a d'autres avantages. Pour l'exploiter, il faut distribuer non pas le code source, mais le ''bytecode''. En clair, l'interpréteur ne prend pas en entrée le code source, mais du ''bytecode'' déjà compilé. La traduction est alors beaucoup plus simple, car le code source a déjà été partiellement compilé, par la traduction en ''bytecode''. Interpréter du code source est en effet assez compliqué : il faut effectuer des étapes d'analyse lexicale, sémantique, et bien d'autres. Avec du ''bytecode'', ces étapes ont été réalisées lors de la compilation du ''bytecode'', l'interpréteur a alors peut de choses à faire. L'interprétation du ''bytecode'' se fait instruction par instruction, au niveau du ''bytecode''. Une instruction du ''bytecode'' est traduite en une instruction machine équivalente, qui est exécutée. Si l'instruction est un peu complexe, il exécute une fonction/procédure qui fait la même chose. Pour résumer, le ''bytecode'' est ensuite traduit à la volée et exécuté instruction par instruction par un logiciel appelé l'interpréteur. [[File:Control table.png|centre|vignette|upright=2|Control table]] Le langage intermédiaire peut être vu comme l'assembleur d'un processeur, n'existe pas forcément dans la réalité, mais dont le jeu d'instruction est décrit en détail. Le processeur en question est appelé une '''machine abstraite''', ou encore une ''machine virtuelle''. Nous utiliserons le terme de machine abstraite dans ce qui suit. La machine abstraite n'est pas la même suivant que l'on cible la compilation ou l'interprétation/JIT. Pour l'interprétation/JIT, la machine abstraite est souvent une machine à pile, car cela simplifie grandement la traduction en code machine final. L'interprétation demande que la traduction du ''bytecode'' en code machine soit la plus simple possible. Et cela demande de prendre en compte pas mal de détails. Par exemple, les différents jeux d'instruction existants n'ont pas le même nombre de registres, ce qui pose problème lors de la traduction du ''bytecode''. Lors de la transformation en code machine, un algorithme d'allocation de registres se débrouille pour traduire le code intermédiaire en code qui utilise un nombre limité de registres. Pour améliorer cette allocation de registres, il y a deux solutions niveau ''bytecode'' : soit utiliser un nombre illimité de registres, soit utiliser une machine à pile. Pour les machines à pile, il existe un algorithme simple et rapide pour traduire un code écrit pour une machine à pile en un code écrit pour un processeur avec des registres, qui se débrouille pas trop mal pour allouer efficacement les registres. C'est un avantage assez important pour les langages interprétés. ==Les processeurs qui exécutent du bytecode== Le ''bytecode'' est un code machine, ce qui signifie qu'il peut en théorie s'exécuter sur un processeur qui implémente le jeu d’instruction associé. Si le jeu d'instruction d'un ''bytecode'' est souvent une description censée être fictive, elle n'en reste pas moins un jeu d'instruction et des caractéristiques précises, qu'on peut l'implémenter en matériel ! Néanmoins, tous les ''bytecode'' ne sont pas égaux de ce point de vue. Certains sont faciles à implémenter en matériel, d'autres non. Les ''bytecodes'' implémentés en matériel sont ceux dont les machines abstraites sont des machines à pile. Un premier exemple est celui des processeurs Pascal MicroEngine, qui exécutaient directement le bytecode du langage Pascal, le fameux UCSD P-code. Un second exemple est la '''machine SECD''', qui sert de langage intermédiaire pour certains compilateurs de langages fonctionnels. Elle a été implémentée en matériel par plusieurs équipes, notamment par les chercheurs de l'université de Calgary en 1989. Dans le même genre, quelques processeurs simples étaient capables d’exécuter directement le ''bytecode'' du langage FORTH. Le FORTH, un des premiers langages à pile de haut niveau, possède de nombreuses implémentations hardware et est un des rares langages de haut niveau dont le ''bytecode'' a été utilisé comme langage machine sur certains processeurs. Par exemple, on peut citer le processeur FC16, capable d’exécuter nativement du ''bytecode'' FORTH. ===Les processeurs Java=== Le cas le plus impressionnant est celui de la machine virtuelle Java, qui est un design de processeur comme un autre. En temps normal, le ''bytecode'' Java est compilé ou interprété, mais certains processeurs exécutaient du ''bytecode'' Java directement, sans interprétation ni compilation. Ils sont appelés des '''processeurs Java'''. On peut citer les processeurs ARM disposant de l'extension Jazelle, du JEMCore et du aJ-100 de aJile Systems, le picoJava II de Sun, et quelques autres. Il y a aussi eu quelques projets de recherche ou processeurs open source, comme Komodo, jamuth, le ''Java Optimized Processor'' et quelques autres projets du même genre. L'implémentation de la machine virtuelle Java n'était cependant pas complète. Quelques instructions complexes n'étaient pas gérées par le processeur et devaient être émulées en logiciel. Mais la grosse majorité des instructions l'était. Les instructions non-supportées étaient émulées avec la méthode ''trap and emulate'', à savoir qu'un opcode inconnu déclenchait une exception matérielle ''opcode invalide'', dont la routine pouvait émuler les instructions qui doivent l'être. Le processeur aJ-100 de aJile est capable d'exécuter la quasi-totalité des instructions de la JVM Java, seules deux instructions faisant exception. Les '''accélérateurs Java''' sont une solution intermédiaire, qui permet de traduire à la volée du ''bytecode'' Java en instructions RISC, exécutées sur un autre processeur. Il s'agit formellement de traduction binaire, mais je préfère en parler ici. L'idée est de combiner un processeur RISC avec un coprocesseur Java. Le coprocesseur lit le ''bytecode'' Java et le traduit en instructions RISC, ces dernières étant exécutées sur le processeur RISC. Le JA108 de Nozomi était un coprocesseur de ce genre, au même titre que le coprocesseur JSTAR de JEDI. L'extension Jazelle de certains CPU ARM fonctionnait vraisemblablement sur un principe similaire, sauf que le coprocesseur était intégré dans le processeur ARM, entre le cache d'instruction et le décodeur d'instructions ARM. L'intérêt des processeurs Java n'est pas qu'une question de performance. Certes, ils permettent d’exécuter plus vite les programmes compilés en ''bytecode'', comme des programmes Java pour la JVM Java. Mais l'intérêt est aussi de faire des économies de mémoire RAM et ROM. Je rappelle que la machine virtuelle Java est une machine à pile, ce qui fait que sa densité de code est excellente. Les programmes en ''bytecode'' Java sont donc très petits, et prennent peu de place en mémoire ROM. De plus, utiliser un processeur Java permet de se passer d'interpréteur ou de JIT Java, ce qui économise encore plus de mémoire. ===Transformer un processeur RISC en machine à pile=== La plupart des processeurs Java utilisent en interne une architecture à registre généraux, qui émule une machine à registre en microcode. Quelques registres sont réservés pour l'état de la machine virtuelle, avec au minimum un registre réservé pour le pointeur de pile, un autre pour un pointeur vers un ''pool'' de constantes, et quelques autres. D'autres registres servent à mémoriser le haut de la pile, à savoir le sommet de la pile et quelques opérandes situées juste en-dessous. Par exemple, le processeur picoJava II gardait les 64 opérandes au sommet de la pile dans 64 registres généraux, séparés des registres pour les pointeurs et l'état de la JVM. Une optimisation possible, envisagée sur les processeurs picoJava, était une sorte de macro-fusion sous stéroïde. L'idée était de fusionner certaines séries d'instructions Java en une seule instruction machine. Typiquement, pour faire une opération arithmétique, une machine à pile empile deux opérandes et exécute une opération, ce qui prend trois instructions machine. La spécification picoJava 1 a proposé d'envoyer directement la seconde opérande en entrée de l'ALU, ce qui fusionnait l'empilement de la seconde opérande et l'opération arithmétique. Le processeur picoJava II va plus loin et fusionne les trois instructions en une seule instruction RISC, agissant sur des registres. Mais la majorité des processeurs Java ne faisaient pas cela, car cela complexifie grandement le décodeur. Les processeurs ''bigfoot'' de DCT utilisaient un système différent pour émuler une pile avec des registres. Leur banc de registre de 64 bits était coupé en deux : les 16 premiers registres étaient utilisés pour la pile, les 48 suivants étaient utilisés pour autre chose. L'idée est que le registre 0 mémorise un emplacement vide, le registre 1 contient le sommet de la pile, le registre 2 l'opérande sous le sommet de la pile, et ainsi de suite. Les 15 premières opérandes de la pile sont donc mémorisées dans les registres. Les instructions écrivent leur résultat dans le registre adéquat, puis tous les registres sont décalés pour que le résultat passe dans le registre R1. Par exemple, les instructions PUSH chargent une opérande dans le registre 0, puis tous les registres sont décalés de un rang, pour que l'opérande chargée soit dans le registre 1. Le registre 0 devient alors le registre numéro 1, le registre 1 devient le 2, et ainsi de suite. Par contre, les opérations arithmétiques font autrement. Une instruction d'addition, par exemple, lit les opérandes dans le registre 1 et 2, puis place son résultat dans le registre 2 (les deux opérandes sont dépilées). Puis, les registres sont décalés d'un rang pour que ce registre 2 devienne le 1. Les registres ne sont en réalité pas décalés, il n'y a pas de transferts entre registres. A la place, les 16 premiers registres utilisaient une forme limitée de renommage de registres, similaire au banc de registre tournant utilisé sur les processeurs EPIC comme l'Itanium. Les numéros de registres sont renommés, décalés, à chaque fois qu'on empile ou dépile une opérande. Pour faire le renommage de registres, processeur contient un ''register stack counter'' de 4 bits, qui est incrémenté à chaque fois qu'on empile une donnée, et est décrémenté quand on dépile une donnée. Quand on veut accéder à un opérande, le numéro de registre architectural associé est traduit en numéro de registre physique, en additionnant le ''register stack counter'' (modulo 16, pour rester sur 4 bits). ==L'accélération de l'interprétation/JIT : le ''Thumb-EE'' d'ARM== Quelques processeurs ont ajouté des instructions pour faciliter le travail des interpréteurs/JIT. Par exemple, en 2005, ARM a ajouté le mode ''Thumb Execution Environment'', qui faisait cela. Il reprenait le jeu d'instruction compact ''thumb'' et ajoutait quelques instructions et modifiait le comportement d'instructions existantes. Le jeu d'instruction ''Thumb-EE'' a été ajouté sur les CPU ARM en 2005, mais a été déprécié en 2011, par manque d'utilité. Mais la tentative mérite qu'on s'attarde dessus. Les CPU de gamme M, déstinés à l'embarqué, n'ont jamais supporté ''Thumb-EE'', vu que de tels processeurs ne sont pas conçus pour exécuter du code interprété/JIT. Le mode ''Thumb-EE'' est un mode d'exécution, séparé de l'ARM normal. Ainsi, une instruction ''Thumb-EE'' et une instruction ARM peuvent avoir le même encodage en binaire, mais se comporter différemment. Le processeur est à tout moment soit en mode ''Thumb-EE'', soit en mode ARM normal, ce qui précise comment ces instructions doivent se comporter. Des instructions ont été ajoutées par ''Thumb-EE'', pour qu'il fasse son travail. L'entrée et la sortie du mode ''Thumb-EE'' se fait avec deux instructions : ENTERX et LEAVEX. Une fois en mode ''Thumb-EE'', le décodage des instructions ''thumb'' se fait avec les règles du ''Thumb-EE'', et non celles du ''thumb'' normal. Une première différence entre ''thumb'' normal et ''Thumb-EE'' est l'ajout de ce qui s'appelle un ''null check'' pour les instructions mémoire. Le ''null check'' est utilisé pour les instructions en mode d'adressage "base + indice" ou "Base + décalage", ou tout autre mode d'adressage avec un registre de base utilisés dans des calculs d'adresse. L'idée est de vérifier si l'adresse de base vaut zéro ou non. Si c'est le cas, le processeur lève une exception matérielle, qui est traitée par l'interprétation ou le compilateur JIT. Une telle situation est en effet signe d'une erreur d'adressage mémoire, qui doit être traitée par l'interpréteur. Typiquement, c'est signe qu'un pointeur n'a pas bien été initialisé, ce qui peut arriver avec du code interprété ou JIT. Les interpréteurs/JIT traitent généralement la situation en ajoutant un test avant toute instruction mémoire, pour vérifier si le pointeur accédé vaut zéro ou non. Avec ''Thumb-EE'', pas besoin d'ajouter les tests en questions : ils sont réalisés automatiquement lors de chaque instruction mémoire. : Les registres pour la pile, comme le pointeur de pile et le pointeur de ''frame'', sont aussi concernés. L'instruction CHKA vérifie que les accès à un tableau ne débordent pas en dehors du tableau en question. Par exemple, pour un tableau de 1024 éléments, elle vérifie si l'indice est compris entre 0 et 1024. Pour cela, elle vérifie si l'indice de tableau est bien dans l'intervalle adéquat et lève une exception matérielle si ce n'est pas le cas. Son utilité se comprend quand on sait que tous les accès à un tableau sont vérifiés par l'interpréteur. L'interpréteur est censé ajouter des instructions pour vérifier les indices, à savoir deux ou trois branchements. Avec CHKA, le test se fait en une seule instruction. D'autres modifications mineures de l'encodage des instructions sont aussi présentes, ainsi que des modifications pour les instructions LOAD/STORE. Précisément, dans les modes "base + indice" et "base + décalage", le décalage et l'indice subissent maintenant des décalages. L'idée est de simplifier le calcul d'adresse. De quoi économiser quelques instructions lors des calculs d'adresse, ce qui facilite le travail de l'interpréteur/JIT. Et des instructions LOAD/STORE précises voient leur comportement modifié pour faciliter la gestion de la pile de Java ou l'exécution des méthodes locales. Pour faciliter le travail de l'interpréteur, deux instructions HBP et HBLP ont été ajoutées. Elles branchent vers une fonction qui gère les exceptions logicielles (une fonctionnalité présente dans de nombreux langages, comme Java). La première effectue un branchement vers la fonction en question, la seconde sauvegarde l'adresse de retour avant de faire ce branchement. Les deux peuvent brancher vers 256 fonctions pré-déterminées, sans avoir à présenter leur adresse. ==La traduction binaire accélérée par le matériel== Plus haut, nous avons surtout parlé des compilateurs et des interpréteurs. Cependant, nous devons aussi parler de la '''traduction binaire'''. Elle traduit un programme écrit dans un code machine vers un autre code machine. Par exemple, elle traduit un programme compilé pour un CPU x86 vers un code machine ARM. En général, le système d'exploitation est généralement compilé pour le jeu d'instruction natif, mais il exécute des applications prévues pour le x86. Les applications sont traduites par le système d'exploitation, avant d'exécuter le code traduit. : Nous parlerons dans la suite de code machine source et de code machine cible, pour parler respectivement du code à traduire et du code obtenu après traduction. Idem avec d'autres termes comme architecture cible/source, ou jeu d'instruction cible/source. La traduction binaire est surtout utilisée pour des questions d'émulation ou de compatibilité. Par exemple, lorsque les Macintosh sont passés de processeurs Power PC vers des processeurs x86, le système d'exploitation Mac OS utilisait la traduction binaire pour convertir les anciennes applications Power PC vers du code x86. Les utilisateurs n'y ont vu que du feu. Le système de traduction binaire était appelé Rosetta 1. Par la suite, lors de la transition de processeurs x86 vers des processeurs Apple, Rosetta 2 a vu le jour. La traduction binaire a surtout été utilisée pour traduire du code x86 vers un autre jeu d'instruction. Il faut dire que le x86 est le jeu d'instruction dominant. De nombreuses entreprises ont eu pour ambition de briser l'hégémonie du x86 sur PC, en remplaçant le x86 par un jeu d'instruction plus performant, tout en gardant une compatibilité maximale. La traduction binaire était la seule solution pratique. Elle était le plus souvent intégralement réalisée en logiciel, comme c'était le cas sur les architectures Itanium avec le ''IA-32 Execution Layer''. ===Les généralités sur la traduction binaire assistée en matériel=== L'architecture source est presque tout le temps une architecture CISC, assez ancienne, qu'on souhaite émuler. Le choix de l'architecture cible se porte souvent sur une architecture VLIW, fort différente de l'architecture source, et ce pour obtenir une économie de matériel conséquente. Pour s'exécuter rapidement, le code traduit doit exploiter le parallélisme d'instruction, à savoir exécuter plusieurs instructions en même temps dans des unités de calcul séparées. Il est possible d'utiliser un processeur superscalaire avec exécution dans le désordre pour cela, mais au prix d'un cout important en transistors. Alors qu'en utilisant un CPU VLIW, c'est le traducteur binaire qui fait tout le travail d'extraction du parallélisme d'instruction. Et après tout, quitte à avoir un traducteur binaire, autant lui refiler le boulot d'optimisation. Compiler du code à la volée est certes assez lourd, mais qu'il existe des algorithmes efficaces pour regrouper des instructions indépendantes dans une seule instruction VLIW. Au passage, le processeur VLIW a plus de registres que le processeur source. Cela permet de faire du renommage de registres directement en logiciel. Le traducteur binaire n'hésite pas à changer les noms de registres entre instructions source et instruction VLIW cible, afin de supprimer des dépendances de données. En conséquence, cela ouvre des opportunités de parallélisme, qui sont exploitées lors du regroupement des opérations en instructions VLIW. Vous pourriez penser que le choix d'un CPU VLIW pour émuler du CISC est tout sauf optimal. Vous devez penser que la traduction binaire est d'autant plus simple que l'architecture source et cibles sont semblables. Dans les faits, ce n'est pas tellement traduire les instructions qui pose problème, mais plus la gestion du registre d'état, des exceptions matérielles, de la mémoire virtuelle, la différence entre gros-boutisme et petit-boutisme, de même que des différences d'adressage pour les périphériques. Le premier problème est la gestion des conditions, notamment en présence d'un registre d'état. En pratique, la traduction binaire s'utilise pour traduire du code CISC vers du code RISC ou VLIW. Les architectures source ont donc un registre d'état, qui est mis à jour non seulement par des instructions de test, mais aussi des instructions arithmétiques. L'architecture cible n'a elle pas de registre d'état, mais des registres à prédicats. La traduction de l'un vers l'autre est alors quelque peu compliquée. Et elle est d'autant plus compliquée que le registre d'état est la source de dépendances d'instruction implicites, qui réduisent les performances. Par exemple, si une instruction arithmétique modifie le registre d'état, cela peut impacter l'exécution d'une instruction ultérieure, qui lit ce registre d'état. Pour éliminer ces fausses dépendances, le traducteur binaire doit renommer ce registre en logiciel et détecter les dépendances utiles. Et le compilateur doit gérer les cas où il y a beaucoup de distance entre les deux, voire les cas où l'instruction dépendante n'a pas encore été analysée par le compilateur. Un autre problème est lié à la gestion des exceptions matérielles, et précisément des exceptions précises. Pour rappel, une instruction VLIW regroupe plusieurs opérations, opérations qui correspondraient à une instruction machine sur un CPU pas VLIW. En conséquence, plusieurs instructions du langage machine "source" sont regroupées en une seule instruction VLIW. Et il faut tenir compte du cas où une instruction source lève une exception. Dans le code VLIW, cela signifie qu'une opération lève une exception, et il faut annuler partiellement l'instruction VLIW associée. Et par partiellement, on veut dire que seules les opérations suivantes dans l'ordre du programme source doivent être annulées, puis ré-exécutées. Et c'est un sacré casse-tête ! Les processeurs VLIW utilisent comme solution le mécanisme d'exceptions différées des processeurs EPIC, vu il y a quelques chapitres. Pour résumer, le code est exécuté par blocs d'instruction, délimités par des branchements ou tout autre limite/barrière pertinente dans le code. Un bloc de code est compilé en deux versions : une version rapide sans exceptions matérielles, une version lente qui gère les exceptions précises. La version rapide s'exécute sans exécuter les exceptions. Cependant, les exceptions matérielles sont enregistrées, pour être prises en compte à la toute fin du bloc de code. La version rapide est exécutée en premier et elle mémorise si une exception matérielle a eu lieu. Puis une instruction vérifie si une exception a eu lieu et décide quoi faire. Si aucune exception n'a eu lieu, elle passe à la suite du programme, les résultats du bloc de code sont définitivement acceptés. Mais si une exception a eu lieu, tout est annulé. Le processeur est remis dans l'état initial, puis le code est ré-exécuté instruction par instruction de manière à gérer l'exception correctement. ===La traduction par pré-décodage=== Une première solution serait de faire la traduction binaire dans le cache d'instruction. Lors d'un défaut de cache, le code chargé depuis la RAM est traduit en code machine cible. Et c'est ce code machine cible qui est mémorisé dans le cache d'instruction et exécuté par le processeur. La technique marche sur le papier et n'est qu'une amélioration des techniques de pré-décodage vues il y a quelques chapitres. Cependant, le code obtenu est une traduction assez basique, qui n'incorpore pas d'optimisations dignes de ce nom. De plus, elle gère mal le cas où la taille du code source et cible sont potentiellement très différentes. Concrètement, elle est surtout utile pour traduire du code d'un processeur RISC vers un autre processeur RISC, dont les tailles d'instructions sont similaires. Elle ne permet pas d'émuler plusieurs jeux d'instructions différents, le cout en matériel (un circuit de pré-décodage par jeu d'instruction) serait trop important. ===Le projet DAISY d'IBM=== De très rares processeurs étaient conçus pour accélérer cette traduction binaire, afin de garder de bonnes performances. Les premiers à avoir étudié l'idée étaient IBM, avec leur projet DAISY (''Dynamically Architected Instruction Set from Yorktown''). Le projet de base était de convertir à la volée du code compilé pour des CPU Power PC, vers du code VLIW. Les chercheurs d'IBM avaient développé un algorithme de traduction dynamique efficace, ainsi qu'un processeur VLIW disposant d'optimisations spécifiques à la traduction binaire dynamique, à la volée. De nombreuses idées de ce projet ont été reprises ou re-découvertes par la société Transmetta, avec ses processeurs Crusoe et Efficieon, puis par NVIDIA avec son projet Denver. Les ingénieurs de ce projet ont étudié la possibilité d'émuler plusieurs jeux d'instructions différents sur un même processeur, notamment le s390 d'IBM et le x86. En théorie, cela demande juste d'avoir plusieurs programmes de traduction binaire : un pour le x86, un autre pour le s390, éventuellement un autre pour le Power PC. Mais cela ne s'est pas concrétisé. Mais les ingénieurs ont étudié quelle pourrait être l'architecture VLIW idéale pour ça. Par exemple, il fallait des additions 3-opérandes pour simplifier les calculs d'adresse. Une difficulté était la gestion du registre d'état, dont les bits ne sont pas mis à jour de la même manière sur le x86 et le s390 ou le Power PC. La différence entre gros-boutisme et petit-boutisme était aussi un problème, de même que des différences d'adressage pour les périphériques. Pour que la traduction binaire soit efficace, le processeur VLIW intègre diverses optimisations, comme les exceptions différées et les branchements multi-voies. La principale est la suivante : seul le code des boucles ou fonctions exécutées fréquemment est traduit en code VLIW, le reste du code est interprété. En effet, traduire du code binaire prend plus de temps qu'une simple interprétation. Pour du code qui ne sera exécutée qu'une seule fois, il est plus rapide d'utiliser l'interprétation. Par contre, pour du code exécuté beaucoup de fois, le cout de la traduction binaire est amorti, dilué sur N exécutions, compensé par le gain en temps d'exécution de ce code traduit. Au final, cela permet de ne traduire que le code qui le mérite. La détection du code fréquemment exécuté est réalisée dans le cœur VLIW. L'unité de branchement mémorise les derniers branchements rencontrés et le nombre de fois qu'ils ont été exécutés. Si ils ont été exécutés un certain nombre de fois, l'unité de branchement lève une exception matérielle, qui invoque le traducteur binaire. La routine de cette exception est le traducteur binaire proprement dit. En clair, la traduction binaire est démarrée quand le processeur détecte qu'une fonction a été exécutée plus de N fois, via une exception matérielle dédiée. La mesure du nombre d'exécution d'un branchement se fait dans l'unité de calcul dédiée aux branchements, avec l'aide d'une sorte de ''branch adress buffer'' modifié. Pour rappel, le ''branch adress buffer'' mémorise le ''Program counter'' de chaque branchement récemment rencontré. Ici, chaque entrée du ''branch adress buffer'' est associée à un compteur incrémenté à chaque exécution du branchement. ===Les processeurs Crusoe et Efficieon de Transmetta=== Les processeurs Crusoe Et Efficeron sont deux processeurs VLIW produits par la société Transmetta, une société californienne rachetée par NVIDIA. Ils étaient conçus pour exécuter spécifiquement des programmes x86, système d'exploitation inclus. L'idée derrière ce projet était d'exécuter du code x86 sans que les applications, ni même le système d'exploitation et le BIOS soient au courant ! Les processeurs Transmetta étaient en réalité conçus pour exécuter un programme unique, le ''Code Morphing Software'' (CMS), qui traduisait le code x86 en code VLIW. Transmetta ne voulait pas que son processeur VLIW soit exploité directement, sans le CMS. Il n'avait pas rendu public de compilateur pour traduire du C vers du code VLIW, il n'avait pas donné la documentation du jeu d'instruction VLIW. Des efforts de rétro-ingénieurie, documentés sur le site realworldtech, ont cependant permis de comprendre comment étaient encodées les instructions du processeur Crusoe. Comme pour le projet Daisy, le ''Code Morphing Software'' utilisait à la fois interprétation et traduction binaire, selon les besoins. Les instructions sont interprétées lors de leurs premières exécutions, mais le CMS bascule sur de la traduction en code VLIW après un certain nombre d'exécution. Ainsi, les boucles souvent exécutées sont traduites en code VLIW, alors que le reste du code est interprété. Le choix du code à traduire est le fait du CMS, il utilise des heuristiques complexes pour, qui ne sont pas connues dans le détail. Le ''Code Morphing Software'' est mémorisé dans une EEPROM, ce qui en fait un ''firmware'' situé en dessous du BIOS. Le CMS démarre ensuite le BIOS, qui lui-même démarre le système d'exploitation, qui lui-même démarre les pilotes de périphériques et les programmes. Le CMS se réserve les 16 premiers mébioctets de l'espace d'adressage. L'EEPROM du CMS est mappée dedans, mais n'en utilise que la moitié. Le reste est utilisé comme cache, pour mémoriser le code VLIW traduit par le CMS. Les deux processeurs intégraient aussi deux mémoires SRAM, utilisées par le CMS, appelées la ''local program memory'' (LPM) et la ''local data memory'' (LDM). La première contient du code qui gére les interruptions, la mémoire virtuelle, les exceptions matérielles, les problèmes d'alignement mémoire, et quelques fonctions très fréquemment utilisées par le CMS. La seconde est de la mémoire RAM utilisée par ces fonctions de la ''local program memory''. Les mémoires caches L1 et L2 sont séparées de ces deux mémoires. Le processeur Crusoe et Efficieon étaient des processeurs VLIW très simples. Ils n'avaient même pas de MMU, ni de fonctionnalités importantes sur les CPU x86. Pour Crusoe, les instructions VLIW étaient encodées sur 64 ou 128 bits, et regroupaient entre 2 à 4 opérations. Les instructions LOAD/STORE ne supportaient pas d'adressage indicé, juste de l'adressage indirect à registre. Crusoe avait 5 unités de calcul : deux ALU entières, une unité LOAD/STORE, une unité de branchement, une FPU. Efficieon doublait le nombre d'ALU entière et d'unité LOAD/STORE, les instructions VLIW passaient à 256 bits. Le processeur Crusoe contient 160 registres, dont 64 registres généraux, 32 registres flottants. Les registres généraux font 32 bits, ce qui est cohérent avec le fait que les CPU x86 de l'époque étaient des processeurs 32 bits. Quant aux registres flottants, ils faisaient 80 bits, ce qui colle avec la taille des registres flottants de la FPU x87 utilisée à l'époque. Sur les 64 registres généraux, seuls 48 étaient réellement utilisable pour mémoriser des opérandes. Une partie des registres généraux étaient utilisés pour la gestion de la pile, d'autres pour mémoriser l'état du CPU x86 émulé, un registre était un registre zéro non accesible en écriture. La gestion des exceptions est optimisée avec un système d'exceptions différées, le même que celui décrit plus haut. Le processeur utilise un système similaire, pour effectuer des lectures anticipées, qui a été expliqué dans le chapitre sur les processeurs VLIW/EPIC. Sauf qu'il s'agit là d'une optimisation pour gagner en performances en général, pas une aide utile pour gérer des exceptions précises ou toute autre subtilité de la traduction binaire. Pour gérer les exceptions différées et les lectures anticipées, le processeur mémorise l'état du processeur avant de démarrer un bloc de code. Pour cela, le processeur contient des ''shadow registers'', 48 registres entiers et 16 registres flottants. Vous remarquerez qu'il y a autant de registres entiers que de ''shadow registers'' entiers. Les registres entiers/flottants sont mémorisés dans les ''shadow registers'' avant d'exécuter un bloc de code, afin que le processeur puisse revenir à l'état de base. Une autre fonctionnalité liée est le ''gated store buffer''. Le principe est d'éviter toute écriture en mémoire RAM, tant que le code VLIW n'a pas émis d'instruction ''commit''. L'idée est que les écritures sont conservées dans la ''store queue'' de l'unité LOAD/STORE. Pour rappel, la ''store queue'' met en attente les écritures soit tant que les écritures ne sont pas terminées, soit tant que la RAM est occupée. L'idée est que les écritures sont accumulées dans la ''store queue'' tant que l'instruction ''commit'' n'a pas eu lieu. Le ''store queue'' est "déversé dans la RAM" seulement quand l'instruction ''commit'' s'exécute. ===Les CPU NVIDIA Denver=== Nvidia a tenté quelque chose de similaire avec son projet Denver. NVIDIA a racheté l'entreprise Transmetta, ses brevets, et a tenté de refaire la même chose. Le projet initial était de traduire du code x86 vers un jeu d'instruction VLIW propriétaire, comme l'a fait Transmetta. Mais NVIDIA n'a pas réussi à acquérir la licence du jeu d'instruction x86 et s'est rabattu sur le jeu d'instruction ARM. Le processeur né de ce projet est le Tegra K1-64 CPU. Il s'agit d'un processeur composé d'un cœur VLIW, couplé à deux décodeurs d'instructions ARM. Les deux décodeurs ARM traduisent une paire d'instructions ARM consécutives en instructions VLIW, pour les exécuter sur le cœur VLIW. Le CPU peut fonctionner selon deux modes : ARM et VLIW. En somme, le processeur supporte deux jeux d'instruction : le jeu d'instruction ARM, et un jeu d'instruction VLIW. En mode ARM, les instructions ARM sont chargées depuis le cache, traduites en VLIW par les décodeurs ARM, puis exécutées par le cœur VLIW. En mode optimisé, le CPU exécute des instructions VLIW chargées depuis le cache d'instruction, les décodeurs ne sont pas utilisés. : Le CPU supporte en réalité deux jeux d'instruction ARM : ARM8 et 7. Mais ils correspondent au même mode pour le CPU. Un point important est que le processeur peut changer très rapidement de mode, en à peine quelques cycles d'horloges. On n'est clairement pas dans le cas des CPU x86, qui mettent des plombes pour passer du mode 32 à 64 bits et inversement. La commutation est tellement rapide qu'on peut considérer que le processeur supporte deux jeux d'instruction simultanément. Et le support simultané de deux jeux d'instruction facilite l'implémentation de la traduction binaire. Encore une fois, inutile de traduire du code qui ne sera exécutée qu'une seule fois, mieux vaut l'exécuter directement dans le mode ARM. Surtout que le processeur supporte un mode ARM qui permet d'exécuter du code ARM sans perte de performance. Par contre, le code fréquemment utilisé, notamment dans des boucles critiques, est traduit en VLIW. Au final, les performances sont optimisées, en limitant le travail du logiciel traducteur binaire. La détection du code fréquemment exécuté se fait différemment que pour les processeurs Transmetta et Daisy. L'unité de branchement ne détecte pas directement le code beaucoup exécuté, même si elle a un rôle à jouer. A la place, la détection est réalisée en logiciel, par un ''thread'' dédié, qui s'exécute sur un cœur séparé. Il détecte le code fréquemment utilisé en regardent les branchements exécutés récemment. Pour cela, le processeur mémorise l'historique des branchements pris récemment et copie cette historique dans une mémoire tampon dédiée, partagée entre tous les cœurs. Vu qu'elle est partagée, le ''thread'' de détection a accès à l'historique sans pertes de performances, seule la copie de l'historique dans ce tampon a un cout en performance, pas son partage. Le code VLIW obtenu après traduction binaire est mémorisé dans la mémoire RAM, dans une portion spécialement réservée pour. Elle fait 128 mébioctets, et est appelée le '''cache d'optimisation''' par NVIDIA. Le cache d'optimisation est protégé en écriture et seul le traducteur binaire peut écrire dedans. Au passage, le traducteur binaire est du code VLIW, ce qui fait qu'il est placé dans le cache d'optimisation. Le processeur détecte automatiquement quand une fonction pour laquelle le code compilé équivalent est disponible. Pour cela, l'unité de chargement contient une table de correspondance entre l'adresse de la fonction ARM, et l'adresse de son équivalent compilé. Quand une fonction est appelée, l'unité de chargement regarde l'adresse de destination du branchement. Si l'adresse est dans cette table, elle récupère l'adresse de la fonction compilée et branche vers celle-ci. Le processeur dispose d'un cache d'instruction de 128 kibioctets, ce qui est très important, mais nécessaire vu la taille des instructions VLIW. Le coeur VLIW dispose de ses propres décodeurs, d'un ''scoreboard'', de 7 unités de calcul et de registres. Pour les unités de calcul, il a deux FPU, deux ALU, une unité de branchement, et deux unités LOAD-STORE faisant aussi ALU. Un point important est que le CPU dispose de 64 registres entiers et de 64 registres flottants. C'est deux fois plus que supporte le jeu d'instruction ARM. En mode ARM, seule la moitié des registres est utilisée. Mais en mode optimisé, le traducteur binaire utilise bien les 64 registres, grâce à une sorte de renommage de registres logicielle. Pour simplifier la traduction binaire, le processeur supporte les techniques vues précédemment. Le processeur intègre un ''gated store buffer'' similaire à celui des processeurs Transmetta. Les techniques d'exceptions différées sont aussi supportées, comme sur les processeurs Transmetta. Il a aussi une unité de préchargement décrite par NVIDIA comme agressive, avec support du préchargement de type anticipé (''runahead''). ===L'émulation de la mémoire virtuelle=== Pour finir, il faut parler de la mémoire virtuelle avec la traduction binaire. Sur le processeur source, le processeur gère à la fois des adresses physiques et virtuelles et sa MMU fait la conversion entre les deux. Sur le processeur VLIW, la MMU est simulée par le programme de traduction binaire, partiellement ou totalement. L'implémentation exacte varie suivant que l'on parle du projet Daisy, des processeurs Transmetta ou de Denver. Sur les processeurs Transmetta, il n'y a pas de MMU ni de mémoire virtuelle. A la place, le programme de traduction binaire émule la mémoire virtuelle du processeur source. Le processeur VLIW ne gère que des adresses physiques, rien d'autre. L'espace d'adressage physique du processeur VLIW a la même taille que celui du processeur émulé, ici des processeurs x86. Il est vraisemblable que les adresses physiques utilisées par le processeur x86 sont les mêmes que celles du processeur VLIW. Sur le projet Daisy, le processeur VLIW gère la mémoire virtuelle via pagination, comme les processeurs émulés. Ce qui fait qu'il gère un espace d'adressage virtuel et un espace d'adressage physique. Pour éviter toute confusion, nous parlerons d'adresse physique/virtuelle VLIW pour les adresses physiques/virtuelles du processeur VLIW, d'adresse virtuelle/physique source pour celle du jeu d'instruction traduit, à savoir du Power PC ou du s390, éventuellement de l'x86. Il faut alors faire le lien entre adresses physiques source et adresses virtuelles VLIW. Pour cela, rien de plus simple : il y a correspondance parfaite. L'adresse physique source numéro N correspond à l'adresse virtuelle VLIW numéro N. Cependant, cela signifie que tout l'espace d'adressage virtuel VLIW serait utilisé par le code à traduire. En réalité, il faut ajouter de la place pour le code traduit et le programme de traduction binaire. La conséquence est que l'espace d'adressage virtuel VLIW est plus large que l'espace d'adressage source. L'espace d'adressage virtuel VLIW est découpé en trois sections : une section pour le code Power PC à traduire, une autre pour le code traduit en VLIW, et une dernière réservée au traducteur binaire. Le programme de traduction est placé dans une ROM mappée en mémoire dans la seconde section. Le reste de la seconde section est réservé à une mémoire RAM utilisée par le programme de traduction. [[File:Mémoire virtuelle sur le projet Daisy.png|centre|vignette|upright=1.5|Mémoire virtuelle sur le projet Daisy]] Il faut noter que chaque page physique source, une fois traduite, correspond à N pages virtuelles VLIW. La raison est que le code traduit prend plus de place que le code machine originel. En conséquence, on doit utiliser une page finale plus grande. Pour le reste, des pages contiguës en mémoire physique source sont elles aussi contiguës en mémoire virtuelle VLIW. Le calcul d'adresse est donc simple : l'adresse physique source est multipliée par N, puis on ajoute l'adresse de base à laquelle commence la section pour le code traduit. ==Les processeurs à jeu d'instruction reconfigurable== Nous venons de voir comment accélérer en matériel la traduction binaire. Mais il existe une alternative qui permet de se passer de traduction binaire, complétement. L'idée est d'avoir un processeur avec un '''jeu d'instruction reconfigurable'''. Le nom est assez parlent : on peut configurer le processeur de manière à supporter le jeu d'instruction de son choix ! Par exemple, on pouvait reconfigurer un CPU de manière à ce qu'il supporte le jeu d'instruction x86, ou qu'il exécute du ''bytecode'' Java, ou du code ARMv8, etc. De tels processeurs sont encore plus rare que ceux vu avant dans ce chapitre, mais ils ont existé ! Les processeurs de ce type avaient un ''microcode réinscriptible'', à savoir que c'était des processeurs microcodés pour lesquels on pouvait modifier le microcode. L'idée était qu'en changeant le microcode, on changeait le jeu d'instruction du processeur. Pour cela, le microcode était associé à une mémoire SRAM dans laquelle le microcode était copié au démarrage du CPU. Il y avait donc une micro-ROM et une micro-RAM. Les instructions étaient décodées en utilisant le microcode dans la micro-SRAM, la micro-ROM ne servait qu'au démarrage. En modifiant le contenu de la micro-SRAM, on pouvait altérer le microcode, voire le remplacer totalement. ===Le Burroughs B1700 : architecture externe=== Un exemple est celui du Burroughs B1700, et il est intéressant d'étudier en détail son architecture. Son architecture est décrite dans le livre "Interpreting Machines : Architecture and Programming of the Bl700/Bl800 Series", écrit par Elliott I. Organick et James A. Hinds. Une partie de ce qui vfa suivre est un très court résumé de ce livre. Il disposait de 4 registres généraux, nommés X, Y, T et L, tous de 24 bits. Les registres T et L étaient découpés en 6 sous-registres de 4 bits, nommés LA LB LC LD LE et LF pour le registre L, et TA TB TC TD TE et TF pour le registre T. Ils étaient adressables comme n'importe quel registre. Un point important est que le registre T avait des possibilités d’extraction spécifiques. Il était possible d'extrait N bits, placés n'importe où dans ce registre, et de les copier dans un autre registre. Nous verrons pourquoi cette possibilité est intéressante et ne sert que pour le registre T. Les registres X et Y étaient reliés à une ALU de 24 bits capable d'effectuer des opérations arithmétiques basiques. Fait étonnant, l'ALU faisait tous les calculs en même temps et fournissait ses résultats dans 7 registres : un registre SUM pour l'addition, un registre DIFFERENCE pour la soustraction, et un registre par opération bit à bit. Les registres pour les opérations bit à bit sont : CMPX et CMPY pour l'opération NOT sur chaque opérande, XANY pour le ET bit à bit, XEOY pour le XOR, XORY pour le OU logique. Les additions et soustractions pouvaient se faire sur des opérandes codés en binaire ou en BCD, suivant le code opération utilisé. A cela, il fallait ajouter deux registres de "résultat" MSKX et MSKY. Ils fournissaient un masque dépendant de la taille du résultat en bits. Cela permettait de gérer des données de 8 ou 16 bits, alors que l'ALU faisait 24 bits. Et cela n'était pas limité à 16 ou 8 bits, mais n'importe quelle taille inférieure ou égale à 24 bits était supportée, même des tailles non-conventionnelles comme des résultats codés sur 3 bits, 9 bits, 12 bits, etc. L'ALU fournissait 12 conditions, qui alimentait un registre d'état. Le registre d'état était segmenté en trois registres appelés XYST, XYCN et BICN. Le processeur contenait de nombreux autres registres de 4 bits, dont les sous-registres de T et L. En tout, il y a 27 registres de 4 bits adressables ! Et le processeur disposait d'une ALU 4 bits pour manipuler ces registres de 4 bits ! Elle est capable de faire des MOV entre registres de 4 bits, des opérations bit à bit, et peut aussi tester si un bit vaut 0 ou 1. La dernière possibilité est très utile pour implémenter les branchements. Pour communiquer avec les entrées-sorties, il disposait de deux registres CMND et DATA : CMND pour envoyer une commande à une entrée-sortie, DATA pour lire ou écrire une donnée. Pour la mémoire, il avait deux registres séparés : READ et WRITE. READ contient la donnée lue lors d'une lecture,; WRITE est pour une donnée à écrire lors d’une écriture. Il s'agit de registres d’interfaçage mémoire, qui ont été promus au rang de registres architecturaux. Les deux registres sont gérés par le microcode, qui est le langage principal de la machine. Les instructions machines configurées font sans, mais le microcode doit y avoir accès facilement. Et vu que le microcode est le langage principal de ce processeur, ces deux registres deviennent architecturaux. ===Une architecture bit-adressable=== Si avoir un microcode réinscriptible est suffisant, quelques choix de ''design'' facilitent l'émulation de jeux d'instructions variés. Par exemple, le CPU Burroughs B1700 avait ajouté une fonctionnalité très importante : il était bit-adressable. En clair, il pouvait adresser la mémoire bit par bit, si nécessaire. L'avantage est que cela permettait d'émuler des architectures dont la taille des registres/''byte''/mots était très différente. Un autre avantage est que cela facilitait la gestion des instructions de taille variable. Détaillons rapidement le premier avantage. Le processeur était un CPU 24 bits, mais il pouvait émuler des processeurs 16 bits, 8 bits, 4 bits, 1 bit, 9 bits, ou toute autre valeur. Pour cela, le processeur lisait des mots de 24 bits et utilisant un ''barrel shifter'' pour sélectionner les bits adéquats. Par exemple, pour un accès de 16 bits, la donnée lue en mémoire était masquée de manière à ne garder que les 16 bits de poids faible. Le ''barrel shifter''/circuit de masquage était placé directement avant le bus mémoire, sur le trajet des données/instructions. Le ''barrel shifter'' était commandé par un registre ''Field Unit'', qui précisait quelle était la taille des données à charger. ===La gestion de la taille des instructions=== Les instructions à décoder n'ont pas de longueur fixe, à savoir qu'elles peuvent faire 16 bit avec tel jeu d'instruction, 8 bits avec un autre, 32 bits sur un autre, etc. Elles peuvent aussi être de longueur fixe ou variable ! Le B1700 intégrait donc un support des instructions de longueur variable, qui était "détourné" pour gérer des jeux d'instruction différents. Notons que vu que l'architecture est bit-adressable, les contraintes d'alignement des instructions disparaissent, ce qui a des avantages qu'on va détailler dans ce qui suit ! L'implémentation des instructions de taille variable ajoutait un registre avant le registre d'instruction. Le registre d'instruction proprement dit était un registre M de 24 bits. Il était précédé par un registre T, dans lequel l'instruction était assemblée, en combinant plusieurs morceaux. Si l'instruction est codée est codée sur moins de 24 bits, le registre T se contente de masquer les bits en trop. Si l'instruction chargée dépasser 24 bits, le chargement se faisait en plusieurs passes, avec plusieurs lectures en mémoire. Dans ce qui suit, on suppose que l'instruction est composée de '''champs''' : l'opcode, les opérandes, les adresses absolues, les constantes immédiates, les numéros de registre, etc. L'idée est que l'instruction est chargée dans le registre T, puis découpée en champs, qui sont copiés dans les registres du processeur ou envoyés au microcode. Je rappelle que le registre T est relié à un ''barrel shifter'', différent au précédent, qui permettait de masquer, décaler et extraire une donnée de ce registre. Et il y a la possibilité d'extraire une suite de bit de ce registre T, pour la copier dans un autre registre. Par exemple, prenons une instruction composée d'un opcode, d'une constante immédiate et d'une adresse immédiate. L'opcode est lu et interprété par le microcode. Le microcode extrait la constante immédiate du registre T et la copie dans le registre X. Ensuite, l'adresse est extraite et envoyée au registre FA, une lecture est démarrée, et l'opérande est copiée dans le registre Y. Enfin, l'instruction est exécutée par le microcode. Le tout prend plusieurs micro-opérations : une première pour lire l'opcode, une seconde pour lire la première opérande, une troisième pour lire la seconde opérande, una quatrième pour tout exécuter. Notons que suivant la taille des champs, on peut en charger plusieurs à la fois. Par exemple, imaginons qu'on ait chargé 24 bits, contenant un opcode de 8 bits et une constante immédiate de 16 bits : pas besoin de faire deux accès mémoire, le registre T est juste découpé en un opcode et une constante. Le registre T est fait pour ce genre d'extraction. ===Le chargement d'un champ=== L'architecture est bit-adressable car elle gére des champs qui ne font pas forcément 8 ou 16 bits. Par exemple, certains jeux d'instructions ont des opcodes codés sur 6 ou 7 bits, voire 9-10 bits. Par exemple, prenons un opcode de 7 bits, suivi par une adresse de 12 bits et une constante immédiate de 15 bits : aucun de ces champs ne sera aligné sur 8/16/24 bits. Utiliser une architecture bit-adressable règle le problème d'alignement. Et en général, les contraintes d'alignement des instructions disparaissent ! Quand on charge une instruction, peut importe qu'elle soit ou non alignée sur 24 bits, elle sera placée dans les bits de poids faible, il suffira de masquer les bits de poids fort inutiles. Par exemple, si on charge une instruction de 16 bits, les 16 bits seront copiés dans les 16 bits de poids faible du registre T, il suffira de masquer l'octet de poids fort. Pas besoin de faire de décalages pour gérer des instructions mal alignées... Pour charger une instruction, il faut connaitre plusieurs informations : l'adresse de son premier bit, sa taille. Pour cela, le B1700 contient deux registres, nommés ''Field Adress'' (FA) et ''Field Length'' (FL), qui indiquent l'adresse du premier bit et la taille de l'instruction à charger. Le premier fait 24 bits, ce qui permet de gérer des adresses de 24 bits. N'oubliez pas que le processeur est bit-adressable, ce qui permet d'adresser 16 Mébi-bits, soit 2 mébioctets. Le registre pour la taille fait lui 16 bits, ce qui permet de gérer des instructions de 65536 bits, soit 8192 octets ! Leur nom commence par ''field'', car le processeur peut gérer des instructions de taille variable et/ou plus longues que 24 bits. Dans ce cas, le chargement de l'instruction se fait champ par champ. Pour donner un exemple, prenons une instruction composée de 20 champs de 12 bits chacun. Les champs sont chargés un par un dans le registre T. Le registre FA est alors initialisé avec l'adresse de l'instruction, il est incrémenté de 12 à chaque cycle. Le registre FL est initialisé avec la taille de l'instruction, soit 20 * 12 = 240 bits, et il est décrémenté de 16 à chaque cycle. Précisons cependant que la taille d'un champ peut être changée d'un cycle à l'autre. Par exemple, le processeur peut charger un opcode de 8 bits, l'interpréter, détecter que l'instruction demande ensuite de charger une adresse de 16 bits. Dans ce cas, le registre FL est immédiatement altéré pour charger 16 bits, au lieu des 8 bits de l'opcode immédiatement précédent. ==Les jeux d'instructions dédiés à un langage de programmation== De rares processeurs sont conçus pour un langage de programmation en particulier. On appelle ces processeurs, conçus pour des besoins particuliers, des '''processeurs dédiés'''. Par exemple, les fameux ''Burrough E-mode'' B5000/B6000/B7000 étaient spécialement conçus pour exécuter de l'ALGOL-60. Leurs cousins B2000/B3000/B4000 étaient eux conçus pour le COBOL. Des langages fonctionnels ont aussi eu droit à leurs processeurs dédiés. Le prolog en est un bel exemple, avec les superordinateurs de 5ème génération qui lui étaient dédié. On peut aussi citer les machines LISP, dédiés au langage LISP, qui datent des années 1970. Elles étaient capables d’exécuter certaines fonctions de base du langage directement dans leurs circuits : elles possédaient notamment un ''garbage collector'' câblé dans ses circuits ainsi que des instructions machines supportant un typage déterminé à l’exécution. Les processeurs dédiés ont eu leur heure de gloire au début de l'informatique, à une époque où les langages de haut niveau venaient d'être inventés. À cette époque, les compilateurs n'étaient pas performants et ne savaient pas bien optimiser le code machine. Il était alors rationnel, pour l'époque, de rapprocher le code machine cible et le langage de programmation de haut niveau. De nombreuses architectures dédiés ont ainsi été inventées, avant que les concepteurs se rendent compte des défauts de cette approche. Les défauts en question ne sont pas nombreux, mais assez simples à comprendre. Premièrement, elles sont très rapides pour un langage de programmation en particulier, mais sont assez mauvaises pour les autres, d'où un problème de "compatibilité". Ajoutons à cela que les langages de programmation peuvent évoluer, devenir de moins en moins populaires/utilisés, ce qui rend la création d'architectures généralistes plus pertinente. Enfin, les architectures dédiées sont évidemment des processeurs CISC, pour implémenter les nombreuses fonctionnalités des langages évolués. Et les défauts des CISC sont assez rédhibitoires à l'heure actuelle. À l'heure actuelle, les algorithmes des compilateurs se sont améliorés et savent nettement mieux utiliser le matériel. Ils produisent du code machine efficace, ce qui rend les architecture dédiées bien moins intéressantes. Si on ajoute les défauts de ces architectures dédiées, par étonnant que les architectures dédiées aient presque disparues. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'accélération matérielle de la virtualisation | prevText=L'accélération matérielle de la virtualisation | next=Le matériel réseau | nextText=Le matériel réseau }} </noinclude> go5uzcscey0qk2zstf9p8nfhj83kkvj 764019 764018 2026-04-19T17:01:27Z Mewtow 31375 /* Le chargement d'un champ */ 764019 wikitext text/x-wiki De nos jours, la majorité des programmeurs programment dans des langages de haut niveau. Il est très rare pour eux d'avoir à utiliser de l'assembleur, et encore moins un langage machine. Les programmes écrits dans un langage de haut niveau sont traduits en langage machine par un logiciel appelé le compilateur. [[File:Compilation.PNG|centre|vignette|upright=2.5|Compilateur : principe.]] Il se trouve que quelques rares processeurs sont conçus pour faciliter le travail du compilateur. Et ce sont ces processeurs que nous allons voir dans ce qui suit. Ils représentent une catégorie de jeux d'instruction à part, qui n'a pas vraiment de nom. ==La compilation et l’interprétation== Un compilateur traduit donc du code source, écrit dans un langage de haut niveau, vers du langage machine. La traduction est rarement directe. En général, le compilateur traduit le code source en assembleur, qui est lui-même traduit en langage machine. L'assembleur est une représentation textuelle du langage machine alors que le code machine est du binaire exécutable par le processeur. La traduction se fait donc en deux étapes, la '''compilation''' proprement dite et l''''assemblage''', réalisées respectivement par un compilateur et un assembleur. Il faut parfois rajouter une troisième phase d'édition des liens, que nous passons volontairement sous silence. {| |[[File:Code C.png|vignette|200px|Le code source, ici en C.]] |[[File:Assem.png|vignette|250px|Le code assembleur.]] |[[File:Binary file - hello world (C programming).png|vignette|150px|Le code machine.]] |} ===L'interprétation et la compilation à la volée=== Une alternative à la compilation est l''''interprétation''', qui transforme le code source en code machine à la volée. Avec l'interprétation, le code source est passé à un logiciel appelé l''''interpréteur''', qui exécute le code source ligne de code par ligne de code. Une ligne de code est traduite en code machine, qui est exécutée, puis l'interpréteur passe à la ligne suivante. [[File:Kaantotulkkaus.png|centre|vignette|upright=3|Compilation versus interprétation.]] Un défaut de l'interprétation est son cout en performance, qui est assez important. Aussi, l'interprétation stricte a évolué vers un hybride entre interprétation et compilation. L'idée est qu'une partie peu importante du code source est interprétée, alors que le code important est compilé juste avant d'être exécuté. Pour le dire autrement, le code source est partiellement compilé à la volée, juste avant son exécution. Aussi, on parle de '''compilation à la volée'''. Le terme en anglais est ''Just In Time compilation'', abrévié en JIT. En soi, le JIT est associé à de l'interprétation, les JIT sont en réalité des hybrides interpréteurs-compilateurs. Un point important est que compiler du code à la volée est assez lourd, cela a un cout en performance. Par contre, le code compilé s'exécute plus vite que du code interprété. Si on exécute le code compilé une seule fois, le cout de la compilation l'emporte sur le gain à l'exécution. Mais le code compilé est réutilisé autant de fois que nécessaire. Le gain à l'exécution est donc multiplié par le nombre d'exécutions, alors que le cout de la compilation est répartit sur plusieurs exécutions, il est amorti. Les portions du code sont choisies de manière à avoir un gain en performance maximal. L'idée générale est que le code exécuté très souvent est compilé, alors que du code exécuté pas souvent est interprété. Typiquement, les boucles les plus souvent exécutées sont compilées, le code exécuté une seule fois est interprété. ===Les langages intermédiaires=== Les compilateurs modernes passent par un '''langage intermédiaire''' pour faire la transformation en code machine. Le compilateur traduit le langage de haut niveau en langage intermédiaire, puis traduit le langage intermédiaire en code machine cible. Le langage intermédiaire est parfois appelé le '''''bytecode''''', et ce terme recouvre aussi le code écrit avec du ''bytecode''. L'interprétation aussi peut passer par l'intermédiaire d'un un ''bytecode'', ce n'est pas limité aux compilateurs proprement dit. {| |[[File:Intermediate representation scheme.png|centre|vignette|upright=1.5|Représentation intermédiaire d'un compilateur.]] |[[File:Code interpreter scheme.png|centre|vignette|upright=1.5|Fonctionnement d'un interpréteur.]] |} Faire ainsi a de nombreux avantages, le principal étant d'avoir un compilateur capable de traduire un langage de haut niveau vers plusieurs jeux d'instructions différents. Cela permet d'avoir, par exemple, un compilateur qui traduit du C soit en code machine x86, soit en code machine pour un CPU ARM, soit pour un CPU POWERPC, etc. [[File:Compiler design.svg|centre|vignette|upright=3|Compiler design]] Pour l'interprétation, l'usage d'un ''bytecode'' a d'autres avantages. Pour l'exploiter, il faut distribuer non pas le code source, mais le ''bytecode''. En clair, l'interpréteur ne prend pas en entrée le code source, mais du ''bytecode'' déjà compilé. La traduction est alors beaucoup plus simple, car le code source a déjà été partiellement compilé, par la traduction en ''bytecode''. Interpréter du code source est en effet assez compliqué : il faut effectuer des étapes d'analyse lexicale, sémantique, et bien d'autres. Avec du ''bytecode'', ces étapes ont été réalisées lors de la compilation du ''bytecode'', l'interpréteur a alors peut de choses à faire. L'interprétation du ''bytecode'' se fait instruction par instruction, au niveau du ''bytecode''. Une instruction du ''bytecode'' est traduite en une instruction machine équivalente, qui est exécutée. Si l'instruction est un peu complexe, il exécute une fonction/procédure qui fait la même chose. Pour résumer, le ''bytecode'' est ensuite traduit à la volée et exécuté instruction par instruction par un logiciel appelé l'interpréteur. [[File:Control table.png|centre|vignette|upright=2|Control table]] Le langage intermédiaire peut être vu comme l'assembleur d'un processeur, n'existe pas forcément dans la réalité, mais dont le jeu d'instruction est décrit en détail. Le processeur en question est appelé une '''machine abstraite''', ou encore une ''machine virtuelle''. Nous utiliserons le terme de machine abstraite dans ce qui suit. La machine abstraite n'est pas la même suivant que l'on cible la compilation ou l'interprétation/JIT. Pour l'interprétation/JIT, la machine abstraite est souvent une machine à pile, car cela simplifie grandement la traduction en code machine final. L'interprétation demande que la traduction du ''bytecode'' en code machine soit la plus simple possible. Et cela demande de prendre en compte pas mal de détails. Par exemple, les différents jeux d'instruction existants n'ont pas le même nombre de registres, ce qui pose problème lors de la traduction du ''bytecode''. Lors de la transformation en code machine, un algorithme d'allocation de registres se débrouille pour traduire le code intermédiaire en code qui utilise un nombre limité de registres. Pour améliorer cette allocation de registres, il y a deux solutions niveau ''bytecode'' : soit utiliser un nombre illimité de registres, soit utiliser une machine à pile. Pour les machines à pile, il existe un algorithme simple et rapide pour traduire un code écrit pour une machine à pile en un code écrit pour un processeur avec des registres, qui se débrouille pas trop mal pour allouer efficacement les registres. C'est un avantage assez important pour les langages interprétés. ==Les processeurs qui exécutent du bytecode== Le ''bytecode'' est un code machine, ce qui signifie qu'il peut en théorie s'exécuter sur un processeur qui implémente le jeu d’instruction associé. Si le jeu d'instruction d'un ''bytecode'' est souvent une description censée être fictive, elle n'en reste pas moins un jeu d'instruction et des caractéristiques précises, qu'on peut l'implémenter en matériel ! Néanmoins, tous les ''bytecode'' ne sont pas égaux de ce point de vue. Certains sont faciles à implémenter en matériel, d'autres non. Les ''bytecodes'' implémentés en matériel sont ceux dont les machines abstraites sont des machines à pile. Un premier exemple est celui des processeurs Pascal MicroEngine, qui exécutaient directement le bytecode du langage Pascal, le fameux UCSD P-code. Un second exemple est la '''machine SECD''', qui sert de langage intermédiaire pour certains compilateurs de langages fonctionnels. Elle a été implémentée en matériel par plusieurs équipes, notamment par les chercheurs de l'université de Calgary en 1989. Dans le même genre, quelques processeurs simples étaient capables d’exécuter directement le ''bytecode'' du langage FORTH. Le FORTH, un des premiers langages à pile de haut niveau, possède de nombreuses implémentations hardware et est un des rares langages de haut niveau dont le ''bytecode'' a été utilisé comme langage machine sur certains processeurs. Par exemple, on peut citer le processeur FC16, capable d’exécuter nativement du ''bytecode'' FORTH. ===Les processeurs Java=== Le cas le plus impressionnant est celui de la machine virtuelle Java, qui est un design de processeur comme un autre. En temps normal, le ''bytecode'' Java est compilé ou interprété, mais certains processeurs exécutaient du ''bytecode'' Java directement, sans interprétation ni compilation. Ils sont appelés des '''processeurs Java'''. On peut citer les processeurs ARM disposant de l'extension Jazelle, du JEMCore et du aJ-100 de aJile Systems, le picoJava II de Sun, et quelques autres. Il y a aussi eu quelques projets de recherche ou processeurs open source, comme Komodo, jamuth, le ''Java Optimized Processor'' et quelques autres projets du même genre. L'implémentation de la machine virtuelle Java n'était cependant pas complète. Quelques instructions complexes n'étaient pas gérées par le processeur et devaient être émulées en logiciel. Mais la grosse majorité des instructions l'était. Les instructions non-supportées étaient émulées avec la méthode ''trap and emulate'', à savoir qu'un opcode inconnu déclenchait une exception matérielle ''opcode invalide'', dont la routine pouvait émuler les instructions qui doivent l'être. Le processeur aJ-100 de aJile est capable d'exécuter la quasi-totalité des instructions de la JVM Java, seules deux instructions faisant exception. Les '''accélérateurs Java''' sont une solution intermédiaire, qui permet de traduire à la volée du ''bytecode'' Java en instructions RISC, exécutées sur un autre processeur. Il s'agit formellement de traduction binaire, mais je préfère en parler ici. L'idée est de combiner un processeur RISC avec un coprocesseur Java. Le coprocesseur lit le ''bytecode'' Java et le traduit en instructions RISC, ces dernières étant exécutées sur le processeur RISC. Le JA108 de Nozomi était un coprocesseur de ce genre, au même titre que le coprocesseur JSTAR de JEDI. L'extension Jazelle de certains CPU ARM fonctionnait vraisemblablement sur un principe similaire, sauf que le coprocesseur était intégré dans le processeur ARM, entre le cache d'instruction et le décodeur d'instructions ARM. L'intérêt des processeurs Java n'est pas qu'une question de performance. Certes, ils permettent d’exécuter plus vite les programmes compilés en ''bytecode'', comme des programmes Java pour la JVM Java. Mais l'intérêt est aussi de faire des économies de mémoire RAM et ROM. Je rappelle que la machine virtuelle Java est une machine à pile, ce qui fait que sa densité de code est excellente. Les programmes en ''bytecode'' Java sont donc très petits, et prennent peu de place en mémoire ROM. De plus, utiliser un processeur Java permet de se passer d'interpréteur ou de JIT Java, ce qui économise encore plus de mémoire. ===Transformer un processeur RISC en machine à pile=== La plupart des processeurs Java utilisent en interne une architecture à registre généraux, qui émule une machine à registre en microcode. Quelques registres sont réservés pour l'état de la machine virtuelle, avec au minimum un registre réservé pour le pointeur de pile, un autre pour un pointeur vers un ''pool'' de constantes, et quelques autres. D'autres registres servent à mémoriser le haut de la pile, à savoir le sommet de la pile et quelques opérandes situées juste en-dessous. Par exemple, le processeur picoJava II gardait les 64 opérandes au sommet de la pile dans 64 registres généraux, séparés des registres pour les pointeurs et l'état de la JVM. Une optimisation possible, envisagée sur les processeurs picoJava, était une sorte de macro-fusion sous stéroïde. L'idée était de fusionner certaines séries d'instructions Java en une seule instruction machine. Typiquement, pour faire une opération arithmétique, une machine à pile empile deux opérandes et exécute une opération, ce qui prend trois instructions machine. La spécification picoJava 1 a proposé d'envoyer directement la seconde opérande en entrée de l'ALU, ce qui fusionnait l'empilement de la seconde opérande et l'opération arithmétique. Le processeur picoJava II va plus loin et fusionne les trois instructions en une seule instruction RISC, agissant sur des registres. Mais la majorité des processeurs Java ne faisaient pas cela, car cela complexifie grandement le décodeur. Les processeurs ''bigfoot'' de DCT utilisaient un système différent pour émuler une pile avec des registres. Leur banc de registre de 64 bits était coupé en deux : les 16 premiers registres étaient utilisés pour la pile, les 48 suivants étaient utilisés pour autre chose. L'idée est que le registre 0 mémorise un emplacement vide, le registre 1 contient le sommet de la pile, le registre 2 l'opérande sous le sommet de la pile, et ainsi de suite. Les 15 premières opérandes de la pile sont donc mémorisées dans les registres. Les instructions écrivent leur résultat dans le registre adéquat, puis tous les registres sont décalés pour que le résultat passe dans le registre R1. Par exemple, les instructions PUSH chargent une opérande dans le registre 0, puis tous les registres sont décalés de un rang, pour que l'opérande chargée soit dans le registre 1. Le registre 0 devient alors le registre numéro 1, le registre 1 devient le 2, et ainsi de suite. Par contre, les opérations arithmétiques font autrement. Une instruction d'addition, par exemple, lit les opérandes dans le registre 1 et 2, puis place son résultat dans le registre 2 (les deux opérandes sont dépilées). Puis, les registres sont décalés d'un rang pour que ce registre 2 devienne le 1. Les registres ne sont en réalité pas décalés, il n'y a pas de transferts entre registres. A la place, les 16 premiers registres utilisaient une forme limitée de renommage de registres, similaire au banc de registre tournant utilisé sur les processeurs EPIC comme l'Itanium. Les numéros de registres sont renommés, décalés, à chaque fois qu'on empile ou dépile une opérande. Pour faire le renommage de registres, processeur contient un ''register stack counter'' de 4 bits, qui est incrémenté à chaque fois qu'on empile une donnée, et est décrémenté quand on dépile une donnée. Quand on veut accéder à un opérande, le numéro de registre architectural associé est traduit en numéro de registre physique, en additionnant le ''register stack counter'' (modulo 16, pour rester sur 4 bits). ==L'accélération de l'interprétation/JIT : le ''Thumb-EE'' d'ARM== Quelques processeurs ont ajouté des instructions pour faciliter le travail des interpréteurs/JIT. Par exemple, en 2005, ARM a ajouté le mode ''Thumb Execution Environment'', qui faisait cela. Il reprenait le jeu d'instruction compact ''thumb'' et ajoutait quelques instructions et modifiait le comportement d'instructions existantes. Le jeu d'instruction ''Thumb-EE'' a été ajouté sur les CPU ARM en 2005, mais a été déprécié en 2011, par manque d'utilité. Mais la tentative mérite qu'on s'attarde dessus. Les CPU de gamme M, déstinés à l'embarqué, n'ont jamais supporté ''Thumb-EE'', vu que de tels processeurs ne sont pas conçus pour exécuter du code interprété/JIT. Le mode ''Thumb-EE'' est un mode d'exécution, séparé de l'ARM normal. Ainsi, une instruction ''Thumb-EE'' et une instruction ARM peuvent avoir le même encodage en binaire, mais se comporter différemment. Le processeur est à tout moment soit en mode ''Thumb-EE'', soit en mode ARM normal, ce qui précise comment ces instructions doivent se comporter. Des instructions ont été ajoutées par ''Thumb-EE'', pour qu'il fasse son travail. L'entrée et la sortie du mode ''Thumb-EE'' se fait avec deux instructions : ENTERX et LEAVEX. Une fois en mode ''Thumb-EE'', le décodage des instructions ''thumb'' se fait avec les règles du ''Thumb-EE'', et non celles du ''thumb'' normal. Une première différence entre ''thumb'' normal et ''Thumb-EE'' est l'ajout de ce qui s'appelle un ''null check'' pour les instructions mémoire. Le ''null check'' est utilisé pour les instructions en mode d'adressage "base + indice" ou "Base + décalage", ou tout autre mode d'adressage avec un registre de base utilisés dans des calculs d'adresse. L'idée est de vérifier si l'adresse de base vaut zéro ou non. Si c'est le cas, le processeur lève une exception matérielle, qui est traitée par l'interprétation ou le compilateur JIT. Une telle situation est en effet signe d'une erreur d'adressage mémoire, qui doit être traitée par l'interpréteur. Typiquement, c'est signe qu'un pointeur n'a pas bien été initialisé, ce qui peut arriver avec du code interprété ou JIT. Les interpréteurs/JIT traitent généralement la situation en ajoutant un test avant toute instruction mémoire, pour vérifier si le pointeur accédé vaut zéro ou non. Avec ''Thumb-EE'', pas besoin d'ajouter les tests en questions : ils sont réalisés automatiquement lors de chaque instruction mémoire. : Les registres pour la pile, comme le pointeur de pile et le pointeur de ''frame'', sont aussi concernés. L'instruction CHKA vérifie que les accès à un tableau ne débordent pas en dehors du tableau en question. Par exemple, pour un tableau de 1024 éléments, elle vérifie si l'indice est compris entre 0 et 1024. Pour cela, elle vérifie si l'indice de tableau est bien dans l'intervalle adéquat et lève une exception matérielle si ce n'est pas le cas. Son utilité se comprend quand on sait que tous les accès à un tableau sont vérifiés par l'interpréteur. L'interpréteur est censé ajouter des instructions pour vérifier les indices, à savoir deux ou trois branchements. Avec CHKA, le test se fait en une seule instruction. D'autres modifications mineures de l'encodage des instructions sont aussi présentes, ainsi que des modifications pour les instructions LOAD/STORE. Précisément, dans les modes "base + indice" et "base + décalage", le décalage et l'indice subissent maintenant des décalages. L'idée est de simplifier le calcul d'adresse. De quoi économiser quelques instructions lors des calculs d'adresse, ce qui facilite le travail de l'interpréteur/JIT. Et des instructions LOAD/STORE précises voient leur comportement modifié pour faciliter la gestion de la pile de Java ou l'exécution des méthodes locales. Pour faciliter le travail de l'interpréteur, deux instructions HBP et HBLP ont été ajoutées. Elles branchent vers une fonction qui gère les exceptions logicielles (une fonctionnalité présente dans de nombreux langages, comme Java). La première effectue un branchement vers la fonction en question, la seconde sauvegarde l'adresse de retour avant de faire ce branchement. Les deux peuvent brancher vers 256 fonctions pré-déterminées, sans avoir à présenter leur adresse. ==La traduction binaire accélérée par le matériel== Plus haut, nous avons surtout parlé des compilateurs et des interpréteurs. Cependant, nous devons aussi parler de la '''traduction binaire'''. Elle traduit un programme écrit dans un code machine vers un autre code machine. Par exemple, elle traduit un programme compilé pour un CPU x86 vers un code machine ARM. En général, le système d'exploitation est généralement compilé pour le jeu d'instruction natif, mais il exécute des applications prévues pour le x86. Les applications sont traduites par le système d'exploitation, avant d'exécuter le code traduit. : Nous parlerons dans la suite de code machine source et de code machine cible, pour parler respectivement du code à traduire et du code obtenu après traduction. Idem avec d'autres termes comme architecture cible/source, ou jeu d'instruction cible/source. La traduction binaire est surtout utilisée pour des questions d'émulation ou de compatibilité. Par exemple, lorsque les Macintosh sont passés de processeurs Power PC vers des processeurs x86, le système d'exploitation Mac OS utilisait la traduction binaire pour convertir les anciennes applications Power PC vers du code x86. Les utilisateurs n'y ont vu que du feu. Le système de traduction binaire était appelé Rosetta 1. Par la suite, lors de la transition de processeurs x86 vers des processeurs Apple, Rosetta 2 a vu le jour. La traduction binaire a surtout été utilisée pour traduire du code x86 vers un autre jeu d'instruction. Il faut dire que le x86 est le jeu d'instruction dominant. De nombreuses entreprises ont eu pour ambition de briser l'hégémonie du x86 sur PC, en remplaçant le x86 par un jeu d'instruction plus performant, tout en gardant une compatibilité maximale. La traduction binaire était la seule solution pratique. Elle était le plus souvent intégralement réalisée en logiciel, comme c'était le cas sur les architectures Itanium avec le ''IA-32 Execution Layer''. ===Les généralités sur la traduction binaire assistée en matériel=== L'architecture source est presque tout le temps une architecture CISC, assez ancienne, qu'on souhaite émuler. Le choix de l'architecture cible se porte souvent sur une architecture VLIW, fort différente de l'architecture source, et ce pour obtenir une économie de matériel conséquente. Pour s'exécuter rapidement, le code traduit doit exploiter le parallélisme d'instruction, à savoir exécuter plusieurs instructions en même temps dans des unités de calcul séparées. Il est possible d'utiliser un processeur superscalaire avec exécution dans le désordre pour cela, mais au prix d'un cout important en transistors. Alors qu'en utilisant un CPU VLIW, c'est le traducteur binaire qui fait tout le travail d'extraction du parallélisme d'instruction. Et après tout, quitte à avoir un traducteur binaire, autant lui refiler le boulot d'optimisation. Compiler du code à la volée est certes assez lourd, mais qu'il existe des algorithmes efficaces pour regrouper des instructions indépendantes dans une seule instruction VLIW. Au passage, le processeur VLIW a plus de registres que le processeur source. Cela permet de faire du renommage de registres directement en logiciel. Le traducteur binaire n'hésite pas à changer les noms de registres entre instructions source et instruction VLIW cible, afin de supprimer des dépendances de données. En conséquence, cela ouvre des opportunités de parallélisme, qui sont exploitées lors du regroupement des opérations en instructions VLIW. Vous pourriez penser que le choix d'un CPU VLIW pour émuler du CISC est tout sauf optimal. Vous devez penser que la traduction binaire est d'autant plus simple que l'architecture source et cibles sont semblables. Dans les faits, ce n'est pas tellement traduire les instructions qui pose problème, mais plus la gestion du registre d'état, des exceptions matérielles, de la mémoire virtuelle, la différence entre gros-boutisme et petit-boutisme, de même que des différences d'adressage pour les périphériques. Le premier problème est la gestion des conditions, notamment en présence d'un registre d'état. En pratique, la traduction binaire s'utilise pour traduire du code CISC vers du code RISC ou VLIW. Les architectures source ont donc un registre d'état, qui est mis à jour non seulement par des instructions de test, mais aussi des instructions arithmétiques. L'architecture cible n'a elle pas de registre d'état, mais des registres à prédicats. La traduction de l'un vers l'autre est alors quelque peu compliquée. Et elle est d'autant plus compliquée que le registre d'état est la source de dépendances d'instruction implicites, qui réduisent les performances. Par exemple, si une instruction arithmétique modifie le registre d'état, cela peut impacter l'exécution d'une instruction ultérieure, qui lit ce registre d'état. Pour éliminer ces fausses dépendances, le traducteur binaire doit renommer ce registre en logiciel et détecter les dépendances utiles. Et le compilateur doit gérer les cas où il y a beaucoup de distance entre les deux, voire les cas où l'instruction dépendante n'a pas encore été analysée par le compilateur. Un autre problème est lié à la gestion des exceptions matérielles, et précisément des exceptions précises. Pour rappel, une instruction VLIW regroupe plusieurs opérations, opérations qui correspondraient à une instruction machine sur un CPU pas VLIW. En conséquence, plusieurs instructions du langage machine "source" sont regroupées en une seule instruction VLIW. Et il faut tenir compte du cas où une instruction source lève une exception. Dans le code VLIW, cela signifie qu'une opération lève une exception, et il faut annuler partiellement l'instruction VLIW associée. Et par partiellement, on veut dire que seules les opérations suivantes dans l'ordre du programme source doivent être annulées, puis ré-exécutées. Et c'est un sacré casse-tête ! Les processeurs VLIW utilisent comme solution le mécanisme d'exceptions différées des processeurs EPIC, vu il y a quelques chapitres. Pour résumer, le code est exécuté par blocs d'instruction, délimités par des branchements ou tout autre limite/barrière pertinente dans le code. Un bloc de code est compilé en deux versions : une version rapide sans exceptions matérielles, une version lente qui gère les exceptions précises. La version rapide s'exécute sans exécuter les exceptions. Cependant, les exceptions matérielles sont enregistrées, pour être prises en compte à la toute fin du bloc de code. La version rapide est exécutée en premier et elle mémorise si une exception matérielle a eu lieu. Puis une instruction vérifie si une exception a eu lieu et décide quoi faire. Si aucune exception n'a eu lieu, elle passe à la suite du programme, les résultats du bloc de code sont définitivement acceptés. Mais si une exception a eu lieu, tout est annulé. Le processeur est remis dans l'état initial, puis le code est ré-exécuté instruction par instruction de manière à gérer l'exception correctement. ===La traduction par pré-décodage=== Une première solution serait de faire la traduction binaire dans le cache d'instruction. Lors d'un défaut de cache, le code chargé depuis la RAM est traduit en code machine cible. Et c'est ce code machine cible qui est mémorisé dans le cache d'instruction et exécuté par le processeur. La technique marche sur le papier et n'est qu'une amélioration des techniques de pré-décodage vues il y a quelques chapitres. Cependant, le code obtenu est une traduction assez basique, qui n'incorpore pas d'optimisations dignes de ce nom. De plus, elle gère mal le cas où la taille du code source et cible sont potentiellement très différentes. Concrètement, elle est surtout utile pour traduire du code d'un processeur RISC vers un autre processeur RISC, dont les tailles d'instructions sont similaires. Elle ne permet pas d'émuler plusieurs jeux d'instructions différents, le cout en matériel (un circuit de pré-décodage par jeu d'instruction) serait trop important. ===Le projet DAISY d'IBM=== De très rares processeurs étaient conçus pour accélérer cette traduction binaire, afin de garder de bonnes performances. Les premiers à avoir étudié l'idée étaient IBM, avec leur projet DAISY (''Dynamically Architected Instruction Set from Yorktown''). Le projet de base était de convertir à la volée du code compilé pour des CPU Power PC, vers du code VLIW. Les chercheurs d'IBM avaient développé un algorithme de traduction dynamique efficace, ainsi qu'un processeur VLIW disposant d'optimisations spécifiques à la traduction binaire dynamique, à la volée. De nombreuses idées de ce projet ont été reprises ou re-découvertes par la société Transmetta, avec ses processeurs Crusoe et Efficieon, puis par NVIDIA avec son projet Denver. Les ingénieurs de ce projet ont étudié la possibilité d'émuler plusieurs jeux d'instructions différents sur un même processeur, notamment le s390 d'IBM et le x86. En théorie, cela demande juste d'avoir plusieurs programmes de traduction binaire : un pour le x86, un autre pour le s390, éventuellement un autre pour le Power PC. Mais cela ne s'est pas concrétisé. Mais les ingénieurs ont étudié quelle pourrait être l'architecture VLIW idéale pour ça. Par exemple, il fallait des additions 3-opérandes pour simplifier les calculs d'adresse. Une difficulté était la gestion du registre d'état, dont les bits ne sont pas mis à jour de la même manière sur le x86 et le s390 ou le Power PC. La différence entre gros-boutisme et petit-boutisme était aussi un problème, de même que des différences d'adressage pour les périphériques. Pour que la traduction binaire soit efficace, le processeur VLIW intègre diverses optimisations, comme les exceptions différées et les branchements multi-voies. La principale est la suivante : seul le code des boucles ou fonctions exécutées fréquemment est traduit en code VLIW, le reste du code est interprété. En effet, traduire du code binaire prend plus de temps qu'une simple interprétation. Pour du code qui ne sera exécutée qu'une seule fois, il est plus rapide d'utiliser l'interprétation. Par contre, pour du code exécuté beaucoup de fois, le cout de la traduction binaire est amorti, dilué sur N exécutions, compensé par le gain en temps d'exécution de ce code traduit. Au final, cela permet de ne traduire que le code qui le mérite. La détection du code fréquemment exécuté est réalisée dans le cœur VLIW. L'unité de branchement mémorise les derniers branchements rencontrés et le nombre de fois qu'ils ont été exécutés. Si ils ont été exécutés un certain nombre de fois, l'unité de branchement lève une exception matérielle, qui invoque le traducteur binaire. La routine de cette exception est le traducteur binaire proprement dit. En clair, la traduction binaire est démarrée quand le processeur détecte qu'une fonction a été exécutée plus de N fois, via une exception matérielle dédiée. La mesure du nombre d'exécution d'un branchement se fait dans l'unité de calcul dédiée aux branchements, avec l'aide d'une sorte de ''branch adress buffer'' modifié. Pour rappel, le ''branch adress buffer'' mémorise le ''Program counter'' de chaque branchement récemment rencontré. Ici, chaque entrée du ''branch adress buffer'' est associée à un compteur incrémenté à chaque exécution du branchement. ===Les processeurs Crusoe et Efficieon de Transmetta=== Les processeurs Crusoe Et Efficeron sont deux processeurs VLIW produits par la société Transmetta, une société californienne rachetée par NVIDIA. Ils étaient conçus pour exécuter spécifiquement des programmes x86, système d'exploitation inclus. L'idée derrière ce projet était d'exécuter du code x86 sans que les applications, ni même le système d'exploitation et le BIOS soient au courant ! Les processeurs Transmetta étaient en réalité conçus pour exécuter un programme unique, le ''Code Morphing Software'' (CMS), qui traduisait le code x86 en code VLIW. Transmetta ne voulait pas que son processeur VLIW soit exploité directement, sans le CMS. Il n'avait pas rendu public de compilateur pour traduire du C vers du code VLIW, il n'avait pas donné la documentation du jeu d'instruction VLIW. Des efforts de rétro-ingénieurie, documentés sur le site realworldtech, ont cependant permis de comprendre comment étaient encodées les instructions du processeur Crusoe. Comme pour le projet Daisy, le ''Code Morphing Software'' utilisait à la fois interprétation et traduction binaire, selon les besoins. Les instructions sont interprétées lors de leurs premières exécutions, mais le CMS bascule sur de la traduction en code VLIW après un certain nombre d'exécution. Ainsi, les boucles souvent exécutées sont traduites en code VLIW, alors que le reste du code est interprété. Le choix du code à traduire est le fait du CMS, il utilise des heuristiques complexes pour, qui ne sont pas connues dans le détail. Le ''Code Morphing Software'' est mémorisé dans une EEPROM, ce qui en fait un ''firmware'' situé en dessous du BIOS. Le CMS démarre ensuite le BIOS, qui lui-même démarre le système d'exploitation, qui lui-même démarre les pilotes de périphériques et les programmes. Le CMS se réserve les 16 premiers mébioctets de l'espace d'adressage. L'EEPROM du CMS est mappée dedans, mais n'en utilise que la moitié. Le reste est utilisé comme cache, pour mémoriser le code VLIW traduit par le CMS. Les deux processeurs intégraient aussi deux mémoires SRAM, utilisées par le CMS, appelées la ''local program memory'' (LPM) et la ''local data memory'' (LDM). La première contient du code qui gére les interruptions, la mémoire virtuelle, les exceptions matérielles, les problèmes d'alignement mémoire, et quelques fonctions très fréquemment utilisées par le CMS. La seconde est de la mémoire RAM utilisée par ces fonctions de la ''local program memory''. Les mémoires caches L1 et L2 sont séparées de ces deux mémoires. Le processeur Crusoe et Efficieon étaient des processeurs VLIW très simples. Ils n'avaient même pas de MMU, ni de fonctionnalités importantes sur les CPU x86. Pour Crusoe, les instructions VLIW étaient encodées sur 64 ou 128 bits, et regroupaient entre 2 à 4 opérations. Les instructions LOAD/STORE ne supportaient pas d'adressage indicé, juste de l'adressage indirect à registre. Crusoe avait 5 unités de calcul : deux ALU entières, une unité LOAD/STORE, une unité de branchement, une FPU. Efficieon doublait le nombre d'ALU entière et d'unité LOAD/STORE, les instructions VLIW passaient à 256 bits. Le processeur Crusoe contient 160 registres, dont 64 registres généraux, 32 registres flottants. Les registres généraux font 32 bits, ce qui est cohérent avec le fait que les CPU x86 de l'époque étaient des processeurs 32 bits. Quant aux registres flottants, ils faisaient 80 bits, ce qui colle avec la taille des registres flottants de la FPU x87 utilisée à l'époque. Sur les 64 registres généraux, seuls 48 étaient réellement utilisable pour mémoriser des opérandes. Une partie des registres généraux étaient utilisés pour la gestion de la pile, d'autres pour mémoriser l'état du CPU x86 émulé, un registre était un registre zéro non accesible en écriture. La gestion des exceptions est optimisée avec un système d'exceptions différées, le même que celui décrit plus haut. Le processeur utilise un système similaire, pour effectuer des lectures anticipées, qui a été expliqué dans le chapitre sur les processeurs VLIW/EPIC. Sauf qu'il s'agit là d'une optimisation pour gagner en performances en général, pas une aide utile pour gérer des exceptions précises ou toute autre subtilité de la traduction binaire. Pour gérer les exceptions différées et les lectures anticipées, le processeur mémorise l'état du processeur avant de démarrer un bloc de code. Pour cela, le processeur contient des ''shadow registers'', 48 registres entiers et 16 registres flottants. Vous remarquerez qu'il y a autant de registres entiers que de ''shadow registers'' entiers. Les registres entiers/flottants sont mémorisés dans les ''shadow registers'' avant d'exécuter un bloc de code, afin que le processeur puisse revenir à l'état de base. Une autre fonctionnalité liée est le ''gated store buffer''. Le principe est d'éviter toute écriture en mémoire RAM, tant que le code VLIW n'a pas émis d'instruction ''commit''. L'idée est que les écritures sont conservées dans la ''store queue'' de l'unité LOAD/STORE. Pour rappel, la ''store queue'' met en attente les écritures soit tant que les écritures ne sont pas terminées, soit tant que la RAM est occupée. L'idée est que les écritures sont accumulées dans la ''store queue'' tant que l'instruction ''commit'' n'a pas eu lieu. Le ''store queue'' est "déversé dans la RAM" seulement quand l'instruction ''commit'' s'exécute. ===Les CPU NVIDIA Denver=== Nvidia a tenté quelque chose de similaire avec son projet Denver. NVIDIA a racheté l'entreprise Transmetta, ses brevets, et a tenté de refaire la même chose. Le projet initial était de traduire du code x86 vers un jeu d'instruction VLIW propriétaire, comme l'a fait Transmetta. Mais NVIDIA n'a pas réussi à acquérir la licence du jeu d'instruction x86 et s'est rabattu sur le jeu d'instruction ARM. Le processeur né de ce projet est le Tegra K1-64 CPU. Il s'agit d'un processeur composé d'un cœur VLIW, couplé à deux décodeurs d'instructions ARM. Les deux décodeurs ARM traduisent une paire d'instructions ARM consécutives en instructions VLIW, pour les exécuter sur le cœur VLIW. Le CPU peut fonctionner selon deux modes : ARM et VLIW. En somme, le processeur supporte deux jeux d'instruction : le jeu d'instruction ARM, et un jeu d'instruction VLIW. En mode ARM, les instructions ARM sont chargées depuis le cache, traduites en VLIW par les décodeurs ARM, puis exécutées par le cœur VLIW. En mode optimisé, le CPU exécute des instructions VLIW chargées depuis le cache d'instruction, les décodeurs ne sont pas utilisés. : Le CPU supporte en réalité deux jeux d'instruction ARM : ARM8 et 7. Mais ils correspondent au même mode pour le CPU. Un point important est que le processeur peut changer très rapidement de mode, en à peine quelques cycles d'horloges. On n'est clairement pas dans le cas des CPU x86, qui mettent des plombes pour passer du mode 32 à 64 bits et inversement. La commutation est tellement rapide qu'on peut considérer que le processeur supporte deux jeux d'instruction simultanément. Et le support simultané de deux jeux d'instruction facilite l'implémentation de la traduction binaire. Encore une fois, inutile de traduire du code qui ne sera exécutée qu'une seule fois, mieux vaut l'exécuter directement dans le mode ARM. Surtout que le processeur supporte un mode ARM qui permet d'exécuter du code ARM sans perte de performance. Par contre, le code fréquemment utilisé, notamment dans des boucles critiques, est traduit en VLIW. Au final, les performances sont optimisées, en limitant le travail du logiciel traducteur binaire. La détection du code fréquemment exécuté se fait différemment que pour les processeurs Transmetta et Daisy. L'unité de branchement ne détecte pas directement le code beaucoup exécuté, même si elle a un rôle à jouer. A la place, la détection est réalisée en logiciel, par un ''thread'' dédié, qui s'exécute sur un cœur séparé. Il détecte le code fréquemment utilisé en regardent les branchements exécutés récemment. Pour cela, le processeur mémorise l'historique des branchements pris récemment et copie cette historique dans une mémoire tampon dédiée, partagée entre tous les cœurs. Vu qu'elle est partagée, le ''thread'' de détection a accès à l'historique sans pertes de performances, seule la copie de l'historique dans ce tampon a un cout en performance, pas son partage. Le code VLIW obtenu après traduction binaire est mémorisé dans la mémoire RAM, dans une portion spécialement réservée pour. Elle fait 128 mébioctets, et est appelée le '''cache d'optimisation''' par NVIDIA. Le cache d'optimisation est protégé en écriture et seul le traducteur binaire peut écrire dedans. Au passage, le traducteur binaire est du code VLIW, ce qui fait qu'il est placé dans le cache d'optimisation. Le processeur détecte automatiquement quand une fonction pour laquelle le code compilé équivalent est disponible. Pour cela, l'unité de chargement contient une table de correspondance entre l'adresse de la fonction ARM, et l'adresse de son équivalent compilé. Quand une fonction est appelée, l'unité de chargement regarde l'adresse de destination du branchement. Si l'adresse est dans cette table, elle récupère l'adresse de la fonction compilée et branche vers celle-ci. Le processeur dispose d'un cache d'instruction de 128 kibioctets, ce qui est très important, mais nécessaire vu la taille des instructions VLIW. Le coeur VLIW dispose de ses propres décodeurs, d'un ''scoreboard'', de 7 unités de calcul et de registres. Pour les unités de calcul, il a deux FPU, deux ALU, une unité de branchement, et deux unités LOAD-STORE faisant aussi ALU. Un point important est que le CPU dispose de 64 registres entiers et de 64 registres flottants. C'est deux fois plus que supporte le jeu d'instruction ARM. En mode ARM, seule la moitié des registres est utilisée. Mais en mode optimisé, le traducteur binaire utilise bien les 64 registres, grâce à une sorte de renommage de registres logicielle. Pour simplifier la traduction binaire, le processeur supporte les techniques vues précédemment. Le processeur intègre un ''gated store buffer'' similaire à celui des processeurs Transmetta. Les techniques d'exceptions différées sont aussi supportées, comme sur les processeurs Transmetta. Il a aussi une unité de préchargement décrite par NVIDIA comme agressive, avec support du préchargement de type anticipé (''runahead''). ===L'émulation de la mémoire virtuelle=== Pour finir, il faut parler de la mémoire virtuelle avec la traduction binaire. Sur le processeur source, le processeur gère à la fois des adresses physiques et virtuelles et sa MMU fait la conversion entre les deux. Sur le processeur VLIW, la MMU est simulée par le programme de traduction binaire, partiellement ou totalement. L'implémentation exacte varie suivant que l'on parle du projet Daisy, des processeurs Transmetta ou de Denver. Sur les processeurs Transmetta, il n'y a pas de MMU ni de mémoire virtuelle. A la place, le programme de traduction binaire émule la mémoire virtuelle du processeur source. Le processeur VLIW ne gère que des adresses physiques, rien d'autre. L'espace d'adressage physique du processeur VLIW a la même taille que celui du processeur émulé, ici des processeurs x86. Il est vraisemblable que les adresses physiques utilisées par le processeur x86 sont les mêmes que celles du processeur VLIW. Sur le projet Daisy, le processeur VLIW gère la mémoire virtuelle via pagination, comme les processeurs émulés. Ce qui fait qu'il gère un espace d'adressage virtuel et un espace d'adressage physique. Pour éviter toute confusion, nous parlerons d'adresse physique/virtuelle VLIW pour les adresses physiques/virtuelles du processeur VLIW, d'adresse virtuelle/physique source pour celle du jeu d'instruction traduit, à savoir du Power PC ou du s390, éventuellement de l'x86. Il faut alors faire le lien entre adresses physiques source et adresses virtuelles VLIW. Pour cela, rien de plus simple : il y a correspondance parfaite. L'adresse physique source numéro N correspond à l'adresse virtuelle VLIW numéro N. Cependant, cela signifie que tout l'espace d'adressage virtuel VLIW serait utilisé par le code à traduire. En réalité, il faut ajouter de la place pour le code traduit et le programme de traduction binaire. La conséquence est que l'espace d'adressage virtuel VLIW est plus large que l'espace d'adressage source. L'espace d'adressage virtuel VLIW est découpé en trois sections : une section pour le code Power PC à traduire, une autre pour le code traduit en VLIW, et une dernière réservée au traducteur binaire. Le programme de traduction est placé dans une ROM mappée en mémoire dans la seconde section. Le reste de la seconde section est réservé à une mémoire RAM utilisée par le programme de traduction. [[File:Mémoire virtuelle sur le projet Daisy.png|centre|vignette|upright=1.5|Mémoire virtuelle sur le projet Daisy]] Il faut noter que chaque page physique source, une fois traduite, correspond à N pages virtuelles VLIW. La raison est que le code traduit prend plus de place que le code machine originel. En conséquence, on doit utiliser une page finale plus grande. Pour le reste, des pages contiguës en mémoire physique source sont elles aussi contiguës en mémoire virtuelle VLIW. Le calcul d'adresse est donc simple : l'adresse physique source est multipliée par N, puis on ajoute l'adresse de base à laquelle commence la section pour le code traduit. ==Les processeurs à jeu d'instruction reconfigurable== Nous venons de voir comment accélérer en matériel la traduction binaire. Mais il existe une alternative qui permet de se passer de traduction binaire, complétement. L'idée est d'avoir un processeur avec un '''jeu d'instruction reconfigurable'''. Le nom est assez parlent : on peut configurer le processeur de manière à supporter le jeu d'instruction de son choix ! Par exemple, on pouvait reconfigurer un CPU de manière à ce qu'il supporte le jeu d'instruction x86, ou qu'il exécute du ''bytecode'' Java, ou du code ARMv8, etc. De tels processeurs sont encore plus rare que ceux vu avant dans ce chapitre, mais ils ont existé ! Les processeurs de ce type avaient un ''microcode réinscriptible'', à savoir que c'était des processeurs microcodés pour lesquels on pouvait modifier le microcode. L'idée était qu'en changeant le microcode, on changeait le jeu d'instruction du processeur. Pour cela, le microcode était associé à une mémoire SRAM dans laquelle le microcode était copié au démarrage du CPU. Il y avait donc une micro-ROM et une micro-RAM. Les instructions étaient décodées en utilisant le microcode dans la micro-SRAM, la micro-ROM ne servait qu'au démarrage. En modifiant le contenu de la micro-SRAM, on pouvait altérer le microcode, voire le remplacer totalement. ===Le Burroughs B1700 : architecture externe=== Un exemple est celui du Burroughs B1700, et il est intéressant d'étudier en détail son architecture. Son architecture est décrite dans le livre "Interpreting Machines : Architecture and Programming of the Bl700/Bl800 Series", écrit par Elliott I. Organick et James A. Hinds. Une partie de ce qui vfa suivre est un très court résumé de ce livre. Il disposait de 4 registres généraux, nommés X, Y, T et L, tous de 24 bits. Les registres T et L étaient découpés en 6 sous-registres de 4 bits, nommés LA LB LC LD LE et LF pour le registre L, et TA TB TC TD TE et TF pour le registre T. Ils étaient adressables comme n'importe quel registre. Un point important est que le registre T avait des possibilités d’extraction spécifiques. Il était possible d'extrait N bits, placés n'importe où dans ce registre, et de les copier dans un autre registre. Nous verrons pourquoi cette possibilité est intéressante et ne sert que pour le registre T. Les registres X et Y étaient reliés à une ALU de 24 bits capable d'effectuer des opérations arithmétiques basiques. Fait étonnant, l'ALU faisait tous les calculs en même temps et fournissait ses résultats dans 7 registres : un registre SUM pour l'addition, un registre DIFFERENCE pour la soustraction, et un registre par opération bit à bit. Les registres pour les opérations bit à bit sont : CMPX et CMPY pour l'opération NOT sur chaque opérande, XANY pour le ET bit à bit, XEOY pour le XOR, XORY pour le OU logique. Les additions et soustractions pouvaient se faire sur des opérandes codés en binaire ou en BCD, suivant le code opération utilisé. A cela, il fallait ajouter deux registres de "résultat" MSKX et MSKY. Ils fournissaient un masque dépendant de la taille du résultat en bits. Cela permettait de gérer des données de 8 ou 16 bits, alors que l'ALU faisait 24 bits. Et cela n'était pas limité à 16 ou 8 bits, mais n'importe quelle taille inférieure ou égale à 24 bits était supportée, même des tailles non-conventionnelles comme des résultats codés sur 3 bits, 9 bits, 12 bits, etc. L'ALU fournissait 12 conditions, qui alimentait un registre d'état. Le registre d'état était segmenté en trois registres appelés XYST, XYCN et BICN. Le processeur contenait de nombreux autres registres de 4 bits, dont les sous-registres de T et L. En tout, il y a 27 registres de 4 bits adressables ! Et le processeur disposait d'une ALU 4 bits pour manipuler ces registres de 4 bits ! Elle est capable de faire des MOV entre registres de 4 bits, des opérations bit à bit, et peut aussi tester si un bit vaut 0 ou 1. La dernière possibilité est très utile pour implémenter les branchements. Pour communiquer avec les entrées-sorties, il disposait de deux registres CMND et DATA : CMND pour envoyer une commande à une entrée-sortie, DATA pour lire ou écrire une donnée. Pour la mémoire, il avait deux registres séparés : READ et WRITE. READ contient la donnée lue lors d'une lecture,; WRITE est pour une donnée à écrire lors d’une écriture. Il s'agit de registres d’interfaçage mémoire, qui ont été promus au rang de registres architecturaux. Les deux registres sont gérés par le microcode, qui est le langage principal de la machine. Les instructions machines configurées font sans, mais le microcode doit y avoir accès facilement. Et vu que le microcode est le langage principal de ce processeur, ces deux registres deviennent architecturaux. ===Une architecture bit-adressable=== Si avoir un microcode réinscriptible est suffisant, quelques choix de ''design'' facilitent l'émulation de jeux d'instructions variés. Par exemple, le CPU Burroughs B1700 avait ajouté une fonctionnalité très importante : il était bit-adressable. En clair, il pouvait adresser la mémoire bit par bit, si nécessaire. L'avantage est que cela permettait d'émuler des architectures dont la taille des registres/''byte''/mots était très différente. Un autre avantage est que cela facilitait la gestion des instructions de taille variable. Détaillons rapidement le premier avantage. Le processeur était un CPU 24 bits, mais il pouvait émuler des processeurs 16 bits, 8 bits, 4 bits, 1 bit, 9 bits, ou toute autre valeur. Pour cela, le processeur lisait des mots de 24 bits et utilisant un ''barrel shifter'' pour sélectionner les bits adéquats. Par exemple, pour un accès de 16 bits, la donnée lue en mémoire était masquée de manière à ne garder que les 16 bits de poids faible. Le ''barrel shifter''/circuit de masquage était placé directement avant le bus mémoire, sur le trajet des données/instructions. Le ''barrel shifter'' était commandé par un registre ''Field Unit'', qui précisait quelle était la taille des données à charger. ===La gestion de la taille des instructions=== Les instructions à décoder n'ont pas de longueur fixe, à savoir qu'elles peuvent faire 16 bit avec tel jeu d'instruction, 8 bits avec un autre, 32 bits sur un autre, etc. Elles peuvent aussi être de longueur fixe ou variable ! Le B1700 intégrait donc un support des instructions de longueur variable, qui était "détourné" pour gérer des jeux d'instruction différents. Notons que vu que l'architecture est bit-adressable, les contraintes d'alignement des instructions disparaissent, ce qui a des avantages qu'on va détailler dans ce qui suit ! L'implémentation des instructions de taille variable ajoutait un registre avant le registre d'instruction. Le registre d'instruction proprement dit était un registre M de 24 bits. Il était précédé par un registre T, dans lequel l'instruction était assemblée, en combinant plusieurs morceaux. Si l'instruction est codée est codée sur moins de 24 bits, le registre T se contente de masquer les bits en trop. Si l'instruction chargée dépasser 24 bits, le chargement se faisait en plusieurs passes, avec plusieurs lectures en mémoire. Dans ce qui suit, on suppose que l'instruction est composée de '''champs''' : l'opcode, les opérandes, les adresses absolues, les constantes immédiates, les numéros de registre, etc. L'idée est que l'instruction est chargée dans le registre T, puis découpée en champs, qui sont copiés dans les registres du processeur ou envoyés au microcode. Je rappelle que le registre T est relié à un ''barrel shifter'', différent au précédent, qui permettait de masquer, décaler et extraire une donnée de ce registre. Et il y a la possibilité d'extraire une suite de bit de ce registre T, pour la copier dans un autre registre. Par exemple, prenons une instruction composée d'un opcode, d'une constante immédiate et d'une adresse immédiate. L'opcode est lu et interprété par le microcode. Le microcode extrait la constante immédiate du registre T et la copie dans le registre X. Ensuite, l'adresse est extraite et envoyée au registre FA, une lecture est démarrée, et l'opérande est copiée dans le registre Y. Enfin, l'instruction est exécutée par le microcode. Le tout prend plusieurs micro-opérations : une première pour lire l'opcode, une seconde pour lire la première opérande, une troisième pour lire la seconde opérande, una quatrième pour tout exécuter. Notons que suivant la taille des champs, on peut en charger plusieurs à la fois. Par exemple, imaginons qu'on ait chargé 24 bits, contenant un opcode de 8 bits et une constante immédiate de 16 bits : pas besoin de faire deux accès mémoire, le registre T est juste découpé en un opcode et une constante. Le registre T est fait pour ce genre d'extraction. ===Le chargement d'un champ=== L'architecture est bit-adressable car elle gére des champs qui ne font pas forcément 8 ou 16 bits. Par exemple, certains jeux d'instructions ont des opcodes codés sur 6 ou 7 bits, voire 9-10 bits. Par exemple, prenons un opcode de 7 bits, suivi par une adresse de 12 bits et une constante immédiate de 15 bits : aucun de ces champs ne sera aligné sur 8/16/24 bits. Utiliser une architecture bit-adressable règle le problème d'alignement. Et en général, les contraintes d'alignement des instructions disparaissent ! Quand on charge une instruction, peut importe qu'elle soit ou non alignée sur 24 bits, elle sera placée dans les bits de poids faible, il suffira de masquer les bits de poids fort inutiles. Par exemple, si on charge une instruction de 16 bits, les 16 bits seront copiés dans les 16 bits de poids faible du registre T, il suffira de masquer l'octet de poids fort. Pas besoin de faire de décalages pour gérer des instructions mal alignées... Pour charger une instruction, il faut connaitre plusieurs informations : l'adresse de son premier bit, sa taille. Pour cela, le B1700 contient deux registres, nommés ''Field Adress'' (FA) et ''Field Length'' (FL), qui indiquent l'adresse du premier bit et la taille de l'instruction à charger. Le premier fait 24 bits, ce qui permet de gérer des adresses de 24 bits. N'oubliez pas que le processeur est bit-adressable, ce qui permet d'adresser 16 Mébi-bits, soit 2 mébioctets. Le registre pour la taille fait lui 16 bits, ce qui permet de gérer des instructions de 65536 bits, soit 8192 octets ! Leur nom commence par ''field'', car le processeur peut gérer des instructions de taille variable et/ou plus longues que 24 bits. Dans ce cas, le chargement de l'instruction se fait champ par champ. Pour donner un exemple, prenons une instruction composée de 20 champs de 12 bits chacun. Les champs sont chargés un par un dans le registre T. Le registre FA est alors initialisé avec l'adresse de l'instruction, il est incrémenté de 12 à chaque cycle. Le registre FL est initialisé avec la taille de l'instruction, soit 20 * 12 = 240 bits, et il est décrémenté de 16 à chaque cycle. Précisons cependant que la taille d'un champ peut être changée d'un cycle à l'autre. Par exemple, le processeur peut charger un opcode de 8 bits, l'interpréter, détecter que l'instruction demande ensuite de charger une adresse de 16 bits. Dans ce cas, le registre FL est immédiatement altéré pour charger 16 bits, au lieu des 8 bits de l'opcode immédiatement précédent. ===Le microcode réinscriptible=== Le microcode utilise des micro-opérations de 16 bits de long, ce qui est très peu et bien en-dessous des micro-opérations des autres processeurs. Les micro-opérations sortant du microcode sont envoyées au registre M, un registre de micro-opération de 16 bits. les micro-opérations écrites dans ce registre sont ensuite décodées par le hardware et exécutées. Fait intéressant, il était possible de faire un OU logique entre la micro-opération sortante et un registre du processeur, le résultat étant mémorisé dans le registre M. Cela permettait de faire du code automodifiant dans le microcode ! Utiliser du code automodifiant avec un microcode réinscriptible est parfaitement possible, et c'est utile pour émuler des modes d'adressage complexe dans le microcode. Cela permettait par exemple de remplacer une adresse dans une micro-opération. L'adresse est alors surimposée avec un OU dans le champ d'adresse, alors que le reste de l'instruction n'est pas modifié. Mais modifier directement le microcode est rarement une bonne idée. Aussi, pour éviter cela, l'idée est de modifier les instructions avec un OU logique avec un autre registre. Les micro-opérations du microcode ne sont donc pas modifiées dans le microcode, mais en sortie du microcode. ==Les jeux d'instructions dédiés à un langage de programmation== De rares processeurs sont conçus pour un langage de programmation en particulier. On appelle ces processeurs, conçus pour des besoins particuliers, des '''processeurs dédiés'''. Par exemple, les fameux ''Burrough E-mode'' B5000/B6000/B7000 étaient spécialement conçus pour exécuter de l'ALGOL-60. Leurs cousins B2000/B3000/B4000 étaient eux conçus pour le COBOL. Des langages fonctionnels ont aussi eu droit à leurs processeurs dédiés. Le prolog en est un bel exemple, avec les superordinateurs de 5ème génération qui lui étaient dédié. On peut aussi citer les machines LISP, dédiés au langage LISP, qui datent des années 1970. Elles étaient capables d’exécuter certaines fonctions de base du langage directement dans leurs circuits : elles possédaient notamment un ''garbage collector'' câblé dans ses circuits ainsi que des instructions machines supportant un typage déterminé à l’exécution. Les processeurs dédiés ont eu leur heure de gloire au début de l'informatique, à une époque où les langages de haut niveau venaient d'être inventés. À cette époque, les compilateurs n'étaient pas performants et ne savaient pas bien optimiser le code machine. Il était alors rationnel, pour l'époque, de rapprocher le code machine cible et le langage de programmation de haut niveau. De nombreuses architectures dédiés ont ainsi été inventées, avant que les concepteurs se rendent compte des défauts de cette approche. Les défauts en question ne sont pas nombreux, mais assez simples à comprendre. Premièrement, elles sont très rapides pour un langage de programmation en particulier, mais sont assez mauvaises pour les autres, d'où un problème de "compatibilité". Ajoutons à cela que les langages de programmation peuvent évoluer, devenir de moins en moins populaires/utilisés, ce qui rend la création d'architectures généralistes plus pertinente. Enfin, les architectures dédiées sont évidemment des processeurs CISC, pour implémenter les nombreuses fonctionnalités des langages évolués. Et les défauts des CISC sont assez rédhibitoires à l'heure actuelle. À l'heure actuelle, les algorithmes des compilateurs se sont améliorés et savent nettement mieux utiliser le matériel. Ils produisent du code machine efficace, ce qui rend les architecture dédiées bien moins intéressantes. Si on ajoute les défauts de ces architectures dédiées, par étonnant que les architectures dédiées aient presque disparues. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'accélération matérielle de la virtualisation | prevText=L'accélération matérielle de la virtualisation | next=Le matériel réseau | nextText=Le matériel réseau }} </noinclude> klb41w66ofoy93gmzcdviad2au66u44 764023 764019 2026-04-19T18:11:53Z Mewtow 31375 /* Le microcode réinscriptible */ 764023 wikitext text/x-wiki De nos jours, la majorité des programmeurs programment dans des langages de haut niveau. Il est très rare pour eux d'avoir à utiliser de l'assembleur, et encore moins un langage machine. Les programmes écrits dans un langage de haut niveau sont traduits en langage machine par un logiciel appelé le compilateur. [[File:Compilation.PNG|centre|vignette|upright=2.5|Compilateur : principe.]] Il se trouve que quelques rares processeurs sont conçus pour faciliter le travail du compilateur. Et ce sont ces processeurs que nous allons voir dans ce qui suit. Ils représentent une catégorie de jeux d'instruction à part, qui n'a pas vraiment de nom. ==La compilation et l’interprétation== Un compilateur traduit donc du code source, écrit dans un langage de haut niveau, vers du langage machine. La traduction est rarement directe. En général, le compilateur traduit le code source en assembleur, qui est lui-même traduit en langage machine. L'assembleur est une représentation textuelle du langage machine alors que le code machine est du binaire exécutable par le processeur. La traduction se fait donc en deux étapes, la '''compilation''' proprement dite et l''''assemblage''', réalisées respectivement par un compilateur et un assembleur. Il faut parfois rajouter une troisième phase d'édition des liens, que nous passons volontairement sous silence. {| |[[File:Code C.png|vignette|200px|Le code source, ici en C.]] |[[File:Assem.png|vignette|250px|Le code assembleur.]] |[[File:Binary file - hello world (C programming).png|vignette|150px|Le code machine.]] |} ===L'interprétation et la compilation à la volée=== Une alternative à la compilation est l''''interprétation''', qui transforme le code source en code machine à la volée. Avec l'interprétation, le code source est passé à un logiciel appelé l''''interpréteur''', qui exécute le code source ligne de code par ligne de code. Une ligne de code est traduite en code machine, qui est exécutée, puis l'interpréteur passe à la ligne suivante. [[File:Kaantotulkkaus.png|centre|vignette|upright=3|Compilation versus interprétation.]] Un défaut de l'interprétation est son cout en performance, qui est assez important. Aussi, l'interprétation stricte a évolué vers un hybride entre interprétation et compilation. L'idée est qu'une partie peu importante du code source est interprétée, alors que le code important est compilé juste avant d'être exécuté. Pour le dire autrement, le code source est partiellement compilé à la volée, juste avant son exécution. Aussi, on parle de '''compilation à la volée'''. Le terme en anglais est ''Just In Time compilation'', abrévié en JIT. En soi, le JIT est associé à de l'interprétation, les JIT sont en réalité des hybrides interpréteurs-compilateurs. Un point important est que compiler du code à la volée est assez lourd, cela a un cout en performance. Par contre, le code compilé s'exécute plus vite que du code interprété. Si on exécute le code compilé une seule fois, le cout de la compilation l'emporte sur le gain à l'exécution. Mais le code compilé est réutilisé autant de fois que nécessaire. Le gain à l'exécution est donc multiplié par le nombre d'exécutions, alors que le cout de la compilation est répartit sur plusieurs exécutions, il est amorti. Les portions du code sont choisies de manière à avoir un gain en performance maximal. L'idée générale est que le code exécuté très souvent est compilé, alors que du code exécuté pas souvent est interprété. Typiquement, les boucles les plus souvent exécutées sont compilées, le code exécuté une seule fois est interprété. ===Les langages intermédiaires=== Les compilateurs modernes passent par un '''langage intermédiaire''' pour faire la transformation en code machine. Le compilateur traduit le langage de haut niveau en langage intermédiaire, puis traduit le langage intermédiaire en code machine cible. Le langage intermédiaire est parfois appelé le '''''bytecode''''', et ce terme recouvre aussi le code écrit avec du ''bytecode''. L'interprétation aussi peut passer par l'intermédiaire d'un un ''bytecode'', ce n'est pas limité aux compilateurs proprement dit. {| |[[File:Intermediate representation scheme.png|centre|vignette|upright=1.5|Représentation intermédiaire d'un compilateur.]] |[[File:Code interpreter scheme.png|centre|vignette|upright=1.5|Fonctionnement d'un interpréteur.]] |} Faire ainsi a de nombreux avantages, le principal étant d'avoir un compilateur capable de traduire un langage de haut niveau vers plusieurs jeux d'instructions différents. Cela permet d'avoir, par exemple, un compilateur qui traduit du C soit en code machine x86, soit en code machine pour un CPU ARM, soit pour un CPU POWERPC, etc. [[File:Compiler design.svg|centre|vignette|upright=3|Compiler design]] Pour l'interprétation, l'usage d'un ''bytecode'' a d'autres avantages. Pour l'exploiter, il faut distribuer non pas le code source, mais le ''bytecode''. En clair, l'interpréteur ne prend pas en entrée le code source, mais du ''bytecode'' déjà compilé. La traduction est alors beaucoup plus simple, car le code source a déjà été partiellement compilé, par la traduction en ''bytecode''. Interpréter du code source est en effet assez compliqué : il faut effectuer des étapes d'analyse lexicale, sémantique, et bien d'autres. Avec du ''bytecode'', ces étapes ont été réalisées lors de la compilation du ''bytecode'', l'interpréteur a alors peut de choses à faire. L'interprétation du ''bytecode'' se fait instruction par instruction, au niveau du ''bytecode''. Une instruction du ''bytecode'' est traduite en une instruction machine équivalente, qui est exécutée. Si l'instruction est un peu complexe, il exécute une fonction/procédure qui fait la même chose. Pour résumer, le ''bytecode'' est ensuite traduit à la volée et exécuté instruction par instruction par un logiciel appelé l'interpréteur. [[File:Control table.png|centre|vignette|upright=2|Control table]] Le langage intermédiaire peut être vu comme l'assembleur d'un processeur, n'existe pas forcément dans la réalité, mais dont le jeu d'instruction est décrit en détail. Le processeur en question est appelé une '''machine abstraite''', ou encore une ''machine virtuelle''. Nous utiliserons le terme de machine abstraite dans ce qui suit. La machine abstraite n'est pas la même suivant que l'on cible la compilation ou l'interprétation/JIT. Pour l'interprétation/JIT, la machine abstraite est souvent une machine à pile, car cela simplifie grandement la traduction en code machine final. L'interprétation demande que la traduction du ''bytecode'' en code machine soit la plus simple possible. Et cela demande de prendre en compte pas mal de détails. Par exemple, les différents jeux d'instruction existants n'ont pas le même nombre de registres, ce qui pose problème lors de la traduction du ''bytecode''. Lors de la transformation en code machine, un algorithme d'allocation de registres se débrouille pour traduire le code intermédiaire en code qui utilise un nombre limité de registres. Pour améliorer cette allocation de registres, il y a deux solutions niveau ''bytecode'' : soit utiliser un nombre illimité de registres, soit utiliser une machine à pile. Pour les machines à pile, il existe un algorithme simple et rapide pour traduire un code écrit pour une machine à pile en un code écrit pour un processeur avec des registres, qui se débrouille pas trop mal pour allouer efficacement les registres. C'est un avantage assez important pour les langages interprétés. ==Les processeurs qui exécutent du bytecode== Le ''bytecode'' est un code machine, ce qui signifie qu'il peut en théorie s'exécuter sur un processeur qui implémente le jeu d’instruction associé. Si le jeu d'instruction d'un ''bytecode'' est souvent une description censée être fictive, elle n'en reste pas moins un jeu d'instruction et des caractéristiques précises, qu'on peut l'implémenter en matériel ! Néanmoins, tous les ''bytecode'' ne sont pas égaux de ce point de vue. Certains sont faciles à implémenter en matériel, d'autres non. Les ''bytecodes'' implémentés en matériel sont ceux dont les machines abstraites sont des machines à pile. Un premier exemple est celui des processeurs Pascal MicroEngine, qui exécutaient directement le bytecode du langage Pascal, le fameux UCSD P-code. Un second exemple est la '''machine SECD''', qui sert de langage intermédiaire pour certains compilateurs de langages fonctionnels. Elle a été implémentée en matériel par plusieurs équipes, notamment par les chercheurs de l'université de Calgary en 1989. Dans le même genre, quelques processeurs simples étaient capables d’exécuter directement le ''bytecode'' du langage FORTH. Le FORTH, un des premiers langages à pile de haut niveau, possède de nombreuses implémentations hardware et est un des rares langages de haut niveau dont le ''bytecode'' a été utilisé comme langage machine sur certains processeurs. Par exemple, on peut citer le processeur FC16, capable d’exécuter nativement du ''bytecode'' FORTH. ===Les processeurs Java=== Le cas le plus impressionnant est celui de la machine virtuelle Java, qui est un design de processeur comme un autre. En temps normal, le ''bytecode'' Java est compilé ou interprété, mais certains processeurs exécutaient du ''bytecode'' Java directement, sans interprétation ni compilation. Ils sont appelés des '''processeurs Java'''. On peut citer les processeurs ARM disposant de l'extension Jazelle, du JEMCore et du aJ-100 de aJile Systems, le picoJava II de Sun, et quelques autres. Il y a aussi eu quelques projets de recherche ou processeurs open source, comme Komodo, jamuth, le ''Java Optimized Processor'' et quelques autres projets du même genre. L'implémentation de la machine virtuelle Java n'était cependant pas complète. Quelques instructions complexes n'étaient pas gérées par le processeur et devaient être émulées en logiciel. Mais la grosse majorité des instructions l'était. Les instructions non-supportées étaient émulées avec la méthode ''trap and emulate'', à savoir qu'un opcode inconnu déclenchait une exception matérielle ''opcode invalide'', dont la routine pouvait émuler les instructions qui doivent l'être. Le processeur aJ-100 de aJile est capable d'exécuter la quasi-totalité des instructions de la JVM Java, seules deux instructions faisant exception. Les '''accélérateurs Java''' sont une solution intermédiaire, qui permet de traduire à la volée du ''bytecode'' Java en instructions RISC, exécutées sur un autre processeur. Il s'agit formellement de traduction binaire, mais je préfère en parler ici. L'idée est de combiner un processeur RISC avec un coprocesseur Java. Le coprocesseur lit le ''bytecode'' Java et le traduit en instructions RISC, ces dernières étant exécutées sur le processeur RISC. Le JA108 de Nozomi était un coprocesseur de ce genre, au même titre que le coprocesseur JSTAR de JEDI. L'extension Jazelle de certains CPU ARM fonctionnait vraisemblablement sur un principe similaire, sauf que le coprocesseur était intégré dans le processeur ARM, entre le cache d'instruction et le décodeur d'instructions ARM. L'intérêt des processeurs Java n'est pas qu'une question de performance. Certes, ils permettent d’exécuter plus vite les programmes compilés en ''bytecode'', comme des programmes Java pour la JVM Java. Mais l'intérêt est aussi de faire des économies de mémoire RAM et ROM. Je rappelle que la machine virtuelle Java est une machine à pile, ce qui fait que sa densité de code est excellente. Les programmes en ''bytecode'' Java sont donc très petits, et prennent peu de place en mémoire ROM. De plus, utiliser un processeur Java permet de se passer d'interpréteur ou de JIT Java, ce qui économise encore plus de mémoire. ===Transformer un processeur RISC en machine à pile=== La plupart des processeurs Java utilisent en interne une architecture à registre généraux, qui émule une machine à registre en microcode. Quelques registres sont réservés pour l'état de la machine virtuelle, avec au minimum un registre réservé pour le pointeur de pile, un autre pour un pointeur vers un ''pool'' de constantes, et quelques autres. D'autres registres servent à mémoriser le haut de la pile, à savoir le sommet de la pile et quelques opérandes situées juste en-dessous. Par exemple, le processeur picoJava II gardait les 64 opérandes au sommet de la pile dans 64 registres généraux, séparés des registres pour les pointeurs et l'état de la JVM. Une optimisation possible, envisagée sur les processeurs picoJava, était une sorte de macro-fusion sous stéroïde. L'idée était de fusionner certaines séries d'instructions Java en une seule instruction machine. Typiquement, pour faire une opération arithmétique, une machine à pile empile deux opérandes et exécute une opération, ce qui prend trois instructions machine. La spécification picoJava 1 a proposé d'envoyer directement la seconde opérande en entrée de l'ALU, ce qui fusionnait l'empilement de la seconde opérande et l'opération arithmétique. Le processeur picoJava II va plus loin et fusionne les trois instructions en une seule instruction RISC, agissant sur des registres. Mais la majorité des processeurs Java ne faisaient pas cela, car cela complexifie grandement le décodeur. Les processeurs ''bigfoot'' de DCT utilisaient un système différent pour émuler une pile avec des registres. Leur banc de registre de 64 bits était coupé en deux : les 16 premiers registres étaient utilisés pour la pile, les 48 suivants étaient utilisés pour autre chose. L'idée est que le registre 0 mémorise un emplacement vide, le registre 1 contient le sommet de la pile, le registre 2 l'opérande sous le sommet de la pile, et ainsi de suite. Les 15 premières opérandes de la pile sont donc mémorisées dans les registres. Les instructions écrivent leur résultat dans le registre adéquat, puis tous les registres sont décalés pour que le résultat passe dans le registre R1. Par exemple, les instructions PUSH chargent une opérande dans le registre 0, puis tous les registres sont décalés de un rang, pour que l'opérande chargée soit dans le registre 1. Le registre 0 devient alors le registre numéro 1, le registre 1 devient le 2, et ainsi de suite. Par contre, les opérations arithmétiques font autrement. Une instruction d'addition, par exemple, lit les opérandes dans le registre 1 et 2, puis place son résultat dans le registre 2 (les deux opérandes sont dépilées). Puis, les registres sont décalés d'un rang pour que ce registre 2 devienne le 1. Les registres ne sont en réalité pas décalés, il n'y a pas de transferts entre registres. A la place, les 16 premiers registres utilisaient une forme limitée de renommage de registres, similaire au banc de registre tournant utilisé sur les processeurs EPIC comme l'Itanium. Les numéros de registres sont renommés, décalés, à chaque fois qu'on empile ou dépile une opérande. Pour faire le renommage de registres, processeur contient un ''register stack counter'' de 4 bits, qui est incrémenté à chaque fois qu'on empile une donnée, et est décrémenté quand on dépile une donnée. Quand on veut accéder à un opérande, le numéro de registre architectural associé est traduit en numéro de registre physique, en additionnant le ''register stack counter'' (modulo 16, pour rester sur 4 bits). ==L'accélération de l'interprétation/JIT : le ''Thumb-EE'' d'ARM== Quelques processeurs ont ajouté des instructions pour faciliter le travail des interpréteurs/JIT. Par exemple, en 2005, ARM a ajouté le mode ''Thumb Execution Environment'', qui faisait cela. Il reprenait le jeu d'instruction compact ''thumb'' et ajoutait quelques instructions et modifiait le comportement d'instructions existantes. Le jeu d'instruction ''Thumb-EE'' a été ajouté sur les CPU ARM en 2005, mais a été déprécié en 2011, par manque d'utilité. Mais la tentative mérite qu'on s'attarde dessus. Les CPU de gamme M, déstinés à l'embarqué, n'ont jamais supporté ''Thumb-EE'', vu que de tels processeurs ne sont pas conçus pour exécuter du code interprété/JIT. Le mode ''Thumb-EE'' est un mode d'exécution, séparé de l'ARM normal. Ainsi, une instruction ''Thumb-EE'' et une instruction ARM peuvent avoir le même encodage en binaire, mais se comporter différemment. Le processeur est à tout moment soit en mode ''Thumb-EE'', soit en mode ARM normal, ce qui précise comment ces instructions doivent se comporter. Des instructions ont été ajoutées par ''Thumb-EE'', pour qu'il fasse son travail. L'entrée et la sortie du mode ''Thumb-EE'' se fait avec deux instructions : ENTERX et LEAVEX. Une fois en mode ''Thumb-EE'', le décodage des instructions ''thumb'' se fait avec les règles du ''Thumb-EE'', et non celles du ''thumb'' normal. Une première différence entre ''thumb'' normal et ''Thumb-EE'' est l'ajout de ce qui s'appelle un ''null check'' pour les instructions mémoire. Le ''null check'' est utilisé pour les instructions en mode d'adressage "base + indice" ou "Base + décalage", ou tout autre mode d'adressage avec un registre de base utilisés dans des calculs d'adresse. L'idée est de vérifier si l'adresse de base vaut zéro ou non. Si c'est le cas, le processeur lève une exception matérielle, qui est traitée par l'interprétation ou le compilateur JIT. Une telle situation est en effet signe d'une erreur d'adressage mémoire, qui doit être traitée par l'interpréteur. Typiquement, c'est signe qu'un pointeur n'a pas bien été initialisé, ce qui peut arriver avec du code interprété ou JIT. Les interpréteurs/JIT traitent généralement la situation en ajoutant un test avant toute instruction mémoire, pour vérifier si le pointeur accédé vaut zéro ou non. Avec ''Thumb-EE'', pas besoin d'ajouter les tests en questions : ils sont réalisés automatiquement lors de chaque instruction mémoire. : Les registres pour la pile, comme le pointeur de pile et le pointeur de ''frame'', sont aussi concernés. L'instruction CHKA vérifie que les accès à un tableau ne débordent pas en dehors du tableau en question. Par exemple, pour un tableau de 1024 éléments, elle vérifie si l'indice est compris entre 0 et 1024. Pour cela, elle vérifie si l'indice de tableau est bien dans l'intervalle adéquat et lève une exception matérielle si ce n'est pas le cas. Son utilité se comprend quand on sait que tous les accès à un tableau sont vérifiés par l'interpréteur. L'interpréteur est censé ajouter des instructions pour vérifier les indices, à savoir deux ou trois branchements. Avec CHKA, le test se fait en une seule instruction. D'autres modifications mineures de l'encodage des instructions sont aussi présentes, ainsi que des modifications pour les instructions LOAD/STORE. Précisément, dans les modes "base + indice" et "base + décalage", le décalage et l'indice subissent maintenant des décalages. L'idée est de simplifier le calcul d'adresse. De quoi économiser quelques instructions lors des calculs d'adresse, ce qui facilite le travail de l'interpréteur/JIT. Et des instructions LOAD/STORE précises voient leur comportement modifié pour faciliter la gestion de la pile de Java ou l'exécution des méthodes locales. Pour faciliter le travail de l'interpréteur, deux instructions HBP et HBLP ont été ajoutées. Elles branchent vers une fonction qui gère les exceptions logicielles (une fonctionnalité présente dans de nombreux langages, comme Java). La première effectue un branchement vers la fonction en question, la seconde sauvegarde l'adresse de retour avant de faire ce branchement. Les deux peuvent brancher vers 256 fonctions pré-déterminées, sans avoir à présenter leur adresse. ==La traduction binaire accélérée par le matériel== Plus haut, nous avons surtout parlé des compilateurs et des interpréteurs. Cependant, nous devons aussi parler de la '''traduction binaire'''. Elle traduit un programme écrit dans un code machine vers un autre code machine. Par exemple, elle traduit un programme compilé pour un CPU x86 vers un code machine ARM. En général, le système d'exploitation est généralement compilé pour le jeu d'instruction natif, mais il exécute des applications prévues pour le x86. Les applications sont traduites par le système d'exploitation, avant d'exécuter le code traduit. : Nous parlerons dans la suite de code machine source et de code machine cible, pour parler respectivement du code à traduire et du code obtenu après traduction. Idem avec d'autres termes comme architecture cible/source, ou jeu d'instruction cible/source. La traduction binaire est surtout utilisée pour des questions d'émulation ou de compatibilité. Par exemple, lorsque les Macintosh sont passés de processeurs Power PC vers des processeurs x86, le système d'exploitation Mac OS utilisait la traduction binaire pour convertir les anciennes applications Power PC vers du code x86. Les utilisateurs n'y ont vu que du feu. Le système de traduction binaire était appelé Rosetta 1. Par la suite, lors de la transition de processeurs x86 vers des processeurs Apple, Rosetta 2 a vu le jour. La traduction binaire a surtout été utilisée pour traduire du code x86 vers un autre jeu d'instruction. Il faut dire que le x86 est le jeu d'instruction dominant. De nombreuses entreprises ont eu pour ambition de briser l'hégémonie du x86 sur PC, en remplaçant le x86 par un jeu d'instruction plus performant, tout en gardant une compatibilité maximale. La traduction binaire était la seule solution pratique. Elle était le plus souvent intégralement réalisée en logiciel, comme c'était le cas sur les architectures Itanium avec le ''IA-32 Execution Layer''. ===Les généralités sur la traduction binaire assistée en matériel=== L'architecture source est presque tout le temps une architecture CISC, assez ancienne, qu'on souhaite émuler. Le choix de l'architecture cible se porte souvent sur une architecture VLIW, fort différente de l'architecture source, et ce pour obtenir une économie de matériel conséquente. Pour s'exécuter rapidement, le code traduit doit exploiter le parallélisme d'instruction, à savoir exécuter plusieurs instructions en même temps dans des unités de calcul séparées. Il est possible d'utiliser un processeur superscalaire avec exécution dans le désordre pour cela, mais au prix d'un cout important en transistors. Alors qu'en utilisant un CPU VLIW, c'est le traducteur binaire qui fait tout le travail d'extraction du parallélisme d'instruction. Et après tout, quitte à avoir un traducteur binaire, autant lui refiler le boulot d'optimisation. Compiler du code à la volée est certes assez lourd, mais qu'il existe des algorithmes efficaces pour regrouper des instructions indépendantes dans une seule instruction VLIW. Au passage, le processeur VLIW a plus de registres que le processeur source. Cela permet de faire du renommage de registres directement en logiciel. Le traducteur binaire n'hésite pas à changer les noms de registres entre instructions source et instruction VLIW cible, afin de supprimer des dépendances de données. En conséquence, cela ouvre des opportunités de parallélisme, qui sont exploitées lors du regroupement des opérations en instructions VLIW. Vous pourriez penser que le choix d'un CPU VLIW pour émuler du CISC est tout sauf optimal. Vous devez penser que la traduction binaire est d'autant plus simple que l'architecture source et cibles sont semblables. Dans les faits, ce n'est pas tellement traduire les instructions qui pose problème, mais plus la gestion du registre d'état, des exceptions matérielles, de la mémoire virtuelle, la différence entre gros-boutisme et petit-boutisme, de même que des différences d'adressage pour les périphériques. Le premier problème est la gestion des conditions, notamment en présence d'un registre d'état. En pratique, la traduction binaire s'utilise pour traduire du code CISC vers du code RISC ou VLIW. Les architectures source ont donc un registre d'état, qui est mis à jour non seulement par des instructions de test, mais aussi des instructions arithmétiques. L'architecture cible n'a elle pas de registre d'état, mais des registres à prédicats. La traduction de l'un vers l'autre est alors quelque peu compliquée. Et elle est d'autant plus compliquée que le registre d'état est la source de dépendances d'instruction implicites, qui réduisent les performances. Par exemple, si une instruction arithmétique modifie le registre d'état, cela peut impacter l'exécution d'une instruction ultérieure, qui lit ce registre d'état. Pour éliminer ces fausses dépendances, le traducteur binaire doit renommer ce registre en logiciel et détecter les dépendances utiles. Et le compilateur doit gérer les cas où il y a beaucoup de distance entre les deux, voire les cas où l'instruction dépendante n'a pas encore été analysée par le compilateur. Un autre problème est lié à la gestion des exceptions matérielles, et précisément des exceptions précises. Pour rappel, une instruction VLIW regroupe plusieurs opérations, opérations qui correspondraient à une instruction machine sur un CPU pas VLIW. En conséquence, plusieurs instructions du langage machine "source" sont regroupées en une seule instruction VLIW. Et il faut tenir compte du cas où une instruction source lève une exception. Dans le code VLIW, cela signifie qu'une opération lève une exception, et il faut annuler partiellement l'instruction VLIW associée. Et par partiellement, on veut dire que seules les opérations suivantes dans l'ordre du programme source doivent être annulées, puis ré-exécutées. Et c'est un sacré casse-tête ! Les processeurs VLIW utilisent comme solution le mécanisme d'exceptions différées des processeurs EPIC, vu il y a quelques chapitres. Pour résumer, le code est exécuté par blocs d'instruction, délimités par des branchements ou tout autre limite/barrière pertinente dans le code. Un bloc de code est compilé en deux versions : une version rapide sans exceptions matérielles, une version lente qui gère les exceptions précises. La version rapide s'exécute sans exécuter les exceptions. Cependant, les exceptions matérielles sont enregistrées, pour être prises en compte à la toute fin du bloc de code. La version rapide est exécutée en premier et elle mémorise si une exception matérielle a eu lieu. Puis une instruction vérifie si une exception a eu lieu et décide quoi faire. Si aucune exception n'a eu lieu, elle passe à la suite du programme, les résultats du bloc de code sont définitivement acceptés. Mais si une exception a eu lieu, tout est annulé. Le processeur est remis dans l'état initial, puis le code est ré-exécuté instruction par instruction de manière à gérer l'exception correctement. ===La traduction par pré-décodage=== Une première solution serait de faire la traduction binaire dans le cache d'instruction. Lors d'un défaut de cache, le code chargé depuis la RAM est traduit en code machine cible. Et c'est ce code machine cible qui est mémorisé dans le cache d'instruction et exécuté par le processeur. La technique marche sur le papier et n'est qu'une amélioration des techniques de pré-décodage vues il y a quelques chapitres. Cependant, le code obtenu est une traduction assez basique, qui n'incorpore pas d'optimisations dignes de ce nom. De plus, elle gère mal le cas où la taille du code source et cible sont potentiellement très différentes. Concrètement, elle est surtout utile pour traduire du code d'un processeur RISC vers un autre processeur RISC, dont les tailles d'instructions sont similaires. Elle ne permet pas d'émuler plusieurs jeux d'instructions différents, le cout en matériel (un circuit de pré-décodage par jeu d'instruction) serait trop important. ===Le projet DAISY d'IBM=== De très rares processeurs étaient conçus pour accélérer cette traduction binaire, afin de garder de bonnes performances. Les premiers à avoir étudié l'idée étaient IBM, avec leur projet DAISY (''Dynamically Architected Instruction Set from Yorktown''). Le projet de base était de convertir à la volée du code compilé pour des CPU Power PC, vers du code VLIW. Les chercheurs d'IBM avaient développé un algorithme de traduction dynamique efficace, ainsi qu'un processeur VLIW disposant d'optimisations spécifiques à la traduction binaire dynamique, à la volée. De nombreuses idées de ce projet ont été reprises ou re-découvertes par la société Transmetta, avec ses processeurs Crusoe et Efficieon, puis par NVIDIA avec son projet Denver. Les ingénieurs de ce projet ont étudié la possibilité d'émuler plusieurs jeux d'instructions différents sur un même processeur, notamment le s390 d'IBM et le x86. En théorie, cela demande juste d'avoir plusieurs programmes de traduction binaire : un pour le x86, un autre pour le s390, éventuellement un autre pour le Power PC. Mais cela ne s'est pas concrétisé. Mais les ingénieurs ont étudié quelle pourrait être l'architecture VLIW idéale pour ça. Par exemple, il fallait des additions 3-opérandes pour simplifier les calculs d'adresse. Une difficulté était la gestion du registre d'état, dont les bits ne sont pas mis à jour de la même manière sur le x86 et le s390 ou le Power PC. La différence entre gros-boutisme et petit-boutisme était aussi un problème, de même que des différences d'adressage pour les périphériques. Pour que la traduction binaire soit efficace, le processeur VLIW intègre diverses optimisations, comme les exceptions différées et les branchements multi-voies. La principale est la suivante : seul le code des boucles ou fonctions exécutées fréquemment est traduit en code VLIW, le reste du code est interprété. En effet, traduire du code binaire prend plus de temps qu'une simple interprétation. Pour du code qui ne sera exécutée qu'une seule fois, il est plus rapide d'utiliser l'interprétation. Par contre, pour du code exécuté beaucoup de fois, le cout de la traduction binaire est amorti, dilué sur N exécutions, compensé par le gain en temps d'exécution de ce code traduit. Au final, cela permet de ne traduire que le code qui le mérite. La détection du code fréquemment exécuté est réalisée dans le cœur VLIW. L'unité de branchement mémorise les derniers branchements rencontrés et le nombre de fois qu'ils ont été exécutés. Si ils ont été exécutés un certain nombre de fois, l'unité de branchement lève une exception matérielle, qui invoque le traducteur binaire. La routine de cette exception est le traducteur binaire proprement dit. En clair, la traduction binaire est démarrée quand le processeur détecte qu'une fonction a été exécutée plus de N fois, via une exception matérielle dédiée. La mesure du nombre d'exécution d'un branchement se fait dans l'unité de calcul dédiée aux branchements, avec l'aide d'une sorte de ''branch adress buffer'' modifié. Pour rappel, le ''branch adress buffer'' mémorise le ''Program counter'' de chaque branchement récemment rencontré. Ici, chaque entrée du ''branch adress buffer'' est associée à un compteur incrémenté à chaque exécution du branchement. ===Les processeurs Crusoe et Efficieon de Transmetta=== Les processeurs Crusoe Et Efficeron sont deux processeurs VLIW produits par la société Transmetta, une société californienne rachetée par NVIDIA. Ils étaient conçus pour exécuter spécifiquement des programmes x86, système d'exploitation inclus. L'idée derrière ce projet était d'exécuter du code x86 sans que les applications, ni même le système d'exploitation et le BIOS soient au courant ! Les processeurs Transmetta étaient en réalité conçus pour exécuter un programme unique, le ''Code Morphing Software'' (CMS), qui traduisait le code x86 en code VLIW. Transmetta ne voulait pas que son processeur VLIW soit exploité directement, sans le CMS. Il n'avait pas rendu public de compilateur pour traduire du C vers du code VLIW, il n'avait pas donné la documentation du jeu d'instruction VLIW. Des efforts de rétro-ingénieurie, documentés sur le site realworldtech, ont cependant permis de comprendre comment étaient encodées les instructions du processeur Crusoe. Comme pour le projet Daisy, le ''Code Morphing Software'' utilisait à la fois interprétation et traduction binaire, selon les besoins. Les instructions sont interprétées lors de leurs premières exécutions, mais le CMS bascule sur de la traduction en code VLIW après un certain nombre d'exécution. Ainsi, les boucles souvent exécutées sont traduites en code VLIW, alors que le reste du code est interprété. Le choix du code à traduire est le fait du CMS, il utilise des heuristiques complexes pour, qui ne sont pas connues dans le détail. Le ''Code Morphing Software'' est mémorisé dans une EEPROM, ce qui en fait un ''firmware'' situé en dessous du BIOS. Le CMS démarre ensuite le BIOS, qui lui-même démarre le système d'exploitation, qui lui-même démarre les pilotes de périphériques et les programmes. Le CMS se réserve les 16 premiers mébioctets de l'espace d'adressage. L'EEPROM du CMS est mappée dedans, mais n'en utilise que la moitié. Le reste est utilisé comme cache, pour mémoriser le code VLIW traduit par le CMS. Les deux processeurs intégraient aussi deux mémoires SRAM, utilisées par le CMS, appelées la ''local program memory'' (LPM) et la ''local data memory'' (LDM). La première contient du code qui gére les interruptions, la mémoire virtuelle, les exceptions matérielles, les problèmes d'alignement mémoire, et quelques fonctions très fréquemment utilisées par le CMS. La seconde est de la mémoire RAM utilisée par ces fonctions de la ''local program memory''. Les mémoires caches L1 et L2 sont séparées de ces deux mémoires. Le processeur Crusoe et Efficieon étaient des processeurs VLIW très simples. Ils n'avaient même pas de MMU, ni de fonctionnalités importantes sur les CPU x86. Pour Crusoe, les instructions VLIW étaient encodées sur 64 ou 128 bits, et regroupaient entre 2 à 4 opérations. Les instructions LOAD/STORE ne supportaient pas d'adressage indicé, juste de l'adressage indirect à registre. Crusoe avait 5 unités de calcul : deux ALU entières, une unité LOAD/STORE, une unité de branchement, une FPU. Efficieon doublait le nombre d'ALU entière et d'unité LOAD/STORE, les instructions VLIW passaient à 256 bits. Le processeur Crusoe contient 160 registres, dont 64 registres généraux, 32 registres flottants. Les registres généraux font 32 bits, ce qui est cohérent avec le fait que les CPU x86 de l'époque étaient des processeurs 32 bits. Quant aux registres flottants, ils faisaient 80 bits, ce qui colle avec la taille des registres flottants de la FPU x87 utilisée à l'époque. Sur les 64 registres généraux, seuls 48 étaient réellement utilisable pour mémoriser des opérandes. Une partie des registres généraux étaient utilisés pour la gestion de la pile, d'autres pour mémoriser l'état du CPU x86 émulé, un registre était un registre zéro non accesible en écriture. La gestion des exceptions est optimisée avec un système d'exceptions différées, le même que celui décrit plus haut. Le processeur utilise un système similaire, pour effectuer des lectures anticipées, qui a été expliqué dans le chapitre sur les processeurs VLIW/EPIC. Sauf qu'il s'agit là d'une optimisation pour gagner en performances en général, pas une aide utile pour gérer des exceptions précises ou toute autre subtilité de la traduction binaire. Pour gérer les exceptions différées et les lectures anticipées, le processeur mémorise l'état du processeur avant de démarrer un bloc de code. Pour cela, le processeur contient des ''shadow registers'', 48 registres entiers et 16 registres flottants. Vous remarquerez qu'il y a autant de registres entiers que de ''shadow registers'' entiers. Les registres entiers/flottants sont mémorisés dans les ''shadow registers'' avant d'exécuter un bloc de code, afin que le processeur puisse revenir à l'état de base. Une autre fonctionnalité liée est le ''gated store buffer''. Le principe est d'éviter toute écriture en mémoire RAM, tant que le code VLIW n'a pas émis d'instruction ''commit''. L'idée est que les écritures sont conservées dans la ''store queue'' de l'unité LOAD/STORE. Pour rappel, la ''store queue'' met en attente les écritures soit tant que les écritures ne sont pas terminées, soit tant que la RAM est occupée. L'idée est que les écritures sont accumulées dans la ''store queue'' tant que l'instruction ''commit'' n'a pas eu lieu. Le ''store queue'' est "déversé dans la RAM" seulement quand l'instruction ''commit'' s'exécute. ===Les CPU NVIDIA Denver=== Nvidia a tenté quelque chose de similaire avec son projet Denver. NVIDIA a racheté l'entreprise Transmetta, ses brevets, et a tenté de refaire la même chose. Le projet initial était de traduire du code x86 vers un jeu d'instruction VLIW propriétaire, comme l'a fait Transmetta. Mais NVIDIA n'a pas réussi à acquérir la licence du jeu d'instruction x86 et s'est rabattu sur le jeu d'instruction ARM. Le processeur né de ce projet est le Tegra K1-64 CPU. Il s'agit d'un processeur composé d'un cœur VLIW, couplé à deux décodeurs d'instructions ARM. Les deux décodeurs ARM traduisent une paire d'instructions ARM consécutives en instructions VLIW, pour les exécuter sur le cœur VLIW. Le CPU peut fonctionner selon deux modes : ARM et VLIW. En somme, le processeur supporte deux jeux d'instruction : le jeu d'instruction ARM, et un jeu d'instruction VLIW. En mode ARM, les instructions ARM sont chargées depuis le cache, traduites en VLIW par les décodeurs ARM, puis exécutées par le cœur VLIW. En mode optimisé, le CPU exécute des instructions VLIW chargées depuis le cache d'instruction, les décodeurs ne sont pas utilisés. : Le CPU supporte en réalité deux jeux d'instruction ARM : ARM8 et 7. Mais ils correspondent au même mode pour le CPU. Un point important est que le processeur peut changer très rapidement de mode, en à peine quelques cycles d'horloges. On n'est clairement pas dans le cas des CPU x86, qui mettent des plombes pour passer du mode 32 à 64 bits et inversement. La commutation est tellement rapide qu'on peut considérer que le processeur supporte deux jeux d'instruction simultanément. Et le support simultané de deux jeux d'instruction facilite l'implémentation de la traduction binaire. Encore une fois, inutile de traduire du code qui ne sera exécutée qu'une seule fois, mieux vaut l'exécuter directement dans le mode ARM. Surtout que le processeur supporte un mode ARM qui permet d'exécuter du code ARM sans perte de performance. Par contre, le code fréquemment utilisé, notamment dans des boucles critiques, est traduit en VLIW. Au final, les performances sont optimisées, en limitant le travail du logiciel traducteur binaire. La détection du code fréquemment exécuté se fait différemment que pour les processeurs Transmetta et Daisy. L'unité de branchement ne détecte pas directement le code beaucoup exécuté, même si elle a un rôle à jouer. A la place, la détection est réalisée en logiciel, par un ''thread'' dédié, qui s'exécute sur un cœur séparé. Il détecte le code fréquemment utilisé en regardent les branchements exécutés récemment. Pour cela, le processeur mémorise l'historique des branchements pris récemment et copie cette historique dans une mémoire tampon dédiée, partagée entre tous les cœurs. Vu qu'elle est partagée, le ''thread'' de détection a accès à l'historique sans pertes de performances, seule la copie de l'historique dans ce tampon a un cout en performance, pas son partage. Le code VLIW obtenu après traduction binaire est mémorisé dans la mémoire RAM, dans une portion spécialement réservée pour. Elle fait 128 mébioctets, et est appelée le '''cache d'optimisation''' par NVIDIA. Le cache d'optimisation est protégé en écriture et seul le traducteur binaire peut écrire dedans. Au passage, le traducteur binaire est du code VLIW, ce qui fait qu'il est placé dans le cache d'optimisation. Le processeur détecte automatiquement quand une fonction pour laquelle le code compilé équivalent est disponible. Pour cela, l'unité de chargement contient une table de correspondance entre l'adresse de la fonction ARM, et l'adresse de son équivalent compilé. Quand une fonction est appelée, l'unité de chargement regarde l'adresse de destination du branchement. Si l'adresse est dans cette table, elle récupère l'adresse de la fonction compilée et branche vers celle-ci. Le processeur dispose d'un cache d'instruction de 128 kibioctets, ce qui est très important, mais nécessaire vu la taille des instructions VLIW. Le coeur VLIW dispose de ses propres décodeurs, d'un ''scoreboard'', de 7 unités de calcul et de registres. Pour les unités de calcul, il a deux FPU, deux ALU, une unité de branchement, et deux unités LOAD-STORE faisant aussi ALU. Un point important est que le CPU dispose de 64 registres entiers et de 64 registres flottants. C'est deux fois plus que supporte le jeu d'instruction ARM. En mode ARM, seule la moitié des registres est utilisée. Mais en mode optimisé, le traducteur binaire utilise bien les 64 registres, grâce à une sorte de renommage de registres logicielle. Pour simplifier la traduction binaire, le processeur supporte les techniques vues précédemment. Le processeur intègre un ''gated store buffer'' similaire à celui des processeurs Transmetta. Les techniques d'exceptions différées sont aussi supportées, comme sur les processeurs Transmetta. Il a aussi une unité de préchargement décrite par NVIDIA comme agressive, avec support du préchargement de type anticipé (''runahead''). ===L'émulation de la mémoire virtuelle=== Pour finir, il faut parler de la mémoire virtuelle avec la traduction binaire. Sur le processeur source, le processeur gère à la fois des adresses physiques et virtuelles et sa MMU fait la conversion entre les deux. Sur le processeur VLIW, la MMU est simulée par le programme de traduction binaire, partiellement ou totalement. L'implémentation exacte varie suivant que l'on parle du projet Daisy, des processeurs Transmetta ou de Denver. Sur les processeurs Transmetta, il n'y a pas de MMU ni de mémoire virtuelle. A la place, le programme de traduction binaire émule la mémoire virtuelle du processeur source. Le processeur VLIW ne gère que des adresses physiques, rien d'autre. L'espace d'adressage physique du processeur VLIW a la même taille que celui du processeur émulé, ici des processeurs x86. Il est vraisemblable que les adresses physiques utilisées par le processeur x86 sont les mêmes que celles du processeur VLIW. Sur le projet Daisy, le processeur VLIW gère la mémoire virtuelle via pagination, comme les processeurs émulés. Ce qui fait qu'il gère un espace d'adressage virtuel et un espace d'adressage physique. Pour éviter toute confusion, nous parlerons d'adresse physique/virtuelle VLIW pour les adresses physiques/virtuelles du processeur VLIW, d'adresse virtuelle/physique source pour celle du jeu d'instruction traduit, à savoir du Power PC ou du s390, éventuellement de l'x86. Il faut alors faire le lien entre adresses physiques source et adresses virtuelles VLIW. Pour cela, rien de plus simple : il y a correspondance parfaite. L'adresse physique source numéro N correspond à l'adresse virtuelle VLIW numéro N. Cependant, cela signifie que tout l'espace d'adressage virtuel VLIW serait utilisé par le code à traduire. En réalité, il faut ajouter de la place pour le code traduit et le programme de traduction binaire. La conséquence est que l'espace d'adressage virtuel VLIW est plus large que l'espace d'adressage source. L'espace d'adressage virtuel VLIW est découpé en trois sections : une section pour le code Power PC à traduire, une autre pour le code traduit en VLIW, et une dernière réservée au traducteur binaire. Le programme de traduction est placé dans une ROM mappée en mémoire dans la seconde section. Le reste de la seconde section est réservé à une mémoire RAM utilisée par le programme de traduction. [[File:Mémoire virtuelle sur le projet Daisy.png|centre|vignette|upright=1.5|Mémoire virtuelle sur le projet Daisy]] Il faut noter que chaque page physique source, une fois traduite, correspond à N pages virtuelles VLIW. La raison est que le code traduit prend plus de place que le code machine originel. En conséquence, on doit utiliser une page finale plus grande. Pour le reste, des pages contiguës en mémoire physique source sont elles aussi contiguës en mémoire virtuelle VLIW. Le calcul d'adresse est donc simple : l'adresse physique source est multipliée par N, puis on ajoute l'adresse de base à laquelle commence la section pour le code traduit. ==Les processeurs à jeu d'instruction reconfigurable== Nous venons de voir comment accélérer en matériel la traduction binaire. Mais il existe une alternative qui permet de se passer de traduction binaire, complétement. L'idée est d'avoir un processeur avec un '''jeu d'instruction reconfigurable'''. Le nom est assez parlent : on peut configurer le processeur de manière à supporter le jeu d'instruction de son choix ! Par exemple, on pouvait reconfigurer un CPU de manière à ce qu'il supporte le jeu d'instruction x86, ou qu'il exécute du ''bytecode'' Java, ou du code ARMv8, etc. De tels processeurs sont encore plus rare que ceux vu avant dans ce chapitre, mais ils ont existé ! Les processeurs de ce type avaient un ''microcode réinscriptible'', à savoir que c'était des processeurs microcodés pour lesquels on pouvait modifier le microcode. L'idée était qu'en changeant le microcode, on changeait le jeu d'instruction du processeur. Pour cela, le microcode était associé à une mémoire SRAM dans laquelle le microcode était copié au démarrage du CPU. Il y avait donc une micro-ROM et une micro-RAM. Les instructions étaient décodées en utilisant le microcode dans la micro-SRAM, la micro-ROM ne servait qu'au démarrage. En modifiant le contenu de la micro-SRAM, on pouvait altérer le microcode, voire le remplacer totalement. ===Le Burroughs B1700 : architecture externe=== Un exemple est celui du Burroughs B1700, et il est intéressant d'étudier en détail son architecture. Son architecture est décrite dans le livre "Interpreting Machines : Architecture and Programming of the Bl700/Bl800 Series", écrit par Elliott I. Organick et James A. Hinds. Une partie de ce qui vfa suivre est un très court résumé de ce livre. Il disposait de 4 registres généraux, nommés X, Y, T et L, tous de 24 bits. Les registres T et L étaient découpés en 6 sous-registres de 4 bits, nommés LA LB LC LD LE et LF pour le registre L, et TA TB TC TD TE et TF pour le registre T. Ils étaient adressables comme n'importe quel registre. Un point important est que le registre T avait des possibilités d’extraction spécifiques. Il était possible d'extrait N bits, placés n'importe où dans ce registre, et de les copier dans un autre registre. Nous verrons pourquoi cette possibilité est intéressante et ne sert que pour le registre T. Les registres X et Y étaient reliés à une ALU de 24 bits capable d'effectuer des opérations arithmétiques basiques. Fait étonnant, l'ALU faisait tous les calculs en même temps et fournissait ses résultats dans 7 registres : un registre SUM pour l'addition, un registre DIFFERENCE pour la soustraction, et un registre par opération bit à bit. Les registres pour les opérations bit à bit sont : CMPX et CMPY pour l'opération NOT sur chaque opérande, XANY pour le ET bit à bit, XEOY pour le XOR, XORY pour le OU logique. Les additions et soustractions pouvaient se faire sur des opérandes codés en binaire ou en BCD, suivant le code opération utilisé. A cela, il fallait ajouter deux registres de "résultat" MSKX et MSKY. Ils fournissaient un masque dépendant de la taille du résultat en bits. Cela permettait de gérer des données de 8 ou 16 bits, alors que l'ALU faisait 24 bits. Et cela n'était pas limité à 16 ou 8 bits, mais n'importe quelle taille inférieure ou égale à 24 bits était supportée, même des tailles non-conventionnelles comme des résultats codés sur 3 bits, 9 bits, 12 bits, etc. L'ALU fournissait 12 conditions, qui alimentait un registre d'état. Le registre d'état était segmenté en trois registres appelés XYST, XYCN et BICN. Le processeur contenait de nombreux autres registres de 4 bits, dont les sous-registres de T et L. En tout, il y a 27 registres de 4 bits adressables ! Et le processeur disposait d'une ALU 4 bits pour manipuler ces registres de 4 bits ! Elle est capable de faire des MOV entre registres de 4 bits, des opérations bit à bit, et peut aussi tester si un bit vaut 0 ou 1. La dernière possibilité est très utile pour implémenter les branchements. Pour communiquer avec les entrées-sorties, il disposait de deux registres CMND et DATA : CMND pour envoyer une commande à une entrée-sortie, DATA pour lire ou écrire une donnée. Pour la mémoire, il avait deux registres séparés : READ et WRITE. READ contient la donnée lue lors d'une lecture,; WRITE est pour une donnée à écrire lors d’une écriture. Il s'agit de registres d’interfaçage mémoire, qui ont été promus au rang de registres architecturaux. Les deux registres sont gérés par le microcode, qui est le langage principal de la machine. Les instructions machines configurées font sans, mais le microcode doit y avoir accès facilement. Et vu que le microcode est le langage principal de ce processeur, ces deux registres deviennent architecturaux. ===Une architecture bit-adressable=== Si avoir un microcode réinscriptible est suffisant, quelques choix de ''design'' facilitent l'émulation de jeux d'instructions variés. Par exemple, le CPU Burroughs B1700 avait ajouté une fonctionnalité très importante : il était bit-adressable. En clair, il pouvait adresser la mémoire bit par bit, si nécessaire. L'avantage est que cela permettait d'émuler des architectures dont la taille des registres/''byte''/mots était très différente. Un autre avantage est que cela facilitait la gestion des instructions de taille variable. Détaillons rapidement le premier avantage. Le processeur était un CPU 24 bits, mais il pouvait émuler des processeurs 16 bits, 8 bits, 4 bits, 1 bit, 9 bits, ou toute autre valeur. Pour cela, le processeur lisait des mots de 24 bits et utilisant un ''barrel shifter'' pour sélectionner les bits adéquats. Par exemple, pour un accès de 16 bits, la donnée lue en mémoire était masquée de manière à ne garder que les 16 bits de poids faible. Le ''barrel shifter''/circuit de masquage était placé directement avant le bus mémoire, sur le trajet des données/instructions. Le ''barrel shifter'' était commandé par un registre ''Field Unit'', qui précisait quelle était la taille des données à charger. ===La gestion de la taille des instructions=== Les instructions à décoder n'ont pas de longueur fixe, à savoir qu'elles peuvent faire 16 bit avec tel jeu d'instruction, 8 bits avec un autre, 32 bits sur un autre, etc. Elles peuvent aussi être de longueur fixe ou variable ! Le B1700 intégrait donc un support des instructions de longueur variable, qui était "détourné" pour gérer des jeux d'instruction différents. Notons que vu que l'architecture est bit-adressable, les contraintes d'alignement des instructions disparaissent, ce qui a des avantages qu'on va détailler dans ce qui suit ! L'implémentation des instructions de taille variable ajoutait un registre avant le registre d'instruction. Le registre d'instruction proprement dit était un registre M de 24 bits. Il était précédé par un registre T, dans lequel l'instruction était assemblée, en combinant plusieurs morceaux. Si l'instruction est codée est codée sur moins de 24 bits, le registre T se contente de masquer les bits en trop. Si l'instruction chargée dépasser 24 bits, le chargement se faisait en plusieurs passes, avec plusieurs lectures en mémoire. Dans ce qui suit, on suppose que l'instruction est composée de '''champs''' : l'opcode, les opérandes, les adresses absolues, les constantes immédiates, les numéros de registre, etc. L'idée est que l'instruction est chargée dans le registre T, puis découpée en champs, qui sont copiés dans les registres du processeur ou envoyés au microcode. Je rappelle que le registre T est relié à un ''barrel shifter'', différent au précédent, qui permettait de masquer, décaler et extraire une donnée de ce registre. Et il y a la possibilité d'extraire une suite de bit de ce registre T, pour la copier dans un autre registre. Par exemple, prenons une instruction composée d'un opcode, d'une constante immédiate et d'une adresse immédiate. L'opcode est lu et interprété par le microcode. Le microcode extrait la constante immédiate du registre T et la copie dans le registre X. Ensuite, l'adresse est extraite et envoyée au registre FA, une lecture est démarrée, et l'opérande est copiée dans le registre Y. Enfin, l'instruction est exécutée par le microcode. Le tout prend plusieurs micro-opérations : une première pour lire l'opcode, une seconde pour lire la première opérande, une troisième pour lire la seconde opérande, una quatrième pour tout exécuter. Notons que suivant la taille des champs, on peut en charger plusieurs à la fois. Par exemple, imaginons qu'on ait chargé 24 bits, contenant un opcode de 8 bits et une constante immédiate de 16 bits : pas besoin de faire deux accès mémoire, le registre T est juste découpé en un opcode et une constante. Le registre T est fait pour ce genre d'extraction. ===Le chargement d'un champ=== L'architecture est bit-adressable car elle gére des champs qui ne font pas forcément 8 ou 16 bits. Par exemple, certains jeux d'instructions ont des opcodes codés sur 6 ou 7 bits, voire 9-10 bits. Par exemple, prenons un opcode de 7 bits, suivi par une adresse de 12 bits et une constante immédiate de 15 bits : aucun de ces champs ne sera aligné sur 8/16/24 bits. Utiliser une architecture bit-adressable règle le problème d'alignement. Et en général, les contraintes d'alignement des instructions disparaissent ! Quand on charge une instruction, peut importe qu'elle soit ou non alignée sur 24 bits, elle sera placée dans les bits de poids faible, il suffira de masquer les bits de poids fort inutiles. Par exemple, si on charge une instruction de 16 bits, les 16 bits seront copiés dans les 16 bits de poids faible du registre T, il suffira de masquer l'octet de poids fort. Pas besoin de faire de décalages pour gérer des instructions mal alignées... Pour charger une instruction, il faut connaitre plusieurs informations : l'adresse de son premier bit, sa taille. Pour cela, le B1700 contient deux registres, nommés ''Field Adress'' (FA) et ''Field Length'' (FL), qui indiquent l'adresse du premier bit et la taille de l'instruction à charger. Le premier fait 24 bits, ce qui permet de gérer des adresses de 24 bits. N'oubliez pas que le processeur est bit-adressable, ce qui permet d'adresser 16 Mébi-bits, soit 2 mébioctets. Le registre pour la taille fait lui 16 bits, ce qui permet de gérer des instructions de 65536 bits, soit 8192 octets ! Leur nom commence par ''field'', car le processeur peut gérer des instructions de taille variable et/ou plus longues que 24 bits. Dans ce cas, le chargement de l'instruction se fait champ par champ. Pour donner un exemple, prenons une instruction composée de 20 champs de 12 bits chacun. Les champs sont chargés un par un dans le registre T. Le registre FA est alors initialisé avec l'adresse de l'instruction, il est incrémenté de 12 à chaque cycle. Le registre FL est initialisé avec la taille de l'instruction, soit 20 * 12 = 240 bits, et il est décrémenté de 16 à chaque cycle. Précisons cependant que la taille d'un champ peut être changée d'un cycle à l'autre. Par exemple, le processeur peut charger un opcode de 8 bits, l'interpréter, détecter que l'instruction demande ensuite de charger une adresse de 16 bits. Dans ce cas, le registre FL est immédiatement altéré pour charger 16 bits, au lieu des 8 bits de l'opcode immédiatement précédent. ===Le microcode réinscriptible=== Le microcode utilise des micro-opérations de 16 bits de long, ce qui est très peu et bien en-dessous des micro-opérations des autres processeurs. Les micro-opérations sortant du microcode sont envoyées au registre M, un registre de micro-opération de 16 bits. les micro-opérations écrites dans ce registre sont ensuite décodées par le hardware et exécutées. Fait intéressant, il était possible de faire un OU logique entre la micro-opération sortante et un registre du processeur, le résultat étant mémorisé dans le registre M. Cela permettait de faire du code automodifiant dans le microcode ! Utiliser du code automodifiant avec un microcode réinscriptible est parfaitement possible, et c'est utile pour émuler des modes d'adressage complexe dans le microcode. Cela permettait par exemple de remplacer une adresse dans une micro-opération. L'adresse est alors surimposée avec un OU dans le champ d'adresse, alors que le reste de l'instruction n'est pas modifié. Mais modifier directement le microcode est rarement une bonne idée. Aussi, pour éviter cela, l'idée est de modifier les instructions avec un OU logique avec un autre registre. Les micro-opérations du microcode ne sont donc pas modifiées dans le microcode, mais en sortie du microcode. La RAM mémorisant le microcode était de petite taille, et il arrivait qu'elle soit trop petit pour mémoriser le microcode complet. Dans ce cas, le programmeur devait gérer la situation en découpant le microcode en segments, certains étant placé dans le processeur, les autres étant en mémoire RAM. Le processeur peut lire le microcode placé en mémoire RAM et même l'exécuter, mais avec une pénalité en termes de performances. Quelques registres intégrés au processeur permettent ce genre de magie. Le microcode pouvait servir pour émuler un autre jeu d'instruction, mais il était aussi possible d'implémenter un véritable interpréteur, grâce aux techniques du paragraphe précédent. Le processeur était même conçu pour ! L'interpréteur était placé dans un "segment" en mémoire RAM, dont le processeur connaissait la position. Il incorporait pour cela deux registres, un pour l'adresse de base de l'interpréteur, un autre pour son adresse de fin. Il incorporait une protection mémoire limitée sur ce segment, à savoir que seul l'interpréteur pouvait lire ou écrire dans ce segment. ==Les jeux d'instructions dédiés à un langage de programmation== De rares processeurs sont conçus pour un langage de programmation en particulier. On appelle ces processeurs, conçus pour des besoins particuliers, des '''processeurs dédiés'''. Par exemple, les fameux ''Burrough E-mode'' B5000/B6000/B7000 étaient spécialement conçus pour exécuter de l'ALGOL-60. Leurs cousins B2000/B3000/B4000 étaient eux conçus pour le COBOL. Des langages fonctionnels ont aussi eu droit à leurs processeurs dédiés. Le prolog en est un bel exemple, avec les superordinateurs de 5ème génération qui lui étaient dédié. On peut aussi citer les machines LISP, dédiés au langage LISP, qui datent des années 1970. Elles étaient capables d’exécuter certaines fonctions de base du langage directement dans leurs circuits : elles possédaient notamment un ''garbage collector'' câblé dans ses circuits ainsi que des instructions machines supportant un typage déterminé à l’exécution. Les processeurs dédiés ont eu leur heure de gloire au début de l'informatique, à une époque où les langages de haut niveau venaient d'être inventés. À cette époque, les compilateurs n'étaient pas performants et ne savaient pas bien optimiser le code machine. Il était alors rationnel, pour l'époque, de rapprocher le code machine cible et le langage de programmation de haut niveau. De nombreuses architectures dédiés ont ainsi été inventées, avant que les concepteurs se rendent compte des défauts de cette approche. Les défauts en question ne sont pas nombreux, mais assez simples à comprendre. Premièrement, elles sont très rapides pour un langage de programmation en particulier, mais sont assez mauvaises pour les autres, d'où un problème de "compatibilité". Ajoutons à cela que les langages de programmation peuvent évoluer, devenir de moins en moins populaires/utilisés, ce qui rend la création d'architectures généralistes plus pertinente. Enfin, les architectures dédiées sont évidemment des processeurs CISC, pour implémenter les nombreuses fonctionnalités des langages évolués. Et les défauts des CISC sont assez rédhibitoires à l'heure actuelle. À l'heure actuelle, les algorithmes des compilateurs se sont améliorés et savent nettement mieux utiliser le matériel. Ils produisent du code machine efficace, ce qui rend les architecture dédiées bien moins intéressantes. Si on ajoute les défauts de ces architectures dédiées, par étonnant que les architectures dédiées aient presque disparues. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'accélération matérielle de la virtualisation | prevText=L'accélération matérielle de la virtualisation | next=Le matériel réseau | nextText=Le matériel réseau }} </noinclude> mkzqkg5wiy6mncbgosi4v9a8u4g1379 764024 764023 2026-04-19T18:17:58Z Mewtow 31375 /* Le microcode réinscriptible */ 764024 wikitext text/x-wiki De nos jours, la majorité des programmeurs programment dans des langages de haut niveau. Il est très rare pour eux d'avoir à utiliser de l'assembleur, et encore moins un langage machine. Les programmes écrits dans un langage de haut niveau sont traduits en langage machine par un logiciel appelé le compilateur. [[File:Compilation.PNG|centre|vignette|upright=2.5|Compilateur : principe.]] Il se trouve que quelques rares processeurs sont conçus pour faciliter le travail du compilateur. Et ce sont ces processeurs que nous allons voir dans ce qui suit. Ils représentent une catégorie de jeux d'instruction à part, qui n'a pas vraiment de nom. ==La compilation et l’interprétation== Un compilateur traduit donc du code source, écrit dans un langage de haut niveau, vers du langage machine. La traduction est rarement directe. En général, le compilateur traduit le code source en assembleur, qui est lui-même traduit en langage machine. L'assembleur est une représentation textuelle du langage machine alors que le code machine est du binaire exécutable par le processeur. La traduction se fait donc en deux étapes, la '''compilation''' proprement dite et l''''assemblage''', réalisées respectivement par un compilateur et un assembleur. Il faut parfois rajouter une troisième phase d'édition des liens, que nous passons volontairement sous silence. {| |[[File:Code C.png|vignette|200px|Le code source, ici en C.]] |[[File:Assem.png|vignette|250px|Le code assembleur.]] |[[File:Binary file - hello world (C programming).png|vignette|150px|Le code machine.]] |} ===L'interprétation et la compilation à la volée=== Une alternative à la compilation est l''''interprétation''', qui transforme le code source en code machine à la volée. Avec l'interprétation, le code source est passé à un logiciel appelé l''''interpréteur''', qui exécute le code source ligne de code par ligne de code. Une ligne de code est traduite en code machine, qui est exécutée, puis l'interpréteur passe à la ligne suivante. [[File:Kaantotulkkaus.png|centre|vignette|upright=3|Compilation versus interprétation.]] Un défaut de l'interprétation est son cout en performance, qui est assez important. Aussi, l'interprétation stricte a évolué vers un hybride entre interprétation et compilation. L'idée est qu'une partie peu importante du code source est interprétée, alors que le code important est compilé juste avant d'être exécuté. Pour le dire autrement, le code source est partiellement compilé à la volée, juste avant son exécution. Aussi, on parle de '''compilation à la volée'''. Le terme en anglais est ''Just In Time compilation'', abrévié en JIT. En soi, le JIT est associé à de l'interprétation, les JIT sont en réalité des hybrides interpréteurs-compilateurs. Un point important est que compiler du code à la volée est assez lourd, cela a un cout en performance. Par contre, le code compilé s'exécute plus vite que du code interprété. Si on exécute le code compilé une seule fois, le cout de la compilation l'emporte sur le gain à l'exécution. Mais le code compilé est réutilisé autant de fois que nécessaire. Le gain à l'exécution est donc multiplié par le nombre d'exécutions, alors que le cout de la compilation est répartit sur plusieurs exécutions, il est amorti. Les portions du code sont choisies de manière à avoir un gain en performance maximal. L'idée générale est que le code exécuté très souvent est compilé, alors que du code exécuté pas souvent est interprété. Typiquement, les boucles les plus souvent exécutées sont compilées, le code exécuté une seule fois est interprété. ===Les langages intermédiaires=== Les compilateurs modernes passent par un '''langage intermédiaire''' pour faire la transformation en code machine. Le compilateur traduit le langage de haut niveau en langage intermédiaire, puis traduit le langage intermédiaire en code machine cible. Le langage intermédiaire est parfois appelé le '''''bytecode''''', et ce terme recouvre aussi le code écrit avec du ''bytecode''. L'interprétation aussi peut passer par l'intermédiaire d'un un ''bytecode'', ce n'est pas limité aux compilateurs proprement dit. {| |[[File:Intermediate representation scheme.png|centre|vignette|upright=1.5|Représentation intermédiaire d'un compilateur.]] |[[File:Code interpreter scheme.png|centre|vignette|upright=1.5|Fonctionnement d'un interpréteur.]] |} Faire ainsi a de nombreux avantages, le principal étant d'avoir un compilateur capable de traduire un langage de haut niveau vers plusieurs jeux d'instructions différents. Cela permet d'avoir, par exemple, un compilateur qui traduit du C soit en code machine x86, soit en code machine pour un CPU ARM, soit pour un CPU POWERPC, etc. [[File:Compiler design.svg|centre|vignette|upright=3|Compiler design]] Pour l'interprétation, l'usage d'un ''bytecode'' a d'autres avantages. Pour l'exploiter, il faut distribuer non pas le code source, mais le ''bytecode''. En clair, l'interpréteur ne prend pas en entrée le code source, mais du ''bytecode'' déjà compilé. La traduction est alors beaucoup plus simple, car le code source a déjà été partiellement compilé, par la traduction en ''bytecode''. Interpréter du code source est en effet assez compliqué : il faut effectuer des étapes d'analyse lexicale, sémantique, et bien d'autres. Avec du ''bytecode'', ces étapes ont été réalisées lors de la compilation du ''bytecode'', l'interpréteur a alors peut de choses à faire. L'interprétation du ''bytecode'' se fait instruction par instruction, au niveau du ''bytecode''. Une instruction du ''bytecode'' est traduite en une instruction machine équivalente, qui est exécutée. Si l'instruction est un peu complexe, il exécute une fonction/procédure qui fait la même chose. Pour résumer, le ''bytecode'' est ensuite traduit à la volée et exécuté instruction par instruction par un logiciel appelé l'interpréteur. [[File:Control table.png|centre|vignette|upright=2|Control table]] Le langage intermédiaire peut être vu comme l'assembleur d'un processeur, n'existe pas forcément dans la réalité, mais dont le jeu d'instruction est décrit en détail. Le processeur en question est appelé une '''machine abstraite''', ou encore une ''machine virtuelle''. Nous utiliserons le terme de machine abstraite dans ce qui suit. La machine abstraite n'est pas la même suivant que l'on cible la compilation ou l'interprétation/JIT. Pour l'interprétation/JIT, la machine abstraite est souvent une machine à pile, car cela simplifie grandement la traduction en code machine final. L'interprétation demande que la traduction du ''bytecode'' en code machine soit la plus simple possible. Et cela demande de prendre en compte pas mal de détails. Par exemple, les différents jeux d'instruction existants n'ont pas le même nombre de registres, ce qui pose problème lors de la traduction du ''bytecode''. Lors de la transformation en code machine, un algorithme d'allocation de registres se débrouille pour traduire le code intermédiaire en code qui utilise un nombre limité de registres. Pour améliorer cette allocation de registres, il y a deux solutions niveau ''bytecode'' : soit utiliser un nombre illimité de registres, soit utiliser une machine à pile. Pour les machines à pile, il existe un algorithme simple et rapide pour traduire un code écrit pour une machine à pile en un code écrit pour un processeur avec des registres, qui se débrouille pas trop mal pour allouer efficacement les registres. C'est un avantage assez important pour les langages interprétés. ==Les processeurs qui exécutent du bytecode== Le ''bytecode'' est un code machine, ce qui signifie qu'il peut en théorie s'exécuter sur un processeur qui implémente le jeu d’instruction associé. Si le jeu d'instruction d'un ''bytecode'' est souvent une description censée être fictive, elle n'en reste pas moins un jeu d'instruction et des caractéristiques précises, qu'on peut l'implémenter en matériel ! Néanmoins, tous les ''bytecode'' ne sont pas égaux de ce point de vue. Certains sont faciles à implémenter en matériel, d'autres non. Les ''bytecodes'' implémentés en matériel sont ceux dont les machines abstraites sont des machines à pile. Un premier exemple est celui des processeurs Pascal MicroEngine, qui exécutaient directement le bytecode du langage Pascal, le fameux UCSD P-code. Un second exemple est la '''machine SECD''', qui sert de langage intermédiaire pour certains compilateurs de langages fonctionnels. Elle a été implémentée en matériel par plusieurs équipes, notamment par les chercheurs de l'université de Calgary en 1989. Dans le même genre, quelques processeurs simples étaient capables d’exécuter directement le ''bytecode'' du langage FORTH. Le FORTH, un des premiers langages à pile de haut niveau, possède de nombreuses implémentations hardware et est un des rares langages de haut niveau dont le ''bytecode'' a été utilisé comme langage machine sur certains processeurs. Par exemple, on peut citer le processeur FC16, capable d’exécuter nativement du ''bytecode'' FORTH. ===Les processeurs Java=== Le cas le plus impressionnant est celui de la machine virtuelle Java, qui est un design de processeur comme un autre. En temps normal, le ''bytecode'' Java est compilé ou interprété, mais certains processeurs exécutaient du ''bytecode'' Java directement, sans interprétation ni compilation. Ils sont appelés des '''processeurs Java'''. On peut citer les processeurs ARM disposant de l'extension Jazelle, du JEMCore et du aJ-100 de aJile Systems, le picoJava II de Sun, et quelques autres. Il y a aussi eu quelques projets de recherche ou processeurs open source, comme Komodo, jamuth, le ''Java Optimized Processor'' et quelques autres projets du même genre. L'implémentation de la machine virtuelle Java n'était cependant pas complète. Quelques instructions complexes n'étaient pas gérées par le processeur et devaient être émulées en logiciel. Mais la grosse majorité des instructions l'était. Les instructions non-supportées étaient émulées avec la méthode ''trap and emulate'', à savoir qu'un opcode inconnu déclenchait une exception matérielle ''opcode invalide'', dont la routine pouvait émuler les instructions qui doivent l'être. Le processeur aJ-100 de aJile est capable d'exécuter la quasi-totalité des instructions de la JVM Java, seules deux instructions faisant exception. Les '''accélérateurs Java''' sont une solution intermédiaire, qui permet de traduire à la volée du ''bytecode'' Java en instructions RISC, exécutées sur un autre processeur. Il s'agit formellement de traduction binaire, mais je préfère en parler ici. L'idée est de combiner un processeur RISC avec un coprocesseur Java. Le coprocesseur lit le ''bytecode'' Java et le traduit en instructions RISC, ces dernières étant exécutées sur le processeur RISC. Le JA108 de Nozomi était un coprocesseur de ce genre, au même titre que le coprocesseur JSTAR de JEDI. L'extension Jazelle de certains CPU ARM fonctionnait vraisemblablement sur un principe similaire, sauf que le coprocesseur était intégré dans le processeur ARM, entre le cache d'instruction et le décodeur d'instructions ARM. L'intérêt des processeurs Java n'est pas qu'une question de performance. Certes, ils permettent d’exécuter plus vite les programmes compilés en ''bytecode'', comme des programmes Java pour la JVM Java. Mais l'intérêt est aussi de faire des économies de mémoire RAM et ROM. Je rappelle que la machine virtuelle Java est une machine à pile, ce qui fait que sa densité de code est excellente. Les programmes en ''bytecode'' Java sont donc très petits, et prennent peu de place en mémoire ROM. De plus, utiliser un processeur Java permet de se passer d'interpréteur ou de JIT Java, ce qui économise encore plus de mémoire. ===Transformer un processeur RISC en machine à pile=== La plupart des processeurs Java utilisent en interne une architecture à registre généraux, qui émule une machine à registre en microcode. Quelques registres sont réservés pour l'état de la machine virtuelle, avec au minimum un registre réservé pour le pointeur de pile, un autre pour un pointeur vers un ''pool'' de constantes, et quelques autres. D'autres registres servent à mémoriser le haut de la pile, à savoir le sommet de la pile et quelques opérandes situées juste en-dessous. Par exemple, le processeur picoJava II gardait les 64 opérandes au sommet de la pile dans 64 registres généraux, séparés des registres pour les pointeurs et l'état de la JVM. Une optimisation possible, envisagée sur les processeurs picoJava, était une sorte de macro-fusion sous stéroïde. L'idée était de fusionner certaines séries d'instructions Java en une seule instruction machine. Typiquement, pour faire une opération arithmétique, une machine à pile empile deux opérandes et exécute une opération, ce qui prend trois instructions machine. La spécification picoJava 1 a proposé d'envoyer directement la seconde opérande en entrée de l'ALU, ce qui fusionnait l'empilement de la seconde opérande et l'opération arithmétique. Le processeur picoJava II va plus loin et fusionne les trois instructions en une seule instruction RISC, agissant sur des registres. Mais la majorité des processeurs Java ne faisaient pas cela, car cela complexifie grandement le décodeur. Les processeurs ''bigfoot'' de DCT utilisaient un système différent pour émuler une pile avec des registres. Leur banc de registre de 64 bits était coupé en deux : les 16 premiers registres étaient utilisés pour la pile, les 48 suivants étaient utilisés pour autre chose. L'idée est que le registre 0 mémorise un emplacement vide, le registre 1 contient le sommet de la pile, le registre 2 l'opérande sous le sommet de la pile, et ainsi de suite. Les 15 premières opérandes de la pile sont donc mémorisées dans les registres. Les instructions écrivent leur résultat dans le registre adéquat, puis tous les registres sont décalés pour que le résultat passe dans le registre R1. Par exemple, les instructions PUSH chargent une opérande dans le registre 0, puis tous les registres sont décalés de un rang, pour que l'opérande chargée soit dans le registre 1. Le registre 0 devient alors le registre numéro 1, le registre 1 devient le 2, et ainsi de suite. Par contre, les opérations arithmétiques font autrement. Une instruction d'addition, par exemple, lit les opérandes dans le registre 1 et 2, puis place son résultat dans le registre 2 (les deux opérandes sont dépilées). Puis, les registres sont décalés d'un rang pour que ce registre 2 devienne le 1. Les registres ne sont en réalité pas décalés, il n'y a pas de transferts entre registres. A la place, les 16 premiers registres utilisaient une forme limitée de renommage de registres, similaire au banc de registre tournant utilisé sur les processeurs EPIC comme l'Itanium. Les numéros de registres sont renommés, décalés, à chaque fois qu'on empile ou dépile une opérande. Pour faire le renommage de registres, processeur contient un ''register stack counter'' de 4 bits, qui est incrémenté à chaque fois qu'on empile une donnée, et est décrémenté quand on dépile une donnée. Quand on veut accéder à un opérande, le numéro de registre architectural associé est traduit en numéro de registre physique, en additionnant le ''register stack counter'' (modulo 16, pour rester sur 4 bits). ==L'accélération de l'interprétation/JIT : le ''Thumb-EE'' d'ARM== Quelques processeurs ont ajouté des instructions pour faciliter le travail des interpréteurs/JIT. Par exemple, en 2005, ARM a ajouté le mode ''Thumb Execution Environment'', qui faisait cela. Il reprenait le jeu d'instruction compact ''thumb'' et ajoutait quelques instructions et modifiait le comportement d'instructions existantes. Le jeu d'instruction ''Thumb-EE'' a été ajouté sur les CPU ARM en 2005, mais a été déprécié en 2011, par manque d'utilité. Mais la tentative mérite qu'on s'attarde dessus. Les CPU de gamme M, déstinés à l'embarqué, n'ont jamais supporté ''Thumb-EE'', vu que de tels processeurs ne sont pas conçus pour exécuter du code interprété/JIT. Le mode ''Thumb-EE'' est un mode d'exécution, séparé de l'ARM normal. Ainsi, une instruction ''Thumb-EE'' et une instruction ARM peuvent avoir le même encodage en binaire, mais se comporter différemment. Le processeur est à tout moment soit en mode ''Thumb-EE'', soit en mode ARM normal, ce qui précise comment ces instructions doivent se comporter. Des instructions ont été ajoutées par ''Thumb-EE'', pour qu'il fasse son travail. L'entrée et la sortie du mode ''Thumb-EE'' se fait avec deux instructions : ENTERX et LEAVEX. Une fois en mode ''Thumb-EE'', le décodage des instructions ''thumb'' se fait avec les règles du ''Thumb-EE'', et non celles du ''thumb'' normal. Une première différence entre ''thumb'' normal et ''Thumb-EE'' est l'ajout de ce qui s'appelle un ''null check'' pour les instructions mémoire. Le ''null check'' est utilisé pour les instructions en mode d'adressage "base + indice" ou "Base + décalage", ou tout autre mode d'adressage avec un registre de base utilisés dans des calculs d'adresse. L'idée est de vérifier si l'adresse de base vaut zéro ou non. Si c'est le cas, le processeur lève une exception matérielle, qui est traitée par l'interprétation ou le compilateur JIT. Une telle situation est en effet signe d'une erreur d'adressage mémoire, qui doit être traitée par l'interpréteur. Typiquement, c'est signe qu'un pointeur n'a pas bien été initialisé, ce qui peut arriver avec du code interprété ou JIT. Les interpréteurs/JIT traitent généralement la situation en ajoutant un test avant toute instruction mémoire, pour vérifier si le pointeur accédé vaut zéro ou non. Avec ''Thumb-EE'', pas besoin d'ajouter les tests en questions : ils sont réalisés automatiquement lors de chaque instruction mémoire. : Les registres pour la pile, comme le pointeur de pile et le pointeur de ''frame'', sont aussi concernés. L'instruction CHKA vérifie que les accès à un tableau ne débordent pas en dehors du tableau en question. Par exemple, pour un tableau de 1024 éléments, elle vérifie si l'indice est compris entre 0 et 1024. Pour cela, elle vérifie si l'indice de tableau est bien dans l'intervalle adéquat et lève une exception matérielle si ce n'est pas le cas. Son utilité se comprend quand on sait que tous les accès à un tableau sont vérifiés par l'interpréteur. L'interpréteur est censé ajouter des instructions pour vérifier les indices, à savoir deux ou trois branchements. Avec CHKA, le test se fait en une seule instruction. D'autres modifications mineures de l'encodage des instructions sont aussi présentes, ainsi que des modifications pour les instructions LOAD/STORE. Précisément, dans les modes "base + indice" et "base + décalage", le décalage et l'indice subissent maintenant des décalages. L'idée est de simplifier le calcul d'adresse. De quoi économiser quelques instructions lors des calculs d'adresse, ce qui facilite le travail de l'interpréteur/JIT. Et des instructions LOAD/STORE précises voient leur comportement modifié pour faciliter la gestion de la pile de Java ou l'exécution des méthodes locales. Pour faciliter le travail de l'interpréteur, deux instructions HBP et HBLP ont été ajoutées. Elles branchent vers une fonction qui gère les exceptions logicielles (une fonctionnalité présente dans de nombreux langages, comme Java). La première effectue un branchement vers la fonction en question, la seconde sauvegarde l'adresse de retour avant de faire ce branchement. Les deux peuvent brancher vers 256 fonctions pré-déterminées, sans avoir à présenter leur adresse. ==La traduction binaire accélérée par le matériel== Plus haut, nous avons surtout parlé des compilateurs et des interpréteurs. Cependant, nous devons aussi parler de la '''traduction binaire'''. Elle traduit un programme écrit dans un code machine vers un autre code machine. Par exemple, elle traduit un programme compilé pour un CPU x86 vers un code machine ARM. En général, le système d'exploitation est généralement compilé pour le jeu d'instruction natif, mais il exécute des applications prévues pour le x86. Les applications sont traduites par le système d'exploitation, avant d'exécuter le code traduit. : Nous parlerons dans la suite de code machine source et de code machine cible, pour parler respectivement du code à traduire et du code obtenu après traduction. Idem avec d'autres termes comme architecture cible/source, ou jeu d'instruction cible/source. La traduction binaire est surtout utilisée pour des questions d'émulation ou de compatibilité. Par exemple, lorsque les Macintosh sont passés de processeurs Power PC vers des processeurs x86, le système d'exploitation Mac OS utilisait la traduction binaire pour convertir les anciennes applications Power PC vers du code x86. Les utilisateurs n'y ont vu que du feu. Le système de traduction binaire était appelé Rosetta 1. Par la suite, lors de la transition de processeurs x86 vers des processeurs Apple, Rosetta 2 a vu le jour. La traduction binaire a surtout été utilisée pour traduire du code x86 vers un autre jeu d'instruction. Il faut dire que le x86 est le jeu d'instruction dominant. De nombreuses entreprises ont eu pour ambition de briser l'hégémonie du x86 sur PC, en remplaçant le x86 par un jeu d'instruction plus performant, tout en gardant une compatibilité maximale. La traduction binaire était la seule solution pratique. Elle était le plus souvent intégralement réalisée en logiciel, comme c'était le cas sur les architectures Itanium avec le ''IA-32 Execution Layer''. ===Les généralités sur la traduction binaire assistée en matériel=== L'architecture source est presque tout le temps une architecture CISC, assez ancienne, qu'on souhaite émuler. Le choix de l'architecture cible se porte souvent sur une architecture VLIW, fort différente de l'architecture source, et ce pour obtenir une économie de matériel conséquente. Pour s'exécuter rapidement, le code traduit doit exploiter le parallélisme d'instruction, à savoir exécuter plusieurs instructions en même temps dans des unités de calcul séparées. Il est possible d'utiliser un processeur superscalaire avec exécution dans le désordre pour cela, mais au prix d'un cout important en transistors. Alors qu'en utilisant un CPU VLIW, c'est le traducteur binaire qui fait tout le travail d'extraction du parallélisme d'instruction. Et après tout, quitte à avoir un traducteur binaire, autant lui refiler le boulot d'optimisation. Compiler du code à la volée est certes assez lourd, mais qu'il existe des algorithmes efficaces pour regrouper des instructions indépendantes dans une seule instruction VLIW. Au passage, le processeur VLIW a plus de registres que le processeur source. Cela permet de faire du renommage de registres directement en logiciel. Le traducteur binaire n'hésite pas à changer les noms de registres entre instructions source et instruction VLIW cible, afin de supprimer des dépendances de données. En conséquence, cela ouvre des opportunités de parallélisme, qui sont exploitées lors du regroupement des opérations en instructions VLIW. Vous pourriez penser que le choix d'un CPU VLIW pour émuler du CISC est tout sauf optimal. Vous devez penser que la traduction binaire est d'autant plus simple que l'architecture source et cibles sont semblables. Dans les faits, ce n'est pas tellement traduire les instructions qui pose problème, mais plus la gestion du registre d'état, des exceptions matérielles, de la mémoire virtuelle, la différence entre gros-boutisme et petit-boutisme, de même que des différences d'adressage pour les périphériques. Le premier problème est la gestion des conditions, notamment en présence d'un registre d'état. En pratique, la traduction binaire s'utilise pour traduire du code CISC vers du code RISC ou VLIW. Les architectures source ont donc un registre d'état, qui est mis à jour non seulement par des instructions de test, mais aussi des instructions arithmétiques. L'architecture cible n'a elle pas de registre d'état, mais des registres à prédicats. La traduction de l'un vers l'autre est alors quelque peu compliquée. Et elle est d'autant plus compliquée que le registre d'état est la source de dépendances d'instruction implicites, qui réduisent les performances. Par exemple, si une instruction arithmétique modifie le registre d'état, cela peut impacter l'exécution d'une instruction ultérieure, qui lit ce registre d'état. Pour éliminer ces fausses dépendances, le traducteur binaire doit renommer ce registre en logiciel et détecter les dépendances utiles. Et le compilateur doit gérer les cas où il y a beaucoup de distance entre les deux, voire les cas où l'instruction dépendante n'a pas encore été analysée par le compilateur. Un autre problème est lié à la gestion des exceptions matérielles, et précisément des exceptions précises. Pour rappel, une instruction VLIW regroupe plusieurs opérations, opérations qui correspondraient à une instruction machine sur un CPU pas VLIW. En conséquence, plusieurs instructions du langage machine "source" sont regroupées en une seule instruction VLIW. Et il faut tenir compte du cas où une instruction source lève une exception. Dans le code VLIW, cela signifie qu'une opération lève une exception, et il faut annuler partiellement l'instruction VLIW associée. Et par partiellement, on veut dire que seules les opérations suivantes dans l'ordre du programme source doivent être annulées, puis ré-exécutées. Et c'est un sacré casse-tête ! Les processeurs VLIW utilisent comme solution le mécanisme d'exceptions différées des processeurs EPIC, vu il y a quelques chapitres. Pour résumer, le code est exécuté par blocs d'instruction, délimités par des branchements ou tout autre limite/barrière pertinente dans le code. Un bloc de code est compilé en deux versions : une version rapide sans exceptions matérielles, une version lente qui gère les exceptions précises. La version rapide s'exécute sans exécuter les exceptions. Cependant, les exceptions matérielles sont enregistrées, pour être prises en compte à la toute fin du bloc de code. La version rapide est exécutée en premier et elle mémorise si une exception matérielle a eu lieu. Puis une instruction vérifie si une exception a eu lieu et décide quoi faire. Si aucune exception n'a eu lieu, elle passe à la suite du programme, les résultats du bloc de code sont définitivement acceptés. Mais si une exception a eu lieu, tout est annulé. Le processeur est remis dans l'état initial, puis le code est ré-exécuté instruction par instruction de manière à gérer l'exception correctement. ===La traduction par pré-décodage=== Une première solution serait de faire la traduction binaire dans le cache d'instruction. Lors d'un défaut de cache, le code chargé depuis la RAM est traduit en code machine cible. Et c'est ce code machine cible qui est mémorisé dans le cache d'instruction et exécuté par le processeur. La technique marche sur le papier et n'est qu'une amélioration des techniques de pré-décodage vues il y a quelques chapitres. Cependant, le code obtenu est une traduction assez basique, qui n'incorpore pas d'optimisations dignes de ce nom. De plus, elle gère mal le cas où la taille du code source et cible sont potentiellement très différentes. Concrètement, elle est surtout utile pour traduire du code d'un processeur RISC vers un autre processeur RISC, dont les tailles d'instructions sont similaires. Elle ne permet pas d'émuler plusieurs jeux d'instructions différents, le cout en matériel (un circuit de pré-décodage par jeu d'instruction) serait trop important. ===Le projet DAISY d'IBM=== De très rares processeurs étaient conçus pour accélérer cette traduction binaire, afin de garder de bonnes performances. Les premiers à avoir étudié l'idée étaient IBM, avec leur projet DAISY (''Dynamically Architected Instruction Set from Yorktown''). Le projet de base était de convertir à la volée du code compilé pour des CPU Power PC, vers du code VLIW. Les chercheurs d'IBM avaient développé un algorithme de traduction dynamique efficace, ainsi qu'un processeur VLIW disposant d'optimisations spécifiques à la traduction binaire dynamique, à la volée. De nombreuses idées de ce projet ont été reprises ou re-découvertes par la société Transmetta, avec ses processeurs Crusoe et Efficieon, puis par NVIDIA avec son projet Denver. Les ingénieurs de ce projet ont étudié la possibilité d'émuler plusieurs jeux d'instructions différents sur un même processeur, notamment le s390 d'IBM et le x86. En théorie, cela demande juste d'avoir plusieurs programmes de traduction binaire : un pour le x86, un autre pour le s390, éventuellement un autre pour le Power PC. Mais cela ne s'est pas concrétisé. Mais les ingénieurs ont étudié quelle pourrait être l'architecture VLIW idéale pour ça. Par exemple, il fallait des additions 3-opérandes pour simplifier les calculs d'adresse. Une difficulté était la gestion du registre d'état, dont les bits ne sont pas mis à jour de la même manière sur le x86 et le s390 ou le Power PC. La différence entre gros-boutisme et petit-boutisme était aussi un problème, de même que des différences d'adressage pour les périphériques. Pour que la traduction binaire soit efficace, le processeur VLIW intègre diverses optimisations, comme les exceptions différées et les branchements multi-voies. La principale est la suivante : seul le code des boucles ou fonctions exécutées fréquemment est traduit en code VLIW, le reste du code est interprété. En effet, traduire du code binaire prend plus de temps qu'une simple interprétation. Pour du code qui ne sera exécutée qu'une seule fois, il est plus rapide d'utiliser l'interprétation. Par contre, pour du code exécuté beaucoup de fois, le cout de la traduction binaire est amorti, dilué sur N exécutions, compensé par le gain en temps d'exécution de ce code traduit. Au final, cela permet de ne traduire que le code qui le mérite. La détection du code fréquemment exécuté est réalisée dans le cœur VLIW. L'unité de branchement mémorise les derniers branchements rencontrés et le nombre de fois qu'ils ont été exécutés. Si ils ont été exécutés un certain nombre de fois, l'unité de branchement lève une exception matérielle, qui invoque le traducteur binaire. La routine de cette exception est le traducteur binaire proprement dit. En clair, la traduction binaire est démarrée quand le processeur détecte qu'une fonction a été exécutée plus de N fois, via une exception matérielle dédiée. La mesure du nombre d'exécution d'un branchement se fait dans l'unité de calcul dédiée aux branchements, avec l'aide d'une sorte de ''branch adress buffer'' modifié. Pour rappel, le ''branch adress buffer'' mémorise le ''Program counter'' de chaque branchement récemment rencontré. Ici, chaque entrée du ''branch adress buffer'' est associée à un compteur incrémenté à chaque exécution du branchement. ===Les processeurs Crusoe et Efficieon de Transmetta=== Les processeurs Crusoe Et Efficeron sont deux processeurs VLIW produits par la société Transmetta, une société californienne rachetée par NVIDIA. Ils étaient conçus pour exécuter spécifiquement des programmes x86, système d'exploitation inclus. L'idée derrière ce projet était d'exécuter du code x86 sans que les applications, ni même le système d'exploitation et le BIOS soient au courant ! Les processeurs Transmetta étaient en réalité conçus pour exécuter un programme unique, le ''Code Morphing Software'' (CMS), qui traduisait le code x86 en code VLIW. Transmetta ne voulait pas que son processeur VLIW soit exploité directement, sans le CMS. Il n'avait pas rendu public de compilateur pour traduire du C vers du code VLIW, il n'avait pas donné la documentation du jeu d'instruction VLIW. Des efforts de rétro-ingénieurie, documentés sur le site realworldtech, ont cependant permis de comprendre comment étaient encodées les instructions du processeur Crusoe. Comme pour le projet Daisy, le ''Code Morphing Software'' utilisait à la fois interprétation et traduction binaire, selon les besoins. Les instructions sont interprétées lors de leurs premières exécutions, mais le CMS bascule sur de la traduction en code VLIW après un certain nombre d'exécution. Ainsi, les boucles souvent exécutées sont traduites en code VLIW, alors que le reste du code est interprété. Le choix du code à traduire est le fait du CMS, il utilise des heuristiques complexes pour, qui ne sont pas connues dans le détail. Le ''Code Morphing Software'' est mémorisé dans une EEPROM, ce qui en fait un ''firmware'' situé en dessous du BIOS. Le CMS démarre ensuite le BIOS, qui lui-même démarre le système d'exploitation, qui lui-même démarre les pilotes de périphériques et les programmes. Le CMS se réserve les 16 premiers mébioctets de l'espace d'adressage. L'EEPROM du CMS est mappée dedans, mais n'en utilise que la moitié. Le reste est utilisé comme cache, pour mémoriser le code VLIW traduit par le CMS. Les deux processeurs intégraient aussi deux mémoires SRAM, utilisées par le CMS, appelées la ''local program memory'' (LPM) et la ''local data memory'' (LDM). La première contient du code qui gére les interruptions, la mémoire virtuelle, les exceptions matérielles, les problèmes d'alignement mémoire, et quelques fonctions très fréquemment utilisées par le CMS. La seconde est de la mémoire RAM utilisée par ces fonctions de la ''local program memory''. Les mémoires caches L1 et L2 sont séparées de ces deux mémoires. Le processeur Crusoe et Efficieon étaient des processeurs VLIW très simples. Ils n'avaient même pas de MMU, ni de fonctionnalités importantes sur les CPU x86. Pour Crusoe, les instructions VLIW étaient encodées sur 64 ou 128 bits, et regroupaient entre 2 à 4 opérations. Les instructions LOAD/STORE ne supportaient pas d'adressage indicé, juste de l'adressage indirect à registre. Crusoe avait 5 unités de calcul : deux ALU entières, une unité LOAD/STORE, une unité de branchement, une FPU. Efficieon doublait le nombre d'ALU entière et d'unité LOAD/STORE, les instructions VLIW passaient à 256 bits. Le processeur Crusoe contient 160 registres, dont 64 registres généraux, 32 registres flottants. Les registres généraux font 32 bits, ce qui est cohérent avec le fait que les CPU x86 de l'époque étaient des processeurs 32 bits. Quant aux registres flottants, ils faisaient 80 bits, ce qui colle avec la taille des registres flottants de la FPU x87 utilisée à l'époque. Sur les 64 registres généraux, seuls 48 étaient réellement utilisable pour mémoriser des opérandes. Une partie des registres généraux étaient utilisés pour la gestion de la pile, d'autres pour mémoriser l'état du CPU x86 émulé, un registre était un registre zéro non accesible en écriture. La gestion des exceptions est optimisée avec un système d'exceptions différées, le même que celui décrit plus haut. Le processeur utilise un système similaire, pour effectuer des lectures anticipées, qui a été expliqué dans le chapitre sur les processeurs VLIW/EPIC. Sauf qu'il s'agit là d'une optimisation pour gagner en performances en général, pas une aide utile pour gérer des exceptions précises ou toute autre subtilité de la traduction binaire. Pour gérer les exceptions différées et les lectures anticipées, le processeur mémorise l'état du processeur avant de démarrer un bloc de code. Pour cela, le processeur contient des ''shadow registers'', 48 registres entiers et 16 registres flottants. Vous remarquerez qu'il y a autant de registres entiers que de ''shadow registers'' entiers. Les registres entiers/flottants sont mémorisés dans les ''shadow registers'' avant d'exécuter un bloc de code, afin que le processeur puisse revenir à l'état de base. Une autre fonctionnalité liée est le ''gated store buffer''. Le principe est d'éviter toute écriture en mémoire RAM, tant que le code VLIW n'a pas émis d'instruction ''commit''. L'idée est que les écritures sont conservées dans la ''store queue'' de l'unité LOAD/STORE. Pour rappel, la ''store queue'' met en attente les écritures soit tant que les écritures ne sont pas terminées, soit tant que la RAM est occupée. L'idée est que les écritures sont accumulées dans la ''store queue'' tant que l'instruction ''commit'' n'a pas eu lieu. Le ''store queue'' est "déversé dans la RAM" seulement quand l'instruction ''commit'' s'exécute. ===Les CPU NVIDIA Denver=== Nvidia a tenté quelque chose de similaire avec son projet Denver. NVIDIA a racheté l'entreprise Transmetta, ses brevets, et a tenté de refaire la même chose. Le projet initial était de traduire du code x86 vers un jeu d'instruction VLIW propriétaire, comme l'a fait Transmetta. Mais NVIDIA n'a pas réussi à acquérir la licence du jeu d'instruction x86 et s'est rabattu sur le jeu d'instruction ARM. Le processeur né de ce projet est le Tegra K1-64 CPU. Il s'agit d'un processeur composé d'un cœur VLIW, couplé à deux décodeurs d'instructions ARM. Les deux décodeurs ARM traduisent une paire d'instructions ARM consécutives en instructions VLIW, pour les exécuter sur le cœur VLIW. Le CPU peut fonctionner selon deux modes : ARM et VLIW. En somme, le processeur supporte deux jeux d'instruction : le jeu d'instruction ARM, et un jeu d'instruction VLIW. En mode ARM, les instructions ARM sont chargées depuis le cache, traduites en VLIW par les décodeurs ARM, puis exécutées par le cœur VLIW. En mode optimisé, le CPU exécute des instructions VLIW chargées depuis le cache d'instruction, les décodeurs ne sont pas utilisés. : Le CPU supporte en réalité deux jeux d'instruction ARM : ARM8 et 7. Mais ils correspondent au même mode pour le CPU. Un point important est que le processeur peut changer très rapidement de mode, en à peine quelques cycles d'horloges. On n'est clairement pas dans le cas des CPU x86, qui mettent des plombes pour passer du mode 32 à 64 bits et inversement. La commutation est tellement rapide qu'on peut considérer que le processeur supporte deux jeux d'instruction simultanément. Et le support simultané de deux jeux d'instruction facilite l'implémentation de la traduction binaire. Encore une fois, inutile de traduire du code qui ne sera exécutée qu'une seule fois, mieux vaut l'exécuter directement dans le mode ARM. Surtout que le processeur supporte un mode ARM qui permet d'exécuter du code ARM sans perte de performance. Par contre, le code fréquemment utilisé, notamment dans des boucles critiques, est traduit en VLIW. Au final, les performances sont optimisées, en limitant le travail du logiciel traducteur binaire. La détection du code fréquemment exécuté se fait différemment que pour les processeurs Transmetta et Daisy. L'unité de branchement ne détecte pas directement le code beaucoup exécuté, même si elle a un rôle à jouer. A la place, la détection est réalisée en logiciel, par un ''thread'' dédié, qui s'exécute sur un cœur séparé. Il détecte le code fréquemment utilisé en regardent les branchements exécutés récemment. Pour cela, le processeur mémorise l'historique des branchements pris récemment et copie cette historique dans une mémoire tampon dédiée, partagée entre tous les cœurs. Vu qu'elle est partagée, le ''thread'' de détection a accès à l'historique sans pertes de performances, seule la copie de l'historique dans ce tampon a un cout en performance, pas son partage. Le code VLIW obtenu après traduction binaire est mémorisé dans la mémoire RAM, dans une portion spécialement réservée pour. Elle fait 128 mébioctets, et est appelée le '''cache d'optimisation''' par NVIDIA. Le cache d'optimisation est protégé en écriture et seul le traducteur binaire peut écrire dedans. Au passage, le traducteur binaire est du code VLIW, ce qui fait qu'il est placé dans le cache d'optimisation. Le processeur détecte automatiquement quand une fonction pour laquelle le code compilé équivalent est disponible. Pour cela, l'unité de chargement contient une table de correspondance entre l'adresse de la fonction ARM, et l'adresse de son équivalent compilé. Quand une fonction est appelée, l'unité de chargement regarde l'adresse de destination du branchement. Si l'adresse est dans cette table, elle récupère l'adresse de la fonction compilée et branche vers celle-ci. Le processeur dispose d'un cache d'instruction de 128 kibioctets, ce qui est très important, mais nécessaire vu la taille des instructions VLIW. Le coeur VLIW dispose de ses propres décodeurs, d'un ''scoreboard'', de 7 unités de calcul et de registres. Pour les unités de calcul, il a deux FPU, deux ALU, une unité de branchement, et deux unités LOAD-STORE faisant aussi ALU. Un point important est que le CPU dispose de 64 registres entiers et de 64 registres flottants. C'est deux fois plus que supporte le jeu d'instruction ARM. En mode ARM, seule la moitié des registres est utilisée. Mais en mode optimisé, le traducteur binaire utilise bien les 64 registres, grâce à une sorte de renommage de registres logicielle. Pour simplifier la traduction binaire, le processeur supporte les techniques vues précédemment. Le processeur intègre un ''gated store buffer'' similaire à celui des processeurs Transmetta. Les techniques d'exceptions différées sont aussi supportées, comme sur les processeurs Transmetta. Il a aussi une unité de préchargement décrite par NVIDIA comme agressive, avec support du préchargement de type anticipé (''runahead''). ===L'émulation de la mémoire virtuelle=== Pour finir, il faut parler de la mémoire virtuelle avec la traduction binaire. Sur le processeur source, le processeur gère à la fois des adresses physiques et virtuelles et sa MMU fait la conversion entre les deux. Sur le processeur VLIW, la MMU est simulée par le programme de traduction binaire, partiellement ou totalement. L'implémentation exacte varie suivant que l'on parle du projet Daisy, des processeurs Transmetta ou de Denver. Sur les processeurs Transmetta, il n'y a pas de MMU ni de mémoire virtuelle. A la place, le programme de traduction binaire émule la mémoire virtuelle du processeur source. Le processeur VLIW ne gère que des adresses physiques, rien d'autre. L'espace d'adressage physique du processeur VLIW a la même taille que celui du processeur émulé, ici des processeurs x86. Il est vraisemblable que les adresses physiques utilisées par le processeur x86 sont les mêmes que celles du processeur VLIW. Sur le projet Daisy, le processeur VLIW gère la mémoire virtuelle via pagination, comme les processeurs émulés. Ce qui fait qu'il gère un espace d'adressage virtuel et un espace d'adressage physique. Pour éviter toute confusion, nous parlerons d'adresse physique/virtuelle VLIW pour les adresses physiques/virtuelles du processeur VLIW, d'adresse virtuelle/physique source pour celle du jeu d'instruction traduit, à savoir du Power PC ou du s390, éventuellement de l'x86. Il faut alors faire le lien entre adresses physiques source et adresses virtuelles VLIW. Pour cela, rien de plus simple : il y a correspondance parfaite. L'adresse physique source numéro N correspond à l'adresse virtuelle VLIW numéro N. Cependant, cela signifie que tout l'espace d'adressage virtuel VLIW serait utilisé par le code à traduire. En réalité, il faut ajouter de la place pour le code traduit et le programme de traduction binaire. La conséquence est que l'espace d'adressage virtuel VLIW est plus large que l'espace d'adressage source. L'espace d'adressage virtuel VLIW est découpé en trois sections : une section pour le code Power PC à traduire, une autre pour le code traduit en VLIW, et une dernière réservée au traducteur binaire. Le programme de traduction est placé dans une ROM mappée en mémoire dans la seconde section. Le reste de la seconde section est réservé à une mémoire RAM utilisée par le programme de traduction. [[File:Mémoire virtuelle sur le projet Daisy.png|centre|vignette|upright=1.5|Mémoire virtuelle sur le projet Daisy]] Il faut noter que chaque page physique source, une fois traduite, correspond à N pages virtuelles VLIW. La raison est que le code traduit prend plus de place que le code machine originel. En conséquence, on doit utiliser une page finale plus grande. Pour le reste, des pages contiguës en mémoire physique source sont elles aussi contiguës en mémoire virtuelle VLIW. Le calcul d'adresse est donc simple : l'adresse physique source est multipliée par N, puis on ajoute l'adresse de base à laquelle commence la section pour le code traduit. ==Les processeurs à jeu d'instruction reconfigurable== Nous venons de voir comment accélérer en matériel la traduction binaire. Mais il existe une alternative qui permet de se passer de traduction binaire, complétement. L'idée est d'avoir un processeur avec un '''jeu d'instruction reconfigurable'''. Le nom est assez parlent : on peut configurer le processeur de manière à supporter le jeu d'instruction de son choix ! Par exemple, on pouvait reconfigurer un CPU de manière à ce qu'il supporte le jeu d'instruction x86, ou qu'il exécute du ''bytecode'' Java, ou du code ARMv8, etc. De tels processeurs sont encore plus rare que ceux vu avant dans ce chapitre, mais ils ont existé ! Les processeurs de ce type avaient un ''microcode réinscriptible'', à savoir que c'était des processeurs microcodés pour lesquels on pouvait modifier le microcode. L'idée était qu'en changeant le microcode, on changeait le jeu d'instruction du processeur. Pour cela, le microcode était associé à une mémoire SRAM dans laquelle le microcode était copié au démarrage du CPU. Il y avait donc une micro-ROM et une micro-RAM. Les instructions étaient décodées en utilisant le microcode dans la micro-SRAM, la micro-ROM ne servait qu'au démarrage. En modifiant le contenu de la micro-SRAM, on pouvait altérer le microcode, voire le remplacer totalement. ===Le Burroughs B1700 : architecture externe=== Un exemple est celui du Burroughs B1700, et il est intéressant d'étudier en détail son architecture. Son architecture est décrite dans le livre "Interpreting Machines : Architecture and Programming of the Bl700/Bl800 Series", écrit par Elliott I. Organick et James A. Hinds. Une partie de ce qui vfa suivre est un très court résumé de ce livre. Il disposait de 4 registres généraux, nommés X, Y, T et L, tous de 24 bits. Les registres T et L étaient découpés en 6 sous-registres de 4 bits, nommés LA LB LC LD LE et LF pour le registre L, et TA TB TC TD TE et TF pour le registre T. Ils étaient adressables comme n'importe quel registre. Un point important est que le registre T avait des possibilités d’extraction spécifiques. Il était possible d'extrait N bits, placés n'importe où dans ce registre, et de les copier dans un autre registre. Nous verrons pourquoi cette possibilité est intéressante et ne sert que pour le registre T. Les registres X et Y étaient reliés à une ALU de 24 bits capable d'effectuer des opérations arithmétiques basiques. Fait étonnant, l'ALU faisait tous les calculs en même temps et fournissait ses résultats dans 7 registres : un registre SUM pour l'addition, un registre DIFFERENCE pour la soustraction, et un registre par opération bit à bit. Les registres pour les opérations bit à bit sont : CMPX et CMPY pour l'opération NOT sur chaque opérande, XANY pour le ET bit à bit, XEOY pour le XOR, XORY pour le OU logique. Les additions et soustractions pouvaient se faire sur des opérandes codés en binaire ou en BCD, suivant le code opération utilisé. A cela, il fallait ajouter deux registres de "résultat" MSKX et MSKY. Ils fournissaient un masque dépendant de la taille du résultat en bits. Cela permettait de gérer des données de 8 ou 16 bits, alors que l'ALU faisait 24 bits. Et cela n'était pas limité à 16 ou 8 bits, mais n'importe quelle taille inférieure ou égale à 24 bits était supportée, même des tailles non-conventionnelles comme des résultats codés sur 3 bits, 9 bits, 12 bits, etc. L'ALU fournissait 12 conditions, qui alimentait un registre d'état. Le registre d'état était segmenté en trois registres appelés XYST, XYCN et BICN. Le processeur contenait de nombreux autres registres de 4 bits, dont les sous-registres de T et L. En tout, il y a 27 registres de 4 bits adressables ! Et le processeur disposait d'une ALU 4 bits pour manipuler ces registres de 4 bits ! Elle est capable de faire des MOV entre registres de 4 bits, des opérations bit à bit, et peut aussi tester si un bit vaut 0 ou 1. La dernière possibilité est très utile pour implémenter les branchements. Pour communiquer avec les entrées-sorties, il disposait de deux registres CMND et DATA : CMND pour envoyer une commande à une entrée-sortie, DATA pour lire ou écrire une donnée. Pour la mémoire, il avait deux registres séparés : READ et WRITE. READ contient la donnée lue lors d'une lecture,; WRITE est pour une donnée à écrire lors d’une écriture. Il s'agit de registres d’interfaçage mémoire, qui ont été promus au rang de registres architecturaux. Les deux registres sont gérés par le microcode, qui est le langage principal de la machine. Les instructions machines configurées font sans, mais le microcode doit y avoir accès facilement. Et vu que le microcode est le langage principal de ce processeur, ces deux registres deviennent architecturaux. ===Une architecture bit-adressable=== Si avoir un microcode réinscriptible est suffisant, quelques choix de ''design'' facilitent l'émulation de jeux d'instructions variés. Par exemple, le CPU Burroughs B1700 avait ajouté une fonctionnalité très importante : il était bit-adressable. En clair, il pouvait adresser la mémoire bit par bit, si nécessaire. L'avantage est que cela permettait d'émuler des architectures dont la taille des registres/''byte''/mots était très différente. Un autre avantage est que cela facilitait la gestion des instructions de taille variable. Détaillons rapidement le premier avantage. Le processeur était un CPU 24 bits, mais il pouvait émuler des processeurs 16 bits, 8 bits, 4 bits, 1 bit, 9 bits, ou toute autre valeur. Pour cela, le processeur lisait des mots de 24 bits et utilisant un ''barrel shifter'' pour sélectionner les bits adéquats. Par exemple, pour un accès de 16 bits, la donnée lue en mémoire était masquée de manière à ne garder que les 16 bits de poids faible. Le ''barrel shifter''/circuit de masquage était placé directement avant le bus mémoire, sur le trajet des données/instructions. Le ''barrel shifter'' était commandé par un registre ''Field Unit'', qui précisait quelle était la taille des données à charger. ===La gestion de la taille des instructions=== Les instructions à décoder n'ont pas de longueur fixe, à savoir qu'elles peuvent faire 16 bit avec tel jeu d'instruction, 8 bits avec un autre, 32 bits sur un autre, etc. Elles peuvent aussi être de longueur fixe ou variable ! Le B1700 intégrait donc un support des instructions de longueur variable, qui était "détourné" pour gérer des jeux d'instruction différents. Notons que vu que l'architecture est bit-adressable, les contraintes d'alignement des instructions disparaissent, ce qui a des avantages qu'on va détailler dans ce qui suit ! L'implémentation des instructions de taille variable ajoutait un registre avant le registre d'instruction. Le registre d'instruction proprement dit était un registre M de 24 bits. Il était précédé par un registre T, dans lequel l'instruction était assemblée, en combinant plusieurs morceaux. Si l'instruction est codée est codée sur moins de 24 bits, le registre T se contente de masquer les bits en trop. Si l'instruction chargée dépasser 24 bits, le chargement se faisait en plusieurs passes, avec plusieurs lectures en mémoire. Dans ce qui suit, on suppose que l'instruction est composée de '''champs''' : l'opcode, les opérandes, les adresses absolues, les constantes immédiates, les numéros de registre, etc. L'idée est que l'instruction est chargée dans le registre T, puis découpée en champs, qui sont copiés dans les registres du processeur ou envoyés au microcode. Je rappelle que le registre T est relié à un ''barrel shifter'', différent au précédent, qui permettait de masquer, décaler et extraire une donnée de ce registre. Et il y a la possibilité d'extraire une suite de bit de ce registre T, pour la copier dans un autre registre. Par exemple, prenons une instruction composée d'un opcode, d'une constante immédiate et d'une adresse immédiate. L'opcode est lu et interprété par le microcode. Le microcode extrait la constante immédiate du registre T et la copie dans le registre X. Ensuite, l'adresse est extraite et envoyée au registre FA, une lecture est démarrée, et l'opérande est copiée dans le registre Y. Enfin, l'instruction est exécutée par le microcode. Le tout prend plusieurs micro-opérations : une première pour lire l'opcode, une seconde pour lire la première opérande, une troisième pour lire la seconde opérande, una quatrième pour tout exécuter. Notons que suivant la taille des champs, on peut en charger plusieurs à la fois. Par exemple, imaginons qu'on ait chargé 24 bits, contenant un opcode de 8 bits et une constante immédiate de 16 bits : pas besoin de faire deux accès mémoire, le registre T est juste découpé en un opcode et une constante. Le registre T est fait pour ce genre d'extraction. ===Le chargement d'un champ=== L'architecture est bit-adressable car elle gére des champs qui ne font pas forcément 8 ou 16 bits. Par exemple, certains jeux d'instructions ont des opcodes codés sur 6 ou 7 bits, voire 9-10 bits. Par exemple, prenons un opcode de 7 bits, suivi par une adresse de 12 bits et une constante immédiate de 15 bits : aucun de ces champs ne sera aligné sur 8/16/24 bits. Utiliser une architecture bit-adressable règle le problème d'alignement. Et en général, les contraintes d'alignement des instructions disparaissent ! Quand on charge une instruction, peut importe qu'elle soit ou non alignée sur 24 bits, elle sera placée dans les bits de poids faible, il suffira de masquer les bits de poids fort inutiles. Par exemple, si on charge une instruction de 16 bits, les 16 bits seront copiés dans les 16 bits de poids faible du registre T, il suffira de masquer l'octet de poids fort. Pas besoin de faire de décalages pour gérer des instructions mal alignées... Pour charger une instruction, il faut connaitre plusieurs informations : l'adresse de son premier bit, sa taille. Pour cela, le B1700 contient deux registres, nommés ''Field Adress'' (FA) et ''Field Length'' (FL), qui indiquent l'adresse du premier bit et la taille de l'instruction à charger. Le premier fait 24 bits, ce qui permet de gérer des adresses de 24 bits. N'oubliez pas que le processeur est bit-adressable, ce qui permet d'adresser 16 Mébi-bits, soit 2 mébioctets. Le registre pour la taille fait lui 16 bits, ce qui permet de gérer des instructions de 65536 bits, soit 8192 octets ! Leur nom commence par ''field'', car le processeur peut gérer des instructions de taille variable et/ou plus longues que 24 bits. Dans ce cas, le chargement de l'instruction se fait champ par champ. Pour donner un exemple, prenons une instruction composée de 20 champs de 12 bits chacun. Les champs sont chargés un par un dans le registre T. Le registre FA est alors initialisé avec l'adresse de l'instruction, il est incrémenté de 12 à chaque cycle. Le registre FL est initialisé avec la taille de l'instruction, soit 20 * 12 = 240 bits, et il est décrémenté de 16 à chaque cycle. Précisons cependant que la taille d'un champ peut être changée d'un cycle à l'autre. Par exemple, le processeur peut charger un opcode de 8 bits, l'interpréter, détecter que l'instruction demande ensuite de charger une adresse de 16 bits. Dans ce cas, le registre FL est immédiatement altéré pour charger 16 bits, au lieu des 8 bits de l'opcode immédiatement précédent. ===Le microcode réinscriptible=== Le microcode utilise des micro-opérations de 16 bits de long, ce qui est très peu et bien en-dessous des micro-opérations des autres processeurs. Les micro-opérations sortant du microcode sont envoyées au registre M, un registre de micro-opération de 16 bits. les micro-opérations écrites dans ce registre sont ensuite décodées par le hardware et exécutées. Fait intéressant, il était possible de faire un OU logique entre la micro-opération sortante et un registre du processeur, le résultat étant mémorisé dans le registre M. Cela permettait de faire du code automodifiant dans le microcode ! Utiliser du code automodifiant avec un microcode réinscriptible est parfaitement possible, et c'est utile pour émuler des modes d'adressage complexe dans le microcode. Cela permettait par exemple de remplacer une adresse dans une micro-opération. L'adresse est alors surimposée avec un OU dans le champ d'adresse, alors que le reste de l'instruction n'est pas modifié. Mais modifier directement le microcode est rarement une bonne idée. Aussi, pour éviter cela, l'idée est de modifier les instructions avec un OU logique avec un autre registre. Les micro-opérations du microcode ne sont donc pas modifiées dans le microcode, mais en sortie du microcode. La RAM mémorisant le microcode était de petite taille, et il arrivait qu'elle soit trop petit pour mémoriser le microcode complet. Dans ce cas, le programmeur devait gérer la situation en découpant le microcode en segments, certains étant placé dans le processeur, les autres étant en mémoire RAM. Le processeur peut lire le microcode placé en mémoire RAM et même l'exécuter, mais avec une pénalité en termes de performances. Quelques registres intégrés au processeur permettent ce genre de magie. Le microcode pouvait servir pour émuler un autre jeu d'instruction, mais il était aussi possible d'implémenter un véritable interpréteur, grâce aux techniques du paragraphe précédent. Le processeur était même conçu pour ! L'interpréteur était placé dans un "segment" en mémoire RAM, dont le processeur connaissait la position. Il incorporait pour cela deux registres, un pour l'adresse de base de l'interpréteur, un autre pour son adresse de fin. Il incorporait une protection mémoire limitée sur ce segment, à savoir que seul l'interpréteur pouvait lire ou écrire dans ce segment. Afin de gérer les interruptions, ces dernières étaient partiellement détectées en logiciel. Les interruptions ne s'exécutaient pas automatiquement, pour émuler correctement les interruptions. Une interruption doit être prise en compte à la toute fin de l'exécution d'une instruction, quand celle-ci a finit son travail. Mais vu qu'ici, les instructions sont émulées avec des séries de micro-opérations, il y a un problème : il est possible d'interrompre une instruction émulée en plein milieu. Pour éviter cela, le processeur ne déclenche pas les interruptions matérielles immédiatement, elles sont mises en attente. Une micro-opération dédiée vérifie si une interruption est en attente, et l'exécute alors cas échéant. La micro-opération est utilisée comme micro-opération finale d'une instruction émulée. le processeur intégrait une micro-opération qui vérifiait si une interruption a eu lieu. ==Les jeux d'instructions dédiés à un langage de programmation== De rares processeurs sont conçus pour un langage de programmation en particulier. On appelle ces processeurs, conçus pour des besoins particuliers, des '''processeurs dédiés'''. Par exemple, les fameux ''Burrough E-mode'' B5000/B6000/B7000 étaient spécialement conçus pour exécuter de l'ALGOL-60. Leurs cousins B2000/B3000/B4000 étaient eux conçus pour le COBOL. Des langages fonctionnels ont aussi eu droit à leurs processeurs dédiés. Le prolog en est un bel exemple, avec les superordinateurs de 5ème génération qui lui étaient dédié. On peut aussi citer les machines LISP, dédiés au langage LISP, qui datent des années 1970. Elles étaient capables d’exécuter certaines fonctions de base du langage directement dans leurs circuits : elles possédaient notamment un ''garbage collector'' câblé dans ses circuits ainsi que des instructions machines supportant un typage déterminé à l’exécution. Les processeurs dédiés ont eu leur heure de gloire au début de l'informatique, à une époque où les langages de haut niveau venaient d'être inventés. À cette époque, les compilateurs n'étaient pas performants et ne savaient pas bien optimiser le code machine. Il était alors rationnel, pour l'époque, de rapprocher le code machine cible et le langage de programmation de haut niveau. De nombreuses architectures dédiés ont ainsi été inventées, avant que les concepteurs se rendent compte des défauts de cette approche. Les défauts en question ne sont pas nombreux, mais assez simples à comprendre. Premièrement, elles sont très rapides pour un langage de programmation en particulier, mais sont assez mauvaises pour les autres, d'où un problème de "compatibilité". Ajoutons à cela que les langages de programmation peuvent évoluer, devenir de moins en moins populaires/utilisés, ce qui rend la création d'architectures généralistes plus pertinente. Enfin, les architectures dédiées sont évidemment des processeurs CISC, pour implémenter les nombreuses fonctionnalités des langages évolués. Et les défauts des CISC sont assez rédhibitoires à l'heure actuelle. À l'heure actuelle, les algorithmes des compilateurs se sont améliorés et savent nettement mieux utiliser le matériel. Ils produisent du code machine efficace, ce qui rend les architecture dédiées bien moins intéressantes. Si on ajoute les défauts de ces architectures dédiées, par étonnant que les architectures dédiées aient presque disparues. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'accélération matérielle de la virtualisation | prevText=L'accélération matérielle de la virtualisation | next=Le matériel réseau | nextText=Le matériel réseau }} </noinclude> 4luijk8f71zc9awzoksw5h13pdtjhx0 764025 764024 2026-04-19T18:18:06Z Mewtow 31375 /* Le microcode réinscriptible */ 764025 wikitext text/x-wiki De nos jours, la majorité des programmeurs programment dans des langages de haut niveau. Il est très rare pour eux d'avoir à utiliser de l'assembleur, et encore moins un langage machine. Les programmes écrits dans un langage de haut niveau sont traduits en langage machine par un logiciel appelé le compilateur. [[File:Compilation.PNG|centre|vignette|upright=2.5|Compilateur : principe.]] Il se trouve que quelques rares processeurs sont conçus pour faciliter le travail du compilateur. Et ce sont ces processeurs que nous allons voir dans ce qui suit. Ils représentent une catégorie de jeux d'instruction à part, qui n'a pas vraiment de nom. ==La compilation et l’interprétation== Un compilateur traduit donc du code source, écrit dans un langage de haut niveau, vers du langage machine. La traduction est rarement directe. En général, le compilateur traduit le code source en assembleur, qui est lui-même traduit en langage machine. L'assembleur est une représentation textuelle du langage machine alors que le code machine est du binaire exécutable par le processeur. La traduction se fait donc en deux étapes, la '''compilation''' proprement dite et l''''assemblage''', réalisées respectivement par un compilateur et un assembleur. Il faut parfois rajouter une troisième phase d'édition des liens, que nous passons volontairement sous silence. {| |[[File:Code C.png|vignette|200px|Le code source, ici en C.]] |[[File:Assem.png|vignette|250px|Le code assembleur.]] |[[File:Binary file - hello world (C programming).png|vignette|150px|Le code machine.]] |} ===L'interprétation et la compilation à la volée=== Une alternative à la compilation est l''''interprétation''', qui transforme le code source en code machine à la volée. Avec l'interprétation, le code source est passé à un logiciel appelé l''''interpréteur''', qui exécute le code source ligne de code par ligne de code. Une ligne de code est traduite en code machine, qui est exécutée, puis l'interpréteur passe à la ligne suivante. [[File:Kaantotulkkaus.png|centre|vignette|upright=3|Compilation versus interprétation.]] Un défaut de l'interprétation est son cout en performance, qui est assez important. Aussi, l'interprétation stricte a évolué vers un hybride entre interprétation et compilation. L'idée est qu'une partie peu importante du code source est interprétée, alors que le code important est compilé juste avant d'être exécuté. Pour le dire autrement, le code source est partiellement compilé à la volée, juste avant son exécution. Aussi, on parle de '''compilation à la volée'''. Le terme en anglais est ''Just In Time compilation'', abrévié en JIT. En soi, le JIT est associé à de l'interprétation, les JIT sont en réalité des hybrides interpréteurs-compilateurs. Un point important est que compiler du code à la volée est assez lourd, cela a un cout en performance. Par contre, le code compilé s'exécute plus vite que du code interprété. Si on exécute le code compilé une seule fois, le cout de la compilation l'emporte sur le gain à l'exécution. Mais le code compilé est réutilisé autant de fois que nécessaire. Le gain à l'exécution est donc multiplié par le nombre d'exécutions, alors que le cout de la compilation est répartit sur plusieurs exécutions, il est amorti. Les portions du code sont choisies de manière à avoir un gain en performance maximal. L'idée générale est que le code exécuté très souvent est compilé, alors que du code exécuté pas souvent est interprété. Typiquement, les boucles les plus souvent exécutées sont compilées, le code exécuté une seule fois est interprété. ===Les langages intermédiaires=== Les compilateurs modernes passent par un '''langage intermédiaire''' pour faire la transformation en code machine. Le compilateur traduit le langage de haut niveau en langage intermédiaire, puis traduit le langage intermédiaire en code machine cible. Le langage intermédiaire est parfois appelé le '''''bytecode''''', et ce terme recouvre aussi le code écrit avec du ''bytecode''. L'interprétation aussi peut passer par l'intermédiaire d'un un ''bytecode'', ce n'est pas limité aux compilateurs proprement dit. {| |[[File:Intermediate representation scheme.png|centre|vignette|upright=1.5|Représentation intermédiaire d'un compilateur.]] |[[File:Code interpreter scheme.png|centre|vignette|upright=1.5|Fonctionnement d'un interpréteur.]] |} Faire ainsi a de nombreux avantages, le principal étant d'avoir un compilateur capable de traduire un langage de haut niveau vers plusieurs jeux d'instructions différents. Cela permet d'avoir, par exemple, un compilateur qui traduit du C soit en code machine x86, soit en code machine pour un CPU ARM, soit pour un CPU POWERPC, etc. [[File:Compiler design.svg|centre|vignette|upright=3|Compiler design]] Pour l'interprétation, l'usage d'un ''bytecode'' a d'autres avantages. Pour l'exploiter, il faut distribuer non pas le code source, mais le ''bytecode''. En clair, l'interpréteur ne prend pas en entrée le code source, mais du ''bytecode'' déjà compilé. La traduction est alors beaucoup plus simple, car le code source a déjà été partiellement compilé, par la traduction en ''bytecode''. Interpréter du code source est en effet assez compliqué : il faut effectuer des étapes d'analyse lexicale, sémantique, et bien d'autres. Avec du ''bytecode'', ces étapes ont été réalisées lors de la compilation du ''bytecode'', l'interpréteur a alors peut de choses à faire. L'interprétation du ''bytecode'' se fait instruction par instruction, au niveau du ''bytecode''. Une instruction du ''bytecode'' est traduite en une instruction machine équivalente, qui est exécutée. Si l'instruction est un peu complexe, il exécute une fonction/procédure qui fait la même chose. Pour résumer, le ''bytecode'' est ensuite traduit à la volée et exécuté instruction par instruction par un logiciel appelé l'interpréteur. [[File:Control table.png|centre|vignette|upright=2|Control table]] Le langage intermédiaire peut être vu comme l'assembleur d'un processeur, n'existe pas forcément dans la réalité, mais dont le jeu d'instruction est décrit en détail. Le processeur en question est appelé une '''machine abstraite''', ou encore une ''machine virtuelle''. Nous utiliserons le terme de machine abstraite dans ce qui suit. La machine abstraite n'est pas la même suivant que l'on cible la compilation ou l'interprétation/JIT. Pour l'interprétation/JIT, la machine abstraite est souvent une machine à pile, car cela simplifie grandement la traduction en code machine final. L'interprétation demande que la traduction du ''bytecode'' en code machine soit la plus simple possible. Et cela demande de prendre en compte pas mal de détails. Par exemple, les différents jeux d'instruction existants n'ont pas le même nombre de registres, ce qui pose problème lors de la traduction du ''bytecode''. Lors de la transformation en code machine, un algorithme d'allocation de registres se débrouille pour traduire le code intermédiaire en code qui utilise un nombre limité de registres. Pour améliorer cette allocation de registres, il y a deux solutions niveau ''bytecode'' : soit utiliser un nombre illimité de registres, soit utiliser une machine à pile. Pour les machines à pile, il existe un algorithme simple et rapide pour traduire un code écrit pour une machine à pile en un code écrit pour un processeur avec des registres, qui se débrouille pas trop mal pour allouer efficacement les registres. C'est un avantage assez important pour les langages interprétés. ==Les processeurs qui exécutent du bytecode== Le ''bytecode'' est un code machine, ce qui signifie qu'il peut en théorie s'exécuter sur un processeur qui implémente le jeu d’instruction associé. Si le jeu d'instruction d'un ''bytecode'' est souvent une description censée être fictive, elle n'en reste pas moins un jeu d'instruction et des caractéristiques précises, qu'on peut l'implémenter en matériel ! Néanmoins, tous les ''bytecode'' ne sont pas égaux de ce point de vue. Certains sont faciles à implémenter en matériel, d'autres non. Les ''bytecodes'' implémentés en matériel sont ceux dont les machines abstraites sont des machines à pile. Un premier exemple est celui des processeurs Pascal MicroEngine, qui exécutaient directement le bytecode du langage Pascal, le fameux UCSD P-code. Un second exemple est la '''machine SECD''', qui sert de langage intermédiaire pour certains compilateurs de langages fonctionnels. Elle a été implémentée en matériel par plusieurs équipes, notamment par les chercheurs de l'université de Calgary en 1989. Dans le même genre, quelques processeurs simples étaient capables d’exécuter directement le ''bytecode'' du langage FORTH. Le FORTH, un des premiers langages à pile de haut niveau, possède de nombreuses implémentations hardware et est un des rares langages de haut niveau dont le ''bytecode'' a été utilisé comme langage machine sur certains processeurs. Par exemple, on peut citer le processeur FC16, capable d’exécuter nativement du ''bytecode'' FORTH. ===Les processeurs Java=== Le cas le plus impressionnant est celui de la machine virtuelle Java, qui est un design de processeur comme un autre. En temps normal, le ''bytecode'' Java est compilé ou interprété, mais certains processeurs exécutaient du ''bytecode'' Java directement, sans interprétation ni compilation. Ils sont appelés des '''processeurs Java'''. On peut citer les processeurs ARM disposant de l'extension Jazelle, du JEMCore et du aJ-100 de aJile Systems, le picoJava II de Sun, et quelques autres. Il y a aussi eu quelques projets de recherche ou processeurs open source, comme Komodo, jamuth, le ''Java Optimized Processor'' et quelques autres projets du même genre. L'implémentation de la machine virtuelle Java n'était cependant pas complète. Quelques instructions complexes n'étaient pas gérées par le processeur et devaient être émulées en logiciel. Mais la grosse majorité des instructions l'était. Les instructions non-supportées étaient émulées avec la méthode ''trap and emulate'', à savoir qu'un opcode inconnu déclenchait une exception matérielle ''opcode invalide'', dont la routine pouvait émuler les instructions qui doivent l'être. Le processeur aJ-100 de aJile est capable d'exécuter la quasi-totalité des instructions de la JVM Java, seules deux instructions faisant exception. Les '''accélérateurs Java''' sont une solution intermédiaire, qui permet de traduire à la volée du ''bytecode'' Java en instructions RISC, exécutées sur un autre processeur. Il s'agit formellement de traduction binaire, mais je préfère en parler ici. L'idée est de combiner un processeur RISC avec un coprocesseur Java. Le coprocesseur lit le ''bytecode'' Java et le traduit en instructions RISC, ces dernières étant exécutées sur le processeur RISC. Le JA108 de Nozomi était un coprocesseur de ce genre, au même titre que le coprocesseur JSTAR de JEDI. L'extension Jazelle de certains CPU ARM fonctionnait vraisemblablement sur un principe similaire, sauf que le coprocesseur était intégré dans le processeur ARM, entre le cache d'instruction et le décodeur d'instructions ARM. L'intérêt des processeurs Java n'est pas qu'une question de performance. Certes, ils permettent d’exécuter plus vite les programmes compilés en ''bytecode'', comme des programmes Java pour la JVM Java. Mais l'intérêt est aussi de faire des économies de mémoire RAM et ROM. Je rappelle que la machine virtuelle Java est une machine à pile, ce qui fait que sa densité de code est excellente. Les programmes en ''bytecode'' Java sont donc très petits, et prennent peu de place en mémoire ROM. De plus, utiliser un processeur Java permet de se passer d'interpréteur ou de JIT Java, ce qui économise encore plus de mémoire. ===Transformer un processeur RISC en machine à pile=== La plupart des processeurs Java utilisent en interne une architecture à registre généraux, qui émule une machine à registre en microcode. Quelques registres sont réservés pour l'état de la machine virtuelle, avec au minimum un registre réservé pour le pointeur de pile, un autre pour un pointeur vers un ''pool'' de constantes, et quelques autres. D'autres registres servent à mémoriser le haut de la pile, à savoir le sommet de la pile et quelques opérandes situées juste en-dessous. Par exemple, le processeur picoJava II gardait les 64 opérandes au sommet de la pile dans 64 registres généraux, séparés des registres pour les pointeurs et l'état de la JVM. Une optimisation possible, envisagée sur les processeurs picoJava, était une sorte de macro-fusion sous stéroïde. L'idée était de fusionner certaines séries d'instructions Java en une seule instruction machine. Typiquement, pour faire une opération arithmétique, une machine à pile empile deux opérandes et exécute une opération, ce qui prend trois instructions machine. La spécification picoJava 1 a proposé d'envoyer directement la seconde opérande en entrée de l'ALU, ce qui fusionnait l'empilement de la seconde opérande et l'opération arithmétique. Le processeur picoJava II va plus loin et fusionne les trois instructions en une seule instruction RISC, agissant sur des registres. Mais la majorité des processeurs Java ne faisaient pas cela, car cela complexifie grandement le décodeur. Les processeurs ''bigfoot'' de DCT utilisaient un système différent pour émuler une pile avec des registres. Leur banc de registre de 64 bits était coupé en deux : les 16 premiers registres étaient utilisés pour la pile, les 48 suivants étaient utilisés pour autre chose. L'idée est que le registre 0 mémorise un emplacement vide, le registre 1 contient le sommet de la pile, le registre 2 l'opérande sous le sommet de la pile, et ainsi de suite. Les 15 premières opérandes de la pile sont donc mémorisées dans les registres. Les instructions écrivent leur résultat dans le registre adéquat, puis tous les registres sont décalés pour que le résultat passe dans le registre R1. Par exemple, les instructions PUSH chargent une opérande dans le registre 0, puis tous les registres sont décalés de un rang, pour que l'opérande chargée soit dans le registre 1. Le registre 0 devient alors le registre numéro 1, le registre 1 devient le 2, et ainsi de suite. Par contre, les opérations arithmétiques font autrement. Une instruction d'addition, par exemple, lit les opérandes dans le registre 1 et 2, puis place son résultat dans le registre 2 (les deux opérandes sont dépilées). Puis, les registres sont décalés d'un rang pour que ce registre 2 devienne le 1. Les registres ne sont en réalité pas décalés, il n'y a pas de transferts entre registres. A la place, les 16 premiers registres utilisaient une forme limitée de renommage de registres, similaire au banc de registre tournant utilisé sur les processeurs EPIC comme l'Itanium. Les numéros de registres sont renommés, décalés, à chaque fois qu'on empile ou dépile une opérande. Pour faire le renommage de registres, processeur contient un ''register stack counter'' de 4 bits, qui est incrémenté à chaque fois qu'on empile une donnée, et est décrémenté quand on dépile une donnée. Quand on veut accéder à un opérande, le numéro de registre architectural associé est traduit en numéro de registre physique, en additionnant le ''register stack counter'' (modulo 16, pour rester sur 4 bits). ==L'accélération de l'interprétation/JIT : le ''Thumb-EE'' d'ARM== Quelques processeurs ont ajouté des instructions pour faciliter le travail des interpréteurs/JIT. Par exemple, en 2005, ARM a ajouté le mode ''Thumb Execution Environment'', qui faisait cela. Il reprenait le jeu d'instruction compact ''thumb'' et ajoutait quelques instructions et modifiait le comportement d'instructions existantes. Le jeu d'instruction ''Thumb-EE'' a été ajouté sur les CPU ARM en 2005, mais a été déprécié en 2011, par manque d'utilité. Mais la tentative mérite qu'on s'attarde dessus. Les CPU de gamme M, déstinés à l'embarqué, n'ont jamais supporté ''Thumb-EE'', vu que de tels processeurs ne sont pas conçus pour exécuter du code interprété/JIT. Le mode ''Thumb-EE'' est un mode d'exécution, séparé de l'ARM normal. Ainsi, une instruction ''Thumb-EE'' et une instruction ARM peuvent avoir le même encodage en binaire, mais se comporter différemment. Le processeur est à tout moment soit en mode ''Thumb-EE'', soit en mode ARM normal, ce qui précise comment ces instructions doivent se comporter. Des instructions ont été ajoutées par ''Thumb-EE'', pour qu'il fasse son travail. L'entrée et la sortie du mode ''Thumb-EE'' se fait avec deux instructions : ENTERX et LEAVEX. Une fois en mode ''Thumb-EE'', le décodage des instructions ''thumb'' se fait avec les règles du ''Thumb-EE'', et non celles du ''thumb'' normal. Une première différence entre ''thumb'' normal et ''Thumb-EE'' est l'ajout de ce qui s'appelle un ''null check'' pour les instructions mémoire. Le ''null check'' est utilisé pour les instructions en mode d'adressage "base + indice" ou "Base + décalage", ou tout autre mode d'adressage avec un registre de base utilisés dans des calculs d'adresse. L'idée est de vérifier si l'adresse de base vaut zéro ou non. Si c'est le cas, le processeur lève une exception matérielle, qui est traitée par l'interprétation ou le compilateur JIT. Une telle situation est en effet signe d'une erreur d'adressage mémoire, qui doit être traitée par l'interpréteur. Typiquement, c'est signe qu'un pointeur n'a pas bien été initialisé, ce qui peut arriver avec du code interprété ou JIT. Les interpréteurs/JIT traitent généralement la situation en ajoutant un test avant toute instruction mémoire, pour vérifier si le pointeur accédé vaut zéro ou non. Avec ''Thumb-EE'', pas besoin d'ajouter les tests en questions : ils sont réalisés automatiquement lors de chaque instruction mémoire. : Les registres pour la pile, comme le pointeur de pile et le pointeur de ''frame'', sont aussi concernés. L'instruction CHKA vérifie que les accès à un tableau ne débordent pas en dehors du tableau en question. Par exemple, pour un tableau de 1024 éléments, elle vérifie si l'indice est compris entre 0 et 1024. Pour cela, elle vérifie si l'indice de tableau est bien dans l'intervalle adéquat et lève une exception matérielle si ce n'est pas le cas. Son utilité se comprend quand on sait que tous les accès à un tableau sont vérifiés par l'interpréteur. L'interpréteur est censé ajouter des instructions pour vérifier les indices, à savoir deux ou trois branchements. Avec CHKA, le test se fait en une seule instruction. D'autres modifications mineures de l'encodage des instructions sont aussi présentes, ainsi que des modifications pour les instructions LOAD/STORE. Précisément, dans les modes "base + indice" et "base + décalage", le décalage et l'indice subissent maintenant des décalages. L'idée est de simplifier le calcul d'adresse. De quoi économiser quelques instructions lors des calculs d'adresse, ce qui facilite le travail de l'interpréteur/JIT. Et des instructions LOAD/STORE précises voient leur comportement modifié pour faciliter la gestion de la pile de Java ou l'exécution des méthodes locales. Pour faciliter le travail de l'interpréteur, deux instructions HBP et HBLP ont été ajoutées. Elles branchent vers une fonction qui gère les exceptions logicielles (une fonctionnalité présente dans de nombreux langages, comme Java). La première effectue un branchement vers la fonction en question, la seconde sauvegarde l'adresse de retour avant de faire ce branchement. Les deux peuvent brancher vers 256 fonctions pré-déterminées, sans avoir à présenter leur adresse. ==La traduction binaire accélérée par le matériel== Plus haut, nous avons surtout parlé des compilateurs et des interpréteurs. Cependant, nous devons aussi parler de la '''traduction binaire'''. Elle traduit un programme écrit dans un code machine vers un autre code machine. Par exemple, elle traduit un programme compilé pour un CPU x86 vers un code machine ARM. En général, le système d'exploitation est généralement compilé pour le jeu d'instruction natif, mais il exécute des applications prévues pour le x86. Les applications sont traduites par le système d'exploitation, avant d'exécuter le code traduit. : Nous parlerons dans la suite de code machine source et de code machine cible, pour parler respectivement du code à traduire et du code obtenu après traduction. Idem avec d'autres termes comme architecture cible/source, ou jeu d'instruction cible/source. La traduction binaire est surtout utilisée pour des questions d'émulation ou de compatibilité. Par exemple, lorsque les Macintosh sont passés de processeurs Power PC vers des processeurs x86, le système d'exploitation Mac OS utilisait la traduction binaire pour convertir les anciennes applications Power PC vers du code x86. Les utilisateurs n'y ont vu que du feu. Le système de traduction binaire était appelé Rosetta 1. Par la suite, lors de la transition de processeurs x86 vers des processeurs Apple, Rosetta 2 a vu le jour. La traduction binaire a surtout été utilisée pour traduire du code x86 vers un autre jeu d'instruction. Il faut dire que le x86 est le jeu d'instruction dominant. De nombreuses entreprises ont eu pour ambition de briser l'hégémonie du x86 sur PC, en remplaçant le x86 par un jeu d'instruction plus performant, tout en gardant une compatibilité maximale. La traduction binaire était la seule solution pratique. Elle était le plus souvent intégralement réalisée en logiciel, comme c'était le cas sur les architectures Itanium avec le ''IA-32 Execution Layer''. ===Les généralités sur la traduction binaire assistée en matériel=== L'architecture source est presque tout le temps une architecture CISC, assez ancienne, qu'on souhaite émuler. Le choix de l'architecture cible se porte souvent sur une architecture VLIW, fort différente de l'architecture source, et ce pour obtenir une économie de matériel conséquente. Pour s'exécuter rapidement, le code traduit doit exploiter le parallélisme d'instruction, à savoir exécuter plusieurs instructions en même temps dans des unités de calcul séparées. Il est possible d'utiliser un processeur superscalaire avec exécution dans le désordre pour cela, mais au prix d'un cout important en transistors. Alors qu'en utilisant un CPU VLIW, c'est le traducteur binaire qui fait tout le travail d'extraction du parallélisme d'instruction. Et après tout, quitte à avoir un traducteur binaire, autant lui refiler le boulot d'optimisation. Compiler du code à la volée est certes assez lourd, mais qu'il existe des algorithmes efficaces pour regrouper des instructions indépendantes dans une seule instruction VLIW. Au passage, le processeur VLIW a plus de registres que le processeur source. Cela permet de faire du renommage de registres directement en logiciel. Le traducteur binaire n'hésite pas à changer les noms de registres entre instructions source et instruction VLIW cible, afin de supprimer des dépendances de données. En conséquence, cela ouvre des opportunités de parallélisme, qui sont exploitées lors du regroupement des opérations en instructions VLIW. Vous pourriez penser que le choix d'un CPU VLIW pour émuler du CISC est tout sauf optimal. Vous devez penser que la traduction binaire est d'autant plus simple que l'architecture source et cibles sont semblables. Dans les faits, ce n'est pas tellement traduire les instructions qui pose problème, mais plus la gestion du registre d'état, des exceptions matérielles, de la mémoire virtuelle, la différence entre gros-boutisme et petit-boutisme, de même que des différences d'adressage pour les périphériques. Le premier problème est la gestion des conditions, notamment en présence d'un registre d'état. En pratique, la traduction binaire s'utilise pour traduire du code CISC vers du code RISC ou VLIW. Les architectures source ont donc un registre d'état, qui est mis à jour non seulement par des instructions de test, mais aussi des instructions arithmétiques. L'architecture cible n'a elle pas de registre d'état, mais des registres à prédicats. La traduction de l'un vers l'autre est alors quelque peu compliquée. Et elle est d'autant plus compliquée que le registre d'état est la source de dépendances d'instruction implicites, qui réduisent les performances. Par exemple, si une instruction arithmétique modifie le registre d'état, cela peut impacter l'exécution d'une instruction ultérieure, qui lit ce registre d'état. Pour éliminer ces fausses dépendances, le traducteur binaire doit renommer ce registre en logiciel et détecter les dépendances utiles. Et le compilateur doit gérer les cas où il y a beaucoup de distance entre les deux, voire les cas où l'instruction dépendante n'a pas encore été analysée par le compilateur. Un autre problème est lié à la gestion des exceptions matérielles, et précisément des exceptions précises. Pour rappel, une instruction VLIW regroupe plusieurs opérations, opérations qui correspondraient à une instruction machine sur un CPU pas VLIW. En conséquence, plusieurs instructions du langage machine "source" sont regroupées en une seule instruction VLIW. Et il faut tenir compte du cas où une instruction source lève une exception. Dans le code VLIW, cela signifie qu'une opération lève une exception, et il faut annuler partiellement l'instruction VLIW associée. Et par partiellement, on veut dire que seules les opérations suivantes dans l'ordre du programme source doivent être annulées, puis ré-exécutées. Et c'est un sacré casse-tête ! Les processeurs VLIW utilisent comme solution le mécanisme d'exceptions différées des processeurs EPIC, vu il y a quelques chapitres. Pour résumer, le code est exécuté par blocs d'instruction, délimités par des branchements ou tout autre limite/barrière pertinente dans le code. Un bloc de code est compilé en deux versions : une version rapide sans exceptions matérielles, une version lente qui gère les exceptions précises. La version rapide s'exécute sans exécuter les exceptions. Cependant, les exceptions matérielles sont enregistrées, pour être prises en compte à la toute fin du bloc de code. La version rapide est exécutée en premier et elle mémorise si une exception matérielle a eu lieu. Puis une instruction vérifie si une exception a eu lieu et décide quoi faire. Si aucune exception n'a eu lieu, elle passe à la suite du programme, les résultats du bloc de code sont définitivement acceptés. Mais si une exception a eu lieu, tout est annulé. Le processeur est remis dans l'état initial, puis le code est ré-exécuté instruction par instruction de manière à gérer l'exception correctement. ===La traduction par pré-décodage=== Une première solution serait de faire la traduction binaire dans le cache d'instruction. Lors d'un défaut de cache, le code chargé depuis la RAM est traduit en code machine cible. Et c'est ce code machine cible qui est mémorisé dans le cache d'instruction et exécuté par le processeur. La technique marche sur le papier et n'est qu'une amélioration des techniques de pré-décodage vues il y a quelques chapitres. Cependant, le code obtenu est une traduction assez basique, qui n'incorpore pas d'optimisations dignes de ce nom. De plus, elle gère mal le cas où la taille du code source et cible sont potentiellement très différentes. Concrètement, elle est surtout utile pour traduire du code d'un processeur RISC vers un autre processeur RISC, dont les tailles d'instructions sont similaires. Elle ne permet pas d'émuler plusieurs jeux d'instructions différents, le cout en matériel (un circuit de pré-décodage par jeu d'instruction) serait trop important. ===Le projet DAISY d'IBM=== De très rares processeurs étaient conçus pour accélérer cette traduction binaire, afin de garder de bonnes performances. Les premiers à avoir étudié l'idée étaient IBM, avec leur projet DAISY (''Dynamically Architected Instruction Set from Yorktown''). Le projet de base était de convertir à la volée du code compilé pour des CPU Power PC, vers du code VLIW. Les chercheurs d'IBM avaient développé un algorithme de traduction dynamique efficace, ainsi qu'un processeur VLIW disposant d'optimisations spécifiques à la traduction binaire dynamique, à la volée. De nombreuses idées de ce projet ont été reprises ou re-découvertes par la société Transmetta, avec ses processeurs Crusoe et Efficieon, puis par NVIDIA avec son projet Denver. Les ingénieurs de ce projet ont étudié la possibilité d'émuler plusieurs jeux d'instructions différents sur un même processeur, notamment le s390 d'IBM et le x86. En théorie, cela demande juste d'avoir plusieurs programmes de traduction binaire : un pour le x86, un autre pour le s390, éventuellement un autre pour le Power PC. Mais cela ne s'est pas concrétisé. Mais les ingénieurs ont étudié quelle pourrait être l'architecture VLIW idéale pour ça. Par exemple, il fallait des additions 3-opérandes pour simplifier les calculs d'adresse. Une difficulté était la gestion du registre d'état, dont les bits ne sont pas mis à jour de la même manière sur le x86 et le s390 ou le Power PC. La différence entre gros-boutisme et petit-boutisme était aussi un problème, de même que des différences d'adressage pour les périphériques. Pour que la traduction binaire soit efficace, le processeur VLIW intègre diverses optimisations, comme les exceptions différées et les branchements multi-voies. La principale est la suivante : seul le code des boucles ou fonctions exécutées fréquemment est traduit en code VLIW, le reste du code est interprété. En effet, traduire du code binaire prend plus de temps qu'une simple interprétation. Pour du code qui ne sera exécutée qu'une seule fois, il est plus rapide d'utiliser l'interprétation. Par contre, pour du code exécuté beaucoup de fois, le cout de la traduction binaire est amorti, dilué sur N exécutions, compensé par le gain en temps d'exécution de ce code traduit. Au final, cela permet de ne traduire que le code qui le mérite. La détection du code fréquemment exécuté est réalisée dans le cœur VLIW. L'unité de branchement mémorise les derniers branchements rencontrés et le nombre de fois qu'ils ont été exécutés. Si ils ont été exécutés un certain nombre de fois, l'unité de branchement lève une exception matérielle, qui invoque le traducteur binaire. La routine de cette exception est le traducteur binaire proprement dit. En clair, la traduction binaire est démarrée quand le processeur détecte qu'une fonction a été exécutée plus de N fois, via une exception matérielle dédiée. La mesure du nombre d'exécution d'un branchement se fait dans l'unité de calcul dédiée aux branchements, avec l'aide d'une sorte de ''branch adress buffer'' modifié. Pour rappel, le ''branch adress buffer'' mémorise le ''Program counter'' de chaque branchement récemment rencontré. Ici, chaque entrée du ''branch adress buffer'' est associée à un compteur incrémenté à chaque exécution du branchement. ===Les processeurs Crusoe et Efficieon de Transmetta=== Les processeurs Crusoe Et Efficeron sont deux processeurs VLIW produits par la société Transmetta, une société californienne rachetée par NVIDIA. Ils étaient conçus pour exécuter spécifiquement des programmes x86, système d'exploitation inclus. L'idée derrière ce projet était d'exécuter du code x86 sans que les applications, ni même le système d'exploitation et le BIOS soient au courant ! Les processeurs Transmetta étaient en réalité conçus pour exécuter un programme unique, le ''Code Morphing Software'' (CMS), qui traduisait le code x86 en code VLIW. Transmetta ne voulait pas que son processeur VLIW soit exploité directement, sans le CMS. Il n'avait pas rendu public de compilateur pour traduire du C vers du code VLIW, il n'avait pas donné la documentation du jeu d'instruction VLIW. Des efforts de rétro-ingénieurie, documentés sur le site realworldtech, ont cependant permis de comprendre comment étaient encodées les instructions du processeur Crusoe. Comme pour le projet Daisy, le ''Code Morphing Software'' utilisait à la fois interprétation et traduction binaire, selon les besoins. Les instructions sont interprétées lors de leurs premières exécutions, mais le CMS bascule sur de la traduction en code VLIW après un certain nombre d'exécution. Ainsi, les boucles souvent exécutées sont traduites en code VLIW, alors que le reste du code est interprété. Le choix du code à traduire est le fait du CMS, il utilise des heuristiques complexes pour, qui ne sont pas connues dans le détail. Le ''Code Morphing Software'' est mémorisé dans une EEPROM, ce qui en fait un ''firmware'' situé en dessous du BIOS. Le CMS démarre ensuite le BIOS, qui lui-même démarre le système d'exploitation, qui lui-même démarre les pilotes de périphériques et les programmes. Le CMS se réserve les 16 premiers mébioctets de l'espace d'adressage. L'EEPROM du CMS est mappée dedans, mais n'en utilise que la moitié. Le reste est utilisé comme cache, pour mémoriser le code VLIW traduit par le CMS. Les deux processeurs intégraient aussi deux mémoires SRAM, utilisées par le CMS, appelées la ''local program memory'' (LPM) et la ''local data memory'' (LDM). La première contient du code qui gére les interruptions, la mémoire virtuelle, les exceptions matérielles, les problèmes d'alignement mémoire, et quelques fonctions très fréquemment utilisées par le CMS. La seconde est de la mémoire RAM utilisée par ces fonctions de la ''local program memory''. Les mémoires caches L1 et L2 sont séparées de ces deux mémoires. Le processeur Crusoe et Efficieon étaient des processeurs VLIW très simples. Ils n'avaient même pas de MMU, ni de fonctionnalités importantes sur les CPU x86. Pour Crusoe, les instructions VLIW étaient encodées sur 64 ou 128 bits, et regroupaient entre 2 à 4 opérations. Les instructions LOAD/STORE ne supportaient pas d'adressage indicé, juste de l'adressage indirect à registre. Crusoe avait 5 unités de calcul : deux ALU entières, une unité LOAD/STORE, une unité de branchement, une FPU. Efficieon doublait le nombre d'ALU entière et d'unité LOAD/STORE, les instructions VLIW passaient à 256 bits. Le processeur Crusoe contient 160 registres, dont 64 registres généraux, 32 registres flottants. Les registres généraux font 32 bits, ce qui est cohérent avec le fait que les CPU x86 de l'époque étaient des processeurs 32 bits. Quant aux registres flottants, ils faisaient 80 bits, ce qui colle avec la taille des registres flottants de la FPU x87 utilisée à l'époque. Sur les 64 registres généraux, seuls 48 étaient réellement utilisable pour mémoriser des opérandes. Une partie des registres généraux étaient utilisés pour la gestion de la pile, d'autres pour mémoriser l'état du CPU x86 émulé, un registre était un registre zéro non accesible en écriture. La gestion des exceptions est optimisée avec un système d'exceptions différées, le même que celui décrit plus haut. Le processeur utilise un système similaire, pour effectuer des lectures anticipées, qui a été expliqué dans le chapitre sur les processeurs VLIW/EPIC. Sauf qu'il s'agit là d'une optimisation pour gagner en performances en général, pas une aide utile pour gérer des exceptions précises ou toute autre subtilité de la traduction binaire. Pour gérer les exceptions différées et les lectures anticipées, le processeur mémorise l'état du processeur avant de démarrer un bloc de code. Pour cela, le processeur contient des ''shadow registers'', 48 registres entiers et 16 registres flottants. Vous remarquerez qu'il y a autant de registres entiers que de ''shadow registers'' entiers. Les registres entiers/flottants sont mémorisés dans les ''shadow registers'' avant d'exécuter un bloc de code, afin que le processeur puisse revenir à l'état de base. Une autre fonctionnalité liée est le ''gated store buffer''. Le principe est d'éviter toute écriture en mémoire RAM, tant que le code VLIW n'a pas émis d'instruction ''commit''. L'idée est que les écritures sont conservées dans la ''store queue'' de l'unité LOAD/STORE. Pour rappel, la ''store queue'' met en attente les écritures soit tant que les écritures ne sont pas terminées, soit tant que la RAM est occupée. L'idée est que les écritures sont accumulées dans la ''store queue'' tant que l'instruction ''commit'' n'a pas eu lieu. Le ''store queue'' est "déversé dans la RAM" seulement quand l'instruction ''commit'' s'exécute. ===Les CPU NVIDIA Denver=== Nvidia a tenté quelque chose de similaire avec son projet Denver. NVIDIA a racheté l'entreprise Transmetta, ses brevets, et a tenté de refaire la même chose. Le projet initial était de traduire du code x86 vers un jeu d'instruction VLIW propriétaire, comme l'a fait Transmetta. Mais NVIDIA n'a pas réussi à acquérir la licence du jeu d'instruction x86 et s'est rabattu sur le jeu d'instruction ARM. Le processeur né de ce projet est le Tegra K1-64 CPU. Il s'agit d'un processeur composé d'un cœur VLIW, couplé à deux décodeurs d'instructions ARM. Les deux décodeurs ARM traduisent une paire d'instructions ARM consécutives en instructions VLIW, pour les exécuter sur le cœur VLIW. Le CPU peut fonctionner selon deux modes : ARM et VLIW. En somme, le processeur supporte deux jeux d'instruction : le jeu d'instruction ARM, et un jeu d'instruction VLIW. En mode ARM, les instructions ARM sont chargées depuis le cache, traduites en VLIW par les décodeurs ARM, puis exécutées par le cœur VLIW. En mode optimisé, le CPU exécute des instructions VLIW chargées depuis le cache d'instruction, les décodeurs ne sont pas utilisés. : Le CPU supporte en réalité deux jeux d'instruction ARM : ARM8 et 7. Mais ils correspondent au même mode pour le CPU. Un point important est que le processeur peut changer très rapidement de mode, en à peine quelques cycles d'horloges. On n'est clairement pas dans le cas des CPU x86, qui mettent des plombes pour passer du mode 32 à 64 bits et inversement. La commutation est tellement rapide qu'on peut considérer que le processeur supporte deux jeux d'instruction simultanément. Et le support simultané de deux jeux d'instruction facilite l'implémentation de la traduction binaire. Encore une fois, inutile de traduire du code qui ne sera exécutée qu'une seule fois, mieux vaut l'exécuter directement dans le mode ARM. Surtout que le processeur supporte un mode ARM qui permet d'exécuter du code ARM sans perte de performance. Par contre, le code fréquemment utilisé, notamment dans des boucles critiques, est traduit en VLIW. Au final, les performances sont optimisées, en limitant le travail du logiciel traducteur binaire. La détection du code fréquemment exécuté se fait différemment que pour les processeurs Transmetta et Daisy. L'unité de branchement ne détecte pas directement le code beaucoup exécuté, même si elle a un rôle à jouer. A la place, la détection est réalisée en logiciel, par un ''thread'' dédié, qui s'exécute sur un cœur séparé. Il détecte le code fréquemment utilisé en regardent les branchements exécutés récemment. Pour cela, le processeur mémorise l'historique des branchements pris récemment et copie cette historique dans une mémoire tampon dédiée, partagée entre tous les cœurs. Vu qu'elle est partagée, le ''thread'' de détection a accès à l'historique sans pertes de performances, seule la copie de l'historique dans ce tampon a un cout en performance, pas son partage. Le code VLIW obtenu après traduction binaire est mémorisé dans la mémoire RAM, dans une portion spécialement réservée pour. Elle fait 128 mébioctets, et est appelée le '''cache d'optimisation''' par NVIDIA. Le cache d'optimisation est protégé en écriture et seul le traducteur binaire peut écrire dedans. Au passage, le traducteur binaire est du code VLIW, ce qui fait qu'il est placé dans le cache d'optimisation. Le processeur détecte automatiquement quand une fonction pour laquelle le code compilé équivalent est disponible. Pour cela, l'unité de chargement contient une table de correspondance entre l'adresse de la fonction ARM, et l'adresse de son équivalent compilé. Quand une fonction est appelée, l'unité de chargement regarde l'adresse de destination du branchement. Si l'adresse est dans cette table, elle récupère l'adresse de la fonction compilée et branche vers celle-ci. Le processeur dispose d'un cache d'instruction de 128 kibioctets, ce qui est très important, mais nécessaire vu la taille des instructions VLIW. Le coeur VLIW dispose de ses propres décodeurs, d'un ''scoreboard'', de 7 unités de calcul et de registres. Pour les unités de calcul, il a deux FPU, deux ALU, une unité de branchement, et deux unités LOAD-STORE faisant aussi ALU. Un point important est que le CPU dispose de 64 registres entiers et de 64 registres flottants. C'est deux fois plus que supporte le jeu d'instruction ARM. En mode ARM, seule la moitié des registres est utilisée. Mais en mode optimisé, le traducteur binaire utilise bien les 64 registres, grâce à une sorte de renommage de registres logicielle. Pour simplifier la traduction binaire, le processeur supporte les techniques vues précédemment. Le processeur intègre un ''gated store buffer'' similaire à celui des processeurs Transmetta. Les techniques d'exceptions différées sont aussi supportées, comme sur les processeurs Transmetta. Il a aussi une unité de préchargement décrite par NVIDIA comme agressive, avec support du préchargement de type anticipé (''runahead''). ===L'émulation de la mémoire virtuelle=== Pour finir, il faut parler de la mémoire virtuelle avec la traduction binaire. Sur le processeur source, le processeur gère à la fois des adresses physiques et virtuelles et sa MMU fait la conversion entre les deux. Sur le processeur VLIW, la MMU est simulée par le programme de traduction binaire, partiellement ou totalement. L'implémentation exacte varie suivant que l'on parle du projet Daisy, des processeurs Transmetta ou de Denver. Sur les processeurs Transmetta, il n'y a pas de MMU ni de mémoire virtuelle. A la place, le programme de traduction binaire émule la mémoire virtuelle du processeur source. Le processeur VLIW ne gère que des adresses physiques, rien d'autre. L'espace d'adressage physique du processeur VLIW a la même taille que celui du processeur émulé, ici des processeurs x86. Il est vraisemblable que les adresses physiques utilisées par le processeur x86 sont les mêmes que celles du processeur VLIW. Sur le projet Daisy, le processeur VLIW gère la mémoire virtuelle via pagination, comme les processeurs émulés. Ce qui fait qu'il gère un espace d'adressage virtuel et un espace d'adressage physique. Pour éviter toute confusion, nous parlerons d'adresse physique/virtuelle VLIW pour les adresses physiques/virtuelles du processeur VLIW, d'adresse virtuelle/physique source pour celle du jeu d'instruction traduit, à savoir du Power PC ou du s390, éventuellement de l'x86. Il faut alors faire le lien entre adresses physiques source et adresses virtuelles VLIW. Pour cela, rien de plus simple : il y a correspondance parfaite. L'adresse physique source numéro N correspond à l'adresse virtuelle VLIW numéro N. Cependant, cela signifie que tout l'espace d'adressage virtuel VLIW serait utilisé par le code à traduire. En réalité, il faut ajouter de la place pour le code traduit et le programme de traduction binaire. La conséquence est que l'espace d'adressage virtuel VLIW est plus large que l'espace d'adressage source. L'espace d'adressage virtuel VLIW est découpé en trois sections : une section pour le code Power PC à traduire, une autre pour le code traduit en VLIW, et une dernière réservée au traducteur binaire. Le programme de traduction est placé dans une ROM mappée en mémoire dans la seconde section. Le reste de la seconde section est réservé à une mémoire RAM utilisée par le programme de traduction. [[File:Mémoire virtuelle sur le projet Daisy.png|centre|vignette|upright=1.5|Mémoire virtuelle sur le projet Daisy]] Il faut noter que chaque page physique source, une fois traduite, correspond à N pages virtuelles VLIW. La raison est que le code traduit prend plus de place que le code machine originel. En conséquence, on doit utiliser une page finale plus grande. Pour le reste, des pages contiguës en mémoire physique source sont elles aussi contiguës en mémoire virtuelle VLIW. Le calcul d'adresse est donc simple : l'adresse physique source est multipliée par N, puis on ajoute l'adresse de base à laquelle commence la section pour le code traduit. ==Les processeurs à jeu d'instruction reconfigurable== Nous venons de voir comment accélérer en matériel la traduction binaire. Mais il existe une alternative qui permet de se passer de traduction binaire, complétement. L'idée est d'avoir un processeur avec un '''jeu d'instruction reconfigurable'''. Le nom est assez parlent : on peut configurer le processeur de manière à supporter le jeu d'instruction de son choix ! Par exemple, on pouvait reconfigurer un CPU de manière à ce qu'il supporte le jeu d'instruction x86, ou qu'il exécute du ''bytecode'' Java, ou du code ARMv8, etc. De tels processeurs sont encore plus rare que ceux vu avant dans ce chapitre, mais ils ont existé ! Les processeurs de ce type avaient un ''microcode réinscriptible'', à savoir que c'était des processeurs microcodés pour lesquels on pouvait modifier le microcode. L'idée était qu'en changeant le microcode, on changeait le jeu d'instruction du processeur. Pour cela, le microcode était associé à une mémoire SRAM dans laquelle le microcode était copié au démarrage du CPU. Il y avait donc une micro-ROM et une micro-RAM. Les instructions étaient décodées en utilisant le microcode dans la micro-SRAM, la micro-ROM ne servait qu'au démarrage. En modifiant le contenu de la micro-SRAM, on pouvait altérer le microcode, voire le remplacer totalement. ===Le Burroughs B1700 : architecture externe=== Un exemple est celui du Burroughs B1700, et il est intéressant d'étudier en détail son architecture. Son architecture est décrite dans le livre "Interpreting Machines : Architecture and Programming of the Bl700/Bl800 Series", écrit par Elliott I. Organick et James A. Hinds. Une partie de ce qui vfa suivre est un très court résumé de ce livre. Il disposait de 4 registres généraux, nommés X, Y, T et L, tous de 24 bits. Les registres T et L étaient découpés en 6 sous-registres de 4 bits, nommés LA LB LC LD LE et LF pour le registre L, et TA TB TC TD TE et TF pour le registre T. Ils étaient adressables comme n'importe quel registre. Un point important est que le registre T avait des possibilités d’extraction spécifiques. Il était possible d'extrait N bits, placés n'importe où dans ce registre, et de les copier dans un autre registre. Nous verrons pourquoi cette possibilité est intéressante et ne sert que pour le registre T. Les registres X et Y étaient reliés à une ALU de 24 bits capable d'effectuer des opérations arithmétiques basiques. Fait étonnant, l'ALU faisait tous les calculs en même temps et fournissait ses résultats dans 7 registres : un registre SUM pour l'addition, un registre DIFFERENCE pour la soustraction, et un registre par opération bit à bit. Les registres pour les opérations bit à bit sont : CMPX et CMPY pour l'opération NOT sur chaque opérande, XANY pour le ET bit à bit, XEOY pour le XOR, XORY pour le OU logique. Les additions et soustractions pouvaient se faire sur des opérandes codés en binaire ou en BCD, suivant le code opération utilisé. A cela, il fallait ajouter deux registres de "résultat" MSKX et MSKY. Ils fournissaient un masque dépendant de la taille du résultat en bits. Cela permettait de gérer des données de 8 ou 16 bits, alors que l'ALU faisait 24 bits. Et cela n'était pas limité à 16 ou 8 bits, mais n'importe quelle taille inférieure ou égale à 24 bits était supportée, même des tailles non-conventionnelles comme des résultats codés sur 3 bits, 9 bits, 12 bits, etc. L'ALU fournissait 12 conditions, qui alimentait un registre d'état. Le registre d'état était segmenté en trois registres appelés XYST, XYCN et BICN. Le processeur contenait de nombreux autres registres de 4 bits, dont les sous-registres de T et L. En tout, il y a 27 registres de 4 bits adressables ! Et le processeur disposait d'une ALU 4 bits pour manipuler ces registres de 4 bits ! Elle est capable de faire des MOV entre registres de 4 bits, des opérations bit à bit, et peut aussi tester si un bit vaut 0 ou 1. La dernière possibilité est très utile pour implémenter les branchements. Pour communiquer avec les entrées-sorties, il disposait de deux registres CMND et DATA : CMND pour envoyer une commande à une entrée-sortie, DATA pour lire ou écrire une donnée. Pour la mémoire, il avait deux registres séparés : READ et WRITE. READ contient la donnée lue lors d'une lecture,; WRITE est pour une donnée à écrire lors d’une écriture. Il s'agit de registres d’interfaçage mémoire, qui ont été promus au rang de registres architecturaux. Les deux registres sont gérés par le microcode, qui est le langage principal de la machine. Les instructions machines configurées font sans, mais le microcode doit y avoir accès facilement. Et vu que le microcode est le langage principal de ce processeur, ces deux registres deviennent architecturaux. ===Une architecture bit-adressable=== Si avoir un microcode réinscriptible est suffisant, quelques choix de ''design'' facilitent l'émulation de jeux d'instructions variés. Par exemple, le CPU Burroughs B1700 avait ajouté une fonctionnalité très importante : il était bit-adressable. En clair, il pouvait adresser la mémoire bit par bit, si nécessaire. L'avantage est que cela permettait d'émuler des architectures dont la taille des registres/''byte''/mots était très différente. Un autre avantage est que cela facilitait la gestion des instructions de taille variable. Détaillons rapidement le premier avantage. Le processeur était un CPU 24 bits, mais il pouvait émuler des processeurs 16 bits, 8 bits, 4 bits, 1 bit, 9 bits, ou toute autre valeur. Pour cela, le processeur lisait des mots de 24 bits et utilisant un ''barrel shifter'' pour sélectionner les bits adéquats. Par exemple, pour un accès de 16 bits, la donnée lue en mémoire était masquée de manière à ne garder que les 16 bits de poids faible. Le ''barrel shifter''/circuit de masquage était placé directement avant le bus mémoire, sur le trajet des données/instructions. Le ''barrel shifter'' était commandé par un registre ''Field Unit'', qui précisait quelle était la taille des données à charger. ===La gestion de la taille des instructions=== Les instructions à décoder n'ont pas de longueur fixe, à savoir qu'elles peuvent faire 16 bit avec tel jeu d'instruction, 8 bits avec un autre, 32 bits sur un autre, etc. Elles peuvent aussi être de longueur fixe ou variable ! Le B1700 intégrait donc un support des instructions de longueur variable, qui était "détourné" pour gérer des jeux d'instruction différents. Notons que vu que l'architecture est bit-adressable, les contraintes d'alignement des instructions disparaissent, ce qui a des avantages qu'on va détailler dans ce qui suit ! L'implémentation des instructions de taille variable ajoutait un registre avant le registre d'instruction. Le registre d'instruction proprement dit était un registre M de 24 bits. Il était précédé par un registre T, dans lequel l'instruction était assemblée, en combinant plusieurs morceaux. Si l'instruction est codée est codée sur moins de 24 bits, le registre T se contente de masquer les bits en trop. Si l'instruction chargée dépasser 24 bits, le chargement se faisait en plusieurs passes, avec plusieurs lectures en mémoire. Dans ce qui suit, on suppose que l'instruction est composée de '''champs''' : l'opcode, les opérandes, les adresses absolues, les constantes immédiates, les numéros de registre, etc. L'idée est que l'instruction est chargée dans le registre T, puis découpée en champs, qui sont copiés dans les registres du processeur ou envoyés au microcode. Je rappelle que le registre T est relié à un ''barrel shifter'', différent au précédent, qui permettait de masquer, décaler et extraire une donnée de ce registre. Et il y a la possibilité d'extraire une suite de bit de ce registre T, pour la copier dans un autre registre. Par exemple, prenons une instruction composée d'un opcode, d'une constante immédiate et d'une adresse immédiate. L'opcode est lu et interprété par le microcode. Le microcode extrait la constante immédiate du registre T et la copie dans le registre X. Ensuite, l'adresse est extraite et envoyée au registre FA, une lecture est démarrée, et l'opérande est copiée dans le registre Y. Enfin, l'instruction est exécutée par le microcode. Le tout prend plusieurs micro-opérations : une première pour lire l'opcode, une seconde pour lire la première opérande, une troisième pour lire la seconde opérande, una quatrième pour tout exécuter. Notons que suivant la taille des champs, on peut en charger plusieurs à la fois. Par exemple, imaginons qu'on ait chargé 24 bits, contenant un opcode de 8 bits et une constante immédiate de 16 bits : pas besoin de faire deux accès mémoire, le registre T est juste découpé en un opcode et une constante. Le registre T est fait pour ce genre d'extraction. ===Le chargement d'un champ=== L'architecture est bit-adressable car elle gére des champs qui ne font pas forcément 8 ou 16 bits. Par exemple, certains jeux d'instructions ont des opcodes codés sur 6 ou 7 bits, voire 9-10 bits. Par exemple, prenons un opcode de 7 bits, suivi par une adresse de 12 bits et une constante immédiate de 15 bits : aucun de ces champs ne sera aligné sur 8/16/24 bits. Utiliser une architecture bit-adressable règle le problème d'alignement. Et en général, les contraintes d'alignement des instructions disparaissent ! Quand on charge une instruction, peut importe qu'elle soit ou non alignée sur 24 bits, elle sera placée dans les bits de poids faible, il suffira de masquer les bits de poids fort inutiles. Par exemple, si on charge une instruction de 16 bits, les 16 bits seront copiés dans les 16 bits de poids faible du registre T, il suffira de masquer l'octet de poids fort. Pas besoin de faire de décalages pour gérer des instructions mal alignées... Pour charger une instruction, il faut connaitre plusieurs informations : l'adresse de son premier bit, sa taille. Pour cela, le B1700 contient deux registres, nommés ''Field Adress'' (FA) et ''Field Length'' (FL), qui indiquent l'adresse du premier bit et la taille de l'instruction à charger. Le premier fait 24 bits, ce qui permet de gérer des adresses de 24 bits. N'oubliez pas que le processeur est bit-adressable, ce qui permet d'adresser 16 Mébi-bits, soit 2 mébioctets. Le registre pour la taille fait lui 16 bits, ce qui permet de gérer des instructions de 65536 bits, soit 8192 octets ! Leur nom commence par ''field'', car le processeur peut gérer des instructions de taille variable et/ou plus longues que 24 bits. Dans ce cas, le chargement de l'instruction se fait champ par champ. Pour donner un exemple, prenons une instruction composée de 20 champs de 12 bits chacun. Les champs sont chargés un par un dans le registre T. Le registre FA est alors initialisé avec l'adresse de l'instruction, il est incrémenté de 12 à chaque cycle. Le registre FL est initialisé avec la taille de l'instruction, soit 20 * 12 = 240 bits, et il est décrémenté de 16 à chaque cycle. Précisons cependant que la taille d'un champ peut être changée d'un cycle à l'autre. Par exemple, le processeur peut charger un opcode de 8 bits, l'interpréter, détecter que l'instruction demande ensuite de charger une adresse de 16 bits. Dans ce cas, le registre FL est immédiatement altéré pour charger 16 bits, au lieu des 8 bits de l'opcode immédiatement précédent. ===Le microcode réinscriptible=== Le microcode utilise des micro-opérations de 16 bits de long, ce qui est très peu et bien en-dessous des micro-opérations des autres processeurs. Les micro-opérations sortant du microcode sont envoyées au registre M, un registre de micro-opération de 16 bits. les micro-opérations écrites dans ce registre sont ensuite décodées par le hardware et exécutées. Fait intéressant, il était possible de faire un OU logique entre la micro-opération sortante et un registre du processeur, le résultat étant mémorisé dans le registre M. Cela permettait de faire du code automodifiant dans le microcode ! Utiliser du code automodifiant avec un microcode réinscriptible est parfaitement possible, et c'est utile pour émuler des modes d'adressage complexe dans le microcode. Cela permettait par exemple de remplacer une adresse dans une micro-opération. L'adresse est alors surimposée avec un OU dans le champ d'adresse, alors que le reste de l'instruction n'est pas modifié. Mais modifier directement le microcode est rarement une bonne idée. Aussi, pour éviter cela, l'idée est de modifier les instructions avec un OU logique avec un autre registre. Les micro-opérations du microcode ne sont donc pas modifiées dans le microcode, mais en sortie du microcode. La RAM mémorisant le microcode était de petite taille, et il arrivait qu'elle soit trop petit pour mémoriser le microcode complet. Dans ce cas, le programmeur devait gérer la situation en découpant le microcode en segments, certains étant placé dans le processeur, les autres étant en mémoire RAM. Le processeur peut lire le microcode placé en mémoire RAM et même l'exécuter, mais avec une pénalité en termes de performances. Quelques registres intégrés au processeur permettent ce genre de magie. Le microcode pouvait servir pour émuler un autre jeu d'instruction, mais il était aussi possible d'implémenter un véritable interpréteur, grâce aux techniques du paragraphe précédent. Le processeur était même conçu pour ! L'interpréteur était placé dans un "segment" en mémoire RAM, dont le processeur connaissait la position. Il incorporait pour cela deux registres, un pour l'adresse de base de l'interpréteur, un autre pour son adresse de fin. Il incorporait une protection mémoire limitée sur ce segment, à savoir que seul l'interpréteur pouvait lire ou écrire dans ce segment. Afin de gérer les interruptions, ces dernières étaient partiellement détectées en logiciel. Les interruptions ne s'exécutaient pas automatiquement, pour émuler correctement les interruptions. Une interruption doit être prise en compte à la toute fin de l'exécution d'une instruction, quand celle-ci a finit son travail. Mais vu qu'ici, les instructions sont émulées avec des séries de micro-opérations, il y a un problème : il est possible d'interrompre une instruction émulée en plein milieu. Pour éviter cela, le processeur ne déclenche pas les interruptions matérielles immédiatement, elles sont mises en attente. Une micro-opération dédiée vérifie si une interruption est en attente, et l'exécute alors cas échéant. La micro-opération est utilisée comme micro-opération finale d'une instruction émulée. ==Les jeux d'instructions dédiés à un langage de programmation== De rares processeurs sont conçus pour un langage de programmation en particulier. On appelle ces processeurs, conçus pour des besoins particuliers, des '''processeurs dédiés'''. Par exemple, les fameux ''Burrough E-mode'' B5000/B6000/B7000 étaient spécialement conçus pour exécuter de l'ALGOL-60. Leurs cousins B2000/B3000/B4000 étaient eux conçus pour le COBOL. Des langages fonctionnels ont aussi eu droit à leurs processeurs dédiés. Le prolog en est un bel exemple, avec les superordinateurs de 5ème génération qui lui étaient dédié. On peut aussi citer les machines LISP, dédiés au langage LISP, qui datent des années 1970. Elles étaient capables d’exécuter certaines fonctions de base du langage directement dans leurs circuits : elles possédaient notamment un ''garbage collector'' câblé dans ses circuits ainsi que des instructions machines supportant un typage déterminé à l’exécution. Les processeurs dédiés ont eu leur heure de gloire au début de l'informatique, à une époque où les langages de haut niveau venaient d'être inventés. À cette époque, les compilateurs n'étaient pas performants et ne savaient pas bien optimiser le code machine. Il était alors rationnel, pour l'époque, de rapprocher le code machine cible et le langage de programmation de haut niveau. De nombreuses architectures dédiés ont ainsi été inventées, avant que les concepteurs se rendent compte des défauts de cette approche. Les défauts en question ne sont pas nombreux, mais assez simples à comprendre. Premièrement, elles sont très rapides pour un langage de programmation en particulier, mais sont assez mauvaises pour les autres, d'où un problème de "compatibilité". Ajoutons à cela que les langages de programmation peuvent évoluer, devenir de moins en moins populaires/utilisés, ce qui rend la création d'architectures généralistes plus pertinente. Enfin, les architectures dédiées sont évidemment des processeurs CISC, pour implémenter les nombreuses fonctionnalités des langages évolués. Et les défauts des CISC sont assez rédhibitoires à l'heure actuelle. À l'heure actuelle, les algorithmes des compilateurs se sont améliorés et savent nettement mieux utiliser le matériel. Ils produisent du code machine efficace, ce qui rend les architecture dédiées bien moins intéressantes. Si on ajoute les défauts de ces architectures dédiées, par étonnant que les architectures dédiées aient presque disparues. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'accélération matérielle de la virtualisation | prevText=L'accélération matérielle de la virtualisation | next=Le matériel réseau | nextText=Le matériel réseau }} </noinclude> 99aawufes24mczoxqnu9pu98jmz09ub 764026 764025 2026-04-19T18:29:34Z Mewtow 31375 /* Le Burroughs B1700 : architecture externe */ 764026 wikitext text/x-wiki De nos jours, la majorité des programmeurs programment dans des langages de haut niveau. Il est très rare pour eux d'avoir à utiliser de l'assembleur, et encore moins un langage machine. Les programmes écrits dans un langage de haut niveau sont traduits en langage machine par un logiciel appelé le compilateur. [[File:Compilation.PNG|centre|vignette|upright=2.5|Compilateur : principe.]] Il se trouve que quelques rares processeurs sont conçus pour faciliter le travail du compilateur. Et ce sont ces processeurs que nous allons voir dans ce qui suit. Ils représentent une catégorie de jeux d'instruction à part, qui n'a pas vraiment de nom. ==La compilation et l’interprétation== Un compilateur traduit donc du code source, écrit dans un langage de haut niveau, vers du langage machine. La traduction est rarement directe. En général, le compilateur traduit le code source en assembleur, qui est lui-même traduit en langage machine. L'assembleur est une représentation textuelle du langage machine alors que le code machine est du binaire exécutable par le processeur. La traduction se fait donc en deux étapes, la '''compilation''' proprement dite et l''''assemblage''', réalisées respectivement par un compilateur et un assembleur. Il faut parfois rajouter une troisième phase d'édition des liens, que nous passons volontairement sous silence. {| |[[File:Code C.png|vignette|200px|Le code source, ici en C.]] |[[File:Assem.png|vignette|250px|Le code assembleur.]] |[[File:Binary file - hello world (C programming).png|vignette|150px|Le code machine.]] |} ===L'interprétation et la compilation à la volée=== Une alternative à la compilation est l''''interprétation''', qui transforme le code source en code machine à la volée. Avec l'interprétation, le code source est passé à un logiciel appelé l''''interpréteur''', qui exécute le code source ligne de code par ligne de code. Une ligne de code est traduite en code machine, qui est exécutée, puis l'interpréteur passe à la ligne suivante. [[File:Kaantotulkkaus.png|centre|vignette|upright=3|Compilation versus interprétation.]] Un défaut de l'interprétation est son cout en performance, qui est assez important. Aussi, l'interprétation stricte a évolué vers un hybride entre interprétation et compilation. L'idée est qu'une partie peu importante du code source est interprétée, alors que le code important est compilé juste avant d'être exécuté. Pour le dire autrement, le code source est partiellement compilé à la volée, juste avant son exécution. Aussi, on parle de '''compilation à la volée'''. Le terme en anglais est ''Just In Time compilation'', abrévié en JIT. En soi, le JIT est associé à de l'interprétation, les JIT sont en réalité des hybrides interpréteurs-compilateurs. Un point important est que compiler du code à la volée est assez lourd, cela a un cout en performance. Par contre, le code compilé s'exécute plus vite que du code interprété. Si on exécute le code compilé une seule fois, le cout de la compilation l'emporte sur le gain à l'exécution. Mais le code compilé est réutilisé autant de fois que nécessaire. Le gain à l'exécution est donc multiplié par le nombre d'exécutions, alors que le cout de la compilation est répartit sur plusieurs exécutions, il est amorti. Les portions du code sont choisies de manière à avoir un gain en performance maximal. L'idée générale est que le code exécuté très souvent est compilé, alors que du code exécuté pas souvent est interprété. Typiquement, les boucles les plus souvent exécutées sont compilées, le code exécuté une seule fois est interprété. ===Les langages intermédiaires=== Les compilateurs modernes passent par un '''langage intermédiaire''' pour faire la transformation en code machine. Le compilateur traduit le langage de haut niveau en langage intermédiaire, puis traduit le langage intermédiaire en code machine cible. Le langage intermédiaire est parfois appelé le '''''bytecode''''', et ce terme recouvre aussi le code écrit avec du ''bytecode''. L'interprétation aussi peut passer par l'intermédiaire d'un un ''bytecode'', ce n'est pas limité aux compilateurs proprement dit. {| |[[File:Intermediate representation scheme.png|centre|vignette|upright=1.5|Représentation intermédiaire d'un compilateur.]] |[[File:Code interpreter scheme.png|centre|vignette|upright=1.5|Fonctionnement d'un interpréteur.]] |} Faire ainsi a de nombreux avantages, le principal étant d'avoir un compilateur capable de traduire un langage de haut niveau vers plusieurs jeux d'instructions différents. Cela permet d'avoir, par exemple, un compilateur qui traduit du C soit en code machine x86, soit en code machine pour un CPU ARM, soit pour un CPU POWERPC, etc. [[File:Compiler design.svg|centre|vignette|upright=3|Compiler design]] Pour l'interprétation, l'usage d'un ''bytecode'' a d'autres avantages. Pour l'exploiter, il faut distribuer non pas le code source, mais le ''bytecode''. En clair, l'interpréteur ne prend pas en entrée le code source, mais du ''bytecode'' déjà compilé. La traduction est alors beaucoup plus simple, car le code source a déjà été partiellement compilé, par la traduction en ''bytecode''. Interpréter du code source est en effet assez compliqué : il faut effectuer des étapes d'analyse lexicale, sémantique, et bien d'autres. Avec du ''bytecode'', ces étapes ont été réalisées lors de la compilation du ''bytecode'', l'interpréteur a alors peut de choses à faire. L'interprétation du ''bytecode'' se fait instruction par instruction, au niveau du ''bytecode''. Une instruction du ''bytecode'' est traduite en une instruction machine équivalente, qui est exécutée. Si l'instruction est un peu complexe, il exécute une fonction/procédure qui fait la même chose. Pour résumer, le ''bytecode'' est ensuite traduit à la volée et exécuté instruction par instruction par un logiciel appelé l'interpréteur. [[File:Control table.png|centre|vignette|upright=2|Control table]] Le langage intermédiaire peut être vu comme l'assembleur d'un processeur, n'existe pas forcément dans la réalité, mais dont le jeu d'instruction est décrit en détail. Le processeur en question est appelé une '''machine abstraite''', ou encore une ''machine virtuelle''. Nous utiliserons le terme de machine abstraite dans ce qui suit. La machine abstraite n'est pas la même suivant que l'on cible la compilation ou l'interprétation/JIT. Pour l'interprétation/JIT, la machine abstraite est souvent une machine à pile, car cela simplifie grandement la traduction en code machine final. L'interprétation demande que la traduction du ''bytecode'' en code machine soit la plus simple possible. Et cela demande de prendre en compte pas mal de détails. Par exemple, les différents jeux d'instruction existants n'ont pas le même nombre de registres, ce qui pose problème lors de la traduction du ''bytecode''. Lors de la transformation en code machine, un algorithme d'allocation de registres se débrouille pour traduire le code intermédiaire en code qui utilise un nombre limité de registres. Pour améliorer cette allocation de registres, il y a deux solutions niveau ''bytecode'' : soit utiliser un nombre illimité de registres, soit utiliser une machine à pile. Pour les machines à pile, il existe un algorithme simple et rapide pour traduire un code écrit pour une machine à pile en un code écrit pour un processeur avec des registres, qui se débrouille pas trop mal pour allouer efficacement les registres. C'est un avantage assez important pour les langages interprétés. ==Les processeurs qui exécutent du bytecode== Le ''bytecode'' est un code machine, ce qui signifie qu'il peut en théorie s'exécuter sur un processeur qui implémente le jeu d’instruction associé. Si le jeu d'instruction d'un ''bytecode'' est souvent une description censée être fictive, elle n'en reste pas moins un jeu d'instruction et des caractéristiques précises, qu'on peut l'implémenter en matériel ! Néanmoins, tous les ''bytecode'' ne sont pas égaux de ce point de vue. Certains sont faciles à implémenter en matériel, d'autres non. Les ''bytecodes'' implémentés en matériel sont ceux dont les machines abstraites sont des machines à pile. Un premier exemple est celui des processeurs Pascal MicroEngine, qui exécutaient directement le bytecode du langage Pascal, le fameux UCSD P-code. Un second exemple est la '''machine SECD''', qui sert de langage intermédiaire pour certains compilateurs de langages fonctionnels. Elle a été implémentée en matériel par plusieurs équipes, notamment par les chercheurs de l'université de Calgary en 1989. Dans le même genre, quelques processeurs simples étaient capables d’exécuter directement le ''bytecode'' du langage FORTH. Le FORTH, un des premiers langages à pile de haut niveau, possède de nombreuses implémentations hardware et est un des rares langages de haut niveau dont le ''bytecode'' a été utilisé comme langage machine sur certains processeurs. Par exemple, on peut citer le processeur FC16, capable d’exécuter nativement du ''bytecode'' FORTH. ===Les processeurs Java=== Le cas le plus impressionnant est celui de la machine virtuelle Java, qui est un design de processeur comme un autre. En temps normal, le ''bytecode'' Java est compilé ou interprété, mais certains processeurs exécutaient du ''bytecode'' Java directement, sans interprétation ni compilation. Ils sont appelés des '''processeurs Java'''. On peut citer les processeurs ARM disposant de l'extension Jazelle, du JEMCore et du aJ-100 de aJile Systems, le picoJava II de Sun, et quelques autres. Il y a aussi eu quelques projets de recherche ou processeurs open source, comme Komodo, jamuth, le ''Java Optimized Processor'' et quelques autres projets du même genre. L'implémentation de la machine virtuelle Java n'était cependant pas complète. Quelques instructions complexes n'étaient pas gérées par le processeur et devaient être émulées en logiciel. Mais la grosse majorité des instructions l'était. Les instructions non-supportées étaient émulées avec la méthode ''trap and emulate'', à savoir qu'un opcode inconnu déclenchait une exception matérielle ''opcode invalide'', dont la routine pouvait émuler les instructions qui doivent l'être. Le processeur aJ-100 de aJile est capable d'exécuter la quasi-totalité des instructions de la JVM Java, seules deux instructions faisant exception. Les '''accélérateurs Java''' sont une solution intermédiaire, qui permet de traduire à la volée du ''bytecode'' Java en instructions RISC, exécutées sur un autre processeur. Il s'agit formellement de traduction binaire, mais je préfère en parler ici. L'idée est de combiner un processeur RISC avec un coprocesseur Java. Le coprocesseur lit le ''bytecode'' Java et le traduit en instructions RISC, ces dernières étant exécutées sur le processeur RISC. Le JA108 de Nozomi était un coprocesseur de ce genre, au même titre que le coprocesseur JSTAR de JEDI. L'extension Jazelle de certains CPU ARM fonctionnait vraisemblablement sur un principe similaire, sauf que le coprocesseur était intégré dans le processeur ARM, entre le cache d'instruction et le décodeur d'instructions ARM. L'intérêt des processeurs Java n'est pas qu'une question de performance. Certes, ils permettent d’exécuter plus vite les programmes compilés en ''bytecode'', comme des programmes Java pour la JVM Java. Mais l'intérêt est aussi de faire des économies de mémoire RAM et ROM. Je rappelle que la machine virtuelle Java est une machine à pile, ce qui fait que sa densité de code est excellente. Les programmes en ''bytecode'' Java sont donc très petits, et prennent peu de place en mémoire ROM. De plus, utiliser un processeur Java permet de se passer d'interpréteur ou de JIT Java, ce qui économise encore plus de mémoire. ===Transformer un processeur RISC en machine à pile=== La plupart des processeurs Java utilisent en interne une architecture à registre généraux, qui émule une machine à registre en microcode. Quelques registres sont réservés pour l'état de la machine virtuelle, avec au minimum un registre réservé pour le pointeur de pile, un autre pour un pointeur vers un ''pool'' de constantes, et quelques autres. D'autres registres servent à mémoriser le haut de la pile, à savoir le sommet de la pile et quelques opérandes situées juste en-dessous. Par exemple, le processeur picoJava II gardait les 64 opérandes au sommet de la pile dans 64 registres généraux, séparés des registres pour les pointeurs et l'état de la JVM. Une optimisation possible, envisagée sur les processeurs picoJava, était une sorte de macro-fusion sous stéroïde. L'idée était de fusionner certaines séries d'instructions Java en une seule instruction machine. Typiquement, pour faire une opération arithmétique, une machine à pile empile deux opérandes et exécute une opération, ce qui prend trois instructions machine. La spécification picoJava 1 a proposé d'envoyer directement la seconde opérande en entrée de l'ALU, ce qui fusionnait l'empilement de la seconde opérande et l'opération arithmétique. Le processeur picoJava II va plus loin et fusionne les trois instructions en une seule instruction RISC, agissant sur des registres. Mais la majorité des processeurs Java ne faisaient pas cela, car cela complexifie grandement le décodeur. Les processeurs ''bigfoot'' de DCT utilisaient un système différent pour émuler une pile avec des registres. Leur banc de registre de 64 bits était coupé en deux : les 16 premiers registres étaient utilisés pour la pile, les 48 suivants étaient utilisés pour autre chose. L'idée est que le registre 0 mémorise un emplacement vide, le registre 1 contient le sommet de la pile, le registre 2 l'opérande sous le sommet de la pile, et ainsi de suite. Les 15 premières opérandes de la pile sont donc mémorisées dans les registres. Les instructions écrivent leur résultat dans le registre adéquat, puis tous les registres sont décalés pour que le résultat passe dans le registre R1. Par exemple, les instructions PUSH chargent une opérande dans le registre 0, puis tous les registres sont décalés de un rang, pour que l'opérande chargée soit dans le registre 1. Le registre 0 devient alors le registre numéro 1, le registre 1 devient le 2, et ainsi de suite. Par contre, les opérations arithmétiques font autrement. Une instruction d'addition, par exemple, lit les opérandes dans le registre 1 et 2, puis place son résultat dans le registre 2 (les deux opérandes sont dépilées). Puis, les registres sont décalés d'un rang pour que ce registre 2 devienne le 1. Les registres ne sont en réalité pas décalés, il n'y a pas de transferts entre registres. A la place, les 16 premiers registres utilisaient une forme limitée de renommage de registres, similaire au banc de registre tournant utilisé sur les processeurs EPIC comme l'Itanium. Les numéros de registres sont renommés, décalés, à chaque fois qu'on empile ou dépile une opérande. Pour faire le renommage de registres, processeur contient un ''register stack counter'' de 4 bits, qui est incrémenté à chaque fois qu'on empile une donnée, et est décrémenté quand on dépile une donnée. Quand on veut accéder à un opérande, le numéro de registre architectural associé est traduit en numéro de registre physique, en additionnant le ''register stack counter'' (modulo 16, pour rester sur 4 bits). ==L'accélération de l'interprétation/JIT : le ''Thumb-EE'' d'ARM== Quelques processeurs ont ajouté des instructions pour faciliter le travail des interpréteurs/JIT. Par exemple, en 2005, ARM a ajouté le mode ''Thumb Execution Environment'', qui faisait cela. Il reprenait le jeu d'instruction compact ''thumb'' et ajoutait quelques instructions et modifiait le comportement d'instructions existantes. Le jeu d'instruction ''Thumb-EE'' a été ajouté sur les CPU ARM en 2005, mais a été déprécié en 2011, par manque d'utilité. Mais la tentative mérite qu'on s'attarde dessus. Les CPU de gamme M, déstinés à l'embarqué, n'ont jamais supporté ''Thumb-EE'', vu que de tels processeurs ne sont pas conçus pour exécuter du code interprété/JIT. Le mode ''Thumb-EE'' est un mode d'exécution, séparé de l'ARM normal. Ainsi, une instruction ''Thumb-EE'' et une instruction ARM peuvent avoir le même encodage en binaire, mais se comporter différemment. Le processeur est à tout moment soit en mode ''Thumb-EE'', soit en mode ARM normal, ce qui précise comment ces instructions doivent se comporter. Des instructions ont été ajoutées par ''Thumb-EE'', pour qu'il fasse son travail. L'entrée et la sortie du mode ''Thumb-EE'' se fait avec deux instructions : ENTERX et LEAVEX. Une fois en mode ''Thumb-EE'', le décodage des instructions ''thumb'' se fait avec les règles du ''Thumb-EE'', et non celles du ''thumb'' normal. Une première différence entre ''thumb'' normal et ''Thumb-EE'' est l'ajout de ce qui s'appelle un ''null check'' pour les instructions mémoire. Le ''null check'' est utilisé pour les instructions en mode d'adressage "base + indice" ou "Base + décalage", ou tout autre mode d'adressage avec un registre de base utilisés dans des calculs d'adresse. L'idée est de vérifier si l'adresse de base vaut zéro ou non. Si c'est le cas, le processeur lève une exception matérielle, qui est traitée par l'interprétation ou le compilateur JIT. Une telle situation est en effet signe d'une erreur d'adressage mémoire, qui doit être traitée par l'interpréteur. Typiquement, c'est signe qu'un pointeur n'a pas bien été initialisé, ce qui peut arriver avec du code interprété ou JIT. Les interpréteurs/JIT traitent généralement la situation en ajoutant un test avant toute instruction mémoire, pour vérifier si le pointeur accédé vaut zéro ou non. Avec ''Thumb-EE'', pas besoin d'ajouter les tests en questions : ils sont réalisés automatiquement lors de chaque instruction mémoire. : Les registres pour la pile, comme le pointeur de pile et le pointeur de ''frame'', sont aussi concernés. L'instruction CHKA vérifie que les accès à un tableau ne débordent pas en dehors du tableau en question. Par exemple, pour un tableau de 1024 éléments, elle vérifie si l'indice est compris entre 0 et 1024. Pour cela, elle vérifie si l'indice de tableau est bien dans l'intervalle adéquat et lève une exception matérielle si ce n'est pas le cas. Son utilité se comprend quand on sait que tous les accès à un tableau sont vérifiés par l'interpréteur. L'interpréteur est censé ajouter des instructions pour vérifier les indices, à savoir deux ou trois branchements. Avec CHKA, le test se fait en une seule instruction. D'autres modifications mineures de l'encodage des instructions sont aussi présentes, ainsi que des modifications pour les instructions LOAD/STORE. Précisément, dans les modes "base + indice" et "base + décalage", le décalage et l'indice subissent maintenant des décalages. L'idée est de simplifier le calcul d'adresse. De quoi économiser quelques instructions lors des calculs d'adresse, ce qui facilite le travail de l'interpréteur/JIT. Et des instructions LOAD/STORE précises voient leur comportement modifié pour faciliter la gestion de la pile de Java ou l'exécution des méthodes locales. Pour faciliter le travail de l'interpréteur, deux instructions HBP et HBLP ont été ajoutées. Elles branchent vers une fonction qui gère les exceptions logicielles (une fonctionnalité présente dans de nombreux langages, comme Java). La première effectue un branchement vers la fonction en question, la seconde sauvegarde l'adresse de retour avant de faire ce branchement. Les deux peuvent brancher vers 256 fonctions pré-déterminées, sans avoir à présenter leur adresse. ==La traduction binaire accélérée par le matériel== Plus haut, nous avons surtout parlé des compilateurs et des interpréteurs. Cependant, nous devons aussi parler de la '''traduction binaire'''. Elle traduit un programme écrit dans un code machine vers un autre code machine. Par exemple, elle traduit un programme compilé pour un CPU x86 vers un code machine ARM. En général, le système d'exploitation est généralement compilé pour le jeu d'instruction natif, mais il exécute des applications prévues pour le x86. Les applications sont traduites par le système d'exploitation, avant d'exécuter le code traduit. : Nous parlerons dans la suite de code machine source et de code machine cible, pour parler respectivement du code à traduire et du code obtenu après traduction. Idem avec d'autres termes comme architecture cible/source, ou jeu d'instruction cible/source. La traduction binaire est surtout utilisée pour des questions d'émulation ou de compatibilité. Par exemple, lorsque les Macintosh sont passés de processeurs Power PC vers des processeurs x86, le système d'exploitation Mac OS utilisait la traduction binaire pour convertir les anciennes applications Power PC vers du code x86. Les utilisateurs n'y ont vu que du feu. Le système de traduction binaire était appelé Rosetta 1. Par la suite, lors de la transition de processeurs x86 vers des processeurs Apple, Rosetta 2 a vu le jour. La traduction binaire a surtout été utilisée pour traduire du code x86 vers un autre jeu d'instruction. Il faut dire que le x86 est le jeu d'instruction dominant. De nombreuses entreprises ont eu pour ambition de briser l'hégémonie du x86 sur PC, en remplaçant le x86 par un jeu d'instruction plus performant, tout en gardant une compatibilité maximale. La traduction binaire était la seule solution pratique. Elle était le plus souvent intégralement réalisée en logiciel, comme c'était le cas sur les architectures Itanium avec le ''IA-32 Execution Layer''. ===Les généralités sur la traduction binaire assistée en matériel=== L'architecture source est presque tout le temps une architecture CISC, assez ancienne, qu'on souhaite émuler. Le choix de l'architecture cible se porte souvent sur une architecture VLIW, fort différente de l'architecture source, et ce pour obtenir une économie de matériel conséquente. Pour s'exécuter rapidement, le code traduit doit exploiter le parallélisme d'instruction, à savoir exécuter plusieurs instructions en même temps dans des unités de calcul séparées. Il est possible d'utiliser un processeur superscalaire avec exécution dans le désordre pour cela, mais au prix d'un cout important en transistors. Alors qu'en utilisant un CPU VLIW, c'est le traducteur binaire qui fait tout le travail d'extraction du parallélisme d'instruction. Et après tout, quitte à avoir un traducteur binaire, autant lui refiler le boulot d'optimisation. Compiler du code à la volée est certes assez lourd, mais qu'il existe des algorithmes efficaces pour regrouper des instructions indépendantes dans une seule instruction VLIW. Au passage, le processeur VLIW a plus de registres que le processeur source. Cela permet de faire du renommage de registres directement en logiciel. Le traducteur binaire n'hésite pas à changer les noms de registres entre instructions source et instruction VLIW cible, afin de supprimer des dépendances de données. En conséquence, cela ouvre des opportunités de parallélisme, qui sont exploitées lors du regroupement des opérations en instructions VLIW. Vous pourriez penser que le choix d'un CPU VLIW pour émuler du CISC est tout sauf optimal. Vous devez penser que la traduction binaire est d'autant plus simple que l'architecture source et cibles sont semblables. Dans les faits, ce n'est pas tellement traduire les instructions qui pose problème, mais plus la gestion du registre d'état, des exceptions matérielles, de la mémoire virtuelle, la différence entre gros-boutisme et petit-boutisme, de même que des différences d'adressage pour les périphériques. Le premier problème est la gestion des conditions, notamment en présence d'un registre d'état. En pratique, la traduction binaire s'utilise pour traduire du code CISC vers du code RISC ou VLIW. Les architectures source ont donc un registre d'état, qui est mis à jour non seulement par des instructions de test, mais aussi des instructions arithmétiques. L'architecture cible n'a elle pas de registre d'état, mais des registres à prédicats. La traduction de l'un vers l'autre est alors quelque peu compliquée. Et elle est d'autant plus compliquée que le registre d'état est la source de dépendances d'instruction implicites, qui réduisent les performances. Par exemple, si une instruction arithmétique modifie le registre d'état, cela peut impacter l'exécution d'une instruction ultérieure, qui lit ce registre d'état. Pour éliminer ces fausses dépendances, le traducteur binaire doit renommer ce registre en logiciel et détecter les dépendances utiles. Et le compilateur doit gérer les cas où il y a beaucoup de distance entre les deux, voire les cas où l'instruction dépendante n'a pas encore été analysée par le compilateur. Un autre problème est lié à la gestion des exceptions matérielles, et précisément des exceptions précises. Pour rappel, une instruction VLIW regroupe plusieurs opérations, opérations qui correspondraient à une instruction machine sur un CPU pas VLIW. En conséquence, plusieurs instructions du langage machine "source" sont regroupées en une seule instruction VLIW. Et il faut tenir compte du cas où une instruction source lève une exception. Dans le code VLIW, cela signifie qu'une opération lève une exception, et il faut annuler partiellement l'instruction VLIW associée. Et par partiellement, on veut dire que seules les opérations suivantes dans l'ordre du programme source doivent être annulées, puis ré-exécutées. Et c'est un sacré casse-tête ! Les processeurs VLIW utilisent comme solution le mécanisme d'exceptions différées des processeurs EPIC, vu il y a quelques chapitres. Pour résumer, le code est exécuté par blocs d'instruction, délimités par des branchements ou tout autre limite/barrière pertinente dans le code. Un bloc de code est compilé en deux versions : une version rapide sans exceptions matérielles, une version lente qui gère les exceptions précises. La version rapide s'exécute sans exécuter les exceptions. Cependant, les exceptions matérielles sont enregistrées, pour être prises en compte à la toute fin du bloc de code. La version rapide est exécutée en premier et elle mémorise si une exception matérielle a eu lieu. Puis une instruction vérifie si une exception a eu lieu et décide quoi faire. Si aucune exception n'a eu lieu, elle passe à la suite du programme, les résultats du bloc de code sont définitivement acceptés. Mais si une exception a eu lieu, tout est annulé. Le processeur est remis dans l'état initial, puis le code est ré-exécuté instruction par instruction de manière à gérer l'exception correctement. ===La traduction par pré-décodage=== Une première solution serait de faire la traduction binaire dans le cache d'instruction. Lors d'un défaut de cache, le code chargé depuis la RAM est traduit en code machine cible. Et c'est ce code machine cible qui est mémorisé dans le cache d'instruction et exécuté par le processeur. La technique marche sur le papier et n'est qu'une amélioration des techniques de pré-décodage vues il y a quelques chapitres. Cependant, le code obtenu est une traduction assez basique, qui n'incorpore pas d'optimisations dignes de ce nom. De plus, elle gère mal le cas où la taille du code source et cible sont potentiellement très différentes. Concrètement, elle est surtout utile pour traduire du code d'un processeur RISC vers un autre processeur RISC, dont les tailles d'instructions sont similaires. Elle ne permet pas d'émuler plusieurs jeux d'instructions différents, le cout en matériel (un circuit de pré-décodage par jeu d'instruction) serait trop important. ===Le projet DAISY d'IBM=== De très rares processeurs étaient conçus pour accélérer cette traduction binaire, afin de garder de bonnes performances. Les premiers à avoir étudié l'idée étaient IBM, avec leur projet DAISY (''Dynamically Architected Instruction Set from Yorktown''). Le projet de base était de convertir à la volée du code compilé pour des CPU Power PC, vers du code VLIW. Les chercheurs d'IBM avaient développé un algorithme de traduction dynamique efficace, ainsi qu'un processeur VLIW disposant d'optimisations spécifiques à la traduction binaire dynamique, à la volée. De nombreuses idées de ce projet ont été reprises ou re-découvertes par la société Transmetta, avec ses processeurs Crusoe et Efficieon, puis par NVIDIA avec son projet Denver. Les ingénieurs de ce projet ont étudié la possibilité d'émuler plusieurs jeux d'instructions différents sur un même processeur, notamment le s390 d'IBM et le x86. En théorie, cela demande juste d'avoir plusieurs programmes de traduction binaire : un pour le x86, un autre pour le s390, éventuellement un autre pour le Power PC. Mais cela ne s'est pas concrétisé. Mais les ingénieurs ont étudié quelle pourrait être l'architecture VLIW idéale pour ça. Par exemple, il fallait des additions 3-opérandes pour simplifier les calculs d'adresse. Une difficulté était la gestion du registre d'état, dont les bits ne sont pas mis à jour de la même manière sur le x86 et le s390 ou le Power PC. La différence entre gros-boutisme et petit-boutisme était aussi un problème, de même que des différences d'adressage pour les périphériques. Pour que la traduction binaire soit efficace, le processeur VLIW intègre diverses optimisations, comme les exceptions différées et les branchements multi-voies. La principale est la suivante : seul le code des boucles ou fonctions exécutées fréquemment est traduit en code VLIW, le reste du code est interprété. En effet, traduire du code binaire prend plus de temps qu'une simple interprétation. Pour du code qui ne sera exécutée qu'une seule fois, il est plus rapide d'utiliser l'interprétation. Par contre, pour du code exécuté beaucoup de fois, le cout de la traduction binaire est amorti, dilué sur N exécutions, compensé par le gain en temps d'exécution de ce code traduit. Au final, cela permet de ne traduire que le code qui le mérite. La détection du code fréquemment exécuté est réalisée dans le cœur VLIW. L'unité de branchement mémorise les derniers branchements rencontrés et le nombre de fois qu'ils ont été exécutés. Si ils ont été exécutés un certain nombre de fois, l'unité de branchement lève une exception matérielle, qui invoque le traducteur binaire. La routine de cette exception est le traducteur binaire proprement dit. En clair, la traduction binaire est démarrée quand le processeur détecte qu'une fonction a été exécutée plus de N fois, via une exception matérielle dédiée. La mesure du nombre d'exécution d'un branchement se fait dans l'unité de calcul dédiée aux branchements, avec l'aide d'une sorte de ''branch adress buffer'' modifié. Pour rappel, le ''branch adress buffer'' mémorise le ''Program counter'' de chaque branchement récemment rencontré. Ici, chaque entrée du ''branch adress buffer'' est associée à un compteur incrémenté à chaque exécution du branchement. ===Les processeurs Crusoe et Efficieon de Transmetta=== Les processeurs Crusoe Et Efficeron sont deux processeurs VLIW produits par la société Transmetta, une société californienne rachetée par NVIDIA. Ils étaient conçus pour exécuter spécifiquement des programmes x86, système d'exploitation inclus. L'idée derrière ce projet était d'exécuter du code x86 sans que les applications, ni même le système d'exploitation et le BIOS soient au courant ! Les processeurs Transmetta étaient en réalité conçus pour exécuter un programme unique, le ''Code Morphing Software'' (CMS), qui traduisait le code x86 en code VLIW. Transmetta ne voulait pas que son processeur VLIW soit exploité directement, sans le CMS. Il n'avait pas rendu public de compilateur pour traduire du C vers du code VLIW, il n'avait pas donné la documentation du jeu d'instruction VLIW. Des efforts de rétro-ingénieurie, documentés sur le site realworldtech, ont cependant permis de comprendre comment étaient encodées les instructions du processeur Crusoe. Comme pour le projet Daisy, le ''Code Morphing Software'' utilisait à la fois interprétation et traduction binaire, selon les besoins. Les instructions sont interprétées lors de leurs premières exécutions, mais le CMS bascule sur de la traduction en code VLIW après un certain nombre d'exécution. Ainsi, les boucles souvent exécutées sont traduites en code VLIW, alors que le reste du code est interprété. Le choix du code à traduire est le fait du CMS, il utilise des heuristiques complexes pour, qui ne sont pas connues dans le détail. Le ''Code Morphing Software'' est mémorisé dans une EEPROM, ce qui en fait un ''firmware'' situé en dessous du BIOS. Le CMS démarre ensuite le BIOS, qui lui-même démarre le système d'exploitation, qui lui-même démarre les pilotes de périphériques et les programmes. Le CMS se réserve les 16 premiers mébioctets de l'espace d'adressage. L'EEPROM du CMS est mappée dedans, mais n'en utilise que la moitié. Le reste est utilisé comme cache, pour mémoriser le code VLIW traduit par le CMS. Les deux processeurs intégraient aussi deux mémoires SRAM, utilisées par le CMS, appelées la ''local program memory'' (LPM) et la ''local data memory'' (LDM). La première contient du code qui gére les interruptions, la mémoire virtuelle, les exceptions matérielles, les problèmes d'alignement mémoire, et quelques fonctions très fréquemment utilisées par le CMS. La seconde est de la mémoire RAM utilisée par ces fonctions de la ''local program memory''. Les mémoires caches L1 et L2 sont séparées de ces deux mémoires. Le processeur Crusoe et Efficieon étaient des processeurs VLIW très simples. Ils n'avaient même pas de MMU, ni de fonctionnalités importantes sur les CPU x86. Pour Crusoe, les instructions VLIW étaient encodées sur 64 ou 128 bits, et regroupaient entre 2 à 4 opérations. Les instructions LOAD/STORE ne supportaient pas d'adressage indicé, juste de l'adressage indirect à registre. Crusoe avait 5 unités de calcul : deux ALU entières, une unité LOAD/STORE, une unité de branchement, une FPU. Efficieon doublait le nombre d'ALU entière et d'unité LOAD/STORE, les instructions VLIW passaient à 256 bits. Le processeur Crusoe contient 160 registres, dont 64 registres généraux, 32 registres flottants. Les registres généraux font 32 bits, ce qui est cohérent avec le fait que les CPU x86 de l'époque étaient des processeurs 32 bits. Quant aux registres flottants, ils faisaient 80 bits, ce qui colle avec la taille des registres flottants de la FPU x87 utilisée à l'époque. Sur les 64 registres généraux, seuls 48 étaient réellement utilisable pour mémoriser des opérandes. Une partie des registres généraux étaient utilisés pour la gestion de la pile, d'autres pour mémoriser l'état du CPU x86 émulé, un registre était un registre zéro non accesible en écriture. La gestion des exceptions est optimisée avec un système d'exceptions différées, le même que celui décrit plus haut. Le processeur utilise un système similaire, pour effectuer des lectures anticipées, qui a été expliqué dans le chapitre sur les processeurs VLIW/EPIC. Sauf qu'il s'agit là d'une optimisation pour gagner en performances en général, pas une aide utile pour gérer des exceptions précises ou toute autre subtilité de la traduction binaire. Pour gérer les exceptions différées et les lectures anticipées, le processeur mémorise l'état du processeur avant de démarrer un bloc de code. Pour cela, le processeur contient des ''shadow registers'', 48 registres entiers et 16 registres flottants. Vous remarquerez qu'il y a autant de registres entiers que de ''shadow registers'' entiers. Les registres entiers/flottants sont mémorisés dans les ''shadow registers'' avant d'exécuter un bloc de code, afin que le processeur puisse revenir à l'état de base. Une autre fonctionnalité liée est le ''gated store buffer''. Le principe est d'éviter toute écriture en mémoire RAM, tant que le code VLIW n'a pas émis d'instruction ''commit''. L'idée est que les écritures sont conservées dans la ''store queue'' de l'unité LOAD/STORE. Pour rappel, la ''store queue'' met en attente les écritures soit tant que les écritures ne sont pas terminées, soit tant que la RAM est occupée. L'idée est que les écritures sont accumulées dans la ''store queue'' tant que l'instruction ''commit'' n'a pas eu lieu. Le ''store queue'' est "déversé dans la RAM" seulement quand l'instruction ''commit'' s'exécute. ===Les CPU NVIDIA Denver=== Nvidia a tenté quelque chose de similaire avec son projet Denver. NVIDIA a racheté l'entreprise Transmetta, ses brevets, et a tenté de refaire la même chose. Le projet initial était de traduire du code x86 vers un jeu d'instruction VLIW propriétaire, comme l'a fait Transmetta. Mais NVIDIA n'a pas réussi à acquérir la licence du jeu d'instruction x86 et s'est rabattu sur le jeu d'instruction ARM. Le processeur né de ce projet est le Tegra K1-64 CPU. Il s'agit d'un processeur composé d'un cœur VLIW, couplé à deux décodeurs d'instructions ARM. Les deux décodeurs ARM traduisent une paire d'instructions ARM consécutives en instructions VLIW, pour les exécuter sur le cœur VLIW. Le CPU peut fonctionner selon deux modes : ARM et VLIW. En somme, le processeur supporte deux jeux d'instruction : le jeu d'instruction ARM, et un jeu d'instruction VLIW. En mode ARM, les instructions ARM sont chargées depuis le cache, traduites en VLIW par les décodeurs ARM, puis exécutées par le cœur VLIW. En mode optimisé, le CPU exécute des instructions VLIW chargées depuis le cache d'instruction, les décodeurs ne sont pas utilisés. : Le CPU supporte en réalité deux jeux d'instruction ARM : ARM8 et 7. Mais ils correspondent au même mode pour le CPU. Un point important est que le processeur peut changer très rapidement de mode, en à peine quelques cycles d'horloges. On n'est clairement pas dans le cas des CPU x86, qui mettent des plombes pour passer du mode 32 à 64 bits et inversement. La commutation est tellement rapide qu'on peut considérer que le processeur supporte deux jeux d'instruction simultanément. Et le support simultané de deux jeux d'instruction facilite l'implémentation de la traduction binaire. Encore une fois, inutile de traduire du code qui ne sera exécutée qu'une seule fois, mieux vaut l'exécuter directement dans le mode ARM. Surtout que le processeur supporte un mode ARM qui permet d'exécuter du code ARM sans perte de performance. Par contre, le code fréquemment utilisé, notamment dans des boucles critiques, est traduit en VLIW. Au final, les performances sont optimisées, en limitant le travail du logiciel traducteur binaire. La détection du code fréquemment exécuté se fait différemment que pour les processeurs Transmetta et Daisy. L'unité de branchement ne détecte pas directement le code beaucoup exécuté, même si elle a un rôle à jouer. A la place, la détection est réalisée en logiciel, par un ''thread'' dédié, qui s'exécute sur un cœur séparé. Il détecte le code fréquemment utilisé en regardent les branchements exécutés récemment. Pour cela, le processeur mémorise l'historique des branchements pris récemment et copie cette historique dans une mémoire tampon dédiée, partagée entre tous les cœurs. Vu qu'elle est partagée, le ''thread'' de détection a accès à l'historique sans pertes de performances, seule la copie de l'historique dans ce tampon a un cout en performance, pas son partage. Le code VLIW obtenu après traduction binaire est mémorisé dans la mémoire RAM, dans une portion spécialement réservée pour. Elle fait 128 mébioctets, et est appelée le '''cache d'optimisation''' par NVIDIA. Le cache d'optimisation est protégé en écriture et seul le traducteur binaire peut écrire dedans. Au passage, le traducteur binaire est du code VLIW, ce qui fait qu'il est placé dans le cache d'optimisation. Le processeur détecte automatiquement quand une fonction pour laquelle le code compilé équivalent est disponible. Pour cela, l'unité de chargement contient une table de correspondance entre l'adresse de la fonction ARM, et l'adresse de son équivalent compilé. Quand une fonction est appelée, l'unité de chargement regarde l'adresse de destination du branchement. Si l'adresse est dans cette table, elle récupère l'adresse de la fonction compilée et branche vers celle-ci. Le processeur dispose d'un cache d'instruction de 128 kibioctets, ce qui est très important, mais nécessaire vu la taille des instructions VLIW. Le coeur VLIW dispose de ses propres décodeurs, d'un ''scoreboard'', de 7 unités de calcul et de registres. Pour les unités de calcul, il a deux FPU, deux ALU, une unité de branchement, et deux unités LOAD-STORE faisant aussi ALU. Un point important est que le CPU dispose de 64 registres entiers et de 64 registres flottants. C'est deux fois plus que supporte le jeu d'instruction ARM. En mode ARM, seule la moitié des registres est utilisée. Mais en mode optimisé, le traducteur binaire utilise bien les 64 registres, grâce à une sorte de renommage de registres logicielle. Pour simplifier la traduction binaire, le processeur supporte les techniques vues précédemment. Le processeur intègre un ''gated store buffer'' similaire à celui des processeurs Transmetta. Les techniques d'exceptions différées sont aussi supportées, comme sur les processeurs Transmetta. Il a aussi une unité de préchargement décrite par NVIDIA comme agressive, avec support du préchargement de type anticipé (''runahead''). ===L'émulation de la mémoire virtuelle=== Pour finir, il faut parler de la mémoire virtuelle avec la traduction binaire. Sur le processeur source, le processeur gère à la fois des adresses physiques et virtuelles et sa MMU fait la conversion entre les deux. Sur le processeur VLIW, la MMU est simulée par le programme de traduction binaire, partiellement ou totalement. L'implémentation exacte varie suivant que l'on parle du projet Daisy, des processeurs Transmetta ou de Denver. Sur les processeurs Transmetta, il n'y a pas de MMU ni de mémoire virtuelle. A la place, le programme de traduction binaire émule la mémoire virtuelle du processeur source. Le processeur VLIW ne gère que des adresses physiques, rien d'autre. L'espace d'adressage physique du processeur VLIW a la même taille que celui du processeur émulé, ici des processeurs x86. Il est vraisemblable que les adresses physiques utilisées par le processeur x86 sont les mêmes que celles du processeur VLIW. Sur le projet Daisy, le processeur VLIW gère la mémoire virtuelle via pagination, comme les processeurs émulés. Ce qui fait qu'il gère un espace d'adressage virtuel et un espace d'adressage physique. Pour éviter toute confusion, nous parlerons d'adresse physique/virtuelle VLIW pour les adresses physiques/virtuelles du processeur VLIW, d'adresse virtuelle/physique source pour celle du jeu d'instruction traduit, à savoir du Power PC ou du s390, éventuellement de l'x86. Il faut alors faire le lien entre adresses physiques source et adresses virtuelles VLIW. Pour cela, rien de plus simple : il y a correspondance parfaite. L'adresse physique source numéro N correspond à l'adresse virtuelle VLIW numéro N. Cependant, cela signifie que tout l'espace d'adressage virtuel VLIW serait utilisé par le code à traduire. En réalité, il faut ajouter de la place pour le code traduit et le programme de traduction binaire. La conséquence est que l'espace d'adressage virtuel VLIW est plus large que l'espace d'adressage source. L'espace d'adressage virtuel VLIW est découpé en trois sections : une section pour le code Power PC à traduire, une autre pour le code traduit en VLIW, et une dernière réservée au traducteur binaire. Le programme de traduction est placé dans une ROM mappée en mémoire dans la seconde section. Le reste de la seconde section est réservé à une mémoire RAM utilisée par le programme de traduction. [[File:Mémoire virtuelle sur le projet Daisy.png|centre|vignette|upright=1.5|Mémoire virtuelle sur le projet Daisy]] Il faut noter que chaque page physique source, une fois traduite, correspond à N pages virtuelles VLIW. La raison est que le code traduit prend plus de place que le code machine originel. En conséquence, on doit utiliser une page finale plus grande. Pour le reste, des pages contiguës en mémoire physique source sont elles aussi contiguës en mémoire virtuelle VLIW. Le calcul d'adresse est donc simple : l'adresse physique source est multipliée par N, puis on ajoute l'adresse de base à laquelle commence la section pour le code traduit. ==Les processeurs à jeu d'instruction reconfigurable== Nous venons de voir comment accélérer en matériel la traduction binaire. Mais il existe une alternative qui permet de se passer de traduction binaire, complétement. L'idée est d'avoir un processeur avec un '''jeu d'instruction reconfigurable'''. Le nom est assez parlent : on peut configurer le processeur de manière à supporter le jeu d'instruction de son choix ! Par exemple, on pouvait reconfigurer un CPU de manière à ce qu'il supporte le jeu d'instruction x86, ou qu'il exécute du ''bytecode'' Java, ou du code ARMv8, etc. De tels processeurs sont encore plus rare que ceux vu avant dans ce chapitre, mais ils ont existé ! Les processeurs de ce type avaient un ''microcode réinscriptible'', à savoir que c'était des processeurs microcodés pour lesquels on pouvait modifier le microcode. L'idée était qu'en changeant le microcode, on changeait le jeu d'instruction du processeur. Pour cela, le microcode était associé à une mémoire SRAM dans laquelle le microcode était copié au démarrage du CPU. Il y avait donc une micro-ROM et une micro-RAM. Les instructions étaient décodées en utilisant le microcode dans la micro-SRAM, la micro-ROM ne servait qu'au démarrage. En modifiant le contenu de la micro-SRAM, on pouvait altérer le microcode, voire le remplacer totalement. ===Le Burroughs B1700 : architecture externe=== Un exemple est celui du Burroughs B1700, et il est intéressant d'étudier en détail son architecture. Son architecture est décrite dans le livre "Interpreting Machines : Architecture and Programming of the Bl700/Bl800 Series", écrit par Elliott I. Organick et James A. Hinds. Une partie de ce qui vfa suivre est un très court résumé de ce livre. Il disposait de 4 registres généraux, nommés X, Y, T et L, tous de 24 bits. Les registres T et L étaient découpés en 6 sous-registres de 4 bits, nommés LA LB LC LD LE et LF pour le registre L, et TA TB TC TD TE et TF pour le registre T. Ils étaient adressables comme n'importe quel registre. Un point important est que le registre T avait des possibilités d’extraction spécifiques. Il était possible d'extrait N bits, placés n'importe où dans ce registre, et de les copier dans un autre registre. Nous verrons pourquoi cette possibilité est intéressante et ne sert que pour le registre T. Les registres X et Y étaient reliés à une ALU de 24 bits capable d'effectuer des opérations arithmétiques basiques. Fait étonnant, l'ALU faisait tous les calculs en même temps et fournissait ses résultats dans 7 registres : un registre SUM pour l'addition, un registre DIFFERENCE pour la soustraction, et un registre par opération bit à bit. Les registres pour les opérations bit à bit sont : CMPX et CMPY pour l'opération NOT sur chaque opérande, XANY pour le ET bit à bit, XEOY pour le XOR, XORY pour le OU logique. Les additions et soustractions pouvaient se faire sur des opérandes codés en binaire ou en BCD, suivant le code opération utilisé. A cela, il fallait ajouter deux registres de "résultat" MSKX et MSKY. Ils fournissaient un masque dépendant de la taille du résultat en bits. Cela permettait de gérer des données de 8 ou 16 bits, alors que l'ALU faisait 24 bits. Et cela n'était pas limité à 16 ou 8 bits, mais n'importe quelle taille inférieure ou égale à 24 bits était supportée, même des tailles non-conventionnelles comme des résultats codés sur 3 bits, 9 bits, 12 bits, etc. L'ALU peenait en opérande les registres X et Y, mais aussi un registre CYF pour la retenue entrante. En sortie, il fournissait aussi deux retenue sortantes : une pour l'addition et une autre pour la soustraction. Elles étaient mémorisées dans les registres CYL et CYD, respectivement. L'ALU fournissait aussi 12 conditions, qui alimentait un registre d'état. Le registre d'état était segmenté en trois registres appelés XYST, XYCN et BICN. Le processeur contenait de nombreux autres registres de 4 bits, dont les sous-registres de T et L. En tout, il y a 27 registres de 4 bits adressables ! Et le processeur disposait d'une ALU 4 bits pour manipuler ces registres de 4 bits ! Elle est capable de faire des MOV entre registres de 4 bits, des opérations bit à bit, et peut aussi tester si un bit vaut 0 ou 1. La dernière possibilité est très utile pour implémenter les branchements. Pour communiquer avec les entrées-sorties, il disposait de deux registres CMND et DATA : CMND pour envoyer une commande à une entrée-sortie, DATA pour lire ou écrire une donnée. Pour la mémoire, il avait deux registres séparés : READ et WRITE. READ contient la donnée lue lors d'une lecture,; WRITE est pour une donnée à écrire lors d’une écriture. Il s'agit de registres d’interfaçage mémoire, qui ont été promus au rang de registres architecturaux. Les deux registres sont gérés par le microcode, qui est le langage principal de la machine. Les instructions machines configurées font sans, mais le microcode doit y avoir accès facilement. Et vu que le microcode est le langage principal de ce processeur, ces deux registres deviennent architecturaux. ===Une architecture bit-adressable=== Si avoir un microcode réinscriptible est suffisant, quelques choix de ''design'' facilitent l'émulation de jeux d'instructions variés. Par exemple, le CPU Burroughs B1700 avait ajouté une fonctionnalité très importante : il était bit-adressable. En clair, il pouvait adresser la mémoire bit par bit, si nécessaire. L'avantage est que cela permettait d'émuler des architectures dont la taille des registres/''byte''/mots était très différente. Un autre avantage est que cela facilitait la gestion des instructions de taille variable. Détaillons rapidement le premier avantage. Le processeur était un CPU 24 bits, mais il pouvait émuler des processeurs 16 bits, 8 bits, 4 bits, 1 bit, 9 bits, ou toute autre valeur. Pour cela, le processeur lisait des mots de 24 bits et utilisant un ''barrel shifter'' pour sélectionner les bits adéquats. Par exemple, pour un accès de 16 bits, la donnée lue en mémoire était masquée de manière à ne garder que les 16 bits de poids faible. Le ''barrel shifter''/circuit de masquage était placé directement avant le bus mémoire, sur le trajet des données/instructions. Le ''barrel shifter'' était commandé par un registre ''Field Unit'', qui précisait quelle était la taille des données à charger. ===La gestion de la taille des instructions=== Les instructions à décoder n'ont pas de longueur fixe, à savoir qu'elles peuvent faire 16 bit avec tel jeu d'instruction, 8 bits avec un autre, 32 bits sur un autre, etc. Elles peuvent aussi être de longueur fixe ou variable ! Le B1700 intégrait donc un support des instructions de longueur variable, qui était "détourné" pour gérer des jeux d'instruction différents. Notons que vu que l'architecture est bit-adressable, les contraintes d'alignement des instructions disparaissent, ce qui a des avantages qu'on va détailler dans ce qui suit ! L'implémentation des instructions de taille variable ajoutait un registre avant le registre d'instruction. Le registre d'instruction proprement dit était un registre M de 24 bits. Il était précédé par un registre T, dans lequel l'instruction était assemblée, en combinant plusieurs morceaux. Si l'instruction est codée est codée sur moins de 24 bits, le registre T se contente de masquer les bits en trop. Si l'instruction chargée dépasser 24 bits, le chargement se faisait en plusieurs passes, avec plusieurs lectures en mémoire. Dans ce qui suit, on suppose que l'instruction est composée de '''champs''' : l'opcode, les opérandes, les adresses absolues, les constantes immédiates, les numéros de registre, etc. L'idée est que l'instruction est chargée dans le registre T, puis découpée en champs, qui sont copiés dans les registres du processeur ou envoyés au microcode. Je rappelle que le registre T est relié à un ''barrel shifter'', différent au précédent, qui permettait de masquer, décaler et extraire une donnée de ce registre. Et il y a la possibilité d'extraire une suite de bit de ce registre T, pour la copier dans un autre registre. Par exemple, prenons une instruction composée d'un opcode, d'une constante immédiate et d'une adresse immédiate. L'opcode est lu et interprété par le microcode. Le microcode extrait la constante immédiate du registre T et la copie dans le registre X. Ensuite, l'adresse est extraite et envoyée au registre FA, une lecture est démarrée, et l'opérande est copiée dans le registre Y. Enfin, l'instruction est exécutée par le microcode. Le tout prend plusieurs micro-opérations : une première pour lire l'opcode, une seconde pour lire la première opérande, une troisième pour lire la seconde opérande, una quatrième pour tout exécuter. Notons que suivant la taille des champs, on peut en charger plusieurs à la fois. Par exemple, imaginons qu'on ait chargé 24 bits, contenant un opcode de 8 bits et une constante immédiate de 16 bits : pas besoin de faire deux accès mémoire, le registre T est juste découpé en un opcode et une constante. Le registre T est fait pour ce genre d'extraction. ===Le chargement d'un champ=== L'architecture est bit-adressable car elle gére des champs qui ne font pas forcément 8 ou 16 bits. Par exemple, certains jeux d'instructions ont des opcodes codés sur 6 ou 7 bits, voire 9-10 bits. Par exemple, prenons un opcode de 7 bits, suivi par une adresse de 12 bits et une constante immédiate de 15 bits : aucun de ces champs ne sera aligné sur 8/16/24 bits. Utiliser une architecture bit-adressable règle le problème d'alignement. Et en général, les contraintes d'alignement des instructions disparaissent ! Quand on charge une instruction, peut importe qu'elle soit ou non alignée sur 24 bits, elle sera placée dans les bits de poids faible, il suffira de masquer les bits de poids fort inutiles. Par exemple, si on charge une instruction de 16 bits, les 16 bits seront copiés dans les 16 bits de poids faible du registre T, il suffira de masquer l'octet de poids fort. Pas besoin de faire de décalages pour gérer des instructions mal alignées... Pour charger une instruction, il faut connaitre plusieurs informations : l'adresse de son premier bit, sa taille. Pour cela, le B1700 contient deux registres, nommés ''Field Adress'' (FA) et ''Field Length'' (FL), qui indiquent l'adresse du premier bit et la taille de l'instruction à charger. Le premier fait 24 bits, ce qui permet de gérer des adresses de 24 bits. N'oubliez pas que le processeur est bit-adressable, ce qui permet d'adresser 16 Mébi-bits, soit 2 mébioctets. Le registre pour la taille fait lui 16 bits, ce qui permet de gérer des instructions de 65536 bits, soit 8192 octets ! Leur nom commence par ''field'', car le processeur peut gérer des instructions de taille variable et/ou plus longues que 24 bits. Dans ce cas, le chargement de l'instruction se fait champ par champ. Pour donner un exemple, prenons une instruction composée de 20 champs de 12 bits chacun. Les champs sont chargés un par un dans le registre T. Le registre FA est alors initialisé avec l'adresse de l'instruction, il est incrémenté de 12 à chaque cycle. Le registre FL est initialisé avec la taille de l'instruction, soit 20 * 12 = 240 bits, et il est décrémenté de 16 à chaque cycle. Précisons cependant que la taille d'un champ peut être changée d'un cycle à l'autre. Par exemple, le processeur peut charger un opcode de 8 bits, l'interpréter, détecter que l'instruction demande ensuite de charger une adresse de 16 bits. Dans ce cas, le registre FL est immédiatement altéré pour charger 16 bits, au lieu des 8 bits de l'opcode immédiatement précédent. ===Le microcode réinscriptible=== Le microcode utilise des micro-opérations de 16 bits de long, ce qui est très peu et bien en-dessous des micro-opérations des autres processeurs. Les micro-opérations sortant du microcode sont envoyées au registre M, un registre de micro-opération de 16 bits. les micro-opérations écrites dans ce registre sont ensuite décodées par le hardware et exécutées. Fait intéressant, il était possible de faire un OU logique entre la micro-opération sortante et un registre du processeur, le résultat étant mémorisé dans le registre M. Cela permettait de faire du code automodifiant dans le microcode ! Utiliser du code automodifiant avec un microcode réinscriptible est parfaitement possible, et c'est utile pour émuler des modes d'adressage complexe dans le microcode. Cela permettait par exemple de remplacer une adresse dans une micro-opération. L'adresse est alors surimposée avec un OU dans le champ d'adresse, alors que le reste de l'instruction n'est pas modifié. Mais modifier directement le microcode est rarement une bonne idée. Aussi, pour éviter cela, l'idée est de modifier les instructions avec un OU logique avec un autre registre. Les micro-opérations du microcode ne sont donc pas modifiées dans le microcode, mais en sortie du microcode. La RAM mémorisant le microcode était de petite taille, et il arrivait qu'elle soit trop petit pour mémoriser le microcode complet. Dans ce cas, le programmeur devait gérer la situation en découpant le microcode en segments, certains étant placé dans le processeur, les autres étant en mémoire RAM. Le processeur peut lire le microcode placé en mémoire RAM et même l'exécuter, mais avec une pénalité en termes de performances. Quelques registres intégrés au processeur permettent ce genre de magie. Le microcode pouvait servir pour émuler un autre jeu d'instruction, mais il était aussi possible d'implémenter un véritable interpréteur, grâce aux techniques du paragraphe précédent. Le processeur était même conçu pour ! L'interpréteur était placé dans un "segment" en mémoire RAM, dont le processeur connaissait la position. Il incorporait pour cela deux registres, un pour l'adresse de base de l'interpréteur, un autre pour son adresse de fin. Il incorporait une protection mémoire limitée sur ce segment, à savoir que seul l'interpréteur pouvait lire ou écrire dans ce segment. Afin de gérer les interruptions, ces dernières étaient partiellement détectées en logiciel. Les interruptions ne s'exécutaient pas automatiquement, pour émuler correctement les interruptions. Une interruption doit être prise en compte à la toute fin de l'exécution d'une instruction, quand celle-ci a finit son travail. Mais vu qu'ici, les instructions sont émulées avec des séries de micro-opérations, il y a un problème : il est possible d'interrompre une instruction émulée en plein milieu. Pour éviter cela, le processeur ne déclenche pas les interruptions matérielles immédiatement, elles sont mises en attente. Une micro-opération dédiée vérifie si une interruption est en attente, et l'exécute alors cas échéant. La micro-opération est utilisée comme micro-opération finale d'une instruction émulée. ==Les jeux d'instructions dédiés à un langage de programmation== De rares processeurs sont conçus pour un langage de programmation en particulier. On appelle ces processeurs, conçus pour des besoins particuliers, des '''processeurs dédiés'''. Par exemple, les fameux ''Burrough E-mode'' B5000/B6000/B7000 étaient spécialement conçus pour exécuter de l'ALGOL-60. Leurs cousins B2000/B3000/B4000 étaient eux conçus pour le COBOL. Des langages fonctionnels ont aussi eu droit à leurs processeurs dédiés. Le prolog en est un bel exemple, avec les superordinateurs de 5ème génération qui lui étaient dédié. On peut aussi citer les machines LISP, dédiés au langage LISP, qui datent des années 1970. Elles étaient capables d’exécuter certaines fonctions de base du langage directement dans leurs circuits : elles possédaient notamment un ''garbage collector'' câblé dans ses circuits ainsi que des instructions machines supportant un typage déterminé à l’exécution. Les processeurs dédiés ont eu leur heure de gloire au début de l'informatique, à une époque où les langages de haut niveau venaient d'être inventés. À cette époque, les compilateurs n'étaient pas performants et ne savaient pas bien optimiser le code machine. Il était alors rationnel, pour l'époque, de rapprocher le code machine cible et le langage de programmation de haut niveau. De nombreuses architectures dédiés ont ainsi été inventées, avant que les concepteurs se rendent compte des défauts de cette approche. Les défauts en question ne sont pas nombreux, mais assez simples à comprendre. Premièrement, elles sont très rapides pour un langage de programmation en particulier, mais sont assez mauvaises pour les autres, d'où un problème de "compatibilité". Ajoutons à cela que les langages de programmation peuvent évoluer, devenir de moins en moins populaires/utilisés, ce qui rend la création d'architectures généralistes plus pertinente. Enfin, les architectures dédiées sont évidemment des processeurs CISC, pour implémenter les nombreuses fonctionnalités des langages évolués. Et les défauts des CISC sont assez rédhibitoires à l'heure actuelle. À l'heure actuelle, les algorithmes des compilateurs se sont améliorés et savent nettement mieux utiliser le matériel. Ils produisent du code machine efficace, ce qui rend les architecture dédiées bien moins intéressantes. Si on ajoute les défauts de ces architectures dédiées, par étonnant que les architectures dédiées aient presque disparues. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'accélération matérielle de la virtualisation | prevText=L'accélération matérielle de la virtualisation | next=Le matériel réseau | nextText=Le matériel réseau }} </noinclude> 4iqzpxa8f13ljw3c7yz0ciqujqan9l4 764027 764026 2026-04-19T18:30:28Z Mewtow 31375 /* Le Burroughs B1700 : architecture externe */ 764027 wikitext text/x-wiki De nos jours, la majorité des programmeurs programment dans des langages de haut niveau. Il est très rare pour eux d'avoir à utiliser de l'assembleur, et encore moins un langage machine. Les programmes écrits dans un langage de haut niveau sont traduits en langage machine par un logiciel appelé le compilateur. [[File:Compilation.PNG|centre|vignette|upright=2.5|Compilateur : principe.]] Il se trouve que quelques rares processeurs sont conçus pour faciliter le travail du compilateur. Et ce sont ces processeurs que nous allons voir dans ce qui suit. Ils représentent une catégorie de jeux d'instruction à part, qui n'a pas vraiment de nom. ==La compilation et l’interprétation== Un compilateur traduit donc du code source, écrit dans un langage de haut niveau, vers du langage machine. La traduction est rarement directe. En général, le compilateur traduit le code source en assembleur, qui est lui-même traduit en langage machine. L'assembleur est une représentation textuelle du langage machine alors que le code machine est du binaire exécutable par le processeur. La traduction se fait donc en deux étapes, la '''compilation''' proprement dite et l''''assemblage''', réalisées respectivement par un compilateur et un assembleur. Il faut parfois rajouter une troisième phase d'édition des liens, que nous passons volontairement sous silence. {| |[[File:Code C.png|vignette|200px|Le code source, ici en C.]] |[[File:Assem.png|vignette|250px|Le code assembleur.]] |[[File:Binary file - hello world (C programming).png|vignette|150px|Le code machine.]] |} ===L'interprétation et la compilation à la volée=== Une alternative à la compilation est l''''interprétation''', qui transforme le code source en code machine à la volée. Avec l'interprétation, le code source est passé à un logiciel appelé l''''interpréteur''', qui exécute le code source ligne de code par ligne de code. Une ligne de code est traduite en code machine, qui est exécutée, puis l'interpréteur passe à la ligne suivante. [[File:Kaantotulkkaus.png|centre|vignette|upright=3|Compilation versus interprétation.]] Un défaut de l'interprétation est son cout en performance, qui est assez important. Aussi, l'interprétation stricte a évolué vers un hybride entre interprétation et compilation. L'idée est qu'une partie peu importante du code source est interprétée, alors que le code important est compilé juste avant d'être exécuté. Pour le dire autrement, le code source est partiellement compilé à la volée, juste avant son exécution. Aussi, on parle de '''compilation à la volée'''. Le terme en anglais est ''Just In Time compilation'', abrévié en JIT. En soi, le JIT est associé à de l'interprétation, les JIT sont en réalité des hybrides interpréteurs-compilateurs. Un point important est que compiler du code à la volée est assez lourd, cela a un cout en performance. Par contre, le code compilé s'exécute plus vite que du code interprété. Si on exécute le code compilé une seule fois, le cout de la compilation l'emporte sur le gain à l'exécution. Mais le code compilé est réutilisé autant de fois que nécessaire. Le gain à l'exécution est donc multiplié par le nombre d'exécutions, alors que le cout de la compilation est répartit sur plusieurs exécutions, il est amorti. Les portions du code sont choisies de manière à avoir un gain en performance maximal. L'idée générale est que le code exécuté très souvent est compilé, alors que du code exécuté pas souvent est interprété. Typiquement, les boucles les plus souvent exécutées sont compilées, le code exécuté une seule fois est interprété. ===Les langages intermédiaires=== Les compilateurs modernes passent par un '''langage intermédiaire''' pour faire la transformation en code machine. Le compilateur traduit le langage de haut niveau en langage intermédiaire, puis traduit le langage intermédiaire en code machine cible. Le langage intermédiaire est parfois appelé le '''''bytecode''''', et ce terme recouvre aussi le code écrit avec du ''bytecode''. L'interprétation aussi peut passer par l'intermédiaire d'un un ''bytecode'', ce n'est pas limité aux compilateurs proprement dit. {| |[[File:Intermediate representation scheme.png|centre|vignette|upright=1.5|Représentation intermédiaire d'un compilateur.]] |[[File:Code interpreter scheme.png|centre|vignette|upright=1.5|Fonctionnement d'un interpréteur.]] |} Faire ainsi a de nombreux avantages, le principal étant d'avoir un compilateur capable de traduire un langage de haut niveau vers plusieurs jeux d'instructions différents. Cela permet d'avoir, par exemple, un compilateur qui traduit du C soit en code machine x86, soit en code machine pour un CPU ARM, soit pour un CPU POWERPC, etc. [[File:Compiler design.svg|centre|vignette|upright=3|Compiler design]] Pour l'interprétation, l'usage d'un ''bytecode'' a d'autres avantages. Pour l'exploiter, il faut distribuer non pas le code source, mais le ''bytecode''. En clair, l'interpréteur ne prend pas en entrée le code source, mais du ''bytecode'' déjà compilé. La traduction est alors beaucoup plus simple, car le code source a déjà été partiellement compilé, par la traduction en ''bytecode''. Interpréter du code source est en effet assez compliqué : il faut effectuer des étapes d'analyse lexicale, sémantique, et bien d'autres. Avec du ''bytecode'', ces étapes ont été réalisées lors de la compilation du ''bytecode'', l'interpréteur a alors peut de choses à faire. L'interprétation du ''bytecode'' se fait instruction par instruction, au niveau du ''bytecode''. Une instruction du ''bytecode'' est traduite en une instruction machine équivalente, qui est exécutée. Si l'instruction est un peu complexe, il exécute une fonction/procédure qui fait la même chose. Pour résumer, le ''bytecode'' est ensuite traduit à la volée et exécuté instruction par instruction par un logiciel appelé l'interpréteur. [[File:Control table.png|centre|vignette|upright=2|Control table]] Le langage intermédiaire peut être vu comme l'assembleur d'un processeur, n'existe pas forcément dans la réalité, mais dont le jeu d'instruction est décrit en détail. Le processeur en question est appelé une '''machine abstraite''', ou encore une ''machine virtuelle''. Nous utiliserons le terme de machine abstraite dans ce qui suit. La machine abstraite n'est pas la même suivant que l'on cible la compilation ou l'interprétation/JIT. Pour l'interprétation/JIT, la machine abstraite est souvent une machine à pile, car cela simplifie grandement la traduction en code machine final. L'interprétation demande que la traduction du ''bytecode'' en code machine soit la plus simple possible. Et cela demande de prendre en compte pas mal de détails. Par exemple, les différents jeux d'instruction existants n'ont pas le même nombre de registres, ce qui pose problème lors de la traduction du ''bytecode''. Lors de la transformation en code machine, un algorithme d'allocation de registres se débrouille pour traduire le code intermédiaire en code qui utilise un nombre limité de registres. Pour améliorer cette allocation de registres, il y a deux solutions niveau ''bytecode'' : soit utiliser un nombre illimité de registres, soit utiliser une machine à pile. Pour les machines à pile, il existe un algorithme simple et rapide pour traduire un code écrit pour une machine à pile en un code écrit pour un processeur avec des registres, qui se débrouille pas trop mal pour allouer efficacement les registres. C'est un avantage assez important pour les langages interprétés. ==Les processeurs qui exécutent du bytecode== Le ''bytecode'' est un code machine, ce qui signifie qu'il peut en théorie s'exécuter sur un processeur qui implémente le jeu d’instruction associé. Si le jeu d'instruction d'un ''bytecode'' est souvent une description censée être fictive, elle n'en reste pas moins un jeu d'instruction et des caractéristiques précises, qu'on peut l'implémenter en matériel ! Néanmoins, tous les ''bytecode'' ne sont pas égaux de ce point de vue. Certains sont faciles à implémenter en matériel, d'autres non. Les ''bytecodes'' implémentés en matériel sont ceux dont les machines abstraites sont des machines à pile. Un premier exemple est celui des processeurs Pascal MicroEngine, qui exécutaient directement le bytecode du langage Pascal, le fameux UCSD P-code. Un second exemple est la '''machine SECD''', qui sert de langage intermédiaire pour certains compilateurs de langages fonctionnels. Elle a été implémentée en matériel par plusieurs équipes, notamment par les chercheurs de l'université de Calgary en 1989. Dans le même genre, quelques processeurs simples étaient capables d’exécuter directement le ''bytecode'' du langage FORTH. Le FORTH, un des premiers langages à pile de haut niveau, possède de nombreuses implémentations hardware et est un des rares langages de haut niveau dont le ''bytecode'' a été utilisé comme langage machine sur certains processeurs. Par exemple, on peut citer le processeur FC16, capable d’exécuter nativement du ''bytecode'' FORTH. ===Les processeurs Java=== Le cas le plus impressionnant est celui de la machine virtuelle Java, qui est un design de processeur comme un autre. En temps normal, le ''bytecode'' Java est compilé ou interprété, mais certains processeurs exécutaient du ''bytecode'' Java directement, sans interprétation ni compilation. Ils sont appelés des '''processeurs Java'''. On peut citer les processeurs ARM disposant de l'extension Jazelle, du JEMCore et du aJ-100 de aJile Systems, le picoJava II de Sun, et quelques autres. Il y a aussi eu quelques projets de recherche ou processeurs open source, comme Komodo, jamuth, le ''Java Optimized Processor'' et quelques autres projets du même genre. L'implémentation de la machine virtuelle Java n'était cependant pas complète. Quelques instructions complexes n'étaient pas gérées par le processeur et devaient être émulées en logiciel. Mais la grosse majorité des instructions l'était. Les instructions non-supportées étaient émulées avec la méthode ''trap and emulate'', à savoir qu'un opcode inconnu déclenchait une exception matérielle ''opcode invalide'', dont la routine pouvait émuler les instructions qui doivent l'être. Le processeur aJ-100 de aJile est capable d'exécuter la quasi-totalité des instructions de la JVM Java, seules deux instructions faisant exception. Les '''accélérateurs Java''' sont une solution intermédiaire, qui permet de traduire à la volée du ''bytecode'' Java en instructions RISC, exécutées sur un autre processeur. Il s'agit formellement de traduction binaire, mais je préfère en parler ici. L'idée est de combiner un processeur RISC avec un coprocesseur Java. Le coprocesseur lit le ''bytecode'' Java et le traduit en instructions RISC, ces dernières étant exécutées sur le processeur RISC. Le JA108 de Nozomi était un coprocesseur de ce genre, au même titre que le coprocesseur JSTAR de JEDI. L'extension Jazelle de certains CPU ARM fonctionnait vraisemblablement sur un principe similaire, sauf que le coprocesseur était intégré dans le processeur ARM, entre le cache d'instruction et le décodeur d'instructions ARM. L'intérêt des processeurs Java n'est pas qu'une question de performance. Certes, ils permettent d’exécuter plus vite les programmes compilés en ''bytecode'', comme des programmes Java pour la JVM Java. Mais l'intérêt est aussi de faire des économies de mémoire RAM et ROM. Je rappelle que la machine virtuelle Java est une machine à pile, ce qui fait que sa densité de code est excellente. Les programmes en ''bytecode'' Java sont donc très petits, et prennent peu de place en mémoire ROM. De plus, utiliser un processeur Java permet de se passer d'interpréteur ou de JIT Java, ce qui économise encore plus de mémoire. ===Transformer un processeur RISC en machine à pile=== La plupart des processeurs Java utilisent en interne une architecture à registre généraux, qui émule une machine à registre en microcode. Quelques registres sont réservés pour l'état de la machine virtuelle, avec au minimum un registre réservé pour le pointeur de pile, un autre pour un pointeur vers un ''pool'' de constantes, et quelques autres. D'autres registres servent à mémoriser le haut de la pile, à savoir le sommet de la pile et quelques opérandes situées juste en-dessous. Par exemple, le processeur picoJava II gardait les 64 opérandes au sommet de la pile dans 64 registres généraux, séparés des registres pour les pointeurs et l'état de la JVM. Une optimisation possible, envisagée sur les processeurs picoJava, était une sorte de macro-fusion sous stéroïde. L'idée était de fusionner certaines séries d'instructions Java en une seule instruction machine. Typiquement, pour faire une opération arithmétique, une machine à pile empile deux opérandes et exécute une opération, ce qui prend trois instructions machine. La spécification picoJava 1 a proposé d'envoyer directement la seconde opérande en entrée de l'ALU, ce qui fusionnait l'empilement de la seconde opérande et l'opération arithmétique. Le processeur picoJava II va plus loin et fusionne les trois instructions en une seule instruction RISC, agissant sur des registres. Mais la majorité des processeurs Java ne faisaient pas cela, car cela complexifie grandement le décodeur. Les processeurs ''bigfoot'' de DCT utilisaient un système différent pour émuler une pile avec des registres. Leur banc de registre de 64 bits était coupé en deux : les 16 premiers registres étaient utilisés pour la pile, les 48 suivants étaient utilisés pour autre chose. L'idée est que le registre 0 mémorise un emplacement vide, le registre 1 contient le sommet de la pile, le registre 2 l'opérande sous le sommet de la pile, et ainsi de suite. Les 15 premières opérandes de la pile sont donc mémorisées dans les registres. Les instructions écrivent leur résultat dans le registre adéquat, puis tous les registres sont décalés pour que le résultat passe dans le registre R1. Par exemple, les instructions PUSH chargent une opérande dans le registre 0, puis tous les registres sont décalés de un rang, pour que l'opérande chargée soit dans le registre 1. Le registre 0 devient alors le registre numéro 1, le registre 1 devient le 2, et ainsi de suite. Par contre, les opérations arithmétiques font autrement. Une instruction d'addition, par exemple, lit les opérandes dans le registre 1 et 2, puis place son résultat dans le registre 2 (les deux opérandes sont dépilées). Puis, les registres sont décalés d'un rang pour que ce registre 2 devienne le 1. Les registres ne sont en réalité pas décalés, il n'y a pas de transferts entre registres. A la place, les 16 premiers registres utilisaient une forme limitée de renommage de registres, similaire au banc de registre tournant utilisé sur les processeurs EPIC comme l'Itanium. Les numéros de registres sont renommés, décalés, à chaque fois qu'on empile ou dépile une opérande. Pour faire le renommage de registres, processeur contient un ''register stack counter'' de 4 bits, qui est incrémenté à chaque fois qu'on empile une donnée, et est décrémenté quand on dépile une donnée. Quand on veut accéder à un opérande, le numéro de registre architectural associé est traduit en numéro de registre physique, en additionnant le ''register stack counter'' (modulo 16, pour rester sur 4 bits). ==L'accélération de l'interprétation/JIT : le ''Thumb-EE'' d'ARM== Quelques processeurs ont ajouté des instructions pour faciliter le travail des interpréteurs/JIT. Par exemple, en 2005, ARM a ajouté le mode ''Thumb Execution Environment'', qui faisait cela. Il reprenait le jeu d'instruction compact ''thumb'' et ajoutait quelques instructions et modifiait le comportement d'instructions existantes. Le jeu d'instruction ''Thumb-EE'' a été ajouté sur les CPU ARM en 2005, mais a été déprécié en 2011, par manque d'utilité. Mais la tentative mérite qu'on s'attarde dessus. Les CPU de gamme M, déstinés à l'embarqué, n'ont jamais supporté ''Thumb-EE'', vu que de tels processeurs ne sont pas conçus pour exécuter du code interprété/JIT. Le mode ''Thumb-EE'' est un mode d'exécution, séparé de l'ARM normal. Ainsi, une instruction ''Thumb-EE'' et une instruction ARM peuvent avoir le même encodage en binaire, mais se comporter différemment. Le processeur est à tout moment soit en mode ''Thumb-EE'', soit en mode ARM normal, ce qui précise comment ces instructions doivent se comporter. Des instructions ont été ajoutées par ''Thumb-EE'', pour qu'il fasse son travail. L'entrée et la sortie du mode ''Thumb-EE'' se fait avec deux instructions : ENTERX et LEAVEX. Une fois en mode ''Thumb-EE'', le décodage des instructions ''thumb'' se fait avec les règles du ''Thumb-EE'', et non celles du ''thumb'' normal. Une première différence entre ''thumb'' normal et ''Thumb-EE'' est l'ajout de ce qui s'appelle un ''null check'' pour les instructions mémoire. Le ''null check'' est utilisé pour les instructions en mode d'adressage "base + indice" ou "Base + décalage", ou tout autre mode d'adressage avec un registre de base utilisés dans des calculs d'adresse. L'idée est de vérifier si l'adresse de base vaut zéro ou non. Si c'est le cas, le processeur lève une exception matérielle, qui est traitée par l'interprétation ou le compilateur JIT. Une telle situation est en effet signe d'une erreur d'adressage mémoire, qui doit être traitée par l'interpréteur. Typiquement, c'est signe qu'un pointeur n'a pas bien été initialisé, ce qui peut arriver avec du code interprété ou JIT. Les interpréteurs/JIT traitent généralement la situation en ajoutant un test avant toute instruction mémoire, pour vérifier si le pointeur accédé vaut zéro ou non. Avec ''Thumb-EE'', pas besoin d'ajouter les tests en questions : ils sont réalisés automatiquement lors de chaque instruction mémoire. : Les registres pour la pile, comme le pointeur de pile et le pointeur de ''frame'', sont aussi concernés. L'instruction CHKA vérifie que les accès à un tableau ne débordent pas en dehors du tableau en question. Par exemple, pour un tableau de 1024 éléments, elle vérifie si l'indice est compris entre 0 et 1024. Pour cela, elle vérifie si l'indice de tableau est bien dans l'intervalle adéquat et lève une exception matérielle si ce n'est pas le cas. Son utilité se comprend quand on sait que tous les accès à un tableau sont vérifiés par l'interpréteur. L'interpréteur est censé ajouter des instructions pour vérifier les indices, à savoir deux ou trois branchements. Avec CHKA, le test se fait en une seule instruction. D'autres modifications mineures de l'encodage des instructions sont aussi présentes, ainsi que des modifications pour les instructions LOAD/STORE. Précisément, dans les modes "base + indice" et "base + décalage", le décalage et l'indice subissent maintenant des décalages. L'idée est de simplifier le calcul d'adresse. De quoi économiser quelques instructions lors des calculs d'adresse, ce qui facilite le travail de l'interpréteur/JIT. Et des instructions LOAD/STORE précises voient leur comportement modifié pour faciliter la gestion de la pile de Java ou l'exécution des méthodes locales. Pour faciliter le travail de l'interpréteur, deux instructions HBP et HBLP ont été ajoutées. Elles branchent vers une fonction qui gère les exceptions logicielles (une fonctionnalité présente dans de nombreux langages, comme Java). La première effectue un branchement vers la fonction en question, la seconde sauvegarde l'adresse de retour avant de faire ce branchement. Les deux peuvent brancher vers 256 fonctions pré-déterminées, sans avoir à présenter leur adresse. ==La traduction binaire accélérée par le matériel== Plus haut, nous avons surtout parlé des compilateurs et des interpréteurs. Cependant, nous devons aussi parler de la '''traduction binaire'''. Elle traduit un programme écrit dans un code machine vers un autre code machine. Par exemple, elle traduit un programme compilé pour un CPU x86 vers un code machine ARM. En général, le système d'exploitation est généralement compilé pour le jeu d'instruction natif, mais il exécute des applications prévues pour le x86. Les applications sont traduites par le système d'exploitation, avant d'exécuter le code traduit. : Nous parlerons dans la suite de code machine source et de code machine cible, pour parler respectivement du code à traduire et du code obtenu après traduction. Idem avec d'autres termes comme architecture cible/source, ou jeu d'instruction cible/source. La traduction binaire est surtout utilisée pour des questions d'émulation ou de compatibilité. Par exemple, lorsque les Macintosh sont passés de processeurs Power PC vers des processeurs x86, le système d'exploitation Mac OS utilisait la traduction binaire pour convertir les anciennes applications Power PC vers du code x86. Les utilisateurs n'y ont vu que du feu. Le système de traduction binaire était appelé Rosetta 1. Par la suite, lors de la transition de processeurs x86 vers des processeurs Apple, Rosetta 2 a vu le jour. La traduction binaire a surtout été utilisée pour traduire du code x86 vers un autre jeu d'instruction. Il faut dire que le x86 est le jeu d'instruction dominant. De nombreuses entreprises ont eu pour ambition de briser l'hégémonie du x86 sur PC, en remplaçant le x86 par un jeu d'instruction plus performant, tout en gardant une compatibilité maximale. La traduction binaire était la seule solution pratique. Elle était le plus souvent intégralement réalisée en logiciel, comme c'était le cas sur les architectures Itanium avec le ''IA-32 Execution Layer''. ===Les généralités sur la traduction binaire assistée en matériel=== L'architecture source est presque tout le temps une architecture CISC, assez ancienne, qu'on souhaite émuler. Le choix de l'architecture cible se porte souvent sur une architecture VLIW, fort différente de l'architecture source, et ce pour obtenir une économie de matériel conséquente. Pour s'exécuter rapidement, le code traduit doit exploiter le parallélisme d'instruction, à savoir exécuter plusieurs instructions en même temps dans des unités de calcul séparées. Il est possible d'utiliser un processeur superscalaire avec exécution dans le désordre pour cela, mais au prix d'un cout important en transistors. Alors qu'en utilisant un CPU VLIW, c'est le traducteur binaire qui fait tout le travail d'extraction du parallélisme d'instruction. Et après tout, quitte à avoir un traducteur binaire, autant lui refiler le boulot d'optimisation. Compiler du code à la volée est certes assez lourd, mais qu'il existe des algorithmes efficaces pour regrouper des instructions indépendantes dans une seule instruction VLIW. Au passage, le processeur VLIW a plus de registres que le processeur source. Cela permet de faire du renommage de registres directement en logiciel. Le traducteur binaire n'hésite pas à changer les noms de registres entre instructions source et instruction VLIW cible, afin de supprimer des dépendances de données. En conséquence, cela ouvre des opportunités de parallélisme, qui sont exploitées lors du regroupement des opérations en instructions VLIW. Vous pourriez penser que le choix d'un CPU VLIW pour émuler du CISC est tout sauf optimal. Vous devez penser que la traduction binaire est d'autant plus simple que l'architecture source et cibles sont semblables. Dans les faits, ce n'est pas tellement traduire les instructions qui pose problème, mais plus la gestion du registre d'état, des exceptions matérielles, de la mémoire virtuelle, la différence entre gros-boutisme et petit-boutisme, de même que des différences d'adressage pour les périphériques. Le premier problème est la gestion des conditions, notamment en présence d'un registre d'état. En pratique, la traduction binaire s'utilise pour traduire du code CISC vers du code RISC ou VLIW. Les architectures source ont donc un registre d'état, qui est mis à jour non seulement par des instructions de test, mais aussi des instructions arithmétiques. L'architecture cible n'a elle pas de registre d'état, mais des registres à prédicats. La traduction de l'un vers l'autre est alors quelque peu compliquée. Et elle est d'autant plus compliquée que le registre d'état est la source de dépendances d'instruction implicites, qui réduisent les performances. Par exemple, si une instruction arithmétique modifie le registre d'état, cela peut impacter l'exécution d'une instruction ultérieure, qui lit ce registre d'état. Pour éliminer ces fausses dépendances, le traducteur binaire doit renommer ce registre en logiciel et détecter les dépendances utiles. Et le compilateur doit gérer les cas où il y a beaucoup de distance entre les deux, voire les cas où l'instruction dépendante n'a pas encore été analysée par le compilateur. Un autre problème est lié à la gestion des exceptions matérielles, et précisément des exceptions précises. Pour rappel, une instruction VLIW regroupe plusieurs opérations, opérations qui correspondraient à une instruction machine sur un CPU pas VLIW. En conséquence, plusieurs instructions du langage machine "source" sont regroupées en une seule instruction VLIW. Et il faut tenir compte du cas où une instruction source lève une exception. Dans le code VLIW, cela signifie qu'une opération lève une exception, et il faut annuler partiellement l'instruction VLIW associée. Et par partiellement, on veut dire que seules les opérations suivantes dans l'ordre du programme source doivent être annulées, puis ré-exécutées. Et c'est un sacré casse-tête ! Les processeurs VLIW utilisent comme solution le mécanisme d'exceptions différées des processeurs EPIC, vu il y a quelques chapitres. Pour résumer, le code est exécuté par blocs d'instruction, délimités par des branchements ou tout autre limite/barrière pertinente dans le code. Un bloc de code est compilé en deux versions : une version rapide sans exceptions matérielles, une version lente qui gère les exceptions précises. La version rapide s'exécute sans exécuter les exceptions. Cependant, les exceptions matérielles sont enregistrées, pour être prises en compte à la toute fin du bloc de code. La version rapide est exécutée en premier et elle mémorise si une exception matérielle a eu lieu. Puis une instruction vérifie si une exception a eu lieu et décide quoi faire. Si aucune exception n'a eu lieu, elle passe à la suite du programme, les résultats du bloc de code sont définitivement acceptés. Mais si une exception a eu lieu, tout est annulé. Le processeur est remis dans l'état initial, puis le code est ré-exécuté instruction par instruction de manière à gérer l'exception correctement. ===La traduction par pré-décodage=== Une première solution serait de faire la traduction binaire dans le cache d'instruction. Lors d'un défaut de cache, le code chargé depuis la RAM est traduit en code machine cible. Et c'est ce code machine cible qui est mémorisé dans le cache d'instruction et exécuté par le processeur. La technique marche sur le papier et n'est qu'une amélioration des techniques de pré-décodage vues il y a quelques chapitres. Cependant, le code obtenu est une traduction assez basique, qui n'incorpore pas d'optimisations dignes de ce nom. De plus, elle gère mal le cas où la taille du code source et cible sont potentiellement très différentes. Concrètement, elle est surtout utile pour traduire du code d'un processeur RISC vers un autre processeur RISC, dont les tailles d'instructions sont similaires. Elle ne permet pas d'émuler plusieurs jeux d'instructions différents, le cout en matériel (un circuit de pré-décodage par jeu d'instruction) serait trop important. ===Le projet DAISY d'IBM=== De très rares processeurs étaient conçus pour accélérer cette traduction binaire, afin de garder de bonnes performances. Les premiers à avoir étudié l'idée étaient IBM, avec leur projet DAISY (''Dynamically Architected Instruction Set from Yorktown''). Le projet de base était de convertir à la volée du code compilé pour des CPU Power PC, vers du code VLIW. Les chercheurs d'IBM avaient développé un algorithme de traduction dynamique efficace, ainsi qu'un processeur VLIW disposant d'optimisations spécifiques à la traduction binaire dynamique, à la volée. De nombreuses idées de ce projet ont été reprises ou re-découvertes par la société Transmetta, avec ses processeurs Crusoe et Efficieon, puis par NVIDIA avec son projet Denver. Les ingénieurs de ce projet ont étudié la possibilité d'émuler plusieurs jeux d'instructions différents sur un même processeur, notamment le s390 d'IBM et le x86. En théorie, cela demande juste d'avoir plusieurs programmes de traduction binaire : un pour le x86, un autre pour le s390, éventuellement un autre pour le Power PC. Mais cela ne s'est pas concrétisé. Mais les ingénieurs ont étudié quelle pourrait être l'architecture VLIW idéale pour ça. Par exemple, il fallait des additions 3-opérandes pour simplifier les calculs d'adresse. Une difficulté était la gestion du registre d'état, dont les bits ne sont pas mis à jour de la même manière sur le x86 et le s390 ou le Power PC. La différence entre gros-boutisme et petit-boutisme était aussi un problème, de même que des différences d'adressage pour les périphériques. Pour que la traduction binaire soit efficace, le processeur VLIW intègre diverses optimisations, comme les exceptions différées et les branchements multi-voies. La principale est la suivante : seul le code des boucles ou fonctions exécutées fréquemment est traduit en code VLIW, le reste du code est interprété. En effet, traduire du code binaire prend plus de temps qu'une simple interprétation. Pour du code qui ne sera exécutée qu'une seule fois, il est plus rapide d'utiliser l'interprétation. Par contre, pour du code exécuté beaucoup de fois, le cout de la traduction binaire est amorti, dilué sur N exécutions, compensé par le gain en temps d'exécution de ce code traduit. Au final, cela permet de ne traduire que le code qui le mérite. La détection du code fréquemment exécuté est réalisée dans le cœur VLIW. L'unité de branchement mémorise les derniers branchements rencontrés et le nombre de fois qu'ils ont été exécutés. Si ils ont été exécutés un certain nombre de fois, l'unité de branchement lève une exception matérielle, qui invoque le traducteur binaire. La routine de cette exception est le traducteur binaire proprement dit. En clair, la traduction binaire est démarrée quand le processeur détecte qu'une fonction a été exécutée plus de N fois, via une exception matérielle dédiée. La mesure du nombre d'exécution d'un branchement se fait dans l'unité de calcul dédiée aux branchements, avec l'aide d'une sorte de ''branch adress buffer'' modifié. Pour rappel, le ''branch adress buffer'' mémorise le ''Program counter'' de chaque branchement récemment rencontré. Ici, chaque entrée du ''branch adress buffer'' est associée à un compteur incrémenté à chaque exécution du branchement. ===Les processeurs Crusoe et Efficieon de Transmetta=== Les processeurs Crusoe Et Efficeron sont deux processeurs VLIW produits par la société Transmetta, une société californienne rachetée par NVIDIA. Ils étaient conçus pour exécuter spécifiquement des programmes x86, système d'exploitation inclus. L'idée derrière ce projet était d'exécuter du code x86 sans que les applications, ni même le système d'exploitation et le BIOS soient au courant ! Les processeurs Transmetta étaient en réalité conçus pour exécuter un programme unique, le ''Code Morphing Software'' (CMS), qui traduisait le code x86 en code VLIW. Transmetta ne voulait pas que son processeur VLIW soit exploité directement, sans le CMS. Il n'avait pas rendu public de compilateur pour traduire du C vers du code VLIW, il n'avait pas donné la documentation du jeu d'instruction VLIW. Des efforts de rétro-ingénieurie, documentés sur le site realworldtech, ont cependant permis de comprendre comment étaient encodées les instructions du processeur Crusoe. Comme pour le projet Daisy, le ''Code Morphing Software'' utilisait à la fois interprétation et traduction binaire, selon les besoins. Les instructions sont interprétées lors de leurs premières exécutions, mais le CMS bascule sur de la traduction en code VLIW après un certain nombre d'exécution. Ainsi, les boucles souvent exécutées sont traduites en code VLIW, alors que le reste du code est interprété. Le choix du code à traduire est le fait du CMS, il utilise des heuristiques complexes pour, qui ne sont pas connues dans le détail. Le ''Code Morphing Software'' est mémorisé dans une EEPROM, ce qui en fait un ''firmware'' situé en dessous du BIOS. Le CMS démarre ensuite le BIOS, qui lui-même démarre le système d'exploitation, qui lui-même démarre les pilotes de périphériques et les programmes. Le CMS se réserve les 16 premiers mébioctets de l'espace d'adressage. L'EEPROM du CMS est mappée dedans, mais n'en utilise que la moitié. Le reste est utilisé comme cache, pour mémoriser le code VLIW traduit par le CMS. Les deux processeurs intégraient aussi deux mémoires SRAM, utilisées par le CMS, appelées la ''local program memory'' (LPM) et la ''local data memory'' (LDM). La première contient du code qui gére les interruptions, la mémoire virtuelle, les exceptions matérielles, les problèmes d'alignement mémoire, et quelques fonctions très fréquemment utilisées par le CMS. La seconde est de la mémoire RAM utilisée par ces fonctions de la ''local program memory''. Les mémoires caches L1 et L2 sont séparées de ces deux mémoires. Le processeur Crusoe et Efficieon étaient des processeurs VLIW très simples. Ils n'avaient même pas de MMU, ni de fonctionnalités importantes sur les CPU x86. Pour Crusoe, les instructions VLIW étaient encodées sur 64 ou 128 bits, et regroupaient entre 2 à 4 opérations. Les instructions LOAD/STORE ne supportaient pas d'adressage indicé, juste de l'adressage indirect à registre. Crusoe avait 5 unités de calcul : deux ALU entières, une unité LOAD/STORE, une unité de branchement, une FPU. Efficieon doublait le nombre d'ALU entière et d'unité LOAD/STORE, les instructions VLIW passaient à 256 bits. Le processeur Crusoe contient 160 registres, dont 64 registres généraux, 32 registres flottants. Les registres généraux font 32 bits, ce qui est cohérent avec le fait que les CPU x86 de l'époque étaient des processeurs 32 bits. Quant aux registres flottants, ils faisaient 80 bits, ce qui colle avec la taille des registres flottants de la FPU x87 utilisée à l'époque. Sur les 64 registres généraux, seuls 48 étaient réellement utilisable pour mémoriser des opérandes. Une partie des registres généraux étaient utilisés pour la gestion de la pile, d'autres pour mémoriser l'état du CPU x86 émulé, un registre était un registre zéro non accesible en écriture. La gestion des exceptions est optimisée avec un système d'exceptions différées, le même que celui décrit plus haut. Le processeur utilise un système similaire, pour effectuer des lectures anticipées, qui a été expliqué dans le chapitre sur les processeurs VLIW/EPIC. Sauf qu'il s'agit là d'une optimisation pour gagner en performances en général, pas une aide utile pour gérer des exceptions précises ou toute autre subtilité de la traduction binaire. Pour gérer les exceptions différées et les lectures anticipées, le processeur mémorise l'état du processeur avant de démarrer un bloc de code. Pour cela, le processeur contient des ''shadow registers'', 48 registres entiers et 16 registres flottants. Vous remarquerez qu'il y a autant de registres entiers que de ''shadow registers'' entiers. Les registres entiers/flottants sont mémorisés dans les ''shadow registers'' avant d'exécuter un bloc de code, afin que le processeur puisse revenir à l'état de base. Une autre fonctionnalité liée est le ''gated store buffer''. Le principe est d'éviter toute écriture en mémoire RAM, tant que le code VLIW n'a pas émis d'instruction ''commit''. L'idée est que les écritures sont conservées dans la ''store queue'' de l'unité LOAD/STORE. Pour rappel, la ''store queue'' met en attente les écritures soit tant que les écritures ne sont pas terminées, soit tant que la RAM est occupée. L'idée est que les écritures sont accumulées dans la ''store queue'' tant que l'instruction ''commit'' n'a pas eu lieu. Le ''store queue'' est "déversé dans la RAM" seulement quand l'instruction ''commit'' s'exécute. ===Les CPU NVIDIA Denver=== Nvidia a tenté quelque chose de similaire avec son projet Denver. NVIDIA a racheté l'entreprise Transmetta, ses brevets, et a tenté de refaire la même chose. Le projet initial était de traduire du code x86 vers un jeu d'instruction VLIW propriétaire, comme l'a fait Transmetta. Mais NVIDIA n'a pas réussi à acquérir la licence du jeu d'instruction x86 et s'est rabattu sur le jeu d'instruction ARM. Le processeur né de ce projet est le Tegra K1-64 CPU. Il s'agit d'un processeur composé d'un cœur VLIW, couplé à deux décodeurs d'instructions ARM. Les deux décodeurs ARM traduisent une paire d'instructions ARM consécutives en instructions VLIW, pour les exécuter sur le cœur VLIW. Le CPU peut fonctionner selon deux modes : ARM et VLIW. En somme, le processeur supporte deux jeux d'instruction : le jeu d'instruction ARM, et un jeu d'instruction VLIW. En mode ARM, les instructions ARM sont chargées depuis le cache, traduites en VLIW par les décodeurs ARM, puis exécutées par le cœur VLIW. En mode optimisé, le CPU exécute des instructions VLIW chargées depuis le cache d'instruction, les décodeurs ne sont pas utilisés. : Le CPU supporte en réalité deux jeux d'instruction ARM : ARM8 et 7. Mais ils correspondent au même mode pour le CPU. Un point important est que le processeur peut changer très rapidement de mode, en à peine quelques cycles d'horloges. On n'est clairement pas dans le cas des CPU x86, qui mettent des plombes pour passer du mode 32 à 64 bits et inversement. La commutation est tellement rapide qu'on peut considérer que le processeur supporte deux jeux d'instruction simultanément. Et le support simultané de deux jeux d'instruction facilite l'implémentation de la traduction binaire. Encore une fois, inutile de traduire du code qui ne sera exécutée qu'une seule fois, mieux vaut l'exécuter directement dans le mode ARM. Surtout que le processeur supporte un mode ARM qui permet d'exécuter du code ARM sans perte de performance. Par contre, le code fréquemment utilisé, notamment dans des boucles critiques, est traduit en VLIW. Au final, les performances sont optimisées, en limitant le travail du logiciel traducteur binaire. La détection du code fréquemment exécuté se fait différemment que pour les processeurs Transmetta et Daisy. L'unité de branchement ne détecte pas directement le code beaucoup exécuté, même si elle a un rôle à jouer. A la place, la détection est réalisée en logiciel, par un ''thread'' dédié, qui s'exécute sur un cœur séparé. Il détecte le code fréquemment utilisé en regardent les branchements exécutés récemment. Pour cela, le processeur mémorise l'historique des branchements pris récemment et copie cette historique dans une mémoire tampon dédiée, partagée entre tous les cœurs. Vu qu'elle est partagée, le ''thread'' de détection a accès à l'historique sans pertes de performances, seule la copie de l'historique dans ce tampon a un cout en performance, pas son partage. Le code VLIW obtenu après traduction binaire est mémorisé dans la mémoire RAM, dans une portion spécialement réservée pour. Elle fait 128 mébioctets, et est appelée le '''cache d'optimisation''' par NVIDIA. Le cache d'optimisation est protégé en écriture et seul le traducteur binaire peut écrire dedans. Au passage, le traducteur binaire est du code VLIW, ce qui fait qu'il est placé dans le cache d'optimisation. Le processeur détecte automatiquement quand une fonction pour laquelle le code compilé équivalent est disponible. Pour cela, l'unité de chargement contient une table de correspondance entre l'adresse de la fonction ARM, et l'adresse de son équivalent compilé. Quand une fonction est appelée, l'unité de chargement regarde l'adresse de destination du branchement. Si l'adresse est dans cette table, elle récupère l'adresse de la fonction compilée et branche vers celle-ci. Le processeur dispose d'un cache d'instruction de 128 kibioctets, ce qui est très important, mais nécessaire vu la taille des instructions VLIW. Le coeur VLIW dispose de ses propres décodeurs, d'un ''scoreboard'', de 7 unités de calcul et de registres. Pour les unités de calcul, il a deux FPU, deux ALU, une unité de branchement, et deux unités LOAD-STORE faisant aussi ALU. Un point important est que le CPU dispose de 64 registres entiers et de 64 registres flottants. C'est deux fois plus que supporte le jeu d'instruction ARM. En mode ARM, seule la moitié des registres est utilisée. Mais en mode optimisé, le traducteur binaire utilise bien les 64 registres, grâce à une sorte de renommage de registres logicielle. Pour simplifier la traduction binaire, le processeur supporte les techniques vues précédemment. Le processeur intègre un ''gated store buffer'' similaire à celui des processeurs Transmetta. Les techniques d'exceptions différées sont aussi supportées, comme sur les processeurs Transmetta. Il a aussi une unité de préchargement décrite par NVIDIA comme agressive, avec support du préchargement de type anticipé (''runahead''). ===L'émulation de la mémoire virtuelle=== Pour finir, il faut parler de la mémoire virtuelle avec la traduction binaire. Sur le processeur source, le processeur gère à la fois des adresses physiques et virtuelles et sa MMU fait la conversion entre les deux. Sur le processeur VLIW, la MMU est simulée par le programme de traduction binaire, partiellement ou totalement. L'implémentation exacte varie suivant que l'on parle du projet Daisy, des processeurs Transmetta ou de Denver. Sur les processeurs Transmetta, il n'y a pas de MMU ni de mémoire virtuelle. A la place, le programme de traduction binaire émule la mémoire virtuelle du processeur source. Le processeur VLIW ne gère que des adresses physiques, rien d'autre. L'espace d'adressage physique du processeur VLIW a la même taille que celui du processeur émulé, ici des processeurs x86. Il est vraisemblable que les adresses physiques utilisées par le processeur x86 sont les mêmes que celles du processeur VLIW. Sur le projet Daisy, le processeur VLIW gère la mémoire virtuelle via pagination, comme les processeurs émulés. Ce qui fait qu'il gère un espace d'adressage virtuel et un espace d'adressage physique. Pour éviter toute confusion, nous parlerons d'adresse physique/virtuelle VLIW pour les adresses physiques/virtuelles du processeur VLIW, d'adresse virtuelle/physique source pour celle du jeu d'instruction traduit, à savoir du Power PC ou du s390, éventuellement de l'x86. Il faut alors faire le lien entre adresses physiques source et adresses virtuelles VLIW. Pour cela, rien de plus simple : il y a correspondance parfaite. L'adresse physique source numéro N correspond à l'adresse virtuelle VLIW numéro N. Cependant, cela signifie que tout l'espace d'adressage virtuel VLIW serait utilisé par le code à traduire. En réalité, il faut ajouter de la place pour le code traduit et le programme de traduction binaire. La conséquence est que l'espace d'adressage virtuel VLIW est plus large que l'espace d'adressage source. L'espace d'adressage virtuel VLIW est découpé en trois sections : une section pour le code Power PC à traduire, une autre pour le code traduit en VLIW, et une dernière réservée au traducteur binaire. Le programme de traduction est placé dans une ROM mappée en mémoire dans la seconde section. Le reste de la seconde section est réservé à une mémoire RAM utilisée par le programme de traduction. [[File:Mémoire virtuelle sur le projet Daisy.png|centre|vignette|upright=1.5|Mémoire virtuelle sur le projet Daisy]] Il faut noter que chaque page physique source, une fois traduite, correspond à N pages virtuelles VLIW. La raison est que le code traduit prend plus de place que le code machine originel. En conséquence, on doit utiliser une page finale plus grande. Pour le reste, des pages contiguës en mémoire physique source sont elles aussi contiguës en mémoire virtuelle VLIW. Le calcul d'adresse est donc simple : l'adresse physique source est multipliée par N, puis on ajoute l'adresse de base à laquelle commence la section pour le code traduit. ==Les processeurs à jeu d'instruction reconfigurable== Nous venons de voir comment accélérer en matériel la traduction binaire. Mais il existe une alternative qui permet de se passer de traduction binaire, complétement. L'idée est d'avoir un processeur avec un '''jeu d'instruction reconfigurable'''. Le nom est assez parlent : on peut configurer le processeur de manière à supporter le jeu d'instruction de son choix ! Par exemple, on pouvait reconfigurer un CPU de manière à ce qu'il supporte le jeu d'instruction x86, ou qu'il exécute du ''bytecode'' Java, ou du code ARMv8, etc. De tels processeurs sont encore plus rare que ceux vu avant dans ce chapitre, mais ils ont existé ! Les processeurs de ce type avaient un ''microcode réinscriptible'', à savoir que c'était des processeurs microcodés pour lesquels on pouvait modifier le microcode. L'idée était qu'en changeant le microcode, on changeait le jeu d'instruction du processeur. Pour cela, le microcode était associé à une mémoire SRAM dans laquelle le microcode était copié au démarrage du CPU. Il y avait donc une micro-ROM et une micro-RAM. Les instructions étaient décodées en utilisant le microcode dans la micro-SRAM, la micro-ROM ne servait qu'au démarrage. En modifiant le contenu de la micro-SRAM, on pouvait altérer le microcode, voire le remplacer totalement. ===Le Burroughs B1700 : architecture externe=== Un exemple est celui du Burroughs B1700, et il est intéressant d'étudier en détail son architecture. Son architecture est décrite dans le livre "Interpreting Machines : Architecture and Programming of the Bl700/Bl800 Series", écrit par Elliott I. Organick et James A. Hinds. Une partie de ce qui vfa suivre est un très court résumé de ce livre. Il disposait de 4 registres généraux, nommés X, Y, T et L, tous de 24 bits. Les registres T et L étaient découpés en 6 sous-registres de 4 bits, nommés LA LB LC LD LE et LF pour le registre L, et TA TB TC TD TE et TF pour le registre T. Ils étaient adressables comme n'importe quel registre. Un point important est que le registre T avait des possibilités d’extraction spécifiques. Il était possible d'extrait N bits, placés n'importe où dans ce registre, et de les copier dans un autre registre. Nous verrons pourquoi cette possibilité est intéressante et ne sert que pour le registre T. Les registres X et Y étaient reliés à une ALU de 24 bits capable d'effectuer des opérations arithmétiques basiques. Fait étonnant, l'ALU faisait tous les calculs en même temps et fournissait ses résultats dans 7 registres : un registre SUM pour l'addition, un registre DIFFERENCE pour la soustraction, et un registre par opération bit à bit. Les registres pour les opérations bit à bit sont : CMPX et CMPY pour l'opération NOT sur chaque opérande, XANY pour le ET bit à bit, XEOY pour le XOR, XORY pour le OU logique. Les additions et soustractions pouvaient se faire sur des opérandes codés en binaire ou en BCD, suivant le code opération utilisé. A cela, il fallait ajouter deux registres de "résultat" MSKX et MSKY. Ils fournissaient un masque dépendant de la taille du résultat en bits. Cela permettait de gérer des données de 0, 4, 8, 12, 16, 20, et 24 bits, alors que l'ALU faisait 24 bits. Et cela n'était pas limité à 16 ou 8 bits, mais n'importe quelle taille inférieure ou égale à 24 bits était supportée, même des tailles non-conventionnelles comme des résultats codés sur 3 bits, 9 bits, 12 bits, etc. L'ALU peenait en opérande les registres X et Y, mais aussi un registre CYF pour la retenue entrante. En sortie, il fournissait aussi deux retenue sortantes : une pour l'addition et une autre pour la soustraction. Elles étaient mémorisées dans les registres CYL et CYD, respectivement. L'ALU fournissait aussi 12 conditions, qui alimentait un registre d'état. Le registre d'état était segmenté en trois registres appelés XYST, XYCN et BICN. Le processeur contenait de nombreux autres registres de 4 bits, dont les sous-registres de T et L. En tout, il y a 27 registres de 4 bits adressables ! Et le processeur disposait d'une ALU 4 bits pour manipuler ces registres de 4 bits ! Elle est capable de faire des MOV entre registres de 4 bits, des opérations bit à bit, et peut aussi tester si un bit vaut 0 ou 1. La dernière possibilité est très utile pour implémenter les branchements. Pour communiquer avec les entrées-sorties, il disposait de deux registres CMND et DATA : CMND pour envoyer une commande à une entrée-sortie, DATA pour lire ou écrire une donnée. Pour la mémoire, il avait deux registres séparés : READ et WRITE. READ contient la donnée lue lors d'une lecture,; WRITE est pour une donnée à écrire lors d’une écriture. Il s'agit de registres d’interfaçage mémoire, qui ont été promus au rang de registres architecturaux. Les deux registres sont gérés par le microcode, qui est le langage principal de la machine. Les instructions machines configurées font sans, mais le microcode doit y avoir accès facilement. Et vu que le microcode est le langage principal de ce processeur, ces deux registres deviennent architecturaux. ===Une architecture bit-adressable=== Si avoir un microcode réinscriptible est suffisant, quelques choix de ''design'' facilitent l'émulation de jeux d'instructions variés. Par exemple, le CPU Burroughs B1700 avait ajouté une fonctionnalité très importante : il était bit-adressable. En clair, il pouvait adresser la mémoire bit par bit, si nécessaire. L'avantage est que cela permettait d'émuler des architectures dont la taille des registres/''byte''/mots était très différente. Un autre avantage est que cela facilitait la gestion des instructions de taille variable. Détaillons rapidement le premier avantage. Le processeur était un CPU 24 bits, mais il pouvait émuler des processeurs 16 bits, 8 bits, 4 bits, 1 bit, 9 bits, ou toute autre valeur. Pour cela, le processeur lisait des mots de 24 bits et utilisant un ''barrel shifter'' pour sélectionner les bits adéquats. Par exemple, pour un accès de 16 bits, la donnée lue en mémoire était masquée de manière à ne garder que les 16 bits de poids faible. Le ''barrel shifter''/circuit de masquage était placé directement avant le bus mémoire, sur le trajet des données/instructions. Le ''barrel shifter'' était commandé par un registre ''Field Unit'', qui précisait quelle était la taille des données à charger. ===La gestion de la taille des instructions=== Les instructions à décoder n'ont pas de longueur fixe, à savoir qu'elles peuvent faire 16 bit avec tel jeu d'instruction, 8 bits avec un autre, 32 bits sur un autre, etc. Elles peuvent aussi être de longueur fixe ou variable ! Le B1700 intégrait donc un support des instructions de longueur variable, qui était "détourné" pour gérer des jeux d'instruction différents. Notons que vu que l'architecture est bit-adressable, les contraintes d'alignement des instructions disparaissent, ce qui a des avantages qu'on va détailler dans ce qui suit ! L'implémentation des instructions de taille variable ajoutait un registre avant le registre d'instruction. Le registre d'instruction proprement dit était un registre M de 24 bits. Il était précédé par un registre T, dans lequel l'instruction était assemblée, en combinant plusieurs morceaux. Si l'instruction est codée est codée sur moins de 24 bits, le registre T se contente de masquer les bits en trop. Si l'instruction chargée dépasser 24 bits, le chargement se faisait en plusieurs passes, avec plusieurs lectures en mémoire. Dans ce qui suit, on suppose que l'instruction est composée de '''champs''' : l'opcode, les opérandes, les adresses absolues, les constantes immédiates, les numéros de registre, etc. L'idée est que l'instruction est chargée dans le registre T, puis découpée en champs, qui sont copiés dans les registres du processeur ou envoyés au microcode. Je rappelle que le registre T est relié à un ''barrel shifter'', différent au précédent, qui permettait de masquer, décaler et extraire une donnée de ce registre. Et il y a la possibilité d'extraire une suite de bit de ce registre T, pour la copier dans un autre registre. Par exemple, prenons une instruction composée d'un opcode, d'une constante immédiate et d'une adresse immédiate. L'opcode est lu et interprété par le microcode. Le microcode extrait la constante immédiate du registre T et la copie dans le registre X. Ensuite, l'adresse est extraite et envoyée au registre FA, une lecture est démarrée, et l'opérande est copiée dans le registre Y. Enfin, l'instruction est exécutée par le microcode. Le tout prend plusieurs micro-opérations : une première pour lire l'opcode, une seconde pour lire la première opérande, une troisième pour lire la seconde opérande, una quatrième pour tout exécuter. Notons que suivant la taille des champs, on peut en charger plusieurs à la fois. Par exemple, imaginons qu'on ait chargé 24 bits, contenant un opcode de 8 bits et une constante immédiate de 16 bits : pas besoin de faire deux accès mémoire, le registre T est juste découpé en un opcode et une constante. Le registre T est fait pour ce genre d'extraction. ===Le chargement d'un champ=== L'architecture est bit-adressable car elle gére des champs qui ne font pas forcément 8 ou 16 bits. Par exemple, certains jeux d'instructions ont des opcodes codés sur 6 ou 7 bits, voire 9-10 bits. Par exemple, prenons un opcode de 7 bits, suivi par une adresse de 12 bits et une constante immédiate de 15 bits : aucun de ces champs ne sera aligné sur 8/16/24 bits. Utiliser une architecture bit-adressable règle le problème d'alignement. Et en général, les contraintes d'alignement des instructions disparaissent ! Quand on charge une instruction, peut importe qu'elle soit ou non alignée sur 24 bits, elle sera placée dans les bits de poids faible, il suffira de masquer les bits de poids fort inutiles. Par exemple, si on charge une instruction de 16 bits, les 16 bits seront copiés dans les 16 bits de poids faible du registre T, il suffira de masquer l'octet de poids fort. Pas besoin de faire de décalages pour gérer des instructions mal alignées... Pour charger une instruction, il faut connaitre plusieurs informations : l'adresse de son premier bit, sa taille. Pour cela, le B1700 contient deux registres, nommés ''Field Adress'' (FA) et ''Field Length'' (FL), qui indiquent l'adresse du premier bit et la taille de l'instruction à charger. Le premier fait 24 bits, ce qui permet de gérer des adresses de 24 bits. N'oubliez pas que le processeur est bit-adressable, ce qui permet d'adresser 16 Mébi-bits, soit 2 mébioctets. Le registre pour la taille fait lui 16 bits, ce qui permet de gérer des instructions de 65536 bits, soit 8192 octets ! Leur nom commence par ''field'', car le processeur peut gérer des instructions de taille variable et/ou plus longues que 24 bits. Dans ce cas, le chargement de l'instruction se fait champ par champ. Pour donner un exemple, prenons une instruction composée de 20 champs de 12 bits chacun. Les champs sont chargés un par un dans le registre T. Le registre FA est alors initialisé avec l'adresse de l'instruction, il est incrémenté de 12 à chaque cycle. Le registre FL est initialisé avec la taille de l'instruction, soit 20 * 12 = 240 bits, et il est décrémenté de 16 à chaque cycle. Précisons cependant que la taille d'un champ peut être changée d'un cycle à l'autre. Par exemple, le processeur peut charger un opcode de 8 bits, l'interpréter, détecter que l'instruction demande ensuite de charger une adresse de 16 bits. Dans ce cas, le registre FL est immédiatement altéré pour charger 16 bits, au lieu des 8 bits de l'opcode immédiatement précédent. ===Le microcode réinscriptible=== Le microcode utilise des micro-opérations de 16 bits de long, ce qui est très peu et bien en-dessous des micro-opérations des autres processeurs. Les micro-opérations sortant du microcode sont envoyées au registre M, un registre de micro-opération de 16 bits. les micro-opérations écrites dans ce registre sont ensuite décodées par le hardware et exécutées. Fait intéressant, il était possible de faire un OU logique entre la micro-opération sortante et un registre du processeur, le résultat étant mémorisé dans le registre M. Cela permettait de faire du code automodifiant dans le microcode ! Utiliser du code automodifiant avec un microcode réinscriptible est parfaitement possible, et c'est utile pour émuler des modes d'adressage complexe dans le microcode. Cela permettait par exemple de remplacer une adresse dans une micro-opération. L'adresse est alors surimposée avec un OU dans le champ d'adresse, alors que le reste de l'instruction n'est pas modifié. Mais modifier directement le microcode est rarement une bonne idée. Aussi, pour éviter cela, l'idée est de modifier les instructions avec un OU logique avec un autre registre. Les micro-opérations du microcode ne sont donc pas modifiées dans le microcode, mais en sortie du microcode. La RAM mémorisant le microcode était de petite taille, et il arrivait qu'elle soit trop petit pour mémoriser le microcode complet. Dans ce cas, le programmeur devait gérer la situation en découpant le microcode en segments, certains étant placé dans le processeur, les autres étant en mémoire RAM. Le processeur peut lire le microcode placé en mémoire RAM et même l'exécuter, mais avec une pénalité en termes de performances. Quelques registres intégrés au processeur permettent ce genre de magie. Le microcode pouvait servir pour émuler un autre jeu d'instruction, mais il était aussi possible d'implémenter un véritable interpréteur, grâce aux techniques du paragraphe précédent. Le processeur était même conçu pour ! L'interpréteur était placé dans un "segment" en mémoire RAM, dont le processeur connaissait la position. Il incorporait pour cela deux registres, un pour l'adresse de base de l'interpréteur, un autre pour son adresse de fin. Il incorporait une protection mémoire limitée sur ce segment, à savoir que seul l'interpréteur pouvait lire ou écrire dans ce segment. Afin de gérer les interruptions, ces dernières étaient partiellement détectées en logiciel. Les interruptions ne s'exécutaient pas automatiquement, pour émuler correctement les interruptions. Une interruption doit être prise en compte à la toute fin de l'exécution d'une instruction, quand celle-ci a finit son travail. Mais vu qu'ici, les instructions sont émulées avec des séries de micro-opérations, il y a un problème : il est possible d'interrompre une instruction émulée en plein milieu. Pour éviter cela, le processeur ne déclenche pas les interruptions matérielles immédiatement, elles sont mises en attente. Une micro-opération dédiée vérifie si une interruption est en attente, et l'exécute alors cas échéant. La micro-opération est utilisée comme micro-opération finale d'une instruction émulée. ==Les jeux d'instructions dédiés à un langage de programmation== De rares processeurs sont conçus pour un langage de programmation en particulier. On appelle ces processeurs, conçus pour des besoins particuliers, des '''processeurs dédiés'''. Par exemple, les fameux ''Burrough E-mode'' B5000/B6000/B7000 étaient spécialement conçus pour exécuter de l'ALGOL-60. Leurs cousins B2000/B3000/B4000 étaient eux conçus pour le COBOL. Des langages fonctionnels ont aussi eu droit à leurs processeurs dédiés. Le prolog en est un bel exemple, avec les superordinateurs de 5ème génération qui lui étaient dédié. On peut aussi citer les machines LISP, dédiés au langage LISP, qui datent des années 1970. Elles étaient capables d’exécuter certaines fonctions de base du langage directement dans leurs circuits : elles possédaient notamment un ''garbage collector'' câblé dans ses circuits ainsi que des instructions machines supportant un typage déterminé à l’exécution. Les processeurs dédiés ont eu leur heure de gloire au début de l'informatique, à une époque où les langages de haut niveau venaient d'être inventés. À cette époque, les compilateurs n'étaient pas performants et ne savaient pas bien optimiser le code machine. Il était alors rationnel, pour l'époque, de rapprocher le code machine cible et le langage de programmation de haut niveau. De nombreuses architectures dédiés ont ainsi été inventées, avant que les concepteurs se rendent compte des défauts de cette approche. Les défauts en question ne sont pas nombreux, mais assez simples à comprendre. Premièrement, elles sont très rapides pour un langage de programmation en particulier, mais sont assez mauvaises pour les autres, d'où un problème de "compatibilité". Ajoutons à cela que les langages de programmation peuvent évoluer, devenir de moins en moins populaires/utilisés, ce qui rend la création d'architectures généralistes plus pertinente. Enfin, les architectures dédiées sont évidemment des processeurs CISC, pour implémenter les nombreuses fonctionnalités des langages évolués. Et les défauts des CISC sont assez rédhibitoires à l'heure actuelle. À l'heure actuelle, les algorithmes des compilateurs se sont améliorés et savent nettement mieux utiliser le matériel. Ils produisent du code machine efficace, ce qui rend les architecture dédiées bien moins intéressantes. Si on ajoute les défauts de ces architectures dédiées, par étonnant que les architectures dédiées aient presque disparues. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'accélération matérielle de la virtualisation | prevText=L'accélération matérielle de la virtualisation | next=Le matériel réseau | nextText=Le matériel réseau }} </noinclude> 8fkerq65lx4duvr35g0addka1kwffos 764028 764027 2026-04-19T18:32:46Z Mewtow 31375 /* Le microcode réinscriptible */ 764028 wikitext text/x-wiki De nos jours, la majorité des programmeurs programment dans des langages de haut niveau. Il est très rare pour eux d'avoir à utiliser de l'assembleur, et encore moins un langage machine. Les programmes écrits dans un langage de haut niveau sont traduits en langage machine par un logiciel appelé le compilateur. [[File:Compilation.PNG|centre|vignette|upright=2.5|Compilateur : principe.]] Il se trouve que quelques rares processeurs sont conçus pour faciliter le travail du compilateur. Et ce sont ces processeurs que nous allons voir dans ce qui suit. Ils représentent une catégorie de jeux d'instruction à part, qui n'a pas vraiment de nom. ==La compilation et l’interprétation== Un compilateur traduit donc du code source, écrit dans un langage de haut niveau, vers du langage machine. La traduction est rarement directe. En général, le compilateur traduit le code source en assembleur, qui est lui-même traduit en langage machine. L'assembleur est une représentation textuelle du langage machine alors que le code machine est du binaire exécutable par le processeur. La traduction se fait donc en deux étapes, la '''compilation''' proprement dite et l''''assemblage''', réalisées respectivement par un compilateur et un assembleur. Il faut parfois rajouter une troisième phase d'édition des liens, que nous passons volontairement sous silence. {| |[[File:Code C.png|vignette|200px|Le code source, ici en C.]] |[[File:Assem.png|vignette|250px|Le code assembleur.]] |[[File:Binary file - hello world (C programming).png|vignette|150px|Le code machine.]] |} ===L'interprétation et la compilation à la volée=== Une alternative à la compilation est l''''interprétation''', qui transforme le code source en code machine à la volée. Avec l'interprétation, le code source est passé à un logiciel appelé l''''interpréteur''', qui exécute le code source ligne de code par ligne de code. Une ligne de code est traduite en code machine, qui est exécutée, puis l'interpréteur passe à la ligne suivante. [[File:Kaantotulkkaus.png|centre|vignette|upright=3|Compilation versus interprétation.]] Un défaut de l'interprétation est son cout en performance, qui est assez important. Aussi, l'interprétation stricte a évolué vers un hybride entre interprétation et compilation. L'idée est qu'une partie peu importante du code source est interprétée, alors que le code important est compilé juste avant d'être exécuté. Pour le dire autrement, le code source est partiellement compilé à la volée, juste avant son exécution. Aussi, on parle de '''compilation à la volée'''. Le terme en anglais est ''Just In Time compilation'', abrévié en JIT. En soi, le JIT est associé à de l'interprétation, les JIT sont en réalité des hybrides interpréteurs-compilateurs. Un point important est que compiler du code à la volée est assez lourd, cela a un cout en performance. Par contre, le code compilé s'exécute plus vite que du code interprété. Si on exécute le code compilé une seule fois, le cout de la compilation l'emporte sur le gain à l'exécution. Mais le code compilé est réutilisé autant de fois que nécessaire. Le gain à l'exécution est donc multiplié par le nombre d'exécutions, alors que le cout de la compilation est répartit sur plusieurs exécutions, il est amorti. Les portions du code sont choisies de manière à avoir un gain en performance maximal. L'idée générale est que le code exécuté très souvent est compilé, alors que du code exécuté pas souvent est interprété. Typiquement, les boucles les plus souvent exécutées sont compilées, le code exécuté une seule fois est interprété. ===Les langages intermédiaires=== Les compilateurs modernes passent par un '''langage intermédiaire''' pour faire la transformation en code machine. Le compilateur traduit le langage de haut niveau en langage intermédiaire, puis traduit le langage intermédiaire en code machine cible. Le langage intermédiaire est parfois appelé le '''''bytecode''''', et ce terme recouvre aussi le code écrit avec du ''bytecode''. L'interprétation aussi peut passer par l'intermédiaire d'un un ''bytecode'', ce n'est pas limité aux compilateurs proprement dit. {| |[[File:Intermediate representation scheme.png|centre|vignette|upright=1.5|Représentation intermédiaire d'un compilateur.]] |[[File:Code interpreter scheme.png|centre|vignette|upright=1.5|Fonctionnement d'un interpréteur.]] |} Faire ainsi a de nombreux avantages, le principal étant d'avoir un compilateur capable de traduire un langage de haut niveau vers plusieurs jeux d'instructions différents. Cela permet d'avoir, par exemple, un compilateur qui traduit du C soit en code machine x86, soit en code machine pour un CPU ARM, soit pour un CPU POWERPC, etc. [[File:Compiler design.svg|centre|vignette|upright=3|Compiler design]] Pour l'interprétation, l'usage d'un ''bytecode'' a d'autres avantages. Pour l'exploiter, il faut distribuer non pas le code source, mais le ''bytecode''. En clair, l'interpréteur ne prend pas en entrée le code source, mais du ''bytecode'' déjà compilé. La traduction est alors beaucoup plus simple, car le code source a déjà été partiellement compilé, par la traduction en ''bytecode''. Interpréter du code source est en effet assez compliqué : il faut effectuer des étapes d'analyse lexicale, sémantique, et bien d'autres. Avec du ''bytecode'', ces étapes ont été réalisées lors de la compilation du ''bytecode'', l'interpréteur a alors peut de choses à faire. L'interprétation du ''bytecode'' se fait instruction par instruction, au niveau du ''bytecode''. Une instruction du ''bytecode'' est traduite en une instruction machine équivalente, qui est exécutée. Si l'instruction est un peu complexe, il exécute une fonction/procédure qui fait la même chose. Pour résumer, le ''bytecode'' est ensuite traduit à la volée et exécuté instruction par instruction par un logiciel appelé l'interpréteur. [[File:Control table.png|centre|vignette|upright=2|Control table]] Le langage intermédiaire peut être vu comme l'assembleur d'un processeur, n'existe pas forcément dans la réalité, mais dont le jeu d'instruction est décrit en détail. Le processeur en question est appelé une '''machine abstraite''', ou encore une ''machine virtuelle''. Nous utiliserons le terme de machine abstraite dans ce qui suit. La machine abstraite n'est pas la même suivant que l'on cible la compilation ou l'interprétation/JIT. Pour l'interprétation/JIT, la machine abstraite est souvent une machine à pile, car cela simplifie grandement la traduction en code machine final. L'interprétation demande que la traduction du ''bytecode'' en code machine soit la plus simple possible. Et cela demande de prendre en compte pas mal de détails. Par exemple, les différents jeux d'instruction existants n'ont pas le même nombre de registres, ce qui pose problème lors de la traduction du ''bytecode''. Lors de la transformation en code machine, un algorithme d'allocation de registres se débrouille pour traduire le code intermédiaire en code qui utilise un nombre limité de registres. Pour améliorer cette allocation de registres, il y a deux solutions niveau ''bytecode'' : soit utiliser un nombre illimité de registres, soit utiliser une machine à pile. Pour les machines à pile, il existe un algorithme simple et rapide pour traduire un code écrit pour une machine à pile en un code écrit pour un processeur avec des registres, qui se débrouille pas trop mal pour allouer efficacement les registres. C'est un avantage assez important pour les langages interprétés. ==Les processeurs qui exécutent du bytecode== Le ''bytecode'' est un code machine, ce qui signifie qu'il peut en théorie s'exécuter sur un processeur qui implémente le jeu d’instruction associé. Si le jeu d'instruction d'un ''bytecode'' est souvent une description censée être fictive, elle n'en reste pas moins un jeu d'instruction et des caractéristiques précises, qu'on peut l'implémenter en matériel ! Néanmoins, tous les ''bytecode'' ne sont pas égaux de ce point de vue. Certains sont faciles à implémenter en matériel, d'autres non. Les ''bytecodes'' implémentés en matériel sont ceux dont les machines abstraites sont des machines à pile. Un premier exemple est celui des processeurs Pascal MicroEngine, qui exécutaient directement le bytecode du langage Pascal, le fameux UCSD P-code. Un second exemple est la '''machine SECD''', qui sert de langage intermédiaire pour certains compilateurs de langages fonctionnels. Elle a été implémentée en matériel par plusieurs équipes, notamment par les chercheurs de l'université de Calgary en 1989. Dans le même genre, quelques processeurs simples étaient capables d’exécuter directement le ''bytecode'' du langage FORTH. Le FORTH, un des premiers langages à pile de haut niveau, possède de nombreuses implémentations hardware et est un des rares langages de haut niveau dont le ''bytecode'' a été utilisé comme langage machine sur certains processeurs. Par exemple, on peut citer le processeur FC16, capable d’exécuter nativement du ''bytecode'' FORTH. ===Les processeurs Java=== Le cas le plus impressionnant est celui de la machine virtuelle Java, qui est un design de processeur comme un autre. En temps normal, le ''bytecode'' Java est compilé ou interprété, mais certains processeurs exécutaient du ''bytecode'' Java directement, sans interprétation ni compilation. Ils sont appelés des '''processeurs Java'''. On peut citer les processeurs ARM disposant de l'extension Jazelle, du JEMCore et du aJ-100 de aJile Systems, le picoJava II de Sun, et quelques autres. Il y a aussi eu quelques projets de recherche ou processeurs open source, comme Komodo, jamuth, le ''Java Optimized Processor'' et quelques autres projets du même genre. L'implémentation de la machine virtuelle Java n'était cependant pas complète. Quelques instructions complexes n'étaient pas gérées par le processeur et devaient être émulées en logiciel. Mais la grosse majorité des instructions l'était. Les instructions non-supportées étaient émulées avec la méthode ''trap and emulate'', à savoir qu'un opcode inconnu déclenchait une exception matérielle ''opcode invalide'', dont la routine pouvait émuler les instructions qui doivent l'être. Le processeur aJ-100 de aJile est capable d'exécuter la quasi-totalité des instructions de la JVM Java, seules deux instructions faisant exception. Les '''accélérateurs Java''' sont une solution intermédiaire, qui permet de traduire à la volée du ''bytecode'' Java en instructions RISC, exécutées sur un autre processeur. Il s'agit formellement de traduction binaire, mais je préfère en parler ici. L'idée est de combiner un processeur RISC avec un coprocesseur Java. Le coprocesseur lit le ''bytecode'' Java et le traduit en instructions RISC, ces dernières étant exécutées sur le processeur RISC. Le JA108 de Nozomi était un coprocesseur de ce genre, au même titre que le coprocesseur JSTAR de JEDI. L'extension Jazelle de certains CPU ARM fonctionnait vraisemblablement sur un principe similaire, sauf que le coprocesseur était intégré dans le processeur ARM, entre le cache d'instruction et le décodeur d'instructions ARM. L'intérêt des processeurs Java n'est pas qu'une question de performance. Certes, ils permettent d’exécuter plus vite les programmes compilés en ''bytecode'', comme des programmes Java pour la JVM Java. Mais l'intérêt est aussi de faire des économies de mémoire RAM et ROM. Je rappelle que la machine virtuelle Java est une machine à pile, ce qui fait que sa densité de code est excellente. Les programmes en ''bytecode'' Java sont donc très petits, et prennent peu de place en mémoire ROM. De plus, utiliser un processeur Java permet de se passer d'interpréteur ou de JIT Java, ce qui économise encore plus de mémoire. ===Transformer un processeur RISC en machine à pile=== La plupart des processeurs Java utilisent en interne une architecture à registre généraux, qui émule une machine à registre en microcode. Quelques registres sont réservés pour l'état de la machine virtuelle, avec au minimum un registre réservé pour le pointeur de pile, un autre pour un pointeur vers un ''pool'' de constantes, et quelques autres. D'autres registres servent à mémoriser le haut de la pile, à savoir le sommet de la pile et quelques opérandes situées juste en-dessous. Par exemple, le processeur picoJava II gardait les 64 opérandes au sommet de la pile dans 64 registres généraux, séparés des registres pour les pointeurs et l'état de la JVM. Une optimisation possible, envisagée sur les processeurs picoJava, était une sorte de macro-fusion sous stéroïde. L'idée était de fusionner certaines séries d'instructions Java en une seule instruction machine. Typiquement, pour faire une opération arithmétique, une machine à pile empile deux opérandes et exécute une opération, ce qui prend trois instructions machine. La spécification picoJava 1 a proposé d'envoyer directement la seconde opérande en entrée de l'ALU, ce qui fusionnait l'empilement de la seconde opérande et l'opération arithmétique. Le processeur picoJava II va plus loin et fusionne les trois instructions en une seule instruction RISC, agissant sur des registres. Mais la majorité des processeurs Java ne faisaient pas cela, car cela complexifie grandement le décodeur. Les processeurs ''bigfoot'' de DCT utilisaient un système différent pour émuler une pile avec des registres. Leur banc de registre de 64 bits était coupé en deux : les 16 premiers registres étaient utilisés pour la pile, les 48 suivants étaient utilisés pour autre chose. L'idée est que le registre 0 mémorise un emplacement vide, le registre 1 contient le sommet de la pile, le registre 2 l'opérande sous le sommet de la pile, et ainsi de suite. Les 15 premières opérandes de la pile sont donc mémorisées dans les registres. Les instructions écrivent leur résultat dans le registre adéquat, puis tous les registres sont décalés pour que le résultat passe dans le registre R1. Par exemple, les instructions PUSH chargent une opérande dans le registre 0, puis tous les registres sont décalés de un rang, pour que l'opérande chargée soit dans le registre 1. Le registre 0 devient alors le registre numéro 1, le registre 1 devient le 2, et ainsi de suite. Par contre, les opérations arithmétiques font autrement. Une instruction d'addition, par exemple, lit les opérandes dans le registre 1 et 2, puis place son résultat dans le registre 2 (les deux opérandes sont dépilées). Puis, les registres sont décalés d'un rang pour que ce registre 2 devienne le 1. Les registres ne sont en réalité pas décalés, il n'y a pas de transferts entre registres. A la place, les 16 premiers registres utilisaient une forme limitée de renommage de registres, similaire au banc de registre tournant utilisé sur les processeurs EPIC comme l'Itanium. Les numéros de registres sont renommés, décalés, à chaque fois qu'on empile ou dépile une opérande. Pour faire le renommage de registres, processeur contient un ''register stack counter'' de 4 bits, qui est incrémenté à chaque fois qu'on empile une donnée, et est décrémenté quand on dépile une donnée. Quand on veut accéder à un opérande, le numéro de registre architectural associé est traduit en numéro de registre physique, en additionnant le ''register stack counter'' (modulo 16, pour rester sur 4 bits). ==L'accélération de l'interprétation/JIT : le ''Thumb-EE'' d'ARM== Quelques processeurs ont ajouté des instructions pour faciliter le travail des interpréteurs/JIT. Par exemple, en 2005, ARM a ajouté le mode ''Thumb Execution Environment'', qui faisait cela. Il reprenait le jeu d'instruction compact ''thumb'' et ajoutait quelques instructions et modifiait le comportement d'instructions existantes. Le jeu d'instruction ''Thumb-EE'' a été ajouté sur les CPU ARM en 2005, mais a été déprécié en 2011, par manque d'utilité. Mais la tentative mérite qu'on s'attarde dessus. Les CPU de gamme M, déstinés à l'embarqué, n'ont jamais supporté ''Thumb-EE'', vu que de tels processeurs ne sont pas conçus pour exécuter du code interprété/JIT. Le mode ''Thumb-EE'' est un mode d'exécution, séparé de l'ARM normal. Ainsi, une instruction ''Thumb-EE'' et une instruction ARM peuvent avoir le même encodage en binaire, mais se comporter différemment. Le processeur est à tout moment soit en mode ''Thumb-EE'', soit en mode ARM normal, ce qui précise comment ces instructions doivent se comporter. Des instructions ont été ajoutées par ''Thumb-EE'', pour qu'il fasse son travail. L'entrée et la sortie du mode ''Thumb-EE'' se fait avec deux instructions : ENTERX et LEAVEX. Une fois en mode ''Thumb-EE'', le décodage des instructions ''thumb'' se fait avec les règles du ''Thumb-EE'', et non celles du ''thumb'' normal. Une première différence entre ''thumb'' normal et ''Thumb-EE'' est l'ajout de ce qui s'appelle un ''null check'' pour les instructions mémoire. Le ''null check'' est utilisé pour les instructions en mode d'adressage "base + indice" ou "Base + décalage", ou tout autre mode d'adressage avec un registre de base utilisés dans des calculs d'adresse. L'idée est de vérifier si l'adresse de base vaut zéro ou non. Si c'est le cas, le processeur lève une exception matérielle, qui est traitée par l'interprétation ou le compilateur JIT. Une telle situation est en effet signe d'une erreur d'adressage mémoire, qui doit être traitée par l'interpréteur. Typiquement, c'est signe qu'un pointeur n'a pas bien été initialisé, ce qui peut arriver avec du code interprété ou JIT. Les interpréteurs/JIT traitent généralement la situation en ajoutant un test avant toute instruction mémoire, pour vérifier si le pointeur accédé vaut zéro ou non. Avec ''Thumb-EE'', pas besoin d'ajouter les tests en questions : ils sont réalisés automatiquement lors de chaque instruction mémoire. : Les registres pour la pile, comme le pointeur de pile et le pointeur de ''frame'', sont aussi concernés. L'instruction CHKA vérifie que les accès à un tableau ne débordent pas en dehors du tableau en question. Par exemple, pour un tableau de 1024 éléments, elle vérifie si l'indice est compris entre 0 et 1024. Pour cela, elle vérifie si l'indice de tableau est bien dans l'intervalle adéquat et lève une exception matérielle si ce n'est pas le cas. Son utilité se comprend quand on sait que tous les accès à un tableau sont vérifiés par l'interpréteur. L'interpréteur est censé ajouter des instructions pour vérifier les indices, à savoir deux ou trois branchements. Avec CHKA, le test se fait en une seule instruction. D'autres modifications mineures de l'encodage des instructions sont aussi présentes, ainsi que des modifications pour les instructions LOAD/STORE. Précisément, dans les modes "base + indice" et "base + décalage", le décalage et l'indice subissent maintenant des décalages. L'idée est de simplifier le calcul d'adresse. De quoi économiser quelques instructions lors des calculs d'adresse, ce qui facilite le travail de l'interpréteur/JIT. Et des instructions LOAD/STORE précises voient leur comportement modifié pour faciliter la gestion de la pile de Java ou l'exécution des méthodes locales. Pour faciliter le travail de l'interpréteur, deux instructions HBP et HBLP ont été ajoutées. Elles branchent vers une fonction qui gère les exceptions logicielles (une fonctionnalité présente dans de nombreux langages, comme Java). La première effectue un branchement vers la fonction en question, la seconde sauvegarde l'adresse de retour avant de faire ce branchement. Les deux peuvent brancher vers 256 fonctions pré-déterminées, sans avoir à présenter leur adresse. ==La traduction binaire accélérée par le matériel== Plus haut, nous avons surtout parlé des compilateurs et des interpréteurs. Cependant, nous devons aussi parler de la '''traduction binaire'''. Elle traduit un programme écrit dans un code machine vers un autre code machine. Par exemple, elle traduit un programme compilé pour un CPU x86 vers un code machine ARM. En général, le système d'exploitation est généralement compilé pour le jeu d'instruction natif, mais il exécute des applications prévues pour le x86. Les applications sont traduites par le système d'exploitation, avant d'exécuter le code traduit. : Nous parlerons dans la suite de code machine source et de code machine cible, pour parler respectivement du code à traduire et du code obtenu après traduction. Idem avec d'autres termes comme architecture cible/source, ou jeu d'instruction cible/source. La traduction binaire est surtout utilisée pour des questions d'émulation ou de compatibilité. Par exemple, lorsque les Macintosh sont passés de processeurs Power PC vers des processeurs x86, le système d'exploitation Mac OS utilisait la traduction binaire pour convertir les anciennes applications Power PC vers du code x86. Les utilisateurs n'y ont vu que du feu. Le système de traduction binaire était appelé Rosetta 1. Par la suite, lors de la transition de processeurs x86 vers des processeurs Apple, Rosetta 2 a vu le jour. La traduction binaire a surtout été utilisée pour traduire du code x86 vers un autre jeu d'instruction. Il faut dire que le x86 est le jeu d'instruction dominant. De nombreuses entreprises ont eu pour ambition de briser l'hégémonie du x86 sur PC, en remplaçant le x86 par un jeu d'instruction plus performant, tout en gardant une compatibilité maximale. La traduction binaire était la seule solution pratique. Elle était le plus souvent intégralement réalisée en logiciel, comme c'était le cas sur les architectures Itanium avec le ''IA-32 Execution Layer''. ===Les généralités sur la traduction binaire assistée en matériel=== L'architecture source est presque tout le temps une architecture CISC, assez ancienne, qu'on souhaite émuler. Le choix de l'architecture cible se porte souvent sur une architecture VLIW, fort différente de l'architecture source, et ce pour obtenir une économie de matériel conséquente. Pour s'exécuter rapidement, le code traduit doit exploiter le parallélisme d'instruction, à savoir exécuter plusieurs instructions en même temps dans des unités de calcul séparées. Il est possible d'utiliser un processeur superscalaire avec exécution dans le désordre pour cela, mais au prix d'un cout important en transistors. Alors qu'en utilisant un CPU VLIW, c'est le traducteur binaire qui fait tout le travail d'extraction du parallélisme d'instruction. Et après tout, quitte à avoir un traducteur binaire, autant lui refiler le boulot d'optimisation. Compiler du code à la volée est certes assez lourd, mais qu'il existe des algorithmes efficaces pour regrouper des instructions indépendantes dans une seule instruction VLIW. Au passage, le processeur VLIW a plus de registres que le processeur source. Cela permet de faire du renommage de registres directement en logiciel. Le traducteur binaire n'hésite pas à changer les noms de registres entre instructions source et instruction VLIW cible, afin de supprimer des dépendances de données. En conséquence, cela ouvre des opportunités de parallélisme, qui sont exploitées lors du regroupement des opérations en instructions VLIW. Vous pourriez penser que le choix d'un CPU VLIW pour émuler du CISC est tout sauf optimal. Vous devez penser que la traduction binaire est d'autant plus simple que l'architecture source et cibles sont semblables. Dans les faits, ce n'est pas tellement traduire les instructions qui pose problème, mais plus la gestion du registre d'état, des exceptions matérielles, de la mémoire virtuelle, la différence entre gros-boutisme et petit-boutisme, de même que des différences d'adressage pour les périphériques. Le premier problème est la gestion des conditions, notamment en présence d'un registre d'état. En pratique, la traduction binaire s'utilise pour traduire du code CISC vers du code RISC ou VLIW. Les architectures source ont donc un registre d'état, qui est mis à jour non seulement par des instructions de test, mais aussi des instructions arithmétiques. L'architecture cible n'a elle pas de registre d'état, mais des registres à prédicats. La traduction de l'un vers l'autre est alors quelque peu compliquée. Et elle est d'autant plus compliquée que le registre d'état est la source de dépendances d'instruction implicites, qui réduisent les performances. Par exemple, si une instruction arithmétique modifie le registre d'état, cela peut impacter l'exécution d'une instruction ultérieure, qui lit ce registre d'état. Pour éliminer ces fausses dépendances, le traducteur binaire doit renommer ce registre en logiciel et détecter les dépendances utiles. Et le compilateur doit gérer les cas où il y a beaucoup de distance entre les deux, voire les cas où l'instruction dépendante n'a pas encore été analysée par le compilateur. Un autre problème est lié à la gestion des exceptions matérielles, et précisément des exceptions précises. Pour rappel, une instruction VLIW regroupe plusieurs opérations, opérations qui correspondraient à une instruction machine sur un CPU pas VLIW. En conséquence, plusieurs instructions du langage machine "source" sont regroupées en une seule instruction VLIW. Et il faut tenir compte du cas où une instruction source lève une exception. Dans le code VLIW, cela signifie qu'une opération lève une exception, et il faut annuler partiellement l'instruction VLIW associée. Et par partiellement, on veut dire que seules les opérations suivantes dans l'ordre du programme source doivent être annulées, puis ré-exécutées. Et c'est un sacré casse-tête ! Les processeurs VLIW utilisent comme solution le mécanisme d'exceptions différées des processeurs EPIC, vu il y a quelques chapitres. Pour résumer, le code est exécuté par blocs d'instruction, délimités par des branchements ou tout autre limite/barrière pertinente dans le code. Un bloc de code est compilé en deux versions : une version rapide sans exceptions matérielles, une version lente qui gère les exceptions précises. La version rapide s'exécute sans exécuter les exceptions. Cependant, les exceptions matérielles sont enregistrées, pour être prises en compte à la toute fin du bloc de code. La version rapide est exécutée en premier et elle mémorise si une exception matérielle a eu lieu. Puis une instruction vérifie si une exception a eu lieu et décide quoi faire. Si aucune exception n'a eu lieu, elle passe à la suite du programme, les résultats du bloc de code sont définitivement acceptés. Mais si une exception a eu lieu, tout est annulé. Le processeur est remis dans l'état initial, puis le code est ré-exécuté instruction par instruction de manière à gérer l'exception correctement. ===La traduction par pré-décodage=== Une première solution serait de faire la traduction binaire dans le cache d'instruction. Lors d'un défaut de cache, le code chargé depuis la RAM est traduit en code machine cible. Et c'est ce code machine cible qui est mémorisé dans le cache d'instruction et exécuté par le processeur. La technique marche sur le papier et n'est qu'une amélioration des techniques de pré-décodage vues il y a quelques chapitres. Cependant, le code obtenu est une traduction assez basique, qui n'incorpore pas d'optimisations dignes de ce nom. De plus, elle gère mal le cas où la taille du code source et cible sont potentiellement très différentes. Concrètement, elle est surtout utile pour traduire du code d'un processeur RISC vers un autre processeur RISC, dont les tailles d'instructions sont similaires. Elle ne permet pas d'émuler plusieurs jeux d'instructions différents, le cout en matériel (un circuit de pré-décodage par jeu d'instruction) serait trop important. ===Le projet DAISY d'IBM=== De très rares processeurs étaient conçus pour accélérer cette traduction binaire, afin de garder de bonnes performances. Les premiers à avoir étudié l'idée étaient IBM, avec leur projet DAISY (''Dynamically Architected Instruction Set from Yorktown''). Le projet de base était de convertir à la volée du code compilé pour des CPU Power PC, vers du code VLIW. Les chercheurs d'IBM avaient développé un algorithme de traduction dynamique efficace, ainsi qu'un processeur VLIW disposant d'optimisations spécifiques à la traduction binaire dynamique, à la volée. De nombreuses idées de ce projet ont été reprises ou re-découvertes par la société Transmetta, avec ses processeurs Crusoe et Efficieon, puis par NVIDIA avec son projet Denver. Les ingénieurs de ce projet ont étudié la possibilité d'émuler plusieurs jeux d'instructions différents sur un même processeur, notamment le s390 d'IBM et le x86. En théorie, cela demande juste d'avoir plusieurs programmes de traduction binaire : un pour le x86, un autre pour le s390, éventuellement un autre pour le Power PC. Mais cela ne s'est pas concrétisé. Mais les ingénieurs ont étudié quelle pourrait être l'architecture VLIW idéale pour ça. Par exemple, il fallait des additions 3-opérandes pour simplifier les calculs d'adresse. Une difficulté était la gestion du registre d'état, dont les bits ne sont pas mis à jour de la même manière sur le x86 et le s390 ou le Power PC. La différence entre gros-boutisme et petit-boutisme était aussi un problème, de même que des différences d'adressage pour les périphériques. Pour que la traduction binaire soit efficace, le processeur VLIW intègre diverses optimisations, comme les exceptions différées et les branchements multi-voies. La principale est la suivante : seul le code des boucles ou fonctions exécutées fréquemment est traduit en code VLIW, le reste du code est interprété. En effet, traduire du code binaire prend plus de temps qu'une simple interprétation. Pour du code qui ne sera exécutée qu'une seule fois, il est plus rapide d'utiliser l'interprétation. Par contre, pour du code exécuté beaucoup de fois, le cout de la traduction binaire est amorti, dilué sur N exécutions, compensé par le gain en temps d'exécution de ce code traduit. Au final, cela permet de ne traduire que le code qui le mérite. La détection du code fréquemment exécuté est réalisée dans le cœur VLIW. L'unité de branchement mémorise les derniers branchements rencontrés et le nombre de fois qu'ils ont été exécutés. Si ils ont été exécutés un certain nombre de fois, l'unité de branchement lève une exception matérielle, qui invoque le traducteur binaire. La routine de cette exception est le traducteur binaire proprement dit. En clair, la traduction binaire est démarrée quand le processeur détecte qu'une fonction a été exécutée plus de N fois, via une exception matérielle dédiée. La mesure du nombre d'exécution d'un branchement se fait dans l'unité de calcul dédiée aux branchements, avec l'aide d'une sorte de ''branch adress buffer'' modifié. Pour rappel, le ''branch adress buffer'' mémorise le ''Program counter'' de chaque branchement récemment rencontré. Ici, chaque entrée du ''branch adress buffer'' est associée à un compteur incrémenté à chaque exécution du branchement. ===Les processeurs Crusoe et Efficieon de Transmetta=== Les processeurs Crusoe Et Efficeron sont deux processeurs VLIW produits par la société Transmetta, une société californienne rachetée par NVIDIA. Ils étaient conçus pour exécuter spécifiquement des programmes x86, système d'exploitation inclus. L'idée derrière ce projet était d'exécuter du code x86 sans que les applications, ni même le système d'exploitation et le BIOS soient au courant ! Les processeurs Transmetta étaient en réalité conçus pour exécuter un programme unique, le ''Code Morphing Software'' (CMS), qui traduisait le code x86 en code VLIW. Transmetta ne voulait pas que son processeur VLIW soit exploité directement, sans le CMS. Il n'avait pas rendu public de compilateur pour traduire du C vers du code VLIW, il n'avait pas donné la documentation du jeu d'instruction VLIW. Des efforts de rétro-ingénieurie, documentés sur le site realworldtech, ont cependant permis de comprendre comment étaient encodées les instructions du processeur Crusoe. Comme pour le projet Daisy, le ''Code Morphing Software'' utilisait à la fois interprétation et traduction binaire, selon les besoins. Les instructions sont interprétées lors de leurs premières exécutions, mais le CMS bascule sur de la traduction en code VLIW après un certain nombre d'exécution. Ainsi, les boucles souvent exécutées sont traduites en code VLIW, alors que le reste du code est interprété. Le choix du code à traduire est le fait du CMS, il utilise des heuristiques complexes pour, qui ne sont pas connues dans le détail. Le ''Code Morphing Software'' est mémorisé dans une EEPROM, ce qui en fait un ''firmware'' situé en dessous du BIOS. Le CMS démarre ensuite le BIOS, qui lui-même démarre le système d'exploitation, qui lui-même démarre les pilotes de périphériques et les programmes. Le CMS se réserve les 16 premiers mébioctets de l'espace d'adressage. L'EEPROM du CMS est mappée dedans, mais n'en utilise que la moitié. Le reste est utilisé comme cache, pour mémoriser le code VLIW traduit par le CMS. Les deux processeurs intégraient aussi deux mémoires SRAM, utilisées par le CMS, appelées la ''local program memory'' (LPM) et la ''local data memory'' (LDM). La première contient du code qui gére les interruptions, la mémoire virtuelle, les exceptions matérielles, les problèmes d'alignement mémoire, et quelques fonctions très fréquemment utilisées par le CMS. La seconde est de la mémoire RAM utilisée par ces fonctions de la ''local program memory''. Les mémoires caches L1 et L2 sont séparées de ces deux mémoires. Le processeur Crusoe et Efficieon étaient des processeurs VLIW très simples. Ils n'avaient même pas de MMU, ni de fonctionnalités importantes sur les CPU x86. Pour Crusoe, les instructions VLIW étaient encodées sur 64 ou 128 bits, et regroupaient entre 2 à 4 opérations. Les instructions LOAD/STORE ne supportaient pas d'adressage indicé, juste de l'adressage indirect à registre. Crusoe avait 5 unités de calcul : deux ALU entières, une unité LOAD/STORE, une unité de branchement, une FPU. Efficieon doublait le nombre d'ALU entière et d'unité LOAD/STORE, les instructions VLIW passaient à 256 bits. Le processeur Crusoe contient 160 registres, dont 64 registres généraux, 32 registres flottants. Les registres généraux font 32 bits, ce qui est cohérent avec le fait que les CPU x86 de l'époque étaient des processeurs 32 bits. Quant aux registres flottants, ils faisaient 80 bits, ce qui colle avec la taille des registres flottants de la FPU x87 utilisée à l'époque. Sur les 64 registres généraux, seuls 48 étaient réellement utilisable pour mémoriser des opérandes. Une partie des registres généraux étaient utilisés pour la gestion de la pile, d'autres pour mémoriser l'état du CPU x86 émulé, un registre était un registre zéro non accesible en écriture. La gestion des exceptions est optimisée avec un système d'exceptions différées, le même que celui décrit plus haut. Le processeur utilise un système similaire, pour effectuer des lectures anticipées, qui a été expliqué dans le chapitre sur les processeurs VLIW/EPIC. Sauf qu'il s'agit là d'une optimisation pour gagner en performances en général, pas une aide utile pour gérer des exceptions précises ou toute autre subtilité de la traduction binaire. Pour gérer les exceptions différées et les lectures anticipées, le processeur mémorise l'état du processeur avant de démarrer un bloc de code. Pour cela, le processeur contient des ''shadow registers'', 48 registres entiers et 16 registres flottants. Vous remarquerez qu'il y a autant de registres entiers que de ''shadow registers'' entiers. Les registres entiers/flottants sont mémorisés dans les ''shadow registers'' avant d'exécuter un bloc de code, afin que le processeur puisse revenir à l'état de base. Une autre fonctionnalité liée est le ''gated store buffer''. Le principe est d'éviter toute écriture en mémoire RAM, tant que le code VLIW n'a pas émis d'instruction ''commit''. L'idée est que les écritures sont conservées dans la ''store queue'' de l'unité LOAD/STORE. Pour rappel, la ''store queue'' met en attente les écritures soit tant que les écritures ne sont pas terminées, soit tant que la RAM est occupée. L'idée est que les écritures sont accumulées dans la ''store queue'' tant que l'instruction ''commit'' n'a pas eu lieu. Le ''store queue'' est "déversé dans la RAM" seulement quand l'instruction ''commit'' s'exécute. ===Les CPU NVIDIA Denver=== Nvidia a tenté quelque chose de similaire avec son projet Denver. NVIDIA a racheté l'entreprise Transmetta, ses brevets, et a tenté de refaire la même chose. Le projet initial était de traduire du code x86 vers un jeu d'instruction VLIW propriétaire, comme l'a fait Transmetta. Mais NVIDIA n'a pas réussi à acquérir la licence du jeu d'instruction x86 et s'est rabattu sur le jeu d'instruction ARM. Le processeur né de ce projet est le Tegra K1-64 CPU. Il s'agit d'un processeur composé d'un cœur VLIW, couplé à deux décodeurs d'instructions ARM. Les deux décodeurs ARM traduisent une paire d'instructions ARM consécutives en instructions VLIW, pour les exécuter sur le cœur VLIW. Le CPU peut fonctionner selon deux modes : ARM et VLIW. En somme, le processeur supporte deux jeux d'instruction : le jeu d'instruction ARM, et un jeu d'instruction VLIW. En mode ARM, les instructions ARM sont chargées depuis le cache, traduites en VLIW par les décodeurs ARM, puis exécutées par le cœur VLIW. En mode optimisé, le CPU exécute des instructions VLIW chargées depuis le cache d'instruction, les décodeurs ne sont pas utilisés. : Le CPU supporte en réalité deux jeux d'instruction ARM : ARM8 et 7. Mais ils correspondent au même mode pour le CPU. Un point important est que le processeur peut changer très rapidement de mode, en à peine quelques cycles d'horloges. On n'est clairement pas dans le cas des CPU x86, qui mettent des plombes pour passer du mode 32 à 64 bits et inversement. La commutation est tellement rapide qu'on peut considérer que le processeur supporte deux jeux d'instruction simultanément. Et le support simultané de deux jeux d'instruction facilite l'implémentation de la traduction binaire. Encore une fois, inutile de traduire du code qui ne sera exécutée qu'une seule fois, mieux vaut l'exécuter directement dans le mode ARM. Surtout que le processeur supporte un mode ARM qui permet d'exécuter du code ARM sans perte de performance. Par contre, le code fréquemment utilisé, notamment dans des boucles critiques, est traduit en VLIW. Au final, les performances sont optimisées, en limitant le travail du logiciel traducteur binaire. La détection du code fréquemment exécuté se fait différemment que pour les processeurs Transmetta et Daisy. L'unité de branchement ne détecte pas directement le code beaucoup exécuté, même si elle a un rôle à jouer. A la place, la détection est réalisée en logiciel, par un ''thread'' dédié, qui s'exécute sur un cœur séparé. Il détecte le code fréquemment utilisé en regardent les branchements exécutés récemment. Pour cela, le processeur mémorise l'historique des branchements pris récemment et copie cette historique dans une mémoire tampon dédiée, partagée entre tous les cœurs. Vu qu'elle est partagée, le ''thread'' de détection a accès à l'historique sans pertes de performances, seule la copie de l'historique dans ce tampon a un cout en performance, pas son partage. Le code VLIW obtenu après traduction binaire est mémorisé dans la mémoire RAM, dans une portion spécialement réservée pour. Elle fait 128 mébioctets, et est appelée le '''cache d'optimisation''' par NVIDIA. Le cache d'optimisation est protégé en écriture et seul le traducteur binaire peut écrire dedans. Au passage, le traducteur binaire est du code VLIW, ce qui fait qu'il est placé dans le cache d'optimisation. Le processeur détecte automatiquement quand une fonction pour laquelle le code compilé équivalent est disponible. Pour cela, l'unité de chargement contient une table de correspondance entre l'adresse de la fonction ARM, et l'adresse de son équivalent compilé. Quand une fonction est appelée, l'unité de chargement regarde l'adresse de destination du branchement. Si l'adresse est dans cette table, elle récupère l'adresse de la fonction compilée et branche vers celle-ci. Le processeur dispose d'un cache d'instruction de 128 kibioctets, ce qui est très important, mais nécessaire vu la taille des instructions VLIW. Le coeur VLIW dispose de ses propres décodeurs, d'un ''scoreboard'', de 7 unités de calcul et de registres. Pour les unités de calcul, il a deux FPU, deux ALU, une unité de branchement, et deux unités LOAD-STORE faisant aussi ALU. Un point important est que le CPU dispose de 64 registres entiers et de 64 registres flottants. C'est deux fois plus que supporte le jeu d'instruction ARM. En mode ARM, seule la moitié des registres est utilisée. Mais en mode optimisé, le traducteur binaire utilise bien les 64 registres, grâce à une sorte de renommage de registres logicielle. Pour simplifier la traduction binaire, le processeur supporte les techniques vues précédemment. Le processeur intègre un ''gated store buffer'' similaire à celui des processeurs Transmetta. Les techniques d'exceptions différées sont aussi supportées, comme sur les processeurs Transmetta. Il a aussi une unité de préchargement décrite par NVIDIA comme agressive, avec support du préchargement de type anticipé (''runahead''). ===L'émulation de la mémoire virtuelle=== Pour finir, il faut parler de la mémoire virtuelle avec la traduction binaire. Sur le processeur source, le processeur gère à la fois des adresses physiques et virtuelles et sa MMU fait la conversion entre les deux. Sur le processeur VLIW, la MMU est simulée par le programme de traduction binaire, partiellement ou totalement. L'implémentation exacte varie suivant que l'on parle du projet Daisy, des processeurs Transmetta ou de Denver. Sur les processeurs Transmetta, il n'y a pas de MMU ni de mémoire virtuelle. A la place, le programme de traduction binaire émule la mémoire virtuelle du processeur source. Le processeur VLIW ne gère que des adresses physiques, rien d'autre. L'espace d'adressage physique du processeur VLIW a la même taille que celui du processeur émulé, ici des processeurs x86. Il est vraisemblable que les adresses physiques utilisées par le processeur x86 sont les mêmes que celles du processeur VLIW. Sur le projet Daisy, le processeur VLIW gère la mémoire virtuelle via pagination, comme les processeurs émulés. Ce qui fait qu'il gère un espace d'adressage virtuel et un espace d'adressage physique. Pour éviter toute confusion, nous parlerons d'adresse physique/virtuelle VLIW pour les adresses physiques/virtuelles du processeur VLIW, d'adresse virtuelle/physique source pour celle du jeu d'instruction traduit, à savoir du Power PC ou du s390, éventuellement de l'x86. Il faut alors faire le lien entre adresses physiques source et adresses virtuelles VLIW. Pour cela, rien de plus simple : il y a correspondance parfaite. L'adresse physique source numéro N correspond à l'adresse virtuelle VLIW numéro N. Cependant, cela signifie que tout l'espace d'adressage virtuel VLIW serait utilisé par le code à traduire. En réalité, il faut ajouter de la place pour le code traduit et le programme de traduction binaire. La conséquence est que l'espace d'adressage virtuel VLIW est plus large que l'espace d'adressage source. L'espace d'adressage virtuel VLIW est découpé en trois sections : une section pour le code Power PC à traduire, une autre pour le code traduit en VLIW, et une dernière réservée au traducteur binaire. Le programme de traduction est placé dans une ROM mappée en mémoire dans la seconde section. Le reste de la seconde section est réservé à une mémoire RAM utilisée par le programme de traduction. [[File:Mémoire virtuelle sur le projet Daisy.png|centre|vignette|upright=1.5|Mémoire virtuelle sur le projet Daisy]] Il faut noter que chaque page physique source, une fois traduite, correspond à N pages virtuelles VLIW. La raison est que le code traduit prend plus de place que le code machine originel. En conséquence, on doit utiliser une page finale plus grande. Pour le reste, des pages contiguës en mémoire physique source sont elles aussi contiguës en mémoire virtuelle VLIW. Le calcul d'adresse est donc simple : l'adresse physique source est multipliée par N, puis on ajoute l'adresse de base à laquelle commence la section pour le code traduit. ==Les processeurs à jeu d'instruction reconfigurable== Nous venons de voir comment accélérer en matériel la traduction binaire. Mais il existe une alternative qui permet de se passer de traduction binaire, complétement. L'idée est d'avoir un processeur avec un '''jeu d'instruction reconfigurable'''. Le nom est assez parlent : on peut configurer le processeur de manière à supporter le jeu d'instruction de son choix ! Par exemple, on pouvait reconfigurer un CPU de manière à ce qu'il supporte le jeu d'instruction x86, ou qu'il exécute du ''bytecode'' Java, ou du code ARMv8, etc. De tels processeurs sont encore plus rare que ceux vu avant dans ce chapitre, mais ils ont existé ! Les processeurs de ce type avaient un ''microcode réinscriptible'', à savoir que c'était des processeurs microcodés pour lesquels on pouvait modifier le microcode. L'idée était qu'en changeant le microcode, on changeait le jeu d'instruction du processeur. Pour cela, le microcode était associé à une mémoire SRAM dans laquelle le microcode était copié au démarrage du CPU. Il y avait donc une micro-ROM et une micro-RAM. Les instructions étaient décodées en utilisant le microcode dans la micro-SRAM, la micro-ROM ne servait qu'au démarrage. En modifiant le contenu de la micro-SRAM, on pouvait altérer le microcode, voire le remplacer totalement. ===Le Burroughs B1700 : architecture externe=== Un exemple est celui du Burroughs B1700, et il est intéressant d'étudier en détail son architecture. Son architecture est décrite dans le livre "Interpreting Machines : Architecture and Programming of the Bl700/Bl800 Series", écrit par Elliott I. Organick et James A. Hinds. Une partie de ce qui vfa suivre est un très court résumé de ce livre. Il disposait de 4 registres généraux, nommés X, Y, T et L, tous de 24 bits. Les registres T et L étaient découpés en 6 sous-registres de 4 bits, nommés LA LB LC LD LE et LF pour le registre L, et TA TB TC TD TE et TF pour le registre T. Ils étaient adressables comme n'importe quel registre. Un point important est que le registre T avait des possibilités d’extraction spécifiques. Il était possible d'extrait N bits, placés n'importe où dans ce registre, et de les copier dans un autre registre. Nous verrons pourquoi cette possibilité est intéressante et ne sert que pour le registre T. Les registres X et Y étaient reliés à une ALU de 24 bits capable d'effectuer des opérations arithmétiques basiques. Fait étonnant, l'ALU faisait tous les calculs en même temps et fournissait ses résultats dans 7 registres : un registre SUM pour l'addition, un registre DIFFERENCE pour la soustraction, et un registre par opération bit à bit. Les registres pour les opérations bit à bit sont : CMPX et CMPY pour l'opération NOT sur chaque opérande, XANY pour le ET bit à bit, XEOY pour le XOR, XORY pour le OU logique. Les additions et soustractions pouvaient se faire sur des opérandes codés en binaire ou en BCD, suivant le code opération utilisé. A cela, il fallait ajouter deux registres de "résultat" MSKX et MSKY. Ils fournissaient un masque dépendant de la taille du résultat en bits. Cela permettait de gérer des données de 0, 4, 8, 12, 16, 20, et 24 bits, alors que l'ALU faisait 24 bits. Et cela n'était pas limité à 16 ou 8 bits, mais n'importe quelle taille inférieure ou égale à 24 bits était supportée, même des tailles non-conventionnelles comme des résultats codés sur 3 bits, 9 bits, 12 bits, etc. L'ALU peenait en opérande les registres X et Y, mais aussi un registre CYF pour la retenue entrante. En sortie, il fournissait aussi deux retenue sortantes : une pour l'addition et une autre pour la soustraction. Elles étaient mémorisées dans les registres CYL et CYD, respectivement. L'ALU fournissait aussi 12 conditions, qui alimentait un registre d'état. Le registre d'état était segmenté en trois registres appelés XYST, XYCN et BICN. Le processeur contenait de nombreux autres registres de 4 bits, dont les sous-registres de T et L. En tout, il y a 27 registres de 4 bits adressables ! Et le processeur disposait d'une ALU 4 bits pour manipuler ces registres de 4 bits ! Elle est capable de faire des MOV entre registres de 4 bits, des opérations bit à bit, et peut aussi tester si un bit vaut 0 ou 1. La dernière possibilité est très utile pour implémenter les branchements. Pour communiquer avec les entrées-sorties, il disposait de deux registres CMND et DATA : CMND pour envoyer une commande à une entrée-sortie, DATA pour lire ou écrire une donnée. Pour la mémoire, il avait deux registres séparés : READ et WRITE. READ contient la donnée lue lors d'une lecture,; WRITE est pour une donnée à écrire lors d’une écriture. Il s'agit de registres d’interfaçage mémoire, qui ont été promus au rang de registres architecturaux. Les deux registres sont gérés par le microcode, qui est le langage principal de la machine. Les instructions machines configurées font sans, mais le microcode doit y avoir accès facilement. Et vu que le microcode est le langage principal de ce processeur, ces deux registres deviennent architecturaux. ===Une architecture bit-adressable=== Si avoir un microcode réinscriptible est suffisant, quelques choix de ''design'' facilitent l'émulation de jeux d'instructions variés. Par exemple, le CPU Burroughs B1700 avait ajouté une fonctionnalité très importante : il était bit-adressable. En clair, il pouvait adresser la mémoire bit par bit, si nécessaire. L'avantage est que cela permettait d'émuler des architectures dont la taille des registres/''byte''/mots était très différente. Un autre avantage est que cela facilitait la gestion des instructions de taille variable. Détaillons rapidement le premier avantage. Le processeur était un CPU 24 bits, mais il pouvait émuler des processeurs 16 bits, 8 bits, 4 bits, 1 bit, 9 bits, ou toute autre valeur. Pour cela, le processeur lisait des mots de 24 bits et utilisant un ''barrel shifter'' pour sélectionner les bits adéquats. Par exemple, pour un accès de 16 bits, la donnée lue en mémoire était masquée de manière à ne garder que les 16 bits de poids faible. Le ''barrel shifter''/circuit de masquage était placé directement avant le bus mémoire, sur le trajet des données/instructions. Le ''barrel shifter'' était commandé par un registre ''Field Unit'', qui précisait quelle était la taille des données à charger. ===La gestion de la taille des instructions=== Les instructions à décoder n'ont pas de longueur fixe, à savoir qu'elles peuvent faire 16 bit avec tel jeu d'instruction, 8 bits avec un autre, 32 bits sur un autre, etc. Elles peuvent aussi être de longueur fixe ou variable ! Le B1700 intégrait donc un support des instructions de longueur variable, qui était "détourné" pour gérer des jeux d'instruction différents. Notons que vu que l'architecture est bit-adressable, les contraintes d'alignement des instructions disparaissent, ce qui a des avantages qu'on va détailler dans ce qui suit ! L'implémentation des instructions de taille variable ajoutait un registre avant le registre d'instruction. Le registre d'instruction proprement dit était un registre M de 24 bits. Il était précédé par un registre T, dans lequel l'instruction était assemblée, en combinant plusieurs morceaux. Si l'instruction est codée est codée sur moins de 24 bits, le registre T se contente de masquer les bits en trop. Si l'instruction chargée dépasser 24 bits, le chargement se faisait en plusieurs passes, avec plusieurs lectures en mémoire. Dans ce qui suit, on suppose que l'instruction est composée de '''champs''' : l'opcode, les opérandes, les adresses absolues, les constantes immédiates, les numéros de registre, etc. L'idée est que l'instruction est chargée dans le registre T, puis découpée en champs, qui sont copiés dans les registres du processeur ou envoyés au microcode. Je rappelle que le registre T est relié à un ''barrel shifter'', différent au précédent, qui permettait de masquer, décaler et extraire une donnée de ce registre. Et il y a la possibilité d'extraire une suite de bit de ce registre T, pour la copier dans un autre registre. Par exemple, prenons une instruction composée d'un opcode, d'une constante immédiate et d'une adresse immédiate. L'opcode est lu et interprété par le microcode. Le microcode extrait la constante immédiate du registre T et la copie dans le registre X. Ensuite, l'adresse est extraite et envoyée au registre FA, une lecture est démarrée, et l'opérande est copiée dans le registre Y. Enfin, l'instruction est exécutée par le microcode. Le tout prend plusieurs micro-opérations : une première pour lire l'opcode, une seconde pour lire la première opérande, une troisième pour lire la seconde opérande, una quatrième pour tout exécuter. Notons que suivant la taille des champs, on peut en charger plusieurs à la fois. Par exemple, imaginons qu'on ait chargé 24 bits, contenant un opcode de 8 bits et une constante immédiate de 16 bits : pas besoin de faire deux accès mémoire, le registre T est juste découpé en un opcode et une constante. Le registre T est fait pour ce genre d'extraction. ===Le chargement d'un champ=== L'architecture est bit-adressable car elle gére des champs qui ne font pas forcément 8 ou 16 bits. Par exemple, certains jeux d'instructions ont des opcodes codés sur 6 ou 7 bits, voire 9-10 bits. Par exemple, prenons un opcode de 7 bits, suivi par une adresse de 12 bits et une constante immédiate de 15 bits : aucun de ces champs ne sera aligné sur 8/16/24 bits. Utiliser une architecture bit-adressable règle le problème d'alignement. Et en général, les contraintes d'alignement des instructions disparaissent ! Quand on charge une instruction, peut importe qu'elle soit ou non alignée sur 24 bits, elle sera placée dans les bits de poids faible, il suffira de masquer les bits de poids fort inutiles. Par exemple, si on charge une instruction de 16 bits, les 16 bits seront copiés dans les 16 bits de poids faible du registre T, il suffira de masquer l'octet de poids fort. Pas besoin de faire de décalages pour gérer des instructions mal alignées... Pour charger une instruction, il faut connaitre plusieurs informations : l'adresse de son premier bit, sa taille. Pour cela, le B1700 contient deux registres, nommés ''Field Adress'' (FA) et ''Field Length'' (FL), qui indiquent l'adresse du premier bit et la taille de l'instruction à charger. Le premier fait 24 bits, ce qui permet de gérer des adresses de 24 bits. N'oubliez pas que le processeur est bit-adressable, ce qui permet d'adresser 16 Mébi-bits, soit 2 mébioctets. Le registre pour la taille fait lui 16 bits, ce qui permet de gérer des instructions de 65536 bits, soit 8192 octets ! Leur nom commence par ''field'', car le processeur peut gérer des instructions de taille variable et/ou plus longues que 24 bits. Dans ce cas, le chargement de l'instruction se fait champ par champ. Pour donner un exemple, prenons une instruction composée de 20 champs de 12 bits chacun. Les champs sont chargés un par un dans le registre T. Le registre FA est alors initialisé avec l'adresse de l'instruction, il est incrémenté de 12 à chaque cycle. Le registre FL est initialisé avec la taille de l'instruction, soit 20 * 12 = 240 bits, et il est décrémenté de 16 à chaque cycle. Précisons cependant que la taille d'un champ peut être changée d'un cycle à l'autre. Par exemple, le processeur peut charger un opcode de 8 bits, l'interpréter, détecter que l'instruction demande ensuite de charger une adresse de 16 bits. Dans ce cas, le registre FL est immédiatement altéré pour charger 16 bits, au lieu des 8 bits de l'opcode immédiatement précédent. ===Le microcode réinscriptible=== Le microcode utilise des micro-opérations de 16 bits de long, ce qui est très peu et bien en-dessous des micro-opérations des autres processeurs. Les micro-opérations sortant du microcode sont envoyées au registre M, un registre de micro-opération de 16 bits. les micro-opérations écrites dans ce registre sont ensuite décodées par le hardware et exécutées. Fait intéressant, il était possible de faire un OU logique entre la micro-opération sortante et un registre du processeur, le résultat étant mémorisé dans le registre M. Cela permettait de faire du code automodifiant dans le microcode ! Utiliser du code automodifiant avec un microcode réinscriptible est parfaitement possible, et c'est utile pour émuler des modes d'adressage complexe dans le microcode. Cela permettait par exemple de remplacer une adresse dans une micro-opération. L'adresse est alors surimposée avec un OU dans le champ d'adresse, alors que le reste de l'instruction n'est pas modifié. Mais modifier directement le microcode est rarement une bonne idée. Aussi, pour éviter cela, l'idée est de modifier les instructions avec un OU logique avec un autre registre. Les micro-opérations du microcode ne sont donc pas modifiées dans le microcode, mais en sortie du microcode. La RAM mémorisant le microcode était de petite taille, et il arrivait qu'elle soit trop petit pour mémoriser le microcode complet. Dans ce cas, le programmeur devait gérer la situation en découpant le microcode en segments, certains étant placé dans le processeur, les autres étant en mémoire RAM. Le processeur peut lire le microcode placé en mémoire RAM et même l'exécuter, mais avec une pénalité en termes de performances. La documentation appelle cela un '''''split-level control store'''''. Quelques registres intégrés au processeur permettent ce genre de magie. Le microcode pouvait servir pour émuler un autre jeu d'instruction, mais il était aussi possible d'implémenter un véritable interpréteur, grâce aux techniques du paragraphe précédent. Le processeur était même conçu pour ! L'interpréteur était placé dans un "segment" en mémoire RAM, dont le processeur connaissait la position. Il incorporait pour cela deux registres, un pour l'adresse de base de l'interpréteur, un autre pour son adresse de fin. Il incorporait une protection mémoire limitée sur ce segment, à savoir que seul l'interpréteur pouvait lire ou écrire dans ce segment. Afin de gérer les interruptions, ces dernières étaient partiellement détectées en logiciel. Les interruptions ne s'exécutaient pas automatiquement, pour émuler correctement les interruptions. Une interruption doit être prise en compte à la toute fin de l'exécution d'une instruction, quand celle-ci a finit son travail. Mais vu qu'ici, les instructions sont émulées avec des séries de micro-opérations, il y a un problème : il est possible d'interrompre une instruction émulée en plein milieu. Pour éviter cela, le processeur ne déclenche pas les interruptions matérielles immédiatement, elles sont mises en attente. Une micro-opération dédiée vérifie si une interruption est en attente, et l'exécute alors cas échéant. La micro-opération est utilisée comme micro-opération finale d'une instruction émulée. ==Les jeux d'instructions dédiés à un langage de programmation== De rares processeurs sont conçus pour un langage de programmation en particulier. On appelle ces processeurs, conçus pour des besoins particuliers, des '''processeurs dédiés'''. Par exemple, les fameux ''Burrough E-mode'' B5000/B6000/B7000 étaient spécialement conçus pour exécuter de l'ALGOL-60. Leurs cousins B2000/B3000/B4000 étaient eux conçus pour le COBOL. Des langages fonctionnels ont aussi eu droit à leurs processeurs dédiés. Le prolog en est un bel exemple, avec les superordinateurs de 5ème génération qui lui étaient dédié. On peut aussi citer les machines LISP, dédiés au langage LISP, qui datent des années 1970. Elles étaient capables d’exécuter certaines fonctions de base du langage directement dans leurs circuits : elles possédaient notamment un ''garbage collector'' câblé dans ses circuits ainsi que des instructions machines supportant un typage déterminé à l’exécution. Les processeurs dédiés ont eu leur heure de gloire au début de l'informatique, à une époque où les langages de haut niveau venaient d'être inventés. À cette époque, les compilateurs n'étaient pas performants et ne savaient pas bien optimiser le code machine. Il était alors rationnel, pour l'époque, de rapprocher le code machine cible et le langage de programmation de haut niveau. De nombreuses architectures dédiés ont ainsi été inventées, avant que les concepteurs se rendent compte des défauts de cette approche. Les défauts en question ne sont pas nombreux, mais assez simples à comprendre. Premièrement, elles sont très rapides pour un langage de programmation en particulier, mais sont assez mauvaises pour les autres, d'où un problème de "compatibilité". Ajoutons à cela que les langages de programmation peuvent évoluer, devenir de moins en moins populaires/utilisés, ce qui rend la création d'architectures généralistes plus pertinente. Enfin, les architectures dédiées sont évidemment des processeurs CISC, pour implémenter les nombreuses fonctionnalités des langages évolués. Et les défauts des CISC sont assez rédhibitoires à l'heure actuelle. À l'heure actuelle, les algorithmes des compilateurs se sont améliorés et savent nettement mieux utiliser le matériel. Ils produisent du code machine efficace, ce qui rend les architecture dédiées bien moins intéressantes. Si on ajoute les défauts de ces architectures dédiées, par étonnant que les architectures dédiées aient presque disparues. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'accélération matérielle de la virtualisation | prevText=L'accélération matérielle de la virtualisation | next=Le matériel réseau | nextText=Le matériel réseau }} </noinclude> 0qkvjwlqvcy10iocja11jarh0e2sw6w 764029 764028 2026-04-19T18:35:36Z Mewtow 31375 /* Le microcode réinscriptible */ 764029 wikitext text/x-wiki De nos jours, la majorité des programmeurs programment dans des langages de haut niveau. Il est très rare pour eux d'avoir à utiliser de l'assembleur, et encore moins un langage machine. Les programmes écrits dans un langage de haut niveau sont traduits en langage machine par un logiciel appelé le compilateur. [[File:Compilation.PNG|centre|vignette|upright=2.5|Compilateur : principe.]] Il se trouve que quelques rares processeurs sont conçus pour faciliter le travail du compilateur. Et ce sont ces processeurs que nous allons voir dans ce qui suit. Ils représentent une catégorie de jeux d'instruction à part, qui n'a pas vraiment de nom. ==La compilation et l’interprétation== Un compilateur traduit donc du code source, écrit dans un langage de haut niveau, vers du langage machine. La traduction est rarement directe. En général, le compilateur traduit le code source en assembleur, qui est lui-même traduit en langage machine. L'assembleur est une représentation textuelle du langage machine alors que le code machine est du binaire exécutable par le processeur. La traduction se fait donc en deux étapes, la '''compilation''' proprement dite et l''''assemblage''', réalisées respectivement par un compilateur et un assembleur. Il faut parfois rajouter une troisième phase d'édition des liens, que nous passons volontairement sous silence. {| |[[File:Code C.png|vignette|200px|Le code source, ici en C.]] |[[File:Assem.png|vignette|250px|Le code assembleur.]] |[[File:Binary file - hello world (C programming).png|vignette|150px|Le code machine.]] |} ===L'interprétation et la compilation à la volée=== Une alternative à la compilation est l''''interprétation''', qui transforme le code source en code machine à la volée. Avec l'interprétation, le code source est passé à un logiciel appelé l''''interpréteur''', qui exécute le code source ligne de code par ligne de code. Une ligne de code est traduite en code machine, qui est exécutée, puis l'interpréteur passe à la ligne suivante. [[File:Kaantotulkkaus.png|centre|vignette|upright=3|Compilation versus interprétation.]] Un défaut de l'interprétation est son cout en performance, qui est assez important. Aussi, l'interprétation stricte a évolué vers un hybride entre interprétation et compilation. L'idée est qu'une partie peu importante du code source est interprétée, alors que le code important est compilé juste avant d'être exécuté. Pour le dire autrement, le code source est partiellement compilé à la volée, juste avant son exécution. Aussi, on parle de '''compilation à la volée'''. Le terme en anglais est ''Just In Time compilation'', abrévié en JIT. En soi, le JIT est associé à de l'interprétation, les JIT sont en réalité des hybrides interpréteurs-compilateurs. Un point important est que compiler du code à la volée est assez lourd, cela a un cout en performance. Par contre, le code compilé s'exécute plus vite que du code interprété. Si on exécute le code compilé une seule fois, le cout de la compilation l'emporte sur le gain à l'exécution. Mais le code compilé est réutilisé autant de fois que nécessaire. Le gain à l'exécution est donc multiplié par le nombre d'exécutions, alors que le cout de la compilation est répartit sur plusieurs exécutions, il est amorti. Les portions du code sont choisies de manière à avoir un gain en performance maximal. L'idée générale est que le code exécuté très souvent est compilé, alors que du code exécuté pas souvent est interprété. Typiquement, les boucles les plus souvent exécutées sont compilées, le code exécuté une seule fois est interprété. ===Les langages intermédiaires=== Les compilateurs modernes passent par un '''langage intermédiaire''' pour faire la transformation en code machine. Le compilateur traduit le langage de haut niveau en langage intermédiaire, puis traduit le langage intermédiaire en code machine cible. Le langage intermédiaire est parfois appelé le '''''bytecode''''', et ce terme recouvre aussi le code écrit avec du ''bytecode''. L'interprétation aussi peut passer par l'intermédiaire d'un un ''bytecode'', ce n'est pas limité aux compilateurs proprement dit. {| |[[File:Intermediate representation scheme.png|centre|vignette|upright=1.5|Représentation intermédiaire d'un compilateur.]] |[[File:Code interpreter scheme.png|centre|vignette|upright=1.5|Fonctionnement d'un interpréteur.]] |} Faire ainsi a de nombreux avantages, le principal étant d'avoir un compilateur capable de traduire un langage de haut niveau vers plusieurs jeux d'instructions différents. Cela permet d'avoir, par exemple, un compilateur qui traduit du C soit en code machine x86, soit en code machine pour un CPU ARM, soit pour un CPU POWERPC, etc. [[File:Compiler design.svg|centre|vignette|upright=3|Compiler design]] Pour l'interprétation, l'usage d'un ''bytecode'' a d'autres avantages. Pour l'exploiter, il faut distribuer non pas le code source, mais le ''bytecode''. En clair, l'interpréteur ne prend pas en entrée le code source, mais du ''bytecode'' déjà compilé. La traduction est alors beaucoup plus simple, car le code source a déjà été partiellement compilé, par la traduction en ''bytecode''. Interpréter du code source est en effet assez compliqué : il faut effectuer des étapes d'analyse lexicale, sémantique, et bien d'autres. Avec du ''bytecode'', ces étapes ont été réalisées lors de la compilation du ''bytecode'', l'interpréteur a alors peut de choses à faire. L'interprétation du ''bytecode'' se fait instruction par instruction, au niveau du ''bytecode''. Une instruction du ''bytecode'' est traduite en une instruction machine équivalente, qui est exécutée. Si l'instruction est un peu complexe, il exécute une fonction/procédure qui fait la même chose. Pour résumer, le ''bytecode'' est ensuite traduit à la volée et exécuté instruction par instruction par un logiciel appelé l'interpréteur. [[File:Control table.png|centre|vignette|upright=2|Control table]] Le langage intermédiaire peut être vu comme l'assembleur d'un processeur, n'existe pas forcément dans la réalité, mais dont le jeu d'instruction est décrit en détail. Le processeur en question est appelé une '''machine abstraite''', ou encore une ''machine virtuelle''. Nous utiliserons le terme de machine abstraite dans ce qui suit. La machine abstraite n'est pas la même suivant que l'on cible la compilation ou l'interprétation/JIT. Pour l'interprétation/JIT, la machine abstraite est souvent une machine à pile, car cela simplifie grandement la traduction en code machine final. L'interprétation demande que la traduction du ''bytecode'' en code machine soit la plus simple possible. Et cela demande de prendre en compte pas mal de détails. Par exemple, les différents jeux d'instruction existants n'ont pas le même nombre de registres, ce qui pose problème lors de la traduction du ''bytecode''. Lors de la transformation en code machine, un algorithme d'allocation de registres se débrouille pour traduire le code intermédiaire en code qui utilise un nombre limité de registres. Pour améliorer cette allocation de registres, il y a deux solutions niveau ''bytecode'' : soit utiliser un nombre illimité de registres, soit utiliser une machine à pile. Pour les machines à pile, il existe un algorithme simple et rapide pour traduire un code écrit pour une machine à pile en un code écrit pour un processeur avec des registres, qui se débrouille pas trop mal pour allouer efficacement les registres. C'est un avantage assez important pour les langages interprétés. ==Les processeurs qui exécutent du bytecode== Le ''bytecode'' est un code machine, ce qui signifie qu'il peut en théorie s'exécuter sur un processeur qui implémente le jeu d’instruction associé. Si le jeu d'instruction d'un ''bytecode'' est souvent une description censée être fictive, elle n'en reste pas moins un jeu d'instruction et des caractéristiques précises, qu'on peut l'implémenter en matériel ! Néanmoins, tous les ''bytecode'' ne sont pas égaux de ce point de vue. Certains sont faciles à implémenter en matériel, d'autres non. Les ''bytecodes'' implémentés en matériel sont ceux dont les machines abstraites sont des machines à pile. Un premier exemple est celui des processeurs Pascal MicroEngine, qui exécutaient directement le bytecode du langage Pascal, le fameux UCSD P-code. Un second exemple est la '''machine SECD''', qui sert de langage intermédiaire pour certains compilateurs de langages fonctionnels. Elle a été implémentée en matériel par plusieurs équipes, notamment par les chercheurs de l'université de Calgary en 1989. Dans le même genre, quelques processeurs simples étaient capables d’exécuter directement le ''bytecode'' du langage FORTH. Le FORTH, un des premiers langages à pile de haut niveau, possède de nombreuses implémentations hardware et est un des rares langages de haut niveau dont le ''bytecode'' a été utilisé comme langage machine sur certains processeurs. Par exemple, on peut citer le processeur FC16, capable d’exécuter nativement du ''bytecode'' FORTH. ===Les processeurs Java=== Le cas le plus impressionnant est celui de la machine virtuelle Java, qui est un design de processeur comme un autre. En temps normal, le ''bytecode'' Java est compilé ou interprété, mais certains processeurs exécutaient du ''bytecode'' Java directement, sans interprétation ni compilation. Ils sont appelés des '''processeurs Java'''. On peut citer les processeurs ARM disposant de l'extension Jazelle, du JEMCore et du aJ-100 de aJile Systems, le picoJava II de Sun, et quelques autres. Il y a aussi eu quelques projets de recherche ou processeurs open source, comme Komodo, jamuth, le ''Java Optimized Processor'' et quelques autres projets du même genre. L'implémentation de la machine virtuelle Java n'était cependant pas complète. Quelques instructions complexes n'étaient pas gérées par le processeur et devaient être émulées en logiciel. Mais la grosse majorité des instructions l'était. Les instructions non-supportées étaient émulées avec la méthode ''trap and emulate'', à savoir qu'un opcode inconnu déclenchait une exception matérielle ''opcode invalide'', dont la routine pouvait émuler les instructions qui doivent l'être. Le processeur aJ-100 de aJile est capable d'exécuter la quasi-totalité des instructions de la JVM Java, seules deux instructions faisant exception. Les '''accélérateurs Java''' sont une solution intermédiaire, qui permet de traduire à la volée du ''bytecode'' Java en instructions RISC, exécutées sur un autre processeur. Il s'agit formellement de traduction binaire, mais je préfère en parler ici. L'idée est de combiner un processeur RISC avec un coprocesseur Java. Le coprocesseur lit le ''bytecode'' Java et le traduit en instructions RISC, ces dernières étant exécutées sur le processeur RISC. Le JA108 de Nozomi était un coprocesseur de ce genre, au même titre que le coprocesseur JSTAR de JEDI. L'extension Jazelle de certains CPU ARM fonctionnait vraisemblablement sur un principe similaire, sauf que le coprocesseur était intégré dans le processeur ARM, entre le cache d'instruction et le décodeur d'instructions ARM. L'intérêt des processeurs Java n'est pas qu'une question de performance. Certes, ils permettent d’exécuter plus vite les programmes compilés en ''bytecode'', comme des programmes Java pour la JVM Java. Mais l'intérêt est aussi de faire des économies de mémoire RAM et ROM. Je rappelle que la machine virtuelle Java est une machine à pile, ce qui fait que sa densité de code est excellente. Les programmes en ''bytecode'' Java sont donc très petits, et prennent peu de place en mémoire ROM. De plus, utiliser un processeur Java permet de se passer d'interpréteur ou de JIT Java, ce qui économise encore plus de mémoire. ===Transformer un processeur RISC en machine à pile=== La plupart des processeurs Java utilisent en interne une architecture à registre généraux, qui émule une machine à registre en microcode. Quelques registres sont réservés pour l'état de la machine virtuelle, avec au minimum un registre réservé pour le pointeur de pile, un autre pour un pointeur vers un ''pool'' de constantes, et quelques autres. D'autres registres servent à mémoriser le haut de la pile, à savoir le sommet de la pile et quelques opérandes situées juste en-dessous. Par exemple, le processeur picoJava II gardait les 64 opérandes au sommet de la pile dans 64 registres généraux, séparés des registres pour les pointeurs et l'état de la JVM. Une optimisation possible, envisagée sur les processeurs picoJava, était une sorte de macro-fusion sous stéroïde. L'idée était de fusionner certaines séries d'instructions Java en une seule instruction machine. Typiquement, pour faire une opération arithmétique, une machine à pile empile deux opérandes et exécute une opération, ce qui prend trois instructions machine. La spécification picoJava 1 a proposé d'envoyer directement la seconde opérande en entrée de l'ALU, ce qui fusionnait l'empilement de la seconde opérande et l'opération arithmétique. Le processeur picoJava II va plus loin et fusionne les trois instructions en une seule instruction RISC, agissant sur des registres. Mais la majorité des processeurs Java ne faisaient pas cela, car cela complexifie grandement le décodeur. Les processeurs ''bigfoot'' de DCT utilisaient un système différent pour émuler une pile avec des registres. Leur banc de registre de 64 bits était coupé en deux : les 16 premiers registres étaient utilisés pour la pile, les 48 suivants étaient utilisés pour autre chose. L'idée est que le registre 0 mémorise un emplacement vide, le registre 1 contient le sommet de la pile, le registre 2 l'opérande sous le sommet de la pile, et ainsi de suite. Les 15 premières opérandes de la pile sont donc mémorisées dans les registres. Les instructions écrivent leur résultat dans le registre adéquat, puis tous les registres sont décalés pour que le résultat passe dans le registre R1. Par exemple, les instructions PUSH chargent une opérande dans le registre 0, puis tous les registres sont décalés de un rang, pour que l'opérande chargée soit dans le registre 1. Le registre 0 devient alors le registre numéro 1, le registre 1 devient le 2, et ainsi de suite. Par contre, les opérations arithmétiques font autrement. Une instruction d'addition, par exemple, lit les opérandes dans le registre 1 et 2, puis place son résultat dans le registre 2 (les deux opérandes sont dépilées). Puis, les registres sont décalés d'un rang pour que ce registre 2 devienne le 1. Les registres ne sont en réalité pas décalés, il n'y a pas de transferts entre registres. A la place, les 16 premiers registres utilisaient une forme limitée de renommage de registres, similaire au banc de registre tournant utilisé sur les processeurs EPIC comme l'Itanium. Les numéros de registres sont renommés, décalés, à chaque fois qu'on empile ou dépile une opérande. Pour faire le renommage de registres, processeur contient un ''register stack counter'' de 4 bits, qui est incrémenté à chaque fois qu'on empile une donnée, et est décrémenté quand on dépile une donnée. Quand on veut accéder à un opérande, le numéro de registre architectural associé est traduit en numéro de registre physique, en additionnant le ''register stack counter'' (modulo 16, pour rester sur 4 bits). ==L'accélération de l'interprétation/JIT : le ''Thumb-EE'' d'ARM== Quelques processeurs ont ajouté des instructions pour faciliter le travail des interpréteurs/JIT. Par exemple, en 2005, ARM a ajouté le mode ''Thumb Execution Environment'', qui faisait cela. Il reprenait le jeu d'instruction compact ''thumb'' et ajoutait quelques instructions et modifiait le comportement d'instructions existantes. Le jeu d'instruction ''Thumb-EE'' a été ajouté sur les CPU ARM en 2005, mais a été déprécié en 2011, par manque d'utilité. Mais la tentative mérite qu'on s'attarde dessus. Les CPU de gamme M, déstinés à l'embarqué, n'ont jamais supporté ''Thumb-EE'', vu que de tels processeurs ne sont pas conçus pour exécuter du code interprété/JIT. Le mode ''Thumb-EE'' est un mode d'exécution, séparé de l'ARM normal. Ainsi, une instruction ''Thumb-EE'' et une instruction ARM peuvent avoir le même encodage en binaire, mais se comporter différemment. Le processeur est à tout moment soit en mode ''Thumb-EE'', soit en mode ARM normal, ce qui précise comment ces instructions doivent se comporter. Des instructions ont été ajoutées par ''Thumb-EE'', pour qu'il fasse son travail. L'entrée et la sortie du mode ''Thumb-EE'' se fait avec deux instructions : ENTERX et LEAVEX. Une fois en mode ''Thumb-EE'', le décodage des instructions ''thumb'' se fait avec les règles du ''Thumb-EE'', et non celles du ''thumb'' normal. Une première différence entre ''thumb'' normal et ''Thumb-EE'' est l'ajout de ce qui s'appelle un ''null check'' pour les instructions mémoire. Le ''null check'' est utilisé pour les instructions en mode d'adressage "base + indice" ou "Base + décalage", ou tout autre mode d'adressage avec un registre de base utilisés dans des calculs d'adresse. L'idée est de vérifier si l'adresse de base vaut zéro ou non. Si c'est le cas, le processeur lève une exception matérielle, qui est traitée par l'interprétation ou le compilateur JIT. Une telle situation est en effet signe d'une erreur d'adressage mémoire, qui doit être traitée par l'interpréteur. Typiquement, c'est signe qu'un pointeur n'a pas bien été initialisé, ce qui peut arriver avec du code interprété ou JIT. Les interpréteurs/JIT traitent généralement la situation en ajoutant un test avant toute instruction mémoire, pour vérifier si le pointeur accédé vaut zéro ou non. Avec ''Thumb-EE'', pas besoin d'ajouter les tests en questions : ils sont réalisés automatiquement lors de chaque instruction mémoire. : Les registres pour la pile, comme le pointeur de pile et le pointeur de ''frame'', sont aussi concernés. L'instruction CHKA vérifie que les accès à un tableau ne débordent pas en dehors du tableau en question. Par exemple, pour un tableau de 1024 éléments, elle vérifie si l'indice est compris entre 0 et 1024. Pour cela, elle vérifie si l'indice de tableau est bien dans l'intervalle adéquat et lève une exception matérielle si ce n'est pas le cas. Son utilité se comprend quand on sait que tous les accès à un tableau sont vérifiés par l'interpréteur. L'interpréteur est censé ajouter des instructions pour vérifier les indices, à savoir deux ou trois branchements. Avec CHKA, le test se fait en une seule instruction. D'autres modifications mineures de l'encodage des instructions sont aussi présentes, ainsi que des modifications pour les instructions LOAD/STORE. Précisément, dans les modes "base + indice" et "base + décalage", le décalage et l'indice subissent maintenant des décalages. L'idée est de simplifier le calcul d'adresse. De quoi économiser quelques instructions lors des calculs d'adresse, ce qui facilite le travail de l'interpréteur/JIT. Et des instructions LOAD/STORE précises voient leur comportement modifié pour faciliter la gestion de la pile de Java ou l'exécution des méthodes locales. Pour faciliter le travail de l'interpréteur, deux instructions HBP et HBLP ont été ajoutées. Elles branchent vers une fonction qui gère les exceptions logicielles (une fonctionnalité présente dans de nombreux langages, comme Java). La première effectue un branchement vers la fonction en question, la seconde sauvegarde l'adresse de retour avant de faire ce branchement. Les deux peuvent brancher vers 256 fonctions pré-déterminées, sans avoir à présenter leur adresse. ==La traduction binaire accélérée par le matériel== Plus haut, nous avons surtout parlé des compilateurs et des interpréteurs. Cependant, nous devons aussi parler de la '''traduction binaire'''. Elle traduit un programme écrit dans un code machine vers un autre code machine. Par exemple, elle traduit un programme compilé pour un CPU x86 vers un code machine ARM. En général, le système d'exploitation est généralement compilé pour le jeu d'instruction natif, mais il exécute des applications prévues pour le x86. Les applications sont traduites par le système d'exploitation, avant d'exécuter le code traduit. : Nous parlerons dans la suite de code machine source et de code machine cible, pour parler respectivement du code à traduire et du code obtenu après traduction. Idem avec d'autres termes comme architecture cible/source, ou jeu d'instruction cible/source. La traduction binaire est surtout utilisée pour des questions d'émulation ou de compatibilité. Par exemple, lorsque les Macintosh sont passés de processeurs Power PC vers des processeurs x86, le système d'exploitation Mac OS utilisait la traduction binaire pour convertir les anciennes applications Power PC vers du code x86. Les utilisateurs n'y ont vu que du feu. Le système de traduction binaire était appelé Rosetta 1. Par la suite, lors de la transition de processeurs x86 vers des processeurs Apple, Rosetta 2 a vu le jour. La traduction binaire a surtout été utilisée pour traduire du code x86 vers un autre jeu d'instruction. Il faut dire que le x86 est le jeu d'instruction dominant. De nombreuses entreprises ont eu pour ambition de briser l'hégémonie du x86 sur PC, en remplaçant le x86 par un jeu d'instruction plus performant, tout en gardant une compatibilité maximale. La traduction binaire était la seule solution pratique. Elle était le plus souvent intégralement réalisée en logiciel, comme c'était le cas sur les architectures Itanium avec le ''IA-32 Execution Layer''. ===Les généralités sur la traduction binaire assistée en matériel=== L'architecture source est presque tout le temps une architecture CISC, assez ancienne, qu'on souhaite émuler. Le choix de l'architecture cible se porte souvent sur une architecture VLIW, fort différente de l'architecture source, et ce pour obtenir une économie de matériel conséquente. Pour s'exécuter rapidement, le code traduit doit exploiter le parallélisme d'instruction, à savoir exécuter plusieurs instructions en même temps dans des unités de calcul séparées. Il est possible d'utiliser un processeur superscalaire avec exécution dans le désordre pour cela, mais au prix d'un cout important en transistors. Alors qu'en utilisant un CPU VLIW, c'est le traducteur binaire qui fait tout le travail d'extraction du parallélisme d'instruction. Et après tout, quitte à avoir un traducteur binaire, autant lui refiler le boulot d'optimisation. Compiler du code à la volée est certes assez lourd, mais qu'il existe des algorithmes efficaces pour regrouper des instructions indépendantes dans une seule instruction VLIW. Au passage, le processeur VLIW a plus de registres que le processeur source. Cela permet de faire du renommage de registres directement en logiciel. Le traducteur binaire n'hésite pas à changer les noms de registres entre instructions source et instruction VLIW cible, afin de supprimer des dépendances de données. En conséquence, cela ouvre des opportunités de parallélisme, qui sont exploitées lors du regroupement des opérations en instructions VLIW. Vous pourriez penser que le choix d'un CPU VLIW pour émuler du CISC est tout sauf optimal. Vous devez penser que la traduction binaire est d'autant plus simple que l'architecture source et cibles sont semblables. Dans les faits, ce n'est pas tellement traduire les instructions qui pose problème, mais plus la gestion du registre d'état, des exceptions matérielles, de la mémoire virtuelle, la différence entre gros-boutisme et petit-boutisme, de même que des différences d'adressage pour les périphériques. Le premier problème est la gestion des conditions, notamment en présence d'un registre d'état. En pratique, la traduction binaire s'utilise pour traduire du code CISC vers du code RISC ou VLIW. Les architectures source ont donc un registre d'état, qui est mis à jour non seulement par des instructions de test, mais aussi des instructions arithmétiques. L'architecture cible n'a elle pas de registre d'état, mais des registres à prédicats. La traduction de l'un vers l'autre est alors quelque peu compliquée. Et elle est d'autant plus compliquée que le registre d'état est la source de dépendances d'instruction implicites, qui réduisent les performances. Par exemple, si une instruction arithmétique modifie le registre d'état, cela peut impacter l'exécution d'une instruction ultérieure, qui lit ce registre d'état. Pour éliminer ces fausses dépendances, le traducteur binaire doit renommer ce registre en logiciel et détecter les dépendances utiles. Et le compilateur doit gérer les cas où il y a beaucoup de distance entre les deux, voire les cas où l'instruction dépendante n'a pas encore été analysée par le compilateur. Un autre problème est lié à la gestion des exceptions matérielles, et précisément des exceptions précises. Pour rappel, une instruction VLIW regroupe plusieurs opérations, opérations qui correspondraient à une instruction machine sur un CPU pas VLIW. En conséquence, plusieurs instructions du langage machine "source" sont regroupées en une seule instruction VLIW. Et il faut tenir compte du cas où une instruction source lève une exception. Dans le code VLIW, cela signifie qu'une opération lève une exception, et il faut annuler partiellement l'instruction VLIW associée. Et par partiellement, on veut dire que seules les opérations suivantes dans l'ordre du programme source doivent être annulées, puis ré-exécutées. Et c'est un sacré casse-tête ! Les processeurs VLIW utilisent comme solution le mécanisme d'exceptions différées des processeurs EPIC, vu il y a quelques chapitres. Pour résumer, le code est exécuté par blocs d'instruction, délimités par des branchements ou tout autre limite/barrière pertinente dans le code. Un bloc de code est compilé en deux versions : une version rapide sans exceptions matérielles, une version lente qui gère les exceptions précises. La version rapide s'exécute sans exécuter les exceptions. Cependant, les exceptions matérielles sont enregistrées, pour être prises en compte à la toute fin du bloc de code. La version rapide est exécutée en premier et elle mémorise si une exception matérielle a eu lieu. Puis une instruction vérifie si une exception a eu lieu et décide quoi faire. Si aucune exception n'a eu lieu, elle passe à la suite du programme, les résultats du bloc de code sont définitivement acceptés. Mais si une exception a eu lieu, tout est annulé. Le processeur est remis dans l'état initial, puis le code est ré-exécuté instruction par instruction de manière à gérer l'exception correctement. ===La traduction par pré-décodage=== Une première solution serait de faire la traduction binaire dans le cache d'instruction. Lors d'un défaut de cache, le code chargé depuis la RAM est traduit en code machine cible. Et c'est ce code machine cible qui est mémorisé dans le cache d'instruction et exécuté par le processeur. La technique marche sur le papier et n'est qu'une amélioration des techniques de pré-décodage vues il y a quelques chapitres. Cependant, le code obtenu est une traduction assez basique, qui n'incorpore pas d'optimisations dignes de ce nom. De plus, elle gère mal le cas où la taille du code source et cible sont potentiellement très différentes. Concrètement, elle est surtout utile pour traduire du code d'un processeur RISC vers un autre processeur RISC, dont les tailles d'instructions sont similaires. Elle ne permet pas d'émuler plusieurs jeux d'instructions différents, le cout en matériel (un circuit de pré-décodage par jeu d'instruction) serait trop important. ===Le projet DAISY d'IBM=== De très rares processeurs étaient conçus pour accélérer cette traduction binaire, afin de garder de bonnes performances. Les premiers à avoir étudié l'idée étaient IBM, avec leur projet DAISY (''Dynamically Architected Instruction Set from Yorktown''). Le projet de base était de convertir à la volée du code compilé pour des CPU Power PC, vers du code VLIW. Les chercheurs d'IBM avaient développé un algorithme de traduction dynamique efficace, ainsi qu'un processeur VLIW disposant d'optimisations spécifiques à la traduction binaire dynamique, à la volée. De nombreuses idées de ce projet ont été reprises ou re-découvertes par la société Transmetta, avec ses processeurs Crusoe et Efficieon, puis par NVIDIA avec son projet Denver. Les ingénieurs de ce projet ont étudié la possibilité d'émuler plusieurs jeux d'instructions différents sur un même processeur, notamment le s390 d'IBM et le x86. En théorie, cela demande juste d'avoir plusieurs programmes de traduction binaire : un pour le x86, un autre pour le s390, éventuellement un autre pour le Power PC. Mais cela ne s'est pas concrétisé. Mais les ingénieurs ont étudié quelle pourrait être l'architecture VLIW idéale pour ça. Par exemple, il fallait des additions 3-opérandes pour simplifier les calculs d'adresse. Une difficulté était la gestion du registre d'état, dont les bits ne sont pas mis à jour de la même manière sur le x86 et le s390 ou le Power PC. La différence entre gros-boutisme et petit-boutisme était aussi un problème, de même que des différences d'adressage pour les périphériques. Pour que la traduction binaire soit efficace, le processeur VLIW intègre diverses optimisations, comme les exceptions différées et les branchements multi-voies. La principale est la suivante : seul le code des boucles ou fonctions exécutées fréquemment est traduit en code VLIW, le reste du code est interprété. En effet, traduire du code binaire prend plus de temps qu'une simple interprétation. Pour du code qui ne sera exécutée qu'une seule fois, il est plus rapide d'utiliser l'interprétation. Par contre, pour du code exécuté beaucoup de fois, le cout de la traduction binaire est amorti, dilué sur N exécutions, compensé par le gain en temps d'exécution de ce code traduit. Au final, cela permet de ne traduire que le code qui le mérite. La détection du code fréquemment exécuté est réalisée dans le cœur VLIW. L'unité de branchement mémorise les derniers branchements rencontrés et le nombre de fois qu'ils ont été exécutés. Si ils ont été exécutés un certain nombre de fois, l'unité de branchement lève une exception matérielle, qui invoque le traducteur binaire. La routine de cette exception est le traducteur binaire proprement dit. En clair, la traduction binaire est démarrée quand le processeur détecte qu'une fonction a été exécutée plus de N fois, via une exception matérielle dédiée. La mesure du nombre d'exécution d'un branchement se fait dans l'unité de calcul dédiée aux branchements, avec l'aide d'une sorte de ''branch adress buffer'' modifié. Pour rappel, le ''branch adress buffer'' mémorise le ''Program counter'' de chaque branchement récemment rencontré. Ici, chaque entrée du ''branch adress buffer'' est associée à un compteur incrémenté à chaque exécution du branchement. ===Les processeurs Crusoe et Efficieon de Transmetta=== Les processeurs Crusoe Et Efficeron sont deux processeurs VLIW produits par la société Transmetta, une société californienne rachetée par NVIDIA. Ils étaient conçus pour exécuter spécifiquement des programmes x86, système d'exploitation inclus. L'idée derrière ce projet était d'exécuter du code x86 sans que les applications, ni même le système d'exploitation et le BIOS soient au courant ! Les processeurs Transmetta étaient en réalité conçus pour exécuter un programme unique, le ''Code Morphing Software'' (CMS), qui traduisait le code x86 en code VLIW. Transmetta ne voulait pas que son processeur VLIW soit exploité directement, sans le CMS. Il n'avait pas rendu public de compilateur pour traduire du C vers du code VLIW, il n'avait pas donné la documentation du jeu d'instruction VLIW. Des efforts de rétro-ingénieurie, documentés sur le site realworldtech, ont cependant permis de comprendre comment étaient encodées les instructions du processeur Crusoe. Comme pour le projet Daisy, le ''Code Morphing Software'' utilisait à la fois interprétation et traduction binaire, selon les besoins. Les instructions sont interprétées lors de leurs premières exécutions, mais le CMS bascule sur de la traduction en code VLIW après un certain nombre d'exécution. Ainsi, les boucles souvent exécutées sont traduites en code VLIW, alors que le reste du code est interprété. Le choix du code à traduire est le fait du CMS, il utilise des heuristiques complexes pour, qui ne sont pas connues dans le détail. Le ''Code Morphing Software'' est mémorisé dans une EEPROM, ce qui en fait un ''firmware'' situé en dessous du BIOS. Le CMS démarre ensuite le BIOS, qui lui-même démarre le système d'exploitation, qui lui-même démarre les pilotes de périphériques et les programmes. Le CMS se réserve les 16 premiers mébioctets de l'espace d'adressage. L'EEPROM du CMS est mappée dedans, mais n'en utilise que la moitié. Le reste est utilisé comme cache, pour mémoriser le code VLIW traduit par le CMS. Les deux processeurs intégraient aussi deux mémoires SRAM, utilisées par le CMS, appelées la ''local program memory'' (LPM) et la ''local data memory'' (LDM). La première contient du code qui gére les interruptions, la mémoire virtuelle, les exceptions matérielles, les problèmes d'alignement mémoire, et quelques fonctions très fréquemment utilisées par le CMS. La seconde est de la mémoire RAM utilisée par ces fonctions de la ''local program memory''. Les mémoires caches L1 et L2 sont séparées de ces deux mémoires. Le processeur Crusoe et Efficieon étaient des processeurs VLIW très simples. Ils n'avaient même pas de MMU, ni de fonctionnalités importantes sur les CPU x86. Pour Crusoe, les instructions VLIW étaient encodées sur 64 ou 128 bits, et regroupaient entre 2 à 4 opérations. Les instructions LOAD/STORE ne supportaient pas d'adressage indicé, juste de l'adressage indirect à registre. Crusoe avait 5 unités de calcul : deux ALU entières, une unité LOAD/STORE, une unité de branchement, une FPU. Efficieon doublait le nombre d'ALU entière et d'unité LOAD/STORE, les instructions VLIW passaient à 256 bits. Le processeur Crusoe contient 160 registres, dont 64 registres généraux, 32 registres flottants. Les registres généraux font 32 bits, ce qui est cohérent avec le fait que les CPU x86 de l'époque étaient des processeurs 32 bits. Quant aux registres flottants, ils faisaient 80 bits, ce qui colle avec la taille des registres flottants de la FPU x87 utilisée à l'époque. Sur les 64 registres généraux, seuls 48 étaient réellement utilisable pour mémoriser des opérandes. Une partie des registres généraux étaient utilisés pour la gestion de la pile, d'autres pour mémoriser l'état du CPU x86 émulé, un registre était un registre zéro non accesible en écriture. La gestion des exceptions est optimisée avec un système d'exceptions différées, le même que celui décrit plus haut. Le processeur utilise un système similaire, pour effectuer des lectures anticipées, qui a été expliqué dans le chapitre sur les processeurs VLIW/EPIC. Sauf qu'il s'agit là d'une optimisation pour gagner en performances en général, pas une aide utile pour gérer des exceptions précises ou toute autre subtilité de la traduction binaire. Pour gérer les exceptions différées et les lectures anticipées, le processeur mémorise l'état du processeur avant de démarrer un bloc de code. Pour cela, le processeur contient des ''shadow registers'', 48 registres entiers et 16 registres flottants. Vous remarquerez qu'il y a autant de registres entiers que de ''shadow registers'' entiers. Les registres entiers/flottants sont mémorisés dans les ''shadow registers'' avant d'exécuter un bloc de code, afin que le processeur puisse revenir à l'état de base. Une autre fonctionnalité liée est le ''gated store buffer''. Le principe est d'éviter toute écriture en mémoire RAM, tant que le code VLIW n'a pas émis d'instruction ''commit''. L'idée est que les écritures sont conservées dans la ''store queue'' de l'unité LOAD/STORE. Pour rappel, la ''store queue'' met en attente les écritures soit tant que les écritures ne sont pas terminées, soit tant que la RAM est occupée. L'idée est que les écritures sont accumulées dans la ''store queue'' tant que l'instruction ''commit'' n'a pas eu lieu. Le ''store queue'' est "déversé dans la RAM" seulement quand l'instruction ''commit'' s'exécute. ===Les CPU NVIDIA Denver=== Nvidia a tenté quelque chose de similaire avec son projet Denver. NVIDIA a racheté l'entreprise Transmetta, ses brevets, et a tenté de refaire la même chose. Le projet initial était de traduire du code x86 vers un jeu d'instruction VLIW propriétaire, comme l'a fait Transmetta. Mais NVIDIA n'a pas réussi à acquérir la licence du jeu d'instruction x86 et s'est rabattu sur le jeu d'instruction ARM. Le processeur né de ce projet est le Tegra K1-64 CPU. Il s'agit d'un processeur composé d'un cœur VLIW, couplé à deux décodeurs d'instructions ARM. Les deux décodeurs ARM traduisent une paire d'instructions ARM consécutives en instructions VLIW, pour les exécuter sur le cœur VLIW. Le CPU peut fonctionner selon deux modes : ARM et VLIW. En somme, le processeur supporte deux jeux d'instruction : le jeu d'instruction ARM, et un jeu d'instruction VLIW. En mode ARM, les instructions ARM sont chargées depuis le cache, traduites en VLIW par les décodeurs ARM, puis exécutées par le cœur VLIW. En mode optimisé, le CPU exécute des instructions VLIW chargées depuis le cache d'instruction, les décodeurs ne sont pas utilisés. : Le CPU supporte en réalité deux jeux d'instruction ARM : ARM8 et 7. Mais ils correspondent au même mode pour le CPU. Un point important est que le processeur peut changer très rapidement de mode, en à peine quelques cycles d'horloges. On n'est clairement pas dans le cas des CPU x86, qui mettent des plombes pour passer du mode 32 à 64 bits et inversement. La commutation est tellement rapide qu'on peut considérer que le processeur supporte deux jeux d'instruction simultanément. Et le support simultané de deux jeux d'instruction facilite l'implémentation de la traduction binaire. Encore une fois, inutile de traduire du code qui ne sera exécutée qu'une seule fois, mieux vaut l'exécuter directement dans le mode ARM. Surtout que le processeur supporte un mode ARM qui permet d'exécuter du code ARM sans perte de performance. Par contre, le code fréquemment utilisé, notamment dans des boucles critiques, est traduit en VLIW. Au final, les performances sont optimisées, en limitant le travail du logiciel traducteur binaire. La détection du code fréquemment exécuté se fait différemment que pour les processeurs Transmetta et Daisy. L'unité de branchement ne détecte pas directement le code beaucoup exécuté, même si elle a un rôle à jouer. A la place, la détection est réalisée en logiciel, par un ''thread'' dédié, qui s'exécute sur un cœur séparé. Il détecte le code fréquemment utilisé en regardent les branchements exécutés récemment. Pour cela, le processeur mémorise l'historique des branchements pris récemment et copie cette historique dans une mémoire tampon dédiée, partagée entre tous les cœurs. Vu qu'elle est partagée, le ''thread'' de détection a accès à l'historique sans pertes de performances, seule la copie de l'historique dans ce tampon a un cout en performance, pas son partage. Le code VLIW obtenu après traduction binaire est mémorisé dans la mémoire RAM, dans une portion spécialement réservée pour. Elle fait 128 mébioctets, et est appelée le '''cache d'optimisation''' par NVIDIA. Le cache d'optimisation est protégé en écriture et seul le traducteur binaire peut écrire dedans. Au passage, le traducteur binaire est du code VLIW, ce qui fait qu'il est placé dans le cache d'optimisation. Le processeur détecte automatiquement quand une fonction pour laquelle le code compilé équivalent est disponible. Pour cela, l'unité de chargement contient une table de correspondance entre l'adresse de la fonction ARM, et l'adresse de son équivalent compilé. Quand une fonction est appelée, l'unité de chargement regarde l'adresse de destination du branchement. Si l'adresse est dans cette table, elle récupère l'adresse de la fonction compilée et branche vers celle-ci. Le processeur dispose d'un cache d'instruction de 128 kibioctets, ce qui est très important, mais nécessaire vu la taille des instructions VLIW. Le coeur VLIW dispose de ses propres décodeurs, d'un ''scoreboard'', de 7 unités de calcul et de registres. Pour les unités de calcul, il a deux FPU, deux ALU, une unité de branchement, et deux unités LOAD-STORE faisant aussi ALU. Un point important est que le CPU dispose de 64 registres entiers et de 64 registres flottants. C'est deux fois plus que supporte le jeu d'instruction ARM. En mode ARM, seule la moitié des registres est utilisée. Mais en mode optimisé, le traducteur binaire utilise bien les 64 registres, grâce à une sorte de renommage de registres logicielle. Pour simplifier la traduction binaire, le processeur supporte les techniques vues précédemment. Le processeur intègre un ''gated store buffer'' similaire à celui des processeurs Transmetta. Les techniques d'exceptions différées sont aussi supportées, comme sur les processeurs Transmetta. Il a aussi une unité de préchargement décrite par NVIDIA comme agressive, avec support du préchargement de type anticipé (''runahead''). ===L'émulation de la mémoire virtuelle=== Pour finir, il faut parler de la mémoire virtuelle avec la traduction binaire. Sur le processeur source, le processeur gère à la fois des adresses physiques et virtuelles et sa MMU fait la conversion entre les deux. Sur le processeur VLIW, la MMU est simulée par le programme de traduction binaire, partiellement ou totalement. L'implémentation exacte varie suivant que l'on parle du projet Daisy, des processeurs Transmetta ou de Denver. Sur les processeurs Transmetta, il n'y a pas de MMU ni de mémoire virtuelle. A la place, le programme de traduction binaire émule la mémoire virtuelle du processeur source. Le processeur VLIW ne gère que des adresses physiques, rien d'autre. L'espace d'adressage physique du processeur VLIW a la même taille que celui du processeur émulé, ici des processeurs x86. Il est vraisemblable que les adresses physiques utilisées par le processeur x86 sont les mêmes que celles du processeur VLIW. Sur le projet Daisy, le processeur VLIW gère la mémoire virtuelle via pagination, comme les processeurs émulés. Ce qui fait qu'il gère un espace d'adressage virtuel et un espace d'adressage physique. Pour éviter toute confusion, nous parlerons d'adresse physique/virtuelle VLIW pour les adresses physiques/virtuelles du processeur VLIW, d'adresse virtuelle/physique source pour celle du jeu d'instruction traduit, à savoir du Power PC ou du s390, éventuellement de l'x86. Il faut alors faire le lien entre adresses physiques source et adresses virtuelles VLIW. Pour cela, rien de plus simple : il y a correspondance parfaite. L'adresse physique source numéro N correspond à l'adresse virtuelle VLIW numéro N. Cependant, cela signifie que tout l'espace d'adressage virtuel VLIW serait utilisé par le code à traduire. En réalité, il faut ajouter de la place pour le code traduit et le programme de traduction binaire. La conséquence est que l'espace d'adressage virtuel VLIW est plus large que l'espace d'adressage source. L'espace d'adressage virtuel VLIW est découpé en trois sections : une section pour le code Power PC à traduire, une autre pour le code traduit en VLIW, et une dernière réservée au traducteur binaire. Le programme de traduction est placé dans une ROM mappée en mémoire dans la seconde section. Le reste de la seconde section est réservé à une mémoire RAM utilisée par le programme de traduction. [[File:Mémoire virtuelle sur le projet Daisy.png|centre|vignette|upright=1.5|Mémoire virtuelle sur le projet Daisy]] Il faut noter que chaque page physique source, une fois traduite, correspond à N pages virtuelles VLIW. La raison est que le code traduit prend plus de place que le code machine originel. En conséquence, on doit utiliser une page finale plus grande. Pour le reste, des pages contiguës en mémoire physique source sont elles aussi contiguës en mémoire virtuelle VLIW. Le calcul d'adresse est donc simple : l'adresse physique source est multipliée par N, puis on ajoute l'adresse de base à laquelle commence la section pour le code traduit. ==Les processeurs à jeu d'instruction reconfigurable== Nous venons de voir comment accélérer en matériel la traduction binaire. Mais il existe une alternative qui permet de se passer de traduction binaire, complétement. L'idée est d'avoir un processeur avec un '''jeu d'instruction reconfigurable'''. Le nom est assez parlent : on peut configurer le processeur de manière à supporter le jeu d'instruction de son choix ! Par exemple, on pouvait reconfigurer un CPU de manière à ce qu'il supporte le jeu d'instruction x86, ou qu'il exécute du ''bytecode'' Java, ou du code ARMv8, etc. De tels processeurs sont encore plus rare que ceux vu avant dans ce chapitre, mais ils ont existé ! Les processeurs de ce type avaient un ''microcode réinscriptible'', à savoir que c'était des processeurs microcodés pour lesquels on pouvait modifier le microcode. L'idée était qu'en changeant le microcode, on changeait le jeu d'instruction du processeur. Pour cela, le microcode était associé à une mémoire SRAM dans laquelle le microcode était copié au démarrage du CPU. Il y avait donc une micro-ROM et une micro-RAM. Les instructions étaient décodées en utilisant le microcode dans la micro-SRAM, la micro-ROM ne servait qu'au démarrage. En modifiant le contenu de la micro-SRAM, on pouvait altérer le microcode, voire le remplacer totalement. ===Le Burroughs B1700 : architecture externe=== Un exemple est celui du Burroughs B1700, et il est intéressant d'étudier en détail son architecture. Son architecture est décrite dans le livre "Interpreting Machines : Architecture and Programming of the Bl700/Bl800 Series", écrit par Elliott I. Organick et James A. Hinds. Une partie de ce qui vfa suivre est un très court résumé de ce livre. Il disposait de 4 registres généraux, nommés X, Y, T et L, tous de 24 bits. Les registres T et L étaient découpés en 6 sous-registres de 4 bits, nommés LA LB LC LD LE et LF pour le registre L, et TA TB TC TD TE et TF pour le registre T. Ils étaient adressables comme n'importe quel registre. Un point important est que le registre T avait des possibilités d’extraction spécifiques. Il était possible d'extrait N bits, placés n'importe où dans ce registre, et de les copier dans un autre registre. Nous verrons pourquoi cette possibilité est intéressante et ne sert que pour le registre T. Les registres X et Y étaient reliés à une ALU de 24 bits capable d'effectuer des opérations arithmétiques basiques. Fait étonnant, l'ALU faisait tous les calculs en même temps et fournissait ses résultats dans 7 registres : un registre SUM pour l'addition, un registre DIFFERENCE pour la soustraction, et un registre par opération bit à bit. Les registres pour les opérations bit à bit sont : CMPX et CMPY pour l'opération NOT sur chaque opérande, XANY pour le ET bit à bit, XEOY pour le XOR, XORY pour le OU logique. Les additions et soustractions pouvaient se faire sur des opérandes codés en binaire ou en BCD, suivant le code opération utilisé. A cela, il fallait ajouter deux registres de "résultat" MSKX et MSKY. Ils fournissaient un masque dépendant de la taille du résultat en bits. Cela permettait de gérer des données de 0, 4, 8, 12, 16, 20, et 24 bits, alors que l'ALU faisait 24 bits. Et cela n'était pas limité à 16 ou 8 bits, mais n'importe quelle taille inférieure ou égale à 24 bits était supportée, même des tailles non-conventionnelles comme des résultats codés sur 3 bits, 9 bits, 12 bits, etc. L'ALU peenait en opérande les registres X et Y, mais aussi un registre CYF pour la retenue entrante. En sortie, il fournissait aussi deux retenue sortantes : une pour l'addition et une autre pour la soustraction. Elles étaient mémorisées dans les registres CYL et CYD, respectivement. L'ALU fournissait aussi 12 conditions, qui alimentait un registre d'état. Le registre d'état était segmenté en trois registres appelés XYST, XYCN et BICN. Le processeur contenait de nombreux autres registres de 4 bits, dont les sous-registres de T et L. En tout, il y a 27 registres de 4 bits adressables ! Et le processeur disposait d'une ALU 4 bits pour manipuler ces registres de 4 bits ! Elle est capable de faire des MOV entre registres de 4 bits, des opérations bit à bit, et peut aussi tester si un bit vaut 0 ou 1. La dernière possibilité est très utile pour implémenter les branchements. Pour communiquer avec les entrées-sorties, il disposait de deux registres CMND et DATA : CMND pour envoyer une commande à une entrée-sortie, DATA pour lire ou écrire une donnée. Pour la mémoire, il avait deux registres séparés : READ et WRITE. READ contient la donnée lue lors d'une lecture,; WRITE est pour une donnée à écrire lors d’une écriture. Il s'agit de registres d’interfaçage mémoire, qui ont été promus au rang de registres architecturaux. Les deux registres sont gérés par le microcode, qui est le langage principal de la machine. Les instructions machines configurées font sans, mais le microcode doit y avoir accès facilement. Et vu que le microcode est le langage principal de ce processeur, ces deux registres deviennent architecturaux. ===Une architecture bit-adressable=== Si avoir un microcode réinscriptible est suffisant, quelques choix de ''design'' facilitent l'émulation de jeux d'instructions variés. Par exemple, le CPU Burroughs B1700 avait ajouté une fonctionnalité très importante : il était bit-adressable. En clair, il pouvait adresser la mémoire bit par bit, si nécessaire. L'avantage est que cela permettait d'émuler des architectures dont la taille des registres/''byte''/mots était très différente. Un autre avantage est que cela facilitait la gestion des instructions de taille variable. Détaillons rapidement le premier avantage. Le processeur était un CPU 24 bits, mais il pouvait émuler des processeurs 16 bits, 8 bits, 4 bits, 1 bit, 9 bits, ou toute autre valeur. Pour cela, le processeur lisait des mots de 24 bits et utilisant un ''barrel shifter'' pour sélectionner les bits adéquats. Par exemple, pour un accès de 16 bits, la donnée lue en mémoire était masquée de manière à ne garder que les 16 bits de poids faible. Le ''barrel shifter''/circuit de masquage était placé directement avant le bus mémoire, sur le trajet des données/instructions. Le ''barrel shifter'' était commandé par un registre ''Field Unit'', qui précisait quelle était la taille des données à charger. ===La gestion de la taille des instructions=== Les instructions à décoder n'ont pas de longueur fixe, à savoir qu'elles peuvent faire 16 bit avec tel jeu d'instruction, 8 bits avec un autre, 32 bits sur un autre, etc. Elles peuvent aussi être de longueur fixe ou variable ! Le B1700 intégrait donc un support des instructions de longueur variable, qui était "détourné" pour gérer des jeux d'instruction différents. Notons que vu que l'architecture est bit-adressable, les contraintes d'alignement des instructions disparaissent, ce qui a des avantages qu'on va détailler dans ce qui suit ! L'implémentation des instructions de taille variable ajoutait un registre avant le registre d'instruction. Le registre d'instruction proprement dit était un registre M de 24 bits. Il était précédé par un registre T, dans lequel l'instruction était assemblée, en combinant plusieurs morceaux. Si l'instruction est codée est codée sur moins de 24 bits, le registre T se contente de masquer les bits en trop. Si l'instruction chargée dépasser 24 bits, le chargement se faisait en plusieurs passes, avec plusieurs lectures en mémoire. Dans ce qui suit, on suppose que l'instruction est composée de '''champs''' : l'opcode, les opérandes, les adresses absolues, les constantes immédiates, les numéros de registre, etc. L'idée est que l'instruction est chargée dans le registre T, puis découpée en champs, qui sont copiés dans les registres du processeur ou envoyés au microcode. Je rappelle que le registre T est relié à un ''barrel shifter'', différent au précédent, qui permettait de masquer, décaler et extraire une donnée de ce registre. Et il y a la possibilité d'extraire une suite de bit de ce registre T, pour la copier dans un autre registre. Par exemple, prenons une instruction composée d'un opcode, d'une constante immédiate et d'une adresse immédiate. L'opcode est lu et interprété par le microcode. Le microcode extrait la constante immédiate du registre T et la copie dans le registre X. Ensuite, l'adresse est extraite et envoyée au registre FA, une lecture est démarrée, et l'opérande est copiée dans le registre Y. Enfin, l'instruction est exécutée par le microcode. Le tout prend plusieurs micro-opérations : une première pour lire l'opcode, une seconde pour lire la première opérande, une troisième pour lire la seconde opérande, una quatrième pour tout exécuter. Notons que suivant la taille des champs, on peut en charger plusieurs à la fois. Par exemple, imaginons qu'on ait chargé 24 bits, contenant un opcode de 8 bits et une constante immédiate de 16 bits : pas besoin de faire deux accès mémoire, le registre T est juste découpé en un opcode et une constante. Le registre T est fait pour ce genre d'extraction. ===Le chargement d'un champ=== L'architecture est bit-adressable car elle gére des champs qui ne font pas forcément 8 ou 16 bits. Par exemple, certains jeux d'instructions ont des opcodes codés sur 6 ou 7 bits, voire 9-10 bits. Par exemple, prenons un opcode de 7 bits, suivi par une adresse de 12 bits et une constante immédiate de 15 bits : aucun de ces champs ne sera aligné sur 8/16/24 bits. Utiliser une architecture bit-adressable règle le problème d'alignement. Et en général, les contraintes d'alignement des instructions disparaissent ! Quand on charge une instruction, peut importe qu'elle soit ou non alignée sur 24 bits, elle sera placée dans les bits de poids faible, il suffira de masquer les bits de poids fort inutiles. Par exemple, si on charge une instruction de 16 bits, les 16 bits seront copiés dans les 16 bits de poids faible du registre T, il suffira de masquer l'octet de poids fort. Pas besoin de faire de décalages pour gérer des instructions mal alignées... Pour charger une instruction, il faut connaitre plusieurs informations : l'adresse de son premier bit, sa taille. Pour cela, le B1700 contient deux registres, nommés ''Field Adress'' (FA) et ''Field Length'' (FL), qui indiquent l'adresse du premier bit et la taille de l'instruction à charger. Le premier fait 24 bits, ce qui permet de gérer des adresses de 24 bits. N'oubliez pas que le processeur est bit-adressable, ce qui permet d'adresser 16 Mébi-bits, soit 2 mébioctets. Le registre pour la taille fait lui 16 bits, ce qui permet de gérer des instructions de 65536 bits, soit 8192 octets ! Leur nom commence par ''field'', car le processeur peut gérer des instructions de taille variable et/ou plus longues que 24 bits. Dans ce cas, le chargement de l'instruction se fait champ par champ. Pour donner un exemple, prenons une instruction composée de 20 champs de 12 bits chacun. Les champs sont chargés un par un dans le registre T. Le registre FA est alors initialisé avec l'adresse de l'instruction, il est incrémenté de 12 à chaque cycle. Le registre FL est initialisé avec la taille de l'instruction, soit 20 * 12 = 240 bits, et il est décrémenté de 16 à chaque cycle. Précisons cependant que la taille d'un champ peut être changée d'un cycle à l'autre. Par exemple, le processeur peut charger un opcode de 8 bits, l'interpréter, détecter que l'instruction demande ensuite de charger une adresse de 16 bits. Dans ce cas, le registre FL est immédiatement altéré pour charger 16 bits, au lieu des 8 bits de l'opcode immédiatement précédent. ===Le microcode réinscriptible=== Le microcode utilise des micro-opérations de 16 bits de long, ce qui est très peu et bien en-dessous des micro-opérations des autres processeurs. Les micro-opérations sortant du microcode sont envoyées au registre M, un registre de micro-opération de 16 bits. les micro-opérations écrites dans ce registre sont ensuite décodées par le hardware et exécutées. Fait intéressant, il était possible de faire un OU logique entre la micro-opération sortante et un registre du processeur, le résultat étant mémorisé dans le registre M. Cela permettait de faire du code automodifiant dans le microcode ! Utiliser du code automodifiant avec un microcode réinscriptible est parfaitement possible, et c'est utile pour émuler des modes d'adressage complexe dans le microcode. Cela permettait par exemple de remplacer une adresse dans une micro-opération. L'adresse est alors surimposée avec un OU dans le champ d'adresse, alors que le reste de l'instruction n'est pas modifié. Mais modifier directement le microcode est rarement une bonne idée. Aussi, pour éviter cela, l'idée est de modifier les instructions avec un OU logique avec un autre registre. Les micro-opérations du microcode ne sont donc pas modifiées dans le microcode, mais en sortie du microcode. La RAM mémorisant le microcode était de petite taille, et il arrivait qu'elle soit trop petit pour mémoriser le microcode complet. Dans ce cas, le programmeur devait gérer la situation en découpant le microcode en segments, certains étant placé dans le processeur, les autres étant en mémoire RAM. Le processeur peut lire le microcode placé en mémoire RAM et même l'exécuter, mais avec une pénalité en termes de performances. La documentation appelle cela un '''''split-level control store'''''. Quelques registres intégrés au processeur permettent ce genre de magie. Un tel microcode permettait de placer le microcode le plus fréquemment utilisé dans le processeur, alors que le reste du microcode était placé en mémoire RAM. Les instructions fréquentes étaient ainsi décodées dans le processeur, alors que les instructions peu fréquentes étaient décodées en lisant le microcode associé en mémoire RAM. La pénalité en termes de performance est alors concentrée sur les instructions les plus rares, ce qui est un très bon compromis. Mieux que ça, le processeur autorisait d'utiliser la technique de l'''overlaying'' pour le microcode ! Le microcode pouvait servir pour émuler un autre jeu d'instruction, mais il était aussi possible d'implémenter un véritable interpréteur, grâce aux techniques du paragraphe précédent. Le processeur était même conçu pour ! L'interpréteur était placé dans un "segment" en mémoire RAM, dont le processeur connaissait la position. Il incorporait pour cela deux registres, un pour l'adresse de base de l'interpréteur, un autre pour son adresse de fin. Il incorporait une protection mémoire limitée sur ce segment, à savoir que seul l'interpréteur pouvait lire ou écrire dans ce segment. Afin de gérer les interruptions, ces dernières étaient partiellement détectées en logiciel. Les interruptions ne s'exécutaient pas automatiquement, pour émuler correctement les interruptions. Une interruption doit être prise en compte à la toute fin de l'exécution d'une instruction, quand celle-ci a finit son travail. Mais vu qu'ici, les instructions sont émulées avec des séries de micro-opérations, il y a un problème : il est possible d'interrompre une instruction émulée en plein milieu. Pour éviter cela, le processeur ne déclenche pas les interruptions matérielles immédiatement, elles sont mises en attente. Une micro-opération dédiée vérifie si une interruption est en attente, et l'exécute alors cas échéant. La micro-opération est utilisée comme micro-opération finale d'une instruction émulée. ==Les jeux d'instructions dédiés à un langage de programmation== De rares processeurs sont conçus pour un langage de programmation en particulier. On appelle ces processeurs, conçus pour des besoins particuliers, des '''processeurs dédiés'''. Par exemple, les fameux ''Burrough E-mode'' B5000/B6000/B7000 étaient spécialement conçus pour exécuter de l'ALGOL-60. Leurs cousins B2000/B3000/B4000 étaient eux conçus pour le COBOL. Des langages fonctionnels ont aussi eu droit à leurs processeurs dédiés. Le prolog en est un bel exemple, avec les superordinateurs de 5ème génération qui lui étaient dédié. On peut aussi citer les machines LISP, dédiés au langage LISP, qui datent des années 1970. Elles étaient capables d’exécuter certaines fonctions de base du langage directement dans leurs circuits : elles possédaient notamment un ''garbage collector'' câblé dans ses circuits ainsi que des instructions machines supportant un typage déterminé à l’exécution. Les processeurs dédiés ont eu leur heure de gloire au début de l'informatique, à une époque où les langages de haut niveau venaient d'être inventés. À cette époque, les compilateurs n'étaient pas performants et ne savaient pas bien optimiser le code machine. Il était alors rationnel, pour l'époque, de rapprocher le code machine cible et le langage de programmation de haut niveau. De nombreuses architectures dédiés ont ainsi été inventées, avant que les concepteurs se rendent compte des défauts de cette approche. Les défauts en question ne sont pas nombreux, mais assez simples à comprendre. Premièrement, elles sont très rapides pour un langage de programmation en particulier, mais sont assez mauvaises pour les autres, d'où un problème de "compatibilité". Ajoutons à cela que les langages de programmation peuvent évoluer, devenir de moins en moins populaires/utilisés, ce qui rend la création d'architectures généralistes plus pertinente. Enfin, les architectures dédiées sont évidemment des processeurs CISC, pour implémenter les nombreuses fonctionnalités des langages évolués. Et les défauts des CISC sont assez rédhibitoires à l'heure actuelle. À l'heure actuelle, les algorithmes des compilateurs se sont améliorés et savent nettement mieux utiliser le matériel. Ils produisent du code machine efficace, ce qui rend les architecture dédiées bien moins intéressantes. Si on ajoute les défauts de ces architectures dédiées, par étonnant que les architectures dédiées aient presque disparues. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'accélération matérielle de la virtualisation | prevText=L'accélération matérielle de la virtualisation | next=Le matériel réseau | nextText=Le matériel réseau }} </noinclude> 5mtvt7u2dxsh9hhlv610hght5alp4pn 764030 764029 2026-04-19T18:37:52Z Mewtow 31375 /* Le microcode réinscriptible */ 764030 wikitext text/x-wiki De nos jours, la majorité des programmeurs programment dans des langages de haut niveau. Il est très rare pour eux d'avoir à utiliser de l'assembleur, et encore moins un langage machine. Les programmes écrits dans un langage de haut niveau sont traduits en langage machine par un logiciel appelé le compilateur. [[File:Compilation.PNG|centre|vignette|upright=2.5|Compilateur : principe.]] Il se trouve que quelques rares processeurs sont conçus pour faciliter le travail du compilateur. Et ce sont ces processeurs que nous allons voir dans ce qui suit. Ils représentent une catégorie de jeux d'instruction à part, qui n'a pas vraiment de nom. ==La compilation et l’interprétation== Un compilateur traduit donc du code source, écrit dans un langage de haut niveau, vers du langage machine. La traduction est rarement directe. En général, le compilateur traduit le code source en assembleur, qui est lui-même traduit en langage machine. L'assembleur est une représentation textuelle du langage machine alors que le code machine est du binaire exécutable par le processeur. La traduction se fait donc en deux étapes, la '''compilation''' proprement dite et l''''assemblage''', réalisées respectivement par un compilateur et un assembleur. Il faut parfois rajouter une troisième phase d'édition des liens, que nous passons volontairement sous silence. {| |[[File:Code C.png|vignette|200px|Le code source, ici en C.]] |[[File:Assem.png|vignette|250px|Le code assembleur.]] |[[File:Binary file - hello world (C programming).png|vignette|150px|Le code machine.]] |} ===L'interprétation et la compilation à la volée=== Une alternative à la compilation est l''''interprétation''', qui transforme le code source en code machine à la volée. Avec l'interprétation, le code source est passé à un logiciel appelé l''''interpréteur''', qui exécute le code source ligne de code par ligne de code. Une ligne de code est traduite en code machine, qui est exécutée, puis l'interpréteur passe à la ligne suivante. [[File:Kaantotulkkaus.png|centre|vignette|upright=3|Compilation versus interprétation.]] Un défaut de l'interprétation est son cout en performance, qui est assez important. Aussi, l'interprétation stricte a évolué vers un hybride entre interprétation et compilation. L'idée est qu'une partie peu importante du code source est interprétée, alors que le code important est compilé juste avant d'être exécuté. Pour le dire autrement, le code source est partiellement compilé à la volée, juste avant son exécution. Aussi, on parle de '''compilation à la volée'''. Le terme en anglais est ''Just In Time compilation'', abrévié en JIT. En soi, le JIT est associé à de l'interprétation, les JIT sont en réalité des hybrides interpréteurs-compilateurs. Un point important est que compiler du code à la volée est assez lourd, cela a un cout en performance. Par contre, le code compilé s'exécute plus vite que du code interprété. Si on exécute le code compilé une seule fois, le cout de la compilation l'emporte sur le gain à l'exécution. Mais le code compilé est réutilisé autant de fois que nécessaire. Le gain à l'exécution est donc multiplié par le nombre d'exécutions, alors que le cout de la compilation est répartit sur plusieurs exécutions, il est amorti. Les portions du code sont choisies de manière à avoir un gain en performance maximal. L'idée générale est que le code exécuté très souvent est compilé, alors que du code exécuté pas souvent est interprété. Typiquement, les boucles les plus souvent exécutées sont compilées, le code exécuté une seule fois est interprété. ===Les langages intermédiaires=== Les compilateurs modernes passent par un '''langage intermédiaire''' pour faire la transformation en code machine. Le compilateur traduit le langage de haut niveau en langage intermédiaire, puis traduit le langage intermédiaire en code machine cible. Le langage intermédiaire est parfois appelé le '''''bytecode''''', et ce terme recouvre aussi le code écrit avec du ''bytecode''. L'interprétation aussi peut passer par l'intermédiaire d'un un ''bytecode'', ce n'est pas limité aux compilateurs proprement dit. {| |[[File:Intermediate representation scheme.png|centre|vignette|upright=1.5|Représentation intermédiaire d'un compilateur.]] |[[File:Code interpreter scheme.png|centre|vignette|upright=1.5|Fonctionnement d'un interpréteur.]] |} Faire ainsi a de nombreux avantages, le principal étant d'avoir un compilateur capable de traduire un langage de haut niveau vers plusieurs jeux d'instructions différents. Cela permet d'avoir, par exemple, un compilateur qui traduit du C soit en code machine x86, soit en code machine pour un CPU ARM, soit pour un CPU POWERPC, etc. [[File:Compiler design.svg|centre|vignette|upright=3|Compiler design]] Pour l'interprétation, l'usage d'un ''bytecode'' a d'autres avantages. Pour l'exploiter, il faut distribuer non pas le code source, mais le ''bytecode''. En clair, l'interpréteur ne prend pas en entrée le code source, mais du ''bytecode'' déjà compilé. La traduction est alors beaucoup plus simple, car le code source a déjà été partiellement compilé, par la traduction en ''bytecode''. Interpréter du code source est en effet assez compliqué : il faut effectuer des étapes d'analyse lexicale, sémantique, et bien d'autres. Avec du ''bytecode'', ces étapes ont été réalisées lors de la compilation du ''bytecode'', l'interpréteur a alors peut de choses à faire. L'interprétation du ''bytecode'' se fait instruction par instruction, au niveau du ''bytecode''. Une instruction du ''bytecode'' est traduite en une instruction machine équivalente, qui est exécutée. Si l'instruction est un peu complexe, il exécute une fonction/procédure qui fait la même chose. Pour résumer, le ''bytecode'' est ensuite traduit à la volée et exécuté instruction par instruction par un logiciel appelé l'interpréteur. [[File:Control table.png|centre|vignette|upright=2|Control table]] Le langage intermédiaire peut être vu comme l'assembleur d'un processeur, n'existe pas forcément dans la réalité, mais dont le jeu d'instruction est décrit en détail. Le processeur en question est appelé une '''machine abstraite''', ou encore une ''machine virtuelle''. Nous utiliserons le terme de machine abstraite dans ce qui suit. La machine abstraite n'est pas la même suivant que l'on cible la compilation ou l'interprétation/JIT. Pour l'interprétation/JIT, la machine abstraite est souvent une machine à pile, car cela simplifie grandement la traduction en code machine final. L'interprétation demande que la traduction du ''bytecode'' en code machine soit la plus simple possible. Et cela demande de prendre en compte pas mal de détails. Par exemple, les différents jeux d'instruction existants n'ont pas le même nombre de registres, ce qui pose problème lors de la traduction du ''bytecode''. Lors de la transformation en code machine, un algorithme d'allocation de registres se débrouille pour traduire le code intermédiaire en code qui utilise un nombre limité de registres. Pour améliorer cette allocation de registres, il y a deux solutions niveau ''bytecode'' : soit utiliser un nombre illimité de registres, soit utiliser une machine à pile. Pour les machines à pile, il existe un algorithme simple et rapide pour traduire un code écrit pour une machine à pile en un code écrit pour un processeur avec des registres, qui se débrouille pas trop mal pour allouer efficacement les registres. C'est un avantage assez important pour les langages interprétés. ==Les processeurs qui exécutent du bytecode== Le ''bytecode'' est un code machine, ce qui signifie qu'il peut en théorie s'exécuter sur un processeur qui implémente le jeu d’instruction associé. Si le jeu d'instruction d'un ''bytecode'' est souvent une description censée être fictive, elle n'en reste pas moins un jeu d'instruction et des caractéristiques précises, qu'on peut l'implémenter en matériel ! Néanmoins, tous les ''bytecode'' ne sont pas égaux de ce point de vue. Certains sont faciles à implémenter en matériel, d'autres non. Les ''bytecodes'' implémentés en matériel sont ceux dont les machines abstraites sont des machines à pile. Un premier exemple est celui des processeurs Pascal MicroEngine, qui exécutaient directement le bytecode du langage Pascal, le fameux UCSD P-code. Un second exemple est la '''machine SECD''', qui sert de langage intermédiaire pour certains compilateurs de langages fonctionnels. Elle a été implémentée en matériel par plusieurs équipes, notamment par les chercheurs de l'université de Calgary en 1989. Dans le même genre, quelques processeurs simples étaient capables d’exécuter directement le ''bytecode'' du langage FORTH. Le FORTH, un des premiers langages à pile de haut niveau, possède de nombreuses implémentations hardware et est un des rares langages de haut niveau dont le ''bytecode'' a été utilisé comme langage machine sur certains processeurs. Par exemple, on peut citer le processeur FC16, capable d’exécuter nativement du ''bytecode'' FORTH. ===Les processeurs Java=== Le cas le plus impressionnant est celui de la machine virtuelle Java, qui est un design de processeur comme un autre. En temps normal, le ''bytecode'' Java est compilé ou interprété, mais certains processeurs exécutaient du ''bytecode'' Java directement, sans interprétation ni compilation. Ils sont appelés des '''processeurs Java'''. On peut citer les processeurs ARM disposant de l'extension Jazelle, du JEMCore et du aJ-100 de aJile Systems, le picoJava II de Sun, et quelques autres. Il y a aussi eu quelques projets de recherche ou processeurs open source, comme Komodo, jamuth, le ''Java Optimized Processor'' et quelques autres projets du même genre. L'implémentation de la machine virtuelle Java n'était cependant pas complète. Quelques instructions complexes n'étaient pas gérées par le processeur et devaient être émulées en logiciel. Mais la grosse majorité des instructions l'était. Les instructions non-supportées étaient émulées avec la méthode ''trap and emulate'', à savoir qu'un opcode inconnu déclenchait une exception matérielle ''opcode invalide'', dont la routine pouvait émuler les instructions qui doivent l'être. Le processeur aJ-100 de aJile est capable d'exécuter la quasi-totalité des instructions de la JVM Java, seules deux instructions faisant exception. Les '''accélérateurs Java''' sont une solution intermédiaire, qui permet de traduire à la volée du ''bytecode'' Java en instructions RISC, exécutées sur un autre processeur. Il s'agit formellement de traduction binaire, mais je préfère en parler ici. L'idée est de combiner un processeur RISC avec un coprocesseur Java. Le coprocesseur lit le ''bytecode'' Java et le traduit en instructions RISC, ces dernières étant exécutées sur le processeur RISC. Le JA108 de Nozomi était un coprocesseur de ce genre, au même titre que le coprocesseur JSTAR de JEDI. L'extension Jazelle de certains CPU ARM fonctionnait vraisemblablement sur un principe similaire, sauf que le coprocesseur était intégré dans le processeur ARM, entre le cache d'instruction et le décodeur d'instructions ARM. L'intérêt des processeurs Java n'est pas qu'une question de performance. Certes, ils permettent d’exécuter plus vite les programmes compilés en ''bytecode'', comme des programmes Java pour la JVM Java. Mais l'intérêt est aussi de faire des économies de mémoire RAM et ROM. Je rappelle que la machine virtuelle Java est une machine à pile, ce qui fait que sa densité de code est excellente. Les programmes en ''bytecode'' Java sont donc très petits, et prennent peu de place en mémoire ROM. De plus, utiliser un processeur Java permet de se passer d'interpréteur ou de JIT Java, ce qui économise encore plus de mémoire. ===Transformer un processeur RISC en machine à pile=== La plupart des processeurs Java utilisent en interne une architecture à registre généraux, qui émule une machine à registre en microcode. Quelques registres sont réservés pour l'état de la machine virtuelle, avec au minimum un registre réservé pour le pointeur de pile, un autre pour un pointeur vers un ''pool'' de constantes, et quelques autres. D'autres registres servent à mémoriser le haut de la pile, à savoir le sommet de la pile et quelques opérandes situées juste en-dessous. Par exemple, le processeur picoJava II gardait les 64 opérandes au sommet de la pile dans 64 registres généraux, séparés des registres pour les pointeurs et l'état de la JVM. Une optimisation possible, envisagée sur les processeurs picoJava, était une sorte de macro-fusion sous stéroïde. L'idée était de fusionner certaines séries d'instructions Java en une seule instruction machine. Typiquement, pour faire une opération arithmétique, une machine à pile empile deux opérandes et exécute une opération, ce qui prend trois instructions machine. La spécification picoJava 1 a proposé d'envoyer directement la seconde opérande en entrée de l'ALU, ce qui fusionnait l'empilement de la seconde opérande et l'opération arithmétique. Le processeur picoJava II va plus loin et fusionne les trois instructions en une seule instruction RISC, agissant sur des registres. Mais la majorité des processeurs Java ne faisaient pas cela, car cela complexifie grandement le décodeur. Les processeurs ''bigfoot'' de DCT utilisaient un système différent pour émuler une pile avec des registres. Leur banc de registre de 64 bits était coupé en deux : les 16 premiers registres étaient utilisés pour la pile, les 48 suivants étaient utilisés pour autre chose. L'idée est que le registre 0 mémorise un emplacement vide, le registre 1 contient le sommet de la pile, le registre 2 l'opérande sous le sommet de la pile, et ainsi de suite. Les 15 premières opérandes de la pile sont donc mémorisées dans les registres. Les instructions écrivent leur résultat dans le registre adéquat, puis tous les registres sont décalés pour que le résultat passe dans le registre R1. Par exemple, les instructions PUSH chargent une opérande dans le registre 0, puis tous les registres sont décalés de un rang, pour que l'opérande chargée soit dans le registre 1. Le registre 0 devient alors le registre numéro 1, le registre 1 devient le 2, et ainsi de suite. Par contre, les opérations arithmétiques font autrement. Une instruction d'addition, par exemple, lit les opérandes dans le registre 1 et 2, puis place son résultat dans le registre 2 (les deux opérandes sont dépilées). Puis, les registres sont décalés d'un rang pour que ce registre 2 devienne le 1. Les registres ne sont en réalité pas décalés, il n'y a pas de transferts entre registres. A la place, les 16 premiers registres utilisaient une forme limitée de renommage de registres, similaire au banc de registre tournant utilisé sur les processeurs EPIC comme l'Itanium. Les numéros de registres sont renommés, décalés, à chaque fois qu'on empile ou dépile une opérande. Pour faire le renommage de registres, processeur contient un ''register stack counter'' de 4 bits, qui est incrémenté à chaque fois qu'on empile une donnée, et est décrémenté quand on dépile une donnée. Quand on veut accéder à un opérande, le numéro de registre architectural associé est traduit en numéro de registre physique, en additionnant le ''register stack counter'' (modulo 16, pour rester sur 4 bits). ==L'accélération de l'interprétation/JIT : le ''Thumb-EE'' d'ARM== Quelques processeurs ont ajouté des instructions pour faciliter le travail des interpréteurs/JIT. Par exemple, en 2005, ARM a ajouté le mode ''Thumb Execution Environment'', qui faisait cela. Il reprenait le jeu d'instruction compact ''thumb'' et ajoutait quelques instructions et modifiait le comportement d'instructions existantes. Le jeu d'instruction ''Thumb-EE'' a été ajouté sur les CPU ARM en 2005, mais a été déprécié en 2011, par manque d'utilité. Mais la tentative mérite qu'on s'attarde dessus. Les CPU de gamme M, déstinés à l'embarqué, n'ont jamais supporté ''Thumb-EE'', vu que de tels processeurs ne sont pas conçus pour exécuter du code interprété/JIT. Le mode ''Thumb-EE'' est un mode d'exécution, séparé de l'ARM normal. Ainsi, une instruction ''Thumb-EE'' et une instruction ARM peuvent avoir le même encodage en binaire, mais se comporter différemment. Le processeur est à tout moment soit en mode ''Thumb-EE'', soit en mode ARM normal, ce qui précise comment ces instructions doivent se comporter. Des instructions ont été ajoutées par ''Thumb-EE'', pour qu'il fasse son travail. L'entrée et la sortie du mode ''Thumb-EE'' se fait avec deux instructions : ENTERX et LEAVEX. Une fois en mode ''Thumb-EE'', le décodage des instructions ''thumb'' se fait avec les règles du ''Thumb-EE'', et non celles du ''thumb'' normal. Une première différence entre ''thumb'' normal et ''Thumb-EE'' est l'ajout de ce qui s'appelle un ''null check'' pour les instructions mémoire. Le ''null check'' est utilisé pour les instructions en mode d'adressage "base + indice" ou "Base + décalage", ou tout autre mode d'adressage avec un registre de base utilisés dans des calculs d'adresse. L'idée est de vérifier si l'adresse de base vaut zéro ou non. Si c'est le cas, le processeur lève une exception matérielle, qui est traitée par l'interprétation ou le compilateur JIT. Une telle situation est en effet signe d'une erreur d'adressage mémoire, qui doit être traitée par l'interpréteur. Typiquement, c'est signe qu'un pointeur n'a pas bien été initialisé, ce qui peut arriver avec du code interprété ou JIT. Les interpréteurs/JIT traitent généralement la situation en ajoutant un test avant toute instruction mémoire, pour vérifier si le pointeur accédé vaut zéro ou non. Avec ''Thumb-EE'', pas besoin d'ajouter les tests en questions : ils sont réalisés automatiquement lors de chaque instruction mémoire. : Les registres pour la pile, comme le pointeur de pile et le pointeur de ''frame'', sont aussi concernés. L'instruction CHKA vérifie que les accès à un tableau ne débordent pas en dehors du tableau en question. Par exemple, pour un tableau de 1024 éléments, elle vérifie si l'indice est compris entre 0 et 1024. Pour cela, elle vérifie si l'indice de tableau est bien dans l'intervalle adéquat et lève une exception matérielle si ce n'est pas le cas. Son utilité se comprend quand on sait que tous les accès à un tableau sont vérifiés par l'interpréteur. L'interpréteur est censé ajouter des instructions pour vérifier les indices, à savoir deux ou trois branchements. Avec CHKA, le test se fait en une seule instruction. D'autres modifications mineures de l'encodage des instructions sont aussi présentes, ainsi que des modifications pour les instructions LOAD/STORE. Précisément, dans les modes "base + indice" et "base + décalage", le décalage et l'indice subissent maintenant des décalages. L'idée est de simplifier le calcul d'adresse. De quoi économiser quelques instructions lors des calculs d'adresse, ce qui facilite le travail de l'interpréteur/JIT. Et des instructions LOAD/STORE précises voient leur comportement modifié pour faciliter la gestion de la pile de Java ou l'exécution des méthodes locales. Pour faciliter le travail de l'interpréteur, deux instructions HBP et HBLP ont été ajoutées. Elles branchent vers une fonction qui gère les exceptions logicielles (une fonctionnalité présente dans de nombreux langages, comme Java). La première effectue un branchement vers la fonction en question, la seconde sauvegarde l'adresse de retour avant de faire ce branchement. Les deux peuvent brancher vers 256 fonctions pré-déterminées, sans avoir à présenter leur adresse. ==La traduction binaire accélérée par le matériel== Plus haut, nous avons surtout parlé des compilateurs et des interpréteurs. Cependant, nous devons aussi parler de la '''traduction binaire'''. Elle traduit un programme écrit dans un code machine vers un autre code machine. Par exemple, elle traduit un programme compilé pour un CPU x86 vers un code machine ARM. En général, le système d'exploitation est généralement compilé pour le jeu d'instruction natif, mais il exécute des applications prévues pour le x86. Les applications sont traduites par le système d'exploitation, avant d'exécuter le code traduit. : Nous parlerons dans la suite de code machine source et de code machine cible, pour parler respectivement du code à traduire et du code obtenu après traduction. Idem avec d'autres termes comme architecture cible/source, ou jeu d'instruction cible/source. La traduction binaire est surtout utilisée pour des questions d'émulation ou de compatibilité. Par exemple, lorsque les Macintosh sont passés de processeurs Power PC vers des processeurs x86, le système d'exploitation Mac OS utilisait la traduction binaire pour convertir les anciennes applications Power PC vers du code x86. Les utilisateurs n'y ont vu que du feu. Le système de traduction binaire était appelé Rosetta 1. Par la suite, lors de la transition de processeurs x86 vers des processeurs Apple, Rosetta 2 a vu le jour. La traduction binaire a surtout été utilisée pour traduire du code x86 vers un autre jeu d'instruction. Il faut dire que le x86 est le jeu d'instruction dominant. De nombreuses entreprises ont eu pour ambition de briser l'hégémonie du x86 sur PC, en remplaçant le x86 par un jeu d'instruction plus performant, tout en gardant une compatibilité maximale. La traduction binaire était la seule solution pratique. Elle était le plus souvent intégralement réalisée en logiciel, comme c'était le cas sur les architectures Itanium avec le ''IA-32 Execution Layer''. ===Les généralités sur la traduction binaire assistée en matériel=== L'architecture source est presque tout le temps une architecture CISC, assez ancienne, qu'on souhaite émuler. Le choix de l'architecture cible se porte souvent sur une architecture VLIW, fort différente de l'architecture source, et ce pour obtenir une économie de matériel conséquente. Pour s'exécuter rapidement, le code traduit doit exploiter le parallélisme d'instruction, à savoir exécuter plusieurs instructions en même temps dans des unités de calcul séparées. Il est possible d'utiliser un processeur superscalaire avec exécution dans le désordre pour cela, mais au prix d'un cout important en transistors. Alors qu'en utilisant un CPU VLIW, c'est le traducteur binaire qui fait tout le travail d'extraction du parallélisme d'instruction. Et après tout, quitte à avoir un traducteur binaire, autant lui refiler le boulot d'optimisation. Compiler du code à la volée est certes assez lourd, mais qu'il existe des algorithmes efficaces pour regrouper des instructions indépendantes dans une seule instruction VLIW. Au passage, le processeur VLIW a plus de registres que le processeur source. Cela permet de faire du renommage de registres directement en logiciel. Le traducteur binaire n'hésite pas à changer les noms de registres entre instructions source et instruction VLIW cible, afin de supprimer des dépendances de données. En conséquence, cela ouvre des opportunités de parallélisme, qui sont exploitées lors du regroupement des opérations en instructions VLIW. Vous pourriez penser que le choix d'un CPU VLIW pour émuler du CISC est tout sauf optimal. Vous devez penser que la traduction binaire est d'autant plus simple que l'architecture source et cibles sont semblables. Dans les faits, ce n'est pas tellement traduire les instructions qui pose problème, mais plus la gestion du registre d'état, des exceptions matérielles, de la mémoire virtuelle, la différence entre gros-boutisme et petit-boutisme, de même que des différences d'adressage pour les périphériques. Le premier problème est la gestion des conditions, notamment en présence d'un registre d'état. En pratique, la traduction binaire s'utilise pour traduire du code CISC vers du code RISC ou VLIW. Les architectures source ont donc un registre d'état, qui est mis à jour non seulement par des instructions de test, mais aussi des instructions arithmétiques. L'architecture cible n'a elle pas de registre d'état, mais des registres à prédicats. La traduction de l'un vers l'autre est alors quelque peu compliquée. Et elle est d'autant plus compliquée que le registre d'état est la source de dépendances d'instruction implicites, qui réduisent les performances. Par exemple, si une instruction arithmétique modifie le registre d'état, cela peut impacter l'exécution d'une instruction ultérieure, qui lit ce registre d'état. Pour éliminer ces fausses dépendances, le traducteur binaire doit renommer ce registre en logiciel et détecter les dépendances utiles. Et le compilateur doit gérer les cas où il y a beaucoup de distance entre les deux, voire les cas où l'instruction dépendante n'a pas encore été analysée par le compilateur. Un autre problème est lié à la gestion des exceptions matérielles, et précisément des exceptions précises. Pour rappel, une instruction VLIW regroupe plusieurs opérations, opérations qui correspondraient à une instruction machine sur un CPU pas VLIW. En conséquence, plusieurs instructions du langage machine "source" sont regroupées en une seule instruction VLIW. Et il faut tenir compte du cas où une instruction source lève une exception. Dans le code VLIW, cela signifie qu'une opération lève une exception, et il faut annuler partiellement l'instruction VLIW associée. Et par partiellement, on veut dire que seules les opérations suivantes dans l'ordre du programme source doivent être annulées, puis ré-exécutées. Et c'est un sacré casse-tête ! Les processeurs VLIW utilisent comme solution le mécanisme d'exceptions différées des processeurs EPIC, vu il y a quelques chapitres. Pour résumer, le code est exécuté par blocs d'instruction, délimités par des branchements ou tout autre limite/barrière pertinente dans le code. Un bloc de code est compilé en deux versions : une version rapide sans exceptions matérielles, une version lente qui gère les exceptions précises. La version rapide s'exécute sans exécuter les exceptions. Cependant, les exceptions matérielles sont enregistrées, pour être prises en compte à la toute fin du bloc de code. La version rapide est exécutée en premier et elle mémorise si une exception matérielle a eu lieu. Puis une instruction vérifie si une exception a eu lieu et décide quoi faire. Si aucune exception n'a eu lieu, elle passe à la suite du programme, les résultats du bloc de code sont définitivement acceptés. Mais si une exception a eu lieu, tout est annulé. Le processeur est remis dans l'état initial, puis le code est ré-exécuté instruction par instruction de manière à gérer l'exception correctement. ===La traduction par pré-décodage=== Une première solution serait de faire la traduction binaire dans le cache d'instruction. Lors d'un défaut de cache, le code chargé depuis la RAM est traduit en code machine cible. Et c'est ce code machine cible qui est mémorisé dans le cache d'instruction et exécuté par le processeur. La technique marche sur le papier et n'est qu'une amélioration des techniques de pré-décodage vues il y a quelques chapitres. Cependant, le code obtenu est une traduction assez basique, qui n'incorpore pas d'optimisations dignes de ce nom. De plus, elle gère mal le cas où la taille du code source et cible sont potentiellement très différentes. Concrètement, elle est surtout utile pour traduire du code d'un processeur RISC vers un autre processeur RISC, dont les tailles d'instructions sont similaires. Elle ne permet pas d'émuler plusieurs jeux d'instructions différents, le cout en matériel (un circuit de pré-décodage par jeu d'instruction) serait trop important. ===Le projet DAISY d'IBM=== De très rares processeurs étaient conçus pour accélérer cette traduction binaire, afin de garder de bonnes performances. Les premiers à avoir étudié l'idée étaient IBM, avec leur projet DAISY (''Dynamically Architected Instruction Set from Yorktown''). Le projet de base était de convertir à la volée du code compilé pour des CPU Power PC, vers du code VLIW. Les chercheurs d'IBM avaient développé un algorithme de traduction dynamique efficace, ainsi qu'un processeur VLIW disposant d'optimisations spécifiques à la traduction binaire dynamique, à la volée. De nombreuses idées de ce projet ont été reprises ou re-découvertes par la société Transmetta, avec ses processeurs Crusoe et Efficieon, puis par NVIDIA avec son projet Denver. Les ingénieurs de ce projet ont étudié la possibilité d'émuler plusieurs jeux d'instructions différents sur un même processeur, notamment le s390 d'IBM et le x86. En théorie, cela demande juste d'avoir plusieurs programmes de traduction binaire : un pour le x86, un autre pour le s390, éventuellement un autre pour le Power PC. Mais cela ne s'est pas concrétisé. Mais les ingénieurs ont étudié quelle pourrait être l'architecture VLIW idéale pour ça. Par exemple, il fallait des additions 3-opérandes pour simplifier les calculs d'adresse. Une difficulté était la gestion du registre d'état, dont les bits ne sont pas mis à jour de la même manière sur le x86 et le s390 ou le Power PC. La différence entre gros-boutisme et petit-boutisme était aussi un problème, de même que des différences d'adressage pour les périphériques. Pour que la traduction binaire soit efficace, le processeur VLIW intègre diverses optimisations, comme les exceptions différées et les branchements multi-voies. La principale est la suivante : seul le code des boucles ou fonctions exécutées fréquemment est traduit en code VLIW, le reste du code est interprété. En effet, traduire du code binaire prend plus de temps qu'une simple interprétation. Pour du code qui ne sera exécutée qu'une seule fois, il est plus rapide d'utiliser l'interprétation. Par contre, pour du code exécuté beaucoup de fois, le cout de la traduction binaire est amorti, dilué sur N exécutions, compensé par le gain en temps d'exécution de ce code traduit. Au final, cela permet de ne traduire que le code qui le mérite. La détection du code fréquemment exécuté est réalisée dans le cœur VLIW. L'unité de branchement mémorise les derniers branchements rencontrés et le nombre de fois qu'ils ont été exécutés. Si ils ont été exécutés un certain nombre de fois, l'unité de branchement lève une exception matérielle, qui invoque le traducteur binaire. La routine de cette exception est le traducteur binaire proprement dit. En clair, la traduction binaire est démarrée quand le processeur détecte qu'une fonction a été exécutée plus de N fois, via une exception matérielle dédiée. La mesure du nombre d'exécution d'un branchement se fait dans l'unité de calcul dédiée aux branchements, avec l'aide d'une sorte de ''branch adress buffer'' modifié. Pour rappel, le ''branch adress buffer'' mémorise le ''Program counter'' de chaque branchement récemment rencontré. Ici, chaque entrée du ''branch adress buffer'' est associée à un compteur incrémenté à chaque exécution du branchement. ===Les processeurs Crusoe et Efficieon de Transmetta=== Les processeurs Crusoe Et Efficeron sont deux processeurs VLIW produits par la société Transmetta, une société californienne rachetée par NVIDIA. Ils étaient conçus pour exécuter spécifiquement des programmes x86, système d'exploitation inclus. L'idée derrière ce projet était d'exécuter du code x86 sans que les applications, ni même le système d'exploitation et le BIOS soient au courant ! Les processeurs Transmetta étaient en réalité conçus pour exécuter un programme unique, le ''Code Morphing Software'' (CMS), qui traduisait le code x86 en code VLIW. Transmetta ne voulait pas que son processeur VLIW soit exploité directement, sans le CMS. Il n'avait pas rendu public de compilateur pour traduire du C vers du code VLIW, il n'avait pas donné la documentation du jeu d'instruction VLIW. Des efforts de rétro-ingénieurie, documentés sur le site realworldtech, ont cependant permis de comprendre comment étaient encodées les instructions du processeur Crusoe. Comme pour le projet Daisy, le ''Code Morphing Software'' utilisait à la fois interprétation et traduction binaire, selon les besoins. Les instructions sont interprétées lors de leurs premières exécutions, mais le CMS bascule sur de la traduction en code VLIW après un certain nombre d'exécution. Ainsi, les boucles souvent exécutées sont traduites en code VLIW, alors que le reste du code est interprété. Le choix du code à traduire est le fait du CMS, il utilise des heuristiques complexes pour, qui ne sont pas connues dans le détail. Le ''Code Morphing Software'' est mémorisé dans une EEPROM, ce qui en fait un ''firmware'' situé en dessous du BIOS. Le CMS démarre ensuite le BIOS, qui lui-même démarre le système d'exploitation, qui lui-même démarre les pilotes de périphériques et les programmes. Le CMS se réserve les 16 premiers mébioctets de l'espace d'adressage. L'EEPROM du CMS est mappée dedans, mais n'en utilise que la moitié. Le reste est utilisé comme cache, pour mémoriser le code VLIW traduit par le CMS. Les deux processeurs intégraient aussi deux mémoires SRAM, utilisées par le CMS, appelées la ''local program memory'' (LPM) et la ''local data memory'' (LDM). La première contient du code qui gére les interruptions, la mémoire virtuelle, les exceptions matérielles, les problèmes d'alignement mémoire, et quelques fonctions très fréquemment utilisées par le CMS. La seconde est de la mémoire RAM utilisée par ces fonctions de la ''local program memory''. Les mémoires caches L1 et L2 sont séparées de ces deux mémoires. Le processeur Crusoe et Efficieon étaient des processeurs VLIW très simples. Ils n'avaient même pas de MMU, ni de fonctionnalités importantes sur les CPU x86. Pour Crusoe, les instructions VLIW étaient encodées sur 64 ou 128 bits, et regroupaient entre 2 à 4 opérations. Les instructions LOAD/STORE ne supportaient pas d'adressage indicé, juste de l'adressage indirect à registre. Crusoe avait 5 unités de calcul : deux ALU entières, une unité LOAD/STORE, une unité de branchement, une FPU. Efficieon doublait le nombre d'ALU entière et d'unité LOAD/STORE, les instructions VLIW passaient à 256 bits. Le processeur Crusoe contient 160 registres, dont 64 registres généraux, 32 registres flottants. Les registres généraux font 32 bits, ce qui est cohérent avec le fait que les CPU x86 de l'époque étaient des processeurs 32 bits. Quant aux registres flottants, ils faisaient 80 bits, ce qui colle avec la taille des registres flottants de la FPU x87 utilisée à l'époque. Sur les 64 registres généraux, seuls 48 étaient réellement utilisable pour mémoriser des opérandes. Une partie des registres généraux étaient utilisés pour la gestion de la pile, d'autres pour mémoriser l'état du CPU x86 émulé, un registre était un registre zéro non accesible en écriture. La gestion des exceptions est optimisée avec un système d'exceptions différées, le même que celui décrit plus haut. Le processeur utilise un système similaire, pour effectuer des lectures anticipées, qui a été expliqué dans le chapitre sur les processeurs VLIW/EPIC. Sauf qu'il s'agit là d'une optimisation pour gagner en performances en général, pas une aide utile pour gérer des exceptions précises ou toute autre subtilité de la traduction binaire. Pour gérer les exceptions différées et les lectures anticipées, le processeur mémorise l'état du processeur avant de démarrer un bloc de code. Pour cela, le processeur contient des ''shadow registers'', 48 registres entiers et 16 registres flottants. Vous remarquerez qu'il y a autant de registres entiers que de ''shadow registers'' entiers. Les registres entiers/flottants sont mémorisés dans les ''shadow registers'' avant d'exécuter un bloc de code, afin que le processeur puisse revenir à l'état de base. Une autre fonctionnalité liée est le ''gated store buffer''. Le principe est d'éviter toute écriture en mémoire RAM, tant que le code VLIW n'a pas émis d'instruction ''commit''. L'idée est que les écritures sont conservées dans la ''store queue'' de l'unité LOAD/STORE. Pour rappel, la ''store queue'' met en attente les écritures soit tant que les écritures ne sont pas terminées, soit tant que la RAM est occupée. L'idée est que les écritures sont accumulées dans la ''store queue'' tant que l'instruction ''commit'' n'a pas eu lieu. Le ''store queue'' est "déversé dans la RAM" seulement quand l'instruction ''commit'' s'exécute. ===Les CPU NVIDIA Denver=== Nvidia a tenté quelque chose de similaire avec son projet Denver. NVIDIA a racheté l'entreprise Transmetta, ses brevets, et a tenté de refaire la même chose. Le projet initial était de traduire du code x86 vers un jeu d'instruction VLIW propriétaire, comme l'a fait Transmetta. Mais NVIDIA n'a pas réussi à acquérir la licence du jeu d'instruction x86 et s'est rabattu sur le jeu d'instruction ARM. Le processeur né de ce projet est le Tegra K1-64 CPU. Il s'agit d'un processeur composé d'un cœur VLIW, couplé à deux décodeurs d'instructions ARM. Les deux décodeurs ARM traduisent une paire d'instructions ARM consécutives en instructions VLIW, pour les exécuter sur le cœur VLIW. Le CPU peut fonctionner selon deux modes : ARM et VLIW. En somme, le processeur supporte deux jeux d'instruction : le jeu d'instruction ARM, et un jeu d'instruction VLIW. En mode ARM, les instructions ARM sont chargées depuis le cache, traduites en VLIW par les décodeurs ARM, puis exécutées par le cœur VLIW. En mode optimisé, le CPU exécute des instructions VLIW chargées depuis le cache d'instruction, les décodeurs ne sont pas utilisés. : Le CPU supporte en réalité deux jeux d'instruction ARM : ARM8 et 7. Mais ils correspondent au même mode pour le CPU. Un point important est que le processeur peut changer très rapidement de mode, en à peine quelques cycles d'horloges. On n'est clairement pas dans le cas des CPU x86, qui mettent des plombes pour passer du mode 32 à 64 bits et inversement. La commutation est tellement rapide qu'on peut considérer que le processeur supporte deux jeux d'instruction simultanément. Et le support simultané de deux jeux d'instruction facilite l'implémentation de la traduction binaire. Encore une fois, inutile de traduire du code qui ne sera exécutée qu'une seule fois, mieux vaut l'exécuter directement dans le mode ARM. Surtout que le processeur supporte un mode ARM qui permet d'exécuter du code ARM sans perte de performance. Par contre, le code fréquemment utilisé, notamment dans des boucles critiques, est traduit en VLIW. Au final, les performances sont optimisées, en limitant le travail du logiciel traducteur binaire. La détection du code fréquemment exécuté se fait différemment que pour les processeurs Transmetta et Daisy. L'unité de branchement ne détecte pas directement le code beaucoup exécuté, même si elle a un rôle à jouer. A la place, la détection est réalisée en logiciel, par un ''thread'' dédié, qui s'exécute sur un cœur séparé. Il détecte le code fréquemment utilisé en regardent les branchements exécutés récemment. Pour cela, le processeur mémorise l'historique des branchements pris récemment et copie cette historique dans une mémoire tampon dédiée, partagée entre tous les cœurs. Vu qu'elle est partagée, le ''thread'' de détection a accès à l'historique sans pertes de performances, seule la copie de l'historique dans ce tampon a un cout en performance, pas son partage. Le code VLIW obtenu après traduction binaire est mémorisé dans la mémoire RAM, dans une portion spécialement réservée pour. Elle fait 128 mébioctets, et est appelée le '''cache d'optimisation''' par NVIDIA. Le cache d'optimisation est protégé en écriture et seul le traducteur binaire peut écrire dedans. Au passage, le traducteur binaire est du code VLIW, ce qui fait qu'il est placé dans le cache d'optimisation. Le processeur détecte automatiquement quand une fonction pour laquelle le code compilé équivalent est disponible. Pour cela, l'unité de chargement contient une table de correspondance entre l'adresse de la fonction ARM, et l'adresse de son équivalent compilé. Quand une fonction est appelée, l'unité de chargement regarde l'adresse de destination du branchement. Si l'adresse est dans cette table, elle récupère l'adresse de la fonction compilée et branche vers celle-ci. Le processeur dispose d'un cache d'instruction de 128 kibioctets, ce qui est très important, mais nécessaire vu la taille des instructions VLIW. Le coeur VLIW dispose de ses propres décodeurs, d'un ''scoreboard'', de 7 unités de calcul et de registres. Pour les unités de calcul, il a deux FPU, deux ALU, une unité de branchement, et deux unités LOAD-STORE faisant aussi ALU. Un point important est que le CPU dispose de 64 registres entiers et de 64 registres flottants. C'est deux fois plus que supporte le jeu d'instruction ARM. En mode ARM, seule la moitié des registres est utilisée. Mais en mode optimisé, le traducteur binaire utilise bien les 64 registres, grâce à une sorte de renommage de registres logicielle. Pour simplifier la traduction binaire, le processeur supporte les techniques vues précédemment. Le processeur intègre un ''gated store buffer'' similaire à celui des processeurs Transmetta. Les techniques d'exceptions différées sont aussi supportées, comme sur les processeurs Transmetta. Il a aussi une unité de préchargement décrite par NVIDIA comme agressive, avec support du préchargement de type anticipé (''runahead''). ===L'émulation de la mémoire virtuelle=== Pour finir, il faut parler de la mémoire virtuelle avec la traduction binaire. Sur le processeur source, le processeur gère à la fois des adresses physiques et virtuelles et sa MMU fait la conversion entre les deux. Sur le processeur VLIW, la MMU est simulée par le programme de traduction binaire, partiellement ou totalement. L'implémentation exacte varie suivant que l'on parle du projet Daisy, des processeurs Transmetta ou de Denver. Sur les processeurs Transmetta, il n'y a pas de MMU ni de mémoire virtuelle. A la place, le programme de traduction binaire émule la mémoire virtuelle du processeur source. Le processeur VLIW ne gère que des adresses physiques, rien d'autre. L'espace d'adressage physique du processeur VLIW a la même taille que celui du processeur émulé, ici des processeurs x86. Il est vraisemblable que les adresses physiques utilisées par le processeur x86 sont les mêmes que celles du processeur VLIW. Sur le projet Daisy, le processeur VLIW gère la mémoire virtuelle via pagination, comme les processeurs émulés. Ce qui fait qu'il gère un espace d'adressage virtuel et un espace d'adressage physique. Pour éviter toute confusion, nous parlerons d'adresse physique/virtuelle VLIW pour les adresses physiques/virtuelles du processeur VLIW, d'adresse virtuelle/physique source pour celle du jeu d'instruction traduit, à savoir du Power PC ou du s390, éventuellement de l'x86. Il faut alors faire le lien entre adresses physiques source et adresses virtuelles VLIW. Pour cela, rien de plus simple : il y a correspondance parfaite. L'adresse physique source numéro N correspond à l'adresse virtuelle VLIW numéro N. Cependant, cela signifie que tout l'espace d'adressage virtuel VLIW serait utilisé par le code à traduire. En réalité, il faut ajouter de la place pour le code traduit et le programme de traduction binaire. La conséquence est que l'espace d'adressage virtuel VLIW est plus large que l'espace d'adressage source. L'espace d'adressage virtuel VLIW est découpé en trois sections : une section pour le code Power PC à traduire, une autre pour le code traduit en VLIW, et une dernière réservée au traducteur binaire. Le programme de traduction est placé dans une ROM mappée en mémoire dans la seconde section. Le reste de la seconde section est réservé à une mémoire RAM utilisée par le programme de traduction. [[File:Mémoire virtuelle sur le projet Daisy.png|centre|vignette|upright=1.5|Mémoire virtuelle sur le projet Daisy]] Il faut noter que chaque page physique source, une fois traduite, correspond à N pages virtuelles VLIW. La raison est que le code traduit prend plus de place que le code machine originel. En conséquence, on doit utiliser une page finale plus grande. Pour le reste, des pages contiguës en mémoire physique source sont elles aussi contiguës en mémoire virtuelle VLIW. Le calcul d'adresse est donc simple : l'adresse physique source est multipliée par N, puis on ajoute l'adresse de base à laquelle commence la section pour le code traduit. ==Les processeurs à jeu d'instruction reconfigurable== Nous venons de voir comment accélérer en matériel la traduction binaire. Mais il existe une alternative qui permet de se passer de traduction binaire, complétement. L'idée est d'avoir un processeur avec un '''jeu d'instruction reconfigurable'''. Le nom est assez parlent : on peut configurer le processeur de manière à supporter le jeu d'instruction de son choix ! Par exemple, on pouvait reconfigurer un CPU de manière à ce qu'il supporte le jeu d'instruction x86, ou qu'il exécute du ''bytecode'' Java, ou du code ARMv8, etc. De tels processeurs sont encore plus rare que ceux vu avant dans ce chapitre, mais ils ont existé ! Les processeurs de ce type avaient un ''microcode réinscriptible'', à savoir que c'était des processeurs microcodés pour lesquels on pouvait modifier le microcode. L'idée était qu'en changeant le microcode, on changeait le jeu d'instruction du processeur. Pour cela, le microcode était associé à une mémoire SRAM dans laquelle le microcode était copié au démarrage du CPU. Il y avait donc une micro-ROM et une micro-RAM. Les instructions étaient décodées en utilisant le microcode dans la micro-SRAM, la micro-ROM ne servait qu'au démarrage. En modifiant le contenu de la micro-SRAM, on pouvait altérer le microcode, voire le remplacer totalement. ===Le Burroughs B1700 : architecture externe=== Un exemple est celui du Burroughs B1700, et il est intéressant d'étudier en détail son architecture. Son architecture est décrite dans le livre "Interpreting Machines : Architecture and Programming of the Bl700/Bl800 Series", écrit par Elliott I. Organick et James A. Hinds. Une partie de ce qui vfa suivre est un très court résumé de ce livre. Il disposait de 4 registres généraux, nommés X, Y, T et L, tous de 24 bits. Les registres T et L étaient découpés en 6 sous-registres de 4 bits, nommés LA LB LC LD LE et LF pour le registre L, et TA TB TC TD TE et TF pour le registre T. Ils étaient adressables comme n'importe quel registre. Un point important est que le registre T avait des possibilités d’extraction spécifiques. Il était possible d'extrait N bits, placés n'importe où dans ce registre, et de les copier dans un autre registre. Nous verrons pourquoi cette possibilité est intéressante et ne sert que pour le registre T. Les registres X et Y étaient reliés à une ALU de 24 bits capable d'effectuer des opérations arithmétiques basiques. Fait étonnant, l'ALU faisait tous les calculs en même temps et fournissait ses résultats dans 7 registres : un registre SUM pour l'addition, un registre DIFFERENCE pour la soustraction, et un registre par opération bit à bit. Les registres pour les opérations bit à bit sont : CMPX et CMPY pour l'opération NOT sur chaque opérande, XANY pour le ET bit à bit, XEOY pour le XOR, XORY pour le OU logique. Les additions et soustractions pouvaient se faire sur des opérandes codés en binaire ou en BCD, suivant le code opération utilisé. A cela, il fallait ajouter deux registres de "résultat" MSKX et MSKY. Ils fournissaient un masque dépendant de la taille du résultat en bits. Cela permettait de gérer des données de 0, 4, 8, 12, 16, 20, et 24 bits, alors que l'ALU faisait 24 bits. Et cela n'était pas limité à 16 ou 8 bits, mais n'importe quelle taille inférieure ou égale à 24 bits était supportée, même des tailles non-conventionnelles comme des résultats codés sur 3 bits, 9 bits, 12 bits, etc. L'ALU peenait en opérande les registres X et Y, mais aussi un registre CYF pour la retenue entrante. En sortie, il fournissait aussi deux retenue sortantes : une pour l'addition et une autre pour la soustraction. Elles étaient mémorisées dans les registres CYL et CYD, respectivement. L'ALU fournissait aussi 12 conditions, qui alimentait un registre d'état. Le registre d'état était segmenté en trois registres appelés XYST, XYCN et BICN. Le processeur contenait de nombreux autres registres de 4 bits, dont les sous-registres de T et L. En tout, il y a 27 registres de 4 bits adressables ! Et le processeur disposait d'une ALU 4 bits pour manipuler ces registres de 4 bits ! Elle est capable de faire des MOV entre registres de 4 bits, des opérations bit à bit, et peut aussi tester si un bit vaut 0 ou 1. La dernière possibilité est très utile pour implémenter les branchements. Pour communiquer avec les entrées-sorties, il disposait de deux registres CMND et DATA : CMND pour envoyer une commande à une entrée-sortie, DATA pour lire ou écrire une donnée. Pour la mémoire, il avait deux registres séparés : READ et WRITE. READ contient la donnée lue lors d'une lecture,; WRITE est pour une donnée à écrire lors d’une écriture. Il s'agit de registres d’interfaçage mémoire, qui ont été promus au rang de registres architecturaux. Les deux registres sont gérés par le microcode, qui est le langage principal de la machine. Les instructions machines configurées font sans, mais le microcode doit y avoir accès facilement. Et vu que le microcode est le langage principal de ce processeur, ces deux registres deviennent architecturaux. ===Une architecture bit-adressable=== Si avoir un microcode réinscriptible est suffisant, quelques choix de ''design'' facilitent l'émulation de jeux d'instructions variés. Par exemple, le CPU Burroughs B1700 avait ajouté une fonctionnalité très importante : il était bit-adressable. En clair, il pouvait adresser la mémoire bit par bit, si nécessaire. L'avantage est que cela permettait d'émuler des architectures dont la taille des registres/''byte''/mots était très différente. Un autre avantage est que cela facilitait la gestion des instructions de taille variable. Détaillons rapidement le premier avantage. Le processeur était un CPU 24 bits, mais il pouvait émuler des processeurs 16 bits, 8 bits, 4 bits, 1 bit, 9 bits, ou toute autre valeur. Pour cela, le processeur lisait des mots de 24 bits et utilisant un ''barrel shifter'' pour sélectionner les bits adéquats. Par exemple, pour un accès de 16 bits, la donnée lue en mémoire était masquée de manière à ne garder que les 16 bits de poids faible. Le ''barrel shifter''/circuit de masquage était placé directement avant le bus mémoire, sur le trajet des données/instructions. Le ''barrel shifter'' était commandé par un registre ''Field Unit'', qui précisait quelle était la taille des données à charger. ===La gestion de la taille des instructions=== Les instructions à décoder n'ont pas de longueur fixe, à savoir qu'elles peuvent faire 16 bit avec tel jeu d'instruction, 8 bits avec un autre, 32 bits sur un autre, etc. Elles peuvent aussi être de longueur fixe ou variable ! Le B1700 intégrait donc un support des instructions de longueur variable, qui était "détourné" pour gérer des jeux d'instruction différents. Notons que vu que l'architecture est bit-adressable, les contraintes d'alignement des instructions disparaissent, ce qui a des avantages qu'on va détailler dans ce qui suit ! L'implémentation des instructions de taille variable ajoutait un registre avant le registre d'instruction. Le registre d'instruction proprement dit était un registre M de 24 bits. Il était précédé par un registre T, dans lequel l'instruction était assemblée, en combinant plusieurs morceaux. Si l'instruction est codée est codée sur moins de 24 bits, le registre T se contente de masquer les bits en trop. Si l'instruction chargée dépasser 24 bits, le chargement se faisait en plusieurs passes, avec plusieurs lectures en mémoire. Dans ce qui suit, on suppose que l'instruction est composée de '''champs''' : l'opcode, les opérandes, les adresses absolues, les constantes immédiates, les numéros de registre, etc. L'idée est que l'instruction est chargée dans le registre T, puis découpée en champs, qui sont copiés dans les registres du processeur ou envoyés au microcode. Je rappelle que le registre T est relié à un ''barrel shifter'', différent au précédent, qui permettait de masquer, décaler et extraire une donnée de ce registre. Et il y a la possibilité d'extraire une suite de bit de ce registre T, pour la copier dans un autre registre. Par exemple, prenons une instruction composée d'un opcode, d'une constante immédiate et d'une adresse immédiate. L'opcode est lu et interprété par le microcode. Le microcode extrait la constante immédiate du registre T et la copie dans le registre X. Ensuite, l'adresse est extraite et envoyée au registre FA, une lecture est démarrée, et l'opérande est copiée dans le registre Y. Enfin, l'instruction est exécutée par le microcode. Le tout prend plusieurs micro-opérations : une première pour lire l'opcode, une seconde pour lire la première opérande, une troisième pour lire la seconde opérande, una quatrième pour tout exécuter. Notons que suivant la taille des champs, on peut en charger plusieurs à la fois. Par exemple, imaginons qu'on ait chargé 24 bits, contenant un opcode de 8 bits et une constante immédiate de 16 bits : pas besoin de faire deux accès mémoire, le registre T est juste découpé en un opcode et une constante. Le registre T est fait pour ce genre d'extraction. ===Le chargement d'un champ=== L'architecture est bit-adressable car elle gére des champs qui ne font pas forcément 8 ou 16 bits. Par exemple, certains jeux d'instructions ont des opcodes codés sur 6 ou 7 bits, voire 9-10 bits. Par exemple, prenons un opcode de 7 bits, suivi par une adresse de 12 bits et une constante immédiate de 15 bits : aucun de ces champs ne sera aligné sur 8/16/24 bits. Utiliser une architecture bit-adressable règle le problème d'alignement. Et en général, les contraintes d'alignement des instructions disparaissent ! Quand on charge une instruction, peut importe qu'elle soit ou non alignée sur 24 bits, elle sera placée dans les bits de poids faible, il suffira de masquer les bits de poids fort inutiles. Par exemple, si on charge une instruction de 16 bits, les 16 bits seront copiés dans les 16 bits de poids faible du registre T, il suffira de masquer l'octet de poids fort. Pas besoin de faire de décalages pour gérer des instructions mal alignées... Pour charger une instruction, il faut connaitre plusieurs informations : l'adresse de son premier bit, sa taille. Pour cela, le B1700 contient deux registres, nommés ''Field Adress'' (FA) et ''Field Length'' (FL), qui indiquent l'adresse du premier bit et la taille de l'instruction à charger. Le premier fait 24 bits, ce qui permet de gérer des adresses de 24 bits. N'oubliez pas que le processeur est bit-adressable, ce qui permet d'adresser 16 Mébi-bits, soit 2 mébioctets. Le registre pour la taille fait lui 16 bits, ce qui permet de gérer des instructions de 65536 bits, soit 8192 octets ! Leur nom commence par ''field'', car le processeur peut gérer des instructions de taille variable et/ou plus longues que 24 bits. Dans ce cas, le chargement de l'instruction se fait champ par champ. Pour donner un exemple, prenons une instruction composée de 20 champs de 12 bits chacun. Les champs sont chargés un par un dans le registre T. Le registre FA est alors initialisé avec l'adresse de l'instruction, il est incrémenté de 12 à chaque cycle. Le registre FL est initialisé avec la taille de l'instruction, soit 20 * 12 = 240 bits, et il est décrémenté de 16 à chaque cycle. Précisons cependant que la taille d'un champ peut être changée d'un cycle à l'autre. Par exemple, le processeur peut charger un opcode de 8 bits, l'interpréter, détecter que l'instruction demande ensuite de charger une adresse de 16 bits. Dans ce cas, le registre FL est immédiatement altéré pour charger 16 bits, au lieu des 8 bits de l'opcode immédiatement précédent. ===Le microcode réinscriptible=== Le microcode utilise des micro-opérations de 16 bits de long, ce qui est très peu et bien en-dessous des micro-opérations des autres processeurs. Les micro-opérations sortant du microcode sont envoyées au registre M, un registre de micro-opération de 16 bits. les micro-opérations écrites dans ce registre sont ensuite décodées par le hardware et exécutées. Fait intéressant, il était possible de faire un OU logique entre la micro-opération sortante et un registre du processeur, le résultat étant mémorisé dans le registre M. Cela permettait de faire du code automodifiant dans le microcode ! Utiliser du code automodifiant avec un microcode réinscriptible est parfaitement possible, et c'est utile pour émuler des modes d'adressage complexe dans le microcode. Cela permettait par exemple de remplacer une adresse dans une micro-opération. L'adresse est alors surimposée avec un OU dans le champ d'adresse, alors que le reste de l'instruction n'est pas modifié. Mais modifier directement le microcode est rarement une bonne idée. Aussi, pour éviter cela, l'idée est de modifier les instructions avec un OU logique avec un autre registre. Les micro-opérations du microcode ne sont donc pas modifiées dans le microcode, mais en sortie du microcode. Afin de gérer les interruptions, ces dernières étaient partiellement détectées en logiciel. Les interruptions ne s'exécutaient pas automatiquement, pour émuler correctement les interruptions. Une interruption doit être prise en compte à la toute fin de l'exécution d'une instruction, quand celle-ci a finit son travail. Mais vu qu'ici, les instructions sont émulées avec des séries de micro-opérations, il y a un problème : il est possible d'interrompre une instruction émulée en plein milieu. Pour éviter cela, le processeur ne déclenche pas les interruptions matérielles immédiatement, elles sont mises en attente. Une micro-opération dédiée vérifie si une interruption est en attente, et l'exécute alors cas échéant. La micro-opération est utilisée comme micro-opération finale d'une instruction émulée. ===Un ''split-level control store''=== La RAM mémorisant le microcode était de petite taille, et il arrivait qu'elle soit trop petit pour mémoriser le microcode complet. Dans ce cas, le programmeur devait gérer la situation en découpant le microcode en segments, certains étant placé dans le processeur, les autres étant en mémoire RAM. Le processeur peut lire le microcode placé en mémoire RAM et même l'exécuter, mais avec une pénalité en termes de performances. La documentation appelle cela un '''''split-level control store'''''. Quelques registres intégrés au processeur permettent ce genre de magie. Un tel microcode permettait de placer le microcode le plus fréquemment utilisé dans le processeur, alors que le reste du microcode était placé en mémoire RAM. Les instructions fréquentes étaient ainsi décodées dans le processeur, alors que les instructions peu fréquentes étaient décodées en lisant le microcode associé en mémoire RAM. La pénalité en termes de performance est alors concentrée sur les instructions les plus rares, ce qui est un très bon compromis. Mieux que ça, le processeur autorisait d'utiliser la technique de l'''overlaying'' pour le microcode ! Le microcode pouvait servir pour émuler un autre jeu d'instruction, mais il était aussi possible d'implémenter un véritable interpréteur, grâce aux techniques du paragraphe précédent. Le processeur était même conçu pour ! L'interpréteur était placé dans un "segment" en mémoire RAM, dont le processeur connaissait la position. Il incorporait pour cela deux registres, un pour l'adresse de base de l'interpréteur, un autre pour son adresse de fin. Il incorporait une protection mémoire limitée sur ce segment, à savoir que seul l'interpréteur pouvait lire ou écrire dans ce segment. Il faut noter que le ''split-level control store'' n'était disponible que sur le modèle B1720, pas sur le B1710 ou le B1800. Le B1700 lisait le microcode depuis la mémoire RAM, il n'avait pas de RAM dédiée pour le microcode réinscriptible ! Le B1800 faisait pareil, mais avait un cache dédié au microcode, afin d'avoir de meilleures performances. ==Les jeux d'instructions dédiés à un langage de programmation== De rares processeurs sont conçus pour un langage de programmation en particulier. On appelle ces processeurs, conçus pour des besoins particuliers, des '''processeurs dédiés'''. Par exemple, les fameux ''Burrough E-mode'' B5000/B6000/B7000 étaient spécialement conçus pour exécuter de l'ALGOL-60. Leurs cousins B2000/B3000/B4000 étaient eux conçus pour le COBOL. Des langages fonctionnels ont aussi eu droit à leurs processeurs dédiés. Le prolog en est un bel exemple, avec les superordinateurs de 5ème génération qui lui étaient dédié. On peut aussi citer les machines LISP, dédiés au langage LISP, qui datent des années 1970. Elles étaient capables d’exécuter certaines fonctions de base du langage directement dans leurs circuits : elles possédaient notamment un ''garbage collector'' câblé dans ses circuits ainsi que des instructions machines supportant un typage déterminé à l’exécution. Les processeurs dédiés ont eu leur heure de gloire au début de l'informatique, à une époque où les langages de haut niveau venaient d'être inventés. À cette époque, les compilateurs n'étaient pas performants et ne savaient pas bien optimiser le code machine. Il était alors rationnel, pour l'époque, de rapprocher le code machine cible et le langage de programmation de haut niveau. De nombreuses architectures dédiés ont ainsi été inventées, avant que les concepteurs se rendent compte des défauts de cette approche. Les défauts en question ne sont pas nombreux, mais assez simples à comprendre. Premièrement, elles sont très rapides pour un langage de programmation en particulier, mais sont assez mauvaises pour les autres, d'où un problème de "compatibilité". Ajoutons à cela que les langages de programmation peuvent évoluer, devenir de moins en moins populaires/utilisés, ce qui rend la création d'architectures généralistes plus pertinente. Enfin, les architectures dédiées sont évidemment des processeurs CISC, pour implémenter les nombreuses fonctionnalités des langages évolués. Et les défauts des CISC sont assez rédhibitoires à l'heure actuelle. À l'heure actuelle, les algorithmes des compilateurs se sont améliorés et savent nettement mieux utiliser le matériel. Ils produisent du code machine efficace, ce qui rend les architecture dédiées bien moins intéressantes. Si on ajoute les défauts de ces architectures dédiées, par étonnant que les architectures dédiées aient presque disparues. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'accélération matérielle de la virtualisation | prevText=L'accélération matérielle de la virtualisation | next=Le matériel réseau | nextText=Le matériel réseau }} </noinclude> 1qwtvx8hsn0kbrvpkafwkwbrd2mvuct 764031 764030 2026-04-19T18:38:54Z Mewtow 31375 /* Un split-level control store */ 764031 wikitext text/x-wiki De nos jours, la majorité des programmeurs programment dans des langages de haut niveau. Il est très rare pour eux d'avoir à utiliser de l'assembleur, et encore moins un langage machine. Les programmes écrits dans un langage de haut niveau sont traduits en langage machine par un logiciel appelé le compilateur. [[File:Compilation.PNG|centre|vignette|upright=2.5|Compilateur : principe.]] Il se trouve que quelques rares processeurs sont conçus pour faciliter le travail du compilateur. Et ce sont ces processeurs que nous allons voir dans ce qui suit. Ils représentent une catégorie de jeux d'instruction à part, qui n'a pas vraiment de nom. ==La compilation et l’interprétation== Un compilateur traduit donc du code source, écrit dans un langage de haut niveau, vers du langage machine. La traduction est rarement directe. En général, le compilateur traduit le code source en assembleur, qui est lui-même traduit en langage machine. L'assembleur est une représentation textuelle du langage machine alors que le code machine est du binaire exécutable par le processeur. La traduction se fait donc en deux étapes, la '''compilation''' proprement dite et l''''assemblage''', réalisées respectivement par un compilateur et un assembleur. Il faut parfois rajouter une troisième phase d'édition des liens, que nous passons volontairement sous silence. {| |[[File:Code C.png|vignette|200px|Le code source, ici en C.]] |[[File:Assem.png|vignette|250px|Le code assembleur.]] |[[File:Binary file - hello world (C programming).png|vignette|150px|Le code machine.]] |} ===L'interprétation et la compilation à la volée=== Une alternative à la compilation est l''''interprétation''', qui transforme le code source en code machine à la volée. Avec l'interprétation, le code source est passé à un logiciel appelé l''''interpréteur''', qui exécute le code source ligne de code par ligne de code. Une ligne de code est traduite en code machine, qui est exécutée, puis l'interpréteur passe à la ligne suivante. [[File:Kaantotulkkaus.png|centre|vignette|upright=3|Compilation versus interprétation.]] Un défaut de l'interprétation est son cout en performance, qui est assez important. Aussi, l'interprétation stricte a évolué vers un hybride entre interprétation et compilation. L'idée est qu'une partie peu importante du code source est interprétée, alors que le code important est compilé juste avant d'être exécuté. Pour le dire autrement, le code source est partiellement compilé à la volée, juste avant son exécution. Aussi, on parle de '''compilation à la volée'''. Le terme en anglais est ''Just In Time compilation'', abrévié en JIT. En soi, le JIT est associé à de l'interprétation, les JIT sont en réalité des hybrides interpréteurs-compilateurs. Un point important est que compiler du code à la volée est assez lourd, cela a un cout en performance. Par contre, le code compilé s'exécute plus vite que du code interprété. Si on exécute le code compilé une seule fois, le cout de la compilation l'emporte sur le gain à l'exécution. Mais le code compilé est réutilisé autant de fois que nécessaire. Le gain à l'exécution est donc multiplié par le nombre d'exécutions, alors que le cout de la compilation est répartit sur plusieurs exécutions, il est amorti. Les portions du code sont choisies de manière à avoir un gain en performance maximal. L'idée générale est que le code exécuté très souvent est compilé, alors que du code exécuté pas souvent est interprété. Typiquement, les boucles les plus souvent exécutées sont compilées, le code exécuté une seule fois est interprété. ===Les langages intermédiaires=== Les compilateurs modernes passent par un '''langage intermédiaire''' pour faire la transformation en code machine. Le compilateur traduit le langage de haut niveau en langage intermédiaire, puis traduit le langage intermédiaire en code machine cible. Le langage intermédiaire est parfois appelé le '''''bytecode''''', et ce terme recouvre aussi le code écrit avec du ''bytecode''. L'interprétation aussi peut passer par l'intermédiaire d'un un ''bytecode'', ce n'est pas limité aux compilateurs proprement dit. {| |[[File:Intermediate representation scheme.png|centre|vignette|upright=1.5|Représentation intermédiaire d'un compilateur.]] |[[File:Code interpreter scheme.png|centre|vignette|upright=1.5|Fonctionnement d'un interpréteur.]] |} Faire ainsi a de nombreux avantages, le principal étant d'avoir un compilateur capable de traduire un langage de haut niveau vers plusieurs jeux d'instructions différents. Cela permet d'avoir, par exemple, un compilateur qui traduit du C soit en code machine x86, soit en code machine pour un CPU ARM, soit pour un CPU POWERPC, etc. [[File:Compiler design.svg|centre|vignette|upright=3|Compiler design]] Pour l'interprétation, l'usage d'un ''bytecode'' a d'autres avantages. Pour l'exploiter, il faut distribuer non pas le code source, mais le ''bytecode''. En clair, l'interpréteur ne prend pas en entrée le code source, mais du ''bytecode'' déjà compilé. La traduction est alors beaucoup plus simple, car le code source a déjà été partiellement compilé, par la traduction en ''bytecode''. Interpréter du code source est en effet assez compliqué : il faut effectuer des étapes d'analyse lexicale, sémantique, et bien d'autres. Avec du ''bytecode'', ces étapes ont été réalisées lors de la compilation du ''bytecode'', l'interpréteur a alors peut de choses à faire. L'interprétation du ''bytecode'' se fait instruction par instruction, au niveau du ''bytecode''. Une instruction du ''bytecode'' est traduite en une instruction machine équivalente, qui est exécutée. Si l'instruction est un peu complexe, il exécute une fonction/procédure qui fait la même chose. Pour résumer, le ''bytecode'' est ensuite traduit à la volée et exécuté instruction par instruction par un logiciel appelé l'interpréteur. [[File:Control table.png|centre|vignette|upright=2|Control table]] Le langage intermédiaire peut être vu comme l'assembleur d'un processeur, n'existe pas forcément dans la réalité, mais dont le jeu d'instruction est décrit en détail. Le processeur en question est appelé une '''machine abstraite''', ou encore une ''machine virtuelle''. Nous utiliserons le terme de machine abstraite dans ce qui suit. La machine abstraite n'est pas la même suivant que l'on cible la compilation ou l'interprétation/JIT. Pour l'interprétation/JIT, la machine abstraite est souvent une machine à pile, car cela simplifie grandement la traduction en code machine final. L'interprétation demande que la traduction du ''bytecode'' en code machine soit la plus simple possible. Et cela demande de prendre en compte pas mal de détails. Par exemple, les différents jeux d'instruction existants n'ont pas le même nombre de registres, ce qui pose problème lors de la traduction du ''bytecode''. Lors de la transformation en code machine, un algorithme d'allocation de registres se débrouille pour traduire le code intermédiaire en code qui utilise un nombre limité de registres. Pour améliorer cette allocation de registres, il y a deux solutions niveau ''bytecode'' : soit utiliser un nombre illimité de registres, soit utiliser une machine à pile. Pour les machines à pile, il existe un algorithme simple et rapide pour traduire un code écrit pour une machine à pile en un code écrit pour un processeur avec des registres, qui se débrouille pas trop mal pour allouer efficacement les registres. C'est un avantage assez important pour les langages interprétés. ==Les processeurs qui exécutent du bytecode== Le ''bytecode'' est un code machine, ce qui signifie qu'il peut en théorie s'exécuter sur un processeur qui implémente le jeu d’instruction associé. Si le jeu d'instruction d'un ''bytecode'' est souvent une description censée être fictive, elle n'en reste pas moins un jeu d'instruction et des caractéristiques précises, qu'on peut l'implémenter en matériel ! Néanmoins, tous les ''bytecode'' ne sont pas égaux de ce point de vue. Certains sont faciles à implémenter en matériel, d'autres non. Les ''bytecodes'' implémentés en matériel sont ceux dont les machines abstraites sont des machines à pile. Un premier exemple est celui des processeurs Pascal MicroEngine, qui exécutaient directement le bytecode du langage Pascal, le fameux UCSD P-code. Un second exemple est la '''machine SECD''', qui sert de langage intermédiaire pour certains compilateurs de langages fonctionnels. Elle a été implémentée en matériel par plusieurs équipes, notamment par les chercheurs de l'université de Calgary en 1989. Dans le même genre, quelques processeurs simples étaient capables d’exécuter directement le ''bytecode'' du langage FORTH. Le FORTH, un des premiers langages à pile de haut niveau, possède de nombreuses implémentations hardware et est un des rares langages de haut niveau dont le ''bytecode'' a été utilisé comme langage machine sur certains processeurs. Par exemple, on peut citer le processeur FC16, capable d’exécuter nativement du ''bytecode'' FORTH. ===Les processeurs Java=== Le cas le plus impressionnant est celui de la machine virtuelle Java, qui est un design de processeur comme un autre. En temps normal, le ''bytecode'' Java est compilé ou interprété, mais certains processeurs exécutaient du ''bytecode'' Java directement, sans interprétation ni compilation. Ils sont appelés des '''processeurs Java'''. On peut citer les processeurs ARM disposant de l'extension Jazelle, du JEMCore et du aJ-100 de aJile Systems, le picoJava II de Sun, et quelques autres. Il y a aussi eu quelques projets de recherche ou processeurs open source, comme Komodo, jamuth, le ''Java Optimized Processor'' et quelques autres projets du même genre. L'implémentation de la machine virtuelle Java n'était cependant pas complète. Quelques instructions complexes n'étaient pas gérées par le processeur et devaient être émulées en logiciel. Mais la grosse majorité des instructions l'était. Les instructions non-supportées étaient émulées avec la méthode ''trap and emulate'', à savoir qu'un opcode inconnu déclenchait une exception matérielle ''opcode invalide'', dont la routine pouvait émuler les instructions qui doivent l'être. Le processeur aJ-100 de aJile est capable d'exécuter la quasi-totalité des instructions de la JVM Java, seules deux instructions faisant exception. Les '''accélérateurs Java''' sont une solution intermédiaire, qui permet de traduire à la volée du ''bytecode'' Java en instructions RISC, exécutées sur un autre processeur. Il s'agit formellement de traduction binaire, mais je préfère en parler ici. L'idée est de combiner un processeur RISC avec un coprocesseur Java. Le coprocesseur lit le ''bytecode'' Java et le traduit en instructions RISC, ces dernières étant exécutées sur le processeur RISC. Le JA108 de Nozomi était un coprocesseur de ce genre, au même titre que le coprocesseur JSTAR de JEDI. L'extension Jazelle de certains CPU ARM fonctionnait vraisemblablement sur un principe similaire, sauf que le coprocesseur était intégré dans le processeur ARM, entre le cache d'instruction et le décodeur d'instructions ARM. L'intérêt des processeurs Java n'est pas qu'une question de performance. Certes, ils permettent d’exécuter plus vite les programmes compilés en ''bytecode'', comme des programmes Java pour la JVM Java. Mais l'intérêt est aussi de faire des économies de mémoire RAM et ROM. Je rappelle que la machine virtuelle Java est une machine à pile, ce qui fait que sa densité de code est excellente. Les programmes en ''bytecode'' Java sont donc très petits, et prennent peu de place en mémoire ROM. De plus, utiliser un processeur Java permet de se passer d'interpréteur ou de JIT Java, ce qui économise encore plus de mémoire. ===Transformer un processeur RISC en machine à pile=== La plupart des processeurs Java utilisent en interne une architecture à registre généraux, qui émule une machine à registre en microcode. Quelques registres sont réservés pour l'état de la machine virtuelle, avec au minimum un registre réservé pour le pointeur de pile, un autre pour un pointeur vers un ''pool'' de constantes, et quelques autres. D'autres registres servent à mémoriser le haut de la pile, à savoir le sommet de la pile et quelques opérandes situées juste en-dessous. Par exemple, le processeur picoJava II gardait les 64 opérandes au sommet de la pile dans 64 registres généraux, séparés des registres pour les pointeurs et l'état de la JVM. Une optimisation possible, envisagée sur les processeurs picoJava, était une sorte de macro-fusion sous stéroïde. L'idée était de fusionner certaines séries d'instructions Java en une seule instruction machine. Typiquement, pour faire une opération arithmétique, une machine à pile empile deux opérandes et exécute une opération, ce qui prend trois instructions machine. La spécification picoJava 1 a proposé d'envoyer directement la seconde opérande en entrée de l'ALU, ce qui fusionnait l'empilement de la seconde opérande et l'opération arithmétique. Le processeur picoJava II va plus loin et fusionne les trois instructions en une seule instruction RISC, agissant sur des registres. Mais la majorité des processeurs Java ne faisaient pas cela, car cela complexifie grandement le décodeur. Les processeurs ''bigfoot'' de DCT utilisaient un système différent pour émuler une pile avec des registres. Leur banc de registre de 64 bits était coupé en deux : les 16 premiers registres étaient utilisés pour la pile, les 48 suivants étaient utilisés pour autre chose. L'idée est que le registre 0 mémorise un emplacement vide, le registre 1 contient le sommet de la pile, le registre 2 l'opérande sous le sommet de la pile, et ainsi de suite. Les 15 premières opérandes de la pile sont donc mémorisées dans les registres. Les instructions écrivent leur résultat dans le registre adéquat, puis tous les registres sont décalés pour que le résultat passe dans le registre R1. Par exemple, les instructions PUSH chargent une opérande dans le registre 0, puis tous les registres sont décalés de un rang, pour que l'opérande chargée soit dans le registre 1. Le registre 0 devient alors le registre numéro 1, le registre 1 devient le 2, et ainsi de suite. Par contre, les opérations arithmétiques font autrement. Une instruction d'addition, par exemple, lit les opérandes dans le registre 1 et 2, puis place son résultat dans le registre 2 (les deux opérandes sont dépilées). Puis, les registres sont décalés d'un rang pour que ce registre 2 devienne le 1. Les registres ne sont en réalité pas décalés, il n'y a pas de transferts entre registres. A la place, les 16 premiers registres utilisaient une forme limitée de renommage de registres, similaire au banc de registre tournant utilisé sur les processeurs EPIC comme l'Itanium. Les numéros de registres sont renommés, décalés, à chaque fois qu'on empile ou dépile une opérande. Pour faire le renommage de registres, processeur contient un ''register stack counter'' de 4 bits, qui est incrémenté à chaque fois qu'on empile une donnée, et est décrémenté quand on dépile une donnée. Quand on veut accéder à un opérande, le numéro de registre architectural associé est traduit en numéro de registre physique, en additionnant le ''register stack counter'' (modulo 16, pour rester sur 4 bits). ==L'accélération de l'interprétation/JIT : le ''Thumb-EE'' d'ARM== Quelques processeurs ont ajouté des instructions pour faciliter le travail des interpréteurs/JIT. Par exemple, en 2005, ARM a ajouté le mode ''Thumb Execution Environment'', qui faisait cela. Il reprenait le jeu d'instruction compact ''thumb'' et ajoutait quelques instructions et modifiait le comportement d'instructions existantes. Le jeu d'instruction ''Thumb-EE'' a été ajouté sur les CPU ARM en 2005, mais a été déprécié en 2011, par manque d'utilité. Mais la tentative mérite qu'on s'attarde dessus. Les CPU de gamme M, déstinés à l'embarqué, n'ont jamais supporté ''Thumb-EE'', vu que de tels processeurs ne sont pas conçus pour exécuter du code interprété/JIT. Le mode ''Thumb-EE'' est un mode d'exécution, séparé de l'ARM normal. Ainsi, une instruction ''Thumb-EE'' et une instruction ARM peuvent avoir le même encodage en binaire, mais se comporter différemment. Le processeur est à tout moment soit en mode ''Thumb-EE'', soit en mode ARM normal, ce qui précise comment ces instructions doivent se comporter. Des instructions ont été ajoutées par ''Thumb-EE'', pour qu'il fasse son travail. L'entrée et la sortie du mode ''Thumb-EE'' se fait avec deux instructions : ENTERX et LEAVEX. Une fois en mode ''Thumb-EE'', le décodage des instructions ''thumb'' se fait avec les règles du ''Thumb-EE'', et non celles du ''thumb'' normal. Une première différence entre ''thumb'' normal et ''Thumb-EE'' est l'ajout de ce qui s'appelle un ''null check'' pour les instructions mémoire. Le ''null check'' est utilisé pour les instructions en mode d'adressage "base + indice" ou "Base + décalage", ou tout autre mode d'adressage avec un registre de base utilisés dans des calculs d'adresse. L'idée est de vérifier si l'adresse de base vaut zéro ou non. Si c'est le cas, le processeur lève une exception matérielle, qui est traitée par l'interprétation ou le compilateur JIT. Une telle situation est en effet signe d'une erreur d'adressage mémoire, qui doit être traitée par l'interpréteur. Typiquement, c'est signe qu'un pointeur n'a pas bien été initialisé, ce qui peut arriver avec du code interprété ou JIT. Les interpréteurs/JIT traitent généralement la situation en ajoutant un test avant toute instruction mémoire, pour vérifier si le pointeur accédé vaut zéro ou non. Avec ''Thumb-EE'', pas besoin d'ajouter les tests en questions : ils sont réalisés automatiquement lors de chaque instruction mémoire. : Les registres pour la pile, comme le pointeur de pile et le pointeur de ''frame'', sont aussi concernés. L'instruction CHKA vérifie que les accès à un tableau ne débordent pas en dehors du tableau en question. Par exemple, pour un tableau de 1024 éléments, elle vérifie si l'indice est compris entre 0 et 1024. Pour cela, elle vérifie si l'indice de tableau est bien dans l'intervalle adéquat et lève une exception matérielle si ce n'est pas le cas. Son utilité se comprend quand on sait que tous les accès à un tableau sont vérifiés par l'interpréteur. L'interpréteur est censé ajouter des instructions pour vérifier les indices, à savoir deux ou trois branchements. Avec CHKA, le test se fait en une seule instruction. D'autres modifications mineures de l'encodage des instructions sont aussi présentes, ainsi que des modifications pour les instructions LOAD/STORE. Précisément, dans les modes "base + indice" et "base + décalage", le décalage et l'indice subissent maintenant des décalages. L'idée est de simplifier le calcul d'adresse. De quoi économiser quelques instructions lors des calculs d'adresse, ce qui facilite le travail de l'interpréteur/JIT. Et des instructions LOAD/STORE précises voient leur comportement modifié pour faciliter la gestion de la pile de Java ou l'exécution des méthodes locales. Pour faciliter le travail de l'interpréteur, deux instructions HBP et HBLP ont été ajoutées. Elles branchent vers une fonction qui gère les exceptions logicielles (une fonctionnalité présente dans de nombreux langages, comme Java). La première effectue un branchement vers la fonction en question, la seconde sauvegarde l'adresse de retour avant de faire ce branchement. Les deux peuvent brancher vers 256 fonctions pré-déterminées, sans avoir à présenter leur adresse. ==La traduction binaire accélérée par le matériel== Plus haut, nous avons surtout parlé des compilateurs et des interpréteurs. Cependant, nous devons aussi parler de la '''traduction binaire'''. Elle traduit un programme écrit dans un code machine vers un autre code machine. Par exemple, elle traduit un programme compilé pour un CPU x86 vers un code machine ARM. En général, le système d'exploitation est généralement compilé pour le jeu d'instruction natif, mais il exécute des applications prévues pour le x86. Les applications sont traduites par le système d'exploitation, avant d'exécuter le code traduit. : Nous parlerons dans la suite de code machine source et de code machine cible, pour parler respectivement du code à traduire et du code obtenu après traduction. Idem avec d'autres termes comme architecture cible/source, ou jeu d'instruction cible/source. La traduction binaire est surtout utilisée pour des questions d'émulation ou de compatibilité. Par exemple, lorsque les Macintosh sont passés de processeurs Power PC vers des processeurs x86, le système d'exploitation Mac OS utilisait la traduction binaire pour convertir les anciennes applications Power PC vers du code x86. Les utilisateurs n'y ont vu que du feu. Le système de traduction binaire était appelé Rosetta 1. Par la suite, lors de la transition de processeurs x86 vers des processeurs Apple, Rosetta 2 a vu le jour. La traduction binaire a surtout été utilisée pour traduire du code x86 vers un autre jeu d'instruction. Il faut dire que le x86 est le jeu d'instruction dominant. De nombreuses entreprises ont eu pour ambition de briser l'hégémonie du x86 sur PC, en remplaçant le x86 par un jeu d'instruction plus performant, tout en gardant une compatibilité maximale. La traduction binaire était la seule solution pratique. Elle était le plus souvent intégralement réalisée en logiciel, comme c'était le cas sur les architectures Itanium avec le ''IA-32 Execution Layer''. ===Les généralités sur la traduction binaire assistée en matériel=== L'architecture source est presque tout le temps une architecture CISC, assez ancienne, qu'on souhaite émuler. Le choix de l'architecture cible se porte souvent sur une architecture VLIW, fort différente de l'architecture source, et ce pour obtenir une économie de matériel conséquente. Pour s'exécuter rapidement, le code traduit doit exploiter le parallélisme d'instruction, à savoir exécuter plusieurs instructions en même temps dans des unités de calcul séparées. Il est possible d'utiliser un processeur superscalaire avec exécution dans le désordre pour cela, mais au prix d'un cout important en transistors. Alors qu'en utilisant un CPU VLIW, c'est le traducteur binaire qui fait tout le travail d'extraction du parallélisme d'instruction. Et après tout, quitte à avoir un traducteur binaire, autant lui refiler le boulot d'optimisation. Compiler du code à la volée est certes assez lourd, mais qu'il existe des algorithmes efficaces pour regrouper des instructions indépendantes dans une seule instruction VLIW. Au passage, le processeur VLIW a plus de registres que le processeur source. Cela permet de faire du renommage de registres directement en logiciel. Le traducteur binaire n'hésite pas à changer les noms de registres entre instructions source et instruction VLIW cible, afin de supprimer des dépendances de données. En conséquence, cela ouvre des opportunités de parallélisme, qui sont exploitées lors du regroupement des opérations en instructions VLIW. Vous pourriez penser que le choix d'un CPU VLIW pour émuler du CISC est tout sauf optimal. Vous devez penser que la traduction binaire est d'autant plus simple que l'architecture source et cibles sont semblables. Dans les faits, ce n'est pas tellement traduire les instructions qui pose problème, mais plus la gestion du registre d'état, des exceptions matérielles, de la mémoire virtuelle, la différence entre gros-boutisme et petit-boutisme, de même que des différences d'adressage pour les périphériques. Le premier problème est la gestion des conditions, notamment en présence d'un registre d'état. En pratique, la traduction binaire s'utilise pour traduire du code CISC vers du code RISC ou VLIW. Les architectures source ont donc un registre d'état, qui est mis à jour non seulement par des instructions de test, mais aussi des instructions arithmétiques. L'architecture cible n'a elle pas de registre d'état, mais des registres à prédicats. La traduction de l'un vers l'autre est alors quelque peu compliquée. Et elle est d'autant plus compliquée que le registre d'état est la source de dépendances d'instruction implicites, qui réduisent les performances. Par exemple, si une instruction arithmétique modifie le registre d'état, cela peut impacter l'exécution d'une instruction ultérieure, qui lit ce registre d'état. Pour éliminer ces fausses dépendances, le traducteur binaire doit renommer ce registre en logiciel et détecter les dépendances utiles. Et le compilateur doit gérer les cas où il y a beaucoup de distance entre les deux, voire les cas où l'instruction dépendante n'a pas encore été analysée par le compilateur. Un autre problème est lié à la gestion des exceptions matérielles, et précisément des exceptions précises. Pour rappel, une instruction VLIW regroupe plusieurs opérations, opérations qui correspondraient à une instruction machine sur un CPU pas VLIW. En conséquence, plusieurs instructions du langage machine "source" sont regroupées en une seule instruction VLIW. Et il faut tenir compte du cas où une instruction source lève une exception. Dans le code VLIW, cela signifie qu'une opération lève une exception, et il faut annuler partiellement l'instruction VLIW associée. Et par partiellement, on veut dire que seules les opérations suivantes dans l'ordre du programme source doivent être annulées, puis ré-exécutées. Et c'est un sacré casse-tête ! Les processeurs VLIW utilisent comme solution le mécanisme d'exceptions différées des processeurs EPIC, vu il y a quelques chapitres. Pour résumer, le code est exécuté par blocs d'instruction, délimités par des branchements ou tout autre limite/barrière pertinente dans le code. Un bloc de code est compilé en deux versions : une version rapide sans exceptions matérielles, une version lente qui gère les exceptions précises. La version rapide s'exécute sans exécuter les exceptions. Cependant, les exceptions matérielles sont enregistrées, pour être prises en compte à la toute fin du bloc de code. La version rapide est exécutée en premier et elle mémorise si une exception matérielle a eu lieu. Puis une instruction vérifie si une exception a eu lieu et décide quoi faire. Si aucune exception n'a eu lieu, elle passe à la suite du programme, les résultats du bloc de code sont définitivement acceptés. Mais si une exception a eu lieu, tout est annulé. Le processeur est remis dans l'état initial, puis le code est ré-exécuté instruction par instruction de manière à gérer l'exception correctement. ===La traduction par pré-décodage=== Une première solution serait de faire la traduction binaire dans le cache d'instruction. Lors d'un défaut de cache, le code chargé depuis la RAM est traduit en code machine cible. Et c'est ce code machine cible qui est mémorisé dans le cache d'instruction et exécuté par le processeur. La technique marche sur le papier et n'est qu'une amélioration des techniques de pré-décodage vues il y a quelques chapitres. Cependant, le code obtenu est une traduction assez basique, qui n'incorpore pas d'optimisations dignes de ce nom. De plus, elle gère mal le cas où la taille du code source et cible sont potentiellement très différentes. Concrètement, elle est surtout utile pour traduire du code d'un processeur RISC vers un autre processeur RISC, dont les tailles d'instructions sont similaires. Elle ne permet pas d'émuler plusieurs jeux d'instructions différents, le cout en matériel (un circuit de pré-décodage par jeu d'instruction) serait trop important. ===Le projet DAISY d'IBM=== De très rares processeurs étaient conçus pour accélérer cette traduction binaire, afin de garder de bonnes performances. Les premiers à avoir étudié l'idée étaient IBM, avec leur projet DAISY (''Dynamically Architected Instruction Set from Yorktown''). Le projet de base était de convertir à la volée du code compilé pour des CPU Power PC, vers du code VLIW. Les chercheurs d'IBM avaient développé un algorithme de traduction dynamique efficace, ainsi qu'un processeur VLIW disposant d'optimisations spécifiques à la traduction binaire dynamique, à la volée. De nombreuses idées de ce projet ont été reprises ou re-découvertes par la société Transmetta, avec ses processeurs Crusoe et Efficieon, puis par NVIDIA avec son projet Denver. Les ingénieurs de ce projet ont étudié la possibilité d'émuler plusieurs jeux d'instructions différents sur un même processeur, notamment le s390 d'IBM et le x86. En théorie, cela demande juste d'avoir plusieurs programmes de traduction binaire : un pour le x86, un autre pour le s390, éventuellement un autre pour le Power PC. Mais cela ne s'est pas concrétisé. Mais les ingénieurs ont étudié quelle pourrait être l'architecture VLIW idéale pour ça. Par exemple, il fallait des additions 3-opérandes pour simplifier les calculs d'adresse. Une difficulté était la gestion du registre d'état, dont les bits ne sont pas mis à jour de la même manière sur le x86 et le s390 ou le Power PC. La différence entre gros-boutisme et petit-boutisme était aussi un problème, de même que des différences d'adressage pour les périphériques. Pour que la traduction binaire soit efficace, le processeur VLIW intègre diverses optimisations, comme les exceptions différées et les branchements multi-voies. La principale est la suivante : seul le code des boucles ou fonctions exécutées fréquemment est traduit en code VLIW, le reste du code est interprété. En effet, traduire du code binaire prend plus de temps qu'une simple interprétation. Pour du code qui ne sera exécutée qu'une seule fois, il est plus rapide d'utiliser l'interprétation. Par contre, pour du code exécuté beaucoup de fois, le cout de la traduction binaire est amorti, dilué sur N exécutions, compensé par le gain en temps d'exécution de ce code traduit. Au final, cela permet de ne traduire que le code qui le mérite. La détection du code fréquemment exécuté est réalisée dans le cœur VLIW. L'unité de branchement mémorise les derniers branchements rencontrés et le nombre de fois qu'ils ont été exécutés. Si ils ont été exécutés un certain nombre de fois, l'unité de branchement lève une exception matérielle, qui invoque le traducteur binaire. La routine de cette exception est le traducteur binaire proprement dit. En clair, la traduction binaire est démarrée quand le processeur détecte qu'une fonction a été exécutée plus de N fois, via une exception matérielle dédiée. La mesure du nombre d'exécution d'un branchement se fait dans l'unité de calcul dédiée aux branchements, avec l'aide d'une sorte de ''branch adress buffer'' modifié. Pour rappel, le ''branch adress buffer'' mémorise le ''Program counter'' de chaque branchement récemment rencontré. Ici, chaque entrée du ''branch adress buffer'' est associée à un compteur incrémenté à chaque exécution du branchement. ===Les processeurs Crusoe et Efficieon de Transmetta=== Les processeurs Crusoe Et Efficeron sont deux processeurs VLIW produits par la société Transmetta, une société californienne rachetée par NVIDIA. Ils étaient conçus pour exécuter spécifiquement des programmes x86, système d'exploitation inclus. L'idée derrière ce projet était d'exécuter du code x86 sans que les applications, ni même le système d'exploitation et le BIOS soient au courant ! Les processeurs Transmetta étaient en réalité conçus pour exécuter un programme unique, le ''Code Morphing Software'' (CMS), qui traduisait le code x86 en code VLIW. Transmetta ne voulait pas que son processeur VLIW soit exploité directement, sans le CMS. Il n'avait pas rendu public de compilateur pour traduire du C vers du code VLIW, il n'avait pas donné la documentation du jeu d'instruction VLIW. Des efforts de rétro-ingénieurie, documentés sur le site realworldtech, ont cependant permis de comprendre comment étaient encodées les instructions du processeur Crusoe. Comme pour le projet Daisy, le ''Code Morphing Software'' utilisait à la fois interprétation et traduction binaire, selon les besoins. Les instructions sont interprétées lors de leurs premières exécutions, mais le CMS bascule sur de la traduction en code VLIW après un certain nombre d'exécution. Ainsi, les boucles souvent exécutées sont traduites en code VLIW, alors que le reste du code est interprété. Le choix du code à traduire est le fait du CMS, il utilise des heuristiques complexes pour, qui ne sont pas connues dans le détail. Le ''Code Morphing Software'' est mémorisé dans une EEPROM, ce qui en fait un ''firmware'' situé en dessous du BIOS. Le CMS démarre ensuite le BIOS, qui lui-même démarre le système d'exploitation, qui lui-même démarre les pilotes de périphériques et les programmes. Le CMS se réserve les 16 premiers mébioctets de l'espace d'adressage. L'EEPROM du CMS est mappée dedans, mais n'en utilise que la moitié. Le reste est utilisé comme cache, pour mémoriser le code VLIW traduit par le CMS. Les deux processeurs intégraient aussi deux mémoires SRAM, utilisées par le CMS, appelées la ''local program memory'' (LPM) et la ''local data memory'' (LDM). La première contient du code qui gére les interruptions, la mémoire virtuelle, les exceptions matérielles, les problèmes d'alignement mémoire, et quelques fonctions très fréquemment utilisées par le CMS. La seconde est de la mémoire RAM utilisée par ces fonctions de la ''local program memory''. Les mémoires caches L1 et L2 sont séparées de ces deux mémoires. Le processeur Crusoe et Efficieon étaient des processeurs VLIW très simples. Ils n'avaient même pas de MMU, ni de fonctionnalités importantes sur les CPU x86. Pour Crusoe, les instructions VLIW étaient encodées sur 64 ou 128 bits, et regroupaient entre 2 à 4 opérations. Les instructions LOAD/STORE ne supportaient pas d'adressage indicé, juste de l'adressage indirect à registre. Crusoe avait 5 unités de calcul : deux ALU entières, une unité LOAD/STORE, une unité de branchement, une FPU. Efficieon doublait le nombre d'ALU entière et d'unité LOAD/STORE, les instructions VLIW passaient à 256 bits. Le processeur Crusoe contient 160 registres, dont 64 registres généraux, 32 registres flottants. Les registres généraux font 32 bits, ce qui est cohérent avec le fait que les CPU x86 de l'époque étaient des processeurs 32 bits. Quant aux registres flottants, ils faisaient 80 bits, ce qui colle avec la taille des registres flottants de la FPU x87 utilisée à l'époque. Sur les 64 registres généraux, seuls 48 étaient réellement utilisable pour mémoriser des opérandes. Une partie des registres généraux étaient utilisés pour la gestion de la pile, d'autres pour mémoriser l'état du CPU x86 émulé, un registre était un registre zéro non accesible en écriture. La gestion des exceptions est optimisée avec un système d'exceptions différées, le même que celui décrit plus haut. Le processeur utilise un système similaire, pour effectuer des lectures anticipées, qui a été expliqué dans le chapitre sur les processeurs VLIW/EPIC. Sauf qu'il s'agit là d'une optimisation pour gagner en performances en général, pas une aide utile pour gérer des exceptions précises ou toute autre subtilité de la traduction binaire. Pour gérer les exceptions différées et les lectures anticipées, le processeur mémorise l'état du processeur avant de démarrer un bloc de code. Pour cela, le processeur contient des ''shadow registers'', 48 registres entiers et 16 registres flottants. Vous remarquerez qu'il y a autant de registres entiers que de ''shadow registers'' entiers. Les registres entiers/flottants sont mémorisés dans les ''shadow registers'' avant d'exécuter un bloc de code, afin que le processeur puisse revenir à l'état de base. Une autre fonctionnalité liée est le ''gated store buffer''. Le principe est d'éviter toute écriture en mémoire RAM, tant que le code VLIW n'a pas émis d'instruction ''commit''. L'idée est que les écritures sont conservées dans la ''store queue'' de l'unité LOAD/STORE. Pour rappel, la ''store queue'' met en attente les écritures soit tant que les écritures ne sont pas terminées, soit tant que la RAM est occupée. L'idée est que les écritures sont accumulées dans la ''store queue'' tant que l'instruction ''commit'' n'a pas eu lieu. Le ''store queue'' est "déversé dans la RAM" seulement quand l'instruction ''commit'' s'exécute. ===Les CPU NVIDIA Denver=== Nvidia a tenté quelque chose de similaire avec son projet Denver. NVIDIA a racheté l'entreprise Transmetta, ses brevets, et a tenté de refaire la même chose. Le projet initial était de traduire du code x86 vers un jeu d'instruction VLIW propriétaire, comme l'a fait Transmetta. Mais NVIDIA n'a pas réussi à acquérir la licence du jeu d'instruction x86 et s'est rabattu sur le jeu d'instruction ARM. Le processeur né de ce projet est le Tegra K1-64 CPU. Il s'agit d'un processeur composé d'un cœur VLIW, couplé à deux décodeurs d'instructions ARM. Les deux décodeurs ARM traduisent une paire d'instructions ARM consécutives en instructions VLIW, pour les exécuter sur le cœur VLIW. Le CPU peut fonctionner selon deux modes : ARM et VLIW. En somme, le processeur supporte deux jeux d'instruction : le jeu d'instruction ARM, et un jeu d'instruction VLIW. En mode ARM, les instructions ARM sont chargées depuis le cache, traduites en VLIW par les décodeurs ARM, puis exécutées par le cœur VLIW. En mode optimisé, le CPU exécute des instructions VLIW chargées depuis le cache d'instruction, les décodeurs ne sont pas utilisés. : Le CPU supporte en réalité deux jeux d'instruction ARM : ARM8 et 7. Mais ils correspondent au même mode pour le CPU. Un point important est que le processeur peut changer très rapidement de mode, en à peine quelques cycles d'horloges. On n'est clairement pas dans le cas des CPU x86, qui mettent des plombes pour passer du mode 32 à 64 bits et inversement. La commutation est tellement rapide qu'on peut considérer que le processeur supporte deux jeux d'instruction simultanément. Et le support simultané de deux jeux d'instruction facilite l'implémentation de la traduction binaire. Encore une fois, inutile de traduire du code qui ne sera exécutée qu'une seule fois, mieux vaut l'exécuter directement dans le mode ARM. Surtout que le processeur supporte un mode ARM qui permet d'exécuter du code ARM sans perte de performance. Par contre, le code fréquemment utilisé, notamment dans des boucles critiques, est traduit en VLIW. Au final, les performances sont optimisées, en limitant le travail du logiciel traducteur binaire. La détection du code fréquemment exécuté se fait différemment que pour les processeurs Transmetta et Daisy. L'unité de branchement ne détecte pas directement le code beaucoup exécuté, même si elle a un rôle à jouer. A la place, la détection est réalisée en logiciel, par un ''thread'' dédié, qui s'exécute sur un cœur séparé. Il détecte le code fréquemment utilisé en regardent les branchements exécutés récemment. Pour cela, le processeur mémorise l'historique des branchements pris récemment et copie cette historique dans une mémoire tampon dédiée, partagée entre tous les cœurs. Vu qu'elle est partagée, le ''thread'' de détection a accès à l'historique sans pertes de performances, seule la copie de l'historique dans ce tampon a un cout en performance, pas son partage. Le code VLIW obtenu après traduction binaire est mémorisé dans la mémoire RAM, dans une portion spécialement réservée pour. Elle fait 128 mébioctets, et est appelée le '''cache d'optimisation''' par NVIDIA. Le cache d'optimisation est protégé en écriture et seul le traducteur binaire peut écrire dedans. Au passage, le traducteur binaire est du code VLIW, ce qui fait qu'il est placé dans le cache d'optimisation. Le processeur détecte automatiquement quand une fonction pour laquelle le code compilé équivalent est disponible. Pour cela, l'unité de chargement contient une table de correspondance entre l'adresse de la fonction ARM, et l'adresse de son équivalent compilé. Quand une fonction est appelée, l'unité de chargement regarde l'adresse de destination du branchement. Si l'adresse est dans cette table, elle récupère l'adresse de la fonction compilée et branche vers celle-ci. Le processeur dispose d'un cache d'instruction de 128 kibioctets, ce qui est très important, mais nécessaire vu la taille des instructions VLIW. Le coeur VLIW dispose de ses propres décodeurs, d'un ''scoreboard'', de 7 unités de calcul et de registres. Pour les unités de calcul, il a deux FPU, deux ALU, une unité de branchement, et deux unités LOAD-STORE faisant aussi ALU. Un point important est que le CPU dispose de 64 registres entiers et de 64 registres flottants. C'est deux fois plus que supporte le jeu d'instruction ARM. En mode ARM, seule la moitié des registres est utilisée. Mais en mode optimisé, le traducteur binaire utilise bien les 64 registres, grâce à une sorte de renommage de registres logicielle. Pour simplifier la traduction binaire, le processeur supporte les techniques vues précédemment. Le processeur intègre un ''gated store buffer'' similaire à celui des processeurs Transmetta. Les techniques d'exceptions différées sont aussi supportées, comme sur les processeurs Transmetta. Il a aussi une unité de préchargement décrite par NVIDIA comme agressive, avec support du préchargement de type anticipé (''runahead''). ===L'émulation de la mémoire virtuelle=== Pour finir, il faut parler de la mémoire virtuelle avec la traduction binaire. Sur le processeur source, le processeur gère à la fois des adresses physiques et virtuelles et sa MMU fait la conversion entre les deux. Sur le processeur VLIW, la MMU est simulée par le programme de traduction binaire, partiellement ou totalement. L'implémentation exacte varie suivant que l'on parle du projet Daisy, des processeurs Transmetta ou de Denver. Sur les processeurs Transmetta, il n'y a pas de MMU ni de mémoire virtuelle. A la place, le programme de traduction binaire émule la mémoire virtuelle du processeur source. Le processeur VLIW ne gère que des adresses physiques, rien d'autre. L'espace d'adressage physique du processeur VLIW a la même taille que celui du processeur émulé, ici des processeurs x86. Il est vraisemblable que les adresses physiques utilisées par le processeur x86 sont les mêmes que celles du processeur VLIW. Sur le projet Daisy, le processeur VLIW gère la mémoire virtuelle via pagination, comme les processeurs émulés. Ce qui fait qu'il gère un espace d'adressage virtuel et un espace d'adressage physique. Pour éviter toute confusion, nous parlerons d'adresse physique/virtuelle VLIW pour les adresses physiques/virtuelles du processeur VLIW, d'adresse virtuelle/physique source pour celle du jeu d'instruction traduit, à savoir du Power PC ou du s390, éventuellement de l'x86. Il faut alors faire le lien entre adresses physiques source et adresses virtuelles VLIW. Pour cela, rien de plus simple : il y a correspondance parfaite. L'adresse physique source numéro N correspond à l'adresse virtuelle VLIW numéro N. Cependant, cela signifie que tout l'espace d'adressage virtuel VLIW serait utilisé par le code à traduire. En réalité, il faut ajouter de la place pour le code traduit et le programme de traduction binaire. La conséquence est que l'espace d'adressage virtuel VLIW est plus large que l'espace d'adressage source. L'espace d'adressage virtuel VLIW est découpé en trois sections : une section pour le code Power PC à traduire, une autre pour le code traduit en VLIW, et une dernière réservée au traducteur binaire. Le programme de traduction est placé dans une ROM mappée en mémoire dans la seconde section. Le reste de la seconde section est réservé à une mémoire RAM utilisée par le programme de traduction. [[File:Mémoire virtuelle sur le projet Daisy.png|centre|vignette|upright=1.5|Mémoire virtuelle sur le projet Daisy]] Il faut noter que chaque page physique source, une fois traduite, correspond à N pages virtuelles VLIW. La raison est que le code traduit prend plus de place que le code machine originel. En conséquence, on doit utiliser une page finale plus grande. Pour le reste, des pages contiguës en mémoire physique source sont elles aussi contiguës en mémoire virtuelle VLIW. Le calcul d'adresse est donc simple : l'adresse physique source est multipliée par N, puis on ajoute l'adresse de base à laquelle commence la section pour le code traduit. ==Les processeurs à jeu d'instruction reconfigurable== Nous venons de voir comment accélérer en matériel la traduction binaire. Mais il existe une alternative qui permet de se passer de traduction binaire, complétement. L'idée est d'avoir un processeur avec un '''jeu d'instruction reconfigurable'''. Le nom est assez parlent : on peut configurer le processeur de manière à supporter le jeu d'instruction de son choix ! Par exemple, on pouvait reconfigurer un CPU de manière à ce qu'il supporte le jeu d'instruction x86, ou qu'il exécute du ''bytecode'' Java, ou du code ARMv8, etc. De tels processeurs sont encore plus rare que ceux vu avant dans ce chapitre, mais ils ont existé ! Les processeurs de ce type avaient un ''microcode réinscriptible'', à savoir que c'était des processeurs microcodés pour lesquels on pouvait modifier le microcode. L'idée était qu'en changeant le microcode, on changeait le jeu d'instruction du processeur. Pour cela, le microcode était associé à une mémoire SRAM dans laquelle le microcode était copié au démarrage du CPU. Il y avait donc une micro-ROM et une micro-RAM. Les instructions étaient décodées en utilisant le microcode dans la micro-SRAM, la micro-ROM ne servait qu'au démarrage. En modifiant le contenu de la micro-SRAM, on pouvait altérer le microcode, voire le remplacer totalement. ===Le Burroughs B1700 : architecture externe=== Un exemple est celui du Burroughs B1700, et il est intéressant d'étudier en détail son architecture. Son architecture est décrite dans le livre "Interpreting Machines : Architecture and Programming of the Bl700/Bl800 Series", écrit par Elliott I. Organick et James A. Hinds. Une partie de ce qui vfa suivre est un très court résumé de ce livre. Il disposait de 4 registres généraux, nommés X, Y, T et L, tous de 24 bits. Les registres T et L étaient découpés en 6 sous-registres de 4 bits, nommés LA LB LC LD LE et LF pour le registre L, et TA TB TC TD TE et TF pour le registre T. Ils étaient adressables comme n'importe quel registre. Un point important est que le registre T avait des possibilités d’extraction spécifiques. Il était possible d'extrait N bits, placés n'importe où dans ce registre, et de les copier dans un autre registre. Nous verrons pourquoi cette possibilité est intéressante et ne sert que pour le registre T. Les registres X et Y étaient reliés à une ALU de 24 bits capable d'effectuer des opérations arithmétiques basiques. Fait étonnant, l'ALU faisait tous les calculs en même temps et fournissait ses résultats dans 7 registres : un registre SUM pour l'addition, un registre DIFFERENCE pour la soustraction, et un registre par opération bit à bit. Les registres pour les opérations bit à bit sont : CMPX et CMPY pour l'opération NOT sur chaque opérande, XANY pour le ET bit à bit, XEOY pour le XOR, XORY pour le OU logique. Les additions et soustractions pouvaient se faire sur des opérandes codés en binaire ou en BCD, suivant le code opération utilisé. A cela, il fallait ajouter deux registres de "résultat" MSKX et MSKY. Ils fournissaient un masque dépendant de la taille du résultat en bits. Cela permettait de gérer des données de 0, 4, 8, 12, 16, 20, et 24 bits, alors que l'ALU faisait 24 bits. Et cela n'était pas limité à 16 ou 8 bits, mais n'importe quelle taille inférieure ou égale à 24 bits était supportée, même des tailles non-conventionnelles comme des résultats codés sur 3 bits, 9 bits, 12 bits, etc. L'ALU peenait en opérande les registres X et Y, mais aussi un registre CYF pour la retenue entrante. En sortie, il fournissait aussi deux retenue sortantes : une pour l'addition et une autre pour la soustraction. Elles étaient mémorisées dans les registres CYL et CYD, respectivement. L'ALU fournissait aussi 12 conditions, qui alimentait un registre d'état. Le registre d'état était segmenté en trois registres appelés XYST, XYCN et BICN. Le processeur contenait de nombreux autres registres de 4 bits, dont les sous-registres de T et L. En tout, il y a 27 registres de 4 bits adressables ! Et le processeur disposait d'une ALU 4 bits pour manipuler ces registres de 4 bits ! Elle est capable de faire des MOV entre registres de 4 bits, des opérations bit à bit, et peut aussi tester si un bit vaut 0 ou 1. La dernière possibilité est très utile pour implémenter les branchements. Pour communiquer avec les entrées-sorties, il disposait de deux registres CMND et DATA : CMND pour envoyer une commande à une entrée-sortie, DATA pour lire ou écrire une donnée. Pour la mémoire, il avait deux registres séparés : READ et WRITE. READ contient la donnée lue lors d'une lecture,; WRITE est pour une donnée à écrire lors d’une écriture. Il s'agit de registres d’interfaçage mémoire, qui ont été promus au rang de registres architecturaux. Les deux registres sont gérés par le microcode, qui est le langage principal de la machine. Les instructions machines configurées font sans, mais le microcode doit y avoir accès facilement. Et vu que le microcode est le langage principal de ce processeur, ces deux registres deviennent architecturaux. ===Une architecture bit-adressable=== Si avoir un microcode réinscriptible est suffisant, quelques choix de ''design'' facilitent l'émulation de jeux d'instructions variés. Par exemple, le CPU Burroughs B1700 avait ajouté une fonctionnalité très importante : il était bit-adressable. En clair, il pouvait adresser la mémoire bit par bit, si nécessaire. L'avantage est que cela permettait d'émuler des architectures dont la taille des registres/''byte''/mots était très différente. Un autre avantage est que cela facilitait la gestion des instructions de taille variable. Détaillons rapidement le premier avantage. Le processeur était un CPU 24 bits, mais il pouvait émuler des processeurs 16 bits, 8 bits, 4 bits, 1 bit, 9 bits, ou toute autre valeur. Pour cela, le processeur lisait des mots de 24 bits et utilisant un ''barrel shifter'' pour sélectionner les bits adéquats. Par exemple, pour un accès de 16 bits, la donnée lue en mémoire était masquée de manière à ne garder que les 16 bits de poids faible. Le ''barrel shifter''/circuit de masquage était placé directement avant le bus mémoire, sur le trajet des données/instructions. Le ''barrel shifter'' était commandé par un registre ''Field Unit'', qui précisait quelle était la taille des données à charger. ===La gestion de la taille des instructions=== Les instructions à décoder n'ont pas de longueur fixe, à savoir qu'elles peuvent faire 16 bit avec tel jeu d'instruction, 8 bits avec un autre, 32 bits sur un autre, etc. Elles peuvent aussi être de longueur fixe ou variable ! Le B1700 intégrait donc un support des instructions de longueur variable, qui était "détourné" pour gérer des jeux d'instruction différents. Notons que vu que l'architecture est bit-adressable, les contraintes d'alignement des instructions disparaissent, ce qui a des avantages qu'on va détailler dans ce qui suit ! L'implémentation des instructions de taille variable ajoutait un registre avant le registre d'instruction. Le registre d'instruction proprement dit était un registre M de 24 bits. Il était précédé par un registre T, dans lequel l'instruction était assemblée, en combinant plusieurs morceaux. Si l'instruction est codée est codée sur moins de 24 bits, le registre T se contente de masquer les bits en trop. Si l'instruction chargée dépasser 24 bits, le chargement se faisait en plusieurs passes, avec plusieurs lectures en mémoire. Dans ce qui suit, on suppose que l'instruction est composée de '''champs''' : l'opcode, les opérandes, les adresses absolues, les constantes immédiates, les numéros de registre, etc. L'idée est que l'instruction est chargée dans le registre T, puis découpée en champs, qui sont copiés dans les registres du processeur ou envoyés au microcode. Je rappelle que le registre T est relié à un ''barrel shifter'', différent au précédent, qui permettait de masquer, décaler et extraire une donnée de ce registre. Et il y a la possibilité d'extraire une suite de bit de ce registre T, pour la copier dans un autre registre. Par exemple, prenons une instruction composée d'un opcode, d'une constante immédiate et d'une adresse immédiate. L'opcode est lu et interprété par le microcode. Le microcode extrait la constante immédiate du registre T et la copie dans le registre X. Ensuite, l'adresse est extraite et envoyée au registre FA, une lecture est démarrée, et l'opérande est copiée dans le registre Y. Enfin, l'instruction est exécutée par le microcode. Le tout prend plusieurs micro-opérations : une première pour lire l'opcode, une seconde pour lire la première opérande, une troisième pour lire la seconde opérande, una quatrième pour tout exécuter. Notons que suivant la taille des champs, on peut en charger plusieurs à la fois. Par exemple, imaginons qu'on ait chargé 24 bits, contenant un opcode de 8 bits et une constante immédiate de 16 bits : pas besoin de faire deux accès mémoire, le registre T est juste découpé en un opcode et une constante. Le registre T est fait pour ce genre d'extraction. ===Le chargement d'un champ=== L'architecture est bit-adressable car elle gére des champs qui ne font pas forcément 8 ou 16 bits. Par exemple, certains jeux d'instructions ont des opcodes codés sur 6 ou 7 bits, voire 9-10 bits. Par exemple, prenons un opcode de 7 bits, suivi par une adresse de 12 bits et une constante immédiate de 15 bits : aucun de ces champs ne sera aligné sur 8/16/24 bits. Utiliser une architecture bit-adressable règle le problème d'alignement. Et en général, les contraintes d'alignement des instructions disparaissent ! Quand on charge une instruction, peut importe qu'elle soit ou non alignée sur 24 bits, elle sera placée dans les bits de poids faible, il suffira de masquer les bits de poids fort inutiles. Par exemple, si on charge une instruction de 16 bits, les 16 bits seront copiés dans les 16 bits de poids faible du registre T, il suffira de masquer l'octet de poids fort. Pas besoin de faire de décalages pour gérer des instructions mal alignées... Pour charger une instruction, il faut connaitre plusieurs informations : l'adresse de son premier bit, sa taille. Pour cela, le B1700 contient deux registres, nommés ''Field Adress'' (FA) et ''Field Length'' (FL), qui indiquent l'adresse du premier bit et la taille de l'instruction à charger. Le premier fait 24 bits, ce qui permet de gérer des adresses de 24 bits. N'oubliez pas que le processeur est bit-adressable, ce qui permet d'adresser 16 Mébi-bits, soit 2 mébioctets. Le registre pour la taille fait lui 16 bits, ce qui permet de gérer des instructions de 65536 bits, soit 8192 octets ! Leur nom commence par ''field'', car le processeur peut gérer des instructions de taille variable et/ou plus longues que 24 bits. Dans ce cas, le chargement de l'instruction se fait champ par champ. Pour donner un exemple, prenons une instruction composée de 20 champs de 12 bits chacun. Les champs sont chargés un par un dans le registre T. Le registre FA est alors initialisé avec l'adresse de l'instruction, il est incrémenté de 12 à chaque cycle. Le registre FL est initialisé avec la taille de l'instruction, soit 20 * 12 = 240 bits, et il est décrémenté de 16 à chaque cycle. Précisons cependant que la taille d'un champ peut être changée d'un cycle à l'autre. Par exemple, le processeur peut charger un opcode de 8 bits, l'interpréter, détecter que l'instruction demande ensuite de charger une adresse de 16 bits. Dans ce cas, le registre FL est immédiatement altéré pour charger 16 bits, au lieu des 8 bits de l'opcode immédiatement précédent. ===Le microcode réinscriptible=== Le microcode utilise des micro-opérations de 16 bits de long, ce qui est très peu et bien en-dessous des micro-opérations des autres processeurs. Les micro-opérations sortant du microcode sont envoyées au registre M, un registre de micro-opération de 16 bits. les micro-opérations écrites dans ce registre sont ensuite décodées par le hardware et exécutées. Fait intéressant, il était possible de faire un OU logique entre la micro-opération sortante et un registre du processeur, le résultat étant mémorisé dans le registre M. Cela permettait de faire du code automodifiant dans le microcode ! Utiliser du code automodifiant avec un microcode réinscriptible est parfaitement possible, et c'est utile pour émuler des modes d'adressage complexe dans le microcode. Cela permettait par exemple de remplacer une adresse dans une micro-opération. L'adresse est alors surimposée avec un OU dans le champ d'adresse, alors que le reste de l'instruction n'est pas modifié. Mais modifier directement le microcode est rarement une bonne idée. Aussi, pour éviter cela, l'idée est de modifier les instructions avec un OU logique avec un autre registre. Les micro-opérations du microcode ne sont donc pas modifiées dans le microcode, mais en sortie du microcode. Afin de gérer les interruptions, ces dernières étaient partiellement détectées en logiciel. Les interruptions ne s'exécutaient pas automatiquement, pour émuler correctement les interruptions. Une interruption doit être prise en compte à la toute fin de l'exécution d'une instruction, quand celle-ci a finit son travail. Mais vu qu'ici, les instructions sont émulées avec des séries de micro-opérations, il y a un problème : il est possible d'interrompre une instruction émulée en plein milieu. Pour éviter cela, le processeur ne déclenche pas les interruptions matérielles immédiatement, elles sont mises en attente. Une micro-opération dédiée vérifie si une interruption est en attente, et l'exécute alors cas échéant. La micro-opération est utilisée comme micro-opération finale d'une instruction émulée. ===Un ''split-level control store''=== La RAM mémorisant le microcode était de petite taille, et il arrivait qu'elle soit trop petit pour mémoriser le microcode complet. Dans ce cas, le programmeur devait gérer la situation en découpant le microcode en segments, certains étant placé dans le processeur, les autres étant en mémoire RAM. Le processeur peut lire le microcode placé en mémoire RAM et même l'exécuter, mais avec une pénalité en termes de performances. La documentation appelle cela un '''''split-level control store'''''. Quelques registres intégrés au processeur permettent ce genre de magie. Un tel microcode permettait de placer le microcode le plus fréquemment utilisé dans le processeur, alors que le reste du microcode était placé en mémoire RAM. Les instructions fréquentes étaient ainsi décodées dans le processeur, alors que les instructions peu fréquentes étaient décodées en lisant le microcode associé en mémoire RAM. La pénalité en termes de performance est alors concentrée sur les instructions les plus rares, ce qui est un très bon compromis. Mieux que ça, le processeur autorisait d'utiliser la technique de l'''overlaying'' pour le microcode ! Le microcode pouvait servir pour émuler un autre jeu d'instruction, mais il était aussi possible d'implémenter un véritable interpréteur, grâce aux techniques du paragraphe précédent. Le processeur était même conçu pour ! L'interpréteur était placé dans un "segment" en mémoire RAM, dont le processeur connaissait la position. Il incorporait pour cela deux registres, un pour l'adresse de base de l'interpréteur, un autre pour son adresse de fin. Il incorporait une protection mémoire limitée sur ce segment, à savoir que seul l'interpréteur pouvait lire ou écrire dans ce segment. Il faut noter que le ''split-level control store'' n'était disponible que sur le modèle B1726, pas sur le B1710 ou le B1800. Le B1700 lisait le microcode depuis la mémoire RAM, il n'avait pas de RAM dédiée pour le microcode réinscriptible ! Le B1800 faisait pareil, mais avait un cache dédié au microcode, afin d'avoir de meilleures performances. ==Les jeux d'instructions dédiés à un langage de programmation== De rares processeurs sont conçus pour un langage de programmation en particulier. On appelle ces processeurs, conçus pour des besoins particuliers, des '''processeurs dédiés'''. Par exemple, les fameux ''Burrough E-mode'' B5000/B6000/B7000 étaient spécialement conçus pour exécuter de l'ALGOL-60. Leurs cousins B2000/B3000/B4000 étaient eux conçus pour le COBOL. Des langages fonctionnels ont aussi eu droit à leurs processeurs dédiés. Le prolog en est un bel exemple, avec les superordinateurs de 5ème génération qui lui étaient dédié. On peut aussi citer les machines LISP, dédiés au langage LISP, qui datent des années 1970. Elles étaient capables d’exécuter certaines fonctions de base du langage directement dans leurs circuits : elles possédaient notamment un ''garbage collector'' câblé dans ses circuits ainsi que des instructions machines supportant un typage déterminé à l’exécution. Les processeurs dédiés ont eu leur heure de gloire au début de l'informatique, à une époque où les langages de haut niveau venaient d'être inventés. À cette époque, les compilateurs n'étaient pas performants et ne savaient pas bien optimiser le code machine. Il était alors rationnel, pour l'époque, de rapprocher le code machine cible et le langage de programmation de haut niveau. De nombreuses architectures dédiés ont ainsi été inventées, avant que les concepteurs se rendent compte des défauts de cette approche. Les défauts en question ne sont pas nombreux, mais assez simples à comprendre. Premièrement, elles sont très rapides pour un langage de programmation en particulier, mais sont assez mauvaises pour les autres, d'où un problème de "compatibilité". Ajoutons à cela que les langages de programmation peuvent évoluer, devenir de moins en moins populaires/utilisés, ce qui rend la création d'architectures généralistes plus pertinente. Enfin, les architectures dédiées sont évidemment des processeurs CISC, pour implémenter les nombreuses fonctionnalités des langages évolués. Et les défauts des CISC sont assez rédhibitoires à l'heure actuelle. À l'heure actuelle, les algorithmes des compilateurs se sont améliorés et savent nettement mieux utiliser le matériel. Ils produisent du code machine efficace, ce qui rend les architecture dédiées bien moins intéressantes. Si on ajoute les défauts de ces architectures dédiées, par étonnant que les architectures dédiées aient presque disparues. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'accélération matérielle de la virtualisation | prevText=L'accélération matérielle de la virtualisation | next=Le matériel réseau | nextText=Le matériel réseau }} </noinclude> b32znocz1e3qi5n342zbw0zwx5t15bs 764032 764031 2026-04-19T18:47:43Z Mewtow 31375 /* Les processeurs à jeu d'instruction reconfigurable */ 764032 wikitext text/x-wiki De nos jours, la majorité des programmeurs programment dans des langages de haut niveau. Il est très rare pour eux d'avoir à utiliser de l'assembleur, et encore moins un langage machine. Les programmes écrits dans un langage de haut niveau sont traduits en langage machine par un logiciel appelé le compilateur. [[File:Compilation.PNG|centre|vignette|upright=2.5|Compilateur : principe.]] Il se trouve que quelques rares processeurs sont conçus pour faciliter le travail du compilateur. Et ce sont ces processeurs que nous allons voir dans ce qui suit. Ils représentent une catégorie de jeux d'instruction à part, qui n'a pas vraiment de nom. ==La compilation et l’interprétation== Un compilateur traduit donc du code source, écrit dans un langage de haut niveau, vers du langage machine. La traduction est rarement directe. En général, le compilateur traduit le code source en assembleur, qui est lui-même traduit en langage machine. L'assembleur est une représentation textuelle du langage machine alors que le code machine est du binaire exécutable par le processeur. La traduction se fait donc en deux étapes, la '''compilation''' proprement dite et l''''assemblage''', réalisées respectivement par un compilateur et un assembleur. Il faut parfois rajouter une troisième phase d'édition des liens, que nous passons volontairement sous silence. {| |[[File:Code C.png|vignette|200px|Le code source, ici en C.]] |[[File:Assem.png|vignette|250px|Le code assembleur.]] |[[File:Binary file - hello world (C programming).png|vignette|150px|Le code machine.]] |} ===L'interprétation et la compilation à la volée=== Une alternative à la compilation est l''''interprétation''', qui transforme le code source en code machine à la volée. Avec l'interprétation, le code source est passé à un logiciel appelé l''''interpréteur''', qui exécute le code source ligne de code par ligne de code. Une ligne de code est traduite en code machine, qui est exécutée, puis l'interpréteur passe à la ligne suivante. [[File:Kaantotulkkaus.png|centre|vignette|upright=3|Compilation versus interprétation.]] Un défaut de l'interprétation est son cout en performance, qui est assez important. Aussi, l'interprétation stricte a évolué vers un hybride entre interprétation et compilation. L'idée est qu'une partie peu importante du code source est interprétée, alors que le code important est compilé juste avant d'être exécuté. Pour le dire autrement, le code source est partiellement compilé à la volée, juste avant son exécution. Aussi, on parle de '''compilation à la volée'''. Le terme en anglais est ''Just In Time compilation'', abrévié en JIT. En soi, le JIT est associé à de l'interprétation, les JIT sont en réalité des hybrides interpréteurs-compilateurs. Un point important est que compiler du code à la volée est assez lourd, cela a un cout en performance. Par contre, le code compilé s'exécute plus vite que du code interprété. Si on exécute le code compilé une seule fois, le cout de la compilation l'emporte sur le gain à l'exécution. Mais le code compilé est réutilisé autant de fois que nécessaire. Le gain à l'exécution est donc multiplié par le nombre d'exécutions, alors que le cout de la compilation est répartit sur plusieurs exécutions, il est amorti. Les portions du code sont choisies de manière à avoir un gain en performance maximal. L'idée générale est que le code exécuté très souvent est compilé, alors que du code exécuté pas souvent est interprété. Typiquement, les boucles les plus souvent exécutées sont compilées, le code exécuté une seule fois est interprété. ===Les langages intermédiaires=== Les compilateurs modernes passent par un '''langage intermédiaire''' pour faire la transformation en code machine. Le compilateur traduit le langage de haut niveau en langage intermédiaire, puis traduit le langage intermédiaire en code machine cible. Le langage intermédiaire est parfois appelé le '''''bytecode''''', et ce terme recouvre aussi le code écrit avec du ''bytecode''. L'interprétation aussi peut passer par l'intermédiaire d'un un ''bytecode'', ce n'est pas limité aux compilateurs proprement dit. {| |[[File:Intermediate representation scheme.png|centre|vignette|upright=1.5|Représentation intermédiaire d'un compilateur.]] |[[File:Code interpreter scheme.png|centre|vignette|upright=1.5|Fonctionnement d'un interpréteur.]] |} Faire ainsi a de nombreux avantages, le principal étant d'avoir un compilateur capable de traduire un langage de haut niveau vers plusieurs jeux d'instructions différents. Cela permet d'avoir, par exemple, un compilateur qui traduit du C soit en code machine x86, soit en code machine pour un CPU ARM, soit pour un CPU POWERPC, etc. [[File:Compiler design.svg|centre|vignette|upright=3|Compiler design]] Pour l'interprétation, l'usage d'un ''bytecode'' a d'autres avantages. Pour l'exploiter, il faut distribuer non pas le code source, mais le ''bytecode''. En clair, l'interpréteur ne prend pas en entrée le code source, mais du ''bytecode'' déjà compilé. La traduction est alors beaucoup plus simple, car le code source a déjà été partiellement compilé, par la traduction en ''bytecode''. Interpréter du code source est en effet assez compliqué : il faut effectuer des étapes d'analyse lexicale, sémantique, et bien d'autres. Avec du ''bytecode'', ces étapes ont été réalisées lors de la compilation du ''bytecode'', l'interpréteur a alors peut de choses à faire. L'interprétation du ''bytecode'' se fait instruction par instruction, au niveau du ''bytecode''. Une instruction du ''bytecode'' est traduite en une instruction machine équivalente, qui est exécutée. Si l'instruction est un peu complexe, il exécute une fonction/procédure qui fait la même chose. Pour résumer, le ''bytecode'' est ensuite traduit à la volée et exécuté instruction par instruction par un logiciel appelé l'interpréteur. [[File:Control table.png|centre|vignette|upright=2|Control table]] Le langage intermédiaire peut être vu comme l'assembleur d'un processeur, n'existe pas forcément dans la réalité, mais dont le jeu d'instruction est décrit en détail. Le processeur en question est appelé une '''machine abstraite''', ou encore une ''machine virtuelle''. Nous utiliserons le terme de machine abstraite dans ce qui suit. La machine abstraite n'est pas la même suivant que l'on cible la compilation ou l'interprétation/JIT. Pour l'interprétation/JIT, la machine abstraite est souvent une machine à pile, car cela simplifie grandement la traduction en code machine final. L'interprétation demande que la traduction du ''bytecode'' en code machine soit la plus simple possible. Et cela demande de prendre en compte pas mal de détails. Par exemple, les différents jeux d'instruction existants n'ont pas le même nombre de registres, ce qui pose problème lors de la traduction du ''bytecode''. Lors de la transformation en code machine, un algorithme d'allocation de registres se débrouille pour traduire le code intermédiaire en code qui utilise un nombre limité de registres. Pour améliorer cette allocation de registres, il y a deux solutions niveau ''bytecode'' : soit utiliser un nombre illimité de registres, soit utiliser une machine à pile. Pour les machines à pile, il existe un algorithme simple et rapide pour traduire un code écrit pour une machine à pile en un code écrit pour un processeur avec des registres, qui se débrouille pas trop mal pour allouer efficacement les registres. C'est un avantage assez important pour les langages interprétés. ==Les processeurs qui exécutent du bytecode== Le ''bytecode'' est un code machine, ce qui signifie qu'il peut en théorie s'exécuter sur un processeur qui implémente le jeu d’instruction associé. Si le jeu d'instruction d'un ''bytecode'' est souvent une description censée être fictive, elle n'en reste pas moins un jeu d'instruction et des caractéristiques précises, qu'on peut l'implémenter en matériel ! Néanmoins, tous les ''bytecode'' ne sont pas égaux de ce point de vue. Certains sont faciles à implémenter en matériel, d'autres non. Les ''bytecodes'' implémentés en matériel sont ceux dont les machines abstraites sont des machines à pile. Un premier exemple est celui des processeurs Pascal MicroEngine, qui exécutaient directement le bytecode du langage Pascal, le fameux UCSD P-code. Un second exemple est la '''machine SECD''', qui sert de langage intermédiaire pour certains compilateurs de langages fonctionnels. Elle a été implémentée en matériel par plusieurs équipes, notamment par les chercheurs de l'université de Calgary en 1989. Dans le même genre, quelques processeurs simples étaient capables d’exécuter directement le ''bytecode'' du langage FORTH. Le FORTH, un des premiers langages à pile de haut niveau, possède de nombreuses implémentations hardware et est un des rares langages de haut niveau dont le ''bytecode'' a été utilisé comme langage machine sur certains processeurs. Par exemple, on peut citer le processeur FC16, capable d’exécuter nativement du ''bytecode'' FORTH. ===Les processeurs Java=== Le cas le plus impressionnant est celui de la machine virtuelle Java, qui est un design de processeur comme un autre. En temps normal, le ''bytecode'' Java est compilé ou interprété, mais certains processeurs exécutaient du ''bytecode'' Java directement, sans interprétation ni compilation. Ils sont appelés des '''processeurs Java'''. On peut citer les processeurs ARM disposant de l'extension Jazelle, du JEMCore et du aJ-100 de aJile Systems, le picoJava II de Sun, et quelques autres. Il y a aussi eu quelques projets de recherche ou processeurs open source, comme Komodo, jamuth, le ''Java Optimized Processor'' et quelques autres projets du même genre. L'implémentation de la machine virtuelle Java n'était cependant pas complète. Quelques instructions complexes n'étaient pas gérées par le processeur et devaient être émulées en logiciel. Mais la grosse majorité des instructions l'était. Les instructions non-supportées étaient émulées avec la méthode ''trap and emulate'', à savoir qu'un opcode inconnu déclenchait une exception matérielle ''opcode invalide'', dont la routine pouvait émuler les instructions qui doivent l'être. Le processeur aJ-100 de aJile est capable d'exécuter la quasi-totalité des instructions de la JVM Java, seules deux instructions faisant exception. Les '''accélérateurs Java''' sont une solution intermédiaire, qui permet de traduire à la volée du ''bytecode'' Java en instructions RISC, exécutées sur un autre processeur. Il s'agit formellement de traduction binaire, mais je préfère en parler ici. L'idée est de combiner un processeur RISC avec un coprocesseur Java. Le coprocesseur lit le ''bytecode'' Java et le traduit en instructions RISC, ces dernières étant exécutées sur le processeur RISC. Le JA108 de Nozomi était un coprocesseur de ce genre, au même titre que le coprocesseur JSTAR de JEDI. L'extension Jazelle de certains CPU ARM fonctionnait vraisemblablement sur un principe similaire, sauf que le coprocesseur était intégré dans le processeur ARM, entre le cache d'instruction et le décodeur d'instructions ARM. L'intérêt des processeurs Java n'est pas qu'une question de performance. Certes, ils permettent d’exécuter plus vite les programmes compilés en ''bytecode'', comme des programmes Java pour la JVM Java. Mais l'intérêt est aussi de faire des économies de mémoire RAM et ROM. Je rappelle que la machine virtuelle Java est une machine à pile, ce qui fait que sa densité de code est excellente. Les programmes en ''bytecode'' Java sont donc très petits, et prennent peu de place en mémoire ROM. De plus, utiliser un processeur Java permet de se passer d'interpréteur ou de JIT Java, ce qui économise encore plus de mémoire. ===Transformer un processeur RISC en machine à pile=== La plupart des processeurs Java utilisent en interne une architecture à registre généraux, qui émule une machine à registre en microcode. Quelques registres sont réservés pour l'état de la machine virtuelle, avec au minimum un registre réservé pour le pointeur de pile, un autre pour un pointeur vers un ''pool'' de constantes, et quelques autres. D'autres registres servent à mémoriser le haut de la pile, à savoir le sommet de la pile et quelques opérandes situées juste en-dessous. Par exemple, le processeur picoJava II gardait les 64 opérandes au sommet de la pile dans 64 registres généraux, séparés des registres pour les pointeurs et l'état de la JVM. Une optimisation possible, envisagée sur les processeurs picoJava, était une sorte de macro-fusion sous stéroïde. L'idée était de fusionner certaines séries d'instructions Java en une seule instruction machine. Typiquement, pour faire une opération arithmétique, une machine à pile empile deux opérandes et exécute une opération, ce qui prend trois instructions machine. La spécification picoJava 1 a proposé d'envoyer directement la seconde opérande en entrée de l'ALU, ce qui fusionnait l'empilement de la seconde opérande et l'opération arithmétique. Le processeur picoJava II va plus loin et fusionne les trois instructions en une seule instruction RISC, agissant sur des registres. Mais la majorité des processeurs Java ne faisaient pas cela, car cela complexifie grandement le décodeur. Les processeurs ''bigfoot'' de DCT utilisaient un système différent pour émuler une pile avec des registres. Leur banc de registre de 64 bits était coupé en deux : les 16 premiers registres étaient utilisés pour la pile, les 48 suivants étaient utilisés pour autre chose. L'idée est que le registre 0 mémorise un emplacement vide, le registre 1 contient le sommet de la pile, le registre 2 l'opérande sous le sommet de la pile, et ainsi de suite. Les 15 premières opérandes de la pile sont donc mémorisées dans les registres. Les instructions écrivent leur résultat dans le registre adéquat, puis tous les registres sont décalés pour que le résultat passe dans le registre R1. Par exemple, les instructions PUSH chargent une opérande dans le registre 0, puis tous les registres sont décalés de un rang, pour que l'opérande chargée soit dans le registre 1. Le registre 0 devient alors le registre numéro 1, le registre 1 devient le 2, et ainsi de suite. Par contre, les opérations arithmétiques font autrement. Une instruction d'addition, par exemple, lit les opérandes dans le registre 1 et 2, puis place son résultat dans le registre 2 (les deux opérandes sont dépilées). Puis, les registres sont décalés d'un rang pour que ce registre 2 devienne le 1. Les registres ne sont en réalité pas décalés, il n'y a pas de transferts entre registres. A la place, les 16 premiers registres utilisaient une forme limitée de renommage de registres, similaire au banc de registre tournant utilisé sur les processeurs EPIC comme l'Itanium. Les numéros de registres sont renommés, décalés, à chaque fois qu'on empile ou dépile une opérande. Pour faire le renommage de registres, processeur contient un ''register stack counter'' de 4 bits, qui est incrémenté à chaque fois qu'on empile une donnée, et est décrémenté quand on dépile une donnée. Quand on veut accéder à un opérande, le numéro de registre architectural associé est traduit en numéro de registre physique, en additionnant le ''register stack counter'' (modulo 16, pour rester sur 4 bits). ==L'accélération de l'interprétation/JIT : le ''Thumb-EE'' d'ARM== Quelques processeurs ont ajouté des instructions pour faciliter le travail des interpréteurs/JIT. Par exemple, en 2005, ARM a ajouté le mode ''Thumb Execution Environment'', qui faisait cela. Il reprenait le jeu d'instruction compact ''thumb'' et ajoutait quelques instructions et modifiait le comportement d'instructions existantes. Le jeu d'instruction ''Thumb-EE'' a été ajouté sur les CPU ARM en 2005, mais a été déprécié en 2011, par manque d'utilité. Mais la tentative mérite qu'on s'attarde dessus. Les CPU de gamme M, déstinés à l'embarqué, n'ont jamais supporté ''Thumb-EE'', vu que de tels processeurs ne sont pas conçus pour exécuter du code interprété/JIT. Le mode ''Thumb-EE'' est un mode d'exécution, séparé de l'ARM normal. Ainsi, une instruction ''Thumb-EE'' et une instruction ARM peuvent avoir le même encodage en binaire, mais se comporter différemment. Le processeur est à tout moment soit en mode ''Thumb-EE'', soit en mode ARM normal, ce qui précise comment ces instructions doivent se comporter. Des instructions ont été ajoutées par ''Thumb-EE'', pour qu'il fasse son travail. L'entrée et la sortie du mode ''Thumb-EE'' se fait avec deux instructions : ENTERX et LEAVEX. Une fois en mode ''Thumb-EE'', le décodage des instructions ''thumb'' se fait avec les règles du ''Thumb-EE'', et non celles du ''thumb'' normal. Une première différence entre ''thumb'' normal et ''Thumb-EE'' est l'ajout de ce qui s'appelle un ''null check'' pour les instructions mémoire. Le ''null check'' est utilisé pour les instructions en mode d'adressage "base + indice" ou "Base + décalage", ou tout autre mode d'adressage avec un registre de base utilisés dans des calculs d'adresse. L'idée est de vérifier si l'adresse de base vaut zéro ou non. Si c'est le cas, le processeur lève une exception matérielle, qui est traitée par l'interprétation ou le compilateur JIT. Une telle situation est en effet signe d'une erreur d'adressage mémoire, qui doit être traitée par l'interpréteur. Typiquement, c'est signe qu'un pointeur n'a pas bien été initialisé, ce qui peut arriver avec du code interprété ou JIT. Les interpréteurs/JIT traitent généralement la situation en ajoutant un test avant toute instruction mémoire, pour vérifier si le pointeur accédé vaut zéro ou non. Avec ''Thumb-EE'', pas besoin d'ajouter les tests en questions : ils sont réalisés automatiquement lors de chaque instruction mémoire. : Les registres pour la pile, comme le pointeur de pile et le pointeur de ''frame'', sont aussi concernés. L'instruction CHKA vérifie que les accès à un tableau ne débordent pas en dehors du tableau en question. Par exemple, pour un tableau de 1024 éléments, elle vérifie si l'indice est compris entre 0 et 1024. Pour cela, elle vérifie si l'indice de tableau est bien dans l'intervalle adéquat et lève une exception matérielle si ce n'est pas le cas. Son utilité se comprend quand on sait que tous les accès à un tableau sont vérifiés par l'interpréteur. L'interpréteur est censé ajouter des instructions pour vérifier les indices, à savoir deux ou trois branchements. Avec CHKA, le test se fait en une seule instruction. D'autres modifications mineures de l'encodage des instructions sont aussi présentes, ainsi que des modifications pour les instructions LOAD/STORE. Précisément, dans les modes "base + indice" et "base + décalage", le décalage et l'indice subissent maintenant des décalages. L'idée est de simplifier le calcul d'adresse. De quoi économiser quelques instructions lors des calculs d'adresse, ce qui facilite le travail de l'interpréteur/JIT. Et des instructions LOAD/STORE précises voient leur comportement modifié pour faciliter la gestion de la pile de Java ou l'exécution des méthodes locales. Pour faciliter le travail de l'interpréteur, deux instructions HBP et HBLP ont été ajoutées. Elles branchent vers une fonction qui gère les exceptions logicielles (une fonctionnalité présente dans de nombreux langages, comme Java). La première effectue un branchement vers la fonction en question, la seconde sauvegarde l'adresse de retour avant de faire ce branchement. Les deux peuvent brancher vers 256 fonctions pré-déterminées, sans avoir à présenter leur adresse. ==La traduction binaire accélérée par le matériel== Plus haut, nous avons surtout parlé des compilateurs et des interpréteurs. Cependant, nous devons aussi parler de la '''traduction binaire'''. Elle traduit un programme écrit dans un code machine vers un autre code machine. Par exemple, elle traduit un programme compilé pour un CPU x86 vers un code machine ARM. En général, le système d'exploitation est généralement compilé pour le jeu d'instruction natif, mais il exécute des applications prévues pour le x86. Les applications sont traduites par le système d'exploitation, avant d'exécuter le code traduit. : Nous parlerons dans la suite de code machine source et de code machine cible, pour parler respectivement du code à traduire et du code obtenu après traduction. Idem avec d'autres termes comme architecture cible/source, ou jeu d'instruction cible/source. La traduction binaire est surtout utilisée pour des questions d'émulation ou de compatibilité. Par exemple, lorsque les Macintosh sont passés de processeurs Power PC vers des processeurs x86, le système d'exploitation Mac OS utilisait la traduction binaire pour convertir les anciennes applications Power PC vers du code x86. Les utilisateurs n'y ont vu que du feu. Le système de traduction binaire était appelé Rosetta 1. Par la suite, lors de la transition de processeurs x86 vers des processeurs Apple, Rosetta 2 a vu le jour. La traduction binaire a surtout été utilisée pour traduire du code x86 vers un autre jeu d'instruction. Il faut dire que le x86 est le jeu d'instruction dominant. De nombreuses entreprises ont eu pour ambition de briser l'hégémonie du x86 sur PC, en remplaçant le x86 par un jeu d'instruction plus performant, tout en gardant une compatibilité maximale. La traduction binaire était la seule solution pratique. Elle était le plus souvent intégralement réalisée en logiciel, comme c'était le cas sur les architectures Itanium avec le ''IA-32 Execution Layer''. ===Les généralités sur la traduction binaire assistée en matériel=== L'architecture source est presque tout le temps une architecture CISC, assez ancienne, qu'on souhaite émuler. Le choix de l'architecture cible se porte souvent sur une architecture VLIW, fort différente de l'architecture source, et ce pour obtenir une économie de matériel conséquente. Pour s'exécuter rapidement, le code traduit doit exploiter le parallélisme d'instruction, à savoir exécuter plusieurs instructions en même temps dans des unités de calcul séparées. Il est possible d'utiliser un processeur superscalaire avec exécution dans le désordre pour cela, mais au prix d'un cout important en transistors. Alors qu'en utilisant un CPU VLIW, c'est le traducteur binaire qui fait tout le travail d'extraction du parallélisme d'instruction. Et après tout, quitte à avoir un traducteur binaire, autant lui refiler le boulot d'optimisation. Compiler du code à la volée est certes assez lourd, mais qu'il existe des algorithmes efficaces pour regrouper des instructions indépendantes dans une seule instruction VLIW. Au passage, le processeur VLIW a plus de registres que le processeur source. Cela permet de faire du renommage de registres directement en logiciel. Le traducteur binaire n'hésite pas à changer les noms de registres entre instructions source et instruction VLIW cible, afin de supprimer des dépendances de données. En conséquence, cela ouvre des opportunités de parallélisme, qui sont exploitées lors du regroupement des opérations en instructions VLIW. Vous pourriez penser que le choix d'un CPU VLIW pour émuler du CISC est tout sauf optimal. Vous devez penser que la traduction binaire est d'autant plus simple que l'architecture source et cibles sont semblables. Dans les faits, ce n'est pas tellement traduire les instructions qui pose problème, mais plus la gestion du registre d'état, des exceptions matérielles, de la mémoire virtuelle, la différence entre gros-boutisme et petit-boutisme, de même que des différences d'adressage pour les périphériques. Le premier problème est la gestion des conditions, notamment en présence d'un registre d'état. En pratique, la traduction binaire s'utilise pour traduire du code CISC vers du code RISC ou VLIW. Les architectures source ont donc un registre d'état, qui est mis à jour non seulement par des instructions de test, mais aussi des instructions arithmétiques. L'architecture cible n'a elle pas de registre d'état, mais des registres à prédicats. La traduction de l'un vers l'autre est alors quelque peu compliquée. Et elle est d'autant plus compliquée que le registre d'état est la source de dépendances d'instruction implicites, qui réduisent les performances. Par exemple, si une instruction arithmétique modifie le registre d'état, cela peut impacter l'exécution d'une instruction ultérieure, qui lit ce registre d'état. Pour éliminer ces fausses dépendances, le traducteur binaire doit renommer ce registre en logiciel et détecter les dépendances utiles. Et le compilateur doit gérer les cas où il y a beaucoup de distance entre les deux, voire les cas où l'instruction dépendante n'a pas encore été analysée par le compilateur. Un autre problème est lié à la gestion des exceptions matérielles, et précisément des exceptions précises. Pour rappel, une instruction VLIW regroupe plusieurs opérations, opérations qui correspondraient à une instruction machine sur un CPU pas VLIW. En conséquence, plusieurs instructions du langage machine "source" sont regroupées en une seule instruction VLIW. Et il faut tenir compte du cas où une instruction source lève une exception. Dans le code VLIW, cela signifie qu'une opération lève une exception, et il faut annuler partiellement l'instruction VLIW associée. Et par partiellement, on veut dire que seules les opérations suivantes dans l'ordre du programme source doivent être annulées, puis ré-exécutées. Et c'est un sacré casse-tête ! Les processeurs VLIW utilisent comme solution le mécanisme d'exceptions différées des processeurs EPIC, vu il y a quelques chapitres. Pour résumer, le code est exécuté par blocs d'instruction, délimités par des branchements ou tout autre limite/barrière pertinente dans le code. Un bloc de code est compilé en deux versions : une version rapide sans exceptions matérielles, une version lente qui gère les exceptions précises. La version rapide s'exécute sans exécuter les exceptions. Cependant, les exceptions matérielles sont enregistrées, pour être prises en compte à la toute fin du bloc de code. La version rapide est exécutée en premier et elle mémorise si une exception matérielle a eu lieu. Puis une instruction vérifie si une exception a eu lieu et décide quoi faire. Si aucune exception n'a eu lieu, elle passe à la suite du programme, les résultats du bloc de code sont définitivement acceptés. Mais si une exception a eu lieu, tout est annulé. Le processeur est remis dans l'état initial, puis le code est ré-exécuté instruction par instruction de manière à gérer l'exception correctement. ===La traduction par pré-décodage=== Une première solution serait de faire la traduction binaire dans le cache d'instruction. Lors d'un défaut de cache, le code chargé depuis la RAM est traduit en code machine cible. Et c'est ce code machine cible qui est mémorisé dans le cache d'instruction et exécuté par le processeur. La technique marche sur le papier et n'est qu'une amélioration des techniques de pré-décodage vues il y a quelques chapitres. Cependant, le code obtenu est une traduction assez basique, qui n'incorpore pas d'optimisations dignes de ce nom. De plus, elle gère mal le cas où la taille du code source et cible sont potentiellement très différentes. Concrètement, elle est surtout utile pour traduire du code d'un processeur RISC vers un autre processeur RISC, dont les tailles d'instructions sont similaires. Elle ne permet pas d'émuler plusieurs jeux d'instructions différents, le cout en matériel (un circuit de pré-décodage par jeu d'instruction) serait trop important. ===Le projet DAISY d'IBM=== De très rares processeurs étaient conçus pour accélérer cette traduction binaire, afin de garder de bonnes performances. Les premiers à avoir étudié l'idée étaient IBM, avec leur projet DAISY (''Dynamically Architected Instruction Set from Yorktown''). Le projet de base était de convertir à la volée du code compilé pour des CPU Power PC, vers du code VLIW. Les chercheurs d'IBM avaient développé un algorithme de traduction dynamique efficace, ainsi qu'un processeur VLIW disposant d'optimisations spécifiques à la traduction binaire dynamique, à la volée. De nombreuses idées de ce projet ont été reprises ou re-découvertes par la société Transmetta, avec ses processeurs Crusoe et Efficieon, puis par NVIDIA avec son projet Denver. Les ingénieurs de ce projet ont étudié la possibilité d'émuler plusieurs jeux d'instructions différents sur un même processeur, notamment le s390 d'IBM et le x86. En théorie, cela demande juste d'avoir plusieurs programmes de traduction binaire : un pour le x86, un autre pour le s390, éventuellement un autre pour le Power PC. Mais cela ne s'est pas concrétisé. Mais les ingénieurs ont étudié quelle pourrait être l'architecture VLIW idéale pour ça. Par exemple, il fallait des additions 3-opérandes pour simplifier les calculs d'adresse. Une difficulté était la gestion du registre d'état, dont les bits ne sont pas mis à jour de la même manière sur le x86 et le s390 ou le Power PC. La différence entre gros-boutisme et petit-boutisme était aussi un problème, de même que des différences d'adressage pour les périphériques. Pour que la traduction binaire soit efficace, le processeur VLIW intègre diverses optimisations, comme les exceptions différées et les branchements multi-voies. La principale est la suivante : seul le code des boucles ou fonctions exécutées fréquemment est traduit en code VLIW, le reste du code est interprété. En effet, traduire du code binaire prend plus de temps qu'une simple interprétation. Pour du code qui ne sera exécutée qu'une seule fois, il est plus rapide d'utiliser l'interprétation. Par contre, pour du code exécuté beaucoup de fois, le cout de la traduction binaire est amorti, dilué sur N exécutions, compensé par le gain en temps d'exécution de ce code traduit. Au final, cela permet de ne traduire que le code qui le mérite. La détection du code fréquemment exécuté est réalisée dans le cœur VLIW. L'unité de branchement mémorise les derniers branchements rencontrés et le nombre de fois qu'ils ont été exécutés. Si ils ont été exécutés un certain nombre de fois, l'unité de branchement lève une exception matérielle, qui invoque le traducteur binaire. La routine de cette exception est le traducteur binaire proprement dit. En clair, la traduction binaire est démarrée quand le processeur détecte qu'une fonction a été exécutée plus de N fois, via une exception matérielle dédiée. La mesure du nombre d'exécution d'un branchement se fait dans l'unité de calcul dédiée aux branchements, avec l'aide d'une sorte de ''branch adress buffer'' modifié. Pour rappel, le ''branch adress buffer'' mémorise le ''Program counter'' de chaque branchement récemment rencontré. Ici, chaque entrée du ''branch adress buffer'' est associée à un compteur incrémenté à chaque exécution du branchement. ===Les processeurs Crusoe et Efficieon de Transmetta=== Les processeurs Crusoe Et Efficeron sont deux processeurs VLIW produits par la société Transmetta, une société californienne rachetée par NVIDIA. Ils étaient conçus pour exécuter spécifiquement des programmes x86, système d'exploitation inclus. L'idée derrière ce projet était d'exécuter du code x86 sans que les applications, ni même le système d'exploitation et le BIOS soient au courant ! Les processeurs Transmetta étaient en réalité conçus pour exécuter un programme unique, le ''Code Morphing Software'' (CMS), qui traduisait le code x86 en code VLIW. Transmetta ne voulait pas que son processeur VLIW soit exploité directement, sans le CMS. Il n'avait pas rendu public de compilateur pour traduire du C vers du code VLIW, il n'avait pas donné la documentation du jeu d'instruction VLIW. Des efforts de rétro-ingénieurie, documentés sur le site realworldtech, ont cependant permis de comprendre comment étaient encodées les instructions du processeur Crusoe. Comme pour le projet Daisy, le ''Code Morphing Software'' utilisait à la fois interprétation et traduction binaire, selon les besoins. Les instructions sont interprétées lors de leurs premières exécutions, mais le CMS bascule sur de la traduction en code VLIW après un certain nombre d'exécution. Ainsi, les boucles souvent exécutées sont traduites en code VLIW, alors que le reste du code est interprété. Le choix du code à traduire est le fait du CMS, il utilise des heuristiques complexes pour, qui ne sont pas connues dans le détail. Le ''Code Morphing Software'' est mémorisé dans une EEPROM, ce qui en fait un ''firmware'' situé en dessous du BIOS. Le CMS démarre ensuite le BIOS, qui lui-même démarre le système d'exploitation, qui lui-même démarre les pilotes de périphériques et les programmes. Le CMS se réserve les 16 premiers mébioctets de l'espace d'adressage. L'EEPROM du CMS est mappée dedans, mais n'en utilise que la moitié. Le reste est utilisé comme cache, pour mémoriser le code VLIW traduit par le CMS. Les deux processeurs intégraient aussi deux mémoires SRAM, utilisées par le CMS, appelées la ''local program memory'' (LPM) et la ''local data memory'' (LDM). La première contient du code qui gére les interruptions, la mémoire virtuelle, les exceptions matérielles, les problèmes d'alignement mémoire, et quelques fonctions très fréquemment utilisées par le CMS. La seconde est de la mémoire RAM utilisée par ces fonctions de la ''local program memory''. Les mémoires caches L1 et L2 sont séparées de ces deux mémoires. Le processeur Crusoe et Efficieon étaient des processeurs VLIW très simples. Ils n'avaient même pas de MMU, ni de fonctionnalités importantes sur les CPU x86. Pour Crusoe, les instructions VLIW étaient encodées sur 64 ou 128 bits, et regroupaient entre 2 à 4 opérations. Les instructions LOAD/STORE ne supportaient pas d'adressage indicé, juste de l'adressage indirect à registre. Crusoe avait 5 unités de calcul : deux ALU entières, une unité LOAD/STORE, une unité de branchement, une FPU. Efficieon doublait le nombre d'ALU entière et d'unité LOAD/STORE, les instructions VLIW passaient à 256 bits. Le processeur Crusoe contient 160 registres, dont 64 registres généraux, 32 registres flottants. Les registres généraux font 32 bits, ce qui est cohérent avec le fait que les CPU x86 de l'époque étaient des processeurs 32 bits. Quant aux registres flottants, ils faisaient 80 bits, ce qui colle avec la taille des registres flottants de la FPU x87 utilisée à l'époque. Sur les 64 registres généraux, seuls 48 étaient réellement utilisable pour mémoriser des opérandes. Une partie des registres généraux étaient utilisés pour la gestion de la pile, d'autres pour mémoriser l'état du CPU x86 émulé, un registre était un registre zéro non accesible en écriture. La gestion des exceptions est optimisée avec un système d'exceptions différées, le même que celui décrit plus haut. Le processeur utilise un système similaire, pour effectuer des lectures anticipées, qui a été expliqué dans le chapitre sur les processeurs VLIW/EPIC. Sauf qu'il s'agit là d'une optimisation pour gagner en performances en général, pas une aide utile pour gérer des exceptions précises ou toute autre subtilité de la traduction binaire. Pour gérer les exceptions différées et les lectures anticipées, le processeur mémorise l'état du processeur avant de démarrer un bloc de code. Pour cela, le processeur contient des ''shadow registers'', 48 registres entiers et 16 registres flottants. Vous remarquerez qu'il y a autant de registres entiers que de ''shadow registers'' entiers. Les registres entiers/flottants sont mémorisés dans les ''shadow registers'' avant d'exécuter un bloc de code, afin que le processeur puisse revenir à l'état de base. Une autre fonctionnalité liée est le ''gated store buffer''. Le principe est d'éviter toute écriture en mémoire RAM, tant que le code VLIW n'a pas émis d'instruction ''commit''. L'idée est que les écritures sont conservées dans la ''store queue'' de l'unité LOAD/STORE. Pour rappel, la ''store queue'' met en attente les écritures soit tant que les écritures ne sont pas terminées, soit tant que la RAM est occupée. L'idée est que les écritures sont accumulées dans la ''store queue'' tant que l'instruction ''commit'' n'a pas eu lieu. Le ''store queue'' est "déversé dans la RAM" seulement quand l'instruction ''commit'' s'exécute. ===Les CPU NVIDIA Denver=== Nvidia a tenté quelque chose de similaire avec son projet Denver. NVIDIA a racheté l'entreprise Transmetta, ses brevets, et a tenté de refaire la même chose. Le projet initial était de traduire du code x86 vers un jeu d'instruction VLIW propriétaire, comme l'a fait Transmetta. Mais NVIDIA n'a pas réussi à acquérir la licence du jeu d'instruction x86 et s'est rabattu sur le jeu d'instruction ARM. Le processeur né de ce projet est le Tegra K1-64 CPU. Il s'agit d'un processeur composé d'un cœur VLIW, couplé à deux décodeurs d'instructions ARM. Les deux décodeurs ARM traduisent une paire d'instructions ARM consécutives en instructions VLIW, pour les exécuter sur le cœur VLIW. Le CPU peut fonctionner selon deux modes : ARM et VLIW. En somme, le processeur supporte deux jeux d'instruction : le jeu d'instruction ARM, et un jeu d'instruction VLIW. En mode ARM, les instructions ARM sont chargées depuis le cache, traduites en VLIW par les décodeurs ARM, puis exécutées par le cœur VLIW. En mode optimisé, le CPU exécute des instructions VLIW chargées depuis le cache d'instruction, les décodeurs ne sont pas utilisés. : Le CPU supporte en réalité deux jeux d'instruction ARM : ARM8 et 7. Mais ils correspondent au même mode pour le CPU. Un point important est que le processeur peut changer très rapidement de mode, en à peine quelques cycles d'horloges. On n'est clairement pas dans le cas des CPU x86, qui mettent des plombes pour passer du mode 32 à 64 bits et inversement. La commutation est tellement rapide qu'on peut considérer que le processeur supporte deux jeux d'instruction simultanément. Et le support simultané de deux jeux d'instruction facilite l'implémentation de la traduction binaire. Encore une fois, inutile de traduire du code qui ne sera exécutée qu'une seule fois, mieux vaut l'exécuter directement dans le mode ARM. Surtout que le processeur supporte un mode ARM qui permet d'exécuter du code ARM sans perte de performance. Par contre, le code fréquemment utilisé, notamment dans des boucles critiques, est traduit en VLIW. Au final, les performances sont optimisées, en limitant le travail du logiciel traducteur binaire. La détection du code fréquemment exécuté se fait différemment que pour les processeurs Transmetta et Daisy. L'unité de branchement ne détecte pas directement le code beaucoup exécuté, même si elle a un rôle à jouer. A la place, la détection est réalisée en logiciel, par un ''thread'' dédié, qui s'exécute sur un cœur séparé. Il détecte le code fréquemment utilisé en regardent les branchements exécutés récemment. Pour cela, le processeur mémorise l'historique des branchements pris récemment et copie cette historique dans une mémoire tampon dédiée, partagée entre tous les cœurs. Vu qu'elle est partagée, le ''thread'' de détection a accès à l'historique sans pertes de performances, seule la copie de l'historique dans ce tampon a un cout en performance, pas son partage. Le code VLIW obtenu après traduction binaire est mémorisé dans la mémoire RAM, dans une portion spécialement réservée pour. Elle fait 128 mébioctets, et est appelée le '''cache d'optimisation''' par NVIDIA. Le cache d'optimisation est protégé en écriture et seul le traducteur binaire peut écrire dedans. Au passage, le traducteur binaire est du code VLIW, ce qui fait qu'il est placé dans le cache d'optimisation. Le processeur détecte automatiquement quand une fonction pour laquelle le code compilé équivalent est disponible. Pour cela, l'unité de chargement contient une table de correspondance entre l'adresse de la fonction ARM, et l'adresse de son équivalent compilé. Quand une fonction est appelée, l'unité de chargement regarde l'adresse de destination du branchement. Si l'adresse est dans cette table, elle récupère l'adresse de la fonction compilée et branche vers celle-ci. Le processeur dispose d'un cache d'instruction de 128 kibioctets, ce qui est très important, mais nécessaire vu la taille des instructions VLIW. Le coeur VLIW dispose de ses propres décodeurs, d'un ''scoreboard'', de 7 unités de calcul et de registres. Pour les unités de calcul, il a deux FPU, deux ALU, une unité de branchement, et deux unités LOAD-STORE faisant aussi ALU. Un point important est que le CPU dispose de 64 registres entiers et de 64 registres flottants. C'est deux fois plus que supporte le jeu d'instruction ARM. En mode ARM, seule la moitié des registres est utilisée. Mais en mode optimisé, le traducteur binaire utilise bien les 64 registres, grâce à une sorte de renommage de registres logicielle. Pour simplifier la traduction binaire, le processeur supporte les techniques vues précédemment. Le processeur intègre un ''gated store buffer'' similaire à celui des processeurs Transmetta. Les techniques d'exceptions différées sont aussi supportées, comme sur les processeurs Transmetta. Il a aussi une unité de préchargement décrite par NVIDIA comme agressive, avec support du préchargement de type anticipé (''runahead''). ===L'émulation de la mémoire virtuelle=== Pour finir, il faut parler de la mémoire virtuelle avec la traduction binaire. Sur le processeur source, le processeur gère à la fois des adresses physiques et virtuelles et sa MMU fait la conversion entre les deux. Sur le processeur VLIW, la MMU est simulée par le programme de traduction binaire, partiellement ou totalement. L'implémentation exacte varie suivant que l'on parle du projet Daisy, des processeurs Transmetta ou de Denver. Sur les processeurs Transmetta, il n'y a pas de MMU ni de mémoire virtuelle. A la place, le programme de traduction binaire émule la mémoire virtuelle du processeur source. Le processeur VLIW ne gère que des adresses physiques, rien d'autre. L'espace d'adressage physique du processeur VLIW a la même taille que celui du processeur émulé, ici des processeurs x86. Il est vraisemblable que les adresses physiques utilisées par le processeur x86 sont les mêmes que celles du processeur VLIW. Sur le projet Daisy, le processeur VLIW gère la mémoire virtuelle via pagination, comme les processeurs émulés. Ce qui fait qu'il gère un espace d'adressage virtuel et un espace d'adressage physique. Pour éviter toute confusion, nous parlerons d'adresse physique/virtuelle VLIW pour les adresses physiques/virtuelles du processeur VLIW, d'adresse virtuelle/physique source pour celle du jeu d'instruction traduit, à savoir du Power PC ou du s390, éventuellement de l'x86. Il faut alors faire le lien entre adresses physiques source et adresses virtuelles VLIW. Pour cela, rien de plus simple : il y a correspondance parfaite. L'adresse physique source numéro N correspond à l'adresse virtuelle VLIW numéro N. Cependant, cela signifie que tout l'espace d'adressage virtuel VLIW serait utilisé par le code à traduire. En réalité, il faut ajouter de la place pour le code traduit et le programme de traduction binaire. La conséquence est que l'espace d'adressage virtuel VLIW est plus large que l'espace d'adressage source. L'espace d'adressage virtuel VLIW est découpé en trois sections : une section pour le code Power PC à traduire, une autre pour le code traduit en VLIW, et une dernière réservée au traducteur binaire. Le programme de traduction est placé dans une ROM mappée en mémoire dans la seconde section. Le reste de la seconde section est réservé à une mémoire RAM utilisée par le programme de traduction. [[File:Mémoire virtuelle sur le projet Daisy.png|centre|vignette|upright=1.5|Mémoire virtuelle sur le projet Daisy]] Il faut noter que chaque page physique source, une fois traduite, correspond à N pages virtuelles VLIW. La raison est que le code traduit prend plus de place que le code machine originel. En conséquence, on doit utiliser une page finale plus grande. Pour le reste, des pages contiguës en mémoire physique source sont elles aussi contiguës en mémoire virtuelle VLIW. Le calcul d'adresse est donc simple : l'adresse physique source est multipliée par N, puis on ajoute l'adresse de base à laquelle commence la section pour le code traduit. ==Les processeurs à jeu d'instruction reconfigurable== Nous venons de voir comment accélérer en matériel la traduction binaire. Mais il existe une alternative qui permet de se passer de traduction binaire, complétement. L'idée est d'avoir un processeur avec un '''jeu d'instruction reconfigurable'''. Le nom est assez parlent : on peut configurer le processeur de manière à supporter le jeu d'instruction de son choix ! Par exemple, on pouvait reconfigurer un CPU de manière à ce qu'il supporte le jeu d'instruction x86, ou qu'il exécute du ''bytecode'' Java, ou du code ARMv8, etc. De tels processeurs sont encore plus rare que ceux vu avant dans ce chapitre, mais ils ont existé ! Les processeurs de ce type avaient un '''microcode réinscriptible''', à savoir que c'était des processeurs microcodés pour lesquels on pouvait modifier le microcode. L'idée était qu'en changeant le microcode, on changeait le jeu d'instruction du processeur. Pour cela, le microcode était associé à une mémoire SRAM, en plus de la mémoire ROM du ''control store''. Le microcode était copié dans cette micro-SRAM au démarrage du CPU, puis les instructions étaient décodées en utilisant le microcode dans la micro-SRAM. En modifiant le contenu de la micro-SRAM, on pouvait altérer le microcode, voire le remplacer totalement. Il s'agit là de l'implémentation la plus simple. Mais le Burroughs B1700 a procédé autrement. Il lisait le microcode directement depuis la mémoire RAM ! Pour être précis, il avait plusieurs modèles : le B1726, le B1710 et le B1800. Le B1710 placait le microcode en mémoire RAM, ce qui fait que chaque exécution d'une instruction lisait les micro-opérations en mémoire RAM ! Le B1800 faisait pareil, mais avait ajouté un cache dédié au microcode, afin d'améliorer les performances. Le B1726 couplaiti un microcode scindé en deux : une partie était en mémoire RAM, l'autre était dans un microcode réinscriptible classique, intégré au processeur. ===Le Burroughs B1700 : architecture externe=== Un exemple est celui du Burroughs B1700, et il est intéressant d'étudier en détail son architecture. Son architecture est décrite dans le livre "Interpreting Machines : Architecture and Programming of the Bl700/Bl800 Series", écrit par Elliott I. Organick et James A. Hinds. Une partie de ce qui vfa suivre est un très court résumé de ce livre. Il disposait de 4 registres généraux, nommés X, Y, T et L, tous de 24 bits. Les registres T et L étaient découpés en 6 sous-registres de 4 bits, nommés LA LB LC LD LE et LF pour le registre L, et TA TB TC TD TE et TF pour le registre T. Ils étaient adressables comme n'importe quel registre. Un point important est que le registre T avait des possibilités d’extraction spécifiques. Il était possible d'extrait N bits, placés n'importe où dans ce registre, et de les copier dans un autre registre. Nous verrons pourquoi cette possibilité est intéressante et ne sert que pour le registre T. Les registres X et Y étaient reliés à une ALU de 24 bits capable d'effectuer des opérations arithmétiques basiques. Fait étonnant, l'ALU faisait tous les calculs en même temps et fournissait ses résultats dans 7 registres : un registre SUM pour l'addition, un registre DIFFERENCE pour la soustraction, et un registre par opération bit à bit. Les registres pour les opérations bit à bit sont : CMPX et CMPY pour l'opération NOT sur chaque opérande, XANY pour le ET bit à bit, XEOY pour le XOR, XORY pour le OU logique. Les additions et soustractions pouvaient se faire sur des opérandes codés en binaire ou en BCD, suivant le code opération utilisé. A cela, il fallait ajouter deux registres de "résultat" MSKX et MSKY. Ils fournissaient un masque dépendant de la taille du résultat en bits. Cela permettait de gérer des données de 0, 4, 8, 12, 16, 20, et 24 bits, alors que l'ALU faisait 24 bits. Et cela n'était pas limité à 16 ou 8 bits, mais n'importe quelle taille inférieure ou égale à 24 bits était supportée, même des tailles non-conventionnelles comme des résultats codés sur 3 bits, 9 bits, 12 bits, etc. L'ALU peenait en opérande les registres X et Y, mais aussi un registre CYF pour la retenue entrante. En sortie, il fournissait aussi deux retenue sortantes : une pour l'addition et une autre pour la soustraction. Elles étaient mémorisées dans les registres CYL et CYD, respectivement. L'ALU fournissait aussi 12 conditions, qui alimentait un registre d'état. Le registre d'état était segmenté en trois registres appelés XYST, XYCN et BICN. Le processeur contenait de nombreux autres registres de 4 bits, dont les sous-registres de T et L. En tout, il y a 27 registres de 4 bits adressables ! Et le processeur disposait d'une ALU 4 bits pour manipuler ces registres de 4 bits ! Elle est capable de faire des MOV entre registres de 4 bits, des opérations bit à bit, et peut aussi tester si un bit vaut 0 ou 1. La dernière possibilité est très utile pour implémenter les branchements. Pour communiquer avec les entrées-sorties, il disposait de deux registres CMND et DATA : CMND pour envoyer une commande à une entrée-sortie, DATA pour lire ou écrire une donnée. Pour la mémoire, il avait deux registres séparés : READ et WRITE. READ contient la donnée lue lors d'une lecture,; WRITE est pour une donnée à écrire lors d’une écriture. Il s'agit de registres d’interfaçage mémoire, qui ont été promus au rang de registres architecturaux. Les deux registres sont gérés par le microcode, qui est le langage principal de la machine. Les instructions machines configurées font sans, mais le microcode doit y avoir accès facilement. Et vu que le microcode est le langage principal de ce processeur, ces deux registres deviennent architecturaux. ===Une architecture bit-adressable=== Si avoir un microcode réinscriptible est suffisant, quelques choix de ''design'' facilitent l'émulation de jeux d'instructions variés. Par exemple, le CPU Burroughs B1700 avait ajouté une fonctionnalité très importante : il était bit-adressable. En clair, il pouvait adresser la mémoire bit par bit, si nécessaire. L'avantage est que cela permettait d'émuler des architectures dont la taille des registres/''byte''/mots était très différente. Un autre avantage est que cela facilitait la gestion des instructions de taille variable. Détaillons rapidement le premier avantage. Le processeur était un CPU 24 bits, mais il pouvait émuler des processeurs 16 bits, 8 bits, 4 bits, 1 bit, 9 bits, ou toute autre valeur. Pour cela, le processeur lisait des mots de 24 bits et utilisant un ''barrel shifter'' pour sélectionner les bits adéquats. Par exemple, pour un accès de 16 bits, la donnée lue en mémoire était masquée de manière à ne garder que les 16 bits de poids faible. Le ''barrel shifter''/circuit de masquage était placé directement avant le bus mémoire, sur le trajet des données/instructions. Le ''barrel shifter'' était commandé par un registre ''Field Unit'', qui précisait quelle était la taille des données à charger. ===La gestion de la taille des instructions=== Les instructions à décoder n'ont pas de longueur fixe, à savoir qu'elles peuvent faire 16 bit avec tel jeu d'instruction, 8 bits avec un autre, 32 bits sur un autre, etc. Elles peuvent aussi être de longueur fixe ou variable ! Le B1700 intégrait donc un support des instructions de longueur variable, qui était "détourné" pour gérer des jeux d'instruction différents. Notons que vu que l'architecture est bit-adressable, les contraintes d'alignement des instructions disparaissent, ce qui a des avantages qu'on va détailler dans ce qui suit ! L'implémentation des instructions de taille variable ajoutait un registre avant le registre d'instruction. Le registre d'instruction proprement dit était un registre M de 24 bits. Il était précédé par un registre T, dans lequel l'instruction était assemblée, en combinant plusieurs morceaux. Si l'instruction est codée est codée sur moins de 24 bits, le registre T se contente de masquer les bits en trop. Si l'instruction chargée dépasser 24 bits, le chargement se faisait en plusieurs passes, avec plusieurs lectures en mémoire. Dans ce qui suit, on suppose que l'instruction est composée de '''champs''' : l'opcode, les opérandes, les adresses absolues, les constantes immédiates, les numéros de registre, etc. L'idée est que l'instruction est chargée dans le registre T, puis découpée en champs, qui sont copiés dans les registres du processeur ou envoyés au microcode. Je rappelle que le registre T est relié à un ''barrel shifter'', différent au précédent, qui permettait de masquer, décaler et extraire une donnée de ce registre. Et il y a la possibilité d'extraire une suite de bit de ce registre T, pour la copier dans un autre registre. Par exemple, prenons une instruction composée d'un opcode, d'une constante immédiate et d'une adresse immédiate. L'opcode est lu et interprété par le microcode. Le microcode extrait la constante immédiate du registre T et la copie dans le registre X. Ensuite, l'adresse est extraite et envoyée au registre FA, une lecture est démarrée, et l'opérande est copiée dans le registre Y. Enfin, l'instruction est exécutée par le microcode. Le tout prend plusieurs micro-opérations : une première pour lire l'opcode, une seconde pour lire la première opérande, une troisième pour lire la seconde opérande, una quatrième pour tout exécuter. Notons que suivant la taille des champs, on peut en charger plusieurs à la fois. Par exemple, imaginons qu'on ait chargé 24 bits, contenant un opcode de 8 bits et une constante immédiate de 16 bits : pas besoin de faire deux accès mémoire, le registre T est juste découpé en un opcode et une constante. Le registre T est fait pour ce genre d'extraction. ===Le chargement d'un champ=== L'architecture est bit-adressable car elle gére des champs qui ne font pas forcément 8 ou 16 bits. Par exemple, certains jeux d'instructions ont des opcodes codés sur 6 ou 7 bits, voire 9-10 bits. Par exemple, prenons un opcode de 7 bits, suivi par une adresse de 12 bits et une constante immédiate de 15 bits : aucun de ces champs ne sera aligné sur 8/16/24 bits. Utiliser une architecture bit-adressable règle le problème d'alignement. Et en général, les contraintes d'alignement des instructions disparaissent ! Quand on charge une instruction, peut importe qu'elle soit ou non alignée sur 24 bits, elle sera placée dans les bits de poids faible, il suffira de masquer les bits de poids fort inutiles. Par exemple, si on charge une instruction de 16 bits, les 16 bits seront copiés dans les 16 bits de poids faible du registre T, il suffira de masquer l'octet de poids fort. Pas besoin de faire de décalages pour gérer des instructions mal alignées... Pour charger une instruction, il faut connaitre plusieurs informations : l'adresse de son premier bit, sa taille. Pour cela, le B1700 contient deux registres, nommés ''Field Adress'' (FA) et ''Field Length'' (FL), qui indiquent l'adresse du premier bit et la taille de l'instruction à charger. Le premier fait 24 bits, ce qui permet de gérer des adresses de 24 bits. N'oubliez pas que le processeur est bit-adressable, ce qui permet d'adresser 16 Mébi-bits, soit 2 mébioctets. Le registre pour la taille fait lui 16 bits, ce qui permet de gérer des instructions de 65536 bits, soit 8192 octets ! Leur nom commence par ''field'', car le processeur peut gérer des instructions de taille variable et/ou plus longues que 24 bits. Dans ce cas, le chargement de l'instruction se fait champ par champ. Pour donner un exemple, prenons une instruction composée de 20 champs de 12 bits chacun. Les champs sont chargés un par un dans le registre T. Le registre FA est alors initialisé avec l'adresse de l'instruction, il est incrémenté de 12 à chaque cycle. Le registre FL est initialisé avec la taille de l'instruction, soit 20 * 12 = 240 bits, et il est décrémenté de 16 à chaque cycle. Précisons cependant que la taille d'un champ peut être changée d'un cycle à l'autre. Par exemple, le processeur peut charger un opcode de 8 bits, l'interpréter, détecter que l'instruction demande ensuite de charger une adresse de 16 bits. Dans ce cas, le registre FL est immédiatement altéré pour charger 16 bits, au lieu des 8 bits de l'opcode immédiatement précédent. ===Le microcode réinscriptible=== Le microcode utilise des micro-opérations de 16 bits de long, ce qui est très peu et bien en-dessous des micro-opérations des autres processeurs. Les micro-opérations sortant du microcode sont envoyées au registre M, un registre de micro-opération de 16 bits. les micro-opérations écrites dans ce registre sont ensuite décodées par le hardware et exécutées. Fait intéressant, il était possible de faire un OU logique entre la micro-opération sortante et un registre du processeur, le résultat étant mémorisé dans le registre M. Cela permettait de faire du code automodifiant dans le microcode ! Utiliser du code automodifiant avec un microcode réinscriptible est parfaitement possible, et c'est utile pour émuler des modes d'adressage complexe dans le microcode. Cela permettait par exemple de remplacer une adresse dans une micro-opération. L'adresse est alors surimposée avec un OU dans le champ d'adresse, alors que le reste de l'instruction n'est pas modifié. Mais modifier directement le microcode est rarement une bonne idée. Aussi, pour éviter cela, l'idée est de modifier les instructions avec un OU logique avec un autre registre. Les micro-opérations du microcode ne sont donc pas modifiées dans le microcode, mais en sortie du microcode. Afin de gérer les interruptions, ces dernières étaient partiellement détectées en logiciel. Les interruptions ne s'exécutaient pas automatiquement, pour émuler correctement les interruptions. Une interruption doit être prise en compte à la toute fin de l'exécution d'une instruction, quand celle-ci a finit son travail. Mais vu qu'ici, les instructions sont émulées avec des séries de micro-opérations, il y a un problème : il est possible d'interrompre une instruction émulée en plein milieu. Pour éviter cela, le processeur ne déclenche pas les interruptions matérielles immédiatement, elles sont mises en attente. Une micro-opération dédiée vérifie si une interruption est en attente, et l'exécute alors cas échéant. La micro-opération est utilisée comme micro-opération finale d'une instruction émulée. ===Un ''split-level control store''=== La RAM mémorisant le microcode était de petite taille, et il arrivait qu'elle soit trop petit pour mémoriser le microcode complet. Dans ce cas, le programmeur devait gérer la situation en découpant le microcode en segments, certains étant placé dans le processeur, les autres étant en mémoire RAM. Le processeur peut lire le microcode placé en mémoire RAM et même l'exécuter, mais avec une pénalité en termes de performances. La documentation appelle cela un '''''split-level control store'''''. Quelques registres intégrés au processeur permettent ce genre de magie. Un tel microcode permettait de placer le microcode le plus fréquemment utilisé dans le processeur, alors que le reste du microcode était placé en mémoire RAM. Les instructions fréquentes étaient ainsi décodées dans le processeur, alors que les instructions peu fréquentes étaient décodées en lisant le microcode associé en mémoire RAM. La pénalité en termes de performance est alors concentrée sur les instructions les plus rares, ce qui est un très bon compromis. Mieux que ça, le processeur autorisait d'utiliser la technique de l'''overlaying'' pour le microcode ! Le microcode pouvait servir pour émuler un autre jeu d'instruction, mais il était aussi possible d'implémenter un véritable interpréteur, grâce aux techniques du paragraphe précédent. Le processeur était même conçu pour ! L'interpréteur était placé dans un "segment" en mémoire RAM, dont le processeur connaissait la position. Il incorporait pour cela deux registres, un pour l'adresse de base de l'interpréteur, un autre pour son adresse de fin. Il incorporait une protection mémoire limitée sur ce segment, à savoir que seul l'interpréteur pouvait lire ou écrire dans ce segment. Il faut noter que le ''split-level control store'' n'était disponible que sur le modèle B1726, pas sur le B1710 ou le B1800. Le B1700 lisait le microcode depuis la mémoire RAM, il n'avait pas de RAM dédiée pour le microcode réinscriptible ! Le B1800 faisait pareil, mais avait un cache dédié au microcode, afin d'avoir de meilleures performances. ==Les jeux d'instructions dédiés à un langage de programmation== De rares processeurs sont conçus pour un langage de programmation en particulier. On appelle ces processeurs, conçus pour des besoins particuliers, des '''processeurs dédiés'''. Par exemple, les fameux ''Burrough E-mode'' B5000/B6000/B7000 étaient spécialement conçus pour exécuter de l'ALGOL-60. Leurs cousins B2000/B3000/B4000 étaient eux conçus pour le COBOL. Des langages fonctionnels ont aussi eu droit à leurs processeurs dédiés. Le prolog en est un bel exemple, avec les superordinateurs de 5ème génération qui lui étaient dédié. On peut aussi citer les machines LISP, dédiés au langage LISP, qui datent des années 1970. Elles étaient capables d’exécuter certaines fonctions de base du langage directement dans leurs circuits : elles possédaient notamment un ''garbage collector'' câblé dans ses circuits ainsi que des instructions machines supportant un typage déterminé à l’exécution. Les processeurs dédiés ont eu leur heure de gloire au début de l'informatique, à une époque où les langages de haut niveau venaient d'être inventés. À cette époque, les compilateurs n'étaient pas performants et ne savaient pas bien optimiser le code machine. Il était alors rationnel, pour l'époque, de rapprocher le code machine cible et le langage de programmation de haut niveau. De nombreuses architectures dédiés ont ainsi été inventées, avant que les concepteurs se rendent compte des défauts de cette approche. Les défauts en question ne sont pas nombreux, mais assez simples à comprendre. Premièrement, elles sont très rapides pour un langage de programmation en particulier, mais sont assez mauvaises pour les autres, d'où un problème de "compatibilité". Ajoutons à cela que les langages de programmation peuvent évoluer, devenir de moins en moins populaires/utilisés, ce qui rend la création d'architectures généralistes plus pertinente. Enfin, les architectures dédiées sont évidemment des processeurs CISC, pour implémenter les nombreuses fonctionnalités des langages évolués. Et les défauts des CISC sont assez rédhibitoires à l'heure actuelle. À l'heure actuelle, les algorithmes des compilateurs se sont améliorés et savent nettement mieux utiliser le matériel. Ils produisent du code machine efficace, ce qui rend les architecture dédiées bien moins intéressantes. Si on ajoute les défauts de ces architectures dédiées, par étonnant que les architectures dédiées aient presque disparues. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'accélération matérielle de la virtualisation | prevText=L'accélération matérielle de la virtualisation | next=Le matériel réseau | nextText=Le matériel réseau }} </noinclude> rudcnrqirvtl1xeoq4ja59jmzoobk5n 764033 764032 2026-04-19T19:07:53Z Mewtow 31375 /* Les processeurs à jeu d'instruction reconfigurable */ 764033 wikitext text/x-wiki De nos jours, la majorité des programmeurs programment dans des langages de haut niveau. Il est très rare pour eux d'avoir à utiliser de l'assembleur, et encore moins un langage machine. Les programmes écrits dans un langage de haut niveau sont traduits en langage machine par un logiciel appelé le compilateur. [[File:Compilation.PNG|centre|vignette|upright=2.5|Compilateur : principe.]] Il se trouve que quelques rares processeurs sont conçus pour faciliter le travail du compilateur. Et ce sont ces processeurs que nous allons voir dans ce qui suit. Ils représentent une catégorie de jeux d'instruction à part, qui n'a pas vraiment de nom. ==La compilation et l’interprétation== Un compilateur traduit donc du code source, écrit dans un langage de haut niveau, vers du langage machine. La traduction est rarement directe. En général, le compilateur traduit le code source en assembleur, qui est lui-même traduit en langage machine. L'assembleur est une représentation textuelle du langage machine alors que le code machine est du binaire exécutable par le processeur. La traduction se fait donc en deux étapes, la '''compilation''' proprement dite et l''''assemblage''', réalisées respectivement par un compilateur et un assembleur. Il faut parfois rajouter une troisième phase d'édition des liens, que nous passons volontairement sous silence. {| |[[File:Code C.png|vignette|200px|Le code source, ici en C.]] |[[File:Assem.png|vignette|250px|Le code assembleur.]] |[[File:Binary file - hello world (C programming).png|vignette|150px|Le code machine.]] |} ===L'interprétation et la compilation à la volée=== Une alternative à la compilation est l''''interprétation''', qui transforme le code source en code machine à la volée. Avec l'interprétation, le code source est passé à un logiciel appelé l''''interpréteur''', qui exécute le code source ligne de code par ligne de code. Une ligne de code est traduite en code machine, qui est exécutée, puis l'interpréteur passe à la ligne suivante. [[File:Kaantotulkkaus.png|centre|vignette|upright=3|Compilation versus interprétation.]] Un défaut de l'interprétation est son cout en performance, qui est assez important. Aussi, l'interprétation stricte a évolué vers un hybride entre interprétation et compilation. L'idée est qu'une partie peu importante du code source est interprétée, alors que le code important est compilé juste avant d'être exécuté. Pour le dire autrement, le code source est partiellement compilé à la volée, juste avant son exécution. Aussi, on parle de '''compilation à la volée'''. Le terme en anglais est ''Just In Time compilation'', abrévié en JIT. En soi, le JIT est associé à de l'interprétation, les JIT sont en réalité des hybrides interpréteurs-compilateurs. Un point important est que compiler du code à la volée est assez lourd, cela a un cout en performance. Par contre, le code compilé s'exécute plus vite que du code interprété. Si on exécute le code compilé une seule fois, le cout de la compilation l'emporte sur le gain à l'exécution. Mais le code compilé est réutilisé autant de fois que nécessaire. Le gain à l'exécution est donc multiplié par le nombre d'exécutions, alors que le cout de la compilation est répartit sur plusieurs exécutions, il est amorti. Les portions du code sont choisies de manière à avoir un gain en performance maximal. L'idée générale est que le code exécuté très souvent est compilé, alors que du code exécuté pas souvent est interprété. Typiquement, les boucles les plus souvent exécutées sont compilées, le code exécuté une seule fois est interprété. ===Les langages intermédiaires=== Les compilateurs modernes passent par un '''langage intermédiaire''' pour faire la transformation en code machine. Le compilateur traduit le langage de haut niveau en langage intermédiaire, puis traduit le langage intermédiaire en code machine cible. Le langage intermédiaire est parfois appelé le '''''bytecode''''', et ce terme recouvre aussi le code écrit avec du ''bytecode''. L'interprétation aussi peut passer par l'intermédiaire d'un un ''bytecode'', ce n'est pas limité aux compilateurs proprement dit. {| |[[File:Intermediate representation scheme.png|centre|vignette|upright=1.5|Représentation intermédiaire d'un compilateur.]] |[[File:Code interpreter scheme.png|centre|vignette|upright=1.5|Fonctionnement d'un interpréteur.]] |} Faire ainsi a de nombreux avantages, le principal étant d'avoir un compilateur capable de traduire un langage de haut niveau vers plusieurs jeux d'instructions différents. Cela permet d'avoir, par exemple, un compilateur qui traduit du C soit en code machine x86, soit en code machine pour un CPU ARM, soit pour un CPU POWERPC, etc. [[File:Compiler design.svg|centre|vignette|upright=3|Compiler design]] Pour l'interprétation, l'usage d'un ''bytecode'' a d'autres avantages. Pour l'exploiter, il faut distribuer non pas le code source, mais le ''bytecode''. En clair, l'interpréteur ne prend pas en entrée le code source, mais du ''bytecode'' déjà compilé. La traduction est alors beaucoup plus simple, car le code source a déjà été partiellement compilé, par la traduction en ''bytecode''. Interpréter du code source est en effet assez compliqué : il faut effectuer des étapes d'analyse lexicale, sémantique, et bien d'autres. Avec du ''bytecode'', ces étapes ont été réalisées lors de la compilation du ''bytecode'', l'interpréteur a alors peut de choses à faire. L'interprétation du ''bytecode'' se fait instruction par instruction, au niveau du ''bytecode''. Une instruction du ''bytecode'' est traduite en une instruction machine équivalente, qui est exécutée. Si l'instruction est un peu complexe, il exécute une fonction/procédure qui fait la même chose. Pour résumer, le ''bytecode'' est ensuite traduit à la volée et exécuté instruction par instruction par un logiciel appelé l'interpréteur. [[File:Control table.png|centre|vignette|upright=2|Control table]] Le langage intermédiaire peut être vu comme l'assembleur d'un processeur, n'existe pas forcément dans la réalité, mais dont le jeu d'instruction est décrit en détail. Le processeur en question est appelé une '''machine abstraite''', ou encore une ''machine virtuelle''. Nous utiliserons le terme de machine abstraite dans ce qui suit. La machine abstraite n'est pas la même suivant que l'on cible la compilation ou l'interprétation/JIT. Pour l'interprétation/JIT, la machine abstraite est souvent une machine à pile, car cela simplifie grandement la traduction en code machine final. L'interprétation demande que la traduction du ''bytecode'' en code machine soit la plus simple possible. Et cela demande de prendre en compte pas mal de détails. Par exemple, les différents jeux d'instruction existants n'ont pas le même nombre de registres, ce qui pose problème lors de la traduction du ''bytecode''. Lors de la transformation en code machine, un algorithme d'allocation de registres se débrouille pour traduire le code intermédiaire en code qui utilise un nombre limité de registres. Pour améliorer cette allocation de registres, il y a deux solutions niveau ''bytecode'' : soit utiliser un nombre illimité de registres, soit utiliser une machine à pile. Pour les machines à pile, il existe un algorithme simple et rapide pour traduire un code écrit pour une machine à pile en un code écrit pour un processeur avec des registres, qui se débrouille pas trop mal pour allouer efficacement les registres. C'est un avantage assez important pour les langages interprétés. ==Les processeurs qui exécutent du bytecode== Le ''bytecode'' est un code machine, ce qui signifie qu'il peut en théorie s'exécuter sur un processeur qui implémente le jeu d’instruction associé. Si le jeu d'instruction d'un ''bytecode'' est souvent une description censée être fictive, elle n'en reste pas moins un jeu d'instruction et des caractéristiques précises, qu'on peut l'implémenter en matériel ! Néanmoins, tous les ''bytecode'' ne sont pas égaux de ce point de vue. Certains sont faciles à implémenter en matériel, d'autres non. Les ''bytecodes'' implémentés en matériel sont ceux dont les machines abstraites sont des machines à pile. Un premier exemple est celui des processeurs Pascal MicroEngine, qui exécutaient directement le bytecode du langage Pascal, le fameux UCSD P-code. Un second exemple est la '''machine SECD''', qui sert de langage intermédiaire pour certains compilateurs de langages fonctionnels. Elle a été implémentée en matériel par plusieurs équipes, notamment par les chercheurs de l'université de Calgary en 1989. Dans le même genre, quelques processeurs simples étaient capables d’exécuter directement le ''bytecode'' du langage FORTH. Le FORTH, un des premiers langages à pile de haut niveau, possède de nombreuses implémentations hardware et est un des rares langages de haut niveau dont le ''bytecode'' a été utilisé comme langage machine sur certains processeurs. Par exemple, on peut citer le processeur FC16, capable d’exécuter nativement du ''bytecode'' FORTH. ===Les processeurs Java=== Le cas le plus impressionnant est celui de la machine virtuelle Java, qui est un design de processeur comme un autre. En temps normal, le ''bytecode'' Java est compilé ou interprété, mais certains processeurs exécutaient du ''bytecode'' Java directement, sans interprétation ni compilation. Ils sont appelés des '''processeurs Java'''. On peut citer les processeurs ARM disposant de l'extension Jazelle, du JEMCore et du aJ-100 de aJile Systems, le picoJava II de Sun, et quelques autres. Il y a aussi eu quelques projets de recherche ou processeurs open source, comme Komodo, jamuth, le ''Java Optimized Processor'' et quelques autres projets du même genre. L'implémentation de la machine virtuelle Java n'était cependant pas complète. Quelques instructions complexes n'étaient pas gérées par le processeur et devaient être émulées en logiciel. Mais la grosse majorité des instructions l'était. Les instructions non-supportées étaient émulées avec la méthode ''trap and emulate'', à savoir qu'un opcode inconnu déclenchait une exception matérielle ''opcode invalide'', dont la routine pouvait émuler les instructions qui doivent l'être. Le processeur aJ-100 de aJile est capable d'exécuter la quasi-totalité des instructions de la JVM Java, seules deux instructions faisant exception. Les '''accélérateurs Java''' sont une solution intermédiaire, qui permet de traduire à la volée du ''bytecode'' Java en instructions RISC, exécutées sur un autre processeur. Il s'agit formellement de traduction binaire, mais je préfère en parler ici. L'idée est de combiner un processeur RISC avec un coprocesseur Java. Le coprocesseur lit le ''bytecode'' Java et le traduit en instructions RISC, ces dernières étant exécutées sur le processeur RISC. Le JA108 de Nozomi était un coprocesseur de ce genre, au même titre que le coprocesseur JSTAR de JEDI. L'extension Jazelle de certains CPU ARM fonctionnait vraisemblablement sur un principe similaire, sauf que le coprocesseur était intégré dans le processeur ARM, entre le cache d'instruction et le décodeur d'instructions ARM. L'intérêt des processeurs Java n'est pas qu'une question de performance. Certes, ils permettent d’exécuter plus vite les programmes compilés en ''bytecode'', comme des programmes Java pour la JVM Java. Mais l'intérêt est aussi de faire des économies de mémoire RAM et ROM. Je rappelle que la machine virtuelle Java est une machine à pile, ce qui fait que sa densité de code est excellente. Les programmes en ''bytecode'' Java sont donc très petits, et prennent peu de place en mémoire ROM. De plus, utiliser un processeur Java permet de se passer d'interpréteur ou de JIT Java, ce qui économise encore plus de mémoire. ===Transformer un processeur RISC en machine à pile=== La plupart des processeurs Java utilisent en interne une architecture à registre généraux, qui émule une machine à registre en microcode. Quelques registres sont réservés pour l'état de la machine virtuelle, avec au minimum un registre réservé pour le pointeur de pile, un autre pour un pointeur vers un ''pool'' de constantes, et quelques autres. D'autres registres servent à mémoriser le haut de la pile, à savoir le sommet de la pile et quelques opérandes situées juste en-dessous. Par exemple, le processeur picoJava II gardait les 64 opérandes au sommet de la pile dans 64 registres généraux, séparés des registres pour les pointeurs et l'état de la JVM. Une optimisation possible, envisagée sur les processeurs picoJava, était une sorte de macro-fusion sous stéroïde. L'idée était de fusionner certaines séries d'instructions Java en une seule instruction machine. Typiquement, pour faire une opération arithmétique, une machine à pile empile deux opérandes et exécute une opération, ce qui prend trois instructions machine. La spécification picoJava 1 a proposé d'envoyer directement la seconde opérande en entrée de l'ALU, ce qui fusionnait l'empilement de la seconde opérande et l'opération arithmétique. Le processeur picoJava II va plus loin et fusionne les trois instructions en une seule instruction RISC, agissant sur des registres. Mais la majorité des processeurs Java ne faisaient pas cela, car cela complexifie grandement le décodeur. Les processeurs ''bigfoot'' de DCT utilisaient un système différent pour émuler une pile avec des registres. Leur banc de registre de 64 bits était coupé en deux : les 16 premiers registres étaient utilisés pour la pile, les 48 suivants étaient utilisés pour autre chose. L'idée est que le registre 0 mémorise un emplacement vide, le registre 1 contient le sommet de la pile, le registre 2 l'opérande sous le sommet de la pile, et ainsi de suite. Les 15 premières opérandes de la pile sont donc mémorisées dans les registres. Les instructions écrivent leur résultat dans le registre adéquat, puis tous les registres sont décalés pour que le résultat passe dans le registre R1. Par exemple, les instructions PUSH chargent une opérande dans le registre 0, puis tous les registres sont décalés de un rang, pour que l'opérande chargée soit dans le registre 1. Le registre 0 devient alors le registre numéro 1, le registre 1 devient le 2, et ainsi de suite. Par contre, les opérations arithmétiques font autrement. Une instruction d'addition, par exemple, lit les opérandes dans le registre 1 et 2, puis place son résultat dans le registre 2 (les deux opérandes sont dépilées). Puis, les registres sont décalés d'un rang pour que ce registre 2 devienne le 1. Les registres ne sont en réalité pas décalés, il n'y a pas de transferts entre registres. A la place, les 16 premiers registres utilisaient une forme limitée de renommage de registres, similaire au banc de registre tournant utilisé sur les processeurs EPIC comme l'Itanium. Les numéros de registres sont renommés, décalés, à chaque fois qu'on empile ou dépile une opérande. Pour faire le renommage de registres, processeur contient un ''register stack counter'' de 4 bits, qui est incrémenté à chaque fois qu'on empile une donnée, et est décrémenté quand on dépile une donnée. Quand on veut accéder à un opérande, le numéro de registre architectural associé est traduit en numéro de registre physique, en additionnant le ''register stack counter'' (modulo 16, pour rester sur 4 bits). ==L'accélération de l'interprétation/JIT : le ''Thumb-EE'' d'ARM== Quelques processeurs ont ajouté des instructions pour faciliter le travail des interpréteurs/JIT. Par exemple, en 2005, ARM a ajouté le mode ''Thumb Execution Environment'', qui faisait cela. Il reprenait le jeu d'instruction compact ''thumb'' et ajoutait quelques instructions et modifiait le comportement d'instructions existantes. Le jeu d'instruction ''Thumb-EE'' a été ajouté sur les CPU ARM en 2005, mais a été déprécié en 2011, par manque d'utilité. Mais la tentative mérite qu'on s'attarde dessus. Les CPU de gamme M, déstinés à l'embarqué, n'ont jamais supporté ''Thumb-EE'', vu que de tels processeurs ne sont pas conçus pour exécuter du code interprété/JIT. Le mode ''Thumb-EE'' est un mode d'exécution, séparé de l'ARM normal. Ainsi, une instruction ''Thumb-EE'' et une instruction ARM peuvent avoir le même encodage en binaire, mais se comporter différemment. Le processeur est à tout moment soit en mode ''Thumb-EE'', soit en mode ARM normal, ce qui précise comment ces instructions doivent se comporter. Des instructions ont été ajoutées par ''Thumb-EE'', pour qu'il fasse son travail. L'entrée et la sortie du mode ''Thumb-EE'' se fait avec deux instructions : ENTERX et LEAVEX. Une fois en mode ''Thumb-EE'', le décodage des instructions ''thumb'' se fait avec les règles du ''Thumb-EE'', et non celles du ''thumb'' normal. Une première différence entre ''thumb'' normal et ''Thumb-EE'' est l'ajout de ce qui s'appelle un ''null check'' pour les instructions mémoire. Le ''null check'' est utilisé pour les instructions en mode d'adressage "base + indice" ou "Base + décalage", ou tout autre mode d'adressage avec un registre de base utilisés dans des calculs d'adresse. L'idée est de vérifier si l'adresse de base vaut zéro ou non. Si c'est le cas, le processeur lève une exception matérielle, qui est traitée par l'interprétation ou le compilateur JIT. Une telle situation est en effet signe d'une erreur d'adressage mémoire, qui doit être traitée par l'interpréteur. Typiquement, c'est signe qu'un pointeur n'a pas bien été initialisé, ce qui peut arriver avec du code interprété ou JIT. Les interpréteurs/JIT traitent généralement la situation en ajoutant un test avant toute instruction mémoire, pour vérifier si le pointeur accédé vaut zéro ou non. Avec ''Thumb-EE'', pas besoin d'ajouter les tests en questions : ils sont réalisés automatiquement lors de chaque instruction mémoire. : Les registres pour la pile, comme le pointeur de pile et le pointeur de ''frame'', sont aussi concernés. L'instruction CHKA vérifie que les accès à un tableau ne débordent pas en dehors du tableau en question. Par exemple, pour un tableau de 1024 éléments, elle vérifie si l'indice est compris entre 0 et 1024. Pour cela, elle vérifie si l'indice de tableau est bien dans l'intervalle adéquat et lève une exception matérielle si ce n'est pas le cas. Son utilité se comprend quand on sait que tous les accès à un tableau sont vérifiés par l'interpréteur. L'interpréteur est censé ajouter des instructions pour vérifier les indices, à savoir deux ou trois branchements. Avec CHKA, le test se fait en une seule instruction. D'autres modifications mineures de l'encodage des instructions sont aussi présentes, ainsi que des modifications pour les instructions LOAD/STORE. Précisément, dans les modes "base + indice" et "base + décalage", le décalage et l'indice subissent maintenant des décalages. L'idée est de simplifier le calcul d'adresse. De quoi économiser quelques instructions lors des calculs d'adresse, ce qui facilite le travail de l'interpréteur/JIT. Et des instructions LOAD/STORE précises voient leur comportement modifié pour faciliter la gestion de la pile de Java ou l'exécution des méthodes locales. Pour faciliter le travail de l'interpréteur, deux instructions HBP et HBLP ont été ajoutées. Elles branchent vers une fonction qui gère les exceptions logicielles (une fonctionnalité présente dans de nombreux langages, comme Java). La première effectue un branchement vers la fonction en question, la seconde sauvegarde l'adresse de retour avant de faire ce branchement. Les deux peuvent brancher vers 256 fonctions pré-déterminées, sans avoir à présenter leur adresse. ==La traduction binaire accélérée par le matériel== Plus haut, nous avons surtout parlé des compilateurs et des interpréteurs. Cependant, nous devons aussi parler de la '''traduction binaire'''. Elle traduit un programme écrit dans un code machine vers un autre code machine. Par exemple, elle traduit un programme compilé pour un CPU x86 vers un code machine ARM. En général, le système d'exploitation est généralement compilé pour le jeu d'instruction natif, mais il exécute des applications prévues pour le x86. Les applications sont traduites par le système d'exploitation, avant d'exécuter le code traduit. : Nous parlerons dans la suite de code machine source et de code machine cible, pour parler respectivement du code à traduire et du code obtenu après traduction. Idem avec d'autres termes comme architecture cible/source, ou jeu d'instruction cible/source. La traduction binaire est surtout utilisée pour des questions d'émulation ou de compatibilité. Par exemple, lorsque les Macintosh sont passés de processeurs Power PC vers des processeurs x86, le système d'exploitation Mac OS utilisait la traduction binaire pour convertir les anciennes applications Power PC vers du code x86. Les utilisateurs n'y ont vu que du feu. Le système de traduction binaire était appelé Rosetta 1. Par la suite, lors de la transition de processeurs x86 vers des processeurs Apple, Rosetta 2 a vu le jour. La traduction binaire a surtout été utilisée pour traduire du code x86 vers un autre jeu d'instruction. Il faut dire que le x86 est le jeu d'instruction dominant. De nombreuses entreprises ont eu pour ambition de briser l'hégémonie du x86 sur PC, en remplaçant le x86 par un jeu d'instruction plus performant, tout en gardant une compatibilité maximale. La traduction binaire était la seule solution pratique. Elle était le plus souvent intégralement réalisée en logiciel, comme c'était le cas sur les architectures Itanium avec le ''IA-32 Execution Layer''. ===Les généralités sur la traduction binaire assistée en matériel=== L'architecture source est presque tout le temps une architecture CISC, assez ancienne, qu'on souhaite émuler. Le choix de l'architecture cible se porte souvent sur une architecture VLIW, fort différente de l'architecture source, et ce pour obtenir une économie de matériel conséquente. Pour s'exécuter rapidement, le code traduit doit exploiter le parallélisme d'instruction, à savoir exécuter plusieurs instructions en même temps dans des unités de calcul séparées. Il est possible d'utiliser un processeur superscalaire avec exécution dans le désordre pour cela, mais au prix d'un cout important en transistors. Alors qu'en utilisant un CPU VLIW, c'est le traducteur binaire qui fait tout le travail d'extraction du parallélisme d'instruction. Et après tout, quitte à avoir un traducteur binaire, autant lui refiler le boulot d'optimisation. Compiler du code à la volée est certes assez lourd, mais qu'il existe des algorithmes efficaces pour regrouper des instructions indépendantes dans une seule instruction VLIW. Au passage, le processeur VLIW a plus de registres que le processeur source. Cela permet de faire du renommage de registres directement en logiciel. Le traducteur binaire n'hésite pas à changer les noms de registres entre instructions source et instruction VLIW cible, afin de supprimer des dépendances de données. En conséquence, cela ouvre des opportunités de parallélisme, qui sont exploitées lors du regroupement des opérations en instructions VLIW. Vous pourriez penser que le choix d'un CPU VLIW pour émuler du CISC est tout sauf optimal. Vous devez penser que la traduction binaire est d'autant plus simple que l'architecture source et cibles sont semblables. Dans les faits, ce n'est pas tellement traduire les instructions qui pose problème, mais plus la gestion du registre d'état, des exceptions matérielles, de la mémoire virtuelle, la différence entre gros-boutisme et petit-boutisme, de même que des différences d'adressage pour les périphériques. Le premier problème est la gestion des conditions, notamment en présence d'un registre d'état. En pratique, la traduction binaire s'utilise pour traduire du code CISC vers du code RISC ou VLIW. Les architectures source ont donc un registre d'état, qui est mis à jour non seulement par des instructions de test, mais aussi des instructions arithmétiques. L'architecture cible n'a elle pas de registre d'état, mais des registres à prédicats. La traduction de l'un vers l'autre est alors quelque peu compliquée. Et elle est d'autant plus compliquée que le registre d'état est la source de dépendances d'instruction implicites, qui réduisent les performances. Par exemple, si une instruction arithmétique modifie le registre d'état, cela peut impacter l'exécution d'une instruction ultérieure, qui lit ce registre d'état. Pour éliminer ces fausses dépendances, le traducteur binaire doit renommer ce registre en logiciel et détecter les dépendances utiles. Et le compilateur doit gérer les cas où il y a beaucoup de distance entre les deux, voire les cas où l'instruction dépendante n'a pas encore été analysée par le compilateur. Un autre problème est lié à la gestion des exceptions matérielles, et précisément des exceptions précises. Pour rappel, une instruction VLIW regroupe plusieurs opérations, opérations qui correspondraient à une instruction machine sur un CPU pas VLIW. En conséquence, plusieurs instructions du langage machine "source" sont regroupées en une seule instruction VLIW. Et il faut tenir compte du cas où une instruction source lève une exception. Dans le code VLIW, cela signifie qu'une opération lève une exception, et il faut annuler partiellement l'instruction VLIW associée. Et par partiellement, on veut dire que seules les opérations suivantes dans l'ordre du programme source doivent être annulées, puis ré-exécutées. Et c'est un sacré casse-tête ! Les processeurs VLIW utilisent comme solution le mécanisme d'exceptions différées des processeurs EPIC, vu il y a quelques chapitres. Pour résumer, le code est exécuté par blocs d'instruction, délimités par des branchements ou tout autre limite/barrière pertinente dans le code. Un bloc de code est compilé en deux versions : une version rapide sans exceptions matérielles, une version lente qui gère les exceptions précises. La version rapide s'exécute sans exécuter les exceptions. Cependant, les exceptions matérielles sont enregistrées, pour être prises en compte à la toute fin du bloc de code. La version rapide est exécutée en premier et elle mémorise si une exception matérielle a eu lieu. Puis une instruction vérifie si une exception a eu lieu et décide quoi faire. Si aucune exception n'a eu lieu, elle passe à la suite du programme, les résultats du bloc de code sont définitivement acceptés. Mais si une exception a eu lieu, tout est annulé. Le processeur est remis dans l'état initial, puis le code est ré-exécuté instruction par instruction de manière à gérer l'exception correctement. ===La traduction par pré-décodage=== Une première solution serait de faire la traduction binaire dans le cache d'instruction. Lors d'un défaut de cache, le code chargé depuis la RAM est traduit en code machine cible. Et c'est ce code machine cible qui est mémorisé dans le cache d'instruction et exécuté par le processeur. La technique marche sur le papier et n'est qu'une amélioration des techniques de pré-décodage vues il y a quelques chapitres. Cependant, le code obtenu est une traduction assez basique, qui n'incorpore pas d'optimisations dignes de ce nom. De plus, elle gère mal le cas où la taille du code source et cible sont potentiellement très différentes. Concrètement, elle est surtout utile pour traduire du code d'un processeur RISC vers un autre processeur RISC, dont les tailles d'instructions sont similaires. Elle ne permet pas d'émuler plusieurs jeux d'instructions différents, le cout en matériel (un circuit de pré-décodage par jeu d'instruction) serait trop important. ===Le projet DAISY d'IBM=== De très rares processeurs étaient conçus pour accélérer cette traduction binaire, afin de garder de bonnes performances. Les premiers à avoir étudié l'idée étaient IBM, avec leur projet DAISY (''Dynamically Architected Instruction Set from Yorktown''). Le projet de base était de convertir à la volée du code compilé pour des CPU Power PC, vers du code VLIW. Les chercheurs d'IBM avaient développé un algorithme de traduction dynamique efficace, ainsi qu'un processeur VLIW disposant d'optimisations spécifiques à la traduction binaire dynamique, à la volée. De nombreuses idées de ce projet ont été reprises ou re-découvertes par la société Transmetta, avec ses processeurs Crusoe et Efficieon, puis par NVIDIA avec son projet Denver. Les ingénieurs de ce projet ont étudié la possibilité d'émuler plusieurs jeux d'instructions différents sur un même processeur, notamment le s390 d'IBM et le x86. En théorie, cela demande juste d'avoir plusieurs programmes de traduction binaire : un pour le x86, un autre pour le s390, éventuellement un autre pour le Power PC. Mais cela ne s'est pas concrétisé. Mais les ingénieurs ont étudié quelle pourrait être l'architecture VLIW idéale pour ça. Par exemple, il fallait des additions 3-opérandes pour simplifier les calculs d'adresse. Une difficulté était la gestion du registre d'état, dont les bits ne sont pas mis à jour de la même manière sur le x86 et le s390 ou le Power PC. La différence entre gros-boutisme et petit-boutisme était aussi un problème, de même que des différences d'adressage pour les périphériques. Pour que la traduction binaire soit efficace, le processeur VLIW intègre diverses optimisations, comme les exceptions différées et les branchements multi-voies. La principale est la suivante : seul le code des boucles ou fonctions exécutées fréquemment est traduit en code VLIW, le reste du code est interprété. En effet, traduire du code binaire prend plus de temps qu'une simple interprétation. Pour du code qui ne sera exécutée qu'une seule fois, il est plus rapide d'utiliser l'interprétation. Par contre, pour du code exécuté beaucoup de fois, le cout de la traduction binaire est amorti, dilué sur N exécutions, compensé par le gain en temps d'exécution de ce code traduit. Au final, cela permet de ne traduire que le code qui le mérite. La détection du code fréquemment exécuté est réalisée dans le cœur VLIW. L'unité de branchement mémorise les derniers branchements rencontrés et le nombre de fois qu'ils ont été exécutés. Si ils ont été exécutés un certain nombre de fois, l'unité de branchement lève une exception matérielle, qui invoque le traducteur binaire. La routine de cette exception est le traducteur binaire proprement dit. En clair, la traduction binaire est démarrée quand le processeur détecte qu'une fonction a été exécutée plus de N fois, via une exception matérielle dédiée. La mesure du nombre d'exécution d'un branchement se fait dans l'unité de calcul dédiée aux branchements, avec l'aide d'une sorte de ''branch adress buffer'' modifié. Pour rappel, le ''branch adress buffer'' mémorise le ''Program counter'' de chaque branchement récemment rencontré. Ici, chaque entrée du ''branch adress buffer'' est associée à un compteur incrémenté à chaque exécution du branchement. ===Les processeurs Crusoe et Efficieon de Transmetta=== Les processeurs Crusoe Et Efficeron sont deux processeurs VLIW produits par la société Transmetta, une société californienne rachetée par NVIDIA. Ils étaient conçus pour exécuter spécifiquement des programmes x86, système d'exploitation inclus. L'idée derrière ce projet était d'exécuter du code x86 sans que les applications, ni même le système d'exploitation et le BIOS soient au courant ! Les processeurs Transmetta étaient en réalité conçus pour exécuter un programme unique, le ''Code Morphing Software'' (CMS), qui traduisait le code x86 en code VLIW. Transmetta ne voulait pas que son processeur VLIW soit exploité directement, sans le CMS. Il n'avait pas rendu public de compilateur pour traduire du C vers du code VLIW, il n'avait pas donné la documentation du jeu d'instruction VLIW. Des efforts de rétro-ingénieurie, documentés sur le site realworldtech, ont cependant permis de comprendre comment étaient encodées les instructions du processeur Crusoe. Comme pour le projet Daisy, le ''Code Morphing Software'' utilisait à la fois interprétation et traduction binaire, selon les besoins. Les instructions sont interprétées lors de leurs premières exécutions, mais le CMS bascule sur de la traduction en code VLIW après un certain nombre d'exécution. Ainsi, les boucles souvent exécutées sont traduites en code VLIW, alors que le reste du code est interprété. Le choix du code à traduire est le fait du CMS, il utilise des heuristiques complexes pour, qui ne sont pas connues dans le détail. Le ''Code Morphing Software'' est mémorisé dans une EEPROM, ce qui en fait un ''firmware'' situé en dessous du BIOS. Le CMS démarre ensuite le BIOS, qui lui-même démarre le système d'exploitation, qui lui-même démarre les pilotes de périphériques et les programmes. Le CMS se réserve les 16 premiers mébioctets de l'espace d'adressage. L'EEPROM du CMS est mappée dedans, mais n'en utilise que la moitié. Le reste est utilisé comme cache, pour mémoriser le code VLIW traduit par le CMS. Les deux processeurs intégraient aussi deux mémoires SRAM, utilisées par le CMS, appelées la ''local program memory'' (LPM) et la ''local data memory'' (LDM). La première contient du code qui gére les interruptions, la mémoire virtuelle, les exceptions matérielles, les problèmes d'alignement mémoire, et quelques fonctions très fréquemment utilisées par le CMS. La seconde est de la mémoire RAM utilisée par ces fonctions de la ''local program memory''. Les mémoires caches L1 et L2 sont séparées de ces deux mémoires. Le processeur Crusoe et Efficieon étaient des processeurs VLIW très simples. Ils n'avaient même pas de MMU, ni de fonctionnalités importantes sur les CPU x86. Pour Crusoe, les instructions VLIW étaient encodées sur 64 ou 128 bits, et regroupaient entre 2 à 4 opérations. Les instructions LOAD/STORE ne supportaient pas d'adressage indicé, juste de l'adressage indirect à registre. Crusoe avait 5 unités de calcul : deux ALU entières, une unité LOAD/STORE, une unité de branchement, une FPU. Efficieon doublait le nombre d'ALU entière et d'unité LOAD/STORE, les instructions VLIW passaient à 256 bits. Le processeur Crusoe contient 160 registres, dont 64 registres généraux, 32 registres flottants. Les registres généraux font 32 bits, ce qui est cohérent avec le fait que les CPU x86 de l'époque étaient des processeurs 32 bits. Quant aux registres flottants, ils faisaient 80 bits, ce qui colle avec la taille des registres flottants de la FPU x87 utilisée à l'époque. Sur les 64 registres généraux, seuls 48 étaient réellement utilisable pour mémoriser des opérandes. Une partie des registres généraux étaient utilisés pour la gestion de la pile, d'autres pour mémoriser l'état du CPU x86 émulé, un registre était un registre zéro non accesible en écriture. La gestion des exceptions est optimisée avec un système d'exceptions différées, le même que celui décrit plus haut. Le processeur utilise un système similaire, pour effectuer des lectures anticipées, qui a été expliqué dans le chapitre sur les processeurs VLIW/EPIC. Sauf qu'il s'agit là d'une optimisation pour gagner en performances en général, pas une aide utile pour gérer des exceptions précises ou toute autre subtilité de la traduction binaire. Pour gérer les exceptions différées et les lectures anticipées, le processeur mémorise l'état du processeur avant de démarrer un bloc de code. Pour cela, le processeur contient des ''shadow registers'', 48 registres entiers et 16 registres flottants. Vous remarquerez qu'il y a autant de registres entiers que de ''shadow registers'' entiers. Les registres entiers/flottants sont mémorisés dans les ''shadow registers'' avant d'exécuter un bloc de code, afin que le processeur puisse revenir à l'état de base. Une autre fonctionnalité liée est le ''gated store buffer''. Le principe est d'éviter toute écriture en mémoire RAM, tant que le code VLIW n'a pas émis d'instruction ''commit''. L'idée est que les écritures sont conservées dans la ''store queue'' de l'unité LOAD/STORE. Pour rappel, la ''store queue'' met en attente les écritures soit tant que les écritures ne sont pas terminées, soit tant que la RAM est occupée. L'idée est que les écritures sont accumulées dans la ''store queue'' tant que l'instruction ''commit'' n'a pas eu lieu. Le ''store queue'' est "déversé dans la RAM" seulement quand l'instruction ''commit'' s'exécute. ===Les CPU NVIDIA Denver=== Nvidia a tenté quelque chose de similaire avec son projet Denver. NVIDIA a racheté l'entreprise Transmetta, ses brevets, et a tenté de refaire la même chose. Le projet initial était de traduire du code x86 vers un jeu d'instruction VLIW propriétaire, comme l'a fait Transmetta. Mais NVIDIA n'a pas réussi à acquérir la licence du jeu d'instruction x86 et s'est rabattu sur le jeu d'instruction ARM. Le processeur né de ce projet est le Tegra K1-64 CPU. Il s'agit d'un processeur composé d'un cœur VLIW, couplé à deux décodeurs d'instructions ARM. Les deux décodeurs ARM traduisent une paire d'instructions ARM consécutives en instructions VLIW, pour les exécuter sur le cœur VLIW. Le CPU peut fonctionner selon deux modes : ARM et VLIW. En somme, le processeur supporte deux jeux d'instruction : le jeu d'instruction ARM, et un jeu d'instruction VLIW. En mode ARM, les instructions ARM sont chargées depuis le cache, traduites en VLIW par les décodeurs ARM, puis exécutées par le cœur VLIW. En mode optimisé, le CPU exécute des instructions VLIW chargées depuis le cache d'instruction, les décodeurs ne sont pas utilisés. : Le CPU supporte en réalité deux jeux d'instruction ARM : ARM8 et 7. Mais ils correspondent au même mode pour le CPU. Un point important est que le processeur peut changer très rapidement de mode, en à peine quelques cycles d'horloges. On n'est clairement pas dans le cas des CPU x86, qui mettent des plombes pour passer du mode 32 à 64 bits et inversement. La commutation est tellement rapide qu'on peut considérer que le processeur supporte deux jeux d'instruction simultanément. Et le support simultané de deux jeux d'instruction facilite l'implémentation de la traduction binaire. Encore une fois, inutile de traduire du code qui ne sera exécutée qu'une seule fois, mieux vaut l'exécuter directement dans le mode ARM. Surtout que le processeur supporte un mode ARM qui permet d'exécuter du code ARM sans perte de performance. Par contre, le code fréquemment utilisé, notamment dans des boucles critiques, est traduit en VLIW. Au final, les performances sont optimisées, en limitant le travail du logiciel traducteur binaire. La détection du code fréquemment exécuté se fait différemment que pour les processeurs Transmetta et Daisy. L'unité de branchement ne détecte pas directement le code beaucoup exécuté, même si elle a un rôle à jouer. A la place, la détection est réalisée en logiciel, par un ''thread'' dédié, qui s'exécute sur un cœur séparé. Il détecte le code fréquemment utilisé en regardent les branchements exécutés récemment. Pour cela, le processeur mémorise l'historique des branchements pris récemment et copie cette historique dans une mémoire tampon dédiée, partagée entre tous les cœurs. Vu qu'elle est partagée, le ''thread'' de détection a accès à l'historique sans pertes de performances, seule la copie de l'historique dans ce tampon a un cout en performance, pas son partage. Le code VLIW obtenu après traduction binaire est mémorisé dans la mémoire RAM, dans une portion spécialement réservée pour. Elle fait 128 mébioctets, et est appelée le '''cache d'optimisation''' par NVIDIA. Le cache d'optimisation est protégé en écriture et seul le traducteur binaire peut écrire dedans. Au passage, le traducteur binaire est du code VLIW, ce qui fait qu'il est placé dans le cache d'optimisation. Le processeur détecte automatiquement quand une fonction pour laquelle le code compilé équivalent est disponible. Pour cela, l'unité de chargement contient une table de correspondance entre l'adresse de la fonction ARM, et l'adresse de son équivalent compilé. Quand une fonction est appelée, l'unité de chargement regarde l'adresse de destination du branchement. Si l'adresse est dans cette table, elle récupère l'adresse de la fonction compilée et branche vers celle-ci. Le processeur dispose d'un cache d'instruction de 128 kibioctets, ce qui est très important, mais nécessaire vu la taille des instructions VLIW. Le coeur VLIW dispose de ses propres décodeurs, d'un ''scoreboard'', de 7 unités de calcul et de registres. Pour les unités de calcul, il a deux FPU, deux ALU, une unité de branchement, et deux unités LOAD-STORE faisant aussi ALU. Un point important est que le CPU dispose de 64 registres entiers et de 64 registres flottants. C'est deux fois plus que supporte le jeu d'instruction ARM. En mode ARM, seule la moitié des registres est utilisée. Mais en mode optimisé, le traducteur binaire utilise bien les 64 registres, grâce à une sorte de renommage de registres logicielle. Pour simplifier la traduction binaire, le processeur supporte les techniques vues précédemment. Le processeur intègre un ''gated store buffer'' similaire à celui des processeurs Transmetta. Les techniques d'exceptions différées sont aussi supportées, comme sur les processeurs Transmetta. Il a aussi une unité de préchargement décrite par NVIDIA comme agressive, avec support du préchargement de type anticipé (''runahead''). ===L'émulation de la mémoire virtuelle=== Pour finir, il faut parler de la mémoire virtuelle avec la traduction binaire. Sur le processeur source, le processeur gère à la fois des adresses physiques et virtuelles et sa MMU fait la conversion entre les deux. Sur le processeur VLIW, la MMU est simulée par le programme de traduction binaire, partiellement ou totalement. L'implémentation exacte varie suivant que l'on parle du projet Daisy, des processeurs Transmetta ou de Denver. Sur les processeurs Transmetta, il n'y a pas de MMU ni de mémoire virtuelle. A la place, le programme de traduction binaire émule la mémoire virtuelle du processeur source. Le processeur VLIW ne gère que des adresses physiques, rien d'autre. L'espace d'adressage physique du processeur VLIW a la même taille que celui du processeur émulé, ici des processeurs x86. Il est vraisemblable que les adresses physiques utilisées par le processeur x86 sont les mêmes que celles du processeur VLIW. Sur le projet Daisy, le processeur VLIW gère la mémoire virtuelle via pagination, comme les processeurs émulés. Ce qui fait qu'il gère un espace d'adressage virtuel et un espace d'adressage physique. Pour éviter toute confusion, nous parlerons d'adresse physique/virtuelle VLIW pour les adresses physiques/virtuelles du processeur VLIW, d'adresse virtuelle/physique source pour celle du jeu d'instruction traduit, à savoir du Power PC ou du s390, éventuellement de l'x86. Il faut alors faire le lien entre adresses physiques source et adresses virtuelles VLIW. Pour cela, rien de plus simple : il y a correspondance parfaite. L'adresse physique source numéro N correspond à l'adresse virtuelle VLIW numéro N. Cependant, cela signifie que tout l'espace d'adressage virtuel VLIW serait utilisé par le code à traduire. En réalité, il faut ajouter de la place pour le code traduit et le programme de traduction binaire. La conséquence est que l'espace d'adressage virtuel VLIW est plus large que l'espace d'adressage source. L'espace d'adressage virtuel VLIW est découpé en trois sections : une section pour le code Power PC à traduire, une autre pour le code traduit en VLIW, et une dernière réservée au traducteur binaire. Le programme de traduction est placé dans une ROM mappée en mémoire dans la seconde section. Le reste de la seconde section est réservé à une mémoire RAM utilisée par le programme de traduction. [[File:Mémoire virtuelle sur le projet Daisy.png|centre|vignette|upright=1.5|Mémoire virtuelle sur le projet Daisy]] Il faut noter que chaque page physique source, une fois traduite, correspond à N pages virtuelles VLIW. La raison est que le code traduit prend plus de place que le code machine originel. En conséquence, on doit utiliser une page finale plus grande. Pour le reste, des pages contiguës en mémoire physique source sont elles aussi contiguës en mémoire virtuelle VLIW. Le calcul d'adresse est donc simple : l'adresse physique source est multipliée par N, puis on ajoute l'adresse de base à laquelle commence la section pour le code traduit. ==Les processeurs à jeu d'instruction reconfigurable== Nous venons de voir comment accélérer en matériel la traduction binaire. Mais il existe une alternative qui permet de se passer de traduction binaire, complétement. L'idée est d'avoir un processeur avec un '''jeu d'instruction reconfigurable'''. Le nom est assez parlent : on peut configurer le processeur de manière à supporter le jeu d'instruction de son choix ! Par exemple, on pouvait reconfigurer un CPU de manière à ce qu'il supporte le jeu d'instruction x86, ou qu'il exécute du ''bytecode'' Java, ou du code ARMv8, etc. De tels processeurs sont encore plus rare que ceux vu avant dans ce chapitre, mais ils ont existé ! Les processeurs de ce type avaient un '''microcode réinscriptible''', à savoir que c'était des processeurs microcodés pour lesquels on pouvait modifier le microcode. L'idée était qu'en changeant le microcode, on changeait le jeu d'instruction du processeur. Pour cela, le microcode était associé à une mémoire SRAM, en plus de la mémoire ROM du ''control store''. La SRAM était appelée la micro-SRAM, alors que le ''control store'' basique était appelé la micro-ROM. Le microcode était copié dans cette micro-SRAM au démarrage du CPU, puis les instructions étaient décodées en utilisant le microcode dans la micro-SRAM. En modifiant le contenu de la micro-SRAM, on pouvait altérer le microcode, voire le remplacer totalement. Il s'agit là de l'implémentation la plus simple. Mais le Burroughs B1700 a procédé autrement. ===Le burroughs B1700 : un ''split-level control store''=== Le burroughs B1700 lisait le microcode directement depuis la mémoire RAM ! Pour être précis, il avait plusieurs modèles : le B1726, le B1710 et le B1800. Le B1710 plaquait le microcode en mémoire RAM, ce qui fait que chaque exécution d'une instruction lisait les micro-opérations en mémoire RAM ! Le B1800 faisait pareil, mais avait ajouté un cache dédié au microcode, afin d'améliorer les performances. Le B1726 avait un microcode scindé en deux : une partie était en mémoire RAM, l'autre était dans un microcode réinscriptible classique, avec une micro-SRAM intégrée au processeur. Le processeur peut exécuter le microcode directement depuis la mémoire RAM, mais avec une pénalité en termes de performances. La documentation appelle cela un '''''split-level control store'''''. Le programmeur devait découper le microcode en segments, certains étant placé dans la micro-SRAM, les autres étant en mémoire RAM. Un tel microcode permettait de placer le microcode le plus fréquemment utilisé dans le processeur, alors que le reste du microcode était placé en mémoire RAM. Les instructions fréquentes étaient ainsi décodées avec la micro-SRAM, alors que les instructions peu fréquentes étaient décodées en lisant le microcode associé en mémoire RAM. La pénalité en termes de performance est alors concentrée sur les instructions les plus rares, ce qui est un très bon compromis. Mieux que ça, le processeur autorisait d'utiliser la technique de l'''overlaying'' pour le microcode ! Le microcode pouvait servir pour émuler un autre jeu d'instruction, mais il était aussi possible d'implémenter un véritable interpréteur, grâce aux techniques du paragraphe précédent. Le processeur était même conçu pour ! L'interpréteur était placé dans un "segment" en mémoire RAM, dont le processeur connaissait la position. Il incorporait pour cela deux registres, un pour l'adresse de base de l'interpréteur, un autre pour son adresse de fin. Il incorporait une protection mémoire limitée sur ce segment, à savoir que seul l'interpréteur pouvait lire ou écrire dans ce segment. Il faut noter que le ''split-level control store'' n'était disponible que sur le modèle B1726, pas sur le B1710 ou le B1800. Le B1700 lisait le microcode depuis la mémoire RAM, il n'avait pas de RAM dédiée pour le microcode réinscriptible ! Le B1800 faisait pareil, mais avait un cache dédié au microcode, afin d'avoir de meilleures performances. La micro-SRAM était mappée en mémoire RAM. Les premières adresses mémoire correspondaient à la micro-SRAM, le reste était des adresses en mémoire RAM. Typiquement, le processeur gérait un microcode de 2048 micro-opérations. La micro-SRAM mémorisait 512 micro-opérations, ce qui fait que les 512 premières "adresses" correspondaient à la micro-SRAM. Les suivantes étaient des adresses de mémoire RAM qui contenaient le reste du microcode. En pratique, l'adresse qui séparait les deux portions du microcode était configurable ! On pouvait limiter la taille du microcode en micro-SRAM à moins de 512 micro-opérations ! Les adresses restantes étaient alors réattribuées à la RAM. Par exemple, on pouvait attribuer 128 adresses à la micro-SRAM, les 2048 - 128 adresses restantes étaient alors attribuées à la mémoire RAM. Un registre limite, nommé TDPM, indiquait quelle était l'adresse de démarcation entre les deux. ===Le microcode réinscriptible=== Le microcode utilise des micro-opérations de 16 bits de long, ce qui est très peu et bien en-dessous des micro-opérations des autres processeurs. Les micro-opérations sortant du microcode sont envoyées au registre M, un registre de micro-opération de 16 bits. les micro-opérations écrites dans ce registre sont ensuite décodées par le hardware et exécutées. Fait intéressant, il était possible de faire un OU logique entre la micro-opération sortante et un registre du processeur, le résultat étant mémorisé dans le registre M. Cela permettait de faire du code automodifiant dans le microcode ! Utiliser du code automodifiant avec un microcode réinscriptible est parfaitement possible, et c'est utile pour émuler des modes d'adressage complexe dans le microcode. Cela permettait par exemple de remplacer une adresse dans une micro-opération. L'adresse est alors surimposée avec un OU dans le champ d'adresse, alors que le reste de l'instruction n'est pas modifié. Mais modifier directement le microcode est rarement une bonne idée. Aussi, pour éviter cela, l'idée est de modifier les instructions avec un OU logique avec un autre registre. Les micro-opérations du microcode ne sont donc pas modifiées dans le microcode, mais en sortie du microcode. Afin de gérer les interruptions, ces dernières étaient partiellement détectées en logiciel. Les interruptions ne s'exécutaient pas automatiquement, pour émuler correctement les interruptions. Une interruption doit être prise en compte à la toute fin de l'exécution d'une instruction, quand celle-ci a finit son travail. Mais vu qu'ici, les instructions sont émulées avec des séries de micro-opérations, il y a un problème : il est possible d'interrompre une instruction émulée en plein milieu. Pour éviter cela, le processeur ne déclenche pas les interruptions matérielles immédiatement, elles sont mises en attente. Une micro-opération dédiée vérifie si une interruption est en attente, et l'exécute alors cas échéant. La micro-opération est utilisée comme micro-opération finale d'une instruction émulée. ===Une architecture bit-adressable=== Si avoir un microcode réinscriptible est suffisant, quelques choix de ''design'' facilitent l'émulation de jeux d'instructions variés. Par exemple, le CPU Burroughs B1700 avait ajouté une fonctionnalité très importante : il était bit-adressable. En clair, il pouvait adresser la mémoire bit par bit, si nécessaire. L'avantage est que cela permettait d'émuler des architectures dont la taille des registres/''byte''/mots était très différente. Un autre avantage est que cela facilitait la gestion des instructions de taille variable. Détaillons rapidement le premier avantage. Le processeur était un CPU 24 bits, mais il pouvait émuler des processeurs 16 bits, 8 bits, 4 bits, 1 bit, 9 bits, ou toute autre valeur. Pour cela, le processeur lisait des mots de 24 bits et utilisant un ''barrel shifter'' pour sélectionner les bits adéquats. Par exemple, pour un accès de 16 bits, la donnée lue en mémoire était masquée de manière à ne garder que les 16 bits de poids faible. Le ''barrel shifter''/circuit de masquage était placé directement avant le bus mémoire, sur le trajet des données/instructions. Le ''barrel shifter'' était commandé par un registre ''Field Unit'', qui précisait quelle était la taille des données à charger. Le processeur gère un micro-''program counter'' appelé le registre A. Les micro-opérations du processeur font 16 bits de long, et elles sont alignées en RAM sur 16 bits, ce qui fait que les 4 bits de poids fort de ce registre valent 0. Le registre fait 18 bits, mais seuls les 14 bits de poids fort sont utiles. Les adresses mentionnées plus haut, qui adressent les micro-opérations en mémoire RAM, sont des adresses de mots de 16 bits, pas les adresses de bit individuels. ===Le Burroughs B1700 : architecture externe=== Un exemple est celui du Burroughs B1700, et il est intéressant d'étudier en détail son architecture. Son architecture est décrite dans le livre "Interpreting Machines : Architecture and Programming of the Bl700/Bl800 Series", écrit par Elliott I. Organick et James A. Hinds. Une partie de ce qui vfa suivre est un très court résumé de ce livre. Il disposait de 4 registres généraux, nommés X, Y, T et L, tous de 24 bits. Les registres T et L étaient découpés en 6 sous-registres de 4 bits, nommés LA LB LC LD LE et LF pour le registre L, et TA TB TC TD TE et TF pour le registre T. Ils étaient adressables comme n'importe quel registre. Un point important est que le registre T avait des possibilités d’extraction spécifiques. Il était possible d'extrait N bits, placés n'importe où dans ce registre, et de les copier dans un autre registre. Nous verrons pourquoi cette possibilité est intéressante et ne sert que pour le registre T. Les registres X et Y étaient reliés à une ALU de 24 bits capable d'effectuer des opérations arithmétiques basiques. Fait étonnant, l'ALU faisait tous les calculs en même temps et fournissait ses résultats dans 7 registres : un registre SUM pour l'addition, un registre DIFFERENCE pour la soustraction, et un registre par opération bit à bit. Les registres pour les opérations bit à bit sont : CMPX et CMPY pour l'opération NOT sur chaque opérande, XANY pour le ET bit à bit, XEOY pour le XOR, XORY pour le OU logique. Les additions et soustractions pouvaient se faire sur des opérandes codés en binaire ou en BCD, suivant le code opération utilisé. A cela, il fallait ajouter deux registres de "résultat" MSKX et MSKY. Ils fournissaient un masque dépendant de la taille du résultat en bits. Cela permettait de gérer des données de 0, 4, 8, 12, 16, 20, et 24 bits, alors que l'ALU faisait 24 bits. Et cela n'était pas limité à 16 ou 8 bits, mais n'importe quelle taille inférieure ou égale à 24 bits était supportée, même des tailles non-conventionnelles comme des résultats codés sur 3 bits, 9 bits, 12 bits, etc. L'ALU peenait en opérande les registres X et Y, mais aussi un registre CYF pour la retenue entrante. En sortie, il fournissait aussi deux retenue sortantes : une pour l'addition et une autre pour la soustraction. Elles étaient mémorisées dans les registres CYL et CYD, respectivement. L'ALU fournissait aussi 12 conditions, qui alimentait un registre d'état. Le registre d'état était segmenté en trois registres appelés XYST, XYCN et BICN. Le processeur contenait de nombreux autres registres de 4 bits, dont les sous-registres de T et L. En tout, il y a 27 registres de 4 bits adressables ! Et le processeur disposait d'une ALU 4 bits pour manipuler ces registres de 4 bits ! Elle est capable de faire des MOV entre registres de 4 bits, des opérations bit à bit, et peut aussi tester si un bit vaut 0 ou 1. La dernière possibilité est très utile pour implémenter les branchements. Pour communiquer avec les entrées-sorties, il disposait de deux registres CMND et DATA : CMND pour envoyer une commande à une entrée-sortie, DATA pour lire ou écrire une donnée. Pour la mémoire, il avait deux registres séparés : READ et WRITE. READ contient la donnée lue lors d'une lecture,; WRITE est pour une donnée à écrire lors d’une écriture. Il s'agit de registres d’interfaçage mémoire, qui ont été promus au rang de registres architecturaux. Les deux registres sont gérés par le microcode, qui est le langage principal de la machine. Les instructions machines configurées font sans, mais le microcode doit y avoir accès facilement. Et vu que le microcode est le langage principal de ce processeur, ces deux registres deviennent architecturaux. ===La gestion de la taille des instructions=== Les instructions à décoder n'ont pas de longueur fixe, à savoir qu'elles peuvent faire 16 bit avec tel jeu d'instruction, 8 bits avec un autre, 32 bits sur un autre, etc. Elles peuvent aussi être de longueur fixe ou variable ! Le B1700 intégrait donc un support des instructions de longueur variable, qui était "détourné" pour gérer des jeux d'instruction différents. Notons que vu que l'architecture est bit-adressable, les contraintes d'alignement des instructions disparaissent, ce qui a des avantages qu'on va détailler dans ce qui suit ! L'implémentation des instructions de taille variable ajoutait un registre avant le registre d'instruction. Le registre d'instruction proprement dit était un registre M de 24 bits. Il était précédé par un registre T, dans lequel l'instruction était assemblée, en combinant plusieurs morceaux. Si l'instruction est codée est codée sur moins de 24 bits, le registre T se contente de masquer les bits en trop. Si l'instruction chargée dépasser 24 bits, le chargement se faisait en plusieurs passes, avec plusieurs lectures en mémoire. Dans ce qui suit, on suppose que l'instruction est composée de '''champs''' : l'opcode, les opérandes, les adresses absolues, les constantes immédiates, les numéros de registre, etc. L'idée est que l'instruction est chargée dans le registre T, puis découpée en champs, qui sont copiés dans les registres du processeur ou envoyés au microcode. Je rappelle que le registre T est relié à un ''barrel shifter'', différent au précédent, qui permettait de masquer, décaler et extraire une donnée de ce registre. Et il y a la possibilité d'extraire une suite de bit de ce registre T, pour la copier dans un autre registre. Par exemple, prenons une instruction composée d'un opcode, d'une constante immédiate et d'une adresse immédiate. L'opcode est lu et interprété par le microcode. Le microcode extrait la constante immédiate du registre T et la copie dans le registre X. Ensuite, l'adresse est extraite et envoyée au registre FA, une lecture est démarrée, et l'opérande est copiée dans le registre Y. Enfin, l'instruction est exécutée par le microcode. Le tout prend plusieurs micro-opérations : une première pour lire l'opcode, une seconde pour lire la première opérande, une troisième pour lire la seconde opérande, una quatrième pour tout exécuter. Notons que suivant la taille des champs, on peut en charger plusieurs à la fois. Par exemple, imaginons qu'on ait chargé 24 bits, contenant un opcode de 8 bits et une constante immédiate de 16 bits : pas besoin de faire deux accès mémoire, le registre T est juste découpé en un opcode et une constante. Le registre T est fait pour ce genre d'extraction. ===Le chargement d'un champ=== L'architecture est bit-adressable car elle gére des champs qui ne font pas forcément 8 ou 16 bits. Par exemple, certains jeux d'instructions ont des opcodes codés sur 6 ou 7 bits, voire 9-10 bits. Par exemple, prenons un opcode de 7 bits, suivi par une adresse de 12 bits et une constante immédiate de 15 bits : aucun de ces champs ne sera aligné sur 8/16/24 bits. Utiliser une architecture bit-adressable règle le problème d'alignement. Et en général, les contraintes d'alignement des instructions disparaissent ! Quand on charge une instruction, peut importe qu'elle soit ou non alignée sur 24 bits, elle sera placée dans les bits de poids faible, il suffira de masquer les bits de poids fort inutiles. Par exemple, si on charge une instruction de 16 bits, les 16 bits seront copiés dans les 16 bits de poids faible du registre T, il suffira de masquer l'octet de poids fort. Pas besoin de faire de décalages pour gérer des instructions mal alignées... Pour charger une instruction, il faut connaitre plusieurs informations : l'adresse de son premier bit, sa taille. Pour cela, le B1700 contient deux registres, nommés ''Field Adress'' (FA) et ''Field Length'' (FL), qui indiquent l'adresse du premier bit et la taille de l'instruction à charger. Le premier fait 24 bits, ce qui permet de gérer des adresses de 24 bits. N'oubliez pas que le processeur est bit-adressable, ce qui permet d'adresser 16 Mébi-bits, soit 2 mébioctets. Le registre pour la taille fait lui 16 bits, ce qui permet de gérer des instructions de 65536 bits, soit 8192 octets ! Leur nom commence par ''field'', car le processeur peut gérer des instructions de taille variable et/ou plus longues que 24 bits. Dans ce cas, le chargement de l'instruction se fait champ par champ. Pour donner un exemple, prenons une instruction composée de 20 champs de 12 bits chacun. Les champs sont chargés un par un dans le registre T. Le registre FA est alors initialisé avec l'adresse de l'instruction, il est incrémenté de 12 à chaque cycle. Le registre FL est initialisé avec la taille de l'instruction, soit 20 * 12 = 240 bits, et il est décrémenté de 16 à chaque cycle. Précisons cependant que la taille d'un champ peut être changée d'un cycle à l'autre. Par exemple, le processeur peut charger un opcode de 8 bits, l'interpréter, détecter que l'instruction demande ensuite de charger une adresse de 16 bits. Dans ce cas, le registre FL est immédiatement altéré pour charger 16 bits, au lieu des 8 bits de l'opcode immédiatement précédent. ==Les jeux d'instructions dédiés à un langage de programmation== De rares processeurs sont conçus pour un langage de programmation en particulier. On appelle ces processeurs, conçus pour des besoins particuliers, des '''processeurs dédiés'''. Par exemple, les fameux ''Burrough E-mode'' B5000/B6000/B7000 étaient spécialement conçus pour exécuter de l'ALGOL-60. Leurs cousins B2000/B3000/B4000 étaient eux conçus pour le COBOL. Des langages fonctionnels ont aussi eu droit à leurs processeurs dédiés. Le prolog en est un bel exemple, avec les superordinateurs de 5ème génération qui lui étaient dédié. On peut aussi citer les machines LISP, dédiés au langage LISP, qui datent des années 1970. Elles étaient capables d’exécuter certaines fonctions de base du langage directement dans leurs circuits : elles possédaient notamment un ''garbage collector'' câblé dans ses circuits ainsi que des instructions machines supportant un typage déterminé à l’exécution. Les processeurs dédiés ont eu leur heure de gloire au début de l'informatique, à une époque où les langages de haut niveau venaient d'être inventés. À cette époque, les compilateurs n'étaient pas performants et ne savaient pas bien optimiser le code machine. Il était alors rationnel, pour l'époque, de rapprocher le code machine cible et le langage de programmation de haut niveau. De nombreuses architectures dédiés ont ainsi été inventées, avant que les concepteurs se rendent compte des défauts de cette approche. Les défauts en question ne sont pas nombreux, mais assez simples à comprendre. Premièrement, elles sont très rapides pour un langage de programmation en particulier, mais sont assez mauvaises pour les autres, d'où un problème de "compatibilité". Ajoutons à cela que les langages de programmation peuvent évoluer, devenir de moins en moins populaires/utilisés, ce qui rend la création d'architectures généralistes plus pertinente. Enfin, les architectures dédiées sont évidemment des processeurs CISC, pour implémenter les nombreuses fonctionnalités des langages évolués. Et les défauts des CISC sont assez rédhibitoires à l'heure actuelle. À l'heure actuelle, les algorithmes des compilateurs se sont améliorés et savent nettement mieux utiliser le matériel. Ils produisent du code machine efficace, ce qui rend les architecture dédiées bien moins intéressantes. Si on ajoute les défauts de ces architectures dédiées, par étonnant que les architectures dédiées aient presque disparues. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'accélération matérielle de la virtualisation | prevText=L'accélération matérielle de la virtualisation | next=Le matériel réseau | nextText=Le matériel réseau }} </noinclude> 9py38hs7x1tfl97nui4wcg5aog6i3sd 764034 764033 2026-04-19T19:14:04Z Mewtow 31375 /* Le burroughs B1700 : un split-level control store */ 764034 wikitext text/x-wiki De nos jours, la majorité des programmeurs programment dans des langages de haut niveau. Il est très rare pour eux d'avoir à utiliser de l'assembleur, et encore moins un langage machine. Les programmes écrits dans un langage de haut niveau sont traduits en langage machine par un logiciel appelé le compilateur. [[File:Compilation.PNG|centre|vignette|upright=2.5|Compilateur : principe.]] Il se trouve que quelques rares processeurs sont conçus pour faciliter le travail du compilateur. Et ce sont ces processeurs que nous allons voir dans ce qui suit. Ils représentent une catégorie de jeux d'instruction à part, qui n'a pas vraiment de nom. ==La compilation et l’interprétation== Un compilateur traduit donc du code source, écrit dans un langage de haut niveau, vers du langage machine. La traduction est rarement directe. En général, le compilateur traduit le code source en assembleur, qui est lui-même traduit en langage machine. L'assembleur est une représentation textuelle du langage machine alors que le code machine est du binaire exécutable par le processeur. La traduction se fait donc en deux étapes, la '''compilation''' proprement dite et l''''assemblage''', réalisées respectivement par un compilateur et un assembleur. Il faut parfois rajouter une troisième phase d'édition des liens, que nous passons volontairement sous silence. {| |[[File:Code C.png|vignette|200px|Le code source, ici en C.]] |[[File:Assem.png|vignette|250px|Le code assembleur.]] |[[File:Binary file - hello world (C programming).png|vignette|150px|Le code machine.]] |} ===L'interprétation et la compilation à la volée=== Une alternative à la compilation est l''''interprétation''', qui transforme le code source en code machine à la volée. Avec l'interprétation, le code source est passé à un logiciel appelé l''''interpréteur''', qui exécute le code source ligne de code par ligne de code. Une ligne de code est traduite en code machine, qui est exécutée, puis l'interpréteur passe à la ligne suivante. [[File:Kaantotulkkaus.png|centre|vignette|upright=3|Compilation versus interprétation.]] Un défaut de l'interprétation est son cout en performance, qui est assez important. Aussi, l'interprétation stricte a évolué vers un hybride entre interprétation et compilation. L'idée est qu'une partie peu importante du code source est interprétée, alors que le code important est compilé juste avant d'être exécuté. Pour le dire autrement, le code source est partiellement compilé à la volée, juste avant son exécution. Aussi, on parle de '''compilation à la volée'''. Le terme en anglais est ''Just In Time compilation'', abrévié en JIT. En soi, le JIT est associé à de l'interprétation, les JIT sont en réalité des hybrides interpréteurs-compilateurs. Un point important est que compiler du code à la volée est assez lourd, cela a un cout en performance. Par contre, le code compilé s'exécute plus vite que du code interprété. Si on exécute le code compilé une seule fois, le cout de la compilation l'emporte sur le gain à l'exécution. Mais le code compilé est réutilisé autant de fois que nécessaire. Le gain à l'exécution est donc multiplié par le nombre d'exécutions, alors que le cout de la compilation est répartit sur plusieurs exécutions, il est amorti. Les portions du code sont choisies de manière à avoir un gain en performance maximal. L'idée générale est que le code exécuté très souvent est compilé, alors que du code exécuté pas souvent est interprété. Typiquement, les boucles les plus souvent exécutées sont compilées, le code exécuté une seule fois est interprété. ===Les langages intermédiaires=== Les compilateurs modernes passent par un '''langage intermédiaire''' pour faire la transformation en code machine. Le compilateur traduit le langage de haut niveau en langage intermédiaire, puis traduit le langage intermédiaire en code machine cible. Le langage intermédiaire est parfois appelé le '''''bytecode''''', et ce terme recouvre aussi le code écrit avec du ''bytecode''. L'interprétation aussi peut passer par l'intermédiaire d'un un ''bytecode'', ce n'est pas limité aux compilateurs proprement dit. {| |[[File:Intermediate representation scheme.png|centre|vignette|upright=1.5|Représentation intermédiaire d'un compilateur.]] |[[File:Code interpreter scheme.png|centre|vignette|upright=1.5|Fonctionnement d'un interpréteur.]] |} Faire ainsi a de nombreux avantages, le principal étant d'avoir un compilateur capable de traduire un langage de haut niveau vers plusieurs jeux d'instructions différents. Cela permet d'avoir, par exemple, un compilateur qui traduit du C soit en code machine x86, soit en code machine pour un CPU ARM, soit pour un CPU POWERPC, etc. [[File:Compiler design.svg|centre|vignette|upright=3|Compiler design]] Pour l'interprétation, l'usage d'un ''bytecode'' a d'autres avantages. Pour l'exploiter, il faut distribuer non pas le code source, mais le ''bytecode''. En clair, l'interpréteur ne prend pas en entrée le code source, mais du ''bytecode'' déjà compilé. La traduction est alors beaucoup plus simple, car le code source a déjà été partiellement compilé, par la traduction en ''bytecode''. Interpréter du code source est en effet assez compliqué : il faut effectuer des étapes d'analyse lexicale, sémantique, et bien d'autres. Avec du ''bytecode'', ces étapes ont été réalisées lors de la compilation du ''bytecode'', l'interpréteur a alors peut de choses à faire. L'interprétation du ''bytecode'' se fait instruction par instruction, au niveau du ''bytecode''. Une instruction du ''bytecode'' est traduite en une instruction machine équivalente, qui est exécutée. Si l'instruction est un peu complexe, il exécute une fonction/procédure qui fait la même chose. Pour résumer, le ''bytecode'' est ensuite traduit à la volée et exécuté instruction par instruction par un logiciel appelé l'interpréteur. [[File:Control table.png|centre|vignette|upright=2|Control table]] Le langage intermédiaire peut être vu comme l'assembleur d'un processeur, n'existe pas forcément dans la réalité, mais dont le jeu d'instruction est décrit en détail. Le processeur en question est appelé une '''machine abstraite''', ou encore une ''machine virtuelle''. Nous utiliserons le terme de machine abstraite dans ce qui suit. La machine abstraite n'est pas la même suivant que l'on cible la compilation ou l'interprétation/JIT. Pour l'interprétation/JIT, la machine abstraite est souvent une machine à pile, car cela simplifie grandement la traduction en code machine final. L'interprétation demande que la traduction du ''bytecode'' en code machine soit la plus simple possible. Et cela demande de prendre en compte pas mal de détails. Par exemple, les différents jeux d'instruction existants n'ont pas le même nombre de registres, ce qui pose problème lors de la traduction du ''bytecode''. Lors de la transformation en code machine, un algorithme d'allocation de registres se débrouille pour traduire le code intermédiaire en code qui utilise un nombre limité de registres. Pour améliorer cette allocation de registres, il y a deux solutions niveau ''bytecode'' : soit utiliser un nombre illimité de registres, soit utiliser une machine à pile. Pour les machines à pile, il existe un algorithme simple et rapide pour traduire un code écrit pour une machine à pile en un code écrit pour un processeur avec des registres, qui se débrouille pas trop mal pour allouer efficacement les registres. C'est un avantage assez important pour les langages interprétés. ==Les processeurs qui exécutent du bytecode== Le ''bytecode'' est un code machine, ce qui signifie qu'il peut en théorie s'exécuter sur un processeur qui implémente le jeu d’instruction associé. Si le jeu d'instruction d'un ''bytecode'' est souvent une description censée être fictive, elle n'en reste pas moins un jeu d'instruction et des caractéristiques précises, qu'on peut l'implémenter en matériel ! Néanmoins, tous les ''bytecode'' ne sont pas égaux de ce point de vue. Certains sont faciles à implémenter en matériel, d'autres non. Les ''bytecodes'' implémentés en matériel sont ceux dont les machines abstraites sont des machines à pile. Un premier exemple est celui des processeurs Pascal MicroEngine, qui exécutaient directement le bytecode du langage Pascal, le fameux UCSD P-code. Un second exemple est la '''machine SECD''', qui sert de langage intermédiaire pour certains compilateurs de langages fonctionnels. Elle a été implémentée en matériel par plusieurs équipes, notamment par les chercheurs de l'université de Calgary en 1989. Dans le même genre, quelques processeurs simples étaient capables d’exécuter directement le ''bytecode'' du langage FORTH. Le FORTH, un des premiers langages à pile de haut niveau, possède de nombreuses implémentations hardware et est un des rares langages de haut niveau dont le ''bytecode'' a été utilisé comme langage machine sur certains processeurs. Par exemple, on peut citer le processeur FC16, capable d’exécuter nativement du ''bytecode'' FORTH. ===Les processeurs Java=== Le cas le plus impressionnant est celui de la machine virtuelle Java, qui est un design de processeur comme un autre. En temps normal, le ''bytecode'' Java est compilé ou interprété, mais certains processeurs exécutaient du ''bytecode'' Java directement, sans interprétation ni compilation. Ils sont appelés des '''processeurs Java'''. On peut citer les processeurs ARM disposant de l'extension Jazelle, du JEMCore et du aJ-100 de aJile Systems, le picoJava II de Sun, et quelques autres. Il y a aussi eu quelques projets de recherche ou processeurs open source, comme Komodo, jamuth, le ''Java Optimized Processor'' et quelques autres projets du même genre. L'implémentation de la machine virtuelle Java n'était cependant pas complète. Quelques instructions complexes n'étaient pas gérées par le processeur et devaient être émulées en logiciel. Mais la grosse majorité des instructions l'était. Les instructions non-supportées étaient émulées avec la méthode ''trap and emulate'', à savoir qu'un opcode inconnu déclenchait une exception matérielle ''opcode invalide'', dont la routine pouvait émuler les instructions qui doivent l'être. Le processeur aJ-100 de aJile est capable d'exécuter la quasi-totalité des instructions de la JVM Java, seules deux instructions faisant exception. Les '''accélérateurs Java''' sont une solution intermédiaire, qui permet de traduire à la volée du ''bytecode'' Java en instructions RISC, exécutées sur un autre processeur. Il s'agit formellement de traduction binaire, mais je préfère en parler ici. L'idée est de combiner un processeur RISC avec un coprocesseur Java. Le coprocesseur lit le ''bytecode'' Java et le traduit en instructions RISC, ces dernières étant exécutées sur le processeur RISC. Le JA108 de Nozomi était un coprocesseur de ce genre, au même titre que le coprocesseur JSTAR de JEDI. L'extension Jazelle de certains CPU ARM fonctionnait vraisemblablement sur un principe similaire, sauf que le coprocesseur était intégré dans le processeur ARM, entre le cache d'instruction et le décodeur d'instructions ARM. L'intérêt des processeurs Java n'est pas qu'une question de performance. Certes, ils permettent d’exécuter plus vite les programmes compilés en ''bytecode'', comme des programmes Java pour la JVM Java. Mais l'intérêt est aussi de faire des économies de mémoire RAM et ROM. Je rappelle que la machine virtuelle Java est une machine à pile, ce qui fait que sa densité de code est excellente. Les programmes en ''bytecode'' Java sont donc très petits, et prennent peu de place en mémoire ROM. De plus, utiliser un processeur Java permet de se passer d'interpréteur ou de JIT Java, ce qui économise encore plus de mémoire. ===Transformer un processeur RISC en machine à pile=== La plupart des processeurs Java utilisent en interne une architecture à registre généraux, qui émule une machine à registre en microcode. Quelques registres sont réservés pour l'état de la machine virtuelle, avec au minimum un registre réservé pour le pointeur de pile, un autre pour un pointeur vers un ''pool'' de constantes, et quelques autres. D'autres registres servent à mémoriser le haut de la pile, à savoir le sommet de la pile et quelques opérandes situées juste en-dessous. Par exemple, le processeur picoJava II gardait les 64 opérandes au sommet de la pile dans 64 registres généraux, séparés des registres pour les pointeurs et l'état de la JVM. Une optimisation possible, envisagée sur les processeurs picoJava, était une sorte de macro-fusion sous stéroïde. L'idée était de fusionner certaines séries d'instructions Java en une seule instruction machine. Typiquement, pour faire une opération arithmétique, une machine à pile empile deux opérandes et exécute une opération, ce qui prend trois instructions machine. La spécification picoJava 1 a proposé d'envoyer directement la seconde opérande en entrée de l'ALU, ce qui fusionnait l'empilement de la seconde opérande et l'opération arithmétique. Le processeur picoJava II va plus loin et fusionne les trois instructions en une seule instruction RISC, agissant sur des registres. Mais la majorité des processeurs Java ne faisaient pas cela, car cela complexifie grandement le décodeur. Les processeurs ''bigfoot'' de DCT utilisaient un système différent pour émuler une pile avec des registres. Leur banc de registre de 64 bits était coupé en deux : les 16 premiers registres étaient utilisés pour la pile, les 48 suivants étaient utilisés pour autre chose. L'idée est que le registre 0 mémorise un emplacement vide, le registre 1 contient le sommet de la pile, le registre 2 l'opérande sous le sommet de la pile, et ainsi de suite. Les 15 premières opérandes de la pile sont donc mémorisées dans les registres. Les instructions écrivent leur résultat dans le registre adéquat, puis tous les registres sont décalés pour que le résultat passe dans le registre R1. Par exemple, les instructions PUSH chargent une opérande dans le registre 0, puis tous les registres sont décalés de un rang, pour que l'opérande chargée soit dans le registre 1. Le registre 0 devient alors le registre numéro 1, le registre 1 devient le 2, et ainsi de suite. Par contre, les opérations arithmétiques font autrement. Une instruction d'addition, par exemple, lit les opérandes dans le registre 1 et 2, puis place son résultat dans le registre 2 (les deux opérandes sont dépilées). Puis, les registres sont décalés d'un rang pour que ce registre 2 devienne le 1. Les registres ne sont en réalité pas décalés, il n'y a pas de transferts entre registres. A la place, les 16 premiers registres utilisaient une forme limitée de renommage de registres, similaire au banc de registre tournant utilisé sur les processeurs EPIC comme l'Itanium. Les numéros de registres sont renommés, décalés, à chaque fois qu'on empile ou dépile une opérande. Pour faire le renommage de registres, processeur contient un ''register stack counter'' de 4 bits, qui est incrémenté à chaque fois qu'on empile une donnée, et est décrémenté quand on dépile une donnée. Quand on veut accéder à un opérande, le numéro de registre architectural associé est traduit en numéro de registre physique, en additionnant le ''register stack counter'' (modulo 16, pour rester sur 4 bits). ==L'accélération de l'interprétation/JIT : le ''Thumb-EE'' d'ARM== Quelques processeurs ont ajouté des instructions pour faciliter le travail des interpréteurs/JIT. Par exemple, en 2005, ARM a ajouté le mode ''Thumb Execution Environment'', qui faisait cela. Il reprenait le jeu d'instruction compact ''thumb'' et ajoutait quelques instructions et modifiait le comportement d'instructions existantes. Le jeu d'instruction ''Thumb-EE'' a été ajouté sur les CPU ARM en 2005, mais a été déprécié en 2011, par manque d'utilité. Mais la tentative mérite qu'on s'attarde dessus. Les CPU de gamme M, déstinés à l'embarqué, n'ont jamais supporté ''Thumb-EE'', vu que de tels processeurs ne sont pas conçus pour exécuter du code interprété/JIT. Le mode ''Thumb-EE'' est un mode d'exécution, séparé de l'ARM normal. Ainsi, une instruction ''Thumb-EE'' et une instruction ARM peuvent avoir le même encodage en binaire, mais se comporter différemment. Le processeur est à tout moment soit en mode ''Thumb-EE'', soit en mode ARM normal, ce qui précise comment ces instructions doivent se comporter. Des instructions ont été ajoutées par ''Thumb-EE'', pour qu'il fasse son travail. L'entrée et la sortie du mode ''Thumb-EE'' se fait avec deux instructions : ENTERX et LEAVEX. Une fois en mode ''Thumb-EE'', le décodage des instructions ''thumb'' se fait avec les règles du ''Thumb-EE'', et non celles du ''thumb'' normal. Une première différence entre ''thumb'' normal et ''Thumb-EE'' est l'ajout de ce qui s'appelle un ''null check'' pour les instructions mémoire. Le ''null check'' est utilisé pour les instructions en mode d'adressage "base + indice" ou "Base + décalage", ou tout autre mode d'adressage avec un registre de base utilisés dans des calculs d'adresse. L'idée est de vérifier si l'adresse de base vaut zéro ou non. Si c'est le cas, le processeur lève une exception matérielle, qui est traitée par l'interprétation ou le compilateur JIT. Une telle situation est en effet signe d'une erreur d'adressage mémoire, qui doit être traitée par l'interpréteur. Typiquement, c'est signe qu'un pointeur n'a pas bien été initialisé, ce qui peut arriver avec du code interprété ou JIT. Les interpréteurs/JIT traitent généralement la situation en ajoutant un test avant toute instruction mémoire, pour vérifier si le pointeur accédé vaut zéro ou non. Avec ''Thumb-EE'', pas besoin d'ajouter les tests en questions : ils sont réalisés automatiquement lors de chaque instruction mémoire. : Les registres pour la pile, comme le pointeur de pile et le pointeur de ''frame'', sont aussi concernés. L'instruction CHKA vérifie que les accès à un tableau ne débordent pas en dehors du tableau en question. Par exemple, pour un tableau de 1024 éléments, elle vérifie si l'indice est compris entre 0 et 1024. Pour cela, elle vérifie si l'indice de tableau est bien dans l'intervalle adéquat et lève une exception matérielle si ce n'est pas le cas. Son utilité se comprend quand on sait que tous les accès à un tableau sont vérifiés par l'interpréteur. L'interpréteur est censé ajouter des instructions pour vérifier les indices, à savoir deux ou trois branchements. Avec CHKA, le test se fait en une seule instruction. D'autres modifications mineures de l'encodage des instructions sont aussi présentes, ainsi que des modifications pour les instructions LOAD/STORE. Précisément, dans les modes "base + indice" et "base + décalage", le décalage et l'indice subissent maintenant des décalages. L'idée est de simplifier le calcul d'adresse. De quoi économiser quelques instructions lors des calculs d'adresse, ce qui facilite le travail de l'interpréteur/JIT. Et des instructions LOAD/STORE précises voient leur comportement modifié pour faciliter la gestion de la pile de Java ou l'exécution des méthodes locales. Pour faciliter le travail de l'interpréteur, deux instructions HBP et HBLP ont été ajoutées. Elles branchent vers une fonction qui gère les exceptions logicielles (une fonctionnalité présente dans de nombreux langages, comme Java). La première effectue un branchement vers la fonction en question, la seconde sauvegarde l'adresse de retour avant de faire ce branchement. Les deux peuvent brancher vers 256 fonctions pré-déterminées, sans avoir à présenter leur adresse. ==La traduction binaire accélérée par le matériel== Plus haut, nous avons surtout parlé des compilateurs et des interpréteurs. Cependant, nous devons aussi parler de la '''traduction binaire'''. Elle traduit un programme écrit dans un code machine vers un autre code machine. Par exemple, elle traduit un programme compilé pour un CPU x86 vers un code machine ARM. En général, le système d'exploitation est généralement compilé pour le jeu d'instruction natif, mais il exécute des applications prévues pour le x86. Les applications sont traduites par le système d'exploitation, avant d'exécuter le code traduit. : Nous parlerons dans la suite de code machine source et de code machine cible, pour parler respectivement du code à traduire et du code obtenu après traduction. Idem avec d'autres termes comme architecture cible/source, ou jeu d'instruction cible/source. La traduction binaire est surtout utilisée pour des questions d'émulation ou de compatibilité. Par exemple, lorsque les Macintosh sont passés de processeurs Power PC vers des processeurs x86, le système d'exploitation Mac OS utilisait la traduction binaire pour convertir les anciennes applications Power PC vers du code x86. Les utilisateurs n'y ont vu que du feu. Le système de traduction binaire était appelé Rosetta 1. Par la suite, lors de la transition de processeurs x86 vers des processeurs Apple, Rosetta 2 a vu le jour. La traduction binaire a surtout été utilisée pour traduire du code x86 vers un autre jeu d'instruction. Il faut dire que le x86 est le jeu d'instruction dominant. De nombreuses entreprises ont eu pour ambition de briser l'hégémonie du x86 sur PC, en remplaçant le x86 par un jeu d'instruction plus performant, tout en gardant une compatibilité maximale. La traduction binaire était la seule solution pratique. Elle était le plus souvent intégralement réalisée en logiciel, comme c'était le cas sur les architectures Itanium avec le ''IA-32 Execution Layer''. ===Les généralités sur la traduction binaire assistée en matériel=== L'architecture source est presque tout le temps une architecture CISC, assez ancienne, qu'on souhaite émuler. Le choix de l'architecture cible se porte souvent sur une architecture VLIW, fort différente de l'architecture source, et ce pour obtenir une économie de matériel conséquente. Pour s'exécuter rapidement, le code traduit doit exploiter le parallélisme d'instruction, à savoir exécuter plusieurs instructions en même temps dans des unités de calcul séparées. Il est possible d'utiliser un processeur superscalaire avec exécution dans le désordre pour cela, mais au prix d'un cout important en transistors. Alors qu'en utilisant un CPU VLIW, c'est le traducteur binaire qui fait tout le travail d'extraction du parallélisme d'instruction. Et après tout, quitte à avoir un traducteur binaire, autant lui refiler le boulot d'optimisation. Compiler du code à la volée est certes assez lourd, mais qu'il existe des algorithmes efficaces pour regrouper des instructions indépendantes dans une seule instruction VLIW. Au passage, le processeur VLIW a plus de registres que le processeur source. Cela permet de faire du renommage de registres directement en logiciel. Le traducteur binaire n'hésite pas à changer les noms de registres entre instructions source et instruction VLIW cible, afin de supprimer des dépendances de données. En conséquence, cela ouvre des opportunités de parallélisme, qui sont exploitées lors du regroupement des opérations en instructions VLIW. Vous pourriez penser que le choix d'un CPU VLIW pour émuler du CISC est tout sauf optimal. Vous devez penser que la traduction binaire est d'autant plus simple que l'architecture source et cibles sont semblables. Dans les faits, ce n'est pas tellement traduire les instructions qui pose problème, mais plus la gestion du registre d'état, des exceptions matérielles, de la mémoire virtuelle, la différence entre gros-boutisme et petit-boutisme, de même que des différences d'adressage pour les périphériques. Le premier problème est la gestion des conditions, notamment en présence d'un registre d'état. En pratique, la traduction binaire s'utilise pour traduire du code CISC vers du code RISC ou VLIW. Les architectures source ont donc un registre d'état, qui est mis à jour non seulement par des instructions de test, mais aussi des instructions arithmétiques. L'architecture cible n'a elle pas de registre d'état, mais des registres à prédicats. La traduction de l'un vers l'autre est alors quelque peu compliquée. Et elle est d'autant plus compliquée que le registre d'état est la source de dépendances d'instruction implicites, qui réduisent les performances. Par exemple, si une instruction arithmétique modifie le registre d'état, cela peut impacter l'exécution d'une instruction ultérieure, qui lit ce registre d'état. Pour éliminer ces fausses dépendances, le traducteur binaire doit renommer ce registre en logiciel et détecter les dépendances utiles. Et le compilateur doit gérer les cas où il y a beaucoup de distance entre les deux, voire les cas où l'instruction dépendante n'a pas encore été analysée par le compilateur. Un autre problème est lié à la gestion des exceptions matérielles, et précisément des exceptions précises. Pour rappel, une instruction VLIW regroupe plusieurs opérations, opérations qui correspondraient à une instruction machine sur un CPU pas VLIW. En conséquence, plusieurs instructions du langage machine "source" sont regroupées en une seule instruction VLIW. Et il faut tenir compte du cas où une instruction source lève une exception. Dans le code VLIW, cela signifie qu'une opération lève une exception, et il faut annuler partiellement l'instruction VLIW associée. Et par partiellement, on veut dire que seules les opérations suivantes dans l'ordre du programme source doivent être annulées, puis ré-exécutées. Et c'est un sacré casse-tête ! Les processeurs VLIW utilisent comme solution le mécanisme d'exceptions différées des processeurs EPIC, vu il y a quelques chapitres. Pour résumer, le code est exécuté par blocs d'instruction, délimités par des branchements ou tout autre limite/barrière pertinente dans le code. Un bloc de code est compilé en deux versions : une version rapide sans exceptions matérielles, une version lente qui gère les exceptions précises. La version rapide s'exécute sans exécuter les exceptions. Cependant, les exceptions matérielles sont enregistrées, pour être prises en compte à la toute fin du bloc de code. La version rapide est exécutée en premier et elle mémorise si une exception matérielle a eu lieu. Puis une instruction vérifie si une exception a eu lieu et décide quoi faire. Si aucune exception n'a eu lieu, elle passe à la suite du programme, les résultats du bloc de code sont définitivement acceptés. Mais si une exception a eu lieu, tout est annulé. Le processeur est remis dans l'état initial, puis le code est ré-exécuté instruction par instruction de manière à gérer l'exception correctement. ===La traduction par pré-décodage=== Une première solution serait de faire la traduction binaire dans le cache d'instruction. Lors d'un défaut de cache, le code chargé depuis la RAM est traduit en code machine cible. Et c'est ce code machine cible qui est mémorisé dans le cache d'instruction et exécuté par le processeur. La technique marche sur le papier et n'est qu'une amélioration des techniques de pré-décodage vues il y a quelques chapitres. Cependant, le code obtenu est une traduction assez basique, qui n'incorpore pas d'optimisations dignes de ce nom. De plus, elle gère mal le cas où la taille du code source et cible sont potentiellement très différentes. Concrètement, elle est surtout utile pour traduire du code d'un processeur RISC vers un autre processeur RISC, dont les tailles d'instructions sont similaires. Elle ne permet pas d'émuler plusieurs jeux d'instructions différents, le cout en matériel (un circuit de pré-décodage par jeu d'instruction) serait trop important. ===Le projet DAISY d'IBM=== De très rares processeurs étaient conçus pour accélérer cette traduction binaire, afin de garder de bonnes performances. Les premiers à avoir étudié l'idée étaient IBM, avec leur projet DAISY (''Dynamically Architected Instruction Set from Yorktown''). Le projet de base était de convertir à la volée du code compilé pour des CPU Power PC, vers du code VLIW. Les chercheurs d'IBM avaient développé un algorithme de traduction dynamique efficace, ainsi qu'un processeur VLIW disposant d'optimisations spécifiques à la traduction binaire dynamique, à la volée. De nombreuses idées de ce projet ont été reprises ou re-découvertes par la société Transmetta, avec ses processeurs Crusoe et Efficieon, puis par NVIDIA avec son projet Denver. Les ingénieurs de ce projet ont étudié la possibilité d'émuler plusieurs jeux d'instructions différents sur un même processeur, notamment le s390 d'IBM et le x86. En théorie, cela demande juste d'avoir plusieurs programmes de traduction binaire : un pour le x86, un autre pour le s390, éventuellement un autre pour le Power PC. Mais cela ne s'est pas concrétisé. Mais les ingénieurs ont étudié quelle pourrait être l'architecture VLIW idéale pour ça. Par exemple, il fallait des additions 3-opérandes pour simplifier les calculs d'adresse. Une difficulté était la gestion du registre d'état, dont les bits ne sont pas mis à jour de la même manière sur le x86 et le s390 ou le Power PC. La différence entre gros-boutisme et petit-boutisme était aussi un problème, de même que des différences d'adressage pour les périphériques. Pour que la traduction binaire soit efficace, le processeur VLIW intègre diverses optimisations, comme les exceptions différées et les branchements multi-voies. La principale est la suivante : seul le code des boucles ou fonctions exécutées fréquemment est traduit en code VLIW, le reste du code est interprété. En effet, traduire du code binaire prend plus de temps qu'une simple interprétation. Pour du code qui ne sera exécutée qu'une seule fois, il est plus rapide d'utiliser l'interprétation. Par contre, pour du code exécuté beaucoup de fois, le cout de la traduction binaire est amorti, dilué sur N exécutions, compensé par le gain en temps d'exécution de ce code traduit. Au final, cela permet de ne traduire que le code qui le mérite. La détection du code fréquemment exécuté est réalisée dans le cœur VLIW. L'unité de branchement mémorise les derniers branchements rencontrés et le nombre de fois qu'ils ont été exécutés. Si ils ont été exécutés un certain nombre de fois, l'unité de branchement lève une exception matérielle, qui invoque le traducteur binaire. La routine de cette exception est le traducteur binaire proprement dit. En clair, la traduction binaire est démarrée quand le processeur détecte qu'une fonction a été exécutée plus de N fois, via une exception matérielle dédiée. La mesure du nombre d'exécution d'un branchement se fait dans l'unité de calcul dédiée aux branchements, avec l'aide d'une sorte de ''branch adress buffer'' modifié. Pour rappel, le ''branch adress buffer'' mémorise le ''Program counter'' de chaque branchement récemment rencontré. Ici, chaque entrée du ''branch adress buffer'' est associée à un compteur incrémenté à chaque exécution du branchement. ===Les processeurs Crusoe et Efficieon de Transmetta=== Les processeurs Crusoe Et Efficeron sont deux processeurs VLIW produits par la société Transmetta, une société californienne rachetée par NVIDIA. Ils étaient conçus pour exécuter spécifiquement des programmes x86, système d'exploitation inclus. L'idée derrière ce projet était d'exécuter du code x86 sans que les applications, ni même le système d'exploitation et le BIOS soient au courant ! Les processeurs Transmetta étaient en réalité conçus pour exécuter un programme unique, le ''Code Morphing Software'' (CMS), qui traduisait le code x86 en code VLIW. Transmetta ne voulait pas que son processeur VLIW soit exploité directement, sans le CMS. Il n'avait pas rendu public de compilateur pour traduire du C vers du code VLIW, il n'avait pas donné la documentation du jeu d'instruction VLIW. Des efforts de rétro-ingénieurie, documentés sur le site realworldtech, ont cependant permis de comprendre comment étaient encodées les instructions du processeur Crusoe. Comme pour le projet Daisy, le ''Code Morphing Software'' utilisait à la fois interprétation et traduction binaire, selon les besoins. Les instructions sont interprétées lors de leurs premières exécutions, mais le CMS bascule sur de la traduction en code VLIW après un certain nombre d'exécution. Ainsi, les boucles souvent exécutées sont traduites en code VLIW, alors que le reste du code est interprété. Le choix du code à traduire est le fait du CMS, il utilise des heuristiques complexes pour, qui ne sont pas connues dans le détail. Le ''Code Morphing Software'' est mémorisé dans une EEPROM, ce qui en fait un ''firmware'' situé en dessous du BIOS. Le CMS démarre ensuite le BIOS, qui lui-même démarre le système d'exploitation, qui lui-même démarre les pilotes de périphériques et les programmes. Le CMS se réserve les 16 premiers mébioctets de l'espace d'adressage. L'EEPROM du CMS est mappée dedans, mais n'en utilise que la moitié. Le reste est utilisé comme cache, pour mémoriser le code VLIW traduit par le CMS. Les deux processeurs intégraient aussi deux mémoires SRAM, utilisées par le CMS, appelées la ''local program memory'' (LPM) et la ''local data memory'' (LDM). La première contient du code qui gére les interruptions, la mémoire virtuelle, les exceptions matérielles, les problèmes d'alignement mémoire, et quelques fonctions très fréquemment utilisées par le CMS. La seconde est de la mémoire RAM utilisée par ces fonctions de la ''local program memory''. Les mémoires caches L1 et L2 sont séparées de ces deux mémoires. Le processeur Crusoe et Efficieon étaient des processeurs VLIW très simples. Ils n'avaient même pas de MMU, ni de fonctionnalités importantes sur les CPU x86. Pour Crusoe, les instructions VLIW étaient encodées sur 64 ou 128 bits, et regroupaient entre 2 à 4 opérations. Les instructions LOAD/STORE ne supportaient pas d'adressage indicé, juste de l'adressage indirect à registre. Crusoe avait 5 unités de calcul : deux ALU entières, une unité LOAD/STORE, une unité de branchement, une FPU. Efficieon doublait le nombre d'ALU entière et d'unité LOAD/STORE, les instructions VLIW passaient à 256 bits. Le processeur Crusoe contient 160 registres, dont 64 registres généraux, 32 registres flottants. Les registres généraux font 32 bits, ce qui est cohérent avec le fait que les CPU x86 de l'époque étaient des processeurs 32 bits. Quant aux registres flottants, ils faisaient 80 bits, ce qui colle avec la taille des registres flottants de la FPU x87 utilisée à l'époque. Sur les 64 registres généraux, seuls 48 étaient réellement utilisable pour mémoriser des opérandes. Une partie des registres généraux étaient utilisés pour la gestion de la pile, d'autres pour mémoriser l'état du CPU x86 émulé, un registre était un registre zéro non accesible en écriture. La gestion des exceptions est optimisée avec un système d'exceptions différées, le même que celui décrit plus haut. Le processeur utilise un système similaire, pour effectuer des lectures anticipées, qui a été expliqué dans le chapitre sur les processeurs VLIW/EPIC. Sauf qu'il s'agit là d'une optimisation pour gagner en performances en général, pas une aide utile pour gérer des exceptions précises ou toute autre subtilité de la traduction binaire. Pour gérer les exceptions différées et les lectures anticipées, le processeur mémorise l'état du processeur avant de démarrer un bloc de code. Pour cela, le processeur contient des ''shadow registers'', 48 registres entiers et 16 registres flottants. Vous remarquerez qu'il y a autant de registres entiers que de ''shadow registers'' entiers. Les registres entiers/flottants sont mémorisés dans les ''shadow registers'' avant d'exécuter un bloc de code, afin que le processeur puisse revenir à l'état de base. Une autre fonctionnalité liée est le ''gated store buffer''. Le principe est d'éviter toute écriture en mémoire RAM, tant que le code VLIW n'a pas émis d'instruction ''commit''. L'idée est que les écritures sont conservées dans la ''store queue'' de l'unité LOAD/STORE. Pour rappel, la ''store queue'' met en attente les écritures soit tant que les écritures ne sont pas terminées, soit tant que la RAM est occupée. L'idée est que les écritures sont accumulées dans la ''store queue'' tant que l'instruction ''commit'' n'a pas eu lieu. Le ''store queue'' est "déversé dans la RAM" seulement quand l'instruction ''commit'' s'exécute. ===Les CPU NVIDIA Denver=== Nvidia a tenté quelque chose de similaire avec son projet Denver. NVIDIA a racheté l'entreprise Transmetta, ses brevets, et a tenté de refaire la même chose. Le projet initial était de traduire du code x86 vers un jeu d'instruction VLIW propriétaire, comme l'a fait Transmetta. Mais NVIDIA n'a pas réussi à acquérir la licence du jeu d'instruction x86 et s'est rabattu sur le jeu d'instruction ARM. Le processeur né de ce projet est le Tegra K1-64 CPU. Il s'agit d'un processeur composé d'un cœur VLIW, couplé à deux décodeurs d'instructions ARM. Les deux décodeurs ARM traduisent une paire d'instructions ARM consécutives en instructions VLIW, pour les exécuter sur le cœur VLIW. Le CPU peut fonctionner selon deux modes : ARM et VLIW. En somme, le processeur supporte deux jeux d'instruction : le jeu d'instruction ARM, et un jeu d'instruction VLIW. En mode ARM, les instructions ARM sont chargées depuis le cache, traduites en VLIW par les décodeurs ARM, puis exécutées par le cœur VLIW. En mode optimisé, le CPU exécute des instructions VLIW chargées depuis le cache d'instruction, les décodeurs ne sont pas utilisés. : Le CPU supporte en réalité deux jeux d'instruction ARM : ARM8 et 7. Mais ils correspondent au même mode pour le CPU. Un point important est que le processeur peut changer très rapidement de mode, en à peine quelques cycles d'horloges. On n'est clairement pas dans le cas des CPU x86, qui mettent des plombes pour passer du mode 32 à 64 bits et inversement. La commutation est tellement rapide qu'on peut considérer que le processeur supporte deux jeux d'instruction simultanément. Et le support simultané de deux jeux d'instruction facilite l'implémentation de la traduction binaire. Encore une fois, inutile de traduire du code qui ne sera exécutée qu'une seule fois, mieux vaut l'exécuter directement dans le mode ARM. Surtout que le processeur supporte un mode ARM qui permet d'exécuter du code ARM sans perte de performance. Par contre, le code fréquemment utilisé, notamment dans des boucles critiques, est traduit en VLIW. Au final, les performances sont optimisées, en limitant le travail du logiciel traducteur binaire. La détection du code fréquemment exécuté se fait différemment que pour les processeurs Transmetta et Daisy. L'unité de branchement ne détecte pas directement le code beaucoup exécuté, même si elle a un rôle à jouer. A la place, la détection est réalisée en logiciel, par un ''thread'' dédié, qui s'exécute sur un cœur séparé. Il détecte le code fréquemment utilisé en regardent les branchements exécutés récemment. Pour cela, le processeur mémorise l'historique des branchements pris récemment et copie cette historique dans une mémoire tampon dédiée, partagée entre tous les cœurs. Vu qu'elle est partagée, le ''thread'' de détection a accès à l'historique sans pertes de performances, seule la copie de l'historique dans ce tampon a un cout en performance, pas son partage. Le code VLIW obtenu après traduction binaire est mémorisé dans la mémoire RAM, dans une portion spécialement réservée pour. Elle fait 128 mébioctets, et est appelée le '''cache d'optimisation''' par NVIDIA. Le cache d'optimisation est protégé en écriture et seul le traducteur binaire peut écrire dedans. Au passage, le traducteur binaire est du code VLIW, ce qui fait qu'il est placé dans le cache d'optimisation. Le processeur détecte automatiquement quand une fonction pour laquelle le code compilé équivalent est disponible. Pour cela, l'unité de chargement contient une table de correspondance entre l'adresse de la fonction ARM, et l'adresse de son équivalent compilé. Quand une fonction est appelée, l'unité de chargement regarde l'adresse de destination du branchement. Si l'adresse est dans cette table, elle récupère l'adresse de la fonction compilée et branche vers celle-ci. Le processeur dispose d'un cache d'instruction de 128 kibioctets, ce qui est très important, mais nécessaire vu la taille des instructions VLIW. Le coeur VLIW dispose de ses propres décodeurs, d'un ''scoreboard'', de 7 unités de calcul et de registres. Pour les unités de calcul, il a deux FPU, deux ALU, une unité de branchement, et deux unités LOAD-STORE faisant aussi ALU. Un point important est que le CPU dispose de 64 registres entiers et de 64 registres flottants. C'est deux fois plus que supporte le jeu d'instruction ARM. En mode ARM, seule la moitié des registres est utilisée. Mais en mode optimisé, le traducteur binaire utilise bien les 64 registres, grâce à une sorte de renommage de registres logicielle. Pour simplifier la traduction binaire, le processeur supporte les techniques vues précédemment. Le processeur intègre un ''gated store buffer'' similaire à celui des processeurs Transmetta. Les techniques d'exceptions différées sont aussi supportées, comme sur les processeurs Transmetta. Il a aussi une unité de préchargement décrite par NVIDIA comme agressive, avec support du préchargement de type anticipé (''runahead''). ===L'émulation de la mémoire virtuelle=== Pour finir, il faut parler de la mémoire virtuelle avec la traduction binaire. Sur le processeur source, le processeur gère à la fois des adresses physiques et virtuelles et sa MMU fait la conversion entre les deux. Sur le processeur VLIW, la MMU est simulée par le programme de traduction binaire, partiellement ou totalement. L'implémentation exacte varie suivant que l'on parle du projet Daisy, des processeurs Transmetta ou de Denver. Sur les processeurs Transmetta, il n'y a pas de MMU ni de mémoire virtuelle. A la place, le programme de traduction binaire émule la mémoire virtuelle du processeur source. Le processeur VLIW ne gère que des adresses physiques, rien d'autre. L'espace d'adressage physique du processeur VLIW a la même taille que celui du processeur émulé, ici des processeurs x86. Il est vraisemblable que les adresses physiques utilisées par le processeur x86 sont les mêmes que celles du processeur VLIW. Sur le projet Daisy, le processeur VLIW gère la mémoire virtuelle via pagination, comme les processeurs émulés. Ce qui fait qu'il gère un espace d'adressage virtuel et un espace d'adressage physique. Pour éviter toute confusion, nous parlerons d'adresse physique/virtuelle VLIW pour les adresses physiques/virtuelles du processeur VLIW, d'adresse virtuelle/physique source pour celle du jeu d'instruction traduit, à savoir du Power PC ou du s390, éventuellement de l'x86. Il faut alors faire le lien entre adresses physiques source et adresses virtuelles VLIW. Pour cela, rien de plus simple : il y a correspondance parfaite. L'adresse physique source numéro N correspond à l'adresse virtuelle VLIW numéro N. Cependant, cela signifie que tout l'espace d'adressage virtuel VLIW serait utilisé par le code à traduire. En réalité, il faut ajouter de la place pour le code traduit et le programme de traduction binaire. La conséquence est que l'espace d'adressage virtuel VLIW est plus large que l'espace d'adressage source. L'espace d'adressage virtuel VLIW est découpé en trois sections : une section pour le code Power PC à traduire, une autre pour le code traduit en VLIW, et une dernière réservée au traducteur binaire. Le programme de traduction est placé dans une ROM mappée en mémoire dans la seconde section. Le reste de la seconde section est réservé à une mémoire RAM utilisée par le programme de traduction. [[File:Mémoire virtuelle sur le projet Daisy.png|centre|vignette|upright=1.5|Mémoire virtuelle sur le projet Daisy]] Il faut noter que chaque page physique source, une fois traduite, correspond à N pages virtuelles VLIW. La raison est que le code traduit prend plus de place que le code machine originel. En conséquence, on doit utiliser une page finale plus grande. Pour le reste, des pages contiguës en mémoire physique source sont elles aussi contiguës en mémoire virtuelle VLIW. Le calcul d'adresse est donc simple : l'adresse physique source est multipliée par N, puis on ajoute l'adresse de base à laquelle commence la section pour le code traduit. ==Les processeurs à jeu d'instruction reconfigurable== Nous venons de voir comment accélérer en matériel la traduction binaire. Mais il existe une alternative qui permet de se passer de traduction binaire, complétement. L'idée est d'avoir un processeur avec un '''jeu d'instruction reconfigurable'''. Le nom est assez parlent : on peut configurer le processeur de manière à supporter le jeu d'instruction de son choix ! Par exemple, on pouvait reconfigurer un CPU de manière à ce qu'il supporte le jeu d'instruction x86, ou qu'il exécute du ''bytecode'' Java, ou du code ARMv8, etc. De tels processeurs sont encore plus rare que ceux vu avant dans ce chapitre, mais ils ont existé ! Les processeurs de ce type avaient un '''microcode réinscriptible''', à savoir que c'était des processeurs microcodés pour lesquels on pouvait modifier le microcode. L'idée était qu'en changeant le microcode, on changeait le jeu d'instruction du processeur. Pour cela, le microcode était associé à une mémoire SRAM, en plus de la mémoire ROM du ''control store''. La SRAM était appelée la micro-SRAM, alors que le ''control store'' basique était appelé la micro-ROM. Le microcode était copié dans cette micro-SRAM au démarrage du CPU, puis les instructions étaient décodées en utilisant le microcode dans la micro-SRAM. En modifiant le contenu de la micro-SRAM, on pouvait altérer le microcode, voire le remplacer totalement. Il s'agit là de l'implémentation la plus simple. Mais le Burroughs B1700 a procédé autrement. ===Le burroughs B1700 : un ''split-level control store''=== Le burroughs B1700 lisait le microcode directement depuis la mémoire RAM ! Pour être précis, il avait plusieurs modèles : le B1726, le B1710 et le B1800. Le B1710 plaquait le microcode en mémoire RAM, ce qui fait que chaque exécution d'une instruction lisait les micro-opérations en mémoire RAM ! Le B1800 faisait pareil, mais avait ajouté un cache dédié au microcode, afin d'améliorer les performances. Le B1726 avait un microcode scindé en deux : une partie était en mémoire RAM, l'autre était dans un microcode réinscriptible classique, avec une micro-SRAM intégrée au processeur. Le processeur peut exécuter le microcode directement depuis la mémoire RAM, mais avec une pénalité en termes de performances. La documentation appelle cela un '''''split-level control store'''''. Le programmeur devait découper le microcode en segments, certains étant placé dans la micro-SRAM, les autres étant en mémoire RAM. Un tel microcode permettait de placer le microcode le plus fréquemment utilisé dans le processeur, alors que le reste du microcode était placé en mémoire RAM. Les instructions fréquentes étaient ainsi décodées avec la micro-SRAM, alors que les instructions peu fréquentes étaient décodées en lisant le microcode associé en mémoire RAM. La pénalité en termes de performance est alors concentrée sur les instructions les plus rares, ce qui est un très bon compromis. Mieux que ça, le processeur autorisait d'utiliser la technique de l'''overlaying'' pour le microcode ! Il y avait même une micro-opération OVERLAY dédiée pour ! Elle permettait de copier du microcode de la mémoire RAM vers la micro-SRAM ! OVERLAY avait besoin de trois opérandes : la taille du segment à copier, son adresse source en mémoire RAM, son adresse de destination dans la micro-SRAM. Le microcode pouvait servir pour émuler un autre jeu d'instruction, mais il était aussi possible d'implémenter un véritable interpréteur, grâce aux techniques du paragraphe précédent. Le processeur était même conçu pour ! L'interpréteur était placé dans un "segment" en mémoire RAM, dont le processeur connaissait la position. Il incorporait pour cela deux registres, un pour l'adresse de base de l'interpréteur, un autre pour son adresse de fin. Il incorporait une protection mémoire limitée sur ce segment, à savoir que seul l'interpréteur pouvait lire ou écrire dans ce segment. La micro-SRAM mémorisait au maximum 7680 micro-opérations, ce qui fait que les 7680 premières "adresses" correspondaient à la micro-SRAM. Les suivantes étaient des adresses de mémoire RAM qui contenaient le reste du microcode. En pratique, l'adresse qui séparait les deux portions du microcode était configurable ! On pouvait limiter la taille du microcode en micro-SRAM à un multiple de 512 micro-opérations ! Les adresses restantes étaient alors réattribuées à la RAM. Par exemple, on pouvait attribuer 512 adresses à la micro-SRAM, les 7680 - 512 adresses restantes étaient alors attribuées à la mémoire RAM. Un registre limite, nommé TDPM, indiquait quelle était l'adresse de démarcation entre les deux. Il contenaient 4 bits, dont la valeur était multipliée par 512, pour obtenir l'adresse limite. ===Le microcode réinscriptible=== Le microcode utilise des micro-opérations de 16 bits de long, ce qui est très peu et bien en-dessous des micro-opérations des autres processeurs. Les micro-opérations sortant du microcode sont envoyées au registre M, un registre de micro-opération de 16 bits. les micro-opérations écrites dans ce registre sont ensuite décodées par le hardware et exécutées. Fait intéressant, il était possible de faire un OU logique entre la micro-opération sortante et un registre du processeur, le résultat étant mémorisé dans le registre M. Cela permettait de faire du code automodifiant dans le microcode ! Utiliser du code automodifiant avec un microcode réinscriptible est parfaitement possible, et c'est utile pour émuler des modes d'adressage complexe dans le microcode. Cela permettait par exemple de remplacer une adresse dans une micro-opération. L'adresse est alors surimposée avec un OU dans le champ d'adresse, alors que le reste de l'instruction n'est pas modifié. Mais modifier directement le microcode est rarement une bonne idée. Aussi, pour éviter cela, l'idée est de modifier les instructions avec un OU logique avec un autre registre. Les micro-opérations du microcode ne sont donc pas modifiées dans le microcode, mais en sortie du microcode. Afin de gérer les interruptions, ces dernières étaient partiellement détectées en logiciel. Les interruptions ne s'exécutaient pas automatiquement, pour émuler correctement les interruptions. Une interruption doit être prise en compte à la toute fin de l'exécution d'une instruction, quand celle-ci a finit son travail. Mais vu qu'ici, les instructions sont émulées avec des séries de micro-opérations, il y a un problème : il est possible d'interrompre une instruction émulée en plein milieu. Pour éviter cela, le processeur ne déclenche pas les interruptions matérielles immédiatement, elles sont mises en attente. Une micro-opération dédiée vérifie si une interruption est en attente, et l'exécute alors cas échéant. La micro-opération est utilisée comme micro-opération finale d'une instruction émulée. ===Une architecture bit-adressable=== Si avoir un microcode réinscriptible est suffisant, quelques choix de ''design'' facilitent l'émulation de jeux d'instructions variés. Par exemple, le CPU Burroughs B1700 avait ajouté une fonctionnalité très importante : il était bit-adressable. En clair, il pouvait adresser la mémoire bit par bit, si nécessaire. L'avantage est que cela permettait d'émuler des architectures dont la taille des registres/''byte''/mots était très différente. Un autre avantage est que cela facilitait la gestion des instructions de taille variable. Détaillons rapidement le premier avantage. Le processeur était un CPU 24 bits, mais il pouvait émuler des processeurs 16 bits, 8 bits, 4 bits, 1 bit, 9 bits, ou toute autre valeur. Pour cela, le processeur lisait des mots de 24 bits et utilisant un ''barrel shifter'' pour sélectionner les bits adéquats. Par exemple, pour un accès de 16 bits, la donnée lue en mémoire était masquée de manière à ne garder que les 16 bits de poids faible. Le ''barrel shifter''/circuit de masquage était placé directement avant le bus mémoire, sur le trajet des données/instructions. Le ''barrel shifter'' était commandé par un registre ''Field Unit'', qui précisait quelle était la taille des données à charger. Le processeur gère un micro-''program counter'' appelé le registre A. Les micro-opérations du processeur font 16 bits de long, et elles sont alignées en RAM sur 16 bits, ce qui fait que les 4 bits de poids fort de ce registre valent 0. Le registre fait 18 bits, mais seuls les 14 bits de poids fort sont utiles. Les adresses mentionnées plus haut, qui adressent les micro-opérations en mémoire RAM, sont des adresses de mots de 16 bits, pas les adresses de bit individuels. ===Le Burroughs B1700 : architecture externe=== Un exemple est celui du Burroughs B1700, et il est intéressant d'étudier en détail son architecture. Son architecture est décrite dans le livre "Interpreting Machines : Architecture and Programming of the Bl700/Bl800 Series", écrit par Elliott I. Organick et James A. Hinds. Une partie de ce qui vfa suivre est un très court résumé de ce livre. Il disposait de 4 registres généraux, nommés X, Y, T et L, tous de 24 bits. Les registres T et L étaient découpés en 6 sous-registres de 4 bits, nommés LA LB LC LD LE et LF pour le registre L, et TA TB TC TD TE et TF pour le registre T. Ils étaient adressables comme n'importe quel registre. Un point important est que le registre T avait des possibilités d’extraction spécifiques. Il était possible d'extrait N bits, placés n'importe où dans ce registre, et de les copier dans un autre registre. Nous verrons pourquoi cette possibilité est intéressante et ne sert que pour le registre T. Les registres X et Y étaient reliés à une ALU de 24 bits capable d'effectuer des opérations arithmétiques basiques. Fait étonnant, l'ALU faisait tous les calculs en même temps et fournissait ses résultats dans 7 registres : un registre SUM pour l'addition, un registre DIFFERENCE pour la soustraction, et un registre par opération bit à bit. Les registres pour les opérations bit à bit sont : CMPX et CMPY pour l'opération NOT sur chaque opérande, XANY pour le ET bit à bit, XEOY pour le XOR, XORY pour le OU logique. Les additions et soustractions pouvaient se faire sur des opérandes codés en binaire ou en BCD, suivant le code opération utilisé. A cela, il fallait ajouter deux registres de "résultat" MSKX et MSKY. Ils fournissaient un masque dépendant de la taille du résultat en bits. Cela permettait de gérer des données de 0, 4, 8, 12, 16, 20, et 24 bits, alors que l'ALU faisait 24 bits. Et cela n'était pas limité à 16 ou 8 bits, mais n'importe quelle taille inférieure ou égale à 24 bits était supportée, même des tailles non-conventionnelles comme des résultats codés sur 3 bits, 9 bits, 12 bits, etc. L'ALU peenait en opérande les registres X et Y, mais aussi un registre CYF pour la retenue entrante. En sortie, il fournissait aussi deux retenue sortantes : une pour l'addition et une autre pour la soustraction. Elles étaient mémorisées dans les registres CYL et CYD, respectivement. L'ALU fournissait aussi 12 conditions, qui alimentait un registre d'état. Le registre d'état était segmenté en trois registres appelés XYST, XYCN et BICN. Le processeur contenait de nombreux autres registres de 4 bits, dont les sous-registres de T et L. En tout, il y a 27 registres de 4 bits adressables ! Et le processeur disposait d'une ALU 4 bits pour manipuler ces registres de 4 bits ! Elle est capable de faire des MOV entre registres de 4 bits, des opérations bit à bit, et peut aussi tester si un bit vaut 0 ou 1. La dernière possibilité est très utile pour implémenter les branchements. Pour communiquer avec les entrées-sorties, il disposait de deux registres CMND et DATA : CMND pour envoyer une commande à une entrée-sortie, DATA pour lire ou écrire une donnée. Pour la mémoire, il avait deux registres séparés : READ et WRITE. READ contient la donnée lue lors d'une lecture,; WRITE est pour une donnée à écrire lors d’une écriture. Il s'agit de registres d’interfaçage mémoire, qui ont été promus au rang de registres architecturaux. Les deux registres sont gérés par le microcode, qui est le langage principal de la machine. Les instructions machines configurées font sans, mais le microcode doit y avoir accès facilement. Et vu que le microcode est le langage principal de ce processeur, ces deux registres deviennent architecturaux. ===La gestion de la taille des instructions=== Les instructions à décoder n'ont pas de longueur fixe, à savoir qu'elles peuvent faire 16 bit avec tel jeu d'instruction, 8 bits avec un autre, 32 bits sur un autre, etc. Elles peuvent aussi être de longueur fixe ou variable ! Le B1700 intégrait donc un support des instructions de longueur variable, qui était "détourné" pour gérer des jeux d'instruction différents. Notons que vu que l'architecture est bit-adressable, les contraintes d'alignement des instructions disparaissent, ce qui a des avantages qu'on va détailler dans ce qui suit ! L'implémentation des instructions de taille variable ajoutait un registre avant le registre d'instruction. Le registre d'instruction proprement dit était un registre M de 24 bits. Il était précédé par un registre T, dans lequel l'instruction était assemblée, en combinant plusieurs morceaux. Si l'instruction est codée est codée sur moins de 24 bits, le registre T se contente de masquer les bits en trop. Si l'instruction chargée dépasser 24 bits, le chargement se faisait en plusieurs passes, avec plusieurs lectures en mémoire. Dans ce qui suit, on suppose que l'instruction est composée de '''champs''' : l'opcode, les opérandes, les adresses absolues, les constantes immédiates, les numéros de registre, etc. L'idée est que l'instruction est chargée dans le registre T, puis découpée en champs, qui sont copiés dans les registres du processeur ou envoyés au microcode. Je rappelle que le registre T est relié à un ''barrel shifter'', différent au précédent, qui permettait de masquer, décaler et extraire une donnée de ce registre. Et il y a la possibilité d'extraire une suite de bit de ce registre T, pour la copier dans un autre registre. Par exemple, prenons une instruction composée d'un opcode, d'une constante immédiate et d'une adresse immédiate. L'opcode est lu et interprété par le microcode. Le microcode extrait la constante immédiate du registre T et la copie dans le registre X. Ensuite, l'adresse est extraite et envoyée au registre FA, une lecture est démarrée, et l'opérande est copiée dans le registre Y. Enfin, l'instruction est exécutée par le microcode. Le tout prend plusieurs micro-opérations : une première pour lire l'opcode, une seconde pour lire la première opérande, une troisième pour lire la seconde opérande, una quatrième pour tout exécuter. Notons que suivant la taille des champs, on peut en charger plusieurs à la fois. Par exemple, imaginons qu'on ait chargé 24 bits, contenant un opcode de 8 bits et une constante immédiate de 16 bits : pas besoin de faire deux accès mémoire, le registre T est juste découpé en un opcode et une constante. Le registre T est fait pour ce genre d'extraction. ===Le chargement d'un champ=== L'architecture est bit-adressable car elle gére des champs qui ne font pas forcément 8 ou 16 bits. Par exemple, certains jeux d'instructions ont des opcodes codés sur 6 ou 7 bits, voire 9-10 bits. Par exemple, prenons un opcode de 7 bits, suivi par une adresse de 12 bits et une constante immédiate de 15 bits : aucun de ces champs ne sera aligné sur 8/16/24 bits. Utiliser une architecture bit-adressable règle le problème d'alignement. Et en général, les contraintes d'alignement des instructions disparaissent ! Quand on charge une instruction, peut importe qu'elle soit ou non alignée sur 24 bits, elle sera placée dans les bits de poids faible, il suffira de masquer les bits de poids fort inutiles. Par exemple, si on charge une instruction de 16 bits, les 16 bits seront copiés dans les 16 bits de poids faible du registre T, il suffira de masquer l'octet de poids fort. Pas besoin de faire de décalages pour gérer des instructions mal alignées... Pour charger une instruction, il faut connaitre plusieurs informations : l'adresse de son premier bit, sa taille. Pour cela, le B1700 contient deux registres, nommés ''Field Adress'' (FA) et ''Field Length'' (FL), qui indiquent l'adresse du premier bit et la taille de l'instruction à charger. Le premier fait 24 bits, ce qui permet de gérer des adresses de 24 bits. N'oubliez pas que le processeur est bit-adressable, ce qui permet d'adresser 16 Mébi-bits, soit 2 mébioctets. Le registre pour la taille fait lui 16 bits, ce qui permet de gérer des instructions de 65536 bits, soit 8192 octets ! Leur nom commence par ''field'', car le processeur peut gérer des instructions de taille variable et/ou plus longues que 24 bits. Dans ce cas, le chargement de l'instruction se fait champ par champ. Pour donner un exemple, prenons une instruction composée de 20 champs de 12 bits chacun. Les champs sont chargés un par un dans le registre T. Le registre FA est alors initialisé avec l'adresse de l'instruction, il est incrémenté de 12 à chaque cycle. Le registre FL est initialisé avec la taille de l'instruction, soit 20 * 12 = 240 bits, et il est décrémenté de 16 à chaque cycle. Précisons cependant que la taille d'un champ peut être changée d'un cycle à l'autre. Par exemple, le processeur peut charger un opcode de 8 bits, l'interpréter, détecter que l'instruction demande ensuite de charger une adresse de 16 bits. Dans ce cas, le registre FL est immédiatement altéré pour charger 16 bits, au lieu des 8 bits de l'opcode immédiatement précédent. ==Les jeux d'instructions dédiés à un langage de programmation== De rares processeurs sont conçus pour un langage de programmation en particulier. On appelle ces processeurs, conçus pour des besoins particuliers, des '''processeurs dédiés'''. Par exemple, les fameux ''Burrough E-mode'' B5000/B6000/B7000 étaient spécialement conçus pour exécuter de l'ALGOL-60. Leurs cousins B2000/B3000/B4000 étaient eux conçus pour le COBOL. Des langages fonctionnels ont aussi eu droit à leurs processeurs dédiés. Le prolog en est un bel exemple, avec les superordinateurs de 5ème génération qui lui étaient dédié. On peut aussi citer les machines LISP, dédiés au langage LISP, qui datent des années 1970. Elles étaient capables d’exécuter certaines fonctions de base du langage directement dans leurs circuits : elles possédaient notamment un ''garbage collector'' câblé dans ses circuits ainsi que des instructions machines supportant un typage déterminé à l’exécution. Les processeurs dédiés ont eu leur heure de gloire au début de l'informatique, à une époque où les langages de haut niveau venaient d'être inventés. À cette époque, les compilateurs n'étaient pas performants et ne savaient pas bien optimiser le code machine. Il était alors rationnel, pour l'époque, de rapprocher le code machine cible et le langage de programmation de haut niveau. De nombreuses architectures dédiés ont ainsi été inventées, avant que les concepteurs se rendent compte des défauts de cette approche. Les défauts en question ne sont pas nombreux, mais assez simples à comprendre. Premièrement, elles sont très rapides pour un langage de programmation en particulier, mais sont assez mauvaises pour les autres, d'où un problème de "compatibilité". Ajoutons à cela que les langages de programmation peuvent évoluer, devenir de moins en moins populaires/utilisés, ce qui rend la création d'architectures généralistes plus pertinente. Enfin, les architectures dédiées sont évidemment des processeurs CISC, pour implémenter les nombreuses fonctionnalités des langages évolués. Et les défauts des CISC sont assez rédhibitoires à l'heure actuelle. À l'heure actuelle, les algorithmes des compilateurs se sont améliorés et savent nettement mieux utiliser le matériel. Ils produisent du code machine efficace, ce qui rend les architecture dédiées bien moins intéressantes. Si on ajoute les défauts de ces architectures dédiées, par étonnant que les architectures dédiées aient presque disparues. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'accélération matérielle de la virtualisation | prevText=L'accélération matérielle de la virtualisation | next=Le matériel réseau | nextText=Le matériel réseau }} </noinclude> kwuqy2445hncv82j9mzw6lxyxoi53ny 764035 764034 2026-04-19T19:18:11Z Mewtow 31375 /* Les processeurs à jeu d'instruction reconfigurable */ 764035 wikitext text/x-wiki De nos jours, la majorité des programmeurs programment dans des langages de haut niveau. Il est très rare pour eux d'avoir à utiliser de l'assembleur, et encore moins un langage machine. Les programmes écrits dans un langage de haut niveau sont traduits en langage machine par un logiciel appelé le compilateur. [[File:Compilation.PNG|centre|vignette|upright=2.5|Compilateur : principe.]] Il se trouve que quelques rares processeurs sont conçus pour faciliter le travail du compilateur. Et ce sont ces processeurs que nous allons voir dans ce qui suit. Ils représentent une catégorie de jeux d'instruction à part, qui n'a pas vraiment de nom. ==La compilation et l’interprétation== Un compilateur traduit donc du code source, écrit dans un langage de haut niveau, vers du langage machine. La traduction est rarement directe. En général, le compilateur traduit le code source en assembleur, qui est lui-même traduit en langage machine. L'assembleur est une représentation textuelle du langage machine alors que le code machine est du binaire exécutable par le processeur. La traduction se fait donc en deux étapes, la '''compilation''' proprement dite et l''''assemblage''', réalisées respectivement par un compilateur et un assembleur. Il faut parfois rajouter une troisième phase d'édition des liens, que nous passons volontairement sous silence. {| |[[File:Code C.png|vignette|200px|Le code source, ici en C.]] |[[File:Assem.png|vignette|250px|Le code assembleur.]] |[[File:Binary file - hello world (C programming).png|vignette|150px|Le code machine.]] |} ===L'interprétation et la compilation à la volée=== Une alternative à la compilation est l''''interprétation''', qui transforme le code source en code machine à la volée. Avec l'interprétation, le code source est passé à un logiciel appelé l''''interpréteur''', qui exécute le code source ligne de code par ligne de code. Une ligne de code est traduite en code machine, qui est exécutée, puis l'interpréteur passe à la ligne suivante. [[File:Kaantotulkkaus.png|centre|vignette|upright=3|Compilation versus interprétation.]] Un défaut de l'interprétation est son cout en performance, qui est assez important. Aussi, l'interprétation stricte a évolué vers un hybride entre interprétation et compilation. L'idée est qu'une partie peu importante du code source est interprétée, alors que le code important est compilé juste avant d'être exécuté. Pour le dire autrement, le code source est partiellement compilé à la volée, juste avant son exécution. Aussi, on parle de '''compilation à la volée'''. Le terme en anglais est ''Just In Time compilation'', abrévié en JIT. En soi, le JIT est associé à de l'interprétation, les JIT sont en réalité des hybrides interpréteurs-compilateurs. Un point important est que compiler du code à la volée est assez lourd, cela a un cout en performance. Par contre, le code compilé s'exécute plus vite que du code interprété. Si on exécute le code compilé une seule fois, le cout de la compilation l'emporte sur le gain à l'exécution. Mais le code compilé est réutilisé autant de fois que nécessaire. Le gain à l'exécution est donc multiplié par le nombre d'exécutions, alors que le cout de la compilation est répartit sur plusieurs exécutions, il est amorti. Les portions du code sont choisies de manière à avoir un gain en performance maximal. L'idée générale est que le code exécuté très souvent est compilé, alors que du code exécuté pas souvent est interprété. Typiquement, les boucles les plus souvent exécutées sont compilées, le code exécuté une seule fois est interprété. ===Les langages intermédiaires=== Les compilateurs modernes passent par un '''langage intermédiaire''' pour faire la transformation en code machine. Le compilateur traduit le langage de haut niveau en langage intermédiaire, puis traduit le langage intermédiaire en code machine cible. Le langage intermédiaire est parfois appelé le '''''bytecode''''', et ce terme recouvre aussi le code écrit avec du ''bytecode''. L'interprétation aussi peut passer par l'intermédiaire d'un un ''bytecode'', ce n'est pas limité aux compilateurs proprement dit. {| |[[File:Intermediate representation scheme.png|centre|vignette|upright=1.5|Représentation intermédiaire d'un compilateur.]] |[[File:Code interpreter scheme.png|centre|vignette|upright=1.5|Fonctionnement d'un interpréteur.]] |} Faire ainsi a de nombreux avantages, le principal étant d'avoir un compilateur capable de traduire un langage de haut niveau vers plusieurs jeux d'instructions différents. Cela permet d'avoir, par exemple, un compilateur qui traduit du C soit en code machine x86, soit en code machine pour un CPU ARM, soit pour un CPU POWERPC, etc. [[File:Compiler design.svg|centre|vignette|upright=3|Compiler design]] Pour l'interprétation, l'usage d'un ''bytecode'' a d'autres avantages. Pour l'exploiter, il faut distribuer non pas le code source, mais le ''bytecode''. En clair, l'interpréteur ne prend pas en entrée le code source, mais du ''bytecode'' déjà compilé. La traduction est alors beaucoup plus simple, car le code source a déjà été partiellement compilé, par la traduction en ''bytecode''. Interpréter du code source est en effet assez compliqué : il faut effectuer des étapes d'analyse lexicale, sémantique, et bien d'autres. Avec du ''bytecode'', ces étapes ont été réalisées lors de la compilation du ''bytecode'', l'interpréteur a alors peut de choses à faire. L'interprétation du ''bytecode'' se fait instruction par instruction, au niveau du ''bytecode''. Une instruction du ''bytecode'' est traduite en une instruction machine équivalente, qui est exécutée. Si l'instruction est un peu complexe, il exécute une fonction/procédure qui fait la même chose. Pour résumer, le ''bytecode'' est ensuite traduit à la volée et exécuté instruction par instruction par un logiciel appelé l'interpréteur. [[File:Control table.png|centre|vignette|upright=2|Control table]] Le langage intermédiaire peut être vu comme l'assembleur d'un processeur, n'existe pas forcément dans la réalité, mais dont le jeu d'instruction est décrit en détail. Le processeur en question est appelé une '''machine abstraite''', ou encore une ''machine virtuelle''. Nous utiliserons le terme de machine abstraite dans ce qui suit. La machine abstraite n'est pas la même suivant que l'on cible la compilation ou l'interprétation/JIT. Pour l'interprétation/JIT, la machine abstraite est souvent une machine à pile, car cela simplifie grandement la traduction en code machine final. L'interprétation demande que la traduction du ''bytecode'' en code machine soit la plus simple possible. Et cela demande de prendre en compte pas mal de détails. Par exemple, les différents jeux d'instruction existants n'ont pas le même nombre de registres, ce qui pose problème lors de la traduction du ''bytecode''. Lors de la transformation en code machine, un algorithme d'allocation de registres se débrouille pour traduire le code intermédiaire en code qui utilise un nombre limité de registres. Pour améliorer cette allocation de registres, il y a deux solutions niveau ''bytecode'' : soit utiliser un nombre illimité de registres, soit utiliser une machine à pile. Pour les machines à pile, il existe un algorithme simple et rapide pour traduire un code écrit pour une machine à pile en un code écrit pour un processeur avec des registres, qui se débrouille pas trop mal pour allouer efficacement les registres. C'est un avantage assez important pour les langages interprétés. ==Les processeurs qui exécutent du bytecode== Le ''bytecode'' est un code machine, ce qui signifie qu'il peut en théorie s'exécuter sur un processeur qui implémente le jeu d’instruction associé. Si le jeu d'instruction d'un ''bytecode'' est souvent une description censée être fictive, elle n'en reste pas moins un jeu d'instruction et des caractéristiques précises, qu'on peut l'implémenter en matériel ! Néanmoins, tous les ''bytecode'' ne sont pas égaux de ce point de vue. Certains sont faciles à implémenter en matériel, d'autres non. Les ''bytecodes'' implémentés en matériel sont ceux dont les machines abstraites sont des machines à pile. Un premier exemple est celui des processeurs Pascal MicroEngine, qui exécutaient directement le bytecode du langage Pascal, le fameux UCSD P-code. Un second exemple est la '''machine SECD''', qui sert de langage intermédiaire pour certains compilateurs de langages fonctionnels. Elle a été implémentée en matériel par plusieurs équipes, notamment par les chercheurs de l'université de Calgary en 1989. Dans le même genre, quelques processeurs simples étaient capables d’exécuter directement le ''bytecode'' du langage FORTH. Le FORTH, un des premiers langages à pile de haut niveau, possède de nombreuses implémentations hardware et est un des rares langages de haut niveau dont le ''bytecode'' a été utilisé comme langage machine sur certains processeurs. Par exemple, on peut citer le processeur FC16, capable d’exécuter nativement du ''bytecode'' FORTH. ===Les processeurs Java=== Le cas le plus impressionnant est celui de la machine virtuelle Java, qui est un design de processeur comme un autre. En temps normal, le ''bytecode'' Java est compilé ou interprété, mais certains processeurs exécutaient du ''bytecode'' Java directement, sans interprétation ni compilation. Ils sont appelés des '''processeurs Java'''. On peut citer les processeurs ARM disposant de l'extension Jazelle, du JEMCore et du aJ-100 de aJile Systems, le picoJava II de Sun, et quelques autres. Il y a aussi eu quelques projets de recherche ou processeurs open source, comme Komodo, jamuth, le ''Java Optimized Processor'' et quelques autres projets du même genre. L'implémentation de la machine virtuelle Java n'était cependant pas complète. Quelques instructions complexes n'étaient pas gérées par le processeur et devaient être émulées en logiciel. Mais la grosse majorité des instructions l'était. Les instructions non-supportées étaient émulées avec la méthode ''trap and emulate'', à savoir qu'un opcode inconnu déclenchait une exception matérielle ''opcode invalide'', dont la routine pouvait émuler les instructions qui doivent l'être. Le processeur aJ-100 de aJile est capable d'exécuter la quasi-totalité des instructions de la JVM Java, seules deux instructions faisant exception. Les '''accélérateurs Java''' sont une solution intermédiaire, qui permet de traduire à la volée du ''bytecode'' Java en instructions RISC, exécutées sur un autre processeur. Il s'agit formellement de traduction binaire, mais je préfère en parler ici. L'idée est de combiner un processeur RISC avec un coprocesseur Java. Le coprocesseur lit le ''bytecode'' Java et le traduit en instructions RISC, ces dernières étant exécutées sur le processeur RISC. Le JA108 de Nozomi était un coprocesseur de ce genre, au même titre que le coprocesseur JSTAR de JEDI. L'extension Jazelle de certains CPU ARM fonctionnait vraisemblablement sur un principe similaire, sauf que le coprocesseur était intégré dans le processeur ARM, entre le cache d'instruction et le décodeur d'instructions ARM. L'intérêt des processeurs Java n'est pas qu'une question de performance. Certes, ils permettent d’exécuter plus vite les programmes compilés en ''bytecode'', comme des programmes Java pour la JVM Java. Mais l'intérêt est aussi de faire des économies de mémoire RAM et ROM. Je rappelle que la machine virtuelle Java est une machine à pile, ce qui fait que sa densité de code est excellente. Les programmes en ''bytecode'' Java sont donc très petits, et prennent peu de place en mémoire ROM. De plus, utiliser un processeur Java permet de se passer d'interpréteur ou de JIT Java, ce qui économise encore plus de mémoire. ===Transformer un processeur RISC en machine à pile=== La plupart des processeurs Java utilisent en interne une architecture à registre généraux, qui émule une machine à registre en microcode. Quelques registres sont réservés pour l'état de la machine virtuelle, avec au minimum un registre réservé pour le pointeur de pile, un autre pour un pointeur vers un ''pool'' de constantes, et quelques autres. D'autres registres servent à mémoriser le haut de la pile, à savoir le sommet de la pile et quelques opérandes situées juste en-dessous. Par exemple, le processeur picoJava II gardait les 64 opérandes au sommet de la pile dans 64 registres généraux, séparés des registres pour les pointeurs et l'état de la JVM. Une optimisation possible, envisagée sur les processeurs picoJava, était une sorte de macro-fusion sous stéroïde. L'idée était de fusionner certaines séries d'instructions Java en une seule instruction machine. Typiquement, pour faire une opération arithmétique, une machine à pile empile deux opérandes et exécute une opération, ce qui prend trois instructions machine. La spécification picoJava 1 a proposé d'envoyer directement la seconde opérande en entrée de l'ALU, ce qui fusionnait l'empilement de la seconde opérande et l'opération arithmétique. Le processeur picoJava II va plus loin et fusionne les trois instructions en une seule instruction RISC, agissant sur des registres. Mais la majorité des processeurs Java ne faisaient pas cela, car cela complexifie grandement le décodeur. Les processeurs ''bigfoot'' de DCT utilisaient un système différent pour émuler une pile avec des registres. Leur banc de registre de 64 bits était coupé en deux : les 16 premiers registres étaient utilisés pour la pile, les 48 suivants étaient utilisés pour autre chose. L'idée est que le registre 0 mémorise un emplacement vide, le registre 1 contient le sommet de la pile, le registre 2 l'opérande sous le sommet de la pile, et ainsi de suite. Les 15 premières opérandes de la pile sont donc mémorisées dans les registres. Les instructions écrivent leur résultat dans le registre adéquat, puis tous les registres sont décalés pour que le résultat passe dans le registre R1. Par exemple, les instructions PUSH chargent une opérande dans le registre 0, puis tous les registres sont décalés de un rang, pour que l'opérande chargée soit dans le registre 1. Le registre 0 devient alors le registre numéro 1, le registre 1 devient le 2, et ainsi de suite. Par contre, les opérations arithmétiques font autrement. Une instruction d'addition, par exemple, lit les opérandes dans le registre 1 et 2, puis place son résultat dans le registre 2 (les deux opérandes sont dépilées). Puis, les registres sont décalés d'un rang pour que ce registre 2 devienne le 1. Les registres ne sont en réalité pas décalés, il n'y a pas de transferts entre registres. A la place, les 16 premiers registres utilisaient une forme limitée de renommage de registres, similaire au banc de registre tournant utilisé sur les processeurs EPIC comme l'Itanium. Les numéros de registres sont renommés, décalés, à chaque fois qu'on empile ou dépile une opérande. Pour faire le renommage de registres, processeur contient un ''register stack counter'' de 4 bits, qui est incrémenté à chaque fois qu'on empile une donnée, et est décrémenté quand on dépile une donnée. Quand on veut accéder à un opérande, le numéro de registre architectural associé est traduit en numéro de registre physique, en additionnant le ''register stack counter'' (modulo 16, pour rester sur 4 bits). ==L'accélération de l'interprétation/JIT : le ''Thumb-EE'' d'ARM== Quelques processeurs ont ajouté des instructions pour faciliter le travail des interpréteurs/JIT. Par exemple, en 2005, ARM a ajouté le mode ''Thumb Execution Environment'', qui faisait cela. Il reprenait le jeu d'instruction compact ''thumb'' et ajoutait quelques instructions et modifiait le comportement d'instructions existantes. Le jeu d'instruction ''Thumb-EE'' a été ajouté sur les CPU ARM en 2005, mais a été déprécié en 2011, par manque d'utilité. Mais la tentative mérite qu'on s'attarde dessus. Les CPU de gamme M, déstinés à l'embarqué, n'ont jamais supporté ''Thumb-EE'', vu que de tels processeurs ne sont pas conçus pour exécuter du code interprété/JIT. Le mode ''Thumb-EE'' est un mode d'exécution, séparé de l'ARM normal. Ainsi, une instruction ''Thumb-EE'' et une instruction ARM peuvent avoir le même encodage en binaire, mais se comporter différemment. Le processeur est à tout moment soit en mode ''Thumb-EE'', soit en mode ARM normal, ce qui précise comment ces instructions doivent se comporter. Des instructions ont été ajoutées par ''Thumb-EE'', pour qu'il fasse son travail. L'entrée et la sortie du mode ''Thumb-EE'' se fait avec deux instructions : ENTERX et LEAVEX. Une fois en mode ''Thumb-EE'', le décodage des instructions ''thumb'' se fait avec les règles du ''Thumb-EE'', et non celles du ''thumb'' normal. Une première différence entre ''thumb'' normal et ''Thumb-EE'' est l'ajout de ce qui s'appelle un ''null check'' pour les instructions mémoire. Le ''null check'' est utilisé pour les instructions en mode d'adressage "base + indice" ou "Base + décalage", ou tout autre mode d'adressage avec un registre de base utilisés dans des calculs d'adresse. L'idée est de vérifier si l'adresse de base vaut zéro ou non. Si c'est le cas, le processeur lève une exception matérielle, qui est traitée par l'interprétation ou le compilateur JIT. Une telle situation est en effet signe d'une erreur d'adressage mémoire, qui doit être traitée par l'interpréteur. Typiquement, c'est signe qu'un pointeur n'a pas bien été initialisé, ce qui peut arriver avec du code interprété ou JIT. Les interpréteurs/JIT traitent généralement la situation en ajoutant un test avant toute instruction mémoire, pour vérifier si le pointeur accédé vaut zéro ou non. Avec ''Thumb-EE'', pas besoin d'ajouter les tests en questions : ils sont réalisés automatiquement lors de chaque instruction mémoire. : Les registres pour la pile, comme le pointeur de pile et le pointeur de ''frame'', sont aussi concernés. L'instruction CHKA vérifie que les accès à un tableau ne débordent pas en dehors du tableau en question. Par exemple, pour un tableau de 1024 éléments, elle vérifie si l'indice est compris entre 0 et 1024. Pour cela, elle vérifie si l'indice de tableau est bien dans l'intervalle adéquat et lève une exception matérielle si ce n'est pas le cas. Son utilité se comprend quand on sait que tous les accès à un tableau sont vérifiés par l'interpréteur. L'interpréteur est censé ajouter des instructions pour vérifier les indices, à savoir deux ou trois branchements. Avec CHKA, le test se fait en une seule instruction. D'autres modifications mineures de l'encodage des instructions sont aussi présentes, ainsi que des modifications pour les instructions LOAD/STORE. Précisément, dans les modes "base + indice" et "base + décalage", le décalage et l'indice subissent maintenant des décalages. L'idée est de simplifier le calcul d'adresse. De quoi économiser quelques instructions lors des calculs d'adresse, ce qui facilite le travail de l'interpréteur/JIT. Et des instructions LOAD/STORE précises voient leur comportement modifié pour faciliter la gestion de la pile de Java ou l'exécution des méthodes locales. Pour faciliter le travail de l'interpréteur, deux instructions HBP et HBLP ont été ajoutées. Elles branchent vers une fonction qui gère les exceptions logicielles (une fonctionnalité présente dans de nombreux langages, comme Java). La première effectue un branchement vers la fonction en question, la seconde sauvegarde l'adresse de retour avant de faire ce branchement. Les deux peuvent brancher vers 256 fonctions pré-déterminées, sans avoir à présenter leur adresse. ==La traduction binaire accélérée par le matériel== Plus haut, nous avons surtout parlé des compilateurs et des interpréteurs. Cependant, nous devons aussi parler de la '''traduction binaire'''. Elle traduit un programme écrit dans un code machine vers un autre code machine. Par exemple, elle traduit un programme compilé pour un CPU x86 vers un code machine ARM. En général, le système d'exploitation est généralement compilé pour le jeu d'instruction natif, mais il exécute des applications prévues pour le x86. Les applications sont traduites par le système d'exploitation, avant d'exécuter le code traduit. : Nous parlerons dans la suite de code machine source et de code machine cible, pour parler respectivement du code à traduire et du code obtenu après traduction. Idem avec d'autres termes comme architecture cible/source, ou jeu d'instruction cible/source. La traduction binaire est surtout utilisée pour des questions d'émulation ou de compatibilité. Par exemple, lorsque les Macintosh sont passés de processeurs Power PC vers des processeurs x86, le système d'exploitation Mac OS utilisait la traduction binaire pour convertir les anciennes applications Power PC vers du code x86. Les utilisateurs n'y ont vu que du feu. Le système de traduction binaire était appelé Rosetta 1. Par la suite, lors de la transition de processeurs x86 vers des processeurs Apple, Rosetta 2 a vu le jour. La traduction binaire a surtout été utilisée pour traduire du code x86 vers un autre jeu d'instruction. Il faut dire que le x86 est le jeu d'instruction dominant. De nombreuses entreprises ont eu pour ambition de briser l'hégémonie du x86 sur PC, en remplaçant le x86 par un jeu d'instruction plus performant, tout en gardant une compatibilité maximale. La traduction binaire était la seule solution pratique. Elle était le plus souvent intégralement réalisée en logiciel, comme c'était le cas sur les architectures Itanium avec le ''IA-32 Execution Layer''. ===Les généralités sur la traduction binaire assistée en matériel=== L'architecture source est presque tout le temps une architecture CISC, assez ancienne, qu'on souhaite émuler. Le choix de l'architecture cible se porte souvent sur une architecture VLIW, fort différente de l'architecture source, et ce pour obtenir une économie de matériel conséquente. Pour s'exécuter rapidement, le code traduit doit exploiter le parallélisme d'instruction, à savoir exécuter plusieurs instructions en même temps dans des unités de calcul séparées. Il est possible d'utiliser un processeur superscalaire avec exécution dans le désordre pour cela, mais au prix d'un cout important en transistors. Alors qu'en utilisant un CPU VLIW, c'est le traducteur binaire qui fait tout le travail d'extraction du parallélisme d'instruction. Et après tout, quitte à avoir un traducteur binaire, autant lui refiler le boulot d'optimisation. Compiler du code à la volée est certes assez lourd, mais qu'il existe des algorithmes efficaces pour regrouper des instructions indépendantes dans une seule instruction VLIW. Au passage, le processeur VLIW a plus de registres que le processeur source. Cela permet de faire du renommage de registres directement en logiciel. Le traducteur binaire n'hésite pas à changer les noms de registres entre instructions source et instruction VLIW cible, afin de supprimer des dépendances de données. En conséquence, cela ouvre des opportunités de parallélisme, qui sont exploitées lors du regroupement des opérations en instructions VLIW. Vous pourriez penser que le choix d'un CPU VLIW pour émuler du CISC est tout sauf optimal. Vous devez penser que la traduction binaire est d'autant plus simple que l'architecture source et cibles sont semblables. Dans les faits, ce n'est pas tellement traduire les instructions qui pose problème, mais plus la gestion du registre d'état, des exceptions matérielles, de la mémoire virtuelle, la différence entre gros-boutisme et petit-boutisme, de même que des différences d'adressage pour les périphériques. Le premier problème est la gestion des conditions, notamment en présence d'un registre d'état. En pratique, la traduction binaire s'utilise pour traduire du code CISC vers du code RISC ou VLIW. Les architectures source ont donc un registre d'état, qui est mis à jour non seulement par des instructions de test, mais aussi des instructions arithmétiques. L'architecture cible n'a elle pas de registre d'état, mais des registres à prédicats. La traduction de l'un vers l'autre est alors quelque peu compliquée. Et elle est d'autant plus compliquée que le registre d'état est la source de dépendances d'instruction implicites, qui réduisent les performances. Par exemple, si une instruction arithmétique modifie le registre d'état, cela peut impacter l'exécution d'une instruction ultérieure, qui lit ce registre d'état. Pour éliminer ces fausses dépendances, le traducteur binaire doit renommer ce registre en logiciel et détecter les dépendances utiles. Et le compilateur doit gérer les cas où il y a beaucoup de distance entre les deux, voire les cas où l'instruction dépendante n'a pas encore été analysée par le compilateur. Un autre problème est lié à la gestion des exceptions matérielles, et précisément des exceptions précises. Pour rappel, une instruction VLIW regroupe plusieurs opérations, opérations qui correspondraient à une instruction machine sur un CPU pas VLIW. En conséquence, plusieurs instructions du langage machine "source" sont regroupées en une seule instruction VLIW. Et il faut tenir compte du cas où une instruction source lève une exception. Dans le code VLIW, cela signifie qu'une opération lève une exception, et il faut annuler partiellement l'instruction VLIW associée. Et par partiellement, on veut dire que seules les opérations suivantes dans l'ordre du programme source doivent être annulées, puis ré-exécutées. Et c'est un sacré casse-tête ! Les processeurs VLIW utilisent comme solution le mécanisme d'exceptions différées des processeurs EPIC, vu il y a quelques chapitres. Pour résumer, le code est exécuté par blocs d'instruction, délimités par des branchements ou tout autre limite/barrière pertinente dans le code. Un bloc de code est compilé en deux versions : une version rapide sans exceptions matérielles, une version lente qui gère les exceptions précises. La version rapide s'exécute sans exécuter les exceptions. Cependant, les exceptions matérielles sont enregistrées, pour être prises en compte à la toute fin du bloc de code. La version rapide est exécutée en premier et elle mémorise si une exception matérielle a eu lieu. Puis une instruction vérifie si une exception a eu lieu et décide quoi faire. Si aucune exception n'a eu lieu, elle passe à la suite du programme, les résultats du bloc de code sont définitivement acceptés. Mais si une exception a eu lieu, tout est annulé. Le processeur est remis dans l'état initial, puis le code est ré-exécuté instruction par instruction de manière à gérer l'exception correctement. ===La traduction par pré-décodage=== Une première solution serait de faire la traduction binaire dans le cache d'instruction. Lors d'un défaut de cache, le code chargé depuis la RAM est traduit en code machine cible. Et c'est ce code machine cible qui est mémorisé dans le cache d'instruction et exécuté par le processeur. La technique marche sur le papier et n'est qu'une amélioration des techniques de pré-décodage vues il y a quelques chapitres. Cependant, le code obtenu est une traduction assez basique, qui n'incorpore pas d'optimisations dignes de ce nom. De plus, elle gère mal le cas où la taille du code source et cible sont potentiellement très différentes. Concrètement, elle est surtout utile pour traduire du code d'un processeur RISC vers un autre processeur RISC, dont les tailles d'instructions sont similaires. Elle ne permet pas d'émuler plusieurs jeux d'instructions différents, le cout en matériel (un circuit de pré-décodage par jeu d'instruction) serait trop important. ===Le projet DAISY d'IBM=== De très rares processeurs étaient conçus pour accélérer cette traduction binaire, afin de garder de bonnes performances. Les premiers à avoir étudié l'idée étaient IBM, avec leur projet DAISY (''Dynamically Architected Instruction Set from Yorktown''). Le projet de base était de convertir à la volée du code compilé pour des CPU Power PC, vers du code VLIW. Les chercheurs d'IBM avaient développé un algorithme de traduction dynamique efficace, ainsi qu'un processeur VLIW disposant d'optimisations spécifiques à la traduction binaire dynamique, à la volée. De nombreuses idées de ce projet ont été reprises ou re-découvertes par la société Transmetta, avec ses processeurs Crusoe et Efficieon, puis par NVIDIA avec son projet Denver. Les ingénieurs de ce projet ont étudié la possibilité d'émuler plusieurs jeux d'instructions différents sur un même processeur, notamment le s390 d'IBM et le x86. En théorie, cela demande juste d'avoir plusieurs programmes de traduction binaire : un pour le x86, un autre pour le s390, éventuellement un autre pour le Power PC. Mais cela ne s'est pas concrétisé. Mais les ingénieurs ont étudié quelle pourrait être l'architecture VLIW idéale pour ça. Par exemple, il fallait des additions 3-opérandes pour simplifier les calculs d'adresse. Une difficulté était la gestion du registre d'état, dont les bits ne sont pas mis à jour de la même manière sur le x86 et le s390 ou le Power PC. La différence entre gros-boutisme et petit-boutisme était aussi un problème, de même que des différences d'adressage pour les périphériques. Pour que la traduction binaire soit efficace, le processeur VLIW intègre diverses optimisations, comme les exceptions différées et les branchements multi-voies. La principale est la suivante : seul le code des boucles ou fonctions exécutées fréquemment est traduit en code VLIW, le reste du code est interprété. En effet, traduire du code binaire prend plus de temps qu'une simple interprétation. Pour du code qui ne sera exécutée qu'une seule fois, il est plus rapide d'utiliser l'interprétation. Par contre, pour du code exécuté beaucoup de fois, le cout de la traduction binaire est amorti, dilué sur N exécutions, compensé par le gain en temps d'exécution de ce code traduit. Au final, cela permet de ne traduire que le code qui le mérite. La détection du code fréquemment exécuté est réalisée dans le cœur VLIW. L'unité de branchement mémorise les derniers branchements rencontrés et le nombre de fois qu'ils ont été exécutés. Si ils ont été exécutés un certain nombre de fois, l'unité de branchement lève une exception matérielle, qui invoque le traducteur binaire. La routine de cette exception est le traducteur binaire proprement dit. En clair, la traduction binaire est démarrée quand le processeur détecte qu'une fonction a été exécutée plus de N fois, via une exception matérielle dédiée. La mesure du nombre d'exécution d'un branchement se fait dans l'unité de calcul dédiée aux branchements, avec l'aide d'une sorte de ''branch adress buffer'' modifié. Pour rappel, le ''branch adress buffer'' mémorise le ''Program counter'' de chaque branchement récemment rencontré. Ici, chaque entrée du ''branch adress buffer'' est associée à un compteur incrémenté à chaque exécution du branchement. ===Les processeurs Crusoe et Efficieon de Transmetta=== Les processeurs Crusoe Et Efficeron sont deux processeurs VLIW produits par la société Transmetta, une société californienne rachetée par NVIDIA. Ils étaient conçus pour exécuter spécifiquement des programmes x86, système d'exploitation inclus. L'idée derrière ce projet était d'exécuter du code x86 sans que les applications, ni même le système d'exploitation et le BIOS soient au courant ! Les processeurs Transmetta étaient en réalité conçus pour exécuter un programme unique, le ''Code Morphing Software'' (CMS), qui traduisait le code x86 en code VLIW. Transmetta ne voulait pas que son processeur VLIW soit exploité directement, sans le CMS. Il n'avait pas rendu public de compilateur pour traduire du C vers du code VLIW, il n'avait pas donné la documentation du jeu d'instruction VLIW. Des efforts de rétro-ingénieurie, documentés sur le site realworldtech, ont cependant permis de comprendre comment étaient encodées les instructions du processeur Crusoe. Comme pour le projet Daisy, le ''Code Morphing Software'' utilisait à la fois interprétation et traduction binaire, selon les besoins. Les instructions sont interprétées lors de leurs premières exécutions, mais le CMS bascule sur de la traduction en code VLIW après un certain nombre d'exécution. Ainsi, les boucles souvent exécutées sont traduites en code VLIW, alors que le reste du code est interprété. Le choix du code à traduire est le fait du CMS, il utilise des heuristiques complexes pour, qui ne sont pas connues dans le détail. Le ''Code Morphing Software'' est mémorisé dans une EEPROM, ce qui en fait un ''firmware'' situé en dessous du BIOS. Le CMS démarre ensuite le BIOS, qui lui-même démarre le système d'exploitation, qui lui-même démarre les pilotes de périphériques et les programmes. Le CMS se réserve les 16 premiers mébioctets de l'espace d'adressage. L'EEPROM du CMS est mappée dedans, mais n'en utilise que la moitié. Le reste est utilisé comme cache, pour mémoriser le code VLIW traduit par le CMS. Les deux processeurs intégraient aussi deux mémoires SRAM, utilisées par le CMS, appelées la ''local program memory'' (LPM) et la ''local data memory'' (LDM). La première contient du code qui gére les interruptions, la mémoire virtuelle, les exceptions matérielles, les problèmes d'alignement mémoire, et quelques fonctions très fréquemment utilisées par le CMS. La seconde est de la mémoire RAM utilisée par ces fonctions de la ''local program memory''. Les mémoires caches L1 et L2 sont séparées de ces deux mémoires. Le processeur Crusoe et Efficieon étaient des processeurs VLIW très simples. Ils n'avaient même pas de MMU, ni de fonctionnalités importantes sur les CPU x86. Pour Crusoe, les instructions VLIW étaient encodées sur 64 ou 128 bits, et regroupaient entre 2 à 4 opérations. Les instructions LOAD/STORE ne supportaient pas d'adressage indicé, juste de l'adressage indirect à registre. Crusoe avait 5 unités de calcul : deux ALU entières, une unité LOAD/STORE, une unité de branchement, une FPU. Efficieon doublait le nombre d'ALU entière et d'unité LOAD/STORE, les instructions VLIW passaient à 256 bits. Le processeur Crusoe contient 160 registres, dont 64 registres généraux, 32 registres flottants. Les registres généraux font 32 bits, ce qui est cohérent avec le fait que les CPU x86 de l'époque étaient des processeurs 32 bits. Quant aux registres flottants, ils faisaient 80 bits, ce qui colle avec la taille des registres flottants de la FPU x87 utilisée à l'époque. Sur les 64 registres généraux, seuls 48 étaient réellement utilisable pour mémoriser des opérandes. Une partie des registres généraux étaient utilisés pour la gestion de la pile, d'autres pour mémoriser l'état du CPU x86 émulé, un registre était un registre zéro non accesible en écriture. La gestion des exceptions est optimisée avec un système d'exceptions différées, le même que celui décrit plus haut. Le processeur utilise un système similaire, pour effectuer des lectures anticipées, qui a été expliqué dans le chapitre sur les processeurs VLIW/EPIC. Sauf qu'il s'agit là d'une optimisation pour gagner en performances en général, pas une aide utile pour gérer des exceptions précises ou toute autre subtilité de la traduction binaire. Pour gérer les exceptions différées et les lectures anticipées, le processeur mémorise l'état du processeur avant de démarrer un bloc de code. Pour cela, le processeur contient des ''shadow registers'', 48 registres entiers et 16 registres flottants. Vous remarquerez qu'il y a autant de registres entiers que de ''shadow registers'' entiers. Les registres entiers/flottants sont mémorisés dans les ''shadow registers'' avant d'exécuter un bloc de code, afin que le processeur puisse revenir à l'état de base. Une autre fonctionnalité liée est le ''gated store buffer''. Le principe est d'éviter toute écriture en mémoire RAM, tant que le code VLIW n'a pas émis d'instruction ''commit''. L'idée est que les écritures sont conservées dans la ''store queue'' de l'unité LOAD/STORE. Pour rappel, la ''store queue'' met en attente les écritures soit tant que les écritures ne sont pas terminées, soit tant que la RAM est occupée. L'idée est que les écritures sont accumulées dans la ''store queue'' tant que l'instruction ''commit'' n'a pas eu lieu. Le ''store queue'' est "déversé dans la RAM" seulement quand l'instruction ''commit'' s'exécute. ===Les CPU NVIDIA Denver=== Nvidia a tenté quelque chose de similaire avec son projet Denver. NVIDIA a racheté l'entreprise Transmetta, ses brevets, et a tenté de refaire la même chose. Le projet initial était de traduire du code x86 vers un jeu d'instruction VLIW propriétaire, comme l'a fait Transmetta. Mais NVIDIA n'a pas réussi à acquérir la licence du jeu d'instruction x86 et s'est rabattu sur le jeu d'instruction ARM. Le processeur né de ce projet est le Tegra K1-64 CPU. Il s'agit d'un processeur composé d'un cœur VLIW, couplé à deux décodeurs d'instructions ARM. Les deux décodeurs ARM traduisent une paire d'instructions ARM consécutives en instructions VLIW, pour les exécuter sur le cœur VLIW. Le CPU peut fonctionner selon deux modes : ARM et VLIW. En somme, le processeur supporte deux jeux d'instruction : le jeu d'instruction ARM, et un jeu d'instruction VLIW. En mode ARM, les instructions ARM sont chargées depuis le cache, traduites en VLIW par les décodeurs ARM, puis exécutées par le cœur VLIW. En mode optimisé, le CPU exécute des instructions VLIW chargées depuis le cache d'instruction, les décodeurs ne sont pas utilisés. : Le CPU supporte en réalité deux jeux d'instruction ARM : ARM8 et 7. Mais ils correspondent au même mode pour le CPU. Un point important est que le processeur peut changer très rapidement de mode, en à peine quelques cycles d'horloges. On n'est clairement pas dans le cas des CPU x86, qui mettent des plombes pour passer du mode 32 à 64 bits et inversement. La commutation est tellement rapide qu'on peut considérer que le processeur supporte deux jeux d'instruction simultanément. Et le support simultané de deux jeux d'instruction facilite l'implémentation de la traduction binaire. Encore une fois, inutile de traduire du code qui ne sera exécutée qu'une seule fois, mieux vaut l'exécuter directement dans le mode ARM. Surtout que le processeur supporte un mode ARM qui permet d'exécuter du code ARM sans perte de performance. Par contre, le code fréquemment utilisé, notamment dans des boucles critiques, est traduit en VLIW. Au final, les performances sont optimisées, en limitant le travail du logiciel traducteur binaire. La détection du code fréquemment exécuté se fait différemment que pour les processeurs Transmetta et Daisy. L'unité de branchement ne détecte pas directement le code beaucoup exécuté, même si elle a un rôle à jouer. A la place, la détection est réalisée en logiciel, par un ''thread'' dédié, qui s'exécute sur un cœur séparé. Il détecte le code fréquemment utilisé en regardent les branchements exécutés récemment. Pour cela, le processeur mémorise l'historique des branchements pris récemment et copie cette historique dans une mémoire tampon dédiée, partagée entre tous les cœurs. Vu qu'elle est partagée, le ''thread'' de détection a accès à l'historique sans pertes de performances, seule la copie de l'historique dans ce tampon a un cout en performance, pas son partage. Le code VLIW obtenu après traduction binaire est mémorisé dans la mémoire RAM, dans une portion spécialement réservée pour. Elle fait 128 mébioctets, et est appelée le '''cache d'optimisation''' par NVIDIA. Le cache d'optimisation est protégé en écriture et seul le traducteur binaire peut écrire dedans. Au passage, le traducteur binaire est du code VLIW, ce qui fait qu'il est placé dans le cache d'optimisation. Le processeur détecte automatiquement quand une fonction pour laquelle le code compilé équivalent est disponible. Pour cela, l'unité de chargement contient une table de correspondance entre l'adresse de la fonction ARM, et l'adresse de son équivalent compilé. Quand une fonction est appelée, l'unité de chargement regarde l'adresse de destination du branchement. Si l'adresse est dans cette table, elle récupère l'adresse de la fonction compilée et branche vers celle-ci. Le processeur dispose d'un cache d'instruction de 128 kibioctets, ce qui est très important, mais nécessaire vu la taille des instructions VLIW. Le coeur VLIW dispose de ses propres décodeurs, d'un ''scoreboard'', de 7 unités de calcul et de registres. Pour les unités de calcul, il a deux FPU, deux ALU, une unité de branchement, et deux unités LOAD-STORE faisant aussi ALU. Un point important est que le CPU dispose de 64 registres entiers et de 64 registres flottants. C'est deux fois plus que supporte le jeu d'instruction ARM. En mode ARM, seule la moitié des registres est utilisée. Mais en mode optimisé, le traducteur binaire utilise bien les 64 registres, grâce à une sorte de renommage de registres logicielle. Pour simplifier la traduction binaire, le processeur supporte les techniques vues précédemment. Le processeur intègre un ''gated store buffer'' similaire à celui des processeurs Transmetta. Les techniques d'exceptions différées sont aussi supportées, comme sur les processeurs Transmetta. Il a aussi une unité de préchargement décrite par NVIDIA comme agressive, avec support du préchargement de type anticipé (''runahead''). ===L'émulation de la mémoire virtuelle=== Pour finir, il faut parler de la mémoire virtuelle avec la traduction binaire. Sur le processeur source, le processeur gère à la fois des adresses physiques et virtuelles et sa MMU fait la conversion entre les deux. Sur le processeur VLIW, la MMU est simulée par le programme de traduction binaire, partiellement ou totalement. L'implémentation exacte varie suivant que l'on parle du projet Daisy, des processeurs Transmetta ou de Denver. Sur les processeurs Transmetta, il n'y a pas de MMU ni de mémoire virtuelle. A la place, le programme de traduction binaire émule la mémoire virtuelle du processeur source. Le processeur VLIW ne gère que des adresses physiques, rien d'autre. L'espace d'adressage physique du processeur VLIW a la même taille que celui du processeur émulé, ici des processeurs x86. Il est vraisemblable que les adresses physiques utilisées par le processeur x86 sont les mêmes que celles du processeur VLIW. Sur le projet Daisy, le processeur VLIW gère la mémoire virtuelle via pagination, comme les processeurs émulés. Ce qui fait qu'il gère un espace d'adressage virtuel et un espace d'adressage physique. Pour éviter toute confusion, nous parlerons d'adresse physique/virtuelle VLIW pour les adresses physiques/virtuelles du processeur VLIW, d'adresse virtuelle/physique source pour celle du jeu d'instruction traduit, à savoir du Power PC ou du s390, éventuellement de l'x86. Il faut alors faire le lien entre adresses physiques source et adresses virtuelles VLIW. Pour cela, rien de plus simple : il y a correspondance parfaite. L'adresse physique source numéro N correspond à l'adresse virtuelle VLIW numéro N. Cependant, cela signifie que tout l'espace d'adressage virtuel VLIW serait utilisé par le code à traduire. En réalité, il faut ajouter de la place pour le code traduit et le programme de traduction binaire. La conséquence est que l'espace d'adressage virtuel VLIW est plus large que l'espace d'adressage source. L'espace d'adressage virtuel VLIW est découpé en trois sections : une section pour le code Power PC à traduire, une autre pour le code traduit en VLIW, et une dernière réservée au traducteur binaire. Le programme de traduction est placé dans une ROM mappée en mémoire dans la seconde section. Le reste de la seconde section est réservé à une mémoire RAM utilisée par le programme de traduction. [[File:Mémoire virtuelle sur le projet Daisy.png|centre|vignette|upright=1.5|Mémoire virtuelle sur le projet Daisy]] Il faut noter que chaque page physique source, une fois traduite, correspond à N pages virtuelles VLIW. La raison est que le code traduit prend plus de place que le code machine originel. En conséquence, on doit utiliser une page finale plus grande. Pour le reste, des pages contiguës en mémoire physique source sont elles aussi contiguës en mémoire virtuelle VLIW. Le calcul d'adresse est donc simple : l'adresse physique source est multipliée par N, puis on ajoute l'adresse de base à laquelle commence la section pour le code traduit. ==Les processeurs à jeu d'instruction reconfigurable== Nous venons de voir comment accélérer en matériel la traduction binaire. Mais il existe une alternative qui permet de se passer de traduction binaire, complétement. L'idée est d'avoir un processeur avec un '''jeu d'instruction reconfigurable'''. Le nom est assez parlent : on peut configurer le processeur de manière à supporter le jeu d'instruction de son choix ! Par exemple, on pouvait reconfigurer un CPU de manière à ce qu'il supporte le jeu d'instruction x86, ou qu'il exécute du ''bytecode'' Java, ou du code ARMv8, etc. De tels processeurs sont encore plus rare que ceux vu avant dans ce chapitre, mais ils ont existé ! Les processeurs de ce type avaient un '''microcode réinscriptible''', à savoir que c'était des processeurs microcodés pour lesquels on pouvait modifier le microcode. L'idée était qu'en changeant le microcode, on changeait le jeu d'instruction du processeur. Pour cela, le microcode était associé à une mémoire SRAM, en plus de la mémoire ROM du ''control store''. La SRAM était appelée la micro-SRAM, alors que le ''control store'' basique était appelé la micro-ROM. Le microcode était copié dans cette micro-SRAM au démarrage du CPU, puis les instructions étaient décodées en utilisant le microcode dans la micro-SRAM. En modifiant le contenu de la micro-SRAM, on pouvait altérer le microcode, voire le remplacer totalement. Il s'agit là de l'implémentation la plus simple. Mais le Burroughs B1700 a procédé autrement et il est intéressant d'étudier en détail son architecture. Son architecture est décrite dans le livre "Interpreting Machines : Architecture and Programming of the Bl700/Bl800 Series", écrit par Elliott I. Organick et James A. Hinds. Une partie de ce qui vfa suivre est un très court résumé de ce livre. ===Le Burroughs B1700 : architecture externe=== Il disposait de 4 registres généraux, nommés X, Y, T et L, tous de 24 bits. Les registres T et L étaient découpés en 6 sous-registres de 4 bits, nommés LA LB LC LD LE et LF pour le registre L, et TA TB TC TD TE et TF pour le registre T. Ils étaient adressables comme n'importe quel registre. Un point important est que le registre T avait des possibilités d’extraction spécifiques. Il était possible d'extrait N bits, placés n'importe où dans ce registre, et de les copier dans un autre registre. Nous verrons pourquoi cette possibilité est intéressante et ne sert que pour le registre T. Les registres X et Y étaient reliés à une ALU de 24 bits capable d'effectuer des opérations arithmétiques basiques. Fait étonnant, l'ALU faisait tous les calculs en même temps et fournissait ses résultats dans 7 registres : un registre SUM pour l'addition, un registre DIFFERENCE pour la soustraction, et un registre par opération bit à bit. Les registres pour les opérations bit à bit sont : CMPX et CMPY pour l'opération NOT sur chaque opérande, XANY pour le ET bit à bit, XEOY pour le XOR, XORY pour le OU logique. Les additions et soustractions pouvaient se faire sur des opérandes codés en binaire ou en BCD, suivant le code opération utilisé. A cela, il fallait ajouter deux registres de "résultat" MSKX et MSKY. Ils fournissaient un masque dépendant de la taille du résultat en bits. Cela permettait de gérer des données de 0, 4, 8, 12, 16, 20, et 24 bits, alors que l'ALU faisait 24 bits. Et cela n'était pas limité à 16 ou 8 bits, mais n'importe quelle taille inférieure ou égale à 24 bits était supportée, même des tailles non-conventionnelles comme des résultats codés sur 3 bits, 9 bits, 12 bits, etc. L'ALU peenait en opérande les registres X et Y, mais aussi un registre CYF pour la retenue entrante. En sortie, il fournissait aussi deux retenue sortantes : une pour l'addition et une autre pour la soustraction. Elles étaient mémorisées dans les registres CYL et CYD, respectivement. L'ALU fournissait aussi 12 conditions, qui alimentait un registre d'état. Le registre d'état était segmenté en trois registres appelés XYST, XYCN et BICN. Le processeur contenait de nombreux autres registres de 4 bits, dont les sous-registres de T et L. En tout, il y a 27 registres de 4 bits adressables ! Et le processeur disposait d'une ALU 4 bits pour manipuler ces registres de 4 bits ! Elle est capable de faire des MOV entre registres de 4 bits, des opérations bit à bit, et peut aussi tester si un bit vaut 0 ou 1. La dernière possibilité est très utile pour implémenter les branchements. Pour communiquer avec les entrées-sorties, il disposait de deux registres CMND et DATA : CMND pour envoyer une commande à une entrée-sortie, DATA pour lire ou écrire une donnée. Pour la mémoire, il avait deux registres séparés : READ et WRITE. READ contient la donnée lue lors d'une lecture,; WRITE est pour une donnée à écrire lors d’une écriture. Il s'agit de registres d’interfaçage mémoire, qui ont été promus au rang de registres architecturaux. Les deux registres sont gérés par le microcode, qui est le langage principal de la machine. Les instructions machines configurées font sans, mais le microcode doit y avoir accès facilement. Et vu que le microcode est le langage principal de ce processeur, ces deux registres deviennent architecturaux. ===Le burroughs B1700 : un ''split-level control store''=== Le burroughs B1700 lisait le microcode directement depuis la mémoire RAM ! Pour être précis, il avait plusieurs modèles : le B1726, le B1710 et le B1800. Le B1710 plaquait le microcode en mémoire RAM, ce qui fait que chaque exécution d'une instruction lisait les micro-opérations en mémoire RAM ! Le B1800 faisait pareil, mais avait ajouté un cache dédié au microcode, afin d'améliorer les performances. Le B1726 avait un microcode scindé en deux : une partie était en mémoire RAM, l'autre était dans un microcode réinscriptible classique, avec une micro-SRAM intégrée au processeur. Le processeur peut exécuter le microcode directement depuis la mémoire RAM, mais avec une pénalité en termes de performances. La documentation appelle cela un '''''split-level control store'''''. Le programmeur devait découper le microcode en segments, certains étant placé dans la micro-SRAM, les autres étant en mémoire RAM. Un tel microcode permettait de placer le microcode le plus fréquemment utilisé dans le processeur, alors que le reste du microcode était placé en mémoire RAM. Les instructions fréquentes étaient ainsi décodées avec la micro-SRAM, alors que les instructions peu fréquentes étaient décodées en lisant le microcode associé en mémoire RAM. La pénalité en termes de performance est alors concentrée sur les instructions les plus rares, ce qui est un très bon compromis. Mieux que ça, le processeur autorisait d'utiliser la technique de l'''overlaying'' pour le microcode ! Il y avait même une micro-opération OVERLAY dédiée pour ! Elle permettait de copier du microcode de la mémoire RAM vers la micro-SRAM ! OVERLAY avait besoin de trois opérandes : la taille du segment à copier, son adresse source en mémoire RAM, son adresse de destination dans la micro-SRAM. Le microcode pouvait servir pour émuler un autre jeu d'instruction, mais il était aussi possible d'implémenter un véritable interpréteur, grâce aux techniques du paragraphe précédent. Le processeur était même conçu pour ! L'interpréteur était placé dans un "segment" en mémoire RAM, dont le processeur connaissait la position. Il incorporait pour cela deux registres, un pour l'adresse de base de l'interpréteur, un autre pour son adresse de fin. Il incorporait une protection mémoire limitée sur ce segment, à savoir que seul l'interpréteur pouvait lire ou écrire dans ce segment. La micro-SRAM mémorisait au maximum 7680 micro-opérations, ce qui fait que les 7680 premières "adresses" correspondaient à la micro-SRAM. Les suivantes étaient des adresses de mémoire RAM qui contenaient le reste du microcode. En pratique, l'adresse qui séparait les deux portions du microcode était configurable ! On pouvait limiter la taille du microcode en micro-SRAM à un multiple de 512 micro-opérations ! Les adresses restantes étaient alors réattribuées à la RAM. Par exemple, on pouvait attribuer 512 adresses à la micro-SRAM, les 7680 - 512 adresses restantes étaient alors attribuées à la mémoire RAM. Un registre limite, nommé TDPM, indiquait quelle était l'adresse de démarcation entre les deux. Il contenaient 4 bits, dont la valeur était multipliée par 512, pour obtenir l'adresse limite. ===Le microcode réinscriptible=== Le microcode utilise des micro-opérations de 16 bits de long, ce qui est très peu et bien en-dessous des micro-opérations des autres processeurs. Les micro-opérations sortant du microcode sont envoyées au registre M, un registre de micro-opération de 16 bits. les micro-opérations écrites dans ce registre sont ensuite décodées par le hardware et exécutées. Fait intéressant, il était possible de faire un OU logique entre la micro-opération sortante et un registre du processeur, le résultat étant mémorisé dans le registre M. Cela permettait de faire du code automodifiant dans le microcode ! Utiliser du code automodifiant avec un microcode réinscriptible est parfaitement possible, et c'est utile pour émuler des modes d'adressage complexe dans le microcode. Cela permettait par exemple de remplacer une adresse dans une micro-opération. L'adresse est alors surimposée avec un OU dans le champ d'adresse, alors que le reste de l'instruction n'est pas modifié. Mais modifier directement le microcode est rarement une bonne idée. Aussi, pour éviter cela, l'idée est de modifier les instructions avec un OU logique avec un autre registre. Les micro-opérations du microcode ne sont donc pas modifiées dans le microcode, mais en sortie du microcode. Afin de gérer les interruptions, ces dernières étaient partiellement détectées en logiciel. Les interruptions ne s'exécutaient pas automatiquement, pour émuler correctement les interruptions. Une interruption doit être prise en compte à la toute fin de l'exécution d'une instruction, quand celle-ci a finit son travail. Mais vu qu'ici, les instructions sont émulées avec des séries de micro-opérations, il y a un problème : il est possible d'interrompre une instruction émulée en plein milieu. Pour éviter cela, le processeur ne déclenche pas les interruptions matérielles immédiatement, elles sont mises en attente. Une micro-opération dédiée vérifie si une interruption est en attente, et l'exécute alors cas échéant. La micro-opération est utilisée comme micro-opération finale d'une instruction émulée. ===Une architecture bit-adressable=== Si avoir un microcode réinscriptible est suffisant, quelques choix de ''design'' facilitent l'émulation de jeux d'instructions variés. Par exemple, le CPU Burroughs B1700 avait ajouté une fonctionnalité très importante : il était bit-adressable. En clair, il pouvait adresser la mémoire bit par bit, si nécessaire. L'avantage est que cela permettait d'émuler des architectures dont la taille des registres/''byte''/mots était très différente. Un autre avantage est que cela facilitait la gestion des instructions de taille variable. Détaillons rapidement le premier avantage. Le processeur était un CPU 24 bits, mais il pouvait émuler des processeurs 16 bits, 8 bits, 4 bits, 1 bit, 9 bits, ou toute autre valeur. Pour cela, le processeur lisait des mots de 24 bits et utilisant un ''barrel shifter'' pour sélectionner les bits adéquats. Par exemple, pour un accès de 16 bits, la donnée lue en mémoire était masquée de manière à ne garder que les 16 bits de poids faible. Le ''barrel shifter''/circuit de masquage était placé directement avant le bus mémoire, sur le trajet des données/instructions. Le ''barrel shifter'' était commandé par un registre ''Field Unit'', qui précisait quelle était la taille des données à charger. Le processeur gère un micro-''program counter'' appelé le registre A. Les micro-opérations du processeur font 16 bits de long, et elles sont alignées en RAM sur 16 bits, ce qui fait que les 4 bits de poids fort de ce registre valent 0. Le registre fait 18 bits, mais seuls les 14 bits de poids fort sont utiles. Les adresses mentionnées plus haut, qui adressent les micro-opérations en mémoire RAM, sont des adresses de mots de 16 bits, pas les adresses de bit individuels. ===La gestion de la taille des instructions=== Les instructions à décoder n'ont pas de longueur fixe, à savoir qu'elles peuvent faire 16 bit avec tel jeu d'instruction, 8 bits avec un autre, 32 bits sur un autre, etc. Elles peuvent aussi être de longueur fixe ou variable ! Le B1700 intégrait donc un support des instructions de longueur variable, qui était "détourné" pour gérer des jeux d'instruction différents. Notons que vu que l'architecture est bit-adressable, les contraintes d'alignement des instructions disparaissent, ce qui a des avantages qu'on va détailler dans ce qui suit ! L'implémentation des instructions de taille variable ajoutait un registre avant le registre d'instruction. Le registre d'instruction proprement dit était un registre M de 24 bits. Il était précédé par un registre T, dans lequel l'instruction était assemblée, en combinant plusieurs morceaux. Si l'instruction est codée est codée sur moins de 24 bits, le registre T se contente de masquer les bits en trop. Si l'instruction chargée dépasser 24 bits, le chargement se faisait en plusieurs passes, avec plusieurs lectures en mémoire. Dans ce qui suit, on suppose que l'instruction est composée de '''champs''' : l'opcode, les opérandes, les adresses absolues, les constantes immédiates, les numéros de registre, etc. L'idée est que l'instruction est chargée dans le registre T, puis découpée en champs, qui sont copiés dans les registres du processeur ou envoyés au microcode. Je rappelle que le registre T est relié à un ''barrel shifter'', différent au précédent, qui permettait de masquer, décaler et extraire une donnée de ce registre. Et il y a la possibilité d'extraire une suite de bit de ce registre T, pour la copier dans un autre registre. Par exemple, prenons une instruction composée d'un opcode, d'une constante immédiate et d'une adresse immédiate. L'opcode est lu et interprété par le microcode. Le microcode extrait la constante immédiate du registre T et la copie dans le registre X. Ensuite, l'adresse est extraite et envoyée au registre FA, une lecture est démarrée, et l'opérande est copiée dans le registre Y. Enfin, l'instruction est exécutée par le microcode. Le tout prend plusieurs micro-opérations : une première pour lire l'opcode, une seconde pour lire la première opérande, une troisième pour lire la seconde opérande, una quatrième pour tout exécuter. Notons que suivant la taille des champs, on peut en charger plusieurs à la fois. Par exemple, imaginons qu'on ait chargé 24 bits, contenant un opcode de 8 bits et une constante immédiate de 16 bits : pas besoin de faire deux accès mémoire, le registre T est juste découpé en un opcode et une constante. Le registre T est fait pour ce genre d'extraction. ===Le chargement d'un champ=== L'architecture est bit-adressable car elle gére des champs qui ne font pas forcément 8 ou 16 bits. Par exemple, certains jeux d'instructions ont des opcodes codés sur 6 ou 7 bits, voire 9-10 bits. Par exemple, prenons un opcode de 7 bits, suivi par une adresse de 12 bits et une constante immédiate de 15 bits : aucun de ces champs ne sera aligné sur 8/16/24 bits. Utiliser une architecture bit-adressable règle le problème d'alignement. Et en général, les contraintes d'alignement des instructions disparaissent ! Quand on charge une instruction, peut importe qu'elle soit ou non alignée sur 24 bits, elle sera placée dans les bits de poids faible, il suffira de masquer les bits de poids fort inutiles. Par exemple, si on charge une instruction de 16 bits, les 16 bits seront copiés dans les 16 bits de poids faible du registre T, il suffira de masquer l'octet de poids fort. Pas besoin de faire de décalages pour gérer des instructions mal alignées... Pour charger une instruction, il faut connaitre plusieurs informations : l'adresse de son premier bit, sa taille. Pour cela, le B1700 contient deux registres, nommés ''Field Adress'' (FA) et ''Field Length'' (FL), qui indiquent l'adresse du premier bit et la taille de l'instruction à charger. Le premier fait 24 bits, ce qui permet de gérer des adresses de 24 bits. N'oubliez pas que le processeur est bit-adressable, ce qui permet d'adresser 16 Mébi-bits, soit 2 mébioctets. Le registre pour la taille fait lui 16 bits, ce qui permet de gérer des instructions de 65536 bits, soit 8192 octets ! Leur nom commence par ''field'', car le processeur peut gérer des instructions de taille variable et/ou plus longues que 24 bits. Dans ce cas, le chargement de l'instruction se fait champ par champ. Pour donner un exemple, prenons une instruction composée de 20 champs de 12 bits chacun. Les champs sont chargés un par un dans le registre T. Le registre FA est alors initialisé avec l'adresse de l'instruction, il est incrémenté de 12 à chaque cycle. Le registre FL est initialisé avec la taille de l'instruction, soit 20 * 12 = 240 bits, et il est décrémenté de 16 à chaque cycle. Précisons cependant que la taille d'un champ peut être changée d'un cycle à l'autre. Par exemple, le processeur peut charger un opcode de 8 bits, l'interpréter, détecter que l'instruction demande ensuite de charger une adresse de 16 bits. Dans ce cas, le registre FL est immédiatement altéré pour charger 16 bits, au lieu des 8 bits de l'opcode immédiatement précédent. ==Les jeux d'instructions dédiés à un langage de programmation== De rares processeurs sont conçus pour un langage de programmation en particulier. On appelle ces processeurs, conçus pour des besoins particuliers, des '''processeurs dédiés'''. Par exemple, les fameux ''Burrough E-mode'' B5000/B6000/B7000 étaient spécialement conçus pour exécuter de l'ALGOL-60. Leurs cousins B2000/B3000/B4000 étaient eux conçus pour le COBOL. Des langages fonctionnels ont aussi eu droit à leurs processeurs dédiés. Le prolog en est un bel exemple, avec les superordinateurs de 5ème génération qui lui étaient dédié. On peut aussi citer les machines LISP, dédiés au langage LISP, qui datent des années 1970. Elles étaient capables d’exécuter certaines fonctions de base du langage directement dans leurs circuits : elles possédaient notamment un ''garbage collector'' câblé dans ses circuits ainsi que des instructions machines supportant un typage déterminé à l’exécution. Les processeurs dédiés ont eu leur heure de gloire au début de l'informatique, à une époque où les langages de haut niveau venaient d'être inventés. À cette époque, les compilateurs n'étaient pas performants et ne savaient pas bien optimiser le code machine. Il était alors rationnel, pour l'époque, de rapprocher le code machine cible et le langage de programmation de haut niveau. De nombreuses architectures dédiés ont ainsi été inventées, avant que les concepteurs se rendent compte des défauts de cette approche. Les défauts en question ne sont pas nombreux, mais assez simples à comprendre. Premièrement, elles sont très rapides pour un langage de programmation en particulier, mais sont assez mauvaises pour les autres, d'où un problème de "compatibilité". Ajoutons à cela que les langages de programmation peuvent évoluer, devenir de moins en moins populaires/utilisés, ce qui rend la création d'architectures généralistes plus pertinente. Enfin, les architectures dédiées sont évidemment des processeurs CISC, pour implémenter les nombreuses fonctionnalités des langages évolués. Et les défauts des CISC sont assez rédhibitoires à l'heure actuelle. À l'heure actuelle, les algorithmes des compilateurs se sont améliorés et savent nettement mieux utiliser le matériel. Ils produisent du code machine efficace, ce qui rend les architecture dédiées bien moins intéressantes. Si on ajoute les défauts de ces architectures dédiées, par étonnant que les architectures dédiées aient presque disparues. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'accélération matérielle de la virtualisation | prevText=L'accélération matérielle de la virtualisation | next=Le matériel réseau | nextText=Le matériel réseau }} </noinclude> l54opnqm7miyylngij0lr772zur2e0y 764036 764035 2026-04-19T19:22:29Z Mewtow 31375 /* Le burroughs B1700 : un split-level control store */ 764036 wikitext text/x-wiki De nos jours, la majorité des programmeurs programment dans des langages de haut niveau. Il est très rare pour eux d'avoir à utiliser de l'assembleur, et encore moins un langage machine. Les programmes écrits dans un langage de haut niveau sont traduits en langage machine par un logiciel appelé le compilateur. [[File:Compilation.PNG|centre|vignette|upright=2.5|Compilateur : principe.]] Il se trouve que quelques rares processeurs sont conçus pour faciliter le travail du compilateur. Et ce sont ces processeurs que nous allons voir dans ce qui suit. Ils représentent une catégorie de jeux d'instruction à part, qui n'a pas vraiment de nom. ==La compilation et l’interprétation== Un compilateur traduit donc du code source, écrit dans un langage de haut niveau, vers du langage machine. La traduction est rarement directe. En général, le compilateur traduit le code source en assembleur, qui est lui-même traduit en langage machine. L'assembleur est une représentation textuelle du langage machine alors que le code machine est du binaire exécutable par le processeur. La traduction se fait donc en deux étapes, la '''compilation''' proprement dite et l''''assemblage''', réalisées respectivement par un compilateur et un assembleur. Il faut parfois rajouter une troisième phase d'édition des liens, que nous passons volontairement sous silence. {| |[[File:Code C.png|vignette|200px|Le code source, ici en C.]] |[[File:Assem.png|vignette|250px|Le code assembleur.]] |[[File:Binary file - hello world (C programming).png|vignette|150px|Le code machine.]] |} ===L'interprétation et la compilation à la volée=== Une alternative à la compilation est l''''interprétation''', qui transforme le code source en code machine à la volée. Avec l'interprétation, le code source est passé à un logiciel appelé l''''interpréteur''', qui exécute le code source ligne de code par ligne de code. Une ligne de code est traduite en code machine, qui est exécutée, puis l'interpréteur passe à la ligne suivante. [[File:Kaantotulkkaus.png|centre|vignette|upright=3|Compilation versus interprétation.]] Un défaut de l'interprétation est son cout en performance, qui est assez important. Aussi, l'interprétation stricte a évolué vers un hybride entre interprétation et compilation. L'idée est qu'une partie peu importante du code source est interprétée, alors que le code important est compilé juste avant d'être exécuté. Pour le dire autrement, le code source est partiellement compilé à la volée, juste avant son exécution. Aussi, on parle de '''compilation à la volée'''. Le terme en anglais est ''Just In Time compilation'', abrévié en JIT. En soi, le JIT est associé à de l'interprétation, les JIT sont en réalité des hybrides interpréteurs-compilateurs. Un point important est que compiler du code à la volée est assez lourd, cela a un cout en performance. Par contre, le code compilé s'exécute plus vite que du code interprété. Si on exécute le code compilé une seule fois, le cout de la compilation l'emporte sur le gain à l'exécution. Mais le code compilé est réutilisé autant de fois que nécessaire. Le gain à l'exécution est donc multiplié par le nombre d'exécutions, alors que le cout de la compilation est répartit sur plusieurs exécutions, il est amorti. Les portions du code sont choisies de manière à avoir un gain en performance maximal. L'idée générale est que le code exécuté très souvent est compilé, alors que du code exécuté pas souvent est interprété. Typiquement, les boucles les plus souvent exécutées sont compilées, le code exécuté une seule fois est interprété. ===Les langages intermédiaires=== Les compilateurs modernes passent par un '''langage intermédiaire''' pour faire la transformation en code machine. Le compilateur traduit le langage de haut niveau en langage intermédiaire, puis traduit le langage intermédiaire en code machine cible. Le langage intermédiaire est parfois appelé le '''''bytecode''''', et ce terme recouvre aussi le code écrit avec du ''bytecode''. L'interprétation aussi peut passer par l'intermédiaire d'un un ''bytecode'', ce n'est pas limité aux compilateurs proprement dit. {| |[[File:Intermediate representation scheme.png|centre|vignette|upright=1.5|Représentation intermédiaire d'un compilateur.]] |[[File:Code interpreter scheme.png|centre|vignette|upright=1.5|Fonctionnement d'un interpréteur.]] |} Faire ainsi a de nombreux avantages, le principal étant d'avoir un compilateur capable de traduire un langage de haut niveau vers plusieurs jeux d'instructions différents. Cela permet d'avoir, par exemple, un compilateur qui traduit du C soit en code machine x86, soit en code machine pour un CPU ARM, soit pour un CPU POWERPC, etc. [[File:Compiler design.svg|centre|vignette|upright=3|Compiler design]] Pour l'interprétation, l'usage d'un ''bytecode'' a d'autres avantages. Pour l'exploiter, il faut distribuer non pas le code source, mais le ''bytecode''. En clair, l'interpréteur ne prend pas en entrée le code source, mais du ''bytecode'' déjà compilé. La traduction est alors beaucoup plus simple, car le code source a déjà été partiellement compilé, par la traduction en ''bytecode''. Interpréter du code source est en effet assez compliqué : il faut effectuer des étapes d'analyse lexicale, sémantique, et bien d'autres. Avec du ''bytecode'', ces étapes ont été réalisées lors de la compilation du ''bytecode'', l'interpréteur a alors peut de choses à faire. L'interprétation du ''bytecode'' se fait instruction par instruction, au niveau du ''bytecode''. Une instruction du ''bytecode'' est traduite en une instruction machine équivalente, qui est exécutée. Si l'instruction est un peu complexe, il exécute une fonction/procédure qui fait la même chose. Pour résumer, le ''bytecode'' est ensuite traduit à la volée et exécuté instruction par instruction par un logiciel appelé l'interpréteur. [[File:Control table.png|centre|vignette|upright=2|Control table]] Le langage intermédiaire peut être vu comme l'assembleur d'un processeur, n'existe pas forcément dans la réalité, mais dont le jeu d'instruction est décrit en détail. Le processeur en question est appelé une '''machine abstraite''', ou encore une ''machine virtuelle''. Nous utiliserons le terme de machine abstraite dans ce qui suit. La machine abstraite n'est pas la même suivant que l'on cible la compilation ou l'interprétation/JIT. Pour l'interprétation/JIT, la machine abstraite est souvent une machine à pile, car cela simplifie grandement la traduction en code machine final. L'interprétation demande que la traduction du ''bytecode'' en code machine soit la plus simple possible. Et cela demande de prendre en compte pas mal de détails. Par exemple, les différents jeux d'instruction existants n'ont pas le même nombre de registres, ce qui pose problème lors de la traduction du ''bytecode''. Lors de la transformation en code machine, un algorithme d'allocation de registres se débrouille pour traduire le code intermédiaire en code qui utilise un nombre limité de registres. Pour améliorer cette allocation de registres, il y a deux solutions niveau ''bytecode'' : soit utiliser un nombre illimité de registres, soit utiliser une machine à pile. Pour les machines à pile, il existe un algorithme simple et rapide pour traduire un code écrit pour une machine à pile en un code écrit pour un processeur avec des registres, qui se débrouille pas trop mal pour allouer efficacement les registres. C'est un avantage assez important pour les langages interprétés. ==Les processeurs qui exécutent du bytecode== Le ''bytecode'' est un code machine, ce qui signifie qu'il peut en théorie s'exécuter sur un processeur qui implémente le jeu d’instruction associé. Si le jeu d'instruction d'un ''bytecode'' est souvent une description censée être fictive, elle n'en reste pas moins un jeu d'instruction et des caractéristiques précises, qu'on peut l'implémenter en matériel ! Néanmoins, tous les ''bytecode'' ne sont pas égaux de ce point de vue. Certains sont faciles à implémenter en matériel, d'autres non. Les ''bytecodes'' implémentés en matériel sont ceux dont les machines abstraites sont des machines à pile. Un premier exemple est celui des processeurs Pascal MicroEngine, qui exécutaient directement le bytecode du langage Pascal, le fameux UCSD P-code. Un second exemple est la '''machine SECD''', qui sert de langage intermédiaire pour certains compilateurs de langages fonctionnels. Elle a été implémentée en matériel par plusieurs équipes, notamment par les chercheurs de l'université de Calgary en 1989. Dans le même genre, quelques processeurs simples étaient capables d’exécuter directement le ''bytecode'' du langage FORTH. Le FORTH, un des premiers langages à pile de haut niveau, possède de nombreuses implémentations hardware et est un des rares langages de haut niveau dont le ''bytecode'' a été utilisé comme langage machine sur certains processeurs. Par exemple, on peut citer le processeur FC16, capable d’exécuter nativement du ''bytecode'' FORTH. ===Les processeurs Java=== Le cas le plus impressionnant est celui de la machine virtuelle Java, qui est un design de processeur comme un autre. En temps normal, le ''bytecode'' Java est compilé ou interprété, mais certains processeurs exécutaient du ''bytecode'' Java directement, sans interprétation ni compilation. Ils sont appelés des '''processeurs Java'''. On peut citer les processeurs ARM disposant de l'extension Jazelle, du JEMCore et du aJ-100 de aJile Systems, le picoJava II de Sun, et quelques autres. Il y a aussi eu quelques projets de recherche ou processeurs open source, comme Komodo, jamuth, le ''Java Optimized Processor'' et quelques autres projets du même genre. L'implémentation de la machine virtuelle Java n'était cependant pas complète. Quelques instructions complexes n'étaient pas gérées par le processeur et devaient être émulées en logiciel. Mais la grosse majorité des instructions l'était. Les instructions non-supportées étaient émulées avec la méthode ''trap and emulate'', à savoir qu'un opcode inconnu déclenchait une exception matérielle ''opcode invalide'', dont la routine pouvait émuler les instructions qui doivent l'être. Le processeur aJ-100 de aJile est capable d'exécuter la quasi-totalité des instructions de la JVM Java, seules deux instructions faisant exception. Les '''accélérateurs Java''' sont une solution intermédiaire, qui permet de traduire à la volée du ''bytecode'' Java en instructions RISC, exécutées sur un autre processeur. Il s'agit formellement de traduction binaire, mais je préfère en parler ici. L'idée est de combiner un processeur RISC avec un coprocesseur Java. Le coprocesseur lit le ''bytecode'' Java et le traduit en instructions RISC, ces dernières étant exécutées sur le processeur RISC. Le JA108 de Nozomi était un coprocesseur de ce genre, au même titre que le coprocesseur JSTAR de JEDI. L'extension Jazelle de certains CPU ARM fonctionnait vraisemblablement sur un principe similaire, sauf que le coprocesseur était intégré dans le processeur ARM, entre le cache d'instruction et le décodeur d'instructions ARM. L'intérêt des processeurs Java n'est pas qu'une question de performance. Certes, ils permettent d’exécuter plus vite les programmes compilés en ''bytecode'', comme des programmes Java pour la JVM Java. Mais l'intérêt est aussi de faire des économies de mémoire RAM et ROM. Je rappelle que la machine virtuelle Java est une machine à pile, ce qui fait que sa densité de code est excellente. Les programmes en ''bytecode'' Java sont donc très petits, et prennent peu de place en mémoire ROM. De plus, utiliser un processeur Java permet de se passer d'interpréteur ou de JIT Java, ce qui économise encore plus de mémoire. ===Transformer un processeur RISC en machine à pile=== La plupart des processeurs Java utilisent en interne une architecture à registre généraux, qui émule une machine à registre en microcode. Quelques registres sont réservés pour l'état de la machine virtuelle, avec au minimum un registre réservé pour le pointeur de pile, un autre pour un pointeur vers un ''pool'' de constantes, et quelques autres. D'autres registres servent à mémoriser le haut de la pile, à savoir le sommet de la pile et quelques opérandes situées juste en-dessous. Par exemple, le processeur picoJava II gardait les 64 opérandes au sommet de la pile dans 64 registres généraux, séparés des registres pour les pointeurs et l'état de la JVM. Une optimisation possible, envisagée sur les processeurs picoJava, était une sorte de macro-fusion sous stéroïde. L'idée était de fusionner certaines séries d'instructions Java en une seule instruction machine. Typiquement, pour faire une opération arithmétique, une machine à pile empile deux opérandes et exécute une opération, ce qui prend trois instructions machine. La spécification picoJava 1 a proposé d'envoyer directement la seconde opérande en entrée de l'ALU, ce qui fusionnait l'empilement de la seconde opérande et l'opération arithmétique. Le processeur picoJava II va plus loin et fusionne les trois instructions en une seule instruction RISC, agissant sur des registres. Mais la majorité des processeurs Java ne faisaient pas cela, car cela complexifie grandement le décodeur. Les processeurs ''bigfoot'' de DCT utilisaient un système différent pour émuler une pile avec des registres. Leur banc de registre de 64 bits était coupé en deux : les 16 premiers registres étaient utilisés pour la pile, les 48 suivants étaient utilisés pour autre chose. L'idée est que le registre 0 mémorise un emplacement vide, le registre 1 contient le sommet de la pile, le registre 2 l'opérande sous le sommet de la pile, et ainsi de suite. Les 15 premières opérandes de la pile sont donc mémorisées dans les registres. Les instructions écrivent leur résultat dans le registre adéquat, puis tous les registres sont décalés pour que le résultat passe dans le registre R1. Par exemple, les instructions PUSH chargent une opérande dans le registre 0, puis tous les registres sont décalés de un rang, pour que l'opérande chargée soit dans le registre 1. Le registre 0 devient alors le registre numéro 1, le registre 1 devient le 2, et ainsi de suite. Par contre, les opérations arithmétiques font autrement. Une instruction d'addition, par exemple, lit les opérandes dans le registre 1 et 2, puis place son résultat dans le registre 2 (les deux opérandes sont dépilées). Puis, les registres sont décalés d'un rang pour que ce registre 2 devienne le 1. Les registres ne sont en réalité pas décalés, il n'y a pas de transferts entre registres. A la place, les 16 premiers registres utilisaient une forme limitée de renommage de registres, similaire au banc de registre tournant utilisé sur les processeurs EPIC comme l'Itanium. Les numéros de registres sont renommés, décalés, à chaque fois qu'on empile ou dépile une opérande. Pour faire le renommage de registres, processeur contient un ''register stack counter'' de 4 bits, qui est incrémenté à chaque fois qu'on empile une donnée, et est décrémenté quand on dépile une donnée. Quand on veut accéder à un opérande, le numéro de registre architectural associé est traduit en numéro de registre physique, en additionnant le ''register stack counter'' (modulo 16, pour rester sur 4 bits). ==L'accélération de l'interprétation/JIT : le ''Thumb-EE'' d'ARM== Quelques processeurs ont ajouté des instructions pour faciliter le travail des interpréteurs/JIT. Par exemple, en 2005, ARM a ajouté le mode ''Thumb Execution Environment'', qui faisait cela. Il reprenait le jeu d'instruction compact ''thumb'' et ajoutait quelques instructions et modifiait le comportement d'instructions existantes. Le jeu d'instruction ''Thumb-EE'' a été ajouté sur les CPU ARM en 2005, mais a été déprécié en 2011, par manque d'utilité. Mais la tentative mérite qu'on s'attarde dessus. Les CPU de gamme M, déstinés à l'embarqué, n'ont jamais supporté ''Thumb-EE'', vu que de tels processeurs ne sont pas conçus pour exécuter du code interprété/JIT. Le mode ''Thumb-EE'' est un mode d'exécution, séparé de l'ARM normal. Ainsi, une instruction ''Thumb-EE'' et une instruction ARM peuvent avoir le même encodage en binaire, mais se comporter différemment. Le processeur est à tout moment soit en mode ''Thumb-EE'', soit en mode ARM normal, ce qui précise comment ces instructions doivent se comporter. Des instructions ont été ajoutées par ''Thumb-EE'', pour qu'il fasse son travail. L'entrée et la sortie du mode ''Thumb-EE'' se fait avec deux instructions : ENTERX et LEAVEX. Une fois en mode ''Thumb-EE'', le décodage des instructions ''thumb'' se fait avec les règles du ''Thumb-EE'', et non celles du ''thumb'' normal. Une première différence entre ''thumb'' normal et ''Thumb-EE'' est l'ajout de ce qui s'appelle un ''null check'' pour les instructions mémoire. Le ''null check'' est utilisé pour les instructions en mode d'adressage "base + indice" ou "Base + décalage", ou tout autre mode d'adressage avec un registre de base utilisés dans des calculs d'adresse. L'idée est de vérifier si l'adresse de base vaut zéro ou non. Si c'est le cas, le processeur lève une exception matérielle, qui est traitée par l'interprétation ou le compilateur JIT. Une telle situation est en effet signe d'une erreur d'adressage mémoire, qui doit être traitée par l'interpréteur. Typiquement, c'est signe qu'un pointeur n'a pas bien été initialisé, ce qui peut arriver avec du code interprété ou JIT. Les interpréteurs/JIT traitent généralement la situation en ajoutant un test avant toute instruction mémoire, pour vérifier si le pointeur accédé vaut zéro ou non. Avec ''Thumb-EE'', pas besoin d'ajouter les tests en questions : ils sont réalisés automatiquement lors de chaque instruction mémoire. : Les registres pour la pile, comme le pointeur de pile et le pointeur de ''frame'', sont aussi concernés. L'instruction CHKA vérifie que les accès à un tableau ne débordent pas en dehors du tableau en question. Par exemple, pour un tableau de 1024 éléments, elle vérifie si l'indice est compris entre 0 et 1024. Pour cela, elle vérifie si l'indice de tableau est bien dans l'intervalle adéquat et lève une exception matérielle si ce n'est pas le cas. Son utilité se comprend quand on sait que tous les accès à un tableau sont vérifiés par l'interpréteur. L'interpréteur est censé ajouter des instructions pour vérifier les indices, à savoir deux ou trois branchements. Avec CHKA, le test se fait en une seule instruction. D'autres modifications mineures de l'encodage des instructions sont aussi présentes, ainsi que des modifications pour les instructions LOAD/STORE. Précisément, dans les modes "base + indice" et "base + décalage", le décalage et l'indice subissent maintenant des décalages. L'idée est de simplifier le calcul d'adresse. De quoi économiser quelques instructions lors des calculs d'adresse, ce qui facilite le travail de l'interpréteur/JIT. Et des instructions LOAD/STORE précises voient leur comportement modifié pour faciliter la gestion de la pile de Java ou l'exécution des méthodes locales. Pour faciliter le travail de l'interpréteur, deux instructions HBP et HBLP ont été ajoutées. Elles branchent vers une fonction qui gère les exceptions logicielles (une fonctionnalité présente dans de nombreux langages, comme Java). La première effectue un branchement vers la fonction en question, la seconde sauvegarde l'adresse de retour avant de faire ce branchement. Les deux peuvent brancher vers 256 fonctions pré-déterminées, sans avoir à présenter leur adresse. ==La traduction binaire accélérée par le matériel== Plus haut, nous avons surtout parlé des compilateurs et des interpréteurs. Cependant, nous devons aussi parler de la '''traduction binaire'''. Elle traduit un programme écrit dans un code machine vers un autre code machine. Par exemple, elle traduit un programme compilé pour un CPU x86 vers un code machine ARM. En général, le système d'exploitation est généralement compilé pour le jeu d'instruction natif, mais il exécute des applications prévues pour le x86. Les applications sont traduites par le système d'exploitation, avant d'exécuter le code traduit. : Nous parlerons dans la suite de code machine source et de code machine cible, pour parler respectivement du code à traduire et du code obtenu après traduction. Idem avec d'autres termes comme architecture cible/source, ou jeu d'instruction cible/source. La traduction binaire est surtout utilisée pour des questions d'émulation ou de compatibilité. Par exemple, lorsque les Macintosh sont passés de processeurs Power PC vers des processeurs x86, le système d'exploitation Mac OS utilisait la traduction binaire pour convertir les anciennes applications Power PC vers du code x86. Les utilisateurs n'y ont vu que du feu. Le système de traduction binaire était appelé Rosetta 1. Par la suite, lors de la transition de processeurs x86 vers des processeurs Apple, Rosetta 2 a vu le jour. La traduction binaire a surtout été utilisée pour traduire du code x86 vers un autre jeu d'instruction. Il faut dire que le x86 est le jeu d'instruction dominant. De nombreuses entreprises ont eu pour ambition de briser l'hégémonie du x86 sur PC, en remplaçant le x86 par un jeu d'instruction plus performant, tout en gardant une compatibilité maximale. La traduction binaire était la seule solution pratique. Elle était le plus souvent intégralement réalisée en logiciel, comme c'était le cas sur les architectures Itanium avec le ''IA-32 Execution Layer''. ===Les généralités sur la traduction binaire assistée en matériel=== L'architecture source est presque tout le temps une architecture CISC, assez ancienne, qu'on souhaite émuler. Le choix de l'architecture cible se porte souvent sur une architecture VLIW, fort différente de l'architecture source, et ce pour obtenir une économie de matériel conséquente. Pour s'exécuter rapidement, le code traduit doit exploiter le parallélisme d'instruction, à savoir exécuter plusieurs instructions en même temps dans des unités de calcul séparées. Il est possible d'utiliser un processeur superscalaire avec exécution dans le désordre pour cela, mais au prix d'un cout important en transistors. Alors qu'en utilisant un CPU VLIW, c'est le traducteur binaire qui fait tout le travail d'extraction du parallélisme d'instruction. Et après tout, quitte à avoir un traducteur binaire, autant lui refiler le boulot d'optimisation. Compiler du code à la volée est certes assez lourd, mais qu'il existe des algorithmes efficaces pour regrouper des instructions indépendantes dans une seule instruction VLIW. Au passage, le processeur VLIW a plus de registres que le processeur source. Cela permet de faire du renommage de registres directement en logiciel. Le traducteur binaire n'hésite pas à changer les noms de registres entre instructions source et instruction VLIW cible, afin de supprimer des dépendances de données. En conséquence, cela ouvre des opportunités de parallélisme, qui sont exploitées lors du regroupement des opérations en instructions VLIW. Vous pourriez penser que le choix d'un CPU VLIW pour émuler du CISC est tout sauf optimal. Vous devez penser que la traduction binaire est d'autant plus simple que l'architecture source et cibles sont semblables. Dans les faits, ce n'est pas tellement traduire les instructions qui pose problème, mais plus la gestion du registre d'état, des exceptions matérielles, de la mémoire virtuelle, la différence entre gros-boutisme et petit-boutisme, de même que des différences d'adressage pour les périphériques. Le premier problème est la gestion des conditions, notamment en présence d'un registre d'état. En pratique, la traduction binaire s'utilise pour traduire du code CISC vers du code RISC ou VLIW. Les architectures source ont donc un registre d'état, qui est mis à jour non seulement par des instructions de test, mais aussi des instructions arithmétiques. L'architecture cible n'a elle pas de registre d'état, mais des registres à prédicats. La traduction de l'un vers l'autre est alors quelque peu compliquée. Et elle est d'autant plus compliquée que le registre d'état est la source de dépendances d'instruction implicites, qui réduisent les performances. Par exemple, si une instruction arithmétique modifie le registre d'état, cela peut impacter l'exécution d'une instruction ultérieure, qui lit ce registre d'état. Pour éliminer ces fausses dépendances, le traducteur binaire doit renommer ce registre en logiciel et détecter les dépendances utiles. Et le compilateur doit gérer les cas où il y a beaucoup de distance entre les deux, voire les cas où l'instruction dépendante n'a pas encore été analysée par le compilateur. Un autre problème est lié à la gestion des exceptions matérielles, et précisément des exceptions précises. Pour rappel, une instruction VLIW regroupe plusieurs opérations, opérations qui correspondraient à une instruction machine sur un CPU pas VLIW. En conséquence, plusieurs instructions du langage machine "source" sont regroupées en une seule instruction VLIW. Et il faut tenir compte du cas où une instruction source lève une exception. Dans le code VLIW, cela signifie qu'une opération lève une exception, et il faut annuler partiellement l'instruction VLIW associée. Et par partiellement, on veut dire que seules les opérations suivantes dans l'ordre du programme source doivent être annulées, puis ré-exécutées. Et c'est un sacré casse-tête ! Les processeurs VLIW utilisent comme solution le mécanisme d'exceptions différées des processeurs EPIC, vu il y a quelques chapitres. Pour résumer, le code est exécuté par blocs d'instruction, délimités par des branchements ou tout autre limite/barrière pertinente dans le code. Un bloc de code est compilé en deux versions : une version rapide sans exceptions matérielles, une version lente qui gère les exceptions précises. La version rapide s'exécute sans exécuter les exceptions. Cependant, les exceptions matérielles sont enregistrées, pour être prises en compte à la toute fin du bloc de code. La version rapide est exécutée en premier et elle mémorise si une exception matérielle a eu lieu. Puis une instruction vérifie si une exception a eu lieu et décide quoi faire. Si aucune exception n'a eu lieu, elle passe à la suite du programme, les résultats du bloc de code sont définitivement acceptés. Mais si une exception a eu lieu, tout est annulé. Le processeur est remis dans l'état initial, puis le code est ré-exécuté instruction par instruction de manière à gérer l'exception correctement. ===La traduction par pré-décodage=== Une première solution serait de faire la traduction binaire dans le cache d'instruction. Lors d'un défaut de cache, le code chargé depuis la RAM est traduit en code machine cible. Et c'est ce code machine cible qui est mémorisé dans le cache d'instruction et exécuté par le processeur. La technique marche sur le papier et n'est qu'une amélioration des techniques de pré-décodage vues il y a quelques chapitres. Cependant, le code obtenu est une traduction assez basique, qui n'incorpore pas d'optimisations dignes de ce nom. De plus, elle gère mal le cas où la taille du code source et cible sont potentiellement très différentes. Concrètement, elle est surtout utile pour traduire du code d'un processeur RISC vers un autre processeur RISC, dont les tailles d'instructions sont similaires. Elle ne permet pas d'émuler plusieurs jeux d'instructions différents, le cout en matériel (un circuit de pré-décodage par jeu d'instruction) serait trop important. ===Le projet DAISY d'IBM=== De très rares processeurs étaient conçus pour accélérer cette traduction binaire, afin de garder de bonnes performances. Les premiers à avoir étudié l'idée étaient IBM, avec leur projet DAISY (''Dynamically Architected Instruction Set from Yorktown''). Le projet de base était de convertir à la volée du code compilé pour des CPU Power PC, vers du code VLIW. Les chercheurs d'IBM avaient développé un algorithme de traduction dynamique efficace, ainsi qu'un processeur VLIW disposant d'optimisations spécifiques à la traduction binaire dynamique, à la volée. De nombreuses idées de ce projet ont été reprises ou re-découvertes par la société Transmetta, avec ses processeurs Crusoe et Efficieon, puis par NVIDIA avec son projet Denver. Les ingénieurs de ce projet ont étudié la possibilité d'émuler plusieurs jeux d'instructions différents sur un même processeur, notamment le s390 d'IBM et le x86. En théorie, cela demande juste d'avoir plusieurs programmes de traduction binaire : un pour le x86, un autre pour le s390, éventuellement un autre pour le Power PC. Mais cela ne s'est pas concrétisé. Mais les ingénieurs ont étudié quelle pourrait être l'architecture VLIW idéale pour ça. Par exemple, il fallait des additions 3-opérandes pour simplifier les calculs d'adresse. Une difficulté était la gestion du registre d'état, dont les bits ne sont pas mis à jour de la même manière sur le x86 et le s390 ou le Power PC. La différence entre gros-boutisme et petit-boutisme était aussi un problème, de même que des différences d'adressage pour les périphériques. Pour que la traduction binaire soit efficace, le processeur VLIW intègre diverses optimisations, comme les exceptions différées et les branchements multi-voies. La principale est la suivante : seul le code des boucles ou fonctions exécutées fréquemment est traduit en code VLIW, le reste du code est interprété. En effet, traduire du code binaire prend plus de temps qu'une simple interprétation. Pour du code qui ne sera exécutée qu'une seule fois, il est plus rapide d'utiliser l'interprétation. Par contre, pour du code exécuté beaucoup de fois, le cout de la traduction binaire est amorti, dilué sur N exécutions, compensé par le gain en temps d'exécution de ce code traduit. Au final, cela permet de ne traduire que le code qui le mérite. La détection du code fréquemment exécuté est réalisée dans le cœur VLIW. L'unité de branchement mémorise les derniers branchements rencontrés et le nombre de fois qu'ils ont été exécutés. Si ils ont été exécutés un certain nombre de fois, l'unité de branchement lève une exception matérielle, qui invoque le traducteur binaire. La routine de cette exception est le traducteur binaire proprement dit. En clair, la traduction binaire est démarrée quand le processeur détecte qu'une fonction a été exécutée plus de N fois, via une exception matérielle dédiée. La mesure du nombre d'exécution d'un branchement se fait dans l'unité de calcul dédiée aux branchements, avec l'aide d'une sorte de ''branch adress buffer'' modifié. Pour rappel, le ''branch adress buffer'' mémorise le ''Program counter'' de chaque branchement récemment rencontré. Ici, chaque entrée du ''branch adress buffer'' est associée à un compteur incrémenté à chaque exécution du branchement. ===Les processeurs Crusoe et Efficieon de Transmetta=== Les processeurs Crusoe Et Efficeron sont deux processeurs VLIW produits par la société Transmetta, une société californienne rachetée par NVIDIA. Ils étaient conçus pour exécuter spécifiquement des programmes x86, système d'exploitation inclus. L'idée derrière ce projet était d'exécuter du code x86 sans que les applications, ni même le système d'exploitation et le BIOS soient au courant ! Les processeurs Transmetta étaient en réalité conçus pour exécuter un programme unique, le ''Code Morphing Software'' (CMS), qui traduisait le code x86 en code VLIW. Transmetta ne voulait pas que son processeur VLIW soit exploité directement, sans le CMS. Il n'avait pas rendu public de compilateur pour traduire du C vers du code VLIW, il n'avait pas donné la documentation du jeu d'instruction VLIW. Des efforts de rétro-ingénieurie, documentés sur le site realworldtech, ont cependant permis de comprendre comment étaient encodées les instructions du processeur Crusoe. Comme pour le projet Daisy, le ''Code Morphing Software'' utilisait à la fois interprétation et traduction binaire, selon les besoins. Les instructions sont interprétées lors de leurs premières exécutions, mais le CMS bascule sur de la traduction en code VLIW après un certain nombre d'exécution. Ainsi, les boucles souvent exécutées sont traduites en code VLIW, alors que le reste du code est interprété. Le choix du code à traduire est le fait du CMS, il utilise des heuristiques complexes pour, qui ne sont pas connues dans le détail. Le ''Code Morphing Software'' est mémorisé dans une EEPROM, ce qui en fait un ''firmware'' situé en dessous du BIOS. Le CMS démarre ensuite le BIOS, qui lui-même démarre le système d'exploitation, qui lui-même démarre les pilotes de périphériques et les programmes. Le CMS se réserve les 16 premiers mébioctets de l'espace d'adressage. L'EEPROM du CMS est mappée dedans, mais n'en utilise que la moitié. Le reste est utilisé comme cache, pour mémoriser le code VLIW traduit par le CMS. Les deux processeurs intégraient aussi deux mémoires SRAM, utilisées par le CMS, appelées la ''local program memory'' (LPM) et la ''local data memory'' (LDM). La première contient du code qui gére les interruptions, la mémoire virtuelle, les exceptions matérielles, les problèmes d'alignement mémoire, et quelques fonctions très fréquemment utilisées par le CMS. La seconde est de la mémoire RAM utilisée par ces fonctions de la ''local program memory''. Les mémoires caches L1 et L2 sont séparées de ces deux mémoires. Le processeur Crusoe et Efficieon étaient des processeurs VLIW très simples. Ils n'avaient même pas de MMU, ni de fonctionnalités importantes sur les CPU x86. Pour Crusoe, les instructions VLIW étaient encodées sur 64 ou 128 bits, et regroupaient entre 2 à 4 opérations. Les instructions LOAD/STORE ne supportaient pas d'adressage indicé, juste de l'adressage indirect à registre. Crusoe avait 5 unités de calcul : deux ALU entières, une unité LOAD/STORE, une unité de branchement, une FPU. Efficieon doublait le nombre d'ALU entière et d'unité LOAD/STORE, les instructions VLIW passaient à 256 bits. Le processeur Crusoe contient 160 registres, dont 64 registres généraux, 32 registres flottants. Les registres généraux font 32 bits, ce qui est cohérent avec le fait que les CPU x86 de l'époque étaient des processeurs 32 bits. Quant aux registres flottants, ils faisaient 80 bits, ce qui colle avec la taille des registres flottants de la FPU x87 utilisée à l'époque. Sur les 64 registres généraux, seuls 48 étaient réellement utilisable pour mémoriser des opérandes. Une partie des registres généraux étaient utilisés pour la gestion de la pile, d'autres pour mémoriser l'état du CPU x86 émulé, un registre était un registre zéro non accesible en écriture. La gestion des exceptions est optimisée avec un système d'exceptions différées, le même que celui décrit plus haut. Le processeur utilise un système similaire, pour effectuer des lectures anticipées, qui a été expliqué dans le chapitre sur les processeurs VLIW/EPIC. Sauf qu'il s'agit là d'une optimisation pour gagner en performances en général, pas une aide utile pour gérer des exceptions précises ou toute autre subtilité de la traduction binaire. Pour gérer les exceptions différées et les lectures anticipées, le processeur mémorise l'état du processeur avant de démarrer un bloc de code. Pour cela, le processeur contient des ''shadow registers'', 48 registres entiers et 16 registres flottants. Vous remarquerez qu'il y a autant de registres entiers que de ''shadow registers'' entiers. Les registres entiers/flottants sont mémorisés dans les ''shadow registers'' avant d'exécuter un bloc de code, afin que le processeur puisse revenir à l'état de base. Une autre fonctionnalité liée est le ''gated store buffer''. Le principe est d'éviter toute écriture en mémoire RAM, tant que le code VLIW n'a pas émis d'instruction ''commit''. L'idée est que les écritures sont conservées dans la ''store queue'' de l'unité LOAD/STORE. Pour rappel, la ''store queue'' met en attente les écritures soit tant que les écritures ne sont pas terminées, soit tant que la RAM est occupée. L'idée est que les écritures sont accumulées dans la ''store queue'' tant que l'instruction ''commit'' n'a pas eu lieu. Le ''store queue'' est "déversé dans la RAM" seulement quand l'instruction ''commit'' s'exécute. ===Les CPU NVIDIA Denver=== Nvidia a tenté quelque chose de similaire avec son projet Denver. NVIDIA a racheté l'entreprise Transmetta, ses brevets, et a tenté de refaire la même chose. Le projet initial était de traduire du code x86 vers un jeu d'instruction VLIW propriétaire, comme l'a fait Transmetta. Mais NVIDIA n'a pas réussi à acquérir la licence du jeu d'instruction x86 et s'est rabattu sur le jeu d'instruction ARM. Le processeur né de ce projet est le Tegra K1-64 CPU. Il s'agit d'un processeur composé d'un cœur VLIW, couplé à deux décodeurs d'instructions ARM. Les deux décodeurs ARM traduisent une paire d'instructions ARM consécutives en instructions VLIW, pour les exécuter sur le cœur VLIW. Le CPU peut fonctionner selon deux modes : ARM et VLIW. En somme, le processeur supporte deux jeux d'instruction : le jeu d'instruction ARM, et un jeu d'instruction VLIW. En mode ARM, les instructions ARM sont chargées depuis le cache, traduites en VLIW par les décodeurs ARM, puis exécutées par le cœur VLIW. En mode optimisé, le CPU exécute des instructions VLIW chargées depuis le cache d'instruction, les décodeurs ne sont pas utilisés. : Le CPU supporte en réalité deux jeux d'instruction ARM : ARM8 et 7. Mais ils correspondent au même mode pour le CPU. Un point important est que le processeur peut changer très rapidement de mode, en à peine quelques cycles d'horloges. On n'est clairement pas dans le cas des CPU x86, qui mettent des plombes pour passer du mode 32 à 64 bits et inversement. La commutation est tellement rapide qu'on peut considérer que le processeur supporte deux jeux d'instruction simultanément. Et le support simultané de deux jeux d'instruction facilite l'implémentation de la traduction binaire. Encore une fois, inutile de traduire du code qui ne sera exécutée qu'une seule fois, mieux vaut l'exécuter directement dans le mode ARM. Surtout que le processeur supporte un mode ARM qui permet d'exécuter du code ARM sans perte de performance. Par contre, le code fréquemment utilisé, notamment dans des boucles critiques, est traduit en VLIW. Au final, les performances sont optimisées, en limitant le travail du logiciel traducteur binaire. La détection du code fréquemment exécuté se fait différemment que pour les processeurs Transmetta et Daisy. L'unité de branchement ne détecte pas directement le code beaucoup exécuté, même si elle a un rôle à jouer. A la place, la détection est réalisée en logiciel, par un ''thread'' dédié, qui s'exécute sur un cœur séparé. Il détecte le code fréquemment utilisé en regardent les branchements exécutés récemment. Pour cela, le processeur mémorise l'historique des branchements pris récemment et copie cette historique dans une mémoire tampon dédiée, partagée entre tous les cœurs. Vu qu'elle est partagée, le ''thread'' de détection a accès à l'historique sans pertes de performances, seule la copie de l'historique dans ce tampon a un cout en performance, pas son partage. Le code VLIW obtenu après traduction binaire est mémorisé dans la mémoire RAM, dans une portion spécialement réservée pour. Elle fait 128 mébioctets, et est appelée le '''cache d'optimisation''' par NVIDIA. Le cache d'optimisation est protégé en écriture et seul le traducteur binaire peut écrire dedans. Au passage, le traducteur binaire est du code VLIW, ce qui fait qu'il est placé dans le cache d'optimisation. Le processeur détecte automatiquement quand une fonction pour laquelle le code compilé équivalent est disponible. Pour cela, l'unité de chargement contient une table de correspondance entre l'adresse de la fonction ARM, et l'adresse de son équivalent compilé. Quand une fonction est appelée, l'unité de chargement regarde l'adresse de destination du branchement. Si l'adresse est dans cette table, elle récupère l'adresse de la fonction compilée et branche vers celle-ci. Le processeur dispose d'un cache d'instruction de 128 kibioctets, ce qui est très important, mais nécessaire vu la taille des instructions VLIW. Le coeur VLIW dispose de ses propres décodeurs, d'un ''scoreboard'', de 7 unités de calcul et de registres. Pour les unités de calcul, il a deux FPU, deux ALU, une unité de branchement, et deux unités LOAD-STORE faisant aussi ALU. Un point important est que le CPU dispose de 64 registres entiers et de 64 registres flottants. C'est deux fois plus que supporte le jeu d'instruction ARM. En mode ARM, seule la moitié des registres est utilisée. Mais en mode optimisé, le traducteur binaire utilise bien les 64 registres, grâce à une sorte de renommage de registres logicielle. Pour simplifier la traduction binaire, le processeur supporte les techniques vues précédemment. Le processeur intègre un ''gated store buffer'' similaire à celui des processeurs Transmetta. Les techniques d'exceptions différées sont aussi supportées, comme sur les processeurs Transmetta. Il a aussi une unité de préchargement décrite par NVIDIA comme agressive, avec support du préchargement de type anticipé (''runahead''). ===L'émulation de la mémoire virtuelle=== Pour finir, il faut parler de la mémoire virtuelle avec la traduction binaire. Sur le processeur source, le processeur gère à la fois des adresses physiques et virtuelles et sa MMU fait la conversion entre les deux. Sur le processeur VLIW, la MMU est simulée par le programme de traduction binaire, partiellement ou totalement. L'implémentation exacte varie suivant que l'on parle du projet Daisy, des processeurs Transmetta ou de Denver. Sur les processeurs Transmetta, il n'y a pas de MMU ni de mémoire virtuelle. A la place, le programme de traduction binaire émule la mémoire virtuelle du processeur source. Le processeur VLIW ne gère que des adresses physiques, rien d'autre. L'espace d'adressage physique du processeur VLIW a la même taille que celui du processeur émulé, ici des processeurs x86. Il est vraisemblable que les adresses physiques utilisées par le processeur x86 sont les mêmes que celles du processeur VLIW. Sur le projet Daisy, le processeur VLIW gère la mémoire virtuelle via pagination, comme les processeurs émulés. Ce qui fait qu'il gère un espace d'adressage virtuel et un espace d'adressage physique. Pour éviter toute confusion, nous parlerons d'adresse physique/virtuelle VLIW pour les adresses physiques/virtuelles du processeur VLIW, d'adresse virtuelle/physique source pour celle du jeu d'instruction traduit, à savoir du Power PC ou du s390, éventuellement de l'x86. Il faut alors faire le lien entre adresses physiques source et adresses virtuelles VLIW. Pour cela, rien de plus simple : il y a correspondance parfaite. L'adresse physique source numéro N correspond à l'adresse virtuelle VLIW numéro N. Cependant, cela signifie que tout l'espace d'adressage virtuel VLIW serait utilisé par le code à traduire. En réalité, il faut ajouter de la place pour le code traduit et le programme de traduction binaire. La conséquence est que l'espace d'adressage virtuel VLIW est plus large que l'espace d'adressage source. L'espace d'adressage virtuel VLIW est découpé en trois sections : une section pour le code Power PC à traduire, une autre pour le code traduit en VLIW, et une dernière réservée au traducteur binaire. Le programme de traduction est placé dans une ROM mappée en mémoire dans la seconde section. Le reste de la seconde section est réservé à une mémoire RAM utilisée par le programme de traduction. [[File:Mémoire virtuelle sur le projet Daisy.png|centre|vignette|upright=1.5|Mémoire virtuelle sur le projet Daisy]] Il faut noter que chaque page physique source, une fois traduite, correspond à N pages virtuelles VLIW. La raison est que le code traduit prend plus de place que le code machine originel. En conséquence, on doit utiliser une page finale plus grande. Pour le reste, des pages contiguës en mémoire physique source sont elles aussi contiguës en mémoire virtuelle VLIW. Le calcul d'adresse est donc simple : l'adresse physique source est multipliée par N, puis on ajoute l'adresse de base à laquelle commence la section pour le code traduit. ==Les processeurs à jeu d'instruction reconfigurable== Nous venons de voir comment accélérer en matériel la traduction binaire. Mais il existe une alternative qui permet de se passer de traduction binaire, complétement. L'idée est d'avoir un processeur avec un '''jeu d'instruction reconfigurable'''. Le nom est assez parlent : on peut configurer le processeur de manière à supporter le jeu d'instruction de son choix ! Par exemple, on pouvait reconfigurer un CPU de manière à ce qu'il supporte le jeu d'instruction x86, ou qu'il exécute du ''bytecode'' Java, ou du code ARMv8, etc. De tels processeurs sont encore plus rare que ceux vu avant dans ce chapitre, mais ils ont existé ! Les processeurs de ce type avaient un '''microcode réinscriptible''', à savoir que c'était des processeurs microcodés pour lesquels on pouvait modifier le microcode. L'idée était qu'en changeant le microcode, on changeait le jeu d'instruction du processeur. Pour cela, le microcode était associé à une mémoire SRAM, en plus de la mémoire ROM du ''control store''. La SRAM était appelée la micro-SRAM, alors que le ''control store'' basique était appelé la micro-ROM. Le microcode était copié dans cette micro-SRAM au démarrage du CPU, puis les instructions étaient décodées en utilisant le microcode dans la micro-SRAM. En modifiant le contenu de la micro-SRAM, on pouvait altérer le microcode, voire le remplacer totalement. Il s'agit là de l'implémentation la plus simple. Mais le Burroughs B1700 a procédé autrement et il est intéressant d'étudier en détail son architecture. Son architecture est décrite dans le livre "Interpreting Machines : Architecture and Programming of the Bl700/Bl800 Series", écrit par Elliott I. Organick et James A. Hinds. Une partie de ce qui vfa suivre est un très court résumé de ce livre. ===Le Burroughs B1700 : architecture externe=== Il disposait de 4 registres généraux, nommés X, Y, T et L, tous de 24 bits. Les registres T et L étaient découpés en 6 sous-registres de 4 bits, nommés LA LB LC LD LE et LF pour le registre L, et TA TB TC TD TE et TF pour le registre T. Ils étaient adressables comme n'importe quel registre. Un point important est que le registre T avait des possibilités d’extraction spécifiques. Il était possible d'extrait N bits, placés n'importe où dans ce registre, et de les copier dans un autre registre. Nous verrons pourquoi cette possibilité est intéressante et ne sert que pour le registre T. Les registres X et Y étaient reliés à une ALU de 24 bits capable d'effectuer des opérations arithmétiques basiques. Fait étonnant, l'ALU faisait tous les calculs en même temps et fournissait ses résultats dans 7 registres : un registre SUM pour l'addition, un registre DIFFERENCE pour la soustraction, et un registre par opération bit à bit. Les registres pour les opérations bit à bit sont : CMPX et CMPY pour l'opération NOT sur chaque opérande, XANY pour le ET bit à bit, XEOY pour le XOR, XORY pour le OU logique. Les additions et soustractions pouvaient se faire sur des opérandes codés en binaire ou en BCD, suivant le code opération utilisé. A cela, il fallait ajouter deux registres de "résultat" MSKX et MSKY. Ils fournissaient un masque dépendant de la taille du résultat en bits. Cela permettait de gérer des données de 0, 4, 8, 12, 16, 20, et 24 bits, alors que l'ALU faisait 24 bits. Et cela n'était pas limité à 16 ou 8 bits, mais n'importe quelle taille inférieure ou égale à 24 bits était supportée, même des tailles non-conventionnelles comme des résultats codés sur 3 bits, 9 bits, 12 bits, etc. L'ALU peenait en opérande les registres X et Y, mais aussi un registre CYF pour la retenue entrante. En sortie, il fournissait aussi deux retenue sortantes : une pour l'addition et une autre pour la soustraction. Elles étaient mémorisées dans les registres CYL et CYD, respectivement. L'ALU fournissait aussi 12 conditions, qui alimentait un registre d'état. Le registre d'état était segmenté en trois registres appelés XYST, XYCN et BICN. Le processeur contenait de nombreux autres registres de 4 bits, dont les sous-registres de T et L. En tout, il y a 27 registres de 4 bits adressables ! Et le processeur disposait d'une ALU 4 bits pour manipuler ces registres de 4 bits ! Elle est capable de faire des MOV entre registres de 4 bits, des opérations bit à bit, et peut aussi tester si un bit vaut 0 ou 1. La dernière possibilité est très utile pour implémenter les branchements. Pour communiquer avec les entrées-sorties, il disposait de deux registres CMND et DATA : CMND pour envoyer une commande à une entrée-sortie, DATA pour lire ou écrire une donnée. Pour la mémoire, il avait deux registres séparés : READ et WRITE. READ contient la donnée lue lors d'une lecture,; WRITE est pour une donnée à écrire lors d’une écriture. Il s'agit de registres d’interfaçage mémoire, qui ont été promus au rang de registres architecturaux. Les deux registres sont gérés par le microcode, qui est le langage principal de la machine. Les instructions machines configurées font sans, mais le microcode doit y avoir accès facilement. Et vu que le microcode est le langage principal de ce processeur, ces deux registres deviennent architecturaux. ===Le burroughs B1700 : un ''split-level control store''=== Le burroughs B1700 lisait le microcode directement depuis la mémoire RAM ! Pour être précis, il avait plusieurs modèles : le B1726, le B1710 et le B1800. Le B1710 plaquait le microcode en mémoire RAM, ce qui fait que chaque exécution d'une instruction lisait les micro-opérations en mémoire RAM ! Le B1800 faisait pareil, mais avait ajouté un cache dédié au microcode, afin d'améliorer les performances. Le B1726 avait un microcode scindé en deux : une partie était en mémoire RAM, l'autre était dans un microcode réinscriptible classique, avec une micro-SRAM intégrée au processeur. Le processeur peut exécuter le microcode directement depuis la mémoire RAM, mais avec une pénalité en termes de performances. La documentation appelle cela un '''''split-level control store'''''. Le programmeur devait découper le microcode en segments, certains étant placé dans la micro-SRAM, les autres étant en mémoire RAM. Un tel microcode permettait de placer le microcode le plus fréquemment utilisé dans le processeur, alors que le reste du microcode était placé en mémoire RAM. Les instructions fréquentes étaient ainsi décodées avec la micro-SRAM, alors que les instructions peu fréquentes étaient décodées en lisant le microcode associé en mémoire RAM. La pénalité en termes de performance est alors concentrée sur les instructions les plus rares, ce qui est un très bon compromis. Mieux que ça, le processeur autorisait d'utiliser la technique de l'''overlaying'' pour le microcode ! Il y avait même une micro-opération OVERLAY dédiée pour ! Elle permettait de copier du microcode de la mémoire RAM vers la micro-SRAM ! OVERLAY avait besoin de trois opérandes : la taille du segment à copier, son adresse source en mémoire RAM, son adresse de destination dans la micro-SRAM. La micro-SRAM mémorisait au maximum 7680 micro-opérations, ce qui fait que les 7680 premières "adresses" correspondaient à la micro-SRAM. Les suivantes étaient des adresses de mémoire RAM qui contenaient le reste du microcode. En pratique, l'adresse qui séparait les deux portions du microcode était configurable ! On pouvait limiter la taille du microcode en micro-SRAM à un multiple de 512 micro-opérations ! Les adresses restantes étaient alors réattribuées à la RAM. Par exemple, on pouvait attribuer 512 adresses à la micro-SRAM, les 7680 - 512 adresses restantes étaient alors attribuées à la mémoire RAM. Un registre limite, nommé TDPM, indiquait quelle était l'adresse de démarcation entre les deux. Il contenaient 4 bits, dont la valeur était multipliée par 512, pour obtenir l'adresse limite. Le microcode pouvait servir pour émuler un autre jeu d'instruction, mais il était aussi possible d'implémenter un véritable interpréteur ! L'interpréteur exécutait alors un code source écrit dans un langage de haut niveau, comme le FORTRAN ou le COBOL, qui était convertit à la volée en micro-opérations, sans passer par un langage machine intermédiaire ! Le processeur était même conçu pour ! L'interpréteur était placé dans un "segment" en mémoire RAM, dont le processeur connaissait la position. Il incorporait pour cela deux registres, un pour l'adresse de base de l'interpréteur, un autre pour son adresse de fin. Il incorporait une protection mémoire limitée sur ce segment, à savoir que seul l'interpréteur pouvait lire ou écrire dans ce segment. ===Le microcode réinscriptible=== Le microcode utilise des micro-opérations de 16 bits de long, ce qui est très peu et bien en-dessous des micro-opérations des autres processeurs. Les micro-opérations sortant du microcode sont envoyées au registre M, un registre de micro-opération de 16 bits. les micro-opérations écrites dans ce registre sont ensuite décodées par le hardware et exécutées. Fait intéressant, il était possible de faire un OU logique entre la micro-opération sortante et un registre du processeur, le résultat étant mémorisé dans le registre M. Cela permettait de faire du code automodifiant dans le microcode ! Utiliser du code automodifiant avec un microcode réinscriptible est parfaitement possible, et c'est utile pour émuler des modes d'adressage complexe dans le microcode. Cela permettait par exemple de remplacer une adresse dans une micro-opération. L'adresse est alors surimposée avec un OU dans le champ d'adresse, alors que le reste de l'instruction n'est pas modifié. Mais modifier directement le microcode est rarement une bonne idée. Aussi, pour éviter cela, l'idée est de modifier les instructions avec un OU logique avec un autre registre. Les micro-opérations du microcode ne sont donc pas modifiées dans le microcode, mais en sortie du microcode. Afin de gérer les interruptions, ces dernières étaient partiellement détectées en logiciel. Les interruptions ne s'exécutaient pas automatiquement, pour émuler correctement les interruptions. Une interruption doit être prise en compte à la toute fin de l'exécution d'une instruction, quand celle-ci a finit son travail. Mais vu qu'ici, les instructions sont émulées avec des séries de micro-opérations, il y a un problème : il est possible d'interrompre une instruction émulée en plein milieu. Pour éviter cela, le processeur ne déclenche pas les interruptions matérielles immédiatement, elles sont mises en attente. Une micro-opération dédiée vérifie si une interruption est en attente, et l'exécute alors cas échéant. La micro-opération est utilisée comme micro-opération finale d'une instruction émulée. ===Une architecture bit-adressable=== Si avoir un microcode réinscriptible est suffisant, quelques choix de ''design'' facilitent l'émulation de jeux d'instructions variés. Par exemple, le CPU Burroughs B1700 avait ajouté une fonctionnalité très importante : il était bit-adressable. En clair, il pouvait adresser la mémoire bit par bit, si nécessaire. L'avantage est que cela permettait d'émuler des architectures dont la taille des registres/''byte''/mots était très différente. Un autre avantage est que cela facilitait la gestion des instructions de taille variable. Détaillons rapidement le premier avantage. Le processeur était un CPU 24 bits, mais il pouvait émuler des processeurs 16 bits, 8 bits, 4 bits, 1 bit, 9 bits, ou toute autre valeur. Pour cela, le processeur lisait des mots de 24 bits et utilisant un ''barrel shifter'' pour sélectionner les bits adéquats. Par exemple, pour un accès de 16 bits, la donnée lue en mémoire était masquée de manière à ne garder que les 16 bits de poids faible. Le ''barrel shifter''/circuit de masquage était placé directement avant le bus mémoire, sur le trajet des données/instructions. Le ''barrel shifter'' était commandé par un registre ''Field Unit'', qui précisait quelle était la taille des données à charger. Le processeur gère un micro-''program counter'' appelé le registre A. Les micro-opérations du processeur font 16 bits de long, et elles sont alignées en RAM sur 16 bits, ce qui fait que les 4 bits de poids fort de ce registre valent 0. Le registre fait 18 bits, mais seuls les 14 bits de poids fort sont utiles. Les adresses mentionnées plus haut, qui adressent les micro-opérations en mémoire RAM, sont des adresses de mots de 16 bits, pas les adresses de bit individuels. ===La gestion de la taille des instructions=== Les instructions à décoder n'ont pas de longueur fixe, à savoir qu'elles peuvent faire 16 bit avec tel jeu d'instruction, 8 bits avec un autre, 32 bits sur un autre, etc. Elles peuvent aussi être de longueur fixe ou variable ! Le B1700 intégrait donc un support des instructions de longueur variable, qui était "détourné" pour gérer des jeux d'instruction différents. Notons que vu que l'architecture est bit-adressable, les contraintes d'alignement des instructions disparaissent, ce qui a des avantages qu'on va détailler dans ce qui suit ! L'implémentation des instructions de taille variable ajoutait un registre avant le registre d'instruction. Le registre d'instruction proprement dit était un registre M de 24 bits. Il était précédé par un registre T, dans lequel l'instruction était assemblée, en combinant plusieurs morceaux. Si l'instruction est codée est codée sur moins de 24 bits, le registre T se contente de masquer les bits en trop. Si l'instruction chargée dépasser 24 bits, le chargement se faisait en plusieurs passes, avec plusieurs lectures en mémoire. Dans ce qui suit, on suppose que l'instruction est composée de '''champs''' : l'opcode, les opérandes, les adresses absolues, les constantes immédiates, les numéros de registre, etc. L'idée est que l'instruction est chargée dans le registre T, puis découpée en champs, qui sont copiés dans les registres du processeur ou envoyés au microcode. Je rappelle que le registre T est relié à un ''barrel shifter'', différent au précédent, qui permettait de masquer, décaler et extraire une donnée de ce registre. Et il y a la possibilité d'extraire une suite de bit de ce registre T, pour la copier dans un autre registre. Par exemple, prenons une instruction composée d'un opcode, d'une constante immédiate et d'une adresse immédiate. L'opcode est lu et interprété par le microcode. Le microcode extrait la constante immédiate du registre T et la copie dans le registre X. Ensuite, l'adresse est extraite et envoyée au registre FA, une lecture est démarrée, et l'opérande est copiée dans le registre Y. Enfin, l'instruction est exécutée par le microcode. Le tout prend plusieurs micro-opérations : une première pour lire l'opcode, une seconde pour lire la première opérande, une troisième pour lire la seconde opérande, una quatrième pour tout exécuter. Notons que suivant la taille des champs, on peut en charger plusieurs à la fois. Par exemple, imaginons qu'on ait chargé 24 bits, contenant un opcode de 8 bits et une constante immédiate de 16 bits : pas besoin de faire deux accès mémoire, le registre T est juste découpé en un opcode et une constante. Le registre T est fait pour ce genre d'extraction. ===Le chargement d'un champ=== L'architecture est bit-adressable car elle gére des champs qui ne font pas forcément 8 ou 16 bits. Par exemple, certains jeux d'instructions ont des opcodes codés sur 6 ou 7 bits, voire 9-10 bits. Par exemple, prenons un opcode de 7 bits, suivi par une adresse de 12 bits et une constante immédiate de 15 bits : aucun de ces champs ne sera aligné sur 8/16/24 bits. Utiliser une architecture bit-adressable règle le problème d'alignement. Et en général, les contraintes d'alignement des instructions disparaissent ! Quand on charge une instruction, peut importe qu'elle soit ou non alignée sur 24 bits, elle sera placée dans les bits de poids faible, il suffira de masquer les bits de poids fort inutiles. Par exemple, si on charge une instruction de 16 bits, les 16 bits seront copiés dans les 16 bits de poids faible du registre T, il suffira de masquer l'octet de poids fort. Pas besoin de faire de décalages pour gérer des instructions mal alignées... Pour charger une instruction, il faut connaitre plusieurs informations : l'adresse de son premier bit, sa taille. Pour cela, le B1700 contient deux registres, nommés ''Field Adress'' (FA) et ''Field Length'' (FL), qui indiquent l'adresse du premier bit et la taille de l'instruction à charger. Le premier fait 24 bits, ce qui permet de gérer des adresses de 24 bits. N'oubliez pas que le processeur est bit-adressable, ce qui permet d'adresser 16 Mébi-bits, soit 2 mébioctets. Le registre pour la taille fait lui 16 bits, ce qui permet de gérer des instructions de 65536 bits, soit 8192 octets ! Leur nom commence par ''field'', car le processeur peut gérer des instructions de taille variable et/ou plus longues que 24 bits. Dans ce cas, le chargement de l'instruction se fait champ par champ. Pour donner un exemple, prenons une instruction composée de 20 champs de 12 bits chacun. Les champs sont chargés un par un dans le registre T. Le registre FA est alors initialisé avec l'adresse de l'instruction, il est incrémenté de 12 à chaque cycle. Le registre FL est initialisé avec la taille de l'instruction, soit 20 * 12 = 240 bits, et il est décrémenté de 16 à chaque cycle. Précisons cependant que la taille d'un champ peut être changée d'un cycle à l'autre. Par exemple, le processeur peut charger un opcode de 8 bits, l'interpréter, détecter que l'instruction demande ensuite de charger une adresse de 16 bits. Dans ce cas, le registre FL est immédiatement altéré pour charger 16 bits, au lieu des 8 bits de l'opcode immédiatement précédent. ==Les jeux d'instructions dédiés à un langage de programmation== De rares processeurs sont conçus pour un langage de programmation en particulier. On appelle ces processeurs, conçus pour des besoins particuliers, des '''processeurs dédiés'''. Par exemple, les fameux ''Burrough E-mode'' B5000/B6000/B7000 étaient spécialement conçus pour exécuter de l'ALGOL-60. Leurs cousins B2000/B3000/B4000 étaient eux conçus pour le COBOL. Des langages fonctionnels ont aussi eu droit à leurs processeurs dédiés. Le prolog en est un bel exemple, avec les superordinateurs de 5ème génération qui lui étaient dédié. On peut aussi citer les machines LISP, dédiés au langage LISP, qui datent des années 1970. Elles étaient capables d’exécuter certaines fonctions de base du langage directement dans leurs circuits : elles possédaient notamment un ''garbage collector'' câblé dans ses circuits ainsi que des instructions machines supportant un typage déterminé à l’exécution. Les processeurs dédiés ont eu leur heure de gloire au début de l'informatique, à une époque où les langages de haut niveau venaient d'être inventés. À cette époque, les compilateurs n'étaient pas performants et ne savaient pas bien optimiser le code machine. Il était alors rationnel, pour l'époque, de rapprocher le code machine cible et le langage de programmation de haut niveau. De nombreuses architectures dédiés ont ainsi été inventées, avant que les concepteurs se rendent compte des défauts de cette approche. Les défauts en question ne sont pas nombreux, mais assez simples à comprendre. Premièrement, elles sont très rapides pour un langage de programmation en particulier, mais sont assez mauvaises pour les autres, d'où un problème de "compatibilité". Ajoutons à cela que les langages de programmation peuvent évoluer, devenir de moins en moins populaires/utilisés, ce qui rend la création d'architectures généralistes plus pertinente. Enfin, les architectures dédiées sont évidemment des processeurs CISC, pour implémenter les nombreuses fonctionnalités des langages évolués. Et les défauts des CISC sont assez rédhibitoires à l'heure actuelle. À l'heure actuelle, les algorithmes des compilateurs se sont améliorés et savent nettement mieux utiliser le matériel. Ils produisent du code machine efficace, ce qui rend les architecture dédiées bien moins intéressantes. Si on ajoute les défauts de ces architectures dédiées, par étonnant que les architectures dédiées aient presque disparues. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'accélération matérielle de la virtualisation | prevText=L'accélération matérielle de la virtualisation | next=Le matériel réseau | nextText=Le matériel réseau }} </noinclude> g5h35kdeu1kd0d0mj7tot3f1l0kmjmc 764037 764036 2026-04-19T19:36:31Z Mewtow 31375 /* Le burroughs B1700 : un split-level control store */ 764037 wikitext text/x-wiki De nos jours, la majorité des programmeurs programment dans des langages de haut niveau. Il est très rare pour eux d'avoir à utiliser de l'assembleur, et encore moins un langage machine. Les programmes écrits dans un langage de haut niveau sont traduits en langage machine par un logiciel appelé le compilateur. [[File:Compilation.PNG|centre|vignette|upright=2.5|Compilateur : principe.]] Il se trouve que quelques rares processeurs sont conçus pour faciliter le travail du compilateur. Et ce sont ces processeurs que nous allons voir dans ce qui suit. Ils représentent une catégorie de jeux d'instruction à part, qui n'a pas vraiment de nom. ==La compilation et l’interprétation== Un compilateur traduit donc du code source, écrit dans un langage de haut niveau, vers du langage machine. La traduction est rarement directe. En général, le compilateur traduit le code source en assembleur, qui est lui-même traduit en langage machine. L'assembleur est une représentation textuelle du langage machine alors que le code machine est du binaire exécutable par le processeur. La traduction se fait donc en deux étapes, la '''compilation''' proprement dite et l''''assemblage''', réalisées respectivement par un compilateur et un assembleur. Il faut parfois rajouter une troisième phase d'édition des liens, que nous passons volontairement sous silence. {| |[[File:Code C.png|vignette|200px|Le code source, ici en C.]] |[[File:Assem.png|vignette|250px|Le code assembleur.]] |[[File:Binary file - hello world (C programming).png|vignette|150px|Le code machine.]] |} ===L'interprétation et la compilation à la volée=== Une alternative à la compilation est l''''interprétation''', qui transforme le code source en code machine à la volée. Avec l'interprétation, le code source est passé à un logiciel appelé l''''interpréteur''', qui exécute le code source ligne de code par ligne de code. Une ligne de code est traduite en code machine, qui est exécutée, puis l'interpréteur passe à la ligne suivante. [[File:Kaantotulkkaus.png|centre|vignette|upright=3|Compilation versus interprétation.]] Un défaut de l'interprétation est son cout en performance, qui est assez important. Aussi, l'interprétation stricte a évolué vers un hybride entre interprétation et compilation. L'idée est qu'une partie peu importante du code source est interprétée, alors que le code important est compilé juste avant d'être exécuté. Pour le dire autrement, le code source est partiellement compilé à la volée, juste avant son exécution. Aussi, on parle de '''compilation à la volée'''. Le terme en anglais est ''Just In Time compilation'', abrévié en JIT. En soi, le JIT est associé à de l'interprétation, les JIT sont en réalité des hybrides interpréteurs-compilateurs. Un point important est que compiler du code à la volée est assez lourd, cela a un cout en performance. Par contre, le code compilé s'exécute plus vite que du code interprété. Si on exécute le code compilé une seule fois, le cout de la compilation l'emporte sur le gain à l'exécution. Mais le code compilé est réutilisé autant de fois que nécessaire. Le gain à l'exécution est donc multiplié par le nombre d'exécutions, alors que le cout de la compilation est répartit sur plusieurs exécutions, il est amorti. Les portions du code sont choisies de manière à avoir un gain en performance maximal. L'idée générale est que le code exécuté très souvent est compilé, alors que du code exécuté pas souvent est interprété. Typiquement, les boucles les plus souvent exécutées sont compilées, le code exécuté une seule fois est interprété. ===Les langages intermédiaires=== Les compilateurs modernes passent par un '''langage intermédiaire''' pour faire la transformation en code machine. Le compilateur traduit le langage de haut niveau en langage intermédiaire, puis traduit le langage intermédiaire en code machine cible. Le langage intermédiaire est parfois appelé le '''''bytecode''''', et ce terme recouvre aussi le code écrit avec du ''bytecode''. L'interprétation aussi peut passer par l'intermédiaire d'un un ''bytecode'', ce n'est pas limité aux compilateurs proprement dit. {| |[[File:Intermediate representation scheme.png|centre|vignette|upright=1.5|Représentation intermédiaire d'un compilateur.]] |[[File:Code interpreter scheme.png|centre|vignette|upright=1.5|Fonctionnement d'un interpréteur.]] |} Faire ainsi a de nombreux avantages, le principal étant d'avoir un compilateur capable de traduire un langage de haut niveau vers plusieurs jeux d'instructions différents. Cela permet d'avoir, par exemple, un compilateur qui traduit du C soit en code machine x86, soit en code machine pour un CPU ARM, soit pour un CPU POWERPC, etc. [[File:Compiler design.svg|centre|vignette|upright=3|Compiler design]] Pour l'interprétation, l'usage d'un ''bytecode'' a d'autres avantages. Pour l'exploiter, il faut distribuer non pas le code source, mais le ''bytecode''. En clair, l'interpréteur ne prend pas en entrée le code source, mais du ''bytecode'' déjà compilé. La traduction est alors beaucoup plus simple, car le code source a déjà été partiellement compilé, par la traduction en ''bytecode''. Interpréter du code source est en effet assez compliqué : il faut effectuer des étapes d'analyse lexicale, sémantique, et bien d'autres. Avec du ''bytecode'', ces étapes ont été réalisées lors de la compilation du ''bytecode'', l'interpréteur a alors peut de choses à faire. L'interprétation du ''bytecode'' se fait instruction par instruction, au niveau du ''bytecode''. Une instruction du ''bytecode'' est traduite en une instruction machine équivalente, qui est exécutée. Si l'instruction est un peu complexe, il exécute une fonction/procédure qui fait la même chose. Pour résumer, le ''bytecode'' est ensuite traduit à la volée et exécuté instruction par instruction par un logiciel appelé l'interpréteur. [[File:Control table.png|centre|vignette|upright=2|Control table]] Le langage intermédiaire peut être vu comme l'assembleur d'un processeur, n'existe pas forcément dans la réalité, mais dont le jeu d'instruction est décrit en détail. Le processeur en question est appelé une '''machine abstraite''', ou encore une ''machine virtuelle''. Nous utiliserons le terme de machine abstraite dans ce qui suit. La machine abstraite n'est pas la même suivant que l'on cible la compilation ou l'interprétation/JIT. Pour l'interprétation/JIT, la machine abstraite est souvent une machine à pile, car cela simplifie grandement la traduction en code machine final. L'interprétation demande que la traduction du ''bytecode'' en code machine soit la plus simple possible. Et cela demande de prendre en compte pas mal de détails. Par exemple, les différents jeux d'instruction existants n'ont pas le même nombre de registres, ce qui pose problème lors de la traduction du ''bytecode''. Lors de la transformation en code machine, un algorithme d'allocation de registres se débrouille pour traduire le code intermédiaire en code qui utilise un nombre limité de registres. Pour améliorer cette allocation de registres, il y a deux solutions niveau ''bytecode'' : soit utiliser un nombre illimité de registres, soit utiliser une machine à pile. Pour les machines à pile, il existe un algorithme simple et rapide pour traduire un code écrit pour une machine à pile en un code écrit pour un processeur avec des registres, qui se débrouille pas trop mal pour allouer efficacement les registres. C'est un avantage assez important pour les langages interprétés. ==Les processeurs qui exécutent du bytecode== Le ''bytecode'' est un code machine, ce qui signifie qu'il peut en théorie s'exécuter sur un processeur qui implémente le jeu d’instruction associé. Si le jeu d'instruction d'un ''bytecode'' est souvent une description censée être fictive, elle n'en reste pas moins un jeu d'instruction et des caractéristiques précises, qu'on peut l'implémenter en matériel ! Néanmoins, tous les ''bytecode'' ne sont pas égaux de ce point de vue. Certains sont faciles à implémenter en matériel, d'autres non. Les ''bytecodes'' implémentés en matériel sont ceux dont les machines abstraites sont des machines à pile. Un premier exemple est celui des processeurs Pascal MicroEngine, qui exécutaient directement le bytecode du langage Pascal, le fameux UCSD P-code. Un second exemple est la '''machine SECD''', qui sert de langage intermédiaire pour certains compilateurs de langages fonctionnels. Elle a été implémentée en matériel par plusieurs équipes, notamment par les chercheurs de l'université de Calgary en 1989. Dans le même genre, quelques processeurs simples étaient capables d’exécuter directement le ''bytecode'' du langage FORTH. Le FORTH, un des premiers langages à pile de haut niveau, possède de nombreuses implémentations hardware et est un des rares langages de haut niveau dont le ''bytecode'' a été utilisé comme langage machine sur certains processeurs. Par exemple, on peut citer le processeur FC16, capable d’exécuter nativement du ''bytecode'' FORTH. ===Les processeurs Java=== Le cas le plus impressionnant est celui de la machine virtuelle Java, qui est un design de processeur comme un autre. En temps normal, le ''bytecode'' Java est compilé ou interprété, mais certains processeurs exécutaient du ''bytecode'' Java directement, sans interprétation ni compilation. Ils sont appelés des '''processeurs Java'''. On peut citer les processeurs ARM disposant de l'extension Jazelle, du JEMCore et du aJ-100 de aJile Systems, le picoJava II de Sun, et quelques autres. Il y a aussi eu quelques projets de recherche ou processeurs open source, comme Komodo, jamuth, le ''Java Optimized Processor'' et quelques autres projets du même genre. L'implémentation de la machine virtuelle Java n'était cependant pas complète. Quelques instructions complexes n'étaient pas gérées par le processeur et devaient être émulées en logiciel. Mais la grosse majorité des instructions l'était. Les instructions non-supportées étaient émulées avec la méthode ''trap and emulate'', à savoir qu'un opcode inconnu déclenchait une exception matérielle ''opcode invalide'', dont la routine pouvait émuler les instructions qui doivent l'être. Le processeur aJ-100 de aJile est capable d'exécuter la quasi-totalité des instructions de la JVM Java, seules deux instructions faisant exception. Les '''accélérateurs Java''' sont une solution intermédiaire, qui permet de traduire à la volée du ''bytecode'' Java en instructions RISC, exécutées sur un autre processeur. Il s'agit formellement de traduction binaire, mais je préfère en parler ici. L'idée est de combiner un processeur RISC avec un coprocesseur Java. Le coprocesseur lit le ''bytecode'' Java et le traduit en instructions RISC, ces dernières étant exécutées sur le processeur RISC. Le JA108 de Nozomi était un coprocesseur de ce genre, au même titre que le coprocesseur JSTAR de JEDI. L'extension Jazelle de certains CPU ARM fonctionnait vraisemblablement sur un principe similaire, sauf que le coprocesseur était intégré dans le processeur ARM, entre le cache d'instruction et le décodeur d'instructions ARM. L'intérêt des processeurs Java n'est pas qu'une question de performance. Certes, ils permettent d’exécuter plus vite les programmes compilés en ''bytecode'', comme des programmes Java pour la JVM Java. Mais l'intérêt est aussi de faire des économies de mémoire RAM et ROM. Je rappelle que la machine virtuelle Java est une machine à pile, ce qui fait que sa densité de code est excellente. Les programmes en ''bytecode'' Java sont donc très petits, et prennent peu de place en mémoire ROM. De plus, utiliser un processeur Java permet de se passer d'interpréteur ou de JIT Java, ce qui économise encore plus de mémoire. ===Transformer un processeur RISC en machine à pile=== La plupart des processeurs Java utilisent en interne une architecture à registre généraux, qui émule une machine à registre en microcode. Quelques registres sont réservés pour l'état de la machine virtuelle, avec au minimum un registre réservé pour le pointeur de pile, un autre pour un pointeur vers un ''pool'' de constantes, et quelques autres. D'autres registres servent à mémoriser le haut de la pile, à savoir le sommet de la pile et quelques opérandes situées juste en-dessous. Par exemple, le processeur picoJava II gardait les 64 opérandes au sommet de la pile dans 64 registres généraux, séparés des registres pour les pointeurs et l'état de la JVM. Une optimisation possible, envisagée sur les processeurs picoJava, était une sorte de macro-fusion sous stéroïde. L'idée était de fusionner certaines séries d'instructions Java en une seule instruction machine. Typiquement, pour faire une opération arithmétique, une machine à pile empile deux opérandes et exécute une opération, ce qui prend trois instructions machine. La spécification picoJava 1 a proposé d'envoyer directement la seconde opérande en entrée de l'ALU, ce qui fusionnait l'empilement de la seconde opérande et l'opération arithmétique. Le processeur picoJava II va plus loin et fusionne les trois instructions en une seule instruction RISC, agissant sur des registres. Mais la majorité des processeurs Java ne faisaient pas cela, car cela complexifie grandement le décodeur. Les processeurs ''bigfoot'' de DCT utilisaient un système différent pour émuler une pile avec des registres. Leur banc de registre de 64 bits était coupé en deux : les 16 premiers registres étaient utilisés pour la pile, les 48 suivants étaient utilisés pour autre chose. L'idée est que le registre 0 mémorise un emplacement vide, le registre 1 contient le sommet de la pile, le registre 2 l'opérande sous le sommet de la pile, et ainsi de suite. Les 15 premières opérandes de la pile sont donc mémorisées dans les registres. Les instructions écrivent leur résultat dans le registre adéquat, puis tous les registres sont décalés pour que le résultat passe dans le registre R1. Par exemple, les instructions PUSH chargent une opérande dans le registre 0, puis tous les registres sont décalés de un rang, pour que l'opérande chargée soit dans le registre 1. Le registre 0 devient alors le registre numéro 1, le registre 1 devient le 2, et ainsi de suite. Par contre, les opérations arithmétiques font autrement. Une instruction d'addition, par exemple, lit les opérandes dans le registre 1 et 2, puis place son résultat dans le registre 2 (les deux opérandes sont dépilées). Puis, les registres sont décalés d'un rang pour que ce registre 2 devienne le 1. Les registres ne sont en réalité pas décalés, il n'y a pas de transferts entre registres. A la place, les 16 premiers registres utilisaient une forme limitée de renommage de registres, similaire au banc de registre tournant utilisé sur les processeurs EPIC comme l'Itanium. Les numéros de registres sont renommés, décalés, à chaque fois qu'on empile ou dépile une opérande. Pour faire le renommage de registres, processeur contient un ''register stack counter'' de 4 bits, qui est incrémenté à chaque fois qu'on empile une donnée, et est décrémenté quand on dépile une donnée. Quand on veut accéder à un opérande, le numéro de registre architectural associé est traduit en numéro de registre physique, en additionnant le ''register stack counter'' (modulo 16, pour rester sur 4 bits). ==L'accélération de l'interprétation/JIT : le ''Thumb-EE'' d'ARM== Quelques processeurs ont ajouté des instructions pour faciliter le travail des interpréteurs/JIT. Par exemple, en 2005, ARM a ajouté le mode ''Thumb Execution Environment'', qui faisait cela. Il reprenait le jeu d'instruction compact ''thumb'' et ajoutait quelques instructions et modifiait le comportement d'instructions existantes. Le jeu d'instruction ''Thumb-EE'' a été ajouté sur les CPU ARM en 2005, mais a été déprécié en 2011, par manque d'utilité. Mais la tentative mérite qu'on s'attarde dessus. Les CPU de gamme M, déstinés à l'embarqué, n'ont jamais supporté ''Thumb-EE'', vu que de tels processeurs ne sont pas conçus pour exécuter du code interprété/JIT. Le mode ''Thumb-EE'' est un mode d'exécution, séparé de l'ARM normal. Ainsi, une instruction ''Thumb-EE'' et une instruction ARM peuvent avoir le même encodage en binaire, mais se comporter différemment. Le processeur est à tout moment soit en mode ''Thumb-EE'', soit en mode ARM normal, ce qui précise comment ces instructions doivent se comporter. Des instructions ont été ajoutées par ''Thumb-EE'', pour qu'il fasse son travail. L'entrée et la sortie du mode ''Thumb-EE'' se fait avec deux instructions : ENTERX et LEAVEX. Une fois en mode ''Thumb-EE'', le décodage des instructions ''thumb'' se fait avec les règles du ''Thumb-EE'', et non celles du ''thumb'' normal. Une première différence entre ''thumb'' normal et ''Thumb-EE'' est l'ajout de ce qui s'appelle un ''null check'' pour les instructions mémoire. Le ''null check'' est utilisé pour les instructions en mode d'adressage "base + indice" ou "Base + décalage", ou tout autre mode d'adressage avec un registre de base utilisés dans des calculs d'adresse. L'idée est de vérifier si l'adresse de base vaut zéro ou non. Si c'est le cas, le processeur lève une exception matérielle, qui est traitée par l'interprétation ou le compilateur JIT. Une telle situation est en effet signe d'une erreur d'adressage mémoire, qui doit être traitée par l'interpréteur. Typiquement, c'est signe qu'un pointeur n'a pas bien été initialisé, ce qui peut arriver avec du code interprété ou JIT. Les interpréteurs/JIT traitent généralement la situation en ajoutant un test avant toute instruction mémoire, pour vérifier si le pointeur accédé vaut zéro ou non. Avec ''Thumb-EE'', pas besoin d'ajouter les tests en questions : ils sont réalisés automatiquement lors de chaque instruction mémoire. : Les registres pour la pile, comme le pointeur de pile et le pointeur de ''frame'', sont aussi concernés. L'instruction CHKA vérifie que les accès à un tableau ne débordent pas en dehors du tableau en question. Par exemple, pour un tableau de 1024 éléments, elle vérifie si l'indice est compris entre 0 et 1024. Pour cela, elle vérifie si l'indice de tableau est bien dans l'intervalle adéquat et lève une exception matérielle si ce n'est pas le cas. Son utilité se comprend quand on sait que tous les accès à un tableau sont vérifiés par l'interpréteur. L'interpréteur est censé ajouter des instructions pour vérifier les indices, à savoir deux ou trois branchements. Avec CHKA, le test se fait en une seule instruction. D'autres modifications mineures de l'encodage des instructions sont aussi présentes, ainsi que des modifications pour les instructions LOAD/STORE. Précisément, dans les modes "base + indice" et "base + décalage", le décalage et l'indice subissent maintenant des décalages. L'idée est de simplifier le calcul d'adresse. De quoi économiser quelques instructions lors des calculs d'adresse, ce qui facilite le travail de l'interpréteur/JIT. Et des instructions LOAD/STORE précises voient leur comportement modifié pour faciliter la gestion de la pile de Java ou l'exécution des méthodes locales. Pour faciliter le travail de l'interpréteur, deux instructions HBP et HBLP ont été ajoutées. Elles branchent vers une fonction qui gère les exceptions logicielles (une fonctionnalité présente dans de nombreux langages, comme Java). La première effectue un branchement vers la fonction en question, la seconde sauvegarde l'adresse de retour avant de faire ce branchement. Les deux peuvent brancher vers 256 fonctions pré-déterminées, sans avoir à présenter leur adresse. ==La traduction binaire accélérée par le matériel== Plus haut, nous avons surtout parlé des compilateurs et des interpréteurs. Cependant, nous devons aussi parler de la '''traduction binaire'''. Elle traduit un programme écrit dans un code machine vers un autre code machine. Par exemple, elle traduit un programme compilé pour un CPU x86 vers un code machine ARM. En général, le système d'exploitation est généralement compilé pour le jeu d'instruction natif, mais il exécute des applications prévues pour le x86. Les applications sont traduites par le système d'exploitation, avant d'exécuter le code traduit. : Nous parlerons dans la suite de code machine source et de code machine cible, pour parler respectivement du code à traduire et du code obtenu après traduction. Idem avec d'autres termes comme architecture cible/source, ou jeu d'instruction cible/source. La traduction binaire est surtout utilisée pour des questions d'émulation ou de compatibilité. Par exemple, lorsque les Macintosh sont passés de processeurs Power PC vers des processeurs x86, le système d'exploitation Mac OS utilisait la traduction binaire pour convertir les anciennes applications Power PC vers du code x86. Les utilisateurs n'y ont vu que du feu. Le système de traduction binaire était appelé Rosetta 1. Par la suite, lors de la transition de processeurs x86 vers des processeurs Apple, Rosetta 2 a vu le jour. La traduction binaire a surtout été utilisée pour traduire du code x86 vers un autre jeu d'instruction. Il faut dire que le x86 est le jeu d'instruction dominant. De nombreuses entreprises ont eu pour ambition de briser l'hégémonie du x86 sur PC, en remplaçant le x86 par un jeu d'instruction plus performant, tout en gardant une compatibilité maximale. La traduction binaire était la seule solution pratique. Elle était le plus souvent intégralement réalisée en logiciel, comme c'était le cas sur les architectures Itanium avec le ''IA-32 Execution Layer''. ===Les généralités sur la traduction binaire assistée en matériel=== L'architecture source est presque tout le temps une architecture CISC, assez ancienne, qu'on souhaite émuler. Le choix de l'architecture cible se porte souvent sur une architecture VLIW, fort différente de l'architecture source, et ce pour obtenir une économie de matériel conséquente. Pour s'exécuter rapidement, le code traduit doit exploiter le parallélisme d'instruction, à savoir exécuter plusieurs instructions en même temps dans des unités de calcul séparées. Il est possible d'utiliser un processeur superscalaire avec exécution dans le désordre pour cela, mais au prix d'un cout important en transistors. Alors qu'en utilisant un CPU VLIW, c'est le traducteur binaire qui fait tout le travail d'extraction du parallélisme d'instruction. Et après tout, quitte à avoir un traducteur binaire, autant lui refiler le boulot d'optimisation. Compiler du code à la volée est certes assez lourd, mais qu'il existe des algorithmes efficaces pour regrouper des instructions indépendantes dans une seule instruction VLIW. Au passage, le processeur VLIW a plus de registres que le processeur source. Cela permet de faire du renommage de registres directement en logiciel. Le traducteur binaire n'hésite pas à changer les noms de registres entre instructions source et instruction VLIW cible, afin de supprimer des dépendances de données. En conséquence, cela ouvre des opportunités de parallélisme, qui sont exploitées lors du regroupement des opérations en instructions VLIW. Vous pourriez penser que le choix d'un CPU VLIW pour émuler du CISC est tout sauf optimal. Vous devez penser que la traduction binaire est d'autant plus simple que l'architecture source et cibles sont semblables. Dans les faits, ce n'est pas tellement traduire les instructions qui pose problème, mais plus la gestion du registre d'état, des exceptions matérielles, de la mémoire virtuelle, la différence entre gros-boutisme et petit-boutisme, de même que des différences d'adressage pour les périphériques. Le premier problème est la gestion des conditions, notamment en présence d'un registre d'état. En pratique, la traduction binaire s'utilise pour traduire du code CISC vers du code RISC ou VLIW. Les architectures source ont donc un registre d'état, qui est mis à jour non seulement par des instructions de test, mais aussi des instructions arithmétiques. L'architecture cible n'a elle pas de registre d'état, mais des registres à prédicats. La traduction de l'un vers l'autre est alors quelque peu compliquée. Et elle est d'autant plus compliquée que le registre d'état est la source de dépendances d'instruction implicites, qui réduisent les performances. Par exemple, si une instruction arithmétique modifie le registre d'état, cela peut impacter l'exécution d'une instruction ultérieure, qui lit ce registre d'état. Pour éliminer ces fausses dépendances, le traducteur binaire doit renommer ce registre en logiciel et détecter les dépendances utiles. Et le compilateur doit gérer les cas où il y a beaucoup de distance entre les deux, voire les cas où l'instruction dépendante n'a pas encore été analysée par le compilateur. Un autre problème est lié à la gestion des exceptions matérielles, et précisément des exceptions précises. Pour rappel, une instruction VLIW regroupe plusieurs opérations, opérations qui correspondraient à une instruction machine sur un CPU pas VLIW. En conséquence, plusieurs instructions du langage machine "source" sont regroupées en une seule instruction VLIW. Et il faut tenir compte du cas où une instruction source lève une exception. Dans le code VLIW, cela signifie qu'une opération lève une exception, et il faut annuler partiellement l'instruction VLIW associée. Et par partiellement, on veut dire que seules les opérations suivantes dans l'ordre du programme source doivent être annulées, puis ré-exécutées. Et c'est un sacré casse-tête ! Les processeurs VLIW utilisent comme solution le mécanisme d'exceptions différées des processeurs EPIC, vu il y a quelques chapitres. Pour résumer, le code est exécuté par blocs d'instruction, délimités par des branchements ou tout autre limite/barrière pertinente dans le code. Un bloc de code est compilé en deux versions : une version rapide sans exceptions matérielles, une version lente qui gère les exceptions précises. La version rapide s'exécute sans exécuter les exceptions. Cependant, les exceptions matérielles sont enregistrées, pour être prises en compte à la toute fin du bloc de code. La version rapide est exécutée en premier et elle mémorise si une exception matérielle a eu lieu. Puis une instruction vérifie si une exception a eu lieu et décide quoi faire. Si aucune exception n'a eu lieu, elle passe à la suite du programme, les résultats du bloc de code sont définitivement acceptés. Mais si une exception a eu lieu, tout est annulé. Le processeur est remis dans l'état initial, puis le code est ré-exécuté instruction par instruction de manière à gérer l'exception correctement. ===La traduction par pré-décodage=== Une première solution serait de faire la traduction binaire dans le cache d'instruction. Lors d'un défaut de cache, le code chargé depuis la RAM est traduit en code machine cible. Et c'est ce code machine cible qui est mémorisé dans le cache d'instruction et exécuté par le processeur. La technique marche sur le papier et n'est qu'une amélioration des techniques de pré-décodage vues il y a quelques chapitres. Cependant, le code obtenu est une traduction assez basique, qui n'incorpore pas d'optimisations dignes de ce nom. De plus, elle gère mal le cas où la taille du code source et cible sont potentiellement très différentes. Concrètement, elle est surtout utile pour traduire du code d'un processeur RISC vers un autre processeur RISC, dont les tailles d'instructions sont similaires. Elle ne permet pas d'émuler plusieurs jeux d'instructions différents, le cout en matériel (un circuit de pré-décodage par jeu d'instruction) serait trop important. ===Le projet DAISY d'IBM=== De très rares processeurs étaient conçus pour accélérer cette traduction binaire, afin de garder de bonnes performances. Les premiers à avoir étudié l'idée étaient IBM, avec leur projet DAISY (''Dynamically Architected Instruction Set from Yorktown''). Le projet de base était de convertir à la volée du code compilé pour des CPU Power PC, vers du code VLIW. Les chercheurs d'IBM avaient développé un algorithme de traduction dynamique efficace, ainsi qu'un processeur VLIW disposant d'optimisations spécifiques à la traduction binaire dynamique, à la volée. De nombreuses idées de ce projet ont été reprises ou re-découvertes par la société Transmetta, avec ses processeurs Crusoe et Efficieon, puis par NVIDIA avec son projet Denver. Les ingénieurs de ce projet ont étudié la possibilité d'émuler plusieurs jeux d'instructions différents sur un même processeur, notamment le s390 d'IBM et le x86. En théorie, cela demande juste d'avoir plusieurs programmes de traduction binaire : un pour le x86, un autre pour le s390, éventuellement un autre pour le Power PC. Mais cela ne s'est pas concrétisé. Mais les ingénieurs ont étudié quelle pourrait être l'architecture VLIW idéale pour ça. Par exemple, il fallait des additions 3-opérandes pour simplifier les calculs d'adresse. Une difficulté était la gestion du registre d'état, dont les bits ne sont pas mis à jour de la même manière sur le x86 et le s390 ou le Power PC. La différence entre gros-boutisme et petit-boutisme était aussi un problème, de même que des différences d'adressage pour les périphériques. Pour que la traduction binaire soit efficace, le processeur VLIW intègre diverses optimisations, comme les exceptions différées et les branchements multi-voies. La principale est la suivante : seul le code des boucles ou fonctions exécutées fréquemment est traduit en code VLIW, le reste du code est interprété. En effet, traduire du code binaire prend plus de temps qu'une simple interprétation. Pour du code qui ne sera exécutée qu'une seule fois, il est plus rapide d'utiliser l'interprétation. Par contre, pour du code exécuté beaucoup de fois, le cout de la traduction binaire est amorti, dilué sur N exécutions, compensé par le gain en temps d'exécution de ce code traduit. Au final, cela permet de ne traduire que le code qui le mérite. La détection du code fréquemment exécuté est réalisée dans le cœur VLIW. L'unité de branchement mémorise les derniers branchements rencontrés et le nombre de fois qu'ils ont été exécutés. Si ils ont été exécutés un certain nombre de fois, l'unité de branchement lève une exception matérielle, qui invoque le traducteur binaire. La routine de cette exception est le traducteur binaire proprement dit. En clair, la traduction binaire est démarrée quand le processeur détecte qu'une fonction a été exécutée plus de N fois, via une exception matérielle dédiée. La mesure du nombre d'exécution d'un branchement se fait dans l'unité de calcul dédiée aux branchements, avec l'aide d'une sorte de ''branch adress buffer'' modifié. Pour rappel, le ''branch adress buffer'' mémorise le ''Program counter'' de chaque branchement récemment rencontré. Ici, chaque entrée du ''branch adress buffer'' est associée à un compteur incrémenté à chaque exécution du branchement. ===Les processeurs Crusoe et Efficieon de Transmetta=== Les processeurs Crusoe Et Efficeron sont deux processeurs VLIW produits par la société Transmetta, une société californienne rachetée par NVIDIA. Ils étaient conçus pour exécuter spécifiquement des programmes x86, système d'exploitation inclus. L'idée derrière ce projet était d'exécuter du code x86 sans que les applications, ni même le système d'exploitation et le BIOS soient au courant ! Les processeurs Transmetta étaient en réalité conçus pour exécuter un programme unique, le ''Code Morphing Software'' (CMS), qui traduisait le code x86 en code VLIW. Transmetta ne voulait pas que son processeur VLIW soit exploité directement, sans le CMS. Il n'avait pas rendu public de compilateur pour traduire du C vers du code VLIW, il n'avait pas donné la documentation du jeu d'instruction VLIW. Des efforts de rétro-ingénieurie, documentés sur le site realworldtech, ont cependant permis de comprendre comment étaient encodées les instructions du processeur Crusoe. Comme pour le projet Daisy, le ''Code Morphing Software'' utilisait à la fois interprétation et traduction binaire, selon les besoins. Les instructions sont interprétées lors de leurs premières exécutions, mais le CMS bascule sur de la traduction en code VLIW après un certain nombre d'exécution. Ainsi, les boucles souvent exécutées sont traduites en code VLIW, alors que le reste du code est interprété. Le choix du code à traduire est le fait du CMS, il utilise des heuristiques complexes pour, qui ne sont pas connues dans le détail. Le ''Code Morphing Software'' est mémorisé dans une EEPROM, ce qui en fait un ''firmware'' situé en dessous du BIOS. Le CMS démarre ensuite le BIOS, qui lui-même démarre le système d'exploitation, qui lui-même démarre les pilotes de périphériques et les programmes. Le CMS se réserve les 16 premiers mébioctets de l'espace d'adressage. L'EEPROM du CMS est mappée dedans, mais n'en utilise que la moitié. Le reste est utilisé comme cache, pour mémoriser le code VLIW traduit par le CMS. Les deux processeurs intégraient aussi deux mémoires SRAM, utilisées par le CMS, appelées la ''local program memory'' (LPM) et la ''local data memory'' (LDM). La première contient du code qui gére les interruptions, la mémoire virtuelle, les exceptions matérielles, les problèmes d'alignement mémoire, et quelques fonctions très fréquemment utilisées par le CMS. La seconde est de la mémoire RAM utilisée par ces fonctions de la ''local program memory''. Les mémoires caches L1 et L2 sont séparées de ces deux mémoires. Le processeur Crusoe et Efficieon étaient des processeurs VLIW très simples. Ils n'avaient même pas de MMU, ni de fonctionnalités importantes sur les CPU x86. Pour Crusoe, les instructions VLIW étaient encodées sur 64 ou 128 bits, et regroupaient entre 2 à 4 opérations. Les instructions LOAD/STORE ne supportaient pas d'adressage indicé, juste de l'adressage indirect à registre. Crusoe avait 5 unités de calcul : deux ALU entières, une unité LOAD/STORE, une unité de branchement, une FPU. Efficieon doublait le nombre d'ALU entière et d'unité LOAD/STORE, les instructions VLIW passaient à 256 bits. Le processeur Crusoe contient 160 registres, dont 64 registres généraux, 32 registres flottants. Les registres généraux font 32 bits, ce qui est cohérent avec le fait que les CPU x86 de l'époque étaient des processeurs 32 bits. Quant aux registres flottants, ils faisaient 80 bits, ce qui colle avec la taille des registres flottants de la FPU x87 utilisée à l'époque. Sur les 64 registres généraux, seuls 48 étaient réellement utilisable pour mémoriser des opérandes. Une partie des registres généraux étaient utilisés pour la gestion de la pile, d'autres pour mémoriser l'état du CPU x86 émulé, un registre était un registre zéro non accesible en écriture. La gestion des exceptions est optimisée avec un système d'exceptions différées, le même que celui décrit plus haut. Le processeur utilise un système similaire, pour effectuer des lectures anticipées, qui a été expliqué dans le chapitre sur les processeurs VLIW/EPIC. Sauf qu'il s'agit là d'une optimisation pour gagner en performances en général, pas une aide utile pour gérer des exceptions précises ou toute autre subtilité de la traduction binaire. Pour gérer les exceptions différées et les lectures anticipées, le processeur mémorise l'état du processeur avant de démarrer un bloc de code. Pour cela, le processeur contient des ''shadow registers'', 48 registres entiers et 16 registres flottants. Vous remarquerez qu'il y a autant de registres entiers que de ''shadow registers'' entiers. Les registres entiers/flottants sont mémorisés dans les ''shadow registers'' avant d'exécuter un bloc de code, afin que le processeur puisse revenir à l'état de base. Une autre fonctionnalité liée est le ''gated store buffer''. Le principe est d'éviter toute écriture en mémoire RAM, tant que le code VLIW n'a pas émis d'instruction ''commit''. L'idée est que les écritures sont conservées dans la ''store queue'' de l'unité LOAD/STORE. Pour rappel, la ''store queue'' met en attente les écritures soit tant que les écritures ne sont pas terminées, soit tant que la RAM est occupée. L'idée est que les écritures sont accumulées dans la ''store queue'' tant que l'instruction ''commit'' n'a pas eu lieu. Le ''store queue'' est "déversé dans la RAM" seulement quand l'instruction ''commit'' s'exécute. ===Les CPU NVIDIA Denver=== Nvidia a tenté quelque chose de similaire avec son projet Denver. NVIDIA a racheté l'entreprise Transmetta, ses brevets, et a tenté de refaire la même chose. Le projet initial était de traduire du code x86 vers un jeu d'instruction VLIW propriétaire, comme l'a fait Transmetta. Mais NVIDIA n'a pas réussi à acquérir la licence du jeu d'instruction x86 et s'est rabattu sur le jeu d'instruction ARM. Le processeur né de ce projet est le Tegra K1-64 CPU. Il s'agit d'un processeur composé d'un cœur VLIW, couplé à deux décodeurs d'instructions ARM. Les deux décodeurs ARM traduisent une paire d'instructions ARM consécutives en instructions VLIW, pour les exécuter sur le cœur VLIW. Le CPU peut fonctionner selon deux modes : ARM et VLIW. En somme, le processeur supporte deux jeux d'instruction : le jeu d'instruction ARM, et un jeu d'instruction VLIW. En mode ARM, les instructions ARM sont chargées depuis le cache, traduites en VLIW par les décodeurs ARM, puis exécutées par le cœur VLIW. En mode optimisé, le CPU exécute des instructions VLIW chargées depuis le cache d'instruction, les décodeurs ne sont pas utilisés. : Le CPU supporte en réalité deux jeux d'instruction ARM : ARM8 et 7. Mais ils correspondent au même mode pour le CPU. Un point important est que le processeur peut changer très rapidement de mode, en à peine quelques cycles d'horloges. On n'est clairement pas dans le cas des CPU x86, qui mettent des plombes pour passer du mode 32 à 64 bits et inversement. La commutation est tellement rapide qu'on peut considérer que le processeur supporte deux jeux d'instruction simultanément. Et le support simultané de deux jeux d'instruction facilite l'implémentation de la traduction binaire. Encore une fois, inutile de traduire du code qui ne sera exécutée qu'une seule fois, mieux vaut l'exécuter directement dans le mode ARM. Surtout que le processeur supporte un mode ARM qui permet d'exécuter du code ARM sans perte de performance. Par contre, le code fréquemment utilisé, notamment dans des boucles critiques, est traduit en VLIW. Au final, les performances sont optimisées, en limitant le travail du logiciel traducteur binaire. La détection du code fréquemment exécuté se fait différemment que pour les processeurs Transmetta et Daisy. L'unité de branchement ne détecte pas directement le code beaucoup exécuté, même si elle a un rôle à jouer. A la place, la détection est réalisée en logiciel, par un ''thread'' dédié, qui s'exécute sur un cœur séparé. Il détecte le code fréquemment utilisé en regardent les branchements exécutés récemment. Pour cela, le processeur mémorise l'historique des branchements pris récemment et copie cette historique dans une mémoire tampon dédiée, partagée entre tous les cœurs. Vu qu'elle est partagée, le ''thread'' de détection a accès à l'historique sans pertes de performances, seule la copie de l'historique dans ce tampon a un cout en performance, pas son partage. Le code VLIW obtenu après traduction binaire est mémorisé dans la mémoire RAM, dans une portion spécialement réservée pour. Elle fait 128 mébioctets, et est appelée le '''cache d'optimisation''' par NVIDIA. Le cache d'optimisation est protégé en écriture et seul le traducteur binaire peut écrire dedans. Au passage, le traducteur binaire est du code VLIW, ce qui fait qu'il est placé dans le cache d'optimisation. Le processeur détecte automatiquement quand une fonction pour laquelle le code compilé équivalent est disponible. Pour cela, l'unité de chargement contient une table de correspondance entre l'adresse de la fonction ARM, et l'adresse de son équivalent compilé. Quand une fonction est appelée, l'unité de chargement regarde l'adresse de destination du branchement. Si l'adresse est dans cette table, elle récupère l'adresse de la fonction compilée et branche vers celle-ci. Le processeur dispose d'un cache d'instruction de 128 kibioctets, ce qui est très important, mais nécessaire vu la taille des instructions VLIW. Le coeur VLIW dispose de ses propres décodeurs, d'un ''scoreboard'', de 7 unités de calcul et de registres. Pour les unités de calcul, il a deux FPU, deux ALU, une unité de branchement, et deux unités LOAD-STORE faisant aussi ALU. Un point important est que le CPU dispose de 64 registres entiers et de 64 registres flottants. C'est deux fois plus que supporte le jeu d'instruction ARM. En mode ARM, seule la moitié des registres est utilisée. Mais en mode optimisé, le traducteur binaire utilise bien les 64 registres, grâce à une sorte de renommage de registres logicielle. Pour simplifier la traduction binaire, le processeur supporte les techniques vues précédemment. Le processeur intègre un ''gated store buffer'' similaire à celui des processeurs Transmetta. Les techniques d'exceptions différées sont aussi supportées, comme sur les processeurs Transmetta. Il a aussi une unité de préchargement décrite par NVIDIA comme agressive, avec support du préchargement de type anticipé (''runahead''). ===L'émulation de la mémoire virtuelle=== Pour finir, il faut parler de la mémoire virtuelle avec la traduction binaire. Sur le processeur source, le processeur gère à la fois des adresses physiques et virtuelles et sa MMU fait la conversion entre les deux. Sur le processeur VLIW, la MMU est simulée par le programme de traduction binaire, partiellement ou totalement. L'implémentation exacte varie suivant que l'on parle du projet Daisy, des processeurs Transmetta ou de Denver. Sur les processeurs Transmetta, il n'y a pas de MMU ni de mémoire virtuelle. A la place, le programme de traduction binaire émule la mémoire virtuelle du processeur source. Le processeur VLIW ne gère que des adresses physiques, rien d'autre. L'espace d'adressage physique du processeur VLIW a la même taille que celui du processeur émulé, ici des processeurs x86. Il est vraisemblable que les adresses physiques utilisées par le processeur x86 sont les mêmes que celles du processeur VLIW. Sur le projet Daisy, le processeur VLIW gère la mémoire virtuelle via pagination, comme les processeurs émulés. Ce qui fait qu'il gère un espace d'adressage virtuel et un espace d'adressage physique. Pour éviter toute confusion, nous parlerons d'adresse physique/virtuelle VLIW pour les adresses physiques/virtuelles du processeur VLIW, d'adresse virtuelle/physique source pour celle du jeu d'instruction traduit, à savoir du Power PC ou du s390, éventuellement de l'x86. Il faut alors faire le lien entre adresses physiques source et adresses virtuelles VLIW. Pour cela, rien de plus simple : il y a correspondance parfaite. L'adresse physique source numéro N correspond à l'adresse virtuelle VLIW numéro N. Cependant, cela signifie que tout l'espace d'adressage virtuel VLIW serait utilisé par le code à traduire. En réalité, il faut ajouter de la place pour le code traduit et le programme de traduction binaire. La conséquence est que l'espace d'adressage virtuel VLIW est plus large que l'espace d'adressage source. L'espace d'adressage virtuel VLIW est découpé en trois sections : une section pour le code Power PC à traduire, une autre pour le code traduit en VLIW, et une dernière réservée au traducteur binaire. Le programme de traduction est placé dans une ROM mappée en mémoire dans la seconde section. Le reste de la seconde section est réservé à une mémoire RAM utilisée par le programme de traduction. [[File:Mémoire virtuelle sur le projet Daisy.png|centre|vignette|upright=1.5|Mémoire virtuelle sur le projet Daisy]] Il faut noter que chaque page physique source, une fois traduite, correspond à N pages virtuelles VLIW. La raison est que le code traduit prend plus de place que le code machine originel. En conséquence, on doit utiliser une page finale plus grande. Pour le reste, des pages contiguës en mémoire physique source sont elles aussi contiguës en mémoire virtuelle VLIW. Le calcul d'adresse est donc simple : l'adresse physique source est multipliée par N, puis on ajoute l'adresse de base à laquelle commence la section pour le code traduit. ==Les processeurs à jeu d'instruction reconfigurable== Nous venons de voir comment accélérer en matériel la traduction binaire. Mais il existe une alternative qui permet de se passer de traduction binaire, complétement. L'idée est d'avoir un processeur avec un '''jeu d'instruction reconfigurable'''. Le nom est assez parlent : on peut configurer le processeur de manière à supporter le jeu d'instruction de son choix ! Par exemple, on pouvait reconfigurer un CPU de manière à ce qu'il supporte le jeu d'instruction x86, ou qu'il exécute du ''bytecode'' Java, ou du code ARMv8, etc. De tels processeurs sont encore plus rare que ceux vu avant dans ce chapitre, mais ils ont existé ! Les processeurs de ce type avaient un '''microcode réinscriptible''', à savoir que c'était des processeurs microcodés pour lesquels on pouvait modifier le microcode. L'idée était qu'en changeant le microcode, on changeait le jeu d'instruction du processeur. Pour cela, le microcode était associé à une mémoire SRAM, en plus de la mémoire ROM du ''control store''. La SRAM était appelée la micro-SRAM, alors que le ''control store'' basique était appelé la micro-ROM. Le microcode était copié dans cette micro-SRAM au démarrage du CPU, puis les instructions étaient décodées en utilisant le microcode dans la micro-SRAM. En modifiant le contenu de la micro-SRAM, on pouvait altérer le microcode, voire le remplacer totalement. Il s'agit là de l'implémentation la plus simple. Mais le Burroughs B1700 a procédé autrement et il est intéressant d'étudier en détail son architecture. Son architecture est décrite dans le livre "Interpreting Machines : Architecture and Programming of the Bl700/Bl800 Series", écrit par Elliott I. Organick et James A. Hinds. Une partie de ce qui vfa suivre est un très court résumé de ce livre. ===Le Burroughs B1700 : architecture externe=== Il disposait de 4 registres généraux, nommés X, Y, T et L, tous de 24 bits. Les registres T et L étaient découpés en 6 sous-registres de 4 bits, nommés LA LB LC LD LE et LF pour le registre L, et TA TB TC TD TE et TF pour le registre T. Ils étaient adressables comme n'importe quel registre. Un point important est que le registre T avait des possibilités d’extraction spécifiques. Il était possible d'extrait N bits, placés n'importe où dans ce registre, et de les copier dans un autre registre. Nous verrons pourquoi cette possibilité est intéressante et ne sert que pour le registre T. Les registres X et Y étaient reliés à une ALU de 24 bits capable d'effectuer des opérations arithmétiques basiques. Fait étonnant, l'ALU faisait tous les calculs en même temps et fournissait ses résultats dans 7 registres : un registre SUM pour l'addition, un registre DIFFERENCE pour la soustraction, et un registre par opération bit à bit. Les registres pour les opérations bit à bit sont : CMPX et CMPY pour l'opération NOT sur chaque opérande, XANY pour le ET bit à bit, XEOY pour le XOR, XORY pour le OU logique. Les additions et soustractions pouvaient se faire sur des opérandes codés en binaire ou en BCD, suivant le code opération utilisé. A cela, il fallait ajouter deux registres de "résultat" MSKX et MSKY. Ils fournissaient un masque dépendant de la taille du résultat en bits. Cela permettait de gérer des données de 0, 4, 8, 12, 16, 20, et 24 bits, alors que l'ALU faisait 24 bits. Et cela n'était pas limité à 16 ou 8 bits, mais n'importe quelle taille inférieure ou égale à 24 bits était supportée, même des tailles non-conventionnelles comme des résultats codés sur 3 bits, 9 bits, 12 bits, etc. L'ALU peenait en opérande les registres X et Y, mais aussi un registre CYF pour la retenue entrante. En sortie, il fournissait aussi deux retenue sortantes : une pour l'addition et une autre pour la soustraction. Elles étaient mémorisées dans les registres CYL et CYD, respectivement. L'ALU fournissait aussi 12 conditions, qui alimentait un registre d'état. Le registre d'état était segmenté en trois registres appelés XYST, XYCN et BICN. Le processeur contenait de nombreux autres registres de 4 bits, dont les sous-registres de T et L. En tout, il y a 27 registres de 4 bits adressables ! Et le processeur disposait d'une ALU 4 bits pour manipuler ces registres de 4 bits ! Elle est capable de faire des MOV entre registres de 4 bits, des opérations bit à bit, et peut aussi tester si un bit vaut 0 ou 1. La dernière possibilité est très utile pour implémenter les branchements. Pour communiquer avec les entrées-sorties, il disposait de deux registres CMND et DATA : CMND pour envoyer une commande à une entrée-sortie, DATA pour lire ou écrire une donnée. Pour la mémoire, il avait deux registres séparés : READ et WRITE. READ contient la donnée lue lors d'une lecture,; WRITE est pour une donnée à écrire lors d’une écriture. Il s'agit de registres d’interfaçage mémoire, qui ont été promus au rang de registres architecturaux. Les deux registres sont gérés par le microcode, qui est le langage principal de la machine. Les instructions machines configurées font sans, mais le microcode doit y avoir accès facilement. Et vu que le microcode est le langage principal de ce processeur, ces deux registres deviennent architecturaux. ===Le burroughs B1700 : un ''split-level control store''=== Le burroughs B1700 lisait le microcode directement depuis la mémoire RAM ! Pour être précis, il avait plusieurs modèles : le B1726, le B1710 et le B1800. Le B1710 plaquait le microcode en mémoire RAM, ce qui fait que chaque exécution d'une instruction lisait les micro-opérations en mémoire RAM ! Le B1800 faisait pareil, mais avait ajouté un cache dédié au microcode, afin d'améliorer les performances. Le B1726 avait un microcode scindé en deux : une partie était en mémoire RAM, l'autre était dans un microcode réinscriptible classique, avec une micro-SRAM intégrée au processeur. Le processeur peut exécuter le microcode directement depuis la mémoire RAM, mais avec une pénalité en termes de performances. La documentation appelle cela un '''''split-level control store'''''. Le programmeur devait découper le microcode en segments, certains étant placé dans la micro-SRAM, les autres étant en mémoire RAM. Un tel microcode permettait de placer le microcode le plus fréquemment utilisé dans le processeur, alors que le reste du microcode était placé en mémoire RAM. Les instructions fréquentes étaient ainsi décodées avec la micro-SRAM, alors que les instructions peu fréquentes étaient décodées en lisant le microcode associé en mémoire RAM. La pénalité en termes de performance est alors concentrée sur les instructions les plus rares, ce qui est un très bon compromis. Mieux que ça, le processeur autorisait d'utiliser la technique de l'''overlaying'' pour le microcode ! Il y avait même une micro-opération OVERLAY dédiée pour ! Elle permettait de copier du microcode de la mémoire RAM vers la micro-SRAM ! OVERLAY avait besoin de trois opérandes : la taille du segment à copier, son adresse source en mémoire RAM, son adresse de destination dans la micro-SRAM. La micro-SRAM mémorisait au maximum 2048 micro-opérations, ce qui fait que les 2048 premières "adresses" correspondaient à la micro-SRAM. Les suivantes étaient des adresses de mémoire RAM qui contenaient le reste du microcode. En pratique, l'adresse qui séparait les deux portions du microcode était configurable ! On pouvait limiter la taille du microcode en micro-SRAM à un multiple de 32 micro-opérations ! Les adresses restantes étaient alors réattribuées à la RAM. Par exemple, on pouvait attribuer 64 adresses à la micro-SRAM, les 2048 - 64 adresses restantes étaient alors attribuées à la mémoire RAM. Un registre limite, nommé TDPM, indiquait quelle était l'adresse de démarcation entre les deux. Il contenait 4 bits, dont la valeur était multipliée par 32, pour obtenir l'adresse limite. Le microcode pouvait servir pour émuler un autre jeu d'instruction, mais il était aussi possible d'implémenter un véritable interpréteur ! L'interpréteur exécutait alors un code source écrit dans un langage de haut niveau, comme le FORTRAN ou le COBOL, qui était convertit à la volée en micro-opérations, sans passer par un langage machine intermédiaire ! Le processeur était même conçu pour ! L'interpréteur était placé dans un "segment" en mémoire RAM, dont le processeur connaissait la position. Il incorporait pour cela deux registres, un pour l'adresse de base de l'interpréteur, un autre pour son adresse de fin. Il incorporait une protection mémoire limitée sur ce segment, à savoir que seul l'interpréteur pouvait lire ou écrire dans ce segment. ===Le microcode réinscriptible=== Le microcode utilise des micro-opérations de 16 bits de long, ce qui est très peu et bien en-dessous des micro-opérations des autres processeurs. Les micro-opérations sortant du microcode sont envoyées au registre M, un registre de micro-opération de 16 bits. les micro-opérations écrites dans ce registre sont ensuite décodées par le hardware et exécutées. Fait intéressant, il était possible de faire un OU logique entre la micro-opération sortante et un registre du processeur, le résultat étant mémorisé dans le registre M. Cela permettait de faire du code automodifiant dans le microcode ! Utiliser du code automodifiant avec un microcode réinscriptible est parfaitement possible, et c'est utile pour émuler des modes d'adressage complexe dans le microcode. Cela permettait par exemple de remplacer une adresse dans une micro-opération. L'adresse est alors surimposée avec un OU dans le champ d'adresse, alors que le reste de l'instruction n'est pas modifié. Mais modifier directement le microcode est rarement une bonne idée. Aussi, pour éviter cela, l'idée est de modifier les instructions avec un OU logique avec un autre registre. Les micro-opérations du microcode ne sont donc pas modifiées dans le microcode, mais en sortie du microcode. Afin de gérer les interruptions, ces dernières étaient partiellement détectées en logiciel. Les interruptions ne s'exécutaient pas automatiquement, pour émuler correctement les interruptions. Une interruption doit être prise en compte à la toute fin de l'exécution d'une instruction, quand celle-ci a finit son travail. Mais vu qu'ici, les instructions sont émulées avec des séries de micro-opérations, il y a un problème : il est possible d'interrompre une instruction émulée en plein milieu. Pour éviter cela, le processeur ne déclenche pas les interruptions matérielles immédiatement, elles sont mises en attente. Une micro-opération dédiée vérifie si une interruption est en attente, et l'exécute alors cas échéant. La micro-opération est utilisée comme micro-opération finale d'une instruction émulée. ===Une architecture bit-adressable=== Si avoir un microcode réinscriptible est suffisant, quelques choix de ''design'' facilitent l'émulation de jeux d'instructions variés. Par exemple, le CPU Burroughs B1700 avait ajouté une fonctionnalité très importante : il était bit-adressable. En clair, il pouvait adresser la mémoire bit par bit, si nécessaire. L'avantage est que cela permettait d'émuler des architectures dont la taille des registres/''byte''/mots était très différente. Un autre avantage est que cela facilitait la gestion des instructions de taille variable. Détaillons rapidement le premier avantage. Le processeur était un CPU 24 bits, mais il pouvait émuler des processeurs 16 bits, 8 bits, 4 bits, 1 bit, 9 bits, ou toute autre valeur. Pour cela, le processeur lisait des mots de 24 bits et utilisant un ''barrel shifter'' pour sélectionner les bits adéquats. Par exemple, pour un accès de 16 bits, la donnée lue en mémoire était masquée de manière à ne garder que les 16 bits de poids faible. Le ''barrel shifter''/circuit de masquage était placé directement avant le bus mémoire, sur le trajet des données/instructions. Le ''barrel shifter'' était commandé par un registre ''Field Unit'', qui précisait quelle était la taille des données à charger. Le processeur gère un micro-''program counter'' appelé le registre A. Les micro-opérations du processeur font 16 bits de long, et elles sont alignées en RAM sur 16 bits, ce qui fait que les 4 bits de poids fort de ce registre valent 0. Le registre fait 18 bits, mais seuls les 14 bits de poids fort sont utiles. Les adresses mentionnées plus haut, qui adressent les micro-opérations en mémoire RAM, sont des adresses de mots de 16 bits, pas les adresses de bit individuels. ===La gestion de la taille des instructions=== Les instructions à décoder n'ont pas de longueur fixe, à savoir qu'elles peuvent faire 16 bit avec tel jeu d'instruction, 8 bits avec un autre, 32 bits sur un autre, etc. Elles peuvent aussi être de longueur fixe ou variable ! Le B1700 intégrait donc un support des instructions de longueur variable, qui était "détourné" pour gérer des jeux d'instruction différents. Notons que vu que l'architecture est bit-adressable, les contraintes d'alignement des instructions disparaissent, ce qui a des avantages qu'on va détailler dans ce qui suit ! L'implémentation des instructions de taille variable ajoutait un registre avant le registre d'instruction. Le registre d'instruction proprement dit était un registre M de 24 bits. Il était précédé par un registre T, dans lequel l'instruction était assemblée, en combinant plusieurs morceaux. Si l'instruction est codée est codée sur moins de 24 bits, le registre T se contente de masquer les bits en trop. Si l'instruction chargée dépasser 24 bits, le chargement se faisait en plusieurs passes, avec plusieurs lectures en mémoire. Dans ce qui suit, on suppose que l'instruction est composée de '''champs''' : l'opcode, les opérandes, les adresses absolues, les constantes immédiates, les numéros de registre, etc. L'idée est que l'instruction est chargée dans le registre T, puis découpée en champs, qui sont copiés dans les registres du processeur ou envoyés au microcode. Je rappelle que le registre T est relié à un ''barrel shifter'', différent au précédent, qui permettait de masquer, décaler et extraire une donnée de ce registre. Et il y a la possibilité d'extraire une suite de bit de ce registre T, pour la copier dans un autre registre. Par exemple, prenons une instruction composée d'un opcode, d'une constante immédiate et d'une adresse immédiate. L'opcode est lu et interprété par le microcode. Le microcode extrait la constante immédiate du registre T et la copie dans le registre X. Ensuite, l'adresse est extraite et envoyée au registre FA, une lecture est démarrée, et l'opérande est copiée dans le registre Y. Enfin, l'instruction est exécutée par le microcode. Le tout prend plusieurs micro-opérations : une première pour lire l'opcode, une seconde pour lire la première opérande, une troisième pour lire la seconde opérande, una quatrième pour tout exécuter. Notons que suivant la taille des champs, on peut en charger plusieurs à la fois. Par exemple, imaginons qu'on ait chargé 24 bits, contenant un opcode de 8 bits et une constante immédiate de 16 bits : pas besoin de faire deux accès mémoire, le registre T est juste découpé en un opcode et une constante. Le registre T est fait pour ce genre d'extraction. ===Le chargement d'un champ=== L'architecture est bit-adressable car elle gére des champs qui ne font pas forcément 8 ou 16 bits. Par exemple, certains jeux d'instructions ont des opcodes codés sur 6 ou 7 bits, voire 9-10 bits. Par exemple, prenons un opcode de 7 bits, suivi par une adresse de 12 bits et une constante immédiate de 15 bits : aucun de ces champs ne sera aligné sur 8/16/24 bits. Utiliser une architecture bit-adressable règle le problème d'alignement. Et en général, les contraintes d'alignement des instructions disparaissent ! Quand on charge une instruction, peut importe qu'elle soit ou non alignée sur 24 bits, elle sera placée dans les bits de poids faible, il suffira de masquer les bits de poids fort inutiles. Par exemple, si on charge une instruction de 16 bits, les 16 bits seront copiés dans les 16 bits de poids faible du registre T, il suffira de masquer l'octet de poids fort. Pas besoin de faire de décalages pour gérer des instructions mal alignées... Pour charger une instruction, il faut connaitre plusieurs informations : l'adresse de son premier bit, sa taille. Pour cela, le B1700 contient deux registres, nommés ''Field Adress'' (FA) et ''Field Length'' (FL), qui indiquent l'adresse du premier bit et la taille de l'instruction à charger. Le premier fait 24 bits, ce qui permet de gérer des adresses de 24 bits. N'oubliez pas que le processeur est bit-adressable, ce qui permet d'adresser 16 Mébi-bits, soit 2 mébioctets. Le registre pour la taille fait lui 16 bits, ce qui permet de gérer des instructions de 65536 bits, soit 8192 octets ! Leur nom commence par ''field'', car le processeur peut gérer des instructions de taille variable et/ou plus longues que 24 bits. Dans ce cas, le chargement de l'instruction se fait champ par champ. Pour donner un exemple, prenons une instruction composée de 20 champs de 12 bits chacun. Les champs sont chargés un par un dans le registre T. Le registre FA est alors initialisé avec l'adresse de l'instruction, il est incrémenté de 12 à chaque cycle. Le registre FL est initialisé avec la taille de l'instruction, soit 20 * 12 = 240 bits, et il est décrémenté de 16 à chaque cycle. Précisons cependant que la taille d'un champ peut être changée d'un cycle à l'autre. Par exemple, le processeur peut charger un opcode de 8 bits, l'interpréter, détecter que l'instruction demande ensuite de charger une adresse de 16 bits. Dans ce cas, le registre FL est immédiatement altéré pour charger 16 bits, au lieu des 8 bits de l'opcode immédiatement précédent. ==Les jeux d'instructions dédiés à un langage de programmation== De rares processeurs sont conçus pour un langage de programmation en particulier. On appelle ces processeurs, conçus pour des besoins particuliers, des '''processeurs dédiés'''. Par exemple, les fameux ''Burrough E-mode'' B5000/B6000/B7000 étaient spécialement conçus pour exécuter de l'ALGOL-60. Leurs cousins B2000/B3000/B4000 étaient eux conçus pour le COBOL. Des langages fonctionnels ont aussi eu droit à leurs processeurs dédiés. Le prolog en est un bel exemple, avec les superordinateurs de 5ème génération qui lui étaient dédié. On peut aussi citer les machines LISP, dédiés au langage LISP, qui datent des années 1970. Elles étaient capables d’exécuter certaines fonctions de base du langage directement dans leurs circuits : elles possédaient notamment un ''garbage collector'' câblé dans ses circuits ainsi que des instructions machines supportant un typage déterminé à l’exécution. Les processeurs dédiés ont eu leur heure de gloire au début de l'informatique, à une époque où les langages de haut niveau venaient d'être inventés. À cette époque, les compilateurs n'étaient pas performants et ne savaient pas bien optimiser le code machine. Il était alors rationnel, pour l'époque, de rapprocher le code machine cible et le langage de programmation de haut niveau. De nombreuses architectures dédiés ont ainsi été inventées, avant que les concepteurs se rendent compte des défauts de cette approche. Les défauts en question ne sont pas nombreux, mais assez simples à comprendre. Premièrement, elles sont très rapides pour un langage de programmation en particulier, mais sont assez mauvaises pour les autres, d'où un problème de "compatibilité". Ajoutons à cela que les langages de programmation peuvent évoluer, devenir de moins en moins populaires/utilisés, ce qui rend la création d'architectures généralistes plus pertinente. Enfin, les architectures dédiées sont évidemment des processeurs CISC, pour implémenter les nombreuses fonctionnalités des langages évolués. Et les défauts des CISC sont assez rédhibitoires à l'heure actuelle. À l'heure actuelle, les algorithmes des compilateurs se sont améliorés et savent nettement mieux utiliser le matériel. Ils produisent du code machine efficace, ce qui rend les architecture dédiées bien moins intéressantes. Si on ajoute les défauts de ces architectures dédiées, par étonnant que les architectures dédiées aient presque disparues. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'accélération matérielle de la virtualisation | prevText=L'accélération matérielle de la virtualisation | next=Le matériel réseau | nextText=Le matériel réseau }} </noinclude> cutt51nw6lnsuiqjf5eiasq0u8rjbws 764038 764037 2026-04-19T19:37:40Z Mewtow 31375 /* Le burroughs B1700 : un split-level control store */ 764038 wikitext text/x-wiki De nos jours, la majorité des programmeurs programment dans des langages de haut niveau. Il est très rare pour eux d'avoir à utiliser de l'assembleur, et encore moins un langage machine. Les programmes écrits dans un langage de haut niveau sont traduits en langage machine par un logiciel appelé le compilateur. [[File:Compilation.PNG|centre|vignette|upright=2.5|Compilateur : principe.]] Il se trouve que quelques rares processeurs sont conçus pour faciliter le travail du compilateur. Et ce sont ces processeurs que nous allons voir dans ce qui suit. Ils représentent une catégorie de jeux d'instruction à part, qui n'a pas vraiment de nom. ==La compilation et l’interprétation== Un compilateur traduit donc du code source, écrit dans un langage de haut niveau, vers du langage machine. La traduction est rarement directe. En général, le compilateur traduit le code source en assembleur, qui est lui-même traduit en langage machine. L'assembleur est une représentation textuelle du langage machine alors que le code machine est du binaire exécutable par le processeur. La traduction se fait donc en deux étapes, la '''compilation''' proprement dite et l''''assemblage''', réalisées respectivement par un compilateur et un assembleur. Il faut parfois rajouter une troisième phase d'édition des liens, que nous passons volontairement sous silence. {| |[[File:Code C.png|vignette|200px|Le code source, ici en C.]] |[[File:Assem.png|vignette|250px|Le code assembleur.]] |[[File:Binary file - hello world (C programming).png|vignette|150px|Le code machine.]] |} ===L'interprétation et la compilation à la volée=== Une alternative à la compilation est l''''interprétation''', qui transforme le code source en code machine à la volée. Avec l'interprétation, le code source est passé à un logiciel appelé l''''interpréteur''', qui exécute le code source ligne de code par ligne de code. Une ligne de code est traduite en code machine, qui est exécutée, puis l'interpréteur passe à la ligne suivante. [[File:Kaantotulkkaus.png|centre|vignette|upright=3|Compilation versus interprétation.]] Un défaut de l'interprétation est son cout en performance, qui est assez important. Aussi, l'interprétation stricte a évolué vers un hybride entre interprétation et compilation. L'idée est qu'une partie peu importante du code source est interprétée, alors que le code important est compilé juste avant d'être exécuté. Pour le dire autrement, le code source est partiellement compilé à la volée, juste avant son exécution. Aussi, on parle de '''compilation à la volée'''. Le terme en anglais est ''Just In Time compilation'', abrévié en JIT. En soi, le JIT est associé à de l'interprétation, les JIT sont en réalité des hybrides interpréteurs-compilateurs. Un point important est que compiler du code à la volée est assez lourd, cela a un cout en performance. Par contre, le code compilé s'exécute plus vite que du code interprété. Si on exécute le code compilé une seule fois, le cout de la compilation l'emporte sur le gain à l'exécution. Mais le code compilé est réutilisé autant de fois que nécessaire. Le gain à l'exécution est donc multiplié par le nombre d'exécutions, alors que le cout de la compilation est répartit sur plusieurs exécutions, il est amorti. Les portions du code sont choisies de manière à avoir un gain en performance maximal. L'idée générale est que le code exécuté très souvent est compilé, alors que du code exécuté pas souvent est interprété. Typiquement, les boucles les plus souvent exécutées sont compilées, le code exécuté une seule fois est interprété. ===Les langages intermédiaires=== Les compilateurs modernes passent par un '''langage intermédiaire''' pour faire la transformation en code machine. Le compilateur traduit le langage de haut niveau en langage intermédiaire, puis traduit le langage intermédiaire en code machine cible. Le langage intermédiaire est parfois appelé le '''''bytecode''''', et ce terme recouvre aussi le code écrit avec du ''bytecode''. L'interprétation aussi peut passer par l'intermédiaire d'un un ''bytecode'', ce n'est pas limité aux compilateurs proprement dit. {| |[[File:Intermediate representation scheme.png|centre|vignette|upright=1.5|Représentation intermédiaire d'un compilateur.]] |[[File:Code interpreter scheme.png|centre|vignette|upright=1.5|Fonctionnement d'un interpréteur.]] |} Faire ainsi a de nombreux avantages, le principal étant d'avoir un compilateur capable de traduire un langage de haut niveau vers plusieurs jeux d'instructions différents. Cela permet d'avoir, par exemple, un compilateur qui traduit du C soit en code machine x86, soit en code machine pour un CPU ARM, soit pour un CPU POWERPC, etc. [[File:Compiler design.svg|centre|vignette|upright=3|Compiler design]] Pour l'interprétation, l'usage d'un ''bytecode'' a d'autres avantages. Pour l'exploiter, il faut distribuer non pas le code source, mais le ''bytecode''. En clair, l'interpréteur ne prend pas en entrée le code source, mais du ''bytecode'' déjà compilé. La traduction est alors beaucoup plus simple, car le code source a déjà été partiellement compilé, par la traduction en ''bytecode''. Interpréter du code source est en effet assez compliqué : il faut effectuer des étapes d'analyse lexicale, sémantique, et bien d'autres. Avec du ''bytecode'', ces étapes ont été réalisées lors de la compilation du ''bytecode'', l'interpréteur a alors peut de choses à faire. L'interprétation du ''bytecode'' se fait instruction par instruction, au niveau du ''bytecode''. Une instruction du ''bytecode'' est traduite en une instruction machine équivalente, qui est exécutée. Si l'instruction est un peu complexe, il exécute une fonction/procédure qui fait la même chose. Pour résumer, le ''bytecode'' est ensuite traduit à la volée et exécuté instruction par instruction par un logiciel appelé l'interpréteur. [[File:Control table.png|centre|vignette|upright=2|Control table]] Le langage intermédiaire peut être vu comme l'assembleur d'un processeur, n'existe pas forcément dans la réalité, mais dont le jeu d'instruction est décrit en détail. Le processeur en question est appelé une '''machine abstraite''', ou encore une ''machine virtuelle''. Nous utiliserons le terme de machine abstraite dans ce qui suit. La machine abstraite n'est pas la même suivant que l'on cible la compilation ou l'interprétation/JIT. Pour l'interprétation/JIT, la machine abstraite est souvent une machine à pile, car cela simplifie grandement la traduction en code machine final. L'interprétation demande que la traduction du ''bytecode'' en code machine soit la plus simple possible. Et cela demande de prendre en compte pas mal de détails. Par exemple, les différents jeux d'instruction existants n'ont pas le même nombre de registres, ce qui pose problème lors de la traduction du ''bytecode''. Lors de la transformation en code machine, un algorithme d'allocation de registres se débrouille pour traduire le code intermédiaire en code qui utilise un nombre limité de registres. Pour améliorer cette allocation de registres, il y a deux solutions niveau ''bytecode'' : soit utiliser un nombre illimité de registres, soit utiliser une machine à pile. Pour les machines à pile, il existe un algorithme simple et rapide pour traduire un code écrit pour une machine à pile en un code écrit pour un processeur avec des registres, qui se débrouille pas trop mal pour allouer efficacement les registres. C'est un avantage assez important pour les langages interprétés. ==Les processeurs qui exécutent du bytecode== Le ''bytecode'' est un code machine, ce qui signifie qu'il peut en théorie s'exécuter sur un processeur qui implémente le jeu d’instruction associé. Si le jeu d'instruction d'un ''bytecode'' est souvent une description censée être fictive, elle n'en reste pas moins un jeu d'instruction et des caractéristiques précises, qu'on peut l'implémenter en matériel ! Néanmoins, tous les ''bytecode'' ne sont pas égaux de ce point de vue. Certains sont faciles à implémenter en matériel, d'autres non. Les ''bytecodes'' implémentés en matériel sont ceux dont les machines abstraites sont des machines à pile. Un premier exemple est celui des processeurs Pascal MicroEngine, qui exécutaient directement le bytecode du langage Pascal, le fameux UCSD P-code. Un second exemple est la '''machine SECD''', qui sert de langage intermédiaire pour certains compilateurs de langages fonctionnels. Elle a été implémentée en matériel par plusieurs équipes, notamment par les chercheurs de l'université de Calgary en 1989. Dans le même genre, quelques processeurs simples étaient capables d’exécuter directement le ''bytecode'' du langage FORTH. Le FORTH, un des premiers langages à pile de haut niveau, possède de nombreuses implémentations hardware et est un des rares langages de haut niveau dont le ''bytecode'' a été utilisé comme langage machine sur certains processeurs. Par exemple, on peut citer le processeur FC16, capable d’exécuter nativement du ''bytecode'' FORTH. ===Les processeurs Java=== Le cas le plus impressionnant est celui de la machine virtuelle Java, qui est un design de processeur comme un autre. En temps normal, le ''bytecode'' Java est compilé ou interprété, mais certains processeurs exécutaient du ''bytecode'' Java directement, sans interprétation ni compilation. Ils sont appelés des '''processeurs Java'''. On peut citer les processeurs ARM disposant de l'extension Jazelle, du JEMCore et du aJ-100 de aJile Systems, le picoJava II de Sun, et quelques autres. Il y a aussi eu quelques projets de recherche ou processeurs open source, comme Komodo, jamuth, le ''Java Optimized Processor'' et quelques autres projets du même genre. L'implémentation de la machine virtuelle Java n'était cependant pas complète. Quelques instructions complexes n'étaient pas gérées par le processeur et devaient être émulées en logiciel. Mais la grosse majorité des instructions l'était. Les instructions non-supportées étaient émulées avec la méthode ''trap and emulate'', à savoir qu'un opcode inconnu déclenchait une exception matérielle ''opcode invalide'', dont la routine pouvait émuler les instructions qui doivent l'être. Le processeur aJ-100 de aJile est capable d'exécuter la quasi-totalité des instructions de la JVM Java, seules deux instructions faisant exception. Les '''accélérateurs Java''' sont une solution intermédiaire, qui permet de traduire à la volée du ''bytecode'' Java en instructions RISC, exécutées sur un autre processeur. Il s'agit formellement de traduction binaire, mais je préfère en parler ici. L'idée est de combiner un processeur RISC avec un coprocesseur Java. Le coprocesseur lit le ''bytecode'' Java et le traduit en instructions RISC, ces dernières étant exécutées sur le processeur RISC. Le JA108 de Nozomi était un coprocesseur de ce genre, au même titre que le coprocesseur JSTAR de JEDI. L'extension Jazelle de certains CPU ARM fonctionnait vraisemblablement sur un principe similaire, sauf que le coprocesseur était intégré dans le processeur ARM, entre le cache d'instruction et le décodeur d'instructions ARM. L'intérêt des processeurs Java n'est pas qu'une question de performance. Certes, ils permettent d’exécuter plus vite les programmes compilés en ''bytecode'', comme des programmes Java pour la JVM Java. Mais l'intérêt est aussi de faire des économies de mémoire RAM et ROM. Je rappelle que la machine virtuelle Java est une machine à pile, ce qui fait que sa densité de code est excellente. Les programmes en ''bytecode'' Java sont donc très petits, et prennent peu de place en mémoire ROM. De plus, utiliser un processeur Java permet de se passer d'interpréteur ou de JIT Java, ce qui économise encore plus de mémoire. ===Transformer un processeur RISC en machine à pile=== La plupart des processeurs Java utilisent en interne une architecture à registre généraux, qui émule une machine à registre en microcode. Quelques registres sont réservés pour l'état de la machine virtuelle, avec au minimum un registre réservé pour le pointeur de pile, un autre pour un pointeur vers un ''pool'' de constantes, et quelques autres. D'autres registres servent à mémoriser le haut de la pile, à savoir le sommet de la pile et quelques opérandes situées juste en-dessous. Par exemple, le processeur picoJava II gardait les 64 opérandes au sommet de la pile dans 64 registres généraux, séparés des registres pour les pointeurs et l'état de la JVM. Une optimisation possible, envisagée sur les processeurs picoJava, était une sorte de macro-fusion sous stéroïde. L'idée était de fusionner certaines séries d'instructions Java en une seule instruction machine. Typiquement, pour faire une opération arithmétique, une machine à pile empile deux opérandes et exécute une opération, ce qui prend trois instructions machine. La spécification picoJava 1 a proposé d'envoyer directement la seconde opérande en entrée de l'ALU, ce qui fusionnait l'empilement de la seconde opérande et l'opération arithmétique. Le processeur picoJava II va plus loin et fusionne les trois instructions en une seule instruction RISC, agissant sur des registres. Mais la majorité des processeurs Java ne faisaient pas cela, car cela complexifie grandement le décodeur. Les processeurs ''bigfoot'' de DCT utilisaient un système différent pour émuler une pile avec des registres. Leur banc de registre de 64 bits était coupé en deux : les 16 premiers registres étaient utilisés pour la pile, les 48 suivants étaient utilisés pour autre chose. L'idée est que le registre 0 mémorise un emplacement vide, le registre 1 contient le sommet de la pile, le registre 2 l'opérande sous le sommet de la pile, et ainsi de suite. Les 15 premières opérandes de la pile sont donc mémorisées dans les registres. Les instructions écrivent leur résultat dans le registre adéquat, puis tous les registres sont décalés pour que le résultat passe dans le registre R1. Par exemple, les instructions PUSH chargent une opérande dans le registre 0, puis tous les registres sont décalés de un rang, pour que l'opérande chargée soit dans le registre 1. Le registre 0 devient alors le registre numéro 1, le registre 1 devient le 2, et ainsi de suite. Par contre, les opérations arithmétiques font autrement. Une instruction d'addition, par exemple, lit les opérandes dans le registre 1 et 2, puis place son résultat dans le registre 2 (les deux opérandes sont dépilées). Puis, les registres sont décalés d'un rang pour que ce registre 2 devienne le 1. Les registres ne sont en réalité pas décalés, il n'y a pas de transferts entre registres. A la place, les 16 premiers registres utilisaient une forme limitée de renommage de registres, similaire au banc de registre tournant utilisé sur les processeurs EPIC comme l'Itanium. Les numéros de registres sont renommés, décalés, à chaque fois qu'on empile ou dépile une opérande. Pour faire le renommage de registres, processeur contient un ''register stack counter'' de 4 bits, qui est incrémenté à chaque fois qu'on empile une donnée, et est décrémenté quand on dépile une donnée. Quand on veut accéder à un opérande, le numéro de registre architectural associé est traduit en numéro de registre physique, en additionnant le ''register stack counter'' (modulo 16, pour rester sur 4 bits). ==L'accélération de l'interprétation/JIT : le ''Thumb-EE'' d'ARM== Quelques processeurs ont ajouté des instructions pour faciliter le travail des interpréteurs/JIT. Par exemple, en 2005, ARM a ajouté le mode ''Thumb Execution Environment'', qui faisait cela. Il reprenait le jeu d'instruction compact ''thumb'' et ajoutait quelques instructions et modifiait le comportement d'instructions existantes. Le jeu d'instruction ''Thumb-EE'' a été ajouté sur les CPU ARM en 2005, mais a été déprécié en 2011, par manque d'utilité. Mais la tentative mérite qu'on s'attarde dessus. Les CPU de gamme M, déstinés à l'embarqué, n'ont jamais supporté ''Thumb-EE'', vu que de tels processeurs ne sont pas conçus pour exécuter du code interprété/JIT. Le mode ''Thumb-EE'' est un mode d'exécution, séparé de l'ARM normal. Ainsi, une instruction ''Thumb-EE'' et une instruction ARM peuvent avoir le même encodage en binaire, mais se comporter différemment. Le processeur est à tout moment soit en mode ''Thumb-EE'', soit en mode ARM normal, ce qui précise comment ces instructions doivent se comporter. Des instructions ont été ajoutées par ''Thumb-EE'', pour qu'il fasse son travail. L'entrée et la sortie du mode ''Thumb-EE'' se fait avec deux instructions : ENTERX et LEAVEX. Une fois en mode ''Thumb-EE'', le décodage des instructions ''thumb'' se fait avec les règles du ''Thumb-EE'', et non celles du ''thumb'' normal. Une première différence entre ''thumb'' normal et ''Thumb-EE'' est l'ajout de ce qui s'appelle un ''null check'' pour les instructions mémoire. Le ''null check'' est utilisé pour les instructions en mode d'adressage "base + indice" ou "Base + décalage", ou tout autre mode d'adressage avec un registre de base utilisés dans des calculs d'adresse. L'idée est de vérifier si l'adresse de base vaut zéro ou non. Si c'est le cas, le processeur lève une exception matérielle, qui est traitée par l'interprétation ou le compilateur JIT. Une telle situation est en effet signe d'une erreur d'adressage mémoire, qui doit être traitée par l'interpréteur. Typiquement, c'est signe qu'un pointeur n'a pas bien été initialisé, ce qui peut arriver avec du code interprété ou JIT. Les interpréteurs/JIT traitent généralement la situation en ajoutant un test avant toute instruction mémoire, pour vérifier si le pointeur accédé vaut zéro ou non. Avec ''Thumb-EE'', pas besoin d'ajouter les tests en questions : ils sont réalisés automatiquement lors de chaque instruction mémoire. : Les registres pour la pile, comme le pointeur de pile et le pointeur de ''frame'', sont aussi concernés. L'instruction CHKA vérifie que les accès à un tableau ne débordent pas en dehors du tableau en question. Par exemple, pour un tableau de 1024 éléments, elle vérifie si l'indice est compris entre 0 et 1024. Pour cela, elle vérifie si l'indice de tableau est bien dans l'intervalle adéquat et lève une exception matérielle si ce n'est pas le cas. Son utilité se comprend quand on sait que tous les accès à un tableau sont vérifiés par l'interpréteur. L'interpréteur est censé ajouter des instructions pour vérifier les indices, à savoir deux ou trois branchements. Avec CHKA, le test se fait en une seule instruction. D'autres modifications mineures de l'encodage des instructions sont aussi présentes, ainsi que des modifications pour les instructions LOAD/STORE. Précisément, dans les modes "base + indice" et "base + décalage", le décalage et l'indice subissent maintenant des décalages. L'idée est de simplifier le calcul d'adresse. De quoi économiser quelques instructions lors des calculs d'adresse, ce qui facilite le travail de l'interpréteur/JIT. Et des instructions LOAD/STORE précises voient leur comportement modifié pour faciliter la gestion de la pile de Java ou l'exécution des méthodes locales. Pour faciliter le travail de l'interpréteur, deux instructions HBP et HBLP ont été ajoutées. Elles branchent vers une fonction qui gère les exceptions logicielles (une fonctionnalité présente dans de nombreux langages, comme Java). La première effectue un branchement vers la fonction en question, la seconde sauvegarde l'adresse de retour avant de faire ce branchement. Les deux peuvent brancher vers 256 fonctions pré-déterminées, sans avoir à présenter leur adresse. ==La traduction binaire accélérée par le matériel== Plus haut, nous avons surtout parlé des compilateurs et des interpréteurs. Cependant, nous devons aussi parler de la '''traduction binaire'''. Elle traduit un programme écrit dans un code machine vers un autre code machine. Par exemple, elle traduit un programme compilé pour un CPU x86 vers un code machine ARM. En général, le système d'exploitation est généralement compilé pour le jeu d'instruction natif, mais il exécute des applications prévues pour le x86. Les applications sont traduites par le système d'exploitation, avant d'exécuter le code traduit. : Nous parlerons dans la suite de code machine source et de code machine cible, pour parler respectivement du code à traduire et du code obtenu après traduction. Idem avec d'autres termes comme architecture cible/source, ou jeu d'instruction cible/source. La traduction binaire est surtout utilisée pour des questions d'émulation ou de compatibilité. Par exemple, lorsque les Macintosh sont passés de processeurs Power PC vers des processeurs x86, le système d'exploitation Mac OS utilisait la traduction binaire pour convertir les anciennes applications Power PC vers du code x86. Les utilisateurs n'y ont vu que du feu. Le système de traduction binaire était appelé Rosetta 1. Par la suite, lors de la transition de processeurs x86 vers des processeurs Apple, Rosetta 2 a vu le jour. La traduction binaire a surtout été utilisée pour traduire du code x86 vers un autre jeu d'instruction. Il faut dire que le x86 est le jeu d'instruction dominant. De nombreuses entreprises ont eu pour ambition de briser l'hégémonie du x86 sur PC, en remplaçant le x86 par un jeu d'instruction plus performant, tout en gardant une compatibilité maximale. La traduction binaire était la seule solution pratique. Elle était le plus souvent intégralement réalisée en logiciel, comme c'était le cas sur les architectures Itanium avec le ''IA-32 Execution Layer''. ===Les généralités sur la traduction binaire assistée en matériel=== L'architecture source est presque tout le temps une architecture CISC, assez ancienne, qu'on souhaite émuler. Le choix de l'architecture cible se porte souvent sur une architecture VLIW, fort différente de l'architecture source, et ce pour obtenir une économie de matériel conséquente. Pour s'exécuter rapidement, le code traduit doit exploiter le parallélisme d'instruction, à savoir exécuter plusieurs instructions en même temps dans des unités de calcul séparées. Il est possible d'utiliser un processeur superscalaire avec exécution dans le désordre pour cela, mais au prix d'un cout important en transistors. Alors qu'en utilisant un CPU VLIW, c'est le traducteur binaire qui fait tout le travail d'extraction du parallélisme d'instruction. Et après tout, quitte à avoir un traducteur binaire, autant lui refiler le boulot d'optimisation. Compiler du code à la volée est certes assez lourd, mais qu'il existe des algorithmes efficaces pour regrouper des instructions indépendantes dans une seule instruction VLIW. Au passage, le processeur VLIW a plus de registres que le processeur source. Cela permet de faire du renommage de registres directement en logiciel. Le traducteur binaire n'hésite pas à changer les noms de registres entre instructions source et instruction VLIW cible, afin de supprimer des dépendances de données. En conséquence, cela ouvre des opportunités de parallélisme, qui sont exploitées lors du regroupement des opérations en instructions VLIW. Vous pourriez penser que le choix d'un CPU VLIW pour émuler du CISC est tout sauf optimal. Vous devez penser que la traduction binaire est d'autant plus simple que l'architecture source et cibles sont semblables. Dans les faits, ce n'est pas tellement traduire les instructions qui pose problème, mais plus la gestion du registre d'état, des exceptions matérielles, de la mémoire virtuelle, la différence entre gros-boutisme et petit-boutisme, de même que des différences d'adressage pour les périphériques. Le premier problème est la gestion des conditions, notamment en présence d'un registre d'état. En pratique, la traduction binaire s'utilise pour traduire du code CISC vers du code RISC ou VLIW. Les architectures source ont donc un registre d'état, qui est mis à jour non seulement par des instructions de test, mais aussi des instructions arithmétiques. L'architecture cible n'a elle pas de registre d'état, mais des registres à prédicats. La traduction de l'un vers l'autre est alors quelque peu compliquée. Et elle est d'autant plus compliquée que le registre d'état est la source de dépendances d'instruction implicites, qui réduisent les performances. Par exemple, si une instruction arithmétique modifie le registre d'état, cela peut impacter l'exécution d'une instruction ultérieure, qui lit ce registre d'état. Pour éliminer ces fausses dépendances, le traducteur binaire doit renommer ce registre en logiciel et détecter les dépendances utiles. Et le compilateur doit gérer les cas où il y a beaucoup de distance entre les deux, voire les cas où l'instruction dépendante n'a pas encore été analysée par le compilateur. Un autre problème est lié à la gestion des exceptions matérielles, et précisément des exceptions précises. Pour rappel, une instruction VLIW regroupe plusieurs opérations, opérations qui correspondraient à une instruction machine sur un CPU pas VLIW. En conséquence, plusieurs instructions du langage machine "source" sont regroupées en une seule instruction VLIW. Et il faut tenir compte du cas où une instruction source lève une exception. Dans le code VLIW, cela signifie qu'une opération lève une exception, et il faut annuler partiellement l'instruction VLIW associée. Et par partiellement, on veut dire que seules les opérations suivantes dans l'ordre du programme source doivent être annulées, puis ré-exécutées. Et c'est un sacré casse-tête ! Les processeurs VLIW utilisent comme solution le mécanisme d'exceptions différées des processeurs EPIC, vu il y a quelques chapitres. Pour résumer, le code est exécuté par blocs d'instruction, délimités par des branchements ou tout autre limite/barrière pertinente dans le code. Un bloc de code est compilé en deux versions : une version rapide sans exceptions matérielles, une version lente qui gère les exceptions précises. La version rapide s'exécute sans exécuter les exceptions. Cependant, les exceptions matérielles sont enregistrées, pour être prises en compte à la toute fin du bloc de code. La version rapide est exécutée en premier et elle mémorise si une exception matérielle a eu lieu. Puis une instruction vérifie si une exception a eu lieu et décide quoi faire. Si aucune exception n'a eu lieu, elle passe à la suite du programme, les résultats du bloc de code sont définitivement acceptés. Mais si une exception a eu lieu, tout est annulé. Le processeur est remis dans l'état initial, puis le code est ré-exécuté instruction par instruction de manière à gérer l'exception correctement. ===La traduction par pré-décodage=== Une première solution serait de faire la traduction binaire dans le cache d'instruction. Lors d'un défaut de cache, le code chargé depuis la RAM est traduit en code machine cible. Et c'est ce code machine cible qui est mémorisé dans le cache d'instruction et exécuté par le processeur. La technique marche sur le papier et n'est qu'une amélioration des techniques de pré-décodage vues il y a quelques chapitres. Cependant, le code obtenu est une traduction assez basique, qui n'incorpore pas d'optimisations dignes de ce nom. De plus, elle gère mal le cas où la taille du code source et cible sont potentiellement très différentes. Concrètement, elle est surtout utile pour traduire du code d'un processeur RISC vers un autre processeur RISC, dont les tailles d'instructions sont similaires. Elle ne permet pas d'émuler plusieurs jeux d'instructions différents, le cout en matériel (un circuit de pré-décodage par jeu d'instruction) serait trop important. ===Le projet DAISY d'IBM=== De très rares processeurs étaient conçus pour accélérer cette traduction binaire, afin de garder de bonnes performances. Les premiers à avoir étudié l'idée étaient IBM, avec leur projet DAISY (''Dynamically Architected Instruction Set from Yorktown''). Le projet de base était de convertir à la volée du code compilé pour des CPU Power PC, vers du code VLIW. Les chercheurs d'IBM avaient développé un algorithme de traduction dynamique efficace, ainsi qu'un processeur VLIW disposant d'optimisations spécifiques à la traduction binaire dynamique, à la volée. De nombreuses idées de ce projet ont été reprises ou re-découvertes par la société Transmetta, avec ses processeurs Crusoe et Efficieon, puis par NVIDIA avec son projet Denver. Les ingénieurs de ce projet ont étudié la possibilité d'émuler plusieurs jeux d'instructions différents sur un même processeur, notamment le s390 d'IBM et le x86. En théorie, cela demande juste d'avoir plusieurs programmes de traduction binaire : un pour le x86, un autre pour le s390, éventuellement un autre pour le Power PC. Mais cela ne s'est pas concrétisé. Mais les ingénieurs ont étudié quelle pourrait être l'architecture VLIW idéale pour ça. Par exemple, il fallait des additions 3-opérandes pour simplifier les calculs d'adresse. Une difficulté était la gestion du registre d'état, dont les bits ne sont pas mis à jour de la même manière sur le x86 et le s390 ou le Power PC. La différence entre gros-boutisme et petit-boutisme était aussi un problème, de même que des différences d'adressage pour les périphériques. Pour que la traduction binaire soit efficace, le processeur VLIW intègre diverses optimisations, comme les exceptions différées et les branchements multi-voies. La principale est la suivante : seul le code des boucles ou fonctions exécutées fréquemment est traduit en code VLIW, le reste du code est interprété. En effet, traduire du code binaire prend plus de temps qu'une simple interprétation. Pour du code qui ne sera exécutée qu'une seule fois, il est plus rapide d'utiliser l'interprétation. Par contre, pour du code exécuté beaucoup de fois, le cout de la traduction binaire est amorti, dilué sur N exécutions, compensé par le gain en temps d'exécution de ce code traduit. Au final, cela permet de ne traduire que le code qui le mérite. La détection du code fréquemment exécuté est réalisée dans le cœur VLIW. L'unité de branchement mémorise les derniers branchements rencontrés et le nombre de fois qu'ils ont été exécutés. Si ils ont été exécutés un certain nombre de fois, l'unité de branchement lève une exception matérielle, qui invoque le traducteur binaire. La routine de cette exception est le traducteur binaire proprement dit. En clair, la traduction binaire est démarrée quand le processeur détecte qu'une fonction a été exécutée plus de N fois, via une exception matérielle dédiée. La mesure du nombre d'exécution d'un branchement se fait dans l'unité de calcul dédiée aux branchements, avec l'aide d'une sorte de ''branch adress buffer'' modifié. Pour rappel, le ''branch adress buffer'' mémorise le ''Program counter'' de chaque branchement récemment rencontré. Ici, chaque entrée du ''branch adress buffer'' est associée à un compteur incrémenté à chaque exécution du branchement. ===Les processeurs Crusoe et Efficieon de Transmetta=== Les processeurs Crusoe Et Efficeron sont deux processeurs VLIW produits par la société Transmetta, une société californienne rachetée par NVIDIA. Ils étaient conçus pour exécuter spécifiquement des programmes x86, système d'exploitation inclus. L'idée derrière ce projet était d'exécuter du code x86 sans que les applications, ni même le système d'exploitation et le BIOS soient au courant ! Les processeurs Transmetta étaient en réalité conçus pour exécuter un programme unique, le ''Code Morphing Software'' (CMS), qui traduisait le code x86 en code VLIW. Transmetta ne voulait pas que son processeur VLIW soit exploité directement, sans le CMS. Il n'avait pas rendu public de compilateur pour traduire du C vers du code VLIW, il n'avait pas donné la documentation du jeu d'instruction VLIW. Des efforts de rétro-ingénieurie, documentés sur le site realworldtech, ont cependant permis de comprendre comment étaient encodées les instructions du processeur Crusoe. Comme pour le projet Daisy, le ''Code Morphing Software'' utilisait à la fois interprétation et traduction binaire, selon les besoins. Les instructions sont interprétées lors de leurs premières exécutions, mais le CMS bascule sur de la traduction en code VLIW après un certain nombre d'exécution. Ainsi, les boucles souvent exécutées sont traduites en code VLIW, alors que le reste du code est interprété. Le choix du code à traduire est le fait du CMS, il utilise des heuristiques complexes pour, qui ne sont pas connues dans le détail. Le ''Code Morphing Software'' est mémorisé dans une EEPROM, ce qui en fait un ''firmware'' situé en dessous du BIOS. Le CMS démarre ensuite le BIOS, qui lui-même démarre le système d'exploitation, qui lui-même démarre les pilotes de périphériques et les programmes. Le CMS se réserve les 16 premiers mébioctets de l'espace d'adressage. L'EEPROM du CMS est mappée dedans, mais n'en utilise que la moitié. Le reste est utilisé comme cache, pour mémoriser le code VLIW traduit par le CMS. Les deux processeurs intégraient aussi deux mémoires SRAM, utilisées par le CMS, appelées la ''local program memory'' (LPM) et la ''local data memory'' (LDM). La première contient du code qui gére les interruptions, la mémoire virtuelle, les exceptions matérielles, les problèmes d'alignement mémoire, et quelques fonctions très fréquemment utilisées par le CMS. La seconde est de la mémoire RAM utilisée par ces fonctions de la ''local program memory''. Les mémoires caches L1 et L2 sont séparées de ces deux mémoires. Le processeur Crusoe et Efficieon étaient des processeurs VLIW très simples. Ils n'avaient même pas de MMU, ni de fonctionnalités importantes sur les CPU x86. Pour Crusoe, les instructions VLIW étaient encodées sur 64 ou 128 bits, et regroupaient entre 2 à 4 opérations. Les instructions LOAD/STORE ne supportaient pas d'adressage indicé, juste de l'adressage indirect à registre. Crusoe avait 5 unités de calcul : deux ALU entières, une unité LOAD/STORE, une unité de branchement, une FPU. Efficieon doublait le nombre d'ALU entière et d'unité LOAD/STORE, les instructions VLIW passaient à 256 bits. Le processeur Crusoe contient 160 registres, dont 64 registres généraux, 32 registres flottants. Les registres généraux font 32 bits, ce qui est cohérent avec le fait que les CPU x86 de l'époque étaient des processeurs 32 bits. Quant aux registres flottants, ils faisaient 80 bits, ce qui colle avec la taille des registres flottants de la FPU x87 utilisée à l'époque. Sur les 64 registres généraux, seuls 48 étaient réellement utilisable pour mémoriser des opérandes. Une partie des registres généraux étaient utilisés pour la gestion de la pile, d'autres pour mémoriser l'état du CPU x86 émulé, un registre était un registre zéro non accesible en écriture. La gestion des exceptions est optimisée avec un système d'exceptions différées, le même que celui décrit plus haut. Le processeur utilise un système similaire, pour effectuer des lectures anticipées, qui a été expliqué dans le chapitre sur les processeurs VLIW/EPIC. Sauf qu'il s'agit là d'une optimisation pour gagner en performances en général, pas une aide utile pour gérer des exceptions précises ou toute autre subtilité de la traduction binaire. Pour gérer les exceptions différées et les lectures anticipées, le processeur mémorise l'état du processeur avant de démarrer un bloc de code. Pour cela, le processeur contient des ''shadow registers'', 48 registres entiers et 16 registres flottants. Vous remarquerez qu'il y a autant de registres entiers que de ''shadow registers'' entiers. Les registres entiers/flottants sont mémorisés dans les ''shadow registers'' avant d'exécuter un bloc de code, afin que le processeur puisse revenir à l'état de base. Une autre fonctionnalité liée est le ''gated store buffer''. Le principe est d'éviter toute écriture en mémoire RAM, tant que le code VLIW n'a pas émis d'instruction ''commit''. L'idée est que les écritures sont conservées dans la ''store queue'' de l'unité LOAD/STORE. Pour rappel, la ''store queue'' met en attente les écritures soit tant que les écritures ne sont pas terminées, soit tant que la RAM est occupée. L'idée est que les écritures sont accumulées dans la ''store queue'' tant que l'instruction ''commit'' n'a pas eu lieu. Le ''store queue'' est "déversé dans la RAM" seulement quand l'instruction ''commit'' s'exécute. ===Les CPU NVIDIA Denver=== Nvidia a tenté quelque chose de similaire avec son projet Denver. NVIDIA a racheté l'entreprise Transmetta, ses brevets, et a tenté de refaire la même chose. Le projet initial était de traduire du code x86 vers un jeu d'instruction VLIW propriétaire, comme l'a fait Transmetta. Mais NVIDIA n'a pas réussi à acquérir la licence du jeu d'instruction x86 et s'est rabattu sur le jeu d'instruction ARM. Le processeur né de ce projet est le Tegra K1-64 CPU. Il s'agit d'un processeur composé d'un cœur VLIW, couplé à deux décodeurs d'instructions ARM. Les deux décodeurs ARM traduisent une paire d'instructions ARM consécutives en instructions VLIW, pour les exécuter sur le cœur VLIW. Le CPU peut fonctionner selon deux modes : ARM et VLIW. En somme, le processeur supporte deux jeux d'instruction : le jeu d'instruction ARM, et un jeu d'instruction VLIW. En mode ARM, les instructions ARM sont chargées depuis le cache, traduites en VLIW par les décodeurs ARM, puis exécutées par le cœur VLIW. En mode optimisé, le CPU exécute des instructions VLIW chargées depuis le cache d'instruction, les décodeurs ne sont pas utilisés. : Le CPU supporte en réalité deux jeux d'instruction ARM : ARM8 et 7. Mais ils correspondent au même mode pour le CPU. Un point important est que le processeur peut changer très rapidement de mode, en à peine quelques cycles d'horloges. On n'est clairement pas dans le cas des CPU x86, qui mettent des plombes pour passer du mode 32 à 64 bits et inversement. La commutation est tellement rapide qu'on peut considérer que le processeur supporte deux jeux d'instruction simultanément. Et le support simultané de deux jeux d'instruction facilite l'implémentation de la traduction binaire. Encore une fois, inutile de traduire du code qui ne sera exécutée qu'une seule fois, mieux vaut l'exécuter directement dans le mode ARM. Surtout que le processeur supporte un mode ARM qui permet d'exécuter du code ARM sans perte de performance. Par contre, le code fréquemment utilisé, notamment dans des boucles critiques, est traduit en VLIW. Au final, les performances sont optimisées, en limitant le travail du logiciel traducteur binaire. La détection du code fréquemment exécuté se fait différemment que pour les processeurs Transmetta et Daisy. L'unité de branchement ne détecte pas directement le code beaucoup exécuté, même si elle a un rôle à jouer. A la place, la détection est réalisée en logiciel, par un ''thread'' dédié, qui s'exécute sur un cœur séparé. Il détecte le code fréquemment utilisé en regardent les branchements exécutés récemment. Pour cela, le processeur mémorise l'historique des branchements pris récemment et copie cette historique dans une mémoire tampon dédiée, partagée entre tous les cœurs. Vu qu'elle est partagée, le ''thread'' de détection a accès à l'historique sans pertes de performances, seule la copie de l'historique dans ce tampon a un cout en performance, pas son partage. Le code VLIW obtenu après traduction binaire est mémorisé dans la mémoire RAM, dans une portion spécialement réservée pour. Elle fait 128 mébioctets, et est appelée le '''cache d'optimisation''' par NVIDIA. Le cache d'optimisation est protégé en écriture et seul le traducteur binaire peut écrire dedans. Au passage, le traducteur binaire est du code VLIW, ce qui fait qu'il est placé dans le cache d'optimisation. Le processeur détecte automatiquement quand une fonction pour laquelle le code compilé équivalent est disponible. Pour cela, l'unité de chargement contient une table de correspondance entre l'adresse de la fonction ARM, et l'adresse de son équivalent compilé. Quand une fonction est appelée, l'unité de chargement regarde l'adresse de destination du branchement. Si l'adresse est dans cette table, elle récupère l'adresse de la fonction compilée et branche vers celle-ci. Le processeur dispose d'un cache d'instruction de 128 kibioctets, ce qui est très important, mais nécessaire vu la taille des instructions VLIW. Le coeur VLIW dispose de ses propres décodeurs, d'un ''scoreboard'', de 7 unités de calcul et de registres. Pour les unités de calcul, il a deux FPU, deux ALU, une unité de branchement, et deux unités LOAD-STORE faisant aussi ALU. Un point important est que le CPU dispose de 64 registres entiers et de 64 registres flottants. C'est deux fois plus que supporte le jeu d'instruction ARM. En mode ARM, seule la moitié des registres est utilisée. Mais en mode optimisé, le traducteur binaire utilise bien les 64 registres, grâce à une sorte de renommage de registres logicielle. Pour simplifier la traduction binaire, le processeur supporte les techniques vues précédemment. Le processeur intègre un ''gated store buffer'' similaire à celui des processeurs Transmetta. Les techniques d'exceptions différées sont aussi supportées, comme sur les processeurs Transmetta. Il a aussi une unité de préchargement décrite par NVIDIA comme agressive, avec support du préchargement de type anticipé (''runahead''). ===L'émulation de la mémoire virtuelle=== Pour finir, il faut parler de la mémoire virtuelle avec la traduction binaire. Sur le processeur source, le processeur gère à la fois des adresses physiques et virtuelles et sa MMU fait la conversion entre les deux. Sur le processeur VLIW, la MMU est simulée par le programme de traduction binaire, partiellement ou totalement. L'implémentation exacte varie suivant que l'on parle du projet Daisy, des processeurs Transmetta ou de Denver. Sur les processeurs Transmetta, il n'y a pas de MMU ni de mémoire virtuelle. A la place, le programme de traduction binaire émule la mémoire virtuelle du processeur source. Le processeur VLIW ne gère que des adresses physiques, rien d'autre. L'espace d'adressage physique du processeur VLIW a la même taille que celui du processeur émulé, ici des processeurs x86. Il est vraisemblable que les adresses physiques utilisées par le processeur x86 sont les mêmes que celles du processeur VLIW. Sur le projet Daisy, le processeur VLIW gère la mémoire virtuelle via pagination, comme les processeurs émulés. Ce qui fait qu'il gère un espace d'adressage virtuel et un espace d'adressage physique. Pour éviter toute confusion, nous parlerons d'adresse physique/virtuelle VLIW pour les adresses physiques/virtuelles du processeur VLIW, d'adresse virtuelle/physique source pour celle du jeu d'instruction traduit, à savoir du Power PC ou du s390, éventuellement de l'x86. Il faut alors faire le lien entre adresses physiques source et adresses virtuelles VLIW. Pour cela, rien de plus simple : il y a correspondance parfaite. L'adresse physique source numéro N correspond à l'adresse virtuelle VLIW numéro N. Cependant, cela signifie que tout l'espace d'adressage virtuel VLIW serait utilisé par le code à traduire. En réalité, il faut ajouter de la place pour le code traduit et le programme de traduction binaire. La conséquence est que l'espace d'adressage virtuel VLIW est plus large que l'espace d'adressage source. L'espace d'adressage virtuel VLIW est découpé en trois sections : une section pour le code Power PC à traduire, une autre pour le code traduit en VLIW, et une dernière réservée au traducteur binaire. Le programme de traduction est placé dans une ROM mappée en mémoire dans la seconde section. Le reste de la seconde section est réservé à une mémoire RAM utilisée par le programme de traduction. [[File:Mémoire virtuelle sur le projet Daisy.png|centre|vignette|upright=1.5|Mémoire virtuelle sur le projet Daisy]] Il faut noter que chaque page physique source, une fois traduite, correspond à N pages virtuelles VLIW. La raison est que le code traduit prend plus de place que le code machine originel. En conséquence, on doit utiliser une page finale plus grande. Pour le reste, des pages contiguës en mémoire physique source sont elles aussi contiguës en mémoire virtuelle VLIW. Le calcul d'adresse est donc simple : l'adresse physique source est multipliée par N, puis on ajoute l'adresse de base à laquelle commence la section pour le code traduit. ==Les processeurs à jeu d'instruction reconfigurable== Nous venons de voir comment accélérer en matériel la traduction binaire. Mais il existe une alternative qui permet de se passer de traduction binaire, complétement. L'idée est d'avoir un processeur avec un '''jeu d'instruction reconfigurable'''. Le nom est assez parlent : on peut configurer le processeur de manière à supporter le jeu d'instruction de son choix ! Par exemple, on pouvait reconfigurer un CPU de manière à ce qu'il supporte le jeu d'instruction x86, ou qu'il exécute du ''bytecode'' Java, ou du code ARMv8, etc. De tels processeurs sont encore plus rare que ceux vu avant dans ce chapitre, mais ils ont existé ! Les processeurs de ce type avaient un '''microcode réinscriptible''', à savoir que c'était des processeurs microcodés pour lesquels on pouvait modifier le microcode. L'idée était qu'en changeant le microcode, on changeait le jeu d'instruction du processeur. Pour cela, le microcode était associé à une mémoire SRAM, en plus de la mémoire ROM du ''control store''. La SRAM était appelée la micro-SRAM, alors que le ''control store'' basique était appelé la micro-ROM. Le microcode était copié dans cette micro-SRAM au démarrage du CPU, puis les instructions étaient décodées en utilisant le microcode dans la micro-SRAM. En modifiant le contenu de la micro-SRAM, on pouvait altérer le microcode, voire le remplacer totalement. Il s'agit là de l'implémentation la plus simple. Mais le Burroughs B1700 a procédé autrement et il est intéressant d'étudier en détail son architecture. Son architecture est décrite dans le livre "Interpreting Machines : Architecture and Programming of the Bl700/Bl800 Series", écrit par Elliott I. Organick et James A. Hinds. Une partie de ce qui vfa suivre est un très court résumé de ce livre. ===Le Burroughs B1700 : architecture externe=== Il disposait de 4 registres généraux, nommés X, Y, T et L, tous de 24 bits. Les registres T et L étaient découpés en 6 sous-registres de 4 bits, nommés LA LB LC LD LE et LF pour le registre L, et TA TB TC TD TE et TF pour le registre T. Ils étaient adressables comme n'importe quel registre. Un point important est que le registre T avait des possibilités d’extraction spécifiques. Il était possible d'extrait N bits, placés n'importe où dans ce registre, et de les copier dans un autre registre. Nous verrons pourquoi cette possibilité est intéressante et ne sert que pour le registre T. Les registres X et Y étaient reliés à une ALU de 24 bits capable d'effectuer des opérations arithmétiques basiques. Fait étonnant, l'ALU faisait tous les calculs en même temps et fournissait ses résultats dans 7 registres : un registre SUM pour l'addition, un registre DIFFERENCE pour la soustraction, et un registre par opération bit à bit. Les registres pour les opérations bit à bit sont : CMPX et CMPY pour l'opération NOT sur chaque opérande, XANY pour le ET bit à bit, XEOY pour le XOR, XORY pour le OU logique. Les additions et soustractions pouvaient se faire sur des opérandes codés en binaire ou en BCD, suivant le code opération utilisé. A cela, il fallait ajouter deux registres de "résultat" MSKX et MSKY. Ils fournissaient un masque dépendant de la taille du résultat en bits. Cela permettait de gérer des données de 0, 4, 8, 12, 16, 20, et 24 bits, alors que l'ALU faisait 24 bits. Et cela n'était pas limité à 16 ou 8 bits, mais n'importe quelle taille inférieure ou égale à 24 bits était supportée, même des tailles non-conventionnelles comme des résultats codés sur 3 bits, 9 bits, 12 bits, etc. L'ALU peenait en opérande les registres X et Y, mais aussi un registre CYF pour la retenue entrante. En sortie, il fournissait aussi deux retenue sortantes : une pour l'addition et une autre pour la soustraction. Elles étaient mémorisées dans les registres CYL et CYD, respectivement. L'ALU fournissait aussi 12 conditions, qui alimentait un registre d'état. Le registre d'état était segmenté en trois registres appelés XYST, XYCN et BICN. Le processeur contenait de nombreux autres registres de 4 bits, dont les sous-registres de T et L. En tout, il y a 27 registres de 4 bits adressables ! Et le processeur disposait d'une ALU 4 bits pour manipuler ces registres de 4 bits ! Elle est capable de faire des MOV entre registres de 4 bits, des opérations bit à bit, et peut aussi tester si un bit vaut 0 ou 1. La dernière possibilité est très utile pour implémenter les branchements. Pour communiquer avec les entrées-sorties, il disposait de deux registres CMND et DATA : CMND pour envoyer une commande à une entrée-sortie, DATA pour lire ou écrire une donnée. Pour la mémoire, il avait deux registres séparés : READ et WRITE. READ contient la donnée lue lors d'une lecture,; WRITE est pour une donnée à écrire lors d’une écriture. Il s'agit de registres d’interfaçage mémoire, qui ont été promus au rang de registres architecturaux. Les deux registres sont gérés par le microcode, qui est le langage principal de la machine. Les instructions machines configurées font sans, mais le microcode doit y avoir accès facilement. Et vu que le microcode est le langage principal de ce processeur, ces deux registres deviennent architecturaux. ===Le burroughs B1700 : un ''split-level control store''=== Le burroughs B1700 lisait le microcode directement depuis la mémoire RAM ! Pour être précis, il avait plusieurs modèles : le B1726, le B1710 et le B1800. Le B1710 plaquait le microcode en mémoire RAM, ce qui fait que chaque exécution d'une instruction lisait les micro-opérations en mémoire RAM ! Le B1800 faisait pareil, mais avait ajouté un cache dédié au microcode, afin d'améliorer les performances. Le B1726 avait un microcode scindé en deux : une partie était en mémoire RAM, l'autre était dans un microcode réinscriptible classique, avec une micro-SRAM intégrée au processeur. Le processeur peut exécuter le microcode directement depuis la mémoire RAM, mais avec une pénalité en termes de performances. La documentation appelle cela un '''''split-level control store'''''. Les instructions fréquentes étaient ainsi décodées avec la micro-SRAM, alors que les instructions peu fréquentes étaient décodées en lisant le microcode associé en mémoire RAM. La pénalité en termes de performance est alors concentrée sur les instructions les plus rares, ce qui est un très bon compromis. Le programmeur devait découper le microcode en segments, certains étant placé dans la micro-SRAM, les autres étant en mémoire RAM. Un tel microcode permettait de placer le microcode le plus fréquemment utilisé dans le processeur, alors que le reste du microcode était placé en mémoire RAM. Mieux que ça, le processeur autorisait d'utiliser la technique de l'''overlaying'' pour le microcode ! Il y avait même une micro-opération OVERLAY dédiée pour ! Elle permettait de copier du microcode de la mémoire RAM vers la micro-SRAM ! OVERLAY avait besoin de trois opérandes : la taille du segment à copier, son adresse source en mémoire RAM, son adresse de destination dans la micro-SRAM. La micro-SRAM mémorisait au maximum 2048 micro-opérations, ce qui fait que les 2048 premières "adresses" correspondaient à la micro-SRAM. Les suivantes étaient des adresses de mémoire RAM qui contenaient le reste du microcode. En pratique, l'adresse qui séparait les deux portions du microcode était configurable ! On pouvait limiter la taille du microcode en micro-SRAM à un multiple de 32 micro-opérations ! Les adresses restantes étaient alors réattribuées à la RAM. Par exemple, on pouvait attribuer 64 adresses à la micro-SRAM, les 2048 - 64 adresses restantes étaient alors attribuées à la mémoire RAM. Un registre limite, nommé TDPM, indiquait quelle était l'adresse de démarcation entre les deux. Il contenait 4 bits, dont la valeur était multipliée par 32, pour obtenir l'adresse limite. Le microcode pouvait servir pour émuler un autre jeu d'instruction, mais il était aussi possible d'implémenter un véritable interpréteur ! L'interpréteur exécutait alors un code source écrit dans un langage de haut niveau, comme le FORTRAN ou le COBOL, qui était convertit à la volée en micro-opérations, sans passer par un langage machine intermédiaire ! Le processeur était même conçu pour ! L'interpréteur était placé dans un "segment" en mémoire RAM, dont le processeur connaissait la position. Il incorporait pour cela deux registres, un pour l'adresse de base de l'interpréteur, un autre pour son adresse de fin. Il incorporait une protection mémoire limitée sur ce segment, à savoir que seul l'interpréteur pouvait lire ou écrire dans ce segment. ===Le microcode réinscriptible=== Le microcode utilise des micro-opérations de 16 bits de long, ce qui est très peu et bien en-dessous des micro-opérations des autres processeurs. Les micro-opérations sortant du microcode sont envoyées au registre M, un registre de micro-opération de 16 bits. les micro-opérations écrites dans ce registre sont ensuite décodées par le hardware et exécutées. Fait intéressant, il était possible de faire un OU logique entre la micro-opération sortante et un registre du processeur, le résultat étant mémorisé dans le registre M. Cela permettait de faire du code automodifiant dans le microcode ! Utiliser du code automodifiant avec un microcode réinscriptible est parfaitement possible, et c'est utile pour émuler des modes d'adressage complexe dans le microcode. Cela permettait par exemple de remplacer une adresse dans une micro-opération. L'adresse est alors surimposée avec un OU dans le champ d'adresse, alors que le reste de l'instruction n'est pas modifié. Mais modifier directement le microcode est rarement une bonne idée. Aussi, pour éviter cela, l'idée est de modifier les instructions avec un OU logique avec un autre registre. Les micro-opérations du microcode ne sont donc pas modifiées dans le microcode, mais en sortie du microcode. Afin de gérer les interruptions, ces dernières étaient partiellement détectées en logiciel. Les interruptions ne s'exécutaient pas automatiquement, pour émuler correctement les interruptions. Une interruption doit être prise en compte à la toute fin de l'exécution d'une instruction, quand celle-ci a finit son travail. Mais vu qu'ici, les instructions sont émulées avec des séries de micro-opérations, il y a un problème : il est possible d'interrompre une instruction émulée en plein milieu. Pour éviter cela, le processeur ne déclenche pas les interruptions matérielles immédiatement, elles sont mises en attente. Une micro-opération dédiée vérifie si une interruption est en attente, et l'exécute alors cas échéant. La micro-opération est utilisée comme micro-opération finale d'une instruction émulée. ===Une architecture bit-adressable=== Si avoir un microcode réinscriptible est suffisant, quelques choix de ''design'' facilitent l'émulation de jeux d'instructions variés. Par exemple, le CPU Burroughs B1700 avait ajouté une fonctionnalité très importante : il était bit-adressable. En clair, il pouvait adresser la mémoire bit par bit, si nécessaire. L'avantage est que cela permettait d'émuler des architectures dont la taille des registres/''byte''/mots était très différente. Un autre avantage est que cela facilitait la gestion des instructions de taille variable. Détaillons rapidement le premier avantage. Le processeur était un CPU 24 bits, mais il pouvait émuler des processeurs 16 bits, 8 bits, 4 bits, 1 bit, 9 bits, ou toute autre valeur. Pour cela, le processeur lisait des mots de 24 bits et utilisant un ''barrel shifter'' pour sélectionner les bits adéquats. Par exemple, pour un accès de 16 bits, la donnée lue en mémoire était masquée de manière à ne garder que les 16 bits de poids faible. Le ''barrel shifter''/circuit de masquage était placé directement avant le bus mémoire, sur le trajet des données/instructions. Le ''barrel shifter'' était commandé par un registre ''Field Unit'', qui précisait quelle était la taille des données à charger. Le processeur gère un micro-''program counter'' appelé le registre A. Les micro-opérations du processeur font 16 bits de long, et elles sont alignées en RAM sur 16 bits, ce qui fait que les 4 bits de poids fort de ce registre valent 0. Le registre fait 18 bits, mais seuls les 14 bits de poids fort sont utiles. Les adresses mentionnées plus haut, qui adressent les micro-opérations en mémoire RAM, sont des adresses de mots de 16 bits, pas les adresses de bit individuels. ===La gestion de la taille des instructions=== Les instructions à décoder n'ont pas de longueur fixe, à savoir qu'elles peuvent faire 16 bit avec tel jeu d'instruction, 8 bits avec un autre, 32 bits sur un autre, etc. Elles peuvent aussi être de longueur fixe ou variable ! Le B1700 intégrait donc un support des instructions de longueur variable, qui était "détourné" pour gérer des jeux d'instruction différents. Notons que vu que l'architecture est bit-adressable, les contraintes d'alignement des instructions disparaissent, ce qui a des avantages qu'on va détailler dans ce qui suit ! L'implémentation des instructions de taille variable ajoutait un registre avant le registre d'instruction. Le registre d'instruction proprement dit était un registre M de 24 bits. Il était précédé par un registre T, dans lequel l'instruction était assemblée, en combinant plusieurs morceaux. Si l'instruction est codée est codée sur moins de 24 bits, le registre T se contente de masquer les bits en trop. Si l'instruction chargée dépasser 24 bits, le chargement se faisait en plusieurs passes, avec plusieurs lectures en mémoire. Dans ce qui suit, on suppose que l'instruction est composée de '''champs''' : l'opcode, les opérandes, les adresses absolues, les constantes immédiates, les numéros de registre, etc. L'idée est que l'instruction est chargée dans le registre T, puis découpée en champs, qui sont copiés dans les registres du processeur ou envoyés au microcode. Je rappelle que le registre T est relié à un ''barrel shifter'', différent au précédent, qui permettait de masquer, décaler et extraire une donnée de ce registre. Et il y a la possibilité d'extraire une suite de bit de ce registre T, pour la copier dans un autre registre. Par exemple, prenons une instruction composée d'un opcode, d'une constante immédiate et d'une adresse immédiate. L'opcode est lu et interprété par le microcode. Le microcode extrait la constante immédiate du registre T et la copie dans le registre X. Ensuite, l'adresse est extraite et envoyée au registre FA, une lecture est démarrée, et l'opérande est copiée dans le registre Y. Enfin, l'instruction est exécutée par le microcode. Le tout prend plusieurs micro-opérations : une première pour lire l'opcode, une seconde pour lire la première opérande, une troisième pour lire la seconde opérande, una quatrième pour tout exécuter. Notons que suivant la taille des champs, on peut en charger plusieurs à la fois. Par exemple, imaginons qu'on ait chargé 24 bits, contenant un opcode de 8 bits et une constante immédiate de 16 bits : pas besoin de faire deux accès mémoire, le registre T est juste découpé en un opcode et une constante. Le registre T est fait pour ce genre d'extraction. ===Le chargement d'un champ=== L'architecture est bit-adressable car elle gére des champs qui ne font pas forcément 8 ou 16 bits. Par exemple, certains jeux d'instructions ont des opcodes codés sur 6 ou 7 bits, voire 9-10 bits. Par exemple, prenons un opcode de 7 bits, suivi par une adresse de 12 bits et une constante immédiate de 15 bits : aucun de ces champs ne sera aligné sur 8/16/24 bits. Utiliser une architecture bit-adressable règle le problème d'alignement. Et en général, les contraintes d'alignement des instructions disparaissent ! Quand on charge une instruction, peut importe qu'elle soit ou non alignée sur 24 bits, elle sera placée dans les bits de poids faible, il suffira de masquer les bits de poids fort inutiles. Par exemple, si on charge une instruction de 16 bits, les 16 bits seront copiés dans les 16 bits de poids faible du registre T, il suffira de masquer l'octet de poids fort. Pas besoin de faire de décalages pour gérer des instructions mal alignées... Pour charger une instruction, il faut connaitre plusieurs informations : l'adresse de son premier bit, sa taille. Pour cela, le B1700 contient deux registres, nommés ''Field Adress'' (FA) et ''Field Length'' (FL), qui indiquent l'adresse du premier bit et la taille de l'instruction à charger. Le premier fait 24 bits, ce qui permet de gérer des adresses de 24 bits. N'oubliez pas que le processeur est bit-adressable, ce qui permet d'adresser 16 Mébi-bits, soit 2 mébioctets. Le registre pour la taille fait lui 16 bits, ce qui permet de gérer des instructions de 65536 bits, soit 8192 octets ! Leur nom commence par ''field'', car le processeur peut gérer des instructions de taille variable et/ou plus longues que 24 bits. Dans ce cas, le chargement de l'instruction se fait champ par champ. Pour donner un exemple, prenons une instruction composée de 20 champs de 12 bits chacun. Les champs sont chargés un par un dans le registre T. Le registre FA est alors initialisé avec l'adresse de l'instruction, il est incrémenté de 12 à chaque cycle. Le registre FL est initialisé avec la taille de l'instruction, soit 20 * 12 = 240 bits, et il est décrémenté de 16 à chaque cycle. Précisons cependant que la taille d'un champ peut être changée d'un cycle à l'autre. Par exemple, le processeur peut charger un opcode de 8 bits, l'interpréter, détecter que l'instruction demande ensuite de charger une adresse de 16 bits. Dans ce cas, le registre FL est immédiatement altéré pour charger 16 bits, au lieu des 8 bits de l'opcode immédiatement précédent. ==Les jeux d'instructions dédiés à un langage de programmation== De rares processeurs sont conçus pour un langage de programmation en particulier. On appelle ces processeurs, conçus pour des besoins particuliers, des '''processeurs dédiés'''. Par exemple, les fameux ''Burrough E-mode'' B5000/B6000/B7000 étaient spécialement conçus pour exécuter de l'ALGOL-60. Leurs cousins B2000/B3000/B4000 étaient eux conçus pour le COBOL. Des langages fonctionnels ont aussi eu droit à leurs processeurs dédiés. Le prolog en est un bel exemple, avec les superordinateurs de 5ème génération qui lui étaient dédié. On peut aussi citer les machines LISP, dédiés au langage LISP, qui datent des années 1970. Elles étaient capables d’exécuter certaines fonctions de base du langage directement dans leurs circuits : elles possédaient notamment un ''garbage collector'' câblé dans ses circuits ainsi que des instructions machines supportant un typage déterminé à l’exécution. Les processeurs dédiés ont eu leur heure de gloire au début de l'informatique, à une époque où les langages de haut niveau venaient d'être inventés. À cette époque, les compilateurs n'étaient pas performants et ne savaient pas bien optimiser le code machine. Il était alors rationnel, pour l'époque, de rapprocher le code machine cible et le langage de programmation de haut niveau. De nombreuses architectures dédiés ont ainsi été inventées, avant que les concepteurs se rendent compte des défauts de cette approche. Les défauts en question ne sont pas nombreux, mais assez simples à comprendre. Premièrement, elles sont très rapides pour un langage de programmation en particulier, mais sont assez mauvaises pour les autres, d'où un problème de "compatibilité". Ajoutons à cela que les langages de programmation peuvent évoluer, devenir de moins en moins populaires/utilisés, ce qui rend la création d'architectures généralistes plus pertinente. Enfin, les architectures dédiées sont évidemment des processeurs CISC, pour implémenter les nombreuses fonctionnalités des langages évolués. Et les défauts des CISC sont assez rédhibitoires à l'heure actuelle. À l'heure actuelle, les algorithmes des compilateurs se sont améliorés et savent nettement mieux utiliser le matériel. Ils produisent du code machine efficace, ce qui rend les architecture dédiées bien moins intéressantes. Si on ajoute les défauts de ces architectures dédiées, par étonnant que les architectures dédiées aient presque disparues. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'accélération matérielle de la virtualisation | prevText=L'accélération matérielle de la virtualisation | next=Le matériel réseau | nextText=Le matériel réseau }} </noinclude> 8wqkfm2g12kto7rdfz1k0ohu48qabq9 764040 764038 2026-04-19T19:58:41Z Mewtow 31375 /* Les processeurs à jeu d'instruction reconfigurable */ 764040 wikitext text/x-wiki De nos jours, la majorité des programmeurs programment dans des langages de haut niveau. Il est très rare pour eux d'avoir à utiliser de l'assembleur, et encore moins un langage machine. Les programmes écrits dans un langage de haut niveau sont traduits en langage machine par un logiciel appelé le compilateur. [[File:Compilation.PNG|centre|vignette|upright=2.5|Compilateur : principe.]] Il se trouve que quelques rares processeurs sont conçus pour faciliter le travail du compilateur. Et ce sont ces processeurs que nous allons voir dans ce qui suit. Ils représentent une catégorie de jeux d'instruction à part, qui n'a pas vraiment de nom. ==La compilation et l’interprétation== Un compilateur traduit donc du code source, écrit dans un langage de haut niveau, vers du langage machine. La traduction est rarement directe. En général, le compilateur traduit le code source en assembleur, qui est lui-même traduit en langage machine. L'assembleur est une représentation textuelle du langage machine alors que le code machine est du binaire exécutable par le processeur. La traduction se fait donc en deux étapes, la '''compilation''' proprement dite et l''''assemblage''', réalisées respectivement par un compilateur et un assembleur. Il faut parfois rajouter une troisième phase d'édition des liens, que nous passons volontairement sous silence. {| |[[File:Code C.png|vignette|200px|Le code source, ici en C.]] |[[File:Assem.png|vignette|250px|Le code assembleur.]] |[[File:Binary file - hello world (C programming).png|vignette|150px|Le code machine.]] |} ===L'interprétation et la compilation à la volée=== Une alternative à la compilation est l''''interprétation''', qui transforme le code source en code machine à la volée. Avec l'interprétation, le code source est passé à un logiciel appelé l''''interpréteur''', qui exécute le code source ligne de code par ligne de code. Une ligne de code est traduite en code machine, qui est exécutée, puis l'interpréteur passe à la ligne suivante. [[File:Kaantotulkkaus.png|centre|vignette|upright=3|Compilation versus interprétation.]] Un défaut de l'interprétation est son cout en performance, qui est assez important. Aussi, l'interprétation stricte a évolué vers un hybride entre interprétation et compilation. L'idée est qu'une partie peu importante du code source est interprétée, alors que le code important est compilé juste avant d'être exécuté. Pour le dire autrement, le code source est partiellement compilé à la volée, juste avant son exécution. Aussi, on parle de '''compilation à la volée'''. Le terme en anglais est ''Just In Time compilation'', abrévié en JIT. En soi, le JIT est associé à de l'interprétation, les JIT sont en réalité des hybrides interpréteurs-compilateurs. Un point important est que compiler du code à la volée est assez lourd, cela a un cout en performance. Par contre, le code compilé s'exécute plus vite que du code interprété. Si on exécute le code compilé une seule fois, le cout de la compilation l'emporte sur le gain à l'exécution. Mais le code compilé est réutilisé autant de fois que nécessaire. Le gain à l'exécution est donc multiplié par le nombre d'exécutions, alors que le cout de la compilation est répartit sur plusieurs exécutions, il est amorti. Les portions du code sont choisies de manière à avoir un gain en performance maximal. L'idée générale est que le code exécuté très souvent est compilé, alors que du code exécuté pas souvent est interprété. Typiquement, les boucles les plus souvent exécutées sont compilées, le code exécuté une seule fois est interprété. ===Les langages intermédiaires=== Les compilateurs modernes passent par un '''langage intermédiaire''' pour faire la transformation en code machine. Le compilateur traduit le langage de haut niveau en langage intermédiaire, puis traduit le langage intermédiaire en code machine cible. Le langage intermédiaire est parfois appelé le '''''bytecode''''', et ce terme recouvre aussi le code écrit avec du ''bytecode''. L'interprétation aussi peut passer par l'intermédiaire d'un un ''bytecode'', ce n'est pas limité aux compilateurs proprement dit. {| |[[File:Intermediate representation scheme.png|centre|vignette|upright=1.5|Représentation intermédiaire d'un compilateur.]] |[[File:Code interpreter scheme.png|centre|vignette|upright=1.5|Fonctionnement d'un interpréteur.]] |} Faire ainsi a de nombreux avantages, le principal étant d'avoir un compilateur capable de traduire un langage de haut niveau vers plusieurs jeux d'instructions différents. Cela permet d'avoir, par exemple, un compilateur qui traduit du C soit en code machine x86, soit en code machine pour un CPU ARM, soit pour un CPU POWERPC, etc. [[File:Compiler design.svg|centre|vignette|upright=3|Compiler design]] Pour l'interprétation, l'usage d'un ''bytecode'' a d'autres avantages. Pour l'exploiter, il faut distribuer non pas le code source, mais le ''bytecode''. En clair, l'interpréteur ne prend pas en entrée le code source, mais du ''bytecode'' déjà compilé. La traduction est alors beaucoup plus simple, car le code source a déjà été partiellement compilé, par la traduction en ''bytecode''. Interpréter du code source est en effet assez compliqué : il faut effectuer des étapes d'analyse lexicale, sémantique, et bien d'autres. Avec du ''bytecode'', ces étapes ont été réalisées lors de la compilation du ''bytecode'', l'interpréteur a alors peut de choses à faire. L'interprétation du ''bytecode'' se fait instruction par instruction, au niveau du ''bytecode''. Une instruction du ''bytecode'' est traduite en une instruction machine équivalente, qui est exécutée. Si l'instruction est un peu complexe, il exécute une fonction/procédure qui fait la même chose. Pour résumer, le ''bytecode'' est ensuite traduit à la volée et exécuté instruction par instruction par un logiciel appelé l'interpréteur. [[File:Control table.png|centre|vignette|upright=2|Control table]] Le langage intermédiaire peut être vu comme l'assembleur d'un processeur, n'existe pas forcément dans la réalité, mais dont le jeu d'instruction est décrit en détail. Le processeur en question est appelé une '''machine abstraite''', ou encore une ''machine virtuelle''. Nous utiliserons le terme de machine abstraite dans ce qui suit. La machine abstraite n'est pas la même suivant que l'on cible la compilation ou l'interprétation/JIT. Pour l'interprétation/JIT, la machine abstraite est souvent une machine à pile, car cela simplifie grandement la traduction en code machine final. L'interprétation demande que la traduction du ''bytecode'' en code machine soit la plus simple possible. Et cela demande de prendre en compte pas mal de détails. Par exemple, les différents jeux d'instruction existants n'ont pas le même nombre de registres, ce qui pose problème lors de la traduction du ''bytecode''. Lors de la transformation en code machine, un algorithme d'allocation de registres se débrouille pour traduire le code intermédiaire en code qui utilise un nombre limité de registres. Pour améliorer cette allocation de registres, il y a deux solutions niveau ''bytecode'' : soit utiliser un nombre illimité de registres, soit utiliser une machine à pile. Pour les machines à pile, il existe un algorithme simple et rapide pour traduire un code écrit pour une machine à pile en un code écrit pour un processeur avec des registres, qui se débrouille pas trop mal pour allouer efficacement les registres. C'est un avantage assez important pour les langages interprétés. ==Les processeurs qui exécutent du bytecode== Le ''bytecode'' est un code machine, ce qui signifie qu'il peut en théorie s'exécuter sur un processeur qui implémente le jeu d’instruction associé. Si le jeu d'instruction d'un ''bytecode'' est souvent une description censée être fictive, elle n'en reste pas moins un jeu d'instruction et des caractéristiques précises, qu'on peut l'implémenter en matériel ! Néanmoins, tous les ''bytecode'' ne sont pas égaux de ce point de vue. Certains sont faciles à implémenter en matériel, d'autres non. Les ''bytecodes'' implémentés en matériel sont ceux dont les machines abstraites sont des machines à pile. Un premier exemple est celui des processeurs Pascal MicroEngine, qui exécutaient directement le bytecode du langage Pascal, le fameux UCSD P-code. Un second exemple est la '''machine SECD''', qui sert de langage intermédiaire pour certains compilateurs de langages fonctionnels. Elle a été implémentée en matériel par plusieurs équipes, notamment par les chercheurs de l'université de Calgary en 1989. Dans le même genre, quelques processeurs simples étaient capables d’exécuter directement le ''bytecode'' du langage FORTH. Le FORTH, un des premiers langages à pile de haut niveau, possède de nombreuses implémentations hardware et est un des rares langages de haut niveau dont le ''bytecode'' a été utilisé comme langage machine sur certains processeurs. Par exemple, on peut citer le processeur FC16, capable d’exécuter nativement du ''bytecode'' FORTH. ===Les processeurs Java=== Le cas le plus impressionnant est celui de la machine virtuelle Java, qui est un design de processeur comme un autre. En temps normal, le ''bytecode'' Java est compilé ou interprété, mais certains processeurs exécutaient du ''bytecode'' Java directement, sans interprétation ni compilation. Ils sont appelés des '''processeurs Java'''. On peut citer les processeurs ARM disposant de l'extension Jazelle, du JEMCore et du aJ-100 de aJile Systems, le picoJava II de Sun, et quelques autres. Il y a aussi eu quelques projets de recherche ou processeurs open source, comme Komodo, jamuth, le ''Java Optimized Processor'' et quelques autres projets du même genre. L'implémentation de la machine virtuelle Java n'était cependant pas complète. Quelques instructions complexes n'étaient pas gérées par le processeur et devaient être émulées en logiciel. Mais la grosse majorité des instructions l'était. Les instructions non-supportées étaient émulées avec la méthode ''trap and emulate'', à savoir qu'un opcode inconnu déclenchait une exception matérielle ''opcode invalide'', dont la routine pouvait émuler les instructions qui doivent l'être. Le processeur aJ-100 de aJile est capable d'exécuter la quasi-totalité des instructions de la JVM Java, seules deux instructions faisant exception. Les '''accélérateurs Java''' sont une solution intermédiaire, qui permet de traduire à la volée du ''bytecode'' Java en instructions RISC, exécutées sur un autre processeur. Il s'agit formellement de traduction binaire, mais je préfère en parler ici. L'idée est de combiner un processeur RISC avec un coprocesseur Java. Le coprocesseur lit le ''bytecode'' Java et le traduit en instructions RISC, ces dernières étant exécutées sur le processeur RISC. Le JA108 de Nozomi était un coprocesseur de ce genre, au même titre que le coprocesseur JSTAR de JEDI. L'extension Jazelle de certains CPU ARM fonctionnait vraisemblablement sur un principe similaire, sauf que le coprocesseur était intégré dans le processeur ARM, entre le cache d'instruction et le décodeur d'instructions ARM. L'intérêt des processeurs Java n'est pas qu'une question de performance. Certes, ils permettent d’exécuter plus vite les programmes compilés en ''bytecode'', comme des programmes Java pour la JVM Java. Mais l'intérêt est aussi de faire des économies de mémoire RAM et ROM. Je rappelle que la machine virtuelle Java est une machine à pile, ce qui fait que sa densité de code est excellente. Les programmes en ''bytecode'' Java sont donc très petits, et prennent peu de place en mémoire ROM. De plus, utiliser un processeur Java permet de se passer d'interpréteur ou de JIT Java, ce qui économise encore plus de mémoire. ===Transformer un processeur RISC en machine à pile=== La plupart des processeurs Java utilisent en interne une architecture à registre généraux, qui émule une machine à registre en microcode. Quelques registres sont réservés pour l'état de la machine virtuelle, avec au minimum un registre réservé pour le pointeur de pile, un autre pour un pointeur vers un ''pool'' de constantes, et quelques autres. D'autres registres servent à mémoriser le haut de la pile, à savoir le sommet de la pile et quelques opérandes situées juste en-dessous. Par exemple, le processeur picoJava II gardait les 64 opérandes au sommet de la pile dans 64 registres généraux, séparés des registres pour les pointeurs et l'état de la JVM. Une optimisation possible, envisagée sur les processeurs picoJava, était une sorte de macro-fusion sous stéroïde. L'idée était de fusionner certaines séries d'instructions Java en une seule instruction machine. Typiquement, pour faire une opération arithmétique, une machine à pile empile deux opérandes et exécute une opération, ce qui prend trois instructions machine. La spécification picoJava 1 a proposé d'envoyer directement la seconde opérande en entrée de l'ALU, ce qui fusionnait l'empilement de la seconde opérande et l'opération arithmétique. Le processeur picoJava II va plus loin et fusionne les trois instructions en une seule instruction RISC, agissant sur des registres. Mais la majorité des processeurs Java ne faisaient pas cela, car cela complexifie grandement le décodeur. Les processeurs ''bigfoot'' de DCT utilisaient un système différent pour émuler une pile avec des registres. Leur banc de registre de 64 bits était coupé en deux : les 16 premiers registres étaient utilisés pour la pile, les 48 suivants étaient utilisés pour autre chose. L'idée est que le registre 0 mémorise un emplacement vide, le registre 1 contient le sommet de la pile, le registre 2 l'opérande sous le sommet de la pile, et ainsi de suite. Les 15 premières opérandes de la pile sont donc mémorisées dans les registres. Les instructions écrivent leur résultat dans le registre adéquat, puis tous les registres sont décalés pour que le résultat passe dans le registre R1. Par exemple, les instructions PUSH chargent une opérande dans le registre 0, puis tous les registres sont décalés de un rang, pour que l'opérande chargée soit dans le registre 1. Le registre 0 devient alors le registre numéro 1, le registre 1 devient le 2, et ainsi de suite. Par contre, les opérations arithmétiques font autrement. Une instruction d'addition, par exemple, lit les opérandes dans le registre 1 et 2, puis place son résultat dans le registre 2 (les deux opérandes sont dépilées). Puis, les registres sont décalés d'un rang pour que ce registre 2 devienne le 1. Les registres ne sont en réalité pas décalés, il n'y a pas de transferts entre registres. A la place, les 16 premiers registres utilisaient une forme limitée de renommage de registres, similaire au banc de registre tournant utilisé sur les processeurs EPIC comme l'Itanium. Les numéros de registres sont renommés, décalés, à chaque fois qu'on empile ou dépile une opérande. Pour faire le renommage de registres, processeur contient un ''register stack counter'' de 4 bits, qui est incrémenté à chaque fois qu'on empile une donnée, et est décrémenté quand on dépile une donnée. Quand on veut accéder à un opérande, le numéro de registre architectural associé est traduit en numéro de registre physique, en additionnant le ''register stack counter'' (modulo 16, pour rester sur 4 bits). ==L'accélération de l'interprétation/JIT : le ''Thumb-EE'' d'ARM== Quelques processeurs ont ajouté des instructions pour faciliter le travail des interpréteurs/JIT. Par exemple, en 2005, ARM a ajouté le mode ''Thumb Execution Environment'', qui faisait cela. Il reprenait le jeu d'instruction compact ''thumb'' et ajoutait quelques instructions et modifiait le comportement d'instructions existantes. Le jeu d'instruction ''Thumb-EE'' a été ajouté sur les CPU ARM en 2005, mais a été déprécié en 2011, par manque d'utilité. Mais la tentative mérite qu'on s'attarde dessus. Les CPU de gamme M, déstinés à l'embarqué, n'ont jamais supporté ''Thumb-EE'', vu que de tels processeurs ne sont pas conçus pour exécuter du code interprété/JIT. Le mode ''Thumb-EE'' est un mode d'exécution, séparé de l'ARM normal. Ainsi, une instruction ''Thumb-EE'' et une instruction ARM peuvent avoir le même encodage en binaire, mais se comporter différemment. Le processeur est à tout moment soit en mode ''Thumb-EE'', soit en mode ARM normal, ce qui précise comment ces instructions doivent se comporter. Des instructions ont été ajoutées par ''Thumb-EE'', pour qu'il fasse son travail. L'entrée et la sortie du mode ''Thumb-EE'' se fait avec deux instructions : ENTERX et LEAVEX. Une fois en mode ''Thumb-EE'', le décodage des instructions ''thumb'' se fait avec les règles du ''Thumb-EE'', et non celles du ''thumb'' normal. Une première différence entre ''thumb'' normal et ''Thumb-EE'' est l'ajout de ce qui s'appelle un ''null check'' pour les instructions mémoire. Le ''null check'' est utilisé pour les instructions en mode d'adressage "base + indice" ou "Base + décalage", ou tout autre mode d'adressage avec un registre de base utilisés dans des calculs d'adresse. L'idée est de vérifier si l'adresse de base vaut zéro ou non. Si c'est le cas, le processeur lève une exception matérielle, qui est traitée par l'interprétation ou le compilateur JIT. Une telle situation est en effet signe d'une erreur d'adressage mémoire, qui doit être traitée par l'interpréteur. Typiquement, c'est signe qu'un pointeur n'a pas bien été initialisé, ce qui peut arriver avec du code interprété ou JIT. Les interpréteurs/JIT traitent généralement la situation en ajoutant un test avant toute instruction mémoire, pour vérifier si le pointeur accédé vaut zéro ou non. Avec ''Thumb-EE'', pas besoin d'ajouter les tests en questions : ils sont réalisés automatiquement lors de chaque instruction mémoire. : Les registres pour la pile, comme le pointeur de pile et le pointeur de ''frame'', sont aussi concernés. L'instruction CHKA vérifie que les accès à un tableau ne débordent pas en dehors du tableau en question. Par exemple, pour un tableau de 1024 éléments, elle vérifie si l'indice est compris entre 0 et 1024. Pour cela, elle vérifie si l'indice de tableau est bien dans l'intervalle adéquat et lève une exception matérielle si ce n'est pas le cas. Son utilité se comprend quand on sait que tous les accès à un tableau sont vérifiés par l'interpréteur. L'interpréteur est censé ajouter des instructions pour vérifier les indices, à savoir deux ou trois branchements. Avec CHKA, le test se fait en une seule instruction. D'autres modifications mineures de l'encodage des instructions sont aussi présentes, ainsi que des modifications pour les instructions LOAD/STORE. Précisément, dans les modes "base + indice" et "base + décalage", le décalage et l'indice subissent maintenant des décalages. L'idée est de simplifier le calcul d'adresse. De quoi économiser quelques instructions lors des calculs d'adresse, ce qui facilite le travail de l'interpréteur/JIT. Et des instructions LOAD/STORE précises voient leur comportement modifié pour faciliter la gestion de la pile de Java ou l'exécution des méthodes locales. Pour faciliter le travail de l'interpréteur, deux instructions HBP et HBLP ont été ajoutées. Elles branchent vers une fonction qui gère les exceptions logicielles (une fonctionnalité présente dans de nombreux langages, comme Java). La première effectue un branchement vers la fonction en question, la seconde sauvegarde l'adresse de retour avant de faire ce branchement. Les deux peuvent brancher vers 256 fonctions pré-déterminées, sans avoir à présenter leur adresse. ==La traduction binaire accélérée par le matériel== Plus haut, nous avons surtout parlé des compilateurs et des interpréteurs. Cependant, nous devons aussi parler de la '''traduction binaire'''. Elle traduit un programme écrit dans un code machine vers un autre code machine. Par exemple, elle traduit un programme compilé pour un CPU x86 vers un code machine ARM. En général, le système d'exploitation est généralement compilé pour le jeu d'instruction natif, mais il exécute des applications prévues pour le x86. Les applications sont traduites par le système d'exploitation, avant d'exécuter le code traduit. : Nous parlerons dans la suite de code machine source et de code machine cible, pour parler respectivement du code à traduire et du code obtenu après traduction. Idem avec d'autres termes comme architecture cible/source, ou jeu d'instruction cible/source. La traduction binaire est surtout utilisée pour des questions d'émulation ou de compatibilité. Par exemple, lorsque les Macintosh sont passés de processeurs Power PC vers des processeurs x86, le système d'exploitation Mac OS utilisait la traduction binaire pour convertir les anciennes applications Power PC vers du code x86. Les utilisateurs n'y ont vu que du feu. Le système de traduction binaire était appelé Rosetta 1. Par la suite, lors de la transition de processeurs x86 vers des processeurs Apple, Rosetta 2 a vu le jour. La traduction binaire a surtout été utilisée pour traduire du code x86 vers un autre jeu d'instruction. Il faut dire que le x86 est le jeu d'instruction dominant. De nombreuses entreprises ont eu pour ambition de briser l'hégémonie du x86 sur PC, en remplaçant le x86 par un jeu d'instruction plus performant, tout en gardant une compatibilité maximale. La traduction binaire était la seule solution pratique. Elle était le plus souvent intégralement réalisée en logiciel, comme c'était le cas sur les architectures Itanium avec le ''IA-32 Execution Layer''. ===Les généralités sur la traduction binaire assistée en matériel=== L'architecture source est presque tout le temps une architecture CISC, assez ancienne, qu'on souhaite émuler. Le choix de l'architecture cible se porte souvent sur une architecture VLIW, fort différente de l'architecture source, et ce pour obtenir une économie de matériel conséquente. Pour s'exécuter rapidement, le code traduit doit exploiter le parallélisme d'instruction, à savoir exécuter plusieurs instructions en même temps dans des unités de calcul séparées. Il est possible d'utiliser un processeur superscalaire avec exécution dans le désordre pour cela, mais au prix d'un cout important en transistors. Alors qu'en utilisant un CPU VLIW, c'est le traducteur binaire qui fait tout le travail d'extraction du parallélisme d'instruction. Et après tout, quitte à avoir un traducteur binaire, autant lui refiler le boulot d'optimisation. Compiler du code à la volée est certes assez lourd, mais qu'il existe des algorithmes efficaces pour regrouper des instructions indépendantes dans une seule instruction VLIW. Au passage, le processeur VLIW a plus de registres que le processeur source. Cela permet de faire du renommage de registres directement en logiciel. Le traducteur binaire n'hésite pas à changer les noms de registres entre instructions source et instruction VLIW cible, afin de supprimer des dépendances de données. En conséquence, cela ouvre des opportunités de parallélisme, qui sont exploitées lors du regroupement des opérations en instructions VLIW. Vous pourriez penser que le choix d'un CPU VLIW pour émuler du CISC est tout sauf optimal. Vous devez penser que la traduction binaire est d'autant plus simple que l'architecture source et cibles sont semblables. Dans les faits, ce n'est pas tellement traduire les instructions qui pose problème, mais plus la gestion du registre d'état, des exceptions matérielles, de la mémoire virtuelle, la différence entre gros-boutisme et petit-boutisme, de même que des différences d'adressage pour les périphériques. Le premier problème est la gestion des conditions, notamment en présence d'un registre d'état. En pratique, la traduction binaire s'utilise pour traduire du code CISC vers du code RISC ou VLIW. Les architectures source ont donc un registre d'état, qui est mis à jour non seulement par des instructions de test, mais aussi des instructions arithmétiques. L'architecture cible n'a elle pas de registre d'état, mais des registres à prédicats. La traduction de l'un vers l'autre est alors quelque peu compliquée. Et elle est d'autant plus compliquée que le registre d'état est la source de dépendances d'instruction implicites, qui réduisent les performances. Par exemple, si une instruction arithmétique modifie le registre d'état, cela peut impacter l'exécution d'une instruction ultérieure, qui lit ce registre d'état. Pour éliminer ces fausses dépendances, le traducteur binaire doit renommer ce registre en logiciel et détecter les dépendances utiles. Et le compilateur doit gérer les cas où il y a beaucoup de distance entre les deux, voire les cas où l'instruction dépendante n'a pas encore été analysée par le compilateur. Un autre problème est lié à la gestion des exceptions matérielles, et précisément des exceptions précises. Pour rappel, une instruction VLIW regroupe plusieurs opérations, opérations qui correspondraient à une instruction machine sur un CPU pas VLIW. En conséquence, plusieurs instructions du langage machine "source" sont regroupées en une seule instruction VLIW. Et il faut tenir compte du cas où une instruction source lève une exception. Dans le code VLIW, cela signifie qu'une opération lève une exception, et il faut annuler partiellement l'instruction VLIW associée. Et par partiellement, on veut dire que seules les opérations suivantes dans l'ordre du programme source doivent être annulées, puis ré-exécutées. Et c'est un sacré casse-tête ! Les processeurs VLIW utilisent comme solution le mécanisme d'exceptions différées des processeurs EPIC, vu il y a quelques chapitres. Pour résumer, le code est exécuté par blocs d'instruction, délimités par des branchements ou tout autre limite/barrière pertinente dans le code. Un bloc de code est compilé en deux versions : une version rapide sans exceptions matérielles, une version lente qui gère les exceptions précises. La version rapide s'exécute sans exécuter les exceptions. Cependant, les exceptions matérielles sont enregistrées, pour être prises en compte à la toute fin du bloc de code. La version rapide est exécutée en premier et elle mémorise si une exception matérielle a eu lieu. Puis une instruction vérifie si une exception a eu lieu et décide quoi faire. Si aucune exception n'a eu lieu, elle passe à la suite du programme, les résultats du bloc de code sont définitivement acceptés. Mais si une exception a eu lieu, tout est annulé. Le processeur est remis dans l'état initial, puis le code est ré-exécuté instruction par instruction de manière à gérer l'exception correctement. ===La traduction par pré-décodage=== Une première solution serait de faire la traduction binaire dans le cache d'instruction. Lors d'un défaut de cache, le code chargé depuis la RAM est traduit en code machine cible. Et c'est ce code machine cible qui est mémorisé dans le cache d'instruction et exécuté par le processeur. La technique marche sur le papier et n'est qu'une amélioration des techniques de pré-décodage vues il y a quelques chapitres. Cependant, le code obtenu est une traduction assez basique, qui n'incorpore pas d'optimisations dignes de ce nom. De plus, elle gère mal le cas où la taille du code source et cible sont potentiellement très différentes. Concrètement, elle est surtout utile pour traduire du code d'un processeur RISC vers un autre processeur RISC, dont les tailles d'instructions sont similaires. Elle ne permet pas d'émuler plusieurs jeux d'instructions différents, le cout en matériel (un circuit de pré-décodage par jeu d'instruction) serait trop important. ===Le projet DAISY d'IBM=== De très rares processeurs étaient conçus pour accélérer cette traduction binaire, afin de garder de bonnes performances. Les premiers à avoir étudié l'idée étaient IBM, avec leur projet DAISY (''Dynamically Architected Instruction Set from Yorktown''). Le projet de base était de convertir à la volée du code compilé pour des CPU Power PC, vers du code VLIW. Les chercheurs d'IBM avaient développé un algorithme de traduction dynamique efficace, ainsi qu'un processeur VLIW disposant d'optimisations spécifiques à la traduction binaire dynamique, à la volée. De nombreuses idées de ce projet ont été reprises ou re-découvertes par la société Transmetta, avec ses processeurs Crusoe et Efficieon, puis par NVIDIA avec son projet Denver. Les ingénieurs de ce projet ont étudié la possibilité d'émuler plusieurs jeux d'instructions différents sur un même processeur, notamment le s390 d'IBM et le x86. En théorie, cela demande juste d'avoir plusieurs programmes de traduction binaire : un pour le x86, un autre pour le s390, éventuellement un autre pour le Power PC. Mais cela ne s'est pas concrétisé. Mais les ingénieurs ont étudié quelle pourrait être l'architecture VLIW idéale pour ça. Par exemple, il fallait des additions 3-opérandes pour simplifier les calculs d'adresse. Une difficulté était la gestion du registre d'état, dont les bits ne sont pas mis à jour de la même manière sur le x86 et le s390 ou le Power PC. La différence entre gros-boutisme et petit-boutisme était aussi un problème, de même que des différences d'adressage pour les périphériques. Pour que la traduction binaire soit efficace, le processeur VLIW intègre diverses optimisations, comme les exceptions différées et les branchements multi-voies. La principale est la suivante : seul le code des boucles ou fonctions exécutées fréquemment est traduit en code VLIW, le reste du code est interprété. En effet, traduire du code binaire prend plus de temps qu'une simple interprétation. Pour du code qui ne sera exécutée qu'une seule fois, il est plus rapide d'utiliser l'interprétation. Par contre, pour du code exécuté beaucoup de fois, le cout de la traduction binaire est amorti, dilué sur N exécutions, compensé par le gain en temps d'exécution de ce code traduit. Au final, cela permet de ne traduire que le code qui le mérite. La détection du code fréquemment exécuté est réalisée dans le cœur VLIW. L'unité de branchement mémorise les derniers branchements rencontrés et le nombre de fois qu'ils ont été exécutés. Si ils ont été exécutés un certain nombre de fois, l'unité de branchement lève une exception matérielle, qui invoque le traducteur binaire. La routine de cette exception est le traducteur binaire proprement dit. En clair, la traduction binaire est démarrée quand le processeur détecte qu'une fonction a été exécutée plus de N fois, via une exception matérielle dédiée. La mesure du nombre d'exécution d'un branchement se fait dans l'unité de calcul dédiée aux branchements, avec l'aide d'une sorte de ''branch adress buffer'' modifié. Pour rappel, le ''branch adress buffer'' mémorise le ''Program counter'' de chaque branchement récemment rencontré. Ici, chaque entrée du ''branch adress buffer'' est associée à un compteur incrémenté à chaque exécution du branchement. ===Les processeurs Crusoe et Efficieon de Transmetta=== Les processeurs Crusoe Et Efficeron sont deux processeurs VLIW produits par la société Transmetta, une société californienne rachetée par NVIDIA. Ils étaient conçus pour exécuter spécifiquement des programmes x86, système d'exploitation inclus. L'idée derrière ce projet était d'exécuter du code x86 sans que les applications, ni même le système d'exploitation et le BIOS soient au courant ! Les processeurs Transmetta étaient en réalité conçus pour exécuter un programme unique, le ''Code Morphing Software'' (CMS), qui traduisait le code x86 en code VLIW. Transmetta ne voulait pas que son processeur VLIW soit exploité directement, sans le CMS. Il n'avait pas rendu public de compilateur pour traduire du C vers du code VLIW, il n'avait pas donné la documentation du jeu d'instruction VLIW. Des efforts de rétro-ingénieurie, documentés sur le site realworldtech, ont cependant permis de comprendre comment étaient encodées les instructions du processeur Crusoe. Comme pour le projet Daisy, le ''Code Morphing Software'' utilisait à la fois interprétation et traduction binaire, selon les besoins. Les instructions sont interprétées lors de leurs premières exécutions, mais le CMS bascule sur de la traduction en code VLIW après un certain nombre d'exécution. Ainsi, les boucles souvent exécutées sont traduites en code VLIW, alors que le reste du code est interprété. Le choix du code à traduire est le fait du CMS, il utilise des heuristiques complexes pour, qui ne sont pas connues dans le détail. Le ''Code Morphing Software'' est mémorisé dans une EEPROM, ce qui en fait un ''firmware'' situé en dessous du BIOS. Le CMS démarre ensuite le BIOS, qui lui-même démarre le système d'exploitation, qui lui-même démarre les pilotes de périphériques et les programmes. Le CMS se réserve les 16 premiers mébioctets de l'espace d'adressage. L'EEPROM du CMS est mappée dedans, mais n'en utilise que la moitié. Le reste est utilisé comme cache, pour mémoriser le code VLIW traduit par le CMS. Les deux processeurs intégraient aussi deux mémoires SRAM, utilisées par le CMS, appelées la ''local program memory'' (LPM) et la ''local data memory'' (LDM). La première contient du code qui gére les interruptions, la mémoire virtuelle, les exceptions matérielles, les problèmes d'alignement mémoire, et quelques fonctions très fréquemment utilisées par le CMS. La seconde est de la mémoire RAM utilisée par ces fonctions de la ''local program memory''. Les mémoires caches L1 et L2 sont séparées de ces deux mémoires. Le processeur Crusoe et Efficieon étaient des processeurs VLIW très simples. Ils n'avaient même pas de MMU, ni de fonctionnalités importantes sur les CPU x86. Pour Crusoe, les instructions VLIW étaient encodées sur 64 ou 128 bits, et regroupaient entre 2 à 4 opérations. Les instructions LOAD/STORE ne supportaient pas d'adressage indicé, juste de l'adressage indirect à registre. Crusoe avait 5 unités de calcul : deux ALU entières, une unité LOAD/STORE, une unité de branchement, une FPU. Efficieon doublait le nombre d'ALU entière et d'unité LOAD/STORE, les instructions VLIW passaient à 256 bits. Le processeur Crusoe contient 160 registres, dont 64 registres généraux, 32 registres flottants. Les registres généraux font 32 bits, ce qui est cohérent avec le fait que les CPU x86 de l'époque étaient des processeurs 32 bits. Quant aux registres flottants, ils faisaient 80 bits, ce qui colle avec la taille des registres flottants de la FPU x87 utilisée à l'époque. Sur les 64 registres généraux, seuls 48 étaient réellement utilisable pour mémoriser des opérandes. Une partie des registres généraux étaient utilisés pour la gestion de la pile, d'autres pour mémoriser l'état du CPU x86 émulé, un registre était un registre zéro non accesible en écriture. La gestion des exceptions est optimisée avec un système d'exceptions différées, le même que celui décrit plus haut. Le processeur utilise un système similaire, pour effectuer des lectures anticipées, qui a été expliqué dans le chapitre sur les processeurs VLIW/EPIC. Sauf qu'il s'agit là d'une optimisation pour gagner en performances en général, pas une aide utile pour gérer des exceptions précises ou toute autre subtilité de la traduction binaire. Pour gérer les exceptions différées et les lectures anticipées, le processeur mémorise l'état du processeur avant de démarrer un bloc de code. Pour cela, le processeur contient des ''shadow registers'', 48 registres entiers et 16 registres flottants. Vous remarquerez qu'il y a autant de registres entiers que de ''shadow registers'' entiers. Les registres entiers/flottants sont mémorisés dans les ''shadow registers'' avant d'exécuter un bloc de code, afin que le processeur puisse revenir à l'état de base. Une autre fonctionnalité liée est le ''gated store buffer''. Le principe est d'éviter toute écriture en mémoire RAM, tant que le code VLIW n'a pas émis d'instruction ''commit''. L'idée est que les écritures sont conservées dans la ''store queue'' de l'unité LOAD/STORE. Pour rappel, la ''store queue'' met en attente les écritures soit tant que les écritures ne sont pas terminées, soit tant que la RAM est occupée. L'idée est que les écritures sont accumulées dans la ''store queue'' tant que l'instruction ''commit'' n'a pas eu lieu. Le ''store queue'' est "déversé dans la RAM" seulement quand l'instruction ''commit'' s'exécute. ===Les CPU NVIDIA Denver=== Nvidia a tenté quelque chose de similaire avec son projet Denver. NVIDIA a racheté l'entreprise Transmetta, ses brevets, et a tenté de refaire la même chose. Le projet initial était de traduire du code x86 vers un jeu d'instruction VLIW propriétaire, comme l'a fait Transmetta. Mais NVIDIA n'a pas réussi à acquérir la licence du jeu d'instruction x86 et s'est rabattu sur le jeu d'instruction ARM. Le processeur né de ce projet est le Tegra K1-64 CPU. Il s'agit d'un processeur composé d'un cœur VLIW, couplé à deux décodeurs d'instructions ARM. Les deux décodeurs ARM traduisent une paire d'instructions ARM consécutives en instructions VLIW, pour les exécuter sur le cœur VLIW. Le CPU peut fonctionner selon deux modes : ARM et VLIW. En somme, le processeur supporte deux jeux d'instruction : le jeu d'instruction ARM, et un jeu d'instruction VLIW. En mode ARM, les instructions ARM sont chargées depuis le cache, traduites en VLIW par les décodeurs ARM, puis exécutées par le cœur VLIW. En mode optimisé, le CPU exécute des instructions VLIW chargées depuis le cache d'instruction, les décodeurs ne sont pas utilisés. : Le CPU supporte en réalité deux jeux d'instruction ARM : ARM8 et 7. Mais ils correspondent au même mode pour le CPU. Un point important est que le processeur peut changer très rapidement de mode, en à peine quelques cycles d'horloges. On n'est clairement pas dans le cas des CPU x86, qui mettent des plombes pour passer du mode 32 à 64 bits et inversement. La commutation est tellement rapide qu'on peut considérer que le processeur supporte deux jeux d'instruction simultanément. Et le support simultané de deux jeux d'instruction facilite l'implémentation de la traduction binaire. Encore une fois, inutile de traduire du code qui ne sera exécutée qu'une seule fois, mieux vaut l'exécuter directement dans le mode ARM. Surtout que le processeur supporte un mode ARM qui permet d'exécuter du code ARM sans perte de performance. Par contre, le code fréquemment utilisé, notamment dans des boucles critiques, est traduit en VLIW. Au final, les performances sont optimisées, en limitant le travail du logiciel traducteur binaire. La détection du code fréquemment exécuté se fait différemment que pour les processeurs Transmetta et Daisy. L'unité de branchement ne détecte pas directement le code beaucoup exécuté, même si elle a un rôle à jouer. A la place, la détection est réalisée en logiciel, par un ''thread'' dédié, qui s'exécute sur un cœur séparé. Il détecte le code fréquemment utilisé en regardent les branchements exécutés récemment. Pour cela, le processeur mémorise l'historique des branchements pris récemment et copie cette historique dans une mémoire tampon dédiée, partagée entre tous les cœurs. Vu qu'elle est partagée, le ''thread'' de détection a accès à l'historique sans pertes de performances, seule la copie de l'historique dans ce tampon a un cout en performance, pas son partage. Le code VLIW obtenu après traduction binaire est mémorisé dans la mémoire RAM, dans une portion spécialement réservée pour. Elle fait 128 mébioctets, et est appelée le '''cache d'optimisation''' par NVIDIA. Le cache d'optimisation est protégé en écriture et seul le traducteur binaire peut écrire dedans. Au passage, le traducteur binaire est du code VLIW, ce qui fait qu'il est placé dans le cache d'optimisation. Le processeur détecte automatiquement quand une fonction pour laquelle le code compilé équivalent est disponible. Pour cela, l'unité de chargement contient une table de correspondance entre l'adresse de la fonction ARM, et l'adresse de son équivalent compilé. Quand une fonction est appelée, l'unité de chargement regarde l'adresse de destination du branchement. Si l'adresse est dans cette table, elle récupère l'adresse de la fonction compilée et branche vers celle-ci. Le processeur dispose d'un cache d'instruction de 128 kibioctets, ce qui est très important, mais nécessaire vu la taille des instructions VLIW. Le coeur VLIW dispose de ses propres décodeurs, d'un ''scoreboard'', de 7 unités de calcul et de registres. Pour les unités de calcul, il a deux FPU, deux ALU, une unité de branchement, et deux unités LOAD-STORE faisant aussi ALU. Un point important est que le CPU dispose de 64 registres entiers et de 64 registres flottants. C'est deux fois plus que supporte le jeu d'instruction ARM. En mode ARM, seule la moitié des registres est utilisée. Mais en mode optimisé, le traducteur binaire utilise bien les 64 registres, grâce à une sorte de renommage de registres logicielle. Pour simplifier la traduction binaire, le processeur supporte les techniques vues précédemment. Le processeur intègre un ''gated store buffer'' similaire à celui des processeurs Transmetta. Les techniques d'exceptions différées sont aussi supportées, comme sur les processeurs Transmetta. Il a aussi une unité de préchargement décrite par NVIDIA comme agressive, avec support du préchargement de type anticipé (''runahead''). ===L'émulation de la mémoire virtuelle=== Pour finir, il faut parler de la mémoire virtuelle avec la traduction binaire. Sur le processeur source, le processeur gère à la fois des adresses physiques et virtuelles et sa MMU fait la conversion entre les deux. Sur le processeur VLIW, la MMU est simulée par le programme de traduction binaire, partiellement ou totalement. L'implémentation exacte varie suivant que l'on parle du projet Daisy, des processeurs Transmetta ou de Denver. Sur les processeurs Transmetta, il n'y a pas de MMU ni de mémoire virtuelle. A la place, le programme de traduction binaire émule la mémoire virtuelle du processeur source. Le processeur VLIW ne gère que des adresses physiques, rien d'autre. L'espace d'adressage physique du processeur VLIW a la même taille que celui du processeur émulé, ici des processeurs x86. Il est vraisemblable que les adresses physiques utilisées par le processeur x86 sont les mêmes que celles du processeur VLIW. Sur le projet Daisy, le processeur VLIW gère la mémoire virtuelle via pagination, comme les processeurs émulés. Ce qui fait qu'il gère un espace d'adressage virtuel et un espace d'adressage physique. Pour éviter toute confusion, nous parlerons d'adresse physique/virtuelle VLIW pour les adresses physiques/virtuelles du processeur VLIW, d'adresse virtuelle/physique source pour celle du jeu d'instruction traduit, à savoir du Power PC ou du s390, éventuellement de l'x86. Il faut alors faire le lien entre adresses physiques source et adresses virtuelles VLIW. Pour cela, rien de plus simple : il y a correspondance parfaite. L'adresse physique source numéro N correspond à l'adresse virtuelle VLIW numéro N. Cependant, cela signifie que tout l'espace d'adressage virtuel VLIW serait utilisé par le code à traduire. En réalité, il faut ajouter de la place pour le code traduit et le programme de traduction binaire. La conséquence est que l'espace d'adressage virtuel VLIW est plus large que l'espace d'adressage source. L'espace d'adressage virtuel VLIW est découpé en trois sections : une section pour le code Power PC à traduire, une autre pour le code traduit en VLIW, et une dernière réservée au traducteur binaire. Le programme de traduction est placé dans une ROM mappée en mémoire dans la seconde section. Le reste de la seconde section est réservé à une mémoire RAM utilisée par le programme de traduction. [[File:Mémoire virtuelle sur le projet Daisy.png|centre|vignette|upright=1.5|Mémoire virtuelle sur le projet Daisy]] Il faut noter que chaque page physique source, une fois traduite, correspond à N pages virtuelles VLIW. La raison est que le code traduit prend plus de place que le code machine originel. En conséquence, on doit utiliser une page finale plus grande. Pour le reste, des pages contiguës en mémoire physique source sont elles aussi contiguës en mémoire virtuelle VLIW. Le calcul d'adresse est donc simple : l'adresse physique source est multipliée par N, puis on ajoute l'adresse de base à laquelle commence la section pour le code traduit. ==La traduction binaire sur le Burroughs B1700== Nous venons de voir comment accélérer en matériel la traduction binaire, avec des techniques bien spécifiques. Le Burroughs B1700 a procédé autrement et il est intéressant d'étudier en détail son architecture. Son architecture est décrite dans le livre "Interpreting Machines : Architecture and Programming of the Bl700/Bl800 Series", écrit par Elliott I. Organick et James A. Hinds. Une partie de ce qui vfa suivre est un très court résumé de ce livre. ===Le Burroughs B1700 : architecture externe=== Il disposait de 4 registres généraux, nommés X, Y, T et L, tous de 24 bits. Les registres T et L étaient découpés en 6 sous-registres de 4 bits, nommés LA LB LC LD LE et LF pour le registre L, et TA TB TC TD TE et TF pour le registre T. Ils étaient adressables comme n'importe quel registre. Un point important est que le registre T avait des possibilités d’extraction spécifiques. Il était possible d'extrait N bits, placés n'importe où dans ce registre, et de les copier dans un autre registre. Nous verrons pourquoi cette possibilité est intéressante et ne sert que pour le registre T. Les registres X et Y étaient reliés à une ALU de 24 bits capable d'effectuer des opérations arithmétiques basiques. Fait étonnant, l'ALU faisait tous les calculs en même temps et fournissait ses résultats dans 7 registres : un registre SUM pour l'addition, un registre DIFFERENCE pour la soustraction, et un registre par opération bit à bit. Les registres pour les opérations bit à bit sont : CMPX et CMPY pour l'opération NOT sur chaque opérande, XANY pour le ET bit à bit, XEOY pour le XOR, XORY pour le OU logique. Les additions et soustractions pouvaient se faire sur des opérandes codés en binaire ou en BCD, suivant le code opération utilisé. A cela, il fallait ajouter deux registres de "résultat" MSKX et MSKY. Ils fournissaient un masque dépendant de la taille du résultat en bits. Cela permettait de gérer des données de 0, 4, 8, 12, 16, 20, et 24 bits, alors que l'ALU faisait 24 bits. Et cela n'était pas limité à 16 ou 8 bits, mais n'importe quelle taille inférieure ou égale à 24 bits était supportée, même des tailles non-conventionnelles comme des résultats codés sur 3 bits, 9 bits, 12 bits, etc. L'ALU prenait en opérande les registres X et Y, mais aussi un registre CYF pour la retenue entrante. En sortie, il fournissait aussi deux retenue sortantes : une pour l'addition et une autre pour la soustraction. Elles étaient mémorisées dans les registres CYL et CYD, respectivement. L'ALU fournissait aussi 12 conditions, qui alimentait un registre d'état. Le registre d'état était segmenté en trois registres appelés XYST, XYCN et BICN. Le processeur contenait de nombreux autres registres de 4 bits, dont les sous-registres de T et L. En tout, il y a 27 registres de 4 bits adressables ! Et le processeur disposait d'une ALU 4 bits pour manipuler ces registres de 4 bits ! Elle est capable de faire des MOV entre registres de 4 bits, des opérations bit à bit, et peut aussi tester si un bit vaut 0 ou 1. La dernière possibilité est très utile pour implémenter les branchements. Pour communiquer avec les entrées-sorties, il disposait de deux registres CMND et DATA : CMND pour envoyer une commande à une entrée-sortie, DATA pour lire ou écrire une donnée. Pour la mémoire, il avait deux registres séparés : READ et WRITE. READ contient la donnée lue lors d'une lecture,; WRITE est pour une donnée à écrire lors d’une écriture. Il s'agit de registres d’interfaçage mémoire, qui ont été promus au rang de registres architecturaux. Les deux registres sont gérés par le microcode, qui est le langage principal de la machine. Les instructions machines configurées font sans, mais le microcode doit y avoir accès facilement. Et vu que le microcode est le langage principal de ce processeur, ces deux registres deviennent architecturaux. ===Le Burroughs B1700 : le mal-nommé ''split-level control store''=== Le Burroughs B1700 avait plusieurs modèles : le B1726, le B1710 et le B1800. Ils se distinguaient par la manière dont le code machine source était traduit par le traducteur binaire. Dans ce qui suit, nous parlerons de traducteur binaire pour parler soit d'un interpréteur pour un langage de programmation de haut niveau, soit pour un logiciel de traduction binaire. Le B1710 plaquait le traducteur binaire en mémoire RAM. Le B1800 faisait pareil, mais avait ajouté un cache dédié au code du traducteur binaire, afin d'améliorer les performances. Le B1726 avait un traducteur binaire scindé en deux : une partie était en mémoire RAM, l'autre était dans un ''local store'' dédié. Le processeur peut exécuter le traducteur binaire soit depuis le ''local store'', soit depuis la mémoire RAM avec une pénalité en termes de performances. La documentation appelle cela un '''''split-level control store''''', mais le terme est trompeur : la documentation qualifie de microcode le langage machine du Burroughs B1700, ce qui est à l'origine de beaucoup de confusions. Le programmeur devait découper le traducteur binaire en segments, certains étant placé dans le ''local store'', les autres étant en mémoire RAM. Mieux que ça, le processeur autorisait d'utiliser la technique de l'''overlaying'' pour le microcode ! Il y avait même une fonction OVERLAY dédiée pour ! OVERLAY avait besoin de trois opérandes : la taille du segment à copier, son adresse source en mémoire RAM, son adresse de destination dans la micro-SRAM. Le ''local store'' mémorisait au maximum 2048 instructions machine, ce qui fait que les 2048 premières "adresses" correspondaient au ''local store''. Les suivantes étaient des adresses de mémoire RAM qui contenaient le reste du traducteur binaire. En pratique, l'adresse qui séparait les deux était configurable ! On pouvait limiter la taille du ''local store'' à un multiple de 32 instructions ! Les adresses restantes étaient alors réattribuées à la RAM. Par exemple, on pouvait attribuer 64 adresses au ''local store'', les 2048 - 64 adresses restantes étaient alors attribuées à la mémoire RAM. Un registre limite, nommé TDPM, indiquait quelle était l'adresse de démarcation entre les deux. Il contenait 4 bits, dont la valeur était multipliée par 32, pour obtenir l'adresse limite. L'interpréteur réservait un "segment" en mémoire RAM, pour les données, dont le processeur connaissait la position. En-dehors de ce segment, il n'y a que des instructions machines à traduire, et le traducteur binaire. Le CPU incorporait pour cela deux registres, un pour l'adresse de base du segment de données, un autre pour son adresse de fin. Il incorporait une protection mémoire limitée sur ce segment, à savoir que seul l'interpréteur pouvait lire ou écrire dans ce segment. ===Le code machien du Burroughs B1700=== Le processeur utilise des micro-opérations de 16 bits de long, qui sont copiées dans registre d'instruction M de 16 bits. Fait intéressant, il était possible de faire un OU logique entre l'instruction lue et un registre du processeur, le résultat étant mémorisé dans le registre M. Cela permettait de faire du code automodifiant, pour émuler des modes d'adressage complexe. Cela permettait par exemple de remplacer une adresse dans une micro-opération. L'adresse est alors surimposée avec un OU dans le champ d'adresse, alors que le reste de l'instruction n'est pas modifié. Afin de gérer les interruptions, ces dernières étaient partiellement détectées en logiciel. Les interruptions ne s'exécutaient pas automatiquement, pour émuler correctement les interruptions. Une interruption doit être prise en compte à la toute fin de l'exécution d'une instruction, quand celle-ci a finit son travail. Mais vu qu'ici, les instructions sont émulées avec des séries de micro-opérations, il y a un problème : il est possible d'interrompre une instruction émulée en plein milieu. Pour éviter cela, le processeur ne déclenche pas les interruptions matérielles immédiatement, elles sont mises en attente. Une micro-opération dédiée vérifie si une interruption est en attente, et l'exécute alors cas échéant. La micro-opération est utilisée comme micro-opération finale d'une instruction émulée. ===Une architecture bit-adressable=== Si avoir un microcode réinscriptible est suffisant, quelques choix de ''design'' facilitent l'émulation de jeux d'instructions variés. Par exemple, le CPU Burroughs B1700 avait ajouté une fonctionnalité très importante : il était bit-adressable. En clair, il pouvait adresser la mémoire bit par bit, si nécessaire. L'avantage est que cela permettait d'émuler des architectures dont la taille des registres/''byte''/mots était très différente. Un autre avantage est que cela facilitait la gestion des instructions de taille variable. Détaillons rapidement le premier avantage. Le processeur était un CPU 24 bits, mais il pouvait émuler des processeurs 16 bits, 8 bits, 4 bits, 1 bit, 9 bits, ou toute autre valeur. Pour cela, le processeur lisait des mots de 24 bits et utilisant un ''barrel shifter'' pour sélectionner les bits adéquats. Par exemple, pour un accès de 16 bits, la donnée lue en mémoire était masquée de manière à ne garder que les 16 bits de poids faible. Le ''barrel shifter''/circuit de masquage était placé directement avant le bus mémoire, sur le trajet des données/instructions. Le ''barrel shifter'' était commandé par un registre ''Field Unit'', qui précisait quelle était la taille des données à charger. Le processeur gère un micro-''program counter'' appelé le registre A. Les micro-opérations du processeur font 16 bits de long, et elles sont alignées en RAM sur 16 bits, ce qui fait que les 4 bits de poids fort de ce registre valent 0. Le registre fait 18 bits, mais seuls les 14 bits de poids fort sont utiles. Les adresses mentionnées plus haut, qui adressent les micro-opérations en mémoire RAM, sont des adresses de mots de 16 bits, pas les adresses de bit individuels. ===La traduction binaire est aidée par des registres et instructions dédiées=== Les instructions à décoder n'ont pas de longueur fixe, à savoir qu'elles peuvent faire 16 bit avec tel jeu d'instruction, 8 bits avec un autre, 32 bits sur un autre, etc. Elles peuvent aussi être de longueur fixe ou variable ! Le B1700 intégrait donc un support des instructions de longueur variable, qui était "détourné" pour gérer des jeux d'instruction différents. Notons que vu que l'architecture est bit-adressable, les contraintes d'alignement des instructions disparaissent, ce qui a des avantages qu'on va détailler dans ce qui suit ! L'implémentation des instructions de taille variable ajoutait un registre avant le registre d'instruction. Le registre d'instruction proprement dit était un registre M de 24 bits. Il était précédé par un registre T, dans lequel l'instruction était assemblée, en combinant plusieurs morceaux. Si l'instruction est codée est codée sur moins de 24 bits, le registre T se contente de masquer les bits en trop. Si l'instruction chargée dépasser 24 bits, le chargement se faisait en plusieurs passes, avec plusieurs lectures en mémoire. Dans ce qui suit, on suppose que l'instruction est composée de '''champs''' : l'opcode, les opérandes, les adresses absolues, les constantes immédiates, les numéros de registre, etc. L'idée est que l'instruction est chargée dans le registre T, puis découpée en champs, qui sont copiés dans les registres du processeur ou envoyés au microcode. Je rappelle que le registre T est relié à un ''barrel shifter'', différent au précédent, qui permettait de masquer, décaler et extraire une donnée de ce registre. Et il y a la possibilité d'extraire une suite de bit de ce registre T, pour la copier dans un autre registre. Par exemple, prenons une instruction composée d'un opcode, d'une constante immédiate et d'une adresse immédiate. L'opcode est lu et interprété par le microcode. Le microcode extrait la constante immédiate du registre T et la copie dans le registre X. Ensuite, l'adresse est extraite et envoyée au registre FA, une lecture est démarrée, et l'opérande est copiée dans le registre Y. Enfin, l'instruction est exécutée par le microcode. Le tout prend plusieurs micro-opérations : une première pour lire l'opcode, une seconde pour lire la première opérande, une troisième pour lire la seconde opérande, una quatrième pour tout exécuter. Notons que suivant la taille des champs, on peut en charger plusieurs à la fois. Par exemple, imaginons qu'on ait chargé 24 bits, contenant un opcode de 8 bits et une constante immédiate de 16 bits : pas besoin de faire deux accès mémoire, le registre T est juste découpé en un opcode et une constante. Le registre T est fait pour ce genre d'extraction. ===Le chargement d'un champ=== L'architecture est bit-adressable car elle gére des champs qui ne font pas forcément 8 ou 16 bits. Par exemple, certains jeux d'instructions ont des opcodes codés sur 6 ou 7 bits, voire 9-10 bits. Par exemple, prenons un opcode de 7 bits, suivi par une adresse de 12 bits et une constante immédiate de 15 bits : aucun de ces champs ne sera aligné sur 8/16/24 bits. Utiliser une architecture bit-adressable règle le problème d'alignement. Et en général, les contraintes d'alignement des instructions disparaissent ! Quand on charge une instruction, peut importe qu'elle soit ou non alignée sur 24 bits, elle sera placée dans les bits de poids faible, il suffira de masquer les bits de poids fort inutiles. Par exemple, si on charge une instruction de 16 bits, les 16 bits seront copiés dans les 16 bits de poids faible du registre T, il suffira de masquer l'octet de poids fort. Pas besoin de faire de décalages pour gérer des instructions mal alignées... Pour charger une instruction, il faut connaitre plusieurs informations : l'adresse de son premier bit, sa taille. Pour cela, le B1700 contient deux registres, nommés ''Field Adress'' (FA) et ''Field Length'' (FL), qui indiquent l'adresse du premier bit et la taille de l'instruction à charger. Le premier fait 24 bits, ce qui permet de gérer des adresses de 24 bits. N'oubliez pas que le processeur est bit-adressable, ce qui permet d'adresser 16 Mébi-bits, soit 2 mébioctets. Le registre pour la taille fait lui 16 bits, ce qui permet de gérer des instructions de 65536 bits, soit 8192 octets ! Leur nom commence par ''field'', car le processeur peut gérer des instructions de taille variable et/ou plus longues que 24 bits. Dans ce cas, le chargement de l'instruction se fait champ par champ. Pour donner un exemple, prenons une instruction composée de 20 champs de 12 bits chacun. Les champs sont chargés un par un dans le registre T. Le registre FA est alors initialisé avec l'adresse de l'instruction, il est incrémenté de 12 à chaque cycle. Le registre FL est initialisé avec la taille de l'instruction, soit 20 * 12 = 240 bits, et il est décrémenté de 16 à chaque cycle. Précisons cependant que la taille d'un champ peut être changée d'un cycle à l'autre. Par exemple, le processeur peut charger un opcode de 8 bits, l'interpréter, détecter que l'instruction demande ensuite de charger une adresse de 16 bits. Dans ce cas, le registre FL est immédiatement altéré pour charger 16 bits, au lieu des 8 bits de l'opcode immédiatement précédent. ==Les jeux d'instructions dédiés à un langage de programmation== De rares processeurs sont conçus pour un langage de programmation en particulier. On appelle ces processeurs, conçus pour des besoins particuliers, des '''processeurs dédiés'''. Par exemple, les fameux ''Burrough E-mode'' B5000/B6000/B7000 étaient spécialement conçus pour exécuter de l'ALGOL-60. Leurs cousins B2000/B3000/B4000 étaient eux conçus pour le COBOL. Des langages fonctionnels ont aussi eu droit à leurs processeurs dédiés. Le prolog en est un bel exemple, avec les superordinateurs de 5ème génération qui lui étaient dédié. On peut aussi citer les machines LISP, dédiés au langage LISP, qui datent des années 1970. Elles étaient capables d’exécuter certaines fonctions de base du langage directement dans leurs circuits : elles possédaient notamment un ''garbage collector'' câblé dans ses circuits ainsi que des instructions machines supportant un typage déterminé à l’exécution. Les processeurs dédiés ont eu leur heure de gloire au début de l'informatique, à une époque où les langages de haut niveau venaient d'être inventés. À cette époque, les compilateurs n'étaient pas performants et ne savaient pas bien optimiser le code machine. Il était alors rationnel, pour l'époque, de rapprocher le code machine cible et le langage de programmation de haut niveau. De nombreuses architectures dédiés ont ainsi été inventées, avant que les concepteurs se rendent compte des défauts de cette approche. Les défauts en question ne sont pas nombreux, mais assez simples à comprendre. Premièrement, elles sont très rapides pour un langage de programmation en particulier, mais sont assez mauvaises pour les autres, d'où un problème de "compatibilité". Ajoutons à cela que les langages de programmation peuvent évoluer, devenir de moins en moins populaires/utilisés, ce qui rend la création d'architectures généralistes plus pertinente. Enfin, les architectures dédiées sont évidemment des processeurs CISC, pour implémenter les nombreuses fonctionnalités des langages évolués. Et les défauts des CISC sont assez rédhibitoires à l'heure actuelle. À l'heure actuelle, les algorithmes des compilateurs se sont améliorés et savent nettement mieux utiliser le matériel. Ils produisent du code machine efficace, ce qui rend les architecture dédiées bien moins intéressantes. Si on ajoute les défauts de ces architectures dédiées, par étonnant que les architectures dédiées aient presque disparues. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'accélération matérielle de la virtualisation | prevText=L'accélération matérielle de la virtualisation | next=Le matériel réseau | nextText=Le matériel réseau }} </noinclude> fvh20jbpsam3u7th1tw4kcb5ku0tbj4 764041 764040 2026-04-19T19:59:16Z Mewtow 31375 /* Le code machien du Burroughs B1700 */ 764041 wikitext text/x-wiki De nos jours, la majorité des programmeurs programment dans des langages de haut niveau. Il est très rare pour eux d'avoir à utiliser de l'assembleur, et encore moins un langage machine. Les programmes écrits dans un langage de haut niveau sont traduits en langage machine par un logiciel appelé le compilateur. [[File:Compilation.PNG|centre|vignette|upright=2.5|Compilateur : principe.]] Il se trouve que quelques rares processeurs sont conçus pour faciliter le travail du compilateur. Et ce sont ces processeurs que nous allons voir dans ce qui suit. Ils représentent une catégorie de jeux d'instruction à part, qui n'a pas vraiment de nom. ==La compilation et l’interprétation== Un compilateur traduit donc du code source, écrit dans un langage de haut niveau, vers du langage machine. La traduction est rarement directe. En général, le compilateur traduit le code source en assembleur, qui est lui-même traduit en langage machine. L'assembleur est une représentation textuelle du langage machine alors que le code machine est du binaire exécutable par le processeur. La traduction se fait donc en deux étapes, la '''compilation''' proprement dite et l''''assemblage''', réalisées respectivement par un compilateur et un assembleur. Il faut parfois rajouter une troisième phase d'édition des liens, que nous passons volontairement sous silence. {| |[[File:Code C.png|vignette|200px|Le code source, ici en C.]] |[[File:Assem.png|vignette|250px|Le code assembleur.]] |[[File:Binary file - hello world (C programming).png|vignette|150px|Le code machine.]] |} ===L'interprétation et la compilation à la volée=== Une alternative à la compilation est l''''interprétation''', qui transforme le code source en code machine à la volée. Avec l'interprétation, le code source est passé à un logiciel appelé l''''interpréteur''', qui exécute le code source ligne de code par ligne de code. Une ligne de code est traduite en code machine, qui est exécutée, puis l'interpréteur passe à la ligne suivante. [[File:Kaantotulkkaus.png|centre|vignette|upright=3|Compilation versus interprétation.]] Un défaut de l'interprétation est son cout en performance, qui est assez important. Aussi, l'interprétation stricte a évolué vers un hybride entre interprétation et compilation. L'idée est qu'une partie peu importante du code source est interprétée, alors que le code important est compilé juste avant d'être exécuté. Pour le dire autrement, le code source est partiellement compilé à la volée, juste avant son exécution. Aussi, on parle de '''compilation à la volée'''. Le terme en anglais est ''Just In Time compilation'', abrévié en JIT. En soi, le JIT est associé à de l'interprétation, les JIT sont en réalité des hybrides interpréteurs-compilateurs. Un point important est que compiler du code à la volée est assez lourd, cela a un cout en performance. Par contre, le code compilé s'exécute plus vite que du code interprété. Si on exécute le code compilé une seule fois, le cout de la compilation l'emporte sur le gain à l'exécution. Mais le code compilé est réutilisé autant de fois que nécessaire. Le gain à l'exécution est donc multiplié par le nombre d'exécutions, alors que le cout de la compilation est répartit sur plusieurs exécutions, il est amorti. Les portions du code sont choisies de manière à avoir un gain en performance maximal. L'idée générale est que le code exécuté très souvent est compilé, alors que du code exécuté pas souvent est interprété. Typiquement, les boucles les plus souvent exécutées sont compilées, le code exécuté une seule fois est interprété. ===Les langages intermédiaires=== Les compilateurs modernes passent par un '''langage intermédiaire''' pour faire la transformation en code machine. Le compilateur traduit le langage de haut niveau en langage intermédiaire, puis traduit le langage intermédiaire en code machine cible. Le langage intermédiaire est parfois appelé le '''''bytecode''''', et ce terme recouvre aussi le code écrit avec du ''bytecode''. L'interprétation aussi peut passer par l'intermédiaire d'un un ''bytecode'', ce n'est pas limité aux compilateurs proprement dit. {| |[[File:Intermediate representation scheme.png|centre|vignette|upright=1.5|Représentation intermédiaire d'un compilateur.]] |[[File:Code interpreter scheme.png|centre|vignette|upright=1.5|Fonctionnement d'un interpréteur.]] |} Faire ainsi a de nombreux avantages, le principal étant d'avoir un compilateur capable de traduire un langage de haut niveau vers plusieurs jeux d'instructions différents. Cela permet d'avoir, par exemple, un compilateur qui traduit du C soit en code machine x86, soit en code machine pour un CPU ARM, soit pour un CPU POWERPC, etc. [[File:Compiler design.svg|centre|vignette|upright=3|Compiler design]] Pour l'interprétation, l'usage d'un ''bytecode'' a d'autres avantages. Pour l'exploiter, il faut distribuer non pas le code source, mais le ''bytecode''. En clair, l'interpréteur ne prend pas en entrée le code source, mais du ''bytecode'' déjà compilé. La traduction est alors beaucoup plus simple, car le code source a déjà été partiellement compilé, par la traduction en ''bytecode''. Interpréter du code source est en effet assez compliqué : il faut effectuer des étapes d'analyse lexicale, sémantique, et bien d'autres. Avec du ''bytecode'', ces étapes ont été réalisées lors de la compilation du ''bytecode'', l'interpréteur a alors peut de choses à faire. L'interprétation du ''bytecode'' se fait instruction par instruction, au niveau du ''bytecode''. Une instruction du ''bytecode'' est traduite en une instruction machine équivalente, qui est exécutée. Si l'instruction est un peu complexe, il exécute une fonction/procédure qui fait la même chose. Pour résumer, le ''bytecode'' est ensuite traduit à la volée et exécuté instruction par instruction par un logiciel appelé l'interpréteur. [[File:Control table.png|centre|vignette|upright=2|Control table]] Le langage intermédiaire peut être vu comme l'assembleur d'un processeur, n'existe pas forcément dans la réalité, mais dont le jeu d'instruction est décrit en détail. Le processeur en question est appelé une '''machine abstraite''', ou encore une ''machine virtuelle''. Nous utiliserons le terme de machine abstraite dans ce qui suit. La machine abstraite n'est pas la même suivant que l'on cible la compilation ou l'interprétation/JIT. Pour l'interprétation/JIT, la machine abstraite est souvent une machine à pile, car cela simplifie grandement la traduction en code machine final. L'interprétation demande que la traduction du ''bytecode'' en code machine soit la plus simple possible. Et cela demande de prendre en compte pas mal de détails. Par exemple, les différents jeux d'instruction existants n'ont pas le même nombre de registres, ce qui pose problème lors de la traduction du ''bytecode''. Lors de la transformation en code machine, un algorithme d'allocation de registres se débrouille pour traduire le code intermédiaire en code qui utilise un nombre limité de registres. Pour améliorer cette allocation de registres, il y a deux solutions niveau ''bytecode'' : soit utiliser un nombre illimité de registres, soit utiliser une machine à pile. Pour les machines à pile, il existe un algorithme simple et rapide pour traduire un code écrit pour une machine à pile en un code écrit pour un processeur avec des registres, qui se débrouille pas trop mal pour allouer efficacement les registres. C'est un avantage assez important pour les langages interprétés. ==Les processeurs qui exécutent du bytecode== Le ''bytecode'' est un code machine, ce qui signifie qu'il peut en théorie s'exécuter sur un processeur qui implémente le jeu d’instruction associé. Si le jeu d'instruction d'un ''bytecode'' est souvent une description censée être fictive, elle n'en reste pas moins un jeu d'instruction et des caractéristiques précises, qu'on peut l'implémenter en matériel ! Néanmoins, tous les ''bytecode'' ne sont pas égaux de ce point de vue. Certains sont faciles à implémenter en matériel, d'autres non. Les ''bytecodes'' implémentés en matériel sont ceux dont les machines abstraites sont des machines à pile. Un premier exemple est celui des processeurs Pascal MicroEngine, qui exécutaient directement le bytecode du langage Pascal, le fameux UCSD P-code. Un second exemple est la '''machine SECD''', qui sert de langage intermédiaire pour certains compilateurs de langages fonctionnels. Elle a été implémentée en matériel par plusieurs équipes, notamment par les chercheurs de l'université de Calgary en 1989. Dans le même genre, quelques processeurs simples étaient capables d’exécuter directement le ''bytecode'' du langage FORTH. Le FORTH, un des premiers langages à pile de haut niveau, possède de nombreuses implémentations hardware et est un des rares langages de haut niveau dont le ''bytecode'' a été utilisé comme langage machine sur certains processeurs. Par exemple, on peut citer le processeur FC16, capable d’exécuter nativement du ''bytecode'' FORTH. ===Les processeurs Java=== Le cas le plus impressionnant est celui de la machine virtuelle Java, qui est un design de processeur comme un autre. En temps normal, le ''bytecode'' Java est compilé ou interprété, mais certains processeurs exécutaient du ''bytecode'' Java directement, sans interprétation ni compilation. Ils sont appelés des '''processeurs Java'''. On peut citer les processeurs ARM disposant de l'extension Jazelle, du JEMCore et du aJ-100 de aJile Systems, le picoJava II de Sun, et quelques autres. Il y a aussi eu quelques projets de recherche ou processeurs open source, comme Komodo, jamuth, le ''Java Optimized Processor'' et quelques autres projets du même genre. L'implémentation de la machine virtuelle Java n'était cependant pas complète. Quelques instructions complexes n'étaient pas gérées par le processeur et devaient être émulées en logiciel. Mais la grosse majorité des instructions l'était. Les instructions non-supportées étaient émulées avec la méthode ''trap and emulate'', à savoir qu'un opcode inconnu déclenchait une exception matérielle ''opcode invalide'', dont la routine pouvait émuler les instructions qui doivent l'être. Le processeur aJ-100 de aJile est capable d'exécuter la quasi-totalité des instructions de la JVM Java, seules deux instructions faisant exception. Les '''accélérateurs Java''' sont une solution intermédiaire, qui permet de traduire à la volée du ''bytecode'' Java en instructions RISC, exécutées sur un autre processeur. Il s'agit formellement de traduction binaire, mais je préfère en parler ici. L'idée est de combiner un processeur RISC avec un coprocesseur Java. Le coprocesseur lit le ''bytecode'' Java et le traduit en instructions RISC, ces dernières étant exécutées sur le processeur RISC. Le JA108 de Nozomi était un coprocesseur de ce genre, au même titre que le coprocesseur JSTAR de JEDI. L'extension Jazelle de certains CPU ARM fonctionnait vraisemblablement sur un principe similaire, sauf que le coprocesseur était intégré dans le processeur ARM, entre le cache d'instruction et le décodeur d'instructions ARM. L'intérêt des processeurs Java n'est pas qu'une question de performance. Certes, ils permettent d’exécuter plus vite les programmes compilés en ''bytecode'', comme des programmes Java pour la JVM Java. Mais l'intérêt est aussi de faire des économies de mémoire RAM et ROM. Je rappelle que la machine virtuelle Java est une machine à pile, ce qui fait que sa densité de code est excellente. Les programmes en ''bytecode'' Java sont donc très petits, et prennent peu de place en mémoire ROM. De plus, utiliser un processeur Java permet de se passer d'interpréteur ou de JIT Java, ce qui économise encore plus de mémoire. ===Transformer un processeur RISC en machine à pile=== La plupart des processeurs Java utilisent en interne une architecture à registre généraux, qui émule une machine à registre en microcode. Quelques registres sont réservés pour l'état de la machine virtuelle, avec au minimum un registre réservé pour le pointeur de pile, un autre pour un pointeur vers un ''pool'' de constantes, et quelques autres. D'autres registres servent à mémoriser le haut de la pile, à savoir le sommet de la pile et quelques opérandes situées juste en-dessous. Par exemple, le processeur picoJava II gardait les 64 opérandes au sommet de la pile dans 64 registres généraux, séparés des registres pour les pointeurs et l'état de la JVM. Une optimisation possible, envisagée sur les processeurs picoJava, était une sorte de macro-fusion sous stéroïde. L'idée était de fusionner certaines séries d'instructions Java en une seule instruction machine. Typiquement, pour faire une opération arithmétique, une machine à pile empile deux opérandes et exécute une opération, ce qui prend trois instructions machine. La spécification picoJava 1 a proposé d'envoyer directement la seconde opérande en entrée de l'ALU, ce qui fusionnait l'empilement de la seconde opérande et l'opération arithmétique. Le processeur picoJava II va plus loin et fusionne les trois instructions en une seule instruction RISC, agissant sur des registres. Mais la majorité des processeurs Java ne faisaient pas cela, car cela complexifie grandement le décodeur. Les processeurs ''bigfoot'' de DCT utilisaient un système différent pour émuler une pile avec des registres. Leur banc de registre de 64 bits était coupé en deux : les 16 premiers registres étaient utilisés pour la pile, les 48 suivants étaient utilisés pour autre chose. L'idée est que le registre 0 mémorise un emplacement vide, le registre 1 contient le sommet de la pile, le registre 2 l'opérande sous le sommet de la pile, et ainsi de suite. Les 15 premières opérandes de la pile sont donc mémorisées dans les registres. Les instructions écrivent leur résultat dans le registre adéquat, puis tous les registres sont décalés pour que le résultat passe dans le registre R1. Par exemple, les instructions PUSH chargent une opérande dans le registre 0, puis tous les registres sont décalés de un rang, pour que l'opérande chargée soit dans le registre 1. Le registre 0 devient alors le registre numéro 1, le registre 1 devient le 2, et ainsi de suite. Par contre, les opérations arithmétiques font autrement. Une instruction d'addition, par exemple, lit les opérandes dans le registre 1 et 2, puis place son résultat dans le registre 2 (les deux opérandes sont dépilées). Puis, les registres sont décalés d'un rang pour que ce registre 2 devienne le 1. Les registres ne sont en réalité pas décalés, il n'y a pas de transferts entre registres. A la place, les 16 premiers registres utilisaient une forme limitée de renommage de registres, similaire au banc de registre tournant utilisé sur les processeurs EPIC comme l'Itanium. Les numéros de registres sont renommés, décalés, à chaque fois qu'on empile ou dépile une opérande. Pour faire le renommage de registres, processeur contient un ''register stack counter'' de 4 bits, qui est incrémenté à chaque fois qu'on empile une donnée, et est décrémenté quand on dépile une donnée. Quand on veut accéder à un opérande, le numéro de registre architectural associé est traduit en numéro de registre physique, en additionnant le ''register stack counter'' (modulo 16, pour rester sur 4 bits). ==L'accélération de l'interprétation/JIT : le ''Thumb-EE'' d'ARM== Quelques processeurs ont ajouté des instructions pour faciliter le travail des interpréteurs/JIT. Par exemple, en 2005, ARM a ajouté le mode ''Thumb Execution Environment'', qui faisait cela. Il reprenait le jeu d'instruction compact ''thumb'' et ajoutait quelques instructions et modifiait le comportement d'instructions existantes. Le jeu d'instruction ''Thumb-EE'' a été ajouté sur les CPU ARM en 2005, mais a été déprécié en 2011, par manque d'utilité. Mais la tentative mérite qu'on s'attarde dessus. Les CPU de gamme M, déstinés à l'embarqué, n'ont jamais supporté ''Thumb-EE'', vu que de tels processeurs ne sont pas conçus pour exécuter du code interprété/JIT. Le mode ''Thumb-EE'' est un mode d'exécution, séparé de l'ARM normal. Ainsi, une instruction ''Thumb-EE'' et une instruction ARM peuvent avoir le même encodage en binaire, mais se comporter différemment. Le processeur est à tout moment soit en mode ''Thumb-EE'', soit en mode ARM normal, ce qui précise comment ces instructions doivent se comporter. Des instructions ont été ajoutées par ''Thumb-EE'', pour qu'il fasse son travail. L'entrée et la sortie du mode ''Thumb-EE'' se fait avec deux instructions : ENTERX et LEAVEX. Une fois en mode ''Thumb-EE'', le décodage des instructions ''thumb'' se fait avec les règles du ''Thumb-EE'', et non celles du ''thumb'' normal. Une première différence entre ''thumb'' normal et ''Thumb-EE'' est l'ajout de ce qui s'appelle un ''null check'' pour les instructions mémoire. Le ''null check'' est utilisé pour les instructions en mode d'adressage "base + indice" ou "Base + décalage", ou tout autre mode d'adressage avec un registre de base utilisés dans des calculs d'adresse. L'idée est de vérifier si l'adresse de base vaut zéro ou non. Si c'est le cas, le processeur lève une exception matérielle, qui est traitée par l'interprétation ou le compilateur JIT. Une telle situation est en effet signe d'une erreur d'adressage mémoire, qui doit être traitée par l'interpréteur. Typiquement, c'est signe qu'un pointeur n'a pas bien été initialisé, ce qui peut arriver avec du code interprété ou JIT. Les interpréteurs/JIT traitent généralement la situation en ajoutant un test avant toute instruction mémoire, pour vérifier si le pointeur accédé vaut zéro ou non. Avec ''Thumb-EE'', pas besoin d'ajouter les tests en questions : ils sont réalisés automatiquement lors de chaque instruction mémoire. : Les registres pour la pile, comme le pointeur de pile et le pointeur de ''frame'', sont aussi concernés. L'instruction CHKA vérifie que les accès à un tableau ne débordent pas en dehors du tableau en question. Par exemple, pour un tableau de 1024 éléments, elle vérifie si l'indice est compris entre 0 et 1024. Pour cela, elle vérifie si l'indice de tableau est bien dans l'intervalle adéquat et lève une exception matérielle si ce n'est pas le cas. Son utilité se comprend quand on sait que tous les accès à un tableau sont vérifiés par l'interpréteur. L'interpréteur est censé ajouter des instructions pour vérifier les indices, à savoir deux ou trois branchements. Avec CHKA, le test se fait en une seule instruction. D'autres modifications mineures de l'encodage des instructions sont aussi présentes, ainsi que des modifications pour les instructions LOAD/STORE. Précisément, dans les modes "base + indice" et "base + décalage", le décalage et l'indice subissent maintenant des décalages. L'idée est de simplifier le calcul d'adresse. De quoi économiser quelques instructions lors des calculs d'adresse, ce qui facilite le travail de l'interpréteur/JIT. Et des instructions LOAD/STORE précises voient leur comportement modifié pour faciliter la gestion de la pile de Java ou l'exécution des méthodes locales. Pour faciliter le travail de l'interpréteur, deux instructions HBP et HBLP ont été ajoutées. Elles branchent vers une fonction qui gère les exceptions logicielles (une fonctionnalité présente dans de nombreux langages, comme Java). La première effectue un branchement vers la fonction en question, la seconde sauvegarde l'adresse de retour avant de faire ce branchement. Les deux peuvent brancher vers 256 fonctions pré-déterminées, sans avoir à présenter leur adresse. ==La traduction binaire accélérée par le matériel== Plus haut, nous avons surtout parlé des compilateurs et des interpréteurs. Cependant, nous devons aussi parler de la '''traduction binaire'''. Elle traduit un programme écrit dans un code machine vers un autre code machine. Par exemple, elle traduit un programme compilé pour un CPU x86 vers un code machine ARM. En général, le système d'exploitation est généralement compilé pour le jeu d'instruction natif, mais il exécute des applications prévues pour le x86. Les applications sont traduites par le système d'exploitation, avant d'exécuter le code traduit. : Nous parlerons dans la suite de code machine source et de code machine cible, pour parler respectivement du code à traduire et du code obtenu après traduction. Idem avec d'autres termes comme architecture cible/source, ou jeu d'instruction cible/source. La traduction binaire est surtout utilisée pour des questions d'émulation ou de compatibilité. Par exemple, lorsque les Macintosh sont passés de processeurs Power PC vers des processeurs x86, le système d'exploitation Mac OS utilisait la traduction binaire pour convertir les anciennes applications Power PC vers du code x86. Les utilisateurs n'y ont vu que du feu. Le système de traduction binaire était appelé Rosetta 1. Par la suite, lors de la transition de processeurs x86 vers des processeurs Apple, Rosetta 2 a vu le jour. La traduction binaire a surtout été utilisée pour traduire du code x86 vers un autre jeu d'instruction. Il faut dire que le x86 est le jeu d'instruction dominant. De nombreuses entreprises ont eu pour ambition de briser l'hégémonie du x86 sur PC, en remplaçant le x86 par un jeu d'instruction plus performant, tout en gardant une compatibilité maximale. La traduction binaire était la seule solution pratique. Elle était le plus souvent intégralement réalisée en logiciel, comme c'était le cas sur les architectures Itanium avec le ''IA-32 Execution Layer''. ===Les généralités sur la traduction binaire assistée en matériel=== L'architecture source est presque tout le temps une architecture CISC, assez ancienne, qu'on souhaite émuler. Le choix de l'architecture cible se porte souvent sur une architecture VLIW, fort différente de l'architecture source, et ce pour obtenir une économie de matériel conséquente. Pour s'exécuter rapidement, le code traduit doit exploiter le parallélisme d'instruction, à savoir exécuter plusieurs instructions en même temps dans des unités de calcul séparées. Il est possible d'utiliser un processeur superscalaire avec exécution dans le désordre pour cela, mais au prix d'un cout important en transistors. Alors qu'en utilisant un CPU VLIW, c'est le traducteur binaire qui fait tout le travail d'extraction du parallélisme d'instruction. Et après tout, quitte à avoir un traducteur binaire, autant lui refiler le boulot d'optimisation. Compiler du code à la volée est certes assez lourd, mais qu'il existe des algorithmes efficaces pour regrouper des instructions indépendantes dans une seule instruction VLIW. Au passage, le processeur VLIW a plus de registres que le processeur source. Cela permet de faire du renommage de registres directement en logiciel. Le traducteur binaire n'hésite pas à changer les noms de registres entre instructions source et instruction VLIW cible, afin de supprimer des dépendances de données. En conséquence, cela ouvre des opportunités de parallélisme, qui sont exploitées lors du regroupement des opérations en instructions VLIW. Vous pourriez penser que le choix d'un CPU VLIW pour émuler du CISC est tout sauf optimal. Vous devez penser que la traduction binaire est d'autant plus simple que l'architecture source et cibles sont semblables. Dans les faits, ce n'est pas tellement traduire les instructions qui pose problème, mais plus la gestion du registre d'état, des exceptions matérielles, de la mémoire virtuelle, la différence entre gros-boutisme et petit-boutisme, de même que des différences d'adressage pour les périphériques. Le premier problème est la gestion des conditions, notamment en présence d'un registre d'état. En pratique, la traduction binaire s'utilise pour traduire du code CISC vers du code RISC ou VLIW. Les architectures source ont donc un registre d'état, qui est mis à jour non seulement par des instructions de test, mais aussi des instructions arithmétiques. L'architecture cible n'a elle pas de registre d'état, mais des registres à prédicats. La traduction de l'un vers l'autre est alors quelque peu compliquée. Et elle est d'autant plus compliquée que le registre d'état est la source de dépendances d'instruction implicites, qui réduisent les performances. Par exemple, si une instruction arithmétique modifie le registre d'état, cela peut impacter l'exécution d'une instruction ultérieure, qui lit ce registre d'état. Pour éliminer ces fausses dépendances, le traducteur binaire doit renommer ce registre en logiciel et détecter les dépendances utiles. Et le compilateur doit gérer les cas où il y a beaucoup de distance entre les deux, voire les cas où l'instruction dépendante n'a pas encore été analysée par le compilateur. Un autre problème est lié à la gestion des exceptions matérielles, et précisément des exceptions précises. Pour rappel, une instruction VLIW regroupe plusieurs opérations, opérations qui correspondraient à une instruction machine sur un CPU pas VLIW. En conséquence, plusieurs instructions du langage machine "source" sont regroupées en une seule instruction VLIW. Et il faut tenir compte du cas où une instruction source lève une exception. Dans le code VLIW, cela signifie qu'une opération lève une exception, et il faut annuler partiellement l'instruction VLIW associée. Et par partiellement, on veut dire que seules les opérations suivantes dans l'ordre du programme source doivent être annulées, puis ré-exécutées. Et c'est un sacré casse-tête ! Les processeurs VLIW utilisent comme solution le mécanisme d'exceptions différées des processeurs EPIC, vu il y a quelques chapitres. Pour résumer, le code est exécuté par blocs d'instruction, délimités par des branchements ou tout autre limite/barrière pertinente dans le code. Un bloc de code est compilé en deux versions : une version rapide sans exceptions matérielles, une version lente qui gère les exceptions précises. La version rapide s'exécute sans exécuter les exceptions. Cependant, les exceptions matérielles sont enregistrées, pour être prises en compte à la toute fin du bloc de code. La version rapide est exécutée en premier et elle mémorise si une exception matérielle a eu lieu. Puis une instruction vérifie si une exception a eu lieu et décide quoi faire. Si aucune exception n'a eu lieu, elle passe à la suite du programme, les résultats du bloc de code sont définitivement acceptés. Mais si une exception a eu lieu, tout est annulé. Le processeur est remis dans l'état initial, puis le code est ré-exécuté instruction par instruction de manière à gérer l'exception correctement. ===La traduction par pré-décodage=== Une première solution serait de faire la traduction binaire dans le cache d'instruction. Lors d'un défaut de cache, le code chargé depuis la RAM est traduit en code machine cible. Et c'est ce code machine cible qui est mémorisé dans le cache d'instruction et exécuté par le processeur. La technique marche sur le papier et n'est qu'une amélioration des techniques de pré-décodage vues il y a quelques chapitres. Cependant, le code obtenu est une traduction assez basique, qui n'incorpore pas d'optimisations dignes de ce nom. De plus, elle gère mal le cas où la taille du code source et cible sont potentiellement très différentes. Concrètement, elle est surtout utile pour traduire du code d'un processeur RISC vers un autre processeur RISC, dont les tailles d'instructions sont similaires. Elle ne permet pas d'émuler plusieurs jeux d'instructions différents, le cout en matériel (un circuit de pré-décodage par jeu d'instruction) serait trop important. ===Le projet DAISY d'IBM=== De très rares processeurs étaient conçus pour accélérer cette traduction binaire, afin de garder de bonnes performances. Les premiers à avoir étudié l'idée étaient IBM, avec leur projet DAISY (''Dynamically Architected Instruction Set from Yorktown''). Le projet de base était de convertir à la volée du code compilé pour des CPU Power PC, vers du code VLIW. Les chercheurs d'IBM avaient développé un algorithme de traduction dynamique efficace, ainsi qu'un processeur VLIW disposant d'optimisations spécifiques à la traduction binaire dynamique, à la volée. De nombreuses idées de ce projet ont été reprises ou re-découvertes par la société Transmetta, avec ses processeurs Crusoe et Efficieon, puis par NVIDIA avec son projet Denver. Les ingénieurs de ce projet ont étudié la possibilité d'émuler plusieurs jeux d'instructions différents sur un même processeur, notamment le s390 d'IBM et le x86. En théorie, cela demande juste d'avoir plusieurs programmes de traduction binaire : un pour le x86, un autre pour le s390, éventuellement un autre pour le Power PC. Mais cela ne s'est pas concrétisé. Mais les ingénieurs ont étudié quelle pourrait être l'architecture VLIW idéale pour ça. Par exemple, il fallait des additions 3-opérandes pour simplifier les calculs d'adresse. Une difficulté était la gestion du registre d'état, dont les bits ne sont pas mis à jour de la même manière sur le x86 et le s390 ou le Power PC. La différence entre gros-boutisme et petit-boutisme était aussi un problème, de même que des différences d'adressage pour les périphériques. Pour que la traduction binaire soit efficace, le processeur VLIW intègre diverses optimisations, comme les exceptions différées et les branchements multi-voies. La principale est la suivante : seul le code des boucles ou fonctions exécutées fréquemment est traduit en code VLIW, le reste du code est interprété. En effet, traduire du code binaire prend plus de temps qu'une simple interprétation. Pour du code qui ne sera exécutée qu'une seule fois, il est plus rapide d'utiliser l'interprétation. Par contre, pour du code exécuté beaucoup de fois, le cout de la traduction binaire est amorti, dilué sur N exécutions, compensé par le gain en temps d'exécution de ce code traduit. Au final, cela permet de ne traduire que le code qui le mérite. La détection du code fréquemment exécuté est réalisée dans le cœur VLIW. L'unité de branchement mémorise les derniers branchements rencontrés et le nombre de fois qu'ils ont été exécutés. Si ils ont été exécutés un certain nombre de fois, l'unité de branchement lève une exception matérielle, qui invoque le traducteur binaire. La routine de cette exception est le traducteur binaire proprement dit. En clair, la traduction binaire est démarrée quand le processeur détecte qu'une fonction a été exécutée plus de N fois, via une exception matérielle dédiée. La mesure du nombre d'exécution d'un branchement se fait dans l'unité de calcul dédiée aux branchements, avec l'aide d'une sorte de ''branch adress buffer'' modifié. Pour rappel, le ''branch adress buffer'' mémorise le ''Program counter'' de chaque branchement récemment rencontré. Ici, chaque entrée du ''branch adress buffer'' est associée à un compteur incrémenté à chaque exécution du branchement. ===Les processeurs Crusoe et Efficieon de Transmetta=== Les processeurs Crusoe Et Efficeron sont deux processeurs VLIW produits par la société Transmetta, une société californienne rachetée par NVIDIA. Ils étaient conçus pour exécuter spécifiquement des programmes x86, système d'exploitation inclus. L'idée derrière ce projet était d'exécuter du code x86 sans que les applications, ni même le système d'exploitation et le BIOS soient au courant ! Les processeurs Transmetta étaient en réalité conçus pour exécuter un programme unique, le ''Code Morphing Software'' (CMS), qui traduisait le code x86 en code VLIW. Transmetta ne voulait pas que son processeur VLIW soit exploité directement, sans le CMS. Il n'avait pas rendu public de compilateur pour traduire du C vers du code VLIW, il n'avait pas donné la documentation du jeu d'instruction VLIW. Des efforts de rétro-ingénieurie, documentés sur le site realworldtech, ont cependant permis de comprendre comment étaient encodées les instructions du processeur Crusoe. Comme pour le projet Daisy, le ''Code Morphing Software'' utilisait à la fois interprétation et traduction binaire, selon les besoins. Les instructions sont interprétées lors de leurs premières exécutions, mais le CMS bascule sur de la traduction en code VLIW après un certain nombre d'exécution. Ainsi, les boucles souvent exécutées sont traduites en code VLIW, alors que le reste du code est interprété. Le choix du code à traduire est le fait du CMS, il utilise des heuristiques complexes pour, qui ne sont pas connues dans le détail. Le ''Code Morphing Software'' est mémorisé dans une EEPROM, ce qui en fait un ''firmware'' situé en dessous du BIOS. Le CMS démarre ensuite le BIOS, qui lui-même démarre le système d'exploitation, qui lui-même démarre les pilotes de périphériques et les programmes. Le CMS se réserve les 16 premiers mébioctets de l'espace d'adressage. L'EEPROM du CMS est mappée dedans, mais n'en utilise que la moitié. Le reste est utilisé comme cache, pour mémoriser le code VLIW traduit par le CMS. Les deux processeurs intégraient aussi deux mémoires SRAM, utilisées par le CMS, appelées la ''local program memory'' (LPM) et la ''local data memory'' (LDM). La première contient du code qui gére les interruptions, la mémoire virtuelle, les exceptions matérielles, les problèmes d'alignement mémoire, et quelques fonctions très fréquemment utilisées par le CMS. La seconde est de la mémoire RAM utilisée par ces fonctions de la ''local program memory''. Les mémoires caches L1 et L2 sont séparées de ces deux mémoires. Le processeur Crusoe et Efficieon étaient des processeurs VLIW très simples. Ils n'avaient même pas de MMU, ni de fonctionnalités importantes sur les CPU x86. Pour Crusoe, les instructions VLIW étaient encodées sur 64 ou 128 bits, et regroupaient entre 2 à 4 opérations. Les instructions LOAD/STORE ne supportaient pas d'adressage indicé, juste de l'adressage indirect à registre. Crusoe avait 5 unités de calcul : deux ALU entières, une unité LOAD/STORE, une unité de branchement, une FPU. Efficieon doublait le nombre d'ALU entière et d'unité LOAD/STORE, les instructions VLIW passaient à 256 bits. Le processeur Crusoe contient 160 registres, dont 64 registres généraux, 32 registres flottants. Les registres généraux font 32 bits, ce qui est cohérent avec le fait que les CPU x86 de l'époque étaient des processeurs 32 bits. Quant aux registres flottants, ils faisaient 80 bits, ce qui colle avec la taille des registres flottants de la FPU x87 utilisée à l'époque. Sur les 64 registres généraux, seuls 48 étaient réellement utilisable pour mémoriser des opérandes. Une partie des registres généraux étaient utilisés pour la gestion de la pile, d'autres pour mémoriser l'état du CPU x86 émulé, un registre était un registre zéro non accesible en écriture. La gestion des exceptions est optimisée avec un système d'exceptions différées, le même que celui décrit plus haut. Le processeur utilise un système similaire, pour effectuer des lectures anticipées, qui a été expliqué dans le chapitre sur les processeurs VLIW/EPIC. Sauf qu'il s'agit là d'une optimisation pour gagner en performances en général, pas une aide utile pour gérer des exceptions précises ou toute autre subtilité de la traduction binaire. Pour gérer les exceptions différées et les lectures anticipées, le processeur mémorise l'état du processeur avant de démarrer un bloc de code. Pour cela, le processeur contient des ''shadow registers'', 48 registres entiers et 16 registres flottants. Vous remarquerez qu'il y a autant de registres entiers que de ''shadow registers'' entiers. Les registres entiers/flottants sont mémorisés dans les ''shadow registers'' avant d'exécuter un bloc de code, afin que le processeur puisse revenir à l'état de base. Une autre fonctionnalité liée est le ''gated store buffer''. Le principe est d'éviter toute écriture en mémoire RAM, tant que le code VLIW n'a pas émis d'instruction ''commit''. L'idée est que les écritures sont conservées dans la ''store queue'' de l'unité LOAD/STORE. Pour rappel, la ''store queue'' met en attente les écritures soit tant que les écritures ne sont pas terminées, soit tant que la RAM est occupée. L'idée est que les écritures sont accumulées dans la ''store queue'' tant que l'instruction ''commit'' n'a pas eu lieu. Le ''store queue'' est "déversé dans la RAM" seulement quand l'instruction ''commit'' s'exécute. ===Les CPU NVIDIA Denver=== Nvidia a tenté quelque chose de similaire avec son projet Denver. NVIDIA a racheté l'entreprise Transmetta, ses brevets, et a tenté de refaire la même chose. Le projet initial était de traduire du code x86 vers un jeu d'instruction VLIW propriétaire, comme l'a fait Transmetta. Mais NVIDIA n'a pas réussi à acquérir la licence du jeu d'instruction x86 et s'est rabattu sur le jeu d'instruction ARM. Le processeur né de ce projet est le Tegra K1-64 CPU. Il s'agit d'un processeur composé d'un cœur VLIW, couplé à deux décodeurs d'instructions ARM. Les deux décodeurs ARM traduisent une paire d'instructions ARM consécutives en instructions VLIW, pour les exécuter sur le cœur VLIW. Le CPU peut fonctionner selon deux modes : ARM et VLIW. En somme, le processeur supporte deux jeux d'instruction : le jeu d'instruction ARM, et un jeu d'instruction VLIW. En mode ARM, les instructions ARM sont chargées depuis le cache, traduites en VLIW par les décodeurs ARM, puis exécutées par le cœur VLIW. En mode optimisé, le CPU exécute des instructions VLIW chargées depuis le cache d'instruction, les décodeurs ne sont pas utilisés. : Le CPU supporte en réalité deux jeux d'instruction ARM : ARM8 et 7. Mais ils correspondent au même mode pour le CPU. Un point important est que le processeur peut changer très rapidement de mode, en à peine quelques cycles d'horloges. On n'est clairement pas dans le cas des CPU x86, qui mettent des plombes pour passer du mode 32 à 64 bits et inversement. La commutation est tellement rapide qu'on peut considérer que le processeur supporte deux jeux d'instruction simultanément. Et le support simultané de deux jeux d'instruction facilite l'implémentation de la traduction binaire. Encore une fois, inutile de traduire du code qui ne sera exécutée qu'une seule fois, mieux vaut l'exécuter directement dans le mode ARM. Surtout que le processeur supporte un mode ARM qui permet d'exécuter du code ARM sans perte de performance. Par contre, le code fréquemment utilisé, notamment dans des boucles critiques, est traduit en VLIW. Au final, les performances sont optimisées, en limitant le travail du logiciel traducteur binaire. La détection du code fréquemment exécuté se fait différemment que pour les processeurs Transmetta et Daisy. L'unité de branchement ne détecte pas directement le code beaucoup exécuté, même si elle a un rôle à jouer. A la place, la détection est réalisée en logiciel, par un ''thread'' dédié, qui s'exécute sur un cœur séparé. Il détecte le code fréquemment utilisé en regardent les branchements exécutés récemment. Pour cela, le processeur mémorise l'historique des branchements pris récemment et copie cette historique dans une mémoire tampon dédiée, partagée entre tous les cœurs. Vu qu'elle est partagée, le ''thread'' de détection a accès à l'historique sans pertes de performances, seule la copie de l'historique dans ce tampon a un cout en performance, pas son partage. Le code VLIW obtenu après traduction binaire est mémorisé dans la mémoire RAM, dans une portion spécialement réservée pour. Elle fait 128 mébioctets, et est appelée le '''cache d'optimisation''' par NVIDIA. Le cache d'optimisation est protégé en écriture et seul le traducteur binaire peut écrire dedans. Au passage, le traducteur binaire est du code VLIW, ce qui fait qu'il est placé dans le cache d'optimisation. Le processeur détecte automatiquement quand une fonction pour laquelle le code compilé équivalent est disponible. Pour cela, l'unité de chargement contient une table de correspondance entre l'adresse de la fonction ARM, et l'adresse de son équivalent compilé. Quand une fonction est appelée, l'unité de chargement regarde l'adresse de destination du branchement. Si l'adresse est dans cette table, elle récupère l'adresse de la fonction compilée et branche vers celle-ci. Le processeur dispose d'un cache d'instruction de 128 kibioctets, ce qui est très important, mais nécessaire vu la taille des instructions VLIW. Le coeur VLIW dispose de ses propres décodeurs, d'un ''scoreboard'', de 7 unités de calcul et de registres. Pour les unités de calcul, il a deux FPU, deux ALU, une unité de branchement, et deux unités LOAD-STORE faisant aussi ALU. Un point important est que le CPU dispose de 64 registres entiers et de 64 registres flottants. C'est deux fois plus que supporte le jeu d'instruction ARM. En mode ARM, seule la moitié des registres est utilisée. Mais en mode optimisé, le traducteur binaire utilise bien les 64 registres, grâce à une sorte de renommage de registres logicielle. Pour simplifier la traduction binaire, le processeur supporte les techniques vues précédemment. Le processeur intègre un ''gated store buffer'' similaire à celui des processeurs Transmetta. Les techniques d'exceptions différées sont aussi supportées, comme sur les processeurs Transmetta. Il a aussi une unité de préchargement décrite par NVIDIA comme agressive, avec support du préchargement de type anticipé (''runahead''). ===L'émulation de la mémoire virtuelle=== Pour finir, il faut parler de la mémoire virtuelle avec la traduction binaire. Sur le processeur source, le processeur gère à la fois des adresses physiques et virtuelles et sa MMU fait la conversion entre les deux. Sur le processeur VLIW, la MMU est simulée par le programme de traduction binaire, partiellement ou totalement. L'implémentation exacte varie suivant que l'on parle du projet Daisy, des processeurs Transmetta ou de Denver. Sur les processeurs Transmetta, il n'y a pas de MMU ni de mémoire virtuelle. A la place, le programme de traduction binaire émule la mémoire virtuelle du processeur source. Le processeur VLIW ne gère que des adresses physiques, rien d'autre. L'espace d'adressage physique du processeur VLIW a la même taille que celui du processeur émulé, ici des processeurs x86. Il est vraisemblable que les adresses physiques utilisées par le processeur x86 sont les mêmes que celles du processeur VLIW. Sur le projet Daisy, le processeur VLIW gère la mémoire virtuelle via pagination, comme les processeurs émulés. Ce qui fait qu'il gère un espace d'adressage virtuel et un espace d'adressage physique. Pour éviter toute confusion, nous parlerons d'adresse physique/virtuelle VLIW pour les adresses physiques/virtuelles du processeur VLIW, d'adresse virtuelle/physique source pour celle du jeu d'instruction traduit, à savoir du Power PC ou du s390, éventuellement de l'x86. Il faut alors faire le lien entre adresses physiques source et adresses virtuelles VLIW. Pour cela, rien de plus simple : il y a correspondance parfaite. L'adresse physique source numéro N correspond à l'adresse virtuelle VLIW numéro N. Cependant, cela signifie que tout l'espace d'adressage virtuel VLIW serait utilisé par le code à traduire. En réalité, il faut ajouter de la place pour le code traduit et le programme de traduction binaire. La conséquence est que l'espace d'adressage virtuel VLIW est plus large que l'espace d'adressage source. L'espace d'adressage virtuel VLIW est découpé en trois sections : une section pour le code Power PC à traduire, une autre pour le code traduit en VLIW, et une dernière réservée au traducteur binaire. Le programme de traduction est placé dans une ROM mappée en mémoire dans la seconde section. Le reste de la seconde section est réservé à une mémoire RAM utilisée par le programme de traduction. [[File:Mémoire virtuelle sur le projet Daisy.png|centre|vignette|upright=1.5|Mémoire virtuelle sur le projet Daisy]] Il faut noter que chaque page physique source, une fois traduite, correspond à N pages virtuelles VLIW. La raison est que le code traduit prend plus de place que le code machine originel. En conséquence, on doit utiliser une page finale plus grande. Pour le reste, des pages contiguës en mémoire physique source sont elles aussi contiguës en mémoire virtuelle VLIW. Le calcul d'adresse est donc simple : l'adresse physique source est multipliée par N, puis on ajoute l'adresse de base à laquelle commence la section pour le code traduit. ==La traduction binaire sur le Burroughs B1700== Nous venons de voir comment accélérer en matériel la traduction binaire, avec des techniques bien spécifiques. Le Burroughs B1700 a procédé autrement et il est intéressant d'étudier en détail son architecture. Son architecture est décrite dans le livre "Interpreting Machines : Architecture and Programming of the Bl700/Bl800 Series", écrit par Elliott I. Organick et James A. Hinds. Une partie de ce qui vfa suivre est un très court résumé de ce livre. ===Le Burroughs B1700 : architecture externe=== Il disposait de 4 registres généraux, nommés X, Y, T et L, tous de 24 bits. Les registres T et L étaient découpés en 6 sous-registres de 4 bits, nommés LA LB LC LD LE et LF pour le registre L, et TA TB TC TD TE et TF pour le registre T. Ils étaient adressables comme n'importe quel registre. Un point important est que le registre T avait des possibilités d’extraction spécifiques. Il était possible d'extrait N bits, placés n'importe où dans ce registre, et de les copier dans un autre registre. Nous verrons pourquoi cette possibilité est intéressante et ne sert que pour le registre T. Les registres X et Y étaient reliés à une ALU de 24 bits capable d'effectuer des opérations arithmétiques basiques. Fait étonnant, l'ALU faisait tous les calculs en même temps et fournissait ses résultats dans 7 registres : un registre SUM pour l'addition, un registre DIFFERENCE pour la soustraction, et un registre par opération bit à bit. Les registres pour les opérations bit à bit sont : CMPX et CMPY pour l'opération NOT sur chaque opérande, XANY pour le ET bit à bit, XEOY pour le XOR, XORY pour le OU logique. Les additions et soustractions pouvaient se faire sur des opérandes codés en binaire ou en BCD, suivant le code opération utilisé. A cela, il fallait ajouter deux registres de "résultat" MSKX et MSKY. Ils fournissaient un masque dépendant de la taille du résultat en bits. Cela permettait de gérer des données de 0, 4, 8, 12, 16, 20, et 24 bits, alors que l'ALU faisait 24 bits. Et cela n'était pas limité à 16 ou 8 bits, mais n'importe quelle taille inférieure ou égale à 24 bits était supportée, même des tailles non-conventionnelles comme des résultats codés sur 3 bits, 9 bits, 12 bits, etc. L'ALU prenait en opérande les registres X et Y, mais aussi un registre CYF pour la retenue entrante. En sortie, il fournissait aussi deux retenue sortantes : une pour l'addition et une autre pour la soustraction. Elles étaient mémorisées dans les registres CYL et CYD, respectivement. L'ALU fournissait aussi 12 conditions, qui alimentait un registre d'état. Le registre d'état était segmenté en trois registres appelés XYST, XYCN et BICN. Le processeur contenait de nombreux autres registres de 4 bits, dont les sous-registres de T et L. En tout, il y a 27 registres de 4 bits adressables ! Et le processeur disposait d'une ALU 4 bits pour manipuler ces registres de 4 bits ! Elle est capable de faire des MOV entre registres de 4 bits, des opérations bit à bit, et peut aussi tester si un bit vaut 0 ou 1. La dernière possibilité est très utile pour implémenter les branchements. Pour communiquer avec les entrées-sorties, il disposait de deux registres CMND et DATA : CMND pour envoyer une commande à une entrée-sortie, DATA pour lire ou écrire une donnée. Pour la mémoire, il avait deux registres séparés : READ et WRITE. READ contient la donnée lue lors d'une lecture,; WRITE est pour une donnée à écrire lors d’une écriture. Il s'agit de registres d’interfaçage mémoire, qui ont été promus au rang de registres architecturaux. Les deux registres sont gérés par le microcode, qui est le langage principal de la machine. Les instructions machines configurées font sans, mais le microcode doit y avoir accès facilement. Et vu que le microcode est le langage principal de ce processeur, ces deux registres deviennent architecturaux. ===Le Burroughs B1700 : le mal-nommé ''split-level control store''=== Le Burroughs B1700 avait plusieurs modèles : le B1726, le B1710 et le B1800. Ils se distinguaient par la manière dont le code machine source était traduit par le traducteur binaire. Dans ce qui suit, nous parlerons de traducteur binaire pour parler soit d'un interpréteur pour un langage de programmation de haut niveau, soit pour un logiciel de traduction binaire. Le B1710 plaquait le traducteur binaire en mémoire RAM. Le B1800 faisait pareil, mais avait ajouté un cache dédié au code du traducteur binaire, afin d'améliorer les performances. Le B1726 avait un traducteur binaire scindé en deux : une partie était en mémoire RAM, l'autre était dans un ''local store'' dédié. Le processeur peut exécuter le traducteur binaire soit depuis le ''local store'', soit depuis la mémoire RAM avec une pénalité en termes de performances. La documentation appelle cela un '''''split-level control store''''', mais le terme est trompeur : la documentation qualifie de microcode le langage machine du Burroughs B1700, ce qui est à l'origine de beaucoup de confusions. Le programmeur devait découper le traducteur binaire en segments, certains étant placé dans le ''local store'', les autres étant en mémoire RAM. Mieux que ça, le processeur autorisait d'utiliser la technique de l'''overlaying'' pour le microcode ! Il y avait même une fonction OVERLAY dédiée pour ! OVERLAY avait besoin de trois opérandes : la taille du segment à copier, son adresse source en mémoire RAM, son adresse de destination dans la micro-SRAM. Le ''local store'' mémorisait au maximum 2048 instructions machine, ce qui fait que les 2048 premières "adresses" correspondaient au ''local store''. Les suivantes étaient des adresses de mémoire RAM qui contenaient le reste du traducteur binaire. En pratique, l'adresse qui séparait les deux était configurable ! On pouvait limiter la taille du ''local store'' à un multiple de 32 instructions ! Les adresses restantes étaient alors réattribuées à la RAM. Par exemple, on pouvait attribuer 64 adresses au ''local store'', les 2048 - 64 adresses restantes étaient alors attribuées à la mémoire RAM. Un registre limite, nommé TDPM, indiquait quelle était l'adresse de démarcation entre les deux. Il contenait 4 bits, dont la valeur était multipliée par 32, pour obtenir l'adresse limite. L'interpréteur réservait un "segment" en mémoire RAM, pour les données, dont le processeur connaissait la position. En-dehors de ce segment, il n'y a que des instructions machines à traduire, et le traducteur binaire. Le CPU incorporait pour cela deux registres, un pour l'adresse de base du segment de données, un autre pour son adresse de fin. Il incorporait une protection mémoire limitée sur ce segment, à savoir que seul l'interpréteur pouvait lire ou écrire dans ce segment. ===Le code machine du Burroughs B1700=== Le processeur utilise des micro-opérations de 16 bits de long, qui sont copiées dans registre d'instruction M de 16 bits. Fait intéressant, il était possible de faire un OU logique entre l'instruction lue et un registre du processeur, le résultat étant mémorisé dans le registre M. Cela permettait de faire du code automodifiant, pour émuler des modes d'adressage complexe. Cela permettait par exemple de remplacer une adresse dans une micro-opération. L'adresse est alors surimposée avec un OU dans le champ d'adresse, alors que le reste de l'instruction n'est pas modifié. Afin de gérer les interruptions, ces dernières étaient partiellement détectées en logiciel. Les interruptions ne s'exécutaient pas automatiquement, pour émuler correctement les interruptions. Une interruption doit être prise en compte à la toute fin de l'exécution d'une instruction, quand celle-ci a finit son travail. Mais vu qu'ici, les instructions sont émulées avec des séries de micro-opérations, il y a un problème : il est possible d'interrompre une instruction émulée en plein milieu. Pour éviter cela, le processeur ne déclenche pas les interruptions matérielles immédiatement, elles sont mises en attente. Une micro-opération dédiée vérifie si une interruption est en attente, et l'exécute alors cas échéant. La micro-opération est utilisée comme micro-opération finale d'une instruction émulée. ===Une architecture bit-adressable=== Si avoir un microcode réinscriptible est suffisant, quelques choix de ''design'' facilitent l'émulation de jeux d'instructions variés. Par exemple, le CPU Burroughs B1700 avait ajouté une fonctionnalité très importante : il était bit-adressable. En clair, il pouvait adresser la mémoire bit par bit, si nécessaire. L'avantage est que cela permettait d'émuler des architectures dont la taille des registres/''byte''/mots était très différente. Un autre avantage est que cela facilitait la gestion des instructions de taille variable. Détaillons rapidement le premier avantage. Le processeur était un CPU 24 bits, mais il pouvait émuler des processeurs 16 bits, 8 bits, 4 bits, 1 bit, 9 bits, ou toute autre valeur. Pour cela, le processeur lisait des mots de 24 bits et utilisant un ''barrel shifter'' pour sélectionner les bits adéquats. Par exemple, pour un accès de 16 bits, la donnée lue en mémoire était masquée de manière à ne garder que les 16 bits de poids faible. Le ''barrel shifter''/circuit de masquage était placé directement avant le bus mémoire, sur le trajet des données/instructions. Le ''barrel shifter'' était commandé par un registre ''Field Unit'', qui précisait quelle était la taille des données à charger. Le processeur gère un micro-''program counter'' appelé le registre A. Les micro-opérations du processeur font 16 bits de long, et elles sont alignées en RAM sur 16 bits, ce qui fait que les 4 bits de poids fort de ce registre valent 0. Le registre fait 18 bits, mais seuls les 14 bits de poids fort sont utiles. Les adresses mentionnées plus haut, qui adressent les micro-opérations en mémoire RAM, sont des adresses de mots de 16 bits, pas les adresses de bit individuels. ===La traduction binaire est aidée par des registres et instructions dédiées=== Les instructions à décoder n'ont pas de longueur fixe, à savoir qu'elles peuvent faire 16 bit avec tel jeu d'instruction, 8 bits avec un autre, 32 bits sur un autre, etc. Elles peuvent aussi être de longueur fixe ou variable ! Le B1700 intégrait donc un support des instructions de longueur variable, qui était "détourné" pour gérer des jeux d'instruction différents. Notons que vu que l'architecture est bit-adressable, les contraintes d'alignement des instructions disparaissent, ce qui a des avantages qu'on va détailler dans ce qui suit ! L'implémentation des instructions de taille variable ajoutait un registre avant le registre d'instruction. Le registre d'instruction proprement dit était un registre M de 24 bits. Il était précédé par un registre T, dans lequel l'instruction était assemblée, en combinant plusieurs morceaux. Si l'instruction est codée est codée sur moins de 24 bits, le registre T se contente de masquer les bits en trop. Si l'instruction chargée dépasser 24 bits, le chargement se faisait en plusieurs passes, avec plusieurs lectures en mémoire. Dans ce qui suit, on suppose que l'instruction est composée de '''champs''' : l'opcode, les opérandes, les adresses absolues, les constantes immédiates, les numéros de registre, etc. L'idée est que l'instruction est chargée dans le registre T, puis découpée en champs, qui sont copiés dans les registres du processeur ou envoyés au microcode. Je rappelle que le registre T est relié à un ''barrel shifter'', différent au précédent, qui permettait de masquer, décaler et extraire une donnée de ce registre. Et il y a la possibilité d'extraire une suite de bit de ce registre T, pour la copier dans un autre registre. Par exemple, prenons une instruction composée d'un opcode, d'une constante immédiate et d'une adresse immédiate. L'opcode est lu et interprété par le microcode. Le microcode extrait la constante immédiate du registre T et la copie dans le registre X. Ensuite, l'adresse est extraite et envoyée au registre FA, une lecture est démarrée, et l'opérande est copiée dans le registre Y. Enfin, l'instruction est exécutée par le microcode. Le tout prend plusieurs micro-opérations : une première pour lire l'opcode, une seconde pour lire la première opérande, une troisième pour lire la seconde opérande, una quatrième pour tout exécuter. Notons que suivant la taille des champs, on peut en charger plusieurs à la fois. Par exemple, imaginons qu'on ait chargé 24 bits, contenant un opcode de 8 bits et une constante immédiate de 16 bits : pas besoin de faire deux accès mémoire, le registre T est juste découpé en un opcode et une constante. Le registre T est fait pour ce genre d'extraction. ===Le chargement d'un champ=== L'architecture est bit-adressable car elle gére des champs qui ne font pas forcément 8 ou 16 bits. Par exemple, certains jeux d'instructions ont des opcodes codés sur 6 ou 7 bits, voire 9-10 bits. Par exemple, prenons un opcode de 7 bits, suivi par une adresse de 12 bits et une constante immédiate de 15 bits : aucun de ces champs ne sera aligné sur 8/16/24 bits. Utiliser une architecture bit-adressable règle le problème d'alignement. Et en général, les contraintes d'alignement des instructions disparaissent ! Quand on charge une instruction, peut importe qu'elle soit ou non alignée sur 24 bits, elle sera placée dans les bits de poids faible, il suffira de masquer les bits de poids fort inutiles. Par exemple, si on charge une instruction de 16 bits, les 16 bits seront copiés dans les 16 bits de poids faible du registre T, il suffira de masquer l'octet de poids fort. Pas besoin de faire de décalages pour gérer des instructions mal alignées... Pour charger une instruction, il faut connaitre plusieurs informations : l'adresse de son premier bit, sa taille. Pour cela, le B1700 contient deux registres, nommés ''Field Adress'' (FA) et ''Field Length'' (FL), qui indiquent l'adresse du premier bit et la taille de l'instruction à charger. Le premier fait 24 bits, ce qui permet de gérer des adresses de 24 bits. N'oubliez pas que le processeur est bit-adressable, ce qui permet d'adresser 16 Mébi-bits, soit 2 mébioctets. Le registre pour la taille fait lui 16 bits, ce qui permet de gérer des instructions de 65536 bits, soit 8192 octets ! Leur nom commence par ''field'', car le processeur peut gérer des instructions de taille variable et/ou plus longues que 24 bits. Dans ce cas, le chargement de l'instruction se fait champ par champ. Pour donner un exemple, prenons une instruction composée de 20 champs de 12 bits chacun. Les champs sont chargés un par un dans le registre T. Le registre FA est alors initialisé avec l'adresse de l'instruction, il est incrémenté de 12 à chaque cycle. Le registre FL est initialisé avec la taille de l'instruction, soit 20 * 12 = 240 bits, et il est décrémenté de 16 à chaque cycle. Précisons cependant que la taille d'un champ peut être changée d'un cycle à l'autre. Par exemple, le processeur peut charger un opcode de 8 bits, l'interpréter, détecter que l'instruction demande ensuite de charger une adresse de 16 bits. Dans ce cas, le registre FL est immédiatement altéré pour charger 16 bits, au lieu des 8 bits de l'opcode immédiatement précédent. ==Les jeux d'instructions dédiés à un langage de programmation== De rares processeurs sont conçus pour un langage de programmation en particulier. On appelle ces processeurs, conçus pour des besoins particuliers, des '''processeurs dédiés'''. Par exemple, les fameux ''Burrough E-mode'' B5000/B6000/B7000 étaient spécialement conçus pour exécuter de l'ALGOL-60. Leurs cousins B2000/B3000/B4000 étaient eux conçus pour le COBOL. Des langages fonctionnels ont aussi eu droit à leurs processeurs dédiés. Le prolog en est un bel exemple, avec les superordinateurs de 5ème génération qui lui étaient dédié. On peut aussi citer les machines LISP, dédiés au langage LISP, qui datent des années 1970. Elles étaient capables d’exécuter certaines fonctions de base du langage directement dans leurs circuits : elles possédaient notamment un ''garbage collector'' câblé dans ses circuits ainsi que des instructions machines supportant un typage déterminé à l’exécution. Les processeurs dédiés ont eu leur heure de gloire au début de l'informatique, à une époque où les langages de haut niveau venaient d'être inventés. À cette époque, les compilateurs n'étaient pas performants et ne savaient pas bien optimiser le code machine. Il était alors rationnel, pour l'époque, de rapprocher le code machine cible et le langage de programmation de haut niveau. De nombreuses architectures dédiés ont ainsi été inventées, avant que les concepteurs se rendent compte des défauts de cette approche. Les défauts en question ne sont pas nombreux, mais assez simples à comprendre. Premièrement, elles sont très rapides pour un langage de programmation en particulier, mais sont assez mauvaises pour les autres, d'où un problème de "compatibilité". Ajoutons à cela que les langages de programmation peuvent évoluer, devenir de moins en moins populaires/utilisés, ce qui rend la création d'architectures généralistes plus pertinente. Enfin, les architectures dédiées sont évidemment des processeurs CISC, pour implémenter les nombreuses fonctionnalités des langages évolués. Et les défauts des CISC sont assez rédhibitoires à l'heure actuelle. À l'heure actuelle, les algorithmes des compilateurs se sont améliorés et savent nettement mieux utiliser le matériel. Ils produisent du code machine efficace, ce qui rend les architecture dédiées bien moins intéressantes. Si on ajoute les défauts de ces architectures dédiées, par étonnant que les architectures dédiées aient presque disparues. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'accélération matérielle de la virtualisation | prevText=L'accélération matérielle de la virtualisation | next=Le matériel réseau | nextText=Le matériel réseau }} </noinclude> 7ogx07exun5ng4wga0k727r93l7bbvi 764042 764041 2026-04-19T19:59:52Z Mewtow 31375 /* Une architecture bit-adressable */ 764042 wikitext text/x-wiki De nos jours, la majorité des programmeurs programment dans des langages de haut niveau. Il est très rare pour eux d'avoir à utiliser de l'assembleur, et encore moins un langage machine. Les programmes écrits dans un langage de haut niveau sont traduits en langage machine par un logiciel appelé le compilateur. [[File:Compilation.PNG|centre|vignette|upright=2.5|Compilateur : principe.]] Il se trouve que quelques rares processeurs sont conçus pour faciliter le travail du compilateur. Et ce sont ces processeurs que nous allons voir dans ce qui suit. Ils représentent une catégorie de jeux d'instruction à part, qui n'a pas vraiment de nom. ==La compilation et l’interprétation== Un compilateur traduit donc du code source, écrit dans un langage de haut niveau, vers du langage machine. La traduction est rarement directe. En général, le compilateur traduit le code source en assembleur, qui est lui-même traduit en langage machine. L'assembleur est une représentation textuelle du langage machine alors que le code machine est du binaire exécutable par le processeur. La traduction se fait donc en deux étapes, la '''compilation''' proprement dite et l''''assemblage''', réalisées respectivement par un compilateur et un assembleur. Il faut parfois rajouter une troisième phase d'édition des liens, que nous passons volontairement sous silence. {| |[[File:Code C.png|vignette|200px|Le code source, ici en C.]] |[[File:Assem.png|vignette|250px|Le code assembleur.]] |[[File:Binary file - hello world (C programming).png|vignette|150px|Le code machine.]] |} ===L'interprétation et la compilation à la volée=== Une alternative à la compilation est l''''interprétation''', qui transforme le code source en code machine à la volée. Avec l'interprétation, le code source est passé à un logiciel appelé l''''interpréteur''', qui exécute le code source ligne de code par ligne de code. Une ligne de code est traduite en code machine, qui est exécutée, puis l'interpréteur passe à la ligne suivante. [[File:Kaantotulkkaus.png|centre|vignette|upright=3|Compilation versus interprétation.]] Un défaut de l'interprétation est son cout en performance, qui est assez important. Aussi, l'interprétation stricte a évolué vers un hybride entre interprétation et compilation. L'idée est qu'une partie peu importante du code source est interprétée, alors que le code important est compilé juste avant d'être exécuté. Pour le dire autrement, le code source est partiellement compilé à la volée, juste avant son exécution. Aussi, on parle de '''compilation à la volée'''. Le terme en anglais est ''Just In Time compilation'', abrévié en JIT. En soi, le JIT est associé à de l'interprétation, les JIT sont en réalité des hybrides interpréteurs-compilateurs. Un point important est que compiler du code à la volée est assez lourd, cela a un cout en performance. Par contre, le code compilé s'exécute plus vite que du code interprété. Si on exécute le code compilé une seule fois, le cout de la compilation l'emporte sur le gain à l'exécution. Mais le code compilé est réutilisé autant de fois que nécessaire. Le gain à l'exécution est donc multiplié par le nombre d'exécutions, alors que le cout de la compilation est répartit sur plusieurs exécutions, il est amorti. Les portions du code sont choisies de manière à avoir un gain en performance maximal. L'idée générale est que le code exécuté très souvent est compilé, alors que du code exécuté pas souvent est interprété. Typiquement, les boucles les plus souvent exécutées sont compilées, le code exécuté une seule fois est interprété. ===Les langages intermédiaires=== Les compilateurs modernes passent par un '''langage intermédiaire''' pour faire la transformation en code machine. Le compilateur traduit le langage de haut niveau en langage intermédiaire, puis traduit le langage intermédiaire en code machine cible. Le langage intermédiaire est parfois appelé le '''''bytecode''''', et ce terme recouvre aussi le code écrit avec du ''bytecode''. L'interprétation aussi peut passer par l'intermédiaire d'un un ''bytecode'', ce n'est pas limité aux compilateurs proprement dit. {| |[[File:Intermediate representation scheme.png|centre|vignette|upright=1.5|Représentation intermédiaire d'un compilateur.]] |[[File:Code interpreter scheme.png|centre|vignette|upright=1.5|Fonctionnement d'un interpréteur.]] |} Faire ainsi a de nombreux avantages, le principal étant d'avoir un compilateur capable de traduire un langage de haut niveau vers plusieurs jeux d'instructions différents. Cela permet d'avoir, par exemple, un compilateur qui traduit du C soit en code machine x86, soit en code machine pour un CPU ARM, soit pour un CPU POWERPC, etc. [[File:Compiler design.svg|centre|vignette|upright=3|Compiler design]] Pour l'interprétation, l'usage d'un ''bytecode'' a d'autres avantages. Pour l'exploiter, il faut distribuer non pas le code source, mais le ''bytecode''. En clair, l'interpréteur ne prend pas en entrée le code source, mais du ''bytecode'' déjà compilé. La traduction est alors beaucoup plus simple, car le code source a déjà été partiellement compilé, par la traduction en ''bytecode''. Interpréter du code source est en effet assez compliqué : il faut effectuer des étapes d'analyse lexicale, sémantique, et bien d'autres. Avec du ''bytecode'', ces étapes ont été réalisées lors de la compilation du ''bytecode'', l'interpréteur a alors peut de choses à faire. L'interprétation du ''bytecode'' se fait instruction par instruction, au niveau du ''bytecode''. Une instruction du ''bytecode'' est traduite en une instruction machine équivalente, qui est exécutée. Si l'instruction est un peu complexe, il exécute une fonction/procédure qui fait la même chose. Pour résumer, le ''bytecode'' est ensuite traduit à la volée et exécuté instruction par instruction par un logiciel appelé l'interpréteur. [[File:Control table.png|centre|vignette|upright=2|Control table]] Le langage intermédiaire peut être vu comme l'assembleur d'un processeur, n'existe pas forcément dans la réalité, mais dont le jeu d'instruction est décrit en détail. Le processeur en question est appelé une '''machine abstraite''', ou encore une ''machine virtuelle''. Nous utiliserons le terme de machine abstraite dans ce qui suit. La machine abstraite n'est pas la même suivant que l'on cible la compilation ou l'interprétation/JIT. Pour l'interprétation/JIT, la machine abstraite est souvent une machine à pile, car cela simplifie grandement la traduction en code machine final. L'interprétation demande que la traduction du ''bytecode'' en code machine soit la plus simple possible. Et cela demande de prendre en compte pas mal de détails. Par exemple, les différents jeux d'instruction existants n'ont pas le même nombre de registres, ce qui pose problème lors de la traduction du ''bytecode''. Lors de la transformation en code machine, un algorithme d'allocation de registres se débrouille pour traduire le code intermédiaire en code qui utilise un nombre limité de registres. Pour améliorer cette allocation de registres, il y a deux solutions niveau ''bytecode'' : soit utiliser un nombre illimité de registres, soit utiliser une machine à pile. Pour les machines à pile, il existe un algorithme simple et rapide pour traduire un code écrit pour une machine à pile en un code écrit pour un processeur avec des registres, qui se débrouille pas trop mal pour allouer efficacement les registres. C'est un avantage assez important pour les langages interprétés. ==Les processeurs qui exécutent du bytecode== Le ''bytecode'' est un code machine, ce qui signifie qu'il peut en théorie s'exécuter sur un processeur qui implémente le jeu d’instruction associé. Si le jeu d'instruction d'un ''bytecode'' est souvent une description censée être fictive, elle n'en reste pas moins un jeu d'instruction et des caractéristiques précises, qu'on peut l'implémenter en matériel ! Néanmoins, tous les ''bytecode'' ne sont pas égaux de ce point de vue. Certains sont faciles à implémenter en matériel, d'autres non. Les ''bytecodes'' implémentés en matériel sont ceux dont les machines abstraites sont des machines à pile. Un premier exemple est celui des processeurs Pascal MicroEngine, qui exécutaient directement le bytecode du langage Pascal, le fameux UCSD P-code. Un second exemple est la '''machine SECD''', qui sert de langage intermédiaire pour certains compilateurs de langages fonctionnels. Elle a été implémentée en matériel par plusieurs équipes, notamment par les chercheurs de l'université de Calgary en 1989. Dans le même genre, quelques processeurs simples étaient capables d’exécuter directement le ''bytecode'' du langage FORTH. Le FORTH, un des premiers langages à pile de haut niveau, possède de nombreuses implémentations hardware et est un des rares langages de haut niveau dont le ''bytecode'' a été utilisé comme langage machine sur certains processeurs. Par exemple, on peut citer le processeur FC16, capable d’exécuter nativement du ''bytecode'' FORTH. ===Les processeurs Java=== Le cas le plus impressionnant est celui de la machine virtuelle Java, qui est un design de processeur comme un autre. En temps normal, le ''bytecode'' Java est compilé ou interprété, mais certains processeurs exécutaient du ''bytecode'' Java directement, sans interprétation ni compilation. Ils sont appelés des '''processeurs Java'''. On peut citer les processeurs ARM disposant de l'extension Jazelle, du JEMCore et du aJ-100 de aJile Systems, le picoJava II de Sun, et quelques autres. Il y a aussi eu quelques projets de recherche ou processeurs open source, comme Komodo, jamuth, le ''Java Optimized Processor'' et quelques autres projets du même genre. L'implémentation de la machine virtuelle Java n'était cependant pas complète. Quelques instructions complexes n'étaient pas gérées par le processeur et devaient être émulées en logiciel. Mais la grosse majorité des instructions l'était. Les instructions non-supportées étaient émulées avec la méthode ''trap and emulate'', à savoir qu'un opcode inconnu déclenchait une exception matérielle ''opcode invalide'', dont la routine pouvait émuler les instructions qui doivent l'être. Le processeur aJ-100 de aJile est capable d'exécuter la quasi-totalité des instructions de la JVM Java, seules deux instructions faisant exception. Les '''accélérateurs Java''' sont une solution intermédiaire, qui permet de traduire à la volée du ''bytecode'' Java en instructions RISC, exécutées sur un autre processeur. Il s'agit formellement de traduction binaire, mais je préfère en parler ici. L'idée est de combiner un processeur RISC avec un coprocesseur Java. Le coprocesseur lit le ''bytecode'' Java et le traduit en instructions RISC, ces dernières étant exécutées sur le processeur RISC. Le JA108 de Nozomi était un coprocesseur de ce genre, au même titre que le coprocesseur JSTAR de JEDI. L'extension Jazelle de certains CPU ARM fonctionnait vraisemblablement sur un principe similaire, sauf que le coprocesseur était intégré dans le processeur ARM, entre le cache d'instruction et le décodeur d'instructions ARM. L'intérêt des processeurs Java n'est pas qu'une question de performance. Certes, ils permettent d’exécuter plus vite les programmes compilés en ''bytecode'', comme des programmes Java pour la JVM Java. Mais l'intérêt est aussi de faire des économies de mémoire RAM et ROM. Je rappelle que la machine virtuelle Java est une machine à pile, ce qui fait que sa densité de code est excellente. Les programmes en ''bytecode'' Java sont donc très petits, et prennent peu de place en mémoire ROM. De plus, utiliser un processeur Java permet de se passer d'interpréteur ou de JIT Java, ce qui économise encore plus de mémoire. ===Transformer un processeur RISC en machine à pile=== La plupart des processeurs Java utilisent en interne une architecture à registre généraux, qui émule une machine à registre en microcode. Quelques registres sont réservés pour l'état de la machine virtuelle, avec au minimum un registre réservé pour le pointeur de pile, un autre pour un pointeur vers un ''pool'' de constantes, et quelques autres. D'autres registres servent à mémoriser le haut de la pile, à savoir le sommet de la pile et quelques opérandes situées juste en-dessous. Par exemple, le processeur picoJava II gardait les 64 opérandes au sommet de la pile dans 64 registres généraux, séparés des registres pour les pointeurs et l'état de la JVM. Une optimisation possible, envisagée sur les processeurs picoJava, était une sorte de macro-fusion sous stéroïde. L'idée était de fusionner certaines séries d'instructions Java en une seule instruction machine. Typiquement, pour faire une opération arithmétique, une machine à pile empile deux opérandes et exécute une opération, ce qui prend trois instructions machine. La spécification picoJava 1 a proposé d'envoyer directement la seconde opérande en entrée de l'ALU, ce qui fusionnait l'empilement de la seconde opérande et l'opération arithmétique. Le processeur picoJava II va plus loin et fusionne les trois instructions en une seule instruction RISC, agissant sur des registres. Mais la majorité des processeurs Java ne faisaient pas cela, car cela complexifie grandement le décodeur. Les processeurs ''bigfoot'' de DCT utilisaient un système différent pour émuler une pile avec des registres. Leur banc de registre de 64 bits était coupé en deux : les 16 premiers registres étaient utilisés pour la pile, les 48 suivants étaient utilisés pour autre chose. L'idée est que le registre 0 mémorise un emplacement vide, le registre 1 contient le sommet de la pile, le registre 2 l'opérande sous le sommet de la pile, et ainsi de suite. Les 15 premières opérandes de la pile sont donc mémorisées dans les registres. Les instructions écrivent leur résultat dans le registre adéquat, puis tous les registres sont décalés pour que le résultat passe dans le registre R1. Par exemple, les instructions PUSH chargent une opérande dans le registre 0, puis tous les registres sont décalés de un rang, pour que l'opérande chargée soit dans le registre 1. Le registre 0 devient alors le registre numéro 1, le registre 1 devient le 2, et ainsi de suite. Par contre, les opérations arithmétiques font autrement. Une instruction d'addition, par exemple, lit les opérandes dans le registre 1 et 2, puis place son résultat dans le registre 2 (les deux opérandes sont dépilées). Puis, les registres sont décalés d'un rang pour que ce registre 2 devienne le 1. Les registres ne sont en réalité pas décalés, il n'y a pas de transferts entre registres. A la place, les 16 premiers registres utilisaient une forme limitée de renommage de registres, similaire au banc de registre tournant utilisé sur les processeurs EPIC comme l'Itanium. Les numéros de registres sont renommés, décalés, à chaque fois qu'on empile ou dépile une opérande. Pour faire le renommage de registres, processeur contient un ''register stack counter'' de 4 bits, qui est incrémenté à chaque fois qu'on empile une donnée, et est décrémenté quand on dépile une donnée. Quand on veut accéder à un opérande, le numéro de registre architectural associé est traduit en numéro de registre physique, en additionnant le ''register stack counter'' (modulo 16, pour rester sur 4 bits). ==L'accélération de l'interprétation/JIT : le ''Thumb-EE'' d'ARM== Quelques processeurs ont ajouté des instructions pour faciliter le travail des interpréteurs/JIT. Par exemple, en 2005, ARM a ajouté le mode ''Thumb Execution Environment'', qui faisait cela. Il reprenait le jeu d'instruction compact ''thumb'' et ajoutait quelques instructions et modifiait le comportement d'instructions existantes. Le jeu d'instruction ''Thumb-EE'' a été ajouté sur les CPU ARM en 2005, mais a été déprécié en 2011, par manque d'utilité. Mais la tentative mérite qu'on s'attarde dessus. Les CPU de gamme M, déstinés à l'embarqué, n'ont jamais supporté ''Thumb-EE'', vu que de tels processeurs ne sont pas conçus pour exécuter du code interprété/JIT. Le mode ''Thumb-EE'' est un mode d'exécution, séparé de l'ARM normal. Ainsi, une instruction ''Thumb-EE'' et une instruction ARM peuvent avoir le même encodage en binaire, mais se comporter différemment. Le processeur est à tout moment soit en mode ''Thumb-EE'', soit en mode ARM normal, ce qui précise comment ces instructions doivent se comporter. Des instructions ont été ajoutées par ''Thumb-EE'', pour qu'il fasse son travail. L'entrée et la sortie du mode ''Thumb-EE'' se fait avec deux instructions : ENTERX et LEAVEX. Une fois en mode ''Thumb-EE'', le décodage des instructions ''thumb'' se fait avec les règles du ''Thumb-EE'', et non celles du ''thumb'' normal. Une première différence entre ''thumb'' normal et ''Thumb-EE'' est l'ajout de ce qui s'appelle un ''null check'' pour les instructions mémoire. Le ''null check'' est utilisé pour les instructions en mode d'adressage "base + indice" ou "Base + décalage", ou tout autre mode d'adressage avec un registre de base utilisés dans des calculs d'adresse. L'idée est de vérifier si l'adresse de base vaut zéro ou non. Si c'est le cas, le processeur lève une exception matérielle, qui est traitée par l'interprétation ou le compilateur JIT. Une telle situation est en effet signe d'une erreur d'adressage mémoire, qui doit être traitée par l'interpréteur. Typiquement, c'est signe qu'un pointeur n'a pas bien été initialisé, ce qui peut arriver avec du code interprété ou JIT. Les interpréteurs/JIT traitent généralement la situation en ajoutant un test avant toute instruction mémoire, pour vérifier si le pointeur accédé vaut zéro ou non. Avec ''Thumb-EE'', pas besoin d'ajouter les tests en questions : ils sont réalisés automatiquement lors de chaque instruction mémoire. : Les registres pour la pile, comme le pointeur de pile et le pointeur de ''frame'', sont aussi concernés. L'instruction CHKA vérifie que les accès à un tableau ne débordent pas en dehors du tableau en question. Par exemple, pour un tableau de 1024 éléments, elle vérifie si l'indice est compris entre 0 et 1024. Pour cela, elle vérifie si l'indice de tableau est bien dans l'intervalle adéquat et lève une exception matérielle si ce n'est pas le cas. Son utilité se comprend quand on sait que tous les accès à un tableau sont vérifiés par l'interpréteur. L'interpréteur est censé ajouter des instructions pour vérifier les indices, à savoir deux ou trois branchements. Avec CHKA, le test se fait en une seule instruction. D'autres modifications mineures de l'encodage des instructions sont aussi présentes, ainsi que des modifications pour les instructions LOAD/STORE. Précisément, dans les modes "base + indice" et "base + décalage", le décalage et l'indice subissent maintenant des décalages. L'idée est de simplifier le calcul d'adresse. De quoi économiser quelques instructions lors des calculs d'adresse, ce qui facilite le travail de l'interpréteur/JIT. Et des instructions LOAD/STORE précises voient leur comportement modifié pour faciliter la gestion de la pile de Java ou l'exécution des méthodes locales. Pour faciliter le travail de l'interpréteur, deux instructions HBP et HBLP ont été ajoutées. Elles branchent vers une fonction qui gère les exceptions logicielles (une fonctionnalité présente dans de nombreux langages, comme Java). La première effectue un branchement vers la fonction en question, la seconde sauvegarde l'adresse de retour avant de faire ce branchement. Les deux peuvent brancher vers 256 fonctions pré-déterminées, sans avoir à présenter leur adresse. ==La traduction binaire accélérée par le matériel== Plus haut, nous avons surtout parlé des compilateurs et des interpréteurs. Cependant, nous devons aussi parler de la '''traduction binaire'''. Elle traduit un programme écrit dans un code machine vers un autre code machine. Par exemple, elle traduit un programme compilé pour un CPU x86 vers un code machine ARM. En général, le système d'exploitation est généralement compilé pour le jeu d'instruction natif, mais il exécute des applications prévues pour le x86. Les applications sont traduites par le système d'exploitation, avant d'exécuter le code traduit. : Nous parlerons dans la suite de code machine source et de code machine cible, pour parler respectivement du code à traduire et du code obtenu après traduction. Idem avec d'autres termes comme architecture cible/source, ou jeu d'instruction cible/source. La traduction binaire est surtout utilisée pour des questions d'émulation ou de compatibilité. Par exemple, lorsque les Macintosh sont passés de processeurs Power PC vers des processeurs x86, le système d'exploitation Mac OS utilisait la traduction binaire pour convertir les anciennes applications Power PC vers du code x86. Les utilisateurs n'y ont vu que du feu. Le système de traduction binaire était appelé Rosetta 1. Par la suite, lors de la transition de processeurs x86 vers des processeurs Apple, Rosetta 2 a vu le jour. La traduction binaire a surtout été utilisée pour traduire du code x86 vers un autre jeu d'instruction. Il faut dire que le x86 est le jeu d'instruction dominant. De nombreuses entreprises ont eu pour ambition de briser l'hégémonie du x86 sur PC, en remplaçant le x86 par un jeu d'instruction plus performant, tout en gardant une compatibilité maximale. La traduction binaire était la seule solution pratique. Elle était le plus souvent intégralement réalisée en logiciel, comme c'était le cas sur les architectures Itanium avec le ''IA-32 Execution Layer''. ===Les généralités sur la traduction binaire assistée en matériel=== L'architecture source est presque tout le temps une architecture CISC, assez ancienne, qu'on souhaite émuler. Le choix de l'architecture cible se porte souvent sur une architecture VLIW, fort différente de l'architecture source, et ce pour obtenir une économie de matériel conséquente. Pour s'exécuter rapidement, le code traduit doit exploiter le parallélisme d'instruction, à savoir exécuter plusieurs instructions en même temps dans des unités de calcul séparées. Il est possible d'utiliser un processeur superscalaire avec exécution dans le désordre pour cela, mais au prix d'un cout important en transistors. Alors qu'en utilisant un CPU VLIW, c'est le traducteur binaire qui fait tout le travail d'extraction du parallélisme d'instruction. Et après tout, quitte à avoir un traducteur binaire, autant lui refiler le boulot d'optimisation. Compiler du code à la volée est certes assez lourd, mais qu'il existe des algorithmes efficaces pour regrouper des instructions indépendantes dans une seule instruction VLIW. Au passage, le processeur VLIW a plus de registres que le processeur source. Cela permet de faire du renommage de registres directement en logiciel. Le traducteur binaire n'hésite pas à changer les noms de registres entre instructions source et instruction VLIW cible, afin de supprimer des dépendances de données. En conséquence, cela ouvre des opportunités de parallélisme, qui sont exploitées lors du regroupement des opérations en instructions VLIW. Vous pourriez penser que le choix d'un CPU VLIW pour émuler du CISC est tout sauf optimal. Vous devez penser que la traduction binaire est d'autant plus simple que l'architecture source et cibles sont semblables. Dans les faits, ce n'est pas tellement traduire les instructions qui pose problème, mais plus la gestion du registre d'état, des exceptions matérielles, de la mémoire virtuelle, la différence entre gros-boutisme et petit-boutisme, de même que des différences d'adressage pour les périphériques. Le premier problème est la gestion des conditions, notamment en présence d'un registre d'état. En pratique, la traduction binaire s'utilise pour traduire du code CISC vers du code RISC ou VLIW. Les architectures source ont donc un registre d'état, qui est mis à jour non seulement par des instructions de test, mais aussi des instructions arithmétiques. L'architecture cible n'a elle pas de registre d'état, mais des registres à prédicats. La traduction de l'un vers l'autre est alors quelque peu compliquée. Et elle est d'autant plus compliquée que le registre d'état est la source de dépendances d'instruction implicites, qui réduisent les performances. Par exemple, si une instruction arithmétique modifie le registre d'état, cela peut impacter l'exécution d'une instruction ultérieure, qui lit ce registre d'état. Pour éliminer ces fausses dépendances, le traducteur binaire doit renommer ce registre en logiciel et détecter les dépendances utiles. Et le compilateur doit gérer les cas où il y a beaucoup de distance entre les deux, voire les cas où l'instruction dépendante n'a pas encore été analysée par le compilateur. Un autre problème est lié à la gestion des exceptions matérielles, et précisément des exceptions précises. Pour rappel, une instruction VLIW regroupe plusieurs opérations, opérations qui correspondraient à une instruction machine sur un CPU pas VLIW. En conséquence, plusieurs instructions du langage machine "source" sont regroupées en une seule instruction VLIW. Et il faut tenir compte du cas où une instruction source lève une exception. Dans le code VLIW, cela signifie qu'une opération lève une exception, et il faut annuler partiellement l'instruction VLIW associée. Et par partiellement, on veut dire que seules les opérations suivantes dans l'ordre du programme source doivent être annulées, puis ré-exécutées. Et c'est un sacré casse-tête ! Les processeurs VLIW utilisent comme solution le mécanisme d'exceptions différées des processeurs EPIC, vu il y a quelques chapitres. Pour résumer, le code est exécuté par blocs d'instruction, délimités par des branchements ou tout autre limite/barrière pertinente dans le code. Un bloc de code est compilé en deux versions : une version rapide sans exceptions matérielles, une version lente qui gère les exceptions précises. La version rapide s'exécute sans exécuter les exceptions. Cependant, les exceptions matérielles sont enregistrées, pour être prises en compte à la toute fin du bloc de code. La version rapide est exécutée en premier et elle mémorise si une exception matérielle a eu lieu. Puis une instruction vérifie si une exception a eu lieu et décide quoi faire. Si aucune exception n'a eu lieu, elle passe à la suite du programme, les résultats du bloc de code sont définitivement acceptés. Mais si une exception a eu lieu, tout est annulé. Le processeur est remis dans l'état initial, puis le code est ré-exécuté instruction par instruction de manière à gérer l'exception correctement. ===La traduction par pré-décodage=== Une première solution serait de faire la traduction binaire dans le cache d'instruction. Lors d'un défaut de cache, le code chargé depuis la RAM est traduit en code machine cible. Et c'est ce code machine cible qui est mémorisé dans le cache d'instruction et exécuté par le processeur. La technique marche sur le papier et n'est qu'une amélioration des techniques de pré-décodage vues il y a quelques chapitres. Cependant, le code obtenu est une traduction assez basique, qui n'incorpore pas d'optimisations dignes de ce nom. De plus, elle gère mal le cas où la taille du code source et cible sont potentiellement très différentes. Concrètement, elle est surtout utile pour traduire du code d'un processeur RISC vers un autre processeur RISC, dont les tailles d'instructions sont similaires. Elle ne permet pas d'émuler plusieurs jeux d'instructions différents, le cout en matériel (un circuit de pré-décodage par jeu d'instruction) serait trop important. ===Le projet DAISY d'IBM=== De très rares processeurs étaient conçus pour accélérer cette traduction binaire, afin de garder de bonnes performances. Les premiers à avoir étudié l'idée étaient IBM, avec leur projet DAISY (''Dynamically Architected Instruction Set from Yorktown''). Le projet de base était de convertir à la volée du code compilé pour des CPU Power PC, vers du code VLIW. Les chercheurs d'IBM avaient développé un algorithme de traduction dynamique efficace, ainsi qu'un processeur VLIW disposant d'optimisations spécifiques à la traduction binaire dynamique, à la volée. De nombreuses idées de ce projet ont été reprises ou re-découvertes par la société Transmetta, avec ses processeurs Crusoe et Efficieon, puis par NVIDIA avec son projet Denver. Les ingénieurs de ce projet ont étudié la possibilité d'émuler plusieurs jeux d'instructions différents sur un même processeur, notamment le s390 d'IBM et le x86. En théorie, cela demande juste d'avoir plusieurs programmes de traduction binaire : un pour le x86, un autre pour le s390, éventuellement un autre pour le Power PC. Mais cela ne s'est pas concrétisé. Mais les ingénieurs ont étudié quelle pourrait être l'architecture VLIW idéale pour ça. Par exemple, il fallait des additions 3-opérandes pour simplifier les calculs d'adresse. Une difficulté était la gestion du registre d'état, dont les bits ne sont pas mis à jour de la même manière sur le x86 et le s390 ou le Power PC. La différence entre gros-boutisme et petit-boutisme était aussi un problème, de même que des différences d'adressage pour les périphériques. Pour que la traduction binaire soit efficace, le processeur VLIW intègre diverses optimisations, comme les exceptions différées et les branchements multi-voies. La principale est la suivante : seul le code des boucles ou fonctions exécutées fréquemment est traduit en code VLIW, le reste du code est interprété. En effet, traduire du code binaire prend plus de temps qu'une simple interprétation. Pour du code qui ne sera exécutée qu'une seule fois, il est plus rapide d'utiliser l'interprétation. Par contre, pour du code exécuté beaucoup de fois, le cout de la traduction binaire est amorti, dilué sur N exécutions, compensé par le gain en temps d'exécution de ce code traduit. Au final, cela permet de ne traduire que le code qui le mérite. La détection du code fréquemment exécuté est réalisée dans le cœur VLIW. L'unité de branchement mémorise les derniers branchements rencontrés et le nombre de fois qu'ils ont été exécutés. Si ils ont été exécutés un certain nombre de fois, l'unité de branchement lève une exception matérielle, qui invoque le traducteur binaire. La routine de cette exception est le traducteur binaire proprement dit. En clair, la traduction binaire est démarrée quand le processeur détecte qu'une fonction a été exécutée plus de N fois, via une exception matérielle dédiée. La mesure du nombre d'exécution d'un branchement se fait dans l'unité de calcul dédiée aux branchements, avec l'aide d'une sorte de ''branch adress buffer'' modifié. Pour rappel, le ''branch adress buffer'' mémorise le ''Program counter'' de chaque branchement récemment rencontré. Ici, chaque entrée du ''branch adress buffer'' est associée à un compteur incrémenté à chaque exécution du branchement. ===Les processeurs Crusoe et Efficieon de Transmetta=== Les processeurs Crusoe Et Efficeron sont deux processeurs VLIW produits par la société Transmetta, une société californienne rachetée par NVIDIA. Ils étaient conçus pour exécuter spécifiquement des programmes x86, système d'exploitation inclus. L'idée derrière ce projet était d'exécuter du code x86 sans que les applications, ni même le système d'exploitation et le BIOS soient au courant ! Les processeurs Transmetta étaient en réalité conçus pour exécuter un programme unique, le ''Code Morphing Software'' (CMS), qui traduisait le code x86 en code VLIW. Transmetta ne voulait pas que son processeur VLIW soit exploité directement, sans le CMS. Il n'avait pas rendu public de compilateur pour traduire du C vers du code VLIW, il n'avait pas donné la documentation du jeu d'instruction VLIW. Des efforts de rétro-ingénieurie, documentés sur le site realworldtech, ont cependant permis de comprendre comment étaient encodées les instructions du processeur Crusoe. Comme pour le projet Daisy, le ''Code Morphing Software'' utilisait à la fois interprétation et traduction binaire, selon les besoins. Les instructions sont interprétées lors de leurs premières exécutions, mais le CMS bascule sur de la traduction en code VLIW après un certain nombre d'exécution. Ainsi, les boucles souvent exécutées sont traduites en code VLIW, alors que le reste du code est interprété. Le choix du code à traduire est le fait du CMS, il utilise des heuristiques complexes pour, qui ne sont pas connues dans le détail. Le ''Code Morphing Software'' est mémorisé dans une EEPROM, ce qui en fait un ''firmware'' situé en dessous du BIOS. Le CMS démarre ensuite le BIOS, qui lui-même démarre le système d'exploitation, qui lui-même démarre les pilotes de périphériques et les programmes. Le CMS se réserve les 16 premiers mébioctets de l'espace d'adressage. L'EEPROM du CMS est mappée dedans, mais n'en utilise que la moitié. Le reste est utilisé comme cache, pour mémoriser le code VLIW traduit par le CMS. Les deux processeurs intégraient aussi deux mémoires SRAM, utilisées par le CMS, appelées la ''local program memory'' (LPM) et la ''local data memory'' (LDM). La première contient du code qui gére les interruptions, la mémoire virtuelle, les exceptions matérielles, les problèmes d'alignement mémoire, et quelques fonctions très fréquemment utilisées par le CMS. La seconde est de la mémoire RAM utilisée par ces fonctions de la ''local program memory''. Les mémoires caches L1 et L2 sont séparées de ces deux mémoires. Le processeur Crusoe et Efficieon étaient des processeurs VLIW très simples. Ils n'avaient même pas de MMU, ni de fonctionnalités importantes sur les CPU x86. Pour Crusoe, les instructions VLIW étaient encodées sur 64 ou 128 bits, et regroupaient entre 2 à 4 opérations. Les instructions LOAD/STORE ne supportaient pas d'adressage indicé, juste de l'adressage indirect à registre. Crusoe avait 5 unités de calcul : deux ALU entières, une unité LOAD/STORE, une unité de branchement, une FPU. Efficieon doublait le nombre d'ALU entière et d'unité LOAD/STORE, les instructions VLIW passaient à 256 bits. Le processeur Crusoe contient 160 registres, dont 64 registres généraux, 32 registres flottants. Les registres généraux font 32 bits, ce qui est cohérent avec le fait que les CPU x86 de l'époque étaient des processeurs 32 bits. Quant aux registres flottants, ils faisaient 80 bits, ce qui colle avec la taille des registres flottants de la FPU x87 utilisée à l'époque. Sur les 64 registres généraux, seuls 48 étaient réellement utilisable pour mémoriser des opérandes. Une partie des registres généraux étaient utilisés pour la gestion de la pile, d'autres pour mémoriser l'état du CPU x86 émulé, un registre était un registre zéro non accesible en écriture. La gestion des exceptions est optimisée avec un système d'exceptions différées, le même que celui décrit plus haut. Le processeur utilise un système similaire, pour effectuer des lectures anticipées, qui a été expliqué dans le chapitre sur les processeurs VLIW/EPIC. Sauf qu'il s'agit là d'une optimisation pour gagner en performances en général, pas une aide utile pour gérer des exceptions précises ou toute autre subtilité de la traduction binaire. Pour gérer les exceptions différées et les lectures anticipées, le processeur mémorise l'état du processeur avant de démarrer un bloc de code. Pour cela, le processeur contient des ''shadow registers'', 48 registres entiers et 16 registres flottants. Vous remarquerez qu'il y a autant de registres entiers que de ''shadow registers'' entiers. Les registres entiers/flottants sont mémorisés dans les ''shadow registers'' avant d'exécuter un bloc de code, afin que le processeur puisse revenir à l'état de base. Une autre fonctionnalité liée est le ''gated store buffer''. Le principe est d'éviter toute écriture en mémoire RAM, tant que le code VLIW n'a pas émis d'instruction ''commit''. L'idée est que les écritures sont conservées dans la ''store queue'' de l'unité LOAD/STORE. Pour rappel, la ''store queue'' met en attente les écritures soit tant que les écritures ne sont pas terminées, soit tant que la RAM est occupée. L'idée est que les écritures sont accumulées dans la ''store queue'' tant que l'instruction ''commit'' n'a pas eu lieu. Le ''store queue'' est "déversé dans la RAM" seulement quand l'instruction ''commit'' s'exécute. ===Les CPU NVIDIA Denver=== Nvidia a tenté quelque chose de similaire avec son projet Denver. NVIDIA a racheté l'entreprise Transmetta, ses brevets, et a tenté de refaire la même chose. Le projet initial était de traduire du code x86 vers un jeu d'instruction VLIW propriétaire, comme l'a fait Transmetta. Mais NVIDIA n'a pas réussi à acquérir la licence du jeu d'instruction x86 et s'est rabattu sur le jeu d'instruction ARM. Le processeur né de ce projet est le Tegra K1-64 CPU. Il s'agit d'un processeur composé d'un cœur VLIW, couplé à deux décodeurs d'instructions ARM. Les deux décodeurs ARM traduisent une paire d'instructions ARM consécutives en instructions VLIW, pour les exécuter sur le cœur VLIW. Le CPU peut fonctionner selon deux modes : ARM et VLIW. En somme, le processeur supporte deux jeux d'instruction : le jeu d'instruction ARM, et un jeu d'instruction VLIW. En mode ARM, les instructions ARM sont chargées depuis le cache, traduites en VLIW par les décodeurs ARM, puis exécutées par le cœur VLIW. En mode optimisé, le CPU exécute des instructions VLIW chargées depuis le cache d'instruction, les décodeurs ne sont pas utilisés. : Le CPU supporte en réalité deux jeux d'instruction ARM : ARM8 et 7. Mais ils correspondent au même mode pour le CPU. Un point important est que le processeur peut changer très rapidement de mode, en à peine quelques cycles d'horloges. On n'est clairement pas dans le cas des CPU x86, qui mettent des plombes pour passer du mode 32 à 64 bits et inversement. La commutation est tellement rapide qu'on peut considérer que le processeur supporte deux jeux d'instruction simultanément. Et le support simultané de deux jeux d'instruction facilite l'implémentation de la traduction binaire. Encore une fois, inutile de traduire du code qui ne sera exécutée qu'une seule fois, mieux vaut l'exécuter directement dans le mode ARM. Surtout que le processeur supporte un mode ARM qui permet d'exécuter du code ARM sans perte de performance. Par contre, le code fréquemment utilisé, notamment dans des boucles critiques, est traduit en VLIW. Au final, les performances sont optimisées, en limitant le travail du logiciel traducteur binaire. La détection du code fréquemment exécuté se fait différemment que pour les processeurs Transmetta et Daisy. L'unité de branchement ne détecte pas directement le code beaucoup exécuté, même si elle a un rôle à jouer. A la place, la détection est réalisée en logiciel, par un ''thread'' dédié, qui s'exécute sur un cœur séparé. Il détecte le code fréquemment utilisé en regardent les branchements exécutés récemment. Pour cela, le processeur mémorise l'historique des branchements pris récemment et copie cette historique dans une mémoire tampon dédiée, partagée entre tous les cœurs. Vu qu'elle est partagée, le ''thread'' de détection a accès à l'historique sans pertes de performances, seule la copie de l'historique dans ce tampon a un cout en performance, pas son partage. Le code VLIW obtenu après traduction binaire est mémorisé dans la mémoire RAM, dans une portion spécialement réservée pour. Elle fait 128 mébioctets, et est appelée le '''cache d'optimisation''' par NVIDIA. Le cache d'optimisation est protégé en écriture et seul le traducteur binaire peut écrire dedans. Au passage, le traducteur binaire est du code VLIW, ce qui fait qu'il est placé dans le cache d'optimisation. Le processeur détecte automatiquement quand une fonction pour laquelle le code compilé équivalent est disponible. Pour cela, l'unité de chargement contient une table de correspondance entre l'adresse de la fonction ARM, et l'adresse de son équivalent compilé. Quand une fonction est appelée, l'unité de chargement regarde l'adresse de destination du branchement. Si l'adresse est dans cette table, elle récupère l'adresse de la fonction compilée et branche vers celle-ci. Le processeur dispose d'un cache d'instruction de 128 kibioctets, ce qui est très important, mais nécessaire vu la taille des instructions VLIW. Le coeur VLIW dispose de ses propres décodeurs, d'un ''scoreboard'', de 7 unités de calcul et de registres. Pour les unités de calcul, il a deux FPU, deux ALU, une unité de branchement, et deux unités LOAD-STORE faisant aussi ALU. Un point important est que le CPU dispose de 64 registres entiers et de 64 registres flottants. C'est deux fois plus que supporte le jeu d'instruction ARM. En mode ARM, seule la moitié des registres est utilisée. Mais en mode optimisé, le traducteur binaire utilise bien les 64 registres, grâce à une sorte de renommage de registres logicielle. Pour simplifier la traduction binaire, le processeur supporte les techniques vues précédemment. Le processeur intègre un ''gated store buffer'' similaire à celui des processeurs Transmetta. Les techniques d'exceptions différées sont aussi supportées, comme sur les processeurs Transmetta. Il a aussi une unité de préchargement décrite par NVIDIA comme agressive, avec support du préchargement de type anticipé (''runahead''). ===L'émulation de la mémoire virtuelle=== Pour finir, il faut parler de la mémoire virtuelle avec la traduction binaire. Sur le processeur source, le processeur gère à la fois des adresses physiques et virtuelles et sa MMU fait la conversion entre les deux. Sur le processeur VLIW, la MMU est simulée par le programme de traduction binaire, partiellement ou totalement. L'implémentation exacte varie suivant que l'on parle du projet Daisy, des processeurs Transmetta ou de Denver. Sur les processeurs Transmetta, il n'y a pas de MMU ni de mémoire virtuelle. A la place, le programme de traduction binaire émule la mémoire virtuelle du processeur source. Le processeur VLIW ne gère que des adresses physiques, rien d'autre. L'espace d'adressage physique du processeur VLIW a la même taille que celui du processeur émulé, ici des processeurs x86. Il est vraisemblable que les adresses physiques utilisées par le processeur x86 sont les mêmes que celles du processeur VLIW. Sur le projet Daisy, le processeur VLIW gère la mémoire virtuelle via pagination, comme les processeurs émulés. Ce qui fait qu'il gère un espace d'adressage virtuel et un espace d'adressage physique. Pour éviter toute confusion, nous parlerons d'adresse physique/virtuelle VLIW pour les adresses physiques/virtuelles du processeur VLIW, d'adresse virtuelle/physique source pour celle du jeu d'instruction traduit, à savoir du Power PC ou du s390, éventuellement de l'x86. Il faut alors faire le lien entre adresses physiques source et adresses virtuelles VLIW. Pour cela, rien de plus simple : il y a correspondance parfaite. L'adresse physique source numéro N correspond à l'adresse virtuelle VLIW numéro N. Cependant, cela signifie que tout l'espace d'adressage virtuel VLIW serait utilisé par le code à traduire. En réalité, il faut ajouter de la place pour le code traduit et le programme de traduction binaire. La conséquence est que l'espace d'adressage virtuel VLIW est plus large que l'espace d'adressage source. L'espace d'adressage virtuel VLIW est découpé en trois sections : une section pour le code Power PC à traduire, une autre pour le code traduit en VLIW, et une dernière réservée au traducteur binaire. Le programme de traduction est placé dans une ROM mappée en mémoire dans la seconde section. Le reste de la seconde section est réservé à une mémoire RAM utilisée par le programme de traduction. [[File:Mémoire virtuelle sur le projet Daisy.png|centre|vignette|upright=1.5|Mémoire virtuelle sur le projet Daisy]] Il faut noter que chaque page physique source, une fois traduite, correspond à N pages virtuelles VLIW. La raison est que le code traduit prend plus de place que le code machine originel. En conséquence, on doit utiliser une page finale plus grande. Pour le reste, des pages contiguës en mémoire physique source sont elles aussi contiguës en mémoire virtuelle VLIW. Le calcul d'adresse est donc simple : l'adresse physique source est multipliée par N, puis on ajoute l'adresse de base à laquelle commence la section pour le code traduit. ==La traduction binaire sur le Burroughs B1700== Nous venons de voir comment accélérer en matériel la traduction binaire, avec des techniques bien spécifiques. Le Burroughs B1700 a procédé autrement et il est intéressant d'étudier en détail son architecture. Son architecture est décrite dans le livre "Interpreting Machines : Architecture and Programming of the Bl700/Bl800 Series", écrit par Elliott I. Organick et James A. Hinds. Une partie de ce qui vfa suivre est un très court résumé de ce livre. ===Le Burroughs B1700 : architecture externe=== Il disposait de 4 registres généraux, nommés X, Y, T et L, tous de 24 bits. Les registres T et L étaient découpés en 6 sous-registres de 4 bits, nommés LA LB LC LD LE et LF pour le registre L, et TA TB TC TD TE et TF pour le registre T. Ils étaient adressables comme n'importe quel registre. Un point important est que le registre T avait des possibilités d’extraction spécifiques. Il était possible d'extrait N bits, placés n'importe où dans ce registre, et de les copier dans un autre registre. Nous verrons pourquoi cette possibilité est intéressante et ne sert que pour le registre T. Les registres X et Y étaient reliés à une ALU de 24 bits capable d'effectuer des opérations arithmétiques basiques. Fait étonnant, l'ALU faisait tous les calculs en même temps et fournissait ses résultats dans 7 registres : un registre SUM pour l'addition, un registre DIFFERENCE pour la soustraction, et un registre par opération bit à bit. Les registres pour les opérations bit à bit sont : CMPX et CMPY pour l'opération NOT sur chaque opérande, XANY pour le ET bit à bit, XEOY pour le XOR, XORY pour le OU logique. Les additions et soustractions pouvaient se faire sur des opérandes codés en binaire ou en BCD, suivant le code opération utilisé. A cela, il fallait ajouter deux registres de "résultat" MSKX et MSKY. Ils fournissaient un masque dépendant de la taille du résultat en bits. Cela permettait de gérer des données de 0, 4, 8, 12, 16, 20, et 24 bits, alors que l'ALU faisait 24 bits. Et cela n'était pas limité à 16 ou 8 bits, mais n'importe quelle taille inférieure ou égale à 24 bits était supportée, même des tailles non-conventionnelles comme des résultats codés sur 3 bits, 9 bits, 12 bits, etc. L'ALU prenait en opérande les registres X et Y, mais aussi un registre CYF pour la retenue entrante. En sortie, il fournissait aussi deux retenue sortantes : une pour l'addition et une autre pour la soustraction. Elles étaient mémorisées dans les registres CYL et CYD, respectivement. L'ALU fournissait aussi 12 conditions, qui alimentait un registre d'état. Le registre d'état était segmenté en trois registres appelés XYST, XYCN et BICN. Le processeur contenait de nombreux autres registres de 4 bits, dont les sous-registres de T et L. En tout, il y a 27 registres de 4 bits adressables ! Et le processeur disposait d'une ALU 4 bits pour manipuler ces registres de 4 bits ! Elle est capable de faire des MOV entre registres de 4 bits, des opérations bit à bit, et peut aussi tester si un bit vaut 0 ou 1. La dernière possibilité est très utile pour implémenter les branchements. Pour communiquer avec les entrées-sorties, il disposait de deux registres CMND et DATA : CMND pour envoyer une commande à une entrée-sortie, DATA pour lire ou écrire une donnée. Pour la mémoire, il avait deux registres séparés : READ et WRITE. READ contient la donnée lue lors d'une lecture,; WRITE est pour une donnée à écrire lors d’une écriture. Il s'agit de registres d’interfaçage mémoire, qui ont été promus au rang de registres architecturaux. Les deux registres sont gérés par le microcode, qui est le langage principal de la machine. Les instructions machines configurées font sans, mais le microcode doit y avoir accès facilement. Et vu que le microcode est le langage principal de ce processeur, ces deux registres deviennent architecturaux. ===Le Burroughs B1700 : le mal-nommé ''split-level control store''=== Le Burroughs B1700 avait plusieurs modèles : le B1726, le B1710 et le B1800. Ils se distinguaient par la manière dont le code machine source était traduit par le traducteur binaire. Dans ce qui suit, nous parlerons de traducteur binaire pour parler soit d'un interpréteur pour un langage de programmation de haut niveau, soit pour un logiciel de traduction binaire. Le B1710 plaquait le traducteur binaire en mémoire RAM. Le B1800 faisait pareil, mais avait ajouté un cache dédié au code du traducteur binaire, afin d'améliorer les performances. Le B1726 avait un traducteur binaire scindé en deux : une partie était en mémoire RAM, l'autre était dans un ''local store'' dédié. Le processeur peut exécuter le traducteur binaire soit depuis le ''local store'', soit depuis la mémoire RAM avec une pénalité en termes de performances. La documentation appelle cela un '''''split-level control store''''', mais le terme est trompeur : la documentation qualifie de microcode le langage machine du Burroughs B1700, ce qui est à l'origine de beaucoup de confusions. Le programmeur devait découper le traducteur binaire en segments, certains étant placé dans le ''local store'', les autres étant en mémoire RAM. Mieux que ça, le processeur autorisait d'utiliser la technique de l'''overlaying'' pour le microcode ! Il y avait même une fonction OVERLAY dédiée pour ! OVERLAY avait besoin de trois opérandes : la taille du segment à copier, son adresse source en mémoire RAM, son adresse de destination dans la micro-SRAM. Le ''local store'' mémorisait au maximum 2048 instructions machine, ce qui fait que les 2048 premières "adresses" correspondaient au ''local store''. Les suivantes étaient des adresses de mémoire RAM qui contenaient le reste du traducteur binaire. En pratique, l'adresse qui séparait les deux était configurable ! On pouvait limiter la taille du ''local store'' à un multiple de 32 instructions ! Les adresses restantes étaient alors réattribuées à la RAM. Par exemple, on pouvait attribuer 64 adresses au ''local store'', les 2048 - 64 adresses restantes étaient alors attribuées à la mémoire RAM. Un registre limite, nommé TDPM, indiquait quelle était l'adresse de démarcation entre les deux. Il contenait 4 bits, dont la valeur était multipliée par 32, pour obtenir l'adresse limite. L'interpréteur réservait un "segment" en mémoire RAM, pour les données, dont le processeur connaissait la position. En-dehors de ce segment, il n'y a que des instructions machines à traduire, et le traducteur binaire. Le CPU incorporait pour cela deux registres, un pour l'adresse de base du segment de données, un autre pour son adresse de fin. Il incorporait une protection mémoire limitée sur ce segment, à savoir que seul l'interpréteur pouvait lire ou écrire dans ce segment. ===Le code machine du Burroughs B1700=== Le processeur utilise des micro-opérations de 16 bits de long, qui sont copiées dans registre d'instruction M de 16 bits. Fait intéressant, il était possible de faire un OU logique entre l'instruction lue et un registre du processeur, le résultat étant mémorisé dans le registre M. Cela permettait de faire du code automodifiant, pour émuler des modes d'adressage complexe. Cela permettait par exemple de remplacer une adresse dans une micro-opération. L'adresse est alors surimposée avec un OU dans le champ d'adresse, alors que le reste de l'instruction n'est pas modifié. Afin de gérer les interruptions, ces dernières étaient partiellement détectées en logiciel. Les interruptions ne s'exécutaient pas automatiquement, pour émuler correctement les interruptions. Une interruption doit être prise en compte à la toute fin de l'exécution d'une instruction, quand celle-ci a finit son travail. Mais vu qu'ici, les instructions sont émulées avec des séries de micro-opérations, il y a un problème : il est possible d'interrompre une instruction émulée en plein milieu. Pour éviter cela, le processeur ne déclenche pas les interruptions matérielles immédiatement, elles sont mises en attente. Une micro-opération dédiée vérifie si une interruption est en attente, et l'exécute alors cas échéant. La micro-opération est utilisée comme micro-opération finale d'une instruction émulée. ===Une architecture bit-adressable=== Si avoir un microcode réinscriptible est suffisant, quelques choix de ''design'' facilitent l'émulation de jeux d'instructions variés. Par exemple, le CPU Burroughs B1700 avait ajouté une fonctionnalité très importante : il était bit-adressable. En clair, il pouvait adresser la mémoire bit par bit, si nécessaire. L'avantage est que cela permettait d'émuler des architectures dont la taille des registres/''byte''/mots était très différente. Un autre avantage est que cela facilitait la traduction binaire des instructions machine. Détaillons rapidement le premier avantage. Le processeur était un CPU 24 bits, mais il pouvait émuler des processeurs 16 bits, 8 bits, 4 bits, 1 bit, 9 bits, ou toute autre valeur. Pour cela, le processeur lisait des mots de 24 bits et utilisant un ''barrel shifter'' pour sélectionner les bits adéquats. Par exemple, pour un accès de 16 bits, la donnée lue en mémoire était masquée de manière à ne garder que les 16 bits de poids faible. Le ''barrel shifter''/circuit de masquage était placé directement avant le bus mémoire, sur le trajet des données/instructions. Le ''barrel shifter'' était commandé par un registre ''Field Unit'', qui précisait quelle était la taille des données à charger. Le processeur gère un micro-''program counter'' appelé le registre A. Les micro-opérations du processeur font 16 bits de long, et elles sont alignées en RAM sur 16 bits, ce qui fait que les 4 bits de poids fort de ce registre valent 0. Le registre fait 18 bits, mais seuls les 14 bits de poids fort sont utiles. Les adresses mentionnées plus haut, qui adressent les micro-opérations en mémoire RAM, sont des adresses de mots de 16 bits, pas les adresses de bit individuels. ===La traduction binaire est aidée par des registres et instructions dédiées=== Les instructions à décoder n'ont pas de longueur fixe, à savoir qu'elles peuvent faire 16 bit avec tel jeu d'instruction, 8 bits avec un autre, 32 bits sur un autre, etc. Elles peuvent aussi être de longueur fixe ou variable ! Le B1700 intégrait donc un support des instructions de longueur variable, qui était "détourné" pour gérer des jeux d'instruction différents. Notons que vu que l'architecture est bit-adressable, les contraintes d'alignement des instructions disparaissent, ce qui a des avantages qu'on va détailler dans ce qui suit ! L'implémentation des instructions de taille variable ajoutait un registre avant le registre d'instruction. Le registre d'instruction proprement dit était un registre M de 24 bits. Il était précédé par un registre T, dans lequel l'instruction était assemblée, en combinant plusieurs morceaux. Si l'instruction est codée est codée sur moins de 24 bits, le registre T se contente de masquer les bits en trop. Si l'instruction chargée dépasser 24 bits, le chargement se faisait en plusieurs passes, avec plusieurs lectures en mémoire. Dans ce qui suit, on suppose que l'instruction est composée de '''champs''' : l'opcode, les opérandes, les adresses absolues, les constantes immédiates, les numéros de registre, etc. L'idée est que l'instruction est chargée dans le registre T, puis découpée en champs, qui sont copiés dans les registres du processeur ou envoyés au microcode. Je rappelle que le registre T est relié à un ''barrel shifter'', différent au précédent, qui permettait de masquer, décaler et extraire une donnée de ce registre. Et il y a la possibilité d'extraire une suite de bit de ce registre T, pour la copier dans un autre registre. Par exemple, prenons une instruction composée d'un opcode, d'une constante immédiate et d'une adresse immédiate. L'opcode est lu et interprété par le microcode. Le microcode extrait la constante immédiate du registre T et la copie dans le registre X. Ensuite, l'adresse est extraite et envoyée au registre FA, une lecture est démarrée, et l'opérande est copiée dans le registre Y. Enfin, l'instruction est exécutée par le microcode. Le tout prend plusieurs micro-opérations : une première pour lire l'opcode, une seconde pour lire la première opérande, une troisième pour lire la seconde opérande, una quatrième pour tout exécuter. Notons que suivant la taille des champs, on peut en charger plusieurs à la fois. Par exemple, imaginons qu'on ait chargé 24 bits, contenant un opcode de 8 bits et une constante immédiate de 16 bits : pas besoin de faire deux accès mémoire, le registre T est juste découpé en un opcode et une constante. Le registre T est fait pour ce genre d'extraction. ===Le chargement d'un champ=== L'architecture est bit-adressable car elle gére des champs qui ne font pas forcément 8 ou 16 bits. Par exemple, certains jeux d'instructions ont des opcodes codés sur 6 ou 7 bits, voire 9-10 bits. Par exemple, prenons un opcode de 7 bits, suivi par une adresse de 12 bits et une constante immédiate de 15 bits : aucun de ces champs ne sera aligné sur 8/16/24 bits. Utiliser une architecture bit-adressable règle le problème d'alignement. Et en général, les contraintes d'alignement des instructions disparaissent ! Quand on charge une instruction, peut importe qu'elle soit ou non alignée sur 24 bits, elle sera placée dans les bits de poids faible, il suffira de masquer les bits de poids fort inutiles. Par exemple, si on charge une instruction de 16 bits, les 16 bits seront copiés dans les 16 bits de poids faible du registre T, il suffira de masquer l'octet de poids fort. Pas besoin de faire de décalages pour gérer des instructions mal alignées... Pour charger une instruction, il faut connaitre plusieurs informations : l'adresse de son premier bit, sa taille. Pour cela, le B1700 contient deux registres, nommés ''Field Adress'' (FA) et ''Field Length'' (FL), qui indiquent l'adresse du premier bit et la taille de l'instruction à charger. Le premier fait 24 bits, ce qui permet de gérer des adresses de 24 bits. N'oubliez pas que le processeur est bit-adressable, ce qui permet d'adresser 16 Mébi-bits, soit 2 mébioctets. Le registre pour la taille fait lui 16 bits, ce qui permet de gérer des instructions de 65536 bits, soit 8192 octets ! Leur nom commence par ''field'', car le processeur peut gérer des instructions de taille variable et/ou plus longues que 24 bits. Dans ce cas, le chargement de l'instruction se fait champ par champ. Pour donner un exemple, prenons une instruction composée de 20 champs de 12 bits chacun. Les champs sont chargés un par un dans le registre T. Le registre FA est alors initialisé avec l'adresse de l'instruction, il est incrémenté de 12 à chaque cycle. Le registre FL est initialisé avec la taille de l'instruction, soit 20 * 12 = 240 bits, et il est décrémenté de 16 à chaque cycle. Précisons cependant que la taille d'un champ peut être changée d'un cycle à l'autre. Par exemple, le processeur peut charger un opcode de 8 bits, l'interpréter, détecter que l'instruction demande ensuite de charger une adresse de 16 bits. Dans ce cas, le registre FL est immédiatement altéré pour charger 16 bits, au lieu des 8 bits de l'opcode immédiatement précédent. ==Les jeux d'instructions dédiés à un langage de programmation== De rares processeurs sont conçus pour un langage de programmation en particulier. On appelle ces processeurs, conçus pour des besoins particuliers, des '''processeurs dédiés'''. Par exemple, les fameux ''Burrough E-mode'' B5000/B6000/B7000 étaient spécialement conçus pour exécuter de l'ALGOL-60. Leurs cousins B2000/B3000/B4000 étaient eux conçus pour le COBOL. Des langages fonctionnels ont aussi eu droit à leurs processeurs dédiés. Le prolog en est un bel exemple, avec les superordinateurs de 5ème génération qui lui étaient dédié. On peut aussi citer les machines LISP, dédiés au langage LISP, qui datent des années 1970. Elles étaient capables d’exécuter certaines fonctions de base du langage directement dans leurs circuits : elles possédaient notamment un ''garbage collector'' câblé dans ses circuits ainsi que des instructions machines supportant un typage déterminé à l’exécution. Les processeurs dédiés ont eu leur heure de gloire au début de l'informatique, à une époque où les langages de haut niveau venaient d'être inventés. À cette époque, les compilateurs n'étaient pas performants et ne savaient pas bien optimiser le code machine. Il était alors rationnel, pour l'époque, de rapprocher le code machine cible et le langage de programmation de haut niveau. De nombreuses architectures dédiés ont ainsi été inventées, avant que les concepteurs se rendent compte des défauts de cette approche. Les défauts en question ne sont pas nombreux, mais assez simples à comprendre. Premièrement, elles sont très rapides pour un langage de programmation en particulier, mais sont assez mauvaises pour les autres, d'où un problème de "compatibilité". Ajoutons à cela que les langages de programmation peuvent évoluer, devenir de moins en moins populaires/utilisés, ce qui rend la création d'architectures généralistes plus pertinente. Enfin, les architectures dédiées sont évidemment des processeurs CISC, pour implémenter les nombreuses fonctionnalités des langages évolués. Et les défauts des CISC sont assez rédhibitoires à l'heure actuelle. À l'heure actuelle, les algorithmes des compilateurs se sont améliorés et savent nettement mieux utiliser le matériel. Ils produisent du code machine efficace, ce qui rend les architecture dédiées bien moins intéressantes. Si on ajoute les défauts de ces architectures dédiées, par étonnant que les architectures dédiées aient presque disparues. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'accélération matérielle de la virtualisation | prevText=L'accélération matérielle de la virtualisation | next=Le matériel réseau | nextText=Le matériel réseau }} </noinclude> p1h106we4imszeco2qmdq2zgl9kutdq 764043 764042 2026-04-19T20:00:28Z Mewtow 31375 /* Une architecture bit-adressable */ 764043 wikitext text/x-wiki De nos jours, la majorité des programmeurs programment dans des langages de haut niveau. Il est très rare pour eux d'avoir à utiliser de l'assembleur, et encore moins un langage machine. Les programmes écrits dans un langage de haut niveau sont traduits en langage machine par un logiciel appelé le compilateur. [[File:Compilation.PNG|centre|vignette|upright=2.5|Compilateur : principe.]] Il se trouve que quelques rares processeurs sont conçus pour faciliter le travail du compilateur. Et ce sont ces processeurs que nous allons voir dans ce qui suit. Ils représentent une catégorie de jeux d'instruction à part, qui n'a pas vraiment de nom. ==La compilation et l’interprétation== Un compilateur traduit donc du code source, écrit dans un langage de haut niveau, vers du langage machine. La traduction est rarement directe. En général, le compilateur traduit le code source en assembleur, qui est lui-même traduit en langage machine. L'assembleur est une représentation textuelle du langage machine alors que le code machine est du binaire exécutable par le processeur. La traduction se fait donc en deux étapes, la '''compilation''' proprement dite et l''''assemblage''', réalisées respectivement par un compilateur et un assembleur. Il faut parfois rajouter une troisième phase d'édition des liens, que nous passons volontairement sous silence. {| |[[File:Code C.png|vignette|200px|Le code source, ici en C.]] |[[File:Assem.png|vignette|250px|Le code assembleur.]] |[[File:Binary file - hello world (C programming).png|vignette|150px|Le code machine.]] |} ===L'interprétation et la compilation à la volée=== Une alternative à la compilation est l''''interprétation''', qui transforme le code source en code machine à la volée. Avec l'interprétation, le code source est passé à un logiciel appelé l''''interpréteur''', qui exécute le code source ligne de code par ligne de code. Une ligne de code est traduite en code machine, qui est exécutée, puis l'interpréteur passe à la ligne suivante. [[File:Kaantotulkkaus.png|centre|vignette|upright=3|Compilation versus interprétation.]] Un défaut de l'interprétation est son cout en performance, qui est assez important. Aussi, l'interprétation stricte a évolué vers un hybride entre interprétation et compilation. L'idée est qu'une partie peu importante du code source est interprétée, alors que le code important est compilé juste avant d'être exécuté. Pour le dire autrement, le code source est partiellement compilé à la volée, juste avant son exécution. Aussi, on parle de '''compilation à la volée'''. Le terme en anglais est ''Just In Time compilation'', abrévié en JIT. En soi, le JIT est associé à de l'interprétation, les JIT sont en réalité des hybrides interpréteurs-compilateurs. Un point important est que compiler du code à la volée est assez lourd, cela a un cout en performance. Par contre, le code compilé s'exécute plus vite que du code interprété. Si on exécute le code compilé une seule fois, le cout de la compilation l'emporte sur le gain à l'exécution. Mais le code compilé est réutilisé autant de fois que nécessaire. Le gain à l'exécution est donc multiplié par le nombre d'exécutions, alors que le cout de la compilation est répartit sur plusieurs exécutions, il est amorti. Les portions du code sont choisies de manière à avoir un gain en performance maximal. L'idée générale est que le code exécuté très souvent est compilé, alors que du code exécuté pas souvent est interprété. Typiquement, les boucles les plus souvent exécutées sont compilées, le code exécuté une seule fois est interprété. ===Les langages intermédiaires=== Les compilateurs modernes passent par un '''langage intermédiaire''' pour faire la transformation en code machine. Le compilateur traduit le langage de haut niveau en langage intermédiaire, puis traduit le langage intermédiaire en code machine cible. Le langage intermédiaire est parfois appelé le '''''bytecode''''', et ce terme recouvre aussi le code écrit avec du ''bytecode''. L'interprétation aussi peut passer par l'intermédiaire d'un un ''bytecode'', ce n'est pas limité aux compilateurs proprement dit. {| |[[File:Intermediate representation scheme.png|centre|vignette|upright=1.5|Représentation intermédiaire d'un compilateur.]] |[[File:Code interpreter scheme.png|centre|vignette|upright=1.5|Fonctionnement d'un interpréteur.]] |} Faire ainsi a de nombreux avantages, le principal étant d'avoir un compilateur capable de traduire un langage de haut niveau vers plusieurs jeux d'instructions différents. Cela permet d'avoir, par exemple, un compilateur qui traduit du C soit en code machine x86, soit en code machine pour un CPU ARM, soit pour un CPU POWERPC, etc. [[File:Compiler design.svg|centre|vignette|upright=3|Compiler design]] Pour l'interprétation, l'usage d'un ''bytecode'' a d'autres avantages. Pour l'exploiter, il faut distribuer non pas le code source, mais le ''bytecode''. En clair, l'interpréteur ne prend pas en entrée le code source, mais du ''bytecode'' déjà compilé. La traduction est alors beaucoup plus simple, car le code source a déjà été partiellement compilé, par la traduction en ''bytecode''. Interpréter du code source est en effet assez compliqué : il faut effectuer des étapes d'analyse lexicale, sémantique, et bien d'autres. Avec du ''bytecode'', ces étapes ont été réalisées lors de la compilation du ''bytecode'', l'interpréteur a alors peut de choses à faire. L'interprétation du ''bytecode'' se fait instruction par instruction, au niveau du ''bytecode''. Une instruction du ''bytecode'' est traduite en une instruction machine équivalente, qui est exécutée. Si l'instruction est un peu complexe, il exécute une fonction/procédure qui fait la même chose. Pour résumer, le ''bytecode'' est ensuite traduit à la volée et exécuté instruction par instruction par un logiciel appelé l'interpréteur. [[File:Control table.png|centre|vignette|upright=2|Control table]] Le langage intermédiaire peut être vu comme l'assembleur d'un processeur, n'existe pas forcément dans la réalité, mais dont le jeu d'instruction est décrit en détail. Le processeur en question est appelé une '''machine abstraite''', ou encore une ''machine virtuelle''. Nous utiliserons le terme de machine abstraite dans ce qui suit. La machine abstraite n'est pas la même suivant que l'on cible la compilation ou l'interprétation/JIT. Pour l'interprétation/JIT, la machine abstraite est souvent une machine à pile, car cela simplifie grandement la traduction en code machine final. L'interprétation demande que la traduction du ''bytecode'' en code machine soit la plus simple possible. Et cela demande de prendre en compte pas mal de détails. Par exemple, les différents jeux d'instruction existants n'ont pas le même nombre de registres, ce qui pose problème lors de la traduction du ''bytecode''. Lors de la transformation en code machine, un algorithme d'allocation de registres se débrouille pour traduire le code intermédiaire en code qui utilise un nombre limité de registres. Pour améliorer cette allocation de registres, il y a deux solutions niveau ''bytecode'' : soit utiliser un nombre illimité de registres, soit utiliser une machine à pile. Pour les machines à pile, il existe un algorithme simple et rapide pour traduire un code écrit pour une machine à pile en un code écrit pour un processeur avec des registres, qui se débrouille pas trop mal pour allouer efficacement les registres. C'est un avantage assez important pour les langages interprétés. ==Les processeurs qui exécutent du bytecode== Le ''bytecode'' est un code machine, ce qui signifie qu'il peut en théorie s'exécuter sur un processeur qui implémente le jeu d’instruction associé. Si le jeu d'instruction d'un ''bytecode'' est souvent une description censée être fictive, elle n'en reste pas moins un jeu d'instruction et des caractéristiques précises, qu'on peut l'implémenter en matériel ! Néanmoins, tous les ''bytecode'' ne sont pas égaux de ce point de vue. Certains sont faciles à implémenter en matériel, d'autres non. Les ''bytecodes'' implémentés en matériel sont ceux dont les machines abstraites sont des machines à pile. Un premier exemple est celui des processeurs Pascal MicroEngine, qui exécutaient directement le bytecode du langage Pascal, le fameux UCSD P-code. Un second exemple est la '''machine SECD''', qui sert de langage intermédiaire pour certains compilateurs de langages fonctionnels. Elle a été implémentée en matériel par plusieurs équipes, notamment par les chercheurs de l'université de Calgary en 1989. Dans le même genre, quelques processeurs simples étaient capables d’exécuter directement le ''bytecode'' du langage FORTH. Le FORTH, un des premiers langages à pile de haut niveau, possède de nombreuses implémentations hardware et est un des rares langages de haut niveau dont le ''bytecode'' a été utilisé comme langage machine sur certains processeurs. Par exemple, on peut citer le processeur FC16, capable d’exécuter nativement du ''bytecode'' FORTH. ===Les processeurs Java=== Le cas le plus impressionnant est celui de la machine virtuelle Java, qui est un design de processeur comme un autre. En temps normal, le ''bytecode'' Java est compilé ou interprété, mais certains processeurs exécutaient du ''bytecode'' Java directement, sans interprétation ni compilation. Ils sont appelés des '''processeurs Java'''. On peut citer les processeurs ARM disposant de l'extension Jazelle, du JEMCore et du aJ-100 de aJile Systems, le picoJava II de Sun, et quelques autres. Il y a aussi eu quelques projets de recherche ou processeurs open source, comme Komodo, jamuth, le ''Java Optimized Processor'' et quelques autres projets du même genre. L'implémentation de la machine virtuelle Java n'était cependant pas complète. Quelques instructions complexes n'étaient pas gérées par le processeur et devaient être émulées en logiciel. Mais la grosse majorité des instructions l'était. Les instructions non-supportées étaient émulées avec la méthode ''trap and emulate'', à savoir qu'un opcode inconnu déclenchait une exception matérielle ''opcode invalide'', dont la routine pouvait émuler les instructions qui doivent l'être. Le processeur aJ-100 de aJile est capable d'exécuter la quasi-totalité des instructions de la JVM Java, seules deux instructions faisant exception. Les '''accélérateurs Java''' sont une solution intermédiaire, qui permet de traduire à la volée du ''bytecode'' Java en instructions RISC, exécutées sur un autre processeur. Il s'agit formellement de traduction binaire, mais je préfère en parler ici. L'idée est de combiner un processeur RISC avec un coprocesseur Java. Le coprocesseur lit le ''bytecode'' Java et le traduit en instructions RISC, ces dernières étant exécutées sur le processeur RISC. Le JA108 de Nozomi était un coprocesseur de ce genre, au même titre que le coprocesseur JSTAR de JEDI. L'extension Jazelle de certains CPU ARM fonctionnait vraisemblablement sur un principe similaire, sauf que le coprocesseur était intégré dans le processeur ARM, entre le cache d'instruction et le décodeur d'instructions ARM. L'intérêt des processeurs Java n'est pas qu'une question de performance. Certes, ils permettent d’exécuter plus vite les programmes compilés en ''bytecode'', comme des programmes Java pour la JVM Java. Mais l'intérêt est aussi de faire des économies de mémoire RAM et ROM. Je rappelle que la machine virtuelle Java est une machine à pile, ce qui fait que sa densité de code est excellente. Les programmes en ''bytecode'' Java sont donc très petits, et prennent peu de place en mémoire ROM. De plus, utiliser un processeur Java permet de se passer d'interpréteur ou de JIT Java, ce qui économise encore plus de mémoire. ===Transformer un processeur RISC en machine à pile=== La plupart des processeurs Java utilisent en interne une architecture à registre généraux, qui émule une machine à registre en microcode. Quelques registres sont réservés pour l'état de la machine virtuelle, avec au minimum un registre réservé pour le pointeur de pile, un autre pour un pointeur vers un ''pool'' de constantes, et quelques autres. D'autres registres servent à mémoriser le haut de la pile, à savoir le sommet de la pile et quelques opérandes situées juste en-dessous. Par exemple, le processeur picoJava II gardait les 64 opérandes au sommet de la pile dans 64 registres généraux, séparés des registres pour les pointeurs et l'état de la JVM. Une optimisation possible, envisagée sur les processeurs picoJava, était une sorte de macro-fusion sous stéroïde. L'idée était de fusionner certaines séries d'instructions Java en une seule instruction machine. Typiquement, pour faire une opération arithmétique, une machine à pile empile deux opérandes et exécute une opération, ce qui prend trois instructions machine. La spécification picoJava 1 a proposé d'envoyer directement la seconde opérande en entrée de l'ALU, ce qui fusionnait l'empilement de la seconde opérande et l'opération arithmétique. Le processeur picoJava II va plus loin et fusionne les trois instructions en une seule instruction RISC, agissant sur des registres. Mais la majorité des processeurs Java ne faisaient pas cela, car cela complexifie grandement le décodeur. Les processeurs ''bigfoot'' de DCT utilisaient un système différent pour émuler une pile avec des registres. Leur banc de registre de 64 bits était coupé en deux : les 16 premiers registres étaient utilisés pour la pile, les 48 suivants étaient utilisés pour autre chose. L'idée est que le registre 0 mémorise un emplacement vide, le registre 1 contient le sommet de la pile, le registre 2 l'opérande sous le sommet de la pile, et ainsi de suite. Les 15 premières opérandes de la pile sont donc mémorisées dans les registres. Les instructions écrivent leur résultat dans le registre adéquat, puis tous les registres sont décalés pour que le résultat passe dans le registre R1. Par exemple, les instructions PUSH chargent une opérande dans le registre 0, puis tous les registres sont décalés de un rang, pour que l'opérande chargée soit dans le registre 1. Le registre 0 devient alors le registre numéro 1, le registre 1 devient le 2, et ainsi de suite. Par contre, les opérations arithmétiques font autrement. Une instruction d'addition, par exemple, lit les opérandes dans le registre 1 et 2, puis place son résultat dans le registre 2 (les deux opérandes sont dépilées). Puis, les registres sont décalés d'un rang pour que ce registre 2 devienne le 1. Les registres ne sont en réalité pas décalés, il n'y a pas de transferts entre registres. A la place, les 16 premiers registres utilisaient une forme limitée de renommage de registres, similaire au banc de registre tournant utilisé sur les processeurs EPIC comme l'Itanium. Les numéros de registres sont renommés, décalés, à chaque fois qu'on empile ou dépile une opérande. Pour faire le renommage de registres, processeur contient un ''register stack counter'' de 4 bits, qui est incrémenté à chaque fois qu'on empile une donnée, et est décrémenté quand on dépile une donnée. Quand on veut accéder à un opérande, le numéro de registre architectural associé est traduit en numéro de registre physique, en additionnant le ''register stack counter'' (modulo 16, pour rester sur 4 bits). ==L'accélération de l'interprétation/JIT : le ''Thumb-EE'' d'ARM== Quelques processeurs ont ajouté des instructions pour faciliter le travail des interpréteurs/JIT. Par exemple, en 2005, ARM a ajouté le mode ''Thumb Execution Environment'', qui faisait cela. Il reprenait le jeu d'instruction compact ''thumb'' et ajoutait quelques instructions et modifiait le comportement d'instructions existantes. Le jeu d'instruction ''Thumb-EE'' a été ajouté sur les CPU ARM en 2005, mais a été déprécié en 2011, par manque d'utilité. Mais la tentative mérite qu'on s'attarde dessus. Les CPU de gamme M, déstinés à l'embarqué, n'ont jamais supporté ''Thumb-EE'', vu que de tels processeurs ne sont pas conçus pour exécuter du code interprété/JIT. Le mode ''Thumb-EE'' est un mode d'exécution, séparé de l'ARM normal. Ainsi, une instruction ''Thumb-EE'' et une instruction ARM peuvent avoir le même encodage en binaire, mais se comporter différemment. Le processeur est à tout moment soit en mode ''Thumb-EE'', soit en mode ARM normal, ce qui précise comment ces instructions doivent se comporter. Des instructions ont été ajoutées par ''Thumb-EE'', pour qu'il fasse son travail. L'entrée et la sortie du mode ''Thumb-EE'' se fait avec deux instructions : ENTERX et LEAVEX. Une fois en mode ''Thumb-EE'', le décodage des instructions ''thumb'' se fait avec les règles du ''Thumb-EE'', et non celles du ''thumb'' normal. Une première différence entre ''thumb'' normal et ''Thumb-EE'' est l'ajout de ce qui s'appelle un ''null check'' pour les instructions mémoire. Le ''null check'' est utilisé pour les instructions en mode d'adressage "base + indice" ou "Base + décalage", ou tout autre mode d'adressage avec un registre de base utilisés dans des calculs d'adresse. L'idée est de vérifier si l'adresse de base vaut zéro ou non. Si c'est le cas, le processeur lève une exception matérielle, qui est traitée par l'interprétation ou le compilateur JIT. Une telle situation est en effet signe d'une erreur d'adressage mémoire, qui doit être traitée par l'interpréteur. Typiquement, c'est signe qu'un pointeur n'a pas bien été initialisé, ce qui peut arriver avec du code interprété ou JIT. Les interpréteurs/JIT traitent généralement la situation en ajoutant un test avant toute instruction mémoire, pour vérifier si le pointeur accédé vaut zéro ou non. Avec ''Thumb-EE'', pas besoin d'ajouter les tests en questions : ils sont réalisés automatiquement lors de chaque instruction mémoire. : Les registres pour la pile, comme le pointeur de pile et le pointeur de ''frame'', sont aussi concernés. L'instruction CHKA vérifie que les accès à un tableau ne débordent pas en dehors du tableau en question. Par exemple, pour un tableau de 1024 éléments, elle vérifie si l'indice est compris entre 0 et 1024. Pour cela, elle vérifie si l'indice de tableau est bien dans l'intervalle adéquat et lève une exception matérielle si ce n'est pas le cas. Son utilité se comprend quand on sait que tous les accès à un tableau sont vérifiés par l'interpréteur. L'interpréteur est censé ajouter des instructions pour vérifier les indices, à savoir deux ou trois branchements. Avec CHKA, le test se fait en une seule instruction. D'autres modifications mineures de l'encodage des instructions sont aussi présentes, ainsi que des modifications pour les instructions LOAD/STORE. Précisément, dans les modes "base + indice" et "base + décalage", le décalage et l'indice subissent maintenant des décalages. L'idée est de simplifier le calcul d'adresse. De quoi économiser quelques instructions lors des calculs d'adresse, ce qui facilite le travail de l'interpréteur/JIT. Et des instructions LOAD/STORE précises voient leur comportement modifié pour faciliter la gestion de la pile de Java ou l'exécution des méthodes locales. Pour faciliter le travail de l'interpréteur, deux instructions HBP et HBLP ont été ajoutées. Elles branchent vers une fonction qui gère les exceptions logicielles (une fonctionnalité présente dans de nombreux langages, comme Java). La première effectue un branchement vers la fonction en question, la seconde sauvegarde l'adresse de retour avant de faire ce branchement. Les deux peuvent brancher vers 256 fonctions pré-déterminées, sans avoir à présenter leur adresse. ==La traduction binaire accélérée par le matériel== Plus haut, nous avons surtout parlé des compilateurs et des interpréteurs. Cependant, nous devons aussi parler de la '''traduction binaire'''. Elle traduit un programme écrit dans un code machine vers un autre code machine. Par exemple, elle traduit un programme compilé pour un CPU x86 vers un code machine ARM. En général, le système d'exploitation est généralement compilé pour le jeu d'instruction natif, mais il exécute des applications prévues pour le x86. Les applications sont traduites par le système d'exploitation, avant d'exécuter le code traduit. : Nous parlerons dans la suite de code machine source et de code machine cible, pour parler respectivement du code à traduire et du code obtenu après traduction. Idem avec d'autres termes comme architecture cible/source, ou jeu d'instruction cible/source. La traduction binaire est surtout utilisée pour des questions d'émulation ou de compatibilité. Par exemple, lorsque les Macintosh sont passés de processeurs Power PC vers des processeurs x86, le système d'exploitation Mac OS utilisait la traduction binaire pour convertir les anciennes applications Power PC vers du code x86. Les utilisateurs n'y ont vu que du feu. Le système de traduction binaire était appelé Rosetta 1. Par la suite, lors de la transition de processeurs x86 vers des processeurs Apple, Rosetta 2 a vu le jour. La traduction binaire a surtout été utilisée pour traduire du code x86 vers un autre jeu d'instruction. Il faut dire que le x86 est le jeu d'instruction dominant. De nombreuses entreprises ont eu pour ambition de briser l'hégémonie du x86 sur PC, en remplaçant le x86 par un jeu d'instruction plus performant, tout en gardant une compatibilité maximale. La traduction binaire était la seule solution pratique. Elle était le plus souvent intégralement réalisée en logiciel, comme c'était le cas sur les architectures Itanium avec le ''IA-32 Execution Layer''. ===Les généralités sur la traduction binaire assistée en matériel=== L'architecture source est presque tout le temps une architecture CISC, assez ancienne, qu'on souhaite émuler. Le choix de l'architecture cible se porte souvent sur une architecture VLIW, fort différente de l'architecture source, et ce pour obtenir une économie de matériel conséquente. Pour s'exécuter rapidement, le code traduit doit exploiter le parallélisme d'instruction, à savoir exécuter plusieurs instructions en même temps dans des unités de calcul séparées. Il est possible d'utiliser un processeur superscalaire avec exécution dans le désordre pour cela, mais au prix d'un cout important en transistors. Alors qu'en utilisant un CPU VLIW, c'est le traducteur binaire qui fait tout le travail d'extraction du parallélisme d'instruction. Et après tout, quitte à avoir un traducteur binaire, autant lui refiler le boulot d'optimisation. Compiler du code à la volée est certes assez lourd, mais qu'il existe des algorithmes efficaces pour regrouper des instructions indépendantes dans une seule instruction VLIW. Au passage, le processeur VLIW a plus de registres que le processeur source. Cela permet de faire du renommage de registres directement en logiciel. Le traducteur binaire n'hésite pas à changer les noms de registres entre instructions source et instruction VLIW cible, afin de supprimer des dépendances de données. En conséquence, cela ouvre des opportunités de parallélisme, qui sont exploitées lors du regroupement des opérations en instructions VLIW. Vous pourriez penser que le choix d'un CPU VLIW pour émuler du CISC est tout sauf optimal. Vous devez penser que la traduction binaire est d'autant plus simple que l'architecture source et cibles sont semblables. Dans les faits, ce n'est pas tellement traduire les instructions qui pose problème, mais plus la gestion du registre d'état, des exceptions matérielles, de la mémoire virtuelle, la différence entre gros-boutisme et petit-boutisme, de même que des différences d'adressage pour les périphériques. Le premier problème est la gestion des conditions, notamment en présence d'un registre d'état. En pratique, la traduction binaire s'utilise pour traduire du code CISC vers du code RISC ou VLIW. Les architectures source ont donc un registre d'état, qui est mis à jour non seulement par des instructions de test, mais aussi des instructions arithmétiques. L'architecture cible n'a elle pas de registre d'état, mais des registres à prédicats. La traduction de l'un vers l'autre est alors quelque peu compliquée. Et elle est d'autant plus compliquée que le registre d'état est la source de dépendances d'instruction implicites, qui réduisent les performances. Par exemple, si une instruction arithmétique modifie le registre d'état, cela peut impacter l'exécution d'une instruction ultérieure, qui lit ce registre d'état. Pour éliminer ces fausses dépendances, le traducteur binaire doit renommer ce registre en logiciel et détecter les dépendances utiles. Et le compilateur doit gérer les cas où il y a beaucoup de distance entre les deux, voire les cas où l'instruction dépendante n'a pas encore été analysée par le compilateur. Un autre problème est lié à la gestion des exceptions matérielles, et précisément des exceptions précises. Pour rappel, une instruction VLIW regroupe plusieurs opérations, opérations qui correspondraient à une instruction machine sur un CPU pas VLIW. En conséquence, plusieurs instructions du langage machine "source" sont regroupées en une seule instruction VLIW. Et il faut tenir compte du cas où une instruction source lève une exception. Dans le code VLIW, cela signifie qu'une opération lève une exception, et il faut annuler partiellement l'instruction VLIW associée. Et par partiellement, on veut dire que seules les opérations suivantes dans l'ordre du programme source doivent être annulées, puis ré-exécutées. Et c'est un sacré casse-tête ! Les processeurs VLIW utilisent comme solution le mécanisme d'exceptions différées des processeurs EPIC, vu il y a quelques chapitres. Pour résumer, le code est exécuté par blocs d'instruction, délimités par des branchements ou tout autre limite/barrière pertinente dans le code. Un bloc de code est compilé en deux versions : une version rapide sans exceptions matérielles, une version lente qui gère les exceptions précises. La version rapide s'exécute sans exécuter les exceptions. Cependant, les exceptions matérielles sont enregistrées, pour être prises en compte à la toute fin du bloc de code. La version rapide est exécutée en premier et elle mémorise si une exception matérielle a eu lieu. Puis une instruction vérifie si une exception a eu lieu et décide quoi faire. Si aucune exception n'a eu lieu, elle passe à la suite du programme, les résultats du bloc de code sont définitivement acceptés. Mais si une exception a eu lieu, tout est annulé. Le processeur est remis dans l'état initial, puis le code est ré-exécuté instruction par instruction de manière à gérer l'exception correctement. ===La traduction par pré-décodage=== Une première solution serait de faire la traduction binaire dans le cache d'instruction. Lors d'un défaut de cache, le code chargé depuis la RAM est traduit en code machine cible. Et c'est ce code machine cible qui est mémorisé dans le cache d'instruction et exécuté par le processeur. La technique marche sur le papier et n'est qu'une amélioration des techniques de pré-décodage vues il y a quelques chapitres. Cependant, le code obtenu est une traduction assez basique, qui n'incorpore pas d'optimisations dignes de ce nom. De plus, elle gère mal le cas où la taille du code source et cible sont potentiellement très différentes. Concrètement, elle est surtout utile pour traduire du code d'un processeur RISC vers un autre processeur RISC, dont les tailles d'instructions sont similaires. Elle ne permet pas d'émuler plusieurs jeux d'instructions différents, le cout en matériel (un circuit de pré-décodage par jeu d'instruction) serait trop important. ===Le projet DAISY d'IBM=== De très rares processeurs étaient conçus pour accélérer cette traduction binaire, afin de garder de bonnes performances. Les premiers à avoir étudié l'idée étaient IBM, avec leur projet DAISY (''Dynamically Architected Instruction Set from Yorktown''). Le projet de base était de convertir à la volée du code compilé pour des CPU Power PC, vers du code VLIW. Les chercheurs d'IBM avaient développé un algorithme de traduction dynamique efficace, ainsi qu'un processeur VLIW disposant d'optimisations spécifiques à la traduction binaire dynamique, à la volée. De nombreuses idées de ce projet ont été reprises ou re-découvertes par la société Transmetta, avec ses processeurs Crusoe et Efficieon, puis par NVIDIA avec son projet Denver. Les ingénieurs de ce projet ont étudié la possibilité d'émuler plusieurs jeux d'instructions différents sur un même processeur, notamment le s390 d'IBM et le x86. En théorie, cela demande juste d'avoir plusieurs programmes de traduction binaire : un pour le x86, un autre pour le s390, éventuellement un autre pour le Power PC. Mais cela ne s'est pas concrétisé. Mais les ingénieurs ont étudié quelle pourrait être l'architecture VLIW idéale pour ça. Par exemple, il fallait des additions 3-opérandes pour simplifier les calculs d'adresse. Une difficulté était la gestion du registre d'état, dont les bits ne sont pas mis à jour de la même manière sur le x86 et le s390 ou le Power PC. La différence entre gros-boutisme et petit-boutisme était aussi un problème, de même que des différences d'adressage pour les périphériques. Pour que la traduction binaire soit efficace, le processeur VLIW intègre diverses optimisations, comme les exceptions différées et les branchements multi-voies. La principale est la suivante : seul le code des boucles ou fonctions exécutées fréquemment est traduit en code VLIW, le reste du code est interprété. En effet, traduire du code binaire prend plus de temps qu'une simple interprétation. Pour du code qui ne sera exécutée qu'une seule fois, il est plus rapide d'utiliser l'interprétation. Par contre, pour du code exécuté beaucoup de fois, le cout de la traduction binaire est amorti, dilué sur N exécutions, compensé par le gain en temps d'exécution de ce code traduit. Au final, cela permet de ne traduire que le code qui le mérite. La détection du code fréquemment exécuté est réalisée dans le cœur VLIW. L'unité de branchement mémorise les derniers branchements rencontrés et le nombre de fois qu'ils ont été exécutés. Si ils ont été exécutés un certain nombre de fois, l'unité de branchement lève une exception matérielle, qui invoque le traducteur binaire. La routine de cette exception est le traducteur binaire proprement dit. En clair, la traduction binaire est démarrée quand le processeur détecte qu'une fonction a été exécutée plus de N fois, via une exception matérielle dédiée. La mesure du nombre d'exécution d'un branchement se fait dans l'unité de calcul dédiée aux branchements, avec l'aide d'une sorte de ''branch adress buffer'' modifié. Pour rappel, le ''branch adress buffer'' mémorise le ''Program counter'' de chaque branchement récemment rencontré. Ici, chaque entrée du ''branch adress buffer'' est associée à un compteur incrémenté à chaque exécution du branchement. ===Les processeurs Crusoe et Efficieon de Transmetta=== Les processeurs Crusoe Et Efficeron sont deux processeurs VLIW produits par la société Transmetta, une société californienne rachetée par NVIDIA. Ils étaient conçus pour exécuter spécifiquement des programmes x86, système d'exploitation inclus. L'idée derrière ce projet était d'exécuter du code x86 sans que les applications, ni même le système d'exploitation et le BIOS soient au courant ! Les processeurs Transmetta étaient en réalité conçus pour exécuter un programme unique, le ''Code Morphing Software'' (CMS), qui traduisait le code x86 en code VLIW. Transmetta ne voulait pas que son processeur VLIW soit exploité directement, sans le CMS. Il n'avait pas rendu public de compilateur pour traduire du C vers du code VLIW, il n'avait pas donné la documentation du jeu d'instruction VLIW. Des efforts de rétro-ingénieurie, documentés sur le site realworldtech, ont cependant permis de comprendre comment étaient encodées les instructions du processeur Crusoe. Comme pour le projet Daisy, le ''Code Morphing Software'' utilisait à la fois interprétation et traduction binaire, selon les besoins. Les instructions sont interprétées lors de leurs premières exécutions, mais le CMS bascule sur de la traduction en code VLIW après un certain nombre d'exécution. Ainsi, les boucles souvent exécutées sont traduites en code VLIW, alors que le reste du code est interprété. Le choix du code à traduire est le fait du CMS, il utilise des heuristiques complexes pour, qui ne sont pas connues dans le détail. Le ''Code Morphing Software'' est mémorisé dans une EEPROM, ce qui en fait un ''firmware'' situé en dessous du BIOS. Le CMS démarre ensuite le BIOS, qui lui-même démarre le système d'exploitation, qui lui-même démarre les pilotes de périphériques et les programmes. Le CMS se réserve les 16 premiers mébioctets de l'espace d'adressage. L'EEPROM du CMS est mappée dedans, mais n'en utilise que la moitié. Le reste est utilisé comme cache, pour mémoriser le code VLIW traduit par le CMS. Les deux processeurs intégraient aussi deux mémoires SRAM, utilisées par le CMS, appelées la ''local program memory'' (LPM) et la ''local data memory'' (LDM). La première contient du code qui gére les interruptions, la mémoire virtuelle, les exceptions matérielles, les problèmes d'alignement mémoire, et quelques fonctions très fréquemment utilisées par le CMS. La seconde est de la mémoire RAM utilisée par ces fonctions de la ''local program memory''. Les mémoires caches L1 et L2 sont séparées de ces deux mémoires. Le processeur Crusoe et Efficieon étaient des processeurs VLIW très simples. Ils n'avaient même pas de MMU, ni de fonctionnalités importantes sur les CPU x86. Pour Crusoe, les instructions VLIW étaient encodées sur 64 ou 128 bits, et regroupaient entre 2 à 4 opérations. Les instructions LOAD/STORE ne supportaient pas d'adressage indicé, juste de l'adressage indirect à registre. Crusoe avait 5 unités de calcul : deux ALU entières, une unité LOAD/STORE, une unité de branchement, une FPU. Efficieon doublait le nombre d'ALU entière et d'unité LOAD/STORE, les instructions VLIW passaient à 256 bits. Le processeur Crusoe contient 160 registres, dont 64 registres généraux, 32 registres flottants. Les registres généraux font 32 bits, ce qui est cohérent avec le fait que les CPU x86 de l'époque étaient des processeurs 32 bits. Quant aux registres flottants, ils faisaient 80 bits, ce qui colle avec la taille des registres flottants de la FPU x87 utilisée à l'époque. Sur les 64 registres généraux, seuls 48 étaient réellement utilisable pour mémoriser des opérandes. Une partie des registres généraux étaient utilisés pour la gestion de la pile, d'autres pour mémoriser l'état du CPU x86 émulé, un registre était un registre zéro non accesible en écriture. La gestion des exceptions est optimisée avec un système d'exceptions différées, le même que celui décrit plus haut. Le processeur utilise un système similaire, pour effectuer des lectures anticipées, qui a été expliqué dans le chapitre sur les processeurs VLIW/EPIC. Sauf qu'il s'agit là d'une optimisation pour gagner en performances en général, pas une aide utile pour gérer des exceptions précises ou toute autre subtilité de la traduction binaire. Pour gérer les exceptions différées et les lectures anticipées, le processeur mémorise l'état du processeur avant de démarrer un bloc de code. Pour cela, le processeur contient des ''shadow registers'', 48 registres entiers et 16 registres flottants. Vous remarquerez qu'il y a autant de registres entiers que de ''shadow registers'' entiers. Les registres entiers/flottants sont mémorisés dans les ''shadow registers'' avant d'exécuter un bloc de code, afin que le processeur puisse revenir à l'état de base. Une autre fonctionnalité liée est le ''gated store buffer''. Le principe est d'éviter toute écriture en mémoire RAM, tant que le code VLIW n'a pas émis d'instruction ''commit''. L'idée est que les écritures sont conservées dans la ''store queue'' de l'unité LOAD/STORE. Pour rappel, la ''store queue'' met en attente les écritures soit tant que les écritures ne sont pas terminées, soit tant que la RAM est occupée. L'idée est que les écritures sont accumulées dans la ''store queue'' tant que l'instruction ''commit'' n'a pas eu lieu. Le ''store queue'' est "déversé dans la RAM" seulement quand l'instruction ''commit'' s'exécute. ===Les CPU NVIDIA Denver=== Nvidia a tenté quelque chose de similaire avec son projet Denver. NVIDIA a racheté l'entreprise Transmetta, ses brevets, et a tenté de refaire la même chose. Le projet initial était de traduire du code x86 vers un jeu d'instruction VLIW propriétaire, comme l'a fait Transmetta. Mais NVIDIA n'a pas réussi à acquérir la licence du jeu d'instruction x86 et s'est rabattu sur le jeu d'instruction ARM. Le processeur né de ce projet est le Tegra K1-64 CPU. Il s'agit d'un processeur composé d'un cœur VLIW, couplé à deux décodeurs d'instructions ARM. Les deux décodeurs ARM traduisent une paire d'instructions ARM consécutives en instructions VLIW, pour les exécuter sur le cœur VLIW. Le CPU peut fonctionner selon deux modes : ARM et VLIW. En somme, le processeur supporte deux jeux d'instruction : le jeu d'instruction ARM, et un jeu d'instruction VLIW. En mode ARM, les instructions ARM sont chargées depuis le cache, traduites en VLIW par les décodeurs ARM, puis exécutées par le cœur VLIW. En mode optimisé, le CPU exécute des instructions VLIW chargées depuis le cache d'instruction, les décodeurs ne sont pas utilisés. : Le CPU supporte en réalité deux jeux d'instruction ARM : ARM8 et 7. Mais ils correspondent au même mode pour le CPU. Un point important est que le processeur peut changer très rapidement de mode, en à peine quelques cycles d'horloges. On n'est clairement pas dans le cas des CPU x86, qui mettent des plombes pour passer du mode 32 à 64 bits et inversement. La commutation est tellement rapide qu'on peut considérer que le processeur supporte deux jeux d'instruction simultanément. Et le support simultané de deux jeux d'instruction facilite l'implémentation de la traduction binaire. Encore une fois, inutile de traduire du code qui ne sera exécutée qu'une seule fois, mieux vaut l'exécuter directement dans le mode ARM. Surtout que le processeur supporte un mode ARM qui permet d'exécuter du code ARM sans perte de performance. Par contre, le code fréquemment utilisé, notamment dans des boucles critiques, est traduit en VLIW. Au final, les performances sont optimisées, en limitant le travail du logiciel traducteur binaire. La détection du code fréquemment exécuté se fait différemment que pour les processeurs Transmetta et Daisy. L'unité de branchement ne détecte pas directement le code beaucoup exécuté, même si elle a un rôle à jouer. A la place, la détection est réalisée en logiciel, par un ''thread'' dédié, qui s'exécute sur un cœur séparé. Il détecte le code fréquemment utilisé en regardent les branchements exécutés récemment. Pour cela, le processeur mémorise l'historique des branchements pris récemment et copie cette historique dans une mémoire tampon dédiée, partagée entre tous les cœurs. Vu qu'elle est partagée, le ''thread'' de détection a accès à l'historique sans pertes de performances, seule la copie de l'historique dans ce tampon a un cout en performance, pas son partage. Le code VLIW obtenu après traduction binaire est mémorisé dans la mémoire RAM, dans une portion spécialement réservée pour. Elle fait 128 mébioctets, et est appelée le '''cache d'optimisation''' par NVIDIA. Le cache d'optimisation est protégé en écriture et seul le traducteur binaire peut écrire dedans. Au passage, le traducteur binaire est du code VLIW, ce qui fait qu'il est placé dans le cache d'optimisation. Le processeur détecte automatiquement quand une fonction pour laquelle le code compilé équivalent est disponible. Pour cela, l'unité de chargement contient une table de correspondance entre l'adresse de la fonction ARM, et l'adresse de son équivalent compilé. Quand une fonction est appelée, l'unité de chargement regarde l'adresse de destination du branchement. Si l'adresse est dans cette table, elle récupère l'adresse de la fonction compilée et branche vers celle-ci. Le processeur dispose d'un cache d'instruction de 128 kibioctets, ce qui est très important, mais nécessaire vu la taille des instructions VLIW. Le coeur VLIW dispose de ses propres décodeurs, d'un ''scoreboard'', de 7 unités de calcul et de registres. Pour les unités de calcul, il a deux FPU, deux ALU, une unité de branchement, et deux unités LOAD-STORE faisant aussi ALU. Un point important est que le CPU dispose de 64 registres entiers et de 64 registres flottants. C'est deux fois plus que supporte le jeu d'instruction ARM. En mode ARM, seule la moitié des registres est utilisée. Mais en mode optimisé, le traducteur binaire utilise bien les 64 registres, grâce à une sorte de renommage de registres logicielle. Pour simplifier la traduction binaire, le processeur supporte les techniques vues précédemment. Le processeur intègre un ''gated store buffer'' similaire à celui des processeurs Transmetta. Les techniques d'exceptions différées sont aussi supportées, comme sur les processeurs Transmetta. Il a aussi une unité de préchargement décrite par NVIDIA comme agressive, avec support du préchargement de type anticipé (''runahead''). ===L'émulation de la mémoire virtuelle=== Pour finir, il faut parler de la mémoire virtuelle avec la traduction binaire. Sur le processeur source, le processeur gère à la fois des adresses physiques et virtuelles et sa MMU fait la conversion entre les deux. Sur le processeur VLIW, la MMU est simulée par le programme de traduction binaire, partiellement ou totalement. L'implémentation exacte varie suivant que l'on parle du projet Daisy, des processeurs Transmetta ou de Denver. Sur les processeurs Transmetta, il n'y a pas de MMU ni de mémoire virtuelle. A la place, le programme de traduction binaire émule la mémoire virtuelle du processeur source. Le processeur VLIW ne gère que des adresses physiques, rien d'autre. L'espace d'adressage physique du processeur VLIW a la même taille que celui du processeur émulé, ici des processeurs x86. Il est vraisemblable que les adresses physiques utilisées par le processeur x86 sont les mêmes que celles du processeur VLIW. Sur le projet Daisy, le processeur VLIW gère la mémoire virtuelle via pagination, comme les processeurs émulés. Ce qui fait qu'il gère un espace d'adressage virtuel et un espace d'adressage physique. Pour éviter toute confusion, nous parlerons d'adresse physique/virtuelle VLIW pour les adresses physiques/virtuelles du processeur VLIW, d'adresse virtuelle/physique source pour celle du jeu d'instruction traduit, à savoir du Power PC ou du s390, éventuellement de l'x86. Il faut alors faire le lien entre adresses physiques source et adresses virtuelles VLIW. Pour cela, rien de plus simple : il y a correspondance parfaite. L'adresse physique source numéro N correspond à l'adresse virtuelle VLIW numéro N. Cependant, cela signifie que tout l'espace d'adressage virtuel VLIW serait utilisé par le code à traduire. En réalité, il faut ajouter de la place pour le code traduit et le programme de traduction binaire. La conséquence est que l'espace d'adressage virtuel VLIW est plus large que l'espace d'adressage source. L'espace d'adressage virtuel VLIW est découpé en trois sections : une section pour le code Power PC à traduire, une autre pour le code traduit en VLIW, et une dernière réservée au traducteur binaire. Le programme de traduction est placé dans une ROM mappée en mémoire dans la seconde section. Le reste de la seconde section est réservé à une mémoire RAM utilisée par le programme de traduction. [[File:Mémoire virtuelle sur le projet Daisy.png|centre|vignette|upright=1.5|Mémoire virtuelle sur le projet Daisy]] Il faut noter que chaque page physique source, une fois traduite, correspond à N pages virtuelles VLIW. La raison est que le code traduit prend plus de place que le code machine originel. En conséquence, on doit utiliser une page finale plus grande. Pour le reste, des pages contiguës en mémoire physique source sont elles aussi contiguës en mémoire virtuelle VLIW. Le calcul d'adresse est donc simple : l'adresse physique source est multipliée par N, puis on ajoute l'adresse de base à laquelle commence la section pour le code traduit. ==La traduction binaire sur le Burroughs B1700== Nous venons de voir comment accélérer en matériel la traduction binaire, avec des techniques bien spécifiques. Le Burroughs B1700 a procédé autrement et il est intéressant d'étudier en détail son architecture. Son architecture est décrite dans le livre "Interpreting Machines : Architecture and Programming of the Bl700/Bl800 Series", écrit par Elliott I. Organick et James A. Hinds. Une partie de ce qui vfa suivre est un très court résumé de ce livre. ===Le Burroughs B1700 : architecture externe=== Il disposait de 4 registres généraux, nommés X, Y, T et L, tous de 24 bits. Les registres T et L étaient découpés en 6 sous-registres de 4 bits, nommés LA LB LC LD LE et LF pour le registre L, et TA TB TC TD TE et TF pour le registre T. Ils étaient adressables comme n'importe quel registre. Un point important est que le registre T avait des possibilités d’extraction spécifiques. Il était possible d'extrait N bits, placés n'importe où dans ce registre, et de les copier dans un autre registre. Nous verrons pourquoi cette possibilité est intéressante et ne sert que pour le registre T. Les registres X et Y étaient reliés à une ALU de 24 bits capable d'effectuer des opérations arithmétiques basiques. Fait étonnant, l'ALU faisait tous les calculs en même temps et fournissait ses résultats dans 7 registres : un registre SUM pour l'addition, un registre DIFFERENCE pour la soustraction, et un registre par opération bit à bit. Les registres pour les opérations bit à bit sont : CMPX et CMPY pour l'opération NOT sur chaque opérande, XANY pour le ET bit à bit, XEOY pour le XOR, XORY pour le OU logique. Les additions et soustractions pouvaient se faire sur des opérandes codés en binaire ou en BCD, suivant le code opération utilisé. A cela, il fallait ajouter deux registres de "résultat" MSKX et MSKY. Ils fournissaient un masque dépendant de la taille du résultat en bits. Cela permettait de gérer des données de 0, 4, 8, 12, 16, 20, et 24 bits, alors que l'ALU faisait 24 bits. Et cela n'était pas limité à 16 ou 8 bits, mais n'importe quelle taille inférieure ou égale à 24 bits était supportée, même des tailles non-conventionnelles comme des résultats codés sur 3 bits, 9 bits, 12 bits, etc. L'ALU prenait en opérande les registres X et Y, mais aussi un registre CYF pour la retenue entrante. En sortie, il fournissait aussi deux retenue sortantes : une pour l'addition et une autre pour la soustraction. Elles étaient mémorisées dans les registres CYL et CYD, respectivement. L'ALU fournissait aussi 12 conditions, qui alimentait un registre d'état. Le registre d'état était segmenté en trois registres appelés XYST, XYCN et BICN. Le processeur contenait de nombreux autres registres de 4 bits, dont les sous-registres de T et L. En tout, il y a 27 registres de 4 bits adressables ! Et le processeur disposait d'une ALU 4 bits pour manipuler ces registres de 4 bits ! Elle est capable de faire des MOV entre registres de 4 bits, des opérations bit à bit, et peut aussi tester si un bit vaut 0 ou 1. La dernière possibilité est très utile pour implémenter les branchements. Pour communiquer avec les entrées-sorties, il disposait de deux registres CMND et DATA : CMND pour envoyer une commande à une entrée-sortie, DATA pour lire ou écrire une donnée. Pour la mémoire, il avait deux registres séparés : READ et WRITE. READ contient la donnée lue lors d'une lecture,; WRITE est pour une donnée à écrire lors d’une écriture. Il s'agit de registres d’interfaçage mémoire, qui ont été promus au rang de registres architecturaux. Les deux registres sont gérés par le microcode, qui est le langage principal de la machine. Les instructions machines configurées font sans, mais le microcode doit y avoir accès facilement. Et vu que le microcode est le langage principal de ce processeur, ces deux registres deviennent architecturaux. ===Le Burroughs B1700 : le mal-nommé ''split-level control store''=== Le Burroughs B1700 avait plusieurs modèles : le B1726, le B1710 et le B1800. Ils se distinguaient par la manière dont le code machine source était traduit par le traducteur binaire. Dans ce qui suit, nous parlerons de traducteur binaire pour parler soit d'un interpréteur pour un langage de programmation de haut niveau, soit pour un logiciel de traduction binaire. Le B1710 plaquait le traducteur binaire en mémoire RAM. Le B1800 faisait pareil, mais avait ajouté un cache dédié au code du traducteur binaire, afin d'améliorer les performances. Le B1726 avait un traducteur binaire scindé en deux : une partie était en mémoire RAM, l'autre était dans un ''local store'' dédié. Le processeur peut exécuter le traducteur binaire soit depuis le ''local store'', soit depuis la mémoire RAM avec une pénalité en termes de performances. La documentation appelle cela un '''''split-level control store''''', mais le terme est trompeur : la documentation qualifie de microcode le langage machine du Burroughs B1700, ce qui est à l'origine de beaucoup de confusions. Le programmeur devait découper le traducteur binaire en segments, certains étant placé dans le ''local store'', les autres étant en mémoire RAM. Mieux que ça, le processeur autorisait d'utiliser la technique de l'''overlaying'' pour le microcode ! Il y avait même une fonction OVERLAY dédiée pour ! OVERLAY avait besoin de trois opérandes : la taille du segment à copier, son adresse source en mémoire RAM, son adresse de destination dans la micro-SRAM. Le ''local store'' mémorisait au maximum 2048 instructions machine, ce qui fait que les 2048 premières "adresses" correspondaient au ''local store''. Les suivantes étaient des adresses de mémoire RAM qui contenaient le reste du traducteur binaire. En pratique, l'adresse qui séparait les deux était configurable ! On pouvait limiter la taille du ''local store'' à un multiple de 32 instructions ! Les adresses restantes étaient alors réattribuées à la RAM. Par exemple, on pouvait attribuer 64 adresses au ''local store'', les 2048 - 64 adresses restantes étaient alors attribuées à la mémoire RAM. Un registre limite, nommé TDPM, indiquait quelle était l'adresse de démarcation entre les deux. Il contenait 4 bits, dont la valeur était multipliée par 32, pour obtenir l'adresse limite. L'interpréteur réservait un "segment" en mémoire RAM, pour les données, dont le processeur connaissait la position. En-dehors de ce segment, il n'y a que des instructions machines à traduire, et le traducteur binaire. Le CPU incorporait pour cela deux registres, un pour l'adresse de base du segment de données, un autre pour son adresse de fin. Il incorporait une protection mémoire limitée sur ce segment, à savoir que seul l'interpréteur pouvait lire ou écrire dans ce segment. ===Le code machine du Burroughs B1700=== Le processeur utilise des micro-opérations de 16 bits de long, qui sont copiées dans registre d'instruction M de 16 bits. Fait intéressant, il était possible de faire un OU logique entre l'instruction lue et un registre du processeur, le résultat étant mémorisé dans le registre M. Cela permettait de faire du code automodifiant, pour émuler des modes d'adressage complexe. Cela permettait par exemple de remplacer une adresse dans une micro-opération. L'adresse est alors surimposée avec un OU dans le champ d'adresse, alors que le reste de l'instruction n'est pas modifié. Afin de gérer les interruptions, ces dernières étaient partiellement détectées en logiciel. Les interruptions ne s'exécutaient pas automatiquement, pour émuler correctement les interruptions. Une interruption doit être prise en compte à la toute fin de l'exécution d'une instruction, quand celle-ci a finit son travail. Mais vu qu'ici, les instructions sont émulées avec des séries de micro-opérations, il y a un problème : il est possible d'interrompre une instruction émulée en plein milieu. Pour éviter cela, le processeur ne déclenche pas les interruptions matérielles immédiatement, elles sont mises en attente. Une micro-opération dédiée vérifie si une interruption est en attente, et l'exécute alors cas échéant. La micro-opération est utilisée comme micro-opération finale d'une instruction émulée. ===Une architecture bit-adressable=== Si avoir un microcode réinscriptible est suffisant, quelques choix de ''design'' facilitent l'émulation de jeux d'instructions variés. Par exemple, le CPU Burroughs B1700 avait ajouté une fonctionnalité très importante : il était bit-adressable. En clair, il pouvait adresser la mémoire bit par bit, si nécessaire. L'avantage est que cela permettait d'émuler des architectures dont la taille des registres/''byte''/mots était très différente. Un autre avantage est que cela facilitait la traduction binaire des instructions machine. Détaillons rapidement le premier avantage. Le processeur était un CPU 24 bits, mais il pouvait émuler des processeurs 16 bits, 8 bits, 4 bits, 1 bit, 9 bits, ou toute autre valeur. Pour cela, le processeur lisait des mots de 24 bits et utilisant un ''barrel shifter'' pour sélectionner les bits adéquats. Par exemple, pour un accès de 16 bits, la donnée lue en mémoire était masquée de manière à ne garder que les 16 bits de poids faible. Le ''barrel shifter''/circuit de masquage était placé directement avant le bus mémoire, sur le trajet des données/instructions. Le ''barrel shifter'' était commandé par un registre ''Field Unit'', qui précisait quelle était la taille des données à charger. Le ''program counter'' est appelé le registre A. Les instructions du processeur font 16 bits de long, et elles sont alignées en RAM sur 16 bits, ce qui fait que les 4 bits de poids fort de ce registre valent 0, seuls les 14 bits de poids fort sont utiles. Les adresses mentionnées plus haut, qui adressent les micro-opérations en mémoire RAM, sont des adresses de mots de 16 bits, pas les adresses de bit individuels. ===La traduction binaire est aidée par des registres et instructions dédiées=== Les instructions à décoder n'ont pas de longueur fixe, à savoir qu'elles peuvent faire 16 bit avec tel jeu d'instruction, 8 bits avec un autre, 32 bits sur un autre, etc. Elles peuvent aussi être de longueur fixe ou variable ! Le B1700 intégrait donc un support des instructions de longueur variable, qui était "détourné" pour gérer des jeux d'instruction différents. Notons que vu que l'architecture est bit-adressable, les contraintes d'alignement des instructions disparaissent, ce qui a des avantages qu'on va détailler dans ce qui suit ! L'implémentation des instructions de taille variable ajoutait un registre avant le registre d'instruction. Le registre d'instruction proprement dit était un registre M de 24 bits. Il était précédé par un registre T, dans lequel l'instruction était assemblée, en combinant plusieurs morceaux. Si l'instruction est codée est codée sur moins de 24 bits, le registre T se contente de masquer les bits en trop. Si l'instruction chargée dépasser 24 bits, le chargement se faisait en plusieurs passes, avec plusieurs lectures en mémoire. Dans ce qui suit, on suppose que l'instruction est composée de '''champs''' : l'opcode, les opérandes, les adresses absolues, les constantes immédiates, les numéros de registre, etc. L'idée est que l'instruction est chargée dans le registre T, puis découpée en champs, qui sont copiés dans les registres du processeur ou envoyés au microcode. Je rappelle que le registre T est relié à un ''barrel shifter'', différent au précédent, qui permettait de masquer, décaler et extraire une donnée de ce registre. Et il y a la possibilité d'extraire une suite de bit de ce registre T, pour la copier dans un autre registre. Par exemple, prenons une instruction composée d'un opcode, d'une constante immédiate et d'une adresse immédiate. L'opcode est lu et interprété par le microcode. Le microcode extrait la constante immédiate du registre T et la copie dans le registre X. Ensuite, l'adresse est extraite et envoyée au registre FA, une lecture est démarrée, et l'opérande est copiée dans le registre Y. Enfin, l'instruction est exécutée par le microcode. Le tout prend plusieurs micro-opérations : une première pour lire l'opcode, une seconde pour lire la première opérande, une troisième pour lire la seconde opérande, una quatrième pour tout exécuter. Notons que suivant la taille des champs, on peut en charger plusieurs à la fois. Par exemple, imaginons qu'on ait chargé 24 bits, contenant un opcode de 8 bits et une constante immédiate de 16 bits : pas besoin de faire deux accès mémoire, le registre T est juste découpé en un opcode et une constante. Le registre T est fait pour ce genre d'extraction. ===Le chargement d'un champ=== L'architecture est bit-adressable car elle gére des champs qui ne font pas forcément 8 ou 16 bits. Par exemple, certains jeux d'instructions ont des opcodes codés sur 6 ou 7 bits, voire 9-10 bits. Par exemple, prenons un opcode de 7 bits, suivi par une adresse de 12 bits et une constante immédiate de 15 bits : aucun de ces champs ne sera aligné sur 8/16/24 bits. Utiliser une architecture bit-adressable règle le problème d'alignement. Et en général, les contraintes d'alignement des instructions disparaissent ! Quand on charge une instruction, peut importe qu'elle soit ou non alignée sur 24 bits, elle sera placée dans les bits de poids faible, il suffira de masquer les bits de poids fort inutiles. Par exemple, si on charge une instruction de 16 bits, les 16 bits seront copiés dans les 16 bits de poids faible du registre T, il suffira de masquer l'octet de poids fort. Pas besoin de faire de décalages pour gérer des instructions mal alignées... Pour charger une instruction, il faut connaitre plusieurs informations : l'adresse de son premier bit, sa taille. Pour cela, le B1700 contient deux registres, nommés ''Field Adress'' (FA) et ''Field Length'' (FL), qui indiquent l'adresse du premier bit et la taille de l'instruction à charger. Le premier fait 24 bits, ce qui permet de gérer des adresses de 24 bits. N'oubliez pas que le processeur est bit-adressable, ce qui permet d'adresser 16 Mébi-bits, soit 2 mébioctets. Le registre pour la taille fait lui 16 bits, ce qui permet de gérer des instructions de 65536 bits, soit 8192 octets ! Leur nom commence par ''field'', car le processeur peut gérer des instructions de taille variable et/ou plus longues que 24 bits. Dans ce cas, le chargement de l'instruction se fait champ par champ. Pour donner un exemple, prenons une instruction composée de 20 champs de 12 bits chacun. Les champs sont chargés un par un dans le registre T. Le registre FA est alors initialisé avec l'adresse de l'instruction, il est incrémenté de 12 à chaque cycle. Le registre FL est initialisé avec la taille de l'instruction, soit 20 * 12 = 240 bits, et il est décrémenté de 16 à chaque cycle. Précisons cependant que la taille d'un champ peut être changée d'un cycle à l'autre. Par exemple, le processeur peut charger un opcode de 8 bits, l'interpréter, détecter que l'instruction demande ensuite de charger une adresse de 16 bits. Dans ce cas, le registre FL est immédiatement altéré pour charger 16 bits, au lieu des 8 bits de l'opcode immédiatement précédent. ==Les jeux d'instructions dédiés à un langage de programmation== De rares processeurs sont conçus pour un langage de programmation en particulier. On appelle ces processeurs, conçus pour des besoins particuliers, des '''processeurs dédiés'''. Par exemple, les fameux ''Burrough E-mode'' B5000/B6000/B7000 étaient spécialement conçus pour exécuter de l'ALGOL-60. Leurs cousins B2000/B3000/B4000 étaient eux conçus pour le COBOL. Des langages fonctionnels ont aussi eu droit à leurs processeurs dédiés. Le prolog en est un bel exemple, avec les superordinateurs de 5ème génération qui lui étaient dédié. On peut aussi citer les machines LISP, dédiés au langage LISP, qui datent des années 1970. Elles étaient capables d’exécuter certaines fonctions de base du langage directement dans leurs circuits : elles possédaient notamment un ''garbage collector'' câblé dans ses circuits ainsi que des instructions machines supportant un typage déterminé à l’exécution. Les processeurs dédiés ont eu leur heure de gloire au début de l'informatique, à une époque où les langages de haut niveau venaient d'être inventés. À cette époque, les compilateurs n'étaient pas performants et ne savaient pas bien optimiser le code machine. Il était alors rationnel, pour l'époque, de rapprocher le code machine cible et le langage de programmation de haut niveau. De nombreuses architectures dédiés ont ainsi été inventées, avant que les concepteurs se rendent compte des défauts de cette approche. Les défauts en question ne sont pas nombreux, mais assez simples à comprendre. Premièrement, elles sont très rapides pour un langage de programmation en particulier, mais sont assez mauvaises pour les autres, d'où un problème de "compatibilité". Ajoutons à cela que les langages de programmation peuvent évoluer, devenir de moins en moins populaires/utilisés, ce qui rend la création d'architectures généralistes plus pertinente. Enfin, les architectures dédiées sont évidemment des processeurs CISC, pour implémenter les nombreuses fonctionnalités des langages évolués. Et les défauts des CISC sont assez rédhibitoires à l'heure actuelle. À l'heure actuelle, les algorithmes des compilateurs se sont améliorés et savent nettement mieux utiliser le matériel. Ils produisent du code machine efficace, ce qui rend les architecture dédiées bien moins intéressantes. Si on ajoute les défauts de ces architectures dédiées, par étonnant que les architectures dédiées aient presque disparues. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'accélération matérielle de la virtualisation | prevText=L'accélération matérielle de la virtualisation | next=Le matériel réseau | nextText=Le matériel réseau }} </noinclude> qdwc4x09boadop46658yubrxd386dea 764044 764043 2026-04-19T20:03:51Z Mewtow 31375 /* La traduction binaire sur le Burroughs B1700 */ 764044 wikitext text/x-wiki De nos jours, la majorité des programmeurs programment dans des langages de haut niveau. Il est très rare pour eux d'avoir à utiliser de l'assembleur, et encore moins un langage machine. Les programmes écrits dans un langage de haut niveau sont traduits en langage machine par un logiciel appelé le compilateur. [[File:Compilation.PNG|centre|vignette|upright=2.5|Compilateur : principe.]] Il se trouve que quelques rares processeurs sont conçus pour faciliter le travail du compilateur. Et ce sont ces processeurs que nous allons voir dans ce qui suit. Ils représentent une catégorie de jeux d'instruction à part, qui n'a pas vraiment de nom. ==La compilation et l’interprétation== Un compilateur traduit donc du code source, écrit dans un langage de haut niveau, vers du langage machine. La traduction est rarement directe. En général, le compilateur traduit le code source en assembleur, qui est lui-même traduit en langage machine. L'assembleur est une représentation textuelle du langage machine alors que le code machine est du binaire exécutable par le processeur. La traduction se fait donc en deux étapes, la '''compilation''' proprement dite et l''''assemblage''', réalisées respectivement par un compilateur et un assembleur. Il faut parfois rajouter une troisième phase d'édition des liens, que nous passons volontairement sous silence. {| |[[File:Code C.png|vignette|200px|Le code source, ici en C.]] |[[File:Assem.png|vignette|250px|Le code assembleur.]] |[[File:Binary file - hello world (C programming).png|vignette|150px|Le code machine.]] |} ===L'interprétation et la compilation à la volée=== Une alternative à la compilation est l''''interprétation''', qui transforme le code source en code machine à la volée. Avec l'interprétation, le code source est passé à un logiciel appelé l''''interpréteur''', qui exécute le code source ligne de code par ligne de code. Une ligne de code est traduite en code machine, qui est exécutée, puis l'interpréteur passe à la ligne suivante. [[File:Kaantotulkkaus.png|centre|vignette|upright=3|Compilation versus interprétation.]] Un défaut de l'interprétation est son cout en performance, qui est assez important. Aussi, l'interprétation stricte a évolué vers un hybride entre interprétation et compilation. L'idée est qu'une partie peu importante du code source est interprétée, alors que le code important est compilé juste avant d'être exécuté. Pour le dire autrement, le code source est partiellement compilé à la volée, juste avant son exécution. Aussi, on parle de '''compilation à la volée'''. Le terme en anglais est ''Just In Time compilation'', abrévié en JIT. En soi, le JIT est associé à de l'interprétation, les JIT sont en réalité des hybrides interpréteurs-compilateurs. Un point important est que compiler du code à la volée est assez lourd, cela a un cout en performance. Par contre, le code compilé s'exécute plus vite que du code interprété. Si on exécute le code compilé une seule fois, le cout de la compilation l'emporte sur le gain à l'exécution. Mais le code compilé est réutilisé autant de fois que nécessaire. Le gain à l'exécution est donc multiplié par le nombre d'exécutions, alors que le cout de la compilation est répartit sur plusieurs exécutions, il est amorti. Les portions du code sont choisies de manière à avoir un gain en performance maximal. L'idée générale est que le code exécuté très souvent est compilé, alors que du code exécuté pas souvent est interprété. Typiquement, les boucles les plus souvent exécutées sont compilées, le code exécuté une seule fois est interprété. ===Les langages intermédiaires=== Les compilateurs modernes passent par un '''langage intermédiaire''' pour faire la transformation en code machine. Le compilateur traduit le langage de haut niveau en langage intermédiaire, puis traduit le langage intermédiaire en code machine cible. Le langage intermédiaire est parfois appelé le '''''bytecode''''', et ce terme recouvre aussi le code écrit avec du ''bytecode''. L'interprétation aussi peut passer par l'intermédiaire d'un un ''bytecode'', ce n'est pas limité aux compilateurs proprement dit. {| |[[File:Intermediate representation scheme.png|centre|vignette|upright=1.5|Représentation intermédiaire d'un compilateur.]] |[[File:Code interpreter scheme.png|centre|vignette|upright=1.5|Fonctionnement d'un interpréteur.]] |} Faire ainsi a de nombreux avantages, le principal étant d'avoir un compilateur capable de traduire un langage de haut niveau vers plusieurs jeux d'instructions différents. Cela permet d'avoir, par exemple, un compilateur qui traduit du C soit en code machine x86, soit en code machine pour un CPU ARM, soit pour un CPU POWERPC, etc. [[File:Compiler design.svg|centre|vignette|upright=3|Compiler design]] Pour l'interprétation, l'usage d'un ''bytecode'' a d'autres avantages. Pour l'exploiter, il faut distribuer non pas le code source, mais le ''bytecode''. En clair, l'interpréteur ne prend pas en entrée le code source, mais du ''bytecode'' déjà compilé. La traduction est alors beaucoup plus simple, car le code source a déjà été partiellement compilé, par la traduction en ''bytecode''. Interpréter du code source est en effet assez compliqué : il faut effectuer des étapes d'analyse lexicale, sémantique, et bien d'autres. Avec du ''bytecode'', ces étapes ont été réalisées lors de la compilation du ''bytecode'', l'interpréteur a alors peut de choses à faire. L'interprétation du ''bytecode'' se fait instruction par instruction, au niveau du ''bytecode''. Une instruction du ''bytecode'' est traduite en une instruction machine équivalente, qui est exécutée. Si l'instruction est un peu complexe, il exécute une fonction/procédure qui fait la même chose. Pour résumer, le ''bytecode'' est ensuite traduit à la volée et exécuté instruction par instruction par un logiciel appelé l'interpréteur. [[File:Control table.png|centre|vignette|upright=2|Control table]] Le langage intermédiaire peut être vu comme l'assembleur d'un processeur, n'existe pas forcément dans la réalité, mais dont le jeu d'instruction est décrit en détail. Le processeur en question est appelé une '''machine abstraite''', ou encore une ''machine virtuelle''. Nous utiliserons le terme de machine abstraite dans ce qui suit. La machine abstraite n'est pas la même suivant que l'on cible la compilation ou l'interprétation/JIT. Pour l'interprétation/JIT, la machine abstraite est souvent une machine à pile, car cela simplifie grandement la traduction en code machine final. L'interprétation demande que la traduction du ''bytecode'' en code machine soit la plus simple possible. Et cela demande de prendre en compte pas mal de détails. Par exemple, les différents jeux d'instruction existants n'ont pas le même nombre de registres, ce qui pose problème lors de la traduction du ''bytecode''. Lors de la transformation en code machine, un algorithme d'allocation de registres se débrouille pour traduire le code intermédiaire en code qui utilise un nombre limité de registres. Pour améliorer cette allocation de registres, il y a deux solutions niveau ''bytecode'' : soit utiliser un nombre illimité de registres, soit utiliser une machine à pile. Pour les machines à pile, il existe un algorithme simple et rapide pour traduire un code écrit pour une machine à pile en un code écrit pour un processeur avec des registres, qui se débrouille pas trop mal pour allouer efficacement les registres. C'est un avantage assez important pour les langages interprétés. ==Les processeurs qui exécutent du bytecode== Le ''bytecode'' est un code machine, ce qui signifie qu'il peut en théorie s'exécuter sur un processeur qui implémente le jeu d’instruction associé. Si le jeu d'instruction d'un ''bytecode'' est souvent une description censée être fictive, elle n'en reste pas moins un jeu d'instruction et des caractéristiques précises, qu'on peut l'implémenter en matériel ! Néanmoins, tous les ''bytecode'' ne sont pas égaux de ce point de vue. Certains sont faciles à implémenter en matériel, d'autres non. Les ''bytecodes'' implémentés en matériel sont ceux dont les machines abstraites sont des machines à pile. Un premier exemple est celui des processeurs Pascal MicroEngine, qui exécutaient directement le bytecode du langage Pascal, le fameux UCSD P-code. Un second exemple est la '''machine SECD''', qui sert de langage intermédiaire pour certains compilateurs de langages fonctionnels. Elle a été implémentée en matériel par plusieurs équipes, notamment par les chercheurs de l'université de Calgary en 1989. Dans le même genre, quelques processeurs simples étaient capables d’exécuter directement le ''bytecode'' du langage FORTH. Le FORTH, un des premiers langages à pile de haut niveau, possède de nombreuses implémentations hardware et est un des rares langages de haut niveau dont le ''bytecode'' a été utilisé comme langage machine sur certains processeurs. Par exemple, on peut citer le processeur FC16, capable d’exécuter nativement du ''bytecode'' FORTH. ===Les processeurs Java=== Le cas le plus impressionnant est celui de la machine virtuelle Java, qui est un design de processeur comme un autre. En temps normal, le ''bytecode'' Java est compilé ou interprété, mais certains processeurs exécutaient du ''bytecode'' Java directement, sans interprétation ni compilation. Ils sont appelés des '''processeurs Java'''. On peut citer les processeurs ARM disposant de l'extension Jazelle, du JEMCore et du aJ-100 de aJile Systems, le picoJava II de Sun, et quelques autres. Il y a aussi eu quelques projets de recherche ou processeurs open source, comme Komodo, jamuth, le ''Java Optimized Processor'' et quelques autres projets du même genre. L'implémentation de la machine virtuelle Java n'était cependant pas complète. Quelques instructions complexes n'étaient pas gérées par le processeur et devaient être émulées en logiciel. Mais la grosse majorité des instructions l'était. Les instructions non-supportées étaient émulées avec la méthode ''trap and emulate'', à savoir qu'un opcode inconnu déclenchait une exception matérielle ''opcode invalide'', dont la routine pouvait émuler les instructions qui doivent l'être. Le processeur aJ-100 de aJile est capable d'exécuter la quasi-totalité des instructions de la JVM Java, seules deux instructions faisant exception. Les '''accélérateurs Java''' sont une solution intermédiaire, qui permet de traduire à la volée du ''bytecode'' Java en instructions RISC, exécutées sur un autre processeur. Il s'agit formellement de traduction binaire, mais je préfère en parler ici. L'idée est de combiner un processeur RISC avec un coprocesseur Java. Le coprocesseur lit le ''bytecode'' Java et le traduit en instructions RISC, ces dernières étant exécutées sur le processeur RISC. Le JA108 de Nozomi était un coprocesseur de ce genre, au même titre que le coprocesseur JSTAR de JEDI. L'extension Jazelle de certains CPU ARM fonctionnait vraisemblablement sur un principe similaire, sauf que le coprocesseur était intégré dans le processeur ARM, entre le cache d'instruction et le décodeur d'instructions ARM. L'intérêt des processeurs Java n'est pas qu'une question de performance. Certes, ils permettent d’exécuter plus vite les programmes compilés en ''bytecode'', comme des programmes Java pour la JVM Java. Mais l'intérêt est aussi de faire des économies de mémoire RAM et ROM. Je rappelle que la machine virtuelle Java est une machine à pile, ce qui fait que sa densité de code est excellente. Les programmes en ''bytecode'' Java sont donc très petits, et prennent peu de place en mémoire ROM. De plus, utiliser un processeur Java permet de se passer d'interpréteur ou de JIT Java, ce qui économise encore plus de mémoire. ===Transformer un processeur RISC en machine à pile=== La plupart des processeurs Java utilisent en interne une architecture à registre généraux, qui émule une machine à registre en microcode. Quelques registres sont réservés pour l'état de la machine virtuelle, avec au minimum un registre réservé pour le pointeur de pile, un autre pour un pointeur vers un ''pool'' de constantes, et quelques autres. D'autres registres servent à mémoriser le haut de la pile, à savoir le sommet de la pile et quelques opérandes situées juste en-dessous. Par exemple, le processeur picoJava II gardait les 64 opérandes au sommet de la pile dans 64 registres généraux, séparés des registres pour les pointeurs et l'état de la JVM. Une optimisation possible, envisagée sur les processeurs picoJava, était une sorte de macro-fusion sous stéroïde. L'idée était de fusionner certaines séries d'instructions Java en une seule instruction machine. Typiquement, pour faire une opération arithmétique, une machine à pile empile deux opérandes et exécute une opération, ce qui prend trois instructions machine. La spécification picoJava 1 a proposé d'envoyer directement la seconde opérande en entrée de l'ALU, ce qui fusionnait l'empilement de la seconde opérande et l'opération arithmétique. Le processeur picoJava II va plus loin et fusionne les trois instructions en une seule instruction RISC, agissant sur des registres. Mais la majorité des processeurs Java ne faisaient pas cela, car cela complexifie grandement le décodeur. Les processeurs ''bigfoot'' de DCT utilisaient un système différent pour émuler une pile avec des registres. Leur banc de registre de 64 bits était coupé en deux : les 16 premiers registres étaient utilisés pour la pile, les 48 suivants étaient utilisés pour autre chose. L'idée est que le registre 0 mémorise un emplacement vide, le registre 1 contient le sommet de la pile, le registre 2 l'opérande sous le sommet de la pile, et ainsi de suite. Les 15 premières opérandes de la pile sont donc mémorisées dans les registres. Les instructions écrivent leur résultat dans le registre adéquat, puis tous les registres sont décalés pour que le résultat passe dans le registre R1. Par exemple, les instructions PUSH chargent une opérande dans le registre 0, puis tous les registres sont décalés de un rang, pour que l'opérande chargée soit dans le registre 1. Le registre 0 devient alors le registre numéro 1, le registre 1 devient le 2, et ainsi de suite. Par contre, les opérations arithmétiques font autrement. Une instruction d'addition, par exemple, lit les opérandes dans le registre 1 et 2, puis place son résultat dans le registre 2 (les deux opérandes sont dépilées). Puis, les registres sont décalés d'un rang pour que ce registre 2 devienne le 1. Les registres ne sont en réalité pas décalés, il n'y a pas de transferts entre registres. A la place, les 16 premiers registres utilisaient une forme limitée de renommage de registres, similaire au banc de registre tournant utilisé sur les processeurs EPIC comme l'Itanium. Les numéros de registres sont renommés, décalés, à chaque fois qu'on empile ou dépile une opérande. Pour faire le renommage de registres, processeur contient un ''register stack counter'' de 4 bits, qui est incrémenté à chaque fois qu'on empile une donnée, et est décrémenté quand on dépile une donnée. Quand on veut accéder à un opérande, le numéro de registre architectural associé est traduit en numéro de registre physique, en additionnant le ''register stack counter'' (modulo 16, pour rester sur 4 bits). ==L'accélération de l'interprétation/JIT : le ''Thumb-EE'' d'ARM== Quelques processeurs ont ajouté des instructions pour faciliter le travail des interpréteurs/JIT. Par exemple, en 2005, ARM a ajouté le mode ''Thumb Execution Environment'', qui faisait cela. Il reprenait le jeu d'instruction compact ''thumb'' et ajoutait quelques instructions et modifiait le comportement d'instructions existantes. Le jeu d'instruction ''Thumb-EE'' a été ajouté sur les CPU ARM en 2005, mais a été déprécié en 2011, par manque d'utilité. Mais la tentative mérite qu'on s'attarde dessus. Les CPU de gamme M, déstinés à l'embarqué, n'ont jamais supporté ''Thumb-EE'', vu que de tels processeurs ne sont pas conçus pour exécuter du code interprété/JIT. Le mode ''Thumb-EE'' est un mode d'exécution, séparé de l'ARM normal. Ainsi, une instruction ''Thumb-EE'' et une instruction ARM peuvent avoir le même encodage en binaire, mais se comporter différemment. Le processeur est à tout moment soit en mode ''Thumb-EE'', soit en mode ARM normal, ce qui précise comment ces instructions doivent se comporter. Des instructions ont été ajoutées par ''Thumb-EE'', pour qu'il fasse son travail. L'entrée et la sortie du mode ''Thumb-EE'' se fait avec deux instructions : ENTERX et LEAVEX. Une fois en mode ''Thumb-EE'', le décodage des instructions ''thumb'' se fait avec les règles du ''Thumb-EE'', et non celles du ''thumb'' normal. Une première différence entre ''thumb'' normal et ''Thumb-EE'' est l'ajout de ce qui s'appelle un ''null check'' pour les instructions mémoire. Le ''null check'' est utilisé pour les instructions en mode d'adressage "base + indice" ou "Base + décalage", ou tout autre mode d'adressage avec un registre de base utilisés dans des calculs d'adresse. L'idée est de vérifier si l'adresse de base vaut zéro ou non. Si c'est le cas, le processeur lève une exception matérielle, qui est traitée par l'interprétation ou le compilateur JIT. Une telle situation est en effet signe d'une erreur d'adressage mémoire, qui doit être traitée par l'interpréteur. Typiquement, c'est signe qu'un pointeur n'a pas bien été initialisé, ce qui peut arriver avec du code interprété ou JIT. Les interpréteurs/JIT traitent généralement la situation en ajoutant un test avant toute instruction mémoire, pour vérifier si le pointeur accédé vaut zéro ou non. Avec ''Thumb-EE'', pas besoin d'ajouter les tests en questions : ils sont réalisés automatiquement lors de chaque instruction mémoire. : Les registres pour la pile, comme le pointeur de pile et le pointeur de ''frame'', sont aussi concernés. L'instruction CHKA vérifie que les accès à un tableau ne débordent pas en dehors du tableau en question. Par exemple, pour un tableau de 1024 éléments, elle vérifie si l'indice est compris entre 0 et 1024. Pour cela, elle vérifie si l'indice de tableau est bien dans l'intervalle adéquat et lève une exception matérielle si ce n'est pas le cas. Son utilité se comprend quand on sait que tous les accès à un tableau sont vérifiés par l'interpréteur. L'interpréteur est censé ajouter des instructions pour vérifier les indices, à savoir deux ou trois branchements. Avec CHKA, le test se fait en une seule instruction. D'autres modifications mineures de l'encodage des instructions sont aussi présentes, ainsi que des modifications pour les instructions LOAD/STORE. Précisément, dans les modes "base + indice" et "base + décalage", le décalage et l'indice subissent maintenant des décalages. L'idée est de simplifier le calcul d'adresse. De quoi économiser quelques instructions lors des calculs d'adresse, ce qui facilite le travail de l'interpréteur/JIT. Et des instructions LOAD/STORE précises voient leur comportement modifié pour faciliter la gestion de la pile de Java ou l'exécution des méthodes locales. Pour faciliter le travail de l'interpréteur, deux instructions HBP et HBLP ont été ajoutées. Elles branchent vers une fonction qui gère les exceptions logicielles (une fonctionnalité présente dans de nombreux langages, comme Java). La première effectue un branchement vers la fonction en question, la seconde sauvegarde l'adresse de retour avant de faire ce branchement. Les deux peuvent brancher vers 256 fonctions pré-déterminées, sans avoir à présenter leur adresse. ==La traduction binaire accélérée par le matériel== Plus haut, nous avons surtout parlé des compilateurs et des interpréteurs. Cependant, nous devons aussi parler de la '''traduction binaire'''. Elle traduit un programme écrit dans un code machine vers un autre code machine. Par exemple, elle traduit un programme compilé pour un CPU x86 vers un code machine ARM. En général, le système d'exploitation est généralement compilé pour le jeu d'instruction natif, mais il exécute des applications prévues pour le x86. Les applications sont traduites par le système d'exploitation, avant d'exécuter le code traduit. : Nous parlerons dans la suite de code machine source et de code machine cible, pour parler respectivement du code à traduire et du code obtenu après traduction. Idem avec d'autres termes comme architecture cible/source, ou jeu d'instruction cible/source. La traduction binaire est surtout utilisée pour des questions d'émulation ou de compatibilité. Par exemple, lorsque les Macintosh sont passés de processeurs Power PC vers des processeurs x86, le système d'exploitation Mac OS utilisait la traduction binaire pour convertir les anciennes applications Power PC vers du code x86. Les utilisateurs n'y ont vu que du feu. Le système de traduction binaire était appelé Rosetta 1. Par la suite, lors de la transition de processeurs x86 vers des processeurs Apple, Rosetta 2 a vu le jour. La traduction binaire a surtout été utilisée pour traduire du code x86 vers un autre jeu d'instruction. Il faut dire que le x86 est le jeu d'instruction dominant. De nombreuses entreprises ont eu pour ambition de briser l'hégémonie du x86 sur PC, en remplaçant le x86 par un jeu d'instruction plus performant, tout en gardant une compatibilité maximale. La traduction binaire était la seule solution pratique. Elle était le plus souvent intégralement réalisée en logiciel, comme c'était le cas sur les architectures Itanium avec le ''IA-32 Execution Layer''. ===Les généralités sur la traduction binaire assistée en matériel=== L'architecture source est presque tout le temps une architecture CISC, assez ancienne, qu'on souhaite émuler. Le choix de l'architecture cible se porte souvent sur une architecture VLIW, fort différente de l'architecture source, et ce pour obtenir une économie de matériel conséquente. Pour s'exécuter rapidement, le code traduit doit exploiter le parallélisme d'instruction, à savoir exécuter plusieurs instructions en même temps dans des unités de calcul séparées. Il est possible d'utiliser un processeur superscalaire avec exécution dans le désordre pour cela, mais au prix d'un cout important en transistors. Alors qu'en utilisant un CPU VLIW, c'est le traducteur binaire qui fait tout le travail d'extraction du parallélisme d'instruction. Et après tout, quitte à avoir un traducteur binaire, autant lui refiler le boulot d'optimisation. Compiler du code à la volée est certes assez lourd, mais qu'il existe des algorithmes efficaces pour regrouper des instructions indépendantes dans une seule instruction VLIW. Au passage, le processeur VLIW a plus de registres que le processeur source. Cela permet de faire du renommage de registres directement en logiciel. Le traducteur binaire n'hésite pas à changer les noms de registres entre instructions source et instruction VLIW cible, afin de supprimer des dépendances de données. En conséquence, cela ouvre des opportunités de parallélisme, qui sont exploitées lors du regroupement des opérations en instructions VLIW. Vous pourriez penser que le choix d'un CPU VLIW pour émuler du CISC est tout sauf optimal. Vous devez penser que la traduction binaire est d'autant plus simple que l'architecture source et cibles sont semblables. Dans les faits, ce n'est pas tellement traduire les instructions qui pose problème, mais plus la gestion du registre d'état, des exceptions matérielles, de la mémoire virtuelle, la différence entre gros-boutisme et petit-boutisme, de même que des différences d'adressage pour les périphériques. Le premier problème est la gestion des conditions, notamment en présence d'un registre d'état. En pratique, la traduction binaire s'utilise pour traduire du code CISC vers du code RISC ou VLIW. Les architectures source ont donc un registre d'état, qui est mis à jour non seulement par des instructions de test, mais aussi des instructions arithmétiques. L'architecture cible n'a elle pas de registre d'état, mais des registres à prédicats. La traduction de l'un vers l'autre est alors quelque peu compliquée. Et elle est d'autant plus compliquée que le registre d'état est la source de dépendances d'instruction implicites, qui réduisent les performances. Par exemple, si une instruction arithmétique modifie le registre d'état, cela peut impacter l'exécution d'une instruction ultérieure, qui lit ce registre d'état. Pour éliminer ces fausses dépendances, le traducteur binaire doit renommer ce registre en logiciel et détecter les dépendances utiles. Et le compilateur doit gérer les cas où il y a beaucoup de distance entre les deux, voire les cas où l'instruction dépendante n'a pas encore été analysée par le compilateur. Un autre problème est lié à la gestion des exceptions matérielles, et précisément des exceptions précises. Pour rappel, une instruction VLIW regroupe plusieurs opérations, opérations qui correspondraient à une instruction machine sur un CPU pas VLIW. En conséquence, plusieurs instructions du langage machine "source" sont regroupées en une seule instruction VLIW. Et il faut tenir compte du cas où une instruction source lève une exception. Dans le code VLIW, cela signifie qu'une opération lève une exception, et il faut annuler partiellement l'instruction VLIW associée. Et par partiellement, on veut dire que seules les opérations suivantes dans l'ordre du programme source doivent être annulées, puis ré-exécutées. Et c'est un sacré casse-tête ! Les processeurs VLIW utilisent comme solution le mécanisme d'exceptions différées des processeurs EPIC, vu il y a quelques chapitres. Pour résumer, le code est exécuté par blocs d'instruction, délimités par des branchements ou tout autre limite/barrière pertinente dans le code. Un bloc de code est compilé en deux versions : une version rapide sans exceptions matérielles, une version lente qui gère les exceptions précises. La version rapide s'exécute sans exécuter les exceptions. Cependant, les exceptions matérielles sont enregistrées, pour être prises en compte à la toute fin du bloc de code. La version rapide est exécutée en premier et elle mémorise si une exception matérielle a eu lieu. Puis une instruction vérifie si une exception a eu lieu et décide quoi faire. Si aucune exception n'a eu lieu, elle passe à la suite du programme, les résultats du bloc de code sont définitivement acceptés. Mais si une exception a eu lieu, tout est annulé. Le processeur est remis dans l'état initial, puis le code est ré-exécuté instruction par instruction de manière à gérer l'exception correctement. ===La traduction par pré-décodage=== Une première solution serait de faire la traduction binaire dans le cache d'instruction. Lors d'un défaut de cache, le code chargé depuis la RAM est traduit en code machine cible. Et c'est ce code machine cible qui est mémorisé dans le cache d'instruction et exécuté par le processeur. La technique marche sur le papier et n'est qu'une amélioration des techniques de pré-décodage vues il y a quelques chapitres. Cependant, le code obtenu est une traduction assez basique, qui n'incorpore pas d'optimisations dignes de ce nom. De plus, elle gère mal le cas où la taille du code source et cible sont potentiellement très différentes. Concrètement, elle est surtout utile pour traduire du code d'un processeur RISC vers un autre processeur RISC, dont les tailles d'instructions sont similaires. Elle ne permet pas d'émuler plusieurs jeux d'instructions différents, le cout en matériel (un circuit de pré-décodage par jeu d'instruction) serait trop important. ===Le projet DAISY d'IBM=== De très rares processeurs étaient conçus pour accélérer cette traduction binaire, afin de garder de bonnes performances. Les premiers à avoir étudié l'idée étaient IBM, avec leur projet DAISY (''Dynamically Architected Instruction Set from Yorktown''). Le projet de base était de convertir à la volée du code compilé pour des CPU Power PC, vers du code VLIW. Les chercheurs d'IBM avaient développé un algorithme de traduction dynamique efficace, ainsi qu'un processeur VLIW disposant d'optimisations spécifiques à la traduction binaire dynamique, à la volée. De nombreuses idées de ce projet ont été reprises ou re-découvertes par la société Transmetta, avec ses processeurs Crusoe et Efficieon, puis par NVIDIA avec son projet Denver. Les ingénieurs de ce projet ont étudié la possibilité d'émuler plusieurs jeux d'instructions différents sur un même processeur, notamment le s390 d'IBM et le x86. En théorie, cela demande juste d'avoir plusieurs programmes de traduction binaire : un pour le x86, un autre pour le s390, éventuellement un autre pour le Power PC. Mais cela ne s'est pas concrétisé. Mais les ingénieurs ont étudié quelle pourrait être l'architecture VLIW idéale pour ça. Par exemple, il fallait des additions 3-opérandes pour simplifier les calculs d'adresse. Une difficulté était la gestion du registre d'état, dont les bits ne sont pas mis à jour de la même manière sur le x86 et le s390 ou le Power PC. La différence entre gros-boutisme et petit-boutisme était aussi un problème, de même que des différences d'adressage pour les périphériques. Pour que la traduction binaire soit efficace, le processeur VLIW intègre diverses optimisations, comme les exceptions différées et les branchements multi-voies. La principale est la suivante : seul le code des boucles ou fonctions exécutées fréquemment est traduit en code VLIW, le reste du code est interprété. En effet, traduire du code binaire prend plus de temps qu'une simple interprétation. Pour du code qui ne sera exécutée qu'une seule fois, il est plus rapide d'utiliser l'interprétation. Par contre, pour du code exécuté beaucoup de fois, le cout de la traduction binaire est amorti, dilué sur N exécutions, compensé par le gain en temps d'exécution de ce code traduit. Au final, cela permet de ne traduire que le code qui le mérite. La détection du code fréquemment exécuté est réalisée dans le cœur VLIW. L'unité de branchement mémorise les derniers branchements rencontrés et le nombre de fois qu'ils ont été exécutés. Si ils ont été exécutés un certain nombre de fois, l'unité de branchement lève une exception matérielle, qui invoque le traducteur binaire. La routine de cette exception est le traducteur binaire proprement dit. En clair, la traduction binaire est démarrée quand le processeur détecte qu'une fonction a été exécutée plus de N fois, via une exception matérielle dédiée. La mesure du nombre d'exécution d'un branchement se fait dans l'unité de calcul dédiée aux branchements, avec l'aide d'une sorte de ''branch adress buffer'' modifié. Pour rappel, le ''branch adress buffer'' mémorise le ''Program counter'' de chaque branchement récemment rencontré. Ici, chaque entrée du ''branch adress buffer'' est associée à un compteur incrémenté à chaque exécution du branchement. ===Les processeurs Crusoe et Efficieon de Transmetta=== Les processeurs Crusoe Et Efficeron sont deux processeurs VLIW produits par la société Transmetta, une société californienne rachetée par NVIDIA. Ils étaient conçus pour exécuter spécifiquement des programmes x86, système d'exploitation inclus. L'idée derrière ce projet était d'exécuter du code x86 sans que les applications, ni même le système d'exploitation et le BIOS soient au courant ! Les processeurs Transmetta étaient en réalité conçus pour exécuter un programme unique, le ''Code Morphing Software'' (CMS), qui traduisait le code x86 en code VLIW. Transmetta ne voulait pas que son processeur VLIW soit exploité directement, sans le CMS. Il n'avait pas rendu public de compilateur pour traduire du C vers du code VLIW, il n'avait pas donné la documentation du jeu d'instruction VLIW. Des efforts de rétro-ingénieurie, documentés sur le site realworldtech, ont cependant permis de comprendre comment étaient encodées les instructions du processeur Crusoe. Comme pour le projet Daisy, le ''Code Morphing Software'' utilisait à la fois interprétation et traduction binaire, selon les besoins. Les instructions sont interprétées lors de leurs premières exécutions, mais le CMS bascule sur de la traduction en code VLIW après un certain nombre d'exécution. Ainsi, les boucles souvent exécutées sont traduites en code VLIW, alors que le reste du code est interprété. Le choix du code à traduire est le fait du CMS, il utilise des heuristiques complexes pour, qui ne sont pas connues dans le détail. Le ''Code Morphing Software'' est mémorisé dans une EEPROM, ce qui en fait un ''firmware'' situé en dessous du BIOS. Le CMS démarre ensuite le BIOS, qui lui-même démarre le système d'exploitation, qui lui-même démarre les pilotes de périphériques et les programmes. Le CMS se réserve les 16 premiers mébioctets de l'espace d'adressage. L'EEPROM du CMS est mappée dedans, mais n'en utilise que la moitié. Le reste est utilisé comme cache, pour mémoriser le code VLIW traduit par le CMS. Les deux processeurs intégraient aussi deux mémoires SRAM, utilisées par le CMS, appelées la ''local program memory'' (LPM) et la ''local data memory'' (LDM). La première contient du code qui gére les interruptions, la mémoire virtuelle, les exceptions matérielles, les problèmes d'alignement mémoire, et quelques fonctions très fréquemment utilisées par le CMS. La seconde est de la mémoire RAM utilisée par ces fonctions de la ''local program memory''. Les mémoires caches L1 et L2 sont séparées de ces deux mémoires. Le processeur Crusoe et Efficieon étaient des processeurs VLIW très simples. Ils n'avaient même pas de MMU, ni de fonctionnalités importantes sur les CPU x86. Pour Crusoe, les instructions VLIW étaient encodées sur 64 ou 128 bits, et regroupaient entre 2 à 4 opérations. Les instructions LOAD/STORE ne supportaient pas d'adressage indicé, juste de l'adressage indirect à registre. Crusoe avait 5 unités de calcul : deux ALU entières, une unité LOAD/STORE, une unité de branchement, une FPU. Efficieon doublait le nombre d'ALU entière et d'unité LOAD/STORE, les instructions VLIW passaient à 256 bits. Le processeur Crusoe contient 160 registres, dont 64 registres généraux, 32 registres flottants. Les registres généraux font 32 bits, ce qui est cohérent avec le fait que les CPU x86 de l'époque étaient des processeurs 32 bits. Quant aux registres flottants, ils faisaient 80 bits, ce qui colle avec la taille des registres flottants de la FPU x87 utilisée à l'époque. Sur les 64 registres généraux, seuls 48 étaient réellement utilisable pour mémoriser des opérandes. Une partie des registres généraux étaient utilisés pour la gestion de la pile, d'autres pour mémoriser l'état du CPU x86 émulé, un registre était un registre zéro non accesible en écriture. La gestion des exceptions est optimisée avec un système d'exceptions différées, le même que celui décrit plus haut. Le processeur utilise un système similaire, pour effectuer des lectures anticipées, qui a été expliqué dans le chapitre sur les processeurs VLIW/EPIC. Sauf qu'il s'agit là d'une optimisation pour gagner en performances en général, pas une aide utile pour gérer des exceptions précises ou toute autre subtilité de la traduction binaire. Pour gérer les exceptions différées et les lectures anticipées, le processeur mémorise l'état du processeur avant de démarrer un bloc de code. Pour cela, le processeur contient des ''shadow registers'', 48 registres entiers et 16 registres flottants. Vous remarquerez qu'il y a autant de registres entiers que de ''shadow registers'' entiers. Les registres entiers/flottants sont mémorisés dans les ''shadow registers'' avant d'exécuter un bloc de code, afin que le processeur puisse revenir à l'état de base. Une autre fonctionnalité liée est le ''gated store buffer''. Le principe est d'éviter toute écriture en mémoire RAM, tant que le code VLIW n'a pas émis d'instruction ''commit''. L'idée est que les écritures sont conservées dans la ''store queue'' de l'unité LOAD/STORE. Pour rappel, la ''store queue'' met en attente les écritures soit tant que les écritures ne sont pas terminées, soit tant que la RAM est occupée. L'idée est que les écritures sont accumulées dans la ''store queue'' tant que l'instruction ''commit'' n'a pas eu lieu. Le ''store queue'' est "déversé dans la RAM" seulement quand l'instruction ''commit'' s'exécute. ===Les CPU NVIDIA Denver=== Nvidia a tenté quelque chose de similaire avec son projet Denver. NVIDIA a racheté l'entreprise Transmetta, ses brevets, et a tenté de refaire la même chose. Le projet initial était de traduire du code x86 vers un jeu d'instruction VLIW propriétaire, comme l'a fait Transmetta. Mais NVIDIA n'a pas réussi à acquérir la licence du jeu d'instruction x86 et s'est rabattu sur le jeu d'instruction ARM. Le processeur né de ce projet est le Tegra K1-64 CPU. Il s'agit d'un processeur composé d'un cœur VLIW, couplé à deux décodeurs d'instructions ARM. Les deux décodeurs ARM traduisent une paire d'instructions ARM consécutives en instructions VLIW, pour les exécuter sur le cœur VLIW. Le CPU peut fonctionner selon deux modes : ARM et VLIW. En somme, le processeur supporte deux jeux d'instruction : le jeu d'instruction ARM, et un jeu d'instruction VLIW. En mode ARM, les instructions ARM sont chargées depuis le cache, traduites en VLIW par les décodeurs ARM, puis exécutées par le cœur VLIW. En mode optimisé, le CPU exécute des instructions VLIW chargées depuis le cache d'instruction, les décodeurs ne sont pas utilisés. : Le CPU supporte en réalité deux jeux d'instruction ARM : ARM8 et 7. Mais ils correspondent au même mode pour le CPU. Un point important est que le processeur peut changer très rapidement de mode, en à peine quelques cycles d'horloges. On n'est clairement pas dans le cas des CPU x86, qui mettent des plombes pour passer du mode 32 à 64 bits et inversement. La commutation est tellement rapide qu'on peut considérer que le processeur supporte deux jeux d'instruction simultanément. Et le support simultané de deux jeux d'instruction facilite l'implémentation de la traduction binaire. Encore une fois, inutile de traduire du code qui ne sera exécutée qu'une seule fois, mieux vaut l'exécuter directement dans le mode ARM. Surtout que le processeur supporte un mode ARM qui permet d'exécuter du code ARM sans perte de performance. Par contre, le code fréquemment utilisé, notamment dans des boucles critiques, est traduit en VLIW. Au final, les performances sont optimisées, en limitant le travail du logiciel traducteur binaire. La détection du code fréquemment exécuté se fait différemment que pour les processeurs Transmetta et Daisy. L'unité de branchement ne détecte pas directement le code beaucoup exécuté, même si elle a un rôle à jouer. A la place, la détection est réalisée en logiciel, par un ''thread'' dédié, qui s'exécute sur un cœur séparé. Il détecte le code fréquemment utilisé en regardent les branchements exécutés récemment. Pour cela, le processeur mémorise l'historique des branchements pris récemment et copie cette historique dans une mémoire tampon dédiée, partagée entre tous les cœurs. Vu qu'elle est partagée, le ''thread'' de détection a accès à l'historique sans pertes de performances, seule la copie de l'historique dans ce tampon a un cout en performance, pas son partage. Le code VLIW obtenu après traduction binaire est mémorisé dans la mémoire RAM, dans une portion spécialement réservée pour. Elle fait 128 mébioctets, et est appelée le '''cache d'optimisation''' par NVIDIA. Le cache d'optimisation est protégé en écriture et seul le traducteur binaire peut écrire dedans. Au passage, le traducteur binaire est du code VLIW, ce qui fait qu'il est placé dans le cache d'optimisation. Le processeur détecte automatiquement quand une fonction pour laquelle le code compilé équivalent est disponible. Pour cela, l'unité de chargement contient une table de correspondance entre l'adresse de la fonction ARM, et l'adresse de son équivalent compilé. Quand une fonction est appelée, l'unité de chargement regarde l'adresse de destination du branchement. Si l'adresse est dans cette table, elle récupère l'adresse de la fonction compilée et branche vers celle-ci. Le processeur dispose d'un cache d'instruction de 128 kibioctets, ce qui est très important, mais nécessaire vu la taille des instructions VLIW. Le coeur VLIW dispose de ses propres décodeurs, d'un ''scoreboard'', de 7 unités de calcul et de registres. Pour les unités de calcul, il a deux FPU, deux ALU, une unité de branchement, et deux unités LOAD-STORE faisant aussi ALU. Un point important est que le CPU dispose de 64 registres entiers et de 64 registres flottants. C'est deux fois plus que supporte le jeu d'instruction ARM. En mode ARM, seule la moitié des registres est utilisée. Mais en mode optimisé, le traducteur binaire utilise bien les 64 registres, grâce à une sorte de renommage de registres logicielle. Pour simplifier la traduction binaire, le processeur supporte les techniques vues précédemment. Le processeur intègre un ''gated store buffer'' similaire à celui des processeurs Transmetta. Les techniques d'exceptions différées sont aussi supportées, comme sur les processeurs Transmetta. Il a aussi une unité de préchargement décrite par NVIDIA comme agressive, avec support du préchargement de type anticipé (''runahead''). ===L'émulation de la mémoire virtuelle=== Pour finir, il faut parler de la mémoire virtuelle avec la traduction binaire. Sur le processeur source, le processeur gère à la fois des adresses physiques et virtuelles et sa MMU fait la conversion entre les deux. Sur le processeur VLIW, la MMU est simulée par le programme de traduction binaire, partiellement ou totalement. L'implémentation exacte varie suivant que l'on parle du projet Daisy, des processeurs Transmetta ou de Denver. Sur les processeurs Transmetta, il n'y a pas de MMU ni de mémoire virtuelle. A la place, le programme de traduction binaire émule la mémoire virtuelle du processeur source. Le processeur VLIW ne gère que des adresses physiques, rien d'autre. L'espace d'adressage physique du processeur VLIW a la même taille que celui du processeur émulé, ici des processeurs x86. Il est vraisemblable que les adresses physiques utilisées par le processeur x86 sont les mêmes que celles du processeur VLIW. Sur le projet Daisy, le processeur VLIW gère la mémoire virtuelle via pagination, comme les processeurs émulés. Ce qui fait qu'il gère un espace d'adressage virtuel et un espace d'adressage physique. Pour éviter toute confusion, nous parlerons d'adresse physique/virtuelle VLIW pour les adresses physiques/virtuelles du processeur VLIW, d'adresse virtuelle/physique source pour celle du jeu d'instruction traduit, à savoir du Power PC ou du s390, éventuellement de l'x86. Il faut alors faire le lien entre adresses physiques source et adresses virtuelles VLIW. Pour cela, rien de plus simple : il y a correspondance parfaite. L'adresse physique source numéro N correspond à l'adresse virtuelle VLIW numéro N. Cependant, cela signifie que tout l'espace d'adressage virtuel VLIW serait utilisé par le code à traduire. En réalité, il faut ajouter de la place pour le code traduit et le programme de traduction binaire. La conséquence est que l'espace d'adressage virtuel VLIW est plus large que l'espace d'adressage source. L'espace d'adressage virtuel VLIW est découpé en trois sections : une section pour le code Power PC à traduire, une autre pour le code traduit en VLIW, et une dernière réservée au traducteur binaire. Le programme de traduction est placé dans une ROM mappée en mémoire dans la seconde section. Le reste de la seconde section est réservé à une mémoire RAM utilisée par le programme de traduction. [[File:Mémoire virtuelle sur le projet Daisy.png|centre|vignette|upright=1.5|Mémoire virtuelle sur le projet Daisy]] Il faut noter que chaque page physique source, une fois traduite, correspond à N pages virtuelles VLIW. La raison est que le code traduit prend plus de place que le code machine originel. En conséquence, on doit utiliser une page finale plus grande. Pour le reste, des pages contiguës en mémoire physique source sont elles aussi contiguës en mémoire virtuelle VLIW. Le calcul d'adresse est donc simple : l'adresse physique source est multipliée par N, puis on ajoute l'adresse de base à laquelle commence la section pour le code traduit. ==La traduction binaire sur le Burroughs B1700== Nous venons de voir comment accélérer en matériel la traduction binaire, avec des techniques bien spécifiques. Le Burroughs B1700 a procédé autrement et il est intéressant d'étudier en détail son architecture. Son architecture est décrite dans le livre "Interpreting Machines : Architecture and Programming of the Bl700/Bl800 Series", écrit par Elliott I. Organick et James A. Hinds. Une partie de ce qui vfa suivre est un très court résumé de ce livre. ===Le Burroughs B1700 : architecture externe=== Il disposait de 4 registres généraux, nommés X, Y, T et L, tous de 24 bits. Les registres T et L étaient découpés en 6 sous-registres de 4 bits, nommés LA LB LC LD LE et LF pour le registre L, et TA TB TC TD TE et TF pour le registre T. Ils étaient adressables comme n'importe quel registre. Un point important est que le registre T avait des possibilités d’extraction spécifiques. Il était possible d'extrait N bits, placés n'importe où dans ce registre, et de les copier dans un autre registre. Nous verrons pourquoi cette possibilité est intéressante et ne sert que pour le registre T. Les registres X et Y étaient reliés à une ALU de 24 bits capable d'effectuer des opérations arithmétiques basiques. Fait étonnant, l'ALU faisait tous les calculs en même temps et fournissait ses résultats dans 7 registres : un registre SUM pour l'addition, un registre DIFFERENCE pour la soustraction, et un registre par opération bit à bit. Les registres pour les opérations bit à bit sont : CMPX et CMPY pour l'opération NOT sur chaque opérande, XANY pour le ET bit à bit, XEOY pour le XOR, XORY pour le OU logique. Les additions et soustractions pouvaient se faire sur des opérandes codés en binaire ou en BCD, suivant le code opération utilisé. A cela, il fallait ajouter deux registres de "résultat" MSKX et MSKY. Ils fournissaient un masque dépendant de la taille du résultat en bits. Cela permettait de gérer des données de 0, 4, 8, 12, 16, 20, et 24 bits, alors que l'ALU faisait 24 bits. Et cela n'était pas limité à 16 ou 8 bits, mais n'importe quelle taille inférieure ou égale à 24 bits était supportée, même des tailles non-conventionnelles comme des résultats codés sur 3 bits, 9 bits, 12 bits, etc. L'ALU prenait en opérande les registres X et Y, mais aussi un registre CYF pour la retenue entrante. En sortie, il fournissait aussi deux retenue sortantes : une pour l'addition et une autre pour la soustraction. Elles étaient mémorisées dans les registres CYL et CYD, respectivement. L'ALU fournissait aussi 12 conditions, qui alimentait un registre d'état. Le registre d'état était segmenté en trois registres appelés XYST, XYCN et BICN. Le processeur contenait de nombreux autres registres de 4 bits, dont les sous-registres de T et L. En tout, il y a 27 registres de 4 bits adressables ! Et le processeur disposait d'une ALU 4 bits pour manipuler ces registres de 4 bits ! Elle est capable de faire des MOV entre registres de 4 bits, des opérations bit à bit, et peut aussi tester si un bit vaut 0 ou 1. La dernière possibilité est très utile pour implémenter les branchements. Pour communiquer avec les entrées-sorties, il disposait de deux registres CMND et DATA : CMND pour envoyer une commande à une entrée-sortie, DATA pour lire ou écrire une donnée. Pour la mémoire, il avait deux registres séparés : READ et WRITE. READ contient la donnée lue lors d'une lecture,; WRITE est pour une donnée à écrire lors d’une écriture. Il s'agit de registres d’interfaçage mémoire, qui ont été promus au rang de registres architecturaux. Les deux registres sont gérés par le microcode, qui est le langage principal de la machine. Les instructions machines configurées font sans, mais le microcode doit y avoir accès facilement. Et vu que le microcode est le langage principal de ce processeur, ces deux registres deviennent architecturaux. ===Le Burroughs B1700 : le mal-nommé ''split-level control store''=== Le Burroughs B1700 avait plusieurs modèles : le B1726, le B1710 et le B1800. Ils se distinguaient par la manière dont le code machine source était traduit par le traducteur binaire. Dans ce qui suit, nous parlerons de traducteur binaire pour parler soit d'un interpréteur pour un langage de programmation de haut niveau, soit pour un logiciel de traduction binaire. Le B1710 plaquait le traducteur binaire en mémoire RAM. Le B1800 faisait pareil, mais avait ajouté un cache dédié au code du traducteur binaire, afin d'améliorer les performances. Le B1726 avait un traducteur binaire scindé en deux : une partie était en mémoire RAM, l'autre était dans un ''local store'' dédié. Le processeur peut exécuter le traducteur binaire soit depuis le ''local store'', soit depuis la mémoire RAM avec une pénalité en termes de performances. La documentation appelle cela un '''''split-level control store''''', mais le terme est trompeur : la documentation qualifie de microcode le langage machine du Burroughs B1700, ce qui est à l'origine de beaucoup de confusions. Le programmeur devait découper le traducteur binaire en segments, certains étant placé dans le ''local store'', les autres étant en mémoire RAM. Mieux que ça, le processeur autorisait d'utiliser la technique de l'''overlaying'' pour le microcode ! Il y avait même une fonction OVERLAY dédiée pour ! OVERLAY avait besoin de trois opérandes : la taille du segment à copier, son adresse source en mémoire RAM, son adresse de destination dans la micro-SRAM. Le ''local store'' mémorisait au maximum 2048 instructions machine, ce qui fait que les 2048 premières "adresses" correspondaient au ''local store''. Les suivantes étaient des adresses de mémoire RAM qui contenaient le reste du traducteur binaire. En pratique, l'adresse qui séparait les deux était configurable ! On pouvait limiter la taille du ''local store'' à un multiple de 32 instructions ! Les adresses restantes étaient alors réattribuées à la RAM. Par exemple, on pouvait attribuer 64 adresses au ''local store'', les 2048 - 64 adresses restantes étaient alors attribuées à la mémoire RAM. Un registre limite, nommé TDPM, indiquait quelle était l'adresse de démarcation entre les deux. Il contenait 4 bits, dont la valeur était multipliée par 32, pour obtenir l'adresse limite. L'interpréteur réservait un "segment" en mémoire RAM, pour les données, dont le processeur connaissait la position. En-dehors de ce segment, il n'y a que des instructions machines à traduire, et le traducteur binaire. Le CPU incorporait pour cela deux registres, un pour l'adresse de base du segment de données, un autre pour son adresse de fin. Il incorporait une protection mémoire limitée sur ce segment, à savoir que seul l'interpréteur pouvait lire ou écrire dans ce segment. ===Une architecture bit-adressable=== Le CPU Burroughs B1700 était '''bit-adressable'''. En clair, il pouvait adresser la mémoire bit par bit, si nécessaire. L'avantage est que cela permettait d'émuler des architectures dont la taille des registres/''byte''/mots était très différente. Un autre avantage est que cela facilitait la traduction binaire des instructions machine. Détaillons rapidement le premier avantage. Le processeur était un CPU 24 bits, mais il pouvait émuler des processeurs 16 bits, 8 bits, 4 bits, 1 bit, 9 bits, ou toute autre valeur. Pour cela, le processeur lisait des mots de 24 bits et utilisant un ''barrel shifter'' pour sélectionner les bits adéquats. Par exemple, pour un accès de 16 bits, la donnée lue en mémoire était masquée de manière à ne garder que les 16 bits de poids faible. Le ''barrel shifter''/circuit de masquage était placé directement avant le bus mémoire, sur le trajet des données/instructions. Le ''barrel shifter'' était commandé par un registre ''Field Unit'', qui précisait quelle était la taille des données à charger. De plus, le processeur avait une '''ALU de taille variable'''. En clair, l'ALU pouvait faire des calculs sur un nombre de bits compris entre 0 et 24. Pour cela, l'ALU était une ALU de 24 bits classique, à laquelle on a jouté des capacités de masquage. L'ALU pouvait masquer les bits de poids fort du résultat, si besoin. ===Le code machine du Burroughs B1700=== Le processeur utilise des micro-opérations de 16 bits de long, qui sont copiées dans registre d'instruction M de 16 bits. Fait intéressant, il était possible de faire un OU logique entre l'instruction lue et un registre du processeur, le résultat étant mémorisé dans le registre M. Cela permettait de faire du code automodifiant, pour émuler des modes d'adressage complexe. Cela permettait par exemple de remplacer une adresse dans une micro-opération. L'adresse est alors surimposée avec un OU dans le champ d'adresse, alors que le reste de l'instruction n'est pas modifié. Afin de gérer les interruptions, ces dernières étaient partiellement détectées en logiciel. Les interruptions ne s'exécutaient pas automatiquement, pour émuler correctement les interruptions. Une interruption doit être prise en compte à la toute fin de l'exécution d'une instruction, quand celle-ci a finit son travail. Mais vu qu'ici, les instructions sont émulées avec des séries de micro-opérations, il y a un problème : il est possible d'interrompre une instruction émulée en plein milieu. Pour éviter cela, le processeur ne déclenche pas les interruptions matérielles immédiatement, elles sont mises en attente. Une micro-opération dédiée vérifie si une interruption est en attente, et l'exécute alors cas échéant. La micro-opération est utilisée comme micro-opération finale d'une instruction émulée. Le ''program counter'' est appelé le registre A. Les instructions du processeur font 16 bits de long, et elles sont alignées en RAM sur 16 bits, ce qui fait que les 4 bits de poids fort de ce registre valent 0, seuls les 14 bits de poids fort sont utiles. Les adresses mentionnées plus haut, qui adressent les micro-opérations en mémoire RAM, sont des adresses de mots de 16 bits, pas les adresses de bit individuels. ===La traduction binaire est aidée par des registres et instructions dédiées=== Les instructions à décoder n'ont pas de longueur fixe, à savoir qu'elles peuvent faire 16 bit avec tel jeu d'instruction, 8 bits avec un autre, 32 bits sur un autre, etc. Elles peuvent aussi être de longueur fixe ou variable ! Le B1700 intégrait donc un support des instructions de longueur variable, qui était "détourné" pour gérer des jeux d'instruction différents. Notons que vu que l'architecture est bit-adressable, les contraintes d'alignement des instructions disparaissent, ce qui a des avantages qu'on va détailler dans ce qui suit ! L'implémentation des instructions de taille variable ajoutait un registre avant le registre d'instruction. Le registre d'instruction proprement dit était un registre M de 24 bits. Il était précédé par un registre T, dans lequel l'instruction était assemblée, en combinant plusieurs morceaux. Si l'instruction est codée est codée sur moins de 24 bits, le registre T se contente de masquer les bits en trop. Si l'instruction chargée dépasser 24 bits, le chargement se faisait en plusieurs passes, avec plusieurs lectures en mémoire. Dans ce qui suit, on suppose que l'instruction est composée de '''champs''' : l'opcode, les opérandes, les adresses absolues, les constantes immédiates, les numéros de registre, etc. L'idée est que l'instruction est chargée dans le registre T, puis découpée en champs, qui sont copiés dans les registres du processeur ou envoyés au microcode. Je rappelle que le registre T est relié à un ''barrel shifter'', différent au précédent, qui permettait de masquer, décaler et extraire une donnée de ce registre. Et il y a la possibilité d'extraire une suite de bit de ce registre T, pour la copier dans un autre registre. Par exemple, prenons une instruction composée d'un opcode, d'une constante immédiate et d'une adresse immédiate. L'opcode est lu et interprété par le microcode. Le microcode extrait la constante immédiate du registre T et la copie dans le registre X. Ensuite, l'adresse est extraite et envoyée au registre FA, une lecture est démarrée, et l'opérande est copiée dans le registre Y. Enfin, l'instruction est exécutée par le microcode. Le tout prend plusieurs micro-opérations : une première pour lire l'opcode, une seconde pour lire la première opérande, une troisième pour lire la seconde opérande, una quatrième pour tout exécuter. Notons que suivant la taille des champs, on peut en charger plusieurs à la fois. Par exemple, imaginons qu'on ait chargé 24 bits, contenant un opcode de 8 bits et une constante immédiate de 16 bits : pas besoin de faire deux accès mémoire, le registre T est juste découpé en un opcode et une constante. Le registre T est fait pour ce genre d'extraction. ===Le chargement d'un champ=== L'architecture est bit-adressable car elle gére des champs qui ne font pas forcément 8 ou 16 bits. Par exemple, certains jeux d'instructions ont des opcodes codés sur 6 ou 7 bits, voire 9-10 bits. Par exemple, prenons un opcode de 7 bits, suivi par une adresse de 12 bits et une constante immédiate de 15 bits : aucun de ces champs ne sera aligné sur 8/16/24 bits. Utiliser une architecture bit-adressable règle le problème d'alignement. Et en général, les contraintes d'alignement des instructions disparaissent ! Quand on charge une instruction, peut importe qu'elle soit ou non alignée sur 24 bits, elle sera placée dans les bits de poids faible, il suffira de masquer les bits de poids fort inutiles. Par exemple, si on charge une instruction de 16 bits, les 16 bits seront copiés dans les 16 bits de poids faible du registre T, il suffira de masquer l'octet de poids fort. Pas besoin de faire de décalages pour gérer des instructions mal alignées... Pour charger une instruction, il faut connaitre plusieurs informations : l'adresse de son premier bit, sa taille. Pour cela, le B1700 contient deux registres, nommés ''Field Adress'' (FA) et ''Field Length'' (FL), qui indiquent l'adresse du premier bit et la taille de l'instruction à charger. Le premier fait 24 bits, ce qui permet de gérer des adresses de 24 bits. N'oubliez pas que le processeur est bit-adressable, ce qui permet d'adresser 16 Mébi-bits, soit 2 mébioctets. Le registre pour la taille fait lui 16 bits, ce qui permet de gérer des instructions de 65536 bits, soit 8192 octets ! Leur nom commence par ''field'', car le processeur peut gérer des instructions de taille variable et/ou plus longues que 24 bits. Dans ce cas, le chargement de l'instruction se fait champ par champ. Pour donner un exemple, prenons une instruction composée de 20 champs de 12 bits chacun. Les champs sont chargés un par un dans le registre T. Le registre FA est alors initialisé avec l'adresse de l'instruction, il est incrémenté de 12 à chaque cycle. Le registre FL est initialisé avec la taille de l'instruction, soit 20 * 12 = 240 bits, et il est décrémenté de 16 à chaque cycle. Précisons cependant que la taille d'un champ peut être changée d'un cycle à l'autre. Par exemple, le processeur peut charger un opcode de 8 bits, l'interpréter, détecter que l'instruction demande ensuite de charger une adresse de 16 bits. Dans ce cas, le registre FL est immédiatement altéré pour charger 16 bits, au lieu des 8 bits de l'opcode immédiatement précédent. ==Les jeux d'instructions dédiés à un langage de programmation== De rares processeurs sont conçus pour un langage de programmation en particulier. On appelle ces processeurs, conçus pour des besoins particuliers, des '''processeurs dédiés'''. Par exemple, les fameux ''Burrough E-mode'' B5000/B6000/B7000 étaient spécialement conçus pour exécuter de l'ALGOL-60. Leurs cousins B2000/B3000/B4000 étaient eux conçus pour le COBOL. Des langages fonctionnels ont aussi eu droit à leurs processeurs dédiés. Le prolog en est un bel exemple, avec les superordinateurs de 5ème génération qui lui étaient dédié. On peut aussi citer les machines LISP, dédiés au langage LISP, qui datent des années 1970. Elles étaient capables d’exécuter certaines fonctions de base du langage directement dans leurs circuits : elles possédaient notamment un ''garbage collector'' câblé dans ses circuits ainsi que des instructions machines supportant un typage déterminé à l’exécution. Les processeurs dédiés ont eu leur heure de gloire au début de l'informatique, à une époque où les langages de haut niveau venaient d'être inventés. À cette époque, les compilateurs n'étaient pas performants et ne savaient pas bien optimiser le code machine. Il était alors rationnel, pour l'époque, de rapprocher le code machine cible et le langage de programmation de haut niveau. De nombreuses architectures dédiés ont ainsi été inventées, avant que les concepteurs se rendent compte des défauts de cette approche. Les défauts en question ne sont pas nombreux, mais assez simples à comprendre. Premièrement, elles sont très rapides pour un langage de programmation en particulier, mais sont assez mauvaises pour les autres, d'où un problème de "compatibilité". Ajoutons à cela que les langages de programmation peuvent évoluer, devenir de moins en moins populaires/utilisés, ce qui rend la création d'architectures généralistes plus pertinente. Enfin, les architectures dédiées sont évidemment des processeurs CISC, pour implémenter les nombreuses fonctionnalités des langages évolués. Et les défauts des CISC sont assez rédhibitoires à l'heure actuelle. À l'heure actuelle, les algorithmes des compilateurs se sont améliorés et savent nettement mieux utiliser le matériel. Ils produisent du code machine efficace, ce qui rend les architecture dédiées bien moins intéressantes. Si on ajoute les défauts de ces architectures dédiées, par étonnant que les architectures dédiées aient presque disparues. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'accélération matérielle de la virtualisation | prevText=L'accélération matérielle de la virtualisation | next=Le matériel réseau | nextText=Le matériel réseau }} </noinclude> ih1nslz9j37zplb2c220xa201sr0f04 764045 764044 2026-04-19T20:04:11Z Mewtow 31375 /* Le Burroughs B1700 : architecture externe */ 764045 wikitext text/x-wiki De nos jours, la majorité des programmeurs programment dans des langages de haut niveau. Il est très rare pour eux d'avoir à utiliser de l'assembleur, et encore moins un langage machine. Les programmes écrits dans un langage de haut niveau sont traduits en langage machine par un logiciel appelé le compilateur. [[File:Compilation.PNG|centre|vignette|upright=2.5|Compilateur : principe.]] Il se trouve que quelques rares processeurs sont conçus pour faciliter le travail du compilateur. Et ce sont ces processeurs que nous allons voir dans ce qui suit. Ils représentent une catégorie de jeux d'instruction à part, qui n'a pas vraiment de nom. ==La compilation et l’interprétation== Un compilateur traduit donc du code source, écrit dans un langage de haut niveau, vers du langage machine. La traduction est rarement directe. En général, le compilateur traduit le code source en assembleur, qui est lui-même traduit en langage machine. L'assembleur est une représentation textuelle du langage machine alors que le code machine est du binaire exécutable par le processeur. La traduction se fait donc en deux étapes, la '''compilation''' proprement dite et l''''assemblage''', réalisées respectivement par un compilateur et un assembleur. Il faut parfois rajouter une troisième phase d'édition des liens, que nous passons volontairement sous silence. {| |[[File:Code C.png|vignette|200px|Le code source, ici en C.]] |[[File:Assem.png|vignette|250px|Le code assembleur.]] |[[File:Binary file - hello world (C programming).png|vignette|150px|Le code machine.]] |} ===L'interprétation et la compilation à la volée=== Une alternative à la compilation est l''''interprétation''', qui transforme le code source en code machine à la volée. Avec l'interprétation, le code source est passé à un logiciel appelé l''''interpréteur''', qui exécute le code source ligne de code par ligne de code. Une ligne de code est traduite en code machine, qui est exécutée, puis l'interpréteur passe à la ligne suivante. [[File:Kaantotulkkaus.png|centre|vignette|upright=3|Compilation versus interprétation.]] Un défaut de l'interprétation est son cout en performance, qui est assez important. Aussi, l'interprétation stricte a évolué vers un hybride entre interprétation et compilation. L'idée est qu'une partie peu importante du code source est interprétée, alors que le code important est compilé juste avant d'être exécuté. Pour le dire autrement, le code source est partiellement compilé à la volée, juste avant son exécution. Aussi, on parle de '''compilation à la volée'''. Le terme en anglais est ''Just In Time compilation'', abrévié en JIT. En soi, le JIT est associé à de l'interprétation, les JIT sont en réalité des hybrides interpréteurs-compilateurs. Un point important est que compiler du code à la volée est assez lourd, cela a un cout en performance. Par contre, le code compilé s'exécute plus vite que du code interprété. Si on exécute le code compilé une seule fois, le cout de la compilation l'emporte sur le gain à l'exécution. Mais le code compilé est réutilisé autant de fois que nécessaire. Le gain à l'exécution est donc multiplié par le nombre d'exécutions, alors que le cout de la compilation est répartit sur plusieurs exécutions, il est amorti. Les portions du code sont choisies de manière à avoir un gain en performance maximal. L'idée générale est que le code exécuté très souvent est compilé, alors que du code exécuté pas souvent est interprété. Typiquement, les boucles les plus souvent exécutées sont compilées, le code exécuté une seule fois est interprété. ===Les langages intermédiaires=== Les compilateurs modernes passent par un '''langage intermédiaire''' pour faire la transformation en code machine. Le compilateur traduit le langage de haut niveau en langage intermédiaire, puis traduit le langage intermédiaire en code machine cible. Le langage intermédiaire est parfois appelé le '''''bytecode''''', et ce terme recouvre aussi le code écrit avec du ''bytecode''. L'interprétation aussi peut passer par l'intermédiaire d'un un ''bytecode'', ce n'est pas limité aux compilateurs proprement dit. {| |[[File:Intermediate representation scheme.png|centre|vignette|upright=1.5|Représentation intermédiaire d'un compilateur.]] |[[File:Code interpreter scheme.png|centre|vignette|upright=1.5|Fonctionnement d'un interpréteur.]] |} Faire ainsi a de nombreux avantages, le principal étant d'avoir un compilateur capable de traduire un langage de haut niveau vers plusieurs jeux d'instructions différents. Cela permet d'avoir, par exemple, un compilateur qui traduit du C soit en code machine x86, soit en code machine pour un CPU ARM, soit pour un CPU POWERPC, etc. [[File:Compiler design.svg|centre|vignette|upright=3|Compiler design]] Pour l'interprétation, l'usage d'un ''bytecode'' a d'autres avantages. Pour l'exploiter, il faut distribuer non pas le code source, mais le ''bytecode''. En clair, l'interpréteur ne prend pas en entrée le code source, mais du ''bytecode'' déjà compilé. La traduction est alors beaucoup plus simple, car le code source a déjà été partiellement compilé, par la traduction en ''bytecode''. Interpréter du code source est en effet assez compliqué : il faut effectuer des étapes d'analyse lexicale, sémantique, et bien d'autres. Avec du ''bytecode'', ces étapes ont été réalisées lors de la compilation du ''bytecode'', l'interpréteur a alors peut de choses à faire. L'interprétation du ''bytecode'' se fait instruction par instruction, au niveau du ''bytecode''. Une instruction du ''bytecode'' est traduite en une instruction machine équivalente, qui est exécutée. Si l'instruction est un peu complexe, il exécute une fonction/procédure qui fait la même chose. Pour résumer, le ''bytecode'' est ensuite traduit à la volée et exécuté instruction par instruction par un logiciel appelé l'interpréteur. [[File:Control table.png|centre|vignette|upright=2|Control table]] Le langage intermédiaire peut être vu comme l'assembleur d'un processeur, n'existe pas forcément dans la réalité, mais dont le jeu d'instruction est décrit en détail. Le processeur en question est appelé une '''machine abstraite''', ou encore une ''machine virtuelle''. Nous utiliserons le terme de machine abstraite dans ce qui suit. La machine abstraite n'est pas la même suivant que l'on cible la compilation ou l'interprétation/JIT. Pour l'interprétation/JIT, la machine abstraite est souvent une machine à pile, car cela simplifie grandement la traduction en code machine final. L'interprétation demande que la traduction du ''bytecode'' en code machine soit la plus simple possible. Et cela demande de prendre en compte pas mal de détails. Par exemple, les différents jeux d'instruction existants n'ont pas le même nombre de registres, ce qui pose problème lors de la traduction du ''bytecode''. Lors de la transformation en code machine, un algorithme d'allocation de registres se débrouille pour traduire le code intermédiaire en code qui utilise un nombre limité de registres. Pour améliorer cette allocation de registres, il y a deux solutions niveau ''bytecode'' : soit utiliser un nombre illimité de registres, soit utiliser une machine à pile. Pour les machines à pile, il existe un algorithme simple et rapide pour traduire un code écrit pour une machine à pile en un code écrit pour un processeur avec des registres, qui se débrouille pas trop mal pour allouer efficacement les registres. C'est un avantage assez important pour les langages interprétés. ==Les processeurs qui exécutent du bytecode== Le ''bytecode'' est un code machine, ce qui signifie qu'il peut en théorie s'exécuter sur un processeur qui implémente le jeu d’instruction associé. Si le jeu d'instruction d'un ''bytecode'' est souvent une description censée être fictive, elle n'en reste pas moins un jeu d'instruction et des caractéristiques précises, qu'on peut l'implémenter en matériel ! Néanmoins, tous les ''bytecode'' ne sont pas égaux de ce point de vue. Certains sont faciles à implémenter en matériel, d'autres non. Les ''bytecodes'' implémentés en matériel sont ceux dont les machines abstraites sont des machines à pile. Un premier exemple est celui des processeurs Pascal MicroEngine, qui exécutaient directement le bytecode du langage Pascal, le fameux UCSD P-code. Un second exemple est la '''machine SECD''', qui sert de langage intermédiaire pour certains compilateurs de langages fonctionnels. Elle a été implémentée en matériel par plusieurs équipes, notamment par les chercheurs de l'université de Calgary en 1989. Dans le même genre, quelques processeurs simples étaient capables d’exécuter directement le ''bytecode'' du langage FORTH. Le FORTH, un des premiers langages à pile de haut niveau, possède de nombreuses implémentations hardware et est un des rares langages de haut niveau dont le ''bytecode'' a été utilisé comme langage machine sur certains processeurs. Par exemple, on peut citer le processeur FC16, capable d’exécuter nativement du ''bytecode'' FORTH. ===Les processeurs Java=== Le cas le plus impressionnant est celui de la machine virtuelle Java, qui est un design de processeur comme un autre. En temps normal, le ''bytecode'' Java est compilé ou interprété, mais certains processeurs exécutaient du ''bytecode'' Java directement, sans interprétation ni compilation. Ils sont appelés des '''processeurs Java'''. On peut citer les processeurs ARM disposant de l'extension Jazelle, du JEMCore et du aJ-100 de aJile Systems, le picoJava II de Sun, et quelques autres. Il y a aussi eu quelques projets de recherche ou processeurs open source, comme Komodo, jamuth, le ''Java Optimized Processor'' et quelques autres projets du même genre. L'implémentation de la machine virtuelle Java n'était cependant pas complète. Quelques instructions complexes n'étaient pas gérées par le processeur et devaient être émulées en logiciel. Mais la grosse majorité des instructions l'était. Les instructions non-supportées étaient émulées avec la méthode ''trap and emulate'', à savoir qu'un opcode inconnu déclenchait une exception matérielle ''opcode invalide'', dont la routine pouvait émuler les instructions qui doivent l'être. Le processeur aJ-100 de aJile est capable d'exécuter la quasi-totalité des instructions de la JVM Java, seules deux instructions faisant exception. Les '''accélérateurs Java''' sont une solution intermédiaire, qui permet de traduire à la volée du ''bytecode'' Java en instructions RISC, exécutées sur un autre processeur. Il s'agit formellement de traduction binaire, mais je préfère en parler ici. L'idée est de combiner un processeur RISC avec un coprocesseur Java. Le coprocesseur lit le ''bytecode'' Java et le traduit en instructions RISC, ces dernières étant exécutées sur le processeur RISC. Le JA108 de Nozomi était un coprocesseur de ce genre, au même titre que le coprocesseur JSTAR de JEDI. L'extension Jazelle de certains CPU ARM fonctionnait vraisemblablement sur un principe similaire, sauf que le coprocesseur était intégré dans le processeur ARM, entre le cache d'instruction et le décodeur d'instructions ARM. L'intérêt des processeurs Java n'est pas qu'une question de performance. Certes, ils permettent d’exécuter plus vite les programmes compilés en ''bytecode'', comme des programmes Java pour la JVM Java. Mais l'intérêt est aussi de faire des économies de mémoire RAM et ROM. Je rappelle que la machine virtuelle Java est une machine à pile, ce qui fait que sa densité de code est excellente. Les programmes en ''bytecode'' Java sont donc très petits, et prennent peu de place en mémoire ROM. De plus, utiliser un processeur Java permet de se passer d'interpréteur ou de JIT Java, ce qui économise encore plus de mémoire. ===Transformer un processeur RISC en machine à pile=== La plupart des processeurs Java utilisent en interne une architecture à registre généraux, qui émule une machine à registre en microcode. Quelques registres sont réservés pour l'état de la machine virtuelle, avec au minimum un registre réservé pour le pointeur de pile, un autre pour un pointeur vers un ''pool'' de constantes, et quelques autres. D'autres registres servent à mémoriser le haut de la pile, à savoir le sommet de la pile et quelques opérandes situées juste en-dessous. Par exemple, le processeur picoJava II gardait les 64 opérandes au sommet de la pile dans 64 registres généraux, séparés des registres pour les pointeurs et l'état de la JVM. Une optimisation possible, envisagée sur les processeurs picoJava, était une sorte de macro-fusion sous stéroïde. L'idée était de fusionner certaines séries d'instructions Java en une seule instruction machine. Typiquement, pour faire une opération arithmétique, une machine à pile empile deux opérandes et exécute une opération, ce qui prend trois instructions machine. La spécification picoJava 1 a proposé d'envoyer directement la seconde opérande en entrée de l'ALU, ce qui fusionnait l'empilement de la seconde opérande et l'opération arithmétique. Le processeur picoJava II va plus loin et fusionne les trois instructions en une seule instruction RISC, agissant sur des registres. Mais la majorité des processeurs Java ne faisaient pas cela, car cela complexifie grandement le décodeur. Les processeurs ''bigfoot'' de DCT utilisaient un système différent pour émuler une pile avec des registres. Leur banc de registre de 64 bits était coupé en deux : les 16 premiers registres étaient utilisés pour la pile, les 48 suivants étaient utilisés pour autre chose. L'idée est que le registre 0 mémorise un emplacement vide, le registre 1 contient le sommet de la pile, le registre 2 l'opérande sous le sommet de la pile, et ainsi de suite. Les 15 premières opérandes de la pile sont donc mémorisées dans les registres. Les instructions écrivent leur résultat dans le registre adéquat, puis tous les registres sont décalés pour que le résultat passe dans le registre R1. Par exemple, les instructions PUSH chargent une opérande dans le registre 0, puis tous les registres sont décalés de un rang, pour que l'opérande chargée soit dans le registre 1. Le registre 0 devient alors le registre numéro 1, le registre 1 devient le 2, et ainsi de suite. Par contre, les opérations arithmétiques font autrement. Une instruction d'addition, par exemple, lit les opérandes dans le registre 1 et 2, puis place son résultat dans le registre 2 (les deux opérandes sont dépilées). Puis, les registres sont décalés d'un rang pour que ce registre 2 devienne le 1. Les registres ne sont en réalité pas décalés, il n'y a pas de transferts entre registres. A la place, les 16 premiers registres utilisaient une forme limitée de renommage de registres, similaire au banc de registre tournant utilisé sur les processeurs EPIC comme l'Itanium. Les numéros de registres sont renommés, décalés, à chaque fois qu'on empile ou dépile une opérande. Pour faire le renommage de registres, processeur contient un ''register stack counter'' de 4 bits, qui est incrémenté à chaque fois qu'on empile une donnée, et est décrémenté quand on dépile une donnée. Quand on veut accéder à un opérande, le numéro de registre architectural associé est traduit en numéro de registre physique, en additionnant le ''register stack counter'' (modulo 16, pour rester sur 4 bits). ==L'accélération de l'interprétation/JIT : le ''Thumb-EE'' d'ARM== Quelques processeurs ont ajouté des instructions pour faciliter le travail des interpréteurs/JIT. Par exemple, en 2005, ARM a ajouté le mode ''Thumb Execution Environment'', qui faisait cela. Il reprenait le jeu d'instruction compact ''thumb'' et ajoutait quelques instructions et modifiait le comportement d'instructions existantes. Le jeu d'instruction ''Thumb-EE'' a été ajouté sur les CPU ARM en 2005, mais a été déprécié en 2011, par manque d'utilité. Mais la tentative mérite qu'on s'attarde dessus. Les CPU de gamme M, déstinés à l'embarqué, n'ont jamais supporté ''Thumb-EE'', vu que de tels processeurs ne sont pas conçus pour exécuter du code interprété/JIT. Le mode ''Thumb-EE'' est un mode d'exécution, séparé de l'ARM normal. Ainsi, une instruction ''Thumb-EE'' et une instruction ARM peuvent avoir le même encodage en binaire, mais se comporter différemment. Le processeur est à tout moment soit en mode ''Thumb-EE'', soit en mode ARM normal, ce qui précise comment ces instructions doivent se comporter. Des instructions ont été ajoutées par ''Thumb-EE'', pour qu'il fasse son travail. L'entrée et la sortie du mode ''Thumb-EE'' se fait avec deux instructions : ENTERX et LEAVEX. Une fois en mode ''Thumb-EE'', le décodage des instructions ''thumb'' se fait avec les règles du ''Thumb-EE'', et non celles du ''thumb'' normal. Une première différence entre ''thumb'' normal et ''Thumb-EE'' est l'ajout de ce qui s'appelle un ''null check'' pour les instructions mémoire. Le ''null check'' est utilisé pour les instructions en mode d'adressage "base + indice" ou "Base + décalage", ou tout autre mode d'adressage avec un registre de base utilisés dans des calculs d'adresse. L'idée est de vérifier si l'adresse de base vaut zéro ou non. Si c'est le cas, le processeur lève une exception matérielle, qui est traitée par l'interprétation ou le compilateur JIT. Une telle situation est en effet signe d'une erreur d'adressage mémoire, qui doit être traitée par l'interpréteur. Typiquement, c'est signe qu'un pointeur n'a pas bien été initialisé, ce qui peut arriver avec du code interprété ou JIT. Les interpréteurs/JIT traitent généralement la situation en ajoutant un test avant toute instruction mémoire, pour vérifier si le pointeur accédé vaut zéro ou non. Avec ''Thumb-EE'', pas besoin d'ajouter les tests en questions : ils sont réalisés automatiquement lors de chaque instruction mémoire. : Les registres pour la pile, comme le pointeur de pile et le pointeur de ''frame'', sont aussi concernés. L'instruction CHKA vérifie que les accès à un tableau ne débordent pas en dehors du tableau en question. Par exemple, pour un tableau de 1024 éléments, elle vérifie si l'indice est compris entre 0 et 1024. Pour cela, elle vérifie si l'indice de tableau est bien dans l'intervalle adéquat et lève une exception matérielle si ce n'est pas le cas. Son utilité se comprend quand on sait que tous les accès à un tableau sont vérifiés par l'interpréteur. L'interpréteur est censé ajouter des instructions pour vérifier les indices, à savoir deux ou trois branchements. Avec CHKA, le test se fait en une seule instruction. D'autres modifications mineures de l'encodage des instructions sont aussi présentes, ainsi que des modifications pour les instructions LOAD/STORE. Précisément, dans les modes "base + indice" et "base + décalage", le décalage et l'indice subissent maintenant des décalages. L'idée est de simplifier le calcul d'adresse. De quoi économiser quelques instructions lors des calculs d'adresse, ce qui facilite le travail de l'interpréteur/JIT. Et des instructions LOAD/STORE précises voient leur comportement modifié pour faciliter la gestion de la pile de Java ou l'exécution des méthodes locales. Pour faciliter le travail de l'interpréteur, deux instructions HBP et HBLP ont été ajoutées. Elles branchent vers une fonction qui gère les exceptions logicielles (une fonctionnalité présente dans de nombreux langages, comme Java). La première effectue un branchement vers la fonction en question, la seconde sauvegarde l'adresse de retour avant de faire ce branchement. Les deux peuvent brancher vers 256 fonctions pré-déterminées, sans avoir à présenter leur adresse. ==La traduction binaire accélérée par le matériel== Plus haut, nous avons surtout parlé des compilateurs et des interpréteurs. Cependant, nous devons aussi parler de la '''traduction binaire'''. Elle traduit un programme écrit dans un code machine vers un autre code machine. Par exemple, elle traduit un programme compilé pour un CPU x86 vers un code machine ARM. En général, le système d'exploitation est généralement compilé pour le jeu d'instruction natif, mais il exécute des applications prévues pour le x86. Les applications sont traduites par le système d'exploitation, avant d'exécuter le code traduit. : Nous parlerons dans la suite de code machine source et de code machine cible, pour parler respectivement du code à traduire et du code obtenu après traduction. Idem avec d'autres termes comme architecture cible/source, ou jeu d'instruction cible/source. La traduction binaire est surtout utilisée pour des questions d'émulation ou de compatibilité. Par exemple, lorsque les Macintosh sont passés de processeurs Power PC vers des processeurs x86, le système d'exploitation Mac OS utilisait la traduction binaire pour convertir les anciennes applications Power PC vers du code x86. Les utilisateurs n'y ont vu que du feu. Le système de traduction binaire était appelé Rosetta 1. Par la suite, lors de la transition de processeurs x86 vers des processeurs Apple, Rosetta 2 a vu le jour. La traduction binaire a surtout été utilisée pour traduire du code x86 vers un autre jeu d'instruction. Il faut dire que le x86 est le jeu d'instruction dominant. De nombreuses entreprises ont eu pour ambition de briser l'hégémonie du x86 sur PC, en remplaçant le x86 par un jeu d'instruction plus performant, tout en gardant une compatibilité maximale. La traduction binaire était la seule solution pratique. Elle était le plus souvent intégralement réalisée en logiciel, comme c'était le cas sur les architectures Itanium avec le ''IA-32 Execution Layer''. ===Les généralités sur la traduction binaire assistée en matériel=== L'architecture source est presque tout le temps une architecture CISC, assez ancienne, qu'on souhaite émuler. Le choix de l'architecture cible se porte souvent sur une architecture VLIW, fort différente de l'architecture source, et ce pour obtenir une économie de matériel conséquente. Pour s'exécuter rapidement, le code traduit doit exploiter le parallélisme d'instruction, à savoir exécuter plusieurs instructions en même temps dans des unités de calcul séparées. Il est possible d'utiliser un processeur superscalaire avec exécution dans le désordre pour cela, mais au prix d'un cout important en transistors. Alors qu'en utilisant un CPU VLIW, c'est le traducteur binaire qui fait tout le travail d'extraction du parallélisme d'instruction. Et après tout, quitte à avoir un traducteur binaire, autant lui refiler le boulot d'optimisation. Compiler du code à la volée est certes assez lourd, mais qu'il existe des algorithmes efficaces pour regrouper des instructions indépendantes dans une seule instruction VLIW. Au passage, le processeur VLIW a plus de registres que le processeur source. Cela permet de faire du renommage de registres directement en logiciel. Le traducteur binaire n'hésite pas à changer les noms de registres entre instructions source et instruction VLIW cible, afin de supprimer des dépendances de données. En conséquence, cela ouvre des opportunités de parallélisme, qui sont exploitées lors du regroupement des opérations en instructions VLIW. Vous pourriez penser que le choix d'un CPU VLIW pour émuler du CISC est tout sauf optimal. Vous devez penser que la traduction binaire est d'autant plus simple que l'architecture source et cibles sont semblables. Dans les faits, ce n'est pas tellement traduire les instructions qui pose problème, mais plus la gestion du registre d'état, des exceptions matérielles, de la mémoire virtuelle, la différence entre gros-boutisme et petit-boutisme, de même que des différences d'adressage pour les périphériques. Le premier problème est la gestion des conditions, notamment en présence d'un registre d'état. En pratique, la traduction binaire s'utilise pour traduire du code CISC vers du code RISC ou VLIW. Les architectures source ont donc un registre d'état, qui est mis à jour non seulement par des instructions de test, mais aussi des instructions arithmétiques. L'architecture cible n'a elle pas de registre d'état, mais des registres à prédicats. La traduction de l'un vers l'autre est alors quelque peu compliquée. Et elle est d'autant plus compliquée que le registre d'état est la source de dépendances d'instruction implicites, qui réduisent les performances. Par exemple, si une instruction arithmétique modifie le registre d'état, cela peut impacter l'exécution d'une instruction ultérieure, qui lit ce registre d'état. Pour éliminer ces fausses dépendances, le traducteur binaire doit renommer ce registre en logiciel et détecter les dépendances utiles. Et le compilateur doit gérer les cas où il y a beaucoup de distance entre les deux, voire les cas où l'instruction dépendante n'a pas encore été analysée par le compilateur. Un autre problème est lié à la gestion des exceptions matérielles, et précisément des exceptions précises. Pour rappel, une instruction VLIW regroupe plusieurs opérations, opérations qui correspondraient à une instruction machine sur un CPU pas VLIW. En conséquence, plusieurs instructions du langage machine "source" sont regroupées en une seule instruction VLIW. Et il faut tenir compte du cas où une instruction source lève une exception. Dans le code VLIW, cela signifie qu'une opération lève une exception, et il faut annuler partiellement l'instruction VLIW associée. Et par partiellement, on veut dire que seules les opérations suivantes dans l'ordre du programme source doivent être annulées, puis ré-exécutées. Et c'est un sacré casse-tête ! Les processeurs VLIW utilisent comme solution le mécanisme d'exceptions différées des processeurs EPIC, vu il y a quelques chapitres. Pour résumer, le code est exécuté par blocs d'instruction, délimités par des branchements ou tout autre limite/barrière pertinente dans le code. Un bloc de code est compilé en deux versions : une version rapide sans exceptions matérielles, une version lente qui gère les exceptions précises. La version rapide s'exécute sans exécuter les exceptions. Cependant, les exceptions matérielles sont enregistrées, pour être prises en compte à la toute fin du bloc de code. La version rapide est exécutée en premier et elle mémorise si une exception matérielle a eu lieu. Puis une instruction vérifie si une exception a eu lieu et décide quoi faire. Si aucune exception n'a eu lieu, elle passe à la suite du programme, les résultats du bloc de code sont définitivement acceptés. Mais si une exception a eu lieu, tout est annulé. Le processeur est remis dans l'état initial, puis le code est ré-exécuté instruction par instruction de manière à gérer l'exception correctement. ===La traduction par pré-décodage=== Une première solution serait de faire la traduction binaire dans le cache d'instruction. Lors d'un défaut de cache, le code chargé depuis la RAM est traduit en code machine cible. Et c'est ce code machine cible qui est mémorisé dans le cache d'instruction et exécuté par le processeur. La technique marche sur le papier et n'est qu'une amélioration des techniques de pré-décodage vues il y a quelques chapitres. Cependant, le code obtenu est une traduction assez basique, qui n'incorpore pas d'optimisations dignes de ce nom. De plus, elle gère mal le cas où la taille du code source et cible sont potentiellement très différentes. Concrètement, elle est surtout utile pour traduire du code d'un processeur RISC vers un autre processeur RISC, dont les tailles d'instructions sont similaires. Elle ne permet pas d'émuler plusieurs jeux d'instructions différents, le cout en matériel (un circuit de pré-décodage par jeu d'instruction) serait trop important. ===Le projet DAISY d'IBM=== De très rares processeurs étaient conçus pour accélérer cette traduction binaire, afin de garder de bonnes performances. Les premiers à avoir étudié l'idée étaient IBM, avec leur projet DAISY (''Dynamically Architected Instruction Set from Yorktown''). Le projet de base était de convertir à la volée du code compilé pour des CPU Power PC, vers du code VLIW. Les chercheurs d'IBM avaient développé un algorithme de traduction dynamique efficace, ainsi qu'un processeur VLIW disposant d'optimisations spécifiques à la traduction binaire dynamique, à la volée. De nombreuses idées de ce projet ont été reprises ou re-découvertes par la société Transmetta, avec ses processeurs Crusoe et Efficieon, puis par NVIDIA avec son projet Denver. Les ingénieurs de ce projet ont étudié la possibilité d'émuler plusieurs jeux d'instructions différents sur un même processeur, notamment le s390 d'IBM et le x86. En théorie, cela demande juste d'avoir plusieurs programmes de traduction binaire : un pour le x86, un autre pour le s390, éventuellement un autre pour le Power PC. Mais cela ne s'est pas concrétisé. Mais les ingénieurs ont étudié quelle pourrait être l'architecture VLIW idéale pour ça. Par exemple, il fallait des additions 3-opérandes pour simplifier les calculs d'adresse. Une difficulté était la gestion du registre d'état, dont les bits ne sont pas mis à jour de la même manière sur le x86 et le s390 ou le Power PC. La différence entre gros-boutisme et petit-boutisme était aussi un problème, de même que des différences d'adressage pour les périphériques. Pour que la traduction binaire soit efficace, le processeur VLIW intègre diverses optimisations, comme les exceptions différées et les branchements multi-voies. La principale est la suivante : seul le code des boucles ou fonctions exécutées fréquemment est traduit en code VLIW, le reste du code est interprété. En effet, traduire du code binaire prend plus de temps qu'une simple interprétation. Pour du code qui ne sera exécutée qu'une seule fois, il est plus rapide d'utiliser l'interprétation. Par contre, pour du code exécuté beaucoup de fois, le cout de la traduction binaire est amorti, dilué sur N exécutions, compensé par le gain en temps d'exécution de ce code traduit. Au final, cela permet de ne traduire que le code qui le mérite. La détection du code fréquemment exécuté est réalisée dans le cœur VLIW. L'unité de branchement mémorise les derniers branchements rencontrés et le nombre de fois qu'ils ont été exécutés. Si ils ont été exécutés un certain nombre de fois, l'unité de branchement lève une exception matérielle, qui invoque le traducteur binaire. La routine de cette exception est le traducteur binaire proprement dit. En clair, la traduction binaire est démarrée quand le processeur détecte qu'une fonction a été exécutée plus de N fois, via une exception matérielle dédiée. La mesure du nombre d'exécution d'un branchement se fait dans l'unité de calcul dédiée aux branchements, avec l'aide d'une sorte de ''branch adress buffer'' modifié. Pour rappel, le ''branch adress buffer'' mémorise le ''Program counter'' de chaque branchement récemment rencontré. Ici, chaque entrée du ''branch adress buffer'' est associée à un compteur incrémenté à chaque exécution du branchement. ===Les processeurs Crusoe et Efficieon de Transmetta=== Les processeurs Crusoe Et Efficeron sont deux processeurs VLIW produits par la société Transmetta, une société californienne rachetée par NVIDIA. Ils étaient conçus pour exécuter spécifiquement des programmes x86, système d'exploitation inclus. L'idée derrière ce projet était d'exécuter du code x86 sans que les applications, ni même le système d'exploitation et le BIOS soient au courant ! Les processeurs Transmetta étaient en réalité conçus pour exécuter un programme unique, le ''Code Morphing Software'' (CMS), qui traduisait le code x86 en code VLIW. Transmetta ne voulait pas que son processeur VLIW soit exploité directement, sans le CMS. Il n'avait pas rendu public de compilateur pour traduire du C vers du code VLIW, il n'avait pas donné la documentation du jeu d'instruction VLIW. Des efforts de rétro-ingénieurie, documentés sur le site realworldtech, ont cependant permis de comprendre comment étaient encodées les instructions du processeur Crusoe. Comme pour le projet Daisy, le ''Code Morphing Software'' utilisait à la fois interprétation et traduction binaire, selon les besoins. Les instructions sont interprétées lors de leurs premières exécutions, mais le CMS bascule sur de la traduction en code VLIW après un certain nombre d'exécution. Ainsi, les boucles souvent exécutées sont traduites en code VLIW, alors que le reste du code est interprété. Le choix du code à traduire est le fait du CMS, il utilise des heuristiques complexes pour, qui ne sont pas connues dans le détail. Le ''Code Morphing Software'' est mémorisé dans une EEPROM, ce qui en fait un ''firmware'' situé en dessous du BIOS. Le CMS démarre ensuite le BIOS, qui lui-même démarre le système d'exploitation, qui lui-même démarre les pilotes de périphériques et les programmes. Le CMS se réserve les 16 premiers mébioctets de l'espace d'adressage. L'EEPROM du CMS est mappée dedans, mais n'en utilise que la moitié. Le reste est utilisé comme cache, pour mémoriser le code VLIW traduit par le CMS. Les deux processeurs intégraient aussi deux mémoires SRAM, utilisées par le CMS, appelées la ''local program memory'' (LPM) et la ''local data memory'' (LDM). La première contient du code qui gére les interruptions, la mémoire virtuelle, les exceptions matérielles, les problèmes d'alignement mémoire, et quelques fonctions très fréquemment utilisées par le CMS. La seconde est de la mémoire RAM utilisée par ces fonctions de la ''local program memory''. Les mémoires caches L1 et L2 sont séparées de ces deux mémoires. Le processeur Crusoe et Efficieon étaient des processeurs VLIW très simples. Ils n'avaient même pas de MMU, ni de fonctionnalités importantes sur les CPU x86. Pour Crusoe, les instructions VLIW étaient encodées sur 64 ou 128 bits, et regroupaient entre 2 à 4 opérations. Les instructions LOAD/STORE ne supportaient pas d'adressage indicé, juste de l'adressage indirect à registre. Crusoe avait 5 unités de calcul : deux ALU entières, une unité LOAD/STORE, une unité de branchement, une FPU. Efficieon doublait le nombre d'ALU entière et d'unité LOAD/STORE, les instructions VLIW passaient à 256 bits. Le processeur Crusoe contient 160 registres, dont 64 registres généraux, 32 registres flottants. Les registres généraux font 32 bits, ce qui est cohérent avec le fait que les CPU x86 de l'époque étaient des processeurs 32 bits. Quant aux registres flottants, ils faisaient 80 bits, ce qui colle avec la taille des registres flottants de la FPU x87 utilisée à l'époque. Sur les 64 registres généraux, seuls 48 étaient réellement utilisable pour mémoriser des opérandes. Une partie des registres généraux étaient utilisés pour la gestion de la pile, d'autres pour mémoriser l'état du CPU x86 émulé, un registre était un registre zéro non accesible en écriture. La gestion des exceptions est optimisée avec un système d'exceptions différées, le même que celui décrit plus haut. Le processeur utilise un système similaire, pour effectuer des lectures anticipées, qui a été expliqué dans le chapitre sur les processeurs VLIW/EPIC. Sauf qu'il s'agit là d'une optimisation pour gagner en performances en général, pas une aide utile pour gérer des exceptions précises ou toute autre subtilité de la traduction binaire. Pour gérer les exceptions différées et les lectures anticipées, le processeur mémorise l'état du processeur avant de démarrer un bloc de code. Pour cela, le processeur contient des ''shadow registers'', 48 registres entiers et 16 registres flottants. Vous remarquerez qu'il y a autant de registres entiers que de ''shadow registers'' entiers. Les registres entiers/flottants sont mémorisés dans les ''shadow registers'' avant d'exécuter un bloc de code, afin que le processeur puisse revenir à l'état de base. Une autre fonctionnalité liée est le ''gated store buffer''. Le principe est d'éviter toute écriture en mémoire RAM, tant que le code VLIW n'a pas émis d'instruction ''commit''. L'idée est que les écritures sont conservées dans la ''store queue'' de l'unité LOAD/STORE. Pour rappel, la ''store queue'' met en attente les écritures soit tant que les écritures ne sont pas terminées, soit tant que la RAM est occupée. L'idée est que les écritures sont accumulées dans la ''store queue'' tant que l'instruction ''commit'' n'a pas eu lieu. Le ''store queue'' est "déversé dans la RAM" seulement quand l'instruction ''commit'' s'exécute. ===Les CPU NVIDIA Denver=== Nvidia a tenté quelque chose de similaire avec son projet Denver. NVIDIA a racheté l'entreprise Transmetta, ses brevets, et a tenté de refaire la même chose. Le projet initial était de traduire du code x86 vers un jeu d'instruction VLIW propriétaire, comme l'a fait Transmetta. Mais NVIDIA n'a pas réussi à acquérir la licence du jeu d'instruction x86 et s'est rabattu sur le jeu d'instruction ARM. Le processeur né de ce projet est le Tegra K1-64 CPU. Il s'agit d'un processeur composé d'un cœur VLIW, couplé à deux décodeurs d'instructions ARM. Les deux décodeurs ARM traduisent une paire d'instructions ARM consécutives en instructions VLIW, pour les exécuter sur le cœur VLIW. Le CPU peut fonctionner selon deux modes : ARM et VLIW. En somme, le processeur supporte deux jeux d'instruction : le jeu d'instruction ARM, et un jeu d'instruction VLIW. En mode ARM, les instructions ARM sont chargées depuis le cache, traduites en VLIW par les décodeurs ARM, puis exécutées par le cœur VLIW. En mode optimisé, le CPU exécute des instructions VLIW chargées depuis le cache d'instruction, les décodeurs ne sont pas utilisés. : Le CPU supporte en réalité deux jeux d'instruction ARM : ARM8 et 7. Mais ils correspondent au même mode pour le CPU. Un point important est que le processeur peut changer très rapidement de mode, en à peine quelques cycles d'horloges. On n'est clairement pas dans le cas des CPU x86, qui mettent des plombes pour passer du mode 32 à 64 bits et inversement. La commutation est tellement rapide qu'on peut considérer que le processeur supporte deux jeux d'instruction simultanément. Et le support simultané de deux jeux d'instruction facilite l'implémentation de la traduction binaire. Encore une fois, inutile de traduire du code qui ne sera exécutée qu'une seule fois, mieux vaut l'exécuter directement dans le mode ARM. Surtout que le processeur supporte un mode ARM qui permet d'exécuter du code ARM sans perte de performance. Par contre, le code fréquemment utilisé, notamment dans des boucles critiques, est traduit en VLIW. Au final, les performances sont optimisées, en limitant le travail du logiciel traducteur binaire. La détection du code fréquemment exécuté se fait différemment que pour les processeurs Transmetta et Daisy. L'unité de branchement ne détecte pas directement le code beaucoup exécuté, même si elle a un rôle à jouer. A la place, la détection est réalisée en logiciel, par un ''thread'' dédié, qui s'exécute sur un cœur séparé. Il détecte le code fréquemment utilisé en regardent les branchements exécutés récemment. Pour cela, le processeur mémorise l'historique des branchements pris récemment et copie cette historique dans une mémoire tampon dédiée, partagée entre tous les cœurs. Vu qu'elle est partagée, le ''thread'' de détection a accès à l'historique sans pertes de performances, seule la copie de l'historique dans ce tampon a un cout en performance, pas son partage. Le code VLIW obtenu après traduction binaire est mémorisé dans la mémoire RAM, dans une portion spécialement réservée pour. Elle fait 128 mébioctets, et est appelée le '''cache d'optimisation''' par NVIDIA. Le cache d'optimisation est protégé en écriture et seul le traducteur binaire peut écrire dedans. Au passage, le traducteur binaire est du code VLIW, ce qui fait qu'il est placé dans le cache d'optimisation. Le processeur détecte automatiquement quand une fonction pour laquelle le code compilé équivalent est disponible. Pour cela, l'unité de chargement contient une table de correspondance entre l'adresse de la fonction ARM, et l'adresse de son équivalent compilé. Quand une fonction est appelée, l'unité de chargement regarde l'adresse de destination du branchement. Si l'adresse est dans cette table, elle récupère l'adresse de la fonction compilée et branche vers celle-ci. Le processeur dispose d'un cache d'instruction de 128 kibioctets, ce qui est très important, mais nécessaire vu la taille des instructions VLIW. Le coeur VLIW dispose de ses propres décodeurs, d'un ''scoreboard'', de 7 unités de calcul et de registres. Pour les unités de calcul, il a deux FPU, deux ALU, une unité de branchement, et deux unités LOAD-STORE faisant aussi ALU. Un point important est que le CPU dispose de 64 registres entiers et de 64 registres flottants. C'est deux fois plus que supporte le jeu d'instruction ARM. En mode ARM, seule la moitié des registres est utilisée. Mais en mode optimisé, le traducteur binaire utilise bien les 64 registres, grâce à une sorte de renommage de registres logicielle. Pour simplifier la traduction binaire, le processeur supporte les techniques vues précédemment. Le processeur intègre un ''gated store buffer'' similaire à celui des processeurs Transmetta. Les techniques d'exceptions différées sont aussi supportées, comme sur les processeurs Transmetta. Il a aussi une unité de préchargement décrite par NVIDIA comme agressive, avec support du préchargement de type anticipé (''runahead''). ===L'émulation de la mémoire virtuelle=== Pour finir, il faut parler de la mémoire virtuelle avec la traduction binaire. Sur le processeur source, le processeur gère à la fois des adresses physiques et virtuelles et sa MMU fait la conversion entre les deux. Sur le processeur VLIW, la MMU est simulée par le programme de traduction binaire, partiellement ou totalement. L'implémentation exacte varie suivant que l'on parle du projet Daisy, des processeurs Transmetta ou de Denver. Sur les processeurs Transmetta, il n'y a pas de MMU ni de mémoire virtuelle. A la place, le programme de traduction binaire émule la mémoire virtuelle du processeur source. Le processeur VLIW ne gère que des adresses physiques, rien d'autre. L'espace d'adressage physique du processeur VLIW a la même taille que celui du processeur émulé, ici des processeurs x86. Il est vraisemblable que les adresses physiques utilisées par le processeur x86 sont les mêmes que celles du processeur VLIW. Sur le projet Daisy, le processeur VLIW gère la mémoire virtuelle via pagination, comme les processeurs émulés. Ce qui fait qu'il gère un espace d'adressage virtuel et un espace d'adressage physique. Pour éviter toute confusion, nous parlerons d'adresse physique/virtuelle VLIW pour les adresses physiques/virtuelles du processeur VLIW, d'adresse virtuelle/physique source pour celle du jeu d'instruction traduit, à savoir du Power PC ou du s390, éventuellement de l'x86. Il faut alors faire le lien entre adresses physiques source et adresses virtuelles VLIW. Pour cela, rien de plus simple : il y a correspondance parfaite. L'adresse physique source numéro N correspond à l'adresse virtuelle VLIW numéro N. Cependant, cela signifie que tout l'espace d'adressage virtuel VLIW serait utilisé par le code à traduire. En réalité, il faut ajouter de la place pour le code traduit et le programme de traduction binaire. La conséquence est que l'espace d'adressage virtuel VLIW est plus large que l'espace d'adressage source. L'espace d'adressage virtuel VLIW est découpé en trois sections : une section pour le code Power PC à traduire, une autre pour le code traduit en VLIW, et une dernière réservée au traducteur binaire. Le programme de traduction est placé dans une ROM mappée en mémoire dans la seconde section. Le reste de la seconde section est réservé à une mémoire RAM utilisée par le programme de traduction. [[File:Mémoire virtuelle sur le projet Daisy.png|centre|vignette|upright=1.5|Mémoire virtuelle sur le projet Daisy]] Il faut noter que chaque page physique source, une fois traduite, correspond à N pages virtuelles VLIW. La raison est que le code traduit prend plus de place que le code machine originel. En conséquence, on doit utiliser une page finale plus grande. Pour le reste, des pages contiguës en mémoire physique source sont elles aussi contiguës en mémoire virtuelle VLIW. Le calcul d'adresse est donc simple : l'adresse physique source est multipliée par N, puis on ajoute l'adresse de base à laquelle commence la section pour le code traduit. ==La traduction binaire sur le Burroughs B1700== Nous venons de voir comment accélérer en matériel la traduction binaire, avec des techniques bien spécifiques. Le Burroughs B1700 a procédé autrement et il est intéressant d'étudier en détail son architecture. Son architecture est décrite dans le livre "Interpreting Machines : Architecture and Programming of the Bl700/Bl800 Series", écrit par Elliott I. Organick et James A. Hinds. Une partie de ce qui vfa suivre est un très court résumé de ce livre. ===Le Burroughs B1700 : architecture externe=== Il disposait de 4 registres généraux, nommés X, Y, T et L, tous de 24 bits. Les registres T et L étaient découpés en 6 sous-registres de 4 bits, nommés LA LB LC LD LE et LF pour le registre L, et TA TB TC TD TE et TF pour le registre T. Ils étaient adressables comme n'importe quel registre. Un point important est que le registre T avait des possibilités d’extraction spécifiques. Il était possible d'extrait N bits, placés n'importe où dans ce registre, et de les copier dans un autre registre. Nous verrons pourquoi cette possibilité est intéressante et ne sert que pour le registre T. Les registres X et Y étaient reliés à une ALU de 24 bits capable d'effectuer des opérations arithmétiques basiques. Fait étonnant, l'ALU faisait tous les calculs en même temps et fournissait ses résultats dans 7 registres : un registre SUM pour l'addition, un registre DIFFERENCE pour la soustraction, et un registre par opération bit à bit. Les registres pour les opérations bit à bit sont : CMPX et CMPY pour l'opération NOT sur chaque opérande, XANY pour le ET bit à bit, XEOY pour le XOR, XORY pour le OU logique. Les additions et soustractions pouvaient se faire sur des opérandes codés en binaire ou en BCD, suivant le code opération utilisé. A cela, il fallait ajouter deux registres de "résultat" MSKX et MSKY. Ils fournissaient un masque dépendant de la taille du résultat en bits. Cela permettait de gérer des données de 0, 4, 8, 12, 16, 20, et 24 bits, alors que l'ALU faisait 24 bits. L'ALU prenait en opérande les registres X et Y, mais aussi un registre CYF pour la retenue entrante. En sortie, il fournissait aussi deux retenue sortantes : une pour l'addition et une autre pour la soustraction. Elles étaient mémorisées dans les registres CYL et CYD, respectivement. L'ALU fournissait aussi 12 conditions, qui alimentait un registre d'état. Le registre d'état était segmenté en trois registres appelés XYST, XYCN et BICN. Le processeur contenait de nombreux autres registres de 4 bits, dont les sous-registres de T et L. En tout, il y a 27 registres de 4 bits adressables ! Et le processeur disposait d'une ALU 4 bits pour manipuler ces registres de 4 bits ! Elle est capable de faire des MOV entre registres de 4 bits, des opérations bit à bit, et peut aussi tester si un bit vaut 0 ou 1. La dernière possibilité est très utile pour implémenter les branchements. Pour communiquer avec les entrées-sorties, il disposait de deux registres CMND et DATA : CMND pour envoyer une commande à une entrée-sortie, DATA pour lire ou écrire une donnée. Pour la mémoire, il avait deux registres séparés : READ et WRITE. READ contient la donnée lue lors d'une lecture,; WRITE est pour une donnée à écrire lors d’une écriture. Il s'agit de registres d’interfaçage mémoire, qui ont été promus au rang de registres architecturaux. Les deux registres sont gérés par le microcode, qui est le langage principal de la machine. Les instructions machines configurées font sans, mais le microcode doit y avoir accès facilement. Et vu que le microcode est le langage principal de ce processeur, ces deux registres deviennent architecturaux. ===Le Burroughs B1700 : le mal-nommé ''split-level control store''=== Le Burroughs B1700 avait plusieurs modèles : le B1726, le B1710 et le B1800. Ils se distinguaient par la manière dont le code machine source était traduit par le traducteur binaire. Dans ce qui suit, nous parlerons de traducteur binaire pour parler soit d'un interpréteur pour un langage de programmation de haut niveau, soit pour un logiciel de traduction binaire. Le B1710 plaquait le traducteur binaire en mémoire RAM. Le B1800 faisait pareil, mais avait ajouté un cache dédié au code du traducteur binaire, afin d'améliorer les performances. Le B1726 avait un traducteur binaire scindé en deux : une partie était en mémoire RAM, l'autre était dans un ''local store'' dédié. Le processeur peut exécuter le traducteur binaire soit depuis le ''local store'', soit depuis la mémoire RAM avec une pénalité en termes de performances. La documentation appelle cela un '''''split-level control store''''', mais le terme est trompeur : la documentation qualifie de microcode le langage machine du Burroughs B1700, ce qui est à l'origine de beaucoup de confusions. Le programmeur devait découper le traducteur binaire en segments, certains étant placé dans le ''local store'', les autres étant en mémoire RAM. Mieux que ça, le processeur autorisait d'utiliser la technique de l'''overlaying'' pour le microcode ! Il y avait même une fonction OVERLAY dédiée pour ! OVERLAY avait besoin de trois opérandes : la taille du segment à copier, son adresse source en mémoire RAM, son adresse de destination dans la micro-SRAM. Le ''local store'' mémorisait au maximum 2048 instructions machine, ce qui fait que les 2048 premières "adresses" correspondaient au ''local store''. Les suivantes étaient des adresses de mémoire RAM qui contenaient le reste du traducteur binaire. En pratique, l'adresse qui séparait les deux était configurable ! On pouvait limiter la taille du ''local store'' à un multiple de 32 instructions ! Les adresses restantes étaient alors réattribuées à la RAM. Par exemple, on pouvait attribuer 64 adresses au ''local store'', les 2048 - 64 adresses restantes étaient alors attribuées à la mémoire RAM. Un registre limite, nommé TDPM, indiquait quelle était l'adresse de démarcation entre les deux. Il contenait 4 bits, dont la valeur était multipliée par 32, pour obtenir l'adresse limite. L'interpréteur réservait un "segment" en mémoire RAM, pour les données, dont le processeur connaissait la position. En-dehors de ce segment, il n'y a que des instructions machines à traduire, et le traducteur binaire. Le CPU incorporait pour cela deux registres, un pour l'adresse de base du segment de données, un autre pour son adresse de fin. Il incorporait une protection mémoire limitée sur ce segment, à savoir que seul l'interpréteur pouvait lire ou écrire dans ce segment. ===Une architecture bit-adressable=== Le CPU Burroughs B1700 était '''bit-adressable'''. En clair, il pouvait adresser la mémoire bit par bit, si nécessaire. L'avantage est que cela permettait d'émuler des architectures dont la taille des registres/''byte''/mots était très différente. Un autre avantage est que cela facilitait la traduction binaire des instructions machine. Détaillons rapidement le premier avantage. Le processeur était un CPU 24 bits, mais il pouvait émuler des processeurs 16 bits, 8 bits, 4 bits, 1 bit, 9 bits, ou toute autre valeur. Pour cela, le processeur lisait des mots de 24 bits et utilisant un ''barrel shifter'' pour sélectionner les bits adéquats. Par exemple, pour un accès de 16 bits, la donnée lue en mémoire était masquée de manière à ne garder que les 16 bits de poids faible. Le ''barrel shifter''/circuit de masquage était placé directement avant le bus mémoire, sur le trajet des données/instructions. Le ''barrel shifter'' était commandé par un registre ''Field Unit'', qui précisait quelle était la taille des données à charger. De plus, le processeur avait une '''ALU de taille variable'''. En clair, l'ALU pouvait faire des calculs sur un nombre de bits compris entre 0 et 24. Pour cela, l'ALU était une ALU de 24 bits classique, à laquelle on a jouté des capacités de masquage. L'ALU pouvait masquer les bits de poids fort du résultat, si besoin. ===Le code machine du Burroughs B1700=== Le processeur utilise des micro-opérations de 16 bits de long, qui sont copiées dans registre d'instruction M de 16 bits. Fait intéressant, il était possible de faire un OU logique entre l'instruction lue et un registre du processeur, le résultat étant mémorisé dans le registre M. Cela permettait de faire du code automodifiant, pour émuler des modes d'adressage complexe. Cela permettait par exemple de remplacer une adresse dans une micro-opération. L'adresse est alors surimposée avec un OU dans le champ d'adresse, alors que le reste de l'instruction n'est pas modifié. Afin de gérer les interruptions, ces dernières étaient partiellement détectées en logiciel. Les interruptions ne s'exécutaient pas automatiquement, pour émuler correctement les interruptions. Une interruption doit être prise en compte à la toute fin de l'exécution d'une instruction, quand celle-ci a finit son travail. Mais vu qu'ici, les instructions sont émulées avec des séries de micro-opérations, il y a un problème : il est possible d'interrompre une instruction émulée en plein milieu. Pour éviter cela, le processeur ne déclenche pas les interruptions matérielles immédiatement, elles sont mises en attente. Une micro-opération dédiée vérifie si une interruption est en attente, et l'exécute alors cas échéant. La micro-opération est utilisée comme micro-opération finale d'une instruction émulée. Le ''program counter'' est appelé le registre A. Les instructions du processeur font 16 bits de long, et elles sont alignées en RAM sur 16 bits, ce qui fait que les 4 bits de poids fort de ce registre valent 0, seuls les 14 bits de poids fort sont utiles. Les adresses mentionnées plus haut, qui adressent les micro-opérations en mémoire RAM, sont des adresses de mots de 16 bits, pas les adresses de bit individuels. ===La traduction binaire est aidée par des registres et instructions dédiées=== Les instructions à décoder n'ont pas de longueur fixe, à savoir qu'elles peuvent faire 16 bit avec tel jeu d'instruction, 8 bits avec un autre, 32 bits sur un autre, etc. Elles peuvent aussi être de longueur fixe ou variable ! Le B1700 intégrait donc un support des instructions de longueur variable, qui était "détourné" pour gérer des jeux d'instruction différents. Notons que vu que l'architecture est bit-adressable, les contraintes d'alignement des instructions disparaissent, ce qui a des avantages qu'on va détailler dans ce qui suit ! L'implémentation des instructions de taille variable ajoutait un registre avant le registre d'instruction. Le registre d'instruction proprement dit était un registre M de 24 bits. Il était précédé par un registre T, dans lequel l'instruction était assemblée, en combinant plusieurs morceaux. Si l'instruction est codée est codée sur moins de 24 bits, le registre T se contente de masquer les bits en trop. Si l'instruction chargée dépasser 24 bits, le chargement se faisait en plusieurs passes, avec plusieurs lectures en mémoire. Dans ce qui suit, on suppose que l'instruction est composée de '''champs''' : l'opcode, les opérandes, les adresses absolues, les constantes immédiates, les numéros de registre, etc. L'idée est que l'instruction est chargée dans le registre T, puis découpée en champs, qui sont copiés dans les registres du processeur ou envoyés au microcode. Je rappelle que le registre T est relié à un ''barrel shifter'', différent au précédent, qui permettait de masquer, décaler et extraire une donnée de ce registre. Et il y a la possibilité d'extraire une suite de bit de ce registre T, pour la copier dans un autre registre. Par exemple, prenons une instruction composée d'un opcode, d'une constante immédiate et d'une adresse immédiate. L'opcode est lu et interprété par le microcode. Le microcode extrait la constante immédiate du registre T et la copie dans le registre X. Ensuite, l'adresse est extraite et envoyée au registre FA, une lecture est démarrée, et l'opérande est copiée dans le registre Y. Enfin, l'instruction est exécutée par le microcode. Le tout prend plusieurs micro-opérations : une première pour lire l'opcode, une seconde pour lire la première opérande, une troisième pour lire la seconde opérande, una quatrième pour tout exécuter. Notons que suivant la taille des champs, on peut en charger plusieurs à la fois. Par exemple, imaginons qu'on ait chargé 24 bits, contenant un opcode de 8 bits et une constante immédiate de 16 bits : pas besoin de faire deux accès mémoire, le registre T est juste découpé en un opcode et une constante. Le registre T est fait pour ce genre d'extraction. ===Le chargement d'un champ=== L'architecture est bit-adressable car elle gére des champs qui ne font pas forcément 8 ou 16 bits. Par exemple, certains jeux d'instructions ont des opcodes codés sur 6 ou 7 bits, voire 9-10 bits. Par exemple, prenons un opcode de 7 bits, suivi par une adresse de 12 bits et une constante immédiate de 15 bits : aucun de ces champs ne sera aligné sur 8/16/24 bits. Utiliser une architecture bit-adressable règle le problème d'alignement. Et en général, les contraintes d'alignement des instructions disparaissent ! Quand on charge une instruction, peut importe qu'elle soit ou non alignée sur 24 bits, elle sera placée dans les bits de poids faible, il suffira de masquer les bits de poids fort inutiles. Par exemple, si on charge une instruction de 16 bits, les 16 bits seront copiés dans les 16 bits de poids faible du registre T, il suffira de masquer l'octet de poids fort. Pas besoin de faire de décalages pour gérer des instructions mal alignées... Pour charger une instruction, il faut connaitre plusieurs informations : l'adresse de son premier bit, sa taille. Pour cela, le B1700 contient deux registres, nommés ''Field Adress'' (FA) et ''Field Length'' (FL), qui indiquent l'adresse du premier bit et la taille de l'instruction à charger. Le premier fait 24 bits, ce qui permet de gérer des adresses de 24 bits. N'oubliez pas que le processeur est bit-adressable, ce qui permet d'adresser 16 Mébi-bits, soit 2 mébioctets. Le registre pour la taille fait lui 16 bits, ce qui permet de gérer des instructions de 65536 bits, soit 8192 octets ! Leur nom commence par ''field'', car le processeur peut gérer des instructions de taille variable et/ou plus longues que 24 bits. Dans ce cas, le chargement de l'instruction se fait champ par champ. Pour donner un exemple, prenons une instruction composée de 20 champs de 12 bits chacun. Les champs sont chargés un par un dans le registre T. Le registre FA est alors initialisé avec l'adresse de l'instruction, il est incrémenté de 12 à chaque cycle. Le registre FL est initialisé avec la taille de l'instruction, soit 20 * 12 = 240 bits, et il est décrémenté de 16 à chaque cycle. Précisons cependant que la taille d'un champ peut être changée d'un cycle à l'autre. Par exemple, le processeur peut charger un opcode de 8 bits, l'interpréter, détecter que l'instruction demande ensuite de charger une adresse de 16 bits. Dans ce cas, le registre FL est immédiatement altéré pour charger 16 bits, au lieu des 8 bits de l'opcode immédiatement précédent. ==Les jeux d'instructions dédiés à un langage de programmation== De rares processeurs sont conçus pour un langage de programmation en particulier. On appelle ces processeurs, conçus pour des besoins particuliers, des '''processeurs dédiés'''. Par exemple, les fameux ''Burrough E-mode'' B5000/B6000/B7000 étaient spécialement conçus pour exécuter de l'ALGOL-60. Leurs cousins B2000/B3000/B4000 étaient eux conçus pour le COBOL. Des langages fonctionnels ont aussi eu droit à leurs processeurs dédiés. Le prolog en est un bel exemple, avec les superordinateurs de 5ème génération qui lui étaient dédié. On peut aussi citer les machines LISP, dédiés au langage LISP, qui datent des années 1970. Elles étaient capables d’exécuter certaines fonctions de base du langage directement dans leurs circuits : elles possédaient notamment un ''garbage collector'' câblé dans ses circuits ainsi que des instructions machines supportant un typage déterminé à l’exécution. Les processeurs dédiés ont eu leur heure de gloire au début de l'informatique, à une époque où les langages de haut niveau venaient d'être inventés. À cette époque, les compilateurs n'étaient pas performants et ne savaient pas bien optimiser le code machine. Il était alors rationnel, pour l'époque, de rapprocher le code machine cible et le langage de programmation de haut niveau. De nombreuses architectures dédiés ont ainsi été inventées, avant que les concepteurs se rendent compte des défauts de cette approche. Les défauts en question ne sont pas nombreux, mais assez simples à comprendre. Premièrement, elles sont très rapides pour un langage de programmation en particulier, mais sont assez mauvaises pour les autres, d'où un problème de "compatibilité". Ajoutons à cela que les langages de programmation peuvent évoluer, devenir de moins en moins populaires/utilisés, ce qui rend la création d'architectures généralistes plus pertinente. Enfin, les architectures dédiées sont évidemment des processeurs CISC, pour implémenter les nombreuses fonctionnalités des langages évolués. Et les défauts des CISC sont assez rédhibitoires à l'heure actuelle. À l'heure actuelle, les algorithmes des compilateurs se sont améliorés et savent nettement mieux utiliser le matériel. Ils produisent du code machine efficace, ce qui rend les architecture dédiées bien moins intéressantes. Si on ajoute les défauts de ces architectures dédiées, par étonnant que les architectures dédiées aient presque disparues. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'accélération matérielle de la virtualisation | prevText=L'accélération matérielle de la virtualisation | next=Le matériel réseau | nextText=Le matériel réseau }} </noinclude> 1f4ei68891vmfaf30ez2osww9060zji 764046 764045 2026-04-19T20:09:12Z Mewtow 31375 /* Le Burroughs B1700 : le mal-nommé split-level control store */ 764046 wikitext text/x-wiki De nos jours, la majorité des programmeurs programment dans des langages de haut niveau. Il est très rare pour eux d'avoir à utiliser de l'assembleur, et encore moins un langage machine. Les programmes écrits dans un langage de haut niveau sont traduits en langage machine par un logiciel appelé le compilateur. [[File:Compilation.PNG|centre|vignette|upright=2.5|Compilateur : principe.]] Il se trouve que quelques rares processeurs sont conçus pour faciliter le travail du compilateur. Et ce sont ces processeurs que nous allons voir dans ce qui suit. Ils représentent une catégorie de jeux d'instruction à part, qui n'a pas vraiment de nom. ==La compilation et l’interprétation== Un compilateur traduit donc du code source, écrit dans un langage de haut niveau, vers du langage machine. La traduction est rarement directe. En général, le compilateur traduit le code source en assembleur, qui est lui-même traduit en langage machine. L'assembleur est une représentation textuelle du langage machine alors que le code machine est du binaire exécutable par le processeur. La traduction se fait donc en deux étapes, la '''compilation''' proprement dite et l''''assemblage''', réalisées respectivement par un compilateur et un assembleur. Il faut parfois rajouter une troisième phase d'édition des liens, que nous passons volontairement sous silence. {| |[[File:Code C.png|vignette|200px|Le code source, ici en C.]] |[[File:Assem.png|vignette|250px|Le code assembleur.]] |[[File:Binary file - hello world (C programming).png|vignette|150px|Le code machine.]] |} ===L'interprétation et la compilation à la volée=== Une alternative à la compilation est l''''interprétation''', qui transforme le code source en code machine à la volée. Avec l'interprétation, le code source est passé à un logiciel appelé l''''interpréteur''', qui exécute le code source ligne de code par ligne de code. Une ligne de code est traduite en code machine, qui est exécutée, puis l'interpréteur passe à la ligne suivante. [[File:Kaantotulkkaus.png|centre|vignette|upright=3|Compilation versus interprétation.]] Un défaut de l'interprétation est son cout en performance, qui est assez important. Aussi, l'interprétation stricte a évolué vers un hybride entre interprétation et compilation. L'idée est qu'une partie peu importante du code source est interprétée, alors que le code important est compilé juste avant d'être exécuté. Pour le dire autrement, le code source est partiellement compilé à la volée, juste avant son exécution. Aussi, on parle de '''compilation à la volée'''. Le terme en anglais est ''Just In Time compilation'', abrévié en JIT. En soi, le JIT est associé à de l'interprétation, les JIT sont en réalité des hybrides interpréteurs-compilateurs. Un point important est que compiler du code à la volée est assez lourd, cela a un cout en performance. Par contre, le code compilé s'exécute plus vite que du code interprété. Si on exécute le code compilé une seule fois, le cout de la compilation l'emporte sur le gain à l'exécution. Mais le code compilé est réutilisé autant de fois que nécessaire. Le gain à l'exécution est donc multiplié par le nombre d'exécutions, alors que le cout de la compilation est répartit sur plusieurs exécutions, il est amorti. Les portions du code sont choisies de manière à avoir un gain en performance maximal. L'idée générale est que le code exécuté très souvent est compilé, alors que du code exécuté pas souvent est interprété. Typiquement, les boucles les plus souvent exécutées sont compilées, le code exécuté une seule fois est interprété. ===Les langages intermédiaires=== Les compilateurs modernes passent par un '''langage intermédiaire''' pour faire la transformation en code machine. Le compilateur traduit le langage de haut niveau en langage intermédiaire, puis traduit le langage intermédiaire en code machine cible. Le langage intermédiaire est parfois appelé le '''''bytecode''''', et ce terme recouvre aussi le code écrit avec du ''bytecode''. L'interprétation aussi peut passer par l'intermédiaire d'un un ''bytecode'', ce n'est pas limité aux compilateurs proprement dit. {| |[[File:Intermediate representation scheme.png|centre|vignette|upright=1.5|Représentation intermédiaire d'un compilateur.]] |[[File:Code interpreter scheme.png|centre|vignette|upright=1.5|Fonctionnement d'un interpréteur.]] |} Faire ainsi a de nombreux avantages, le principal étant d'avoir un compilateur capable de traduire un langage de haut niveau vers plusieurs jeux d'instructions différents. Cela permet d'avoir, par exemple, un compilateur qui traduit du C soit en code machine x86, soit en code machine pour un CPU ARM, soit pour un CPU POWERPC, etc. [[File:Compiler design.svg|centre|vignette|upright=3|Compiler design]] Pour l'interprétation, l'usage d'un ''bytecode'' a d'autres avantages. Pour l'exploiter, il faut distribuer non pas le code source, mais le ''bytecode''. En clair, l'interpréteur ne prend pas en entrée le code source, mais du ''bytecode'' déjà compilé. La traduction est alors beaucoup plus simple, car le code source a déjà été partiellement compilé, par la traduction en ''bytecode''. Interpréter du code source est en effet assez compliqué : il faut effectuer des étapes d'analyse lexicale, sémantique, et bien d'autres. Avec du ''bytecode'', ces étapes ont été réalisées lors de la compilation du ''bytecode'', l'interpréteur a alors peut de choses à faire. L'interprétation du ''bytecode'' se fait instruction par instruction, au niveau du ''bytecode''. Une instruction du ''bytecode'' est traduite en une instruction machine équivalente, qui est exécutée. Si l'instruction est un peu complexe, il exécute une fonction/procédure qui fait la même chose. Pour résumer, le ''bytecode'' est ensuite traduit à la volée et exécuté instruction par instruction par un logiciel appelé l'interpréteur. [[File:Control table.png|centre|vignette|upright=2|Control table]] Le langage intermédiaire peut être vu comme l'assembleur d'un processeur, n'existe pas forcément dans la réalité, mais dont le jeu d'instruction est décrit en détail. Le processeur en question est appelé une '''machine abstraite''', ou encore une ''machine virtuelle''. Nous utiliserons le terme de machine abstraite dans ce qui suit. La machine abstraite n'est pas la même suivant que l'on cible la compilation ou l'interprétation/JIT. Pour l'interprétation/JIT, la machine abstraite est souvent une machine à pile, car cela simplifie grandement la traduction en code machine final. L'interprétation demande que la traduction du ''bytecode'' en code machine soit la plus simple possible. Et cela demande de prendre en compte pas mal de détails. Par exemple, les différents jeux d'instruction existants n'ont pas le même nombre de registres, ce qui pose problème lors de la traduction du ''bytecode''. Lors de la transformation en code machine, un algorithme d'allocation de registres se débrouille pour traduire le code intermédiaire en code qui utilise un nombre limité de registres. Pour améliorer cette allocation de registres, il y a deux solutions niveau ''bytecode'' : soit utiliser un nombre illimité de registres, soit utiliser une machine à pile. Pour les machines à pile, il existe un algorithme simple et rapide pour traduire un code écrit pour une machine à pile en un code écrit pour un processeur avec des registres, qui se débrouille pas trop mal pour allouer efficacement les registres. C'est un avantage assez important pour les langages interprétés. ==Les processeurs qui exécutent du bytecode== Le ''bytecode'' est un code machine, ce qui signifie qu'il peut en théorie s'exécuter sur un processeur qui implémente le jeu d’instruction associé. Si le jeu d'instruction d'un ''bytecode'' est souvent une description censée être fictive, elle n'en reste pas moins un jeu d'instruction et des caractéristiques précises, qu'on peut l'implémenter en matériel ! Néanmoins, tous les ''bytecode'' ne sont pas égaux de ce point de vue. Certains sont faciles à implémenter en matériel, d'autres non. Les ''bytecodes'' implémentés en matériel sont ceux dont les machines abstraites sont des machines à pile. Un premier exemple est celui des processeurs Pascal MicroEngine, qui exécutaient directement le bytecode du langage Pascal, le fameux UCSD P-code. Un second exemple est la '''machine SECD''', qui sert de langage intermédiaire pour certains compilateurs de langages fonctionnels. Elle a été implémentée en matériel par plusieurs équipes, notamment par les chercheurs de l'université de Calgary en 1989. Dans le même genre, quelques processeurs simples étaient capables d’exécuter directement le ''bytecode'' du langage FORTH. Le FORTH, un des premiers langages à pile de haut niveau, possède de nombreuses implémentations hardware et est un des rares langages de haut niveau dont le ''bytecode'' a été utilisé comme langage machine sur certains processeurs. Par exemple, on peut citer le processeur FC16, capable d’exécuter nativement du ''bytecode'' FORTH. ===Les processeurs Java=== Le cas le plus impressionnant est celui de la machine virtuelle Java, qui est un design de processeur comme un autre. En temps normal, le ''bytecode'' Java est compilé ou interprété, mais certains processeurs exécutaient du ''bytecode'' Java directement, sans interprétation ni compilation. Ils sont appelés des '''processeurs Java'''. On peut citer les processeurs ARM disposant de l'extension Jazelle, du JEMCore et du aJ-100 de aJile Systems, le picoJava II de Sun, et quelques autres. Il y a aussi eu quelques projets de recherche ou processeurs open source, comme Komodo, jamuth, le ''Java Optimized Processor'' et quelques autres projets du même genre. L'implémentation de la machine virtuelle Java n'était cependant pas complète. Quelques instructions complexes n'étaient pas gérées par le processeur et devaient être émulées en logiciel. Mais la grosse majorité des instructions l'était. Les instructions non-supportées étaient émulées avec la méthode ''trap and emulate'', à savoir qu'un opcode inconnu déclenchait une exception matérielle ''opcode invalide'', dont la routine pouvait émuler les instructions qui doivent l'être. Le processeur aJ-100 de aJile est capable d'exécuter la quasi-totalité des instructions de la JVM Java, seules deux instructions faisant exception. Les '''accélérateurs Java''' sont une solution intermédiaire, qui permet de traduire à la volée du ''bytecode'' Java en instructions RISC, exécutées sur un autre processeur. Il s'agit formellement de traduction binaire, mais je préfère en parler ici. L'idée est de combiner un processeur RISC avec un coprocesseur Java. Le coprocesseur lit le ''bytecode'' Java et le traduit en instructions RISC, ces dernières étant exécutées sur le processeur RISC. Le JA108 de Nozomi était un coprocesseur de ce genre, au même titre que le coprocesseur JSTAR de JEDI. L'extension Jazelle de certains CPU ARM fonctionnait vraisemblablement sur un principe similaire, sauf que le coprocesseur était intégré dans le processeur ARM, entre le cache d'instruction et le décodeur d'instructions ARM. L'intérêt des processeurs Java n'est pas qu'une question de performance. Certes, ils permettent d’exécuter plus vite les programmes compilés en ''bytecode'', comme des programmes Java pour la JVM Java. Mais l'intérêt est aussi de faire des économies de mémoire RAM et ROM. Je rappelle que la machine virtuelle Java est une machine à pile, ce qui fait que sa densité de code est excellente. Les programmes en ''bytecode'' Java sont donc très petits, et prennent peu de place en mémoire ROM. De plus, utiliser un processeur Java permet de se passer d'interpréteur ou de JIT Java, ce qui économise encore plus de mémoire. ===Transformer un processeur RISC en machine à pile=== La plupart des processeurs Java utilisent en interne une architecture à registre généraux, qui émule une machine à registre en microcode. Quelques registres sont réservés pour l'état de la machine virtuelle, avec au minimum un registre réservé pour le pointeur de pile, un autre pour un pointeur vers un ''pool'' de constantes, et quelques autres. D'autres registres servent à mémoriser le haut de la pile, à savoir le sommet de la pile et quelques opérandes situées juste en-dessous. Par exemple, le processeur picoJava II gardait les 64 opérandes au sommet de la pile dans 64 registres généraux, séparés des registres pour les pointeurs et l'état de la JVM. Une optimisation possible, envisagée sur les processeurs picoJava, était une sorte de macro-fusion sous stéroïde. L'idée était de fusionner certaines séries d'instructions Java en une seule instruction machine. Typiquement, pour faire une opération arithmétique, une machine à pile empile deux opérandes et exécute une opération, ce qui prend trois instructions machine. La spécification picoJava 1 a proposé d'envoyer directement la seconde opérande en entrée de l'ALU, ce qui fusionnait l'empilement de la seconde opérande et l'opération arithmétique. Le processeur picoJava II va plus loin et fusionne les trois instructions en une seule instruction RISC, agissant sur des registres. Mais la majorité des processeurs Java ne faisaient pas cela, car cela complexifie grandement le décodeur. Les processeurs ''bigfoot'' de DCT utilisaient un système différent pour émuler une pile avec des registres. Leur banc de registre de 64 bits était coupé en deux : les 16 premiers registres étaient utilisés pour la pile, les 48 suivants étaient utilisés pour autre chose. L'idée est que le registre 0 mémorise un emplacement vide, le registre 1 contient le sommet de la pile, le registre 2 l'opérande sous le sommet de la pile, et ainsi de suite. Les 15 premières opérandes de la pile sont donc mémorisées dans les registres. Les instructions écrivent leur résultat dans le registre adéquat, puis tous les registres sont décalés pour que le résultat passe dans le registre R1. Par exemple, les instructions PUSH chargent une opérande dans le registre 0, puis tous les registres sont décalés de un rang, pour que l'opérande chargée soit dans le registre 1. Le registre 0 devient alors le registre numéro 1, le registre 1 devient le 2, et ainsi de suite. Par contre, les opérations arithmétiques font autrement. Une instruction d'addition, par exemple, lit les opérandes dans le registre 1 et 2, puis place son résultat dans le registre 2 (les deux opérandes sont dépilées). Puis, les registres sont décalés d'un rang pour que ce registre 2 devienne le 1. Les registres ne sont en réalité pas décalés, il n'y a pas de transferts entre registres. A la place, les 16 premiers registres utilisaient une forme limitée de renommage de registres, similaire au banc de registre tournant utilisé sur les processeurs EPIC comme l'Itanium. Les numéros de registres sont renommés, décalés, à chaque fois qu'on empile ou dépile une opérande. Pour faire le renommage de registres, processeur contient un ''register stack counter'' de 4 bits, qui est incrémenté à chaque fois qu'on empile une donnée, et est décrémenté quand on dépile une donnée. Quand on veut accéder à un opérande, le numéro de registre architectural associé est traduit en numéro de registre physique, en additionnant le ''register stack counter'' (modulo 16, pour rester sur 4 bits). ==L'accélération de l'interprétation/JIT : le ''Thumb-EE'' d'ARM== Quelques processeurs ont ajouté des instructions pour faciliter le travail des interpréteurs/JIT. Par exemple, en 2005, ARM a ajouté le mode ''Thumb Execution Environment'', qui faisait cela. Il reprenait le jeu d'instruction compact ''thumb'' et ajoutait quelques instructions et modifiait le comportement d'instructions existantes. Le jeu d'instruction ''Thumb-EE'' a été ajouté sur les CPU ARM en 2005, mais a été déprécié en 2011, par manque d'utilité. Mais la tentative mérite qu'on s'attarde dessus. Les CPU de gamme M, déstinés à l'embarqué, n'ont jamais supporté ''Thumb-EE'', vu que de tels processeurs ne sont pas conçus pour exécuter du code interprété/JIT. Le mode ''Thumb-EE'' est un mode d'exécution, séparé de l'ARM normal. Ainsi, une instruction ''Thumb-EE'' et une instruction ARM peuvent avoir le même encodage en binaire, mais se comporter différemment. Le processeur est à tout moment soit en mode ''Thumb-EE'', soit en mode ARM normal, ce qui précise comment ces instructions doivent se comporter. Des instructions ont été ajoutées par ''Thumb-EE'', pour qu'il fasse son travail. L'entrée et la sortie du mode ''Thumb-EE'' se fait avec deux instructions : ENTERX et LEAVEX. Une fois en mode ''Thumb-EE'', le décodage des instructions ''thumb'' se fait avec les règles du ''Thumb-EE'', et non celles du ''thumb'' normal. Une première différence entre ''thumb'' normal et ''Thumb-EE'' est l'ajout de ce qui s'appelle un ''null check'' pour les instructions mémoire. Le ''null check'' est utilisé pour les instructions en mode d'adressage "base + indice" ou "Base + décalage", ou tout autre mode d'adressage avec un registre de base utilisés dans des calculs d'adresse. L'idée est de vérifier si l'adresse de base vaut zéro ou non. Si c'est le cas, le processeur lève une exception matérielle, qui est traitée par l'interprétation ou le compilateur JIT. Une telle situation est en effet signe d'une erreur d'adressage mémoire, qui doit être traitée par l'interpréteur. Typiquement, c'est signe qu'un pointeur n'a pas bien été initialisé, ce qui peut arriver avec du code interprété ou JIT. Les interpréteurs/JIT traitent généralement la situation en ajoutant un test avant toute instruction mémoire, pour vérifier si le pointeur accédé vaut zéro ou non. Avec ''Thumb-EE'', pas besoin d'ajouter les tests en questions : ils sont réalisés automatiquement lors de chaque instruction mémoire. : Les registres pour la pile, comme le pointeur de pile et le pointeur de ''frame'', sont aussi concernés. L'instruction CHKA vérifie que les accès à un tableau ne débordent pas en dehors du tableau en question. Par exemple, pour un tableau de 1024 éléments, elle vérifie si l'indice est compris entre 0 et 1024. Pour cela, elle vérifie si l'indice de tableau est bien dans l'intervalle adéquat et lève une exception matérielle si ce n'est pas le cas. Son utilité se comprend quand on sait que tous les accès à un tableau sont vérifiés par l'interpréteur. L'interpréteur est censé ajouter des instructions pour vérifier les indices, à savoir deux ou trois branchements. Avec CHKA, le test se fait en une seule instruction. D'autres modifications mineures de l'encodage des instructions sont aussi présentes, ainsi que des modifications pour les instructions LOAD/STORE. Précisément, dans les modes "base + indice" et "base + décalage", le décalage et l'indice subissent maintenant des décalages. L'idée est de simplifier le calcul d'adresse. De quoi économiser quelques instructions lors des calculs d'adresse, ce qui facilite le travail de l'interpréteur/JIT. Et des instructions LOAD/STORE précises voient leur comportement modifié pour faciliter la gestion de la pile de Java ou l'exécution des méthodes locales. Pour faciliter le travail de l'interpréteur, deux instructions HBP et HBLP ont été ajoutées. Elles branchent vers une fonction qui gère les exceptions logicielles (une fonctionnalité présente dans de nombreux langages, comme Java). La première effectue un branchement vers la fonction en question, la seconde sauvegarde l'adresse de retour avant de faire ce branchement. Les deux peuvent brancher vers 256 fonctions pré-déterminées, sans avoir à présenter leur adresse. ==La traduction binaire accélérée par le matériel== Plus haut, nous avons surtout parlé des compilateurs et des interpréteurs. Cependant, nous devons aussi parler de la '''traduction binaire'''. Elle traduit un programme écrit dans un code machine vers un autre code machine. Par exemple, elle traduit un programme compilé pour un CPU x86 vers un code machine ARM. En général, le système d'exploitation est généralement compilé pour le jeu d'instruction natif, mais il exécute des applications prévues pour le x86. Les applications sont traduites par le système d'exploitation, avant d'exécuter le code traduit. : Nous parlerons dans la suite de code machine source et de code machine cible, pour parler respectivement du code à traduire et du code obtenu après traduction. Idem avec d'autres termes comme architecture cible/source, ou jeu d'instruction cible/source. La traduction binaire est surtout utilisée pour des questions d'émulation ou de compatibilité. Par exemple, lorsque les Macintosh sont passés de processeurs Power PC vers des processeurs x86, le système d'exploitation Mac OS utilisait la traduction binaire pour convertir les anciennes applications Power PC vers du code x86. Les utilisateurs n'y ont vu que du feu. Le système de traduction binaire était appelé Rosetta 1. Par la suite, lors de la transition de processeurs x86 vers des processeurs Apple, Rosetta 2 a vu le jour. La traduction binaire a surtout été utilisée pour traduire du code x86 vers un autre jeu d'instruction. Il faut dire que le x86 est le jeu d'instruction dominant. De nombreuses entreprises ont eu pour ambition de briser l'hégémonie du x86 sur PC, en remplaçant le x86 par un jeu d'instruction plus performant, tout en gardant une compatibilité maximale. La traduction binaire était la seule solution pratique. Elle était le plus souvent intégralement réalisée en logiciel, comme c'était le cas sur les architectures Itanium avec le ''IA-32 Execution Layer''. ===Les généralités sur la traduction binaire assistée en matériel=== L'architecture source est presque tout le temps une architecture CISC, assez ancienne, qu'on souhaite émuler. Le choix de l'architecture cible se porte souvent sur une architecture VLIW, fort différente de l'architecture source, et ce pour obtenir une économie de matériel conséquente. Pour s'exécuter rapidement, le code traduit doit exploiter le parallélisme d'instruction, à savoir exécuter plusieurs instructions en même temps dans des unités de calcul séparées. Il est possible d'utiliser un processeur superscalaire avec exécution dans le désordre pour cela, mais au prix d'un cout important en transistors. Alors qu'en utilisant un CPU VLIW, c'est le traducteur binaire qui fait tout le travail d'extraction du parallélisme d'instruction. Et après tout, quitte à avoir un traducteur binaire, autant lui refiler le boulot d'optimisation. Compiler du code à la volée est certes assez lourd, mais qu'il existe des algorithmes efficaces pour regrouper des instructions indépendantes dans une seule instruction VLIW. Au passage, le processeur VLIW a plus de registres que le processeur source. Cela permet de faire du renommage de registres directement en logiciel. Le traducteur binaire n'hésite pas à changer les noms de registres entre instructions source et instruction VLIW cible, afin de supprimer des dépendances de données. En conséquence, cela ouvre des opportunités de parallélisme, qui sont exploitées lors du regroupement des opérations en instructions VLIW. Vous pourriez penser que le choix d'un CPU VLIW pour émuler du CISC est tout sauf optimal. Vous devez penser que la traduction binaire est d'autant plus simple que l'architecture source et cibles sont semblables. Dans les faits, ce n'est pas tellement traduire les instructions qui pose problème, mais plus la gestion du registre d'état, des exceptions matérielles, de la mémoire virtuelle, la différence entre gros-boutisme et petit-boutisme, de même que des différences d'adressage pour les périphériques. Le premier problème est la gestion des conditions, notamment en présence d'un registre d'état. En pratique, la traduction binaire s'utilise pour traduire du code CISC vers du code RISC ou VLIW. Les architectures source ont donc un registre d'état, qui est mis à jour non seulement par des instructions de test, mais aussi des instructions arithmétiques. L'architecture cible n'a elle pas de registre d'état, mais des registres à prédicats. La traduction de l'un vers l'autre est alors quelque peu compliquée. Et elle est d'autant plus compliquée que le registre d'état est la source de dépendances d'instruction implicites, qui réduisent les performances. Par exemple, si une instruction arithmétique modifie le registre d'état, cela peut impacter l'exécution d'une instruction ultérieure, qui lit ce registre d'état. Pour éliminer ces fausses dépendances, le traducteur binaire doit renommer ce registre en logiciel et détecter les dépendances utiles. Et le compilateur doit gérer les cas où il y a beaucoup de distance entre les deux, voire les cas où l'instruction dépendante n'a pas encore été analysée par le compilateur. Un autre problème est lié à la gestion des exceptions matérielles, et précisément des exceptions précises. Pour rappel, une instruction VLIW regroupe plusieurs opérations, opérations qui correspondraient à une instruction machine sur un CPU pas VLIW. En conséquence, plusieurs instructions du langage machine "source" sont regroupées en une seule instruction VLIW. Et il faut tenir compte du cas où une instruction source lève une exception. Dans le code VLIW, cela signifie qu'une opération lève une exception, et il faut annuler partiellement l'instruction VLIW associée. Et par partiellement, on veut dire que seules les opérations suivantes dans l'ordre du programme source doivent être annulées, puis ré-exécutées. Et c'est un sacré casse-tête ! Les processeurs VLIW utilisent comme solution le mécanisme d'exceptions différées des processeurs EPIC, vu il y a quelques chapitres. Pour résumer, le code est exécuté par blocs d'instruction, délimités par des branchements ou tout autre limite/barrière pertinente dans le code. Un bloc de code est compilé en deux versions : une version rapide sans exceptions matérielles, une version lente qui gère les exceptions précises. La version rapide s'exécute sans exécuter les exceptions. Cependant, les exceptions matérielles sont enregistrées, pour être prises en compte à la toute fin du bloc de code. La version rapide est exécutée en premier et elle mémorise si une exception matérielle a eu lieu. Puis une instruction vérifie si une exception a eu lieu et décide quoi faire. Si aucune exception n'a eu lieu, elle passe à la suite du programme, les résultats du bloc de code sont définitivement acceptés. Mais si une exception a eu lieu, tout est annulé. Le processeur est remis dans l'état initial, puis le code est ré-exécuté instruction par instruction de manière à gérer l'exception correctement. ===La traduction par pré-décodage=== Une première solution serait de faire la traduction binaire dans le cache d'instruction. Lors d'un défaut de cache, le code chargé depuis la RAM est traduit en code machine cible. Et c'est ce code machine cible qui est mémorisé dans le cache d'instruction et exécuté par le processeur. La technique marche sur le papier et n'est qu'une amélioration des techniques de pré-décodage vues il y a quelques chapitres. Cependant, le code obtenu est une traduction assez basique, qui n'incorpore pas d'optimisations dignes de ce nom. De plus, elle gère mal le cas où la taille du code source et cible sont potentiellement très différentes. Concrètement, elle est surtout utile pour traduire du code d'un processeur RISC vers un autre processeur RISC, dont les tailles d'instructions sont similaires. Elle ne permet pas d'émuler plusieurs jeux d'instructions différents, le cout en matériel (un circuit de pré-décodage par jeu d'instruction) serait trop important. ===Le projet DAISY d'IBM=== De très rares processeurs étaient conçus pour accélérer cette traduction binaire, afin de garder de bonnes performances. Les premiers à avoir étudié l'idée étaient IBM, avec leur projet DAISY (''Dynamically Architected Instruction Set from Yorktown''). Le projet de base était de convertir à la volée du code compilé pour des CPU Power PC, vers du code VLIW. Les chercheurs d'IBM avaient développé un algorithme de traduction dynamique efficace, ainsi qu'un processeur VLIW disposant d'optimisations spécifiques à la traduction binaire dynamique, à la volée. De nombreuses idées de ce projet ont été reprises ou re-découvertes par la société Transmetta, avec ses processeurs Crusoe et Efficieon, puis par NVIDIA avec son projet Denver. Les ingénieurs de ce projet ont étudié la possibilité d'émuler plusieurs jeux d'instructions différents sur un même processeur, notamment le s390 d'IBM et le x86. En théorie, cela demande juste d'avoir plusieurs programmes de traduction binaire : un pour le x86, un autre pour le s390, éventuellement un autre pour le Power PC. Mais cela ne s'est pas concrétisé. Mais les ingénieurs ont étudié quelle pourrait être l'architecture VLIW idéale pour ça. Par exemple, il fallait des additions 3-opérandes pour simplifier les calculs d'adresse. Une difficulté était la gestion du registre d'état, dont les bits ne sont pas mis à jour de la même manière sur le x86 et le s390 ou le Power PC. La différence entre gros-boutisme et petit-boutisme était aussi un problème, de même que des différences d'adressage pour les périphériques. Pour que la traduction binaire soit efficace, le processeur VLIW intègre diverses optimisations, comme les exceptions différées et les branchements multi-voies. La principale est la suivante : seul le code des boucles ou fonctions exécutées fréquemment est traduit en code VLIW, le reste du code est interprété. En effet, traduire du code binaire prend plus de temps qu'une simple interprétation. Pour du code qui ne sera exécutée qu'une seule fois, il est plus rapide d'utiliser l'interprétation. Par contre, pour du code exécuté beaucoup de fois, le cout de la traduction binaire est amorti, dilué sur N exécutions, compensé par le gain en temps d'exécution de ce code traduit. Au final, cela permet de ne traduire que le code qui le mérite. La détection du code fréquemment exécuté est réalisée dans le cœur VLIW. L'unité de branchement mémorise les derniers branchements rencontrés et le nombre de fois qu'ils ont été exécutés. Si ils ont été exécutés un certain nombre de fois, l'unité de branchement lève une exception matérielle, qui invoque le traducteur binaire. La routine de cette exception est le traducteur binaire proprement dit. En clair, la traduction binaire est démarrée quand le processeur détecte qu'une fonction a été exécutée plus de N fois, via une exception matérielle dédiée. La mesure du nombre d'exécution d'un branchement se fait dans l'unité de calcul dédiée aux branchements, avec l'aide d'une sorte de ''branch adress buffer'' modifié. Pour rappel, le ''branch adress buffer'' mémorise le ''Program counter'' de chaque branchement récemment rencontré. Ici, chaque entrée du ''branch adress buffer'' est associée à un compteur incrémenté à chaque exécution du branchement. ===Les processeurs Crusoe et Efficieon de Transmetta=== Les processeurs Crusoe Et Efficeron sont deux processeurs VLIW produits par la société Transmetta, une société californienne rachetée par NVIDIA. Ils étaient conçus pour exécuter spécifiquement des programmes x86, système d'exploitation inclus. L'idée derrière ce projet était d'exécuter du code x86 sans que les applications, ni même le système d'exploitation et le BIOS soient au courant ! Les processeurs Transmetta étaient en réalité conçus pour exécuter un programme unique, le ''Code Morphing Software'' (CMS), qui traduisait le code x86 en code VLIW. Transmetta ne voulait pas que son processeur VLIW soit exploité directement, sans le CMS. Il n'avait pas rendu public de compilateur pour traduire du C vers du code VLIW, il n'avait pas donné la documentation du jeu d'instruction VLIW. Des efforts de rétro-ingénieurie, documentés sur le site realworldtech, ont cependant permis de comprendre comment étaient encodées les instructions du processeur Crusoe. Comme pour le projet Daisy, le ''Code Morphing Software'' utilisait à la fois interprétation et traduction binaire, selon les besoins. Les instructions sont interprétées lors de leurs premières exécutions, mais le CMS bascule sur de la traduction en code VLIW après un certain nombre d'exécution. Ainsi, les boucles souvent exécutées sont traduites en code VLIW, alors que le reste du code est interprété. Le choix du code à traduire est le fait du CMS, il utilise des heuristiques complexes pour, qui ne sont pas connues dans le détail. Le ''Code Morphing Software'' est mémorisé dans une EEPROM, ce qui en fait un ''firmware'' situé en dessous du BIOS. Le CMS démarre ensuite le BIOS, qui lui-même démarre le système d'exploitation, qui lui-même démarre les pilotes de périphériques et les programmes. Le CMS se réserve les 16 premiers mébioctets de l'espace d'adressage. L'EEPROM du CMS est mappée dedans, mais n'en utilise que la moitié. Le reste est utilisé comme cache, pour mémoriser le code VLIW traduit par le CMS. Les deux processeurs intégraient aussi deux mémoires SRAM, utilisées par le CMS, appelées la ''local program memory'' (LPM) et la ''local data memory'' (LDM). La première contient du code qui gére les interruptions, la mémoire virtuelle, les exceptions matérielles, les problèmes d'alignement mémoire, et quelques fonctions très fréquemment utilisées par le CMS. La seconde est de la mémoire RAM utilisée par ces fonctions de la ''local program memory''. Les mémoires caches L1 et L2 sont séparées de ces deux mémoires. Le processeur Crusoe et Efficieon étaient des processeurs VLIW très simples. Ils n'avaient même pas de MMU, ni de fonctionnalités importantes sur les CPU x86. Pour Crusoe, les instructions VLIW étaient encodées sur 64 ou 128 bits, et regroupaient entre 2 à 4 opérations. Les instructions LOAD/STORE ne supportaient pas d'adressage indicé, juste de l'adressage indirect à registre. Crusoe avait 5 unités de calcul : deux ALU entières, une unité LOAD/STORE, une unité de branchement, une FPU. Efficieon doublait le nombre d'ALU entière et d'unité LOAD/STORE, les instructions VLIW passaient à 256 bits. Le processeur Crusoe contient 160 registres, dont 64 registres généraux, 32 registres flottants. Les registres généraux font 32 bits, ce qui est cohérent avec le fait que les CPU x86 de l'époque étaient des processeurs 32 bits. Quant aux registres flottants, ils faisaient 80 bits, ce qui colle avec la taille des registres flottants de la FPU x87 utilisée à l'époque. Sur les 64 registres généraux, seuls 48 étaient réellement utilisable pour mémoriser des opérandes. Une partie des registres généraux étaient utilisés pour la gestion de la pile, d'autres pour mémoriser l'état du CPU x86 émulé, un registre était un registre zéro non accesible en écriture. La gestion des exceptions est optimisée avec un système d'exceptions différées, le même que celui décrit plus haut. Le processeur utilise un système similaire, pour effectuer des lectures anticipées, qui a été expliqué dans le chapitre sur les processeurs VLIW/EPIC. Sauf qu'il s'agit là d'une optimisation pour gagner en performances en général, pas une aide utile pour gérer des exceptions précises ou toute autre subtilité de la traduction binaire. Pour gérer les exceptions différées et les lectures anticipées, le processeur mémorise l'état du processeur avant de démarrer un bloc de code. Pour cela, le processeur contient des ''shadow registers'', 48 registres entiers et 16 registres flottants. Vous remarquerez qu'il y a autant de registres entiers que de ''shadow registers'' entiers. Les registres entiers/flottants sont mémorisés dans les ''shadow registers'' avant d'exécuter un bloc de code, afin que le processeur puisse revenir à l'état de base. Une autre fonctionnalité liée est le ''gated store buffer''. Le principe est d'éviter toute écriture en mémoire RAM, tant que le code VLIW n'a pas émis d'instruction ''commit''. L'idée est que les écritures sont conservées dans la ''store queue'' de l'unité LOAD/STORE. Pour rappel, la ''store queue'' met en attente les écritures soit tant que les écritures ne sont pas terminées, soit tant que la RAM est occupée. L'idée est que les écritures sont accumulées dans la ''store queue'' tant que l'instruction ''commit'' n'a pas eu lieu. Le ''store queue'' est "déversé dans la RAM" seulement quand l'instruction ''commit'' s'exécute. ===Les CPU NVIDIA Denver=== Nvidia a tenté quelque chose de similaire avec son projet Denver. NVIDIA a racheté l'entreprise Transmetta, ses brevets, et a tenté de refaire la même chose. Le projet initial était de traduire du code x86 vers un jeu d'instruction VLIW propriétaire, comme l'a fait Transmetta. Mais NVIDIA n'a pas réussi à acquérir la licence du jeu d'instruction x86 et s'est rabattu sur le jeu d'instruction ARM. Le processeur né de ce projet est le Tegra K1-64 CPU. Il s'agit d'un processeur composé d'un cœur VLIW, couplé à deux décodeurs d'instructions ARM. Les deux décodeurs ARM traduisent une paire d'instructions ARM consécutives en instructions VLIW, pour les exécuter sur le cœur VLIW. Le CPU peut fonctionner selon deux modes : ARM et VLIW. En somme, le processeur supporte deux jeux d'instruction : le jeu d'instruction ARM, et un jeu d'instruction VLIW. En mode ARM, les instructions ARM sont chargées depuis le cache, traduites en VLIW par les décodeurs ARM, puis exécutées par le cœur VLIW. En mode optimisé, le CPU exécute des instructions VLIW chargées depuis le cache d'instruction, les décodeurs ne sont pas utilisés. : Le CPU supporte en réalité deux jeux d'instruction ARM : ARM8 et 7. Mais ils correspondent au même mode pour le CPU. Un point important est que le processeur peut changer très rapidement de mode, en à peine quelques cycles d'horloges. On n'est clairement pas dans le cas des CPU x86, qui mettent des plombes pour passer du mode 32 à 64 bits et inversement. La commutation est tellement rapide qu'on peut considérer que le processeur supporte deux jeux d'instruction simultanément. Et le support simultané de deux jeux d'instruction facilite l'implémentation de la traduction binaire. Encore une fois, inutile de traduire du code qui ne sera exécutée qu'une seule fois, mieux vaut l'exécuter directement dans le mode ARM. Surtout que le processeur supporte un mode ARM qui permet d'exécuter du code ARM sans perte de performance. Par contre, le code fréquemment utilisé, notamment dans des boucles critiques, est traduit en VLIW. Au final, les performances sont optimisées, en limitant le travail du logiciel traducteur binaire. La détection du code fréquemment exécuté se fait différemment que pour les processeurs Transmetta et Daisy. L'unité de branchement ne détecte pas directement le code beaucoup exécuté, même si elle a un rôle à jouer. A la place, la détection est réalisée en logiciel, par un ''thread'' dédié, qui s'exécute sur un cœur séparé. Il détecte le code fréquemment utilisé en regardent les branchements exécutés récemment. Pour cela, le processeur mémorise l'historique des branchements pris récemment et copie cette historique dans une mémoire tampon dédiée, partagée entre tous les cœurs. Vu qu'elle est partagée, le ''thread'' de détection a accès à l'historique sans pertes de performances, seule la copie de l'historique dans ce tampon a un cout en performance, pas son partage. Le code VLIW obtenu après traduction binaire est mémorisé dans la mémoire RAM, dans une portion spécialement réservée pour. Elle fait 128 mébioctets, et est appelée le '''cache d'optimisation''' par NVIDIA. Le cache d'optimisation est protégé en écriture et seul le traducteur binaire peut écrire dedans. Au passage, le traducteur binaire est du code VLIW, ce qui fait qu'il est placé dans le cache d'optimisation. Le processeur détecte automatiquement quand une fonction pour laquelle le code compilé équivalent est disponible. Pour cela, l'unité de chargement contient une table de correspondance entre l'adresse de la fonction ARM, et l'adresse de son équivalent compilé. Quand une fonction est appelée, l'unité de chargement regarde l'adresse de destination du branchement. Si l'adresse est dans cette table, elle récupère l'adresse de la fonction compilée et branche vers celle-ci. Le processeur dispose d'un cache d'instruction de 128 kibioctets, ce qui est très important, mais nécessaire vu la taille des instructions VLIW. Le coeur VLIW dispose de ses propres décodeurs, d'un ''scoreboard'', de 7 unités de calcul et de registres. Pour les unités de calcul, il a deux FPU, deux ALU, une unité de branchement, et deux unités LOAD-STORE faisant aussi ALU. Un point important est que le CPU dispose de 64 registres entiers et de 64 registres flottants. C'est deux fois plus que supporte le jeu d'instruction ARM. En mode ARM, seule la moitié des registres est utilisée. Mais en mode optimisé, le traducteur binaire utilise bien les 64 registres, grâce à une sorte de renommage de registres logicielle. Pour simplifier la traduction binaire, le processeur supporte les techniques vues précédemment. Le processeur intègre un ''gated store buffer'' similaire à celui des processeurs Transmetta. Les techniques d'exceptions différées sont aussi supportées, comme sur les processeurs Transmetta. Il a aussi une unité de préchargement décrite par NVIDIA comme agressive, avec support du préchargement de type anticipé (''runahead''). ===L'émulation de la mémoire virtuelle=== Pour finir, il faut parler de la mémoire virtuelle avec la traduction binaire. Sur le processeur source, le processeur gère à la fois des adresses physiques et virtuelles et sa MMU fait la conversion entre les deux. Sur le processeur VLIW, la MMU est simulée par le programme de traduction binaire, partiellement ou totalement. L'implémentation exacte varie suivant que l'on parle du projet Daisy, des processeurs Transmetta ou de Denver. Sur les processeurs Transmetta, il n'y a pas de MMU ni de mémoire virtuelle. A la place, le programme de traduction binaire émule la mémoire virtuelle du processeur source. Le processeur VLIW ne gère que des adresses physiques, rien d'autre. L'espace d'adressage physique du processeur VLIW a la même taille que celui du processeur émulé, ici des processeurs x86. Il est vraisemblable que les adresses physiques utilisées par le processeur x86 sont les mêmes que celles du processeur VLIW. Sur le projet Daisy, le processeur VLIW gère la mémoire virtuelle via pagination, comme les processeurs émulés. Ce qui fait qu'il gère un espace d'adressage virtuel et un espace d'adressage physique. Pour éviter toute confusion, nous parlerons d'adresse physique/virtuelle VLIW pour les adresses physiques/virtuelles du processeur VLIW, d'adresse virtuelle/physique source pour celle du jeu d'instruction traduit, à savoir du Power PC ou du s390, éventuellement de l'x86. Il faut alors faire le lien entre adresses physiques source et adresses virtuelles VLIW. Pour cela, rien de plus simple : il y a correspondance parfaite. L'adresse physique source numéro N correspond à l'adresse virtuelle VLIW numéro N. Cependant, cela signifie que tout l'espace d'adressage virtuel VLIW serait utilisé par le code à traduire. En réalité, il faut ajouter de la place pour le code traduit et le programme de traduction binaire. La conséquence est que l'espace d'adressage virtuel VLIW est plus large que l'espace d'adressage source. L'espace d'adressage virtuel VLIW est découpé en trois sections : une section pour le code Power PC à traduire, une autre pour le code traduit en VLIW, et une dernière réservée au traducteur binaire. Le programme de traduction est placé dans une ROM mappée en mémoire dans la seconde section. Le reste de la seconde section est réservé à une mémoire RAM utilisée par le programme de traduction. [[File:Mémoire virtuelle sur le projet Daisy.png|centre|vignette|upright=1.5|Mémoire virtuelle sur le projet Daisy]] Il faut noter que chaque page physique source, une fois traduite, correspond à N pages virtuelles VLIW. La raison est que le code traduit prend plus de place que le code machine originel. En conséquence, on doit utiliser une page finale plus grande. Pour le reste, des pages contiguës en mémoire physique source sont elles aussi contiguës en mémoire virtuelle VLIW. Le calcul d'adresse est donc simple : l'adresse physique source est multipliée par N, puis on ajoute l'adresse de base à laquelle commence la section pour le code traduit. ==La traduction binaire sur le Burroughs B1700== Nous venons de voir comment accélérer en matériel la traduction binaire, avec des techniques bien spécifiques. Le Burroughs B1700 a procédé autrement et il est intéressant d'étudier en détail son architecture. Son architecture est décrite dans le livre "Interpreting Machines : Architecture and Programming of the Bl700/Bl800 Series", écrit par Elliott I. Organick et James A. Hinds. Une partie de ce qui vfa suivre est un très court résumé de ce livre. ===Le Burroughs B1700 : architecture externe=== Il disposait de 4 registres généraux, nommés X, Y, T et L, tous de 24 bits. Les registres T et L étaient découpés en 6 sous-registres de 4 bits, nommés LA LB LC LD LE et LF pour le registre L, et TA TB TC TD TE et TF pour le registre T. Ils étaient adressables comme n'importe quel registre. Un point important est que le registre T avait des possibilités d’extraction spécifiques. Il était possible d'extrait N bits, placés n'importe où dans ce registre, et de les copier dans un autre registre. Nous verrons pourquoi cette possibilité est intéressante et ne sert que pour le registre T. Les registres X et Y étaient reliés à une ALU de 24 bits capable d'effectuer des opérations arithmétiques basiques. Fait étonnant, l'ALU faisait tous les calculs en même temps et fournissait ses résultats dans 7 registres : un registre SUM pour l'addition, un registre DIFFERENCE pour la soustraction, et un registre par opération bit à bit. Les registres pour les opérations bit à bit sont : CMPX et CMPY pour l'opération NOT sur chaque opérande, XANY pour le ET bit à bit, XEOY pour le XOR, XORY pour le OU logique. Les additions et soustractions pouvaient se faire sur des opérandes codés en binaire ou en BCD, suivant le code opération utilisé. A cela, il fallait ajouter deux registres de "résultat" MSKX et MSKY. Ils fournissaient un masque dépendant de la taille du résultat en bits. Cela permettait de gérer des données de 0, 4, 8, 12, 16, 20, et 24 bits, alors que l'ALU faisait 24 bits. L'ALU prenait en opérande les registres X et Y, mais aussi un registre CYF pour la retenue entrante. En sortie, il fournissait aussi deux retenue sortantes : une pour l'addition et une autre pour la soustraction. Elles étaient mémorisées dans les registres CYL et CYD, respectivement. L'ALU fournissait aussi 12 conditions, qui alimentait un registre d'état. Le registre d'état était segmenté en trois registres appelés XYST, XYCN et BICN. Le processeur contenait de nombreux autres registres de 4 bits, dont les sous-registres de T et L. En tout, il y a 27 registres de 4 bits adressables ! Et le processeur disposait d'une ALU 4 bits pour manipuler ces registres de 4 bits ! Elle est capable de faire des MOV entre registres de 4 bits, des opérations bit à bit, et peut aussi tester si un bit vaut 0 ou 1. La dernière possibilité est très utile pour implémenter les branchements. Pour communiquer avec les entrées-sorties, il disposait de deux registres CMND et DATA : CMND pour envoyer une commande à une entrée-sortie, DATA pour lire ou écrire une donnée. Pour la mémoire, il avait deux registres séparés : READ et WRITE. READ contient la donnée lue lors d'une lecture,; WRITE est pour une donnée à écrire lors d’une écriture. Il s'agit de registres d’interfaçage mémoire, qui ont été promus au rang de registres architecturaux. Les deux registres sont gérés par le microcode, qui est le langage principal de la machine. Les instructions machines configurées font sans, mais le microcode doit y avoir accès facilement. Et vu que le microcode est le langage principal de ce processeur, ces deux registres deviennent architecturaux. ===Le Burroughs B1700 : le mal-nommé ''split-level control store''=== Le Burroughs B1700 avait plusieurs modèles : le B1726, le B1710 et le B1800. Ils se distinguaient par la manière dont le code machine source était traduit par le traducteur binaire. Dans ce qui suit, nous parlerons de traducteur binaire pour parler soit d'un interpréteur pour un langage de programmation de haut niveau, soit pour un logiciel de traduction binaire. Le B1710 plaquait le traducteur binaire en mémoire RAM. Le B1800 faisait pareil, mais avait ajouté un cache dédié au code du traducteur binaire, afin d'améliorer les performances. Le B1726 avait un traducteur binaire scindé en deux : une partie était en mémoire RAM, l'autre était dans un ''local store'' dédié. Le processeur peut exécuter le traducteur binaire soit depuis le ''local store'', soit depuis la mémoire RAM avec une pénalité en termes de performances. La documentation appelle cela un '''''split-level control store''''', mais le terme est trompeur : la documentation qualifie de microcode le langage machine du Burroughs B1700, ce qui est à l'origine de beaucoup de confusions. Une autre source de confusion est que ce langage machine a quelques propriétés qu'on pourrait attendre d'un microcode, mais certaines sont manquantes. Par exemple, il peut adresser les registres d’interfaçage mémoire, mais c'est parce que ce sont des registres architecturaux sur ce processeur. Ses instructions n'encodent pas de signaux de commande, par contre, elles font 16 bits et adressent les opérandes de manière implicite. Le ''local store'' mémorisait au maximum 2048 instructions machine, ce qui fait que les 2048 premières "adresses" correspondaient au ''local store''. Les suivantes étaient des adresses de mémoire RAM qui contenaient le reste du traducteur binaire. En pratique, l'adresse qui séparait les deux était configurable ! On pouvait limiter la taille du ''local store'' à un multiple de 32 instructions ! Les adresses restantes étaient alors réattribuées à la RAM. Par exemple, on pouvait attribuer 64 adresses au ''local store'', les 2048 - 64 adresses restantes étaient alors attribuées à la mémoire RAM. Un registre limite, nommé TDPM, indiquait quelle était l'adresse de démarcation entre les deux. Il contenait 4 bits, dont la valeur était multipliée par 32, pour obtenir l'adresse limite. Le programmeur devait découper le traducteur binaire en segments, certains étant placé dans le ''local store'', les autres étant en mémoire RAM. Mieux que ça, le processeur autorisait d'utiliser la technique de l'''overlaying'' pour le microcode ! Il y avait même une fonction OVERLAY dédiée pour ! OVERLAY avait besoin de trois opérandes : la taille du segment à copier, son adresse source en mémoire RAM, son adresse de destination dans la micro-SRAM. L'interpréteur réservait un "segment" en mémoire RAM, pour les données, dont le processeur connaissait la position. En-dehors de ce segment, il n'y a que des instructions machines à traduire, et le traducteur binaire. Le CPU incorporait pour cela deux registres, un pour l'adresse de base du segment de données, un autre pour son adresse de fin. Il incorporait une protection mémoire limitée sur ce segment, à savoir que seul l'interpréteur pouvait lire ou écrire dans ce segment. ===Une architecture bit-adressable=== Le CPU Burroughs B1700 était '''bit-adressable'''. En clair, il pouvait adresser la mémoire bit par bit, si nécessaire. L'avantage est que cela permettait d'émuler des architectures dont la taille des registres/''byte''/mots était très différente. Un autre avantage est que cela facilitait la traduction binaire des instructions machine. Détaillons rapidement le premier avantage. Le processeur était un CPU 24 bits, mais il pouvait émuler des processeurs 16 bits, 8 bits, 4 bits, 1 bit, 9 bits, ou toute autre valeur. Pour cela, le processeur lisait des mots de 24 bits et utilisant un ''barrel shifter'' pour sélectionner les bits adéquats. Par exemple, pour un accès de 16 bits, la donnée lue en mémoire était masquée de manière à ne garder que les 16 bits de poids faible. Le ''barrel shifter''/circuit de masquage était placé directement avant le bus mémoire, sur le trajet des données/instructions. Le ''barrel shifter'' était commandé par un registre ''Field Unit'', qui précisait quelle était la taille des données à charger. De plus, le processeur avait une '''ALU de taille variable'''. En clair, l'ALU pouvait faire des calculs sur un nombre de bits compris entre 0 et 24. Pour cela, l'ALU était une ALU de 24 bits classique, à laquelle on a jouté des capacités de masquage. L'ALU pouvait masquer les bits de poids fort du résultat, si besoin. ===Le code machine du Burroughs B1700=== Le processeur utilise des micro-opérations de 16 bits de long, qui sont copiées dans registre d'instruction M de 16 bits. Fait intéressant, il était possible de faire un OU logique entre l'instruction lue et un registre du processeur, le résultat étant mémorisé dans le registre M. Cela permettait de faire du code automodifiant, pour émuler des modes d'adressage complexe. Cela permettait par exemple de remplacer une adresse dans une micro-opération. L'adresse est alors surimposée avec un OU dans le champ d'adresse, alors que le reste de l'instruction n'est pas modifié. Afin de gérer les interruptions, ces dernières étaient partiellement détectées en logiciel. Les interruptions ne s'exécutaient pas automatiquement, pour émuler correctement les interruptions. Une interruption doit être prise en compte à la toute fin de l'exécution d'une instruction, quand celle-ci a finit son travail. Mais vu qu'ici, les instructions sont émulées avec des séries de micro-opérations, il y a un problème : il est possible d'interrompre une instruction émulée en plein milieu. Pour éviter cela, le processeur ne déclenche pas les interruptions matérielles immédiatement, elles sont mises en attente. Une micro-opération dédiée vérifie si une interruption est en attente, et l'exécute alors cas échéant. La micro-opération est utilisée comme micro-opération finale d'une instruction émulée. Le ''program counter'' est appelé le registre A. Les instructions du processeur font 16 bits de long, et elles sont alignées en RAM sur 16 bits, ce qui fait que les 4 bits de poids fort de ce registre valent 0, seuls les 14 bits de poids fort sont utiles. Les adresses mentionnées plus haut, qui adressent les micro-opérations en mémoire RAM, sont des adresses de mots de 16 bits, pas les adresses de bit individuels. ===La traduction binaire est aidée par des registres et instructions dédiées=== Les instructions à décoder n'ont pas de longueur fixe, à savoir qu'elles peuvent faire 16 bit avec tel jeu d'instruction, 8 bits avec un autre, 32 bits sur un autre, etc. Elles peuvent aussi être de longueur fixe ou variable ! Le B1700 intégrait donc un support des instructions de longueur variable, qui était "détourné" pour gérer des jeux d'instruction différents. Notons que vu que l'architecture est bit-adressable, les contraintes d'alignement des instructions disparaissent, ce qui a des avantages qu'on va détailler dans ce qui suit ! L'implémentation des instructions de taille variable ajoutait un registre avant le registre d'instruction. Le registre d'instruction proprement dit était un registre M de 24 bits. Il était précédé par un registre T, dans lequel l'instruction était assemblée, en combinant plusieurs morceaux. Si l'instruction est codée est codée sur moins de 24 bits, le registre T se contente de masquer les bits en trop. Si l'instruction chargée dépasser 24 bits, le chargement se faisait en plusieurs passes, avec plusieurs lectures en mémoire. Dans ce qui suit, on suppose que l'instruction est composée de '''champs''' : l'opcode, les opérandes, les adresses absolues, les constantes immédiates, les numéros de registre, etc. L'idée est que l'instruction est chargée dans le registre T, puis découpée en champs, qui sont copiés dans les registres du processeur ou envoyés au microcode. Je rappelle que le registre T est relié à un ''barrel shifter'', différent au précédent, qui permettait de masquer, décaler et extraire une donnée de ce registre. Et il y a la possibilité d'extraire une suite de bit de ce registre T, pour la copier dans un autre registre. Par exemple, prenons une instruction composée d'un opcode, d'une constante immédiate et d'une adresse immédiate. L'opcode est lu et interprété par le microcode. Le microcode extrait la constante immédiate du registre T et la copie dans le registre X. Ensuite, l'adresse est extraite et envoyée au registre FA, une lecture est démarrée, et l'opérande est copiée dans le registre Y. Enfin, l'instruction est exécutée par le microcode. Le tout prend plusieurs micro-opérations : une première pour lire l'opcode, une seconde pour lire la première opérande, une troisième pour lire la seconde opérande, una quatrième pour tout exécuter. Notons que suivant la taille des champs, on peut en charger plusieurs à la fois. Par exemple, imaginons qu'on ait chargé 24 bits, contenant un opcode de 8 bits et une constante immédiate de 16 bits : pas besoin de faire deux accès mémoire, le registre T est juste découpé en un opcode et une constante. Le registre T est fait pour ce genre d'extraction. ===Le chargement d'un champ=== L'architecture est bit-adressable car elle gére des champs qui ne font pas forcément 8 ou 16 bits. Par exemple, certains jeux d'instructions ont des opcodes codés sur 6 ou 7 bits, voire 9-10 bits. Par exemple, prenons un opcode de 7 bits, suivi par une adresse de 12 bits et une constante immédiate de 15 bits : aucun de ces champs ne sera aligné sur 8/16/24 bits. Utiliser une architecture bit-adressable règle le problème d'alignement. Et en général, les contraintes d'alignement des instructions disparaissent ! Quand on charge une instruction, peut importe qu'elle soit ou non alignée sur 24 bits, elle sera placée dans les bits de poids faible, il suffira de masquer les bits de poids fort inutiles. Par exemple, si on charge une instruction de 16 bits, les 16 bits seront copiés dans les 16 bits de poids faible du registre T, il suffira de masquer l'octet de poids fort. Pas besoin de faire de décalages pour gérer des instructions mal alignées... Pour charger une instruction, il faut connaitre plusieurs informations : l'adresse de son premier bit, sa taille. Pour cela, le B1700 contient deux registres, nommés ''Field Adress'' (FA) et ''Field Length'' (FL), qui indiquent l'adresse du premier bit et la taille de l'instruction à charger. Le premier fait 24 bits, ce qui permet de gérer des adresses de 24 bits. N'oubliez pas que le processeur est bit-adressable, ce qui permet d'adresser 16 Mébi-bits, soit 2 mébioctets. Le registre pour la taille fait lui 16 bits, ce qui permet de gérer des instructions de 65536 bits, soit 8192 octets ! Leur nom commence par ''field'', car le processeur peut gérer des instructions de taille variable et/ou plus longues que 24 bits. Dans ce cas, le chargement de l'instruction se fait champ par champ. Pour donner un exemple, prenons une instruction composée de 20 champs de 12 bits chacun. Les champs sont chargés un par un dans le registre T. Le registre FA est alors initialisé avec l'adresse de l'instruction, il est incrémenté de 12 à chaque cycle. Le registre FL est initialisé avec la taille de l'instruction, soit 20 * 12 = 240 bits, et il est décrémenté de 16 à chaque cycle. Précisons cependant que la taille d'un champ peut être changée d'un cycle à l'autre. Par exemple, le processeur peut charger un opcode de 8 bits, l'interpréter, détecter que l'instruction demande ensuite de charger une adresse de 16 bits. Dans ce cas, le registre FL est immédiatement altéré pour charger 16 bits, au lieu des 8 bits de l'opcode immédiatement précédent. ==Les jeux d'instructions dédiés à un langage de programmation== De rares processeurs sont conçus pour un langage de programmation en particulier. On appelle ces processeurs, conçus pour des besoins particuliers, des '''processeurs dédiés'''. Par exemple, les fameux ''Burrough E-mode'' B5000/B6000/B7000 étaient spécialement conçus pour exécuter de l'ALGOL-60. Leurs cousins B2000/B3000/B4000 étaient eux conçus pour le COBOL. Des langages fonctionnels ont aussi eu droit à leurs processeurs dédiés. Le prolog en est un bel exemple, avec les superordinateurs de 5ème génération qui lui étaient dédié. On peut aussi citer les machines LISP, dédiés au langage LISP, qui datent des années 1970. Elles étaient capables d’exécuter certaines fonctions de base du langage directement dans leurs circuits : elles possédaient notamment un ''garbage collector'' câblé dans ses circuits ainsi que des instructions machines supportant un typage déterminé à l’exécution. Les processeurs dédiés ont eu leur heure de gloire au début de l'informatique, à une époque où les langages de haut niveau venaient d'être inventés. À cette époque, les compilateurs n'étaient pas performants et ne savaient pas bien optimiser le code machine. Il était alors rationnel, pour l'époque, de rapprocher le code machine cible et le langage de programmation de haut niveau. De nombreuses architectures dédiés ont ainsi été inventées, avant que les concepteurs se rendent compte des défauts de cette approche. Les défauts en question ne sont pas nombreux, mais assez simples à comprendre. Premièrement, elles sont très rapides pour un langage de programmation en particulier, mais sont assez mauvaises pour les autres, d'où un problème de "compatibilité". Ajoutons à cela que les langages de programmation peuvent évoluer, devenir de moins en moins populaires/utilisés, ce qui rend la création d'architectures généralistes plus pertinente. Enfin, les architectures dédiées sont évidemment des processeurs CISC, pour implémenter les nombreuses fonctionnalités des langages évolués. Et les défauts des CISC sont assez rédhibitoires à l'heure actuelle. À l'heure actuelle, les algorithmes des compilateurs se sont améliorés et savent nettement mieux utiliser le matériel. Ils produisent du code machine efficace, ce qui rend les architecture dédiées bien moins intéressantes. Si on ajoute les défauts de ces architectures dédiées, par étonnant que les architectures dédiées aient presque disparues. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'accélération matérielle de la virtualisation | prevText=L'accélération matérielle de la virtualisation | next=Le matériel réseau | nextText=Le matériel réseau }} </noinclude> 6lqwg7e140nhpmnu3xe0ecslav1eyz3 764047 764046 2026-04-19T20:12:02Z Mewtow 31375 /* Une architecture bit-adressable */ 764047 wikitext text/x-wiki De nos jours, la majorité des programmeurs programment dans des langages de haut niveau. Il est très rare pour eux d'avoir à utiliser de l'assembleur, et encore moins un langage machine. Les programmes écrits dans un langage de haut niveau sont traduits en langage machine par un logiciel appelé le compilateur. [[File:Compilation.PNG|centre|vignette|upright=2.5|Compilateur : principe.]] Il se trouve que quelques rares processeurs sont conçus pour faciliter le travail du compilateur. Et ce sont ces processeurs que nous allons voir dans ce qui suit. Ils représentent une catégorie de jeux d'instruction à part, qui n'a pas vraiment de nom. ==La compilation et l’interprétation== Un compilateur traduit donc du code source, écrit dans un langage de haut niveau, vers du langage machine. La traduction est rarement directe. En général, le compilateur traduit le code source en assembleur, qui est lui-même traduit en langage machine. L'assembleur est une représentation textuelle du langage machine alors que le code machine est du binaire exécutable par le processeur. La traduction se fait donc en deux étapes, la '''compilation''' proprement dite et l''''assemblage''', réalisées respectivement par un compilateur et un assembleur. Il faut parfois rajouter une troisième phase d'édition des liens, que nous passons volontairement sous silence. {| |[[File:Code C.png|vignette|200px|Le code source, ici en C.]] |[[File:Assem.png|vignette|250px|Le code assembleur.]] |[[File:Binary file - hello world (C programming).png|vignette|150px|Le code machine.]] |} ===L'interprétation et la compilation à la volée=== Une alternative à la compilation est l''''interprétation''', qui transforme le code source en code machine à la volée. Avec l'interprétation, le code source est passé à un logiciel appelé l''''interpréteur''', qui exécute le code source ligne de code par ligne de code. Une ligne de code est traduite en code machine, qui est exécutée, puis l'interpréteur passe à la ligne suivante. [[File:Kaantotulkkaus.png|centre|vignette|upright=3|Compilation versus interprétation.]] Un défaut de l'interprétation est son cout en performance, qui est assez important. Aussi, l'interprétation stricte a évolué vers un hybride entre interprétation et compilation. L'idée est qu'une partie peu importante du code source est interprétée, alors que le code important est compilé juste avant d'être exécuté. Pour le dire autrement, le code source est partiellement compilé à la volée, juste avant son exécution. Aussi, on parle de '''compilation à la volée'''. Le terme en anglais est ''Just In Time compilation'', abrévié en JIT. En soi, le JIT est associé à de l'interprétation, les JIT sont en réalité des hybrides interpréteurs-compilateurs. Un point important est que compiler du code à la volée est assez lourd, cela a un cout en performance. Par contre, le code compilé s'exécute plus vite que du code interprété. Si on exécute le code compilé une seule fois, le cout de la compilation l'emporte sur le gain à l'exécution. Mais le code compilé est réutilisé autant de fois que nécessaire. Le gain à l'exécution est donc multiplié par le nombre d'exécutions, alors que le cout de la compilation est répartit sur plusieurs exécutions, il est amorti. Les portions du code sont choisies de manière à avoir un gain en performance maximal. L'idée générale est que le code exécuté très souvent est compilé, alors que du code exécuté pas souvent est interprété. Typiquement, les boucles les plus souvent exécutées sont compilées, le code exécuté une seule fois est interprété. ===Les langages intermédiaires=== Les compilateurs modernes passent par un '''langage intermédiaire''' pour faire la transformation en code machine. Le compilateur traduit le langage de haut niveau en langage intermédiaire, puis traduit le langage intermédiaire en code machine cible. Le langage intermédiaire est parfois appelé le '''''bytecode''''', et ce terme recouvre aussi le code écrit avec du ''bytecode''. L'interprétation aussi peut passer par l'intermédiaire d'un un ''bytecode'', ce n'est pas limité aux compilateurs proprement dit. {| |[[File:Intermediate representation scheme.png|centre|vignette|upright=1.5|Représentation intermédiaire d'un compilateur.]] |[[File:Code interpreter scheme.png|centre|vignette|upright=1.5|Fonctionnement d'un interpréteur.]] |} Faire ainsi a de nombreux avantages, le principal étant d'avoir un compilateur capable de traduire un langage de haut niveau vers plusieurs jeux d'instructions différents. Cela permet d'avoir, par exemple, un compilateur qui traduit du C soit en code machine x86, soit en code machine pour un CPU ARM, soit pour un CPU POWERPC, etc. [[File:Compiler design.svg|centre|vignette|upright=3|Compiler design]] Pour l'interprétation, l'usage d'un ''bytecode'' a d'autres avantages. Pour l'exploiter, il faut distribuer non pas le code source, mais le ''bytecode''. En clair, l'interpréteur ne prend pas en entrée le code source, mais du ''bytecode'' déjà compilé. La traduction est alors beaucoup plus simple, car le code source a déjà été partiellement compilé, par la traduction en ''bytecode''. Interpréter du code source est en effet assez compliqué : il faut effectuer des étapes d'analyse lexicale, sémantique, et bien d'autres. Avec du ''bytecode'', ces étapes ont été réalisées lors de la compilation du ''bytecode'', l'interpréteur a alors peut de choses à faire. L'interprétation du ''bytecode'' se fait instruction par instruction, au niveau du ''bytecode''. Une instruction du ''bytecode'' est traduite en une instruction machine équivalente, qui est exécutée. Si l'instruction est un peu complexe, il exécute une fonction/procédure qui fait la même chose. Pour résumer, le ''bytecode'' est ensuite traduit à la volée et exécuté instruction par instruction par un logiciel appelé l'interpréteur. [[File:Control table.png|centre|vignette|upright=2|Control table]] Le langage intermédiaire peut être vu comme l'assembleur d'un processeur, n'existe pas forcément dans la réalité, mais dont le jeu d'instruction est décrit en détail. Le processeur en question est appelé une '''machine abstraite''', ou encore une ''machine virtuelle''. Nous utiliserons le terme de machine abstraite dans ce qui suit. La machine abstraite n'est pas la même suivant que l'on cible la compilation ou l'interprétation/JIT. Pour l'interprétation/JIT, la machine abstraite est souvent une machine à pile, car cela simplifie grandement la traduction en code machine final. L'interprétation demande que la traduction du ''bytecode'' en code machine soit la plus simple possible. Et cela demande de prendre en compte pas mal de détails. Par exemple, les différents jeux d'instruction existants n'ont pas le même nombre de registres, ce qui pose problème lors de la traduction du ''bytecode''. Lors de la transformation en code machine, un algorithme d'allocation de registres se débrouille pour traduire le code intermédiaire en code qui utilise un nombre limité de registres. Pour améliorer cette allocation de registres, il y a deux solutions niveau ''bytecode'' : soit utiliser un nombre illimité de registres, soit utiliser une machine à pile. Pour les machines à pile, il existe un algorithme simple et rapide pour traduire un code écrit pour une machine à pile en un code écrit pour un processeur avec des registres, qui se débrouille pas trop mal pour allouer efficacement les registres. C'est un avantage assez important pour les langages interprétés. ==Les processeurs qui exécutent du bytecode== Le ''bytecode'' est un code machine, ce qui signifie qu'il peut en théorie s'exécuter sur un processeur qui implémente le jeu d’instruction associé. Si le jeu d'instruction d'un ''bytecode'' est souvent une description censée être fictive, elle n'en reste pas moins un jeu d'instruction et des caractéristiques précises, qu'on peut l'implémenter en matériel ! Néanmoins, tous les ''bytecode'' ne sont pas égaux de ce point de vue. Certains sont faciles à implémenter en matériel, d'autres non. Les ''bytecodes'' implémentés en matériel sont ceux dont les machines abstraites sont des machines à pile. Un premier exemple est celui des processeurs Pascal MicroEngine, qui exécutaient directement le bytecode du langage Pascal, le fameux UCSD P-code. Un second exemple est la '''machine SECD''', qui sert de langage intermédiaire pour certains compilateurs de langages fonctionnels. Elle a été implémentée en matériel par plusieurs équipes, notamment par les chercheurs de l'université de Calgary en 1989. Dans le même genre, quelques processeurs simples étaient capables d’exécuter directement le ''bytecode'' du langage FORTH. Le FORTH, un des premiers langages à pile de haut niveau, possède de nombreuses implémentations hardware et est un des rares langages de haut niveau dont le ''bytecode'' a été utilisé comme langage machine sur certains processeurs. Par exemple, on peut citer le processeur FC16, capable d’exécuter nativement du ''bytecode'' FORTH. ===Les processeurs Java=== Le cas le plus impressionnant est celui de la machine virtuelle Java, qui est un design de processeur comme un autre. En temps normal, le ''bytecode'' Java est compilé ou interprété, mais certains processeurs exécutaient du ''bytecode'' Java directement, sans interprétation ni compilation. Ils sont appelés des '''processeurs Java'''. On peut citer les processeurs ARM disposant de l'extension Jazelle, du JEMCore et du aJ-100 de aJile Systems, le picoJava II de Sun, et quelques autres. Il y a aussi eu quelques projets de recherche ou processeurs open source, comme Komodo, jamuth, le ''Java Optimized Processor'' et quelques autres projets du même genre. L'implémentation de la machine virtuelle Java n'était cependant pas complète. Quelques instructions complexes n'étaient pas gérées par le processeur et devaient être émulées en logiciel. Mais la grosse majorité des instructions l'était. Les instructions non-supportées étaient émulées avec la méthode ''trap and emulate'', à savoir qu'un opcode inconnu déclenchait une exception matérielle ''opcode invalide'', dont la routine pouvait émuler les instructions qui doivent l'être. Le processeur aJ-100 de aJile est capable d'exécuter la quasi-totalité des instructions de la JVM Java, seules deux instructions faisant exception. Les '''accélérateurs Java''' sont une solution intermédiaire, qui permet de traduire à la volée du ''bytecode'' Java en instructions RISC, exécutées sur un autre processeur. Il s'agit formellement de traduction binaire, mais je préfère en parler ici. L'idée est de combiner un processeur RISC avec un coprocesseur Java. Le coprocesseur lit le ''bytecode'' Java et le traduit en instructions RISC, ces dernières étant exécutées sur le processeur RISC. Le JA108 de Nozomi était un coprocesseur de ce genre, au même titre que le coprocesseur JSTAR de JEDI. L'extension Jazelle de certains CPU ARM fonctionnait vraisemblablement sur un principe similaire, sauf que le coprocesseur était intégré dans le processeur ARM, entre le cache d'instruction et le décodeur d'instructions ARM. L'intérêt des processeurs Java n'est pas qu'une question de performance. Certes, ils permettent d’exécuter plus vite les programmes compilés en ''bytecode'', comme des programmes Java pour la JVM Java. Mais l'intérêt est aussi de faire des économies de mémoire RAM et ROM. Je rappelle que la machine virtuelle Java est une machine à pile, ce qui fait que sa densité de code est excellente. Les programmes en ''bytecode'' Java sont donc très petits, et prennent peu de place en mémoire ROM. De plus, utiliser un processeur Java permet de se passer d'interpréteur ou de JIT Java, ce qui économise encore plus de mémoire. ===Transformer un processeur RISC en machine à pile=== La plupart des processeurs Java utilisent en interne une architecture à registre généraux, qui émule une machine à registre en microcode. Quelques registres sont réservés pour l'état de la machine virtuelle, avec au minimum un registre réservé pour le pointeur de pile, un autre pour un pointeur vers un ''pool'' de constantes, et quelques autres. D'autres registres servent à mémoriser le haut de la pile, à savoir le sommet de la pile et quelques opérandes situées juste en-dessous. Par exemple, le processeur picoJava II gardait les 64 opérandes au sommet de la pile dans 64 registres généraux, séparés des registres pour les pointeurs et l'état de la JVM. Une optimisation possible, envisagée sur les processeurs picoJava, était une sorte de macro-fusion sous stéroïde. L'idée était de fusionner certaines séries d'instructions Java en une seule instruction machine. Typiquement, pour faire une opération arithmétique, une machine à pile empile deux opérandes et exécute une opération, ce qui prend trois instructions machine. La spécification picoJava 1 a proposé d'envoyer directement la seconde opérande en entrée de l'ALU, ce qui fusionnait l'empilement de la seconde opérande et l'opération arithmétique. Le processeur picoJava II va plus loin et fusionne les trois instructions en une seule instruction RISC, agissant sur des registres. Mais la majorité des processeurs Java ne faisaient pas cela, car cela complexifie grandement le décodeur. Les processeurs ''bigfoot'' de DCT utilisaient un système différent pour émuler une pile avec des registres. Leur banc de registre de 64 bits était coupé en deux : les 16 premiers registres étaient utilisés pour la pile, les 48 suivants étaient utilisés pour autre chose. L'idée est que le registre 0 mémorise un emplacement vide, le registre 1 contient le sommet de la pile, le registre 2 l'opérande sous le sommet de la pile, et ainsi de suite. Les 15 premières opérandes de la pile sont donc mémorisées dans les registres. Les instructions écrivent leur résultat dans le registre adéquat, puis tous les registres sont décalés pour que le résultat passe dans le registre R1. Par exemple, les instructions PUSH chargent une opérande dans le registre 0, puis tous les registres sont décalés de un rang, pour que l'opérande chargée soit dans le registre 1. Le registre 0 devient alors le registre numéro 1, le registre 1 devient le 2, et ainsi de suite. Par contre, les opérations arithmétiques font autrement. Une instruction d'addition, par exemple, lit les opérandes dans le registre 1 et 2, puis place son résultat dans le registre 2 (les deux opérandes sont dépilées). Puis, les registres sont décalés d'un rang pour que ce registre 2 devienne le 1. Les registres ne sont en réalité pas décalés, il n'y a pas de transferts entre registres. A la place, les 16 premiers registres utilisaient une forme limitée de renommage de registres, similaire au banc de registre tournant utilisé sur les processeurs EPIC comme l'Itanium. Les numéros de registres sont renommés, décalés, à chaque fois qu'on empile ou dépile une opérande. Pour faire le renommage de registres, processeur contient un ''register stack counter'' de 4 bits, qui est incrémenté à chaque fois qu'on empile une donnée, et est décrémenté quand on dépile une donnée. Quand on veut accéder à un opérande, le numéro de registre architectural associé est traduit en numéro de registre physique, en additionnant le ''register stack counter'' (modulo 16, pour rester sur 4 bits). ==L'accélération de l'interprétation/JIT : le ''Thumb-EE'' d'ARM== Quelques processeurs ont ajouté des instructions pour faciliter le travail des interpréteurs/JIT. Par exemple, en 2005, ARM a ajouté le mode ''Thumb Execution Environment'', qui faisait cela. Il reprenait le jeu d'instruction compact ''thumb'' et ajoutait quelques instructions et modifiait le comportement d'instructions existantes. Le jeu d'instruction ''Thumb-EE'' a été ajouté sur les CPU ARM en 2005, mais a été déprécié en 2011, par manque d'utilité. Mais la tentative mérite qu'on s'attarde dessus. Les CPU de gamme M, déstinés à l'embarqué, n'ont jamais supporté ''Thumb-EE'', vu que de tels processeurs ne sont pas conçus pour exécuter du code interprété/JIT. Le mode ''Thumb-EE'' est un mode d'exécution, séparé de l'ARM normal. Ainsi, une instruction ''Thumb-EE'' et une instruction ARM peuvent avoir le même encodage en binaire, mais se comporter différemment. Le processeur est à tout moment soit en mode ''Thumb-EE'', soit en mode ARM normal, ce qui précise comment ces instructions doivent se comporter. Des instructions ont été ajoutées par ''Thumb-EE'', pour qu'il fasse son travail. L'entrée et la sortie du mode ''Thumb-EE'' se fait avec deux instructions : ENTERX et LEAVEX. Une fois en mode ''Thumb-EE'', le décodage des instructions ''thumb'' se fait avec les règles du ''Thumb-EE'', et non celles du ''thumb'' normal. Une première différence entre ''thumb'' normal et ''Thumb-EE'' est l'ajout de ce qui s'appelle un ''null check'' pour les instructions mémoire. Le ''null check'' est utilisé pour les instructions en mode d'adressage "base + indice" ou "Base + décalage", ou tout autre mode d'adressage avec un registre de base utilisés dans des calculs d'adresse. L'idée est de vérifier si l'adresse de base vaut zéro ou non. Si c'est le cas, le processeur lève une exception matérielle, qui est traitée par l'interprétation ou le compilateur JIT. Une telle situation est en effet signe d'une erreur d'adressage mémoire, qui doit être traitée par l'interpréteur. Typiquement, c'est signe qu'un pointeur n'a pas bien été initialisé, ce qui peut arriver avec du code interprété ou JIT. Les interpréteurs/JIT traitent généralement la situation en ajoutant un test avant toute instruction mémoire, pour vérifier si le pointeur accédé vaut zéro ou non. Avec ''Thumb-EE'', pas besoin d'ajouter les tests en questions : ils sont réalisés automatiquement lors de chaque instruction mémoire. : Les registres pour la pile, comme le pointeur de pile et le pointeur de ''frame'', sont aussi concernés. L'instruction CHKA vérifie que les accès à un tableau ne débordent pas en dehors du tableau en question. Par exemple, pour un tableau de 1024 éléments, elle vérifie si l'indice est compris entre 0 et 1024. Pour cela, elle vérifie si l'indice de tableau est bien dans l'intervalle adéquat et lève une exception matérielle si ce n'est pas le cas. Son utilité se comprend quand on sait que tous les accès à un tableau sont vérifiés par l'interpréteur. L'interpréteur est censé ajouter des instructions pour vérifier les indices, à savoir deux ou trois branchements. Avec CHKA, le test se fait en une seule instruction. D'autres modifications mineures de l'encodage des instructions sont aussi présentes, ainsi que des modifications pour les instructions LOAD/STORE. Précisément, dans les modes "base + indice" et "base + décalage", le décalage et l'indice subissent maintenant des décalages. L'idée est de simplifier le calcul d'adresse. De quoi économiser quelques instructions lors des calculs d'adresse, ce qui facilite le travail de l'interpréteur/JIT. Et des instructions LOAD/STORE précises voient leur comportement modifié pour faciliter la gestion de la pile de Java ou l'exécution des méthodes locales. Pour faciliter le travail de l'interpréteur, deux instructions HBP et HBLP ont été ajoutées. Elles branchent vers une fonction qui gère les exceptions logicielles (une fonctionnalité présente dans de nombreux langages, comme Java). La première effectue un branchement vers la fonction en question, la seconde sauvegarde l'adresse de retour avant de faire ce branchement. Les deux peuvent brancher vers 256 fonctions pré-déterminées, sans avoir à présenter leur adresse. ==La traduction binaire accélérée par le matériel== Plus haut, nous avons surtout parlé des compilateurs et des interpréteurs. Cependant, nous devons aussi parler de la '''traduction binaire'''. Elle traduit un programme écrit dans un code machine vers un autre code machine. Par exemple, elle traduit un programme compilé pour un CPU x86 vers un code machine ARM. En général, le système d'exploitation est généralement compilé pour le jeu d'instruction natif, mais il exécute des applications prévues pour le x86. Les applications sont traduites par le système d'exploitation, avant d'exécuter le code traduit. : Nous parlerons dans la suite de code machine source et de code machine cible, pour parler respectivement du code à traduire et du code obtenu après traduction. Idem avec d'autres termes comme architecture cible/source, ou jeu d'instruction cible/source. La traduction binaire est surtout utilisée pour des questions d'émulation ou de compatibilité. Par exemple, lorsque les Macintosh sont passés de processeurs Power PC vers des processeurs x86, le système d'exploitation Mac OS utilisait la traduction binaire pour convertir les anciennes applications Power PC vers du code x86. Les utilisateurs n'y ont vu que du feu. Le système de traduction binaire était appelé Rosetta 1. Par la suite, lors de la transition de processeurs x86 vers des processeurs Apple, Rosetta 2 a vu le jour. La traduction binaire a surtout été utilisée pour traduire du code x86 vers un autre jeu d'instruction. Il faut dire que le x86 est le jeu d'instruction dominant. De nombreuses entreprises ont eu pour ambition de briser l'hégémonie du x86 sur PC, en remplaçant le x86 par un jeu d'instruction plus performant, tout en gardant une compatibilité maximale. La traduction binaire était la seule solution pratique. Elle était le plus souvent intégralement réalisée en logiciel, comme c'était le cas sur les architectures Itanium avec le ''IA-32 Execution Layer''. ===Les généralités sur la traduction binaire assistée en matériel=== L'architecture source est presque tout le temps une architecture CISC, assez ancienne, qu'on souhaite émuler. Le choix de l'architecture cible se porte souvent sur une architecture VLIW, fort différente de l'architecture source, et ce pour obtenir une économie de matériel conséquente. Pour s'exécuter rapidement, le code traduit doit exploiter le parallélisme d'instruction, à savoir exécuter plusieurs instructions en même temps dans des unités de calcul séparées. Il est possible d'utiliser un processeur superscalaire avec exécution dans le désordre pour cela, mais au prix d'un cout important en transistors. Alors qu'en utilisant un CPU VLIW, c'est le traducteur binaire qui fait tout le travail d'extraction du parallélisme d'instruction. Et après tout, quitte à avoir un traducteur binaire, autant lui refiler le boulot d'optimisation. Compiler du code à la volée est certes assez lourd, mais qu'il existe des algorithmes efficaces pour regrouper des instructions indépendantes dans une seule instruction VLIW. Au passage, le processeur VLIW a plus de registres que le processeur source. Cela permet de faire du renommage de registres directement en logiciel. Le traducteur binaire n'hésite pas à changer les noms de registres entre instructions source et instruction VLIW cible, afin de supprimer des dépendances de données. En conséquence, cela ouvre des opportunités de parallélisme, qui sont exploitées lors du regroupement des opérations en instructions VLIW. Vous pourriez penser que le choix d'un CPU VLIW pour émuler du CISC est tout sauf optimal. Vous devez penser que la traduction binaire est d'autant plus simple que l'architecture source et cibles sont semblables. Dans les faits, ce n'est pas tellement traduire les instructions qui pose problème, mais plus la gestion du registre d'état, des exceptions matérielles, de la mémoire virtuelle, la différence entre gros-boutisme et petit-boutisme, de même que des différences d'adressage pour les périphériques. Le premier problème est la gestion des conditions, notamment en présence d'un registre d'état. En pratique, la traduction binaire s'utilise pour traduire du code CISC vers du code RISC ou VLIW. Les architectures source ont donc un registre d'état, qui est mis à jour non seulement par des instructions de test, mais aussi des instructions arithmétiques. L'architecture cible n'a elle pas de registre d'état, mais des registres à prédicats. La traduction de l'un vers l'autre est alors quelque peu compliquée. Et elle est d'autant plus compliquée que le registre d'état est la source de dépendances d'instruction implicites, qui réduisent les performances. Par exemple, si une instruction arithmétique modifie le registre d'état, cela peut impacter l'exécution d'une instruction ultérieure, qui lit ce registre d'état. Pour éliminer ces fausses dépendances, le traducteur binaire doit renommer ce registre en logiciel et détecter les dépendances utiles. Et le compilateur doit gérer les cas où il y a beaucoup de distance entre les deux, voire les cas où l'instruction dépendante n'a pas encore été analysée par le compilateur. Un autre problème est lié à la gestion des exceptions matérielles, et précisément des exceptions précises. Pour rappel, une instruction VLIW regroupe plusieurs opérations, opérations qui correspondraient à une instruction machine sur un CPU pas VLIW. En conséquence, plusieurs instructions du langage machine "source" sont regroupées en une seule instruction VLIW. Et il faut tenir compte du cas où une instruction source lève une exception. Dans le code VLIW, cela signifie qu'une opération lève une exception, et il faut annuler partiellement l'instruction VLIW associée. Et par partiellement, on veut dire que seules les opérations suivantes dans l'ordre du programme source doivent être annulées, puis ré-exécutées. Et c'est un sacré casse-tête ! Les processeurs VLIW utilisent comme solution le mécanisme d'exceptions différées des processeurs EPIC, vu il y a quelques chapitres. Pour résumer, le code est exécuté par blocs d'instruction, délimités par des branchements ou tout autre limite/barrière pertinente dans le code. Un bloc de code est compilé en deux versions : une version rapide sans exceptions matérielles, une version lente qui gère les exceptions précises. La version rapide s'exécute sans exécuter les exceptions. Cependant, les exceptions matérielles sont enregistrées, pour être prises en compte à la toute fin du bloc de code. La version rapide est exécutée en premier et elle mémorise si une exception matérielle a eu lieu. Puis une instruction vérifie si une exception a eu lieu et décide quoi faire. Si aucune exception n'a eu lieu, elle passe à la suite du programme, les résultats du bloc de code sont définitivement acceptés. Mais si une exception a eu lieu, tout est annulé. Le processeur est remis dans l'état initial, puis le code est ré-exécuté instruction par instruction de manière à gérer l'exception correctement. ===La traduction par pré-décodage=== Une première solution serait de faire la traduction binaire dans le cache d'instruction. Lors d'un défaut de cache, le code chargé depuis la RAM est traduit en code machine cible. Et c'est ce code machine cible qui est mémorisé dans le cache d'instruction et exécuté par le processeur. La technique marche sur le papier et n'est qu'une amélioration des techniques de pré-décodage vues il y a quelques chapitres. Cependant, le code obtenu est une traduction assez basique, qui n'incorpore pas d'optimisations dignes de ce nom. De plus, elle gère mal le cas où la taille du code source et cible sont potentiellement très différentes. Concrètement, elle est surtout utile pour traduire du code d'un processeur RISC vers un autre processeur RISC, dont les tailles d'instructions sont similaires. Elle ne permet pas d'émuler plusieurs jeux d'instructions différents, le cout en matériel (un circuit de pré-décodage par jeu d'instruction) serait trop important. ===Le projet DAISY d'IBM=== De très rares processeurs étaient conçus pour accélérer cette traduction binaire, afin de garder de bonnes performances. Les premiers à avoir étudié l'idée étaient IBM, avec leur projet DAISY (''Dynamically Architected Instruction Set from Yorktown''). Le projet de base était de convertir à la volée du code compilé pour des CPU Power PC, vers du code VLIW. Les chercheurs d'IBM avaient développé un algorithme de traduction dynamique efficace, ainsi qu'un processeur VLIW disposant d'optimisations spécifiques à la traduction binaire dynamique, à la volée. De nombreuses idées de ce projet ont été reprises ou re-découvertes par la société Transmetta, avec ses processeurs Crusoe et Efficieon, puis par NVIDIA avec son projet Denver. Les ingénieurs de ce projet ont étudié la possibilité d'émuler plusieurs jeux d'instructions différents sur un même processeur, notamment le s390 d'IBM et le x86. En théorie, cela demande juste d'avoir plusieurs programmes de traduction binaire : un pour le x86, un autre pour le s390, éventuellement un autre pour le Power PC. Mais cela ne s'est pas concrétisé. Mais les ingénieurs ont étudié quelle pourrait être l'architecture VLIW idéale pour ça. Par exemple, il fallait des additions 3-opérandes pour simplifier les calculs d'adresse. Une difficulté était la gestion du registre d'état, dont les bits ne sont pas mis à jour de la même manière sur le x86 et le s390 ou le Power PC. La différence entre gros-boutisme et petit-boutisme était aussi un problème, de même que des différences d'adressage pour les périphériques. Pour que la traduction binaire soit efficace, le processeur VLIW intègre diverses optimisations, comme les exceptions différées et les branchements multi-voies. La principale est la suivante : seul le code des boucles ou fonctions exécutées fréquemment est traduit en code VLIW, le reste du code est interprété. En effet, traduire du code binaire prend plus de temps qu'une simple interprétation. Pour du code qui ne sera exécutée qu'une seule fois, il est plus rapide d'utiliser l'interprétation. Par contre, pour du code exécuté beaucoup de fois, le cout de la traduction binaire est amorti, dilué sur N exécutions, compensé par le gain en temps d'exécution de ce code traduit. Au final, cela permet de ne traduire que le code qui le mérite. La détection du code fréquemment exécuté est réalisée dans le cœur VLIW. L'unité de branchement mémorise les derniers branchements rencontrés et le nombre de fois qu'ils ont été exécutés. Si ils ont été exécutés un certain nombre de fois, l'unité de branchement lève une exception matérielle, qui invoque le traducteur binaire. La routine de cette exception est le traducteur binaire proprement dit. En clair, la traduction binaire est démarrée quand le processeur détecte qu'une fonction a été exécutée plus de N fois, via une exception matérielle dédiée. La mesure du nombre d'exécution d'un branchement se fait dans l'unité de calcul dédiée aux branchements, avec l'aide d'une sorte de ''branch adress buffer'' modifié. Pour rappel, le ''branch adress buffer'' mémorise le ''Program counter'' de chaque branchement récemment rencontré. Ici, chaque entrée du ''branch adress buffer'' est associée à un compteur incrémenté à chaque exécution du branchement. ===Les processeurs Crusoe et Efficieon de Transmetta=== Les processeurs Crusoe Et Efficeron sont deux processeurs VLIW produits par la société Transmetta, une société californienne rachetée par NVIDIA. Ils étaient conçus pour exécuter spécifiquement des programmes x86, système d'exploitation inclus. L'idée derrière ce projet était d'exécuter du code x86 sans que les applications, ni même le système d'exploitation et le BIOS soient au courant ! Les processeurs Transmetta étaient en réalité conçus pour exécuter un programme unique, le ''Code Morphing Software'' (CMS), qui traduisait le code x86 en code VLIW. Transmetta ne voulait pas que son processeur VLIW soit exploité directement, sans le CMS. Il n'avait pas rendu public de compilateur pour traduire du C vers du code VLIW, il n'avait pas donné la documentation du jeu d'instruction VLIW. Des efforts de rétro-ingénieurie, documentés sur le site realworldtech, ont cependant permis de comprendre comment étaient encodées les instructions du processeur Crusoe. Comme pour le projet Daisy, le ''Code Morphing Software'' utilisait à la fois interprétation et traduction binaire, selon les besoins. Les instructions sont interprétées lors de leurs premières exécutions, mais le CMS bascule sur de la traduction en code VLIW après un certain nombre d'exécution. Ainsi, les boucles souvent exécutées sont traduites en code VLIW, alors que le reste du code est interprété. Le choix du code à traduire est le fait du CMS, il utilise des heuristiques complexes pour, qui ne sont pas connues dans le détail. Le ''Code Morphing Software'' est mémorisé dans une EEPROM, ce qui en fait un ''firmware'' situé en dessous du BIOS. Le CMS démarre ensuite le BIOS, qui lui-même démarre le système d'exploitation, qui lui-même démarre les pilotes de périphériques et les programmes. Le CMS se réserve les 16 premiers mébioctets de l'espace d'adressage. L'EEPROM du CMS est mappée dedans, mais n'en utilise que la moitié. Le reste est utilisé comme cache, pour mémoriser le code VLIW traduit par le CMS. Les deux processeurs intégraient aussi deux mémoires SRAM, utilisées par le CMS, appelées la ''local program memory'' (LPM) et la ''local data memory'' (LDM). La première contient du code qui gére les interruptions, la mémoire virtuelle, les exceptions matérielles, les problèmes d'alignement mémoire, et quelques fonctions très fréquemment utilisées par le CMS. La seconde est de la mémoire RAM utilisée par ces fonctions de la ''local program memory''. Les mémoires caches L1 et L2 sont séparées de ces deux mémoires. Le processeur Crusoe et Efficieon étaient des processeurs VLIW très simples. Ils n'avaient même pas de MMU, ni de fonctionnalités importantes sur les CPU x86. Pour Crusoe, les instructions VLIW étaient encodées sur 64 ou 128 bits, et regroupaient entre 2 à 4 opérations. Les instructions LOAD/STORE ne supportaient pas d'adressage indicé, juste de l'adressage indirect à registre. Crusoe avait 5 unités de calcul : deux ALU entières, une unité LOAD/STORE, une unité de branchement, une FPU. Efficieon doublait le nombre d'ALU entière et d'unité LOAD/STORE, les instructions VLIW passaient à 256 bits. Le processeur Crusoe contient 160 registres, dont 64 registres généraux, 32 registres flottants. Les registres généraux font 32 bits, ce qui est cohérent avec le fait que les CPU x86 de l'époque étaient des processeurs 32 bits. Quant aux registres flottants, ils faisaient 80 bits, ce qui colle avec la taille des registres flottants de la FPU x87 utilisée à l'époque. Sur les 64 registres généraux, seuls 48 étaient réellement utilisable pour mémoriser des opérandes. Une partie des registres généraux étaient utilisés pour la gestion de la pile, d'autres pour mémoriser l'état du CPU x86 émulé, un registre était un registre zéro non accesible en écriture. La gestion des exceptions est optimisée avec un système d'exceptions différées, le même que celui décrit plus haut. Le processeur utilise un système similaire, pour effectuer des lectures anticipées, qui a été expliqué dans le chapitre sur les processeurs VLIW/EPIC. Sauf qu'il s'agit là d'une optimisation pour gagner en performances en général, pas une aide utile pour gérer des exceptions précises ou toute autre subtilité de la traduction binaire. Pour gérer les exceptions différées et les lectures anticipées, le processeur mémorise l'état du processeur avant de démarrer un bloc de code. Pour cela, le processeur contient des ''shadow registers'', 48 registres entiers et 16 registres flottants. Vous remarquerez qu'il y a autant de registres entiers que de ''shadow registers'' entiers. Les registres entiers/flottants sont mémorisés dans les ''shadow registers'' avant d'exécuter un bloc de code, afin que le processeur puisse revenir à l'état de base. Une autre fonctionnalité liée est le ''gated store buffer''. Le principe est d'éviter toute écriture en mémoire RAM, tant que le code VLIW n'a pas émis d'instruction ''commit''. L'idée est que les écritures sont conservées dans la ''store queue'' de l'unité LOAD/STORE. Pour rappel, la ''store queue'' met en attente les écritures soit tant que les écritures ne sont pas terminées, soit tant que la RAM est occupée. L'idée est que les écritures sont accumulées dans la ''store queue'' tant que l'instruction ''commit'' n'a pas eu lieu. Le ''store queue'' est "déversé dans la RAM" seulement quand l'instruction ''commit'' s'exécute. ===Les CPU NVIDIA Denver=== Nvidia a tenté quelque chose de similaire avec son projet Denver. NVIDIA a racheté l'entreprise Transmetta, ses brevets, et a tenté de refaire la même chose. Le projet initial était de traduire du code x86 vers un jeu d'instruction VLIW propriétaire, comme l'a fait Transmetta. Mais NVIDIA n'a pas réussi à acquérir la licence du jeu d'instruction x86 et s'est rabattu sur le jeu d'instruction ARM. Le processeur né de ce projet est le Tegra K1-64 CPU. Il s'agit d'un processeur composé d'un cœur VLIW, couplé à deux décodeurs d'instructions ARM. Les deux décodeurs ARM traduisent une paire d'instructions ARM consécutives en instructions VLIW, pour les exécuter sur le cœur VLIW. Le CPU peut fonctionner selon deux modes : ARM et VLIW. En somme, le processeur supporte deux jeux d'instruction : le jeu d'instruction ARM, et un jeu d'instruction VLIW. En mode ARM, les instructions ARM sont chargées depuis le cache, traduites en VLIW par les décodeurs ARM, puis exécutées par le cœur VLIW. En mode optimisé, le CPU exécute des instructions VLIW chargées depuis le cache d'instruction, les décodeurs ne sont pas utilisés. : Le CPU supporte en réalité deux jeux d'instruction ARM : ARM8 et 7. Mais ils correspondent au même mode pour le CPU. Un point important est que le processeur peut changer très rapidement de mode, en à peine quelques cycles d'horloges. On n'est clairement pas dans le cas des CPU x86, qui mettent des plombes pour passer du mode 32 à 64 bits et inversement. La commutation est tellement rapide qu'on peut considérer que le processeur supporte deux jeux d'instruction simultanément. Et le support simultané de deux jeux d'instruction facilite l'implémentation de la traduction binaire. Encore une fois, inutile de traduire du code qui ne sera exécutée qu'une seule fois, mieux vaut l'exécuter directement dans le mode ARM. Surtout que le processeur supporte un mode ARM qui permet d'exécuter du code ARM sans perte de performance. Par contre, le code fréquemment utilisé, notamment dans des boucles critiques, est traduit en VLIW. Au final, les performances sont optimisées, en limitant le travail du logiciel traducteur binaire. La détection du code fréquemment exécuté se fait différemment que pour les processeurs Transmetta et Daisy. L'unité de branchement ne détecte pas directement le code beaucoup exécuté, même si elle a un rôle à jouer. A la place, la détection est réalisée en logiciel, par un ''thread'' dédié, qui s'exécute sur un cœur séparé. Il détecte le code fréquemment utilisé en regardent les branchements exécutés récemment. Pour cela, le processeur mémorise l'historique des branchements pris récemment et copie cette historique dans une mémoire tampon dédiée, partagée entre tous les cœurs. Vu qu'elle est partagée, le ''thread'' de détection a accès à l'historique sans pertes de performances, seule la copie de l'historique dans ce tampon a un cout en performance, pas son partage. Le code VLIW obtenu après traduction binaire est mémorisé dans la mémoire RAM, dans une portion spécialement réservée pour. Elle fait 128 mébioctets, et est appelée le '''cache d'optimisation''' par NVIDIA. Le cache d'optimisation est protégé en écriture et seul le traducteur binaire peut écrire dedans. Au passage, le traducteur binaire est du code VLIW, ce qui fait qu'il est placé dans le cache d'optimisation. Le processeur détecte automatiquement quand une fonction pour laquelle le code compilé équivalent est disponible. Pour cela, l'unité de chargement contient une table de correspondance entre l'adresse de la fonction ARM, et l'adresse de son équivalent compilé. Quand une fonction est appelée, l'unité de chargement regarde l'adresse de destination du branchement. Si l'adresse est dans cette table, elle récupère l'adresse de la fonction compilée et branche vers celle-ci. Le processeur dispose d'un cache d'instruction de 128 kibioctets, ce qui est très important, mais nécessaire vu la taille des instructions VLIW. Le coeur VLIW dispose de ses propres décodeurs, d'un ''scoreboard'', de 7 unités de calcul et de registres. Pour les unités de calcul, il a deux FPU, deux ALU, une unité de branchement, et deux unités LOAD-STORE faisant aussi ALU. Un point important est que le CPU dispose de 64 registres entiers et de 64 registres flottants. C'est deux fois plus que supporte le jeu d'instruction ARM. En mode ARM, seule la moitié des registres est utilisée. Mais en mode optimisé, le traducteur binaire utilise bien les 64 registres, grâce à une sorte de renommage de registres logicielle. Pour simplifier la traduction binaire, le processeur supporte les techniques vues précédemment. Le processeur intègre un ''gated store buffer'' similaire à celui des processeurs Transmetta. Les techniques d'exceptions différées sont aussi supportées, comme sur les processeurs Transmetta. Il a aussi une unité de préchargement décrite par NVIDIA comme agressive, avec support du préchargement de type anticipé (''runahead''). ===L'émulation de la mémoire virtuelle=== Pour finir, il faut parler de la mémoire virtuelle avec la traduction binaire. Sur le processeur source, le processeur gère à la fois des adresses physiques et virtuelles et sa MMU fait la conversion entre les deux. Sur le processeur VLIW, la MMU est simulée par le programme de traduction binaire, partiellement ou totalement. L'implémentation exacte varie suivant que l'on parle du projet Daisy, des processeurs Transmetta ou de Denver. Sur les processeurs Transmetta, il n'y a pas de MMU ni de mémoire virtuelle. A la place, le programme de traduction binaire émule la mémoire virtuelle du processeur source. Le processeur VLIW ne gère que des adresses physiques, rien d'autre. L'espace d'adressage physique du processeur VLIW a la même taille que celui du processeur émulé, ici des processeurs x86. Il est vraisemblable que les adresses physiques utilisées par le processeur x86 sont les mêmes que celles du processeur VLIW. Sur le projet Daisy, le processeur VLIW gère la mémoire virtuelle via pagination, comme les processeurs émulés. Ce qui fait qu'il gère un espace d'adressage virtuel et un espace d'adressage physique. Pour éviter toute confusion, nous parlerons d'adresse physique/virtuelle VLIW pour les adresses physiques/virtuelles du processeur VLIW, d'adresse virtuelle/physique source pour celle du jeu d'instruction traduit, à savoir du Power PC ou du s390, éventuellement de l'x86. Il faut alors faire le lien entre adresses physiques source et adresses virtuelles VLIW. Pour cela, rien de plus simple : il y a correspondance parfaite. L'adresse physique source numéro N correspond à l'adresse virtuelle VLIW numéro N. Cependant, cela signifie que tout l'espace d'adressage virtuel VLIW serait utilisé par le code à traduire. En réalité, il faut ajouter de la place pour le code traduit et le programme de traduction binaire. La conséquence est que l'espace d'adressage virtuel VLIW est plus large que l'espace d'adressage source. L'espace d'adressage virtuel VLIW est découpé en trois sections : une section pour le code Power PC à traduire, une autre pour le code traduit en VLIW, et une dernière réservée au traducteur binaire. Le programme de traduction est placé dans une ROM mappée en mémoire dans la seconde section. Le reste de la seconde section est réservé à une mémoire RAM utilisée par le programme de traduction. [[File:Mémoire virtuelle sur le projet Daisy.png|centre|vignette|upright=1.5|Mémoire virtuelle sur le projet Daisy]] Il faut noter que chaque page physique source, une fois traduite, correspond à N pages virtuelles VLIW. La raison est que le code traduit prend plus de place que le code machine originel. En conséquence, on doit utiliser une page finale plus grande. Pour le reste, des pages contiguës en mémoire physique source sont elles aussi contiguës en mémoire virtuelle VLIW. Le calcul d'adresse est donc simple : l'adresse physique source est multipliée par N, puis on ajoute l'adresse de base à laquelle commence la section pour le code traduit. ==La traduction binaire sur le Burroughs B1700== Nous venons de voir comment accélérer en matériel la traduction binaire, avec des techniques bien spécifiques. Le Burroughs B1700 a procédé autrement et il est intéressant d'étudier en détail son architecture. Son architecture est décrite dans le livre "Interpreting Machines : Architecture and Programming of the Bl700/Bl800 Series", écrit par Elliott I. Organick et James A. Hinds. Une partie de ce qui vfa suivre est un très court résumé de ce livre. ===Le Burroughs B1700 : architecture externe=== Il disposait de 4 registres généraux, nommés X, Y, T et L, tous de 24 bits. Les registres T et L étaient découpés en 6 sous-registres de 4 bits, nommés LA LB LC LD LE et LF pour le registre L, et TA TB TC TD TE et TF pour le registre T. Ils étaient adressables comme n'importe quel registre. Un point important est que le registre T avait des possibilités d’extraction spécifiques. Il était possible d'extrait N bits, placés n'importe où dans ce registre, et de les copier dans un autre registre. Nous verrons pourquoi cette possibilité est intéressante et ne sert que pour le registre T. Les registres X et Y étaient reliés à une ALU de 24 bits capable d'effectuer des opérations arithmétiques basiques. Fait étonnant, l'ALU faisait tous les calculs en même temps et fournissait ses résultats dans 7 registres : un registre SUM pour l'addition, un registre DIFFERENCE pour la soustraction, et un registre par opération bit à bit. Les registres pour les opérations bit à bit sont : CMPX et CMPY pour l'opération NOT sur chaque opérande, XANY pour le ET bit à bit, XEOY pour le XOR, XORY pour le OU logique. Les additions et soustractions pouvaient se faire sur des opérandes codés en binaire ou en BCD, suivant le code opération utilisé. A cela, il fallait ajouter deux registres de "résultat" MSKX et MSKY. Ils fournissaient un masque dépendant de la taille du résultat en bits. Cela permettait de gérer des données de 0, 4, 8, 12, 16, 20, et 24 bits, alors que l'ALU faisait 24 bits. L'ALU prenait en opérande les registres X et Y, mais aussi un registre CYF pour la retenue entrante. En sortie, il fournissait aussi deux retenue sortantes : une pour l'addition et une autre pour la soustraction. Elles étaient mémorisées dans les registres CYL et CYD, respectivement. L'ALU fournissait aussi 12 conditions, qui alimentait un registre d'état. Le registre d'état était segmenté en trois registres appelés XYST, XYCN et BICN. Le processeur contenait de nombreux autres registres de 4 bits, dont les sous-registres de T et L. En tout, il y a 27 registres de 4 bits adressables ! Et le processeur disposait d'une ALU 4 bits pour manipuler ces registres de 4 bits ! Elle est capable de faire des MOV entre registres de 4 bits, des opérations bit à bit, et peut aussi tester si un bit vaut 0 ou 1. La dernière possibilité est très utile pour implémenter les branchements. Pour communiquer avec les entrées-sorties, il disposait de deux registres CMND et DATA : CMND pour envoyer une commande à une entrée-sortie, DATA pour lire ou écrire une donnée. Pour la mémoire, il avait deux registres séparés : READ et WRITE. READ contient la donnée lue lors d'une lecture,; WRITE est pour une donnée à écrire lors d’une écriture. Il s'agit de registres d’interfaçage mémoire, qui ont été promus au rang de registres architecturaux. Les deux registres sont gérés par le microcode, qui est le langage principal de la machine. Les instructions machines configurées font sans, mais le microcode doit y avoir accès facilement. Et vu que le microcode est le langage principal de ce processeur, ces deux registres deviennent architecturaux. ===Le Burroughs B1700 : le mal-nommé ''split-level control store''=== Le Burroughs B1700 avait plusieurs modèles : le B1726, le B1710 et le B1800. Ils se distinguaient par la manière dont le code machine source était traduit par le traducteur binaire. Dans ce qui suit, nous parlerons de traducteur binaire pour parler soit d'un interpréteur pour un langage de programmation de haut niveau, soit pour un logiciel de traduction binaire. Le B1710 plaquait le traducteur binaire en mémoire RAM. Le B1800 faisait pareil, mais avait ajouté un cache dédié au code du traducteur binaire, afin d'améliorer les performances. Le B1726 avait un traducteur binaire scindé en deux : une partie était en mémoire RAM, l'autre était dans un ''local store'' dédié. Le processeur peut exécuter le traducteur binaire soit depuis le ''local store'', soit depuis la mémoire RAM avec une pénalité en termes de performances. La documentation appelle cela un '''''split-level control store''''', mais le terme est trompeur : la documentation qualifie de microcode le langage machine du Burroughs B1700, ce qui est à l'origine de beaucoup de confusions. Une autre source de confusion est que ce langage machine a quelques propriétés qu'on pourrait attendre d'un microcode, mais certaines sont manquantes. Par exemple, il peut adresser les registres d’interfaçage mémoire, mais c'est parce que ce sont des registres architecturaux sur ce processeur. Ses instructions n'encodent pas de signaux de commande, par contre, elles font 16 bits et adressent les opérandes de manière implicite. Le ''local store'' mémorisait au maximum 2048 instructions machine, ce qui fait que les 2048 premières "adresses" correspondaient au ''local store''. Les suivantes étaient des adresses de mémoire RAM qui contenaient le reste du traducteur binaire. En pratique, l'adresse qui séparait les deux était configurable ! On pouvait limiter la taille du ''local store'' à un multiple de 32 instructions ! Les adresses restantes étaient alors réattribuées à la RAM. Par exemple, on pouvait attribuer 64 adresses au ''local store'', les 2048 - 64 adresses restantes étaient alors attribuées à la mémoire RAM. Un registre limite, nommé TDPM, indiquait quelle était l'adresse de démarcation entre les deux. Il contenait 4 bits, dont la valeur était multipliée par 32, pour obtenir l'adresse limite. Le programmeur devait découper le traducteur binaire en segments, certains étant placé dans le ''local store'', les autres étant en mémoire RAM. Mieux que ça, le processeur autorisait d'utiliser la technique de l'''overlaying'' pour le microcode ! Il y avait même une fonction OVERLAY dédiée pour ! OVERLAY avait besoin de trois opérandes : la taille du segment à copier, son adresse source en mémoire RAM, son adresse de destination dans la micro-SRAM. L'interpréteur réservait un "segment" en mémoire RAM, pour les données, dont le processeur connaissait la position. En-dehors de ce segment, il n'y a que des instructions machines à traduire, et le traducteur binaire. Le CPU incorporait pour cela deux registres, un pour l'adresse de base du segment de données, un autre pour son adresse de fin. Il incorporait une protection mémoire limitée sur ce segment, à savoir que seul l'interpréteur pouvait lire ou écrire dans ce segment. ===Une architecture bit-adressable=== Le CPU Burroughs B1700 était '''bit-adressable'''. En clair, il pouvait adresser la mémoire bit par bit, si nécessaire. L'avantage est que cela permettait d'émuler des architectures dont la taille des registres/''byte''/mots était très différente. Un autre avantage est que cela facilitait la traduction binaire des instructions machine. Détaillons rapidement le premier avantage. Le processeur était un CPU 24 bits, mais il pouvait émuler des processeurs 16 bits, 8 bits, 4 bits, 1 bit, 9 bits, ou toute autre valeur. Pour cela, le processeur lisait des mots de 24 bits et utilisant un ''barrel shifter'' pour sélectionner les bits adéquats. Par exemple, pour un accès de 16 bits, la donnée lue en mémoire était masquée de manière à ne garder que les 16 bits de poids faible. Le ''barrel shifter''/circuit de masquage était placé directement avant le bus mémoire, sur le trajet des données/instructions. Le ''barrel shifter'' était commandé par un registre ''Field Unit'', qui précisait quelle était la taille des données à charger. De plus, le processeur avait une '''ALU de taille variable'''. En clair, l'ALU pouvait faire des calculs sur un nombre de bits compris entre 0 et 24. Pour cela, les calculs étaient fait avec une ALU de 24 bits, puis les bits de poids fort inutiles étaient masqués. L'ALU de 24 bits pouvait masquer les bits de poids fort du résultat, mais avec des limitations. Elle gérait des opérandes et résultats de 0, 4, 8, 12, 16, 20 et 24 bits. Pour avoir un réglage plus fin, les registres X et Y pouvaient être découpés en morceaux de 4 bits, qui étaient traités par une ALU de 4 bits séparés. ===Le code machine du Burroughs B1700=== Le processeur utilise des micro-opérations de 16 bits de long, qui sont copiées dans registre d'instruction M de 16 bits. Fait intéressant, il était possible de faire un OU logique entre l'instruction lue et un registre du processeur, le résultat étant mémorisé dans le registre M. Cela permettait de faire du code automodifiant, pour émuler des modes d'adressage complexe. Cela permettait par exemple de remplacer une adresse dans une micro-opération. L'adresse est alors surimposée avec un OU dans le champ d'adresse, alors que le reste de l'instruction n'est pas modifié. Afin de gérer les interruptions, ces dernières étaient partiellement détectées en logiciel. Les interruptions ne s'exécutaient pas automatiquement, pour émuler correctement les interruptions. Une interruption doit être prise en compte à la toute fin de l'exécution d'une instruction, quand celle-ci a finit son travail. Mais vu qu'ici, les instructions sont émulées avec des séries de micro-opérations, il y a un problème : il est possible d'interrompre une instruction émulée en plein milieu. Pour éviter cela, le processeur ne déclenche pas les interruptions matérielles immédiatement, elles sont mises en attente. Une micro-opération dédiée vérifie si une interruption est en attente, et l'exécute alors cas échéant. La micro-opération est utilisée comme micro-opération finale d'une instruction émulée. Le ''program counter'' est appelé le registre A. Les instructions du processeur font 16 bits de long, et elles sont alignées en RAM sur 16 bits, ce qui fait que les 4 bits de poids fort de ce registre valent 0, seuls les 14 bits de poids fort sont utiles. Les adresses mentionnées plus haut, qui adressent les micro-opérations en mémoire RAM, sont des adresses de mots de 16 bits, pas les adresses de bit individuels. ===La traduction binaire est aidée par des registres et instructions dédiées=== Les instructions à décoder n'ont pas de longueur fixe, à savoir qu'elles peuvent faire 16 bit avec tel jeu d'instruction, 8 bits avec un autre, 32 bits sur un autre, etc. Elles peuvent aussi être de longueur fixe ou variable ! Le B1700 intégrait donc un support des instructions de longueur variable, qui était "détourné" pour gérer des jeux d'instruction différents. Notons que vu que l'architecture est bit-adressable, les contraintes d'alignement des instructions disparaissent, ce qui a des avantages qu'on va détailler dans ce qui suit ! L'implémentation des instructions de taille variable ajoutait un registre avant le registre d'instruction. Le registre d'instruction proprement dit était un registre M de 24 bits. Il était précédé par un registre T, dans lequel l'instruction était assemblée, en combinant plusieurs morceaux. Si l'instruction est codée est codée sur moins de 24 bits, le registre T se contente de masquer les bits en trop. Si l'instruction chargée dépasser 24 bits, le chargement se faisait en plusieurs passes, avec plusieurs lectures en mémoire. Dans ce qui suit, on suppose que l'instruction est composée de '''champs''' : l'opcode, les opérandes, les adresses absolues, les constantes immédiates, les numéros de registre, etc. L'idée est que l'instruction est chargée dans le registre T, puis découpée en champs, qui sont copiés dans les registres du processeur ou envoyés au microcode. Je rappelle que le registre T est relié à un ''barrel shifter'', différent au précédent, qui permettait de masquer, décaler et extraire une donnée de ce registre. Et il y a la possibilité d'extraire une suite de bit de ce registre T, pour la copier dans un autre registre. Par exemple, prenons une instruction composée d'un opcode, d'une constante immédiate et d'une adresse immédiate. L'opcode est lu et interprété par le microcode. Le microcode extrait la constante immédiate du registre T et la copie dans le registre X. Ensuite, l'adresse est extraite et envoyée au registre FA, une lecture est démarrée, et l'opérande est copiée dans le registre Y. Enfin, l'instruction est exécutée par le microcode. Le tout prend plusieurs micro-opérations : une première pour lire l'opcode, une seconde pour lire la première opérande, une troisième pour lire la seconde opérande, una quatrième pour tout exécuter. Notons que suivant la taille des champs, on peut en charger plusieurs à la fois. Par exemple, imaginons qu'on ait chargé 24 bits, contenant un opcode de 8 bits et une constante immédiate de 16 bits : pas besoin de faire deux accès mémoire, le registre T est juste découpé en un opcode et une constante. Le registre T est fait pour ce genre d'extraction. ===Le chargement d'un champ=== L'architecture est bit-adressable car elle gére des champs qui ne font pas forcément 8 ou 16 bits. Par exemple, certains jeux d'instructions ont des opcodes codés sur 6 ou 7 bits, voire 9-10 bits. Par exemple, prenons un opcode de 7 bits, suivi par une adresse de 12 bits et une constante immédiate de 15 bits : aucun de ces champs ne sera aligné sur 8/16/24 bits. Utiliser une architecture bit-adressable règle le problème d'alignement. Et en général, les contraintes d'alignement des instructions disparaissent ! Quand on charge une instruction, peut importe qu'elle soit ou non alignée sur 24 bits, elle sera placée dans les bits de poids faible, il suffira de masquer les bits de poids fort inutiles. Par exemple, si on charge une instruction de 16 bits, les 16 bits seront copiés dans les 16 bits de poids faible du registre T, il suffira de masquer l'octet de poids fort. Pas besoin de faire de décalages pour gérer des instructions mal alignées... Pour charger une instruction, il faut connaitre plusieurs informations : l'adresse de son premier bit, sa taille. Pour cela, le B1700 contient deux registres, nommés ''Field Adress'' (FA) et ''Field Length'' (FL), qui indiquent l'adresse du premier bit et la taille de l'instruction à charger. Le premier fait 24 bits, ce qui permet de gérer des adresses de 24 bits. N'oubliez pas que le processeur est bit-adressable, ce qui permet d'adresser 16 Mébi-bits, soit 2 mébioctets. Le registre pour la taille fait lui 16 bits, ce qui permet de gérer des instructions de 65536 bits, soit 8192 octets ! Leur nom commence par ''field'', car le processeur peut gérer des instructions de taille variable et/ou plus longues que 24 bits. Dans ce cas, le chargement de l'instruction se fait champ par champ. Pour donner un exemple, prenons une instruction composée de 20 champs de 12 bits chacun. Les champs sont chargés un par un dans le registre T. Le registre FA est alors initialisé avec l'adresse de l'instruction, il est incrémenté de 12 à chaque cycle. Le registre FL est initialisé avec la taille de l'instruction, soit 20 * 12 = 240 bits, et il est décrémenté de 16 à chaque cycle. Précisons cependant que la taille d'un champ peut être changée d'un cycle à l'autre. Par exemple, le processeur peut charger un opcode de 8 bits, l'interpréter, détecter que l'instruction demande ensuite de charger une adresse de 16 bits. Dans ce cas, le registre FL est immédiatement altéré pour charger 16 bits, au lieu des 8 bits de l'opcode immédiatement précédent. ==Les jeux d'instructions dédiés à un langage de programmation== De rares processeurs sont conçus pour un langage de programmation en particulier. On appelle ces processeurs, conçus pour des besoins particuliers, des '''processeurs dédiés'''. Par exemple, les fameux ''Burrough E-mode'' B5000/B6000/B7000 étaient spécialement conçus pour exécuter de l'ALGOL-60. Leurs cousins B2000/B3000/B4000 étaient eux conçus pour le COBOL. Des langages fonctionnels ont aussi eu droit à leurs processeurs dédiés. Le prolog en est un bel exemple, avec les superordinateurs de 5ème génération qui lui étaient dédié. On peut aussi citer les machines LISP, dédiés au langage LISP, qui datent des années 1970. Elles étaient capables d’exécuter certaines fonctions de base du langage directement dans leurs circuits : elles possédaient notamment un ''garbage collector'' câblé dans ses circuits ainsi que des instructions machines supportant un typage déterminé à l’exécution. Les processeurs dédiés ont eu leur heure de gloire au début de l'informatique, à une époque où les langages de haut niveau venaient d'être inventés. À cette époque, les compilateurs n'étaient pas performants et ne savaient pas bien optimiser le code machine. Il était alors rationnel, pour l'époque, de rapprocher le code machine cible et le langage de programmation de haut niveau. De nombreuses architectures dédiés ont ainsi été inventées, avant que les concepteurs se rendent compte des défauts de cette approche. Les défauts en question ne sont pas nombreux, mais assez simples à comprendre. Premièrement, elles sont très rapides pour un langage de programmation en particulier, mais sont assez mauvaises pour les autres, d'où un problème de "compatibilité". Ajoutons à cela que les langages de programmation peuvent évoluer, devenir de moins en moins populaires/utilisés, ce qui rend la création d'architectures généralistes plus pertinente. Enfin, les architectures dédiées sont évidemment des processeurs CISC, pour implémenter les nombreuses fonctionnalités des langages évolués. Et les défauts des CISC sont assez rédhibitoires à l'heure actuelle. À l'heure actuelle, les algorithmes des compilateurs se sont améliorés et savent nettement mieux utiliser le matériel. Ils produisent du code machine efficace, ce qui rend les architecture dédiées bien moins intéressantes. Si on ajoute les défauts de ces architectures dédiées, par étonnant que les architectures dédiées aient presque disparues. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'accélération matérielle de la virtualisation | prevText=L'accélération matérielle de la virtualisation | next=Le matériel réseau | nextText=Le matériel réseau }} </noinclude> 69s3un4m0j6iun0bptop1lml1zem9pt 764048 764047 2026-04-19T20:25:57Z Mewtow 31375 /* La traduction binaire sur le Burroughs B1700 */ 764048 wikitext text/x-wiki De nos jours, la majorité des programmeurs programment dans des langages de haut niveau. Il est très rare pour eux d'avoir à utiliser de l'assembleur, et encore moins un langage machine. Les programmes écrits dans un langage de haut niveau sont traduits en langage machine par un logiciel appelé le compilateur. [[File:Compilation.PNG|centre|vignette|upright=2.5|Compilateur : principe.]] Il se trouve que quelques rares processeurs sont conçus pour faciliter le travail du compilateur. Et ce sont ces processeurs que nous allons voir dans ce qui suit. Ils représentent une catégorie de jeux d'instruction à part, qui n'a pas vraiment de nom. ==La compilation et l’interprétation== Un compilateur traduit donc du code source, écrit dans un langage de haut niveau, vers du langage machine. La traduction est rarement directe. En général, le compilateur traduit le code source en assembleur, qui est lui-même traduit en langage machine. L'assembleur est une représentation textuelle du langage machine alors que le code machine est du binaire exécutable par le processeur. La traduction se fait donc en deux étapes, la '''compilation''' proprement dite et l''''assemblage''', réalisées respectivement par un compilateur et un assembleur. Il faut parfois rajouter une troisième phase d'édition des liens, que nous passons volontairement sous silence. {| |[[File:Code C.png|vignette|200px|Le code source, ici en C.]] |[[File:Assem.png|vignette|250px|Le code assembleur.]] |[[File:Binary file - hello world (C programming).png|vignette|150px|Le code machine.]] |} ===L'interprétation et la compilation à la volée=== Une alternative à la compilation est l''''interprétation''', qui transforme le code source en code machine à la volée. Avec l'interprétation, le code source est passé à un logiciel appelé l''''interpréteur''', qui exécute le code source ligne de code par ligne de code. Une ligne de code est traduite en code machine, qui est exécutée, puis l'interpréteur passe à la ligne suivante. [[File:Kaantotulkkaus.png|centre|vignette|upright=3|Compilation versus interprétation.]] Un défaut de l'interprétation est son cout en performance, qui est assez important. Aussi, l'interprétation stricte a évolué vers un hybride entre interprétation et compilation. L'idée est qu'une partie peu importante du code source est interprétée, alors que le code important est compilé juste avant d'être exécuté. Pour le dire autrement, le code source est partiellement compilé à la volée, juste avant son exécution. Aussi, on parle de '''compilation à la volée'''. Le terme en anglais est ''Just In Time compilation'', abrévié en JIT. En soi, le JIT est associé à de l'interprétation, les JIT sont en réalité des hybrides interpréteurs-compilateurs. Un point important est que compiler du code à la volée est assez lourd, cela a un cout en performance. Par contre, le code compilé s'exécute plus vite que du code interprété. Si on exécute le code compilé une seule fois, le cout de la compilation l'emporte sur le gain à l'exécution. Mais le code compilé est réutilisé autant de fois que nécessaire. Le gain à l'exécution est donc multiplié par le nombre d'exécutions, alors que le cout de la compilation est répartit sur plusieurs exécutions, il est amorti. Les portions du code sont choisies de manière à avoir un gain en performance maximal. L'idée générale est que le code exécuté très souvent est compilé, alors que du code exécuté pas souvent est interprété. Typiquement, les boucles les plus souvent exécutées sont compilées, le code exécuté une seule fois est interprété. ===Les langages intermédiaires=== Les compilateurs modernes passent par un '''langage intermédiaire''' pour faire la transformation en code machine. Le compilateur traduit le langage de haut niveau en langage intermédiaire, puis traduit le langage intermédiaire en code machine cible. Le langage intermédiaire est parfois appelé le '''''bytecode''''', et ce terme recouvre aussi le code écrit avec du ''bytecode''. L'interprétation aussi peut passer par l'intermédiaire d'un un ''bytecode'', ce n'est pas limité aux compilateurs proprement dit. {| |[[File:Intermediate representation scheme.png|centre|vignette|upright=1.5|Représentation intermédiaire d'un compilateur.]] |[[File:Code interpreter scheme.png|centre|vignette|upright=1.5|Fonctionnement d'un interpréteur.]] |} Faire ainsi a de nombreux avantages, le principal étant d'avoir un compilateur capable de traduire un langage de haut niveau vers plusieurs jeux d'instructions différents. Cela permet d'avoir, par exemple, un compilateur qui traduit du C soit en code machine x86, soit en code machine pour un CPU ARM, soit pour un CPU POWERPC, etc. [[File:Compiler design.svg|centre|vignette|upright=3|Compiler design]] Pour l'interprétation, l'usage d'un ''bytecode'' a d'autres avantages. Pour l'exploiter, il faut distribuer non pas le code source, mais le ''bytecode''. En clair, l'interpréteur ne prend pas en entrée le code source, mais du ''bytecode'' déjà compilé. La traduction est alors beaucoup plus simple, car le code source a déjà été partiellement compilé, par la traduction en ''bytecode''. Interpréter du code source est en effet assez compliqué : il faut effectuer des étapes d'analyse lexicale, sémantique, et bien d'autres. Avec du ''bytecode'', ces étapes ont été réalisées lors de la compilation du ''bytecode'', l'interpréteur a alors peut de choses à faire. L'interprétation du ''bytecode'' se fait instruction par instruction, au niveau du ''bytecode''. Une instruction du ''bytecode'' est traduite en une instruction machine équivalente, qui est exécutée. Si l'instruction est un peu complexe, il exécute une fonction/procédure qui fait la même chose. Pour résumer, le ''bytecode'' est ensuite traduit à la volée et exécuté instruction par instruction par un logiciel appelé l'interpréteur. [[File:Control table.png|centre|vignette|upright=2|Control table]] Le langage intermédiaire peut être vu comme l'assembleur d'un processeur, n'existe pas forcément dans la réalité, mais dont le jeu d'instruction est décrit en détail. Le processeur en question est appelé une '''machine abstraite''', ou encore une ''machine virtuelle''. Nous utiliserons le terme de machine abstraite dans ce qui suit. La machine abstraite n'est pas la même suivant que l'on cible la compilation ou l'interprétation/JIT. Pour l'interprétation/JIT, la machine abstraite est souvent une machine à pile, car cela simplifie grandement la traduction en code machine final. L'interprétation demande que la traduction du ''bytecode'' en code machine soit la plus simple possible. Et cela demande de prendre en compte pas mal de détails. Par exemple, les différents jeux d'instruction existants n'ont pas le même nombre de registres, ce qui pose problème lors de la traduction du ''bytecode''. Lors de la transformation en code machine, un algorithme d'allocation de registres se débrouille pour traduire le code intermédiaire en code qui utilise un nombre limité de registres. Pour améliorer cette allocation de registres, il y a deux solutions niveau ''bytecode'' : soit utiliser un nombre illimité de registres, soit utiliser une machine à pile. Pour les machines à pile, il existe un algorithme simple et rapide pour traduire un code écrit pour une machine à pile en un code écrit pour un processeur avec des registres, qui se débrouille pas trop mal pour allouer efficacement les registres. C'est un avantage assez important pour les langages interprétés. ==Les processeurs qui exécutent du bytecode== Le ''bytecode'' est un code machine, ce qui signifie qu'il peut en théorie s'exécuter sur un processeur qui implémente le jeu d’instruction associé. Si le jeu d'instruction d'un ''bytecode'' est souvent une description censée être fictive, elle n'en reste pas moins un jeu d'instruction et des caractéristiques précises, qu'on peut l'implémenter en matériel ! Néanmoins, tous les ''bytecode'' ne sont pas égaux de ce point de vue. Certains sont faciles à implémenter en matériel, d'autres non. Les ''bytecodes'' implémentés en matériel sont ceux dont les machines abstraites sont des machines à pile. Un premier exemple est celui des processeurs Pascal MicroEngine, qui exécutaient directement le bytecode du langage Pascal, le fameux UCSD P-code. Un second exemple est la '''machine SECD''', qui sert de langage intermédiaire pour certains compilateurs de langages fonctionnels. Elle a été implémentée en matériel par plusieurs équipes, notamment par les chercheurs de l'université de Calgary en 1989. Dans le même genre, quelques processeurs simples étaient capables d’exécuter directement le ''bytecode'' du langage FORTH. Le FORTH, un des premiers langages à pile de haut niveau, possède de nombreuses implémentations hardware et est un des rares langages de haut niveau dont le ''bytecode'' a été utilisé comme langage machine sur certains processeurs. Par exemple, on peut citer le processeur FC16, capable d’exécuter nativement du ''bytecode'' FORTH. ===Les processeurs Java=== Le cas le plus impressionnant est celui de la machine virtuelle Java, qui est un design de processeur comme un autre. En temps normal, le ''bytecode'' Java est compilé ou interprété, mais certains processeurs exécutaient du ''bytecode'' Java directement, sans interprétation ni compilation. Ils sont appelés des '''processeurs Java'''. On peut citer les processeurs ARM disposant de l'extension Jazelle, du JEMCore et du aJ-100 de aJile Systems, le picoJava II de Sun, et quelques autres. Il y a aussi eu quelques projets de recherche ou processeurs open source, comme Komodo, jamuth, le ''Java Optimized Processor'' et quelques autres projets du même genre. L'implémentation de la machine virtuelle Java n'était cependant pas complète. Quelques instructions complexes n'étaient pas gérées par le processeur et devaient être émulées en logiciel. Mais la grosse majorité des instructions l'était. Les instructions non-supportées étaient émulées avec la méthode ''trap and emulate'', à savoir qu'un opcode inconnu déclenchait une exception matérielle ''opcode invalide'', dont la routine pouvait émuler les instructions qui doivent l'être. Le processeur aJ-100 de aJile est capable d'exécuter la quasi-totalité des instructions de la JVM Java, seules deux instructions faisant exception. Les '''accélérateurs Java''' sont une solution intermédiaire, qui permet de traduire à la volée du ''bytecode'' Java en instructions RISC, exécutées sur un autre processeur. Il s'agit formellement de traduction binaire, mais je préfère en parler ici. L'idée est de combiner un processeur RISC avec un coprocesseur Java. Le coprocesseur lit le ''bytecode'' Java et le traduit en instructions RISC, ces dernières étant exécutées sur le processeur RISC. Le JA108 de Nozomi était un coprocesseur de ce genre, au même titre que le coprocesseur JSTAR de JEDI. L'extension Jazelle de certains CPU ARM fonctionnait vraisemblablement sur un principe similaire, sauf que le coprocesseur était intégré dans le processeur ARM, entre le cache d'instruction et le décodeur d'instructions ARM. L'intérêt des processeurs Java n'est pas qu'une question de performance. Certes, ils permettent d’exécuter plus vite les programmes compilés en ''bytecode'', comme des programmes Java pour la JVM Java. Mais l'intérêt est aussi de faire des économies de mémoire RAM et ROM. Je rappelle que la machine virtuelle Java est une machine à pile, ce qui fait que sa densité de code est excellente. Les programmes en ''bytecode'' Java sont donc très petits, et prennent peu de place en mémoire ROM. De plus, utiliser un processeur Java permet de se passer d'interpréteur ou de JIT Java, ce qui économise encore plus de mémoire. ===Transformer un processeur RISC en machine à pile=== La plupart des processeurs Java utilisent en interne une architecture à registre généraux, qui émule une machine à registre en microcode. Quelques registres sont réservés pour l'état de la machine virtuelle, avec au minimum un registre réservé pour le pointeur de pile, un autre pour un pointeur vers un ''pool'' de constantes, et quelques autres. D'autres registres servent à mémoriser le haut de la pile, à savoir le sommet de la pile et quelques opérandes situées juste en-dessous. Par exemple, le processeur picoJava II gardait les 64 opérandes au sommet de la pile dans 64 registres généraux, séparés des registres pour les pointeurs et l'état de la JVM. Une optimisation possible, envisagée sur les processeurs picoJava, était une sorte de macro-fusion sous stéroïde. L'idée était de fusionner certaines séries d'instructions Java en une seule instruction machine. Typiquement, pour faire une opération arithmétique, une machine à pile empile deux opérandes et exécute une opération, ce qui prend trois instructions machine. La spécification picoJava 1 a proposé d'envoyer directement la seconde opérande en entrée de l'ALU, ce qui fusionnait l'empilement de la seconde opérande et l'opération arithmétique. Le processeur picoJava II va plus loin et fusionne les trois instructions en une seule instruction RISC, agissant sur des registres. Mais la majorité des processeurs Java ne faisaient pas cela, car cela complexifie grandement le décodeur. Les processeurs ''bigfoot'' de DCT utilisaient un système différent pour émuler une pile avec des registres. Leur banc de registre de 64 bits était coupé en deux : les 16 premiers registres étaient utilisés pour la pile, les 48 suivants étaient utilisés pour autre chose. L'idée est que le registre 0 mémorise un emplacement vide, le registre 1 contient le sommet de la pile, le registre 2 l'opérande sous le sommet de la pile, et ainsi de suite. Les 15 premières opérandes de la pile sont donc mémorisées dans les registres. Les instructions écrivent leur résultat dans le registre adéquat, puis tous les registres sont décalés pour que le résultat passe dans le registre R1. Par exemple, les instructions PUSH chargent une opérande dans le registre 0, puis tous les registres sont décalés de un rang, pour que l'opérande chargée soit dans le registre 1. Le registre 0 devient alors le registre numéro 1, le registre 1 devient le 2, et ainsi de suite. Par contre, les opérations arithmétiques font autrement. Une instruction d'addition, par exemple, lit les opérandes dans le registre 1 et 2, puis place son résultat dans le registre 2 (les deux opérandes sont dépilées). Puis, les registres sont décalés d'un rang pour que ce registre 2 devienne le 1. Les registres ne sont en réalité pas décalés, il n'y a pas de transferts entre registres. A la place, les 16 premiers registres utilisaient une forme limitée de renommage de registres, similaire au banc de registre tournant utilisé sur les processeurs EPIC comme l'Itanium. Les numéros de registres sont renommés, décalés, à chaque fois qu'on empile ou dépile une opérande. Pour faire le renommage de registres, processeur contient un ''register stack counter'' de 4 bits, qui est incrémenté à chaque fois qu'on empile une donnée, et est décrémenté quand on dépile une donnée. Quand on veut accéder à un opérande, le numéro de registre architectural associé est traduit en numéro de registre physique, en additionnant le ''register stack counter'' (modulo 16, pour rester sur 4 bits). ==L'accélération de l'interprétation/JIT : le ''Thumb-EE'' d'ARM== Quelques processeurs ont ajouté des instructions pour faciliter le travail des interpréteurs/JIT. Par exemple, en 2005, ARM a ajouté le mode ''Thumb Execution Environment'', qui faisait cela. Il reprenait le jeu d'instruction compact ''thumb'' et ajoutait quelques instructions et modifiait le comportement d'instructions existantes. Le jeu d'instruction ''Thumb-EE'' a été ajouté sur les CPU ARM en 2005, mais a été déprécié en 2011, par manque d'utilité. Mais la tentative mérite qu'on s'attarde dessus. Les CPU de gamme M, déstinés à l'embarqué, n'ont jamais supporté ''Thumb-EE'', vu que de tels processeurs ne sont pas conçus pour exécuter du code interprété/JIT. Le mode ''Thumb-EE'' est un mode d'exécution, séparé de l'ARM normal. Ainsi, une instruction ''Thumb-EE'' et une instruction ARM peuvent avoir le même encodage en binaire, mais se comporter différemment. Le processeur est à tout moment soit en mode ''Thumb-EE'', soit en mode ARM normal, ce qui précise comment ces instructions doivent se comporter. Des instructions ont été ajoutées par ''Thumb-EE'', pour qu'il fasse son travail. L'entrée et la sortie du mode ''Thumb-EE'' se fait avec deux instructions : ENTERX et LEAVEX. Une fois en mode ''Thumb-EE'', le décodage des instructions ''thumb'' se fait avec les règles du ''Thumb-EE'', et non celles du ''thumb'' normal. Une première différence entre ''thumb'' normal et ''Thumb-EE'' est l'ajout de ce qui s'appelle un ''null check'' pour les instructions mémoire. Le ''null check'' est utilisé pour les instructions en mode d'adressage "base + indice" ou "Base + décalage", ou tout autre mode d'adressage avec un registre de base utilisés dans des calculs d'adresse. L'idée est de vérifier si l'adresse de base vaut zéro ou non. Si c'est le cas, le processeur lève une exception matérielle, qui est traitée par l'interprétation ou le compilateur JIT. Une telle situation est en effet signe d'une erreur d'adressage mémoire, qui doit être traitée par l'interpréteur. Typiquement, c'est signe qu'un pointeur n'a pas bien été initialisé, ce qui peut arriver avec du code interprété ou JIT. Les interpréteurs/JIT traitent généralement la situation en ajoutant un test avant toute instruction mémoire, pour vérifier si le pointeur accédé vaut zéro ou non. Avec ''Thumb-EE'', pas besoin d'ajouter les tests en questions : ils sont réalisés automatiquement lors de chaque instruction mémoire. : Les registres pour la pile, comme le pointeur de pile et le pointeur de ''frame'', sont aussi concernés. L'instruction CHKA vérifie que les accès à un tableau ne débordent pas en dehors du tableau en question. Par exemple, pour un tableau de 1024 éléments, elle vérifie si l'indice est compris entre 0 et 1024. Pour cela, elle vérifie si l'indice de tableau est bien dans l'intervalle adéquat et lève une exception matérielle si ce n'est pas le cas. Son utilité se comprend quand on sait que tous les accès à un tableau sont vérifiés par l'interpréteur. L'interpréteur est censé ajouter des instructions pour vérifier les indices, à savoir deux ou trois branchements. Avec CHKA, le test se fait en une seule instruction. D'autres modifications mineures de l'encodage des instructions sont aussi présentes, ainsi que des modifications pour les instructions LOAD/STORE. Précisément, dans les modes "base + indice" et "base + décalage", le décalage et l'indice subissent maintenant des décalages. L'idée est de simplifier le calcul d'adresse. De quoi économiser quelques instructions lors des calculs d'adresse, ce qui facilite le travail de l'interpréteur/JIT. Et des instructions LOAD/STORE précises voient leur comportement modifié pour faciliter la gestion de la pile de Java ou l'exécution des méthodes locales. Pour faciliter le travail de l'interpréteur, deux instructions HBP et HBLP ont été ajoutées. Elles branchent vers une fonction qui gère les exceptions logicielles (une fonctionnalité présente dans de nombreux langages, comme Java). La première effectue un branchement vers la fonction en question, la seconde sauvegarde l'adresse de retour avant de faire ce branchement. Les deux peuvent brancher vers 256 fonctions pré-déterminées, sans avoir à présenter leur adresse. ==La traduction binaire accélérée par le matériel== Plus haut, nous avons surtout parlé des compilateurs et des interpréteurs. Cependant, nous devons aussi parler de la '''traduction binaire'''. Elle traduit un programme écrit dans un code machine vers un autre code machine. Par exemple, elle traduit un programme compilé pour un CPU x86 vers un code machine ARM. En général, le système d'exploitation est généralement compilé pour le jeu d'instruction natif, mais il exécute des applications prévues pour le x86. Les applications sont traduites par le système d'exploitation, avant d'exécuter le code traduit. : Nous parlerons dans la suite de code machine source et de code machine cible, pour parler respectivement du code à traduire et du code obtenu après traduction. Idem avec d'autres termes comme architecture cible/source, ou jeu d'instruction cible/source. La traduction binaire est surtout utilisée pour des questions d'émulation ou de compatibilité. Par exemple, lorsque les Macintosh sont passés de processeurs Power PC vers des processeurs x86, le système d'exploitation Mac OS utilisait la traduction binaire pour convertir les anciennes applications Power PC vers du code x86. Les utilisateurs n'y ont vu que du feu. Le système de traduction binaire était appelé Rosetta 1. Par la suite, lors de la transition de processeurs x86 vers des processeurs Apple, Rosetta 2 a vu le jour. La traduction binaire a surtout été utilisée pour traduire du code x86 vers un autre jeu d'instruction. Il faut dire que le x86 est le jeu d'instruction dominant. De nombreuses entreprises ont eu pour ambition de briser l'hégémonie du x86 sur PC, en remplaçant le x86 par un jeu d'instruction plus performant, tout en gardant une compatibilité maximale. La traduction binaire était la seule solution pratique. Elle était le plus souvent intégralement réalisée en logiciel, comme c'était le cas sur les architectures Itanium avec le ''IA-32 Execution Layer''. ===Les généralités sur la traduction binaire assistée en matériel=== L'architecture source est presque tout le temps une architecture CISC, assez ancienne, qu'on souhaite émuler. Le choix de l'architecture cible se porte souvent sur une architecture VLIW, fort différente de l'architecture source, et ce pour obtenir une économie de matériel conséquente. Pour s'exécuter rapidement, le code traduit doit exploiter le parallélisme d'instruction, à savoir exécuter plusieurs instructions en même temps dans des unités de calcul séparées. Il est possible d'utiliser un processeur superscalaire avec exécution dans le désordre pour cela, mais au prix d'un cout important en transistors. Alors qu'en utilisant un CPU VLIW, c'est le traducteur binaire qui fait tout le travail d'extraction du parallélisme d'instruction. Et après tout, quitte à avoir un traducteur binaire, autant lui refiler le boulot d'optimisation. Compiler du code à la volée est certes assez lourd, mais qu'il existe des algorithmes efficaces pour regrouper des instructions indépendantes dans une seule instruction VLIW. Au passage, le processeur VLIW a plus de registres que le processeur source. Cela permet de faire du renommage de registres directement en logiciel. Le traducteur binaire n'hésite pas à changer les noms de registres entre instructions source et instruction VLIW cible, afin de supprimer des dépendances de données. En conséquence, cela ouvre des opportunités de parallélisme, qui sont exploitées lors du regroupement des opérations en instructions VLIW. Vous pourriez penser que le choix d'un CPU VLIW pour émuler du CISC est tout sauf optimal. Vous devez penser que la traduction binaire est d'autant plus simple que l'architecture source et cibles sont semblables. Dans les faits, ce n'est pas tellement traduire les instructions qui pose problème, mais plus la gestion du registre d'état, des exceptions matérielles, de la mémoire virtuelle, la différence entre gros-boutisme et petit-boutisme, de même que des différences d'adressage pour les périphériques. Le premier problème est la gestion des conditions, notamment en présence d'un registre d'état. En pratique, la traduction binaire s'utilise pour traduire du code CISC vers du code RISC ou VLIW. Les architectures source ont donc un registre d'état, qui est mis à jour non seulement par des instructions de test, mais aussi des instructions arithmétiques. L'architecture cible n'a elle pas de registre d'état, mais des registres à prédicats. La traduction de l'un vers l'autre est alors quelque peu compliquée. Et elle est d'autant plus compliquée que le registre d'état est la source de dépendances d'instruction implicites, qui réduisent les performances. Par exemple, si une instruction arithmétique modifie le registre d'état, cela peut impacter l'exécution d'une instruction ultérieure, qui lit ce registre d'état. Pour éliminer ces fausses dépendances, le traducteur binaire doit renommer ce registre en logiciel et détecter les dépendances utiles. Et le compilateur doit gérer les cas où il y a beaucoup de distance entre les deux, voire les cas où l'instruction dépendante n'a pas encore été analysée par le compilateur. Un autre problème est lié à la gestion des exceptions matérielles, et précisément des exceptions précises. Pour rappel, une instruction VLIW regroupe plusieurs opérations, opérations qui correspondraient à une instruction machine sur un CPU pas VLIW. En conséquence, plusieurs instructions du langage machine "source" sont regroupées en une seule instruction VLIW. Et il faut tenir compte du cas où une instruction source lève une exception. Dans le code VLIW, cela signifie qu'une opération lève une exception, et il faut annuler partiellement l'instruction VLIW associée. Et par partiellement, on veut dire que seules les opérations suivantes dans l'ordre du programme source doivent être annulées, puis ré-exécutées. Et c'est un sacré casse-tête ! Les processeurs VLIW utilisent comme solution le mécanisme d'exceptions différées des processeurs EPIC, vu il y a quelques chapitres. Pour résumer, le code est exécuté par blocs d'instruction, délimités par des branchements ou tout autre limite/barrière pertinente dans le code. Un bloc de code est compilé en deux versions : une version rapide sans exceptions matérielles, une version lente qui gère les exceptions précises. La version rapide s'exécute sans exécuter les exceptions. Cependant, les exceptions matérielles sont enregistrées, pour être prises en compte à la toute fin du bloc de code. La version rapide est exécutée en premier et elle mémorise si une exception matérielle a eu lieu. Puis une instruction vérifie si une exception a eu lieu et décide quoi faire. Si aucune exception n'a eu lieu, elle passe à la suite du programme, les résultats du bloc de code sont définitivement acceptés. Mais si une exception a eu lieu, tout est annulé. Le processeur est remis dans l'état initial, puis le code est ré-exécuté instruction par instruction de manière à gérer l'exception correctement. ===La traduction par pré-décodage=== Une première solution serait de faire la traduction binaire dans le cache d'instruction. Lors d'un défaut de cache, le code chargé depuis la RAM est traduit en code machine cible. Et c'est ce code machine cible qui est mémorisé dans le cache d'instruction et exécuté par le processeur. La technique marche sur le papier et n'est qu'une amélioration des techniques de pré-décodage vues il y a quelques chapitres. Cependant, le code obtenu est une traduction assez basique, qui n'incorpore pas d'optimisations dignes de ce nom. De plus, elle gère mal le cas où la taille du code source et cible sont potentiellement très différentes. Concrètement, elle est surtout utile pour traduire du code d'un processeur RISC vers un autre processeur RISC, dont les tailles d'instructions sont similaires. Elle ne permet pas d'émuler plusieurs jeux d'instructions différents, le cout en matériel (un circuit de pré-décodage par jeu d'instruction) serait trop important. ===Le projet DAISY d'IBM=== De très rares processeurs étaient conçus pour accélérer cette traduction binaire, afin de garder de bonnes performances. Les premiers à avoir étudié l'idée étaient IBM, avec leur projet DAISY (''Dynamically Architected Instruction Set from Yorktown''). Le projet de base était de convertir à la volée du code compilé pour des CPU Power PC, vers du code VLIW. Les chercheurs d'IBM avaient développé un algorithme de traduction dynamique efficace, ainsi qu'un processeur VLIW disposant d'optimisations spécifiques à la traduction binaire dynamique, à la volée. De nombreuses idées de ce projet ont été reprises ou re-découvertes par la société Transmetta, avec ses processeurs Crusoe et Efficieon, puis par NVIDIA avec son projet Denver. Les ingénieurs de ce projet ont étudié la possibilité d'émuler plusieurs jeux d'instructions différents sur un même processeur, notamment le s390 d'IBM et le x86. En théorie, cela demande juste d'avoir plusieurs programmes de traduction binaire : un pour le x86, un autre pour le s390, éventuellement un autre pour le Power PC. Mais cela ne s'est pas concrétisé. Mais les ingénieurs ont étudié quelle pourrait être l'architecture VLIW idéale pour ça. Par exemple, il fallait des additions 3-opérandes pour simplifier les calculs d'adresse. Une difficulté était la gestion du registre d'état, dont les bits ne sont pas mis à jour de la même manière sur le x86 et le s390 ou le Power PC. La différence entre gros-boutisme et petit-boutisme était aussi un problème, de même que des différences d'adressage pour les périphériques. Pour que la traduction binaire soit efficace, le processeur VLIW intègre diverses optimisations, comme les exceptions différées et les branchements multi-voies. La principale est la suivante : seul le code des boucles ou fonctions exécutées fréquemment est traduit en code VLIW, le reste du code est interprété. En effet, traduire du code binaire prend plus de temps qu'une simple interprétation. Pour du code qui ne sera exécutée qu'une seule fois, il est plus rapide d'utiliser l'interprétation. Par contre, pour du code exécuté beaucoup de fois, le cout de la traduction binaire est amorti, dilué sur N exécutions, compensé par le gain en temps d'exécution de ce code traduit. Au final, cela permet de ne traduire que le code qui le mérite. La détection du code fréquemment exécuté est réalisée dans le cœur VLIW. L'unité de branchement mémorise les derniers branchements rencontrés et le nombre de fois qu'ils ont été exécutés. Si ils ont été exécutés un certain nombre de fois, l'unité de branchement lève une exception matérielle, qui invoque le traducteur binaire. La routine de cette exception est le traducteur binaire proprement dit. En clair, la traduction binaire est démarrée quand le processeur détecte qu'une fonction a été exécutée plus de N fois, via une exception matérielle dédiée. La mesure du nombre d'exécution d'un branchement se fait dans l'unité de calcul dédiée aux branchements, avec l'aide d'une sorte de ''branch adress buffer'' modifié. Pour rappel, le ''branch adress buffer'' mémorise le ''Program counter'' de chaque branchement récemment rencontré. Ici, chaque entrée du ''branch adress buffer'' est associée à un compteur incrémenté à chaque exécution du branchement. ===Les processeurs Crusoe et Efficieon de Transmetta=== Les processeurs Crusoe Et Efficeron sont deux processeurs VLIW produits par la société Transmetta, une société californienne rachetée par NVIDIA. Ils étaient conçus pour exécuter spécifiquement des programmes x86, système d'exploitation inclus. L'idée derrière ce projet était d'exécuter du code x86 sans que les applications, ni même le système d'exploitation et le BIOS soient au courant ! Les processeurs Transmetta étaient en réalité conçus pour exécuter un programme unique, le ''Code Morphing Software'' (CMS), qui traduisait le code x86 en code VLIW. Transmetta ne voulait pas que son processeur VLIW soit exploité directement, sans le CMS. Il n'avait pas rendu public de compilateur pour traduire du C vers du code VLIW, il n'avait pas donné la documentation du jeu d'instruction VLIW. Des efforts de rétro-ingénieurie, documentés sur le site realworldtech, ont cependant permis de comprendre comment étaient encodées les instructions du processeur Crusoe. Comme pour le projet Daisy, le ''Code Morphing Software'' utilisait à la fois interprétation et traduction binaire, selon les besoins. Les instructions sont interprétées lors de leurs premières exécutions, mais le CMS bascule sur de la traduction en code VLIW après un certain nombre d'exécution. Ainsi, les boucles souvent exécutées sont traduites en code VLIW, alors que le reste du code est interprété. Le choix du code à traduire est le fait du CMS, il utilise des heuristiques complexes pour, qui ne sont pas connues dans le détail. Le ''Code Morphing Software'' est mémorisé dans une EEPROM, ce qui en fait un ''firmware'' situé en dessous du BIOS. Le CMS démarre ensuite le BIOS, qui lui-même démarre le système d'exploitation, qui lui-même démarre les pilotes de périphériques et les programmes. Le CMS se réserve les 16 premiers mébioctets de l'espace d'adressage. L'EEPROM du CMS est mappée dedans, mais n'en utilise que la moitié. Le reste est utilisé comme cache, pour mémoriser le code VLIW traduit par le CMS. Les deux processeurs intégraient aussi deux mémoires SRAM, utilisées par le CMS, appelées la ''local program memory'' (LPM) et la ''local data memory'' (LDM). La première contient du code qui gére les interruptions, la mémoire virtuelle, les exceptions matérielles, les problèmes d'alignement mémoire, et quelques fonctions très fréquemment utilisées par le CMS. La seconde est de la mémoire RAM utilisée par ces fonctions de la ''local program memory''. Les mémoires caches L1 et L2 sont séparées de ces deux mémoires. Le processeur Crusoe et Efficieon étaient des processeurs VLIW très simples. Ils n'avaient même pas de MMU, ni de fonctionnalités importantes sur les CPU x86. Pour Crusoe, les instructions VLIW étaient encodées sur 64 ou 128 bits, et regroupaient entre 2 à 4 opérations. Les instructions LOAD/STORE ne supportaient pas d'adressage indicé, juste de l'adressage indirect à registre. Crusoe avait 5 unités de calcul : deux ALU entières, une unité LOAD/STORE, une unité de branchement, une FPU. Efficieon doublait le nombre d'ALU entière et d'unité LOAD/STORE, les instructions VLIW passaient à 256 bits. Le processeur Crusoe contient 160 registres, dont 64 registres généraux, 32 registres flottants. Les registres généraux font 32 bits, ce qui est cohérent avec le fait que les CPU x86 de l'époque étaient des processeurs 32 bits. Quant aux registres flottants, ils faisaient 80 bits, ce qui colle avec la taille des registres flottants de la FPU x87 utilisée à l'époque. Sur les 64 registres généraux, seuls 48 étaient réellement utilisable pour mémoriser des opérandes. Une partie des registres généraux étaient utilisés pour la gestion de la pile, d'autres pour mémoriser l'état du CPU x86 émulé, un registre était un registre zéro non accesible en écriture. La gestion des exceptions est optimisée avec un système d'exceptions différées, le même que celui décrit plus haut. Le processeur utilise un système similaire, pour effectuer des lectures anticipées, qui a été expliqué dans le chapitre sur les processeurs VLIW/EPIC. Sauf qu'il s'agit là d'une optimisation pour gagner en performances en général, pas une aide utile pour gérer des exceptions précises ou toute autre subtilité de la traduction binaire. Pour gérer les exceptions différées et les lectures anticipées, le processeur mémorise l'état du processeur avant de démarrer un bloc de code. Pour cela, le processeur contient des ''shadow registers'', 48 registres entiers et 16 registres flottants. Vous remarquerez qu'il y a autant de registres entiers que de ''shadow registers'' entiers. Les registres entiers/flottants sont mémorisés dans les ''shadow registers'' avant d'exécuter un bloc de code, afin que le processeur puisse revenir à l'état de base. Une autre fonctionnalité liée est le ''gated store buffer''. Le principe est d'éviter toute écriture en mémoire RAM, tant que le code VLIW n'a pas émis d'instruction ''commit''. L'idée est que les écritures sont conservées dans la ''store queue'' de l'unité LOAD/STORE. Pour rappel, la ''store queue'' met en attente les écritures soit tant que les écritures ne sont pas terminées, soit tant que la RAM est occupée. L'idée est que les écritures sont accumulées dans la ''store queue'' tant que l'instruction ''commit'' n'a pas eu lieu. Le ''store queue'' est "déversé dans la RAM" seulement quand l'instruction ''commit'' s'exécute. ===Les CPU NVIDIA Denver=== Nvidia a tenté quelque chose de similaire avec son projet Denver. NVIDIA a racheté l'entreprise Transmetta, ses brevets, et a tenté de refaire la même chose. Le projet initial était de traduire du code x86 vers un jeu d'instruction VLIW propriétaire, comme l'a fait Transmetta. Mais NVIDIA n'a pas réussi à acquérir la licence du jeu d'instruction x86 et s'est rabattu sur le jeu d'instruction ARM. Le processeur né de ce projet est le Tegra K1-64 CPU. Il s'agit d'un processeur composé d'un cœur VLIW, couplé à deux décodeurs d'instructions ARM. Les deux décodeurs ARM traduisent une paire d'instructions ARM consécutives en instructions VLIW, pour les exécuter sur le cœur VLIW. Le CPU peut fonctionner selon deux modes : ARM et VLIW. En somme, le processeur supporte deux jeux d'instruction : le jeu d'instruction ARM, et un jeu d'instruction VLIW. En mode ARM, les instructions ARM sont chargées depuis le cache, traduites en VLIW par les décodeurs ARM, puis exécutées par le cœur VLIW. En mode optimisé, le CPU exécute des instructions VLIW chargées depuis le cache d'instruction, les décodeurs ne sont pas utilisés. : Le CPU supporte en réalité deux jeux d'instruction ARM : ARM8 et 7. Mais ils correspondent au même mode pour le CPU. Un point important est que le processeur peut changer très rapidement de mode, en à peine quelques cycles d'horloges. On n'est clairement pas dans le cas des CPU x86, qui mettent des plombes pour passer du mode 32 à 64 bits et inversement. La commutation est tellement rapide qu'on peut considérer que le processeur supporte deux jeux d'instruction simultanément. Et le support simultané de deux jeux d'instruction facilite l'implémentation de la traduction binaire. Encore une fois, inutile de traduire du code qui ne sera exécutée qu'une seule fois, mieux vaut l'exécuter directement dans le mode ARM. Surtout que le processeur supporte un mode ARM qui permet d'exécuter du code ARM sans perte de performance. Par contre, le code fréquemment utilisé, notamment dans des boucles critiques, est traduit en VLIW. Au final, les performances sont optimisées, en limitant le travail du logiciel traducteur binaire. La détection du code fréquemment exécuté se fait différemment que pour les processeurs Transmetta et Daisy. L'unité de branchement ne détecte pas directement le code beaucoup exécuté, même si elle a un rôle à jouer. A la place, la détection est réalisée en logiciel, par un ''thread'' dédié, qui s'exécute sur un cœur séparé. Il détecte le code fréquemment utilisé en regardent les branchements exécutés récemment. Pour cela, le processeur mémorise l'historique des branchements pris récemment et copie cette historique dans une mémoire tampon dédiée, partagée entre tous les cœurs. Vu qu'elle est partagée, le ''thread'' de détection a accès à l'historique sans pertes de performances, seule la copie de l'historique dans ce tampon a un cout en performance, pas son partage. Le code VLIW obtenu après traduction binaire est mémorisé dans la mémoire RAM, dans une portion spécialement réservée pour. Elle fait 128 mébioctets, et est appelée le '''cache d'optimisation''' par NVIDIA. Le cache d'optimisation est protégé en écriture et seul le traducteur binaire peut écrire dedans. Au passage, le traducteur binaire est du code VLIW, ce qui fait qu'il est placé dans le cache d'optimisation. Le processeur détecte automatiquement quand une fonction pour laquelle le code compilé équivalent est disponible. Pour cela, l'unité de chargement contient une table de correspondance entre l'adresse de la fonction ARM, et l'adresse de son équivalent compilé. Quand une fonction est appelée, l'unité de chargement regarde l'adresse de destination du branchement. Si l'adresse est dans cette table, elle récupère l'adresse de la fonction compilée et branche vers celle-ci. Le processeur dispose d'un cache d'instruction de 128 kibioctets, ce qui est très important, mais nécessaire vu la taille des instructions VLIW. Le coeur VLIW dispose de ses propres décodeurs, d'un ''scoreboard'', de 7 unités de calcul et de registres. Pour les unités de calcul, il a deux FPU, deux ALU, une unité de branchement, et deux unités LOAD-STORE faisant aussi ALU. Un point important est que le CPU dispose de 64 registres entiers et de 64 registres flottants. C'est deux fois plus que supporte le jeu d'instruction ARM. En mode ARM, seule la moitié des registres est utilisée. Mais en mode optimisé, le traducteur binaire utilise bien les 64 registres, grâce à une sorte de renommage de registres logicielle. Pour simplifier la traduction binaire, le processeur supporte les techniques vues précédemment. Le processeur intègre un ''gated store buffer'' similaire à celui des processeurs Transmetta. Les techniques d'exceptions différées sont aussi supportées, comme sur les processeurs Transmetta. Il a aussi une unité de préchargement décrite par NVIDIA comme agressive, avec support du préchargement de type anticipé (''runahead''). ===L'émulation de la mémoire virtuelle=== Pour finir, il faut parler de la mémoire virtuelle avec la traduction binaire. Sur le processeur source, le processeur gère à la fois des adresses physiques et virtuelles et sa MMU fait la conversion entre les deux. Sur le processeur VLIW, la MMU est simulée par le programme de traduction binaire, partiellement ou totalement. L'implémentation exacte varie suivant que l'on parle du projet Daisy, des processeurs Transmetta ou de Denver. Sur les processeurs Transmetta, il n'y a pas de MMU ni de mémoire virtuelle. A la place, le programme de traduction binaire émule la mémoire virtuelle du processeur source. Le processeur VLIW ne gère que des adresses physiques, rien d'autre. L'espace d'adressage physique du processeur VLIW a la même taille que celui du processeur émulé, ici des processeurs x86. Il est vraisemblable que les adresses physiques utilisées par le processeur x86 sont les mêmes que celles du processeur VLIW. Sur le projet Daisy, le processeur VLIW gère la mémoire virtuelle via pagination, comme les processeurs émulés. Ce qui fait qu'il gère un espace d'adressage virtuel et un espace d'adressage physique. Pour éviter toute confusion, nous parlerons d'adresse physique/virtuelle VLIW pour les adresses physiques/virtuelles du processeur VLIW, d'adresse virtuelle/physique source pour celle du jeu d'instruction traduit, à savoir du Power PC ou du s390, éventuellement de l'x86. Il faut alors faire le lien entre adresses physiques source et adresses virtuelles VLIW. Pour cela, rien de plus simple : il y a correspondance parfaite. L'adresse physique source numéro N correspond à l'adresse virtuelle VLIW numéro N. Cependant, cela signifie que tout l'espace d'adressage virtuel VLIW serait utilisé par le code à traduire. En réalité, il faut ajouter de la place pour le code traduit et le programme de traduction binaire. La conséquence est que l'espace d'adressage virtuel VLIW est plus large que l'espace d'adressage source. L'espace d'adressage virtuel VLIW est découpé en trois sections : une section pour le code Power PC à traduire, une autre pour le code traduit en VLIW, et une dernière réservée au traducteur binaire. Le programme de traduction est placé dans une ROM mappée en mémoire dans la seconde section. Le reste de la seconde section est réservé à une mémoire RAM utilisée par le programme de traduction. [[File:Mémoire virtuelle sur le projet Daisy.png|centre|vignette|upright=1.5|Mémoire virtuelle sur le projet Daisy]] Il faut noter que chaque page physique source, une fois traduite, correspond à N pages virtuelles VLIW. La raison est que le code traduit prend plus de place que le code machine originel. En conséquence, on doit utiliser une page finale plus grande. Pour le reste, des pages contiguës en mémoire physique source sont elles aussi contiguës en mémoire virtuelle VLIW. Le calcul d'adresse est donc simple : l'adresse physique source est multipliée par N, puis on ajoute l'adresse de base à laquelle commence la section pour le code traduit. ==La traduction binaire sur le Burroughs B1700== Nous venons de voir comment accélérer en matériel la traduction binaire, avec des techniques bien spécifiques. Le Burroughs B1700 a procédé autrement et il est intéressant d'étudier en détail son architecture. Son architecture est décrite dans le livre "Interpreting Machines : Architecture and Programming of the Bl700/Bl800 Series", écrit par Elliott I. Organick et James A. Hinds. Une partie de ce qui vfa suivre est un très court résumé de ce livre. ===Le Burroughs B1700 : le mal-nommé ''split-level control store''=== Le Burroughs B1700 avait plusieurs modèles : le B1726, le B1710 et le B1800. Ils se distinguaient par la manière dont le code machine source était traduit par le traducteur binaire. Dans ce qui suit, nous parlerons de traducteur binaire pour parler soit d'un interpréteur pour un langage de programmation de haut niveau, soit pour un logiciel de traduction binaire. Le B1710 plaquait le traducteur binaire en mémoire RAM. Le B1800 faisait pareil, mais avait ajouté un cache dédié au code du traducteur binaire, afin d'améliorer les performances. Le B1726 avait un traducteur binaire scindé en deux : une partie était en mémoire RAM, l'autre était dans un ''local store'' dédié. Le processeur peut exécuter le traducteur binaire soit depuis le ''local store'', soit depuis la mémoire RAM avec une pénalité en termes de performances. La documentation appelle cela un '''''split-level control store''''', mais le terme est trompeur : la documentation qualifie de microcode le langage machine du Burroughs B1700, ce qui est à l'origine de beaucoup de confusions. Une autre source de confusion est que ce langage machine a quelques propriétés qu'on pourrait attendre d'un microcode, mais certaines sont manquantes. Par exemple, il peut adresser les registres d’interfaçage mémoire, mais c'est parce que ce sont des registres architecturaux sur ce processeur. Ses instructions n'encodent pas de signaux de commande, par contre, elles font 16 bits et adressent les opérandes de manière implicite. Le ''local store'' mémorisait au maximum 2048 instructions machine, ce qui fait que les 2048 premières "adresses" correspondaient au ''local store''. Les suivantes étaient des adresses de mémoire RAM qui contenaient le reste du traducteur binaire. En pratique, l'adresse qui séparait les deux était configurable ! On pouvait limiter la taille du ''local store'' à un multiple de 32 instructions ! Les adresses restantes étaient alors réattribuées à la RAM. Par exemple, on pouvait attribuer 64 adresses au ''local store'', les 2048 - 64 adresses restantes étaient alors attribuées à la mémoire RAM. Un registre limite, nommé TDPM, indiquait quelle était l'adresse de démarcation entre les deux. Il contenait 4 bits, dont la valeur était multipliée par 32, pour obtenir l'adresse limite. Le programmeur devait découper le traducteur binaire en segments, certains étant placé dans le ''local store'', les autres étant en mémoire RAM. Mieux que ça, le processeur autorisait d'utiliser la technique de l'''overlaying'' pour le microcode ! Il y avait même une fonction OVERLAY dédiée pour ! OVERLAY avait besoin de trois opérandes : la taille du segment à copier, son adresse source en mémoire RAM, son adresse de destination dans la micro-SRAM. L'interpréteur réservait un "segment" en mémoire RAM, pour les données, dont le processeur connaissait la position. En-dehors de ce segment, il n'y a que des instructions machines à traduire, et le traducteur binaire. Le CPU incorporait pour cela deux registres, un pour l'adresse de base du segment de données, un autre pour son adresse de fin. Il incorporait une protection mémoire limitée sur ce segment, à savoir que seul l'interpréteur pouvait lire ou écrire dans ce segment. ===Une architecture bit-adressable=== Le CPU Burroughs B1700 était '''bit-adressable'''. En clair, il pouvait adresser la mémoire bit par bit, si nécessaire. Un avantage est que cela facilitait la traduction binaire des instructions machine, mais nous détaillerons cela plus bas. Un autre avantage est que cela permettait d'émuler des architectures dont la taille des registres/''byte''/mots était très différente. Et cet avantage mérite quelques explications immédiates. Le processeur était un CPU 24 bits, mais il pouvait émuler des processeurs 16 bits, 8 bits, 4 bits, 1 bit, 9 bits, ou toute autre valeur. Pour cela, le processeur lisait des mots de 24 bits et utilisant un ''barrel shifter'' pour sélectionner les bits adéquats. Par exemple, pour un accès de 16 bits, la donnée lue en mémoire était masquée de manière à ne garder que les 16 bits de poids faible. Le ''barrel shifter''/circuit de masquage était placé directement avant le bus mémoire, sur le trajet des données/instructions. Le ''barrel shifter'' était commandé par un registre ''Field Unit'', qui précisait quelle était la taille des données à charger. Pour communiquer avec la mémoire, il avait trois registres séparés : READ, WRITE, MAR. READ contient la donnée lue lors d'une lecture, WRITE est pour une donnée à écrire lors d’une écriture, MAR contient l'adresse à lire ou en cours de lecture. Il s'agit de registres d’interfaçage mémoire. De même, pour communiquer avec les entrées-sorties, il disposait de deux registres CMND et DATA : CMND pour envoyer une commande à une entrée-sortie, DATA pour lire ou écrire une donnée. Le fait que ces registres d’interfaçage mémoire et IO soit adressable est peu commun, et c'est plus quelque chose qu'on attend d'un microcode que d'instructions machines. Mais il faut bien comprendre que les registres d’interfaçage mémoire ont été promus au rang de registres architecturaux, sur ce processeur. ===L'ALU de taille variable=== Les Burroughs B1700 intégrait une pile d'adresse de retour, capable de mémoriser 16 adresses de 24 bits. Il disposait de 4 registres généraux, nommés X, Y, T et L, tous de 24 bits. Un point important est que le registre T avait des possibilités d’extraction spécifiques. Il était possible d'extrait N bits, placés n'importe où dans ce registre, et de les copier dans un autre registre. Nous verrons pourquoi cette possibilité est intéressante et ne sert que pour le registre T. Ils avaient une '''ALU de taille variable'''. En clair, l'ALU pouvait faire des calculs sur un nombre de bits compris entre 0 et 24. Pour cela, les calculs étaient fait avec une ALU de 24 bits, puis les bits de poids fort inutiles étaient masqués. L'ALU de 24 bits pouvait masquer les bits de poids fort du résultat, mais avec des limitations. Elle gérait des opérandes et résultats de 0, 4, 8, 12, 16, 20 et 24 bits. Pour avoir un réglage plus fin, une ALU 4 bits séparée permettait de corriger le résultat. Les registres X et Y étaient reliés à une ALU de 24 bits capable d'effectuer des opérations arithmétiques basiques. Fait étonnant, l'ALU faisait tous les calculs en même temps et fournissait ses résultats dans 7 registres : un registre SUM pour l'addition, un registre DIFFERENCE pour la soustraction, et un registre par opération bit à bit. Les registres pour les opérations bit à bit sont : CMPX et CMPY pour l'opération NOT sur chaque opérande, XANY pour le ET bit à bit, XEOY pour le XOR, XORY pour le OU logique. Les additions et soustractions pouvaient se faire sur des opérandes codés en binaire ou en BCD, suivant le code opération utilisé. A cela, il fallait ajouter deux registres de "résultat" MSKX et MSKY. Ils fournissaient un masque dépendant de la taille du résultat en bits. C'est ce registre qui permettait de gérer des données de 0, 4, 8, 12, 16, 20, et 24 bits, alors que l'ALU faisait 24 bits. Le masque dans ce registre pouvait servir d'opérande dans une opération de masquage ultérieure, pour corriger le résultat. L'ALU prenait en opérande les registres X et Y, mais aussi un registre CYF pour la retenue entrante. En sortie, il fournissait aussi deux retenue sortantes : une pour l'addition et une autre pour la soustraction. Elles étaient mémorisées dans les registres CYL et CYD, respectivement. L'ALU fournissait aussi 12 conditions, qui alimentait un registre d'état. Le registre d'état était segmenté en trois registres appelés XYST, XYCN et BICN. Les registres X et Y sont des registres pour les opérandes, mais il y a aussi deux autres registres de 24 bits. Il s'agit des registres T et L, qui sont des registres généraux. Les registres T et L étaient découpés en 6 sous-registres de 4 bits, nommés LA LB LC LD LE et LF pour le registre L, et TA TB TC TD TE et TF pour le registre T. Ils étaient adressables comme n'importe quel registre. Le processeur contenait de nombreux autres registres de 4 bits, dont les sous-registres de T et L. En tout, il y a 27 registres de 4 bits adressables ! Et le processeur disposait d'une ALU 4 bits pour manipuler ces registres de 4 bits ! Elle est capable de faire des MOV entre registres de 4 bits, des opérations bit à bit, et peut aussi tester si un bit vaut 0 ou 1. La dernière possibilité est très utile pour implémenter les branchements. De plus, cela permettait de faire des opérations de masquage, notamment pour appliquer un masquage supplémentaire à celui de l'ALU. Par exemple, pour gérer des opérations sur 14 bits, on effectue une opération sur 16 bits avec l'ALU, et on masque les 2 bits manquants avec l'ALU 4 bits. La capacité d'extraction du registre T pouvait aussi être utilisé pour ça. ===Le code machine du Burroughs B1700=== Le processeur utilise des micro-opérations de 16 bits de long, qui sont copiées dans registre d'instruction M de 16 bits. Fait intéressant, il était possible de faire un OU logique entre l'instruction lue et un registre du processeur, le résultat étant mémorisé dans le registre M. Cela permettait de faire du code automodifiant, pour émuler des modes d'adressage complexe. Cela permettait par exemple de remplacer une adresse dans une micro-opération. L'adresse est alors surimposée avec un OU dans le champ d'adresse, alors que le reste de l'instruction n'est pas modifié. Afin de gérer les interruptions, ces dernières étaient partiellement détectées en logiciel. Les interruptions ne s'exécutaient pas automatiquement, pour émuler correctement les interruptions. Une interruption doit être prise en compte à la toute fin de l'exécution d'une instruction, quand celle-ci a finit son travail. Mais vu qu'ici, les instructions sont émulées avec des séries de micro-opérations, il y a un problème : il est possible d'interrompre une instruction émulée en plein milieu. Pour éviter cela, le processeur ne déclenche pas les interruptions matérielles immédiatement, elles sont mises en attente. Une micro-opération dédiée vérifie si une interruption est en attente, et l'exécute alors cas échéant. La micro-opération est utilisée comme micro-opération finale d'une instruction émulée. Le ''program counter'' est appelé le registre A. Les instructions du processeur font 16 bits de long, et elles sont alignées en RAM sur 16 bits, ce qui fait que les 4 bits de poids fort de ce registre valent 0, seuls les 14 bits de poids fort sont utiles. Les adresses mentionnées plus haut, qui adressent les micro-opérations en mémoire RAM, sont des adresses de mots de 16 bits, pas les adresses de bit individuels. ===La traduction binaire est aidée par des registres et instructions dédiées=== Les instructions à décoder n'ont pas de longueur fixe, à savoir qu'elles peuvent faire 16 bit avec tel jeu d'instruction, 8 bits avec un autre, 32 bits sur un autre, etc. Elles peuvent aussi être de longueur fixe ou variable ! Le B1700 intégrait donc un support des instructions de longueur variable, qui était "détourné" pour gérer des jeux d'instruction différents. Notons que vu que l'architecture est bit-adressable, les contraintes d'alignement des instructions disparaissent, ce qui a des avantages qu'on va détailler dans ce qui suit ! L'implémentation des instructions de taille variable ajoutait un registre avant le registre d'instruction. Le registre d'instruction proprement dit était un registre M de 24 bits. Il était précédé par un registre T, dans lequel l'instruction était assemblée, en combinant plusieurs morceaux. Si l'instruction est codée est codée sur moins de 24 bits, le registre T se contente de masquer les bits en trop. Si l'instruction chargée dépasser 24 bits, le chargement se faisait en plusieurs passes, avec plusieurs lectures en mémoire. Dans ce qui suit, on suppose que l'instruction est composée de '''champs''' : l'opcode, les opérandes, les adresses absolues, les constantes immédiates, les numéros de registre, etc. L'idée est que l'instruction est chargée dans le registre T, puis découpée en champs, qui sont copiés dans les registres du processeur ou envoyés au microcode. Je rappelle que le registre T est relié à un ''barrel shifter'', différent au précédent, qui permettait de masquer, décaler et extraire une donnée de ce registre. Et il y a la possibilité d'extraire une suite de bit de ce registre T, pour la copier dans un autre registre. Par exemple, prenons une instruction composée d'un opcode, d'une constante immédiate et d'une adresse immédiate. L'opcode est lu et interprété par le microcode. Le microcode extrait la constante immédiate du registre T et la copie dans le registre X. Ensuite, l'adresse est extraite et envoyée au registre FA, une lecture est démarrée, et l'opérande est copiée dans le registre Y. Enfin, l'instruction est exécutée par le microcode. Le tout prend plusieurs micro-opérations : une première pour lire l'opcode, une seconde pour lire la première opérande, une troisième pour lire la seconde opérande, una quatrième pour tout exécuter. Notons que suivant la taille des champs, on peut en charger plusieurs à la fois. Par exemple, imaginons qu'on ait chargé 24 bits, contenant un opcode de 8 bits et une constante immédiate de 16 bits : pas besoin de faire deux accès mémoire, le registre T est juste découpé en un opcode et une constante. Le registre T est fait pour ce genre d'extraction. ===Le chargement d'un champ=== L'architecture est bit-adressable car elle gére des champs qui ne font pas forcément 8 ou 16 bits. Par exemple, certains jeux d'instructions ont des opcodes codés sur 6 ou 7 bits, voire 9-10 bits. Par exemple, prenons un opcode de 7 bits, suivi par une adresse de 12 bits et une constante immédiate de 15 bits : aucun de ces champs ne sera aligné sur 8/16/24 bits. Utiliser une architecture bit-adressable règle le problème d'alignement. Et en général, les contraintes d'alignement des instructions disparaissent ! Quand on charge une instruction, peut importe qu'elle soit ou non alignée sur 24 bits, elle sera placée dans les bits de poids faible, il suffira de masquer les bits de poids fort inutiles. Par exemple, si on charge une instruction de 16 bits, les 16 bits seront copiés dans les 16 bits de poids faible du registre T, il suffira de masquer l'octet de poids fort. Pas besoin de faire de décalages pour gérer des instructions mal alignées... Pour charger une instruction, il faut connaitre plusieurs informations : l'adresse de son premier bit, sa taille. Pour cela, le B1700 contient deux registres, nommés ''Field Adress'' (FA) et ''Field Length'' (FL), qui indiquent l'adresse du premier bit et la taille de l'instruction à charger. Le premier fait 24 bits, ce qui permet de gérer des adresses de 24 bits. N'oubliez pas que le processeur est bit-adressable, ce qui permet d'adresser 16 Mébi-bits, soit 2 mébioctets. Le registre pour la taille fait lui 16 bits, ce qui permet de gérer des instructions de 65536 bits, soit 8192 octets ! Leur nom commence par ''field'', car le processeur peut gérer des instructions de taille variable et/ou plus longues que 24 bits. Dans ce cas, le chargement de l'instruction se fait champ par champ. Pour donner un exemple, prenons une instruction composée de 20 champs de 12 bits chacun. Les champs sont chargés un par un dans le registre T. Le registre FA est alors initialisé avec l'adresse de l'instruction, il est incrémenté de 12 à chaque cycle. Le registre FL est initialisé avec la taille de l'instruction, soit 20 * 12 = 240 bits, et il est décrémenté de 16 à chaque cycle. Précisons cependant que la taille d'un champ peut être changée d'un cycle à l'autre. Par exemple, le processeur peut charger un opcode de 8 bits, l'interpréter, détecter que l'instruction demande ensuite de charger une adresse de 16 bits. Dans ce cas, le registre FL est immédiatement altéré pour charger 16 bits, au lieu des 8 bits de l'opcode immédiatement précédent. ==Les jeux d'instructions dédiés à un langage de programmation== De rares processeurs sont conçus pour un langage de programmation en particulier. On appelle ces processeurs, conçus pour des besoins particuliers, des '''processeurs dédiés'''. Par exemple, les fameux ''Burrough E-mode'' B5000/B6000/B7000 étaient spécialement conçus pour exécuter de l'ALGOL-60. Leurs cousins B2000/B3000/B4000 étaient eux conçus pour le COBOL. Des langages fonctionnels ont aussi eu droit à leurs processeurs dédiés. Le prolog en est un bel exemple, avec les superordinateurs de 5ème génération qui lui étaient dédié. On peut aussi citer les machines LISP, dédiés au langage LISP, qui datent des années 1970. Elles étaient capables d’exécuter certaines fonctions de base du langage directement dans leurs circuits : elles possédaient notamment un ''garbage collector'' câblé dans ses circuits ainsi que des instructions machines supportant un typage déterminé à l’exécution. Les processeurs dédiés ont eu leur heure de gloire au début de l'informatique, à une époque où les langages de haut niveau venaient d'être inventés. À cette époque, les compilateurs n'étaient pas performants et ne savaient pas bien optimiser le code machine. Il était alors rationnel, pour l'époque, de rapprocher le code machine cible et le langage de programmation de haut niveau. De nombreuses architectures dédiés ont ainsi été inventées, avant que les concepteurs se rendent compte des défauts de cette approche. Les défauts en question ne sont pas nombreux, mais assez simples à comprendre. Premièrement, elles sont très rapides pour un langage de programmation en particulier, mais sont assez mauvaises pour les autres, d'où un problème de "compatibilité". Ajoutons à cela que les langages de programmation peuvent évoluer, devenir de moins en moins populaires/utilisés, ce qui rend la création d'architectures généralistes plus pertinente. Enfin, les architectures dédiées sont évidemment des processeurs CISC, pour implémenter les nombreuses fonctionnalités des langages évolués. Et les défauts des CISC sont assez rédhibitoires à l'heure actuelle. À l'heure actuelle, les algorithmes des compilateurs se sont améliorés et savent nettement mieux utiliser le matériel. Ils produisent du code machine efficace, ce qui rend les architecture dédiées bien moins intéressantes. Si on ajoute les défauts de ces architectures dédiées, par étonnant que les architectures dédiées aient presque disparues. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'accélération matérielle de la virtualisation | prevText=L'accélération matérielle de la virtualisation | next=Le matériel réseau | nextText=Le matériel réseau }} </noinclude> t34zfpek26bei8m9apasev5f7zrwszw 764049 764048 2026-04-19T20:30:05Z Mewtow 31375 /* Une architecture bit-adressable */ 764049 wikitext text/x-wiki De nos jours, la majorité des programmeurs programment dans des langages de haut niveau. Il est très rare pour eux d'avoir à utiliser de l'assembleur, et encore moins un langage machine. Les programmes écrits dans un langage de haut niveau sont traduits en langage machine par un logiciel appelé le compilateur. [[File:Compilation.PNG|centre|vignette|upright=2.5|Compilateur : principe.]] Il se trouve que quelques rares processeurs sont conçus pour faciliter le travail du compilateur. Et ce sont ces processeurs que nous allons voir dans ce qui suit. Ils représentent une catégorie de jeux d'instruction à part, qui n'a pas vraiment de nom. ==La compilation et l’interprétation== Un compilateur traduit donc du code source, écrit dans un langage de haut niveau, vers du langage machine. La traduction est rarement directe. En général, le compilateur traduit le code source en assembleur, qui est lui-même traduit en langage machine. L'assembleur est une représentation textuelle du langage machine alors que le code machine est du binaire exécutable par le processeur. La traduction se fait donc en deux étapes, la '''compilation''' proprement dite et l''''assemblage''', réalisées respectivement par un compilateur et un assembleur. Il faut parfois rajouter une troisième phase d'édition des liens, que nous passons volontairement sous silence. {| |[[File:Code C.png|vignette|200px|Le code source, ici en C.]] |[[File:Assem.png|vignette|250px|Le code assembleur.]] |[[File:Binary file - hello world (C programming).png|vignette|150px|Le code machine.]] |} ===L'interprétation et la compilation à la volée=== Une alternative à la compilation est l''''interprétation''', qui transforme le code source en code machine à la volée. Avec l'interprétation, le code source est passé à un logiciel appelé l''''interpréteur''', qui exécute le code source ligne de code par ligne de code. Une ligne de code est traduite en code machine, qui est exécutée, puis l'interpréteur passe à la ligne suivante. [[File:Kaantotulkkaus.png|centre|vignette|upright=3|Compilation versus interprétation.]] Un défaut de l'interprétation est son cout en performance, qui est assez important. Aussi, l'interprétation stricte a évolué vers un hybride entre interprétation et compilation. L'idée est qu'une partie peu importante du code source est interprétée, alors que le code important est compilé juste avant d'être exécuté. Pour le dire autrement, le code source est partiellement compilé à la volée, juste avant son exécution. Aussi, on parle de '''compilation à la volée'''. Le terme en anglais est ''Just In Time compilation'', abrévié en JIT. En soi, le JIT est associé à de l'interprétation, les JIT sont en réalité des hybrides interpréteurs-compilateurs. Un point important est que compiler du code à la volée est assez lourd, cela a un cout en performance. Par contre, le code compilé s'exécute plus vite que du code interprété. Si on exécute le code compilé une seule fois, le cout de la compilation l'emporte sur le gain à l'exécution. Mais le code compilé est réutilisé autant de fois que nécessaire. Le gain à l'exécution est donc multiplié par le nombre d'exécutions, alors que le cout de la compilation est répartit sur plusieurs exécutions, il est amorti. Les portions du code sont choisies de manière à avoir un gain en performance maximal. L'idée générale est que le code exécuté très souvent est compilé, alors que du code exécuté pas souvent est interprété. Typiquement, les boucles les plus souvent exécutées sont compilées, le code exécuté une seule fois est interprété. ===Les langages intermédiaires=== Les compilateurs modernes passent par un '''langage intermédiaire''' pour faire la transformation en code machine. Le compilateur traduit le langage de haut niveau en langage intermédiaire, puis traduit le langage intermédiaire en code machine cible. Le langage intermédiaire est parfois appelé le '''''bytecode''''', et ce terme recouvre aussi le code écrit avec du ''bytecode''. L'interprétation aussi peut passer par l'intermédiaire d'un un ''bytecode'', ce n'est pas limité aux compilateurs proprement dit. {| |[[File:Intermediate representation scheme.png|centre|vignette|upright=1.5|Représentation intermédiaire d'un compilateur.]] |[[File:Code interpreter scheme.png|centre|vignette|upright=1.5|Fonctionnement d'un interpréteur.]] |} Faire ainsi a de nombreux avantages, le principal étant d'avoir un compilateur capable de traduire un langage de haut niveau vers plusieurs jeux d'instructions différents. Cela permet d'avoir, par exemple, un compilateur qui traduit du C soit en code machine x86, soit en code machine pour un CPU ARM, soit pour un CPU POWERPC, etc. [[File:Compiler design.svg|centre|vignette|upright=3|Compiler design]] Pour l'interprétation, l'usage d'un ''bytecode'' a d'autres avantages. Pour l'exploiter, il faut distribuer non pas le code source, mais le ''bytecode''. En clair, l'interpréteur ne prend pas en entrée le code source, mais du ''bytecode'' déjà compilé. La traduction est alors beaucoup plus simple, car le code source a déjà été partiellement compilé, par la traduction en ''bytecode''. Interpréter du code source est en effet assez compliqué : il faut effectuer des étapes d'analyse lexicale, sémantique, et bien d'autres. Avec du ''bytecode'', ces étapes ont été réalisées lors de la compilation du ''bytecode'', l'interpréteur a alors peut de choses à faire. L'interprétation du ''bytecode'' se fait instruction par instruction, au niveau du ''bytecode''. Une instruction du ''bytecode'' est traduite en une instruction machine équivalente, qui est exécutée. Si l'instruction est un peu complexe, il exécute une fonction/procédure qui fait la même chose. Pour résumer, le ''bytecode'' est ensuite traduit à la volée et exécuté instruction par instruction par un logiciel appelé l'interpréteur. [[File:Control table.png|centre|vignette|upright=2|Control table]] Le langage intermédiaire peut être vu comme l'assembleur d'un processeur, n'existe pas forcément dans la réalité, mais dont le jeu d'instruction est décrit en détail. Le processeur en question est appelé une '''machine abstraite''', ou encore une ''machine virtuelle''. Nous utiliserons le terme de machine abstraite dans ce qui suit. La machine abstraite n'est pas la même suivant que l'on cible la compilation ou l'interprétation/JIT. Pour l'interprétation/JIT, la machine abstraite est souvent une machine à pile, car cela simplifie grandement la traduction en code machine final. L'interprétation demande que la traduction du ''bytecode'' en code machine soit la plus simple possible. Et cela demande de prendre en compte pas mal de détails. Par exemple, les différents jeux d'instruction existants n'ont pas le même nombre de registres, ce qui pose problème lors de la traduction du ''bytecode''. Lors de la transformation en code machine, un algorithme d'allocation de registres se débrouille pour traduire le code intermédiaire en code qui utilise un nombre limité de registres. Pour améliorer cette allocation de registres, il y a deux solutions niveau ''bytecode'' : soit utiliser un nombre illimité de registres, soit utiliser une machine à pile. Pour les machines à pile, il existe un algorithme simple et rapide pour traduire un code écrit pour une machine à pile en un code écrit pour un processeur avec des registres, qui se débrouille pas trop mal pour allouer efficacement les registres. C'est un avantage assez important pour les langages interprétés. ==Les processeurs qui exécutent du bytecode== Le ''bytecode'' est un code machine, ce qui signifie qu'il peut en théorie s'exécuter sur un processeur qui implémente le jeu d’instruction associé. Si le jeu d'instruction d'un ''bytecode'' est souvent une description censée être fictive, elle n'en reste pas moins un jeu d'instruction et des caractéristiques précises, qu'on peut l'implémenter en matériel ! Néanmoins, tous les ''bytecode'' ne sont pas égaux de ce point de vue. Certains sont faciles à implémenter en matériel, d'autres non. Les ''bytecodes'' implémentés en matériel sont ceux dont les machines abstraites sont des machines à pile. Un premier exemple est celui des processeurs Pascal MicroEngine, qui exécutaient directement le bytecode du langage Pascal, le fameux UCSD P-code. Un second exemple est la '''machine SECD''', qui sert de langage intermédiaire pour certains compilateurs de langages fonctionnels. Elle a été implémentée en matériel par plusieurs équipes, notamment par les chercheurs de l'université de Calgary en 1989. Dans le même genre, quelques processeurs simples étaient capables d’exécuter directement le ''bytecode'' du langage FORTH. Le FORTH, un des premiers langages à pile de haut niveau, possède de nombreuses implémentations hardware et est un des rares langages de haut niveau dont le ''bytecode'' a été utilisé comme langage machine sur certains processeurs. Par exemple, on peut citer le processeur FC16, capable d’exécuter nativement du ''bytecode'' FORTH. ===Les processeurs Java=== Le cas le plus impressionnant est celui de la machine virtuelle Java, qui est un design de processeur comme un autre. En temps normal, le ''bytecode'' Java est compilé ou interprété, mais certains processeurs exécutaient du ''bytecode'' Java directement, sans interprétation ni compilation. Ils sont appelés des '''processeurs Java'''. On peut citer les processeurs ARM disposant de l'extension Jazelle, du JEMCore et du aJ-100 de aJile Systems, le picoJava II de Sun, et quelques autres. Il y a aussi eu quelques projets de recherche ou processeurs open source, comme Komodo, jamuth, le ''Java Optimized Processor'' et quelques autres projets du même genre. L'implémentation de la machine virtuelle Java n'était cependant pas complète. Quelques instructions complexes n'étaient pas gérées par le processeur et devaient être émulées en logiciel. Mais la grosse majorité des instructions l'était. Les instructions non-supportées étaient émulées avec la méthode ''trap and emulate'', à savoir qu'un opcode inconnu déclenchait une exception matérielle ''opcode invalide'', dont la routine pouvait émuler les instructions qui doivent l'être. Le processeur aJ-100 de aJile est capable d'exécuter la quasi-totalité des instructions de la JVM Java, seules deux instructions faisant exception. Les '''accélérateurs Java''' sont une solution intermédiaire, qui permet de traduire à la volée du ''bytecode'' Java en instructions RISC, exécutées sur un autre processeur. Il s'agit formellement de traduction binaire, mais je préfère en parler ici. L'idée est de combiner un processeur RISC avec un coprocesseur Java. Le coprocesseur lit le ''bytecode'' Java et le traduit en instructions RISC, ces dernières étant exécutées sur le processeur RISC. Le JA108 de Nozomi était un coprocesseur de ce genre, au même titre que le coprocesseur JSTAR de JEDI. L'extension Jazelle de certains CPU ARM fonctionnait vraisemblablement sur un principe similaire, sauf que le coprocesseur était intégré dans le processeur ARM, entre le cache d'instruction et le décodeur d'instructions ARM. L'intérêt des processeurs Java n'est pas qu'une question de performance. Certes, ils permettent d’exécuter plus vite les programmes compilés en ''bytecode'', comme des programmes Java pour la JVM Java. Mais l'intérêt est aussi de faire des économies de mémoire RAM et ROM. Je rappelle que la machine virtuelle Java est une machine à pile, ce qui fait que sa densité de code est excellente. Les programmes en ''bytecode'' Java sont donc très petits, et prennent peu de place en mémoire ROM. De plus, utiliser un processeur Java permet de se passer d'interpréteur ou de JIT Java, ce qui économise encore plus de mémoire. ===Transformer un processeur RISC en machine à pile=== La plupart des processeurs Java utilisent en interne une architecture à registre généraux, qui émule une machine à registre en microcode. Quelques registres sont réservés pour l'état de la machine virtuelle, avec au minimum un registre réservé pour le pointeur de pile, un autre pour un pointeur vers un ''pool'' de constantes, et quelques autres. D'autres registres servent à mémoriser le haut de la pile, à savoir le sommet de la pile et quelques opérandes situées juste en-dessous. Par exemple, le processeur picoJava II gardait les 64 opérandes au sommet de la pile dans 64 registres généraux, séparés des registres pour les pointeurs et l'état de la JVM. Une optimisation possible, envisagée sur les processeurs picoJava, était une sorte de macro-fusion sous stéroïde. L'idée était de fusionner certaines séries d'instructions Java en une seule instruction machine. Typiquement, pour faire une opération arithmétique, une machine à pile empile deux opérandes et exécute une opération, ce qui prend trois instructions machine. La spécification picoJava 1 a proposé d'envoyer directement la seconde opérande en entrée de l'ALU, ce qui fusionnait l'empilement de la seconde opérande et l'opération arithmétique. Le processeur picoJava II va plus loin et fusionne les trois instructions en une seule instruction RISC, agissant sur des registres. Mais la majorité des processeurs Java ne faisaient pas cela, car cela complexifie grandement le décodeur. Les processeurs ''bigfoot'' de DCT utilisaient un système différent pour émuler une pile avec des registres. Leur banc de registre de 64 bits était coupé en deux : les 16 premiers registres étaient utilisés pour la pile, les 48 suivants étaient utilisés pour autre chose. L'idée est que le registre 0 mémorise un emplacement vide, le registre 1 contient le sommet de la pile, le registre 2 l'opérande sous le sommet de la pile, et ainsi de suite. Les 15 premières opérandes de la pile sont donc mémorisées dans les registres. Les instructions écrivent leur résultat dans le registre adéquat, puis tous les registres sont décalés pour que le résultat passe dans le registre R1. Par exemple, les instructions PUSH chargent une opérande dans le registre 0, puis tous les registres sont décalés de un rang, pour que l'opérande chargée soit dans le registre 1. Le registre 0 devient alors le registre numéro 1, le registre 1 devient le 2, et ainsi de suite. Par contre, les opérations arithmétiques font autrement. Une instruction d'addition, par exemple, lit les opérandes dans le registre 1 et 2, puis place son résultat dans le registre 2 (les deux opérandes sont dépilées). Puis, les registres sont décalés d'un rang pour que ce registre 2 devienne le 1. Les registres ne sont en réalité pas décalés, il n'y a pas de transferts entre registres. A la place, les 16 premiers registres utilisaient une forme limitée de renommage de registres, similaire au banc de registre tournant utilisé sur les processeurs EPIC comme l'Itanium. Les numéros de registres sont renommés, décalés, à chaque fois qu'on empile ou dépile une opérande. Pour faire le renommage de registres, processeur contient un ''register stack counter'' de 4 bits, qui est incrémenté à chaque fois qu'on empile une donnée, et est décrémenté quand on dépile une donnée. Quand on veut accéder à un opérande, le numéro de registre architectural associé est traduit en numéro de registre physique, en additionnant le ''register stack counter'' (modulo 16, pour rester sur 4 bits). ==L'accélération de l'interprétation/JIT : le ''Thumb-EE'' d'ARM== Quelques processeurs ont ajouté des instructions pour faciliter le travail des interpréteurs/JIT. Par exemple, en 2005, ARM a ajouté le mode ''Thumb Execution Environment'', qui faisait cela. Il reprenait le jeu d'instruction compact ''thumb'' et ajoutait quelques instructions et modifiait le comportement d'instructions existantes. Le jeu d'instruction ''Thumb-EE'' a été ajouté sur les CPU ARM en 2005, mais a été déprécié en 2011, par manque d'utilité. Mais la tentative mérite qu'on s'attarde dessus. Les CPU de gamme M, déstinés à l'embarqué, n'ont jamais supporté ''Thumb-EE'', vu que de tels processeurs ne sont pas conçus pour exécuter du code interprété/JIT. Le mode ''Thumb-EE'' est un mode d'exécution, séparé de l'ARM normal. Ainsi, une instruction ''Thumb-EE'' et une instruction ARM peuvent avoir le même encodage en binaire, mais se comporter différemment. Le processeur est à tout moment soit en mode ''Thumb-EE'', soit en mode ARM normal, ce qui précise comment ces instructions doivent se comporter. Des instructions ont été ajoutées par ''Thumb-EE'', pour qu'il fasse son travail. L'entrée et la sortie du mode ''Thumb-EE'' se fait avec deux instructions : ENTERX et LEAVEX. Une fois en mode ''Thumb-EE'', le décodage des instructions ''thumb'' se fait avec les règles du ''Thumb-EE'', et non celles du ''thumb'' normal. Une première différence entre ''thumb'' normal et ''Thumb-EE'' est l'ajout de ce qui s'appelle un ''null check'' pour les instructions mémoire. Le ''null check'' est utilisé pour les instructions en mode d'adressage "base + indice" ou "Base + décalage", ou tout autre mode d'adressage avec un registre de base utilisés dans des calculs d'adresse. L'idée est de vérifier si l'adresse de base vaut zéro ou non. Si c'est le cas, le processeur lève une exception matérielle, qui est traitée par l'interprétation ou le compilateur JIT. Une telle situation est en effet signe d'une erreur d'adressage mémoire, qui doit être traitée par l'interpréteur. Typiquement, c'est signe qu'un pointeur n'a pas bien été initialisé, ce qui peut arriver avec du code interprété ou JIT. Les interpréteurs/JIT traitent généralement la situation en ajoutant un test avant toute instruction mémoire, pour vérifier si le pointeur accédé vaut zéro ou non. Avec ''Thumb-EE'', pas besoin d'ajouter les tests en questions : ils sont réalisés automatiquement lors de chaque instruction mémoire. : Les registres pour la pile, comme le pointeur de pile et le pointeur de ''frame'', sont aussi concernés. L'instruction CHKA vérifie que les accès à un tableau ne débordent pas en dehors du tableau en question. Par exemple, pour un tableau de 1024 éléments, elle vérifie si l'indice est compris entre 0 et 1024. Pour cela, elle vérifie si l'indice de tableau est bien dans l'intervalle adéquat et lève une exception matérielle si ce n'est pas le cas. Son utilité se comprend quand on sait que tous les accès à un tableau sont vérifiés par l'interpréteur. L'interpréteur est censé ajouter des instructions pour vérifier les indices, à savoir deux ou trois branchements. Avec CHKA, le test se fait en une seule instruction. D'autres modifications mineures de l'encodage des instructions sont aussi présentes, ainsi que des modifications pour les instructions LOAD/STORE. Précisément, dans les modes "base + indice" et "base + décalage", le décalage et l'indice subissent maintenant des décalages. L'idée est de simplifier le calcul d'adresse. De quoi économiser quelques instructions lors des calculs d'adresse, ce qui facilite le travail de l'interpréteur/JIT. Et des instructions LOAD/STORE précises voient leur comportement modifié pour faciliter la gestion de la pile de Java ou l'exécution des méthodes locales. Pour faciliter le travail de l'interpréteur, deux instructions HBP et HBLP ont été ajoutées. Elles branchent vers une fonction qui gère les exceptions logicielles (une fonctionnalité présente dans de nombreux langages, comme Java). La première effectue un branchement vers la fonction en question, la seconde sauvegarde l'adresse de retour avant de faire ce branchement. Les deux peuvent brancher vers 256 fonctions pré-déterminées, sans avoir à présenter leur adresse. ==La traduction binaire accélérée par le matériel== Plus haut, nous avons surtout parlé des compilateurs et des interpréteurs. Cependant, nous devons aussi parler de la '''traduction binaire'''. Elle traduit un programme écrit dans un code machine vers un autre code machine. Par exemple, elle traduit un programme compilé pour un CPU x86 vers un code machine ARM. En général, le système d'exploitation est généralement compilé pour le jeu d'instruction natif, mais il exécute des applications prévues pour le x86. Les applications sont traduites par le système d'exploitation, avant d'exécuter le code traduit. : Nous parlerons dans la suite de code machine source et de code machine cible, pour parler respectivement du code à traduire et du code obtenu après traduction. Idem avec d'autres termes comme architecture cible/source, ou jeu d'instruction cible/source. La traduction binaire est surtout utilisée pour des questions d'émulation ou de compatibilité. Par exemple, lorsque les Macintosh sont passés de processeurs Power PC vers des processeurs x86, le système d'exploitation Mac OS utilisait la traduction binaire pour convertir les anciennes applications Power PC vers du code x86. Les utilisateurs n'y ont vu que du feu. Le système de traduction binaire était appelé Rosetta 1. Par la suite, lors de la transition de processeurs x86 vers des processeurs Apple, Rosetta 2 a vu le jour. La traduction binaire a surtout été utilisée pour traduire du code x86 vers un autre jeu d'instruction. Il faut dire que le x86 est le jeu d'instruction dominant. De nombreuses entreprises ont eu pour ambition de briser l'hégémonie du x86 sur PC, en remplaçant le x86 par un jeu d'instruction plus performant, tout en gardant une compatibilité maximale. La traduction binaire était la seule solution pratique. Elle était le plus souvent intégralement réalisée en logiciel, comme c'était le cas sur les architectures Itanium avec le ''IA-32 Execution Layer''. ===Les généralités sur la traduction binaire assistée en matériel=== L'architecture source est presque tout le temps une architecture CISC, assez ancienne, qu'on souhaite émuler. Le choix de l'architecture cible se porte souvent sur une architecture VLIW, fort différente de l'architecture source, et ce pour obtenir une économie de matériel conséquente. Pour s'exécuter rapidement, le code traduit doit exploiter le parallélisme d'instruction, à savoir exécuter plusieurs instructions en même temps dans des unités de calcul séparées. Il est possible d'utiliser un processeur superscalaire avec exécution dans le désordre pour cela, mais au prix d'un cout important en transistors. Alors qu'en utilisant un CPU VLIW, c'est le traducteur binaire qui fait tout le travail d'extraction du parallélisme d'instruction. Et après tout, quitte à avoir un traducteur binaire, autant lui refiler le boulot d'optimisation. Compiler du code à la volée est certes assez lourd, mais qu'il existe des algorithmes efficaces pour regrouper des instructions indépendantes dans une seule instruction VLIW. Au passage, le processeur VLIW a plus de registres que le processeur source. Cela permet de faire du renommage de registres directement en logiciel. Le traducteur binaire n'hésite pas à changer les noms de registres entre instructions source et instruction VLIW cible, afin de supprimer des dépendances de données. En conséquence, cela ouvre des opportunités de parallélisme, qui sont exploitées lors du regroupement des opérations en instructions VLIW. Vous pourriez penser que le choix d'un CPU VLIW pour émuler du CISC est tout sauf optimal. Vous devez penser que la traduction binaire est d'autant plus simple que l'architecture source et cibles sont semblables. Dans les faits, ce n'est pas tellement traduire les instructions qui pose problème, mais plus la gestion du registre d'état, des exceptions matérielles, de la mémoire virtuelle, la différence entre gros-boutisme et petit-boutisme, de même que des différences d'adressage pour les périphériques. Le premier problème est la gestion des conditions, notamment en présence d'un registre d'état. En pratique, la traduction binaire s'utilise pour traduire du code CISC vers du code RISC ou VLIW. Les architectures source ont donc un registre d'état, qui est mis à jour non seulement par des instructions de test, mais aussi des instructions arithmétiques. L'architecture cible n'a elle pas de registre d'état, mais des registres à prédicats. La traduction de l'un vers l'autre est alors quelque peu compliquée. Et elle est d'autant plus compliquée que le registre d'état est la source de dépendances d'instruction implicites, qui réduisent les performances. Par exemple, si une instruction arithmétique modifie le registre d'état, cela peut impacter l'exécution d'une instruction ultérieure, qui lit ce registre d'état. Pour éliminer ces fausses dépendances, le traducteur binaire doit renommer ce registre en logiciel et détecter les dépendances utiles. Et le compilateur doit gérer les cas où il y a beaucoup de distance entre les deux, voire les cas où l'instruction dépendante n'a pas encore été analysée par le compilateur. Un autre problème est lié à la gestion des exceptions matérielles, et précisément des exceptions précises. Pour rappel, une instruction VLIW regroupe plusieurs opérations, opérations qui correspondraient à une instruction machine sur un CPU pas VLIW. En conséquence, plusieurs instructions du langage machine "source" sont regroupées en une seule instruction VLIW. Et il faut tenir compte du cas où une instruction source lève une exception. Dans le code VLIW, cela signifie qu'une opération lève une exception, et il faut annuler partiellement l'instruction VLIW associée. Et par partiellement, on veut dire que seules les opérations suivantes dans l'ordre du programme source doivent être annulées, puis ré-exécutées. Et c'est un sacré casse-tête ! Les processeurs VLIW utilisent comme solution le mécanisme d'exceptions différées des processeurs EPIC, vu il y a quelques chapitres. Pour résumer, le code est exécuté par blocs d'instruction, délimités par des branchements ou tout autre limite/barrière pertinente dans le code. Un bloc de code est compilé en deux versions : une version rapide sans exceptions matérielles, une version lente qui gère les exceptions précises. La version rapide s'exécute sans exécuter les exceptions. Cependant, les exceptions matérielles sont enregistrées, pour être prises en compte à la toute fin du bloc de code. La version rapide est exécutée en premier et elle mémorise si une exception matérielle a eu lieu. Puis une instruction vérifie si une exception a eu lieu et décide quoi faire. Si aucune exception n'a eu lieu, elle passe à la suite du programme, les résultats du bloc de code sont définitivement acceptés. Mais si une exception a eu lieu, tout est annulé. Le processeur est remis dans l'état initial, puis le code est ré-exécuté instruction par instruction de manière à gérer l'exception correctement. ===La traduction par pré-décodage=== Une première solution serait de faire la traduction binaire dans le cache d'instruction. Lors d'un défaut de cache, le code chargé depuis la RAM est traduit en code machine cible. Et c'est ce code machine cible qui est mémorisé dans le cache d'instruction et exécuté par le processeur. La technique marche sur le papier et n'est qu'une amélioration des techniques de pré-décodage vues il y a quelques chapitres. Cependant, le code obtenu est une traduction assez basique, qui n'incorpore pas d'optimisations dignes de ce nom. De plus, elle gère mal le cas où la taille du code source et cible sont potentiellement très différentes. Concrètement, elle est surtout utile pour traduire du code d'un processeur RISC vers un autre processeur RISC, dont les tailles d'instructions sont similaires. Elle ne permet pas d'émuler plusieurs jeux d'instructions différents, le cout en matériel (un circuit de pré-décodage par jeu d'instruction) serait trop important. ===Le projet DAISY d'IBM=== De très rares processeurs étaient conçus pour accélérer cette traduction binaire, afin de garder de bonnes performances. Les premiers à avoir étudié l'idée étaient IBM, avec leur projet DAISY (''Dynamically Architected Instruction Set from Yorktown''). Le projet de base était de convertir à la volée du code compilé pour des CPU Power PC, vers du code VLIW. Les chercheurs d'IBM avaient développé un algorithme de traduction dynamique efficace, ainsi qu'un processeur VLIW disposant d'optimisations spécifiques à la traduction binaire dynamique, à la volée. De nombreuses idées de ce projet ont été reprises ou re-découvertes par la société Transmetta, avec ses processeurs Crusoe et Efficieon, puis par NVIDIA avec son projet Denver. Les ingénieurs de ce projet ont étudié la possibilité d'émuler plusieurs jeux d'instructions différents sur un même processeur, notamment le s390 d'IBM et le x86. En théorie, cela demande juste d'avoir plusieurs programmes de traduction binaire : un pour le x86, un autre pour le s390, éventuellement un autre pour le Power PC. Mais cela ne s'est pas concrétisé. Mais les ingénieurs ont étudié quelle pourrait être l'architecture VLIW idéale pour ça. Par exemple, il fallait des additions 3-opérandes pour simplifier les calculs d'adresse. Une difficulté était la gestion du registre d'état, dont les bits ne sont pas mis à jour de la même manière sur le x86 et le s390 ou le Power PC. La différence entre gros-boutisme et petit-boutisme était aussi un problème, de même que des différences d'adressage pour les périphériques. Pour que la traduction binaire soit efficace, le processeur VLIW intègre diverses optimisations, comme les exceptions différées et les branchements multi-voies. La principale est la suivante : seul le code des boucles ou fonctions exécutées fréquemment est traduit en code VLIW, le reste du code est interprété. En effet, traduire du code binaire prend plus de temps qu'une simple interprétation. Pour du code qui ne sera exécutée qu'une seule fois, il est plus rapide d'utiliser l'interprétation. Par contre, pour du code exécuté beaucoup de fois, le cout de la traduction binaire est amorti, dilué sur N exécutions, compensé par le gain en temps d'exécution de ce code traduit. Au final, cela permet de ne traduire que le code qui le mérite. La détection du code fréquemment exécuté est réalisée dans le cœur VLIW. L'unité de branchement mémorise les derniers branchements rencontrés et le nombre de fois qu'ils ont été exécutés. Si ils ont été exécutés un certain nombre de fois, l'unité de branchement lève une exception matérielle, qui invoque le traducteur binaire. La routine de cette exception est le traducteur binaire proprement dit. En clair, la traduction binaire est démarrée quand le processeur détecte qu'une fonction a été exécutée plus de N fois, via une exception matérielle dédiée. La mesure du nombre d'exécution d'un branchement se fait dans l'unité de calcul dédiée aux branchements, avec l'aide d'une sorte de ''branch adress buffer'' modifié. Pour rappel, le ''branch adress buffer'' mémorise le ''Program counter'' de chaque branchement récemment rencontré. Ici, chaque entrée du ''branch adress buffer'' est associée à un compteur incrémenté à chaque exécution du branchement. ===Les processeurs Crusoe et Efficieon de Transmetta=== Les processeurs Crusoe Et Efficeron sont deux processeurs VLIW produits par la société Transmetta, une société californienne rachetée par NVIDIA. Ils étaient conçus pour exécuter spécifiquement des programmes x86, système d'exploitation inclus. L'idée derrière ce projet était d'exécuter du code x86 sans que les applications, ni même le système d'exploitation et le BIOS soient au courant ! Les processeurs Transmetta étaient en réalité conçus pour exécuter un programme unique, le ''Code Morphing Software'' (CMS), qui traduisait le code x86 en code VLIW. Transmetta ne voulait pas que son processeur VLIW soit exploité directement, sans le CMS. Il n'avait pas rendu public de compilateur pour traduire du C vers du code VLIW, il n'avait pas donné la documentation du jeu d'instruction VLIW. Des efforts de rétro-ingénieurie, documentés sur le site realworldtech, ont cependant permis de comprendre comment étaient encodées les instructions du processeur Crusoe. Comme pour le projet Daisy, le ''Code Morphing Software'' utilisait à la fois interprétation et traduction binaire, selon les besoins. Les instructions sont interprétées lors de leurs premières exécutions, mais le CMS bascule sur de la traduction en code VLIW après un certain nombre d'exécution. Ainsi, les boucles souvent exécutées sont traduites en code VLIW, alors que le reste du code est interprété. Le choix du code à traduire est le fait du CMS, il utilise des heuristiques complexes pour, qui ne sont pas connues dans le détail. Le ''Code Morphing Software'' est mémorisé dans une EEPROM, ce qui en fait un ''firmware'' situé en dessous du BIOS. Le CMS démarre ensuite le BIOS, qui lui-même démarre le système d'exploitation, qui lui-même démarre les pilotes de périphériques et les programmes. Le CMS se réserve les 16 premiers mébioctets de l'espace d'adressage. L'EEPROM du CMS est mappée dedans, mais n'en utilise que la moitié. Le reste est utilisé comme cache, pour mémoriser le code VLIW traduit par le CMS. Les deux processeurs intégraient aussi deux mémoires SRAM, utilisées par le CMS, appelées la ''local program memory'' (LPM) et la ''local data memory'' (LDM). La première contient du code qui gére les interruptions, la mémoire virtuelle, les exceptions matérielles, les problèmes d'alignement mémoire, et quelques fonctions très fréquemment utilisées par le CMS. La seconde est de la mémoire RAM utilisée par ces fonctions de la ''local program memory''. Les mémoires caches L1 et L2 sont séparées de ces deux mémoires. Le processeur Crusoe et Efficieon étaient des processeurs VLIW très simples. Ils n'avaient même pas de MMU, ni de fonctionnalités importantes sur les CPU x86. Pour Crusoe, les instructions VLIW étaient encodées sur 64 ou 128 bits, et regroupaient entre 2 à 4 opérations. Les instructions LOAD/STORE ne supportaient pas d'adressage indicé, juste de l'adressage indirect à registre. Crusoe avait 5 unités de calcul : deux ALU entières, une unité LOAD/STORE, une unité de branchement, une FPU. Efficieon doublait le nombre d'ALU entière et d'unité LOAD/STORE, les instructions VLIW passaient à 256 bits. Le processeur Crusoe contient 160 registres, dont 64 registres généraux, 32 registres flottants. Les registres généraux font 32 bits, ce qui est cohérent avec le fait que les CPU x86 de l'époque étaient des processeurs 32 bits. Quant aux registres flottants, ils faisaient 80 bits, ce qui colle avec la taille des registres flottants de la FPU x87 utilisée à l'époque. Sur les 64 registres généraux, seuls 48 étaient réellement utilisable pour mémoriser des opérandes. Une partie des registres généraux étaient utilisés pour la gestion de la pile, d'autres pour mémoriser l'état du CPU x86 émulé, un registre était un registre zéro non accesible en écriture. La gestion des exceptions est optimisée avec un système d'exceptions différées, le même que celui décrit plus haut. Le processeur utilise un système similaire, pour effectuer des lectures anticipées, qui a été expliqué dans le chapitre sur les processeurs VLIW/EPIC. Sauf qu'il s'agit là d'une optimisation pour gagner en performances en général, pas une aide utile pour gérer des exceptions précises ou toute autre subtilité de la traduction binaire. Pour gérer les exceptions différées et les lectures anticipées, le processeur mémorise l'état du processeur avant de démarrer un bloc de code. Pour cela, le processeur contient des ''shadow registers'', 48 registres entiers et 16 registres flottants. Vous remarquerez qu'il y a autant de registres entiers que de ''shadow registers'' entiers. Les registres entiers/flottants sont mémorisés dans les ''shadow registers'' avant d'exécuter un bloc de code, afin que le processeur puisse revenir à l'état de base. Une autre fonctionnalité liée est le ''gated store buffer''. Le principe est d'éviter toute écriture en mémoire RAM, tant que le code VLIW n'a pas émis d'instruction ''commit''. L'idée est que les écritures sont conservées dans la ''store queue'' de l'unité LOAD/STORE. Pour rappel, la ''store queue'' met en attente les écritures soit tant que les écritures ne sont pas terminées, soit tant que la RAM est occupée. L'idée est que les écritures sont accumulées dans la ''store queue'' tant que l'instruction ''commit'' n'a pas eu lieu. Le ''store queue'' est "déversé dans la RAM" seulement quand l'instruction ''commit'' s'exécute. ===Les CPU NVIDIA Denver=== Nvidia a tenté quelque chose de similaire avec son projet Denver. NVIDIA a racheté l'entreprise Transmetta, ses brevets, et a tenté de refaire la même chose. Le projet initial était de traduire du code x86 vers un jeu d'instruction VLIW propriétaire, comme l'a fait Transmetta. Mais NVIDIA n'a pas réussi à acquérir la licence du jeu d'instruction x86 et s'est rabattu sur le jeu d'instruction ARM. Le processeur né de ce projet est le Tegra K1-64 CPU. Il s'agit d'un processeur composé d'un cœur VLIW, couplé à deux décodeurs d'instructions ARM. Les deux décodeurs ARM traduisent une paire d'instructions ARM consécutives en instructions VLIW, pour les exécuter sur le cœur VLIW. Le CPU peut fonctionner selon deux modes : ARM et VLIW. En somme, le processeur supporte deux jeux d'instruction : le jeu d'instruction ARM, et un jeu d'instruction VLIW. En mode ARM, les instructions ARM sont chargées depuis le cache, traduites en VLIW par les décodeurs ARM, puis exécutées par le cœur VLIW. En mode optimisé, le CPU exécute des instructions VLIW chargées depuis le cache d'instruction, les décodeurs ne sont pas utilisés. : Le CPU supporte en réalité deux jeux d'instruction ARM : ARM8 et 7. Mais ils correspondent au même mode pour le CPU. Un point important est que le processeur peut changer très rapidement de mode, en à peine quelques cycles d'horloges. On n'est clairement pas dans le cas des CPU x86, qui mettent des plombes pour passer du mode 32 à 64 bits et inversement. La commutation est tellement rapide qu'on peut considérer que le processeur supporte deux jeux d'instruction simultanément. Et le support simultané de deux jeux d'instruction facilite l'implémentation de la traduction binaire. Encore une fois, inutile de traduire du code qui ne sera exécutée qu'une seule fois, mieux vaut l'exécuter directement dans le mode ARM. Surtout que le processeur supporte un mode ARM qui permet d'exécuter du code ARM sans perte de performance. Par contre, le code fréquemment utilisé, notamment dans des boucles critiques, est traduit en VLIW. Au final, les performances sont optimisées, en limitant le travail du logiciel traducteur binaire. La détection du code fréquemment exécuté se fait différemment que pour les processeurs Transmetta et Daisy. L'unité de branchement ne détecte pas directement le code beaucoup exécuté, même si elle a un rôle à jouer. A la place, la détection est réalisée en logiciel, par un ''thread'' dédié, qui s'exécute sur un cœur séparé. Il détecte le code fréquemment utilisé en regardent les branchements exécutés récemment. Pour cela, le processeur mémorise l'historique des branchements pris récemment et copie cette historique dans une mémoire tampon dédiée, partagée entre tous les cœurs. Vu qu'elle est partagée, le ''thread'' de détection a accès à l'historique sans pertes de performances, seule la copie de l'historique dans ce tampon a un cout en performance, pas son partage. Le code VLIW obtenu après traduction binaire est mémorisé dans la mémoire RAM, dans une portion spécialement réservée pour. Elle fait 128 mébioctets, et est appelée le '''cache d'optimisation''' par NVIDIA. Le cache d'optimisation est protégé en écriture et seul le traducteur binaire peut écrire dedans. Au passage, le traducteur binaire est du code VLIW, ce qui fait qu'il est placé dans le cache d'optimisation. Le processeur détecte automatiquement quand une fonction pour laquelle le code compilé équivalent est disponible. Pour cela, l'unité de chargement contient une table de correspondance entre l'adresse de la fonction ARM, et l'adresse de son équivalent compilé. Quand une fonction est appelée, l'unité de chargement regarde l'adresse de destination du branchement. Si l'adresse est dans cette table, elle récupère l'adresse de la fonction compilée et branche vers celle-ci. Le processeur dispose d'un cache d'instruction de 128 kibioctets, ce qui est très important, mais nécessaire vu la taille des instructions VLIW. Le coeur VLIW dispose de ses propres décodeurs, d'un ''scoreboard'', de 7 unités de calcul et de registres. Pour les unités de calcul, il a deux FPU, deux ALU, une unité de branchement, et deux unités LOAD-STORE faisant aussi ALU. Un point important est que le CPU dispose de 64 registres entiers et de 64 registres flottants. C'est deux fois plus que supporte le jeu d'instruction ARM. En mode ARM, seule la moitié des registres est utilisée. Mais en mode optimisé, le traducteur binaire utilise bien les 64 registres, grâce à une sorte de renommage de registres logicielle. Pour simplifier la traduction binaire, le processeur supporte les techniques vues précédemment. Le processeur intègre un ''gated store buffer'' similaire à celui des processeurs Transmetta. Les techniques d'exceptions différées sont aussi supportées, comme sur les processeurs Transmetta. Il a aussi une unité de préchargement décrite par NVIDIA comme agressive, avec support du préchargement de type anticipé (''runahead''). ===L'émulation de la mémoire virtuelle=== Pour finir, il faut parler de la mémoire virtuelle avec la traduction binaire. Sur le processeur source, le processeur gère à la fois des adresses physiques et virtuelles et sa MMU fait la conversion entre les deux. Sur le processeur VLIW, la MMU est simulée par le programme de traduction binaire, partiellement ou totalement. L'implémentation exacte varie suivant que l'on parle du projet Daisy, des processeurs Transmetta ou de Denver. Sur les processeurs Transmetta, il n'y a pas de MMU ni de mémoire virtuelle. A la place, le programme de traduction binaire émule la mémoire virtuelle du processeur source. Le processeur VLIW ne gère que des adresses physiques, rien d'autre. L'espace d'adressage physique du processeur VLIW a la même taille que celui du processeur émulé, ici des processeurs x86. Il est vraisemblable que les adresses physiques utilisées par le processeur x86 sont les mêmes que celles du processeur VLIW. Sur le projet Daisy, le processeur VLIW gère la mémoire virtuelle via pagination, comme les processeurs émulés. Ce qui fait qu'il gère un espace d'adressage virtuel et un espace d'adressage physique. Pour éviter toute confusion, nous parlerons d'adresse physique/virtuelle VLIW pour les adresses physiques/virtuelles du processeur VLIW, d'adresse virtuelle/physique source pour celle du jeu d'instruction traduit, à savoir du Power PC ou du s390, éventuellement de l'x86. Il faut alors faire le lien entre adresses physiques source et adresses virtuelles VLIW. Pour cela, rien de plus simple : il y a correspondance parfaite. L'adresse physique source numéro N correspond à l'adresse virtuelle VLIW numéro N. Cependant, cela signifie que tout l'espace d'adressage virtuel VLIW serait utilisé par le code à traduire. En réalité, il faut ajouter de la place pour le code traduit et le programme de traduction binaire. La conséquence est que l'espace d'adressage virtuel VLIW est plus large que l'espace d'adressage source. L'espace d'adressage virtuel VLIW est découpé en trois sections : une section pour le code Power PC à traduire, une autre pour le code traduit en VLIW, et une dernière réservée au traducteur binaire. Le programme de traduction est placé dans une ROM mappée en mémoire dans la seconde section. Le reste de la seconde section est réservé à une mémoire RAM utilisée par le programme de traduction. [[File:Mémoire virtuelle sur le projet Daisy.png|centre|vignette|upright=1.5|Mémoire virtuelle sur le projet Daisy]] Il faut noter que chaque page physique source, une fois traduite, correspond à N pages virtuelles VLIW. La raison est que le code traduit prend plus de place que le code machine originel. En conséquence, on doit utiliser une page finale plus grande. Pour le reste, des pages contiguës en mémoire physique source sont elles aussi contiguës en mémoire virtuelle VLIW. Le calcul d'adresse est donc simple : l'adresse physique source est multipliée par N, puis on ajoute l'adresse de base à laquelle commence la section pour le code traduit. ==La traduction binaire sur le Burroughs B1700== Nous venons de voir comment accélérer en matériel la traduction binaire, avec des techniques bien spécifiques. Le Burroughs B1700 a procédé autrement et il est intéressant d'étudier en détail son architecture. Son architecture est décrite dans le livre "Interpreting Machines : Architecture and Programming of the Bl700/Bl800 Series", écrit par Elliott I. Organick et James A. Hinds. Une partie de ce qui vfa suivre est un très court résumé de ce livre. ===Le Burroughs B1700 : le mal-nommé ''split-level control store''=== Le Burroughs B1700 avait plusieurs modèles : le B1726, le B1710 et le B1800. Ils se distinguaient par la manière dont le code machine source était traduit par le traducteur binaire. Dans ce qui suit, nous parlerons de traducteur binaire pour parler soit d'un interpréteur pour un langage de programmation de haut niveau, soit pour un logiciel de traduction binaire. Le B1710 plaquait le traducteur binaire en mémoire RAM. Le B1800 faisait pareil, mais avait ajouté un cache dédié au code du traducteur binaire, afin d'améliorer les performances. Le B1726 avait un traducteur binaire scindé en deux : une partie était en mémoire RAM, l'autre était dans un ''local store'' dédié. Le processeur peut exécuter le traducteur binaire soit depuis le ''local store'', soit depuis la mémoire RAM avec une pénalité en termes de performances. La documentation appelle cela un '''''split-level control store''''', mais le terme est trompeur : la documentation qualifie de microcode le langage machine du Burroughs B1700, ce qui est à l'origine de beaucoup de confusions. Une autre source de confusion est que ce langage machine a quelques propriétés qu'on pourrait attendre d'un microcode, mais certaines sont manquantes. Par exemple, il peut adresser les registres d’interfaçage mémoire, mais c'est parce que ce sont des registres architecturaux sur ce processeur. Ses instructions n'encodent pas de signaux de commande, par contre, elles font 16 bits et adressent les opérandes de manière implicite. Le ''local store'' mémorisait au maximum 2048 instructions machine, ce qui fait que les 2048 premières "adresses" correspondaient au ''local store''. Les suivantes étaient des adresses de mémoire RAM qui contenaient le reste du traducteur binaire. En pratique, l'adresse qui séparait les deux était configurable ! On pouvait limiter la taille du ''local store'' à un multiple de 32 instructions ! Les adresses restantes étaient alors réattribuées à la RAM. Par exemple, on pouvait attribuer 64 adresses au ''local store'', les 2048 - 64 adresses restantes étaient alors attribuées à la mémoire RAM. Un registre limite, nommé TDPM, indiquait quelle était l'adresse de démarcation entre les deux. Il contenait 4 bits, dont la valeur était multipliée par 32, pour obtenir l'adresse limite. Le programmeur devait découper le traducteur binaire en segments, certains étant placé dans le ''local store'', les autres étant en mémoire RAM. Mieux que ça, le processeur autorisait d'utiliser la technique de l'''overlaying'' pour le microcode ! Il y avait même une fonction OVERLAY dédiée pour ! OVERLAY avait besoin de trois opérandes : la taille du segment à copier, son adresse source en mémoire RAM, son adresse de destination dans la micro-SRAM. L'interpréteur réservait un "segment" en mémoire RAM, pour les données, dont le processeur connaissait la position. En-dehors de ce segment, il n'y a que des instructions machines à traduire, et le traducteur binaire. Le CPU incorporait pour cela deux registres, un pour l'adresse de base du segment de données, un autre pour son adresse de fin. Il incorporait une protection mémoire limitée sur ce segment, à savoir que seul l'interpréteur pouvait lire ou écrire dans ce segment. ===Une architecture bit-adressable=== Le CPU Burroughs B1700 était '''bit-adressable'''. En clair, il pouvait adresser la mémoire bit par bit, si nécessaire. Un avantage est que cela facilitait la traduction binaire des instructions machine, mais nous détaillerons cela plus bas. Un autre avantage est que cela permettait d'émuler des architectures dont la taille des registres/''byte''/mots était très différente. Et cet avantage mérite quelques explications immédiates. Le processeur était un CPU 24 bits, mais il pouvait émuler des processeurs 16 bits, 8 bits, 4 bits, 1 bit, 9 bits, ou toute autre valeur. Pour cela, le processeur lisait des mots de 24 bits et utilisant un ''barrel shifter'' pour sélectionner les bits adéquats. Par exemple, pour un accès de 16 bits, la donnée lue en mémoire était masquée de manière à ne garder que les 16 bits de poids faible. Le ''barrel shifter''/circuit de masquage était placé directement avant le bus mémoire, sur le trajet des données/instructions. Le ''barrel shifter'' était commandé par un registre ''Field Unit'', qui précisait quelle était la taille des données à charger. Pour communiquer avec la mémoire, il avait trois registres séparés : READ, WRITE, MAR. READ contient la donnée lue lors d'une lecture, WRITE est pour une donnée à écrire lors d’une écriture, MAR contient l'adresse à lire ou en cours de lecture. Il s'agit de registres d’interfaçage mémoire. Le fait que ces registres d’interfaçage mémoire et IO soit adressable est peu commun, et c'est plus quelque chose qu'on attend d'un microcode que d'instructions machines. Mais il faut bien comprendre que les registres d’interfaçage mémoire ont été promus au rang de registres architecturaux, sur ce processeur. De même, pour communiquer avec les entrées-sorties, il disposait de deux registres CMND et DATA : CMND pour envoyer une commande à une entrée-sortie, DATA pour lire ou écrire une donnée. Il y avait aussi un registre U, qui mémorisait une donnée lue depuis le lecteur de cassette. ===L'ALU de taille variable=== Les Burroughs B1700 intégrait une pile d'adresse de retour, capable de mémoriser 16 adresses de 24 bits. Il disposait de 4 registres généraux, nommés X, Y, T et L, tous de 24 bits. Un point important est que le registre T avait des possibilités d’extraction spécifiques. Il était possible d'extrait N bits, placés n'importe où dans ce registre, et de les copier dans un autre registre. Nous verrons pourquoi cette possibilité est intéressante et ne sert que pour le registre T. Ils avaient une '''ALU de taille variable'''. En clair, l'ALU pouvait faire des calculs sur un nombre de bits compris entre 0 et 24. Pour cela, les calculs étaient fait avec une ALU de 24 bits, puis les bits de poids fort inutiles étaient masqués. L'ALU de 24 bits pouvait masquer les bits de poids fort du résultat, mais avec des limitations. Elle gérait des opérandes et résultats de 0, 4, 8, 12, 16, 20 et 24 bits. Pour avoir un réglage plus fin, une ALU 4 bits séparée permettait de corriger le résultat. Les registres X et Y étaient reliés à une ALU de 24 bits capable d'effectuer des opérations arithmétiques basiques. Fait étonnant, l'ALU faisait tous les calculs en même temps et fournissait ses résultats dans 7 registres : un registre SUM pour l'addition, un registre DIFFERENCE pour la soustraction, et un registre par opération bit à bit. Les registres pour les opérations bit à bit sont : CMPX et CMPY pour l'opération NOT sur chaque opérande, XANY pour le ET bit à bit, XEOY pour le XOR, XORY pour le OU logique. Les additions et soustractions pouvaient se faire sur des opérandes codés en binaire ou en BCD, suivant le code opération utilisé. A cela, il fallait ajouter deux registres de "résultat" MSKX et MSKY. Ils fournissaient un masque dépendant de la taille du résultat en bits. C'est ce registre qui permettait de gérer des données de 0, 4, 8, 12, 16, 20, et 24 bits, alors que l'ALU faisait 24 bits. Le masque dans ce registre pouvait servir d'opérande dans une opération de masquage ultérieure, pour corriger le résultat. L'ALU prenait en opérande les registres X et Y, mais aussi un registre CYF pour la retenue entrante. En sortie, il fournissait aussi deux retenue sortantes : une pour l'addition et une autre pour la soustraction. Elles étaient mémorisées dans les registres CYL et CYD, respectivement. L'ALU fournissait aussi 12 conditions, qui alimentait un registre d'état. Le registre d'état était segmenté en trois registres appelés XYST, XYCN et BICN. Les registres X et Y sont des registres pour les opérandes, mais il y a aussi deux autres registres de 24 bits. Il s'agit des registres T et L, qui sont des registres généraux. Les registres T et L étaient découpés en 6 sous-registres de 4 bits, nommés LA LB LC LD LE et LF pour le registre L, et TA TB TC TD TE et TF pour le registre T. Ils étaient adressables comme n'importe quel registre. Le processeur contenait de nombreux autres registres de 4 bits, dont les sous-registres de T et L. En tout, il y a 27 registres de 4 bits adressables ! Et le processeur disposait d'une ALU 4 bits pour manipuler ces registres de 4 bits ! Elle est capable de faire des MOV entre registres de 4 bits, des opérations bit à bit, et peut aussi tester si un bit vaut 0 ou 1. La dernière possibilité est très utile pour implémenter les branchements. De plus, cela permettait de faire des opérations de masquage, notamment pour appliquer un masquage supplémentaire à celui de l'ALU. Par exemple, pour gérer des opérations sur 14 bits, on effectue une opération sur 16 bits avec l'ALU, et on masque les 2 bits manquants avec l'ALU 4 bits. La capacité d'extraction du registre T pouvait aussi être utilisé pour ça. ===Le code machine du Burroughs B1700=== Le processeur utilise des micro-opérations de 16 bits de long, qui sont copiées dans registre d'instruction M de 16 bits. Fait intéressant, il était possible de faire un OU logique entre l'instruction lue et un registre du processeur, le résultat étant mémorisé dans le registre M. Cela permettait de faire du code automodifiant, pour émuler des modes d'adressage complexe. Cela permettait par exemple de remplacer une adresse dans une micro-opération. L'adresse est alors surimposée avec un OU dans le champ d'adresse, alors que le reste de l'instruction n'est pas modifié. Afin de gérer les interruptions, ces dernières étaient partiellement détectées en logiciel. Les interruptions ne s'exécutaient pas automatiquement, pour émuler correctement les interruptions. Une interruption doit être prise en compte à la toute fin de l'exécution d'une instruction, quand celle-ci a finit son travail. Mais vu qu'ici, les instructions sont émulées avec des séries de micro-opérations, il y a un problème : il est possible d'interrompre une instruction émulée en plein milieu. Pour éviter cela, le processeur ne déclenche pas les interruptions matérielles immédiatement, elles sont mises en attente. Une micro-opération dédiée vérifie si une interruption est en attente, et l'exécute alors cas échéant. La micro-opération est utilisée comme micro-opération finale d'une instruction émulée. Le ''program counter'' est appelé le registre A. Les instructions du processeur font 16 bits de long, et elles sont alignées en RAM sur 16 bits, ce qui fait que les 4 bits de poids fort de ce registre valent 0, seuls les 14 bits de poids fort sont utiles. Les adresses mentionnées plus haut, qui adressent les micro-opérations en mémoire RAM, sont des adresses de mots de 16 bits, pas les adresses de bit individuels. ===La traduction binaire est aidée par des registres et instructions dédiées=== Les instructions à décoder n'ont pas de longueur fixe, à savoir qu'elles peuvent faire 16 bit avec tel jeu d'instruction, 8 bits avec un autre, 32 bits sur un autre, etc. Elles peuvent aussi être de longueur fixe ou variable ! Le B1700 intégrait donc un support des instructions de longueur variable, qui était "détourné" pour gérer des jeux d'instruction différents. Notons que vu que l'architecture est bit-adressable, les contraintes d'alignement des instructions disparaissent, ce qui a des avantages qu'on va détailler dans ce qui suit ! L'implémentation des instructions de taille variable ajoutait un registre avant le registre d'instruction. Le registre d'instruction proprement dit était un registre M de 24 bits. Il était précédé par un registre T, dans lequel l'instruction était assemblée, en combinant plusieurs morceaux. Si l'instruction est codée est codée sur moins de 24 bits, le registre T se contente de masquer les bits en trop. Si l'instruction chargée dépasser 24 bits, le chargement se faisait en plusieurs passes, avec plusieurs lectures en mémoire. Dans ce qui suit, on suppose que l'instruction est composée de '''champs''' : l'opcode, les opérandes, les adresses absolues, les constantes immédiates, les numéros de registre, etc. L'idée est que l'instruction est chargée dans le registre T, puis découpée en champs, qui sont copiés dans les registres du processeur ou envoyés au microcode. Je rappelle que le registre T est relié à un ''barrel shifter'', différent au précédent, qui permettait de masquer, décaler et extraire une donnée de ce registre. Et il y a la possibilité d'extraire une suite de bit de ce registre T, pour la copier dans un autre registre. Par exemple, prenons une instruction composée d'un opcode, d'une constante immédiate et d'une adresse immédiate. L'opcode est lu et interprété par le microcode. Le microcode extrait la constante immédiate du registre T et la copie dans le registre X. Ensuite, l'adresse est extraite et envoyée au registre FA, une lecture est démarrée, et l'opérande est copiée dans le registre Y. Enfin, l'instruction est exécutée par le microcode. Le tout prend plusieurs micro-opérations : une première pour lire l'opcode, une seconde pour lire la première opérande, une troisième pour lire la seconde opérande, una quatrième pour tout exécuter. Notons que suivant la taille des champs, on peut en charger plusieurs à la fois. Par exemple, imaginons qu'on ait chargé 24 bits, contenant un opcode de 8 bits et une constante immédiate de 16 bits : pas besoin de faire deux accès mémoire, le registre T est juste découpé en un opcode et une constante. Le registre T est fait pour ce genre d'extraction. ===Le chargement d'un champ=== L'architecture est bit-adressable car elle gére des champs qui ne font pas forcément 8 ou 16 bits. Par exemple, certains jeux d'instructions ont des opcodes codés sur 6 ou 7 bits, voire 9-10 bits. Par exemple, prenons un opcode de 7 bits, suivi par une adresse de 12 bits et une constante immédiate de 15 bits : aucun de ces champs ne sera aligné sur 8/16/24 bits. Utiliser une architecture bit-adressable règle le problème d'alignement. Et en général, les contraintes d'alignement des instructions disparaissent ! Quand on charge une instruction, peut importe qu'elle soit ou non alignée sur 24 bits, elle sera placée dans les bits de poids faible, il suffira de masquer les bits de poids fort inutiles. Par exemple, si on charge une instruction de 16 bits, les 16 bits seront copiés dans les 16 bits de poids faible du registre T, il suffira de masquer l'octet de poids fort. Pas besoin de faire de décalages pour gérer des instructions mal alignées... Pour charger une instruction, il faut connaitre plusieurs informations : l'adresse de son premier bit, sa taille. Pour cela, le B1700 contient deux registres, nommés ''Field Adress'' (FA) et ''Field Length'' (FL), qui indiquent l'adresse du premier bit et la taille de l'instruction à charger. Le premier fait 24 bits, ce qui permet de gérer des adresses de 24 bits. N'oubliez pas que le processeur est bit-adressable, ce qui permet d'adresser 16 Mébi-bits, soit 2 mébioctets. Le registre pour la taille fait lui 16 bits, ce qui permet de gérer des instructions de 65536 bits, soit 8192 octets ! Leur nom commence par ''field'', car le processeur peut gérer des instructions de taille variable et/ou plus longues que 24 bits. Dans ce cas, le chargement de l'instruction se fait champ par champ. Pour donner un exemple, prenons une instruction composée de 20 champs de 12 bits chacun. Les champs sont chargés un par un dans le registre T. Le registre FA est alors initialisé avec l'adresse de l'instruction, il est incrémenté de 12 à chaque cycle. Le registre FL est initialisé avec la taille de l'instruction, soit 20 * 12 = 240 bits, et il est décrémenté de 16 à chaque cycle. Précisons cependant que la taille d'un champ peut être changée d'un cycle à l'autre. Par exemple, le processeur peut charger un opcode de 8 bits, l'interpréter, détecter que l'instruction demande ensuite de charger une adresse de 16 bits. Dans ce cas, le registre FL est immédiatement altéré pour charger 16 bits, au lieu des 8 bits de l'opcode immédiatement précédent. ==Les jeux d'instructions dédiés à un langage de programmation== De rares processeurs sont conçus pour un langage de programmation en particulier. On appelle ces processeurs, conçus pour des besoins particuliers, des '''processeurs dédiés'''. Par exemple, les fameux ''Burrough E-mode'' B5000/B6000/B7000 étaient spécialement conçus pour exécuter de l'ALGOL-60. Leurs cousins B2000/B3000/B4000 étaient eux conçus pour le COBOL. Des langages fonctionnels ont aussi eu droit à leurs processeurs dédiés. Le prolog en est un bel exemple, avec les superordinateurs de 5ème génération qui lui étaient dédié. On peut aussi citer les machines LISP, dédiés au langage LISP, qui datent des années 1970. Elles étaient capables d’exécuter certaines fonctions de base du langage directement dans leurs circuits : elles possédaient notamment un ''garbage collector'' câblé dans ses circuits ainsi que des instructions machines supportant un typage déterminé à l’exécution. Les processeurs dédiés ont eu leur heure de gloire au début de l'informatique, à une époque où les langages de haut niveau venaient d'être inventés. À cette époque, les compilateurs n'étaient pas performants et ne savaient pas bien optimiser le code machine. Il était alors rationnel, pour l'époque, de rapprocher le code machine cible et le langage de programmation de haut niveau. De nombreuses architectures dédiés ont ainsi été inventées, avant que les concepteurs se rendent compte des défauts de cette approche. Les défauts en question ne sont pas nombreux, mais assez simples à comprendre. Premièrement, elles sont très rapides pour un langage de programmation en particulier, mais sont assez mauvaises pour les autres, d'où un problème de "compatibilité". Ajoutons à cela que les langages de programmation peuvent évoluer, devenir de moins en moins populaires/utilisés, ce qui rend la création d'architectures généralistes plus pertinente. Enfin, les architectures dédiées sont évidemment des processeurs CISC, pour implémenter les nombreuses fonctionnalités des langages évolués. Et les défauts des CISC sont assez rédhibitoires à l'heure actuelle. À l'heure actuelle, les algorithmes des compilateurs se sont améliorés et savent nettement mieux utiliser le matériel. Ils produisent du code machine efficace, ce qui rend les architecture dédiées bien moins intéressantes. Si on ajoute les défauts de ces architectures dédiées, par étonnant que les architectures dédiées aient presque disparues. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'accélération matérielle de la virtualisation | prevText=L'accélération matérielle de la virtualisation | next=Le matériel réseau | nextText=Le matériel réseau }} </noinclude> 1fj027q9qrz9g2ro510tw7ko4z1ltp2 764050 764049 2026-04-19T20:31:34Z Mewtow 31375 /* Le code machine du Burroughs B1700 */ 764050 wikitext text/x-wiki De nos jours, la majorité des programmeurs programment dans des langages de haut niveau. Il est très rare pour eux d'avoir à utiliser de l'assembleur, et encore moins un langage machine. Les programmes écrits dans un langage de haut niveau sont traduits en langage machine par un logiciel appelé le compilateur. [[File:Compilation.PNG|centre|vignette|upright=2.5|Compilateur : principe.]] Il se trouve que quelques rares processeurs sont conçus pour faciliter le travail du compilateur. Et ce sont ces processeurs que nous allons voir dans ce qui suit. Ils représentent une catégorie de jeux d'instruction à part, qui n'a pas vraiment de nom. ==La compilation et l’interprétation== Un compilateur traduit donc du code source, écrit dans un langage de haut niveau, vers du langage machine. La traduction est rarement directe. En général, le compilateur traduit le code source en assembleur, qui est lui-même traduit en langage machine. L'assembleur est une représentation textuelle du langage machine alors que le code machine est du binaire exécutable par le processeur. La traduction se fait donc en deux étapes, la '''compilation''' proprement dite et l''''assemblage''', réalisées respectivement par un compilateur et un assembleur. Il faut parfois rajouter une troisième phase d'édition des liens, que nous passons volontairement sous silence. {| |[[File:Code C.png|vignette|200px|Le code source, ici en C.]] |[[File:Assem.png|vignette|250px|Le code assembleur.]] |[[File:Binary file - hello world (C programming).png|vignette|150px|Le code machine.]] |} ===L'interprétation et la compilation à la volée=== Une alternative à la compilation est l''''interprétation''', qui transforme le code source en code machine à la volée. Avec l'interprétation, le code source est passé à un logiciel appelé l''''interpréteur''', qui exécute le code source ligne de code par ligne de code. Une ligne de code est traduite en code machine, qui est exécutée, puis l'interpréteur passe à la ligne suivante. [[File:Kaantotulkkaus.png|centre|vignette|upright=3|Compilation versus interprétation.]] Un défaut de l'interprétation est son cout en performance, qui est assez important. Aussi, l'interprétation stricte a évolué vers un hybride entre interprétation et compilation. L'idée est qu'une partie peu importante du code source est interprétée, alors que le code important est compilé juste avant d'être exécuté. Pour le dire autrement, le code source est partiellement compilé à la volée, juste avant son exécution. Aussi, on parle de '''compilation à la volée'''. Le terme en anglais est ''Just In Time compilation'', abrévié en JIT. En soi, le JIT est associé à de l'interprétation, les JIT sont en réalité des hybrides interpréteurs-compilateurs. Un point important est que compiler du code à la volée est assez lourd, cela a un cout en performance. Par contre, le code compilé s'exécute plus vite que du code interprété. Si on exécute le code compilé une seule fois, le cout de la compilation l'emporte sur le gain à l'exécution. Mais le code compilé est réutilisé autant de fois que nécessaire. Le gain à l'exécution est donc multiplié par le nombre d'exécutions, alors que le cout de la compilation est répartit sur plusieurs exécutions, il est amorti. Les portions du code sont choisies de manière à avoir un gain en performance maximal. L'idée générale est que le code exécuté très souvent est compilé, alors que du code exécuté pas souvent est interprété. Typiquement, les boucles les plus souvent exécutées sont compilées, le code exécuté une seule fois est interprété. ===Les langages intermédiaires=== Les compilateurs modernes passent par un '''langage intermédiaire''' pour faire la transformation en code machine. Le compilateur traduit le langage de haut niveau en langage intermédiaire, puis traduit le langage intermédiaire en code machine cible. Le langage intermédiaire est parfois appelé le '''''bytecode''''', et ce terme recouvre aussi le code écrit avec du ''bytecode''. L'interprétation aussi peut passer par l'intermédiaire d'un un ''bytecode'', ce n'est pas limité aux compilateurs proprement dit. {| |[[File:Intermediate representation scheme.png|centre|vignette|upright=1.5|Représentation intermédiaire d'un compilateur.]] |[[File:Code interpreter scheme.png|centre|vignette|upright=1.5|Fonctionnement d'un interpréteur.]] |} Faire ainsi a de nombreux avantages, le principal étant d'avoir un compilateur capable de traduire un langage de haut niveau vers plusieurs jeux d'instructions différents. Cela permet d'avoir, par exemple, un compilateur qui traduit du C soit en code machine x86, soit en code machine pour un CPU ARM, soit pour un CPU POWERPC, etc. [[File:Compiler design.svg|centre|vignette|upright=3|Compiler design]] Pour l'interprétation, l'usage d'un ''bytecode'' a d'autres avantages. Pour l'exploiter, il faut distribuer non pas le code source, mais le ''bytecode''. En clair, l'interpréteur ne prend pas en entrée le code source, mais du ''bytecode'' déjà compilé. La traduction est alors beaucoup plus simple, car le code source a déjà été partiellement compilé, par la traduction en ''bytecode''. Interpréter du code source est en effet assez compliqué : il faut effectuer des étapes d'analyse lexicale, sémantique, et bien d'autres. Avec du ''bytecode'', ces étapes ont été réalisées lors de la compilation du ''bytecode'', l'interpréteur a alors peut de choses à faire. L'interprétation du ''bytecode'' se fait instruction par instruction, au niveau du ''bytecode''. Une instruction du ''bytecode'' est traduite en une instruction machine équivalente, qui est exécutée. Si l'instruction est un peu complexe, il exécute une fonction/procédure qui fait la même chose. Pour résumer, le ''bytecode'' est ensuite traduit à la volée et exécuté instruction par instruction par un logiciel appelé l'interpréteur. [[File:Control table.png|centre|vignette|upright=2|Control table]] Le langage intermédiaire peut être vu comme l'assembleur d'un processeur, n'existe pas forcément dans la réalité, mais dont le jeu d'instruction est décrit en détail. Le processeur en question est appelé une '''machine abstraite''', ou encore une ''machine virtuelle''. Nous utiliserons le terme de machine abstraite dans ce qui suit. La machine abstraite n'est pas la même suivant que l'on cible la compilation ou l'interprétation/JIT. Pour l'interprétation/JIT, la machine abstraite est souvent une machine à pile, car cela simplifie grandement la traduction en code machine final. L'interprétation demande que la traduction du ''bytecode'' en code machine soit la plus simple possible. Et cela demande de prendre en compte pas mal de détails. Par exemple, les différents jeux d'instruction existants n'ont pas le même nombre de registres, ce qui pose problème lors de la traduction du ''bytecode''. Lors de la transformation en code machine, un algorithme d'allocation de registres se débrouille pour traduire le code intermédiaire en code qui utilise un nombre limité de registres. Pour améliorer cette allocation de registres, il y a deux solutions niveau ''bytecode'' : soit utiliser un nombre illimité de registres, soit utiliser une machine à pile. Pour les machines à pile, il existe un algorithme simple et rapide pour traduire un code écrit pour une machine à pile en un code écrit pour un processeur avec des registres, qui se débrouille pas trop mal pour allouer efficacement les registres. C'est un avantage assez important pour les langages interprétés. ==Les processeurs qui exécutent du bytecode== Le ''bytecode'' est un code machine, ce qui signifie qu'il peut en théorie s'exécuter sur un processeur qui implémente le jeu d’instruction associé. Si le jeu d'instruction d'un ''bytecode'' est souvent une description censée être fictive, elle n'en reste pas moins un jeu d'instruction et des caractéristiques précises, qu'on peut l'implémenter en matériel ! Néanmoins, tous les ''bytecode'' ne sont pas égaux de ce point de vue. Certains sont faciles à implémenter en matériel, d'autres non. Les ''bytecodes'' implémentés en matériel sont ceux dont les machines abstraites sont des machines à pile. Un premier exemple est celui des processeurs Pascal MicroEngine, qui exécutaient directement le bytecode du langage Pascal, le fameux UCSD P-code. Un second exemple est la '''machine SECD''', qui sert de langage intermédiaire pour certains compilateurs de langages fonctionnels. Elle a été implémentée en matériel par plusieurs équipes, notamment par les chercheurs de l'université de Calgary en 1989. Dans le même genre, quelques processeurs simples étaient capables d’exécuter directement le ''bytecode'' du langage FORTH. Le FORTH, un des premiers langages à pile de haut niveau, possède de nombreuses implémentations hardware et est un des rares langages de haut niveau dont le ''bytecode'' a été utilisé comme langage machine sur certains processeurs. Par exemple, on peut citer le processeur FC16, capable d’exécuter nativement du ''bytecode'' FORTH. ===Les processeurs Java=== Le cas le plus impressionnant est celui de la machine virtuelle Java, qui est un design de processeur comme un autre. En temps normal, le ''bytecode'' Java est compilé ou interprété, mais certains processeurs exécutaient du ''bytecode'' Java directement, sans interprétation ni compilation. Ils sont appelés des '''processeurs Java'''. On peut citer les processeurs ARM disposant de l'extension Jazelle, du JEMCore et du aJ-100 de aJile Systems, le picoJava II de Sun, et quelques autres. Il y a aussi eu quelques projets de recherche ou processeurs open source, comme Komodo, jamuth, le ''Java Optimized Processor'' et quelques autres projets du même genre. L'implémentation de la machine virtuelle Java n'était cependant pas complète. Quelques instructions complexes n'étaient pas gérées par le processeur et devaient être émulées en logiciel. Mais la grosse majorité des instructions l'était. Les instructions non-supportées étaient émulées avec la méthode ''trap and emulate'', à savoir qu'un opcode inconnu déclenchait une exception matérielle ''opcode invalide'', dont la routine pouvait émuler les instructions qui doivent l'être. Le processeur aJ-100 de aJile est capable d'exécuter la quasi-totalité des instructions de la JVM Java, seules deux instructions faisant exception. Les '''accélérateurs Java''' sont une solution intermédiaire, qui permet de traduire à la volée du ''bytecode'' Java en instructions RISC, exécutées sur un autre processeur. Il s'agit formellement de traduction binaire, mais je préfère en parler ici. L'idée est de combiner un processeur RISC avec un coprocesseur Java. Le coprocesseur lit le ''bytecode'' Java et le traduit en instructions RISC, ces dernières étant exécutées sur le processeur RISC. Le JA108 de Nozomi était un coprocesseur de ce genre, au même titre que le coprocesseur JSTAR de JEDI. L'extension Jazelle de certains CPU ARM fonctionnait vraisemblablement sur un principe similaire, sauf que le coprocesseur était intégré dans le processeur ARM, entre le cache d'instruction et le décodeur d'instructions ARM. L'intérêt des processeurs Java n'est pas qu'une question de performance. Certes, ils permettent d’exécuter plus vite les programmes compilés en ''bytecode'', comme des programmes Java pour la JVM Java. Mais l'intérêt est aussi de faire des économies de mémoire RAM et ROM. Je rappelle que la machine virtuelle Java est une machine à pile, ce qui fait que sa densité de code est excellente. Les programmes en ''bytecode'' Java sont donc très petits, et prennent peu de place en mémoire ROM. De plus, utiliser un processeur Java permet de se passer d'interpréteur ou de JIT Java, ce qui économise encore plus de mémoire. ===Transformer un processeur RISC en machine à pile=== La plupart des processeurs Java utilisent en interne une architecture à registre généraux, qui émule une machine à registre en microcode. Quelques registres sont réservés pour l'état de la machine virtuelle, avec au minimum un registre réservé pour le pointeur de pile, un autre pour un pointeur vers un ''pool'' de constantes, et quelques autres. D'autres registres servent à mémoriser le haut de la pile, à savoir le sommet de la pile et quelques opérandes situées juste en-dessous. Par exemple, le processeur picoJava II gardait les 64 opérandes au sommet de la pile dans 64 registres généraux, séparés des registres pour les pointeurs et l'état de la JVM. Une optimisation possible, envisagée sur les processeurs picoJava, était une sorte de macro-fusion sous stéroïde. L'idée était de fusionner certaines séries d'instructions Java en une seule instruction machine. Typiquement, pour faire une opération arithmétique, une machine à pile empile deux opérandes et exécute une opération, ce qui prend trois instructions machine. La spécification picoJava 1 a proposé d'envoyer directement la seconde opérande en entrée de l'ALU, ce qui fusionnait l'empilement de la seconde opérande et l'opération arithmétique. Le processeur picoJava II va plus loin et fusionne les trois instructions en une seule instruction RISC, agissant sur des registres. Mais la majorité des processeurs Java ne faisaient pas cela, car cela complexifie grandement le décodeur. Les processeurs ''bigfoot'' de DCT utilisaient un système différent pour émuler une pile avec des registres. Leur banc de registre de 64 bits était coupé en deux : les 16 premiers registres étaient utilisés pour la pile, les 48 suivants étaient utilisés pour autre chose. L'idée est que le registre 0 mémorise un emplacement vide, le registre 1 contient le sommet de la pile, le registre 2 l'opérande sous le sommet de la pile, et ainsi de suite. Les 15 premières opérandes de la pile sont donc mémorisées dans les registres. Les instructions écrivent leur résultat dans le registre adéquat, puis tous les registres sont décalés pour que le résultat passe dans le registre R1. Par exemple, les instructions PUSH chargent une opérande dans le registre 0, puis tous les registres sont décalés de un rang, pour que l'opérande chargée soit dans le registre 1. Le registre 0 devient alors le registre numéro 1, le registre 1 devient le 2, et ainsi de suite. Par contre, les opérations arithmétiques font autrement. Une instruction d'addition, par exemple, lit les opérandes dans le registre 1 et 2, puis place son résultat dans le registre 2 (les deux opérandes sont dépilées). Puis, les registres sont décalés d'un rang pour que ce registre 2 devienne le 1. Les registres ne sont en réalité pas décalés, il n'y a pas de transferts entre registres. A la place, les 16 premiers registres utilisaient une forme limitée de renommage de registres, similaire au banc de registre tournant utilisé sur les processeurs EPIC comme l'Itanium. Les numéros de registres sont renommés, décalés, à chaque fois qu'on empile ou dépile une opérande. Pour faire le renommage de registres, processeur contient un ''register stack counter'' de 4 bits, qui est incrémenté à chaque fois qu'on empile une donnée, et est décrémenté quand on dépile une donnée. Quand on veut accéder à un opérande, le numéro de registre architectural associé est traduit en numéro de registre physique, en additionnant le ''register stack counter'' (modulo 16, pour rester sur 4 bits). ==L'accélération de l'interprétation/JIT : le ''Thumb-EE'' d'ARM== Quelques processeurs ont ajouté des instructions pour faciliter le travail des interpréteurs/JIT. Par exemple, en 2005, ARM a ajouté le mode ''Thumb Execution Environment'', qui faisait cela. Il reprenait le jeu d'instruction compact ''thumb'' et ajoutait quelques instructions et modifiait le comportement d'instructions existantes. Le jeu d'instruction ''Thumb-EE'' a été ajouté sur les CPU ARM en 2005, mais a été déprécié en 2011, par manque d'utilité. Mais la tentative mérite qu'on s'attarde dessus. Les CPU de gamme M, déstinés à l'embarqué, n'ont jamais supporté ''Thumb-EE'', vu que de tels processeurs ne sont pas conçus pour exécuter du code interprété/JIT. Le mode ''Thumb-EE'' est un mode d'exécution, séparé de l'ARM normal. Ainsi, une instruction ''Thumb-EE'' et une instruction ARM peuvent avoir le même encodage en binaire, mais se comporter différemment. Le processeur est à tout moment soit en mode ''Thumb-EE'', soit en mode ARM normal, ce qui précise comment ces instructions doivent se comporter. Des instructions ont été ajoutées par ''Thumb-EE'', pour qu'il fasse son travail. L'entrée et la sortie du mode ''Thumb-EE'' se fait avec deux instructions : ENTERX et LEAVEX. Une fois en mode ''Thumb-EE'', le décodage des instructions ''thumb'' se fait avec les règles du ''Thumb-EE'', et non celles du ''thumb'' normal. Une première différence entre ''thumb'' normal et ''Thumb-EE'' est l'ajout de ce qui s'appelle un ''null check'' pour les instructions mémoire. Le ''null check'' est utilisé pour les instructions en mode d'adressage "base + indice" ou "Base + décalage", ou tout autre mode d'adressage avec un registre de base utilisés dans des calculs d'adresse. L'idée est de vérifier si l'adresse de base vaut zéro ou non. Si c'est le cas, le processeur lève une exception matérielle, qui est traitée par l'interprétation ou le compilateur JIT. Une telle situation est en effet signe d'une erreur d'adressage mémoire, qui doit être traitée par l'interpréteur. Typiquement, c'est signe qu'un pointeur n'a pas bien été initialisé, ce qui peut arriver avec du code interprété ou JIT. Les interpréteurs/JIT traitent généralement la situation en ajoutant un test avant toute instruction mémoire, pour vérifier si le pointeur accédé vaut zéro ou non. Avec ''Thumb-EE'', pas besoin d'ajouter les tests en questions : ils sont réalisés automatiquement lors de chaque instruction mémoire. : Les registres pour la pile, comme le pointeur de pile et le pointeur de ''frame'', sont aussi concernés. L'instruction CHKA vérifie que les accès à un tableau ne débordent pas en dehors du tableau en question. Par exemple, pour un tableau de 1024 éléments, elle vérifie si l'indice est compris entre 0 et 1024. Pour cela, elle vérifie si l'indice de tableau est bien dans l'intervalle adéquat et lève une exception matérielle si ce n'est pas le cas. Son utilité se comprend quand on sait que tous les accès à un tableau sont vérifiés par l'interpréteur. L'interpréteur est censé ajouter des instructions pour vérifier les indices, à savoir deux ou trois branchements. Avec CHKA, le test se fait en une seule instruction. D'autres modifications mineures de l'encodage des instructions sont aussi présentes, ainsi que des modifications pour les instructions LOAD/STORE. Précisément, dans les modes "base + indice" et "base + décalage", le décalage et l'indice subissent maintenant des décalages. L'idée est de simplifier le calcul d'adresse. De quoi économiser quelques instructions lors des calculs d'adresse, ce qui facilite le travail de l'interpréteur/JIT. Et des instructions LOAD/STORE précises voient leur comportement modifié pour faciliter la gestion de la pile de Java ou l'exécution des méthodes locales. Pour faciliter le travail de l'interpréteur, deux instructions HBP et HBLP ont été ajoutées. Elles branchent vers une fonction qui gère les exceptions logicielles (une fonctionnalité présente dans de nombreux langages, comme Java). La première effectue un branchement vers la fonction en question, la seconde sauvegarde l'adresse de retour avant de faire ce branchement. Les deux peuvent brancher vers 256 fonctions pré-déterminées, sans avoir à présenter leur adresse. ==La traduction binaire accélérée par le matériel== Plus haut, nous avons surtout parlé des compilateurs et des interpréteurs. Cependant, nous devons aussi parler de la '''traduction binaire'''. Elle traduit un programme écrit dans un code machine vers un autre code machine. Par exemple, elle traduit un programme compilé pour un CPU x86 vers un code machine ARM. En général, le système d'exploitation est généralement compilé pour le jeu d'instruction natif, mais il exécute des applications prévues pour le x86. Les applications sont traduites par le système d'exploitation, avant d'exécuter le code traduit. : Nous parlerons dans la suite de code machine source et de code machine cible, pour parler respectivement du code à traduire et du code obtenu après traduction. Idem avec d'autres termes comme architecture cible/source, ou jeu d'instruction cible/source. La traduction binaire est surtout utilisée pour des questions d'émulation ou de compatibilité. Par exemple, lorsque les Macintosh sont passés de processeurs Power PC vers des processeurs x86, le système d'exploitation Mac OS utilisait la traduction binaire pour convertir les anciennes applications Power PC vers du code x86. Les utilisateurs n'y ont vu que du feu. Le système de traduction binaire était appelé Rosetta 1. Par la suite, lors de la transition de processeurs x86 vers des processeurs Apple, Rosetta 2 a vu le jour. La traduction binaire a surtout été utilisée pour traduire du code x86 vers un autre jeu d'instruction. Il faut dire que le x86 est le jeu d'instruction dominant. De nombreuses entreprises ont eu pour ambition de briser l'hégémonie du x86 sur PC, en remplaçant le x86 par un jeu d'instruction plus performant, tout en gardant une compatibilité maximale. La traduction binaire était la seule solution pratique. Elle était le plus souvent intégralement réalisée en logiciel, comme c'était le cas sur les architectures Itanium avec le ''IA-32 Execution Layer''. ===Les généralités sur la traduction binaire assistée en matériel=== L'architecture source est presque tout le temps une architecture CISC, assez ancienne, qu'on souhaite émuler. Le choix de l'architecture cible se porte souvent sur une architecture VLIW, fort différente de l'architecture source, et ce pour obtenir une économie de matériel conséquente. Pour s'exécuter rapidement, le code traduit doit exploiter le parallélisme d'instruction, à savoir exécuter plusieurs instructions en même temps dans des unités de calcul séparées. Il est possible d'utiliser un processeur superscalaire avec exécution dans le désordre pour cela, mais au prix d'un cout important en transistors. Alors qu'en utilisant un CPU VLIW, c'est le traducteur binaire qui fait tout le travail d'extraction du parallélisme d'instruction. Et après tout, quitte à avoir un traducteur binaire, autant lui refiler le boulot d'optimisation. Compiler du code à la volée est certes assez lourd, mais qu'il existe des algorithmes efficaces pour regrouper des instructions indépendantes dans une seule instruction VLIW. Au passage, le processeur VLIW a plus de registres que le processeur source. Cela permet de faire du renommage de registres directement en logiciel. Le traducteur binaire n'hésite pas à changer les noms de registres entre instructions source et instruction VLIW cible, afin de supprimer des dépendances de données. En conséquence, cela ouvre des opportunités de parallélisme, qui sont exploitées lors du regroupement des opérations en instructions VLIW. Vous pourriez penser que le choix d'un CPU VLIW pour émuler du CISC est tout sauf optimal. Vous devez penser que la traduction binaire est d'autant plus simple que l'architecture source et cibles sont semblables. Dans les faits, ce n'est pas tellement traduire les instructions qui pose problème, mais plus la gestion du registre d'état, des exceptions matérielles, de la mémoire virtuelle, la différence entre gros-boutisme et petit-boutisme, de même que des différences d'adressage pour les périphériques. Le premier problème est la gestion des conditions, notamment en présence d'un registre d'état. En pratique, la traduction binaire s'utilise pour traduire du code CISC vers du code RISC ou VLIW. Les architectures source ont donc un registre d'état, qui est mis à jour non seulement par des instructions de test, mais aussi des instructions arithmétiques. L'architecture cible n'a elle pas de registre d'état, mais des registres à prédicats. La traduction de l'un vers l'autre est alors quelque peu compliquée. Et elle est d'autant plus compliquée que le registre d'état est la source de dépendances d'instruction implicites, qui réduisent les performances. Par exemple, si une instruction arithmétique modifie le registre d'état, cela peut impacter l'exécution d'une instruction ultérieure, qui lit ce registre d'état. Pour éliminer ces fausses dépendances, le traducteur binaire doit renommer ce registre en logiciel et détecter les dépendances utiles. Et le compilateur doit gérer les cas où il y a beaucoup de distance entre les deux, voire les cas où l'instruction dépendante n'a pas encore été analysée par le compilateur. Un autre problème est lié à la gestion des exceptions matérielles, et précisément des exceptions précises. Pour rappel, une instruction VLIW regroupe plusieurs opérations, opérations qui correspondraient à une instruction machine sur un CPU pas VLIW. En conséquence, plusieurs instructions du langage machine "source" sont regroupées en une seule instruction VLIW. Et il faut tenir compte du cas où une instruction source lève une exception. Dans le code VLIW, cela signifie qu'une opération lève une exception, et il faut annuler partiellement l'instruction VLIW associée. Et par partiellement, on veut dire que seules les opérations suivantes dans l'ordre du programme source doivent être annulées, puis ré-exécutées. Et c'est un sacré casse-tête ! Les processeurs VLIW utilisent comme solution le mécanisme d'exceptions différées des processeurs EPIC, vu il y a quelques chapitres. Pour résumer, le code est exécuté par blocs d'instruction, délimités par des branchements ou tout autre limite/barrière pertinente dans le code. Un bloc de code est compilé en deux versions : une version rapide sans exceptions matérielles, une version lente qui gère les exceptions précises. La version rapide s'exécute sans exécuter les exceptions. Cependant, les exceptions matérielles sont enregistrées, pour être prises en compte à la toute fin du bloc de code. La version rapide est exécutée en premier et elle mémorise si une exception matérielle a eu lieu. Puis une instruction vérifie si une exception a eu lieu et décide quoi faire. Si aucune exception n'a eu lieu, elle passe à la suite du programme, les résultats du bloc de code sont définitivement acceptés. Mais si une exception a eu lieu, tout est annulé. Le processeur est remis dans l'état initial, puis le code est ré-exécuté instruction par instruction de manière à gérer l'exception correctement. ===La traduction par pré-décodage=== Une première solution serait de faire la traduction binaire dans le cache d'instruction. Lors d'un défaut de cache, le code chargé depuis la RAM est traduit en code machine cible. Et c'est ce code machine cible qui est mémorisé dans le cache d'instruction et exécuté par le processeur. La technique marche sur le papier et n'est qu'une amélioration des techniques de pré-décodage vues il y a quelques chapitres. Cependant, le code obtenu est une traduction assez basique, qui n'incorpore pas d'optimisations dignes de ce nom. De plus, elle gère mal le cas où la taille du code source et cible sont potentiellement très différentes. Concrètement, elle est surtout utile pour traduire du code d'un processeur RISC vers un autre processeur RISC, dont les tailles d'instructions sont similaires. Elle ne permet pas d'émuler plusieurs jeux d'instructions différents, le cout en matériel (un circuit de pré-décodage par jeu d'instruction) serait trop important. ===Le projet DAISY d'IBM=== De très rares processeurs étaient conçus pour accélérer cette traduction binaire, afin de garder de bonnes performances. Les premiers à avoir étudié l'idée étaient IBM, avec leur projet DAISY (''Dynamically Architected Instruction Set from Yorktown''). Le projet de base était de convertir à la volée du code compilé pour des CPU Power PC, vers du code VLIW. Les chercheurs d'IBM avaient développé un algorithme de traduction dynamique efficace, ainsi qu'un processeur VLIW disposant d'optimisations spécifiques à la traduction binaire dynamique, à la volée. De nombreuses idées de ce projet ont été reprises ou re-découvertes par la société Transmetta, avec ses processeurs Crusoe et Efficieon, puis par NVIDIA avec son projet Denver. Les ingénieurs de ce projet ont étudié la possibilité d'émuler plusieurs jeux d'instructions différents sur un même processeur, notamment le s390 d'IBM et le x86. En théorie, cela demande juste d'avoir plusieurs programmes de traduction binaire : un pour le x86, un autre pour le s390, éventuellement un autre pour le Power PC. Mais cela ne s'est pas concrétisé. Mais les ingénieurs ont étudié quelle pourrait être l'architecture VLIW idéale pour ça. Par exemple, il fallait des additions 3-opérandes pour simplifier les calculs d'adresse. Une difficulté était la gestion du registre d'état, dont les bits ne sont pas mis à jour de la même manière sur le x86 et le s390 ou le Power PC. La différence entre gros-boutisme et petit-boutisme était aussi un problème, de même que des différences d'adressage pour les périphériques. Pour que la traduction binaire soit efficace, le processeur VLIW intègre diverses optimisations, comme les exceptions différées et les branchements multi-voies. La principale est la suivante : seul le code des boucles ou fonctions exécutées fréquemment est traduit en code VLIW, le reste du code est interprété. En effet, traduire du code binaire prend plus de temps qu'une simple interprétation. Pour du code qui ne sera exécutée qu'une seule fois, il est plus rapide d'utiliser l'interprétation. Par contre, pour du code exécuté beaucoup de fois, le cout de la traduction binaire est amorti, dilué sur N exécutions, compensé par le gain en temps d'exécution de ce code traduit. Au final, cela permet de ne traduire que le code qui le mérite. La détection du code fréquemment exécuté est réalisée dans le cœur VLIW. L'unité de branchement mémorise les derniers branchements rencontrés et le nombre de fois qu'ils ont été exécutés. Si ils ont été exécutés un certain nombre de fois, l'unité de branchement lève une exception matérielle, qui invoque le traducteur binaire. La routine de cette exception est le traducteur binaire proprement dit. En clair, la traduction binaire est démarrée quand le processeur détecte qu'une fonction a été exécutée plus de N fois, via une exception matérielle dédiée. La mesure du nombre d'exécution d'un branchement se fait dans l'unité de calcul dédiée aux branchements, avec l'aide d'une sorte de ''branch adress buffer'' modifié. Pour rappel, le ''branch adress buffer'' mémorise le ''Program counter'' de chaque branchement récemment rencontré. Ici, chaque entrée du ''branch adress buffer'' est associée à un compteur incrémenté à chaque exécution du branchement. ===Les processeurs Crusoe et Efficieon de Transmetta=== Les processeurs Crusoe Et Efficeron sont deux processeurs VLIW produits par la société Transmetta, une société californienne rachetée par NVIDIA. Ils étaient conçus pour exécuter spécifiquement des programmes x86, système d'exploitation inclus. L'idée derrière ce projet était d'exécuter du code x86 sans que les applications, ni même le système d'exploitation et le BIOS soient au courant ! Les processeurs Transmetta étaient en réalité conçus pour exécuter un programme unique, le ''Code Morphing Software'' (CMS), qui traduisait le code x86 en code VLIW. Transmetta ne voulait pas que son processeur VLIW soit exploité directement, sans le CMS. Il n'avait pas rendu public de compilateur pour traduire du C vers du code VLIW, il n'avait pas donné la documentation du jeu d'instruction VLIW. Des efforts de rétro-ingénieurie, documentés sur le site realworldtech, ont cependant permis de comprendre comment étaient encodées les instructions du processeur Crusoe. Comme pour le projet Daisy, le ''Code Morphing Software'' utilisait à la fois interprétation et traduction binaire, selon les besoins. Les instructions sont interprétées lors de leurs premières exécutions, mais le CMS bascule sur de la traduction en code VLIW après un certain nombre d'exécution. Ainsi, les boucles souvent exécutées sont traduites en code VLIW, alors que le reste du code est interprété. Le choix du code à traduire est le fait du CMS, il utilise des heuristiques complexes pour, qui ne sont pas connues dans le détail. Le ''Code Morphing Software'' est mémorisé dans une EEPROM, ce qui en fait un ''firmware'' situé en dessous du BIOS. Le CMS démarre ensuite le BIOS, qui lui-même démarre le système d'exploitation, qui lui-même démarre les pilotes de périphériques et les programmes. Le CMS se réserve les 16 premiers mébioctets de l'espace d'adressage. L'EEPROM du CMS est mappée dedans, mais n'en utilise que la moitié. Le reste est utilisé comme cache, pour mémoriser le code VLIW traduit par le CMS. Les deux processeurs intégraient aussi deux mémoires SRAM, utilisées par le CMS, appelées la ''local program memory'' (LPM) et la ''local data memory'' (LDM). La première contient du code qui gére les interruptions, la mémoire virtuelle, les exceptions matérielles, les problèmes d'alignement mémoire, et quelques fonctions très fréquemment utilisées par le CMS. La seconde est de la mémoire RAM utilisée par ces fonctions de la ''local program memory''. Les mémoires caches L1 et L2 sont séparées de ces deux mémoires. Le processeur Crusoe et Efficieon étaient des processeurs VLIW très simples. Ils n'avaient même pas de MMU, ni de fonctionnalités importantes sur les CPU x86. Pour Crusoe, les instructions VLIW étaient encodées sur 64 ou 128 bits, et regroupaient entre 2 à 4 opérations. Les instructions LOAD/STORE ne supportaient pas d'adressage indicé, juste de l'adressage indirect à registre. Crusoe avait 5 unités de calcul : deux ALU entières, une unité LOAD/STORE, une unité de branchement, une FPU. Efficieon doublait le nombre d'ALU entière et d'unité LOAD/STORE, les instructions VLIW passaient à 256 bits. Le processeur Crusoe contient 160 registres, dont 64 registres généraux, 32 registres flottants. Les registres généraux font 32 bits, ce qui est cohérent avec le fait que les CPU x86 de l'époque étaient des processeurs 32 bits. Quant aux registres flottants, ils faisaient 80 bits, ce qui colle avec la taille des registres flottants de la FPU x87 utilisée à l'époque. Sur les 64 registres généraux, seuls 48 étaient réellement utilisable pour mémoriser des opérandes. Une partie des registres généraux étaient utilisés pour la gestion de la pile, d'autres pour mémoriser l'état du CPU x86 émulé, un registre était un registre zéro non accesible en écriture. La gestion des exceptions est optimisée avec un système d'exceptions différées, le même que celui décrit plus haut. Le processeur utilise un système similaire, pour effectuer des lectures anticipées, qui a été expliqué dans le chapitre sur les processeurs VLIW/EPIC. Sauf qu'il s'agit là d'une optimisation pour gagner en performances en général, pas une aide utile pour gérer des exceptions précises ou toute autre subtilité de la traduction binaire. Pour gérer les exceptions différées et les lectures anticipées, le processeur mémorise l'état du processeur avant de démarrer un bloc de code. Pour cela, le processeur contient des ''shadow registers'', 48 registres entiers et 16 registres flottants. Vous remarquerez qu'il y a autant de registres entiers que de ''shadow registers'' entiers. Les registres entiers/flottants sont mémorisés dans les ''shadow registers'' avant d'exécuter un bloc de code, afin que le processeur puisse revenir à l'état de base. Une autre fonctionnalité liée est le ''gated store buffer''. Le principe est d'éviter toute écriture en mémoire RAM, tant que le code VLIW n'a pas émis d'instruction ''commit''. L'idée est que les écritures sont conservées dans la ''store queue'' de l'unité LOAD/STORE. Pour rappel, la ''store queue'' met en attente les écritures soit tant que les écritures ne sont pas terminées, soit tant que la RAM est occupée. L'idée est que les écritures sont accumulées dans la ''store queue'' tant que l'instruction ''commit'' n'a pas eu lieu. Le ''store queue'' est "déversé dans la RAM" seulement quand l'instruction ''commit'' s'exécute. ===Les CPU NVIDIA Denver=== Nvidia a tenté quelque chose de similaire avec son projet Denver. NVIDIA a racheté l'entreprise Transmetta, ses brevets, et a tenté de refaire la même chose. Le projet initial était de traduire du code x86 vers un jeu d'instruction VLIW propriétaire, comme l'a fait Transmetta. Mais NVIDIA n'a pas réussi à acquérir la licence du jeu d'instruction x86 et s'est rabattu sur le jeu d'instruction ARM. Le processeur né de ce projet est le Tegra K1-64 CPU. Il s'agit d'un processeur composé d'un cœur VLIW, couplé à deux décodeurs d'instructions ARM. Les deux décodeurs ARM traduisent une paire d'instructions ARM consécutives en instructions VLIW, pour les exécuter sur le cœur VLIW. Le CPU peut fonctionner selon deux modes : ARM et VLIW. En somme, le processeur supporte deux jeux d'instruction : le jeu d'instruction ARM, et un jeu d'instruction VLIW. En mode ARM, les instructions ARM sont chargées depuis le cache, traduites en VLIW par les décodeurs ARM, puis exécutées par le cœur VLIW. En mode optimisé, le CPU exécute des instructions VLIW chargées depuis le cache d'instruction, les décodeurs ne sont pas utilisés. : Le CPU supporte en réalité deux jeux d'instruction ARM : ARM8 et 7. Mais ils correspondent au même mode pour le CPU. Un point important est que le processeur peut changer très rapidement de mode, en à peine quelques cycles d'horloges. On n'est clairement pas dans le cas des CPU x86, qui mettent des plombes pour passer du mode 32 à 64 bits et inversement. La commutation est tellement rapide qu'on peut considérer que le processeur supporte deux jeux d'instruction simultanément. Et le support simultané de deux jeux d'instruction facilite l'implémentation de la traduction binaire. Encore une fois, inutile de traduire du code qui ne sera exécutée qu'une seule fois, mieux vaut l'exécuter directement dans le mode ARM. Surtout que le processeur supporte un mode ARM qui permet d'exécuter du code ARM sans perte de performance. Par contre, le code fréquemment utilisé, notamment dans des boucles critiques, est traduit en VLIW. Au final, les performances sont optimisées, en limitant le travail du logiciel traducteur binaire. La détection du code fréquemment exécuté se fait différemment que pour les processeurs Transmetta et Daisy. L'unité de branchement ne détecte pas directement le code beaucoup exécuté, même si elle a un rôle à jouer. A la place, la détection est réalisée en logiciel, par un ''thread'' dédié, qui s'exécute sur un cœur séparé. Il détecte le code fréquemment utilisé en regardent les branchements exécutés récemment. Pour cela, le processeur mémorise l'historique des branchements pris récemment et copie cette historique dans une mémoire tampon dédiée, partagée entre tous les cœurs. Vu qu'elle est partagée, le ''thread'' de détection a accès à l'historique sans pertes de performances, seule la copie de l'historique dans ce tampon a un cout en performance, pas son partage. Le code VLIW obtenu après traduction binaire est mémorisé dans la mémoire RAM, dans une portion spécialement réservée pour. Elle fait 128 mébioctets, et est appelée le '''cache d'optimisation''' par NVIDIA. Le cache d'optimisation est protégé en écriture et seul le traducteur binaire peut écrire dedans. Au passage, le traducteur binaire est du code VLIW, ce qui fait qu'il est placé dans le cache d'optimisation. Le processeur détecte automatiquement quand une fonction pour laquelle le code compilé équivalent est disponible. Pour cela, l'unité de chargement contient une table de correspondance entre l'adresse de la fonction ARM, et l'adresse de son équivalent compilé. Quand une fonction est appelée, l'unité de chargement regarde l'adresse de destination du branchement. Si l'adresse est dans cette table, elle récupère l'adresse de la fonction compilée et branche vers celle-ci. Le processeur dispose d'un cache d'instruction de 128 kibioctets, ce qui est très important, mais nécessaire vu la taille des instructions VLIW. Le coeur VLIW dispose de ses propres décodeurs, d'un ''scoreboard'', de 7 unités de calcul et de registres. Pour les unités de calcul, il a deux FPU, deux ALU, une unité de branchement, et deux unités LOAD-STORE faisant aussi ALU. Un point important est que le CPU dispose de 64 registres entiers et de 64 registres flottants. C'est deux fois plus que supporte le jeu d'instruction ARM. En mode ARM, seule la moitié des registres est utilisée. Mais en mode optimisé, le traducteur binaire utilise bien les 64 registres, grâce à une sorte de renommage de registres logicielle. Pour simplifier la traduction binaire, le processeur supporte les techniques vues précédemment. Le processeur intègre un ''gated store buffer'' similaire à celui des processeurs Transmetta. Les techniques d'exceptions différées sont aussi supportées, comme sur les processeurs Transmetta. Il a aussi une unité de préchargement décrite par NVIDIA comme agressive, avec support du préchargement de type anticipé (''runahead''). ===L'émulation de la mémoire virtuelle=== Pour finir, il faut parler de la mémoire virtuelle avec la traduction binaire. Sur le processeur source, le processeur gère à la fois des adresses physiques et virtuelles et sa MMU fait la conversion entre les deux. Sur le processeur VLIW, la MMU est simulée par le programme de traduction binaire, partiellement ou totalement. L'implémentation exacte varie suivant que l'on parle du projet Daisy, des processeurs Transmetta ou de Denver. Sur les processeurs Transmetta, il n'y a pas de MMU ni de mémoire virtuelle. A la place, le programme de traduction binaire émule la mémoire virtuelle du processeur source. Le processeur VLIW ne gère que des adresses physiques, rien d'autre. L'espace d'adressage physique du processeur VLIW a la même taille que celui du processeur émulé, ici des processeurs x86. Il est vraisemblable que les adresses physiques utilisées par le processeur x86 sont les mêmes que celles du processeur VLIW. Sur le projet Daisy, le processeur VLIW gère la mémoire virtuelle via pagination, comme les processeurs émulés. Ce qui fait qu'il gère un espace d'adressage virtuel et un espace d'adressage physique. Pour éviter toute confusion, nous parlerons d'adresse physique/virtuelle VLIW pour les adresses physiques/virtuelles du processeur VLIW, d'adresse virtuelle/physique source pour celle du jeu d'instruction traduit, à savoir du Power PC ou du s390, éventuellement de l'x86. Il faut alors faire le lien entre adresses physiques source et adresses virtuelles VLIW. Pour cela, rien de plus simple : il y a correspondance parfaite. L'adresse physique source numéro N correspond à l'adresse virtuelle VLIW numéro N. Cependant, cela signifie que tout l'espace d'adressage virtuel VLIW serait utilisé par le code à traduire. En réalité, il faut ajouter de la place pour le code traduit et le programme de traduction binaire. La conséquence est que l'espace d'adressage virtuel VLIW est plus large que l'espace d'adressage source. L'espace d'adressage virtuel VLIW est découpé en trois sections : une section pour le code Power PC à traduire, une autre pour le code traduit en VLIW, et une dernière réservée au traducteur binaire. Le programme de traduction est placé dans une ROM mappée en mémoire dans la seconde section. Le reste de la seconde section est réservé à une mémoire RAM utilisée par le programme de traduction. [[File:Mémoire virtuelle sur le projet Daisy.png|centre|vignette|upright=1.5|Mémoire virtuelle sur le projet Daisy]] Il faut noter que chaque page physique source, une fois traduite, correspond à N pages virtuelles VLIW. La raison est que le code traduit prend plus de place que le code machine originel. En conséquence, on doit utiliser une page finale plus grande. Pour le reste, des pages contiguës en mémoire physique source sont elles aussi contiguës en mémoire virtuelle VLIW. Le calcul d'adresse est donc simple : l'adresse physique source est multipliée par N, puis on ajoute l'adresse de base à laquelle commence la section pour le code traduit. ==La traduction binaire sur le Burroughs B1700== Nous venons de voir comment accélérer en matériel la traduction binaire, avec des techniques bien spécifiques. Le Burroughs B1700 a procédé autrement et il est intéressant d'étudier en détail son architecture. Son architecture est décrite dans le livre "Interpreting Machines : Architecture and Programming of the Bl700/Bl800 Series", écrit par Elliott I. Organick et James A. Hinds. Une partie de ce qui vfa suivre est un très court résumé de ce livre. ===Le Burroughs B1700 : le mal-nommé ''split-level control store''=== Le Burroughs B1700 avait plusieurs modèles : le B1726, le B1710 et le B1800. Ils se distinguaient par la manière dont le code machine source était traduit par le traducteur binaire. Dans ce qui suit, nous parlerons de traducteur binaire pour parler soit d'un interpréteur pour un langage de programmation de haut niveau, soit pour un logiciel de traduction binaire. Le B1710 plaquait le traducteur binaire en mémoire RAM. Le B1800 faisait pareil, mais avait ajouté un cache dédié au code du traducteur binaire, afin d'améliorer les performances. Le B1726 avait un traducteur binaire scindé en deux : une partie était en mémoire RAM, l'autre était dans un ''local store'' dédié. Le processeur peut exécuter le traducteur binaire soit depuis le ''local store'', soit depuis la mémoire RAM avec une pénalité en termes de performances. La documentation appelle cela un '''''split-level control store''''', mais le terme est trompeur : la documentation qualifie de microcode le langage machine du Burroughs B1700, ce qui est à l'origine de beaucoup de confusions. Une autre source de confusion est que ce langage machine a quelques propriétés qu'on pourrait attendre d'un microcode, mais certaines sont manquantes. Par exemple, il peut adresser les registres d’interfaçage mémoire, mais c'est parce que ce sont des registres architecturaux sur ce processeur. Ses instructions n'encodent pas de signaux de commande, par contre, elles font 16 bits et adressent les opérandes de manière implicite. Le ''local store'' mémorisait au maximum 2048 instructions machine, ce qui fait que les 2048 premières "adresses" correspondaient au ''local store''. Les suivantes étaient des adresses de mémoire RAM qui contenaient le reste du traducteur binaire. En pratique, l'adresse qui séparait les deux était configurable ! On pouvait limiter la taille du ''local store'' à un multiple de 32 instructions ! Les adresses restantes étaient alors réattribuées à la RAM. Par exemple, on pouvait attribuer 64 adresses au ''local store'', les 2048 - 64 adresses restantes étaient alors attribuées à la mémoire RAM. Un registre limite, nommé TDPM, indiquait quelle était l'adresse de démarcation entre les deux. Il contenait 4 bits, dont la valeur était multipliée par 32, pour obtenir l'adresse limite. Le programmeur devait découper le traducteur binaire en segments, certains étant placé dans le ''local store'', les autres étant en mémoire RAM. Mieux que ça, le processeur autorisait d'utiliser la technique de l'''overlaying'' pour le microcode ! Il y avait même une fonction OVERLAY dédiée pour ! OVERLAY avait besoin de trois opérandes : la taille du segment à copier, son adresse source en mémoire RAM, son adresse de destination dans la micro-SRAM. L'interpréteur réservait un "segment" en mémoire RAM, pour les données, dont le processeur connaissait la position. En-dehors de ce segment, il n'y a que des instructions machines à traduire, et le traducteur binaire. Le CPU incorporait pour cela deux registres, un pour l'adresse de base du segment de données, un autre pour son adresse de fin. Il incorporait une protection mémoire limitée sur ce segment, à savoir que seul l'interpréteur pouvait lire ou écrire dans ce segment. ===Une architecture bit-adressable=== Le CPU Burroughs B1700 était '''bit-adressable'''. En clair, il pouvait adresser la mémoire bit par bit, si nécessaire. Un avantage est que cela facilitait la traduction binaire des instructions machine, mais nous détaillerons cela plus bas. Un autre avantage est que cela permettait d'émuler des architectures dont la taille des registres/''byte''/mots était très différente. Et cet avantage mérite quelques explications immédiates. Le processeur était un CPU 24 bits, mais il pouvait émuler des processeurs 16 bits, 8 bits, 4 bits, 1 bit, 9 bits, ou toute autre valeur. Pour cela, le processeur lisait des mots de 24 bits et utilisant un ''barrel shifter'' pour sélectionner les bits adéquats. Par exemple, pour un accès de 16 bits, la donnée lue en mémoire était masquée de manière à ne garder que les 16 bits de poids faible. Le ''barrel shifter''/circuit de masquage était placé directement avant le bus mémoire, sur le trajet des données/instructions. Le ''barrel shifter'' était commandé par un registre ''Field Unit'', qui précisait quelle était la taille des données à charger. Pour communiquer avec la mémoire, il avait trois registres séparés : READ, WRITE, MAR. READ contient la donnée lue lors d'une lecture, WRITE est pour une donnée à écrire lors d’une écriture, MAR contient l'adresse à lire ou en cours de lecture. Il s'agit de registres d’interfaçage mémoire. Le fait que ces registres d’interfaçage mémoire et IO soit adressable est peu commun, et c'est plus quelque chose qu'on attend d'un microcode que d'instructions machines. Mais il faut bien comprendre que les registres d’interfaçage mémoire ont été promus au rang de registres architecturaux, sur ce processeur. De même, pour communiquer avec les entrées-sorties, il disposait de deux registres CMND et DATA : CMND pour envoyer une commande à une entrée-sortie, DATA pour lire ou écrire une donnée. Il y avait aussi un registre U, qui mémorisait une donnée lue depuis le lecteur de cassette. ===L'ALU de taille variable=== Les Burroughs B1700 intégrait une pile d'adresse de retour, capable de mémoriser 16 adresses de 24 bits. Il disposait de 4 registres généraux, nommés X, Y, T et L, tous de 24 bits. Un point important est que le registre T avait des possibilités d’extraction spécifiques. Il était possible d'extrait N bits, placés n'importe où dans ce registre, et de les copier dans un autre registre. Nous verrons pourquoi cette possibilité est intéressante et ne sert que pour le registre T. Ils avaient une '''ALU de taille variable'''. En clair, l'ALU pouvait faire des calculs sur un nombre de bits compris entre 0 et 24. Pour cela, les calculs étaient fait avec une ALU de 24 bits, puis les bits de poids fort inutiles étaient masqués. L'ALU de 24 bits pouvait masquer les bits de poids fort du résultat, mais avec des limitations. Elle gérait des opérandes et résultats de 0, 4, 8, 12, 16, 20 et 24 bits. Pour avoir un réglage plus fin, une ALU 4 bits séparée permettait de corriger le résultat. Les registres X et Y étaient reliés à une ALU de 24 bits capable d'effectuer des opérations arithmétiques basiques. Fait étonnant, l'ALU faisait tous les calculs en même temps et fournissait ses résultats dans 7 registres : un registre SUM pour l'addition, un registre DIFFERENCE pour la soustraction, et un registre par opération bit à bit. Les registres pour les opérations bit à bit sont : CMPX et CMPY pour l'opération NOT sur chaque opérande, XANY pour le ET bit à bit, XEOY pour le XOR, XORY pour le OU logique. Les additions et soustractions pouvaient se faire sur des opérandes codés en binaire ou en BCD, suivant le code opération utilisé. A cela, il fallait ajouter deux registres de "résultat" MSKX et MSKY. Ils fournissaient un masque dépendant de la taille du résultat en bits. C'est ce registre qui permettait de gérer des données de 0, 4, 8, 12, 16, 20, et 24 bits, alors que l'ALU faisait 24 bits. Le masque dans ce registre pouvait servir d'opérande dans une opération de masquage ultérieure, pour corriger le résultat. L'ALU prenait en opérande les registres X et Y, mais aussi un registre CYF pour la retenue entrante. En sortie, il fournissait aussi deux retenue sortantes : une pour l'addition et une autre pour la soustraction. Elles étaient mémorisées dans les registres CYL et CYD, respectivement. L'ALU fournissait aussi 12 conditions, qui alimentait un registre d'état. Le registre d'état était segmenté en trois registres appelés XYST, XYCN et BICN. Les registres X et Y sont des registres pour les opérandes, mais il y a aussi deux autres registres de 24 bits. Il s'agit des registres T et L, qui sont des registres généraux. Les registres T et L étaient découpés en 6 sous-registres de 4 bits, nommés LA LB LC LD LE et LF pour le registre L, et TA TB TC TD TE et TF pour le registre T. Ils étaient adressables comme n'importe quel registre. Le processeur contenait de nombreux autres registres de 4 bits, dont les sous-registres de T et L. En tout, il y a 27 registres de 4 bits adressables ! Et le processeur disposait d'une ALU 4 bits pour manipuler ces registres de 4 bits ! Elle est capable de faire des MOV entre registres de 4 bits, des opérations bit à bit, et peut aussi tester si un bit vaut 0 ou 1. La dernière possibilité est très utile pour implémenter les branchements. De plus, cela permettait de faire des opérations de masquage, notamment pour appliquer un masquage supplémentaire à celui de l'ALU. Par exemple, pour gérer des opérations sur 14 bits, on effectue une opération sur 16 bits avec l'ALU, et on masque les 2 bits manquants avec l'ALU 4 bits. La capacité d'extraction du registre T pouvait aussi être utilisé pour ça. ===Le code machine du Burroughs B1700=== Le processeur utilise des instructions de 16 bits de long, qui sont copiées dans registre d'instruction M de 16 bits. Fait intéressant, il était possible de faire un OU logique entre l'instruction lue et un registre du processeur, le résultat étant mémorisé dans le registre M. Cela permettait de faire du code automodifiant, pour émuler des modes d'adressage complexe. Cela permettait par exemple de remplacer une adresse dans une micro-opération. L'adresse est alors surimposée avec un OU dans le champ d'adresse, alors que le reste de l'instruction n'est pas modifié. Afin de gérer les interruptions, ces dernières étaient partiellement détectées en logiciel. Une interruption doit être prise en compte à la toute fin de l'exécution d'une instruction source, quand celle-ci a finit son travail. Mais vu qu'ici, les instructions sont émulées avec des séries d'instructions cibles, il y a un problème : il est possible d'interrompre une instruction émulée en plein milieu. Pour éviter cela, le processeur ne déclenche pas les interruptions matérielles immédiatement, elles sont mises en attente. Une instruction dédiée vérifie si une interruption est en attente, et l'exécute alors cas échéant. L'instruction en question est utilisée en dernier, dans la suite d'instruction qui émule l'instruction source. Le ''program counter'' est appelé le registre A. Les instructions du processeur font 16 bits de long, et elles sont alignées en RAM sur 16 bits, ce qui fait que les 4 bits de poids fort de ce registre valent 0, seuls les 14 bits de poids fort sont utiles. Les adresses mentionnées plus haut, qui adressent les micro-opérations en mémoire RAM, sont des adresses de mots de 16 bits, pas les adresses de bit individuels. ===La traduction binaire est aidée par des registres et instructions dédiées=== Les instructions à décoder n'ont pas de longueur fixe, à savoir qu'elles peuvent faire 16 bit avec tel jeu d'instruction, 8 bits avec un autre, 32 bits sur un autre, etc. Elles peuvent aussi être de longueur fixe ou variable ! Le B1700 intégrait donc un support des instructions de longueur variable, qui était "détourné" pour gérer des jeux d'instruction différents. Notons que vu que l'architecture est bit-adressable, les contraintes d'alignement des instructions disparaissent, ce qui a des avantages qu'on va détailler dans ce qui suit ! L'implémentation des instructions de taille variable ajoutait un registre avant le registre d'instruction. Le registre d'instruction proprement dit était un registre M de 24 bits. Il était précédé par un registre T, dans lequel l'instruction était assemblée, en combinant plusieurs morceaux. Si l'instruction est codée est codée sur moins de 24 bits, le registre T se contente de masquer les bits en trop. Si l'instruction chargée dépasser 24 bits, le chargement se faisait en plusieurs passes, avec plusieurs lectures en mémoire. Dans ce qui suit, on suppose que l'instruction est composée de '''champs''' : l'opcode, les opérandes, les adresses absolues, les constantes immédiates, les numéros de registre, etc. L'idée est que l'instruction est chargée dans le registre T, puis découpée en champs, qui sont copiés dans les registres du processeur ou envoyés au microcode. Je rappelle que le registre T est relié à un ''barrel shifter'', différent au précédent, qui permettait de masquer, décaler et extraire une donnée de ce registre. Et il y a la possibilité d'extraire une suite de bit de ce registre T, pour la copier dans un autre registre. Par exemple, prenons une instruction composée d'un opcode, d'une constante immédiate et d'une adresse immédiate. L'opcode est lu et interprété par le microcode. Le microcode extrait la constante immédiate du registre T et la copie dans le registre X. Ensuite, l'adresse est extraite et envoyée au registre FA, une lecture est démarrée, et l'opérande est copiée dans le registre Y. Enfin, l'instruction est exécutée par le microcode. Le tout prend plusieurs micro-opérations : une première pour lire l'opcode, une seconde pour lire la première opérande, une troisième pour lire la seconde opérande, una quatrième pour tout exécuter. Notons que suivant la taille des champs, on peut en charger plusieurs à la fois. Par exemple, imaginons qu'on ait chargé 24 bits, contenant un opcode de 8 bits et une constante immédiate de 16 bits : pas besoin de faire deux accès mémoire, le registre T est juste découpé en un opcode et une constante. Le registre T est fait pour ce genre d'extraction. ===Le chargement d'un champ=== L'architecture est bit-adressable car elle gére des champs qui ne font pas forcément 8 ou 16 bits. Par exemple, certains jeux d'instructions ont des opcodes codés sur 6 ou 7 bits, voire 9-10 bits. Par exemple, prenons un opcode de 7 bits, suivi par une adresse de 12 bits et une constante immédiate de 15 bits : aucun de ces champs ne sera aligné sur 8/16/24 bits. Utiliser une architecture bit-adressable règle le problème d'alignement. Et en général, les contraintes d'alignement des instructions disparaissent ! Quand on charge une instruction, peut importe qu'elle soit ou non alignée sur 24 bits, elle sera placée dans les bits de poids faible, il suffira de masquer les bits de poids fort inutiles. Par exemple, si on charge une instruction de 16 bits, les 16 bits seront copiés dans les 16 bits de poids faible du registre T, il suffira de masquer l'octet de poids fort. Pas besoin de faire de décalages pour gérer des instructions mal alignées... Pour charger une instruction, il faut connaitre plusieurs informations : l'adresse de son premier bit, sa taille. Pour cela, le B1700 contient deux registres, nommés ''Field Adress'' (FA) et ''Field Length'' (FL), qui indiquent l'adresse du premier bit et la taille de l'instruction à charger. Le premier fait 24 bits, ce qui permet de gérer des adresses de 24 bits. N'oubliez pas que le processeur est bit-adressable, ce qui permet d'adresser 16 Mébi-bits, soit 2 mébioctets. Le registre pour la taille fait lui 16 bits, ce qui permet de gérer des instructions de 65536 bits, soit 8192 octets ! Leur nom commence par ''field'', car le processeur peut gérer des instructions de taille variable et/ou plus longues que 24 bits. Dans ce cas, le chargement de l'instruction se fait champ par champ. Pour donner un exemple, prenons une instruction composée de 20 champs de 12 bits chacun. Les champs sont chargés un par un dans le registre T. Le registre FA est alors initialisé avec l'adresse de l'instruction, il est incrémenté de 12 à chaque cycle. Le registre FL est initialisé avec la taille de l'instruction, soit 20 * 12 = 240 bits, et il est décrémenté de 16 à chaque cycle. Précisons cependant que la taille d'un champ peut être changée d'un cycle à l'autre. Par exemple, le processeur peut charger un opcode de 8 bits, l'interpréter, détecter que l'instruction demande ensuite de charger une adresse de 16 bits. Dans ce cas, le registre FL est immédiatement altéré pour charger 16 bits, au lieu des 8 bits de l'opcode immédiatement précédent. ==Les jeux d'instructions dédiés à un langage de programmation== De rares processeurs sont conçus pour un langage de programmation en particulier. On appelle ces processeurs, conçus pour des besoins particuliers, des '''processeurs dédiés'''. Par exemple, les fameux ''Burrough E-mode'' B5000/B6000/B7000 étaient spécialement conçus pour exécuter de l'ALGOL-60. Leurs cousins B2000/B3000/B4000 étaient eux conçus pour le COBOL. Des langages fonctionnels ont aussi eu droit à leurs processeurs dédiés. Le prolog en est un bel exemple, avec les superordinateurs de 5ème génération qui lui étaient dédié. On peut aussi citer les machines LISP, dédiés au langage LISP, qui datent des années 1970. Elles étaient capables d’exécuter certaines fonctions de base du langage directement dans leurs circuits : elles possédaient notamment un ''garbage collector'' câblé dans ses circuits ainsi que des instructions machines supportant un typage déterminé à l’exécution. Les processeurs dédiés ont eu leur heure de gloire au début de l'informatique, à une époque où les langages de haut niveau venaient d'être inventés. À cette époque, les compilateurs n'étaient pas performants et ne savaient pas bien optimiser le code machine. Il était alors rationnel, pour l'époque, de rapprocher le code machine cible et le langage de programmation de haut niveau. De nombreuses architectures dédiés ont ainsi été inventées, avant que les concepteurs se rendent compte des défauts de cette approche. Les défauts en question ne sont pas nombreux, mais assez simples à comprendre. Premièrement, elles sont très rapides pour un langage de programmation en particulier, mais sont assez mauvaises pour les autres, d'où un problème de "compatibilité". Ajoutons à cela que les langages de programmation peuvent évoluer, devenir de moins en moins populaires/utilisés, ce qui rend la création d'architectures généralistes plus pertinente. Enfin, les architectures dédiées sont évidemment des processeurs CISC, pour implémenter les nombreuses fonctionnalités des langages évolués. Et les défauts des CISC sont assez rédhibitoires à l'heure actuelle. À l'heure actuelle, les algorithmes des compilateurs se sont améliorés et savent nettement mieux utiliser le matériel. Ils produisent du code machine efficace, ce qui rend les architecture dédiées bien moins intéressantes. Si on ajoute les défauts de ces architectures dédiées, par étonnant que les architectures dédiées aient presque disparues. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'accélération matérielle de la virtualisation | prevText=L'accélération matérielle de la virtualisation | next=Le matériel réseau | nextText=Le matériel réseau }} </noinclude> hn0ap1o92axij0u1qo1wt2pwl0ee5hc 764051 764050 2026-04-19T20:33:25Z Mewtow 31375 /* La traduction binaire est aidée par des registres et instructions dédiées */ 764051 wikitext text/x-wiki De nos jours, la majorité des programmeurs programment dans des langages de haut niveau. Il est très rare pour eux d'avoir à utiliser de l'assembleur, et encore moins un langage machine. Les programmes écrits dans un langage de haut niveau sont traduits en langage machine par un logiciel appelé le compilateur. [[File:Compilation.PNG|centre|vignette|upright=2.5|Compilateur : principe.]] Il se trouve que quelques rares processeurs sont conçus pour faciliter le travail du compilateur. Et ce sont ces processeurs que nous allons voir dans ce qui suit. Ils représentent une catégorie de jeux d'instruction à part, qui n'a pas vraiment de nom. ==La compilation et l’interprétation== Un compilateur traduit donc du code source, écrit dans un langage de haut niveau, vers du langage machine. La traduction est rarement directe. En général, le compilateur traduit le code source en assembleur, qui est lui-même traduit en langage machine. L'assembleur est une représentation textuelle du langage machine alors que le code machine est du binaire exécutable par le processeur. La traduction se fait donc en deux étapes, la '''compilation''' proprement dite et l''''assemblage''', réalisées respectivement par un compilateur et un assembleur. Il faut parfois rajouter une troisième phase d'édition des liens, que nous passons volontairement sous silence. {| |[[File:Code C.png|vignette|200px|Le code source, ici en C.]] |[[File:Assem.png|vignette|250px|Le code assembleur.]] |[[File:Binary file - hello world (C programming).png|vignette|150px|Le code machine.]] |} ===L'interprétation et la compilation à la volée=== Une alternative à la compilation est l''''interprétation''', qui transforme le code source en code machine à la volée. Avec l'interprétation, le code source est passé à un logiciel appelé l''''interpréteur''', qui exécute le code source ligne de code par ligne de code. Une ligne de code est traduite en code machine, qui est exécutée, puis l'interpréteur passe à la ligne suivante. [[File:Kaantotulkkaus.png|centre|vignette|upright=3|Compilation versus interprétation.]] Un défaut de l'interprétation est son cout en performance, qui est assez important. Aussi, l'interprétation stricte a évolué vers un hybride entre interprétation et compilation. L'idée est qu'une partie peu importante du code source est interprétée, alors que le code important est compilé juste avant d'être exécuté. Pour le dire autrement, le code source est partiellement compilé à la volée, juste avant son exécution. Aussi, on parle de '''compilation à la volée'''. Le terme en anglais est ''Just In Time compilation'', abrévié en JIT. En soi, le JIT est associé à de l'interprétation, les JIT sont en réalité des hybrides interpréteurs-compilateurs. Un point important est que compiler du code à la volée est assez lourd, cela a un cout en performance. Par contre, le code compilé s'exécute plus vite que du code interprété. Si on exécute le code compilé une seule fois, le cout de la compilation l'emporte sur le gain à l'exécution. Mais le code compilé est réutilisé autant de fois que nécessaire. Le gain à l'exécution est donc multiplié par le nombre d'exécutions, alors que le cout de la compilation est répartit sur plusieurs exécutions, il est amorti. Les portions du code sont choisies de manière à avoir un gain en performance maximal. L'idée générale est que le code exécuté très souvent est compilé, alors que du code exécuté pas souvent est interprété. Typiquement, les boucles les plus souvent exécutées sont compilées, le code exécuté une seule fois est interprété. ===Les langages intermédiaires=== Les compilateurs modernes passent par un '''langage intermédiaire''' pour faire la transformation en code machine. Le compilateur traduit le langage de haut niveau en langage intermédiaire, puis traduit le langage intermédiaire en code machine cible. Le langage intermédiaire est parfois appelé le '''''bytecode''''', et ce terme recouvre aussi le code écrit avec du ''bytecode''. L'interprétation aussi peut passer par l'intermédiaire d'un un ''bytecode'', ce n'est pas limité aux compilateurs proprement dit. {| |[[File:Intermediate representation scheme.png|centre|vignette|upright=1.5|Représentation intermédiaire d'un compilateur.]] |[[File:Code interpreter scheme.png|centre|vignette|upright=1.5|Fonctionnement d'un interpréteur.]] |} Faire ainsi a de nombreux avantages, le principal étant d'avoir un compilateur capable de traduire un langage de haut niveau vers plusieurs jeux d'instructions différents. Cela permet d'avoir, par exemple, un compilateur qui traduit du C soit en code machine x86, soit en code machine pour un CPU ARM, soit pour un CPU POWERPC, etc. [[File:Compiler design.svg|centre|vignette|upright=3|Compiler design]] Pour l'interprétation, l'usage d'un ''bytecode'' a d'autres avantages. Pour l'exploiter, il faut distribuer non pas le code source, mais le ''bytecode''. En clair, l'interpréteur ne prend pas en entrée le code source, mais du ''bytecode'' déjà compilé. La traduction est alors beaucoup plus simple, car le code source a déjà été partiellement compilé, par la traduction en ''bytecode''. Interpréter du code source est en effet assez compliqué : il faut effectuer des étapes d'analyse lexicale, sémantique, et bien d'autres. Avec du ''bytecode'', ces étapes ont été réalisées lors de la compilation du ''bytecode'', l'interpréteur a alors peut de choses à faire. L'interprétation du ''bytecode'' se fait instruction par instruction, au niveau du ''bytecode''. Une instruction du ''bytecode'' est traduite en une instruction machine équivalente, qui est exécutée. Si l'instruction est un peu complexe, il exécute une fonction/procédure qui fait la même chose. Pour résumer, le ''bytecode'' est ensuite traduit à la volée et exécuté instruction par instruction par un logiciel appelé l'interpréteur. [[File:Control table.png|centre|vignette|upright=2|Control table]] Le langage intermédiaire peut être vu comme l'assembleur d'un processeur, n'existe pas forcément dans la réalité, mais dont le jeu d'instruction est décrit en détail. Le processeur en question est appelé une '''machine abstraite''', ou encore une ''machine virtuelle''. Nous utiliserons le terme de machine abstraite dans ce qui suit. La machine abstraite n'est pas la même suivant que l'on cible la compilation ou l'interprétation/JIT. Pour l'interprétation/JIT, la machine abstraite est souvent une machine à pile, car cela simplifie grandement la traduction en code machine final. L'interprétation demande que la traduction du ''bytecode'' en code machine soit la plus simple possible. Et cela demande de prendre en compte pas mal de détails. Par exemple, les différents jeux d'instruction existants n'ont pas le même nombre de registres, ce qui pose problème lors de la traduction du ''bytecode''. Lors de la transformation en code machine, un algorithme d'allocation de registres se débrouille pour traduire le code intermédiaire en code qui utilise un nombre limité de registres. Pour améliorer cette allocation de registres, il y a deux solutions niveau ''bytecode'' : soit utiliser un nombre illimité de registres, soit utiliser une machine à pile. Pour les machines à pile, il existe un algorithme simple et rapide pour traduire un code écrit pour une machine à pile en un code écrit pour un processeur avec des registres, qui se débrouille pas trop mal pour allouer efficacement les registres. C'est un avantage assez important pour les langages interprétés. ==Les processeurs qui exécutent du bytecode== Le ''bytecode'' est un code machine, ce qui signifie qu'il peut en théorie s'exécuter sur un processeur qui implémente le jeu d’instruction associé. Si le jeu d'instruction d'un ''bytecode'' est souvent une description censée être fictive, elle n'en reste pas moins un jeu d'instruction et des caractéristiques précises, qu'on peut l'implémenter en matériel ! Néanmoins, tous les ''bytecode'' ne sont pas égaux de ce point de vue. Certains sont faciles à implémenter en matériel, d'autres non. Les ''bytecodes'' implémentés en matériel sont ceux dont les machines abstraites sont des machines à pile. Un premier exemple est celui des processeurs Pascal MicroEngine, qui exécutaient directement le bytecode du langage Pascal, le fameux UCSD P-code. Un second exemple est la '''machine SECD''', qui sert de langage intermédiaire pour certains compilateurs de langages fonctionnels. Elle a été implémentée en matériel par plusieurs équipes, notamment par les chercheurs de l'université de Calgary en 1989. Dans le même genre, quelques processeurs simples étaient capables d’exécuter directement le ''bytecode'' du langage FORTH. Le FORTH, un des premiers langages à pile de haut niveau, possède de nombreuses implémentations hardware et est un des rares langages de haut niveau dont le ''bytecode'' a été utilisé comme langage machine sur certains processeurs. Par exemple, on peut citer le processeur FC16, capable d’exécuter nativement du ''bytecode'' FORTH. ===Les processeurs Java=== Le cas le plus impressionnant est celui de la machine virtuelle Java, qui est un design de processeur comme un autre. En temps normal, le ''bytecode'' Java est compilé ou interprété, mais certains processeurs exécutaient du ''bytecode'' Java directement, sans interprétation ni compilation. Ils sont appelés des '''processeurs Java'''. On peut citer les processeurs ARM disposant de l'extension Jazelle, du JEMCore et du aJ-100 de aJile Systems, le picoJava II de Sun, et quelques autres. Il y a aussi eu quelques projets de recherche ou processeurs open source, comme Komodo, jamuth, le ''Java Optimized Processor'' et quelques autres projets du même genre. L'implémentation de la machine virtuelle Java n'était cependant pas complète. Quelques instructions complexes n'étaient pas gérées par le processeur et devaient être émulées en logiciel. Mais la grosse majorité des instructions l'était. Les instructions non-supportées étaient émulées avec la méthode ''trap and emulate'', à savoir qu'un opcode inconnu déclenchait une exception matérielle ''opcode invalide'', dont la routine pouvait émuler les instructions qui doivent l'être. Le processeur aJ-100 de aJile est capable d'exécuter la quasi-totalité des instructions de la JVM Java, seules deux instructions faisant exception. Les '''accélérateurs Java''' sont une solution intermédiaire, qui permet de traduire à la volée du ''bytecode'' Java en instructions RISC, exécutées sur un autre processeur. Il s'agit formellement de traduction binaire, mais je préfère en parler ici. L'idée est de combiner un processeur RISC avec un coprocesseur Java. Le coprocesseur lit le ''bytecode'' Java et le traduit en instructions RISC, ces dernières étant exécutées sur le processeur RISC. Le JA108 de Nozomi était un coprocesseur de ce genre, au même titre que le coprocesseur JSTAR de JEDI. L'extension Jazelle de certains CPU ARM fonctionnait vraisemblablement sur un principe similaire, sauf que le coprocesseur était intégré dans le processeur ARM, entre le cache d'instruction et le décodeur d'instructions ARM. L'intérêt des processeurs Java n'est pas qu'une question de performance. Certes, ils permettent d’exécuter plus vite les programmes compilés en ''bytecode'', comme des programmes Java pour la JVM Java. Mais l'intérêt est aussi de faire des économies de mémoire RAM et ROM. Je rappelle que la machine virtuelle Java est une machine à pile, ce qui fait que sa densité de code est excellente. Les programmes en ''bytecode'' Java sont donc très petits, et prennent peu de place en mémoire ROM. De plus, utiliser un processeur Java permet de se passer d'interpréteur ou de JIT Java, ce qui économise encore plus de mémoire. ===Transformer un processeur RISC en machine à pile=== La plupart des processeurs Java utilisent en interne une architecture à registre généraux, qui émule une machine à registre en microcode. Quelques registres sont réservés pour l'état de la machine virtuelle, avec au minimum un registre réservé pour le pointeur de pile, un autre pour un pointeur vers un ''pool'' de constantes, et quelques autres. D'autres registres servent à mémoriser le haut de la pile, à savoir le sommet de la pile et quelques opérandes situées juste en-dessous. Par exemple, le processeur picoJava II gardait les 64 opérandes au sommet de la pile dans 64 registres généraux, séparés des registres pour les pointeurs et l'état de la JVM. Une optimisation possible, envisagée sur les processeurs picoJava, était une sorte de macro-fusion sous stéroïde. L'idée était de fusionner certaines séries d'instructions Java en une seule instruction machine. Typiquement, pour faire une opération arithmétique, une machine à pile empile deux opérandes et exécute une opération, ce qui prend trois instructions machine. La spécification picoJava 1 a proposé d'envoyer directement la seconde opérande en entrée de l'ALU, ce qui fusionnait l'empilement de la seconde opérande et l'opération arithmétique. Le processeur picoJava II va plus loin et fusionne les trois instructions en une seule instruction RISC, agissant sur des registres. Mais la majorité des processeurs Java ne faisaient pas cela, car cela complexifie grandement le décodeur. Les processeurs ''bigfoot'' de DCT utilisaient un système différent pour émuler une pile avec des registres. Leur banc de registre de 64 bits était coupé en deux : les 16 premiers registres étaient utilisés pour la pile, les 48 suivants étaient utilisés pour autre chose. L'idée est que le registre 0 mémorise un emplacement vide, le registre 1 contient le sommet de la pile, le registre 2 l'opérande sous le sommet de la pile, et ainsi de suite. Les 15 premières opérandes de la pile sont donc mémorisées dans les registres. Les instructions écrivent leur résultat dans le registre adéquat, puis tous les registres sont décalés pour que le résultat passe dans le registre R1. Par exemple, les instructions PUSH chargent une opérande dans le registre 0, puis tous les registres sont décalés de un rang, pour que l'opérande chargée soit dans le registre 1. Le registre 0 devient alors le registre numéro 1, le registre 1 devient le 2, et ainsi de suite. Par contre, les opérations arithmétiques font autrement. Une instruction d'addition, par exemple, lit les opérandes dans le registre 1 et 2, puis place son résultat dans le registre 2 (les deux opérandes sont dépilées). Puis, les registres sont décalés d'un rang pour que ce registre 2 devienne le 1. Les registres ne sont en réalité pas décalés, il n'y a pas de transferts entre registres. A la place, les 16 premiers registres utilisaient une forme limitée de renommage de registres, similaire au banc de registre tournant utilisé sur les processeurs EPIC comme l'Itanium. Les numéros de registres sont renommés, décalés, à chaque fois qu'on empile ou dépile une opérande. Pour faire le renommage de registres, processeur contient un ''register stack counter'' de 4 bits, qui est incrémenté à chaque fois qu'on empile une donnée, et est décrémenté quand on dépile une donnée. Quand on veut accéder à un opérande, le numéro de registre architectural associé est traduit en numéro de registre physique, en additionnant le ''register stack counter'' (modulo 16, pour rester sur 4 bits). ==L'accélération de l'interprétation/JIT : le ''Thumb-EE'' d'ARM== Quelques processeurs ont ajouté des instructions pour faciliter le travail des interpréteurs/JIT. Par exemple, en 2005, ARM a ajouté le mode ''Thumb Execution Environment'', qui faisait cela. Il reprenait le jeu d'instruction compact ''thumb'' et ajoutait quelques instructions et modifiait le comportement d'instructions existantes. Le jeu d'instruction ''Thumb-EE'' a été ajouté sur les CPU ARM en 2005, mais a été déprécié en 2011, par manque d'utilité. Mais la tentative mérite qu'on s'attarde dessus. Les CPU de gamme M, déstinés à l'embarqué, n'ont jamais supporté ''Thumb-EE'', vu que de tels processeurs ne sont pas conçus pour exécuter du code interprété/JIT. Le mode ''Thumb-EE'' est un mode d'exécution, séparé de l'ARM normal. Ainsi, une instruction ''Thumb-EE'' et une instruction ARM peuvent avoir le même encodage en binaire, mais se comporter différemment. Le processeur est à tout moment soit en mode ''Thumb-EE'', soit en mode ARM normal, ce qui précise comment ces instructions doivent se comporter. Des instructions ont été ajoutées par ''Thumb-EE'', pour qu'il fasse son travail. L'entrée et la sortie du mode ''Thumb-EE'' se fait avec deux instructions : ENTERX et LEAVEX. Une fois en mode ''Thumb-EE'', le décodage des instructions ''thumb'' se fait avec les règles du ''Thumb-EE'', et non celles du ''thumb'' normal. Une première différence entre ''thumb'' normal et ''Thumb-EE'' est l'ajout de ce qui s'appelle un ''null check'' pour les instructions mémoire. Le ''null check'' est utilisé pour les instructions en mode d'adressage "base + indice" ou "Base + décalage", ou tout autre mode d'adressage avec un registre de base utilisés dans des calculs d'adresse. L'idée est de vérifier si l'adresse de base vaut zéro ou non. Si c'est le cas, le processeur lève une exception matérielle, qui est traitée par l'interprétation ou le compilateur JIT. Une telle situation est en effet signe d'une erreur d'adressage mémoire, qui doit être traitée par l'interpréteur. Typiquement, c'est signe qu'un pointeur n'a pas bien été initialisé, ce qui peut arriver avec du code interprété ou JIT. Les interpréteurs/JIT traitent généralement la situation en ajoutant un test avant toute instruction mémoire, pour vérifier si le pointeur accédé vaut zéro ou non. Avec ''Thumb-EE'', pas besoin d'ajouter les tests en questions : ils sont réalisés automatiquement lors de chaque instruction mémoire. : Les registres pour la pile, comme le pointeur de pile et le pointeur de ''frame'', sont aussi concernés. L'instruction CHKA vérifie que les accès à un tableau ne débordent pas en dehors du tableau en question. Par exemple, pour un tableau de 1024 éléments, elle vérifie si l'indice est compris entre 0 et 1024. Pour cela, elle vérifie si l'indice de tableau est bien dans l'intervalle adéquat et lève une exception matérielle si ce n'est pas le cas. Son utilité se comprend quand on sait que tous les accès à un tableau sont vérifiés par l'interpréteur. L'interpréteur est censé ajouter des instructions pour vérifier les indices, à savoir deux ou trois branchements. Avec CHKA, le test se fait en une seule instruction. D'autres modifications mineures de l'encodage des instructions sont aussi présentes, ainsi que des modifications pour les instructions LOAD/STORE. Précisément, dans les modes "base + indice" et "base + décalage", le décalage et l'indice subissent maintenant des décalages. L'idée est de simplifier le calcul d'adresse. De quoi économiser quelques instructions lors des calculs d'adresse, ce qui facilite le travail de l'interpréteur/JIT. Et des instructions LOAD/STORE précises voient leur comportement modifié pour faciliter la gestion de la pile de Java ou l'exécution des méthodes locales. Pour faciliter le travail de l'interpréteur, deux instructions HBP et HBLP ont été ajoutées. Elles branchent vers une fonction qui gère les exceptions logicielles (une fonctionnalité présente dans de nombreux langages, comme Java). La première effectue un branchement vers la fonction en question, la seconde sauvegarde l'adresse de retour avant de faire ce branchement. Les deux peuvent brancher vers 256 fonctions pré-déterminées, sans avoir à présenter leur adresse. ==La traduction binaire accélérée par le matériel== Plus haut, nous avons surtout parlé des compilateurs et des interpréteurs. Cependant, nous devons aussi parler de la '''traduction binaire'''. Elle traduit un programme écrit dans un code machine vers un autre code machine. Par exemple, elle traduit un programme compilé pour un CPU x86 vers un code machine ARM. En général, le système d'exploitation est généralement compilé pour le jeu d'instruction natif, mais il exécute des applications prévues pour le x86. Les applications sont traduites par le système d'exploitation, avant d'exécuter le code traduit. : Nous parlerons dans la suite de code machine source et de code machine cible, pour parler respectivement du code à traduire et du code obtenu après traduction. Idem avec d'autres termes comme architecture cible/source, ou jeu d'instruction cible/source. La traduction binaire est surtout utilisée pour des questions d'émulation ou de compatibilité. Par exemple, lorsque les Macintosh sont passés de processeurs Power PC vers des processeurs x86, le système d'exploitation Mac OS utilisait la traduction binaire pour convertir les anciennes applications Power PC vers du code x86. Les utilisateurs n'y ont vu que du feu. Le système de traduction binaire était appelé Rosetta 1. Par la suite, lors de la transition de processeurs x86 vers des processeurs Apple, Rosetta 2 a vu le jour. La traduction binaire a surtout été utilisée pour traduire du code x86 vers un autre jeu d'instruction. Il faut dire que le x86 est le jeu d'instruction dominant. De nombreuses entreprises ont eu pour ambition de briser l'hégémonie du x86 sur PC, en remplaçant le x86 par un jeu d'instruction plus performant, tout en gardant une compatibilité maximale. La traduction binaire était la seule solution pratique. Elle était le plus souvent intégralement réalisée en logiciel, comme c'était le cas sur les architectures Itanium avec le ''IA-32 Execution Layer''. ===Les généralités sur la traduction binaire assistée en matériel=== L'architecture source est presque tout le temps une architecture CISC, assez ancienne, qu'on souhaite émuler. Le choix de l'architecture cible se porte souvent sur une architecture VLIW, fort différente de l'architecture source, et ce pour obtenir une économie de matériel conséquente. Pour s'exécuter rapidement, le code traduit doit exploiter le parallélisme d'instruction, à savoir exécuter plusieurs instructions en même temps dans des unités de calcul séparées. Il est possible d'utiliser un processeur superscalaire avec exécution dans le désordre pour cela, mais au prix d'un cout important en transistors. Alors qu'en utilisant un CPU VLIW, c'est le traducteur binaire qui fait tout le travail d'extraction du parallélisme d'instruction. Et après tout, quitte à avoir un traducteur binaire, autant lui refiler le boulot d'optimisation. Compiler du code à la volée est certes assez lourd, mais qu'il existe des algorithmes efficaces pour regrouper des instructions indépendantes dans une seule instruction VLIW. Au passage, le processeur VLIW a plus de registres que le processeur source. Cela permet de faire du renommage de registres directement en logiciel. Le traducteur binaire n'hésite pas à changer les noms de registres entre instructions source et instruction VLIW cible, afin de supprimer des dépendances de données. En conséquence, cela ouvre des opportunités de parallélisme, qui sont exploitées lors du regroupement des opérations en instructions VLIW. Vous pourriez penser que le choix d'un CPU VLIW pour émuler du CISC est tout sauf optimal. Vous devez penser que la traduction binaire est d'autant plus simple que l'architecture source et cibles sont semblables. Dans les faits, ce n'est pas tellement traduire les instructions qui pose problème, mais plus la gestion du registre d'état, des exceptions matérielles, de la mémoire virtuelle, la différence entre gros-boutisme et petit-boutisme, de même que des différences d'adressage pour les périphériques. Le premier problème est la gestion des conditions, notamment en présence d'un registre d'état. En pratique, la traduction binaire s'utilise pour traduire du code CISC vers du code RISC ou VLIW. Les architectures source ont donc un registre d'état, qui est mis à jour non seulement par des instructions de test, mais aussi des instructions arithmétiques. L'architecture cible n'a elle pas de registre d'état, mais des registres à prédicats. La traduction de l'un vers l'autre est alors quelque peu compliquée. Et elle est d'autant plus compliquée que le registre d'état est la source de dépendances d'instruction implicites, qui réduisent les performances. Par exemple, si une instruction arithmétique modifie le registre d'état, cela peut impacter l'exécution d'une instruction ultérieure, qui lit ce registre d'état. Pour éliminer ces fausses dépendances, le traducteur binaire doit renommer ce registre en logiciel et détecter les dépendances utiles. Et le compilateur doit gérer les cas où il y a beaucoup de distance entre les deux, voire les cas où l'instruction dépendante n'a pas encore été analysée par le compilateur. Un autre problème est lié à la gestion des exceptions matérielles, et précisément des exceptions précises. Pour rappel, une instruction VLIW regroupe plusieurs opérations, opérations qui correspondraient à une instruction machine sur un CPU pas VLIW. En conséquence, plusieurs instructions du langage machine "source" sont regroupées en une seule instruction VLIW. Et il faut tenir compte du cas où une instruction source lève une exception. Dans le code VLIW, cela signifie qu'une opération lève une exception, et il faut annuler partiellement l'instruction VLIW associée. Et par partiellement, on veut dire que seules les opérations suivantes dans l'ordre du programme source doivent être annulées, puis ré-exécutées. Et c'est un sacré casse-tête ! Les processeurs VLIW utilisent comme solution le mécanisme d'exceptions différées des processeurs EPIC, vu il y a quelques chapitres. Pour résumer, le code est exécuté par blocs d'instruction, délimités par des branchements ou tout autre limite/barrière pertinente dans le code. Un bloc de code est compilé en deux versions : une version rapide sans exceptions matérielles, une version lente qui gère les exceptions précises. La version rapide s'exécute sans exécuter les exceptions. Cependant, les exceptions matérielles sont enregistrées, pour être prises en compte à la toute fin du bloc de code. La version rapide est exécutée en premier et elle mémorise si une exception matérielle a eu lieu. Puis une instruction vérifie si une exception a eu lieu et décide quoi faire. Si aucune exception n'a eu lieu, elle passe à la suite du programme, les résultats du bloc de code sont définitivement acceptés. Mais si une exception a eu lieu, tout est annulé. Le processeur est remis dans l'état initial, puis le code est ré-exécuté instruction par instruction de manière à gérer l'exception correctement. ===La traduction par pré-décodage=== Une première solution serait de faire la traduction binaire dans le cache d'instruction. Lors d'un défaut de cache, le code chargé depuis la RAM est traduit en code machine cible. Et c'est ce code machine cible qui est mémorisé dans le cache d'instruction et exécuté par le processeur. La technique marche sur le papier et n'est qu'une amélioration des techniques de pré-décodage vues il y a quelques chapitres. Cependant, le code obtenu est une traduction assez basique, qui n'incorpore pas d'optimisations dignes de ce nom. De plus, elle gère mal le cas où la taille du code source et cible sont potentiellement très différentes. Concrètement, elle est surtout utile pour traduire du code d'un processeur RISC vers un autre processeur RISC, dont les tailles d'instructions sont similaires. Elle ne permet pas d'émuler plusieurs jeux d'instructions différents, le cout en matériel (un circuit de pré-décodage par jeu d'instruction) serait trop important. ===Le projet DAISY d'IBM=== De très rares processeurs étaient conçus pour accélérer cette traduction binaire, afin de garder de bonnes performances. Les premiers à avoir étudié l'idée étaient IBM, avec leur projet DAISY (''Dynamically Architected Instruction Set from Yorktown''). Le projet de base était de convertir à la volée du code compilé pour des CPU Power PC, vers du code VLIW. Les chercheurs d'IBM avaient développé un algorithme de traduction dynamique efficace, ainsi qu'un processeur VLIW disposant d'optimisations spécifiques à la traduction binaire dynamique, à la volée. De nombreuses idées de ce projet ont été reprises ou re-découvertes par la société Transmetta, avec ses processeurs Crusoe et Efficieon, puis par NVIDIA avec son projet Denver. Les ingénieurs de ce projet ont étudié la possibilité d'émuler plusieurs jeux d'instructions différents sur un même processeur, notamment le s390 d'IBM et le x86. En théorie, cela demande juste d'avoir plusieurs programmes de traduction binaire : un pour le x86, un autre pour le s390, éventuellement un autre pour le Power PC. Mais cela ne s'est pas concrétisé. Mais les ingénieurs ont étudié quelle pourrait être l'architecture VLIW idéale pour ça. Par exemple, il fallait des additions 3-opérandes pour simplifier les calculs d'adresse. Une difficulté était la gestion du registre d'état, dont les bits ne sont pas mis à jour de la même manière sur le x86 et le s390 ou le Power PC. La différence entre gros-boutisme et petit-boutisme était aussi un problème, de même que des différences d'adressage pour les périphériques. Pour que la traduction binaire soit efficace, le processeur VLIW intègre diverses optimisations, comme les exceptions différées et les branchements multi-voies. La principale est la suivante : seul le code des boucles ou fonctions exécutées fréquemment est traduit en code VLIW, le reste du code est interprété. En effet, traduire du code binaire prend plus de temps qu'une simple interprétation. Pour du code qui ne sera exécutée qu'une seule fois, il est plus rapide d'utiliser l'interprétation. Par contre, pour du code exécuté beaucoup de fois, le cout de la traduction binaire est amorti, dilué sur N exécutions, compensé par le gain en temps d'exécution de ce code traduit. Au final, cela permet de ne traduire que le code qui le mérite. La détection du code fréquemment exécuté est réalisée dans le cœur VLIW. L'unité de branchement mémorise les derniers branchements rencontrés et le nombre de fois qu'ils ont été exécutés. Si ils ont été exécutés un certain nombre de fois, l'unité de branchement lève une exception matérielle, qui invoque le traducteur binaire. La routine de cette exception est le traducteur binaire proprement dit. En clair, la traduction binaire est démarrée quand le processeur détecte qu'une fonction a été exécutée plus de N fois, via une exception matérielle dédiée. La mesure du nombre d'exécution d'un branchement se fait dans l'unité de calcul dédiée aux branchements, avec l'aide d'une sorte de ''branch adress buffer'' modifié. Pour rappel, le ''branch adress buffer'' mémorise le ''Program counter'' de chaque branchement récemment rencontré. Ici, chaque entrée du ''branch adress buffer'' est associée à un compteur incrémenté à chaque exécution du branchement. ===Les processeurs Crusoe et Efficieon de Transmetta=== Les processeurs Crusoe Et Efficeron sont deux processeurs VLIW produits par la société Transmetta, une société californienne rachetée par NVIDIA. Ils étaient conçus pour exécuter spécifiquement des programmes x86, système d'exploitation inclus. L'idée derrière ce projet était d'exécuter du code x86 sans que les applications, ni même le système d'exploitation et le BIOS soient au courant ! Les processeurs Transmetta étaient en réalité conçus pour exécuter un programme unique, le ''Code Morphing Software'' (CMS), qui traduisait le code x86 en code VLIW. Transmetta ne voulait pas que son processeur VLIW soit exploité directement, sans le CMS. Il n'avait pas rendu public de compilateur pour traduire du C vers du code VLIW, il n'avait pas donné la documentation du jeu d'instruction VLIW. Des efforts de rétro-ingénieurie, documentés sur le site realworldtech, ont cependant permis de comprendre comment étaient encodées les instructions du processeur Crusoe. Comme pour le projet Daisy, le ''Code Morphing Software'' utilisait à la fois interprétation et traduction binaire, selon les besoins. Les instructions sont interprétées lors de leurs premières exécutions, mais le CMS bascule sur de la traduction en code VLIW après un certain nombre d'exécution. Ainsi, les boucles souvent exécutées sont traduites en code VLIW, alors que le reste du code est interprété. Le choix du code à traduire est le fait du CMS, il utilise des heuristiques complexes pour, qui ne sont pas connues dans le détail. Le ''Code Morphing Software'' est mémorisé dans une EEPROM, ce qui en fait un ''firmware'' situé en dessous du BIOS. Le CMS démarre ensuite le BIOS, qui lui-même démarre le système d'exploitation, qui lui-même démarre les pilotes de périphériques et les programmes. Le CMS se réserve les 16 premiers mébioctets de l'espace d'adressage. L'EEPROM du CMS est mappée dedans, mais n'en utilise que la moitié. Le reste est utilisé comme cache, pour mémoriser le code VLIW traduit par le CMS. Les deux processeurs intégraient aussi deux mémoires SRAM, utilisées par le CMS, appelées la ''local program memory'' (LPM) et la ''local data memory'' (LDM). La première contient du code qui gére les interruptions, la mémoire virtuelle, les exceptions matérielles, les problèmes d'alignement mémoire, et quelques fonctions très fréquemment utilisées par le CMS. La seconde est de la mémoire RAM utilisée par ces fonctions de la ''local program memory''. Les mémoires caches L1 et L2 sont séparées de ces deux mémoires. Le processeur Crusoe et Efficieon étaient des processeurs VLIW très simples. Ils n'avaient même pas de MMU, ni de fonctionnalités importantes sur les CPU x86. Pour Crusoe, les instructions VLIW étaient encodées sur 64 ou 128 bits, et regroupaient entre 2 à 4 opérations. Les instructions LOAD/STORE ne supportaient pas d'adressage indicé, juste de l'adressage indirect à registre. Crusoe avait 5 unités de calcul : deux ALU entières, une unité LOAD/STORE, une unité de branchement, une FPU. Efficieon doublait le nombre d'ALU entière et d'unité LOAD/STORE, les instructions VLIW passaient à 256 bits. Le processeur Crusoe contient 160 registres, dont 64 registres généraux, 32 registres flottants. Les registres généraux font 32 bits, ce qui est cohérent avec le fait que les CPU x86 de l'époque étaient des processeurs 32 bits. Quant aux registres flottants, ils faisaient 80 bits, ce qui colle avec la taille des registres flottants de la FPU x87 utilisée à l'époque. Sur les 64 registres généraux, seuls 48 étaient réellement utilisable pour mémoriser des opérandes. Une partie des registres généraux étaient utilisés pour la gestion de la pile, d'autres pour mémoriser l'état du CPU x86 émulé, un registre était un registre zéro non accesible en écriture. La gestion des exceptions est optimisée avec un système d'exceptions différées, le même que celui décrit plus haut. Le processeur utilise un système similaire, pour effectuer des lectures anticipées, qui a été expliqué dans le chapitre sur les processeurs VLIW/EPIC. Sauf qu'il s'agit là d'une optimisation pour gagner en performances en général, pas une aide utile pour gérer des exceptions précises ou toute autre subtilité de la traduction binaire. Pour gérer les exceptions différées et les lectures anticipées, le processeur mémorise l'état du processeur avant de démarrer un bloc de code. Pour cela, le processeur contient des ''shadow registers'', 48 registres entiers et 16 registres flottants. Vous remarquerez qu'il y a autant de registres entiers que de ''shadow registers'' entiers. Les registres entiers/flottants sont mémorisés dans les ''shadow registers'' avant d'exécuter un bloc de code, afin que le processeur puisse revenir à l'état de base. Une autre fonctionnalité liée est le ''gated store buffer''. Le principe est d'éviter toute écriture en mémoire RAM, tant que le code VLIW n'a pas émis d'instruction ''commit''. L'idée est que les écritures sont conservées dans la ''store queue'' de l'unité LOAD/STORE. Pour rappel, la ''store queue'' met en attente les écritures soit tant que les écritures ne sont pas terminées, soit tant que la RAM est occupée. L'idée est que les écritures sont accumulées dans la ''store queue'' tant que l'instruction ''commit'' n'a pas eu lieu. Le ''store queue'' est "déversé dans la RAM" seulement quand l'instruction ''commit'' s'exécute. ===Les CPU NVIDIA Denver=== Nvidia a tenté quelque chose de similaire avec son projet Denver. NVIDIA a racheté l'entreprise Transmetta, ses brevets, et a tenté de refaire la même chose. Le projet initial était de traduire du code x86 vers un jeu d'instruction VLIW propriétaire, comme l'a fait Transmetta. Mais NVIDIA n'a pas réussi à acquérir la licence du jeu d'instruction x86 et s'est rabattu sur le jeu d'instruction ARM. Le processeur né de ce projet est le Tegra K1-64 CPU. Il s'agit d'un processeur composé d'un cœur VLIW, couplé à deux décodeurs d'instructions ARM. Les deux décodeurs ARM traduisent une paire d'instructions ARM consécutives en instructions VLIW, pour les exécuter sur le cœur VLIW. Le CPU peut fonctionner selon deux modes : ARM et VLIW. En somme, le processeur supporte deux jeux d'instruction : le jeu d'instruction ARM, et un jeu d'instruction VLIW. En mode ARM, les instructions ARM sont chargées depuis le cache, traduites en VLIW par les décodeurs ARM, puis exécutées par le cœur VLIW. En mode optimisé, le CPU exécute des instructions VLIW chargées depuis le cache d'instruction, les décodeurs ne sont pas utilisés. : Le CPU supporte en réalité deux jeux d'instruction ARM : ARM8 et 7. Mais ils correspondent au même mode pour le CPU. Un point important est que le processeur peut changer très rapidement de mode, en à peine quelques cycles d'horloges. On n'est clairement pas dans le cas des CPU x86, qui mettent des plombes pour passer du mode 32 à 64 bits et inversement. La commutation est tellement rapide qu'on peut considérer que le processeur supporte deux jeux d'instruction simultanément. Et le support simultané de deux jeux d'instruction facilite l'implémentation de la traduction binaire. Encore une fois, inutile de traduire du code qui ne sera exécutée qu'une seule fois, mieux vaut l'exécuter directement dans le mode ARM. Surtout que le processeur supporte un mode ARM qui permet d'exécuter du code ARM sans perte de performance. Par contre, le code fréquemment utilisé, notamment dans des boucles critiques, est traduit en VLIW. Au final, les performances sont optimisées, en limitant le travail du logiciel traducteur binaire. La détection du code fréquemment exécuté se fait différemment que pour les processeurs Transmetta et Daisy. L'unité de branchement ne détecte pas directement le code beaucoup exécuté, même si elle a un rôle à jouer. A la place, la détection est réalisée en logiciel, par un ''thread'' dédié, qui s'exécute sur un cœur séparé. Il détecte le code fréquemment utilisé en regardent les branchements exécutés récemment. Pour cela, le processeur mémorise l'historique des branchements pris récemment et copie cette historique dans une mémoire tampon dédiée, partagée entre tous les cœurs. Vu qu'elle est partagée, le ''thread'' de détection a accès à l'historique sans pertes de performances, seule la copie de l'historique dans ce tampon a un cout en performance, pas son partage. Le code VLIW obtenu après traduction binaire est mémorisé dans la mémoire RAM, dans une portion spécialement réservée pour. Elle fait 128 mébioctets, et est appelée le '''cache d'optimisation''' par NVIDIA. Le cache d'optimisation est protégé en écriture et seul le traducteur binaire peut écrire dedans. Au passage, le traducteur binaire est du code VLIW, ce qui fait qu'il est placé dans le cache d'optimisation. Le processeur détecte automatiquement quand une fonction pour laquelle le code compilé équivalent est disponible. Pour cela, l'unité de chargement contient une table de correspondance entre l'adresse de la fonction ARM, et l'adresse de son équivalent compilé. Quand une fonction est appelée, l'unité de chargement regarde l'adresse de destination du branchement. Si l'adresse est dans cette table, elle récupère l'adresse de la fonction compilée et branche vers celle-ci. Le processeur dispose d'un cache d'instruction de 128 kibioctets, ce qui est très important, mais nécessaire vu la taille des instructions VLIW. Le coeur VLIW dispose de ses propres décodeurs, d'un ''scoreboard'', de 7 unités de calcul et de registres. Pour les unités de calcul, il a deux FPU, deux ALU, une unité de branchement, et deux unités LOAD-STORE faisant aussi ALU. Un point important est que le CPU dispose de 64 registres entiers et de 64 registres flottants. C'est deux fois plus que supporte le jeu d'instruction ARM. En mode ARM, seule la moitié des registres est utilisée. Mais en mode optimisé, le traducteur binaire utilise bien les 64 registres, grâce à une sorte de renommage de registres logicielle. Pour simplifier la traduction binaire, le processeur supporte les techniques vues précédemment. Le processeur intègre un ''gated store buffer'' similaire à celui des processeurs Transmetta. Les techniques d'exceptions différées sont aussi supportées, comme sur les processeurs Transmetta. Il a aussi une unité de préchargement décrite par NVIDIA comme agressive, avec support du préchargement de type anticipé (''runahead''). ===L'émulation de la mémoire virtuelle=== Pour finir, il faut parler de la mémoire virtuelle avec la traduction binaire. Sur le processeur source, le processeur gère à la fois des adresses physiques et virtuelles et sa MMU fait la conversion entre les deux. Sur le processeur VLIW, la MMU est simulée par le programme de traduction binaire, partiellement ou totalement. L'implémentation exacte varie suivant que l'on parle du projet Daisy, des processeurs Transmetta ou de Denver. Sur les processeurs Transmetta, il n'y a pas de MMU ni de mémoire virtuelle. A la place, le programme de traduction binaire émule la mémoire virtuelle du processeur source. Le processeur VLIW ne gère que des adresses physiques, rien d'autre. L'espace d'adressage physique du processeur VLIW a la même taille que celui du processeur émulé, ici des processeurs x86. Il est vraisemblable que les adresses physiques utilisées par le processeur x86 sont les mêmes que celles du processeur VLIW. Sur le projet Daisy, le processeur VLIW gère la mémoire virtuelle via pagination, comme les processeurs émulés. Ce qui fait qu'il gère un espace d'adressage virtuel et un espace d'adressage physique. Pour éviter toute confusion, nous parlerons d'adresse physique/virtuelle VLIW pour les adresses physiques/virtuelles du processeur VLIW, d'adresse virtuelle/physique source pour celle du jeu d'instruction traduit, à savoir du Power PC ou du s390, éventuellement de l'x86. Il faut alors faire le lien entre adresses physiques source et adresses virtuelles VLIW. Pour cela, rien de plus simple : il y a correspondance parfaite. L'adresse physique source numéro N correspond à l'adresse virtuelle VLIW numéro N. Cependant, cela signifie que tout l'espace d'adressage virtuel VLIW serait utilisé par le code à traduire. En réalité, il faut ajouter de la place pour le code traduit et le programme de traduction binaire. La conséquence est que l'espace d'adressage virtuel VLIW est plus large que l'espace d'adressage source. L'espace d'adressage virtuel VLIW est découpé en trois sections : une section pour le code Power PC à traduire, une autre pour le code traduit en VLIW, et une dernière réservée au traducteur binaire. Le programme de traduction est placé dans une ROM mappée en mémoire dans la seconde section. Le reste de la seconde section est réservé à une mémoire RAM utilisée par le programme de traduction. [[File:Mémoire virtuelle sur le projet Daisy.png|centre|vignette|upright=1.5|Mémoire virtuelle sur le projet Daisy]] Il faut noter que chaque page physique source, une fois traduite, correspond à N pages virtuelles VLIW. La raison est que le code traduit prend plus de place que le code machine originel. En conséquence, on doit utiliser une page finale plus grande. Pour le reste, des pages contiguës en mémoire physique source sont elles aussi contiguës en mémoire virtuelle VLIW. Le calcul d'adresse est donc simple : l'adresse physique source est multipliée par N, puis on ajoute l'adresse de base à laquelle commence la section pour le code traduit. ==La traduction binaire sur le Burroughs B1700== Nous venons de voir comment accélérer en matériel la traduction binaire, avec des techniques bien spécifiques. Le Burroughs B1700 a procédé autrement et il est intéressant d'étudier en détail son architecture. Son architecture est décrite dans le livre "Interpreting Machines : Architecture and Programming of the Bl700/Bl800 Series", écrit par Elliott I. Organick et James A. Hinds. Une partie de ce qui vfa suivre est un très court résumé de ce livre. ===Le Burroughs B1700 : le mal-nommé ''split-level control store''=== Le Burroughs B1700 avait plusieurs modèles : le B1726, le B1710 et le B1800. Ils se distinguaient par la manière dont le code machine source était traduit par le traducteur binaire. Dans ce qui suit, nous parlerons de traducteur binaire pour parler soit d'un interpréteur pour un langage de programmation de haut niveau, soit pour un logiciel de traduction binaire. Le B1710 plaquait le traducteur binaire en mémoire RAM. Le B1800 faisait pareil, mais avait ajouté un cache dédié au code du traducteur binaire, afin d'améliorer les performances. Le B1726 avait un traducteur binaire scindé en deux : une partie était en mémoire RAM, l'autre était dans un ''local store'' dédié. Le processeur peut exécuter le traducteur binaire soit depuis le ''local store'', soit depuis la mémoire RAM avec une pénalité en termes de performances. La documentation appelle cela un '''''split-level control store''''', mais le terme est trompeur : la documentation qualifie de microcode le langage machine du Burroughs B1700, ce qui est à l'origine de beaucoup de confusions. Une autre source de confusion est que ce langage machine a quelques propriétés qu'on pourrait attendre d'un microcode, mais certaines sont manquantes. Par exemple, il peut adresser les registres d’interfaçage mémoire, mais c'est parce que ce sont des registres architecturaux sur ce processeur. Ses instructions n'encodent pas de signaux de commande, par contre, elles font 16 bits et adressent les opérandes de manière implicite. Le ''local store'' mémorisait au maximum 2048 instructions machine, ce qui fait que les 2048 premières "adresses" correspondaient au ''local store''. Les suivantes étaient des adresses de mémoire RAM qui contenaient le reste du traducteur binaire. En pratique, l'adresse qui séparait les deux était configurable ! On pouvait limiter la taille du ''local store'' à un multiple de 32 instructions ! Les adresses restantes étaient alors réattribuées à la RAM. Par exemple, on pouvait attribuer 64 adresses au ''local store'', les 2048 - 64 adresses restantes étaient alors attribuées à la mémoire RAM. Un registre limite, nommé TDPM, indiquait quelle était l'adresse de démarcation entre les deux. Il contenait 4 bits, dont la valeur était multipliée par 32, pour obtenir l'adresse limite. Le programmeur devait découper le traducteur binaire en segments, certains étant placé dans le ''local store'', les autres étant en mémoire RAM. Mieux que ça, le processeur autorisait d'utiliser la technique de l'''overlaying'' pour le microcode ! Il y avait même une fonction OVERLAY dédiée pour ! OVERLAY avait besoin de trois opérandes : la taille du segment à copier, son adresse source en mémoire RAM, son adresse de destination dans la micro-SRAM. L'interpréteur réservait un "segment" en mémoire RAM, pour les données, dont le processeur connaissait la position. En-dehors de ce segment, il n'y a que des instructions machines à traduire, et le traducteur binaire. Le CPU incorporait pour cela deux registres, un pour l'adresse de base du segment de données, un autre pour son adresse de fin. Il incorporait une protection mémoire limitée sur ce segment, à savoir que seul l'interpréteur pouvait lire ou écrire dans ce segment. ===Une architecture bit-adressable=== Le CPU Burroughs B1700 était '''bit-adressable'''. En clair, il pouvait adresser la mémoire bit par bit, si nécessaire. Un avantage est que cela facilitait la traduction binaire des instructions machine, mais nous détaillerons cela plus bas. Un autre avantage est que cela permettait d'émuler des architectures dont la taille des registres/''byte''/mots était très différente. Et cet avantage mérite quelques explications immédiates. Le processeur était un CPU 24 bits, mais il pouvait émuler des processeurs 16 bits, 8 bits, 4 bits, 1 bit, 9 bits, ou toute autre valeur. Pour cela, le processeur lisait des mots de 24 bits et utilisant un ''barrel shifter'' pour sélectionner les bits adéquats. Par exemple, pour un accès de 16 bits, la donnée lue en mémoire était masquée de manière à ne garder que les 16 bits de poids faible. Le ''barrel shifter''/circuit de masquage était placé directement avant le bus mémoire, sur le trajet des données/instructions. Le ''barrel shifter'' était commandé par un registre ''Field Unit'', qui précisait quelle était la taille des données à charger. Pour communiquer avec la mémoire, il avait trois registres séparés : READ, WRITE, MAR. READ contient la donnée lue lors d'une lecture, WRITE est pour une donnée à écrire lors d’une écriture, MAR contient l'adresse à lire ou en cours de lecture. Il s'agit de registres d’interfaçage mémoire. Le fait que ces registres d’interfaçage mémoire et IO soit adressable est peu commun, et c'est plus quelque chose qu'on attend d'un microcode que d'instructions machines. Mais il faut bien comprendre que les registres d’interfaçage mémoire ont été promus au rang de registres architecturaux, sur ce processeur. De même, pour communiquer avec les entrées-sorties, il disposait de deux registres CMND et DATA : CMND pour envoyer une commande à une entrée-sortie, DATA pour lire ou écrire une donnée. Il y avait aussi un registre U, qui mémorisait une donnée lue depuis le lecteur de cassette. ===L'ALU de taille variable=== Les Burroughs B1700 intégrait une pile d'adresse de retour, capable de mémoriser 16 adresses de 24 bits. Il disposait de 4 registres généraux, nommés X, Y, T et L, tous de 24 bits. Un point important est que le registre T avait des possibilités d’extraction spécifiques. Il était possible d'extrait N bits, placés n'importe où dans ce registre, et de les copier dans un autre registre. Nous verrons pourquoi cette possibilité est intéressante et ne sert que pour le registre T. Ils avaient une '''ALU de taille variable'''. En clair, l'ALU pouvait faire des calculs sur un nombre de bits compris entre 0 et 24. Pour cela, les calculs étaient fait avec une ALU de 24 bits, puis les bits de poids fort inutiles étaient masqués. L'ALU de 24 bits pouvait masquer les bits de poids fort du résultat, mais avec des limitations. Elle gérait des opérandes et résultats de 0, 4, 8, 12, 16, 20 et 24 bits. Pour avoir un réglage plus fin, une ALU 4 bits séparée permettait de corriger le résultat. Les registres X et Y étaient reliés à une ALU de 24 bits capable d'effectuer des opérations arithmétiques basiques. Fait étonnant, l'ALU faisait tous les calculs en même temps et fournissait ses résultats dans 7 registres : un registre SUM pour l'addition, un registre DIFFERENCE pour la soustraction, et un registre par opération bit à bit. Les registres pour les opérations bit à bit sont : CMPX et CMPY pour l'opération NOT sur chaque opérande, XANY pour le ET bit à bit, XEOY pour le XOR, XORY pour le OU logique. Les additions et soustractions pouvaient se faire sur des opérandes codés en binaire ou en BCD, suivant le code opération utilisé. A cela, il fallait ajouter deux registres de "résultat" MSKX et MSKY. Ils fournissaient un masque dépendant de la taille du résultat en bits. C'est ce registre qui permettait de gérer des données de 0, 4, 8, 12, 16, 20, et 24 bits, alors que l'ALU faisait 24 bits. Le masque dans ce registre pouvait servir d'opérande dans une opération de masquage ultérieure, pour corriger le résultat. L'ALU prenait en opérande les registres X et Y, mais aussi un registre CYF pour la retenue entrante. En sortie, il fournissait aussi deux retenue sortantes : une pour l'addition et une autre pour la soustraction. Elles étaient mémorisées dans les registres CYL et CYD, respectivement. L'ALU fournissait aussi 12 conditions, qui alimentait un registre d'état. Le registre d'état était segmenté en trois registres appelés XYST, XYCN et BICN. Les registres X et Y sont des registres pour les opérandes, mais il y a aussi deux autres registres de 24 bits. Il s'agit des registres T et L, qui sont des registres généraux. Les registres T et L étaient découpés en 6 sous-registres de 4 bits, nommés LA LB LC LD LE et LF pour le registre L, et TA TB TC TD TE et TF pour le registre T. Ils étaient adressables comme n'importe quel registre. Le processeur contenait de nombreux autres registres de 4 bits, dont les sous-registres de T et L. En tout, il y a 27 registres de 4 bits adressables ! Et le processeur disposait d'une ALU 4 bits pour manipuler ces registres de 4 bits ! Elle est capable de faire des MOV entre registres de 4 bits, des opérations bit à bit, et peut aussi tester si un bit vaut 0 ou 1. La dernière possibilité est très utile pour implémenter les branchements. De plus, cela permettait de faire des opérations de masquage, notamment pour appliquer un masquage supplémentaire à celui de l'ALU. Par exemple, pour gérer des opérations sur 14 bits, on effectue une opération sur 16 bits avec l'ALU, et on masque les 2 bits manquants avec l'ALU 4 bits. La capacité d'extraction du registre T pouvait aussi être utilisé pour ça. ===Le code machine du Burroughs B1700=== Le processeur utilise des instructions de 16 bits de long, qui sont copiées dans registre d'instruction M de 16 bits. Fait intéressant, il était possible de faire un OU logique entre l'instruction lue et un registre du processeur, le résultat étant mémorisé dans le registre M. Cela permettait de faire du code automodifiant, pour émuler des modes d'adressage complexe. Cela permettait par exemple de remplacer une adresse dans une micro-opération. L'adresse est alors surimposée avec un OU dans le champ d'adresse, alors que le reste de l'instruction n'est pas modifié. Afin de gérer les interruptions, ces dernières étaient partiellement détectées en logiciel. Une interruption doit être prise en compte à la toute fin de l'exécution d'une instruction source, quand celle-ci a finit son travail. Mais vu qu'ici, les instructions sont émulées avec des séries d'instructions cibles, il y a un problème : il est possible d'interrompre une instruction émulée en plein milieu. Pour éviter cela, le processeur ne déclenche pas les interruptions matérielles immédiatement, elles sont mises en attente. Une instruction dédiée vérifie si une interruption est en attente, et l'exécute alors cas échéant. L'instruction en question est utilisée en dernier, dans la suite d'instruction qui émule l'instruction source. Le ''program counter'' est appelé le registre A. Les instructions du processeur font 16 bits de long, et elles sont alignées en RAM sur 16 bits, ce qui fait que les 4 bits de poids fort de ce registre valent 0, seuls les 14 bits de poids fort sont utiles. Les adresses mentionnées plus haut, qui adressent les micro-opérations en mémoire RAM, sont des adresses de mots de 16 bits, pas les adresses de bit individuels. ===La traduction binaire sur le Burroughs B1700=== Les instructions source peuvent faire 16 bit avec tel jeu d'instruction, 8 bits avec un autre, 32 bits sur un autre, etc. Elles peuvent aussi être de longueur fixe ou variable ! Le B1700 utilisait une architecture bit-adressable pour gérer ces contraintes. En effet, avec un CPU bit-adressable, les contraintes d'alignement des instructions disparaissent ! Dans ce qui suit, on suppose que l'instruction est composée de '''champs''' : l'opcode, les opérandes, les adresses absolues, les constantes immédiates, les numéros de registre, etc. L'idée est que l'instruction source est chargée dans le registre T, puis découpée en champs, qui sont copiés dans les registres du processeur ou envoyés au microcode. Je rappelle que le registre T est relié à un ''barrel shifter'', différent au précédent, qui permettait de masquer, décaler et extraire une donnée de ce registre. Et il y a la possibilité d'extraire une suite de bit de ce registre T, pour la copier dans un autre registre. Par exemple, prenons une instruction composée d'un opcode, d'une constante immédiate et d'une adresse immédiate. L'opcode est lu et interprété par le microcode. Le microcode extrait la constante immédiate du registre T et la copie dans le registre X. Ensuite, l'adresse est extraite et envoyée au registre FA, une lecture est démarrée, et l'opérande est copiée dans le registre Y. Enfin, l'instruction est exécutée par le microcode. Le tout prend plusieurs micro-opérations : une première pour lire l'opcode, une seconde pour lire la première opérande, une troisième pour lire la seconde opérande, una quatrième pour tout exécuter. Notons que suivant la taille des champs, on peut en charger plusieurs à la fois. Par exemple, imaginons qu'on ait chargé 24 bits, contenant un opcode de 8 bits et une constante immédiate de 16 bits : pas besoin de faire deux accès mémoire, le registre T est juste découpé en un opcode et une constante. Le registre T est fait pour ce genre d'extraction. ===Le chargement d'un champ=== L'architecture est bit-adressable car elle gére des champs qui ne font pas forcément 8 ou 16 bits. Par exemple, certains jeux d'instructions ont des opcodes codés sur 6 ou 7 bits, voire 9-10 bits. Par exemple, prenons un opcode de 7 bits, suivi par une adresse de 12 bits et une constante immédiate de 15 bits : aucun de ces champs ne sera aligné sur 8/16/24 bits. Utiliser une architecture bit-adressable règle le problème d'alignement. Et en général, les contraintes d'alignement des instructions disparaissent ! Quand on charge une instruction, peut importe qu'elle soit ou non alignée sur 24 bits, elle sera placée dans les bits de poids faible, il suffira de masquer les bits de poids fort inutiles. Par exemple, si on charge une instruction de 16 bits, les 16 bits seront copiés dans les 16 bits de poids faible du registre T, il suffira de masquer l'octet de poids fort. Pas besoin de faire de décalages pour gérer des instructions mal alignées... Pour charger une instruction, il faut connaitre plusieurs informations : l'adresse de son premier bit, sa taille. Pour cela, le B1700 contient deux registres, nommés ''Field Adress'' (FA) et ''Field Length'' (FL), qui indiquent l'adresse du premier bit et la taille de l'instruction à charger. Le premier fait 24 bits, ce qui permet de gérer des adresses de 24 bits. N'oubliez pas que le processeur est bit-adressable, ce qui permet d'adresser 16 Mébi-bits, soit 2 mébioctets. Le registre pour la taille fait lui 16 bits, ce qui permet de gérer des instructions de 65536 bits, soit 8192 octets ! Leur nom commence par ''field'', car le processeur peut gérer des instructions de taille variable et/ou plus longues que 24 bits. Dans ce cas, le chargement de l'instruction se fait champ par champ. Pour donner un exemple, prenons une instruction composée de 20 champs de 12 bits chacun. Les champs sont chargés un par un dans le registre T. Le registre FA est alors initialisé avec l'adresse de l'instruction, il est incrémenté de 12 à chaque cycle. Le registre FL est initialisé avec la taille de l'instruction, soit 20 * 12 = 240 bits, et il est décrémenté de 16 à chaque cycle. Précisons cependant que la taille d'un champ peut être changée d'un cycle à l'autre. Par exemple, le processeur peut charger un opcode de 8 bits, l'interpréter, détecter que l'instruction demande ensuite de charger une adresse de 16 bits. Dans ce cas, le registre FL est immédiatement altéré pour charger 16 bits, au lieu des 8 bits de l'opcode immédiatement précédent. ==Les jeux d'instructions dédiés à un langage de programmation== De rares processeurs sont conçus pour un langage de programmation en particulier. On appelle ces processeurs, conçus pour des besoins particuliers, des '''processeurs dédiés'''. Par exemple, les fameux ''Burrough E-mode'' B5000/B6000/B7000 étaient spécialement conçus pour exécuter de l'ALGOL-60. Leurs cousins B2000/B3000/B4000 étaient eux conçus pour le COBOL. Des langages fonctionnels ont aussi eu droit à leurs processeurs dédiés. Le prolog en est un bel exemple, avec les superordinateurs de 5ème génération qui lui étaient dédié. On peut aussi citer les machines LISP, dédiés au langage LISP, qui datent des années 1970. Elles étaient capables d’exécuter certaines fonctions de base du langage directement dans leurs circuits : elles possédaient notamment un ''garbage collector'' câblé dans ses circuits ainsi que des instructions machines supportant un typage déterminé à l’exécution. Les processeurs dédiés ont eu leur heure de gloire au début de l'informatique, à une époque où les langages de haut niveau venaient d'être inventés. À cette époque, les compilateurs n'étaient pas performants et ne savaient pas bien optimiser le code machine. Il était alors rationnel, pour l'époque, de rapprocher le code machine cible et le langage de programmation de haut niveau. De nombreuses architectures dédiés ont ainsi été inventées, avant que les concepteurs se rendent compte des défauts de cette approche. Les défauts en question ne sont pas nombreux, mais assez simples à comprendre. Premièrement, elles sont très rapides pour un langage de programmation en particulier, mais sont assez mauvaises pour les autres, d'où un problème de "compatibilité". Ajoutons à cela que les langages de programmation peuvent évoluer, devenir de moins en moins populaires/utilisés, ce qui rend la création d'architectures généralistes plus pertinente. Enfin, les architectures dédiées sont évidemment des processeurs CISC, pour implémenter les nombreuses fonctionnalités des langages évolués. Et les défauts des CISC sont assez rédhibitoires à l'heure actuelle. À l'heure actuelle, les algorithmes des compilateurs se sont améliorés et savent nettement mieux utiliser le matériel. Ils produisent du code machine efficace, ce qui rend les architecture dédiées bien moins intéressantes. Si on ajoute les défauts de ces architectures dédiées, par étonnant que les architectures dédiées aient presque disparues. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'accélération matérielle de la virtualisation | prevText=L'accélération matérielle de la virtualisation | next=Le matériel réseau | nextText=Le matériel réseau }} </noinclude> 3ycljfjicsx974lhhxzb8yrvauy1zbj 764052 764051 2026-04-19T20:33:34Z Mewtow 31375 /* Le chargement d'un champ */ 764052 wikitext text/x-wiki De nos jours, la majorité des programmeurs programment dans des langages de haut niveau. Il est très rare pour eux d'avoir à utiliser de l'assembleur, et encore moins un langage machine. Les programmes écrits dans un langage de haut niveau sont traduits en langage machine par un logiciel appelé le compilateur. [[File:Compilation.PNG|centre|vignette|upright=2.5|Compilateur : principe.]] Il se trouve que quelques rares processeurs sont conçus pour faciliter le travail du compilateur. Et ce sont ces processeurs que nous allons voir dans ce qui suit. Ils représentent une catégorie de jeux d'instruction à part, qui n'a pas vraiment de nom. ==La compilation et l’interprétation== Un compilateur traduit donc du code source, écrit dans un langage de haut niveau, vers du langage machine. La traduction est rarement directe. En général, le compilateur traduit le code source en assembleur, qui est lui-même traduit en langage machine. L'assembleur est une représentation textuelle du langage machine alors que le code machine est du binaire exécutable par le processeur. La traduction se fait donc en deux étapes, la '''compilation''' proprement dite et l''''assemblage''', réalisées respectivement par un compilateur et un assembleur. Il faut parfois rajouter une troisième phase d'édition des liens, que nous passons volontairement sous silence. {| |[[File:Code C.png|vignette|200px|Le code source, ici en C.]] |[[File:Assem.png|vignette|250px|Le code assembleur.]] |[[File:Binary file - hello world (C programming).png|vignette|150px|Le code machine.]] |} ===L'interprétation et la compilation à la volée=== Une alternative à la compilation est l''''interprétation''', qui transforme le code source en code machine à la volée. Avec l'interprétation, le code source est passé à un logiciel appelé l''''interpréteur''', qui exécute le code source ligne de code par ligne de code. Une ligne de code est traduite en code machine, qui est exécutée, puis l'interpréteur passe à la ligne suivante. [[File:Kaantotulkkaus.png|centre|vignette|upright=3|Compilation versus interprétation.]] Un défaut de l'interprétation est son cout en performance, qui est assez important. Aussi, l'interprétation stricte a évolué vers un hybride entre interprétation et compilation. L'idée est qu'une partie peu importante du code source est interprétée, alors que le code important est compilé juste avant d'être exécuté. Pour le dire autrement, le code source est partiellement compilé à la volée, juste avant son exécution. Aussi, on parle de '''compilation à la volée'''. Le terme en anglais est ''Just In Time compilation'', abrévié en JIT. En soi, le JIT est associé à de l'interprétation, les JIT sont en réalité des hybrides interpréteurs-compilateurs. Un point important est que compiler du code à la volée est assez lourd, cela a un cout en performance. Par contre, le code compilé s'exécute plus vite que du code interprété. Si on exécute le code compilé une seule fois, le cout de la compilation l'emporte sur le gain à l'exécution. Mais le code compilé est réutilisé autant de fois que nécessaire. Le gain à l'exécution est donc multiplié par le nombre d'exécutions, alors que le cout de la compilation est répartit sur plusieurs exécutions, il est amorti. Les portions du code sont choisies de manière à avoir un gain en performance maximal. L'idée générale est que le code exécuté très souvent est compilé, alors que du code exécuté pas souvent est interprété. Typiquement, les boucles les plus souvent exécutées sont compilées, le code exécuté une seule fois est interprété. ===Les langages intermédiaires=== Les compilateurs modernes passent par un '''langage intermédiaire''' pour faire la transformation en code machine. Le compilateur traduit le langage de haut niveau en langage intermédiaire, puis traduit le langage intermédiaire en code machine cible. Le langage intermédiaire est parfois appelé le '''''bytecode''''', et ce terme recouvre aussi le code écrit avec du ''bytecode''. L'interprétation aussi peut passer par l'intermédiaire d'un un ''bytecode'', ce n'est pas limité aux compilateurs proprement dit. {| |[[File:Intermediate representation scheme.png|centre|vignette|upright=1.5|Représentation intermédiaire d'un compilateur.]] |[[File:Code interpreter scheme.png|centre|vignette|upright=1.5|Fonctionnement d'un interpréteur.]] |} Faire ainsi a de nombreux avantages, le principal étant d'avoir un compilateur capable de traduire un langage de haut niveau vers plusieurs jeux d'instructions différents. Cela permet d'avoir, par exemple, un compilateur qui traduit du C soit en code machine x86, soit en code machine pour un CPU ARM, soit pour un CPU POWERPC, etc. [[File:Compiler design.svg|centre|vignette|upright=3|Compiler design]] Pour l'interprétation, l'usage d'un ''bytecode'' a d'autres avantages. Pour l'exploiter, il faut distribuer non pas le code source, mais le ''bytecode''. En clair, l'interpréteur ne prend pas en entrée le code source, mais du ''bytecode'' déjà compilé. La traduction est alors beaucoup plus simple, car le code source a déjà été partiellement compilé, par la traduction en ''bytecode''. Interpréter du code source est en effet assez compliqué : il faut effectuer des étapes d'analyse lexicale, sémantique, et bien d'autres. Avec du ''bytecode'', ces étapes ont été réalisées lors de la compilation du ''bytecode'', l'interpréteur a alors peut de choses à faire. L'interprétation du ''bytecode'' se fait instruction par instruction, au niveau du ''bytecode''. Une instruction du ''bytecode'' est traduite en une instruction machine équivalente, qui est exécutée. Si l'instruction est un peu complexe, il exécute une fonction/procédure qui fait la même chose. Pour résumer, le ''bytecode'' est ensuite traduit à la volée et exécuté instruction par instruction par un logiciel appelé l'interpréteur. [[File:Control table.png|centre|vignette|upright=2|Control table]] Le langage intermédiaire peut être vu comme l'assembleur d'un processeur, n'existe pas forcément dans la réalité, mais dont le jeu d'instruction est décrit en détail. Le processeur en question est appelé une '''machine abstraite''', ou encore une ''machine virtuelle''. Nous utiliserons le terme de machine abstraite dans ce qui suit. La machine abstraite n'est pas la même suivant que l'on cible la compilation ou l'interprétation/JIT. Pour l'interprétation/JIT, la machine abstraite est souvent une machine à pile, car cela simplifie grandement la traduction en code machine final. L'interprétation demande que la traduction du ''bytecode'' en code machine soit la plus simple possible. Et cela demande de prendre en compte pas mal de détails. Par exemple, les différents jeux d'instruction existants n'ont pas le même nombre de registres, ce qui pose problème lors de la traduction du ''bytecode''. Lors de la transformation en code machine, un algorithme d'allocation de registres se débrouille pour traduire le code intermédiaire en code qui utilise un nombre limité de registres. Pour améliorer cette allocation de registres, il y a deux solutions niveau ''bytecode'' : soit utiliser un nombre illimité de registres, soit utiliser une machine à pile. Pour les machines à pile, il existe un algorithme simple et rapide pour traduire un code écrit pour une machine à pile en un code écrit pour un processeur avec des registres, qui se débrouille pas trop mal pour allouer efficacement les registres. C'est un avantage assez important pour les langages interprétés. ==Les processeurs qui exécutent du bytecode== Le ''bytecode'' est un code machine, ce qui signifie qu'il peut en théorie s'exécuter sur un processeur qui implémente le jeu d’instruction associé. Si le jeu d'instruction d'un ''bytecode'' est souvent une description censée être fictive, elle n'en reste pas moins un jeu d'instruction et des caractéristiques précises, qu'on peut l'implémenter en matériel ! Néanmoins, tous les ''bytecode'' ne sont pas égaux de ce point de vue. Certains sont faciles à implémenter en matériel, d'autres non. Les ''bytecodes'' implémentés en matériel sont ceux dont les machines abstraites sont des machines à pile. Un premier exemple est celui des processeurs Pascal MicroEngine, qui exécutaient directement le bytecode du langage Pascal, le fameux UCSD P-code. Un second exemple est la '''machine SECD''', qui sert de langage intermédiaire pour certains compilateurs de langages fonctionnels. Elle a été implémentée en matériel par plusieurs équipes, notamment par les chercheurs de l'université de Calgary en 1989. Dans le même genre, quelques processeurs simples étaient capables d’exécuter directement le ''bytecode'' du langage FORTH. Le FORTH, un des premiers langages à pile de haut niveau, possède de nombreuses implémentations hardware et est un des rares langages de haut niveau dont le ''bytecode'' a été utilisé comme langage machine sur certains processeurs. Par exemple, on peut citer le processeur FC16, capable d’exécuter nativement du ''bytecode'' FORTH. ===Les processeurs Java=== Le cas le plus impressionnant est celui de la machine virtuelle Java, qui est un design de processeur comme un autre. En temps normal, le ''bytecode'' Java est compilé ou interprété, mais certains processeurs exécutaient du ''bytecode'' Java directement, sans interprétation ni compilation. Ils sont appelés des '''processeurs Java'''. On peut citer les processeurs ARM disposant de l'extension Jazelle, du JEMCore et du aJ-100 de aJile Systems, le picoJava II de Sun, et quelques autres. Il y a aussi eu quelques projets de recherche ou processeurs open source, comme Komodo, jamuth, le ''Java Optimized Processor'' et quelques autres projets du même genre. L'implémentation de la machine virtuelle Java n'était cependant pas complète. Quelques instructions complexes n'étaient pas gérées par le processeur et devaient être émulées en logiciel. Mais la grosse majorité des instructions l'était. Les instructions non-supportées étaient émulées avec la méthode ''trap and emulate'', à savoir qu'un opcode inconnu déclenchait une exception matérielle ''opcode invalide'', dont la routine pouvait émuler les instructions qui doivent l'être. Le processeur aJ-100 de aJile est capable d'exécuter la quasi-totalité des instructions de la JVM Java, seules deux instructions faisant exception. Les '''accélérateurs Java''' sont une solution intermédiaire, qui permet de traduire à la volée du ''bytecode'' Java en instructions RISC, exécutées sur un autre processeur. Il s'agit formellement de traduction binaire, mais je préfère en parler ici. L'idée est de combiner un processeur RISC avec un coprocesseur Java. Le coprocesseur lit le ''bytecode'' Java et le traduit en instructions RISC, ces dernières étant exécutées sur le processeur RISC. Le JA108 de Nozomi était un coprocesseur de ce genre, au même titre que le coprocesseur JSTAR de JEDI. L'extension Jazelle de certains CPU ARM fonctionnait vraisemblablement sur un principe similaire, sauf que le coprocesseur était intégré dans le processeur ARM, entre le cache d'instruction et le décodeur d'instructions ARM. L'intérêt des processeurs Java n'est pas qu'une question de performance. Certes, ils permettent d’exécuter plus vite les programmes compilés en ''bytecode'', comme des programmes Java pour la JVM Java. Mais l'intérêt est aussi de faire des économies de mémoire RAM et ROM. Je rappelle que la machine virtuelle Java est une machine à pile, ce qui fait que sa densité de code est excellente. Les programmes en ''bytecode'' Java sont donc très petits, et prennent peu de place en mémoire ROM. De plus, utiliser un processeur Java permet de se passer d'interpréteur ou de JIT Java, ce qui économise encore plus de mémoire. ===Transformer un processeur RISC en machine à pile=== La plupart des processeurs Java utilisent en interne une architecture à registre généraux, qui émule une machine à registre en microcode. Quelques registres sont réservés pour l'état de la machine virtuelle, avec au minimum un registre réservé pour le pointeur de pile, un autre pour un pointeur vers un ''pool'' de constantes, et quelques autres. D'autres registres servent à mémoriser le haut de la pile, à savoir le sommet de la pile et quelques opérandes situées juste en-dessous. Par exemple, le processeur picoJava II gardait les 64 opérandes au sommet de la pile dans 64 registres généraux, séparés des registres pour les pointeurs et l'état de la JVM. Une optimisation possible, envisagée sur les processeurs picoJava, était une sorte de macro-fusion sous stéroïde. L'idée était de fusionner certaines séries d'instructions Java en une seule instruction machine. Typiquement, pour faire une opération arithmétique, une machine à pile empile deux opérandes et exécute une opération, ce qui prend trois instructions machine. La spécification picoJava 1 a proposé d'envoyer directement la seconde opérande en entrée de l'ALU, ce qui fusionnait l'empilement de la seconde opérande et l'opération arithmétique. Le processeur picoJava II va plus loin et fusionne les trois instructions en une seule instruction RISC, agissant sur des registres. Mais la majorité des processeurs Java ne faisaient pas cela, car cela complexifie grandement le décodeur. Les processeurs ''bigfoot'' de DCT utilisaient un système différent pour émuler une pile avec des registres. Leur banc de registre de 64 bits était coupé en deux : les 16 premiers registres étaient utilisés pour la pile, les 48 suivants étaient utilisés pour autre chose. L'idée est que le registre 0 mémorise un emplacement vide, le registre 1 contient le sommet de la pile, le registre 2 l'opérande sous le sommet de la pile, et ainsi de suite. Les 15 premières opérandes de la pile sont donc mémorisées dans les registres. Les instructions écrivent leur résultat dans le registre adéquat, puis tous les registres sont décalés pour que le résultat passe dans le registre R1. Par exemple, les instructions PUSH chargent une opérande dans le registre 0, puis tous les registres sont décalés de un rang, pour que l'opérande chargée soit dans le registre 1. Le registre 0 devient alors le registre numéro 1, le registre 1 devient le 2, et ainsi de suite. Par contre, les opérations arithmétiques font autrement. Une instruction d'addition, par exemple, lit les opérandes dans le registre 1 et 2, puis place son résultat dans le registre 2 (les deux opérandes sont dépilées). Puis, les registres sont décalés d'un rang pour que ce registre 2 devienne le 1. Les registres ne sont en réalité pas décalés, il n'y a pas de transferts entre registres. A la place, les 16 premiers registres utilisaient une forme limitée de renommage de registres, similaire au banc de registre tournant utilisé sur les processeurs EPIC comme l'Itanium. Les numéros de registres sont renommés, décalés, à chaque fois qu'on empile ou dépile une opérande. Pour faire le renommage de registres, processeur contient un ''register stack counter'' de 4 bits, qui est incrémenté à chaque fois qu'on empile une donnée, et est décrémenté quand on dépile une donnée. Quand on veut accéder à un opérande, le numéro de registre architectural associé est traduit en numéro de registre physique, en additionnant le ''register stack counter'' (modulo 16, pour rester sur 4 bits). ==L'accélération de l'interprétation/JIT : le ''Thumb-EE'' d'ARM== Quelques processeurs ont ajouté des instructions pour faciliter le travail des interpréteurs/JIT. Par exemple, en 2005, ARM a ajouté le mode ''Thumb Execution Environment'', qui faisait cela. Il reprenait le jeu d'instruction compact ''thumb'' et ajoutait quelques instructions et modifiait le comportement d'instructions existantes. Le jeu d'instruction ''Thumb-EE'' a été ajouté sur les CPU ARM en 2005, mais a été déprécié en 2011, par manque d'utilité. Mais la tentative mérite qu'on s'attarde dessus. Les CPU de gamme M, déstinés à l'embarqué, n'ont jamais supporté ''Thumb-EE'', vu que de tels processeurs ne sont pas conçus pour exécuter du code interprété/JIT. Le mode ''Thumb-EE'' est un mode d'exécution, séparé de l'ARM normal. Ainsi, une instruction ''Thumb-EE'' et une instruction ARM peuvent avoir le même encodage en binaire, mais se comporter différemment. Le processeur est à tout moment soit en mode ''Thumb-EE'', soit en mode ARM normal, ce qui précise comment ces instructions doivent se comporter. Des instructions ont été ajoutées par ''Thumb-EE'', pour qu'il fasse son travail. L'entrée et la sortie du mode ''Thumb-EE'' se fait avec deux instructions : ENTERX et LEAVEX. Une fois en mode ''Thumb-EE'', le décodage des instructions ''thumb'' se fait avec les règles du ''Thumb-EE'', et non celles du ''thumb'' normal. Une première différence entre ''thumb'' normal et ''Thumb-EE'' est l'ajout de ce qui s'appelle un ''null check'' pour les instructions mémoire. Le ''null check'' est utilisé pour les instructions en mode d'adressage "base + indice" ou "Base + décalage", ou tout autre mode d'adressage avec un registre de base utilisés dans des calculs d'adresse. L'idée est de vérifier si l'adresse de base vaut zéro ou non. Si c'est le cas, le processeur lève une exception matérielle, qui est traitée par l'interprétation ou le compilateur JIT. Une telle situation est en effet signe d'une erreur d'adressage mémoire, qui doit être traitée par l'interpréteur. Typiquement, c'est signe qu'un pointeur n'a pas bien été initialisé, ce qui peut arriver avec du code interprété ou JIT. Les interpréteurs/JIT traitent généralement la situation en ajoutant un test avant toute instruction mémoire, pour vérifier si le pointeur accédé vaut zéro ou non. Avec ''Thumb-EE'', pas besoin d'ajouter les tests en questions : ils sont réalisés automatiquement lors de chaque instruction mémoire. : Les registres pour la pile, comme le pointeur de pile et le pointeur de ''frame'', sont aussi concernés. L'instruction CHKA vérifie que les accès à un tableau ne débordent pas en dehors du tableau en question. Par exemple, pour un tableau de 1024 éléments, elle vérifie si l'indice est compris entre 0 et 1024. Pour cela, elle vérifie si l'indice de tableau est bien dans l'intervalle adéquat et lève une exception matérielle si ce n'est pas le cas. Son utilité se comprend quand on sait que tous les accès à un tableau sont vérifiés par l'interpréteur. L'interpréteur est censé ajouter des instructions pour vérifier les indices, à savoir deux ou trois branchements. Avec CHKA, le test se fait en une seule instruction. D'autres modifications mineures de l'encodage des instructions sont aussi présentes, ainsi que des modifications pour les instructions LOAD/STORE. Précisément, dans les modes "base + indice" et "base + décalage", le décalage et l'indice subissent maintenant des décalages. L'idée est de simplifier le calcul d'adresse. De quoi économiser quelques instructions lors des calculs d'adresse, ce qui facilite le travail de l'interpréteur/JIT. Et des instructions LOAD/STORE précises voient leur comportement modifié pour faciliter la gestion de la pile de Java ou l'exécution des méthodes locales. Pour faciliter le travail de l'interpréteur, deux instructions HBP et HBLP ont été ajoutées. Elles branchent vers une fonction qui gère les exceptions logicielles (une fonctionnalité présente dans de nombreux langages, comme Java). La première effectue un branchement vers la fonction en question, la seconde sauvegarde l'adresse de retour avant de faire ce branchement. Les deux peuvent brancher vers 256 fonctions pré-déterminées, sans avoir à présenter leur adresse. ==La traduction binaire accélérée par le matériel== Plus haut, nous avons surtout parlé des compilateurs et des interpréteurs. Cependant, nous devons aussi parler de la '''traduction binaire'''. Elle traduit un programme écrit dans un code machine vers un autre code machine. Par exemple, elle traduit un programme compilé pour un CPU x86 vers un code machine ARM. En général, le système d'exploitation est généralement compilé pour le jeu d'instruction natif, mais il exécute des applications prévues pour le x86. Les applications sont traduites par le système d'exploitation, avant d'exécuter le code traduit. : Nous parlerons dans la suite de code machine source et de code machine cible, pour parler respectivement du code à traduire et du code obtenu après traduction. Idem avec d'autres termes comme architecture cible/source, ou jeu d'instruction cible/source. La traduction binaire est surtout utilisée pour des questions d'émulation ou de compatibilité. Par exemple, lorsque les Macintosh sont passés de processeurs Power PC vers des processeurs x86, le système d'exploitation Mac OS utilisait la traduction binaire pour convertir les anciennes applications Power PC vers du code x86. Les utilisateurs n'y ont vu que du feu. Le système de traduction binaire était appelé Rosetta 1. Par la suite, lors de la transition de processeurs x86 vers des processeurs Apple, Rosetta 2 a vu le jour. La traduction binaire a surtout été utilisée pour traduire du code x86 vers un autre jeu d'instruction. Il faut dire que le x86 est le jeu d'instruction dominant. De nombreuses entreprises ont eu pour ambition de briser l'hégémonie du x86 sur PC, en remplaçant le x86 par un jeu d'instruction plus performant, tout en gardant une compatibilité maximale. La traduction binaire était la seule solution pratique. Elle était le plus souvent intégralement réalisée en logiciel, comme c'était le cas sur les architectures Itanium avec le ''IA-32 Execution Layer''. ===Les généralités sur la traduction binaire assistée en matériel=== L'architecture source est presque tout le temps une architecture CISC, assez ancienne, qu'on souhaite émuler. Le choix de l'architecture cible se porte souvent sur une architecture VLIW, fort différente de l'architecture source, et ce pour obtenir une économie de matériel conséquente. Pour s'exécuter rapidement, le code traduit doit exploiter le parallélisme d'instruction, à savoir exécuter plusieurs instructions en même temps dans des unités de calcul séparées. Il est possible d'utiliser un processeur superscalaire avec exécution dans le désordre pour cela, mais au prix d'un cout important en transistors. Alors qu'en utilisant un CPU VLIW, c'est le traducteur binaire qui fait tout le travail d'extraction du parallélisme d'instruction. Et après tout, quitte à avoir un traducteur binaire, autant lui refiler le boulot d'optimisation. Compiler du code à la volée est certes assez lourd, mais qu'il existe des algorithmes efficaces pour regrouper des instructions indépendantes dans une seule instruction VLIW. Au passage, le processeur VLIW a plus de registres que le processeur source. Cela permet de faire du renommage de registres directement en logiciel. Le traducteur binaire n'hésite pas à changer les noms de registres entre instructions source et instruction VLIW cible, afin de supprimer des dépendances de données. En conséquence, cela ouvre des opportunités de parallélisme, qui sont exploitées lors du regroupement des opérations en instructions VLIW. Vous pourriez penser que le choix d'un CPU VLIW pour émuler du CISC est tout sauf optimal. Vous devez penser que la traduction binaire est d'autant plus simple que l'architecture source et cibles sont semblables. Dans les faits, ce n'est pas tellement traduire les instructions qui pose problème, mais plus la gestion du registre d'état, des exceptions matérielles, de la mémoire virtuelle, la différence entre gros-boutisme et petit-boutisme, de même que des différences d'adressage pour les périphériques. Le premier problème est la gestion des conditions, notamment en présence d'un registre d'état. En pratique, la traduction binaire s'utilise pour traduire du code CISC vers du code RISC ou VLIW. Les architectures source ont donc un registre d'état, qui est mis à jour non seulement par des instructions de test, mais aussi des instructions arithmétiques. L'architecture cible n'a elle pas de registre d'état, mais des registres à prédicats. La traduction de l'un vers l'autre est alors quelque peu compliquée. Et elle est d'autant plus compliquée que le registre d'état est la source de dépendances d'instruction implicites, qui réduisent les performances. Par exemple, si une instruction arithmétique modifie le registre d'état, cela peut impacter l'exécution d'une instruction ultérieure, qui lit ce registre d'état. Pour éliminer ces fausses dépendances, le traducteur binaire doit renommer ce registre en logiciel et détecter les dépendances utiles. Et le compilateur doit gérer les cas où il y a beaucoup de distance entre les deux, voire les cas où l'instruction dépendante n'a pas encore été analysée par le compilateur. Un autre problème est lié à la gestion des exceptions matérielles, et précisément des exceptions précises. Pour rappel, une instruction VLIW regroupe plusieurs opérations, opérations qui correspondraient à une instruction machine sur un CPU pas VLIW. En conséquence, plusieurs instructions du langage machine "source" sont regroupées en une seule instruction VLIW. Et il faut tenir compte du cas où une instruction source lève une exception. Dans le code VLIW, cela signifie qu'une opération lève une exception, et il faut annuler partiellement l'instruction VLIW associée. Et par partiellement, on veut dire que seules les opérations suivantes dans l'ordre du programme source doivent être annulées, puis ré-exécutées. Et c'est un sacré casse-tête ! Les processeurs VLIW utilisent comme solution le mécanisme d'exceptions différées des processeurs EPIC, vu il y a quelques chapitres. Pour résumer, le code est exécuté par blocs d'instruction, délimités par des branchements ou tout autre limite/barrière pertinente dans le code. Un bloc de code est compilé en deux versions : une version rapide sans exceptions matérielles, une version lente qui gère les exceptions précises. La version rapide s'exécute sans exécuter les exceptions. Cependant, les exceptions matérielles sont enregistrées, pour être prises en compte à la toute fin du bloc de code. La version rapide est exécutée en premier et elle mémorise si une exception matérielle a eu lieu. Puis une instruction vérifie si une exception a eu lieu et décide quoi faire. Si aucune exception n'a eu lieu, elle passe à la suite du programme, les résultats du bloc de code sont définitivement acceptés. Mais si une exception a eu lieu, tout est annulé. Le processeur est remis dans l'état initial, puis le code est ré-exécuté instruction par instruction de manière à gérer l'exception correctement. ===La traduction par pré-décodage=== Une première solution serait de faire la traduction binaire dans le cache d'instruction. Lors d'un défaut de cache, le code chargé depuis la RAM est traduit en code machine cible. Et c'est ce code machine cible qui est mémorisé dans le cache d'instruction et exécuté par le processeur. La technique marche sur le papier et n'est qu'une amélioration des techniques de pré-décodage vues il y a quelques chapitres. Cependant, le code obtenu est une traduction assez basique, qui n'incorpore pas d'optimisations dignes de ce nom. De plus, elle gère mal le cas où la taille du code source et cible sont potentiellement très différentes. Concrètement, elle est surtout utile pour traduire du code d'un processeur RISC vers un autre processeur RISC, dont les tailles d'instructions sont similaires. Elle ne permet pas d'émuler plusieurs jeux d'instructions différents, le cout en matériel (un circuit de pré-décodage par jeu d'instruction) serait trop important. ===Le projet DAISY d'IBM=== De très rares processeurs étaient conçus pour accélérer cette traduction binaire, afin de garder de bonnes performances. Les premiers à avoir étudié l'idée étaient IBM, avec leur projet DAISY (''Dynamically Architected Instruction Set from Yorktown''). Le projet de base était de convertir à la volée du code compilé pour des CPU Power PC, vers du code VLIW. Les chercheurs d'IBM avaient développé un algorithme de traduction dynamique efficace, ainsi qu'un processeur VLIW disposant d'optimisations spécifiques à la traduction binaire dynamique, à la volée. De nombreuses idées de ce projet ont été reprises ou re-découvertes par la société Transmetta, avec ses processeurs Crusoe et Efficieon, puis par NVIDIA avec son projet Denver. Les ingénieurs de ce projet ont étudié la possibilité d'émuler plusieurs jeux d'instructions différents sur un même processeur, notamment le s390 d'IBM et le x86. En théorie, cela demande juste d'avoir plusieurs programmes de traduction binaire : un pour le x86, un autre pour le s390, éventuellement un autre pour le Power PC. Mais cela ne s'est pas concrétisé. Mais les ingénieurs ont étudié quelle pourrait être l'architecture VLIW idéale pour ça. Par exemple, il fallait des additions 3-opérandes pour simplifier les calculs d'adresse. Une difficulté était la gestion du registre d'état, dont les bits ne sont pas mis à jour de la même manière sur le x86 et le s390 ou le Power PC. La différence entre gros-boutisme et petit-boutisme était aussi un problème, de même que des différences d'adressage pour les périphériques. Pour que la traduction binaire soit efficace, le processeur VLIW intègre diverses optimisations, comme les exceptions différées et les branchements multi-voies. La principale est la suivante : seul le code des boucles ou fonctions exécutées fréquemment est traduit en code VLIW, le reste du code est interprété. En effet, traduire du code binaire prend plus de temps qu'une simple interprétation. Pour du code qui ne sera exécutée qu'une seule fois, il est plus rapide d'utiliser l'interprétation. Par contre, pour du code exécuté beaucoup de fois, le cout de la traduction binaire est amorti, dilué sur N exécutions, compensé par le gain en temps d'exécution de ce code traduit. Au final, cela permet de ne traduire que le code qui le mérite. La détection du code fréquemment exécuté est réalisée dans le cœur VLIW. L'unité de branchement mémorise les derniers branchements rencontrés et le nombre de fois qu'ils ont été exécutés. Si ils ont été exécutés un certain nombre de fois, l'unité de branchement lève une exception matérielle, qui invoque le traducteur binaire. La routine de cette exception est le traducteur binaire proprement dit. En clair, la traduction binaire est démarrée quand le processeur détecte qu'une fonction a été exécutée plus de N fois, via une exception matérielle dédiée. La mesure du nombre d'exécution d'un branchement se fait dans l'unité de calcul dédiée aux branchements, avec l'aide d'une sorte de ''branch adress buffer'' modifié. Pour rappel, le ''branch adress buffer'' mémorise le ''Program counter'' de chaque branchement récemment rencontré. Ici, chaque entrée du ''branch adress buffer'' est associée à un compteur incrémenté à chaque exécution du branchement. ===Les processeurs Crusoe et Efficieon de Transmetta=== Les processeurs Crusoe Et Efficeron sont deux processeurs VLIW produits par la société Transmetta, une société californienne rachetée par NVIDIA. Ils étaient conçus pour exécuter spécifiquement des programmes x86, système d'exploitation inclus. L'idée derrière ce projet était d'exécuter du code x86 sans que les applications, ni même le système d'exploitation et le BIOS soient au courant ! Les processeurs Transmetta étaient en réalité conçus pour exécuter un programme unique, le ''Code Morphing Software'' (CMS), qui traduisait le code x86 en code VLIW. Transmetta ne voulait pas que son processeur VLIW soit exploité directement, sans le CMS. Il n'avait pas rendu public de compilateur pour traduire du C vers du code VLIW, il n'avait pas donné la documentation du jeu d'instruction VLIW. Des efforts de rétro-ingénieurie, documentés sur le site realworldtech, ont cependant permis de comprendre comment étaient encodées les instructions du processeur Crusoe. Comme pour le projet Daisy, le ''Code Morphing Software'' utilisait à la fois interprétation et traduction binaire, selon les besoins. Les instructions sont interprétées lors de leurs premières exécutions, mais le CMS bascule sur de la traduction en code VLIW après un certain nombre d'exécution. Ainsi, les boucles souvent exécutées sont traduites en code VLIW, alors que le reste du code est interprété. Le choix du code à traduire est le fait du CMS, il utilise des heuristiques complexes pour, qui ne sont pas connues dans le détail. Le ''Code Morphing Software'' est mémorisé dans une EEPROM, ce qui en fait un ''firmware'' situé en dessous du BIOS. Le CMS démarre ensuite le BIOS, qui lui-même démarre le système d'exploitation, qui lui-même démarre les pilotes de périphériques et les programmes. Le CMS se réserve les 16 premiers mébioctets de l'espace d'adressage. L'EEPROM du CMS est mappée dedans, mais n'en utilise que la moitié. Le reste est utilisé comme cache, pour mémoriser le code VLIW traduit par le CMS. Les deux processeurs intégraient aussi deux mémoires SRAM, utilisées par le CMS, appelées la ''local program memory'' (LPM) et la ''local data memory'' (LDM). La première contient du code qui gére les interruptions, la mémoire virtuelle, les exceptions matérielles, les problèmes d'alignement mémoire, et quelques fonctions très fréquemment utilisées par le CMS. La seconde est de la mémoire RAM utilisée par ces fonctions de la ''local program memory''. Les mémoires caches L1 et L2 sont séparées de ces deux mémoires. Le processeur Crusoe et Efficieon étaient des processeurs VLIW très simples. Ils n'avaient même pas de MMU, ni de fonctionnalités importantes sur les CPU x86. Pour Crusoe, les instructions VLIW étaient encodées sur 64 ou 128 bits, et regroupaient entre 2 à 4 opérations. Les instructions LOAD/STORE ne supportaient pas d'adressage indicé, juste de l'adressage indirect à registre. Crusoe avait 5 unités de calcul : deux ALU entières, une unité LOAD/STORE, une unité de branchement, une FPU. Efficieon doublait le nombre d'ALU entière et d'unité LOAD/STORE, les instructions VLIW passaient à 256 bits. Le processeur Crusoe contient 160 registres, dont 64 registres généraux, 32 registres flottants. Les registres généraux font 32 bits, ce qui est cohérent avec le fait que les CPU x86 de l'époque étaient des processeurs 32 bits. Quant aux registres flottants, ils faisaient 80 bits, ce qui colle avec la taille des registres flottants de la FPU x87 utilisée à l'époque. Sur les 64 registres généraux, seuls 48 étaient réellement utilisable pour mémoriser des opérandes. Une partie des registres généraux étaient utilisés pour la gestion de la pile, d'autres pour mémoriser l'état du CPU x86 émulé, un registre était un registre zéro non accesible en écriture. La gestion des exceptions est optimisée avec un système d'exceptions différées, le même que celui décrit plus haut. Le processeur utilise un système similaire, pour effectuer des lectures anticipées, qui a été expliqué dans le chapitre sur les processeurs VLIW/EPIC. Sauf qu'il s'agit là d'une optimisation pour gagner en performances en général, pas une aide utile pour gérer des exceptions précises ou toute autre subtilité de la traduction binaire. Pour gérer les exceptions différées et les lectures anticipées, le processeur mémorise l'état du processeur avant de démarrer un bloc de code. Pour cela, le processeur contient des ''shadow registers'', 48 registres entiers et 16 registres flottants. Vous remarquerez qu'il y a autant de registres entiers que de ''shadow registers'' entiers. Les registres entiers/flottants sont mémorisés dans les ''shadow registers'' avant d'exécuter un bloc de code, afin que le processeur puisse revenir à l'état de base. Une autre fonctionnalité liée est le ''gated store buffer''. Le principe est d'éviter toute écriture en mémoire RAM, tant que le code VLIW n'a pas émis d'instruction ''commit''. L'idée est que les écritures sont conservées dans la ''store queue'' de l'unité LOAD/STORE. Pour rappel, la ''store queue'' met en attente les écritures soit tant que les écritures ne sont pas terminées, soit tant que la RAM est occupée. L'idée est que les écritures sont accumulées dans la ''store queue'' tant que l'instruction ''commit'' n'a pas eu lieu. Le ''store queue'' est "déversé dans la RAM" seulement quand l'instruction ''commit'' s'exécute. ===Les CPU NVIDIA Denver=== Nvidia a tenté quelque chose de similaire avec son projet Denver. NVIDIA a racheté l'entreprise Transmetta, ses brevets, et a tenté de refaire la même chose. Le projet initial était de traduire du code x86 vers un jeu d'instruction VLIW propriétaire, comme l'a fait Transmetta. Mais NVIDIA n'a pas réussi à acquérir la licence du jeu d'instruction x86 et s'est rabattu sur le jeu d'instruction ARM. Le processeur né de ce projet est le Tegra K1-64 CPU. Il s'agit d'un processeur composé d'un cœur VLIW, couplé à deux décodeurs d'instructions ARM. Les deux décodeurs ARM traduisent une paire d'instructions ARM consécutives en instructions VLIW, pour les exécuter sur le cœur VLIW. Le CPU peut fonctionner selon deux modes : ARM et VLIW. En somme, le processeur supporte deux jeux d'instruction : le jeu d'instruction ARM, et un jeu d'instruction VLIW. En mode ARM, les instructions ARM sont chargées depuis le cache, traduites en VLIW par les décodeurs ARM, puis exécutées par le cœur VLIW. En mode optimisé, le CPU exécute des instructions VLIW chargées depuis le cache d'instruction, les décodeurs ne sont pas utilisés. : Le CPU supporte en réalité deux jeux d'instruction ARM : ARM8 et 7. Mais ils correspondent au même mode pour le CPU. Un point important est que le processeur peut changer très rapidement de mode, en à peine quelques cycles d'horloges. On n'est clairement pas dans le cas des CPU x86, qui mettent des plombes pour passer du mode 32 à 64 bits et inversement. La commutation est tellement rapide qu'on peut considérer que le processeur supporte deux jeux d'instruction simultanément. Et le support simultané de deux jeux d'instruction facilite l'implémentation de la traduction binaire. Encore une fois, inutile de traduire du code qui ne sera exécutée qu'une seule fois, mieux vaut l'exécuter directement dans le mode ARM. Surtout que le processeur supporte un mode ARM qui permet d'exécuter du code ARM sans perte de performance. Par contre, le code fréquemment utilisé, notamment dans des boucles critiques, est traduit en VLIW. Au final, les performances sont optimisées, en limitant le travail du logiciel traducteur binaire. La détection du code fréquemment exécuté se fait différemment que pour les processeurs Transmetta et Daisy. L'unité de branchement ne détecte pas directement le code beaucoup exécuté, même si elle a un rôle à jouer. A la place, la détection est réalisée en logiciel, par un ''thread'' dédié, qui s'exécute sur un cœur séparé. Il détecte le code fréquemment utilisé en regardent les branchements exécutés récemment. Pour cela, le processeur mémorise l'historique des branchements pris récemment et copie cette historique dans une mémoire tampon dédiée, partagée entre tous les cœurs. Vu qu'elle est partagée, le ''thread'' de détection a accès à l'historique sans pertes de performances, seule la copie de l'historique dans ce tampon a un cout en performance, pas son partage. Le code VLIW obtenu après traduction binaire est mémorisé dans la mémoire RAM, dans une portion spécialement réservée pour. Elle fait 128 mébioctets, et est appelée le '''cache d'optimisation''' par NVIDIA. Le cache d'optimisation est protégé en écriture et seul le traducteur binaire peut écrire dedans. Au passage, le traducteur binaire est du code VLIW, ce qui fait qu'il est placé dans le cache d'optimisation. Le processeur détecte automatiquement quand une fonction pour laquelle le code compilé équivalent est disponible. Pour cela, l'unité de chargement contient une table de correspondance entre l'adresse de la fonction ARM, et l'adresse de son équivalent compilé. Quand une fonction est appelée, l'unité de chargement regarde l'adresse de destination du branchement. Si l'adresse est dans cette table, elle récupère l'adresse de la fonction compilée et branche vers celle-ci. Le processeur dispose d'un cache d'instruction de 128 kibioctets, ce qui est très important, mais nécessaire vu la taille des instructions VLIW. Le coeur VLIW dispose de ses propres décodeurs, d'un ''scoreboard'', de 7 unités de calcul et de registres. Pour les unités de calcul, il a deux FPU, deux ALU, une unité de branchement, et deux unités LOAD-STORE faisant aussi ALU. Un point important est que le CPU dispose de 64 registres entiers et de 64 registres flottants. C'est deux fois plus que supporte le jeu d'instruction ARM. En mode ARM, seule la moitié des registres est utilisée. Mais en mode optimisé, le traducteur binaire utilise bien les 64 registres, grâce à une sorte de renommage de registres logicielle. Pour simplifier la traduction binaire, le processeur supporte les techniques vues précédemment. Le processeur intègre un ''gated store buffer'' similaire à celui des processeurs Transmetta. Les techniques d'exceptions différées sont aussi supportées, comme sur les processeurs Transmetta. Il a aussi une unité de préchargement décrite par NVIDIA comme agressive, avec support du préchargement de type anticipé (''runahead''). ===L'émulation de la mémoire virtuelle=== Pour finir, il faut parler de la mémoire virtuelle avec la traduction binaire. Sur le processeur source, le processeur gère à la fois des adresses physiques et virtuelles et sa MMU fait la conversion entre les deux. Sur le processeur VLIW, la MMU est simulée par le programme de traduction binaire, partiellement ou totalement. L'implémentation exacte varie suivant que l'on parle du projet Daisy, des processeurs Transmetta ou de Denver. Sur les processeurs Transmetta, il n'y a pas de MMU ni de mémoire virtuelle. A la place, le programme de traduction binaire émule la mémoire virtuelle du processeur source. Le processeur VLIW ne gère que des adresses physiques, rien d'autre. L'espace d'adressage physique du processeur VLIW a la même taille que celui du processeur émulé, ici des processeurs x86. Il est vraisemblable que les adresses physiques utilisées par le processeur x86 sont les mêmes que celles du processeur VLIW. Sur le projet Daisy, le processeur VLIW gère la mémoire virtuelle via pagination, comme les processeurs émulés. Ce qui fait qu'il gère un espace d'adressage virtuel et un espace d'adressage physique. Pour éviter toute confusion, nous parlerons d'adresse physique/virtuelle VLIW pour les adresses physiques/virtuelles du processeur VLIW, d'adresse virtuelle/physique source pour celle du jeu d'instruction traduit, à savoir du Power PC ou du s390, éventuellement de l'x86. Il faut alors faire le lien entre adresses physiques source et adresses virtuelles VLIW. Pour cela, rien de plus simple : il y a correspondance parfaite. L'adresse physique source numéro N correspond à l'adresse virtuelle VLIW numéro N. Cependant, cela signifie que tout l'espace d'adressage virtuel VLIW serait utilisé par le code à traduire. En réalité, il faut ajouter de la place pour le code traduit et le programme de traduction binaire. La conséquence est que l'espace d'adressage virtuel VLIW est plus large que l'espace d'adressage source. L'espace d'adressage virtuel VLIW est découpé en trois sections : une section pour le code Power PC à traduire, une autre pour le code traduit en VLIW, et une dernière réservée au traducteur binaire. Le programme de traduction est placé dans une ROM mappée en mémoire dans la seconde section. Le reste de la seconde section est réservé à une mémoire RAM utilisée par le programme de traduction. [[File:Mémoire virtuelle sur le projet Daisy.png|centre|vignette|upright=1.5|Mémoire virtuelle sur le projet Daisy]] Il faut noter que chaque page physique source, une fois traduite, correspond à N pages virtuelles VLIW. La raison est que le code traduit prend plus de place que le code machine originel. En conséquence, on doit utiliser une page finale plus grande. Pour le reste, des pages contiguës en mémoire physique source sont elles aussi contiguës en mémoire virtuelle VLIW. Le calcul d'adresse est donc simple : l'adresse physique source est multipliée par N, puis on ajoute l'adresse de base à laquelle commence la section pour le code traduit. ==La traduction binaire sur le Burroughs B1700== Nous venons de voir comment accélérer en matériel la traduction binaire, avec des techniques bien spécifiques. Le Burroughs B1700 a procédé autrement et il est intéressant d'étudier en détail son architecture. Son architecture est décrite dans le livre "Interpreting Machines : Architecture and Programming of the Bl700/Bl800 Series", écrit par Elliott I. Organick et James A. Hinds. Une partie de ce qui vfa suivre est un très court résumé de ce livre. ===Le Burroughs B1700 : le mal-nommé ''split-level control store''=== Le Burroughs B1700 avait plusieurs modèles : le B1726, le B1710 et le B1800. Ils se distinguaient par la manière dont le code machine source était traduit par le traducteur binaire. Dans ce qui suit, nous parlerons de traducteur binaire pour parler soit d'un interpréteur pour un langage de programmation de haut niveau, soit pour un logiciel de traduction binaire. Le B1710 plaquait le traducteur binaire en mémoire RAM. Le B1800 faisait pareil, mais avait ajouté un cache dédié au code du traducteur binaire, afin d'améliorer les performances. Le B1726 avait un traducteur binaire scindé en deux : une partie était en mémoire RAM, l'autre était dans un ''local store'' dédié. Le processeur peut exécuter le traducteur binaire soit depuis le ''local store'', soit depuis la mémoire RAM avec une pénalité en termes de performances. La documentation appelle cela un '''''split-level control store''''', mais le terme est trompeur : la documentation qualifie de microcode le langage machine du Burroughs B1700, ce qui est à l'origine de beaucoup de confusions. Une autre source de confusion est que ce langage machine a quelques propriétés qu'on pourrait attendre d'un microcode, mais certaines sont manquantes. Par exemple, il peut adresser les registres d’interfaçage mémoire, mais c'est parce que ce sont des registres architecturaux sur ce processeur. Ses instructions n'encodent pas de signaux de commande, par contre, elles font 16 bits et adressent les opérandes de manière implicite. Le ''local store'' mémorisait au maximum 2048 instructions machine, ce qui fait que les 2048 premières "adresses" correspondaient au ''local store''. Les suivantes étaient des adresses de mémoire RAM qui contenaient le reste du traducteur binaire. En pratique, l'adresse qui séparait les deux était configurable ! On pouvait limiter la taille du ''local store'' à un multiple de 32 instructions ! Les adresses restantes étaient alors réattribuées à la RAM. Par exemple, on pouvait attribuer 64 adresses au ''local store'', les 2048 - 64 adresses restantes étaient alors attribuées à la mémoire RAM. Un registre limite, nommé TDPM, indiquait quelle était l'adresse de démarcation entre les deux. Il contenait 4 bits, dont la valeur était multipliée par 32, pour obtenir l'adresse limite. Le programmeur devait découper le traducteur binaire en segments, certains étant placé dans le ''local store'', les autres étant en mémoire RAM. Mieux que ça, le processeur autorisait d'utiliser la technique de l'''overlaying'' pour le microcode ! Il y avait même une fonction OVERLAY dédiée pour ! OVERLAY avait besoin de trois opérandes : la taille du segment à copier, son adresse source en mémoire RAM, son adresse de destination dans la micro-SRAM. L'interpréteur réservait un "segment" en mémoire RAM, pour les données, dont le processeur connaissait la position. En-dehors de ce segment, il n'y a que des instructions machines à traduire, et le traducteur binaire. Le CPU incorporait pour cela deux registres, un pour l'adresse de base du segment de données, un autre pour son adresse de fin. Il incorporait une protection mémoire limitée sur ce segment, à savoir que seul l'interpréteur pouvait lire ou écrire dans ce segment. ===Une architecture bit-adressable=== Le CPU Burroughs B1700 était '''bit-adressable'''. En clair, il pouvait adresser la mémoire bit par bit, si nécessaire. Un avantage est que cela facilitait la traduction binaire des instructions machine, mais nous détaillerons cela plus bas. Un autre avantage est que cela permettait d'émuler des architectures dont la taille des registres/''byte''/mots était très différente. Et cet avantage mérite quelques explications immédiates. Le processeur était un CPU 24 bits, mais il pouvait émuler des processeurs 16 bits, 8 bits, 4 bits, 1 bit, 9 bits, ou toute autre valeur. Pour cela, le processeur lisait des mots de 24 bits et utilisant un ''barrel shifter'' pour sélectionner les bits adéquats. Par exemple, pour un accès de 16 bits, la donnée lue en mémoire était masquée de manière à ne garder que les 16 bits de poids faible. Le ''barrel shifter''/circuit de masquage était placé directement avant le bus mémoire, sur le trajet des données/instructions. Le ''barrel shifter'' était commandé par un registre ''Field Unit'', qui précisait quelle était la taille des données à charger. Pour communiquer avec la mémoire, il avait trois registres séparés : READ, WRITE, MAR. READ contient la donnée lue lors d'une lecture, WRITE est pour une donnée à écrire lors d’une écriture, MAR contient l'adresse à lire ou en cours de lecture. Il s'agit de registres d’interfaçage mémoire. Le fait que ces registres d’interfaçage mémoire et IO soit adressable est peu commun, et c'est plus quelque chose qu'on attend d'un microcode que d'instructions machines. Mais il faut bien comprendre que les registres d’interfaçage mémoire ont été promus au rang de registres architecturaux, sur ce processeur. De même, pour communiquer avec les entrées-sorties, il disposait de deux registres CMND et DATA : CMND pour envoyer une commande à une entrée-sortie, DATA pour lire ou écrire une donnée. Il y avait aussi un registre U, qui mémorisait une donnée lue depuis le lecteur de cassette. ===L'ALU de taille variable=== Les Burroughs B1700 intégrait une pile d'adresse de retour, capable de mémoriser 16 adresses de 24 bits. Il disposait de 4 registres généraux, nommés X, Y, T et L, tous de 24 bits. Un point important est que le registre T avait des possibilités d’extraction spécifiques. Il était possible d'extrait N bits, placés n'importe où dans ce registre, et de les copier dans un autre registre. Nous verrons pourquoi cette possibilité est intéressante et ne sert que pour le registre T. Ils avaient une '''ALU de taille variable'''. En clair, l'ALU pouvait faire des calculs sur un nombre de bits compris entre 0 et 24. Pour cela, les calculs étaient fait avec une ALU de 24 bits, puis les bits de poids fort inutiles étaient masqués. L'ALU de 24 bits pouvait masquer les bits de poids fort du résultat, mais avec des limitations. Elle gérait des opérandes et résultats de 0, 4, 8, 12, 16, 20 et 24 bits. Pour avoir un réglage plus fin, une ALU 4 bits séparée permettait de corriger le résultat. Les registres X et Y étaient reliés à une ALU de 24 bits capable d'effectuer des opérations arithmétiques basiques. Fait étonnant, l'ALU faisait tous les calculs en même temps et fournissait ses résultats dans 7 registres : un registre SUM pour l'addition, un registre DIFFERENCE pour la soustraction, et un registre par opération bit à bit. Les registres pour les opérations bit à bit sont : CMPX et CMPY pour l'opération NOT sur chaque opérande, XANY pour le ET bit à bit, XEOY pour le XOR, XORY pour le OU logique. Les additions et soustractions pouvaient se faire sur des opérandes codés en binaire ou en BCD, suivant le code opération utilisé. A cela, il fallait ajouter deux registres de "résultat" MSKX et MSKY. Ils fournissaient un masque dépendant de la taille du résultat en bits. C'est ce registre qui permettait de gérer des données de 0, 4, 8, 12, 16, 20, et 24 bits, alors que l'ALU faisait 24 bits. Le masque dans ce registre pouvait servir d'opérande dans une opération de masquage ultérieure, pour corriger le résultat. L'ALU prenait en opérande les registres X et Y, mais aussi un registre CYF pour la retenue entrante. En sortie, il fournissait aussi deux retenue sortantes : une pour l'addition et une autre pour la soustraction. Elles étaient mémorisées dans les registres CYL et CYD, respectivement. L'ALU fournissait aussi 12 conditions, qui alimentait un registre d'état. Le registre d'état était segmenté en trois registres appelés XYST, XYCN et BICN. Les registres X et Y sont des registres pour les opérandes, mais il y a aussi deux autres registres de 24 bits. Il s'agit des registres T et L, qui sont des registres généraux. Les registres T et L étaient découpés en 6 sous-registres de 4 bits, nommés LA LB LC LD LE et LF pour le registre L, et TA TB TC TD TE et TF pour le registre T. Ils étaient adressables comme n'importe quel registre. Le processeur contenait de nombreux autres registres de 4 bits, dont les sous-registres de T et L. En tout, il y a 27 registres de 4 bits adressables ! Et le processeur disposait d'une ALU 4 bits pour manipuler ces registres de 4 bits ! Elle est capable de faire des MOV entre registres de 4 bits, des opérations bit à bit, et peut aussi tester si un bit vaut 0 ou 1. La dernière possibilité est très utile pour implémenter les branchements. De plus, cela permettait de faire des opérations de masquage, notamment pour appliquer un masquage supplémentaire à celui de l'ALU. Par exemple, pour gérer des opérations sur 14 bits, on effectue une opération sur 16 bits avec l'ALU, et on masque les 2 bits manquants avec l'ALU 4 bits. La capacité d'extraction du registre T pouvait aussi être utilisé pour ça. ===Le code machine du Burroughs B1700=== Le processeur utilise des instructions de 16 bits de long, qui sont copiées dans registre d'instruction M de 16 bits. Fait intéressant, il était possible de faire un OU logique entre l'instruction lue et un registre du processeur, le résultat étant mémorisé dans le registre M. Cela permettait de faire du code automodifiant, pour émuler des modes d'adressage complexe. Cela permettait par exemple de remplacer une adresse dans une micro-opération. L'adresse est alors surimposée avec un OU dans le champ d'adresse, alors que le reste de l'instruction n'est pas modifié. Afin de gérer les interruptions, ces dernières étaient partiellement détectées en logiciel. Une interruption doit être prise en compte à la toute fin de l'exécution d'une instruction source, quand celle-ci a finit son travail. Mais vu qu'ici, les instructions sont émulées avec des séries d'instructions cibles, il y a un problème : il est possible d'interrompre une instruction émulée en plein milieu. Pour éviter cela, le processeur ne déclenche pas les interruptions matérielles immédiatement, elles sont mises en attente. Une instruction dédiée vérifie si une interruption est en attente, et l'exécute alors cas échéant. L'instruction en question est utilisée en dernier, dans la suite d'instruction qui émule l'instruction source. Le ''program counter'' est appelé le registre A. Les instructions du processeur font 16 bits de long, et elles sont alignées en RAM sur 16 bits, ce qui fait que les 4 bits de poids fort de ce registre valent 0, seuls les 14 bits de poids fort sont utiles. Les adresses mentionnées plus haut, qui adressent les micro-opérations en mémoire RAM, sont des adresses de mots de 16 bits, pas les adresses de bit individuels. ===La traduction binaire sur le Burroughs B1700=== Les instructions source peuvent faire 16 bit avec tel jeu d'instruction, 8 bits avec un autre, 32 bits sur un autre, etc. Elles peuvent aussi être de longueur fixe ou variable ! Le B1700 utilisait une architecture bit-adressable pour gérer ces contraintes. En effet, avec un CPU bit-adressable, les contraintes d'alignement des instructions disparaissent ! Dans ce qui suit, on suppose que l'instruction est composée de '''champs''' : l'opcode, les opérandes, les adresses absolues, les constantes immédiates, les numéros de registre, etc. L'idée est que l'instruction source est chargée dans le registre T, puis découpée en champs, qui sont copiés dans les registres du processeur ou envoyés au microcode. Je rappelle que le registre T est relié à un ''barrel shifter'', différent au précédent, qui permettait de masquer, décaler et extraire une donnée de ce registre. Et il y a la possibilité d'extraire une suite de bit de ce registre T, pour la copier dans un autre registre. Par exemple, prenons une instruction composée d'un opcode, d'une constante immédiate et d'une adresse immédiate. L'opcode est lu et interprété par le microcode. Le microcode extrait la constante immédiate du registre T et la copie dans le registre X. Ensuite, l'adresse est extraite et envoyée au registre FA, une lecture est démarrée, et l'opérande est copiée dans le registre Y. Enfin, l'instruction est exécutée par le microcode. Le tout prend plusieurs micro-opérations : une première pour lire l'opcode, une seconde pour lire la première opérande, une troisième pour lire la seconde opérande, una quatrième pour tout exécuter. Notons que suivant la taille des champs, on peut en charger plusieurs à la fois. Par exemple, imaginons qu'on ait chargé 24 bits, contenant un opcode de 8 bits et une constante immédiate de 16 bits : pas besoin de faire deux accès mémoire, le registre T est juste découpé en un opcode et une constante. Le registre T est fait pour ce genre d'extraction. L'architecture est bit-adressable car elle gére des champs qui ne font pas forcément 8 ou 16 bits. Par exemple, certains jeux d'instructions ont des opcodes codés sur 6 ou 7 bits, voire 9-10 bits. Par exemple, prenons un opcode de 7 bits, suivi par une adresse de 12 bits et une constante immédiate de 15 bits : aucun de ces champs ne sera aligné sur 8/16/24 bits. Utiliser une architecture bit-adressable règle le problème d'alignement. Et en général, les contraintes d'alignement des instructions disparaissent ! Quand on charge une instruction, peut importe qu'elle soit ou non alignée sur 24 bits, elle sera placée dans les bits de poids faible, il suffira de masquer les bits de poids fort inutiles. Par exemple, si on charge une instruction de 16 bits, les 16 bits seront copiés dans les 16 bits de poids faible du registre T, il suffira de masquer l'octet de poids fort. Pas besoin de faire de décalages pour gérer des instructions mal alignées... Pour charger une instruction, il faut connaitre plusieurs informations : l'adresse de son premier bit, sa taille. Pour cela, le B1700 contient deux registres, nommés ''Field Adress'' (FA) et ''Field Length'' (FL), qui indiquent l'adresse du premier bit et la taille de l'instruction à charger. Le premier fait 24 bits, ce qui permet de gérer des adresses de 24 bits. N'oubliez pas que le processeur est bit-adressable, ce qui permet d'adresser 16 Mébi-bits, soit 2 mébioctets. Le registre pour la taille fait lui 16 bits, ce qui permet de gérer des instructions de 65536 bits, soit 8192 octets ! Leur nom commence par ''field'', car le processeur peut gérer des instructions de taille variable et/ou plus longues que 24 bits. Dans ce cas, le chargement de l'instruction se fait champ par champ. Pour donner un exemple, prenons une instruction composée de 20 champs de 12 bits chacun. Les champs sont chargés un par un dans le registre T. Le registre FA est alors initialisé avec l'adresse de l'instruction, il est incrémenté de 12 à chaque cycle. Le registre FL est initialisé avec la taille de l'instruction, soit 20 * 12 = 240 bits, et il est décrémenté de 16 à chaque cycle. Précisons cependant que la taille d'un champ peut être changée d'un cycle à l'autre. Par exemple, le processeur peut charger un opcode de 8 bits, l'interpréter, détecter que l'instruction demande ensuite de charger une adresse de 16 bits. Dans ce cas, le registre FL est immédiatement altéré pour charger 16 bits, au lieu des 8 bits de l'opcode immédiatement précédent. ==Les jeux d'instructions dédiés à un langage de programmation== De rares processeurs sont conçus pour un langage de programmation en particulier. On appelle ces processeurs, conçus pour des besoins particuliers, des '''processeurs dédiés'''. Par exemple, les fameux ''Burrough E-mode'' B5000/B6000/B7000 étaient spécialement conçus pour exécuter de l'ALGOL-60. Leurs cousins B2000/B3000/B4000 étaient eux conçus pour le COBOL. Des langages fonctionnels ont aussi eu droit à leurs processeurs dédiés. Le prolog en est un bel exemple, avec les superordinateurs de 5ème génération qui lui étaient dédié. On peut aussi citer les machines LISP, dédiés au langage LISP, qui datent des années 1970. Elles étaient capables d’exécuter certaines fonctions de base du langage directement dans leurs circuits : elles possédaient notamment un ''garbage collector'' câblé dans ses circuits ainsi que des instructions machines supportant un typage déterminé à l’exécution. Les processeurs dédiés ont eu leur heure de gloire au début de l'informatique, à une époque où les langages de haut niveau venaient d'être inventés. À cette époque, les compilateurs n'étaient pas performants et ne savaient pas bien optimiser le code machine. Il était alors rationnel, pour l'époque, de rapprocher le code machine cible et le langage de programmation de haut niveau. De nombreuses architectures dédiés ont ainsi été inventées, avant que les concepteurs se rendent compte des défauts de cette approche. Les défauts en question ne sont pas nombreux, mais assez simples à comprendre. Premièrement, elles sont très rapides pour un langage de programmation en particulier, mais sont assez mauvaises pour les autres, d'où un problème de "compatibilité". Ajoutons à cela que les langages de programmation peuvent évoluer, devenir de moins en moins populaires/utilisés, ce qui rend la création d'architectures généralistes plus pertinente. Enfin, les architectures dédiées sont évidemment des processeurs CISC, pour implémenter les nombreuses fonctionnalités des langages évolués. Et les défauts des CISC sont assez rédhibitoires à l'heure actuelle. À l'heure actuelle, les algorithmes des compilateurs se sont améliorés et savent nettement mieux utiliser le matériel. Ils produisent du code machine efficace, ce qui rend les architecture dédiées bien moins intéressantes. Si on ajoute les défauts de ces architectures dédiées, par étonnant que les architectures dédiées aient presque disparues. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'accélération matérielle de la virtualisation | prevText=L'accélération matérielle de la virtualisation | next=Le matériel réseau | nextText=Le matériel réseau }} </noinclude> tnypx1xd2xrfjmnhkl0pz857fcrjbs6 764053 764052 2026-04-19T20:39:42Z Mewtow 31375 /* La traduction binaire sur le Burroughs B1700 */ 764053 wikitext text/x-wiki De nos jours, la majorité des programmeurs programment dans des langages de haut niveau. Il est très rare pour eux d'avoir à utiliser de l'assembleur, et encore moins un langage machine. Les programmes écrits dans un langage de haut niveau sont traduits en langage machine par un logiciel appelé le compilateur. [[File:Compilation.PNG|centre|vignette|upright=2.5|Compilateur : principe.]] Il se trouve que quelques rares processeurs sont conçus pour faciliter le travail du compilateur. Et ce sont ces processeurs que nous allons voir dans ce qui suit. Ils représentent une catégorie de jeux d'instruction à part, qui n'a pas vraiment de nom. ==La compilation et l’interprétation== Un compilateur traduit donc du code source, écrit dans un langage de haut niveau, vers du langage machine. La traduction est rarement directe. En général, le compilateur traduit le code source en assembleur, qui est lui-même traduit en langage machine. L'assembleur est une représentation textuelle du langage machine alors que le code machine est du binaire exécutable par le processeur. La traduction se fait donc en deux étapes, la '''compilation''' proprement dite et l''''assemblage''', réalisées respectivement par un compilateur et un assembleur. Il faut parfois rajouter une troisième phase d'édition des liens, que nous passons volontairement sous silence. {| |[[File:Code C.png|vignette|200px|Le code source, ici en C.]] |[[File:Assem.png|vignette|250px|Le code assembleur.]] |[[File:Binary file - hello world (C programming).png|vignette|150px|Le code machine.]] |} ===L'interprétation et la compilation à la volée=== Une alternative à la compilation est l''''interprétation''', qui transforme le code source en code machine à la volée. Avec l'interprétation, le code source est passé à un logiciel appelé l''''interpréteur''', qui exécute le code source ligne de code par ligne de code. Une ligne de code est traduite en code machine, qui est exécutée, puis l'interpréteur passe à la ligne suivante. [[File:Kaantotulkkaus.png|centre|vignette|upright=3|Compilation versus interprétation.]] Un défaut de l'interprétation est son cout en performance, qui est assez important. Aussi, l'interprétation stricte a évolué vers un hybride entre interprétation et compilation. L'idée est qu'une partie peu importante du code source est interprétée, alors que le code important est compilé juste avant d'être exécuté. Pour le dire autrement, le code source est partiellement compilé à la volée, juste avant son exécution. Aussi, on parle de '''compilation à la volée'''. Le terme en anglais est ''Just In Time compilation'', abrévié en JIT. En soi, le JIT est associé à de l'interprétation, les JIT sont en réalité des hybrides interpréteurs-compilateurs. Un point important est que compiler du code à la volée est assez lourd, cela a un cout en performance. Par contre, le code compilé s'exécute plus vite que du code interprété. Si on exécute le code compilé une seule fois, le cout de la compilation l'emporte sur le gain à l'exécution. Mais le code compilé est réutilisé autant de fois que nécessaire. Le gain à l'exécution est donc multiplié par le nombre d'exécutions, alors que le cout de la compilation est répartit sur plusieurs exécutions, il est amorti. Les portions du code sont choisies de manière à avoir un gain en performance maximal. L'idée générale est que le code exécuté très souvent est compilé, alors que du code exécuté pas souvent est interprété. Typiquement, les boucles les plus souvent exécutées sont compilées, le code exécuté une seule fois est interprété. ===Les langages intermédiaires=== Les compilateurs modernes passent par un '''langage intermédiaire''' pour faire la transformation en code machine. Le compilateur traduit le langage de haut niveau en langage intermédiaire, puis traduit le langage intermédiaire en code machine cible. Le langage intermédiaire est parfois appelé le '''''bytecode''''', et ce terme recouvre aussi le code écrit avec du ''bytecode''. L'interprétation aussi peut passer par l'intermédiaire d'un un ''bytecode'', ce n'est pas limité aux compilateurs proprement dit. {| |[[File:Intermediate representation scheme.png|centre|vignette|upright=1.5|Représentation intermédiaire d'un compilateur.]] |[[File:Code interpreter scheme.png|centre|vignette|upright=1.5|Fonctionnement d'un interpréteur.]] |} Faire ainsi a de nombreux avantages, le principal étant d'avoir un compilateur capable de traduire un langage de haut niveau vers plusieurs jeux d'instructions différents. Cela permet d'avoir, par exemple, un compilateur qui traduit du C soit en code machine x86, soit en code machine pour un CPU ARM, soit pour un CPU POWERPC, etc. [[File:Compiler design.svg|centre|vignette|upright=3|Compiler design]] Pour l'interprétation, l'usage d'un ''bytecode'' a d'autres avantages. Pour l'exploiter, il faut distribuer non pas le code source, mais le ''bytecode''. En clair, l'interpréteur ne prend pas en entrée le code source, mais du ''bytecode'' déjà compilé. La traduction est alors beaucoup plus simple, car le code source a déjà été partiellement compilé, par la traduction en ''bytecode''. Interpréter du code source est en effet assez compliqué : il faut effectuer des étapes d'analyse lexicale, sémantique, et bien d'autres. Avec du ''bytecode'', ces étapes ont été réalisées lors de la compilation du ''bytecode'', l'interpréteur a alors peut de choses à faire. L'interprétation du ''bytecode'' se fait instruction par instruction, au niveau du ''bytecode''. Une instruction du ''bytecode'' est traduite en une instruction machine équivalente, qui est exécutée. Si l'instruction est un peu complexe, il exécute une fonction/procédure qui fait la même chose. Pour résumer, le ''bytecode'' est ensuite traduit à la volée et exécuté instruction par instruction par un logiciel appelé l'interpréteur. [[File:Control table.png|centre|vignette|upright=2|Control table]] Le langage intermédiaire peut être vu comme l'assembleur d'un processeur, n'existe pas forcément dans la réalité, mais dont le jeu d'instruction est décrit en détail. Le processeur en question est appelé une '''machine abstraite''', ou encore une ''machine virtuelle''. Nous utiliserons le terme de machine abstraite dans ce qui suit. La machine abstraite n'est pas la même suivant que l'on cible la compilation ou l'interprétation/JIT. Pour l'interprétation/JIT, la machine abstraite est souvent une machine à pile, car cela simplifie grandement la traduction en code machine final. L'interprétation demande que la traduction du ''bytecode'' en code machine soit la plus simple possible. Et cela demande de prendre en compte pas mal de détails. Par exemple, les différents jeux d'instruction existants n'ont pas le même nombre de registres, ce qui pose problème lors de la traduction du ''bytecode''. Lors de la transformation en code machine, un algorithme d'allocation de registres se débrouille pour traduire le code intermédiaire en code qui utilise un nombre limité de registres. Pour améliorer cette allocation de registres, il y a deux solutions niveau ''bytecode'' : soit utiliser un nombre illimité de registres, soit utiliser une machine à pile. Pour les machines à pile, il existe un algorithme simple et rapide pour traduire un code écrit pour une machine à pile en un code écrit pour un processeur avec des registres, qui se débrouille pas trop mal pour allouer efficacement les registres. C'est un avantage assez important pour les langages interprétés. ==Les processeurs qui exécutent du bytecode== Le ''bytecode'' est un code machine, ce qui signifie qu'il peut en théorie s'exécuter sur un processeur qui implémente le jeu d’instruction associé. Si le jeu d'instruction d'un ''bytecode'' est souvent une description censée être fictive, elle n'en reste pas moins un jeu d'instruction et des caractéristiques précises, qu'on peut l'implémenter en matériel ! Néanmoins, tous les ''bytecode'' ne sont pas égaux de ce point de vue. Certains sont faciles à implémenter en matériel, d'autres non. Les ''bytecodes'' implémentés en matériel sont ceux dont les machines abstraites sont des machines à pile. Un premier exemple est celui des processeurs Pascal MicroEngine, qui exécutaient directement le bytecode du langage Pascal, le fameux UCSD P-code. Un second exemple est la '''machine SECD''', qui sert de langage intermédiaire pour certains compilateurs de langages fonctionnels. Elle a été implémentée en matériel par plusieurs équipes, notamment par les chercheurs de l'université de Calgary en 1989. Dans le même genre, quelques processeurs simples étaient capables d’exécuter directement le ''bytecode'' du langage FORTH. Le FORTH, un des premiers langages à pile de haut niveau, possède de nombreuses implémentations hardware et est un des rares langages de haut niveau dont le ''bytecode'' a été utilisé comme langage machine sur certains processeurs. Par exemple, on peut citer le processeur FC16, capable d’exécuter nativement du ''bytecode'' FORTH. ===Les processeurs Java=== Le cas le plus impressionnant est celui de la machine virtuelle Java, qui est un design de processeur comme un autre. En temps normal, le ''bytecode'' Java est compilé ou interprété, mais certains processeurs exécutaient du ''bytecode'' Java directement, sans interprétation ni compilation. Ils sont appelés des '''processeurs Java'''. On peut citer les processeurs ARM disposant de l'extension Jazelle, du JEMCore et du aJ-100 de aJile Systems, le picoJava II de Sun, et quelques autres. Il y a aussi eu quelques projets de recherche ou processeurs open source, comme Komodo, jamuth, le ''Java Optimized Processor'' et quelques autres projets du même genre. L'implémentation de la machine virtuelle Java n'était cependant pas complète. Quelques instructions complexes n'étaient pas gérées par le processeur et devaient être émulées en logiciel. Mais la grosse majorité des instructions l'était. Les instructions non-supportées étaient émulées avec la méthode ''trap and emulate'', à savoir qu'un opcode inconnu déclenchait une exception matérielle ''opcode invalide'', dont la routine pouvait émuler les instructions qui doivent l'être. Le processeur aJ-100 de aJile est capable d'exécuter la quasi-totalité des instructions de la JVM Java, seules deux instructions faisant exception. Les '''accélérateurs Java''' sont une solution intermédiaire, qui permet de traduire à la volée du ''bytecode'' Java en instructions RISC, exécutées sur un autre processeur. Il s'agit formellement de traduction binaire, mais je préfère en parler ici. L'idée est de combiner un processeur RISC avec un coprocesseur Java. Le coprocesseur lit le ''bytecode'' Java et le traduit en instructions RISC, ces dernières étant exécutées sur le processeur RISC. Le JA108 de Nozomi était un coprocesseur de ce genre, au même titre que le coprocesseur JSTAR de JEDI. L'extension Jazelle de certains CPU ARM fonctionnait vraisemblablement sur un principe similaire, sauf que le coprocesseur était intégré dans le processeur ARM, entre le cache d'instruction et le décodeur d'instructions ARM. L'intérêt des processeurs Java n'est pas qu'une question de performance. Certes, ils permettent d’exécuter plus vite les programmes compilés en ''bytecode'', comme des programmes Java pour la JVM Java. Mais l'intérêt est aussi de faire des économies de mémoire RAM et ROM. Je rappelle que la machine virtuelle Java est une machine à pile, ce qui fait que sa densité de code est excellente. Les programmes en ''bytecode'' Java sont donc très petits, et prennent peu de place en mémoire ROM. De plus, utiliser un processeur Java permet de se passer d'interpréteur ou de JIT Java, ce qui économise encore plus de mémoire. ===Transformer un processeur RISC en machine à pile=== La plupart des processeurs Java utilisent en interne une architecture à registre généraux, qui émule une machine à registre en microcode. Quelques registres sont réservés pour l'état de la machine virtuelle, avec au minimum un registre réservé pour le pointeur de pile, un autre pour un pointeur vers un ''pool'' de constantes, et quelques autres. D'autres registres servent à mémoriser le haut de la pile, à savoir le sommet de la pile et quelques opérandes situées juste en-dessous. Par exemple, le processeur picoJava II gardait les 64 opérandes au sommet de la pile dans 64 registres généraux, séparés des registres pour les pointeurs et l'état de la JVM. Une optimisation possible, envisagée sur les processeurs picoJava, était une sorte de macro-fusion sous stéroïde. L'idée était de fusionner certaines séries d'instructions Java en une seule instruction machine. Typiquement, pour faire une opération arithmétique, une machine à pile empile deux opérandes et exécute une opération, ce qui prend trois instructions machine. La spécification picoJava 1 a proposé d'envoyer directement la seconde opérande en entrée de l'ALU, ce qui fusionnait l'empilement de la seconde opérande et l'opération arithmétique. Le processeur picoJava II va plus loin et fusionne les trois instructions en une seule instruction RISC, agissant sur des registres. Mais la majorité des processeurs Java ne faisaient pas cela, car cela complexifie grandement le décodeur. Les processeurs ''bigfoot'' de DCT utilisaient un système différent pour émuler une pile avec des registres. Leur banc de registre de 64 bits était coupé en deux : les 16 premiers registres étaient utilisés pour la pile, les 48 suivants étaient utilisés pour autre chose. L'idée est que le registre 0 mémorise un emplacement vide, le registre 1 contient le sommet de la pile, le registre 2 l'opérande sous le sommet de la pile, et ainsi de suite. Les 15 premières opérandes de la pile sont donc mémorisées dans les registres. Les instructions écrivent leur résultat dans le registre adéquat, puis tous les registres sont décalés pour que le résultat passe dans le registre R1. Par exemple, les instructions PUSH chargent une opérande dans le registre 0, puis tous les registres sont décalés de un rang, pour que l'opérande chargée soit dans le registre 1. Le registre 0 devient alors le registre numéro 1, le registre 1 devient le 2, et ainsi de suite. Par contre, les opérations arithmétiques font autrement. Une instruction d'addition, par exemple, lit les opérandes dans le registre 1 et 2, puis place son résultat dans le registre 2 (les deux opérandes sont dépilées). Puis, les registres sont décalés d'un rang pour que ce registre 2 devienne le 1. Les registres ne sont en réalité pas décalés, il n'y a pas de transferts entre registres. A la place, les 16 premiers registres utilisaient une forme limitée de renommage de registres, similaire au banc de registre tournant utilisé sur les processeurs EPIC comme l'Itanium. Les numéros de registres sont renommés, décalés, à chaque fois qu'on empile ou dépile une opérande. Pour faire le renommage de registres, processeur contient un ''register stack counter'' de 4 bits, qui est incrémenté à chaque fois qu'on empile une donnée, et est décrémenté quand on dépile une donnée. Quand on veut accéder à un opérande, le numéro de registre architectural associé est traduit en numéro de registre physique, en additionnant le ''register stack counter'' (modulo 16, pour rester sur 4 bits). ==L'accélération de l'interprétation/JIT : le ''Thumb-EE'' d'ARM== Quelques processeurs ont ajouté des instructions pour faciliter le travail des interpréteurs/JIT. Par exemple, en 2005, ARM a ajouté le mode ''Thumb Execution Environment'', qui faisait cela. Il reprenait le jeu d'instruction compact ''thumb'' et ajoutait quelques instructions et modifiait le comportement d'instructions existantes. Le jeu d'instruction ''Thumb-EE'' a été ajouté sur les CPU ARM en 2005, mais a été déprécié en 2011, par manque d'utilité. Mais la tentative mérite qu'on s'attarde dessus. Les CPU de gamme M, déstinés à l'embarqué, n'ont jamais supporté ''Thumb-EE'', vu que de tels processeurs ne sont pas conçus pour exécuter du code interprété/JIT. Le mode ''Thumb-EE'' est un mode d'exécution, séparé de l'ARM normal. Ainsi, une instruction ''Thumb-EE'' et une instruction ARM peuvent avoir le même encodage en binaire, mais se comporter différemment. Le processeur est à tout moment soit en mode ''Thumb-EE'', soit en mode ARM normal, ce qui précise comment ces instructions doivent se comporter. Des instructions ont été ajoutées par ''Thumb-EE'', pour qu'il fasse son travail. L'entrée et la sortie du mode ''Thumb-EE'' se fait avec deux instructions : ENTERX et LEAVEX. Une fois en mode ''Thumb-EE'', le décodage des instructions ''thumb'' se fait avec les règles du ''Thumb-EE'', et non celles du ''thumb'' normal. Une première différence entre ''thumb'' normal et ''Thumb-EE'' est l'ajout de ce qui s'appelle un ''null check'' pour les instructions mémoire. Le ''null check'' est utilisé pour les instructions en mode d'adressage "base + indice" ou "Base + décalage", ou tout autre mode d'adressage avec un registre de base utilisés dans des calculs d'adresse. L'idée est de vérifier si l'adresse de base vaut zéro ou non. Si c'est le cas, le processeur lève une exception matérielle, qui est traitée par l'interprétation ou le compilateur JIT. Une telle situation est en effet signe d'une erreur d'adressage mémoire, qui doit être traitée par l'interpréteur. Typiquement, c'est signe qu'un pointeur n'a pas bien été initialisé, ce qui peut arriver avec du code interprété ou JIT. Les interpréteurs/JIT traitent généralement la situation en ajoutant un test avant toute instruction mémoire, pour vérifier si le pointeur accédé vaut zéro ou non. Avec ''Thumb-EE'', pas besoin d'ajouter les tests en questions : ils sont réalisés automatiquement lors de chaque instruction mémoire. : Les registres pour la pile, comme le pointeur de pile et le pointeur de ''frame'', sont aussi concernés. L'instruction CHKA vérifie que les accès à un tableau ne débordent pas en dehors du tableau en question. Par exemple, pour un tableau de 1024 éléments, elle vérifie si l'indice est compris entre 0 et 1024. Pour cela, elle vérifie si l'indice de tableau est bien dans l'intervalle adéquat et lève une exception matérielle si ce n'est pas le cas. Son utilité se comprend quand on sait que tous les accès à un tableau sont vérifiés par l'interpréteur. L'interpréteur est censé ajouter des instructions pour vérifier les indices, à savoir deux ou trois branchements. Avec CHKA, le test se fait en une seule instruction. D'autres modifications mineures de l'encodage des instructions sont aussi présentes, ainsi que des modifications pour les instructions LOAD/STORE. Précisément, dans les modes "base + indice" et "base + décalage", le décalage et l'indice subissent maintenant des décalages. L'idée est de simplifier le calcul d'adresse. De quoi économiser quelques instructions lors des calculs d'adresse, ce qui facilite le travail de l'interpréteur/JIT. Et des instructions LOAD/STORE précises voient leur comportement modifié pour faciliter la gestion de la pile de Java ou l'exécution des méthodes locales. Pour faciliter le travail de l'interpréteur, deux instructions HBP et HBLP ont été ajoutées. Elles branchent vers une fonction qui gère les exceptions logicielles (une fonctionnalité présente dans de nombreux langages, comme Java). La première effectue un branchement vers la fonction en question, la seconde sauvegarde l'adresse de retour avant de faire ce branchement. Les deux peuvent brancher vers 256 fonctions pré-déterminées, sans avoir à présenter leur adresse. ==La traduction binaire accélérée par le matériel== Plus haut, nous avons surtout parlé des compilateurs et des interpréteurs. Cependant, nous devons aussi parler de la '''traduction binaire'''. Elle traduit un programme écrit dans un code machine vers un autre code machine. Par exemple, elle traduit un programme compilé pour un CPU x86 vers un code machine ARM. En général, le système d'exploitation est généralement compilé pour le jeu d'instruction natif, mais il exécute des applications prévues pour le x86. Les applications sont traduites par le système d'exploitation, avant d'exécuter le code traduit. : Nous parlerons dans la suite de code machine source et de code machine cible, pour parler respectivement du code à traduire et du code obtenu après traduction. Idem avec d'autres termes comme architecture cible/source, ou jeu d'instruction cible/source. La traduction binaire est surtout utilisée pour des questions d'émulation ou de compatibilité. Par exemple, lorsque les Macintosh sont passés de processeurs Power PC vers des processeurs x86, le système d'exploitation Mac OS utilisait la traduction binaire pour convertir les anciennes applications Power PC vers du code x86. Les utilisateurs n'y ont vu que du feu. Le système de traduction binaire était appelé Rosetta 1. Par la suite, lors de la transition de processeurs x86 vers des processeurs Apple, Rosetta 2 a vu le jour. La traduction binaire a surtout été utilisée pour traduire du code x86 vers un autre jeu d'instruction. Il faut dire que le x86 est le jeu d'instruction dominant. De nombreuses entreprises ont eu pour ambition de briser l'hégémonie du x86 sur PC, en remplaçant le x86 par un jeu d'instruction plus performant, tout en gardant une compatibilité maximale. La traduction binaire était la seule solution pratique. Elle était le plus souvent intégralement réalisée en logiciel, comme c'était le cas sur les architectures Itanium avec le ''IA-32 Execution Layer''. ===Les généralités sur la traduction binaire assistée en matériel=== L'architecture source est presque tout le temps une architecture CISC, assez ancienne, qu'on souhaite émuler. Le choix de l'architecture cible se porte souvent sur une architecture VLIW, fort différente de l'architecture source, et ce pour obtenir une économie de matériel conséquente. Pour s'exécuter rapidement, le code traduit doit exploiter le parallélisme d'instruction, à savoir exécuter plusieurs instructions en même temps dans des unités de calcul séparées. Il est possible d'utiliser un processeur superscalaire avec exécution dans le désordre pour cela, mais au prix d'un cout important en transistors. Alors qu'en utilisant un CPU VLIW, c'est le traducteur binaire qui fait tout le travail d'extraction du parallélisme d'instruction. Et après tout, quitte à avoir un traducteur binaire, autant lui refiler le boulot d'optimisation. Compiler du code à la volée est certes assez lourd, mais qu'il existe des algorithmes efficaces pour regrouper des instructions indépendantes dans une seule instruction VLIW. Au passage, le processeur VLIW a plus de registres que le processeur source. Cela permet de faire du renommage de registres directement en logiciel. Le traducteur binaire n'hésite pas à changer les noms de registres entre instructions source et instruction VLIW cible, afin de supprimer des dépendances de données. En conséquence, cela ouvre des opportunités de parallélisme, qui sont exploitées lors du regroupement des opérations en instructions VLIW. Vous pourriez penser que le choix d'un CPU VLIW pour émuler du CISC est tout sauf optimal. Vous devez penser que la traduction binaire est d'autant plus simple que l'architecture source et cibles sont semblables. Dans les faits, ce n'est pas tellement traduire les instructions qui pose problème, mais plus la gestion du registre d'état, des exceptions matérielles, de la mémoire virtuelle, la différence entre gros-boutisme et petit-boutisme, de même que des différences d'adressage pour les périphériques. Le premier problème est la gestion des conditions, notamment en présence d'un registre d'état. En pratique, la traduction binaire s'utilise pour traduire du code CISC vers du code RISC ou VLIW. Les architectures source ont donc un registre d'état, qui est mis à jour non seulement par des instructions de test, mais aussi des instructions arithmétiques. L'architecture cible n'a elle pas de registre d'état, mais des registres à prédicats. La traduction de l'un vers l'autre est alors quelque peu compliquée. Et elle est d'autant plus compliquée que le registre d'état est la source de dépendances d'instruction implicites, qui réduisent les performances. Par exemple, si une instruction arithmétique modifie le registre d'état, cela peut impacter l'exécution d'une instruction ultérieure, qui lit ce registre d'état. Pour éliminer ces fausses dépendances, le traducteur binaire doit renommer ce registre en logiciel et détecter les dépendances utiles. Et le compilateur doit gérer les cas où il y a beaucoup de distance entre les deux, voire les cas où l'instruction dépendante n'a pas encore été analysée par le compilateur. Un autre problème est lié à la gestion des exceptions matérielles, et précisément des exceptions précises. Pour rappel, une instruction VLIW regroupe plusieurs opérations, opérations qui correspondraient à une instruction machine sur un CPU pas VLIW. En conséquence, plusieurs instructions du langage machine "source" sont regroupées en une seule instruction VLIW. Et il faut tenir compte du cas où une instruction source lève une exception. Dans le code VLIW, cela signifie qu'une opération lève une exception, et il faut annuler partiellement l'instruction VLIW associée. Et par partiellement, on veut dire que seules les opérations suivantes dans l'ordre du programme source doivent être annulées, puis ré-exécutées. Et c'est un sacré casse-tête ! Les processeurs VLIW utilisent comme solution le mécanisme d'exceptions différées des processeurs EPIC, vu il y a quelques chapitres. Pour résumer, le code est exécuté par blocs d'instruction, délimités par des branchements ou tout autre limite/barrière pertinente dans le code. Un bloc de code est compilé en deux versions : une version rapide sans exceptions matérielles, une version lente qui gère les exceptions précises. La version rapide s'exécute sans exécuter les exceptions. Cependant, les exceptions matérielles sont enregistrées, pour être prises en compte à la toute fin du bloc de code. La version rapide est exécutée en premier et elle mémorise si une exception matérielle a eu lieu. Puis une instruction vérifie si une exception a eu lieu et décide quoi faire. Si aucune exception n'a eu lieu, elle passe à la suite du programme, les résultats du bloc de code sont définitivement acceptés. Mais si une exception a eu lieu, tout est annulé. Le processeur est remis dans l'état initial, puis le code est ré-exécuté instruction par instruction de manière à gérer l'exception correctement. ===La traduction par pré-décodage=== Une première solution serait de faire la traduction binaire dans le cache d'instruction. Lors d'un défaut de cache, le code chargé depuis la RAM est traduit en code machine cible. Et c'est ce code machine cible qui est mémorisé dans le cache d'instruction et exécuté par le processeur. La technique marche sur le papier et n'est qu'une amélioration des techniques de pré-décodage vues il y a quelques chapitres. Cependant, le code obtenu est une traduction assez basique, qui n'incorpore pas d'optimisations dignes de ce nom. De plus, elle gère mal le cas où la taille du code source et cible sont potentiellement très différentes. Concrètement, elle est surtout utile pour traduire du code d'un processeur RISC vers un autre processeur RISC, dont les tailles d'instructions sont similaires. Elle ne permet pas d'émuler plusieurs jeux d'instructions différents, le cout en matériel (un circuit de pré-décodage par jeu d'instruction) serait trop important. ===Le projet DAISY d'IBM=== De très rares processeurs étaient conçus pour accélérer cette traduction binaire, afin de garder de bonnes performances. Les premiers à avoir étudié l'idée étaient IBM, avec leur projet DAISY (''Dynamically Architected Instruction Set from Yorktown''). Le projet de base était de convertir à la volée du code compilé pour des CPU Power PC, vers du code VLIW. Les chercheurs d'IBM avaient développé un algorithme de traduction dynamique efficace, ainsi qu'un processeur VLIW disposant d'optimisations spécifiques à la traduction binaire dynamique, à la volée. De nombreuses idées de ce projet ont été reprises ou re-découvertes par la société Transmetta, avec ses processeurs Crusoe et Efficieon, puis par NVIDIA avec son projet Denver. Les ingénieurs de ce projet ont étudié la possibilité d'émuler plusieurs jeux d'instructions différents sur un même processeur, notamment le s390 d'IBM et le x86. En théorie, cela demande juste d'avoir plusieurs programmes de traduction binaire : un pour le x86, un autre pour le s390, éventuellement un autre pour le Power PC. Mais cela ne s'est pas concrétisé. Mais les ingénieurs ont étudié quelle pourrait être l'architecture VLIW idéale pour ça. Par exemple, il fallait des additions 3-opérandes pour simplifier les calculs d'adresse. Une difficulté était la gestion du registre d'état, dont les bits ne sont pas mis à jour de la même manière sur le x86 et le s390 ou le Power PC. La différence entre gros-boutisme et petit-boutisme était aussi un problème, de même que des différences d'adressage pour les périphériques. Pour que la traduction binaire soit efficace, le processeur VLIW intègre diverses optimisations, comme les exceptions différées et les branchements multi-voies. La principale est la suivante : seul le code des boucles ou fonctions exécutées fréquemment est traduit en code VLIW, le reste du code est interprété. En effet, traduire du code binaire prend plus de temps qu'une simple interprétation. Pour du code qui ne sera exécutée qu'une seule fois, il est plus rapide d'utiliser l'interprétation. Par contre, pour du code exécuté beaucoup de fois, le cout de la traduction binaire est amorti, dilué sur N exécutions, compensé par le gain en temps d'exécution de ce code traduit. Au final, cela permet de ne traduire que le code qui le mérite. La détection du code fréquemment exécuté est réalisée dans le cœur VLIW. L'unité de branchement mémorise les derniers branchements rencontrés et le nombre de fois qu'ils ont été exécutés. Si ils ont été exécutés un certain nombre de fois, l'unité de branchement lève une exception matérielle, qui invoque le traducteur binaire. La routine de cette exception est le traducteur binaire proprement dit. En clair, la traduction binaire est démarrée quand le processeur détecte qu'une fonction a été exécutée plus de N fois, via une exception matérielle dédiée. La mesure du nombre d'exécution d'un branchement se fait dans l'unité de calcul dédiée aux branchements, avec l'aide d'une sorte de ''branch adress buffer'' modifié. Pour rappel, le ''branch adress buffer'' mémorise le ''Program counter'' de chaque branchement récemment rencontré. Ici, chaque entrée du ''branch adress buffer'' est associée à un compteur incrémenté à chaque exécution du branchement. ===Les processeurs Crusoe et Efficieon de Transmetta=== Les processeurs Crusoe Et Efficeron sont deux processeurs VLIW produits par la société Transmetta, une société californienne rachetée par NVIDIA. Ils étaient conçus pour exécuter spécifiquement des programmes x86, système d'exploitation inclus. L'idée derrière ce projet était d'exécuter du code x86 sans que les applications, ni même le système d'exploitation et le BIOS soient au courant ! Les processeurs Transmetta étaient en réalité conçus pour exécuter un programme unique, le ''Code Morphing Software'' (CMS), qui traduisait le code x86 en code VLIW. Transmetta ne voulait pas que son processeur VLIW soit exploité directement, sans le CMS. Il n'avait pas rendu public de compilateur pour traduire du C vers du code VLIW, il n'avait pas donné la documentation du jeu d'instruction VLIW. Des efforts de rétro-ingénieurie, documentés sur le site realworldtech, ont cependant permis de comprendre comment étaient encodées les instructions du processeur Crusoe. Comme pour le projet Daisy, le ''Code Morphing Software'' utilisait à la fois interprétation et traduction binaire, selon les besoins. Les instructions sont interprétées lors de leurs premières exécutions, mais le CMS bascule sur de la traduction en code VLIW après un certain nombre d'exécution. Ainsi, les boucles souvent exécutées sont traduites en code VLIW, alors que le reste du code est interprété. Le choix du code à traduire est le fait du CMS, il utilise des heuristiques complexes pour, qui ne sont pas connues dans le détail. Le ''Code Morphing Software'' est mémorisé dans une EEPROM, ce qui en fait un ''firmware'' situé en dessous du BIOS. Le CMS démarre ensuite le BIOS, qui lui-même démarre le système d'exploitation, qui lui-même démarre les pilotes de périphériques et les programmes. Le CMS se réserve les 16 premiers mébioctets de l'espace d'adressage. L'EEPROM du CMS est mappée dedans, mais n'en utilise que la moitié. Le reste est utilisé comme cache, pour mémoriser le code VLIW traduit par le CMS. Les deux processeurs intégraient aussi deux mémoires SRAM, utilisées par le CMS, appelées la ''local program memory'' (LPM) et la ''local data memory'' (LDM). La première contient du code qui gére les interruptions, la mémoire virtuelle, les exceptions matérielles, les problèmes d'alignement mémoire, et quelques fonctions très fréquemment utilisées par le CMS. La seconde est de la mémoire RAM utilisée par ces fonctions de la ''local program memory''. Les mémoires caches L1 et L2 sont séparées de ces deux mémoires. Le processeur Crusoe et Efficieon étaient des processeurs VLIW très simples. Ils n'avaient même pas de MMU, ni de fonctionnalités importantes sur les CPU x86. Pour Crusoe, les instructions VLIW étaient encodées sur 64 ou 128 bits, et regroupaient entre 2 à 4 opérations. Les instructions LOAD/STORE ne supportaient pas d'adressage indicé, juste de l'adressage indirect à registre. Crusoe avait 5 unités de calcul : deux ALU entières, une unité LOAD/STORE, une unité de branchement, une FPU. Efficieon doublait le nombre d'ALU entière et d'unité LOAD/STORE, les instructions VLIW passaient à 256 bits. Le processeur Crusoe contient 160 registres, dont 64 registres généraux, 32 registres flottants. Les registres généraux font 32 bits, ce qui est cohérent avec le fait que les CPU x86 de l'époque étaient des processeurs 32 bits. Quant aux registres flottants, ils faisaient 80 bits, ce qui colle avec la taille des registres flottants de la FPU x87 utilisée à l'époque. Sur les 64 registres généraux, seuls 48 étaient réellement utilisable pour mémoriser des opérandes. Une partie des registres généraux étaient utilisés pour la gestion de la pile, d'autres pour mémoriser l'état du CPU x86 émulé, un registre était un registre zéro non accesible en écriture. La gestion des exceptions est optimisée avec un système d'exceptions différées, le même que celui décrit plus haut. Le processeur utilise un système similaire, pour effectuer des lectures anticipées, qui a été expliqué dans le chapitre sur les processeurs VLIW/EPIC. Sauf qu'il s'agit là d'une optimisation pour gagner en performances en général, pas une aide utile pour gérer des exceptions précises ou toute autre subtilité de la traduction binaire. Pour gérer les exceptions différées et les lectures anticipées, le processeur mémorise l'état du processeur avant de démarrer un bloc de code. Pour cela, le processeur contient des ''shadow registers'', 48 registres entiers et 16 registres flottants. Vous remarquerez qu'il y a autant de registres entiers que de ''shadow registers'' entiers. Les registres entiers/flottants sont mémorisés dans les ''shadow registers'' avant d'exécuter un bloc de code, afin que le processeur puisse revenir à l'état de base. Une autre fonctionnalité liée est le ''gated store buffer''. Le principe est d'éviter toute écriture en mémoire RAM, tant que le code VLIW n'a pas émis d'instruction ''commit''. L'idée est que les écritures sont conservées dans la ''store queue'' de l'unité LOAD/STORE. Pour rappel, la ''store queue'' met en attente les écritures soit tant que les écritures ne sont pas terminées, soit tant que la RAM est occupée. L'idée est que les écritures sont accumulées dans la ''store queue'' tant que l'instruction ''commit'' n'a pas eu lieu. Le ''store queue'' est "déversé dans la RAM" seulement quand l'instruction ''commit'' s'exécute. ===Les CPU NVIDIA Denver=== Nvidia a tenté quelque chose de similaire avec son projet Denver. NVIDIA a racheté l'entreprise Transmetta, ses brevets, et a tenté de refaire la même chose. Le projet initial était de traduire du code x86 vers un jeu d'instruction VLIW propriétaire, comme l'a fait Transmetta. Mais NVIDIA n'a pas réussi à acquérir la licence du jeu d'instruction x86 et s'est rabattu sur le jeu d'instruction ARM. Le processeur né de ce projet est le Tegra K1-64 CPU. Il s'agit d'un processeur composé d'un cœur VLIW, couplé à deux décodeurs d'instructions ARM. Les deux décodeurs ARM traduisent une paire d'instructions ARM consécutives en instructions VLIW, pour les exécuter sur le cœur VLIW. Le CPU peut fonctionner selon deux modes : ARM et VLIW. En somme, le processeur supporte deux jeux d'instruction : le jeu d'instruction ARM, et un jeu d'instruction VLIW. En mode ARM, les instructions ARM sont chargées depuis le cache, traduites en VLIW par les décodeurs ARM, puis exécutées par le cœur VLIW. En mode optimisé, le CPU exécute des instructions VLIW chargées depuis le cache d'instruction, les décodeurs ne sont pas utilisés. : Le CPU supporte en réalité deux jeux d'instruction ARM : ARM8 et 7. Mais ils correspondent au même mode pour le CPU. Un point important est que le processeur peut changer très rapidement de mode, en à peine quelques cycles d'horloges. On n'est clairement pas dans le cas des CPU x86, qui mettent des plombes pour passer du mode 32 à 64 bits et inversement. La commutation est tellement rapide qu'on peut considérer que le processeur supporte deux jeux d'instruction simultanément. Et le support simultané de deux jeux d'instruction facilite l'implémentation de la traduction binaire. Encore une fois, inutile de traduire du code qui ne sera exécutée qu'une seule fois, mieux vaut l'exécuter directement dans le mode ARM. Surtout que le processeur supporte un mode ARM qui permet d'exécuter du code ARM sans perte de performance. Par contre, le code fréquemment utilisé, notamment dans des boucles critiques, est traduit en VLIW. Au final, les performances sont optimisées, en limitant le travail du logiciel traducteur binaire. La détection du code fréquemment exécuté se fait différemment que pour les processeurs Transmetta et Daisy. L'unité de branchement ne détecte pas directement le code beaucoup exécuté, même si elle a un rôle à jouer. A la place, la détection est réalisée en logiciel, par un ''thread'' dédié, qui s'exécute sur un cœur séparé. Il détecte le code fréquemment utilisé en regardent les branchements exécutés récemment. Pour cela, le processeur mémorise l'historique des branchements pris récemment et copie cette historique dans une mémoire tampon dédiée, partagée entre tous les cœurs. Vu qu'elle est partagée, le ''thread'' de détection a accès à l'historique sans pertes de performances, seule la copie de l'historique dans ce tampon a un cout en performance, pas son partage. Le code VLIW obtenu après traduction binaire est mémorisé dans la mémoire RAM, dans une portion spécialement réservée pour. Elle fait 128 mébioctets, et est appelée le '''cache d'optimisation''' par NVIDIA. Le cache d'optimisation est protégé en écriture et seul le traducteur binaire peut écrire dedans. Au passage, le traducteur binaire est du code VLIW, ce qui fait qu'il est placé dans le cache d'optimisation. Le processeur détecte automatiquement quand une fonction pour laquelle le code compilé équivalent est disponible. Pour cela, l'unité de chargement contient une table de correspondance entre l'adresse de la fonction ARM, et l'adresse de son équivalent compilé. Quand une fonction est appelée, l'unité de chargement regarde l'adresse de destination du branchement. Si l'adresse est dans cette table, elle récupère l'adresse de la fonction compilée et branche vers celle-ci. Le processeur dispose d'un cache d'instruction de 128 kibioctets, ce qui est très important, mais nécessaire vu la taille des instructions VLIW. Le coeur VLIW dispose de ses propres décodeurs, d'un ''scoreboard'', de 7 unités de calcul et de registres. Pour les unités de calcul, il a deux FPU, deux ALU, une unité de branchement, et deux unités LOAD-STORE faisant aussi ALU. Un point important est que le CPU dispose de 64 registres entiers et de 64 registres flottants. C'est deux fois plus que supporte le jeu d'instruction ARM. En mode ARM, seule la moitié des registres est utilisée. Mais en mode optimisé, le traducteur binaire utilise bien les 64 registres, grâce à une sorte de renommage de registres logicielle. Pour simplifier la traduction binaire, le processeur supporte les techniques vues précédemment. Le processeur intègre un ''gated store buffer'' similaire à celui des processeurs Transmetta. Les techniques d'exceptions différées sont aussi supportées, comme sur les processeurs Transmetta. Il a aussi une unité de préchargement décrite par NVIDIA comme agressive, avec support du préchargement de type anticipé (''runahead''). ===L'émulation de la mémoire virtuelle=== Pour finir, il faut parler de la mémoire virtuelle avec la traduction binaire. Sur le processeur source, le processeur gère à la fois des adresses physiques et virtuelles et sa MMU fait la conversion entre les deux. Sur le processeur VLIW, la MMU est simulée par le programme de traduction binaire, partiellement ou totalement. L'implémentation exacte varie suivant que l'on parle du projet Daisy, des processeurs Transmetta ou de Denver. Sur les processeurs Transmetta, il n'y a pas de MMU ni de mémoire virtuelle. A la place, le programme de traduction binaire émule la mémoire virtuelle du processeur source. Le processeur VLIW ne gère que des adresses physiques, rien d'autre. L'espace d'adressage physique du processeur VLIW a la même taille que celui du processeur émulé, ici des processeurs x86. Il est vraisemblable que les adresses physiques utilisées par le processeur x86 sont les mêmes que celles du processeur VLIW. Sur le projet Daisy, le processeur VLIW gère la mémoire virtuelle via pagination, comme les processeurs émulés. Ce qui fait qu'il gère un espace d'adressage virtuel et un espace d'adressage physique. Pour éviter toute confusion, nous parlerons d'adresse physique/virtuelle VLIW pour les adresses physiques/virtuelles du processeur VLIW, d'adresse virtuelle/physique source pour celle du jeu d'instruction traduit, à savoir du Power PC ou du s390, éventuellement de l'x86. Il faut alors faire le lien entre adresses physiques source et adresses virtuelles VLIW. Pour cela, rien de plus simple : il y a correspondance parfaite. L'adresse physique source numéro N correspond à l'adresse virtuelle VLIW numéro N. Cependant, cela signifie que tout l'espace d'adressage virtuel VLIW serait utilisé par le code à traduire. En réalité, il faut ajouter de la place pour le code traduit et le programme de traduction binaire. La conséquence est que l'espace d'adressage virtuel VLIW est plus large que l'espace d'adressage source. L'espace d'adressage virtuel VLIW est découpé en trois sections : une section pour le code Power PC à traduire, une autre pour le code traduit en VLIW, et une dernière réservée au traducteur binaire. Le programme de traduction est placé dans une ROM mappée en mémoire dans la seconde section. Le reste de la seconde section est réservé à une mémoire RAM utilisée par le programme de traduction. [[File:Mémoire virtuelle sur le projet Daisy.png|centre|vignette|upright=1.5|Mémoire virtuelle sur le projet Daisy]] Il faut noter que chaque page physique source, une fois traduite, correspond à N pages virtuelles VLIW. La raison est que le code traduit prend plus de place que le code machine originel. En conséquence, on doit utiliser une page finale plus grande. Pour le reste, des pages contiguës en mémoire physique source sont elles aussi contiguës en mémoire virtuelle VLIW. Le calcul d'adresse est donc simple : l'adresse physique source est multipliée par N, puis on ajoute l'adresse de base à laquelle commence la section pour le code traduit. ==La traduction binaire sur le Burroughs B1700== Nous venons de voir comment accélérer en matériel la traduction binaire, avec des techniques bien spécifiques. Le Burroughs B1700 a procédé autrement et il est intéressant d'étudier en détail son architecture. Son architecture est décrite dans le livre "Interpreting Machines : Architecture and Programming of the Bl700/Bl800 Series", écrit par Elliott I. Organick et James A. Hinds. Une partie de ce qui vfa suivre est un très court résumé de ce livre. ===Le Burroughs B1700 : le mal-nommé ''split-level control store''=== Le Burroughs B1700 avait plusieurs modèles : le B1726, le B1710 et le B1800. Ils se distinguaient par la manière dont le code machine source était traduit par le traducteur binaire. Dans ce qui suit, nous parlerons de traducteur binaire pour parler soit d'un interpréteur pour un langage de programmation de haut niveau, soit pour un logiciel de traduction binaire. Le B1710 plaquait le traducteur binaire en mémoire RAM. Le B1800 faisait pareil, mais avait ajouté un cache dédié au code du traducteur binaire, afin d'améliorer les performances. Le B1726 avait un traducteur binaire scindé en deux : une partie était en mémoire RAM, l'autre était dans un ''local store'' dédié. Le processeur peut exécuter le traducteur binaire soit depuis le ''local store'', soit depuis la mémoire RAM avec une pénalité en termes de performances. La documentation appelle cela un '''''split-level control store''''', mais le terme est trompeur : la documentation qualifie de microcode le langage machine du Burroughs B1700, ce qui est à l'origine de beaucoup de confusions. Une autre source de confusion est que ce langage machine a quelques propriétés qu'on pourrait attendre d'un microcode, mais certaines sont manquantes. Par exemple, il peut adresser les registres d’interfaçage mémoire, mais c'est parce que ce sont des registres architecturaux sur ce processeur. Ses instructions n'encodent pas de signaux de commande, par contre, elles font 16 bits et adressent les opérandes de manière implicite. Le ''local store'' mémorisait au maximum 2048 instructions machine, ce qui fait que les 2048 premières "adresses" correspondaient au ''local store''. Les suivantes étaient des adresses de mémoire RAM qui contenaient le reste du traducteur binaire. En pratique, l'adresse qui séparait les deux était configurable ! On pouvait limiter la taille du ''local store'' à un multiple de 32 instructions ! Les adresses restantes étaient alors réattribuées à la RAM. Par exemple, on pouvait attribuer 64 adresses au ''local store'', les 2048 - 64 adresses restantes étaient alors attribuées à la mémoire RAM. Un registre limite, nommé TDPM, indiquait quelle était l'adresse de démarcation entre les deux. Il contenait 4 bits, dont la valeur était multipliée par 32, pour obtenir l'adresse limite. Le programmeur devait découper le traducteur binaire en segments, certains étant placé dans le ''local store'', les autres étant en mémoire RAM. Mieux que ça, le processeur autorisait d'utiliser la technique de l'''overlaying'' pour le microcode ! Il y avait même une fonction OVERLAY dédiée pour ! OVERLAY avait besoin de trois opérandes : la taille du segment à copier, son adresse source en mémoire RAM, son adresse de destination dans la micro-SRAM. L'interpréteur réservait un "segment" en mémoire RAM, pour les données, dont le processeur connaissait la position. En-dehors de ce segment, il n'y a que des instructions machines à traduire, et le traducteur binaire. Le CPU incorporait pour cela deux registres, un pour l'adresse de base du segment de données, un autre pour son adresse de fin. Il incorporait une protection mémoire limitée sur ce segment, à savoir que seul l'interpréteur pouvait lire ou écrire dans ce segment. ===Une architecture bit-adressable=== Le CPU Burroughs B1700 était '''bit-adressable'''. En clair, il pouvait adresser la mémoire bit par bit, si nécessaire. Un avantage est que cela facilitait la traduction binaire des instructions machine, mais nous détaillerons cela plus bas. Un autre avantage est que cela permettait d'émuler des architectures dont la taille des registres/''byte''/mots était très différente. Et cet avantage mérite quelques explications immédiates. Le processeur était un CPU 24 bits, mais il pouvait émuler des processeurs 16 bits, 8 bits, 4 bits, 1 bit, 9 bits, ou toute autre valeur. Pour cela, le processeur lisait des mots de 24 bits et utilisant un ''barrel shifter'' pour sélectionner les bits adéquats. Par exemple, pour un accès de 16 bits, la donnée lue en mémoire était masquée de manière à ne garder que les 16 bits de poids faible. Le ''barrel shifter''/circuit de masquage était placé directement avant le bus mémoire, sur le trajet des données/instructions. Le ''barrel shifter'' était commandé par un registre ''Field Unit'', qui précisait quelle était la taille des données à charger. Pour communiquer avec la mémoire, il avait trois registres séparés : READ, WRITE, MAR. READ contient la donnée lue lors d'une lecture, WRITE est pour une donnée à écrire lors d’une écriture, MAR contient l'adresse à lire ou en cours de lecture. Il s'agit de registres d’interfaçage mémoire. Le fait que ces registres d’interfaçage mémoire et IO soit adressable est peu commun, et c'est plus quelque chose qu'on attend d'un microcode que d'instructions machines. Mais il faut bien comprendre que les registres d’interfaçage mémoire ont été promus au rang de registres architecturaux, sur ce processeur. De même, pour communiquer avec les entrées-sorties, il disposait de deux registres CMND et DATA : CMND pour envoyer une commande à une entrée-sortie, DATA pour lire ou écrire une donnée. Il y avait aussi un registre U, qui mémorisait une donnée lue depuis le lecteur de cassette. ===L'ALU de taille variable=== Les Burroughs B1700 intégrait une pile d'adresse de retour, capable de mémoriser 16 adresses de 24 bits. Il disposait de 4 registres généraux, nommés X, Y, T et L, tous de 24 bits. Un point important est que le registre T avait des possibilités d’extraction spécifiques. Il était possible d'extrait N bits, placés n'importe où dans ce registre, et de les copier dans un autre registre. Nous verrons pourquoi cette possibilité est intéressante et ne sert que pour le registre T. Ils avaient une '''ALU de taille variable'''. En clair, l'ALU pouvait faire des calculs sur un nombre de bits compris entre 0 et 24. Pour cela, les calculs étaient fait avec une ALU de 24 bits, puis les bits de poids fort inutiles étaient masqués. L'ALU de 24 bits pouvait masquer les bits de poids fort du résultat, mais avec des limitations. Elle gérait des opérandes et résultats de 0, 4, 8, 12, 16, 20 et 24 bits. Pour avoir un réglage plus fin, une ALU 4 bits séparée permettait de corriger le résultat. Les registres X et Y étaient reliés à une ALU de 24 bits capable d'effectuer des opérations arithmétiques basiques. Fait étonnant, l'ALU faisait tous les calculs en même temps et fournissait ses résultats dans 7 registres : un registre SUM pour l'addition, un registre DIFFERENCE pour la soustraction, et un registre par opération bit à bit. Les registres pour les opérations bit à bit sont : CMPX et CMPY pour l'opération NOT sur chaque opérande, XANY pour le ET bit à bit, XEOY pour le XOR, XORY pour le OU logique. Les additions et soustractions pouvaient se faire sur des opérandes codés en binaire ou en BCD, suivant le code opération utilisé. A cela, il fallait ajouter deux registres de "résultat" MSKX et MSKY. Ils fournissaient un masque dépendant de la taille du résultat en bits. C'est ce registre qui permettait de gérer des données de 0, 4, 8, 12, 16, 20, et 24 bits, alors que l'ALU faisait 24 bits. Le masque dans ce registre pouvait servir d'opérande dans une opération de masquage ultérieure, pour corriger le résultat. L'ALU prenait en opérande les registres X et Y, mais aussi un registre CYF pour la retenue entrante. En sortie, il fournissait aussi deux retenue sortantes : une pour l'addition et une autre pour la soustraction. Elles étaient mémorisées dans les registres CYL et CYD, respectivement. L'ALU fournissait aussi 12 conditions, qui alimentait un registre d'état. Le registre d'état était segmenté en trois registres appelés XYST, XYCN et BICN. Les registres X et Y sont des registres pour les opérandes, mais il y a aussi deux autres registres de 24 bits. Il s'agit des registres T et L, qui sont des registres généraux. Les registres T et L étaient découpés en 6 sous-registres de 4 bits, nommés LA LB LC LD LE et LF pour le registre L, et TA TB TC TD TE et TF pour le registre T. Ils étaient adressables comme n'importe quel registre. Le processeur contenait de nombreux autres registres de 4 bits, dont les sous-registres de T et L. En tout, il y a 27 registres de 4 bits adressables ! Et le processeur disposait d'une ALU 4 bits pour manipuler ces registres de 4 bits ! Elle est capable de faire des MOV entre registres de 4 bits, des opérations bit à bit, et peut aussi tester si un bit vaut 0 ou 1. La dernière possibilité est très utile pour implémenter les branchements. De plus, cela permettait de faire des opérations de masquage, notamment pour appliquer un masquage supplémentaire à celui de l'ALU. Par exemple, pour gérer des opérations sur 14 bits, on effectue une opération sur 16 bits avec l'ALU, et on masque les 2 bits manquants avec l'ALU 4 bits. La capacité d'extraction du registre T pouvait aussi être utilisé pour ça. ===Le code machine du Burroughs B1700=== Le processeur utilise des instructions de 16 bits de long, qui sont copiées dans registre d'instruction M de 16 bits. Fait intéressant, il était possible de faire un OU logique entre l'instruction lue et un registre du processeur, le résultat étant mémorisé dans le registre M. Cela permettait de faire du code automodifiant, pour émuler des modes d'adressage complexe. Cela permettait par exemple de remplacer une adresse dans une micro-opération. L'adresse est alors surimposée avec un OU dans le champ d'adresse, alors que le reste de l'instruction n'est pas modifié. Afin de gérer les interruptions, ces dernières étaient partiellement détectées en logiciel. Une interruption doit être prise en compte à la toute fin de l'exécution d'une instruction source, quand celle-ci a finit son travail. Mais vu qu'ici, les instructions sont émulées avec des séries d'instructions cibles, il y a un problème : il est possible d'interrompre une instruction émulée en plein milieu. Pour éviter cela, le processeur ne déclenche pas les interruptions matérielles immédiatement, elles sont mises en attente. Une instruction dédiée vérifie si une interruption est en attente, et l'exécute alors cas échéant. L'instruction en question est utilisée en dernier, dans la suite d'instruction qui émule l'instruction source. Le ''program counter'' est appelé le registre A. Les instructions du processeur font 16 bits de long, et elles sont alignées en RAM sur 16 bits, ce qui fait que les 4 bits de poids fort de ce registre valent 0, seuls les 14 bits de poids fort sont utiles. Les adresses mentionnées plus haut, qui adressent les micro-opérations en mémoire RAM, sont des adresses de mots de 16 bits, pas les adresses de bit individuels. ===La traduction binaire sur le Burroughs B1700=== Les instructions source peuvent faire 16 bit avec tel jeu d'instruction, 8 bits avec un autre, 32 bits sur un autre, etc. Elles peuvent aussi être de longueur fixe ou variable ! Le B1700 utilisait une architecture bit-adressable pour gérer ces contraintes. En effet, avec un CPU bit-adressable, les contraintes d'alignement des instructions disparaissent ! Dans ce qui suit, on suppose que l'instruction est composée de '''champs''' : l'opcode, les opérandes, les adresses absolues, les constantes immédiates, les numéros de registre, etc. L'idée est que l'instruction source est chargée dans le registre T, puis découpée en champs, qui sont copiés dans les registres du processeur ou envoyés au microcode. Je rappelle que le registre T est relié à un ''barrel shifter'', différent au précédent, qui permettait de masquer, décaler et extraire une donnée de ce registre. Et il y a la possibilité d'extraire une suite de bit de ce registre T, pour la copier dans un autre registre. Par exemple, prenons une instruction composée d'un opcode, d'une constante immédiate et d'une adresse immédiate. Premièrement, elle est chargée dans le registre T. Deuxièmement, la constante immédiate est extraite du registre T et copiée dans le registre X. Troisièmement, l'adresse est extraite et est copiée dans le registre d’interfaçage mémoire, une lecture est démarrée, et l'opérande lue est copiée dans le registre Y. Enfin, l'instruction équivalente à l'opcode est exécutée par le microcode. Le tout prend plusieurs instructions : une première pour lire l'opcode, une seconde pour lire la première opérande, une troisième pour lire la seconde opérande, una quatrième pour tout exécuter. Notons que suivant la taille des champs, on peut en charger plusieurs à la fois. Par exemple, imaginons qu'on ait chargé 24 bits, contenant un opcode de 8 bits et une constante immédiate de 16 bits : le registre T est découpé en un opcode et une constante, avec deux instructions EXTRACT. L'architecture est bit-adressable car elle gère des champs qui ne font pas forcément 8 ou 16 bits. Par exemple, certains jeux d'instructions ont des opcodes codés sur 6 ou 7 bits, voire 9-10 bits. Utiliser une architecture bit-adressable règle ce problème d'alignement. On peut lire un champ depuis la mémoire RAM en précisant l'adresse de son premier bit, pas besoin de gérer des accès non-alignés. Par exemple, prenons une instruction source avec un opcode de 7 bits, suivi par une adresse de 12 bits et une constante immédiate de 15 bits. On peut charger 7 + 12 bits en un premier accès, pour extraite l'opcode et l'adresse, suivi par un second accès de 15 bits pour extraire la constante immédiate. Pour charger un champ, il faut connaitre plusieurs informations : l'adresse de son premier bit, sa taille. Pour cela, le B1700 contient deux registres, nommés ''Field Adress'' (FA) et ''Field Length'' (FL), qui indiquent l'adresse du premier bit et la taille de l'instruction à charger. Le premier fait 24 bits, ce qui permet de gérer des adresses de 24 bits. N'oubliez pas que le processeur est bit-adressable, ce qui permet d'adresser 16 Mébi-bits, soit 2 mébioctets. Le registre pour la taille fait lui 16 bits, ce qui permet de gérer des instructions de 65536 bits, soit 8192 octets ! Leur nom commence par ''field'', car le processeur peut gérer des instructions de taille variable et/ou plus longues que 24 bits. Dans ce cas, le chargement de l'instruction se fait champ par champ. Pour donner un exemple, prenons une instruction composée de 20 champs de 12 bits chacun. Les champs sont chargés un par un dans le registre T. Le registre FA est alors initialisé avec l'adresse de l'instruction, il est incrémenté de 12 à chaque cycle. Le registre FL est initialisé avec la taille de l'instruction, soit 20 * 12 = 240 bits, et il est décrémenté de 16 à chaque cycle. Précisons cependant que la taille d'un champ peut être changée d'un cycle à l'autre. Par exemple, le processeur peut charger un opcode de 8 bits, l'interpréter, détecter que l'instruction demande ensuite de charger une adresse de 16 bits. Dans ce cas, le registre FL est immédiatement altéré pour charger 16 bits, au lieu des 8 bits de l'opcode immédiatement précédent. ==Les jeux d'instructions dédiés à un langage de programmation== De rares processeurs sont conçus pour un langage de programmation en particulier. On appelle ces processeurs, conçus pour des besoins particuliers, des '''processeurs dédiés'''. Par exemple, les fameux ''Burrough E-mode'' B5000/B6000/B7000 étaient spécialement conçus pour exécuter de l'ALGOL-60. Leurs cousins B2000/B3000/B4000 étaient eux conçus pour le COBOL. Des langages fonctionnels ont aussi eu droit à leurs processeurs dédiés. Le prolog en est un bel exemple, avec les superordinateurs de 5ème génération qui lui étaient dédié. On peut aussi citer les machines LISP, dédiés au langage LISP, qui datent des années 1970. Elles étaient capables d’exécuter certaines fonctions de base du langage directement dans leurs circuits : elles possédaient notamment un ''garbage collector'' câblé dans ses circuits ainsi que des instructions machines supportant un typage déterminé à l’exécution. Les processeurs dédiés ont eu leur heure de gloire au début de l'informatique, à une époque où les langages de haut niveau venaient d'être inventés. À cette époque, les compilateurs n'étaient pas performants et ne savaient pas bien optimiser le code machine. Il était alors rationnel, pour l'époque, de rapprocher le code machine cible et le langage de programmation de haut niveau. De nombreuses architectures dédiés ont ainsi été inventées, avant que les concepteurs se rendent compte des défauts de cette approche. Les défauts en question ne sont pas nombreux, mais assez simples à comprendre. Premièrement, elles sont très rapides pour un langage de programmation en particulier, mais sont assez mauvaises pour les autres, d'où un problème de "compatibilité". Ajoutons à cela que les langages de programmation peuvent évoluer, devenir de moins en moins populaires/utilisés, ce qui rend la création d'architectures généralistes plus pertinente. Enfin, les architectures dédiées sont évidemment des processeurs CISC, pour implémenter les nombreuses fonctionnalités des langages évolués. Et les défauts des CISC sont assez rédhibitoires à l'heure actuelle. À l'heure actuelle, les algorithmes des compilateurs se sont améliorés et savent nettement mieux utiliser le matériel. Ils produisent du code machine efficace, ce qui rend les architecture dédiées bien moins intéressantes. Si on ajoute les défauts de ces architectures dédiées, par étonnant que les architectures dédiées aient presque disparues. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'accélération matérielle de la virtualisation | prevText=L'accélération matérielle de la virtualisation | next=Le matériel réseau | nextText=Le matériel réseau }} </noinclude> qnac55za3u0hgqv9u0a9yh8578tjn7r 764054 764053 2026-04-19T20:42:26Z Mewtow 31375 /* Le code machine du Burroughs B1700 */ 764054 wikitext text/x-wiki De nos jours, la majorité des programmeurs programment dans des langages de haut niveau. Il est très rare pour eux d'avoir à utiliser de l'assembleur, et encore moins un langage machine. Les programmes écrits dans un langage de haut niveau sont traduits en langage machine par un logiciel appelé le compilateur. [[File:Compilation.PNG|centre|vignette|upright=2.5|Compilateur : principe.]] Il se trouve que quelques rares processeurs sont conçus pour faciliter le travail du compilateur. Et ce sont ces processeurs que nous allons voir dans ce qui suit. Ils représentent une catégorie de jeux d'instruction à part, qui n'a pas vraiment de nom. ==La compilation et l’interprétation== Un compilateur traduit donc du code source, écrit dans un langage de haut niveau, vers du langage machine. La traduction est rarement directe. En général, le compilateur traduit le code source en assembleur, qui est lui-même traduit en langage machine. L'assembleur est une représentation textuelle du langage machine alors que le code machine est du binaire exécutable par le processeur. La traduction se fait donc en deux étapes, la '''compilation''' proprement dite et l''''assemblage''', réalisées respectivement par un compilateur et un assembleur. Il faut parfois rajouter une troisième phase d'édition des liens, que nous passons volontairement sous silence. {| |[[File:Code C.png|vignette|200px|Le code source, ici en C.]] |[[File:Assem.png|vignette|250px|Le code assembleur.]] |[[File:Binary file - hello world (C programming).png|vignette|150px|Le code machine.]] |} ===L'interprétation et la compilation à la volée=== Une alternative à la compilation est l''''interprétation''', qui transforme le code source en code machine à la volée. Avec l'interprétation, le code source est passé à un logiciel appelé l''''interpréteur''', qui exécute le code source ligne de code par ligne de code. Une ligne de code est traduite en code machine, qui est exécutée, puis l'interpréteur passe à la ligne suivante. [[File:Kaantotulkkaus.png|centre|vignette|upright=3|Compilation versus interprétation.]] Un défaut de l'interprétation est son cout en performance, qui est assez important. Aussi, l'interprétation stricte a évolué vers un hybride entre interprétation et compilation. L'idée est qu'une partie peu importante du code source est interprétée, alors que le code important est compilé juste avant d'être exécuté. Pour le dire autrement, le code source est partiellement compilé à la volée, juste avant son exécution. Aussi, on parle de '''compilation à la volée'''. Le terme en anglais est ''Just In Time compilation'', abrévié en JIT. En soi, le JIT est associé à de l'interprétation, les JIT sont en réalité des hybrides interpréteurs-compilateurs. Un point important est que compiler du code à la volée est assez lourd, cela a un cout en performance. Par contre, le code compilé s'exécute plus vite que du code interprété. Si on exécute le code compilé une seule fois, le cout de la compilation l'emporte sur le gain à l'exécution. Mais le code compilé est réutilisé autant de fois que nécessaire. Le gain à l'exécution est donc multiplié par le nombre d'exécutions, alors que le cout de la compilation est répartit sur plusieurs exécutions, il est amorti. Les portions du code sont choisies de manière à avoir un gain en performance maximal. L'idée générale est que le code exécuté très souvent est compilé, alors que du code exécuté pas souvent est interprété. Typiquement, les boucles les plus souvent exécutées sont compilées, le code exécuté une seule fois est interprété. ===Les langages intermédiaires=== Les compilateurs modernes passent par un '''langage intermédiaire''' pour faire la transformation en code machine. Le compilateur traduit le langage de haut niveau en langage intermédiaire, puis traduit le langage intermédiaire en code machine cible. Le langage intermédiaire est parfois appelé le '''''bytecode''''', et ce terme recouvre aussi le code écrit avec du ''bytecode''. L'interprétation aussi peut passer par l'intermédiaire d'un un ''bytecode'', ce n'est pas limité aux compilateurs proprement dit. {| |[[File:Intermediate representation scheme.png|centre|vignette|upright=1.5|Représentation intermédiaire d'un compilateur.]] |[[File:Code interpreter scheme.png|centre|vignette|upright=1.5|Fonctionnement d'un interpréteur.]] |} Faire ainsi a de nombreux avantages, le principal étant d'avoir un compilateur capable de traduire un langage de haut niveau vers plusieurs jeux d'instructions différents. Cela permet d'avoir, par exemple, un compilateur qui traduit du C soit en code machine x86, soit en code machine pour un CPU ARM, soit pour un CPU POWERPC, etc. [[File:Compiler design.svg|centre|vignette|upright=3|Compiler design]] Pour l'interprétation, l'usage d'un ''bytecode'' a d'autres avantages. Pour l'exploiter, il faut distribuer non pas le code source, mais le ''bytecode''. En clair, l'interpréteur ne prend pas en entrée le code source, mais du ''bytecode'' déjà compilé. La traduction est alors beaucoup plus simple, car le code source a déjà été partiellement compilé, par la traduction en ''bytecode''. Interpréter du code source est en effet assez compliqué : il faut effectuer des étapes d'analyse lexicale, sémantique, et bien d'autres. Avec du ''bytecode'', ces étapes ont été réalisées lors de la compilation du ''bytecode'', l'interpréteur a alors peut de choses à faire. L'interprétation du ''bytecode'' se fait instruction par instruction, au niveau du ''bytecode''. Une instruction du ''bytecode'' est traduite en une instruction machine équivalente, qui est exécutée. Si l'instruction est un peu complexe, il exécute une fonction/procédure qui fait la même chose. Pour résumer, le ''bytecode'' est ensuite traduit à la volée et exécuté instruction par instruction par un logiciel appelé l'interpréteur. [[File:Control table.png|centre|vignette|upright=2|Control table]] Le langage intermédiaire peut être vu comme l'assembleur d'un processeur, n'existe pas forcément dans la réalité, mais dont le jeu d'instruction est décrit en détail. Le processeur en question est appelé une '''machine abstraite''', ou encore une ''machine virtuelle''. Nous utiliserons le terme de machine abstraite dans ce qui suit. La machine abstraite n'est pas la même suivant que l'on cible la compilation ou l'interprétation/JIT. Pour l'interprétation/JIT, la machine abstraite est souvent une machine à pile, car cela simplifie grandement la traduction en code machine final. L'interprétation demande que la traduction du ''bytecode'' en code machine soit la plus simple possible. Et cela demande de prendre en compte pas mal de détails. Par exemple, les différents jeux d'instruction existants n'ont pas le même nombre de registres, ce qui pose problème lors de la traduction du ''bytecode''. Lors de la transformation en code machine, un algorithme d'allocation de registres se débrouille pour traduire le code intermédiaire en code qui utilise un nombre limité de registres. Pour améliorer cette allocation de registres, il y a deux solutions niveau ''bytecode'' : soit utiliser un nombre illimité de registres, soit utiliser une machine à pile. Pour les machines à pile, il existe un algorithme simple et rapide pour traduire un code écrit pour une machine à pile en un code écrit pour un processeur avec des registres, qui se débrouille pas trop mal pour allouer efficacement les registres. C'est un avantage assez important pour les langages interprétés. ==Les processeurs qui exécutent du bytecode== Le ''bytecode'' est un code machine, ce qui signifie qu'il peut en théorie s'exécuter sur un processeur qui implémente le jeu d’instruction associé. Si le jeu d'instruction d'un ''bytecode'' est souvent une description censée être fictive, elle n'en reste pas moins un jeu d'instruction et des caractéristiques précises, qu'on peut l'implémenter en matériel ! Néanmoins, tous les ''bytecode'' ne sont pas égaux de ce point de vue. Certains sont faciles à implémenter en matériel, d'autres non. Les ''bytecodes'' implémentés en matériel sont ceux dont les machines abstraites sont des machines à pile. Un premier exemple est celui des processeurs Pascal MicroEngine, qui exécutaient directement le bytecode du langage Pascal, le fameux UCSD P-code. Un second exemple est la '''machine SECD''', qui sert de langage intermédiaire pour certains compilateurs de langages fonctionnels. Elle a été implémentée en matériel par plusieurs équipes, notamment par les chercheurs de l'université de Calgary en 1989. Dans le même genre, quelques processeurs simples étaient capables d’exécuter directement le ''bytecode'' du langage FORTH. Le FORTH, un des premiers langages à pile de haut niveau, possède de nombreuses implémentations hardware et est un des rares langages de haut niveau dont le ''bytecode'' a été utilisé comme langage machine sur certains processeurs. Par exemple, on peut citer le processeur FC16, capable d’exécuter nativement du ''bytecode'' FORTH. ===Les processeurs Java=== Le cas le plus impressionnant est celui de la machine virtuelle Java, qui est un design de processeur comme un autre. En temps normal, le ''bytecode'' Java est compilé ou interprété, mais certains processeurs exécutaient du ''bytecode'' Java directement, sans interprétation ni compilation. Ils sont appelés des '''processeurs Java'''. On peut citer les processeurs ARM disposant de l'extension Jazelle, du JEMCore et du aJ-100 de aJile Systems, le picoJava II de Sun, et quelques autres. Il y a aussi eu quelques projets de recherche ou processeurs open source, comme Komodo, jamuth, le ''Java Optimized Processor'' et quelques autres projets du même genre. L'implémentation de la machine virtuelle Java n'était cependant pas complète. Quelques instructions complexes n'étaient pas gérées par le processeur et devaient être émulées en logiciel. Mais la grosse majorité des instructions l'était. Les instructions non-supportées étaient émulées avec la méthode ''trap and emulate'', à savoir qu'un opcode inconnu déclenchait une exception matérielle ''opcode invalide'', dont la routine pouvait émuler les instructions qui doivent l'être. Le processeur aJ-100 de aJile est capable d'exécuter la quasi-totalité des instructions de la JVM Java, seules deux instructions faisant exception. Les '''accélérateurs Java''' sont une solution intermédiaire, qui permet de traduire à la volée du ''bytecode'' Java en instructions RISC, exécutées sur un autre processeur. Il s'agit formellement de traduction binaire, mais je préfère en parler ici. L'idée est de combiner un processeur RISC avec un coprocesseur Java. Le coprocesseur lit le ''bytecode'' Java et le traduit en instructions RISC, ces dernières étant exécutées sur le processeur RISC. Le JA108 de Nozomi était un coprocesseur de ce genre, au même titre que le coprocesseur JSTAR de JEDI. L'extension Jazelle de certains CPU ARM fonctionnait vraisemblablement sur un principe similaire, sauf que le coprocesseur était intégré dans le processeur ARM, entre le cache d'instruction et le décodeur d'instructions ARM. L'intérêt des processeurs Java n'est pas qu'une question de performance. Certes, ils permettent d’exécuter plus vite les programmes compilés en ''bytecode'', comme des programmes Java pour la JVM Java. Mais l'intérêt est aussi de faire des économies de mémoire RAM et ROM. Je rappelle que la machine virtuelle Java est une machine à pile, ce qui fait que sa densité de code est excellente. Les programmes en ''bytecode'' Java sont donc très petits, et prennent peu de place en mémoire ROM. De plus, utiliser un processeur Java permet de se passer d'interpréteur ou de JIT Java, ce qui économise encore plus de mémoire. ===Transformer un processeur RISC en machine à pile=== La plupart des processeurs Java utilisent en interne une architecture à registre généraux, qui émule une machine à registre en microcode. Quelques registres sont réservés pour l'état de la machine virtuelle, avec au minimum un registre réservé pour le pointeur de pile, un autre pour un pointeur vers un ''pool'' de constantes, et quelques autres. D'autres registres servent à mémoriser le haut de la pile, à savoir le sommet de la pile et quelques opérandes situées juste en-dessous. Par exemple, le processeur picoJava II gardait les 64 opérandes au sommet de la pile dans 64 registres généraux, séparés des registres pour les pointeurs et l'état de la JVM. Une optimisation possible, envisagée sur les processeurs picoJava, était une sorte de macro-fusion sous stéroïde. L'idée était de fusionner certaines séries d'instructions Java en une seule instruction machine. Typiquement, pour faire une opération arithmétique, une machine à pile empile deux opérandes et exécute une opération, ce qui prend trois instructions machine. La spécification picoJava 1 a proposé d'envoyer directement la seconde opérande en entrée de l'ALU, ce qui fusionnait l'empilement de la seconde opérande et l'opération arithmétique. Le processeur picoJava II va plus loin et fusionne les trois instructions en une seule instruction RISC, agissant sur des registres. Mais la majorité des processeurs Java ne faisaient pas cela, car cela complexifie grandement le décodeur. Les processeurs ''bigfoot'' de DCT utilisaient un système différent pour émuler une pile avec des registres. Leur banc de registre de 64 bits était coupé en deux : les 16 premiers registres étaient utilisés pour la pile, les 48 suivants étaient utilisés pour autre chose. L'idée est que le registre 0 mémorise un emplacement vide, le registre 1 contient le sommet de la pile, le registre 2 l'opérande sous le sommet de la pile, et ainsi de suite. Les 15 premières opérandes de la pile sont donc mémorisées dans les registres. Les instructions écrivent leur résultat dans le registre adéquat, puis tous les registres sont décalés pour que le résultat passe dans le registre R1. Par exemple, les instructions PUSH chargent une opérande dans le registre 0, puis tous les registres sont décalés de un rang, pour que l'opérande chargée soit dans le registre 1. Le registre 0 devient alors le registre numéro 1, le registre 1 devient le 2, et ainsi de suite. Par contre, les opérations arithmétiques font autrement. Une instruction d'addition, par exemple, lit les opérandes dans le registre 1 et 2, puis place son résultat dans le registre 2 (les deux opérandes sont dépilées). Puis, les registres sont décalés d'un rang pour que ce registre 2 devienne le 1. Les registres ne sont en réalité pas décalés, il n'y a pas de transferts entre registres. A la place, les 16 premiers registres utilisaient une forme limitée de renommage de registres, similaire au banc de registre tournant utilisé sur les processeurs EPIC comme l'Itanium. Les numéros de registres sont renommés, décalés, à chaque fois qu'on empile ou dépile une opérande. Pour faire le renommage de registres, processeur contient un ''register stack counter'' de 4 bits, qui est incrémenté à chaque fois qu'on empile une donnée, et est décrémenté quand on dépile une donnée. Quand on veut accéder à un opérande, le numéro de registre architectural associé est traduit en numéro de registre physique, en additionnant le ''register stack counter'' (modulo 16, pour rester sur 4 bits). ==L'accélération de l'interprétation/JIT : le ''Thumb-EE'' d'ARM== Quelques processeurs ont ajouté des instructions pour faciliter le travail des interpréteurs/JIT. Par exemple, en 2005, ARM a ajouté le mode ''Thumb Execution Environment'', qui faisait cela. Il reprenait le jeu d'instruction compact ''thumb'' et ajoutait quelques instructions et modifiait le comportement d'instructions existantes. Le jeu d'instruction ''Thumb-EE'' a été ajouté sur les CPU ARM en 2005, mais a été déprécié en 2011, par manque d'utilité. Mais la tentative mérite qu'on s'attarde dessus. Les CPU de gamme M, déstinés à l'embarqué, n'ont jamais supporté ''Thumb-EE'', vu que de tels processeurs ne sont pas conçus pour exécuter du code interprété/JIT. Le mode ''Thumb-EE'' est un mode d'exécution, séparé de l'ARM normal. Ainsi, une instruction ''Thumb-EE'' et une instruction ARM peuvent avoir le même encodage en binaire, mais se comporter différemment. Le processeur est à tout moment soit en mode ''Thumb-EE'', soit en mode ARM normal, ce qui précise comment ces instructions doivent se comporter. Des instructions ont été ajoutées par ''Thumb-EE'', pour qu'il fasse son travail. L'entrée et la sortie du mode ''Thumb-EE'' se fait avec deux instructions : ENTERX et LEAVEX. Une fois en mode ''Thumb-EE'', le décodage des instructions ''thumb'' se fait avec les règles du ''Thumb-EE'', et non celles du ''thumb'' normal. Une première différence entre ''thumb'' normal et ''Thumb-EE'' est l'ajout de ce qui s'appelle un ''null check'' pour les instructions mémoire. Le ''null check'' est utilisé pour les instructions en mode d'adressage "base + indice" ou "Base + décalage", ou tout autre mode d'adressage avec un registre de base utilisés dans des calculs d'adresse. L'idée est de vérifier si l'adresse de base vaut zéro ou non. Si c'est le cas, le processeur lève une exception matérielle, qui est traitée par l'interprétation ou le compilateur JIT. Une telle situation est en effet signe d'une erreur d'adressage mémoire, qui doit être traitée par l'interpréteur. Typiquement, c'est signe qu'un pointeur n'a pas bien été initialisé, ce qui peut arriver avec du code interprété ou JIT. Les interpréteurs/JIT traitent généralement la situation en ajoutant un test avant toute instruction mémoire, pour vérifier si le pointeur accédé vaut zéro ou non. Avec ''Thumb-EE'', pas besoin d'ajouter les tests en questions : ils sont réalisés automatiquement lors de chaque instruction mémoire. : Les registres pour la pile, comme le pointeur de pile et le pointeur de ''frame'', sont aussi concernés. L'instruction CHKA vérifie que les accès à un tableau ne débordent pas en dehors du tableau en question. Par exemple, pour un tableau de 1024 éléments, elle vérifie si l'indice est compris entre 0 et 1024. Pour cela, elle vérifie si l'indice de tableau est bien dans l'intervalle adéquat et lève une exception matérielle si ce n'est pas le cas. Son utilité se comprend quand on sait que tous les accès à un tableau sont vérifiés par l'interpréteur. L'interpréteur est censé ajouter des instructions pour vérifier les indices, à savoir deux ou trois branchements. Avec CHKA, le test se fait en une seule instruction. D'autres modifications mineures de l'encodage des instructions sont aussi présentes, ainsi que des modifications pour les instructions LOAD/STORE. Précisément, dans les modes "base + indice" et "base + décalage", le décalage et l'indice subissent maintenant des décalages. L'idée est de simplifier le calcul d'adresse. De quoi économiser quelques instructions lors des calculs d'adresse, ce qui facilite le travail de l'interpréteur/JIT. Et des instructions LOAD/STORE précises voient leur comportement modifié pour faciliter la gestion de la pile de Java ou l'exécution des méthodes locales. Pour faciliter le travail de l'interpréteur, deux instructions HBP et HBLP ont été ajoutées. Elles branchent vers une fonction qui gère les exceptions logicielles (une fonctionnalité présente dans de nombreux langages, comme Java). La première effectue un branchement vers la fonction en question, la seconde sauvegarde l'adresse de retour avant de faire ce branchement. Les deux peuvent brancher vers 256 fonctions pré-déterminées, sans avoir à présenter leur adresse. ==La traduction binaire accélérée par le matériel== Plus haut, nous avons surtout parlé des compilateurs et des interpréteurs. Cependant, nous devons aussi parler de la '''traduction binaire'''. Elle traduit un programme écrit dans un code machine vers un autre code machine. Par exemple, elle traduit un programme compilé pour un CPU x86 vers un code machine ARM. En général, le système d'exploitation est généralement compilé pour le jeu d'instruction natif, mais il exécute des applications prévues pour le x86. Les applications sont traduites par le système d'exploitation, avant d'exécuter le code traduit. : Nous parlerons dans la suite de code machine source et de code machine cible, pour parler respectivement du code à traduire et du code obtenu après traduction. Idem avec d'autres termes comme architecture cible/source, ou jeu d'instruction cible/source. La traduction binaire est surtout utilisée pour des questions d'émulation ou de compatibilité. Par exemple, lorsque les Macintosh sont passés de processeurs Power PC vers des processeurs x86, le système d'exploitation Mac OS utilisait la traduction binaire pour convertir les anciennes applications Power PC vers du code x86. Les utilisateurs n'y ont vu que du feu. Le système de traduction binaire était appelé Rosetta 1. Par la suite, lors de la transition de processeurs x86 vers des processeurs Apple, Rosetta 2 a vu le jour. La traduction binaire a surtout été utilisée pour traduire du code x86 vers un autre jeu d'instruction. Il faut dire que le x86 est le jeu d'instruction dominant. De nombreuses entreprises ont eu pour ambition de briser l'hégémonie du x86 sur PC, en remplaçant le x86 par un jeu d'instruction plus performant, tout en gardant une compatibilité maximale. La traduction binaire était la seule solution pratique. Elle était le plus souvent intégralement réalisée en logiciel, comme c'était le cas sur les architectures Itanium avec le ''IA-32 Execution Layer''. ===Les généralités sur la traduction binaire assistée en matériel=== L'architecture source est presque tout le temps une architecture CISC, assez ancienne, qu'on souhaite émuler. Le choix de l'architecture cible se porte souvent sur une architecture VLIW, fort différente de l'architecture source, et ce pour obtenir une économie de matériel conséquente. Pour s'exécuter rapidement, le code traduit doit exploiter le parallélisme d'instruction, à savoir exécuter plusieurs instructions en même temps dans des unités de calcul séparées. Il est possible d'utiliser un processeur superscalaire avec exécution dans le désordre pour cela, mais au prix d'un cout important en transistors. Alors qu'en utilisant un CPU VLIW, c'est le traducteur binaire qui fait tout le travail d'extraction du parallélisme d'instruction. Et après tout, quitte à avoir un traducteur binaire, autant lui refiler le boulot d'optimisation. Compiler du code à la volée est certes assez lourd, mais qu'il existe des algorithmes efficaces pour regrouper des instructions indépendantes dans une seule instruction VLIW. Au passage, le processeur VLIW a plus de registres que le processeur source. Cela permet de faire du renommage de registres directement en logiciel. Le traducteur binaire n'hésite pas à changer les noms de registres entre instructions source et instruction VLIW cible, afin de supprimer des dépendances de données. En conséquence, cela ouvre des opportunités de parallélisme, qui sont exploitées lors du regroupement des opérations en instructions VLIW. Vous pourriez penser que le choix d'un CPU VLIW pour émuler du CISC est tout sauf optimal. Vous devez penser que la traduction binaire est d'autant plus simple que l'architecture source et cibles sont semblables. Dans les faits, ce n'est pas tellement traduire les instructions qui pose problème, mais plus la gestion du registre d'état, des exceptions matérielles, de la mémoire virtuelle, la différence entre gros-boutisme et petit-boutisme, de même que des différences d'adressage pour les périphériques. Le premier problème est la gestion des conditions, notamment en présence d'un registre d'état. En pratique, la traduction binaire s'utilise pour traduire du code CISC vers du code RISC ou VLIW. Les architectures source ont donc un registre d'état, qui est mis à jour non seulement par des instructions de test, mais aussi des instructions arithmétiques. L'architecture cible n'a elle pas de registre d'état, mais des registres à prédicats. La traduction de l'un vers l'autre est alors quelque peu compliquée. Et elle est d'autant plus compliquée que le registre d'état est la source de dépendances d'instruction implicites, qui réduisent les performances. Par exemple, si une instruction arithmétique modifie le registre d'état, cela peut impacter l'exécution d'une instruction ultérieure, qui lit ce registre d'état. Pour éliminer ces fausses dépendances, le traducteur binaire doit renommer ce registre en logiciel et détecter les dépendances utiles. Et le compilateur doit gérer les cas où il y a beaucoup de distance entre les deux, voire les cas où l'instruction dépendante n'a pas encore été analysée par le compilateur. Un autre problème est lié à la gestion des exceptions matérielles, et précisément des exceptions précises. Pour rappel, une instruction VLIW regroupe plusieurs opérations, opérations qui correspondraient à une instruction machine sur un CPU pas VLIW. En conséquence, plusieurs instructions du langage machine "source" sont regroupées en une seule instruction VLIW. Et il faut tenir compte du cas où une instruction source lève une exception. Dans le code VLIW, cela signifie qu'une opération lève une exception, et il faut annuler partiellement l'instruction VLIW associée. Et par partiellement, on veut dire que seules les opérations suivantes dans l'ordre du programme source doivent être annulées, puis ré-exécutées. Et c'est un sacré casse-tête ! Les processeurs VLIW utilisent comme solution le mécanisme d'exceptions différées des processeurs EPIC, vu il y a quelques chapitres. Pour résumer, le code est exécuté par blocs d'instruction, délimités par des branchements ou tout autre limite/barrière pertinente dans le code. Un bloc de code est compilé en deux versions : une version rapide sans exceptions matérielles, une version lente qui gère les exceptions précises. La version rapide s'exécute sans exécuter les exceptions. Cependant, les exceptions matérielles sont enregistrées, pour être prises en compte à la toute fin du bloc de code. La version rapide est exécutée en premier et elle mémorise si une exception matérielle a eu lieu. Puis une instruction vérifie si une exception a eu lieu et décide quoi faire. Si aucune exception n'a eu lieu, elle passe à la suite du programme, les résultats du bloc de code sont définitivement acceptés. Mais si une exception a eu lieu, tout est annulé. Le processeur est remis dans l'état initial, puis le code est ré-exécuté instruction par instruction de manière à gérer l'exception correctement. ===La traduction par pré-décodage=== Une première solution serait de faire la traduction binaire dans le cache d'instruction. Lors d'un défaut de cache, le code chargé depuis la RAM est traduit en code machine cible. Et c'est ce code machine cible qui est mémorisé dans le cache d'instruction et exécuté par le processeur. La technique marche sur le papier et n'est qu'une amélioration des techniques de pré-décodage vues il y a quelques chapitres. Cependant, le code obtenu est une traduction assez basique, qui n'incorpore pas d'optimisations dignes de ce nom. De plus, elle gère mal le cas où la taille du code source et cible sont potentiellement très différentes. Concrètement, elle est surtout utile pour traduire du code d'un processeur RISC vers un autre processeur RISC, dont les tailles d'instructions sont similaires. Elle ne permet pas d'émuler plusieurs jeux d'instructions différents, le cout en matériel (un circuit de pré-décodage par jeu d'instruction) serait trop important. ===Le projet DAISY d'IBM=== De très rares processeurs étaient conçus pour accélérer cette traduction binaire, afin de garder de bonnes performances. Les premiers à avoir étudié l'idée étaient IBM, avec leur projet DAISY (''Dynamically Architected Instruction Set from Yorktown''). Le projet de base était de convertir à la volée du code compilé pour des CPU Power PC, vers du code VLIW. Les chercheurs d'IBM avaient développé un algorithme de traduction dynamique efficace, ainsi qu'un processeur VLIW disposant d'optimisations spécifiques à la traduction binaire dynamique, à la volée. De nombreuses idées de ce projet ont été reprises ou re-découvertes par la société Transmetta, avec ses processeurs Crusoe et Efficieon, puis par NVIDIA avec son projet Denver. Les ingénieurs de ce projet ont étudié la possibilité d'émuler plusieurs jeux d'instructions différents sur un même processeur, notamment le s390 d'IBM et le x86. En théorie, cela demande juste d'avoir plusieurs programmes de traduction binaire : un pour le x86, un autre pour le s390, éventuellement un autre pour le Power PC. Mais cela ne s'est pas concrétisé. Mais les ingénieurs ont étudié quelle pourrait être l'architecture VLIW idéale pour ça. Par exemple, il fallait des additions 3-opérandes pour simplifier les calculs d'adresse. Une difficulté était la gestion du registre d'état, dont les bits ne sont pas mis à jour de la même manière sur le x86 et le s390 ou le Power PC. La différence entre gros-boutisme et petit-boutisme était aussi un problème, de même que des différences d'adressage pour les périphériques. Pour que la traduction binaire soit efficace, le processeur VLIW intègre diverses optimisations, comme les exceptions différées et les branchements multi-voies. La principale est la suivante : seul le code des boucles ou fonctions exécutées fréquemment est traduit en code VLIW, le reste du code est interprété. En effet, traduire du code binaire prend plus de temps qu'une simple interprétation. Pour du code qui ne sera exécutée qu'une seule fois, il est plus rapide d'utiliser l'interprétation. Par contre, pour du code exécuté beaucoup de fois, le cout de la traduction binaire est amorti, dilué sur N exécutions, compensé par le gain en temps d'exécution de ce code traduit. Au final, cela permet de ne traduire que le code qui le mérite. La détection du code fréquemment exécuté est réalisée dans le cœur VLIW. L'unité de branchement mémorise les derniers branchements rencontrés et le nombre de fois qu'ils ont été exécutés. Si ils ont été exécutés un certain nombre de fois, l'unité de branchement lève une exception matérielle, qui invoque le traducteur binaire. La routine de cette exception est le traducteur binaire proprement dit. En clair, la traduction binaire est démarrée quand le processeur détecte qu'une fonction a été exécutée plus de N fois, via une exception matérielle dédiée. La mesure du nombre d'exécution d'un branchement se fait dans l'unité de calcul dédiée aux branchements, avec l'aide d'une sorte de ''branch adress buffer'' modifié. Pour rappel, le ''branch adress buffer'' mémorise le ''Program counter'' de chaque branchement récemment rencontré. Ici, chaque entrée du ''branch adress buffer'' est associée à un compteur incrémenté à chaque exécution du branchement. ===Les processeurs Crusoe et Efficieon de Transmetta=== Les processeurs Crusoe Et Efficeron sont deux processeurs VLIW produits par la société Transmetta, une société californienne rachetée par NVIDIA. Ils étaient conçus pour exécuter spécifiquement des programmes x86, système d'exploitation inclus. L'idée derrière ce projet était d'exécuter du code x86 sans que les applications, ni même le système d'exploitation et le BIOS soient au courant ! Les processeurs Transmetta étaient en réalité conçus pour exécuter un programme unique, le ''Code Morphing Software'' (CMS), qui traduisait le code x86 en code VLIW. Transmetta ne voulait pas que son processeur VLIW soit exploité directement, sans le CMS. Il n'avait pas rendu public de compilateur pour traduire du C vers du code VLIW, il n'avait pas donné la documentation du jeu d'instruction VLIW. Des efforts de rétro-ingénieurie, documentés sur le site realworldtech, ont cependant permis de comprendre comment étaient encodées les instructions du processeur Crusoe. Comme pour le projet Daisy, le ''Code Morphing Software'' utilisait à la fois interprétation et traduction binaire, selon les besoins. Les instructions sont interprétées lors de leurs premières exécutions, mais le CMS bascule sur de la traduction en code VLIW après un certain nombre d'exécution. Ainsi, les boucles souvent exécutées sont traduites en code VLIW, alors que le reste du code est interprété. Le choix du code à traduire est le fait du CMS, il utilise des heuristiques complexes pour, qui ne sont pas connues dans le détail. Le ''Code Morphing Software'' est mémorisé dans une EEPROM, ce qui en fait un ''firmware'' situé en dessous du BIOS. Le CMS démarre ensuite le BIOS, qui lui-même démarre le système d'exploitation, qui lui-même démarre les pilotes de périphériques et les programmes. Le CMS se réserve les 16 premiers mébioctets de l'espace d'adressage. L'EEPROM du CMS est mappée dedans, mais n'en utilise que la moitié. Le reste est utilisé comme cache, pour mémoriser le code VLIW traduit par le CMS. Les deux processeurs intégraient aussi deux mémoires SRAM, utilisées par le CMS, appelées la ''local program memory'' (LPM) et la ''local data memory'' (LDM). La première contient du code qui gére les interruptions, la mémoire virtuelle, les exceptions matérielles, les problèmes d'alignement mémoire, et quelques fonctions très fréquemment utilisées par le CMS. La seconde est de la mémoire RAM utilisée par ces fonctions de la ''local program memory''. Les mémoires caches L1 et L2 sont séparées de ces deux mémoires. Le processeur Crusoe et Efficieon étaient des processeurs VLIW très simples. Ils n'avaient même pas de MMU, ni de fonctionnalités importantes sur les CPU x86. Pour Crusoe, les instructions VLIW étaient encodées sur 64 ou 128 bits, et regroupaient entre 2 à 4 opérations. Les instructions LOAD/STORE ne supportaient pas d'adressage indicé, juste de l'adressage indirect à registre. Crusoe avait 5 unités de calcul : deux ALU entières, une unité LOAD/STORE, une unité de branchement, une FPU. Efficieon doublait le nombre d'ALU entière et d'unité LOAD/STORE, les instructions VLIW passaient à 256 bits. Le processeur Crusoe contient 160 registres, dont 64 registres généraux, 32 registres flottants. Les registres généraux font 32 bits, ce qui est cohérent avec le fait que les CPU x86 de l'époque étaient des processeurs 32 bits. Quant aux registres flottants, ils faisaient 80 bits, ce qui colle avec la taille des registres flottants de la FPU x87 utilisée à l'époque. Sur les 64 registres généraux, seuls 48 étaient réellement utilisable pour mémoriser des opérandes. Une partie des registres généraux étaient utilisés pour la gestion de la pile, d'autres pour mémoriser l'état du CPU x86 émulé, un registre était un registre zéro non accesible en écriture. La gestion des exceptions est optimisée avec un système d'exceptions différées, le même que celui décrit plus haut. Le processeur utilise un système similaire, pour effectuer des lectures anticipées, qui a été expliqué dans le chapitre sur les processeurs VLIW/EPIC. Sauf qu'il s'agit là d'une optimisation pour gagner en performances en général, pas une aide utile pour gérer des exceptions précises ou toute autre subtilité de la traduction binaire. Pour gérer les exceptions différées et les lectures anticipées, le processeur mémorise l'état du processeur avant de démarrer un bloc de code. Pour cela, le processeur contient des ''shadow registers'', 48 registres entiers et 16 registres flottants. Vous remarquerez qu'il y a autant de registres entiers que de ''shadow registers'' entiers. Les registres entiers/flottants sont mémorisés dans les ''shadow registers'' avant d'exécuter un bloc de code, afin que le processeur puisse revenir à l'état de base. Une autre fonctionnalité liée est le ''gated store buffer''. Le principe est d'éviter toute écriture en mémoire RAM, tant que le code VLIW n'a pas émis d'instruction ''commit''. L'idée est que les écritures sont conservées dans la ''store queue'' de l'unité LOAD/STORE. Pour rappel, la ''store queue'' met en attente les écritures soit tant que les écritures ne sont pas terminées, soit tant que la RAM est occupée. L'idée est que les écritures sont accumulées dans la ''store queue'' tant que l'instruction ''commit'' n'a pas eu lieu. Le ''store queue'' est "déversé dans la RAM" seulement quand l'instruction ''commit'' s'exécute. ===Les CPU NVIDIA Denver=== Nvidia a tenté quelque chose de similaire avec son projet Denver. NVIDIA a racheté l'entreprise Transmetta, ses brevets, et a tenté de refaire la même chose. Le projet initial était de traduire du code x86 vers un jeu d'instruction VLIW propriétaire, comme l'a fait Transmetta. Mais NVIDIA n'a pas réussi à acquérir la licence du jeu d'instruction x86 et s'est rabattu sur le jeu d'instruction ARM. Le processeur né de ce projet est le Tegra K1-64 CPU. Il s'agit d'un processeur composé d'un cœur VLIW, couplé à deux décodeurs d'instructions ARM. Les deux décodeurs ARM traduisent une paire d'instructions ARM consécutives en instructions VLIW, pour les exécuter sur le cœur VLIW. Le CPU peut fonctionner selon deux modes : ARM et VLIW. En somme, le processeur supporte deux jeux d'instruction : le jeu d'instruction ARM, et un jeu d'instruction VLIW. En mode ARM, les instructions ARM sont chargées depuis le cache, traduites en VLIW par les décodeurs ARM, puis exécutées par le cœur VLIW. En mode optimisé, le CPU exécute des instructions VLIW chargées depuis le cache d'instruction, les décodeurs ne sont pas utilisés. : Le CPU supporte en réalité deux jeux d'instruction ARM : ARM8 et 7. Mais ils correspondent au même mode pour le CPU. Un point important est que le processeur peut changer très rapidement de mode, en à peine quelques cycles d'horloges. On n'est clairement pas dans le cas des CPU x86, qui mettent des plombes pour passer du mode 32 à 64 bits et inversement. La commutation est tellement rapide qu'on peut considérer que le processeur supporte deux jeux d'instruction simultanément. Et le support simultané de deux jeux d'instruction facilite l'implémentation de la traduction binaire. Encore une fois, inutile de traduire du code qui ne sera exécutée qu'une seule fois, mieux vaut l'exécuter directement dans le mode ARM. Surtout que le processeur supporte un mode ARM qui permet d'exécuter du code ARM sans perte de performance. Par contre, le code fréquemment utilisé, notamment dans des boucles critiques, est traduit en VLIW. Au final, les performances sont optimisées, en limitant le travail du logiciel traducteur binaire. La détection du code fréquemment exécuté se fait différemment que pour les processeurs Transmetta et Daisy. L'unité de branchement ne détecte pas directement le code beaucoup exécuté, même si elle a un rôle à jouer. A la place, la détection est réalisée en logiciel, par un ''thread'' dédié, qui s'exécute sur un cœur séparé. Il détecte le code fréquemment utilisé en regardent les branchements exécutés récemment. Pour cela, le processeur mémorise l'historique des branchements pris récemment et copie cette historique dans une mémoire tampon dédiée, partagée entre tous les cœurs. Vu qu'elle est partagée, le ''thread'' de détection a accès à l'historique sans pertes de performances, seule la copie de l'historique dans ce tampon a un cout en performance, pas son partage. Le code VLIW obtenu après traduction binaire est mémorisé dans la mémoire RAM, dans une portion spécialement réservée pour. Elle fait 128 mébioctets, et est appelée le '''cache d'optimisation''' par NVIDIA. Le cache d'optimisation est protégé en écriture et seul le traducteur binaire peut écrire dedans. Au passage, le traducteur binaire est du code VLIW, ce qui fait qu'il est placé dans le cache d'optimisation. Le processeur détecte automatiquement quand une fonction pour laquelle le code compilé équivalent est disponible. Pour cela, l'unité de chargement contient une table de correspondance entre l'adresse de la fonction ARM, et l'adresse de son équivalent compilé. Quand une fonction est appelée, l'unité de chargement regarde l'adresse de destination du branchement. Si l'adresse est dans cette table, elle récupère l'adresse de la fonction compilée et branche vers celle-ci. Le processeur dispose d'un cache d'instruction de 128 kibioctets, ce qui est très important, mais nécessaire vu la taille des instructions VLIW. Le coeur VLIW dispose de ses propres décodeurs, d'un ''scoreboard'', de 7 unités de calcul et de registres. Pour les unités de calcul, il a deux FPU, deux ALU, une unité de branchement, et deux unités LOAD-STORE faisant aussi ALU. Un point important est que le CPU dispose de 64 registres entiers et de 64 registres flottants. C'est deux fois plus que supporte le jeu d'instruction ARM. En mode ARM, seule la moitié des registres est utilisée. Mais en mode optimisé, le traducteur binaire utilise bien les 64 registres, grâce à une sorte de renommage de registres logicielle. Pour simplifier la traduction binaire, le processeur supporte les techniques vues précédemment. Le processeur intègre un ''gated store buffer'' similaire à celui des processeurs Transmetta. Les techniques d'exceptions différées sont aussi supportées, comme sur les processeurs Transmetta. Il a aussi une unité de préchargement décrite par NVIDIA comme agressive, avec support du préchargement de type anticipé (''runahead''). ===L'émulation de la mémoire virtuelle=== Pour finir, il faut parler de la mémoire virtuelle avec la traduction binaire. Sur le processeur source, le processeur gère à la fois des adresses physiques et virtuelles et sa MMU fait la conversion entre les deux. Sur le processeur VLIW, la MMU est simulée par le programme de traduction binaire, partiellement ou totalement. L'implémentation exacte varie suivant que l'on parle du projet Daisy, des processeurs Transmetta ou de Denver. Sur les processeurs Transmetta, il n'y a pas de MMU ni de mémoire virtuelle. A la place, le programme de traduction binaire émule la mémoire virtuelle du processeur source. Le processeur VLIW ne gère que des adresses physiques, rien d'autre. L'espace d'adressage physique du processeur VLIW a la même taille que celui du processeur émulé, ici des processeurs x86. Il est vraisemblable que les adresses physiques utilisées par le processeur x86 sont les mêmes que celles du processeur VLIW. Sur le projet Daisy, le processeur VLIW gère la mémoire virtuelle via pagination, comme les processeurs émulés. Ce qui fait qu'il gère un espace d'adressage virtuel et un espace d'adressage physique. Pour éviter toute confusion, nous parlerons d'adresse physique/virtuelle VLIW pour les adresses physiques/virtuelles du processeur VLIW, d'adresse virtuelle/physique source pour celle du jeu d'instruction traduit, à savoir du Power PC ou du s390, éventuellement de l'x86. Il faut alors faire le lien entre adresses physiques source et adresses virtuelles VLIW. Pour cela, rien de plus simple : il y a correspondance parfaite. L'adresse physique source numéro N correspond à l'adresse virtuelle VLIW numéro N. Cependant, cela signifie que tout l'espace d'adressage virtuel VLIW serait utilisé par le code à traduire. En réalité, il faut ajouter de la place pour le code traduit et le programme de traduction binaire. La conséquence est que l'espace d'adressage virtuel VLIW est plus large que l'espace d'adressage source. L'espace d'adressage virtuel VLIW est découpé en trois sections : une section pour le code Power PC à traduire, une autre pour le code traduit en VLIW, et une dernière réservée au traducteur binaire. Le programme de traduction est placé dans une ROM mappée en mémoire dans la seconde section. Le reste de la seconde section est réservé à une mémoire RAM utilisée par le programme de traduction. [[File:Mémoire virtuelle sur le projet Daisy.png|centre|vignette|upright=1.5|Mémoire virtuelle sur le projet Daisy]] Il faut noter que chaque page physique source, une fois traduite, correspond à N pages virtuelles VLIW. La raison est que le code traduit prend plus de place que le code machine originel. En conséquence, on doit utiliser une page finale plus grande. Pour le reste, des pages contiguës en mémoire physique source sont elles aussi contiguës en mémoire virtuelle VLIW. Le calcul d'adresse est donc simple : l'adresse physique source est multipliée par N, puis on ajoute l'adresse de base à laquelle commence la section pour le code traduit. ==La traduction binaire sur le Burroughs B1700== Nous venons de voir comment accélérer en matériel la traduction binaire, avec des techniques bien spécifiques. Le Burroughs B1700 a procédé autrement et il est intéressant d'étudier en détail son architecture. Son architecture est décrite dans le livre "Interpreting Machines : Architecture and Programming of the Bl700/Bl800 Series", écrit par Elliott I. Organick et James A. Hinds. Une partie de ce qui vfa suivre est un très court résumé de ce livre. ===Le Burroughs B1700 : le mal-nommé ''split-level control store''=== Le Burroughs B1700 avait plusieurs modèles : le B1726, le B1710 et le B1800. Ils se distinguaient par la manière dont le code machine source était traduit par le traducteur binaire. Dans ce qui suit, nous parlerons de traducteur binaire pour parler soit d'un interpréteur pour un langage de programmation de haut niveau, soit pour un logiciel de traduction binaire. Le B1710 plaquait le traducteur binaire en mémoire RAM. Le B1800 faisait pareil, mais avait ajouté un cache dédié au code du traducteur binaire, afin d'améliorer les performances. Le B1726 avait un traducteur binaire scindé en deux : une partie était en mémoire RAM, l'autre était dans un ''local store'' dédié. Le processeur peut exécuter le traducteur binaire soit depuis le ''local store'', soit depuis la mémoire RAM avec une pénalité en termes de performances. La documentation appelle cela un '''''split-level control store''''', mais le terme est trompeur : la documentation qualifie de microcode le langage machine du Burroughs B1700, ce qui est à l'origine de beaucoup de confusions. Une autre source de confusion est que ce langage machine a quelques propriétés qu'on pourrait attendre d'un microcode, mais certaines sont manquantes. Par exemple, il peut adresser les registres d’interfaçage mémoire, mais c'est parce que ce sont des registres architecturaux sur ce processeur. Ses instructions n'encodent pas de signaux de commande, par contre, elles font 16 bits et adressent les opérandes de manière implicite. Le ''local store'' mémorisait au maximum 2048 instructions machine, ce qui fait que les 2048 premières "adresses" correspondaient au ''local store''. Les suivantes étaient des adresses de mémoire RAM qui contenaient le reste du traducteur binaire. En pratique, l'adresse qui séparait les deux était configurable ! On pouvait limiter la taille du ''local store'' à un multiple de 32 instructions ! Les adresses restantes étaient alors réattribuées à la RAM. Par exemple, on pouvait attribuer 64 adresses au ''local store'', les 2048 - 64 adresses restantes étaient alors attribuées à la mémoire RAM. Un registre limite, nommé TDPM, indiquait quelle était l'adresse de démarcation entre les deux. Il contenait 4 bits, dont la valeur était multipliée par 32, pour obtenir l'adresse limite. Le programmeur devait découper le traducteur binaire en segments, certains étant placé dans le ''local store'', les autres étant en mémoire RAM. Mieux que ça, le processeur autorisait d'utiliser la technique de l'''overlaying'' pour le microcode ! Il y avait même une fonction OVERLAY dédiée pour ! OVERLAY avait besoin de trois opérandes : la taille du segment à copier, son adresse source en mémoire RAM, son adresse de destination dans la micro-SRAM. L'interpréteur réservait un "segment" en mémoire RAM, pour les données, dont le processeur connaissait la position. En-dehors de ce segment, il n'y a que des instructions machines à traduire, et le traducteur binaire. Le CPU incorporait pour cela deux registres, un pour l'adresse de base du segment de données, un autre pour son adresse de fin. Il incorporait une protection mémoire limitée sur ce segment, à savoir que seul l'interpréteur pouvait lire ou écrire dans ce segment. ===Une architecture bit-adressable=== Le CPU Burroughs B1700 était '''bit-adressable'''. En clair, il pouvait adresser la mémoire bit par bit, si nécessaire. Un avantage est que cela facilitait la traduction binaire des instructions machine, mais nous détaillerons cela plus bas. Un autre avantage est que cela permettait d'émuler des architectures dont la taille des registres/''byte''/mots était très différente. Et cet avantage mérite quelques explications immédiates. Le processeur était un CPU 24 bits, mais il pouvait émuler des processeurs 16 bits, 8 bits, 4 bits, 1 bit, 9 bits, ou toute autre valeur. Pour cela, le processeur lisait des mots de 24 bits et utilisant un ''barrel shifter'' pour sélectionner les bits adéquats. Par exemple, pour un accès de 16 bits, la donnée lue en mémoire était masquée de manière à ne garder que les 16 bits de poids faible. Le ''barrel shifter''/circuit de masquage était placé directement avant le bus mémoire, sur le trajet des données/instructions. Le ''barrel shifter'' était commandé par un registre ''Field Unit'', qui précisait quelle était la taille des données à charger. Pour communiquer avec la mémoire, il avait trois registres séparés : READ, WRITE, MAR. READ contient la donnée lue lors d'une lecture, WRITE est pour une donnée à écrire lors d’une écriture, MAR contient l'adresse à lire ou en cours de lecture. Il s'agit de registres d’interfaçage mémoire. Le fait que ces registres d’interfaçage mémoire et IO soit adressable est peu commun, et c'est plus quelque chose qu'on attend d'un microcode que d'instructions machines. Mais il faut bien comprendre que les registres d’interfaçage mémoire ont été promus au rang de registres architecturaux, sur ce processeur. De même, pour communiquer avec les entrées-sorties, il disposait de deux registres CMND et DATA : CMND pour envoyer une commande à une entrée-sortie, DATA pour lire ou écrire une donnée. Il y avait aussi un registre U, qui mémorisait une donnée lue depuis le lecteur de cassette. ===L'ALU de taille variable=== Les Burroughs B1700 intégrait une pile d'adresse de retour, capable de mémoriser 16 adresses de 24 bits. Il disposait de 4 registres généraux, nommés X, Y, T et L, tous de 24 bits. Un point important est que le registre T avait des possibilités d’extraction spécifiques. Il était possible d'extrait N bits, placés n'importe où dans ce registre, et de les copier dans un autre registre. Nous verrons pourquoi cette possibilité est intéressante et ne sert que pour le registre T. Ils avaient une '''ALU de taille variable'''. En clair, l'ALU pouvait faire des calculs sur un nombre de bits compris entre 0 et 24. Pour cela, les calculs étaient fait avec une ALU de 24 bits, puis les bits de poids fort inutiles étaient masqués. L'ALU de 24 bits pouvait masquer les bits de poids fort du résultat, mais avec des limitations. Elle gérait des opérandes et résultats de 0, 4, 8, 12, 16, 20 et 24 bits. Pour avoir un réglage plus fin, une ALU 4 bits séparée permettait de corriger le résultat. Les registres X et Y étaient reliés à une ALU de 24 bits capable d'effectuer des opérations arithmétiques basiques. Fait étonnant, l'ALU faisait tous les calculs en même temps et fournissait ses résultats dans 7 registres : un registre SUM pour l'addition, un registre DIFFERENCE pour la soustraction, et un registre par opération bit à bit. Les registres pour les opérations bit à bit sont : CMPX et CMPY pour l'opération NOT sur chaque opérande, XANY pour le ET bit à bit, XEOY pour le XOR, XORY pour le OU logique. Les additions et soustractions pouvaient se faire sur des opérandes codés en binaire ou en BCD, suivant le code opération utilisé. A cela, il fallait ajouter deux registres de "résultat" MSKX et MSKY. Ils fournissaient un masque dépendant de la taille du résultat en bits. C'est ce registre qui permettait de gérer des données de 0, 4, 8, 12, 16, 20, et 24 bits, alors que l'ALU faisait 24 bits. Le masque dans ce registre pouvait servir d'opérande dans une opération de masquage ultérieure, pour corriger le résultat. L'ALU prenait en opérande les registres X et Y, mais aussi un registre CYF pour la retenue entrante. En sortie, il fournissait aussi deux retenue sortantes : une pour l'addition et une autre pour la soustraction. Elles étaient mémorisées dans les registres CYL et CYD, respectivement. L'ALU fournissait aussi 12 conditions, qui alimentait un registre d'état. Le registre d'état était segmenté en trois registres appelés XYST, XYCN et BICN. Les registres X et Y sont des registres pour les opérandes, mais il y a aussi deux autres registres de 24 bits. Il s'agit des registres T et L, qui sont des registres généraux. Les registres T et L étaient découpés en 6 sous-registres de 4 bits, nommés LA LB LC LD LE et LF pour le registre L, et TA TB TC TD TE et TF pour le registre T. Ils étaient adressables comme n'importe quel registre. Le processeur contenait de nombreux autres registres de 4 bits, dont les sous-registres de T et L. En tout, il y a 27 registres de 4 bits adressables ! Et le processeur disposait d'une ALU 4 bits pour manipuler ces registres de 4 bits ! Elle est capable de faire des MOV entre registres de 4 bits, des opérations bit à bit, et peut aussi tester si un bit vaut 0 ou 1. La dernière possibilité est très utile pour implémenter les branchements. De plus, cela permettait de faire des opérations de masquage, notamment pour appliquer un masquage supplémentaire à celui de l'ALU. Par exemple, pour gérer des opérations sur 14 bits, on effectue une opération sur 16 bits avec l'ALU, et on masque les 2 bits manquants avec l'ALU 4 bits. La capacité d'extraction du registre T pouvait aussi être utilisé pour ça. ===Le code machine du Burroughs B1700=== Le processeur utilise des instructions de 16 bits de long, qui sont copiées dans registre d'instruction M de 16 bits. Le ''program counter'' est appelé le registre A. Les instructions du processeur font 16 bits de long, et elles sont alignées en RAM sur 16 bits, ce qui fait que les 4 bits de poids fort de ce registre valent 0, seuls les 14 bits de poids fort sont utiles. Les adresses mentionnées plus haut, qui adressent les micro-opérations en mémoire RAM, sont des adresses de mots de 16 bits, pas les adresses de bit individuels. Fait intéressant, il était possible de faire un OU logique entre l'instruction lue et un registre du processeur, le résultat étant mémorisé dans le registre M. Cela permettait de faire du code automodifiant, pour émuler des modes d'adressage complexe. Cela permettait par exemple de remplacer une adresse dans une micro-opération. L'adresse est alors surimposée avec un OU dans le champ d'adresse, alors que le reste de l'instruction n'est pas modifié. Afin de gérer les interruptions, ces dernières étaient partiellement détectées en logiciel. Une interruption doit être prise en compte à la toute fin de l'exécution d'une instruction source, quand celle-ci a finit son travail. Mais vu qu'ici, les instructions sont émulées avec des séries d'instructions cibles, il y a un problème : il est possible d'interrompre une instruction émulée en plein milieu. Pour éviter cela, le processeur ne déclenche pas les interruptions matérielles immédiatement, elles sont mises en attente. Une instruction dédiée vérifie si une interruption est en attente, et l'exécute alors cas échéant. L'instruction en question est utilisée en dernier, dans la suite d'instruction qui émule l'instruction source. ===La traduction binaire sur le Burroughs B1700=== Les instructions source peuvent faire 16 bit avec tel jeu d'instruction, 8 bits avec un autre, 32 bits sur un autre, etc. Elles peuvent aussi être de longueur fixe ou variable ! Le B1700 utilisait une architecture bit-adressable pour gérer ces contraintes. En effet, avec un CPU bit-adressable, les contraintes d'alignement des instructions disparaissent ! Dans ce qui suit, on suppose que l'instruction est composée de '''champs''' : l'opcode, les opérandes, les adresses absolues, les constantes immédiates, les numéros de registre, etc. L'idée est que l'instruction source est chargée dans le registre T, puis découpée en champs, qui sont copiés dans les registres du processeur ou envoyés au microcode. Je rappelle que le registre T est relié à un ''barrel shifter'', différent au précédent, qui permettait de masquer, décaler et extraire une donnée de ce registre. Et il y a la possibilité d'extraire une suite de bit de ce registre T, pour la copier dans un autre registre. Par exemple, prenons une instruction composée d'un opcode, d'une constante immédiate et d'une adresse immédiate. Premièrement, elle est chargée dans le registre T. Deuxièmement, la constante immédiate est extraite du registre T et copiée dans le registre X. Troisièmement, l'adresse est extraite et est copiée dans le registre d’interfaçage mémoire, une lecture est démarrée, et l'opérande lue est copiée dans le registre Y. Enfin, l'instruction équivalente à l'opcode est exécutée par le microcode. Le tout prend plusieurs instructions : une première pour lire l'opcode, une seconde pour lire la première opérande, une troisième pour lire la seconde opérande, una quatrième pour tout exécuter. Notons que suivant la taille des champs, on peut en charger plusieurs à la fois. Par exemple, imaginons qu'on ait chargé 24 bits, contenant un opcode de 8 bits et une constante immédiate de 16 bits : le registre T est découpé en un opcode et une constante, avec deux instructions EXTRACT. L'architecture est bit-adressable car elle gère des champs qui ne font pas forcément 8 ou 16 bits. Par exemple, certains jeux d'instructions ont des opcodes codés sur 6 ou 7 bits, voire 9-10 bits. Utiliser une architecture bit-adressable règle ce problème d'alignement. On peut lire un champ depuis la mémoire RAM en précisant l'adresse de son premier bit, pas besoin de gérer des accès non-alignés. Par exemple, prenons une instruction source avec un opcode de 7 bits, suivi par une adresse de 12 bits et une constante immédiate de 15 bits. On peut charger 7 + 12 bits en un premier accès, pour extraite l'opcode et l'adresse, suivi par un second accès de 15 bits pour extraire la constante immédiate. Pour charger un champ, il faut connaitre plusieurs informations : l'adresse de son premier bit, sa taille. Pour cela, le B1700 contient deux registres, nommés ''Field Adress'' (FA) et ''Field Length'' (FL), qui indiquent l'adresse du premier bit et la taille de l'instruction à charger. Le premier fait 24 bits, ce qui permet de gérer des adresses de 24 bits. N'oubliez pas que le processeur est bit-adressable, ce qui permet d'adresser 16 Mébi-bits, soit 2 mébioctets. Le registre pour la taille fait lui 16 bits, ce qui permet de gérer des instructions de 65536 bits, soit 8192 octets ! Leur nom commence par ''field'', car le processeur peut gérer des instructions de taille variable et/ou plus longues que 24 bits. Dans ce cas, le chargement de l'instruction se fait champ par champ. Pour donner un exemple, prenons une instruction composée de 20 champs de 12 bits chacun. Les champs sont chargés un par un dans le registre T. Le registre FA est alors initialisé avec l'adresse de l'instruction, il est incrémenté de 12 à chaque cycle. Le registre FL est initialisé avec la taille de l'instruction, soit 20 * 12 = 240 bits, et il est décrémenté de 16 à chaque cycle. Précisons cependant que la taille d'un champ peut être changée d'un cycle à l'autre. Par exemple, le processeur peut charger un opcode de 8 bits, l'interpréter, détecter que l'instruction demande ensuite de charger une adresse de 16 bits. Dans ce cas, le registre FL est immédiatement altéré pour charger 16 bits, au lieu des 8 bits de l'opcode immédiatement précédent. ==Les jeux d'instructions dédiés à un langage de programmation== De rares processeurs sont conçus pour un langage de programmation en particulier. On appelle ces processeurs, conçus pour des besoins particuliers, des '''processeurs dédiés'''. Par exemple, les fameux ''Burrough E-mode'' B5000/B6000/B7000 étaient spécialement conçus pour exécuter de l'ALGOL-60. Leurs cousins B2000/B3000/B4000 étaient eux conçus pour le COBOL. Des langages fonctionnels ont aussi eu droit à leurs processeurs dédiés. Le prolog en est un bel exemple, avec les superordinateurs de 5ème génération qui lui étaient dédié. On peut aussi citer les machines LISP, dédiés au langage LISP, qui datent des années 1970. Elles étaient capables d’exécuter certaines fonctions de base du langage directement dans leurs circuits : elles possédaient notamment un ''garbage collector'' câblé dans ses circuits ainsi que des instructions machines supportant un typage déterminé à l’exécution. Les processeurs dédiés ont eu leur heure de gloire au début de l'informatique, à une époque où les langages de haut niveau venaient d'être inventés. À cette époque, les compilateurs n'étaient pas performants et ne savaient pas bien optimiser le code machine. Il était alors rationnel, pour l'époque, de rapprocher le code machine cible et le langage de programmation de haut niveau. De nombreuses architectures dédiés ont ainsi été inventées, avant que les concepteurs se rendent compte des défauts de cette approche. Les défauts en question ne sont pas nombreux, mais assez simples à comprendre. Premièrement, elles sont très rapides pour un langage de programmation en particulier, mais sont assez mauvaises pour les autres, d'où un problème de "compatibilité". Ajoutons à cela que les langages de programmation peuvent évoluer, devenir de moins en moins populaires/utilisés, ce qui rend la création d'architectures généralistes plus pertinente. Enfin, les architectures dédiées sont évidemment des processeurs CISC, pour implémenter les nombreuses fonctionnalités des langages évolués. Et les défauts des CISC sont assez rédhibitoires à l'heure actuelle. À l'heure actuelle, les algorithmes des compilateurs se sont améliorés et savent nettement mieux utiliser le matériel. Ils produisent du code machine efficace, ce qui rend les architecture dédiées bien moins intéressantes. Si on ajoute les défauts de ces architectures dédiées, par étonnant que les architectures dédiées aient presque disparues. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'accélération matérielle de la virtualisation | prevText=L'accélération matérielle de la virtualisation | next=Le matériel réseau | nextText=Le matériel réseau }} </noinclude> 277i2e7yi5f57nqk9nc1gas4h8golx0 764055 764054 2026-04-19T20:43:53Z Mewtow 31375 /* L'ALU de taille variable */ 764055 wikitext text/x-wiki De nos jours, la majorité des programmeurs programment dans des langages de haut niveau. Il est très rare pour eux d'avoir à utiliser de l'assembleur, et encore moins un langage machine. Les programmes écrits dans un langage de haut niveau sont traduits en langage machine par un logiciel appelé le compilateur. [[File:Compilation.PNG|centre|vignette|upright=2.5|Compilateur : principe.]] Il se trouve que quelques rares processeurs sont conçus pour faciliter le travail du compilateur. Et ce sont ces processeurs que nous allons voir dans ce qui suit. Ils représentent une catégorie de jeux d'instruction à part, qui n'a pas vraiment de nom. ==La compilation et l’interprétation== Un compilateur traduit donc du code source, écrit dans un langage de haut niveau, vers du langage machine. La traduction est rarement directe. En général, le compilateur traduit le code source en assembleur, qui est lui-même traduit en langage machine. L'assembleur est une représentation textuelle du langage machine alors que le code machine est du binaire exécutable par le processeur. La traduction se fait donc en deux étapes, la '''compilation''' proprement dite et l''''assemblage''', réalisées respectivement par un compilateur et un assembleur. Il faut parfois rajouter une troisième phase d'édition des liens, que nous passons volontairement sous silence. {| |[[File:Code C.png|vignette|200px|Le code source, ici en C.]] |[[File:Assem.png|vignette|250px|Le code assembleur.]] |[[File:Binary file - hello world (C programming).png|vignette|150px|Le code machine.]] |} ===L'interprétation et la compilation à la volée=== Une alternative à la compilation est l''''interprétation''', qui transforme le code source en code machine à la volée. Avec l'interprétation, le code source est passé à un logiciel appelé l''''interpréteur''', qui exécute le code source ligne de code par ligne de code. Une ligne de code est traduite en code machine, qui est exécutée, puis l'interpréteur passe à la ligne suivante. [[File:Kaantotulkkaus.png|centre|vignette|upright=3|Compilation versus interprétation.]] Un défaut de l'interprétation est son cout en performance, qui est assez important. Aussi, l'interprétation stricte a évolué vers un hybride entre interprétation et compilation. L'idée est qu'une partie peu importante du code source est interprétée, alors que le code important est compilé juste avant d'être exécuté. Pour le dire autrement, le code source est partiellement compilé à la volée, juste avant son exécution. Aussi, on parle de '''compilation à la volée'''. Le terme en anglais est ''Just In Time compilation'', abrévié en JIT. En soi, le JIT est associé à de l'interprétation, les JIT sont en réalité des hybrides interpréteurs-compilateurs. Un point important est que compiler du code à la volée est assez lourd, cela a un cout en performance. Par contre, le code compilé s'exécute plus vite que du code interprété. Si on exécute le code compilé une seule fois, le cout de la compilation l'emporte sur le gain à l'exécution. Mais le code compilé est réutilisé autant de fois que nécessaire. Le gain à l'exécution est donc multiplié par le nombre d'exécutions, alors que le cout de la compilation est répartit sur plusieurs exécutions, il est amorti. Les portions du code sont choisies de manière à avoir un gain en performance maximal. L'idée générale est que le code exécuté très souvent est compilé, alors que du code exécuté pas souvent est interprété. Typiquement, les boucles les plus souvent exécutées sont compilées, le code exécuté une seule fois est interprété. ===Les langages intermédiaires=== Les compilateurs modernes passent par un '''langage intermédiaire''' pour faire la transformation en code machine. Le compilateur traduit le langage de haut niveau en langage intermédiaire, puis traduit le langage intermédiaire en code machine cible. Le langage intermédiaire est parfois appelé le '''''bytecode''''', et ce terme recouvre aussi le code écrit avec du ''bytecode''. L'interprétation aussi peut passer par l'intermédiaire d'un un ''bytecode'', ce n'est pas limité aux compilateurs proprement dit. {| |[[File:Intermediate representation scheme.png|centre|vignette|upright=1.5|Représentation intermédiaire d'un compilateur.]] |[[File:Code interpreter scheme.png|centre|vignette|upright=1.5|Fonctionnement d'un interpréteur.]] |} Faire ainsi a de nombreux avantages, le principal étant d'avoir un compilateur capable de traduire un langage de haut niveau vers plusieurs jeux d'instructions différents. Cela permet d'avoir, par exemple, un compilateur qui traduit du C soit en code machine x86, soit en code machine pour un CPU ARM, soit pour un CPU POWERPC, etc. [[File:Compiler design.svg|centre|vignette|upright=3|Compiler design]] Pour l'interprétation, l'usage d'un ''bytecode'' a d'autres avantages. Pour l'exploiter, il faut distribuer non pas le code source, mais le ''bytecode''. En clair, l'interpréteur ne prend pas en entrée le code source, mais du ''bytecode'' déjà compilé. La traduction est alors beaucoup plus simple, car le code source a déjà été partiellement compilé, par la traduction en ''bytecode''. Interpréter du code source est en effet assez compliqué : il faut effectuer des étapes d'analyse lexicale, sémantique, et bien d'autres. Avec du ''bytecode'', ces étapes ont été réalisées lors de la compilation du ''bytecode'', l'interpréteur a alors peut de choses à faire. L'interprétation du ''bytecode'' se fait instruction par instruction, au niveau du ''bytecode''. Une instruction du ''bytecode'' est traduite en une instruction machine équivalente, qui est exécutée. Si l'instruction est un peu complexe, il exécute une fonction/procédure qui fait la même chose. Pour résumer, le ''bytecode'' est ensuite traduit à la volée et exécuté instruction par instruction par un logiciel appelé l'interpréteur. [[File:Control table.png|centre|vignette|upright=2|Control table]] Le langage intermédiaire peut être vu comme l'assembleur d'un processeur, n'existe pas forcément dans la réalité, mais dont le jeu d'instruction est décrit en détail. Le processeur en question est appelé une '''machine abstraite''', ou encore une ''machine virtuelle''. Nous utiliserons le terme de machine abstraite dans ce qui suit. La machine abstraite n'est pas la même suivant que l'on cible la compilation ou l'interprétation/JIT. Pour l'interprétation/JIT, la machine abstraite est souvent une machine à pile, car cela simplifie grandement la traduction en code machine final. L'interprétation demande que la traduction du ''bytecode'' en code machine soit la plus simple possible. Et cela demande de prendre en compte pas mal de détails. Par exemple, les différents jeux d'instruction existants n'ont pas le même nombre de registres, ce qui pose problème lors de la traduction du ''bytecode''. Lors de la transformation en code machine, un algorithme d'allocation de registres se débrouille pour traduire le code intermédiaire en code qui utilise un nombre limité de registres. Pour améliorer cette allocation de registres, il y a deux solutions niveau ''bytecode'' : soit utiliser un nombre illimité de registres, soit utiliser une machine à pile. Pour les machines à pile, il existe un algorithme simple et rapide pour traduire un code écrit pour une machine à pile en un code écrit pour un processeur avec des registres, qui se débrouille pas trop mal pour allouer efficacement les registres. C'est un avantage assez important pour les langages interprétés. ==Les processeurs qui exécutent du bytecode== Le ''bytecode'' est un code machine, ce qui signifie qu'il peut en théorie s'exécuter sur un processeur qui implémente le jeu d’instruction associé. Si le jeu d'instruction d'un ''bytecode'' est souvent une description censée être fictive, elle n'en reste pas moins un jeu d'instruction et des caractéristiques précises, qu'on peut l'implémenter en matériel ! Néanmoins, tous les ''bytecode'' ne sont pas égaux de ce point de vue. Certains sont faciles à implémenter en matériel, d'autres non. Les ''bytecodes'' implémentés en matériel sont ceux dont les machines abstraites sont des machines à pile. Un premier exemple est celui des processeurs Pascal MicroEngine, qui exécutaient directement le bytecode du langage Pascal, le fameux UCSD P-code. Un second exemple est la '''machine SECD''', qui sert de langage intermédiaire pour certains compilateurs de langages fonctionnels. Elle a été implémentée en matériel par plusieurs équipes, notamment par les chercheurs de l'université de Calgary en 1989. Dans le même genre, quelques processeurs simples étaient capables d’exécuter directement le ''bytecode'' du langage FORTH. Le FORTH, un des premiers langages à pile de haut niveau, possède de nombreuses implémentations hardware et est un des rares langages de haut niveau dont le ''bytecode'' a été utilisé comme langage machine sur certains processeurs. Par exemple, on peut citer le processeur FC16, capable d’exécuter nativement du ''bytecode'' FORTH. ===Les processeurs Java=== Le cas le plus impressionnant est celui de la machine virtuelle Java, qui est un design de processeur comme un autre. En temps normal, le ''bytecode'' Java est compilé ou interprété, mais certains processeurs exécutaient du ''bytecode'' Java directement, sans interprétation ni compilation. Ils sont appelés des '''processeurs Java'''. On peut citer les processeurs ARM disposant de l'extension Jazelle, du JEMCore et du aJ-100 de aJile Systems, le picoJava II de Sun, et quelques autres. Il y a aussi eu quelques projets de recherche ou processeurs open source, comme Komodo, jamuth, le ''Java Optimized Processor'' et quelques autres projets du même genre. L'implémentation de la machine virtuelle Java n'était cependant pas complète. Quelques instructions complexes n'étaient pas gérées par le processeur et devaient être émulées en logiciel. Mais la grosse majorité des instructions l'était. Les instructions non-supportées étaient émulées avec la méthode ''trap and emulate'', à savoir qu'un opcode inconnu déclenchait une exception matérielle ''opcode invalide'', dont la routine pouvait émuler les instructions qui doivent l'être. Le processeur aJ-100 de aJile est capable d'exécuter la quasi-totalité des instructions de la JVM Java, seules deux instructions faisant exception. Les '''accélérateurs Java''' sont une solution intermédiaire, qui permet de traduire à la volée du ''bytecode'' Java en instructions RISC, exécutées sur un autre processeur. Il s'agit formellement de traduction binaire, mais je préfère en parler ici. L'idée est de combiner un processeur RISC avec un coprocesseur Java. Le coprocesseur lit le ''bytecode'' Java et le traduit en instructions RISC, ces dernières étant exécutées sur le processeur RISC. Le JA108 de Nozomi était un coprocesseur de ce genre, au même titre que le coprocesseur JSTAR de JEDI. L'extension Jazelle de certains CPU ARM fonctionnait vraisemblablement sur un principe similaire, sauf que le coprocesseur était intégré dans le processeur ARM, entre le cache d'instruction et le décodeur d'instructions ARM. L'intérêt des processeurs Java n'est pas qu'une question de performance. Certes, ils permettent d’exécuter plus vite les programmes compilés en ''bytecode'', comme des programmes Java pour la JVM Java. Mais l'intérêt est aussi de faire des économies de mémoire RAM et ROM. Je rappelle que la machine virtuelle Java est une machine à pile, ce qui fait que sa densité de code est excellente. Les programmes en ''bytecode'' Java sont donc très petits, et prennent peu de place en mémoire ROM. De plus, utiliser un processeur Java permet de se passer d'interpréteur ou de JIT Java, ce qui économise encore plus de mémoire. ===Transformer un processeur RISC en machine à pile=== La plupart des processeurs Java utilisent en interne une architecture à registre généraux, qui émule une machine à registre en microcode. Quelques registres sont réservés pour l'état de la machine virtuelle, avec au minimum un registre réservé pour le pointeur de pile, un autre pour un pointeur vers un ''pool'' de constantes, et quelques autres. D'autres registres servent à mémoriser le haut de la pile, à savoir le sommet de la pile et quelques opérandes situées juste en-dessous. Par exemple, le processeur picoJava II gardait les 64 opérandes au sommet de la pile dans 64 registres généraux, séparés des registres pour les pointeurs et l'état de la JVM. Une optimisation possible, envisagée sur les processeurs picoJava, était une sorte de macro-fusion sous stéroïde. L'idée était de fusionner certaines séries d'instructions Java en une seule instruction machine. Typiquement, pour faire une opération arithmétique, une machine à pile empile deux opérandes et exécute une opération, ce qui prend trois instructions machine. La spécification picoJava 1 a proposé d'envoyer directement la seconde opérande en entrée de l'ALU, ce qui fusionnait l'empilement de la seconde opérande et l'opération arithmétique. Le processeur picoJava II va plus loin et fusionne les trois instructions en une seule instruction RISC, agissant sur des registres. Mais la majorité des processeurs Java ne faisaient pas cela, car cela complexifie grandement le décodeur. Les processeurs ''bigfoot'' de DCT utilisaient un système différent pour émuler une pile avec des registres. Leur banc de registre de 64 bits était coupé en deux : les 16 premiers registres étaient utilisés pour la pile, les 48 suivants étaient utilisés pour autre chose. L'idée est que le registre 0 mémorise un emplacement vide, le registre 1 contient le sommet de la pile, le registre 2 l'opérande sous le sommet de la pile, et ainsi de suite. Les 15 premières opérandes de la pile sont donc mémorisées dans les registres. Les instructions écrivent leur résultat dans le registre adéquat, puis tous les registres sont décalés pour que le résultat passe dans le registre R1. Par exemple, les instructions PUSH chargent une opérande dans le registre 0, puis tous les registres sont décalés de un rang, pour que l'opérande chargée soit dans le registre 1. Le registre 0 devient alors le registre numéro 1, le registre 1 devient le 2, et ainsi de suite. Par contre, les opérations arithmétiques font autrement. Une instruction d'addition, par exemple, lit les opérandes dans le registre 1 et 2, puis place son résultat dans le registre 2 (les deux opérandes sont dépilées). Puis, les registres sont décalés d'un rang pour que ce registre 2 devienne le 1. Les registres ne sont en réalité pas décalés, il n'y a pas de transferts entre registres. A la place, les 16 premiers registres utilisaient une forme limitée de renommage de registres, similaire au banc de registre tournant utilisé sur les processeurs EPIC comme l'Itanium. Les numéros de registres sont renommés, décalés, à chaque fois qu'on empile ou dépile une opérande. Pour faire le renommage de registres, processeur contient un ''register stack counter'' de 4 bits, qui est incrémenté à chaque fois qu'on empile une donnée, et est décrémenté quand on dépile une donnée. Quand on veut accéder à un opérande, le numéro de registre architectural associé est traduit en numéro de registre physique, en additionnant le ''register stack counter'' (modulo 16, pour rester sur 4 bits). ==L'accélération de l'interprétation/JIT : le ''Thumb-EE'' d'ARM== Quelques processeurs ont ajouté des instructions pour faciliter le travail des interpréteurs/JIT. Par exemple, en 2005, ARM a ajouté le mode ''Thumb Execution Environment'', qui faisait cela. Il reprenait le jeu d'instruction compact ''thumb'' et ajoutait quelques instructions et modifiait le comportement d'instructions existantes. Le jeu d'instruction ''Thumb-EE'' a été ajouté sur les CPU ARM en 2005, mais a été déprécié en 2011, par manque d'utilité. Mais la tentative mérite qu'on s'attarde dessus. Les CPU de gamme M, déstinés à l'embarqué, n'ont jamais supporté ''Thumb-EE'', vu que de tels processeurs ne sont pas conçus pour exécuter du code interprété/JIT. Le mode ''Thumb-EE'' est un mode d'exécution, séparé de l'ARM normal. Ainsi, une instruction ''Thumb-EE'' et une instruction ARM peuvent avoir le même encodage en binaire, mais se comporter différemment. Le processeur est à tout moment soit en mode ''Thumb-EE'', soit en mode ARM normal, ce qui précise comment ces instructions doivent se comporter. Des instructions ont été ajoutées par ''Thumb-EE'', pour qu'il fasse son travail. L'entrée et la sortie du mode ''Thumb-EE'' se fait avec deux instructions : ENTERX et LEAVEX. Une fois en mode ''Thumb-EE'', le décodage des instructions ''thumb'' se fait avec les règles du ''Thumb-EE'', et non celles du ''thumb'' normal. Une première différence entre ''thumb'' normal et ''Thumb-EE'' est l'ajout de ce qui s'appelle un ''null check'' pour les instructions mémoire. Le ''null check'' est utilisé pour les instructions en mode d'adressage "base + indice" ou "Base + décalage", ou tout autre mode d'adressage avec un registre de base utilisés dans des calculs d'adresse. L'idée est de vérifier si l'adresse de base vaut zéro ou non. Si c'est le cas, le processeur lève une exception matérielle, qui est traitée par l'interprétation ou le compilateur JIT. Une telle situation est en effet signe d'une erreur d'adressage mémoire, qui doit être traitée par l'interpréteur. Typiquement, c'est signe qu'un pointeur n'a pas bien été initialisé, ce qui peut arriver avec du code interprété ou JIT. Les interpréteurs/JIT traitent généralement la situation en ajoutant un test avant toute instruction mémoire, pour vérifier si le pointeur accédé vaut zéro ou non. Avec ''Thumb-EE'', pas besoin d'ajouter les tests en questions : ils sont réalisés automatiquement lors de chaque instruction mémoire. : Les registres pour la pile, comme le pointeur de pile et le pointeur de ''frame'', sont aussi concernés. L'instruction CHKA vérifie que les accès à un tableau ne débordent pas en dehors du tableau en question. Par exemple, pour un tableau de 1024 éléments, elle vérifie si l'indice est compris entre 0 et 1024. Pour cela, elle vérifie si l'indice de tableau est bien dans l'intervalle adéquat et lève une exception matérielle si ce n'est pas le cas. Son utilité se comprend quand on sait que tous les accès à un tableau sont vérifiés par l'interpréteur. L'interpréteur est censé ajouter des instructions pour vérifier les indices, à savoir deux ou trois branchements. Avec CHKA, le test se fait en une seule instruction. D'autres modifications mineures de l'encodage des instructions sont aussi présentes, ainsi que des modifications pour les instructions LOAD/STORE. Précisément, dans les modes "base + indice" et "base + décalage", le décalage et l'indice subissent maintenant des décalages. L'idée est de simplifier le calcul d'adresse. De quoi économiser quelques instructions lors des calculs d'adresse, ce qui facilite le travail de l'interpréteur/JIT. Et des instructions LOAD/STORE précises voient leur comportement modifié pour faciliter la gestion de la pile de Java ou l'exécution des méthodes locales. Pour faciliter le travail de l'interpréteur, deux instructions HBP et HBLP ont été ajoutées. Elles branchent vers une fonction qui gère les exceptions logicielles (une fonctionnalité présente dans de nombreux langages, comme Java). La première effectue un branchement vers la fonction en question, la seconde sauvegarde l'adresse de retour avant de faire ce branchement. Les deux peuvent brancher vers 256 fonctions pré-déterminées, sans avoir à présenter leur adresse. ==La traduction binaire accélérée par le matériel== Plus haut, nous avons surtout parlé des compilateurs et des interpréteurs. Cependant, nous devons aussi parler de la '''traduction binaire'''. Elle traduit un programme écrit dans un code machine vers un autre code machine. Par exemple, elle traduit un programme compilé pour un CPU x86 vers un code machine ARM. En général, le système d'exploitation est généralement compilé pour le jeu d'instruction natif, mais il exécute des applications prévues pour le x86. Les applications sont traduites par le système d'exploitation, avant d'exécuter le code traduit. : Nous parlerons dans la suite de code machine source et de code machine cible, pour parler respectivement du code à traduire et du code obtenu après traduction. Idem avec d'autres termes comme architecture cible/source, ou jeu d'instruction cible/source. La traduction binaire est surtout utilisée pour des questions d'émulation ou de compatibilité. Par exemple, lorsque les Macintosh sont passés de processeurs Power PC vers des processeurs x86, le système d'exploitation Mac OS utilisait la traduction binaire pour convertir les anciennes applications Power PC vers du code x86. Les utilisateurs n'y ont vu que du feu. Le système de traduction binaire était appelé Rosetta 1. Par la suite, lors de la transition de processeurs x86 vers des processeurs Apple, Rosetta 2 a vu le jour. La traduction binaire a surtout été utilisée pour traduire du code x86 vers un autre jeu d'instruction. Il faut dire que le x86 est le jeu d'instruction dominant. De nombreuses entreprises ont eu pour ambition de briser l'hégémonie du x86 sur PC, en remplaçant le x86 par un jeu d'instruction plus performant, tout en gardant une compatibilité maximale. La traduction binaire était la seule solution pratique. Elle était le plus souvent intégralement réalisée en logiciel, comme c'était le cas sur les architectures Itanium avec le ''IA-32 Execution Layer''. ===Les généralités sur la traduction binaire assistée en matériel=== L'architecture source est presque tout le temps une architecture CISC, assez ancienne, qu'on souhaite émuler. Le choix de l'architecture cible se porte souvent sur une architecture VLIW, fort différente de l'architecture source, et ce pour obtenir une économie de matériel conséquente. Pour s'exécuter rapidement, le code traduit doit exploiter le parallélisme d'instruction, à savoir exécuter plusieurs instructions en même temps dans des unités de calcul séparées. Il est possible d'utiliser un processeur superscalaire avec exécution dans le désordre pour cela, mais au prix d'un cout important en transistors. Alors qu'en utilisant un CPU VLIW, c'est le traducteur binaire qui fait tout le travail d'extraction du parallélisme d'instruction. Et après tout, quitte à avoir un traducteur binaire, autant lui refiler le boulot d'optimisation. Compiler du code à la volée est certes assez lourd, mais qu'il existe des algorithmes efficaces pour regrouper des instructions indépendantes dans une seule instruction VLIW. Au passage, le processeur VLIW a plus de registres que le processeur source. Cela permet de faire du renommage de registres directement en logiciel. Le traducteur binaire n'hésite pas à changer les noms de registres entre instructions source et instruction VLIW cible, afin de supprimer des dépendances de données. En conséquence, cela ouvre des opportunités de parallélisme, qui sont exploitées lors du regroupement des opérations en instructions VLIW. Vous pourriez penser que le choix d'un CPU VLIW pour émuler du CISC est tout sauf optimal. Vous devez penser que la traduction binaire est d'autant plus simple que l'architecture source et cibles sont semblables. Dans les faits, ce n'est pas tellement traduire les instructions qui pose problème, mais plus la gestion du registre d'état, des exceptions matérielles, de la mémoire virtuelle, la différence entre gros-boutisme et petit-boutisme, de même que des différences d'adressage pour les périphériques. Le premier problème est la gestion des conditions, notamment en présence d'un registre d'état. En pratique, la traduction binaire s'utilise pour traduire du code CISC vers du code RISC ou VLIW. Les architectures source ont donc un registre d'état, qui est mis à jour non seulement par des instructions de test, mais aussi des instructions arithmétiques. L'architecture cible n'a elle pas de registre d'état, mais des registres à prédicats. La traduction de l'un vers l'autre est alors quelque peu compliquée. Et elle est d'autant plus compliquée que le registre d'état est la source de dépendances d'instruction implicites, qui réduisent les performances. Par exemple, si une instruction arithmétique modifie le registre d'état, cela peut impacter l'exécution d'une instruction ultérieure, qui lit ce registre d'état. Pour éliminer ces fausses dépendances, le traducteur binaire doit renommer ce registre en logiciel et détecter les dépendances utiles. Et le compilateur doit gérer les cas où il y a beaucoup de distance entre les deux, voire les cas où l'instruction dépendante n'a pas encore été analysée par le compilateur. Un autre problème est lié à la gestion des exceptions matérielles, et précisément des exceptions précises. Pour rappel, une instruction VLIW regroupe plusieurs opérations, opérations qui correspondraient à une instruction machine sur un CPU pas VLIW. En conséquence, plusieurs instructions du langage machine "source" sont regroupées en une seule instruction VLIW. Et il faut tenir compte du cas où une instruction source lève une exception. Dans le code VLIW, cela signifie qu'une opération lève une exception, et il faut annuler partiellement l'instruction VLIW associée. Et par partiellement, on veut dire que seules les opérations suivantes dans l'ordre du programme source doivent être annulées, puis ré-exécutées. Et c'est un sacré casse-tête ! Les processeurs VLIW utilisent comme solution le mécanisme d'exceptions différées des processeurs EPIC, vu il y a quelques chapitres. Pour résumer, le code est exécuté par blocs d'instruction, délimités par des branchements ou tout autre limite/barrière pertinente dans le code. Un bloc de code est compilé en deux versions : une version rapide sans exceptions matérielles, une version lente qui gère les exceptions précises. La version rapide s'exécute sans exécuter les exceptions. Cependant, les exceptions matérielles sont enregistrées, pour être prises en compte à la toute fin du bloc de code. La version rapide est exécutée en premier et elle mémorise si une exception matérielle a eu lieu. Puis une instruction vérifie si une exception a eu lieu et décide quoi faire. Si aucune exception n'a eu lieu, elle passe à la suite du programme, les résultats du bloc de code sont définitivement acceptés. Mais si une exception a eu lieu, tout est annulé. Le processeur est remis dans l'état initial, puis le code est ré-exécuté instruction par instruction de manière à gérer l'exception correctement. ===La traduction par pré-décodage=== Une première solution serait de faire la traduction binaire dans le cache d'instruction. Lors d'un défaut de cache, le code chargé depuis la RAM est traduit en code machine cible. Et c'est ce code machine cible qui est mémorisé dans le cache d'instruction et exécuté par le processeur. La technique marche sur le papier et n'est qu'une amélioration des techniques de pré-décodage vues il y a quelques chapitres. Cependant, le code obtenu est une traduction assez basique, qui n'incorpore pas d'optimisations dignes de ce nom. De plus, elle gère mal le cas où la taille du code source et cible sont potentiellement très différentes. Concrètement, elle est surtout utile pour traduire du code d'un processeur RISC vers un autre processeur RISC, dont les tailles d'instructions sont similaires. Elle ne permet pas d'émuler plusieurs jeux d'instructions différents, le cout en matériel (un circuit de pré-décodage par jeu d'instruction) serait trop important. ===Le projet DAISY d'IBM=== De très rares processeurs étaient conçus pour accélérer cette traduction binaire, afin de garder de bonnes performances. Les premiers à avoir étudié l'idée étaient IBM, avec leur projet DAISY (''Dynamically Architected Instruction Set from Yorktown''). Le projet de base était de convertir à la volée du code compilé pour des CPU Power PC, vers du code VLIW. Les chercheurs d'IBM avaient développé un algorithme de traduction dynamique efficace, ainsi qu'un processeur VLIW disposant d'optimisations spécifiques à la traduction binaire dynamique, à la volée. De nombreuses idées de ce projet ont été reprises ou re-découvertes par la société Transmetta, avec ses processeurs Crusoe et Efficieon, puis par NVIDIA avec son projet Denver. Les ingénieurs de ce projet ont étudié la possibilité d'émuler plusieurs jeux d'instructions différents sur un même processeur, notamment le s390 d'IBM et le x86. En théorie, cela demande juste d'avoir plusieurs programmes de traduction binaire : un pour le x86, un autre pour le s390, éventuellement un autre pour le Power PC. Mais cela ne s'est pas concrétisé. Mais les ingénieurs ont étudié quelle pourrait être l'architecture VLIW idéale pour ça. Par exemple, il fallait des additions 3-opérandes pour simplifier les calculs d'adresse. Une difficulté était la gestion du registre d'état, dont les bits ne sont pas mis à jour de la même manière sur le x86 et le s390 ou le Power PC. La différence entre gros-boutisme et petit-boutisme était aussi un problème, de même que des différences d'adressage pour les périphériques. Pour que la traduction binaire soit efficace, le processeur VLIW intègre diverses optimisations, comme les exceptions différées et les branchements multi-voies. La principale est la suivante : seul le code des boucles ou fonctions exécutées fréquemment est traduit en code VLIW, le reste du code est interprété. En effet, traduire du code binaire prend plus de temps qu'une simple interprétation. Pour du code qui ne sera exécutée qu'une seule fois, il est plus rapide d'utiliser l'interprétation. Par contre, pour du code exécuté beaucoup de fois, le cout de la traduction binaire est amorti, dilué sur N exécutions, compensé par le gain en temps d'exécution de ce code traduit. Au final, cela permet de ne traduire que le code qui le mérite. La détection du code fréquemment exécuté est réalisée dans le cœur VLIW. L'unité de branchement mémorise les derniers branchements rencontrés et le nombre de fois qu'ils ont été exécutés. Si ils ont été exécutés un certain nombre de fois, l'unité de branchement lève une exception matérielle, qui invoque le traducteur binaire. La routine de cette exception est le traducteur binaire proprement dit. En clair, la traduction binaire est démarrée quand le processeur détecte qu'une fonction a été exécutée plus de N fois, via une exception matérielle dédiée. La mesure du nombre d'exécution d'un branchement se fait dans l'unité de calcul dédiée aux branchements, avec l'aide d'une sorte de ''branch adress buffer'' modifié. Pour rappel, le ''branch adress buffer'' mémorise le ''Program counter'' de chaque branchement récemment rencontré. Ici, chaque entrée du ''branch adress buffer'' est associée à un compteur incrémenté à chaque exécution du branchement. ===Les processeurs Crusoe et Efficieon de Transmetta=== Les processeurs Crusoe Et Efficeron sont deux processeurs VLIW produits par la société Transmetta, une société californienne rachetée par NVIDIA. Ils étaient conçus pour exécuter spécifiquement des programmes x86, système d'exploitation inclus. L'idée derrière ce projet était d'exécuter du code x86 sans que les applications, ni même le système d'exploitation et le BIOS soient au courant ! Les processeurs Transmetta étaient en réalité conçus pour exécuter un programme unique, le ''Code Morphing Software'' (CMS), qui traduisait le code x86 en code VLIW. Transmetta ne voulait pas que son processeur VLIW soit exploité directement, sans le CMS. Il n'avait pas rendu public de compilateur pour traduire du C vers du code VLIW, il n'avait pas donné la documentation du jeu d'instruction VLIW. Des efforts de rétro-ingénieurie, documentés sur le site realworldtech, ont cependant permis de comprendre comment étaient encodées les instructions du processeur Crusoe. Comme pour le projet Daisy, le ''Code Morphing Software'' utilisait à la fois interprétation et traduction binaire, selon les besoins. Les instructions sont interprétées lors de leurs premières exécutions, mais le CMS bascule sur de la traduction en code VLIW après un certain nombre d'exécution. Ainsi, les boucles souvent exécutées sont traduites en code VLIW, alors que le reste du code est interprété. Le choix du code à traduire est le fait du CMS, il utilise des heuristiques complexes pour, qui ne sont pas connues dans le détail. Le ''Code Morphing Software'' est mémorisé dans une EEPROM, ce qui en fait un ''firmware'' situé en dessous du BIOS. Le CMS démarre ensuite le BIOS, qui lui-même démarre le système d'exploitation, qui lui-même démarre les pilotes de périphériques et les programmes. Le CMS se réserve les 16 premiers mébioctets de l'espace d'adressage. L'EEPROM du CMS est mappée dedans, mais n'en utilise que la moitié. Le reste est utilisé comme cache, pour mémoriser le code VLIW traduit par le CMS. Les deux processeurs intégraient aussi deux mémoires SRAM, utilisées par le CMS, appelées la ''local program memory'' (LPM) et la ''local data memory'' (LDM). La première contient du code qui gére les interruptions, la mémoire virtuelle, les exceptions matérielles, les problèmes d'alignement mémoire, et quelques fonctions très fréquemment utilisées par le CMS. La seconde est de la mémoire RAM utilisée par ces fonctions de la ''local program memory''. Les mémoires caches L1 et L2 sont séparées de ces deux mémoires. Le processeur Crusoe et Efficieon étaient des processeurs VLIW très simples. Ils n'avaient même pas de MMU, ni de fonctionnalités importantes sur les CPU x86. Pour Crusoe, les instructions VLIW étaient encodées sur 64 ou 128 bits, et regroupaient entre 2 à 4 opérations. Les instructions LOAD/STORE ne supportaient pas d'adressage indicé, juste de l'adressage indirect à registre. Crusoe avait 5 unités de calcul : deux ALU entières, une unité LOAD/STORE, une unité de branchement, une FPU. Efficieon doublait le nombre d'ALU entière et d'unité LOAD/STORE, les instructions VLIW passaient à 256 bits. Le processeur Crusoe contient 160 registres, dont 64 registres généraux, 32 registres flottants. Les registres généraux font 32 bits, ce qui est cohérent avec le fait que les CPU x86 de l'époque étaient des processeurs 32 bits. Quant aux registres flottants, ils faisaient 80 bits, ce qui colle avec la taille des registres flottants de la FPU x87 utilisée à l'époque. Sur les 64 registres généraux, seuls 48 étaient réellement utilisable pour mémoriser des opérandes. Une partie des registres généraux étaient utilisés pour la gestion de la pile, d'autres pour mémoriser l'état du CPU x86 émulé, un registre était un registre zéro non accesible en écriture. La gestion des exceptions est optimisée avec un système d'exceptions différées, le même que celui décrit plus haut. Le processeur utilise un système similaire, pour effectuer des lectures anticipées, qui a été expliqué dans le chapitre sur les processeurs VLIW/EPIC. Sauf qu'il s'agit là d'une optimisation pour gagner en performances en général, pas une aide utile pour gérer des exceptions précises ou toute autre subtilité de la traduction binaire. Pour gérer les exceptions différées et les lectures anticipées, le processeur mémorise l'état du processeur avant de démarrer un bloc de code. Pour cela, le processeur contient des ''shadow registers'', 48 registres entiers et 16 registres flottants. Vous remarquerez qu'il y a autant de registres entiers que de ''shadow registers'' entiers. Les registres entiers/flottants sont mémorisés dans les ''shadow registers'' avant d'exécuter un bloc de code, afin que le processeur puisse revenir à l'état de base. Une autre fonctionnalité liée est le ''gated store buffer''. Le principe est d'éviter toute écriture en mémoire RAM, tant que le code VLIW n'a pas émis d'instruction ''commit''. L'idée est que les écritures sont conservées dans la ''store queue'' de l'unité LOAD/STORE. Pour rappel, la ''store queue'' met en attente les écritures soit tant que les écritures ne sont pas terminées, soit tant que la RAM est occupée. L'idée est que les écritures sont accumulées dans la ''store queue'' tant que l'instruction ''commit'' n'a pas eu lieu. Le ''store queue'' est "déversé dans la RAM" seulement quand l'instruction ''commit'' s'exécute. ===Les CPU NVIDIA Denver=== Nvidia a tenté quelque chose de similaire avec son projet Denver. NVIDIA a racheté l'entreprise Transmetta, ses brevets, et a tenté de refaire la même chose. Le projet initial était de traduire du code x86 vers un jeu d'instruction VLIW propriétaire, comme l'a fait Transmetta. Mais NVIDIA n'a pas réussi à acquérir la licence du jeu d'instruction x86 et s'est rabattu sur le jeu d'instruction ARM. Le processeur né de ce projet est le Tegra K1-64 CPU. Il s'agit d'un processeur composé d'un cœur VLIW, couplé à deux décodeurs d'instructions ARM. Les deux décodeurs ARM traduisent une paire d'instructions ARM consécutives en instructions VLIW, pour les exécuter sur le cœur VLIW. Le CPU peut fonctionner selon deux modes : ARM et VLIW. En somme, le processeur supporte deux jeux d'instruction : le jeu d'instruction ARM, et un jeu d'instruction VLIW. En mode ARM, les instructions ARM sont chargées depuis le cache, traduites en VLIW par les décodeurs ARM, puis exécutées par le cœur VLIW. En mode optimisé, le CPU exécute des instructions VLIW chargées depuis le cache d'instruction, les décodeurs ne sont pas utilisés. : Le CPU supporte en réalité deux jeux d'instruction ARM : ARM8 et 7. Mais ils correspondent au même mode pour le CPU. Un point important est que le processeur peut changer très rapidement de mode, en à peine quelques cycles d'horloges. On n'est clairement pas dans le cas des CPU x86, qui mettent des plombes pour passer du mode 32 à 64 bits et inversement. La commutation est tellement rapide qu'on peut considérer que le processeur supporte deux jeux d'instruction simultanément. Et le support simultané de deux jeux d'instruction facilite l'implémentation de la traduction binaire. Encore une fois, inutile de traduire du code qui ne sera exécutée qu'une seule fois, mieux vaut l'exécuter directement dans le mode ARM. Surtout que le processeur supporte un mode ARM qui permet d'exécuter du code ARM sans perte de performance. Par contre, le code fréquemment utilisé, notamment dans des boucles critiques, est traduit en VLIW. Au final, les performances sont optimisées, en limitant le travail du logiciel traducteur binaire. La détection du code fréquemment exécuté se fait différemment que pour les processeurs Transmetta et Daisy. L'unité de branchement ne détecte pas directement le code beaucoup exécuté, même si elle a un rôle à jouer. A la place, la détection est réalisée en logiciel, par un ''thread'' dédié, qui s'exécute sur un cœur séparé. Il détecte le code fréquemment utilisé en regardent les branchements exécutés récemment. Pour cela, le processeur mémorise l'historique des branchements pris récemment et copie cette historique dans une mémoire tampon dédiée, partagée entre tous les cœurs. Vu qu'elle est partagée, le ''thread'' de détection a accès à l'historique sans pertes de performances, seule la copie de l'historique dans ce tampon a un cout en performance, pas son partage. Le code VLIW obtenu après traduction binaire est mémorisé dans la mémoire RAM, dans une portion spécialement réservée pour. Elle fait 128 mébioctets, et est appelée le '''cache d'optimisation''' par NVIDIA. Le cache d'optimisation est protégé en écriture et seul le traducteur binaire peut écrire dedans. Au passage, le traducteur binaire est du code VLIW, ce qui fait qu'il est placé dans le cache d'optimisation. Le processeur détecte automatiquement quand une fonction pour laquelle le code compilé équivalent est disponible. Pour cela, l'unité de chargement contient une table de correspondance entre l'adresse de la fonction ARM, et l'adresse de son équivalent compilé. Quand une fonction est appelée, l'unité de chargement regarde l'adresse de destination du branchement. Si l'adresse est dans cette table, elle récupère l'adresse de la fonction compilée et branche vers celle-ci. Le processeur dispose d'un cache d'instruction de 128 kibioctets, ce qui est très important, mais nécessaire vu la taille des instructions VLIW. Le coeur VLIW dispose de ses propres décodeurs, d'un ''scoreboard'', de 7 unités de calcul et de registres. Pour les unités de calcul, il a deux FPU, deux ALU, une unité de branchement, et deux unités LOAD-STORE faisant aussi ALU. Un point important est que le CPU dispose de 64 registres entiers et de 64 registres flottants. C'est deux fois plus que supporte le jeu d'instruction ARM. En mode ARM, seule la moitié des registres est utilisée. Mais en mode optimisé, le traducteur binaire utilise bien les 64 registres, grâce à une sorte de renommage de registres logicielle. Pour simplifier la traduction binaire, le processeur supporte les techniques vues précédemment. Le processeur intègre un ''gated store buffer'' similaire à celui des processeurs Transmetta. Les techniques d'exceptions différées sont aussi supportées, comme sur les processeurs Transmetta. Il a aussi une unité de préchargement décrite par NVIDIA comme agressive, avec support du préchargement de type anticipé (''runahead''). ===L'émulation de la mémoire virtuelle=== Pour finir, il faut parler de la mémoire virtuelle avec la traduction binaire. Sur le processeur source, le processeur gère à la fois des adresses physiques et virtuelles et sa MMU fait la conversion entre les deux. Sur le processeur VLIW, la MMU est simulée par le programme de traduction binaire, partiellement ou totalement. L'implémentation exacte varie suivant que l'on parle du projet Daisy, des processeurs Transmetta ou de Denver. Sur les processeurs Transmetta, il n'y a pas de MMU ni de mémoire virtuelle. A la place, le programme de traduction binaire émule la mémoire virtuelle du processeur source. Le processeur VLIW ne gère que des adresses physiques, rien d'autre. L'espace d'adressage physique du processeur VLIW a la même taille que celui du processeur émulé, ici des processeurs x86. Il est vraisemblable que les adresses physiques utilisées par le processeur x86 sont les mêmes que celles du processeur VLIW. Sur le projet Daisy, le processeur VLIW gère la mémoire virtuelle via pagination, comme les processeurs émulés. Ce qui fait qu'il gère un espace d'adressage virtuel et un espace d'adressage physique. Pour éviter toute confusion, nous parlerons d'adresse physique/virtuelle VLIW pour les adresses physiques/virtuelles du processeur VLIW, d'adresse virtuelle/physique source pour celle du jeu d'instruction traduit, à savoir du Power PC ou du s390, éventuellement de l'x86. Il faut alors faire le lien entre adresses physiques source et adresses virtuelles VLIW. Pour cela, rien de plus simple : il y a correspondance parfaite. L'adresse physique source numéro N correspond à l'adresse virtuelle VLIW numéro N. Cependant, cela signifie que tout l'espace d'adressage virtuel VLIW serait utilisé par le code à traduire. En réalité, il faut ajouter de la place pour le code traduit et le programme de traduction binaire. La conséquence est que l'espace d'adressage virtuel VLIW est plus large que l'espace d'adressage source. L'espace d'adressage virtuel VLIW est découpé en trois sections : une section pour le code Power PC à traduire, une autre pour le code traduit en VLIW, et une dernière réservée au traducteur binaire. Le programme de traduction est placé dans une ROM mappée en mémoire dans la seconde section. Le reste de la seconde section est réservé à une mémoire RAM utilisée par le programme de traduction. [[File:Mémoire virtuelle sur le projet Daisy.png|centre|vignette|upright=1.5|Mémoire virtuelle sur le projet Daisy]] Il faut noter que chaque page physique source, une fois traduite, correspond à N pages virtuelles VLIW. La raison est que le code traduit prend plus de place que le code machine originel. En conséquence, on doit utiliser une page finale plus grande. Pour le reste, des pages contiguës en mémoire physique source sont elles aussi contiguës en mémoire virtuelle VLIW. Le calcul d'adresse est donc simple : l'adresse physique source est multipliée par N, puis on ajoute l'adresse de base à laquelle commence la section pour le code traduit. ==La traduction binaire sur le Burroughs B1700== Nous venons de voir comment accélérer en matériel la traduction binaire, avec des techniques bien spécifiques. Le Burroughs B1700 a procédé autrement et il est intéressant d'étudier en détail son architecture. Son architecture est décrite dans le livre "Interpreting Machines : Architecture and Programming of the Bl700/Bl800 Series", écrit par Elliott I. Organick et James A. Hinds. Une partie de ce qui vfa suivre est un très court résumé de ce livre. ===Le Burroughs B1700 : le mal-nommé ''split-level control store''=== Le Burroughs B1700 avait plusieurs modèles : le B1726, le B1710 et le B1800. Ils se distinguaient par la manière dont le code machine source était traduit par le traducteur binaire. Dans ce qui suit, nous parlerons de traducteur binaire pour parler soit d'un interpréteur pour un langage de programmation de haut niveau, soit pour un logiciel de traduction binaire. Le B1710 plaquait le traducteur binaire en mémoire RAM. Le B1800 faisait pareil, mais avait ajouté un cache dédié au code du traducteur binaire, afin d'améliorer les performances. Le B1726 avait un traducteur binaire scindé en deux : une partie était en mémoire RAM, l'autre était dans un ''local store'' dédié. Le processeur peut exécuter le traducteur binaire soit depuis le ''local store'', soit depuis la mémoire RAM avec une pénalité en termes de performances. La documentation appelle cela un '''''split-level control store''''', mais le terme est trompeur : la documentation qualifie de microcode le langage machine du Burroughs B1700, ce qui est à l'origine de beaucoup de confusions. Une autre source de confusion est que ce langage machine a quelques propriétés qu'on pourrait attendre d'un microcode, mais certaines sont manquantes. Par exemple, il peut adresser les registres d’interfaçage mémoire, mais c'est parce que ce sont des registres architecturaux sur ce processeur. Ses instructions n'encodent pas de signaux de commande, par contre, elles font 16 bits et adressent les opérandes de manière implicite. Le ''local store'' mémorisait au maximum 2048 instructions machine, ce qui fait que les 2048 premières "adresses" correspondaient au ''local store''. Les suivantes étaient des adresses de mémoire RAM qui contenaient le reste du traducteur binaire. En pratique, l'adresse qui séparait les deux était configurable ! On pouvait limiter la taille du ''local store'' à un multiple de 32 instructions ! Les adresses restantes étaient alors réattribuées à la RAM. Par exemple, on pouvait attribuer 64 adresses au ''local store'', les 2048 - 64 adresses restantes étaient alors attribuées à la mémoire RAM. Un registre limite, nommé TDPM, indiquait quelle était l'adresse de démarcation entre les deux. Il contenait 4 bits, dont la valeur était multipliée par 32, pour obtenir l'adresse limite. Le programmeur devait découper le traducteur binaire en segments, certains étant placé dans le ''local store'', les autres étant en mémoire RAM. Mieux que ça, le processeur autorisait d'utiliser la technique de l'''overlaying'' pour le microcode ! Il y avait même une fonction OVERLAY dédiée pour ! OVERLAY avait besoin de trois opérandes : la taille du segment à copier, son adresse source en mémoire RAM, son adresse de destination dans la micro-SRAM. L'interpréteur réservait un "segment" en mémoire RAM, pour les données, dont le processeur connaissait la position. En-dehors de ce segment, il n'y a que des instructions machines à traduire, et le traducteur binaire. Le CPU incorporait pour cela deux registres, un pour l'adresse de base du segment de données, un autre pour son adresse de fin. Il incorporait une protection mémoire limitée sur ce segment, à savoir que seul l'interpréteur pouvait lire ou écrire dans ce segment. ===Une architecture bit-adressable=== Le CPU Burroughs B1700 était '''bit-adressable'''. En clair, il pouvait adresser la mémoire bit par bit, si nécessaire. Un avantage est que cela facilitait la traduction binaire des instructions machine, mais nous détaillerons cela plus bas. Un autre avantage est que cela permettait d'émuler des architectures dont la taille des registres/''byte''/mots était très différente. Et cet avantage mérite quelques explications immédiates. Le processeur était un CPU 24 bits, mais il pouvait émuler des processeurs 16 bits, 8 bits, 4 bits, 1 bit, 9 bits, ou toute autre valeur. Pour cela, le processeur lisait des mots de 24 bits et utilisant un ''barrel shifter'' pour sélectionner les bits adéquats. Par exemple, pour un accès de 16 bits, la donnée lue en mémoire était masquée de manière à ne garder que les 16 bits de poids faible. Le ''barrel shifter''/circuit de masquage était placé directement avant le bus mémoire, sur le trajet des données/instructions. Le ''barrel shifter'' était commandé par un registre ''Field Unit'', qui précisait quelle était la taille des données à charger. Pour communiquer avec la mémoire, il avait trois registres séparés : READ, WRITE, MAR. READ contient la donnée lue lors d'une lecture, WRITE est pour une donnée à écrire lors d’une écriture, MAR contient l'adresse à lire ou en cours de lecture. Il s'agit de registres d’interfaçage mémoire. Le fait que ces registres d’interfaçage mémoire et IO soit adressable est peu commun, et c'est plus quelque chose qu'on attend d'un microcode que d'instructions machines. Mais il faut bien comprendre que les registres d’interfaçage mémoire ont été promus au rang de registres architecturaux, sur ce processeur. De même, pour communiquer avec les entrées-sorties, il disposait de deux registres CMND et DATA : CMND pour envoyer une commande à une entrée-sortie, DATA pour lire ou écrire une donnée. Il y avait aussi un registre U, qui mémorisait une donnée lue depuis le lecteur de cassette. ===L'ALU de taille variable=== Les Burroughs B1700 intégrait une pile d'adresse de retour, capable de mémoriser 16 adresses de 24 bits. Il disposait de 4 registres généraux, nommés X, Y, T et L, tous de 24 bits. Un point important est que le registre T avait des possibilités d’extraction spécifiques. Il était possible d'extraire N bits, placés n'importe où dans ce registre, et de les copier dans un autre registre. Le CPU supportait une '''instruction EXTRACT''' pour ça. Nous verrons pourquoi cette possibilité est intéressante et ne sert que pour le registre T. Ils avaient une '''ALU de taille variable'''. En clair, l'ALU pouvait faire des calculs sur un nombre de bits compris entre 0 et 24. Pour cela, les calculs étaient fait avec une ALU de 24 bits, puis les bits de poids fort inutiles étaient masqués. L'ALU de 24 bits pouvait masquer les bits de poids fort du résultat, mais avec des limitations. Elle gérait des opérandes et résultats de 0, 4, 8, 12, 16, 20 et 24 bits. Pour avoir un réglage plus fin, une ALU 4 bits séparée permettait de corriger le résultat. Les registres X et Y étaient reliés à une ALU de 24 bits capable d'effectuer des opérations arithmétiques basiques. Fait étonnant, l'ALU faisait tous les calculs en même temps et fournissait ses résultats dans 7 registres : un registre SUM pour l'addition, un registre DIFFERENCE pour la soustraction, et un registre par opération bit à bit. Les registres pour les opérations bit à bit sont : CMPX et CMPY pour l'opération NOT sur chaque opérande, XANY pour le ET bit à bit, XEOY pour le XOR, XORY pour le OU logique. Les additions et soustractions pouvaient se faire sur des opérandes codés en binaire ou en BCD, suivant le code opération utilisé. A cela, il fallait ajouter deux registres de "résultat" MSKX et MSKY. Ils fournissaient un masque dépendant de la taille du résultat en bits. C'est ce registre qui permettait de gérer des données de 0, 4, 8, 12, 16, 20, et 24 bits, alors que l'ALU faisait 24 bits. Le masque dans ce registre pouvait servir d'opérande dans une opération de masquage ultérieure, pour corriger le résultat. L'ALU prenait en opérande les registres X et Y, mais aussi un registre CYF pour la retenue entrante. En sortie, il fournissait aussi deux retenue sortantes : une pour l'addition et une autre pour la soustraction. Elles étaient mémorisées dans les registres CYL et CYD, respectivement. L'ALU fournissait aussi 12 conditions, qui alimentait un registre d'état. Le registre d'état était segmenté en trois registres appelés XYST, XYCN et BICN. Les registres X et Y sont des registres pour les opérandes, mais il y a aussi deux autres registres de 24 bits. Il s'agit des registres T et L, qui sont des registres généraux. Les registres T et L étaient découpés en 6 sous-registres de 4 bits, nommés LA LB LC LD LE et LF pour le registre L, et TA TB TC TD TE et TF pour le registre T. Ils étaient adressables comme n'importe quel registre. Le processeur contenait de nombreux autres registres de 4 bits, dont les sous-registres de T et L. En tout, il y a 27 registres de 4 bits adressables ! Et le processeur disposait d'une ALU 4 bits pour manipuler ces registres de 4 bits ! Elle est capable de faire des MOV entre registres de 4 bits, des opérations bit à bit, et peut aussi tester si un bit vaut 0 ou 1. La dernière possibilité est très utile pour implémenter les branchements. De plus, cela permettait de faire des opérations de masquage, notamment pour appliquer un masquage supplémentaire à celui de l'ALU. Par exemple, pour gérer des opérations sur 14 bits, on effectue une opération sur 16 bits avec l'ALU, et on masque les 2 bits manquants avec l'ALU 4 bits. La capacité d'extraction du registre T pouvait aussi être utilisé pour ça. ===Le code machine du Burroughs B1700=== Le processeur utilise des instructions de 16 bits de long, qui sont copiées dans registre d'instruction M de 16 bits. Le ''program counter'' est appelé le registre A. Les instructions du processeur font 16 bits de long, et elles sont alignées en RAM sur 16 bits, ce qui fait que les 4 bits de poids fort de ce registre valent 0, seuls les 14 bits de poids fort sont utiles. Les adresses mentionnées plus haut, qui adressent les micro-opérations en mémoire RAM, sont des adresses de mots de 16 bits, pas les adresses de bit individuels. Fait intéressant, il était possible de faire un OU logique entre l'instruction lue et un registre du processeur, le résultat étant mémorisé dans le registre M. Cela permettait de faire du code automodifiant, pour émuler des modes d'adressage complexe. Cela permettait par exemple de remplacer une adresse dans une micro-opération. L'adresse est alors surimposée avec un OU dans le champ d'adresse, alors que le reste de l'instruction n'est pas modifié. Afin de gérer les interruptions, ces dernières étaient partiellement détectées en logiciel. Une interruption doit être prise en compte à la toute fin de l'exécution d'une instruction source, quand celle-ci a finit son travail. Mais vu qu'ici, les instructions sont émulées avec des séries d'instructions cibles, il y a un problème : il est possible d'interrompre une instruction émulée en plein milieu. Pour éviter cela, le processeur ne déclenche pas les interruptions matérielles immédiatement, elles sont mises en attente. Une instruction dédiée vérifie si une interruption est en attente, et l'exécute alors cas échéant. L'instruction en question est utilisée en dernier, dans la suite d'instruction qui émule l'instruction source. ===La traduction binaire sur le Burroughs B1700=== Les instructions source peuvent faire 16 bit avec tel jeu d'instruction, 8 bits avec un autre, 32 bits sur un autre, etc. Elles peuvent aussi être de longueur fixe ou variable ! Le B1700 utilisait une architecture bit-adressable pour gérer ces contraintes. En effet, avec un CPU bit-adressable, les contraintes d'alignement des instructions disparaissent ! Dans ce qui suit, on suppose que l'instruction est composée de '''champs''' : l'opcode, les opérandes, les adresses absolues, les constantes immédiates, les numéros de registre, etc. L'idée est que l'instruction source est chargée dans le registre T, puis découpée en champs, qui sont copiés dans les registres du processeur ou envoyés au microcode. Je rappelle que le registre T est relié à un ''barrel shifter'', différent au précédent, qui permettait de masquer, décaler et extraire une donnée de ce registre. Et il y a la possibilité d'extraire une suite de bit de ce registre T, pour la copier dans un autre registre. Par exemple, prenons une instruction composée d'un opcode, d'une constante immédiate et d'une adresse immédiate. Premièrement, elle est chargée dans le registre T. Deuxièmement, la constante immédiate est extraite du registre T et copiée dans le registre X. Troisièmement, l'adresse est extraite et est copiée dans le registre d’interfaçage mémoire, une lecture est démarrée, et l'opérande lue est copiée dans le registre Y. Enfin, l'instruction équivalente à l'opcode est exécutée par le microcode. Le tout prend plusieurs instructions : une première pour lire l'opcode, une seconde pour lire la première opérande, une troisième pour lire la seconde opérande, una quatrième pour tout exécuter. Notons que suivant la taille des champs, on peut en charger plusieurs à la fois. Par exemple, imaginons qu'on ait chargé 24 bits, contenant un opcode de 8 bits et une constante immédiate de 16 bits : le registre T est découpé en un opcode et une constante, avec deux instructions EXTRACT. L'architecture est bit-adressable car elle gère des champs qui ne font pas forcément 8 ou 16 bits. Par exemple, certains jeux d'instructions ont des opcodes codés sur 6 ou 7 bits, voire 9-10 bits. Utiliser une architecture bit-adressable règle ce problème d'alignement. On peut lire un champ depuis la mémoire RAM en précisant l'adresse de son premier bit, pas besoin de gérer des accès non-alignés. Par exemple, prenons une instruction source avec un opcode de 7 bits, suivi par une adresse de 12 bits et une constante immédiate de 15 bits. On peut charger 7 + 12 bits en un premier accès, pour extraite l'opcode et l'adresse, suivi par un second accès de 15 bits pour extraire la constante immédiate. Pour charger un champ, il faut connaitre plusieurs informations : l'adresse de son premier bit, sa taille. Pour cela, le B1700 contient deux registres, nommés ''Field Adress'' (FA) et ''Field Length'' (FL), qui indiquent l'adresse du premier bit et la taille de l'instruction à charger. Le premier fait 24 bits, ce qui permet de gérer des adresses de 24 bits. N'oubliez pas que le processeur est bit-adressable, ce qui permet d'adresser 16 Mébi-bits, soit 2 mébioctets. Le registre pour la taille fait lui 16 bits, ce qui permet de gérer des instructions de 65536 bits, soit 8192 octets ! Leur nom commence par ''field'', car le processeur peut gérer des instructions de taille variable et/ou plus longues que 24 bits. Dans ce cas, le chargement de l'instruction se fait champ par champ. Pour donner un exemple, prenons une instruction composée de 20 champs de 12 bits chacun. Les champs sont chargés un par un dans le registre T. Le registre FA est alors initialisé avec l'adresse de l'instruction, il est incrémenté de 12 à chaque cycle. Le registre FL est initialisé avec la taille de l'instruction, soit 20 * 12 = 240 bits, et il est décrémenté de 16 à chaque cycle. Précisons cependant que la taille d'un champ peut être changée d'un cycle à l'autre. Par exemple, le processeur peut charger un opcode de 8 bits, l'interpréter, détecter que l'instruction demande ensuite de charger une adresse de 16 bits. Dans ce cas, le registre FL est immédiatement altéré pour charger 16 bits, au lieu des 8 bits de l'opcode immédiatement précédent. ==Les jeux d'instructions dédiés à un langage de programmation== De rares processeurs sont conçus pour un langage de programmation en particulier. On appelle ces processeurs, conçus pour des besoins particuliers, des '''processeurs dédiés'''. Par exemple, les fameux ''Burrough E-mode'' B5000/B6000/B7000 étaient spécialement conçus pour exécuter de l'ALGOL-60. Leurs cousins B2000/B3000/B4000 étaient eux conçus pour le COBOL. Des langages fonctionnels ont aussi eu droit à leurs processeurs dédiés. Le prolog en est un bel exemple, avec les superordinateurs de 5ème génération qui lui étaient dédié. On peut aussi citer les machines LISP, dédiés au langage LISP, qui datent des années 1970. Elles étaient capables d’exécuter certaines fonctions de base du langage directement dans leurs circuits : elles possédaient notamment un ''garbage collector'' câblé dans ses circuits ainsi que des instructions machines supportant un typage déterminé à l’exécution. Les processeurs dédiés ont eu leur heure de gloire au début de l'informatique, à une époque où les langages de haut niveau venaient d'être inventés. À cette époque, les compilateurs n'étaient pas performants et ne savaient pas bien optimiser le code machine. Il était alors rationnel, pour l'époque, de rapprocher le code machine cible et le langage de programmation de haut niveau. De nombreuses architectures dédiés ont ainsi été inventées, avant que les concepteurs se rendent compte des défauts de cette approche. Les défauts en question ne sont pas nombreux, mais assez simples à comprendre. Premièrement, elles sont très rapides pour un langage de programmation en particulier, mais sont assez mauvaises pour les autres, d'où un problème de "compatibilité". Ajoutons à cela que les langages de programmation peuvent évoluer, devenir de moins en moins populaires/utilisés, ce qui rend la création d'architectures généralistes plus pertinente. Enfin, les architectures dédiées sont évidemment des processeurs CISC, pour implémenter les nombreuses fonctionnalités des langages évolués. Et les défauts des CISC sont assez rédhibitoires à l'heure actuelle. À l'heure actuelle, les algorithmes des compilateurs se sont améliorés et savent nettement mieux utiliser le matériel. Ils produisent du code machine efficace, ce qui rend les architecture dédiées bien moins intéressantes. Si on ajoute les défauts de ces architectures dédiées, par étonnant que les architectures dédiées aient presque disparues. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'accélération matérielle de la virtualisation | prevText=L'accélération matérielle de la virtualisation | next=Le matériel réseau | nextText=Le matériel réseau }} </noinclude> i46eqankgmkqv24hvufw5ol6elxhubd 764056 764055 2026-04-19T20:44:11Z Mewtow 31375 /* L'ALU de taille variable */ 764056 wikitext text/x-wiki De nos jours, la majorité des programmeurs programment dans des langages de haut niveau. Il est très rare pour eux d'avoir à utiliser de l'assembleur, et encore moins un langage machine. Les programmes écrits dans un langage de haut niveau sont traduits en langage machine par un logiciel appelé le compilateur. [[File:Compilation.PNG|centre|vignette|upright=2.5|Compilateur : principe.]] Il se trouve que quelques rares processeurs sont conçus pour faciliter le travail du compilateur. Et ce sont ces processeurs que nous allons voir dans ce qui suit. Ils représentent une catégorie de jeux d'instruction à part, qui n'a pas vraiment de nom. ==La compilation et l’interprétation== Un compilateur traduit donc du code source, écrit dans un langage de haut niveau, vers du langage machine. La traduction est rarement directe. En général, le compilateur traduit le code source en assembleur, qui est lui-même traduit en langage machine. L'assembleur est une représentation textuelle du langage machine alors que le code machine est du binaire exécutable par le processeur. La traduction se fait donc en deux étapes, la '''compilation''' proprement dite et l''''assemblage''', réalisées respectivement par un compilateur et un assembleur. Il faut parfois rajouter une troisième phase d'édition des liens, que nous passons volontairement sous silence. {| |[[File:Code C.png|vignette|200px|Le code source, ici en C.]] |[[File:Assem.png|vignette|250px|Le code assembleur.]] |[[File:Binary file - hello world (C programming).png|vignette|150px|Le code machine.]] |} ===L'interprétation et la compilation à la volée=== Une alternative à la compilation est l''''interprétation''', qui transforme le code source en code machine à la volée. Avec l'interprétation, le code source est passé à un logiciel appelé l''''interpréteur''', qui exécute le code source ligne de code par ligne de code. Une ligne de code est traduite en code machine, qui est exécutée, puis l'interpréteur passe à la ligne suivante. [[File:Kaantotulkkaus.png|centre|vignette|upright=3|Compilation versus interprétation.]] Un défaut de l'interprétation est son cout en performance, qui est assez important. Aussi, l'interprétation stricte a évolué vers un hybride entre interprétation et compilation. L'idée est qu'une partie peu importante du code source est interprétée, alors que le code important est compilé juste avant d'être exécuté. Pour le dire autrement, le code source est partiellement compilé à la volée, juste avant son exécution. Aussi, on parle de '''compilation à la volée'''. Le terme en anglais est ''Just In Time compilation'', abrévié en JIT. En soi, le JIT est associé à de l'interprétation, les JIT sont en réalité des hybrides interpréteurs-compilateurs. Un point important est que compiler du code à la volée est assez lourd, cela a un cout en performance. Par contre, le code compilé s'exécute plus vite que du code interprété. Si on exécute le code compilé une seule fois, le cout de la compilation l'emporte sur le gain à l'exécution. Mais le code compilé est réutilisé autant de fois que nécessaire. Le gain à l'exécution est donc multiplié par le nombre d'exécutions, alors que le cout de la compilation est répartit sur plusieurs exécutions, il est amorti. Les portions du code sont choisies de manière à avoir un gain en performance maximal. L'idée générale est que le code exécuté très souvent est compilé, alors que du code exécuté pas souvent est interprété. Typiquement, les boucles les plus souvent exécutées sont compilées, le code exécuté une seule fois est interprété. ===Les langages intermédiaires=== Les compilateurs modernes passent par un '''langage intermédiaire''' pour faire la transformation en code machine. Le compilateur traduit le langage de haut niveau en langage intermédiaire, puis traduit le langage intermédiaire en code machine cible. Le langage intermédiaire est parfois appelé le '''''bytecode''''', et ce terme recouvre aussi le code écrit avec du ''bytecode''. L'interprétation aussi peut passer par l'intermédiaire d'un un ''bytecode'', ce n'est pas limité aux compilateurs proprement dit. {| |[[File:Intermediate representation scheme.png|centre|vignette|upright=1.5|Représentation intermédiaire d'un compilateur.]] |[[File:Code interpreter scheme.png|centre|vignette|upright=1.5|Fonctionnement d'un interpréteur.]] |} Faire ainsi a de nombreux avantages, le principal étant d'avoir un compilateur capable de traduire un langage de haut niveau vers plusieurs jeux d'instructions différents. Cela permet d'avoir, par exemple, un compilateur qui traduit du C soit en code machine x86, soit en code machine pour un CPU ARM, soit pour un CPU POWERPC, etc. [[File:Compiler design.svg|centre|vignette|upright=3|Compiler design]] Pour l'interprétation, l'usage d'un ''bytecode'' a d'autres avantages. Pour l'exploiter, il faut distribuer non pas le code source, mais le ''bytecode''. En clair, l'interpréteur ne prend pas en entrée le code source, mais du ''bytecode'' déjà compilé. La traduction est alors beaucoup plus simple, car le code source a déjà été partiellement compilé, par la traduction en ''bytecode''. Interpréter du code source est en effet assez compliqué : il faut effectuer des étapes d'analyse lexicale, sémantique, et bien d'autres. Avec du ''bytecode'', ces étapes ont été réalisées lors de la compilation du ''bytecode'', l'interpréteur a alors peut de choses à faire. L'interprétation du ''bytecode'' se fait instruction par instruction, au niveau du ''bytecode''. Une instruction du ''bytecode'' est traduite en une instruction machine équivalente, qui est exécutée. Si l'instruction est un peu complexe, il exécute une fonction/procédure qui fait la même chose. Pour résumer, le ''bytecode'' est ensuite traduit à la volée et exécuté instruction par instruction par un logiciel appelé l'interpréteur. [[File:Control table.png|centre|vignette|upright=2|Control table]] Le langage intermédiaire peut être vu comme l'assembleur d'un processeur, n'existe pas forcément dans la réalité, mais dont le jeu d'instruction est décrit en détail. Le processeur en question est appelé une '''machine abstraite''', ou encore une ''machine virtuelle''. Nous utiliserons le terme de machine abstraite dans ce qui suit. La machine abstraite n'est pas la même suivant que l'on cible la compilation ou l'interprétation/JIT. Pour l'interprétation/JIT, la machine abstraite est souvent une machine à pile, car cela simplifie grandement la traduction en code machine final. L'interprétation demande que la traduction du ''bytecode'' en code machine soit la plus simple possible. Et cela demande de prendre en compte pas mal de détails. Par exemple, les différents jeux d'instruction existants n'ont pas le même nombre de registres, ce qui pose problème lors de la traduction du ''bytecode''. Lors de la transformation en code machine, un algorithme d'allocation de registres se débrouille pour traduire le code intermédiaire en code qui utilise un nombre limité de registres. Pour améliorer cette allocation de registres, il y a deux solutions niveau ''bytecode'' : soit utiliser un nombre illimité de registres, soit utiliser une machine à pile. Pour les machines à pile, il existe un algorithme simple et rapide pour traduire un code écrit pour une machine à pile en un code écrit pour un processeur avec des registres, qui se débrouille pas trop mal pour allouer efficacement les registres. C'est un avantage assez important pour les langages interprétés. ==Les processeurs qui exécutent du bytecode== Le ''bytecode'' est un code machine, ce qui signifie qu'il peut en théorie s'exécuter sur un processeur qui implémente le jeu d’instruction associé. Si le jeu d'instruction d'un ''bytecode'' est souvent une description censée être fictive, elle n'en reste pas moins un jeu d'instruction et des caractéristiques précises, qu'on peut l'implémenter en matériel ! Néanmoins, tous les ''bytecode'' ne sont pas égaux de ce point de vue. Certains sont faciles à implémenter en matériel, d'autres non. Les ''bytecodes'' implémentés en matériel sont ceux dont les machines abstraites sont des machines à pile. Un premier exemple est celui des processeurs Pascal MicroEngine, qui exécutaient directement le bytecode du langage Pascal, le fameux UCSD P-code. Un second exemple est la '''machine SECD''', qui sert de langage intermédiaire pour certains compilateurs de langages fonctionnels. Elle a été implémentée en matériel par plusieurs équipes, notamment par les chercheurs de l'université de Calgary en 1989. Dans le même genre, quelques processeurs simples étaient capables d’exécuter directement le ''bytecode'' du langage FORTH. Le FORTH, un des premiers langages à pile de haut niveau, possède de nombreuses implémentations hardware et est un des rares langages de haut niveau dont le ''bytecode'' a été utilisé comme langage machine sur certains processeurs. Par exemple, on peut citer le processeur FC16, capable d’exécuter nativement du ''bytecode'' FORTH. ===Les processeurs Java=== Le cas le plus impressionnant est celui de la machine virtuelle Java, qui est un design de processeur comme un autre. En temps normal, le ''bytecode'' Java est compilé ou interprété, mais certains processeurs exécutaient du ''bytecode'' Java directement, sans interprétation ni compilation. Ils sont appelés des '''processeurs Java'''. On peut citer les processeurs ARM disposant de l'extension Jazelle, du JEMCore et du aJ-100 de aJile Systems, le picoJava II de Sun, et quelques autres. Il y a aussi eu quelques projets de recherche ou processeurs open source, comme Komodo, jamuth, le ''Java Optimized Processor'' et quelques autres projets du même genre. L'implémentation de la machine virtuelle Java n'était cependant pas complète. Quelques instructions complexes n'étaient pas gérées par le processeur et devaient être émulées en logiciel. Mais la grosse majorité des instructions l'était. Les instructions non-supportées étaient émulées avec la méthode ''trap and emulate'', à savoir qu'un opcode inconnu déclenchait une exception matérielle ''opcode invalide'', dont la routine pouvait émuler les instructions qui doivent l'être. Le processeur aJ-100 de aJile est capable d'exécuter la quasi-totalité des instructions de la JVM Java, seules deux instructions faisant exception. Les '''accélérateurs Java''' sont une solution intermédiaire, qui permet de traduire à la volée du ''bytecode'' Java en instructions RISC, exécutées sur un autre processeur. Il s'agit formellement de traduction binaire, mais je préfère en parler ici. L'idée est de combiner un processeur RISC avec un coprocesseur Java. Le coprocesseur lit le ''bytecode'' Java et le traduit en instructions RISC, ces dernières étant exécutées sur le processeur RISC. Le JA108 de Nozomi était un coprocesseur de ce genre, au même titre que le coprocesseur JSTAR de JEDI. L'extension Jazelle de certains CPU ARM fonctionnait vraisemblablement sur un principe similaire, sauf que le coprocesseur était intégré dans le processeur ARM, entre le cache d'instruction et le décodeur d'instructions ARM. L'intérêt des processeurs Java n'est pas qu'une question de performance. Certes, ils permettent d’exécuter plus vite les programmes compilés en ''bytecode'', comme des programmes Java pour la JVM Java. Mais l'intérêt est aussi de faire des économies de mémoire RAM et ROM. Je rappelle que la machine virtuelle Java est une machine à pile, ce qui fait que sa densité de code est excellente. Les programmes en ''bytecode'' Java sont donc très petits, et prennent peu de place en mémoire ROM. De plus, utiliser un processeur Java permet de se passer d'interpréteur ou de JIT Java, ce qui économise encore plus de mémoire. ===Transformer un processeur RISC en machine à pile=== La plupart des processeurs Java utilisent en interne une architecture à registre généraux, qui émule une machine à registre en microcode. Quelques registres sont réservés pour l'état de la machine virtuelle, avec au minimum un registre réservé pour le pointeur de pile, un autre pour un pointeur vers un ''pool'' de constantes, et quelques autres. D'autres registres servent à mémoriser le haut de la pile, à savoir le sommet de la pile et quelques opérandes situées juste en-dessous. Par exemple, le processeur picoJava II gardait les 64 opérandes au sommet de la pile dans 64 registres généraux, séparés des registres pour les pointeurs et l'état de la JVM. Une optimisation possible, envisagée sur les processeurs picoJava, était une sorte de macro-fusion sous stéroïde. L'idée était de fusionner certaines séries d'instructions Java en une seule instruction machine. Typiquement, pour faire une opération arithmétique, une machine à pile empile deux opérandes et exécute une opération, ce qui prend trois instructions machine. La spécification picoJava 1 a proposé d'envoyer directement la seconde opérande en entrée de l'ALU, ce qui fusionnait l'empilement de la seconde opérande et l'opération arithmétique. Le processeur picoJava II va plus loin et fusionne les trois instructions en une seule instruction RISC, agissant sur des registres. Mais la majorité des processeurs Java ne faisaient pas cela, car cela complexifie grandement le décodeur. Les processeurs ''bigfoot'' de DCT utilisaient un système différent pour émuler une pile avec des registres. Leur banc de registre de 64 bits était coupé en deux : les 16 premiers registres étaient utilisés pour la pile, les 48 suivants étaient utilisés pour autre chose. L'idée est que le registre 0 mémorise un emplacement vide, le registre 1 contient le sommet de la pile, le registre 2 l'opérande sous le sommet de la pile, et ainsi de suite. Les 15 premières opérandes de la pile sont donc mémorisées dans les registres. Les instructions écrivent leur résultat dans le registre adéquat, puis tous les registres sont décalés pour que le résultat passe dans le registre R1. Par exemple, les instructions PUSH chargent une opérande dans le registre 0, puis tous les registres sont décalés de un rang, pour que l'opérande chargée soit dans le registre 1. Le registre 0 devient alors le registre numéro 1, le registre 1 devient le 2, et ainsi de suite. Par contre, les opérations arithmétiques font autrement. Une instruction d'addition, par exemple, lit les opérandes dans le registre 1 et 2, puis place son résultat dans le registre 2 (les deux opérandes sont dépilées). Puis, les registres sont décalés d'un rang pour que ce registre 2 devienne le 1. Les registres ne sont en réalité pas décalés, il n'y a pas de transferts entre registres. A la place, les 16 premiers registres utilisaient une forme limitée de renommage de registres, similaire au banc de registre tournant utilisé sur les processeurs EPIC comme l'Itanium. Les numéros de registres sont renommés, décalés, à chaque fois qu'on empile ou dépile une opérande. Pour faire le renommage de registres, processeur contient un ''register stack counter'' de 4 bits, qui est incrémenté à chaque fois qu'on empile une donnée, et est décrémenté quand on dépile une donnée. Quand on veut accéder à un opérande, le numéro de registre architectural associé est traduit en numéro de registre physique, en additionnant le ''register stack counter'' (modulo 16, pour rester sur 4 bits). ==L'accélération de l'interprétation/JIT : le ''Thumb-EE'' d'ARM== Quelques processeurs ont ajouté des instructions pour faciliter le travail des interpréteurs/JIT. Par exemple, en 2005, ARM a ajouté le mode ''Thumb Execution Environment'', qui faisait cela. Il reprenait le jeu d'instruction compact ''thumb'' et ajoutait quelques instructions et modifiait le comportement d'instructions existantes. Le jeu d'instruction ''Thumb-EE'' a été ajouté sur les CPU ARM en 2005, mais a été déprécié en 2011, par manque d'utilité. Mais la tentative mérite qu'on s'attarde dessus. Les CPU de gamme M, déstinés à l'embarqué, n'ont jamais supporté ''Thumb-EE'', vu que de tels processeurs ne sont pas conçus pour exécuter du code interprété/JIT. Le mode ''Thumb-EE'' est un mode d'exécution, séparé de l'ARM normal. Ainsi, une instruction ''Thumb-EE'' et une instruction ARM peuvent avoir le même encodage en binaire, mais se comporter différemment. Le processeur est à tout moment soit en mode ''Thumb-EE'', soit en mode ARM normal, ce qui précise comment ces instructions doivent se comporter. Des instructions ont été ajoutées par ''Thumb-EE'', pour qu'il fasse son travail. L'entrée et la sortie du mode ''Thumb-EE'' se fait avec deux instructions : ENTERX et LEAVEX. Une fois en mode ''Thumb-EE'', le décodage des instructions ''thumb'' se fait avec les règles du ''Thumb-EE'', et non celles du ''thumb'' normal. Une première différence entre ''thumb'' normal et ''Thumb-EE'' est l'ajout de ce qui s'appelle un ''null check'' pour les instructions mémoire. Le ''null check'' est utilisé pour les instructions en mode d'adressage "base + indice" ou "Base + décalage", ou tout autre mode d'adressage avec un registre de base utilisés dans des calculs d'adresse. L'idée est de vérifier si l'adresse de base vaut zéro ou non. Si c'est le cas, le processeur lève une exception matérielle, qui est traitée par l'interprétation ou le compilateur JIT. Une telle situation est en effet signe d'une erreur d'adressage mémoire, qui doit être traitée par l'interpréteur. Typiquement, c'est signe qu'un pointeur n'a pas bien été initialisé, ce qui peut arriver avec du code interprété ou JIT. Les interpréteurs/JIT traitent généralement la situation en ajoutant un test avant toute instruction mémoire, pour vérifier si le pointeur accédé vaut zéro ou non. Avec ''Thumb-EE'', pas besoin d'ajouter les tests en questions : ils sont réalisés automatiquement lors de chaque instruction mémoire. : Les registres pour la pile, comme le pointeur de pile et le pointeur de ''frame'', sont aussi concernés. L'instruction CHKA vérifie que les accès à un tableau ne débordent pas en dehors du tableau en question. Par exemple, pour un tableau de 1024 éléments, elle vérifie si l'indice est compris entre 0 et 1024. Pour cela, elle vérifie si l'indice de tableau est bien dans l'intervalle adéquat et lève une exception matérielle si ce n'est pas le cas. Son utilité se comprend quand on sait que tous les accès à un tableau sont vérifiés par l'interpréteur. L'interpréteur est censé ajouter des instructions pour vérifier les indices, à savoir deux ou trois branchements. Avec CHKA, le test se fait en une seule instruction. D'autres modifications mineures de l'encodage des instructions sont aussi présentes, ainsi que des modifications pour les instructions LOAD/STORE. Précisément, dans les modes "base + indice" et "base + décalage", le décalage et l'indice subissent maintenant des décalages. L'idée est de simplifier le calcul d'adresse. De quoi économiser quelques instructions lors des calculs d'adresse, ce qui facilite le travail de l'interpréteur/JIT. Et des instructions LOAD/STORE précises voient leur comportement modifié pour faciliter la gestion de la pile de Java ou l'exécution des méthodes locales. Pour faciliter le travail de l'interpréteur, deux instructions HBP et HBLP ont été ajoutées. Elles branchent vers une fonction qui gère les exceptions logicielles (une fonctionnalité présente dans de nombreux langages, comme Java). La première effectue un branchement vers la fonction en question, la seconde sauvegarde l'adresse de retour avant de faire ce branchement. Les deux peuvent brancher vers 256 fonctions pré-déterminées, sans avoir à présenter leur adresse. ==La traduction binaire accélérée par le matériel== Plus haut, nous avons surtout parlé des compilateurs et des interpréteurs. Cependant, nous devons aussi parler de la '''traduction binaire'''. Elle traduit un programme écrit dans un code machine vers un autre code machine. Par exemple, elle traduit un programme compilé pour un CPU x86 vers un code machine ARM. En général, le système d'exploitation est généralement compilé pour le jeu d'instruction natif, mais il exécute des applications prévues pour le x86. Les applications sont traduites par le système d'exploitation, avant d'exécuter le code traduit. : Nous parlerons dans la suite de code machine source et de code machine cible, pour parler respectivement du code à traduire et du code obtenu après traduction. Idem avec d'autres termes comme architecture cible/source, ou jeu d'instruction cible/source. La traduction binaire est surtout utilisée pour des questions d'émulation ou de compatibilité. Par exemple, lorsque les Macintosh sont passés de processeurs Power PC vers des processeurs x86, le système d'exploitation Mac OS utilisait la traduction binaire pour convertir les anciennes applications Power PC vers du code x86. Les utilisateurs n'y ont vu que du feu. Le système de traduction binaire était appelé Rosetta 1. Par la suite, lors de la transition de processeurs x86 vers des processeurs Apple, Rosetta 2 a vu le jour. La traduction binaire a surtout été utilisée pour traduire du code x86 vers un autre jeu d'instruction. Il faut dire que le x86 est le jeu d'instruction dominant. De nombreuses entreprises ont eu pour ambition de briser l'hégémonie du x86 sur PC, en remplaçant le x86 par un jeu d'instruction plus performant, tout en gardant une compatibilité maximale. La traduction binaire était la seule solution pratique. Elle était le plus souvent intégralement réalisée en logiciel, comme c'était le cas sur les architectures Itanium avec le ''IA-32 Execution Layer''. ===Les généralités sur la traduction binaire assistée en matériel=== L'architecture source est presque tout le temps une architecture CISC, assez ancienne, qu'on souhaite émuler. Le choix de l'architecture cible se porte souvent sur une architecture VLIW, fort différente de l'architecture source, et ce pour obtenir une économie de matériel conséquente. Pour s'exécuter rapidement, le code traduit doit exploiter le parallélisme d'instruction, à savoir exécuter plusieurs instructions en même temps dans des unités de calcul séparées. Il est possible d'utiliser un processeur superscalaire avec exécution dans le désordre pour cela, mais au prix d'un cout important en transistors. Alors qu'en utilisant un CPU VLIW, c'est le traducteur binaire qui fait tout le travail d'extraction du parallélisme d'instruction. Et après tout, quitte à avoir un traducteur binaire, autant lui refiler le boulot d'optimisation. Compiler du code à la volée est certes assez lourd, mais qu'il existe des algorithmes efficaces pour regrouper des instructions indépendantes dans une seule instruction VLIW. Au passage, le processeur VLIW a plus de registres que le processeur source. Cela permet de faire du renommage de registres directement en logiciel. Le traducteur binaire n'hésite pas à changer les noms de registres entre instructions source et instruction VLIW cible, afin de supprimer des dépendances de données. En conséquence, cela ouvre des opportunités de parallélisme, qui sont exploitées lors du regroupement des opérations en instructions VLIW. Vous pourriez penser que le choix d'un CPU VLIW pour émuler du CISC est tout sauf optimal. Vous devez penser que la traduction binaire est d'autant plus simple que l'architecture source et cibles sont semblables. Dans les faits, ce n'est pas tellement traduire les instructions qui pose problème, mais plus la gestion du registre d'état, des exceptions matérielles, de la mémoire virtuelle, la différence entre gros-boutisme et petit-boutisme, de même que des différences d'adressage pour les périphériques. Le premier problème est la gestion des conditions, notamment en présence d'un registre d'état. En pratique, la traduction binaire s'utilise pour traduire du code CISC vers du code RISC ou VLIW. Les architectures source ont donc un registre d'état, qui est mis à jour non seulement par des instructions de test, mais aussi des instructions arithmétiques. L'architecture cible n'a elle pas de registre d'état, mais des registres à prédicats. La traduction de l'un vers l'autre est alors quelque peu compliquée. Et elle est d'autant plus compliquée que le registre d'état est la source de dépendances d'instruction implicites, qui réduisent les performances. Par exemple, si une instruction arithmétique modifie le registre d'état, cela peut impacter l'exécution d'une instruction ultérieure, qui lit ce registre d'état. Pour éliminer ces fausses dépendances, le traducteur binaire doit renommer ce registre en logiciel et détecter les dépendances utiles. Et le compilateur doit gérer les cas où il y a beaucoup de distance entre les deux, voire les cas où l'instruction dépendante n'a pas encore été analysée par le compilateur. Un autre problème est lié à la gestion des exceptions matérielles, et précisément des exceptions précises. Pour rappel, une instruction VLIW regroupe plusieurs opérations, opérations qui correspondraient à une instruction machine sur un CPU pas VLIW. En conséquence, plusieurs instructions du langage machine "source" sont regroupées en une seule instruction VLIW. Et il faut tenir compte du cas où une instruction source lève une exception. Dans le code VLIW, cela signifie qu'une opération lève une exception, et il faut annuler partiellement l'instruction VLIW associée. Et par partiellement, on veut dire que seules les opérations suivantes dans l'ordre du programme source doivent être annulées, puis ré-exécutées. Et c'est un sacré casse-tête ! Les processeurs VLIW utilisent comme solution le mécanisme d'exceptions différées des processeurs EPIC, vu il y a quelques chapitres. Pour résumer, le code est exécuté par blocs d'instruction, délimités par des branchements ou tout autre limite/barrière pertinente dans le code. Un bloc de code est compilé en deux versions : une version rapide sans exceptions matérielles, une version lente qui gère les exceptions précises. La version rapide s'exécute sans exécuter les exceptions. Cependant, les exceptions matérielles sont enregistrées, pour être prises en compte à la toute fin du bloc de code. La version rapide est exécutée en premier et elle mémorise si une exception matérielle a eu lieu. Puis une instruction vérifie si une exception a eu lieu et décide quoi faire. Si aucune exception n'a eu lieu, elle passe à la suite du programme, les résultats du bloc de code sont définitivement acceptés. Mais si une exception a eu lieu, tout est annulé. Le processeur est remis dans l'état initial, puis le code est ré-exécuté instruction par instruction de manière à gérer l'exception correctement. ===La traduction par pré-décodage=== Une première solution serait de faire la traduction binaire dans le cache d'instruction. Lors d'un défaut de cache, le code chargé depuis la RAM est traduit en code machine cible. Et c'est ce code machine cible qui est mémorisé dans le cache d'instruction et exécuté par le processeur. La technique marche sur le papier et n'est qu'une amélioration des techniques de pré-décodage vues il y a quelques chapitres. Cependant, le code obtenu est une traduction assez basique, qui n'incorpore pas d'optimisations dignes de ce nom. De plus, elle gère mal le cas où la taille du code source et cible sont potentiellement très différentes. Concrètement, elle est surtout utile pour traduire du code d'un processeur RISC vers un autre processeur RISC, dont les tailles d'instructions sont similaires. Elle ne permet pas d'émuler plusieurs jeux d'instructions différents, le cout en matériel (un circuit de pré-décodage par jeu d'instruction) serait trop important. ===Le projet DAISY d'IBM=== De très rares processeurs étaient conçus pour accélérer cette traduction binaire, afin de garder de bonnes performances. Les premiers à avoir étudié l'idée étaient IBM, avec leur projet DAISY (''Dynamically Architected Instruction Set from Yorktown''). Le projet de base était de convertir à la volée du code compilé pour des CPU Power PC, vers du code VLIW. Les chercheurs d'IBM avaient développé un algorithme de traduction dynamique efficace, ainsi qu'un processeur VLIW disposant d'optimisations spécifiques à la traduction binaire dynamique, à la volée. De nombreuses idées de ce projet ont été reprises ou re-découvertes par la société Transmetta, avec ses processeurs Crusoe et Efficieon, puis par NVIDIA avec son projet Denver. Les ingénieurs de ce projet ont étudié la possibilité d'émuler plusieurs jeux d'instructions différents sur un même processeur, notamment le s390 d'IBM et le x86. En théorie, cela demande juste d'avoir plusieurs programmes de traduction binaire : un pour le x86, un autre pour le s390, éventuellement un autre pour le Power PC. Mais cela ne s'est pas concrétisé. Mais les ingénieurs ont étudié quelle pourrait être l'architecture VLIW idéale pour ça. Par exemple, il fallait des additions 3-opérandes pour simplifier les calculs d'adresse. Une difficulté était la gestion du registre d'état, dont les bits ne sont pas mis à jour de la même manière sur le x86 et le s390 ou le Power PC. La différence entre gros-boutisme et petit-boutisme était aussi un problème, de même que des différences d'adressage pour les périphériques. Pour que la traduction binaire soit efficace, le processeur VLIW intègre diverses optimisations, comme les exceptions différées et les branchements multi-voies. La principale est la suivante : seul le code des boucles ou fonctions exécutées fréquemment est traduit en code VLIW, le reste du code est interprété. En effet, traduire du code binaire prend plus de temps qu'une simple interprétation. Pour du code qui ne sera exécutée qu'une seule fois, il est plus rapide d'utiliser l'interprétation. Par contre, pour du code exécuté beaucoup de fois, le cout de la traduction binaire est amorti, dilué sur N exécutions, compensé par le gain en temps d'exécution de ce code traduit. Au final, cela permet de ne traduire que le code qui le mérite. La détection du code fréquemment exécuté est réalisée dans le cœur VLIW. L'unité de branchement mémorise les derniers branchements rencontrés et le nombre de fois qu'ils ont été exécutés. Si ils ont été exécutés un certain nombre de fois, l'unité de branchement lève une exception matérielle, qui invoque le traducteur binaire. La routine de cette exception est le traducteur binaire proprement dit. En clair, la traduction binaire est démarrée quand le processeur détecte qu'une fonction a été exécutée plus de N fois, via une exception matérielle dédiée. La mesure du nombre d'exécution d'un branchement se fait dans l'unité de calcul dédiée aux branchements, avec l'aide d'une sorte de ''branch adress buffer'' modifié. Pour rappel, le ''branch adress buffer'' mémorise le ''Program counter'' de chaque branchement récemment rencontré. Ici, chaque entrée du ''branch adress buffer'' est associée à un compteur incrémenté à chaque exécution du branchement. ===Les processeurs Crusoe et Efficieon de Transmetta=== Les processeurs Crusoe Et Efficeron sont deux processeurs VLIW produits par la société Transmetta, une société californienne rachetée par NVIDIA. Ils étaient conçus pour exécuter spécifiquement des programmes x86, système d'exploitation inclus. L'idée derrière ce projet était d'exécuter du code x86 sans que les applications, ni même le système d'exploitation et le BIOS soient au courant ! Les processeurs Transmetta étaient en réalité conçus pour exécuter un programme unique, le ''Code Morphing Software'' (CMS), qui traduisait le code x86 en code VLIW. Transmetta ne voulait pas que son processeur VLIW soit exploité directement, sans le CMS. Il n'avait pas rendu public de compilateur pour traduire du C vers du code VLIW, il n'avait pas donné la documentation du jeu d'instruction VLIW. Des efforts de rétro-ingénieurie, documentés sur le site realworldtech, ont cependant permis de comprendre comment étaient encodées les instructions du processeur Crusoe. Comme pour le projet Daisy, le ''Code Morphing Software'' utilisait à la fois interprétation et traduction binaire, selon les besoins. Les instructions sont interprétées lors de leurs premières exécutions, mais le CMS bascule sur de la traduction en code VLIW après un certain nombre d'exécution. Ainsi, les boucles souvent exécutées sont traduites en code VLIW, alors que le reste du code est interprété. Le choix du code à traduire est le fait du CMS, il utilise des heuristiques complexes pour, qui ne sont pas connues dans le détail. Le ''Code Morphing Software'' est mémorisé dans une EEPROM, ce qui en fait un ''firmware'' situé en dessous du BIOS. Le CMS démarre ensuite le BIOS, qui lui-même démarre le système d'exploitation, qui lui-même démarre les pilotes de périphériques et les programmes. Le CMS se réserve les 16 premiers mébioctets de l'espace d'adressage. L'EEPROM du CMS est mappée dedans, mais n'en utilise que la moitié. Le reste est utilisé comme cache, pour mémoriser le code VLIW traduit par le CMS. Les deux processeurs intégraient aussi deux mémoires SRAM, utilisées par le CMS, appelées la ''local program memory'' (LPM) et la ''local data memory'' (LDM). La première contient du code qui gére les interruptions, la mémoire virtuelle, les exceptions matérielles, les problèmes d'alignement mémoire, et quelques fonctions très fréquemment utilisées par le CMS. La seconde est de la mémoire RAM utilisée par ces fonctions de la ''local program memory''. Les mémoires caches L1 et L2 sont séparées de ces deux mémoires. Le processeur Crusoe et Efficieon étaient des processeurs VLIW très simples. Ils n'avaient même pas de MMU, ni de fonctionnalités importantes sur les CPU x86. Pour Crusoe, les instructions VLIW étaient encodées sur 64 ou 128 bits, et regroupaient entre 2 à 4 opérations. Les instructions LOAD/STORE ne supportaient pas d'adressage indicé, juste de l'adressage indirect à registre. Crusoe avait 5 unités de calcul : deux ALU entières, une unité LOAD/STORE, une unité de branchement, une FPU. Efficieon doublait le nombre d'ALU entière et d'unité LOAD/STORE, les instructions VLIW passaient à 256 bits. Le processeur Crusoe contient 160 registres, dont 64 registres généraux, 32 registres flottants. Les registres généraux font 32 bits, ce qui est cohérent avec le fait que les CPU x86 de l'époque étaient des processeurs 32 bits. Quant aux registres flottants, ils faisaient 80 bits, ce qui colle avec la taille des registres flottants de la FPU x87 utilisée à l'époque. Sur les 64 registres généraux, seuls 48 étaient réellement utilisable pour mémoriser des opérandes. Une partie des registres généraux étaient utilisés pour la gestion de la pile, d'autres pour mémoriser l'état du CPU x86 émulé, un registre était un registre zéro non accesible en écriture. La gestion des exceptions est optimisée avec un système d'exceptions différées, le même que celui décrit plus haut. Le processeur utilise un système similaire, pour effectuer des lectures anticipées, qui a été expliqué dans le chapitre sur les processeurs VLIW/EPIC. Sauf qu'il s'agit là d'une optimisation pour gagner en performances en général, pas une aide utile pour gérer des exceptions précises ou toute autre subtilité de la traduction binaire. Pour gérer les exceptions différées et les lectures anticipées, le processeur mémorise l'état du processeur avant de démarrer un bloc de code. Pour cela, le processeur contient des ''shadow registers'', 48 registres entiers et 16 registres flottants. Vous remarquerez qu'il y a autant de registres entiers que de ''shadow registers'' entiers. Les registres entiers/flottants sont mémorisés dans les ''shadow registers'' avant d'exécuter un bloc de code, afin que le processeur puisse revenir à l'état de base. Une autre fonctionnalité liée est le ''gated store buffer''. Le principe est d'éviter toute écriture en mémoire RAM, tant que le code VLIW n'a pas émis d'instruction ''commit''. L'idée est que les écritures sont conservées dans la ''store queue'' de l'unité LOAD/STORE. Pour rappel, la ''store queue'' met en attente les écritures soit tant que les écritures ne sont pas terminées, soit tant que la RAM est occupée. L'idée est que les écritures sont accumulées dans la ''store queue'' tant que l'instruction ''commit'' n'a pas eu lieu. Le ''store queue'' est "déversé dans la RAM" seulement quand l'instruction ''commit'' s'exécute. ===Les CPU NVIDIA Denver=== Nvidia a tenté quelque chose de similaire avec son projet Denver. NVIDIA a racheté l'entreprise Transmetta, ses brevets, et a tenté de refaire la même chose. Le projet initial était de traduire du code x86 vers un jeu d'instruction VLIW propriétaire, comme l'a fait Transmetta. Mais NVIDIA n'a pas réussi à acquérir la licence du jeu d'instruction x86 et s'est rabattu sur le jeu d'instruction ARM. Le processeur né de ce projet est le Tegra K1-64 CPU. Il s'agit d'un processeur composé d'un cœur VLIW, couplé à deux décodeurs d'instructions ARM. Les deux décodeurs ARM traduisent une paire d'instructions ARM consécutives en instructions VLIW, pour les exécuter sur le cœur VLIW. Le CPU peut fonctionner selon deux modes : ARM et VLIW. En somme, le processeur supporte deux jeux d'instruction : le jeu d'instruction ARM, et un jeu d'instruction VLIW. En mode ARM, les instructions ARM sont chargées depuis le cache, traduites en VLIW par les décodeurs ARM, puis exécutées par le cœur VLIW. En mode optimisé, le CPU exécute des instructions VLIW chargées depuis le cache d'instruction, les décodeurs ne sont pas utilisés. : Le CPU supporte en réalité deux jeux d'instruction ARM : ARM8 et 7. Mais ils correspondent au même mode pour le CPU. Un point important est que le processeur peut changer très rapidement de mode, en à peine quelques cycles d'horloges. On n'est clairement pas dans le cas des CPU x86, qui mettent des plombes pour passer du mode 32 à 64 bits et inversement. La commutation est tellement rapide qu'on peut considérer que le processeur supporte deux jeux d'instruction simultanément. Et le support simultané de deux jeux d'instruction facilite l'implémentation de la traduction binaire. Encore une fois, inutile de traduire du code qui ne sera exécutée qu'une seule fois, mieux vaut l'exécuter directement dans le mode ARM. Surtout que le processeur supporte un mode ARM qui permet d'exécuter du code ARM sans perte de performance. Par contre, le code fréquemment utilisé, notamment dans des boucles critiques, est traduit en VLIW. Au final, les performances sont optimisées, en limitant le travail du logiciel traducteur binaire. La détection du code fréquemment exécuté se fait différemment que pour les processeurs Transmetta et Daisy. L'unité de branchement ne détecte pas directement le code beaucoup exécuté, même si elle a un rôle à jouer. A la place, la détection est réalisée en logiciel, par un ''thread'' dédié, qui s'exécute sur un cœur séparé. Il détecte le code fréquemment utilisé en regardent les branchements exécutés récemment. Pour cela, le processeur mémorise l'historique des branchements pris récemment et copie cette historique dans une mémoire tampon dédiée, partagée entre tous les cœurs. Vu qu'elle est partagée, le ''thread'' de détection a accès à l'historique sans pertes de performances, seule la copie de l'historique dans ce tampon a un cout en performance, pas son partage. Le code VLIW obtenu après traduction binaire est mémorisé dans la mémoire RAM, dans une portion spécialement réservée pour. Elle fait 128 mébioctets, et est appelée le '''cache d'optimisation''' par NVIDIA. Le cache d'optimisation est protégé en écriture et seul le traducteur binaire peut écrire dedans. Au passage, le traducteur binaire est du code VLIW, ce qui fait qu'il est placé dans le cache d'optimisation. Le processeur détecte automatiquement quand une fonction pour laquelle le code compilé équivalent est disponible. Pour cela, l'unité de chargement contient une table de correspondance entre l'adresse de la fonction ARM, et l'adresse de son équivalent compilé. Quand une fonction est appelée, l'unité de chargement regarde l'adresse de destination du branchement. Si l'adresse est dans cette table, elle récupère l'adresse de la fonction compilée et branche vers celle-ci. Le processeur dispose d'un cache d'instruction de 128 kibioctets, ce qui est très important, mais nécessaire vu la taille des instructions VLIW. Le coeur VLIW dispose de ses propres décodeurs, d'un ''scoreboard'', de 7 unités de calcul et de registres. Pour les unités de calcul, il a deux FPU, deux ALU, une unité de branchement, et deux unités LOAD-STORE faisant aussi ALU. Un point important est que le CPU dispose de 64 registres entiers et de 64 registres flottants. C'est deux fois plus que supporte le jeu d'instruction ARM. En mode ARM, seule la moitié des registres est utilisée. Mais en mode optimisé, le traducteur binaire utilise bien les 64 registres, grâce à une sorte de renommage de registres logicielle. Pour simplifier la traduction binaire, le processeur supporte les techniques vues précédemment. Le processeur intègre un ''gated store buffer'' similaire à celui des processeurs Transmetta. Les techniques d'exceptions différées sont aussi supportées, comme sur les processeurs Transmetta. Il a aussi une unité de préchargement décrite par NVIDIA comme agressive, avec support du préchargement de type anticipé (''runahead''). ===L'émulation de la mémoire virtuelle=== Pour finir, il faut parler de la mémoire virtuelle avec la traduction binaire. Sur le processeur source, le processeur gère à la fois des adresses physiques et virtuelles et sa MMU fait la conversion entre les deux. Sur le processeur VLIW, la MMU est simulée par le programme de traduction binaire, partiellement ou totalement. L'implémentation exacte varie suivant que l'on parle du projet Daisy, des processeurs Transmetta ou de Denver. Sur les processeurs Transmetta, il n'y a pas de MMU ni de mémoire virtuelle. A la place, le programme de traduction binaire émule la mémoire virtuelle du processeur source. Le processeur VLIW ne gère que des adresses physiques, rien d'autre. L'espace d'adressage physique du processeur VLIW a la même taille que celui du processeur émulé, ici des processeurs x86. Il est vraisemblable que les adresses physiques utilisées par le processeur x86 sont les mêmes que celles du processeur VLIW. Sur le projet Daisy, le processeur VLIW gère la mémoire virtuelle via pagination, comme les processeurs émulés. Ce qui fait qu'il gère un espace d'adressage virtuel et un espace d'adressage physique. Pour éviter toute confusion, nous parlerons d'adresse physique/virtuelle VLIW pour les adresses physiques/virtuelles du processeur VLIW, d'adresse virtuelle/physique source pour celle du jeu d'instruction traduit, à savoir du Power PC ou du s390, éventuellement de l'x86. Il faut alors faire le lien entre adresses physiques source et adresses virtuelles VLIW. Pour cela, rien de plus simple : il y a correspondance parfaite. L'adresse physique source numéro N correspond à l'adresse virtuelle VLIW numéro N. Cependant, cela signifie que tout l'espace d'adressage virtuel VLIW serait utilisé par le code à traduire. En réalité, il faut ajouter de la place pour le code traduit et le programme de traduction binaire. La conséquence est que l'espace d'adressage virtuel VLIW est plus large que l'espace d'adressage source. L'espace d'adressage virtuel VLIW est découpé en trois sections : une section pour le code Power PC à traduire, une autre pour le code traduit en VLIW, et une dernière réservée au traducteur binaire. Le programme de traduction est placé dans une ROM mappée en mémoire dans la seconde section. Le reste de la seconde section est réservé à une mémoire RAM utilisée par le programme de traduction. [[File:Mémoire virtuelle sur le projet Daisy.png|centre|vignette|upright=1.5|Mémoire virtuelle sur le projet Daisy]] Il faut noter que chaque page physique source, une fois traduite, correspond à N pages virtuelles VLIW. La raison est que le code traduit prend plus de place que le code machine originel. En conséquence, on doit utiliser une page finale plus grande. Pour le reste, des pages contiguës en mémoire physique source sont elles aussi contiguës en mémoire virtuelle VLIW. Le calcul d'adresse est donc simple : l'adresse physique source est multipliée par N, puis on ajoute l'adresse de base à laquelle commence la section pour le code traduit. ==La traduction binaire sur le Burroughs B1700== Nous venons de voir comment accélérer en matériel la traduction binaire, avec des techniques bien spécifiques. Le Burroughs B1700 a procédé autrement et il est intéressant d'étudier en détail son architecture. Son architecture est décrite dans le livre "Interpreting Machines : Architecture and Programming of the Bl700/Bl800 Series", écrit par Elliott I. Organick et James A. Hinds. Une partie de ce qui vfa suivre est un très court résumé de ce livre. ===Le Burroughs B1700 : le mal-nommé ''split-level control store''=== Le Burroughs B1700 avait plusieurs modèles : le B1726, le B1710 et le B1800. Ils se distinguaient par la manière dont le code machine source était traduit par le traducteur binaire. Dans ce qui suit, nous parlerons de traducteur binaire pour parler soit d'un interpréteur pour un langage de programmation de haut niveau, soit pour un logiciel de traduction binaire. Le B1710 plaquait le traducteur binaire en mémoire RAM. Le B1800 faisait pareil, mais avait ajouté un cache dédié au code du traducteur binaire, afin d'améliorer les performances. Le B1726 avait un traducteur binaire scindé en deux : une partie était en mémoire RAM, l'autre était dans un ''local store'' dédié. Le processeur peut exécuter le traducteur binaire soit depuis le ''local store'', soit depuis la mémoire RAM avec une pénalité en termes de performances. La documentation appelle cela un '''''split-level control store''''', mais le terme est trompeur : la documentation qualifie de microcode le langage machine du Burroughs B1700, ce qui est à l'origine de beaucoup de confusions. Une autre source de confusion est que ce langage machine a quelques propriétés qu'on pourrait attendre d'un microcode, mais certaines sont manquantes. Par exemple, il peut adresser les registres d’interfaçage mémoire, mais c'est parce que ce sont des registres architecturaux sur ce processeur. Ses instructions n'encodent pas de signaux de commande, par contre, elles font 16 bits et adressent les opérandes de manière implicite. Le ''local store'' mémorisait au maximum 2048 instructions machine, ce qui fait que les 2048 premières "adresses" correspondaient au ''local store''. Les suivantes étaient des adresses de mémoire RAM qui contenaient le reste du traducteur binaire. En pratique, l'adresse qui séparait les deux était configurable ! On pouvait limiter la taille du ''local store'' à un multiple de 32 instructions ! Les adresses restantes étaient alors réattribuées à la RAM. Par exemple, on pouvait attribuer 64 adresses au ''local store'', les 2048 - 64 adresses restantes étaient alors attribuées à la mémoire RAM. Un registre limite, nommé TDPM, indiquait quelle était l'adresse de démarcation entre les deux. Il contenait 4 bits, dont la valeur était multipliée par 32, pour obtenir l'adresse limite. Le programmeur devait découper le traducteur binaire en segments, certains étant placé dans le ''local store'', les autres étant en mémoire RAM. Mieux que ça, le processeur autorisait d'utiliser la technique de l'''overlaying'' pour le microcode ! Il y avait même une fonction OVERLAY dédiée pour ! OVERLAY avait besoin de trois opérandes : la taille du segment à copier, son adresse source en mémoire RAM, son adresse de destination dans la micro-SRAM. L'interpréteur réservait un "segment" en mémoire RAM, pour les données, dont le processeur connaissait la position. En-dehors de ce segment, il n'y a que des instructions machines à traduire, et le traducteur binaire. Le CPU incorporait pour cela deux registres, un pour l'adresse de base du segment de données, un autre pour son adresse de fin. Il incorporait une protection mémoire limitée sur ce segment, à savoir que seul l'interpréteur pouvait lire ou écrire dans ce segment. ===Une architecture bit-adressable=== Le CPU Burroughs B1700 était '''bit-adressable'''. En clair, il pouvait adresser la mémoire bit par bit, si nécessaire. Un avantage est que cela facilitait la traduction binaire des instructions machine, mais nous détaillerons cela plus bas. Un autre avantage est que cela permettait d'émuler des architectures dont la taille des registres/''byte''/mots était très différente. Et cet avantage mérite quelques explications immédiates. Le processeur était un CPU 24 bits, mais il pouvait émuler des processeurs 16 bits, 8 bits, 4 bits, 1 bit, 9 bits, ou toute autre valeur. Pour cela, le processeur lisait des mots de 24 bits et utilisant un ''barrel shifter'' pour sélectionner les bits adéquats. Par exemple, pour un accès de 16 bits, la donnée lue en mémoire était masquée de manière à ne garder que les 16 bits de poids faible. Le ''barrel shifter''/circuit de masquage était placé directement avant le bus mémoire, sur le trajet des données/instructions. Le ''barrel shifter'' était commandé par un registre ''Field Unit'', qui précisait quelle était la taille des données à charger. Pour communiquer avec la mémoire, il avait trois registres séparés : READ, WRITE, MAR. READ contient la donnée lue lors d'une lecture, WRITE est pour une donnée à écrire lors d’une écriture, MAR contient l'adresse à lire ou en cours de lecture. Il s'agit de registres d’interfaçage mémoire. Le fait que ces registres d’interfaçage mémoire et IO soit adressable est peu commun, et c'est plus quelque chose qu'on attend d'un microcode que d'instructions machines. Mais il faut bien comprendre que les registres d’interfaçage mémoire ont été promus au rang de registres architecturaux, sur ce processeur. De même, pour communiquer avec les entrées-sorties, il disposait de deux registres CMND et DATA : CMND pour envoyer une commande à une entrée-sortie, DATA pour lire ou écrire une donnée. Il y avait aussi un registre U, qui mémorisait une donnée lue depuis le lecteur de cassette. ===L'ALU de taille variable=== Ils avaient une '''ALU de taille variable'''. En clair, l'ALU pouvait faire des calculs sur un nombre de bits compris entre 0 et 24. Pour cela, les calculs étaient fait avec une ALU de 24 bits, puis les bits de poids fort inutiles étaient masqués. L'ALU de 24 bits pouvait masquer les bits de poids fort du résultat, mais avec des limitations. Elle gérait des opérandes et résultats de 0, 4, 8, 12, 16, 20 et 24 bits. Pour avoir un réglage plus fin, une ALU 4 bits séparée permettait de corriger le résultat. Les registres X et Y étaient reliés à une ALU de 24 bits capable d'effectuer des opérations arithmétiques basiques. Fait étonnant, l'ALU faisait tous les calculs en même temps et fournissait ses résultats dans 7 registres : un registre SUM pour l'addition, un registre DIFFERENCE pour la soustraction, et un registre par opération bit à bit. Les registres pour les opérations bit à bit sont : CMPX et CMPY pour l'opération NOT sur chaque opérande, XANY pour le ET bit à bit, XEOY pour le XOR, XORY pour le OU logique. Les additions et soustractions pouvaient se faire sur des opérandes codés en binaire ou en BCD, suivant le code opération utilisé. A cela, il fallait ajouter deux registres de "résultat" MSKX et MSKY. Ils fournissaient un masque dépendant de la taille du résultat en bits. C'est ce registre qui permettait de gérer des données de 0, 4, 8, 12, 16, 20, et 24 bits, alors que l'ALU faisait 24 bits. Le masque dans ce registre pouvait servir d'opérande dans une opération de masquage ultérieure, pour corriger le résultat. L'ALU prenait en opérande les registres X et Y, mais aussi un registre CYF pour la retenue entrante. En sortie, il fournissait aussi deux retenue sortantes : une pour l'addition et une autre pour la soustraction. Elles étaient mémorisées dans les registres CYL et CYD, respectivement. L'ALU fournissait aussi 12 conditions, qui alimentait un registre d'état. Le registre d'état était segmenté en trois registres appelés XYST, XYCN et BICN. Les registres X et Y sont des registres pour les opérandes, mais il y a aussi deux autres registres de 24 bits. Il s'agit des registres T et L, qui sont des registres généraux. Les registres T et L étaient découpés en 6 sous-registres de 4 bits, nommés LA LB LC LD LE et LF pour le registre L, et TA TB TC TD TE et TF pour le registre T. Ils étaient adressables comme n'importe quel registre. Le processeur contenait de nombreux autres registres de 4 bits, dont les sous-registres de T et L. En tout, il y a 27 registres de 4 bits adressables ! Et le processeur disposait d'une ALU 4 bits pour manipuler ces registres de 4 bits ! Elle est capable de faire des MOV entre registres de 4 bits, des opérations bit à bit, et peut aussi tester si un bit vaut 0 ou 1. La dernière possibilité est très utile pour implémenter les branchements. De plus, cela permettait de faire des opérations de masquage, notamment pour appliquer un masquage supplémentaire à celui de l'ALU. Par exemple, pour gérer des opérations sur 14 bits, on effectue une opération sur 16 bits avec l'ALU, et on masque les 2 bits manquants avec l'ALU 4 bits. ===Le code machine du Burroughs B1700=== Le processeur utilise des instructions de 16 bits de long, qui sont copiées dans registre d'instruction M de 16 bits. Le ''program counter'' est appelé le registre A. Les instructions du processeur font 16 bits de long, et elles sont alignées en RAM sur 16 bits, ce qui fait que les 4 bits de poids fort de ce registre valent 0, seuls les 14 bits de poids fort sont utiles. Les adresses mentionnées plus haut, qui adressent les micro-opérations en mémoire RAM, sont des adresses de mots de 16 bits, pas les adresses de bit individuels. Fait intéressant, il était possible de faire un OU logique entre l'instruction lue et un registre du processeur, le résultat étant mémorisé dans le registre M. Cela permettait de faire du code automodifiant, pour émuler des modes d'adressage complexe. Cela permettait par exemple de remplacer une adresse dans une micro-opération. L'adresse est alors surimposée avec un OU dans le champ d'adresse, alors que le reste de l'instruction n'est pas modifié. Afin de gérer les interruptions, ces dernières étaient partiellement détectées en logiciel. Une interruption doit être prise en compte à la toute fin de l'exécution d'une instruction source, quand celle-ci a finit son travail. Mais vu qu'ici, les instructions sont émulées avec des séries d'instructions cibles, il y a un problème : il est possible d'interrompre une instruction émulée en plein milieu. Pour éviter cela, le processeur ne déclenche pas les interruptions matérielles immédiatement, elles sont mises en attente. Une instruction dédiée vérifie si une interruption est en attente, et l'exécute alors cas échéant. L'instruction en question est utilisée en dernier, dans la suite d'instruction qui émule l'instruction source. ===La traduction binaire sur le Burroughs B1700=== Les instructions source peuvent faire 16 bit avec tel jeu d'instruction, 8 bits avec un autre, 32 bits sur un autre, etc. Elles peuvent aussi être de longueur fixe ou variable ! Le B1700 utilisait une architecture bit-adressable pour gérer ces contraintes. En effet, avec un CPU bit-adressable, les contraintes d'alignement des instructions disparaissent ! Dans ce qui suit, on suppose que l'instruction est composée de '''champs''' : l'opcode, les opérandes, les adresses absolues, les constantes immédiates, les numéros de registre, etc. L'idée est que l'instruction source est chargée dans le registre T, puis découpée en champs, qui sont copiés dans les registres du processeur ou envoyés au microcode. Je rappelle que le registre T est relié à un ''barrel shifter'', différent au précédent, qui permettait de masquer, décaler et extraire une donnée de ce registre. Et il y a la possibilité d'extraire une suite de bit de ce registre T, pour la copier dans un autre registre. Par exemple, prenons une instruction composée d'un opcode, d'une constante immédiate et d'une adresse immédiate. Premièrement, elle est chargée dans le registre T. Deuxièmement, la constante immédiate est extraite du registre T et copiée dans le registre X. Troisièmement, l'adresse est extraite et est copiée dans le registre d’interfaçage mémoire, une lecture est démarrée, et l'opérande lue est copiée dans le registre Y. Enfin, l'instruction équivalente à l'opcode est exécutée par le microcode. Le tout prend plusieurs instructions : une première pour lire l'opcode, une seconde pour lire la première opérande, une troisième pour lire la seconde opérande, una quatrième pour tout exécuter. Notons que suivant la taille des champs, on peut en charger plusieurs à la fois. Par exemple, imaginons qu'on ait chargé 24 bits, contenant un opcode de 8 bits et une constante immédiate de 16 bits : le registre T est découpé en un opcode et une constante, avec deux instructions EXTRACT. L'architecture est bit-adressable car elle gère des champs qui ne font pas forcément 8 ou 16 bits. Par exemple, certains jeux d'instructions ont des opcodes codés sur 6 ou 7 bits, voire 9-10 bits. Utiliser une architecture bit-adressable règle ce problème d'alignement. On peut lire un champ depuis la mémoire RAM en précisant l'adresse de son premier bit, pas besoin de gérer des accès non-alignés. Par exemple, prenons une instruction source avec un opcode de 7 bits, suivi par une adresse de 12 bits et une constante immédiate de 15 bits. On peut charger 7 + 12 bits en un premier accès, pour extraite l'opcode et l'adresse, suivi par un second accès de 15 bits pour extraire la constante immédiate. Pour charger un champ, il faut connaitre plusieurs informations : l'adresse de son premier bit, sa taille. Pour cela, le B1700 contient deux registres, nommés ''Field Adress'' (FA) et ''Field Length'' (FL), qui indiquent l'adresse du premier bit et la taille de l'instruction à charger. Le premier fait 24 bits, ce qui permet de gérer des adresses de 24 bits. N'oubliez pas que le processeur est bit-adressable, ce qui permet d'adresser 16 Mébi-bits, soit 2 mébioctets. Le registre pour la taille fait lui 16 bits, ce qui permet de gérer des instructions de 65536 bits, soit 8192 octets ! Leur nom commence par ''field'', car le processeur peut gérer des instructions de taille variable et/ou plus longues que 24 bits. Dans ce cas, le chargement de l'instruction se fait champ par champ. Pour donner un exemple, prenons une instruction composée de 20 champs de 12 bits chacun. Les champs sont chargés un par un dans le registre T. Le registre FA est alors initialisé avec l'adresse de l'instruction, il est incrémenté de 12 à chaque cycle. Le registre FL est initialisé avec la taille de l'instruction, soit 20 * 12 = 240 bits, et il est décrémenté de 16 à chaque cycle. Précisons cependant que la taille d'un champ peut être changée d'un cycle à l'autre. Par exemple, le processeur peut charger un opcode de 8 bits, l'interpréter, détecter que l'instruction demande ensuite de charger une adresse de 16 bits. Dans ce cas, le registre FL est immédiatement altéré pour charger 16 bits, au lieu des 8 bits de l'opcode immédiatement précédent. ==Les jeux d'instructions dédiés à un langage de programmation== De rares processeurs sont conçus pour un langage de programmation en particulier. On appelle ces processeurs, conçus pour des besoins particuliers, des '''processeurs dédiés'''. Par exemple, les fameux ''Burrough E-mode'' B5000/B6000/B7000 étaient spécialement conçus pour exécuter de l'ALGOL-60. Leurs cousins B2000/B3000/B4000 étaient eux conçus pour le COBOL. Des langages fonctionnels ont aussi eu droit à leurs processeurs dédiés. Le prolog en est un bel exemple, avec les superordinateurs de 5ème génération qui lui étaient dédié. On peut aussi citer les machines LISP, dédiés au langage LISP, qui datent des années 1970. Elles étaient capables d’exécuter certaines fonctions de base du langage directement dans leurs circuits : elles possédaient notamment un ''garbage collector'' câblé dans ses circuits ainsi que des instructions machines supportant un typage déterminé à l’exécution. Les processeurs dédiés ont eu leur heure de gloire au début de l'informatique, à une époque où les langages de haut niveau venaient d'être inventés. À cette époque, les compilateurs n'étaient pas performants et ne savaient pas bien optimiser le code machine. Il était alors rationnel, pour l'époque, de rapprocher le code machine cible et le langage de programmation de haut niveau. De nombreuses architectures dédiés ont ainsi été inventées, avant que les concepteurs se rendent compte des défauts de cette approche. Les défauts en question ne sont pas nombreux, mais assez simples à comprendre. Premièrement, elles sont très rapides pour un langage de programmation en particulier, mais sont assez mauvaises pour les autres, d'où un problème de "compatibilité". Ajoutons à cela que les langages de programmation peuvent évoluer, devenir de moins en moins populaires/utilisés, ce qui rend la création d'architectures généralistes plus pertinente. Enfin, les architectures dédiées sont évidemment des processeurs CISC, pour implémenter les nombreuses fonctionnalités des langages évolués. Et les défauts des CISC sont assez rédhibitoires à l'heure actuelle. À l'heure actuelle, les algorithmes des compilateurs se sont améliorés et savent nettement mieux utiliser le matériel. Ils produisent du code machine efficace, ce qui rend les architecture dédiées bien moins intéressantes. Si on ajoute les défauts de ces architectures dédiées, par étonnant que les architectures dédiées aient presque disparues. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'accélération matérielle de la virtualisation | prevText=L'accélération matérielle de la virtualisation | next=Le matériel réseau | nextText=Le matériel réseau }} </noinclude> 70k2g4qgogkbyho1cpbyouiaybc9hpc 764057 764056 2026-04-19T20:44:20Z Mewtow 31375 /* Le code machine du Burroughs B1700 */ 764057 wikitext text/x-wiki De nos jours, la majorité des programmeurs programment dans des langages de haut niveau. Il est très rare pour eux d'avoir à utiliser de l'assembleur, et encore moins un langage machine. Les programmes écrits dans un langage de haut niveau sont traduits en langage machine par un logiciel appelé le compilateur. [[File:Compilation.PNG|centre|vignette|upright=2.5|Compilateur : principe.]] Il se trouve que quelques rares processeurs sont conçus pour faciliter le travail du compilateur. Et ce sont ces processeurs que nous allons voir dans ce qui suit. Ils représentent une catégorie de jeux d'instruction à part, qui n'a pas vraiment de nom. ==La compilation et l’interprétation== Un compilateur traduit donc du code source, écrit dans un langage de haut niveau, vers du langage machine. La traduction est rarement directe. En général, le compilateur traduit le code source en assembleur, qui est lui-même traduit en langage machine. L'assembleur est une représentation textuelle du langage machine alors que le code machine est du binaire exécutable par le processeur. La traduction se fait donc en deux étapes, la '''compilation''' proprement dite et l''''assemblage''', réalisées respectivement par un compilateur et un assembleur. Il faut parfois rajouter une troisième phase d'édition des liens, que nous passons volontairement sous silence. {| |[[File:Code C.png|vignette|200px|Le code source, ici en C.]] |[[File:Assem.png|vignette|250px|Le code assembleur.]] |[[File:Binary file - hello world (C programming).png|vignette|150px|Le code machine.]] |} ===L'interprétation et la compilation à la volée=== Une alternative à la compilation est l''''interprétation''', qui transforme le code source en code machine à la volée. Avec l'interprétation, le code source est passé à un logiciel appelé l''''interpréteur''', qui exécute le code source ligne de code par ligne de code. Une ligne de code est traduite en code machine, qui est exécutée, puis l'interpréteur passe à la ligne suivante. [[File:Kaantotulkkaus.png|centre|vignette|upright=3|Compilation versus interprétation.]] Un défaut de l'interprétation est son cout en performance, qui est assez important. Aussi, l'interprétation stricte a évolué vers un hybride entre interprétation et compilation. L'idée est qu'une partie peu importante du code source est interprétée, alors que le code important est compilé juste avant d'être exécuté. Pour le dire autrement, le code source est partiellement compilé à la volée, juste avant son exécution. Aussi, on parle de '''compilation à la volée'''. Le terme en anglais est ''Just In Time compilation'', abrévié en JIT. En soi, le JIT est associé à de l'interprétation, les JIT sont en réalité des hybrides interpréteurs-compilateurs. Un point important est que compiler du code à la volée est assez lourd, cela a un cout en performance. Par contre, le code compilé s'exécute plus vite que du code interprété. Si on exécute le code compilé une seule fois, le cout de la compilation l'emporte sur le gain à l'exécution. Mais le code compilé est réutilisé autant de fois que nécessaire. Le gain à l'exécution est donc multiplié par le nombre d'exécutions, alors que le cout de la compilation est répartit sur plusieurs exécutions, il est amorti. Les portions du code sont choisies de manière à avoir un gain en performance maximal. L'idée générale est que le code exécuté très souvent est compilé, alors que du code exécuté pas souvent est interprété. Typiquement, les boucles les plus souvent exécutées sont compilées, le code exécuté une seule fois est interprété. ===Les langages intermédiaires=== Les compilateurs modernes passent par un '''langage intermédiaire''' pour faire la transformation en code machine. Le compilateur traduit le langage de haut niveau en langage intermédiaire, puis traduit le langage intermédiaire en code machine cible. Le langage intermédiaire est parfois appelé le '''''bytecode''''', et ce terme recouvre aussi le code écrit avec du ''bytecode''. L'interprétation aussi peut passer par l'intermédiaire d'un un ''bytecode'', ce n'est pas limité aux compilateurs proprement dit. {| |[[File:Intermediate representation scheme.png|centre|vignette|upright=1.5|Représentation intermédiaire d'un compilateur.]] |[[File:Code interpreter scheme.png|centre|vignette|upright=1.5|Fonctionnement d'un interpréteur.]] |} Faire ainsi a de nombreux avantages, le principal étant d'avoir un compilateur capable de traduire un langage de haut niveau vers plusieurs jeux d'instructions différents. Cela permet d'avoir, par exemple, un compilateur qui traduit du C soit en code machine x86, soit en code machine pour un CPU ARM, soit pour un CPU POWERPC, etc. [[File:Compiler design.svg|centre|vignette|upright=3|Compiler design]] Pour l'interprétation, l'usage d'un ''bytecode'' a d'autres avantages. Pour l'exploiter, il faut distribuer non pas le code source, mais le ''bytecode''. En clair, l'interpréteur ne prend pas en entrée le code source, mais du ''bytecode'' déjà compilé. La traduction est alors beaucoup plus simple, car le code source a déjà été partiellement compilé, par la traduction en ''bytecode''. Interpréter du code source est en effet assez compliqué : il faut effectuer des étapes d'analyse lexicale, sémantique, et bien d'autres. Avec du ''bytecode'', ces étapes ont été réalisées lors de la compilation du ''bytecode'', l'interpréteur a alors peut de choses à faire. L'interprétation du ''bytecode'' se fait instruction par instruction, au niveau du ''bytecode''. Une instruction du ''bytecode'' est traduite en une instruction machine équivalente, qui est exécutée. Si l'instruction est un peu complexe, il exécute une fonction/procédure qui fait la même chose. Pour résumer, le ''bytecode'' est ensuite traduit à la volée et exécuté instruction par instruction par un logiciel appelé l'interpréteur. [[File:Control table.png|centre|vignette|upright=2|Control table]] Le langage intermédiaire peut être vu comme l'assembleur d'un processeur, n'existe pas forcément dans la réalité, mais dont le jeu d'instruction est décrit en détail. Le processeur en question est appelé une '''machine abstraite''', ou encore une ''machine virtuelle''. Nous utiliserons le terme de machine abstraite dans ce qui suit. La machine abstraite n'est pas la même suivant que l'on cible la compilation ou l'interprétation/JIT. Pour l'interprétation/JIT, la machine abstraite est souvent une machine à pile, car cela simplifie grandement la traduction en code machine final. L'interprétation demande que la traduction du ''bytecode'' en code machine soit la plus simple possible. Et cela demande de prendre en compte pas mal de détails. Par exemple, les différents jeux d'instruction existants n'ont pas le même nombre de registres, ce qui pose problème lors de la traduction du ''bytecode''. Lors de la transformation en code machine, un algorithme d'allocation de registres se débrouille pour traduire le code intermédiaire en code qui utilise un nombre limité de registres. Pour améliorer cette allocation de registres, il y a deux solutions niveau ''bytecode'' : soit utiliser un nombre illimité de registres, soit utiliser une machine à pile. Pour les machines à pile, il existe un algorithme simple et rapide pour traduire un code écrit pour une machine à pile en un code écrit pour un processeur avec des registres, qui se débrouille pas trop mal pour allouer efficacement les registres. C'est un avantage assez important pour les langages interprétés. ==Les processeurs qui exécutent du bytecode== Le ''bytecode'' est un code machine, ce qui signifie qu'il peut en théorie s'exécuter sur un processeur qui implémente le jeu d’instruction associé. Si le jeu d'instruction d'un ''bytecode'' est souvent une description censée être fictive, elle n'en reste pas moins un jeu d'instruction et des caractéristiques précises, qu'on peut l'implémenter en matériel ! Néanmoins, tous les ''bytecode'' ne sont pas égaux de ce point de vue. Certains sont faciles à implémenter en matériel, d'autres non. Les ''bytecodes'' implémentés en matériel sont ceux dont les machines abstraites sont des machines à pile. Un premier exemple est celui des processeurs Pascal MicroEngine, qui exécutaient directement le bytecode du langage Pascal, le fameux UCSD P-code. Un second exemple est la '''machine SECD''', qui sert de langage intermédiaire pour certains compilateurs de langages fonctionnels. Elle a été implémentée en matériel par plusieurs équipes, notamment par les chercheurs de l'université de Calgary en 1989. Dans le même genre, quelques processeurs simples étaient capables d’exécuter directement le ''bytecode'' du langage FORTH. Le FORTH, un des premiers langages à pile de haut niveau, possède de nombreuses implémentations hardware et est un des rares langages de haut niveau dont le ''bytecode'' a été utilisé comme langage machine sur certains processeurs. Par exemple, on peut citer le processeur FC16, capable d’exécuter nativement du ''bytecode'' FORTH. ===Les processeurs Java=== Le cas le plus impressionnant est celui de la machine virtuelle Java, qui est un design de processeur comme un autre. En temps normal, le ''bytecode'' Java est compilé ou interprété, mais certains processeurs exécutaient du ''bytecode'' Java directement, sans interprétation ni compilation. Ils sont appelés des '''processeurs Java'''. On peut citer les processeurs ARM disposant de l'extension Jazelle, du JEMCore et du aJ-100 de aJile Systems, le picoJava II de Sun, et quelques autres. Il y a aussi eu quelques projets de recherche ou processeurs open source, comme Komodo, jamuth, le ''Java Optimized Processor'' et quelques autres projets du même genre. L'implémentation de la machine virtuelle Java n'était cependant pas complète. Quelques instructions complexes n'étaient pas gérées par le processeur et devaient être émulées en logiciel. Mais la grosse majorité des instructions l'était. Les instructions non-supportées étaient émulées avec la méthode ''trap and emulate'', à savoir qu'un opcode inconnu déclenchait une exception matérielle ''opcode invalide'', dont la routine pouvait émuler les instructions qui doivent l'être. Le processeur aJ-100 de aJile est capable d'exécuter la quasi-totalité des instructions de la JVM Java, seules deux instructions faisant exception. Les '''accélérateurs Java''' sont une solution intermédiaire, qui permet de traduire à la volée du ''bytecode'' Java en instructions RISC, exécutées sur un autre processeur. Il s'agit formellement de traduction binaire, mais je préfère en parler ici. L'idée est de combiner un processeur RISC avec un coprocesseur Java. Le coprocesseur lit le ''bytecode'' Java et le traduit en instructions RISC, ces dernières étant exécutées sur le processeur RISC. Le JA108 de Nozomi était un coprocesseur de ce genre, au même titre que le coprocesseur JSTAR de JEDI. L'extension Jazelle de certains CPU ARM fonctionnait vraisemblablement sur un principe similaire, sauf que le coprocesseur était intégré dans le processeur ARM, entre le cache d'instruction et le décodeur d'instructions ARM. L'intérêt des processeurs Java n'est pas qu'une question de performance. Certes, ils permettent d’exécuter plus vite les programmes compilés en ''bytecode'', comme des programmes Java pour la JVM Java. Mais l'intérêt est aussi de faire des économies de mémoire RAM et ROM. Je rappelle que la machine virtuelle Java est une machine à pile, ce qui fait que sa densité de code est excellente. Les programmes en ''bytecode'' Java sont donc très petits, et prennent peu de place en mémoire ROM. De plus, utiliser un processeur Java permet de se passer d'interpréteur ou de JIT Java, ce qui économise encore plus de mémoire. ===Transformer un processeur RISC en machine à pile=== La plupart des processeurs Java utilisent en interne une architecture à registre généraux, qui émule une machine à registre en microcode. Quelques registres sont réservés pour l'état de la machine virtuelle, avec au minimum un registre réservé pour le pointeur de pile, un autre pour un pointeur vers un ''pool'' de constantes, et quelques autres. D'autres registres servent à mémoriser le haut de la pile, à savoir le sommet de la pile et quelques opérandes situées juste en-dessous. Par exemple, le processeur picoJava II gardait les 64 opérandes au sommet de la pile dans 64 registres généraux, séparés des registres pour les pointeurs et l'état de la JVM. Une optimisation possible, envisagée sur les processeurs picoJava, était une sorte de macro-fusion sous stéroïde. L'idée était de fusionner certaines séries d'instructions Java en une seule instruction machine. Typiquement, pour faire une opération arithmétique, une machine à pile empile deux opérandes et exécute une opération, ce qui prend trois instructions machine. La spécification picoJava 1 a proposé d'envoyer directement la seconde opérande en entrée de l'ALU, ce qui fusionnait l'empilement de la seconde opérande et l'opération arithmétique. Le processeur picoJava II va plus loin et fusionne les trois instructions en une seule instruction RISC, agissant sur des registres. Mais la majorité des processeurs Java ne faisaient pas cela, car cela complexifie grandement le décodeur. Les processeurs ''bigfoot'' de DCT utilisaient un système différent pour émuler une pile avec des registres. Leur banc de registre de 64 bits était coupé en deux : les 16 premiers registres étaient utilisés pour la pile, les 48 suivants étaient utilisés pour autre chose. L'idée est que le registre 0 mémorise un emplacement vide, le registre 1 contient le sommet de la pile, le registre 2 l'opérande sous le sommet de la pile, et ainsi de suite. Les 15 premières opérandes de la pile sont donc mémorisées dans les registres. Les instructions écrivent leur résultat dans le registre adéquat, puis tous les registres sont décalés pour que le résultat passe dans le registre R1. Par exemple, les instructions PUSH chargent une opérande dans le registre 0, puis tous les registres sont décalés de un rang, pour que l'opérande chargée soit dans le registre 1. Le registre 0 devient alors le registre numéro 1, le registre 1 devient le 2, et ainsi de suite. Par contre, les opérations arithmétiques font autrement. Une instruction d'addition, par exemple, lit les opérandes dans le registre 1 et 2, puis place son résultat dans le registre 2 (les deux opérandes sont dépilées). Puis, les registres sont décalés d'un rang pour que ce registre 2 devienne le 1. Les registres ne sont en réalité pas décalés, il n'y a pas de transferts entre registres. A la place, les 16 premiers registres utilisaient une forme limitée de renommage de registres, similaire au banc de registre tournant utilisé sur les processeurs EPIC comme l'Itanium. Les numéros de registres sont renommés, décalés, à chaque fois qu'on empile ou dépile une opérande. Pour faire le renommage de registres, processeur contient un ''register stack counter'' de 4 bits, qui est incrémenté à chaque fois qu'on empile une donnée, et est décrémenté quand on dépile une donnée. Quand on veut accéder à un opérande, le numéro de registre architectural associé est traduit en numéro de registre physique, en additionnant le ''register stack counter'' (modulo 16, pour rester sur 4 bits). ==L'accélération de l'interprétation/JIT : le ''Thumb-EE'' d'ARM== Quelques processeurs ont ajouté des instructions pour faciliter le travail des interpréteurs/JIT. Par exemple, en 2005, ARM a ajouté le mode ''Thumb Execution Environment'', qui faisait cela. Il reprenait le jeu d'instruction compact ''thumb'' et ajoutait quelques instructions et modifiait le comportement d'instructions existantes. Le jeu d'instruction ''Thumb-EE'' a été ajouté sur les CPU ARM en 2005, mais a été déprécié en 2011, par manque d'utilité. Mais la tentative mérite qu'on s'attarde dessus. Les CPU de gamme M, déstinés à l'embarqué, n'ont jamais supporté ''Thumb-EE'', vu que de tels processeurs ne sont pas conçus pour exécuter du code interprété/JIT. Le mode ''Thumb-EE'' est un mode d'exécution, séparé de l'ARM normal. Ainsi, une instruction ''Thumb-EE'' et une instruction ARM peuvent avoir le même encodage en binaire, mais se comporter différemment. Le processeur est à tout moment soit en mode ''Thumb-EE'', soit en mode ARM normal, ce qui précise comment ces instructions doivent se comporter. Des instructions ont été ajoutées par ''Thumb-EE'', pour qu'il fasse son travail. L'entrée et la sortie du mode ''Thumb-EE'' se fait avec deux instructions : ENTERX et LEAVEX. Une fois en mode ''Thumb-EE'', le décodage des instructions ''thumb'' se fait avec les règles du ''Thumb-EE'', et non celles du ''thumb'' normal. Une première différence entre ''thumb'' normal et ''Thumb-EE'' est l'ajout de ce qui s'appelle un ''null check'' pour les instructions mémoire. Le ''null check'' est utilisé pour les instructions en mode d'adressage "base + indice" ou "Base + décalage", ou tout autre mode d'adressage avec un registre de base utilisés dans des calculs d'adresse. L'idée est de vérifier si l'adresse de base vaut zéro ou non. Si c'est le cas, le processeur lève une exception matérielle, qui est traitée par l'interprétation ou le compilateur JIT. Une telle situation est en effet signe d'une erreur d'adressage mémoire, qui doit être traitée par l'interpréteur. Typiquement, c'est signe qu'un pointeur n'a pas bien été initialisé, ce qui peut arriver avec du code interprété ou JIT. Les interpréteurs/JIT traitent généralement la situation en ajoutant un test avant toute instruction mémoire, pour vérifier si le pointeur accédé vaut zéro ou non. Avec ''Thumb-EE'', pas besoin d'ajouter les tests en questions : ils sont réalisés automatiquement lors de chaque instruction mémoire. : Les registres pour la pile, comme le pointeur de pile et le pointeur de ''frame'', sont aussi concernés. L'instruction CHKA vérifie que les accès à un tableau ne débordent pas en dehors du tableau en question. Par exemple, pour un tableau de 1024 éléments, elle vérifie si l'indice est compris entre 0 et 1024. Pour cela, elle vérifie si l'indice de tableau est bien dans l'intervalle adéquat et lève une exception matérielle si ce n'est pas le cas. Son utilité se comprend quand on sait que tous les accès à un tableau sont vérifiés par l'interpréteur. L'interpréteur est censé ajouter des instructions pour vérifier les indices, à savoir deux ou trois branchements. Avec CHKA, le test se fait en une seule instruction. D'autres modifications mineures de l'encodage des instructions sont aussi présentes, ainsi que des modifications pour les instructions LOAD/STORE. Précisément, dans les modes "base + indice" et "base + décalage", le décalage et l'indice subissent maintenant des décalages. L'idée est de simplifier le calcul d'adresse. De quoi économiser quelques instructions lors des calculs d'adresse, ce qui facilite le travail de l'interpréteur/JIT. Et des instructions LOAD/STORE précises voient leur comportement modifié pour faciliter la gestion de la pile de Java ou l'exécution des méthodes locales. Pour faciliter le travail de l'interpréteur, deux instructions HBP et HBLP ont été ajoutées. Elles branchent vers une fonction qui gère les exceptions logicielles (une fonctionnalité présente dans de nombreux langages, comme Java). La première effectue un branchement vers la fonction en question, la seconde sauvegarde l'adresse de retour avant de faire ce branchement. Les deux peuvent brancher vers 256 fonctions pré-déterminées, sans avoir à présenter leur adresse. ==La traduction binaire accélérée par le matériel== Plus haut, nous avons surtout parlé des compilateurs et des interpréteurs. Cependant, nous devons aussi parler de la '''traduction binaire'''. Elle traduit un programme écrit dans un code machine vers un autre code machine. Par exemple, elle traduit un programme compilé pour un CPU x86 vers un code machine ARM. En général, le système d'exploitation est généralement compilé pour le jeu d'instruction natif, mais il exécute des applications prévues pour le x86. Les applications sont traduites par le système d'exploitation, avant d'exécuter le code traduit. : Nous parlerons dans la suite de code machine source et de code machine cible, pour parler respectivement du code à traduire et du code obtenu après traduction. Idem avec d'autres termes comme architecture cible/source, ou jeu d'instruction cible/source. La traduction binaire est surtout utilisée pour des questions d'émulation ou de compatibilité. Par exemple, lorsque les Macintosh sont passés de processeurs Power PC vers des processeurs x86, le système d'exploitation Mac OS utilisait la traduction binaire pour convertir les anciennes applications Power PC vers du code x86. Les utilisateurs n'y ont vu que du feu. Le système de traduction binaire était appelé Rosetta 1. Par la suite, lors de la transition de processeurs x86 vers des processeurs Apple, Rosetta 2 a vu le jour. La traduction binaire a surtout été utilisée pour traduire du code x86 vers un autre jeu d'instruction. Il faut dire que le x86 est le jeu d'instruction dominant. De nombreuses entreprises ont eu pour ambition de briser l'hégémonie du x86 sur PC, en remplaçant le x86 par un jeu d'instruction plus performant, tout en gardant une compatibilité maximale. La traduction binaire était la seule solution pratique. Elle était le plus souvent intégralement réalisée en logiciel, comme c'était le cas sur les architectures Itanium avec le ''IA-32 Execution Layer''. ===Les généralités sur la traduction binaire assistée en matériel=== L'architecture source est presque tout le temps une architecture CISC, assez ancienne, qu'on souhaite émuler. Le choix de l'architecture cible se porte souvent sur une architecture VLIW, fort différente de l'architecture source, et ce pour obtenir une économie de matériel conséquente. Pour s'exécuter rapidement, le code traduit doit exploiter le parallélisme d'instruction, à savoir exécuter plusieurs instructions en même temps dans des unités de calcul séparées. Il est possible d'utiliser un processeur superscalaire avec exécution dans le désordre pour cela, mais au prix d'un cout important en transistors. Alors qu'en utilisant un CPU VLIW, c'est le traducteur binaire qui fait tout le travail d'extraction du parallélisme d'instruction. Et après tout, quitte à avoir un traducteur binaire, autant lui refiler le boulot d'optimisation. Compiler du code à la volée est certes assez lourd, mais qu'il existe des algorithmes efficaces pour regrouper des instructions indépendantes dans une seule instruction VLIW. Au passage, le processeur VLIW a plus de registres que le processeur source. Cela permet de faire du renommage de registres directement en logiciel. Le traducteur binaire n'hésite pas à changer les noms de registres entre instructions source et instruction VLIW cible, afin de supprimer des dépendances de données. En conséquence, cela ouvre des opportunités de parallélisme, qui sont exploitées lors du regroupement des opérations en instructions VLIW. Vous pourriez penser que le choix d'un CPU VLIW pour émuler du CISC est tout sauf optimal. Vous devez penser que la traduction binaire est d'autant plus simple que l'architecture source et cibles sont semblables. Dans les faits, ce n'est pas tellement traduire les instructions qui pose problème, mais plus la gestion du registre d'état, des exceptions matérielles, de la mémoire virtuelle, la différence entre gros-boutisme et petit-boutisme, de même que des différences d'adressage pour les périphériques. Le premier problème est la gestion des conditions, notamment en présence d'un registre d'état. En pratique, la traduction binaire s'utilise pour traduire du code CISC vers du code RISC ou VLIW. Les architectures source ont donc un registre d'état, qui est mis à jour non seulement par des instructions de test, mais aussi des instructions arithmétiques. L'architecture cible n'a elle pas de registre d'état, mais des registres à prédicats. La traduction de l'un vers l'autre est alors quelque peu compliquée. Et elle est d'autant plus compliquée que le registre d'état est la source de dépendances d'instruction implicites, qui réduisent les performances. Par exemple, si une instruction arithmétique modifie le registre d'état, cela peut impacter l'exécution d'une instruction ultérieure, qui lit ce registre d'état. Pour éliminer ces fausses dépendances, le traducteur binaire doit renommer ce registre en logiciel et détecter les dépendances utiles. Et le compilateur doit gérer les cas où il y a beaucoup de distance entre les deux, voire les cas où l'instruction dépendante n'a pas encore été analysée par le compilateur. Un autre problème est lié à la gestion des exceptions matérielles, et précisément des exceptions précises. Pour rappel, une instruction VLIW regroupe plusieurs opérations, opérations qui correspondraient à une instruction machine sur un CPU pas VLIW. En conséquence, plusieurs instructions du langage machine "source" sont regroupées en une seule instruction VLIW. Et il faut tenir compte du cas où une instruction source lève une exception. Dans le code VLIW, cela signifie qu'une opération lève une exception, et il faut annuler partiellement l'instruction VLIW associée. Et par partiellement, on veut dire que seules les opérations suivantes dans l'ordre du programme source doivent être annulées, puis ré-exécutées. Et c'est un sacré casse-tête ! Les processeurs VLIW utilisent comme solution le mécanisme d'exceptions différées des processeurs EPIC, vu il y a quelques chapitres. Pour résumer, le code est exécuté par blocs d'instruction, délimités par des branchements ou tout autre limite/barrière pertinente dans le code. Un bloc de code est compilé en deux versions : une version rapide sans exceptions matérielles, une version lente qui gère les exceptions précises. La version rapide s'exécute sans exécuter les exceptions. Cependant, les exceptions matérielles sont enregistrées, pour être prises en compte à la toute fin du bloc de code. La version rapide est exécutée en premier et elle mémorise si une exception matérielle a eu lieu. Puis une instruction vérifie si une exception a eu lieu et décide quoi faire. Si aucune exception n'a eu lieu, elle passe à la suite du programme, les résultats du bloc de code sont définitivement acceptés. Mais si une exception a eu lieu, tout est annulé. Le processeur est remis dans l'état initial, puis le code est ré-exécuté instruction par instruction de manière à gérer l'exception correctement. ===La traduction par pré-décodage=== Une première solution serait de faire la traduction binaire dans le cache d'instruction. Lors d'un défaut de cache, le code chargé depuis la RAM est traduit en code machine cible. Et c'est ce code machine cible qui est mémorisé dans le cache d'instruction et exécuté par le processeur. La technique marche sur le papier et n'est qu'une amélioration des techniques de pré-décodage vues il y a quelques chapitres. Cependant, le code obtenu est une traduction assez basique, qui n'incorpore pas d'optimisations dignes de ce nom. De plus, elle gère mal le cas où la taille du code source et cible sont potentiellement très différentes. Concrètement, elle est surtout utile pour traduire du code d'un processeur RISC vers un autre processeur RISC, dont les tailles d'instructions sont similaires. Elle ne permet pas d'émuler plusieurs jeux d'instructions différents, le cout en matériel (un circuit de pré-décodage par jeu d'instruction) serait trop important. ===Le projet DAISY d'IBM=== De très rares processeurs étaient conçus pour accélérer cette traduction binaire, afin de garder de bonnes performances. Les premiers à avoir étudié l'idée étaient IBM, avec leur projet DAISY (''Dynamically Architected Instruction Set from Yorktown''). Le projet de base était de convertir à la volée du code compilé pour des CPU Power PC, vers du code VLIW. Les chercheurs d'IBM avaient développé un algorithme de traduction dynamique efficace, ainsi qu'un processeur VLIW disposant d'optimisations spécifiques à la traduction binaire dynamique, à la volée. De nombreuses idées de ce projet ont été reprises ou re-découvertes par la société Transmetta, avec ses processeurs Crusoe et Efficieon, puis par NVIDIA avec son projet Denver. Les ingénieurs de ce projet ont étudié la possibilité d'émuler plusieurs jeux d'instructions différents sur un même processeur, notamment le s390 d'IBM et le x86. En théorie, cela demande juste d'avoir plusieurs programmes de traduction binaire : un pour le x86, un autre pour le s390, éventuellement un autre pour le Power PC. Mais cela ne s'est pas concrétisé. Mais les ingénieurs ont étudié quelle pourrait être l'architecture VLIW idéale pour ça. Par exemple, il fallait des additions 3-opérandes pour simplifier les calculs d'adresse. Une difficulté était la gestion du registre d'état, dont les bits ne sont pas mis à jour de la même manière sur le x86 et le s390 ou le Power PC. La différence entre gros-boutisme et petit-boutisme était aussi un problème, de même que des différences d'adressage pour les périphériques. Pour que la traduction binaire soit efficace, le processeur VLIW intègre diverses optimisations, comme les exceptions différées et les branchements multi-voies. La principale est la suivante : seul le code des boucles ou fonctions exécutées fréquemment est traduit en code VLIW, le reste du code est interprété. En effet, traduire du code binaire prend plus de temps qu'une simple interprétation. Pour du code qui ne sera exécutée qu'une seule fois, il est plus rapide d'utiliser l'interprétation. Par contre, pour du code exécuté beaucoup de fois, le cout de la traduction binaire est amorti, dilué sur N exécutions, compensé par le gain en temps d'exécution de ce code traduit. Au final, cela permet de ne traduire que le code qui le mérite. La détection du code fréquemment exécuté est réalisée dans le cœur VLIW. L'unité de branchement mémorise les derniers branchements rencontrés et le nombre de fois qu'ils ont été exécutés. Si ils ont été exécutés un certain nombre de fois, l'unité de branchement lève une exception matérielle, qui invoque le traducteur binaire. La routine de cette exception est le traducteur binaire proprement dit. En clair, la traduction binaire est démarrée quand le processeur détecte qu'une fonction a été exécutée plus de N fois, via une exception matérielle dédiée. La mesure du nombre d'exécution d'un branchement se fait dans l'unité de calcul dédiée aux branchements, avec l'aide d'une sorte de ''branch adress buffer'' modifié. Pour rappel, le ''branch adress buffer'' mémorise le ''Program counter'' de chaque branchement récemment rencontré. Ici, chaque entrée du ''branch adress buffer'' est associée à un compteur incrémenté à chaque exécution du branchement. ===Les processeurs Crusoe et Efficieon de Transmetta=== Les processeurs Crusoe Et Efficeron sont deux processeurs VLIW produits par la société Transmetta, une société californienne rachetée par NVIDIA. Ils étaient conçus pour exécuter spécifiquement des programmes x86, système d'exploitation inclus. L'idée derrière ce projet était d'exécuter du code x86 sans que les applications, ni même le système d'exploitation et le BIOS soient au courant ! Les processeurs Transmetta étaient en réalité conçus pour exécuter un programme unique, le ''Code Morphing Software'' (CMS), qui traduisait le code x86 en code VLIW. Transmetta ne voulait pas que son processeur VLIW soit exploité directement, sans le CMS. Il n'avait pas rendu public de compilateur pour traduire du C vers du code VLIW, il n'avait pas donné la documentation du jeu d'instruction VLIW. Des efforts de rétro-ingénieurie, documentés sur le site realworldtech, ont cependant permis de comprendre comment étaient encodées les instructions du processeur Crusoe. Comme pour le projet Daisy, le ''Code Morphing Software'' utilisait à la fois interprétation et traduction binaire, selon les besoins. Les instructions sont interprétées lors de leurs premières exécutions, mais le CMS bascule sur de la traduction en code VLIW après un certain nombre d'exécution. Ainsi, les boucles souvent exécutées sont traduites en code VLIW, alors que le reste du code est interprété. Le choix du code à traduire est le fait du CMS, il utilise des heuristiques complexes pour, qui ne sont pas connues dans le détail. Le ''Code Morphing Software'' est mémorisé dans une EEPROM, ce qui en fait un ''firmware'' situé en dessous du BIOS. Le CMS démarre ensuite le BIOS, qui lui-même démarre le système d'exploitation, qui lui-même démarre les pilotes de périphériques et les programmes. Le CMS se réserve les 16 premiers mébioctets de l'espace d'adressage. L'EEPROM du CMS est mappée dedans, mais n'en utilise que la moitié. Le reste est utilisé comme cache, pour mémoriser le code VLIW traduit par le CMS. Les deux processeurs intégraient aussi deux mémoires SRAM, utilisées par le CMS, appelées la ''local program memory'' (LPM) et la ''local data memory'' (LDM). La première contient du code qui gére les interruptions, la mémoire virtuelle, les exceptions matérielles, les problèmes d'alignement mémoire, et quelques fonctions très fréquemment utilisées par le CMS. La seconde est de la mémoire RAM utilisée par ces fonctions de la ''local program memory''. Les mémoires caches L1 et L2 sont séparées de ces deux mémoires. Le processeur Crusoe et Efficieon étaient des processeurs VLIW très simples. Ils n'avaient même pas de MMU, ni de fonctionnalités importantes sur les CPU x86. Pour Crusoe, les instructions VLIW étaient encodées sur 64 ou 128 bits, et regroupaient entre 2 à 4 opérations. Les instructions LOAD/STORE ne supportaient pas d'adressage indicé, juste de l'adressage indirect à registre. Crusoe avait 5 unités de calcul : deux ALU entières, une unité LOAD/STORE, une unité de branchement, une FPU. Efficieon doublait le nombre d'ALU entière et d'unité LOAD/STORE, les instructions VLIW passaient à 256 bits. Le processeur Crusoe contient 160 registres, dont 64 registres généraux, 32 registres flottants. Les registres généraux font 32 bits, ce qui est cohérent avec le fait que les CPU x86 de l'époque étaient des processeurs 32 bits. Quant aux registres flottants, ils faisaient 80 bits, ce qui colle avec la taille des registres flottants de la FPU x87 utilisée à l'époque. Sur les 64 registres généraux, seuls 48 étaient réellement utilisable pour mémoriser des opérandes. Une partie des registres généraux étaient utilisés pour la gestion de la pile, d'autres pour mémoriser l'état du CPU x86 émulé, un registre était un registre zéro non accesible en écriture. La gestion des exceptions est optimisée avec un système d'exceptions différées, le même que celui décrit plus haut. Le processeur utilise un système similaire, pour effectuer des lectures anticipées, qui a été expliqué dans le chapitre sur les processeurs VLIW/EPIC. Sauf qu'il s'agit là d'une optimisation pour gagner en performances en général, pas une aide utile pour gérer des exceptions précises ou toute autre subtilité de la traduction binaire. Pour gérer les exceptions différées et les lectures anticipées, le processeur mémorise l'état du processeur avant de démarrer un bloc de code. Pour cela, le processeur contient des ''shadow registers'', 48 registres entiers et 16 registres flottants. Vous remarquerez qu'il y a autant de registres entiers que de ''shadow registers'' entiers. Les registres entiers/flottants sont mémorisés dans les ''shadow registers'' avant d'exécuter un bloc de code, afin que le processeur puisse revenir à l'état de base. Une autre fonctionnalité liée est le ''gated store buffer''. Le principe est d'éviter toute écriture en mémoire RAM, tant que le code VLIW n'a pas émis d'instruction ''commit''. L'idée est que les écritures sont conservées dans la ''store queue'' de l'unité LOAD/STORE. Pour rappel, la ''store queue'' met en attente les écritures soit tant que les écritures ne sont pas terminées, soit tant que la RAM est occupée. L'idée est que les écritures sont accumulées dans la ''store queue'' tant que l'instruction ''commit'' n'a pas eu lieu. Le ''store queue'' est "déversé dans la RAM" seulement quand l'instruction ''commit'' s'exécute. ===Les CPU NVIDIA Denver=== Nvidia a tenté quelque chose de similaire avec son projet Denver. NVIDIA a racheté l'entreprise Transmetta, ses brevets, et a tenté de refaire la même chose. Le projet initial était de traduire du code x86 vers un jeu d'instruction VLIW propriétaire, comme l'a fait Transmetta. Mais NVIDIA n'a pas réussi à acquérir la licence du jeu d'instruction x86 et s'est rabattu sur le jeu d'instruction ARM. Le processeur né de ce projet est le Tegra K1-64 CPU. Il s'agit d'un processeur composé d'un cœur VLIW, couplé à deux décodeurs d'instructions ARM. Les deux décodeurs ARM traduisent une paire d'instructions ARM consécutives en instructions VLIW, pour les exécuter sur le cœur VLIW. Le CPU peut fonctionner selon deux modes : ARM et VLIW. En somme, le processeur supporte deux jeux d'instruction : le jeu d'instruction ARM, et un jeu d'instruction VLIW. En mode ARM, les instructions ARM sont chargées depuis le cache, traduites en VLIW par les décodeurs ARM, puis exécutées par le cœur VLIW. En mode optimisé, le CPU exécute des instructions VLIW chargées depuis le cache d'instruction, les décodeurs ne sont pas utilisés. : Le CPU supporte en réalité deux jeux d'instruction ARM : ARM8 et 7. Mais ils correspondent au même mode pour le CPU. Un point important est que le processeur peut changer très rapidement de mode, en à peine quelques cycles d'horloges. On n'est clairement pas dans le cas des CPU x86, qui mettent des plombes pour passer du mode 32 à 64 bits et inversement. La commutation est tellement rapide qu'on peut considérer que le processeur supporte deux jeux d'instruction simultanément. Et le support simultané de deux jeux d'instruction facilite l'implémentation de la traduction binaire. Encore une fois, inutile de traduire du code qui ne sera exécutée qu'une seule fois, mieux vaut l'exécuter directement dans le mode ARM. Surtout que le processeur supporte un mode ARM qui permet d'exécuter du code ARM sans perte de performance. Par contre, le code fréquemment utilisé, notamment dans des boucles critiques, est traduit en VLIW. Au final, les performances sont optimisées, en limitant le travail du logiciel traducteur binaire. La détection du code fréquemment exécuté se fait différemment que pour les processeurs Transmetta et Daisy. L'unité de branchement ne détecte pas directement le code beaucoup exécuté, même si elle a un rôle à jouer. A la place, la détection est réalisée en logiciel, par un ''thread'' dédié, qui s'exécute sur un cœur séparé. Il détecte le code fréquemment utilisé en regardent les branchements exécutés récemment. Pour cela, le processeur mémorise l'historique des branchements pris récemment et copie cette historique dans une mémoire tampon dédiée, partagée entre tous les cœurs. Vu qu'elle est partagée, le ''thread'' de détection a accès à l'historique sans pertes de performances, seule la copie de l'historique dans ce tampon a un cout en performance, pas son partage. Le code VLIW obtenu après traduction binaire est mémorisé dans la mémoire RAM, dans une portion spécialement réservée pour. Elle fait 128 mébioctets, et est appelée le '''cache d'optimisation''' par NVIDIA. Le cache d'optimisation est protégé en écriture et seul le traducteur binaire peut écrire dedans. Au passage, le traducteur binaire est du code VLIW, ce qui fait qu'il est placé dans le cache d'optimisation. Le processeur détecte automatiquement quand une fonction pour laquelle le code compilé équivalent est disponible. Pour cela, l'unité de chargement contient une table de correspondance entre l'adresse de la fonction ARM, et l'adresse de son équivalent compilé. Quand une fonction est appelée, l'unité de chargement regarde l'adresse de destination du branchement. Si l'adresse est dans cette table, elle récupère l'adresse de la fonction compilée et branche vers celle-ci. Le processeur dispose d'un cache d'instruction de 128 kibioctets, ce qui est très important, mais nécessaire vu la taille des instructions VLIW. Le coeur VLIW dispose de ses propres décodeurs, d'un ''scoreboard'', de 7 unités de calcul et de registres. Pour les unités de calcul, il a deux FPU, deux ALU, une unité de branchement, et deux unités LOAD-STORE faisant aussi ALU. Un point important est que le CPU dispose de 64 registres entiers et de 64 registres flottants. C'est deux fois plus que supporte le jeu d'instruction ARM. En mode ARM, seule la moitié des registres est utilisée. Mais en mode optimisé, le traducteur binaire utilise bien les 64 registres, grâce à une sorte de renommage de registres logicielle. Pour simplifier la traduction binaire, le processeur supporte les techniques vues précédemment. Le processeur intègre un ''gated store buffer'' similaire à celui des processeurs Transmetta. Les techniques d'exceptions différées sont aussi supportées, comme sur les processeurs Transmetta. Il a aussi une unité de préchargement décrite par NVIDIA comme agressive, avec support du préchargement de type anticipé (''runahead''). ===L'émulation de la mémoire virtuelle=== Pour finir, il faut parler de la mémoire virtuelle avec la traduction binaire. Sur le processeur source, le processeur gère à la fois des adresses physiques et virtuelles et sa MMU fait la conversion entre les deux. Sur le processeur VLIW, la MMU est simulée par le programme de traduction binaire, partiellement ou totalement. L'implémentation exacte varie suivant que l'on parle du projet Daisy, des processeurs Transmetta ou de Denver. Sur les processeurs Transmetta, il n'y a pas de MMU ni de mémoire virtuelle. A la place, le programme de traduction binaire émule la mémoire virtuelle du processeur source. Le processeur VLIW ne gère que des adresses physiques, rien d'autre. L'espace d'adressage physique du processeur VLIW a la même taille que celui du processeur émulé, ici des processeurs x86. Il est vraisemblable que les adresses physiques utilisées par le processeur x86 sont les mêmes que celles du processeur VLIW. Sur le projet Daisy, le processeur VLIW gère la mémoire virtuelle via pagination, comme les processeurs émulés. Ce qui fait qu'il gère un espace d'adressage virtuel et un espace d'adressage physique. Pour éviter toute confusion, nous parlerons d'adresse physique/virtuelle VLIW pour les adresses physiques/virtuelles du processeur VLIW, d'adresse virtuelle/physique source pour celle du jeu d'instruction traduit, à savoir du Power PC ou du s390, éventuellement de l'x86. Il faut alors faire le lien entre adresses physiques source et adresses virtuelles VLIW. Pour cela, rien de plus simple : il y a correspondance parfaite. L'adresse physique source numéro N correspond à l'adresse virtuelle VLIW numéro N. Cependant, cela signifie que tout l'espace d'adressage virtuel VLIW serait utilisé par le code à traduire. En réalité, il faut ajouter de la place pour le code traduit et le programme de traduction binaire. La conséquence est que l'espace d'adressage virtuel VLIW est plus large que l'espace d'adressage source. L'espace d'adressage virtuel VLIW est découpé en trois sections : une section pour le code Power PC à traduire, une autre pour le code traduit en VLIW, et une dernière réservée au traducteur binaire. Le programme de traduction est placé dans une ROM mappée en mémoire dans la seconde section. Le reste de la seconde section est réservé à une mémoire RAM utilisée par le programme de traduction. [[File:Mémoire virtuelle sur le projet Daisy.png|centre|vignette|upright=1.5|Mémoire virtuelle sur le projet Daisy]] Il faut noter que chaque page physique source, une fois traduite, correspond à N pages virtuelles VLIW. La raison est que le code traduit prend plus de place que le code machine originel. En conséquence, on doit utiliser une page finale plus grande. Pour le reste, des pages contiguës en mémoire physique source sont elles aussi contiguës en mémoire virtuelle VLIW. Le calcul d'adresse est donc simple : l'adresse physique source est multipliée par N, puis on ajoute l'adresse de base à laquelle commence la section pour le code traduit. ==La traduction binaire sur le Burroughs B1700== Nous venons de voir comment accélérer en matériel la traduction binaire, avec des techniques bien spécifiques. Le Burroughs B1700 a procédé autrement et il est intéressant d'étudier en détail son architecture. Son architecture est décrite dans le livre "Interpreting Machines : Architecture and Programming of the Bl700/Bl800 Series", écrit par Elliott I. Organick et James A. Hinds. Une partie de ce qui vfa suivre est un très court résumé de ce livre. ===Le Burroughs B1700 : le mal-nommé ''split-level control store''=== Le Burroughs B1700 avait plusieurs modèles : le B1726, le B1710 et le B1800. Ils se distinguaient par la manière dont le code machine source était traduit par le traducteur binaire. Dans ce qui suit, nous parlerons de traducteur binaire pour parler soit d'un interpréteur pour un langage de programmation de haut niveau, soit pour un logiciel de traduction binaire. Le B1710 plaquait le traducteur binaire en mémoire RAM. Le B1800 faisait pareil, mais avait ajouté un cache dédié au code du traducteur binaire, afin d'améliorer les performances. Le B1726 avait un traducteur binaire scindé en deux : une partie était en mémoire RAM, l'autre était dans un ''local store'' dédié. Le processeur peut exécuter le traducteur binaire soit depuis le ''local store'', soit depuis la mémoire RAM avec une pénalité en termes de performances. La documentation appelle cela un '''''split-level control store''''', mais le terme est trompeur : la documentation qualifie de microcode le langage machine du Burroughs B1700, ce qui est à l'origine de beaucoup de confusions. Une autre source de confusion est que ce langage machine a quelques propriétés qu'on pourrait attendre d'un microcode, mais certaines sont manquantes. Par exemple, il peut adresser les registres d’interfaçage mémoire, mais c'est parce que ce sont des registres architecturaux sur ce processeur. Ses instructions n'encodent pas de signaux de commande, par contre, elles font 16 bits et adressent les opérandes de manière implicite. Le ''local store'' mémorisait au maximum 2048 instructions machine, ce qui fait que les 2048 premières "adresses" correspondaient au ''local store''. Les suivantes étaient des adresses de mémoire RAM qui contenaient le reste du traducteur binaire. En pratique, l'adresse qui séparait les deux était configurable ! On pouvait limiter la taille du ''local store'' à un multiple de 32 instructions ! Les adresses restantes étaient alors réattribuées à la RAM. Par exemple, on pouvait attribuer 64 adresses au ''local store'', les 2048 - 64 adresses restantes étaient alors attribuées à la mémoire RAM. Un registre limite, nommé TDPM, indiquait quelle était l'adresse de démarcation entre les deux. Il contenait 4 bits, dont la valeur était multipliée par 32, pour obtenir l'adresse limite. Le programmeur devait découper le traducteur binaire en segments, certains étant placé dans le ''local store'', les autres étant en mémoire RAM. Mieux que ça, le processeur autorisait d'utiliser la technique de l'''overlaying'' pour le microcode ! Il y avait même une fonction OVERLAY dédiée pour ! OVERLAY avait besoin de trois opérandes : la taille du segment à copier, son adresse source en mémoire RAM, son adresse de destination dans la micro-SRAM. L'interpréteur réservait un "segment" en mémoire RAM, pour les données, dont le processeur connaissait la position. En-dehors de ce segment, il n'y a que des instructions machines à traduire, et le traducteur binaire. Le CPU incorporait pour cela deux registres, un pour l'adresse de base du segment de données, un autre pour son adresse de fin. Il incorporait une protection mémoire limitée sur ce segment, à savoir que seul l'interpréteur pouvait lire ou écrire dans ce segment. ===Une architecture bit-adressable=== Le CPU Burroughs B1700 était '''bit-adressable'''. En clair, il pouvait adresser la mémoire bit par bit, si nécessaire. Un avantage est que cela facilitait la traduction binaire des instructions machine, mais nous détaillerons cela plus bas. Un autre avantage est que cela permettait d'émuler des architectures dont la taille des registres/''byte''/mots était très différente. Et cet avantage mérite quelques explications immédiates. Le processeur était un CPU 24 bits, mais il pouvait émuler des processeurs 16 bits, 8 bits, 4 bits, 1 bit, 9 bits, ou toute autre valeur. Pour cela, le processeur lisait des mots de 24 bits et utilisant un ''barrel shifter'' pour sélectionner les bits adéquats. Par exemple, pour un accès de 16 bits, la donnée lue en mémoire était masquée de manière à ne garder que les 16 bits de poids faible. Le ''barrel shifter''/circuit de masquage était placé directement avant le bus mémoire, sur le trajet des données/instructions. Le ''barrel shifter'' était commandé par un registre ''Field Unit'', qui précisait quelle était la taille des données à charger. Pour communiquer avec la mémoire, il avait trois registres séparés : READ, WRITE, MAR. READ contient la donnée lue lors d'une lecture, WRITE est pour une donnée à écrire lors d’une écriture, MAR contient l'adresse à lire ou en cours de lecture. Il s'agit de registres d’interfaçage mémoire. Le fait que ces registres d’interfaçage mémoire et IO soit adressable est peu commun, et c'est plus quelque chose qu'on attend d'un microcode que d'instructions machines. Mais il faut bien comprendre que les registres d’interfaçage mémoire ont été promus au rang de registres architecturaux, sur ce processeur. De même, pour communiquer avec les entrées-sorties, il disposait de deux registres CMND et DATA : CMND pour envoyer une commande à une entrée-sortie, DATA pour lire ou écrire une donnée. Il y avait aussi un registre U, qui mémorisait une donnée lue depuis le lecteur de cassette. ===L'ALU de taille variable=== Ils avaient une '''ALU de taille variable'''. En clair, l'ALU pouvait faire des calculs sur un nombre de bits compris entre 0 et 24. Pour cela, les calculs étaient fait avec une ALU de 24 bits, puis les bits de poids fort inutiles étaient masqués. L'ALU de 24 bits pouvait masquer les bits de poids fort du résultat, mais avec des limitations. Elle gérait des opérandes et résultats de 0, 4, 8, 12, 16, 20 et 24 bits. Pour avoir un réglage plus fin, une ALU 4 bits séparée permettait de corriger le résultat. Les registres X et Y étaient reliés à une ALU de 24 bits capable d'effectuer des opérations arithmétiques basiques. Fait étonnant, l'ALU faisait tous les calculs en même temps et fournissait ses résultats dans 7 registres : un registre SUM pour l'addition, un registre DIFFERENCE pour la soustraction, et un registre par opération bit à bit. Les registres pour les opérations bit à bit sont : CMPX et CMPY pour l'opération NOT sur chaque opérande, XANY pour le ET bit à bit, XEOY pour le XOR, XORY pour le OU logique. Les additions et soustractions pouvaient se faire sur des opérandes codés en binaire ou en BCD, suivant le code opération utilisé. A cela, il fallait ajouter deux registres de "résultat" MSKX et MSKY. Ils fournissaient un masque dépendant de la taille du résultat en bits. C'est ce registre qui permettait de gérer des données de 0, 4, 8, 12, 16, 20, et 24 bits, alors que l'ALU faisait 24 bits. Le masque dans ce registre pouvait servir d'opérande dans une opération de masquage ultérieure, pour corriger le résultat. L'ALU prenait en opérande les registres X et Y, mais aussi un registre CYF pour la retenue entrante. En sortie, il fournissait aussi deux retenue sortantes : une pour l'addition et une autre pour la soustraction. Elles étaient mémorisées dans les registres CYL et CYD, respectivement. L'ALU fournissait aussi 12 conditions, qui alimentait un registre d'état. Le registre d'état était segmenté en trois registres appelés XYST, XYCN et BICN. Les registres X et Y sont des registres pour les opérandes, mais il y a aussi deux autres registres de 24 bits. Il s'agit des registres T et L, qui sont des registres généraux. Les registres T et L étaient découpés en 6 sous-registres de 4 bits, nommés LA LB LC LD LE et LF pour le registre L, et TA TB TC TD TE et TF pour le registre T. Ils étaient adressables comme n'importe quel registre. Le processeur contenait de nombreux autres registres de 4 bits, dont les sous-registres de T et L. En tout, il y a 27 registres de 4 bits adressables ! Et le processeur disposait d'une ALU 4 bits pour manipuler ces registres de 4 bits ! Elle est capable de faire des MOV entre registres de 4 bits, des opérations bit à bit, et peut aussi tester si un bit vaut 0 ou 1. La dernière possibilité est très utile pour implémenter les branchements. De plus, cela permettait de faire des opérations de masquage, notamment pour appliquer un masquage supplémentaire à celui de l'ALU. Par exemple, pour gérer des opérations sur 14 bits, on effectue une opération sur 16 bits avec l'ALU, et on masque les 2 bits manquants avec l'ALU 4 bits. ===Le code machine du Burroughs B1700=== Le processeur utilise des instructions de 16 bits de long, qui sont copiées dans registre d'instruction M de 16 bits. Le ''program counter'' est appelé le registre A. Les instructions du processeur font 16 bits de long, et elles sont alignées en RAM sur 16 bits, ce qui fait que les 4 bits de poids fort de ce registre valent 0, seuls les 14 bits de poids fort sont utiles. Les adresses mentionnées plus haut, qui adressent les micro-opérations en mémoire RAM, sont des adresses de mots de 16 bits, pas les adresses de bit individuels. Fait intéressant, il était possible de faire un OU logique entre l'instruction lue et un registre du processeur, le résultat étant mémorisé dans le registre M. Cela permettait de faire du code automodifiant, pour émuler des modes d'adressage complexe. Cela permettait par exemple de remplacer une adresse dans une micro-opération. L'adresse est alors surimposée avec un OU dans le champ d'adresse, alors que le reste de l'instruction n'est pas modifié. Afin de gérer les interruptions, ces dernières étaient partiellement détectées en logiciel. Une interruption doit être prise en compte à la toute fin de l'exécution d'une instruction source, quand celle-ci a finit son travail. Mais vu qu'ici, les instructions sont émulées avec des séries d'instructions cibles, il y a un problème : il est possible d'interrompre une instruction émulée en plein milieu. Pour éviter cela, le processeur ne déclenche pas les interruptions matérielles immédiatement, elles sont mises en attente. Une instruction dédiée vérifie si une interruption est en attente, et l'exécute alors cas échéant. L'instruction en question est utilisée en dernier, dans la suite d'instruction qui émule l'instruction source. Les Burroughs B1700 intégrait une pile d'adresse de retour, capable de mémoriser 16 adresses de 24 bits. Il disposait de 4 registres généraux, nommés X, Y, T et L, tous de 24 bits. Un point important est que le registre T avait des possibilités d’extraction spécifiques. Il était possible d'extraire N bits, placés n'importe où dans ce registre, et de les copier dans un autre registre. Le CPU supportait une '''instruction EXTRACT''' pour ça. Nous verrons pourquoi cette possibilité est intéressante et ne sert que pour le registre T. ===La traduction binaire sur le Burroughs B1700=== Les instructions source peuvent faire 16 bit avec tel jeu d'instruction, 8 bits avec un autre, 32 bits sur un autre, etc. Elles peuvent aussi être de longueur fixe ou variable ! Le B1700 utilisait une architecture bit-adressable pour gérer ces contraintes. En effet, avec un CPU bit-adressable, les contraintes d'alignement des instructions disparaissent ! Dans ce qui suit, on suppose que l'instruction est composée de '''champs''' : l'opcode, les opérandes, les adresses absolues, les constantes immédiates, les numéros de registre, etc. L'idée est que l'instruction source est chargée dans le registre T, puis découpée en champs, qui sont copiés dans les registres du processeur ou envoyés au microcode. Je rappelle que le registre T est relié à un ''barrel shifter'', différent au précédent, qui permettait de masquer, décaler et extraire une donnée de ce registre. Et il y a la possibilité d'extraire une suite de bit de ce registre T, pour la copier dans un autre registre. Par exemple, prenons une instruction composée d'un opcode, d'une constante immédiate et d'une adresse immédiate. Premièrement, elle est chargée dans le registre T. Deuxièmement, la constante immédiate est extraite du registre T et copiée dans le registre X. Troisièmement, l'adresse est extraite et est copiée dans le registre d’interfaçage mémoire, une lecture est démarrée, et l'opérande lue est copiée dans le registre Y. Enfin, l'instruction équivalente à l'opcode est exécutée par le microcode. Le tout prend plusieurs instructions : une première pour lire l'opcode, une seconde pour lire la première opérande, une troisième pour lire la seconde opérande, una quatrième pour tout exécuter. Notons que suivant la taille des champs, on peut en charger plusieurs à la fois. Par exemple, imaginons qu'on ait chargé 24 bits, contenant un opcode de 8 bits et une constante immédiate de 16 bits : le registre T est découpé en un opcode et une constante, avec deux instructions EXTRACT. L'architecture est bit-adressable car elle gère des champs qui ne font pas forcément 8 ou 16 bits. Par exemple, certains jeux d'instructions ont des opcodes codés sur 6 ou 7 bits, voire 9-10 bits. Utiliser une architecture bit-adressable règle ce problème d'alignement. On peut lire un champ depuis la mémoire RAM en précisant l'adresse de son premier bit, pas besoin de gérer des accès non-alignés. Par exemple, prenons une instruction source avec un opcode de 7 bits, suivi par une adresse de 12 bits et une constante immédiate de 15 bits. On peut charger 7 + 12 bits en un premier accès, pour extraite l'opcode et l'adresse, suivi par un second accès de 15 bits pour extraire la constante immédiate. Pour charger un champ, il faut connaitre plusieurs informations : l'adresse de son premier bit, sa taille. Pour cela, le B1700 contient deux registres, nommés ''Field Adress'' (FA) et ''Field Length'' (FL), qui indiquent l'adresse du premier bit et la taille de l'instruction à charger. Le premier fait 24 bits, ce qui permet de gérer des adresses de 24 bits. N'oubliez pas que le processeur est bit-adressable, ce qui permet d'adresser 16 Mébi-bits, soit 2 mébioctets. Le registre pour la taille fait lui 16 bits, ce qui permet de gérer des instructions de 65536 bits, soit 8192 octets ! Leur nom commence par ''field'', car le processeur peut gérer des instructions de taille variable et/ou plus longues que 24 bits. Dans ce cas, le chargement de l'instruction se fait champ par champ. Pour donner un exemple, prenons une instruction composée de 20 champs de 12 bits chacun. Les champs sont chargés un par un dans le registre T. Le registre FA est alors initialisé avec l'adresse de l'instruction, il est incrémenté de 12 à chaque cycle. Le registre FL est initialisé avec la taille de l'instruction, soit 20 * 12 = 240 bits, et il est décrémenté de 16 à chaque cycle. Précisons cependant que la taille d'un champ peut être changée d'un cycle à l'autre. Par exemple, le processeur peut charger un opcode de 8 bits, l'interpréter, détecter que l'instruction demande ensuite de charger une adresse de 16 bits. Dans ce cas, le registre FL est immédiatement altéré pour charger 16 bits, au lieu des 8 bits de l'opcode immédiatement précédent. ==Les jeux d'instructions dédiés à un langage de programmation== De rares processeurs sont conçus pour un langage de programmation en particulier. On appelle ces processeurs, conçus pour des besoins particuliers, des '''processeurs dédiés'''. Par exemple, les fameux ''Burrough E-mode'' B5000/B6000/B7000 étaient spécialement conçus pour exécuter de l'ALGOL-60. Leurs cousins B2000/B3000/B4000 étaient eux conçus pour le COBOL. Des langages fonctionnels ont aussi eu droit à leurs processeurs dédiés. Le prolog en est un bel exemple, avec les superordinateurs de 5ème génération qui lui étaient dédié. On peut aussi citer les machines LISP, dédiés au langage LISP, qui datent des années 1970. Elles étaient capables d’exécuter certaines fonctions de base du langage directement dans leurs circuits : elles possédaient notamment un ''garbage collector'' câblé dans ses circuits ainsi que des instructions machines supportant un typage déterminé à l’exécution. Les processeurs dédiés ont eu leur heure de gloire au début de l'informatique, à une époque où les langages de haut niveau venaient d'être inventés. À cette époque, les compilateurs n'étaient pas performants et ne savaient pas bien optimiser le code machine. Il était alors rationnel, pour l'époque, de rapprocher le code machine cible et le langage de programmation de haut niveau. De nombreuses architectures dédiés ont ainsi été inventées, avant que les concepteurs se rendent compte des défauts de cette approche. Les défauts en question ne sont pas nombreux, mais assez simples à comprendre. Premièrement, elles sont très rapides pour un langage de programmation en particulier, mais sont assez mauvaises pour les autres, d'où un problème de "compatibilité". Ajoutons à cela que les langages de programmation peuvent évoluer, devenir de moins en moins populaires/utilisés, ce qui rend la création d'architectures généralistes plus pertinente. Enfin, les architectures dédiées sont évidemment des processeurs CISC, pour implémenter les nombreuses fonctionnalités des langages évolués. Et les défauts des CISC sont assez rédhibitoires à l'heure actuelle. À l'heure actuelle, les algorithmes des compilateurs se sont améliorés et savent nettement mieux utiliser le matériel. Ils produisent du code machine efficace, ce qui rend les architecture dédiées bien moins intéressantes. Si on ajoute les défauts de ces architectures dédiées, par étonnant que les architectures dédiées aient presque disparues. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'accélération matérielle de la virtualisation | prevText=L'accélération matérielle de la virtualisation | next=Le matériel réseau | nextText=Le matériel réseau }} </noinclude> gqh316gftskdclci75scnk2y0yaj6wb 764058 764057 2026-04-19T20:44:53Z Mewtow 31375 /* L'ALU de taille variable */ 764058 wikitext text/x-wiki De nos jours, la majorité des programmeurs programment dans des langages de haut niveau. Il est très rare pour eux d'avoir à utiliser de l'assembleur, et encore moins un langage machine. Les programmes écrits dans un langage de haut niveau sont traduits en langage machine par un logiciel appelé le compilateur. [[File:Compilation.PNG|centre|vignette|upright=2.5|Compilateur : principe.]] Il se trouve que quelques rares processeurs sont conçus pour faciliter le travail du compilateur. Et ce sont ces processeurs que nous allons voir dans ce qui suit. Ils représentent une catégorie de jeux d'instruction à part, qui n'a pas vraiment de nom. ==La compilation et l’interprétation== Un compilateur traduit donc du code source, écrit dans un langage de haut niveau, vers du langage machine. La traduction est rarement directe. En général, le compilateur traduit le code source en assembleur, qui est lui-même traduit en langage machine. L'assembleur est une représentation textuelle du langage machine alors que le code machine est du binaire exécutable par le processeur. La traduction se fait donc en deux étapes, la '''compilation''' proprement dite et l''''assemblage''', réalisées respectivement par un compilateur et un assembleur. Il faut parfois rajouter une troisième phase d'édition des liens, que nous passons volontairement sous silence. {| |[[File:Code C.png|vignette|200px|Le code source, ici en C.]] |[[File:Assem.png|vignette|250px|Le code assembleur.]] |[[File:Binary file - hello world (C programming).png|vignette|150px|Le code machine.]] |} ===L'interprétation et la compilation à la volée=== Une alternative à la compilation est l''''interprétation''', qui transforme le code source en code machine à la volée. Avec l'interprétation, le code source est passé à un logiciel appelé l''''interpréteur''', qui exécute le code source ligne de code par ligne de code. Une ligne de code est traduite en code machine, qui est exécutée, puis l'interpréteur passe à la ligne suivante. [[File:Kaantotulkkaus.png|centre|vignette|upright=3|Compilation versus interprétation.]] Un défaut de l'interprétation est son cout en performance, qui est assez important. Aussi, l'interprétation stricte a évolué vers un hybride entre interprétation et compilation. L'idée est qu'une partie peu importante du code source est interprétée, alors que le code important est compilé juste avant d'être exécuté. Pour le dire autrement, le code source est partiellement compilé à la volée, juste avant son exécution. Aussi, on parle de '''compilation à la volée'''. Le terme en anglais est ''Just In Time compilation'', abrévié en JIT. En soi, le JIT est associé à de l'interprétation, les JIT sont en réalité des hybrides interpréteurs-compilateurs. Un point important est que compiler du code à la volée est assez lourd, cela a un cout en performance. Par contre, le code compilé s'exécute plus vite que du code interprété. Si on exécute le code compilé une seule fois, le cout de la compilation l'emporte sur le gain à l'exécution. Mais le code compilé est réutilisé autant de fois que nécessaire. Le gain à l'exécution est donc multiplié par le nombre d'exécutions, alors que le cout de la compilation est répartit sur plusieurs exécutions, il est amorti. Les portions du code sont choisies de manière à avoir un gain en performance maximal. L'idée générale est que le code exécuté très souvent est compilé, alors que du code exécuté pas souvent est interprété. Typiquement, les boucles les plus souvent exécutées sont compilées, le code exécuté une seule fois est interprété. ===Les langages intermédiaires=== Les compilateurs modernes passent par un '''langage intermédiaire''' pour faire la transformation en code machine. Le compilateur traduit le langage de haut niveau en langage intermédiaire, puis traduit le langage intermédiaire en code machine cible. Le langage intermédiaire est parfois appelé le '''''bytecode''''', et ce terme recouvre aussi le code écrit avec du ''bytecode''. L'interprétation aussi peut passer par l'intermédiaire d'un un ''bytecode'', ce n'est pas limité aux compilateurs proprement dit. {| |[[File:Intermediate representation scheme.png|centre|vignette|upright=1.5|Représentation intermédiaire d'un compilateur.]] |[[File:Code interpreter scheme.png|centre|vignette|upright=1.5|Fonctionnement d'un interpréteur.]] |} Faire ainsi a de nombreux avantages, le principal étant d'avoir un compilateur capable de traduire un langage de haut niveau vers plusieurs jeux d'instructions différents. Cela permet d'avoir, par exemple, un compilateur qui traduit du C soit en code machine x86, soit en code machine pour un CPU ARM, soit pour un CPU POWERPC, etc. [[File:Compiler design.svg|centre|vignette|upright=3|Compiler design]] Pour l'interprétation, l'usage d'un ''bytecode'' a d'autres avantages. Pour l'exploiter, il faut distribuer non pas le code source, mais le ''bytecode''. En clair, l'interpréteur ne prend pas en entrée le code source, mais du ''bytecode'' déjà compilé. La traduction est alors beaucoup plus simple, car le code source a déjà été partiellement compilé, par la traduction en ''bytecode''. Interpréter du code source est en effet assez compliqué : il faut effectuer des étapes d'analyse lexicale, sémantique, et bien d'autres. Avec du ''bytecode'', ces étapes ont été réalisées lors de la compilation du ''bytecode'', l'interpréteur a alors peut de choses à faire. L'interprétation du ''bytecode'' se fait instruction par instruction, au niveau du ''bytecode''. Une instruction du ''bytecode'' est traduite en une instruction machine équivalente, qui est exécutée. Si l'instruction est un peu complexe, il exécute une fonction/procédure qui fait la même chose. Pour résumer, le ''bytecode'' est ensuite traduit à la volée et exécuté instruction par instruction par un logiciel appelé l'interpréteur. [[File:Control table.png|centre|vignette|upright=2|Control table]] Le langage intermédiaire peut être vu comme l'assembleur d'un processeur, n'existe pas forcément dans la réalité, mais dont le jeu d'instruction est décrit en détail. Le processeur en question est appelé une '''machine abstraite''', ou encore une ''machine virtuelle''. Nous utiliserons le terme de machine abstraite dans ce qui suit. La machine abstraite n'est pas la même suivant que l'on cible la compilation ou l'interprétation/JIT. Pour l'interprétation/JIT, la machine abstraite est souvent une machine à pile, car cela simplifie grandement la traduction en code machine final. L'interprétation demande que la traduction du ''bytecode'' en code machine soit la plus simple possible. Et cela demande de prendre en compte pas mal de détails. Par exemple, les différents jeux d'instruction existants n'ont pas le même nombre de registres, ce qui pose problème lors de la traduction du ''bytecode''. Lors de la transformation en code machine, un algorithme d'allocation de registres se débrouille pour traduire le code intermédiaire en code qui utilise un nombre limité de registres. Pour améliorer cette allocation de registres, il y a deux solutions niveau ''bytecode'' : soit utiliser un nombre illimité de registres, soit utiliser une machine à pile. Pour les machines à pile, il existe un algorithme simple et rapide pour traduire un code écrit pour une machine à pile en un code écrit pour un processeur avec des registres, qui se débrouille pas trop mal pour allouer efficacement les registres. C'est un avantage assez important pour les langages interprétés. ==Les processeurs qui exécutent du bytecode== Le ''bytecode'' est un code machine, ce qui signifie qu'il peut en théorie s'exécuter sur un processeur qui implémente le jeu d’instruction associé. Si le jeu d'instruction d'un ''bytecode'' est souvent une description censée être fictive, elle n'en reste pas moins un jeu d'instruction et des caractéristiques précises, qu'on peut l'implémenter en matériel ! Néanmoins, tous les ''bytecode'' ne sont pas égaux de ce point de vue. Certains sont faciles à implémenter en matériel, d'autres non. Les ''bytecodes'' implémentés en matériel sont ceux dont les machines abstraites sont des machines à pile. Un premier exemple est celui des processeurs Pascal MicroEngine, qui exécutaient directement le bytecode du langage Pascal, le fameux UCSD P-code. Un second exemple est la '''machine SECD''', qui sert de langage intermédiaire pour certains compilateurs de langages fonctionnels. Elle a été implémentée en matériel par plusieurs équipes, notamment par les chercheurs de l'université de Calgary en 1989. Dans le même genre, quelques processeurs simples étaient capables d’exécuter directement le ''bytecode'' du langage FORTH. Le FORTH, un des premiers langages à pile de haut niveau, possède de nombreuses implémentations hardware et est un des rares langages de haut niveau dont le ''bytecode'' a été utilisé comme langage machine sur certains processeurs. Par exemple, on peut citer le processeur FC16, capable d’exécuter nativement du ''bytecode'' FORTH. ===Les processeurs Java=== Le cas le plus impressionnant est celui de la machine virtuelle Java, qui est un design de processeur comme un autre. En temps normal, le ''bytecode'' Java est compilé ou interprété, mais certains processeurs exécutaient du ''bytecode'' Java directement, sans interprétation ni compilation. Ils sont appelés des '''processeurs Java'''. On peut citer les processeurs ARM disposant de l'extension Jazelle, du JEMCore et du aJ-100 de aJile Systems, le picoJava II de Sun, et quelques autres. Il y a aussi eu quelques projets de recherche ou processeurs open source, comme Komodo, jamuth, le ''Java Optimized Processor'' et quelques autres projets du même genre. L'implémentation de la machine virtuelle Java n'était cependant pas complète. Quelques instructions complexes n'étaient pas gérées par le processeur et devaient être émulées en logiciel. Mais la grosse majorité des instructions l'était. Les instructions non-supportées étaient émulées avec la méthode ''trap and emulate'', à savoir qu'un opcode inconnu déclenchait une exception matérielle ''opcode invalide'', dont la routine pouvait émuler les instructions qui doivent l'être. Le processeur aJ-100 de aJile est capable d'exécuter la quasi-totalité des instructions de la JVM Java, seules deux instructions faisant exception. Les '''accélérateurs Java''' sont une solution intermédiaire, qui permet de traduire à la volée du ''bytecode'' Java en instructions RISC, exécutées sur un autre processeur. Il s'agit formellement de traduction binaire, mais je préfère en parler ici. L'idée est de combiner un processeur RISC avec un coprocesseur Java. Le coprocesseur lit le ''bytecode'' Java et le traduit en instructions RISC, ces dernières étant exécutées sur le processeur RISC. Le JA108 de Nozomi était un coprocesseur de ce genre, au même titre que le coprocesseur JSTAR de JEDI. L'extension Jazelle de certains CPU ARM fonctionnait vraisemblablement sur un principe similaire, sauf que le coprocesseur était intégré dans le processeur ARM, entre le cache d'instruction et le décodeur d'instructions ARM. L'intérêt des processeurs Java n'est pas qu'une question de performance. Certes, ils permettent d’exécuter plus vite les programmes compilés en ''bytecode'', comme des programmes Java pour la JVM Java. Mais l'intérêt est aussi de faire des économies de mémoire RAM et ROM. Je rappelle que la machine virtuelle Java est une machine à pile, ce qui fait que sa densité de code est excellente. Les programmes en ''bytecode'' Java sont donc très petits, et prennent peu de place en mémoire ROM. De plus, utiliser un processeur Java permet de se passer d'interpréteur ou de JIT Java, ce qui économise encore plus de mémoire. ===Transformer un processeur RISC en machine à pile=== La plupart des processeurs Java utilisent en interne une architecture à registre généraux, qui émule une machine à registre en microcode. Quelques registres sont réservés pour l'état de la machine virtuelle, avec au minimum un registre réservé pour le pointeur de pile, un autre pour un pointeur vers un ''pool'' de constantes, et quelques autres. D'autres registres servent à mémoriser le haut de la pile, à savoir le sommet de la pile et quelques opérandes situées juste en-dessous. Par exemple, le processeur picoJava II gardait les 64 opérandes au sommet de la pile dans 64 registres généraux, séparés des registres pour les pointeurs et l'état de la JVM. Une optimisation possible, envisagée sur les processeurs picoJava, était une sorte de macro-fusion sous stéroïde. L'idée était de fusionner certaines séries d'instructions Java en une seule instruction machine. Typiquement, pour faire une opération arithmétique, une machine à pile empile deux opérandes et exécute une opération, ce qui prend trois instructions machine. La spécification picoJava 1 a proposé d'envoyer directement la seconde opérande en entrée de l'ALU, ce qui fusionnait l'empilement de la seconde opérande et l'opération arithmétique. Le processeur picoJava II va plus loin et fusionne les trois instructions en une seule instruction RISC, agissant sur des registres. Mais la majorité des processeurs Java ne faisaient pas cela, car cela complexifie grandement le décodeur. Les processeurs ''bigfoot'' de DCT utilisaient un système différent pour émuler une pile avec des registres. Leur banc de registre de 64 bits était coupé en deux : les 16 premiers registres étaient utilisés pour la pile, les 48 suivants étaient utilisés pour autre chose. L'idée est que le registre 0 mémorise un emplacement vide, le registre 1 contient le sommet de la pile, le registre 2 l'opérande sous le sommet de la pile, et ainsi de suite. Les 15 premières opérandes de la pile sont donc mémorisées dans les registres. Les instructions écrivent leur résultat dans le registre adéquat, puis tous les registres sont décalés pour que le résultat passe dans le registre R1. Par exemple, les instructions PUSH chargent une opérande dans le registre 0, puis tous les registres sont décalés de un rang, pour que l'opérande chargée soit dans le registre 1. Le registre 0 devient alors le registre numéro 1, le registre 1 devient le 2, et ainsi de suite. Par contre, les opérations arithmétiques font autrement. Une instruction d'addition, par exemple, lit les opérandes dans le registre 1 et 2, puis place son résultat dans le registre 2 (les deux opérandes sont dépilées). Puis, les registres sont décalés d'un rang pour que ce registre 2 devienne le 1. Les registres ne sont en réalité pas décalés, il n'y a pas de transferts entre registres. A la place, les 16 premiers registres utilisaient une forme limitée de renommage de registres, similaire au banc de registre tournant utilisé sur les processeurs EPIC comme l'Itanium. Les numéros de registres sont renommés, décalés, à chaque fois qu'on empile ou dépile une opérande. Pour faire le renommage de registres, processeur contient un ''register stack counter'' de 4 bits, qui est incrémenté à chaque fois qu'on empile une donnée, et est décrémenté quand on dépile une donnée. Quand on veut accéder à un opérande, le numéro de registre architectural associé est traduit en numéro de registre physique, en additionnant le ''register stack counter'' (modulo 16, pour rester sur 4 bits). ==L'accélération de l'interprétation/JIT : le ''Thumb-EE'' d'ARM== Quelques processeurs ont ajouté des instructions pour faciliter le travail des interpréteurs/JIT. Par exemple, en 2005, ARM a ajouté le mode ''Thumb Execution Environment'', qui faisait cela. Il reprenait le jeu d'instruction compact ''thumb'' et ajoutait quelques instructions et modifiait le comportement d'instructions existantes. Le jeu d'instruction ''Thumb-EE'' a été ajouté sur les CPU ARM en 2005, mais a été déprécié en 2011, par manque d'utilité. Mais la tentative mérite qu'on s'attarde dessus. Les CPU de gamme M, déstinés à l'embarqué, n'ont jamais supporté ''Thumb-EE'', vu que de tels processeurs ne sont pas conçus pour exécuter du code interprété/JIT. Le mode ''Thumb-EE'' est un mode d'exécution, séparé de l'ARM normal. Ainsi, une instruction ''Thumb-EE'' et une instruction ARM peuvent avoir le même encodage en binaire, mais se comporter différemment. Le processeur est à tout moment soit en mode ''Thumb-EE'', soit en mode ARM normal, ce qui précise comment ces instructions doivent se comporter. Des instructions ont été ajoutées par ''Thumb-EE'', pour qu'il fasse son travail. L'entrée et la sortie du mode ''Thumb-EE'' se fait avec deux instructions : ENTERX et LEAVEX. Une fois en mode ''Thumb-EE'', le décodage des instructions ''thumb'' se fait avec les règles du ''Thumb-EE'', et non celles du ''thumb'' normal. Une première différence entre ''thumb'' normal et ''Thumb-EE'' est l'ajout de ce qui s'appelle un ''null check'' pour les instructions mémoire. Le ''null check'' est utilisé pour les instructions en mode d'adressage "base + indice" ou "Base + décalage", ou tout autre mode d'adressage avec un registre de base utilisés dans des calculs d'adresse. L'idée est de vérifier si l'adresse de base vaut zéro ou non. Si c'est le cas, le processeur lève une exception matérielle, qui est traitée par l'interprétation ou le compilateur JIT. Une telle situation est en effet signe d'une erreur d'adressage mémoire, qui doit être traitée par l'interpréteur. Typiquement, c'est signe qu'un pointeur n'a pas bien été initialisé, ce qui peut arriver avec du code interprété ou JIT. Les interpréteurs/JIT traitent généralement la situation en ajoutant un test avant toute instruction mémoire, pour vérifier si le pointeur accédé vaut zéro ou non. Avec ''Thumb-EE'', pas besoin d'ajouter les tests en questions : ils sont réalisés automatiquement lors de chaque instruction mémoire. : Les registres pour la pile, comme le pointeur de pile et le pointeur de ''frame'', sont aussi concernés. L'instruction CHKA vérifie que les accès à un tableau ne débordent pas en dehors du tableau en question. Par exemple, pour un tableau de 1024 éléments, elle vérifie si l'indice est compris entre 0 et 1024. Pour cela, elle vérifie si l'indice de tableau est bien dans l'intervalle adéquat et lève une exception matérielle si ce n'est pas le cas. Son utilité se comprend quand on sait que tous les accès à un tableau sont vérifiés par l'interpréteur. L'interpréteur est censé ajouter des instructions pour vérifier les indices, à savoir deux ou trois branchements. Avec CHKA, le test se fait en une seule instruction. D'autres modifications mineures de l'encodage des instructions sont aussi présentes, ainsi que des modifications pour les instructions LOAD/STORE. Précisément, dans les modes "base + indice" et "base + décalage", le décalage et l'indice subissent maintenant des décalages. L'idée est de simplifier le calcul d'adresse. De quoi économiser quelques instructions lors des calculs d'adresse, ce qui facilite le travail de l'interpréteur/JIT. Et des instructions LOAD/STORE précises voient leur comportement modifié pour faciliter la gestion de la pile de Java ou l'exécution des méthodes locales. Pour faciliter le travail de l'interpréteur, deux instructions HBP et HBLP ont été ajoutées. Elles branchent vers une fonction qui gère les exceptions logicielles (une fonctionnalité présente dans de nombreux langages, comme Java). La première effectue un branchement vers la fonction en question, la seconde sauvegarde l'adresse de retour avant de faire ce branchement. Les deux peuvent brancher vers 256 fonctions pré-déterminées, sans avoir à présenter leur adresse. ==La traduction binaire accélérée par le matériel== Plus haut, nous avons surtout parlé des compilateurs et des interpréteurs. Cependant, nous devons aussi parler de la '''traduction binaire'''. Elle traduit un programme écrit dans un code machine vers un autre code machine. Par exemple, elle traduit un programme compilé pour un CPU x86 vers un code machine ARM. En général, le système d'exploitation est généralement compilé pour le jeu d'instruction natif, mais il exécute des applications prévues pour le x86. Les applications sont traduites par le système d'exploitation, avant d'exécuter le code traduit. : Nous parlerons dans la suite de code machine source et de code machine cible, pour parler respectivement du code à traduire et du code obtenu après traduction. Idem avec d'autres termes comme architecture cible/source, ou jeu d'instruction cible/source. La traduction binaire est surtout utilisée pour des questions d'émulation ou de compatibilité. Par exemple, lorsque les Macintosh sont passés de processeurs Power PC vers des processeurs x86, le système d'exploitation Mac OS utilisait la traduction binaire pour convertir les anciennes applications Power PC vers du code x86. Les utilisateurs n'y ont vu que du feu. Le système de traduction binaire était appelé Rosetta 1. Par la suite, lors de la transition de processeurs x86 vers des processeurs Apple, Rosetta 2 a vu le jour. La traduction binaire a surtout été utilisée pour traduire du code x86 vers un autre jeu d'instruction. Il faut dire que le x86 est le jeu d'instruction dominant. De nombreuses entreprises ont eu pour ambition de briser l'hégémonie du x86 sur PC, en remplaçant le x86 par un jeu d'instruction plus performant, tout en gardant une compatibilité maximale. La traduction binaire était la seule solution pratique. Elle était le plus souvent intégralement réalisée en logiciel, comme c'était le cas sur les architectures Itanium avec le ''IA-32 Execution Layer''. ===Les généralités sur la traduction binaire assistée en matériel=== L'architecture source est presque tout le temps une architecture CISC, assez ancienne, qu'on souhaite émuler. Le choix de l'architecture cible se porte souvent sur une architecture VLIW, fort différente de l'architecture source, et ce pour obtenir une économie de matériel conséquente. Pour s'exécuter rapidement, le code traduit doit exploiter le parallélisme d'instruction, à savoir exécuter plusieurs instructions en même temps dans des unités de calcul séparées. Il est possible d'utiliser un processeur superscalaire avec exécution dans le désordre pour cela, mais au prix d'un cout important en transistors. Alors qu'en utilisant un CPU VLIW, c'est le traducteur binaire qui fait tout le travail d'extraction du parallélisme d'instruction. Et après tout, quitte à avoir un traducteur binaire, autant lui refiler le boulot d'optimisation. Compiler du code à la volée est certes assez lourd, mais qu'il existe des algorithmes efficaces pour regrouper des instructions indépendantes dans une seule instruction VLIW. Au passage, le processeur VLIW a plus de registres que le processeur source. Cela permet de faire du renommage de registres directement en logiciel. Le traducteur binaire n'hésite pas à changer les noms de registres entre instructions source et instruction VLIW cible, afin de supprimer des dépendances de données. En conséquence, cela ouvre des opportunités de parallélisme, qui sont exploitées lors du regroupement des opérations en instructions VLIW. Vous pourriez penser que le choix d'un CPU VLIW pour émuler du CISC est tout sauf optimal. Vous devez penser que la traduction binaire est d'autant plus simple que l'architecture source et cibles sont semblables. Dans les faits, ce n'est pas tellement traduire les instructions qui pose problème, mais plus la gestion du registre d'état, des exceptions matérielles, de la mémoire virtuelle, la différence entre gros-boutisme et petit-boutisme, de même que des différences d'adressage pour les périphériques. Le premier problème est la gestion des conditions, notamment en présence d'un registre d'état. En pratique, la traduction binaire s'utilise pour traduire du code CISC vers du code RISC ou VLIW. Les architectures source ont donc un registre d'état, qui est mis à jour non seulement par des instructions de test, mais aussi des instructions arithmétiques. L'architecture cible n'a elle pas de registre d'état, mais des registres à prédicats. La traduction de l'un vers l'autre est alors quelque peu compliquée. Et elle est d'autant plus compliquée que le registre d'état est la source de dépendances d'instruction implicites, qui réduisent les performances. Par exemple, si une instruction arithmétique modifie le registre d'état, cela peut impacter l'exécution d'une instruction ultérieure, qui lit ce registre d'état. Pour éliminer ces fausses dépendances, le traducteur binaire doit renommer ce registre en logiciel et détecter les dépendances utiles. Et le compilateur doit gérer les cas où il y a beaucoup de distance entre les deux, voire les cas où l'instruction dépendante n'a pas encore été analysée par le compilateur. Un autre problème est lié à la gestion des exceptions matérielles, et précisément des exceptions précises. Pour rappel, une instruction VLIW regroupe plusieurs opérations, opérations qui correspondraient à une instruction machine sur un CPU pas VLIW. En conséquence, plusieurs instructions du langage machine "source" sont regroupées en une seule instruction VLIW. Et il faut tenir compte du cas où une instruction source lève une exception. Dans le code VLIW, cela signifie qu'une opération lève une exception, et il faut annuler partiellement l'instruction VLIW associée. Et par partiellement, on veut dire que seules les opérations suivantes dans l'ordre du programme source doivent être annulées, puis ré-exécutées. Et c'est un sacré casse-tête ! Les processeurs VLIW utilisent comme solution le mécanisme d'exceptions différées des processeurs EPIC, vu il y a quelques chapitres. Pour résumer, le code est exécuté par blocs d'instruction, délimités par des branchements ou tout autre limite/barrière pertinente dans le code. Un bloc de code est compilé en deux versions : une version rapide sans exceptions matérielles, une version lente qui gère les exceptions précises. La version rapide s'exécute sans exécuter les exceptions. Cependant, les exceptions matérielles sont enregistrées, pour être prises en compte à la toute fin du bloc de code. La version rapide est exécutée en premier et elle mémorise si une exception matérielle a eu lieu. Puis une instruction vérifie si une exception a eu lieu et décide quoi faire. Si aucune exception n'a eu lieu, elle passe à la suite du programme, les résultats du bloc de code sont définitivement acceptés. Mais si une exception a eu lieu, tout est annulé. Le processeur est remis dans l'état initial, puis le code est ré-exécuté instruction par instruction de manière à gérer l'exception correctement. ===La traduction par pré-décodage=== Une première solution serait de faire la traduction binaire dans le cache d'instruction. Lors d'un défaut de cache, le code chargé depuis la RAM est traduit en code machine cible. Et c'est ce code machine cible qui est mémorisé dans le cache d'instruction et exécuté par le processeur. La technique marche sur le papier et n'est qu'une amélioration des techniques de pré-décodage vues il y a quelques chapitres. Cependant, le code obtenu est une traduction assez basique, qui n'incorpore pas d'optimisations dignes de ce nom. De plus, elle gère mal le cas où la taille du code source et cible sont potentiellement très différentes. Concrètement, elle est surtout utile pour traduire du code d'un processeur RISC vers un autre processeur RISC, dont les tailles d'instructions sont similaires. Elle ne permet pas d'émuler plusieurs jeux d'instructions différents, le cout en matériel (un circuit de pré-décodage par jeu d'instruction) serait trop important. ===Le projet DAISY d'IBM=== De très rares processeurs étaient conçus pour accélérer cette traduction binaire, afin de garder de bonnes performances. Les premiers à avoir étudié l'idée étaient IBM, avec leur projet DAISY (''Dynamically Architected Instruction Set from Yorktown''). Le projet de base était de convertir à la volée du code compilé pour des CPU Power PC, vers du code VLIW. Les chercheurs d'IBM avaient développé un algorithme de traduction dynamique efficace, ainsi qu'un processeur VLIW disposant d'optimisations spécifiques à la traduction binaire dynamique, à la volée. De nombreuses idées de ce projet ont été reprises ou re-découvertes par la société Transmetta, avec ses processeurs Crusoe et Efficieon, puis par NVIDIA avec son projet Denver. Les ingénieurs de ce projet ont étudié la possibilité d'émuler plusieurs jeux d'instructions différents sur un même processeur, notamment le s390 d'IBM et le x86. En théorie, cela demande juste d'avoir plusieurs programmes de traduction binaire : un pour le x86, un autre pour le s390, éventuellement un autre pour le Power PC. Mais cela ne s'est pas concrétisé. Mais les ingénieurs ont étudié quelle pourrait être l'architecture VLIW idéale pour ça. Par exemple, il fallait des additions 3-opérandes pour simplifier les calculs d'adresse. Une difficulté était la gestion du registre d'état, dont les bits ne sont pas mis à jour de la même manière sur le x86 et le s390 ou le Power PC. La différence entre gros-boutisme et petit-boutisme était aussi un problème, de même que des différences d'adressage pour les périphériques. Pour que la traduction binaire soit efficace, le processeur VLIW intègre diverses optimisations, comme les exceptions différées et les branchements multi-voies. La principale est la suivante : seul le code des boucles ou fonctions exécutées fréquemment est traduit en code VLIW, le reste du code est interprété. En effet, traduire du code binaire prend plus de temps qu'une simple interprétation. Pour du code qui ne sera exécutée qu'une seule fois, il est plus rapide d'utiliser l'interprétation. Par contre, pour du code exécuté beaucoup de fois, le cout de la traduction binaire est amorti, dilué sur N exécutions, compensé par le gain en temps d'exécution de ce code traduit. Au final, cela permet de ne traduire que le code qui le mérite. La détection du code fréquemment exécuté est réalisée dans le cœur VLIW. L'unité de branchement mémorise les derniers branchements rencontrés et le nombre de fois qu'ils ont été exécutés. Si ils ont été exécutés un certain nombre de fois, l'unité de branchement lève une exception matérielle, qui invoque le traducteur binaire. La routine de cette exception est le traducteur binaire proprement dit. En clair, la traduction binaire est démarrée quand le processeur détecte qu'une fonction a été exécutée plus de N fois, via une exception matérielle dédiée. La mesure du nombre d'exécution d'un branchement se fait dans l'unité de calcul dédiée aux branchements, avec l'aide d'une sorte de ''branch adress buffer'' modifié. Pour rappel, le ''branch adress buffer'' mémorise le ''Program counter'' de chaque branchement récemment rencontré. Ici, chaque entrée du ''branch adress buffer'' est associée à un compteur incrémenté à chaque exécution du branchement. ===Les processeurs Crusoe et Efficieon de Transmetta=== Les processeurs Crusoe Et Efficeron sont deux processeurs VLIW produits par la société Transmetta, une société californienne rachetée par NVIDIA. Ils étaient conçus pour exécuter spécifiquement des programmes x86, système d'exploitation inclus. L'idée derrière ce projet était d'exécuter du code x86 sans que les applications, ni même le système d'exploitation et le BIOS soient au courant ! Les processeurs Transmetta étaient en réalité conçus pour exécuter un programme unique, le ''Code Morphing Software'' (CMS), qui traduisait le code x86 en code VLIW. Transmetta ne voulait pas que son processeur VLIW soit exploité directement, sans le CMS. Il n'avait pas rendu public de compilateur pour traduire du C vers du code VLIW, il n'avait pas donné la documentation du jeu d'instruction VLIW. Des efforts de rétro-ingénieurie, documentés sur le site realworldtech, ont cependant permis de comprendre comment étaient encodées les instructions du processeur Crusoe. Comme pour le projet Daisy, le ''Code Morphing Software'' utilisait à la fois interprétation et traduction binaire, selon les besoins. Les instructions sont interprétées lors de leurs premières exécutions, mais le CMS bascule sur de la traduction en code VLIW après un certain nombre d'exécution. Ainsi, les boucles souvent exécutées sont traduites en code VLIW, alors que le reste du code est interprété. Le choix du code à traduire est le fait du CMS, il utilise des heuristiques complexes pour, qui ne sont pas connues dans le détail. Le ''Code Morphing Software'' est mémorisé dans une EEPROM, ce qui en fait un ''firmware'' situé en dessous du BIOS. Le CMS démarre ensuite le BIOS, qui lui-même démarre le système d'exploitation, qui lui-même démarre les pilotes de périphériques et les programmes. Le CMS se réserve les 16 premiers mébioctets de l'espace d'adressage. L'EEPROM du CMS est mappée dedans, mais n'en utilise que la moitié. Le reste est utilisé comme cache, pour mémoriser le code VLIW traduit par le CMS. Les deux processeurs intégraient aussi deux mémoires SRAM, utilisées par le CMS, appelées la ''local program memory'' (LPM) et la ''local data memory'' (LDM). La première contient du code qui gére les interruptions, la mémoire virtuelle, les exceptions matérielles, les problèmes d'alignement mémoire, et quelques fonctions très fréquemment utilisées par le CMS. La seconde est de la mémoire RAM utilisée par ces fonctions de la ''local program memory''. Les mémoires caches L1 et L2 sont séparées de ces deux mémoires. Le processeur Crusoe et Efficieon étaient des processeurs VLIW très simples. Ils n'avaient même pas de MMU, ni de fonctionnalités importantes sur les CPU x86. Pour Crusoe, les instructions VLIW étaient encodées sur 64 ou 128 bits, et regroupaient entre 2 à 4 opérations. Les instructions LOAD/STORE ne supportaient pas d'adressage indicé, juste de l'adressage indirect à registre. Crusoe avait 5 unités de calcul : deux ALU entières, une unité LOAD/STORE, une unité de branchement, une FPU. Efficieon doublait le nombre d'ALU entière et d'unité LOAD/STORE, les instructions VLIW passaient à 256 bits. Le processeur Crusoe contient 160 registres, dont 64 registres généraux, 32 registres flottants. Les registres généraux font 32 bits, ce qui est cohérent avec le fait que les CPU x86 de l'époque étaient des processeurs 32 bits. Quant aux registres flottants, ils faisaient 80 bits, ce qui colle avec la taille des registres flottants de la FPU x87 utilisée à l'époque. Sur les 64 registres généraux, seuls 48 étaient réellement utilisable pour mémoriser des opérandes. Une partie des registres généraux étaient utilisés pour la gestion de la pile, d'autres pour mémoriser l'état du CPU x86 émulé, un registre était un registre zéro non accesible en écriture. La gestion des exceptions est optimisée avec un système d'exceptions différées, le même que celui décrit plus haut. Le processeur utilise un système similaire, pour effectuer des lectures anticipées, qui a été expliqué dans le chapitre sur les processeurs VLIW/EPIC. Sauf qu'il s'agit là d'une optimisation pour gagner en performances en général, pas une aide utile pour gérer des exceptions précises ou toute autre subtilité de la traduction binaire. Pour gérer les exceptions différées et les lectures anticipées, le processeur mémorise l'état du processeur avant de démarrer un bloc de code. Pour cela, le processeur contient des ''shadow registers'', 48 registres entiers et 16 registres flottants. Vous remarquerez qu'il y a autant de registres entiers que de ''shadow registers'' entiers. Les registres entiers/flottants sont mémorisés dans les ''shadow registers'' avant d'exécuter un bloc de code, afin que le processeur puisse revenir à l'état de base. Une autre fonctionnalité liée est le ''gated store buffer''. Le principe est d'éviter toute écriture en mémoire RAM, tant que le code VLIW n'a pas émis d'instruction ''commit''. L'idée est que les écritures sont conservées dans la ''store queue'' de l'unité LOAD/STORE. Pour rappel, la ''store queue'' met en attente les écritures soit tant que les écritures ne sont pas terminées, soit tant que la RAM est occupée. L'idée est que les écritures sont accumulées dans la ''store queue'' tant que l'instruction ''commit'' n'a pas eu lieu. Le ''store queue'' est "déversé dans la RAM" seulement quand l'instruction ''commit'' s'exécute. ===Les CPU NVIDIA Denver=== Nvidia a tenté quelque chose de similaire avec son projet Denver. NVIDIA a racheté l'entreprise Transmetta, ses brevets, et a tenté de refaire la même chose. Le projet initial était de traduire du code x86 vers un jeu d'instruction VLIW propriétaire, comme l'a fait Transmetta. Mais NVIDIA n'a pas réussi à acquérir la licence du jeu d'instruction x86 et s'est rabattu sur le jeu d'instruction ARM. Le processeur né de ce projet est le Tegra K1-64 CPU. Il s'agit d'un processeur composé d'un cœur VLIW, couplé à deux décodeurs d'instructions ARM. Les deux décodeurs ARM traduisent une paire d'instructions ARM consécutives en instructions VLIW, pour les exécuter sur le cœur VLIW. Le CPU peut fonctionner selon deux modes : ARM et VLIW. En somme, le processeur supporte deux jeux d'instruction : le jeu d'instruction ARM, et un jeu d'instruction VLIW. En mode ARM, les instructions ARM sont chargées depuis le cache, traduites en VLIW par les décodeurs ARM, puis exécutées par le cœur VLIW. En mode optimisé, le CPU exécute des instructions VLIW chargées depuis le cache d'instruction, les décodeurs ne sont pas utilisés. : Le CPU supporte en réalité deux jeux d'instruction ARM : ARM8 et 7. Mais ils correspondent au même mode pour le CPU. Un point important est que le processeur peut changer très rapidement de mode, en à peine quelques cycles d'horloges. On n'est clairement pas dans le cas des CPU x86, qui mettent des plombes pour passer du mode 32 à 64 bits et inversement. La commutation est tellement rapide qu'on peut considérer que le processeur supporte deux jeux d'instruction simultanément. Et le support simultané de deux jeux d'instruction facilite l'implémentation de la traduction binaire. Encore une fois, inutile de traduire du code qui ne sera exécutée qu'une seule fois, mieux vaut l'exécuter directement dans le mode ARM. Surtout que le processeur supporte un mode ARM qui permet d'exécuter du code ARM sans perte de performance. Par contre, le code fréquemment utilisé, notamment dans des boucles critiques, est traduit en VLIW. Au final, les performances sont optimisées, en limitant le travail du logiciel traducteur binaire. La détection du code fréquemment exécuté se fait différemment que pour les processeurs Transmetta et Daisy. L'unité de branchement ne détecte pas directement le code beaucoup exécuté, même si elle a un rôle à jouer. A la place, la détection est réalisée en logiciel, par un ''thread'' dédié, qui s'exécute sur un cœur séparé. Il détecte le code fréquemment utilisé en regardent les branchements exécutés récemment. Pour cela, le processeur mémorise l'historique des branchements pris récemment et copie cette historique dans une mémoire tampon dédiée, partagée entre tous les cœurs. Vu qu'elle est partagée, le ''thread'' de détection a accès à l'historique sans pertes de performances, seule la copie de l'historique dans ce tampon a un cout en performance, pas son partage. Le code VLIW obtenu après traduction binaire est mémorisé dans la mémoire RAM, dans une portion spécialement réservée pour. Elle fait 128 mébioctets, et est appelée le '''cache d'optimisation''' par NVIDIA. Le cache d'optimisation est protégé en écriture et seul le traducteur binaire peut écrire dedans. Au passage, le traducteur binaire est du code VLIW, ce qui fait qu'il est placé dans le cache d'optimisation. Le processeur détecte automatiquement quand une fonction pour laquelle le code compilé équivalent est disponible. Pour cela, l'unité de chargement contient une table de correspondance entre l'adresse de la fonction ARM, et l'adresse de son équivalent compilé. Quand une fonction est appelée, l'unité de chargement regarde l'adresse de destination du branchement. Si l'adresse est dans cette table, elle récupère l'adresse de la fonction compilée et branche vers celle-ci. Le processeur dispose d'un cache d'instruction de 128 kibioctets, ce qui est très important, mais nécessaire vu la taille des instructions VLIW. Le coeur VLIW dispose de ses propres décodeurs, d'un ''scoreboard'', de 7 unités de calcul et de registres. Pour les unités de calcul, il a deux FPU, deux ALU, une unité de branchement, et deux unités LOAD-STORE faisant aussi ALU. Un point important est que le CPU dispose de 64 registres entiers et de 64 registres flottants. C'est deux fois plus que supporte le jeu d'instruction ARM. En mode ARM, seule la moitié des registres est utilisée. Mais en mode optimisé, le traducteur binaire utilise bien les 64 registres, grâce à une sorte de renommage de registres logicielle. Pour simplifier la traduction binaire, le processeur supporte les techniques vues précédemment. Le processeur intègre un ''gated store buffer'' similaire à celui des processeurs Transmetta. Les techniques d'exceptions différées sont aussi supportées, comme sur les processeurs Transmetta. Il a aussi une unité de préchargement décrite par NVIDIA comme agressive, avec support du préchargement de type anticipé (''runahead''). ===L'émulation de la mémoire virtuelle=== Pour finir, il faut parler de la mémoire virtuelle avec la traduction binaire. Sur le processeur source, le processeur gère à la fois des adresses physiques et virtuelles et sa MMU fait la conversion entre les deux. Sur le processeur VLIW, la MMU est simulée par le programme de traduction binaire, partiellement ou totalement. L'implémentation exacte varie suivant que l'on parle du projet Daisy, des processeurs Transmetta ou de Denver. Sur les processeurs Transmetta, il n'y a pas de MMU ni de mémoire virtuelle. A la place, le programme de traduction binaire émule la mémoire virtuelle du processeur source. Le processeur VLIW ne gère que des adresses physiques, rien d'autre. L'espace d'adressage physique du processeur VLIW a la même taille que celui du processeur émulé, ici des processeurs x86. Il est vraisemblable que les adresses physiques utilisées par le processeur x86 sont les mêmes que celles du processeur VLIW. Sur le projet Daisy, le processeur VLIW gère la mémoire virtuelle via pagination, comme les processeurs émulés. Ce qui fait qu'il gère un espace d'adressage virtuel et un espace d'adressage physique. Pour éviter toute confusion, nous parlerons d'adresse physique/virtuelle VLIW pour les adresses physiques/virtuelles du processeur VLIW, d'adresse virtuelle/physique source pour celle du jeu d'instruction traduit, à savoir du Power PC ou du s390, éventuellement de l'x86. Il faut alors faire le lien entre adresses physiques source et adresses virtuelles VLIW. Pour cela, rien de plus simple : il y a correspondance parfaite. L'adresse physique source numéro N correspond à l'adresse virtuelle VLIW numéro N. Cependant, cela signifie que tout l'espace d'adressage virtuel VLIW serait utilisé par le code à traduire. En réalité, il faut ajouter de la place pour le code traduit et le programme de traduction binaire. La conséquence est que l'espace d'adressage virtuel VLIW est plus large que l'espace d'adressage source. L'espace d'adressage virtuel VLIW est découpé en trois sections : une section pour le code Power PC à traduire, une autre pour le code traduit en VLIW, et une dernière réservée au traducteur binaire. Le programme de traduction est placé dans une ROM mappée en mémoire dans la seconde section. Le reste de la seconde section est réservé à une mémoire RAM utilisée par le programme de traduction. [[File:Mémoire virtuelle sur le projet Daisy.png|centre|vignette|upright=1.5|Mémoire virtuelle sur le projet Daisy]] Il faut noter que chaque page physique source, une fois traduite, correspond à N pages virtuelles VLIW. La raison est que le code traduit prend plus de place que le code machine originel. En conséquence, on doit utiliser une page finale plus grande. Pour le reste, des pages contiguës en mémoire physique source sont elles aussi contiguës en mémoire virtuelle VLIW. Le calcul d'adresse est donc simple : l'adresse physique source est multipliée par N, puis on ajoute l'adresse de base à laquelle commence la section pour le code traduit. ==La traduction binaire sur le Burroughs B1700== Nous venons de voir comment accélérer en matériel la traduction binaire, avec des techniques bien spécifiques. Le Burroughs B1700 a procédé autrement et il est intéressant d'étudier en détail son architecture. Son architecture est décrite dans le livre "Interpreting Machines : Architecture and Programming of the Bl700/Bl800 Series", écrit par Elliott I. Organick et James A. Hinds. Une partie de ce qui vfa suivre est un très court résumé de ce livre. ===Le Burroughs B1700 : le mal-nommé ''split-level control store''=== Le Burroughs B1700 avait plusieurs modèles : le B1726, le B1710 et le B1800. Ils se distinguaient par la manière dont le code machine source était traduit par le traducteur binaire. Dans ce qui suit, nous parlerons de traducteur binaire pour parler soit d'un interpréteur pour un langage de programmation de haut niveau, soit pour un logiciel de traduction binaire. Le B1710 plaquait le traducteur binaire en mémoire RAM. Le B1800 faisait pareil, mais avait ajouté un cache dédié au code du traducteur binaire, afin d'améliorer les performances. Le B1726 avait un traducteur binaire scindé en deux : une partie était en mémoire RAM, l'autre était dans un ''local store'' dédié. Le processeur peut exécuter le traducteur binaire soit depuis le ''local store'', soit depuis la mémoire RAM avec une pénalité en termes de performances. La documentation appelle cela un '''''split-level control store''''', mais le terme est trompeur : la documentation qualifie de microcode le langage machine du Burroughs B1700, ce qui est à l'origine de beaucoup de confusions. Une autre source de confusion est que ce langage machine a quelques propriétés qu'on pourrait attendre d'un microcode, mais certaines sont manquantes. Par exemple, il peut adresser les registres d’interfaçage mémoire, mais c'est parce que ce sont des registres architecturaux sur ce processeur. Ses instructions n'encodent pas de signaux de commande, par contre, elles font 16 bits et adressent les opérandes de manière implicite. Le ''local store'' mémorisait au maximum 2048 instructions machine, ce qui fait que les 2048 premières "adresses" correspondaient au ''local store''. Les suivantes étaient des adresses de mémoire RAM qui contenaient le reste du traducteur binaire. En pratique, l'adresse qui séparait les deux était configurable ! On pouvait limiter la taille du ''local store'' à un multiple de 32 instructions ! Les adresses restantes étaient alors réattribuées à la RAM. Par exemple, on pouvait attribuer 64 adresses au ''local store'', les 2048 - 64 adresses restantes étaient alors attribuées à la mémoire RAM. Un registre limite, nommé TDPM, indiquait quelle était l'adresse de démarcation entre les deux. Il contenait 4 bits, dont la valeur était multipliée par 32, pour obtenir l'adresse limite. Le programmeur devait découper le traducteur binaire en segments, certains étant placé dans le ''local store'', les autres étant en mémoire RAM. Mieux que ça, le processeur autorisait d'utiliser la technique de l'''overlaying'' pour le microcode ! Il y avait même une fonction OVERLAY dédiée pour ! OVERLAY avait besoin de trois opérandes : la taille du segment à copier, son adresse source en mémoire RAM, son adresse de destination dans la micro-SRAM. L'interpréteur réservait un "segment" en mémoire RAM, pour les données, dont le processeur connaissait la position. En-dehors de ce segment, il n'y a que des instructions machines à traduire, et le traducteur binaire. Le CPU incorporait pour cela deux registres, un pour l'adresse de base du segment de données, un autre pour son adresse de fin. Il incorporait une protection mémoire limitée sur ce segment, à savoir que seul l'interpréteur pouvait lire ou écrire dans ce segment. ===Une architecture bit-adressable=== Le CPU Burroughs B1700 était '''bit-adressable'''. En clair, il pouvait adresser la mémoire bit par bit, si nécessaire. Un avantage est que cela facilitait la traduction binaire des instructions machine, mais nous détaillerons cela plus bas. Un autre avantage est que cela permettait d'émuler des architectures dont la taille des registres/''byte''/mots était très différente. Et cet avantage mérite quelques explications immédiates. Le processeur était un CPU 24 bits, mais il pouvait émuler des processeurs 16 bits, 8 bits, 4 bits, 1 bit, 9 bits, ou toute autre valeur. Pour cela, le processeur lisait des mots de 24 bits et utilisant un ''barrel shifter'' pour sélectionner les bits adéquats. Par exemple, pour un accès de 16 bits, la donnée lue en mémoire était masquée de manière à ne garder que les 16 bits de poids faible. Le ''barrel shifter''/circuit de masquage était placé directement avant le bus mémoire, sur le trajet des données/instructions. Le ''barrel shifter'' était commandé par un registre ''Field Unit'', qui précisait quelle était la taille des données à charger. Pour communiquer avec la mémoire, il avait trois registres séparés : READ, WRITE, MAR. READ contient la donnée lue lors d'une lecture, WRITE est pour une donnée à écrire lors d’une écriture, MAR contient l'adresse à lire ou en cours de lecture. Il s'agit de registres d’interfaçage mémoire. Le fait que ces registres d’interfaçage mémoire et IO soit adressable est peu commun, et c'est plus quelque chose qu'on attend d'un microcode que d'instructions machines. Mais il faut bien comprendre que les registres d’interfaçage mémoire ont été promus au rang de registres architecturaux, sur ce processeur. De même, pour communiquer avec les entrées-sorties, il disposait de deux registres CMND et DATA : CMND pour envoyer une commande à une entrée-sortie, DATA pour lire ou écrire une donnée. Il y avait aussi un registre U, qui mémorisait une donnée lue depuis le lecteur de cassette. ===L'ALU de taille variable=== Les Burroughs B1700 avaient une '''ALU de taille variable''', ce qui veut dire qu'elle pouvait faire des calculs sur un nombre de bits compris entre 0 et 24. Pour cela, les calculs étaient fait avec une ALU de 24 bits, puis les bits de poids fort inutiles étaient masqués. L'ALU de 24 bits pouvait masquer les bits de poids fort du résultat, mais avec des limitations. Elle gérait des opérandes et résultats de 0, 4, 8, 12, 16, 20 et 24 bits. Pour avoir un réglage plus fin, une ALU 4 bits séparée permettait de corriger le résultat. Les registres X et Y étaient reliés à une ALU de 24 bits capable d'effectuer des opérations arithmétiques basiques. Fait étonnant, l'ALU faisait tous les calculs en même temps et fournissait ses résultats dans 7 registres : un registre SUM pour l'addition, un registre DIFFERENCE pour la soustraction, et un registre par opération bit à bit. Les registres pour les opérations bit à bit sont : CMPX et CMPY pour l'opération NOT sur chaque opérande, XANY pour le ET bit à bit, XEOY pour le XOR, XORY pour le OU logique. Les additions et soustractions pouvaient se faire sur des opérandes codés en binaire ou en BCD, suivant le code opération utilisé. A cela, il fallait ajouter deux registres de "résultat" MSKX et MSKY. Ils fournissaient un masque dépendant de la taille du résultat en bits. C'est ce registre qui permettait de gérer des données de 0, 4, 8, 12, 16, 20, et 24 bits, alors que l'ALU faisait 24 bits. Le masque dans ce registre pouvait servir d'opérande dans une opération de masquage ultérieure, pour corriger le résultat. L'ALU prenait en opérande les registres X et Y, mais aussi un registre CYF pour la retenue entrante. En sortie, il fournissait aussi deux retenue sortantes : une pour l'addition et une autre pour la soustraction. Elles étaient mémorisées dans les registres CYL et CYD, respectivement. L'ALU fournissait aussi 12 conditions, qui alimentait un registre d'état. Le registre d'état était segmenté en trois registres appelés XYST, XYCN et BICN. Les registres X et Y sont des registres pour les opérandes, mais il y a aussi deux autres registres de 24 bits. Il s'agit des registres T et L, qui sont des registres généraux. Les registres T et L étaient découpés en 6 sous-registres de 4 bits, nommés LA LB LC LD LE et LF pour le registre L, et TA TB TC TD TE et TF pour le registre T. Ils étaient adressables comme n'importe quel registre. Le processeur contenait de nombreux autres registres de 4 bits, dont les sous-registres de T et L. En tout, il y a 27 registres de 4 bits adressables ! Et le processeur disposait d'une ALU 4 bits pour manipuler ces registres de 4 bits ! Elle est capable de faire des MOV entre registres de 4 bits, des opérations bit à bit, et peut aussi tester si un bit vaut 0 ou 1. La dernière possibilité est très utile pour implémenter les branchements. De plus, cela permettait de faire des opérations de masquage, notamment pour appliquer un masquage supplémentaire à celui de l'ALU. Par exemple, pour gérer des opérations sur 14 bits, on effectue une opération sur 16 bits avec l'ALU, et on masque les 2 bits manquants avec l'ALU 4 bits. ===Le code machine du Burroughs B1700=== Le processeur utilise des instructions de 16 bits de long, qui sont copiées dans registre d'instruction M de 16 bits. Le ''program counter'' est appelé le registre A. Les instructions du processeur font 16 bits de long, et elles sont alignées en RAM sur 16 bits, ce qui fait que les 4 bits de poids fort de ce registre valent 0, seuls les 14 bits de poids fort sont utiles. Les adresses mentionnées plus haut, qui adressent les micro-opérations en mémoire RAM, sont des adresses de mots de 16 bits, pas les adresses de bit individuels. Fait intéressant, il était possible de faire un OU logique entre l'instruction lue et un registre du processeur, le résultat étant mémorisé dans le registre M. Cela permettait de faire du code automodifiant, pour émuler des modes d'adressage complexe. Cela permettait par exemple de remplacer une adresse dans une micro-opération. L'adresse est alors surimposée avec un OU dans le champ d'adresse, alors que le reste de l'instruction n'est pas modifié. Afin de gérer les interruptions, ces dernières étaient partiellement détectées en logiciel. Une interruption doit être prise en compte à la toute fin de l'exécution d'une instruction source, quand celle-ci a finit son travail. Mais vu qu'ici, les instructions sont émulées avec des séries d'instructions cibles, il y a un problème : il est possible d'interrompre une instruction émulée en plein milieu. Pour éviter cela, le processeur ne déclenche pas les interruptions matérielles immédiatement, elles sont mises en attente. Une instruction dédiée vérifie si une interruption est en attente, et l'exécute alors cas échéant. L'instruction en question est utilisée en dernier, dans la suite d'instruction qui émule l'instruction source. Les Burroughs B1700 intégrait une pile d'adresse de retour, capable de mémoriser 16 adresses de 24 bits. Il disposait de 4 registres généraux, nommés X, Y, T et L, tous de 24 bits. Un point important est que le registre T avait des possibilités d’extraction spécifiques. Il était possible d'extraire N bits, placés n'importe où dans ce registre, et de les copier dans un autre registre. Le CPU supportait une '''instruction EXTRACT''' pour ça. Nous verrons pourquoi cette possibilité est intéressante et ne sert que pour le registre T. ===La traduction binaire sur le Burroughs B1700=== Les instructions source peuvent faire 16 bit avec tel jeu d'instruction, 8 bits avec un autre, 32 bits sur un autre, etc. Elles peuvent aussi être de longueur fixe ou variable ! Le B1700 utilisait une architecture bit-adressable pour gérer ces contraintes. En effet, avec un CPU bit-adressable, les contraintes d'alignement des instructions disparaissent ! Dans ce qui suit, on suppose que l'instruction est composée de '''champs''' : l'opcode, les opérandes, les adresses absolues, les constantes immédiates, les numéros de registre, etc. L'idée est que l'instruction source est chargée dans le registre T, puis découpée en champs, qui sont copiés dans les registres du processeur ou envoyés au microcode. Je rappelle que le registre T est relié à un ''barrel shifter'', différent au précédent, qui permettait de masquer, décaler et extraire une donnée de ce registre. Et il y a la possibilité d'extraire une suite de bit de ce registre T, pour la copier dans un autre registre. Par exemple, prenons une instruction composée d'un opcode, d'une constante immédiate et d'une adresse immédiate. Premièrement, elle est chargée dans le registre T. Deuxièmement, la constante immédiate est extraite du registre T et copiée dans le registre X. Troisièmement, l'adresse est extraite et est copiée dans le registre d’interfaçage mémoire, une lecture est démarrée, et l'opérande lue est copiée dans le registre Y. Enfin, l'instruction équivalente à l'opcode est exécutée par le microcode. Le tout prend plusieurs instructions : une première pour lire l'opcode, une seconde pour lire la première opérande, une troisième pour lire la seconde opérande, una quatrième pour tout exécuter. Notons que suivant la taille des champs, on peut en charger plusieurs à la fois. Par exemple, imaginons qu'on ait chargé 24 bits, contenant un opcode de 8 bits et une constante immédiate de 16 bits : le registre T est découpé en un opcode et une constante, avec deux instructions EXTRACT. L'architecture est bit-adressable car elle gère des champs qui ne font pas forcément 8 ou 16 bits. Par exemple, certains jeux d'instructions ont des opcodes codés sur 6 ou 7 bits, voire 9-10 bits. Utiliser une architecture bit-adressable règle ce problème d'alignement. On peut lire un champ depuis la mémoire RAM en précisant l'adresse de son premier bit, pas besoin de gérer des accès non-alignés. Par exemple, prenons une instruction source avec un opcode de 7 bits, suivi par une adresse de 12 bits et une constante immédiate de 15 bits. On peut charger 7 + 12 bits en un premier accès, pour extraite l'opcode et l'adresse, suivi par un second accès de 15 bits pour extraire la constante immédiate. Pour charger un champ, il faut connaitre plusieurs informations : l'adresse de son premier bit, sa taille. Pour cela, le B1700 contient deux registres, nommés ''Field Adress'' (FA) et ''Field Length'' (FL), qui indiquent l'adresse du premier bit et la taille de l'instruction à charger. Le premier fait 24 bits, ce qui permet de gérer des adresses de 24 bits. N'oubliez pas que le processeur est bit-adressable, ce qui permet d'adresser 16 Mébi-bits, soit 2 mébioctets. Le registre pour la taille fait lui 16 bits, ce qui permet de gérer des instructions de 65536 bits, soit 8192 octets ! Leur nom commence par ''field'', car le processeur peut gérer des instructions de taille variable et/ou plus longues que 24 bits. Dans ce cas, le chargement de l'instruction se fait champ par champ. Pour donner un exemple, prenons une instruction composée de 20 champs de 12 bits chacun. Les champs sont chargés un par un dans le registre T. Le registre FA est alors initialisé avec l'adresse de l'instruction, il est incrémenté de 12 à chaque cycle. Le registre FL est initialisé avec la taille de l'instruction, soit 20 * 12 = 240 bits, et il est décrémenté de 16 à chaque cycle. Précisons cependant que la taille d'un champ peut être changée d'un cycle à l'autre. Par exemple, le processeur peut charger un opcode de 8 bits, l'interpréter, détecter que l'instruction demande ensuite de charger une adresse de 16 bits. Dans ce cas, le registre FL est immédiatement altéré pour charger 16 bits, au lieu des 8 bits de l'opcode immédiatement précédent. ==Les jeux d'instructions dédiés à un langage de programmation== De rares processeurs sont conçus pour un langage de programmation en particulier. On appelle ces processeurs, conçus pour des besoins particuliers, des '''processeurs dédiés'''. Par exemple, les fameux ''Burrough E-mode'' B5000/B6000/B7000 étaient spécialement conçus pour exécuter de l'ALGOL-60. Leurs cousins B2000/B3000/B4000 étaient eux conçus pour le COBOL. Des langages fonctionnels ont aussi eu droit à leurs processeurs dédiés. Le prolog en est un bel exemple, avec les superordinateurs de 5ème génération qui lui étaient dédié. On peut aussi citer les machines LISP, dédiés au langage LISP, qui datent des années 1970. Elles étaient capables d’exécuter certaines fonctions de base du langage directement dans leurs circuits : elles possédaient notamment un ''garbage collector'' câblé dans ses circuits ainsi que des instructions machines supportant un typage déterminé à l’exécution. Les processeurs dédiés ont eu leur heure de gloire au début de l'informatique, à une époque où les langages de haut niveau venaient d'être inventés. À cette époque, les compilateurs n'étaient pas performants et ne savaient pas bien optimiser le code machine. Il était alors rationnel, pour l'époque, de rapprocher le code machine cible et le langage de programmation de haut niveau. De nombreuses architectures dédiés ont ainsi été inventées, avant que les concepteurs se rendent compte des défauts de cette approche. Les défauts en question ne sont pas nombreux, mais assez simples à comprendre. Premièrement, elles sont très rapides pour un langage de programmation en particulier, mais sont assez mauvaises pour les autres, d'où un problème de "compatibilité". Ajoutons à cela que les langages de programmation peuvent évoluer, devenir de moins en moins populaires/utilisés, ce qui rend la création d'architectures généralistes plus pertinente. Enfin, les architectures dédiées sont évidemment des processeurs CISC, pour implémenter les nombreuses fonctionnalités des langages évolués. Et les défauts des CISC sont assez rédhibitoires à l'heure actuelle. À l'heure actuelle, les algorithmes des compilateurs se sont améliorés et savent nettement mieux utiliser le matériel. Ils produisent du code machine efficace, ce qui rend les architecture dédiées bien moins intéressantes. Si on ajoute les défauts de ces architectures dédiées, par étonnant que les architectures dédiées aient presque disparues. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'accélération matérielle de la virtualisation | prevText=L'accélération matérielle de la virtualisation | next=Le matériel réseau | nextText=Le matériel réseau }} </noinclude> hd3260mozjnc8bidq2qwe064b3hazf1 764059 764058 2026-04-19T20:45:18Z Mewtow 31375 /* La traduction binaire sur le Burroughs B1700 */ 764059 wikitext text/x-wiki De nos jours, la majorité des programmeurs programment dans des langages de haut niveau. Il est très rare pour eux d'avoir à utiliser de l'assembleur, et encore moins un langage machine. Les programmes écrits dans un langage de haut niveau sont traduits en langage machine par un logiciel appelé le compilateur. [[File:Compilation.PNG|centre|vignette|upright=2.5|Compilateur : principe.]] Il se trouve que quelques rares processeurs sont conçus pour faciliter le travail du compilateur. Et ce sont ces processeurs que nous allons voir dans ce qui suit. Ils représentent une catégorie de jeux d'instruction à part, qui n'a pas vraiment de nom. ==La compilation et l’interprétation== Un compilateur traduit donc du code source, écrit dans un langage de haut niveau, vers du langage machine. La traduction est rarement directe. En général, le compilateur traduit le code source en assembleur, qui est lui-même traduit en langage machine. L'assembleur est une représentation textuelle du langage machine alors que le code machine est du binaire exécutable par le processeur. La traduction se fait donc en deux étapes, la '''compilation''' proprement dite et l''''assemblage''', réalisées respectivement par un compilateur et un assembleur. Il faut parfois rajouter une troisième phase d'édition des liens, que nous passons volontairement sous silence. {| |[[File:Code C.png|vignette|200px|Le code source, ici en C.]] |[[File:Assem.png|vignette|250px|Le code assembleur.]] |[[File:Binary file - hello world (C programming).png|vignette|150px|Le code machine.]] |} ===L'interprétation et la compilation à la volée=== Une alternative à la compilation est l''''interprétation''', qui transforme le code source en code machine à la volée. Avec l'interprétation, le code source est passé à un logiciel appelé l''''interpréteur''', qui exécute le code source ligne de code par ligne de code. Une ligne de code est traduite en code machine, qui est exécutée, puis l'interpréteur passe à la ligne suivante. [[File:Kaantotulkkaus.png|centre|vignette|upright=3|Compilation versus interprétation.]] Un défaut de l'interprétation est son cout en performance, qui est assez important. Aussi, l'interprétation stricte a évolué vers un hybride entre interprétation et compilation. L'idée est qu'une partie peu importante du code source est interprétée, alors que le code important est compilé juste avant d'être exécuté. Pour le dire autrement, le code source est partiellement compilé à la volée, juste avant son exécution. Aussi, on parle de '''compilation à la volée'''. Le terme en anglais est ''Just In Time compilation'', abrévié en JIT. En soi, le JIT est associé à de l'interprétation, les JIT sont en réalité des hybrides interpréteurs-compilateurs. Un point important est que compiler du code à la volée est assez lourd, cela a un cout en performance. Par contre, le code compilé s'exécute plus vite que du code interprété. Si on exécute le code compilé une seule fois, le cout de la compilation l'emporte sur le gain à l'exécution. Mais le code compilé est réutilisé autant de fois que nécessaire. Le gain à l'exécution est donc multiplié par le nombre d'exécutions, alors que le cout de la compilation est répartit sur plusieurs exécutions, il est amorti. Les portions du code sont choisies de manière à avoir un gain en performance maximal. L'idée générale est que le code exécuté très souvent est compilé, alors que du code exécuté pas souvent est interprété. Typiquement, les boucles les plus souvent exécutées sont compilées, le code exécuté une seule fois est interprété. ===Les langages intermédiaires=== Les compilateurs modernes passent par un '''langage intermédiaire''' pour faire la transformation en code machine. Le compilateur traduit le langage de haut niveau en langage intermédiaire, puis traduit le langage intermédiaire en code machine cible. Le langage intermédiaire est parfois appelé le '''''bytecode''''', et ce terme recouvre aussi le code écrit avec du ''bytecode''. L'interprétation aussi peut passer par l'intermédiaire d'un un ''bytecode'', ce n'est pas limité aux compilateurs proprement dit. {| |[[File:Intermediate representation scheme.png|centre|vignette|upright=1.5|Représentation intermédiaire d'un compilateur.]] |[[File:Code interpreter scheme.png|centre|vignette|upright=1.5|Fonctionnement d'un interpréteur.]] |} Faire ainsi a de nombreux avantages, le principal étant d'avoir un compilateur capable de traduire un langage de haut niveau vers plusieurs jeux d'instructions différents. Cela permet d'avoir, par exemple, un compilateur qui traduit du C soit en code machine x86, soit en code machine pour un CPU ARM, soit pour un CPU POWERPC, etc. [[File:Compiler design.svg|centre|vignette|upright=3|Compiler design]] Pour l'interprétation, l'usage d'un ''bytecode'' a d'autres avantages. Pour l'exploiter, il faut distribuer non pas le code source, mais le ''bytecode''. En clair, l'interpréteur ne prend pas en entrée le code source, mais du ''bytecode'' déjà compilé. La traduction est alors beaucoup plus simple, car le code source a déjà été partiellement compilé, par la traduction en ''bytecode''. Interpréter du code source est en effet assez compliqué : il faut effectuer des étapes d'analyse lexicale, sémantique, et bien d'autres. Avec du ''bytecode'', ces étapes ont été réalisées lors de la compilation du ''bytecode'', l'interpréteur a alors peut de choses à faire. L'interprétation du ''bytecode'' se fait instruction par instruction, au niveau du ''bytecode''. Une instruction du ''bytecode'' est traduite en une instruction machine équivalente, qui est exécutée. Si l'instruction est un peu complexe, il exécute une fonction/procédure qui fait la même chose. Pour résumer, le ''bytecode'' est ensuite traduit à la volée et exécuté instruction par instruction par un logiciel appelé l'interpréteur. [[File:Control table.png|centre|vignette|upright=2|Control table]] Le langage intermédiaire peut être vu comme l'assembleur d'un processeur, n'existe pas forcément dans la réalité, mais dont le jeu d'instruction est décrit en détail. Le processeur en question est appelé une '''machine abstraite''', ou encore une ''machine virtuelle''. Nous utiliserons le terme de machine abstraite dans ce qui suit. La machine abstraite n'est pas la même suivant que l'on cible la compilation ou l'interprétation/JIT. Pour l'interprétation/JIT, la machine abstraite est souvent une machine à pile, car cela simplifie grandement la traduction en code machine final. L'interprétation demande que la traduction du ''bytecode'' en code machine soit la plus simple possible. Et cela demande de prendre en compte pas mal de détails. Par exemple, les différents jeux d'instruction existants n'ont pas le même nombre de registres, ce qui pose problème lors de la traduction du ''bytecode''. Lors de la transformation en code machine, un algorithme d'allocation de registres se débrouille pour traduire le code intermédiaire en code qui utilise un nombre limité de registres. Pour améliorer cette allocation de registres, il y a deux solutions niveau ''bytecode'' : soit utiliser un nombre illimité de registres, soit utiliser une machine à pile. Pour les machines à pile, il existe un algorithme simple et rapide pour traduire un code écrit pour une machine à pile en un code écrit pour un processeur avec des registres, qui se débrouille pas trop mal pour allouer efficacement les registres. C'est un avantage assez important pour les langages interprétés. ==Les processeurs qui exécutent du bytecode== Le ''bytecode'' est un code machine, ce qui signifie qu'il peut en théorie s'exécuter sur un processeur qui implémente le jeu d’instruction associé. Si le jeu d'instruction d'un ''bytecode'' est souvent une description censée être fictive, elle n'en reste pas moins un jeu d'instruction et des caractéristiques précises, qu'on peut l'implémenter en matériel ! Néanmoins, tous les ''bytecode'' ne sont pas égaux de ce point de vue. Certains sont faciles à implémenter en matériel, d'autres non. Les ''bytecodes'' implémentés en matériel sont ceux dont les machines abstraites sont des machines à pile. Un premier exemple est celui des processeurs Pascal MicroEngine, qui exécutaient directement le bytecode du langage Pascal, le fameux UCSD P-code. Un second exemple est la '''machine SECD''', qui sert de langage intermédiaire pour certains compilateurs de langages fonctionnels. Elle a été implémentée en matériel par plusieurs équipes, notamment par les chercheurs de l'université de Calgary en 1989. Dans le même genre, quelques processeurs simples étaient capables d’exécuter directement le ''bytecode'' du langage FORTH. Le FORTH, un des premiers langages à pile de haut niveau, possède de nombreuses implémentations hardware et est un des rares langages de haut niveau dont le ''bytecode'' a été utilisé comme langage machine sur certains processeurs. Par exemple, on peut citer le processeur FC16, capable d’exécuter nativement du ''bytecode'' FORTH. ===Les processeurs Java=== Le cas le plus impressionnant est celui de la machine virtuelle Java, qui est un design de processeur comme un autre. En temps normal, le ''bytecode'' Java est compilé ou interprété, mais certains processeurs exécutaient du ''bytecode'' Java directement, sans interprétation ni compilation. Ils sont appelés des '''processeurs Java'''. On peut citer les processeurs ARM disposant de l'extension Jazelle, du JEMCore et du aJ-100 de aJile Systems, le picoJava II de Sun, et quelques autres. Il y a aussi eu quelques projets de recherche ou processeurs open source, comme Komodo, jamuth, le ''Java Optimized Processor'' et quelques autres projets du même genre. L'implémentation de la machine virtuelle Java n'était cependant pas complète. Quelques instructions complexes n'étaient pas gérées par le processeur et devaient être émulées en logiciel. Mais la grosse majorité des instructions l'était. Les instructions non-supportées étaient émulées avec la méthode ''trap and emulate'', à savoir qu'un opcode inconnu déclenchait une exception matérielle ''opcode invalide'', dont la routine pouvait émuler les instructions qui doivent l'être. Le processeur aJ-100 de aJile est capable d'exécuter la quasi-totalité des instructions de la JVM Java, seules deux instructions faisant exception. Les '''accélérateurs Java''' sont une solution intermédiaire, qui permet de traduire à la volée du ''bytecode'' Java en instructions RISC, exécutées sur un autre processeur. Il s'agit formellement de traduction binaire, mais je préfère en parler ici. L'idée est de combiner un processeur RISC avec un coprocesseur Java. Le coprocesseur lit le ''bytecode'' Java et le traduit en instructions RISC, ces dernières étant exécutées sur le processeur RISC. Le JA108 de Nozomi était un coprocesseur de ce genre, au même titre que le coprocesseur JSTAR de JEDI. L'extension Jazelle de certains CPU ARM fonctionnait vraisemblablement sur un principe similaire, sauf que le coprocesseur était intégré dans le processeur ARM, entre le cache d'instruction et le décodeur d'instructions ARM. L'intérêt des processeurs Java n'est pas qu'une question de performance. Certes, ils permettent d’exécuter plus vite les programmes compilés en ''bytecode'', comme des programmes Java pour la JVM Java. Mais l'intérêt est aussi de faire des économies de mémoire RAM et ROM. Je rappelle que la machine virtuelle Java est une machine à pile, ce qui fait que sa densité de code est excellente. Les programmes en ''bytecode'' Java sont donc très petits, et prennent peu de place en mémoire ROM. De plus, utiliser un processeur Java permet de se passer d'interpréteur ou de JIT Java, ce qui économise encore plus de mémoire. ===Transformer un processeur RISC en machine à pile=== La plupart des processeurs Java utilisent en interne une architecture à registre généraux, qui émule une machine à registre en microcode. Quelques registres sont réservés pour l'état de la machine virtuelle, avec au minimum un registre réservé pour le pointeur de pile, un autre pour un pointeur vers un ''pool'' de constantes, et quelques autres. D'autres registres servent à mémoriser le haut de la pile, à savoir le sommet de la pile et quelques opérandes situées juste en-dessous. Par exemple, le processeur picoJava II gardait les 64 opérandes au sommet de la pile dans 64 registres généraux, séparés des registres pour les pointeurs et l'état de la JVM. Une optimisation possible, envisagée sur les processeurs picoJava, était une sorte de macro-fusion sous stéroïde. L'idée était de fusionner certaines séries d'instructions Java en une seule instruction machine. Typiquement, pour faire une opération arithmétique, une machine à pile empile deux opérandes et exécute une opération, ce qui prend trois instructions machine. La spécification picoJava 1 a proposé d'envoyer directement la seconde opérande en entrée de l'ALU, ce qui fusionnait l'empilement de la seconde opérande et l'opération arithmétique. Le processeur picoJava II va plus loin et fusionne les trois instructions en une seule instruction RISC, agissant sur des registres. Mais la majorité des processeurs Java ne faisaient pas cela, car cela complexifie grandement le décodeur. Les processeurs ''bigfoot'' de DCT utilisaient un système différent pour émuler une pile avec des registres. Leur banc de registre de 64 bits était coupé en deux : les 16 premiers registres étaient utilisés pour la pile, les 48 suivants étaient utilisés pour autre chose. L'idée est que le registre 0 mémorise un emplacement vide, le registre 1 contient le sommet de la pile, le registre 2 l'opérande sous le sommet de la pile, et ainsi de suite. Les 15 premières opérandes de la pile sont donc mémorisées dans les registres. Les instructions écrivent leur résultat dans le registre adéquat, puis tous les registres sont décalés pour que le résultat passe dans le registre R1. Par exemple, les instructions PUSH chargent une opérande dans le registre 0, puis tous les registres sont décalés de un rang, pour que l'opérande chargée soit dans le registre 1. Le registre 0 devient alors le registre numéro 1, le registre 1 devient le 2, et ainsi de suite. Par contre, les opérations arithmétiques font autrement. Une instruction d'addition, par exemple, lit les opérandes dans le registre 1 et 2, puis place son résultat dans le registre 2 (les deux opérandes sont dépilées). Puis, les registres sont décalés d'un rang pour que ce registre 2 devienne le 1. Les registres ne sont en réalité pas décalés, il n'y a pas de transferts entre registres. A la place, les 16 premiers registres utilisaient une forme limitée de renommage de registres, similaire au banc de registre tournant utilisé sur les processeurs EPIC comme l'Itanium. Les numéros de registres sont renommés, décalés, à chaque fois qu'on empile ou dépile une opérande. Pour faire le renommage de registres, processeur contient un ''register stack counter'' de 4 bits, qui est incrémenté à chaque fois qu'on empile une donnée, et est décrémenté quand on dépile une donnée. Quand on veut accéder à un opérande, le numéro de registre architectural associé est traduit en numéro de registre physique, en additionnant le ''register stack counter'' (modulo 16, pour rester sur 4 bits). ==L'accélération de l'interprétation/JIT : le ''Thumb-EE'' d'ARM== Quelques processeurs ont ajouté des instructions pour faciliter le travail des interpréteurs/JIT. Par exemple, en 2005, ARM a ajouté le mode ''Thumb Execution Environment'', qui faisait cela. Il reprenait le jeu d'instruction compact ''thumb'' et ajoutait quelques instructions et modifiait le comportement d'instructions existantes. Le jeu d'instruction ''Thumb-EE'' a été ajouté sur les CPU ARM en 2005, mais a été déprécié en 2011, par manque d'utilité. Mais la tentative mérite qu'on s'attarde dessus. Les CPU de gamme M, déstinés à l'embarqué, n'ont jamais supporté ''Thumb-EE'', vu que de tels processeurs ne sont pas conçus pour exécuter du code interprété/JIT. Le mode ''Thumb-EE'' est un mode d'exécution, séparé de l'ARM normal. Ainsi, une instruction ''Thumb-EE'' et une instruction ARM peuvent avoir le même encodage en binaire, mais se comporter différemment. Le processeur est à tout moment soit en mode ''Thumb-EE'', soit en mode ARM normal, ce qui précise comment ces instructions doivent se comporter. Des instructions ont été ajoutées par ''Thumb-EE'', pour qu'il fasse son travail. L'entrée et la sortie du mode ''Thumb-EE'' se fait avec deux instructions : ENTERX et LEAVEX. Une fois en mode ''Thumb-EE'', le décodage des instructions ''thumb'' se fait avec les règles du ''Thumb-EE'', et non celles du ''thumb'' normal. Une première différence entre ''thumb'' normal et ''Thumb-EE'' est l'ajout de ce qui s'appelle un ''null check'' pour les instructions mémoire. Le ''null check'' est utilisé pour les instructions en mode d'adressage "base + indice" ou "Base + décalage", ou tout autre mode d'adressage avec un registre de base utilisés dans des calculs d'adresse. L'idée est de vérifier si l'adresse de base vaut zéro ou non. Si c'est le cas, le processeur lève une exception matérielle, qui est traitée par l'interprétation ou le compilateur JIT. Une telle situation est en effet signe d'une erreur d'adressage mémoire, qui doit être traitée par l'interpréteur. Typiquement, c'est signe qu'un pointeur n'a pas bien été initialisé, ce qui peut arriver avec du code interprété ou JIT. Les interpréteurs/JIT traitent généralement la situation en ajoutant un test avant toute instruction mémoire, pour vérifier si le pointeur accédé vaut zéro ou non. Avec ''Thumb-EE'', pas besoin d'ajouter les tests en questions : ils sont réalisés automatiquement lors de chaque instruction mémoire. : Les registres pour la pile, comme le pointeur de pile et le pointeur de ''frame'', sont aussi concernés. L'instruction CHKA vérifie que les accès à un tableau ne débordent pas en dehors du tableau en question. Par exemple, pour un tableau de 1024 éléments, elle vérifie si l'indice est compris entre 0 et 1024. Pour cela, elle vérifie si l'indice de tableau est bien dans l'intervalle adéquat et lève une exception matérielle si ce n'est pas le cas. Son utilité se comprend quand on sait que tous les accès à un tableau sont vérifiés par l'interpréteur. L'interpréteur est censé ajouter des instructions pour vérifier les indices, à savoir deux ou trois branchements. Avec CHKA, le test se fait en une seule instruction. D'autres modifications mineures de l'encodage des instructions sont aussi présentes, ainsi que des modifications pour les instructions LOAD/STORE. Précisément, dans les modes "base + indice" et "base + décalage", le décalage et l'indice subissent maintenant des décalages. L'idée est de simplifier le calcul d'adresse. De quoi économiser quelques instructions lors des calculs d'adresse, ce qui facilite le travail de l'interpréteur/JIT. Et des instructions LOAD/STORE précises voient leur comportement modifié pour faciliter la gestion de la pile de Java ou l'exécution des méthodes locales. Pour faciliter le travail de l'interpréteur, deux instructions HBP et HBLP ont été ajoutées. Elles branchent vers une fonction qui gère les exceptions logicielles (une fonctionnalité présente dans de nombreux langages, comme Java). La première effectue un branchement vers la fonction en question, la seconde sauvegarde l'adresse de retour avant de faire ce branchement. Les deux peuvent brancher vers 256 fonctions pré-déterminées, sans avoir à présenter leur adresse. ==La traduction binaire accélérée par le matériel== Plus haut, nous avons surtout parlé des compilateurs et des interpréteurs. Cependant, nous devons aussi parler de la '''traduction binaire'''. Elle traduit un programme écrit dans un code machine vers un autre code machine. Par exemple, elle traduit un programme compilé pour un CPU x86 vers un code machine ARM. En général, le système d'exploitation est généralement compilé pour le jeu d'instruction natif, mais il exécute des applications prévues pour le x86. Les applications sont traduites par le système d'exploitation, avant d'exécuter le code traduit. : Nous parlerons dans la suite de code machine source et de code machine cible, pour parler respectivement du code à traduire et du code obtenu après traduction. Idem avec d'autres termes comme architecture cible/source, ou jeu d'instruction cible/source. La traduction binaire est surtout utilisée pour des questions d'émulation ou de compatibilité. Par exemple, lorsque les Macintosh sont passés de processeurs Power PC vers des processeurs x86, le système d'exploitation Mac OS utilisait la traduction binaire pour convertir les anciennes applications Power PC vers du code x86. Les utilisateurs n'y ont vu que du feu. Le système de traduction binaire était appelé Rosetta 1. Par la suite, lors de la transition de processeurs x86 vers des processeurs Apple, Rosetta 2 a vu le jour. La traduction binaire a surtout été utilisée pour traduire du code x86 vers un autre jeu d'instruction. Il faut dire que le x86 est le jeu d'instruction dominant. De nombreuses entreprises ont eu pour ambition de briser l'hégémonie du x86 sur PC, en remplaçant le x86 par un jeu d'instruction plus performant, tout en gardant une compatibilité maximale. La traduction binaire était la seule solution pratique. Elle était le plus souvent intégralement réalisée en logiciel, comme c'était le cas sur les architectures Itanium avec le ''IA-32 Execution Layer''. ===Les généralités sur la traduction binaire assistée en matériel=== L'architecture source est presque tout le temps une architecture CISC, assez ancienne, qu'on souhaite émuler. Le choix de l'architecture cible se porte souvent sur une architecture VLIW, fort différente de l'architecture source, et ce pour obtenir une économie de matériel conséquente. Pour s'exécuter rapidement, le code traduit doit exploiter le parallélisme d'instruction, à savoir exécuter plusieurs instructions en même temps dans des unités de calcul séparées. Il est possible d'utiliser un processeur superscalaire avec exécution dans le désordre pour cela, mais au prix d'un cout important en transistors. Alors qu'en utilisant un CPU VLIW, c'est le traducteur binaire qui fait tout le travail d'extraction du parallélisme d'instruction. Et après tout, quitte à avoir un traducteur binaire, autant lui refiler le boulot d'optimisation. Compiler du code à la volée est certes assez lourd, mais qu'il existe des algorithmes efficaces pour regrouper des instructions indépendantes dans une seule instruction VLIW. Au passage, le processeur VLIW a plus de registres que le processeur source. Cela permet de faire du renommage de registres directement en logiciel. Le traducteur binaire n'hésite pas à changer les noms de registres entre instructions source et instruction VLIW cible, afin de supprimer des dépendances de données. En conséquence, cela ouvre des opportunités de parallélisme, qui sont exploitées lors du regroupement des opérations en instructions VLIW. Vous pourriez penser que le choix d'un CPU VLIW pour émuler du CISC est tout sauf optimal. Vous devez penser que la traduction binaire est d'autant plus simple que l'architecture source et cibles sont semblables. Dans les faits, ce n'est pas tellement traduire les instructions qui pose problème, mais plus la gestion du registre d'état, des exceptions matérielles, de la mémoire virtuelle, la différence entre gros-boutisme et petit-boutisme, de même que des différences d'adressage pour les périphériques. Le premier problème est la gestion des conditions, notamment en présence d'un registre d'état. En pratique, la traduction binaire s'utilise pour traduire du code CISC vers du code RISC ou VLIW. Les architectures source ont donc un registre d'état, qui est mis à jour non seulement par des instructions de test, mais aussi des instructions arithmétiques. L'architecture cible n'a elle pas de registre d'état, mais des registres à prédicats. La traduction de l'un vers l'autre est alors quelque peu compliquée. Et elle est d'autant plus compliquée que le registre d'état est la source de dépendances d'instruction implicites, qui réduisent les performances. Par exemple, si une instruction arithmétique modifie le registre d'état, cela peut impacter l'exécution d'une instruction ultérieure, qui lit ce registre d'état. Pour éliminer ces fausses dépendances, le traducteur binaire doit renommer ce registre en logiciel et détecter les dépendances utiles. Et le compilateur doit gérer les cas où il y a beaucoup de distance entre les deux, voire les cas où l'instruction dépendante n'a pas encore été analysée par le compilateur. Un autre problème est lié à la gestion des exceptions matérielles, et précisément des exceptions précises. Pour rappel, une instruction VLIW regroupe plusieurs opérations, opérations qui correspondraient à une instruction machine sur un CPU pas VLIW. En conséquence, plusieurs instructions du langage machine "source" sont regroupées en une seule instruction VLIW. Et il faut tenir compte du cas où une instruction source lève une exception. Dans le code VLIW, cela signifie qu'une opération lève une exception, et il faut annuler partiellement l'instruction VLIW associée. Et par partiellement, on veut dire que seules les opérations suivantes dans l'ordre du programme source doivent être annulées, puis ré-exécutées. Et c'est un sacré casse-tête ! Les processeurs VLIW utilisent comme solution le mécanisme d'exceptions différées des processeurs EPIC, vu il y a quelques chapitres. Pour résumer, le code est exécuté par blocs d'instruction, délimités par des branchements ou tout autre limite/barrière pertinente dans le code. Un bloc de code est compilé en deux versions : une version rapide sans exceptions matérielles, une version lente qui gère les exceptions précises. La version rapide s'exécute sans exécuter les exceptions. Cependant, les exceptions matérielles sont enregistrées, pour être prises en compte à la toute fin du bloc de code. La version rapide est exécutée en premier et elle mémorise si une exception matérielle a eu lieu. Puis une instruction vérifie si une exception a eu lieu et décide quoi faire. Si aucune exception n'a eu lieu, elle passe à la suite du programme, les résultats du bloc de code sont définitivement acceptés. Mais si une exception a eu lieu, tout est annulé. Le processeur est remis dans l'état initial, puis le code est ré-exécuté instruction par instruction de manière à gérer l'exception correctement. ===La traduction par pré-décodage=== Une première solution serait de faire la traduction binaire dans le cache d'instruction. Lors d'un défaut de cache, le code chargé depuis la RAM est traduit en code machine cible. Et c'est ce code machine cible qui est mémorisé dans le cache d'instruction et exécuté par le processeur. La technique marche sur le papier et n'est qu'une amélioration des techniques de pré-décodage vues il y a quelques chapitres. Cependant, le code obtenu est une traduction assez basique, qui n'incorpore pas d'optimisations dignes de ce nom. De plus, elle gère mal le cas où la taille du code source et cible sont potentiellement très différentes. Concrètement, elle est surtout utile pour traduire du code d'un processeur RISC vers un autre processeur RISC, dont les tailles d'instructions sont similaires. Elle ne permet pas d'émuler plusieurs jeux d'instructions différents, le cout en matériel (un circuit de pré-décodage par jeu d'instruction) serait trop important. ===Le projet DAISY d'IBM=== De très rares processeurs étaient conçus pour accélérer cette traduction binaire, afin de garder de bonnes performances. Les premiers à avoir étudié l'idée étaient IBM, avec leur projet DAISY (''Dynamically Architected Instruction Set from Yorktown''). Le projet de base était de convertir à la volée du code compilé pour des CPU Power PC, vers du code VLIW. Les chercheurs d'IBM avaient développé un algorithme de traduction dynamique efficace, ainsi qu'un processeur VLIW disposant d'optimisations spécifiques à la traduction binaire dynamique, à la volée. De nombreuses idées de ce projet ont été reprises ou re-découvertes par la société Transmetta, avec ses processeurs Crusoe et Efficieon, puis par NVIDIA avec son projet Denver. Les ingénieurs de ce projet ont étudié la possibilité d'émuler plusieurs jeux d'instructions différents sur un même processeur, notamment le s390 d'IBM et le x86. En théorie, cela demande juste d'avoir plusieurs programmes de traduction binaire : un pour le x86, un autre pour le s390, éventuellement un autre pour le Power PC. Mais cela ne s'est pas concrétisé. Mais les ingénieurs ont étudié quelle pourrait être l'architecture VLIW idéale pour ça. Par exemple, il fallait des additions 3-opérandes pour simplifier les calculs d'adresse. Une difficulté était la gestion du registre d'état, dont les bits ne sont pas mis à jour de la même manière sur le x86 et le s390 ou le Power PC. La différence entre gros-boutisme et petit-boutisme était aussi un problème, de même que des différences d'adressage pour les périphériques. Pour que la traduction binaire soit efficace, le processeur VLIW intègre diverses optimisations, comme les exceptions différées et les branchements multi-voies. La principale est la suivante : seul le code des boucles ou fonctions exécutées fréquemment est traduit en code VLIW, le reste du code est interprété. En effet, traduire du code binaire prend plus de temps qu'une simple interprétation. Pour du code qui ne sera exécutée qu'une seule fois, il est plus rapide d'utiliser l'interprétation. Par contre, pour du code exécuté beaucoup de fois, le cout de la traduction binaire est amorti, dilué sur N exécutions, compensé par le gain en temps d'exécution de ce code traduit. Au final, cela permet de ne traduire que le code qui le mérite. La détection du code fréquemment exécuté est réalisée dans le cœur VLIW. L'unité de branchement mémorise les derniers branchements rencontrés et le nombre de fois qu'ils ont été exécutés. Si ils ont été exécutés un certain nombre de fois, l'unité de branchement lève une exception matérielle, qui invoque le traducteur binaire. La routine de cette exception est le traducteur binaire proprement dit. En clair, la traduction binaire est démarrée quand le processeur détecte qu'une fonction a été exécutée plus de N fois, via une exception matérielle dédiée. La mesure du nombre d'exécution d'un branchement se fait dans l'unité de calcul dédiée aux branchements, avec l'aide d'une sorte de ''branch adress buffer'' modifié. Pour rappel, le ''branch adress buffer'' mémorise le ''Program counter'' de chaque branchement récemment rencontré. Ici, chaque entrée du ''branch adress buffer'' est associée à un compteur incrémenté à chaque exécution du branchement. ===Les processeurs Crusoe et Efficieon de Transmetta=== Les processeurs Crusoe Et Efficeron sont deux processeurs VLIW produits par la société Transmetta, une société californienne rachetée par NVIDIA. Ils étaient conçus pour exécuter spécifiquement des programmes x86, système d'exploitation inclus. L'idée derrière ce projet était d'exécuter du code x86 sans que les applications, ni même le système d'exploitation et le BIOS soient au courant ! Les processeurs Transmetta étaient en réalité conçus pour exécuter un programme unique, le ''Code Morphing Software'' (CMS), qui traduisait le code x86 en code VLIW. Transmetta ne voulait pas que son processeur VLIW soit exploité directement, sans le CMS. Il n'avait pas rendu public de compilateur pour traduire du C vers du code VLIW, il n'avait pas donné la documentation du jeu d'instruction VLIW. Des efforts de rétro-ingénieurie, documentés sur le site realworldtech, ont cependant permis de comprendre comment étaient encodées les instructions du processeur Crusoe. Comme pour le projet Daisy, le ''Code Morphing Software'' utilisait à la fois interprétation et traduction binaire, selon les besoins. Les instructions sont interprétées lors de leurs premières exécutions, mais le CMS bascule sur de la traduction en code VLIW après un certain nombre d'exécution. Ainsi, les boucles souvent exécutées sont traduites en code VLIW, alors que le reste du code est interprété. Le choix du code à traduire est le fait du CMS, il utilise des heuristiques complexes pour, qui ne sont pas connues dans le détail. Le ''Code Morphing Software'' est mémorisé dans une EEPROM, ce qui en fait un ''firmware'' situé en dessous du BIOS. Le CMS démarre ensuite le BIOS, qui lui-même démarre le système d'exploitation, qui lui-même démarre les pilotes de périphériques et les programmes. Le CMS se réserve les 16 premiers mébioctets de l'espace d'adressage. L'EEPROM du CMS est mappée dedans, mais n'en utilise que la moitié. Le reste est utilisé comme cache, pour mémoriser le code VLIW traduit par le CMS. Les deux processeurs intégraient aussi deux mémoires SRAM, utilisées par le CMS, appelées la ''local program memory'' (LPM) et la ''local data memory'' (LDM). La première contient du code qui gére les interruptions, la mémoire virtuelle, les exceptions matérielles, les problèmes d'alignement mémoire, et quelques fonctions très fréquemment utilisées par le CMS. La seconde est de la mémoire RAM utilisée par ces fonctions de la ''local program memory''. Les mémoires caches L1 et L2 sont séparées de ces deux mémoires. Le processeur Crusoe et Efficieon étaient des processeurs VLIW très simples. Ils n'avaient même pas de MMU, ni de fonctionnalités importantes sur les CPU x86. Pour Crusoe, les instructions VLIW étaient encodées sur 64 ou 128 bits, et regroupaient entre 2 à 4 opérations. Les instructions LOAD/STORE ne supportaient pas d'adressage indicé, juste de l'adressage indirect à registre. Crusoe avait 5 unités de calcul : deux ALU entières, une unité LOAD/STORE, une unité de branchement, une FPU. Efficieon doublait le nombre d'ALU entière et d'unité LOAD/STORE, les instructions VLIW passaient à 256 bits. Le processeur Crusoe contient 160 registres, dont 64 registres généraux, 32 registres flottants. Les registres généraux font 32 bits, ce qui est cohérent avec le fait que les CPU x86 de l'époque étaient des processeurs 32 bits. Quant aux registres flottants, ils faisaient 80 bits, ce qui colle avec la taille des registres flottants de la FPU x87 utilisée à l'époque. Sur les 64 registres généraux, seuls 48 étaient réellement utilisable pour mémoriser des opérandes. Une partie des registres généraux étaient utilisés pour la gestion de la pile, d'autres pour mémoriser l'état du CPU x86 émulé, un registre était un registre zéro non accesible en écriture. La gestion des exceptions est optimisée avec un système d'exceptions différées, le même que celui décrit plus haut. Le processeur utilise un système similaire, pour effectuer des lectures anticipées, qui a été expliqué dans le chapitre sur les processeurs VLIW/EPIC. Sauf qu'il s'agit là d'une optimisation pour gagner en performances en général, pas une aide utile pour gérer des exceptions précises ou toute autre subtilité de la traduction binaire. Pour gérer les exceptions différées et les lectures anticipées, le processeur mémorise l'état du processeur avant de démarrer un bloc de code. Pour cela, le processeur contient des ''shadow registers'', 48 registres entiers et 16 registres flottants. Vous remarquerez qu'il y a autant de registres entiers que de ''shadow registers'' entiers. Les registres entiers/flottants sont mémorisés dans les ''shadow registers'' avant d'exécuter un bloc de code, afin que le processeur puisse revenir à l'état de base. Une autre fonctionnalité liée est le ''gated store buffer''. Le principe est d'éviter toute écriture en mémoire RAM, tant que le code VLIW n'a pas émis d'instruction ''commit''. L'idée est que les écritures sont conservées dans la ''store queue'' de l'unité LOAD/STORE. Pour rappel, la ''store queue'' met en attente les écritures soit tant que les écritures ne sont pas terminées, soit tant que la RAM est occupée. L'idée est que les écritures sont accumulées dans la ''store queue'' tant que l'instruction ''commit'' n'a pas eu lieu. Le ''store queue'' est "déversé dans la RAM" seulement quand l'instruction ''commit'' s'exécute. ===Les CPU NVIDIA Denver=== Nvidia a tenté quelque chose de similaire avec son projet Denver. NVIDIA a racheté l'entreprise Transmetta, ses brevets, et a tenté de refaire la même chose. Le projet initial était de traduire du code x86 vers un jeu d'instruction VLIW propriétaire, comme l'a fait Transmetta. Mais NVIDIA n'a pas réussi à acquérir la licence du jeu d'instruction x86 et s'est rabattu sur le jeu d'instruction ARM. Le processeur né de ce projet est le Tegra K1-64 CPU. Il s'agit d'un processeur composé d'un cœur VLIW, couplé à deux décodeurs d'instructions ARM. Les deux décodeurs ARM traduisent une paire d'instructions ARM consécutives en instructions VLIW, pour les exécuter sur le cœur VLIW. Le CPU peut fonctionner selon deux modes : ARM et VLIW. En somme, le processeur supporte deux jeux d'instruction : le jeu d'instruction ARM, et un jeu d'instruction VLIW. En mode ARM, les instructions ARM sont chargées depuis le cache, traduites en VLIW par les décodeurs ARM, puis exécutées par le cœur VLIW. En mode optimisé, le CPU exécute des instructions VLIW chargées depuis le cache d'instruction, les décodeurs ne sont pas utilisés. : Le CPU supporte en réalité deux jeux d'instruction ARM : ARM8 et 7. Mais ils correspondent au même mode pour le CPU. Un point important est que le processeur peut changer très rapidement de mode, en à peine quelques cycles d'horloges. On n'est clairement pas dans le cas des CPU x86, qui mettent des plombes pour passer du mode 32 à 64 bits et inversement. La commutation est tellement rapide qu'on peut considérer que le processeur supporte deux jeux d'instruction simultanément. Et le support simultané de deux jeux d'instruction facilite l'implémentation de la traduction binaire. Encore une fois, inutile de traduire du code qui ne sera exécutée qu'une seule fois, mieux vaut l'exécuter directement dans le mode ARM. Surtout que le processeur supporte un mode ARM qui permet d'exécuter du code ARM sans perte de performance. Par contre, le code fréquemment utilisé, notamment dans des boucles critiques, est traduit en VLIW. Au final, les performances sont optimisées, en limitant le travail du logiciel traducteur binaire. La détection du code fréquemment exécuté se fait différemment que pour les processeurs Transmetta et Daisy. L'unité de branchement ne détecte pas directement le code beaucoup exécuté, même si elle a un rôle à jouer. A la place, la détection est réalisée en logiciel, par un ''thread'' dédié, qui s'exécute sur un cœur séparé. Il détecte le code fréquemment utilisé en regardent les branchements exécutés récemment. Pour cela, le processeur mémorise l'historique des branchements pris récemment et copie cette historique dans une mémoire tampon dédiée, partagée entre tous les cœurs. Vu qu'elle est partagée, le ''thread'' de détection a accès à l'historique sans pertes de performances, seule la copie de l'historique dans ce tampon a un cout en performance, pas son partage. Le code VLIW obtenu après traduction binaire est mémorisé dans la mémoire RAM, dans une portion spécialement réservée pour. Elle fait 128 mébioctets, et est appelée le '''cache d'optimisation''' par NVIDIA. Le cache d'optimisation est protégé en écriture et seul le traducteur binaire peut écrire dedans. Au passage, le traducteur binaire est du code VLIW, ce qui fait qu'il est placé dans le cache d'optimisation. Le processeur détecte automatiquement quand une fonction pour laquelle le code compilé équivalent est disponible. Pour cela, l'unité de chargement contient une table de correspondance entre l'adresse de la fonction ARM, et l'adresse de son équivalent compilé. Quand une fonction est appelée, l'unité de chargement regarde l'adresse de destination du branchement. Si l'adresse est dans cette table, elle récupère l'adresse de la fonction compilée et branche vers celle-ci. Le processeur dispose d'un cache d'instruction de 128 kibioctets, ce qui est très important, mais nécessaire vu la taille des instructions VLIW. Le coeur VLIW dispose de ses propres décodeurs, d'un ''scoreboard'', de 7 unités de calcul et de registres. Pour les unités de calcul, il a deux FPU, deux ALU, une unité de branchement, et deux unités LOAD-STORE faisant aussi ALU. Un point important est que le CPU dispose de 64 registres entiers et de 64 registres flottants. C'est deux fois plus que supporte le jeu d'instruction ARM. En mode ARM, seule la moitié des registres est utilisée. Mais en mode optimisé, le traducteur binaire utilise bien les 64 registres, grâce à une sorte de renommage de registres logicielle. Pour simplifier la traduction binaire, le processeur supporte les techniques vues précédemment. Le processeur intègre un ''gated store buffer'' similaire à celui des processeurs Transmetta. Les techniques d'exceptions différées sont aussi supportées, comme sur les processeurs Transmetta. Il a aussi une unité de préchargement décrite par NVIDIA comme agressive, avec support du préchargement de type anticipé (''runahead''). ===L'émulation de la mémoire virtuelle=== Pour finir, il faut parler de la mémoire virtuelle avec la traduction binaire. Sur le processeur source, le processeur gère à la fois des adresses physiques et virtuelles et sa MMU fait la conversion entre les deux. Sur le processeur VLIW, la MMU est simulée par le programme de traduction binaire, partiellement ou totalement. L'implémentation exacte varie suivant que l'on parle du projet Daisy, des processeurs Transmetta ou de Denver. Sur les processeurs Transmetta, il n'y a pas de MMU ni de mémoire virtuelle. A la place, le programme de traduction binaire émule la mémoire virtuelle du processeur source. Le processeur VLIW ne gère que des adresses physiques, rien d'autre. L'espace d'adressage physique du processeur VLIW a la même taille que celui du processeur émulé, ici des processeurs x86. Il est vraisemblable que les adresses physiques utilisées par le processeur x86 sont les mêmes que celles du processeur VLIW. Sur le projet Daisy, le processeur VLIW gère la mémoire virtuelle via pagination, comme les processeurs émulés. Ce qui fait qu'il gère un espace d'adressage virtuel et un espace d'adressage physique. Pour éviter toute confusion, nous parlerons d'adresse physique/virtuelle VLIW pour les adresses physiques/virtuelles du processeur VLIW, d'adresse virtuelle/physique source pour celle du jeu d'instruction traduit, à savoir du Power PC ou du s390, éventuellement de l'x86. Il faut alors faire le lien entre adresses physiques source et adresses virtuelles VLIW. Pour cela, rien de plus simple : il y a correspondance parfaite. L'adresse physique source numéro N correspond à l'adresse virtuelle VLIW numéro N. Cependant, cela signifie que tout l'espace d'adressage virtuel VLIW serait utilisé par le code à traduire. En réalité, il faut ajouter de la place pour le code traduit et le programme de traduction binaire. La conséquence est que l'espace d'adressage virtuel VLIW est plus large que l'espace d'adressage source. L'espace d'adressage virtuel VLIW est découpé en trois sections : une section pour le code Power PC à traduire, une autre pour le code traduit en VLIW, et une dernière réservée au traducteur binaire. Le programme de traduction est placé dans une ROM mappée en mémoire dans la seconde section. Le reste de la seconde section est réservé à une mémoire RAM utilisée par le programme de traduction. [[File:Mémoire virtuelle sur le projet Daisy.png|centre|vignette|upright=1.5|Mémoire virtuelle sur le projet Daisy]] Il faut noter que chaque page physique source, une fois traduite, correspond à N pages virtuelles VLIW. La raison est que le code traduit prend plus de place que le code machine originel. En conséquence, on doit utiliser une page finale plus grande. Pour le reste, des pages contiguës en mémoire physique source sont elles aussi contiguës en mémoire virtuelle VLIW. Le calcul d'adresse est donc simple : l'adresse physique source est multipliée par N, puis on ajoute l'adresse de base à laquelle commence la section pour le code traduit. ==La traduction binaire sur le Burroughs B1700== Nous venons de voir comment accélérer en matériel la traduction binaire, avec des techniques bien spécifiques. Le Burroughs B1700 a procédé autrement et il est intéressant d'étudier en détail son architecture. Son architecture est décrite dans le livre "Interpreting Machines : Architecture and Programming of the Bl700/Bl800 Series", écrit par Elliott I. Organick et James A. Hinds. Une partie de ce qui vfa suivre est un très court résumé de ce livre. ===Le Burroughs B1700 : le mal-nommé ''split-level control store''=== Le Burroughs B1700 avait plusieurs modèles : le B1726, le B1710 et le B1800. Ils se distinguaient par la manière dont le code machine source était traduit par le traducteur binaire. Dans ce qui suit, nous parlerons de traducteur binaire pour parler soit d'un interpréteur pour un langage de programmation de haut niveau, soit pour un logiciel de traduction binaire. Le B1710 plaquait le traducteur binaire en mémoire RAM. Le B1800 faisait pareil, mais avait ajouté un cache dédié au code du traducteur binaire, afin d'améliorer les performances. Le B1726 avait un traducteur binaire scindé en deux : une partie était en mémoire RAM, l'autre était dans un ''local store'' dédié. Le processeur peut exécuter le traducteur binaire soit depuis le ''local store'', soit depuis la mémoire RAM avec une pénalité en termes de performances. La documentation appelle cela un '''''split-level control store''''', mais le terme est trompeur : la documentation qualifie de microcode le langage machine du Burroughs B1700, ce qui est à l'origine de beaucoup de confusions. Une autre source de confusion est que ce langage machine a quelques propriétés qu'on pourrait attendre d'un microcode, mais certaines sont manquantes. Par exemple, il peut adresser les registres d’interfaçage mémoire, mais c'est parce que ce sont des registres architecturaux sur ce processeur. Ses instructions n'encodent pas de signaux de commande, par contre, elles font 16 bits et adressent les opérandes de manière implicite. Le ''local store'' mémorisait au maximum 2048 instructions machine, ce qui fait que les 2048 premières "adresses" correspondaient au ''local store''. Les suivantes étaient des adresses de mémoire RAM qui contenaient le reste du traducteur binaire. En pratique, l'adresse qui séparait les deux était configurable ! On pouvait limiter la taille du ''local store'' à un multiple de 32 instructions ! Les adresses restantes étaient alors réattribuées à la RAM. Par exemple, on pouvait attribuer 64 adresses au ''local store'', les 2048 - 64 adresses restantes étaient alors attribuées à la mémoire RAM. Un registre limite, nommé TDPM, indiquait quelle était l'adresse de démarcation entre les deux. Il contenait 4 bits, dont la valeur était multipliée par 32, pour obtenir l'adresse limite. Le programmeur devait découper le traducteur binaire en segments, certains étant placé dans le ''local store'', les autres étant en mémoire RAM. Mieux que ça, le processeur autorisait d'utiliser la technique de l'''overlaying'' pour le microcode ! Il y avait même une fonction OVERLAY dédiée pour ! OVERLAY avait besoin de trois opérandes : la taille du segment à copier, son adresse source en mémoire RAM, son adresse de destination dans la micro-SRAM. L'interpréteur réservait un "segment" en mémoire RAM, pour les données, dont le processeur connaissait la position. En-dehors de ce segment, il n'y a que des instructions machines à traduire, et le traducteur binaire. Le CPU incorporait pour cela deux registres, un pour l'adresse de base du segment de données, un autre pour son adresse de fin. Il incorporait une protection mémoire limitée sur ce segment, à savoir que seul l'interpréteur pouvait lire ou écrire dans ce segment. ===L'ALU de taille variable=== Les Burroughs B1700 avaient une '''ALU de taille variable''', ce qui veut dire qu'elle pouvait faire des calculs sur un nombre de bits compris entre 0 et 24. Pour cela, les calculs étaient fait avec une ALU de 24 bits, puis les bits de poids fort inutiles étaient masqués. L'ALU de 24 bits pouvait masquer les bits de poids fort du résultat, mais avec des limitations. Elle gérait des opérandes et résultats de 0, 4, 8, 12, 16, 20 et 24 bits. Pour avoir un réglage plus fin, une ALU 4 bits séparée permettait de corriger le résultat. Les registres X et Y étaient reliés à une ALU de 24 bits capable d'effectuer des opérations arithmétiques basiques. Fait étonnant, l'ALU faisait tous les calculs en même temps et fournissait ses résultats dans 7 registres : un registre SUM pour l'addition, un registre DIFFERENCE pour la soustraction, et un registre par opération bit à bit. Les registres pour les opérations bit à bit sont : CMPX et CMPY pour l'opération NOT sur chaque opérande, XANY pour le ET bit à bit, XEOY pour le XOR, XORY pour le OU logique. Les additions et soustractions pouvaient se faire sur des opérandes codés en binaire ou en BCD, suivant le code opération utilisé. A cela, il fallait ajouter deux registres de "résultat" MSKX et MSKY. Ils fournissaient un masque dépendant de la taille du résultat en bits. C'est ce registre qui permettait de gérer des données de 0, 4, 8, 12, 16, 20, et 24 bits, alors que l'ALU faisait 24 bits. Le masque dans ce registre pouvait servir d'opérande dans une opération de masquage ultérieure, pour corriger le résultat. L'ALU prenait en opérande les registres X et Y, mais aussi un registre CYF pour la retenue entrante. En sortie, il fournissait aussi deux retenue sortantes : une pour l'addition et une autre pour la soustraction. Elles étaient mémorisées dans les registres CYL et CYD, respectivement. L'ALU fournissait aussi 12 conditions, qui alimentait un registre d'état. Le registre d'état était segmenté en trois registres appelés XYST, XYCN et BICN. Les registres X et Y sont des registres pour les opérandes, mais il y a aussi deux autres registres de 24 bits. Il s'agit des registres T et L, qui sont des registres généraux. Les registres T et L étaient découpés en 6 sous-registres de 4 bits, nommés LA LB LC LD LE et LF pour le registre L, et TA TB TC TD TE et TF pour le registre T. Ils étaient adressables comme n'importe quel registre. Le processeur contenait de nombreux autres registres de 4 bits, dont les sous-registres de T et L. En tout, il y a 27 registres de 4 bits adressables ! Et le processeur disposait d'une ALU 4 bits pour manipuler ces registres de 4 bits ! Elle est capable de faire des MOV entre registres de 4 bits, des opérations bit à bit, et peut aussi tester si un bit vaut 0 ou 1. La dernière possibilité est très utile pour implémenter les branchements. De plus, cela permettait de faire des opérations de masquage, notamment pour appliquer un masquage supplémentaire à celui de l'ALU. Par exemple, pour gérer des opérations sur 14 bits, on effectue une opération sur 16 bits avec l'ALU, et on masque les 2 bits manquants avec l'ALU 4 bits. ===Une architecture bit-adressable=== Le CPU Burroughs B1700 était '''bit-adressable'''. En clair, il pouvait adresser la mémoire bit par bit, si nécessaire. Un avantage est que cela facilitait la traduction binaire des instructions machine, mais nous détaillerons cela plus bas. Un autre avantage est que cela permettait d'émuler des architectures dont la taille des registres/''byte''/mots était très différente. Et cet avantage mérite quelques explications immédiates. Le processeur était un CPU 24 bits, mais il pouvait émuler des processeurs 16 bits, 8 bits, 4 bits, 1 bit, 9 bits, ou toute autre valeur. Pour cela, le processeur lisait des mots de 24 bits et utilisant un ''barrel shifter'' pour sélectionner les bits adéquats. Par exemple, pour un accès de 16 bits, la donnée lue en mémoire était masquée de manière à ne garder que les 16 bits de poids faible. Le ''barrel shifter''/circuit de masquage était placé directement avant le bus mémoire, sur le trajet des données/instructions. Le ''barrel shifter'' était commandé par un registre ''Field Unit'', qui précisait quelle était la taille des données à charger. Pour communiquer avec la mémoire, il avait trois registres séparés : READ, WRITE, MAR. READ contient la donnée lue lors d'une lecture, WRITE est pour une donnée à écrire lors d’une écriture, MAR contient l'adresse à lire ou en cours de lecture. Il s'agit de registres d’interfaçage mémoire. Le fait que ces registres d’interfaçage mémoire et IO soit adressable est peu commun, et c'est plus quelque chose qu'on attend d'un microcode que d'instructions machines. Mais il faut bien comprendre que les registres d’interfaçage mémoire ont été promus au rang de registres architecturaux, sur ce processeur. De même, pour communiquer avec les entrées-sorties, il disposait de deux registres CMND et DATA : CMND pour envoyer une commande à une entrée-sortie, DATA pour lire ou écrire une donnée. Il y avait aussi un registre U, qui mémorisait une donnée lue depuis le lecteur de cassette. ===Le code machine du Burroughs B1700=== Le processeur utilise des instructions de 16 bits de long, qui sont copiées dans registre d'instruction M de 16 bits. Le ''program counter'' est appelé le registre A. Les instructions du processeur font 16 bits de long, et elles sont alignées en RAM sur 16 bits, ce qui fait que les 4 bits de poids fort de ce registre valent 0, seuls les 14 bits de poids fort sont utiles. Les adresses mentionnées plus haut, qui adressent les micro-opérations en mémoire RAM, sont des adresses de mots de 16 bits, pas les adresses de bit individuels. Fait intéressant, il était possible de faire un OU logique entre l'instruction lue et un registre du processeur, le résultat étant mémorisé dans le registre M. Cela permettait de faire du code automodifiant, pour émuler des modes d'adressage complexe. Cela permettait par exemple de remplacer une adresse dans une micro-opération. L'adresse est alors surimposée avec un OU dans le champ d'adresse, alors que le reste de l'instruction n'est pas modifié. Afin de gérer les interruptions, ces dernières étaient partiellement détectées en logiciel. Une interruption doit être prise en compte à la toute fin de l'exécution d'une instruction source, quand celle-ci a finit son travail. Mais vu qu'ici, les instructions sont émulées avec des séries d'instructions cibles, il y a un problème : il est possible d'interrompre une instruction émulée en plein milieu. Pour éviter cela, le processeur ne déclenche pas les interruptions matérielles immédiatement, elles sont mises en attente. Une instruction dédiée vérifie si une interruption est en attente, et l'exécute alors cas échéant. L'instruction en question est utilisée en dernier, dans la suite d'instruction qui émule l'instruction source. Les Burroughs B1700 intégrait une pile d'adresse de retour, capable de mémoriser 16 adresses de 24 bits. Il disposait de 4 registres généraux, nommés X, Y, T et L, tous de 24 bits. Un point important est que le registre T avait des possibilités d’extraction spécifiques. Il était possible d'extraire N bits, placés n'importe où dans ce registre, et de les copier dans un autre registre. Le CPU supportait une '''instruction EXTRACT''' pour ça. Nous verrons pourquoi cette possibilité est intéressante et ne sert que pour le registre T. ===La traduction binaire sur le Burroughs B1700=== Les instructions source peuvent faire 16 bit avec tel jeu d'instruction, 8 bits avec un autre, 32 bits sur un autre, etc. Elles peuvent aussi être de longueur fixe ou variable ! Le B1700 utilisait une architecture bit-adressable pour gérer ces contraintes. En effet, avec un CPU bit-adressable, les contraintes d'alignement des instructions disparaissent ! Dans ce qui suit, on suppose que l'instruction est composée de '''champs''' : l'opcode, les opérandes, les adresses absolues, les constantes immédiates, les numéros de registre, etc. L'idée est que l'instruction source est chargée dans le registre T, puis découpée en champs, qui sont copiés dans les registres du processeur ou envoyés au microcode. Je rappelle que le registre T est relié à un ''barrel shifter'', différent au précédent, qui permettait de masquer, décaler et extraire une donnée de ce registre. Et il y a la possibilité d'extraire une suite de bit de ce registre T, pour la copier dans un autre registre. Par exemple, prenons une instruction composée d'un opcode, d'une constante immédiate et d'une adresse immédiate. Premièrement, elle est chargée dans le registre T. Deuxièmement, la constante immédiate est extraite du registre T et copiée dans le registre X. Troisièmement, l'adresse est extraite et est copiée dans le registre d’interfaçage mémoire, une lecture est démarrée, et l'opérande lue est copiée dans le registre Y. Enfin, l'instruction équivalente à l'opcode est exécutée par le microcode. Le tout prend plusieurs instructions : une première pour lire l'opcode, une seconde pour lire la première opérande, une troisième pour lire la seconde opérande, una quatrième pour tout exécuter. Notons que suivant la taille des champs, on peut en charger plusieurs à la fois. Par exemple, imaginons qu'on ait chargé 24 bits, contenant un opcode de 8 bits et une constante immédiate de 16 bits : le registre T est découpé en un opcode et une constante, avec deux instructions EXTRACT. L'architecture est bit-adressable car elle gère des champs qui ne font pas forcément 8 ou 16 bits. Par exemple, certains jeux d'instructions ont des opcodes codés sur 6 ou 7 bits, voire 9-10 bits. Utiliser une architecture bit-adressable règle ce problème d'alignement. On peut lire un champ depuis la mémoire RAM en précisant l'adresse de son premier bit, pas besoin de gérer des accès non-alignés. Par exemple, prenons une instruction source avec un opcode de 7 bits, suivi par une adresse de 12 bits et une constante immédiate de 15 bits. On peut charger 7 + 12 bits en un premier accès, pour extraite l'opcode et l'adresse, suivi par un second accès de 15 bits pour extraire la constante immédiate. Pour charger un champ, il faut connaitre plusieurs informations : l'adresse de son premier bit, sa taille. Pour cela, le B1700 contient deux registres, nommés ''Field Adress'' (FA) et ''Field Length'' (FL), qui indiquent l'adresse du premier bit et la taille de l'instruction à charger. Le premier fait 24 bits, ce qui permet de gérer des adresses de 24 bits. N'oubliez pas que le processeur est bit-adressable, ce qui permet d'adresser 16 Mébi-bits, soit 2 mébioctets. Le registre pour la taille fait lui 16 bits, ce qui permet de gérer des instructions de 65536 bits, soit 8192 octets ! Leur nom commence par ''field'', car le processeur peut gérer des instructions de taille variable et/ou plus longues que 24 bits. Dans ce cas, le chargement de l'instruction se fait champ par champ. Pour donner un exemple, prenons une instruction composée de 20 champs de 12 bits chacun. Les champs sont chargés un par un dans le registre T. Le registre FA est alors initialisé avec l'adresse de l'instruction, il est incrémenté de 12 à chaque cycle. Le registre FL est initialisé avec la taille de l'instruction, soit 20 * 12 = 240 bits, et il est décrémenté de 16 à chaque cycle. Précisons cependant que la taille d'un champ peut être changée d'un cycle à l'autre. Par exemple, le processeur peut charger un opcode de 8 bits, l'interpréter, détecter que l'instruction demande ensuite de charger une adresse de 16 bits. Dans ce cas, le registre FL est immédiatement altéré pour charger 16 bits, au lieu des 8 bits de l'opcode immédiatement précédent. ==Les jeux d'instructions dédiés à un langage de programmation== De rares processeurs sont conçus pour un langage de programmation en particulier. On appelle ces processeurs, conçus pour des besoins particuliers, des '''processeurs dédiés'''. Par exemple, les fameux ''Burrough E-mode'' B5000/B6000/B7000 étaient spécialement conçus pour exécuter de l'ALGOL-60. Leurs cousins B2000/B3000/B4000 étaient eux conçus pour le COBOL. Des langages fonctionnels ont aussi eu droit à leurs processeurs dédiés. Le prolog en est un bel exemple, avec les superordinateurs de 5ème génération qui lui étaient dédié. On peut aussi citer les machines LISP, dédiés au langage LISP, qui datent des années 1970. Elles étaient capables d’exécuter certaines fonctions de base du langage directement dans leurs circuits : elles possédaient notamment un ''garbage collector'' câblé dans ses circuits ainsi que des instructions machines supportant un typage déterminé à l’exécution. Les processeurs dédiés ont eu leur heure de gloire au début de l'informatique, à une époque où les langages de haut niveau venaient d'être inventés. À cette époque, les compilateurs n'étaient pas performants et ne savaient pas bien optimiser le code machine. Il était alors rationnel, pour l'époque, de rapprocher le code machine cible et le langage de programmation de haut niveau. De nombreuses architectures dédiés ont ainsi été inventées, avant que les concepteurs se rendent compte des défauts de cette approche. Les défauts en question ne sont pas nombreux, mais assez simples à comprendre. Premièrement, elles sont très rapides pour un langage de programmation en particulier, mais sont assez mauvaises pour les autres, d'où un problème de "compatibilité". Ajoutons à cela que les langages de programmation peuvent évoluer, devenir de moins en moins populaires/utilisés, ce qui rend la création d'architectures généralistes plus pertinente. Enfin, les architectures dédiées sont évidemment des processeurs CISC, pour implémenter les nombreuses fonctionnalités des langages évolués. Et les défauts des CISC sont assez rédhibitoires à l'heure actuelle. À l'heure actuelle, les algorithmes des compilateurs se sont améliorés et savent nettement mieux utiliser le matériel. Ils produisent du code machine efficace, ce qui rend les architecture dédiées bien moins intéressantes. Si on ajoute les défauts de ces architectures dédiées, par étonnant que les architectures dédiées aient presque disparues. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'accélération matérielle de la virtualisation | prevText=L'accélération matérielle de la virtualisation | next=Le matériel réseau | nextText=Le matériel réseau }} </noinclude> o5aiuyinz5r4hlhed6z52m3j0p9rr8t 764060 764059 2026-04-19T20:45:43Z Mewtow 31375 /* L'ALU de taille variable */ 764060 wikitext text/x-wiki De nos jours, la majorité des programmeurs programment dans des langages de haut niveau. Il est très rare pour eux d'avoir à utiliser de l'assembleur, et encore moins un langage machine. Les programmes écrits dans un langage de haut niveau sont traduits en langage machine par un logiciel appelé le compilateur. [[File:Compilation.PNG|centre|vignette|upright=2.5|Compilateur : principe.]] Il se trouve que quelques rares processeurs sont conçus pour faciliter le travail du compilateur. Et ce sont ces processeurs que nous allons voir dans ce qui suit. Ils représentent une catégorie de jeux d'instruction à part, qui n'a pas vraiment de nom. ==La compilation et l’interprétation== Un compilateur traduit donc du code source, écrit dans un langage de haut niveau, vers du langage machine. La traduction est rarement directe. En général, le compilateur traduit le code source en assembleur, qui est lui-même traduit en langage machine. L'assembleur est une représentation textuelle du langage machine alors que le code machine est du binaire exécutable par le processeur. La traduction se fait donc en deux étapes, la '''compilation''' proprement dite et l''''assemblage''', réalisées respectivement par un compilateur et un assembleur. Il faut parfois rajouter une troisième phase d'édition des liens, que nous passons volontairement sous silence. {| |[[File:Code C.png|vignette|200px|Le code source, ici en C.]] |[[File:Assem.png|vignette|250px|Le code assembleur.]] |[[File:Binary file - hello world (C programming).png|vignette|150px|Le code machine.]] |} ===L'interprétation et la compilation à la volée=== Une alternative à la compilation est l''''interprétation''', qui transforme le code source en code machine à la volée. Avec l'interprétation, le code source est passé à un logiciel appelé l''''interpréteur''', qui exécute le code source ligne de code par ligne de code. Une ligne de code est traduite en code machine, qui est exécutée, puis l'interpréteur passe à la ligne suivante. [[File:Kaantotulkkaus.png|centre|vignette|upright=3|Compilation versus interprétation.]] Un défaut de l'interprétation est son cout en performance, qui est assez important. Aussi, l'interprétation stricte a évolué vers un hybride entre interprétation et compilation. L'idée est qu'une partie peu importante du code source est interprétée, alors que le code important est compilé juste avant d'être exécuté. Pour le dire autrement, le code source est partiellement compilé à la volée, juste avant son exécution. Aussi, on parle de '''compilation à la volée'''. Le terme en anglais est ''Just In Time compilation'', abrévié en JIT. En soi, le JIT est associé à de l'interprétation, les JIT sont en réalité des hybrides interpréteurs-compilateurs. Un point important est que compiler du code à la volée est assez lourd, cela a un cout en performance. Par contre, le code compilé s'exécute plus vite que du code interprété. Si on exécute le code compilé une seule fois, le cout de la compilation l'emporte sur le gain à l'exécution. Mais le code compilé est réutilisé autant de fois que nécessaire. Le gain à l'exécution est donc multiplié par le nombre d'exécutions, alors que le cout de la compilation est répartit sur plusieurs exécutions, il est amorti. Les portions du code sont choisies de manière à avoir un gain en performance maximal. L'idée générale est que le code exécuté très souvent est compilé, alors que du code exécuté pas souvent est interprété. Typiquement, les boucles les plus souvent exécutées sont compilées, le code exécuté une seule fois est interprété. ===Les langages intermédiaires=== Les compilateurs modernes passent par un '''langage intermédiaire''' pour faire la transformation en code machine. Le compilateur traduit le langage de haut niveau en langage intermédiaire, puis traduit le langage intermédiaire en code machine cible. Le langage intermédiaire est parfois appelé le '''''bytecode''''', et ce terme recouvre aussi le code écrit avec du ''bytecode''. L'interprétation aussi peut passer par l'intermédiaire d'un un ''bytecode'', ce n'est pas limité aux compilateurs proprement dit. {| |[[File:Intermediate representation scheme.png|centre|vignette|upright=1.5|Représentation intermédiaire d'un compilateur.]] |[[File:Code interpreter scheme.png|centre|vignette|upright=1.5|Fonctionnement d'un interpréteur.]] |} Faire ainsi a de nombreux avantages, le principal étant d'avoir un compilateur capable de traduire un langage de haut niveau vers plusieurs jeux d'instructions différents. Cela permet d'avoir, par exemple, un compilateur qui traduit du C soit en code machine x86, soit en code machine pour un CPU ARM, soit pour un CPU POWERPC, etc. [[File:Compiler design.svg|centre|vignette|upright=3|Compiler design]] Pour l'interprétation, l'usage d'un ''bytecode'' a d'autres avantages. Pour l'exploiter, il faut distribuer non pas le code source, mais le ''bytecode''. En clair, l'interpréteur ne prend pas en entrée le code source, mais du ''bytecode'' déjà compilé. La traduction est alors beaucoup plus simple, car le code source a déjà été partiellement compilé, par la traduction en ''bytecode''. Interpréter du code source est en effet assez compliqué : il faut effectuer des étapes d'analyse lexicale, sémantique, et bien d'autres. Avec du ''bytecode'', ces étapes ont été réalisées lors de la compilation du ''bytecode'', l'interpréteur a alors peut de choses à faire. L'interprétation du ''bytecode'' se fait instruction par instruction, au niveau du ''bytecode''. Une instruction du ''bytecode'' est traduite en une instruction machine équivalente, qui est exécutée. Si l'instruction est un peu complexe, il exécute une fonction/procédure qui fait la même chose. Pour résumer, le ''bytecode'' est ensuite traduit à la volée et exécuté instruction par instruction par un logiciel appelé l'interpréteur. [[File:Control table.png|centre|vignette|upright=2|Control table]] Le langage intermédiaire peut être vu comme l'assembleur d'un processeur, n'existe pas forcément dans la réalité, mais dont le jeu d'instruction est décrit en détail. Le processeur en question est appelé une '''machine abstraite''', ou encore une ''machine virtuelle''. Nous utiliserons le terme de machine abstraite dans ce qui suit. La machine abstraite n'est pas la même suivant que l'on cible la compilation ou l'interprétation/JIT. Pour l'interprétation/JIT, la machine abstraite est souvent une machine à pile, car cela simplifie grandement la traduction en code machine final. L'interprétation demande que la traduction du ''bytecode'' en code machine soit la plus simple possible. Et cela demande de prendre en compte pas mal de détails. Par exemple, les différents jeux d'instruction existants n'ont pas le même nombre de registres, ce qui pose problème lors de la traduction du ''bytecode''. Lors de la transformation en code machine, un algorithme d'allocation de registres se débrouille pour traduire le code intermédiaire en code qui utilise un nombre limité de registres. Pour améliorer cette allocation de registres, il y a deux solutions niveau ''bytecode'' : soit utiliser un nombre illimité de registres, soit utiliser une machine à pile. Pour les machines à pile, il existe un algorithme simple et rapide pour traduire un code écrit pour une machine à pile en un code écrit pour un processeur avec des registres, qui se débrouille pas trop mal pour allouer efficacement les registres. C'est un avantage assez important pour les langages interprétés. ==Les processeurs qui exécutent du bytecode== Le ''bytecode'' est un code machine, ce qui signifie qu'il peut en théorie s'exécuter sur un processeur qui implémente le jeu d’instruction associé. Si le jeu d'instruction d'un ''bytecode'' est souvent une description censée être fictive, elle n'en reste pas moins un jeu d'instruction et des caractéristiques précises, qu'on peut l'implémenter en matériel ! Néanmoins, tous les ''bytecode'' ne sont pas égaux de ce point de vue. Certains sont faciles à implémenter en matériel, d'autres non. Les ''bytecodes'' implémentés en matériel sont ceux dont les machines abstraites sont des machines à pile. Un premier exemple est celui des processeurs Pascal MicroEngine, qui exécutaient directement le bytecode du langage Pascal, le fameux UCSD P-code. Un second exemple est la '''machine SECD''', qui sert de langage intermédiaire pour certains compilateurs de langages fonctionnels. Elle a été implémentée en matériel par plusieurs équipes, notamment par les chercheurs de l'université de Calgary en 1989. Dans le même genre, quelques processeurs simples étaient capables d’exécuter directement le ''bytecode'' du langage FORTH. Le FORTH, un des premiers langages à pile de haut niveau, possède de nombreuses implémentations hardware et est un des rares langages de haut niveau dont le ''bytecode'' a été utilisé comme langage machine sur certains processeurs. Par exemple, on peut citer le processeur FC16, capable d’exécuter nativement du ''bytecode'' FORTH. ===Les processeurs Java=== Le cas le plus impressionnant est celui de la machine virtuelle Java, qui est un design de processeur comme un autre. En temps normal, le ''bytecode'' Java est compilé ou interprété, mais certains processeurs exécutaient du ''bytecode'' Java directement, sans interprétation ni compilation. Ils sont appelés des '''processeurs Java'''. On peut citer les processeurs ARM disposant de l'extension Jazelle, du JEMCore et du aJ-100 de aJile Systems, le picoJava II de Sun, et quelques autres. Il y a aussi eu quelques projets de recherche ou processeurs open source, comme Komodo, jamuth, le ''Java Optimized Processor'' et quelques autres projets du même genre. L'implémentation de la machine virtuelle Java n'était cependant pas complète. Quelques instructions complexes n'étaient pas gérées par le processeur et devaient être émulées en logiciel. Mais la grosse majorité des instructions l'était. Les instructions non-supportées étaient émulées avec la méthode ''trap and emulate'', à savoir qu'un opcode inconnu déclenchait une exception matérielle ''opcode invalide'', dont la routine pouvait émuler les instructions qui doivent l'être. Le processeur aJ-100 de aJile est capable d'exécuter la quasi-totalité des instructions de la JVM Java, seules deux instructions faisant exception. Les '''accélérateurs Java''' sont une solution intermédiaire, qui permet de traduire à la volée du ''bytecode'' Java en instructions RISC, exécutées sur un autre processeur. Il s'agit formellement de traduction binaire, mais je préfère en parler ici. L'idée est de combiner un processeur RISC avec un coprocesseur Java. Le coprocesseur lit le ''bytecode'' Java et le traduit en instructions RISC, ces dernières étant exécutées sur le processeur RISC. Le JA108 de Nozomi était un coprocesseur de ce genre, au même titre que le coprocesseur JSTAR de JEDI. L'extension Jazelle de certains CPU ARM fonctionnait vraisemblablement sur un principe similaire, sauf que le coprocesseur était intégré dans le processeur ARM, entre le cache d'instruction et le décodeur d'instructions ARM. L'intérêt des processeurs Java n'est pas qu'une question de performance. Certes, ils permettent d’exécuter plus vite les programmes compilés en ''bytecode'', comme des programmes Java pour la JVM Java. Mais l'intérêt est aussi de faire des économies de mémoire RAM et ROM. Je rappelle que la machine virtuelle Java est une machine à pile, ce qui fait que sa densité de code est excellente. Les programmes en ''bytecode'' Java sont donc très petits, et prennent peu de place en mémoire ROM. De plus, utiliser un processeur Java permet de se passer d'interpréteur ou de JIT Java, ce qui économise encore plus de mémoire. ===Transformer un processeur RISC en machine à pile=== La plupart des processeurs Java utilisent en interne une architecture à registre généraux, qui émule une machine à registre en microcode. Quelques registres sont réservés pour l'état de la machine virtuelle, avec au minimum un registre réservé pour le pointeur de pile, un autre pour un pointeur vers un ''pool'' de constantes, et quelques autres. D'autres registres servent à mémoriser le haut de la pile, à savoir le sommet de la pile et quelques opérandes situées juste en-dessous. Par exemple, le processeur picoJava II gardait les 64 opérandes au sommet de la pile dans 64 registres généraux, séparés des registres pour les pointeurs et l'état de la JVM. Une optimisation possible, envisagée sur les processeurs picoJava, était une sorte de macro-fusion sous stéroïde. L'idée était de fusionner certaines séries d'instructions Java en une seule instruction machine. Typiquement, pour faire une opération arithmétique, une machine à pile empile deux opérandes et exécute une opération, ce qui prend trois instructions machine. La spécification picoJava 1 a proposé d'envoyer directement la seconde opérande en entrée de l'ALU, ce qui fusionnait l'empilement de la seconde opérande et l'opération arithmétique. Le processeur picoJava II va plus loin et fusionne les trois instructions en une seule instruction RISC, agissant sur des registres. Mais la majorité des processeurs Java ne faisaient pas cela, car cela complexifie grandement le décodeur. Les processeurs ''bigfoot'' de DCT utilisaient un système différent pour émuler une pile avec des registres. Leur banc de registre de 64 bits était coupé en deux : les 16 premiers registres étaient utilisés pour la pile, les 48 suivants étaient utilisés pour autre chose. L'idée est que le registre 0 mémorise un emplacement vide, le registre 1 contient le sommet de la pile, le registre 2 l'opérande sous le sommet de la pile, et ainsi de suite. Les 15 premières opérandes de la pile sont donc mémorisées dans les registres. Les instructions écrivent leur résultat dans le registre adéquat, puis tous les registres sont décalés pour que le résultat passe dans le registre R1. Par exemple, les instructions PUSH chargent une opérande dans le registre 0, puis tous les registres sont décalés de un rang, pour que l'opérande chargée soit dans le registre 1. Le registre 0 devient alors le registre numéro 1, le registre 1 devient le 2, et ainsi de suite. Par contre, les opérations arithmétiques font autrement. Une instruction d'addition, par exemple, lit les opérandes dans le registre 1 et 2, puis place son résultat dans le registre 2 (les deux opérandes sont dépilées). Puis, les registres sont décalés d'un rang pour que ce registre 2 devienne le 1. Les registres ne sont en réalité pas décalés, il n'y a pas de transferts entre registres. A la place, les 16 premiers registres utilisaient une forme limitée de renommage de registres, similaire au banc de registre tournant utilisé sur les processeurs EPIC comme l'Itanium. Les numéros de registres sont renommés, décalés, à chaque fois qu'on empile ou dépile une opérande. Pour faire le renommage de registres, processeur contient un ''register stack counter'' de 4 bits, qui est incrémenté à chaque fois qu'on empile une donnée, et est décrémenté quand on dépile une donnée. Quand on veut accéder à un opérande, le numéro de registre architectural associé est traduit en numéro de registre physique, en additionnant le ''register stack counter'' (modulo 16, pour rester sur 4 bits). ==L'accélération de l'interprétation/JIT : le ''Thumb-EE'' d'ARM== Quelques processeurs ont ajouté des instructions pour faciliter le travail des interpréteurs/JIT. Par exemple, en 2005, ARM a ajouté le mode ''Thumb Execution Environment'', qui faisait cela. Il reprenait le jeu d'instruction compact ''thumb'' et ajoutait quelques instructions et modifiait le comportement d'instructions existantes. Le jeu d'instruction ''Thumb-EE'' a été ajouté sur les CPU ARM en 2005, mais a été déprécié en 2011, par manque d'utilité. Mais la tentative mérite qu'on s'attarde dessus. Les CPU de gamme M, déstinés à l'embarqué, n'ont jamais supporté ''Thumb-EE'', vu que de tels processeurs ne sont pas conçus pour exécuter du code interprété/JIT. Le mode ''Thumb-EE'' est un mode d'exécution, séparé de l'ARM normal. Ainsi, une instruction ''Thumb-EE'' et une instruction ARM peuvent avoir le même encodage en binaire, mais se comporter différemment. Le processeur est à tout moment soit en mode ''Thumb-EE'', soit en mode ARM normal, ce qui précise comment ces instructions doivent se comporter. Des instructions ont été ajoutées par ''Thumb-EE'', pour qu'il fasse son travail. L'entrée et la sortie du mode ''Thumb-EE'' se fait avec deux instructions : ENTERX et LEAVEX. Une fois en mode ''Thumb-EE'', le décodage des instructions ''thumb'' se fait avec les règles du ''Thumb-EE'', et non celles du ''thumb'' normal. Une première différence entre ''thumb'' normal et ''Thumb-EE'' est l'ajout de ce qui s'appelle un ''null check'' pour les instructions mémoire. Le ''null check'' est utilisé pour les instructions en mode d'adressage "base + indice" ou "Base + décalage", ou tout autre mode d'adressage avec un registre de base utilisés dans des calculs d'adresse. L'idée est de vérifier si l'adresse de base vaut zéro ou non. Si c'est le cas, le processeur lève une exception matérielle, qui est traitée par l'interprétation ou le compilateur JIT. Une telle situation est en effet signe d'une erreur d'adressage mémoire, qui doit être traitée par l'interpréteur. Typiquement, c'est signe qu'un pointeur n'a pas bien été initialisé, ce qui peut arriver avec du code interprété ou JIT. Les interpréteurs/JIT traitent généralement la situation en ajoutant un test avant toute instruction mémoire, pour vérifier si le pointeur accédé vaut zéro ou non. Avec ''Thumb-EE'', pas besoin d'ajouter les tests en questions : ils sont réalisés automatiquement lors de chaque instruction mémoire. : Les registres pour la pile, comme le pointeur de pile et le pointeur de ''frame'', sont aussi concernés. L'instruction CHKA vérifie que les accès à un tableau ne débordent pas en dehors du tableau en question. Par exemple, pour un tableau de 1024 éléments, elle vérifie si l'indice est compris entre 0 et 1024. Pour cela, elle vérifie si l'indice de tableau est bien dans l'intervalle adéquat et lève une exception matérielle si ce n'est pas le cas. Son utilité se comprend quand on sait que tous les accès à un tableau sont vérifiés par l'interpréteur. L'interpréteur est censé ajouter des instructions pour vérifier les indices, à savoir deux ou trois branchements. Avec CHKA, le test se fait en une seule instruction. D'autres modifications mineures de l'encodage des instructions sont aussi présentes, ainsi que des modifications pour les instructions LOAD/STORE. Précisément, dans les modes "base + indice" et "base + décalage", le décalage et l'indice subissent maintenant des décalages. L'idée est de simplifier le calcul d'adresse. De quoi économiser quelques instructions lors des calculs d'adresse, ce qui facilite le travail de l'interpréteur/JIT. Et des instructions LOAD/STORE précises voient leur comportement modifié pour faciliter la gestion de la pile de Java ou l'exécution des méthodes locales. Pour faciliter le travail de l'interpréteur, deux instructions HBP et HBLP ont été ajoutées. Elles branchent vers une fonction qui gère les exceptions logicielles (une fonctionnalité présente dans de nombreux langages, comme Java). La première effectue un branchement vers la fonction en question, la seconde sauvegarde l'adresse de retour avant de faire ce branchement. Les deux peuvent brancher vers 256 fonctions pré-déterminées, sans avoir à présenter leur adresse. ==La traduction binaire accélérée par le matériel== Plus haut, nous avons surtout parlé des compilateurs et des interpréteurs. Cependant, nous devons aussi parler de la '''traduction binaire'''. Elle traduit un programme écrit dans un code machine vers un autre code machine. Par exemple, elle traduit un programme compilé pour un CPU x86 vers un code machine ARM. En général, le système d'exploitation est généralement compilé pour le jeu d'instruction natif, mais il exécute des applications prévues pour le x86. Les applications sont traduites par le système d'exploitation, avant d'exécuter le code traduit. : Nous parlerons dans la suite de code machine source et de code machine cible, pour parler respectivement du code à traduire et du code obtenu après traduction. Idem avec d'autres termes comme architecture cible/source, ou jeu d'instruction cible/source. La traduction binaire est surtout utilisée pour des questions d'émulation ou de compatibilité. Par exemple, lorsque les Macintosh sont passés de processeurs Power PC vers des processeurs x86, le système d'exploitation Mac OS utilisait la traduction binaire pour convertir les anciennes applications Power PC vers du code x86. Les utilisateurs n'y ont vu que du feu. Le système de traduction binaire était appelé Rosetta 1. Par la suite, lors de la transition de processeurs x86 vers des processeurs Apple, Rosetta 2 a vu le jour. La traduction binaire a surtout été utilisée pour traduire du code x86 vers un autre jeu d'instruction. Il faut dire que le x86 est le jeu d'instruction dominant. De nombreuses entreprises ont eu pour ambition de briser l'hégémonie du x86 sur PC, en remplaçant le x86 par un jeu d'instruction plus performant, tout en gardant une compatibilité maximale. La traduction binaire était la seule solution pratique. Elle était le plus souvent intégralement réalisée en logiciel, comme c'était le cas sur les architectures Itanium avec le ''IA-32 Execution Layer''. ===Les généralités sur la traduction binaire assistée en matériel=== L'architecture source est presque tout le temps une architecture CISC, assez ancienne, qu'on souhaite émuler. Le choix de l'architecture cible se porte souvent sur une architecture VLIW, fort différente de l'architecture source, et ce pour obtenir une économie de matériel conséquente. Pour s'exécuter rapidement, le code traduit doit exploiter le parallélisme d'instruction, à savoir exécuter plusieurs instructions en même temps dans des unités de calcul séparées. Il est possible d'utiliser un processeur superscalaire avec exécution dans le désordre pour cela, mais au prix d'un cout important en transistors. Alors qu'en utilisant un CPU VLIW, c'est le traducteur binaire qui fait tout le travail d'extraction du parallélisme d'instruction. Et après tout, quitte à avoir un traducteur binaire, autant lui refiler le boulot d'optimisation. Compiler du code à la volée est certes assez lourd, mais qu'il existe des algorithmes efficaces pour regrouper des instructions indépendantes dans une seule instruction VLIW. Au passage, le processeur VLIW a plus de registres que le processeur source. Cela permet de faire du renommage de registres directement en logiciel. Le traducteur binaire n'hésite pas à changer les noms de registres entre instructions source et instruction VLIW cible, afin de supprimer des dépendances de données. En conséquence, cela ouvre des opportunités de parallélisme, qui sont exploitées lors du regroupement des opérations en instructions VLIW. Vous pourriez penser que le choix d'un CPU VLIW pour émuler du CISC est tout sauf optimal. Vous devez penser que la traduction binaire est d'autant plus simple que l'architecture source et cibles sont semblables. Dans les faits, ce n'est pas tellement traduire les instructions qui pose problème, mais plus la gestion du registre d'état, des exceptions matérielles, de la mémoire virtuelle, la différence entre gros-boutisme et petit-boutisme, de même que des différences d'adressage pour les périphériques. Le premier problème est la gestion des conditions, notamment en présence d'un registre d'état. En pratique, la traduction binaire s'utilise pour traduire du code CISC vers du code RISC ou VLIW. Les architectures source ont donc un registre d'état, qui est mis à jour non seulement par des instructions de test, mais aussi des instructions arithmétiques. L'architecture cible n'a elle pas de registre d'état, mais des registres à prédicats. La traduction de l'un vers l'autre est alors quelque peu compliquée. Et elle est d'autant plus compliquée que le registre d'état est la source de dépendances d'instruction implicites, qui réduisent les performances. Par exemple, si une instruction arithmétique modifie le registre d'état, cela peut impacter l'exécution d'une instruction ultérieure, qui lit ce registre d'état. Pour éliminer ces fausses dépendances, le traducteur binaire doit renommer ce registre en logiciel et détecter les dépendances utiles. Et le compilateur doit gérer les cas où il y a beaucoup de distance entre les deux, voire les cas où l'instruction dépendante n'a pas encore été analysée par le compilateur. Un autre problème est lié à la gestion des exceptions matérielles, et précisément des exceptions précises. Pour rappel, une instruction VLIW regroupe plusieurs opérations, opérations qui correspondraient à une instruction machine sur un CPU pas VLIW. En conséquence, plusieurs instructions du langage machine "source" sont regroupées en une seule instruction VLIW. Et il faut tenir compte du cas où une instruction source lève une exception. Dans le code VLIW, cela signifie qu'une opération lève une exception, et il faut annuler partiellement l'instruction VLIW associée. Et par partiellement, on veut dire que seules les opérations suivantes dans l'ordre du programme source doivent être annulées, puis ré-exécutées. Et c'est un sacré casse-tête ! Les processeurs VLIW utilisent comme solution le mécanisme d'exceptions différées des processeurs EPIC, vu il y a quelques chapitres. Pour résumer, le code est exécuté par blocs d'instruction, délimités par des branchements ou tout autre limite/barrière pertinente dans le code. Un bloc de code est compilé en deux versions : une version rapide sans exceptions matérielles, une version lente qui gère les exceptions précises. La version rapide s'exécute sans exécuter les exceptions. Cependant, les exceptions matérielles sont enregistrées, pour être prises en compte à la toute fin du bloc de code. La version rapide est exécutée en premier et elle mémorise si une exception matérielle a eu lieu. Puis une instruction vérifie si une exception a eu lieu et décide quoi faire. Si aucune exception n'a eu lieu, elle passe à la suite du programme, les résultats du bloc de code sont définitivement acceptés. Mais si une exception a eu lieu, tout est annulé. Le processeur est remis dans l'état initial, puis le code est ré-exécuté instruction par instruction de manière à gérer l'exception correctement. ===La traduction par pré-décodage=== Une première solution serait de faire la traduction binaire dans le cache d'instruction. Lors d'un défaut de cache, le code chargé depuis la RAM est traduit en code machine cible. Et c'est ce code machine cible qui est mémorisé dans le cache d'instruction et exécuté par le processeur. La technique marche sur le papier et n'est qu'une amélioration des techniques de pré-décodage vues il y a quelques chapitres. Cependant, le code obtenu est une traduction assez basique, qui n'incorpore pas d'optimisations dignes de ce nom. De plus, elle gère mal le cas où la taille du code source et cible sont potentiellement très différentes. Concrètement, elle est surtout utile pour traduire du code d'un processeur RISC vers un autre processeur RISC, dont les tailles d'instructions sont similaires. Elle ne permet pas d'émuler plusieurs jeux d'instructions différents, le cout en matériel (un circuit de pré-décodage par jeu d'instruction) serait trop important. ===Le projet DAISY d'IBM=== De très rares processeurs étaient conçus pour accélérer cette traduction binaire, afin de garder de bonnes performances. Les premiers à avoir étudié l'idée étaient IBM, avec leur projet DAISY (''Dynamically Architected Instruction Set from Yorktown''). Le projet de base était de convertir à la volée du code compilé pour des CPU Power PC, vers du code VLIW. Les chercheurs d'IBM avaient développé un algorithme de traduction dynamique efficace, ainsi qu'un processeur VLIW disposant d'optimisations spécifiques à la traduction binaire dynamique, à la volée. De nombreuses idées de ce projet ont été reprises ou re-découvertes par la société Transmetta, avec ses processeurs Crusoe et Efficieon, puis par NVIDIA avec son projet Denver. Les ingénieurs de ce projet ont étudié la possibilité d'émuler plusieurs jeux d'instructions différents sur un même processeur, notamment le s390 d'IBM et le x86. En théorie, cela demande juste d'avoir plusieurs programmes de traduction binaire : un pour le x86, un autre pour le s390, éventuellement un autre pour le Power PC. Mais cela ne s'est pas concrétisé. Mais les ingénieurs ont étudié quelle pourrait être l'architecture VLIW idéale pour ça. Par exemple, il fallait des additions 3-opérandes pour simplifier les calculs d'adresse. Une difficulté était la gestion du registre d'état, dont les bits ne sont pas mis à jour de la même manière sur le x86 et le s390 ou le Power PC. La différence entre gros-boutisme et petit-boutisme était aussi un problème, de même que des différences d'adressage pour les périphériques. Pour que la traduction binaire soit efficace, le processeur VLIW intègre diverses optimisations, comme les exceptions différées et les branchements multi-voies. La principale est la suivante : seul le code des boucles ou fonctions exécutées fréquemment est traduit en code VLIW, le reste du code est interprété. En effet, traduire du code binaire prend plus de temps qu'une simple interprétation. Pour du code qui ne sera exécutée qu'une seule fois, il est plus rapide d'utiliser l'interprétation. Par contre, pour du code exécuté beaucoup de fois, le cout de la traduction binaire est amorti, dilué sur N exécutions, compensé par le gain en temps d'exécution de ce code traduit. Au final, cela permet de ne traduire que le code qui le mérite. La détection du code fréquemment exécuté est réalisée dans le cœur VLIW. L'unité de branchement mémorise les derniers branchements rencontrés et le nombre de fois qu'ils ont été exécutés. Si ils ont été exécutés un certain nombre de fois, l'unité de branchement lève une exception matérielle, qui invoque le traducteur binaire. La routine de cette exception est le traducteur binaire proprement dit. En clair, la traduction binaire est démarrée quand le processeur détecte qu'une fonction a été exécutée plus de N fois, via une exception matérielle dédiée. La mesure du nombre d'exécution d'un branchement se fait dans l'unité de calcul dédiée aux branchements, avec l'aide d'une sorte de ''branch adress buffer'' modifié. Pour rappel, le ''branch adress buffer'' mémorise le ''Program counter'' de chaque branchement récemment rencontré. Ici, chaque entrée du ''branch adress buffer'' est associée à un compteur incrémenté à chaque exécution du branchement. ===Les processeurs Crusoe et Efficieon de Transmetta=== Les processeurs Crusoe Et Efficeron sont deux processeurs VLIW produits par la société Transmetta, une société californienne rachetée par NVIDIA. Ils étaient conçus pour exécuter spécifiquement des programmes x86, système d'exploitation inclus. L'idée derrière ce projet était d'exécuter du code x86 sans que les applications, ni même le système d'exploitation et le BIOS soient au courant ! Les processeurs Transmetta étaient en réalité conçus pour exécuter un programme unique, le ''Code Morphing Software'' (CMS), qui traduisait le code x86 en code VLIW. Transmetta ne voulait pas que son processeur VLIW soit exploité directement, sans le CMS. Il n'avait pas rendu public de compilateur pour traduire du C vers du code VLIW, il n'avait pas donné la documentation du jeu d'instruction VLIW. Des efforts de rétro-ingénieurie, documentés sur le site realworldtech, ont cependant permis de comprendre comment étaient encodées les instructions du processeur Crusoe. Comme pour le projet Daisy, le ''Code Morphing Software'' utilisait à la fois interprétation et traduction binaire, selon les besoins. Les instructions sont interprétées lors de leurs premières exécutions, mais le CMS bascule sur de la traduction en code VLIW après un certain nombre d'exécution. Ainsi, les boucles souvent exécutées sont traduites en code VLIW, alors que le reste du code est interprété. Le choix du code à traduire est le fait du CMS, il utilise des heuristiques complexes pour, qui ne sont pas connues dans le détail. Le ''Code Morphing Software'' est mémorisé dans une EEPROM, ce qui en fait un ''firmware'' situé en dessous du BIOS. Le CMS démarre ensuite le BIOS, qui lui-même démarre le système d'exploitation, qui lui-même démarre les pilotes de périphériques et les programmes. Le CMS se réserve les 16 premiers mébioctets de l'espace d'adressage. L'EEPROM du CMS est mappée dedans, mais n'en utilise que la moitié. Le reste est utilisé comme cache, pour mémoriser le code VLIW traduit par le CMS. Les deux processeurs intégraient aussi deux mémoires SRAM, utilisées par le CMS, appelées la ''local program memory'' (LPM) et la ''local data memory'' (LDM). La première contient du code qui gére les interruptions, la mémoire virtuelle, les exceptions matérielles, les problèmes d'alignement mémoire, et quelques fonctions très fréquemment utilisées par le CMS. La seconde est de la mémoire RAM utilisée par ces fonctions de la ''local program memory''. Les mémoires caches L1 et L2 sont séparées de ces deux mémoires. Le processeur Crusoe et Efficieon étaient des processeurs VLIW très simples. Ils n'avaient même pas de MMU, ni de fonctionnalités importantes sur les CPU x86. Pour Crusoe, les instructions VLIW étaient encodées sur 64 ou 128 bits, et regroupaient entre 2 à 4 opérations. Les instructions LOAD/STORE ne supportaient pas d'adressage indicé, juste de l'adressage indirect à registre. Crusoe avait 5 unités de calcul : deux ALU entières, une unité LOAD/STORE, une unité de branchement, une FPU. Efficieon doublait le nombre d'ALU entière et d'unité LOAD/STORE, les instructions VLIW passaient à 256 bits. Le processeur Crusoe contient 160 registres, dont 64 registres généraux, 32 registres flottants. Les registres généraux font 32 bits, ce qui est cohérent avec le fait que les CPU x86 de l'époque étaient des processeurs 32 bits. Quant aux registres flottants, ils faisaient 80 bits, ce qui colle avec la taille des registres flottants de la FPU x87 utilisée à l'époque. Sur les 64 registres généraux, seuls 48 étaient réellement utilisable pour mémoriser des opérandes. Une partie des registres généraux étaient utilisés pour la gestion de la pile, d'autres pour mémoriser l'état du CPU x86 émulé, un registre était un registre zéro non accesible en écriture. La gestion des exceptions est optimisée avec un système d'exceptions différées, le même que celui décrit plus haut. Le processeur utilise un système similaire, pour effectuer des lectures anticipées, qui a été expliqué dans le chapitre sur les processeurs VLIW/EPIC. Sauf qu'il s'agit là d'une optimisation pour gagner en performances en général, pas une aide utile pour gérer des exceptions précises ou toute autre subtilité de la traduction binaire. Pour gérer les exceptions différées et les lectures anticipées, le processeur mémorise l'état du processeur avant de démarrer un bloc de code. Pour cela, le processeur contient des ''shadow registers'', 48 registres entiers et 16 registres flottants. Vous remarquerez qu'il y a autant de registres entiers que de ''shadow registers'' entiers. Les registres entiers/flottants sont mémorisés dans les ''shadow registers'' avant d'exécuter un bloc de code, afin que le processeur puisse revenir à l'état de base. Une autre fonctionnalité liée est le ''gated store buffer''. Le principe est d'éviter toute écriture en mémoire RAM, tant que le code VLIW n'a pas émis d'instruction ''commit''. L'idée est que les écritures sont conservées dans la ''store queue'' de l'unité LOAD/STORE. Pour rappel, la ''store queue'' met en attente les écritures soit tant que les écritures ne sont pas terminées, soit tant que la RAM est occupée. L'idée est que les écritures sont accumulées dans la ''store queue'' tant que l'instruction ''commit'' n'a pas eu lieu. Le ''store queue'' est "déversé dans la RAM" seulement quand l'instruction ''commit'' s'exécute. ===Les CPU NVIDIA Denver=== Nvidia a tenté quelque chose de similaire avec son projet Denver. NVIDIA a racheté l'entreprise Transmetta, ses brevets, et a tenté de refaire la même chose. Le projet initial était de traduire du code x86 vers un jeu d'instruction VLIW propriétaire, comme l'a fait Transmetta. Mais NVIDIA n'a pas réussi à acquérir la licence du jeu d'instruction x86 et s'est rabattu sur le jeu d'instruction ARM. Le processeur né de ce projet est le Tegra K1-64 CPU. Il s'agit d'un processeur composé d'un cœur VLIW, couplé à deux décodeurs d'instructions ARM. Les deux décodeurs ARM traduisent une paire d'instructions ARM consécutives en instructions VLIW, pour les exécuter sur le cœur VLIW. Le CPU peut fonctionner selon deux modes : ARM et VLIW. En somme, le processeur supporte deux jeux d'instruction : le jeu d'instruction ARM, et un jeu d'instruction VLIW. En mode ARM, les instructions ARM sont chargées depuis le cache, traduites en VLIW par les décodeurs ARM, puis exécutées par le cœur VLIW. En mode optimisé, le CPU exécute des instructions VLIW chargées depuis le cache d'instruction, les décodeurs ne sont pas utilisés. : Le CPU supporte en réalité deux jeux d'instruction ARM : ARM8 et 7. Mais ils correspondent au même mode pour le CPU. Un point important est que le processeur peut changer très rapidement de mode, en à peine quelques cycles d'horloges. On n'est clairement pas dans le cas des CPU x86, qui mettent des plombes pour passer du mode 32 à 64 bits et inversement. La commutation est tellement rapide qu'on peut considérer que le processeur supporte deux jeux d'instruction simultanément. Et le support simultané de deux jeux d'instruction facilite l'implémentation de la traduction binaire. Encore une fois, inutile de traduire du code qui ne sera exécutée qu'une seule fois, mieux vaut l'exécuter directement dans le mode ARM. Surtout que le processeur supporte un mode ARM qui permet d'exécuter du code ARM sans perte de performance. Par contre, le code fréquemment utilisé, notamment dans des boucles critiques, est traduit en VLIW. Au final, les performances sont optimisées, en limitant le travail du logiciel traducteur binaire. La détection du code fréquemment exécuté se fait différemment que pour les processeurs Transmetta et Daisy. L'unité de branchement ne détecte pas directement le code beaucoup exécuté, même si elle a un rôle à jouer. A la place, la détection est réalisée en logiciel, par un ''thread'' dédié, qui s'exécute sur un cœur séparé. Il détecte le code fréquemment utilisé en regardent les branchements exécutés récemment. Pour cela, le processeur mémorise l'historique des branchements pris récemment et copie cette historique dans une mémoire tampon dédiée, partagée entre tous les cœurs. Vu qu'elle est partagée, le ''thread'' de détection a accès à l'historique sans pertes de performances, seule la copie de l'historique dans ce tampon a un cout en performance, pas son partage. Le code VLIW obtenu après traduction binaire est mémorisé dans la mémoire RAM, dans une portion spécialement réservée pour. Elle fait 128 mébioctets, et est appelée le '''cache d'optimisation''' par NVIDIA. Le cache d'optimisation est protégé en écriture et seul le traducteur binaire peut écrire dedans. Au passage, le traducteur binaire est du code VLIW, ce qui fait qu'il est placé dans le cache d'optimisation. Le processeur détecte automatiquement quand une fonction pour laquelle le code compilé équivalent est disponible. Pour cela, l'unité de chargement contient une table de correspondance entre l'adresse de la fonction ARM, et l'adresse de son équivalent compilé. Quand une fonction est appelée, l'unité de chargement regarde l'adresse de destination du branchement. Si l'adresse est dans cette table, elle récupère l'adresse de la fonction compilée et branche vers celle-ci. Le processeur dispose d'un cache d'instruction de 128 kibioctets, ce qui est très important, mais nécessaire vu la taille des instructions VLIW. Le coeur VLIW dispose de ses propres décodeurs, d'un ''scoreboard'', de 7 unités de calcul et de registres. Pour les unités de calcul, il a deux FPU, deux ALU, une unité de branchement, et deux unités LOAD-STORE faisant aussi ALU. Un point important est que le CPU dispose de 64 registres entiers et de 64 registres flottants. C'est deux fois plus que supporte le jeu d'instruction ARM. En mode ARM, seule la moitié des registres est utilisée. Mais en mode optimisé, le traducteur binaire utilise bien les 64 registres, grâce à une sorte de renommage de registres logicielle. Pour simplifier la traduction binaire, le processeur supporte les techniques vues précédemment. Le processeur intègre un ''gated store buffer'' similaire à celui des processeurs Transmetta. Les techniques d'exceptions différées sont aussi supportées, comme sur les processeurs Transmetta. Il a aussi une unité de préchargement décrite par NVIDIA comme agressive, avec support du préchargement de type anticipé (''runahead''). ===L'émulation de la mémoire virtuelle=== Pour finir, il faut parler de la mémoire virtuelle avec la traduction binaire. Sur le processeur source, le processeur gère à la fois des adresses physiques et virtuelles et sa MMU fait la conversion entre les deux. Sur le processeur VLIW, la MMU est simulée par le programme de traduction binaire, partiellement ou totalement. L'implémentation exacte varie suivant que l'on parle du projet Daisy, des processeurs Transmetta ou de Denver. Sur les processeurs Transmetta, il n'y a pas de MMU ni de mémoire virtuelle. A la place, le programme de traduction binaire émule la mémoire virtuelle du processeur source. Le processeur VLIW ne gère que des adresses physiques, rien d'autre. L'espace d'adressage physique du processeur VLIW a la même taille que celui du processeur émulé, ici des processeurs x86. Il est vraisemblable que les adresses physiques utilisées par le processeur x86 sont les mêmes que celles du processeur VLIW. Sur le projet Daisy, le processeur VLIW gère la mémoire virtuelle via pagination, comme les processeurs émulés. Ce qui fait qu'il gère un espace d'adressage virtuel et un espace d'adressage physique. Pour éviter toute confusion, nous parlerons d'adresse physique/virtuelle VLIW pour les adresses physiques/virtuelles du processeur VLIW, d'adresse virtuelle/physique source pour celle du jeu d'instruction traduit, à savoir du Power PC ou du s390, éventuellement de l'x86. Il faut alors faire le lien entre adresses physiques source et adresses virtuelles VLIW. Pour cela, rien de plus simple : il y a correspondance parfaite. L'adresse physique source numéro N correspond à l'adresse virtuelle VLIW numéro N. Cependant, cela signifie que tout l'espace d'adressage virtuel VLIW serait utilisé par le code à traduire. En réalité, il faut ajouter de la place pour le code traduit et le programme de traduction binaire. La conséquence est que l'espace d'adressage virtuel VLIW est plus large que l'espace d'adressage source. L'espace d'adressage virtuel VLIW est découpé en trois sections : une section pour le code Power PC à traduire, une autre pour le code traduit en VLIW, et une dernière réservée au traducteur binaire. Le programme de traduction est placé dans une ROM mappée en mémoire dans la seconde section. Le reste de la seconde section est réservé à une mémoire RAM utilisée par le programme de traduction. [[File:Mémoire virtuelle sur le projet Daisy.png|centre|vignette|upright=1.5|Mémoire virtuelle sur le projet Daisy]] Il faut noter que chaque page physique source, une fois traduite, correspond à N pages virtuelles VLIW. La raison est que le code traduit prend plus de place que le code machine originel. En conséquence, on doit utiliser une page finale plus grande. Pour le reste, des pages contiguës en mémoire physique source sont elles aussi contiguës en mémoire virtuelle VLIW. Le calcul d'adresse est donc simple : l'adresse physique source est multipliée par N, puis on ajoute l'adresse de base à laquelle commence la section pour le code traduit. ==La traduction binaire sur le Burroughs B1700== Nous venons de voir comment accélérer en matériel la traduction binaire, avec des techniques bien spécifiques. Le Burroughs B1700 a procédé autrement et il est intéressant d'étudier en détail son architecture. Son architecture est décrite dans le livre "Interpreting Machines : Architecture and Programming of the Bl700/Bl800 Series", écrit par Elliott I. Organick et James A. Hinds. Une partie de ce qui vfa suivre est un très court résumé de ce livre. ===Le Burroughs B1700 : le mal-nommé ''split-level control store''=== Le Burroughs B1700 avait plusieurs modèles : le B1726, le B1710 et le B1800. Ils se distinguaient par la manière dont le code machine source était traduit par le traducteur binaire. Dans ce qui suit, nous parlerons de traducteur binaire pour parler soit d'un interpréteur pour un langage de programmation de haut niveau, soit pour un logiciel de traduction binaire. Le B1710 plaquait le traducteur binaire en mémoire RAM. Le B1800 faisait pareil, mais avait ajouté un cache dédié au code du traducteur binaire, afin d'améliorer les performances. Le B1726 avait un traducteur binaire scindé en deux : une partie était en mémoire RAM, l'autre était dans un ''local store'' dédié. Le processeur peut exécuter le traducteur binaire soit depuis le ''local store'', soit depuis la mémoire RAM avec une pénalité en termes de performances. La documentation appelle cela un '''''split-level control store''''', mais le terme est trompeur : la documentation qualifie de microcode le langage machine du Burroughs B1700, ce qui est à l'origine de beaucoup de confusions. Une autre source de confusion est que ce langage machine a quelques propriétés qu'on pourrait attendre d'un microcode, mais certaines sont manquantes. Par exemple, il peut adresser les registres d’interfaçage mémoire, mais c'est parce que ce sont des registres architecturaux sur ce processeur. Ses instructions n'encodent pas de signaux de commande, par contre, elles font 16 bits et adressent les opérandes de manière implicite. Le ''local store'' mémorisait au maximum 2048 instructions machine, ce qui fait que les 2048 premières "adresses" correspondaient au ''local store''. Les suivantes étaient des adresses de mémoire RAM qui contenaient le reste du traducteur binaire. En pratique, l'adresse qui séparait les deux était configurable ! On pouvait limiter la taille du ''local store'' à un multiple de 32 instructions ! Les adresses restantes étaient alors réattribuées à la RAM. Par exemple, on pouvait attribuer 64 adresses au ''local store'', les 2048 - 64 adresses restantes étaient alors attribuées à la mémoire RAM. Un registre limite, nommé TDPM, indiquait quelle était l'adresse de démarcation entre les deux. Il contenait 4 bits, dont la valeur était multipliée par 32, pour obtenir l'adresse limite. Le programmeur devait découper le traducteur binaire en segments, certains étant placé dans le ''local store'', les autres étant en mémoire RAM. Mieux que ça, le processeur autorisait d'utiliser la technique de l'''overlaying'' pour le microcode ! Il y avait même une fonction OVERLAY dédiée pour ! OVERLAY avait besoin de trois opérandes : la taille du segment à copier, son adresse source en mémoire RAM, son adresse de destination dans la micro-SRAM. L'interpréteur réservait un "segment" en mémoire RAM, pour les données, dont le processeur connaissait la position. En-dehors de ce segment, il n'y a que des instructions machines à traduire, et le traducteur binaire. Le CPU incorporait pour cela deux registres, un pour l'adresse de base du segment de données, un autre pour son adresse de fin. Il incorporait une protection mémoire limitée sur ce segment, à savoir que seul l'interpréteur pouvait lire ou écrire dans ce segment. ===L'ALU de taille variable=== Les Burroughs B1700 avaient une '''ALU de taille variable''', ce qui veut dire qu'elle pouvait faire des calculs sur un nombre de bits compris entre 0 et 24. Pour cela, les calculs étaient fait avec une ALU de 24 bits, puis les bits de poids fort inutiles étaient masqués. L'ALU de 24 bits pouvait masquer les bits de poids fort du résultat, mais avec des limitations. Elle gérait des opérandes et résultats de 0, 4, 8, 12, 16, 20 et 24 bits. Pour avoir un réglage plus fin, une ALU 4 bits séparée permettait de corriger le résultat. Les registres X et Y étaient reliés à une ALU de 24 bits capable d'effectuer des opérations arithmétiques basiques. Fait étonnant, l'ALU faisait tous les calculs en même temps et fournissait ses résultats dans 7 registres : un registre SUM pour l'addition, un registre DIFFERENCE pour la soustraction, et un registre par opération bit à bit. Les registres pour les opérations bit à bit sont : CMPX et CMPY pour l'opération NOT sur chaque opérande, XANY pour le ET bit à bit, XEOY pour le XOR, XORY pour le OU logique. Les additions et soustractions pouvaient se faire sur des opérandes codés en binaire ou en BCD, suivant le code opération utilisé. A cela, il fallait ajouter deux registres de "résultat" MSKX et MSKY. Ils fournissaient un masque dépendant de la taille du résultat en bits. C'est ce registre qui permettait de gérer des données de 0, 4, 8, 12, 16, 20, et 24 bits, alors que l'ALU faisait 24 bits. Le masque dans ce registre pouvait servir d'opérande dans une opération de masquage ultérieure, pour corriger le résultat. L'ALU prenait en opérande les registres X et Y, mais aussi un registre CYF pour la retenue entrante. En sortie, il fournissait aussi deux retenue sortantes : une pour l'addition et une autre pour la soustraction. Elles étaient mémorisées dans les registres CYL et CYD, respectivement. L'ALU fournissait aussi 12 conditions, qui alimentait un registre d'état. Le registre d'état était segmenté en trois registres appelés XYST, XYCN et BICN. Les registres X et Y sont des registres pour les opérandes, mais il y a aussi deux autres registres de 24 bits. Il s'agit des registres T et L, qui sont des registres généraux. Les registres T et L étaient découpés en 6 sous-registres de 4 bits, nommés LA LB LC LD LE et LF pour le registre L, et TA TB TC TD TE et TF pour le registre T. Ils étaient adressables comme n'importe quel registre. Le processeur contenait de nombreux autres registres de 4 bits, dont les sous-registres de T et L. En tout, il y a 27 registres de 4 bits adressables ! Et le processeur disposait d'une ALU 4 bits pour manipuler ces registres de 4 bits. L'ALU 4 bits est capable de faire des MOV entre registres de 4 bits, des opérations bit à bit, et peut aussi tester si un bit vaut 0 ou 1. La dernière possibilité est très utile pour implémenter les branchements. De plus, cela permettait de faire des opérations de masquage, notamment pour appliquer un masquage supplémentaire à celui de l'ALU. Par exemple, pour gérer des opérations sur 14 bits, on effectue une opération sur 16 bits avec l'ALU, et on masque les 2 bits manquants avec l'ALU 4 bits. ===Une architecture bit-adressable=== Le CPU Burroughs B1700 était '''bit-adressable'''. En clair, il pouvait adresser la mémoire bit par bit, si nécessaire. Un avantage est que cela facilitait la traduction binaire des instructions machine, mais nous détaillerons cela plus bas. Un autre avantage est que cela permettait d'émuler des architectures dont la taille des registres/''byte''/mots était très différente. Et cet avantage mérite quelques explications immédiates. Le processeur était un CPU 24 bits, mais il pouvait émuler des processeurs 16 bits, 8 bits, 4 bits, 1 bit, 9 bits, ou toute autre valeur. Pour cela, le processeur lisait des mots de 24 bits et utilisant un ''barrel shifter'' pour sélectionner les bits adéquats. Par exemple, pour un accès de 16 bits, la donnée lue en mémoire était masquée de manière à ne garder que les 16 bits de poids faible. Le ''barrel shifter''/circuit de masquage était placé directement avant le bus mémoire, sur le trajet des données/instructions. Le ''barrel shifter'' était commandé par un registre ''Field Unit'', qui précisait quelle était la taille des données à charger. Pour communiquer avec la mémoire, il avait trois registres séparés : READ, WRITE, MAR. READ contient la donnée lue lors d'une lecture, WRITE est pour une donnée à écrire lors d’une écriture, MAR contient l'adresse à lire ou en cours de lecture. Il s'agit de registres d’interfaçage mémoire. Le fait que ces registres d’interfaçage mémoire et IO soit adressable est peu commun, et c'est plus quelque chose qu'on attend d'un microcode que d'instructions machines. Mais il faut bien comprendre que les registres d’interfaçage mémoire ont été promus au rang de registres architecturaux, sur ce processeur. De même, pour communiquer avec les entrées-sorties, il disposait de deux registres CMND et DATA : CMND pour envoyer une commande à une entrée-sortie, DATA pour lire ou écrire une donnée. Il y avait aussi un registre U, qui mémorisait une donnée lue depuis le lecteur de cassette. ===Le code machine du Burroughs B1700=== Le processeur utilise des instructions de 16 bits de long, qui sont copiées dans registre d'instruction M de 16 bits. Le ''program counter'' est appelé le registre A. Les instructions du processeur font 16 bits de long, et elles sont alignées en RAM sur 16 bits, ce qui fait que les 4 bits de poids fort de ce registre valent 0, seuls les 14 bits de poids fort sont utiles. Les adresses mentionnées plus haut, qui adressent les micro-opérations en mémoire RAM, sont des adresses de mots de 16 bits, pas les adresses de bit individuels. Fait intéressant, il était possible de faire un OU logique entre l'instruction lue et un registre du processeur, le résultat étant mémorisé dans le registre M. Cela permettait de faire du code automodifiant, pour émuler des modes d'adressage complexe. Cela permettait par exemple de remplacer une adresse dans une micro-opération. L'adresse est alors surimposée avec un OU dans le champ d'adresse, alors que le reste de l'instruction n'est pas modifié. Afin de gérer les interruptions, ces dernières étaient partiellement détectées en logiciel. Une interruption doit être prise en compte à la toute fin de l'exécution d'une instruction source, quand celle-ci a finit son travail. Mais vu qu'ici, les instructions sont émulées avec des séries d'instructions cibles, il y a un problème : il est possible d'interrompre une instruction émulée en plein milieu. Pour éviter cela, le processeur ne déclenche pas les interruptions matérielles immédiatement, elles sont mises en attente. Une instruction dédiée vérifie si une interruption est en attente, et l'exécute alors cas échéant. L'instruction en question est utilisée en dernier, dans la suite d'instruction qui émule l'instruction source. Les Burroughs B1700 intégrait une pile d'adresse de retour, capable de mémoriser 16 adresses de 24 bits. Il disposait de 4 registres généraux, nommés X, Y, T et L, tous de 24 bits. Un point important est que le registre T avait des possibilités d’extraction spécifiques. Il était possible d'extraire N bits, placés n'importe où dans ce registre, et de les copier dans un autre registre. Le CPU supportait une '''instruction EXTRACT''' pour ça. Nous verrons pourquoi cette possibilité est intéressante et ne sert que pour le registre T. ===La traduction binaire sur le Burroughs B1700=== Les instructions source peuvent faire 16 bit avec tel jeu d'instruction, 8 bits avec un autre, 32 bits sur un autre, etc. Elles peuvent aussi être de longueur fixe ou variable ! Le B1700 utilisait une architecture bit-adressable pour gérer ces contraintes. En effet, avec un CPU bit-adressable, les contraintes d'alignement des instructions disparaissent ! Dans ce qui suit, on suppose que l'instruction est composée de '''champs''' : l'opcode, les opérandes, les adresses absolues, les constantes immédiates, les numéros de registre, etc. L'idée est que l'instruction source est chargée dans le registre T, puis découpée en champs, qui sont copiés dans les registres du processeur ou envoyés au microcode. Je rappelle que le registre T est relié à un ''barrel shifter'', différent au précédent, qui permettait de masquer, décaler et extraire une donnée de ce registre. Et il y a la possibilité d'extraire une suite de bit de ce registre T, pour la copier dans un autre registre. Par exemple, prenons une instruction composée d'un opcode, d'une constante immédiate et d'une adresse immédiate. Premièrement, elle est chargée dans le registre T. Deuxièmement, la constante immédiate est extraite du registre T et copiée dans le registre X. Troisièmement, l'adresse est extraite et est copiée dans le registre d’interfaçage mémoire, une lecture est démarrée, et l'opérande lue est copiée dans le registre Y. Enfin, l'instruction équivalente à l'opcode est exécutée par le microcode. Le tout prend plusieurs instructions : une première pour lire l'opcode, une seconde pour lire la première opérande, une troisième pour lire la seconde opérande, una quatrième pour tout exécuter. Notons que suivant la taille des champs, on peut en charger plusieurs à la fois. Par exemple, imaginons qu'on ait chargé 24 bits, contenant un opcode de 8 bits et une constante immédiate de 16 bits : le registre T est découpé en un opcode et une constante, avec deux instructions EXTRACT. L'architecture est bit-adressable car elle gère des champs qui ne font pas forcément 8 ou 16 bits. Par exemple, certains jeux d'instructions ont des opcodes codés sur 6 ou 7 bits, voire 9-10 bits. Utiliser une architecture bit-adressable règle ce problème d'alignement. On peut lire un champ depuis la mémoire RAM en précisant l'adresse de son premier bit, pas besoin de gérer des accès non-alignés. Par exemple, prenons une instruction source avec un opcode de 7 bits, suivi par une adresse de 12 bits et une constante immédiate de 15 bits. On peut charger 7 + 12 bits en un premier accès, pour extraite l'opcode et l'adresse, suivi par un second accès de 15 bits pour extraire la constante immédiate. Pour charger un champ, il faut connaitre plusieurs informations : l'adresse de son premier bit, sa taille. Pour cela, le B1700 contient deux registres, nommés ''Field Adress'' (FA) et ''Field Length'' (FL), qui indiquent l'adresse du premier bit et la taille de l'instruction à charger. Le premier fait 24 bits, ce qui permet de gérer des adresses de 24 bits. N'oubliez pas que le processeur est bit-adressable, ce qui permet d'adresser 16 Mébi-bits, soit 2 mébioctets. Le registre pour la taille fait lui 16 bits, ce qui permet de gérer des instructions de 65536 bits, soit 8192 octets ! Leur nom commence par ''field'', car le processeur peut gérer des instructions de taille variable et/ou plus longues que 24 bits. Dans ce cas, le chargement de l'instruction se fait champ par champ. Pour donner un exemple, prenons une instruction composée de 20 champs de 12 bits chacun. Les champs sont chargés un par un dans le registre T. Le registre FA est alors initialisé avec l'adresse de l'instruction, il est incrémenté de 12 à chaque cycle. Le registre FL est initialisé avec la taille de l'instruction, soit 20 * 12 = 240 bits, et il est décrémenté de 16 à chaque cycle. Précisons cependant que la taille d'un champ peut être changée d'un cycle à l'autre. Par exemple, le processeur peut charger un opcode de 8 bits, l'interpréter, détecter que l'instruction demande ensuite de charger une adresse de 16 bits. Dans ce cas, le registre FL est immédiatement altéré pour charger 16 bits, au lieu des 8 bits de l'opcode immédiatement précédent. ==Les jeux d'instructions dédiés à un langage de programmation== De rares processeurs sont conçus pour un langage de programmation en particulier. On appelle ces processeurs, conçus pour des besoins particuliers, des '''processeurs dédiés'''. Par exemple, les fameux ''Burrough E-mode'' B5000/B6000/B7000 étaient spécialement conçus pour exécuter de l'ALGOL-60. Leurs cousins B2000/B3000/B4000 étaient eux conçus pour le COBOL. Des langages fonctionnels ont aussi eu droit à leurs processeurs dédiés. Le prolog en est un bel exemple, avec les superordinateurs de 5ème génération qui lui étaient dédié. On peut aussi citer les machines LISP, dédiés au langage LISP, qui datent des années 1970. Elles étaient capables d’exécuter certaines fonctions de base du langage directement dans leurs circuits : elles possédaient notamment un ''garbage collector'' câblé dans ses circuits ainsi que des instructions machines supportant un typage déterminé à l’exécution. Les processeurs dédiés ont eu leur heure de gloire au début de l'informatique, à une époque où les langages de haut niveau venaient d'être inventés. À cette époque, les compilateurs n'étaient pas performants et ne savaient pas bien optimiser le code machine. Il était alors rationnel, pour l'époque, de rapprocher le code machine cible et le langage de programmation de haut niveau. De nombreuses architectures dédiés ont ainsi été inventées, avant que les concepteurs se rendent compte des défauts de cette approche. Les défauts en question ne sont pas nombreux, mais assez simples à comprendre. Premièrement, elles sont très rapides pour un langage de programmation en particulier, mais sont assez mauvaises pour les autres, d'où un problème de "compatibilité". Ajoutons à cela que les langages de programmation peuvent évoluer, devenir de moins en moins populaires/utilisés, ce qui rend la création d'architectures généralistes plus pertinente. Enfin, les architectures dédiées sont évidemment des processeurs CISC, pour implémenter les nombreuses fonctionnalités des langages évolués. Et les défauts des CISC sont assez rédhibitoires à l'heure actuelle. À l'heure actuelle, les algorithmes des compilateurs se sont améliorés et savent nettement mieux utiliser le matériel. Ils produisent du code machine efficace, ce qui rend les architecture dédiées bien moins intéressantes. Si on ajoute les défauts de ces architectures dédiées, par étonnant que les architectures dédiées aient presque disparues. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'accélération matérielle de la virtualisation | prevText=L'accélération matérielle de la virtualisation | next=Le matériel réseau | nextText=Le matériel réseau }} </noinclude> grt8ddpnktkkfhpdqw0fdtn0ru4kd5g 764062 764060 2026-04-19T20:50:43Z Mewtow 31375 /* La traduction binaire sur le Burroughs B1700 */ 764062 wikitext text/x-wiki De nos jours, la majorité des programmeurs programment dans des langages de haut niveau. Il est très rare pour eux d'avoir à utiliser de l'assembleur, et encore moins un langage machine. Les programmes écrits dans un langage de haut niveau sont traduits en langage machine par un logiciel appelé le compilateur. [[File:Compilation.PNG|centre|vignette|upright=2.5|Compilateur : principe.]] Il se trouve que quelques rares processeurs sont conçus pour faciliter le travail du compilateur. Et ce sont ces processeurs que nous allons voir dans ce qui suit. Ils représentent une catégorie de jeux d'instruction à part, qui n'a pas vraiment de nom. ==La compilation et l’interprétation== Un compilateur traduit donc du code source, écrit dans un langage de haut niveau, vers du langage machine. La traduction est rarement directe. En général, le compilateur traduit le code source en assembleur, qui est lui-même traduit en langage machine. L'assembleur est une représentation textuelle du langage machine alors que le code machine est du binaire exécutable par le processeur. La traduction se fait donc en deux étapes, la '''compilation''' proprement dite et l''''assemblage''', réalisées respectivement par un compilateur et un assembleur. Il faut parfois rajouter une troisième phase d'édition des liens, que nous passons volontairement sous silence. {| |[[File:Code C.png|vignette|200px|Le code source, ici en C.]] |[[File:Assem.png|vignette|250px|Le code assembleur.]] |[[File:Binary file - hello world (C programming).png|vignette|150px|Le code machine.]] |} ===L'interprétation et la compilation à la volée=== Une alternative à la compilation est l''''interprétation''', qui transforme le code source en code machine à la volée. Avec l'interprétation, le code source est passé à un logiciel appelé l''''interpréteur''', qui exécute le code source ligne de code par ligne de code. Une ligne de code est traduite en code machine, qui est exécutée, puis l'interpréteur passe à la ligne suivante. [[File:Kaantotulkkaus.png|centre|vignette|upright=3|Compilation versus interprétation.]] Un défaut de l'interprétation est son cout en performance, qui est assez important. Aussi, l'interprétation stricte a évolué vers un hybride entre interprétation et compilation. L'idée est qu'une partie peu importante du code source est interprétée, alors que le code important est compilé juste avant d'être exécuté. Pour le dire autrement, le code source est partiellement compilé à la volée, juste avant son exécution. Aussi, on parle de '''compilation à la volée'''. Le terme en anglais est ''Just In Time compilation'', abrévié en JIT. En soi, le JIT est associé à de l'interprétation, les JIT sont en réalité des hybrides interpréteurs-compilateurs. Un point important est que compiler du code à la volée est assez lourd, cela a un cout en performance. Par contre, le code compilé s'exécute plus vite que du code interprété. Si on exécute le code compilé une seule fois, le cout de la compilation l'emporte sur le gain à l'exécution. Mais le code compilé est réutilisé autant de fois que nécessaire. Le gain à l'exécution est donc multiplié par le nombre d'exécutions, alors que le cout de la compilation est répartit sur plusieurs exécutions, il est amorti. Les portions du code sont choisies de manière à avoir un gain en performance maximal. L'idée générale est que le code exécuté très souvent est compilé, alors que du code exécuté pas souvent est interprété. Typiquement, les boucles les plus souvent exécutées sont compilées, le code exécuté une seule fois est interprété. ===Les langages intermédiaires=== Les compilateurs modernes passent par un '''langage intermédiaire''' pour faire la transformation en code machine. Le compilateur traduit le langage de haut niveau en langage intermédiaire, puis traduit le langage intermédiaire en code machine cible. Le langage intermédiaire est parfois appelé le '''''bytecode''''', et ce terme recouvre aussi le code écrit avec du ''bytecode''. L'interprétation aussi peut passer par l'intermédiaire d'un un ''bytecode'', ce n'est pas limité aux compilateurs proprement dit. {| |[[File:Intermediate representation scheme.png|centre|vignette|upright=1.5|Représentation intermédiaire d'un compilateur.]] |[[File:Code interpreter scheme.png|centre|vignette|upright=1.5|Fonctionnement d'un interpréteur.]] |} Faire ainsi a de nombreux avantages, le principal étant d'avoir un compilateur capable de traduire un langage de haut niveau vers plusieurs jeux d'instructions différents. Cela permet d'avoir, par exemple, un compilateur qui traduit du C soit en code machine x86, soit en code machine pour un CPU ARM, soit pour un CPU POWERPC, etc. [[File:Compiler design.svg|centre|vignette|upright=3|Compiler design]] Pour l'interprétation, l'usage d'un ''bytecode'' a d'autres avantages. Pour l'exploiter, il faut distribuer non pas le code source, mais le ''bytecode''. En clair, l'interpréteur ne prend pas en entrée le code source, mais du ''bytecode'' déjà compilé. La traduction est alors beaucoup plus simple, car le code source a déjà été partiellement compilé, par la traduction en ''bytecode''. Interpréter du code source est en effet assez compliqué : il faut effectuer des étapes d'analyse lexicale, sémantique, et bien d'autres. Avec du ''bytecode'', ces étapes ont été réalisées lors de la compilation du ''bytecode'', l'interpréteur a alors peut de choses à faire. L'interprétation du ''bytecode'' se fait instruction par instruction, au niveau du ''bytecode''. Une instruction du ''bytecode'' est traduite en une instruction machine équivalente, qui est exécutée. Si l'instruction est un peu complexe, il exécute une fonction/procédure qui fait la même chose. Pour résumer, le ''bytecode'' est ensuite traduit à la volée et exécuté instruction par instruction par un logiciel appelé l'interpréteur. [[File:Control table.png|centre|vignette|upright=2|Control table]] Le langage intermédiaire peut être vu comme l'assembleur d'un processeur, n'existe pas forcément dans la réalité, mais dont le jeu d'instruction est décrit en détail. Le processeur en question est appelé une '''machine abstraite''', ou encore une ''machine virtuelle''. Nous utiliserons le terme de machine abstraite dans ce qui suit. La machine abstraite n'est pas la même suivant que l'on cible la compilation ou l'interprétation/JIT. Pour l'interprétation/JIT, la machine abstraite est souvent une machine à pile, car cela simplifie grandement la traduction en code machine final. L'interprétation demande que la traduction du ''bytecode'' en code machine soit la plus simple possible. Et cela demande de prendre en compte pas mal de détails. Par exemple, les différents jeux d'instruction existants n'ont pas le même nombre de registres, ce qui pose problème lors de la traduction du ''bytecode''. Lors de la transformation en code machine, un algorithme d'allocation de registres se débrouille pour traduire le code intermédiaire en code qui utilise un nombre limité de registres. Pour améliorer cette allocation de registres, il y a deux solutions niveau ''bytecode'' : soit utiliser un nombre illimité de registres, soit utiliser une machine à pile. Pour les machines à pile, il existe un algorithme simple et rapide pour traduire un code écrit pour une machine à pile en un code écrit pour un processeur avec des registres, qui se débrouille pas trop mal pour allouer efficacement les registres. C'est un avantage assez important pour les langages interprétés. ==Les processeurs qui exécutent du bytecode== Le ''bytecode'' est un code machine, ce qui signifie qu'il peut en théorie s'exécuter sur un processeur qui implémente le jeu d’instruction associé. Si le jeu d'instruction d'un ''bytecode'' est souvent une description censée être fictive, elle n'en reste pas moins un jeu d'instruction et des caractéristiques précises, qu'on peut l'implémenter en matériel ! Néanmoins, tous les ''bytecode'' ne sont pas égaux de ce point de vue. Certains sont faciles à implémenter en matériel, d'autres non. Les ''bytecodes'' implémentés en matériel sont ceux dont les machines abstraites sont des machines à pile. Un premier exemple est celui des processeurs Pascal MicroEngine, qui exécutaient directement le bytecode du langage Pascal, le fameux UCSD P-code. Un second exemple est la '''machine SECD''', qui sert de langage intermédiaire pour certains compilateurs de langages fonctionnels. Elle a été implémentée en matériel par plusieurs équipes, notamment par les chercheurs de l'université de Calgary en 1989. Dans le même genre, quelques processeurs simples étaient capables d’exécuter directement le ''bytecode'' du langage FORTH. Le FORTH, un des premiers langages à pile de haut niveau, possède de nombreuses implémentations hardware et est un des rares langages de haut niveau dont le ''bytecode'' a été utilisé comme langage machine sur certains processeurs. Par exemple, on peut citer le processeur FC16, capable d’exécuter nativement du ''bytecode'' FORTH. ===Les processeurs Java=== Le cas le plus impressionnant est celui de la machine virtuelle Java, qui est un design de processeur comme un autre. En temps normal, le ''bytecode'' Java est compilé ou interprété, mais certains processeurs exécutaient du ''bytecode'' Java directement, sans interprétation ni compilation. Ils sont appelés des '''processeurs Java'''. On peut citer les processeurs ARM disposant de l'extension Jazelle, du JEMCore et du aJ-100 de aJile Systems, le picoJava II de Sun, et quelques autres. Il y a aussi eu quelques projets de recherche ou processeurs open source, comme Komodo, jamuth, le ''Java Optimized Processor'' et quelques autres projets du même genre. L'implémentation de la machine virtuelle Java n'était cependant pas complète. Quelques instructions complexes n'étaient pas gérées par le processeur et devaient être émulées en logiciel. Mais la grosse majorité des instructions l'était. Les instructions non-supportées étaient émulées avec la méthode ''trap and emulate'', à savoir qu'un opcode inconnu déclenchait une exception matérielle ''opcode invalide'', dont la routine pouvait émuler les instructions qui doivent l'être. Le processeur aJ-100 de aJile est capable d'exécuter la quasi-totalité des instructions de la JVM Java, seules deux instructions faisant exception. Les '''accélérateurs Java''' sont une solution intermédiaire, qui permet de traduire à la volée du ''bytecode'' Java en instructions RISC, exécutées sur un autre processeur. Il s'agit formellement de traduction binaire, mais je préfère en parler ici. L'idée est de combiner un processeur RISC avec un coprocesseur Java. Le coprocesseur lit le ''bytecode'' Java et le traduit en instructions RISC, ces dernières étant exécutées sur le processeur RISC. Le JA108 de Nozomi était un coprocesseur de ce genre, au même titre que le coprocesseur JSTAR de JEDI. L'extension Jazelle de certains CPU ARM fonctionnait vraisemblablement sur un principe similaire, sauf que le coprocesseur était intégré dans le processeur ARM, entre le cache d'instruction et le décodeur d'instructions ARM. L'intérêt des processeurs Java n'est pas qu'une question de performance. Certes, ils permettent d’exécuter plus vite les programmes compilés en ''bytecode'', comme des programmes Java pour la JVM Java. Mais l'intérêt est aussi de faire des économies de mémoire RAM et ROM. Je rappelle que la machine virtuelle Java est une machine à pile, ce qui fait que sa densité de code est excellente. Les programmes en ''bytecode'' Java sont donc très petits, et prennent peu de place en mémoire ROM. De plus, utiliser un processeur Java permet de se passer d'interpréteur ou de JIT Java, ce qui économise encore plus de mémoire. ===Transformer un processeur RISC en machine à pile=== La plupart des processeurs Java utilisent en interne une architecture à registre généraux, qui émule une machine à registre en microcode. Quelques registres sont réservés pour l'état de la machine virtuelle, avec au minimum un registre réservé pour le pointeur de pile, un autre pour un pointeur vers un ''pool'' de constantes, et quelques autres. D'autres registres servent à mémoriser le haut de la pile, à savoir le sommet de la pile et quelques opérandes situées juste en-dessous. Par exemple, le processeur picoJava II gardait les 64 opérandes au sommet de la pile dans 64 registres généraux, séparés des registres pour les pointeurs et l'état de la JVM. Une optimisation possible, envisagée sur les processeurs picoJava, était une sorte de macro-fusion sous stéroïde. L'idée était de fusionner certaines séries d'instructions Java en une seule instruction machine. Typiquement, pour faire une opération arithmétique, une machine à pile empile deux opérandes et exécute une opération, ce qui prend trois instructions machine. La spécification picoJava 1 a proposé d'envoyer directement la seconde opérande en entrée de l'ALU, ce qui fusionnait l'empilement de la seconde opérande et l'opération arithmétique. Le processeur picoJava II va plus loin et fusionne les trois instructions en une seule instruction RISC, agissant sur des registres. Mais la majorité des processeurs Java ne faisaient pas cela, car cela complexifie grandement le décodeur. Les processeurs ''bigfoot'' de DCT utilisaient un système différent pour émuler une pile avec des registres. Leur banc de registre de 64 bits était coupé en deux : les 16 premiers registres étaient utilisés pour la pile, les 48 suivants étaient utilisés pour autre chose. L'idée est que le registre 0 mémorise un emplacement vide, le registre 1 contient le sommet de la pile, le registre 2 l'opérande sous le sommet de la pile, et ainsi de suite. Les 15 premières opérandes de la pile sont donc mémorisées dans les registres. Les instructions écrivent leur résultat dans le registre adéquat, puis tous les registres sont décalés pour que le résultat passe dans le registre R1. Par exemple, les instructions PUSH chargent une opérande dans le registre 0, puis tous les registres sont décalés de un rang, pour que l'opérande chargée soit dans le registre 1. Le registre 0 devient alors le registre numéro 1, le registre 1 devient le 2, et ainsi de suite. Par contre, les opérations arithmétiques font autrement. Une instruction d'addition, par exemple, lit les opérandes dans le registre 1 et 2, puis place son résultat dans le registre 2 (les deux opérandes sont dépilées). Puis, les registres sont décalés d'un rang pour que ce registre 2 devienne le 1. Les registres ne sont en réalité pas décalés, il n'y a pas de transferts entre registres. A la place, les 16 premiers registres utilisaient une forme limitée de renommage de registres, similaire au banc de registre tournant utilisé sur les processeurs EPIC comme l'Itanium. Les numéros de registres sont renommés, décalés, à chaque fois qu'on empile ou dépile une opérande. Pour faire le renommage de registres, processeur contient un ''register stack counter'' de 4 bits, qui est incrémenté à chaque fois qu'on empile une donnée, et est décrémenté quand on dépile une donnée. Quand on veut accéder à un opérande, le numéro de registre architectural associé est traduit en numéro de registre physique, en additionnant le ''register stack counter'' (modulo 16, pour rester sur 4 bits). ==L'accélération de l'interprétation/JIT : le ''Thumb-EE'' d'ARM== Quelques processeurs ont ajouté des instructions pour faciliter le travail des interpréteurs/JIT. Par exemple, en 2005, ARM a ajouté le mode ''Thumb Execution Environment'', qui faisait cela. Il reprenait le jeu d'instruction compact ''thumb'' et ajoutait quelques instructions et modifiait le comportement d'instructions existantes. Le jeu d'instruction ''Thumb-EE'' a été ajouté sur les CPU ARM en 2005, mais a été déprécié en 2011, par manque d'utilité. Mais la tentative mérite qu'on s'attarde dessus. Les CPU de gamme M, déstinés à l'embarqué, n'ont jamais supporté ''Thumb-EE'', vu que de tels processeurs ne sont pas conçus pour exécuter du code interprété/JIT. Le mode ''Thumb-EE'' est un mode d'exécution, séparé de l'ARM normal. Ainsi, une instruction ''Thumb-EE'' et une instruction ARM peuvent avoir le même encodage en binaire, mais se comporter différemment. Le processeur est à tout moment soit en mode ''Thumb-EE'', soit en mode ARM normal, ce qui précise comment ces instructions doivent se comporter. Des instructions ont été ajoutées par ''Thumb-EE'', pour qu'il fasse son travail. L'entrée et la sortie du mode ''Thumb-EE'' se fait avec deux instructions : ENTERX et LEAVEX. Une fois en mode ''Thumb-EE'', le décodage des instructions ''thumb'' se fait avec les règles du ''Thumb-EE'', et non celles du ''thumb'' normal. Une première différence entre ''thumb'' normal et ''Thumb-EE'' est l'ajout de ce qui s'appelle un ''null check'' pour les instructions mémoire. Le ''null check'' est utilisé pour les instructions en mode d'adressage "base + indice" ou "Base + décalage", ou tout autre mode d'adressage avec un registre de base utilisés dans des calculs d'adresse. L'idée est de vérifier si l'adresse de base vaut zéro ou non. Si c'est le cas, le processeur lève une exception matérielle, qui est traitée par l'interprétation ou le compilateur JIT. Une telle situation est en effet signe d'une erreur d'adressage mémoire, qui doit être traitée par l'interpréteur. Typiquement, c'est signe qu'un pointeur n'a pas bien été initialisé, ce qui peut arriver avec du code interprété ou JIT. Les interpréteurs/JIT traitent généralement la situation en ajoutant un test avant toute instruction mémoire, pour vérifier si le pointeur accédé vaut zéro ou non. Avec ''Thumb-EE'', pas besoin d'ajouter les tests en questions : ils sont réalisés automatiquement lors de chaque instruction mémoire. : Les registres pour la pile, comme le pointeur de pile et le pointeur de ''frame'', sont aussi concernés. L'instruction CHKA vérifie que les accès à un tableau ne débordent pas en dehors du tableau en question. Par exemple, pour un tableau de 1024 éléments, elle vérifie si l'indice est compris entre 0 et 1024. Pour cela, elle vérifie si l'indice de tableau est bien dans l'intervalle adéquat et lève une exception matérielle si ce n'est pas le cas. Son utilité se comprend quand on sait que tous les accès à un tableau sont vérifiés par l'interpréteur. L'interpréteur est censé ajouter des instructions pour vérifier les indices, à savoir deux ou trois branchements. Avec CHKA, le test se fait en une seule instruction. D'autres modifications mineures de l'encodage des instructions sont aussi présentes, ainsi que des modifications pour les instructions LOAD/STORE. Précisément, dans les modes "base + indice" et "base + décalage", le décalage et l'indice subissent maintenant des décalages. L'idée est de simplifier le calcul d'adresse. De quoi économiser quelques instructions lors des calculs d'adresse, ce qui facilite le travail de l'interpréteur/JIT. Et des instructions LOAD/STORE précises voient leur comportement modifié pour faciliter la gestion de la pile de Java ou l'exécution des méthodes locales. Pour faciliter le travail de l'interpréteur, deux instructions HBP et HBLP ont été ajoutées. Elles branchent vers une fonction qui gère les exceptions logicielles (une fonctionnalité présente dans de nombreux langages, comme Java). La première effectue un branchement vers la fonction en question, la seconde sauvegarde l'adresse de retour avant de faire ce branchement. Les deux peuvent brancher vers 256 fonctions pré-déterminées, sans avoir à présenter leur adresse. ==La traduction binaire accélérée par le matériel== Plus haut, nous avons surtout parlé des compilateurs et des interpréteurs. Cependant, nous devons aussi parler de la '''traduction binaire'''. Elle traduit un programme écrit dans un code machine vers un autre code machine. Par exemple, elle traduit un programme compilé pour un CPU x86 vers un code machine ARM. En général, le système d'exploitation est généralement compilé pour le jeu d'instruction natif, mais il exécute des applications prévues pour le x86. Les applications sont traduites par le système d'exploitation, avant d'exécuter le code traduit. : Nous parlerons dans la suite de code machine source et de code machine cible, pour parler respectivement du code à traduire et du code obtenu après traduction. Idem avec d'autres termes comme architecture cible/source, ou jeu d'instruction cible/source. La traduction binaire est surtout utilisée pour des questions d'émulation ou de compatibilité. Par exemple, lorsque les Macintosh sont passés de processeurs Power PC vers des processeurs x86, le système d'exploitation Mac OS utilisait la traduction binaire pour convertir les anciennes applications Power PC vers du code x86. Les utilisateurs n'y ont vu que du feu. Le système de traduction binaire était appelé Rosetta 1. Par la suite, lors de la transition de processeurs x86 vers des processeurs Apple, Rosetta 2 a vu le jour. La traduction binaire a surtout été utilisée pour traduire du code x86 vers un autre jeu d'instruction. Il faut dire que le x86 est le jeu d'instruction dominant. De nombreuses entreprises ont eu pour ambition de briser l'hégémonie du x86 sur PC, en remplaçant le x86 par un jeu d'instruction plus performant, tout en gardant une compatibilité maximale. La traduction binaire était la seule solution pratique. Elle était le plus souvent intégralement réalisée en logiciel, comme c'était le cas sur les architectures Itanium avec le ''IA-32 Execution Layer''. ===Les généralités sur la traduction binaire assistée en matériel=== L'architecture source est presque tout le temps une architecture CISC, assez ancienne, qu'on souhaite émuler. Le choix de l'architecture cible se porte souvent sur une architecture VLIW, fort différente de l'architecture source, et ce pour obtenir une économie de matériel conséquente. Pour s'exécuter rapidement, le code traduit doit exploiter le parallélisme d'instruction, à savoir exécuter plusieurs instructions en même temps dans des unités de calcul séparées. Il est possible d'utiliser un processeur superscalaire avec exécution dans le désordre pour cela, mais au prix d'un cout important en transistors. Alors qu'en utilisant un CPU VLIW, c'est le traducteur binaire qui fait tout le travail d'extraction du parallélisme d'instruction. Et après tout, quitte à avoir un traducteur binaire, autant lui refiler le boulot d'optimisation. Compiler du code à la volée est certes assez lourd, mais qu'il existe des algorithmes efficaces pour regrouper des instructions indépendantes dans une seule instruction VLIW. Au passage, le processeur VLIW a plus de registres que le processeur source. Cela permet de faire du renommage de registres directement en logiciel. Le traducteur binaire n'hésite pas à changer les noms de registres entre instructions source et instruction VLIW cible, afin de supprimer des dépendances de données. En conséquence, cela ouvre des opportunités de parallélisme, qui sont exploitées lors du regroupement des opérations en instructions VLIW. Vous pourriez penser que le choix d'un CPU VLIW pour émuler du CISC est tout sauf optimal. Vous devez penser que la traduction binaire est d'autant plus simple que l'architecture source et cibles sont semblables. Dans les faits, ce n'est pas tellement traduire les instructions qui pose problème, mais plus la gestion du registre d'état, des exceptions matérielles, de la mémoire virtuelle, la différence entre gros-boutisme et petit-boutisme, de même que des différences d'adressage pour les périphériques. Le premier problème est la gestion des conditions, notamment en présence d'un registre d'état. En pratique, la traduction binaire s'utilise pour traduire du code CISC vers du code RISC ou VLIW. Les architectures source ont donc un registre d'état, qui est mis à jour non seulement par des instructions de test, mais aussi des instructions arithmétiques. L'architecture cible n'a elle pas de registre d'état, mais des registres à prédicats. La traduction de l'un vers l'autre est alors quelque peu compliquée. Et elle est d'autant plus compliquée que le registre d'état est la source de dépendances d'instruction implicites, qui réduisent les performances. Par exemple, si une instruction arithmétique modifie le registre d'état, cela peut impacter l'exécution d'une instruction ultérieure, qui lit ce registre d'état. Pour éliminer ces fausses dépendances, le traducteur binaire doit renommer ce registre en logiciel et détecter les dépendances utiles. Et le compilateur doit gérer les cas où il y a beaucoup de distance entre les deux, voire les cas où l'instruction dépendante n'a pas encore été analysée par le compilateur. Un autre problème est lié à la gestion des exceptions matérielles, et précisément des exceptions précises. Pour rappel, une instruction VLIW regroupe plusieurs opérations, opérations qui correspondraient à une instruction machine sur un CPU pas VLIW. En conséquence, plusieurs instructions du langage machine "source" sont regroupées en une seule instruction VLIW. Et il faut tenir compte du cas où une instruction source lève une exception. Dans le code VLIW, cela signifie qu'une opération lève une exception, et il faut annuler partiellement l'instruction VLIW associée. Et par partiellement, on veut dire que seules les opérations suivantes dans l'ordre du programme source doivent être annulées, puis ré-exécutées. Et c'est un sacré casse-tête ! Les processeurs VLIW utilisent comme solution le mécanisme d'exceptions différées des processeurs EPIC, vu il y a quelques chapitres. Pour résumer, le code est exécuté par blocs d'instruction, délimités par des branchements ou tout autre limite/barrière pertinente dans le code. Un bloc de code est compilé en deux versions : une version rapide sans exceptions matérielles, une version lente qui gère les exceptions précises. La version rapide s'exécute sans exécuter les exceptions. Cependant, les exceptions matérielles sont enregistrées, pour être prises en compte à la toute fin du bloc de code. La version rapide est exécutée en premier et elle mémorise si une exception matérielle a eu lieu. Puis une instruction vérifie si une exception a eu lieu et décide quoi faire. Si aucune exception n'a eu lieu, elle passe à la suite du programme, les résultats du bloc de code sont définitivement acceptés. Mais si une exception a eu lieu, tout est annulé. Le processeur est remis dans l'état initial, puis le code est ré-exécuté instruction par instruction de manière à gérer l'exception correctement. ===La traduction par pré-décodage=== Une première solution serait de faire la traduction binaire dans le cache d'instruction. Lors d'un défaut de cache, le code chargé depuis la RAM est traduit en code machine cible. Et c'est ce code machine cible qui est mémorisé dans le cache d'instruction et exécuté par le processeur. La technique marche sur le papier et n'est qu'une amélioration des techniques de pré-décodage vues il y a quelques chapitres. Cependant, le code obtenu est une traduction assez basique, qui n'incorpore pas d'optimisations dignes de ce nom. De plus, elle gère mal le cas où la taille du code source et cible sont potentiellement très différentes. Concrètement, elle est surtout utile pour traduire du code d'un processeur RISC vers un autre processeur RISC, dont les tailles d'instructions sont similaires. Elle ne permet pas d'émuler plusieurs jeux d'instructions différents, le cout en matériel (un circuit de pré-décodage par jeu d'instruction) serait trop important. ===Le projet DAISY d'IBM=== De très rares processeurs étaient conçus pour accélérer cette traduction binaire, afin de garder de bonnes performances. Les premiers à avoir étudié l'idée étaient IBM, avec leur projet DAISY (''Dynamically Architected Instruction Set from Yorktown''). Le projet de base était de convertir à la volée du code compilé pour des CPU Power PC, vers du code VLIW. Les chercheurs d'IBM avaient développé un algorithme de traduction dynamique efficace, ainsi qu'un processeur VLIW disposant d'optimisations spécifiques à la traduction binaire dynamique, à la volée. De nombreuses idées de ce projet ont été reprises ou re-découvertes par la société Transmetta, avec ses processeurs Crusoe et Efficieon, puis par NVIDIA avec son projet Denver. Les ingénieurs de ce projet ont étudié la possibilité d'émuler plusieurs jeux d'instructions différents sur un même processeur, notamment le s390 d'IBM et le x86. En théorie, cela demande juste d'avoir plusieurs programmes de traduction binaire : un pour le x86, un autre pour le s390, éventuellement un autre pour le Power PC. Mais cela ne s'est pas concrétisé. Mais les ingénieurs ont étudié quelle pourrait être l'architecture VLIW idéale pour ça. Par exemple, il fallait des additions 3-opérandes pour simplifier les calculs d'adresse. Une difficulté était la gestion du registre d'état, dont les bits ne sont pas mis à jour de la même manière sur le x86 et le s390 ou le Power PC. La différence entre gros-boutisme et petit-boutisme était aussi un problème, de même que des différences d'adressage pour les périphériques. Pour que la traduction binaire soit efficace, le processeur VLIW intègre diverses optimisations, comme les exceptions différées et les branchements multi-voies. La principale est la suivante : seul le code des boucles ou fonctions exécutées fréquemment est traduit en code VLIW, le reste du code est interprété. En effet, traduire du code binaire prend plus de temps qu'une simple interprétation. Pour du code qui ne sera exécutée qu'une seule fois, il est plus rapide d'utiliser l'interprétation. Par contre, pour du code exécuté beaucoup de fois, le cout de la traduction binaire est amorti, dilué sur N exécutions, compensé par le gain en temps d'exécution de ce code traduit. Au final, cela permet de ne traduire que le code qui le mérite. La détection du code fréquemment exécuté est réalisée dans le cœur VLIW. L'unité de branchement mémorise les derniers branchements rencontrés et le nombre de fois qu'ils ont été exécutés. Si ils ont été exécutés un certain nombre de fois, l'unité de branchement lève une exception matérielle, qui invoque le traducteur binaire. La routine de cette exception est le traducteur binaire proprement dit. En clair, la traduction binaire est démarrée quand le processeur détecte qu'une fonction a été exécutée plus de N fois, via une exception matérielle dédiée. La mesure du nombre d'exécution d'un branchement se fait dans l'unité de calcul dédiée aux branchements, avec l'aide d'une sorte de ''branch adress buffer'' modifié. Pour rappel, le ''branch adress buffer'' mémorise le ''Program counter'' de chaque branchement récemment rencontré. Ici, chaque entrée du ''branch adress buffer'' est associée à un compteur incrémenté à chaque exécution du branchement. ===Les processeurs Crusoe et Efficieon de Transmetta=== Les processeurs Crusoe Et Efficeron sont deux processeurs VLIW produits par la société Transmetta, une société californienne rachetée par NVIDIA. Ils étaient conçus pour exécuter spécifiquement des programmes x86, système d'exploitation inclus. L'idée derrière ce projet était d'exécuter du code x86 sans que les applications, ni même le système d'exploitation et le BIOS soient au courant ! Les processeurs Transmetta étaient en réalité conçus pour exécuter un programme unique, le ''Code Morphing Software'' (CMS), qui traduisait le code x86 en code VLIW. Transmetta ne voulait pas que son processeur VLIW soit exploité directement, sans le CMS. Il n'avait pas rendu public de compilateur pour traduire du C vers du code VLIW, il n'avait pas donné la documentation du jeu d'instruction VLIW. Des efforts de rétro-ingénieurie, documentés sur le site realworldtech, ont cependant permis de comprendre comment étaient encodées les instructions du processeur Crusoe. Comme pour le projet Daisy, le ''Code Morphing Software'' utilisait à la fois interprétation et traduction binaire, selon les besoins. Les instructions sont interprétées lors de leurs premières exécutions, mais le CMS bascule sur de la traduction en code VLIW après un certain nombre d'exécution. Ainsi, les boucles souvent exécutées sont traduites en code VLIW, alors que le reste du code est interprété. Le choix du code à traduire est le fait du CMS, il utilise des heuristiques complexes pour, qui ne sont pas connues dans le détail. Le ''Code Morphing Software'' est mémorisé dans une EEPROM, ce qui en fait un ''firmware'' situé en dessous du BIOS. Le CMS démarre ensuite le BIOS, qui lui-même démarre le système d'exploitation, qui lui-même démarre les pilotes de périphériques et les programmes. Le CMS se réserve les 16 premiers mébioctets de l'espace d'adressage. L'EEPROM du CMS est mappée dedans, mais n'en utilise que la moitié. Le reste est utilisé comme cache, pour mémoriser le code VLIW traduit par le CMS. Les deux processeurs intégraient aussi deux mémoires SRAM, utilisées par le CMS, appelées la ''local program memory'' (LPM) et la ''local data memory'' (LDM). La première contient du code qui gére les interruptions, la mémoire virtuelle, les exceptions matérielles, les problèmes d'alignement mémoire, et quelques fonctions très fréquemment utilisées par le CMS. La seconde est de la mémoire RAM utilisée par ces fonctions de la ''local program memory''. Les mémoires caches L1 et L2 sont séparées de ces deux mémoires. Le processeur Crusoe et Efficieon étaient des processeurs VLIW très simples. Ils n'avaient même pas de MMU, ni de fonctionnalités importantes sur les CPU x86. Pour Crusoe, les instructions VLIW étaient encodées sur 64 ou 128 bits, et regroupaient entre 2 à 4 opérations. Les instructions LOAD/STORE ne supportaient pas d'adressage indicé, juste de l'adressage indirect à registre. Crusoe avait 5 unités de calcul : deux ALU entières, une unité LOAD/STORE, une unité de branchement, une FPU. Efficieon doublait le nombre d'ALU entière et d'unité LOAD/STORE, les instructions VLIW passaient à 256 bits. Le processeur Crusoe contient 160 registres, dont 64 registres généraux, 32 registres flottants. Les registres généraux font 32 bits, ce qui est cohérent avec le fait que les CPU x86 de l'époque étaient des processeurs 32 bits. Quant aux registres flottants, ils faisaient 80 bits, ce qui colle avec la taille des registres flottants de la FPU x87 utilisée à l'époque. Sur les 64 registres généraux, seuls 48 étaient réellement utilisable pour mémoriser des opérandes. Une partie des registres généraux étaient utilisés pour la gestion de la pile, d'autres pour mémoriser l'état du CPU x86 émulé, un registre était un registre zéro non accesible en écriture. La gestion des exceptions est optimisée avec un système d'exceptions différées, le même que celui décrit plus haut. Le processeur utilise un système similaire, pour effectuer des lectures anticipées, qui a été expliqué dans le chapitre sur les processeurs VLIW/EPIC. Sauf qu'il s'agit là d'une optimisation pour gagner en performances en général, pas une aide utile pour gérer des exceptions précises ou toute autre subtilité de la traduction binaire. Pour gérer les exceptions différées et les lectures anticipées, le processeur mémorise l'état du processeur avant de démarrer un bloc de code. Pour cela, le processeur contient des ''shadow registers'', 48 registres entiers et 16 registres flottants. Vous remarquerez qu'il y a autant de registres entiers que de ''shadow registers'' entiers. Les registres entiers/flottants sont mémorisés dans les ''shadow registers'' avant d'exécuter un bloc de code, afin que le processeur puisse revenir à l'état de base. Une autre fonctionnalité liée est le ''gated store buffer''. Le principe est d'éviter toute écriture en mémoire RAM, tant que le code VLIW n'a pas émis d'instruction ''commit''. L'idée est que les écritures sont conservées dans la ''store queue'' de l'unité LOAD/STORE. Pour rappel, la ''store queue'' met en attente les écritures soit tant que les écritures ne sont pas terminées, soit tant que la RAM est occupée. L'idée est que les écritures sont accumulées dans la ''store queue'' tant que l'instruction ''commit'' n'a pas eu lieu. Le ''store queue'' est "déversé dans la RAM" seulement quand l'instruction ''commit'' s'exécute. ===Les CPU NVIDIA Denver=== Nvidia a tenté quelque chose de similaire avec son projet Denver. NVIDIA a racheté l'entreprise Transmetta, ses brevets, et a tenté de refaire la même chose. Le projet initial était de traduire du code x86 vers un jeu d'instruction VLIW propriétaire, comme l'a fait Transmetta. Mais NVIDIA n'a pas réussi à acquérir la licence du jeu d'instruction x86 et s'est rabattu sur le jeu d'instruction ARM. Le processeur né de ce projet est le Tegra K1-64 CPU. Il s'agit d'un processeur composé d'un cœur VLIW, couplé à deux décodeurs d'instructions ARM. Les deux décodeurs ARM traduisent une paire d'instructions ARM consécutives en instructions VLIW, pour les exécuter sur le cœur VLIW. Le CPU peut fonctionner selon deux modes : ARM et VLIW. En somme, le processeur supporte deux jeux d'instruction : le jeu d'instruction ARM, et un jeu d'instruction VLIW. En mode ARM, les instructions ARM sont chargées depuis le cache, traduites en VLIW par les décodeurs ARM, puis exécutées par le cœur VLIW. En mode optimisé, le CPU exécute des instructions VLIW chargées depuis le cache d'instruction, les décodeurs ne sont pas utilisés. : Le CPU supporte en réalité deux jeux d'instruction ARM : ARM8 et 7. Mais ils correspondent au même mode pour le CPU. Un point important est que le processeur peut changer très rapidement de mode, en à peine quelques cycles d'horloges. On n'est clairement pas dans le cas des CPU x86, qui mettent des plombes pour passer du mode 32 à 64 bits et inversement. La commutation est tellement rapide qu'on peut considérer que le processeur supporte deux jeux d'instruction simultanément. Et le support simultané de deux jeux d'instruction facilite l'implémentation de la traduction binaire. Encore une fois, inutile de traduire du code qui ne sera exécutée qu'une seule fois, mieux vaut l'exécuter directement dans le mode ARM. Surtout que le processeur supporte un mode ARM qui permet d'exécuter du code ARM sans perte de performance. Par contre, le code fréquemment utilisé, notamment dans des boucles critiques, est traduit en VLIW. Au final, les performances sont optimisées, en limitant le travail du logiciel traducteur binaire. La détection du code fréquemment exécuté se fait différemment que pour les processeurs Transmetta et Daisy. L'unité de branchement ne détecte pas directement le code beaucoup exécuté, même si elle a un rôle à jouer. A la place, la détection est réalisée en logiciel, par un ''thread'' dédié, qui s'exécute sur un cœur séparé. Il détecte le code fréquemment utilisé en regardent les branchements exécutés récemment. Pour cela, le processeur mémorise l'historique des branchements pris récemment et copie cette historique dans une mémoire tampon dédiée, partagée entre tous les cœurs. Vu qu'elle est partagée, le ''thread'' de détection a accès à l'historique sans pertes de performances, seule la copie de l'historique dans ce tampon a un cout en performance, pas son partage. Le code VLIW obtenu après traduction binaire est mémorisé dans la mémoire RAM, dans une portion spécialement réservée pour. Elle fait 128 mébioctets, et est appelée le '''cache d'optimisation''' par NVIDIA. Le cache d'optimisation est protégé en écriture et seul le traducteur binaire peut écrire dedans. Au passage, le traducteur binaire est du code VLIW, ce qui fait qu'il est placé dans le cache d'optimisation. Le processeur détecte automatiquement quand une fonction pour laquelle le code compilé équivalent est disponible. Pour cela, l'unité de chargement contient une table de correspondance entre l'adresse de la fonction ARM, et l'adresse de son équivalent compilé. Quand une fonction est appelée, l'unité de chargement regarde l'adresse de destination du branchement. Si l'adresse est dans cette table, elle récupère l'adresse de la fonction compilée et branche vers celle-ci. Le processeur dispose d'un cache d'instruction de 128 kibioctets, ce qui est très important, mais nécessaire vu la taille des instructions VLIW. Le coeur VLIW dispose de ses propres décodeurs, d'un ''scoreboard'', de 7 unités de calcul et de registres. Pour les unités de calcul, il a deux FPU, deux ALU, une unité de branchement, et deux unités LOAD-STORE faisant aussi ALU. Un point important est que le CPU dispose de 64 registres entiers et de 64 registres flottants. C'est deux fois plus que supporte le jeu d'instruction ARM. En mode ARM, seule la moitié des registres est utilisée. Mais en mode optimisé, le traducteur binaire utilise bien les 64 registres, grâce à une sorte de renommage de registres logicielle. Pour simplifier la traduction binaire, le processeur supporte les techniques vues précédemment. Le processeur intègre un ''gated store buffer'' similaire à celui des processeurs Transmetta. Les techniques d'exceptions différées sont aussi supportées, comme sur les processeurs Transmetta. Il a aussi une unité de préchargement décrite par NVIDIA comme agressive, avec support du préchargement de type anticipé (''runahead''). ===L'émulation de la mémoire virtuelle=== Pour finir, il faut parler de la mémoire virtuelle avec la traduction binaire. Sur le processeur source, le processeur gère à la fois des adresses physiques et virtuelles et sa MMU fait la conversion entre les deux. Sur le processeur VLIW, la MMU est simulée par le programme de traduction binaire, partiellement ou totalement. L'implémentation exacte varie suivant que l'on parle du projet Daisy, des processeurs Transmetta ou de Denver. Sur les processeurs Transmetta, il n'y a pas de MMU ni de mémoire virtuelle. A la place, le programme de traduction binaire émule la mémoire virtuelle du processeur source. Le processeur VLIW ne gère que des adresses physiques, rien d'autre. L'espace d'adressage physique du processeur VLIW a la même taille que celui du processeur émulé, ici des processeurs x86. Il est vraisemblable que les adresses physiques utilisées par le processeur x86 sont les mêmes que celles du processeur VLIW. Sur le projet Daisy, le processeur VLIW gère la mémoire virtuelle via pagination, comme les processeurs émulés. Ce qui fait qu'il gère un espace d'adressage virtuel et un espace d'adressage physique. Pour éviter toute confusion, nous parlerons d'adresse physique/virtuelle VLIW pour les adresses physiques/virtuelles du processeur VLIW, d'adresse virtuelle/physique source pour celle du jeu d'instruction traduit, à savoir du Power PC ou du s390, éventuellement de l'x86. Il faut alors faire le lien entre adresses physiques source et adresses virtuelles VLIW. Pour cela, rien de plus simple : il y a correspondance parfaite. L'adresse physique source numéro N correspond à l'adresse virtuelle VLIW numéro N. Cependant, cela signifie que tout l'espace d'adressage virtuel VLIW serait utilisé par le code à traduire. En réalité, il faut ajouter de la place pour le code traduit et le programme de traduction binaire. La conséquence est que l'espace d'adressage virtuel VLIW est plus large que l'espace d'adressage source. L'espace d'adressage virtuel VLIW est découpé en trois sections : une section pour le code Power PC à traduire, une autre pour le code traduit en VLIW, et une dernière réservée au traducteur binaire. Le programme de traduction est placé dans une ROM mappée en mémoire dans la seconde section. Le reste de la seconde section est réservé à une mémoire RAM utilisée par le programme de traduction. [[File:Mémoire virtuelle sur le projet Daisy.png|centre|vignette|upright=1.5|Mémoire virtuelle sur le projet Daisy]] Il faut noter que chaque page physique source, une fois traduite, correspond à N pages virtuelles VLIW. La raison est que le code traduit prend plus de place que le code machine originel. En conséquence, on doit utiliser une page finale plus grande. Pour le reste, des pages contiguës en mémoire physique source sont elles aussi contiguës en mémoire virtuelle VLIW. Le calcul d'adresse est donc simple : l'adresse physique source est multipliée par N, puis on ajoute l'adresse de base à laquelle commence la section pour le code traduit. ==La traduction binaire sur les Burroughs B1700== Nous venons de voir comment accélérer en matériel la traduction binaire, avec des techniques bien spécifiques. Le Burroughs B1700 a procédé autrement et il est intéressant d'étudier en détail son architecture. Son architecture est décrite dans le livre "Interpreting Machines : Architecture and Programming of the Bl700/Bl800 Series", écrit par Elliott I. Organick et James A. Hinds. Une partie de ce qui vfa suivre est un très court résumé de ce livre. ===Le Burroughs B1700 : le mal-nommé ''split-level control store''=== : Dans ce qui suit, nous parlerons de traducteur binaire pour parler soit d'un interpréteur pour un langage de programmation de haut niveau, soit pour un logiciel de traduction binaire. Le Burroughs B1700 avait plusieurs modèles : le B1726, le B1710 et le B1800. * Le B1710 plaquait le traducteur binaire en mémoire RAM. * Le B1800 avait ajouté un cache dédié au code du traducteur binaire, afin d'améliorer les performances. * Le B1726 utilisait un ''local store'' dédié au traducteur binaire. Le B1726 peut exécuter le traducteur binaire soit depuis le ''local store'', soit depuis la mémoire RAM avec une pénalité en termes de performances. La documentation appelle cela un '''''split-level control store''''', mais le terme est trompeur : la documentation qualifie de microcode le langage machine du Burroughs B1700, ce qui est à l'origine de beaucoup de confusions. Une autre source de confusion est que ce langage machine a quelques propriétés qu'on pourrait attendre d'un microcode, mais certaines sont manquantes. Par exemple, il peut adresser les registres d’interfaçage mémoire, mais c'est parce que ce sont des registres architecturaux sur ce processeur. Ses instructions n'encodent pas de signaux de commande, par contre, elles font 16 bits et adressent les opérandes de manière implicite. Le ''local store'' mémorisait au maximum 2048 instructions machine, ce qui fait que les 2048 premières "adresses" correspondaient au ''local store''. Les suivantes étaient des adresses de mémoire RAM qui contenaient le reste du traducteur binaire. En pratique, l'adresse qui séparait les deux était configurable ! On pouvait limiter la taille du ''local store'' à un multiple de 32 instructions ! Les adresses restantes étaient alors réattribuées à la RAM. Par exemple, on pouvait attribuer 64 adresses au ''local store'', les 2048 - 64 adresses restantes étaient alors attribuées à la mémoire RAM. Un registre limite, nommé TDPM, indiquait quelle était l'adresse de démarcation entre les deux. Il contenait 4 bits, dont la valeur était multipliée par 32, pour obtenir l'adresse limite. Le programmeur devait découper le traducteur binaire en segments, certains étant placés dans le ''local store'', les autres étant en mémoire RAM. Mieux que ça, le processeur autorisait d'utiliser la technique de l'''overlaying'' pour le microcode ! Il y avait même une fonction OVERLAY dédiée pour ! OVERLAY avait besoin de trois opérandes : la taille du segment à copier, son adresse source en mémoire RAM, son adresse de destination dans la micro-SRAM. L'interpréteur réservait un "segment" en mémoire RAM, pour les données, dont le processeur connaissait la position. En-dehors de ce segment, il n'y a que des instructions machines à traduire, et le traducteur binaire. Le CPU incorporait pour cela deux registres, un pour l'adresse de base du segment de données, un autre pour son adresse de fin. Il incorporait une protection mémoire limitée sur ce segment, à savoir que seul l'interpréteur pouvait lire ou écrire dans ce segment. ===L'ALU de taille variable=== Les Burroughs B1700 avaient une '''ALU de taille variable''', ce qui veut dire qu'elle pouvait faire des calculs sur un nombre de bits compris entre 0 et 24. Pour cela, les calculs étaient fait avec une ALU de 24 bits, puis les bits de poids fort inutiles étaient masqués. L'ALU de 24 bits pouvait masquer les bits de poids fort du résultat, mais avec des limitations. Elle gérait des opérandes et résultats de 0, 4, 8, 12, 16, 20 et 24 bits. Pour avoir un réglage plus fin, une ALU 4 bits séparée permettait de corriger le résultat. Les registres X et Y étaient reliés à une ALU de 24 bits capable d'effectuer des opérations arithmétiques basiques. Fait étonnant, l'ALU faisait tous les calculs en même temps et fournissait ses résultats dans 7 registres : un registre SUM pour l'addition, un registre DIFFERENCE pour la soustraction, et un registre par opération bit à bit. Les registres pour les opérations bit à bit sont : CMPX et CMPY pour l'opération NOT sur chaque opérande, XANY pour le ET bit à bit, XEOY pour le XOR, XORY pour le OU logique. Les additions et soustractions pouvaient se faire sur des opérandes codés en binaire ou en BCD, suivant le code opération utilisé. A cela, il fallait ajouter deux registres de "résultat" MSKX et MSKY. Ils fournissaient un masque dépendant de la taille du résultat en bits. C'est ce registre qui permettait de gérer des données de 0, 4, 8, 12, 16, 20, et 24 bits, alors que l'ALU faisait 24 bits. Le masque dans ce registre pouvait servir d'opérande dans une opération de masquage ultérieure, pour corriger le résultat. L'ALU prenait en opérande les registres X et Y, mais aussi un registre CYF pour la retenue entrante. En sortie, il fournissait aussi deux retenue sortantes : une pour l'addition et une autre pour la soustraction. Elles étaient mémorisées dans les registres CYL et CYD, respectivement. L'ALU fournissait aussi 12 conditions, qui alimentait un registre d'état. Le registre d'état était segmenté en trois registres appelés XYST, XYCN et BICN. Les registres X et Y sont des registres pour les opérandes, mais il y a aussi deux autres registres de 24 bits. Il s'agit des registres T et L, qui sont des registres généraux. Les registres T et L étaient découpés en 6 sous-registres de 4 bits, nommés LA LB LC LD LE et LF pour le registre L, et TA TB TC TD TE et TF pour le registre T. Ils étaient adressables comme n'importe quel registre. Le processeur contenait de nombreux autres registres de 4 bits, dont les sous-registres de T et L. En tout, il y a 27 registres de 4 bits adressables ! Et le processeur disposait d'une ALU 4 bits pour manipuler ces registres de 4 bits. L'ALU 4 bits est capable de faire des MOV entre registres de 4 bits, des opérations bit à bit, et peut aussi tester si un bit vaut 0 ou 1. La dernière possibilité est très utile pour implémenter les branchements. De plus, cela permettait de faire des opérations de masquage, notamment pour appliquer un masquage supplémentaire à celui de l'ALU. Par exemple, pour gérer des opérations sur 14 bits, on effectue une opération sur 16 bits avec l'ALU, et on masque les 2 bits manquants avec l'ALU 4 bits. ===Une architecture bit-adressable=== Le CPU Burroughs B1700 était '''bit-adressable'''. En clair, il pouvait adresser la mémoire bit par bit, si nécessaire. Un avantage est que cela facilitait la traduction binaire des instructions machine, mais nous détaillerons cela plus bas. Un autre avantage est que cela permettait d'émuler des architectures dont la taille des registres/''byte''/mots était très différente. Et cet avantage mérite quelques explications immédiates. Le processeur était un CPU 24 bits, mais il pouvait émuler des processeurs 16 bits, 8 bits, 4 bits, 1 bit, 9 bits, ou toute autre valeur. Pour cela, le processeur lisait des mots de 24 bits et utilisant un ''barrel shifter'' pour sélectionner les bits adéquats. Par exemple, pour un accès de 16 bits, la donnée lue en mémoire était masquée de manière à ne garder que les 16 bits de poids faible. Le ''barrel shifter''/circuit de masquage était placé directement avant le bus mémoire, sur le trajet des données/instructions. Le ''barrel shifter'' était commandé par un registre ''Field Unit'', qui précisait quelle était la taille des données à charger. Pour communiquer avec la mémoire, il avait trois registres séparés : READ, WRITE, MAR. READ contient la donnée lue lors d'une lecture, WRITE est pour une donnée à écrire lors d’une écriture, MAR contient l'adresse à lire ou en cours de lecture. Il s'agit de registres d’interfaçage mémoire. Le fait que ces registres d’interfaçage mémoire et IO soit adressable est peu commun, et c'est plus quelque chose qu'on attend d'un microcode que d'instructions machines. Mais il faut bien comprendre que les registres d’interfaçage mémoire ont été promus au rang de registres architecturaux, sur ce processeur. De même, pour communiquer avec les entrées-sorties, il disposait de deux registres CMND et DATA : CMND pour envoyer une commande à une entrée-sortie, DATA pour lire ou écrire une donnée. Il y avait aussi un registre U, qui mémorisait une donnée lue depuis le lecteur de cassette. ===Le code machine du Burroughs B1700=== Le processeur utilise des instructions de 16 bits de long, qui sont copiées dans registre d'instruction M de 16 bits. Le ''program counter'' est appelé le registre A. Les instructions du processeur font 16 bits de long, et elles sont alignées en RAM sur 16 bits, ce qui fait que les 4 bits de poids fort de ce registre valent 0, seuls les 14 bits de poids fort sont utiles. Les adresses mentionnées plus haut, qui adressent les micro-opérations en mémoire RAM, sont des adresses de mots de 16 bits, pas les adresses de bit individuels. Fait intéressant, il était possible de faire un OU logique entre l'instruction lue et un registre du processeur, le résultat étant mémorisé dans le registre M. Cela permettait de faire du code automodifiant, pour émuler des modes d'adressage complexe. Cela permettait par exemple de remplacer une adresse dans une micro-opération. L'adresse est alors surimposée avec un OU dans le champ d'adresse, alors que le reste de l'instruction n'est pas modifié. Afin de gérer les interruptions, ces dernières étaient partiellement détectées en logiciel. Une interruption doit être prise en compte à la toute fin de l'exécution d'une instruction source, quand celle-ci a finit son travail. Mais vu qu'ici, les instructions sont émulées avec des séries d'instructions cibles, il y a un problème : il est possible d'interrompre une instruction émulée en plein milieu. Pour éviter cela, le processeur ne déclenche pas les interruptions matérielles immédiatement, elles sont mises en attente. Une instruction dédiée vérifie si une interruption est en attente, et l'exécute alors cas échéant. L'instruction en question est utilisée en dernier, dans la suite d'instruction qui émule l'instruction source. Les Burroughs B1700 intégrait une pile d'adresse de retour, capable de mémoriser 16 adresses de 24 bits. Il disposait de 4 registres généraux, nommés X, Y, T et L, tous de 24 bits. Un point important est que le registre T avait des possibilités d’extraction spécifiques. Il était possible d'extraire N bits, placés n'importe où dans ce registre, et de les copier dans un autre registre. Le CPU supportait une '''instruction EXTRACT''' pour ça. Nous verrons pourquoi cette possibilité est intéressante et ne sert que pour le registre T. ===La traduction binaire sur le Burroughs B1700=== Les instructions source peuvent faire 16 bit avec tel jeu d'instruction, 8 bits avec un autre, 32 bits sur un autre, etc. Elles peuvent aussi être de longueur fixe ou variable ! Le B1700 utilisait une architecture bit-adressable pour gérer ces contraintes. En effet, avec un CPU bit-adressable, les contraintes d'alignement des instructions disparaissent ! Dans ce qui suit, on suppose que l'instruction est composée de '''champs''' : l'opcode, les opérandes, les adresses absolues, les constantes immédiates, les numéros de registre, etc. L'idée est que l'instruction source est chargée dans le registre T, puis découpée en champs, qui sont copiés dans les registres du processeur ou envoyés au microcode. Je rappelle que le registre T est relié à un ''barrel shifter'', différent au précédent, qui permettait de masquer, décaler et extraire une donnée de ce registre. Et il y a la possibilité d'extraire une suite de bit de ce registre T, pour la copier dans un autre registre. Par exemple, prenons une instruction composée d'un opcode, d'une constante immédiate et d'une adresse immédiate. Premièrement, elle est chargée dans le registre T. Deuxièmement, la constante immédiate est extraite du registre T et copiée dans le registre X. Troisièmement, l'adresse est extraite et est copiée dans le registre d’interfaçage mémoire, une lecture est démarrée, et l'opérande lue est copiée dans le registre Y. Enfin, l'instruction équivalente à l'opcode est exécutée par le microcode. Le tout prend plusieurs instructions : une première pour lire l'opcode, une seconde pour lire la première opérande, une troisième pour lire la seconde opérande, una quatrième pour tout exécuter. Notons que suivant la taille des champs, on peut en charger plusieurs à la fois. Par exemple, imaginons qu'on ait chargé 24 bits, contenant un opcode de 8 bits et une constante immédiate de 16 bits : le registre T est découpé en un opcode et une constante, avec deux instructions EXTRACT. L'architecture est bit-adressable car elle gère des champs qui ne font pas forcément 8 ou 16 bits. Par exemple, certains jeux d'instructions ont des opcodes codés sur 6 ou 7 bits, voire 9-10 bits. Utiliser une architecture bit-adressable règle ce problème d'alignement. On peut lire un champ depuis la mémoire RAM en précisant l'adresse de son premier bit, pas besoin de gérer des accès non-alignés. Par exemple, prenons une instruction source avec un opcode de 7 bits, suivi par une adresse de 12 bits et une constante immédiate de 15 bits. On peut charger 7 + 12 bits en un premier accès, pour extraite l'opcode et l'adresse, suivi par un second accès de 15 bits pour extraire la constante immédiate. Pour charger un champ, il faut connaitre plusieurs informations : l'adresse de son premier bit, sa taille. Pour cela, le B1700 contient deux registres, nommés ''Field Adress'' (FA) et ''Field Length'' (FL), qui indiquent l'adresse du premier bit et la taille de l'instruction à charger. Le premier fait 24 bits, ce qui permet de gérer des adresses de 24 bits. N'oubliez pas que le processeur est bit-adressable, ce qui permet d'adresser 16 Mébi-bits, soit 2 mébioctets. Le registre pour la taille fait lui 16 bits, ce qui permet de gérer des instructions de 65536 bits, soit 8192 octets ! Leur nom commence par ''field'', car le processeur peut gérer des instructions de taille variable et/ou plus longues que 24 bits. Dans ce cas, le chargement de l'instruction se fait champ par champ. Pour donner un exemple, prenons une instruction composée de 20 champs de 12 bits chacun. Les champs sont chargés un par un dans le registre T. Le registre FA est alors initialisé avec l'adresse de l'instruction, il est incrémenté de 12 à chaque cycle. Le registre FL est initialisé avec la taille de l'instruction, soit 20 * 12 = 240 bits, et il est décrémenté de 16 à chaque cycle. Précisons cependant que la taille d'un champ peut être changée d'un cycle à l'autre. Par exemple, le processeur peut charger un opcode de 8 bits, l'interpréter, détecter que l'instruction demande ensuite de charger une adresse de 16 bits. Dans ce cas, le registre FL est immédiatement altéré pour charger 16 bits, au lieu des 8 bits de l'opcode immédiatement précédent. ==Les jeux d'instructions dédiés à un langage de programmation== De rares processeurs sont conçus pour un langage de programmation en particulier. On appelle ces processeurs, conçus pour des besoins particuliers, des '''processeurs dédiés'''. Par exemple, les fameux ''Burrough E-mode'' B5000/B6000/B7000 étaient spécialement conçus pour exécuter de l'ALGOL-60. Leurs cousins B2000/B3000/B4000 étaient eux conçus pour le COBOL. Des langages fonctionnels ont aussi eu droit à leurs processeurs dédiés. Le prolog en est un bel exemple, avec les superordinateurs de 5ème génération qui lui étaient dédié. On peut aussi citer les machines LISP, dédiés au langage LISP, qui datent des années 1970. Elles étaient capables d’exécuter certaines fonctions de base du langage directement dans leurs circuits : elles possédaient notamment un ''garbage collector'' câblé dans ses circuits ainsi que des instructions machines supportant un typage déterminé à l’exécution. Les processeurs dédiés ont eu leur heure de gloire au début de l'informatique, à une époque où les langages de haut niveau venaient d'être inventés. À cette époque, les compilateurs n'étaient pas performants et ne savaient pas bien optimiser le code machine. Il était alors rationnel, pour l'époque, de rapprocher le code machine cible et le langage de programmation de haut niveau. De nombreuses architectures dédiés ont ainsi été inventées, avant que les concepteurs se rendent compte des défauts de cette approche. Les défauts en question ne sont pas nombreux, mais assez simples à comprendre. Premièrement, elles sont très rapides pour un langage de programmation en particulier, mais sont assez mauvaises pour les autres, d'où un problème de "compatibilité". Ajoutons à cela que les langages de programmation peuvent évoluer, devenir de moins en moins populaires/utilisés, ce qui rend la création d'architectures généralistes plus pertinente. Enfin, les architectures dédiées sont évidemment des processeurs CISC, pour implémenter les nombreuses fonctionnalités des langages évolués. Et les défauts des CISC sont assez rédhibitoires à l'heure actuelle. À l'heure actuelle, les algorithmes des compilateurs se sont améliorés et savent nettement mieux utiliser le matériel. Ils produisent du code machine efficace, ce qui rend les architecture dédiées bien moins intéressantes. Si on ajoute les défauts de ces architectures dédiées, par étonnant que les architectures dédiées aient presque disparues. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'accélération matérielle de la virtualisation | prevText=L'accélération matérielle de la virtualisation | next=Le matériel réseau | nextText=Le matériel réseau }} </noinclude> sjqanu5czh16a3c3x2cx8b9ip0xooog 764063 764062 2026-04-19T20:51:33Z Mewtow 31375 /* Le Burroughs B1700 : le mal-nommé split-level control store */ 764063 wikitext text/x-wiki De nos jours, la majorité des programmeurs programment dans des langages de haut niveau. Il est très rare pour eux d'avoir à utiliser de l'assembleur, et encore moins un langage machine. Les programmes écrits dans un langage de haut niveau sont traduits en langage machine par un logiciel appelé le compilateur. [[File:Compilation.PNG|centre|vignette|upright=2.5|Compilateur : principe.]] Il se trouve que quelques rares processeurs sont conçus pour faciliter le travail du compilateur. Et ce sont ces processeurs que nous allons voir dans ce qui suit. Ils représentent une catégorie de jeux d'instruction à part, qui n'a pas vraiment de nom. ==La compilation et l’interprétation== Un compilateur traduit donc du code source, écrit dans un langage de haut niveau, vers du langage machine. La traduction est rarement directe. En général, le compilateur traduit le code source en assembleur, qui est lui-même traduit en langage machine. L'assembleur est une représentation textuelle du langage machine alors que le code machine est du binaire exécutable par le processeur. La traduction se fait donc en deux étapes, la '''compilation''' proprement dite et l''''assemblage''', réalisées respectivement par un compilateur et un assembleur. Il faut parfois rajouter une troisième phase d'édition des liens, que nous passons volontairement sous silence. {| |[[File:Code C.png|vignette|200px|Le code source, ici en C.]] |[[File:Assem.png|vignette|250px|Le code assembleur.]] |[[File:Binary file - hello world (C programming).png|vignette|150px|Le code machine.]] |} ===L'interprétation et la compilation à la volée=== Une alternative à la compilation est l''''interprétation''', qui transforme le code source en code machine à la volée. Avec l'interprétation, le code source est passé à un logiciel appelé l''''interpréteur''', qui exécute le code source ligne de code par ligne de code. Une ligne de code est traduite en code machine, qui est exécutée, puis l'interpréteur passe à la ligne suivante. [[File:Kaantotulkkaus.png|centre|vignette|upright=3|Compilation versus interprétation.]] Un défaut de l'interprétation est son cout en performance, qui est assez important. Aussi, l'interprétation stricte a évolué vers un hybride entre interprétation et compilation. L'idée est qu'une partie peu importante du code source est interprétée, alors que le code important est compilé juste avant d'être exécuté. Pour le dire autrement, le code source est partiellement compilé à la volée, juste avant son exécution. Aussi, on parle de '''compilation à la volée'''. Le terme en anglais est ''Just In Time compilation'', abrévié en JIT. En soi, le JIT est associé à de l'interprétation, les JIT sont en réalité des hybrides interpréteurs-compilateurs. Un point important est que compiler du code à la volée est assez lourd, cela a un cout en performance. Par contre, le code compilé s'exécute plus vite que du code interprété. Si on exécute le code compilé une seule fois, le cout de la compilation l'emporte sur le gain à l'exécution. Mais le code compilé est réutilisé autant de fois que nécessaire. Le gain à l'exécution est donc multiplié par le nombre d'exécutions, alors que le cout de la compilation est répartit sur plusieurs exécutions, il est amorti. Les portions du code sont choisies de manière à avoir un gain en performance maximal. L'idée générale est que le code exécuté très souvent est compilé, alors que du code exécuté pas souvent est interprété. Typiquement, les boucles les plus souvent exécutées sont compilées, le code exécuté une seule fois est interprété. ===Les langages intermédiaires=== Les compilateurs modernes passent par un '''langage intermédiaire''' pour faire la transformation en code machine. Le compilateur traduit le langage de haut niveau en langage intermédiaire, puis traduit le langage intermédiaire en code machine cible. Le langage intermédiaire est parfois appelé le '''''bytecode''''', et ce terme recouvre aussi le code écrit avec du ''bytecode''. L'interprétation aussi peut passer par l'intermédiaire d'un un ''bytecode'', ce n'est pas limité aux compilateurs proprement dit. {| |[[File:Intermediate representation scheme.png|centre|vignette|upright=1.5|Représentation intermédiaire d'un compilateur.]] |[[File:Code interpreter scheme.png|centre|vignette|upright=1.5|Fonctionnement d'un interpréteur.]] |} Faire ainsi a de nombreux avantages, le principal étant d'avoir un compilateur capable de traduire un langage de haut niveau vers plusieurs jeux d'instructions différents. Cela permet d'avoir, par exemple, un compilateur qui traduit du C soit en code machine x86, soit en code machine pour un CPU ARM, soit pour un CPU POWERPC, etc. [[File:Compiler design.svg|centre|vignette|upright=3|Compiler design]] Pour l'interprétation, l'usage d'un ''bytecode'' a d'autres avantages. Pour l'exploiter, il faut distribuer non pas le code source, mais le ''bytecode''. En clair, l'interpréteur ne prend pas en entrée le code source, mais du ''bytecode'' déjà compilé. La traduction est alors beaucoup plus simple, car le code source a déjà été partiellement compilé, par la traduction en ''bytecode''. Interpréter du code source est en effet assez compliqué : il faut effectuer des étapes d'analyse lexicale, sémantique, et bien d'autres. Avec du ''bytecode'', ces étapes ont été réalisées lors de la compilation du ''bytecode'', l'interpréteur a alors peut de choses à faire. L'interprétation du ''bytecode'' se fait instruction par instruction, au niveau du ''bytecode''. Une instruction du ''bytecode'' est traduite en une instruction machine équivalente, qui est exécutée. Si l'instruction est un peu complexe, il exécute une fonction/procédure qui fait la même chose. Pour résumer, le ''bytecode'' est ensuite traduit à la volée et exécuté instruction par instruction par un logiciel appelé l'interpréteur. [[File:Control table.png|centre|vignette|upright=2|Control table]] Le langage intermédiaire peut être vu comme l'assembleur d'un processeur, n'existe pas forcément dans la réalité, mais dont le jeu d'instruction est décrit en détail. Le processeur en question est appelé une '''machine abstraite''', ou encore une ''machine virtuelle''. Nous utiliserons le terme de machine abstraite dans ce qui suit. La machine abstraite n'est pas la même suivant que l'on cible la compilation ou l'interprétation/JIT. Pour l'interprétation/JIT, la machine abstraite est souvent une machine à pile, car cela simplifie grandement la traduction en code machine final. L'interprétation demande que la traduction du ''bytecode'' en code machine soit la plus simple possible. Et cela demande de prendre en compte pas mal de détails. Par exemple, les différents jeux d'instruction existants n'ont pas le même nombre de registres, ce qui pose problème lors de la traduction du ''bytecode''. Lors de la transformation en code machine, un algorithme d'allocation de registres se débrouille pour traduire le code intermédiaire en code qui utilise un nombre limité de registres. Pour améliorer cette allocation de registres, il y a deux solutions niveau ''bytecode'' : soit utiliser un nombre illimité de registres, soit utiliser une machine à pile. Pour les machines à pile, il existe un algorithme simple et rapide pour traduire un code écrit pour une machine à pile en un code écrit pour un processeur avec des registres, qui se débrouille pas trop mal pour allouer efficacement les registres. C'est un avantage assez important pour les langages interprétés. ==Les processeurs qui exécutent du bytecode== Le ''bytecode'' est un code machine, ce qui signifie qu'il peut en théorie s'exécuter sur un processeur qui implémente le jeu d’instruction associé. Si le jeu d'instruction d'un ''bytecode'' est souvent une description censée être fictive, elle n'en reste pas moins un jeu d'instruction et des caractéristiques précises, qu'on peut l'implémenter en matériel ! Néanmoins, tous les ''bytecode'' ne sont pas égaux de ce point de vue. Certains sont faciles à implémenter en matériel, d'autres non. Les ''bytecodes'' implémentés en matériel sont ceux dont les machines abstraites sont des machines à pile. Un premier exemple est celui des processeurs Pascal MicroEngine, qui exécutaient directement le bytecode du langage Pascal, le fameux UCSD P-code. Un second exemple est la '''machine SECD''', qui sert de langage intermédiaire pour certains compilateurs de langages fonctionnels. Elle a été implémentée en matériel par plusieurs équipes, notamment par les chercheurs de l'université de Calgary en 1989. Dans le même genre, quelques processeurs simples étaient capables d’exécuter directement le ''bytecode'' du langage FORTH. Le FORTH, un des premiers langages à pile de haut niveau, possède de nombreuses implémentations hardware et est un des rares langages de haut niveau dont le ''bytecode'' a été utilisé comme langage machine sur certains processeurs. Par exemple, on peut citer le processeur FC16, capable d’exécuter nativement du ''bytecode'' FORTH. ===Les processeurs Java=== Le cas le plus impressionnant est celui de la machine virtuelle Java, qui est un design de processeur comme un autre. En temps normal, le ''bytecode'' Java est compilé ou interprété, mais certains processeurs exécutaient du ''bytecode'' Java directement, sans interprétation ni compilation. Ils sont appelés des '''processeurs Java'''. On peut citer les processeurs ARM disposant de l'extension Jazelle, du JEMCore et du aJ-100 de aJile Systems, le picoJava II de Sun, et quelques autres. Il y a aussi eu quelques projets de recherche ou processeurs open source, comme Komodo, jamuth, le ''Java Optimized Processor'' et quelques autres projets du même genre. L'implémentation de la machine virtuelle Java n'était cependant pas complète. Quelques instructions complexes n'étaient pas gérées par le processeur et devaient être émulées en logiciel. Mais la grosse majorité des instructions l'était. Les instructions non-supportées étaient émulées avec la méthode ''trap and emulate'', à savoir qu'un opcode inconnu déclenchait une exception matérielle ''opcode invalide'', dont la routine pouvait émuler les instructions qui doivent l'être. Le processeur aJ-100 de aJile est capable d'exécuter la quasi-totalité des instructions de la JVM Java, seules deux instructions faisant exception. Les '''accélérateurs Java''' sont une solution intermédiaire, qui permet de traduire à la volée du ''bytecode'' Java en instructions RISC, exécutées sur un autre processeur. Il s'agit formellement de traduction binaire, mais je préfère en parler ici. L'idée est de combiner un processeur RISC avec un coprocesseur Java. Le coprocesseur lit le ''bytecode'' Java et le traduit en instructions RISC, ces dernières étant exécutées sur le processeur RISC. Le JA108 de Nozomi était un coprocesseur de ce genre, au même titre que le coprocesseur JSTAR de JEDI. L'extension Jazelle de certains CPU ARM fonctionnait vraisemblablement sur un principe similaire, sauf que le coprocesseur était intégré dans le processeur ARM, entre le cache d'instruction et le décodeur d'instructions ARM. L'intérêt des processeurs Java n'est pas qu'une question de performance. Certes, ils permettent d’exécuter plus vite les programmes compilés en ''bytecode'', comme des programmes Java pour la JVM Java. Mais l'intérêt est aussi de faire des économies de mémoire RAM et ROM. Je rappelle que la machine virtuelle Java est une machine à pile, ce qui fait que sa densité de code est excellente. Les programmes en ''bytecode'' Java sont donc très petits, et prennent peu de place en mémoire ROM. De plus, utiliser un processeur Java permet de se passer d'interpréteur ou de JIT Java, ce qui économise encore plus de mémoire. ===Transformer un processeur RISC en machine à pile=== La plupart des processeurs Java utilisent en interne une architecture à registre généraux, qui émule une machine à registre en microcode. Quelques registres sont réservés pour l'état de la machine virtuelle, avec au minimum un registre réservé pour le pointeur de pile, un autre pour un pointeur vers un ''pool'' de constantes, et quelques autres. D'autres registres servent à mémoriser le haut de la pile, à savoir le sommet de la pile et quelques opérandes situées juste en-dessous. Par exemple, le processeur picoJava II gardait les 64 opérandes au sommet de la pile dans 64 registres généraux, séparés des registres pour les pointeurs et l'état de la JVM. Une optimisation possible, envisagée sur les processeurs picoJava, était une sorte de macro-fusion sous stéroïde. L'idée était de fusionner certaines séries d'instructions Java en une seule instruction machine. Typiquement, pour faire une opération arithmétique, une machine à pile empile deux opérandes et exécute une opération, ce qui prend trois instructions machine. La spécification picoJava 1 a proposé d'envoyer directement la seconde opérande en entrée de l'ALU, ce qui fusionnait l'empilement de la seconde opérande et l'opération arithmétique. Le processeur picoJava II va plus loin et fusionne les trois instructions en une seule instruction RISC, agissant sur des registres. Mais la majorité des processeurs Java ne faisaient pas cela, car cela complexifie grandement le décodeur. Les processeurs ''bigfoot'' de DCT utilisaient un système différent pour émuler une pile avec des registres. Leur banc de registre de 64 bits était coupé en deux : les 16 premiers registres étaient utilisés pour la pile, les 48 suivants étaient utilisés pour autre chose. L'idée est que le registre 0 mémorise un emplacement vide, le registre 1 contient le sommet de la pile, le registre 2 l'opérande sous le sommet de la pile, et ainsi de suite. Les 15 premières opérandes de la pile sont donc mémorisées dans les registres. Les instructions écrivent leur résultat dans le registre adéquat, puis tous les registres sont décalés pour que le résultat passe dans le registre R1. Par exemple, les instructions PUSH chargent une opérande dans le registre 0, puis tous les registres sont décalés de un rang, pour que l'opérande chargée soit dans le registre 1. Le registre 0 devient alors le registre numéro 1, le registre 1 devient le 2, et ainsi de suite. Par contre, les opérations arithmétiques font autrement. Une instruction d'addition, par exemple, lit les opérandes dans le registre 1 et 2, puis place son résultat dans le registre 2 (les deux opérandes sont dépilées). Puis, les registres sont décalés d'un rang pour que ce registre 2 devienne le 1. Les registres ne sont en réalité pas décalés, il n'y a pas de transferts entre registres. A la place, les 16 premiers registres utilisaient une forme limitée de renommage de registres, similaire au banc de registre tournant utilisé sur les processeurs EPIC comme l'Itanium. Les numéros de registres sont renommés, décalés, à chaque fois qu'on empile ou dépile une opérande. Pour faire le renommage de registres, processeur contient un ''register stack counter'' de 4 bits, qui est incrémenté à chaque fois qu'on empile une donnée, et est décrémenté quand on dépile une donnée. Quand on veut accéder à un opérande, le numéro de registre architectural associé est traduit en numéro de registre physique, en additionnant le ''register stack counter'' (modulo 16, pour rester sur 4 bits). ==L'accélération de l'interprétation/JIT : le ''Thumb-EE'' d'ARM== Quelques processeurs ont ajouté des instructions pour faciliter le travail des interpréteurs/JIT. Par exemple, en 2005, ARM a ajouté le mode ''Thumb Execution Environment'', qui faisait cela. Il reprenait le jeu d'instruction compact ''thumb'' et ajoutait quelques instructions et modifiait le comportement d'instructions existantes. Le jeu d'instruction ''Thumb-EE'' a été ajouté sur les CPU ARM en 2005, mais a été déprécié en 2011, par manque d'utilité. Mais la tentative mérite qu'on s'attarde dessus. Les CPU de gamme M, déstinés à l'embarqué, n'ont jamais supporté ''Thumb-EE'', vu que de tels processeurs ne sont pas conçus pour exécuter du code interprété/JIT. Le mode ''Thumb-EE'' est un mode d'exécution, séparé de l'ARM normal. Ainsi, une instruction ''Thumb-EE'' et une instruction ARM peuvent avoir le même encodage en binaire, mais se comporter différemment. Le processeur est à tout moment soit en mode ''Thumb-EE'', soit en mode ARM normal, ce qui précise comment ces instructions doivent se comporter. Des instructions ont été ajoutées par ''Thumb-EE'', pour qu'il fasse son travail. L'entrée et la sortie du mode ''Thumb-EE'' se fait avec deux instructions : ENTERX et LEAVEX. Une fois en mode ''Thumb-EE'', le décodage des instructions ''thumb'' se fait avec les règles du ''Thumb-EE'', et non celles du ''thumb'' normal. Une première différence entre ''thumb'' normal et ''Thumb-EE'' est l'ajout de ce qui s'appelle un ''null check'' pour les instructions mémoire. Le ''null check'' est utilisé pour les instructions en mode d'adressage "base + indice" ou "Base + décalage", ou tout autre mode d'adressage avec un registre de base utilisés dans des calculs d'adresse. L'idée est de vérifier si l'adresse de base vaut zéro ou non. Si c'est le cas, le processeur lève une exception matérielle, qui est traitée par l'interprétation ou le compilateur JIT. Une telle situation est en effet signe d'une erreur d'adressage mémoire, qui doit être traitée par l'interpréteur. Typiquement, c'est signe qu'un pointeur n'a pas bien été initialisé, ce qui peut arriver avec du code interprété ou JIT. Les interpréteurs/JIT traitent généralement la situation en ajoutant un test avant toute instruction mémoire, pour vérifier si le pointeur accédé vaut zéro ou non. Avec ''Thumb-EE'', pas besoin d'ajouter les tests en questions : ils sont réalisés automatiquement lors de chaque instruction mémoire. : Les registres pour la pile, comme le pointeur de pile et le pointeur de ''frame'', sont aussi concernés. L'instruction CHKA vérifie que les accès à un tableau ne débordent pas en dehors du tableau en question. Par exemple, pour un tableau de 1024 éléments, elle vérifie si l'indice est compris entre 0 et 1024. Pour cela, elle vérifie si l'indice de tableau est bien dans l'intervalle adéquat et lève une exception matérielle si ce n'est pas le cas. Son utilité se comprend quand on sait que tous les accès à un tableau sont vérifiés par l'interpréteur. L'interpréteur est censé ajouter des instructions pour vérifier les indices, à savoir deux ou trois branchements. Avec CHKA, le test se fait en une seule instruction. D'autres modifications mineures de l'encodage des instructions sont aussi présentes, ainsi que des modifications pour les instructions LOAD/STORE. Précisément, dans les modes "base + indice" et "base + décalage", le décalage et l'indice subissent maintenant des décalages. L'idée est de simplifier le calcul d'adresse. De quoi économiser quelques instructions lors des calculs d'adresse, ce qui facilite le travail de l'interpréteur/JIT. Et des instructions LOAD/STORE précises voient leur comportement modifié pour faciliter la gestion de la pile de Java ou l'exécution des méthodes locales. Pour faciliter le travail de l'interpréteur, deux instructions HBP et HBLP ont été ajoutées. Elles branchent vers une fonction qui gère les exceptions logicielles (une fonctionnalité présente dans de nombreux langages, comme Java). La première effectue un branchement vers la fonction en question, la seconde sauvegarde l'adresse de retour avant de faire ce branchement. Les deux peuvent brancher vers 256 fonctions pré-déterminées, sans avoir à présenter leur adresse. ==La traduction binaire accélérée par le matériel== Plus haut, nous avons surtout parlé des compilateurs et des interpréteurs. Cependant, nous devons aussi parler de la '''traduction binaire'''. Elle traduit un programme écrit dans un code machine vers un autre code machine. Par exemple, elle traduit un programme compilé pour un CPU x86 vers un code machine ARM. En général, le système d'exploitation est généralement compilé pour le jeu d'instruction natif, mais il exécute des applications prévues pour le x86. Les applications sont traduites par le système d'exploitation, avant d'exécuter le code traduit. : Nous parlerons dans la suite de code machine source et de code machine cible, pour parler respectivement du code à traduire et du code obtenu après traduction. Idem avec d'autres termes comme architecture cible/source, ou jeu d'instruction cible/source. La traduction binaire est surtout utilisée pour des questions d'émulation ou de compatibilité. Par exemple, lorsque les Macintosh sont passés de processeurs Power PC vers des processeurs x86, le système d'exploitation Mac OS utilisait la traduction binaire pour convertir les anciennes applications Power PC vers du code x86. Les utilisateurs n'y ont vu que du feu. Le système de traduction binaire était appelé Rosetta 1. Par la suite, lors de la transition de processeurs x86 vers des processeurs Apple, Rosetta 2 a vu le jour. La traduction binaire a surtout été utilisée pour traduire du code x86 vers un autre jeu d'instruction. Il faut dire que le x86 est le jeu d'instruction dominant. De nombreuses entreprises ont eu pour ambition de briser l'hégémonie du x86 sur PC, en remplaçant le x86 par un jeu d'instruction plus performant, tout en gardant une compatibilité maximale. La traduction binaire était la seule solution pratique. Elle était le plus souvent intégralement réalisée en logiciel, comme c'était le cas sur les architectures Itanium avec le ''IA-32 Execution Layer''. ===Les généralités sur la traduction binaire assistée en matériel=== L'architecture source est presque tout le temps une architecture CISC, assez ancienne, qu'on souhaite émuler. Le choix de l'architecture cible se porte souvent sur une architecture VLIW, fort différente de l'architecture source, et ce pour obtenir une économie de matériel conséquente. Pour s'exécuter rapidement, le code traduit doit exploiter le parallélisme d'instruction, à savoir exécuter plusieurs instructions en même temps dans des unités de calcul séparées. Il est possible d'utiliser un processeur superscalaire avec exécution dans le désordre pour cela, mais au prix d'un cout important en transistors. Alors qu'en utilisant un CPU VLIW, c'est le traducteur binaire qui fait tout le travail d'extraction du parallélisme d'instruction. Et après tout, quitte à avoir un traducteur binaire, autant lui refiler le boulot d'optimisation. Compiler du code à la volée est certes assez lourd, mais qu'il existe des algorithmes efficaces pour regrouper des instructions indépendantes dans une seule instruction VLIW. Au passage, le processeur VLIW a plus de registres que le processeur source. Cela permet de faire du renommage de registres directement en logiciel. Le traducteur binaire n'hésite pas à changer les noms de registres entre instructions source et instruction VLIW cible, afin de supprimer des dépendances de données. En conséquence, cela ouvre des opportunités de parallélisme, qui sont exploitées lors du regroupement des opérations en instructions VLIW. Vous pourriez penser que le choix d'un CPU VLIW pour émuler du CISC est tout sauf optimal. Vous devez penser que la traduction binaire est d'autant plus simple que l'architecture source et cibles sont semblables. Dans les faits, ce n'est pas tellement traduire les instructions qui pose problème, mais plus la gestion du registre d'état, des exceptions matérielles, de la mémoire virtuelle, la différence entre gros-boutisme et petit-boutisme, de même que des différences d'adressage pour les périphériques. Le premier problème est la gestion des conditions, notamment en présence d'un registre d'état. En pratique, la traduction binaire s'utilise pour traduire du code CISC vers du code RISC ou VLIW. Les architectures source ont donc un registre d'état, qui est mis à jour non seulement par des instructions de test, mais aussi des instructions arithmétiques. L'architecture cible n'a elle pas de registre d'état, mais des registres à prédicats. La traduction de l'un vers l'autre est alors quelque peu compliquée. Et elle est d'autant plus compliquée que le registre d'état est la source de dépendances d'instruction implicites, qui réduisent les performances. Par exemple, si une instruction arithmétique modifie le registre d'état, cela peut impacter l'exécution d'une instruction ultérieure, qui lit ce registre d'état. Pour éliminer ces fausses dépendances, le traducteur binaire doit renommer ce registre en logiciel et détecter les dépendances utiles. Et le compilateur doit gérer les cas où il y a beaucoup de distance entre les deux, voire les cas où l'instruction dépendante n'a pas encore été analysée par le compilateur. Un autre problème est lié à la gestion des exceptions matérielles, et précisément des exceptions précises. Pour rappel, une instruction VLIW regroupe plusieurs opérations, opérations qui correspondraient à une instruction machine sur un CPU pas VLIW. En conséquence, plusieurs instructions du langage machine "source" sont regroupées en une seule instruction VLIW. Et il faut tenir compte du cas où une instruction source lève une exception. Dans le code VLIW, cela signifie qu'une opération lève une exception, et il faut annuler partiellement l'instruction VLIW associée. Et par partiellement, on veut dire que seules les opérations suivantes dans l'ordre du programme source doivent être annulées, puis ré-exécutées. Et c'est un sacré casse-tête ! Les processeurs VLIW utilisent comme solution le mécanisme d'exceptions différées des processeurs EPIC, vu il y a quelques chapitres. Pour résumer, le code est exécuté par blocs d'instruction, délimités par des branchements ou tout autre limite/barrière pertinente dans le code. Un bloc de code est compilé en deux versions : une version rapide sans exceptions matérielles, une version lente qui gère les exceptions précises. La version rapide s'exécute sans exécuter les exceptions. Cependant, les exceptions matérielles sont enregistrées, pour être prises en compte à la toute fin du bloc de code. La version rapide est exécutée en premier et elle mémorise si une exception matérielle a eu lieu. Puis une instruction vérifie si une exception a eu lieu et décide quoi faire. Si aucune exception n'a eu lieu, elle passe à la suite du programme, les résultats du bloc de code sont définitivement acceptés. Mais si une exception a eu lieu, tout est annulé. Le processeur est remis dans l'état initial, puis le code est ré-exécuté instruction par instruction de manière à gérer l'exception correctement. ===La traduction par pré-décodage=== Une première solution serait de faire la traduction binaire dans le cache d'instruction. Lors d'un défaut de cache, le code chargé depuis la RAM est traduit en code machine cible. Et c'est ce code machine cible qui est mémorisé dans le cache d'instruction et exécuté par le processeur. La technique marche sur le papier et n'est qu'une amélioration des techniques de pré-décodage vues il y a quelques chapitres. Cependant, le code obtenu est une traduction assez basique, qui n'incorpore pas d'optimisations dignes de ce nom. De plus, elle gère mal le cas où la taille du code source et cible sont potentiellement très différentes. Concrètement, elle est surtout utile pour traduire du code d'un processeur RISC vers un autre processeur RISC, dont les tailles d'instructions sont similaires. Elle ne permet pas d'émuler plusieurs jeux d'instructions différents, le cout en matériel (un circuit de pré-décodage par jeu d'instruction) serait trop important. ===Le projet DAISY d'IBM=== De très rares processeurs étaient conçus pour accélérer cette traduction binaire, afin de garder de bonnes performances. Les premiers à avoir étudié l'idée étaient IBM, avec leur projet DAISY (''Dynamically Architected Instruction Set from Yorktown''). Le projet de base était de convertir à la volée du code compilé pour des CPU Power PC, vers du code VLIW. Les chercheurs d'IBM avaient développé un algorithme de traduction dynamique efficace, ainsi qu'un processeur VLIW disposant d'optimisations spécifiques à la traduction binaire dynamique, à la volée. De nombreuses idées de ce projet ont été reprises ou re-découvertes par la société Transmetta, avec ses processeurs Crusoe et Efficieon, puis par NVIDIA avec son projet Denver. Les ingénieurs de ce projet ont étudié la possibilité d'émuler plusieurs jeux d'instructions différents sur un même processeur, notamment le s390 d'IBM et le x86. En théorie, cela demande juste d'avoir plusieurs programmes de traduction binaire : un pour le x86, un autre pour le s390, éventuellement un autre pour le Power PC. Mais cela ne s'est pas concrétisé. Mais les ingénieurs ont étudié quelle pourrait être l'architecture VLIW idéale pour ça. Par exemple, il fallait des additions 3-opérandes pour simplifier les calculs d'adresse. Une difficulté était la gestion du registre d'état, dont les bits ne sont pas mis à jour de la même manière sur le x86 et le s390 ou le Power PC. La différence entre gros-boutisme et petit-boutisme était aussi un problème, de même que des différences d'adressage pour les périphériques. Pour que la traduction binaire soit efficace, le processeur VLIW intègre diverses optimisations, comme les exceptions différées et les branchements multi-voies. La principale est la suivante : seul le code des boucles ou fonctions exécutées fréquemment est traduit en code VLIW, le reste du code est interprété. En effet, traduire du code binaire prend plus de temps qu'une simple interprétation. Pour du code qui ne sera exécutée qu'une seule fois, il est plus rapide d'utiliser l'interprétation. Par contre, pour du code exécuté beaucoup de fois, le cout de la traduction binaire est amorti, dilué sur N exécutions, compensé par le gain en temps d'exécution de ce code traduit. Au final, cela permet de ne traduire que le code qui le mérite. La détection du code fréquemment exécuté est réalisée dans le cœur VLIW. L'unité de branchement mémorise les derniers branchements rencontrés et le nombre de fois qu'ils ont été exécutés. Si ils ont été exécutés un certain nombre de fois, l'unité de branchement lève une exception matérielle, qui invoque le traducteur binaire. La routine de cette exception est le traducteur binaire proprement dit. En clair, la traduction binaire est démarrée quand le processeur détecte qu'une fonction a été exécutée plus de N fois, via une exception matérielle dédiée. La mesure du nombre d'exécution d'un branchement se fait dans l'unité de calcul dédiée aux branchements, avec l'aide d'une sorte de ''branch adress buffer'' modifié. Pour rappel, le ''branch adress buffer'' mémorise le ''Program counter'' de chaque branchement récemment rencontré. Ici, chaque entrée du ''branch adress buffer'' est associée à un compteur incrémenté à chaque exécution du branchement. ===Les processeurs Crusoe et Efficieon de Transmetta=== Les processeurs Crusoe Et Efficeron sont deux processeurs VLIW produits par la société Transmetta, une société californienne rachetée par NVIDIA. Ils étaient conçus pour exécuter spécifiquement des programmes x86, système d'exploitation inclus. L'idée derrière ce projet était d'exécuter du code x86 sans que les applications, ni même le système d'exploitation et le BIOS soient au courant ! Les processeurs Transmetta étaient en réalité conçus pour exécuter un programme unique, le ''Code Morphing Software'' (CMS), qui traduisait le code x86 en code VLIW. Transmetta ne voulait pas que son processeur VLIW soit exploité directement, sans le CMS. Il n'avait pas rendu public de compilateur pour traduire du C vers du code VLIW, il n'avait pas donné la documentation du jeu d'instruction VLIW. Des efforts de rétro-ingénieurie, documentés sur le site realworldtech, ont cependant permis de comprendre comment étaient encodées les instructions du processeur Crusoe. Comme pour le projet Daisy, le ''Code Morphing Software'' utilisait à la fois interprétation et traduction binaire, selon les besoins. Les instructions sont interprétées lors de leurs premières exécutions, mais le CMS bascule sur de la traduction en code VLIW après un certain nombre d'exécution. Ainsi, les boucles souvent exécutées sont traduites en code VLIW, alors que le reste du code est interprété. Le choix du code à traduire est le fait du CMS, il utilise des heuristiques complexes pour, qui ne sont pas connues dans le détail. Le ''Code Morphing Software'' est mémorisé dans une EEPROM, ce qui en fait un ''firmware'' situé en dessous du BIOS. Le CMS démarre ensuite le BIOS, qui lui-même démarre le système d'exploitation, qui lui-même démarre les pilotes de périphériques et les programmes. Le CMS se réserve les 16 premiers mébioctets de l'espace d'adressage. L'EEPROM du CMS est mappée dedans, mais n'en utilise que la moitié. Le reste est utilisé comme cache, pour mémoriser le code VLIW traduit par le CMS. Les deux processeurs intégraient aussi deux mémoires SRAM, utilisées par le CMS, appelées la ''local program memory'' (LPM) et la ''local data memory'' (LDM). La première contient du code qui gére les interruptions, la mémoire virtuelle, les exceptions matérielles, les problèmes d'alignement mémoire, et quelques fonctions très fréquemment utilisées par le CMS. La seconde est de la mémoire RAM utilisée par ces fonctions de la ''local program memory''. Les mémoires caches L1 et L2 sont séparées de ces deux mémoires. Le processeur Crusoe et Efficieon étaient des processeurs VLIW très simples. Ils n'avaient même pas de MMU, ni de fonctionnalités importantes sur les CPU x86. Pour Crusoe, les instructions VLIW étaient encodées sur 64 ou 128 bits, et regroupaient entre 2 à 4 opérations. Les instructions LOAD/STORE ne supportaient pas d'adressage indicé, juste de l'adressage indirect à registre. Crusoe avait 5 unités de calcul : deux ALU entières, une unité LOAD/STORE, une unité de branchement, une FPU. Efficieon doublait le nombre d'ALU entière et d'unité LOAD/STORE, les instructions VLIW passaient à 256 bits. Le processeur Crusoe contient 160 registres, dont 64 registres généraux, 32 registres flottants. Les registres généraux font 32 bits, ce qui est cohérent avec le fait que les CPU x86 de l'époque étaient des processeurs 32 bits. Quant aux registres flottants, ils faisaient 80 bits, ce qui colle avec la taille des registres flottants de la FPU x87 utilisée à l'époque. Sur les 64 registres généraux, seuls 48 étaient réellement utilisable pour mémoriser des opérandes. Une partie des registres généraux étaient utilisés pour la gestion de la pile, d'autres pour mémoriser l'état du CPU x86 émulé, un registre était un registre zéro non accesible en écriture. La gestion des exceptions est optimisée avec un système d'exceptions différées, le même que celui décrit plus haut. Le processeur utilise un système similaire, pour effectuer des lectures anticipées, qui a été expliqué dans le chapitre sur les processeurs VLIW/EPIC. Sauf qu'il s'agit là d'une optimisation pour gagner en performances en général, pas une aide utile pour gérer des exceptions précises ou toute autre subtilité de la traduction binaire. Pour gérer les exceptions différées et les lectures anticipées, le processeur mémorise l'état du processeur avant de démarrer un bloc de code. Pour cela, le processeur contient des ''shadow registers'', 48 registres entiers et 16 registres flottants. Vous remarquerez qu'il y a autant de registres entiers que de ''shadow registers'' entiers. Les registres entiers/flottants sont mémorisés dans les ''shadow registers'' avant d'exécuter un bloc de code, afin que le processeur puisse revenir à l'état de base. Une autre fonctionnalité liée est le ''gated store buffer''. Le principe est d'éviter toute écriture en mémoire RAM, tant que le code VLIW n'a pas émis d'instruction ''commit''. L'idée est que les écritures sont conservées dans la ''store queue'' de l'unité LOAD/STORE. Pour rappel, la ''store queue'' met en attente les écritures soit tant que les écritures ne sont pas terminées, soit tant que la RAM est occupée. L'idée est que les écritures sont accumulées dans la ''store queue'' tant que l'instruction ''commit'' n'a pas eu lieu. Le ''store queue'' est "déversé dans la RAM" seulement quand l'instruction ''commit'' s'exécute. ===Les CPU NVIDIA Denver=== Nvidia a tenté quelque chose de similaire avec son projet Denver. NVIDIA a racheté l'entreprise Transmetta, ses brevets, et a tenté de refaire la même chose. Le projet initial était de traduire du code x86 vers un jeu d'instruction VLIW propriétaire, comme l'a fait Transmetta. Mais NVIDIA n'a pas réussi à acquérir la licence du jeu d'instruction x86 et s'est rabattu sur le jeu d'instruction ARM. Le processeur né de ce projet est le Tegra K1-64 CPU. Il s'agit d'un processeur composé d'un cœur VLIW, couplé à deux décodeurs d'instructions ARM. Les deux décodeurs ARM traduisent une paire d'instructions ARM consécutives en instructions VLIW, pour les exécuter sur le cœur VLIW. Le CPU peut fonctionner selon deux modes : ARM et VLIW. En somme, le processeur supporte deux jeux d'instruction : le jeu d'instruction ARM, et un jeu d'instruction VLIW. En mode ARM, les instructions ARM sont chargées depuis le cache, traduites en VLIW par les décodeurs ARM, puis exécutées par le cœur VLIW. En mode optimisé, le CPU exécute des instructions VLIW chargées depuis le cache d'instruction, les décodeurs ne sont pas utilisés. : Le CPU supporte en réalité deux jeux d'instruction ARM : ARM8 et 7. Mais ils correspondent au même mode pour le CPU. Un point important est que le processeur peut changer très rapidement de mode, en à peine quelques cycles d'horloges. On n'est clairement pas dans le cas des CPU x86, qui mettent des plombes pour passer du mode 32 à 64 bits et inversement. La commutation est tellement rapide qu'on peut considérer que le processeur supporte deux jeux d'instruction simultanément. Et le support simultané de deux jeux d'instruction facilite l'implémentation de la traduction binaire. Encore une fois, inutile de traduire du code qui ne sera exécutée qu'une seule fois, mieux vaut l'exécuter directement dans le mode ARM. Surtout que le processeur supporte un mode ARM qui permet d'exécuter du code ARM sans perte de performance. Par contre, le code fréquemment utilisé, notamment dans des boucles critiques, est traduit en VLIW. Au final, les performances sont optimisées, en limitant le travail du logiciel traducteur binaire. La détection du code fréquemment exécuté se fait différemment que pour les processeurs Transmetta et Daisy. L'unité de branchement ne détecte pas directement le code beaucoup exécuté, même si elle a un rôle à jouer. A la place, la détection est réalisée en logiciel, par un ''thread'' dédié, qui s'exécute sur un cœur séparé. Il détecte le code fréquemment utilisé en regardent les branchements exécutés récemment. Pour cela, le processeur mémorise l'historique des branchements pris récemment et copie cette historique dans une mémoire tampon dédiée, partagée entre tous les cœurs. Vu qu'elle est partagée, le ''thread'' de détection a accès à l'historique sans pertes de performances, seule la copie de l'historique dans ce tampon a un cout en performance, pas son partage. Le code VLIW obtenu après traduction binaire est mémorisé dans la mémoire RAM, dans une portion spécialement réservée pour. Elle fait 128 mébioctets, et est appelée le '''cache d'optimisation''' par NVIDIA. Le cache d'optimisation est protégé en écriture et seul le traducteur binaire peut écrire dedans. Au passage, le traducteur binaire est du code VLIW, ce qui fait qu'il est placé dans le cache d'optimisation. Le processeur détecte automatiquement quand une fonction pour laquelle le code compilé équivalent est disponible. Pour cela, l'unité de chargement contient une table de correspondance entre l'adresse de la fonction ARM, et l'adresse de son équivalent compilé. Quand une fonction est appelée, l'unité de chargement regarde l'adresse de destination du branchement. Si l'adresse est dans cette table, elle récupère l'adresse de la fonction compilée et branche vers celle-ci. Le processeur dispose d'un cache d'instruction de 128 kibioctets, ce qui est très important, mais nécessaire vu la taille des instructions VLIW. Le coeur VLIW dispose de ses propres décodeurs, d'un ''scoreboard'', de 7 unités de calcul et de registres. Pour les unités de calcul, il a deux FPU, deux ALU, une unité de branchement, et deux unités LOAD-STORE faisant aussi ALU. Un point important est que le CPU dispose de 64 registres entiers et de 64 registres flottants. C'est deux fois plus que supporte le jeu d'instruction ARM. En mode ARM, seule la moitié des registres est utilisée. Mais en mode optimisé, le traducteur binaire utilise bien les 64 registres, grâce à une sorte de renommage de registres logicielle. Pour simplifier la traduction binaire, le processeur supporte les techniques vues précédemment. Le processeur intègre un ''gated store buffer'' similaire à celui des processeurs Transmetta. Les techniques d'exceptions différées sont aussi supportées, comme sur les processeurs Transmetta. Il a aussi une unité de préchargement décrite par NVIDIA comme agressive, avec support du préchargement de type anticipé (''runahead''). ===L'émulation de la mémoire virtuelle=== Pour finir, il faut parler de la mémoire virtuelle avec la traduction binaire. Sur le processeur source, le processeur gère à la fois des adresses physiques et virtuelles et sa MMU fait la conversion entre les deux. Sur le processeur VLIW, la MMU est simulée par le programme de traduction binaire, partiellement ou totalement. L'implémentation exacte varie suivant que l'on parle du projet Daisy, des processeurs Transmetta ou de Denver. Sur les processeurs Transmetta, il n'y a pas de MMU ni de mémoire virtuelle. A la place, le programme de traduction binaire émule la mémoire virtuelle du processeur source. Le processeur VLIW ne gère que des adresses physiques, rien d'autre. L'espace d'adressage physique du processeur VLIW a la même taille que celui du processeur émulé, ici des processeurs x86. Il est vraisemblable que les adresses physiques utilisées par le processeur x86 sont les mêmes que celles du processeur VLIW. Sur le projet Daisy, le processeur VLIW gère la mémoire virtuelle via pagination, comme les processeurs émulés. Ce qui fait qu'il gère un espace d'adressage virtuel et un espace d'adressage physique. Pour éviter toute confusion, nous parlerons d'adresse physique/virtuelle VLIW pour les adresses physiques/virtuelles du processeur VLIW, d'adresse virtuelle/physique source pour celle du jeu d'instruction traduit, à savoir du Power PC ou du s390, éventuellement de l'x86. Il faut alors faire le lien entre adresses physiques source et adresses virtuelles VLIW. Pour cela, rien de plus simple : il y a correspondance parfaite. L'adresse physique source numéro N correspond à l'adresse virtuelle VLIW numéro N. Cependant, cela signifie que tout l'espace d'adressage virtuel VLIW serait utilisé par le code à traduire. En réalité, il faut ajouter de la place pour le code traduit et le programme de traduction binaire. La conséquence est que l'espace d'adressage virtuel VLIW est plus large que l'espace d'adressage source. L'espace d'adressage virtuel VLIW est découpé en trois sections : une section pour le code Power PC à traduire, une autre pour le code traduit en VLIW, et une dernière réservée au traducteur binaire. Le programme de traduction est placé dans une ROM mappée en mémoire dans la seconde section. Le reste de la seconde section est réservé à une mémoire RAM utilisée par le programme de traduction. [[File:Mémoire virtuelle sur le projet Daisy.png|centre|vignette|upright=1.5|Mémoire virtuelle sur le projet Daisy]] Il faut noter que chaque page physique source, une fois traduite, correspond à N pages virtuelles VLIW. La raison est que le code traduit prend plus de place que le code machine originel. En conséquence, on doit utiliser une page finale plus grande. Pour le reste, des pages contiguës en mémoire physique source sont elles aussi contiguës en mémoire virtuelle VLIW. Le calcul d'adresse est donc simple : l'adresse physique source est multipliée par N, puis on ajoute l'adresse de base à laquelle commence la section pour le code traduit. ==La traduction binaire sur les Burroughs B1700== Nous venons de voir comment accélérer en matériel la traduction binaire, avec des techniques bien spécifiques. Le Burroughs B1700 a procédé autrement et il est intéressant d'étudier en détail son architecture. Son architecture est décrite dans le livre "Interpreting Machines : Architecture and Programming of the Bl700/Bl800 Series", écrit par Elliott I. Organick et James A. Hinds. Une partie de ce qui vfa suivre est un très court résumé de ce livre. : Dans ce qui suit, nous parlerons de traducteur binaire pour parler soit d'un interpréteur pour un langage de programmation de haut niveau, soit pour un logiciel de traduction binaire. ===Le Burroughs B1700 : le mal-nommé ''split-level control store''=== Le Burroughs B1700 avait plusieurs modèles : le B1726, le B1710 et le B1800. * Le B1710 plaquait le traducteur binaire en mémoire RAM. * Le B1800 avait ajouté un cache dédié au code du traducteur binaire, afin d'améliorer les performances. * Le B1726 utilisait un ''local store'' dédié au traducteur binaire. Le B1726 peut exécuter le traducteur binaire soit depuis le ''local store'', soit depuis la mémoire RAM avec une pénalité en termes de performances. La documentation appelle cela un '''''split-level control store''''', mais le terme est trompeur : la documentation qualifie de microcode le langage machine du Burroughs B1700, ce qui est à l'origine de beaucoup de confusions. Une autre source de confusion est que ce langage machine a quelques propriétés qu'on pourrait attendre d'un microcode, mais certaines sont manquantes. Par exemple, il peut adresser les registres d’interfaçage mémoire, mais c'est parce que ce sont des registres architecturaux sur ce processeur. Ses instructions n'encodent pas de signaux de commande, par contre, elles font 16 bits et adressent les opérandes de manière implicite. Le ''local store'' mémorisait au maximum 2048 instructions machine, ce qui fait que les 2048 premières "adresses" correspondaient au ''local store''. Les suivantes étaient des adresses de mémoire RAM qui contenaient le reste du traducteur binaire. En pratique, l'adresse qui séparait les deux était configurable ! On pouvait limiter la taille du ''local store'' à un multiple de 32 instructions ! Les adresses restantes étaient alors réattribuées à la RAM. Par exemple, on pouvait attribuer 64 adresses au ''local store'', les 2048 - 64 adresses restantes étaient alors attribuées à la mémoire RAM. Un registre limite, nommé TDPM, indiquait quelle était l'adresse de démarcation entre les deux. Le programmeur devait découper le traducteur binaire en segments, certains étant placés dans le ''local store'', les autres étant en mémoire RAM. Mieux que ça, le processeur autorisait d'utiliser la technique de l'''overlaying'' pour le microcode ! Il y avait même une fonction OVERLAY dédiée pour ! OVERLAY avait besoin de trois opérandes : la taille du segment à copier, son adresse source en mémoire RAM, son adresse de destination dans la micro-SRAM. L'interpréteur réservait un "segment" en mémoire RAM, pour les données, dont le processeur connaissait la position. En dehors de ce segment, il n'y a que des instructions machines à traduire, et le traducteur binaire. Le CPU incorporait pour cela deux registres, un pour l'adresse de base du segment de données, un autre pour son adresse de fin. Il incorporait une protection mémoire limitée sur ce segment, à savoir que seul l'interpréteur pouvait lire ou écrire dans ce segment. ===L'ALU de taille variable=== Les Burroughs B1700 avaient une '''ALU de taille variable''', ce qui veut dire qu'elle pouvait faire des calculs sur un nombre de bits compris entre 0 et 24. Pour cela, les calculs étaient fait avec une ALU de 24 bits, puis les bits de poids fort inutiles étaient masqués. L'ALU de 24 bits pouvait masquer les bits de poids fort du résultat, mais avec des limitations. Elle gérait des opérandes et résultats de 0, 4, 8, 12, 16, 20 et 24 bits. Pour avoir un réglage plus fin, une ALU 4 bits séparée permettait de corriger le résultat. Les registres X et Y étaient reliés à une ALU de 24 bits capable d'effectuer des opérations arithmétiques basiques. Fait étonnant, l'ALU faisait tous les calculs en même temps et fournissait ses résultats dans 7 registres : un registre SUM pour l'addition, un registre DIFFERENCE pour la soustraction, et un registre par opération bit à bit. Les registres pour les opérations bit à bit sont : CMPX et CMPY pour l'opération NOT sur chaque opérande, XANY pour le ET bit à bit, XEOY pour le XOR, XORY pour le OU logique. Les additions et soustractions pouvaient se faire sur des opérandes codés en binaire ou en BCD, suivant le code opération utilisé. A cela, il fallait ajouter deux registres de "résultat" MSKX et MSKY. Ils fournissaient un masque dépendant de la taille du résultat en bits. C'est ce registre qui permettait de gérer des données de 0, 4, 8, 12, 16, 20, et 24 bits, alors que l'ALU faisait 24 bits. Le masque dans ce registre pouvait servir d'opérande dans une opération de masquage ultérieure, pour corriger le résultat. L'ALU prenait en opérande les registres X et Y, mais aussi un registre CYF pour la retenue entrante. En sortie, il fournissait aussi deux retenue sortantes : une pour l'addition et une autre pour la soustraction. Elles étaient mémorisées dans les registres CYL et CYD, respectivement. L'ALU fournissait aussi 12 conditions, qui alimentait un registre d'état. Le registre d'état était segmenté en trois registres appelés XYST, XYCN et BICN. Les registres X et Y sont des registres pour les opérandes, mais il y a aussi deux autres registres de 24 bits. Il s'agit des registres T et L, qui sont des registres généraux. Les registres T et L étaient découpés en 6 sous-registres de 4 bits, nommés LA LB LC LD LE et LF pour le registre L, et TA TB TC TD TE et TF pour le registre T. Ils étaient adressables comme n'importe quel registre. Le processeur contenait de nombreux autres registres de 4 bits, dont les sous-registres de T et L. En tout, il y a 27 registres de 4 bits adressables ! Et le processeur disposait d'une ALU 4 bits pour manipuler ces registres de 4 bits. L'ALU 4 bits est capable de faire des MOV entre registres de 4 bits, des opérations bit à bit, et peut aussi tester si un bit vaut 0 ou 1. La dernière possibilité est très utile pour implémenter les branchements. De plus, cela permettait de faire des opérations de masquage, notamment pour appliquer un masquage supplémentaire à celui de l'ALU. Par exemple, pour gérer des opérations sur 14 bits, on effectue une opération sur 16 bits avec l'ALU, et on masque les 2 bits manquants avec l'ALU 4 bits. ===Une architecture bit-adressable=== Le CPU Burroughs B1700 était '''bit-adressable'''. En clair, il pouvait adresser la mémoire bit par bit, si nécessaire. Un avantage est que cela facilitait la traduction binaire des instructions machine, mais nous détaillerons cela plus bas. Un autre avantage est que cela permettait d'émuler des architectures dont la taille des registres/''byte''/mots était très différente. Et cet avantage mérite quelques explications immédiates. Le processeur était un CPU 24 bits, mais il pouvait émuler des processeurs 16 bits, 8 bits, 4 bits, 1 bit, 9 bits, ou toute autre valeur. Pour cela, le processeur lisait des mots de 24 bits et utilisant un ''barrel shifter'' pour sélectionner les bits adéquats. Par exemple, pour un accès de 16 bits, la donnée lue en mémoire était masquée de manière à ne garder que les 16 bits de poids faible. Le ''barrel shifter''/circuit de masquage était placé directement avant le bus mémoire, sur le trajet des données/instructions. Le ''barrel shifter'' était commandé par un registre ''Field Unit'', qui précisait quelle était la taille des données à charger. Pour communiquer avec la mémoire, il avait trois registres séparés : READ, WRITE, MAR. READ contient la donnée lue lors d'une lecture, WRITE est pour une donnée à écrire lors d’une écriture, MAR contient l'adresse à lire ou en cours de lecture. Il s'agit de registres d’interfaçage mémoire. Le fait que ces registres d’interfaçage mémoire et IO soit adressable est peu commun, et c'est plus quelque chose qu'on attend d'un microcode que d'instructions machines. Mais il faut bien comprendre que les registres d’interfaçage mémoire ont été promus au rang de registres architecturaux, sur ce processeur. De même, pour communiquer avec les entrées-sorties, il disposait de deux registres CMND et DATA : CMND pour envoyer une commande à une entrée-sortie, DATA pour lire ou écrire une donnée. Il y avait aussi un registre U, qui mémorisait une donnée lue depuis le lecteur de cassette. ===Le code machine du Burroughs B1700=== Le processeur utilise des instructions de 16 bits de long, qui sont copiées dans registre d'instruction M de 16 bits. Le ''program counter'' est appelé le registre A. Les instructions du processeur font 16 bits de long, et elles sont alignées en RAM sur 16 bits, ce qui fait que les 4 bits de poids fort de ce registre valent 0, seuls les 14 bits de poids fort sont utiles. Les adresses mentionnées plus haut, qui adressent les micro-opérations en mémoire RAM, sont des adresses de mots de 16 bits, pas les adresses de bit individuels. Fait intéressant, il était possible de faire un OU logique entre l'instruction lue et un registre du processeur, le résultat étant mémorisé dans le registre M. Cela permettait de faire du code automodifiant, pour émuler des modes d'adressage complexe. Cela permettait par exemple de remplacer une adresse dans une micro-opération. L'adresse est alors surimposée avec un OU dans le champ d'adresse, alors que le reste de l'instruction n'est pas modifié. Afin de gérer les interruptions, ces dernières étaient partiellement détectées en logiciel. Une interruption doit être prise en compte à la toute fin de l'exécution d'une instruction source, quand celle-ci a finit son travail. Mais vu qu'ici, les instructions sont émulées avec des séries d'instructions cibles, il y a un problème : il est possible d'interrompre une instruction émulée en plein milieu. Pour éviter cela, le processeur ne déclenche pas les interruptions matérielles immédiatement, elles sont mises en attente. Une instruction dédiée vérifie si une interruption est en attente, et l'exécute alors cas échéant. L'instruction en question est utilisée en dernier, dans la suite d'instruction qui émule l'instruction source. Les Burroughs B1700 intégrait une pile d'adresse de retour, capable de mémoriser 16 adresses de 24 bits. Il disposait de 4 registres généraux, nommés X, Y, T et L, tous de 24 bits. Un point important est que le registre T avait des possibilités d’extraction spécifiques. Il était possible d'extraire N bits, placés n'importe où dans ce registre, et de les copier dans un autre registre. Le CPU supportait une '''instruction EXTRACT''' pour ça. Nous verrons pourquoi cette possibilité est intéressante et ne sert que pour le registre T. ===La traduction binaire sur le Burroughs B1700=== Les instructions source peuvent faire 16 bit avec tel jeu d'instruction, 8 bits avec un autre, 32 bits sur un autre, etc. Elles peuvent aussi être de longueur fixe ou variable ! Le B1700 utilisait une architecture bit-adressable pour gérer ces contraintes. En effet, avec un CPU bit-adressable, les contraintes d'alignement des instructions disparaissent ! Dans ce qui suit, on suppose que l'instruction est composée de '''champs''' : l'opcode, les opérandes, les adresses absolues, les constantes immédiates, les numéros de registre, etc. L'idée est que l'instruction source est chargée dans le registre T, puis découpée en champs, qui sont copiés dans les registres du processeur ou envoyés au microcode. Je rappelle que le registre T est relié à un ''barrel shifter'', différent au précédent, qui permettait de masquer, décaler et extraire une donnée de ce registre. Et il y a la possibilité d'extraire une suite de bit de ce registre T, pour la copier dans un autre registre. Par exemple, prenons une instruction composée d'un opcode, d'une constante immédiate et d'une adresse immédiate. Premièrement, elle est chargée dans le registre T. Deuxièmement, la constante immédiate est extraite du registre T et copiée dans le registre X. Troisièmement, l'adresse est extraite et est copiée dans le registre d’interfaçage mémoire, une lecture est démarrée, et l'opérande lue est copiée dans le registre Y. Enfin, l'instruction équivalente à l'opcode est exécutée par le microcode. Le tout prend plusieurs instructions : une première pour lire l'opcode, une seconde pour lire la première opérande, une troisième pour lire la seconde opérande, una quatrième pour tout exécuter. Notons que suivant la taille des champs, on peut en charger plusieurs à la fois. Par exemple, imaginons qu'on ait chargé 24 bits, contenant un opcode de 8 bits et une constante immédiate de 16 bits : le registre T est découpé en un opcode et une constante, avec deux instructions EXTRACT. L'architecture est bit-adressable car elle gère des champs qui ne font pas forcément 8 ou 16 bits. Par exemple, certains jeux d'instructions ont des opcodes codés sur 6 ou 7 bits, voire 9-10 bits. Utiliser une architecture bit-adressable règle ce problème d'alignement. On peut lire un champ depuis la mémoire RAM en précisant l'adresse de son premier bit, pas besoin de gérer des accès non-alignés. Par exemple, prenons une instruction source avec un opcode de 7 bits, suivi par une adresse de 12 bits et une constante immédiate de 15 bits. On peut charger 7 + 12 bits en un premier accès, pour extraite l'opcode et l'adresse, suivi par un second accès de 15 bits pour extraire la constante immédiate. Pour charger un champ, il faut connaitre plusieurs informations : l'adresse de son premier bit, sa taille. Pour cela, le B1700 contient deux registres, nommés ''Field Adress'' (FA) et ''Field Length'' (FL), qui indiquent l'adresse du premier bit et la taille de l'instruction à charger. Le premier fait 24 bits, ce qui permet de gérer des adresses de 24 bits. N'oubliez pas que le processeur est bit-adressable, ce qui permet d'adresser 16 Mébi-bits, soit 2 mébioctets. Le registre pour la taille fait lui 16 bits, ce qui permet de gérer des instructions de 65536 bits, soit 8192 octets ! Leur nom commence par ''field'', car le processeur peut gérer des instructions de taille variable et/ou plus longues que 24 bits. Dans ce cas, le chargement de l'instruction se fait champ par champ. Pour donner un exemple, prenons une instruction composée de 20 champs de 12 bits chacun. Les champs sont chargés un par un dans le registre T. Le registre FA est alors initialisé avec l'adresse de l'instruction, il est incrémenté de 12 à chaque cycle. Le registre FL est initialisé avec la taille de l'instruction, soit 20 * 12 = 240 bits, et il est décrémenté de 16 à chaque cycle. Précisons cependant que la taille d'un champ peut être changée d'un cycle à l'autre. Par exemple, le processeur peut charger un opcode de 8 bits, l'interpréter, détecter que l'instruction demande ensuite de charger une adresse de 16 bits. Dans ce cas, le registre FL est immédiatement altéré pour charger 16 bits, au lieu des 8 bits de l'opcode immédiatement précédent. ==Les jeux d'instructions dédiés à un langage de programmation== De rares processeurs sont conçus pour un langage de programmation en particulier. On appelle ces processeurs, conçus pour des besoins particuliers, des '''processeurs dédiés'''. Par exemple, les fameux ''Burrough E-mode'' B5000/B6000/B7000 étaient spécialement conçus pour exécuter de l'ALGOL-60. Leurs cousins B2000/B3000/B4000 étaient eux conçus pour le COBOL. Des langages fonctionnels ont aussi eu droit à leurs processeurs dédiés. Le prolog en est un bel exemple, avec les superordinateurs de 5ème génération qui lui étaient dédié. On peut aussi citer les machines LISP, dédiés au langage LISP, qui datent des années 1970. Elles étaient capables d’exécuter certaines fonctions de base du langage directement dans leurs circuits : elles possédaient notamment un ''garbage collector'' câblé dans ses circuits ainsi que des instructions machines supportant un typage déterminé à l’exécution. Les processeurs dédiés ont eu leur heure de gloire au début de l'informatique, à une époque où les langages de haut niveau venaient d'être inventés. À cette époque, les compilateurs n'étaient pas performants et ne savaient pas bien optimiser le code machine. Il était alors rationnel, pour l'époque, de rapprocher le code machine cible et le langage de programmation de haut niveau. De nombreuses architectures dédiés ont ainsi été inventées, avant que les concepteurs se rendent compte des défauts de cette approche. Les défauts en question ne sont pas nombreux, mais assez simples à comprendre. Premièrement, elles sont très rapides pour un langage de programmation en particulier, mais sont assez mauvaises pour les autres, d'où un problème de "compatibilité". Ajoutons à cela que les langages de programmation peuvent évoluer, devenir de moins en moins populaires/utilisés, ce qui rend la création d'architectures généralistes plus pertinente. Enfin, les architectures dédiées sont évidemment des processeurs CISC, pour implémenter les nombreuses fonctionnalités des langages évolués. Et les défauts des CISC sont assez rédhibitoires à l'heure actuelle. À l'heure actuelle, les algorithmes des compilateurs se sont améliorés et savent nettement mieux utiliser le matériel. Ils produisent du code machine efficace, ce qui rend les architecture dédiées bien moins intéressantes. Si on ajoute les défauts de ces architectures dédiées, par étonnant que les architectures dédiées aient presque disparues. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'accélération matérielle de la virtualisation | prevText=L'accélération matérielle de la virtualisation | next=Le matériel réseau | nextText=Le matériel réseau }} </noinclude> sibhev12cvtex2td8ywjisquxxz1xd0 764064 764063 2026-04-19T21:28:14Z Mewtow 31375 /* La traduction binaire sur les Burroughs B1700 */ 764064 wikitext text/x-wiki De nos jours, la majorité des programmeurs programment dans des langages de haut niveau. Il est très rare pour eux d'avoir à utiliser de l'assembleur, et encore moins un langage machine. Les programmes écrits dans un langage de haut niveau sont traduits en langage machine par un logiciel appelé le compilateur. [[File:Compilation.PNG|centre|vignette|upright=2.5|Compilateur : principe.]] Il se trouve que quelques rares processeurs sont conçus pour faciliter le travail du compilateur. Et ce sont ces processeurs que nous allons voir dans ce qui suit. Ils représentent une catégorie de jeux d'instruction à part, qui n'a pas vraiment de nom. ==La compilation et l’interprétation== Un compilateur traduit donc du code source, écrit dans un langage de haut niveau, vers du langage machine. La traduction est rarement directe. En général, le compilateur traduit le code source en assembleur, qui est lui-même traduit en langage machine. L'assembleur est une représentation textuelle du langage machine alors que le code machine est du binaire exécutable par le processeur. La traduction se fait donc en deux étapes, la '''compilation''' proprement dite et l''''assemblage''', réalisées respectivement par un compilateur et un assembleur. Il faut parfois rajouter une troisième phase d'édition des liens, que nous passons volontairement sous silence. {| |[[File:Code C.png|vignette|200px|Le code source, ici en C.]] |[[File:Assem.png|vignette|250px|Le code assembleur.]] |[[File:Binary file - hello world (C programming).png|vignette|150px|Le code machine.]] |} ===L'interprétation et la compilation à la volée=== Une alternative à la compilation est l''''interprétation''', qui transforme le code source en code machine à la volée. Avec l'interprétation, le code source est passé à un logiciel appelé l''''interpréteur''', qui exécute le code source ligne de code par ligne de code. Une ligne de code est traduite en code machine, qui est exécutée, puis l'interpréteur passe à la ligne suivante. [[File:Kaantotulkkaus.png|centre|vignette|upright=3|Compilation versus interprétation.]] Un défaut de l'interprétation est son cout en performance, qui est assez important. Aussi, l'interprétation stricte a évolué vers un hybride entre interprétation et compilation. L'idée est qu'une partie peu importante du code source est interprétée, alors que le code important est compilé juste avant d'être exécuté. Pour le dire autrement, le code source est partiellement compilé à la volée, juste avant son exécution. Aussi, on parle de '''compilation à la volée'''. Le terme en anglais est ''Just In Time compilation'', abrévié en JIT. En soi, le JIT est associé à de l'interprétation, les JIT sont en réalité des hybrides interpréteurs-compilateurs. Un point important est que compiler du code à la volée est assez lourd, cela a un cout en performance. Par contre, le code compilé s'exécute plus vite que du code interprété. Si on exécute le code compilé une seule fois, le cout de la compilation l'emporte sur le gain à l'exécution. Mais le code compilé est réutilisé autant de fois que nécessaire. Le gain à l'exécution est donc multiplié par le nombre d'exécutions, alors que le cout de la compilation est répartit sur plusieurs exécutions, il est amorti. Les portions du code sont choisies de manière à avoir un gain en performance maximal. L'idée générale est que le code exécuté très souvent est compilé, alors que du code exécuté pas souvent est interprété. Typiquement, les boucles les plus souvent exécutées sont compilées, le code exécuté une seule fois est interprété. ===Les langages intermédiaires=== Les compilateurs modernes passent par un '''langage intermédiaire''' pour faire la transformation en code machine. Le compilateur traduit le langage de haut niveau en langage intermédiaire, puis traduit le langage intermédiaire en code machine cible. Le langage intermédiaire est parfois appelé le '''''bytecode''''', et ce terme recouvre aussi le code écrit avec du ''bytecode''. L'interprétation aussi peut passer par l'intermédiaire d'un un ''bytecode'', ce n'est pas limité aux compilateurs proprement dit. {| |[[File:Intermediate representation scheme.png|centre|vignette|upright=1.5|Représentation intermédiaire d'un compilateur.]] |[[File:Code interpreter scheme.png|centre|vignette|upright=1.5|Fonctionnement d'un interpréteur.]] |} Faire ainsi a de nombreux avantages, le principal étant d'avoir un compilateur capable de traduire un langage de haut niveau vers plusieurs jeux d'instructions différents. Cela permet d'avoir, par exemple, un compilateur qui traduit du C soit en code machine x86, soit en code machine pour un CPU ARM, soit pour un CPU POWERPC, etc. [[File:Compiler design.svg|centre|vignette|upright=3|Compiler design]] Pour l'interprétation, l'usage d'un ''bytecode'' a d'autres avantages. Pour l'exploiter, il faut distribuer non pas le code source, mais le ''bytecode''. En clair, l'interpréteur ne prend pas en entrée le code source, mais du ''bytecode'' déjà compilé. La traduction est alors beaucoup plus simple, car le code source a déjà été partiellement compilé, par la traduction en ''bytecode''. Interpréter du code source est en effet assez compliqué : il faut effectuer des étapes d'analyse lexicale, sémantique, et bien d'autres. Avec du ''bytecode'', ces étapes ont été réalisées lors de la compilation du ''bytecode'', l'interpréteur a alors peut de choses à faire. L'interprétation du ''bytecode'' se fait instruction par instruction, au niveau du ''bytecode''. Une instruction du ''bytecode'' est traduite en une instruction machine équivalente, qui est exécutée. Si l'instruction est un peu complexe, il exécute une fonction/procédure qui fait la même chose. Pour résumer, le ''bytecode'' est ensuite traduit à la volée et exécuté instruction par instruction par un logiciel appelé l'interpréteur. [[File:Control table.png|centre|vignette|upright=2|Control table]] Le langage intermédiaire peut être vu comme l'assembleur d'un processeur, n'existe pas forcément dans la réalité, mais dont le jeu d'instruction est décrit en détail. Le processeur en question est appelé une '''machine abstraite''', ou encore une ''machine virtuelle''. Nous utiliserons le terme de machine abstraite dans ce qui suit. La machine abstraite n'est pas la même suivant que l'on cible la compilation ou l'interprétation/JIT. Pour l'interprétation/JIT, la machine abstraite est souvent une machine à pile, car cela simplifie grandement la traduction en code machine final. L'interprétation demande que la traduction du ''bytecode'' en code machine soit la plus simple possible. Et cela demande de prendre en compte pas mal de détails. Par exemple, les différents jeux d'instruction existants n'ont pas le même nombre de registres, ce qui pose problème lors de la traduction du ''bytecode''. Lors de la transformation en code machine, un algorithme d'allocation de registres se débrouille pour traduire le code intermédiaire en code qui utilise un nombre limité de registres. Pour améliorer cette allocation de registres, il y a deux solutions niveau ''bytecode'' : soit utiliser un nombre illimité de registres, soit utiliser une machine à pile. Pour les machines à pile, il existe un algorithme simple et rapide pour traduire un code écrit pour une machine à pile en un code écrit pour un processeur avec des registres, qui se débrouille pas trop mal pour allouer efficacement les registres. C'est un avantage assez important pour les langages interprétés. ==Les processeurs qui exécutent du bytecode== Le ''bytecode'' est un code machine, ce qui signifie qu'il peut en théorie s'exécuter sur un processeur qui implémente le jeu d’instruction associé. Si le jeu d'instruction d'un ''bytecode'' est souvent une description censée être fictive, elle n'en reste pas moins un jeu d'instruction et des caractéristiques précises, qu'on peut l'implémenter en matériel ! Néanmoins, tous les ''bytecode'' ne sont pas égaux de ce point de vue. Certains sont faciles à implémenter en matériel, d'autres non. Les ''bytecodes'' implémentés en matériel sont ceux dont les machines abstraites sont des machines à pile. Un premier exemple est celui des processeurs Pascal MicroEngine, qui exécutaient directement le bytecode du langage Pascal, le fameux UCSD P-code. Un second exemple est la '''machine SECD''', qui sert de langage intermédiaire pour certains compilateurs de langages fonctionnels. Elle a été implémentée en matériel par plusieurs équipes, notamment par les chercheurs de l'université de Calgary en 1989. Dans le même genre, quelques processeurs simples étaient capables d’exécuter directement le ''bytecode'' du langage FORTH. Le FORTH, un des premiers langages à pile de haut niveau, possède de nombreuses implémentations hardware et est un des rares langages de haut niveau dont le ''bytecode'' a été utilisé comme langage machine sur certains processeurs. Par exemple, on peut citer le processeur FC16, capable d’exécuter nativement du ''bytecode'' FORTH. ===Les processeurs Java=== Le cas le plus impressionnant est celui de la machine virtuelle Java, qui est un design de processeur comme un autre. En temps normal, le ''bytecode'' Java est compilé ou interprété, mais certains processeurs exécutaient du ''bytecode'' Java directement, sans interprétation ni compilation. Ils sont appelés des '''processeurs Java'''. On peut citer les processeurs ARM disposant de l'extension Jazelle, du JEMCore et du aJ-100 de aJile Systems, le picoJava II de Sun, et quelques autres. Il y a aussi eu quelques projets de recherche ou processeurs open source, comme Komodo, jamuth, le ''Java Optimized Processor'' et quelques autres projets du même genre. L'implémentation de la machine virtuelle Java n'était cependant pas complète. Quelques instructions complexes n'étaient pas gérées par le processeur et devaient être émulées en logiciel. Mais la grosse majorité des instructions l'était. Les instructions non-supportées étaient émulées avec la méthode ''trap and emulate'', à savoir qu'un opcode inconnu déclenchait une exception matérielle ''opcode invalide'', dont la routine pouvait émuler les instructions qui doivent l'être. Le processeur aJ-100 de aJile est capable d'exécuter la quasi-totalité des instructions de la JVM Java, seules deux instructions faisant exception. Les '''accélérateurs Java''' sont une solution intermédiaire, qui permet de traduire à la volée du ''bytecode'' Java en instructions RISC, exécutées sur un autre processeur. Il s'agit formellement de traduction binaire, mais je préfère en parler ici. L'idée est de combiner un processeur RISC avec un coprocesseur Java. Le coprocesseur lit le ''bytecode'' Java et le traduit en instructions RISC, ces dernières étant exécutées sur le processeur RISC. Le JA108 de Nozomi était un coprocesseur de ce genre, au même titre que le coprocesseur JSTAR de JEDI. L'extension Jazelle de certains CPU ARM fonctionnait vraisemblablement sur un principe similaire, sauf que le coprocesseur était intégré dans le processeur ARM, entre le cache d'instruction et le décodeur d'instructions ARM. L'intérêt des processeurs Java n'est pas qu'une question de performance. Certes, ils permettent d’exécuter plus vite les programmes compilés en ''bytecode'', comme des programmes Java pour la JVM Java. Mais l'intérêt est aussi de faire des économies de mémoire RAM et ROM. Je rappelle que la machine virtuelle Java est une machine à pile, ce qui fait que sa densité de code est excellente. Les programmes en ''bytecode'' Java sont donc très petits, et prennent peu de place en mémoire ROM. De plus, utiliser un processeur Java permet de se passer d'interpréteur ou de JIT Java, ce qui économise encore plus de mémoire. ===Transformer un processeur RISC en machine à pile=== La plupart des processeurs Java utilisent en interne une architecture à registre généraux, qui émule une machine à registre en microcode. Quelques registres sont réservés pour l'état de la machine virtuelle, avec au minimum un registre réservé pour le pointeur de pile, un autre pour un pointeur vers un ''pool'' de constantes, et quelques autres. D'autres registres servent à mémoriser le haut de la pile, à savoir le sommet de la pile et quelques opérandes situées juste en-dessous. Par exemple, le processeur picoJava II gardait les 64 opérandes au sommet de la pile dans 64 registres généraux, séparés des registres pour les pointeurs et l'état de la JVM. Une optimisation possible, envisagée sur les processeurs picoJava, était une sorte de macro-fusion sous stéroïde. L'idée était de fusionner certaines séries d'instructions Java en une seule instruction machine. Typiquement, pour faire une opération arithmétique, une machine à pile empile deux opérandes et exécute une opération, ce qui prend trois instructions machine. La spécification picoJava 1 a proposé d'envoyer directement la seconde opérande en entrée de l'ALU, ce qui fusionnait l'empilement de la seconde opérande et l'opération arithmétique. Le processeur picoJava II va plus loin et fusionne les trois instructions en une seule instruction RISC, agissant sur des registres. Mais la majorité des processeurs Java ne faisaient pas cela, car cela complexifie grandement le décodeur. Les processeurs ''bigfoot'' de DCT utilisaient un système différent pour émuler une pile avec des registres. Leur banc de registre de 64 bits était coupé en deux : les 16 premiers registres étaient utilisés pour la pile, les 48 suivants étaient utilisés pour autre chose. L'idée est que le registre 0 mémorise un emplacement vide, le registre 1 contient le sommet de la pile, le registre 2 l'opérande sous le sommet de la pile, et ainsi de suite. Les 15 premières opérandes de la pile sont donc mémorisées dans les registres. Les instructions écrivent leur résultat dans le registre adéquat, puis tous les registres sont décalés pour que le résultat passe dans le registre R1. Par exemple, les instructions PUSH chargent une opérande dans le registre 0, puis tous les registres sont décalés de un rang, pour que l'opérande chargée soit dans le registre 1. Le registre 0 devient alors le registre numéro 1, le registre 1 devient le 2, et ainsi de suite. Par contre, les opérations arithmétiques font autrement. Une instruction d'addition, par exemple, lit les opérandes dans le registre 1 et 2, puis place son résultat dans le registre 2 (les deux opérandes sont dépilées). Puis, les registres sont décalés d'un rang pour que ce registre 2 devienne le 1. Les registres ne sont en réalité pas décalés, il n'y a pas de transferts entre registres. A la place, les 16 premiers registres utilisaient une forme limitée de renommage de registres, similaire au banc de registre tournant utilisé sur les processeurs EPIC comme l'Itanium. Les numéros de registres sont renommés, décalés, à chaque fois qu'on empile ou dépile une opérande. Pour faire le renommage de registres, processeur contient un ''register stack counter'' de 4 bits, qui est incrémenté à chaque fois qu'on empile une donnée, et est décrémenté quand on dépile une donnée. Quand on veut accéder à un opérande, le numéro de registre architectural associé est traduit en numéro de registre physique, en additionnant le ''register stack counter'' (modulo 16, pour rester sur 4 bits). ==L'accélération de l'interprétation/JIT : le ''Thumb-EE'' d'ARM== Quelques processeurs ont ajouté des instructions pour faciliter le travail des interpréteurs/JIT. Par exemple, en 2005, ARM a ajouté le mode ''Thumb Execution Environment'', qui faisait cela. Il reprenait le jeu d'instruction compact ''thumb'' et ajoutait quelques instructions et modifiait le comportement d'instructions existantes. Le jeu d'instruction ''Thumb-EE'' a été ajouté sur les CPU ARM en 2005, mais a été déprécié en 2011, par manque d'utilité. Mais la tentative mérite qu'on s'attarde dessus. Les CPU de gamme M, déstinés à l'embarqué, n'ont jamais supporté ''Thumb-EE'', vu que de tels processeurs ne sont pas conçus pour exécuter du code interprété/JIT. Le mode ''Thumb-EE'' est un mode d'exécution, séparé de l'ARM normal. Ainsi, une instruction ''Thumb-EE'' et une instruction ARM peuvent avoir le même encodage en binaire, mais se comporter différemment. Le processeur est à tout moment soit en mode ''Thumb-EE'', soit en mode ARM normal, ce qui précise comment ces instructions doivent se comporter. Des instructions ont été ajoutées par ''Thumb-EE'', pour qu'il fasse son travail. L'entrée et la sortie du mode ''Thumb-EE'' se fait avec deux instructions : ENTERX et LEAVEX. Une fois en mode ''Thumb-EE'', le décodage des instructions ''thumb'' se fait avec les règles du ''Thumb-EE'', et non celles du ''thumb'' normal. Une première différence entre ''thumb'' normal et ''Thumb-EE'' est l'ajout de ce qui s'appelle un ''null check'' pour les instructions mémoire. Le ''null check'' est utilisé pour les instructions en mode d'adressage "base + indice" ou "Base + décalage", ou tout autre mode d'adressage avec un registre de base utilisés dans des calculs d'adresse. L'idée est de vérifier si l'adresse de base vaut zéro ou non. Si c'est le cas, le processeur lève une exception matérielle, qui est traitée par l'interprétation ou le compilateur JIT. Une telle situation est en effet signe d'une erreur d'adressage mémoire, qui doit être traitée par l'interpréteur. Typiquement, c'est signe qu'un pointeur n'a pas bien été initialisé, ce qui peut arriver avec du code interprété ou JIT. Les interpréteurs/JIT traitent généralement la situation en ajoutant un test avant toute instruction mémoire, pour vérifier si le pointeur accédé vaut zéro ou non. Avec ''Thumb-EE'', pas besoin d'ajouter les tests en questions : ils sont réalisés automatiquement lors de chaque instruction mémoire. : Les registres pour la pile, comme le pointeur de pile et le pointeur de ''frame'', sont aussi concernés. L'instruction CHKA vérifie que les accès à un tableau ne débordent pas en dehors du tableau en question. Par exemple, pour un tableau de 1024 éléments, elle vérifie si l'indice est compris entre 0 et 1024. Pour cela, elle vérifie si l'indice de tableau est bien dans l'intervalle adéquat et lève une exception matérielle si ce n'est pas le cas. Son utilité se comprend quand on sait que tous les accès à un tableau sont vérifiés par l'interpréteur. L'interpréteur est censé ajouter des instructions pour vérifier les indices, à savoir deux ou trois branchements. Avec CHKA, le test se fait en une seule instruction. D'autres modifications mineures de l'encodage des instructions sont aussi présentes, ainsi que des modifications pour les instructions LOAD/STORE. Précisément, dans les modes "base + indice" et "base + décalage", le décalage et l'indice subissent maintenant des décalages. L'idée est de simplifier le calcul d'adresse. De quoi économiser quelques instructions lors des calculs d'adresse, ce qui facilite le travail de l'interpréteur/JIT. Et des instructions LOAD/STORE précises voient leur comportement modifié pour faciliter la gestion de la pile de Java ou l'exécution des méthodes locales. Pour faciliter le travail de l'interpréteur, deux instructions HBP et HBLP ont été ajoutées. Elles branchent vers une fonction qui gère les exceptions logicielles (une fonctionnalité présente dans de nombreux langages, comme Java). La première effectue un branchement vers la fonction en question, la seconde sauvegarde l'adresse de retour avant de faire ce branchement. Les deux peuvent brancher vers 256 fonctions pré-déterminées, sans avoir à présenter leur adresse. ==La traduction binaire accélérée par le matériel== Plus haut, nous avons surtout parlé des compilateurs et des interpréteurs. Cependant, nous devons aussi parler de la '''traduction binaire'''. Elle traduit un programme écrit dans un code machine vers un autre code machine. Par exemple, elle traduit un programme compilé pour un CPU x86 vers un code machine ARM. En général, le système d'exploitation est généralement compilé pour le jeu d'instruction natif, mais il exécute des applications prévues pour le x86. Les applications sont traduites par le système d'exploitation, avant d'exécuter le code traduit. : Nous parlerons dans la suite de code machine source et de code machine cible, pour parler respectivement du code à traduire et du code obtenu après traduction. Idem avec d'autres termes comme architecture cible/source, ou jeu d'instruction cible/source. La traduction binaire est surtout utilisée pour des questions d'émulation ou de compatibilité. Par exemple, lorsque les Macintosh sont passés de processeurs Power PC vers des processeurs x86, le système d'exploitation Mac OS utilisait la traduction binaire pour convertir les anciennes applications Power PC vers du code x86. Les utilisateurs n'y ont vu que du feu. Le système de traduction binaire était appelé Rosetta 1. Par la suite, lors de la transition de processeurs x86 vers des processeurs Apple, Rosetta 2 a vu le jour. La traduction binaire a surtout été utilisée pour traduire du code x86 vers un autre jeu d'instruction. Il faut dire que le x86 est le jeu d'instruction dominant. De nombreuses entreprises ont eu pour ambition de briser l'hégémonie du x86 sur PC, en remplaçant le x86 par un jeu d'instruction plus performant, tout en gardant une compatibilité maximale. La traduction binaire était la seule solution pratique. Elle était le plus souvent intégralement réalisée en logiciel, comme c'était le cas sur les architectures Itanium avec le ''IA-32 Execution Layer''. ===Les généralités sur la traduction binaire assistée en matériel=== L'architecture source est presque tout le temps une architecture CISC, assez ancienne, qu'on souhaite émuler. Le choix de l'architecture cible se porte souvent sur une architecture VLIW, fort différente de l'architecture source, et ce pour obtenir une économie de matériel conséquente. Pour s'exécuter rapidement, le code traduit doit exploiter le parallélisme d'instruction, à savoir exécuter plusieurs instructions en même temps dans des unités de calcul séparées. Il est possible d'utiliser un processeur superscalaire avec exécution dans le désordre pour cela, mais au prix d'un cout important en transistors. Alors qu'en utilisant un CPU VLIW, c'est le traducteur binaire qui fait tout le travail d'extraction du parallélisme d'instruction. Et après tout, quitte à avoir un traducteur binaire, autant lui refiler le boulot d'optimisation. Compiler du code à la volée est certes assez lourd, mais qu'il existe des algorithmes efficaces pour regrouper des instructions indépendantes dans une seule instruction VLIW. Au passage, le processeur VLIW a plus de registres que le processeur source. Cela permet de faire du renommage de registres directement en logiciel. Le traducteur binaire n'hésite pas à changer les noms de registres entre instructions source et instruction VLIW cible, afin de supprimer des dépendances de données. En conséquence, cela ouvre des opportunités de parallélisme, qui sont exploitées lors du regroupement des opérations en instructions VLIW. Vous pourriez penser que le choix d'un CPU VLIW pour émuler du CISC est tout sauf optimal. Vous devez penser que la traduction binaire est d'autant plus simple que l'architecture source et cibles sont semblables. Dans les faits, ce n'est pas tellement traduire les instructions qui pose problème, mais plus la gestion du registre d'état, des exceptions matérielles, de la mémoire virtuelle, la différence entre gros-boutisme et petit-boutisme, de même que des différences d'adressage pour les périphériques. Le premier problème est la gestion des conditions, notamment en présence d'un registre d'état. En pratique, la traduction binaire s'utilise pour traduire du code CISC vers du code RISC ou VLIW. Les architectures source ont donc un registre d'état, qui est mis à jour non seulement par des instructions de test, mais aussi des instructions arithmétiques. L'architecture cible n'a elle pas de registre d'état, mais des registres à prédicats. La traduction de l'un vers l'autre est alors quelque peu compliquée. Et elle est d'autant plus compliquée que le registre d'état est la source de dépendances d'instruction implicites, qui réduisent les performances. Par exemple, si une instruction arithmétique modifie le registre d'état, cela peut impacter l'exécution d'une instruction ultérieure, qui lit ce registre d'état. Pour éliminer ces fausses dépendances, le traducteur binaire doit renommer ce registre en logiciel et détecter les dépendances utiles. Et le compilateur doit gérer les cas où il y a beaucoup de distance entre les deux, voire les cas où l'instruction dépendante n'a pas encore été analysée par le compilateur. Un autre problème est lié à la gestion des exceptions matérielles, et précisément des exceptions précises. Pour rappel, une instruction VLIW regroupe plusieurs opérations, opérations qui correspondraient à une instruction machine sur un CPU pas VLIW. En conséquence, plusieurs instructions du langage machine "source" sont regroupées en une seule instruction VLIW. Et il faut tenir compte du cas où une instruction source lève une exception. Dans le code VLIW, cela signifie qu'une opération lève une exception, et il faut annuler partiellement l'instruction VLIW associée. Et par partiellement, on veut dire que seules les opérations suivantes dans l'ordre du programme source doivent être annulées, puis ré-exécutées. Et c'est un sacré casse-tête ! Les processeurs VLIW utilisent comme solution le mécanisme d'exceptions différées des processeurs EPIC, vu il y a quelques chapitres. Pour résumer, le code est exécuté par blocs d'instruction, délimités par des branchements ou tout autre limite/barrière pertinente dans le code. Un bloc de code est compilé en deux versions : une version rapide sans exceptions matérielles, une version lente qui gère les exceptions précises. La version rapide s'exécute sans exécuter les exceptions. Cependant, les exceptions matérielles sont enregistrées, pour être prises en compte à la toute fin du bloc de code. La version rapide est exécutée en premier et elle mémorise si une exception matérielle a eu lieu. Puis une instruction vérifie si une exception a eu lieu et décide quoi faire. Si aucune exception n'a eu lieu, elle passe à la suite du programme, les résultats du bloc de code sont définitivement acceptés. Mais si une exception a eu lieu, tout est annulé. Le processeur est remis dans l'état initial, puis le code est ré-exécuté instruction par instruction de manière à gérer l'exception correctement. ===La traduction par pré-décodage=== Une première solution serait de faire la traduction binaire dans le cache d'instruction. Lors d'un défaut de cache, le code chargé depuis la RAM est traduit en code machine cible. Et c'est ce code machine cible qui est mémorisé dans le cache d'instruction et exécuté par le processeur. La technique marche sur le papier et n'est qu'une amélioration des techniques de pré-décodage vues il y a quelques chapitres. Cependant, le code obtenu est une traduction assez basique, qui n'incorpore pas d'optimisations dignes de ce nom. De plus, elle gère mal le cas où la taille du code source et cible sont potentiellement très différentes. Concrètement, elle est surtout utile pour traduire du code d'un processeur RISC vers un autre processeur RISC, dont les tailles d'instructions sont similaires. Elle ne permet pas d'émuler plusieurs jeux d'instructions différents, le cout en matériel (un circuit de pré-décodage par jeu d'instruction) serait trop important. ===Le projet DAISY d'IBM=== De très rares processeurs étaient conçus pour accélérer cette traduction binaire, afin de garder de bonnes performances. Les premiers à avoir étudié l'idée étaient IBM, avec leur projet DAISY (''Dynamically Architected Instruction Set from Yorktown''). Le projet de base était de convertir à la volée du code compilé pour des CPU Power PC, vers du code VLIW. Les chercheurs d'IBM avaient développé un algorithme de traduction dynamique efficace, ainsi qu'un processeur VLIW disposant d'optimisations spécifiques à la traduction binaire dynamique, à la volée. De nombreuses idées de ce projet ont été reprises ou re-découvertes par la société Transmetta, avec ses processeurs Crusoe et Efficieon, puis par NVIDIA avec son projet Denver. Les ingénieurs de ce projet ont étudié la possibilité d'émuler plusieurs jeux d'instructions différents sur un même processeur, notamment le s390 d'IBM et le x86. En théorie, cela demande juste d'avoir plusieurs programmes de traduction binaire : un pour le x86, un autre pour le s390, éventuellement un autre pour le Power PC. Mais cela ne s'est pas concrétisé. Mais les ingénieurs ont étudié quelle pourrait être l'architecture VLIW idéale pour ça. Par exemple, il fallait des additions 3-opérandes pour simplifier les calculs d'adresse. Une difficulté était la gestion du registre d'état, dont les bits ne sont pas mis à jour de la même manière sur le x86 et le s390 ou le Power PC. La différence entre gros-boutisme et petit-boutisme était aussi un problème, de même que des différences d'adressage pour les périphériques. Pour que la traduction binaire soit efficace, le processeur VLIW intègre diverses optimisations, comme les exceptions différées et les branchements multi-voies. La principale est la suivante : seul le code des boucles ou fonctions exécutées fréquemment est traduit en code VLIW, le reste du code est interprété. En effet, traduire du code binaire prend plus de temps qu'une simple interprétation. Pour du code qui ne sera exécutée qu'une seule fois, il est plus rapide d'utiliser l'interprétation. Par contre, pour du code exécuté beaucoup de fois, le cout de la traduction binaire est amorti, dilué sur N exécutions, compensé par le gain en temps d'exécution de ce code traduit. Au final, cela permet de ne traduire que le code qui le mérite. La détection du code fréquemment exécuté est réalisée dans le cœur VLIW. L'unité de branchement mémorise les derniers branchements rencontrés et le nombre de fois qu'ils ont été exécutés. Si ils ont été exécutés un certain nombre de fois, l'unité de branchement lève une exception matérielle, qui invoque le traducteur binaire. La routine de cette exception est le traducteur binaire proprement dit. En clair, la traduction binaire est démarrée quand le processeur détecte qu'une fonction a été exécutée plus de N fois, via une exception matérielle dédiée. La mesure du nombre d'exécution d'un branchement se fait dans l'unité de calcul dédiée aux branchements, avec l'aide d'une sorte de ''branch adress buffer'' modifié. Pour rappel, le ''branch adress buffer'' mémorise le ''Program counter'' de chaque branchement récemment rencontré. Ici, chaque entrée du ''branch adress buffer'' est associée à un compteur incrémenté à chaque exécution du branchement. ===Les processeurs Crusoe et Efficieon de Transmetta=== Les processeurs Crusoe Et Efficeron sont deux processeurs VLIW produits par la société Transmetta, une société californienne rachetée par NVIDIA. Ils étaient conçus pour exécuter spécifiquement des programmes x86, système d'exploitation inclus. L'idée derrière ce projet était d'exécuter du code x86 sans que les applications, ni même le système d'exploitation et le BIOS soient au courant ! Les processeurs Transmetta étaient en réalité conçus pour exécuter un programme unique, le ''Code Morphing Software'' (CMS), qui traduisait le code x86 en code VLIW. Transmetta ne voulait pas que son processeur VLIW soit exploité directement, sans le CMS. Il n'avait pas rendu public de compilateur pour traduire du C vers du code VLIW, il n'avait pas donné la documentation du jeu d'instruction VLIW. Des efforts de rétro-ingénieurie, documentés sur le site realworldtech, ont cependant permis de comprendre comment étaient encodées les instructions du processeur Crusoe. Comme pour le projet Daisy, le ''Code Morphing Software'' utilisait à la fois interprétation et traduction binaire, selon les besoins. Les instructions sont interprétées lors de leurs premières exécutions, mais le CMS bascule sur de la traduction en code VLIW après un certain nombre d'exécution. Ainsi, les boucles souvent exécutées sont traduites en code VLIW, alors que le reste du code est interprété. Le choix du code à traduire est le fait du CMS, il utilise des heuristiques complexes pour, qui ne sont pas connues dans le détail. Le ''Code Morphing Software'' est mémorisé dans une EEPROM, ce qui en fait un ''firmware'' situé en dessous du BIOS. Le CMS démarre ensuite le BIOS, qui lui-même démarre le système d'exploitation, qui lui-même démarre les pilotes de périphériques et les programmes. Le CMS se réserve les 16 premiers mébioctets de l'espace d'adressage. L'EEPROM du CMS est mappée dedans, mais n'en utilise que la moitié. Le reste est utilisé comme cache, pour mémoriser le code VLIW traduit par le CMS. Les deux processeurs intégraient aussi deux mémoires SRAM, utilisées par le CMS, appelées la ''local program memory'' (LPM) et la ''local data memory'' (LDM). La première contient du code qui gére les interruptions, la mémoire virtuelle, les exceptions matérielles, les problèmes d'alignement mémoire, et quelques fonctions très fréquemment utilisées par le CMS. La seconde est de la mémoire RAM utilisée par ces fonctions de la ''local program memory''. Les mémoires caches L1 et L2 sont séparées de ces deux mémoires. Le processeur Crusoe et Efficieon étaient des processeurs VLIW très simples. Ils n'avaient même pas de MMU, ni de fonctionnalités importantes sur les CPU x86. Pour Crusoe, les instructions VLIW étaient encodées sur 64 ou 128 bits, et regroupaient entre 2 à 4 opérations. Les instructions LOAD/STORE ne supportaient pas d'adressage indicé, juste de l'adressage indirect à registre. Crusoe avait 5 unités de calcul : deux ALU entières, une unité LOAD/STORE, une unité de branchement, une FPU. Efficieon doublait le nombre d'ALU entière et d'unité LOAD/STORE, les instructions VLIW passaient à 256 bits. Le processeur Crusoe contient 160 registres, dont 64 registres généraux, 32 registres flottants. Les registres généraux font 32 bits, ce qui est cohérent avec le fait que les CPU x86 de l'époque étaient des processeurs 32 bits. Quant aux registres flottants, ils faisaient 80 bits, ce qui colle avec la taille des registres flottants de la FPU x87 utilisée à l'époque. Sur les 64 registres généraux, seuls 48 étaient réellement utilisable pour mémoriser des opérandes. Une partie des registres généraux étaient utilisés pour la gestion de la pile, d'autres pour mémoriser l'état du CPU x86 émulé, un registre était un registre zéro non accesible en écriture. La gestion des exceptions est optimisée avec un système d'exceptions différées, le même que celui décrit plus haut. Le processeur utilise un système similaire, pour effectuer des lectures anticipées, qui a été expliqué dans le chapitre sur les processeurs VLIW/EPIC. Sauf qu'il s'agit là d'une optimisation pour gagner en performances en général, pas une aide utile pour gérer des exceptions précises ou toute autre subtilité de la traduction binaire. Pour gérer les exceptions différées et les lectures anticipées, le processeur mémorise l'état du processeur avant de démarrer un bloc de code. Pour cela, le processeur contient des ''shadow registers'', 48 registres entiers et 16 registres flottants. Vous remarquerez qu'il y a autant de registres entiers que de ''shadow registers'' entiers. Les registres entiers/flottants sont mémorisés dans les ''shadow registers'' avant d'exécuter un bloc de code, afin que le processeur puisse revenir à l'état de base. Une autre fonctionnalité liée est le ''gated store buffer''. Le principe est d'éviter toute écriture en mémoire RAM, tant que le code VLIW n'a pas émis d'instruction ''commit''. L'idée est que les écritures sont conservées dans la ''store queue'' de l'unité LOAD/STORE. Pour rappel, la ''store queue'' met en attente les écritures soit tant que les écritures ne sont pas terminées, soit tant que la RAM est occupée. L'idée est que les écritures sont accumulées dans la ''store queue'' tant que l'instruction ''commit'' n'a pas eu lieu. Le ''store queue'' est "déversé dans la RAM" seulement quand l'instruction ''commit'' s'exécute. ===Les CPU NVIDIA Denver=== Nvidia a tenté quelque chose de similaire avec son projet Denver. NVIDIA a racheté l'entreprise Transmetta, ses brevets, et a tenté de refaire la même chose. Le projet initial était de traduire du code x86 vers un jeu d'instruction VLIW propriétaire, comme l'a fait Transmetta. Mais NVIDIA n'a pas réussi à acquérir la licence du jeu d'instruction x86 et s'est rabattu sur le jeu d'instruction ARM. Le processeur né de ce projet est le Tegra K1-64 CPU. Il s'agit d'un processeur composé d'un cœur VLIW, couplé à deux décodeurs d'instructions ARM. Les deux décodeurs ARM traduisent une paire d'instructions ARM consécutives en instructions VLIW, pour les exécuter sur le cœur VLIW. Le CPU peut fonctionner selon deux modes : ARM et VLIW. En somme, le processeur supporte deux jeux d'instruction : le jeu d'instruction ARM, et un jeu d'instruction VLIW. En mode ARM, les instructions ARM sont chargées depuis le cache, traduites en VLIW par les décodeurs ARM, puis exécutées par le cœur VLIW. En mode optimisé, le CPU exécute des instructions VLIW chargées depuis le cache d'instruction, les décodeurs ne sont pas utilisés. : Le CPU supporte en réalité deux jeux d'instruction ARM : ARM8 et 7. Mais ils correspondent au même mode pour le CPU. Un point important est que le processeur peut changer très rapidement de mode, en à peine quelques cycles d'horloges. On n'est clairement pas dans le cas des CPU x86, qui mettent des plombes pour passer du mode 32 à 64 bits et inversement. La commutation est tellement rapide qu'on peut considérer que le processeur supporte deux jeux d'instruction simultanément. Et le support simultané de deux jeux d'instruction facilite l'implémentation de la traduction binaire. Encore une fois, inutile de traduire du code qui ne sera exécutée qu'une seule fois, mieux vaut l'exécuter directement dans le mode ARM. Surtout que le processeur supporte un mode ARM qui permet d'exécuter du code ARM sans perte de performance. Par contre, le code fréquemment utilisé, notamment dans des boucles critiques, est traduit en VLIW. Au final, les performances sont optimisées, en limitant le travail du logiciel traducteur binaire. La détection du code fréquemment exécuté se fait différemment que pour les processeurs Transmetta et Daisy. L'unité de branchement ne détecte pas directement le code beaucoup exécuté, même si elle a un rôle à jouer. A la place, la détection est réalisée en logiciel, par un ''thread'' dédié, qui s'exécute sur un cœur séparé. Il détecte le code fréquemment utilisé en regardent les branchements exécutés récemment. Pour cela, le processeur mémorise l'historique des branchements pris récemment et copie cette historique dans une mémoire tampon dédiée, partagée entre tous les cœurs. Vu qu'elle est partagée, le ''thread'' de détection a accès à l'historique sans pertes de performances, seule la copie de l'historique dans ce tampon a un cout en performance, pas son partage. Le code VLIW obtenu après traduction binaire est mémorisé dans la mémoire RAM, dans une portion spécialement réservée pour. Elle fait 128 mébioctets, et est appelée le '''cache d'optimisation''' par NVIDIA. Le cache d'optimisation est protégé en écriture et seul le traducteur binaire peut écrire dedans. Au passage, le traducteur binaire est du code VLIW, ce qui fait qu'il est placé dans le cache d'optimisation. Le processeur détecte automatiquement quand une fonction pour laquelle le code compilé équivalent est disponible. Pour cela, l'unité de chargement contient une table de correspondance entre l'adresse de la fonction ARM, et l'adresse de son équivalent compilé. Quand une fonction est appelée, l'unité de chargement regarde l'adresse de destination du branchement. Si l'adresse est dans cette table, elle récupère l'adresse de la fonction compilée et branche vers celle-ci. Le processeur dispose d'un cache d'instruction de 128 kibioctets, ce qui est très important, mais nécessaire vu la taille des instructions VLIW. Le coeur VLIW dispose de ses propres décodeurs, d'un ''scoreboard'', de 7 unités de calcul et de registres. Pour les unités de calcul, il a deux FPU, deux ALU, une unité de branchement, et deux unités LOAD-STORE faisant aussi ALU. Un point important est que le CPU dispose de 64 registres entiers et de 64 registres flottants. C'est deux fois plus que supporte le jeu d'instruction ARM. En mode ARM, seule la moitié des registres est utilisée. Mais en mode optimisé, le traducteur binaire utilise bien les 64 registres, grâce à une sorte de renommage de registres logicielle. Pour simplifier la traduction binaire, le processeur supporte les techniques vues précédemment. Le processeur intègre un ''gated store buffer'' similaire à celui des processeurs Transmetta. Les techniques d'exceptions différées sont aussi supportées, comme sur les processeurs Transmetta. Il a aussi une unité de préchargement décrite par NVIDIA comme agressive, avec support du préchargement de type anticipé (''runahead''). ===L'émulation de la mémoire virtuelle=== Pour finir, il faut parler de la mémoire virtuelle avec la traduction binaire. Sur le processeur source, le processeur gère à la fois des adresses physiques et virtuelles et sa MMU fait la conversion entre les deux. Sur le processeur VLIW, la MMU est simulée par le programme de traduction binaire, partiellement ou totalement. L'implémentation exacte varie suivant que l'on parle du projet Daisy, des processeurs Transmetta ou de Denver. Sur les processeurs Transmetta, il n'y a pas de MMU ni de mémoire virtuelle. A la place, le programme de traduction binaire émule la mémoire virtuelle du processeur source. Le processeur VLIW ne gère que des adresses physiques, rien d'autre. L'espace d'adressage physique du processeur VLIW a la même taille que celui du processeur émulé, ici des processeurs x86. Il est vraisemblable que les adresses physiques utilisées par le processeur x86 sont les mêmes que celles du processeur VLIW. Sur le projet Daisy, le processeur VLIW gère la mémoire virtuelle via pagination, comme les processeurs émulés. Ce qui fait qu'il gère un espace d'adressage virtuel et un espace d'adressage physique. Pour éviter toute confusion, nous parlerons d'adresse physique/virtuelle VLIW pour les adresses physiques/virtuelles du processeur VLIW, d'adresse virtuelle/physique source pour celle du jeu d'instruction traduit, à savoir du Power PC ou du s390, éventuellement de l'x86. Il faut alors faire le lien entre adresses physiques source et adresses virtuelles VLIW. Pour cela, rien de plus simple : il y a correspondance parfaite. L'adresse physique source numéro N correspond à l'adresse virtuelle VLIW numéro N. Cependant, cela signifie que tout l'espace d'adressage virtuel VLIW serait utilisé par le code à traduire. En réalité, il faut ajouter de la place pour le code traduit et le programme de traduction binaire. La conséquence est que l'espace d'adressage virtuel VLIW est plus large que l'espace d'adressage source. L'espace d'adressage virtuel VLIW est découpé en trois sections : une section pour le code Power PC à traduire, une autre pour le code traduit en VLIW, et une dernière réservée au traducteur binaire. Le programme de traduction est placé dans une ROM mappée en mémoire dans la seconde section. Le reste de la seconde section est réservé à une mémoire RAM utilisée par le programme de traduction. [[File:Mémoire virtuelle sur le projet Daisy.png|centre|vignette|upright=1.5|Mémoire virtuelle sur le projet Daisy]] Il faut noter que chaque page physique source, une fois traduite, correspond à N pages virtuelles VLIW. La raison est que le code traduit prend plus de place que le code machine originel. En conséquence, on doit utiliser une page finale plus grande. Pour le reste, des pages contiguës en mémoire physique source sont elles aussi contiguës en mémoire virtuelle VLIW. Le calcul d'adresse est donc simple : l'adresse physique source est multipliée par N, puis on ajoute l'adresse de base à laquelle commence la section pour le code traduit. ==La traduction binaire sur les Burroughs B1700== Nous venons de voir comment accélérer en matériel la traduction binaire, avec des techniques bien spécifiques. Le Burroughs B1700 a procédé autrement et il est intéressant d'étudier en détail son architecture. Son architecture est décrite dans le livre "Interpreting Machines : Architecture and Programming of the Bl700/Bl800 Series", écrit par Elliott I. Organick et James A. Hinds. Une partie de ce qui vfa suivre est un très court résumé de ce livre. : Dans ce qui suit, nous parlerons de traducteur binaire pour parler soit d'un interpréteur pour un langage de programmation de haut niveau, soit pour un logiciel de traduction binaire. ===L'ALU de taille variable=== Les Burroughs B1700 avaient une '''ALU de taille variable''', ce qui veut dire qu'elle pouvait faire des calculs sur un nombre de bits compris entre 0 et 24. Pour cela, les calculs étaient fait avec une ALU de 24 bits, puis les bits de poids fort inutiles étaient masqués. L'ALU de 24 bits pouvait masquer les bits de poids fort du résultat, mais avec des limitations. Elle gérait des opérandes et résultats de 0, 4, 8, 12, 16, 20 et 24 bits. Pour avoir un réglage plus fin, une ALU 4 bits séparée permettait de corriger le résultat. Les registres X et Y étaient reliés à une ALU de 24 bits capable d'effectuer des opérations arithmétiques basiques. Fait étonnant, l'ALU faisait tous les calculs en même temps et fournissait ses résultats dans 7 registres : un registre SUM pour l'addition, un registre DIFFERENCE pour la soustraction, et un registre par opération bit à bit. Les registres pour les opérations bit à bit sont : CMPX et CMPY pour l'opération NOT sur chaque opérande, XANY pour le ET bit à bit, XEOY pour le XOR, XORY pour le OU logique. Les additions et soustractions pouvaient se faire sur des opérandes codés en binaire ou en BCD, suivant le code opération utilisé. A cela, il fallait ajouter deux registres de "résultat" MSKX et MSKY. Ils fournissaient un masque dépendant de la taille du résultat en bits. C'est ce registre qui permettait de gérer des données de 0, 4, 8, 12, 16, 20, et 24 bits, alors que l'ALU faisait 24 bits. Le masque dans ce registre pouvait servir d'opérande dans une opération de masquage ultérieure, pour corriger le résultat. L'ALU prenait en opérande les registres X et Y, mais aussi un registre CYF pour la retenue entrante. En sortie, il fournissait aussi deux retenue sortantes : une pour l'addition et une autre pour la soustraction. Elles étaient mémorisées dans les registres CYL et CYD, respectivement. L'ALU fournissait aussi 12 conditions, qui alimentait un registre d'état. Le registre d'état était segmenté en trois registres appelés XYST, XYCN et BICN. Les registres X et Y sont des registres pour les opérandes, mais il y a aussi deux autres registres de 24 bits. Il s'agit des registres T et L, qui sont des registres généraux. Les registres T et L étaient découpés en 6 sous-registres de 4 bits, nommés LA LB LC LD LE et LF pour le registre L, et TA TB TC TD TE et TF pour le registre T. Ils étaient adressables comme n'importe quel registre. Le processeur contenait de nombreux autres registres de 4 bits, dont les sous-registres de T et L. En tout, il y a 27 registres de 4 bits adressables ! Et le processeur disposait d'une ALU 4 bits pour manipuler ces registres de 4 bits. L'ALU 4 bits est capable de faire des MOV entre registres de 4 bits, des opérations bit à bit, et peut aussi tester si un bit vaut 0 ou 1. La dernière possibilité est très utile pour implémenter les branchements. De plus, cela permettait de faire des opérations de masquage, notamment pour appliquer un masquage supplémentaire à celui de l'ALU. Par exemple, pour gérer des opérations sur 14 bits, on effectue une opération sur 16 bits avec l'ALU, et on masque les 2 bits manquants avec l'ALU 4 bits. ===Une architecture bit-adressable=== Le CPU Burroughs B1700 était '''bit-adressable'''. En clair, il pouvait adresser la mémoire bit par bit, si nécessaire. Un avantage est que cela facilitait la traduction binaire des instructions machine, mais nous détaillerons cela plus bas. Un autre avantage est que cela permettait d'émuler des architectures dont la taille des registres/''byte''/mots était très différente. Et cet avantage mérite quelques explications immédiates. Le processeur était un CPU 24 bits, mais il pouvait émuler des processeurs 16 bits, 8 bits, 4 bits, 1 bit, 9 bits, ou toute autre valeur. Pour cela, le processeur lisait des mots de 24 bits et utilisant un ''barrel shifter'' pour sélectionner les bits adéquats. Par exemple, pour un accès de 16 bits, la donnée lue en mémoire était masquée de manière à ne garder que les 16 bits de poids faible. Le ''barrel shifter''/circuit de masquage était placé directement avant le bus mémoire, sur le trajet des données/instructions. Le ''barrel shifter'' était commandé par un registre ''Field Unit'', qui précisait quelle était la taille des données à charger. Pour communiquer avec la mémoire, il avait trois registres séparés : READ, WRITE, MAR. READ contient la donnée lue lors d'une lecture, WRITE est pour une donnée à écrire lors d’une écriture, MAR contient l'adresse à lire ou en cours de lecture. Il s'agit de registres d’interfaçage mémoire. Le fait que ces registres d’interfaçage mémoire et IO soit adressable est peu commun, et c'est plus quelque chose qu'on attend d'un microcode que d'instructions machines. Mais il faut bien comprendre que les registres d’interfaçage mémoire ont été promus au rang de registres architecturaux, sur ce processeur. De même, pour communiquer avec les entrées-sorties, il disposait de deux registres CMND et DATA : CMND pour envoyer une commande à une entrée-sortie, DATA pour lire ou écrire une donnée. Il y avait aussi un registre U, qui mémorisait une donnée lue depuis le lecteur de cassette. ===Le code machine du Burroughs B1700=== Le processeur utilise des instructions de 16 bits de long, qui sont copiées dans registre d'instruction M de 16 bits. Le ''program counter'' est appelé le registre A. Les instructions du processeur font 16 bits de long, et elles sont alignées en RAM sur 16 bits, ce qui fait que les 4 bits de poids fort de ce registre valent 0, seuls les 14 bits de poids fort sont utiles. Les adresses mentionnées plus haut, qui adressent les micro-opérations en mémoire RAM, sont des adresses de mots de 16 bits, pas les adresses de bit individuels. Fait intéressant, il était possible de faire un OU logique entre l'instruction lue et un registre du processeur, le résultat étant mémorisé dans le registre M. Cela permettait de faire du code automodifiant, pour émuler des modes d'adressage complexe. Cela permettait par exemple de remplacer une adresse dans une micro-opération. L'adresse est alors surimposée avec un OU dans le champ d'adresse, alors que le reste de l'instruction n'est pas modifié. Afin de gérer les interruptions, ces dernières étaient partiellement détectées en logiciel. Une interruption doit être prise en compte à la toute fin de l'exécution d'une instruction source, quand celle-ci a finit son travail. Mais vu qu'ici, les instructions sont émulées avec des séries d'instructions cibles, il y a un problème : il est possible d'interrompre une instruction émulée en plein milieu. Pour éviter cela, le processeur ne déclenche pas les interruptions matérielles immédiatement, elles sont mises en attente. Une instruction dédiée vérifie si une interruption est en attente, et l'exécute alors cas échéant. L'instruction en question est utilisée en dernier, dans la suite d'instruction qui émule l'instruction source. Les Burroughs B1700 intégrait une pile d'adresse de retour, capable de mémoriser 16 adresses de 24 bits. Il disposait de 4 registres généraux, nommés X, Y, T et L, tous de 24 bits. Un point important est que le registre T avait des possibilités d’extraction spécifiques. Il était possible d'extraire N bits, placés n'importe où dans ce registre, et de les copier dans un autre registre. Le CPU supportait une '''instruction EXTRACT''' pour ça. Nous verrons pourquoi cette possibilité est intéressante et ne sert que pour le registre T. ===La traduction binaire sur le Burroughs B1700=== Les instructions source peuvent faire 16 bit avec tel jeu d'instruction, 8 bits avec un autre, 32 bits sur un autre, etc. Elles peuvent aussi être de longueur fixe ou variable ! Le B1700 utilisait une architecture bit-adressable pour gérer ces contraintes. En effet, avec un CPU bit-adressable, les contraintes d'alignement des instructions disparaissent ! Dans ce qui suit, on suppose que l'instruction est composée de '''champs''' : l'opcode, les opérandes, les adresses absolues, les constantes immédiates, les numéros de registre, etc. L'idée est que l'instruction source est chargée dans le registre T, puis découpée en champs, qui sont copiés dans les registres du processeur ou envoyés au microcode. Je rappelle que le registre T est relié à un ''barrel shifter'', différent au précédent, qui permettait de masquer, décaler et extraire une donnée de ce registre. Et il y a la possibilité d'extraire une suite de bit de ce registre T, pour la copier dans un autre registre. Par exemple, prenons une instruction composée d'un opcode, d'une constante immédiate et d'une adresse immédiate. Premièrement, elle est chargée dans le registre T. Deuxièmement, la constante immédiate est extraite du registre T et copiée dans le registre X. Troisièmement, l'adresse est extraite et est copiée dans le registre d’interfaçage mémoire, une lecture est démarrée, et l'opérande lue est copiée dans le registre Y. Enfin, l'instruction équivalente à l'opcode est exécutée par le microcode. Le tout prend plusieurs instructions : une première pour lire l'opcode, une seconde pour lire la première opérande, une troisième pour lire la seconde opérande, una quatrième pour tout exécuter. Notons que suivant la taille des champs, on peut en charger plusieurs à la fois. Par exemple, imaginons qu'on ait chargé 24 bits, contenant un opcode de 8 bits et une constante immédiate de 16 bits : le registre T est découpé en un opcode et une constante, avec deux instructions EXTRACT. L'architecture est bit-adressable car elle gère des champs qui ne font pas forcément 8 ou 16 bits. Par exemple, certains jeux d'instructions ont des opcodes codés sur 6 ou 7 bits, voire 9-10 bits. Utiliser une architecture bit-adressable règle ce problème d'alignement. On peut lire un champ depuis la mémoire RAM en précisant l'adresse de son premier bit, pas besoin de gérer des accès non-alignés. Par exemple, prenons une instruction source avec un opcode de 7 bits, suivi par une adresse de 12 bits et une constante immédiate de 15 bits. On peut charger 7 + 12 bits en un premier accès, pour extraite l'opcode et l'adresse, suivi par un second accès de 15 bits pour extraire la constante immédiate. Pour charger un champ, il faut connaitre plusieurs informations : l'adresse de son premier bit, sa taille. Pour cela, le B1700 contient deux registres, nommés ''Field Adress'' (FA) et ''Field Length'' (FL), qui indiquent l'adresse du premier bit et la taille de l'instruction à charger. Le premier fait 24 bits, ce qui permet de gérer des adresses de 24 bits. N'oubliez pas que le processeur est bit-adressable, ce qui permet d'adresser 16 Mébi-bits, soit 2 mébioctets. Le registre pour la taille fait lui 16 bits, ce qui permet de gérer des instructions de 65536 bits, soit 8192 octets ! Leur nom commence par ''field'', car le processeur peut gérer des instructions de taille variable et/ou plus longues que 24 bits. Dans ce cas, le chargement de l'instruction se fait champ par champ. Pour donner un exemple, prenons une instruction composée de 20 champs de 12 bits chacun. Les champs sont chargés un par un dans le registre T. Le registre FA est alors initialisé avec l'adresse de l'instruction, il est incrémenté de 12 à chaque cycle. Le registre FL est initialisé avec la taille de l'instruction, soit 20 * 12 = 240 bits, et il est décrémenté de 16 à chaque cycle. Précisons cependant que la taille d'un champ peut être changée d'un cycle à l'autre. Par exemple, le processeur peut charger un opcode de 8 bits, l'interpréter, détecter que l'instruction demande ensuite de charger une adresse de 16 bits. Dans ce cas, le registre FL est immédiatement altéré pour charger 16 bits, au lieu des 8 bits de l'opcode immédiatement précédent. ===Le Burroughs B1726 : le mal-nommé ''split-level control store''=== Le Burroughs B1700 avait plusieurs modèles : le B1726, le B1710 et le B1800. * Le B1710 plaquait le traducteur binaire en mémoire RAM. * Le B1800 avait ajouté un cache dédié au code du traducteur binaire, afin d'améliorer les performances. * Le B1726 utilisait un ''local store'' dédié au traducteur binaire. Le B1726 peut exécuter le traducteur binaire soit depuis le ''local store'', soit depuis la mémoire RAM avec une pénalité en termes de performances. La documentation appelle cela un '''''split-level control store''''', mais le terme est trompeur : la documentation qualifie de microcode le langage machine du Burroughs B1700, ce qui est à l'origine de beaucoup de confusions. Une autre source de confusion est que ce langage machine a quelques propriétés qu'on pourrait attendre d'un microcode, mais certaines sont manquantes. Par exemple, il peut adresser les registres d’interfaçage mémoire, mais c'est parce que ce sont des registres architecturaux sur ce processeur. Ses instructions n'encodent pas de signaux de commande, par contre, elles font 16 bits et adressent les opérandes de manière implicite. Le ''local store'' mémorisait au maximum 2048 instructions machine, ce qui fait que les 2048 premières "adresses" correspondaient au ''local store''. Les suivantes étaient des adresses de mémoire RAM qui contenaient le reste du traducteur binaire. En pratique, l'adresse qui séparait les deux était configurable ! On pouvait limiter la taille du ''local store'' à un multiple de 32 instructions ! Les adresses restantes étaient alors réattribuées à la RAM. Par exemple, on pouvait attribuer 64 adresses au ''local store'', les 2048 - 64 adresses restantes étaient alors attribuées à la mémoire RAM. Un registre limite, nommé TDPM, indiquait quelle était l'adresse de démarcation entre les deux. Le programmeur devait découper le traducteur binaire en segments, certains étant placés dans le ''local store'', les autres étant en mémoire RAM. Mieux que ça, le processeur autorisait d'utiliser la technique de l'''overlaying'' pour le microcode ! Il y avait même une fonction OVERLAY dédiée pour ! OVERLAY avait besoin de trois opérandes : la taille du segment à copier, son adresse source en mémoire RAM, son adresse de destination dans la micro-SRAM. L'interpréteur réservait un "segment" en mémoire RAM, pour les données, dont le processeur connaissait la position. En dehors de ce segment, il n'y a que des instructions machines à traduire, et le traducteur binaire. Le CPU incorporait pour cela deux registres, un pour l'adresse de base du segment de données, un autre pour son adresse de fin. Il incorporait une protection mémoire limitée sur ce segment, à savoir que seul l'interpréteur pouvait lire ou écrire dans ce segment. ==Les jeux d'instructions dédiés à un langage de programmation== De rares processeurs sont conçus pour un langage de programmation en particulier. On appelle ces processeurs, conçus pour des besoins particuliers, des '''processeurs dédiés'''. Par exemple, les fameux ''Burrough E-mode'' B5000/B6000/B7000 étaient spécialement conçus pour exécuter de l'ALGOL-60. Leurs cousins B2000/B3000/B4000 étaient eux conçus pour le COBOL. Des langages fonctionnels ont aussi eu droit à leurs processeurs dédiés. Le prolog en est un bel exemple, avec les superordinateurs de 5ème génération qui lui étaient dédié. On peut aussi citer les machines LISP, dédiés au langage LISP, qui datent des années 1970. Elles étaient capables d’exécuter certaines fonctions de base du langage directement dans leurs circuits : elles possédaient notamment un ''garbage collector'' câblé dans ses circuits ainsi que des instructions machines supportant un typage déterminé à l’exécution. Les processeurs dédiés ont eu leur heure de gloire au début de l'informatique, à une époque où les langages de haut niveau venaient d'être inventés. À cette époque, les compilateurs n'étaient pas performants et ne savaient pas bien optimiser le code machine. Il était alors rationnel, pour l'époque, de rapprocher le code machine cible et le langage de programmation de haut niveau. De nombreuses architectures dédiés ont ainsi été inventées, avant que les concepteurs se rendent compte des défauts de cette approche. Les défauts en question ne sont pas nombreux, mais assez simples à comprendre. Premièrement, elles sont très rapides pour un langage de programmation en particulier, mais sont assez mauvaises pour les autres, d'où un problème de "compatibilité". Ajoutons à cela que les langages de programmation peuvent évoluer, devenir de moins en moins populaires/utilisés, ce qui rend la création d'architectures généralistes plus pertinente. Enfin, les architectures dédiées sont évidemment des processeurs CISC, pour implémenter les nombreuses fonctionnalités des langages évolués. Et les défauts des CISC sont assez rédhibitoires à l'heure actuelle. À l'heure actuelle, les algorithmes des compilateurs se sont améliorés et savent nettement mieux utiliser le matériel. Ils produisent du code machine efficace, ce qui rend les architecture dédiées bien moins intéressantes. Si on ajoute les défauts de ces architectures dédiées, par étonnant que les architectures dédiées aient presque disparues. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'accélération matérielle de la virtualisation | prevText=L'accélération matérielle de la virtualisation | next=Le matériel réseau | nextText=Le matériel réseau }} </noinclude> df7uvm12cgyysdica6focjjwj6q5lx5 764065 764064 2026-04-19T21:33:16Z Mewtow 31375 /* La traduction binaire sur les Burroughs B1700 */ 764065 wikitext text/x-wiki De nos jours, la majorité des programmeurs programment dans des langages de haut niveau. Il est très rare pour eux d'avoir à utiliser de l'assembleur, et encore moins un langage machine. Les programmes écrits dans un langage de haut niveau sont traduits en langage machine par un logiciel appelé le compilateur. [[File:Compilation.PNG|centre|vignette|upright=2.5|Compilateur : principe.]] Il se trouve que quelques rares processeurs sont conçus pour faciliter le travail du compilateur. Et ce sont ces processeurs que nous allons voir dans ce qui suit. Ils représentent une catégorie de jeux d'instruction à part, qui n'a pas vraiment de nom. ==La compilation et l’interprétation== Un compilateur traduit donc du code source, écrit dans un langage de haut niveau, vers du langage machine. La traduction est rarement directe. En général, le compilateur traduit le code source en assembleur, qui est lui-même traduit en langage machine. L'assembleur est une représentation textuelle du langage machine alors que le code machine est du binaire exécutable par le processeur. La traduction se fait donc en deux étapes, la '''compilation''' proprement dite et l''''assemblage''', réalisées respectivement par un compilateur et un assembleur. Il faut parfois rajouter une troisième phase d'édition des liens, que nous passons volontairement sous silence. {| |[[File:Code C.png|vignette|200px|Le code source, ici en C.]] |[[File:Assem.png|vignette|250px|Le code assembleur.]] |[[File:Binary file - hello world (C programming).png|vignette|150px|Le code machine.]] |} ===L'interprétation et la compilation à la volée=== Une alternative à la compilation est l''''interprétation''', qui transforme le code source en code machine à la volée. Avec l'interprétation, le code source est passé à un logiciel appelé l''''interpréteur''', qui exécute le code source ligne de code par ligne de code. Une ligne de code est traduite en code machine, qui est exécutée, puis l'interpréteur passe à la ligne suivante. [[File:Kaantotulkkaus.png|centre|vignette|upright=3|Compilation versus interprétation.]] Un défaut de l'interprétation est son cout en performance, qui est assez important. Aussi, l'interprétation stricte a évolué vers un hybride entre interprétation et compilation. L'idée est qu'une partie peu importante du code source est interprétée, alors que le code important est compilé juste avant d'être exécuté. Pour le dire autrement, le code source est partiellement compilé à la volée, juste avant son exécution. Aussi, on parle de '''compilation à la volée'''. Le terme en anglais est ''Just In Time compilation'', abrévié en JIT. En soi, le JIT est associé à de l'interprétation, les JIT sont en réalité des hybrides interpréteurs-compilateurs. Un point important est que compiler du code à la volée est assez lourd, cela a un cout en performance. Par contre, le code compilé s'exécute plus vite que du code interprété. Si on exécute le code compilé une seule fois, le cout de la compilation l'emporte sur le gain à l'exécution. Mais le code compilé est réutilisé autant de fois que nécessaire. Le gain à l'exécution est donc multiplié par le nombre d'exécutions, alors que le cout de la compilation est répartit sur plusieurs exécutions, il est amorti. Les portions du code sont choisies de manière à avoir un gain en performance maximal. L'idée générale est que le code exécuté très souvent est compilé, alors que du code exécuté pas souvent est interprété. Typiquement, les boucles les plus souvent exécutées sont compilées, le code exécuté une seule fois est interprété. ===Les langages intermédiaires=== Les compilateurs modernes passent par un '''langage intermédiaire''' pour faire la transformation en code machine. Le compilateur traduit le langage de haut niveau en langage intermédiaire, puis traduit le langage intermédiaire en code machine cible. Le langage intermédiaire est parfois appelé le '''''bytecode''''', et ce terme recouvre aussi le code écrit avec du ''bytecode''. L'interprétation aussi peut passer par l'intermédiaire d'un un ''bytecode'', ce n'est pas limité aux compilateurs proprement dit. {| |[[File:Intermediate representation scheme.png|centre|vignette|upright=1.5|Représentation intermédiaire d'un compilateur.]] |[[File:Code interpreter scheme.png|centre|vignette|upright=1.5|Fonctionnement d'un interpréteur.]] |} Faire ainsi a de nombreux avantages, le principal étant d'avoir un compilateur capable de traduire un langage de haut niveau vers plusieurs jeux d'instructions différents. Cela permet d'avoir, par exemple, un compilateur qui traduit du C soit en code machine x86, soit en code machine pour un CPU ARM, soit pour un CPU POWERPC, etc. [[File:Compiler design.svg|centre|vignette|upright=3|Compiler design]] Pour l'interprétation, l'usage d'un ''bytecode'' a d'autres avantages. Pour l'exploiter, il faut distribuer non pas le code source, mais le ''bytecode''. En clair, l'interpréteur ne prend pas en entrée le code source, mais du ''bytecode'' déjà compilé. La traduction est alors beaucoup plus simple, car le code source a déjà été partiellement compilé, par la traduction en ''bytecode''. Interpréter du code source est en effet assez compliqué : il faut effectuer des étapes d'analyse lexicale, sémantique, et bien d'autres. Avec du ''bytecode'', ces étapes ont été réalisées lors de la compilation du ''bytecode'', l'interpréteur a alors peut de choses à faire. L'interprétation du ''bytecode'' se fait instruction par instruction, au niveau du ''bytecode''. Une instruction du ''bytecode'' est traduite en une instruction machine équivalente, qui est exécutée. Si l'instruction est un peu complexe, il exécute une fonction/procédure qui fait la même chose. Pour résumer, le ''bytecode'' est ensuite traduit à la volée et exécuté instruction par instruction par un logiciel appelé l'interpréteur. [[File:Control table.png|centre|vignette|upright=2|Control table]] Le langage intermédiaire peut être vu comme l'assembleur d'un processeur, n'existe pas forcément dans la réalité, mais dont le jeu d'instruction est décrit en détail. Le processeur en question est appelé une '''machine abstraite''', ou encore une ''machine virtuelle''. Nous utiliserons le terme de machine abstraite dans ce qui suit. La machine abstraite n'est pas la même suivant que l'on cible la compilation ou l'interprétation/JIT. Pour l'interprétation/JIT, la machine abstraite est souvent une machine à pile, car cela simplifie grandement la traduction en code machine final. L'interprétation demande que la traduction du ''bytecode'' en code machine soit la plus simple possible. Et cela demande de prendre en compte pas mal de détails. Par exemple, les différents jeux d'instruction existants n'ont pas le même nombre de registres, ce qui pose problème lors de la traduction du ''bytecode''. Lors de la transformation en code machine, un algorithme d'allocation de registres se débrouille pour traduire le code intermédiaire en code qui utilise un nombre limité de registres. Pour améliorer cette allocation de registres, il y a deux solutions niveau ''bytecode'' : soit utiliser un nombre illimité de registres, soit utiliser une machine à pile. Pour les machines à pile, il existe un algorithme simple et rapide pour traduire un code écrit pour une machine à pile en un code écrit pour un processeur avec des registres, qui se débrouille pas trop mal pour allouer efficacement les registres. C'est un avantage assez important pour les langages interprétés. ==Les processeurs qui exécutent du bytecode== Le ''bytecode'' est un code machine, ce qui signifie qu'il peut en théorie s'exécuter sur un processeur qui implémente le jeu d’instruction associé. Si le jeu d'instruction d'un ''bytecode'' est souvent une description censée être fictive, elle n'en reste pas moins un jeu d'instruction et des caractéristiques précises, qu'on peut l'implémenter en matériel ! Néanmoins, tous les ''bytecode'' ne sont pas égaux de ce point de vue. Certains sont faciles à implémenter en matériel, d'autres non. Les ''bytecodes'' implémentés en matériel sont ceux dont les machines abstraites sont des machines à pile. Un premier exemple est celui des processeurs Pascal MicroEngine, qui exécutaient directement le bytecode du langage Pascal, le fameux UCSD P-code. Un second exemple est la '''machine SECD''', qui sert de langage intermédiaire pour certains compilateurs de langages fonctionnels. Elle a été implémentée en matériel par plusieurs équipes, notamment par les chercheurs de l'université de Calgary en 1989. Dans le même genre, quelques processeurs simples étaient capables d’exécuter directement le ''bytecode'' du langage FORTH. Le FORTH, un des premiers langages à pile de haut niveau, possède de nombreuses implémentations hardware et est un des rares langages de haut niveau dont le ''bytecode'' a été utilisé comme langage machine sur certains processeurs. Par exemple, on peut citer le processeur FC16, capable d’exécuter nativement du ''bytecode'' FORTH. ===Les processeurs Java=== Le cas le plus impressionnant est celui de la machine virtuelle Java, qui est un design de processeur comme un autre. En temps normal, le ''bytecode'' Java est compilé ou interprété, mais certains processeurs exécutaient du ''bytecode'' Java directement, sans interprétation ni compilation. Ils sont appelés des '''processeurs Java'''. On peut citer les processeurs ARM disposant de l'extension Jazelle, du JEMCore et du aJ-100 de aJile Systems, le picoJava II de Sun, et quelques autres. Il y a aussi eu quelques projets de recherche ou processeurs open source, comme Komodo, jamuth, le ''Java Optimized Processor'' et quelques autres projets du même genre. L'implémentation de la machine virtuelle Java n'était cependant pas complète. Quelques instructions complexes n'étaient pas gérées par le processeur et devaient être émulées en logiciel. Mais la grosse majorité des instructions l'était. Les instructions non-supportées étaient émulées avec la méthode ''trap and emulate'', à savoir qu'un opcode inconnu déclenchait une exception matérielle ''opcode invalide'', dont la routine pouvait émuler les instructions qui doivent l'être. Le processeur aJ-100 de aJile est capable d'exécuter la quasi-totalité des instructions de la JVM Java, seules deux instructions faisant exception. Les '''accélérateurs Java''' sont une solution intermédiaire, qui permet de traduire à la volée du ''bytecode'' Java en instructions RISC, exécutées sur un autre processeur. Il s'agit formellement de traduction binaire, mais je préfère en parler ici. L'idée est de combiner un processeur RISC avec un coprocesseur Java. Le coprocesseur lit le ''bytecode'' Java et le traduit en instructions RISC, ces dernières étant exécutées sur le processeur RISC. Le JA108 de Nozomi était un coprocesseur de ce genre, au même titre que le coprocesseur JSTAR de JEDI. L'extension Jazelle de certains CPU ARM fonctionnait vraisemblablement sur un principe similaire, sauf que le coprocesseur était intégré dans le processeur ARM, entre le cache d'instruction et le décodeur d'instructions ARM. L'intérêt des processeurs Java n'est pas qu'une question de performance. Certes, ils permettent d’exécuter plus vite les programmes compilés en ''bytecode'', comme des programmes Java pour la JVM Java. Mais l'intérêt est aussi de faire des économies de mémoire RAM et ROM. Je rappelle que la machine virtuelle Java est une machine à pile, ce qui fait que sa densité de code est excellente. Les programmes en ''bytecode'' Java sont donc très petits, et prennent peu de place en mémoire ROM. De plus, utiliser un processeur Java permet de se passer d'interpréteur ou de JIT Java, ce qui économise encore plus de mémoire. ===Transformer un processeur RISC en machine à pile=== La plupart des processeurs Java utilisent en interne une architecture à registre généraux, qui émule une machine à registre en microcode. Quelques registres sont réservés pour l'état de la machine virtuelle, avec au minimum un registre réservé pour le pointeur de pile, un autre pour un pointeur vers un ''pool'' de constantes, et quelques autres. D'autres registres servent à mémoriser le haut de la pile, à savoir le sommet de la pile et quelques opérandes situées juste en-dessous. Par exemple, le processeur picoJava II gardait les 64 opérandes au sommet de la pile dans 64 registres généraux, séparés des registres pour les pointeurs et l'état de la JVM. Une optimisation possible, envisagée sur les processeurs picoJava, était une sorte de macro-fusion sous stéroïde. L'idée était de fusionner certaines séries d'instructions Java en une seule instruction machine. Typiquement, pour faire une opération arithmétique, une machine à pile empile deux opérandes et exécute une opération, ce qui prend trois instructions machine. La spécification picoJava 1 a proposé d'envoyer directement la seconde opérande en entrée de l'ALU, ce qui fusionnait l'empilement de la seconde opérande et l'opération arithmétique. Le processeur picoJava II va plus loin et fusionne les trois instructions en une seule instruction RISC, agissant sur des registres. Mais la majorité des processeurs Java ne faisaient pas cela, car cela complexifie grandement le décodeur. Les processeurs ''bigfoot'' de DCT utilisaient un système différent pour émuler une pile avec des registres. Leur banc de registre de 64 bits était coupé en deux : les 16 premiers registres étaient utilisés pour la pile, les 48 suivants étaient utilisés pour autre chose. L'idée est que le registre 0 mémorise un emplacement vide, le registre 1 contient le sommet de la pile, le registre 2 l'opérande sous le sommet de la pile, et ainsi de suite. Les 15 premières opérandes de la pile sont donc mémorisées dans les registres. Les instructions écrivent leur résultat dans le registre adéquat, puis tous les registres sont décalés pour que le résultat passe dans le registre R1. Par exemple, les instructions PUSH chargent une opérande dans le registre 0, puis tous les registres sont décalés de un rang, pour que l'opérande chargée soit dans le registre 1. Le registre 0 devient alors le registre numéro 1, le registre 1 devient le 2, et ainsi de suite. Par contre, les opérations arithmétiques font autrement. Une instruction d'addition, par exemple, lit les opérandes dans le registre 1 et 2, puis place son résultat dans le registre 2 (les deux opérandes sont dépilées). Puis, les registres sont décalés d'un rang pour que ce registre 2 devienne le 1. Les registres ne sont en réalité pas décalés, il n'y a pas de transferts entre registres. A la place, les 16 premiers registres utilisaient une forme limitée de renommage de registres, similaire au banc de registre tournant utilisé sur les processeurs EPIC comme l'Itanium. Les numéros de registres sont renommés, décalés, à chaque fois qu'on empile ou dépile une opérande. Pour faire le renommage de registres, processeur contient un ''register stack counter'' de 4 bits, qui est incrémenté à chaque fois qu'on empile une donnée, et est décrémenté quand on dépile une donnée. Quand on veut accéder à un opérande, le numéro de registre architectural associé est traduit en numéro de registre physique, en additionnant le ''register stack counter'' (modulo 16, pour rester sur 4 bits). ==L'accélération de l'interprétation/JIT : le ''Thumb-EE'' d'ARM== Quelques processeurs ont ajouté des instructions pour faciliter le travail des interpréteurs/JIT. Par exemple, en 2005, ARM a ajouté le mode ''Thumb Execution Environment'', qui faisait cela. Il reprenait le jeu d'instruction compact ''thumb'' et ajoutait quelques instructions et modifiait le comportement d'instructions existantes. Le jeu d'instruction ''Thumb-EE'' a été ajouté sur les CPU ARM en 2005, mais a été déprécié en 2011, par manque d'utilité. Mais la tentative mérite qu'on s'attarde dessus. Les CPU de gamme M, déstinés à l'embarqué, n'ont jamais supporté ''Thumb-EE'', vu que de tels processeurs ne sont pas conçus pour exécuter du code interprété/JIT. Le mode ''Thumb-EE'' est un mode d'exécution, séparé de l'ARM normal. Ainsi, une instruction ''Thumb-EE'' et une instruction ARM peuvent avoir le même encodage en binaire, mais se comporter différemment. Le processeur est à tout moment soit en mode ''Thumb-EE'', soit en mode ARM normal, ce qui précise comment ces instructions doivent se comporter. Des instructions ont été ajoutées par ''Thumb-EE'', pour qu'il fasse son travail. L'entrée et la sortie du mode ''Thumb-EE'' se fait avec deux instructions : ENTERX et LEAVEX. Une fois en mode ''Thumb-EE'', le décodage des instructions ''thumb'' se fait avec les règles du ''Thumb-EE'', et non celles du ''thumb'' normal. Une première différence entre ''thumb'' normal et ''Thumb-EE'' est l'ajout de ce qui s'appelle un ''null check'' pour les instructions mémoire. Le ''null check'' est utilisé pour les instructions en mode d'adressage "base + indice" ou "Base + décalage", ou tout autre mode d'adressage avec un registre de base utilisés dans des calculs d'adresse. L'idée est de vérifier si l'adresse de base vaut zéro ou non. Si c'est le cas, le processeur lève une exception matérielle, qui est traitée par l'interprétation ou le compilateur JIT. Une telle situation est en effet signe d'une erreur d'adressage mémoire, qui doit être traitée par l'interpréteur. Typiquement, c'est signe qu'un pointeur n'a pas bien été initialisé, ce qui peut arriver avec du code interprété ou JIT. Les interpréteurs/JIT traitent généralement la situation en ajoutant un test avant toute instruction mémoire, pour vérifier si le pointeur accédé vaut zéro ou non. Avec ''Thumb-EE'', pas besoin d'ajouter les tests en questions : ils sont réalisés automatiquement lors de chaque instruction mémoire. : Les registres pour la pile, comme le pointeur de pile et le pointeur de ''frame'', sont aussi concernés. L'instruction CHKA vérifie que les accès à un tableau ne débordent pas en dehors du tableau en question. Par exemple, pour un tableau de 1024 éléments, elle vérifie si l'indice est compris entre 0 et 1024. Pour cela, elle vérifie si l'indice de tableau est bien dans l'intervalle adéquat et lève une exception matérielle si ce n'est pas le cas. Son utilité se comprend quand on sait que tous les accès à un tableau sont vérifiés par l'interpréteur. L'interpréteur est censé ajouter des instructions pour vérifier les indices, à savoir deux ou trois branchements. Avec CHKA, le test se fait en une seule instruction. D'autres modifications mineures de l'encodage des instructions sont aussi présentes, ainsi que des modifications pour les instructions LOAD/STORE. Précisément, dans les modes "base + indice" et "base + décalage", le décalage et l'indice subissent maintenant des décalages. L'idée est de simplifier le calcul d'adresse. De quoi économiser quelques instructions lors des calculs d'adresse, ce qui facilite le travail de l'interpréteur/JIT. Et des instructions LOAD/STORE précises voient leur comportement modifié pour faciliter la gestion de la pile de Java ou l'exécution des méthodes locales. Pour faciliter le travail de l'interpréteur, deux instructions HBP et HBLP ont été ajoutées. Elles branchent vers une fonction qui gère les exceptions logicielles (une fonctionnalité présente dans de nombreux langages, comme Java). La première effectue un branchement vers la fonction en question, la seconde sauvegarde l'adresse de retour avant de faire ce branchement. Les deux peuvent brancher vers 256 fonctions pré-déterminées, sans avoir à présenter leur adresse. ==La traduction binaire accélérée par le matériel== Plus haut, nous avons surtout parlé des compilateurs et des interpréteurs. Cependant, nous devons aussi parler de la '''traduction binaire'''. Elle traduit un programme écrit dans un code machine vers un autre code machine. Par exemple, elle traduit un programme compilé pour un CPU x86 vers un code machine ARM. En général, le système d'exploitation est généralement compilé pour le jeu d'instruction natif, mais il exécute des applications prévues pour le x86. Les applications sont traduites par le système d'exploitation, avant d'exécuter le code traduit. : Nous parlerons dans la suite de code machine source et de code machine cible, pour parler respectivement du code à traduire et du code obtenu après traduction. Idem avec d'autres termes comme architecture cible/source, ou jeu d'instruction cible/source. La traduction binaire est surtout utilisée pour des questions d'émulation ou de compatibilité. Par exemple, lorsque les Macintosh sont passés de processeurs Power PC vers des processeurs x86, le système d'exploitation Mac OS utilisait la traduction binaire pour convertir les anciennes applications Power PC vers du code x86. Les utilisateurs n'y ont vu que du feu. Le système de traduction binaire était appelé Rosetta 1. Par la suite, lors de la transition de processeurs x86 vers des processeurs Apple, Rosetta 2 a vu le jour. La traduction binaire a surtout été utilisée pour traduire du code x86 vers un autre jeu d'instruction. Il faut dire que le x86 est le jeu d'instruction dominant. De nombreuses entreprises ont eu pour ambition de briser l'hégémonie du x86 sur PC, en remplaçant le x86 par un jeu d'instruction plus performant, tout en gardant une compatibilité maximale. La traduction binaire était la seule solution pratique. Elle était le plus souvent intégralement réalisée en logiciel, comme c'était le cas sur les architectures Itanium avec le ''IA-32 Execution Layer''. ===Les généralités sur la traduction binaire assistée en matériel=== L'architecture source est presque tout le temps une architecture CISC, assez ancienne, qu'on souhaite émuler. Le choix de l'architecture cible se porte souvent sur une architecture VLIW, fort différente de l'architecture source, et ce pour obtenir une économie de matériel conséquente. Pour s'exécuter rapidement, le code traduit doit exploiter le parallélisme d'instruction, à savoir exécuter plusieurs instructions en même temps dans des unités de calcul séparées. Il est possible d'utiliser un processeur superscalaire avec exécution dans le désordre pour cela, mais au prix d'un cout important en transistors. Alors qu'en utilisant un CPU VLIW, c'est le traducteur binaire qui fait tout le travail d'extraction du parallélisme d'instruction. Et après tout, quitte à avoir un traducteur binaire, autant lui refiler le boulot d'optimisation. Compiler du code à la volée est certes assez lourd, mais qu'il existe des algorithmes efficaces pour regrouper des instructions indépendantes dans une seule instruction VLIW. Au passage, le processeur VLIW a plus de registres que le processeur source. Cela permet de faire du renommage de registres directement en logiciel. Le traducteur binaire n'hésite pas à changer les noms de registres entre instructions source et instruction VLIW cible, afin de supprimer des dépendances de données. En conséquence, cela ouvre des opportunités de parallélisme, qui sont exploitées lors du regroupement des opérations en instructions VLIW. Vous pourriez penser que le choix d'un CPU VLIW pour émuler du CISC est tout sauf optimal. Vous devez penser que la traduction binaire est d'autant plus simple que l'architecture source et cibles sont semblables. Dans les faits, ce n'est pas tellement traduire les instructions qui pose problème, mais plus la gestion du registre d'état, des exceptions matérielles, de la mémoire virtuelle, la différence entre gros-boutisme et petit-boutisme, de même que des différences d'adressage pour les périphériques. Le premier problème est la gestion des conditions, notamment en présence d'un registre d'état. En pratique, la traduction binaire s'utilise pour traduire du code CISC vers du code RISC ou VLIW. Les architectures source ont donc un registre d'état, qui est mis à jour non seulement par des instructions de test, mais aussi des instructions arithmétiques. L'architecture cible n'a elle pas de registre d'état, mais des registres à prédicats. La traduction de l'un vers l'autre est alors quelque peu compliquée. Et elle est d'autant plus compliquée que le registre d'état est la source de dépendances d'instruction implicites, qui réduisent les performances. Par exemple, si une instruction arithmétique modifie le registre d'état, cela peut impacter l'exécution d'une instruction ultérieure, qui lit ce registre d'état. Pour éliminer ces fausses dépendances, le traducteur binaire doit renommer ce registre en logiciel et détecter les dépendances utiles. Et le compilateur doit gérer les cas où il y a beaucoup de distance entre les deux, voire les cas où l'instruction dépendante n'a pas encore été analysée par le compilateur. Un autre problème est lié à la gestion des exceptions matérielles, et précisément des exceptions précises. Pour rappel, une instruction VLIW regroupe plusieurs opérations, opérations qui correspondraient à une instruction machine sur un CPU pas VLIW. En conséquence, plusieurs instructions du langage machine "source" sont regroupées en une seule instruction VLIW. Et il faut tenir compte du cas où une instruction source lève une exception. Dans le code VLIW, cela signifie qu'une opération lève une exception, et il faut annuler partiellement l'instruction VLIW associée. Et par partiellement, on veut dire que seules les opérations suivantes dans l'ordre du programme source doivent être annulées, puis ré-exécutées. Et c'est un sacré casse-tête ! Les processeurs VLIW utilisent comme solution le mécanisme d'exceptions différées des processeurs EPIC, vu il y a quelques chapitres. Pour résumer, le code est exécuté par blocs d'instruction, délimités par des branchements ou tout autre limite/barrière pertinente dans le code. Un bloc de code est compilé en deux versions : une version rapide sans exceptions matérielles, une version lente qui gère les exceptions précises. La version rapide s'exécute sans exécuter les exceptions. Cependant, les exceptions matérielles sont enregistrées, pour être prises en compte à la toute fin du bloc de code. La version rapide est exécutée en premier et elle mémorise si une exception matérielle a eu lieu. Puis une instruction vérifie si une exception a eu lieu et décide quoi faire. Si aucune exception n'a eu lieu, elle passe à la suite du programme, les résultats du bloc de code sont définitivement acceptés. Mais si une exception a eu lieu, tout est annulé. Le processeur est remis dans l'état initial, puis le code est ré-exécuté instruction par instruction de manière à gérer l'exception correctement. ===La traduction par pré-décodage=== Une première solution serait de faire la traduction binaire dans le cache d'instruction. Lors d'un défaut de cache, le code chargé depuis la RAM est traduit en code machine cible. Et c'est ce code machine cible qui est mémorisé dans le cache d'instruction et exécuté par le processeur. La technique marche sur le papier et n'est qu'une amélioration des techniques de pré-décodage vues il y a quelques chapitres. Cependant, le code obtenu est une traduction assez basique, qui n'incorpore pas d'optimisations dignes de ce nom. De plus, elle gère mal le cas où la taille du code source et cible sont potentiellement très différentes. Concrètement, elle est surtout utile pour traduire du code d'un processeur RISC vers un autre processeur RISC, dont les tailles d'instructions sont similaires. Elle ne permet pas d'émuler plusieurs jeux d'instructions différents, le cout en matériel (un circuit de pré-décodage par jeu d'instruction) serait trop important. ===Le projet DAISY d'IBM=== De très rares processeurs étaient conçus pour accélérer cette traduction binaire, afin de garder de bonnes performances. Les premiers à avoir étudié l'idée étaient IBM, avec leur projet DAISY (''Dynamically Architected Instruction Set from Yorktown''). Le projet de base était de convertir à la volée du code compilé pour des CPU Power PC, vers du code VLIW. Les chercheurs d'IBM avaient développé un algorithme de traduction dynamique efficace, ainsi qu'un processeur VLIW disposant d'optimisations spécifiques à la traduction binaire dynamique, à la volée. De nombreuses idées de ce projet ont été reprises ou re-découvertes par la société Transmetta, avec ses processeurs Crusoe et Efficieon, puis par NVIDIA avec son projet Denver. Les ingénieurs de ce projet ont étudié la possibilité d'émuler plusieurs jeux d'instructions différents sur un même processeur, notamment le s390 d'IBM et le x86. En théorie, cela demande juste d'avoir plusieurs programmes de traduction binaire : un pour le x86, un autre pour le s390, éventuellement un autre pour le Power PC. Mais cela ne s'est pas concrétisé. Mais les ingénieurs ont étudié quelle pourrait être l'architecture VLIW idéale pour ça. Par exemple, il fallait des additions 3-opérandes pour simplifier les calculs d'adresse. Une difficulté était la gestion du registre d'état, dont les bits ne sont pas mis à jour de la même manière sur le x86 et le s390 ou le Power PC. La différence entre gros-boutisme et petit-boutisme était aussi un problème, de même que des différences d'adressage pour les périphériques. Pour que la traduction binaire soit efficace, le processeur VLIW intègre diverses optimisations, comme les exceptions différées et les branchements multi-voies. La principale est la suivante : seul le code des boucles ou fonctions exécutées fréquemment est traduit en code VLIW, le reste du code est interprété. En effet, traduire du code binaire prend plus de temps qu'une simple interprétation. Pour du code qui ne sera exécutée qu'une seule fois, il est plus rapide d'utiliser l'interprétation. Par contre, pour du code exécuté beaucoup de fois, le cout de la traduction binaire est amorti, dilué sur N exécutions, compensé par le gain en temps d'exécution de ce code traduit. Au final, cela permet de ne traduire que le code qui le mérite. La détection du code fréquemment exécuté est réalisée dans le cœur VLIW. L'unité de branchement mémorise les derniers branchements rencontrés et le nombre de fois qu'ils ont été exécutés. Si ils ont été exécutés un certain nombre de fois, l'unité de branchement lève une exception matérielle, qui invoque le traducteur binaire. La routine de cette exception est le traducteur binaire proprement dit. En clair, la traduction binaire est démarrée quand le processeur détecte qu'une fonction a été exécutée plus de N fois, via une exception matérielle dédiée. La mesure du nombre d'exécution d'un branchement se fait dans l'unité de calcul dédiée aux branchements, avec l'aide d'une sorte de ''branch adress buffer'' modifié. Pour rappel, le ''branch adress buffer'' mémorise le ''Program counter'' de chaque branchement récemment rencontré. Ici, chaque entrée du ''branch adress buffer'' est associée à un compteur incrémenté à chaque exécution du branchement. ===Les processeurs Crusoe et Efficieon de Transmetta=== Les processeurs Crusoe Et Efficeron sont deux processeurs VLIW produits par la société Transmetta, une société californienne rachetée par NVIDIA. Ils étaient conçus pour exécuter spécifiquement des programmes x86, système d'exploitation inclus. L'idée derrière ce projet était d'exécuter du code x86 sans que les applications, ni même le système d'exploitation et le BIOS soient au courant ! Les processeurs Transmetta étaient en réalité conçus pour exécuter un programme unique, le ''Code Morphing Software'' (CMS), qui traduisait le code x86 en code VLIW. Transmetta ne voulait pas que son processeur VLIW soit exploité directement, sans le CMS. Il n'avait pas rendu public de compilateur pour traduire du C vers du code VLIW, il n'avait pas donné la documentation du jeu d'instruction VLIW. Des efforts de rétro-ingénieurie, documentés sur le site realworldtech, ont cependant permis de comprendre comment étaient encodées les instructions du processeur Crusoe. Comme pour le projet Daisy, le ''Code Morphing Software'' utilisait à la fois interprétation et traduction binaire, selon les besoins. Les instructions sont interprétées lors de leurs premières exécutions, mais le CMS bascule sur de la traduction en code VLIW après un certain nombre d'exécution. Ainsi, les boucles souvent exécutées sont traduites en code VLIW, alors que le reste du code est interprété. Le choix du code à traduire est le fait du CMS, il utilise des heuristiques complexes pour, qui ne sont pas connues dans le détail. Le ''Code Morphing Software'' est mémorisé dans une EEPROM, ce qui en fait un ''firmware'' situé en dessous du BIOS. Le CMS démarre ensuite le BIOS, qui lui-même démarre le système d'exploitation, qui lui-même démarre les pilotes de périphériques et les programmes. Le CMS se réserve les 16 premiers mébioctets de l'espace d'adressage. L'EEPROM du CMS est mappée dedans, mais n'en utilise que la moitié. Le reste est utilisé comme cache, pour mémoriser le code VLIW traduit par le CMS. Les deux processeurs intégraient aussi deux mémoires SRAM, utilisées par le CMS, appelées la ''local program memory'' (LPM) et la ''local data memory'' (LDM). La première contient du code qui gére les interruptions, la mémoire virtuelle, les exceptions matérielles, les problèmes d'alignement mémoire, et quelques fonctions très fréquemment utilisées par le CMS. La seconde est de la mémoire RAM utilisée par ces fonctions de la ''local program memory''. Les mémoires caches L1 et L2 sont séparées de ces deux mémoires. Le processeur Crusoe et Efficieon étaient des processeurs VLIW très simples. Ils n'avaient même pas de MMU, ni de fonctionnalités importantes sur les CPU x86. Pour Crusoe, les instructions VLIW étaient encodées sur 64 ou 128 bits, et regroupaient entre 2 à 4 opérations. Les instructions LOAD/STORE ne supportaient pas d'adressage indicé, juste de l'adressage indirect à registre. Crusoe avait 5 unités de calcul : deux ALU entières, une unité LOAD/STORE, une unité de branchement, une FPU. Efficieon doublait le nombre d'ALU entière et d'unité LOAD/STORE, les instructions VLIW passaient à 256 bits. Le processeur Crusoe contient 160 registres, dont 64 registres généraux, 32 registres flottants. Les registres généraux font 32 bits, ce qui est cohérent avec le fait que les CPU x86 de l'époque étaient des processeurs 32 bits. Quant aux registres flottants, ils faisaient 80 bits, ce qui colle avec la taille des registres flottants de la FPU x87 utilisée à l'époque. Sur les 64 registres généraux, seuls 48 étaient réellement utilisable pour mémoriser des opérandes. Une partie des registres généraux étaient utilisés pour la gestion de la pile, d'autres pour mémoriser l'état du CPU x86 émulé, un registre était un registre zéro non accesible en écriture. La gestion des exceptions est optimisée avec un système d'exceptions différées, le même que celui décrit plus haut. Le processeur utilise un système similaire, pour effectuer des lectures anticipées, qui a été expliqué dans le chapitre sur les processeurs VLIW/EPIC. Sauf qu'il s'agit là d'une optimisation pour gagner en performances en général, pas une aide utile pour gérer des exceptions précises ou toute autre subtilité de la traduction binaire. Pour gérer les exceptions différées et les lectures anticipées, le processeur mémorise l'état du processeur avant de démarrer un bloc de code. Pour cela, le processeur contient des ''shadow registers'', 48 registres entiers et 16 registres flottants. Vous remarquerez qu'il y a autant de registres entiers que de ''shadow registers'' entiers. Les registres entiers/flottants sont mémorisés dans les ''shadow registers'' avant d'exécuter un bloc de code, afin que le processeur puisse revenir à l'état de base. Une autre fonctionnalité liée est le ''gated store buffer''. Le principe est d'éviter toute écriture en mémoire RAM, tant que le code VLIW n'a pas émis d'instruction ''commit''. L'idée est que les écritures sont conservées dans la ''store queue'' de l'unité LOAD/STORE. Pour rappel, la ''store queue'' met en attente les écritures soit tant que les écritures ne sont pas terminées, soit tant que la RAM est occupée. L'idée est que les écritures sont accumulées dans la ''store queue'' tant que l'instruction ''commit'' n'a pas eu lieu. Le ''store queue'' est "déversé dans la RAM" seulement quand l'instruction ''commit'' s'exécute. ===Les CPU NVIDIA Denver=== Nvidia a tenté quelque chose de similaire avec son projet Denver. NVIDIA a racheté l'entreprise Transmetta, ses brevets, et a tenté de refaire la même chose. Le projet initial était de traduire du code x86 vers un jeu d'instruction VLIW propriétaire, comme l'a fait Transmetta. Mais NVIDIA n'a pas réussi à acquérir la licence du jeu d'instruction x86 et s'est rabattu sur le jeu d'instruction ARM. Le processeur né de ce projet est le Tegra K1-64 CPU. Il s'agit d'un processeur composé d'un cœur VLIW, couplé à deux décodeurs d'instructions ARM. Les deux décodeurs ARM traduisent une paire d'instructions ARM consécutives en instructions VLIW, pour les exécuter sur le cœur VLIW. Le CPU peut fonctionner selon deux modes : ARM et VLIW. En somme, le processeur supporte deux jeux d'instruction : le jeu d'instruction ARM, et un jeu d'instruction VLIW. En mode ARM, les instructions ARM sont chargées depuis le cache, traduites en VLIW par les décodeurs ARM, puis exécutées par le cœur VLIW. En mode optimisé, le CPU exécute des instructions VLIW chargées depuis le cache d'instruction, les décodeurs ne sont pas utilisés. : Le CPU supporte en réalité deux jeux d'instruction ARM : ARM8 et 7. Mais ils correspondent au même mode pour le CPU. Un point important est que le processeur peut changer très rapidement de mode, en à peine quelques cycles d'horloges. On n'est clairement pas dans le cas des CPU x86, qui mettent des plombes pour passer du mode 32 à 64 bits et inversement. La commutation est tellement rapide qu'on peut considérer que le processeur supporte deux jeux d'instruction simultanément. Et le support simultané de deux jeux d'instruction facilite l'implémentation de la traduction binaire. Encore une fois, inutile de traduire du code qui ne sera exécutée qu'une seule fois, mieux vaut l'exécuter directement dans le mode ARM. Surtout que le processeur supporte un mode ARM qui permet d'exécuter du code ARM sans perte de performance. Par contre, le code fréquemment utilisé, notamment dans des boucles critiques, est traduit en VLIW. Au final, les performances sont optimisées, en limitant le travail du logiciel traducteur binaire. La détection du code fréquemment exécuté se fait différemment que pour les processeurs Transmetta et Daisy. L'unité de branchement ne détecte pas directement le code beaucoup exécuté, même si elle a un rôle à jouer. A la place, la détection est réalisée en logiciel, par un ''thread'' dédié, qui s'exécute sur un cœur séparé. Il détecte le code fréquemment utilisé en regardent les branchements exécutés récemment. Pour cela, le processeur mémorise l'historique des branchements pris récemment et copie cette historique dans une mémoire tampon dédiée, partagée entre tous les cœurs. Vu qu'elle est partagée, le ''thread'' de détection a accès à l'historique sans pertes de performances, seule la copie de l'historique dans ce tampon a un cout en performance, pas son partage. Le code VLIW obtenu après traduction binaire est mémorisé dans la mémoire RAM, dans une portion spécialement réservée pour. Elle fait 128 mébioctets, et est appelée le '''cache d'optimisation''' par NVIDIA. Le cache d'optimisation est protégé en écriture et seul le traducteur binaire peut écrire dedans. Au passage, le traducteur binaire est du code VLIW, ce qui fait qu'il est placé dans le cache d'optimisation. Le processeur détecte automatiquement quand une fonction pour laquelle le code compilé équivalent est disponible. Pour cela, l'unité de chargement contient une table de correspondance entre l'adresse de la fonction ARM, et l'adresse de son équivalent compilé. Quand une fonction est appelée, l'unité de chargement regarde l'adresse de destination du branchement. Si l'adresse est dans cette table, elle récupère l'adresse de la fonction compilée et branche vers celle-ci. Le processeur dispose d'un cache d'instruction de 128 kibioctets, ce qui est très important, mais nécessaire vu la taille des instructions VLIW. Le coeur VLIW dispose de ses propres décodeurs, d'un ''scoreboard'', de 7 unités de calcul et de registres. Pour les unités de calcul, il a deux FPU, deux ALU, une unité de branchement, et deux unités LOAD-STORE faisant aussi ALU. Un point important est que le CPU dispose de 64 registres entiers et de 64 registres flottants. C'est deux fois plus que supporte le jeu d'instruction ARM. En mode ARM, seule la moitié des registres est utilisée. Mais en mode optimisé, le traducteur binaire utilise bien les 64 registres, grâce à une sorte de renommage de registres logicielle. Pour simplifier la traduction binaire, le processeur supporte les techniques vues précédemment. Le processeur intègre un ''gated store buffer'' similaire à celui des processeurs Transmetta. Les techniques d'exceptions différées sont aussi supportées, comme sur les processeurs Transmetta. Il a aussi une unité de préchargement décrite par NVIDIA comme agressive, avec support du préchargement de type anticipé (''runahead''). ===L'émulation de la mémoire virtuelle=== Pour finir, il faut parler de la mémoire virtuelle avec la traduction binaire. Sur le processeur source, le processeur gère à la fois des adresses physiques et virtuelles et sa MMU fait la conversion entre les deux. Sur le processeur VLIW, la MMU est simulée par le programme de traduction binaire, partiellement ou totalement. L'implémentation exacte varie suivant que l'on parle du projet Daisy, des processeurs Transmetta ou de Denver. Sur les processeurs Transmetta, il n'y a pas de MMU ni de mémoire virtuelle. A la place, le programme de traduction binaire émule la mémoire virtuelle du processeur source. Le processeur VLIW ne gère que des adresses physiques, rien d'autre. L'espace d'adressage physique du processeur VLIW a la même taille que celui du processeur émulé, ici des processeurs x86. Il est vraisemblable que les adresses physiques utilisées par le processeur x86 sont les mêmes que celles du processeur VLIW. Sur le projet Daisy, le processeur VLIW gère la mémoire virtuelle via pagination, comme les processeurs émulés. Ce qui fait qu'il gère un espace d'adressage virtuel et un espace d'adressage physique. Pour éviter toute confusion, nous parlerons d'adresse physique/virtuelle VLIW pour les adresses physiques/virtuelles du processeur VLIW, d'adresse virtuelle/physique source pour celle du jeu d'instruction traduit, à savoir du Power PC ou du s390, éventuellement de l'x86. Il faut alors faire le lien entre adresses physiques source et adresses virtuelles VLIW. Pour cela, rien de plus simple : il y a correspondance parfaite. L'adresse physique source numéro N correspond à l'adresse virtuelle VLIW numéro N. Cependant, cela signifie que tout l'espace d'adressage virtuel VLIW serait utilisé par le code à traduire. En réalité, il faut ajouter de la place pour le code traduit et le programme de traduction binaire. La conséquence est que l'espace d'adressage virtuel VLIW est plus large que l'espace d'adressage source. L'espace d'adressage virtuel VLIW est découpé en trois sections : une section pour le code Power PC à traduire, une autre pour le code traduit en VLIW, et une dernière réservée au traducteur binaire. Le programme de traduction est placé dans une ROM mappée en mémoire dans la seconde section. Le reste de la seconde section est réservé à une mémoire RAM utilisée par le programme de traduction. [[File:Mémoire virtuelle sur le projet Daisy.png|centre|vignette|upright=1.5|Mémoire virtuelle sur le projet Daisy]] Il faut noter que chaque page physique source, une fois traduite, correspond à N pages virtuelles VLIW. La raison est que le code traduit prend plus de place que le code machine originel. En conséquence, on doit utiliser une page finale plus grande. Pour le reste, des pages contiguës en mémoire physique source sont elles aussi contiguës en mémoire virtuelle VLIW. Le calcul d'adresse est donc simple : l'adresse physique source est multipliée par N, puis on ajoute l'adresse de base à laquelle commence la section pour le code traduit. ==La traduction binaire sur les Burroughs B1700== Nous venons de voir comment accélérer en matériel la traduction binaire, avec des techniques bien spécifiques. Le Burroughs B1700 a procédé autrement et il est intéressant d'étudier en détail son architecture. Son architecture est décrite dans le livre "Interpreting Machines : Architecture and Programming of the Bl700/Bl800 Series", écrit par Elliott I. Organick et James A. Hinds. Une partie de ce qui vfa suivre est un très court résumé de ce livre. : Dans ce qui suit, nous parlerons de traducteur binaire pour parler soit d'un interpréteur pour un langage de programmation de haut niveau, soit pour un logiciel de traduction binaire. Le Burroughs 1700 est un processeur 24 bits, qui a cependant la capacité d'émuler des architecture 8, 16 bits assez simplement, via divers mécanismes. Le Burroughs 1700 incorpore de nombreuses optimisations pour cela. Une partie d'entre elles permet de gérer des opérandes de taille différente de 24 bits, d'autres permettent de gérer des instructions de taille différente de 24 bits, d'autres servent pour les deux. Il incorpore aussi des instructions facilitant le découpage des instructions machines en ''opcode'', adresses et constantes immédiates. ===L'ALU de taille variable=== Les Burroughs B1700 avaient une '''ALU de taille variable''', ce qui veut dire qu'elle pouvait faire des calculs sur un nombre de bits compris entre 0 et 24. Pour cela, les calculs étaient fait avec une ALU de 24 bits, puis les bits de poids fort inutiles étaient masqués. L'ALU de 24 bits pouvait masquer les bits de poids fort du résultat, mais avec des limitations. Elle gérait des opérandes et résultats de 0, 4, 8, 12, 16, 20 et 24 bits. Pour avoir un réglage plus fin, une ALU 4 bits séparée permettait de corriger le résultat. Les registres X et Y étaient reliés à une ALU de 24 bits capable d'effectuer des opérations arithmétiques basiques. Fait étonnant, l'ALU faisait tous les calculs en même temps et fournissait ses résultats dans 7 registres : un registre SUM pour l'addition, un registre DIFFERENCE pour la soustraction, et un registre par opération bit à bit. Les registres pour les opérations bit à bit sont : CMPX et CMPY pour l'opération NOT sur chaque opérande, XANY pour le ET bit à bit, XEOY pour le XOR, XORY pour le OU logique. Les additions et soustractions pouvaient se faire sur des opérandes codés en binaire ou en BCD, suivant le code opération utilisé. A cela, il fallait ajouter deux registres de "résultat" MSKX et MSKY. Ils fournissaient un masque dépendant de la taille du résultat en bits. C'est ce registre qui permettait de gérer des données de 0, 4, 8, 12, 16, 20, et 24 bits, alors que l'ALU faisait 24 bits. Le masque dans ce registre pouvait servir d'opérande dans une opération de masquage ultérieure, pour corriger le résultat. L'ALU prenait en opérande les registres X et Y, mais aussi un registre CYF pour la retenue entrante. En sortie, il fournissait aussi deux retenue sortantes : une pour l'addition et une autre pour la soustraction. Elles étaient mémorisées dans les registres CYL et CYD, respectivement. L'ALU fournissait aussi 12 conditions, qui alimentait un registre d'état. Le registre d'état était segmenté en trois registres appelés XYST, XYCN et BICN. Les registres X et Y sont des registres pour les opérandes, mais il y a aussi deux autres registres de 24 bits. Il s'agit des registres T et L, qui sont des registres généraux. Les registres T et L étaient découpés en 6 sous-registres de 4 bits, nommés LA LB LC LD LE et LF pour le registre L, et TA TB TC TD TE et TF pour le registre T. Ils étaient adressables comme n'importe quel registre. Le processeur contenait de nombreux autres registres de 4 bits, dont les sous-registres de T et L. En tout, il y a 27 registres de 4 bits adressables ! Et le processeur disposait d'une ALU 4 bits pour manipuler ces registres de 4 bits. L'ALU 4 bits est capable de faire des MOV entre registres de 4 bits, des opérations bit à bit, et peut aussi tester si un bit vaut 0 ou 1. La dernière possibilité est très utile pour implémenter les branchements. De plus, cela permettait de faire des opérations de masquage, notamment pour appliquer un masquage supplémentaire à celui de l'ALU. Par exemple, pour gérer des opérations sur 14 bits, on effectue une opération sur 16 bits avec l'ALU, et on masque les 2 bits manquants avec l'ALU 4 bits. ===Une architecture bit-adressable=== Le CPU Burroughs B1700 était '''bit-adressable'''. En clair, il pouvait adresser la mémoire bit par bit, si nécessaire. Un avantage est que cela facilitait la traduction binaire des instructions machine, mais nous détaillerons cela plus bas. Un autre avantage est que cela permettait d'émuler des architectures dont la taille des registres/''byte''/mots était très différente. Et cet avantage mérite quelques explications immédiates. Le processeur était un CPU 24 bits, mais il pouvait émuler des processeurs 16 bits, 8 bits, 4 bits, 1 bit, 9 bits, ou toute autre valeur. Pour cela, le processeur lisait des mots de 24 bits et utilisant un ''barrel shifter'' pour sélectionner les bits adéquats. Par exemple, pour un accès de 16 bits, la donnée lue en mémoire était masquée de manière à ne garder que les 16 bits de poids faible. Le ''barrel shifter''/circuit de masquage était placé directement avant le bus mémoire, sur le trajet des données/instructions. Le ''barrel shifter'' était commandé par un registre ''Field Unit'', qui précisait quelle était la taille des données à charger. Pour communiquer avec la mémoire, il avait trois registres séparés : READ, WRITE, MAR. READ contient la donnée lue lors d'une lecture, WRITE est pour une donnée à écrire lors d’une écriture, MAR contient l'adresse à lire ou en cours de lecture. Il s'agit de registres d’interfaçage mémoire. Le fait que ces registres d’interfaçage mémoire et IO soit adressable est peu commun, et c'est plus quelque chose qu'on attend d'un microcode que d'instructions machines. Mais il faut bien comprendre que les registres d’interfaçage mémoire ont été promus au rang de registres architecturaux, sur ce processeur. De même, pour communiquer avec les entrées-sorties, il disposait de deux registres CMND et DATA : CMND pour envoyer une commande à une entrée-sortie, DATA pour lire ou écrire une donnée. Il y avait aussi un registre U, qui mémorisait une donnée lue depuis le lecteur de cassette. ===Le code machine du Burroughs B1700=== Le processeur utilise des instructions de 16 bits de long, qui sont copiées dans registre d'instruction M de 16 bits. Le ''program counter'' est appelé le registre A. Les instructions du processeur font 16 bits de long, et elles sont alignées en RAM sur 16 bits, ce qui fait que les 4 bits de poids fort de ce registre valent 0, seuls les 14 bits de poids fort sont utiles. Les adresses mentionnées plus haut, qui adressent les micro-opérations en mémoire RAM, sont des adresses de mots de 16 bits, pas les adresses de bit individuels. Fait intéressant, il était possible de faire un OU logique entre l'instruction lue et un registre du processeur, le résultat étant mémorisé dans le registre M. Cela permettait de faire du code automodifiant, pour émuler des modes d'adressage complexe. Cela permettait par exemple de remplacer une adresse dans une micro-opération. L'adresse est alors surimposée avec un OU dans le champ d'adresse, alors que le reste de l'instruction n'est pas modifié. Afin de gérer les interruptions, ces dernières étaient partiellement détectées en logiciel. Une interruption doit être prise en compte à la toute fin de l'exécution d'une instruction source, quand celle-ci a finit son travail. Mais vu qu'ici, les instructions sont émulées avec des séries d'instructions cibles, il y a un problème : il est possible d'interrompre une instruction émulée en plein milieu. Pour éviter cela, le processeur ne déclenche pas les interruptions matérielles immédiatement, elles sont mises en attente. Une instruction dédiée vérifie si une interruption est en attente, et l'exécute alors cas échéant. L'instruction en question est utilisée en dernier, dans la suite d'instruction qui émule l'instruction source. Les Burroughs B1700 intégrait une pile d'adresse de retour, capable de mémoriser 16 adresses de 24 bits. Il disposait de 4 registres généraux, nommés X, Y, T et L, tous de 24 bits. Un point important est que le registre T avait des possibilités d’extraction spécifiques. Il était possible d'extraire N bits, placés n'importe où dans ce registre, et de les copier dans un autre registre. Le CPU supportait une '''instruction EXTRACT''' pour ça. Nous verrons comment cette possibilité est exploitée pour la traduction binaire dans ce qui suit. ===La traduction binaire sur le Burroughs B1700=== Les instructions source peuvent faire 16 bit avec tel jeu d'instruction, 8 bits avec un autre, 32 bits sur un autre, etc. Elles peuvent aussi être de longueur fixe ou variable ! Le B1700 utilisait une architecture bit-adressable pour gérer ces contraintes. En effet, avec un CPU bit-adressable, les contraintes d'alignement des instructions disparaissent ! Dans ce qui suit, on suppose que l'instruction est composée de '''champs''' : l'opcode, les opérandes, les adresses absolues, les constantes immédiates, les numéros de registre, etc. L'idée est que l'instruction source est chargée dans le registre T, puis découpée en champs, qui sont copiés dans les registres du processeur ou envoyés au microcode. Je rappelle que le registre T est relié à un ''barrel shifter'', différent au précédent, qui permettait de masquer, décaler et extraire une donnée de ce registre. Et il y a la possibilité d'extraire une suite de bit de ce registre T, pour la copier dans un autre registre. Par exemple, prenons une instruction composée d'un opcode, d'une constante immédiate et d'une adresse immédiate. Premièrement, elle est chargée dans le registre T. Deuxièmement, la constante immédiate est extraite du registre T et copiée dans le registre X. Troisièmement, l'adresse est extraite et est copiée dans le registre d’interfaçage mémoire, une lecture est démarrée, et l'opérande lue est copiée dans le registre Y. Enfin, l'instruction équivalente à l'opcode est exécutée par le microcode. Le tout prend plusieurs instructions : une première pour lire l'opcode, une seconde pour lire la première opérande, une troisième pour lire la seconde opérande, una quatrième pour tout exécuter. Notons que suivant la taille des champs, on peut en charger plusieurs à la fois. Par exemple, imaginons qu'on ait chargé 24 bits, contenant un opcode de 8 bits et une constante immédiate de 16 bits : le registre T est découpé en un opcode et une constante, avec deux instructions EXTRACT. L'architecture est bit-adressable car elle gère des champs qui ne font pas forcément 8 ou 16 bits. Par exemple, certains jeux d'instructions ont des opcodes codés sur 6 ou 7 bits, voire 9-10 bits. Utiliser une architecture bit-adressable règle ce problème d'alignement. On peut lire un champ depuis la mémoire RAM en précisant l'adresse de son premier bit, pas besoin de gérer des accès non-alignés. Par exemple, prenons une instruction source avec un opcode de 7 bits, suivi par une adresse de 12 bits et une constante immédiate de 15 bits. On peut charger 7 + 12 bits en un premier accès, pour extraite l'opcode et l'adresse, suivi par un second accès de 15 bits pour extraire la constante immédiate. Pour charger un champ, il faut connaitre plusieurs informations : l'adresse de son premier bit, sa taille. Pour cela, le B1700 contient deux registres, nommés ''Field Adress'' (FA) et ''Field Length'' (FL), qui indiquent l'adresse du premier bit et la taille de l'instruction à charger. Le premier fait 24 bits, ce qui permet de gérer des adresses de 24 bits. N'oubliez pas que le processeur est bit-adressable, ce qui permet d'adresser 16 Mébi-bits, soit 2 mébioctets. Le registre pour la taille fait lui 16 bits, ce qui permet de gérer des instructions de 65536 bits, soit 8192 octets ! Leur nom commence par ''field'', car le processeur peut gérer des instructions de taille variable et/ou plus longues que 24 bits. Dans ce cas, le chargement de l'instruction se fait champ par champ. Pour donner un exemple, prenons une instruction composée de 20 champs de 12 bits chacun. Les champs sont chargés un par un dans le registre T. Le registre FA est alors initialisé avec l'adresse de l'instruction, il est incrémenté de 12 à chaque cycle. Le registre FL est initialisé avec la taille de l'instruction, soit 20 * 12 = 240 bits, et il est décrémenté de 16 à chaque cycle. Précisons cependant que la taille d'un champ peut être changée d'un cycle à l'autre. Par exemple, le processeur peut charger un opcode de 8 bits, l'interpréter, détecter que l'instruction demande ensuite de charger une adresse de 16 bits. Dans ce cas, le registre FL est immédiatement altéré pour charger 16 bits, au lieu des 8 bits de l'opcode immédiatement précédent. ===Le Burroughs B1726 : le mal-nommé ''split-level control store''=== Le Burroughs B1700 avait plusieurs modèles : le B1726, le B1710 et le B1800. * Le B1710 plaquait le traducteur binaire en mémoire RAM. * Le B1800 avait ajouté un cache dédié au code du traducteur binaire, afin d'améliorer les performances. * Le B1726 utilisait un ''local store'' dédié au traducteur binaire. Le B1726 peut exécuter le traducteur binaire soit depuis le ''local store'', soit depuis la mémoire RAM avec une pénalité en termes de performances. La documentation appelle cela un '''''split-level control store''''', mais le terme est trompeur : la documentation qualifie de microcode le langage machine du Burroughs B1700, ce qui est à l'origine de beaucoup de confusions. Une autre source de confusion est que ce langage machine a quelques propriétés qu'on pourrait attendre d'un microcode, mais certaines sont manquantes. Par exemple, il peut adresser les registres d’interfaçage mémoire, mais c'est parce que ce sont des registres architecturaux sur ce processeur. Ses instructions n'encodent pas de signaux de commande, par contre, elles font 16 bits et adressent les opérandes de manière implicite. Le ''local store'' mémorisait au maximum 2048 instructions machine, ce qui fait que les 2048 premières "adresses" correspondaient au ''local store''. Les suivantes étaient des adresses de mémoire RAM qui contenaient le reste du traducteur binaire. En pratique, l'adresse qui séparait les deux était configurable ! On pouvait limiter la taille du ''local store'' à un multiple de 32 instructions ! Les adresses restantes étaient alors réattribuées à la RAM. Par exemple, on pouvait attribuer 64 adresses au ''local store'', les 2048 - 64 adresses restantes étaient alors attribuées à la mémoire RAM. Un registre limite, nommé TDPM, indiquait quelle était l'adresse de démarcation entre les deux. Le programmeur devait découper le traducteur binaire en segments, certains étant placés dans le ''local store'', les autres étant en mémoire RAM. Mieux que ça, le processeur autorisait d'utiliser la technique de l'''overlaying'' pour le microcode ! Il y avait même une fonction OVERLAY dédiée pour ! OVERLAY avait besoin de trois opérandes : la taille du segment à copier, son adresse source en mémoire RAM, son adresse de destination dans la micro-SRAM. L'interpréteur réservait un "segment" en mémoire RAM, pour les données, dont le processeur connaissait la position. En dehors de ce segment, il n'y a que des instructions machines à traduire, et le traducteur binaire. Le CPU incorporait pour cela deux registres, un pour l'adresse de base du segment de données, un autre pour son adresse de fin. Il incorporait une protection mémoire limitée sur ce segment, à savoir que seul l'interpréteur pouvait lire ou écrire dans ce segment. ==Les jeux d'instructions dédiés à un langage de programmation== De rares processeurs sont conçus pour un langage de programmation en particulier. On appelle ces processeurs, conçus pour des besoins particuliers, des '''processeurs dédiés'''. Par exemple, les fameux ''Burrough E-mode'' B5000/B6000/B7000 étaient spécialement conçus pour exécuter de l'ALGOL-60. Leurs cousins B2000/B3000/B4000 étaient eux conçus pour le COBOL. Des langages fonctionnels ont aussi eu droit à leurs processeurs dédiés. Le prolog en est un bel exemple, avec les superordinateurs de 5ème génération qui lui étaient dédié. On peut aussi citer les machines LISP, dédiés au langage LISP, qui datent des années 1970. Elles étaient capables d’exécuter certaines fonctions de base du langage directement dans leurs circuits : elles possédaient notamment un ''garbage collector'' câblé dans ses circuits ainsi que des instructions machines supportant un typage déterminé à l’exécution. Les processeurs dédiés ont eu leur heure de gloire au début de l'informatique, à une époque où les langages de haut niveau venaient d'être inventés. À cette époque, les compilateurs n'étaient pas performants et ne savaient pas bien optimiser le code machine. Il était alors rationnel, pour l'époque, de rapprocher le code machine cible et le langage de programmation de haut niveau. De nombreuses architectures dédiés ont ainsi été inventées, avant que les concepteurs se rendent compte des défauts de cette approche. Les défauts en question ne sont pas nombreux, mais assez simples à comprendre. Premièrement, elles sont très rapides pour un langage de programmation en particulier, mais sont assez mauvaises pour les autres, d'où un problème de "compatibilité". Ajoutons à cela que les langages de programmation peuvent évoluer, devenir de moins en moins populaires/utilisés, ce qui rend la création d'architectures généralistes plus pertinente. Enfin, les architectures dédiées sont évidemment des processeurs CISC, pour implémenter les nombreuses fonctionnalités des langages évolués. Et les défauts des CISC sont assez rédhibitoires à l'heure actuelle. À l'heure actuelle, les algorithmes des compilateurs se sont améliorés et savent nettement mieux utiliser le matériel. Ils produisent du code machine efficace, ce qui rend les architecture dédiées bien moins intéressantes. Si on ajoute les défauts de ces architectures dédiées, par étonnant que les architectures dédiées aient presque disparues. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'accélération matérielle de la virtualisation | prevText=L'accélération matérielle de la virtualisation | next=Le matériel réseau | nextText=Le matériel réseau }} </noinclude> p86g09dwhfctzqvvblv0mhome15qovt 764066 764065 2026-04-19T21:38:49Z Mewtow 31375 /* Une architecture bit-adressable */ 764066 wikitext text/x-wiki De nos jours, la majorité des programmeurs programment dans des langages de haut niveau. Il est très rare pour eux d'avoir à utiliser de l'assembleur, et encore moins un langage machine. Les programmes écrits dans un langage de haut niveau sont traduits en langage machine par un logiciel appelé le compilateur. [[File:Compilation.PNG|centre|vignette|upright=2.5|Compilateur : principe.]] Il se trouve que quelques rares processeurs sont conçus pour faciliter le travail du compilateur. Et ce sont ces processeurs que nous allons voir dans ce qui suit. Ils représentent une catégorie de jeux d'instruction à part, qui n'a pas vraiment de nom. ==La compilation et l’interprétation== Un compilateur traduit donc du code source, écrit dans un langage de haut niveau, vers du langage machine. La traduction est rarement directe. En général, le compilateur traduit le code source en assembleur, qui est lui-même traduit en langage machine. L'assembleur est une représentation textuelle du langage machine alors que le code machine est du binaire exécutable par le processeur. La traduction se fait donc en deux étapes, la '''compilation''' proprement dite et l''''assemblage''', réalisées respectivement par un compilateur et un assembleur. Il faut parfois rajouter une troisième phase d'édition des liens, que nous passons volontairement sous silence. {| |[[File:Code C.png|vignette|200px|Le code source, ici en C.]] |[[File:Assem.png|vignette|250px|Le code assembleur.]] |[[File:Binary file - hello world (C programming).png|vignette|150px|Le code machine.]] |} ===L'interprétation et la compilation à la volée=== Une alternative à la compilation est l''''interprétation''', qui transforme le code source en code machine à la volée. Avec l'interprétation, le code source est passé à un logiciel appelé l''''interpréteur''', qui exécute le code source ligne de code par ligne de code. Une ligne de code est traduite en code machine, qui est exécutée, puis l'interpréteur passe à la ligne suivante. [[File:Kaantotulkkaus.png|centre|vignette|upright=3|Compilation versus interprétation.]] Un défaut de l'interprétation est son cout en performance, qui est assez important. Aussi, l'interprétation stricte a évolué vers un hybride entre interprétation et compilation. L'idée est qu'une partie peu importante du code source est interprétée, alors que le code important est compilé juste avant d'être exécuté. Pour le dire autrement, le code source est partiellement compilé à la volée, juste avant son exécution. Aussi, on parle de '''compilation à la volée'''. Le terme en anglais est ''Just In Time compilation'', abrévié en JIT. En soi, le JIT est associé à de l'interprétation, les JIT sont en réalité des hybrides interpréteurs-compilateurs. Un point important est que compiler du code à la volée est assez lourd, cela a un cout en performance. Par contre, le code compilé s'exécute plus vite que du code interprété. Si on exécute le code compilé une seule fois, le cout de la compilation l'emporte sur le gain à l'exécution. Mais le code compilé est réutilisé autant de fois que nécessaire. Le gain à l'exécution est donc multiplié par le nombre d'exécutions, alors que le cout de la compilation est répartit sur plusieurs exécutions, il est amorti. Les portions du code sont choisies de manière à avoir un gain en performance maximal. L'idée générale est que le code exécuté très souvent est compilé, alors que du code exécuté pas souvent est interprété. Typiquement, les boucles les plus souvent exécutées sont compilées, le code exécuté une seule fois est interprété. ===Les langages intermédiaires=== Les compilateurs modernes passent par un '''langage intermédiaire''' pour faire la transformation en code machine. Le compilateur traduit le langage de haut niveau en langage intermédiaire, puis traduit le langage intermédiaire en code machine cible. Le langage intermédiaire est parfois appelé le '''''bytecode''''', et ce terme recouvre aussi le code écrit avec du ''bytecode''. L'interprétation aussi peut passer par l'intermédiaire d'un un ''bytecode'', ce n'est pas limité aux compilateurs proprement dit. {| |[[File:Intermediate representation scheme.png|centre|vignette|upright=1.5|Représentation intermédiaire d'un compilateur.]] |[[File:Code interpreter scheme.png|centre|vignette|upright=1.5|Fonctionnement d'un interpréteur.]] |} Faire ainsi a de nombreux avantages, le principal étant d'avoir un compilateur capable de traduire un langage de haut niveau vers plusieurs jeux d'instructions différents. Cela permet d'avoir, par exemple, un compilateur qui traduit du C soit en code machine x86, soit en code machine pour un CPU ARM, soit pour un CPU POWERPC, etc. [[File:Compiler design.svg|centre|vignette|upright=3|Compiler design]] Pour l'interprétation, l'usage d'un ''bytecode'' a d'autres avantages. Pour l'exploiter, il faut distribuer non pas le code source, mais le ''bytecode''. En clair, l'interpréteur ne prend pas en entrée le code source, mais du ''bytecode'' déjà compilé. La traduction est alors beaucoup plus simple, car le code source a déjà été partiellement compilé, par la traduction en ''bytecode''. Interpréter du code source est en effet assez compliqué : il faut effectuer des étapes d'analyse lexicale, sémantique, et bien d'autres. Avec du ''bytecode'', ces étapes ont été réalisées lors de la compilation du ''bytecode'', l'interpréteur a alors peut de choses à faire. L'interprétation du ''bytecode'' se fait instruction par instruction, au niveau du ''bytecode''. Une instruction du ''bytecode'' est traduite en une instruction machine équivalente, qui est exécutée. Si l'instruction est un peu complexe, il exécute une fonction/procédure qui fait la même chose. Pour résumer, le ''bytecode'' est ensuite traduit à la volée et exécuté instruction par instruction par un logiciel appelé l'interpréteur. [[File:Control table.png|centre|vignette|upright=2|Control table]] Le langage intermédiaire peut être vu comme l'assembleur d'un processeur, n'existe pas forcément dans la réalité, mais dont le jeu d'instruction est décrit en détail. Le processeur en question est appelé une '''machine abstraite''', ou encore une ''machine virtuelle''. Nous utiliserons le terme de machine abstraite dans ce qui suit. La machine abstraite n'est pas la même suivant que l'on cible la compilation ou l'interprétation/JIT. Pour l'interprétation/JIT, la machine abstraite est souvent une machine à pile, car cela simplifie grandement la traduction en code machine final. L'interprétation demande que la traduction du ''bytecode'' en code machine soit la plus simple possible. Et cela demande de prendre en compte pas mal de détails. Par exemple, les différents jeux d'instruction existants n'ont pas le même nombre de registres, ce qui pose problème lors de la traduction du ''bytecode''. Lors de la transformation en code machine, un algorithme d'allocation de registres se débrouille pour traduire le code intermédiaire en code qui utilise un nombre limité de registres. Pour améliorer cette allocation de registres, il y a deux solutions niveau ''bytecode'' : soit utiliser un nombre illimité de registres, soit utiliser une machine à pile. Pour les machines à pile, il existe un algorithme simple et rapide pour traduire un code écrit pour une machine à pile en un code écrit pour un processeur avec des registres, qui se débrouille pas trop mal pour allouer efficacement les registres. C'est un avantage assez important pour les langages interprétés. ==Les processeurs qui exécutent du bytecode== Le ''bytecode'' est un code machine, ce qui signifie qu'il peut en théorie s'exécuter sur un processeur qui implémente le jeu d’instruction associé. Si le jeu d'instruction d'un ''bytecode'' est souvent une description censée être fictive, elle n'en reste pas moins un jeu d'instruction et des caractéristiques précises, qu'on peut l'implémenter en matériel ! Néanmoins, tous les ''bytecode'' ne sont pas égaux de ce point de vue. Certains sont faciles à implémenter en matériel, d'autres non. Les ''bytecodes'' implémentés en matériel sont ceux dont les machines abstraites sont des machines à pile. Un premier exemple est celui des processeurs Pascal MicroEngine, qui exécutaient directement le bytecode du langage Pascal, le fameux UCSD P-code. Un second exemple est la '''machine SECD''', qui sert de langage intermédiaire pour certains compilateurs de langages fonctionnels. Elle a été implémentée en matériel par plusieurs équipes, notamment par les chercheurs de l'université de Calgary en 1989. Dans le même genre, quelques processeurs simples étaient capables d’exécuter directement le ''bytecode'' du langage FORTH. Le FORTH, un des premiers langages à pile de haut niveau, possède de nombreuses implémentations hardware et est un des rares langages de haut niveau dont le ''bytecode'' a été utilisé comme langage machine sur certains processeurs. Par exemple, on peut citer le processeur FC16, capable d’exécuter nativement du ''bytecode'' FORTH. ===Les processeurs Java=== Le cas le plus impressionnant est celui de la machine virtuelle Java, qui est un design de processeur comme un autre. En temps normal, le ''bytecode'' Java est compilé ou interprété, mais certains processeurs exécutaient du ''bytecode'' Java directement, sans interprétation ni compilation. Ils sont appelés des '''processeurs Java'''. On peut citer les processeurs ARM disposant de l'extension Jazelle, du JEMCore et du aJ-100 de aJile Systems, le picoJava II de Sun, et quelques autres. Il y a aussi eu quelques projets de recherche ou processeurs open source, comme Komodo, jamuth, le ''Java Optimized Processor'' et quelques autres projets du même genre. L'implémentation de la machine virtuelle Java n'était cependant pas complète. Quelques instructions complexes n'étaient pas gérées par le processeur et devaient être émulées en logiciel. Mais la grosse majorité des instructions l'était. Les instructions non-supportées étaient émulées avec la méthode ''trap and emulate'', à savoir qu'un opcode inconnu déclenchait une exception matérielle ''opcode invalide'', dont la routine pouvait émuler les instructions qui doivent l'être. Le processeur aJ-100 de aJile est capable d'exécuter la quasi-totalité des instructions de la JVM Java, seules deux instructions faisant exception. Les '''accélérateurs Java''' sont une solution intermédiaire, qui permet de traduire à la volée du ''bytecode'' Java en instructions RISC, exécutées sur un autre processeur. Il s'agit formellement de traduction binaire, mais je préfère en parler ici. L'idée est de combiner un processeur RISC avec un coprocesseur Java. Le coprocesseur lit le ''bytecode'' Java et le traduit en instructions RISC, ces dernières étant exécutées sur le processeur RISC. Le JA108 de Nozomi était un coprocesseur de ce genre, au même titre que le coprocesseur JSTAR de JEDI. L'extension Jazelle de certains CPU ARM fonctionnait vraisemblablement sur un principe similaire, sauf que le coprocesseur était intégré dans le processeur ARM, entre le cache d'instruction et le décodeur d'instructions ARM. L'intérêt des processeurs Java n'est pas qu'une question de performance. Certes, ils permettent d’exécuter plus vite les programmes compilés en ''bytecode'', comme des programmes Java pour la JVM Java. Mais l'intérêt est aussi de faire des économies de mémoire RAM et ROM. Je rappelle que la machine virtuelle Java est une machine à pile, ce qui fait que sa densité de code est excellente. Les programmes en ''bytecode'' Java sont donc très petits, et prennent peu de place en mémoire ROM. De plus, utiliser un processeur Java permet de se passer d'interpréteur ou de JIT Java, ce qui économise encore plus de mémoire. ===Transformer un processeur RISC en machine à pile=== La plupart des processeurs Java utilisent en interne une architecture à registre généraux, qui émule une machine à registre en microcode. Quelques registres sont réservés pour l'état de la machine virtuelle, avec au minimum un registre réservé pour le pointeur de pile, un autre pour un pointeur vers un ''pool'' de constantes, et quelques autres. D'autres registres servent à mémoriser le haut de la pile, à savoir le sommet de la pile et quelques opérandes situées juste en-dessous. Par exemple, le processeur picoJava II gardait les 64 opérandes au sommet de la pile dans 64 registres généraux, séparés des registres pour les pointeurs et l'état de la JVM. Une optimisation possible, envisagée sur les processeurs picoJava, était une sorte de macro-fusion sous stéroïde. L'idée était de fusionner certaines séries d'instructions Java en une seule instruction machine. Typiquement, pour faire une opération arithmétique, une machine à pile empile deux opérandes et exécute une opération, ce qui prend trois instructions machine. La spécification picoJava 1 a proposé d'envoyer directement la seconde opérande en entrée de l'ALU, ce qui fusionnait l'empilement de la seconde opérande et l'opération arithmétique. Le processeur picoJava II va plus loin et fusionne les trois instructions en une seule instruction RISC, agissant sur des registres. Mais la majorité des processeurs Java ne faisaient pas cela, car cela complexifie grandement le décodeur. Les processeurs ''bigfoot'' de DCT utilisaient un système différent pour émuler une pile avec des registres. Leur banc de registre de 64 bits était coupé en deux : les 16 premiers registres étaient utilisés pour la pile, les 48 suivants étaient utilisés pour autre chose. L'idée est que le registre 0 mémorise un emplacement vide, le registre 1 contient le sommet de la pile, le registre 2 l'opérande sous le sommet de la pile, et ainsi de suite. Les 15 premières opérandes de la pile sont donc mémorisées dans les registres. Les instructions écrivent leur résultat dans le registre adéquat, puis tous les registres sont décalés pour que le résultat passe dans le registre R1. Par exemple, les instructions PUSH chargent une opérande dans le registre 0, puis tous les registres sont décalés de un rang, pour que l'opérande chargée soit dans le registre 1. Le registre 0 devient alors le registre numéro 1, le registre 1 devient le 2, et ainsi de suite. Par contre, les opérations arithmétiques font autrement. Une instruction d'addition, par exemple, lit les opérandes dans le registre 1 et 2, puis place son résultat dans le registre 2 (les deux opérandes sont dépilées). Puis, les registres sont décalés d'un rang pour que ce registre 2 devienne le 1. Les registres ne sont en réalité pas décalés, il n'y a pas de transferts entre registres. A la place, les 16 premiers registres utilisaient une forme limitée de renommage de registres, similaire au banc de registre tournant utilisé sur les processeurs EPIC comme l'Itanium. Les numéros de registres sont renommés, décalés, à chaque fois qu'on empile ou dépile une opérande. Pour faire le renommage de registres, processeur contient un ''register stack counter'' de 4 bits, qui est incrémenté à chaque fois qu'on empile une donnée, et est décrémenté quand on dépile une donnée. Quand on veut accéder à un opérande, le numéro de registre architectural associé est traduit en numéro de registre physique, en additionnant le ''register stack counter'' (modulo 16, pour rester sur 4 bits). ==L'accélération de l'interprétation/JIT : le ''Thumb-EE'' d'ARM== Quelques processeurs ont ajouté des instructions pour faciliter le travail des interpréteurs/JIT. Par exemple, en 2005, ARM a ajouté le mode ''Thumb Execution Environment'', qui faisait cela. Il reprenait le jeu d'instruction compact ''thumb'' et ajoutait quelques instructions et modifiait le comportement d'instructions existantes. Le jeu d'instruction ''Thumb-EE'' a été ajouté sur les CPU ARM en 2005, mais a été déprécié en 2011, par manque d'utilité. Mais la tentative mérite qu'on s'attarde dessus. Les CPU de gamme M, déstinés à l'embarqué, n'ont jamais supporté ''Thumb-EE'', vu que de tels processeurs ne sont pas conçus pour exécuter du code interprété/JIT. Le mode ''Thumb-EE'' est un mode d'exécution, séparé de l'ARM normal. Ainsi, une instruction ''Thumb-EE'' et une instruction ARM peuvent avoir le même encodage en binaire, mais se comporter différemment. Le processeur est à tout moment soit en mode ''Thumb-EE'', soit en mode ARM normal, ce qui précise comment ces instructions doivent se comporter. Des instructions ont été ajoutées par ''Thumb-EE'', pour qu'il fasse son travail. L'entrée et la sortie du mode ''Thumb-EE'' se fait avec deux instructions : ENTERX et LEAVEX. Une fois en mode ''Thumb-EE'', le décodage des instructions ''thumb'' se fait avec les règles du ''Thumb-EE'', et non celles du ''thumb'' normal. Une première différence entre ''thumb'' normal et ''Thumb-EE'' est l'ajout de ce qui s'appelle un ''null check'' pour les instructions mémoire. Le ''null check'' est utilisé pour les instructions en mode d'adressage "base + indice" ou "Base + décalage", ou tout autre mode d'adressage avec un registre de base utilisés dans des calculs d'adresse. L'idée est de vérifier si l'adresse de base vaut zéro ou non. Si c'est le cas, le processeur lève une exception matérielle, qui est traitée par l'interprétation ou le compilateur JIT. Une telle situation est en effet signe d'une erreur d'adressage mémoire, qui doit être traitée par l'interpréteur. Typiquement, c'est signe qu'un pointeur n'a pas bien été initialisé, ce qui peut arriver avec du code interprété ou JIT. Les interpréteurs/JIT traitent généralement la situation en ajoutant un test avant toute instruction mémoire, pour vérifier si le pointeur accédé vaut zéro ou non. Avec ''Thumb-EE'', pas besoin d'ajouter les tests en questions : ils sont réalisés automatiquement lors de chaque instruction mémoire. : Les registres pour la pile, comme le pointeur de pile et le pointeur de ''frame'', sont aussi concernés. L'instruction CHKA vérifie que les accès à un tableau ne débordent pas en dehors du tableau en question. Par exemple, pour un tableau de 1024 éléments, elle vérifie si l'indice est compris entre 0 et 1024. Pour cela, elle vérifie si l'indice de tableau est bien dans l'intervalle adéquat et lève une exception matérielle si ce n'est pas le cas. Son utilité se comprend quand on sait que tous les accès à un tableau sont vérifiés par l'interpréteur. L'interpréteur est censé ajouter des instructions pour vérifier les indices, à savoir deux ou trois branchements. Avec CHKA, le test se fait en une seule instruction. D'autres modifications mineures de l'encodage des instructions sont aussi présentes, ainsi que des modifications pour les instructions LOAD/STORE. Précisément, dans les modes "base + indice" et "base + décalage", le décalage et l'indice subissent maintenant des décalages. L'idée est de simplifier le calcul d'adresse. De quoi économiser quelques instructions lors des calculs d'adresse, ce qui facilite le travail de l'interpréteur/JIT. Et des instructions LOAD/STORE précises voient leur comportement modifié pour faciliter la gestion de la pile de Java ou l'exécution des méthodes locales. Pour faciliter le travail de l'interpréteur, deux instructions HBP et HBLP ont été ajoutées. Elles branchent vers une fonction qui gère les exceptions logicielles (une fonctionnalité présente dans de nombreux langages, comme Java). La première effectue un branchement vers la fonction en question, la seconde sauvegarde l'adresse de retour avant de faire ce branchement. Les deux peuvent brancher vers 256 fonctions pré-déterminées, sans avoir à présenter leur adresse. ==La traduction binaire accélérée par le matériel== Plus haut, nous avons surtout parlé des compilateurs et des interpréteurs. Cependant, nous devons aussi parler de la '''traduction binaire'''. Elle traduit un programme écrit dans un code machine vers un autre code machine. Par exemple, elle traduit un programme compilé pour un CPU x86 vers un code machine ARM. En général, le système d'exploitation est généralement compilé pour le jeu d'instruction natif, mais il exécute des applications prévues pour le x86. Les applications sont traduites par le système d'exploitation, avant d'exécuter le code traduit. : Nous parlerons dans la suite de code machine source et de code machine cible, pour parler respectivement du code à traduire et du code obtenu après traduction. Idem avec d'autres termes comme architecture cible/source, ou jeu d'instruction cible/source. La traduction binaire est surtout utilisée pour des questions d'émulation ou de compatibilité. Par exemple, lorsque les Macintosh sont passés de processeurs Power PC vers des processeurs x86, le système d'exploitation Mac OS utilisait la traduction binaire pour convertir les anciennes applications Power PC vers du code x86. Les utilisateurs n'y ont vu que du feu. Le système de traduction binaire était appelé Rosetta 1. Par la suite, lors de la transition de processeurs x86 vers des processeurs Apple, Rosetta 2 a vu le jour. La traduction binaire a surtout été utilisée pour traduire du code x86 vers un autre jeu d'instruction. Il faut dire que le x86 est le jeu d'instruction dominant. De nombreuses entreprises ont eu pour ambition de briser l'hégémonie du x86 sur PC, en remplaçant le x86 par un jeu d'instruction plus performant, tout en gardant une compatibilité maximale. La traduction binaire était la seule solution pratique. Elle était le plus souvent intégralement réalisée en logiciel, comme c'était le cas sur les architectures Itanium avec le ''IA-32 Execution Layer''. ===Les généralités sur la traduction binaire assistée en matériel=== L'architecture source est presque tout le temps une architecture CISC, assez ancienne, qu'on souhaite émuler. Le choix de l'architecture cible se porte souvent sur une architecture VLIW, fort différente de l'architecture source, et ce pour obtenir une économie de matériel conséquente. Pour s'exécuter rapidement, le code traduit doit exploiter le parallélisme d'instruction, à savoir exécuter plusieurs instructions en même temps dans des unités de calcul séparées. Il est possible d'utiliser un processeur superscalaire avec exécution dans le désordre pour cela, mais au prix d'un cout important en transistors. Alors qu'en utilisant un CPU VLIW, c'est le traducteur binaire qui fait tout le travail d'extraction du parallélisme d'instruction. Et après tout, quitte à avoir un traducteur binaire, autant lui refiler le boulot d'optimisation. Compiler du code à la volée est certes assez lourd, mais qu'il existe des algorithmes efficaces pour regrouper des instructions indépendantes dans une seule instruction VLIW. Au passage, le processeur VLIW a plus de registres que le processeur source. Cela permet de faire du renommage de registres directement en logiciel. Le traducteur binaire n'hésite pas à changer les noms de registres entre instructions source et instruction VLIW cible, afin de supprimer des dépendances de données. En conséquence, cela ouvre des opportunités de parallélisme, qui sont exploitées lors du regroupement des opérations en instructions VLIW. Vous pourriez penser que le choix d'un CPU VLIW pour émuler du CISC est tout sauf optimal. Vous devez penser que la traduction binaire est d'autant plus simple que l'architecture source et cibles sont semblables. Dans les faits, ce n'est pas tellement traduire les instructions qui pose problème, mais plus la gestion du registre d'état, des exceptions matérielles, de la mémoire virtuelle, la différence entre gros-boutisme et petit-boutisme, de même que des différences d'adressage pour les périphériques. Le premier problème est la gestion des conditions, notamment en présence d'un registre d'état. En pratique, la traduction binaire s'utilise pour traduire du code CISC vers du code RISC ou VLIW. Les architectures source ont donc un registre d'état, qui est mis à jour non seulement par des instructions de test, mais aussi des instructions arithmétiques. L'architecture cible n'a elle pas de registre d'état, mais des registres à prédicats. La traduction de l'un vers l'autre est alors quelque peu compliquée. Et elle est d'autant plus compliquée que le registre d'état est la source de dépendances d'instruction implicites, qui réduisent les performances. Par exemple, si une instruction arithmétique modifie le registre d'état, cela peut impacter l'exécution d'une instruction ultérieure, qui lit ce registre d'état. Pour éliminer ces fausses dépendances, le traducteur binaire doit renommer ce registre en logiciel et détecter les dépendances utiles. Et le compilateur doit gérer les cas où il y a beaucoup de distance entre les deux, voire les cas où l'instruction dépendante n'a pas encore été analysée par le compilateur. Un autre problème est lié à la gestion des exceptions matérielles, et précisément des exceptions précises. Pour rappel, une instruction VLIW regroupe plusieurs opérations, opérations qui correspondraient à une instruction machine sur un CPU pas VLIW. En conséquence, plusieurs instructions du langage machine "source" sont regroupées en une seule instruction VLIW. Et il faut tenir compte du cas où une instruction source lève une exception. Dans le code VLIW, cela signifie qu'une opération lève une exception, et il faut annuler partiellement l'instruction VLIW associée. Et par partiellement, on veut dire que seules les opérations suivantes dans l'ordre du programme source doivent être annulées, puis ré-exécutées. Et c'est un sacré casse-tête ! Les processeurs VLIW utilisent comme solution le mécanisme d'exceptions différées des processeurs EPIC, vu il y a quelques chapitres. Pour résumer, le code est exécuté par blocs d'instruction, délimités par des branchements ou tout autre limite/barrière pertinente dans le code. Un bloc de code est compilé en deux versions : une version rapide sans exceptions matérielles, une version lente qui gère les exceptions précises. La version rapide s'exécute sans exécuter les exceptions. Cependant, les exceptions matérielles sont enregistrées, pour être prises en compte à la toute fin du bloc de code. La version rapide est exécutée en premier et elle mémorise si une exception matérielle a eu lieu. Puis une instruction vérifie si une exception a eu lieu et décide quoi faire. Si aucune exception n'a eu lieu, elle passe à la suite du programme, les résultats du bloc de code sont définitivement acceptés. Mais si une exception a eu lieu, tout est annulé. Le processeur est remis dans l'état initial, puis le code est ré-exécuté instruction par instruction de manière à gérer l'exception correctement. ===La traduction par pré-décodage=== Une première solution serait de faire la traduction binaire dans le cache d'instruction. Lors d'un défaut de cache, le code chargé depuis la RAM est traduit en code machine cible. Et c'est ce code machine cible qui est mémorisé dans le cache d'instruction et exécuté par le processeur. La technique marche sur le papier et n'est qu'une amélioration des techniques de pré-décodage vues il y a quelques chapitres. Cependant, le code obtenu est une traduction assez basique, qui n'incorpore pas d'optimisations dignes de ce nom. De plus, elle gère mal le cas où la taille du code source et cible sont potentiellement très différentes. Concrètement, elle est surtout utile pour traduire du code d'un processeur RISC vers un autre processeur RISC, dont les tailles d'instructions sont similaires. Elle ne permet pas d'émuler plusieurs jeux d'instructions différents, le cout en matériel (un circuit de pré-décodage par jeu d'instruction) serait trop important. ===Le projet DAISY d'IBM=== De très rares processeurs étaient conçus pour accélérer cette traduction binaire, afin de garder de bonnes performances. Les premiers à avoir étudié l'idée étaient IBM, avec leur projet DAISY (''Dynamically Architected Instruction Set from Yorktown''). Le projet de base était de convertir à la volée du code compilé pour des CPU Power PC, vers du code VLIW. Les chercheurs d'IBM avaient développé un algorithme de traduction dynamique efficace, ainsi qu'un processeur VLIW disposant d'optimisations spécifiques à la traduction binaire dynamique, à la volée. De nombreuses idées de ce projet ont été reprises ou re-découvertes par la société Transmetta, avec ses processeurs Crusoe et Efficieon, puis par NVIDIA avec son projet Denver. Les ingénieurs de ce projet ont étudié la possibilité d'émuler plusieurs jeux d'instructions différents sur un même processeur, notamment le s390 d'IBM et le x86. En théorie, cela demande juste d'avoir plusieurs programmes de traduction binaire : un pour le x86, un autre pour le s390, éventuellement un autre pour le Power PC. Mais cela ne s'est pas concrétisé. Mais les ingénieurs ont étudié quelle pourrait être l'architecture VLIW idéale pour ça. Par exemple, il fallait des additions 3-opérandes pour simplifier les calculs d'adresse. Une difficulté était la gestion du registre d'état, dont les bits ne sont pas mis à jour de la même manière sur le x86 et le s390 ou le Power PC. La différence entre gros-boutisme et petit-boutisme était aussi un problème, de même que des différences d'adressage pour les périphériques. Pour que la traduction binaire soit efficace, le processeur VLIW intègre diverses optimisations, comme les exceptions différées et les branchements multi-voies. La principale est la suivante : seul le code des boucles ou fonctions exécutées fréquemment est traduit en code VLIW, le reste du code est interprété. En effet, traduire du code binaire prend plus de temps qu'une simple interprétation. Pour du code qui ne sera exécutée qu'une seule fois, il est plus rapide d'utiliser l'interprétation. Par contre, pour du code exécuté beaucoup de fois, le cout de la traduction binaire est amorti, dilué sur N exécutions, compensé par le gain en temps d'exécution de ce code traduit. Au final, cela permet de ne traduire que le code qui le mérite. La détection du code fréquemment exécuté est réalisée dans le cœur VLIW. L'unité de branchement mémorise les derniers branchements rencontrés et le nombre de fois qu'ils ont été exécutés. Si ils ont été exécutés un certain nombre de fois, l'unité de branchement lève une exception matérielle, qui invoque le traducteur binaire. La routine de cette exception est le traducteur binaire proprement dit. En clair, la traduction binaire est démarrée quand le processeur détecte qu'une fonction a été exécutée plus de N fois, via une exception matérielle dédiée. La mesure du nombre d'exécution d'un branchement se fait dans l'unité de calcul dédiée aux branchements, avec l'aide d'une sorte de ''branch adress buffer'' modifié. Pour rappel, le ''branch adress buffer'' mémorise le ''Program counter'' de chaque branchement récemment rencontré. Ici, chaque entrée du ''branch adress buffer'' est associée à un compteur incrémenté à chaque exécution du branchement. ===Les processeurs Crusoe et Efficieon de Transmetta=== Les processeurs Crusoe Et Efficeron sont deux processeurs VLIW produits par la société Transmetta, une société californienne rachetée par NVIDIA. Ils étaient conçus pour exécuter spécifiquement des programmes x86, système d'exploitation inclus. L'idée derrière ce projet était d'exécuter du code x86 sans que les applications, ni même le système d'exploitation et le BIOS soient au courant ! Les processeurs Transmetta étaient en réalité conçus pour exécuter un programme unique, le ''Code Morphing Software'' (CMS), qui traduisait le code x86 en code VLIW. Transmetta ne voulait pas que son processeur VLIW soit exploité directement, sans le CMS. Il n'avait pas rendu public de compilateur pour traduire du C vers du code VLIW, il n'avait pas donné la documentation du jeu d'instruction VLIW. Des efforts de rétro-ingénieurie, documentés sur le site realworldtech, ont cependant permis de comprendre comment étaient encodées les instructions du processeur Crusoe. Comme pour le projet Daisy, le ''Code Morphing Software'' utilisait à la fois interprétation et traduction binaire, selon les besoins. Les instructions sont interprétées lors de leurs premières exécutions, mais le CMS bascule sur de la traduction en code VLIW après un certain nombre d'exécution. Ainsi, les boucles souvent exécutées sont traduites en code VLIW, alors que le reste du code est interprété. Le choix du code à traduire est le fait du CMS, il utilise des heuristiques complexes pour, qui ne sont pas connues dans le détail. Le ''Code Morphing Software'' est mémorisé dans une EEPROM, ce qui en fait un ''firmware'' situé en dessous du BIOS. Le CMS démarre ensuite le BIOS, qui lui-même démarre le système d'exploitation, qui lui-même démarre les pilotes de périphériques et les programmes. Le CMS se réserve les 16 premiers mébioctets de l'espace d'adressage. L'EEPROM du CMS est mappée dedans, mais n'en utilise que la moitié. Le reste est utilisé comme cache, pour mémoriser le code VLIW traduit par le CMS. Les deux processeurs intégraient aussi deux mémoires SRAM, utilisées par le CMS, appelées la ''local program memory'' (LPM) et la ''local data memory'' (LDM). La première contient du code qui gére les interruptions, la mémoire virtuelle, les exceptions matérielles, les problèmes d'alignement mémoire, et quelques fonctions très fréquemment utilisées par le CMS. La seconde est de la mémoire RAM utilisée par ces fonctions de la ''local program memory''. Les mémoires caches L1 et L2 sont séparées de ces deux mémoires. Le processeur Crusoe et Efficieon étaient des processeurs VLIW très simples. Ils n'avaient même pas de MMU, ni de fonctionnalités importantes sur les CPU x86. Pour Crusoe, les instructions VLIW étaient encodées sur 64 ou 128 bits, et regroupaient entre 2 à 4 opérations. Les instructions LOAD/STORE ne supportaient pas d'adressage indicé, juste de l'adressage indirect à registre. Crusoe avait 5 unités de calcul : deux ALU entières, une unité LOAD/STORE, une unité de branchement, une FPU. Efficieon doublait le nombre d'ALU entière et d'unité LOAD/STORE, les instructions VLIW passaient à 256 bits. Le processeur Crusoe contient 160 registres, dont 64 registres généraux, 32 registres flottants. Les registres généraux font 32 bits, ce qui est cohérent avec le fait que les CPU x86 de l'époque étaient des processeurs 32 bits. Quant aux registres flottants, ils faisaient 80 bits, ce qui colle avec la taille des registres flottants de la FPU x87 utilisée à l'époque. Sur les 64 registres généraux, seuls 48 étaient réellement utilisable pour mémoriser des opérandes. Une partie des registres généraux étaient utilisés pour la gestion de la pile, d'autres pour mémoriser l'état du CPU x86 émulé, un registre était un registre zéro non accesible en écriture. La gestion des exceptions est optimisée avec un système d'exceptions différées, le même que celui décrit plus haut. Le processeur utilise un système similaire, pour effectuer des lectures anticipées, qui a été expliqué dans le chapitre sur les processeurs VLIW/EPIC. Sauf qu'il s'agit là d'une optimisation pour gagner en performances en général, pas une aide utile pour gérer des exceptions précises ou toute autre subtilité de la traduction binaire. Pour gérer les exceptions différées et les lectures anticipées, le processeur mémorise l'état du processeur avant de démarrer un bloc de code. Pour cela, le processeur contient des ''shadow registers'', 48 registres entiers et 16 registres flottants. Vous remarquerez qu'il y a autant de registres entiers que de ''shadow registers'' entiers. Les registres entiers/flottants sont mémorisés dans les ''shadow registers'' avant d'exécuter un bloc de code, afin que le processeur puisse revenir à l'état de base. Une autre fonctionnalité liée est le ''gated store buffer''. Le principe est d'éviter toute écriture en mémoire RAM, tant que le code VLIW n'a pas émis d'instruction ''commit''. L'idée est que les écritures sont conservées dans la ''store queue'' de l'unité LOAD/STORE. Pour rappel, la ''store queue'' met en attente les écritures soit tant que les écritures ne sont pas terminées, soit tant que la RAM est occupée. L'idée est que les écritures sont accumulées dans la ''store queue'' tant que l'instruction ''commit'' n'a pas eu lieu. Le ''store queue'' est "déversé dans la RAM" seulement quand l'instruction ''commit'' s'exécute. ===Les CPU NVIDIA Denver=== Nvidia a tenté quelque chose de similaire avec son projet Denver. NVIDIA a racheté l'entreprise Transmetta, ses brevets, et a tenté de refaire la même chose. Le projet initial était de traduire du code x86 vers un jeu d'instruction VLIW propriétaire, comme l'a fait Transmetta. Mais NVIDIA n'a pas réussi à acquérir la licence du jeu d'instruction x86 et s'est rabattu sur le jeu d'instruction ARM. Le processeur né de ce projet est le Tegra K1-64 CPU. Il s'agit d'un processeur composé d'un cœur VLIW, couplé à deux décodeurs d'instructions ARM. Les deux décodeurs ARM traduisent une paire d'instructions ARM consécutives en instructions VLIW, pour les exécuter sur le cœur VLIW. Le CPU peut fonctionner selon deux modes : ARM et VLIW. En somme, le processeur supporte deux jeux d'instruction : le jeu d'instruction ARM, et un jeu d'instruction VLIW. En mode ARM, les instructions ARM sont chargées depuis le cache, traduites en VLIW par les décodeurs ARM, puis exécutées par le cœur VLIW. En mode optimisé, le CPU exécute des instructions VLIW chargées depuis le cache d'instruction, les décodeurs ne sont pas utilisés. : Le CPU supporte en réalité deux jeux d'instruction ARM : ARM8 et 7. Mais ils correspondent au même mode pour le CPU. Un point important est que le processeur peut changer très rapidement de mode, en à peine quelques cycles d'horloges. On n'est clairement pas dans le cas des CPU x86, qui mettent des plombes pour passer du mode 32 à 64 bits et inversement. La commutation est tellement rapide qu'on peut considérer que le processeur supporte deux jeux d'instruction simultanément. Et le support simultané de deux jeux d'instruction facilite l'implémentation de la traduction binaire. Encore une fois, inutile de traduire du code qui ne sera exécutée qu'une seule fois, mieux vaut l'exécuter directement dans le mode ARM. Surtout que le processeur supporte un mode ARM qui permet d'exécuter du code ARM sans perte de performance. Par contre, le code fréquemment utilisé, notamment dans des boucles critiques, est traduit en VLIW. Au final, les performances sont optimisées, en limitant le travail du logiciel traducteur binaire. La détection du code fréquemment exécuté se fait différemment que pour les processeurs Transmetta et Daisy. L'unité de branchement ne détecte pas directement le code beaucoup exécuté, même si elle a un rôle à jouer. A la place, la détection est réalisée en logiciel, par un ''thread'' dédié, qui s'exécute sur un cœur séparé. Il détecte le code fréquemment utilisé en regardent les branchements exécutés récemment. Pour cela, le processeur mémorise l'historique des branchements pris récemment et copie cette historique dans une mémoire tampon dédiée, partagée entre tous les cœurs. Vu qu'elle est partagée, le ''thread'' de détection a accès à l'historique sans pertes de performances, seule la copie de l'historique dans ce tampon a un cout en performance, pas son partage. Le code VLIW obtenu après traduction binaire est mémorisé dans la mémoire RAM, dans une portion spécialement réservée pour. Elle fait 128 mébioctets, et est appelée le '''cache d'optimisation''' par NVIDIA. Le cache d'optimisation est protégé en écriture et seul le traducteur binaire peut écrire dedans. Au passage, le traducteur binaire est du code VLIW, ce qui fait qu'il est placé dans le cache d'optimisation. Le processeur détecte automatiquement quand une fonction pour laquelle le code compilé équivalent est disponible. Pour cela, l'unité de chargement contient une table de correspondance entre l'adresse de la fonction ARM, et l'adresse de son équivalent compilé. Quand une fonction est appelée, l'unité de chargement regarde l'adresse de destination du branchement. Si l'adresse est dans cette table, elle récupère l'adresse de la fonction compilée et branche vers celle-ci. Le processeur dispose d'un cache d'instruction de 128 kibioctets, ce qui est très important, mais nécessaire vu la taille des instructions VLIW. Le coeur VLIW dispose de ses propres décodeurs, d'un ''scoreboard'', de 7 unités de calcul et de registres. Pour les unités de calcul, il a deux FPU, deux ALU, une unité de branchement, et deux unités LOAD-STORE faisant aussi ALU. Un point important est que le CPU dispose de 64 registres entiers et de 64 registres flottants. C'est deux fois plus que supporte le jeu d'instruction ARM. En mode ARM, seule la moitié des registres est utilisée. Mais en mode optimisé, le traducteur binaire utilise bien les 64 registres, grâce à une sorte de renommage de registres logicielle. Pour simplifier la traduction binaire, le processeur supporte les techniques vues précédemment. Le processeur intègre un ''gated store buffer'' similaire à celui des processeurs Transmetta. Les techniques d'exceptions différées sont aussi supportées, comme sur les processeurs Transmetta. Il a aussi une unité de préchargement décrite par NVIDIA comme agressive, avec support du préchargement de type anticipé (''runahead''). ===L'émulation de la mémoire virtuelle=== Pour finir, il faut parler de la mémoire virtuelle avec la traduction binaire. Sur le processeur source, le processeur gère à la fois des adresses physiques et virtuelles et sa MMU fait la conversion entre les deux. Sur le processeur VLIW, la MMU est simulée par le programme de traduction binaire, partiellement ou totalement. L'implémentation exacte varie suivant que l'on parle du projet Daisy, des processeurs Transmetta ou de Denver. Sur les processeurs Transmetta, il n'y a pas de MMU ni de mémoire virtuelle. A la place, le programme de traduction binaire émule la mémoire virtuelle du processeur source. Le processeur VLIW ne gère que des adresses physiques, rien d'autre. L'espace d'adressage physique du processeur VLIW a la même taille que celui du processeur émulé, ici des processeurs x86. Il est vraisemblable que les adresses physiques utilisées par le processeur x86 sont les mêmes que celles du processeur VLIW. Sur le projet Daisy, le processeur VLIW gère la mémoire virtuelle via pagination, comme les processeurs émulés. Ce qui fait qu'il gère un espace d'adressage virtuel et un espace d'adressage physique. Pour éviter toute confusion, nous parlerons d'adresse physique/virtuelle VLIW pour les adresses physiques/virtuelles du processeur VLIW, d'adresse virtuelle/physique source pour celle du jeu d'instruction traduit, à savoir du Power PC ou du s390, éventuellement de l'x86. Il faut alors faire le lien entre adresses physiques source et adresses virtuelles VLIW. Pour cela, rien de plus simple : il y a correspondance parfaite. L'adresse physique source numéro N correspond à l'adresse virtuelle VLIW numéro N. Cependant, cela signifie que tout l'espace d'adressage virtuel VLIW serait utilisé par le code à traduire. En réalité, il faut ajouter de la place pour le code traduit et le programme de traduction binaire. La conséquence est que l'espace d'adressage virtuel VLIW est plus large que l'espace d'adressage source. L'espace d'adressage virtuel VLIW est découpé en trois sections : une section pour le code Power PC à traduire, une autre pour le code traduit en VLIW, et une dernière réservée au traducteur binaire. Le programme de traduction est placé dans une ROM mappée en mémoire dans la seconde section. Le reste de la seconde section est réservé à une mémoire RAM utilisée par le programme de traduction. [[File:Mémoire virtuelle sur le projet Daisy.png|centre|vignette|upright=1.5|Mémoire virtuelle sur le projet Daisy]] Il faut noter que chaque page physique source, une fois traduite, correspond à N pages virtuelles VLIW. La raison est que le code traduit prend plus de place que le code machine originel. En conséquence, on doit utiliser une page finale plus grande. Pour le reste, des pages contiguës en mémoire physique source sont elles aussi contiguës en mémoire virtuelle VLIW. Le calcul d'adresse est donc simple : l'adresse physique source est multipliée par N, puis on ajoute l'adresse de base à laquelle commence la section pour le code traduit. ==La traduction binaire sur les Burroughs B1700== Nous venons de voir comment accélérer en matériel la traduction binaire, avec des techniques bien spécifiques. Le Burroughs B1700 a procédé autrement et il est intéressant d'étudier en détail son architecture. Son architecture est décrite dans le livre "Interpreting Machines : Architecture and Programming of the Bl700/Bl800 Series", écrit par Elliott I. Organick et James A. Hinds. Une partie de ce qui vfa suivre est un très court résumé de ce livre. : Dans ce qui suit, nous parlerons de traducteur binaire pour parler soit d'un interpréteur pour un langage de programmation de haut niveau, soit pour un logiciel de traduction binaire. Le Burroughs 1700 est un processeur 24 bits, qui a cependant la capacité d'émuler des architecture 8, 16 bits assez simplement, via divers mécanismes. Le Burroughs 1700 incorpore de nombreuses optimisations pour cela. Une partie d'entre elles permet de gérer des opérandes de taille différente de 24 bits, d'autres permettent de gérer des instructions de taille différente de 24 bits, d'autres servent pour les deux. Il incorpore aussi des instructions facilitant le découpage des instructions machines en ''opcode'', adresses et constantes immédiates. ===L'ALU de taille variable=== Les Burroughs B1700 avaient une '''ALU de taille variable''', ce qui veut dire qu'elle pouvait faire des calculs sur un nombre de bits compris entre 0 et 24. Pour cela, les calculs étaient fait avec une ALU de 24 bits, puis les bits de poids fort inutiles étaient masqués. L'ALU de 24 bits pouvait masquer les bits de poids fort du résultat, mais avec des limitations. Elle gérait des opérandes et résultats de 0, 4, 8, 12, 16, 20 et 24 bits. Pour avoir un réglage plus fin, une ALU 4 bits séparée permettait de corriger le résultat. Les registres X et Y étaient reliés à une ALU de 24 bits capable d'effectuer des opérations arithmétiques basiques. Fait étonnant, l'ALU faisait tous les calculs en même temps et fournissait ses résultats dans 7 registres : un registre SUM pour l'addition, un registre DIFFERENCE pour la soustraction, et un registre par opération bit à bit. Les registres pour les opérations bit à bit sont : CMPX et CMPY pour l'opération NOT sur chaque opérande, XANY pour le ET bit à bit, XEOY pour le XOR, XORY pour le OU logique. Les additions et soustractions pouvaient se faire sur des opérandes codés en binaire ou en BCD, suivant le code opération utilisé. A cela, il fallait ajouter deux registres de "résultat" MSKX et MSKY. Ils fournissaient un masque dépendant de la taille du résultat en bits. C'est ce registre qui permettait de gérer des données de 0, 4, 8, 12, 16, 20, et 24 bits, alors que l'ALU faisait 24 bits. Le masque dans ce registre pouvait servir d'opérande dans une opération de masquage ultérieure, pour corriger le résultat. L'ALU prenait en opérande les registres X et Y, mais aussi un registre CYF pour la retenue entrante. En sortie, il fournissait aussi deux retenue sortantes : une pour l'addition et une autre pour la soustraction. Elles étaient mémorisées dans les registres CYL et CYD, respectivement. L'ALU fournissait aussi 12 conditions, qui alimentait un registre d'état. Le registre d'état était segmenté en trois registres appelés XYST, XYCN et BICN. Les registres X et Y sont des registres pour les opérandes, mais il y a aussi deux autres registres de 24 bits. Il s'agit des registres T et L, qui sont des registres généraux. Les registres T et L étaient découpés en 6 sous-registres de 4 bits, nommés LA LB LC LD LE et LF pour le registre L, et TA TB TC TD TE et TF pour le registre T. Ils étaient adressables comme n'importe quel registre. Le processeur contenait de nombreux autres registres de 4 bits, dont les sous-registres de T et L. En tout, il y a 27 registres de 4 bits adressables ! Et le processeur disposait d'une ALU 4 bits pour manipuler ces registres de 4 bits. L'ALU 4 bits est capable de faire des MOV entre registres de 4 bits, des opérations bit à bit, et peut aussi tester si un bit vaut 0 ou 1. La dernière possibilité est très utile pour implémenter les branchements. De plus, cela permettait de faire des opérations de masquage, notamment pour appliquer un masquage supplémentaire à celui de l'ALU. Par exemple, pour gérer des opérations sur 14 bits, on effectue une opération sur 16 bits avec l'ALU, et on masque les 2 bits manquants avec l'ALU 4 bits. ===Une architecture bit-adressable=== Le CPU Burroughs B1700 était '''bit-adressable'''. En clair, il pouvait adresser la mémoire bit par bit, si nécessaire. Un avantage est que cela facilitait la traduction binaire des instructions machine, mais nous détaillerons cela plus bas. Un autre avantage est que cela permettait d'émuler des architectures dont la taille des registres/''byte''/mots était très différente. Et cet avantage mérite quelques explications immédiates. Le processeur était un CPU 24 bits, mais il pouvait émuler des processeurs 16 bits, 8 bits, 4 bits, 1 bit, 9 bits, ou toute autre valeur. Pour cela, le processeur lisait des mots de 24 bits et utilisant un ''barrel shifter'' pour sélectionner les bits adéquats. Par exemple, pour un accès de 16 bits, la donnée lue en mémoire était masquée de manière à ne garder que les 16 bits de poids faible. Le ''barrel shifter''/circuit de masquage était placé directement avant le bus mémoire, sur le trajet des données/instructions. Le ''barrel shifter'' était commandé par un registre ''Field Unit'', qui précisait quelle était la taille des données à charger. ===Des registres d’interfaçage mémoire adressables=== Pour communiquer avec la mémoire, il avait trois registres séparés : READ, WRITE, MAR. READ contient la donnée lue lors d'une lecture, WRITE est pour une donnée à écrire lors d’une écriture, MAR contient l'adresse à lire ou en cours de lecture. Il s'agit de registres d’interfaçage mémoire, les mêmes que ceux vu dans le chapitre sur le chemin de données du CPU. Une instruction LOAD est donc émulée en deux instructions machines : une instruction MOV pour copier l'adresse dans le registre read, une instruction de lecture proprement dite. L'écriture demande d'ajouter une seconde instruction MOV pour copier la donnée à écrire, et la lecture est remplacée apr une écriture. Le fait que ces registres d’interfaçage mémoire et IO soit adressable est peu commun, et c'est plus quelque chose qu'on attend d'un microcode que d'instructions machines. D'ailleurs, l'implémentation des instructions LOAD/STORE ressemble à ce qui est effectué par le séquenceur d'un CPU. Le séquenceur est d'ordinaire celui qui séquence la copie des adresses/données dans les registres d’interfaçage, puis lance la lecture/écriture. Ici, c'est réalisé via des instructions machines. Il faut bien comprendre que les registres d’interfaçage mémoire ont été promus au rang de registres architecturaux, sur ce processeur. De même, pour communiquer avec les entrées-sorties, il disposait de deux registres CMND et DATA : CMND pour envoyer une commande à une entrée-sortie, DATA pour lire ou écrire une donnée. Il y avait aussi un registre U, qui mémorisait une donnée lue depuis le lecteur de cassette. ===Le code machine du Burroughs B1700=== Le processeur utilise des instructions de 16 bits de long, qui sont copiées dans registre d'instruction M de 16 bits. Le ''program counter'' est appelé le registre A. Les instructions du processeur font 16 bits de long, et elles sont alignées en RAM sur 16 bits, ce qui fait que les 4 bits de poids fort de ce registre valent 0, seuls les 14 bits de poids fort sont utiles. Les adresses mentionnées plus haut, qui adressent les micro-opérations en mémoire RAM, sont des adresses de mots de 16 bits, pas les adresses de bit individuels. Fait intéressant, il était possible de faire un OU logique entre l'instruction lue et un registre du processeur, le résultat étant mémorisé dans le registre M. Cela permettait de faire du code automodifiant, pour émuler des modes d'adressage complexe. Cela permettait par exemple de remplacer une adresse dans une micro-opération. L'adresse est alors surimposée avec un OU dans le champ d'adresse, alors que le reste de l'instruction n'est pas modifié. Afin de gérer les interruptions, ces dernières étaient partiellement détectées en logiciel. Une interruption doit être prise en compte à la toute fin de l'exécution d'une instruction source, quand celle-ci a finit son travail. Mais vu qu'ici, les instructions sont émulées avec des séries d'instructions cibles, il y a un problème : il est possible d'interrompre une instruction émulée en plein milieu. Pour éviter cela, le processeur ne déclenche pas les interruptions matérielles immédiatement, elles sont mises en attente. Une instruction dédiée vérifie si une interruption est en attente, et l'exécute alors cas échéant. L'instruction en question est utilisée en dernier, dans la suite d'instruction qui émule l'instruction source. Les Burroughs B1700 intégrait une pile d'adresse de retour, capable de mémoriser 16 adresses de 24 bits. Il disposait de 4 registres généraux, nommés X, Y, T et L, tous de 24 bits. Un point important est que le registre T avait des possibilités d’extraction spécifiques. Il était possible d'extraire N bits, placés n'importe où dans ce registre, et de les copier dans un autre registre. Le CPU supportait une '''instruction EXTRACT''' pour ça. Nous verrons comment cette possibilité est exploitée pour la traduction binaire dans ce qui suit. ===La traduction binaire sur le Burroughs B1700=== Les instructions source peuvent faire 16 bit avec tel jeu d'instruction, 8 bits avec un autre, 32 bits sur un autre, etc. Elles peuvent aussi être de longueur fixe ou variable ! Le B1700 utilisait une architecture bit-adressable pour gérer ces contraintes. En effet, avec un CPU bit-adressable, les contraintes d'alignement des instructions disparaissent ! Dans ce qui suit, on suppose que l'instruction est composée de '''champs''' : l'opcode, les opérandes, les adresses absolues, les constantes immédiates, les numéros de registre, etc. L'idée est que l'instruction source est chargée dans le registre T, puis découpée en champs, qui sont copiés dans les registres du processeur ou envoyés au microcode. Je rappelle que le registre T est relié à un ''barrel shifter'', différent au précédent, qui permettait de masquer, décaler et extraire une donnée de ce registre. Et il y a la possibilité d'extraire une suite de bit de ce registre T, pour la copier dans un autre registre. Par exemple, prenons une instruction composée d'un opcode, d'une constante immédiate et d'une adresse immédiate. Premièrement, elle est chargée dans le registre T. Deuxièmement, la constante immédiate est extraite du registre T et copiée dans le registre X. Troisièmement, l'adresse est extraite et est copiée dans le registre d’interfaçage mémoire, une lecture est démarrée, et l'opérande lue est copiée dans le registre Y. Enfin, l'instruction équivalente à l'opcode est exécutée par le microcode. Le tout prend plusieurs instructions : une première pour lire l'opcode, une seconde pour lire la première opérande, une troisième pour lire la seconde opérande, una quatrième pour tout exécuter. Notons que suivant la taille des champs, on peut en charger plusieurs à la fois. Par exemple, imaginons qu'on ait chargé 24 bits, contenant un opcode de 8 bits et une constante immédiate de 16 bits : le registre T est découpé en un opcode et une constante, avec deux instructions EXTRACT. L'architecture est bit-adressable car elle gère des champs qui ne font pas forcément 8 ou 16 bits. Par exemple, certains jeux d'instructions ont des opcodes codés sur 6 ou 7 bits, voire 9-10 bits. Utiliser une architecture bit-adressable règle ce problème d'alignement. On peut lire un champ depuis la mémoire RAM en précisant l'adresse de son premier bit, pas besoin de gérer des accès non-alignés. Par exemple, prenons une instruction source avec un opcode de 7 bits, suivi par une adresse de 12 bits et une constante immédiate de 15 bits. On peut charger 7 + 12 bits en un premier accès, pour extraite l'opcode et l'adresse, suivi par un second accès de 15 bits pour extraire la constante immédiate. Pour charger un champ, il faut connaitre plusieurs informations : l'adresse de son premier bit, sa taille. Pour cela, le B1700 contient deux registres, nommés ''Field Adress'' (FA) et ''Field Length'' (FL), qui indiquent l'adresse du premier bit et la taille de l'instruction à charger. Le premier fait 24 bits, ce qui permet de gérer des adresses de 24 bits. N'oubliez pas que le processeur est bit-adressable, ce qui permet d'adresser 16 Mébi-bits, soit 2 mébioctets. Le registre pour la taille fait lui 16 bits, ce qui permet de gérer des instructions de 65536 bits, soit 8192 octets ! Leur nom commence par ''field'', car le processeur peut gérer des instructions de taille variable et/ou plus longues que 24 bits. Dans ce cas, le chargement de l'instruction se fait champ par champ. Pour donner un exemple, prenons une instruction composée de 20 champs de 12 bits chacun. Les champs sont chargés un par un dans le registre T. Le registre FA est alors initialisé avec l'adresse de l'instruction, il est incrémenté de 12 à chaque cycle. Le registre FL est initialisé avec la taille de l'instruction, soit 20 * 12 = 240 bits, et il est décrémenté de 16 à chaque cycle. Précisons cependant que la taille d'un champ peut être changée d'un cycle à l'autre. Par exemple, le processeur peut charger un opcode de 8 bits, l'interpréter, détecter que l'instruction demande ensuite de charger une adresse de 16 bits. Dans ce cas, le registre FL est immédiatement altéré pour charger 16 bits, au lieu des 8 bits de l'opcode immédiatement précédent. ===Le Burroughs B1726 : le mal-nommé ''split-level control store''=== Le Burroughs B1700 avait plusieurs modèles : le B1726, le B1710 et le B1800. * Le B1710 plaquait le traducteur binaire en mémoire RAM. * Le B1800 avait ajouté un cache dédié au code du traducteur binaire, afin d'améliorer les performances. * Le B1726 utilisait un ''local store'' dédié au traducteur binaire. Le B1726 peut exécuter le traducteur binaire soit depuis le ''local store'', soit depuis la mémoire RAM avec une pénalité en termes de performances. La documentation appelle cela un '''''split-level control store''''', mais le terme est trompeur : la documentation qualifie de microcode le langage machine du Burroughs B1700, ce qui est à l'origine de beaucoup de confusions. Une autre source de confusion est que ce langage machine a quelques propriétés qu'on pourrait attendre d'un microcode, mais certaines sont manquantes. Par exemple, il peut adresser les registres d’interfaçage mémoire, mais c'est parce que ce sont des registres architecturaux sur ce processeur. Ses instructions n'encodent pas de signaux de commande, par contre, elles font 16 bits et adressent les opérandes de manière implicite. Le ''local store'' mémorisait au maximum 2048 instructions machine, ce qui fait que les 2048 premières "adresses" correspondaient au ''local store''. Les suivantes étaient des adresses de mémoire RAM qui contenaient le reste du traducteur binaire. En pratique, l'adresse qui séparait les deux était configurable ! On pouvait limiter la taille du ''local store'' à un multiple de 32 instructions ! Les adresses restantes étaient alors réattribuées à la RAM. Par exemple, on pouvait attribuer 64 adresses au ''local store'', les 2048 - 64 adresses restantes étaient alors attribuées à la mémoire RAM. Un registre limite, nommé TDPM, indiquait quelle était l'adresse de démarcation entre les deux. Le programmeur devait découper le traducteur binaire en segments, certains étant placés dans le ''local store'', les autres étant en mémoire RAM. Mieux que ça, le processeur autorisait d'utiliser la technique de l'''overlaying'' pour le microcode ! Il y avait même une fonction OVERLAY dédiée pour ! OVERLAY avait besoin de trois opérandes : la taille du segment à copier, son adresse source en mémoire RAM, son adresse de destination dans la micro-SRAM. L'interpréteur réservait un "segment" en mémoire RAM, pour les données, dont le processeur connaissait la position. En dehors de ce segment, il n'y a que des instructions machines à traduire, et le traducteur binaire. Le CPU incorporait pour cela deux registres, un pour l'adresse de base du segment de données, un autre pour son adresse de fin. Il incorporait une protection mémoire limitée sur ce segment, à savoir que seul l'interpréteur pouvait lire ou écrire dans ce segment. ==Les jeux d'instructions dédiés à un langage de programmation== De rares processeurs sont conçus pour un langage de programmation en particulier. On appelle ces processeurs, conçus pour des besoins particuliers, des '''processeurs dédiés'''. Par exemple, les fameux ''Burrough E-mode'' B5000/B6000/B7000 étaient spécialement conçus pour exécuter de l'ALGOL-60. Leurs cousins B2000/B3000/B4000 étaient eux conçus pour le COBOL. Des langages fonctionnels ont aussi eu droit à leurs processeurs dédiés. Le prolog en est un bel exemple, avec les superordinateurs de 5ème génération qui lui étaient dédié. On peut aussi citer les machines LISP, dédiés au langage LISP, qui datent des années 1970. Elles étaient capables d’exécuter certaines fonctions de base du langage directement dans leurs circuits : elles possédaient notamment un ''garbage collector'' câblé dans ses circuits ainsi que des instructions machines supportant un typage déterminé à l’exécution. Les processeurs dédiés ont eu leur heure de gloire au début de l'informatique, à une époque où les langages de haut niveau venaient d'être inventés. À cette époque, les compilateurs n'étaient pas performants et ne savaient pas bien optimiser le code machine. Il était alors rationnel, pour l'époque, de rapprocher le code machine cible et le langage de programmation de haut niveau. De nombreuses architectures dédiés ont ainsi été inventées, avant que les concepteurs se rendent compte des défauts de cette approche. Les défauts en question ne sont pas nombreux, mais assez simples à comprendre. Premièrement, elles sont très rapides pour un langage de programmation en particulier, mais sont assez mauvaises pour les autres, d'où un problème de "compatibilité". Ajoutons à cela que les langages de programmation peuvent évoluer, devenir de moins en moins populaires/utilisés, ce qui rend la création d'architectures généralistes plus pertinente. Enfin, les architectures dédiées sont évidemment des processeurs CISC, pour implémenter les nombreuses fonctionnalités des langages évolués. Et les défauts des CISC sont assez rédhibitoires à l'heure actuelle. À l'heure actuelle, les algorithmes des compilateurs se sont améliorés et savent nettement mieux utiliser le matériel. Ils produisent du code machine efficace, ce qui rend les architecture dédiées bien moins intéressantes. Si on ajoute les défauts de ces architectures dédiées, par étonnant que les architectures dédiées aient presque disparues. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'accélération matérielle de la virtualisation | prevText=L'accélération matérielle de la virtualisation | next=Le matériel réseau | nextText=Le matériel réseau }} </noinclude> 3mnwbey7brg5pg9b8ole6p1guuwh3b6 764067 764066 2026-04-19T21:40:47Z Mewtow 31375 /* La traduction binaire sur les Burroughs B1700 */ 764067 wikitext text/x-wiki De nos jours, la majorité des programmeurs programment dans des langages de haut niveau. Il est très rare pour eux d'avoir à utiliser de l'assembleur, et encore moins un langage machine. Les programmes écrits dans un langage de haut niveau sont traduits en langage machine par un logiciel appelé le compilateur. [[File:Compilation.PNG|centre|vignette|upright=2.5|Compilateur : principe.]] Il se trouve que quelques rares processeurs sont conçus pour faciliter le travail du compilateur. Et ce sont ces processeurs que nous allons voir dans ce qui suit. Ils représentent une catégorie de jeux d'instruction à part, qui n'a pas vraiment de nom. ==La compilation et l’interprétation== Un compilateur traduit donc du code source, écrit dans un langage de haut niveau, vers du langage machine. La traduction est rarement directe. En général, le compilateur traduit le code source en assembleur, qui est lui-même traduit en langage machine. L'assembleur est une représentation textuelle du langage machine alors que le code machine est du binaire exécutable par le processeur. La traduction se fait donc en deux étapes, la '''compilation''' proprement dite et l''''assemblage''', réalisées respectivement par un compilateur et un assembleur. Il faut parfois rajouter une troisième phase d'édition des liens, que nous passons volontairement sous silence. {| |[[File:Code C.png|vignette|200px|Le code source, ici en C.]] |[[File:Assem.png|vignette|250px|Le code assembleur.]] |[[File:Binary file - hello world (C programming).png|vignette|150px|Le code machine.]] |} ===L'interprétation et la compilation à la volée=== Une alternative à la compilation est l''''interprétation''', qui transforme le code source en code machine à la volée. Avec l'interprétation, le code source est passé à un logiciel appelé l''''interpréteur''', qui exécute le code source ligne de code par ligne de code. Une ligne de code est traduite en code machine, qui est exécutée, puis l'interpréteur passe à la ligne suivante. [[File:Kaantotulkkaus.png|centre|vignette|upright=3|Compilation versus interprétation.]] Un défaut de l'interprétation est son cout en performance, qui est assez important. Aussi, l'interprétation stricte a évolué vers un hybride entre interprétation et compilation. L'idée est qu'une partie peu importante du code source est interprétée, alors que le code important est compilé juste avant d'être exécuté. Pour le dire autrement, le code source est partiellement compilé à la volée, juste avant son exécution. Aussi, on parle de '''compilation à la volée'''. Le terme en anglais est ''Just In Time compilation'', abrévié en JIT. En soi, le JIT est associé à de l'interprétation, les JIT sont en réalité des hybrides interpréteurs-compilateurs. Un point important est que compiler du code à la volée est assez lourd, cela a un cout en performance. Par contre, le code compilé s'exécute plus vite que du code interprété. Si on exécute le code compilé une seule fois, le cout de la compilation l'emporte sur le gain à l'exécution. Mais le code compilé est réutilisé autant de fois que nécessaire. Le gain à l'exécution est donc multiplié par le nombre d'exécutions, alors que le cout de la compilation est répartit sur plusieurs exécutions, il est amorti. Les portions du code sont choisies de manière à avoir un gain en performance maximal. L'idée générale est que le code exécuté très souvent est compilé, alors que du code exécuté pas souvent est interprété. Typiquement, les boucles les plus souvent exécutées sont compilées, le code exécuté une seule fois est interprété. ===Les langages intermédiaires=== Les compilateurs modernes passent par un '''langage intermédiaire''' pour faire la transformation en code machine. Le compilateur traduit le langage de haut niveau en langage intermédiaire, puis traduit le langage intermédiaire en code machine cible. Le langage intermédiaire est parfois appelé le '''''bytecode''''', et ce terme recouvre aussi le code écrit avec du ''bytecode''. L'interprétation aussi peut passer par l'intermédiaire d'un un ''bytecode'', ce n'est pas limité aux compilateurs proprement dit. {| |[[File:Intermediate representation scheme.png|centre|vignette|upright=1.5|Représentation intermédiaire d'un compilateur.]] |[[File:Code interpreter scheme.png|centre|vignette|upright=1.5|Fonctionnement d'un interpréteur.]] |} Faire ainsi a de nombreux avantages, le principal étant d'avoir un compilateur capable de traduire un langage de haut niveau vers plusieurs jeux d'instructions différents. Cela permet d'avoir, par exemple, un compilateur qui traduit du C soit en code machine x86, soit en code machine pour un CPU ARM, soit pour un CPU POWERPC, etc. [[File:Compiler design.svg|centre|vignette|upright=3|Compiler design]] Pour l'interprétation, l'usage d'un ''bytecode'' a d'autres avantages. Pour l'exploiter, il faut distribuer non pas le code source, mais le ''bytecode''. En clair, l'interpréteur ne prend pas en entrée le code source, mais du ''bytecode'' déjà compilé. La traduction est alors beaucoup plus simple, car le code source a déjà été partiellement compilé, par la traduction en ''bytecode''. Interpréter du code source est en effet assez compliqué : il faut effectuer des étapes d'analyse lexicale, sémantique, et bien d'autres. Avec du ''bytecode'', ces étapes ont été réalisées lors de la compilation du ''bytecode'', l'interpréteur a alors peut de choses à faire. L'interprétation du ''bytecode'' se fait instruction par instruction, au niveau du ''bytecode''. Une instruction du ''bytecode'' est traduite en une instruction machine équivalente, qui est exécutée. Si l'instruction est un peu complexe, il exécute une fonction/procédure qui fait la même chose. Pour résumer, le ''bytecode'' est ensuite traduit à la volée et exécuté instruction par instruction par un logiciel appelé l'interpréteur. [[File:Control table.png|centre|vignette|upright=2|Control table]] Le langage intermédiaire peut être vu comme l'assembleur d'un processeur, n'existe pas forcément dans la réalité, mais dont le jeu d'instruction est décrit en détail. Le processeur en question est appelé une '''machine abstraite''', ou encore une ''machine virtuelle''. Nous utiliserons le terme de machine abstraite dans ce qui suit. La machine abstraite n'est pas la même suivant que l'on cible la compilation ou l'interprétation/JIT. Pour l'interprétation/JIT, la machine abstraite est souvent une machine à pile, car cela simplifie grandement la traduction en code machine final. L'interprétation demande que la traduction du ''bytecode'' en code machine soit la plus simple possible. Et cela demande de prendre en compte pas mal de détails. Par exemple, les différents jeux d'instruction existants n'ont pas le même nombre de registres, ce qui pose problème lors de la traduction du ''bytecode''. Lors de la transformation en code machine, un algorithme d'allocation de registres se débrouille pour traduire le code intermédiaire en code qui utilise un nombre limité de registres. Pour améliorer cette allocation de registres, il y a deux solutions niveau ''bytecode'' : soit utiliser un nombre illimité de registres, soit utiliser une machine à pile. Pour les machines à pile, il existe un algorithme simple et rapide pour traduire un code écrit pour une machine à pile en un code écrit pour un processeur avec des registres, qui se débrouille pas trop mal pour allouer efficacement les registres. C'est un avantage assez important pour les langages interprétés. ==Les processeurs qui exécutent du bytecode== Le ''bytecode'' est un code machine, ce qui signifie qu'il peut en théorie s'exécuter sur un processeur qui implémente le jeu d’instruction associé. Si le jeu d'instruction d'un ''bytecode'' est souvent une description censée être fictive, elle n'en reste pas moins un jeu d'instruction et des caractéristiques précises, qu'on peut l'implémenter en matériel ! Néanmoins, tous les ''bytecode'' ne sont pas égaux de ce point de vue. Certains sont faciles à implémenter en matériel, d'autres non. Les ''bytecodes'' implémentés en matériel sont ceux dont les machines abstraites sont des machines à pile. Un premier exemple est celui des processeurs Pascal MicroEngine, qui exécutaient directement le bytecode du langage Pascal, le fameux UCSD P-code. Un second exemple est la '''machine SECD''', qui sert de langage intermédiaire pour certains compilateurs de langages fonctionnels. Elle a été implémentée en matériel par plusieurs équipes, notamment par les chercheurs de l'université de Calgary en 1989. Dans le même genre, quelques processeurs simples étaient capables d’exécuter directement le ''bytecode'' du langage FORTH. Le FORTH, un des premiers langages à pile de haut niveau, possède de nombreuses implémentations hardware et est un des rares langages de haut niveau dont le ''bytecode'' a été utilisé comme langage machine sur certains processeurs. Par exemple, on peut citer le processeur FC16, capable d’exécuter nativement du ''bytecode'' FORTH. ===Les processeurs Java=== Le cas le plus impressionnant est celui de la machine virtuelle Java, qui est un design de processeur comme un autre. En temps normal, le ''bytecode'' Java est compilé ou interprété, mais certains processeurs exécutaient du ''bytecode'' Java directement, sans interprétation ni compilation. Ils sont appelés des '''processeurs Java'''. On peut citer les processeurs ARM disposant de l'extension Jazelle, du JEMCore et du aJ-100 de aJile Systems, le picoJava II de Sun, et quelques autres. Il y a aussi eu quelques projets de recherche ou processeurs open source, comme Komodo, jamuth, le ''Java Optimized Processor'' et quelques autres projets du même genre. L'implémentation de la machine virtuelle Java n'était cependant pas complète. Quelques instructions complexes n'étaient pas gérées par le processeur et devaient être émulées en logiciel. Mais la grosse majorité des instructions l'était. Les instructions non-supportées étaient émulées avec la méthode ''trap and emulate'', à savoir qu'un opcode inconnu déclenchait une exception matérielle ''opcode invalide'', dont la routine pouvait émuler les instructions qui doivent l'être. Le processeur aJ-100 de aJile est capable d'exécuter la quasi-totalité des instructions de la JVM Java, seules deux instructions faisant exception. Les '''accélérateurs Java''' sont une solution intermédiaire, qui permet de traduire à la volée du ''bytecode'' Java en instructions RISC, exécutées sur un autre processeur. Il s'agit formellement de traduction binaire, mais je préfère en parler ici. L'idée est de combiner un processeur RISC avec un coprocesseur Java. Le coprocesseur lit le ''bytecode'' Java et le traduit en instructions RISC, ces dernières étant exécutées sur le processeur RISC. Le JA108 de Nozomi était un coprocesseur de ce genre, au même titre que le coprocesseur JSTAR de JEDI. L'extension Jazelle de certains CPU ARM fonctionnait vraisemblablement sur un principe similaire, sauf que le coprocesseur était intégré dans le processeur ARM, entre le cache d'instruction et le décodeur d'instructions ARM. L'intérêt des processeurs Java n'est pas qu'une question de performance. Certes, ils permettent d’exécuter plus vite les programmes compilés en ''bytecode'', comme des programmes Java pour la JVM Java. Mais l'intérêt est aussi de faire des économies de mémoire RAM et ROM. Je rappelle que la machine virtuelle Java est une machine à pile, ce qui fait que sa densité de code est excellente. Les programmes en ''bytecode'' Java sont donc très petits, et prennent peu de place en mémoire ROM. De plus, utiliser un processeur Java permet de se passer d'interpréteur ou de JIT Java, ce qui économise encore plus de mémoire. ===Transformer un processeur RISC en machine à pile=== La plupart des processeurs Java utilisent en interne une architecture à registre généraux, qui émule une machine à registre en microcode. Quelques registres sont réservés pour l'état de la machine virtuelle, avec au minimum un registre réservé pour le pointeur de pile, un autre pour un pointeur vers un ''pool'' de constantes, et quelques autres. D'autres registres servent à mémoriser le haut de la pile, à savoir le sommet de la pile et quelques opérandes situées juste en-dessous. Par exemple, le processeur picoJava II gardait les 64 opérandes au sommet de la pile dans 64 registres généraux, séparés des registres pour les pointeurs et l'état de la JVM. Une optimisation possible, envisagée sur les processeurs picoJava, était une sorte de macro-fusion sous stéroïde. L'idée était de fusionner certaines séries d'instructions Java en une seule instruction machine. Typiquement, pour faire une opération arithmétique, une machine à pile empile deux opérandes et exécute une opération, ce qui prend trois instructions machine. La spécification picoJava 1 a proposé d'envoyer directement la seconde opérande en entrée de l'ALU, ce qui fusionnait l'empilement de la seconde opérande et l'opération arithmétique. Le processeur picoJava II va plus loin et fusionne les trois instructions en une seule instruction RISC, agissant sur des registres. Mais la majorité des processeurs Java ne faisaient pas cela, car cela complexifie grandement le décodeur. Les processeurs ''bigfoot'' de DCT utilisaient un système différent pour émuler une pile avec des registres. Leur banc de registre de 64 bits était coupé en deux : les 16 premiers registres étaient utilisés pour la pile, les 48 suivants étaient utilisés pour autre chose. L'idée est que le registre 0 mémorise un emplacement vide, le registre 1 contient le sommet de la pile, le registre 2 l'opérande sous le sommet de la pile, et ainsi de suite. Les 15 premières opérandes de la pile sont donc mémorisées dans les registres. Les instructions écrivent leur résultat dans le registre adéquat, puis tous les registres sont décalés pour que le résultat passe dans le registre R1. Par exemple, les instructions PUSH chargent une opérande dans le registre 0, puis tous les registres sont décalés de un rang, pour que l'opérande chargée soit dans le registre 1. Le registre 0 devient alors le registre numéro 1, le registre 1 devient le 2, et ainsi de suite. Par contre, les opérations arithmétiques font autrement. Une instruction d'addition, par exemple, lit les opérandes dans le registre 1 et 2, puis place son résultat dans le registre 2 (les deux opérandes sont dépilées). Puis, les registres sont décalés d'un rang pour que ce registre 2 devienne le 1. Les registres ne sont en réalité pas décalés, il n'y a pas de transferts entre registres. A la place, les 16 premiers registres utilisaient une forme limitée de renommage de registres, similaire au banc de registre tournant utilisé sur les processeurs EPIC comme l'Itanium. Les numéros de registres sont renommés, décalés, à chaque fois qu'on empile ou dépile une opérande. Pour faire le renommage de registres, processeur contient un ''register stack counter'' de 4 bits, qui est incrémenté à chaque fois qu'on empile une donnée, et est décrémenté quand on dépile une donnée. Quand on veut accéder à un opérande, le numéro de registre architectural associé est traduit en numéro de registre physique, en additionnant le ''register stack counter'' (modulo 16, pour rester sur 4 bits). ==L'accélération de l'interprétation/JIT : le ''Thumb-EE'' d'ARM== Quelques processeurs ont ajouté des instructions pour faciliter le travail des interpréteurs/JIT. Par exemple, en 2005, ARM a ajouté le mode ''Thumb Execution Environment'', qui faisait cela. Il reprenait le jeu d'instruction compact ''thumb'' et ajoutait quelques instructions et modifiait le comportement d'instructions existantes. Le jeu d'instruction ''Thumb-EE'' a été ajouté sur les CPU ARM en 2005, mais a été déprécié en 2011, par manque d'utilité. Mais la tentative mérite qu'on s'attarde dessus. Les CPU de gamme M, déstinés à l'embarqué, n'ont jamais supporté ''Thumb-EE'', vu que de tels processeurs ne sont pas conçus pour exécuter du code interprété/JIT. Le mode ''Thumb-EE'' est un mode d'exécution, séparé de l'ARM normal. Ainsi, une instruction ''Thumb-EE'' et une instruction ARM peuvent avoir le même encodage en binaire, mais se comporter différemment. Le processeur est à tout moment soit en mode ''Thumb-EE'', soit en mode ARM normal, ce qui précise comment ces instructions doivent se comporter. Des instructions ont été ajoutées par ''Thumb-EE'', pour qu'il fasse son travail. L'entrée et la sortie du mode ''Thumb-EE'' se fait avec deux instructions : ENTERX et LEAVEX. Une fois en mode ''Thumb-EE'', le décodage des instructions ''thumb'' se fait avec les règles du ''Thumb-EE'', et non celles du ''thumb'' normal. Une première différence entre ''thumb'' normal et ''Thumb-EE'' est l'ajout de ce qui s'appelle un ''null check'' pour les instructions mémoire. Le ''null check'' est utilisé pour les instructions en mode d'adressage "base + indice" ou "Base + décalage", ou tout autre mode d'adressage avec un registre de base utilisés dans des calculs d'adresse. L'idée est de vérifier si l'adresse de base vaut zéro ou non. Si c'est le cas, le processeur lève une exception matérielle, qui est traitée par l'interprétation ou le compilateur JIT. Une telle situation est en effet signe d'une erreur d'adressage mémoire, qui doit être traitée par l'interpréteur. Typiquement, c'est signe qu'un pointeur n'a pas bien été initialisé, ce qui peut arriver avec du code interprété ou JIT. Les interpréteurs/JIT traitent généralement la situation en ajoutant un test avant toute instruction mémoire, pour vérifier si le pointeur accédé vaut zéro ou non. Avec ''Thumb-EE'', pas besoin d'ajouter les tests en questions : ils sont réalisés automatiquement lors de chaque instruction mémoire. : Les registres pour la pile, comme le pointeur de pile et le pointeur de ''frame'', sont aussi concernés. L'instruction CHKA vérifie que les accès à un tableau ne débordent pas en dehors du tableau en question. Par exemple, pour un tableau de 1024 éléments, elle vérifie si l'indice est compris entre 0 et 1024. Pour cela, elle vérifie si l'indice de tableau est bien dans l'intervalle adéquat et lève une exception matérielle si ce n'est pas le cas. Son utilité se comprend quand on sait que tous les accès à un tableau sont vérifiés par l'interpréteur. L'interpréteur est censé ajouter des instructions pour vérifier les indices, à savoir deux ou trois branchements. Avec CHKA, le test se fait en une seule instruction. D'autres modifications mineures de l'encodage des instructions sont aussi présentes, ainsi que des modifications pour les instructions LOAD/STORE. Précisément, dans les modes "base + indice" et "base + décalage", le décalage et l'indice subissent maintenant des décalages. L'idée est de simplifier le calcul d'adresse. De quoi économiser quelques instructions lors des calculs d'adresse, ce qui facilite le travail de l'interpréteur/JIT. Et des instructions LOAD/STORE précises voient leur comportement modifié pour faciliter la gestion de la pile de Java ou l'exécution des méthodes locales. Pour faciliter le travail de l'interpréteur, deux instructions HBP et HBLP ont été ajoutées. Elles branchent vers une fonction qui gère les exceptions logicielles (une fonctionnalité présente dans de nombreux langages, comme Java). La première effectue un branchement vers la fonction en question, la seconde sauvegarde l'adresse de retour avant de faire ce branchement. Les deux peuvent brancher vers 256 fonctions pré-déterminées, sans avoir à présenter leur adresse. ==La traduction binaire accélérée par le matériel== Plus haut, nous avons surtout parlé des compilateurs et des interpréteurs. Cependant, nous devons aussi parler de la '''traduction binaire'''. Elle traduit un programme écrit dans un code machine vers un autre code machine. Par exemple, elle traduit un programme compilé pour un CPU x86 vers un code machine ARM. En général, le système d'exploitation est généralement compilé pour le jeu d'instruction natif, mais il exécute des applications prévues pour le x86. Les applications sont traduites par le système d'exploitation, avant d'exécuter le code traduit. : Nous parlerons dans la suite de code machine source et de code machine cible, pour parler respectivement du code à traduire et du code obtenu après traduction. Idem avec d'autres termes comme architecture cible/source, ou jeu d'instruction cible/source. La traduction binaire est surtout utilisée pour des questions d'émulation ou de compatibilité. Par exemple, lorsque les Macintosh sont passés de processeurs Power PC vers des processeurs x86, le système d'exploitation Mac OS utilisait la traduction binaire pour convertir les anciennes applications Power PC vers du code x86. Les utilisateurs n'y ont vu que du feu. Le système de traduction binaire était appelé Rosetta 1. Par la suite, lors de la transition de processeurs x86 vers des processeurs Apple, Rosetta 2 a vu le jour. La traduction binaire a surtout été utilisée pour traduire du code x86 vers un autre jeu d'instruction. Il faut dire que le x86 est le jeu d'instruction dominant. De nombreuses entreprises ont eu pour ambition de briser l'hégémonie du x86 sur PC, en remplaçant le x86 par un jeu d'instruction plus performant, tout en gardant une compatibilité maximale. La traduction binaire était la seule solution pratique. Elle était le plus souvent intégralement réalisée en logiciel, comme c'était le cas sur les architectures Itanium avec le ''IA-32 Execution Layer''. ===Les généralités sur la traduction binaire assistée en matériel=== L'architecture source est presque tout le temps une architecture CISC, assez ancienne, qu'on souhaite émuler. Le choix de l'architecture cible se porte souvent sur une architecture VLIW, fort différente de l'architecture source, et ce pour obtenir une économie de matériel conséquente. Pour s'exécuter rapidement, le code traduit doit exploiter le parallélisme d'instruction, à savoir exécuter plusieurs instructions en même temps dans des unités de calcul séparées. Il est possible d'utiliser un processeur superscalaire avec exécution dans le désordre pour cela, mais au prix d'un cout important en transistors. Alors qu'en utilisant un CPU VLIW, c'est le traducteur binaire qui fait tout le travail d'extraction du parallélisme d'instruction. Et après tout, quitte à avoir un traducteur binaire, autant lui refiler le boulot d'optimisation. Compiler du code à la volée est certes assez lourd, mais qu'il existe des algorithmes efficaces pour regrouper des instructions indépendantes dans une seule instruction VLIW. Au passage, le processeur VLIW a plus de registres que le processeur source. Cela permet de faire du renommage de registres directement en logiciel. Le traducteur binaire n'hésite pas à changer les noms de registres entre instructions source et instruction VLIW cible, afin de supprimer des dépendances de données. En conséquence, cela ouvre des opportunités de parallélisme, qui sont exploitées lors du regroupement des opérations en instructions VLIW. Vous pourriez penser que le choix d'un CPU VLIW pour émuler du CISC est tout sauf optimal. Vous devez penser que la traduction binaire est d'autant plus simple que l'architecture source et cibles sont semblables. Dans les faits, ce n'est pas tellement traduire les instructions qui pose problème, mais plus la gestion du registre d'état, des exceptions matérielles, de la mémoire virtuelle, la différence entre gros-boutisme et petit-boutisme, de même que des différences d'adressage pour les périphériques. Le premier problème est la gestion des conditions, notamment en présence d'un registre d'état. En pratique, la traduction binaire s'utilise pour traduire du code CISC vers du code RISC ou VLIW. Les architectures source ont donc un registre d'état, qui est mis à jour non seulement par des instructions de test, mais aussi des instructions arithmétiques. L'architecture cible n'a elle pas de registre d'état, mais des registres à prédicats. La traduction de l'un vers l'autre est alors quelque peu compliquée. Et elle est d'autant plus compliquée que le registre d'état est la source de dépendances d'instruction implicites, qui réduisent les performances. Par exemple, si une instruction arithmétique modifie le registre d'état, cela peut impacter l'exécution d'une instruction ultérieure, qui lit ce registre d'état. Pour éliminer ces fausses dépendances, le traducteur binaire doit renommer ce registre en logiciel et détecter les dépendances utiles. Et le compilateur doit gérer les cas où il y a beaucoup de distance entre les deux, voire les cas où l'instruction dépendante n'a pas encore été analysée par le compilateur. Un autre problème est lié à la gestion des exceptions matérielles, et précisément des exceptions précises. Pour rappel, une instruction VLIW regroupe plusieurs opérations, opérations qui correspondraient à une instruction machine sur un CPU pas VLIW. En conséquence, plusieurs instructions du langage machine "source" sont regroupées en une seule instruction VLIW. Et il faut tenir compte du cas où une instruction source lève une exception. Dans le code VLIW, cela signifie qu'une opération lève une exception, et il faut annuler partiellement l'instruction VLIW associée. Et par partiellement, on veut dire que seules les opérations suivantes dans l'ordre du programme source doivent être annulées, puis ré-exécutées. Et c'est un sacré casse-tête ! Les processeurs VLIW utilisent comme solution le mécanisme d'exceptions différées des processeurs EPIC, vu il y a quelques chapitres. Pour résumer, le code est exécuté par blocs d'instruction, délimités par des branchements ou tout autre limite/barrière pertinente dans le code. Un bloc de code est compilé en deux versions : une version rapide sans exceptions matérielles, une version lente qui gère les exceptions précises. La version rapide s'exécute sans exécuter les exceptions. Cependant, les exceptions matérielles sont enregistrées, pour être prises en compte à la toute fin du bloc de code. La version rapide est exécutée en premier et elle mémorise si une exception matérielle a eu lieu. Puis une instruction vérifie si une exception a eu lieu et décide quoi faire. Si aucune exception n'a eu lieu, elle passe à la suite du programme, les résultats du bloc de code sont définitivement acceptés. Mais si une exception a eu lieu, tout est annulé. Le processeur est remis dans l'état initial, puis le code est ré-exécuté instruction par instruction de manière à gérer l'exception correctement. ===La traduction par pré-décodage=== Une première solution serait de faire la traduction binaire dans le cache d'instruction. Lors d'un défaut de cache, le code chargé depuis la RAM est traduit en code machine cible. Et c'est ce code machine cible qui est mémorisé dans le cache d'instruction et exécuté par le processeur. La technique marche sur le papier et n'est qu'une amélioration des techniques de pré-décodage vues il y a quelques chapitres. Cependant, le code obtenu est une traduction assez basique, qui n'incorpore pas d'optimisations dignes de ce nom. De plus, elle gère mal le cas où la taille du code source et cible sont potentiellement très différentes. Concrètement, elle est surtout utile pour traduire du code d'un processeur RISC vers un autre processeur RISC, dont les tailles d'instructions sont similaires. Elle ne permet pas d'émuler plusieurs jeux d'instructions différents, le cout en matériel (un circuit de pré-décodage par jeu d'instruction) serait trop important. ===Le projet DAISY d'IBM=== De très rares processeurs étaient conçus pour accélérer cette traduction binaire, afin de garder de bonnes performances. Les premiers à avoir étudié l'idée étaient IBM, avec leur projet DAISY (''Dynamically Architected Instruction Set from Yorktown''). Le projet de base était de convertir à la volée du code compilé pour des CPU Power PC, vers du code VLIW. Les chercheurs d'IBM avaient développé un algorithme de traduction dynamique efficace, ainsi qu'un processeur VLIW disposant d'optimisations spécifiques à la traduction binaire dynamique, à la volée. De nombreuses idées de ce projet ont été reprises ou re-découvertes par la société Transmetta, avec ses processeurs Crusoe et Efficieon, puis par NVIDIA avec son projet Denver. Les ingénieurs de ce projet ont étudié la possibilité d'émuler plusieurs jeux d'instructions différents sur un même processeur, notamment le s390 d'IBM et le x86. En théorie, cela demande juste d'avoir plusieurs programmes de traduction binaire : un pour le x86, un autre pour le s390, éventuellement un autre pour le Power PC. Mais cela ne s'est pas concrétisé. Mais les ingénieurs ont étudié quelle pourrait être l'architecture VLIW idéale pour ça. Par exemple, il fallait des additions 3-opérandes pour simplifier les calculs d'adresse. Une difficulté était la gestion du registre d'état, dont les bits ne sont pas mis à jour de la même manière sur le x86 et le s390 ou le Power PC. La différence entre gros-boutisme et petit-boutisme était aussi un problème, de même que des différences d'adressage pour les périphériques. Pour que la traduction binaire soit efficace, le processeur VLIW intègre diverses optimisations, comme les exceptions différées et les branchements multi-voies. La principale est la suivante : seul le code des boucles ou fonctions exécutées fréquemment est traduit en code VLIW, le reste du code est interprété. En effet, traduire du code binaire prend plus de temps qu'une simple interprétation. Pour du code qui ne sera exécutée qu'une seule fois, il est plus rapide d'utiliser l'interprétation. Par contre, pour du code exécuté beaucoup de fois, le cout de la traduction binaire est amorti, dilué sur N exécutions, compensé par le gain en temps d'exécution de ce code traduit. Au final, cela permet de ne traduire que le code qui le mérite. La détection du code fréquemment exécuté est réalisée dans le cœur VLIW. L'unité de branchement mémorise les derniers branchements rencontrés et le nombre de fois qu'ils ont été exécutés. Si ils ont été exécutés un certain nombre de fois, l'unité de branchement lève une exception matérielle, qui invoque le traducteur binaire. La routine de cette exception est le traducteur binaire proprement dit. En clair, la traduction binaire est démarrée quand le processeur détecte qu'une fonction a été exécutée plus de N fois, via une exception matérielle dédiée. La mesure du nombre d'exécution d'un branchement se fait dans l'unité de calcul dédiée aux branchements, avec l'aide d'une sorte de ''branch adress buffer'' modifié. Pour rappel, le ''branch adress buffer'' mémorise le ''Program counter'' de chaque branchement récemment rencontré. Ici, chaque entrée du ''branch adress buffer'' est associée à un compteur incrémenté à chaque exécution du branchement. ===Les processeurs Crusoe et Efficieon de Transmetta=== Les processeurs Crusoe Et Efficeron sont deux processeurs VLIW produits par la société Transmetta, une société californienne rachetée par NVIDIA. Ils étaient conçus pour exécuter spécifiquement des programmes x86, système d'exploitation inclus. L'idée derrière ce projet était d'exécuter du code x86 sans que les applications, ni même le système d'exploitation et le BIOS soient au courant ! Les processeurs Transmetta étaient en réalité conçus pour exécuter un programme unique, le ''Code Morphing Software'' (CMS), qui traduisait le code x86 en code VLIW. Transmetta ne voulait pas que son processeur VLIW soit exploité directement, sans le CMS. Il n'avait pas rendu public de compilateur pour traduire du C vers du code VLIW, il n'avait pas donné la documentation du jeu d'instruction VLIW. Des efforts de rétro-ingénieurie, documentés sur le site realworldtech, ont cependant permis de comprendre comment étaient encodées les instructions du processeur Crusoe. Comme pour le projet Daisy, le ''Code Morphing Software'' utilisait à la fois interprétation et traduction binaire, selon les besoins. Les instructions sont interprétées lors de leurs premières exécutions, mais le CMS bascule sur de la traduction en code VLIW après un certain nombre d'exécution. Ainsi, les boucles souvent exécutées sont traduites en code VLIW, alors que le reste du code est interprété. Le choix du code à traduire est le fait du CMS, il utilise des heuristiques complexes pour, qui ne sont pas connues dans le détail. Le ''Code Morphing Software'' est mémorisé dans une EEPROM, ce qui en fait un ''firmware'' situé en dessous du BIOS. Le CMS démarre ensuite le BIOS, qui lui-même démarre le système d'exploitation, qui lui-même démarre les pilotes de périphériques et les programmes. Le CMS se réserve les 16 premiers mébioctets de l'espace d'adressage. L'EEPROM du CMS est mappée dedans, mais n'en utilise que la moitié. Le reste est utilisé comme cache, pour mémoriser le code VLIW traduit par le CMS. Les deux processeurs intégraient aussi deux mémoires SRAM, utilisées par le CMS, appelées la ''local program memory'' (LPM) et la ''local data memory'' (LDM). La première contient du code qui gére les interruptions, la mémoire virtuelle, les exceptions matérielles, les problèmes d'alignement mémoire, et quelques fonctions très fréquemment utilisées par le CMS. La seconde est de la mémoire RAM utilisée par ces fonctions de la ''local program memory''. Les mémoires caches L1 et L2 sont séparées de ces deux mémoires. Le processeur Crusoe et Efficieon étaient des processeurs VLIW très simples. Ils n'avaient même pas de MMU, ni de fonctionnalités importantes sur les CPU x86. Pour Crusoe, les instructions VLIW étaient encodées sur 64 ou 128 bits, et regroupaient entre 2 à 4 opérations. Les instructions LOAD/STORE ne supportaient pas d'adressage indicé, juste de l'adressage indirect à registre. Crusoe avait 5 unités de calcul : deux ALU entières, une unité LOAD/STORE, une unité de branchement, une FPU. Efficieon doublait le nombre d'ALU entière et d'unité LOAD/STORE, les instructions VLIW passaient à 256 bits. Le processeur Crusoe contient 160 registres, dont 64 registres généraux, 32 registres flottants. Les registres généraux font 32 bits, ce qui est cohérent avec le fait que les CPU x86 de l'époque étaient des processeurs 32 bits. Quant aux registres flottants, ils faisaient 80 bits, ce qui colle avec la taille des registres flottants de la FPU x87 utilisée à l'époque. Sur les 64 registres généraux, seuls 48 étaient réellement utilisable pour mémoriser des opérandes. Une partie des registres généraux étaient utilisés pour la gestion de la pile, d'autres pour mémoriser l'état du CPU x86 émulé, un registre était un registre zéro non accesible en écriture. La gestion des exceptions est optimisée avec un système d'exceptions différées, le même que celui décrit plus haut. Le processeur utilise un système similaire, pour effectuer des lectures anticipées, qui a été expliqué dans le chapitre sur les processeurs VLIW/EPIC. Sauf qu'il s'agit là d'une optimisation pour gagner en performances en général, pas une aide utile pour gérer des exceptions précises ou toute autre subtilité de la traduction binaire. Pour gérer les exceptions différées et les lectures anticipées, le processeur mémorise l'état du processeur avant de démarrer un bloc de code. Pour cela, le processeur contient des ''shadow registers'', 48 registres entiers et 16 registres flottants. Vous remarquerez qu'il y a autant de registres entiers que de ''shadow registers'' entiers. Les registres entiers/flottants sont mémorisés dans les ''shadow registers'' avant d'exécuter un bloc de code, afin que le processeur puisse revenir à l'état de base. Une autre fonctionnalité liée est le ''gated store buffer''. Le principe est d'éviter toute écriture en mémoire RAM, tant que le code VLIW n'a pas émis d'instruction ''commit''. L'idée est que les écritures sont conservées dans la ''store queue'' de l'unité LOAD/STORE. Pour rappel, la ''store queue'' met en attente les écritures soit tant que les écritures ne sont pas terminées, soit tant que la RAM est occupée. L'idée est que les écritures sont accumulées dans la ''store queue'' tant que l'instruction ''commit'' n'a pas eu lieu. Le ''store queue'' est "déversé dans la RAM" seulement quand l'instruction ''commit'' s'exécute. ===Les CPU NVIDIA Denver=== Nvidia a tenté quelque chose de similaire avec son projet Denver. NVIDIA a racheté l'entreprise Transmetta, ses brevets, et a tenté de refaire la même chose. Le projet initial était de traduire du code x86 vers un jeu d'instruction VLIW propriétaire, comme l'a fait Transmetta. Mais NVIDIA n'a pas réussi à acquérir la licence du jeu d'instruction x86 et s'est rabattu sur le jeu d'instruction ARM. Le processeur né de ce projet est le Tegra K1-64 CPU. Il s'agit d'un processeur composé d'un cœur VLIW, couplé à deux décodeurs d'instructions ARM. Les deux décodeurs ARM traduisent une paire d'instructions ARM consécutives en instructions VLIW, pour les exécuter sur le cœur VLIW. Le CPU peut fonctionner selon deux modes : ARM et VLIW. En somme, le processeur supporte deux jeux d'instruction : le jeu d'instruction ARM, et un jeu d'instruction VLIW. En mode ARM, les instructions ARM sont chargées depuis le cache, traduites en VLIW par les décodeurs ARM, puis exécutées par le cœur VLIW. En mode optimisé, le CPU exécute des instructions VLIW chargées depuis le cache d'instruction, les décodeurs ne sont pas utilisés. : Le CPU supporte en réalité deux jeux d'instruction ARM : ARM8 et 7. Mais ils correspondent au même mode pour le CPU. Un point important est que le processeur peut changer très rapidement de mode, en à peine quelques cycles d'horloges. On n'est clairement pas dans le cas des CPU x86, qui mettent des plombes pour passer du mode 32 à 64 bits et inversement. La commutation est tellement rapide qu'on peut considérer que le processeur supporte deux jeux d'instruction simultanément. Et le support simultané de deux jeux d'instruction facilite l'implémentation de la traduction binaire. Encore une fois, inutile de traduire du code qui ne sera exécutée qu'une seule fois, mieux vaut l'exécuter directement dans le mode ARM. Surtout que le processeur supporte un mode ARM qui permet d'exécuter du code ARM sans perte de performance. Par contre, le code fréquemment utilisé, notamment dans des boucles critiques, est traduit en VLIW. Au final, les performances sont optimisées, en limitant le travail du logiciel traducteur binaire. La détection du code fréquemment exécuté se fait différemment que pour les processeurs Transmetta et Daisy. L'unité de branchement ne détecte pas directement le code beaucoup exécuté, même si elle a un rôle à jouer. A la place, la détection est réalisée en logiciel, par un ''thread'' dédié, qui s'exécute sur un cœur séparé. Il détecte le code fréquemment utilisé en regardent les branchements exécutés récemment. Pour cela, le processeur mémorise l'historique des branchements pris récemment et copie cette historique dans une mémoire tampon dédiée, partagée entre tous les cœurs. Vu qu'elle est partagée, le ''thread'' de détection a accès à l'historique sans pertes de performances, seule la copie de l'historique dans ce tampon a un cout en performance, pas son partage. Le code VLIW obtenu après traduction binaire est mémorisé dans la mémoire RAM, dans une portion spécialement réservée pour. Elle fait 128 mébioctets, et est appelée le '''cache d'optimisation''' par NVIDIA. Le cache d'optimisation est protégé en écriture et seul le traducteur binaire peut écrire dedans. Au passage, le traducteur binaire est du code VLIW, ce qui fait qu'il est placé dans le cache d'optimisation. Le processeur détecte automatiquement quand une fonction pour laquelle le code compilé équivalent est disponible. Pour cela, l'unité de chargement contient une table de correspondance entre l'adresse de la fonction ARM, et l'adresse de son équivalent compilé. Quand une fonction est appelée, l'unité de chargement regarde l'adresse de destination du branchement. Si l'adresse est dans cette table, elle récupère l'adresse de la fonction compilée et branche vers celle-ci. Le processeur dispose d'un cache d'instruction de 128 kibioctets, ce qui est très important, mais nécessaire vu la taille des instructions VLIW. Le coeur VLIW dispose de ses propres décodeurs, d'un ''scoreboard'', de 7 unités de calcul et de registres. Pour les unités de calcul, il a deux FPU, deux ALU, une unité de branchement, et deux unités LOAD-STORE faisant aussi ALU. Un point important est que le CPU dispose de 64 registres entiers et de 64 registres flottants. C'est deux fois plus que supporte le jeu d'instruction ARM. En mode ARM, seule la moitié des registres est utilisée. Mais en mode optimisé, le traducteur binaire utilise bien les 64 registres, grâce à une sorte de renommage de registres logicielle. Pour simplifier la traduction binaire, le processeur supporte les techniques vues précédemment. Le processeur intègre un ''gated store buffer'' similaire à celui des processeurs Transmetta. Les techniques d'exceptions différées sont aussi supportées, comme sur les processeurs Transmetta. Il a aussi une unité de préchargement décrite par NVIDIA comme agressive, avec support du préchargement de type anticipé (''runahead''). ===L'émulation de la mémoire virtuelle=== Pour finir, il faut parler de la mémoire virtuelle avec la traduction binaire. Sur le processeur source, le processeur gère à la fois des adresses physiques et virtuelles et sa MMU fait la conversion entre les deux. Sur le processeur VLIW, la MMU est simulée par le programme de traduction binaire, partiellement ou totalement. L'implémentation exacte varie suivant que l'on parle du projet Daisy, des processeurs Transmetta ou de Denver. Sur les processeurs Transmetta, il n'y a pas de MMU ni de mémoire virtuelle. A la place, le programme de traduction binaire émule la mémoire virtuelle du processeur source. Le processeur VLIW ne gère que des adresses physiques, rien d'autre. L'espace d'adressage physique du processeur VLIW a la même taille que celui du processeur émulé, ici des processeurs x86. Il est vraisemblable que les adresses physiques utilisées par le processeur x86 sont les mêmes que celles du processeur VLIW. Sur le projet Daisy, le processeur VLIW gère la mémoire virtuelle via pagination, comme les processeurs émulés. Ce qui fait qu'il gère un espace d'adressage virtuel et un espace d'adressage physique. Pour éviter toute confusion, nous parlerons d'adresse physique/virtuelle VLIW pour les adresses physiques/virtuelles du processeur VLIW, d'adresse virtuelle/physique source pour celle du jeu d'instruction traduit, à savoir du Power PC ou du s390, éventuellement de l'x86. Il faut alors faire le lien entre adresses physiques source et adresses virtuelles VLIW. Pour cela, rien de plus simple : il y a correspondance parfaite. L'adresse physique source numéro N correspond à l'adresse virtuelle VLIW numéro N. Cependant, cela signifie que tout l'espace d'adressage virtuel VLIW serait utilisé par le code à traduire. En réalité, il faut ajouter de la place pour le code traduit et le programme de traduction binaire. La conséquence est que l'espace d'adressage virtuel VLIW est plus large que l'espace d'adressage source. L'espace d'adressage virtuel VLIW est découpé en trois sections : une section pour le code Power PC à traduire, une autre pour le code traduit en VLIW, et une dernière réservée au traducteur binaire. Le programme de traduction est placé dans une ROM mappée en mémoire dans la seconde section. Le reste de la seconde section est réservé à une mémoire RAM utilisée par le programme de traduction. [[File:Mémoire virtuelle sur le projet Daisy.png|centre|vignette|upright=1.5|Mémoire virtuelle sur le projet Daisy]] Il faut noter que chaque page physique source, une fois traduite, correspond à N pages virtuelles VLIW. La raison est que le code traduit prend plus de place que le code machine originel. En conséquence, on doit utiliser une page finale plus grande. Pour le reste, des pages contiguës en mémoire physique source sont elles aussi contiguës en mémoire virtuelle VLIW. Le calcul d'adresse est donc simple : l'adresse physique source est multipliée par N, puis on ajoute l'adresse de base à laquelle commence la section pour le code traduit. ==La traduction binaire sur les Burroughs B1700== Nous venons de voir comment accélérer en matériel la traduction binaire, avec des techniques bien spécifiques. Le Burroughs B1700 a procédé autrement et il est intéressant d'étudier en détail son architecture. Son architecture est décrite dans le livre "Interpreting Machines : Architecture and Programming of the Bl700/Bl800 Series", écrit par Elliott I. Organick et James A. Hinds. Une partie de ce qui vfa suivre est un très court résumé de ce livre. : Dans ce qui suit, nous parlerons de traducteur binaire pour parler soit d'un interpréteur pour un langage de programmation de haut niveau, soit pour un logiciel de traduction binaire. Le Burroughs 1700 est un processeur 24 bits, qui a cependant la capacité d'émuler des architectures 8, 16 bits assez simplement, via divers mécanismes. Le Burroughs 1700 incorpore de nombreuses optimisations pour cela. Une partie d'entre elles permet de gérer des opérandes de taille différente de 24 bits, d'autres permettent de gérer des instructions de taille différente de 24 bits, d'autres servent pour les deux. Il incorpore aussi des instructions facilitant le découpage des instructions machines en ''opcode'', adresses et constantes immédiates. ===L'ALU de taille variable=== Les Burroughs B1700 avaient une '''ALU de taille variable''', ce qui veut dire qu'elle pouvait faire des calculs sur un nombre de bits compris entre 0 et 24. Pour cela, les calculs étaient faits avec une ALU de 24 bits, puis les bits de poids fort inutiles étaient masqués. L'ALU de 24 bits pouvait masquer les bits de poids fort du résultat, mais avec des limitations. Elle gérait des opérandes et résultats de 0, 4, 8, 12, 16, 20 et 24 bits. Pour avoir un réglage plus fin, une ALU 4 bits séparée permettait de corriger le résultat. Les registres X et Y étaient reliés à une ALU de 24 bits capable d'effectuer des opérations arithmétiques basiques. Fait étonnant, l'ALU faisait tous les calculs en même temps et fournissait ses résultats dans 7 registres : un registre SUM pour l'addition, un registre DIFFERENCE pour la soustraction, et un registre par opération bit à bit. Les registres pour les opérations bit à bit sont : CMPX et CMPY pour l'opération NOT sur chaque opérande, XANY pour le ET bit à bit, XEOY pour le XOR, XORY pour le OU logique. Les additions et soustractions pouvaient se faire sur des opérandes codés en binaire ou en BCD, suivant le code opération utilisé. A cela, il fallait ajouter deux registres de "résultat" MSKX et MSKY. Ils fournissaient un masque dépendant de la taille du résultat en bits. C'est ce registre qui permettait de gérer des données de 0, 4, 8, 12, 16, 20, et 24 bits, alors que l'ALU faisait 24 bits. Le masque dans ce registre pouvait servir d'opérande dans une opération de masquage ultérieure, pour corriger le résultat. L'ALU prenait en opérande les registres X et Y, mais aussi un registre CYF pour la retenue entrante. En sortie, il fournissait aussi deux retenues sortantes : une pour l'addition et une autre pour la soustraction. Elles étaient mémorisées dans les registres CYL et CYD, respectivement. L'ALU fournissait aussi 12 conditions, qui alimentait un registre d'état. Le registre d'état était segmenté en trois registres appelés XYST, XYCN et BICN. Les registres X et Y sont des registres pour les opérandes, mais il y a aussi deux autres registres de 24 bits. Il s'agit des registres T et L, qui sont des registres généraux. Les registres T et L étaient découpés en 6 sous-registres de 4 bits, nommés LA LB LC LD LE et LF pour le registre L, TA TB TC TD TE et TF pour le registre T. Ils étaient adressables comme n'importe quel registre. Le processeur contenait de nombreux autres registres de 4 bits, dont les sous-registres de T et L. En tout, il y a 27 registres de 4 bits adressables ! Et le processeur disposait d'une ALU 4 bits pour manipuler ces registres de 4 bits. L'ALU 4 bits est capable de faire des MOV entre registres de 4 bits, des opérations bit à bit, et peut aussi tester si un bit vaut 0 ou 1. La dernière possibilité est très utile pour implémenter les branchements. De plus, cela permettait de faire des opérations de masquage, notamment pour appliquer un masquage supplémentaire à celui de l'ALU. Par exemple, pour gérer des opérations sur 14 bits, on effectue une opération sur 16 bits avec l'ALU, et on masque les 2 bits manquants avec l'ALU 4 bits. ===Une architecture bit-adressable=== Le CPU Burroughs B1700 était '''bit-adressable'''. En clair, il pouvait adresser la mémoire bit par bit, si nécessaire. Un avantage est que cela facilitait la traduction binaire des instructions machine, mais nous détaillerons cela plus bas. Un autre avantage est que cela permettait d'émuler des architectures dont la taille des registres/''byte''/mots était très différente. Et cet avantage mérite quelques explications immédiates. Le processeur était un CPU 24 bits, mais il pouvait émuler des processeurs 16 bits, 8 bits, 4 bits, 1 bit, 9 bits, ou toute autre valeur. Pour cela, le processeur lisait des mots de 24 bits et utilisant un ''barrel shifter'' pour sélectionner les bits adéquats. Par exemple, pour un accès de 16 bits, la donnée lue en mémoire était masquée de manière à ne garder que les 16 bits de poids faible. Le ''barrel shifter''/circuit de masquage était placé directement avant le bus mémoire, sur le trajet des données/instructions. Le ''barrel shifter'' était commandé par un registre ''Field Unit'', qui précisait quelle était la taille des données à charger. ===Des registres d’interfaçage mémoire adressables=== Pour communiquer avec la mémoire, il avait trois registres séparés : READ, WRITE, MAR. READ contient la donnée lue lors d'une lecture, WRITE est pour une donnée à écrire lors d’une écriture, MAR contient l'adresse à lire ou en cours de lecture. Il s'agit de registres d’interfaçage mémoire, les mêmes que ceux vus dans le chapitre sur le chemin de données du CPU. Une instruction LOAD est donc émulée en deux instructions machines : une instruction MOV pour copier l'adresse dans le registre READ, une instruction de lecture proprement dite. L'écriture demande d'ajouter une seconde instruction MOV pour copier la donnée à écrire, et la lecture est remplacée par une écriture. Le fait que ces registres d’interfaçage mémoire et IO soit adressable est peu commun, et c'est plus quelque chose qu'on attend d'un microcode que d'instructions machines. D'ailleurs, l'implémentation des instructions LOAD/STORE ressemble à ce qui est effectué par le séquenceur d'un CPU. Le séquenceur est d'ordinaire celui qui séquence la copie des adresses/données dans les registres d’interfaçage, puis lance la lecture/écriture. Ici, c'est réalisé via des instructions machines. Il faut bien comprendre que les registres d’interfaçage mémoire ont été promus au rang de registres architecturaux, sur ce processeur. De même, pour communiquer avec les entrées-sorties, il disposait de deux registres CMND et DATA : CMND pour envoyer une commande à une entrée-sortie, DATA pour lire ou écrire une donnée. Il y avait aussi un registre U, qui mémorisait une donnée lue depuis le lecteur de cassette. ===Le code machine du Burroughs B1700=== Le processeur utilise des instructions de 16 bits de long, qui sont copiées dans registre d'instruction M de 16 bits. Le ''program counter'' est appelé le registre A. Les instructions du processeur font 16 bits de long, et elles sont alignées en RAM sur 16 bits, ce qui fait que les 4 bits de poids fort de ce registre valent 0, seuls les 14 bits de poids fort sont utiles. Les adresses mentionnées plus haut, qui adressent les micro-opérations en mémoire RAM, sont des adresses de mots de 16 bits, pas les adresses de bit individuels. Fait intéressant, il était possible de faire un OU logique entre l'instruction lue et un registre du processeur, le résultat étant mémorisé dans le registre M. Cela permettait de faire du code automodifiant, pour émuler des modes d'adressage complexe. Cela permettait par exemple de remplacer une adresse dans une micro-opération. L'adresse est alors surimposée avec un OU dans le champ d'adresse, alors que le reste de l'instruction n'est pas modifié. Afin de gérer les interruptions, ces dernières étaient partiellement détectées en logiciel. Une interruption doit être prise en compte à la toute fin de l'exécution d'une instruction source, quand celle-ci a fini son travail. Mais vu qu'ici, les instructions sont émulées avec des séries d'instructions cibles, il y a un problème : il est possible d'interrompre une instruction émulée en plein milieu. Pour éviter cela, le processeur ne déclenche pas les interruptions matérielles immédiatement, elles sont mises en attente. Une instruction dédiée vérifie si une interruption est en attente, et l'exécute alors cas échéant. L'instruction en question est utilisée en dernier, dans la suite d'instruction qui émule l'instruction source. Les Burroughs B1700 intégrait une pile d'adresse de retour, capable de mémoriser 16 adresses de 24 bits. Il disposait de 4 registres généraux, nommés X, Y, T et L, tous de 24 bits. Un point important est que le registre T avait des possibilités d’extraction spécifiques. Il était possible d'extraire N bits, placés n'importe où dans ce registre, et de les copier dans un autre registre. Le CPU supportait une '''instruction EXTRACT''' pour ça. Nous verrons comment cette possibilité est exploitée pour la traduction binaire dans ce qui suit. ===La traduction binaire sur le Burroughs B1700=== Les instructions source peuvent faire 16 bit avec tel jeu d'instruction, 8 bits avec un autre, 32 bits sur un autre, etc. Elles peuvent aussi être de longueur fixe ou variable ! Le B1700 utilisait une architecture bit-adressable pour gérer ces contraintes. En effet, avec un CPU bit-adressable, les contraintes d'alignement des instructions disparaissent ! Dans ce qui suit, on suppose que l'instruction est composée de '''champs''' : l'opcode, les opérandes, les adresses absolues, les constantes immédiates, les numéros de registre, etc. L'idée est que l'instruction source est chargée dans le registre T, puis découpée en champs, qui sont copiés dans les registres du processeur ou envoyés au microcode. Je rappelle que le registre T est relié à un ''barrel shifter'', différent au précédent, qui permettait de masquer, décaler et extraire une donnée de ce registre. Et il y a la possibilité d'extraire une suite de bit de ce registre T, pour la copier dans un autre registre. Par exemple, prenons une instruction composée d'un opcode, d'une constante immédiate et d'une adresse immédiate. Premièrement, elle est chargée dans le registre T. Deuxièmement, la constante immédiate est extraite du registre T et copiée dans le registre X. Troisièmement, l'adresse est extraite et est copiée dans le registre d’interfaçage mémoire, une lecture est démarrée, et l'opérande lu est copiée dans le registre Y. Enfin, l'instruction équivalente à l'opcode est exécutée par le microcode. Le tout prend plusieurs instructions : une première pour lire l'opcode, une seconde pour lire le premier opérande, une troisième pour lire la seconde opérande, une quatrième pour tout exécuter. Notons que suivant la taille des champs, on peut en charger plusieurs à la fois. Par exemple, imaginons qu'on ait chargé 24 bits, contenant un opcode de 8 bits et une constante immédiate de 16 bits : le registre T est découpé en un opcode et une constante, avec deux instructions EXTRACT. L'architecture est bit-adressable, car elle gère des champs qui ne font pas forcément 8 ou 16 bits. Par exemple, certains jeux d'instructions ont des opcodes codés sur 6 ou 7 bits, voire 9-10 bits. Utiliser une architecture bit-adressable règle ce problème d'alignement. On peut lire un champ depuis la mémoire RAM en précisant l'adresse de son premier bit, pas besoin de gérer des accès non-alignés. Par exemple, prenons une instruction source avec un opcode de 7 bits, suivi par une adresse de 12 bits et une constante immédiate de 15 bits. On peut charger 7 + 12 bits en un premier accès, pour extraite l'opcode et l'adresse, suivi par un second accès de 15 bits pour extraire la constante immédiate. Pour charger un champ, il faut connaitre plusieurs informations : l'adresse de son premier bit, sa taille. Pour cela, le B1700 contient deux registres, nommés ''Field Adress'' (FA) et ''Field Length'' (FL), qui indiquent l'adresse du premier bit et la taille de l'instruction à charger. Le premier fait 24 bits, ce qui permet de gérer des adresses de 24 bits. N'oubliez pas que le processeur est bit-adressable, ce qui permet d'adresser 16 Mébi-bits, soit 2 mébioctets. Le registre pour la taille fait lui 16 bits, ce qui permet de gérer des instructions de 65536 bits, soit 8192 octets ! Leur nom commence par ''field'', car le processeur peut gérer des instructions de taille variable et/ou plus longues que 24 bits. Dans ce cas, le chargement de l'instruction se fait champ par champ. Pour donner un exemple, prenons une instruction composée de 20 champs de 12 bits chacun. Les champs sont chargés un par un dans le registre T. Le registre FA est alors initialisé avec l'adresse de l'instruction, il est incrémenté de 12 à chaque cycle. Le registre FL est initialisé avec la taille de l'instruction, soit 20 × 12 = 240 bits, et il est décrémenté de 16 à chaque cycle. Précisons cependant que la taille d'un champ peut être changée d'un cycle à l'autre. Par exemple, le processeur peut charger un opcode de 8 bits, l'interpréter, détecter que l'instruction demande ensuite de charger une adresse de 16 bits. Dans ce cas, le registre FL est immédiatement altéré pour charger 16 bits, au lieu des 8 bits de l'opcode immédiatement précédent. ===Le Burroughs B1726 : le mal-nommé ''split-level control store''=== Le Burroughs B1700 avait plusieurs modèles : le B1726, le B1710 et le B1800. * Le B1710 plaquait le traducteur binaire en mémoire RAM. * Le B1800 avait ajouté un cache dédié au code du traducteur binaire, afin d'améliorer les performances. * Le B1726 utilisait un ''local store'' dédié au traducteur binaire. Le B1726 peut exécuter le traducteur binaire soit depuis le ''local store'', soit depuis la mémoire RAM avec une pénalité en termes de performances. La documentation appelle cela un '''''split-level control store''''', mais le terme est trompeur : la documentation qualifie de microcode le langage machine du Burroughs B1700, ce qui est à l'origine de beaucoup de confusions. Une autre source de confusion est que ce langage machine a quelques propriétés qu'on pourrait attendre d'un microcode, mais certaines sont manquantes. Par exemple, il peut adresser les registres d’interfaçage mémoire, mais c'est parce que ce sont des registres architecturaux sur ce processeur. Ses instructions n'encodent pas de signaux de commande, par contre, elles font 16 bits et adressent les opérandes de manière implicite. Le ''local store'' mémorisait au maximum 2048 instructions machine, ce qui fait que les 2048 premières "adresses" correspondaient au ''local store''. Les suivantes étaient des adresses de mémoire RAM qui contenaient le reste du traducteur binaire. En pratique, l'adresse qui séparait les deux était configurable ! On pouvait limiter la taille du ''local store'' à un multiple de 32 instructions ! Les adresses restantes étaient alors réattribuées à la RAM. Par exemple, on pouvait attribuer 64 adresses au ''local store'', les 2048 - 64 adresses restantes étaient alors attribuées à la mémoire RAM. Un registre limite, nommé TDPM, indiquait quelle était l'adresse de démarcation entre les deux. Le programmeur devait découper le traducteur binaire en segments, certains étant placés dans le ''local store'', les autres étant en mémoire RAM. Mieux que ça, le processeur autorisait d'utiliser la technique de l'''overlaying'' pour le microcode ! Il y avait même une fonction OVERLAY dédiée pour ! OVERLAY avait besoin de trois opérandes : la taille du segment à copier, son adresse source en mémoire RAM, son adresse de destination dans la micro-SRAM. L'interpréteur réservait un "segment" en mémoire RAM, pour les données, dont le processeur connaissait la position. En dehors de ce segment, il n'y a que des instructions machines à traduire, et le traducteur binaire. Le CPU incorporait pour cela deux registres, un pour l'adresse de base du segment de données, un autre pour son adresse de fin. Il incorporait une protection mémoire limitée sur ce segment, à savoir que seul l'interpréteur pouvait lire ou écrire dans ce segment. ==Les jeux d'instructions dédiés à un langage de programmation== De rares processeurs sont conçus pour un langage de programmation en particulier. On appelle ces processeurs, conçus pour des besoins particuliers, des '''processeurs dédiés'''. Par exemple, les fameux ''Burrough E-mode'' B5000/B6000/B7000 étaient spécialement conçus pour exécuter de l'ALGOL-60. Leurs cousins B2000/B3000/B4000 étaient eux conçus pour le COBOL. Des langages fonctionnels ont aussi eu droit à leurs processeurs dédiés. Le prolog en est un bel exemple, avec les superordinateurs de 5ème génération qui lui étaient dédié. On peut aussi citer les machines LISP, dédiés au langage LISP, qui datent des années 1970. Elles étaient capables d’exécuter certaines fonctions de base du langage directement dans leurs circuits : elles possédaient notamment un ''garbage collector'' câblé dans ses circuits ainsi que des instructions machines supportant un typage déterminé à l’exécution. Les processeurs dédiés ont eu leur heure de gloire au début de l'informatique, à une époque où les langages de haut niveau venaient d'être inventés. À cette époque, les compilateurs n'étaient pas performants et ne savaient pas bien optimiser le code machine. Il était alors rationnel, pour l'époque, de rapprocher le code machine cible et le langage de programmation de haut niveau. De nombreuses architectures dédiés ont ainsi été inventées, avant que les concepteurs se rendent compte des défauts de cette approche. Les défauts en question ne sont pas nombreux, mais assez simples à comprendre. Premièrement, elles sont très rapides pour un langage de programmation en particulier, mais sont assez mauvaises pour les autres, d'où un problème de "compatibilité". Ajoutons à cela que les langages de programmation peuvent évoluer, devenir de moins en moins populaires/utilisés, ce qui rend la création d'architectures généralistes plus pertinente. Enfin, les architectures dédiées sont évidemment des processeurs CISC, pour implémenter les nombreuses fonctionnalités des langages évolués. Et les défauts des CISC sont assez rédhibitoires à l'heure actuelle. À l'heure actuelle, les algorithmes des compilateurs se sont améliorés et savent nettement mieux utiliser le matériel. Ils produisent du code machine efficace, ce qui rend les architecture dédiées bien moins intéressantes. Si on ajoute les défauts de ces architectures dédiées, par étonnant que les architectures dédiées aient presque disparues. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'accélération matérielle de la virtualisation | prevText=L'accélération matérielle de la virtualisation | next=Le matériel réseau | nextText=Le matériel réseau }} </noinclude> nw3n8hw7j3a5l5p06jy2r2ltjpm0hy0 764070 764067 2026-04-19T22:31:07Z Mewtow 31375 /* La traduction binaire sur les Burroughs B1700 */ 764070 wikitext text/x-wiki De nos jours, la majorité des programmeurs programment dans des langages de haut niveau. Il est très rare pour eux d'avoir à utiliser de l'assembleur, et encore moins un langage machine. Les programmes écrits dans un langage de haut niveau sont traduits en langage machine par un logiciel appelé le compilateur. [[File:Compilation.PNG|centre|vignette|upright=2.5|Compilateur : principe.]] Il se trouve que quelques rares processeurs sont conçus pour faciliter le travail du compilateur. Et ce sont ces processeurs que nous allons voir dans ce qui suit. Ils représentent une catégorie de jeux d'instruction à part, qui n'a pas vraiment de nom. ==La compilation et l’interprétation== Un compilateur traduit donc du code source, écrit dans un langage de haut niveau, vers du langage machine. La traduction est rarement directe. En général, le compilateur traduit le code source en assembleur, qui est lui-même traduit en langage machine. L'assembleur est une représentation textuelle du langage machine alors que le code machine est du binaire exécutable par le processeur. La traduction se fait donc en deux étapes, la '''compilation''' proprement dite et l''''assemblage''', réalisées respectivement par un compilateur et un assembleur. Il faut parfois rajouter une troisième phase d'édition des liens, que nous passons volontairement sous silence. {| |[[File:Code C.png|vignette|200px|Le code source, ici en C.]] |[[File:Assem.png|vignette|250px|Le code assembleur.]] |[[File:Binary file - hello world (C programming).png|vignette|150px|Le code machine.]] |} ===L'interprétation et la compilation à la volée=== Une alternative à la compilation est l''''interprétation''', qui transforme le code source en code machine à la volée. Avec l'interprétation, le code source est passé à un logiciel appelé l''''interpréteur''', qui exécute le code source ligne de code par ligne de code. Une ligne de code est traduite en code machine, qui est exécutée, puis l'interpréteur passe à la ligne suivante. [[File:Kaantotulkkaus.png|centre|vignette|upright=3|Compilation versus interprétation.]] Un défaut de l'interprétation est son cout en performance, qui est assez important. Aussi, l'interprétation stricte a évolué vers un hybride entre interprétation et compilation. L'idée est qu'une partie peu importante du code source est interprétée, alors que le code important est compilé juste avant d'être exécuté. Pour le dire autrement, le code source est partiellement compilé à la volée, juste avant son exécution. Aussi, on parle de '''compilation à la volée'''. Le terme en anglais est ''Just In Time compilation'', abrévié en JIT. En soi, le JIT est associé à de l'interprétation, les JIT sont en réalité des hybrides interpréteurs-compilateurs. Un point important est que compiler du code à la volée est assez lourd, cela a un cout en performance. Par contre, le code compilé s'exécute plus vite que du code interprété. Si on exécute le code compilé une seule fois, le cout de la compilation l'emporte sur le gain à l'exécution. Mais le code compilé est réutilisé autant de fois que nécessaire. Le gain à l'exécution est donc multiplié par le nombre d'exécutions, alors que le cout de la compilation est répartit sur plusieurs exécutions, il est amorti. Les portions du code sont choisies de manière à avoir un gain en performance maximal. L'idée générale est que le code exécuté très souvent est compilé, alors que du code exécuté pas souvent est interprété. Typiquement, les boucles les plus souvent exécutées sont compilées, le code exécuté une seule fois est interprété. ===Les langages intermédiaires=== Les compilateurs modernes passent par un '''langage intermédiaire''' pour faire la transformation en code machine. Le compilateur traduit le langage de haut niveau en langage intermédiaire, puis traduit le langage intermédiaire en code machine cible. Le langage intermédiaire est parfois appelé le '''''bytecode''''', et ce terme recouvre aussi le code écrit avec du ''bytecode''. L'interprétation aussi peut passer par l'intermédiaire d'un un ''bytecode'', ce n'est pas limité aux compilateurs proprement dit. {| |[[File:Intermediate representation scheme.png|centre|vignette|upright=1.5|Représentation intermédiaire d'un compilateur.]] |[[File:Code interpreter scheme.png|centre|vignette|upright=1.5|Fonctionnement d'un interpréteur.]] |} Faire ainsi a de nombreux avantages, le principal étant d'avoir un compilateur capable de traduire un langage de haut niveau vers plusieurs jeux d'instructions différents. Cela permet d'avoir, par exemple, un compilateur qui traduit du C soit en code machine x86, soit en code machine pour un CPU ARM, soit pour un CPU POWERPC, etc. [[File:Compiler design.svg|centre|vignette|upright=3|Compiler design]] Pour l'interprétation, l'usage d'un ''bytecode'' a d'autres avantages. Pour l'exploiter, il faut distribuer non pas le code source, mais le ''bytecode''. En clair, l'interpréteur ne prend pas en entrée le code source, mais du ''bytecode'' déjà compilé. La traduction est alors beaucoup plus simple, car le code source a déjà été partiellement compilé, par la traduction en ''bytecode''. Interpréter du code source est en effet assez compliqué : il faut effectuer des étapes d'analyse lexicale, sémantique, et bien d'autres. Avec du ''bytecode'', ces étapes ont été réalisées lors de la compilation du ''bytecode'', l'interpréteur a alors peut de choses à faire. L'interprétation du ''bytecode'' se fait instruction par instruction, au niveau du ''bytecode''. Une instruction du ''bytecode'' est traduite en une instruction machine équivalente, qui est exécutée. Si l'instruction est un peu complexe, il exécute une fonction/procédure qui fait la même chose. Pour résumer, le ''bytecode'' est ensuite traduit à la volée et exécuté instruction par instruction par un logiciel appelé l'interpréteur. [[File:Control table.png|centre|vignette|upright=2|Control table]] Le langage intermédiaire peut être vu comme l'assembleur d'un processeur, n'existe pas forcément dans la réalité, mais dont le jeu d'instruction est décrit en détail. Le processeur en question est appelé une '''machine abstraite''', ou encore une ''machine virtuelle''. Nous utiliserons le terme de machine abstraite dans ce qui suit. La machine abstraite n'est pas la même suivant que l'on cible la compilation ou l'interprétation/JIT. Pour l'interprétation/JIT, la machine abstraite est souvent une machine à pile, car cela simplifie grandement la traduction en code machine final. L'interprétation demande que la traduction du ''bytecode'' en code machine soit la plus simple possible. Et cela demande de prendre en compte pas mal de détails. Par exemple, les différents jeux d'instruction existants n'ont pas le même nombre de registres, ce qui pose problème lors de la traduction du ''bytecode''. Lors de la transformation en code machine, un algorithme d'allocation de registres se débrouille pour traduire le code intermédiaire en code qui utilise un nombre limité de registres. Pour améliorer cette allocation de registres, il y a deux solutions niveau ''bytecode'' : soit utiliser un nombre illimité de registres, soit utiliser une machine à pile. Pour les machines à pile, il existe un algorithme simple et rapide pour traduire un code écrit pour une machine à pile en un code écrit pour un processeur avec des registres, qui se débrouille pas trop mal pour allouer efficacement les registres. C'est un avantage assez important pour les langages interprétés. ==Les processeurs qui exécutent du bytecode== Le ''bytecode'' est un code machine, ce qui signifie qu'il peut en théorie s'exécuter sur un processeur qui implémente le jeu d’instruction associé. Si le jeu d'instruction d'un ''bytecode'' est souvent une description censée être fictive, elle n'en reste pas moins un jeu d'instruction et des caractéristiques précises, qu'on peut l'implémenter en matériel ! Néanmoins, tous les ''bytecode'' ne sont pas égaux de ce point de vue. Certains sont faciles à implémenter en matériel, d'autres non. Les ''bytecodes'' implémentés en matériel sont ceux dont les machines abstraites sont des machines à pile. Un premier exemple est celui des processeurs Pascal MicroEngine, qui exécutaient directement le bytecode du langage Pascal, le fameux UCSD P-code. Un second exemple est la '''machine SECD''', qui sert de langage intermédiaire pour certains compilateurs de langages fonctionnels. Elle a été implémentée en matériel par plusieurs équipes, notamment par les chercheurs de l'université de Calgary en 1989. Dans le même genre, quelques processeurs simples étaient capables d’exécuter directement le ''bytecode'' du langage FORTH. Le FORTH, un des premiers langages à pile de haut niveau, possède de nombreuses implémentations hardware et est un des rares langages de haut niveau dont le ''bytecode'' a été utilisé comme langage machine sur certains processeurs. Par exemple, on peut citer le processeur FC16, capable d’exécuter nativement du ''bytecode'' FORTH. ===Les processeurs Java=== Le cas le plus impressionnant est celui de la machine virtuelle Java, qui est un design de processeur comme un autre. En temps normal, le ''bytecode'' Java est compilé ou interprété, mais certains processeurs exécutaient du ''bytecode'' Java directement, sans interprétation ni compilation. Ils sont appelés des '''processeurs Java'''. On peut citer les processeurs ARM disposant de l'extension Jazelle, du JEMCore et du aJ-100 de aJile Systems, le picoJava II de Sun, et quelques autres. Il y a aussi eu quelques projets de recherche ou processeurs open source, comme Komodo, jamuth, le ''Java Optimized Processor'' et quelques autres projets du même genre. L'implémentation de la machine virtuelle Java n'était cependant pas complète. Quelques instructions complexes n'étaient pas gérées par le processeur et devaient être émulées en logiciel. Mais la grosse majorité des instructions l'était. Les instructions non-supportées étaient émulées avec la méthode ''trap and emulate'', à savoir qu'un opcode inconnu déclenchait une exception matérielle ''opcode invalide'', dont la routine pouvait émuler les instructions qui doivent l'être. Le processeur aJ-100 de aJile est capable d'exécuter la quasi-totalité des instructions de la JVM Java, seules deux instructions faisant exception. Les '''accélérateurs Java''' sont une solution intermédiaire, qui permet de traduire à la volée du ''bytecode'' Java en instructions RISC, exécutées sur un autre processeur. Il s'agit formellement de traduction binaire, mais je préfère en parler ici. L'idée est de combiner un processeur RISC avec un coprocesseur Java. Le coprocesseur lit le ''bytecode'' Java et le traduit en instructions RISC, ces dernières étant exécutées sur le processeur RISC. Le JA108 de Nozomi était un coprocesseur de ce genre, au même titre que le coprocesseur JSTAR de JEDI. L'extension Jazelle de certains CPU ARM fonctionnait vraisemblablement sur un principe similaire, sauf que le coprocesseur était intégré dans le processeur ARM, entre le cache d'instruction et le décodeur d'instructions ARM. L'intérêt des processeurs Java n'est pas qu'une question de performance. Certes, ils permettent d’exécuter plus vite les programmes compilés en ''bytecode'', comme des programmes Java pour la JVM Java. Mais l'intérêt est aussi de faire des économies de mémoire RAM et ROM. Je rappelle que la machine virtuelle Java est une machine à pile, ce qui fait que sa densité de code est excellente. Les programmes en ''bytecode'' Java sont donc très petits, et prennent peu de place en mémoire ROM. De plus, utiliser un processeur Java permet de se passer d'interpréteur ou de JIT Java, ce qui économise encore plus de mémoire. ===Transformer un processeur RISC en machine à pile=== La plupart des processeurs Java utilisent en interne une architecture à registre généraux, qui émule une machine à registre en microcode. Quelques registres sont réservés pour l'état de la machine virtuelle, avec au minimum un registre réservé pour le pointeur de pile, un autre pour un pointeur vers un ''pool'' de constantes, et quelques autres. D'autres registres servent à mémoriser le haut de la pile, à savoir le sommet de la pile et quelques opérandes situées juste en-dessous. Par exemple, le processeur picoJava II gardait les 64 opérandes au sommet de la pile dans 64 registres généraux, séparés des registres pour les pointeurs et l'état de la JVM. Une optimisation possible, envisagée sur les processeurs picoJava, était une sorte de macro-fusion sous stéroïde. L'idée était de fusionner certaines séries d'instructions Java en une seule instruction machine. Typiquement, pour faire une opération arithmétique, une machine à pile empile deux opérandes et exécute une opération, ce qui prend trois instructions machine. La spécification picoJava 1 a proposé d'envoyer directement la seconde opérande en entrée de l'ALU, ce qui fusionnait l'empilement de la seconde opérande et l'opération arithmétique. Le processeur picoJava II va plus loin et fusionne les trois instructions en une seule instruction RISC, agissant sur des registres. Mais la majorité des processeurs Java ne faisaient pas cela, car cela complexifie grandement le décodeur. Les processeurs ''bigfoot'' de DCT utilisaient un système différent pour émuler une pile avec des registres. Leur banc de registre de 64 bits était coupé en deux : les 16 premiers registres étaient utilisés pour la pile, les 48 suivants étaient utilisés pour autre chose. L'idée est que le registre 0 mémorise un emplacement vide, le registre 1 contient le sommet de la pile, le registre 2 l'opérande sous le sommet de la pile, et ainsi de suite. Les 15 premières opérandes de la pile sont donc mémorisées dans les registres. Les instructions écrivent leur résultat dans le registre adéquat, puis tous les registres sont décalés pour que le résultat passe dans le registre R1. Par exemple, les instructions PUSH chargent une opérande dans le registre 0, puis tous les registres sont décalés de un rang, pour que l'opérande chargée soit dans le registre 1. Le registre 0 devient alors le registre numéro 1, le registre 1 devient le 2, et ainsi de suite. Par contre, les opérations arithmétiques font autrement. Une instruction d'addition, par exemple, lit les opérandes dans le registre 1 et 2, puis place son résultat dans le registre 2 (les deux opérandes sont dépilées). Puis, les registres sont décalés d'un rang pour que ce registre 2 devienne le 1. Les registres ne sont en réalité pas décalés, il n'y a pas de transferts entre registres. A la place, les 16 premiers registres utilisaient une forme limitée de renommage de registres, similaire au banc de registre tournant utilisé sur les processeurs EPIC comme l'Itanium. Les numéros de registres sont renommés, décalés, à chaque fois qu'on empile ou dépile une opérande. Pour faire le renommage de registres, processeur contient un ''register stack counter'' de 4 bits, qui est incrémenté à chaque fois qu'on empile une donnée, et est décrémenté quand on dépile une donnée. Quand on veut accéder à un opérande, le numéro de registre architectural associé est traduit en numéro de registre physique, en additionnant le ''register stack counter'' (modulo 16, pour rester sur 4 bits). ==L'accélération de l'interprétation/JIT : le ''Thumb-EE'' d'ARM== Quelques processeurs ont ajouté des instructions pour faciliter le travail des interpréteurs/JIT. Par exemple, en 2005, ARM a ajouté le mode ''Thumb Execution Environment'', qui faisait cela. Il reprenait le jeu d'instruction compact ''thumb'' et ajoutait quelques instructions et modifiait le comportement d'instructions existantes. Le jeu d'instruction ''Thumb-EE'' a été ajouté sur les CPU ARM en 2005, mais a été déprécié en 2011, par manque d'utilité. Mais la tentative mérite qu'on s'attarde dessus. Les CPU de gamme M, déstinés à l'embarqué, n'ont jamais supporté ''Thumb-EE'', vu que de tels processeurs ne sont pas conçus pour exécuter du code interprété/JIT. Le mode ''Thumb-EE'' est un mode d'exécution, séparé de l'ARM normal. Ainsi, une instruction ''Thumb-EE'' et une instruction ARM peuvent avoir le même encodage en binaire, mais se comporter différemment. Le processeur est à tout moment soit en mode ''Thumb-EE'', soit en mode ARM normal, ce qui précise comment ces instructions doivent se comporter. Des instructions ont été ajoutées par ''Thumb-EE'', pour qu'il fasse son travail. L'entrée et la sortie du mode ''Thumb-EE'' se fait avec deux instructions : ENTERX et LEAVEX. Une fois en mode ''Thumb-EE'', le décodage des instructions ''thumb'' se fait avec les règles du ''Thumb-EE'', et non celles du ''thumb'' normal. Une première différence entre ''thumb'' normal et ''Thumb-EE'' est l'ajout de ce qui s'appelle un ''null check'' pour les instructions mémoire. Le ''null check'' est utilisé pour les instructions en mode d'adressage "base + indice" ou "Base + décalage", ou tout autre mode d'adressage avec un registre de base utilisés dans des calculs d'adresse. L'idée est de vérifier si l'adresse de base vaut zéro ou non. Si c'est le cas, le processeur lève une exception matérielle, qui est traitée par l'interprétation ou le compilateur JIT. Une telle situation est en effet signe d'une erreur d'adressage mémoire, qui doit être traitée par l'interpréteur. Typiquement, c'est signe qu'un pointeur n'a pas bien été initialisé, ce qui peut arriver avec du code interprété ou JIT. Les interpréteurs/JIT traitent généralement la situation en ajoutant un test avant toute instruction mémoire, pour vérifier si le pointeur accédé vaut zéro ou non. Avec ''Thumb-EE'', pas besoin d'ajouter les tests en questions : ils sont réalisés automatiquement lors de chaque instruction mémoire. : Les registres pour la pile, comme le pointeur de pile et le pointeur de ''frame'', sont aussi concernés. L'instruction CHKA vérifie que les accès à un tableau ne débordent pas en dehors du tableau en question. Par exemple, pour un tableau de 1024 éléments, elle vérifie si l'indice est compris entre 0 et 1024. Pour cela, elle vérifie si l'indice de tableau est bien dans l'intervalle adéquat et lève une exception matérielle si ce n'est pas le cas. Son utilité se comprend quand on sait que tous les accès à un tableau sont vérifiés par l'interpréteur. L'interpréteur est censé ajouter des instructions pour vérifier les indices, à savoir deux ou trois branchements. Avec CHKA, le test se fait en une seule instruction. D'autres modifications mineures de l'encodage des instructions sont aussi présentes, ainsi que des modifications pour les instructions LOAD/STORE. Précisément, dans les modes "base + indice" et "base + décalage", le décalage et l'indice subissent maintenant des décalages. L'idée est de simplifier le calcul d'adresse. De quoi économiser quelques instructions lors des calculs d'adresse, ce qui facilite le travail de l'interpréteur/JIT. Et des instructions LOAD/STORE précises voient leur comportement modifié pour faciliter la gestion de la pile de Java ou l'exécution des méthodes locales. Pour faciliter le travail de l'interpréteur, deux instructions HBP et HBLP ont été ajoutées. Elles branchent vers une fonction qui gère les exceptions logicielles (une fonctionnalité présente dans de nombreux langages, comme Java). La première effectue un branchement vers la fonction en question, la seconde sauvegarde l'adresse de retour avant de faire ce branchement. Les deux peuvent brancher vers 256 fonctions pré-déterminées, sans avoir à présenter leur adresse. ==La traduction binaire accélérée par le matériel== Plus haut, nous avons surtout parlé des compilateurs et des interpréteurs. Cependant, nous devons aussi parler de la '''traduction binaire'''. Elle traduit un programme écrit dans un code machine vers un autre code machine. Par exemple, elle traduit un programme compilé pour un CPU x86 vers un code machine ARM. En général, le système d'exploitation est généralement compilé pour le jeu d'instruction natif, mais il exécute des applications prévues pour le x86. Les applications sont traduites par le système d'exploitation, avant d'exécuter le code traduit. : Nous parlerons dans la suite de code machine source et de code machine cible, pour parler respectivement du code à traduire et du code obtenu après traduction. Idem avec d'autres termes comme architecture cible/source, ou jeu d'instruction cible/source. La traduction binaire est surtout utilisée pour des questions d'émulation ou de compatibilité. Par exemple, lorsque les Macintosh sont passés de processeurs Power PC vers des processeurs x86, le système d'exploitation Mac OS utilisait la traduction binaire pour convertir les anciennes applications Power PC vers du code x86. Les utilisateurs n'y ont vu que du feu. Le système de traduction binaire était appelé Rosetta 1. Par la suite, lors de la transition de processeurs x86 vers des processeurs Apple, Rosetta 2 a vu le jour. La traduction binaire a surtout été utilisée pour traduire du code x86 vers un autre jeu d'instruction. Il faut dire que le x86 est le jeu d'instruction dominant. De nombreuses entreprises ont eu pour ambition de briser l'hégémonie du x86 sur PC, en remplaçant le x86 par un jeu d'instruction plus performant, tout en gardant une compatibilité maximale. La traduction binaire était la seule solution pratique. Elle était le plus souvent intégralement réalisée en logiciel, comme c'était le cas sur les architectures Itanium avec le ''IA-32 Execution Layer''. ===Les généralités sur la traduction binaire assistée en matériel=== L'architecture source est presque tout le temps une architecture CISC, assez ancienne, qu'on souhaite émuler. Le choix de l'architecture cible se porte souvent sur une architecture VLIW, fort différente de l'architecture source, et ce pour obtenir une économie de matériel conséquente. Pour s'exécuter rapidement, le code traduit doit exploiter le parallélisme d'instruction, à savoir exécuter plusieurs instructions en même temps dans des unités de calcul séparées. Il est possible d'utiliser un processeur superscalaire avec exécution dans le désordre pour cela, mais au prix d'un cout important en transistors. Alors qu'en utilisant un CPU VLIW, c'est le traducteur binaire qui fait tout le travail d'extraction du parallélisme d'instruction. Et après tout, quitte à avoir un traducteur binaire, autant lui refiler le boulot d'optimisation. Compiler du code à la volée est certes assez lourd, mais qu'il existe des algorithmes efficaces pour regrouper des instructions indépendantes dans une seule instruction VLIW. Au passage, le processeur VLIW a plus de registres que le processeur source. Cela permet de faire du renommage de registres directement en logiciel. Le traducteur binaire n'hésite pas à changer les noms de registres entre instructions source et instruction VLIW cible, afin de supprimer des dépendances de données. En conséquence, cela ouvre des opportunités de parallélisme, qui sont exploitées lors du regroupement des opérations en instructions VLIW. Vous pourriez penser que le choix d'un CPU VLIW pour émuler du CISC est tout sauf optimal. Vous devez penser que la traduction binaire est d'autant plus simple que l'architecture source et cibles sont semblables. Dans les faits, ce n'est pas tellement traduire les instructions qui pose problème, mais plus la gestion du registre d'état, des exceptions matérielles, de la mémoire virtuelle, la différence entre gros-boutisme et petit-boutisme, de même que des différences d'adressage pour les périphériques. Le premier problème est la gestion des conditions, notamment en présence d'un registre d'état. En pratique, la traduction binaire s'utilise pour traduire du code CISC vers du code RISC ou VLIW. Les architectures source ont donc un registre d'état, qui est mis à jour non seulement par des instructions de test, mais aussi des instructions arithmétiques. L'architecture cible n'a elle pas de registre d'état, mais des registres à prédicats. La traduction de l'un vers l'autre est alors quelque peu compliquée. Et elle est d'autant plus compliquée que le registre d'état est la source de dépendances d'instruction implicites, qui réduisent les performances. Par exemple, si une instruction arithmétique modifie le registre d'état, cela peut impacter l'exécution d'une instruction ultérieure, qui lit ce registre d'état. Pour éliminer ces fausses dépendances, le traducteur binaire doit renommer ce registre en logiciel et détecter les dépendances utiles. Et le compilateur doit gérer les cas où il y a beaucoup de distance entre les deux, voire les cas où l'instruction dépendante n'a pas encore été analysée par le compilateur. Un autre problème est lié à la gestion des exceptions matérielles, et précisément des exceptions précises. Pour rappel, une instruction VLIW regroupe plusieurs opérations, opérations qui correspondraient à une instruction machine sur un CPU pas VLIW. En conséquence, plusieurs instructions du langage machine "source" sont regroupées en une seule instruction VLIW. Et il faut tenir compte du cas où une instruction source lève une exception. Dans le code VLIW, cela signifie qu'une opération lève une exception, et il faut annuler partiellement l'instruction VLIW associée. Et par partiellement, on veut dire que seules les opérations suivantes dans l'ordre du programme source doivent être annulées, puis ré-exécutées. Et c'est un sacré casse-tête ! Les processeurs VLIW utilisent comme solution le mécanisme d'exceptions différées des processeurs EPIC, vu il y a quelques chapitres. Pour résumer, le code est exécuté par blocs d'instruction, délimités par des branchements ou tout autre limite/barrière pertinente dans le code. Un bloc de code est compilé en deux versions : une version rapide sans exceptions matérielles, une version lente qui gère les exceptions précises. La version rapide s'exécute sans exécuter les exceptions. Cependant, les exceptions matérielles sont enregistrées, pour être prises en compte à la toute fin du bloc de code. La version rapide est exécutée en premier et elle mémorise si une exception matérielle a eu lieu. Puis une instruction vérifie si une exception a eu lieu et décide quoi faire. Si aucune exception n'a eu lieu, elle passe à la suite du programme, les résultats du bloc de code sont définitivement acceptés. Mais si une exception a eu lieu, tout est annulé. Le processeur est remis dans l'état initial, puis le code est ré-exécuté instruction par instruction de manière à gérer l'exception correctement. ===La traduction par pré-décodage=== Une première solution serait de faire la traduction binaire dans le cache d'instruction. Lors d'un défaut de cache, le code chargé depuis la RAM est traduit en code machine cible. Et c'est ce code machine cible qui est mémorisé dans le cache d'instruction et exécuté par le processeur. La technique marche sur le papier et n'est qu'une amélioration des techniques de pré-décodage vues il y a quelques chapitres. Cependant, le code obtenu est une traduction assez basique, qui n'incorpore pas d'optimisations dignes de ce nom. De plus, elle gère mal le cas où la taille du code source et cible sont potentiellement très différentes. Concrètement, elle est surtout utile pour traduire du code d'un processeur RISC vers un autre processeur RISC, dont les tailles d'instructions sont similaires. Elle ne permet pas d'émuler plusieurs jeux d'instructions différents, le cout en matériel (un circuit de pré-décodage par jeu d'instruction) serait trop important. ===Le projet DAISY d'IBM=== De très rares processeurs étaient conçus pour accélérer cette traduction binaire, afin de garder de bonnes performances. Les premiers à avoir étudié l'idée étaient IBM, avec leur projet DAISY (''Dynamically Architected Instruction Set from Yorktown''). Le projet de base était de convertir à la volée du code compilé pour des CPU Power PC, vers du code VLIW. Les chercheurs d'IBM avaient développé un algorithme de traduction dynamique efficace, ainsi qu'un processeur VLIW disposant d'optimisations spécifiques à la traduction binaire dynamique, à la volée. De nombreuses idées de ce projet ont été reprises ou re-découvertes par la société Transmetta, avec ses processeurs Crusoe et Efficieon, puis par NVIDIA avec son projet Denver. Les ingénieurs de ce projet ont étudié la possibilité d'émuler plusieurs jeux d'instructions différents sur un même processeur, notamment le s390 d'IBM et le x86. En théorie, cela demande juste d'avoir plusieurs programmes de traduction binaire : un pour le x86, un autre pour le s390, éventuellement un autre pour le Power PC. Mais cela ne s'est pas concrétisé. Mais les ingénieurs ont étudié quelle pourrait être l'architecture VLIW idéale pour ça. Par exemple, il fallait des additions 3-opérandes pour simplifier les calculs d'adresse. Une difficulté était la gestion du registre d'état, dont les bits ne sont pas mis à jour de la même manière sur le x86 et le s390 ou le Power PC. La différence entre gros-boutisme et petit-boutisme était aussi un problème, de même que des différences d'adressage pour les périphériques. Pour que la traduction binaire soit efficace, le processeur VLIW intègre diverses optimisations, comme les exceptions différées et les branchements multi-voies. La principale est la suivante : seul le code des boucles ou fonctions exécutées fréquemment est traduit en code VLIW, le reste du code est interprété. En effet, traduire du code binaire prend plus de temps qu'une simple interprétation. Pour du code qui ne sera exécutée qu'une seule fois, il est plus rapide d'utiliser l'interprétation. Par contre, pour du code exécuté beaucoup de fois, le cout de la traduction binaire est amorti, dilué sur N exécutions, compensé par le gain en temps d'exécution de ce code traduit. Au final, cela permet de ne traduire que le code qui le mérite. La détection du code fréquemment exécuté est réalisée dans le cœur VLIW. L'unité de branchement mémorise les derniers branchements rencontrés et le nombre de fois qu'ils ont été exécutés. Si ils ont été exécutés un certain nombre de fois, l'unité de branchement lève une exception matérielle, qui invoque le traducteur binaire. La routine de cette exception est le traducteur binaire proprement dit. En clair, la traduction binaire est démarrée quand le processeur détecte qu'une fonction a été exécutée plus de N fois, via une exception matérielle dédiée. La mesure du nombre d'exécution d'un branchement se fait dans l'unité de calcul dédiée aux branchements, avec l'aide d'une sorte de ''branch adress buffer'' modifié. Pour rappel, le ''branch adress buffer'' mémorise le ''Program counter'' de chaque branchement récemment rencontré. Ici, chaque entrée du ''branch adress buffer'' est associée à un compteur incrémenté à chaque exécution du branchement. ===Les processeurs Crusoe et Efficieon de Transmetta=== Les processeurs Crusoe Et Efficeron sont deux processeurs VLIW produits par la société Transmetta, une société californienne rachetée par NVIDIA. Ils étaient conçus pour exécuter spécifiquement des programmes x86, système d'exploitation inclus. L'idée derrière ce projet était d'exécuter du code x86 sans que les applications, ni même le système d'exploitation et le BIOS soient au courant ! Les processeurs Transmetta étaient en réalité conçus pour exécuter un programme unique, le ''Code Morphing Software'' (CMS), qui traduisait le code x86 en code VLIW. Transmetta ne voulait pas que son processeur VLIW soit exploité directement, sans le CMS. Il n'avait pas rendu public de compilateur pour traduire du C vers du code VLIW, il n'avait pas donné la documentation du jeu d'instruction VLIW. Des efforts de rétro-ingénieurie, documentés sur le site realworldtech, ont cependant permis de comprendre comment étaient encodées les instructions du processeur Crusoe. Comme pour le projet Daisy, le ''Code Morphing Software'' utilisait à la fois interprétation et traduction binaire, selon les besoins. Les instructions sont interprétées lors de leurs premières exécutions, mais le CMS bascule sur de la traduction en code VLIW après un certain nombre d'exécution. Ainsi, les boucles souvent exécutées sont traduites en code VLIW, alors que le reste du code est interprété. Le choix du code à traduire est le fait du CMS, il utilise des heuristiques complexes pour, qui ne sont pas connues dans le détail. Le ''Code Morphing Software'' est mémorisé dans une EEPROM, ce qui en fait un ''firmware'' situé en dessous du BIOS. Le CMS démarre ensuite le BIOS, qui lui-même démarre le système d'exploitation, qui lui-même démarre les pilotes de périphériques et les programmes. Le CMS se réserve les 16 premiers mébioctets de l'espace d'adressage. L'EEPROM du CMS est mappée dedans, mais n'en utilise que la moitié. Le reste est utilisé comme cache, pour mémoriser le code VLIW traduit par le CMS. Les deux processeurs intégraient aussi deux mémoires SRAM, utilisées par le CMS, appelées la ''local program memory'' (LPM) et la ''local data memory'' (LDM). La première contient du code qui gére les interruptions, la mémoire virtuelle, les exceptions matérielles, les problèmes d'alignement mémoire, et quelques fonctions très fréquemment utilisées par le CMS. La seconde est de la mémoire RAM utilisée par ces fonctions de la ''local program memory''. Les mémoires caches L1 et L2 sont séparées de ces deux mémoires. Le processeur Crusoe et Efficieon étaient des processeurs VLIW très simples. Ils n'avaient même pas de MMU, ni de fonctionnalités importantes sur les CPU x86. Pour Crusoe, les instructions VLIW étaient encodées sur 64 ou 128 bits, et regroupaient entre 2 à 4 opérations. Les instructions LOAD/STORE ne supportaient pas d'adressage indicé, juste de l'adressage indirect à registre. Crusoe avait 5 unités de calcul : deux ALU entières, une unité LOAD/STORE, une unité de branchement, une FPU. Efficieon doublait le nombre d'ALU entière et d'unité LOAD/STORE, les instructions VLIW passaient à 256 bits. Le processeur Crusoe contient 160 registres, dont 64 registres généraux, 32 registres flottants. Les registres généraux font 32 bits, ce qui est cohérent avec le fait que les CPU x86 de l'époque étaient des processeurs 32 bits. Quant aux registres flottants, ils faisaient 80 bits, ce qui colle avec la taille des registres flottants de la FPU x87 utilisée à l'époque. Sur les 64 registres généraux, seuls 48 étaient réellement utilisable pour mémoriser des opérandes. Une partie des registres généraux étaient utilisés pour la gestion de la pile, d'autres pour mémoriser l'état du CPU x86 émulé, un registre était un registre zéro non accesible en écriture. La gestion des exceptions est optimisée avec un système d'exceptions différées, le même que celui décrit plus haut. Le processeur utilise un système similaire, pour effectuer des lectures anticipées, qui a été expliqué dans le chapitre sur les processeurs VLIW/EPIC. Sauf qu'il s'agit là d'une optimisation pour gagner en performances en général, pas une aide utile pour gérer des exceptions précises ou toute autre subtilité de la traduction binaire. Pour gérer les exceptions différées et les lectures anticipées, le processeur mémorise l'état du processeur avant de démarrer un bloc de code. Pour cela, le processeur contient des ''shadow registers'', 48 registres entiers et 16 registres flottants. Vous remarquerez qu'il y a autant de registres entiers que de ''shadow registers'' entiers. Les registres entiers/flottants sont mémorisés dans les ''shadow registers'' avant d'exécuter un bloc de code, afin que le processeur puisse revenir à l'état de base. Une autre fonctionnalité liée est le ''gated store buffer''. Le principe est d'éviter toute écriture en mémoire RAM, tant que le code VLIW n'a pas émis d'instruction ''commit''. L'idée est que les écritures sont conservées dans la ''store queue'' de l'unité LOAD/STORE. Pour rappel, la ''store queue'' met en attente les écritures soit tant que les écritures ne sont pas terminées, soit tant que la RAM est occupée. L'idée est que les écritures sont accumulées dans la ''store queue'' tant que l'instruction ''commit'' n'a pas eu lieu. Le ''store queue'' est "déversé dans la RAM" seulement quand l'instruction ''commit'' s'exécute. ===Les CPU NVIDIA Denver=== Nvidia a tenté quelque chose de similaire avec son projet Denver. NVIDIA a racheté l'entreprise Transmetta, ses brevets, et a tenté de refaire la même chose. Le projet initial était de traduire du code x86 vers un jeu d'instruction VLIW propriétaire, comme l'a fait Transmetta. Mais NVIDIA n'a pas réussi à acquérir la licence du jeu d'instruction x86 et s'est rabattu sur le jeu d'instruction ARM. Le processeur né de ce projet est le Tegra K1-64 CPU. Il s'agit d'un processeur composé d'un cœur VLIW, couplé à deux décodeurs d'instructions ARM. Les deux décodeurs ARM traduisent une paire d'instructions ARM consécutives en instructions VLIW, pour les exécuter sur le cœur VLIW. Le CPU peut fonctionner selon deux modes : ARM et VLIW. En somme, le processeur supporte deux jeux d'instruction : le jeu d'instruction ARM, et un jeu d'instruction VLIW. En mode ARM, les instructions ARM sont chargées depuis le cache, traduites en VLIW par les décodeurs ARM, puis exécutées par le cœur VLIW. En mode optimisé, le CPU exécute des instructions VLIW chargées depuis le cache d'instruction, les décodeurs ne sont pas utilisés. : Le CPU supporte en réalité deux jeux d'instruction ARM : ARM8 et 7. Mais ils correspondent au même mode pour le CPU. Un point important est que le processeur peut changer très rapidement de mode, en à peine quelques cycles d'horloges. On n'est clairement pas dans le cas des CPU x86, qui mettent des plombes pour passer du mode 32 à 64 bits et inversement. La commutation est tellement rapide qu'on peut considérer que le processeur supporte deux jeux d'instruction simultanément. Et le support simultané de deux jeux d'instruction facilite l'implémentation de la traduction binaire. Encore une fois, inutile de traduire du code qui ne sera exécutée qu'une seule fois, mieux vaut l'exécuter directement dans le mode ARM. Surtout que le processeur supporte un mode ARM qui permet d'exécuter du code ARM sans perte de performance. Par contre, le code fréquemment utilisé, notamment dans des boucles critiques, est traduit en VLIW. Au final, les performances sont optimisées, en limitant le travail du logiciel traducteur binaire. La détection du code fréquemment exécuté se fait différemment que pour les processeurs Transmetta et Daisy. L'unité de branchement ne détecte pas directement le code beaucoup exécuté, même si elle a un rôle à jouer. A la place, la détection est réalisée en logiciel, par un ''thread'' dédié, qui s'exécute sur un cœur séparé. Il détecte le code fréquemment utilisé en regardent les branchements exécutés récemment. Pour cela, le processeur mémorise l'historique des branchements pris récemment et copie cette historique dans une mémoire tampon dédiée, partagée entre tous les cœurs. Vu qu'elle est partagée, le ''thread'' de détection a accès à l'historique sans pertes de performances, seule la copie de l'historique dans ce tampon a un cout en performance, pas son partage. Le code VLIW obtenu après traduction binaire est mémorisé dans la mémoire RAM, dans une portion spécialement réservée pour. Elle fait 128 mébioctets, et est appelée le '''cache d'optimisation''' par NVIDIA. Le cache d'optimisation est protégé en écriture et seul le traducteur binaire peut écrire dedans. Au passage, le traducteur binaire est du code VLIW, ce qui fait qu'il est placé dans le cache d'optimisation. Le processeur détecte automatiquement quand une fonction pour laquelle le code compilé équivalent est disponible. Pour cela, l'unité de chargement contient une table de correspondance entre l'adresse de la fonction ARM, et l'adresse de son équivalent compilé. Quand une fonction est appelée, l'unité de chargement regarde l'adresse de destination du branchement. Si l'adresse est dans cette table, elle récupère l'adresse de la fonction compilée et branche vers celle-ci. Le processeur dispose d'un cache d'instruction de 128 kibioctets, ce qui est très important, mais nécessaire vu la taille des instructions VLIW. Le coeur VLIW dispose de ses propres décodeurs, d'un ''scoreboard'', de 7 unités de calcul et de registres. Pour les unités de calcul, il a deux FPU, deux ALU, une unité de branchement, et deux unités LOAD-STORE faisant aussi ALU. Un point important est que le CPU dispose de 64 registres entiers et de 64 registres flottants. C'est deux fois plus que supporte le jeu d'instruction ARM. En mode ARM, seule la moitié des registres est utilisée. Mais en mode optimisé, le traducteur binaire utilise bien les 64 registres, grâce à une sorte de renommage de registres logicielle. Pour simplifier la traduction binaire, le processeur supporte les techniques vues précédemment. Le processeur intègre un ''gated store buffer'' similaire à celui des processeurs Transmetta. Les techniques d'exceptions différées sont aussi supportées, comme sur les processeurs Transmetta. Il a aussi une unité de préchargement décrite par NVIDIA comme agressive, avec support du préchargement de type anticipé (''runahead''). ===L'émulation de la mémoire virtuelle=== Pour finir, il faut parler de la mémoire virtuelle avec la traduction binaire. Sur le processeur source, le processeur gère à la fois des adresses physiques et virtuelles et sa MMU fait la conversion entre les deux. Sur le processeur VLIW, la MMU est simulée par le programme de traduction binaire, partiellement ou totalement. L'implémentation exacte varie suivant que l'on parle du projet Daisy, des processeurs Transmetta ou de Denver. Sur les processeurs Transmetta, il n'y a pas de MMU ni de mémoire virtuelle. A la place, le programme de traduction binaire émule la mémoire virtuelle du processeur source. Le processeur VLIW ne gère que des adresses physiques, rien d'autre. L'espace d'adressage physique du processeur VLIW a la même taille que celui du processeur émulé, ici des processeurs x86. Il est vraisemblable que les adresses physiques utilisées par le processeur x86 sont les mêmes que celles du processeur VLIW. Sur le projet Daisy, le processeur VLIW gère la mémoire virtuelle via pagination, comme les processeurs émulés. Ce qui fait qu'il gère un espace d'adressage virtuel et un espace d'adressage physique. Pour éviter toute confusion, nous parlerons d'adresse physique/virtuelle VLIW pour les adresses physiques/virtuelles du processeur VLIW, d'adresse virtuelle/physique source pour celle du jeu d'instruction traduit, à savoir du Power PC ou du s390, éventuellement de l'x86. Il faut alors faire le lien entre adresses physiques source et adresses virtuelles VLIW. Pour cela, rien de plus simple : il y a correspondance parfaite. L'adresse physique source numéro N correspond à l'adresse virtuelle VLIW numéro N. Cependant, cela signifie que tout l'espace d'adressage virtuel VLIW serait utilisé par le code à traduire. En réalité, il faut ajouter de la place pour le code traduit et le programme de traduction binaire. La conséquence est que l'espace d'adressage virtuel VLIW est plus large que l'espace d'adressage source. L'espace d'adressage virtuel VLIW est découpé en trois sections : une section pour le code Power PC à traduire, une autre pour le code traduit en VLIW, et une dernière réservée au traducteur binaire. Le programme de traduction est placé dans une ROM mappée en mémoire dans la seconde section. Le reste de la seconde section est réservé à une mémoire RAM utilisée par le programme de traduction. [[File:Mémoire virtuelle sur le projet Daisy.png|centre|vignette|upright=1.5|Mémoire virtuelle sur le projet Daisy]] Il faut noter que chaque page physique source, une fois traduite, correspond à N pages virtuelles VLIW. La raison est que le code traduit prend plus de place que le code machine originel. En conséquence, on doit utiliser une page finale plus grande. Pour le reste, des pages contiguës en mémoire physique source sont elles aussi contiguës en mémoire virtuelle VLIW. Le calcul d'adresse est donc simple : l'adresse physique source est multipliée par N, puis on ajoute l'adresse de base à laquelle commence la section pour le code traduit. ==La traduction binaire sur les Burroughs B1700== Nous venons de voir comment accélérer en matériel la traduction binaire, avec des techniques bien spécifiques. Le Burroughs B1700 a procédé autrement et il est intéressant d'étudier en détail son architecture. Son architecture est décrite dans le livre "Interpreting Machines : Architecture and Programming of the Bl700/Bl800 Series", écrit par Elliott I. Organick et James A. Hinds. Une partie de ce qui vfa suivre est un très court résumé de ce livre. : Dans ce qui suit, nous parlerons de traducteur binaire pour parler soit d'un interpréteur pour un langage de programmation de haut niveau, soit pour un logiciel de traduction binaire. Le Burroughs 1700 est un processeur 24 bits, qui a cependant la capacité d'émuler des architectures 8, 16 bits assez simplement, via divers mécanismes. Le Burroughs 1700 incorpore de nombreuses optimisations pour cela. Une partie d'entre elles permet de gérer des opérandes de taille différente de 24 bits, d'autres permettent de gérer des instructions de taille différente de 24 bits, d'autres servent pour les deux. Il incorpore aussi des instructions facilitant le découpage des instructions machines en ''opcode'', adresses et constantes immédiates. ===L'ALU de taille variable=== Les Burroughs B1700 avaient une '''ALU de taille variable''', ce qui veut dire qu'elle pouvait faire des calculs sur un nombre de bits compris entre 0 et 24. Pour cela, les calculs étaient faits avec une ALU de 24 bits, puis les bits de poids fort inutiles étaient masqués. L'ALU de 24 bits pouvait masquer les bits de poids fort du résultat, mais avec des limitations. Elle gérait des opérandes et résultats de 0, 4, 8, 12, 16, 20 et 24 bits. Pour avoir un réglage plus fin, une ALU 4 bits séparée permettait de corriger le résultat. Les registres X et Y étaient reliés à une ALU de 24 bits capable d'effectuer des opérations arithmétiques basiques. Fait étonnant, l'ALU faisait tous les calculs en même temps et fournissait ses résultats dans 7 registres : un registre SUM pour l'addition, un registre DIFFERENCE pour la soustraction, et un registre par opération bit à bit. Les registres pour les opérations bit à bit sont : CMPX et CMPY pour l'opération NOT sur chaque opérande, XANY pour le ET bit à bit, XEOY pour le XOR, XORY pour le OU logique. Les additions et soustractions pouvaient se faire sur des opérandes codés en binaire ou en BCD, suivant le code opération utilisé. A cela, il fallait ajouter deux registres de "résultat" MSKX et MSKY. Ils fournissaient un masque dépendant de la taille du résultat en bits. C'est ce registre qui permettait de gérer des données de 0, 4, 8, 12, 16, 20, et 24 bits, alors que l'ALU faisait 24 bits. Le masque dans ce registre pouvait servir d'opérande dans une opération de masquage ultérieure, pour corriger le résultat. L'ALU prenait en opérande les registres X et Y, mais aussi un registre CYF pour la retenue entrante. En sortie, il fournissait aussi deux retenues sortantes : une pour l'addition et une autre pour la soustraction. Elles étaient mémorisées dans les registres CYL et CYD, respectivement. L'ALU fournissait aussi 12 conditions, qui alimentait un registre d'état. Le registre d'état était segmenté en trois registres appelés XYST, XYCN et BICN. Les registres X et Y sont des registres pour les opérandes, mais il y a aussi deux autres registres de 24 bits. Il s'agit des registres T et L, qui sont des registres généraux. Les registres T et L étaient découpés en 6 sous-registres de 4 bits, nommés LA LB LC LD LE et LF pour le registre L, TA TB TC TD TE et TF pour le registre T. Ils étaient adressables comme n'importe quel registre. Le processeur contenait de nombreux autres registres de 4 bits, dont les sous-registres de T et L. En tout, il y a 27 registres de 4 bits adressables ! Et le processeur disposait d'une ALU 4 bits pour manipuler ces registres de 4 bits. L'ALU 4 bits est capable de faire des MOV entre registres de 4 bits, des opérations bit à bit, et peut aussi tester si un bit vaut 0 ou 1. La dernière possibilité est très utile pour implémenter les branchements. De plus, cela permettait de faire des opérations de masquage, notamment pour appliquer un masquage supplémentaire à celui de l'ALU. Par exemple, pour gérer des opérations sur 14 bits, on effectue une opération sur 16 bits avec l'ALU, et on masque les 2 bits manquants avec l'ALU 4 bits. ===Une architecture bit-adressable=== Le CPU Burroughs B1700 était '''bit-adressable'''. En clair, il pouvait adresser la mémoire bit par bit, si nécessaire. Un avantage est que cela facilitait la traduction binaire des instructions machine, mais nous détaillerons cela plus bas. Un autre avantage est que cela permettait d'émuler des architectures dont la taille des registres/''byte''/mots était très différente. Et cet avantage mérite quelques explications immédiates. Le processeur était un CPU 24 bits, mais il pouvait émuler des processeurs 16 bits, 8 bits, 4 bits, 1 bit, 9 bits, ou toute autre valeur. Pour cela, le processeur lisait des mots de 24 bits et utilisant un ''barrel shifter'' pour sélectionner les bits adéquats. Par exemple, pour un accès de 16 bits, la donnée lue en mémoire était masquée de manière à ne garder que les 16 bits de poids faible. Le ''barrel shifter''/circuit de masquage était placé directement avant le bus mémoire, sur le trajet des données/instructions. Le ''barrel shifter'' était commandé par un registre ''Field Unit'', qui précisait quelle était la taille des données à charger. Les instructions source , à traduire, peuvent faire 16 bit avec tel jeu d'instruction, 8 bits avec un autre, 32 bits sur un autre, etc. Elles peuvent aussi être de longueur fixe ou variable ! Le B1700 utilisait une architecture bit-adressable pour gérer ces contraintes. En effet, avec un CPU bit-adressable, les contraintes d'alignement des instructions disparaissent ! ===Des registres d’interfaçage mémoire adressables=== Pour communiquer avec la mémoire, il avait trois registres séparés : READ, WRITE, MAR. READ contient la donnée lue lors d'une lecture, WRITE est pour une donnée à écrire lors d’une écriture, MAR contient l'adresse à lire ou en cours de lecture. Il s'agit de registres d’interfaçage mémoire, les mêmes que ceux vus dans le chapitre sur le chemin de données du CPU. Une instruction LOAD est donc émulée en deux instructions machines : une instruction MOV pour copier l'adresse dans le registre READ, une instruction de lecture proprement dite. L'écriture demande d'ajouter une seconde instruction MOV pour copier la donnée à écrire, et la lecture est remplacée par une écriture. Le fait que ces registres d’interfaçage mémoire et IO soit adressable est peu commun, et c'est plus quelque chose qu'on attend d'un microcode que d'instructions machines. D'ailleurs, l'implémentation des instructions LOAD/STORE ressemble à ce qui est effectué par le séquenceur d'un CPU. Le séquenceur est d'ordinaire celui qui séquence la copie des adresses/données dans les registres d’interfaçage, puis lance la lecture/écriture. Ici, c'est réalisé via des instructions machines. Il faut bien comprendre que les registres d’interfaçage mémoire ont été promus au rang de registres architecturaux, sur ce processeur. De même, pour communiquer avec les entrées-sorties, il disposait de deux registres CMND et DATA : CMND pour envoyer une commande à une entrée-sortie, DATA pour lire ou écrire une donnée. Il y avait aussi un registre U, qui mémorisait une donnée lue depuis le lecteur de cassette. ===Le code machine du Burroughs B1700=== Le processeur utilise des instructions de 16 bits de long, qui sont copiées dans registre d'instruction M de 16 bits. Le ''program counter'' est appelé le registre A. Les instructions du processeur font 16 bits de long, et elles sont alignées en RAM sur 16 bits, ce qui fait que les 4 bits de poids fort de ce registre valent 0, seuls les 14 bits de poids fort sont utiles. Les adresses mentionnées plus haut, qui adressent les micro-opérations en mémoire RAM, sont des adresses de mots de 16 bits, pas les adresses de bit individuels. Fait intéressant, il était possible de faire un OU logique entre l'instruction lue et un registre du processeur, le résultat étant mémorisé dans le registre M. Cela permettait de faire du code automodifiant, pour émuler des modes d'adressage complexe. Cela permettait par exemple de remplacer une adresse dans une micro-opération. L'adresse est alors surimposée avec un OU dans le champ d'adresse, alors que le reste de l'instruction n'est pas modifié. Afin de gérer les interruptions, ces dernières étaient partiellement détectées en logiciel. Une interruption doit être prise en compte à la toute fin de l'exécution d'une instruction source, quand celle-ci a fini son travail. Mais vu qu'ici, les instructions sont émulées avec des séries d'instructions cibles, il y a un problème : il est possible d'interrompre une instruction émulée en plein milieu. Pour éviter cela, le processeur ne déclenche pas les interruptions matérielles immédiatement, elles sont mises en attente. Une instruction dédiée vérifie si une interruption est en attente, et l'exécute alors cas échéant. L'instruction en question est utilisée en dernier, dans la suite d'instruction qui émule l'instruction source. ===La traduction binaire sur le Burroughs B1700=== Les Burroughs B1700 intégraient une pile d'adresse de retour, capable de mémoriser 16 adresses de 24 bits, ainsi que 4 registres généraux, nommés X, Y, T et L, tous de 24 bits. Un point important est que le registre T avait des possibilités d’extraction spécifiques. Il était possible d'extraire N bits, placés n'importe où dans ce registre, et de les copier dans un autre registre. Le CPU supportait une '''instruction EXTRACT''' pour ça. Nous verrons comment cette possibilité est exploitée pour la traduction binaire dans ce qui suit. Dans ce qui suit, on suppose que l'instruction est composée de '''champs''' : l'opcode, les opérandes, les adresses absolues, les constantes immédiates, les numéros de registre, etc. L'idée est que l'instruction source est chargée dans le registre T, puis découpée en champs, qui sont copiés dans les registres du processeur ou envoyés au microcode. Je rappelle que le registre T est relié à un ''barrel shifter'', différent au précédent, qui permettait de masquer, décaler et extraire une donnée de ce registre. Et il y a la possibilité d'extraire une suite de bit de ce registre T, pour la copier dans un autre registre. Par exemple, prenons une instruction composée d'un opcode, d'une constante immédiate et d'une adresse immédiate. Premièrement, elle est chargée dans le registre T. Deuxièmement, la constante immédiate est extraite du registre T et copiée dans le registre X. Troisièmement, l'adresse est extraite et est copiée dans le registre d’interfaçage mémoire, une lecture est démarrée, et l'opérande lu est copiée dans le registre Y. Enfin, l'instruction équivalente à l'opcode est exécutée par le microcode. Le tout prend plusieurs instructions : une première pour lire l'opcode, une seconde pour lire le premier opérande, une troisième pour lire la seconde opérande, une quatrième pour tout exécuter. Notons que suivant la taille des champs, on peut en charger plusieurs à la fois. Par exemple, imaginons qu'on ait chargé 24 bits, contenant un opcode de 8 bits et une constante immédiate de 16 bits : le registre T est découpé en un opcode et une constante, avec deux instructions EXTRACT. L'architecture est bit-adressable, car elle gère des champs qui ne font pas forcément 8 ou 16 bits. Par exemple, certains jeux d'instructions ont des opcodes codés sur 6 ou 7 bits, voire 9-10 bits. Utiliser une architecture bit-adressable règle ce problème d'alignement. On peut lire un champ depuis la mémoire RAM en précisant l'adresse de son premier bit, pas besoin de gérer des accès non-alignés. Par exemple, prenons une instruction source avec un opcode de 7 bits, suivi par une adresse de 12 bits et une constante immédiate de 15 bits. On peut charger 7 + 12 bits en un premier accès, pour extraite l'opcode et l'adresse, suivi par un second accès de 15 bits pour extraire la constante immédiate. Pour charger un champ, il faut connaitre plusieurs informations : l'adresse de son premier bit, sa taille. Pour cela, le B1700 contient deux registres, nommés ''Field Adress'' (FA) et ''Field Length'' (FL), qui indiquent l'adresse du premier bit et la taille de l'instruction à charger. Le premier fait 24 bits, ce qui permet de gérer des adresses de 24 bits. N'oubliez pas que le processeur est bit-adressable, ce qui permet d'adresser 16 Mébi-bits, soit 2 mébioctets. Le registre pour la taille fait lui 16 bits, ce qui permet de gérer des instructions de 65536 bits, soit 8192 octets ! Leur nom commence par ''field'', car le processeur peut gérer des instructions de taille variable et/ou plus longues que 24 bits. Dans ce cas, le chargement de l'instruction se fait champ par champ. Pour donner un exemple, prenons une instruction composée de 20 champs de 12 bits chacun. Les champs sont chargés un par un dans le registre T. Le registre FA est alors initialisé avec l'adresse de l'instruction, il est incrémenté de 12 à chaque cycle. Le registre FL est initialisé avec la taille de l'instruction, soit 20 × 12 = 240 bits, et il est décrémenté de 16 à chaque cycle. Précisons cependant que la taille d'un champ peut être changée d'un cycle à l'autre. Par exemple, le processeur peut charger un opcode de 8 bits, l'interpréter, détecter que l'instruction demande ensuite de charger une adresse de 16 bits. Dans ce cas, le registre FL est immédiatement altéré pour charger 16 bits, au lieu des 8 bits de l'opcode immédiatement précédent. ===Le Burroughs B1726 : le mal-nommé ''split-level control store''=== Le Burroughs B1700 avait plusieurs modèles : le B1726, le B1710 et le B1800. * Le B1710 plaquait le traducteur binaire en mémoire RAM. * Le B1800 avait ajouté un cache dédié au code du traducteur binaire, afin d'améliorer les performances. * Le B1726 utilisait un ''local store'' dédié au traducteur binaire. Le B1726 peut exécuter le traducteur binaire soit depuis le ''local store'', soit depuis la mémoire RAM avec une pénalité en termes de performances. La documentation appelle cela un '''''split-level control store''''', mais le terme est trompeur : la documentation qualifie de microcode le langage machine du Burroughs B1700, ce qui est à l'origine de beaucoup de confusions. Une autre source de confusion est que ce langage machine a quelques propriétés qu'on pourrait attendre d'un microcode, mais certaines sont manquantes. Par exemple, il peut adresser les registres d’interfaçage mémoire, mais c'est parce que ce sont des registres architecturaux sur ce processeur. Ses instructions n'encodent pas de signaux de commande, par contre, elles font 16 bits et adressent les opérandes de manière implicite. Le ''local store'' mémorisait au maximum 2048 instructions machine, ce qui fait que les 2048 premières "adresses" correspondaient au ''local store''. Les suivantes étaient des adresses de mémoire RAM qui contenaient le reste du traducteur binaire. En pratique, l'adresse qui séparait les deux était configurable ! On pouvait limiter la taille du ''local store'' à un multiple de 32 instructions ! Les adresses restantes étaient alors réattribuées à la RAM. Par exemple, on pouvait attribuer 64 adresses au ''local store'', les 2048 - 64 adresses restantes étaient alors attribuées à la mémoire RAM. Un registre limite, nommé TDPM, indiquait quelle était l'adresse de démarcation entre les deux. Le programmeur devait découper le traducteur binaire en segments, certains étant placés dans le ''local store'', les autres étant en mémoire RAM. Mieux que ça, le processeur autorisait d'utiliser la technique de l'''overlaying'' pour le microcode ! Il y avait même une fonction OVERLAY dédiée pour ! OVERLAY avait besoin de trois opérandes : la taille du segment à copier, son adresse source en mémoire RAM, son adresse de destination dans la micro-SRAM. L'interpréteur réservait un "segment" en mémoire RAM, pour les données, dont le processeur connaissait la position. En dehors de ce segment, il n'y a que des instructions machines à traduire, et le traducteur binaire. Le CPU incorporait pour cela deux registres, un pour l'adresse de base du segment de données, un autre pour son adresse de fin. Il incorporait une protection mémoire limitée sur ce segment, à savoir que seul l'interpréteur pouvait lire ou écrire dans ce segment. ==Les jeux d'instructions dédiés à un langage de programmation== De rares processeurs sont conçus pour un langage de programmation en particulier. On appelle ces processeurs, conçus pour des besoins particuliers, des '''processeurs dédiés'''. Par exemple, les fameux ''Burrough E-mode'' B5000/B6000/B7000 étaient spécialement conçus pour exécuter de l'ALGOL-60. Leurs cousins B2000/B3000/B4000 étaient eux conçus pour le COBOL. Des langages fonctionnels ont aussi eu droit à leurs processeurs dédiés. Le prolog en est un bel exemple, avec les superordinateurs de 5ème génération qui lui étaient dédié. On peut aussi citer les machines LISP, dédiés au langage LISP, qui datent des années 1970. Elles étaient capables d’exécuter certaines fonctions de base du langage directement dans leurs circuits : elles possédaient notamment un ''garbage collector'' câblé dans ses circuits ainsi que des instructions machines supportant un typage déterminé à l’exécution. Les processeurs dédiés ont eu leur heure de gloire au début de l'informatique, à une époque où les langages de haut niveau venaient d'être inventés. À cette époque, les compilateurs n'étaient pas performants et ne savaient pas bien optimiser le code machine. Il était alors rationnel, pour l'époque, de rapprocher le code machine cible et le langage de programmation de haut niveau. De nombreuses architectures dédiés ont ainsi été inventées, avant que les concepteurs se rendent compte des défauts de cette approche. Les défauts en question ne sont pas nombreux, mais assez simples à comprendre. Premièrement, elles sont très rapides pour un langage de programmation en particulier, mais sont assez mauvaises pour les autres, d'où un problème de "compatibilité". Ajoutons à cela que les langages de programmation peuvent évoluer, devenir de moins en moins populaires/utilisés, ce qui rend la création d'architectures généralistes plus pertinente. Enfin, les architectures dédiées sont évidemment des processeurs CISC, pour implémenter les nombreuses fonctionnalités des langages évolués. Et les défauts des CISC sont assez rédhibitoires à l'heure actuelle. À l'heure actuelle, les algorithmes des compilateurs se sont améliorés et savent nettement mieux utiliser le matériel. Ils produisent du code machine efficace, ce qui rend les architecture dédiées bien moins intéressantes. Si on ajoute les défauts de ces architectures dédiées, par étonnant que les architectures dédiées aient presque disparues. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'accélération matérielle de la virtualisation | prevText=L'accélération matérielle de la virtualisation | next=Le matériel réseau | nextText=Le matériel réseau }} </noinclude> 8wlngtptjlq5q90vqopau5kxdlgznpo Wikilivres:Demandes de suppression/2026 4 83725 764081 762142 2026-04-20T09:23:25Z Xhungab 23827 /* Mécanique classique */ nouvelle section 764081 wikitext text/x-wiki {{Wikilivres:Pages à supprimer/En-tête}} == [[Shein enjeux et mode durable]] ([[Discussion:Shein enjeux et mode durable|d]] · h · j · ↵) == * Rédigé probablement par IA. Il n'y a pas de référence précise permettant de remonter aux sources. --[[Utilisateur:Fourmidable|Fourmidable]] ([[Discussion utilisateur:Fourmidable|discussion]]) 17 mars 2026 à 15:52 (CET) * {{VoteSupprimer}} En accord avec l'avis précédent. Sans compter que la fin de page s'apparente plus à du spam que du référencement. '''Le site indiqué en bas ([[Shein_enjeux_et_mode_durable#Remerciment|section « Remerciment »]]) est signalé comme dangereux par mon anti-virus.''' —[[Utilisateur:Eihel|Eihel]] ([[Discussion utilisateur:Eihel|discussion]]) 22 mars 2026 à 12:22 (CET) == [[Utilisateur:Matthius/Devenir un Génie]] == * {{VoteSuppression immédiate}} C'est du spam pur et simple : la page est remplie de LE. Ça ne mérite pas d'être dans un projet wikimédien ! —[[Utilisateur:Eihel|Eihel]] ([[Discussion utilisateur:Eihel|discussion]]) 22 mars 2026 à 16:58 (CET) == Mécanique classique == Livre créé le <bdi>[https://fr.wikibooks.org/w/index.php?title=M%C3%A9canique_classique&oldid=607022 6 novembre 2018 à 15:51]</bdi> [[Utilisateur:DavidMaxwell|<bdi>DavidMaxwell</bdi>]] sans contenu ([[Mécanique classique]]) Merci [[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 20 avril 2026 à 11:23 (CEST) 5140zqi9n6409mxuicg0vn24mjqcrcf La grammaire fondamentale de l'ido/Structure de la phrase 0 83745 764073 763906 2026-04-20T06:37:28Z Francucelo 123176 /* Propositions relatives */Erreur grammaticale 764073 wikitext text/x-wiki Après avoir appris les leçons précédentes, vous pouvez commencer à former des phrases. La structure des phrases en ido est similaire à celle du français. == Ordre des mots == Comme dans les exemples des leçons précédentes, l'ordre des mots par défaut en ido est Sujet-Verbe-Objet, c'est-à-dire qu'on mentionne d'abord l'auteur de l'action, puis l'action elle-même, et enfin le destinataire de l'action. * Par exemple, dans « Me amas tu », on a « me » (je, sujet), « amas » (aimer, verbe) et « tu » (toi, objet). Cependant, l'utilisateur peut également modifier l'ordre des mots dans une phrase en fonction des habitudes de sa langue ou, par exemple, pour des raisons artistiques, tout en conservant le sens d'origine. Dans ce cas, il faut utiliser le marqueur d'accusatif « n ». === Marqueur d'accusatif === Si la phrase ne suit pas l'ordre SVO (par exemple, SOV) pour éviter toute ambiguïté, il faut utiliser le marqueur d'accusatif « n ». Celui-ci est placé à la fin du complément d'objet direct. * Par exemple, « Me tun amas » correspond à « me » (je, sujet), « tun » (toi, objet) et « amas » (aimer, verbe) Lorsqu'un nom est précédé d'une préposition, il fait office de complément d'objet indirect et ne nécessite donc pas de marque d'accusatif. De plus, le marqueur d'accusatif ne s'applique qu'aux noms ou pronoms qui constituent le complément d'objet direct. Les adjectifs ne portent pas de marqueur d'accusatif. === Position des adjectifs === En ido, comme les classes grammaticales sont déjà indiquées, la position de l'adjectif est libre. Il peut se placer aussi bien avant qu'après le nom. Par défaut, l'adjectif se place avant le nom. * Par exemple, « granda homo » (grand homme) peut aussi s'écrire « homo granda ». == Phrases interrogatives == === Questions générales === Pour les questions fermées (oui ou non), il suffit d'ajouter « ka » avant la phrase. * Par exemple, « Tu havas un kato » (Tu as un chat ; phrase affirmative) et « Ka tu havas un kato ? » (As-tu un chat ? ; phrase interrogative). === Questions particulières === Pour demander des informations précises, il faut utiliser des mots interrogatifs spécifiques. * Les trois pronoms relatifs (voir la section « [[La grammaire fondamentale de l'ido/Mots grammaticaux/Pronoms|Mots grammaticaux : Pronoms : Pronoms relatifs]] ») * Ube : où * Kande : quand * Quale : comment * Pro quo : pourquoi * Quanta : combien Dans les phrases interrogatives, les mots interrogatifs sont généralement placés en début de phrase. * Par exemple, « Quale vu sentas ? » (Comment vous sentez-vous ?) Il convient de noter que si les trois pronoms relatifs sont des compléments d'objet direct dans la question, ils doivent être marqués au cas objectif dans l'ordre inversé. * Par exemple, « Quin tu manjas ? » (Qu'est-ce que tu manges ?) == Propositions subordonnées == === Propositions nominales === Les propositions nominales sont des propositions qui font office de nom. Elles commencent par « ke ». * Par exemple, « Me volas ke tu lektas libri » (Je veux que tu lises des livres) === Propositions relatives === Les propositions relatives servent à caractériser le nom qui les précède. Elles sont introduites par un pronom relatif. * Par exemple, « La homo qua kantas esas mea fratulo » (L'homme qui chante est mon frère). De plus, si le nom modifié dans la subordonnée est un complément d'objet, le pronom relatif doit alors prendre la marque d'objet « n ». * Par exemple, « La libro quon me skribis » (le livre que j'ai écrit). === Propositions circonstancielles === Les propositions circonstancielles sont introduites par diverses conjonctions subordonnées et expriment le temps, la cause, la condition, etc. Voici les mots essentiels des propositions circonstancielles : * Por ke : pour que * Pro ke : parce que * Kande : quand * Se : si * Malgre ke : bien que Voici quelques exemples : * Kande me esus richa, me ne laborus : Quand je serai riche, je ne travaillerai plus. * Pro ke la vetero esis tre varma, me ne ekiris : Parce qu'il faisait très chaud, je ne suis pas sorti. == Comparatif == En ido, le comparatif est principalement exprimé à l'aide de trois adverbes primitifs : plu (plus), min (moins) et tam (aussi), et on utilise « kam » pour introduire l'élément comparé. * Par exemple, « Me esas '''plu''' alta '''kam''' mea fratulo » (Je suis plus haut que mon frère). == Superlatif == Pour exprimer le superlatif, on peut utiliser les adverbes primitifs : « maxim » (le plus) et « minim » (le moins). * « Me esas la '''minim''' alta en la grupo » (Je suis le plus petit dans le groupe). == Exercices == Essayez de traduire les phrases suivantes. Les racines nécessaires sont indiquées ci-dessous : * Je lis le livre que tu as donné à moi. ** Lire : lekt ** Donner : don ** Livre : libr * Es-tu plus basse que tes sœurs ? ** Sœur : fratin ** Bas : bas * Où es-tu ? Quand arrives-tu ? ** Être : es ** Arriver : ariv {{Boîte déroulante|titre=Voir les réponses|contenu=* Me lektas la libro quan tu donis a me. * Ka tu esas plu basa kam tua fratini? ** Ou : Ka tu esas min alta kam tua fratini? * Ube tu esas? Kande tu arivos? ** Tu esas ube? Tu arivos kande?}} {{AutoCat}} cm263u6dhj43vhzfm6pc1furuqiwvbz Dictionnaire de philosophie/Afrique 0 83760 764083 762109 2026-04-20T10:53:13Z Xhungab 23827 764083 wikitext text/x-wiki {{Suppression Immédiate | raison = A la demande de l'auteur | utilisateur = [[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 20 avril 2026 à 12:52 (CEST) }} {{suppression}} 04xnwpo2ra0akwf7tvlze64x3xdnipy Catégorie:La grammaire fondamentale de l'ido (livre) 14 83819 764075 763874 2026-04-20T07:58:47Z Xhungab 23827 764075 wikitext text/x-wiki Voir le chapitre principal : [[La grammaire fondamentale de l'ido|La grammaire fondamentale de l'ido]] [[Catégorie:Livres par titre]] [[Catégorie:Enseignement des langues|La grammaire fondamentale de l'ido]] [[Catégorie:Langues construites]] mza3z3c31bp8z8mk7u5n8ednmhy9w67 Pour lire Platon/Introduction par les mythes 0 83823 764003 763978 2026-04-19T14:49:51Z PandaMystique 119061 764003 wikitext text/x-wiki <noinclude>{{sous-pages}}</noinclude> Invitons-nous à la table philosophique de Platon. Chez lui, le mythe n'est ni un simple ornement, ni une concession à l'imagination : il prolonge l'argument là où la dialectique rencontre ses limites et donne à penser par images ce que la seule discussion conceptuelle transmet mal. Cette page parcourt quelques grands récits et métaphores platoniciens : la généalogie d'Éros, l'anneau de Gygès, la prosopopée des lois du ''Criton''. À chaque fois, on s'efforcera de signaler qui parle dans le dialogue, dans quel contexte, et à quel degré Platon engage sa propre position. La liste des questions proposées n'est pas close. == La philosophie == === Fille de l'étonnement : une affection avant d'être un savoir === [[Fichier:'Paris à l'Arc-en-ciel' by Robert Delaunay, 1914.jpg|vignette|''Paris à l'Arc-en-ciel'', Robert Delaunay, 1914.]] <blockquote> « Cette émotion, l'étonnement, est tout à fait d'un philosophe : la philosophie n'a pas d'autre origine, et celui qui a fait d'Iris la fille de Thaumas n'est pas un mauvais généalogiste. » Platon, ''Théétète'', 155d, trad. Michel Narcy, Paris, GF Flammarion, 1994, p. 163. </blockquote> Le propos est adressé par Socrate au jeune Théétète, embarrassé par sa propre incapacité à répondre à une question qui paraissait simple : qu'est-ce que le savoir ? Socrate le rassure. Ce trouble n'est pas un défaut : il est l'affect qui ouvre l'enquête philosophique. L'étonnement (''thaumazein'') désigne ici le moment où ce qui paraissait aller de soi cesse d'aller de soi. Aristote reprendra cette idée au livre A de la ''Métaphysique'' (982b 12-20), en faisant de l'étonnement le point de départ des premières recherches sur la nature. Socrate appuie son propos sur une référence mythologique. Iris, messagère des dieux dans la tradition homérique et hésiodique, est la divinité de l'arc-en-ciel ; elle est fille de Thaumas, dont le nom évoque la « merveille ». On a souvent proposé, à la suite de commentateurs modernes, d'y lire une image de la philosophie comme lien entre ciel et terre, ou entre visible et invisible. Le texte lui-même reste cependant discret : il se contente d'un clin d'œil étymologique. Mieux vaut retenir ce que Platon affirme clairement : la philosophie naît d'une affection, non d'un simple calcul intellectuel. Le philosophe n'est ni stupéfait ni béat ; il est saisi par ce qui, dans l'expérience ordinaire, appelle à être interrogé. === Le désir du savoir : la question du manque dans ''le Banquet'' === [[Fichier:Chouette Puy dy Fou.jpg|gauche|vignette|La chouette, figure associée à Athéna et à la philosophie.]] ==== Étymologie et énigme ==== L'étymologie du mot ''philosophia'' («&nbsp;amour de la sagesse&nbsp;» ou «&nbsp;du savoir&nbsp;») est connue, mais sa portée mérite qu'on s'y arrête. Pour deux raisons au moins. D'abord, la sagesse semble relever d'un état intellectuel, et il n'est pas évident qu'un contenu de pensée puisse susciter une affection, moins encore un désir proprement amoureux. Ensuite, dire qu'il n'y a pas de savoir sans désir engage une conception du philosophe très différente du modèle du sage achevé : le philosophe n'est pas celui qui possède la sagesse, mais celui qui tend vers elle. C'est cette tension que ''le Banquet'' tente de rendre intelligible, au moyen de plusieurs discours emboîtés. Le dialogue met en scène une succession d'éloges d'Éros prononcés lors d'un banquet. Chaque discours déplace et précise l'analyse du précédent. Aristophane y tient une place singulière, et Diotime la place ultime, celle que Socrate adopte comme la plus haute. La composition du dialogue est donc ordonnée : on ne peut lire le discours d'Aristophane comme la pensée de Platon, pas plus qu'on ne peut isoler les discours antérieurs du mouvement qui les conduit vers l'enseignement de Diotime. [[Fichier:Nature dévoilée par Philosophie.jpg|vignette|''La Nature se dévoilant devant la Philosophie'', allégorie classique.]] ==== Le discours d'Aristophane : Éros comme nostalgie du même ==== Le poète comique Aristophane<ref>Théâtre complet d'Aristophane sur [[s:Auteur:Aristophane|Wikisource]].</ref> vient de se remettre d'un hoquet persistant lorsqu'il prend la parole (''Banquet'', 185c-e). Le détail a son importance : Platon, ailleurs critique à l'égard de la comédie, prête à Aristophane un discours brillant tout en l'inscrivant dans un cadre où l'ivresse et les fonctions du corps sont présentes, rappelant que le dialogue ne se tient pas dans la froideur d'un tribunal académique. Aristophane raconte alors le mythe de l'androgyne (''Banquet'', 189d-193d, trad. Luc Brisson, Paris, GF Flammarion, 1998, p. 117-123). Les humains, à l'origine, formaient des êtres sphériques complets ; pris d'orgueil, ils voulurent escalader le ciel et se mesurer aux dieux. Zeus, pour les punir, les coupa en deux : le nombril est la trace de cette cicatrice. Depuis, chaque moitié cherche sa moitié perdue. L'amour serait donc le désir de retrouver une unité perdue. Deux points méritent d'être soulignés. Le discours d'Aristophane fait de l'amour une ''nostalgie'' : on désire ce que l'on a déjà eu et que l'on a perdu. Le manque est ici pensé comme défaut d'une plénitude antérieure. Mais cette analyse, dans l'économie du dialogue, n'est pas le dernier mot. Diotime, rapportée par Socrate, la déplacera en affirmant que l'amour ne tend pas à rejoindre «&nbsp;sa moitié&nbsp;», mais ce qui est bon et beau en tant que tel (''Banquet'', 205d-206a). Le discours d'Aristophane est donc une étape, non une thèse définitive. ==== Le discours de Diotime : Éros entre manque et ressource ==== [[Fichier:Eros Farnese MAN Napoli 6353.jpg|gauche|vignette|Éros Farnèse, Musée archéologique national de Naples.]] Socrate, lorsque vient son tour, ne prononce pas un éloge d'Éros sous son propre nom : il rapporte un enseignement reçu d'une prêtresse de Mantinée, Diotime. Ce détour est remarquable. L'interlocuteur le plus dialectique du dialogue s'efface derrière une figure féminine, religieuse, aux allures d'oracle. Diotime commence par refuser la grammaire commune des éloges : Éros n'est ni beau, ni bon, ni dieu, contrairement à ce qu'ont affirmé les discours précédents. Il est intermédiaire, ''metaxu''. <blockquote> «&nbsp;C'est une assez longue histoire, je vais pourtant te la raconter. Il faut savoir que, le jour où naquit Aphrodite, les dieux festoyaient ; parmi eux se trouvait le fils de Mètis, Poros. Or, quand le banquet fut terminé, arriva Pénia, qui était venue mendier comme cela est naturel un jour de bombance, et elle se tenait sur le pas de la porte. Or Poros, qui s'était enivré de nectar, car le vin n'existait pas encore à cette époque, se traîna dans le jardin de Zeus et, appesanti par l'ivresse, s'y endormit. Alors, Pénia, dans sa pénurie, eut le projet de se faire faire un enfant par Poros ; elle s'étendit près de lui et devint grosse d'Éros. [...] Puis donc qu'il est le fils de Poros et de Pénia, Éros se trouve dans la condition que voici. D'abord, il est toujours pauvre, et il s'en faut de beaucoup qu'il soit délicat et beau, comme le croient la plupart des gens. Au contraire, il est rude, malpropre, va-nu-pieds et il n'a pas de gîte, couchant toujours par terre et à la dure, dormant à la belle étoile sur le pas des portes et sur le bord des chemins, car, puisqu'il tient de sa mère, c'est l'indigence qu'il a en partage. À l'exemple de son père en revanche, il est à l'affût de ce qui est beau et de ce qui est bon, il est viril, résolu, ardent, c'est un chasseur redoutable ; il ne cesse de tramer des ruses, il est passionné de savoir et fertile en expédients, il passe tout son temps à philosopher, c'est un sorcier redoutable, un magicien et un expert. [...] Par ailleurs, il se trouve à mi-chemin entre le savoir et l'ignorance. Voici en effet ce qui en est. Aucun dieu ne tend vers le savoir ni ne désire devenir savant, car il l'est ; or, si l'on est savant, on n'a pas besoin de tendre vers le savoir. Les ignorants ne tendent pas davantage vers le savoir ni ne désirent devenir savants. Mais c'est justement ce qu'il y a de fâcheux dans l'ignorance : alors que l'on n'est ni beau ni bon ni savant, on croit l'être suffisamment. Non, celui qui ne s'imagine pas en être dépourvu ne désire pas ce dont il ne croit pas devoir être pourvu. [...] Il va de soi, en effet, que le savoir compte parmi les choses qui sont les plus belles ; or Éros est amour du beau. Par suite, Éros doit nécessairement tendre vers le savoir, et, puisqu'il tend vers le savoir, il doit tenir le milieu entre celui qui sait et l'ignorant.&nbsp;» Platon, ''Le Banquet'', 203b-204b, trad. Luc Brisson, Paris, GF Flammarion, 1998, p. 142-145. </blockquote> La généalogie d'Éros est composée avec soin. ''Poros'', le père, signifie la «&nbsp;ressource&nbsp;», la «&nbsp;voie de passage&nbsp;», ce qui permet de se tirer d'affaire. ''Pénia'', la mère, est la «&nbsp;pauvreté&nbsp;», l'indigence. Éros n'est ni l'un ni l'autre : il ''tient'' de ses deux parents, ce qui est une manière platonicienne de dire qu'il occupe une position intermédiaire. Cette position intermédiaire est triple : entre mortel et immortel, entre savoir et ignorance, entre beauté et laideur. Diotime ajoute que le philosophe est précisément dans la situation d'Éros. Il n'est pas sage, puisque les sages sont les dieux ; il n'est pas ignorant, puisque l'ignorant ne sait même pas qu'il est ignorant et ne désire donc pas savoir. Le philosophe est celui qui sait qu'il ne sait pas et qui, précisément parce qu'il le sait, désire savoir. C'est là la meilleure formulation de la définition socratique rendue célèbre par l'''Apologie'' (21d). Deux points méritent d'être mis en lumière. Premièrement, le manque n'est plus, comme chez Aristophane, la perte d'une plénitude originaire : il est constitutif du désir lui-même, qui est toujours tension vers ce qu'il n'a pas encore. Diotime le formule explicitement : on ne désire que ce dont on manque (''Banquet'', 200a-b). Deuxièmement, l'amour du beau débouche sur l'amour du savoir par une continuité que Diotime décrit comme une ascension (''Banquet'', 210a-212a) : du beau corps singulier aux beaux corps, puis aux belles âmes, aux belles pratiques, aux belles connaissances, et enfin à la Beauté elle-même. L'éros philosophique n'est donc pas un détachement du désir sensible ; il en est le prolongement converti. == Socrate de Platon : la question socratique et la critique de l'écriture == [[Fichier:P1050775 Louvre statue Gudea statue A détail ecriture rwk.JPG|vignette|Détail d'écriture cunéiforme, statue de Gudea, Musée du Louvre.]] Le Socrate des dialogues n'est pas simplement le Socrate historique. Ce constat, qui nourrit ce que les spécialistes appellent depuis longtemps la «&nbsp;question socratique&nbsp;», ne conduit pas à la conclusion, trop tranchée, que Socrate serait un pur personnage fictif. Il était bien un Athénien historique, condamné à mort en 399 av. J.-C., que plusieurs contemporains (Xénophon dans les ''Mémorables'', Aristophane dans ''Les Nuées'', Eschine de Sphettos, et Platon) ont portraituré de manières divergentes. Les chercheurs modernes, de Gregory Vlastos (''Socrates. Ironist and Moral Philosopher'', Cornell University Press, 1991) à Louis-André Dorion (''Socrate'', Paris, PUF, coll. «&nbsp;Que sais-je ?&nbsp;», 2004), s'accordent cependant sur un point : on ne peut remonter avec certitude au Socrate «&nbsp;réel&nbsp;» par-delà les figurations qu'en ont proposées ses disciples et ses adversaires. Le Socrate de Platon est donc un personnage philosophique construit, dont la fonction évolue au fil de l'œuvre. On distingue classiquement trois périodes. Les dialogues de jeunesse, dits «&nbsp;socratiques&nbsp;» ou «&nbsp;aporétiques&nbsp;» (''Charmide'', ''Lachès'', ''Euthyphron'', ''Ion'', ''Hippias mineur'', et jusqu'au ''Ménon''), s'achèvent souvent sur une impasse ou sur un désaccord irrésolu. Les dialogues de la maturité (''Phédon'', ''Banquet'', ''République'', ''Phèdre'') déploient les thèses platoniciennes propres (théorie des Idées, immortalité de l'âme, tripartition de l'âme). Les dialogues tardifs (''Parménide'', ''Théétète'', ''Sophiste'', ''Politique'', ''Philèbe'', ''Timée'', ''Lois'') voient Socrate céder parfois la parole à d'autres figures, tandis que la théorie est soumise à autocritique. Dans le ''Gorgias'', la violence de Calliclès contre Socrate marque une tension particulière : l'interlocuteur refuse la loi morale au nom du droit du plus fort, et Socrate ne parvient pas à le convaincre, ce qui pose la question des limites de la persuasion philosophique face à qui refuse la règle du dialogue. Platon, lui, n'a jamais cessé d'écrire, alors même qu'il met en scène un Socrate qui n'écrit pas et qu'il formule à plusieurs reprises une critique de l'écriture. Le passage le plus connu se trouve dans le ''Phèdre'' (274c-275b), où Socrate rapporte le mythe égyptien de Theuth : ce dieu inventeur présente l'écriture au roi Thamous comme un «&nbsp;remède pour la mémoire et le savoir&nbsp;» ; Thamous lui répond qu'il s'agit, au contraire, d'un remède pour le rappel et non pour la mémoire vive, et que les hommes, se fiant à des traces extérieures, cesseront d'exercer leur propre pensée. La ''Lettre VII'' (341c-d) formule une réserve voisine : l'écrit ne peut pas transmettre ce qui se découvre seulement dans la rencontre prolongée d'un maître et d'un élève. Ces textes ne disqualifient pas l'écriture en bloc ; ils marquent une hiérarchie entre parole vivante et trace écrite, et mettent en garde contre une illusion : croire qu'on sait parce qu'on possède un texte. Il faut donc se garder de deux interprétations simplificatrices. La première affirmerait que Platon aurait constaté l'«&nbsp;échec&nbsp;» de la maïeutique socratique dans les dialogues aporétiques et s'en serait détourné. Or l'aporie n'est pas un échec : elle est la forme d'un apprentissage par la prise de conscience de l'ignorance, comme le montre l'entretien avec l'esclave du ''Ménon'' (82b-85b). La seconde affirmerait que la critique de l'écriture dans le ''Phèdre'' condamne le projet même d'écrire des dialogues. Mais Platon écrit, et la forme dialoguée est précisément celle qui se rapproche le plus de la conversation vivante. Sur ces débats, on peut se reporter à Monique Dixsaut, ''Le Naturel philosophe. Essai sur les dialogues de Platon'', Paris, Vrin, 2001. == Pourquoi Platon recourt-il aux mythes ? == Platon réserve le mythe à des moments précis : quand l'argument ne peut plus avancer par la seule dialectique ; quand la question excède les moyens de la démonstration (eschatologie, origine de l'âme, naissance du cosmos) ; quand il s'agit de persuader et non seulement de prouver ; quand la mise en image donne prise à l'imagination sans pour autant abdiquer la rigueur. Luc Brisson, dans ''Platon, les mots et les mythes'' (Paris, La Découverte, 1982 ; rééd. 1994), a montré que le mythe platonicien n'est pas un résidu pré-rationnel, mais un instrument philosophique spécifique : il articule une tradition (les récits hérités) et une invention (les récits composés par Platon), et il poursuit une fonction argumentative plus qu'illustrative. Le mythe, chez Platon, a donc plusieurs fonctions. Il introduit un problème sous une forme frappante (comme l'anneau de Gygès ouvre la question de la justice). Il prolonge l'argument en donnant à voir ce qu'il énonçait abstraitement (comme l'allégorie de la caverne prolonge la discussion sur l'éducation). Il répond à une question que la dialectique ne peut pas résoudre (comme les mythes eschatologiques du ''Gorgias'', du ''Phédon'' ou de la ''République'', qui traitent du sort des âmes). Dans tous les cas, il faut le lire comme un texte qui ''pense'', et non comme un ornement. === Le mythe de l'anneau de Gygès : un défi posé à Socrate === Le mythe de l'anneau de Gygès est l'un des plus commentés de l'œuvre platonicienne. Avant d'en proposer une lecture, il est nécessaire d'en situer précisément le contexte, sous peine de confondre la position d'un interlocuteur avec celle de Platon. Le récit apparaît dans la ''République'', livre II (359c-360b). Au livre I, Thrasymaque a défendu avec agressivité la thèse selon laquelle «&nbsp;la justice est l'intérêt du plus fort&nbsp;» (338c), thèse que Socrate n'a pas pleinement réfutée aux yeux de ses auditeurs. Au livre II, Glaucon et Adimante, frères de Platon, reprennent donc l'argument sous une forme plus exigeante : ils ne partagent pas cette thèse, mais veulent mettre Socrate au défi d'y répondre de manière satisfaisante. Glaucon propose alors une analyse en trois temps. Il expose d'abord la thèse «&nbsp;des gens&nbsp;» sur l'origine conventionnelle de la justice. Il cherche ensuite à montrer que même l'homme juste ne le serait que par impuissance. Il demande enfin à Socrate de prouver que la vie juste est préférable à la vie injuste, même pour celui qui pourrait être injuste impunément. L'anneau de Gygès intervient dans le deuxième temps. <blockquote> Les hommes prétendent que, par nature, il est bon de commettre l'injustice et mauvais de la souffrir, mais qu'il y a plus de mal à la souffrir que de bien à la commettre. Aussi, lorsque mutuellement ils la commettent et la subissent, et qu'ils goûtent des deux états, ceux qui ne peuvent point éviter l'un ni choisir l'autre estiment utile de s'entendre pour ne plus commettre ni subir l'injustice. De là prirent naissance les lois et les conventions, et l'on appela ce que prescrivait la loi légitime et juste. Voilà l'origine et l'essence de la justice : elle tient le milieu entre le plus grand bien (commettre impunément l'injustice) et le plus grand mal (la subir quand on est incapable de se venger). Entre ces deux extrêmes, la justice est aimée non comme un bien en soi, mais parce que l'impuissance de commettre l'injustice lui donne du prix. [...] Telle est donc, Socrate, la nature de la justice et telle son origine, selon l'opinion commune. Maintenant, que ceux qui la pratiquent agissent par impuissance de commettre l'injustice, c'est ce que nous sentirons particulièrement bien si nous faisons la supposition suivante. Donnons licence au juste et à l'injuste de faire ce qu'ils veulent ; suivons-les et regardons où, l'un et l'autre, les mène le désir. Nous prendrons le juste en flagrant délit de poursuivre le même but que l'injuste, poussé par le besoin de l'emporter sur les autres : c'est ce que recherche toute nature comme un bien, mais que, par loi et par force, on ramène au respect de l'égalité. La licence dont je parle serait surtout significative s'ils recevaient le pouvoir qu'eut jadis, dit-on, l'ancêtre de Gygès le Lydien. Cet homme était berger au service du roi qui gouvernait alors la Lydie. Un jour, au cours d'un violent orage accompagné d'un séisme, le sol se fendit et il se forma une ouverture béante près de l'endroit où il faisait paître son troupeau. Plein d'étonnement, il y descendit, et, entre autres merveilles que la fable énumère, il vit un cheval d'airain creux, percé de petites portes ; s'étant penché vers l'intérieur, il y aperçut un cadavre de taille plus grande, semblait-il, que celle d'un homme, et qui avait à la main un anneau d'or, dont il s'empara ; puis il partit sans prendre autre chose. Or, à l'assemblée habituelle des bergers qui se tenait chaque mois pour informer le roi de l'état de ses troupeaux, il se rendit portant au doigt cet anneau. Ayant pris place au milieu des autres, il tourna par hasard le chaton de la bague vers l'intérieur de sa main ; aussitôt il devint invisible à ses voisins qui parlèrent de lui comme s'il était parti. [...] S'étant rendu compte de cela, il répéta l'expérience pour voir si l'anneau avait bien ce pouvoir ; le même prodige se reproduisit : en tournant le chaton en dedans il devenait invisible, en dehors visible. Dès qu'il fut sûr de son fait, il fit en sorte d'être au nombre des messagers qui se rendaient auprès du roi. Arrivé au palais, il séduisit la reine, complota avec elle la mort du roi, le tua, et obtint ainsi le pouvoir. Si donc il existait deux anneaux de cette sorte, et que le juste reçût l'un, l'injuste l'autre, aucun, pense-t-on, ne serait de nature assez adamantine pour persévérer dans la justice et pour avoir le courage de ne pas toucher au bien d'autrui [...]. Platon, ''République'', II, 358e-360c, trad. Émile Chambry, Paris, Les Belles Lettres, coll. «&nbsp;Classiques en poche&nbsp;», 2002, p. 89-93 (autre traduction disponible : Georges Leroux, Paris, GF Flammarion, 2002). </blockquote> [[Fichier:Paphos Haus des Theseus - Mosaik Achilles 1.jpg|vignette|Mosaïque d'Achille, Maison de Thésée, Paphos.]] Il faut lire ce texte en gardant à l'esprit sa fonction dans l'économie du dialogue. Glaucon ne défend pas, à titre personnel, la thèse qu'il expose : il la formule dans toute sa force pour contraindre Socrate à y répondre vraiment. L'expérience de pensée de l'anneau, qui demande ce que ferait un juste devenu invisible et impuni, est précisément conçue pour rendre l'objection inévitable. Or ce n'est pas dans cette scène que tient le cœur du dialogue, mais dans la réponse que Socrate construit sur l'ensemble de la ''République''. À partir du livre IV, il défend l'idée que la justice n'est pas simple convention extérieure, mais harmonie intérieure de l'âme, entre la raison, le cœur et le désir (''République'', IV, 441d-444a). Aux livres VIII et IX, il tente de montrer que l'homme juste est plus heureux que l'injuste, y compris dans le pire des cas. Le livre X s'achève sur un mythe eschatologique, celui d'Er le Pamphylien, qui affirme que les injustices finissent par se payer, quand bien même elles paraissent impunies dans cette vie. La thèse que Glaucon expose n'est donc pas celle de Platon, mais celle à laquelle Platon veut répondre. Confondre les deux, c'est attribuer à l'auteur la position même qu'il s'emploie à combattre. L'intérêt philosophique du récit est ailleurs : il met en lumière le caractère fragile des motivations qui nous tiennent à la justice dans la vie ordinaire, et invite à chercher, en dehors de toute récompense ou sanction extérieure, une raison plus profonde d'être juste. Les lectures modernes qu'on a voulu rapprocher du passage (Hobbes, Rousseau, Marx) sont intéressantes, mais supposent un changement de cadre théorique qu'il faut signaler comme tel. Hobbes, dans le ''Léviathan'' (1651), analyse l'état de nature comme un état où la pulsion de conservation, en l'absence de pouvoir commun, engendre la défiance mutuelle et la guerre ; la thèse de Glaucon contient effectivement une hypothèse qui peut y faire écho, mais Hobbes ne croit ni à une justice en soi ni à une harmonie intérieure de l'âme au sens platonicien. Rousseau, dans le ''Discours sur l'origine de l'inégalité'' (1755), propose une autre généalogie de la propriété et de la justice, qu'il relie à l'histoire sociale de l'homme et non à une nature injuste originaire. Marx, quant à lui, développe dans ''Sur la question juive'' (1843) puis dans la ''Critique du programme de Gotha'' (1875) une analyse du droit formel comme expression des rapports de production, étrangère à la problématique platonicienne du rapport entre la justice et le bien. Ces rapprochements peuvent nourrir la réflexion comparative, à condition de ne pas laisser entendre que Platon serait déjà hobbien, rousseauiste ou marxiste avant l'heure. == La place du philosophe dans la cité == '''Lecture conseillée''' : ''Apologie de Socrate'' (17a-42a, trad. Luc Brisson, Paris, GF Flammarion, 2005) ; [[Pour lire Platon/Études de quelques passages des dialogues]] (étude de la ''Lettre VII''). Socrate prononce plusieurs discours devant les juges d'Athènes, rapportés par Platon dans l{{'}}''Apologie de Socrate''. Il est accusé d'impiété (introduire de nouvelles divinités) et de corrompre la jeunesse (24b-c). Son activité principale, telle qu'il la décrit, consistait à interroger ses concitoyens sans enseigner de doctrine particulière : il se comparait à un taon posé sur un noble cheval pour le réveiller (30e). Se condamnera-t-il lui-même à l'exil, comme la cité semble l'y inviter ? Il s'y refuse (37c-38a) : il tient cet examen critique pour l'activité la plus précieuse qu'il puisse rendre à la cité. L'''Apologie'' pose ainsi une question qui parcourt toute l'œuvre de Platon : le philosophe, dans la cité, est-il nécessairement malvenu ? A-t-il une place, et si oui, à quel prix ? La réponse de la ''République'' (487a-502c, notamment l'allégorie du «&nbsp;vrai pilote&nbsp;») est sévère. Dans les cités actuelles, le philosophe est perçu comme inutile, parfois comme dangereux, précisément parce que les valeurs qui y dominent ne sont pas les siennes. Mais cette marginalité n'est pas sans remède : c'est pour penser les conditions d'une cité où philosophie et politique ne se contrarient plus que Platon écrit la ''République'' (et plus tard les ''Lois''). La ''Lettre VII'', dont l'authenticité reste discutée mais est aujourd'hui largement acceptée, rapporte l'expérience des voyages de Platon en Sicile auprès de Denys II de Syracuse, et montre à la fois la vocation politique du philosophe et les difficultés concrètes de son action. == Une philosophie politique en réponse au procès : pourquoi obéir aux lois ? == '''Lecture conseillée''' : ''Criton'' ([[s:Criton|sur Wikisource]]), passages cités d'après la trad. Luc Brisson, Paris, GF Flammarion, 1997. Quelques jours avant son exécution, Socrate reçoit dans sa prison la visite d'un vieil ami, Criton. Celui-ci lui propose de s'évader : les conditions matérielles sont réunies, les gardiens se laisseront corrompre, des amis sont prêts à l'accueillir à Thessalie ou ailleurs. Socrate refuse, et engage avec Criton un dialogue qui occupe la seconde moitié du texte. Le point culminant de l'argument prend la forme d'une prosopopée<ref>Figure de rhétorique par laquelle on fait parler une personne absente, morte, un animal, une chose, ou, comme ici, une abstraction juridique.</ref> : Socrate fait parler les Lois d'Athènes (''Criton'', 50a-54d). === Le contrat implicite entre le citoyen et la cité === <blockquote> Vois si tu l'entendras de cette autre manière : Au moment de nous enfuir ou de sortir d'ici, quel que soit le mot qu'il te plaira de choisir, si les Lois et la République venaient se présenter devant nous, et nous disaient : «&nbsp;Réponds-moi, Socrate, que vas-tu faire ? L'action que tu entreprends a-t-elle d'autre but que de nous détruire, nous qui sommes les Lois, et avec nous la République tout entière, autant qu'il dépend de toi ? Ou te semble-t-il possible que l'État subsiste et ne soit pas renversé, lorsque les arrêts rendus restent sans force et que de simples particuliers leur enlèvent l'effet et la sanction qu'ils doivent avoir ?&nbsp;» Platon, ''Criton'', 50a-b. </blockquote> Le premier argument des Lois est institutionnel. Si chaque particulier pouvait refuser d'exécuter une sentence qu'il juge injuste, aucun arrêt de justice ne tiendrait et l'État s'effondrerait. Ce premier point est énoncé sous forme de question rhétorique et fait appel au bon sens juridique. Les Lois développent ensuite un deuxième argument, plus ambitieux, sous la forme d'une analogie filiale : <blockquote> «&nbsp;Eh quoi ! Socrate, diraient les Lois, est-ce là ce dont nous étions convenues avec toi ? Ou plutôt n'étions-nous pas convenues avec toi que les jugements rendus par la République seraient exécutés ?&nbsp;» [...] «&nbsp;Voyons : quel sujet de plainte as-tu contre nous et contre la République pour entreprendre ainsi de nous renverser ? Et d'abord, n'est-ce pas nous qui t'avons donné la vie ? N'est-ce pas nous qui avons présidé à l'union de ton père et de ta mère, ainsi qu'à ta naissance ? [...] Mais, puisque c'est à nous que tu dois ta naissance, ta nourriture et ton éducation, peux-tu nier que tu sois notre enfant, notre esclave même, toi et tes ancêtres ? Et s'il en est ainsi, crois-tu que tu aies contre nous les mêmes droits que nous avons contre toi, et que tout ce que nous pourrions entreprendre contre toi, tu puisses à ton tour l'entreprendre justement contre nous ? [...] Ta sagesse va-t-elle jusqu'à ignorer que la patrie est, aux yeux des dieux et des hommes sensés, quelque chose de plus cher, plus respectable, plus auguste et plus saint qu'une mère, un père et tous les aïeux ? qu'il faut avoir pour la patrie, même irritée, plus de respect, de soumission et d'égard, que pour un père ? qu'il faut l'adoucir par la persuasion ou faire tout ce qu'elle ordonne [...] ?&nbsp;» Platon, ''Criton'', 50c-51c. </blockquote> Les Lois rappellent à Socrate les bienfaits reçus : mariage légitime de ses parents, éducation, protection, partage des biens communs. Cette dette fonde, selon leur propos, une autorité comparable à celle d'un parent, et même supérieure. Il faut prendre au sérieux la dissymétrie qui est ici affirmée : le citoyen ne peut pas répondre à la loi comme un égal répond à un égal. Ce point, lourd de conséquences, sera discuté par toute la tradition philosophique ultérieure. On peut dès maintenant souligner qu'il ne coïncide pas avec ce qu'on appellera plus tard «&nbsp;contrat social&nbsp;» à la manière moderne : il n'est pas question ici d'un acte volontaire par lequel des individus libres instituent l'État, mais d'un rapport de dette envers une institution qui précède le citoyen et le rend possible. Les Lois précisent aussitôt un second volet, qui tempère l'image de la soumission filiale. Tout citoyen devenu adulte (éphèbe) peut choisir de s'en aller librement, avec ses biens. <blockquote> «&nbsp;Nous déclarons encore, et c'est un droit que nous reconnaissons à tout Athénien qui veut en user, qu'aussitôt qu'il a été reçu dans la classe des éphèbes, qu'il a vu ce qui se passe dans la République, et qu'il nous a vues aussi, nous qui sommes les Lois, il est libre, si nous ne lui plaisons pas, d'emporter ce qu'il possède et de se retirer où il voudra. Et si quelqu'un d'entre vous veut aller dans une colonie, parce que nous lui déplaisons, nous et la République, si même il veut aller s'établir quelque part à l'étranger, aucune de nous ne s'y oppose et ne le défend : il peut aller partout où il voudra avec tous ses biens. Mais quant à celui de vous qui persiste à demeurer ici, en voyant de quelle manière nous rendons la justice et nous administrons toutes les affaires de la république, nous déclarons dès lors que par le fait il s'est engagé envers nous à faire tout ce que nous lui ordonnerions, et s'il n'obéit pas, nous le déclarons trois fois coupable : d'abord, parce qu'il nous désobéit à nous qui lui avons donné la vie, ensuite parce que c'est nous qui l'avons élevé, enfin parce qu'ayant pris l'engagement d'être soumis, il ne veut ni obéir ni employer la persuasion à notre égard, si nous faisons quelque chose qui ne soit pas bien ; et tandis que nous lui proposons à titre de simple proposition, et non comme un ordre tyrannique, de faire ce que nous lui commandons, lui laissant même le choix d'en appeler à la persuasion ou d'obéir, il ne fait ni l'un ni l'autre.&nbsp;» Platon, ''Criton'', 51c-52a. </blockquote> Deux idées méritent d'être dégagées. D'une part, la cité n'est pas présentée comme une instance qui retient par la force : qui n'est pas satisfait peut partir. Rester, c'est implicitement s'engager à obéir. D'autre part, l'obéissance n'exclut pas la discussion. Les Lois ménagent une alternative expresse, «&nbsp;obéir ou persuader&nbsp;» (''peíthein ē poieîn'', 51b-c). Celui qui juge injuste ce qu'on lui commande a le droit, et même le devoir, de tenter de convaincre. Ce n'est qu'après l'échec de cette persuasion qu'il doit exécuter la sentence. La prosopopée ne défend donc pas une obéissance aveugle : elle définit une obéissance réfléchie, exercée à l'intérieur d'un espace de discussion. === Socrate et la décision d'obéir === Les Lois prolongent leur plaidoyer en mettant en évidence la cohérence propre de Socrate. Il n'a jamais quitté Athènes, sinon pour des campagnes militaires et une visite à l'Isthme de Corinthe ; il y a fondé une famille ; lors du procès, il a préféré la mort à l'exil (''Apologie'', 37c-38a). S'évader maintenant reviendrait à contredire tout ce qu'il a dit et fait jusque-là. <blockquote> «&nbsp;Or, tu n'as préféré le séjour ni de Lacédémone, ni de la Crète, dont tu vantes sans cesse le gouvernement, ni d'aucune ville grecque ou barbare, mais tu es sorti d'Athènes moins souvent que les boiteux, les aveugles et les autres infirmes : preuve évidente que tu avais plus d'amour que les autres Athéniens pour cette ville et pour nous-mêmes qui sommes les Lois de cette ville : car peut-on aimer une cité sans en aimer les lois ?&nbsp;» Platon, ''Criton'', 52b-53a. </blockquote> L'argument se déplace ici du terrain institutionnel à celui de l'identité. Qui Socrate serait-il, s'il fuyait ? Comment continuerait-il à tenir, en exil, les discours qu'il a tenus toute sa vie sur la vertu, sur la justice, sur la loi ? La question n'est pas seulement ce qu'il a le droit de faire, mais ce qu'il peut faire sans cesser d'être lui-même. La fidélité à la cité se confond alors avec une fidélité à soi. Inversement, l'injustice subie ne l'autorise pas à commettre lui-même une injustice : Socrate ne répond pas au mal par le mal (49b-c), principe énoncé dès le début du dialogue et que les Lois reprennent à leur compte. <blockquote> «&nbsp;Maintenant, au contraire, si tu meurs, tu meurs victime de l'injustice, non des lois, mais des hommes, au lieu que, si tu sors de la ville, après avoir si honteusement commis l'injustice à ton tour, rendu le mal pour le mal, violé toutes les conventions, tous les engagements que tu as contractés envers nous, maltraité ceux que tu devrais le plus ménager, toi-même, tes amis, ta patrie et nous, alors nous te poursuivrons de notre inimitié pendant ta vie [...].&nbsp;» Platon, ''Criton'', 54b-c. </blockquote> Ce passage distingue soigneusement les lois et les hommes qui les appliquent. La sentence qui condamne Socrate peut être injuste, dans la mesure où les juges se sont trompés ; les lois elles-mêmes ne sont pas en cause. Socrate n'obéit donc pas à une loi injuste au sens où la loi serait intrinsèquement mauvaise ; il obéit à une loi juste appliquée injustement par des hommes. On voit que le ''Criton'' ne prescrit pas une obéissance inconditionnelle à n'importe quelle prescription. La difficulté apparaît, et a été amplement discutée par les commentateurs, lorsqu'on tente d'articuler cette position avec celle de l'''Apologie'' (29d-30a), où Socrate affirme qu'il obéirait au dieu plutôt qu'aux juges s'ils lui ordonnaient de cesser de philosopher. === Limites et portée d'une lecture moderne === On a longtemps tenté de lire ce texte à la lumière des théories modernes du contrat social (Hobbes, Locke, Rousseau), ou dans une perspective critique inspirée de Marx qui y verrait l'alibi d'une domination juridique. Ces rapprochements demandent prudence. Les Lois du ''Criton'' ne se posent pas comme un contrat librement consenti par des individus préexistants à la cité, mais comme une instance qui précède le citoyen et lui donne son existence sociale. Et la critique marxiste du droit, formulée dans ''Sur la question juive'' et dans la ''Critique du programme de Gotha'', repose sur une analyse économique du rapport entre droit formel et rapports de production, tout à fait étrangère à l'horizon de Platon. On peut s'en inspirer pour interroger Platon, non pour le traduire. Une lecture plus proche du texte permet pourtant de dégager une intuition qu'Alain, bien plus tard, a reformulée avec netteté : <blockquote> «&nbsp;Résistance et obéissance, voilà les deux vertus du citoyen. Par l'obéissance, il assure l'ordre ; par la résistance il assure la liberté. Et il est bien clair que l'ordre et la liberté ne sont point séparables, car le jeu des forces, c'est-à-dire la guerre privée, à toute minute, n'enferme aucune liberté ; c'est une vie animale, livrée à tous les hasards. Donc les deux termes, ordre et liberté, sont bien loin d'être opposés ; j'aime mieux dire qu'ils sont corrélatifs. La liberté ne va pas sans l'ordre ; l'ordre ne vaut rien sans la liberté. Obéir en résistant, c'est tout le secret. Ce qui détruit l'obéissance est anarchie ; ce qui détruit la résistance est tyrannie.&nbsp;» Alain, ''Propos d'un Normand'', 4 septembre 1912, éd. M. Savin, Paris, Gallimard, coll. «&nbsp;Bibliothèque de la Pléiade&nbsp;», 1956, t. II, p. 164. </blockquote> La formule d'Alain n'est pas une paraphrase du ''Criton'', mais un écho. Elle aide à saisir ce que Platon a déjà pensé : une loi qui n'admettrait aucune critique tomberait dans la tyrannie, une critique qui refuserait toute obéissance tomberait dans l'anarchie. L'«&nbsp;obéir ou persuader&nbsp;» du ''Criton'' tient précisément cet équilibre. Il ne légitime ni le conformisme silencieux, ni la désobéissance sans discussion préalable. Dans le cas particulier de Socrate, la persuasion a échoué (les juges n'ont pas été convaincus) : reste l'obéissance, et cette obéissance consentie est, paradoxalement, la preuve ultime de la liberté philosophique de Socrate, celle qui le distingue de tout personnage comme Calliclès, pour qui la loi n'est qu'un masque de la faiblesse (''Gorgias'', 482c-486d). == Liste des mythes platoniciens == Voici une liste non exhaustive des principaux mythes et allégories présents dans l'œuvre de Platon, avec leur localisation stéphanienne. Chacun pourra faire l'objet d'une étude séparée. * '''Mythe de l'androgyne''' : discours d'Aristophane (''Banquet'', 189d-193d). * '''Généalogie d'Éros''' : Poros et Pénia, enseignement de Diotime (''Banquet'', 203b-204b). * '''Allégorie de la ligne''' (''République'', VI, 509d-511e) et '''allégorie de la caverne''' (''République'', VII, 514a-517a). * '''Anneau de Gygès''' : défi de Glaucon (''République'', II, 359c-360b). * '''Mythe d'Er le Pamphylien''' : eschatologie, choix des vies (''République'', X, 614b-621d). * '''Mythe de l'attelage ailé''' : tripartition de l'âme, chute et remontée vers les Idées (''Phèdre'', 246a-256e). * '''Mythe de Theuth''' : invention de l'écriture et critique de la mémoire écrite (''Phèdre'', 274c-275b). * '''Mythe de Prométhée''' : discours de Protagoras sur l'origine des arts et de la politique (''Protagoras'', 320c-322d). * '''Mythes eschatologiques''' : jugement des âmes (''Gorgias'', 523a-527a ; ''Phédon'', 107c-114c ; ''République'', X, 614b-621d). * '''Atlantide''' : cité idéale engloutie, récit rapporté par Critias (''Timée'', 20d-26e ; ''Critias'', 108e-121c). * '''Démiurge et fabrication du cosmos''' : cosmogonie (''Timée'', 27d-47e). * '''Origine des noms et des langues''' (''Cratyle'', en partie). == Notes == {{références}} == Bibliographie sommaire == === Éditions et traductions de référence === * Platon, ''Œuvres complètes'', sous la direction de Luc Brisson, Paris, Flammarion, 2008 (un volume unique regroupant l'ensemble des dialogues). * Platon, ''Œuvres complètes'', trad. Léon Robin, Paris, Gallimard, coll. «&nbsp;Bibliothèque de la Pléiade&nbsp;», 2 vol., 1940-1942. * Platon, ''Le Banquet'', trad. Luc Brisson, Paris, GF Flammarion, 1998. * Platon, ''Théétète'', trad. Michel Narcy, Paris, GF Flammarion, 1994. * Platon, ''La République'', trad. Georges Leroux, Paris, GF Flammarion, 2002 ; trad. Émile Chambry, Paris, Les Belles Lettres, 2002. * Platon, ''Apologie de Socrate, Criton'', trad. Luc Brisson, Paris, GF Flammarion, 1997 (rééd. 2005). * Platon, ''Phèdre'', trad. Luc Brisson, Paris, GF Flammarion, 2004. * Platon, ''Gorgias'', trad. Monique Canto-Sperber, Paris, GF Flammarion, 1987 (rééd. 2007). * Platon, ''Phédon'', trad. Monique Dixsaut, Paris, GF Flammarion, 1991. * Platon, ''Protagoras'', trad. Frédérique Ildefonse, Paris, GF Flammarion, 1997. * Platon, ''Lettres'', trad. Luc Brisson, Paris, GF Flammarion, 1987 (rééd. 2004). === Études === * Luc Brisson, ''Platon, les mots et les mythes. Comment et pourquoi Platon nomma le mythe ?'', Paris, La Découverte, 1982 (rééd. 1994). * Monique Dixsaut, ''Le Naturel philosophe. Essai sur les dialogues de Platon'', Paris, Vrin, 2001. * Monique Dixsaut, ''Platon'', Paris, Vrin, coll. «&nbsp;Bibliothèque des philosophies&nbsp;», 2003. * Louis-André Dorion, ''Socrate'', Paris, PUF, coll. «&nbsp;Que sais-je ?&nbsp;», 2004. * Gregory Vlastos, ''Socrate. Ironie et philosophie morale'', trad. fr. Catherine Dalimier, Paris, Aubier, 1994 (éd. originale Cornell University Press, 1991). * Monique Canto-Sperber (éd.), ''Philosophie grecque'', Paris, PUF, 1997. * Julia Annas, ''Introduction à la République de Platon'', trad. fr. Béatrice Han, Paris, PUF, 1994. * Michel Narcy, ''Le Philosophe et son double. Un commentaire de l'Euthydème de Platon'', Paris, Vrin, 1984. * ''Stanford Encyclopedia of Philosophy'', entrée «&nbsp;Plato's Myths&nbsp;» (Catalin Partenie), 2009 (mise à jour 2022), [https://plato.stanford.edu/entries/plato-myths/ en ligne]. 6nal7q83ny88xay1plk49u33f139kcl 764005 764003 2026-04-19T15:32:32Z PandaMystique 119061 764005 wikitext text/x-wiki <noinclude>{{sous-pages}}</noinclude> Invitons-nous à la table philosophique de Platon. Chez lui, le mythe n'est ni un simple ornement, ni une concession à l'imagination : il prolonge l'argument là où la dialectique rencontre ses limites et donne à penser par images ce que la seule discussion conceptuelle transmet mal. Cette page parcourt quelques grands récits et métaphores platoniciens : la généalogie d'Éros, l'anneau de Gygès, la prosopopée des lois du ''Criton''. À chaque fois, on s'efforcera de signaler qui parle dans le dialogue, dans quel contexte, et à quel degré Platon engage sa propre position. La liste des questions proposées n'est pas close. == La philosophie == === Fille de l'étonnement : une affection avant d'être un savoir === [[Fichier:'Paris à l'Arc-en-ciel' by Robert Delaunay, 1914.jpg|vignette|''Paris à l'Arc-en-ciel'', Robert Delaunay, 1914.]] <blockquote> « Cette émotion, l'étonnement, est tout à fait d'un philosophe : la philosophie n'a pas d'autre origine, et celui qui a fait d'Iris la fille de Thaumas n'est pas un mauvais généalogiste. » Platon, ''Théétète'', 155d, trad. Michel Narcy, Paris, GF Flammarion, 1994, p. 163. </blockquote> Le propos est adressé par Socrate au jeune Théétète, embarrassé par sa propre incapacité à répondre à une question qui paraissait simple : qu'est-ce que le savoir ? Socrate le rassure. Ce trouble n'est pas un défaut : il est l'affect qui ouvre l'enquête philosophique. L'étonnement (''thaumazein'') désigne ici le moment où ce qui paraissait aller de soi cesse d'aller de soi. Aristote reprendra cette idée au livre A de la ''Métaphysique'' (982b 12-20), en faisant de l'étonnement le point de départ des premières recherches sur la nature. La référence mythologique sur laquelle s'appuie Socrate est discrète. Iris, messagère des dieux dans la tradition homérique et hésiodique, est fille de Thaumas, dont le nom évoque la « merveille ». Socrate joue sur cette étymologie et n'en développe rien d'autre : le texte ne dit pas en quoi la philosophie ressemblerait à l'arc-en-ciel, et les amplifications qu'on a parfois proposées (la philosophie comme pont entre deux ordres de réalité, etc.) sortent du dit platonicien. Ce que Platon affirme clairement, c'est que la philosophie naît d'une affection et non d'un simple état intellectuel : le philosophe n'est ni stupéfait ni béat ; il est saisi par ce qui, dans l'expérience ordinaire, appelle à être interrogé. === Le désir du savoir : la question du manque dans ''le Banquet'' === [[Fichier:Chouette Puy dy Fou.jpg|gauche|vignette|La chouette, figure associée à Athéna et à la philosophie.]] ==== Étymologie et énigme ==== L'étymologie du mot ''philosophia'' («&nbsp;amour de la sagesse&nbsp;» ou «&nbsp;du savoir&nbsp;») est connue, mais sa portée mérite qu'on s'y arrête. Pour deux raisons au moins. D'abord, la sagesse semble relever d'un état intellectuel, et il n'est pas évident qu'un contenu de pensée puisse susciter une affection, moins encore un désir proprement amoureux. Ensuite, dire qu'il n'y a pas de savoir sans désir engage une conception du philosophe très différente du modèle du sage achevé : le philosophe n'est pas celui qui possède la sagesse, mais celui qui tend vers elle. C'est cette tension que ''le Banquet'' tente de rendre intelligible, au moyen de plusieurs discours emboîtés. Le dialogue met en scène une succession d'éloges d'Éros prononcés lors d'un banquet. Chaque discours déplace et précise l'analyse du précédent. Aristophane y tient une place singulière, et Diotime la place ultime, celle que Socrate adopte comme la plus haute. La composition du dialogue est donc ordonnée : on ne peut lire le discours d'Aristophane comme la pensée de Platon, pas plus qu'on ne peut isoler les discours antérieurs du mouvement qui les conduit vers l'enseignement de Diotime. [[Fichier:Nature dévoilée par Philosophie.jpg|vignette|''La Nature se dévoilant devant la Philosophie'', allégorie classique.]] ==== Le discours d'Aristophane : Éros comme nostalgie du même ==== Le poète comique Aristophane<ref>Théâtre complet d'Aristophane sur [[s:Auteur:Aristophane|Wikisource]].</ref> vient de se remettre d'un hoquet persistant lorsqu'il prend la parole (''Banquet'', 185c-e). Le détail a son importance : Platon, ailleurs critique à l'égard de la comédie, prête à Aristophane un discours brillant tout en l'inscrivant dans un cadre où l'ivresse et les fonctions du corps sont présentes, rappelant que le dialogue ne se tient pas dans la froideur d'un tribunal académique. Aristophane raconte alors le mythe de l'androgyne (''Banquet'', 189d-193d, trad. Luc Brisson, Paris, GF Flammarion, 1998, p. 117-123). Les humains, à l'origine, formaient des êtres sphériques complets ; pris d'orgueil, ils voulurent escalader le ciel et se mesurer aux dieux. Zeus, pour les punir, les coupa en deux : le nombril est la trace de cette cicatrice. Depuis, chaque moitié cherche sa moitié perdue. L'amour serait donc le désir de retrouver une unité perdue. Deux points méritent d'être soulignés. Le discours d'Aristophane fait de l'amour une ''nostalgie'' : on désire ce que l'on a déjà eu et que l'on a perdu. Le manque est ici pensé comme défaut d'une plénitude antérieure. Mais cette analyse, dans l'économie du dialogue, n'est pas le dernier mot. Diotime, rapportée par Socrate, la déplacera en affirmant que l'amour ne tend pas à rejoindre «&nbsp;sa moitié&nbsp;», mais ce qui est bon et beau en tant que tel (''Banquet'', 205d-206a). Le discours d'Aristophane est donc une étape, non une thèse définitive. ==== Le discours de Diotime : Éros entre manque et ressource ==== [[Fichier:Eros Farnese MAN Napoli 6353.jpg|gauche|vignette|Éros Farnèse, Musée archéologique national de Naples.]] Socrate, lorsque vient son tour, ne prononce pas un éloge d'Éros sous son propre nom : il rapporte un enseignement reçu d'une prêtresse de Mantinée, Diotime. Ce détour est remarquable. L'interlocuteur le plus dialectique du dialogue s'efface derrière une figure féminine, religieuse, aux allures d'oracle. Diotime commence par refuser la grammaire commune des éloges : Éros n'est ni beau, ni bon, ni dieu, contrairement à ce qu'ont affirmé les discours précédents. Il est intermédiaire, ''metaxu''. <blockquote> «&nbsp;C'est une assez longue histoire, je vais pourtant te la raconter. Il faut savoir que, le jour où naquit Aphrodite, les dieux festoyaient ; parmi eux se trouvait le fils de Mètis, Poros. Or, quand le banquet fut terminé, arriva Pénia, qui était venue mendier comme cela est naturel un jour de bombance, et elle se tenait sur le pas de la porte. Or Poros, qui s'était enivré de nectar, car le vin n'existait pas encore à cette époque, se traîna dans le jardin de Zeus et, appesanti par l'ivresse, s'y endormit. Alors, Pénia, dans sa pénurie, eut le projet de se faire faire un enfant par Poros ; elle s'étendit près de lui et devint grosse d'Éros. [...] Puis donc qu'il est le fils de Poros et de Pénia, Éros se trouve dans la condition que voici. D'abord, il est toujours pauvre, et il s'en faut de beaucoup qu'il soit délicat et beau, comme le croient la plupart des gens. Au contraire, il est rude, malpropre, va-nu-pieds et il n'a pas de gîte, couchant toujours par terre et à la dure, dormant à la belle étoile sur le pas des portes et sur le bord des chemins, car, puisqu'il tient de sa mère, c'est l'indigence qu'il a en partage. À l'exemple de son père en revanche, il est à l'affût de ce qui est beau et de ce qui est bon, il est viril, résolu, ardent, c'est un chasseur redoutable ; il ne cesse de tramer des ruses, il est passionné de savoir et fertile en expédients, il passe tout son temps à philosopher, c'est un sorcier redoutable, un magicien et un expert. [...] Par ailleurs, il se trouve à mi-chemin entre le savoir et l'ignorance. Voici en effet ce qui en est. Aucun dieu ne tend vers le savoir ni ne désire devenir savant, car il l'est ; or, si l'on est savant, on n'a pas besoin de tendre vers le savoir. Les ignorants ne tendent pas davantage vers le savoir ni ne désirent devenir savants. Mais c'est justement ce qu'il y a de fâcheux dans l'ignorance : alors que l'on n'est ni beau ni bon ni savant, on croit l'être suffisamment. Non, celui qui ne s'imagine pas en être dépourvu ne désire pas ce dont il ne croit pas devoir être pourvu. [...] Il va de soi, en effet, que le savoir compte parmi les choses qui sont les plus belles ; or Éros est amour du beau. Par suite, Éros doit nécessairement tendre vers le savoir, et, puisqu'il tend vers le savoir, il doit tenir le milieu entre celui qui sait et l'ignorant.&nbsp;» Platon, ''Le Banquet'', 203b-204b, trad. Luc Brisson, Paris, GF Flammarion, 1998, p. 142-145. </blockquote> La généalogie d'Éros est composée avec soin. ''Poros'', le père, signifie la «&nbsp;ressource&nbsp;», la «&nbsp;voie de passage&nbsp;», ce qui permet de se tirer d'affaire. ''Pénia'', la mère, est la «&nbsp;pauvreté&nbsp;», l'indigence. Éros n'est ni l'un ni l'autre : il ''tient'' de ses deux parents, ce qui est une manière platonicienne de dire qu'il occupe une position intermédiaire. Cette position intermédiaire est triple : entre mortel et immortel, entre savoir et ignorance, entre beauté et laideur. Diotime ajoute que le philosophe est précisément dans la situation d'Éros. Il n'est pas sage, puisque les sages sont les dieux ; il n'est pas ignorant, puisque l'ignorant ne sait même pas qu'il est ignorant et ne désire donc pas savoir. Le philosophe est celui qui sait qu'il ne sait pas et qui, précisément parce qu'il le sait, désire savoir. C'est là la meilleure formulation de la définition socratique rendue célèbre par l'''Apologie'' (21d). Deux points méritent d'être mis en lumière. Premièrement, le manque n'est plus, comme chez Aristophane, la perte d'une plénitude originaire : il est constitutif du désir lui-même, qui est toujours tension vers ce qu'il n'a pas encore. Diotime le formule explicitement : on ne désire que ce dont on manque (''Banquet'', 200a-b). Deuxièmement, l'amour du beau débouche sur l'amour du savoir par une continuité que Diotime décrit comme une ascension (''Banquet'', 210a-212a) : du beau corps singulier aux beaux corps, puis aux belles âmes, aux belles pratiques, aux belles connaissances, et enfin à la Beauté elle-même. L'éros philosophique n'est donc pas un détachement du désir sensible ; il en est le prolongement converti. == Socrate de Platon : la question socratique et la critique de l'écriture == [[Fichier:P1050775 Louvre statue Gudea statue A détail ecriture rwk.JPG|vignette|Détail d'écriture cunéiforme, statue de Gudea, Musée du Louvre.]] Le Socrate des dialogues n'est pas simplement le Socrate historique. Ce constat, qui nourrit ce que les spécialistes appellent depuis longtemps la «&nbsp;question socratique&nbsp;», ne conduit pas à la conclusion, trop tranchée, que Socrate serait un pur personnage fictif. Il était bien un Athénien historique, condamné à mort en 399 av. J.-C., que plusieurs contemporains (Xénophon dans les ''Mémorables'', Aristophane dans ''Les Nuées'', Eschine de Sphettos, et Platon) ont portraituré de manières divergentes. Les chercheurs modernes, de Gregory Vlastos (''Socrates. Ironist and Moral Philosopher'', Cornell University Press, 1991) à Louis-André Dorion (''Socrate'', Paris, PUF, coll. «&nbsp;Que sais-je ?&nbsp;», 2004), s'accordent cependant sur un point : on ne peut remonter avec certitude au Socrate «&nbsp;réel&nbsp;» par-delà les figurations qu'en ont proposées ses disciples et ses adversaires. Le Socrate de Platon est donc un personnage philosophique construit, dont la fonction évolue au fil de l'œuvre. Cette construction littéraire ne coupe cependant pas Platon de son témoignage. Plusieurs commentateurs tiennent les dialogues de jeunesse pour une approximation raisonnable du Socrate historique, et l'''Apologie de Socrate'' est souvent considérée comme le texte platonicien le plus proche de la parole que Socrate a réellement tenue devant ses juges. Il ne s'agit donc pas d'opposer un Socrate fictif à un Socrate réel, mais de reconnaître que la médiation de Platon est elle-même une source, travaillée par la visée philosophique de son auteur. On distingue classiquement trois périodes. Les dialogues de jeunesse, dits «&nbsp;socratiques&nbsp;» ou «&nbsp;aporétiques&nbsp;» (''Charmide'', ''Lachès'', ''Euthyphron'', ''Ion'', ''Hippias mineur'', et jusqu'au ''Ménon''), s'achèvent souvent sur une impasse ou sur un désaccord irrésolu. Les dialogues de la maturité (''Phédon'', ''Banquet'', ''République'', ''Phèdre'') déploient les thèses platoniciennes propres (théorie des Idées, immortalité de l'âme, tripartition de l'âme). Les dialogues tardifs (''Parménide'', ''Théétète'', ''Sophiste'', ''Politique'', ''Philèbe'', ''Timée'', ''Lois'') voient Socrate céder parfois la parole à d'autres figures, tandis que la théorie est soumise à autocritique. Dans le ''Gorgias'', la violence de Calliclès contre Socrate marque une tension particulière : l'interlocuteur refuse la loi morale au nom du droit du plus fort, et Socrate ne parvient pas à le convaincre, ce qui pose la question des limites de la persuasion philosophique face à qui refuse la règle du dialogue. Platon, lui, n'a jamais cessé d'écrire, alors même qu'il met en scène un Socrate qui n'écrit pas et qu'il formule à plusieurs reprises une critique de l'écriture. Le passage le plus connu se trouve dans le ''Phèdre'' (274c-275b), où Socrate rapporte le mythe égyptien de Theuth : ce dieu inventeur présente l'écriture au roi Thamous comme un «&nbsp;remède pour la mémoire et le savoir&nbsp;» ; Thamous lui répond qu'il s'agit, au contraire, d'un remède pour le rappel et non pour la mémoire vive, et que les hommes, se fiant à des traces extérieures, cesseront d'exercer leur propre pensée. La ''Lettre VII'' (341c-d) formule une réserve voisine : l'écrit ne peut pas transmettre ce qui se découvre seulement dans la rencontre prolongée d'un maître et d'un élève. Ces textes ne disqualifient pas l'écriture en bloc ; ils marquent une hiérarchie entre parole vivante et trace écrite, et mettent en garde contre une illusion : croire qu'on sait parce qu'on possède un texte. Il faut donc se garder de deux interprétations simplificatrices. La première affirmerait que Platon aurait constaté l'«&nbsp;échec&nbsp;» de la maïeutique socratique dans les dialogues aporétiques et s'en serait détourné. Or l'aporie n'est pas un échec : elle est la forme d'un apprentissage par la prise de conscience de l'ignorance, comme le montre l'entretien avec l'esclave du ''Ménon'' (82b-85b). La seconde affirmerait que la critique de l'écriture dans le ''Phèdre'' condamne le projet même d'écrire des dialogues. Mais Platon écrit, et la forme dialoguée est précisément celle qui se rapproche le plus de la conversation vivante. Sur ces débats, on peut se reporter à Monique Dixsaut, ''Le Naturel philosophe. Essai sur les dialogues de Platon'', Paris, Vrin, 2001. == Pourquoi Platon recourt-il aux mythes ? == Platon réserve le mythe à des moments précis : quand l'argument ne peut plus avancer par la seule dialectique ; quand la question excède les moyens de la démonstration (eschatologie, origine de l'âme, naissance du cosmos) ; quand il s'agit de persuader et non seulement de prouver ; quand la mise en image donne prise à l'imagination sans pour autant abdiquer la rigueur. Luc Brisson, dans ''Platon, les mots et les mythes'' (Paris, La Découverte, 1982 ; rééd. 1994), a montré que le mythe platonicien n'est pas un résidu pré-rationnel, mais un instrument philosophique spécifique : il articule une tradition (les récits hérités) et une invention (les récits composés par Platon), et il poursuit une fonction argumentative plus qu'illustrative. Le mythe, chez Platon, a donc plusieurs fonctions. Il introduit un problème sous une forme frappante (comme l'anneau de Gygès ouvre la question de la justice). Il prolonge l'argument en donnant à voir ce qu'il énonçait abstraitement (comme l'allégorie de la caverne prolonge la discussion sur l'éducation). Il répond à une question que la dialectique ne peut pas résoudre (comme les mythes eschatologiques du ''Gorgias'', du ''Phédon'' ou de la ''République'', qui traitent du sort des âmes). Dans tous les cas, il faut le lire comme un texte qui ''pense'', et non comme un ornement. === Le mythe de l'anneau de Gygès : un défi posé à Socrate === Le mythe de l'anneau de Gygès est l'un des plus commentés de l'œuvre platonicienne. Avant d'en proposer une lecture, il est nécessaire d'en situer précisément le contexte, sous peine de confondre la position d'un interlocuteur avec celle de Platon. Le récit apparaît dans la ''République'', livre II (359c-360b). Au livre I, Thrasymaque a défendu avec agressivité la thèse selon laquelle «&nbsp;la justice est l'intérêt du plus fort&nbsp;» (338c), thèse que Socrate n'a pas pleinement réfutée aux yeux de ses auditeurs. Au livre II, Glaucon et Adimante, frères de Platon, reprennent donc l'argument sous une forme plus exigeante. Glaucon annonce lui-même qu'il va exposer la position adverse avec le plus de vigueur possible, non parce qu'il y adhère, mais afin de contraindre Socrate à y répondre vraiment (''République'', II, 358c-d). Son argumentation se déploie en trois temps. Il expose d'abord la thèse «&nbsp;des gens&nbsp;» sur l'origine conventionnelle de la justice. Il cherche ensuite à montrer que même l'homme juste ne le serait que par impuissance. Il demande enfin à Socrate de prouver que la vie juste est préférable à la vie injuste, même pour celui qui pourrait être injuste impunément. L'anneau de Gygès intervient dans le deuxième temps. <blockquote> Les hommes prétendent que, par nature, il est bon de commettre l'injustice et mauvais de la souffrir, mais qu'il y a plus de mal à la souffrir que de bien à la commettre. Aussi, lorsque mutuellement ils la commettent et la subissent, et qu'ils goûtent des deux états, ceux qui ne peuvent point éviter l'un ni choisir l'autre estiment utile de s'entendre pour ne plus commettre ni subir l'injustice. De là prirent naissance les lois et les conventions, et l'on appela ce que prescrivait la loi légitime et juste. Voilà l'origine et l'essence de la justice : elle tient le milieu entre le plus grand bien (commettre impunément l'injustice) et le plus grand mal (la subir quand on est incapable de se venger). Entre ces deux extrêmes, la justice est aimée non comme un bien en soi, mais parce que l'impuissance de commettre l'injustice lui donne du prix. [...] Telle est donc, Socrate, la nature de la justice et telle son origine, selon l'opinion commune. Maintenant, que ceux qui la pratiquent agissent par impuissance de commettre l'injustice, c'est ce que nous sentirons particulièrement bien si nous faisons la supposition suivante. Donnons licence au juste et à l'injuste de faire ce qu'ils veulent ; suivons-les et regardons où, l'un et l'autre, les mène le désir. Nous prendrons le juste en flagrant délit de poursuivre le même but que l'injuste, poussé par le besoin de l'emporter sur les autres : c'est ce que recherche toute nature comme un bien, mais que, par loi et par force, on ramène au respect de l'égalité. La licence dont je parle serait surtout significative s'ils recevaient le pouvoir qu'eut jadis, dit-on, l'ancêtre de Gygès le Lydien. Cet homme était berger au service du roi qui gouvernait alors la Lydie. Un jour, au cours d'un violent orage accompagné d'un séisme, le sol se fendit et il se forma une ouverture béante près de l'endroit où il faisait paître son troupeau. Plein d'étonnement, il y descendit, et, entre autres merveilles que la fable énumère, il vit un cheval d'airain creux, percé de petites portes ; s'étant penché vers l'intérieur, il y aperçut un cadavre de taille plus grande, semblait-il, que celle d'un homme, et qui avait à la main un anneau d'or, dont il s'empara ; puis il partit sans prendre autre chose. Or, à l'assemblée habituelle des bergers qui se tenait chaque mois pour informer le roi de l'état de ses troupeaux, il se rendit portant au doigt cet anneau. Ayant pris place au milieu des autres, il tourna par hasard le chaton de la bague vers l'intérieur de sa main ; aussitôt il devint invisible à ses voisins qui parlèrent de lui comme s'il était parti. [...] S'étant rendu compte de cela, il répéta l'expérience pour voir si l'anneau avait bien ce pouvoir ; le même prodige se reproduisit : en tournant le chaton en dedans il devenait invisible, en dehors visible. Dès qu'il fut sûr de son fait, il fit en sorte d'être au nombre des messagers qui se rendaient auprès du roi. Arrivé au palais, il séduisit la reine, complota avec elle la mort du roi, le tua, et obtint ainsi le pouvoir. Si donc il existait deux anneaux de cette sorte, et que le juste reçût l'un, l'injuste l'autre, aucun, pense-t-on, ne serait de nature assez adamantine pour persévérer dans la justice et pour avoir le courage de ne pas toucher au bien d'autrui [...]. Platon, ''République'', II, 358e-360c, trad. Émile Chambry, Paris, Les Belles Lettres, coll. «&nbsp;Classiques en poche&nbsp;», 2002, p. 89-93 (autre traduction disponible : Georges Leroux, Paris, GF Flammarion, 2002). </blockquote> [[Fichier:Paphos Haus des Theseus - Mosaik Achilles 1.jpg|vignette|Mosaïque d'Achille, Maison de Thésée, Paphos.]] Il faut lire ce texte en gardant à l'esprit sa fonction dans l'économie du dialogue. Glaucon ne défend pas, à titre personnel, la thèse qu'il expose : il annonce en toutes lettres qu'il va la défendre avec vigueur uniquement pour que Socrate soit contraint d'y répondre (''République'', II, 358c-d). L'anneau de Gygès est donc une construction d'adversaire, présentée dans le cadre d'un exercice dialectique réglé, et non une affirmation que Platon voudrait soutenir en son nom propre. L'expérience de pensée qu'il supporte (que ferait un juste devenu invisible et impuni ?) est précisément conçue pour rendre l'objection inévitable. Or ce n'est pas dans cette scène que tient le cœur du dialogue, mais dans la réponse que Socrate construit sur l'ensemble de la ''République''. À partir du livre IV, il défend l'idée que la justice n'est pas simple convention extérieure, mais harmonie intérieure de l'âme, entre la raison, le cœur et le désir (''République'', IV, 441d-444a). Aux livres VIII et IX, il tente de montrer que l'homme juste est plus heureux que l'injuste, y compris dans le pire des cas. Le livre X s'achève sur un mythe eschatologique, celui d'Er le Pamphylien, qui affirme que les injustices finissent par se payer, quand bien même elles paraissent impunies dans cette vie. La thèse que Glaucon expose n'est donc pas celle de Platon, mais celle à laquelle Platon veut répondre. Confondre les deux, c'est attribuer à l'auteur la position même qu'il s'emploie à combattre. L'intérêt philosophique du récit est ailleurs : il met en lumière le caractère fragile des motivations qui nous tiennent à la justice dans la vie ordinaire, et invite à chercher, en dehors de toute récompense ou sanction extérieure, une raison plus profonde d'être juste. Les lectures modernes qu'on a voulu rapprocher du passage (Hobbes, Rousseau, Marx) sont intéressantes, mais supposent un changement de cadre théorique qu'il faut signaler comme tel. Hobbes, dans le ''Léviathan'' (1651), analyse l'état de nature comme un état où la pulsion de conservation, en l'absence de pouvoir commun, engendre la défiance mutuelle et la guerre ; la thèse de Glaucon contient effectivement une hypothèse qui peut y faire écho, mais Hobbes ne croit ni à une justice en soi ni à une harmonie intérieure de l'âme au sens platonicien. Rousseau, dans le ''Discours sur l'origine de l'inégalité'' (1755), propose une autre généalogie de la propriété et de la justice, qu'il relie à l'histoire sociale de l'homme et non à une nature injuste originaire. Marx, quant à lui, développe dans ''Sur la question juive'' (1843) puis dans la ''Critique du programme de Gotha'' (1875) une analyse du droit formel comme expression des rapports de production, étrangère à la problématique platonicienne du rapport entre la justice et le bien. Ces rapprochements peuvent nourrir la réflexion comparative, à condition de ne pas laisser entendre que Platon serait déjà hobbien, rousseauiste ou marxiste avant l'heure. == La place du philosophe dans la cité == '''Lecture conseillée''' : ''Apologie de Socrate'' (17a-42a, trad. Luc Brisson, Paris, GF Flammarion, 2005) ; [[Pour lire Platon/Études de quelques passages des dialogues]] (étude de la ''Lettre VII''). Socrate prononce plusieurs discours devant les juges d'Athènes, rapportés par Platon dans l{{'}}''Apologie de Socrate''. Il est accusé d'impiété (introduire de nouvelles divinités) et de corrompre la jeunesse (24b-c). Son activité principale, telle qu'il la décrit, consistait à interroger ses concitoyens sans enseigner de doctrine particulière : il se comparait à un taon posé sur un noble cheval pour le réveiller (30e). Se condamnera-t-il lui-même à l'exil, comme la cité semble l'y inviter ? Il s'y refuse (37c-38a) : il tient cet examen critique pour l'activité la plus précieuse qu'il puisse rendre à la cité. L'''Apologie'' pose ainsi une question qui parcourt toute l'œuvre de Platon : le philosophe, dans la cité, est-il nécessairement malvenu ? A-t-il une place, et si oui, à quel prix ? La réponse de la ''République'' (487a-502c, notamment l'allégorie du «&nbsp;vrai pilote&nbsp;») est sévère. Dans les cités actuelles, le philosophe est perçu comme inutile, parfois comme dangereux, précisément parce que les valeurs qui y dominent ne sont pas les siennes. Mais cette marginalité n'est pas sans remède : c'est pour penser les conditions d'une cité où philosophie et politique ne se contrarient plus que Platon écrit la ''République'' (et plus tard les ''Lois''). La ''Lettre VII'', dont l'authenticité reste discutée mais est aujourd'hui largement acceptée, rapporte l'expérience des voyages de Platon en Sicile auprès de Denys II de Syracuse, et montre à la fois la vocation politique du philosophe et les difficultés concrètes de son action. == Une philosophie politique en réponse au procès : pourquoi obéir aux lois ? == '''Lecture conseillée''' : ''Criton'' ([[s:Criton|sur Wikisource]]), passages cités d'après la trad. Luc Brisson, Paris, GF Flammarion, 1997. Quelques jours avant son exécution, Socrate reçoit dans sa prison la visite d'un vieil ami, Criton. Celui-ci lui propose de s'évader : les conditions matérielles sont réunies, les gardiens se laisseront corrompre, des amis sont prêts à l'accueillir à Thessalie ou ailleurs. Socrate refuse, et engage avec Criton un dialogue qui occupe la seconde moitié du texte. === Le principe fondateur : ne jamais commettre l'injustice === Avant même que les Lois ne prennent la parole, Socrate pose avec Criton un principe qui commande tout le reste du dialogue : il ne faut jamais commettre d'injustice, pas même en réponse à une injustice (''Criton'', 49a-e). Cette thèse éthique est présentée comme irréductible. Répondre au mal par le mal ne produit que du mal, et la valeur d'une vie ne se mesure pas au nombre d'années vécues mais à la rectitude des actions (48b). Socrate demande expressément à Criton s'il accepte ce principe, et note que très peu d'hommes y adhèrent, tandis que ceux qui l'acceptent ne peuvent plus s'entendre avec ceux qui le refusent (49c-d). De là découle la question qui va occuper la suite du dialogue. S'évader ne serait-il pas, précisément, commettre une injustice envers les lois d'Athènes, et cela en réponse à l'injustice qu'Athènes a commise en condamnant Socrate à mort ? Pour répondre, Socrate fait parler les Lois : c'est la prosopopée<ref>Figure de rhétorique par laquelle on fait parler une personne absente, morte, un animal, une chose, ou, comme ici, une abstraction juridique.</ref> fameuse du ''Criton'' (50a-54d). On remarquera que cette prosopopée n'est pas un point de départ, mais une conséquence : elle dépend du principe éthique que Criton vient d'accepter quelques pages plus tôt. Tout le reste consiste à vérifier ce qu'un tel principe implique, dans la situation concrète où Socrate se trouve. === Le contrat implicite entre le citoyen et la cité === <blockquote> Vois si tu l'entendras de cette autre manière : Au moment de nous enfuir ou de sortir d'ici, quel que soit le mot qu'il te plaira de choisir, si les Lois et la République venaient se présenter devant nous, et nous disaient : «&nbsp;Réponds-moi, Socrate, que vas-tu faire ? L'action que tu entreprends a-t-elle d'autre but que de nous détruire, nous qui sommes les Lois, et avec nous la République tout entière, autant qu'il dépend de toi ? Ou te semble-t-il possible que l'État subsiste et ne soit pas renversé, lorsque les arrêts rendus restent sans force et que de simples particuliers leur enlèvent l'effet et la sanction qu'ils doivent avoir ?&nbsp;» Platon, ''Criton'', 50a-b. </blockquote> Le premier argument des Lois est institutionnel. Si chaque particulier pouvait refuser d'exécuter une sentence qu'il juge injuste, aucun arrêt de justice ne tiendrait et l'État s'effondrerait. Ce premier point est énoncé sous forme de question rhétorique et fait appel au bon sens juridique. Les Lois développent ensuite un deuxième argument, plus ambitieux, sous la forme d'une analogie filiale : <blockquote> «&nbsp;Eh quoi ! Socrate, diraient les Lois, est-ce là ce dont nous étions convenues avec toi ? Ou plutôt n'étions-nous pas convenues avec toi que les jugements rendus par la République seraient exécutés ?&nbsp;» [...] «&nbsp;Voyons : quel sujet de plainte as-tu contre nous et contre la République pour entreprendre ainsi de nous renverser ? Et d'abord, n'est-ce pas nous qui t'avons donné la vie ? N'est-ce pas nous qui avons présidé à l'union de ton père et de ta mère, ainsi qu'à ta naissance ? [...] Mais, puisque c'est à nous que tu dois ta naissance, ta nourriture et ton éducation, peux-tu nier que tu sois notre enfant, notre esclave même, toi et tes ancêtres ? Et s'il en est ainsi, crois-tu que tu aies contre nous les mêmes droits que nous avons contre toi, et que tout ce que nous pourrions entreprendre contre toi, tu puisses à ton tour l'entreprendre justement contre nous ? [...] Ta sagesse va-t-elle jusqu'à ignorer que la patrie est, aux yeux des dieux et des hommes sensés, quelque chose de plus cher, plus respectable, plus auguste et plus saint qu'une mère, un père et tous les aïeux ? qu'il faut avoir pour la patrie, même irritée, plus de respect, de soumission et d'égard, que pour un père ? qu'il faut l'adoucir par la persuasion ou faire tout ce qu'elle ordonne [...] ?&nbsp;» Platon, ''Criton'', 50c-51c. </blockquote> Les Lois rappellent à Socrate les bienfaits reçus : mariage légitime de ses parents, éducation, protection, partage des biens communs. Cette dette fonde, selon leur propos, une autorité comparable à celle d'un parent, et même supérieure. Il faut prendre au sérieux la dissymétrie qui est ici affirmée : le citoyen ne peut pas répondre à la loi comme un égal répond à un égal. Ce point, lourd de conséquences, sera discuté par toute la tradition philosophique ultérieure. On peut dès maintenant souligner qu'il ne coïncide pas avec ce qu'on appellera plus tard «&nbsp;contrat social&nbsp;» à la manière moderne : il n'est pas question ici d'un acte volontaire par lequel des individus libres instituent l'État, mais d'un rapport de dette envers une institution qui précède le citoyen et le rend possible. Les Lois précisent aussitôt un second volet, qui tempère l'image de la soumission filiale. Tout citoyen devenu adulte (éphèbe) peut choisir de s'en aller librement, avec ses biens. <blockquote> «&nbsp;Nous déclarons encore, et c'est un droit que nous reconnaissons à tout Athénien qui veut en user, qu'aussitôt qu'il a été reçu dans la classe des éphèbes, qu'il a vu ce qui se passe dans la République, et qu'il nous a vues aussi, nous qui sommes les Lois, il est libre, si nous ne lui plaisons pas, d'emporter ce qu'il possède et de se retirer où il voudra. Et si quelqu'un d'entre vous veut aller dans une colonie, parce que nous lui déplaisons, nous et la République, si même il veut aller s'établir quelque part à l'étranger, aucune de nous ne s'y oppose et ne le défend : il peut aller partout où il voudra avec tous ses biens. Mais quant à celui de vous qui persiste à demeurer ici, en voyant de quelle manière nous rendons la justice et nous administrons toutes les affaires de la république, nous déclarons dès lors que par le fait il s'est engagé envers nous à faire tout ce que nous lui ordonnerions, et s'il n'obéit pas, nous le déclarons trois fois coupable : d'abord, parce qu'il nous désobéit à nous qui lui avons donné la vie, ensuite parce que c'est nous qui l'avons élevé, enfin parce qu'ayant pris l'engagement d'être soumis, il ne veut ni obéir ni employer la persuasion à notre égard, si nous faisons quelque chose qui ne soit pas bien ; et tandis que nous lui proposons à titre de simple proposition, et non comme un ordre tyrannique, de faire ce que nous lui commandons, lui laissant même le choix d'en appeler à la persuasion ou d'obéir, il ne fait ni l'un ni l'autre.&nbsp;» Platon, ''Criton'', 51c-52a. </blockquote> Deux idées méritent d'être dégagées. D'une part, la cité n'est pas présentée comme une instance qui retient par la force : qui n'est pas satisfait peut partir. Rester, c'est implicitement s'engager à obéir. D'autre part, l'obéissance n'exclut pas la discussion. Les Lois ménagent une alternative expresse, «&nbsp;obéir ou persuader&nbsp;» (''peíthein ē poieîn'', 51b-c). Celui qui juge injuste ce qu'on lui commande a le droit, et même le devoir, de tenter de convaincre. Ce n'est qu'après l'échec de cette persuasion qu'il doit exécuter la sentence. La prosopopée ne défend donc pas une obéissance aveugle : elle définit une obéissance réfléchie, exercée à l'intérieur d'un espace de discussion. === Socrate et la décision d'obéir === Les Lois prolongent leur plaidoyer en mettant en évidence la cohérence propre de Socrate. Il n'a jamais quitté Athènes, sinon pour des campagnes militaires et une visite à l'Isthme de Corinthe ; il y a fondé une famille ; lors du procès, il a préféré la mort à l'exil (''Apologie'', 37c-38a). S'évader maintenant reviendrait à contredire tout ce qu'il a dit et fait jusque-là. <blockquote> «&nbsp;Or, tu n'as préféré le séjour ni de Lacédémone, ni de la Crète, dont tu vantes sans cesse le gouvernement, ni d'aucune ville grecque ou barbare, mais tu es sorti d'Athènes moins souvent que les boiteux, les aveugles et les autres infirmes : preuve évidente que tu avais plus d'amour que les autres Athéniens pour cette ville et pour nous-mêmes qui sommes les Lois de cette ville : car peut-on aimer une cité sans en aimer les lois ?&nbsp;» Platon, ''Criton'', 52b-53a. </blockquote> L'argument se déplace ici du terrain institutionnel à celui de l'identité. Qui Socrate serait-il, s'il fuyait ? Comment continuerait-il à tenir, en exil, les discours qu'il a tenus toute sa vie sur la vertu, sur la justice, sur la loi ? La question n'est pas seulement ce qu'il a le droit de faire, mais ce qu'il peut faire sans cesser d'être lui-même. La fidélité à la cité se confond alors avec une fidélité à soi. On retrouve ici, appliqué à une situation concrète, le principe posé au début du dialogue : s'évader reviendrait à répondre au mal par le mal, donc à trahir précisément la thèse éthique que Socrate a passé sa vie à défendre. <blockquote> «&nbsp;Maintenant, au contraire, si tu meurs, tu meurs victime de l'injustice, non des lois, mais des hommes, au lieu que, si tu sors de la ville, après avoir si honteusement commis l'injustice à ton tour, rendu le mal pour le mal, violé toutes les conventions, tous les engagements que tu as contractés envers nous, maltraité ceux que tu devrais le plus ménager, toi-même, tes amis, ta patrie et nous, alors nous te poursuivrons de notre inimitié pendant ta vie [...].&nbsp;» Platon, ''Criton'', 54b-c. </blockquote> Ce passage distingue soigneusement les lois et les hommes qui les appliquent. La sentence qui condamne Socrate peut être injuste, dans la mesure où les juges se sont trompés ; les lois elles-mêmes ne sont pas en cause. Socrate n'obéit donc pas à une loi injuste au sens où la loi serait intrinsèquement mauvaise ; il obéit à une loi juste appliquée injustement par des hommes. On voit que le ''Criton'' ne prescrit pas une obéissance inconditionnelle à n'importe quelle prescription. La difficulté apparaît, et a été amplement discutée par les commentateurs, lorsqu'on tente d'articuler cette position avec celle de l'''Apologie'' (29d-30a), où Socrate affirme qu'il obéirait au dieu plutôt qu'aux juges s'ils lui ordonnaient de cesser de philosopher. === Limites et portée d'une lecture moderne === On a longtemps tenté de lire ce texte à la lumière des théories modernes du contrat social (Hobbes, Locke, Rousseau), ou dans une perspective critique inspirée de Marx qui y verrait l'alibi d'une domination juridique. Ces rapprochements demandent prudence. Les Lois du ''Criton'' ne se posent pas comme un contrat librement consenti par des individus préexistants à la cité, mais comme une instance qui précède le citoyen et lui donne son existence sociale. Et la critique marxiste du droit, formulée dans ''Sur la question juive'' et dans la ''Critique du programme de Gotha'', repose sur une analyse économique du rapport entre droit formel et rapports de production, tout à fait étrangère à l'horizon de Platon. On peut s'en inspirer pour interroger Platon, non pour le traduire. Une lecture plus proche du texte permet pourtant de dégager une intuition qu'Alain, bien plus tard, a reformulée avec netteté : <blockquote> «&nbsp;Résistance et obéissance, voilà les deux vertus du citoyen. Par l'obéissance, il assure l'ordre ; par la résistance il assure la liberté. Et il est bien clair que l'ordre et la liberté ne sont point séparables, car le jeu des forces, c'est-à-dire la guerre privée, à toute minute, n'enferme aucune liberté ; c'est une vie animale, livrée à tous les hasards. Donc les deux termes, ordre et liberté, sont bien loin d'être opposés ; j'aime mieux dire qu'ils sont corrélatifs. La liberté ne va pas sans l'ordre ; l'ordre ne vaut rien sans la liberté. Obéir en résistant, c'est tout le secret. Ce qui détruit l'obéissance est anarchie ; ce qui détruit la résistance est tyrannie.&nbsp;» Alain, ''Propos d'un Normand'', 4 septembre 1912, éd. M. Savin, Paris, Gallimard, coll. «&nbsp;Bibliothèque de la Pléiade&nbsp;», 1956, t. II, p. 164. </blockquote> La formule d'Alain n'est pas une paraphrase du ''Criton'', mais un écho. Elle aide à saisir ce que Platon a déjà pensé : une loi qui n'admettrait aucune critique tomberait dans la tyrannie, une critique qui refuserait toute obéissance tomberait dans l'anarchie. L'«&nbsp;obéir ou persuader&nbsp;» du ''Criton'' tient précisément cet équilibre. Il ne légitime ni le conformisme silencieux, ni la désobéissance sans discussion préalable. Dans le cas particulier de Socrate, la persuasion a échoué (les juges n'ont pas été convaincus) : reste l'obéissance, et cette obéissance consentie est, paradoxalement, la preuve ultime de la liberté philosophique de Socrate, celle qui le distingue de tout personnage comme Calliclès, pour qui la loi n'est qu'un masque de la faiblesse (''Gorgias'', 482c-486d). == Liste des mythes platoniciens == Voici une liste non exhaustive des principaux mythes et allégories présents dans l'œuvre de Platon, avec leur localisation stéphanienne. Chacun pourra faire l'objet d'une étude séparée. * '''Mythe de l'androgyne''' : discours d'Aristophane (''Banquet'', 189d-193d). * '''Généalogie d'Éros''' : Poros et Pénia, enseignement de Diotime (''Banquet'', 203b-204b). * '''Allégorie de la ligne''' (''République'', VI, 509d-511e) et '''allégorie de la caverne''' (''République'', VII, 514a-517a). * '''Anneau de Gygès''' : défi de Glaucon (''République'', II, 359c-360b). * '''Mythe d'Er le Pamphylien''' : eschatologie, choix des vies (''République'', X, 614b-621d). * '''Mythe de l'attelage ailé''' : tripartition de l'âme, chute et remontée vers les Idées (''Phèdre'', 246a-256e). * '''Mythe de Theuth''' : invention de l'écriture et critique de la mémoire écrite (''Phèdre'', 274c-275b). * '''Mythe de Prométhée''' : discours de Protagoras sur l'origine des arts et de la politique (''Protagoras'', 320c-322d). * '''Mythes eschatologiques''' : jugement des âmes (''Gorgias'', 523a-527a ; ''Phédon'', 107c-114c ; ''République'', X, 614b-621d). * '''Atlantide''' : cité idéale engloutie, récit rapporté par Critias (''Timée'', 20d-26e ; ''Critias'', 108e-121c). * '''Démiurge et fabrication du cosmos''' : cosmogonie (''Timée'', 27d-47e). * '''Origine des noms et des langues''' (''Cratyle'', en partie). == Notes == {{références}} == Bibliographie sommaire == === Éditions et traductions de référence === * Platon, ''Œuvres complètes'', sous la direction de Luc Brisson, Paris, Flammarion, 2008 (un volume unique regroupant l'ensemble des dialogues). * Platon, ''Œuvres complètes'', trad. Léon Robin, Paris, Gallimard, coll. «&nbsp;Bibliothèque de la Pléiade&nbsp;», 2 vol., 1940-1942. * Platon, ''Le Banquet'', trad. Luc Brisson, Paris, GF Flammarion, 1998. * Platon, ''Théétète'', trad. Michel Narcy, Paris, GF Flammarion, 1994. * Platon, ''La République'', trad. Georges Leroux, Paris, GF Flammarion, 2002 ; trad. Émile Chambry, Paris, Les Belles Lettres, 2002. * Platon, ''Apologie de Socrate, Criton'', trad. Luc Brisson, Paris, GF Flammarion, 1997 (rééd. 2005). * Platon, ''Phèdre'', trad. Luc Brisson, Paris, GF Flammarion, 2004. * Platon, ''Gorgias'', trad. Monique Canto-Sperber, Paris, GF Flammarion, 1987 (rééd. 2007). * Platon, ''Phédon'', trad. Monique Dixsaut, Paris, GF Flammarion, 1991. * Platon, ''Protagoras'', trad. Frédérique Ildefonse, Paris, GF Flammarion, 1997. * Platon, ''Lettres'', trad. Luc Brisson, Paris, GF Flammarion, 1987 (rééd. 2004). === Études === * Luc Brisson, ''Platon, les mots et les mythes. Comment et pourquoi Platon nomma le mythe ?'', Paris, La Découverte, 1982 (rééd. 1994). * Monique Dixsaut, ''Le Naturel philosophe. Essai sur les dialogues de Platon'', Paris, Vrin, 2001. * Monique Dixsaut, ''Platon'', Paris, Vrin, coll. «&nbsp;Bibliothèque des philosophies&nbsp;», 2003. * Louis-André Dorion, ''Socrate'', Paris, PUF, coll. «&nbsp;Que sais-je ?&nbsp;», 2004. * Gregory Vlastos, ''Socrate. Ironie et philosophie morale'', trad. fr. Catherine Dalimier, Paris, Aubier, 1994 (éd. originale Cornell University Press, 1991). * Monique Canto-Sperber (éd.), ''Philosophie grecque'', Paris, PUF, 1997. * Julia Annas, ''Introduction à la République de Platon'', trad. fr. Béatrice Han, Paris, PUF, 1994. * Michel Narcy, ''Le Philosophe et son double. Un commentaire de l'Euthydème de Platon'', Paris, Vrin, 1984. * ''Stanford Encyclopedia of Philosophy'', entrée «&nbsp;Plato's Myths&nbsp;» (Catalin Partenie), 2009 (mise à jour 2022), [https://plato.stanford.edu/entries/plato-myths/ en ligne]. 40ccuwwjm9reruenq1qaua4lt7rg26x 764009 764005 2026-04-19T15:53:51Z PandaMystique 119061 764009 wikitext text/x-wiki <noinclude>{{sous-pages}}</noinclude> Invitons-nous à la table philosophique de Platon. Chez lui, le mythe n'est ni un simple ornement, ni une concession à l'imagination : il prolonge l'argument là où la dialectique rencontre ses limites et donne à penser par images ce que la seule discussion conceptuelle transmet mal. Cette page parcourt quelques grands récits et métaphores platoniciens : la généalogie d'Éros, l'anneau de Gygès, la prosopopée des lois du ''Criton''. À chaque fois, on s'efforcera de signaler qui parle dans le dialogue, dans quel contexte, et à quel degré Platon engage sa propre position. La liste des questions proposées n'est pas close. == La philosophie == === Fille de l'étonnement : une affection avant d'être un savoir === [[Fichier:'Paris à l'Arc-en-ciel' by Robert Delaunay, 1914.jpg|vignette|''Paris à l'Arc-en-ciel'', Robert Delaunay, 1914.]] <blockquote> « Cette émotion, l'étonnement, est tout à fait d'un philosophe : la philosophie n'a pas d'autre origine, et celui qui a fait d'Iris la fille de Thaumas n'est pas un mauvais généalogiste. » Platon, ''Théétète'', 155d, trad. Michel Narcy, Paris, GF Flammarion, 1994, p. 163. </blockquote> Le propos est adressé par Socrate au jeune Théétète, embarrassé par sa propre incapacité à répondre à une question qui paraissait simple : qu'est-ce que le savoir ? Socrate le rassure. Ce trouble n'est pas un défaut : il est l'affect qui ouvre l'enquête philosophique. L'étonnement (''thaumazein'') désigne ici le moment où ce qui paraissait aller de soi cesse d'aller de soi. Aristote reprendra cette idée au livre A de la ''Métaphysique'' (982b 12-20), en faisant de l'étonnement le point de départ des premières recherches sur la nature. La référence mythologique sur laquelle s'appuie Socrate est discrète. Iris, messagère des dieux dans la tradition homérique et hésiodique, est fille de Thaumas, dont le nom évoque la « merveille » (''thauma''). Socrate s'en tient à ce jeu étymologique : le nom de la mère d'Iris désigne la même affection que celle éprouvée par le philosophe. Il ne dit rien de plus, et le texte n'invite pas à prolonger davantage l'image. Ce que Platon affirme, c'est que la philosophie naît d'une affection et non d'un simple état intellectuel : le philosophe n'est ni stupéfait ni béat ; il est saisi par ce qui, dans l'expérience ordinaire, appelle à être interrogé. === Le désir du savoir : la question du manque dans ''le Banquet'' === [[Fichier:Chouette Puy dy Fou.jpg|gauche|vignette|La chouette, figure associée à Athéna et à la philosophie.]] ==== Étymologie et énigme ==== L'étymologie du mot ''philosophia'' («&nbsp;amour de la sagesse&nbsp;» ou «&nbsp;du savoir&nbsp;») est connue, mais sa portée mérite qu'on s'y arrête. Pour deux raisons au moins. D'abord, la sagesse semble relever d'un état intellectuel, et il n'est pas évident qu'un contenu de pensée puisse susciter une affection, moins encore un désir proprement amoureux. Ensuite, dire qu'il n'y a pas de savoir sans désir engage une conception du philosophe très différente du modèle du sage achevé : le philosophe n'est pas celui qui possède la sagesse, mais celui qui tend vers elle. C'est cette tension que ''le Banquet'' tente de rendre intelligible, au moyen de plusieurs discours emboîtés. Le dialogue met en scène une succession d'éloges d'Éros prononcés lors d'un banquet. Chaque discours déplace et précise l'analyse du précédent. Aristophane y tient une place singulière, et Diotime la place ultime, celle que Socrate adopte comme la plus haute. La composition du dialogue est donc ordonnée : on ne peut lire le discours d'Aristophane comme la pensée de Platon, pas plus qu'on ne peut isoler les discours antérieurs du mouvement qui les conduit vers l'enseignement de Diotime. [[Fichier:Nature dévoilée par Philosophie.jpg|vignette|''La Nature se dévoilant devant la Philosophie'', allégorie classique.]] ==== Le discours d'Aristophane : Éros comme nostalgie du même ==== Le poète comique Aristophane<ref>Théâtre complet d'Aristophane sur [[s:Auteur:Aristophane|Wikisource]].</ref> vient de se remettre d'un hoquet persistant lorsqu'il prend la parole (''Banquet'', 185c-e). Le détail a son importance : Platon, ailleurs critique à l'égard de la comédie, prête à Aristophane un discours brillant tout en l'inscrivant dans un cadre où l'ivresse et les fonctions du corps sont présentes, rappelant que le dialogue ne se tient pas dans la froideur d'un tribunal académique. Aristophane raconte alors le mythe de l'androgyne (''Banquet'', 189d-193d, trad. Luc Brisson, Paris, GF Flammarion, 1998, p. 117-123). Les humains, à l'origine, formaient des êtres sphériques complets ; pris d'orgueil, ils voulurent escalader le ciel et se mesurer aux dieux. Zeus, pour les punir, les coupa en deux : le nombril est la trace de cette cicatrice. Depuis, chaque moitié cherche sa moitié perdue. L'amour serait donc le désir de retrouver une unité perdue. Deux points méritent d'être soulignés. Le discours d'Aristophane fait de l'amour une ''nostalgie'' : on désire ce que l'on a déjà eu et que l'on a perdu. Le manque est ici pensé comme défaut d'une plénitude antérieure. Mais cette analyse, dans l'économie du dialogue, n'est pas le dernier mot. Diotime, rapportée par Socrate, la déplacera en affirmant que l'amour ne tend pas à rejoindre «&nbsp;sa moitié&nbsp;», mais ce qui est bon et beau en tant que tel (''Banquet'', 205d-206a). Le discours d'Aristophane est donc une étape, non une thèse définitive. ==== Le discours de Diotime : Éros entre manque et ressource ==== [[Fichier:Eros Farnese MAN Napoli 6353.jpg|gauche|vignette|Éros Farnèse, Musée archéologique national de Naples.]] Socrate, lorsque vient son tour, ne prononce pas un éloge d'Éros sous son propre nom : il rapporte un enseignement reçu d'une prêtresse de Mantinée, Diotime. Ce détour est remarquable. L'interlocuteur le plus dialectique du dialogue s'efface derrière une figure féminine, religieuse, aux allures d'oracle. Diotime commence par refuser la grammaire commune des éloges : Éros n'est ni beau, ni bon, ni dieu, contrairement à ce qu'ont affirmé les discours précédents. Il est intermédiaire, ''metaxu''. <blockquote> «&nbsp;C'est une assez longue histoire, je vais pourtant te la raconter. Il faut savoir que, le jour où naquit Aphrodite, les dieux festoyaient ; parmi eux se trouvait le fils de Mètis, Poros. Or, quand le banquet fut terminé, arriva Pénia, qui était venue mendier comme cela est naturel un jour de bombance, et elle se tenait sur le pas de la porte. Or Poros, qui s'était enivré de nectar, car le vin n'existait pas encore à cette époque, se traîna dans le jardin de Zeus et, appesanti par l'ivresse, s'y endormit. Alors, Pénia, dans sa pénurie, eut le projet de se faire faire un enfant par Poros ; elle s'étendit près de lui et devint grosse d'Éros. [...] Puis donc qu'il est le fils de Poros et de Pénia, Éros se trouve dans la condition que voici. D'abord, il est toujours pauvre, et il s'en faut de beaucoup qu'il soit délicat et beau, comme le croient la plupart des gens. Au contraire, il est rude, malpropre, va-nu-pieds et il n'a pas de gîte, couchant toujours par terre et à la dure, dormant à la belle étoile sur le pas des portes et sur le bord des chemins, car, puisqu'il tient de sa mère, c'est l'indigence qu'il a en partage. À l'exemple de son père en revanche, il est à l'affût de ce qui est beau et de ce qui est bon, il est viril, résolu, ardent, c'est un chasseur redoutable ; il ne cesse de tramer des ruses, il est passionné de savoir et fertile en expédients, il passe tout son temps à philosopher, c'est un sorcier redoutable, un magicien et un expert. [...] Par ailleurs, il se trouve à mi-chemin entre le savoir et l'ignorance. Voici en effet ce qui en est. Aucun dieu ne tend vers le savoir ni ne désire devenir savant, car il l'est ; or, si l'on est savant, on n'a pas besoin de tendre vers le savoir. Les ignorants ne tendent pas davantage vers le savoir ni ne désirent devenir savants. Mais c'est justement ce qu'il y a de fâcheux dans l'ignorance : alors que l'on n'est ni beau ni bon ni savant, on croit l'être suffisamment. Non, celui qui ne s'imagine pas en être dépourvu ne désire pas ce dont il ne croit pas devoir être pourvu. [...] Il va de soi, en effet, que le savoir compte parmi les choses qui sont les plus belles ; or Éros est amour du beau. Par suite, Éros doit nécessairement tendre vers le savoir, et, puisqu'il tend vers le savoir, il doit tenir le milieu entre celui qui sait et l'ignorant.&nbsp;» Platon, ''Le Banquet'', 203b-204b, trad. Luc Brisson, Paris, GF Flammarion, 1998, p. 142-145. </blockquote> La généalogie d'Éros est composée avec soin. ''Poros'', le père, signifie la «&nbsp;ressource&nbsp;», la «&nbsp;voie de passage&nbsp;», ce qui permet de se tirer d'affaire. ''Pénia'', la mère, est la «&nbsp;pauvreté&nbsp;», l'indigence. Éros n'est ni l'un ni l'autre : il ''tient'' de ses deux parents, ce qui est une manière platonicienne de dire qu'il occupe une position intermédiaire. Cette position intermédiaire est triple : entre mortel et immortel, entre savoir et ignorance, entre beauté et laideur. Diotime ajoute que le philosophe est précisément dans la situation d'Éros. Il n'est pas sage, puisque les sages sont les dieux ; il n'est pas ignorant, puisque l'ignorant ne sait même pas qu'il est ignorant et ne désire donc pas savoir. Le philosophe est celui qui sait qu'il ne sait pas et qui, précisément parce qu'il le sait, désire savoir. C'est là la meilleure formulation de la définition socratique rendue célèbre par l'''Apologie'' (21d). Deux points méritent d'être mis en lumière. Premièrement, le manque n'est plus, comme chez Aristophane, la perte d'une plénitude originaire : il est constitutif du désir lui-même, qui est toujours tension vers ce qu'il n'a pas encore. Diotime le formule explicitement : on ne désire que ce dont on manque (''Banquet'', 200a-b). Deuxièmement, l'amour du beau débouche sur l'amour du savoir par une continuité que Diotime décrit comme une ascension (''Banquet'', 210a-212a) : du beau corps singulier aux beaux corps, puis aux belles âmes, aux belles pratiques, aux belles connaissances, et enfin à la Beauté elle-même. L'éros philosophique n'est donc pas un détachement du désir sensible ; il en est le prolongement converti. == Socrate de Platon : la question socratique et la critique de l'écriture == [[Fichier:P1050775 Louvre statue Gudea statue A détail ecriture rwk.JPG|vignette|Détail d'écriture cunéiforme, statue de Gudea, Musée du Louvre.]] Le Socrate des dialogues n'est pas simplement le Socrate historique. Ce constat, qui nourrit ce que les spécialistes appellent depuis longtemps la «&nbsp;question socratique&nbsp;», ne conduit pas à la conclusion, trop tranchée, que Socrate serait un pur personnage fictif. Il était bien un Athénien historique, condamné à mort en 399 av. J.-C., que plusieurs contemporains (Xénophon dans les ''Mémorables'', Aristophane dans ''Les Nuées'', Eschine de Sphettos, et Platon) ont portraituré de manières divergentes. Les chercheurs modernes, de Gregory Vlastos (''Socrates. Ironist and Moral Philosopher'', Cornell University Press, 1991) à Louis-André Dorion (''Socrate'', Paris, PUF, coll. «&nbsp;Que sais-je ?&nbsp;», 2004), s'accordent cependant sur un point : on ne peut remonter avec certitude au Socrate «&nbsp;réel&nbsp;» par-delà les figurations qu'en ont proposées ses disciples et ses adversaires. Le Socrate de Platon est donc un personnage philosophique construit, dont la fonction évolue au fil de l'œuvre. Cette construction littéraire ne coupe cependant pas Platon de son témoignage. La recherche contemporaine sur ce qu'on appelle le « problème socratique » insiste précisément sur la difficulté de départager ce qui relève du Socrate historique, du Socrate mis en scène par Platon, et de la construction philosophique propre à chaque dialogue. Plusieurs commentateurs tiennent les dialogues de jeunesse pour une approximation raisonnable du Socrate historique, et l'''Apologie de Socrate'' est fréquemment considérée, parmi les œuvres de Platon, comme celle qui se rapproche le plus de la parole que Socrate a réellement tenue devant ses juges. Il ne s'agit donc pas d'opposer un Socrate fictif à un Socrate réel, mais de reconnaître que la médiation de Platon est elle-même une source, travaillée par la visée philosophique de son auteur. On distingue classiquement trois périodes. Les dialogues de jeunesse, dits «&nbsp;socratiques&nbsp;» ou «&nbsp;aporétiques&nbsp;» (''Charmide'', ''Lachès'', ''Euthyphron'', ''Ion'', ''Hippias mineur'', et jusqu'au ''Ménon''), s'achèvent souvent sur une impasse ou sur un désaccord irrésolu. Les dialogues de la maturité (''Phédon'', ''Banquet'', ''République'', ''Phèdre'') déploient les thèses platoniciennes propres (théorie des Idées, immortalité de l'âme, tripartition de l'âme). Les dialogues tardifs (''Parménide'', ''Théétète'', ''Sophiste'', ''Politique'', ''Philèbe'', ''Timée'', ''Lois'') voient Socrate céder parfois la parole à d'autres figures, tandis que la théorie est soumise à autocritique. Dans le ''Gorgias'', la violence de Calliclès contre Socrate marque une tension particulière : l'interlocuteur refuse la loi morale au nom du droit du plus fort, et Socrate ne parvient pas à le convaincre, ce qui pose la question des limites de la persuasion philosophique face à qui refuse la règle du dialogue. Platon, lui, n'a jamais cessé d'écrire, alors même qu'il met en scène un Socrate qui n'écrit pas et qu'il formule à plusieurs reprises une critique de l'écriture. Le passage le plus connu se trouve dans le ''Phèdre'' (274c-275b), où Socrate rapporte le mythe égyptien de Theuth : ce dieu inventeur présente l'écriture au roi Thamous comme un «&nbsp;remède pour la mémoire et le savoir&nbsp;» ; Thamous lui répond qu'il s'agit, au contraire, d'un remède pour le rappel et non pour la mémoire vive, et que les hommes, se fiant à des traces extérieures, cesseront d'exercer leur propre pensée. La ''Lettre VII'' (341c-d) formule une réserve voisine : l'écrit ne peut pas transmettre ce qui se découvre seulement dans la rencontre prolongée d'un maître et d'un élève. Ces textes ne disqualifient pas l'écriture en bloc ; ils marquent une hiérarchie entre parole vivante et trace écrite, et mettent en garde contre une illusion : croire qu'on sait parce qu'on possède un texte. Il faut donc se garder de deux interprétations simplificatrices. La première affirmerait que Platon aurait constaté l'«&nbsp;échec&nbsp;» de la maïeutique socratique dans les dialogues aporétiques et s'en serait détourné. Or l'aporie n'est pas un échec : elle est la forme d'un apprentissage par la prise de conscience de l'ignorance, comme le montre l'entretien avec l'esclave du ''Ménon'' (82b-85b). La seconde affirmerait que la critique de l'écriture dans le ''Phèdre'' condamne le projet même d'écrire des dialogues. Mais Platon écrit, et la forme dialoguée est précisément celle qui se rapproche le plus de la conversation vivante. Sur ces débats, on peut se reporter à Monique Dixsaut, ''Le Naturel philosophe. Essai sur les dialogues de Platon'', Paris, Vrin, 2001. == Pourquoi Platon recourt-il aux mythes ? == Platon réserve le mythe à des moments précis : quand l'argument ne peut plus avancer par la seule dialectique ; quand la question excède les moyens de la démonstration (eschatologie, origine de l'âme, naissance du cosmos) ; quand il s'agit de persuader et non seulement de prouver ; quand la mise en image donne prise à l'imagination sans pour autant abdiquer la rigueur. Luc Brisson, dans ''Platon, les mots et les mythes'' (Paris, La Découverte, 1982 ; rééd. 1994), a montré que le mythe platonicien n'est pas un résidu pré-rationnel, mais un instrument philosophique spécifique : il articule une tradition (les récits hérités) et une invention (les récits composés par Platon), et il poursuit une fonction argumentative plus qu'illustrative. Le mythe, chez Platon, a donc plusieurs fonctions. Il introduit un problème sous une forme frappante (comme l'anneau de Gygès ouvre la question de la justice). Il prolonge l'argument en donnant à voir ce qu'il énonçait abstraitement (comme l'allégorie de la caverne prolonge la discussion sur l'éducation). Il répond à une question que la dialectique ne peut pas résoudre (comme les mythes eschatologiques du ''Gorgias'', du ''Phédon'' ou de la ''République'', qui traitent du sort des âmes). Dans tous les cas, il faut le lire comme un texte qui ''pense'', et non comme un ornement. === Le mythe de l'anneau de Gygès : un défi posé à Socrate === Le mythe de l'anneau de Gygès est l'un des plus commentés de l'œuvre platonicienne. Avant d'en proposer une lecture, il est nécessaire d'en situer précisément le contexte, sous peine de confondre la position d'un interlocuteur avec celle de Platon. Le récit apparaît dans la ''République'', livre II (359c-360b). Au livre I, Thrasymaque a défendu avec agressivité la thèse selon laquelle «&nbsp;la justice est l'intérêt du plus fort&nbsp;» (338c), thèse que Socrate n'a pas pleinement réfutée aux yeux de ses auditeurs. Au livre II, Glaucon et Adimante, frères de Platon, reprennent donc l'argument sous une forme plus exigeante. Glaucon annonce lui-même qu'il va exposer la position adverse avec le plus de vigueur possible, non parce qu'il y adhère, mais afin de contraindre Socrate à y répondre vraiment (''République'', II, 358c-d). Son argumentation se déploie en trois temps. Il expose d'abord la thèse «&nbsp;des gens&nbsp;» sur l'origine conventionnelle de la justice. Il cherche ensuite à montrer que même l'homme juste ne le serait que par impuissance. Il demande enfin à Socrate de prouver que la vie juste est préférable à la vie injuste, même pour celui qui pourrait être injuste impunément. L'anneau de Gygès intervient dans le deuxième temps. <blockquote> Les hommes prétendent que, par nature, il est bon de commettre l'injustice et mauvais de la souffrir, mais qu'il y a plus de mal à la souffrir que de bien à la commettre. Aussi, lorsque mutuellement ils la commettent et la subissent, et qu'ils goûtent des deux états, ceux qui ne peuvent point éviter l'un ni choisir l'autre estiment utile de s'entendre pour ne plus commettre ni subir l'injustice. De là prirent naissance les lois et les conventions, et l'on appela ce que prescrivait la loi légitime et juste. Voilà l'origine et l'essence de la justice : elle tient le milieu entre le plus grand bien (commettre impunément l'injustice) et le plus grand mal (la subir quand on est incapable de se venger). Entre ces deux extrêmes, la justice est aimée non comme un bien en soi, mais parce que l'impuissance de commettre l'injustice lui donne du prix. [...] Telle est donc, Socrate, la nature de la justice et telle son origine, selon l'opinion commune. Maintenant, que ceux qui la pratiquent agissent par impuissance de commettre l'injustice, c'est ce que nous sentirons particulièrement bien si nous faisons la supposition suivante. Donnons licence au juste et à l'injuste de faire ce qu'ils veulent ; suivons-les et regardons où, l'un et l'autre, les mène le désir. Nous prendrons le juste en flagrant délit de poursuivre le même but que l'injuste, poussé par le besoin de l'emporter sur les autres : c'est ce que recherche toute nature comme un bien, mais que, par loi et par force, on ramène au respect de l'égalité. La licence dont je parle serait surtout significative s'ils recevaient le pouvoir qu'eut jadis, dit-on, l'ancêtre de Gygès le Lydien. Cet homme était berger au service du roi qui gouvernait alors la Lydie. Un jour, au cours d'un violent orage accompagné d'un séisme, le sol se fendit et il se forma une ouverture béante près de l'endroit où il faisait paître son troupeau. Plein d'étonnement, il y descendit, et, entre autres merveilles que la fable énumère, il vit un cheval d'airain creux, percé de petites portes ; s'étant penché vers l'intérieur, il y aperçut un cadavre de taille plus grande, semblait-il, que celle d'un homme, et qui avait à la main un anneau d'or, dont il s'empara ; puis il partit sans prendre autre chose. Or, à l'assemblée habituelle des bergers qui se tenait chaque mois pour informer le roi de l'état de ses troupeaux, il se rendit portant au doigt cet anneau. Ayant pris place au milieu des autres, il tourna par hasard le chaton de la bague vers l'intérieur de sa main ; aussitôt il devint invisible à ses voisins qui parlèrent de lui comme s'il était parti. [...] S'étant rendu compte de cela, il répéta l'expérience pour voir si l'anneau avait bien ce pouvoir ; le même prodige se reproduisit : en tournant le chaton en dedans il devenait invisible, en dehors visible. Dès qu'il fut sûr de son fait, il fit en sorte d'être au nombre des messagers qui se rendaient auprès du roi. Arrivé au palais, il séduisit la reine, complota avec elle la mort du roi, le tua, et obtint ainsi le pouvoir. Si donc il existait deux anneaux de cette sorte, et que le juste reçût l'un, l'injuste l'autre, aucun, pense-t-on, ne serait de nature assez adamantine pour persévérer dans la justice et pour avoir le courage de ne pas toucher au bien d'autrui [...]. Platon, ''République'', II, 358e-360c, trad. Émile Chambry, Paris, Les Belles Lettres, coll. «&nbsp;Classiques en poche&nbsp;», 2002, p. 89-93 (autre traduction disponible : Georges Leroux, Paris, GF Flammarion, 2002). </blockquote> [[Fichier:Paphos Haus des Theseus - Mosaik Achilles 1.jpg|vignette|Mosaïque d'Achille, Maison de Thésée, Paphos.]] Il faut lire ce texte en gardant à l'esprit sa fonction dans l'économie du dialogue. Plus largement, les travaux de référence sur les mythes platoniciens rappellent que ces récits s'insèrent en général dans une argumentation en cours : ils ne valent jamais comme déclarations doctrinales isolées, mais prennent leur sens dans le mouvement qui les encadre. C'est encore plus net ici. Glaucon ne défend pas, à titre personnel, la thèse qu'il expose : il annonce en toutes lettres qu'il va la défendre avec vigueur uniquement pour que Socrate soit contraint d'y répondre (''République'', II, 358c-d). L'anneau de Gygès est donc une construction d'adversaire, présentée dans le cadre d'un exercice dialectique réglé, et non une affirmation que Platon voudrait soutenir en son nom propre. L'expérience de pensée qu'il supporte (que ferait un juste devenu invisible et impuni ?) est précisément conçue pour rendre l'objection inévitable. Or ce n'est pas dans cette scène que tient le cœur du dialogue, mais dans la réponse que Socrate construit sur l'ensemble de la ''République''. À partir du livre IV, il défend l'idée que la justice n'est pas simple convention extérieure, mais harmonie intérieure de l'âme, entre la raison, le cœur et le désir (''République'', IV, 441d-444a). Aux livres VIII et IX, il tente de montrer que l'homme juste est plus heureux que l'injuste, y compris dans le pire des cas. Le livre X s'achève sur un mythe eschatologique, celui d'Er le Pamphylien, qui affirme que les injustices finissent par se payer, quand bien même elles paraissent impunies dans cette vie. La thèse que Glaucon expose n'est donc pas celle de Platon, mais celle à laquelle Platon veut répondre. Confondre les deux, c'est attribuer à l'auteur la position même qu'il s'emploie à combattre. L'intérêt philosophique du récit est ailleurs : il met en lumière le caractère fragile des motivations qui nous tiennent à la justice dans la vie ordinaire, et invite à chercher, en dehors de toute récompense ou sanction extérieure, une raison plus profonde d'être juste. Les lectures modernes qu'on a voulu rapprocher du passage (Hobbes, Rousseau, Marx) sont intéressantes, mais supposent un changement de cadre théorique qu'il faut signaler comme tel. Hobbes, dans le ''Léviathan'' (1651), analyse l'état de nature comme un état où la pulsion de conservation, en l'absence de pouvoir commun, engendre la défiance mutuelle et la guerre ; la thèse de Glaucon contient effectivement une hypothèse qui peut y faire écho, mais Hobbes ne croit ni à une justice en soi ni à une harmonie intérieure de l'âme au sens platonicien. Rousseau, dans le ''Discours sur l'origine de l'inégalité'' (1755), propose une autre généalogie de la propriété et de la justice, qu'il relie à l'histoire sociale de l'homme et non à une nature injuste originaire. Marx, quant à lui, développe dans ''Sur la question juive'' (1843) puis dans la ''Critique du programme de Gotha'' (1875) une analyse du droit formel comme expression des rapports de production, étrangère à la problématique platonicienne du rapport entre la justice et le bien. Ces rapprochements peuvent nourrir la réflexion comparative, à condition de ne pas laisser entendre que Platon serait déjà hobbien, rousseauiste ou marxiste avant l'heure. == La place du philosophe dans la cité == '''Lecture conseillée''' : ''Apologie de Socrate'' (17a-42a, trad. Luc Brisson, Paris, GF Flammarion, 2005) ; [[Pour lire Platon/Études de quelques passages des dialogues]] (étude de la ''Lettre VII''). Socrate prononce plusieurs discours devant les juges d'Athènes, rapportés par Platon dans l{{'}}''Apologie de Socrate''. Il est accusé d'impiété (introduire de nouvelles divinités) et de corrompre la jeunesse (24b-c). Son activité principale, telle qu'il la décrit, consistait à interroger ses concitoyens sans enseigner de doctrine particulière : il se comparait à un taon posé sur un noble cheval pour le réveiller (30e). Se condamnera-t-il lui-même à l'exil, comme la cité semble l'y inviter ? Il s'y refuse (37c-38a) : il tient cet examen critique pour l'activité la plus précieuse qu'il puisse rendre à la cité. L'''Apologie'' pose ainsi une question qui parcourt toute l'œuvre de Platon : le philosophe, dans la cité, est-il nécessairement malvenu ? A-t-il une place, et si oui, à quel prix ? La réponse de la ''République'' (487a-502c, notamment l'allégorie du «&nbsp;vrai pilote&nbsp;») est sévère. Dans les cités actuelles, le philosophe est perçu comme inutile, parfois comme dangereux, précisément parce que les valeurs qui y dominent ne sont pas les siennes. Mais cette marginalité n'est pas sans remède : c'est pour penser les conditions d'une cité où philosophie et politique ne se contrarient plus que Platon écrit la ''République'' (et plus tard les ''Lois''). La ''Lettre VII'', dont l'authenticité reste discutée mais est aujourd'hui largement acceptée, rapporte l'expérience des voyages de Platon en Sicile auprès de Denys II de Syracuse, et montre à la fois la vocation politique du philosophe et les difficultés concrètes de son action. == Une philosophie politique en réponse au procès : pourquoi obéir aux lois ? == '''Lecture conseillée''' : ''Criton'' ([[s:Criton|sur Wikisource]]), passages cités d'après la trad. Luc Brisson, Paris, GF Flammarion, 1997. Quelques jours avant son exécution, Socrate reçoit dans sa prison la visite d'un vieil ami, Criton. Celui-ci lui propose de s'évader : les conditions matérielles sont réunies, les gardiens se laisseront corrompre, des amis sont prêts à l'accueillir à Thessalie ou ailleurs. Socrate refuse, et engage avec Criton un dialogue qui occupe la seconde moitié du texte. === Le principe fondateur : ne jamais commettre l'injustice === Avant même que les Lois ne prennent la parole, Socrate pose avec Criton un principe qui commande tout le reste du dialogue : il ne faut jamais commettre d'injustice, pas même en réponse à une injustice (''Criton'', 49a-e). Cette thèse éthique est présentée comme irréductible. Répondre au mal par le mal ne produit que du mal, et la valeur d'une vie ne se mesure pas au nombre d'années vécues mais à la rectitude des actions (48b). Socrate demande expressément à Criton s'il accepte ce principe, et note que très peu d'hommes y adhèrent, tandis que ceux qui l'acceptent ne peuvent plus s'entendre avec ceux qui le refusent (49c-d). De là découle la question qui va occuper la suite du dialogue. S'évader ne serait-il pas, précisément, commettre une injustice envers les lois d'Athènes, et cela en réponse à l'injustice qu'Athènes a commise en condamnant Socrate à mort ? Pour répondre, Socrate fait parler les Lois : c'est la prosopopée<ref>Figure de rhétorique par laquelle on fait parler une personne absente, morte, un animal, une chose, ou, comme ici, une abstraction juridique.</ref> fameuse du ''Criton'' (50a-54d). On remarquera que cette prosopopée n'est pas un point de départ, mais une conséquence : elle dépend du principe éthique que Criton vient d'accepter quelques pages plus tôt. Tout le reste consiste à vérifier ce qu'un tel principe implique, dans la situation concrète où Socrate se trouve. === Le contrat implicite entre le citoyen et la cité === La prosopopée articule principalement deux arguments : celui des '''bienfaits reçus''', qui compare la cité à un parent dont on tient la vie et l'éducation, et celui de l'''''accord implicite''''', qui fait du maintien du citoyen dans la cité un consentement tacite à ses lois. Ces deux axes sont précédés d'un rappel institutionnel plus général, que l'on peut lire comme le point de départ du raisonnement. <blockquote> Vois si tu l'entendras de cette autre manière : Au moment de nous enfuir ou de sortir d'ici, quel que soit le mot qu'il te plaira de choisir, si les Lois et la République venaient se présenter devant nous, et nous disaient : «&nbsp;Réponds-moi, Socrate, que vas-tu faire ? L'action que tu entreprends a-t-elle d'autre but que de nous détruire, nous qui sommes les Lois, et avec nous la République tout entière, autant qu'il dépend de toi ? Ou te semble-t-il possible que l'État subsiste et ne soit pas renversé, lorsque les arrêts rendus restent sans force et que de simples particuliers leur enlèvent l'effet et la sanction qu'ils doivent avoir ?&nbsp;» Platon, ''Criton'', 50a-b. </blockquote> Ce préalable institutionnel est énoncé sous forme de question rhétorique et fait appel au bon sens juridique : si chaque particulier pouvait refuser d'exécuter une sentence qu'il juge injuste, aucun arrêt de justice ne tiendrait et l'État s'effondrerait. L'argument n'est pas le cœur de la prosopopée ; il prépare la question décisive, qui est de savoir ce que Socrate doit personnellement à la cité. ==== Premier argument : les bienfaits reçus ==== Les Lois passent à leur premier argument de fond, construit sous la forme d'une analogie filiale : <blockquote> «&nbsp;Eh quoi ! Socrate, diraient les Lois, est-ce là ce dont nous étions convenues avec toi ? Ou plutôt n'étions-nous pas convenues avec toi que les jugements rendus par la République seraient exécutés ?&nbsp;» [...] «&nbsp;Voyons : quel sujet de plainte as-tu contre nous et contre la République pour entreprendre ainsi de nous renverser ? Et d'abord, n'est-ce pas nous qui t'avons donné la vie ? N'est-ce pas nous qui avons présidé à l'union de ton père et de ta mère, ainsi qu'à ta naissance ? [...] Mais, puisque c'est à nous que tu dois ta naissance, ta nourriture et ton éducation, peux-tu nier que tu sois notre enfant, notre esclave même, toi et tes ancêtres ? Et s'il en est ainsi, crois-tu que tu aies contre nous les mêmes droits que nous avons contre toi, et que tout ce que nous pourrions entreprendre contre toi, tu puisses à ton tour l'entreprendre justement contre nous ? [...] Ta sagesse va-t-elle jusqu'à ignorer que la patrie est, aux yeux des dieux et des hommes sensés, quelque chose de plus cher, plus respectable, plus auguste et plus saint qu'une mère, un père et tous les aïeux ? qu'il faut avoir pour la patrie, même irritée, plus de respect, de soumission et d'égard, que pour un père ? qu'il faut l'adoucir par la persuasion ou faire tout ce qu'elle ordonne [...] ?&nbsp;» Platon, ''Criton'', 50c-51c. </blockquote> Les Lois rappellent à Socrate les bienfaits reçus : mariage légitime de ses parents, éducation, protection, partage des biens communs. Cette dette fonde, selon leur propos, une autorité comparable à celle d'un parent, et même supérieure. Il faut prendre au sérieux la dissymétrie qui est ici affirmée : le citoyen ne peut pas répondre à la loi comme un égal répond à un égal. Ce point, lourd de conséquences, sera discuté par toute la tradition philosophique ultérieure. On peut dès maintenant souligner qu'il ne coïncide pas avec ce qu'on appellera plus tard «&nbsp;contrat social&nbsp;» à la manière moderne : il n'est pas question ici d'un acte volontaire par lequel des individus libres instituent l'État, mais d'un rapport de dette envers une institution qui précède le citoyen et le rend possible. ==== Second argument : l'accord implicite ==== Les Lois précisent aussitôt un second argument, qui tempère l'image de la soumission filiale et introduit la notion d'accord implicite. Tout citoyen devenu adulte (éphèbe) peut choisir de s'en aller librement, avec ses biens. <blockquote> «&nbsp;Nous déclarons encore, et c'est un droit que nous reconnaissons à tout Athénien qui veut en user, qu'aussitôt qu'il a été reçu dans la classe des éphèbes, qu'il a vu ce qui se passe dans la République, et qu'il nous a vues aussi, nous qui sommes les Lois, il est libre, si nous ne lui plaisons pas, d'emporter ce qu'il possède et de se retirer où il voudra. Et si quelqu'un d'entre vous veut aller dans une colonie, parce que nous lui déplaisons, nous et la République, si même il veut aller s'établir quelque part à l'étranger, aucune de nous ne s'y oppose et ne le défend : il peut aller partout où il voudra avec tous ses biens. Mais quant à celui de vous qui persiste à demeurer ici, en voyant de quelle manière nous rendons la justice et nous administrons toutes les affaires de la république, nous déclarons dès lors que par le fait il s'est engagé envers nous à faire tout ce que nous lui ordonnerions, et s'il n'obéit pas, nous le déclarons trois fois coupable : d'abord, parce qu'il nous désobéit à nous qui lui avons donné la vie, ensuite parce que c'est nous qui l'avons élevé, enfin parce qu'ayant pris l'engagement d'être soumis, il ne veut ni obéir ni employer la persuasion à notre égard, si nous faisons quelque chose qui ne soit pas bien ; et tandis que nous lui proposons à titre de simple proposition, et non comme un ordre tyrannique, de faire ce que nous lui commandons, lui laissant même le choix d'en appeler à la persuasion ou d'obéir, il ne fait ni l'un ni l'autre.&nbsp;» Platon, ''Criton'', 51c-52a. </blockquote> Deux idées méritent d'être dégagées. D'une part, la cité n'est pas présentée comme une instance qui retient par la force : qui n'est pas satisfait peut partir. Rester, c'est implicitement s'engager à obéir. D'autre part, l'obéissance n'exclut pas la discussion. Les Lois ménagent une alternative expresse, «&nbsp;obéir ou persuader&nbsp;» (''peíthein ē poieîn'', 51b-c). Celui qui juge injuste ce qu'on lui commande a le droit, et même le devoir, de tenter de convaincre. Ce n'est qu'après l'échec de cette persuasion qu'il doit exécuter la sentence. La prosopopée ne défend donc pas une obéissance aveugle : elle définit une obéissance réfléchie, exercée à l'intérieur d'un espace de discussion. === Socrate et la décision d'obéir === Les Lois prolongent leur plaidoyer en mettant en évidence la cohérence propre de Socrate. Il n'a jamais quitté Athènes, sinon pour des campagnes militaires et une visite à l'Isthme de Corinthe ; il y a fondé une famille ; lors du procès, il a préféré la mort à l'exil (''Apologie'', 37c-38a). S'évader maintenant reviendrait à contredire tout ce qu'il a dit et fait jusque-là. <blockquote> «&nbsp;Or, tu n'as préféré le séjour ni de Lacédémone, ni de la Crète, dont tu vantes sans cesse le gouvernement, ni d'aucune ville grecque ou barbare, mais tu es sorti d'Athènes moins souvent que les boiteux, les aveugles et les autres infirmes : preuve évidente que tu avais plus d'amour que les autres Athéniens pour cette ville et pour nous-mêmes qui sommes les Lois de cette ville : car peut-on aimer une cité sans en aimer les lois ?&nbsp;» Platon, ''Criton'', 52b-53a. </blockquote> L'argument se déplace ici du terrain institutionnel à celui de l'identité. Qui Socrate serait-il, s'il fuyait ? Comment continuerait-il à tenir, en exil, les discours qu'il a tenus toute sa vie sur la vertu, sur la justice, sur la loi ? La question n'est pas seulement ce qu'il a le droit de faire, mais ce qu'il peut faire sans cesser d'être lui-même. La fidélité à la cité se confond alors avec une fidélité à soi. On retrouve ici, appliqué à une situation concrète, le principe posé au début du dialogue : s'évader reviendrait à répondre au mal par le mal, donc à trahir précisément la thèse éthique que Socrate a passé sa vie à défendre. <blockquote> «&nbsp;Maintenant, au contraire, si tu meurs, tu meurs victime de l'injustice, non des lois, mais des hommes, au lieu que, si tu sors de la ville, après avoir si honteusement commis l'injustice à ton tour, rendu le mal pour le mal, violé toutes les conventions, tous les engagements que tu as contractés envers nous, maltraité ceux que tu devrais le plus ménager, toi-même, tes amis, ta patrie et nous, alors nous te poursuivrons de notre inimitié pendant ta vie [...].&nbsp;» Platon, ''Criton'', 54b-c. </blockquote> Ce passage distingue soigneusement les lois et les hommes qui les appliquent. La sentence qui condamne Socrate peut être injuste, dans la mesure où les juges se sont trompés ; les lois elles-mêmes ne sont pas en cause. Socrate n'obéit donc pas à une loi injuste au sens où la loi serait intrinsèquement mauvaise ; il obéit à une loi juste appliquée injustement par des hommes. On voit que le ''Criton'' ne prescrit pas une obéissance inconditionnelle à n'importe quelle prescription. La difficulté apparaît, et a été amplement discutée par les commentateurs, lorsqu'on tente d'articuler cette position avec celle de l'''Apologie'' (29d-30a), où Socrate affirme qu'il obéirait au dieu plutôt qu'aux juges s'ils lui ordonnaient de cesser de philosopher. === Limites et portée d'une lecture moderne === On a longtemps tenté de lire ce texte à la lumière des théories modernes du contrat social (Hobbes, Locke, Rousseau), ou dans une perspective critique inspirée de Marx qui y verrait l'alibi d'une domination juridique. Ces rapprochements demandent prudence. Les Lois du ''Criton'' ne se posent pas comme un contrat librement consenti par des individus préexistants à la cité, mais comme une instance qui précède le citoyen et lui donne son existence sociale. Et la critique marxiste du droit, formulée dans ''Sur la question juive'' et dans la ''Critique du programme de Gotha'', repose sur une analyse économique du rapport entre droit formel et rapports de production, tout à fait étrangère à l'horizon de Platon. On peut s'en inspirer pour interroger Platon, non pour le traduire. Une lecture plus proche du texte permet pourtant de dégager une intuition qu'Alain, bien plus tard, a reformulée avec netteté : <blockquote> «&nbsp;Résistance et obéissance, voilà les deux vertus du citoyen. Par l'obéissance, il assure l'ordre ; par la résistance il assure la liberté. Et il est bien clair que l'ordre et la liberté ne sont point séparables, car le jeu des forces, c'est-à-dire la guerre privée, à toute minute, n'enferme aucune liberté ; c'est une vie animale, livrée à tous les hasards. Donc les deux termes, ordre et liberté, sont bien loin d'être opposés ; j'aime mieux dire qu'ils sont corrélatifs. La liberté ne va pas sans l'ordre ; l'ordre ne vaut rien sans la liberté. Obéir en résistant, c'est tout le secret. Ce qui détruit l'obéissance est anarchie ; ce qui détruit la résistance est tyrannie.&nbsp;» Alain, ''Propos d'un Normand'', 4 septembre 1912, éd. M. Savin, Paris, Gallimard, coll. «&nbsp;Bibliothèque de la Pléiade&nbsp;», 1956, t. II, p. 164. </blockquote> La formule d'Alain n'est pas une paraphrase du ''Criton'', mais un écho. Elle aide à saisir ce que Platon a déjà pensé : une loi qui n'admettrait aucune critique tomberait dans la tyrannie, une critique qui refuserait toute obéissance tomberait dans l'anarchie. L'«&nbsp;obéir ou persuader&nbsp;» du ''Criton'' tient précisément cet équilibre. Il ne légitime ni le conformisme silencieux, ni la désobéissance sans discussion préalable. Dans le cas particulier de Socrate, la persuasion a échoué (les juges n'ont pas été convaincus) : reste l'obéissance, et cette obéissance consentie est, paradoxalement, la preuve ultime de la liberté philosophique de Socrate, celle qui le distingue de tout personnage comme Calliclès, pour qui la loi n'est qu'un masque de la faiblesse (''Gorgias'', 482c-486d). == Liste des mythes platoniciens == Voici une liste non exhaustive des principaux mythes et allégories présents dans l'œuvre de Platon, avec leur localisation stéphanienne. Chacun pourra faire l'objet d'une étude séparée. * '''Mythe de l'androgyne''' : discours d'Aristophane (''Banquet'', 189d-193d). * '''Généalogie d'Éros''' : Poros et Pénia, enseignement de Diotime (''Banquet'', 203b-204b). * '''Allégorie de la ligne''' (''République'', VI, 509d-511e) et '''allégorie de la caverne''' (''République'', VII, 514a-517a). * '''Anneau de Gygès''' : défi de Glaucon (''République'', II, 359c-360b). * '''Mythe d'Er le Pamphylien''' : eschatologie, choix des vies (''République'', X, 614b-621d). * '''Mythe de l'attelage ailé''' : tripartition de l'âme, chute et remontée vers les Idées (''Phèdre'', 246a-256e). * '''Mythe de Theuth''' : invention de l'écriture et critique de la mémoire écrite (''Phèdre'', 274c-275b). * '''Mythe de Prométhée''' : discours de Protagoras sur l'origine des arts et de la politique (''Protagoras'', 320c-322d). * '''Mythes eschatologiques''' : jugement des âmes (''Gorgias'', 523a-527a ; ''Phédon'', 107c-114c ; ''République'', X, 614b-621d). * '''Atlantide''' : cité idéale engloutie, récit rapporté par Critias (''Timée'', 20d-26e ; ''Critias'', 108e-121c). * '''Démiurge et fabrication du cosmos''' : cosmogonie (''Timée'', 27d-47e). * '''Origine des noms et des langues''' (''Cratyle'', en partie). == Notes == {{références}} == Bibliographie sommaire == === Éditions et traductions de référence === * Platon, ''Œuvres complètes'', sous la direction de Luc Brisson, Paris, Flammarion, 2008 (un volume unique regroupant l'ensemble des dialogues). * Platon, ''Œuvres complètes'', trad. Léon Robin, Paris, Gallimard, coll. «&nbsp;Bibliothèque de la Pléiade&nbsp;», 2 vol., 1940-1942. * Platon, ''Le Banquet'', trad. Luc Brisson, Paris, GF Flammarion, 1998. * Platon, ''Théétète'', trad. Michel Narcy, Paris, GF Flammarion, 1994. * Platon, ''La République'', trad. Georges Leroux, Paris, GF Flammarion, 2002 ; trad. Émile Chambry, Paris, Les Belles Lettres, 2002. * Platon, ''Apologie de Socrate, Criton'', trad. Luc Brisson, Paris, GF Flammarion, 1997 (rééd. 2005). * Platon, ''Phèdre'', trad. Luc Brisson, Paris, GF Flammarion, 2004. * Platon, ''Gorgias'', trad. Monique Canto-Sperber, Paris, GF Flammarion, 1987 (rééd. 2007). * Platon, ''Phédon'', trad. Monique Dixsaut, Paris, GF Flammarion, 1991. * Platon, ''Protagoras'', trad. Frédérique Ildefonse, Paris, GF Flammarion, 1997. * Platon, ''Lettres'', trad. Luc Brisson, Paris, GF Flammarion, 1987 (rééd. 2004). === Études === * Luc Brisson, ''Platon, les mots et les mythes. Comment et pourquoi Platon nomma le mythe ?'', Paris, La Découverte, 1982 (rééd. 1994). * Monique Dixsaut, ''Le Naturel philosophe. Essai sur les dialogues de Platon'', Paris, Vrin, 2001. * Monique Dixsaut, ''Platon'', Paris, Vrin, coll. «&nbsp;Bibliothèque des philosophies&nbsp;», 2003. * Louis-André Dorion, ''Socrate'', Paris, PUF, coll. «&nbsp;Que sais-je ?&nbsp;», 2004. * Gregory Vlastos, ''Socrate. Ironie et philosophie morale'', trad. fr. Catherine Dalimier, Paris, Aubier, 1994 (éd. originale Cornell University Press, 1991). * Monique Canto-Sperber (éd.), ''Philosophie grecque'', Paris, PUF, 1997. * Julia Annas, ''Introduction à la République de Platon'', trad. fr. Béatrice Han, Paris, PUF, 1994. * Michel Narcy, ''Le Philosophe et son double. Un commentaire de l'Euthydème de Platon'', Paris, Vrin, 1984. * ''Stanford Encyclopedia of Philosophy'', entrée «&nbsp;Plato's Myths&nbsp;» (Catalin Partenie), 2009 (mise à jour 2022), [https://plato.stanford.edu/entries/plato-myths/ en ligne]. dt6uo56vdj4daofs38sf79na19bmr4t Pour lire Platon/Guide des dialogues/Apologie de Socrate 0 83824 764071 763994 2026-04-20T01:00:33Z JackBot 14683 Formatage, [[Spécial:Pages non catégorisées]] 764071 wikitext text/x-wiki == Introduction générale == {{wikisource|Apologie de Socrate (Platon)|Apologie de Socrate}} === L’événement et sa portée === [[Fichier:Socrate du Louvre.jpg|vignette|droite|upright=0.9|Portrait de Socrate, marbre, copie romaine d’époque impériale reproduisant un original grec attribué à Lysippe (IV{{e}} siècle av. J.-C.). Musée du Louvre, Paris (inv. Ma 59).]] En l’an 399 avant notre ère, à Athènes, un vieil homme de soixante-dix ans comparaît devant un tribunal populaire composé de cinq cent un jurés citoyens. Il s’appelle Socrate, fils de Sophronisque ; il est accusé d’impiété et de corruption de la jeunesse. Au terme d’une longue journée d’audience, à une faible majorité d’abord (telle que, selon ses propres mots en 36a, un basculement de trente voix aurait suffi à l’acquittement), puis à une majorité plus large pour la peine capitale, il sera condamné à mort. Quelques semaines plus tard, il boira la ciguë dans sa cellule, devant ses disciples, après le retour du bateau sacré de Délos. Cet événement, déjà traumatisant en son temps, est devenu le mythe fondateur de la philosophie occidentale : le moment où la cité met à mort celui qui prétendait y exercer, par la parole et l’examen, un magistère moral. L’''Apologie de Socrate'' est l’œuvre par laquelle Platon, alors âgé d’une quarantaine d’années, rend compte de ce procès. On situe sa composition probablement entre 390 et 385, soit une dizaine d’années après les faits. Le texte se présente sans cadre fictionnel : nul narrateur, nul personnage introductif, nulle scène préalable comme il en existe dans la plupart des autres dialogues. Nous sommes plongés d’emblée dans la parole de Socrate, au moment où il se lève pour prendre la parole. Contrairement à la plupart des dialogues platoniciens, Platon s’efface presque entièrement : comme le remarque B. Piettre dans son commentaire, dans aucune autre œuvre on n’a l’impression d’entendre avec une telle proximité la parole de Socrate, « comme s’il nous était donné d’assister au procès »<ref name="piettre">Bernard Piettre, ''Platon, Apologie de Socrate'', traduction, présentation et notes de Bernard et Renée Piettre, Paris, Le Livre de Poche (Librairie générale française), coll. « Libretti », 1997, p. 21.</ref>. Cela ne signifie pas que l’''Apologie'' soit une transcription sténographique du plaidoyer : Platon recompose, réorganise, stylise. Mais il s’efforce, probablement plus que dans tout autre dialogue, de restituer la voix, le ton, l’argumentation du maître. Il convient de rappeler que l’''Apologie'' de Platon n’est pas la seule défense posthume de Socrate. Xénophon a également rédigé une ''Apologie'' qui nous est parvenue, brève et d’un ton psychologique différent, ainsi que des ''Mémorables'' qui reprennent certains motifs<ref>Xénophon, ''Apologie de Socrate'' et ''Mémorables'', trad. P. Chambry, Paris, Garnier-Flammarion, 1967.</ref>. D’autres disciples, comme Eschine de Sphettos, ont composé des écrits socratiques aujourd’hui perdus<ref>Voir Gabriele Giannantoni, ''Socratis et Socraticorum Reliquiae'', 4 vol., Naples, Bibliopolis, 1990.</ref>. Un pamphlet hostile, l’''Accusation de Socrate'' du sophiste Polycrate (probablement composé vers 393-392), circulait et aurait donné à Platon l’occasion de répondre indirectement. La concurrence entre ces « socratiques » explique vraisemblablement en partie le soin que Platon met à camper son propre Socrate, qui finira par s’imposer comme la figure canonique dans la postérité. L’enjeu de l’''Apologie'' dépasse de loin la réhabilitation posthume d’un homme. À travers le procès de Socrate, Platon met en accusation la cité qui l’a condamné ; il ouvre l’espace d’une autre politique, où la philosophie, cet « amour du savoir » (''philosophía''), apparaît comme la véritable vocation civique. Condamner le philosophe, c’est pour Athènes se condamner elle-même, préférer l’ignorance au savoir, la facilité au courage, la flatterie à la vérité. L’''Apologie'' constitue ainsi, selon une formule souvent reprise, l’un des textes fondateurs de la philosophie comme discours distinct, à la fois opposé à la sophistique et à la rhétorique, et radicalement engagé dans l’existence concrète. Elle offre en outre une cristallisation exemplaire de ce qu’on appellera le « dialogue socratique », genre dont Platon fera sa forme propre de pensée. === Contexte historique du procès === Pour comprendre la portée du texte, il faut garder à l’esprit la situation d’Athènes en 399. La cité sort épuisée de la longue guerre du Péloponnèse (431-404), qui l’a opposée à Sparte et s’est soldée par la défaite totale d’Athènes. Après la capitulation, Lysandre, le général spartiate, a imposé un gouvernement oligarchique (les Trente) qui a régné par la terreur pendant environ huit mois (404-403) : exécutions sommaires, confiscations, exil des démocrates. La démocratie a été rétablie par un soulèvement armé parti de Phylè en 403, et, dans le cadre des accords de réconciliation conclus sous l’archontat d’Euclide, une amnistie générale a été proclamée pour ne pas ajouter la guerre civile aux malheurs déjà subis : il était désormais interdit, sous peine de sanctions, de se référer aux événements antérieurs à la restauration<ref>Sur cette amnistie, qu’il ne faut pas confondre avec le décret de Patroclide de 405 (qui portait sur la réhabilitation des ''atimoi''), voir Aristote, ''Constitution d’Athènes'', 39-40 ; et Xénophon, ''Helléniques'', II, 4, 38-43.</ref>. Mais les plaies étaient béantes, et le ressentiment courait souterrainement. Or Socrate avait été, dans les années antérieures, un familier de personnages qui incarnaient précisément ces désastres. Alcibiade, son disciple brillant et fantasque, avait entraîné Athènes dans la désastreuse expédition de Sicile (415-413), scandalisé la cité par l’affaire de la mutilation des hermès, puis fini par trahir sa patrie en passant chez Sparte. Critias, autre de ses proches, avait été l’un des chefs, peut-être le principal, du gouvernement des Trente, artisan de la terreur oligarchique. Charmide, oncle de Platon et lui aussi de l’entourage socratique, avait également appartenu à ce régime. Même si Socrate lui-même avait refusé de collaborer avec les Trente (comme il le rappellera dans son plaidoyer à propos de l’arrestation de Léon de Salamine), sa réputation se trouvait entachée. Le procès de 399, bien que portant officiellement sur des griefs religieux, fonctionne donc aussi comme un règlement de comptes politique, que l’amnistie interdisait pourtant de mener ouvertement. Eschine le rhéteur, quelques décennies plus tard, dira sans détour que les Athéniens avaient condamné Socrate « parce qu’il avait été le maître de Critias »<ref>Eschine, ''Contre Timarque'', I, 173.</ref>. L’accusation est portée par trois hommes. Mélétos, poète tragique obscur, plutôt jeune, est le plaignant officiel, celui qui a déposé la plainte auprès de l’archonte-roi, magistrat compétent pour les affaires religieuses. Mais Socrate sait parfaitement que le véritable moteur de la procédure est Anytos, riche tanneur, homme politique influent de la démocratie restaurée, qui avait participé à la chute des Trente. Son fils, raconte Xénophon, fréquentait Socrate et préférait ses entretiens à la tannerie paternelle : hostilité d’autant plus vive<ref>Xénophon, ''Apologie de Socrate'', 29-31.</ref>. Dans le ''Ménon'' de Platon (90b-95a), Anytos apparaît comme le type même du démocrate borné, ennemi viscéral des sophistes et plus largement des intellectuels<ref>Platon, ''Ménon'', 89e-95a.</ref>. Le troisième accusateur, Lycon, est un orateur dont la fonction semble avoir été de soutenir la plaidoirie par une péroraison énergique. La plainte, dont Diogène Laërce nous a conservé le texte<ref>Diogène Laërce, ''Vies et doctrines des philosophes illustres'', II, 40.</ref>, s’énonce approximativement ainsi : <blockquote>Socrate est coupable de ne pas reconnaître les dieux que reconnaît la cité et d’introduire des divinités nouvelles ; il est aussi coupable de corrompre la jeunesse. Peine requise : la mort.</blockquote> Trois griefs, donc : la négation des dieux traditionnels, l’introduction de nouvelles divinités, la corruption de la jeunesse. Ces trois chefs d’accusation sont intimement liés dans la logique de l’accusation : en introduisant de nouvelles divinités et en niant les anciennes, Socrate aurait, par son enseignement, détourné les jeunes gens du respect dû à la religion civique, donc corrompu la cité dans ses fondements. L’accusation d’impiété (''asébeia'') était une accusation politique au sens fort, puisque la religion à Athènes n’était pas une affaire privée mais la substance même du lien civique. === Le cadre procédural === Le procès se tient à l’Héliée, le tribunal populaire, probablement dans un local situé sur l’agora. Les jurés (501 ce jour-là, nombre impair pour éviter les égalités) ont été tirés au sort le matin même parmi les six mille citoyens qui s’étaient inscrits comme héliastes pour l’année. Une journée entière est consacrée à la procédure, dont le temps est rigoureusement partagé entre l’accusation et la défense au moyen de la clepsydre, horloge à eau. Chaque partie parle pour son propre compte : ni procureur ni avocat. Il est permis de faire corroborer son discours par un orateur plus habile (ce dont Mélétos semble avoir bénéficié avec Lycon), ou de lire un discours écrit par un logographe. La tradition rapporte que Lysias, le plus grand logographe de l’époque, aurait composé pour Socrate un plaidoyer de défense ; Socrate en aurait reconnu la beauté avant de le rejeter comme ne lui convenant pas<ref>Diogène Laërce, ''Vies et doctrines des philosophes illustres'', II, 40 ; Cicéron, ''De l’orateur'', I, 231.</ref>. La procédure, pour un procès où la loi n’a pas fixé la peine à l’avance (on parle alors d’''agôn timētós'', « procès à peine à estimer »), comporte deux votes. Après les plaidoiries, un premier vote tranche sur la culpabilité. En cas de condamnation, l’accusateur propose une peine (ici, la mort), l’accusé doit proposer une contre-peine, et un second vote choisit entre les deux sans possibilité de moyen terme. Cette contrainte procédurale pèse lourd sur la suite : les jurés, contraints de choisir entre deux propositions extrêmes, se trouvent ainsi, par la stratégie de Socrate refusant de proposer une peine crédible, poussés à voter la mort. C’est la procédure en deux temps qui explique la structure tripartite de l’''Apologie'' telle que Platon nous l’a transmise : d’abord le grand plaidoyer de défense (17a-35d), puis le discours de contre-proposition pénale après le verdict de culpabilité (35e-38b), enfin une dernière allocution prononcée après la condamnation à mort, adressée tour à tour aux jurés qui ont voté contre lui et à ceux qui l’ont soutenu (38c-42a). Il faut ajouter que les procès athéniens étaient des spectacles autant que des procédures. Le public y assiste ; les plaideurs usent de toute la rhétorique, des larmes de la famille éplorée, des supplications ostentatoires, des témoignages de moralité. Aristophane, dans ''Les Guêpes'' (422), ridiculise la passion que les Athéniens portaient à ces mises en scène judiciaires et le plaisir qu’ils prenaient à voir les accusés se répandre en lamentations. Socrate, comme on le verra, refuse délibérément toute cette théâtralité, ce qui nourrira, après la condamnation, son reproche aux juges : ils l’ont condamné non pas parce qu’il était coupable, mais parce qu’il n’a pas consenti à s’abaisser. === Structure et dispositif du texte === Lu rétrospectivement à travers la grille qu’Aristote constituera dans sa ''Rhétorique'', le plaidoyer de Socrate laisse reconnaître, dans son grand plan, les parties canoniques du discours judiciaire : on y distingue successivement l’exorde (17a-18a), la diabolè ou dénigrement des accusations antérieures (18a-19d), la narration ou ''diḗgēsis'' qui expose les faits (19d-24b), la réfutation proprement dite (24b-28a), l’amplification et la péroraison (28a-35d). Il ne s’agit pas de prétendre que Platon a composé son texte en suivant consciemment un schéma déjà codifié (la ''Rhétorique'' aristotélicienne est postérieure), mais de remarquer que le plaidoyer épouse, dans ses grandes articulations, les usages rhétoriques de son temps. Cette conformité apparente est constamment subvertie par l’ironie socratique : Socrate adopte la forme du discours judiciaire tout en la démentant dans son contenu, en refusant ses procédés et en retournant ses attentes. Le texte fonctionne ainsi comme une parodie philosophique de la rhétorique : il emprunte à l’art oratoire sa structure, mais seulement pour en exhiber la vanité quand il s’agit de la vérité. == Lecture suivie du texte == === Le premier discours : la défense (17a-35d) === ==== L’exorde (17a-18a) : la vérité contre l’éloquence ==== Dès les premiers mots, Socrate pose le ton. La formule d’ouverture, <blockquote>Quel effet, Athéniens, ont produit sur vous mes accusateurs, je l’ignore,</blockquote> est un exorde classique (en grec ''prooímion'') dont la fonction rhétorique, comme le rappelle Aristote dans la ''Rhétorique'' (III, 14), est de capter la bienveillance et l’attention de l’auditoire<ref>Aristote, ''Rhétorique'', III, 14, 1415a22-b30.</ref>. Socrate s’en sert pour établir immédiatement l’antithèse qui structurera tout son plaidoyer : ses accusateurs ont parlé avec persuasion, mais n’ont dit « rien de vrai ou presque » ; lui, en revanche, dira « toute la vérité ». Cette opposition entre l’éloquence ornée des adversaires et la parole nue de Socrate est un retournement ironique savamment préparé. Les accusateurs ont mis les juges en garde contre le « redoutable discoureur » qu’est Socrate. Mais, rétorque-t-il, s’ils entendent par là celui qui dit la vérité, il concède qu’il est un orateur, bien que pas à leur manière. Ses discours ne seront pas, dit-il, « des discours élégamment tournés, comme les leurs, ni même des discours qu’embellissent des expressions et des termes choisis », mais « des choses dites à l’improviste dans les termes qui [lui] viendront à l’esprit ». L’opposition technique est nette : d’un côté le discours apprêté, fondé sur la sélection du vocabulaire (''onómata''), la tournure des phrases (''rhḗmata'') et les arrangements (''kósmos'') ; de l’autre une parole qui se veut « au hasard », sans préparation. Socrate demande aux juges de lui pardonner cette façon de parler : il a soixante-dix ans, comparaît pour la première fois de sa vie devant un tribunal, et il est donc « étranger à la langue en usage ici ». Comme un véritable étranger qui parlerait dans son dialecte, il entend s’exprimer comme il le fait ordinairement, sur l’agora ou devant les comptoirs des changeurs. Le juge, conclut-il, doit juger sur le fond : <blockquote>si mes allégations sont justes ou non. Telle est en effet la vertu du juge, tandis que celle de l’orateur est de dire la vérité. (18a)</blockquote> Cette prise de position initiale est philosophiquement lourde de conséquences. Elle reformule, dès l’ouverture, l’opposition fondamentale entre la rhétorique (l’art de persuader, tel que le pratiquent sophistes et logographes) et la philosophie (recherche de la vérité par l’examen rationnel). Les sophistes, comme Gorgias ou Protagoras, avaient fait de la rhétorique un instrument neutre, capable de « faire de l’argument le plus faible l’argument le plus fort » (formule qui reviendra explicitement contre Socrate au chef d’accusation). Socrate, à l’inverse, affirme que la parole a pour fonction première de manifester le vrai, et que celui qui se défend en justice ne doit pas jouer des passions ou des artifices, mais soumettre les arguments à l’examen rationnel des jurés. On notera pourtant que cette protestation de simplicité est elle-même une figure rhétorique sophistiquée : celui qui clame qu’il ne sait pas parler parle déjà, et fort bien. Il s’agit de la ''dissimulatio artis'' que Cicéron théorisera plus tard : l’art supérieur consiste à cacher l’art. L’ironie socratique commence dès l’exorde. Socrate se présente comme un ''idiṓtēs'' (un simple particulier, un « idiot » au sens grec), étranger aux codes du tribunal, mais cette posture est elle-même un dispositif stratégique qui retourne contre l’accusation la suspicion de sophistique : ce n’est pas Socrate qui manipule les mots, ce sont les accusateurs. L’inversion est complète. ==== Les « anciens accusateurs » : la calomnie de longue durée (18a-19d) ==== Socrate introduit ensuite une distinction cruciale : il doit répondre à deux catégories d’accusations, celles de ses « accusateurs récents » (Mélétos, Anytos, Lycon), mais aussi, et d’abord, celles de ses « anciens accusateurs », qui ont « depuis de nombreuses années » répandu une image fausse de lui. Ceux-là, il les redoute davantage que les nouveaux, pour trois raisons convergentes. Ils sont d’abord nombreux : il ne s’agit pas de trois personnes identifiables, mais d’une rumeur collective. Leurs accusations sont ensuite anciennes : elles ont eu le temps de s’enraciner dans les esprits. Enfin, ils ont agi auprès des juges « dès l’enfance », à un âge où « vous aviez le moins de défiance », de sorte que les jurés en ont reçu l’empreinte avant même d’avoir l’âge de l’examiner. Quelle est la teneur de cette calomnie ancienne ? Socrate la résume en une formule qui reviendra plusieurs fois dans le texte comme un chef d’accusation fantasmatique : <blockquote>Il existe un certain Socrate, un savant, un « penseur » qui s’intéresse aux choses qui se trouvent en l’air, qui mène des recherches sur tout ce qui se trouve sous la terre et qui de l’argument le plus faible fait l’argument le plus fort. (18b)</blockquote> Cette caricature hétéroclite condense en réalité trois traits initialement distincts. Il y a d’abord la figure du physicien à la manière des présocratiques (Anaxagore, Diogène d’Apollonie), qui spéculait sur les phénomènes célestes (''tà metéōra'') et souterrains. Il y a ensuite celle de l’athée, puisque interroger la nature par la raison revenait, dans la perception populaire, à nier les dieux traditionnels (Anaxagore avait été poursuivi pour cette raison vers 433, Protagoras également, et tous deux avaient été contraints à l’exil). Il y a enfin celle du sophiste, expert en retournements dialectiques, capable de faire triompher n’importe quelle cause par le seul art des mots. L’incohérence de ce portrait (un philosophe de la nature qui serait en même temps un manipulateur rhétorique) ne l’empêche pas d’être efficace dans l’opinion. C’est le propre de la rumeur (''diabolḗ'') : elle n’a pas à être cohérente pour être puissante. Socrate le reconnaît avec lucidité : la force de cette calomnie vient précisément de ce qu’elle ne repose sur aucune source identifiable, qu’on ne peut donc ni l’interroger, ni la réfuter. Combattre ces accusateurs anonymes, dit Socrate, « c’est comme se battre contre des ombres » (18d). C’est une difficulté propre au philosophe dans la cité : face à l’opinion établie, il n’a pas d’adversaire identifiable, donc pas de prise dialectique. Socrate désigne cependant une source probable : les comédies, et en particulier ''Les Nuées'' d’Aristophane, représentées en 423 (vingt-quatre ans avant le procès)<ref>Aristophane, ''Les Nuées'', représentées pour la première fois aux Grandes Dionysies de 423 av. J.-C.</ref>. Il la mentionne d’abord anonymement, parlant d’un <blockquote>Socrate qui se balançait, en prétendant qu’il se déplaçait dans les airs et en débitant plein d’autres bêtises concernant des sujets sur lesquels je ne suis un expert ni peu ni prou. (19c)</blockquote> Le nom d’Aristophane sera explicitement prononcé peu après. Dans cette pièce, le poète comique met en scène un Socrate juché dans une corbeille suspendue, pour mieux « mêler sa pensée subtile à l’air », invoquant les Nuées comme divinités substitutives à celles de la religion populaire, enseignant à un paysan, Strepsiade, puis à son fils Phidippide, comment faire triompher le « Raisonnement injuste » et ruiner par cet apprentissage la piété filiale. La pièce s’achève d’ailleurs sur l’incendie du « Pensoir » socratique. L’enjeu pour Socrate n’est pas seulement de corriger une image fausse : c’est de montrer que les accusations de Mélétos se rabattent exactement sur cette caricature (négation des dieux, introduction de nouveautés religieuses, corruption de la jeunesse), de sorte que contre-attaquer la comédie, c’est déjà dissoudre la plainte. Il y a là un geste tranché : Socrate refuse explicitement d’être confondu, d’une part avec les physiciens qui spéculent sur la nature, d’autre part avec les sophistes qui enseignent la rhétorique contre rémunération. Il énumère d’ailleurs plusieurs sophistes célèbres (Gorgias de Léontinoi, Prodicos de Céos, Hippias d’Élis) et rappelle l’anecdote d’Événos de Paros, qui faisait payer cinq mines pour son enseignement (20b) : somme considérable, correspondant à environ un an et demi de salaire d’un ouvrier qualifié<ref name="brisson">Platon, ''Apologie de Socrate. Criton'', trad. et notes de Luc Brisson, Paris, GF-Flammarion, 2016, p. 131, note 54.</ref>. Socrate, lui, n’a rien à vendre, et c’est précisément cela, comme on le verra, qui témoigne de la pureté de sa démarche. Notons enfin une dimension cruciale de ce moment : Socrate récuse par avance toute identification à la figure du philosophe-penseur retiré du monde, que Platon décrit dans le ''Théétète'' (174a) par l’anecdote célèbre de Thalès tombant dans le puits en contemplant les étoiles<ref>Platon, ''Théétète'', 174a.</ref>. La défense de Socrate sera celle d’un homme de l’agora, immergé dans la cité, engagé dans l’examen de ses concitoyens. Le philosophe socratique n’est pas un contemplatif éloigné des affaires humaines : c’est au contraire le plus urbain des hommes, celui qui ne quitte jamais la ville (comme il le dit dans le ''Phèdre''), celui dont l’activité est fondamentalement politique, même s’il n’exerce aucune charge politique. ==== La narration : l’oracle de Delphes et la naissance de la mission (19d-24b) ==== Pour expliquer l’origine de la calomnie, Socrate engage ce qui correspond à la narration (''diḗgēsis'') du discours rhétorique. Il entreprend de raconter comment il est devenu ce personnage que certains considèrent comme un savant. Cette narration, qui occupe une part importante du discours, contient deux éléments structurants : le récit de l’oracle de Delphes et l’exposé de l’enquête qui s’ensuivit. ===== Le savoir humain et le savoir plus qu’humain (20d-21a) ===== Avant de raconter l’oracle, Socrate pose une distinction fondamentale qui constitue peut-être la pièce conceptuelle centrale de tout le plaidoyer. On dit qu’il est « savant » (''sophós'') : soit. Mais savant en quoi ? Il existe, dit-il, un savoir qui excède la mesure humaine, celui auquel prétendent, moyennant rétribution, les sophistes. Ce savoir-là, Socrate ne le possède pas ; et il aurait « des chances d’être un savant » seulement dans un sens plus modeste, celui d’un savoir qui « se rapporte à l’être humain », une sagesse humaine (''anthrōpínē sophía''). La distinction est capitale. Elle sépare deux ordres de connaissance : celui qui porte sur les choses divines (la nature, le cosmos, les causes premières), que Socrate refuse de revendiquer ; et celui qui porte sur l’humain, sur ce qu’il convient de faire pour vivre bien. Cette distinction préfigure le partage que toute l’histoire de la philosophie reprendra entre philosophie théorique et philosophie pratique, et elle annonce également la réorientation socratique qu’évoque Cicéron dans un passage célèbre : <blockquote>Socrate a fait descendre la philosophie du ciel sur la terre, l’a introduite dans les villes et même dans les maisons, et l’a obligée à s’enquérir de la vie, des mœurs, des choses bonnes et mauvaises. (''Tusculanes'', V, 10-11)<ref>Cicéron, ''Tusculanes'', V, 4, 10-11 : « Socrates autem primus philosophiam devocavit e caelo et in urbibus conlocavit et in domus etiam introduxit... »</ref></blockquote> La philosophie cesse d’être cosmologie pour devenir éthique. Pour prouver l’existence et la nature de ce savoir proprement humain, Socrate invoque un témoin insolite mais sans appel : « le dieu de Delphes », c’est-à-dire Apollon pythien. Le recours à la parole oraculaire, dans un tribunal populaire attaché à la religion civique, est rhétoriquement habile : il retourne l’accusation d’impiété en présentant Socrate comme un serviteur du dieu. ===== L’oracle et l’enquête (21a-23c) ===== [[Fichier:John Collier - Priestess of Delphi.jpg|vignette|gauche|upright=0.85|John Collier, ''La Prêtresse de Delphes'', 1891, huile sur toile, Art Gallery of South Australia. La Pythie est figurée assise sur son trépied, dans les vapeurs qui montent de la faille, laurier en main. C’est à cette prêtresse que Chéréphon aurait posé sa question, selon le récit que fait Socrate en ''Apologie'' 20e-21a.]] C’est le moment où surgit, dans le texte, le récit de l’oracle. Chéréphon, ami d’enfance de Socrate (un homme à la passion impétueuse, démocrate, exilé sous les Trente et revenu avec la démocratie, que la comédie moquait pour sa maigreur ascétique et sa « mine d’endive »<ref>Aristophane, ''Les Guêpes'', v. 1408 ; ''Les Oiseaux'', v. 1296 ; ''Les Nuées'', v. 104.</ref>), était allé un jour à Delphes et avait eu l’audace de demander à la Pythie s’il existait quelqu’un de plus sage que Socrate. La Pythie, prêtresse d’Apollon, parle au nom du dieu et rend des oracles aux consultants : elle répondit que « personne n’était plus sage ». Chéréphon est mort entre-temps (probablement vers 403), mais son frère, présent à l’audience, pourra en témoigner. Cette réponse divine met Socrate dans un profond embarras. Il a « conscience de n’être savant ni peu ni prou ». Mais le dieu, par définition, ne peut mentir : « la loi divine l’interdit » (21b). Comment résoudre l’énigme (''aínigma'') ? Socrate décide alors d’entreprendre une vérification. Il va chercher quelqu’un de plus sage que lui, afin de pouvoir revenir à Delphes et dire : <blockquote>ce dieu m’avait désigné comme le plus sage, mais voici qui l’est davantage.</blockquote> Cette démarche, loin d’être un geste d’orgueil, est présentée comme un service rendu au dieu : c’est pour vérifier l’oracle, non pour le contredire, que Socrate entreprend son enquête. Il accorde à l’oracle une autorité suffisante pour l’interroger méthodiquement, selon un principe qui rappelle la maxime exégétique attribuée à Héraclite : « le maître dont l’oracle est à Delphes ne dit ni ne cache rien : il fait signe »<ref>Héraclite d’Éphèse, fragment DK 22 B 93 (Diels-Kranz) = fragment 14 Conche. Voir Marcel Conche, ''Héraclite. Fragments'', Paris, PUF, coll. « Épiméthée », 1986, p. 168-170.</ref>. L’enquête est méthodiquement menée dans trois directions, que Socrate parcourt successivement. Il va d’abord trouver un homme politique (dont il tait le nom, conformément à la pudeur judiciaire) qui passait pour sage. Après l’avoir interrogé, il constate que cet homme se croit sage mais ne l’est pas. Socrate tire alors la leçon capitale qui donnera son contenu à la sagesse humaine : <blockquote>Il y a des chances que je sois moi-même plus sage que cet homme. Car aucun de nous, il est vraisemblable, ne sait rien qui en vaille la peine ; mais lui pense savoir alors qu’il ne sait pas, tandis que moi, tout comme je ne sais pas, je ne pense pas non plus savoir. (21d)</blockquote> Tel est le célèbre savoir du non-savoir socratique : non pas une ignorance totale, mais la conscience lucide et réfléchie de sa propre ignorance, qui vaut davantage que l’illusion de la science. Socrate répète l’opération avec d’autres hommes politiques, et chaque fois la déception est la même, mais pire : plus l’homme est réputé, plus son ignorance est grande ; plus il est humble, plus il est proche du vrai. Socrate passe ensuite aux poètes : auteurs de tragédies, poètes dithyrambiques et autres. Il leur demande ce que signifient leurs propres œuvres, espérant d’eux un savoir, puisqu’ils produisent de la beauté. Il découvre alors qu’ils composent « non par savoir, mais par une sorte de disposition naturelle et par inspiration, comme les devins et les oracles » (22c). Les poètes disent beaucoup de belles choses sans savoir ce qu’elles veulent dire ; et, comme les hommes politiques, ils se croient, à cause de leur art, savants dans d’autres domaines où ils ne le sont pas. L’inspiration poétique est ainsi reconnue comme réelle, mais dissociée du savoir : le poète est possédé par une muse, non par une compétence. Cette analyse, qui reviendra dans le ''Ion'', est une pièce importante de la pensée platonicienne sur l’art<ref>Platon, ''Ion'', 533d-535a.</ref>. Socrate examine enfin les artisans (''cheirotéchnai''). Ici, la situation est plus nuancée. Les artisans possèdent une compétence réelle, une ''tékhnē'', que Socrate ne songe pas à leur dénier. Mais cette compétence les induit à se croire savants « dans les choses les plus importantes », c’est-à-dire dans les questions morales et politiques, alors qu’ils ne le sont pas. L’artisan, parce qu’il sait faire une paire de chaussures, se croit en droit d’avoir un avis éclairé sur la justice. C’est là un point crucial : Socrate reconnaît la légitimité de la ''tékhnē'' dans son domaine propre, mais refuse l’extrapolation de la compétence technique à la sagesse pratique. La conclusion de l’enquête est double. D’une part, Socrate conclut à la véracité de l’oracle : sa sagesse consiste en ce qu’il ne croit pas savoir ce qu’il ne sait pas. D’autre part, il comprend que l’oracle ne le désignait pas ''lui'' spécifiquement comme sage, mais se servait de son nom à titre d’exemple : <blockquote>il y a des chances, Messieurs, pour qu’en réalité le sage, ce soit le dieu, et que dans ce fameux oracle il veuille dire que la sagesse humaine a bien peu de valeur, et même aucune ; et il est clair qu’en désignant Socrate il s’est servi de mon nom pour me prendre en exemple, comme s’il disait : « Le plus sage d’entre vous, hommes, est celui qui, comme Socrate, a reconnu qu’en réalité sa sagesse ne vaut rien. » (23a-b)</blockquote> Socrate devient ainsi, non pas un savant, mais un exemple (''parádeigma'') par lequel le dieu invite tous les hommes à reconnaître la misère de leur prétention au savoir. La sagesse n’est pas une propriété de l’individu, mais une relation à l’ignorance ; elle est, on pourrait dire, éminemment ''maïeutique'' : elle fait accoucher les autres de la conscience de leur propre ignorance. ===== La mission et la naissance de la haine (23b-24b) ===== De cette interprétation de l’oracle naît la mission socratique. Socrate continue, « en service pour le dieu » (''hypēresía toû theoû''), à enquêter sur quiconque prétend être sage, pour manifester chaque fois qu’il ne l’est pas. Cette activité l’a réduit à « une grande pauvreté », car elle lui a occupé toute sa vie au détriment de ses affaires. Mais elle a aussi suscité contre lui des haines innombrables, chaque fois qu’il a démasqué l’ignorance d’un puissant ou d’un réputé. On touche ici à un mécanisme psychologique qu’il faut bien mesurer : nul n’aime être convaincu d’ignorance, surtout publiquement ; celui qui le fait, même par amour du vrai, se constitue des ennemis à proportion de sa rigueur. Les jeunes gens de bonne famille, ceux qui disposent de loisir (''scholḗ''), prennent plaisir à le voir faire ; ils tentent à leur tour d’imiter sa démarche, et ainsi « se font eux-mêmes haïr par ceux qu’ils examinent » (23c), qui ne s’en prennent pas à ces jeunes, mais à Socrate. Ce dernier devient ainsi le responsable imaginaire d’une activité critique qui déborde largement sa personne. C’est le ressort profond de l’accusation de « corruption de la jeunesse » : ce n’est pas que Socrate ait enseigné le mal, c’est que ses méthodes, imitées par ses disciples, font éclater un scandale qu’on veut lui imputer. C’est ainsi que s’est formée la réputation selon laquelle Socrate serait un « corrupteur de la jeunesse », et qu’on a repris contre lui la vieille caricature : un homme qui fait des recherches sur le ciel et la terre et qui fait triompher la mauvaise cause. Socrate tire la conclusion rhétorique : « c’est en disant la vérité que je me fais des ennemis », ce qui est, dit-il, la preuve qu’il dit vrai. Les causes de l’accusation sont là : non dans une faute réelle, mais dans le ressentiment de ceux qu’il a démasqués. Cette section du plaidoyer est philosophiquement fondamentale. Elle met en place plusieurs concepts clefs du socratisme tel que Platon l’entend. D’abord, la philosophie comme examen (''exétasis'') et non comme doctrine : on ne peut enseigner la philosophie de Socrate parce qu’elle n’est pas un corps de propositions à transmettre, mais une pratique à exercer. Ensuite, le savoir de l’ignorance comme seule forme accessible du savoir humain. Enfin, la philosophie comme mission divine, ce qui lui confère une légitimité supérieure à celle des institutions politiques. Cette dimension religieuse de la philosophie est essentielle à l’économie du texte : si la mission est divine, elle ne peut être interrompue par un décret humain, fût-il celui d’un tribunal souverain. On notera également que cette démarche, en démasquant l’ignorance des prétendus savants, introduit une différence entre savoir et non-savoir sur des sujets où la démocratie athénienne supposait, pour délibérer, qu’il n’y en avait pas. Comme l’explique un passage du ''Protagoras'' (319c-d), rédigé par Platon peu après l’''Apologie'', les assemblées démocratiques distinguent les sujets techniques (sur lesquels seuls les compétents s’expriment) et les sujets politiques (sur lesquels tous les citoyens, cordonniers, potiers, tanneurs ou menuisiers, peuvent donner leur avis)<ref>Platon, ''Protagoras'', 319b-d.</ref>. La délibération collective suppose que sur les sujets politiques, il n’existe pas de différence de compétence entre les citoyens. Or l’enquête socratique a précisément pour effet de réintroduire cette différence sur les objets où l’institution démocratique la refoulait. Se prétendre ignorant soi-même et révéler l’ignorance d’autrui sur les questions de justice ou de vertu, c’est mettre en cause le principe majoritaire là où il s’applique. On comprend en quoi, malgré les apparences, la posture socratique est subversive pour la démocratie athénienne : elle frappe à sa racine épistémologique. ==== La réfutation de Mélétos (24b-28a) ==== Socrate en vient maintenant aux accusations officielles. Il relit la plainte, <blockquote>Socrate est coupable de corrompre la jeunesse et de reconnaître non pas les dieux que la cité reconnaît, mais, au lieu de ceux-là, des divinités nouvelles,</blockquote> et entreprend de l’examiner point par point. Il utilise alors la prérogative que la loi athénienne accorde à l’accusé : interroger directement son accusateur, qui est tenu de répondre. Ce qui suit est l’un des grands morceaux dialectiques de l’''Apologie''. Socrate, en un véritable elenchos (cette réfutation par interrogation qui est sa marque de fabrique), démonte successivement les trois volets de l’accusation. On notera d’emblée un jeu de mots grec qui court tout l’interrogatoire : le nom de Mélétos (''Mélētos'') évoque le verbe ''mélei'' (« se soucier de »). Socrate va reprocher à Mélétos, précisément, de ne pas se soucier (mélein) des choses dont il prétend se soucier. L’ironie est ciselée jusque dans l’onomastique. La méthode que Socrate utilise ici est l’''elenchos'' : il part des thèses de l’interlocuteur, l’amène par des questions à en reconnaître les conséquences, puis lui fait constater la contradiction entre ces conséquences et d’autres thèses qu’il tient également pour vraies. Cette procédure, qui ne démontre rien positivement mais montre qu’une thèse est intenable, est le moteur de la dialectique socratique dans les dialogues de jeunesse de Platon<ref>Sur la méthode de l'elenchus, voir Gregory Vlastos, « The Socratic Elenchus », ''Oxford Studies in Ancient Philosophy'', I, 1983, p. 27-58.</ref>. ===== Qui rend les jeunes meilleurs ? (24b-25c) ===== Premier volet : la corruption de la jeunesse. Socrate commence par un piège dialectique. Si Mélétos accuse quelqu’un de corrompre les jeunes, c’est qu’il a à l’esprit ce qui les rend meilleurs. Qu’il le dise donc. Mélétos, pris au dépourvu, balbutie : « Les lois ». Mais ce n’est pas une personne, objecte Socrate. Il insiste : quel ''homme'' rend les jeunes meilleurs ? Mélétos répond alors, au gré d’une improvisation visiblement embarrassée : les juges, puis les membres du Conseil, puis ceux de l’Assemblée, bref tous les Athéniens ; tous, sauf Socrate. Socrate retourne alors l’argument par une analogie mémorable, celle des chevaux. Suppose-t-on que tous les hommes rendent les chevaux meilleurs, et qu’un seul les corromprait ? Non, c’est évidemment le contraire : pour les chevaux comme pour tous les animaux, seuls quelques spécialistes savent les rendre meilleurs, et la plupart des gens, s’ils s’en occupent, les nuisent. Il en va de même pour les jeunes gens. L’éducation est un art, et l’art suppose la compétence, qui n’est pas le partage du plus grand nombre. En prétendant que tous éduquent bien sauf Socrate, Mélétos révèle qu’il n’a jamais sérieusement réfléchi à ce dont il parle : il s’est désintéressé de la question. Ce qui est précisément ce que le jeu de mots sur son nom entendait suggérer. Ce premier argument est intéressant par ce qu’il laisse apercevoir de la position de Socrate à l’égard de la démocratie. L’éducation, comme le souligne le commentaire de C. Chrétien, est « une affaire politique tant la formation de l’homme paraît indissociable de celle du citoyen »<ref name="chretien">Claude Chrétien, ''Platon, Apologie de Socrate'', Paris, Hatier, coll. « Profil philosophie », 1993, p. 44.</ref>. Or, pour Mélétos et pour les Athéniens en général, la cité est à elle-même sa propre pédagogie : chaque citoyen forme, par son exemple et sa participation à la vie publique, les futurs citoyens. Socrate, à l’inverse, soutient que l’éducation, comme les autres arts, relève d’une compétence particulière, ce qui va à l’encontre du présupposé démocratique d’une compétence civique également partagée. En paraissant ne se battre que sur un détail logique, Socrate met en cause, à travers cet argument, l’un des présupposés majeurs de la démocratie athénienne : celui selon lequel chaque citoyen serait naturellement compétent pour la délibération politique. ===== Corrompre volontairement ? (25c-26a) ===== Deuxième pièce du dispositif : Socrate demande à Mélétos s’il le pense corrompre les jeunes volontairement ou involontairement. Mélétos, rageusement, répond : volontairement. Mais Socrate montre alors que cette réponse est intenable. Car si les méchants font du mal à leur entourage, et les bons du bien, alors corrompre volontairement les gens qui nous entourent (avec lesquels on vit) c’est s’exposer soi-même à en pâtir. Personne, pas même le plus ignorant, ne choisit délibérément de se nuire à soi-même. Donc, ou bien Socrate ne corrompt pas, ou bien, s’il corrompt, c’est involontairement. Et dans ce cas, la loi ne prévoit pas un procès mais une remontrance privée : si on m’avait averti, j’aurais cessé. En me traînant devant le tribunal, Mélétos prouve que son but n’est pas de me corriger mais de me punir ; ce qui est contradictoire avec l’idée d’une faute involontaire. Cet argument repose sur l’un des principes les plus fermes du socratisme, le célèbre paradoxe socratique selon lequel nul ne fait le mal volontairement (''oudeís hekṓn hamartánei''). Socrate y croit sincèrement : si l’on savait vraiment ce qu’est le bien, on ne pourrait pas ne pas le vouloir. Le vice n’est pas une perversion de la volonté, c’est une forme d’ignorance. Cette thèse, qui paraîtra contre-intuitive à toute la tradition postérieure (notamment chrétienne, qui mettra l’accent sur la malice du mal), sera développée dans plusieurs dialogues platoniciens, notamment le ''Gorgias'' et le ''Protagoras''<ref>Platon, ''Gorgias'', 466a-468e, 509c-e ; ''Protagoras'', 352b-358d.</ref>. Aristote, dans l’''Éthique à Nicomaque'', la critiquera comme négligeant la réalité de la ''akrasía'' (faiblesse de la volonté)<ref>Aristote, ''Éthique à Nicomaque'', VII, 2-3, 1145b21-1147b19.</ref>. Mais l’argument sert ici surtout à piéger Mélétos dans une contradiction : soit tu m’accuses d’une faute involontaire, et le procès est illégitime (car la procédure judiciaire vise des fautes intentionnelles) ; soit tu m’accuses d’une faute volontaire, mais celle-ci est psychologiquement impossible (personne ne choisit de se nuire à soi-même). Dans les deux cas, la plainte s’effondre. Remarquons la précision juridique : Socrate joue habilement sur la distinction athénienne entre fautes volontaires (justiciables) et involontaires (pour lesquelles la remontrance privée, la ''nouthesía'', était la procédure appropriée). ===== L’athéisme et les divinités démoniques (26a-28a) ===== Troisième volet : la question religieuse, qui est le cœur même de l’accusation. Socrate demande à Mélétos de préciser son propos : l’accuse-t-il de reconnaître des dieux différents de ceux de la cité, ou de ne reconnaître aucun dieu du tout ? La distinction est cruciale, car la plainte elle-même est ambiguë (elle évoque des « divinités nouvelles », ce qui présuppose que Socrate croit à des divinités, mais lui reproche aussi de ne pas reconnaître celles de la cité). Mélétos, avec une maladresse que Socrate exploite pleinement, s’emporte et répond : « aucun dieu du tout ». Il ajoute même, piégé par sa propre fureur, que Socrate prétend, à la manière d’Anaxagore, que « le soleil est une pierre et la lune une terre ». Socrate saisit l’occasion avec une précision chirurgicale. D’abord, il ridiculise la confusion : Anaxagore, en effet, a soutenu cette thèse physicienne, mais les livres d’Anaxagore, qui se trouvent au marché (à l’orchestre, endroit de l’agora où l’on vendait les livres), coûtent « tout au plus une drachme » ; pourquoi donc accuser Socrate d’avoir inventé ce qu’on peut lire partout et qui n’est pas de lui ? Le trait est doublement dévastateur : il innocente Socrate et il prouve l’incompétence de Mélétos, qui ne distingue pas Socrate d’Anaxagore. Ensuite, il tend son piège principal. La plainte officielle dit que Socrate introduit de nouvelles divinités (''daimónia''). Or Mélétos vient d’affirmer que Socrate n’admet aucun dieu du tout. Ces deux affirmations sont contradictoires : on ne peut pas à la fois reconnaître des divinités et ne reconnaître aucun dieu. Mélétos se contredit lui-même, et sous serment, car la plainte avait été déposée sous serment réciproque (''antōmosía''). Socrate poursuit par un syllogisme subtil. Peut-on reconnaître des « phénomènes démoniques » (''daimónia prágmata'') sans reconnaître l’existence de démons ? De même que l’on ne peut reconnaître des phénomènes hippiques sans reconnaître les chevaux, ni des phénomènes musicaux sans reconnaître les musiciens, on ne peut reconnaître des phénomènes démoniques sans reconnaître les démons. Or les démons, selon la religion grecque traditionnelle, sont soit des dieux soit des enfants de dieux. Donc, si Socrate reconnaît des démons, il reconnaît aussi des dieux, ou à tout le moins des êtres divins. La plainte se contredit elle-même : Mélétos affirme à la fois que Socrate ne reconnaît aucun dieu et qu’il reconnaît des démons, donc des dieux. Ce raisonnement est brillant sur le plan dialectique, mais il a suscité la perplexité des commentateurs. Il repose sur une définition traditionnelle des démons comme « enfants des dieux », qui n’est pas toujours stabilisée dans la culture grecque (chez Hésiode, les démons sont plutôt des hommes de l’âge d’or devenus esprits), et laisse entière la question de savoir ce que sont les « nouvelles divinités » dont Socrate était réellement accusé. La plupart des commentateurs modernes estiment que l’accusation visait précisément le fameux ''daimónion'' socratique, la voix intérieure divine dont Socrate parlera plus loin, qui serait apparue aux Athéniens comme une divinité privée, nouvelle, donc impie car non reconnue par la cité. Socrate, en déplaçant le débat sur la question abstraite de l’existence ou non des démons, élude habilement cette difficulté. C’est un procédé dialectique, non un argument de fond. Il faut aussi percevoir le geste de fond. Comme le note Claude Chrétien, Socrate, par ce raisonnement, rattache sa croyance en des « phénomènes démoniques » à une croyance minimale mais ferme en la divinité, sur un mode qui relève d’une théologie négative : il ne dit rien de positif sur les dieux, mais affirme seulement que quelque chose, dans l’expérience humaine, manifeste leur existence<ref name="chretien-30">Claude Chrétien, ''Platon, Apologie de Socrate'', ''op. cit.'', p. 30-31.</ref>. Cela est cohérent avec son agnosticisme sur les mythes (dans le ''Phèdre'', Socrate refuse de spéculer sur les aventures de Borée enlevant Orithye<ref>Platon, ''Phèdre'', 229b-230a.</ref>) et avec sa dévotion pratique à la religion de la cité (on le voit obéir aux rites dans plusieurs dialogues). Socrate est pieux en acte, agnostique en théorie : il reconnaît une transcendance divine, sans prétendre en décrire la nature. Cette position, fine et paradoxale, est à l’origine d’une tension féconde dans toute la théologie philosophique ultérieure. À la fin de cette section, Socrate conclut qu’il a suffisamment montré que l’accusation de Mélétos est sans consistance. Mais, ajoute-t-il avec lucidité, il sait bien que ce n’est pas elle qui le fera condamner : c’est la vieille calomnie, la haine accumulée au fil des années. D’autres hommes justes, avant lui, ont subi le même sort, et beaucoup en subiront après. ==== La mission divine et le modèle héroïque (28a-30c) ==== Ayant réfuté l’accusation formelle, Socrate entreprend alors ce que Piettre appelle une « amplification »<ref>Piettre, ''op. cit.'', p. 45-46 (sur le découpage du plaidoyer en réfutation et amplification).</ref> : il justifie tout son mode de vie. L’''Apologie'' bascule ici d’un plaidoyer juridique vers une proclamation philosophique. La question n’est plus « suis-je coupable de ces griefs précis ? » mais : « comment justifier un mode d’existence qui expose à la mort ? » C’est à partir de ce moment que l’''Apologie'' cesse d’être un simple plaidoyer pour devenir un manifeste. ===== Le modèle d’Achille (28b-d) ===== Socrate anticipe une objection qu’un auditoire athénien pouvait sincèrement formuler : n’as-tu pas honte, Socrate, d’avoir mené une existence qui t’expose à mourir ? Il y répond par l’évocation des héros homériques, et notamment d’Achille, la figure de référence de la vertu guerrière grecque. Quand Thétis, sa mère, lui annonça qu’il mourrait s’il vengeait Patrocle en tuant Hector, Achille répondit, selon l’''Iliade'' : <blockquote>que je meure immédiatement, pour peu que je punisse le coupable, plutôt que de rester ici, à être la risée de tous, assis sur mes vaisseaux, poids inutile de la terre.<ref>Homère, ''Iliade'', XVIII, v. 96-104 ; cité par Socrate en ''Apologie'', 28c-d.</ref></blockquote> Achille a donc méprisé la mort pour préserver l’honneur. Celui qui occupe une place, explique Socrate, doit y rester, au péril de sa vie, « sans tenir compte d’autre chose que du déshonneur ». Cet argument est une adaptation du modèle homérique, et non une simple reprise. Socrate transforme l’héroïsme aristocratique d’Achille (celui d’un demi-dieu, fils d’une déesse) en une vertu démocratique accessible à tout soldat de la phalange hoplitique : il s’agit de tenir son poste, quelle que soit sa place, qu’on l’ait choisie ou qu’on y ait été affecté par son chef (28d). La vertu n’est plus aristocratique, elle est civique ; elle n’est plus la propriété d’une élite, elle est accessible à quiconque occupe sa place avec fermeté. Cette démocratisation de la vertu héroïque prépare la transposition qui va suivre. Socrate rappelle d’ailleurs son propre passé militaire. Il a combattu à Potidée (432-429), à Amphipolis (424), et surtout à Délion (424), où son courage fut loué par Alcibiade dans le ''Banquet'' (219e et suivants<ref>Platon, ''Banquet'', 219e-221c.</ref>) et par le général Lachès dans le dialogue du même nom (''Lachès'', 181a-b<ref>Platon, ''Lachès'', 181a-b.</ref>). Comme ces soldats qui ne quittent pas leur poste, Socrate ne peut quitter celui qui lui a été assigné, par le dieu. ===== Le poste assigné par le dieu (28d-29a) ===== Socrate pose alors la transposition capitale : <blockquote>le poste qu’on m’a assigné, moi, est celui du philosophe, qui doit vivre en philosophant, en s’examinant soi-même et en examinant les autres. Je ne peux le quitter par crainte de la mort, pas plus qu’un soldat ne peut quitter le sien.</blockquote> La philosophie est ainsi présentée comme une assignation divine, équivalente à l’ordre d’un chef de guerre, et plus impérieuse encore, puisque l’ordre vient du dieu. Cette analogie entre vie philosophique et vie militaire, qui fera carrière dans toute la tradition stoïcienne (Sénèque, Épictète, Marc Aurèle en useront abondamment<ref>Voir notamment Épictète, ''Entretiens'', I, 9, 24 ; III, 24, 31-36 ; Marc Aurèle, ''Pensées'', III, 5 ; VII, 45.</ref>), fonde la philosophie comme service, comme ''officium'', comme devoir qu’on ne peut déserter sans se déshonorer. ===== L’ignorance de la mort (29a-b) ===== Vient alors l’un des passages les plus célèbres du texte, où Socrate renverse la psychologie commune du courage : <blockquote>Craindre la mort, Athéniens, ce n’est rien d’autre que se donner pour savant sans l’être ; c’est donner l’impression qu’on sait ce qu’on ne sait pas. (29a)</blockquote> Car personne ne sait ce qu’est la mort, ni si elle n’est pas pour l’homme le plus grand des biens ; mais on la redoute comme si l’on savait qu’elle est le plus grand des maux. C’est là, dit Socrate, la forme la plus répréhensible d’ignorance : croire savoir ce qu’on ne sait pas, donc la même erreur que celle des faux sages qu’il a démasqués dans son enquête. Le raisonnement est d’une rigueur remarquable. Il articule le savoir du non-savoir à l’éthique du courage. Socrate, lui, sait qu’il ne sait rien de la mort ; il ne la craint donc pas. Mais il sait en revanche, et c’est la seule « exception » à son ignorance proclamée, que <blockquote>commettre une injustice et désobéir à un meilleur que soi, dieu ou homme, cela je sais que c’est mauvais et honteux. (29b)</blockquote> On voit ici le dispositif éthique qui va commander toute la suite : entre un mal certain (l’injustice et la lâcheté) et un mal supposé mais incertain (la mort), le sage choisit sans hésiter d’éviter le premier. Le courage philosophique n’est donc pas un mépris enthousiaste de la mort, comme celui d’Achille, c’est une lucidité sur ce qui est réellement à craindre. Ce renversement de la psychologie héroïque en lucidité rationnelle est l’un des gestes fondateurs de la philosophie morale. ===== L’hypothèse de l’acquittement conditionnel (29c-30c) ===== Socrate imagine alors une situation extrême, une sorte d’expérience de pensée. Supposons que les juges lui offrent de l’acquitter à la condition qu’il cesse de philosopher. Alors Socrate répondrait : <blockquote>Athéniens, je vous suis reconnaissant et je vous aime, mais j’obéirai au dieu plutôt qu’à vous ; et tant qu’il me restera un souffle de vie, tant que j’en serai capable, je ne cesserai, soyez-en sûrs, de philosopher, de vous exhorter et de m’expliquer avec tel ou tel d’entre vous. (29d)</blockquote> Il continuerait à dire à chacun la formule qui résume toute sa mission : <blockquote>Ô excellent homme, toi qui es d’Athènes, la cité la plus grande et la plus réputée pour son savoir et sa puissance, tu n’as pas honte de t’occuper de ta fortune et des moyens de t’enrichir le plus possible, de ta réputation, des honneurs, alors que de ton intelligence, de la vérité, de ton âme et des moyens de la perfectionner, tu ne t’en occupes et ne t’en soucies aucunement ? (29d-e)</blockquote> On est ici au cœur du message socratique, tel que Platon l’a consigné. Le renversement qu’opère ce passage est philosophiquement majeur. D’une part, Socrate affirme que son obéissance au dieu l’emporte sur son obéissance à la cité : c’est, en puissance, toute la doctrine de la désobéissance civile au nom d’une norme transcendante. D’autre part, il renverse la hiérarchie des biens : la vertu ne vient pas de l’argent, mais l’argent et tous les autres biens viennent de la vertu ; il faut donc se soucier prioritairement de son âme (''psuchḗ''), et non de ses biens matériels ou de sa réputation. L’''Apologie'' est ainsi, dans la philosophie occidentale, l’un des textes où s’origine ce thème du souci de soi (''epiméleia heautoû'') compris comme soin de l’âme et examen permanent de soi-même, thème qui parcourra toute la tradition ultérieure, des écoles hellénistiques aux spirituels chrétiens, jusqu’aux lectures contemporaines de Michel Foucault qui en fera un objet majeur de ses derniers cours au Collège de France<ref name="foucault-hs">Michel Foucault, ''L’Herméneutique du sujet. Cours au Collège de France, 1981-1982'', éd. F. Gros, Paris, Gallimard/Seuil, coll. « Hautes Études », 2001, en particulier les leçons des 6, 13 et 20 janvier 1982.</ref>. Socrate ajoute alors l’une de ses déclarations les plus provocatrices : <blockquote>Là-dessus, Athéniens, croyez-en ou non Anytos, acquittez-moi ou ne m’acquittez pas, toujours est-il que je ne changerai pas de conduite, même si je devais souffrir mille morts. (30c)</blockquote> La défense ne demande plus un acquittement ; elle proclame la pérennité de la mission, quel que soit le verdict. Socrate, à ce moment précis du plaidoyer, cesse d’être un accusé pour devenir un apôtre. Cette attitude explique, rétrospectivement, l’incompréhension et l’irritation des jurés : il ne se défend pas, il les défie. ==== Socrate, « taon de la cité » (30c-31c) ==== Socrate affirme alors, avec une audace étonnante, <blockquote>si vous me condamnez à mort, ce n’est pas à moi, mais à vous-mêmes, que vous ferez le plus de tort. (30c)</blockquote> Car ni Mélétos ni Anytos ne peuvent le léser véritablement : ils peuvent le tuer, l’exiler, le priver de ses droits civiques (''atimía''), choses que certains tiendraient pour de grands malheurs, mais lui ne les tient pas pour tels. Le vrai mal est celui que font à leur âme ceux qui entreprennent de tuer injustement. Cette thèse, que Platon développera dans le ''Gorgias'' (469c) sous la forme « il vaut mieux subir l’injustice que la commettre »<ref>Platon, ''Gorgias'', 469c : « [...] je choisirais de subir plutôt que de commettre l’injustice. » Voir aussi 474b-479e.</ref>, est probablement la plus radicale de toutes les thèses morales de l’antiquité. Surgit alors la célèbre image du taon. Socrate est <blockquote>un homme attaché à la cité par le dieu, comme le serait un taon au flanc d’un cheval de grande taille et de bonne race, mais qui se montrerait un peu mou en raison même de sa taille et qui aurait besoin d’être réveillé par l’insecte. (30e)</blockquote> Athènes est ce cheval noble mais assoupi ; Socrate est l’insecte qui le pique, le réveille, le harcèle. Le dieu lui a donné cette mission, qui explique qu’il passe son temps à aborder chacun, « comme un père ou un frère plus âgé », pour le persuader d’avoir souci de la vertu. Cette image mérite qu’on s’y arrête, tant elle est dense. Elle articule trois éléments. D’abord, la noblesse du cheval : Athènes n’est pas critiquée absolument, mais reconnue pour ce qu’elle est, la plus belle cité du monde grec, de « grande taille et de bonne race ». Ensuite, son engourdissement : cette grandeur même la rend molle, somnolente, incapable de s’ébrouer spontanément. Enfin, la nécessité du taon : seule une figure dérangeante, insupportable, inutile en apparence, peut réveiller la cité. Le taon n’est pas à sa place dans le cheval ; il est un corps étranger, irritant ; mais précisément, c’est de cette position dérangeante que vient son utilité. La philosophie est pensée ici comme critique nécessaire, comme dissidence féconde, comme décalage qui maintient la cité vivante. On voit se dessiner une dialectique subtile. Socrate est indissociablement dedans et dehors : citoyen d’Athènes, engagé dans sa cité, respectueux de ses lois au point d’accepter la mort plutôt que de fuir (comme l’exprimera le ''Criton''<ref>Platon, ''Criton'', 50a-54d.</ref>), et en même temps étranger à ses conformismes, à ses illusions, à ses complaisances. Sans le taon, le cheval dormirait ; mais le cheval peut aussi, d’un mouvement irrité, écraser le taon. C’est exactement ce qui se passe au procès. L’image contient en elle-même une prophétie : tuer le taon, c’est se priver de la piqûre bienfaisante, condamner la cité au sommeil. D’où la prédiction de Socrate : <blockquote>en suite de quoi, vous passeriez votre vie à dormir, à moins que le dieu, ayant souci de vous, ne vous envoie quelqu’un d’autre. (31a)</blockquote> La suite de l’histoire, à tout le moins celle de la pensée, dira que cet autre sera Platon lui-même, puis Aristote, et la longue lignée des philosophes que l’''Apologie'' aura rendus possibles. Socrate apporte ensuite une preuve empirique de son désintéressement : sa pauvreté. Si son activité avait un but intéressé, si elle rapportait un salaire, on pourrait douter de la pureté de ses motivations. Mais ses accusateurs, malgré leur acharnement, n’ont pu produire aucun témoin attestant qu’il ait jamais exigé ou reçu un salaire. Sa misère est la meilleure preuve qu’il dit vrai. Cette insistance sur la gratuité de son enseignement est une pique adressée aux sophistes, qui se faisaient richement rémunérer, et un trait supplémentaire qui distingue la philosophie socratique de la ''téchnē'' marchande des sophistes. Socrate n’est pas un prestataire de services ; il est un serviteur du dieu. ==== Le démon et la prudence politique (31c-32e) ==== Une objection se présente naturellement : si Socrate est ce grand conseiller des particuliers, pourquoi ne monte-t-il pas à la tribune pour conseiller la cité elle-même dans ses assemblées ? La réponse est le fameux passage sur le ''daimónion'' socratique. ===== Qu’est-ce que le démon de Socrate ? (31c-d) ===== Socrate confie aux juges ce qu’il a déjà dit « maintes fois en maints endroits » : <blockquote>il m’advient quelque chose de divin et de démonique (''theîón ti kai daimónion''), une voix intérieure qui, depuis [mon] enfance, [...] chaque fois qu’elle m’advient, me détourne toujours de ce que je me propose de faire, mais jamais ne m’y encourage. (31c-d)</blockquote> Cette voix a trois caractéristiques remarquables. Elle est toujours dissuasive : « jamais elle ne m’y encourage ». Elle est personnelle : elle ne s’adresse qu’à Socrate. Elle est présente depuis l’enfance, donc constitutive de son rapport au monde. Elle s’est précisément opposée à son entrée en politique. Il s’agit donc, littéralement, de cette « divinité nouvelle » que l’accusation lui impute, ce que Mélétos, ironise Socrate, a d’ailleurs « consigné dans son acte d’accusation » (31d). Cette ironie est cinglante : l’accusation a pris pour un crime ce que Socrate revendique comme une grâce. La nature du ''daimónion'' a fait l’objet de multiples interprétations, dès l’Antiquité et jusqu’aux temps modernes. Plutarque a consacré un traité entier à la question (''Du démon de Socrate'') dans lequel il discute plusieurs hypothèses<ref>Plutarque, ''Du démon de Socrate'' (''De genio Socratis''), dans ''Œuvres morales'', t. VIII, trad. J. Hani, Paris, Les Belles Lettres, 1980.</ref>. À l’époque moderne, on l’a interprété tour à tour comme une hallucination d’un névrosé<ref>F. Lélut, ''Du démon de Socrate, spécimen d’une application de la science psychologique à celle de l’histoire'', Paris, Trinquart, 1836.</ref>, comme une manifestation de l’inconscient<ref>Arthur Koestler, ''Le Démon de Socrate'', Paris, Calmann-Lévy, 1970.</ref>, comme la voix de la conscience morale<ref>G. W. F. Hegel, ''Leçons sur l’histoire de la philosophie'', tome II (sur Socrate), trad. G. Marmasse, Paris, Vrin, 2007, p. 316-321.</ref>, comme une inspiration divine authentique<ref>Henri Bergson, ''Les Deux sources de la morale et de la religion'' (1932), dans ''Œuvres'', éd. du Centenaire, Paris, PUF, 1959, p. 1027.</ref>, comme une intuition irrationnelle<ref>E. R. Dodds, ''Les Grecs et l’irrationnel'' (1951), trad. M. Gibson, Paris, Flammarion, coll. « Champs », 1977, chap. VII.</ref>. Nietzsche y voyait, de manière originale, la monstruosité d’un homme chez qui l’instinct, contrairement à l’ordinaire, ne crée pas mais critique, et chez qui la conscience rationnelle est au contraire créatrice : Socrate comme « homme théorique », rupture dans l’histoire de l’esprit grec dionysiaque<ref name="nietzsche-nt">Friedrich Nietzsche, ''La Naissance de la tragédie'' (1872), § 13-15, trad. P. Lacoue-Labarthe, dans ''Œuvres philosophiques complètes'', t. I, Paris, Gallimard, 1977.</ref>. Il faut probablement admettre, avec Claude Chrétien, que le démon est irréductible à une simple astuce défensive ou à un symbole : Socrate y croyait réellement, au point de risquer sa vie en le suivant<ref>Chrétien, ''op. cit.'', p. 28-29.</ref>. Son caractère purement négatif (il inhibe, ne prescrit jamais) en fait un signe du divin dans la vie humaine, mais un signe essentiellement limitatif : la divinité indique seulement ce qu’il ne faut pas faire, et laisse à l’homme la responsabilité de chercher, par l’examen rationnel, ce qu’il doit faire. Cette structure, où le divin ne donne pas la vérité mais seulement la limite, est cohérente avec la théologie négative que Socrate manifeste dans tout le plaidoyer : nous ne savons pas positivement ce que sont les dieux, mais nous recevons d’eux des signes qui nous empêchent de nous égarer. ===== Pourquoi pas la politique ? (31d-32a) ===== Socrate explique que si le démon l’a détourné de la politique, c’est pour préserver sa vie : « s’il y avait longtemps que j’avais entrepris de faire de la politique, il y a longtemps que je serais mort ». Et il formule alors une sentence vertigineuse, qui est peut-être la plus critique de l’''Apologie'' à l’égard de la démocratie athénienne : <blockquote>il n’y a personne au monde qui puisse garder la vie sauve s’il s’oppose loyalement à vous ou à toute autre collectivité, et s’il cherche à empêcher qu’il ne se produise dans la cité de nombreuses injustices et illégalités. Mais nécessairement tout vrai champion de la justice, s’il veut garder la vie sauve ne serait-ce qu’un peu de temps, doit vivre en simple particulier (''idiōteúein'') mais non en homme public. (32a)</blockquote> Cette sentence est d’une portée immense. Elle signifie que la politique telle qu’elle se pratique à Athènes est incompatible avec la justice. Le juste, s’il veut vivre, doit rester à l’écart des affaires publiques ; et s’il y entre, il doit s’attendre à mourir. C’est la cassure socratique avec la tradition civique grecque, qui voyait dans la participation politique (''politeía'') la plus haute réalisation de l’homme libre. L’homme proprement libre, pour les Grecs classiques, c’est le citoyen qui prend part aux assemblées ; l’''idiṓtēs'' qui se retire dans la sphère privée est, sinon méprisé, du moins considéré comme incomplet. Socrate renverse cette hiérarchie : la vraie vie politique, pour le juste, passe par le retrait de la politique officielle et par une politique privée, celle de la discussion personne à personne, de l’enseignement moral qui opère non par les discours publics mais par l’examen intime de chacun. C’est, comme le suggère la présentation de la collection Flammarion, « l’espace d’une autre politique »<ref name="brisson-mace-presentation">Arnaud Macé, « Présentation », dans Platon, ''Apologie de Socrate'', traduction par Luc Brisson, Paris, GF-Flammarion, 2017.</ref>. Cette thèse, qu’on peut lire comme une désertion civique, est aussi une critique profonde des conditions de la délibération démocratique. Elle rejoint ce que Platon développera dans la ''République'' : la cité idéale est celle où la philosophie serait au pouvoir, non celle où elle est écrasée par le plus grand nombre. Mais l’''Apologie'' n’est pas encore la ''République'' : Socrate n’y propose pas une contre-cité, il y constate seulement qu’aucune cité existante ne permet au juste de participer à ses affaires sans se renier. ===== Les deux épisodes probants (32a-e) ===== Socrate prouve cette thèse par deux épisodes biographiques, choisis avec soin. Le premier se situe sous la démocratie, en 406, l’année du procès des généraux des Arginuses. Les Athéniens avaient remporté une victoire navale importante contre Sparte près des îles Arginuses (au large de Lesbos), mais les généraux victorieux avaient été empêchés par une tempête de ramasser les cadavres et les naufragés athéniens, violation grave des usages religieux. Rentrés à Athènes, ils furent mis en cause ; mais au lieu de leur garantir un procès individuel comme l’exigeait le droit athénien, l’Assemblée, emportée par la colère populaire, voulut les juger en bloc. Socrate siégeait ce jour-là au Conseil, comme prytane pour sa tribu, l’Antiochide. Il fut le seul des cinquante prytanes à refuser de mettre aux voix cette motion collective, malgré les menaces et les cris de la foule<ref>Le récit détaillé du procès des généraux des Arginuses se trouve chez Xénophon, ''Helléniques'', I, 7, 1-35.</ref>. Les orateurs voulaient le faire arrêter sur-le-champ, les citoyens eux-mêmes l’y encourageaient ; il tint bon. Les généraux furent néanmoins jugés et six d’entre eux exécutés. Peu après, Athènes regretta sa décision, mais Socrate avait risqué sa vie pour la légalité, sans succès immédiat. Le second épisode se situe sous l’oligarchie, en 404. Les Trente l’avaient convoqué avec quatre autres citoyens à la Tholos (la rotonde, siège des prytanes occupée par le régime) et lui avaient ordonné d’aller chercher à Salamine un riche citoyen démocrate, Léon, pour qu’il soit exécuté et que ses biens soient confisqués. C’était une manœuvre classique des Trente : compromettre un maximum de citoyens dans leurs crimes pour les rendre solidaires du régime<ref>Xénophon, ''Helléniques'', II, 3, 39 ; Platon, ''Lettre VII'', 324d-325a.</ref>. Socrate, lui, refusa l’ordre. Il rentra simplement chez lui pendant que les quatre autres allaient chercher Léon, qui fut assassiné. Socrate, rappelle-t-il, aurait probablement payé cela de sa vie si les Trente n’avaient pas été renversés peu après (c’était le cas : le régime tomba en 403). Ces deux épisodes sont politiquement remarquables, et Platon les a sans doute choisis avec une intention nette. Ils montrent Socrate s’opposant également aux excès de la démocratie (procès des Arginuses) et à ceux de l’oligarchie (affaire de Léon), par fidélité à une justice supérieure au régime en place. Il n’est ni un démocrate de conviction ni un oligarque : il est un homme qui, dans l’un et l’autre cas, risque sa vie pour ne pas commettre d’injustice. Cette double symétrie est cruciale : elle répond par avance à tous ceux qui, dans la démocratie restaurée de 399, voudraient voir en Socrate un sympathisant des Trente, en raison de ses liens avec Critias et Charmide. Platon montre au contraire que Socrate a résisté aux Trente au péril de sa vie. Mais il montre également qu’il a résisté à la démocratie elle-même, quand celle-ci violait le droit. La neutralité politique de Socrate, ou plutôt cet au-delà du partisanisme, constitue une part de sa radicalité, qui déconcerte tous ceux qui voudraient l’enrôler dans un camp. ==== Socrate et ses « disciples » (33a-34b) ==== Socrate en vient alors au dernier volet du premier discours : la question des jeunes gens qu’on l’accuse d’avoir corrompus. Il récuse d’abord le terme même de « disciple » : <blockquote>je n’ai jamais, moi, été le maître (''didáskalos'') de personne. (33a)</blockquote> Cette affirmation est importante. Elle distingue radicalement Socrate des sophistes, qui se présentaient comme maîtres (''didáskaloi'') et vendaient un enseignement structuré ; Socrate n’a jamais promis un enseignement, jamais fait payer, jamais suivi un programme ; il a parlé à quiconque voulait l’écouter, jeunes et vieux, riches et pauvres, sans distinction, et n’est pas responsable de ce que chacun devient au sortir de la conversation. Cette posture est philosophiquement significative : elle implique que la philosophie n’est pas transmissible comme une technique, mais seulement comme une pratique qu’on ne peut qu’imiter. Socrate produit alors un argument a contrario d’une grande force. Si réellement il avait corrompu les jeunes, pourquoi n’est-ce pas ''eux-mêmes'', devenus adultes, qui viendraient témoigner contre lui ? Ou du moins leurs proches, parents, frères, qui auraient à se plaindre de cette corruption et en seraient les premiers concernés ? Or, bien au contraire, nombre de ses familiers, ou de leurs proches, sont présents à l’audience pour le soutenir. Socrate en énumère plusieurs, nommément, dans un passage qui vaut témoignage historique : Criton et son fils Critobule, Lysanias de Sphettos père d’Eschine (l’auteur de dialogues socratiques), Antiphon père d’Épigène, Nicostratos frère de Théodotos, ainsi qu’Adimante (le frère aîné de Platon) et plusieurs autres<ref>''Apologie'', 33d-34a. Sur ces personnages, voir Debra Nails, ''The People of Plato: A Prosopography of Plato and Other Socratics'', Indianapolis, Hackett, 2002.</ref>. <blockquote>Je pourrais citer pour vous beaucoup d’autres hommes, parmi lesquels il aurait fallu que Mélétos produise, au cours de son discours, quelque témoin. (34a)</blockquote> Cette preuve par les témoins absents est d’une grande force logique : l’accusateur a été incapable de trouver, parmi tous ceux qui auraient dû être les premières victimes, un seul pour le blâmer. C’est ce qu’on appelle un argument ''a silentio'' : le silence des supposées victimes prouve l’innocence de l’accusé. On notera que Platon se fait ici historien. En nommant les disciples présents, il fixe un moment dans le temps et offre à la postérité un témoignage vérifiable. Il se nomme lui-même plus loin (34a, puis 38b), parmi les amis prêts à se porter caution pour l’amende. Cette présence documentaire est rare dans les dialogues platoniciens, où Platon s’efface généralement : ici, il est témoin du procès de son maître. ==== Le refus du pathos (34b-35d) ==== Socrate aborde alors la péroraison de son premier discours. Il sait parfaitement ce qu’on attend d’un accusé athénien au moment crucial : larmes, supplications, présentation de la femme et des enfants éplorés, appel à la pitié, théâtralité de la détresse. Cette mise en scène, connue de tous par les comédies d’Aristophane et par l’habitude des tribunaux, était devenue quasi rituelle<ref>Sur la caricature du tribunal populaire, voir Aristophane, ''Les Guêpes'', v. 548-630.</ref>. Socrate a trois fils, dont l’un est déjà adolescent (''meirákion'') et les deux autres encore petits ; il pourrait les faire paraître, lui qui ne refuse pas d’être un homme. Mais il ne le fera pas. Pourquoi ? Deux motifs convergent. D’abord, par souci de l’honneur : un homme de sa réputation, réelle ou supposée, ne peut s’abaisser à ces scènes sans se ridiculiser et sans « ridiculiser la cité ». Les citoyens étrangers qui l’observent et qui connaissent la réputation d’Athènes s’étonneraient de voir les plus éminents de ses hommes se comporter de manière indigne. Socrate invoque la ''dóxa'' (l’opinion) d’Athènes aux yeux du monde grec pour justifier son refus de jouer le jeu. Mais surtout, et c’est le second motif, plus profond : il ne serait pas juste de supplier le juge. Et ici, Socrate formule une analyse capitale de la fonction judiciaire : <blockquote>le juge ne siège pas pour réduire la justice en faveur (''charízesthai''), mais pour décider de ce qui est juste ; et il a fait serment non de favoriser qui lui plaît, mais de rendre la justice selon les lois. (35c)</blockquote> Le juge athénien prêtait en effet un serment (l’''héliastikós hórkos'') par lequel il s’engageait à juger selon les lois<ref>Sur le serment héliastique, voir Démosthène, ''Contre Timocrate'', 149-151 ; Adriaan Lanni, ''Law and Justice in the Courts of Classical Athens'', Cambridge, Cambridge University Press, 2006, p. 75-76.</ref>. Supplier les juges, c’est leur demander de se parjurer, donc d’introduire le parjure dans la cité, donc de commettre une impiété effective contre les dieux garants du serment. Le geste de Socrate est ici d’une cohérence parfaite et quasi mathématique. Lui qui est accusé d’impiété ne peut, à l’instant critique, demander aux juges de commettre une vraie impiété (le parjure) pour le sauver. Ce serait confirmer dans les faits, activement, l’accusation qu’il réfute en paroles. Il préfère la mort à cet abaissement, qui serait en outre, non plus imaginairement mais réellement, une atteinte aux dieux de la cité. Par ce refus, Socrate démontre par les actes ce qu’il prétendait par les mots : il est le véritable pieux, et ce sont les accusateurs qui, en voulant la mort d’un juste, pratiquent la vraie impiété. Le premier discours s’achève là. Les juges votent. Socrate est déclaré coupable. La majorité est étroite : si trente voix s’étaient portées sur l’autre bord, Socrate aurait été acquitté. Pour un jury de 501, cela suggère un vote d’environ 280 contre 221 (chiffres que Diogène Laërce confirme dans son récit<ref>Diogène Laërce, ''Vies et doctrines des philosophes illustres'', II, 41.</ref>, mais qui ne sont pas dans Platon). Ce qui est frappant, c’est la relative faiblesse de la condamnation : l’acquittement était à portée. === Le second discours : la contre-peine (35e-38b) === La procédure athénienne exige maintenant que Socrate propose une peine alternative à celle réclamée par l’accusation. Mélétos a proposé la mort. L’usage voulait qu’on proposât une peine sensiblement moins sévère (un lourd exil, une amende importante) pour donner au jury une alternative crédible. Le calcul tacite, dans les procès à peine à estimer, consistait à offrir une sanction à peine en deçà de celle demandée par l’accusateur, de manière à ne pas trop décevoir les attentes du jury tout en se ménageant un sort moins dur. Socrate va adopter une tout autre stratégie. ==== La contingence du vote (35e-36b) ==== Socrate remarque d’abord, avec une ironie qui frise la provocation, qu’il est étonné non pas d’avoir été condamné, mais de l’avoir été à une si faible majorité. Il s’attendait à une condamnation bien plus nette. Si trente voix de plus avaient basculé, il aurait été acquitté. Il observe aussi que, ce qui le perd véritablement, ce n’est pas Mélétos : car sans l’appui d’Anytos et de Lycon, le seul Mélétos, compte tenu des voix obtenues, aurait dû payer mille drachmes d’amende pour n’avoir pas recueilli le cinquième des suffrages (règle destinée à décourager les plaintes frivoles). En divisant malicieusement les voix reçues entre ses trois accusateurs, Socrate montre que Mélétos seul n’aurait pas obtenu sa condamnation. Cette remarque, en apparence technique, est profondément déstabilisante : elle montre que la procédure qui vient de condamner Socrate est elle-même contingente, dépendante du nombre d’accusateurs autant que du fond du dossier. Elle suggère, au passage, que la véritable force du parti de l’accusation réside dans Anytos, l’homme politique, non dans Mélétos, le plaignant nominal. Le procès apparaît donc comme un montage politique, sous un habillage religieux. ==== La proposition du Prytanée (36b-37a) ==== Quelle contre-peine proposer ? Socrate prend la question au sérieux, mais dans un sens retourné : quelle peine mérité-je ? Il rappelle toute sa vie : avoir négligé les affaires, l’argent, les magistratures, les assemblées et les honneurs, pour se consacrer au service privé de la vertu, <blockquote>en essayant de convaincre chacun d’entre vous de ne pas se préoccuper de ses affaires personnelles avant de se préoccuper, pour lui-même, de la façon de devenir le meilleur et le plus sensé possible. (36c)</blockquote> Que mérite un homme ainsi ? Un bon traitement, dit-il, et non une peine. Il propose donc non pas un châtiment, mais une récompense : être nourri aux frais de l’État au Prytanée. Le Prytanée était à Athènes l’édifice public où la cité nourrissait, aux frais de l’État, les prytanes en exercice, les hôtes officiels et les citoyens illustres, notamment les vainqueurs des jeux Olympiques et les bienfaiteurs de la patrie. C’était la plus haute distinction civique, l’équivalent d’une reconnaissance par l’État comme héros ou sauveur de la cité. Socrate explique qu’il la mérite plus que quiconque : <blockquote>si celui-ci [le vainqueur olympique] vous procure l’apparence du bonheur, je vous en offre, moi, la réalité ; lui n’a aucun besoin d’être nourri, mais moi, j’en ai besoin. (36d-e)</blockquote> L’argument est double : Socrate est un bienfaiteur réel (plus que le champion olympique, dont la gloire n’est que sportive) et il est pauvre (donc il a besoin de ce soutien alimentaire, alors que le champion en a moins besoin). Cette proposition est manifestement provocatrice, et aucun commentateur n’en doute. Socrate ne joue plus le jeu de la procédure ; il retourne le tribunal. Pour un jury qui vient de le condamner, proposer d’être traité en héros civique est une humiliation délibérée. Comment comprendre cette audace ? Plusieurs explications se combinent. D’abord, une cohérence logique : Socrate est convaincu de n’avoir commis aucune injustice, il refuse donc de s’infliger une peine comme s’il était coupable. Ensuite, une fidélité à sa parole : s’il proposait une peine qu’il estime injuste, il trahirait son principe d’agir toujours selon le juste. Enfin, peut-être, une acceptation anticipée de la mort : il est âgé (soixante-dix ans, espérance de vie largement dépassée dans l’Antiquité), il a accompli sa mission, il ne craint pas la sentence, il n’a donc aucune raison de ruser. À quoi s’ajoute, plus subtilement, un calcul dramatique : proposer une vraie contre-peine reviendrait à reconnaître la compétence du tribunal ; en proposant une récompense, Socrate refuse symboliquement la sentence avant même qu’elle ne tombe. ==== L’examen des autres peines et la proposition d’amende (37a-38b) ==== Socrate continue son raisonnement avec une rigueur didactique : puisque je sais que je ne me cause volontairement de tort à personne, je ne vais pas, loin de là, m’en causer à moi-même en proposant une peine qui serait un tort. Il passe alors en revue, méthodiquement, les peines possibles. La prison ? Ce serait passer sa vie soumis au pouvoir des Onze, les magistrats qui administraient les prisons et les exécutions, donc à une sujétion indigne. Une amende lourde assortie de contrainte par corps jusqu’au paiement ? Cela revient au même : il n’a pas d’argent. L’exil ? C’est ici que Socrate développe sa réponse la plus fine. Il serait absurde, dit-il, de choisir l’exil : si ses propres concitoyens ne supportent plus ses entretiens, au point d’en vouloir « se débarrasser », pourquoi les étrangers les supporteraient-ils davantage ? Il serait chassé de ville en ville. Et ne pourrait-il vivre en se taisant ? Non, et ici vient l’un des sommets du texte : cesser de philosopher équivaudrait à désobéir au dieu. <blockquote>Il n’y a pas pour un homme de plus grand bien que de s’entretenir chaque jour de la vertu et des autres sujets dont vous m’entendez discuter, en examinant moi-même les autres ; car une vie sans examen n’est pas digne d’être vécue par un homme. (38a)</blockquote> Cette dernière formule, en grec ''ho anéxetastos bíos ou biōtós anthrṓpōi'', est probablement la plus célèbre de toute l’''Apologie'' et peut-être de toute la philosophie antique. Elle condense le programme de la philosophie socratique : la vie doit être soumise à un examen (''exétasis'') constant, à une interrogation rationnelle sur ses buts et ses valeurs. Sans cet examen, on ne vit pas une vie proprement humaine. Formule vertigineuse, qui fait de la philosophie non pas un luxe intellectuel mais la condition même d’une existence digne de ce nom. Elle suggère qu’il existe un seuil d’humanité : en deçà de l’examen, l’homme n’est pas pleinement homme. La philosophie cesse alors d’être une option ajoutée à la vie pour devenir l’élément qui en fait une vie humaine. Finalement, Socrate concède une mine d’argent (somme modique, correspondant à peu près à trois mois de salaire d’un ouvrier qualifié), mais accepte, sur la pression de ses amis (Platon, Criton, Critobule, Apollodore), de proposer trente mines, avec leur caution personnelle. C’est une somme importante, équivalent à plusieurs années de salaire, mais manifestement insuffisante face à la proposition de mort, et la manière dont elle est introduite (sous la pression des amis, comme en dernier recours) ne masque pas que Socrate lui-même n’y adhère pas vraiment. Le jury vote une seconde fois. Cette fois, la majorité est beaucoup plus nette : selon les chiffres traditionnels rapportés par Diogène Laërce, quatre-vingts jurés supplémentaires ont voté contre Socrate par rapport au premier vote, manifestement indignés par son attitude. Socrate est condamné à mort. === Le troisième discours : après la condamnation (38c-42a) === Ce troisième discours n’est pas prévu par la procédure. Une fois la sentence prononcée, les magistrats passent aux formalités d’enregistrement, notamment la notification aux Onze, chefs des geôliers et du bourreau. Socrate prend pourtant la parole une dernière fois, sans doute pendant que les Onze procèdent à ces écritures, pour s’adresser à la partie de l’assistance qui est restée sur place. Il s’agit d’une péroraison spontanée, hors procédure, probablement élargie et composée par Platon pour les besoins du texte, même si l’événement lui-même est plausible. Ce discours se divise en deux moments : d’abord aux jurés qui ont voté sa mort, puis à ceux qui ont voté son acquittement. ==== Aux jurés de la condamnation (38c-39d) ==== Socrate commence par un constat ironique : les Athéniens ont gagné peu de temps, il est âgé, il serait mort bientôt de toute façon. Mais en échange, ils vont gagner <blockquote>le renom, auprès des gens avides de diffamer notre cité, d’avoir fait mourir un sage en la personne de Socrate. (38c)</blockquote> La réputation d’Athènes souffrira de ce procès bien au-delà de ce qu’elle a cru gagner. La prédiction est parfaitement exacte : la condamnation de Socrate a, dans la mémoire collective de l’Occident, sérieusement terni l’image de la démocratie athénienne. Surtout, Socrate retourne contre les juges l’argument central de son plaidoyer : s’il n’a pas réussi à les persuader, ce n’est pas faute d’arguments, mais parce qu’il n’a pas voulu employer les tactiques indignes (larmes, supplications) qu’ils attendaient. <blockquote>Je préfère de beaucoup mourir après m’être défendu comme je l’ai fait plutôt que vivre après un plaidoyer à leur façon. (38e)</blockquote> Et il prononce cette antithèse fameuse : <blockquote>la difficulté n’est pas d’échapper à la mort, elle est bien plus d’échapper à la lâcheté (''ponēría''), car elle court plus vite que la mort. En l’occurrence, moi qui suis lent et vieux, j’ai été rattrapé par la plus lente des deux [la mort], cependant que mes accusateurs, qui sont lestes et rapides, ont été rattrapés par la plus rapide, qui est la méchanceté. (39a-b)</blockquote> L’image est saisissante : chaque homme est poursuivi par son destin, mais les uns sont rattrapés par la mort physique et les autres par la mort morale, infiniment plus grave. Socrate se permet alors une prophétie (''manteúomai''). Il est, dit-il, <blockquote>au point où les hommes prophétisent le mieux, quand ils sont à la veille de mourir, (39c)</blockquote> allusion à la croyance grecque selon laquelle les mourants acquièrent un don de divination (voir le motif du chant du cygne dans le ''Phédon'' 84e<ref>Platon, ''Phédon'', 84e-85b : « les cygnes [...], lorsqu’ils sentent qu’ils vont mourir, chantent ce jour-là plus fort et plus beau qu’ils n’ont jamais chanté, dans la joie d’aller trouver le dieu dont ils sont les serviteurs ».</ref>). Sa prophétie est terrible : <blockquote>un châtiment vous viendra aussitôt après ma mort, bien plus pénible que celui par lequel vous m’aurez tué. [...] Le nombre croîtra de ceux qui vous demanderont des comptes, que je retenais jusqu’ici, sans que vous vous en aperceviez ; et ils seront d’autant plus pénibles qu’ils sont plus jeunes. (39c-d)</blockquote> Tuer n’est pas la façon de se délivrer du blâme ; la seule manière honorable est « de se préparer soi-même à être le meilleur possible ». Cette prophétie peut sembler s’accomplir, dans une certaine mesure, par l’histoire qui suivra : les « petits socratiques » (Antisthène le cynique, Aristippe, Euclide de Mégare), puis Platon et son Académie, puis Aristote et le Lycée, poursuivront inlassablement l’interrogation commencée par Socrate. Le procès et la mort de Socrate peuvent ainsi être lus comme le moment à partir duquel la philosophie s’affirme, dans la tradition platonicienne, comme une vocation publique à part entière. ==== Aux jurés de l’acquittement (39e-42a) ==== Socrate se tourne alors vers ceux qui ont voté pour lui, qu’il appelle désormais, seuls, ''juges'' véritables. Cette distinction terminologique est capitale : avant le verdict, tous étaient indistinctement ''andres'' (« messieurs ») ou ''Athēnaîoi'' (« Athéniens ») ; maintenant, seuls ceux qui ont voté juste méritent, selon Socrate, le titre de juges (''dikastaí''). Le tribunal vient d’être scindé en deux catégories asymétriques. Il leur confie deux pensées, l’une sur le signe divin, l’autre sur la mort. ===== Le silence du démon (39e-40c) ===== Son démon, dit-il, avait coutume de l’arrêter chaque fois qu’il s’apprêtait à faire quelque chose de mauvais, même dans des circonstances mineures. Or, aujourd’hui, depuis le matin, tout au long de cette journée qui l’a mené à la condamnation à mort, le démon ne s’est pas manifesté une seule fois. Il ne l’a pas arrêté au moment où il quittait son domicile, ni lorsqu’il montait au tribunal, ni à aucun moment de son plaidoyer. Cette absence est elle-même un signe : <blockquote>il y a des chances pour que ce qui m’est arrivé soit un bien ; et c’est nous qui faisons des suppositions incorrectes quand nous considérons la mort comme un mal. (40b-c)</blockquote> Le silence du démon est la preuve, pour Socrate, que la voie qu’il suit est la bonne. L’argument est philosophiquement subtil. Socrate ne dit pas qu’il sait que la mort est un bien ; il dit qu’il a une raison de penser que ce qui lui arrive est un bien, puisque le dieu (par la voix du démon) ne l’a pas arrêté. C’est un argument ''e silentio'' transposé sur le plan religieux : le silence divin, pour qui est habitué à être averti, vaut approbation. Cette structure de raisonnement est fragile, mais elle est cohérente avec la théologie socratique : la divinité se manifeste par ses interventions plutôt que par ses paroles positives ; son silence, quand il y a habitude d’intervention, est significatif. ===== Les deux hypothèses sur la mort (40c-41d) ===== [[Fichier:David - The Death of Socrates.jpg|vignette|centre|upright=2.0|Jacques-Louis David, ''La Mort de Socrate'', 1787, huile sur toile, 129,5 × 196,2 cm, New York, Metropolitan Museum of Art. Le tableau illustre à proprement parler la scène finale du ''Phédon'' plutôt que l’''Apologie'' ; il est cependant devenu l’image emblématique, dans la culture occidentale moderne, du philosophe maintenant, jusque dans la mort, la cohérence entre sa parole et sa vie.]] Socrate propose alors une méditation sur la mort, l’une des plus belles pages de la philosophie antique, construite comme une alternative raisonnée. De deux choses l’une : ou bien la mort est l’absence de toute sensation, ou bien elle est une migration (''metoíkēsis'') de l’âme de ce lieu vers un autre lieu. Dans la première hypothèse (la mort comme absence de sensation), la mort ressemble au sommeil sans rêve. Or qui n’aimerait pas, si on lui demandait de choisir entre une nuit de sommeil profond sans rêves et toutes les autres nuits et jours de sa vie, reconnaître que cette nuit est plus précieuse que la plupart ? Même le Grand Roi de Perse (homme réputé le plus heureux du monde aux yeux des Grecs) trouverait peu de jours comparables à une telle nuit. Si la mort est cela, alors la totalité du temps après la mort se réduit à « une seule nuit », et cette nuit est un gain. L’argument est intéressant par sa structure : il part d’une expérience commune (le sommeil sans rêve) pour désamorcer la peur métaphysique de la mort. Si l’on aime le sommeil quand il nous prend, pourquoi craindre la mort si elle lui ressemble ? La stratégie argumentative, qui rappelle celle d’Épicure et de Lucrèce plus tard (« la mort n’est rien pour nous »<ref>Épicure, ''Lettre à Ménécée'', § 125 : « la mort n’est rien pour nous, puisque tant que nous sommes, la mort n’est pas, et quand la mort est là, nous ne sommes plus ». Lucrèce, ''De la nature des choses'', III, v. 830 et suiv.</ref>), désarticule la crainte en la confrontant à ce que nous expérimentons quotidiennement. Dans la seconde hypothèse (la mort comme migration), la mort est un voyage vers l’au-delà, où l’on retrouve tous les morts. Que pourrait-on imaginer de plus heureux ? On serait délivré des juges qui prétendent juger ici-bas, pour rencontrer les vrais juges dont on dit qu’ils y rendent la justice : Minos, Rhadamante, Éaque (les trois juges infernaux traditionnels), plus Triptolème (qui les remplace parfois dans l’iconographie attique). On rencontrerait Orphée, Musée, Hésiode, Homère, les grandes figures poétiques et religieuses du passé. On pourrait y continuer, sans cette fois risquer la mort, l’activité d’examen qui fut la sienne sur terre : interroger Palamède, Ajax (tous deux morts par jugements injustes, comme lui-même : la comparaison est évidemment à son avantage), le chef de l’armée grecque à Troie (Agamemnon), Ulysse, Sisyphe, « et tant d’autres hommes et femmes qu’on pourrait nommer ». <blockquote>Discuter avec ceux de là-bas, vivre en leur société, les soumettre à examen, ne serait-ce pas le comble du bonheur ? Aussi bien, les gens de là-bas ne mettent à mort personne pour ce motif. (41c)</blockquote> Ce passage est riche de tonalités. Il y a une évidente dimension humoristique : Socrate imagine l’au-delà comme la continuation indéfinie de son activité terrestre, l’examen dialectique, mais cette fois sans risque, puisqu’on n’y meurt plus. L’Hadès devient une agora élargie à tous les temps. Il y a également une dimension consolatoire : la comparaison avec Palamède et Ajax, héros victimes de procès injustes, ennoblit le sort de Socrate. Il y a enfin une dimension ironique vis-à-vis des jurés athéniens : les vrais juges ne sont pas à Athènes, mais dans l’Hadès, et Socrate se réjouit d’aller les retrouver. Ce passage a suscité des interprétations contrastées. Certains commentateurs y voient une réelle espérance platonicienne en l’immortalité de l’âme, telle qu’elle sera développée dans le ''Phédon''<ref>Platon, ''Phédon'', 80a-84b, 105c-107a.</ref>. D’autres, comme Chrétien, soulignent que le Socrate de l’''Apologie'' reste fondamentalement agnostique : il présente deux hypothèses, il ne tranche pas entre elles, et l’imagination y a au moins autant de part que la raison<ref>Chrétien, ''op. cit.'', p. 32-36.</ref>. Socrate lui-même conclut prudemment : <blockquote>aucun mal ne peut toucher un homme de bien ni pendant sa vie ni après sa mort, et les dieux ne se désintéressent pas de son sort. (41d)</blockquote> Cette conclusion n’affirme pas dogmatiquement une survie, mais énonce une foi pratique : quoi qu’il arrive, le juste n’a rien à craindre. L’''Apologie'', contrairement au ''Phédon'', ne construit pas de doctrine positive sur l’immortalité ; elle se tient au seuil d’une telle doctrine, dans un agnosticisme serein. ===== Le testament (41e-42a) ===== Socrate clôt son discours par un testament pour ses fils. Il ne demande qu’une chose aux Athéniens : <blockquote>Quand mes fils seront grands, punissez-les, citoyens, en les tourmentant comme je vous tourmentais, pour peu qu’ils vous paraissent se soucier d’argent ou de n’importe quoi d’autre plus que de la vertu. Et, s’ils croient être quelque chose, alors qu’ils ne sont rien, adressez-leur le reproche que je vous adressais. (41e)</blockquote> La mission philosophique se transmet ainsi, comme un héritage inversé : Socrate demande à ses bourreaux de devenir eux-mêmes, envers ses enfants, ce qu’il était pour eux, des tourmenteurs par la vertu. C’est le pardon actif d’un homme qui refuse de laisser sa mort briser la chaîne de l’examen. Puis vient la dernière phrase, l’une des plus célèbres de la littérature philosophique : <blockquote>Mais voici déjà l’heure de partir, moi pour mourir et vous pour vivre. De mon sort ou du vôtre lequel est le meilleur ? La réponse reste incertaine pour tout le monde, sauf pour la divinité. (''plḕn hē tôi theôi'', 42a)</blockquote> Cette clôture ouvre, sur l’indécidable, l’agnosticisme ultime. Socrate ne sait pas, nul ne sait, lequel est le plus heureux, de lui qui va mourir ou de ses juges qui vont continuer à vivre. Seule la divinité le sait. L’''Apologie'' se ferme ainsi sur le mot même de la sagesse socratique : la reconnaissance de l’ignorance humaine, couplée à la confiance paisible en un ordre divin qui excède notre mesure. Rien n’est affirmé dogmatiquement ; tout se termine sur une interrogation qui n’attend pas de réponse humaine. C’est une fin d’une sobriété admirable, qui évite à la fois la plainte et la consolation artificielle. Elle est, par son rythme et par son contenu, digne d’une page d’évangile ou d’un chapitre des ''Pensées'' de Marc-Aurèle. == Concepts et thèmes majeurs == Une lecture suivie ne serait pas complète sans reprendre quelques-uns des grands thèmes qui courent à travers le texte et en font la portée philosophique durable. === La sagesse humaine : le savoir du non-savoir === Le concept central de l’''Apologie'' est celui de la sagesse humaine (''anthrōpínē sophía'', 20d). Socrate n’est pas savant au sens fort du terme, c’est-à-dire à la manière des sophistes qui prétendaient posséder un savoir sur les choses divines, sur la nature, sur la politique, sur la vertu. Mais il possède une sagesse proprement humaine, qui consiste à reconnaître que l’on ne sait pas. Ce savoir du non-savoir n’est pas un scepticisme, encore moins un renoncement. C’est une position éthique : celui qui sait qu’il ne sait pas est disposé à chercher, à interroger, à examiner ; celui qui croit savoir est fermé à toute remise en question et, par là même, incapable de tout progrès. Cette sagesse n’est pourtant pas purement négative. Socrate affirme savoir au moins deux choses : qu’il est mauvais et laid de commettre l’injustice et de désobéir à un meilleur que soi (29b), et que la vie non examinée n’est pas digne d’être vécue (38a). De ces deux « savoirs » découle tout le comportement de Socrate au procès : il ne peut trahir la justice pour sauver sa vie, et il ne peut cesser d’examiner les autres sans trahir sa vocation propre. On voit donc que le savoir du non-savoir n’est pas une position d’ignorance totale, mais une structure articulée : une ignorance avouée sur les grandes questions métaphysiques, et une certitude pratique sur les exigences éthiques. Cette combinaison fait du socratisme une forme de philosophie pratique : elle rend possible l’action juste sans requérir une science achevée. Il faut enfin noter que le savoir du non-savoir est peut-être la thèse la plus féconde du socratisme pour l’histoire de la pensée. Toute la philosophie postérieure, chaque fois qu’elle commence par un doute méthodique (Descartes), par une critique des prétentions de la raison (Kant), par une déconstruction des évidences (phénoménologie), s’inscrit dans le sillage de l’interrogation socratique. La philosophie moderne est, en ce sens, socratique par son geste inaugural, même quand elle ne l’est plus par ses conclusions. === La méthode de l’elenchus === L’''Apologie'' donne à voir, en acte, la méthode philosophique propre de Socrate : l’elenchus ou réfutation par interrogation. Elle apparaît à plusieurs reprises, mais surtout dans l’interrogatoire de Mélétos (24b-28a). Son fonctionnement est simple dans son principe : Socrate ne pose pas directement ses propres thèses ; il prend pour point de départ celles de son interlocuteur, puis, par une série de questions dont chacune requiert une réponse qui semble évidente, il conduit l’interlocuteur à reconnaître des conséquences incompatibles avec d’autres thèses qu’il tient également pour vraies. La contradiction ainsi mise au jour n’est pas celle de Socrate, mais celle de l’interlocuteur avec lui-même. Cette méthode a plusieurs vertus philosophiques. D’abord, elle respecte la liberté de l’interlocuteur : Socrate ne lui impose rien, il l’amène seulement à voir ce qu’il pensait déjà. Ensuite, elle produit un savoir négatif sûr : la réfutation, à défaut de démontrer le vrai, prouve au moins que la thèse examinée est fausse. Enfin, elle a un effet moral : elle introduit chez l’interlocuteur l’expérience de l’''aporía'' (la perplexité), qui est le point de départ possible d’une recherche véritable. L’interlocuteur, déchargé de son illusion de savoir, peut commencer à apprendre. L’elenchus est ainsi, au sens propre, maïeutique : il fait accoucher les esprits. Mais l’elenchus a aussi ses limites, que l’''Apologie'' laisse apercevoir. Il peut blesser l’amour-propre ; il peut créer des haines durables ; il peut donner à ceux qui en sont la cible l’impression d’être humiliés publiquement. Socrate lui-même en témoigne : son enquête a suscité contre lui des rancœurs innombrables. La philosophie a un coût social, que l’elenchus rend visible avec une particulière acuité. === Le souci de l’âme === Le message moral central de l’''Apologie'' tient dans une exhortation : il faut se soucier prioritairement de son âme (''psuchḗ'') et de son amélioration, non de son corps, de sa richesse ou de sa réputation (29d-30b). <blockquote>La vertu ne naît pas de l’argent, mais c’est de la vertu que naissent et l’argent et tout le reste des biens utiles aux hommes, aussi bien privés que publics. (30b)</blockquote> Ce renversement est l’une des sources principales de la morale occidentale : la hiérarchie des biens est réordonnée autour du bien intérieur, et la richesse extérieure n’a de valeur qu’en tant qu’elle découle d’une vertu préalable. Ce souci de soi (''epiméleia heautoû'') n’est pas égoïste. Il est au contraire la condition du souci des autres : on ne peut aider autrui à améliorer son âme sans avoir travaillé à la sienne. Socrate harcèle ses concitoyens parce qu’il veut qu’ils prennent soin de leur âme, non parce qu’il veut sauver la sienne à ses dépens. Et c’est précisément parce qu’il se soucie de la cité qu’il la secoue : le taon pique le cheval pour le réveiller, non pour le faire souffrir. Cette thèse a eu une postérité immense. Elle est reprise, transformée, intériorisée par les écoles hellénistiques (stoïciens, épicuriens), qui en font le cœur de la sagesse pratique. Elle passe ensuite dans le christianisme, où le « soin de l’âme » devient le salut personnel. À l’époque moderne, Michel Foucault lui a consacré une part importante de ses derniers cours, voyant dans le souci de soi une alternative à l’éthique cartésienne fondée sur la seule connaissance de soi<ref>Michel Foucault, ''Le Souci de soi'' (''Histoire de la sexualité'', t. III), Paris, Gallimard, 1984 ; et ''L’Herméneutique du sujet'', ''op. cit.''</ref>. L’''Apologie'' est à l’origine de cette longue tradition, même si le souci de soi socratique reste, par certains aspects, très différent des élaborations postérieures : il est moins un travail sur soi qu’un examen de soi par la discussion avec autrui. === La philosophie comme mission divine === L’''Apologie'' fonde la légitimité de la philosophie sur une mission divine. Socrate ne philosophe pas par goût ou par choix : il le fait parce que le dieu lui en a fait l’ordre, à travers l’oracle de Delphes et à travers le démon. Cette dimension religieuse est essentielle pour comprendre la posture socratique. S’il pouvait cesser d’interroger, il le ferait peut-être (c’est une activité ingrate et dangereuse) ; mais il ne le peut pas, car ce serait désobéir au dieu. La philosophie est ainsi un service sacré, équivalent à celui des prêtres ou des devins, mais accompli par d’autres moyens : non par le rite, mais par l’examen rationnel. Cette dimension place Socrate dans une position paradoxale par rapport aux accusations d’impiété. L’homme que l’on accuse de ne pas croire aux dieux est en réalité celui qui les sert le plus fidèlement, au point de mourir pour leur obéir. Platon retourne ainsi l’accusation : les véritables impies sont ceux qui, en condamnant Socrate, refusent le présent que le dieu leur a fait. Le procès apparaît alors sous un jour inversé : non plus un acte de piété de la cité contre un impie, mais un acte d’impiété de la cité contre un serviteur du dieu. Ce thème a eu un écho particulier dans la tradition chrétienne, qui a parfois vu en Socrate une figure prophétique du Christ : un juste mis à mort par une communauté religieuse qui croyait servir ses dieux en le tuant. Justin martyr, au IIᵉ siècle, comparera explicitement Socrate et Jésus, voyant dans le philosophe athénien une préfiguration providentielle de la Passion<ref>Justin de Naplouse, ''Première Apologie'', 46, 1-4 : les chrétiens considèrent comme chrétiens avant la lettre « ceux qui ont vécu avec le Logos [...] parmi les Grecs, Socrate, Héraclite et ceux qui leur furent semblables ». Voir aussi ''Seconde Apologie'', 10, 5-6.</ref>. La philosophie chrétienne primitive a ainsi trouvé dans l’''Apologie'' une matrice pour penser la martyrologie. === La justice supérieure === Le thème de la justice traverse tout le texte. Socrate se présente comme un homme profondément respectueux des lois : il a risqué sa vie pour le respect de la procédure sous la démocratie (affaire des Arginuses) ; il mourra par fidélité aux lois dans le ''Criton'' plutôt que de s’évader. Mais son obéissance aux lois n’est pas inconditionnelle : il y a une justice supérieure, fondée sur des valeurs, qui commande dans certains cas la désobéissance civile, comme lorsqu’il refuse d’exécuter les ordres des Trente concernant Léon de Salamine. Cette justice supérieure n’est pas un simple idéal abstrait ; elle est, pour Socrate, ce qui fait le prix (''axía'') de la vie humaine. Elle a une origine divine et s’impose à la conscience au-delà des conventions sociales. On voit déjà poindre ici, avant les développements platoniciens de la ''République'', l’idée d’une justice en soi, distincte de la justice légale, et qui fonde celle-ci sans s’y réduire. Antigone, chez Sophocle, invoquait déjà les « lois non écrites » des dieux contre les décrets humains<ref>Sophocle, ''Antigone'', v. 450-460.</ref> ; Socrate, à sa manière, s’inscrit dans cette tradition. Mais il la rationalise : ce n’est plus le simple respect d’une tradition familiale ou religieuse, c’est la fidélité à un ordre supérieur accessible par l’examen rationnel. La philosophie devient ainsi le lieu où se manifeste cette justice transcendante, dont les lois humaines ne sont qu’une approximation imparfaite. Cette double fidélité (aux lois de la cité et à une justice supérieure) est source d’une tension qui traversera toute la tradition philosophique et juridique occidentale. Elle est au cœur des doctrines du droit naturel<ref>Voir notamment Cicéron, ''De republica'', III, 33 sur la ''lex vera'' ; Thomas d’Aquin, ''Somme théologique'', I-II, q. 94 sur la loi naturelle.</ref>, des théories modernes de la désobéissance civile (Thoreau, Gandhi, Martin Luther King), et des débats contemporains sur la légitimité du droit positif. === La mort et le courage === Le rapport de Socrate à la mort est une pièce maîtresse du texte. Sa thèse est à double face. D’un côté, la mort en elle-même est inconnue : nul ne sait si elle est un mal ou un bien, et la craindre comme un mal certain est la plus répréhensible des ignorances. De l’autre, il y a pire que la mort : la lâcheté, l’injustice, l’abandon de son poste. Le courage socratique n’est donc pas, comme celui des héros homériques, la volonté exaltée de mourir pour l’honneur ; c’est la lucidité sur ce qui est réellement à craindre, non la mort mais le vice. Ce renversement a nourri toute la morale stoïcienne (qui en fait un de ses principes cardinaux : ne rien craindre de ce qui ne dépend pas de nous, donc pas la mort) puis, par des voies détournées, la pensée chrétienne du martyre. Il faut insister sur la finesse de l’analyse socratique. Elle ne dit pas : « la mort est un bien » (affirmation dogmatique contraire à son savoir du non-savoir). Elle ne dit pas non plus : « la mort est indifférente » (comme le diront plus tard les stoïciens). Elle dit : « la mort est inconnue, donc je ne peux la craindre comme un mal certain ; mais l’injustice est un mal certain, donc je peux la craindre ». Le courage n’est pas fondé sur une espérance métaphysique, mais sur une hiérarchie des savoirs : le connu prime sur l’inconnu, et j’organise ma conduite en fonction de ce que je sais. Cette analyse aura un long destin. Épicure la reprendra pour dire : « la mort n’est rien pour nous »<ref>Épicure, ''Lettre à Ménécée'', § 125.</ref>. Les stoïciens la transformeront en doctrine de l’indifférence aux choses externes. Montaigne en fera un objet central de ses ''Essais''<ref>Montaigne, ''Essais'', I, 20 « Que philosopher, c’est apprendre à mourir » ; III, 12 « De la physionomie ».</ref>. Heidegger, au XXᵉ siècle, retournera au Socrate de l’''Apologie'' en interrogeant le rapport authentique à la mort comme condition d’une existence propre<ref>Martin Heidegger, ''Sein und Zeit'' (1927), § 46-53, sur l’''être-pour-la-mort''.</ref>. Chaque fois, c’est le même geste inaugural qui est repris, celui d’une mort désarmée par la pensée. === Philosophie et cité : l’autre politique === L’''Apologie'' esquisse une conception originale de la politique. Socrate se déclare non-politique au sens courant du terme : il n’a pas fréquenté l’assemblée, il n’a pas cherché les magistratures, il n’a pas fait carrière publique. Mais il se présente comme le plus politique des Athéniens au sens profond : il s’est occupé de la cité elle-même (plus que de ses affaires), en se souciant du perfectionnement de ses concitoyens. C’est une autre politique, qui ne passe pas par les institutions officielles (corrompues selon lui par la démagogie et l’ignorance) mais par la conversation privée, par l’interpellation personnelle, par la formation morale. Cette vision a un versant critique radical vis-à-vis de la démocratie athénienne, qui émerge des épisodes des Arginuses et de Léon de Salamine : la démocratie, livrée à ses passions, peut se conduire de manière aussi injuste qu’une tyrannie. Il serait naïf de voir en Socrate un démocrate simple ou un anti-démocrate simple ; il est un critique des deux régimes en tant qu’ils s’éloignent de la justice. Mais sa critique vise, au-delà des régimes, la capacité même des collectivités humaines à délibérer justement : « il n’y a personne au monde qui puisse garder la vie sauve s’il s’oppose loyalement à vous ou à toute autre collectivité » (32a). Ce diagnostic, désabusé, n’est pas tant politique qu’anthropologique : les foules, quelles qu’elles soient, résistent mal à l’examen rationnel. Mais cette critique a aussi un versant constructif. La philosophie, par l’examen qu’elle exerce sur les esprits, prépare la possibilité d’une politique véritablement juste. Platon développera cette intuition dans la ''République'', en allant jusqu’à imaginer une cité où les philosophes seraient rois, mais ce développement excède le cadre de l’''Apologie''. Au moment où Platon écrit ce texte, la cité idéale n’est pas encore pensée ; il y a seulement la dénonciation d’une cité qui a tué son meilleur citoyen, et la promesse implicite d’une pensée qui prolongera la mission interrompue. === L’ironie socratique === Un mot enfin sur l’ironie, qui est omniprésente dans le texte, et qu’il faut distinguer en plusieurs registres. Il y a d’abord l’ironie au sens étroit : dire le contraire de ce que l’on pense, comme lorsque Socrate qualifie Mélétos de « bon citoyen » et « patriote ». Il y a ensuite l’ironie dialectique : amener l’interlocuteur à se contredire lui-même par des questions prétendument naïves, alors que Socrate sait parfaitement où il le mène. Il y a enfin l’ironie existentielle : vivre de telle façon que toute son existence est un démenti de ce qu’on attendrait d’un homme dans sa situation. La proposition d’être nourri au Prytanée relève de cette dernière : Socrate, condamné, se présente comme un bienfaiteur ; il retourne les rôles, fait du tribunal une scène tragicomique. Cette ironie n’est pas seulement un trait de style. Elle est l’expression d’une distance philosophique à l’égard des conventions et des évidences. Celui qui a compris que le savoir commun est illusoire, que la rhétorique est trompeuse, que les hiérarchies sociales reposent sur des malentendus, ne peut plus prendre au sérieux les rituels qui ordonnent la vie ordinaire. L’ironie socratique est le signe visible d’une conscience libre, qui refuse de se soumettre aux attentes. C’est pourquoi Søren Kierkegaard, au XIXᵉ siècle, en a fait dans sa thèse sur ''Le Concept d’ironie'' le trait caractéristique de la subjectivité philosophique naissante : avec Socrate, la conscience se sépare du monde, s’intériorise, devient sujet<ref name="kierkegaard">Søren Kierkegaard, ''Le Concept d’ironie constamment rapporté à Socrate'' (''Om Begrebet Ironi'', 1841), trad. P.-H. Tisseau et E.-M. Jacquet-Tisseau, dans ''Œuvres complètes'', t. II, Paris, Éditions de l’Orante, 1975.</ref>. L’ironie est ce mode de la subjectivité qui se pose en se distinguant de ce qui est. == Questions de lecture et postérité == === Le Socrate de l’''Apologie'' et le Socrate historique === Une question classique est de savoir dans quelle mesure l’''Apologie'' restitue fidèlement le plaidoyer effectivement prononcé par Socrate en 399. Les éléments de réponse sont partiels. D’une part, Platon, jeune témoin du procès (il avait environ vingt-huit ans), avait de puissantes raisons d’être fidèle : la mémoire des jurés qui liraient le texte était fraîche, et toute infidélité flagrante aurait nui à la thèse défensive. D’autre part, Xénophon, dans sa propre ''Apologie'', confirme certains points centraux (l’oracle de Delphes, le refus de préparer sa défense, le rôle du démon, l’attitude provocante devant le tribunal). Les convergences entre Platon et Xénophon, qui écrivent séparément, garantissent la réalité d’un noyau historique. Pour autant, l’''Apologie'' de Platon est une œuvre écrite, composée probablement plusieurs années après le procès, et qui obéit aux lois de la composition littéraire. La structure en trois discours parfaitement articulée, la densité dialectique de l’interrogatoire de Mélétos, la beauté rythmique de certaines périodes (la finale : « moi pour mourir et vous pour vivre… ») relèvent de l’art platonicien. Le témoignage historique et la recréation littéraire ne sont pas dissociables. La question du « Socrate historique » a été débattue à l’infini. On distingue traditionnellement plusieurs Socrate : celui d’Aristophane (caricatural), celui de Xénophon (pragmatique, moraliste de bon sens), celui de Platon (dialectique et idéaliste), celui d’Aristote (prédécesseur des idées, attribuant à Socrate la recherche des définitions universelles et l’induction<ref>Aristote, ''Métaphysique'', XIII, 4, 1078b17-30 : « deux choses peuvent à bon droit être attribuées à Socrate : les raisonnements inductifs et la définition universelle ».</ref>). Lequel est le vrai ? La réponse la plus raisonnable est qu’aucun ne l’est entièrement, mais qu’ils donnent, collectivement, une image composite d’un homme dont la puissance personnelle a dépassé de beaucoup ce que les documents peuvent restituer. L’''Apologie'' est probablement, de tous les textes, celui qui se tient le plus près de la voix réellement entendue, parce que Platon y a choisi, exceptionnellement, de s’effacer devant son maître. Il est usuel, chez les commentateurs, de distinguer dans l’''Apologie'' des couches. Certains éléments semblent historiques au plus haut degré : le cadre procédural, les noms des accusateurs, l’attitude refusant la supplication, la référence à Chéréphon (mort avant le procès, mais dont les héritiers étaient vivants pour confirmer ou démentir), la condamnation et son déroulement. D’autres éléments sont vraisemblablement platoniciens : l’articulation en trois discours parfaitement équilibrés, la méditation finale sur la mort, peut-être la prophétie adressée aux juges condamnateurs. Mais tout cela relève d’appréciations délicates, et Platon lui-même dans la ''Lettre VII'' revendique le droit de penser philosophiquement à partir du Socrate qu’il a connu<ref>Platon, ''Lettre VII'', 324d-326b. Sur la question socratique, voir Louis-André Dorion, ''Socrate'', Paris, PUF, coll. « Que sais-je ? », 2004.</ref>. === La postérité de l’''Apologie'' === [[Fichier:Statues of Plato (left) and Socrates (right) by Leonidas Drosis at the Academy of Athens.jpg|vignette|droite|upright=1.2|Statues de Platon (à gauche) et de Socrate (à droite), par le sculpteur Leonidas Drosis (seconde moitié du XIX{{e}} siècle), à l’entrée de l’Académie d’Athènes. La réunion des deux figures sur le fronton d’une institution savante moderne témoigne de la postérité canonique que le procès aura contribué à instituer.]] L’''Apologie de Socrate'' est l’un des textes les plus lus, les plus imités, les plus commentés de la philosophie occidentale. Sa postérité ne se laisse pas résumer en quelques lignes, mais on peut en indiquer quelques étapes majeures. Dans l’Antiquité, l’''Apologie'' est immédiatement imitée : Xénophon en donne sa version ; des apologies perdues, d’Eschine de Sphettos ou de Lysias, avaient également circulé. Au IIᵉ siècle, Apulée compose une ''Apologie'' (sur son propre procès en magie) qui imite la structure platonicienne<ref>Apulée, ''Apologie ou De la magie'', texte établi et traduit par Paul Vallette, Paris, Les Belles Lettres, 1924.</ref>. Les écoles hellénistiques (stoïciens, cyniques) prennent Socrate comme figure tutélaire du sage qui meurt pour sa vérité : Épictète et Marc Aurèle se réfèrent constamment à lui<ref>Voir notamment Épictète, ''Entretiens'', II, 1, 32 ; II, 2, 8-20 ; et Marc Aurèle, ''Pensées'', VII, 19 ; XI, 25, 28.</ref>. Les cyniques voient en lui le philosophe de la pauvreté et de la provocation, exemple d’une existence libre des conventions. La tradition chrétienne primitive trouve dans Socrate une figure prophétique. Justin martyr, au IIᵉ siècle, fait de Socrate un « chrétien avant le Christ », guidé par le Logos divin<ref>Justin, ''Première Apologie'', 46 ; ''Seconde Apologie'', 10.</ref>. Les Pères de l’Église le mentionnent souvent, tantôt pour s’en réclamer (Clément d’Alexandrie, Origène), tantôt pour le mettre à distance (Tertullien)<ref>Clément d’Alexandrie, ''Stromates'', I, 14 ; Tertullien, ''Apologétique'', 46.</ref>. La mort de Socrate, rapprochée du martyre, fournit un modèle de témoignage pour la vérité. Toute la hagiographie martyrologique se nourrit, en partie, de l’''Apologie''. À la Renaissance, la redécouverte de Platon par Marsile Ficin et l’Académie florentine met l’''Apologie'' au centre du canon philosophique<ref>Marsile Ficin traduit les œuvres complètes de Platon en latin (''Platonis Opera Omnia'', Florence, 1484), rendant l’''Apologie'' accessible à l’Europe savante.</ref>. Montaigne, dans les ''Essais'', lui consacre plusieurs chapitres et voit en Socrate la figure même de la sagesse sans science, du bon sens humain qui vaut mieux que toutes les spéculations. <blockquote>Socrate fait mouvoir son âme d’un mouvement naturel et commun. Ainsi dit un paysan, ainsi dit une femme. (Montaigne, ''Essais'', III, 12)<ref>Montaigne, ''Essais'', III, 12 « De la physionomie », édition Villey-Saulnier, PUF, 1965, p. 1037-1038.</ref></blockquote> À l’époque des Lumières, l’''Apologie'' devient un manifeste anticlérical. Voltaire y voit le procès de l’intolérance religieuse, Diderot une défense de la libre pensée, Rousseau un modèle d’éthique civile. David peint ''La Mort de Socrate'' (1787)<ref>Jacques-Louis David, ''La Mort de Socrate'', 1787, huile sur toile, 129,5 × 196,2 cm, New York, Metropolitan Museum of Art.</ref>, tableau emblématique où le philosophe saisit la coupe avec sérénité tandis que ses disciples se lamentent : image iconique qui influencera durablement l’imaginaire philosophique. Au XIXᵉ siècle, Hegel, Kierkegaard et Nietzsche proposent trois lectures marquantes. Hegel, dans ses ''Leçons sur l’histoire de la philosophie'', voit Socrate comme le moment où la conscience morale s’intériorise, et dans sa condamnation le conflit tragique entre l’ancienne cité et la subjectivité naissante<ref>G. W. F. Hegel, ''Leçons sur l’histoire de la philosophie'', t. II, trad. P. Garniron, Paris, Vrin, 1971, p. 263-328.</ref>. Kierkegaard fait de l’ironie socratique, analysée dans ''Le Concept d’ironie'' (1841), le modèle de la subjectivité existentielle<ref name="kierkegaard" />. Nietzsche, dans ''La Naissance de la tragédie'' (1872), voit au contraire en Socrate le destructeur du tragique grec, l’homme théorique qui substitue la raison critique à la sagesse instinctive, et accomplit ainsi une « monstruosité par défaut »<ref name="nietzsche-nt" />. Les trois lectures sont opposées, mais elles attestent toutes la centralité de Socrate dans la pensée moderne. Au XXᵉ siècle, l’''Apologie'' continue d’être un lieu de lecture privilégié. Hannah Arendt, dans ''La Vie de l’esprit'', en fait le modèle de la pensée responsable<ref>Hannah Arendt, ''La Vie de l’esprit'', trad. L. Lotringer, Paris, PUF, 1981 ; voir aussi « Philosophie et politique », ''Les Cahiers de philosophie'', n° 4, 1987.</ref>. Michel Foucault, dans ses derniers cours au Collège de France (''Le Courage de la vérité'', 1984), y voit le texte fondateur de la ''parrhēsía'', c’est-à-dire du « franc-parler » philosophique qui engage la vie de celui qui parle<ref>Michel Foucault, ''Le Courage de la vérité. Le Gouvernement de soi et des autres II. Cours au Collège de France, 1983-1984'', éd. F. Gros, Paris, Gallimard/Seuil, 2009.</ref>. Leo Strauss et son école ont proposé une relecture « ésotérique » du texte, attentive à ce que Socrate ne dit pas et à ce qu’il suggère entre les lignes<ref>Leo Strauss, ''Studies in Platonic Political Philosophy'', Chicago, University of Chicago Press, 1983 ; ''The City and Man'', Chicago, Rand McNally, 1964.</ref>. Lire l’''Apologie'' aujourd’hui, c’est donc entrer dans un texte sédimenté par vingt-quatre siècles de commentaires, et qui n’a pas encore épuisé sa charge philosophique. Chaque époque y trouve un Socrate à sa mesure, et c’est peut-être la marque des grandes œuvres de pouvoir soutenir cette pluralité indéfinie d’interprétations. == Conclusion == L’''Apologie de Socrate'' n’est pas un plaidoyer ordinaire. Ce n’est même pas, à proprement parler, un plaidoyer au sens technique, puisque Socrate y refuse presque toutes les tactiques qui auraient pu le faire acquitter : il ne se fait pas écrire par un logographe, il ne supplie pas, il ne fait pas paraître ses enfants, il provoque le jury quand il lui faudrait le ménager. C’est une profession de foi philosophique, prononcée au moment où la vie de l’auteur est en jeu, et qui tire précisément de cet enjeu sa gravité unique. L’''Apologie'' est la preuve par la mort d’une thèse que Socrate n’a cessé de soutenir par les mots : la vertu vaut plus que la vie. Trois traits en font un texte fondateur. D’abord, il inaugure la figure du philosophe comme témoin : celui dont la cohérence entre la parole et la vie va jusqu’au sacrifice. Socrate meurt parce qu’il ne veut pas trahir ce qu’il a dit ; et Platon, en écrivant l’''Apologie'', fait de cette mort le sceau de la vérité philosophique. La philosophie, à partir de ce texte, n’est plus seulement une activité intellectuelle : elle est un engagement existentiel, et le philosophe n’est plus seulement celui qui sait, mais celui qui vit ce qu’il dit. Ensuite, le texte définit la philosophie elle-même, en l’opposant à la sophistique, à la rhétorique et à la religion populaire. Non un savoir constitué, mais un examen ; non une éloquence, mais une recherche du vrai ; non un rite, mais un service intérieur du divin. Cette triple opposition structure toute la pensée platonicienne ultérieure et, par voie de conséquence, la pensée occidentale. La philosophie, depuis Socrate, se définit comme ce qui n’est pas sophistique : un savoir désintéressé, inséparable d’une pratique de vérité. Enfin, le texte pose la question politique dans ses termes platoniciens : la cité peut-elle accueillir le philosophe ? La condamnation de Socrate se laisse lire comme une réponse négative qu’Athènes aurait donnée en acte à cette question, et c’est ainsi que Platon semble l’interpréter. L’œuvre de Platon tout entière tentera de penser une cité qui répondrait autrement, depuis les esquisses du ''Gorgias'' et de la ''République'' jusqu’à la construction ultime des ''Lois''. Le procès de Socrate est ainsi, indirectement, à l’origine de la philosophie politique occidentale. Lire l’''Apologie'' aujourd’hui, c’est s’exposer à une exigence intacte. La vie non examinée n’est pas digne d’être vécue ; la vertu vaut plus que l’argent, que la réputation, que la vie même ; la lâcheté est pire que la mort ; le juste préfère subir l’injustice plutôt que la commettre. Ces formules, qui paraissent extrêmes, sont le cœur d’une éthique dont la philosophie occidentale n’a cessé de se réclamer, même quand elle croyait s’en affranchir. Le taon, vingt-quatre siècles plus tard, pique toujours. == Notes et références == {{Références}} == Bibliographie sélective == === Éditions et traductions de l’''Apologie'' === * Platon, ''Apologie de Socrate. Criton'', traduction, introduction et notes par Luc Brisson, Paris, GF-Flammarion, 2016. * Platon, ''Apologie de Socrate'', traduction par Luc Brisson, présentation, notes, dossier, répertoire et glossaire par Arnaud Macé, Paris, GF-Flammarion, 2017. * Platon, ''Apologie de Socrate'', traduction, présentation et notes de Bernard et Renée Piettre, Paris, Le Livre de Poche (Librairie générale française), coll. « Libretti », 1997. * Platon, ''Apologie de Socrate'', texte établi et traduit par Maurice Croiset, Paris, Les Belles Lettres, coll. des Universités de France, 1920 (nombreuses rééditions). * Platon, ''Œuvres complètes'', sous la direction de Luc Brisson, Paris, Flammarion, 2008 (contient l’''Apologie''). === Commentaires === * Claude Chrétien, ''Platon, Apologie de Socrate'', Paris, Hatier, coll. « Profil philosophie », 1993. * Paul Allen Miller, Charles Platter, ''Plato’s Apology of Socrates: A Commentary'', Norman, University of Oklahoma Press, 2010. * Émile de Strycker, S. R. Slings, ''Plato’s Apology of Socrates: A Literary and Philosophical Study with a Running Commentary'', édité et complété par S. R. Slings, Leyde, E. J. Brill, coll. « Mnemosyne Supplements » 137, 1994. * Thomas G. West, ''Plato’s Apology of Socrates: An Interpretation with a New Translation'', Ithaca, Cornell University Press, 1979. === Études sur Socrate et l’''Apologie'' === * Louis-André Dorion, ''Socrate'', Paris, PUF, coll. « Que sais-je ? », 2004. * Gregory Vlastos, ''Socrate : ironie et philosophie morale'', trad. C. Dalimier, Paris, Aubier, 1994 (''Socrates: Ironist and Moral Philosopher'', 1991). * Gregory Vlastos, « The Socratic Elenchus », ''Oxford Studies in Ancient Philosophy'', I, 1983, p. 27-58. * Debra Nails, ''The People of Plato: A Prosopography of Plato and Other Socratics'', Indianapolis, Hackett, 2002. * Gabriele Giannantoni, ''Socratis et Socraticorum Reliquiae'', 4 vol., Naples, Bibliopolis, 1990. * Monique Canto-Sperber (dir.), ''Les Paradoxes de la connaissance. Essais sur le Ménon de Platon'', Paris, Odile Jacob, 1991. === Réceptions modernes === * G. W. F. Hegel, ''Leçons sur l’histoire de la philosophie'', t. II, trad. P. Garniron, Paris, Vrin, 1971. * Søren Kierkegaard, ''Le Concept d’ironie constamment rapporté à Socrate'' (1841), trad. P.-H. Tisseau et E.-M. Jacquet-Tisseau, Paris, Éditions de l’Orante, 1975. * Friedrich Nietzsche, ''La Naissance de la tragédie'' (1872), dans ''Œuvres philosophiques complètes'', t. I, Paris, Gallimard, 1977. * E. R. Dodds, ''Les Grecs et l’irrationnel'' (1951), trad. M. Gibson, Paris, Flammarion, coll. « Champs », 1977. * Hannah Arendt, ''La Vie de l’esprit'', trad. L. Lotringer, Paris, PUF, 1981. * Michel Foucault, ''L’Herméneutique du sujet. Cours au Collège de France, 1981-1982'', éd. F. Gros, Paris, Gallimard/Seuil, 2001. * Michel Foucault, ''Le Courage de la vérité. Cours au Collège de France, 1983-1984'', éd. F. Gros, Paris, Gallimard/Seuil, 2009. * Leo Strauss, ''Studies in Platonic Political Philosophy'', Chicago, University of Chicago Press, 1983. === Sources antiques === * Aristophane, ''Les Nuées'', dans ''Théâtre complet'', t. I, trad. V.-H. Debidour, Paris, Gallimard, coll. « Folio », 1965. * Xénophon, ''Apologie de Socrate'' et ''Mémorables'', trad. P. Chambry, Paris, Garnier-Flammarion, 1967. * Diogène Laërce, ''Vies et doctrines des philosophes illustres'', trad. sous la direction de M.-O. Goulet-Cazé, Paris, Le Livre de Poche, coll. « La Pochothèque », 1999. * Plutarque, ''Du démon de Socrate'', dans ''Œuvres morales'', t. VIII, trad. J. Hani, Paris, Les Belles Lettres, 1980. * Aristote, ''Rhétorique'', trad. M. Dufour et A. Wartelle, Paris, Les Belles Lettres, 1938-1973. * Cicéron, ''Tusculanes'', trad. J. Humbert, Paris, Les Belles Lettres, 1931. {{AutoCat}} eq0ajzr1psamxp4bma5x5f4cs33v6u0 Modèle:Sous-pages 10 83825 763999 2026-04-19T14:05:20Z PandaMystique 119061 Page créée avec « <includeonly> {{#ifeq:{{{haut de page|}}}|non||{{haut de page|livre={{{1|{{BOOKNAME}}}}}|{{#ifexist:{{{1|{{BOOKNAME}}}}}/Sommaire|{{{1|{{BOOKNAME}}}}}/Sommaire|{{BOOKNAME}}}}|image={{{image|{{#ifeq:{{{image|{{{2|}}}}}}|-||{{{2|}}}}}}}}}}}} {{#ifeq:{{{bas de page|}}}|non||{{bas de page|{{#ifexist:{{{1|{{BOOKNAME}}}}}/Sommaire|{{{1|{{BOOKNAME}}}}}/Sommaire|{{BOOKNAME}}}}}}}}</includeonly> » 763999 wikitext text/x-wiki <includeonly> {{#ifeq:{{{haut de page|}}}|non||{{haut de page|livre={{{1|{{BOOKNAME}}}}}|{{#ifexist:{{{1|{{BOOKNAME}}}}}/Sommaire|{{{1|{{BOOKNAME}}}}}/Sommaire|{{BOOKNAME}}}}|image={{{image|{{#ifeq:{{{image|{{{2|}}}}}}|-||{{{2|}}}}}}}}}}}} {{#ifeq:{{{bas de page|}}}|non||{{bas de page|{{#ifexist:{{{1|{{BOOKNAME}}}}}/Sommaire|{{{1|{{BOOKNAME}}}}}/Sommaire|{{BOOKNAME}}}}}}}}</includeonly> qphx7f1q2s28pvtw67dzr3k9h2rom2s