x86 Funzione codice macchina a 32 bit, 21 byte
funzione codice macchina x86-64, 22 byte
1B salvataggio in modalità a 32 bit richiede utilizzando separatore = riempitivo-1, ad esempio fill=0e sep=/. La versione a 22 byte può utilizzare una scelta arbitraria di separatore e riempimento.
Questa è la versione a 21 byte, con input-separator = \n(0xa), output-filler = 0, output-separator = /= filler-1. Queste costanti possono essere facilmente modificate.
; see the source for more comments
; RDI points to the output buffer, RSI points to the src string
; EDX holds the base
; This is the 32-bit version.
; The 64-bit version is the same, but the DEC is one byte longer (or we can just mov al,output_separator)
08048080 <str_exp>:
8048080: 6a 01 push 0x1
8048082: 59 pop ecx ; ecx = 1 = base**0
8048083: ac lods al,BYTE PTR ds:[esi] ; skip the first char so we don't do too many multiplies
; read an input row and accumulate base**n as we go.
08048084 <str_exp.read_bar>:
8048084: 0f af ca imul ecx,edx ; accumulate the exponential
8048087: ac lods al,BYTE PTR ds:[esi]
8048088: 3c 0a cmp al,0xa ; input_separator = newline
804808a: 77 f8 ja 8048084 <str_exp.read_bar>
; AL = separator or terminator
; flags = below (CF=1) or equal (ZF=1). Equal also implies CF=0 in this case.
; store the output row
804808c: b0 30 mov al,0x30 ; output_filler
804808e: f3 aa rep stos BYTE PTR es:[edi],al ; ecx bytes of filler
8048090: 48 dec eax ; mov al,output_separator
8048091: aa stos BYTE PTR es:[edi],al ;append delim
; CF still set from the inner loop, even after DEC clobbers the other flags
8048092: 73 ec jnc 8048080 <str_exp> ; new row if this is a separator, not terminator
8048094: c3 ret
08048095 <end_of_function>
; 0x95 - 0x80 = 0x15 = 21 bytes
La versione a 64 bit è più lunga di 1 byte, usando un DEC a 2 byte o a mov al, output_separator. A parte questo, il codice macchina è lo stesso per entrambe le versioni, ma alcuni nomi di registro cambiano (ad es. rcxAnziché ecxin pop).
Esempio di output dall'esecuzione del programma di test (base 3):
$ ./string-exponential $'.\n..\n...\n....' $(seq 3);echo
000/000000000/000000000000000000000000000/000000000000000000000000000000000000000000000000000000000000000000000000000000000/
Algoritmo :
Passa sopra l'input, facendo exp *= baseper ogni carattere di riempimento. Sui delimitatori e sul byte zero finale, aggiungere expbyte di riempimento e quindi un separatore alla stringa di output e reimpostare su exp=1. È molto comodo che l'ingresso sia garantito per non terminare con una nuova riga e un terminatore.
All'input, qualsiasi valore di byte al di sopra del separatore (confronto senza segno) viene trattato come filler e qualsiasi valore di byte al di sotto del separatore viene trattato come un marker di fine stringa. (Il controllo esplicito di un byte zero richiederebbe un extra test al,alrispetto alla ramificazione sui flag impostati dal loop interno).
Le regole consentono un separatore finale solo quando si tratta di una nuova riga finale. La mia implementazione aggiunge sempre il separatore. Per ottenere il salvataggio 1B in modalità a 32 bit, quella regola richiede separatore = 0xa ( '\n'ASCII LF = avanzamento riga), filler = 0xb ( '\v'ASCII VT = scheda verticale). Non è molto amico degli umani, ma soddisfa la lettera della legge. (È possibile eseguire
tr $'\v' xil dump esadecimale o l'output per verificare che funzioni o modificare la costante in modo che il separatore di output e il filler siano stampabili. Ho anche notato che le regole sembrano richiedere che possa accettare input con lo stesso riempimento / sep che utilizza per l'output , ma non vedo nulla da guadagnare infrangendo quella regola.).
Fonte NASM / YASM. Costruisci come codice a 32 o 64 bit, usando le %ifcose incluse con il programma di test o cambia semplicemente rcx in ecx.
input_separator equ 0xa ; `\n` in NASM syntax, but YASM doesn't do C-style escapes
output_filler equ '0' ; For strict rules-compliance, needs to be input_separator+1
output_separator equ output_filler-1 ; saves 1B in 32-bit vs. an arbitrary choice
;; Using output_filler+1 is also possible, but isn't compatible with using the same filler and separator for input and output.
global str_exp
str_exp: ; void str_exp(char *out /*rdi*/, const char *src /*rsi*/,
; unsigned base /*edx*/);
.new_row:
push 1
pop rcx ; ecx=1 = base**0
lodsb ; Skip the first char, since we multiply for the separator
.read_bar:
imul ecx, edx ; accumulate the exponential
lodsb
cmp al, input_separator
ja .read_bar ; anything > separator is treated as filler
; AL = separator or terminator
; flags = below (CF=1) or equal (ZF=1). Equal also implies CF=0, since x-x doesn't produce carry.
mov al, output_filler
rep stosb ; append ecx bytes of filler to the output string
%if output_separator == output_filler-1
dec eax ; saves 1B in the 32-bit version. Use dec even in 64-bit for easier testing
%else
mov al, output_separator
%endif
stosb ; append the delimiter
; CF is still set from the .read_bar loop, even if DEC clobbered the other flags
; JNC/JNB here is equivalent to JE on the original flags, because we can only be here if the char was below-or-equal the separator
jnc .new_row ; separator means more rows, else it's a terminator
; (f+s)+f+ full-match guarantees that the input doesn't end with separator + terminator
ret
La funzione segue l'ABI SystemV x86-64, con firma
void str_exp(char *out /*rdi*/, const char *src /*rsi*/, unsigned base /*edx*/);
Informa solo il chiamante della lunghezza della stringa di output lasciando in essa un puntatore uno-oltre-fine rdi, quindi è possibile considerare questo valore di ritorno in un non convenzione di chiamata standard.
xchg eax,ediCosterebbe 1 o 2 byte ( ) per restituire il puntatore finale in eax o rax. (Se si utilizza l'ABI x32, i puntatori sono garantiti solo a 32 bit, altrimenti dobbiamo usarli xchg rax,rdinel caso in cui il chiamante passi un puntatore a un buffer esterno ai 32 bit bassi.) Non ho incluso questo nella versione pubblicazione perché esistono soluzioni alternative che il chiamante può utilizzare senza ottenere il valore da rdi, quindi è possibile chiamarlo da C senza wrapper.
Non terminiamo nemmeno con null la stringa di output o altro, quindi è solo terminata da nuova riga. Ci vorrebbero 2 byte per risolvere questo: xchg eax,ecx / stosb (rcx è zero da rep stosb.)
I modi per scoprire la lunghezza della stringa di output sono:
- rdi punta a uno-oltre-la-fine della stringa al ritorno (quindi il chiamante può fare len = end-start)
- il chiamante può semplicemente sapere quante righe c'erano nell'input e contare le nuove righe
- il chiamante può utilizzare un buffer azzerato di grandi dimensioni e
strlen()successivamente.
Non sono belli o efficienti (tranne per l'utilizzo del valore di ritorno RDI da un chiamante asm), ma se lo desideri, non chiamare le funzioni asm golfate da C.: P
Limitazioni di dimensioni / intervallo
La dimensione massima della stringa di output è limitata solo dalle limitazioni dello spazio indirizzo della memoria virtuale. (Principalmente che l'attuale hardware x86-64 supporta solo 48 bit significativi negli indirizzi virtuali, divisi a metà perché firmano estensione anziché estensione zero. Vedi il diagramma nella risposta collegata .)
Ogni riga può avere solo un massimo di 2 ** 32 - 1 byte di riempimento, poiché accumulo l'esponenziale in un registro a 32 bit.
La funzione funziona correttamente per le basi da 0 a 2 ** 32 - 1. (La correzione per la base 0 è 0 ^ x = 0, ovvero solo righe vuote senza byte di riempimento. La correzione per la base 1 è 1 ^ x = 1, quindi sempre 1 riempimento per riga.)
È anche incredibilmente veloce su Intel IvyBridge e versioni successive, specialmente per le righe di grandi dimensioni che vengono scritte nella memoria allineata. rep stosbè un'implementazione ottimale di memset()grandi conteggi con puntatori allineati su CPU con la funzione ERMSB . es. 180 ** 4 è 0,97 GB e impiega 0,27 secondi sul mio i7-6700k Skylake (con ~ 256k errori di pagina morbida) per scrivere su / dev / null. (Su Linux il driver di dispositivo per / dev / null non copia i dati da nessuna parte, ma semplicemente ritorna. Quindi tutto il tempo è nei rep stosbe dei difetti della pagina morbida che si innesca quando si tocca la memoria per la prima volta. È purtroppo non usando hugepage trasparenti per l'array in BSS. Probabilmente una madvise()chiamata di sistema lo accelererebbe.)
Programma di test :
Costruisci un binario statico ed eseguilo come ./string-exponential $'#\n##\n###' $(seq 2)per la base 2. Per evitare di implementare un atoi, usa base = argc-2. (I limiti di lunghezza della riga di comando impediscono di testare basi ridicolmente grandi.)
Questo wrapper funziona per stringhe di output fino a 1 GB. (Fa solo una singola chiamata di sistema write () anche per stringhe gigantesche, ma Linux lo supporta anche per scrivere su pipe). Per contare i caratteri, esegui il pipe wc -co usa strace ./foo ... > /dev/nullper vedere l'arg nella call syscall.
Questo sfrutta il valore di ritorno RDI per calcolare la lunghezza della stringa come argomento per write().
;;; Test program that calls it
;;; Assembles correctly for either x86-64 or i386, using the following %if stuff.
;;; This block of macro-stuff also lets us build the function itself as 32 or 64-bit with no source changes.
%ifidn __OUTPUT_FORMAT__, elf64
%define CPUMODE 64
%define STACKWIDTH 8 ; push / pop 8 bytes
%define PTRWIDTH 8
%elifidn __OUTPUT_FORMAT__, elfx32
%define CPUMODE 64
%define STACKWIDTH 8 ; push / pop 8 bytes
%define PTRWIDTH 4
%else
%define CPUMODE 32
%define STACKWIDTH 4 ; push / pop 4 bytes
%define PTRWIDTH 4
%define rcx ecx ; Use the 32-bit names everywhere, even in addressing modes and push/pop, for 32-bit code
%define rsi esi
%define rdi edi
%define rsp esp
%endif
global _start
_start:
mov rsi, [rsp+PTRWIDTH + PTRWIDTH*1] ; rsi = argv[1]
mov edx, [rsp] ; base = argc
sub edx, 2 ; base = argc-2 (so it's possible to test base=0 and base=1, and so ./foo $'xxx\nxx\nx' $(seq 2) has the actual base in the arg to seq)
mov edi, outbuf ; output buffer. static data is in the low 2G of address space, so 32-bit mov is fine. This part isn't golfed, though
call str_exp ; str_exp(outbuf, argv[1], argc-2)
; leaves RDI pointing to one-past-the-end of the string
mov esi, outbuf
mov edx, edi
sub edx, esi ; length = end - start
%if CPUMODE == 64 ; use the x86-64 ABI
mov edi, 1 ; fd=1 (stdout)
mov eax, 1 ; SYS_write (Linux x86-64 ABI, from /usr/include/asm/unistd_64.h)
syscall ; write(1, outbuf, length);
xor edi,edi
mov eax,231 ; exit_group(0)
syscall
%else ; Use the i386 32-bit ABI (with legacy int 0x80 instead of sysenter for convenience)
mov ebx, 1
mov eax, 4 ; SYS_write (Linux i386 ABI, from /usr/include/asm/unistd_32.h)
mov ecx, esi ; outbuf
; 3rd arg goes in edx for both ABIs, conveniently enough
int 0x80 ; write(1, outbuf, length)
xor ebx,ebx
mov eax, 1
int 0x80 ; 32-bit ABI _exit(0)
%endif
section .bss
align 2*1024*1024 ; hugepage alignment (32-bit uses 4M hugepages, but whatever)
outbuf: resb 1024*1024*1024 * 1
; 2GB of code+data is the limit for the default 64-bit code model.
; But with -m32, a 2GB bss doesn't get mapped, so we segfault. 1GB is plenty anyway.
Questa è stata una sfida divertente che si è prestata molto bene ad asm, in particolare operazioni su stringhe x86 . Le regole sono ben progettate per evitare di dover gestire una nuova riga e quindi un terminatore alla fine della stringa di input.
Un esponenziale con moltiplicazioni ripetute è proprio come moltiplicarsi con addizioni ripetute, e ho comunque bisogno di fare un ciclo per contare i caratteri in ogni riga di input.
Ho preso in considerazione l'uso di un operando mulo imulinvece del più lungo imul r,r, ma il suo uso implicito di EAX sarebbe in conflitto con LODSB.
Ho anche provato SCASB invece di caricare e confrontare , ma avevo bisogno xchg esi,ediprima e dopo il ciclo interno, perché SCASB e STOSB usano entrambi EDI. (Quindi la versione a 64 bit deve usare l'ABI x32 per evitare di troncare i puntatori a 64 bit).
Evitare STOSB non è un'opzione; nient'altro è vicino come breve. E metà del vantaggio dell'utilizzo di SCASB è che AL = filler dopo aver lasciato il loop interno, quindi non abbiamo bisogno di alcuna configurazione per REP STOSB.
SCASB si confronta nell'altra direzione rispetto a quello che avevo fatto, quindi ho dovuto invertire i confronti.
Il mio miglior tentativo con xchg e scasb. Funziona, ma non è più corto. (Codice a 32 bit, usando il inc/ dectrucco per cambiare riempitore in separatore ).
; SCASB version, 24 bytes. Also experimenting with a different loop structure for the inner loop, but all these ideas are break-even at best
; Using separator = filler+1 instead of filler-1 was necessary to distinguish separator from terminator from just CF.
input_filler equ '.' ; bytes below this -> terminator. Bytes above this -> separator
output_filler equ input_filler ; implicit
output_separator equ input_filler+1 ; ('/') implicit
8048080: 89 d1 mov ecx,edx ; ecx=base**1
8048082: b0 2e mov al,0x2e ; input_filler= .
8048084: 87 fe xchg esi,edi
8048086: ae scas al,BYTE PTR es:[edi]
08048087 <str_exp.read_bar>:
8048087: ae scas al,BYTE PTR es:[edi]
8048088: 75 05 jne 804808f <str_exp.bar_end>
804808a: 0f af ca imul ecx,edx ; exit the loop before multiplying for non-filler
804808d: eb f8 jmp 8048087 <str_exp.read_bar> ; The other loop structure (ending with the conditional) would work with SCASB, too. Just showing this for variety.
0804808f <str_exp.bar_end>:
; flags = below if CF=1 (filler<separator), above if CF=0 (filler<terminator)
; (CF=0 is the AE condition, but we can't be here on equal)
; So CF is enough info to distinguish separator from terminator if we clobber ZF with INC
; AL = input_filler = output_filler
804808f: 87 fe xchg esi,edi
8048091: f3 aa rep stos BYTE PTR es:[edi],al
8048093: 40 inc eax ; output_separator
8048094: aa stos BYTE PTR es:[edi],al
8048095: 72 e9 jc 8048080 <str_exp> ; CF is still set from the inner loop
8048097: c3 ret
Per un input di ../.../., produce ..../......../../. Non mi preoccuperò di mostrare un hexdump della versione con separator = newline.
"" <> "#"~Table~#è di 3 byte più breve di"#"~StringRepeat~#, probabilmente anche ulteriormente giocabile.