Неожиданная потеря точности при делении двойников

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

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 (предположим, что это примерный результат деления)? Как я могу получить хороший результат для своего дивизиона?

заранее спасибо, мадалина


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

std::cout<<slope<<endl;

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

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

спасибо, мадалина


person madalina    schedule 30.03.2009    source источник
comment
Проверьте, как метод был скомпилирован на ассемблере. Я думаю, вы можете сделать это в отладчике (по крайней мере, в Visual Studio).   -  person Saulius Žemaitaitis    schedule 30.03.2009
comment
Мой Windows calc дает такой же хороший результат, как Mathematica: D   -  person klew    schedule 30.03.2009
comment
Какой компилятор, какая платформа?   -  person peterchen    schedule 27.05.2009


Ответы (8)


Я пробовал с float вместо double и в результате получил 10,845110. Это все еще выглядит лучше, чем результат Мадалины.

РЕДАКТИРОВАТЬ:

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

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

double c = 2.7041304;

Я получаю 10.845806. Я добавляю только 0,0000004 к c! Так что я думаю, что ваши "ошибки" не являются ошибками. Выведите a, b, c и d с большей точностью, а затем поместите их в Mathematica.

person klew    schedule 30.03.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++. Как вы печатаете результат в своем коде?

person Community    schedule 30.03.2009
comment
Неважно, как вы напечатаете 10,845222072678331, оно не будет округлено или усечено до 10,8557. - person Pete Kirkham; 30.03.2009

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

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

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

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

D3DCREATE_FPU_PRESERVE
person Mykola Golubyev    schedule 30.03.2009
comment
как именно они это делают? - person ; 30.03.2009
comment
Есть варианты при инициализации директ 3д. Я не помню названия, но я потерял месяц, проверяя все формулы диплома, и только потом я сделал простую проверку с помощью sqrt(x)*sqrt(x), и точность сильно нарушилась, если только я не отключил эту опцию. - person Mykola Golubyev; 30.03.2009
comment
При компиляции со стандартным консольным приложением Win32 в VS2008 он дает правильный ответ. Я бы согласился и сказал, что это настройка компилятора. - person Binary Worrier; 30.03.2009

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

РЕДАКТИРОВАТЬ:

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

person Patrick McDonald    schedule 30.03.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].

person Pete Kirkham    schedule 30.03.2009

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

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

person xtofl    schedule 30.03.2009

Патрик кажется прав насчет (ca) основная причина:

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;
}
person Treb    schedule 30.03.2009
comment
Вы, кажется, перепутали точность (как представлено число) с точностью (каковы допуски на значения). Указываете ли вы двойное число как 2.70413 или 2.7041300000, не имеет значения, что приведет к C++ - person Pete Kirkham; 30.03.2009
comment
@Pete Kirkham: Невозможно представить значение, например. 0.1 точно в двойном, поэтому сохранение его в переменной с большей областью действия может дать разные результаты. - person Treb; 30.03.2009
comment
Как я показываю в своем ответе, вы начинаете с точностью до шести цифр, это не имеет отношения к результату, который дает код в ОП. Это имеет отношение к тому, сколько цифр этого результата вам следует заботиться, но (c-a) не является причиной ошибки, если результат вычисляется с использованием 64-битного двойного числа. - person Pete Kirkham; 30.03.2009

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

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

person IanGilham    schedule 27.05.2009
comment
Рекомендация арифметики с произвольной точностью, хотя и популярная в StackOverflow, не является лучшим ответом на все вопросы о вычислениях с плавающей запятой. - person quant_dev; 09.09.2009
comment
Это верно, но, тем не менее, зачастую это самое простое работоспособное решение. Часто есть лучшие, более быстрые и более сложные способы сделать что-то, но даже сама по себе простота имеет большую ценность в программном проекте. - person IanGilham; 19.10.2009