2 points par GN⁺ 2025-02-23 | 1 commentaires | Partager sur WhatsApp
  • Le premier cours d’assembleur de FFmpeg présente, à un niveau débutant, pourquoi l’assembleur SIMD écrit à la main est nécessaire pour le traitement multimédia et résume les conventions d’écriture de fonctions propres à FFmpeg
  • Le public visé doit connaître les pointeurs en C ainsi que les notions de scalaire, vecteur, addition et multiplication de niveau lycée, et le cours s’appuie sur le x86 64-bit et la syntaxe Intel
  • Dans FFmpeg, assembly function, SIMD et vectorise sont utilisés presque comme des synonymes, pour désigner une façon de traiter plusieurs éléments de données à la fois, bien adaptée au traitement d’images, de vidéos et d’audio
  • Côté performances, le projet préfère l’assembleur manuel : les intrinsics sont généralement 10 à 15 % plus lentes, l’auto-vectorisation de dav1d a montré environ 2x, contre jusqu’à 8x pour une version écrite à la main
  • La première fonction d’exemple utilise x86inc.asm, INIT_XMM sse2, cglobal, movu, paddb, RET pour additionner en SIMD les valeurs de 16 octets de deux buffers uint8_t, puis réécrire le résultat dans le premier buffer

Objectif du cours et prérequis

  • FFmpeg Assembly Language Lesson One est un cours d’introduction qui couvre les bases de la manière dont le langage assembleur est écrit dans FFmpeg, et aide à comprendre ce qui se passe réellement à l’intérieur d’un ordinateur
  • Les connaissances nécessaires sont les suivantes
    • le langage C, en particulier les pointeurs
    • si vous ne connaissez pas C, il est recommandé d’étudier The C Programming Language
    • les notions de scalaire et de vecteur, d’addition et de multiplication, au niveau des mathématiques du lycée

Langage assembleur et SIMD

  • Le langage assembleur est un langage de programmation qui permet d’écrire sous une forme lisible par l’humain un code correspondant directement aux instructions traitées par le CPU
  • Le code assembleur lisible par l’humain est ensuite converti par un assembleur en données binaires de langage machine (machine code) compréhensibles par le CPU
  • Le code assembleur de FFmpeg est en grande majorité du SIMD (Single Instruction Multiple Data)
    • une seule instruction agit simultanément sur plusieurs éléments de données
    • on parle aussi de programmation vectorielle
    • la programmation scalaire classique traite un seul élément de données à la fois
  • Le SIMD est particulièrement adapté au traitement d’images, de vidéos et d’audio, qui manipulent de grandes quantités de données disposées séquentiellement en mémoire
  • Dans FFmpeg, les expressions suivantes sont utilisées presque dans le même sens
    • assembly function
    • SIMD
    • vectorise
    • elles désignent l’écriture manuelle de fonctions assembleur pour traiter plusieurs éléments de données en une fois

Pourquoi FFmpeg écrit directement de l’assembleur

  • L’objectif principal est d’accélérer le traitement multimédia
    • des gains de vitesse de plus de 10x sont courants avec du code assembleur
    • c’est important pour réduire les saccades lors de la lecture vidéo en temps réel
    • cela peut aussi réduire la consommation d’énergie et améliorer l’autonomie
  • Les fonctions d’encodage et de décodage vidéo sont énormément utilisées, aussi bien chez les utilisateurs finaux que dans les datacenters, donc même de petites améliorations s’accumulent vite
  • FFmpeg utilise de l’assembleur écrit à la main plutôt que des intrinsics
    • les intrinsics sont des fonctions de type C correspondant à des instructions assembleur
    • elles sont en général 10 à 15 % plus lentes que de l’assembleur écrit à la main
    • ce chiffre varie selon le compilateur, et les partisans des intrinsics peuvent ne pas être d’accord
    • certains estiment aussi qu’elles sont difficiles à lire à cause de l’usage de la Hungarian Notation
  • L’inline assembly peut encore exister dans certaines anciennes parties de FFmpeg ou dans des projets comme le noyau Linux
    • il s’agit d’écrire directement l’assembleur dans le code C au lieu d’un fichier séparé
    • dans un projet comme FFmpeg, l’opinion dominante est que cela nuit à la lisibilité, offre un support compilateur limité et complique la maintenance
  • L’idée selon laquelle l’auto-vectorisation du compilateur suffirait doit être ignorée dans un cadre d’apprentissage
    • dans des tests récents du projet dav1d, l’auto-vectorisation a montré un gain d’environ 2x
    • une version écrite à la main pouvait atteindre jusqu’à 8x

