C4 — Mémoire et périphériquesDans le dialogue d’impression, choisissez « Enregistrer au format PDF » comme destination.
Retour

Licence 1 · Architecture des ordinateurs

Cours 4Mémoire et périphériques

Expliquer pourquoi une machine rapide passe son temps à attendre, et ce qui la fait attendre moins : cache, interruptions, pipeline.

2 chapitres · 12 h de travail estimé

  1. 1. Hiérarchie mémoire7 h
  2. 2. Entrées/sorties et performance5 h

Chapitre 1 · 7 h

Hiérarchie mémoire

Registres, cache, RAM, stockage ; principe de localité, fonctionnement du cache et taux de succès ; ROM, SRAM et DRAM ; première approche de la mémoire virtuelle.

Ramenons les temps d'accès d'une machine à l'échelle humaine, en posant qu'un accès à un registre dure une seconde.

NiveauTemps réelÀ l'échelle « registre = 1 s »
Registre0,3 ns1 seconde
Cache L11 ns4 secondes
Cache L24 ns12 secondes
Mémoire vive60 ns3 minutes
Disque SSD100 µs3 jours
Disque magnétique10 ms1 an

Le chapitre 5 a nommé le problème — le processeur attend la mémoire. Le voici chiffré : entre un registre et la mémoire vive, il y a le rapport d'une seconde à trois minutes. Aucune astuce de programmation ne comble un tel écart. Ce qui le comble, c'est une hiérarchie de mémoires, et un pari statistique sur la façon dont les programmes accèdent aux données.

La pyramide, et pourquoi elle existe

On voudrait une mémoire à la fois rapide, vaste et bon marché. Les trois sont incompatibles : la technologie qui va vite coûte cher au bit et consomme, donc on n'en met pas beaucoup.

La solution est de superposer plusieurs technologies, la plus rapide et la plus petite au plus près du processeur :

        registres      ~ 1 Kio        le plus rapide, le plus cher        cache L1       ~ 64 Kio        cache L2       ~ 512 Kio        cache L3       ~ 32 Mio       partagé entre les cœurs        mémoire vive   ~ 16 Gio        SSD / disque   ~ 1 Tio        le plus lent, le moins cher

Chaque niveau contient un sous-ensemble du niveau inférieur, et sert de tampon pour lui. L'objectif est de donner l'illusion d'une mémoire aussi vaste que le disque et presque aussi rapide que le cache. Ce n'est pas une illusion gratuite : elle repose sur une propriété observée des programmes réels.

Le principe de localité

Un programme n'accède pas à la mémoire au hasard. Il le fait suivant deux régularités, et toute la hiérarchie repose sur elles.

La localité temporelle. Une case qui vient d'être utilisée a de fortes chances de l'être à nouveau bientôt. Une variable de boucle, un compteur, l'adresse de retour d'une fonction : on y revient à chaque tour.

La localité spatiale. Si une case est utilisée, ses voisines le seront probablement. Parcourir un tableau, exécuter des instructions consécutives, lire les champs d'une structure : tous ces accès sont contigus.

Le programme du chapitre 6 en est l'illustration parfaite. Les cinq instructions du corps de la boucle sont relues à chaque tour — localité temporelle — et se suivent en mémoire — localité spatiale. Les registres $t0 et $t1 sont écrits et relus sans cesse.

De là découle la décision de conception la plus importante du cache : on ne transfère jamais un mot isolé, on transfère une ligne de 32 ou 64 octets. Puisque les voisines seront probablement demandées, autant les rapporter tout de suite — le coût dominant est celui du déplacement, pas celui de la quantité.

Comment fonctionne un cache

Le cache est un petit tableau de lignes. Le processeur demande une adresse ; s'il la trouve c'est un succès (hit), sinon un échec (miss) et il faut aller au niveau inférieur.

Pour savoir où chercher, l'adresse est découpée en trois champs, et ce découpage est exactement la somme pondérée du chapitre 1 coupée en morceaux :

        ┌──────────────┬──────────┬──────────────┐adresse │   étiquette  │  index   │  déplacement │        └──────────────┴──────────┴──────────────┘             qui ?        où ?        quel octet                                      dans la ligne

Le déplacement désigne l'octet dans la ligne : pour une ligne de 64 octets, 6 bits. L'index désigne l'emplacement du cache où cette adresse a le droit de se trouver : pour 128 emplacements, 7 bits. L'étiquette est tout le reste, et elle est stockée avec la ligne pour savoir laquelle des nombreuses adresses possibles y réside.

Trois organisations existent, et elles se distinguent par le nombre d'emplacements où une adresse peut aller.

La correspondance directe : un seul emplacement possible, calculé par l'index. Recherche immédiate, matériel minimal — mais deux adresses de même index se chassent mutuellement, même si le reste du cache est vide. C'est le conflit, et il est spectaculaire sur les parcours dont le pas est une puissance de deux.

Le totalement associatif : n'importe quelle ligne peut aller n'importe où. Aucun conflit, mais il faut comparer l'étiquette à toutes les lignes en parallèle — trop coûteux au-delà de quelques dizaines d'entrées.

L'associatif par ensembles est le compromis universellement retenu : le cache est découpé en ensembles de kk lignes, l'index désigne l'ensemble, et l'adresse peut occuper n'importe laquelle des kk voies. Avec k=4k = 4 ou 8, les conflits deviennent rares pour un coût de comparaison modeste.

Quiz · 1 question

Un cache à correspondance directe de 128 lignes affiche un taux de succès catastrophique sur un programme qui parcourt alternativement deux tableaux dont les adresses de départ diffèrent d'un multiple exact de la taille du cache. Pourquoi ?

  • Les deux tableaux sont trop gros pour tenir dans le cachecapacité insuffisante
  • Les cases de même rang des deux tableaux ont le même index, donc un seul emplacement possible : elles se chassent l'une l'autre à chaque accès, alors que le reste du cache est videconflit d'index
  • Le cache ne sait pas gérer deux zones mémoire distinctes simultanémentlimite du matériel

Réponse : C'est l'échec par CONFLIT, à distinguer de l'échec par capacité. Les deux tableaux tiendraient largement dans le cache ; le problème est que la correspondance directe n'offre qu'UN emplacement par index. Si les adresses de départ diffèrent d'un multiple de la taille du cache, T1[i] et T2[i] tombent sur le même index et s'évincent alternativement, à chaque tour de boucle, pendant que 126 lignes restent inutilisées. Deux remèdes : rendre le cache associatif par ensembles, ce qui offre k emplacements par index, ou décaler l'un des tableaux de quelques octets — un correctif qu'on trouve réellement dans les bibliothèques de calcul.

