Skip to content

Niveau 0 · Chapitre 0.5

Pourquoi mon ordinateur est-il lent ?

Les raisons habituelles de la lenteur d’un ordinateur, rattachées aux niveaux qui les causent : trop de calcul, attente de la mémoire, manque de RAM, attente du stockage ou du réseau, chaleur et concurrence, chacune mesurée, avec les outils pour les distinguer.

« L’ordinateur est lent » peut vouloir dire une douzaine de choses différentes. Un programme fait peut-être trop de travail, ou attend la mémoire, ou le disque, ou un serveur à l’autre bout du monde. La machine manque peut-être de mémoire et passe son temps à déplacer des pages, ou elle est trop chaude et tourne à une fréquence réduite, ou elle est simplement partagée entre trop de programmes. Chacun de ces cas a une cause différente, à un niveau différent, et un remède différent.

Ce chapitre passe en revue les suspects habituels, mesure chacun sur le Mac sur lequel il a été écrit (un Apple M2 Ultra avec 16 cœurs performance, 8 cœurs efficacité et 64 Go de mémoire), et montre comment les distinguer.

Quatre ressources

Presque tous les ralentissements se ramènent à un programme qui attend l’une de quatre choses :

RessourceLe programme…Ce qu’on voitOù c’est expliqué
CPUcalcule, et il y a trop à calculer% CPU élevépipeline, exécution dans le désordre
mémoireattend que les données arrivent de la RAM% CPU élevé, mais peu de travail faitcaches
capacité de RAMattend que des pages soient décompressées ou relues depuis le disquepression mémoire, swap utilisémémoire virtuelle
stockage et réseauest bloqué dans un appel système, en attente d’E/S% CPU faible, et pourtant l’application caleappels système, chargement d’une page web

La première étape est toujours de regarder. Sous macOS, le Moniteur d’activité montre le CPU, la mémoire, l’énergie, le disque et le réseau par processus ; top montre la même chose dans un terminal. Son en-tête, relevé pendant l’écriture de ce chapitre :

Processes: 1231 total, 12 running, 1219 sleeping, 10197 threads
Load Avg: 11.54, 14.44, 13.97
CPU usage: 30.48% user, 9.64% sys, 59.86% idle
PhysMem: 58G used (6043M wired, 8978M compressor), 5373M unused.
VM: 19165(0) swapins, 461956(0) swapouts.

La charge moyenne (load average) est le nombre moyen de fils d’exécution en cours d’exécution ou prêts à s’exécuter sur les 1, 5 et 15 dernières minutes. Ici entre 12 et 14 environ, pour 24 cœurs : occupé, mais avec de la marge. Sous Linux, les mêmes outils existent (top, htop, vmstat, free), et sous Windows, le Gestionnaire des tâches et le Moniteur de ressources.

Limité par le CPU : trop de travail

Un programme est limité par le CPU (CPU-bound) quand sa vitesse dépend de la vitesse à laquelle les cœurs exécutent ses instructions. Le remède est de faire moins de travail (un meilleur algorithme, comme dans le chapitre sur la complexité) ou de mettre plus de cœurs dessus en même temps.

Ce second remède a ses limites. Un programme à un seul fil d’exécution utilise un cœur, quel que soit le nombre dont dispose la machine : dans le Moniteur d’activité il apparaît à 100 %, c’est-à-dire un cœur plein, pendant que les 23 autres ne font rien. Et quand il y a plus de fils occupés que de cœurs, ils se relaient. Ce test fait tourner une boucle purement arithmétique (sans aucun trafic mémoire) dans plusieurs processus à la fois, et mesure le temps jusqu’à ce que tous aient fini :

Copies en coursTemps écoulé (deux essais)
10,75 s
160,86–0,89 s
241,14–1,19 s
482,03–2,07 s

Jusqu’à 16 copies, chacune a son propre cœur performance et le temps bouge à peine. De 17 à 24, les copies supplémentaires atterrissent sur les cœurs efficacité, plus lents, et tout le monde attend les retardataires. À 48 copies, deux fois le nombre de cœurs, l’ordonnanceur partage chaque cœur entre deux processus, et tout prend 2,7 fois plus longtemps que seul. C’est la concurrence pour les ressources (contention) : une tâche de fond qui occupe tous les cœurs (une sauvegarde, une indexation, une compilation, un onglet de navigateur qui exécute un script lourd) ralentit tout le reste, même si le programme que vous utilisez n’a rien d’anormal.