Périmètre de la syntaxe et ressources de référence

  • Le cours se concentre sur l’assembleur x86 64-bit
    • aussi appelé amd64, et fonctionne également sur les CPU Intel
    • l’extension future vers d’autres CPU comme ARM ou RISC-V reste possible
  • Il existe deux syntaxes principales pour l’assembleur x86 : AT&T et Intel
    • la syntaxe AT&T est plus ancienne et est considérée ici comme plus difficile à lire que la syntaxe Intel
    • le cours utilise la syntaxe Intel
  • Les livres généralistes ou les ressources en ligne comme Stack Overflow ne sont pas forcément très utiles comme référence pour l’assembleur FFmpeg
    • car il utilise de l’assembleur écrit à la main en syntaxe Intel
    • beaucoup de ressources en ligne se concentrent sur la programmation système, la programmation matérielle ou du code non SIMD
    • l’assembleur FFmpeg suit une approche particulière, spécialisée dans le traitement d’image haute performance
  • Les diagrammes visualisant les instructions et le fonctionnement SIMD dans la seconde moitié de The Art of 64-bit assembly peuvent être utiles
  • Un serveur Discord est fourni pour poser des questions

Notions de base sur les registres

  • Un registre est une zone du CPU où les données sont traitées
  • Le CPU n’opère pas directement sur la mémoire : il charge les données dans des registres, les traite, puis les réécrit en mémoire
  • En assembleur, on ne peut généralement pas copier directement des données d’un emplacement mémoire à un autre ; il faut d’abord passer par un registre

Registres à usage général

  • Un GPR (General Purpose Register) est un registre polyvalent capable de contenir des données ou des adresses mémoire
    • ici, il peut contenir une valeur allant jusqu’à 64 bits
    • il peut aussi contenir un pointeur
    • il permet des opérations comme l’addition, la multiplication ou les décalages
  • De nombreux livres d’assembleur détaillent longuement les GPR et leur historique
  • Dans le code assembleur de FFmpeg, les GPR servent surtout d’échafaudage (scaffolding), et la plupart de leur complexité n’est ni nécessaire ni visible grâce à l’abstraction

Registres vectoriels et taille des données

  • Les registres vectoriels contiennent plusieurs éléments de données
  • Les principaux registres vectoriels de x86 sont les suivants
    • mm : registres MMX, taille de 64 bits, historiques et peu utilisés aujourd’hui
    • xmm : registres XMM, taille de 128 bits, largement disponibles
    • ymm : registres YMM, taille de 256 bits, avec certaines complexités d’usage
    • zmm : registres ZMM, taille de 512 bits, avec une disponibilité plus limitée
  • La plupart des calculs de compression et de décompression vidéo reposent sur des entiers, donc le cours se concentre lui aussi sur les entiers
  • Un registre xmm de 128 bits peut être interprété ainsi
    • 16 byte, de 8 bits chacun
    • 8 word, de 16 bits chacun
    • 4 doubleword, de 32 bits chacun
    • 2 quadword, de 64 bits chacun
  • Ces abréviations deviendront importantes par la suite
    • byte : donnée de 8 bits
    • word : donnée de 16 bits
    • doubleword : donnée de 32 bits
    • quadword : donnée de 64 bits
    • double quadword : donnée de 128 bits

Rôle de x86inc.asm

  • x86inc.asm est une fine couche d’abstraction utilisée par FFmpeg, x264 et dav1d
  • Elle fournit plusieurs fonctions facilitant l’écriture du code pour les programmeurs assembleur
  • L’une des fonctions importantes au début consiste à attribuer aux GPR des labels comme r0, r1, r2
    • il n’est pas nécessaire de mémoriser les noms réels des registres
    • comme les GPR servent surtout d’échafaudage dans FFmpeg, cela allège la charge d’écriture

Exemple simple d’asm scalaire