Mesurer, et les deux politiques

Le taux de succès tt est la proportion d'accès trouvés dans le cache. Le temps d'accès moyen s'en déduit :

Tmoyen=t×Tcache+(1t)×TmeˊmoireT_{\text{moyen}} = t \times T_{\text{cache}} + (1 - t) \times T_{\text{mémoire}}

Le calcul réserve une surprise, et c'est le point à retenir du chapitre. Avec un cache à 1 ns et une mémoire à 60 ns : à 95 % de succès, on obtient 3,95 ns ; à 99 %, 1,59 ns. Passer de 95 % à 99 % divise le temps moyen par 2,5, alors que le taux ne progresse que de quatre points. C'est parce que le terme d'échec est pondéré par un coût soixante fois supérieur — raison pour laquelle on parle plutôt du taux d'échec que du taux de succès : c'est lui qui pilote la performance.

Deux politiques restent à fixer.

Le remplacement : quelle ligne évincer quand l'ensemble est plein ? LRU (least recently used), qui sacrifie la moins récemment utilisée, est le choix naturel — c'est un pari direct sur la localité temporelle. Le cours de systèmes reprendra exactement cette question au chapitre 6, pour les pages en mémoire.

L'écriture : que faire quand le processeur écrit ? En écriture immédiate (write-through), on écrit dans le cache et dans la mémoire à la fois — simple, cohérent, lent. En écriture différée (write-back), on n'écrit que dans le cache, on marque la ligne « sale », et on ne recopie en mémoire qu'à son éviction — rapide, mais il faut gérer la cohérence, question qui devient épineuse dès qu'il y a plusieurs cœurs partageant des données.

ROM, SRAM, DRAM

Trois technologies, trois rôles, et les distinguer évite bien des confusions.

La ROM est non volatile : elle survit à la coupure de courant. Elle contient le micrologiciel qui démarre la machine — le processeur, au premier front d'horloge, va chercher son instruction à une adresse fixe qui pointe dans la ROM. Ses variantes modernes, EEPROM et mémoire flash, se réécrivent, ce qui permet les mises à jour de micrologiciel et fonde les SSD.

La SRAM stocke chaque bit dans une bascule — celle du chapitre 4, six transistors. Elle est rapide et tient son état tant qu'elle est alimentée, mais elle est encombrante et chère. C'est la mémoire des caches et des registres.

La DRAM stocke chaque bit dans un condensateur, avec un seul transistor. D'où sa densité, donc son prix bas au gigaoctet, donc son emploi comme mémoire principale. Mais le condensateur se décharge : il faut le rafraîchir toutes les quelques dizaines de millisecondes, en relisant et réécrivant chaque ligne. Ce rafraîchissement coûte du temps et de l'énergie, et c'est la raison de fond pour laquelle la DRAM est plus lente que la SRAM.

Toutes deux sont volatiles : contenu perdu à l'extinction. C'est ce qui rend nécessaire le système de fichiers du chapitre 7 du cours de systèmes.

Un mot de mémoire virtuelle

Le même raisonnement se rejoue un cran plus bas, entre la mémoire vive et le disque, et il porte alors le nom de mémoire virtuelle.

Chaque programme manipule des adresses logiques, qu'un composant matériel — l'unité de gestion mémoire — traduit en adresses physiques. La mémoire est découpée en pages de 4 Kio, et toutes les pages n'ont pas besoin d'être en mémoire vive : les autres attendent sur le disque, et le matériel signale un défaut de page quand on touche l'une d'elles.

Les analogies avec le cache sont exactes, terme à terme : la page est la ligne, le défaut de page est l'échec, l'algorithme de remplacement est le LRU, et la localité est ce qui rend le tout viable. Seuls les ordres de grandeur changent — et ils changent tellement, un facteur cent mille sur le coût de l'échec, que la gestion passe du matériel au logiciel. C'est pourquoi la mémoire virtuelle est traitée en détail par le cours de systèmes d'exploitation, chapitre 6, et seulement introduite ici.

Quiz · 1 question

Un cache a un temps d'accès de 1 ns, la mémoire de 60 ns. En améliorant le programme, le taux de succès passe de 90 % à 98 %. Que devient le temps d'accès moyen ?

  • Il passe de 6,9 ns à 2,2 ns, soit une division par plus de trois : le terme d'échec pèse 60 fois plus que celui de succèsdivision par trois
  • Il passe de 6,9 ns à 6,3 ns : le gain est proportionnel aux 8 points gagnés, soit environ 8 %gain proportionnel
  • Il ne change pas : le taux de succès n'influe que sur la consommation, pas sur le tempsaucun effet

Réponse : Le calcul : 0,90 × 1 + 0,10 × 60 = 6,9 ns, puis 0,98 × 1 + 0,02 × 60 = 2,18 ns. Le temps moyen est dominé par le terme d'échec, pondéré par un coût soixante fois supérieur : diviser le taux d'échec par cinq (de 10 % à 2 %) divise presque d'autant la part coûteuse. C'est pourquoi on raisonne sur le TAUX D'ÉCHEC et non sur le taux de succès — « 98 % de succès » sonne comme une amélioration marginale par rapport à 90 %, alors que c'est un facteur cinq sur ce qui coûte. La même arithmétique se retrouvera dans la loi d'Amdahl au chapitre 8.

À vous

L'exercice construit un simulateur de cache : découpage d'adresse en étiquette, index et déplacement, puis comptage des succès et des échecs sur une trace d'accès.

Deux expériences valent le détour, et ce sont celles du TD. La première compare la correspondance directe et l'associatif à deux voies sur un motif qui provoque des conflits. La seconde parcourt une matrice par lignes puis par colonnes : même nombre d'accès, même tableau, et un taux de succès qui s'effondre dans le second cas — c'est la localité spatiale rendue mesurable, et l'argument le plus convaincant du chapitre.

Exercice de code

Complétez le LRU du simulateur, puis mesurez l'effet de l'ordre de parcours d'une matrice.

Point de départ

