Page suivante - Page précédente - Table des matières

2. Avez-vous besoin de l'assembleur?

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!


Page suivante - Page précédente - Table des matières