4

Есть функция

inline unsigned long long d(double d)
{
    return (864E9 * d + 0x014f35a9a90cc000 - 0x019DB1DED53E8000) / 10;
}

При ее работе вылетает такая ошибка(код 0xC00002B4)

Множественные ошибки вычислений с плавающей точкой (parameters: 0x00000000, 0x000005A0).

Самое интересное что эта функция вызывается дважды с одним и тем же входным параметром. И такая ошибка появляется при повторном вызове. Я пробовал сделать семпл в отдельном проекте но воспроизвести ошибку не получилось.

Что эта ошибка вообще означает? и как это можно исправить?

PS. VisualStudio 2015

Настройки проекта(Code Generation)

Smaller Type Check: No
Basic Runtime Checks: Default
Runtime Library: /MDd
Floating Point Model: Precise (/fp:precise)

PS2. Если переписать функцию так:

inline unsigned long long GetTsFromDate(double date)
{
    unsigned long long tmp = 0x014f35a9a90cc000 - 0x019DB1DED53E8000;
    double dbl = 864E9 * date;
    double dbl2 = dbl + tmp;
    double result = dbl2 / 10;
    return result;
}

То будет падать на double dbl = 864E9 * date

Rikitikitavi
  • 2,443
  • 4
  • 20
  • 37
  • 2
    А вы уверены, что это выражение делает то, что вы хотите? мешанина double и long не есть хорошо. Также приведите код, воспроизводящий заявленную проблему. – user7860670 Dec 13 '17 at 10:22
  • Да уверен, это конвертация VT_DATE в количество микросекунд прошедших с 01.01.1970 – Rikitikitavi Dec 13 '17 at 10:26
  • 1
    Возможно, код где-то ещё выключает маскирование исключений, и если их было несколько видов, выбрасывается такая общая ошибка. – Lyth Dec 13 '17 at 10:36
  • А какие настройки проекта? (C++ -> Code generation) – VladD Dec 13 '17 at 10:46
  • Добавил настройки проекта. Все остальные по умолчанию – Rikitikitavi Dec 13 '17 at 10:53
  • 4
    А какие-то конкретные значения d можно? чтоб попробовать воспроизвести? – Harry Dec 13 '17 at 12:16
  • Почему в вопросе не указаны значения d/date, приводящие к падению? Также: http://forums.codeguru.com/showthread.php?456550-Error-Multiple-Floating-Point-Traps – AnT stands with Russia Dec 13 '17 at 16:52
  • Или вы портите память и ловите странности (это обычно с тем, что первые пару раз все работает, а потом нет), причем портить память можно совсем в другом месте, либо тут хитрые оптимизации компилятора, попробуй просто выключить все оптимизации – bukkojot Dec 13 '17 at 17:25
  • Вот например значения на которых падает 43083.341116850243; 43082.678764358694;43082.611894165559 – Rikitikitavi Dec 14 '17 at 01:11
  • Не воспроизводится в VS2017. Поднял VS2015. Не воспроизводится и в VS2015. Дело то ли в настройках проекта, то ли в вашей версии VS2015, то в том, что вы что-то недоговариваете. – AnT stands with Russia Jan 08 '18 at 20:52
  • Эти две функции работают по-разному - подробнее в комментарии. – Qwertiy Jun 18 '19 at 17:30

2 Answers2

8

Провёл несколько экспериментов в Visual Studio 2010.

Создал консольное приложение с настройками по умолчанию (в частности, модель вычислений с плавающей точкой /fp:precise, исключения с плавающей точкой отключены /fp:except-). Все примеры выполнялись в Debug-режиме.

Воспользуемся функцией _statusfp2 для того чтобы узнать floating-point status word в следующем простом примере:

#include <iostream>
#include <limits>
#include <float.h>
using namespace std;
#pragma fenv_access (on)

int main()
{
    cout.precision(numeric_limits<double>::max_digits10);
    unsigned int x86, SSE2;
    double d;

    _statusfp2(&x86, &SSE2);
    cout << x86 << " " << SSE2 << endl;
    d = 0.0;
    d = d * 1.0;
    _statusfp2(&x86, &SSE2);
    cout << d << endl;
    cout << x86 << " " << SSE2 << endl;

    return 0;
}

В моём случае, программа выдала следующий результат:

0 0
0
0 0

Насколько я понял, нулевые значения переменных x86 и SSE2 означают, что при вычислениях с плавающей точкой никаких исключительных ситуаций не произошло. Это логично, ибо никаких "опасных" вычислений в данном пример не осуществляется.

Воспользуемся примером из вашего вопроса:

d = 864E9;
d = d * 43083.341116850243; 
_statusfp2(&x86, &SSE2);
cout << d << endl;
cout << x86 << " " << SSE2 << endl;

В моем случае программа выдала следующий результат:

37224006724958608
1 0

В приведённом выводе программы есть два важных момента.

Во-первых, результат перемножения двух чисел 864E9 и 43083.341116850243 получился не точным. Это легко проверить перемножением этих чисел на калькуляторе: точный результат содержит 20 значащих цифр. Такое число не может быть представлено в переменной типа double.

