Странные результаты со сравнением с плавающей точкой - PullRequest
4 голосов
/ 03 апреля 2009

У меня есть этот простой тест:

double h;
...
// code that assigns h its initial value, used below
...
if ((h>0) && (h<1)){
 //branch 1 -some computations
}
else{
 //branch 2- no computations
}

Я перечислил свои значения, так как получил действительно странные результаты, например, если: h = 1, тогда достигается первая ветвь, и я не понимаю, почему, так как если h = 1, я хочу вычислить branch2. Я что-то смущаюсь?


Edit:

Вот как я вычисляю, а затем использую h:

double* QSweep::findIntersection(edge_t edge1,edge_t edge2) {  
point_t p1=myPoints_[edge1[0]];
point_t p2=myPoints_[edge1[1]];
point_t p3=myPoints_[edge2[0]];
point_t p4=myPoints_[edge2[1]];

double xD1,yD1,xD2,yD2,xD3,yD3,xP,yP,h,denom;
double* pt=new double[3];

// calculate differences  
xD1=p2[0]-p1[0];  
xD2=p4[0]-p3[0];  
yD1=p2[1]-p1[1];  
yD2=p4[1]-p3[1];  
xD3=p1[0]-p3[0];  
yD3=p1[1]-p3[1];    

xP=-yD1;
yP=xD1;
denom=xD2*(-yD1)+yD2*xD1;
if (denom==0) {
    return NULL;
}
else{
h=(xD3*(-yD1)+yD3*xD1)/denom;
}
std::cout<<"h is"<<h<<endl;
if (h < 1) std::cout<<"no"<<endl;
else std::cout<<"yes"<<endl;
if (h==1) {
    return NULL;
}
else{
if ((h>0)&&(h<1)){
    pt[0]=p3[0]+xD2*h;  
    pt[1]=p3[1]+yD2*h;
    pt[2]=0.00;
}
else{
    return NULL;
}
}


return pt;  

}


Edit:

Хорошо, ясно, как мне переформулировать условие.

От:

double h;
if (h==1){
   //computations here
}

Кому:

double h;
if (abs(h-1)<tolerance){
  //computations here
}

Когда я использую двойные числа.

Но как мне переформулировать это?

double h;
if (h<1){
   //computations here
}

Ответы [ 10 ]

14 голосов
/ 03 апреля 2009

Так как h является двойным, возможно, он был достаточно близок к 1, чтобы вывести его как 1, но на самом деле он немного меньше 1, поэтому сравнение успешно выполняется. Числа с плавающей точкой делают это много.

5 голосов
/ 03 апреля 2009

Проверьте фактическое значение h с помощью , распечатав его с максимальной точностью . Вы, вероятно, обнаружите, что оно на самом деле немного меньше 1,0.

Я запустил следующий код в качестве теста

#include <iostream>

int main()
{
    double h = 1.0;
    if((h>0) && (h<1))
    {
        std::cout << "first branch" << std::endl;
    }
    else
    {
        std::cout << "second branch" << std::endl;
    }
}

и вывод был "первая ветвь" (с использованием g ++ 4.3.2 в Ubuntu 8.10), но Indeera упомянул в комментарии, что тот же код, работающий в Windows XP, скомпилированный с VS2005, дает вывод "вторая ветвь" (спасибо Indeera ).

Вы можете изменить свой код, чтобы сравнить различия между h и 0,0 и h и 1,0 с небольшим значением дельты.

double allowedDelta = 0.000001;

if( ((h - 0.0) > allowedDelta) && ((1.0 - h) > allowedDelta) )
... // h is between 0.000001 and 0.9999990

Обратите внимание, что в этом особом случае "(h - 0.0)" можно заменить на "h". Я оставляю это так, как оно есть для наглядности.

Также обратите внимание, что если вы проводите только одно сравнение, вам нужно сравнить дельту с абсолютным значением разницы между h и некоторой константой. Поскольку здесь вы проверяете диапазон, два сравнения AND, объединенные вместе, представляют собой особый случай, когда вы можете обойти использование abs. Если h является отрицательным значением или некоторым положительным значением больше 1,0, оно выйдет за пределы допустимого диапазона и провалит один из двух тестов, приведенных выше.

4 голосов
/ 03 апреля 2009

Короткая история: Ваши тесты неверны, потому что числа с плавающей запятой ведут себя не так, как вы, вероятно, ожидаете. В частности, такие вещи, как «denom == 0» проблематичны.

Sun был достаточно хорош, чтобы предоставить эту статью онлайн:

Что должен знать каждый компьютерщик об арифметике с плавающей запятой

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

