(.1f+.2f==.3f)!= (.1f+.2f).Равняется.(3Ф) почему?


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

Я понимаю, почему .1f + .2f == .3f - это false.1m + .2m == .3m и true).
Я понимаю, что == ссылка и .Equals() сравнение стоимости. (Edit

5 68

5 ответов:

вопрос неправильно сформулирован. Давайте разберем его на множество более мелких вопросов:

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

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

x = 1.00000 / 3.00000;

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

y = 2.00000 / 3.00000;

вы ожидаете, что y будет 0.66667, верно? Потому что, опять же, это ближе номер в нашей системе к реальные ответ. 0.666666 - это дальше от двух третей чем 0.66667.

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

теперь, когда мы скажи

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

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

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

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

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

b = 4.00000 / 7.00000;

b будет 0.57143, который округляется от истинного значения 0.571428571... Если бы мы отправились в восемь мест, это было бы 0.57142857, что имеет гораздо меньшую величину ошибки, но в противоположном направлении; он округлился вниз.

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

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

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

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

Если вам требуется точная десятичная математика, то используйте 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 может быть вычислено, чтобы быть истинным или совершенно ложным по прихоти компилятора. Он может производить истину по вторникам и ложь по четвергам, он может производить истину на одной машине и ложь на другой, он может производить как истину и false, если выражение появилось дважды в одной и той же программы. Это выражение может иметь любое значение по любой причине; компилятору разрешено быть полностью здесь ненадежные.

правильно.

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

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

правильно.

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

не я, это для проклятых конечно.

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

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

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

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

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

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

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

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

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

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

есть лоф вопроса в так указывая, что вопрос, как (разница между decimal, float и double в .NET? и работа с ошибками с плавающей запятой в .NET) но я предлагаю вам прочитать классную статью звонил;

What Every Computer Scientist Should Know About Floating-Point Arithmetic

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

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

Регистрация демо.

Совет: когда я писал Console.WriteLine(.1f + .2f == .3f); Решарпер предупреждает меня;

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

enter image description here

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

  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

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.1 f + 0.2 f= = 0.3 f

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

большинство людей прыгают на этот вопрос с неправильного угла я думаю, что до сих пор

обновление:

еще один любопытный тест, я думаю,

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)));
}

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

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