Dans de nombreux cas, la manière optimale d'exécuter une tâche peut dépendre du contexte dans lequel la tâche est exécutée. Si une routine est écrite en langage d'assemblage, il ne sera généralement pas possible de faire varier la séquence d'instructions en fonction du contexte. À titre d'exemple simple, considérons la méthode simple suivante:
inline void set_port_high(void)
{
(*((volatile unsigned char*)0x40001204) = 0xFF);
}
Un compilateur pour le code ARM 32 bits, étant donné ce qui précède, le rendrait probablement comme quelque chose comme:
ldr r0,=0x40001204
mov r1,#0
strb r1,[r0]
[a fourth word somewhere holding the constant 0x40001204]
ou peut-être
ldr r0,=0x40001000 ; Some assemblers like to round pointer loads to multiples of 4096
mov r1,#0
strb r1,[r0+0x204]
[a fourth word somewhere holding the constant 0x40001000]
Cela pourrait être légèrement optimisé dans un code assemblé à la main, comme:
ldr r0,=0x400011FF
strb r0,[r0+5]
[a third word somewhere holding the constant 0x400011FF]
ou
mvn r0,#0xC0 ; Load with 0x3FFFFFFF
add r0,r0,#0x1200 ; Add 0x1200, yielding 0x400011FF
strb r0,[r0+5]
Les deux approches assemblées manuellement nécessiteraient 12 octets d'espace de code au lieu de 16; ce dernier remplacerait un "load" par un "add", qui sur un ARM7-TDMI exécuterait deux cycles plus rapidement. Si le code devait être exécuté dans un contexte où r0 était ne sait pas / ne se soucie pas, les versions en langage assembleur seraient donc un peu meilleures que la version compilée. D'un autre côté, supposons que le compilateur sache qu'un certain registre [par exemple r5] allait contenir une valeur qui était à moins de 2047 octets de l'adresse désirée 0x40001204 [par exemple 0x40001000], et savait en outre qu'un autre registre [par exemple r7] allait pour contenir une valeur dont les bits faibles étaient 0xFF. Dans ce cas, un compilateur pourrait optimiser la version C du code pour simplement:
strb r7,[r5+0x204]
Beaucoup plus court et plus rapide que même le code d'assemblage optimisé à la main. De plus, supposons que set_port_high se soit produit dans le contexte:
int temp = function1();
set_port_high();
function2(temp); // Assume temp is not used after this
Pas du tout invraisemblable lors du codage pour un système embarqué. Si set_port_high
est écrit en code d'assemblage, le compilateur devrait déplacer r0 (qui contient la valeur de retour de function1
) ailleurs avant d'appeler le code d'assemblage, puis déplacer cette valeur vers r0 par la suite (car function2
attendra son premier paramètre dans r0), le code d'assemblage "optimisé" aurait donc besoin de cinq instructions. Même si le compilateur ne connaissait aucun registre contenant l'adresse ou la valeur à stocker, sa version à quatre instructions (qu'il pourrait adapter pour utiliser tous les registres disponibles - pas nécessairement r0 et r1) battrait l'assemblage "optimisé" -version linguistique. Si le compilateur avait l'adresse et les données nécessaires dans r5 et r7 comme décrit précédemment, instruction -function1
ne modifierait pas ces registres et pourrait donc remplacerset_port_high
avec quatre instructions plus petites et plus rapides que le code d'assemblage «optimisé à la main».strb
Notez que le code d'assemblage optimisé à la main peut souvent surpasser un compilateur dans les cas où le programmeur connaît le déroulement précis du programme, mais les compilateurs brillent dans les cas où un morceau de code est écrit avant que son contexte ne soit connu, ou où un morceau de code source peut être invoqué à partir de plusieurs contextes [s'il set_port_high
est utilisé à cinquante endroits différents dans le code, le compilateur pourrait décider indépendamment pour chacun de ceux-ci de la meilleure façon de l'étendre].
En général, je suggérerais que le langage assembleur est susceptible d'apporter les plus grandes améliorations de performances dans les cas où chaque morceau de code peut être approché à partir d'un nombre très limité de contextes, et est susceptible d'être préjudiciable aux performances dans les endroits où un morceau de le code peut être abordé dans de nombreux contextes différents. Fait intéressant (et pratique), les cas où l'assemblage est le plus bénéfique pour les performances sont souvent ceux où le code est le plus simple et le plus facile à lire. Les endroits où le code en langage assembleur se transformerait en un gâchis gluant sont souvent ceux où l'écriture en assembleur offrirait le plus petit avantage en termes de performances.
[Note mineure: il y a des endroits où le code d'assemblage peut être utilisé pour produire un désordre gluant hyper-optimisé; par exemple, un morceau de code que j'ai créé pour l'ARM avait besoin de récupérer un mot de la RAM et d'exécuter l'une des douze routines environ basées sur les six bits supérieurs de la valeur (de nombreuses valeurs mappées sur la même routine). Je pense que j'ai optimisé ce code pour quelque chose comme:
ldrh r0,[r1],#2! ; Fetch with post-increment
ldrb r1,[r8,r0 asr #10]
sub pc,r8,r1,asl #2
Le registre r8 contenait toujours l'adresse de la table de répartition principale (dans la boucle où le code passe 98% de son temps, rien ne l'a jamais utilisé à d'autres fins); les 64 entrées se référaient à des adresses dans les 256 octets qui la précédaient. Étant donné que la boucle primaire avait dans la plupart des cas une limite de temps d'exécution stricte d'environ 60 cycles, la récupération et l'envoi de neuf cycles ont été très utiles pour atteindre cet objectif. Utiliser une table de 256 adresses 32 bits aurait été un cycle plus rapide, mais aurait englouti 1 Ko de RAM très précieuse [le flash aurait ajouté plus d'un état d'attente]. Utiliser 64 adresses 32 bits aurait nécessité l'ajout d'une instruction pour masquer certains bits du mot récupéré, et aurait encore englouti 192 octets de plus que la table que j'ai réellement utilisée. L'utilisation du tableau des décalages 8 bits a donné un code très compact et rapide, mais ce n'est pas quelque chose que j'attendrais d'un compilateur; Je ne m'attendrais pas non plus à ce qu'un compilateur consacre un registre «à plein temps» à la tenue de l'adresse de la table.
Le code ci-dessus a été conçu pour fonctionner comme un système autonome; il pouvait périodiquement appeler du code C, mais seulement à certains moments où le matériel avec lequel il communiquait pouvait être mis en toute sécurité dans un état «inactif» pendant deux intervalles d'environ une milliseconde toutes les 16 ms.