Можно ли получить 0, вычитая два неравных числа с плавающей запятой?

131

Можно ли в следующем примере получить деление на 0 (или бесконечность)?

public double calculation(double a, double b)
{
     if (a == b)
     {
         return 0;
     }
     else
     {
         return 2 / (a - b);
     }
}

В обычных случаях, конечно, не будет. Но что, если aи bочень близки, может(a-b) быть 0связано с точностью расчета?

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

Thirler
источник
49
Придется
3
@Thirler для меня похоже на время использовать JUnit Testing!
Мэтт Кларк,
7
@bluebrain, я предполагаю, что ваше буквальное число 2.000 и т. д. содержит много десятичных знаков, которые будут представлены поплавком. Таким образом, последние не будут представлены фактическим использованным числом при сравнении.
Thirler
4
@Thirler наверное. «вы не можете действительно гарантировать, что число, которое вы присваиваете float или double, является точным»
Guness
4
Просто обратите внимание, что возврат 0 в этом случае может привести к неоднозначности, которую трудно отладить, поэтому убедитесь, что вы действительно хотите вернуть 0 вместо того, чтобы генерировать исключение или возвращать NaN.
m0skit0

Ответы:

132

В Java a - bникогда не бывает равным 0if a != b. Это связано с тем, что Java требует операций с плавающей запятой IEEE 754, которые поддерживают денормализованные числа. Из спецификации :

В частности, язык программирования Java требует поддержки денормализованных чисел с плавающей запятой IEEE 754 и постепенного потери значимости, что упрощает доказательство желаемых свойств конкретных численных алгоритмов. Операции с плавающей точкой не сбрасываются в ноль, если результат вычисления является денормализованным числом.

Если FPU работает с денормализованными числами , вычитание неравных чисел никогда не может привести к нулю (в отличие от умножения), также см. Этот вопрос .

Для других языков это зависит. Например, в C или C ++ поддержка IEEE 754 не является обязательной.

Тем не менее, это возможно для выражения 2 / (a - b)к переполнению, например , с a = 5e-308и b = 4e-308.

nwellnhof
источник
4
Однако OP хочет знать о 2 / (ab). Можно ли гарантировать, что это будет конечным?
Taemyr
Спасибо за ответ, я добавил ссылку на википедию для объяснения денормализованных чисел.
Thirler
3
@Taemyr См. Мою правку. Деление действительно может переполниться.
nwellnhof
@Taemyr (a,b) = (3,1)=> 2/(a-b) = 2/(3-1) = 2/2 = 1Верно ли это с плавающей запятой IEEE, я не знаю
Коул Джонсон,
1
@DrewDormann IEEE 754 также является необязательным для C99. См. Приложение F стандарта.
nwellnhof
50

Как насчет следующего?

public double calculation(double a, double b) {
     double c = a - b;
     if (c == 0)
     {
         return 0;
     }
     else
     {
         return 2 / c;
     }
}

Таким образом, вы не зависите от поддержки IEEE на любом языке.

malarres
источник
6
Избегайте проблемы и сразу упростите тест. Мне нравится.
Джошуа,
11
-1 Если a=b, не надо возвращаться 0. Деление на 0в IEEE 754 дает вам бесконечность, а не исключение. Вы избегаете проблемы, поэтому возвращение 0- это ожидаемая ошибка. Посмотрим 1/x + 1. Если x=0это приведет 1к неправильному значению: бесконечность.
Коул Джонсон,
5
@ColeJohnson правильный ответ также не бесконечен (если вы не укажете, с какой стороны исходит предел, правая сторона = + inf, левая сторона = -inf, unspecified = undefined или NaN).
Nick T
12
@ChrisHayes: Это действительный ответ на вопрос, признающий, что вопрос может быть проблемой XY: meta.stackexchange.com/questions/66377/what-is-the-xy-problem
slebetman
17
@ColeJohnson Возвращение 0- не проблема. Это то, что делает OP в вопросе. Вы можете поместить исключение или что-то другое, подходящее для ситуации в этой части блока. Если вам не нравится возвращаться 0, это должна быть критика вопроса. Конечно, выполнение того, что сделал OP, не гарантирует отрицательного ответа. Этот вопрос не имеет ничего общего с дальнейшими вычислениями после завершения данной функции. Насколько вам известно, требования программы требуют возврата 0.
jpmc26,
25