Limité par la mémoire : occupé, mais en attente

Un programme peut afficher 100 % de CPU et passer pourtant l’essentiel de son temps à attendre : la mémoire. Un cœur peut exécuter plusieurs instructions par nanoseconde, mais un chargement qui rate tous les caches doit aller jusqu’à la DRAM. Ce test suit une chaîne de pointeurs à travers un bloc de mémoire dans un ordre aléatoire, si bien que l’adresse de chaque chargement dépend du précédent et que rien ne peut être prédit ni recouvert. Le temps par chargement, selon la taille du bloc :

Ensemble de travailTemps par chargementServi par
16–64 Kio1,2 nsle cache L1 (128 Kio par cœur)
1 Mio6,2–6,3 nsle cache L2 (16 Mio, partagé par 4 cœurs)
8 Mio10–12 nstoujours le L2, mais plus lent à mesure qu’il se remplit
32 Mio79–81 nsen partie au-delà des caches
128 Mio – 1 Gio129–137 nsla DRAM

Du plus petit cache à la mémoire principale, la même instruction devient environ 100 fois plus lente. Le chapitre sur les caches explique pourquoi ; voici à quoi cela ressemble du côté de l’application. Ce programme additionne deux fois les mêmes 64 millions d’entiers (256 Mio), avec un code identique : une fois en les lisant dans l’ordre de la mémoire, une fois à travers un index mélangé.

for (size_t i = 0; i < n; i++) s1 += a[seq[i]];   /* seq = 0, 1, 2, 3, … */
for (size_t i = 0; i < n; i++) s2 += a[idx[i]];   /* idx = a shuffled 0 … n-1 */
Ordre d’accèsTempsPar élément
dans l’ordre14 ms0,2 ns
mélangé258–269 ms3,9–4,0 ns

Mêmes additions, même résultat, environ 19 fois plus lent. La boucle mélangée reste bien plus rapide que 130 ns par élément parce que ses chargements ne dépendent pas les uns des autres : un cœur à exécution dans le désordre garde de nombreux défauts de cache en vol en même temps. Mais les deux boucles se ressemblent trait pour trait dans le Moniteur d’activité : un cœur à 100 %. Le taux d’occupation du CPU ne dit pas si le CPU calcule ou s’il attend. Les profileurs qui lisent les compteurs de performance du matériel (Instruments sous macOS, perf sous Linux, VTune chez Intel) savent faire la différence en comptant les défauts de cache et les cycles de blocage.

Ce qui compte, c’est l’ensemble de travail (working set) : les données qu’un programme utilise sur une courte période. S’il tient dans un cache, l’accès est rapide ; s’il est ne serait-ce qu’un peu trop gros, l’ensemble de travail entier peut sortir du cache à chaque passage. La démo ci-dessous le rend visible avec un cache qui a de la place pour 16 éléments, et garde les plus récemment utilisés, et un programme qui parcourt ses données quatre fois :

La falaise du cache

À essayer : Choisissez combien d’éléments le programme utilise, puis regardez-le les parcourir quatre fois. Le cache (l’étagère) en contient 16 : vert, c’est un succès ; rouge, un détour par la mémoire lente.

Éléments utilisés :
Les données, en mémoire principale
0123456789101112131415
Le cache : 16 places
passage 1/40 succès · 0 échecs

Avec 16 éléments, le premier passage rate 16 fois et les trois suivants trouvent tout : un taux de succès de 75 %. Avec 17, un élément de plus que la place disponible, le taux de succès tombe à zéro : quand le programme revient à un élément, le cache vient de l’évincer pour faire de la place aux autres. Les performances ne se dégradent pas doucement quand un ensemble de travail dépasse la taille d’un cache : elles tombent d’une falaise.

À court de mémoire : compression et swap

