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

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

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

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

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

Ответ 1

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

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

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

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

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

Ответ 2

Как обходной путь, как насчет следующего?

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

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

Ответ 3

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

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

ИЗМЕНИТЬ:

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

  • "Число не сравнивается" с самим собой, но будет иметь одинаковые битовые шаблоны.

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

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

Ответ 4

Нет случая, когда здесь может произойти деление на ноль.

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), и вы можете задать произвольные вопросы. Не нужно толковать и приводить спецификации. Вы даже можете задать смешанные вопросы с float и integer, такие как "этот правильный алгоритм int log2(float) правильный?".

Ответ 5

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

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

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

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

Ответ 7

В прежние времена до IEEE 754 вполне возможно, что a!= b не означало a-b!= 0 и наоборот. Это было одной из причин создания IEEE 754 в первую очередь.

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

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

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

Ответ 8

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

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

if ((first >= second - error) || (first <= second + error)

Ответ 9

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

Числа с плавающей запятой хранятся более или менее в научной нотации - то есть вместо 35.2 сохраненное число будет больше похоже на 3.52e2.

Представьте себе, что для удобства мы имеем блок с плавающей точкой, который работает в базе 10 и имеет 3 цифры точности. Что произойдет, если вы вычтите 9.99 из 10.0?

1.00e2-9.99e1

Сдвиг, чтобы дать каждому значению тот же показатель

1.00e2-0.999e2

От трех до трех цифр

1.00e2-1.00e2

Uh oh!

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

Ответ 10

Основываясь на ответе @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 актуальным для выполнения деления.

Ответ 11

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

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

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

         return Double.NaN; // Java
     }
     else
     {
         return 2 / (a - b);
     }
}

Ответ 12

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

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

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

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

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