// Un cache associatif par ensembles. voies = 1 donne la correspondance
// directe, voies = nbLignes donne le totalement associatif.
function creerCache({ tailleLigne, nbLignes, voies }) {
  const ensembles = nbLignes / voies;
  const table = Array.from({ length: ensembles }, () => []);  // [etiquette, ...] par ensemble
  let succes = 0, echecs = 0;

  return {
    acceder(adresse) {
      // Découpage de l'adresse. Le déplacement dans la ligne ne sert pas à
      // localiser la ligne : on le jette en divisant.
      const numeroLigne = Math.floor(adresse / tailleLigne);
      const index = numeroLigne % ensembles;
      const etiquette = Math.floor(numeroLigne / ensembles);

      const jeu = table[index];
      const place = jeu.indexOf(etiquette);
      if (place !== -1) {
        succes++;
        // ← LRU : la ligne qu'on vient d'utiliser redevient la plus récente
        return true;
      }
      echecs++;
      jeu.push(etiquette);
      if (jeu.length > voies) jeu.shift();   // évince la plus ancienne
      return false;
    },
    bilan(nom) {
      const total = succes + echecs;
      const taux = ((succes / total) * 100).toFixed(1);
      console.log(nom.padEnd(34) + succes + " succès / " + total +
                  "  ->  " + taux + " % de succès");
      return succes / total;
    },
  };
}

// ── À VOUS ────────────────────────────────────────────────────────────────
// 1. Le LRU est incomplet : en cas de succès, la ligne touchée doit
//    redevenir la plus récente. Sans cela, la politique est un simple FIFO.
// 2. Expérience 2 : écrivez le parcours PAR COLONNES et comparez.

const MOT = 4, LIGNE = 64, LIGNES = 64;

// Expérience 1 — deux tableaux distants d'un multiple exact de la taille du
// cache : leurs cases de même rang tombent sur le même index.
function alternance(voies) {
  const c = creerCache({ tailleLigne: LIGNE, nbLignes: LIGNES, voies });
  const A = 0, B = LIGNE * LIGNES;
  for (let tour = 0; tour < 4; tour++) {
    for (let i = 0; i < 64; i++) { c.acceder(A + i * MOT); c.acceder(B + i * MOT); }
  }
  return c.bilan("alternance, " + voies + " voie(s)");
}
alternance(1);
alternance(2);

// Expérience 2 — une matrice 64 x 64 d'entiers, rangée ligne par ligne.
const N = 64;
const adresseMatrice = (i, j) => (i * N + j) * MOT;

function parLignes() {
  const c = creerCache({ tailleLigne: LIGNE, nbLignes: LIGNES, voies: 2 });
  for (let i = 0; i < N; i++) for (let j = 0; j < N; j++) c.acceder(adresseMatrice(i, j));
  return c.bilan("matrice parcourue par lignes");
}
parLignes();
// ← écrivez parColonnes() : mêmes accès, ordre des boucles inversé

Solution

function creerCache({ tailleLigne, nbLignes, voies }) {
  const ensembles = nbLignes / voies;
  const table = Array.from({ length: ensembles }, () => []);
  let succes = 0, echecs = 0;

  return {
    acceder(adresse) {
      const numeroLigne = Math.floor(adresse / tailleLigne);
      const index = numeroLigne % ensembles;
      const etiquette = Math.floor(numeroLigne / ensembles);

      const jeu = table[index];
      const place = jeu.indexOf(etiquette);
      if (place !== -1) {
        succes++;
        // LRU : on retire la ligne de sa position et on la remet en queue,
        // qui est le bout « le plus récemment utilisé ». Sans ce geste, la
        // politique dégénère en FIFO et perd la localité temporelle.
        jeu.splice(place, 1);
        jeu.push(etiquette);
        return true;
      }
      echecs++;
      jeu.push(etiquette);
      if (jeu.length > voies) jeu.shift();
      return false;
    },
    bilan(nom) {
      const total = succes + echecs;
      const taux = ((succes / total) * 100).toFixed(1);
      console.log(nom.padEnd(34) + succes + " succès / " + total +
                  "  ->  " + taux + " % de succès");
      return succes / total;
    },
  };
}

const MOT = 4, LIGNE = 64, LIGNES = 64;

function alternance(voies) {
  const c = creerCache({ tailleLigne: LIGNE, nbLignes: LIGNES, voies });
  const A = 0, B = LIGNE * LIGNES;
  for (let tour = 0; tour < 4; tour++) {
    for (let i = 0; i < 64; i++) { c.acceder(A + i * MOT); c.acceder(B + i * MOT); }
  }
  return c.bilan("alternance, " + voies + " voie(s)");
}
alternance(1);   // conflit systématique : chaque accès chasse le précédent
alternance(2);   // deux voies suffisent à loger les deux tableaux

const N = 64;
const adresseMatrice = (i, j) => (i * N + j) * MOT;

function parLignes() {
  const c = creerCache({ tailleLigne: LIGNE, nbLignes: LIGNES, voies: 2 });
  for (let i = 0; i < N; i++) for (let j = 0; j < N; j++) c.acceder(adresseMatrice(i, j));
  return c.bilan("matrice parcourue par lignes");
}

function parColonnes() {
  const c = creerCache({ tailleLigne: LIGNE, nbLignes: LIGNES, voies: 2 });
  // Mêmes 4096 accès, dans un autre ordre. Chaque accès saute 64 mots, donc
  // change de ligne de cache : la ligne rapportée ne sert qu'une fois.
  for (let j = 0; j < N; j++) for (let i = 0; i < N; i++) c.acceder(adresseMatrice(i, j));
  return c.bilan("matrice parcourue par colonnes");
}

const a = parLignes();
const b = parColonnes();
console.log("Même tableau, mêmes accès, ordre inversé : " +
  (a / b).toFixed(1) + " fois plus de succès par lignes.");

En travaux pratiques

Travaux pratiques 7 · 3 h

Le même calcul, dix fois plus lent

Mesurer soi-même l'effet du cache sur du code réel : deux programmes qui font exactement les mêmes opérations, dans un ordre différent, et qui ne mettent pas le même temps.

Avant de commencer

  • gcc et un moyen de chronométrer
  • perf, ou tout compteur matériel disponible sur votre machine