mov r0q, 3
inc r0q
dec r0q
imul r0q, 5
  • La première ligne stocke la valeur immédiate (immediate value) 3 dans le registre r0 sous forme de quadword
    • une valeur immédiate n’est pas lue depuis la mémoire : elle est intégrée directement dans le code assembleur
  • En syntaxe Intel, l’opérande source à droite est transféré vers l’opérande destination à gauche
    • on peut le lire comme r0q = 3
    • l’ordre ressemble à celui de memcpy
  • Le suffixe q dans r0q indique que le registre est utilisé comme quadword
  • Les opérations suivantes donnent alors
    • inc fait passer la valeur à 4
    • dec la ramène à 3
    • imul la multiplie par 5, si bien que r0q vaut finalement 15
  • Les instructions lisibles par l’humain comme mov ou inc sont appelées des mnemonics
    • l’assembleur les convertit en langage machine
    • les majuscules MOV, INC et les minuscules mov, inc sont équivalentes
    • dans FFmpeg, les mnemonics sont écrits en minuscules, les majuscules étant réservées aux macros

Premier exemple de fonction SIMD

%include "x86inc.asm"
SECTION .text
;static void add_values(uint8_t *src, const uint8_t * src2)
INIT_XMM sse2
cglobal add_values, 2, 2, 2, src, src2
movu m0, [srcq]
movu m1, [src2q]
paddb m0, m1
movu [srcq], m0
RET
  • Cette fonction additionne en SIMD les données de src et src2, puis réécrit le résultat à l’emplacement src
  • %include "x86inc.asm" inclut des helpers, noms prédéfinis et macros développés par les communautés x264, FFmpeg et dav1d
  • SECTION .text indique la section où se trouve le code exécutable
    • les données constantes peuvent être placées dans la section .data
  • ;static void add_values(uint8_t *src, const uint8_t * src2) est un commentaire montrant la forme des arguments de la fonction C
    • en assembleur, le point-virgule ; sert à commenter, comme // en C
  • INIT_XMM sse2 active l’usage des registres XMM et du jeu d’instructions sse2
    • car paddb est une instruction sse2
  • cglobal add_values, 2, 2, 2, src, src2 définit la fonction C add_values
    • la fonction prend 2 arguments
    • elle utilise 2 GPR, arguments compris
    • elle utilise 2 registres XMM
    • les deux derniers éléments sont les labels des arguments src et src2
    • dans du code plus ancien, on peut utiliser directement des GPR comme r0, r1 sans labels d’arguments

load, packed add, store

movu m0, [srcq]
movu m1, [src2q]
  • movu est une forme abrégée de movdqu, qui signifie move double quad unaligned
  • L’alignement sera traité dans un cours ultérieur ; ici, il suffit de voir cela comme une instruction déplaçant 128 bits depuis [srcq]
  • Dans mov, les crochets indiquent un déréférencement d’adresse
    • c’est l’équivalent conceptuel de *src en C
    • cette opération est un load
  • Le suffixe q désigne la taille du pointeur
    • sur un système 64 bits, cela correspond aux 8 octets d’un pointeur C
    • x86asm utilise 32 bits sur un système 32 bits
    • le load réel, lui, porte sur 128 bits
  • Les registres vectoriels sont référencés via l’abstraction m0 au lieu du nom complet xmm0
    • cela prépare la suite du cours, où un même code pourra viser plusieurs tailles de registres SIMD
paddb m0, m1
  • paddb additionne entre eux les éléments de type byte de chaque registre
  • Le préfixe p signifie packed et sert à distinguer les instructions vectorielles des instructions scalaires
  • Le suffixe b indique une addition au niveau byte
  • Si l’on additionne deux registres contenant chacun 16 byte, les éléments correspondants sont additionnés position par position, comme a+q, b+r, c+s
movu [srcq], m0
RET
  • movu [srcq], m0 réécrit les données résultat à l’adresse pointée par srcq
    • cette opération est un store
  • RET est une macro indiquant le retour de la fonction
  • Presque toutes les fonctions assembleur de FFmpeg modifient les données reçues en argument au lieu de renvoyer une valeur
  • Dans les exercices, la suite consiste à créer des pointeurs de fonction vers les fonctions assembleur disponibles, puis à les utiliser

