Entrées/sorties et performanceDans le dialogue d’impression, choisissez « Enregistrer au format PDF » comme destination.
Retour

Architecture des ordinateurs · C4 Mémoire et périphériques · 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).