Cours 4 · Mémoire et périphériquesLeçon 2 sur 2
Entrées/sorties et performance
5 h de lecture9 sections Version PDF
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éthode | Attente | Transfert | Interruptions par bloc |
|---|---|---|---|
| Scrutation | processeur bloqué | par le processeur | 0 |
| Interruptions | processeur libre | par le processeur | 1 par mot |
| DMA | processeur libre | par le contrôleur | 1 par bloc |
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 ?
Mesurer la performance
Le chapitre 5 a donné la formule ; il est temps de s'en servir et d'en tirer les conséquences.
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 ; cette portion représente une fraction du temps total. L'accélération globale vaut :
Le passage à la limite est brutal. Si tend vers l'infini — la portion devient instantanée — l'accélération plafonne à . Ce qu'on n'accélère pas fixe le plafond.
Un exemple, avec et : on divise par dix les trois cinquièmes du temps, et l'accélération globale n'est que de . Même en rendant cette portion instantanée, on ne dépasserait jamais 2,5.
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.
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 ?
À 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.
Calculez l'accélération d'Amdahl, puis le CPI réel d'un pipeline à cinq étages.
// ── 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);
En travaux pratiques
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.
- Le TP 7 : ordres de grandeur des accès mémoire
- strace, ou un équivalent traceur d'appels système
- 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 blocs
Recommencez en écrivant par blocs de 4096 octets. Chronométrez et calculez le rapport.
- 3. Compter les appels
Comptez 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 gratuit
Refaites la version octet par octet, mais avec fputc au lieu de write. Chronométrez, comparez aux deux précédentes, et expliquez.
- 5. Forcer l'écriture
Ajoutez 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 grandeurs
Rassemblez 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é.
- 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
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
Vous avez parcouru les 9 sections.
Marquez-la terminée pour faire avancer votre parcours, ou revenez sur un point avant de passer à la suite.