Безопасно ли проверять значения с плавающей запятой на равенство 0?

100

Я знаю, что обычно нельзя полагаться на равенство между значениями типа double или decimal, но мне интересно, является ли 0 особым случаем.

Хотя я могу понять неточности между 0,00000000000001 и 0,00000000000002, само значение 0 кажется довольно сложным, поскольку это просто ничего. Если вы неточны ни в чем, это уже не ничто.

Но я не очень разбираюсь в этой теме, поэтому не мне говорить.

double x = 0.0;
return (x == 0.0) ? true : false;

Всегда ли это будет верным?

Джин Робертс
источник
69
Тернарный оператор в этом коде избыточен :)
Джоэл Кохорн,
5
LOL, ты прав. Идите ко мне
Джин Робертс
Я бы не стал этого делать, потому что вы не знаете, как x стал равным нулю. Если вы все еще хотите это сделать, вы, вероятно, захотите округлить или перекрыть x, чтобы избавиться от 1e-12 или чего-то еще, что может быть помечено на конце.
Рекс Логан,

Ответы:

115

Можно с уверенностью ожидать, что сравнение вернется trueтогда и только тогда, когда переменная double имеет значение точно 0.0(что, конечно же, имеет место в исходном фрагменте кода). Это соответствует семантике ==оператора. a == bозначает « aравно b».

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

Даниэль Даранас
источник
51

Если вам нужно провести много сравнений «на равенство», было бы неплохо написать небольшую вспомогательную функцию или метод расширения в .NET 3.5 для сравнения:

public static bool AlmostEquals(this double double1, double double2, double precision)
{
    return (Math.Abs(double1 - double2) <= precision);
}

Это можно было бы использовать следующим образом:

double d1 = 10.0 * .1;
bool equals = d1.AlmostEquals(0.0, 0.0000001);
Дирк Фольмар
источник
4
У вас может быть ошибка субтрактивной отмены при сравнении double1 и double2, если эти числа имеют значения, очень близкие друг к другу. Я бы удалил Math.Abs ​​и проверял каждую ветвь индивидуально d1> = d2 - e и d1 <= d2 + e
Теодор Зографос
«Поскольку Epsilon определяет минимальное выражение положительного значения, диапазон которого близок к нулю, разница между двумя подобными значениями должна быть больше, чем у Epsilon. Как правило, она во много раз больше, чем у Epsilon. Поэтому мы рекомендуем вам не используйте Epsilon при сравнении значений Double на равенство ". - msdn.microsoft.com/en-gb/library/ya2zha7s(v=vs.110).aspx
Рафаэль Коста
15

Для вашего простого образца этот тест подходит. А как насчет этого:

bool b = ( 10.0 * .1 - 1.0 == 0.0 );

Помните, что .1 - это повторяющееся десятичное число в двоичном формате и не может быть представлено точно. Затем сравните это с этим кодом:

double d1 = 10.0 * .1; // make sure the compiler hasn't optimized the .1 issue away
bool b = ( d1 - 1.0 == 0.0 );

Я оставлю вас провести тест, чтобы увидеть фактические результаты: вы, скорее всего, так запомните.

Джоэл Кохорн
источник
5
Собственно, по какой-то причине это возвращает true (по крайней мере, в LINQPad).
Алексей Романов
О чем вы говорите ".1 проблема"?
Тиджей,
14

Из записи MSDN для Double.Equals :

Точность сравнений

Метод Equals следует использовать с осторожностью, потому что два явно эквивалентных значения могут быть неравными из-за разной точности двух значений. В следующем примере сообщается, что значение Double 0,333 и значение Double, возвращаемое путем деления 1 на 3, не равны.

...

Вместо сравнения на равенство один из рекомендуемых методов включает определение допустимой разницы между двумя значениями (например, 0,01% от одного из значений). Если абсолютное значение разницы между двумя значениями меньше или равно этому запасу, разница, вероятно, связана с различиями в точности и, следовательно, значения, вероятно, будут равны. В следующем примере этот метод используется для сравнения .33333 и 1/3, двух значений Double, которые в предыдущем примере кода оказались неравными.

Также см. Double.Epsilon .