Вы не получите деления на ноль независимо от значения a - b , поскольку деление с плавающей запятой на 0 не вызывает исключения. Возвращает бесконечность.

Теперь единственный способ a == bвернуть истину - это если aи bсодержать точно такие же биты. Если они отличаются только младшим битом, разница между ними не будет равна 0.

РЕДАКТИРОВАТЬ :

Как правильно прокомментировала Вирсавия, есть некоторые исключения:

  1. «Не число сравнивает» ложь с собой, но будет иметь идентичные битовые комбинации.

  2. -0.0 определен для сравнения истины с +0.0, и их битовые шаблоны отличаются.

Таким образом, если оба aи bесть Double.NaN, вы достигнете предложения else, но, поскольку NaN - NaNтакже возвращает NaN, вы не будете делить на ноль.

Эран
источник
11
Эран; не совсем верно. «Не число сравнивает» ложь с самим собой, но будет иметь идентичные битовые комбинации. Также -0.0 определено для сравнения истины с +0.0, и их битовые шаблоны отличаются.
Вирсавия,
1
@Bathsheba Я не рассматривал эти особые случаи. Спасибо за комментарий.
Эран
2
@Eran, очень хорошая точка зрения, что деление на 0 вернет бесконечность с плавающей запятой. Добавил в вопрос.
Thirler
2
@Prashant, но в этом случае деление не произойдет, так как a == b вернет true.
Эран
3
На самом деле вы можете получить исключение FP для деления на ноль, это опция, определенная стандартом IEEE-754, хотя, вероятно, это не то, что большинство людей имели бы в виду под словом «исключение»;)
Voo
17

Здесь не может быть деления на ноль.

SMT Solver Z3 поддерживает точную IEEE арифметику с плавающей точкой. Давайте попросим Z3 найти числа aи bтакие, что a != b && (a - b) == 0:

(set-info :status unknown)
(set-logic QF_FP)
(declare-fun b () (FloatingPoint 8 24))
(declare-fun a () (FloatingPoint 8 24))
(declare-fun rm () RoundingMode)
(assert
(and (not (fp.eq a b)) (fp.eq (fp.sub rm a b) +zero) true))
(check-sat)

Результат UNSAT . Таких номеров нет.

Вышеупомянутая строка SMTLIB также позволяет Z3 выбрать произвольный режим округления ( rm). Это означает, что результат верен для всех возможных режимов округления (их пять). Результат также включает возможность того, что любая из переменных в игре может иметь значение NaNили бесконечность.

a == bреализовано как fp.eqкачество, так что +0fи -0fсравнивать равно. Сравнение с нулем осуществляется с помощьюfp.eq . Поскольку вопрос направлен на то, чтобы избежать деления на ноль, это подходящее сравнение.

Если бы проверка на равенство была реализована с использованием побитового равенства, +0fи -0fэто был бы способ сделать a - bноль. Некорректная предыдущая версия этого ответа содержит подробные сведения о режиме для любопытных.

Z3 Online пока не поддерживает теорию FPA. Этот результат был получен с использованием последней нестабильной ветки. Его можно воспроизвести с помощью привязок .NET следующим образом:

var fpSort = context.MkFPSort32();
var aExpr = (FPExpr)context.MkConst("a", fpSort);
var bExpr = (FPExpr)context.MkConst("b", fpSort);
var rmExpr = (FPRMExpr)context.MkConst("rm", context.MkFPRoundingModeSort());
var fpZero = context.MkFP(0f, fpSort);
var subExpr = context.MkFPSub(rmExpr, aExpr, bExpr);
var constraintExpr = context.MkAnd(
        context.MkNot(context.MkFPEq(aExpr, bExpr)),
        context.MkFPEq(subExpr, fpZero),
        context.MkTrue()
    );

