Votre service est « lié au CPU ». Les tableaux de bord le disent. Le CPU est à 80–90 %, la latence est mauvaise, et la première réaction de l’équipe est d’ajouter des cœurs.
Puis vous ajoutez des cœurs et rien ne s’améliore. Ou ça empire. Félicitations : vous venez de rencontrer le véritable boss — la mémoire.
Les caches CPU (L1/L2/L3) existent parce que les processeurs modernes peuvent faire des calculs plus vite que votre système ne peut leur fournir des données. La plupart des
défaillances de performances en production ne sont pas un « CPU lent ». Ce sont des « CPU qui attendent ». Cet article explique les caches sans langue de bois, puis montre
comment prouver ce qui se passe sur une machine Linux réelle avec des commandes que vous pouvez lancer aujourd’hui.
Pourquoi la mémoire gagne (et que le CPU attend surtout)
Les CPU sont absurdes. Un cœur moderne peut exécuter plusieurs instructions par cycle, spéculer, réordonner, vectoriser, et agir généralement comme un comptable surexcité faisant des
déclarations à 4 h du matin. Pendant ce temps, la DRAM est relativement lente. Le cœur peut clore des instructions en sous-nanosecondes ; un aller-retour vers la DRAM peut prendre
des dizaines à des centaines de nanosecondes selon la topologie, la contention et si vous êtes allé chercher de la mémoire NUMA distante.
Conséquence pratique : votre CPU passe beaucoup de temps bloqué sur des chargements mémoire. Pas sur le disque. Pas sur le réseau. Pas même sur du « code lent » au sens habituel.
Il attend la prochaine ligne de cache.
Les caches tentent de garder le CPU occupé en maintenant les données fréquemment utilisées à proximité. Ce n’est pas « agréable à avoir ». C’est la seule raison pour laquelle l’informatique
générale fonctionne aux fréquences actuelles. Si chaque chargement touchait la DRAM, vos cœurs passeraient la plupart des cycles à tourner en rond.
Voici le modèle mental qui résiste en production : la performance est dominée par la fréquence des ratés de cache et
le coût de ces ratés. Les ratés les plus coûteux sont ceux qui quittent le package du processeur et vont jusqu’à la DRAM, et les vraiment épicés sont les accès à la DRAM NUMA
distante via un interconnect pendant que d’autres cœurs se battent pour la bande passante.
Une règle empirique : quand votre chemin de requête touche « beaucoup de choses », le coût n’est pas l’arithmétique ; ce sont les suivis de pointeurs et les ratés de cache.
Et si vous le faites en concurrence avec de nombreux threads, vous pouvez transformer votre sous-système mémoire en goulot d’étranglement tandis que les graphes CPU vous mentent.
L1/L2/L3 en clair
Pensez aux niveaux de cache comme à des « garde-manger » de plus en plus grands et de plus en plus lents entre le cœur et la DRAM.
La dénomination est historique et simple : L1 est le plus proche du cœur, L2 vient ensuite, L3 est généralement partagé entre les cœurs sur un socket (pas toujours), puis vient la DRAM.
À quoi sert chaque niveau
- Cache L1 : minuscule et extrêmement rapide. Souvent séparé en L1i (instructions) et L1d (données). C’est le premier endroit où le cœur regarde.
- Cache L2 : plus grand, un peu plus lent, typiquement privé par cœur. Il récupère ce qui tombe hors du L1.
- Cache L3 : beaucoup plus grand, plus lent, souvent partagé entre cœurs. Il réduit les allers-retours vers la DRAM et agit comme un amortisseur de contention.
Ce que signifient opérationnellement « hit » et « miss »
Un hit de cache signifie que les données dont vous avez besoin sont déjà à proximité ; le chargement est satisfait rapidement et le pipeline continue.
Un miss de cache signifie que le CPU doit aller chercher ces données à un niveau inférieur. Si le miss atteint la DRAM, le cœur peut être fortement bloqué.
Les misses surviennent parce que les caches sont finis et parce que les charges réelles ont des schémas d’accès désordonnés. Le CPU essaie de prédire et de précharger, mais il ne peut pas tout prévoir—
surtout le code axé sur les pointeurs, l’accès aléatoire ou les structures de données plus grandes que le cache.
Pourquoi vous ne pouvez pas « simplement utiliser le L3 »
On parle parfois du L3 comme d’un pool partagé magique qui retiendra votre working set. Ce n’est pas le cas. L3 est partagé, sujet à la contention, et souvent inclusif ou partiellement inclusif
selon l’architecture. De plus, la bande passante et la latence du L3 restent meilleures que la DRAM, mais elles ont un coût.
Si le working set de votre charge est plus grand que le L3, vous irez en DRAM. Si c’est plus grand que la DRAM… eh bien, ça s’appelle le « swap », et c’est un appel à l’aide.
Lignes de cache, localité et la règle « vous l’avez touché, vous l’avez acheté »
Les CPU ne chargent pas des octets uniques en cache. Ils chargent des lignes de cache, généralement 64 octets sur x86_64. Quand vous chargez une valeur, vous ramenez souvent
des valeurs voisines aussi. C’est bien si votre code utilise la mémoire voisine (localité spatiale). C’est mauvais si vous ne vouliez qu’un champ et que le reste est du junk,
car vous venez de polluer le cache avec des choses que vous ne réutiliserez pas.
La localité est tout le jeu :
- Localité temporelle : si vous réutilisez bientôt, le cache aide.
- Localité spatiale : si vous utilisez la mémoire voisine, le cache aide.
Les bases de données, caches et routeurs de requêtes vivent ou meurent souvent selon la prévisibilité de leurs schémas d’accès. Les scans séquentiels peuvent être rapides parce que
les préchargeurs matériels suivent. Le suivi de pointeurs aléatoire dans une grande table de hachage peut être lent parce que chaque étape signifie « surprise, aller en mémoire ».
Traduction opérationnelle : si vous voyez un CPU élevé mais aussi beaucoup de cycles bloqués, vous n’avez pas un problème de « calcul ». Vous avez un problème de « nourrir le cœur ».
Votre chemin critique est probablement dominé par des misses de cache ou des mauvaises prédictions de branche, pas par des calculs.
Blague n°1 : les misses de cache sont comme les « questions rapides » dans le chat d’entreprise — chacune semble petite jusqu’à ce que vous réalisiez que votre journée entière attend sur elles.
Préchargement : la tentative utile du CPU
Les CPU essaient de détecter des motifs et de précharger les lignes de cache futures. Ça marche bien pour les accès en streaming et striés. Ça marche mal pour le suivi de pointeurs,
parce que l’adresse du prochain chargement dépend du résultat du précédent.
C’est pourquoi « j’ai optimisé la boucle » ne change parfois rien. La boucle n’est pas le problème ; c’est la chaîne de dépendances mémoire.
La partie que personne ne veut déboguer : cohérence et faux partage
Dans les systèmes multi‑cœurs, chaque cœur a ses propres caches. Quand un cœur écrit une ligne de cache, les copies des autres cœurs doivent être invalidées ou mises à jour pour que tout le monde
voie une vue cohérente. C’est la cohérence de cache. C’est nécessaire. C’est aussi un piège de performance.
Faux partage : quand vos threads se disputent une ligne de cache qu’ils ne « partagent » pas
Le faux partage survient lorsque deux threads mettent à jour des variables différentes qui se trouvent sur la même ligne de cache. Ils ne partagent pas logiquement les données, mais le protocole de
cohérence considère la ligne entière comme une unité. Ainsi chaque écriture déclenche des invalidations et des transferts de propriété, et vos performances plongent.
Au niveau des symptômes, cela ressemble à « plus de threads a ralenti » avec beaucoup de temps CPU dépensé, mais peu de progrès. Vous verrez beaucoup de trafic cache-à-cache et des misses de cohérence
si vous regardez avec les bons outils.
Blague n°2 : le faux partage c’est quand deux équipes « possèdent » la même cellule de feuille de calcul ; les modifications sont correctes, le processus ne l’est pas.
Les charges d’écriture paient plus cher
Les lectures peuvent être partagées. Les écritures nécessitent la propriété exclusive de la ligne, ce qui déclenche des actions de cohérence. Si vous avez un compteur chaud mis à jour par de nombreux threads,
le compteur devient un goulot sérialisé même si vous « avez beaucoup de cœurs ».
C’est pourquoi existent les compteurs par thread, les verrous éclatés et le batching. Vous n’êtes pas en train d’être fantaisiste. Vous évitez une facture physique.
NUMA : la taxe de latence que vous payez en montée en charge
Sur beaucoup de serveurs, la mémoire est physiquement attachée aux sockets CPU. Accéder à la mémoire « locale » est plus rapide qu’accéder à la mémoire attachée à un autre socket.
C’est NUMA (Non-Uniform Memory Access). Ce n’est pas un cas marginal. C’est le réglage par défaut sur beaucoup de matériel de production.
Vous pouvez vous permettre d’ignorer NUMA jusqu’à ce que vous ne le puissiez plus. Le mode d’échec apparait quand :
- vous répartissez les threads sur plusieurs sockets,
- votre allocateur étale les pages entre les nœuds,
- ou votre ordonnanceur migre des threads loin de leur mémoire.
Ensuite, la latence explose, le débit plafonne et le CPU semble « occupé » parce qu’il est bloqué. Vous pouvez facilement perdre des semaines à optimiser le code applicatif alors que la correction
consiste à pinner les processus, corriger la politique d’allocation, ou choisir moins de sockets avec des horloges plus élevées pour les charges sensibles à la latence.
Faits intéressants et histoire à répéter en réunion
- Le « mur de la mémoire » est devenu une préoccupation grand public dans les années 1990 : la vitesse des CPU a augmenté plus vite que la latence DRAM, rendant les caches indispensables.
- Les tailles de ligne de cache sont un choix de conception : 64 octets est courant sur x86, mais d’autres architectures ont utilisé des tailles différentes ; c’est un compromis entre bande passante et pollution.
- Le L1 est souvent séparé en caches d’instructions et de données parce que les mélanger provoque des conflits ; les flux de code et de données ont des motifs différents.
- Le partage du L3 est intentionnel : il aide quand des threads partagent des données majoritairement en lecture et réduit les allers-retours DRAM, mais crée aussi de la contention sous charge.
- Les préchargeurs matériels existent parce que l’accès séquentiel est fréquent ; ils peuvent accélérer considérablement les lectures en streaming sans changement de code.
- Les protocoles de cohérence (variantes de MESI) sont une grande raison pour laquelle le multi‑cœur « fonctionne simplement », mais ils imposent aussi des coûts réels sous contention d’écriture.
- Les TLB sont aussi des caches : la Translation Lookaside Buffer met en cache les traductions d’adresse ; les misses de TLB peuvent faire aussi mal que les misses de cache.
- Les huge pages réduisent la pression sur le TLB en mappant plus de mémoire par entrée ; elles peuvent aider certaines charges et nuire à d’autres.
- Les surprises de montée en charge multi‑cœur des années 2000 ont appris aux équipes que « plus de threads » n’est pas un plan de performance si la mémoire et le verrouillage ne sont pas gérés.
Playbook de diagnostic rapide
Quand un système est lent, vous voulez trouver la ressource limitante rapidement, pas écrire de la poésie sur la microarchitecture. Voici une checklist de terrain.
Première étape : confirmer si vous êtes lié au calcul ou bloqué
- Vérifiez l’utilisation CPU et les métriques liées au niveau d’exécution : file d’attente run, changements de contexte, pression IRQ.
- Cherchez les cycles bloqués / les misses de cache avec
perfsi possible. - Si les instructions par cycle sont faibles et que les misses de cache sont élevés, c’est probablement lié à la latence mémoire ou à la bande passante mémoire.
Deuxième étape : décider si c’est lié à la latence ou à la bande passante
- Latence : suivi de pointeurs, accès aléatoire, beaucoup de misses LLC, faible bande passante mémoire.
- Bande passante : streaming, grands scans, nombreux cœurs lisant/écrivant, bande passante mémoire proche des limites de la plateforme.
Troisième étape : vérifier NUMA et la topologie
- Les threads tournent‑ils sur un socket mais allouent sur un autre ?
- Faites‑vous du thrashing inter-socket du LLC ?
- La charge est‑elle sensible à la latence tail (elle l’est généralement), faisant de la mémoire distante un tueur silencieux ?
Quatrième étape : vérifier l’« évident mais ennuyeux »
- Y a‑t‑il du swap ou de la pression mémoire (storms de reclaim) ?
- Atteignez‑vous des limites mémoire de cgroup ?
- Saturez‑vous un verrou ou un compteur unique (faux partage, mutex contendu) ?
Idée paraphrasée (attribuée) : le message opérationnel de Gene Kim est que les boucles de rétroaction rapides battent l’héroïsme — mesurer d’abord, puis changer une chose à la fois.
Tâches pratiques : commandes, sorties et décisions
Celles‑ci sont destinées à être exécutées sur un hôte Linux où vous diagnostiquez des performances. Certaines nécessitent les droits root ou des permissions perf.
L’objectif n’est pas de mémoriser les commandes ; c’est de relier les sorties aux décisions.
Task 1: Identify cache sizes and topology
cr0x@server:~$ lscpu
Architecture: x86_64
CPU(s): 64
Thread(s) per core: 2
Core(s) per socket: 16
Socket(s): 2
L1d cache: 32K
L1i cache: 32K
L2 cache: 1M
L3 cache: 35.8M
NUMA node(s): 2
NUMA node0 CPU(s): 0-31
NUMA node1 CPU(s): 32-63
Ce que cela signifie : vous avez deux sockets, deux nœuds NUMA, et un L3 par socket (souvent). Votre working set qui déborde d’environ 36 Mo par socket
commence à payer le prix DRAM.
Décision : si le service est sensible à la latence, prévoyez une prise en compte de NUMA (pinning, politique mémoire) et gardez les structures de données chaudes petites.
Task 2: Verify cache line size (and stop guessing)
cr0x@server:~$ getconf LEVEL1_DCACHE_LINESIZE
64
Ce que cela signifie : les frontières de risque de faux partage sont de 64 octets.
Décision : dans le code bas niveau, alignez les compteurs/structs chauds par thread sur des frontières de 64B pour éviter le ping‑pong des lignes de cache.
Task 3: Confirm NUMA distances
cr0x@server:~$ numactl --hardware
available: 2 nodes (0-1)
node 0 cpus: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
node 0 size: 256000 MB
node 0 free: 120000 MB
node 1 cpus: 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
node 1 size: 256000 MB
node 1 free: 118000 MB
node distances:
node 0 1
0: 10 21
1: 21 10
Ce que cela signifie : la mémoire distante a une « distance » d’environ 2x. Pas littéralement 2x la latence, mais directionnellement significatif.
Décision : si vous êtes sensible à la latence tail, gardez les threads et leur mémoire locaux (ou réduisez le trafic inter-socket en limitant l’affinité CPU).
Task 4: Check if the kernel is fighting you with automatic NUMA balancing
cr0x@server:~$ cat /proc/sys/kernel/numa_balancing
1
Ce que cela signifie : le noyau peut migrer des pages pour « suivre » les threads. Parfois utile, parfois bruyant.
Décision : pour des charges stables et pinées, vous pouvez le désactiver (avec prudence, testé) ou imposer un placement explicite.
Task 5: Observe per-process NUMA memory placement
cr0x@server:~$ pidof myservice
24718
cr0x@server:~$ numastat -p 24718
Per-node process memory usage (in MBs) for PID 24718 (myservice)
Node 0 38000.25
Node 1 2100.10
Total 40100.35
Ce que cela signifie : le processus utilise principalement la mémoire du nœud 0. Si ses threads tournent sur le nœud 1, vous paierez des pénalités distantes.
Décision : alignez l’affinité CPU et la politique d’allocation mémoire ; si la répartition est accidentelle, corrigez l’ordonnancement ou le placement au démarrage.
Task 6: Check memory pressure and swapping (the performance cliff)
cr0x@server:~$ vmstat 1 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
3 0 0 1200000 80000 9000000 0 0 2 15 900 3200 45 7 48 0 0
5 0 0 1180000 80000 8900000 0 0 0 0 1100 4100 55 8 37 0 0
7 0 0 1170000 80000 8850000 0 0 0 0 1300 5200 61 9 30 0 0
Ce que cela signifie : pas de swap‑in/out (si/so = 0), donc vous n’êtes pas dans la catégorie « tout est terrible ». Le CPU est occupé, mais pas en attente d’IO.
Décision : passez à l’analyse cache/mémoire ; ne perdez pas de temps à accuser le disque.
Task 7: See if you’re bandwidth-bound (quick read on memory throughput)
cr0x@server:~$ sudo perf stat -a -e cycles,instructions,cache-references,cache-misses,LLC-loads,LLC-load-misses -I 1000 -- sleep 5
# time(ms) cycles instructions cache-references cache-misses LLC-loads LLC-load-misses
1000 5,210,000,000 2,340,000,000 120,000,000 9,800,000 22,000,000 6,700,000
2000 5,300,000,000 2,310,000,000 118,000,000 10,200,000 21,500,000 6,900,000
3000 5,280,000,000 2,290,000,000 121,000,000 10,500,000 22,300,000 7,100,000
Ce que cela signifie : instructions/cycle est relativement bas (environ 0,43 ici), et les misses de cache/LLC sont significatifs. Le CPU attend beaucoup.
Décision : traitez cela comme dominé par la latence mémoire à moins que les compteurs de bande passante n’indiquent une saturation ; cherchez des accès aléatoires, du pointer chasing ou NUMA.
Task 8: Identify top functions and whether they stall (profile with perf)
cr0x@server:~$ sudo perf top -p 24718
Samples: 2K of event 'cycles', Event count (approx.): 2500000000
18.50% myservice myservice [.] hashmap_lookup
12.20% myservice myservice [.] parse_request
8.90% libc.so.6 libc.so.6 [.] memcmp
7.40% myservice myservice [.] cache_get
5.10% myservice myservice [.] serialize_response
Ce que cela signifie : les hotspots sont des recherches/comparaisons — candidats classiques pour des misses de cache et des mauvaises prédictions de branche.
Décision : inspectez les structures de données : les clés sont‑elles dispersées ? poursuivez‑vous des pointeurs ? pouvez‑vous compacter les données ? réduire les comparaisons ?
Task 9: Check for scheduler migration (NUMA’s quiet enabler)
cr0x@server:~$ pidstat -w -p 24718 1 3
Linux 6.5.0 (server) 01/09/2026 _x86_64_ (64 CPU)
01:02:11 UID PID cswch/s nvcswch/s Command
01:02:12 1001 24718 1200.00 850.00 myservice
01:02:13 1001 24718 1350.00 920.00 myservice
01:02:14 1001 24718 1100.00 800.00 myservice
Ce que cela signifie : des changements de contexte élevés peuvent indiquer une contention sur des verrous ou trop de threads exécutables.
Décision : si la latence est en pics, réduisez le nombre de threads, enquêtez sur les verrous, ou pinnez les threads critiques pour réduire la migration.
Task 10: Check run queue and per-CPU saturation (don’t confuse “busy” with “progress”)
cr0x@server:~$ mpstat -P ALL 1 2
Linux 6.5.0 (server) 01/09/2026 _x86_64_ (64 CPU)
01:03:01 AM CPU %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle
01:03:02 AM all 62.0 0.0 9.0 0.1 0.0 0.5 0.0 0.0 0.0 28.4
01:03:02 AM 0 95.0 0.0 4.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0
01:03:02 AM 32 20.0 0.0 5.0 0.0 0.0 0.0 0.0 0.0 0.0 75.0
Ce que cela signifie : le CPU0 est saturé tandis que le CPU32 est plutôt inactif. Cela peut être un problème d’affinité, un shard chaud unique, ou un goulet de verrou.
Décision : si un seul cœur est chaud, la montée en charge ne fonctionnera pas tant que vous n’avez pas supprimé le funnel. Inspectez la distribution du travail par cœur et les verrous.
Task 11: Verify CPU affinity and cgroup constraints
cr0x@server:~$ taskset -pc 24718
pid 24718's current affinity list: 0-15
Ce que cela signifie : le processus est épinglé aux CPU 0–15 (un sous-ensemble d’un socket). Cela peut être volontaire ou accidentel.
Décision : si épinglé, assurez‑vous que la mémoire est locale à ce nœud ; si c’est accidentel, corrigez votre unité/systemd ou le CPU set de l’orchestrateur.
Task 12: Check LLC miss rate per process (perf stat on PID)
cr0x@server:~$ sudo perf stat -p 24718 -e cycles,instructions,LLC-loads,LLC-load-misses -- sleep 10
Performance counter stats for process id '24718':
18,320,000,000 cycles
7,410,000,000 instructions # 0.40 insn per cycle
210,000,000 LLC-loads
78,000,000 LLC-load-misses # 37.14% of all LLC hits
10.001948393 seconds time elapsed
Ce que cela signifie : un taux de miss LLC d’environ 37 % est un signal fort que votre working set ne tient pas dans le cache ou que l’accès est aléatoire.
Décision : réduisez le working set, augmentez la localité, ou changez la disposition des données. Validez aussi la localité NUMA.
Task 13: Spot page faults and major faults (TLB and paging hints)
cr0x@server:~$ pidstat -r -p 24718 1 3
Linux 6.5.0 (server) 01/09/2026 _x86_64_ (64 CPU)
01:04:10 UID PID minflt/s majflt/s VSZ RSS %MEM Command
01:04:11 1001 24718 8200.00 0.00 9800000 4200000 12.8 myservice
01:04:12 1001 24718 7900.00 0.00 9800000 4200000 12.8 myservice
01:04:13 1001 24718 8100.00 0.00 9800000 4200000 12.8 myservice
Ce que cela signifie : des faults mineurs élevés peuvent être normaux (paging à la demande, fichiers mmappés), mais si les faults montent sous charge cela peut être corrélé au churn de pages et à la pression sur le TLB.
Décision : si les faults se corrèlent avec des pics de latence, vérifiez le comportement de l’allocateur, l’usage de mmap, et envisagez les huge pages seulement après mesure.
Task 14: Validate transparent huge pages (THP) status
cr0x@server:~$ cat /sys/kernel/mm/transparent_hugepage/enabled
[always] madvise never
Ce que cela signifie : THP est toujours activé. Certaines bases de données l’adorent, certains services sensibles à la latence détestent le comportement d’allocation/compaction.
Décision : si vous observez des stalls périodiques, testez madvise ou never en staging et comparez la latence tail.
Task 15: Check memory bandwidth counters (Intel/AMD tooling varies)
cr0x@server:~$ sudo perf stat -a -e uncore_imc_0/cas_count_read/,uncore_imc_0/cas_count_write/ -- sleep 5
Performance counter stats for 'system wide':
8,120,000,000 uncore_imc_0/cas_count_read/
4,010,000,000 uncore_imc_0/cas_count_write/
5.001234567 seconds time elapsed
Ce que cela signifie : ces compteurs approximant les transactions DRAM ; si elles sont élevées et proches des limites de la plateforme, vous êtes lié à la bande passante.
Décision : si vous êtes limité par la bande passante, ajouter des cœurs n’aidera pas. Réduisez les données scannées, compressez, améliorez la localité ou rapprochez le travail des données.
Task 16: Identify lock contention (often misdiagnosed as “cache issues”)
cr0x@server:~$ sudo perf lock report -p 24718
Name acquired contended total wait (ns) avg wait (ns)
pthread_mutex_lock 12000 3400 9800000000 2882352
Ce que cela signifie : les threads passent du temps réel à attendre des verrous. Cela peut amplifier les effets de cache (les lignes de cache rebondissent avec la propriété du verrou).
Décision : réduisez la granularité des verrous, éclatez-les, ou changez l’algorithme. N’« optimisez pas la mémoire » si votre goulot est un mutex.
Task 17: Watch LLC occupancy and memory stalls (if supported)
cr0x@server:~$ sudo perf stat -p 24718 -e cpu/mem-loads/,cpu/mem-stores/ -- sleep 5
Performance counter stats for process id '24718':
320,000,000 cpu/mem-loads/
95,000,000 cpu/mem-stores/
5.000912345 seconds time elapsed
Ce que cela signifie : un trafic lourd de loads/stores suggère que le travail est centré sur la mémoire. Combinez avec les métriques de miss LLC pour décider si c’est cache-friendly.
Décision : si les loads sont nombreux avec des taux de miss élevés, concentrez‑vous sur la localité des structures et la réduction du pointer chasing.
Task 18: Validate that you’re not accidentally throttling (frequency matters)
cr0x@server:~$ cat /proc/cpuinfo | grep -m1 "cpu MHz"
cpu MHz : 1796.234
Ce que cela signifie : la fréquence CPU est relativement basse (possiblement économie d’énergie ou contraintes thermiques).
Décision : si la performance a régressé après un changement de plateforme, validez le governor CPU et les thermiques avant d’accuser les caches.
Trois mini-récits d’entreprise issus du terrain
Mini‑récit 1 : L’incident causé par une fausse hypothèse
Un service de paiements a commencé à générer des timeouts chaque jour à peu près à la même heure. L’équipe l’a appelé « saturation CPU » parce que les tableaux de bord montraient le CPU à 90 %,
et le flame graph mettait en avant le parsing JSON et du hashing. Ils ont fait ce que font les équipes : ajouté des instances, augmenté les pools de threads, relevé les limites d’autoscaling.
L’incident a empiré. Les queues de latence se sont allongées.
La fausse hypothèse était subtile : « CPU élevé signifie cœur occupé à calculer ». En réalité, les cœurs attendaient. perf stat montrait un IPC bas et un fort taux de miss LLC.
Le chemin de requête avait une recherche « d’enrichissement » cachée qui avait silencieusement grossi : plus de clés, plus de métadonnées, plus d’objets pointer-heavy, et un working set qui ne tenait plus près du L3.
Ensuite, le changement de scaling l’a poussé dans un nouveau mode d’échec. Plus de threads signifiait plus d’accès aléatoires en parallèle, ce qui a augmenté le parallélisme mémoire
mais aussi la contention. Le contrôleur mémoire a chauffé, la bande passante est montée, et la latence moyenne a suivi. Classique : plus vous poussez, plus le sous-système mémoire résiste.
La correction n’a pas été héroïque. Ils ont réduit l’overhead des objets, compacté les champs dans des tableaux contigus pour le hot path, et plafonné l’ensemble d’enrichissement par requête.
Ils ont aussi arrêté d’épingler le processus sur les deux sockets sans contrôler le placement mémoire. Une fois la localité améliorée, l’utilisation CPU est restée élevée,
mais le débit a augmenté et la latence tail a chuté. Les graphes CPU ressemblaient aux mêmes ; le comportement système était différent. Voilà la leçon.
Mini‑récit 2 : L’optimisation qui a échoué
Une équipe a voulu accélérer une API d’analytics en « améliorant le cache ». Ils ont remplacé un simple vecteur de structs par une table de hachage mappée par chaîne pour éviter les scans linéaires.
Les microbenchmarks sur un laptop semblaient excellents. La production a dit non.
La nouvelle structure a détruit la localité. L’ancien code scannait un tableau contigu : prédictible, favorable au prefetch, ami du cache. Le nouveau code faisait des recherches aléatoires,
chacune impliquant du pointer chasing, du hachage de chaînes et plusieurs chargements dépendants. Sur de vrais serveurs sous charge, cela a transformé une boucle majoritairement L2/L3-friendly
en une fête DRAM.
Pire, la table de hachage a introduit un chemin de redimensionnement partagé. Sous rafales, des redimensionnements ont eu lieu, des verrous ont été disputés, et les lignes de cache ont rebondi entre cœurs.
L’équipe a vu un CPU plus élevé et a conclu « il nous faut plus de CPU ». Mais le « plus de CPU » a augmenté la contention, et leur p99 s’est dégradé.
Ils ont roll-back, puis implémenté un compromis ennuyeux : garder un vecteur trié pour le hot path et faire des rebuilds occasionnels hors du thread de requête, avec un pointeur snapshot stable.
Ils ont accepté O(log n) avec bonne localité au lieu de O(1) avec des constantes terribles. La production est redevenue ennuyeuse, ce qui est le type de succès sur lequel on peut construire une carrière.
Mini‑récit 3 : La pratique ennuyeuse mais correcte qui a sauvé la journée
Un service adjacent au stockage — beaucoup de lectures de métadonnées, quelques écritures — a été migré vers une nouvelle plateforme matérielle. Tout le monde s’attendait à une amélioration.
Ce ne fut pas le cas. Il y avait des pics de latence sporadiques et des baisses de débit occasionnelles, sans rien d’évident : pas de swap, disques OK, réseau OK.
L’équipe avait une habitude qui les a sauvés : un « bundle de triage performance » qu’ils exécutent pour toute régression. Il incluait lscpu,
topologie NUMA, perf stat pour IPC et misses LLC, et un contrôle rapide de la fréquence CPU et des governors. Pas excitant. Fiable.
Le bundle a immédiatement montré deux surprises. Premièrement, les nouveaux hôtes avaient plus de sockets, et le service était ordonnancé à travers les sockets sans placement mémoire cohérent.
Deuxièmement, la fréquence CPU était plus basse sous charge soutenue à cause des réglages d’énergie de l’image de base.
La correction a été procédurale : ils ont mis à jour le baseline de tuning hôte (governor, réglages firmware quand pertinent), et ils ont épinglé le service à un seul nœud NUMA avec mémoire liée à ce nœud.
Aucun changement de code. La latence s’est stabilisée. Le déploiement s’est terminé. Le postmortem a été court, ce qui est un luxe.
Erreurs courantes (symptômes → cause racine → correction)
1) « Le CPU est élevé donc il nous faut plus de CPU »
Symptômes : CPU 80–95 %, débit stable, p95/p99 empirent en ajoutant threads/instances.
Cause racine : IPC faible dû aux misses de cache ou stalls mémoire ; le CPU est « occupé à attendre ».
Correction : mesurer IPC et misses LLC avec perf stat ; réduire le working set, améliorer la localité, ou corriger le placement NUMA. Ne pas augmenter les threads aveuglément.
2) « Une table de hachage est toujours plus rapide qu’un scan »
Symptômes : plus lent après passage à une structure « O(1) » ; perf montre des hotspots en hashing/strcmp/memcmp.
Cause racine : accès aléatoire et pointer chasing provoquent des allers-retours DRAM ; la mauvaise localité bat le grand‑O sur du matériel réel.
Correction : privilégier des structures contiguës pour les hot paths (tableaux, vecteurs, vecteurs triés). Benchmarkez avec des jeux de données et de la concurrence proches de la production.
3) « Plus de threads = plus de débit »
Symptômes : le débit s’améliore puis s’effondre ; les changements de contexte augmentent ; les misses LLC grimpent.
Cause racine : saturation de la bande passante mémoire, contention de verrous, ou faux partage domine.
Correction : plafonnez le nombre de threads près du « knee » de la courbe ; éclatez les verrous/compteurs ; évitez les écritures partagées chaudes ; épinglez les threads si sensible à NUMA.
4) « NUMA n’a pas d’importance ; Linux gère »
Symptômes : bonne latence moyenne, latence tail catastrophique ; régressions en changeant vers des hôtes multi-socket.
Cause racine : accès mémoire distant et trafic inter-socket ; la migration de l’ordonnanceur casse la localité.
Correction : utilisez numastat et numactl ; épinglez CPU et mémoire ; envisagez d’exécuter un processus par socket pour la prévisibilité.
5) « Si on désactive les caches, on teste le pire cas »
Symptômes : quelqu’un propose de désactiver les caches ou de vider constamment comme stratégie de test.
Cause racine : incompréhension ; les systèmes modernes ne sont pas conçus pour ce mode et les résultats ne se transposeront pas à la réalité.
Correction : testez avec des working sets réalistes et des schémas d’accès ; utilisez les compteurs de profiling, pas des expériences de foire scientifique.
6) « Les huge pages aident toujours »
Symptômes : THP activé et stalls périodiques ; activité de compaction ; pics de latence pendant la croissance mémoire.
Cause racine : overhead d’allocation/compaction THP ; inadéquation avec les schémas d’allocation.
Correction : benchmarquez always vs madvise vs never ; si vous utilisez des huge pages, allouez‑les à l’avance et surveillez la latence tail.
Listes de contrôle / plan pas à pas
Checklist A: Prouver que c’est la mémoire, pas le calcul
- Capturez la topologie CPU :
lscpu. Enregistrez sockets/NUMA et tailles de cache. - Vérifiez le swap/la pression mémoire :
vmstat 1. Sisi/so> 0, réglez la mémoire d’abord. - Mesurez IPC et misses LLC :
perf stat(système ou PID). IPC bas + misses LLC élevés = suspicion de stall mémoire. - Cherchez les fonctions chaudes :
perf top. Si les hotspots sont lookup/compare/alloc, attendez des problèmes de localité.
Checklist B: Décider si c’est latence ou bande passante
- Si le taux de miss LLC est élevé mais que les compteurs de bande passante sont modérés : le pointer chasing lié à la latence est probable.
- Si les compteurs de bande passante sont proches des limites de la plateforme et que les cœurs n’aident pas : scan/stream lié à la bande passante est probable.
- Changez une chose et re‑mesurez : réduisez la concurrence, réduisez le working set, ou changez le schéma d’accès.
Checklist C: Corriger NUMA avant de réécrire le code
- Mappez les nœuds NUMA :
numactl --hardware. - Vérifiez la mémoire du processus par nœud :
numastat -p PID. - Vérifiez l’affinité CPU :
taskset -pc PID. - Alignez : épinglez les CPUs à un nœud et liez la mémoire au même nœud (testez en staging d’abord).
Checklist D: Rendre les données cache‑friendly (l’ennuyeux gagne)
- Aplatissez les structures pointer‑heavy dans les hot paths.
- Compressez les champs chauds ensemble ; séparez les champs froids (split hot/cold).
- Privilégiez tableaux/vecteurs et itération prédictible plutôt que l’accès aléatoire.
- Éclatez les compteurs write‑heavy ; regroupez les mises à jour.
- Benchmarkez avec des tailles proches de la production ; les effets cache apparaissent quand les données sont assez grandes pour importer.
Foire aux questions
1) Le L1 est‑il toujours plus rapide que le L2, et le L2 toujours plus rapide que le L3 ?
Généralement oui en termes de latence, mais la performance réelle dépend de la contention, du motif d’accès, et de si la ligne est déjà présente grâce au préchargement.
De plus, les caractéristiques de bande passante diffèrent ; le L3 peut offrir une bande passante agrégée élevée mais une latence supérieure.
2) Pourquoi mon CPU affiche 90 % d’utilisation s’il « attend la mémoire » ?
Parce que « utilisation CPU » signifie surtout que le cœur n’est pas inactif. Un pipeline bloqué exécute encore des instructions, gère des misses, fait de la spéculation,
et brûle des cycles. Vous avez besoin de compteurs (IPC, misses, cycles bloqués) pour voir l’attente.
3) Quelle est la différence entre le cache CPU et le cache de pages Linux ?
Les caches CPU sont gérés par le matériel et sont petits (Ko/Mo). Le cache de pages Linux est géré par l’OS, utilise la DRAM, et met en cache des données de fichiers (Go).
Ils interagissent, mais résolvent des problèmes à des échelles différentes.
4) Puis‑je « augmenter le L3 » par le logiciel ?
Pas littéralement. Ce que vous pouvez faire, c’est agir comme si vous aviez plus de cache en réduisant votre working set chaud, en améliorant la localité, et en évitant la pollution du cache.
5) Pourquoi les listes chaînées et les arbres pointer‑heavy sont‑ils mauvais ?
Ils détruisent la localité spatiale. Chaque pointeur mène à une ligne de cache différente, souvent loin. Cela signifie des chargements dépendants et des allers‑retours fréquents vers la DRAM,
qui bloquent le cœur.
6) Quand devrais‑je me soucier du faux partage ?
Quand plusieurs threads mettent à jour des champs/counters distincts dans des boucles serrées et que les performances empirent avec plus de threads.
C’est courant dans les compteurs métriques, les buffers circulaires et les « tableaux d’état par connexion » naïfs.
7) Les misses de cache sont‑ils toujours mauvais ?
Certains misses sont inévitables. La question est de savoir si votre charge est structurée pour amortir les misses (streaming) ou si elles sont catastrophiques (charges dépendantes aléatoires).
Vous optimisez pour réduire les misses sur le hot path, pas pour atteindre un mythique « zéro miss ».
8) Des CPUs plus rapides résolvent‑ils les problèmes mémoire ?
Parfois ils les aggravent. Des cœurs plus rapides peuvent exiger des données plus vite et heurter le mur mémoire plus rapidement. Une plateforme avec meilleure bande passante mémoire,
meilleure topologie NUMA ou caches plus larges compte souvent plus que le simple GHz.
9) Dois‑je tout épingler sur un socket ?
Pour les services sensibles à la latence, épingler sur un socket (et binder la mémoire) peut être un gros gain : localité prévisible, moins d’accès distants.
Pour les jobs à haut débit, répartir sur les sockets peut aider — si vous maintenez la localité et évitez les hotspots d’écriture partagés.
10) Quelle métrique surveiller sur les dashboards pour détecter tôt les problèmes de cache ?
Si possible, exportez IPC (instructions par cycle) et taux de miss LLC ou cycles bloqués depuis perf/PMU. Sinon, surveillez le motif :
CPU monte, débit stable, latence augmente lors du scaling. Ce motif crie « mémoire ».
Conclusion : que faire la semaine prochaine
Les caches CPU ne sont pas anecdotiques. Ce sont la raison pour laquelle un changement « simple » peut casser le p99 et pourquoi ajouter des cœurs ajoute souvent de la déception.
La mémoire gagne parce qu’elle fixe le rythme : si votre cœur ne peut pas obtenir les données bon marché, il ne peut pas faire du travail utile.
Prochaines étapes pratiques :
- Ajoutez
perf stat(IPC + misses LLC) à votre trousse d’incident standard pour les pages « liées CPU ». - Documentez la topologie NUMA par classe d’hôte et décidez si les services doivent être pinés (et comment) par défaut.
- Auditez les hot paths pour la localité : aplatissez les structures, séparez hot/cold fields, et évitez les hotspots d’écritures partagées.
- Benchmarkez avec des tailles de jeu de données réalistes. Si votre benchmark tient dans le L3, ce n’est pas un benchmark ; c’est une démo.
- Quand on propose une optimisation, posez d’abord une question : « Qu’est‑ce que ça fait aux misses de cache et au trafic mémoire ? »