Visualiser une révision

cpu

devnewton 🍺 : révision n°2 (10 juin 2023 10:10:17)

# Détails de fonctionnement processeurs et architecture modernes


Les [CPU](https://fr.wikipedia.org/wiki/Microprocesseur) ont beaucoup évolué ces dernières années. L'importante notion de "cycle par instruction" qui avait un sens au temps des vieux processeurs z80, 6502, M68000 ou autre i386, est dépassée depuis... Les CPU ont maintenant un [pipeline](https://fr.wikipedia.org/wiki/Pipeline_(architecture_des_processeurs)) qui a pour objectif de diviser les instructions par blocs indépendants afin que chacune puisse s'exécuter indépendamment, ce qui a pour effet que chaque instruction consomme idéalement 1 cycle. Pour donner un exemple simple, c'est comme si pour fabriquer une bouteille, on mettait 3 secondes pour :



- faire le contenu en plastique
- remplir de l'eau
- créer et fermer le bouchon.



On fera chaque étape en 1 seconde (oui on est rapide), rien n'empêchera donc d'avoir une chaîne de fabrication avec 3 machines qui réaliseraient chacunes ces trois opérations. À part la première bouteille dont la fabrication demandera 3 secondes, les suivantes se feront certes en 3 secondes mais il en sortira une par seconde !



1ère seconde | 2ème sec.| 3ème sec. | bouteille
-------------|----------|-----------|----------
Contenu 1    |          |           |
Contenu 2    | Eau 1    |           |
Contenu 3    | Eau 2    | Bouchon 1 | fin de la bouteille 1
Contenu 4    | Eau 3    | Bouchon 2 | fin de la bouteille 2
Contenu 5    | Eau 4    | Bouchon 3 | fin de la bouteille 3
 etc.        |          |           |



On voit que tout se fait en parallèle et c'est comme si on avait une bouteille par seconde ! Le principe est le même pour les instructions. Mais on peut faire mieux, car sur un processeur on a au moins 3 unités de calcul : 



- ALU (arithmetic logic unit) pour les calculs arithmétiques (plus, moins, division, multiplication) et logiques (AND, OR, XOR, décalage binaire etc);
- LSU (loadstore unit) pour les lectures/écritures sur la RAM ou autre;
- FPU (float point unit) pour les calculs à nombres flottants.



Lorsque ces unités de calcul peuvent travailler en parallèle, on parle de processeur [superscalaire](https://fr.wikipedia.org/wiki/Processeur_superscalaire) qui peut donc exécuter plusieurs instructions par cycle. Mais est-ce si simple ? La réponse est non, il existe plusieurs choses qui feront que le processeur se bloquera avec le pipeline.



Pour optimiser vous avez quatre choses à faire : 



1. Résoudre les dépendances de données;
2. Paralléliser les instructions;
3. Optimiser les conditions;
4. Optimiser les caches.



## Dépendances des données
Une « dépendance de données », c'est lorsqu'un calcul dépend d'un résultat précédent. Exemple : 



1. A = B + C
1. D = A + 5



La deuxième opération ne peut se faire que quand on connaît A. Que fait un processeur s'il est dans ce cas-là ? Il se bloque. Pour éviter ces blocages, il y a plusieurs solutions. L'une d'elles est de faire autre chose entre-temps ou de faire un _bypass_ (que l'on expliquera plus tard).



## Parallélisation
La parallélisation est l'exécution simultanée de plusieurs instructions. La difficulté est de trouver le "maximum" d'instructions à exécuter en parallèle.



## Optimisation des conditions
Les conditions (if/else) rendent complexes la parallélisation : sans optimisation, un processeur ne peut pas exécuter des instructions en parallèle avant de savoir si le code ira dans telle ou telle branche (dans le if ou dans le else). La prédiction de branchement est utilisé pour que "la plupart du temps", le code ne se trompe pas. En cas d'erreurs, les calculs sont oubliés (et si c'est mal fait on vient de créer une faille spectre ou meltdown).



Quand un processeur exécute les instructions dans l'ordre et qu'il est [superscalaire](https://fr.wikipedia.org/wiki/Processeur_superscalaire), on le nomme tout simplement _superscalaire in order_. Je devrais être plus clair : un processeur _superscalaire in order_ exécute plusieurs instructions, mais pour cela il doit regarder l'instruction suivante et s'il peut les exécuter en parallèle, il le fait. Cela demande au compilateur d'aider beaucoup le processeur pour éviter les divers blocages. 



Sinon, un processeur peut faire automatiquement ce qu'on appelle l'exécution dans le désordre. Pour éviter les blocages et paralléliser les instructions, il va tenter d'exécuter plein d'instructions. En gros, il peuvent regarder les "100 suivantes" et il cherche les instructions parallélisables ou à faire pendant un blocage et à prédire les conditions. On parle alors de [prédiction de branchement](https://fr.wikipedia.org/wiki/Pr%C3%A9diction_de_branchement).



Actuellement les processeurs sont out or order pour atteindre de hautes performances.
C'est ce que font Intel et AMD depuis 30 ans (depuis le Pentium 2 à peu près).

Les processeurs in order sont souvent peu performants, et sont plus utilisé actuellement dans l'embarqué.
Même s'ils ont perduré assez longtemps dans le monde des consoles (la PS3 et la X360 avait des processeurs superscalaire in order).


Il y a une autre école (dont fait partie le CPU de [Kannagi](https://linuxfr.org/users/kannagichan), l'[AltairX fait partie](https://github.com/Kannagi/AltairX)) qui dit qu'il faut tout simplement être plus explicite avec le processeur. Par exemple, sur le nombre d'instructions à exécuter ou sur des informations internes (_bypass_, renommage de registre, prédiction de branche). Cela permet d'éviter d'avoir la complexité d'un _superscalaire out of order_ tout en gardant de bonnes performances.
À la base, le [VLIW](https://fr.wikipedia.org/wiki/Very_long_instruction_word) explicite seulement le nombre d'instructions en parallèle. Le désavantage est qu'il faut un très, très bon compilateur. Certains comme [LLVM](https://llvm.org/) ont beaucoup progressé, ce qui donne bon espoir dans cette approche.



## Cache
Il existe 3 types de caches :



- Direct mapped
- Set associative
- Full associative



Le _direct mapped_ est assez simpliste, on va prendre l'adresse, on va stocker une partie de cette adresse, et ça sera l'index dans notre cache (comme un tableau). Mais du coup vous allez forcément avoir pas mal de conflits d'adresse.



Le _set associative_ est une amélioration : c'est pareil que le _direct mapped_, mais vous rajoutez sur chaque "index" une comparaison pour chaque adresse (en groupe de 2, 4, 8 ou 16) et on garde l'adresse entière. Le nombre d'adresses stockées sur chaque "index" est appelé _way_.



Avec le _full associative_, on stocke les adresses en entier. Cela implique de comparer chaque adresse, ce qui est relativement long (c'est la raison pour laquelle il n'est utilisé que pour un cache très petit comme le [TLB](https://fr.wikipedia.org/wiki/Translation_lookaside_buffer)).