L’I/O n’est-il plus le goulot d’étranglement ?
(stoppels.ch)- 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 -wne 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.ca é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
wctraite 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
VPCMPEQBpour 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
strlendans Cosmopolitan libc
- Inspiré de l’implémentation de
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)
- Vérification effectuée avec le même résultat que
- 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
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
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é
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
Il semble que l’auteur du billet original ait mal interprété la sortie de la commande
timeLe temps
systemcorrespond au temps CPU utilisé par le noyau pour le compte du processusDans l’exemple, avec
realà 0,395 s,userà 0,196 s etsysà 0,117 s, le CPU n’a travaillé qu’un total de 313 ms, et les 82 ms restants étaient inactifsEn 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
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
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 quemalloc(), on pourrait même confier la persistance à l’OS en désignant la mémoire du programme par des noms de fichiersBeaucoup de conceptions logicielles restent encore prisonnières des contraintes de l’ère du disque dur
fsync()reste lentPour une vraie persistance, il faut une autre approche, qu’il s’agisse ou non de disques rotatifs
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èsDans 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
C’est inefficace comme poste de travail distant pour développeur
Telegram ou FB Messenger sont rapides, mais pas Teams ni Skype
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
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
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