Fonction de code machine x86 32 bits, 21 octets
Fonction de code machine x86-64, 22 octets
L'économie 1B en mode 32 bits nécessite l'utilisation de separator = filler-1, par exemple fill=0
et sep=/
. La version 22 octets peut utiliser un choix arbitraire de séparateur et de remplissage.
Il s'agit de la version à 21 octets, avec séparateur d'entrée = \n
(0xa), remplisseur de 0
sortie = , séparateur de sortie = /
= remplisseur-1. Ces constantes peuvent être facilement modifiées.
; 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 version 64 bits est plus longue de 1 octet, utilisant un DEC de 2 octets ou un mov al, output_separator
. En dehors de cela, le code machine est le même pour les deux versions, mais certains noms de registre changent (par exemple rcx
au lieu de ecx
dans pop
).
Exemple de résultat de l'exécution du programme de test (base 3):
$ ./string-exponential $'.\n..\n...\n....' $(seq 3);echo
000/000000000/000000000000000000000000000/000000000000000000000000000000000000000000000000000000000000000000000000000000000/
Algorithme :
Faites une boucle sur l'entrée, exp *= base
pour chaque caractère de remplissage. Sur les délimiteurs et l'octet zéro de fin, ajoutez des exp
octets de remplissage, puis un séparateur à la chaîne de sortie et réinitialisez-le exp=1
. Il est très pratique de garantir que l'entrée ne se termine pas à la fois par une nouvelle ligne et un terminateur.
En entrée, toute valeur d'octet au-dessus du séparateur (comparaison non signée) est traitée comme un remplissage et toute valeur d'octet au-dessous du séparateur est traitée comme un marqueur de fin de chaîne. (La vérification explicite d'un octet zéro prendrait un test al,al
branchement supplémentaire par rapport aux indicateurs définis par la boucle interne).
Les règles n'autorisent un séparateur de fin que lorsqu'il s'agit d'une nouvelle ligne de fin. Mon implémentation ajoute toujours le séparateur. Pour obtenir l'enregistrement 1B en mode 32 bits, cette règle nécessite un séparateur = 0xa ( '\n'
ASCII LF = saut de ligne), filler = 0xb ( '\v'
ASCII VT = tabulation verticale). Ce n'est pas très convivial pour l'homme, mais satisfait à la lettre de la loi. (Vous pouvez hexdump ou
tr $'\v' x
la sortie pour vérifier qu'elle fonctionne, ou changer la constante pour que le séparateur et le remplisseur de sortie soient imprimables. J'ai également remarqué que les règles semblent exiger qu'il puisse accepter l'entrée avec le même remplissage / sep qu'il utilise pour la sortie , mais je ne vois rien à gagner à enfreindre cette règle.).
Source NASM / YASM. Construisez en code 32 ou 64 bits, en utilisant les %if
éléments inclus avec le programme de test ou changez simplement rcx en 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 fonction suit l'ABI SystemV x86-64, avec signature.
void str_exp(char *out /*rdi*/, const char *src /*rsi*/, unsigned base /*edx*/);
Elle n'informe l'appelant que de la longueur de la chaîne de sortie en lui laissant un pointeur un-la-fin rdi
, vous pouvez donc considérer cela comme la valeur de retour dans un non - convention d'appel standard.
Cela coûterait 1 ou 2 octets ( xchg eax,edi
) pour retourner le pointeur de fin en eax ou rax. (Si vous utilisez l'ABI x32, les pointeurs sont garantis à seulement 32 bits, sinon nous devons utiliser xchg rax,rdi
au cas où l'appelant passe un pointeur vers un tampon en dehors des 32 bits bas.) Je n'ai pas inclus cela dans la version que je suis la publication car il existe des solutions de contournement que l'appelant peut utiliser sans obtenir la valeur rdi
, vous pouvez donc l'appeler à partir de C sans wrapper.
Nous ne terminons même pas null la chaîne de sortie ou quoi que ce soit, donc c'est seulement terminé par une nouvelle ligne. Il faudrait 2 octets pour corriger cela: xchg eax,ecx / stosb
(rcx est zéro à partir de rep stosb
.)
Les moyens de connaître la longueur de la chaîne de sortie sont les suivants:
- rdi pointe vers un bout de la fin de la chaîne au retour (pour que l'appelant puisse faire len = end-start)
- l'appelant peut simplement savoir combien de lignes étaient en entrée et compter les retours à la ligne
- l'appelant peut utiliser un grand tampon mis à zéro et
strlen()
ensuite.
Ils ne sont pas jolis ou efficaces (sauf pour utiliser la valeur de retour RDI d'un appelant asm), mais si vous le souhaitez, n'appelez pas les fonctions asm golfées de C.: P
Limites de taille / gamme
La taille maximale de la chaîne de sortie n'est limitée que par les limitations de l'espace d'adressage de la mémoire virtuelle. (Principalement, le matériel x86-64 actuel ne prend en charge que 48 bits significatifs dans les adresses virtuelles, divisés en deux car ils signent-étendent au lieu de zéro-étendent. Voir le diagramme dans la réponse liée .)
Chaque ligne ne peut avoir qu'un maximum de 2 ** 32 - 1 octets de remplissage, car j'accumule l'exponentielle dans un registre 32 bits.
La fonction fonctionne correctement pour les bases de 0 à 2 ** 32 - 1. (Correct pour la base 0 est 0 ^ x = 0, c'est-à-dire juste des lignes vides sans octets de remplissage. Correct pour la base 1 est 1 ^ x = 1, donc toujours 1 charge par ligne.)
Il est également incroyablement rapide sur Intel IvyBridge et versions ultérieures, en particulier pour les grandes lignes écrites dans la mémoire alignée. rep stosb
est une implémentation optimale de memset()
pour les grands nombres avec des pointeurs alignés sur les CPU avec la fonction ERMSB . Par exemple, 180 ** 4 fait 0,97 Go et prend 0,27 seconde sur mon i7-6700k Skylake (avec ~ 256 k de défauts de page) pour écrire dans / dev / null. (Sur Linux , le pilote de périphérique pour / dev / null ne copie pas les données partout, il retourne juste. Donc , tout le temps est en rep stosb
et les douces fautes de page qui se déclenche lorsque touchant la mémoire pour la première fois. Il est malheureusement, n'utilise pas d'énormes pages transparentes pour le tableau dans le BSS. Un madvise()
appel système pourrait probablement l' accélérer.)
Programme de test :
Construisez un binaire statique et exécutez comme ./string-exponential $'#\n##\n###' $(seq 2)
pour la base 2. Pour éviter d'implémenter un atoi
, il utilise base = argc-2
. (Les limites de longueur de ligne de commande empêchent de tester des bases ridiculement grandes.)
Ce wrapper fonctionne pour les chaînes de sortie jusqu'à 1 Go. (Il ne fait qu'un seul appel système write () même pour des chaînes gigantesques, mais Linux le prend en charge même pour l'écriture dans des tuyaux). Pour compter les caractères, canalisez wc -c
ou utilisez strace ./foo ... > /dev/null
pour voir l'argument de l'appel système d'écriture.
Cela tire parti de la valeur de retour RDI pour calculer la longueur de chaîne comme argument pour 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.
C'était un défi amusant qui se prêtait très bien à asm, en particulier aux opérations de chaîne x86 . Les règles sont bien conçues pour éviter d'avoir à gérer une nouvelle ligne puis un terminateur à la fin de la chaîne d'entrée.
Une exponentielle avec multiplication répétée est tout comme la multiplication avec addition répétée, et j'avais besoin de boucler pour compter les caractères dans chaque ligne d'entrée de toute façon.
J'ai envisagé d'utiliser un opérande mul
ou imul
au lieu du plus long imul r,r
, mais son utilisation implicite d'EAX entrerait en conflit avec LODSB.
J'ai également essayé SCASB au lieu de charger et de comparer , mais j'avais besoin xchg esi,edi
avant et après la boucle interne, car SCASB et STOSB utilisent tous deux EDI. (La version 64 bits doit donc utiliser l'ABI x32 pour éviter de tronquer les pointeurs 64 bits).
Éviter STOSB n'est pas une option; rien d'autre n'est aussi court. Et la moitié des avantages de l'utilisation de SCASB est que AL = filler après avoir quitté la boucle intérieure, nous n'avons donc pas besoin de configuration pour REP STOSB.
SCASB compare dans l'autre sens par rapport à ce que j'avais fait, j'ai donc dû inverser les comparaisons.
Ma meilleure tentative avec xchg et scasb. Fonctionne, mais n'est pas plus court. ( Code 32 bits, en utilisant la astuce inc
/ dec
pour changer le remplissage en séparateur ).
; 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
Pour une entrée de ../.../.
, produit ..../......../../
. Je ne vais pas prendre la peine de montrer un vidage hexadécimal de la version avec separator = newline.
"" <> "#"~Table~#
est 3 octets plus court que"#"~StringRepeat~#
, probablement jouable au golf également.