Aller au contenu

Architecture d'un processeur

Comment un processeur exécuté des instructions — et pourquoi cela conditionne vos choix logiciels.


Le cycle fondamental

Tout processeur, du microcontrôleur embarque au Xeon de datacenter, repose sur le même cycle de base : fetch-décodé-exécuté. Le processeur lit une instruction depuis la mémoire (fetch), détermine ce qu'elle signifie (décodé), puis l'exécuté (exécuté). Ce cycle se répété des milliards de fois par seconde.

graph LR
    F["Fetch"] --instruction--> D["Decode"]
    D --operation--> E["Execute"]
    E --resultat--> W["Write-back"]
    W --PC+1--> F

Le compteur de programme (PC) avance à chaque cycle. La simplicité apparente de ce mécanisme masque une complexité considérable : pour atteindre les fréquences actuelles (3-5 GHz), les processeurs modernes découpent ce cycle en dizaines d'étapes internes et exécutent plusieurs instructions simultanément.

La fréquence d'horloge donne le rythme. A 4 GHz, un cycle dure 0.25 nanoseconde. La lumiere parcourt environ 7.5 cm en ce temps. Cette contrainte physique explique pourquoi les processeurs sont si petits : les signaux doivent traverser la puce en un seul cycle. Plus la puce est grande, plus il faut ralentir l'horloge — ou découper le travail en plus d'étapes.


Pipeline classique

Le pipeline est l'optimisation fondamentale du processeur moderne. L'idée est simple : pendant qu'une instruction est en phase d'exécution, la suivante est déjà en phase de décodage, et celle d'après est en cours de chargement. On emprunte le concept à la chaîne de montage industrielle.

Le pipeline RISC classique comporte cinq étapes :

graph LR
    IF["IF<br>Instruction<br>Fetch"] --text--> ID["ID<br>Instruction<br>Decode"]
    ID --text--> EX["EX<br>Execute"]
    EX --text--> MEM["MEM<br>Memory<br>Access"]
    MEM --text--> WB["WB<br>Write<br>Back"]
Étape Rôle Durée typique
IF (Instruction Fetch) Lire l'instruction depuis le cache d'instructions 1 cycle
ID (Instruction Décodé) Décoder l'opcode, lire les registres sources 1 cycle
EX (Exécuté) Effectuer le calcul dans l'ALU 1 cycle
MEM (Memory Access) Accéder à la mémoire si nécessaire (load/store) 1 cycle
WB (Write Back) Écrire le résultat dans le registre destination 1 cycle

Sans pipeline, une instruction prend 5 cycles. Avec pipeline, on termine une instruction par cycle en regime permanent — un gain théorique de 5x. En pratique, ce gain est réduit par les aleas (hazards).

Pour visualiser le fonctionnement, imaginons quatre instructions I1 a I4 traversant le pipeline :

gantt
    title Pipeline — 4 instructions
    dateFormat X
    axisFormat %s
    tickInterval 1second

    section I1
    IF  : 0, 1
    ID  : 1, 2
    EX  : 2, 3
    MEM : 3, 4
    WB  : 4, 5

    section I2
    IF  : 1, 2
    ID  : 2, 3
    EX  : 3, 4
    MEM : 4, 5
    WB  : 5, 6

    section I3
    IF  : 2, 3
    ID  : 3, 4
    EX  : 4, 5
    MEM : 5, 6
    WB  : 6, 7

    section I4
    IF  : 3, 4
    ID  : 4, 5
    EX  : 5, 6
    MEM : 6, 7
    WB  : 7, 8

Au cycle 5, les cinq étages du pipeline sont tous occupes — chaque étage travaille sur une instruction différente. C'est le regime permanent, et c'est la que le pipeline atteint son débit maximal.

Pipeline superscalaire

Les processeurs modernes vont plus loin : ils sont superscalaires. Un processeur superscalaire peut lancer plusieurs instructions par cycle dans des unites d'exécution parallèles. Un Core i7 typique dispose de 4 a 6 ports d'exécution : deux ALU entières, une ou deux unites flottantes, une unite de chargement, une unite de stockage.

Le processeur réordonne dynamiquement les instructions (out-of-order exécution) pour remplir ces unites au maximum. Le programmeur écrit du code séquentiel ; le matériel retrouve le parallélisme implicite. Cette technique, décrite en détail par Hennessy et Patterson, est ce qui permet aux processeurs modernes d'atteindre 3 a 6 instructions par cycle en pratique.