La même falaise existe un niveau plus bas, entre la RAM et le stockage. Quand les ensembles de travail cumulés des programmes ne tiennent plus en mémoire physique, le système d’exploitation doit faire de la place. Les premiers systèmes à temps partagé évinçaient des processus entiers toutes les centaines de millisecondes environ, et le modèle de l’ensemble de travail de Denning décidait quoi garder. Le modèle a survécu ; l’éviction de processus entiers, non. Les systèmes modernes évincent des pages individuelles, les moins récemment utilisées d’abord, et avant d’écrire quoi que ce soit sur le disque, macOS, Windows 10 et ses successeurs, et beaucoup de configurations Linux les compressent en mémoire.

vm_stat sur ce Mac montre le compresseur à l’œuvre (les pages font 16 Kio) :

Pages stored in compressor:                  2436936.
Pages occupied by compressor:                 574584.
Swapins:                                       19165.
Swapouts:                                     461956.

37,2 Gio de contenu mémoire tassés dans 8,8 Gio, un rapport de 4,2 pour 1. Décompresser une page coûte quelques microsecondes ; la relire sur le stockage coûte davantage ; la recalculer ou la retélécharger coûte bien plus encore. Au-delà, près de 7 Go d’espace d’échange (swap) sur le SSD étaient utilisés :

$ sysctl vm.swapusage
vm.swapusage: total = 8192.00M  used = 6953.88M  free = 1238.12M  (encrypted)

Rien de tout cela n’est un problème tant que les pages évincées restent inutilisées : de la mémoire qui contient un onglet que vous n’avez pas regardé depuis une semaine. Cela en devient un quand les ensembles de travail eux-mêmes ne tiennent pas : chaque accès à une page évincée est un défaut de page qui attend le disque, et les pages ramenées chassent d’autres pages dont on aura besoin un instant plus tard. C’est l’écroulement (thrashing) : le disque est occupé, le CPU est inactif, et tout se traîne. Le graphique de pression de la mémoire du Moniteur d’activité passe au jaune, puis au rouge, et en ligne de commande vm_stat montre les relectures depuis le swap qui grimpent. Le remède est d’utiliser moins de mémoire à la fois (fermer quelque chose) ou d’en avoir davantage.

Attendre le stockage et le réseau

Un programme qui attend un disque ou un serveur n’utilise pas du tout le CPU. Il est bloqué dans un appel système (read, recv, connect), et l’ordonnanceur a donné son cœur à quelqu’un d’autre. L’occupation du CPU est faible, et pourtant l’application ne répond pas, surtout si l’attente a lieu sur son fil principal, celui qui fait tourner sa boucle d’événements.

Les coûts s’étalent sur plusieurs ordres de grandeur. Lire un bloc aléatoire de 4 Kio dans un fichier de 2 Gio sur ce Mac :

D’où venaient les donnéesTemps par lecture
le SSD (cache de fichiers contourné avec F_NOCACHE), médiane80–140 µs
le cache de fichiers du système, en RAM2,2 µs
pour comparaison : un chargement depuis la DRAM0,13 µs

Le chiffre du SSD bouge avec l’état du disque et la charge de la machine : le chapitre disques durs et SSD a mesuré une médiane de 80–85 µs, et des essais répétés pour ce chapitre, sur une machine plus chargée, ont donné 101–140 µs. Ce chapitre montre aussi qu’un SSD abat bien plus de travail quand de nombreuses lectures sont en vol en même temps.

Un disque dur à plateaux a besoin de plusieurs millisecondes pour déplacer sa tête et attendre que le plateau tourne, environ cent fois plus que ce SSD, c’est pourquoi le disque dur d’un vieux portable était si souvent « la » raison de sa lenteur. Et un aller-retour réseau, comme l’a mesuré le chapitre sur les pages web, coûte environ 6 ms vers un serveur proche et bien plus vers un serveur lointain. Une application qui fait cinquante requêtes successives vers un serveur à 100 ms a besoin de cinq secondes, quel que soit le CPU.

Les onglets Disque et Réseau du Moniteur d’activité montrent quels processus déplacent des données. Un processus qui consomme peu de CPU mais beaucoup de disque ou de réseau, ou un pic général de l’un ou de l’autre, oriente vers cette piste.

Mises côte à côte, ces attentes couvrent plus de sept ordres de grandeur. Étirées à l’échelle humaine, les écarts sautent aux yeux : si une lecture dans le cache L1 prenait une seconde, un aller-retour vers la mémoire principale prendrait presque deux minutes, une lecture sur le SSD environ une journée, et le chargement d’une page web presque une année.

