2 points par GN⁺ 2026-01-08 | 1 commentaires | Partager sur WhatsApp
  • Une validation expérimentale du déséquilibre entre les performances d’I/O et la vitesse de traitement du CPU récemment discuté montre qu’en pratique, le CPU reste la principale contrainte
  • La vitesse de lecture séquentielle atteint 1,6 Go/s avec un cache froid et 12,8 Go/s avec un cache chaud, mais le calcul de fréquence des mots sur un seul thread reste autour de 278 Mo/s
  • La structure en branches du code empêche la vectorisation, et même une simple optimisation de conversion en minuscules n’améliore les performances qu’à environ 330 Mo/s
  • Même la commande wc -w ne atteint que 245 Mo/s, ce qui confirme que le calcul CPU et le traitement des branches, plutôt que le disque, constituent le goulot d’étranglement
  • Une vectorisation manuelle basée sur AVX2 a permis d’atteindre 1,45 Go/s, mais cela ne représente encore qu’environ 11 % de la vitesse de lecture séquentielle, ce qui prouve que le CPU, et non l’I/O, est le goulot d’étranglement

Comparaison entre vitesse d’I/O et performances CPU

  • En suivant l’affirmation de Ben Hoyt, une expérience a été menée pour vérifier si la récente hausse de la vitesse de lecture séquentielle avait dépassé la stagnation des performances CPU
    • Mesuré de la même manière, le résultat est de 1,6 Go/s avec cache froid et 12,8 Go/s avec cache chaud
  • Pourtant, le calcul de fréquence des mots sur un seul thread n’atteint que 278 Mo/s
    • Même avec un cache chaud, cela représente environ un cinquième de la vitesse de lecture du disque

Expérience de calcul de fréquence des mots en C

  • Avec GCC 12, optimized.c a été compilé avec les options -O3 -march=native, puis exécuté sur un fichier d’entrée de 425 Mo
    • Résultat : 1,525 seconde, soit un débit de 278 Mo/s
  • Les multiples branches dans le code et les sorties anticipées empêchent les optimisations de vectorisation du compilateur
    • Après avoir déplacé la logique de conversion en minuscules hors de la boucle, le débit est monté à 330 Mo/s
    • Avec Clang, la vectorisation est mieux réalisée

Comparaison avec un simple comptage de mots (wc -w)

  • Exécution de la commande wc -w, qui compte seulement le nombre de mots au lieu de calculer leur fréquence
    • Résultat : 245,2 Mo/s, plus lent que prévu
  • wc traite divers caractères d’espacement comme ' ', '\n', '\t', ainsi que des caractères dépendants de la locale
    • Il effectue donc davantage d’opérations qu’un code qui ne distingue que l’espace simple

Tentative de vectorisation basée sur AVX2

  • Mise en œuvre d’une vectorisation avec le jeu d’instructions AVX2 en exploitant les fonctionnalités des CPU récents
    • Utilisation de registres 256 bits et alignement des données sur 32 bits
    • Utilisation de l’instruction VPCMPEQB pour comparer les caractères d’espacement
  • Détection des frontières de mots à l’aide d’un masque de bits (PMOVMSKB) et de l’instruction Find First Set (ffs)
    • Inspiré de l’implémentation de strlen dans Cosmopolitan libc

Résultats de performance et conclusion

  • Le code vectorisé manuellement (wc-avx2) atteint un débit de 1,45 Go/s
    • Vérification effectuée avec le même résultat que wc -w (82,113,300 mots)
  • Même avec un cache froid, le temps de calcul en mode user reste dominant
    • Cela confirme que le calcul CPU est le goulot d’étranglement, plus encore que l’I/O disque
  • Globalement, la vitesse du disque est suffisante, mais le traitement des branches et les calculs de hachage côté CPU restent les facteurs limitants
  • Le code et les résultats expérimentaux sont publiés sur GitHub (haampie/wc-avx2)

