code machine x86-64, 12 octets pour int64_t
entrée
6 octets pour l' double
entrée
Nécessite l' popcnt
extension ISA ( CPUID.01H:ECX.POPCNT [Bit 23] = 1
).
(Ou 13 octets si la modification de l'argument sur place nécessite l'écriture de tous les 64 bits, au lieu de laisser des ordures dans les 32 supérieurs. Je pense qu'il est raisonnable de dire que l'appelant ne voudra probablement que charger le 32b bas de toute façon, et x86 zéro -étend implicitement de 32 à 64 à chaque opération 32 bits. Néanmoins, cela empêche l'appelant de faire add rbx, [rdi]
quelque chose.)
Les instructions x87 sont plus courtes que la SSE2 cvtsi2sd
/ la plus évidente movq
(utilisée dans la réponse de @ plafondcat ), et un [reg]
mode d'adressage a la même taille qu'un reg
: juste un octet mod / rm.
L'astuce consistait à trouver un moyen de faire passer la valeur en mémoire, sans avoir besoin de trop d'octets pour les modes d'adressage. (Par exemple, transmettre la pile n'est pas terrible.) Heureusement, les règles autorisent les arguments de lecture / écriture ou les arguments de sortie séparés , donc je peux simplement demander à l'appelant de me passer un pointeur sur la mémoire que je suis autorisé à écrire.
Appelable depuis C avec la signature: void popc_double(int64_t *in_out);
seul le 32b bas du résultat est valide, ce qui est peut-être bizarre pour C mais naturel pour asm. (La correction de ce problème nécessite un préfixe REX sur le magasin final ( mov [rdi], rax
), donc un octet de plus.) Sous Windows, passez rdi
à rdx
, car Windows n'utilise pas le x86-64 System V ABI.
Liste NASM. Le lien TIO a le code source sans le démontage.
1 addr machine global popcnt_double_outarg
2 code popcnt_double_outarg:
3 ;; normal x86-64 ABI, or x32: void pcd(int64_t *in_out)
4 00000000 DF2F fild qword [rdi] ; int64_t -> st0
5 00000002 DD1F fstp qword [rdi] ; store binary64, using retval as scratch space.
6 00000004 F3480FB807 popcnt rax, [rdi]
7 00000009 8907 mov [rdi], eax ; update only the low 32b of the in/out arg
8 0000000B C3 ret
# ends at 0x0C = 12 bytes
Essayez-le en ligne! Comprend un_start
programme de test qui lui transmet une valeur et se termine avec exit status = popcnt return value. (Ouvrez l'onglet "debug" pour le voir.)
Passer des pointeurs d'entrée / sortie séparés fonctionnerait également (rdi et rsi dans l'ABI System86 x64-64), mais nous ne pouvons alors pas raisonnablement détruire l'entrée 64 bits ou justifier aussi facilement d'avoir besoin d'un tampon de sortie 64 bits tout en écrivant uniquement le faible 32b.
Si nous voulons affirmer que nous pouvons prendre un pointeur sur l'entier d'entrée et le détruire, tout en renvoyant la sortie dans rax
, puis simplement omettre le mov [rdi], eax
from popcnt_double_outarg
, le ramenant à 10 octets.
Alternative sans astuces de convention d'appel idiotes, 14 octets
utilisez la pile comme espace de travail, push
pour y arriver. Utilisez push
/ pop
pour copier les registres en 2 octets au lieu de 3 pour mov rdi, rsp
. ( [rsp]
nécessite toujours un octet SIB, il vaut donc la peine de dépenser 2 octets à copier rsp
avant trois instructions qui l'utilisent.)
Appelez de C avec cette signature: int popcnt_double_push(int64_t);
11 global popcnt_double_push
12 popcnt_double_push:
13 00000040 57 push rdi ; put the input arg on the stack (still in binary integer format)
14 00000041 54 push rsp ; pushes the old value (rsp updates after the store).
15 00000042 5A pop rdx ; mov rdx, rsp
16 00000043 DF2A fild qword [rdx]
17 00000045 DD1A fstp qword [rdx]
18 00000047 F3480FB802 popcnt rax, [rdx]
19 0000004C 5F pop rdi ; rebalance the stack
20 0000004D C3 ret
next byte is 0x4E, so size = 14 bytes.
Accepter une entrée au double
format
La question dit simplement que c'est un entier dans une certaine plage, pas qu'il doive être dans une représentation d'entier binaire base2. Accepter l' double
entrée signifie qu'il n'y a plus aucun intérêt à utiliser x87. (À moins que vous n'utilisiez une convention d'appel personnalisée dans laquelle les double
s sont passés dans les registres x87. Ensuite, stockez-les dans la zone rouge sous la pile et ouvrez-les à partir de là.)
11 octets:
57 00000110 66480F7EC0 movq rax, xmm0
58 00000115 F3480FB8C0 popcnt rax, rax
59 0000011A C3 ret
Mais nous pouvons utiliser la même astuce passe-par-référence qu'avant pour créer une version à 6 octets: int pcd(const double&d);
58 00000110 F3480FB807 popcnt rax, [rdi]
59 00000115 C3 ret
6 octets .
binary64
format à virgule flottante si elles le souhaitent? Certaines personnes (dont moi - même, d' abord) interprétaient la question exigeant que les fonctions acceptent des entrées comme un type entier comme C delong
. En C, vous pouvez affirmer que la langue se convertira pour vous, tout comme lorsque vous appelezsqrt((int)foo)
. Mais il existe des réponses asm de code machine x86 (comme codegolf.stackexchange.com/a/136360/30206 et la mienne) qui supposaient toutes les deux que nous devions accepter des entrées entières 64 bits. Accepter unebinary64
valeur économiserait 5 octets.