x86-64-Maschinencode, 34 Byte
Aufrufkonvention = x86-64 System V x32 ABI (Registerargumente mit 32-Bit-Zeigern im ).
Die Funktionssignatur ist void stewie_x87_1reg(float *seq_buf, unsigned Nterms);
. Die Funktion empfängt die Startwerte x0 und x1 in den ersten beiden Elementen des Arrays und erweitert die Sequenz auf mindestens N weitere Elemente. Der Puffer muss auf 2 + N aufgerundet auf das nächste Vielfache von 4 aufgefüllt werden. (dh 2 + ((N+3)&~3)
oder nur N + 5).
Das Erfordernis von gepolsterten Puffern ist beim Zusammenbau von Hochleistungs- oder SIMD-vektorisierten Funktionen normal, und diese entrollte Schleife ist ähnlich, sodass ich nicht denke, dass sie die Regeln zu sehr verbiegt. Der Aufrufer kann (und sollte) problemlos alle Auffüllelemente ignorieren.
Das Übergeben von x0 und x1 als eine Funktion arg, die nicht bereits im Puffer enthalten ist, würde uns nur 3 Byte kosten (für ein movlps [rdi], xmm0
oder movups [rdi], xmm0
), obwohl dies eine nicht standardmäßige Aufrufkonvention wäre, da System V übergibtstruct{ float x,y; };
zwei separate XMM-Register .
Dies wird objdump -drw -Mintel
mit ein wenig Formatierung ausgegeben, um Kommentare hinzuzufügen
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
Diese C-Referenzimplementierung kompiliert (mit gcc -Os
) zu etwas ähnlichem Code. gcc wählt die gleiche Strategie wie ich, nur einen vorherigen Wert in einem Register zu behalten.
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++;
}
}
Ich habe mit anderen Methoden experimentiert, einschließlich einer x87-Version mit zwei Registern und folgendem Code:
; 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
Sie würden es auf diese Weise tun, wenn Sie auf Geschwindigkeit aus sind (und SSE nicht verfügbar war)
Das Einfügen der Ladevorgänge aus dem Speicher in die Schleife anstelle eines einmaligen Eintrags hätte möglicherweise geholfen, da wir die Sub- und Div-Ergebnisse nur in einer nicht ordnungsgemäßen Reihenfolge speichern konnten, aber immer noch zwei FLD-Anweisungen zum Einrichten des Stacks beim Eintrag erforderlich sind.
Ich habe auch versucht, SSE / AVX-Skalarmathematik zu verwenden (beginnend mit Werten in xmm0 und xmm1), aber die größere Anweisungsgröße ist umwerfend. Verwenden von addps
(da das 1B kürzer ist als addss
) hilft ein kleines bisschen. Ich habe AVX VEX-Präfixe für nicht kommutative Anweisungen verwendet, da VSUBSS nur ein Byte länger als SUBPS ist (und dieselbe Länge wie SUBSS hat).
; 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
Getestet mit diesem Testgeschirr:
#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;
}
Kompilieren mit nasm -felfx32 -Worphan-labels -gdwarf2 golf-stewie-sequence.asm &&
gcc -mx32 -o stewie -Og -g golf-stewie-sequence.c golf-stewie-sequence.o
Führen Sie den ersten Testfall mit aus ./stewie 8 1 3
Wenn Sie keine x32-Bibliotheken installiert haben, verwenden Sie nasm -felf64
und lassen Sie gcc mit der Standardeinstellung -m64
. Ich habe malloc
anstelle von float seqbuf[seqlen+8]
(auf dem Stapel) verwendet, um eine niedrige Adresse zu erhalten, ohne sie tatsächlich als x32 erstellen zu müssen.
Unterhaltsame Tatsache: YASM hat einen Fehler: Es verwendet einen rel32-JCC für den Loop-Zweig, wenn das Zweigziel dieselbe Adresse wie ein globales Symbol hat.
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
baut auf ... 11f: 0f 8f db ff ff ff jg 100 <stewie_x87_1reg>