var smtlibString = context.BenchmarkToSMTString(null, "QF_FP", null, null, new BoolExpr[0], constraintExpr);

var solver = context.MkSimpleSolver();
solver.Assert(constraintExpr);

var status = solver.Check();
Console.WriteLine(status);

Использование Z3 , чтобы ответить на вопросы IEEE с плавающей точкой приятно , потому что это трудно игнорировать случаи (например NaN, -0f, +-inf) , и вы можете задать произвольные вопросы. Нет необходимости интерпретировать и цитировать спецификации. Вы даже можете задавать смешанные вопросы с числами с плавающей запятой и целыми числами, такие как « int log2(float)правильный ли этот алгоритм?».

USR
источник
Не могли бы вы добавить ссылку на SMT Solver Z3 и ссылку на онлайн-переводчик? Хотя этот ответ кажется совершенно правильным, кто-то может подумать, что эти результаты неверны.
AL
12

Предоставленная функция действительно может возвращать бесконечность:

public class Test {
    public static double calculation(double a, double b)
    {
         if (a == b)
         {
             return 0;
         }
         else
         {
             return 2 / (a - b);
         }
    }    

    /**
     * @param args
     */
    public static void main(String[] args) {
        double d1 = Double.MIN_VALUE;
        double d2 = 2.0 * Double.MIN_VALUE;
        System.out.println("Result: " + calculation(d1, d2)); 
    }
}

Выход есть Result: -Infinity.

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

Д Крюгер
источник
6

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

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

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

Supercat
источник
6

Раньше, до IEEE 754, было вполне возможно, что a! = B не означало ab! = 0 и наоборот. Это было одной из причин создания IEEE 754.

С IEEE 754 это почти гарантировано. Компиляторам C или C ++ разрешено выполнять операции с более высокой точностью, чем необходимо. Итак, если a и b не переменные, а выражения, тогда (a + b)! = C не подразумевает (a + b) - c! = 0, потому что a + b может быть вычислено один раз с более высокой точностью, а один раз без более высокая точность.

Многие FPU можно переключить в режим, в котором они не возвращают денормализованные числа, а заменяют их на 0. В этом режиме, если a и b являются крошечными нормализованными числами, где разница меньше наименьшего нормализованного числа, но больше 0, a ! = b также не гарантирует a == b.

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

gnasher729
источник
2

Я могу вспомнить случай, когда вы могли бы это сделать. Вот аналогичный образец в базе 10 - действительно, это, конечно, произошло бы в базе 2.

Числа с плавающей запятой хранятся более или менее в экспоненциальной нотации - то есть вместо 35,2 сохраняемое число будет больше похоже на 3,52e2.

Для удобства представьте, что у нас есть блок с плавающей запятой, который работает по основанию 10 и имеет 3-значную точность. Что произойдет, если вычесть 9,99 из 10,0?

1.00e2-9.99e1

Shift, чтобы присвоить каждому значению одинаковую экспоненту

1.00e2-0.999e2

Округлить до трех цифр

1.00e2-1.00e2

Ой!

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

Keldor314
источник
1
Регистры, содержащие выровненные операнды для вычитания, должны содержать два дополнительных бита, называемых «защитными битами», чтобы справиться с этой ситуацией. В сценарии, когда вычитание вызовет заимствование из наиболее значимого бита, либо величина меньшего операнда должна превышать половину величины большего операнда (подразумевая, что он может иметь только один дополнительный бит точности), либо результат должен быть не менее половина величины меньшего операнда (подразумевая, что ему потребуется только еще один бит плюс информация, достаточная для обеспечения правильного округления).
supercat
1
«Может ли это случиться в конечном итоге, зависит от конструкции FPU». Нет, этого не может произойти, потому что определение Java говорит, что не может. Дизайн FPU тут ни при чем.
Паскаль Куок,
@PascalCuoq: поправьте меня, если я ошибаюсь, но strictfpон не включен, вычисления могут давать значения, которые слишком малы для, doubleно будут соответствовать значению с плавающей запятой расширенной точности.
supercat
@supercat Отсутствие strictfpвлияет только на значения «промежуточных результатов», и я цитирую docs.oracle.com/javase/specs/jls/se7/html/jls-15.html#jls-15.4 . aи bявляются doubleпеременными, а не промежуточными результатами, поэтому их значения являются значениями с двойной точностью, поэтому кратны 2 ^ -1074. Следовательно, вычитание этих двух значений двойной точности кратно 2 ^ -1074, поэтому более широкий диапазон экспоненты изменяет свойство, заключающееся в том, что разница равна 0, если a == b.
Паскаль Куок,
@supercat В этом есть смысл - для этого вам понадобится всего один дополнительный бит.
Keldor314
1

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

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

