codice macchina x86-64, 34 byte
Calling convention = x86-64 System V x32 ABI (registro args con puntatori a 32 bit in modalità lunga).
La firma della funzione è void stewie_x87_1reg(float *seq_buf, unsigned Nterms);
. La funzione riceve i valori seed x0 e x1 nei primi due elementi dell'array e estende la sequenza fino ad almeno N più elementi. Il buffer deve essere arrotondato a 2 + N-arrotondato per eccesso al multiplo successivo di 4. (ovvero 2 + ((N+3)&~3)
, o solo N + 5).
Richiedere buffer imbottiti è normale nell'assemblaggio per funzioni ad alte prestazioni o con vettorializzazione SIMD, e questo ciclo srotolato è simile, quindi non credo che stia piegando troppo le regole. Il chiamante può facilmente (e dovrebbe) ignorare tutti gli elementi di riempimento.
Passare x0 e x1 come una funzione arg non già nel buffer ci costerebbe solo 3 byte (per a movlps [rdi], xmm0
o movups [rdi], xmm0
), anche se questa sarebbe una convenzione di chiamata non standard poiché System V passa struct{ float x,y; };
in due registri XMM separati.
Questo viene objdump -drw -Mintel
generato con un po 'di formattazione per aggiungere commenti
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
Questa implementazione di riferimento C si compila (con gcc -Os
) in un codice un po 'simile. gcc sceglie la stessa strategia che ho fatto, di mantenere solo un valore precedente in 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++;
}
}
Ho sperimentato altri modi, tra cui una versione x87 a due registri con codice simile a:
; 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 faresti in questo modo se stavi andando per la velocità (e SSE non era disponibile)
Mettere i carichi dalla memoria all'interno del loop invece che una volta all'entrata potrebbe essere d'aiuto, dal momento che potremmo semplicemente salvare i risultati sub e div in ordine, ma ci vogliono ancora due istruzioni FLD per impostare lo stack all'entrata.
Ho anche provato a usare la matematica scalare SSE / AVX (a partire dai valori in xmm0 e xmm1), ma la dimensione dell'istruzione maggiore è killer. L'uso addps
(dato che è 1B più corto di addss
) aiuta un pochino. Ho usato i prefissi VEX di AVX per istruzioni non commutative, poiché VSUBSS è solo un byte più lungo di SUBPS (e della stessa lunghezza di 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
Testato con questo cablaggio di prova:
#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;
}
Compila con nasm -felfx32 -Worphan-labels -gdwarf2 golf-stewie-sequence.asm &&
gcc -mx32 -o stewie -Og -g golf-stewie-sequence.c golf-stewie-sequence.o
Esegui il primo test-case con ./stewie 8 1 3
Se non hai installato le librerie x32, usa nasm -felf64
e lascia gcc usando il valore predefinito -m64
. Ho usato malloc
invece float seqbuf[seqlen+8]
(sullo stack) per ottenere un indirizzo basso senza dover effettivamente costruire come x32.
Curiosità: YASM ha un bug: usa un rel32 jcc per il ramo del ciclo, quando la destinazione del ramo ha lo stesso indirizzo di un simbolo globale.
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
assembla per ... 11f: 0f 8f db ff ff ff jg 100 <stewie_x87_1reg>