En 32 bits, nous avions 8 registres "d'usage général". Avec le 64bit, la quantité double, mais cela semble indépendant du changement de 64bit lui-même.
Maintenant, si les registres sont si rapides (pas d'accès à la mémoire), pourquoi n'y en a-t-il pas plus naturellement ? Les constructeurs de CPU ne devraient-ils pas intégrer autant de registres que possible dans le CPU ? Quelle est la restriction logique à la raison pour laquelle nous n'avons que la quantité que nous avons ?
Réponses
Trop de publicités?Il y a de nombreuses raisons pour lesquelles vous ne disposez pas d'un grand nombre de registres :
- Ils sont fortement liés à la plupart des étapes du pipeline. Pour commencer, vous devez suivre leur durée de vie et faire remonter les résultats aux étapes précédentes. La complexité devient très vite insurmontable, et le nombre de fils (littéralement) impliqués augmente au même rythme. C'est coûteux en surface, ce qui signifie en fin de compte que c'est coûteux en énergie, en prix et en performances à partir d'un certain point.
- Il occupe de l'espace pour l'encodage des instructions. 16 registres occupent 4 bits pour la source et la destination, et 4 autres si vous avez des instructions à 3 opérandes (par exemple ARM). Cela représente une quantité considérable d'espace d'encodage d'instructions, juste pour spécifier le registre. Cela finit par avoir un impact sur le décodage, la taille du code et à nouveau la complexité.
- Il y a de meilleures façons d'obtenir le même résultat...
De nos jours, nous avons vraiment beaucoup de registres - ils ne sont simplement pas programmés explicitement. Nous avons le "renommage de registre". Alors que vous n'accédez qu'à un petit ensemble (8-32 registres), ils sont en fait soutenus par un ensemble beaucoup plus grand (par exemple 64-256). Le CPU suit alors la visibilité de chaque registre, et les alloue à l'ensemble renommé. Par exemple, vous pouvez charger, modifier, puis stocker dans un registre plusieurs fois de suite, et faire en sorte que chacune de ces opérations soit effectuée indépendamment en fonction des manques de cache, etc. En ARM :
ldr r0, [r4]
add r0, r0, #1
str r0, [r4]
ldr r0, [r5]
add r0, r0, #1
str r0, [r5]
Les cœurs Cortex A9 renomment les registres, donc le premier chargement de "r0" va en fait dans un registre virtuel renommé - appelons-le "v0". Le chargement, l'incrémentation et le stockage se font sur "v0". Pendant ce temps, nous effectuons également un chargement/modification/stockage dans r0, mais il sera renommé "v1" car il s'agit d'une séquence entièrement indépendante utilisant r0. Disons que le chargement à partir du pointeur dans "r4" s'est arrêté à cause d'un manque de cache. Ce n'est pas grave - nous n'avons pas besoin d'attendre que "r0" soit prêt. Comme il a été renommé, nous pouvons exécuter la séquence suivante avec "v1" (également mappé sur r0) - et peut-être que c'est un hit de cache et que nous venons de gagner énormément en performance.
ldr v0, [v2]
add v0, v0, #1
str v0, [v2]
ldr v1, [v3]
add v1, v1, #1
str v1, [v3]
Je pense que le x86 a atteint un nombre gigantesque de registres renommés ces jours-ci (environ 256). Cela signifierait avoir 8 bits multipliés par 2 pour chaque instruction, juste pour indiquer la source et la destination. Cela augmenterait massivement le nombre de fils nécessaires dans le noyau, ainsi que sa taille. La plupart des concepteurs se sont donc contentés d'un nombre de registres compris entre 16 et 32, et pour les conceptions de processeurs hors-ordre, le renommage des registres est le moyen d'atténuer ce problème.
Modifier : L'importance de l'exécution hors ordre et du renommage des registres sur ce point. Une fois que vous avez OOO, le nombre de registres n'a pas tellement d'importance, parce qu'ils sont juste des "étiquettes temporaires" et sont renommés dans le jeu de registres virtuels beaucoup plus grand. Il ne faut pas que le nombre de registres soit trop petit, car il devient difficile d'écrire de petites séquences de code. C'est un problème pour x86-32, parce que les 8 registres limités signifient que beaucoup de temporaires finissent par passer par la pile, et le noyau a besoin d'une logique supplémentaire pour transmettre les lectures/écritures à la mémoire. Si vous n'avez pas de OOO, vous parlez généralement d'un petit noyau, auquel cas un grand jeu de registres est un faible avantage en termes de coût/performance.
Il existe donc un point idéal pour la taille des banques de registres, qui plafonne à environ 32 registres architecturés pour la plupart des classes de CPU. x86-32 a 8 registres et c'est définitivement trop petit. ARM a opté pour 16 registres et c'est un bon compromis. 32 registres, c'est un peu trop - on finit par ne plus avoir besoin des 10 derniers registres environ.
Rien de tout cela ne concerne les registres supplémentaires que vous obtenez pour SSE et d'autres coprocesseurs vectoriels à virgule flottante. Ces registres ont un sens en tant qu'ensemble supplémentaire car ils fonctionnent indépendamment du noyau entier et ne font pas croître la complexité du processeur de manière exponentielle.
Nous Faites En avoir plus
Étant donné que presque chaque instruction doit sélectionner 1, 2 ou 3 registres architecturalement visibles, l'augmentation de leur nombre augmenterait la taille du code de plusieurs bits pour chaque instruction et réduirait ainsi la densité du code. Cela augmente également la quantité de [contexte](http://en.wikipedia.org/wiki/Context%28computing%29) qui doivent être sauvegardés en tant qu'état du fil, et partiellement sauvegardés dans l'état d'une fonction. fiche d'activation ._ Ces opérations sont fréquentes. Les interlocks des pipelines doivent vérifier un tableau d'affichage pour chaque registre et cela a une complexité quadratique en temps et en espace. Et peut-être que la plus grande raison est simplement la compatibilité avec le jeu d'instructions déjà défini.
Mais il s'avère que, grâce à _renommage des registres ,_ nous avons vraiment beaucoup de registres disponibles, et nous n'avons même pas besoin de les sauvegarder. Le processeur dispose en fait de plusieurs jeux de registres, et il passe automatiquement de l'un à l'autre au fur et à mesure que votre code s'exécute. Il fait cela uniquement pour obtenir plus de registres.
Exemple :
load r1, a # x = a
store r1, x
load r1, b # y = b
store r1, y
Dans une architecture qui n'a que r0-r7, le code suivant peut être réécrit automatiquement par le CPU comme quelque chose comme :
load r1, a
store r1, x
load r10, b
store r10, y
Dans ce cas, r10 est un registre caché qui se substitue temporairement à r1. Le CPU peut savoir que la valeur de r1 ne sera plus jamais utilisée après le premier stockage. Cela permet de retarder le premier chargement (même un hit de cache sur puce prend généralement plusieurs cycles) sans nécessiter le retard du second chargement ou du second stockage.
Ils ajoutent des registres en permanence, mais ils sont souvent liés à des instructions spéciales (par exemple SIMD, SSE2, etc.) ou nécessitent une compilation pour une architecture CPU spécifique, ce qui réduit la portabilité. Les instructions existantes fonctionnent souvent sur des registres spécifiques et ne peuvent pas profiter d'autres registres s'ils sont disponibles. Jeu d'instructions hérité et tout le reste.
Pour ajouter une petite information intéressante, vous remarquerez que le fait d'avoir 8 registres de même taille permet aux codes d'opération de rester cohérents avec la notation hexadécimale. Par exemple, l'instruction push ax
est l'opcode 0x50 sur x86 et va jusqu'à 0x57 pour le dernier registre di. Ensuite, l'instruction pop ax
commence à 0x58 et va jusqu'à 0x5F pop di
pour compléter la première base 16. La cohérence hexadécimale est maintenue avec 8 registres par taille.