(.1f +.2f ==. 3f)!= (.1f +.2f).Equals(.3f) Почему?

Мой вопрос не о плавающей точности. Речь идет о том, почему Equals() отличается от ==.

Я понимаю, почему .1f + .2f == .3f есть false (while .1m + .2m == .3m is true).
Я получаю, что == является ссылкой и .Equals() является сопоставлением значений. (Edit: Я знаю, что для этого есть еще больше.)

Но почему (.1f + .2f).Equals(.3f) true, а (.1d+.2d).Equals(.3d) все еще false?

 .1f + .2f == .3f;              // false
(.1f + .2f).Equals(.3f);        // true
(.1d + .2d).Equals(.3d);        // false

Ответ 1

Вопрос смехотворно сформулирован. Позвольте разбить его на многие более мелкие вопросы:

Почему одна десятая плюс две десятых не всегда равна трем десятым в арифметике с плавающей запятой?

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

x = 1.00000 / 3.00000;

Вы ожидали бы, что x будет 0.33333, верно? Потому что это самое близкое число в нашей системе к реальному ответу. Теперь предположим, что вы сказали

y = 2.00000 / 3.00000;

Вы ожидаете, что y будет 0.66667, верно? Потому что снова это самое близкое число в нашей системе к реальному ответу. 0,666666 находится дальше от двух третей, чем 0,666667.

Обратите внимание, что в первом случае мы округлились, а во втором случае округлились.

Теперь, когда мы говорим

q = x + x + x + x;
r = y + x + x;
s = y + y;

что мы получим? Если бы мы сделали точную арифметику, то каждая из них, очевидно, составляла бы четыре трети, и все они были бы равны. Но они не равны. Хотя 1.33333 является самым близким числом в нашей системе до четырех третей, только r имеет это значение.

q равно 1.33332 - потому что x был немного мал, каждое добавление накапливало эту ошибку, а конечный результат довольно мал. Аналогично, s слишком велик; это 1.33334, потому что y было немного слишком большим. r получает правильный ответ, потому что слишком большая значимость y отменяется слишком малой величиной x, и результат заканчивается правильно.

Число точек точности влияет на величину и направление ошибки?

Да; более высокая точность уменьшает величину ошибки, но может изменить, вызывает ли вычисление убыток или коэффициент усиления из-за ошибки. Например:

b = 4.00000 / 7.00000;

b будет 0,57143, что округляется от истинного значения 0,571428571... Если бы мы отправились в восемь мест, которые были бы 0,57142857, что имеет гораздо меньшую величину ошибки, но в противоположном направлении; он округлен.

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

Мы ожидаем, что выполнение вычисления с большей точностью всегда дает ответ ближе к истинному ответу, но этот аргумент показывает обратное. Это объясняет, почему иногда вычисление в поплавках дает "правильный" ответ, но вычисление в двойниках, которые имеют в два раза точную точность, дает "неправильный" ответ, правильно?

Да, это именно то, что происходит в ваших примерах, за исключением того, что вместо пяти цифр десятичной точности у нас есть определенное количество цифр двоичной точности. Точно так же, как одна треть не может быть точно представлена ​​в пять - или любое конечное число - десятичных цифр, 0,1, 0,2 и 0,3 не может быть точно представлена ​​в любом конечном числе двоичных цифр. Некоторые из них будут округлены, некоторые из них будут округлены, и добавляются ли их дополнения к ошибке или отменены ошибка зависит от конкретных деталей количества двоичных цифр в каждой системы. То есть, изменения в точности могут изменить ответ лучше или хуже. Как правило, чем выше точность, тем ближе ответ к истинному ответу, но не всегда.

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

Если вам нужна точная десятичная математика, используйте тип decimal; он использует десятичные дроби, а не двоичные дроби. Цена, которую вы платите, заключается в том, что она значительно больше и медленнее. И, конечно, как мы уже видели, фракции, такие как одна треть или четыре седьмых, не будут представлены точно. Любая фракция, которая на самом деле является десятичной дробью, однако, будет представлена ​​нулевой ошибкой, примерно до 29 значащих цифр.

ОК, я согласен с тем, что все схемы с плавающей точкой вводят неточности из-за ошибки представления и что эти неточности могут иногда накапливаться или отменяться друг от друга на основе количества бит точности, используемой при вычислении. У нас есть хотя бы гарантия того, что эти неточности будут согласованы?

Нет, у вас нет такой гарантии для поплавков или парных. Компилятору и среде выполнения разрешено выполнять вычисления с плавающей запятой с большей точностью, чем это требуется спецификацией. В частности, компилятору и среде выполнения разрешено выполнять арифметику одноточечной (32-разрядной) в 64-битной или 80-битной или 128-битной или любой битте больше 32, как им нравится.

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

Таким образом, это означает, что вычисления, выполненные во время компиляции, например, литералы 0.1 + 0.2, могут давать разные результаты, чем те же вычисления, выполненные во время выполнения с переменными?

Да.

Как сравнить результаты 0.1 + 0.2 == 0.3 с (0.1 + 0.2).Equals(0.3)?

Поскольку первый из них вычисляется компилятором, а второй вычисляется по времени выполнения, и я просто сказал, что им разрешено произвольно использовать более высокую точность, чем это требуется спецификацией по их прихоти, да, это может дать разные Результаты. Возможно, один из них предпочитает делать вычисления только с точностью до 64 бит, тогда как другой выбирает 80-битную или 128-битную точность для частичного или всего вычисления и получает разный ответ.

