x86 función de código de máquina de 32 bits, 42 41 bytes
Actualmente, la respuesta más corta en lenguaje no relacionado con el golf, 1B más corta que @ streetster's q / kdb + .
Con 0 para verdadero y no cero para falso: 41 40 bytes. (en general, guarda 1 byte para 32 bits, 2 bytes para 64 bits).
Con cadenas de longitud implícita (estilo C con terminación 0): 45 44 bytes
Código de máquina x86-64 (con punteros de 32 bits, como el x32 ABI): 44 43 bytes .
x86-64 con cadenas de longitud implícita, todavía 46 bytes (la estrategia de mapa de bits de desplazamiento / máscara es equilibrada ahora).
Esta es una función con C firma _Bool dennis_like(size_t ecx, const char *esi)
. La convención de llamada es ligeramente no estándar, cercana a MS vectorcall / fastcall pero con diferentes registros arg: cadena en ESI y la longitud en ECX. Solo registra sus argumentos arg y EDX. AL contiene el valor de retorno, con los bytes altos que contienen basura (según lo permitido por las ABI S86 x86 y x32. IDK lo que dicen las ABI de MS sobre la basura alta al devolver bool o enteros estrechos).
Explicación del algoritmo. :
Pase por la cadena de entrada, filtre y clasifique en una matriz booleana en la pila: para cada byte, verifique si es un carácter alfabético (si no, continúe con el siguiente carácter) y transfórmelo en un entero de 0-25 (AZ) . Use ese entero 0-25 para verificar un mapa de bits de vocal = 0 / consonante = 1. (El mapa de bits se carga en un registro como una constante inmediata de 32 bits). Empuje 0 o 0xFF en la pila de acuerdo con el resultado del mapa de bits (en realidad en el byte bajo de un elemento de 32 bits, que puede tener basura en los 3 bytes superiores).
El primer bucle produce una matriz de 0 o 0xFF (en elementos dword rellenados con basura). Realice la comprobación de palíndromo habitual con un segundo bucle que se detiene cuando los punteros se cruzan en el medio (o cuando ambos apuntan al mismo elemento si hubiera un número impar de caracteres alfabéticos). El puntero que se mueve hacia arriba es el puntero de la pila, y usamos POP para cargar + incrementar. En lugar de comparar / establecer cc en este bucle, podemos usar XOR para detectar lo mismo / diferente ya que solo hay dos valores posibles. Podríamos acumular (con OR) si encontramos elementos que no coinciden, pero una ramificación temprana en las banderas establecidas por XOR es al menos igual de buena.
Observe que el segundo bucle usa byte
el tamaño de operando, por lo que no le importa qué basura deja el primer bucle fuera del byte bajo de cada elemento de la matriz.
Utiliza la instrucción no documentadasalc
para establecer AL desde CF, de la misma manera que lo sbb al,al
haría. Es compatible con todas las CPU Intel (excepto en el modo de 64 bits), ¡incluso Knight's Landing! Agner Fog enumera los tiempos para ello en todas las CPU AMD (incluida Ryzen), por lo que si los proveedores de x86 insisten en vincular ese byte de espacio de código de operación desde 8086, también podríamos aprovecharlo.
Trucos interesantes:
- truco de comparación sin firmar para una combinación de isalpha () y toupper (), y cero-extiende el byte para llenar eax, configurando para:
- mapa de bits inmediato en un registro para
bt
, inspirado en una buena salida del compilador paraswitch
.
- Crear una matriz de tamaño variable en la pila con push in a loop. (Estándar para asm, pero no es algo que pueda hacer con C para la versión de cadena de longitud implícita). Utiliza 4 bytes de espacio de pila para cada carácter de entrada, pero ahorra al menos 1 byte en comparación con el golf óptimo.
stosb
.
- En lugar de cmp / setne en la matriz booleana, XOR se junta para obtener un valor de verdad directamente. (
cmp
/ salc
no es una opción, porque salc
solo funciona para CF, y 0xFF-0 no establece CF. sete
es de 3 bytes, pero evitaría el inc
exterior del bucle, por un costo neto de 2 bytes (1 en modo de 64 bits )) vs.xor en el bucle y arreglarlo con 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
Esta es probablemente también una de las respuestas más rápidas, ya que ninguno de los juegos de golf realmente duele demasiado, al menos para cadenas de menos de unos pocos miles de caracteres donde el uso de memoria 4x no causa muchos errores de caché. (También puede ser que pierda a las respuestas que tienen un temprano de salida para la no-Dennis como cuerdas antes de bucle sobre todos los caracteres.) salc
Es más lenta que setcc
en muchas CPU (por ejemplo, 3 uops vs 1 en Skylake), pero con un control de mapa de bitsbt/salc
sigue siendo más rápido que una búsqueda de cadenas o una coincidencia de expresiones regulares. Y no hay gastos generales de inicio, por lo que es extremadamente barato para cadenas cortas.
Hacerlo de una vez sobre la marcha significaría repetir el código de clasificación para las direcciones arriba y abajo. Eso sería más rápido pero de mayor tamaño de código. (Por supuesto, si quiere rápido, puede hacer 16 o 32 caracteres a la vez con SSE2 o AVX2, aún utilizando el truco de comparación cambiando de rango al final del rango firmado).
Probar el programa (para ia32 o x32 Linux) para llamar a esta función con un cmdline arg, y salir con status = valor de retorno. strlen
implementación 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
Se podría usar una versión de 64 bits de esta función sbb eax,eax
, que es solo 2 bytes en lugar de 3 para setc al
. También necesitaría un byte adicional para dec
o not
al final (porque solo 32 bits tiene 1 byte inc / dec r32). Usando el x32 ABI (punteros de 32 bits en modo largo), aún podemos evitar los prefijos REX a pesar de que copiamos y comparamos punteros.
setc [rdi]
puede escribir directamente en la memoria, pero reservar bytes ECX de espacio de pila cuesta más tamaño de código que el que ahorra. (Y tenemos que movernos a través de la matriz de salida. [rdi+rcx]
Toma un byte adicional para el modo de direccionamiento, pero realmente necesitamos un contador que no se actualice para los caracteres filtrados, por lo que será peor que eso).
Esta es la fuente YASM / NASM con %if
condicionales. Se puede construir con -felf32
(código de 32 bits) o -felfx32
( código de 64 bits con x32 ABI), y con una longitud implícita o explícita . He probado las 4 versiones. Vea esta respuesta para un script para construir un binario estático a partir de la fuente NASM / YASM.
Para probar la versión de 64 bits en una máquina sin soporte para el x32 ABI, puede cambiar los registros del puntero a 64 bits. (Luego, simplemente reste el número de prefijos REX.W = 1 (0x48 bytes) del recuento. En este caso, 4 instrucciones necesitan prefijos REX para operar en registros de 64 bits). O simplemente llámelo con el rsp
puntero de entrada y en el bajo 4G del espacio de direcciones.
%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.
Miré a jugar con DF (la bandera de dirección que controla lodsd
/ scasd
y así sucesivamente), pero simplemente no parecía ser una victoria. Las ABI habituales requieren que el DF se borre al entrar y salir de la función. Suponiendo que esté despejado en la entrada, pero dejarlo configurado en la salida sería una trampa, en mi opinión. Sería bueno usar LODSD / SCASD para evitar los 3 bytes sub esi, 4
, especialmente en el caso de que no haya mucha basura.
Estrategia de mapa de bits alternativa (para cadenas de longitud implícita x86-64)
Resulta que esto no guarda ningún byte, porque bt r32,r32
aún funciona con mucha basura en el índice de bits. Simplemente no está documentado como shr
está.
En lugar de bt / sbb
obtener el bit dentro / fuera de CF, use un desplazamiento / máscara para aislar el bit que queremos del mapa de bits.
%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
Como esto produce 0/1 en AL al final (en lugar de 0 / 0xFF), podemos hacer la inversión necesaria del valor de retorno al final de la función con xor al, 1
(2B) en lugar de dec eax
(también 2B en x86-64) para todavía produce un valor apropiado bool
/ de_Bool
retorno.
Esto solía guardar 1B para x86-64 con cadenas de longitud implícita, al evitar la necesidad de poner a cero los bytes altos de EAX. (Había estado usando and eax, 0x7F ^ 0x20
para forzar a mayúsculas y poner a cero el resto de eax con un byte de 3 bytes and r32,imm8
. Pero ahora estoy usando la codificación de 2 byte de inmediato con AL que tienen la mayoría de las instrucciones 8086, como ya lo estaba haciendo para el sub
y cmp
.)
Pierde en bt
/ salc
en modo de 32 bits, y las cadenas de longitud explícita necesitan ECX para el recuento, por lo que tampoco funciona allí.
Pero luego me di cuenta de que estaba equivocado: bt edx, eax
todavía funciona con mucha basura en eax. Aparentemente enmascara el turno de contar la misma manera que shr r32, cl
lo hace (mirar sólo a los 5 bits bajas de Cl). Esto es diferente de bt [mem], reg
, que puede acceder fuera de la memoria a la que hace referencia el modo / tamaño de direccionamiento, tratándolo como una cadena de bits. (Crazy CISC ...)
El manual de referencia de Intel Insn Set no documenta el enmascaramiento, por lo que tal vez sea el comportamiento indocumentado lo que Intel está preservando por ahora. (Ese tipo de cosas no es infrecuente. bsf dst, src
Con src = 0 siempre deja dst sin modificar, aunque está documentado que deja dst con un valor indefinido en ese caso. AMD en realidad documenta el comportamiento src = 0). Probé en Skylake y Core2, y la bt
versión funciona con basura distinta de cero en EAX fuera de AL.
Un buen truco aquí es usar xchg eax,ecx
(1 byte) para obtener el recuento en CL. Desafortunadamente, BMI2 shrx eax, edx, eax
es de 5 bytes, frente a solo 2 bytes para shr eax, cl
. El uso bextr
necesita un byte de 2 mov ah,1
(para la cantidad de bits a extraer), por lo que nuevamente son 5 + 2 bytes como SHRX + AND.
El código fuente se ha vuelto bastante desordenado después de agregar %if
condicionales. Aquí está el desmontaje de cadenas de longitud implícita x32 (usando la estrategia alternativa para el mapa de bits, por lo que todavía son 46 bytes).
La principal diferencia con la versión de longitud explícita está en el primer bucle. Observe cómo hay un lods
antes, y en la parte inferior, en lugar de solo uno en la parte superior del bucle.
; 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