ДОБАВЛЕННЫЙ КОММЕНТАРИЙ: Я не предполагаю, что каждый программист легко поймет все в этой статье. Чтение этого документа, по крайней мере, даст лучшее представление о том, что на самом деле представляют собой поплавки, в чем заключаются проблемы, и некоторые советы о том, как правильно обращаться с вещами.

Если вы хотите правильно выполнить большую часть числовой работы, вам придется ознакомиться с различными техниками, но это будет материал из учебника (или нескольких). Комментарии здесь уже указали некоторые из основ, и связаны с более

3 голосов
/ 03 апреля 2009

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

if (abs(x - y) < epsilon) ...

, где эпсилон - какое-то подходящее небольшое значение, например, 0,00001.

Есть несколько причин, по которым математика с плавающей запятой не точна. Во-первых, не все значения могут быть представлены точно (например, 0,1 не могут быть представлены в двоичных числах с плавающей запятой, так же как 1/3 не может быть представлена ​​в десятичных числах)

Другое - то, что с плавающей запятой используется только фиксированное количество значащих цифр (которые «плавают» относительно десятичной точки, отсюда и название). Таким образом, в больших количествах маленькие фракции эффективно усекаются. Никогда не предполагайте, что вычисления с плавающей запятой возвращают точный результат.

1 голос
/ 03 апреля 2009

Это может быть проблема точности. H может быть не совсем 1, но что-то очень близко к нему. Не могли бы вы опубликовать дополнительную информацию о том, что вы пытаетесь сделать, например, откуда берется значение H и куда оно уходит?

0 голосов
/ 03 апреля 2009

Вас может заинтересовать Числовая устойчивость для геометрических вычислений (он же "EPSILON НЕ 0,00001!"), Представленный на GDC 2005.

0 голосов
/ 03 апреля 2009

Хорошо, вы разместили код. Вы вычисляете h с помощью ряда арифметических операций из того, что выглядит как довольно произвольные числа. Это означает, что вы получите очень близкое приближение к идеальному значению h, но не совсем правильное.

Это означает, что вам нужно делать приблизительные сравнения. Тестирование (h == 1.0) будет выполнено только случайно; попробуй (fabs (h - 1.0) <1e-10) или что-то в этом роде (используя const double вместо магического числа для толерантности) Сделайте соответствующие изменения для других сравнений. </p>

0 голосов
/ 03 апреля 2009

Причина в том, что числа с плавающей запятой не являются реальным представлением числа, которое вы сохраняете в переменной. (В противоположность BCD [двоично-десятичные десятичные числа])

Вы можете увидеть определение здесь: Плавающая точка Википедия

Таким образом, проблема в том, что определенные числа не могут быть выражены с заданным набором битов. (Вы могли бы, если бы вы могли добавлять биты бесконечно) Хитрость в том, что в большинстве случаев разница между сохраненным номером и предполагаемым числом довольно мала. На практике у вас есть несколько угловых случаев, когда это может привести к проблемам. Это, например, причина, по которой вам не следует создавать финансовое программное обеспечение и использовать числа с плавающей запятой для расчета денег. Вы можете легко заметить различия, которые не понравятся вашей налоговой инспекции.

Таким образом, для сравнения чисел с плавающей запятой вы всегда должны применять какой-то порог, который подходит для вашего приложения. Что-то вроде:

if(a==b) 

становится

if(abs(a-b)<threshold)

Редактировать: Как отметил Дэвид в своем комментарии, у вас все еще будут проблемы с такими вещами, как пи, 1./3., ... Но вы можете, по крайней мере, хранить цифры без потери точности, которые вы положили в систему. Поскольку у компьютеров ограниченная память, вы всегда можете построить угловые случаи, когда вы не можете полагаться на точное представление ...

Только что видел ваше редактирование текста, поэтому вот следующее редактирование:

if(a<1)

Как-то сложнее, потому что вы не знаете, является ли неправильным только представление чисел или просто реальное значение, близкое к 1. Это действительно зависит от требований вашего алгоритма. Если небольшая ошибка в порядке, тогда выполните:

if(a < 1-threshold)

Если это не нормально, тогда вам нужно использовать другой тип переменной, который не страдает от проблемы.

0 голосов
/ 03 апреля 2009

Это может быть связано с тем фактом, что в C / C ++ значения типа double являются 64-битными, но вычисления могут выполняться с более высокой точностью (регистры с плавающей запятой вашего процессора шире (96-битные), так что даже такой оператор, как cos (x) == cos (x) может быть истинным.

Ссылка: http://www.parashift.com/c++-faq-lite/newbie.html#faq-29.18

0 голосов
/ 03 апреля 2009

если вам нужно использовать поплавки в чеках, округлите их и сохраните, например, в целое число. 1f может быть 1,0000000000000000000000000000000001 или 0,9999999999999999999999999999999999999

...