Итак, держи минутку здесь. Вы говорите не только о том, что 0.1 + 0.2 == 0.3 может отличаться от (0.1 + 0.2).Equals(0.3). Вы говорите, что 0.1 + 0.2 == 0.3 может быть вычислен как истинное или ложное полностью по прихоти компилятора. Это может привести к истине по вторникам и ложным по четвергам, это может привести к истине на одной машине и ложной по другой, это может привести к истинным и ложным, если выражение появляется дважды в одной программе. Это выражение может иметь значение по любой причине; компилятор может быть полностью ненадежным здесь.

Правильно.

Как обычно сообщается команде компилятора С#, у кого-то есть выражение, которое производит true, когда они компилируются в debug и false при компиляции в режиме деблокирования. Это наиболее распространенная ситуация, в которой это происходит, потому что генерация кода отладки и выпуска изменяет схемы распределения регистров. Но компилятору разрешено делать все, что ему нравится, с этим выражением, если оно выбирает true или false. (Это не может, например, создать ошибку времени компиляции.)

Это сумасшествие.

Правильно.

Кому я должен обвинять этот беспорядок?

Не я, это верно для штопа.

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

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

Используйте тип decimal, как я уже говорил. Или сделайте всю свою математику целыми числами.

Мне нужно использовать удвоения или поплавки; могу ли я сделать что угодно, чтобы поощрять последовательные результаты?

Да. Если вы сохраняете какой-либо результат в любом статическом поле, любое поле экземпляра элемента класса или массива типа float или double, то гарантируется, что он будет усечен до 32 или 64-битной точности. (Эта гарантия явно не предназначена для магазинов местным жителям или формальным параметрам.) Также, если вы выполняете время выполнения в (float) или (double) в выражении, которое уже относится к этому типу, тогда компилятор выдает специальный код, который заставляет результат усекает, как если бы он был назначен элементу поля или массива. (Каста, которые выполняются во время компиляции, т.е. Отбрасываются на постоянные выражения), не гарантируется.)

Чтобы прояснить этот последний момент: предоставляет ли спецификация языка С# эти гарантии?

Нет. Среда выполнения гарантирует сохранение в массиве или усечении поля. Спецификация С# не гарантирует, что листинг данных усекается, но реализация Microsoft имеет регрессионные тесты, гарантирующие, что каждая новая версия компилятора имеет такое поведение.

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

Ответ 2

Когда вы пишете

double a = 0.1d;
double b = 0.2d;
double c = 0.3d;

Собственно, это не точно 0.1, 0.2 и 0.3. Из кода IL;

  IL_0001:  ldc.r8     0.10000000000000001
  IL_000a:  stloc.0
  IL_000b:  ldc.r8     0.20000000000000001
  IL_0014:  stloc.1
  IL_0015:  ldc.r8     0.29999999999999999

В SO есть один вопрос, указывающий на то, что проблема (Разница между десятичной, плавающей и двойной в .NET? и Работа с ошибками с плавающей запятой в .NET), но я предлагаю вам прочитать классную статью:

What Every Computer Scientist Should Know About Floating-Point Arithmetic

Хорошо, что leppie сказал более логично. Реальная ситуация здесь, totaly зависит от compiler/computer или cpu.

На основе кода leppie этот код работает на моей Visual Studio 2010 и Linqpad, в результате True/False, но когда я попробовал его на ideone.com, результат будет True/True

Отметьте DEMO.

Совет: Когда я написал Console.WriteLine(.1f + .2f == .3f); Предупреждения Resharper me;

Сравнение числа с плавающей запятой с оператором равенства. Возможное потеря точности при значениях округления.

enter image description here

Ответ 3

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

  var f1 = .1f + .2f;
  var f2 = .3f;
  Console.WriteLine(f1 == f2); // prints true (same as Equals)
  Console.WriteLine(.1f+.2f==.3f); // prints false (acts the same as double)

@Caramiriel также указывает, что .1f+.2f==.3f испускается как false в IL, поэтому компилятор выполнил расчет во время компиляции.

Чтобы подтвердить постоянную оптимизацию компилятора сгибания/распространения

  const float f1 = .1f + .2f;
  const float f2 = .3f;
  Console.WriteLine(f1 == f2); // prints false

Ответ 4

FWIW после прохождения тестов

float x = 0.1f + 0.2f;
float result = 0.3f;
bool isTrue = x.Equals(result);
bool isTrue2 = x == result;
Assert.IsTrue(isTrue);
Assert.IsTrue(isTrue2);

Итак, проблема в том, что эта строка

0.1f + 0.2f == 0.3f

Что, как указано, вероятно, является компилятором /pc specific

Большинство людей прыгают на этот вопрос из-за неправильного угла. Я думаю, что до сих пор

UPDATE:

Еще один любопытный тест, который я считаю

const float f1 = .1f + .2f;
const float f2 = .3f;
Assert.AreEqual(f1, f2); passes
Assert.IsTrue(f1==f2); doesnt pass

Реализация единого равенства:

public bool Equals(float obj)
{
    return ((obj == this) || (IsNaN(obj) && IsNaN(this)));
}

Ответ 5

== заключается в сравнении точных значений поплавков.

Equals - логический метод, который может возвращать true или false. Конкретная реализация может различаться.

Ответ 6

Я не знаю почему, но в настоящее время некоторые мои результаты отличаются от ваших. Обратите внимание, что третий и четвертый тесты противоречат проблеме, поэтому некоторые части ваших объяснений могут быть неверными.

using System;

class Test
{
    static void Main()
    {
        float a = .1f + .2f;
        float b = .3f;
        Console.WriteLine(a == b);                 // true
        Console.WriteLine(a.Equals(b));            // true
        Console.WriteLine(.1f + .2f == .3f);       // true
        Console.WriteLine((1f + .2f).Equals(.3f)); //false
        Console.WriteLine(.1d + .2d == .3d);       //false
        Console.WriteLine((1d + .2d).Equals(.3d)); //false
    }
}