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

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

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

Но почему (.1f + .2f).Equals(.3f) true в то время как (.1d+.2d).Equals(.3d) по-прежнему false?

 .1f + .2f == .3f;              // false
(.1f + .2f).Equals(.3f);        // true
(.1d + .2d).Equals(.3d);        // false
68 голосов | спросил LZW 27 FebruaryEurope/MoscowbWed, 27 Feb 2013 20:21:36 +0400000000pmWed, 27 Feb 2013 20:21:36 +040013 2013, 20:21:36

5 ответов


0

Вопрос озадачен. Давайте разберем его на множество мелких вопросов:

  

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

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

x = 1.00000 / 3.00000;

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

y = 2.00000 / 3.00000;

Вы ожидаете, что y будет 0,66667, верно? Потому что, опять же, это ближайший номер в нашей системе к ответу real . 0,66666 - это дальше на две трети, чем 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, что имеет гораздо меньшую величину ошибки, но в противоположном направлении; оно округлено вниз.

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

  

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

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

Правильно.

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

  

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

Правильно.

  

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

Не я, это точно.

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

  

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

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

  

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

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

  

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

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

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

ответил Eric Lippert 27 FebruaryEurope/MoscowbWed, 27 Feb 2013 20:54:01 +0400000000pmWed, 27 Feb 2013 20:54:01 +040013 2013, 20:54:01
0

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

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

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

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

Проверьте DEMO .

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

  

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

введите описание изображения здесь

ответил Soner Gönül 27 FebruaryEurope/MoscowbWed, 27 Feb 2013 20:35:54 +0400000000pmWed, 27 Feb 2013 20:35:54 +040013 2013, 20:35:54
0

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

  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
ответил leppie 27 FebruaryEurope/MoscowbWed, 27 Feb 2013 20:41:52 +0400000000pmWed, 27 Feb 2013 20:41:52 +040013 2013, 20:41:52
0

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

Что, как указано, возможно, зависит от компилятора /ПК

Большинство людей задаются этим вопросом с неправильной точки зрения, я думаю, до сих пор

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)));
}
ответил Valentin Kuzub 27 FebruaryEurope/MoscowbWed, 27 Feb 2013 20:44:15 +0400000000pmWed, 27 Feb 2013 20:44:15 +040013 2013, 20:44:15
0

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

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

ответил njzk2 27 FebruaryEurope/MoscowbWed, 27 Feb 2013 21:32:49 +0400000000pmWed, 27 Feb 2013 21:32:49 +040013 2013, 21:32:49

Похожие вопросы

Популярные теги

security × 330linux × 316macos × 2827 × 268performance × 244command-line × 241sql-server × 235joomla-3.x × 222java × 189c++ × 186windows × 180cisco × 168bash × 158c# × 142gmail × 139arduino-uno × 139javascript × 134ssh × 133seo × 132mysql × 132