Énoncé

  1. Le parcours qui surprendCréez une matrice de 4096 sur 4096 entiers. Faites-en la somme deux fois : une fois ligne par ligne, une fois colonne par colonne. Chronométrez les deux. NE lisez pas la correction avant d'avoir la mesure.
  2. ExpliquerLes deux versions lisent exactement le même nombre d'éléments. Expliquez l'écart en une phrase, puis vérifiez avec un compteur de défauts de cache. Indice : La mémoire ne se lit pas par octet : elle se lit par blocs.
  3. Mesurer la taille du cacheParcourez un tableau de taille croissante, de 1 Ko à 64 Mo, en mesurant le temps moyen par accès. Tracez la courbe et repérez les marches.
  4. Mesurer la ligne de cacheParcourez un grand tableau avec un pas croissant : 1, 2, 4, 8, 16, 32, 64 éléments. Relevez le pas à partir duquel le temps par accès cesse d'augmenter.
  5. Réparer un vrai calculÉcrivez une multiplication de matrices classique en trois boucles, chronométrez. Échangez ensuite les deux boucles internes et rechronométrez, sans rien changer d'autre.
  6. Par blocsRéécrivez la multiplication en travaillant par blocs qui tiennent dans le cache. Cherchez la taille de bloc qui donne le meilleur temps et comparez-la à ce que vous avez mesuré à l'étape 3.

C'est réussi quand

  • Vous mesurez un écart d'au moins un facteur 5 entre les deux parcours
  • Les marches de votre courbe correspondent aux tailles annoncées par votre processeur
  • L'échange de deux boucles vous fait gagner plus d'un facteur 2

Correction

Les deux parcours, et l'écartparcours.c
/* par lignes : voisins en mémoire */
for (i = 0; i < N; i++)
  for (j = 0; j < N; j++)
      somme += m[i][j];        /* 0,08 s */

/* par colonnes : un saut de N entiers à chaque accès */
for (j = 0; j < N; j++)
  for (i = 0; i < N; i++)
      somme += m[i][j];        /* 0,94 s  → 12 fois plus lent */

Mêmes additions, même nombre d'accès, même résultat. Seul l'ORDRE change. C'est la mesure la plus utile de tout le cours, parce qu'elle contredit le modèle mental dans lequel un accès mémoire coûte un accès mémoire.

Pourquoi
perf stat -e cache-misses ./parcours

par lignes   :     4 200 000 défauts de cache
par colonnes : 16 800 000 défauts de cache   (×4)

une ligne de cache fait 64 octets = 16 entiers
par lignes   : 1 défaut pour 16 éléments utilisés
par colonnes : 1 défaut pour 1 seul élément utilisé,
             les 15 autres sont chargés puis jetés

Le matériel charge 64 octets même si vous en demandez 4, en pariant que vous lirez les voisins — c'est la localité SPATIALE. Le parcours par colonnes rend ce pari perdant à chaque coup. Le processeur n'est pas lent : c'est le code qui gaspille 15 seizièmes de chaque chargement.

La courbe, et ce qu'elle révèle
taille du tableau | ns par accès
    16 Ko       |  1,1     ← tient en L1
    64 Ko       |  1,2
   256 Ko       |  3,8     ← marche : on est passé en L2
     2 Mo       |  4,1
    16 Mo       | 14,7     ← marche : on est passé en L3
   128 Mo       | 92,3     ← marche : mémoire centrale

Vous venez de mesurer la hiérarchie de VOTRE machine sans consulter aucune documentation, uniquement par le temps. Chaque marche est un niveau. L'ordre de grandeur à retenir : environ 1 ns en L1, 100 ns en mémoire centrale — un facteur 100, soit trois cents instructions perdues à attendre.

L'échange de boucles
/* ordre i, j, k : B parcouru par COLONNES */
for (i…) for (j…) for (k…) C[i][j] += A[i][k] * B[k][j];   /* 8,2 s */

/* ordre i, k, j : B parcouru par LIGNES */
for (i…) for (k…) for (j…) C[i][j] += A[i][k] * B[k][j];   /* 2,1 s */

Les mêmes multiplications, dans un ordre différent : quatre fois plus rapide. Aucune ligne n'a été supprimée, aucun algorithme changé — la complexité reste en n³. C'est la démonstration que la complexité asymptotique ne dit pas tout, et que le facteur constant qu'elle néglige peut valoir 4.

Le blocage
for (ii = 0; ii < N; ii += B)
for (kk = 0; kk < N; kk += B)
  for (jj = 0; jj < N; jj += B)
    for (i = ii; i < ii+B; i++)
      for (k = kk; k < kk+B; k++)
        for (j = jj; j < jj+B; j++)
          C[i][j] += A[i][k] * B[k][j];

B = 64 → 0,7 s   (3 blocs de 64×64 entiers ≈ 48 Ko, tiennent en L2)

On découpe pour que les données réutilisées restent dans le cache pendant qu'on s'en sert — c'est la localité TEMPORELLE, exploitée volontairement. La bonne taille de bloc n'est pas devinée : elle se déduit de la mesure de l'étape 3. Toutes les bibliothèques d'algèbre linéaire performantes font cela, et c'est aussi ce qui explique qu'elles soient réglées par architecture.

Ce que la suite en fait

Le chapitre 8 termine le tour de la machine par les entrées/sorties, dont les temps sont ceux de la dernière ligne du tableau d'ouverture — d'où les interruptions et l'accès direct à la mémoire, qui existent précisément pour ne pas faire attendre le processeur.

La loi d'Amdahl y formalisera ce que le calcul du taux d'échec a déjà fait pressentir : accélérer une partie d'un système donne un gain global borné par la part que cette partie occupe. Et le pipeline montrera la dernière raison d'être du cache : sans lui, un processeur capable de terminer une instruction par cycle passerait l'essentiel de son temps arrêté.

À retenir

Flashcards · 5 cartes

