Ошибка Visual C ++ math.h
-
20-09-2019 - |
Вопрос
Я отлаживал свой проект и не смог найти ошибку.Наконец я нашел его.Посмотрите на код.Вы думаете, что все в порядке, и результатом будет "ОК!Хорошо!Хорошо!", не так ли?Теперь скомпилируйте его с помощью VC (я пробовал vs2005 и vs2008).
#include <math.h>
#include <stdio.h>
int main () {
for ( double x = 90100.0; x<90120.0; x+=1 )
{
if ( cos(x) == cos(x) )
printf ("x==%f OK!\n", x);
else
printf ("x==%f FAIL!\n", x);
}
getchar();
return 0;
}
Магическая двойная константа равна 90112.0.Когда x < 90112.0 все в порядке, когда x > 90112.0 - Нет!Ты можешь заменить cos на sin.
Есть какие-нибудь идеи?Не забывайте, что sin и cos являются периодическими.
Решение
Может быть это: http://www.parashift.com/c++-faq-lite/newbie.html#faq-29.18
Я знаю, что это трудно принять, но арифметика с плавающей запятой просто не работает так, как ожидает большинство людей.Хуже того, некоторые различия зависят от особенностей аппаратного обеспечения вашего конкретного компьютера с плавающей запятой и/или настроек оптимизации, которые вы используете в вашем конкретном компиляторе.Возможно, вам это не нравится, но это так.Единственный способ «получить это» — отказаться от своих предположений о том, как обстоят дела. должен вести себя и принимать вещи такими, какие они есть на самом деле делать вести себя...
(с ударением на слове «часто»;поведение зависит от вашего оборудования, компилятора и т. д.):Вычисления и сравнения с плавающей запятой часто выполняются специальным оборудованием, которое часто содержит специальные регистры, и эти регистры часто имеют больше битов, чем
double
.Это означает, что промежуточные вычисления с плавающей запятой часто содержат больше битов, чемsizeof(double)
, и когда значение с плавающей запятой записывается в ОЗУ, оно часто усекается, часто теряя некоторые биты точности...просто запомни это:Сравнения с плавающей запятой сложны, тонки и чреваты опасностью.Будь осторожен.Путь с плавающей запятой на самом деле работает отличается от того, как об этом думает большинство программистов. должен работать.Если вы собираетесь использовать плавающую запятую, вам нужно узнать, как это на самом деле работает...
Другие советы
Как отмечали другие, математическая библиотека VS выполняет вычисления на FPU x87 и генерирует 80-битные результаты, даже если тип двойной.
Таким образом:
- вызывается cos() и возвращает значение cos(x) на вершине стека x87 в виде 80-битного числа с плавающей запятой.
- cos(x) извлекается из стека x87 и сохраняется в памяти как двойной;это приводит к округлению до 64-битного числа с плавающей запятой, что меняет свое значение
- вызывается cos() и возвращает значение cos(x) на вершине стека x87 в виде 80-битного числа с плавающей запятой.
- округленное значение загружается в стек x87 из памяти
- округленные и неокругленные значения cos(x) сравниваются неравными.
Многие математические библиотеки и компиляторы защищают вас от этого, либо выполняя вычисления в 64-битном формате с плавающей запятой в регистрах SSE, если они доступны, либо принудительно сохраняя и округляя значения перед сравнением, либо сохраняя и перезагружая окончательный результат в фактическом вычислении. из cos( ).Комбинация компилятора и библиотеки, с которой вы работаете, не такая уж и прощающая.
Процедура cos(x) == cos(x), созданная в режиме выпуска:
00DB101A call _CIcos (0DB1870h) 00DB101F fld st(0) 00DB1021 fucompp
Значение вычисляется один раз, затем клонируется, затем сравнивается само с собой — результат будет в порядке.
То же самое в режиме отладки:
00A51405 sub esp,8 00A51408 fld qword ptr [x] 00A5140B fstp qword ptr [esp] 00A5140E call @ILT+270(_cos) (0A51113h) 00A51413 fld qword ptr [x] 00A51416 fstp qword ptr [esp] 00A51419 fstp qword ptr [ebp-0D8h] 00A5141F call @ILT+270(_cos) (0A51113h) 00A51424 add esp,8 00A51427 fld qword ptr [ebp-0D8h] 00A5142D fucompp
Теперь происходят странные вещи.
1.X загружается в fstack (X, 0)
2.X хранится в обычном стеке (усечение)
3.Косинус вычисляется, результат в стеке с плавающей запятой
4.X снова загружается
5.X хранится в обычном стеке (усечение, на данный момент мы «симметричны»)
6.Результат 1-го косинуса, который был в стеке, сохраняется в памяти, теперь для 1-го значения происходит еще одно усечение.
7.Косинус вычисляется, второй результат, если в стеке с плавающей запятой, но это значение было усечено только один раз
8.Первое значение загружается в fstack, но это значение было усечено дважды (один раз перед вычислением косинуса, один раз после)
9.Эти два значения сравниваются — мы получаем ошибки округления.
Вам следует никогда не сравнивайте двойные значения на равенство в большинстве случаев.Вы можете не получить то, что ожидаете.
Регистры с плавающей запятой могут иметь размер, отличный от значений памяти (в современных машинах Intel регистры FPU имеют размер 80 бит против 64 бит).Если компилятор генерирует код, который вычисляет первый косинус, затем сохраняет значение в памяти, вычисляет второй косинус и сравнивает значение в памяти со значением в регистре, тогда значения могут отличаться (из-за проблем с округлением от 80 до 64 бит) .
Значения с плавающей запятой немного сложны.Google для сравнения с плавающей запятой.
Компилятор мог сгенерировать код, который в конечном итоге сравнивает 64-разрядное двойное значение с 80-разрядным внутренним регистром с плавающей запятой.Проверка значений с плавающей запятой на равенство подвержена такого рода ошибкам - вам почти всегда лучше выполнять "нечеткое" сравнение, например (fabs(val1 - val2) < ЭПСИЛОН), а не (val1 == val2).
Увеличение и тестирование значения с плавающей запятой в качестве переменной управления циклом, как правило, является очень плохой идеей.Создайте отдельный int LCV только для цикла, если вам нужно.
В этом случае проще:
for ( int i = 90100; i<90120; i+=1 ) {
if ( cos(i) == cos(i) )
printf ("i==%d OK!\n", i);
else
printf ("i==%d FAIL!\n", i);
}
Как обойти проблему?Изменить если блокировать:
if ( (float)cos(x) == (float)cos(x) )