Неожиданная потеря точности при делении двойных - PullRequest
6 голосов
/ 30 марта 2009

У меня есть функция getSlope, которая принимает в качестве параметров 4 double и возвращает еще один double, рассчитанный с использованием этих параметров следующим образом:

double QSweep::getSlope(double a, double b, double c, double d){
double slope;
slope=(d-b)/(c-a);
return slope;
}

Проблема в том, что при вызове этой функции с аргументами, например:

getSlope(2.71156, -1.64161, 2.70413, -1.72219);

возвращаемый результат:

10.8557

и это не очень хороший результат для моих вычислений. Я рассчитал наклон с помощью Mathematica, и результат для наклона для тех же параметров:

10.8452

или с большим количеством цифр для точности:

10.845222072678331.

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

спасибо заранее, Madalina


Я печатаю результат с помощью командной строки:

std::cout<<slope<<endl;

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

Когда вычисляется график, из которого я читаю мои параметры, используются некоторые числовые библиотеки, написанные на C ++ (с шаблонами). OpenGL не используется для этого вычисления.

спасибо, Madalina

Ответы [ 8 ]

7 голосов
/ 30 марта 2009

Я попытался использовать float вместо double, и в результате я получил 10.845110. Это все еще выглядит лучше, чем результат Madalina.

EDIT:

Я думаю, я знаю, почему вы получаете этот результат. Если вы получаете параметры a, b, c и d откуда-то еще и печатаете их, это дает вам округленные значения. Тогда, если вы поместите его в Mathemtacia (или calc;)), он даст вам другой результат.

Я попытался немного изменить один из ваших параметров. Когда я сделал:

double c = 2.7041304;

Я получаю 10,845806. Я только добавить 0,0000004 к с! Поэтому я думаю, что ваши «ошибки» не являются ошибками. Напечатайте a, b, c и d с большей точностью, а затем отправьте их в Mathematica.

5 голосов
/ 30 марта 2009

следующий код:

#include <iostream>
using namespace std;

double getSlope(double a, double b, double c, double d){
    double slope;
    slope=(d-b)/(c-a);
    return slope;
}

int main( ) {
    double s = getSlope(2.71156, -1.64161, 2.70413, -1.72219);
    cout << s << endl;
}

дает результат 10.8452 с g ++. Как вы распечатываете результат в вашем коде?

5 голосов
/ 30 марта 2009

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

Вы можете проверить свои настройки точности с помощью

std::sqrt(x) * std::sqrt(x)

Результат должен быть очень близок к x. Я давно столкнулся с этой проблемой и провёл месяц, проверяя все формулы. Но потом я нашел

D3DCREATE_FPU_PRESERVE
3 голосов
/ 30 марта 2009

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

EDIT:

Нейл прав в своем комментарии к этому вопросу, я вычислил ответ в VB, используя Doubles, и получил тот же ответ, что и mathematica.

2 голосов
/ 30 марта 2009

Получаемые результаты соответствуют 32-битной арифметике. Не зная больше о вашей среде, невозможно посоветовать, что делать.

Если предположить, что показанный код работает, то есть вы ничего не конвертируете в строки или числа с плавающей запятой, то в C ++ нет исправления. Это за пределами кода, который вы показали, и зависит от среды.

Когда Патрик Макдональд и Треб повысили точность ваших входных данных и ошибку на a-c, я подумал, что на это взгляну. Одним из методов рассмотрения ошибок округления является интервальная арифметика, которая устанавливает верхнюю и нижнюю границы, значение которых представляет явное (они неявны в числах с плавающей запятой и фиксируются с точностью представления). Обрабатывая каждое значение как верхнюю и нижнюю границы и расширяя границы на ошибку в представлении (приблизительно x * 2 ^ -53 для двойного значения x), вы получаете результат, который дает нижнюю и верхнюю границы для точность значения с учетом ошибок точности худшего случая.

Например, если у вас есть значение в диапазоне [1.0, 2.0] и вычесть из него значение в диапазоне [0.0, 1.0], то результат должен лежать в диапазоне [ниже (0.0), выше ( 2.0)] как минимальный результат 1,0-1,0, а максимальный 2,0-0,0. below и above эквивалентны полу и потолку, но для следующего представимого значения, а не для целых чисел.

Использование интервалов, которые представляют наихудшее двойное округление:

getSlope(
 a = [2.7115599999999995262:2.7115600000000004144], 
 b = [-1.6416099999999997916:-1.6416100000000002357], 
 c = [2.7041299999999997006:2.7041300000000005888], 
 d = [-1.7221899999999998876:-1.7221900000000003317])
(d-b) = [-0.080580000000000526206:-0.080579999999999665783]
(c-a) = [-0.0074300000000007129439:-0.0074299999999989383218]

to double precision [10.845222072677243474:10.845222072679954195]

Таким образом, хотя c-a мало по сравнению с c или a, оно все равно велико по сравнению с двойным округлением, поэтому, если вы использовали наихудшее из возможных округление с двойной точностью, то вы могли бы доверять этому значению, чтобы быть точным до 12 цифр - 10,8452220727. Вы потеряли несколько цифр из-за двойной точности, но вы по-прежнему работаете не только со значением, которое вы вводите.

Но если бы входные данные были точны только до значащих цифр, то вместо того, чтобы быть двойным значением 2.71156 +/- eps, диапазон ввода был бы [2.711555,2.711565], поэтому вы получите результат:

getSlope(
 a = [2.711555:2.711565], 
 b = [-1.641615:-1.641605], 
 c = [2.704125:2.704135], 
 d = [-1.722195:-1.722185])
(d-b) = [-0.08059:-0.08057]
(c-a) = [-0.00744:-0.00742]

to specified accuracy [10.82930108:10.86118598]

, что намного шире.

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

С другой стороны, если ваши входные данные известны только из 6 цифр, на самом деле не имеет значения, получите ли вы 10.8557 или 10.8452. Оба находятся в пределах [10.82930108: 10.86118598].

1 голос
/ 30 марта 2009

Лучше Распечатайте аргументы тоже. Когда вы, как я предполагаю, передаете параметры в десятичной записи, вы теряете точность для каждого из них. Проблема в том, что 1/5 - это бесконечный ряд в двоичном виде, например, 0.2 становится .001001001 .... Кроме того, десятичные дроби прерываются при преобразовании двоичного числа с плавающей запятой в текстовое представление в десятичном формате.

Кроме того, иногда компилятор выбирает скорость, а не точность. Это должен быть документированный переключатель компилятора.

0 голосов
/ 27 мая 2009

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

Это будет иметь некоторые накладные расходы, но вы сможете найти что-то с довольно гарантированной точностью.

0 голосов
/ 30 марта 2009

Патрик , кажется, прав в том, что (c-a) является основной причиной:

d-b = -1,72219 - (-1,64161) = -0,08058

c-a = 2,70413 - 2,71156 = -0,00743

S = (d-b) / (c-a) = -0,08058 / -0,00743 = 10,845222

Вы начинаете с точностью до шести цифр, благодаря вычитанию вы получаете сокращение до 3 и четырех цифр. Мое лучшее предположение, что вы теряете дополнительную точность, потому что число -0,00743 не может быть представлено точно в двойном размере. Попробуйте использовать промежуточные переменные с большей точностью, например:

double QSweep::getSlope(double a, double b, double c, double d)
{
    double slope;
    long double temp1, temp2;

    temp1 = (d-b);
    temp2 = (c-a);
    slope = temp1/temp2;

    return slope;
}
...