Assemblage x86, 9 octets (pour entrée concurrente)
Tous ceux qui tentent ce défi dans des langages de haut niveau passent à côté du vrai plaisir de manipuler des bits bruts. Il y a tellement de variations subtiles sur les façons de le faire, c'est fou - et beaucoup de plaisir à penser. Voici quelques solutions que j'ai conçues en langage d'assemblage x86 32 bits.
Je m'excuse à l'avance que ce n'est pas la réponse typique de code-golf. Je vais beaucoup divaguer sur le processus de réflexion de l'optimisation itérative (pour la taille). J'espère que c'est intéressant et éducatif pour un public plus large, mais si vous êtes du type TL; DR, je ne serai pas offensé si vous sautez à la fin.
La solution évidente et efficace consiste à tester si la valeur est impaire ou paire (ce qui peut être fait efficacement en regardant le bit le moins significatif), puis à choisir entre n + 1 ou n − 1 en conséquence. En supposant que l'entrée est passée en tant que paramètre dans le ECX
registre et que le résultat est retourné dans le EAX
registre, nous obtenons la fonction suivante:
F6 C1 01 | test cl, 1 ; test last bit to see if odd or even
8D 41 01 | lea eax, DWORD PTR [ecx + 1] ; set EAX to n+1 (without clobbering flags)
8D 49 FF | lea ecx, DWORD PTR [ecx - 1] ; set ECX to n-1 (without clobbering flags)
0F 44 C1 | cmovz eax, ecx ; move in different result if input was even
C3 | ret
(13 octets)
Mais à des fins de code-golf, ces LEA
instructions ne sont pas géniales, car elles nécessitent 3 octets pour être codées. Un simple DEC
élément de ECX
serait beaucoup plus court (un seul octet), mais cela affecte les indicateurs, nous devons donc être un peu intelligents dans la façon dont nous organisons le code. Nous pouvons faire le décrément en premier et le test impair / pair en second , mais ensuite nous devons inverser le résultat du test impair / pair.
De plus, nous pouvons changer l'instruction de déplacement conditionnel en une branche, ce qui peut ralentir le code (selon la prévisibilité de la branche - si l'entrée alterne de manière incohérente entre impair et pair, une branche sera plus lente; s'il y a un modèle, il sera plus rapide), ce qui nous fera économiser un autre octet.
En fait, avec cette révision, toute l'opération peut être effectuée sur place, en utilisant un seul registre. C'est génial si vous insérez ce code quelque part (et les chances sont, vous le seriez, car il est si court).
48 | dec eax ; decrement first
A8 01 | test al, 1 ; test last bit to see if odd or even
75 02 | jnz InputWasEven ; (decrement means test result is inverted)
40 | inc eax ; undo the decrement...
40 | inc eax ; ...and add 1
InputWasEven: ; (two 1-byte INCs are shorter than one 3-byte ADD with 2)
(en ligne: 7 octets; en fonction: 10 octets)
Et si vous vouliez en faire une fonction? Aucune convention d'appel standard n'utilise le même registre pour transmettre les paramètres que pour la valeur de retour, vous devez donc ajouter une MOV
instruction registre-registre au début ou à la fin de la fonction. Cela n'a pratiquement aucun coût en vitesse, mais cela ajoute 2 octets. (L' RET
instruction ajoute également un octet, et il y a un certain surcoût introduit par la nécessité de faire et de revenir à partir d'un appel de fonction, ce qui signifie que c'est un exemple où l'inline produit à la fois un avantage de vitesse et de taille, plutôt que d'être simplement une vitesse classique -pour l'espace.) En tout, écrit en fonction, ce code gonfle à 10 octets.
Que pouvons-nous faire d'autre en 10 octets? Si nous nous soucions des performances (au moins, des performances prévisibles ), ce serait bien de se débarrasser de cette branche. Voici une solution de branchement de bits sans branche qui a la même taille en octets. Le principe de base est simple: nous utilisons un XOR au niveau du bit pour inverser le dernier bit, en convertissant une valeur impaire en paire, et vice versa. Mais il y a un inconvénient - pour les entrées impaires, qui nous donne n-1 , tandis que pour les entrées paires, il nous donne n + 1 - exactement à l'opposé de ce que nous voulons. Donc, pour résoudre ce problème, nous effectuons l'opération sur une valeur négative, inversant efficacement le signe.
8B C1 | mov eax, ecx ; copy parameter (ECX) to return register (EAX)
|
F7 D8 | neg eax ; two's-complement negation
83 F0 01 | xor eax, 1 ; XOR last bit to invert odd/even
F7 D8 | neg eax ; two's-complement negation
|
C3 | ret ; return from function
(en ligne: 7 octets; en fonction: 10 octets)
Assez lisse; il est difficile de voir comment cela peut être amélioré. Une chose attire mon attention, cependant: ces deux NEG
instructions de 2 octets . Franchement, deux octets semblent être un octet de trop pour encoder une simple négation, mais c'est l'ensemble d'instructions avec lequel nous devons travailler. Existe-t-il des solutions de contournement? Sûr! Si on XOR
par -2, on peut remplacer la deuxième NEG
ation par un INC
rement:
8B C1 | mov eax, ecx
|
F7 D8 | neg eax
83 F0 FE | xor eax, -2
40 | inc eax
|
C3 | ret
(en ligne: 6 octets; en fonction: 9 octets)
Une autre des bizarreries du jeu d'instructions x86 est l' LEA
instruction polyvalente , qui peut effectuer un déplacement registre-registre, une addition registre-registre, un décalage par une constante et une mise à l'échelle en une seule instruction!
8B C1 | mov eax, ecx
83 E0 01 | and eax, 1 ; set EAX to 1 if even, or 0 if odd
8D 44 41 FF | lea eax, DWORD PTR [ecx + eax*2 - 1]
C3 | ret
(10 octets)
L' AND
instruction est semblable à l' TEST
instruction que nous avons utilisée précédemment, dans la mesure où les deux effectuent un ET au niveau du bit et définissent les indicateurs en conséquence, mais AND
mettent à jour l'opérande de destination. L' LEA
instruction les met ensuite à l'échelle 2, ajoute la valeur d'entrée d'origine et diminue de 1. Si la valeur d'entrée était impaire, cela en soustrait 1 (2 × 0 - 1 = -1); si la valeur d'entrée était paire, cela ajoute 1 (2 × 1 - 1 = 1).
C'est un moyen très rapide et efficace d'écrire le code, car une grande partie de l'exécution peut être effectuée dans le front-end, mais cela n'achète pas beaucoup d'octets, car il en faut tellement pour coder un complexe LEA
instruction. Cette version ne fonctionne pas aussi bien à des fins d'inline, car elle nécessite que la valeur d'entrée d'origine soit préservée en tant qu'entrée de l' LEA
instruction. Donc, avec cette dernière tentative d'optimisation, nous avons en fait reculé, ce qui suggère qu'il pourrait être temps de s'arrêter.
Ainsi, pour l'entrée finale en compétition, nous avons une fonction de 9 octets qui prend la valeur d'entrée dans le ECX
registre (une convention d'appel basée sur un registre semi-standard sur 32 bits x86), et renvoie le résultat dans le EAX
registre (comme avec toutes les conventions d'appel x86):
SwapParity PROC
8B C1 mov eax, ecx
F7 D8 neg eax
83 F0 FE xor eax, -2
40 inc eax
C3 ret
SwapParity ENDP
Prêt à assembler avec MASM; appeler de C comme:
extern int __fastcall SwapParity(int value); // MSVC
extern int __attribute__((fastcall)) SwapParity(int value); // GNU