http://f-cpu.seul.org/whygee/parinux/conf_yg.html, écrit par Yann Guidon <whygee@f-cpu.org> pour la conférence à Parinux du jeudi 13 juin 2002.

 
De F-CPU à FC0,
    de la spécification à l'architecture    

 

 

Cette partie de la présentation a pour but de faire mieux comprendre comment les aspects logiciels et matériels sont liés, et comment on passe progressivement d'un objet abstrait à une structure implémentable par une petite équipe.

 

 


 

Rappels sur F-CPU et son ISA

Format des données :


Un registre peut contenir n'importe quel type de valeur :


 

Exemples :

L'objectif étant de simplifier à la fois le code (écriture unique et indépendante de l'architecture) et l'architecture des machines, puisqu'il faut juste modifier un paramètre (largeur des registres) pour "booster" les applications "numériques".


 

Extensions du chunk :

Si un CPU implémente des formats en virgule flottante sur 128 bits ou des formats scalaires plus larges que 64 bits, la taille du chunk augmentera pour atteindre cette taille. La taille du registre est une puissance de 2 de celle du chunk. Ainsi, les registres peuvent être plusieurs ordres de grandeur plus grand que la donnée entière la plus grande possible.
 

Format des instructions :

L'opcode d'un côté, la destination de l'autre, le reste au milieu. C'est un peu différent des autres RISC canoniques mais c'est très pratique.


 

Champs d'instruction :

Le but étant bien sûr de réduire la complexité du décodage, même si dans la pratique ce n'est pas toujours atteint pour des raisons cachées, mais c'est un bon début.


 

Flags : selon les instructions, certains flags ont un sens ou un autre, par exemple :

Si un CPU RISC peut avoir parfois plus d'instructions qu'un CPU CISC c'est par la combinaison de différentes options orthogonales. F-CPU définit des opcodes et de nombreuses versions d'une instruction sont possibles en fonction des flags.


 

3 champs pour spécifier les registres et des fonctions dédiées :

Pour comprendre à la fois l'architecture et les sources en assembleur, il faut garder cette structure en tête. Il y a rarement des exceptions car sinon le processeur deviendrait vite compliqué.

Il resterait bien 6 bits pour coder un autre champ mais il n'y aurait plus rien pour les flags, et de toute façon on a déjà suffisamment de champs comme ça.


Jusqu'à 4 registres utilisés par instruction :

Les instructions STORE et LOAD ont été déterminantes pour faire accepter l'extension du format rigide des instructions.

Les instructions "RISC" initiales/classiques ont vite été étendues par certaines autres architectures comme PA-RISC pour augmenter la densité de codage et favoriser le parallélisme des unités. Ici on est cependant limités à 4 valeurs de registres à cause de leur nombre. Et le banc de registres est déjà suffisamment gros comme ça...


=======> 3 ports en lecture et 2 ports en écriture

Le numéro du 4ème registre (en écriture) est

OP1 est une exception mais nécessaire pour garder la cohérence du jeu d'instructions.
De toute façon, OP0 est toujours lu (spéculativement, et ça sert...)


Pour résumer, l'adresse d'écriture vient de :

De toute façon, la complexité du codage de l'adresse d'écriture est moins cruciale que pour l'adresse de lecture...

 

 

Synoptique général :

 

Cette présentation aborde les deux caractéristiques principales de FC0 :

 


 

Le pipeline d'exécution

Types d'opérations

Nous allons d'abord parler des instructions qui agissent sur les données, les instructions "numériques". Les opérations de contrôle sont traitées ensuite ou avec de petites exceptions au scheduling en fonction des besoins.

Pour commencer à faire un manuel de programmation, il faut déterminer les instructions, les opcodes, les flags, les formats, etc.

Donc il faut pouvoir dire si l'opération est possible/réalisable ou pas, donc concevoir l'unité correspondant à l'opération.

Pour aller vite, il faut des unités très spécialisées, pour être plus petites et rapides. Une grosse ALU aurait probablement été un frein. Les CPU RISC simples (MIPS R3000) sont souvent centrées autour d'un additionneur "à tout faire".

