c ++ ошибка вычитания с плавающей запятой и абсолютные значения

Насколько я понимаю, при вычитании двух чисел double с двойной точностью в С ++ они сначала преобразуются в значащую величину, начиная с единицы, умноженной на 2, в степени экспоненты. Тогда можно получить ошибку, если вычитаемые числа имеют одинаковый показатель степени и много одинаковых цифр в мантиссе, что приведет к потере точности. Чтобы проверить это для своего кода, я написал следующую безопасную функцию сложения:

double Sadd(double d1, double d2, int& report, double prec) {
    int exp1, exp2;
    double man1=frexp(d1, &exp1), man2=frexp(d2, &exp2);
    if(d1*d2<0) {
        if(exp1==exp2) {
            if(abs(man1+man2)<prec) {
                cout << "Floating point error" << endl;
                report=0;
            }
        }
    }
    return d1+d2;
}

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

Например, используя 1e-11 в качестве точности prec и вычитая следующие числа:

1) 9.8989898989898-9.8989898989897: функция сообщает об ошибке, и я получаю крайне неверное значение 9.9475983006414e-14

2) 98989898989898-98989898989897: функция сообщает об ошибке, но я получаю правильное значение 1

Очевидно, я что-то неправильно понял. Любые идеи?


person jorgen    schedule 29.04.2013    source источник
comment
Если вы хотите получить не только приблизительный результат, но и оценку ошибки вычисления с плавающей запятой, вы можете использовать интервальную арифметику, округляя вниз для нижней границы и вверх для верхней границы. Для этого должны быть пакеты C ++.   -  person Pascal Cuoq    schedule 30.04.2013


Ответы (3)


Если вычесть два почти равных значения с плавающей запятой, результат будет в основном отражать шум в младших битах. Почти равные здесь - это больше, чем один и тот же показатель степени и почти одинаковые цифры. Например, 1.0001 и 1.0000 почти равны, и их вычитание может быть обнаружено с помощью подобного теста. Но 1,0000 и 0,9999 различаются на одинаковую величину и не будут обнаружены таким тестом.

Кроме того, это небезопасная функция добавления. Скорее, это апостериорная проверка на предмет ошибок дизайна / кодирования. Если вы вычитаете два значения, которые настолько близки друг к другу, что шум имеет значение, вы ошиблись. Исправьте ошибку. Я не возражаю против использования чего-то подобного в качестве средства отладки, но, пожалуйста, назовите это тем, что подразумевает, что это то, что есть, вместо того, чтобы предполагать, что в сложении с плавающей запятой есть что-то по своей сути опасное. Кроме того, размещение проверки внутри функции сложения кажется излишним: утверждение, что два значения не вызовут проблем, с последующим простым старым добавлением с плавающей запятой, вероятно, было бы лучше. В конце концов, большинство дополнений в вашем коде не вызовут проблем, и вам лучше знать, где находятся проблемные места; ставьте утверждения в проблемных местах.

