машинный код x86-64, 34 байта
Соглашение о вызове = x86-64 System V x32 ABI (зарегистрируйте аргументы с 32-разрядными указателями в длинном режиме).
Подпись функции есть void stewie_x87_1reg(float *seq_buf, unsigned Nterms);
. Функция получает начальные значения x0 и x1 в первых двух элементах массива и расширяет последовательность, по крайней мере, еще на N элементов. Буфер должен быть дополнен до 2 + N-округлено до следующего-кратного-4. (т.е. 2 + ((N+3)&~3)
или просто N + 5).
Требование заполненных буферов является нормальным в сборке для высокопроизводительных или SIMD-векторизованных функций, и этот развернутый цикл похож, поэтому я не думаю, что он слишком сильно нарушает правила. Вызывающий может легко (и должен) игнорировать все элементы заполнения.
Передача x0 и x1 как функции arg, которой еще нет в буфере, обойдется нам всего в 3 байта (для a movlps [rdi], xmm0
или movups [rdi], xmm0
), хотя это будет нестандартное соглашение о вызовах, поскольку System V передает struct{ float x,y; };
в два отдельных регистра XMM.
Это objdump -drw -Mintel
вывод с небольшим форматированием для добавления комментариев
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
Эта реализация ссылки C компилирует (с gcc -Os
) в несколько похожий код. gcc выбирает ту же стратегию, что и я: хранить только одно предыдущее значение в регистре.
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++;
}
}
Я экспериментировал с другими способами, включая версию с двумя регистрами x87, которая имеет такой код:
; 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
Вы бы сделали это таким образом, если бы вы шли на скорости (а SSE не было доступно)
Помогло бы размещение нагрузок из памяти внутри цикла вместо однократного ввода, поскольку мы могли бы просто хранить результаты sub и div не по порядку, но все же для настройки стека при входе необходимы две инструкции FLD.
Я также попытался использовать скалярную математику SSE / AVX (начиная со значений в xmm0 и xmm1), но больший размер инструкции является убийственным. Использование addps
(так как это на 1B короче addss
) помогает немного. Я использовал AVX VEX-префиксы для некоммутативных инструкций, поскольку VSUBSS всего на один байт длиннее SUBPS (и такой же длины, что и 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
Протестировано с помощью этого теста:
#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;
}
Компилировать с nasm -felfx32 -Worphan-labels -gdwarf2 golf-stewie-sequence.asm &&
gcc -mx32 -o stewie -Og -g golf-stewie-sequence.c golf-stewie-sequence.o
Запустите первый тест-кейс с ./stewie 8 1 3
Если у вас не установлены библиотеки x32, используйте nasm -felf64
и оставьте gcc по умолчанию -m64
. Я использовал malloc
вместо float seqbuf[seqlen+8]
(в стеке) получение низкого адреса без фактической сборки как x32.
Интересный факт: в YASM есть ошибка: он использует rel32 jcc для ветвления цикла, когда цель ветвления имеет тот же адрес, что и глобальный символ.
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
собирает в ... 11f: 0f 8f db ff ff ff jg 100 <stewie_x87_1reg>