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