Énoncez les deux formes de localité, et la décision de conception qui en découle.
Localité TEMPORELLE : une case utilisée le sera probablement à nouveau bientôt (variable de boucle, compteur). Localité SPATIALE : ses voisines le seront aussi (parcours de tableau, instructions consécutives). D'où la décision : on ne transfère jamais un mot isolé mais une LIGNE entière de 32 ou 64 octets, puisque le coût dominant est le déplacement et non la quantité.
En quels trois champs découpe-t-on une adresse pour interroger un cache ?
Le DÉPLACEMENT (bits de poids faible) désigne l'octet dans la ligne ; l'INDEX désigne l'emplacement ou l'ensemble où l'adresse a le droit de résider ; l'ÉTIQUETTE, tout le reste, est stockée avec la ligne pour savoir laquelle des adresses possibles s'y trouve. Le découpage n'est rien d'autre que la somme pondérée du chapitre 1 coupée en morceaux.
Quelle est la différence entre un échec par conflit et un échec par capacité ?
L'échec par CAPACITÉ survient quand les données actives ne tiennent pas dans le cache. L'échec par CONFLIT survient alors que le cache a de la place : en correspondance directe, deux adresses de même index n'ont qu'un seul emplacement possible et s'évincent mutuellement. Remèdes au conflit : l'associativité par ensembles (k emplacements par index), ou décaler l'une des zones en mémoire.
Pourquoi la DRAM est-elle plus lente que la SRAM, et pourquoi l'emploie-t-on quand même ?
La SRAM stocke un bit dans une bascule (6 transistors) : rapide, mais encombrante et chère. La DRAM le stocke dans un condensateur avec UN transistor : très dense et bon marché, mais le condensateur se décharge, ce qui impose un RAFRAÎCHISSEMENT périodique coûteux en temps et en énergie. On l'emploie pour sa densité : c'est le seul moyen d'avoir des gigaoctets. Les deux sont volatiles.
Pourquoi raisonne-t-on sur le taux d'ÉCHEC plutôt que sur le taux de succès ?
Parce que le temps moyen t × T_cache + (1−t) × T_mémoire est dominé par le terme d'échec, pondéré par un coût plusieurs dizaines de fois supérieur. Passer de 90 % à 98 % de succès semble marginal, mais divise le taux d'échec par cinq et le temps moyen par plus de trois. C'est le taux d'échec qui pilote la performance.

Chapitre 2 · 5 h

Entrées/sorties et performance

Contrôleurs et bus ; scrutation, interruptions et accès direct à la mémoire ; mesure de performance et loi d'Amdahl ; introduction au pipeline et au parallélisme.

Un processeur à 3 GHz exécute environ trois milliards d'opérations élémentaires par seconde. Entre deux frappes au clavier d'un utilisateur rapide, il en a le temps de trois cents millions. S'il attendait cette frappe en surveillant le clavier, il consumerait trois cents millions de cycles à ne rien faire d'utile.

Ce dernier chapitre traite de cette disproportion, et de la question qu'elle pose : comment une machine dialogue-t-elle avec un monde des milliers de fois plus lent qu'elle sans s'y aligner ? Puis il donne les outils pour répondre à la question qui clôt le cours : cette machine, finalement, est-elle rapide ?

Contrôleurs et bus

Le processeur ne parle jamais directement à un périphérique. Entre les deux se trouve un contrôleur, circuit spécialisé qui connaît les détails électriques et mécaniques du matériel et présente au processeur une interface uniforme : quelques registres.

Trois registres suffisent à décrire presque tout contrôleur. Un registre de commande, où le processeur écrit ce qu'il veut. Un registre d'état, qu'il lit pour savoir où en est l'appareil — prêt, occupé, en erreur. Un registre de données, par lequel l'information transite.

Reste à savoir comment le processeur atteint ces registres. Deux écoles. Les entrées/sorties mappées en mémoire leur donnent des adresses ordinaires, dans le même espace que la mémoire : un lw ou un sw du chapitre 6 suffit, sans instruction spéciale, et une partie de l'espace d'adressage est réservée à cet usage. Les entrées/sorties par ports leur donnent un espace d'adressage séparé, avec des instructions dédiées. La première approche domine, parce qu'elle n'ajoute rien au jeu d'instructions — et le cours de systèmes montrera qu'elle permet à un pilote de manipuler un périphérique comme une structure en mémoire.

Trois façons d'attendre

Voilà le cœur du chapitre. Le processeur a demandé une lecture au disque ; celle-ci prendra dix millisecondes, soit trente millions de cycles. Que fait-il pendant ce temps ?

La scrutation (polling). Il boucle sur le registre d'état jusqu'à ce que le bit « prêt » passe à 1. C'est trivial à programmer et parfaitement inefficace : trente millions de cycles brûlés à relire la même case. La scrutation garde pourtant deux usages légitimes — les systèmes sans interruptions, et les périphériques si rapides que le détour par une interruption coûterait plus cher que l'attente.

Les interruptions. Le processeur lance l'opération et passe à autre chose. Quand le périphérique a fini, il lève un signal sur le bus de contrôle. Le processeur termine l'instruction en cours, sauvegarde son état — compteur ordinal, registres, registre d'état —, exécute la routine de traitement de cette interruption, puis restaure l'état et reprend exactement où il en était.

Ce mécanisme est bien plus qu'une optimisation d'entrées/sorties : c'est la condition d'existence du multitâche. Une interruption d'horloge périodique permet au système d'exploitation de reprendre la main à intervalles réguliers, donc de retirer le processeur à un programme qui ne le rend pas. Le chapitre 4 du cours de systèmes en fera le fondement de la préemption.

L'accès direct à la mémoire (DMA). Les interruptions règlent l'attente, pas le transfert : sans DMA, le processeur doit encore copier lui-même chaque mot du contrôleur vers la mémoire. Pour un fichier de dix mégaoctets, c'est deux millions et demi de transferts. Le contrôleur DMA prend ce travail en charge — le processeur lui indique adresse, taille et sens, puis retourne à son calcul ; une seule interruption signale la fin du bloc entier.

MéthodeAttenteTransfertInterruptions par bloc
Scrutationprocesseur bloquépar le processeur0
Interruptionsprocesseur librepar le processeur1 par mot
DMAprocesseur librepar le contrôleur1 par bloc

Quiz · 1 question

Un système utilise déjà les interruptions pour ses entrées/sorties disque, mais reste lent lors du chargement de gros fichiers. Quelle explication est correcte ?

  • Les interruptions sont mal configurées : bien réglées, elles suppriment tout coût de transfertmauvais réglage
  • Les interruptions libèrent le processeur pendant l'ATTENTE, mais c'est encore lui qui copie chaque mot ; sur un gros fichier cela fait des millions de transferts, d'où le DMAattente contre transfert
  • Le problème vient forcément du disque, qui est trop lent quelle que soit la méthodedisque en cause

Réponse : Il faut distinguer deux coûts. Le premier est l'ATTENTE de la fin de l'opération : les interruptions le suppriment, puisque le processeur fait autre chose entre-temps. Le second est le TRANSFERT lui-même, mot par mot, du contrôleur vers la mémoire : les interruptions ne l'éliminent pas, elles ajoutent même un traitement d'interruption par mot. Sur dix mégaoctets, cela représente des millions de sauvegardes et restaurations de contexte. Le DMA règle ce second coût en confiant le transfert à un contrôleur dédié, avec une seule interruption pour tout le bloc — le processeur ne voit passer que le début et la fin.

