Мой fma() сломан?

при использовании double fma(double x, double y, double z); Я ожидал бы ненулевой d в выходных строках ниже, отмеченных '?'. Это появляется для внутреннего использования только long double точности, а не бесконечной точностью как указано.

на fma функции вычислить (x × y) + z, округленный как одна троичная операция: они вычисляют значение (как если бы) до бесконечной точностью и круглый раз к формату результата, согласно текущий режим округления. §7.12.13.1 2 (мой акцент)

мой fma() сломан, или как я использую его неправильно в коде или параметрах компиляции?

#include <float.h>
#include <math.h>
#include <stdio.h>

int main(void) {
  // Invoking: Cygwin C Compiler
  // gcc -std=c11 -O0 -g3 -pedantic -Wall -Wextra -Wconversion -c -fmessage-length=0 
  //   -v -MMD -MP -MF"x.d" -MT"x.o" -o "x.o" "../x.c"

  printf("FLT_EVAL_METHOD %dn", FLT_EVAL_METHOD);
  for (unsigned i = 20; i < 55; i++) {
    volatile double a = 1.0 + 1.0 / pow(2, i);
    volatile double b = a;
    volatile double c = a * b;
    volatile double d = fma(a, b, -c);
    volatile char *nz = ((i >= 27 && a != 1.0) == !d) ? "?" : "";
    printf("i:%2u a:%21.13a c:%21.13a d:%10a %sn", i, a, c, d, nz);
  }
  return 0;
}

выход

FLT_EVAL_METHOD 2
i:20 a: 0x1.0000100000000p+0 c: 0x1.0000200001000p+0 d:    0x0p+0 
i:21 a: 0x1.0000080000000p+0 c: 0x1.0000100000400p+0 d:    0x0p+0 
i:22 a: 0x1.0000040000000p+0 c: 0x1.0000080000100p+0 d:    0x0p+0 
i:23 a: 0x1.0000020000000p+0 c: 0x1.0000040000040p+0 d:    0x0p+0 
i:24 a: 0x1.0000010000000p+0 c: 0x1.0000020000010p+0 d:    0x0p+0 
i:25 a: 0x1.0000008000000p+0 c: 0x1.0000010000004p+0 d:    0x0p+0 
i:26 a: 0x1.0000004000000p+0 c: 0x1.0000008000001p+0 d:    0x0p+0 
i:27 a: 0x1.0000002000000p+0 c: 0x1.0000004000000p+0 d:   0x1p-54 
i:28 a: 0x1.0000001000000p+0 c: 0x1.0000002000000p+0 d:   0x1p-56 
i:29 a: 0x1.0000000800000p+0 c: 0x1.0000001000000p+0 d:   0x1p-58 
i:30 a: 0x1.0000000400000p+0 c: 0x1.0000000800000p+0 d:   0x1p-60 
i:31 a: 0x1.0000000200000p+0 c: 0x1.0000000400000p+0 d:   0x1p-62 
i:32 a: 0x1.0000000100000p+0 c: 0x1.0000000200000p+0 d:    0x0p+0 ?
i:33 a: 0x1.0000000080000p+0 c: 0x1.0000000100000p+0 d:    0x0p+0 ?
i:34 a: 0x1.0000000040000p+0 c: 0x1.0000000080000p+0 d:    0x0p+0 ?
...
i:51 a: 0x1.0000000000002p+0 c: 0x1.0000000000004p+0 d:    0x0p+0 ?
i:52 a: 0x1.0000000000001p+0 c: 0x1.0000000000002p+0 d:    0x0p+0 ?
i:53 a: 0x1.0000000000000p+0 c: 0x1.0000000000000p+0 d:    0x0p+0 
i:54 a: 0x1.0000000000000p+0 c: 0x1.0000000000000p+0 d:    0x0p+0 

информация о версии

gcc -v

Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/lib/gcc/i686-pc-cygwin/5.3.0/lto-wrapper.exe
Target: i686-pc-cygwin
Configured with: /cygdrive/i/szsz/tmpp/gcc/gcc-5.3.0-5.i686/src/gcc-5.3.0/configure --srcdir=/cygdrive/i/szsz/tmpp/gcc/gcc-5.3.0-5.i686/src/gcc-5.3.0 --prefix=/usr --exec-prefix=/usr --localstatedir=/var --sysconfdir=/etc --docdir=/usr/share/doc/gcc --htmldir=/usr/share/doc/gcc/html -C --build=i686-pc-cygwin --host=i686-pc-cygwin --target=i686-pc-cygwin --without-libiconv-prefix --without-libintl-prefix --libexecdir=/usr/lib --enable-shared --enable-shared-libgcc --enable-static --enable-version-specific-runtime-libs --enable-bootstrap --enable-__cxa_atexit --with-dwarf2 --with-arch=i686 --with-tune=generic --disable-sjlj-exceptions --enable-languages=ada,c,c++,fortran,java,lto,objc,obj-c++ --enable-graphite --enable-threads=posix --enable-libatomic --enable-libcilkrts --enable-libgomp --enable-libitm --enable-libquadmath --enable-libquadmath-support --enable-libssp --enable-libada --enable-libjava --enable-libgcj-sublibs --disable-java-awt --disable-symvers --with-ecj-jar=/usr/share/java/ecj.jar --with-gnu-ld --with-gnu-as --with-cloog-include=/usr/include/cloog-isl --without-libiconv-prefix --without-libintl-prefix --with-system-zlib --enable-linker-build-id --with-default-libstdcxx-abi=gcc4-compatible
Thread model: posix
gcc version 5.3.0 (GCC) 