Во-вторых, изменилось состояние floating-point status word. Судя по справке, status word приняло значение _EM_INEXACT. То есть имела место исключительная ситуация — результат арифметической операции не может быть точно представлен в переменной типа double. Тем не менее, программа завершила свою работу нормально, никакого исключения сгенерировано не было. Это обусловлено тем, что по-умолчанию исключения с плавающей точкой замаскированы.

Воспользуемся функцией _controlfp_s для того, чтобы демаскировать исключение _EM_INEXACT:

 unsigned int currentControl
_clearfp();
_controlfp_s(&currentControl, 0, 0);
_controlfp_s(nullptr, 
             currentControl & ~static_cast<unsigned int>(_EM_INEXACT), 
             _MCW_EM);

d = 864E9;
d = d * 43083.341116850243; 

В этот раз на строке d = d * 43083.341116850243; выскакивает ошибка:

0xC000008F: Floating-point inexact result.

В настройках проекта включим расширенный набор инструкций /arch:SSE2 и на той же самой строке выскакивает немного другая ошибка:

0xC00002B4: Множественные ошибки вычислений с плавающей точкой.

А это весьма похоже на ошибку в вашем вопросе.

Полагаю, где-то в вашем проекте по какой-то причине демаскируются исключения с плавающей точкой, и обратная маскировка не производится.

Помимо исключения _EM_INEXACT есть также исключения _EM_INVALID, _EM_DENORMAL, _EM_ZERODIVIDE, _EM_OVERFLOW и _EM_UNDERFLOW. Замаскировать их все можно следующим образом:

_clearfp();
_controlfp_s(nullptr, _MCW_EM, _MCW_EM);

P.S. При манипулировании floating-point status word прагма #pragma fenv_access (on) обязательна, иначе компилятор может проигнорировать манипуляции.

Также, если всё-таки хочется обрабатывать исключения с плавающей точкой, то судя по всему необходимо использовать опцию /fp:except, иначе может что-нибудь сломаться. Но тут я не уверен.

wololo
  • 6,221
1

В вашей второй функции первая строка меня очень смущает.

unsigned long long tmp = 0x014f35a9a90cc000 - 0x019DB1DED53E8000;

Зачем вы присваиваете unsigned long число, которое заведомо меньше нуля? Тут даже видно, 0х014 - 0х019. И зачем вам long long, когда хватит обычного long? У меня при выполнении этого действия(отладка) tmp == 18424652457709551616

Далее, смотрю на первую функцию. Вижу что-то вроде переполнения.

864E9 * d + 0x014f35a9a90cc000 

здесь вы к double результату 864E9 * d прибавляете 0x014f35a9a90cc000, а второе число уже длиннее пятнадцати значащих знаков double, вряд ли вы контролируете это переполнение. (у double после 15-го знака идут нули) Ну и далее, от сомнительного результата предыдущего действия вы отнимаете еще один long, значение которого опять же больше double. Вот вам и множественные ошибки вычислений.

По умолчанию такие ошибки(исключения) скрыты, у вас, видимо, они включены.

  • Если я в чем-то сильно ошибся - комментируйте и минусите смело. – Victor Gorban Jan 08 '18 at 12:30
  • 1
    Ну, присвоение отрицательного значения беззнаковой переменной — вполне определённое поведение. Отрицательное число просто приведётся по модулю ULLONG_MAX + 1. И не очень ясен смысл фразы "второе число уже больше double"... – wololo Jan 08 '18 at 16:00
  • Присвоение отрицательного значения беззнаковой переменной - непрозрачное поведение. Насчет второго числа - исправил. Я имел в виду, что "у double после 15-го знака идут нули", т.е. потеря точности. – Victor Gorban Jan 08 '18 at 17:16
  • @Victor Gorban: Что такое "непрозрачное поведение"? Конвертация отрицательных значений к беззнаковому типу - во многом идиоматическая практика. И вообще-то "потеря точности" - это формальный термин с определенным значением. Появление нулей в хвосте из за фиксированной ширины мантиссы - это еще не потеря точности. – AnT stands with Russia Jan 08 '18 at 17:20
  • Нули появляются, все верно. Но посмотрите на число '0x014f35a9a90cc000', которое мы тут обсуждаем: тут 15 цифр в 16-ричной системе, в 10 с/с будет уже 18. Это не потеря точности даже, это потеря значения, т.к. double не может вместить такое большое целое число. Или я в чем-то ошибаюсь? – Victor Gorban Jan 08 '18 at 18:31
  • @VictorGorban, число 0x014f35a9a90cc000 отлично представимо в double точно. У double есть 53 бита, у этого числа 57 значащих бит, но последние 14 из них нули. Соответственно, для его хранения достаточно 43 бит, что гораздо меньше 53 доступных. Аналогично для числа 0x019DB1DED53E8000 достаточно 42 бит, что тоже меньше 53. Во второй функции вычитание идёт в типе unsigned long long, что означает переполнение и результат 0xFFB183CAD3CE4000, на который надо 50 бит - тоже влезло. Но вот в double это уже совсем другое число (на 2**64 больше), так что переписанный вариант функции, вероятно, ошибочен. – Qwertiy Jun 18 '19 at 17:08
  • https://ru.stackoverflow.com/a/734615/178988 – Qwertiy Jun 18 '19 at 17:21