code machine x86-64, 34 octets
Convention d'appel = x86-64 System V x32 ABI (registre args avec pointeurs 32 bits en mode long).
La signature de la fonction est void stewie_x87_1reg(float *seq_buf, unsigned Nterms);
. La fonction reçoit les valeurs de départ x0 et x1 dans les deux premiers éléments du tableau et étend la séquence à au moins N éléments supplémentaires. Le tampon doit être complété à 2 + N-arrondi au multiple de 4 suivant. (c'est à dire2 + ((N+3)&~3)
- , ou juste N + 5).
L'assemblage de tampons rembourrés est normal en assemblage pour les fonctions haute performance ou vectorisées SIMD, et cette boucle déroulée est similaire, donc je ne pense pas que cela déforme trop les règles. L'appelant peut facilement (et devrait) ignorer tous les éléments de remplissage.
Passer x0 et x1 en tant que fonction arg ne se trouvant pas déjà dans le tampon ne nous coûterait que 3 octets (pour a movlps [rdi], xmm0
ou movups [rdi], xmm0
), bien que ce soit une convention d'appel non standard puisque System V passe struct{ float x,y; };
dans deux registres XMM distincts.
Ceci est objdump -drw -Mintel
produit avec un peu de mise en forme pour ajouter des commentaires
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
Cette implémentation de référence C compile (avec gcc -Os
) un code quelque peu similaire. gcc choisit la même stratégie que moi, de garder une seule valeur précédente dans un registre.
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++;
}
}
J'ai expérimenté d'autres façons, y compris une version x87 à deux registres qui a du code comme:
; 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
Vous le feriez de cette façon si vous optez pour la vitesse (et SSE n'était pas disponible)
Mettre les charges de la mémoire à l'intérieur de la boucle au lieu d'une fois à l'entrée aurait pu aider, car nous pouvions simplement stocker les résultats sub et div dans le désordre, mais il a toujours besoin de deux instructions FLD pour configurer la pile à l'entrée.
J'ai également essayé d'utiliser les mathématiques scalaires SSE / AVX (en commençant par les valeurs de xmm0 et xmm1), mais la plus grande taille d'instruction est mortelle. L'utilisation addps
(puisque c'est 1B plus court que addss
) aide un tout petit peu. J'ai utilisé les préfixes AVX VEX pour les instructions non commutatives, car VSUBSS n'est qu'un octet de plus que SUBPS (et la même longueur 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
Testé avec ce harnais de test:
#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;
}
Compiler avec nasm -felfx32 -Worphan-labels -gdwarf2 golf-stewie-sequence.asm &&
gcc -mx32 -o stewie -Og -g golf-stewie-sequence.c golf-stewie-sequence.o
Exécutez le premier cas de test avec ./stewie 8 1 3
Si vous n'avez pas de bibliothèques x32 installées, utilisez nasm -felf64
et laissez gcc en utilisant la valeur par défaut -m64
. J'ai utilisé à la malloc
place de float seqbuf[seqlen+8]
(sur la pile) pour obtenir une adresse basse sans avoir à construire en tant que x32.
Fait amusant: YASM a un bug: il utilise un jcc rel32 pour la branche de boucle, lorsque la cible de branche a la même adresse qu'un symbole 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
s'assemble pour ... 11f: 0f 8f db ff ff ff jg 100 <stewie_x87_1reg>