Retour à l'accueil

Optimisation en assembleur : Comment une lettre accélère le code 3x

Étude expérimentale démontre comment le remplacement des opérations 16 bits par des 32 bits en assembleur élimine les dépendances fausses entre registres, accélérant la division trois fois. Analyse du fonctionnement de la table d'alias de registres et des fonctionnalités de pipelining.

Secret pour un gain de vitesse 3x : Microoptimisation en assembleur
Advertisement 728x90

Comment remplacer une lettre en assembleur triple la vitesse de la division : Analyse technique

Changer un seul caractère dans une instruction d'assembleur réduit le temps d'exécution de la division de 66 %. Des benchmarks réels sur des CPU modernes montrent comment des dépendances factices sur les registres transforment le potentiel d'exécution hors ordre en goulot d'étranglement. Nous décomposons les subtilités architecturales x86-64 affectant les performances.

Caractéristiques architecturales des registres x86-64

Les processeurs x86-64 modernes utilisent une hiérarchie de registres avec une structure héritée. Le registre rax (64 bits) inclut :

┌───────────────────────────────────────────────────────────────┐
│                           rax (64 bit)                        │
├───────────────────────────────┬───────────────────────────────┤
│          (verkhnie 32)         │           eax (32 bit)        │
│                               ├───────────────┬───────────────┤
│                               │               │   ax (16 bit) │
│                               │               ├───────┬───────┤
│                               │               │ah (8) │al (8) │
└───────────────────────────────┴───────────────┴───────┴───────┘

Point clé : Écrire dans les parties inférieures du registre (ax, al) n'affecte pas les bits supérieurs, tandis que les opérations sur des registres 32 bits (eax) écrasent complètement les 32 bits supérieurs. Cela crée des dépendances cachées critiques pour le pipelining.

Google AdInline article slot

Les instructions de division div/idiv utilisent une paire de registres :

┌──────────┬────────────────┬──────────────┬─────────────────────┐
│ Size   │ Delimoe        │ Chastnoe in    │ Balance in           │
├──────────┼────────────────┼──────────────┼─────────────────────┤
│ 16-bit   │ dx:ax          │ ax           │ dx                  │
├──────────┼────────────────┼──────────────┼─────────────────────┤
│ 32-bit   │ edx:eax        │ eax          │ edx                 │
├──────────┼────────────────┼──────────────┼─────────────────────┤
│ 64-bit   │ rdx:rax        │ rax          │ rdx                 │
└──────────┴────────────────┴──────────────┴─────────────────────┘

Avant la division, la partie supérieure de la paire doit être mise à zéro. Schéma standard :

mov edx, 0
mov eax, 536700
div dword [denominator]

Données expérimentales : Différence de 3,7 ×

La boucle de test a traité 2 073 600 itérations (1920×1080), simulant le rendu de pixels. Résultats en cycles par itération (après soustraction du surcoût de la boucle) :

Google AdInline article slot
┌────────┬──────────────┬──────────────┐
│        │ idiv (clean.) │ div (clean.)  │
├────────┼──────────────┼──────────────┤
│ 16-bit │ 22.2         │ 22.2         │
├────────┼──────────────┼──────────────┤
│ 32-bit │  6.0         │  6.0         │
├────────┼──────────────┼──────────────┤
│ 64-bit │ 26.2         │ 24.1         │
└────────┴──────────────┴──────────────┘

Surprise : les opérations 32 bits s'exécutent 3,7 fois plus vite que les 16 bits avec des données d'entrée identiques. La raison n'est pas la complexité de l'algorithme de division, mais les détails d'exécution microarchitecturaux.

Mécanisme des dépendances factices

Les processeurs utilisent la Register Alias Table (RAT) pour mapper les registres logiques sur les physiques. Notamment :

  • Écrire dans un registre 32 bits (mov edx, ...) écrase complètement la valeur, rompant les dépendances avec les opérations précédentes
  • Écrire dans un registre 16 bits (mov dx, ...) nécessite une fusion des nouveaux données avec les bits supérieurs, créant une dépendance factice

Dans la variante 16 bits, la chaîne de dépendances ressemble à ceci :

Google AdInline article slot
div word → merge → mov dx → merge → div word → ...

Chaque itération doit attendre la précédente en raison des besoins de fusion des registres. Dans la variante 32 bits, cette chaîne se rompt, permettant au processeur de pipeliner l'exécution.

Métriques de performance clés

  • Latence : 21–22 cycles pour div 16 bits (temps jusqu'à ce que le résultat soit prêt)
  • Débit : 6 cycles pour div 16 bits (intervalle de dispatch entre opérations)

Quand les opérations dépendent les unes des autres, la latence est le goulot d'étranglement. Pour des opérations indépendantes — le débit. Le rapport 22/6 ≈ 3,7 explique l'accélération.

Vérification de l'hypothèse : Remplacer une lettre

L'expérience a confirmé la théorie. Remplacer les instructions 16 bits par des 32 bits dans un contexte 16 bits :

; Before
mov dx, 0x0008
mov ax, 0x2B7C

; After
mov edx, 8
mov eax, 0x2B7C

A conduit à des résultats spectaculaires :

┌───────────────────────┬──────────────┬──────────────┐
│                       │ idiv (clean.) │ div (clean.)  │
├───────────────────────┼──────────────┼──────────────┤
│ 16-bit (mov dx)       │ 22.2         │ 22.2         │
├───────────────────────┼──────────────┼──────────────┤
│ 16-bit (mov edx)      │  7.0         │  7.0         │
└───────────────────────┴──────────────┴──────────────┘

Une lettre e dans l'instruction a éliminé la dépendance factice, faisant passer le temps d'exécution de 22,2 à 7,0 cycles — au niveau du débit du diviseur.

Points importants

  • Le choix de la taille des registres est critique : Utiliser des opérations 32 bits pour préparer une division 16 bits élimine les dépendances inutiles
  • Latence vs Débit : Les dépendances rendent les performances limitées par la latence, non par le débit
  • Subtilités architecturales : Comprendre la RAT et la fusion partielle des registres est essentiel pour l'optimisation bas niveau
  • Le diviseur produit le reste plus vite : x86 calcule quotient et reste en parallèle, permettant un usage plus précoce de edx par rapport à rax
  • Les tests sont obligatoires : Les estimations théoriques divergent souvent des mesures réelles en raison des particularités microarchitecturales

Recommandations pratiques pour les développeurs

  • Pour la division, utilisez toujours des opérations 32 bits pour mettre à zéro les parties supérieures des registres, même avec des données 16 bits
  • Évitez de mélanger les tailles de registres dans les boucles
  • Pour du code critique en performances, vérifiez la sortie assembleur du compilateur
  • Notez que le débit du diviseur dépasse la latence, mais seulement sans dépendances
  • Sur les CPU modernes (Ice Lake et ultérieurs), consultez les spécifications uops.info pour les métriques actuelles

Cette expérience montre comment une connaissance approfondie de la microarchitecture révèle des optimisations non évidentes. Échanger une lettre apparemment anodine dans une instruction procure une accélération par trois en supprimant des limites artificielles du pipeline. Pour les programmeurs systèmes, c'est un rappel : même en assembleur bas niveau, les opportunités d'optimisation se cachent dans la compréhension du matériel.

— Editorial Team

Advertisement 728x90

Lire ensuite