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

12 131

12 ответов:

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

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

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

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

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

в качестве обходного пути, как насчет следующего?

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

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

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

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

EDIT:

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

  1. " не число сравнивает " false с самим собой, но будет иметь идентичные битовые шаблоны.

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

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

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

The SMT SolverZ3 поддерживает точную арифметику с плавающей запятой 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 float приятно, потому что трудно игнорировать случаи (такие как NaN,-0f,+-inf) и вы можете задавать произвольные вопросы. Нет необходимости интерпретировать и цитировать спецификации. Вы даже можете задать смешанные вопросы с плавающей точкой и целым числом, такие как " является ли это конкретным исправить?".

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

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.

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

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

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

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

в старые времена до IEEE 754, вполне возможно, что a != b не подразумевал a-b != 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.

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

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

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

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

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

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

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

1.00e2-9.99e1

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

1.00e2-0.999e2

округлить до 3 цифр

1.00e2-1.00e2

ой-ой!

может ли это произойти в конечном итоге зависит от дизайна FPU. Поскольку диапазон показателей для двойника очень велик, аппаратное обеспечение должно округляться внутри в какой-то момент, но в случае выше, только 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 на самом деле для выполнения деления.

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

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

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

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

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

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

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