código de máquina x86-64, 34 bytes
Convenção de chamada = x86-64 System V x32 ABI (registre argumentos com ponteiros de 32 bits no modo longo).
A assinatura da função é void stewie_x87_1reg(float *seq_buf, unsigned Nterms);
. A função recebe os valores iniciais x0 e x1 nos dois primeiros elementos da matriz e estende a sequência para pelo menos mais N elementos. O buffer deve ser preenchido com 2 + N-arredondado-para-o-próximo-múltiplo-de-4. (ou seja 2 + ((N+3)&~3)
, apenas N + 5).
Exigir buffers acolchoados é normal na montagem para funções de alto desempenho ou vetorizadas por SIMD, e esse loop desenrolado é semelhante, portanto, não acho que isso esteja distorcendo as regras demais. O chamador pode facilmente (e deve) ignorar todos os elementos de preenchimento.
Passar x0 e x1 como uma função arg ainda não no buffer nos custaria apenas 3 bytes (para a movlps [rdi], xmm0
ou movups [rdi], xmm0
), embora isso seja uma convenção de chamada não padrão, pois o System V passastruct{ float x,y; };
em dois registros XMM separados.
Isso é objdump -drw -Mintel
produzido com um pouco de formatação para adicionar comentários
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 implementação de referência C compila (com gcc -Os
) um código semelhante. O gcc escolhe a mesma estratégia que eu, de manter apenas um valor anterior em um 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++;
}
}
Eu experimentei de outras maneiras, incluindo uma versão x87 de dois registros que possui 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
Você faria dessa maneira se estivesse buscando velocidade (e o SSE não estava disponível)
Colocar as cargas da memória dentro do loop em vez de uma vez na entrada pode ter ajudado, já que podemos armazenar os resultados sub e div fora de ordem, mas ainda assim precisamos de duas instruções FLD para configurar a pilha na entrada.
Também tentei usar a matemática escalar SSE / AVX (começando com valores em xmm0 e xmm1), mas o tamanho maior da instrução é matador. Usar addps
(já que é 1B menor que addss
) ajuda um pouquinho. Usei prefixos AVX VEX para instruções não comutativas, pois o VSUBSS tem apenas um byte a mais que SUBPS (e o mesmo comprimento 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
Testado com este equipamento de teste:
#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;
}
Ajuntar com nasm -felfx32 -Worphan-labels -gdwarf2 golf-stewie-sequence.asm &&
gcc -mx32 -o stewie -Og -g golf-stewie-sequence.c golf-stewie-sequence.o
Execute o primeiro caso de teste com ./stewie 8 1 3
Se você não possui bibliotecas x32 instaladas, use nasm -felf64
e deixe o gcc usando o padrão -m64
. Eu usei em malloc
vez de float seqbuf[seqlen+8]
(na pilha) para obter um endereço baixo sem ter que realmente construir como x32.
Curiosidade: O YASM possui um bug: ele usa um rel32 jcc para a ramificação do loop, quando o destino da ramificação tem o mesmo endereço que um 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
monta para ... 11f: 0f 8f db ff ff ff jg 100 <stewie_x87_1reg>