1 ответов


это вина Cygwin. Или, вернее,newlib библиотека C используется. Это явно говорит он даже не пытается сделать fma() эмуляция корректная.

библиотека GNU C имеет правильную эмуляцию почти для всех вариантов fma с 2015 года. Дополнительные сведения и исправления, используемые для реализации этого, см. В разделе ошибка sourceware 13304.

если эффективность не является проблемой, то я бы просто использовал, например,

#if defined(__CYGWIN__) && !defined(__FMA__) && !defined(__FMA3__) && !defined(__FMA4__)
#define fma(x, y, z)  fma_emulation(x, y, z)

double fma_emulation(double x, double y, double z)
{
    /* One of the implementations linked above */
}
#endif

Я не лично использовать Windows вообще, но если кто-то делает (используйте Windows и нужна эмуляция fma), я бы предложил им попробовать и нажать патч вверх по течению, со ссылкой на Обсуждение Библиотеки GNU C о правильной эмуляции fma.


что мне интересно, можно ли было бы изучить только низкие M биты результата (отбрасываются при округлении) для определения правильного значения ULP в результате и корректировки полученного результата с помощью прямой a×b+c операции таким образом, используя nextafter(); вместо использования многовариантной арифметики для реализации всей операции.

Edit: нет, потому что добавление может переполниться, отбросив дополнительный бит как MSB отброшенной части. Только по этой причине мы должны провести всю операцию. Другая причина в том, что if a×b и c имеют разные знаки, то вместо сложения мы вычитаем меньшее по величине из большего по величине (результат с использованием знака большего), что может привести к очистке нескольких высоких битов в большем, и это влияет на то, какие биты всего результата отбрасываются в округлении.

однако для IEEE-754 Binary64 double на архитектурах x86 и x86-64 я считаю, что эмуляция fma с использованием 64-битных (целочисленных) регистров и 128-битного продукта по-прежнему вполне возможна. Я буду экспериментировать на представлении, где низкие 2 бита в 64-битном регистре используются для округления битов решения (LSB является логическим или всех отброшенных битов), 53 бита, используемых для мантиссы, и один бит переноса, оставляя 8 неиспользуемых и игнорируемых высоких битов. Округление выполняется, когда целое число без знака мантисса преобразуется в (64-разрядный) двойной. Если эти эксперименты дадут что-нибудь полезное, я опишу их здесь.


первоначальные выводы: fma() эмуляция в 32-разрядной системе происходит медленно. 80-битный материал на 387 FPU в основном бесполезен здесь, и реализация 53×53-битного умножения (и битового сдвига) в 32-битной системе справедлива .. не стоит усилий. В glibc fma() код эмуляции, связанный с выше, на мой взгляд, достаточно хорош.

дополнительные выводы: обработка конечных значений гадость. (Субнормалы лишь слегка раздражают, требуя специальной обработки (поскольку неявный MSB в мантиссе равен нулю).) Если любой из трех аргументов не является конечным (бесконечность или какая-то форма NaN), затем возвращение a*b + c (не сплавленный) единственный здравый вариант. Обработка этих случаев требует дополнительного ветвления, что замедляет эмуляцию.

окончательное решение: количество случаев для обработки оптимизированным способом (а не с использованием многовариантного подхода "конечности", используемого в эмуляции glibc) достаточно велико, чтобы сделать этот подход не стоит усилий. Если каждая конечность 64-битная, каждая из a, b и c это растяните не более 2 конечностей, и a×b в течение трех конечностей. (С 32-битными конечностями, то есть всего 3 и 5 конечностей, соответственно.) В зависимости от того,a×b и c имеют те же или разные знаки, есть только два принципиально разных случая для обработки-в случае разных знаков сложение превращается в вычитание (меньше от большего, результат получает тот же знак, что и большее значение).

In короче говоря, подход multiprecision лучше. Фактическая необходимая точность очень хорошо ограничена и не требует даже динамического распределения. Если произведение мантисс a и b смогите быть высчитано эффективно, часть multiprecision ограничена к держать продукт и регулировать добавление/вычитание. Окончательное округление может быть выполнено путем преобразования результата в 53-битную мантиссу, экспоненту и два дополнительных низких бита (чем выше, тем значительнее бит потерянный в округлении, а нижний-или из остальных битов, потерянных в округлении). По сути, ключевые операции могут выполняться с использованием целых чисел (или регистров SSE/AVX), а окончательное преобразование из 55-битной мантиссы в double обрабатывает округление в соответствии с текущими правилами.