person Pete Becker    schedule 29.04.2013
comment
Если вы написали: «Если вы вычтите два приблизительных значения с плавающей запятой, которые почти равны, результат будет в основном отражать шум в младших битах», то никто не станет придраться. Как вы это написали, кто-то может указать, что вычитание двух почти равных значений с плавающей запятой является точной операцией (лемма Стербенца). - person Pascal Cuoq; 29.04.2013
comment
@PascalCuoq - Я намеренно немного запутался, потому что это написано для новичка, которому не поможет трактат по математике с плавающей запятой. - person Pete Becker; 29.04.2013
comment
@PeteBecker Спасибо! Теперь я понимаю, что мне следовало сделать это, используя вместо этого двоичные значения чисел. Согласны ли вы, что тогда тест будет работать, поскольку тогда мантисса всегда начинается с 1? По названию: вы правы, мне нужен инструмент для поиска проблем в коде; безопасным дополнением был неправильный подбор слов. - person jorgen; 29.04.2013
comment
@jorgen - есть много дискуссий о том, что я несколько пренебрежительно называю почти равным; эти темы посвящены замене == нечетким равенством, чего вы здесь не предлагаете. Но кодирование в этих потоках обычно подходит для помощи при отладке. По сути, если разница, деленная на меньшее из двух чисел, меньше некоторой предполагаемой точности, вы получите почти равное. (Да, следите за нулевыми значениями при делении). Важно то, что вы можете сделать это с помощью обычных вычислений с плавающей запятой; вам не нужно возиться с битами. - person Pete Becker; 29.04.2013
comment
@PeteBecker Да, это правда, так я могу получить функцию, которая делает то, что я хотел. Я до сих пор не понимаю, почему в этом случае 1000000000000-99999999999 и 1-0.99999999999 разные; обе сообщаются моей функцией (в зависимости от точности), но только последний действительно дает неправильный ответ. - person jorgen; 29.04.2013
comment
@jorgen: Если вы хотите проверить, могла ли относительная ошибка увеличиться из-за потери абсолютного значения, вы можете просто проверить, сравнив величину результата с величиной любого из входных данных. Если результат намного ниже по величине, чем входной, то диапазон потенциальной относительной ошибки в нем намного больше, чем во входных данных (если только ошибки в двух входных данных не сильно коррелированы по какой-либо причине). Например, r = d1+d2; if (fabs(r) < 0x1p-20 * fabs(d1)) std::cout << "Relative error may have increased by 0x1p20.\n";. - person Eric Postpischil; 29.04.2013
comment
@jorgen: Кроме того, из предыдущего ясно, что эта проблема не имеет ничего общего с числами с плавающей запятой. Это чистая математика. Если у вас есть два значения, d1 и d2, которые могут содержать какую-либо ошибку (ошибка вычисления, ошибка измерения, ошибка выборки), и вы вычисляете r = d1 + d2, и r намного меньше по величине, чем d1 или d2, тогда относительная ошибка в r намного больше, чем относительная ошибка в d1 или d2. Например, если d1 равно 100 с ошибкой 1% и d2 равно -100 с ошибкой 1%, то r принимает значение от -2 до 2 с потенциально бесконечной относительной ошибкой. Никаких операций с плавающей запятой. - person Eric Postpischil; 29.04.2013
comment
@EricPostpischil Спасибо! Да, вы правы, я путаю термины; Я говорю не об ошибке с плавающей точкой, а о том, что вы описываете. Однако я все еще в замешательстве. учтите это: при вычислении 10000000000000.0-999999999999.0 и 1-0.999999999999 ответ, разделенный на ввод, будет одинаковым (10 ^ -13), но все же последний получает ошибку при вычислении, а первый - нет. - person jorgen; 29.04.2013
comment
@jorgen: Если вы спрашиваете, почему код в вашем вопросе ведет себя по-разному для этих двух ситуаций, обратите внимание, что 10000000000000.0 и 999999999999.0 имеют одинаковый показатель степени двойки (39 и 39), а 1 и 0,999999999999 - нет (0 и -1 ). Итак, первые попадают на путь if (exp1 == exp2), а вторые - нет. - person Eric Postpischil; 29.04.2013
comment
@EricPostpischil Хорошо, понятно! Итак, в конце концов, я могу сравнить относительный размер ответа с входными данными, чтобы оценить опасность ошибки, но на самом деле этого может и не произойти (ложная тревога)? - person jorgen; 29.04.2013
comment
@jorgen: Да. Обычно человек, разрабатывающий код, знает, что ошибка может достигать некоторой суммы, но не знает ее фактического значения. Ошибки могут сокращаться или быть небольшими, даже если они могут быть большими. Знание того, что результат сложения меньше, чем его операнды, только говорит вам, что существует вероятность гораздо большей относительной ошибки; он не сообщает вам, в чем заключается настоящая ошибка. - person Eric Postpischil; 29.04.2013

+1 к ответу Пита Беккера.

Обратите внимание, что проблема вырожденного результата также может возникнуть при exp1! = Exp2

Например, если вычесть

1.0-0.99999999999999

So,

bool degenerated =
       (epx1==exp2   && abs(d1+d2)<prec)
    || (epx1==exp2-1 && abs(d1+2*d2)<prec)
    || (epx1==exp2+1 && abs(2*d1+d2)<prec);

Вы можете опустить проверку для d1 * d2 ‹0 или оставить ее, чтобы избежать всего теста, иначе ...

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

person aka.nice    schedule 30.04.2013
comment
Хорошо, я вижу! Да, d1 * d2 есть, поэтому мне не нужно знать знаки в сложном коде, но я должен поставить его в начало, чтобы в противном случае избежать всего. - person jorgen; 30.04.2013

Довольно легко доказать, что для арифметики с плавающей запятой IEEE 754, если x / 2 ‹= y‹ = 2x, то вычисление x - y является точной операцией и даст точный результат правильно без какой-либо ошибки округления.

И если результатом сложения или вычитания является денормализованное число, то результат будет всегда точным.

person gnasher729    schedule 21.06.2014