1 commentaires

 
GN⁺ 2025-02-23
Avis sur Hacker News
  • Autre ressource sur le même sujet : https://blogs.gnome.org/rbultje/2017/07/14/writing-x86-simd-...

    Dans les commentaires, l’utilité du SIMD écrit à la main va de « totalement peu claire » à « critique pour la mission », et comme le camp du « totalement peu clair » semble plus nombreux, voici un petit éclairage sur le côté critique pour la mission
    FFmpeg est un cas évident vu sa fréquence d’usage, mais le décodeur AV1 généraliste de production dav1d est à mon avis un exemple plus facile pour quantifier l’impact du SIMD écrit à la main
    dav1d est utilisé presque partout, des principaux navigateurs au système d’exploitation Android, a remplacé libgav1, et une grande part de son succès vient de sa vitesse impressionnante, rendue possible par une part importante de sa base de code en SIMD écrit à la main
    C’est très bien que des langages comme Zig aient une prise en charge SIMD intégrée, mais pour certains usages où il vaut la peine d’examiner l’écart de performance potentiel, l’écriture directe devient nécessaire. Certaines lignes de code de dav1d s’exécutent des milliers de milliards de fois par jour, donc elles doivent être aussi rapides que possible, et l’écart entre SIMD écrit à la main et SIMD généré par le compilateur peut aller jusqu’à 50 % selon les cas, ce qui est important
    Je participe moi aussi, dans une certaine mesure, à l’écriture de code exécuté extrêmement souvent, donc je pense que des ressources comme l’école de langage assembleur FFmpeg sont assez importantes si l’on veut éviter que ce savoir-faire disparaisse

    • L’un des aspects intéressants de dav1d est qu’étant écrit en assembleur, il peut utiliser sa propre convention d’appel
      La convention d’appel peut même varier selon la méthode, ce qui réduit fortement les sauvegardes et chargements sur la pile par rapport au code généré par un compilateur tenu de suivre la convention d’appel standard de la plateforme
    • Je suis moi aussi plutôt du côté « critique pour la mission », mais il existe aussi des contre-exemples intéressants. Si l’on se focalise sur de petits détails ou que l’on tombe dans la complexité accidentelle, on peut perdre de vue l’optimisation algorithmique, et si la quantité de code spécifique à chaque plateforme devient trop grande, la friction des modifications augmente, ce qui rend plus difficile la sortie d’un optimum local
      Par exemple, notre nouvelle multiplication de matrices est plus rapide que celle d’une bibliothèque connue pour l’inférence LLM, et cela peut arriver même si l’autre utilise AMX et nous AVX512BF16. La raison vient peut-être d’un goulot d’étranglement dans le threading, ou d’autre chose, mais la présence d’un JIT rend l’analyse difficile
      Si nous avions dû écrire nous-mêmes des kernels spécifiques à chaque plateforme, nous n’aurions probablement pas obtenu ce résultat. Le temps disponible chaque jour est limité, et le fait d’avoir pu écrire une implémentation unique avec Highway nous a permis d’explorer un espace de conception plus large, incluant non seulement de nouveaux types de kernels et tailles de blocs, mais aussi un auto-tuner qui choisit la stratégie de parallélisation et ses paramètres
      On pourra peut-être régler certaines parties à la main dans une deuxième étape, mais j’aimerais qu’une exploration plus large ait lieu avant de micro-optimiser l’allocation de registres et la convention d’appel
    • Je me demande ce que Zig apporte avec sa prise en charge SIMD intégrée au-delà de la simple surcharge d’opérateurs arithmétiques. 90 % de l’utilité du SIMD se situe en dehors de ces opérations élémentaires
      J’aime bien Zig, mais je comprends que dans la plupart des cas, comme en C/C++, il faut utiliser des fonctions intrinsèques spécifiques au CPU
      GCC et Clang prennent en charge l’attribut vector_size et la surcharge des opérateurs arithmétiques pour ces types « vectorisés », et bien plus encore. En pratique, même des intrinsèques comme _mm256_mul_ps sont implémentées sous une forme du genre #define _mm256_mul_ps(a,b) (__m256)((v8sf)(a) * (v8sf)(b))
      L’utilité de ces fonctionnalités est bien plus grande que ce qui semble possible dans Zig
    • C’est une remarque tout à fait juste. Du côté opposé, celui de l’encodeur, on utilise aussi énormément de SIMD écrit à la main pour des raisons similaires
      Côté encodeur, il faut en particulier souvent « structurer » le problème de manière à pouvoir supprimer des boucles et des chargements en amont, et les compilateurs ne savent pas générer ce type de code auto-vectorisé
  • Autrefois, je faisais pas mal de versions SIMD des fonctions critiques, mais presque plus aujourd’hui. Une méthode qui vaut le coup consiste à isoler le code en question et à l’exécuter dans le formidable Compiler Explorer [0]
    puis à examiner le code généré
    De nos jours, l’auto-vectorisation produit souvent de très bonnes versions SIMD d’une fonction, et il suffit fréquemment de donner quelques « indices » au compilateur. Par exemple en précisant l’alignement, ou en fournissant directement des types source/destination vectoriels
    On peut faire beaucoup de choses en « stylisant » le code C en pensant à ce que le compilateur pourrait être capable de faire. Par exemple en utilisant davantage de variables intermédiaires et en découpant très finement les opérations souhaitées
    Même dans le pire des cas, si le compilateur n’est pas assez intelligent, on peut partir de l’assembleur généré et l’ajuster sans avoir à écrire soi-même tout le boilerplate
    Dans la plupart des cas, la fonction C obtenue est vectorisée aussi bien, voire mieux, que ce que j’aurais écrit à la main ; et dans beaucoup d’autres cas, c’est « suffisamment proche » pour que la différence soit négligeable. En plus, ce code a de bonnes chances d’être bien vectorisé aussi pour WASM ou NEON, sans version explicite
    [0] https://godbolt.org/

    • Nous faisions quelque chose d’un peu similaire. Pour les portions très rares et isolées, par exemple les uploads/downloads et conversions d’images qui ne sont pas pris en charge par le pilote GPU ou qui ne sont pas assez gros pour justifier le lancement d’un traitement GPU, nous écrivions d’abord en C, puis utilisions des annotations de compilateur sur l’alignement ou les alias de pointeurs autorisés afin de générer le code voulu
      GCC et Clang prennent tous deux en charge certaines extensions vectorielles, ce qui permet d’implémenter de façon relativement portable des choses comme les chargements/stockages dispersés, les shuffles ou le masquage d’éléments d’un registre unique, qui sont difficiles à exprimer de manière à la fois lisible pour un humain et suffisamment claire pour produire systématiquement le code attendu d’une version de compilateur à l’autre en C « pur »
      Mais comme il fallait aussi prendre en charge d’autres compilateurs et plateformes, dans les builds réels nous finissions par récupérer l’assembleur généré depuis ce fichier source
    • En contre-exemple, je tombe régulièrement sur des cas triviaux que le compilateur auto-vectorise mal : https://gcc.godbolt.org/z/rjEqzf1hh
      Il s’agit d’une addition saturée d’octets non signés. C’est directement pris en charge par une instruction unique sur x86-64 comme sur ARM64 : PADDUSB et UQADD.16B
      Pourtant, tous les compilateurs se ratent sur la formulation intuitive, échouent à vectoriser ou génèrent un code vectorisé inutilement plus gros et plus lent
      Alors qu’il s’agit d’une primitive de vectorisation basique et simple. Obtenir du compilateur qu’il utilise des instructions plus complexes comme le décalage à droite saturé avec rétrécissement et arrondi (UQRSHRN) est difficile, voire presque impossible
    • Le problème, c’est qu’il faut examiner la sortie du compilateur et la comparer à ce qu’on attend. Il faut parfois bricoler un peu jusqu’à ce qu’elle corresponde à ce qu’on aurait écrit soi-même
      En général, l’écrire directement est simplement plus rapide
    • D’après mon expérience, l’auto-vectorisation est une optimisation fragile qui échoue silencieusement dans toutes sortes de conditions, donc je n’ai pas envie d’en dépendre
    • Je ne connais pas de moyen de faire générer par le compilateur un pshufb plus large que 16 octets dans le cas général
      Même pour une largeur de 16 octets, si on utilise la vraie définition de pshufb, on n’obtient pas pshufb, alors qu’en utilisant une version avec comportement indéfini, on obtient bien pshufb
  • Je suis l’auteur de ce cours
    vous pouvez me poser n’importe quelle question

    • En tant qu’utilisateur d’un Mac ARM, je me demande combien d’efforts il faut pour faire en sorte que ce type de code optimisé fonctionne de la même manière sur toutes les plateformes. J’imagine qu’il faut des tests très poussés et des algorithmes de repli
      Vu la quantité d’assembleur, le fait que FFmpeg fonctionne sur mon Mac me paraît presque miraculeux. Tout a été porté à la main ?
    • Je sais à peine lire l’assembleur, mais je n’ai pas encore l’intuition de décomposer et d’écrire des idées en assembleur. Je me demande si vous recommandez une manière d’apprendre cela ou de progresser
      Je me demande à quel moment on se dit « ça pourrait aller plus vite en assembleur ». Si l’on a identifié une fonction qui serait vraiment plus rapide en assembleur, comment l’écrit-on ? Est-ce qu’on part de la sortie générée par le compilateur, ou est-ce qu’on écrit depuis zéro ? Est-ce que c’est vraiment important ?
    • Je suis curieux de savoir comment vous voyez les jeux d’instructions SIMD à largeur variable comme ARM SVE ou l’extension V de RISC-V
      En termes d’ergonomie pour les développeurs et de performance du code, comment cela se compare-t-il au SIMD traditionnel ? Va-t-on vers un monde où il y aura moins de types de jeux d’instructions SIMD à programmer ?
    • Comment FFmpeg génère-t-il les tables SEH pour les fonctions en assembleur sous Windows ? Je me demande si c’est géré par x86asm.inc, ou si vous ne vous en souciez tout simplement pas
    • Ayant écrit du code optimisé x86 professionnellement dans les années 90, faut-il encore vraiment le faire à la main en 2025 ?
      Ne pourrait-on pas simplement écrire les tests, laisser un LLM essayer 10 000 algorithmes différents, puis profiler les résultats ?
      Ou bien est-ce qu’un LLM aurait du mal à trouver l’optimum même avec 10 000 graines aléatoires ?
      Je pose simplement la question par curiosité. Optimiser x86 à la main n’est pas simple. Pour réfléchir, il faut faire tenir tous les registres dans sa tête, en considérer les combinaisons, et connaître aussi le coût de chaque combinaison d’instructions. Et certaines instructions ont en plus des cas particuliers étranges, difficiles à prendre en compte pour un humain, qui peuvent les rendre bien plus lentes ou bien plus rapides
  • Je suis curieux d’avoir l’avis de ceux qui l’ont vraiment pratiqué. Y a-t-il un plaisir à apprendre ou à écrire de l’assembleur, comme avec le LISP ou RISC-V, ou est-ce plutôt quelque chose qu’on apprend pour faire autre chose, comme COBOL à cause d’un besoin système précis ?
    Ça m’a toujours intéressé, mais je n’ai jamais vraiment eu de raison d’approfondir dans mon travail quotidien. Je me demande si ça vaut la peine d’y consacrer du temps pour le plaisir

    • J’ai suivi les 27 premiers chapitres de ce tutoriel parce que je voulais en apprendre davantage, et j’ai vraiment adoré : https://mariokartwii.com/armv8/
      Maintenant, j’aime plutôt bien coder en assembleur. Depuis ce tutoriel, je n’ai pas fait grand-chose à part une bibliothèque de tableaux appelable depuis du C
      À ce niveau, il reste très peu de magie, et c’est sans doute ce qui est agréable. On doit dire exactement ce qui doit se passer, et en général le résultat correspond à ce qu’on voit
      J’ai aussi bien mieux compris l’édition de liens, et ça m’a aidé à clarifier des choses que je comprenais à haut niveau mais dont les détails restaient flous
      J’ai maintenant envie de jeter un œil à ce tutoriel FFmpeg aussi. Celui-ci est en x86 plutôt qu’en ARM
    • Apprendre ne serait-ce qu’un langage assembleur est très enrichissant, parce que cela vous met au contact de la forme la plus primitive de la programmation réelle
      Il existe des modèles théoriques encore plus simples, comme la machine de Turing ou le lambda-calcul, mais les architectures que manipulent réellement les programmeurs ont un certain degré d’indulgence
      Il ne faut pas en avoir peur. L’assembleur n’est pas tant complexe que verbeux. Pour chaque chose qu’on fait, il y a des chargements et des stockages, encore et encore
      Si on ajoute quelques macros et vérifications au moment du build, ou qu’on l’inscrit dans le contexte d’un système Forth permettant le développement interactif et le scripting en enveloppant un interpréteur qui « exécute des blocs d’assembleur », ce n’est pas si éloigné du C, et cela enlève la magie du compilateur
      Je recommande aussi une approche rétro. Une machine 8 bits dans un émulateur enferme le modèle de travail dans un espace réduit et bien documenté, et ses contraintes rendent plus intéressant le fait de faire davantage de choses en assembleur. Sur les architectures post-32 bits, où les ressources abondent, ce n’est pas aussi courant
      Les gens qui écrivent de l’assembleur dans le cadre de leur travail auront sans doute des préférences plus précises, mais pour un débutant, le plus important est un environnement avec une bonne documentation et de bons exemples. Rosetta Code contient de bons exemples d’assembleur utilisables pour apprendre
    • Une chose amusante, c’est que c’est plus haut niveau qu’on ne l’imagine. Les vraies puces gèrent des choses comme la prédiction de branchement et le pipeline, sur lesquelles l’humain n’a en réalité qu’un contrôle très limité
      Je me souviens d’un cours à l’université où l’on faisait la course à celui qui écrirait le programme assembleur le plus performant pour une tâche donnée. Tout le monde essayait d’arracher les meilleures performances possibles et testait différentes variantes de déroulage de boucle pour éviter une mauvaise prédiction de branchement
      Je ne sais pas si j’avais atteint le Ballmer Peak la veille du rendu, mais j’ai gagné de justesse en essayant une configuration que la plupart des autres avaient ratée
      Il y a aussi l’immense plaisir de regarder https://github.com/chrislgarry/Apollo-11 et de pouvoir plaisanter en disant « c’est un système Unix, je connais ça ! ». Le simple émerveillement de pouvoir lire le langage qui a conduit l’humanité sur la Lune ne disparaît jamais
      En bref, oui
    • Apprendre l’assembleur a eu une signification profonde pour moi. En 30 ans de code, je ne m’en suis jamais servi en pratique, mais cela a complété le tableau, des transistors jusqu’à la programmation de haut niveau
      Comprendre comment s’emboîtent les transistors, les portes logiques, l’architecture CPU et la programmation de haut niveau vaut l’effort, même si l’on n’utilise jamais l’assembleur dans sa vie professionnelle
    • Depuis environ 25 ans, je suis profondément plongé dans l’assembleur parce que je trouve ça amusant
      C’est parfois utile aussi, mais il y a surtout le plaisir de placer chaque dernier octet exactement où il faut, de fouiller dans des binaires que personne n’a examinés depuis des décennies, ou de créer des émulateurs qui auraient été impossibles autrefois
      C’est l’un des rares domaines où je ressens encore la magie, comme à mes débuts
  • Personnellement, je ne pense pas qu’il y ait une grande valeur à écrire soi-même de l’assembleur plutôt que d’utiliser des intrinsics, mais lire de l’assembleur m’a vraiment aidé
    J’utilise souvent Compiler Explorer(https://godbolt.org/) pour regarder l’assembleur généré et comprendre les optimisations qu’un compilateur effectue pour améliorer les performances

    • Cela contredit directement le contenu de l’article
      « C’est pour accélérer le traitement multimédia. Il est très courant d’obtenir des gains de vitesse de plus d’un facteur 10 en écrivant du code assembleur, et c’est particulièrement important lorsqu’on essaie de lire de la vidéo en temps réel sans saccades »
  • La référence à K&R m’a fait plaisir. C’est le livre que j’avais acheté pour apprendre le C et la programmation en général
    Au début, j’avais essayé le C++ comme premier langage, mais cela me paraissait trop abstrait et difficile à apprendre, parce que je me demandais sans cesse ce qui se passait en interne

  • Ce document tombe à pic. Je connaissais l’assembleur x86 de l’époque du 386, mais c’était devenu trop complexe avec les processeurs plus avancés
    Je voulais justement en apprendre davantage sur le SIMD des CPU modernes, et cela a l’air d’être une excellente ressource

  • La phrase « notez que le suffixe q indique la taille du pointeur *(*ce qui signifie en C que, sur un système 64 bits, *sizeof(*src) == 8, et x86asm est assez intelligent pour utiliser 32 bits sur un système 32 bits) mais le chargement réel est de 128 bits » est déroutante
    i.e devrait sans doute être i.e.,, et que veut dire *(* ? Ça ne devrait pas être simplement une parenthèse ouvrante ?
    Je ne vois pas non plus dans quel contexte *sizeof(*src) pourrait être valide. À ma connaissance, sizeof ne produit pas de pointeur
    On dirait qu’il y a eu des astérisques semés au hasard dans cette phrase, ou une tentative ratée de mélanger les astérisques d’italique avec la syntaxe du C

    • Oui. On dirait que quelque chose s’est mal passé soit dans le Markdown lui-même, soit pendant la conversion en Markdown du document d’origine
    • On dirait que les deux premières astérisques servent de paire de notes de bas de page
    • Ça ne renvoie pas la taille du pointeur ? On dirait que c’est utilisé uniquement pour gérer la différence d’architecture
  • Sans entrer dans le débat sur le découpage, je veux juste dire que ce guide est vraiment excellent
    J’aurais adoré avoir une ressource comme celle-ci à l’époque où je m’intéressais de près au très bas niveau

  • L’assembleur est 10 fois plus rapide que le C ? Ça a certainement été vrai à un moment, mais est-ce encore le cas aujourd’hui ? Les compilateurs sont-ils vraiment restés à ce point à l’arrêt qu’ils n’arrivent même pas à approcher de l’assembleur écrit à la main ?

    • Du C avec des intrinsics peut se rapprocher énormément des performances de l’assembleur pur
      Les développeurs de FFmpeg sont plutôt réputés pour être assez hostiles aux intrinsics et, si je me souviens bien, ils ne les acceptent pas dans la base de code même quand les performances sont aussi bonnes que celles d’un assembleur équivalent. Malgré cela, d’après l’estimation même du billet, l’écart entre intrinsics et assembleur tourne autour de 10 à 15 %
      Si l’on compare un assembleur soigneusement optimisé à du C naïf, on peut voir un écart de x10 dans les cas où la vectorisation est possible mais que le compilateur ne l’exploite pas. C’est courant, car l’auto-vectorisation reste globalement assez médiocre dès qu’on dépasse les cas triviaux
      Cela dit, qu’un code écrit par un expert écrase un code naïf n’a rien de surprenant
    • Ce n’est pas un problème de stagnation des compilateurs. Les compilateurs ne peuvent pas connaître les informations dont l’auteur de l’assembleur se sert pour concevoir son code
      Plus simplement, un compilateur C ne peut pas déduire, à partir d’une implémentation C ordinaire, qu’il faudrait exprimer certains calculs via des intrinsics SIMD plus efficaces. Il n’a pas accès à l’intention mathématique de l’auteur
      Il y a aussi des considérations propres à chaque cible. Un compilateur est par nature un compilateur généraliste. Les problèmes de ressources, comme l’allocation de registres, sont NP-complets et équivalents au problème du sac à dos. Très peu de gens voudraient qu’un compilateur passe des heures à chercher la génération d’assembleur absolument optimale. Et c’est encore une autre question de savoir si cette optimalité peut être connue statiquement
    • Il s’agit ici de code fortement vectorisé qui utilise toutes les astuces possibles pour exploiter complètement le CPU
      Les compilateurs sont intelligents sur du code généraliste, mais les codecs ne sont pas du code généraliste. Je ne suis pas programmeur FFmpeg, mais j’ai une expérience en traitement audio
    • Les compilateurs C restent encore assez mauvais en auto-vectorisation
      Sur des problèmes qui se prêtent au SIMD, on peut raisonnablement s’attendre à des gains de 2x à 16x par rapport à une implémentation scalaire naïve
    • Il existe probablement des cas très spécialisés
      Je ne saurais pas écrire un assembleur 10 fois meilleur que du C, mais je ne partirais pas du principe que personne ne peut le faire