Échelle des attentes

À essayer : Passez à « Si L1 prenait 1 seconde » pour étirer chaque attente à l’échelle humaine, et à « Linéaire » pour voir les lentes écraser les autres. Cliquez sur une ligne pour la comparer à un accès au cache.

En vert, les lectures en mémoire ; en orange, les lectures de fichiers ; en rose, les attentes que l’on remarque à l’écran.

La chaleur : le bridage thermique

La puissance d’une puce croît avec sa fréquence d’horloge et, plus vite encore, avec sa tension d’alimentation, et les fréquences plus hautes demandent des tensions plus hautes ; le chapitre sur les limites du calcul en donne la formule. Toute cette puissance devient de la chaleur. Quand la puce chauffe trop, le micrologiciel de gestion d’énergie abaisse la fréquence et la tension jusqu’à ce que la chaleur puisse être évacuée : c’est le bridage thermique (thermal throttling). Un portable fin et sans ventilateur, sous une charge longue et lourde, tourne moins vite au bout de quelques minutes qu’au début ; un ordinateur de bureau doté de gros ventilateurs, comme celui-ci, bride rarement.

macOS enregistre les événements thermiques, et ici il n’y en avait aucun :

$ pmset -g therm
Note: No thermal warning level has been recorded
Note: No performance warning level has been recorded
Note: No CPU power status has been recorded

Les portables ralentissent aussi volontairement pour économiser la batterie : les modes basse consommation plafonnent les fréquences, et le travail de fond est orienté vers les cœurs efficacité. Un banc d’essai lancé sur batterie, à chaud, ou juste après une autre tâche lourde peut donner autre chose que le même banc lancé à froid, sur secteur.

Une liste de vérification

SymptômeCause probableOù regarder
un processus à 100 % (un cœur), le reste inactiftravail à un seul fil, limité par le CPUonglet CPU du Moniteur d’activité ; un profileur
tous les cœurs occupéstrop de tâches à la fois : concurrencecharge moyenne ; quels processus utilisent le CPU
CPU élevé, mais plus lent que prévulimité par la mémoire : défauts de cacheun profileur avec compteurs matériels
pression mémoire élevée, swap qui grossitensembles de travail plus gros que la RAMonglet Mémoire ; vm_stat
CPU faible, application figée ou curseur qui tournebloquée sur le disque ou le réseau, sur le fil principalonglets Disque et Réseau ; un échantillon du processus
rapide au début, plus lent après quelques minutesbridage thermiquepmset -g therm ; températures

À retenir

  • La lenteur, c’est presque toujours de l’attente : des cœurs, de la mémoire, de la RAM à libérer, du stockage ou du réseau.
  • Le travail limité par le CPU est borné par les cœurs : un programme à un seul fil en utilise un sur 24 ; 48 processus occupés sur 24 cœurs prennent 2,7 fois plus longtemps.
  • Le travail limité par la mémoire apparaît à 100 % de CPU mais attend : un chargement coûte 1,2 ns depuis le L1 et environ 130 ns depuis la DRAM, et additionner 256 Mio dans un ordre mélangé a pris 19 fois plus longtemps que dans l’ordre.
  • Les performances tombent d’une falaise quand un ensemble de travail dépasse son cache ou la RAM : dans la démo, un seul élément de trop fait passer le taux de succès de 75 % à 0 %.
  • Quand la RAM manque, les systèmes modernes compressent d’abord (4,2 pour 1 ici), puis évincent vers le swap ; l’écroulement commence quand les ensembles de travail eux-mêmes ne tiennent plus.
  • Les attentes d’E/S n’apparaissent pas comme de l’utilisation CPU : une lecture aléatoire sur le SSD a pris environ 100 µs, une lecture en cache 2,2 µs, un aller-retour réseau environ 6 ms.
  • La chaleur et les limites de puissance abaissent la fréquence : une machine peut ralentir sous une charge prolongée.

Dans ce niveau

  1. 0.1Ce qui se passe quand on ouvre une application
  2. 0.2De la touche au pixel
  3. 0.3Ce qui se passe quand une page web se charge
  4. 0.4Applications, processus et fichiers
  5. 0.5Pourquoi mon ordinateur est-il lent ?