Mesurer la performance

Le chapitre 5 a donné la formule ; il est temps de s'en servir et d'en tirer les conséquences.

T=Ninstructions×CPIfT = \frac{N_{\text{instructions}} \times \text{CPI}}{f}

Trois facteurs, et trois responsables différents. Le nombre d'instructions dépend du programme, du compilateur et du jeu d'instructions. Le CPI dépend de la microarchitecture — un processeur pipeliné vise 1, voire moins. La fréquence dépend de la technologie de gravure.

D'où la mise en garde centrale : comparer deux machines sur leur seule fréquence n'a pas de sens, puisqu'il manque deux facteurs sur trois. Un processeur à 2 GHz avec un CPI de 1 est plus rapide qu'un processeur à 3 GHz avec un CPI de 2.

Les indicateurs synthétiques ont le même défaut. Les MIPS — millions d'instructions par seconde — ne comparent que des machines de même jeu d'instructions, sans quoi on compte des instructions qui ne font pas la même chose. Les FLOPS ne mesurent que le calcul flottant. Seule mesure honnête : le temps d'exécution d'un programme réel et représentatif de l'usage visé, ce que cherchent les jeux d'essai normalisés comme SPEC.

La loi d'Amdahl

Reste le résultat le plus utile du chapitre, et le plus souvent ignoré en pratique. On accélère une portion d'un programme d'un facteur kk ; cette portion représente une fraction pp du temps total. L'accélération globale vaut :

A=1(1p)+pkA = \frac{1}{(1-p) + \dfrac{p}{k}}

Le passage à la limite est brutal. Si kk tend vers l'infini — la portion devient instantanée — l'accélération plafonne à 1/(1p)1/(1-p). Ce qu'on n'accélère pas fixe le plafond.

Un exemple, avec p=0,6p = 0{,}6 et k=10k = 10 : on divise par dix les trois cinquièmes du temps, et l'accélération globale n'est que de 1/(0,4+0,06)=2,171/(0{,}4 + 0{,}06) = 2{,}17. Même en rendant cette portion instantanée, on ne dépasserait jamais 2,5.

Graphique

Accélération maximale atteignable selon la part optimisable du programme

  • 50 % optimisable : ×22
  • 75 % : ×44
  • 90 % : ×1010
  • 95 % : ×2020
  • 99 % : ×100100
Ces valeurs supposent une accélération INFINIE de la portion visée : ce sont des plafonds, jamais atteints. Un programme parallélisable à 90 % ne dépassera pas un facteur 10, quel que soit le nombre de cœurs — c'est le seul chiffre à retenir avant d'acheter une machine à 64 cœurs.

La leçon pratique tient en une phrase : mesurer avant d'optimiser. Diviser par cent une fonction qui occupe 2 % du temps fait gagner 2 %. L'intuition du programmeur sur l'endroit où son programme passe son temps est, statistiquement, fausse.

Pipeline et parallélisme

Une dernière question : comment descendre le CPI en dessous de 1 par instruction ?

Le pipeline répond en découpant l'exécution en étages — typiquement recherche, décodage, exécution, accès mémoire, écriture du résultat — et en les faisant travailler simultanément sur des instructions différentes. C'est la buanderie : pendant que le second tambour lave, le premier sèche, et le troisième se plie. La latence d'une instruction n'a pas diminué, mais le débit est multiplié par le nombre d'étages.

Trois obstacles, appelés aléas, l'empêchent d'atteindre ce facteur idéal. Les aléas de données : une instruction a besoin d'un résultat que la précédente n'a pas encore écrit. Les aléas de contrôle : après un branchement, on ne sait pas quelle instruction charger — et le chapitre 6 a montré que les branchements sont partout, d'où la prédiction de branchement, qui parie sur l'issue et annule les instructions engagées si le pari est perdu. Les aléas structurels : deux étages veulent la même ressource matérielle au même cycle.

Au-delà du pipeline, trois formes de parallélisme se cumulent. Le superscalaire duplique les unités pour lancer plusieurs instructions par cycle. Les instructions vectorielles appliquent une même opération à plusieurs données à la fois. Et le multicœur met plusieurs processeurs complets sur la même puce — la voie choisie depuis 2005, quand l'augmentation des fréquences s'est heurtée au mur de la consommation thermique.

Le multicœur ramène directement à Amdahl, et c'est sur ce lien que le cours se referme : doubler le nombre de cœurs ne double la performance que si le programme est parallélisable d'un bout à l'autre. Il ne l'est jamais. La partie séquentielle — lecture du fichier d'entrée, synchronisation entre tâches, écriture du résultat — impose son plafond, et c'est elle, pas le matériel, qui décide de ce que la machine vaudra.

Quiz · 1 question

Un programme passe 80 % de son temps dans une portion parfaitement parallélisable. Quelle accélération peut-on espérer au maximum, avec un nombre illimité de cœurs ?

  • Une accélération illimitée : il suffit d'ajouter des cœursillimité
  • Un facteur 5 au maximum : les 20 % séquentiels restants fixent le plafond à 1/(1−0,8)plafond à 1/(1−p)
  • Un facteur 8, soit 80 % du gain théoriqueproportionnel à p

Réponse : C'est le passage à la limite de la loi d'Amdahl. Avec p = 0,8, l'accélération vaut 1/(0,2 + 0,8/k) ; quand k tend vers l'infini, le second terme s'annule et il reste 1/0,2 = 5. Même en rendant la portion parallèle instantanée, les 20 % séquentiels sont toujours là, et ils représentent le cinquième du temps initial. Conséquence pratique pour un achat de machine : au-delà d'une dizaine de cœurs, ce programme ne gagnera plus rien. La seule façon de dépasser le plafond est de s'attaquer à la partie séquentielle — ce que le programmeur oublie systématiquement, parce qu'elle est moins spectaculaire à optimiser.

À vous

L'exercice met en chiffres les deux résultats du chapitre. D'abord la loi d'Amdahl, appliquée à plusieurs scénarios d'optimisation, dont un piège classique : accélérer énormément une portion minuscule. Ensuite un compteur de cycles de pipeline, qui compare l'exécution séquentielle, le pipeline idéal, et le pipeline avec les bulles qu'imposent les aléas.

