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 ECXregistre et que le résultat est retourné dans le EAXregistre, 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 LEAinstructions ne sont pas géniales, car elles nécessitent 3 octets pour être codées. Un simple DECélément de ECXserait 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 MOVinstruction 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' RETinstruction 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 NEGinstructions 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 XORpar -2, on peut remplacer la deuxième NEGation par un INCrement:
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' LEAinstruction 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' ANDinstruction est semblable à l' TESTinstruction 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 ANDmettent à jour l'opérande de destination. L' LEAinstruction 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 LEAinstruction. 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' LEAinstruction. 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 ECXregistre (une convention d'appel basée sur un registre semi-standard sur 32 bits x86), et renvoie le résultat dans le EAXregistre (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