L'exécution out-of-order repose sur plusieurs composants internes :

  • Le Reorder Buffer (ROB) qui maintient l'ordre logique des instructions même quand elles s'exécutent dans le désordre
  • Le Register Renaming qui élimine les fausses dépendances entre instructions en utilisant des registres physiques supplémentaires (un processeur x86 moderne a ~200 registres physiques pour 16 registres architecturaux)
  • Les Reservation Stations qui stockent les instructions en attente de leurs opérandes

Profondeur de pipeline et compromis

Les processeurs modernes ont des pipelines bien plus profonds que 5 étages. Le Pentium 4 (NetBurst) avait un pipeline de 31 étages, ce qui permettait des fréquences élevées mais rendait les branch mispredict très coûteux. Les architectures actuelles (Zen 4, Golden Cove) utilisent 15 a 20 étages — un compromis entre fréquence et pénalité de mauvaise prédiction.


Hazards et solutions

Le pipeline ne fonctionne parfaitement que si chaque instruction est indépendante de la précédente. En réalité, trois types d'aleas perturbent le flux.

Data hazards

Une instruction a besoin du résultat d'une instruction précédente qui n'a pas encore termine le pipeline.

ADD R1, R2, R3   ; R1 = R2 + R3
SUB R4, R1, R5   ; R4 = R1 - R5 (besoin de R1)

La seconde instruction a besoin de R1, mais la première n'a pas encore écrit son résultat. Sans intervention, le pipeline devrait inserer des bulles (stalls) — des cycles perdus.

La solution principale est le forwarding (ou bypassing) : le résultat est transmis directement depuis la sortie de l'ALU vers l'entree, sans attendre l'écriture en registre. Le forwarding élimine la plupart des stalls lies aux dépendances de données.

Il existe trois sous-types de data hazards :

  • RAW (Read After Write) : le cas classique ci-dessus. Le plus frequent et le plus problematique.
  • WAR (Write After Read) : une instruction écrit dans un registre qu'une instruction précédente doit encore lire. Résolu par le register renaming.
  • WAW (Write After Write) : deux instructions ecrivent dans le même registre. Egalement résolu par le register renaming.

Control hazards

Les branchements conditionnels posent un problème : on ne sait pas quelle instruction charger ensuite tant que la condition n'est pas évaluée. Le pipeline a déjà commence à charger les instructions suivantes — si le branchement est pris, ce travail est perdu (pipeline flush).

La prédiction de branchement résout ce problème en pariant sur la direction du branchement avant de connaître le résultat. Les prédicteurs modernes atteignent des taux de réussite de 95-98%. Ils combinent :

  • Un Branch Target Buffer (BTB) pour prédire l'adresse cible
  • Un historique local et global pour prédire la direction (pris/non pris)
  • Des algorithmes comme TAGE (Tagged Geometric) dans les processeurs récents

Le prédicteur de branchement est l'un des composants les plus complexes d'un processeur moderne. Il utilise des tables d'historique de plusieurs centaines de KB, et les meilleurs prédicteurs actuels n'échouent que sur 1 a 3% des branchements sur du code réel.

Warning

Un branchement mal prédit coute 15 a 25 cycles sur un processeur moderne. Dans une boucle critique, un if imprévisible peut diviser la performance par deux. Les techniques comme le branchless programming existent précisément pour contourner ce coût. Exemple classique : remplacer if (a > b) max = a; else max = b; par une instruction conditionnelle (CMOV sur x86) qui ne crée pas de branchement.

Structural hazards