1 commentaires

 
GN⁺ 2026-01-08
Réactions sur Hacker News
  • Je pense que la limite de performance des CPU modernes est déterminée par la quantité de données qu’un seul cœur peut traiter, c’est-à-dire la vitesse de memcpy()
    La plupart des cœurs x86 tournent autour de 6 Go/s, tandis que la série Apple M se situe vers 20 Go/s
    Les chiffres comme « 200 Go/s » mis en avant dans le marketing ne représentent que la bande passante agrégée de tous les cœurs ; un cœur seul reste encore proche de 6 Go/s
    Donc, même en écrivant un parseur parfait, on ne peut pas dépasser cette limite
    En revanche, avec un format zero-copy, le CPU peut ignorer les données inutiles, ce qui permet en théorie de « dépasser » 6 Go/s
    Le format Lite³ que je développe exploite ce principe et atteint des performances jusqu’à 120 fois supérieures à simdjson

    • Je pense que les chiffres donnés pour un seul cœur sont beaucoup trop bas
      Par exemple, Zen 1 atteint 25 Go/s sur un seul cœur (lien de référence)
      D’après les résultats de mon microbenchmark, Zen 2 monte à 17 Go/s sans AVX, et jusqu’à 35 Go/s avec AVX non temporel
      Sur un Apple M3 Max, j’ai mesuré jusqu’à 125 Go/s avec NEON non temporel
      Donc les chiffres de 6 Go/s sur x86 et 20 Go/s sur Apple sont très en dessous de la réalité
    • Je me demande d’où vient cette limite — est-ce dû à la structure du bus entre le cœur et le cache, ou entre le cache et le contrôleur mémoire ?
    • Je me demande pourquoi la série Apple M offre une bande passante par cœur 3 fois supérieure à celle du x86
    • Sur les puces récentes, le CPU seul a du mal à saturer la bande passante mémoire ; il faut utiliser l’iGPU pour y parvenir
      Comme l’iGPU peut accéder à la mémoire unifiée
      Donc, pour de grosses copies mémoire, du parsing parallèle ou des tâches de compression/décompression, il est techniquement avantageux d’utiliser l’iGPU comme blitter
      Cela dit, le « saut » évoqué par les formats zero-copy se fait à l’échelle des lignes de cache
    • Les SSD NVMe Samsung annoncent 14 Go/s en lecture ; si un seul cœur CPU est limité à 6 Go/s, la relation entre ces deux chiffres est intéressante
  • Il semble que l’auteur du billet original ait mal interprété la sortie de la commande time
    Le temps system correspond au temps CPU utilisé par le noyau pour le compte du processus
    Dans l’exemple, avec real à 0,395 s, user à 0,196 s et sys à 0,117 s, le CPU n’a travaillé qu’un total de 313 ms, et les 82 ms restants étaient inactifs
    En d’autres termes, il allait plus vite que le sous-système disque, mais l’écart n’était pas énorme
    De plus, le chemin d’I/O est dans un état CPU-bound — même si le disque et le code étaient infiniment rapides, l’exécution du code d’I/O du noyau demanderait quand même 117 ms

  • Auteur du billet ici. Il y a une suite : I/O is no longer the bottleneck, part 2

    • J’ai autrefois participé à un concours de comptage de fréquence des mots
      Cette analyse des différentes techniques d’optimisation utilisées par les participants est intéressante
      Selon la complexité du problème ou le nombre de caractères d’espacement à classifier, les approches variaient
    • Si ce test a été exécuté sur un seul cœur, alors la « limite de 6 Go/s » avancée plus haut est réfutée expérimentalement
  • Le goulot d’étranglement des performances n’est jamais toujours un facteur unique comme « CPU ou I/O », mais plutôt la ressource qui sature en premier dans la charge réelle
    Cela peut être le CPU, la bande passante mémoire, le cache, le disque, le réseau, les verrous, la latence, etc.
    Il faut donc mesurer, le prouver avec du profiling, puis mesurer à nouveau après les changements

  • Le vrai problème n’est ni le CPU ni les I/O, mais l’équilibre entre latence et débit
    La plupart des logiciels sont lents parce qu’ils ignorent la latence
    En plaçant les données linéairement en mémoire, ou en appliquant du traitement par lots et de la parallélisation, on peut aller beaucoup plus vite

  • J’imagine une architecture composée uniquement de CPU ↔ cache ↔ stockage non volatil
    Si mmap() avait les mêmes caractéristiques de performance que malloc(), on pourrait même confier la persistance à l’OS en désignant la mémoire du programme par des noms de fichiers
    Beaucoup de conceptions logicielles restent encore prisonnières des contraintes de l’ère du disque dur

    • Mais fsync() reste lent
      Pour une vraie persistance, il faut une autre approche, qu’il s’agisse ou non de disques rotatifs
    • On peut implémenter quelque chose de similaire sous Linux
      En pratique, la plupart des demandes mémoire passent par mmap()
      Mais il peut être plus lent que read/write, car le noyau a du mal à prédire les schémas d’accès
  • Dans le cloud, la performance sert parfois aussi d’outil d’ajustement tarifaire
    Les performances matérielles ont progressé de façon impressionnante, mais certains logiciels, surtout Windows ou les applications de messagerie, donnent paradoxalement l’impression d’être plus lents

    • En pratique, les performances des instances cloud sont 5 fois inférieures à celles d’un MacBook M1 tout en étant bien plus chères
      C’est inefficace comme poste de travail distant pour développeur
    • Si la plupart des applications GUI sont lentes, c’est encore à cause de l’attente I/O
      Telegram ou FB Messenger sont rapides, mais pas Teams ni Skype
    • Les moniteurs CRT affichaient les données plus vite
      Certains écrans LCD ont 500 ms de latence
  • Quand les SSD NVMe sont apparus, je plaisantais en disant : « on a désormais l’équivalent de 2 To de RAM »
    Mais aujourd’hui, les serveurs GPU embarquent réellement 2 To de RAM — c’est un exploit d’ingénierie impressionnant

    • J’avais déjà vu une configuration avec 2 To de RAM DDR4 sur un serveur Epyc d’occasion pour 5 000 dollars
      Je regrette de ne pas l’avoir acheté à l’époque
  • D’après mon expérience d’optimisation de bases de données OLAP dans des environnements à très forte concurrence, le goulot d’étranglement était le plus souvent la vitesse de la mémoire

  • Le goulot d’étranglement des I/O était à l’origine lié non pas à la lecture séquentielle, mais au temps de seek
    Je comprends l’idée du billet, mais je voulais souligner ce point

    • Grâce à des technologies récentes comme CXL/PCIe, on peut désormais considérer la RAM et le contrôleur mémoire eux aussi comme une forme de périphérique d’I/O
    • Dans mes anciens cours de bases de données, les performances I/O se mesuraient au temps de seek des disques durs
      Comme on ne peut pas améliorer en code la vitesse de lecture séquentielle, l’essentiel était d’optimiser les accès non séquentiels