С++ потеря точности с плавающей запятой: 3015/0,00025298219406977296

Проблема.

Компилятор Microsoft Visual C++ 2005, 32-битная Windows XP SP3, процессор amd 64 x2.

Код:

double a = 3015.0; 
double b = 0.00025298219406977296;
//*((unsigned __int64*)(&a)) == 0x40a78e0000000000  
//*((unsigned __int64*)(&b)) == 0x3f30945640000000  
double f = a/b;//3015/0.00025298219406977296;

результат вычисления (т.е. "f") равен 11917835.000000000 (((unsigned __int64)(&f)) == 0x4166bb4160000000), хотя должно быть 11917834.814763514 (т.е. ((unsigned __int64)(&f)) == 0x4166bb415a128aef).
Т.е. дробная часть потеряна.
К сожалению, мне нужно, чтобы дробная часть была правильной.

Вопросы:
1) Почему это происходит?
2) Как решить проблему?

Дополнительная информация:
0) Результат берется напрямую из окна "часы" (не распечатывался, и я не забыл выставить точность печати). Я также предоставил шестнадцатеричный дамп переменной с плавающей запятой, поэтому я абсолютно уверен в результате вычисления.
1) Дизассемблирование f = a/b:

fld         qword ptr [a]  
fdiv        qword ptr [b]  
fstp        qword ptr [f]  

2) f = 3015/0,00025298219406977296; дает правильный результат (f == 11917834.814763514 , ((unsigned __int64)(&f)) == 0x4166bb415a128aef ), но похоже, что в этом случае результат просто вычисляется во время компиляции:

fld         qword ptr [__real@4166bb415a128aef (828EA0h)]  
fstp        qword ptr [f]  

Итак, как я могу решить эту проблему?

P.S. Я нашел временный обходной путь (мне нужна только дробная часть деления, поэтому я просто использую f = fmod(a/b)/b на данный момент), но я все еще хотел бы знать, как правильно решить эту проблему - double предполагается, что точность составляет 16 десятичных цифр, поэтому такой расчет не должен вызывать проблем.


person SigTerm    schedule 28.03.2010    source источник


Ответы (5)


Используете ли вы directx в своей программе где-либо, поскольку это приводит к тому, что блок с плавающей запятой переключается в режим одинарной точности, если вы специально не укажете, что это не так, когда вы создаете устройство, и вызовет именно это

person jcoder    schedule 28.03.2010
comment
Это правильный ответ. Программа использует Direct3D, и, естественно, расчет происходит после создания устройства. Самое смешное, что я знал о D3D, настраивающем точность FPU, но я совершенно забыл об этом, потому что я не видел эту ошибку в последние несколько лет. Проблема решена. - person SigTerm; 28.03.2010
comment
Какой флаг использовать при создании устройства? Такая же проблема существует с Direct2D? - person dalle; 04.08.2010

Интересно, что если вы объявите и a, и b как числа с плавающей запятой, вы получите ровно 1 191 7835.000000000. Итак, я предполагаю, что где-то происходит преобразование в одинарную точность, либо в том, как интерпретируются константы, либо позже в вычислениях.

Однако любой случай немного удивителен, учитывая, насколько прост ваш код. Вы не используете какие-либо экзотические директивы компилятора, устанавливающие одинарную точность для всех чисел с плавающей запятой?

Изменить: вы действительно подтвердили, что скомпилированная программа выдает неверный результат? В противном случае наиболее вероятным кандидатом на (ошибочное) преобразование с одинарной точностью будет отладчик.

person Josef Grahn    schedule 28.03.2010
comment
Нет приведения к одинарной точности, как ясно показывает разборка. - person Axel Gneiting; 28.03.2010
comment
Во всяком случае, не на этих трех линиях. - person Josef Grahn; 28.03.2010

Если вам нужна точная математика, не используйте числа с плавающей запятой.

Сделайте себе одолжение и получите библиотеку BigNum с поддержкой рациональных чисел.

person Frank Krueger    schedule 28.03.2010
comment
Ему не нужно 11917834.814763514100059144562708, ему просто нужно 11917834.814763514. Отказ от порядков в производительности и памяти только для того, чтобы получить точность, встроенную в машину, кажется немного иррациональным (простите за каламбур). - person Gabe; 28.03.2010
comment
Конечно, мы не имеем права ожидать точности, но мы все же вправе требовать того уровня точности, который обещает нам спецификация с плавающей запятой! - person AakashM; 28.03.2010
comment
Без обид, но я думаю, что использовать большие числа только для одного вычисления — это слишком, по крайней мере, в этом случае. - person SigTerm; 29.03.2010

Я предполагаю, что вы печатаете число без указания точности. Попробуй это:

#include <iostream>
#include <iomanip>

int main() { 
    double a = 3015.0; 
    double b = 0.00025298219406977296;
    double f = a/b;

    std::cout << std::fixed << std::setprecision(15) << f << std::endl;
    return 0;
}

Это производит:

11917834.814763514000000

Что мне кажется правильным. Я использую VC++ 2008 вместо 2005, но я думаю, что разница в вашем коде, а не в компиляторе.

person Jerry Coffin    schedule 28.03.2010
comment
Нет, я не печатаю номер, результат берется прямо из окошка часов. - person SigTerm; 28.03.2010
comment
Вы пробовали его распечатать? Может баг в окошке часов!! - person Martin Beckett; 28.03.2010

Вы уверены, что проверяете значение f сразу после инструкции fstp? Если у вас включена оптимизация, возможно, окно просмотра может показывать значение, полученное в какой-то более поздний момент (это кажется немного правдоподобным, поскольку вы говорите, что смотрите на дробную часть f позже - не маскирует ли его какая-то инструкция как-то выходит?)

person Mike Dinsdale    schedule 28.03.2010