Стю Маккеллар
источник
1
Также возможно сравнение не совсем эквивалентных значений как равных. Можно было бы ожидать, что если x.Equals(y), то (1/x).Equals(1/y), но это не тот случай, если xесть 0и yесть 1/Double.NegativeInfinity. Эти значения объявляются равными, хотя их обратные значения не совпадают.
supercat
@supercat: они эквивалентны. И у них нет взаимности. Вы можете снова запустить свой тест с помощью x = 0и y = 0, и вы все равно найдете это 1/x != 1/y.
Бен Фойгт
@BenVoigt: С xи yкак типа double? Как вы сравниваете результаты, чтобы они сообщали о неравенстве? Обратите внимание, что 1 / 0,0 не является NaN.
supercat 05
@supercat: Хорошо, это одна из тех вещей, в которых IEEE-754 ошибается. (Во-первых, это 1.0/0.0не NaN, каким должно быть, поскольку предел не уникален. Во-вторых, бесконечности сравниваются как равные друг другу, не обращая внимания на степени бесконечности)
Бен Фойгт
@BenVoigt: если ноль был результатом умножения двух очень маленьких чисел, то деление 1,0 на это должно дать значение, которое сравнивается с большим, чем любое количество маленьких чисел, имеющих одинаковый знак, и меньшее, чем любое число, если одно из малых числа имели противоположные знаки. IMHO, IEEE-754 было бы лучше, если бы у него был беззнаковый ноль, но положительные и отрицательные бесконечно малые.
supercat 05
6

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

float f = 0.1F;
bool b1 = (f == 0.1); //returns false
bool b2 = (f == 0.1F); //returns true

Проблема в том, что программист иногда забывает, что для сравнения происходит неявное приведение типа (двойное к float), что приводит к ошибке.

Йоги
источник
3

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

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

Вы также можете начать с присвоения целого числа, и простые сравнения продолжат работать, придерживаясь сложения, вычитания или умножения на целые числа (при условии, что результат меньше 24 бит для числа с плавающей запятой и 53 бита для двойного). Таким образом, вы можете рассматривать числа с плавающей запятой и двойные числа как целые при определенных контролируемых условиях.

Кевин Гейл
источник
Я согласен с вашим утверждением в целом (и поддержал его), но я считаю, что это действительно зависит от того, используется ли реализация с плавающей запятой IEEE 754 или нет. И я считаю, что каждый «современный» компьютер использует IEEE 754, по крайней мере, для хранения чисел с плавающей запятой (существуют странные правила округления, которые отличаются).
Марк Лаката
2

Нет, это не нормально. Так называемые денормализованные значения (субнормальные) при сравнении равных 0,0 будут сравниваться как ложные (ненулевые), но при использовании в уравнении будут нормализованы (станут 0,0). Таким образом, использовать это как механизм, позволяющий избежать деления на ноль, небезопасно. Вместо этого добавьте 1,0 и сравните с 1,0. Это гарантирует, что все субнормальные значения будут считаться нулевыми.


источник
Субнормальные явления также известны как денормальные
Мануэль
Субнормальные значения не становятся равными нулю при использовании, хотя они могут давать или не давать одинаковый результат в зависимости от конкретной операции.
wnoise
-2

Попробуйте это, и вы обнаружите, что == ненадежно для double / float.
double d = 0.1 + 0.2; bool b = d == 0.3;

Вот ответ от Quora.

Rickyuu
источник
-4

На самом деле, я думаю, что для сравнения двойного значения с 0,0 лучше использовать следующие коды:

double x = 0.0;
return (Math.Abs(x) < double.Epsilon) ? true : false;

То же самое для поплавка:

float x = 0.0f;
return (Math.Abs(x) < float.Epsilon) ? true : false;
David.Chu.ca
источник
5
Нет. Из документации от double.Epsilon: «Если вы создаете собственный алгоритм, который определяет, можно ли считать два числа с плавающей запятой равными, вы должны использовать значение, которое больше, чем константа Эпсилон, чтобы установить приемлемую абсолютную разницу чтобы два значения считались равными (как правило, эта разница во много раз больше, чем у Epsilon.) »
Аластер Мо
1
@AlastairMaw это относится к проверке двух двойников любого размера на равенство. Для проверки равенства нулю подойдет double.Epsilon.
jwg 03
4
Нет, это не так . Весьма вероятно, что значение, к которому вы пришли с помощью некоторых вычислений, во много раз отличается от нуля на эпсилон, но его все равно следует рассматривать как ноль. Вы не можете волшебным образом достичь целой кучи дополнительной точности в промежуточном результате откуда-то только потому, что он оказывается близким к нулю.
Alastair Maw
4
Например: (1.0 / 5.0 + 1.0 / 5.0 - 1.0 / 10.0 - 1.0 / 10.0 - 1.0 / 10.0 - 1.0 / 10.0) <double.Epsilon == false (и значительно меньше по величине: 2.78E-17 против 4.94E -324)
Alastair Maw
Итак, какая точность рекомендуется, если double.Epsilon не подходит? Будет ли 10 раз эпсилон нормально? 100 раз?
лян