Función de código de máquina x86 de 32 bits, 21 bytes
Función de código de máquina x86-64, 22 bytes
El ahorro de 1B en modo de 32 bits requiere el uso de separator = filler-1, por ejemplo, fill=0
y sep=/
. La versión de 22 bytes puede usar una elección arbitraria de separador y relleno.
Esta es la versión de 21 bytes, con input-separator = \n
(0xa), output-filler = 0
, output-separator = /
= filler-1. Estas constantes se pueden cambiar fácilmente.
; 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 versión de 64 bits es 1 byte más larga, utilizando un DEC de 2 bytes o un mov al, output_separator
. Aparte de eso, el código de máquina es el mismo para ambas versiones, pero algunos nombres de registro cambian (por ejemplo, en rcx
lugar de ecx
en el pop
).
Ejemplo de salida al ejecutar el programa de prueba (base 3):
$ ./string-exponential $'.\n..\n...\n....' $(seq 3);echo
000/000000000/000000000000000000000000000/000000000000000000000000000000000000000000000000000000000000000000000000000000000/
algoritmo :
Pase sobre la entrada, haciendo exp *= base
para cada char de relleno. En los delimitadores y el byte cero de terminación, agregue exp
bytes de relleno y luego un separador a la cadena de salida y restablezca a exp=1
. Es muy conveniente garantizar que la entrada no termine con una nueva línea y un terminador.
En la entrada, cualquier valor de byte por encima del separador (comparación sin signo) se trata como relleno, y cualquier valor de byte por debajo del separador se trata como un marcador de fin de cadena. (La comprobación explícita de un byte cero requeriría una test al,al
ramificación adicional frente a la ramificación en las marcas establecidas por el bucle interno).
Las reglas solo permiten un separador final cuando se trata de una nueva línea final. Mi implementación siempre agrega el separador. Para obtener el ahorro de 1B en modo de 32 bits, esa regla requiere separador = 0xa ( '\n'
ASCII LF = salto de línea), relleno = 0xb ( '\v'
ASCII VT = pestaña vertical). Eso no es muy amigable para los humanos, pero satisface la letra de la ley. (Puede realizar un hexdump o
tr $'\v' x
la salida para verificar que funciona, o cambiar la constante para que el separador de salida y el relleno sean imprimibles. También noté que las reglas parecen requerir que pueda aceptar entradas con el mismo relleno / sep que usa para la salida , pero no veo nada que ganar al romper esa regla).
Fuente NASM / YASM. Compile como código de 32 o 64 bits, usando las %if
cosas incluidas con el programa de prueba o simplemente cambie rcx a 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 función sigue el x86-64 SystemV ABI, con firma
void str_exp(char *out /*rdi*/, const char *src /*rsi*/, unsigned base /*edx*/);
Solo informa al llamante de la longitud de la cadena de salida al dejarle un puntero de un paso al finalrdi
, por lo que podría considerar este el valor de retorno en un no -convención de llamadas estándar.
xchg eax,edi
Costaría 1 o 2 bytes ( ) devolver el puntero final en eax o rax. (Si usa el x32 ABI, se garantiza que los punteros sean de solo 32 bits; de lo contrario, tenemos que usarlos xchg rax,rdi
en caso de que la persona que llama pase un puntero a un búfer fuera de los 32 bits bajos). No incluí esto en la versión que estoy publicar porque hay soluciones que la persona que llama puede usar sin obtener el valor derdi
, por lo que podría llamarlo desde C sin envoltorio.
Ni siquiera terminamos en nulo la cadena de salida ni nada, por lo que solo termina en nueva línea. Se necesitarían 2 bytes para arreglar eso: xchg eax,ecx / stosb
(rcx es cero desde rep stosb
).
Las formas de averiguar la longitud de la cadena de salida son:
- rdi apunta al final de la cadena al regresar (para que la persona que llama pueda hacer len = end-start)
- la persona que llama puede saber cuántas filas había en la entrada y contar nuevas líneas
- la persona que llama puede usar un gran búfer a cero y
strlen()
luego.
No son bonitas ni eficientes (excepto por usar el valor de retorno RDI de una persona que llama), pero si lo desea, no llame a las funciones de golf asm desde C.: P
Limitaciones de tamaño / rango
El tamaño máximo de la cadena de salida solo está limitado por las limitaciones de espacio de direcciones de memoria virtual. (Principalmente, ese hardware x86-64 actual solo admite 48 bits significativos en direcciones virtuales, divididas por la mitad porque firman-extienden en lugar de cero-extienden. Vea el diagrama en la respuesta vinculada ).
Cada fila solo puede tener un máximo de 2 ** 32-1 bytes de relleno, ya que acumulo el exponencial en un registro de 32 bits.
La función funciona correctamente para bases de 0 a 2 ** 32 - 1. (Correcto para la base 0 es 0 ^ x = 0, es decir, solo líneas en blanco sin bytes de relleno. Correcto para la base 1 es 1 ^ x = 1, así que siempre 1 relleno por línea.)
También es increíblemente rápido en Intel IvyBridge y posterior, especialmente para grandes filas que se escriben en la memoria alineada. rep stosb
es una implementación óptima memset()
para grandes recuentos con punteros alineados en CPU con la función ERMSB . por ejemplo, 180 ** 4 es 0.97GB, y toma 0.27 segundos en mi i7-6700k Skylake (con ~ 256k fallas de página suaves) para escribir en / dev / null. (En Linux, el controlador de dispositivo para / dev / null no copia los datos en ninguna parte, simplemente regresa. Por lo tanto, todo el tiempo está en las rep stosb
fallas de página suaves que se activan al tocar la memoria por primera vez. Es desafortunadamente no se usan páginas enormes transparentes para la matriz en el BSS. Probablemente una madvise()
llamada al sistema lo aceleraría).
Programa de prueba :
Cree un binario estático y ejecútelo como ./string-exponential $'#\n##\n###' $(seq 2)
para la base 2. Para evitar implementar un atoi
, lo utiliza base = argc-2
. (Los límites de longitud de la línea de comando evitan probar bases ridículamente grandes).
Este contenedor funciona para cadenas de salida de hasta 1 GB. (Solo hace una sola llamada al sistema write () incluso para cadenas gigantescas, pero Linux lo admite incluso para escribir en tuberías). Para contar caracteres, canalice wc -c
o use strace ./foo ... > /dev/null
para ver el argumento de la llamada al sistema de escritura.
Esto aprovecha el valor de retorno RDI para calcular la longitud de la cadena como un argumento para 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.
Este fue un desafío divertido que se prestó muy bien a asm, especialmente a las operaciones de cadena x86 . Las reglas están bien diseñadas para evitar tener que manejar una nueva línea y luego un terminador al final de la cadena de entrada.
Un exponencial con multiplicación repetida es como multiplicar con suma repetida, y de todos modos necesitaba hacer un bucle para contar los caracteres en cada fila de entrada.
Pensé en usar un operando mul
o en imul
lugar del más largo imul r,r
, pero su uso implícito de EAX entraría en conflicto con LODSB.
También probé SCASB en lugar de cargar y comparar , pero necesitaba xchg esi,edi
antes y después del ciclo interno, porque SCASB y STOSB usan EDI. (Por lo tanto, la versión de 64 bits tiene que usar el x32 ABI para evitar truncar los punteros de 64 bits).
Evitar STOSB no es una opción; nada más es tan corto. Y la mitad del beneficio de usar SCASB es que AL = relleno después de abandonar el bucle interno, por lo que no necesitamos ninguna configuración para REP STOSB.
SCASB se compara en la otra dirección de lo que había estado haciendo, por lo que necesitaba revertir las comparaciones.
Mi mejor intento con xchg y scasb. Funciona, pero no es más corto. ( Código de 32 bits, usando el truco inc
/ dec
para cambiar el relleno al separador ).
; 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
Para una entrada de ../.../.
, produce ..../......../../
. No me voy a molestar en mostrar un hexdump de la versión con separator = newline.
"" <> "#"~Table~#
es 3 bytes más corto que"#"~StringRepeat~#
, probablemente golfable aún más.