função de código de máquina x86 de 32 bits, 42 41 bytes
Atualmente, a resposta mais curta no idioma não-golfe, 1B menor que o q / kdb + do @ streetster .
Com 0 para verdade e diferente de zero para falsidade: 41 40 bytes. (em geral, economiza 1 byte para 32 bits, 2 bytes para 64 bits).
Com seqüências de comprimento implícito (terminação 0 no estilo C): 45 44 bytes
código de máquina x86-64 (com ponteiros de 32 bits, como o ABI x32): 44 43 bytes .
x86-64 com seqüências de comprimento implícito, ainda 46 bytes (a estratégia de bitmap de deslocamento / máscara está equilibrada agora).
Esta é uma função com a assinatura C _Bool dennis_like(size_t ecx, const char *esi)
. A convenção de chamada é um pouco fora do padrão, próxima ao MS vectorcall / fastcall, mas com diferentes registros arg: string no ESI e o comprimento no ECX. Ele só derruba seus arg-regs e EDX. O AL mantém o valor de retorno, com os bytes altos mantendo o lixo (conforme permitido pelas ABIs do SysV x86 e x32. IDK o que as ABIs da MS dizem sobre o lixo alto ao retornar números inteiros estreitos ou booleanos).
Explicação do algoritmo :
Faça um loop sobre a sequência de entrada, filtrando e classificando em uma matriz booleana na pilha: para cada byte, verifique se é um caractere alfabético (se não, continue para o próximo caractere) e transforme-o em um número inteiro de 0 a 25 (AZ) . Use esse número 0-25 para verificar um bitmap da vogal = 0 / consoante = 1. (O bitmap é carregado em um registro como uma constante imediata de 32 bits). Empurre 0 ou 0xFF na pilha de acordo com o resultado do bitmap (na verdade, no byte baixo de um elemento de 32 bits, que pode ter lixo nos 3 bytes principais).
O primeiro loop produz uma matriz de 0 ou 0xFF (em elementos dword preenchidos com lixo). Faça a verificação habitual do palíndromo com um segundo loop que para quando os ponteiros se cruzam no meio (ou quando ambos apontam para o mesmo elemento se houver um número ímpar de caracteres alfabéticos). O ponteiro para cima é o ponteiro da pilha e usamos o POP para carregar + incrementar. Em vez de comparar / setcc nesse loop, podemos apenas usar o XOR para detectar o mesmo / diferente, pois existem apenas dois valores possíveis. Poderíamos acumular (com OR) se encontrássemos elementos não correspondentes, mas uma ramificação antecipada nos sinalizadores definidos pelo XOR é pelo menos tão boa.
Observe que o segundo loop usa byte
tamanho de operando, portanto, não importa o lixo que o primeiro loop deixa fora do byte baixo de cada elemento da matriz.
Ele usa a instrução não documentadasalc
para definir AL a partir de CF, da mesma maneira que sbb al,al
faria. É suportado em todas as CPUs Intel (exceto no modo de 64 bits), até no Knight's Landing! O Agner Fog também lista os horários para todos os processadores AMD (incluindo Ryzen), por isso, se os fornecedores de x86 insistirem em vincular esse byte de espaço do código de operação desde 8086, é melhor aproveitarmos isso.
Truques interessantes:
- truque de comparação não assinada para um isalpha () e um toupper () combinados e estende zero o byte para preencher o eax, configurando para:
- bitmap imediato em um registro para
bt
, inspirado por uma saída agradável do compilador paraswitch
.
- Criando uma matriz de tamanho variável na pilha com push em um loop. (Padrão para asm, mas não algo que você possa fazer com C para a versão de cadeia de comprimento implícito). Ele usa 4 bytes de espaço na pilha para cada caractere de entrada, mas economiza pelo menos 1 byte em relação ao golfe ideal
stosb
.
- Em vez de cmp / setne na matriz booleana, o XOR se ajusta para obter um valor verdade diretamente. (
cmp
/ salc
não é uma opção, porque salc
funciona apenas para CF e 0xFF-0 não define CF. sete
tem 3 bytes, mas evitaria o inc
exterior do loop, por um custo líquido de 2 bytes (1 no modo de 64 bits )) vs. xor no loop e corrigindo-o com 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
Essa é provavelmente também uma das respostas mais rápidas, já que nenhum golfe realmente dói muito, pelo menos para cadeias de caracteres com menos de alguns milhares de caracteres em que o uso da memória 4x não causa muitos erros de cache. (Também pode ser prejudicial para as respostas anteriores a seqüências que não sejam do tipo Dennis antes de repetir todos os caracteres.) salc
É mais lento do que setcc
em muitas CPUs (por exemplo, 3 uops vs. 1 no Skylake), mas uma verificação de bitmap com bt/salc
ainda é mais rápido que uma pesquisa por sequência ou correspondência de expressão regular. E não há sobrecarga de inicialização, por isso é extremamente barato para strings curtas.
Fazer isso de uma só vez, significa repetir o código de classificação para as direções para cima e para baixo. Isso seria mais rápido, mas maior tamanho de código. (Obviamente, se você quiser rápido, poderá executar 16 ou 32 caracteres por vez com o SSE2 ou AVX2, ainda usando o truque de comparação, deslocando o intervalo para a parte inferior do intervalo assinado).
Teste o programa (para ia32 ou x32 Linux) para chamar esta função com um cmdline arg e saia com status = return value. strlen
implementação do 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
Uma versão de 64 bits dessa função poderia usar sbb eax,eax
, que é de apenas 2 bytes em vez de 3 para setc al
. Também seria necessário um byte extra para dec
ou not
no final (porque apenas 32 bits possui 1 byte inc / dec r32). Usando a ABI x32 (ponteiros de 32 bits no modo longo), ainda podemos evitar prefixos REX, mesmo que copiemos e comparemos ponteiros.
setc [rdi]
pode gravar diretamente na memória, mas reservar bytes ECX de espaço na pilha custa mais tamanho de código do que isso economiza. (E precisamos percorrer a matriz de saída. [rdi+rcx]
Leva um byte extra para o modo de endereçamento, mas realmente precisamos de um contador que não atualize para caracteres filtrados, para que seja pior do que isso.)
Essa é a fonte YASM / NASM com %if
condicionais. Ele pode ser criado com -felf32
(código de 32 bits) ou -felfx32
( código de 64 bits com a ABI x32) e com comprimento implícito ou explícito . Eu testei todas as 4 versões. Consulte esta resposta para obter um script para construir um binário estático a partir da fonte NASM / YASM.
Para testar a versão de 64 bits em uma máquina sem suporte para a ABI x32, você pode alterar os registros do ponteiro para 64 bits. (Em seguida, basta subtrair o número de prefixos REX.W = 1 (0x48 bytes) da contagem. Nesse caso, 4 instruções precisam de prefixos REX para operar em regs de 64 bits). Ou simplesmente chame-o com o rsp
e o ponteiro de entrada no baixo espaço de endereço 4G.
%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.
Eu olhei para mexer com o DF (a bandeira de direção que controla lodsd
/ scasd
e assim por diante), mas simplesmente não parecia ser uma vitória. As ABIs usuais exigem que o DF seja limpo na entrada e saída da função. Assumir que a permissão foi liberada na entrada, mas deixá-la definida na saída seria trapaça, IMO. Seria bom usar o LODSD / SCASD para evitar os 3 bytes sub esi, 4
, especialmente no caso em que não há muito lixo.
Estratégia alternativa de bitmap (para cadeias de comprimento implícito x86-64)
Acontece que isso não salva bytes, porque bt r32,r32
ainda funciona com alto lixo no índice de bits. Simplesmente não está documentado do jeito que shr
está.
Em vez de bt / sbb
obter o bit dentro / fora do CF, use um shift / mask para isolar o bit que queremos do 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
Como isso produz 0/1 em AL no final (em vez de 0 / 0xFF), podemos fazer a inversão necessária do valor de retorno no final da função com xor al, 1
(2B) em vez de dec eax
(também 2B em x86-64) para ainda produz um valor adequado bool
/ de_Bool
retorno.
Isso costumava salvar 1B para x86-64 com cadeias de comprimento implícito, evitando a necessidade de zerar os bytes altos do EAX. (Eu estava usando and eax, 0x7F ^ 0x20
para forçar para maiúsculas e zerar o restante do eax com 3 bytes and r32,imm8
. Mas agora estou usando a codificação imediata com AL de 2 bytes que a maioria das instruções do 8086 possui, como eu já estava fazendo para o sub
e cmp
.)
Ele perde para bt
/ salc
no modo de 32 bits e as seqüências de comprimento explícito precisam de ECX para a contagem, para que isso também não funcione.
Mas então eu percebi que estava errado: bt edx, eax
ainda trabalha com alto teor de lixo em eax. Aparentemente mascara o deslocamento contar da mesma forma shr r32, cl
faz (olhando apenas para os baixos 5 bits de cl). É diferente de bt [mem], reg
, que pode acessar fora da memória referenciada pelo modo / tamanho de endereçamento, tratando-a como uma cadeia de bits. (CISC louco ...)
O manual insn set ref da Intel não documenta a máscara, então talvez seja um comportamento não documentado que a Intel esteja preservando por enquanto. (Esse tipo de coisa não é incomum. bsf dst, src
Com src = 0 sempre deixa o dst sem modificação, mesmo que esteja documentado para deixar o dst mantendo um valor indefinido nesse caso. A AMD realmente documenta o comportamento do src = 0.) Testei no Skylake e no Core2, e a bt
versão funciona com lixo diferente de zero no EAX fora do AL.
Um truque interessante aqui é usar xchg eax,ecx
(1 byte) para obter a contagem no CL. Infelizmente, o IMC2 shrx eax, edx, eax
é de 5 bytes, contra apenas 2 bytes para shr eax, cl
. O uso bextr
precisa de 2 bytes mov ah,1
(para o número de bits a serem extraídos), portanto, são novamente 5 + 2 bytes como SHRX + AND.
O código fonte ficou bastante bagunçado depois de adicionar %if
condicionais. Aqui está a desmontagem de cadeias de comprimento implícito x32 (usando a estratégia alternativa para o bitmap, então ainda tem 46 bytes).
A principal diferença da versão de tamanho explícito está no primeiro loop. Observe como há um lods
antes dele e na parte inferior, em vez de apenas um na parte superior do loop.
; 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