Почему проанализированный двойник не равен инициализированному двойнику, предположительно того же значения?

Когда я выполняю эту строку:

double dParsed = double.Parse("0.00000002036");

dParsed на самом деле получает значение: 0.000000020360000000000002

По сравнению с этой строкой

double dInitialized = 0.00000002036;

в этом случае значение dInitialized равно 0,00000002036.

Вот они в отладчике: Разница между double.Parse и инициализатором

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

[Subject("parsing doubles")]
public class when_parsing_crazy_doubles
    {
    static double dInitialized = 0.00000002036;
    static double dParsed;
    Because of = () => dParsed = double.Parse("0.00000002036");
    It should_match = () =>  dParsed.ShouldBeLike(dInitialized);
    }

Это, конечно, терпит неудачу с:

Machine.Specifications.SpecificationException
"":
  Expected: [2.036E-08]
  But was:  [2.036E-08]

В моем производственном коде «проанализированные» двойники считываются из файла данных, тогда как значения сравнения жестко запрограммированы как инициализаторы объектов. Из многих сотен записей 4 или 5 из них не совпадают. Исходные данные отображаются в текстовом файле следующим образом:

0.00000002036 0.90908165072 6256.77753019160

Таким образом, анализируемые значения имеют только 11 знаков после запятой. Есть идеи, как обойти это несоответствие?

Хотя я согласен с тем, что сравнивать двойные числа на равенство рискованно, я удивлен, что компилятор может получить точное представление, когда текст используется в качестве инициализатора объекта, но этот double.Parse не может получить точное представление при разборе точно такого же текст. Как я могу ограничить анализируемые двойники до 11 знаков после запятой?


person Tim Long    schedule 31.01.2014    source источник
comment
Note that 0.00000002036 is really 2.035999999999999834810563976821018439267163557815365493297576904296875e-08 and 0.000000020360000000000002 is really 2.0360000000000001656828089980320883878306403858005069196224212646484375e-08. Разница между ними ровно 2^(-78).   -  person dan04    schedule 31.01.2014
comment
Другими словами, представление компилятора не является точным. Тим Лонг, обратите внимание, как близко число 2.036e-8 находится ровно посередине этих двух чисел, напечатанных dan04. Первое число чуть-чуть ближе к 2,036e-8, чем второе значение. Алгоритм преобразования вашего компилятора дал правильный результат, где правильный означает в пределах половины ULP. Скомпилированное двоичное представление (чуть-чуть) находится в пределах половины ULP от машины с бесконечной точностью. Анализатор не совсем понял это правильно; проанализированное представление (чуть-чуть) больше половины ULP от значения бесконечной точности.   -  person David Hammen    schedule 31.01.2014
comment
По-видимому, компилятор и парсер используют две разные реализации. для любопытных есть отличный сайт по числам с плавающей запятой: floating-point-gui.de   -  person Steve    schedule 31.01.2014
comment
Это означает, что ваш компилятор и входная библиотека используют разные алгоритмы синтаксического анализа. Числа, которые находятся на пороге округления в большую или меньшую сторону, всегда будут немного проблематичными. Цель состоит в том, чтобы ошибка составляла половину ULP или меньше. Это цель. Реальность такова, что эта цель не всегда достигается. Всегда будут неприятные угловые случаи, когда преобразование ошибается, но, надеюсь, лишь немного.   -  person David Hammen    schedule 31.01.2014


Ответы (1)


По сравнению с этой строкой

double dInitialized = 0.00000002036;

в этом случае значение dInitialized равно 0,00000002036.


Если у вас есть что-то, отдаленно напоминающее обычный компьютер, dInitialized не инициализируется ровно 0,00000002036. Этого не может быть, потому что число 0,00000002036 по основанию 10 не имеет конечного представления по основанию 2.

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

Получить правильный размер этого эпсилона немного сложно. Если ваши два числа оба малы (например, меньше единицы), вполне может подойти эпсилон 1e-15. Если числа большие (например, больше десяти), это малое значение эпсилон эквивалентно проверке на равенство.


Изменить: я не ответил на вопрос.

Как я могу ограничить анализируемые двойники до 11 знаков после запятой?

Если вам не нужно беспокоиться об очень малых значениях,

static double epsilon = 1e-11;
if (Math.Abs(dParsed-dInitialized) > epsilon*Math.Abs(dInitialized)) {
    noteTestAsFailed();
}

Вы должны быть в состоянии безопасно изменить это epsilon на 4e-16.


Правка №2. Почему компилятор и double.Parse создают разные внутренние представления для одного и того же текста?

Это вроде очевидно, не так ли? Компилятор и double.Parse используют разные алгоритмы. Рассматриваемое число 0,00000002036 очень близко к порогу того, следует ли использовать округление в большую или меньшую сторону, чтобы получить представимое значение, которое находится в пределах половины ULP от желаемого значения (0,00000002036). «Правильное» значение — это то, которое находится в пределах половины ULP от желаемого значения. В этом случае компилятор принимает правильное решение о выборе округленного значения, в то время как синтаксический анализатор принимает неправильное решение о выборе округленного значения.

Значение 0,00000002036 — неприятный угловой случай. Это не точно представимая величина. Два ближайших значения, которые могут быть точно представлены как двойники IEEE, это 6153432421838462/2^78 и 6153432421838463/2^78. Значение посередине между этими двумя — 12306864843676925/2^79, что очень и очень близко к 0,00000002036. Вот что делает это угловым случаем. Я подозреваю, что все найденные вами значения, в которых скомпилированное значение не тождественно равно значению из double.Parse, являются крайними случаями, случаями, когда желаемое значение находится почти посередине между двумя ближайшими точно представляемыми значениями.


Изменить №3:

Вот несколько различных способов интерпретации 0,00000002036:

  • 2/1e8 + 3/1e10 + 6/1e11
  • 2*1e-8 + 3*1e-10 + 6*1e-11
  • 2.036 * 1e-8
  • 2.036 / 1e8
  • 2036 * 1e-11
  • 2036 / 1e11

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

person David Hammen    schedule 31.01.2014
comment
Обратите внимание: хотя C# определяет double.Epsilon, в документации C# отмечается, что его использование для сравнения двойных чисел на равенство не рекомендуется. double.Epsilon — это наименьшее положительное двойное значение, поэтому оно полезно только со значениями, которые очень близки к нулю. - person Brian S; 31.01.2014
comment
@ Дэвид, я ожидал, что кто-то даст такой ответ, но суть моего вопроса не в этом. Ясно, что числа не равны, потому что они разные. Мой вопрос: почему компилятор и double.Parse создают разные внутренние представления для одного и того же текста? Я обновлю свой вопрос снимком экрана из отладчика, показывающим фактические значения. - person Tim Long; 31.01.2014
comment
Какого черта Microsoft дала имя Epsilon чему-то, что не имеет ничего общего с машинным эпсилоном (2.220446049250313e-16)? Это все равно, что определить Math.PI как 2,718281828459045. - person dan04; 31.01.2014
comment
MSpec имеет x.ShouldBeCloseTo(value, epsilon) для утверждения чисел с плавающей запятой. - person Alexander Groß; 01.02.2014