kod maszynowy x86-64, 34 bajty
Konwencja wywoływania = x86-64 System V x32 ABI (rejestruje argumenty za pomocą 32-bitowych wskaźników w trybie długim).
Sygnatura funkcji to void stewie_x87_1reg(float *seq_buf, unsigned Nterms);
. Funkcja odbiera wartości początkowe x0 i x1 w pierwszych dwóch elementach tablicy i rozszerza sekwencję na co najmniej N więcej elementów. Bufor musi zostać uzupełniony do 2 + N zaokrąglone w górę do następnej wielokrotności 4. (tj. 2 + ((N+3)&~3)
lub tylko N + 5).
Wymaganie buforowanych buforów jest normalne przy składaniu w przypadku funkcji o wysokiej wydajności lub wektoryzacji SIMD, a ta rozwijana pętla jest podobna, więc nie sądzę, że zbyt mocno wygina reguły. Dzwoniący może łatwo (i powinien) zignorować wszystkie elementy dopełniające.
Przekazanie x0 i x1 jako funkcji arg jeszcze nie znajdującej się w buforze kosztowałoby nas tylko 3 bajty (dla a movlps [rdi], xmm0
lub movups [rdi], xmm0
), chociaż byłaby to niestandardowa konwencja wywoływania, ponieważ System V przechodzi struct{ float x,y; };
w dwóch oddzielnych rejestrach XMM.
To jest objdump -drw -Mintel
wyjście z odrobiną formatowania, aby dodać komentarze
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
Ta implementacja odniesienia C kompiluje (z gcc -Os
) do nieco podobnego kodu. gcc wybiera tę samą strategię, co ja, polegającą na zachowaniu tylko jednej poprzedniej wartości w rejestrze.
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++;
}
}
Eksperymentowałem z innymi sposobami, w tym wersją x87 z dwoma rejestrami i kodem:
; 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
Zrobiłbyś to w ten sposób, gdybyś dążył do prędkości (a SSE nie było dostępne)
Umieszczenie ładunków z pamięci w pętli zamiast raz na wejściu mogło pomóc, ponieważ mogliśmy po prostu przechowywać wyniki sub i div poza kolejnością, ale nadal potrzebują dwóch instrukcji FLD, aby ustawić stos przy wejściu.
Próbowałem także użyć matematyki skalarnej SSE / AVX (zaczynając od wartości w xmm0 i xmm1), ale większy rozmiar instrukcji jest zabójczy. Używanie addps
(ponieważ jest to o 1B mniej niż addss
) pomaga trochę. Użyłem przedrostków AVX VEX dla instrukcji nieprzemiennych, ponieważ VSUBSS jest tylko o jeden bajt dłuższy niż SUBPS (i ma taką samą długość jak 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
Testowane za pomocą tej uprzęży testowej:
#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;
}
Połącz z nasm -felfx32 -Worphan-labels -gdwarf2 golf-stewie-sequence.asm &&
gcc -mx32 -o stewie -Og -g golf-stewie-sequence.c golf-stewie-sequence.o
Uruchom pierwszy przypadek testowy za pomocą ./stewie 8 1 3
Jeśli nie masz zainstalowanych bibliotek x32, użyj nasm -felf64
i pozostaw gcc, używając domyślnej -m64
. Użyłem malloc
zamiast float seqbuf[seqlen+8]
(na stosie), aby uzyskać niski adres bez konieczności budowania jako x32.
Ciekawostka: YASM ma błąd: używa rel32 jcc dla gałęzi pętli, gdy cel gałęzi ma ten sam adres co symbol globalny.
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
montuje do ... 11f: 0f 8f db ff ff ff jg 100 <stewie_x87_1reg>