Le second calcul donne le chiffre que le cours cherchait depuis le chapitre 5 : le CPI réel, et l'écart entre la promesse du pipeline et ce qu'il tient.

Exercice de code

Calculez l'accélération d'Amdahl, puis le CPI réel d'un pipeline à cinq étages.

Point de départ

// ── 1. La loi d'Amdahl ────────────────────────────────────────────────────
// p : fraction du temps occupée par la portion accélérée
// k : facteur d'accélération de cette portion
function amdahl(p, k) {
  return 0;   // ← à écrire : 1 / ((1 - p) + p / k)
}

const SCENARIOS = [
  { nom: "60 % du temps, accéléré x10", p: 0.60, k: 10 },
  { nom: "60 % du temps, accéléré x1000", p: 0.60, k: 1000 },
  { nom: "2 % du temps, accéléré x100", p: 0.02, k: 100 },
  { nom: "95 % du temps, accéléré x8", p: 0.95, k: 8 },
];

for (const s of SCENARIOS) {
  const a = amdahl(s.p, s.k);
  const plafond = 1 / (1 - s.p);
  console.log(s.nom.padEnd(34) + "accélération x" + a.toFixed(2) +
              "   (plafond x" + plafond.toFixed(1) + ")");
}

// ── 2. Cycles d'un pipeline ───────────────────────────────────────────────
// n instructions, e étages. Sans pipeline chacune prend e cycles. Avec
// pipeline, le premier résultat sort après e cycles puis un par cycle —
// sauf quand un aléa impose des bulles.
function cycles(n, e, bulles) {
  const sequentiel = n * e;
  const pipeline = 0;   // ← à écrire : e + (n - 1), plus les bulles
  return { sequentiel, pipeline };
}

// Le programme du chapitre 6 : 12 instructions exécutées, dont 3 branchements.
// Un branchement mal prédit coûte 2 bulles.
const N = 12, ETAGES = 5, BRANCHEMENTS = 3;

// ── À VOUS ────────────────────────────────────────────────────────────────
// 1. Écrivez amdahl et cycles.
// 2. Comparez le CPI avec une prédiction parfaite, puis avec une prédiction
//    qui se trompe une fois sur deux. Que devient l'écart au pipeline idéal ?

const r = cycles(N, ETAGES, 0);
console.log("séquentiel :", r.sequentiel, "cycles | pipeline idéal :", r.pipeline);

Solution

function amdahl(p, k) {
  // La portion accélérée passe de p à p/k ; le reste, (1 - p), ne bouge pas.
  // L'accélération est l'inverse du nouveau temps total.
  return 1 / ((1 - p) + p / k);
}

const SCENARIOS = [
  { nom: "60 % du temps, accéléré x10", p: 0.60, k: 10 },
  { nom: "60 % du temps, accéléré x1000", p: 0.60, k: 1000 },
  { nom: "2 % du temps, accéléré x100", p: 0.02, k: 100 },
  { nom: "95 % du temps, accéléré x8", p: 0.95, k: 8 },
];

for (const s of SCENARIOS) {
  const a = amdahl(s.p, s.k);
  const plafond = 1 / (1 - s.p);
  console.log(s.nom.padEnd(34) + "accélération x" + a.toFixed(2) +
              "   (plafond x" + plafond.toFixed(1) + ")");
}
// Deux enseignements. Accélérer x1000 au lieu de x10 sur la même portion ne
// fait passer que de 2,17 à 2,49 : on est déjà collé au plafond. Et une
// portion de 2 % accélérée x100 ne rapporte que 2 % — c'est l'optimisation
// que le programmeur choisit spontanément, et elle ne sert à rien.

function cycles(n, e, bulles) {
  const sequentiel = n * e;
  // Le premier résultat sort après e cycles de remplissage, puis un par
  // cycle pour les n - 1 suivants. Chaque bulle décale tout d'un cycle.
  const pipeline = e + (n - 1) + bulles;
  return { sequentiel, pipeline };
}

const N = 12, ETAGES = 5, BRANCHEMENTS = 3;

console.log("");
for (const [nom, tauxErreur] of [["prédiction parfaite", 0], ["une erreur sur deux", 0.5]]) {
  const bulles = BRANCHEMENTS * tauxErreur * 2;
  const r = cycles(N, ETAGES, bulles);
  console.log(
    nom.padEnd(22) +
    "séquentiel " + String(r.sequentiel).padStart(3) +
    " | pipeline " + String(r.pipeline).padStart(3) +
    " | CPI " + (r.pipeline / N).toFixed(2) +
    " | accélération x" + (r.sequentiel / r.pipeline).toFixed(2)
  );
}
// Le CPI idéal de 1 n'est jamais atteint : les 4 cycles de remplissage le
// tirent à 1,33 sur douze instructions seulement, et les branchements mal
// prédits ajoutent leurs bulles. Sur un programme long, le remplissage
// s'amortit — les branchements, non.

En travaux pratiques

Travaux pratiques 8 · 2 h

Ce que coûte de sortir du processeur

Mesurer le prix d'un appel système et d'un accès disque, pour ranger dans le bon ordre les grandeurs qui décident de la performance d'un programme réel.

Avant de commencer

  • Le TP 7 : ordres de grandeur des accès mémoire
  • strace, ou un équivalent traceur d'appels système

Énoncé

  1. Un octet à la foisÉcrivez un mégaoctet dans un fichier, octet par octet, avec l'appel système write. Chronométrez.
  2. Par blocsRecommencez en écrivant par blocs de 4096 octets. Chronométrez et calculez le rapport.
  3. Compter les appelsComptez les appels système des deux versions avec strace. Divisez le temps gagné par le nombre d'appels évités : vous obtenez le coût d'un appel système.
  4. Le tampon gratuitRefaites la version octet par octet, mais avec fputc au lieu de write. Chronométrez, comparez aux deux précédentes, et expliquez. Indice : La bibliothèque standard ne fait pas ce que vous croyez au moment où vous le croyez.
  5. Forcer l'écritureAjoutez un appel à fsync après chaque bloc, puis mesurez à nouveau. Expliquez l'effondrement, et dites quel programme accepte ce coût.
  6. La table des grandeursRassemblez vos mesures des TP 7 et 8 dans un seul tableau : accès L1, mémoire centrale, appel système, lecture disque, aller-retour réseau. Ramenez tout à la même unité.

