x86 Funzione codice macchina a 32 bit, 42 41 byte
Attualmente la risposta in lingua non golf più breve, 1 B più breve di q / kdb + di @ streetster .
Con 0 per la verità e diverso da zero per la falsità: 41 40 byte. (in generale, salva 1 byte per 32 bit, 2 byte per 64 bit).
Con stringhe a lunghezza implicita (stile C con terminazione 0): 45 44 byte
codice macchina x86-64 (con puntatori a 32 bit, come l'ABI x32): 44 43 byte .
x86-64 con stringhe a lunghezza implicita, ancora 46 byte (la strategia bitmap shift / maschera è ora in pareggio).
Questa è una funzione con la firma C _Bool dennis_like(size_t ecx, const char *esi)
. La convenzione di chiamata è leggermente non standard, vicina a MS vectorcall / fastcall ma con diversi registri arg: stringa in ESI e lunghezza in ECX. Limita solo i suoi arg-reg e EDX. AL contiene il valore di ritorno, con i byte alti che trattengono la spazzatura (come consentito dalle ABI SysV x86 e x32. IDK cosa dicono le ABI di MS su high-garbage quando restituiscono valori bool o interi stretti).
Spiegazione dell'algoritmo :
Passa sopra la stringa di input, filtra e classifica in un array booleano nello stack: per ogni byte, controlla se si tratta di un carattere alfabetico (in caso contrario, continua con il carattere successivo) e trasformalo in un numero intero compreso tra 0 e 25 (AZ) . Usa quell'intero 0-25 per controllare una bitmap di vocale = 0 / consonante = 1. (La bitmap viene caricata in un registro come costante immediata a 32 bit). Spingere 0 o 0xFF sullo stack in base al risultato bitmap (in realtà nel byte basso di un elemento a 32 bit, che può contenere immondizia nei primi 3 byte).
Il primo ciclo produce un array di 0 o 0xFF (in elementi dword riempiti con immondizia). Esegui il solito controllo del palindromo con un secondo ciclo che si interrompe quando i puntatori si incrociano nel mezzo (o quando entrambi puntano allo stesso elemento in presenza di un numero dispari di caratteri alfabetici). Il puntatore verso l'alto è il puntatore dello stack e usiamo POP per caricare + incremento. Invece di compare / setcc in questo loop, possiamo semplicemente usare XOR per rilevare lo stesso / diverso poiché ci sono solo due valori possibili. Potremmo accumulare (con OR) se avessimo trovato elementi non corrispondenti, ma almeno un ramo iniziale sulle bandiere impostato da XOR è altrettanto buono.
Si noti che il secondo ciclo utilizza byte
dimensioni dell'operando, quindi non importa quale immondizia lascia il primo ciclo al di fuori del byte basso di ciascun elemento dell'array.
Utilizza le istruzioni non documentatesalc
per impostare AL da CF, nello stesso modo in cui lo sbb al,al
farebbe. È supportato su tutte le CPU Intel (tranne in modalità 64-bit), anche su Knight's Landing! La nebbia di Agner elenca i tempi per questo anche su tutte le CPU AMD (inclusa Ryzen), quindi se i produttori x86 insistono nel vincolare lo spazio di quel codice operativo fin dall'8086, potremmo anche approfittarne.
Trucchi interessanti:
- trucco unsigned-compare per un isalpha () e un toupper () combinati e estende zero il byte per riempire eax, impostando per:
- bitmap immediata in un registro per
bt
, ispirata a un output di compilatore perswitch
.
- Creazione di un array di dimensioni variabili nello stack con push in un ciclo. (Standard per asm, ma non qualcosa che puoi fare con C per la versione di stringa a lunghezza implicita). Utilizza 4 byte di spazio di stack per ogni carattere di input, ma consente di risparmiare almeno 1 byte rispetto al golf ottimale in circolazione
stosb
.
- Invece di cmp / setne sull'array booleano, XOR booleana insieme per ottenere direttamente un valore di verità. (
cmp
/ salc
non è un'opzione, perché salc
funziona solo per CF e 0xFF-0 non imposta CF. sete
è 3 byte, ma eviterebbe inc
il ciclo esterno, per un costo netto di 2 byte (1 in modalità 64 bit )) vs. xor nel loop e risolvendolo 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
Questa è probabilmente anche una delle risposte più veloci, dal momento che nessuno del golf fa davvero troppo male, almeno per stringhe con poche migliaia di caratteri in cui l'utilizzo della memoria 4x non causa molti errori di cache. (Potrebbe anche perdere le risposte che richiedono un inizio per stringhe non simili a Dennis prima di eseguire il loop su tutti i caratteri.) salc
È più lento rispetto setcc
a molte CPU (ad esempio 3 uops contro 1 su Skylake), ma un controllo bitmap con bt/salc
è ancora più veloce di una ricerca di stringhe o di una corrispondenza regex. E non c'è sovraccarico di avvio, quindi è estremamente economico per stringhe brevi.
Farlo in un passaggio al volo significherebbe ripetere il codice di classificazione per le direzioni su e giù. Sarebbe più veloce ma di dimensioni maggiori del codice. (Ovviamente se vuoi velocemente, puoi fare 16 o 32 caratteri alla volta con SSE2 o AVX2, usando ancora il trucco di confronto spostando il range verso il fondo del range firmato).
Programma di test (per Linux ia32 o x32) per chiamare questa funzione con un arg cmdline e uscire con status = valore di ritorno. strlen
implementazione da 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
È possibile utilizzare una versione a 64 bit di questa funzione sbb eax,eax
, che è solo 2 byte anziché 3 per setc al
. Avrebbe anche bisogno di un byte extra per dec
o not
alla fine (perché solo 32 bit ha 1 byte inc / dec r32). Usando l'ABI x32 (puntatori a 32 bit in modalità lunga), possiamo ancora evitare i prefissi REX anche se copiamo e confrontiamo i puntatori.
setc [rdi]
può scrivere direttamente in memoria, ma riservare byte ECX di spazio nello stack costa più dimensioni del codice rispetto a quelle salvate. (E dobbiamo spostarci attraverso l'array di output. [rdi+rcx]
Richiede un byte in più per la modalità di indirizzamento, ma in realtà abbiamo bisogno di un contatore che non si aggiorni per i caratteri filtrati, quindi sarà peggio di così.)
Questa è la fonte YASM / NASM con %if
condizionali. Può essere costruito con -felf32
(codice a 32 bit) o -felfx32
(codice a 64 bit con l'ABI x32) e con lunghezza implicita o esplicita . Ho testato tutte e 4 le versioni. Vedi questa risposta per uno script per creare un binario statico dalla sorgente NASM / YASM.
Per testare la versione a 64 bit su una macchina senza supporto per l'ABI x32, è possibile modificare i reg del puntatore a 64 bit. (Quindi sottrarre semplicemente il numero di prefissi REX.W = 1 (0x48 byte) dal conteggio. In questo caso, 4 istruzioni hanno bisogno dei prefissi REX per funzionare su reg a 64 bit). O semplicemente chiamalo con il rsp
puntatore di input e nel basso 4G di spazio degli indirizzi.
%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.
Ho guardato in giro con DF (il flag di direzione che controlla lodsd
/ scasd
e così via), ma non sembrava essere una vittoria. I soliti ABI richiedono che DF venga cancellato all'entrata e all'uscita della funzione. Supponendo che sia stato eliminato all'entrata, ma lasciarlo impostato all'uscita sarebbe un imbroglio, IMO. Sarebbe bello usare LODSD / SCASD per evitare i 3 byte sub esi, 4
, specialmente nel caso in cui non ci siano rifiuti elevati.
Strategia bitmap alternativa (per stringhe di lunghezza implicita x86-64)
Si scopre che questo non salva alcun byte, perché bt r32,r32
funziona ancora con immondizia elevata nell'indice di bit. Non è documentato così shr
com'è.
Invece di bt / sbb
ottenere il bit dentro / fuori da CF, usa uno shift / maschera per isolare il bit che vogliamo dalla 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
Dato che questo produce 0/1 in AL alla fine (invece di 0 / 0xFF), possiamo fare la necessaria inversione del valore di ritorno alla fine della funzione con xor al, 1
(2B) invece di dec eax
(anche 2B in x86-64) a produce ancora un valore di ritorno bool
/_Bool
corretto .
Questo ha usato per salvare 1B per x86-64 con stringhe di lunghezza implicita, evitando la necessità di azzerare i byte alti di EAX. (Stavo usando and eax, 0x7F ^ 0x20
per forzare in maiuscolo e azzerare il resto di eax con un 3 byte and r32,imm8
. Ma ora sto usando la codifica a 2 byte immediata con AL che la maggior parte delle istruzioni 8086 hanno, come già stavo facendo per il sub
e cmp
.)
Si perde in bt
/ salc
nella modalità a 32 bit e le stringhe di lunghezza esplicita richiedono ECX per il conteggio, quindi non funziona neanche lì.
Ma poi ho capito che avevo torto: bt edx, eax
funziona ancora con alta immondizia in eax. Apparentemente maschere il conteggio spostamento nello stesso modo shr r32, cl
fa (guardando solo basse 5 bit di cl). Questo è diverso dal bt [mem], reg
quale può accedere all'esterno della memoria a cui fa riferimento la modalità / dimensione di indirizzamento, trattandolo come una stringa di bit. (Crazy CISC ...)
Il manuale di riferimento insn set di Intel non documenta il mascheramento, quindi forse è il comportamento non documentato che Intel sta conservando per ora. (Questo genere di cose non è insolito. bsf dst, src
Con src = 0 lascia sempre dst non modificato, anche se è documentato che in questo caso dst abbia un valore indefinito. AMD documenta effettivamente il comportamento src = 0.) Ho provato su Skylake e Core2, e la bt
versione funziona con immondizia diversa da zero in EAX all'esterno di AL.
Un trucco pulito qui sta usando xchg eax,ecx
(1 byte) per ottenere il conteggio in CL. Sfortunatamente, BMI2 shrx eax, edx, eax
è di 5 byte, contro solo 2 byte per shr eax, cl
. L'utilizzo bextr
richiede un 2 byte mov ah,1
(per il numero di bit da estrarre), quindi sono di nuovo 5 + 2 byte come SHRX + AND.
Il codice sorgente è diventato piuttosto disordinato dopo l'aggiunta di %if
condizionali. Ecco lo smontaggio delle stringhe di lunghezza implicita x32 (usando la strategia alternativa per la bitmap, quindi è ancora 46 byte).
La differenza principale rispetto alla versione a lunghezza esplicita è nel primo ciclo. Notate come c'è un lods
prima, e in fondo, invece di uno solo all'inizio del ciclo.
; 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