"Trouvez une raison à tout et vous comprendrez beaucoup"
Peut-être que mes lecteurs habituels (enfin, il se peut qu'ils ne le soient pas) se souviennent que dans mon article, j'étais perplexe que l'attribut unsigned ait été utilisé pour décrire les registres des périphériques externes. Dans les commentaires, il a été suggéré que cela a été fait pour éviter un comportement indéfini pendant les quarts de travail et j'ai accepté. Comme je l'ai découvert récemment, il y a une autre raison à cette utilisation de l'attribut et il peut être appliqué non seulement aux registres, mais aussi aux variables ordinaires.
Nous commençons donc.
Pour commencer, une petite introduction au ferEn tant que plate-forme cible, nous considérerons un MK 8 bits sans batterie (c'est une tentative si pathétique de cacher le nom compromis AVR), qui a les commandes matérielles suivantes:
lsl / lsr décalage gauche / droite logique, le bit bas / haut est effacé;
rol / ror décalage cyclique gauche / droite par transfert (décalage 9 bits);
asr décalage arithmétique vers la droite, le bit le plus significatif (signé) est stocké (nous faisons attention au fait que réaliser ce type de décalage vers la gauche est généralement impossible en principe).
Toutes ces commandes sont exécutées sur l'opérande d'octet et sont la base de l'implémentation de tous les autres décalages possibles. Par exemple, un décalage de mot (2 octets rh, rl) avec un signe à droite de 1 chiffre est implémenté par la séquence suivante:
asr rh; ror rl;
Considérons un exemple de code simple et le code assembleur correspondant pour MK avec le système de commande AVR, comme toujours, obtenu sur godbolt.org. (implique que l'optimisation est activée et que la variable se trouve dans le registre r24)
int8_t byte; byte = byte << 1;
clr r25 sbrc r24,7 com r25 lsl r24 rol r25
et voir que l'opération prend cinq équipes?
Remarque: Si quelqu'un dans les commentaires vous dit comment organiser ce fragment (et les suivants) en 2 colonnes, je vous en serai reconnaissant.
On peut voir dans le code assembleur que la variable d'octet se développe en un type entier (16 bits) dans les trois premières commandes, et dans les deux suivantes, le nombre à deux octets est en fait décalé - c'est en quelque sorte étrange, pour dire le moins.
Décaler vers la droite n'est pas mieux
byte = byte >> 1; clr r25 sbrc r24,7 com r25 asr r25 ror r24
- les cinq mêmes équipes. Pendant ce temps, il est évident qu'en fait, pour effectuer la dernière opération, vous avez besoin d'une seule commande
sr r24
et pour la première opération plus. J'ai déclaré à plusieurs reprises que le compilateur crée actuellement du code assembleur pas pire qu'un programmeur (bien qu'il s'agisse d'un système de commande ARM), surtout si vous l'aidez un peu, et soudain, un tel problème. Mais essayez d'aider le compilateur à créer le code correct, peut-être s'agit-il de mélanger les types dans une opération de décalage et d'essayer
byte = byte >> (int8_t) 1;
- n'a pas aidé, à partir du mot "complètement", mais l'option
byte=(uint8_t) byte >> 1;
donne un résultat légèrement meilleur
ldi r25,lo8(0) asr r25 ror r24
- trois équipes, puisque l'extension à l'ensemble occupe désormais une équipe - c'est mieux, bien que pas parfait, la même image pour
byte=(uint8_t) byte << 1;
- trois équipes. Eh bien, pour ne pas écrire de transtypages supplémentaires, nous rendons la variable elle-même non signée
uint8_t byteu;
et BINGO - le code assembleur répond pleinement à nos attentes
byteu = byteu << 1; lsr r24
Il est étrange à quel point il semblerait, quelle différence, d'indiquer immédiatement le type correct d'une variable, ou de l'amener directement à une opération - mais il s'avère qu'il y a une différence.
D'autres études ont montré que le code assembleur prend en compte le type de variable auquel le résultat est attribué, car
byteu = byte << 1;
fonctionne bien et produit un minimum de code, et l'option
byte = byteu << 1;
ne peut pas se passer de trois équipes.
Certes, un tel comportement est décrit dans le standard de la langue, je demande à ceux qui savent dans le commentaire, mais encore une fois je déclarerai fièrement que "les Tchouktches ne sont pas un lecteur" et je continuerai l'histoire.
Donc, une telle technique n'a pas aidé à décaler vers la droite - comme auparavant, il y avait 3 équipes (enfin. Ce qui n'est pas 5, comme pour la version avec signe) et je n'ai pas pu améliorer le résultat en aucune façon.
Mais dans tous les cas, on voit que les opérations de décalage avec un numéro non signé sont effectuées plus rapidement qu'avec son adversaire. Par conséquent, si nous n'allons pas traiter le bit le plus significatif d'un nombre comme un signe (et dans le cas des registres, c'est généralement le cas), nous devons certainement ajouter l'attribut unsigned, ce que nous ferons à l'avenir.
Il s'avère qu'avec les décalages en général, tout est extrêmement intéressant, commençons à augmenter le nombre de positions lors du décalage vers la gauche et en regardant les résultats: << 1 prend 1 cycle d'horloge, << 2 - 2, << 3 - 3, 4 - 2 de manière inattendue, le compilateur a appliqué une optimisation délicate
swap r24 andi r24,lo8(-16)
où la commande s
wap permute deux quartets dans un octet. En outre, sur la base de la dernière optimisation << 5 - 3, << 6 - 4, << 7 - 3 encore une fois de manière inattendue, il existe une autre optimisation
ror r24 clr r24 ror r24
le bit de transfert est utilisé, << 8 - 0 mesure, car il s'avère juste 0, il est inutile de chercher plus loin.
Soit dit en passant, voici une tâche intéressante pour vous - pour combien de temps minimum vous pouvez effectuer une opération
uint16_t byteu; byteu = byteu << 4;
ce qui traduit 0x1234 en 0x2340. La solution évidente consiste à exécuter deux fois 4 commandes
lsl rl rol rh
conduit à 4 * 2 = 8 mesures, je suis rapidement venu avec une option
swap rl ; 1243 swap rh ; 2143 andi rh,0xf0 ; 2043 mov tmp,rl andi tmp,0x0f or rh,tmp ; 2343 andi rl,0xf0 ; 2340
qui nécessite 7 mesures et un registre intermédiaire. Ainsi, le compilateur génère un code de 6 commandes et aucun registre intermédiaire - cool, oui.
Je cache ce code sous le spoiler - essayez de trouver une solution vous-même.Astuce: dans le jeu de commandes MK, il y a une commande OU EXCLUSIF ou un
montant total DEUX
Le voici, ce merveilleux code swap rl ; 1243 swap rh ; 2143 andi rh,0xf0 ; 2043 eor rh,rl ; 6343 andi r2l,0xf0 ; 6340 eor rh,rl ; 2340
Je tire juste un plaisir esthétique de ce fragment.
En règle générale, pour les nombres 16 bits, la différence entre le code pour les numéros signés et non signés disparaissait lorsqu'il était déplacé vers la gauche, c'est étrange comme ça.
Revenons à nos octets et commençons à nous déplacer vers la droite. Comme nous nous en souvenons, pour un octet signé, nous avons 5 cycles d'horloge, pour un octet non signé - 3 et ce temps ne peut pas être réduit. Ou tout de même, vous pouvez - oui, vous pouvez, mais c'est une manière très étrange (GCC avec optimisations activées - "c'est un endroit très étrange"), à savoir
byteu = (byteu >> 1) & 0x7F;
qui génère exactement une commande pour les deux variantes du signe. Convient et option
byteu = (byteu & 0xFE) >> 1;
mais seulement pour un numéro non signé, avec un numéro signé, tout devient encore plus déprimant - 7 mesures, nous continuons donc à explorer uniquement la première option.
Je ne peux pas dire que je comprends ce qui se passe, car il est évident que la multiplication logique (&) par une telle constante après un tel décalage n'a aucun sens (et ce n'est pas le cas), mais la présence de l'opération & affecte le code du décalage lui-même. "Vous voyez le gopher - non - et je ne vois pas, mais il l'est."
Les décalages de 2 et ainsi de suite ont montré qu'il est important de payer le bit de signe, mais le nombre est initialement non signé, en général, des ordures sont obtenues, "mais ça marche", est la seule chose qui puisse être dite à ce sujet.
Néanmoins, il est sûr de dire que l'interprétation du contenu des registres et de la mémoire comme des nombres non signés vous permet d'effectuer un certain nombre d'opérations (par exemple, des décalages ou une extension d'une valeur) avec eux plus rapidement et génère un code plus compact, il peut donc être fortement recommandé pour l'écriture de programmes pour MK, sauf indication contraire (l'interprétation comme un nombre est familier) n'est pas une condition préalable.