C'est réussi quand

  • Vous mesurez le coût d'un appel système à quelques centaines de nanosecondes près
  • Vous savez expliquer pourquoi fputc ne coûte pas un appel système par caractère
  • Votre tableau de grandeurs couvre au moins six ordres de grandeur

Correction

Les trois versions
write octet par octet   : 2,84 s   1 048 576 appels système
write par blocs de 4 Ko : 0,004 s        256 appels système
fputc octet par octet   : 0,012 s        256 appels système

fputc n'est PAS un appel système : c'est une écriture dans un tampon en mémoire, vidé par un write quand il est plein. D'où le résultat qui surprend — la version « octet par octet » de la bibliothèque standard est 200 fois plus rapide que la version « octet par octet » de l'appel système. La différence entre une fonction de bibliothèque et un appel système n'est pas de style : elle vaut deux ordres de grandeur.

Le coût d'un appel système
(2,84 - 0,004) s / (1 048 576 - 256) appels ≈ 2,7 µs par appel

pourquoi : passage en mode noyau, changement de contexte,
vérification des arguments, retour en mode utilisateur
soit environ 8 000 cycles pour écrire UN octet

Deux microsecondes paraissent négligeables — jusqu'à ce qu'on en fasse un million. C'est le raisonnement à installer : un coût unitaire ne se juge jamais seul, toujours multiplié par la fréquence. La contre-mesure est toujours la même : regrouper. C'est ce que font le tampon, l'écriture par blocs et le DMA.

fsync, et qui le paie
sans fsync : 0,004 s
avec fsync : 3,10 s   (×775)

sans fsync, « écrit » signifie : le noyau a la donnée en cache
avec fsync, « écrit » signifie : le disque l'a réellement

Une base de données appelle fsync sur son journal avant de confirmer une transaction, et elle a raison : c'est ce qui la rend durable en cas de coupure. Un compilateur écrivant un fichier objet ne le fait pas, et il a raison aussi. Le compromis durabilité/débit se décide par usage, jamais par principe.

Le tableau des grandeurs
opération                      temps      à l'échelle d'une seconde
accès registre                0,3 ns     1 seconde
accès cache L1                  1 ns     3 secondes
accès mémoire centrale        100 ns     5 minutes
appel système                 2,7 µs     2,5 heures
lecture SSD (4 Ko)             80 µs     3 jours
aller-retour réseau local     500 µs     19 jours
lecture disque mécanique       10 ms      1 an

La colonne de droite est celle qui change la façon de programmer. Un accès mémoire raté coûte, à l'échelle du processeur, ce que coûterait à un humain d'attendre cinq minutes ; un accès disque, une année entière. Toute la conception des systèmes — caches, tampons, interruptions, DMA, asynchronisme — découle de ce seul tableau, et il vous suivra dans le cours de Systèmes d'exploitation.

Pourquoi le DMA existe

Si le processeur devait transférer lui-même chaque octet entre le disque et la mémoire, il passerait l'essentiel d'une lecture de 10 ms à recopier. Le contrôleur DMA le fait à sa place et l'avertit par une interruption quand c'est fini : le processeur exécute un autre programme pendant ce temps. C'est le même geste que le tampon de fputc — ne pas déranger l'unité la plus rapide pour chaque unité de travail de la plus lente — et c'est ce qui rend possible le multitâche du cours suivant.

Ce que la suite en fait

Ce chapitre clôt l'architecture, mais il ouvre le cours de systèmes d'exploitation, et de façon très directe. L'interruption d'horloge y devient l'ordonnancement préemptif du chapitre 4. Le contrôleur et ses registres y deviennent le pilote de périphérique du chapitre 8. Les entrées/ sorties mappées en mémoire y rencontrent la pagination du chapitre 6.

Et la disproportion par laquelle ce chapitre commençait — trois cents millions de cycles entre deux frappes — y trouve sa véritable réponse : elle ne se comble pas, elle se remplit, en donnant le processeur à un autre programme pendant l'attente. C'est le multitâche, et c'est tout le sujet du cours suivant.

À retenir

Flashcards · 5 cartes

Quelle est la différence entre les interruptions et le DMA, et pourquoi les deux existent-ils ?
Ils règlent deux coûts distincts. Les interruptions suppriment l'ATTENTE : le processeur fait autre chose et se fait rappeler à la fin de l'opération. Le DMA supprime le TRANSFERT : sans lui, le processeur copie encore chaque mot lui-même, soit des millions de transferts sur un gros fichier. Avec DMA, un contrôleur dédié fait la copie et une seule interruption signale la fin du bloc.
Pourquoi l'interruption est-elle bien plus qu'un mécanisme d'entrée/sortie ?
Parce qu'une interruption d'horloge périodique permet au système d'exploitation de reprendre la main à intervalles réguliers, donc de RETIRER le processeur à un programme qui ne le rend pas. C'est la condition matérielle du multitâche préemptif, et le fondement du chapitre 4 du cours de systèmes.
Pourquoi ne peut-on pas comparer deux processeurs sur leur fréquence ?
Parce que T = N_instructions × CPI / f comporte trois facteurs, relevant de trois responsables : le programme et le compilateur pour N, la microarchitecture pour le CPI, la technologie pour f. Un processeur à 2 GHz avec un CPI de 1 bat un processeur à 3 GHz avec un CPI de 2. Les MIPS ne comparent que des machines de même jeu d'instructions ; seule mesure honnête, le temps d'un programme réel représentatif.
Énoncez la loi d'Amdahl et sa conséquence pratique.
En accélérant d'un facteur k une portion représentant la fraction p du temps, l'accélération globale vaut 1/((1−p) + p/k). Quand k tend vers l'infini, elle plafonne à 1/(1−p) : ce qu'on n'accélère PAS fixe la limite. Conséquence : mesurer avant d'optimiser, et se méfier du multicœur — un programme parallélisable à 90 % ne dépassera jamais un facteur 10, quel que soit le nombre de cœurs.
Qu'apporte le pipeline, et quels sont les trois aléas qui le freinent ?
Il découpe l'exécution en étages travaillant simultanément sur des instructions différentes : la latence d'une instruction ne change pas, mais le DÉBIT est multiplié. Les aléas : de DONNÉES (une instruction attend un résultat pas encore écrit), de CONTRÔLE (après un branchement, on ignore quoi charger — d'où la prédiction de branchement), et STRUCTURELS (deux étages veulent la même ressource).