Chaque unité d'exécution a une fonction très spécifique. On essaie aussi, comme avec les RISC classiques, de réduire la duplication des fonctions et de réutiliser celles qui existent, pouvant donner une autre fonction avec un impact réduit. La fonction est liée à la structure, chaque unité a donc une organisation différente et adaptée. Ces unités spécialisées permettent aussi d'étendre les fonctions disponibles. En se laissant guider ou inspirer par la structure d'une unité, on peut imaginer d'autres instructions génériques très utiles pour de nombreux types de codes (compression, DSP, crypto, ou n'importe quel code un peu bizarre). Evidemment ce n'est pas accessible au niveau du langage C mais on ne va pas laisser les vieux langages nous empêcher d'aller vite... surtout que l'assembleur F-CPU n'est pas si difficile que ça, si vous avez déjà programmé avec Intel...

 

Contrainte de complexité et de fréquence d'horloge

La contrainte principale est la fréquence d'horloge : une unité complexe ralentit le processeur entier.
La règle de conception du processeur est simple : Pour donner un exemple de la complexité d'un étage, voici un octet de l'unité ROP2 :

 

Le terme "superpipeline" vient du fait que la réduction de la complexité des étages augmente mécaniquement la profondeur du pipeline.

En plus d'augmenter la fréquence d'horloge, cette stratégie a un effet bénéfique sur la conception du processeur : toutes les parties ont une complexité bornée, la différence de vitesse est réduite et on a peu de mauvaises surprises puisque la complexité est définie/imposée à l'avance, donc moins de chances de CDP cachés.

 

Par contre, avec si peu de portes, on ne peut pas faire grand-chose en un cycle (le principe est même d'en faire le minimum).

 

 

    MOINS ON EN FAIT    
PLUS CA VA VITE

 

 

Pour ROP2, ça va, mais pour les unités plus complexes, il faut plusieurs cycles :

 

Certaines unités ont même une latence qui dépend de la fonction exécutée, des options ou de la taille des données. Cela peut être déterminé au moment du décodage donc le scheduling "statique" est utilisé.

 

Pour que FC0 fonctionne, la latence de l'opération doit être déterministe

 

Le fait que les unités aient des latences différentes suppose que plusieurs unités délivrent leur résultat en même temps : le scheduling statique permet au décodeur de détecter et d'éviter les cas où plus de 2 données sont écrites dans les registres.

Le Xbar permet de faire un "bypass" du banc de registre et prend un cycle en lecture et en écriture.

 

Règles d'ordonnancement des instructions :

Toutes les instructions commencent avec la même séquence :
Les instructions partagent le pipeline de fetch-decode-Xbar.

 

Selon le type d'opération, l'instruction nécessite plus ou moins de cycles. La plupart des instructions finissent par écrire dans un registre, nous pouvons donc montrer les exemples suivants :

 

 

La latence des instructions détermine l'attente en cas de dépendance de registre :

 

 

Le schéma de principe du pipeline de FC0 est résumé ici :

 

 

Afin de garantir qu'aucune collision n'aura lieu à l'écriture des registres, le décodeur consulte une FIFO qui contient les informations sur l'état du pipeline.

 

 

Puisque les instructions sont validées dans l'ordre naturel du programme
mais peuvent se terminer dans le désordre, on appelle ce type de pipeline "OOOC" :
Out Of Order Completion
("Terminaison dans le désordre")

 

 

En plus de commander la logique de routage du chemin de données, le scheduler sert aussi de "scoreboard" puisqu'il suffit de consulter son contenu pour savoir si un registre est valide et quand il le sera.

 

En l'absence de conflits ou de dépendances de registres (si le programme est correctement "schedulé"), F-CPU peut exécuter 1 instruction par cycle en crête. Cependant pour remplir cette condition, il faut recourir à des techniques modernes comme l'entrelacement des flots d'instructions ou le déroulage des boucles (x2). Dans la plupart des cas, ce sont des techniques utilisées pour les processeurs superscalaires à 2 ou 3 voies.

 

 


 

Le système mémoire

Notion de "flags de propriété d'un registre"

L'une des premières fonctionnalités qui a démarqué F-CPU des autres architectures est l'utilisation intensive de "flags spéculatifs" qui permettent de connaitre une condition sans avoir à la calculer explicitement.

Pour un saut conditionnel ou un move conditionnel, une architecture RISC/MIPS va calculer la condition en passant par l'additionneur (émulé par SUB R1,R0, Rx) puis utilise la retenue pour valider l'opération. Cela réutilise les unités existantes mais accroit le délai de décision.

FC0 utilise, pour déterminer les conditions, des bits supplémentaires du banc de registre. LSB et MSB sont simples, zero est plus compliqué mais reste toujours cohérent avec le contenu du registre, même lors d'une exception ou un changement de tâche. Le décodage peut donc connaitre en peu d'efforts certaines "propriétés" d'un registre et décider de l'exécution de l'instruction associée (cmove, jmp, idiv)

Extension du principe aux flots de données et d'instruction

FC0 utilise ce principe et l'étend pour pouvoir traiter les sauts et les load/store aussi facilement.

Des flags supplémentaires sont ajoutés pour dire si le registre désigné par OP1 contient un pointeur vérifié ET valide, ainsi que des informations sur comment trouver la donnée ou l'instruction dans le CPU.

De manière plus générale, FC0 utilise une "mémoire associative" pour "associer" un numéro de registre à un pointeur.

 

Structure de la LSU et du Fetcher

 

 

 

 

 

Scheduling des instructions Load et Store :

Les instructions d'accès à la mémoire utilisent le même pipeline que les autres instructions pour mettre à jour les pointeurs, mais ils utilisent des chemins parallèles pour accéder à la mémoire :

 

En "sortant" le chemin de données correspondant à la mémoire (instructions et données), on règle deux problèmes :

Il s'en suit :  

Rupture du flot d'instructions :

 

l'instruction jmp sert pour goto, call et return, mais fonctionne selon le même principe que Load et Store dans tous les cas :

Le scheduling est assez simple :

 

 

Il faut toutefois remarquer qu'à cause de la très faible complexité des étages du pipeline, on ne peut pas savoir en moins d'un cycle si le saut peut avoir lieu. On "perd" donc un cycle en cas de saut :

 

 

Les flags de Stream Hint sont utilisés pour définir la condition du saut, mais il reste 2 bits pour la prédiction statique du saut, ça servira plus tard...

L'instruction de boucle utilise une version spéciale de jmp

 

          (début)
          ...
          loopentry r2; // place PC+4 dans R2
                        // et le "tagge" comme entrée de boucle
             ...
             (corps de la boucle)
             ...
          loop r3, r2; // si r3 n'est pas à zéro, saute à [r2]
                       // décrémente r3 dans tous les cas.

          ...
          (fin)

 

 


 

Conclusion

 

    F-CPU est un lourd projet et tout ne peut pas être dit ici mais...

 
 

Et il ne faut pas oublier ...

Il est libre...

 

 

 

    F-CPU C'EST BON    
MANGEZ-EN !