if ((first >= second - error) || (first <= second + error)
Aviad
источник
6
«Не следует никогда» - немного сильное слово, но в целом это хороший совет.
Марк Паттисон,
1
Пока ты прав, abs(first - second) < error(или <= error) проще и лаконичнее.
glglgl
3
Хотя это верно в большинстве случаев ( не во всех ), на самом деле это не дает ответа на вопрос.
milleniumbug
4
Часто бывает полезно проверить числа с плавающей запятой на равенство. Нет ничего разумного в сравнении с эпсилон, который не был тщательно выбран, и даже менее разумным в сравнении с эпсилон, когда кто-то проверяет равенство.
tmyklebu
1
Если вы сортируете массив по ключу с плавающей запятой, я могу гарантировать, что ваш код не будет работать, если вы попытаетесь использовать трюки, сравнивая числа с плавающей запятой с эпсилоном. Потому что гарантии, что a == b и b == c подразумевает a == c, больше нет. Для хеш-таблиц точно такая же проблема. Когда равенство не является транзитивным, ваши алгоритмы просто ломаются.
gnasher729
1

Деление на ноль не определено, так как предел от положительных чисел стремится к бесконечности, ограниченный от отрицательных чисел стремится к отрицательной бесконечности.

Не уверен, C ++ это или Java, поскольку языкового тега нет.

double calculation(double a, double b)
{
     if (a == b)
     {
         return nan(""); // C++

         return Double.NaN; // Java
     }
     else
     {
         return 2 / (a - b);
     }
}
Khaled.K
источник
1

Основная проблема заключается в том, что компьютерное представление числа типа double (также известного как число с плавающей запятой или действительное число на математическом языке) неверно, когда у вас "слишком много" десятичного числа, например, когда вы имеете дело с double, которое нельзя записать как числовое значение ( пи или результат 1/3).

Итак, a == b не может быть выполнено с любым двойным значением a и b, как вы поступаете с a == b, когда a = 0,333 и b = 1/3? В зависимости от вашей ОС, FPU, числа, языка, количества 3 после 0, у вас будет true или false.

В любом случае, если вы выполняете «вычисление двойного значения» на компьютере, вам нужно иметь дело с точностью, поэтому вместо того, чтобы делать a==b, вы должны делать absolute_value(a-b)<epsilon, и эпсилон относится к тому, что вы моделируете в это время в своем алгоритме. У вас не может быть эпсилон-значения для всего вашего двойного сравнения.

Короче говоря, когда вы вводите a == b, вы получаете математическое выражение, которое нельзя перевести на компьютере (для любого числа с плавающей запятой).

PS: гул, все, что я здесь отвечаю, более или менее в других ответах и ​​комментариях.

Жан Дэви
источник
1

Основываясь на ответе @malarres и комментарии @Taemyr, вот мой небольшой вклад:

public double calculation(double a, double b)
{
     double c = 2 / (a - b);

     // Should not have a big cost.
     if (isnan(c) || isinf(c))
     {
         return 0; // A 'whatever' value.
     }
     else
     {
         return c;
     }
}

Я хочу сказать: самый простой способ узнать, является ли результат деления nan или inf, действительно для выполнения деления.

Orace
источник