2. Avez-vous besoin de
l'assembleur?
Contenu de cette
section
Je ne veux en aucun cas jouer les
empêcheurs-de-tourner-en-rond, mais voici quelques conseils
issus d'une expérience gagnée à la dure.
2.1 Le Pour et le Contre
Les avantages de l'assembleur
L'assembleur peut vous permettre de réaliser des
opérations très bas niveau:
- vous pouvez accéder aux registres et aux ports
d'entrées/sorties spécifiques à votre
machine;
- vous pouvez parfaitement contrôler le comportemant du
code dans des sections critiques où pourraient sinon
advenir un blocage du processeur ou des
périphériques;
- vous pouvez sortir des conventions de production de code de
votre compilateur habituel; ce qui peut vous permettre
d'effectuer certaines optimisations (par exemple contourner les
règles d'allocation mémoire, gérer manuellement
le cours de l'éxécution, etc.);
- accéder à des modes de programmation non courants
de votre processeur (par exemple du code 16 bits pour
l'amorçage ou l'interfaçage avec le BIOS, sur les
pécés Intel);
- vous pouvez construire des interfaces entre des fragments
de codes utilisant des conventions incompatibles
(c'est-à-dire produit par des compilateurs différents
ou séparés par une interface bas-niveau);
- vous pouvez générer un code assez rapide pour les
boucles importantes pour pallier aux défauts d'un
compilateur qui ne sait les optimiser (mais bon, il existe des
compilateurs optimisateurs librement disponibles!);
- vous pouvez générer du code optimisé "à
la main" qui est plus parfaitement règlé pour votre
configuration matérielle précise, même s'il ne
l'est pour aucune autre configuration;
- vous pouvez écrire du code pour le compilateur
optimisateur de votre nouveau langage. (c'est là une
activité à laquelle peu se livrent, et encore,
rarement.)
Les inconvénients de l'assembleur
L'assembleur est un langage très bas niveau (le langage
du plus bas niveau qui soit au dessus du codage à la main de
motifs d'instructions en binaire). En conséquence:
- l'écriture de code en est longue et ennuyeuse;
- les bogues apparaissent aisément;
- les bogues sont difficiles à repérer et
supprimer;
- il est difficile de comprendre et de modifier du code (la
maintenance est très compliquée);
- le résultat est extrêmement peu portable vers une
autre architecture, existante ou future;
- votre code ne sera optimisé que une certaine
implémentation d'une même architecture: ainsi, parmi
les plates-formes compatibles Intel, chaque réalisation
d'un processeur et de ses variantes (largeur du bus, vitesse et
taille relatives des CPU/caches/RAM/Bus/disques, présence
ou non d'un coprocesseur arithmétique, et d'extensions MMX
ou autres) implique des techniques d'optimisations parfois
radicalement différentes. Ainsi diffèrent grandement
les processeurs déjà existant et leurs variations:
Intel 386, 486, Pentium, PPro, Pentium II; Cyrix 5x86, 6x86;
AMD K5, K6. Et ce n'est sûrement pas terminé: de
nouveaux modèles apparaissent continuellement, et cette
liste même sera rapidement dépassée, sans parler
du code ``optimisé'' qui aura été écrit
pour l'un quelconque des processeurs ci-dessus.
- le code peut également ne pas être portable entre
différents systèmes d'exploitation sur la même
architecture, par manque d'outils adaptés (GAS semble
fonctionner sur toutes les plates-formes; NASM semble
fonctionner ou être facilement adaptable sur toutes les
plates-formes compatibles Intel);
- un temps incroyable de programmation sera perdu sur de
menus détails, plutôt que d'être efficacement
utilisé pour la conception et le choix des algorithmes
utilisés, alors que ces derniers sont connus pour
être la source de la majeure partie des gains en vitesse
d'un programme. Par exemple, un grand temps peut être
passé à grapiller quelques cycles en écrivant
des routines rapides de manipulation de chaînes ou de
listes, alors qu'un remplacement de la structure de
données à un haut niveau, par des arbres
équilibrés et/ou des tables de hachage permettraient
immédiatement un grand gain en vitesse, et une
parallélisation aisée, de façon portable
permettant un entretien facile.
- une petite modification dans la conception algorithmique
d'un programme anéantit la validité du code
assembleur si patiemment élaboré, réduisant les
développeurs au dilemne de sacrifier le fruit de leur
labeur, ou de s'enchaîner à une conception
algorithmique obsolète.
- pour des programmes qui fait des choses non point trop
éloignées de ce que font les benchmarks standards,
les compilateurs/optimiseurs commerciaux produisent du code
plus rapide que le code assembleur écrit à la main
(c'est moins vrai sur les architectures x86 que sur les
architectures RISC, et sans doute moins vrai encore pour les
compilateurs librement disponible. Toujours est-il que pour du
code C typique, GCC est plus qu'honorable).
- Quoi qu'il en soit, ains le dit le saige John Levine,
modérateur de comp.compilers, "les compilateurs rendent
aisée l'utilisation de structures de données
complexes; ils ne s'arrêtent pas, morts d'ennui, à
mi-chemin du travail, et produisent du code de qualité
tout à fait satisfaisante". Ils permettent également
de propager correctement les transformations du code
à travers l'ensemble du programme, aussi hénaurme
soit-il, et peuvent optimiser le code par-delà les
frontières entre procédures ou entre modules.
Affirmation
En pesant le pour et le contre, on peut conclure que si
l'assembleur est parfois nécessaire, et peut même
être utile dans certains cas où il ne l'est pas, il
vaut mieux:
- minimiser l'utilisation de code écrit en
assembleur;
- encapsuler ce code dans des interfaces bien
définies;
- engendrer automatiquement le code assembleur à partir
de motifs écrits dans un langage plus de haut niveau que
l'assembleur (par exemple, des macros contenant de l'assembleur
en-ligne, avec GCC);
- utiliser des outils automatiques pour transformer ces
programmes en code assembleur;
- faire en sorte que le code soit optimisé, si
possible;
- utiliser toutes les techniques précédentes à
la fois, c'est-à-dire écrire ou étendre la passe
d'optimisation d'un compilateur.
Même dans les cas où l'assembleur est
nécessaire (par exemple lors de développement d'un
système d'exploitation), ce n'est qu'à petite dose, et
sans infirmer les principes ci-dessus.
Consultez à ce sujet les sources du noyau de Linux: vous
verrez qu'il s'y trouve juste le peu qu'il faut d'assembleur, ce
qui permet d'avoir un système d'exploitation rapide, fiable,
portable et d'entretien facile. Même un jeu très
célèbre comme DOOM a été en sa plus grande
partie écrit en C, avec une toute petite routine d'affichage
en assembleur pour accélérer un peu.
2.2 Comment ne pas utiliser
l'assembleur
Méthode générale pour obtenir du code
efficace
Comme le dit Charles Fiterman dans comp.compilers à
propos de la différence entre code écrit par l'homme ou
la machine,
``L'homme devrait toujours gagner, et voici pourquoi:
- Premièrement, l'homme écrit tout dans un langage
de haut nivrau.
- Deuxièmement, il mesure les temps
d'éxécution (profiling) pour déterminer les
endroits où le programme passe la majeure partie du
temps.
- Troisièmement, il demande au compilateur d'engendrer
le code assembleur produit pour ces petites sections de
code.
- Enfin, il effectue à la main modifications et
réglages, à la recherche des petites
améliorations possibles par rapport au code engendré
par la machine.
L'homme gagne parce qu'il peut utiliser la machine.''
Langages avec des compilateurs optimisateurs
Des langages comme ObjectiveCAML, SML, CommonLISP, Scheme,
ADA, Pascal, C, C++, parmi tant d'autres, ont tous des
compilateurs optimiseurs librement disponibles, qui optimiseront
le gros de vos programmes, et produiront souvent du code meilleur
que de l'assembleur fait-main, même pour des boucles
serrées, tout en vous permettant de vous concentrer sur des
détails haut niveau, et sans vous interdire de gagner par la
méthode précédente quelques pourcents de
performance supplémentaire, une fois la phase de conception
générale terminée. Bien sûr, il existe
également des compilateurs optimiseurs commerciaux pour la
plupart de ces langages.
Certains langages ont des compilateurs qui produisent du code
C qui peut ensuite être optimisé par un compilateur C.
C'est le cas des langages LISP, Scheme, Perl, ainsi que de
nombreux autres. La vitesse des programmes obtenus est toute
à fait satisfaisante.
Procédure générale à suivre pour
accélerer votre code
Pour accélérer votre code, vous ne devriez traiter
que les portions d'un programme qu'un outil de mesure de temps
d'éxécution (profiler) aura identifié comme
étant un goulot d'étranglement pour la performance de
votre programme.
Ainsi, si vous identifiez une partie du code comme étant
trop lente, vous devriez
- d'abord essayer d'utiliser un meilleur algorithme;
- essayer de la compiler au lieu de l'interpréter;
- essayer d'activer les bonnes options d'optimisation de
votre compilateur;
- donner au compilateur des indices d'optimisation
(déclarations de typage en LISP; utilisation des
extensions GNU avec GCC; la plupart des compilos fourmillent
d'options);
- enfin de compte seulement, se mettre à l'assembleur si
nécessaire.
Enfin, avant d'en venir à cette dernière option,
vous devriez inspecter le code généré pour
vérifier que le problème vient effectivement d'une
mauvaise génération de code, car il se peut fort bien
que ce ne soit pas le cas: le code produit par le compilateur
pourrait être meilleur que celui que vous auriez écrit,
en particulier sur les architectures modernes à pipelines
multiples! Il se peut que les portions les plus lentes de votre
programme le soit pour des raisons intrinsèques. Les plus
gros problèmes sur les architectures modernes à
processeur rapide sont dues aux délais introduits par les
accès mémoires, manqués des caches et TLB, fautes
de page; l'optimisation des registres devient vaine, et il vaut
mieux repenser les structures de données et
l'enchaînement des routines pour obtenir une meilleur
localité des accès mémoire. Il est possible qu'une
approche complètement différente du problème soit
alors utile.
Inspection du code produit par le compilateur
Il existe de nombreuses raisons pour vouloir regarder le code
assembleur produit par le compilateur. Voici ce que vous pourrez
faire avec ce code:
- vérifier si le code produit peut ou non être
améliorer avec du code assembleur écrit à la
main (ou par un réglage différent des options du
compilateur);
- quand c'est le cas, commencer à partir de code
automatiquement engendré et le modifier plutôt que de
repartir de zéro;
- plus généralement, utilisez le code produit comme
des scions à greffer, ce qui à tout le moins vous
laisse permet d'avoir gratuitement tout le code
d'interfaçage avec le monde extérieur.
- repérer des bogues éventuels dus au compilateur
lui-même (espérons-le très rare, quitte à
se restreindre à des versions ``stables'' du
compilo).
La manière standard d'obtenir le code assembleur
généré est d'appeller le compilateur avec l'option
-S. Cela fonctionne avec la plupart des compilateur
Unix y compris le compilateur GNU C (GCC); mais à vous de
voir dans votre cas. Pour ce qui est de GCC, il produira un code
un peu plus compréhensible avec l'option
-fverbose-asm. Bien sur, si vous souhaitez obtenir
du code assembleur optimisé, n'oubliez pas d'ajouter les
options et indices d'optimisation appropriées!
Chapitre suivant, Chapitre
Précédent
Table des matières de
ce chapitre,
Table des matières
générale
Début du document,
Début de ce chapitre