Один из способов реализации FMA в программном обеспечении - это разделение значительных на высокие и бит.Я использую Dekker's algorithm
typedef struct { float hi; float lo; } doublefloat;
doublefloat split(float a) {
float t = ((1<<12)+1)*a;
float hi = t - (t - a);
float lo = a - hi;
return (doublefloat){hi, lo};
}
После того, как вы разделяете поплавок можно вычислить a*b-c
с одним округлением, как этот
float fmsub(float a, float b, float c) {
doublefloat as = split(a), bs = split(b);
return ((as.hi*bs.hi - c) + as.hi*bs.lo + as.lo*bs.hi) + as.lo*bs.lo;
}
В основном это вычитает c
от (ahi,alo)*(bhi,blo) = (ahi*bhi + ahi*blo + alo*bhi + alo*blo)
.
Я получил эту идею от функции twoProd
в статье Extended-Precision Floating-Point Numbers for GPU Computation и от функции mul_sub_x
в Agner Fog's vector class library. Он использует другую функцию для расщепления векторов поплавков, которые расщепляются по-разному. Я попытался воспроизвести скалярную версию здесь
typedef union {float f; int i;} u;
doublefloat split2(float a) {
u lo, hi = {a};
hi.i &= -(1<<12);
lo.f = a - hi.f;
return (doublefloat){hi.f,lo.f};
}
В любом случае с помощью split
или split2
в fmsub
хорошо согласуется с fma(a,b,-c)
из математической библиотеки в Glibc. По какой-то причине моя версия значительно быстрее, чем fma
, за исключением машины, имеющей аппаратное обеспечение (в этом случае я использую _mm_fmsub_ss
).
В x86 и x86_64 gcc испускает инструкции fma, если ему разрешено ('-mfma' или' -mfma4' или '-march = something', где' something' является fma-поддерживающим процессором). В Linux вы можете посмотреть 'sysdeps/ieee754/dbl-64/s_fma.c' в glibc, чтобы получить представление о том, как выглядит резервная функция библиотеки. – tmyklebu