Código de máquina x86-64, 34 bytes
Convención de llamada = x86-64 Sistema V x32 ABI (registro de argumentos con punteros de 32 bits en modo largo).
La firma de la función es void stewie_x87_1reg(float *seq_buf, unsigned Nterms);
. La función recibe los valores de inicialización x0 y x1 en los dos primeros elementos de la matriz, y extiende la secuencia a al menos N elementos más. El búfer debe rellenarse a 2 + N-redondeado-al-siguiente-múltiplo de 4. (es decir 2 + ((N+3)&~3)
, o solo N + 5).
Requerir buffers acolchados es normal en el ensamblaje para funciones de alto rendimiento o vectorizadas SIMD, y este bucle desenrollado es similar, por lo que no creo que esté doblando las reglas demasiado. La persona que llama puede ignorar fácilmente (y debería) ignorar todos los elementos de relleno.
Pasar x0 y x1 como una función arg que aún no está en el búfer nos costaría solo 3 bytes (para movlps [rdi], xmm0
ao movups [rdi], xmm0
), aunque esto sería una convención de llamada no estándar ya que el Sistema V pasastruct{ float x,y; };
en dos registros XMM separados.
Esto se objdump -drw -Mintel
genera con un poco de formato para agregar comentarios
0000000000000100 <stewie_x87_1reg>:
;; load inside the loop to match FSTP at the end of every iteration
;; x[i-1] is always in ST0
;; x[i-2] is re-loaded from memory
100: d9 47 04 fld DWORD PTR [rdi+0x4]
103: d8 07 fadd DWORD PTR [rdi]
105: d9 57 08 fst DWORD PTR [rdi+0x8]
108: 83 c7 10 add edi,0x10 ; 32-bit pointers save a REX prefix here
10b: d8 4f f4 fmul DWORD PTR [rdi-0xc]
10e: d9 57 fc fst DWORD PTR [rdi-0x4]
111: d8 6f f8 fsubr DWORD PTR [rdi-0x8]
114: d9 17 fst DWORD PTR [rdi]
116: d8 7f fc fdivr DWORD PTR [rdi-0x4]
119: d9 5f 04 fstp DWORD PTR [rdi+0x4]
11c: 83 ee 04 sub esi,0x4
11f: 7f df jg 100 <stewie_x87_1reg>
121: c3 ret
0000000000000122 <stewie_x87_1reg.end>:
## 0x22 = 34 bytes
Esta implementación de referencia C compila (con gcc -Os
) un código algo similar. gcc elige la misma estrategia que yo, de mantener solo un valor anterior en un registro.
void stewie_ref(float *seq, unsigned Nterms)
{
for(unsigned i = 2 ; i<Nterms ; ) {
seq[i] = seq[i-2] + seq[i-1]; i++;
seq[i] = seq[i-2] * seq[i-1]; i++;
seq[i] = seq[i-2] - seq[i-1]; i++;
seq[i] = seq[i-2] / seq[i-1]; i++;
}
}
Experimenté con otras formas, incluida una versión x87 de dos registros que tiene un código como:
; part of loop body from untested 2-register version. faster but slightly larger :/
; x87 FPU register stack ; x1, x2 (1-based notation)
fadd st0, st1 ; x87 = x3, x2
fst dword [rdi+8 - 16] ; x87 = x3, x2
fmul st1, st0 ; x87 = x3, x4
fld st1 ; x87 = x4, x3, x4
fstp dword [rdi+12 - 16] ; x87 = x3, x4
; and similar for the fsubr and fdivr, needing one fld st1
Lo haría de esta manera si fuera por la velocidad (y SSE no estuviera disponible)
Poner las cargas de la memoria dentro del bucle en lugar de una vez en la entrada podría haber ayudado, ya que podríamos almacenar los resultados sub y div fuera de orden, pero aún necesita dos instrucciones FLD para configurar la pila en la entrada.
También intenté usar las matemáticas escalares SSE / AVX (comenzando con valores en xmm0 y xmm1), pero el tamaño de instrucción más grande es asesino. Usar addps
(ya que es 1B más corto que addss
) ayuda un poco. Utilicé los prefijos AVX VEX para instrucciones no conmutativas, ya que VSUBSS es solo un byte más largo que SUBPS (y la misma longitud que SUBSS).
; untested. Bigger than x87 version, and can spuriously raise FP exceptions from garbage in high elements
addps xmm0, xmm1 ; x3
movups [rdi+8 - 16], xmm0
mulps xmm1, xmm0 ; xmm1 = x4, xmm0 = x3
movups [rdi+12 - 16], xmm1
vsubss xmm0, xmm1, xmm0 ; not commutative. Could use a value from memory
movups [rdi+16 - 16], xmm0
vdivss xmm1, xmm0, xmm1 ; not commutative
movups [rdi+20 - 16], xmm1
Probado con este arnés de prueba:
#include <stdlib.h>
#include <stdio.h>
#include <math.h>
int main(int argc, char**argv)
{
unsigned seqlen = 100;
if (argc>1)
seqlen = atoi(argv[1]);
float first = 1.0f, second = 2.1f;
if (argc>2)
first = atof(argv[2]);
if (argc>3)
second = atof(argv[3]);
float *seqbuf = malloc(seqlen+8); // not on the stack, needs to be in the low32
seqbuf[0] = first;
seqbuf[1] = second;
for(unsigned i=seqlen ; i<seqlen+8; ++i)
seqbuf[i] = NAN;
stewie_x87_1reg(seqbuf, seqlen);
// stewie_ref(seqbuf, seqlen);
for (unsigned i=0 ; i< (2 + ((seqlen+3)&~3) + 4) ; i++) {
printf("%4d: %g\n", i, seqbuf[i]);
}
return 0;
}
Compilar con nasm -felfx32 -Worphan-labels -gdwarf2 golf-stewie-sequence.asm &&
gcc -mx32 -o stewie -Og -g golf-stewie-sequence.c golf-stewie-sequence.o
Ejecute el primer caso de prueba con ./stewie 8 1 3
Si no tiene instaladas bibliotecas x32, use nasm -felf64
y deje gcc usando el predeterminado -m64
. Usé en malloc
lugar de float seqbuf[seqlen+8]
(en la pila) para obtener una dirección baja sin tener que construir realmente como x32.
Dato curioso: YASM tiene un error: utiliza un rel32 jcc para la rama de bucle, cuando el objetivo de la rama tiene la misma dirección que un símbolo global.
global stewie_x87_1reg
stewie_x87_1reg:
;; ended up moving all prologue code into the loop, so there's nothing here
.loop:
...
sub esi, 4
jg .loop
se ensambla para ... 11f: 0f 8f db ff ff ff jg 100 <stewie_x87_1reg>