Косяки с арифметикой?

 
Вопрос разработчикам.

Берем два нормализованных числа:
A = NormalizeDouble(1.1115,4);
B = NormalizeDouble(1.1114,4);

Почему выражение A-B == NormalizeDouble(A-B,4) возвращает false?
Выражения A-B == 0.0001 или A-B-0.0001 == 0 также возвращают false,
а выражение A == B+0.0001 - true.


P.S. MetaTrader4, Build 169
 
Все работает верно (0.0001 после расчетов может быть не равен 0.0001), хотя в это сложно поверить. И в C/C++ именно так же работает. Нельзя так сравнивать вещественные числа. Это концептуальная проблема потери точности вычислений на вещественных числах и зависит от их представления и реализации в железе (процессорах).

Пример (из Lint от Gimpel Software):
#include <iostream.h>
2
3     int main()
4         {
5         const double three = 3.0;
6         double x, y, z;
7         x = 1 / three;
8         y = 4 / three;
9         z = 5 / three;
10        if( x + y == z )
11            cout << "1/3 + 4/3 == 5/3 \n";
12        else
13            cout << "1/3 + 4/3 != 5/3 \n";
14        return 0;
15        }


bug777.cpp(15) : Info 777: Testing floats for equality

Testing float's for equality -- This message is issued when the operands of operators == and != are some form of floating type (float, double, or long double). Testing for equality between two floating point quantities is suspect because of round-off error and the lack of perfect representation of fractions. If your numerical algorithm calls for such testing turn the message off. The message is suppressed when one of the operands can be represented exactly, such as 0 or 13.5.


Решение - после матопераций проводить нормализацию, а уже потом сравнивать.

P.S. MetaTrader4, Build 169

Рекомендую проапгрейдиться до 173 билда, особенно, если используете MQL4.

 
Решение - после матопераций проводить нормализацию, а уже потом сравнивать.

И это поможет в 100% случаев? Более формально, если в десятичной арифметике некоторое выражение дает в результате X (истинный результат), а в двоичной это же выражение дает Y, то всегда ли X == Normalize(Y, Digits)?

Я эту же проблему обхожу тем, что вместо
X==Y

использую

MathAbs(X-Y) < Point

, хотя использование нормализации, если она действительно работает в 100% случаев, было бы более удобным.

 
И это поможет в 100% случаев?

Во всяком случае, в тестовом примере на C++ после использования нормализации сравнение стало срабатывать корректно. Естественно, речь идет об вещественных числах (double), а не о целочисленных (тут ошибок не может быть).
 
Все работает верно (0.0001 после расчетов может быть не равен 0.0001), хотя в это сложно поверить.


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


Вот Вам еще пример:

A = NormalizeDouble(2.2230,4);
B = NormalizeDouble(1.1115,4);

A-B == 1.1115 возвращает true как и должно быть.


A = NormalizeDouble(1.2230,4);
B = NormalizeDouble(1.1115,4);

A-B == 0.1115 возвращает false !!!!!!

Ошибка в простейшей арифметике !!!


Решение - после матопераций проводить нормализацию, а уже потом сравнивать.


Да, это спасает от приведенной выше ошибки, но Вы же понимаете, что 99% процентов кодописателей этого не делают, не потому что они неграмотные и не знают языка С, а потому что в этом нет никакой концептуальной необходимости.
 
В это действительно сложно поверить.

Именно про это я и говорю - поверить сложно.
Но надо поверить, проведя собственные исследования.

Как при сложении и вычитании нормализованных чисел могут появиться ошибки округления?

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

Ваш приведенный пример:
Берем два нормализованных числа:
A = NormalizeDouble(1.1115,4);
B = NormalizeDouble(1.1114,4);

Почему выражение A-B == NormalizeDouble(A-B,4) возвращает false?
Выражения A-B == 0.0001 или A-B-0.0001 == 0 также возвращают false,
а выражение A == B+0.0001 - true.

Точно так же глючит и в С/C++. Я сталкивался с этим много раз, но и в данном случае специально проверил это лично, запустив Ваш пример в С++. Если бы это была наша ошибка - мы бы тут же это признали и сегодня же выпустили патч.

99% процентов кодописателей этого не делают, не потому что они неграмотные и не знают языка С

Они этого не делают, так как не знают об особенностях операций с вещественными числами.

Любой, кто сталкивается в серьезной работе с вещественными числами, натыкается на эту проблему. мучается, ищет ошибки у себя, у других, а потом понимает суть и начинает использовать безопасные методы. Один из методов - не использовать чистое сравнение на равенство/неравенство, а использовать операции больше или меньше. И нормализация в критических местах.
 
Еще как могут - и Вы это как раз показали своим примером. Вопрос "как это может появиться?" является теорией, а результат "математические(именно математические, а не только деление/умножение) операции с вещественными числами дают погрешности" - является суровой правдой жизни.

Погрешность для +/- с плавающей арифметикой будет +/- 1-2 эпсилон (или +/- 1-2 эпсилон * Макс(А,В)). Причем чем больше вычислений, тем больше накапливается ошибка.

Эпсилон - это примерно значение младшего разряда мантисы числа.
В языках даже такая константа бывает (в Си кажется есть).
Для double она примерно равна 10 в минус 14 степени.

Причин как минимум 2.

1. Вещественные числа в компе всегда представляются в виде дробного числа.
Т.е. в виде М * 2^Р, где М - мантиса, |M| < 1.0, а Р - порядок, целое со знаком.
Таким способом нельзя точно представить число, все что не влазит в мантису отбрасывается.

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

В результате может оказаться что 1.1 + 2.2 не равно 3.3
Мы записали в десятичной системе, а в компе они представляются в двоичной системе и в нейже выполняются операции.

Это общая проблема всех компов и всех языков.

Проверять вещественные числа на равенство всегда некорректно (за очень редким исключением).

Равенство можно проверить только с некоторой точностью.
И сделать это можно только так
MathAbs(X-Y) < precision


Для примера в начале ветки precision <= 0.0001

 
Собственно вы правы, господа. Однако, эта ловушка так и тянет, чтобы в нее вляпаться. Особенно, когда все данные отображаются в информационных окнах с точностью до MODE_DIGITS, значение double можно вывести с точностью до 8-го знака после запятой, а на самом деле, внутреннее представление double - 8-ми байтовый float. Т.е. при отображении данных мы видим сплошные округления, а реально значения отличаются где-нибудь в 15-ом знаке после запятой. Может было бы лучше, если бы существовал специальный тип данных (типа currency или money как в некоторых языках и базах данных), хотя я не уверен.
 
Может было бы лучше, если бы существовал специальный тип данных (типа currency или money как в некоторых языках и базах данных), хотя я не уверен.

Это уже предлагали,
и меня за эти предложения даже забанили на 2 недели :))
(с диагнозом, что я ничего не смыслю в арифметике на компе :))

Ну что сказать - Коней на переправе не меняют.
 
Может было бы лучше, если бы существовал специальный тип данных (типа currency или money как в некоторых языках и базах данных), хотя я не уверен.


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