Deux instructions ont besoin de la même ressource matérielle au même moment — par exemple, deux accès mémoire simultanés quand il n'y a qu'un seul port mémoire. Les processeurs modernes résolvent cela en dupliquant les ressources critiques (caches multi-port, multiples unites d'exécution).

La séparation du cache L1 en deux caches distincts — un pour les instructions (L1i) et un pour les données (L1d) — est un exemple de cette stratégie. Sans cette séparation, l'étage IF et l'étage MEM entreraient en conflit à chaque cycle.

Exécution spéculative

L'exécution spéculative combine la prédiction de branchement avec l'exécution out-of-order. Le processeur ne se contente pas de prédire la direction d'un branchement — il exécuté les instructions le long du chemin prédit. Si la prédiction etait correcte, le résultat est conserve. Sinon, le travail spéculatif est annule (pipeline flush) et le processeur repart sur le bon chemin.

Cette technique est puissante mais a fait parler d'elle avec les vulnérabilités Spectre et Meltdown (2018), qui exploitent les effets de bord de l'exécution spéculative (empreintes dans le cache) pour extraire des données sensibles.


Caches : L1, L2, L3

La mémoire principale (DRAM) est lente comparée au processeur — environ 200 a 300 cycles d'accès. Sans caches, le processeur passerait la majeure partie de son temps a attendre des données. Les caches sont des mémoires SRAM rapides et coûteuses, placees entre le processeur et la RAM.

Organisation

Niveau Taille typique Latence Partage
L1 Data 32-64 KB 4-5 cycles (~1 ns) Par cœur
L1 Instruction 32-64 KB 4-5 cycles (~1 ns) Par cœur
L2 256 KB - 1 MB 12-14 cycles (~4 ns) Par cœur
L3 8-64 MB 30-40 cycles (~10 ns) Partage entre cœurs

Le cache fonctionne par lignes (cache lines), typiquement de 64 octets. Quand le processeur demande un octet, le cache charge la ligne entière. Ce choix exploite la localité spatiale : si on accede à l'adresse N, on accede probablement bientôt a N+1, N+2, etc.

Associativite

L'associativite déterminé comment les adresses mémoire sont mappées vers les emplacements du cache :

  • Un cache direct-mapped (1-way) est rapide mais sujet aux conflits : deux adresses qui mappent vers la même ligne de cache s'evincent mutuellement, même si le cache est presque vide.
  • Un cache fully-associative élimine les conflits mais est lent a chercher — il faut comparer l'adresse avec toutes les lignes du cache.
  • Le compromis est le cache set-associative : le cache est divise en sets, et chaque adresse peut aller dans n'importe quelle ligne de son set. Le cache L1 est typiquement 8-way, le L3 peut être 16-way.

Un cache 8-way associatif de 32 KB avec des lignes de 64 octets contient 512 lignes organisees en 64 sets de 8 lignes chacun. Pour une adresse donnée, le processeur calcule le set (6 bits de l'adresse), puis cherche parmi les 8 lignes du set — un compromis raisonnable entre vitesse et taux de hit.

Politiques de remplacement

Quand une ligne de cache doit être évincée pour faire place a une nouvelle, le processeur utilisé une politique de remplacement :

  • LRU (Least Recently Used) : evince la ligne la moins récemment utilisée. Optimal en théorie, mais coûteux en metadata pour les caches très associatifs.
  • Pseudo-LRU : approximation de LRU avec un arbre de bits, utilisé en pratique dans la plupart des caches L1/L2.
  • Random : simple et etonnamment efficace pour les grands caches comme le L3, ou la différence avec LRU est negligeable.
  • RRIP (Re-Référence Interval Prédiction) : utilisé dans les caches L3 récents, prédit quand une ligne sera reutilisee et evince en priorité les lignes qui ne le seront probablement pas.

Write policies

  • Write-back : les écritures sont differees — le cache est modifie, la mémoire sera mise à jour plus tard quand la ligne est évincée. Plus performant car les écritures multiples sur la même ligne ne génèrent qu'un seul écriture en mémoire.
  • Write-through : chaque écriture est immédiatement propagée à la mémoire. Plus simple mais plus lent. Utilisé parfois au niveau L1 pour simplifier la cohérence.

Cache misses : les trois C

Les cache misses se classent en trois catégories, connues comme les "3 C" :

  • Compulsory (cold miss) : le premier accès a une donnée est toujours un miss. Inevitable.
  • Capacity : le working set est trop grand pour le cache. Solution : réduire le working set ou augmenter le cache.
  • Conflict : deux adresses mappent vers le même set alors que d'autres sets sont vides. Solution : augmenter l'associativite.

Tip

En tant que développeur, la connaissance des caches influence directement vos choix. Parcourir un tableau lineairement (localité spatiale) est 10 a 100 fois plus rapide que sauter aléatoirement dans une structure chaînée (pointeurs éparpillés en mémoire). Un ArrayList bat souvent un LinkedList en pratique, malgre ce que la théorie algorithmique suggère. Le prefetcher matériel détecté les accès séquentiels et charge les lignes suivantes en avance — mais il ne peut pas prédire les accès aléatoires via pointeurs.


Registres et jeu d'instructions

Le processeur travaille avec des registres — des cases mémoire internes accessibles en un cycle. Un processeur x86-64 dispose de 16 registres généraux (RAX, RBX, ..., R15) et de registres vectoriels (AVX-512 : 32 registres de 512 bits).

Les registres sont la mémoire la plus rapide du système — pas de latence d'accès, pas de cache miss possible. Le compilateur s'efforce de garder les variables les plus utilisées dans les registres plutôt qu'en mémoire. C'est pourquoi les langages compiles (C, C++, Rust, Go) peuvent être significativement plus rapides que les langages interpretes : le compilateur optimise l'allocation des registres, alors qu'un interpréteur passe son temps a charger et décharger des variables depuis la mémoire.

Le jeu d'instructions (ISA) définit le contrat entre le logiciel et le matériel :

ISA Philosophie Exemples
x86-64 (CISC) Instructions complexes, taille variable Intel, AMD — domination serveur
ARM (RISC) Instructions simples, taille fixe Apple M-series, AWS Graviton — montee en puissance
RISC-V RISC ouvert, extensible Emergent, recherche et embarque

La distinction CISC/RISC est aujourd'hui floue : les processeurs x86 modernes décodent les instructions CISC en micro-opérations RISC internes (les "uops"). Le front-end du processeur traduit le code x86 en uops, et le back-end exécuté ces uops comme un processeur RISC.

SIMD : parallélisme de données dans le processeur

Les instructions SIMD (Single Instruction, Multiple Data) permettent d'appliquer la même opération a plusieurs données simultanément. Un registre AVX-512 de 512 bits peut traiter 16 flottants 32 bits en un seul cycle.

Extension Largeur Opérations parallèles (float32)
SSE 128 bits 4
AVX2 256 bits 8
AVX-512 512 bits 16

Les compilateurs modernes vectorisent automatiquement les boucles simples (auto-vectorization). Mais les structures de données doivent coopérer : un array-of-structs ne se vectorise pas bien, un struct-of-arrays est idéal pour le SIMD.


Fréquence, puissance et limites physiques

Le mur de la puissance

La puissance dissipee par un processeur suit approximativement la formule :

\[ P = C \times V^2 \times f \]

Ou C est la capacitance, V la tension et f la fréquence. Doubler la fréquence double la puissance. Mais pour monter en fréquence, il faut aussi augmenter la tension, ce qui quadruple la puissance \((V^2)\). C'est pourquoi les fréquences ont plafonee autour de 4-5 GHz au milieu des années 2000.

Turbo Boost et gestion thermique

Les processeurs modernes adaptent dynamiquement leur fréquence :

  • Turbo Boost (Intel) / Précision Boost (AMD) : un cœur peut monter temporairement en fréquence si les autres sont inactifs et que l'enveloppe thermique le permet
  • Throttling : le processeur réduit sa fréquence quand il surchauffe, ce qui dégradé les performances de façon imprévisible

Note

Le throttling thermique est une cause fréquente de performance dégradée en production. Un serveur mal ventile ou dans un rack trop dense peut perdre 20 a 30% de sa capacité CPU sans aucune alerte logicielle. Surveiller la temperature CPU (sensors sous Linux, IPMI sur les serveurs) est un reflexe d'opérations essentiel.


Implications pour l'architecte logiciel

Les mécanismes décrits dans ce chapitre ont des conséquences directes sur la conception logicielle :

  1. Predictibilite des branchements : un code avec des chemins prévisibles (boucles régulières, données triees) s'exécuté plus vite qu'un code avec des branchements aléatoires. Trier un tableau avant de le filtrer peut accélérer le traitement.

  2. Localité des données : les structures de données compactes et contigues en mémoire exploitent les caches. Un struct-of-arrays bat un array-of-structs quand on accede a un seul champ sur de nombreux éléments. Le choix entre HashMap et Vec trie n'est pas seulement une question de complexité algorithmique — la localité spatiale du Vec compense largement pour les petites collections.

  3. Parallélisme d'instructions : un code sans dépendances entre opérations consecutives permet au processeur de les exécuter en parallèle via l'exécution out-of-order. Les boucles deroulees (loop unrolling) et les expressions indépendantes sont mieux exploitees.

  4. Choix d'architecture matérielle : x86 pour la compatibilité et la performance brute, ARM (Graviton) pour le rapport performance/coût et l'efficacité énergétique en cloud. La décision se prend au niveau de l'architecture système, pas au niveau du code.

  5. SIMD et vectorisation : pour les traitements massifs de données (encodage, compression, serialisation), utiliser des structures de données compatibles SIMD peut multiplier les performances par 4 a 16x.

Ces principes semblent bas-niveau, mais ils expliquent pourquoi un système identique sur le papier peut avoir des performances 10x différentes selon l'implémentation. On y reviendra au chapitre 07 en parlant de dimensionnement.


Chapitre suivant : Hiérarchie mémoire — registres, caches, RAM, et pourquoi la localité est la reine de la performance.