Fonction de code machine x86 32 bits, 42 à 41 octets
Actuellement, la réponse la plus courte dans une langue autre que le golf est 1B plus courte que q / kdb + de @ streetster .
Avec 0 pour la vérité et non nul pour la fausseté: 41 à 40 octets. (en général, enregistre 1 octet pour 32 bits, 2 octets pour 64 bits).
Avec des chaînes de longueur implicite (style C terminé par 0): 45 44 octets
Code machine x86-64 (avec pointeurs 32 bits, comme l’ABI x32): 44 43 octets .
x86-64 avec des chaînes de longueur implicite, toujours 46 octets (la stratégie de bitmap décalage / masque est équilibrée à présent).
Ceci est une fonction avec la signature C _Bool dennis_like(size_t ecx, const char *esi)
. La convention d'appel est légèrement non standard, proche de MS vectorcall / fastcall mais avec des registres arg différents: string dans ESI et la longueur dans ECX. Il ne fait que clobber ses arg-regs et EDX. AL conserve la valeur de retour, avec les octets élevés contenant des déchets (comme le permettent les ABI SysV x86 et x32. IDK ce que les ABI de MS disent à propos des déchets élevés lors du renvoi de nombres entiers bool ou étroit).
Explication de l'algorithme :
Boucle sur la chaîne d'entrée, filtrage et classification en un tableau booléen sur la pile: pour chaque octet, vérifiez s'il s'agit d'un caractère alphabétique (sinon, passez au caractère suivant) et transformez-le en entier de 0 à 25 (AZ) . Utilisez cet entier 0-25 pour vérifier un bitmap de voyelle = 0 / consonne = 1. (Le bitmap est chargé dans un registre en tant que constante immédiate 32 bits). Poussez 0 ou 0xFF sur la pile en fonction du résultat bitmap (en fait, dans l'octet de poids faible d'un élément 32 bits, ce qui peut avoir des défauts dans les 3 premiers octets).
La première boucle produit un tableau de 0 ou 0xFF (dans les éléments dword complétés avec garbage). Effectuez la vérification palindrome habituelle avec une deuxième boucle qui s’arrête lorsque les pointeurs se croisent au milieu (ou quand ils pointent tous deux vers le même élément s’il existe un nombre impair de caractères alphabétiques). Le pointeur qui se déplace vers le haut est le pointeur de la pile et nous utilisons POP pour charger + incrémenter. Au lieu de comparer / setcc dans cette boucle, nous pouvons simplement utiliser XOR pour détecter same / different puisqu'il n'y a que deux valeurs possibles. Nous pourrions accumuler (avec OR) si nous avons trouvé des éléments non correspondants, mais une branche précurseur sur les drapeaux définis par XOR est au moins aussi bonne.
Notez que la seconde boucle utilise la byte
taille de l'opérande, elle ne tient donc pas compte de ce que la première boucle laisse en dehors de l'octet de poids faible de chaque élément du tableau.
Il utilise l' instruction non documentéesalc
pour définir AL à partir de CF, de la même manière sbb al,al
. Il est pris en charge sur tous les processeurs Intel (sauf en mode 64 bits), même dans Knight's Landing! Agner Fog en indique également le minutage sur tous les processeurs AMD (y compris Ryzen). Par conséquent, si les fournisseurs x86 insistent pour bloquer cet octet d'espace disque depuis 8086, nous pourrions également en profiter.
Trucs intéressants:
- astuce - Comparaison astuce pour un isalpha () et toupper () combinés, et zéro-étend l'octet pour remplir eax, mise en place pour:
- bitmap immédiat dans un registre pour
bt
, inspiré par quelques bons résultats du compilateur pourswitch
.
- Création d'un tableau de taille variable sur la pile avec insertion dans une boucle. (Standard pour asm, mais pas quelque chose que vous pouvez faire avec C pour la version de chaîne de longueur implicite). Il utilise 4 octets d’espace de pile pour chaque caractère saisi, mais enregistre au moins 1 octet par rapport à un jeu optimal
stosb
.
- Au lieu de cmp / setne sur le tableau booléen, XOR booléens ensemble pour obtenir directement une valeur de vérité. (
cmp
/ salc
n’est pas une option, car salc
ne fonctionne que pour CF, et 0xFF-0 ne définit pas CF. sete
est de 3 octets, mais éviterait l’ inc
extérieur de la boucle, pour un coût net de 2 octets (1 en mode 64 bits )) vs xor dans la boucle et le réparer avec inc.
; explicit-length version: input string in ESI, byte count in ECX
08048060 <dennis_like>:
8048060: 55 push ebp
8048061: 89 e5 mov ebp,esp ; a stack frame lets us restore esp with LEAVE (1B)
8048063: ba ee be ef 03 mov edx,0x3efbeee ; consonant bitmap
08048068 <dennis_like.filter_loop>:
8048068: ac lods al,BYTE PTR ds:[esi]
8048069: 24 5f and al,0x5f ; uppercase
804806b: 2c 41 sub al,0x41 ; range-shift to 0..25
804806d: 3c 19 cmp al,0x19 ; reject non-letters
804806f: 77 05 ja 8048076 <dennis_like.non_alpha>
8048071: 0f a3 c2 bt edx,eax # AL = 0..25 = position in alphabet
8048074: d6 SALC ; set AL=0 or 0xFF from carry. Undocumented insn, but widely supported
8048075: 50 push eax
08048076 <dennis_like.non_alpha>:
8048076: e2 f0 loop 8048068 <dennis_like.filter_loop> # ecx = remaining string bytes
; end of first loop
8048078: 89 ee mov esi,ebp ; ebp = one-past-the-top of the bool array
0804807a <dennis_like.palindrome_loop>:
804807a: 58 pop eax ; read from the bottom
804807b: 83 ee 04 sub esi,0x4
804807e: 32 06 xor al,BYTE PTR [esi]
8048080: 75 04 jne 8048086 <dennis_like.non_palindrome>
8048082: 39 e6 cmp esi,esp ; until the pointers meet or cross in the middle
8048084: 77 f4 ja 804807a <dennis_like.palindrome_loop>
08048086 <dennis_like.non_palindrome>:
; jump or fall-through to here with al holding an inverted boolean
8048086: 40 inc eax
8048087: c9 leave
8048088: c3 ret
;; 0x89 - 0x60 = 41 bytes
C’est probablement aussi l’une des réponses les plus rapides, puisqu’aucune partie du golf ne fait vraiment mal, du moins pour les chaînes de quelques milliers de caractères où l’utilisation de la mémoire 4x ne cause pas beaucoup de pertes de mémoire cache. (Il pourrait aussi perdre des réponses qui prennent une précoce pour non-Dennis comme des chaînes avant en boucle sur tous les caractères.) salc
Est plus lent que setcc
sur plusieurs unités centrales (par exemple 3 uops contre 1 sur Skylake), mais un contrôle bitmap avec bt/salc
est toujours plus rapide qu'une recherche de chaîne ou une correspondance de regex. Et comme il n’ya pas de frais généraux de démarrage, c’est donc extrêmement économique pour les chaînes courtes.
Le faire en une passe à la volée impliquerait de répéter le code de classification pour les directions montante et descendante. Ce serait plus rapide mais une plus grande taille de code. (Bien sûr, si vous voulez aller vite, vous pouvez faire 16 ou 32 caractères à la fois avec SSE2 ou AVX2, en utilisant toujours l’astuce de comparaison en décalant l’échelle vers le bas de l’étendue signée).
Programme de test (pour ia32 ou x32 Linux) pour appeler cette fonction avec un argument cmdline, et quitter avec status = valeur de retour. strlen
implémentation de int80h.org .
; build with the same %define macros as the source below (so this uses 32-bit regs in 32-bit mode)
global _start
_start:
;%define PTRSIZE 4 ; true for x32 and 32-bit mode.
mov esi, [rsp+4 + 4*1] ; esi = argv[1]
;mov rsi, [rsp+8 + 8*1] ; rsi = argv[1] ; For regular x86-64 (not x32)
%if IMPLICIT_LENGTH == 0
; strlen(esi)
mov rdi, rsi
mov rcx, -1
xor eax, eax
repne scasb ; rcx = -strlen - 2
not rcx
dec rcx
%endif
mov eax, 0xFFFFAEBB ; make sure the function works with garbage in EAX
call dennis_like
;; use the 32-bit ABI _exit syscall, even in x32 code for simplicity
mov ebx, eax
mov eax, 1
int 0x80 ; _exit( dennis_like(argv[1]) )
;; movzx edi, al ; actually mov edi,eax is fine here, too
;; mov eax,231 ; 64-bit ABI exit_group( same thing )
;; syscall
Une version 64 bits de cette fonction pourrait utiliser sbb eax,eax
, qui n’est que 2 octets au lieu de 3 pour setc al
. Il faudrait également un octet supplémentaire pour dec
ou not
à la fin (car seul 32 bits a un octet inc / dec r32). En utilisant l'ABI x32 (pointeurs 32 bits en mode long), nous pouvons toujours éviter les préfixes REX même si nous copions et comparons des pointeurs.
setc [rdi]
peut écrire directement dans la mémoire, mais réserver des octets ECX d’espace de pile coûte plus de taille de code que ce qui est économisé. (Et nous devons nous déplacer dans le tableau de sortie. [rdi+rcx]
Prend un octet supplémentaire pour le mode d'adressage, mais nous avons réellement besoin d'un compteur qui ne soit pas mis à jour pour les caractères filtrés, ce qui sera pire que cela.)
C'est la source YASM / NASM avec des %if
conditions. Il peut être construit avec -felf32
(code 32 bits) ou -felfx32
( code 64 bits avec l'ABI x32), et avec une longueur implicite ou explicite . J'ai testé les 4 versions. Voir cette réponse pour un script permettant de créer un binaire statique à partir d'une source NASM / YASM.
Pour tester la version 64 bits sur un ordinateur ne prenant pas en charge l'interface ABI x32, vous pouvez modifier les paramètres de pointeur sur 64 bits. (Ensuite, soustrayez simplement le nombre de préfixes REX.W = 1 (0x48 octets). Dans ce cas, 4 instructions nécessitent des préfixes REX pour fonctionner sur des registres 64 bits). Ou appelez-le simplement avec le rsp
et le pointeur d'entrée dans la 4G basse de l'espace d'adressage.
%define IMPLICIT_LENGTH 0
; This source can be built as x32, or as plain old 32-bit mode
; x32 needs to push 64-bit regs, and using them in addressing modes avoids address-size prefixes
; 32-bit code needs to use the 32-bit names everywhere
;%if __BITS__ != 32 ; NASM-only
%ifidn __OUTPUT_FORMAT__, elfx32
%define CPUMODE 64
%define STACKWIDTH 8 ; push / pop 8 bytes
%else
%define CPUMODE 32
%define STACKWIDTH 4 ; push / pop 4 bytes
%define rax eax
%define rcx ecx
%define rsi esi
%define rdi edi
%define rbp ebp
%define rsp esp
%endif
; A regular x86-64 version needs 4 REX prefixes to handle 64-bit pointers
; I haven't cluttered the source with that, but I guess stuff like %define ebp rbp would do the trick.
;; Calling convention similar to SysV x32, or to MS vectorcall, but with different arg regs
;; _Bool dennis_like_implicit(const char *esi)
;; _Bool dennis_like_explicit(size_t ecx, const char *esi)
global dennis_like
dennis_like:
; We want to restore esp later, so make a stack frame for LEAVE
push rbp
mov ebp, esp ; enter 0,0 is 4 bytes. Only saves bytes if we had a fixed-size allocation to do.
; ZYXWVUTSRQPONMLKJIHGFEDCBA
mov edx, 11111011111011111011101110b ; consonant/vowel bitmap for use with bt
;;; assume that len >= 1
%if IMPLICIT_LENGTH
lodsb ; pipelining the loop is 1B shorter than jmp .non_alpha
.filter_loop:
%else
.filter_loop:
lodsb
%endif
and al, 0x7F ^ 0x20 ; force ASCII to uppercase.
sub al, 'A' ; range-shift to 'A' = 0
cmp al, 'Z'-'A' ; if al was less than 'A', it will be a large unsigned number
ja .non_alpha
;; AL = position in alphabet (0-25)
bt edx, eax ; 3B
%if CPUMODE == 32
salc ; 1B only sets AL = 0 or 0xFF. Not available in 64-bit mode
%else
sbb eax, eax ; 2B eax = 0 or -1, according to CF.
%endif
push rax
.non_alpha:
%if IMPLICIT_LENGTH
lodsb
test al,al
jnz .filter_loop
%else
loop .filter_loop
%endif
; al = potentially garbage if the last char was non-alpha
; esp = bottom of bool array
mov esi, ebp ; ebp = one-past-the-top of the bool array
.palindrome_loop:
pop rax
sub esi, STACKWIDTH
xor al, [rsi] ; al = (arr[up] != arr[--down]). 8-bit operand-size so flags are set from the non-garbage
jnz .non_palindrome
cmp esi, esp
ja .palindrome_loop
.non_palindrome: ; we jump here with al=1 if we found a difference, or drop out of the loop with al=0 for no diff
inc eax ;; AL transforms 0 -> 1 or 0xFF -> 0.
leave
ret ; return value in AL. high bytes of EAX are allowed to contain garbage.
J'ai regardé déconner avec DF (le drapeau de direction qui contrôle lodsd
/ scasd
et ainsi de suite), mais cela ne semblait pas être une victoire. Les ABI habituelles requièrent que DF soit effacé à l’entrée et à la sortie de la fonction. En supposant que l’entrée soit effacée à l’entrée mais qu’elle reste réglée à la sortie, ce serait de la triche, IMO. Il serait bien d’utiliser LODSD / SCASD pour éviter les 3 octets sub esi, 4
, en particulier dans les cas où il n’ya pas de déchets élevés.
Stratégie de bitmap alternative (pour les chaînes de longueur implicite x86-64)
Il s'avère que cela ne sauvegarde aucun octet, car bt r32,r32
il fonctionne toujours avec un niveau élevé de garbage dans l'index de bits. Ce n'est pas documenté comme ça shr
.
Au lieu d’ bt / sbb
obtenir le bit dans / hors de CF, utilisez un décalage / masque pour isoler le bit souhaité du bitmap.
%if IMPLICIT_LENGTH && CPUMODE == 64
; incompatible with LOOP for explicit-length, both need ECX. In that case, bt/sbb is best
xchg eax, ecx
mov eax, 11111011111011111011101110b ; not hoisted out of the loop
shr eax, cl
and al, 1
%else
bt edx, eax
sbb eax, eax
%endif
push rax
Puisque ceci produit 0/1 en AL à la fin (au lieu de 0 / 0xFF), nous pouvons faire l’inversion nécessaire de la valeur de retour à la fin de la fonction avec xor al, 1
(2B) au lieu de dec eax
(également 2B dans x86-64) pour toujours produire une valeur correcte bool
/ de_Bool
retour.
Cela permettait de sauvegarder 1B pour x86-64 avec des chaînes de longueur implicite, en évitant la nécessité de mettre à zéro les octets de poids fort de EAX. (J'avais l'habitude and eax, 0x7F ^ 0x20
de forcer à mettre en majuscule et à zéro le reste de eax avec un 3 octets and r32,imm8
. Mais maintenant, j'utilise le codage à 2 octets immédiat-avec-AL que la plupart des instructions 8086 ont, comme je le faisais déjà. pour le sub
et cmp
.)
Il perd en bt
/ salc
en mode 32 bits et les chaînes de longueur explicite ont besoin de ECX pour le décompte, de sorte que cela ne fonctionne pas là non plus.
Mais ensuite, j'ai réalisé que j'avais tort: je bt edx, eax
travaille toujours avec des déchets très élevés dans eax. Apparemment, cela masque le compte de décalage de la même manièreshr r32, cl
(en ne regardant que les 5 bits les plus bas de cl). Ceci est différent de bt [mem], reg
ce qui peut accéder en dehors de la mémoire référencée par le mode / taille d'adressage, en le traitant comme une chaîne de bits. (Crazy CISC ...)
Le manuel insn set ref d'Intel ne documente pas le masquage. Il s'agit donc peut-être d'un comportement non documenté qu'Intel préserve pour le moment. (Ce genre de chose n'est pas rare. bsf dst, src
Avec src = 0, dst reste toujours non modifié, même s'il est documenté de laisser dst détenir une valeur indéfinie dans ce cas. AMD documente le comportement de src = 0.) J'ai testé sur Skylake et Core2, et la bt
version fonctionne avec des ordures non nulles dans EAX en dehors de AL.
Une astuce intéressante consiste à utiliser xchg eax,ecx
(1 octet) pour obtenir le nombre dans CL. Malheureusement, BMI2 shrx eax, edx, eax
est de 5 octets, contre seulement 2 octets pour shr eax, cl
. L'utilisation bextr
nécessite 2 octets mov ah,1
(pour le nombre de bits à extraire), donc encore 5 + 2 octets comme SHRX + AND.
Le code source est devenu assez compliqué après l’ajout de %if
conditions. Voici le désassemblage des chaînes de longueur implicite x32 (en utilisant la stratégie alternative pour le bitmap, donc il reste 46 octets).
La principale différence par rapport à la version à longueur explicite réside dans la première boucle. Remarquez comment il y a un lods
avant, et au bas, au lieu d'un seul en haut de la boucle.
; 64-bit implicit-length version using the alternate bitmap strategy
00400060 <dennis_like>:
400060: 55 push rbp
400061: 89 e5 mov ebp,esp
400063: ac lods al,BYTE PTR ds:[rsi]
00400064 <dennis_like.filter_loop>:
400064: 24 5f and al,0x5f
400066: 2c 41 sub al,0x41
400068: 3c 19 cmp al,0x19
40006a: 77 0b ja 400077 <dennis_like.non_alpha>
40006c: 91 xchg ecx,eax
40006d: b8 ee be ef 03 mov eax,0x3efbeee ; inside the loop since SHR destroys it
400072: d3 e8 shr eax,cl
400074: 24 01 and al,0x1
400076: 50 push rax
00400077 <dennis_like.non_alpha>:
400077: ac lods al,BYTE PTR ds:[rsi]
400078: 84 c0 test al,al
40007a: 75 e8 jne 400064 <dennis_like.filter_loop>
40007c: 89 ee mov esi,ebp
0040007e <dennis_like.palindrome_loop>:
40007e: 58 pop rax
40007f: 83 ee 08 sub esi,0x8
400082: 32 06 xor al,BYTE PTR [rsi]
400084: 75 04 jne 40008a <dennis_like.non_palindrome>
400086: 39 e6 cmp esi,esp
400088: 77 f4 ja 40007e <dennis_like.palindrome_loop>
0040008a <dennis_like.non_palindrome>:
40008a: ff c8 dec eax ; invert the 0 / non-zero status of AL. xor al,1 works too, and produces a proper bool.
40008c: c9 leave
40008d: c3 ret
0x8e - 0x60 = 0x2e = 46 bytes