Сравните double to zero, используя epsilon

Сегодня я просматривал код С++ (написанный кем-то еще) и нашел этот раздел:

double someValue = ...
if (someValue <  std::numeric_limits<double>::epsilon() && 
    someValue > -std::numeric_limits<double>::epsilon()) {
  someValue = 0.0;
}

Я пытаюсь выяснить, действительно ли это имеет смысл.

В документации для epsilon() говорится:

Функция возвращает разность между 1 и наименьшим значением больше 1, которое представляется [двойным].

Это относится также к 0, т.е. epsilon() - наименьшее значение, большее 0? Или есть числа между 0 и 0 + epsilon, которые могут быть представлены double?

Если нет, то это не сравнение, эквивалентное someValue == 0.0?

Ответ 1

Предполагая, что 64-битный IEEE double, есть 52-битная мантисса и 11-битная экспонента. Пусть разбить его на куски:

1.0000 00000000 00000000 00000000 00000000 00000000 00000000 × 2^0 = 1

Наименьшее представимое число больше 1:

1.0000 00000000 00000000 00000000 00000000 00000000 00000001 × 2^0 = 1 + 2^-52

Следовательно:

epsilon = (1 + 2^-52) - 1 = 2^-52

Есть ли числа от 0 до эпсилон? Много... Например, минимальное положительное представимое (нормальное) число:

1.0000 00000000 00000000 00000000 00000000 00000000 00000000 × 2^-1022 = 2^-1022

На самом деле между 0 и эпсилонами есть (1022 - 52 + 1)×2^52 = 4372995238176751616 чисел, что составляет 47% всех положительных представимых чисел...

Ответ 2

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

Чтобы использовать более привычную десятичную систему, предположим, что вы сохраняете десятичное значение "до 4 значащих цифр" с показателем экспоненты. Тогда следующее представимое значение больше 1 равно 1.001 * 10^0, а epsilon - 1.000 * 10^-3. Но 1.000 * 10^-4 также представимо, предполагая, что экспонента может хранить -4. Вы можете смириться с этим, что двойной IEEE может хранить экспоненты меньше экспоненты epsilon.

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

Ответ 3

Существуют числа, которые существуют между 0 и epsilon, потому что epsilon - это разница между 1 и следующим самым большим числом, которое может быть представлено выше 1, а не разностью между 0 и следующим самым большим числом, которое может быть представлено выше 0 (если оно были, этот код будет делать очень мало): -

#include <limits>

int main ()
{
  struct Doubles
  {
      double one;
      double epsilon;
      double half_epsilon;
  } values;

  values.one = 1.0;
  values.epsilon = std::numeric_limits<double>::epsilon();
  values.half_epsilon = values.epsilon / 2.0;
}

Используя отладчик, остановите программу в конце main и посмотрите на результаты, и вы увидите, что epsilon/2 отличается от epsilon, zero и one.

Таким образом, эта функция принимает значения между +/- epsilon и делает их нулевыми.

Ответ 4

Аппроксимация epsilon (наименьшая возможная разность) вокруг числа (1.0, 0.0,...) может быть напечатана следующей программой. Он печатает следующий результат:
epsilon for 0.0 is 4.940656e-324
epsilon for 1.0 is 2.220446e-16
Небольшое мышление дает понять, что эпсилон становится меньше, чем меньше число, которое мы используем для просмотра его эпсилон-значения, так как экспоненту можно приспособиться к размеру этого числа.

#include <stdio.h>
#include <assert.h>
double getEps (double m) {
  double approx=1.0;
  double lastApprox=0.0;
  while (m+approx!=m) {
    lastApprox=approx;
    approx/=2.0;
  }
  assert (lastApprox!=0);
  return lastApprox;
}
int main () {
  printf ("epsilon for 0.0 is %e\n", getEps (0.0));
  printf ("epsilon for 1.0 is %e\n", getEps (1.0));
  return 0;
}

Ответ 5

Предположим, что мы работаем с числами с плавающей запятой, которые вписываются в 16-разрядный регистр. Существует бит знака, 5-битный показатель и 10-битная мантисса.

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

Около 1 показатель равен нулю. Таким образом, самая маленькая цифра мантиссы - это одна часть в 1024.

Около 1/2 показатель экспоненты минус один, поэтому наименьшая часть мантиссы вдвое меньше. С пятибитным показателем он может достигать отрицательного 16, в этот момент самая маленькая часть мантиссы стоит одной части в 32 м. И при отрицательном 16 экспоненте значение составляет около одной части в 32 к, намного ближе к нулю, чем эпсилон вокруг той, которую мы рассчитали выше!

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

Ответ 6

Разница между X и следующим значением X изменяется в соответствии с X.
epsilon() - это только разница между 1 и следующим значением 1.
Разница между 0 и следующим значением 0 не равна epsilon().

Вместо этого вы можете использовать std::nextafter для сравнения двойного значения с 0 следующим образом:

bool same(double a, double b)
{
  return std::nextafter(a, std::numeric_limits<double>::lowest()) <= b
    && std::nextafter(a, std::numeric_limits<double>::max()) >= b;
}

double someValue = ...
if (same (someValue, 0.0)) {
  someValue = 0.0;
}

Ответ 7

Я думаю, что это зависит от precision вашего компьютера. Посмотрите на эту таблицу : вы можете видеть, что если ваш epsilon представлен двойным, но ваша точность выше, сравнение не эквивалентно

someValue == 0.0

Хороший вопрос в любом случае!

Ответ 8

Вы не можете применить это к 0, из-за мантиссы и частей экспоненты. Благодаря экспоненте вы можете хранить очень мало номеров, которые меньше, чем epsilon, но когда вы пытаетесь сделать что-то вроде (1.0 - "очень маленькое число" ), вы получите 1.0. Эпсилон - это показатель не значения, а точности значения, который находится в мантиссе. Он показывает, сколько правильных последовательных десятичных цифр числа, которое мы можем сохранить.

Ответ 9

Итак, пусть система не может отличить 1.000000000000000000000 и 1.000000000000000000001. то есть 1.0 и 1.0 + 1e-20. Как вы думаете, все еще существуют некоторые значения, которые могут быть представлены между -1e-20 и + 1e-20?

Ответ 10

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

Было бы возможно, что формат с плавающей запятой может иметь три значения между наименьшими конечными положительными и отрицательными значениями: положительным бесконечно малым, беззнаковым нулем и отрицательным бесконечно малым. Я не знаком с любыми форматами с плавающей запятой, которые на самом деле работают таким образом, но такое поведение было бы вполне разумным и, возможно, лучше, чем у IEEE (возможно, недостаточно было бы лучше добавить дополнительное оборудование для его поддержки, но математически 1/(1/INF), 1/(- 1/INF) и 1/(1-1) должны представлять три отдельных случая, иллюстрирующих три разных нуля). Я не знаю, мог бы ли какой-либо стандарт C подписывать бесконечно малые числа, если они существуют, приходилось сравнивать равным нулю. Если они этого не сделают, код, подобный приведенному выше, может с пользой обеспечить, например, разделение числа несколько раз на два, в конечном итоге даст нуль, а не застревает на "бесконечно малом".

Ответ 11

Кроме того, хорошей причиной для такой функции является удаление "денормалов" (тех очень маленьких чисел, которые больше не могут использовать подразумеваемый ведущий "1" и иметь специальное представление FP). Зачем вам это делать? Потому что некоторые машины (в частности, некоторые старые Pentium 4) действительно становятся действительно медленными при обработке денормалов. Другие просто немного замедлились. Если ваше приложение действительно не нуждается в этих очень маленьких количествах, то их сброс до нуля является хорошим решением. Хорошими местами для рассмотрения этого являются последние шаги любых фильтров IIR или функций распада.

См. также: Почему изменение 0,1f в 0 замедляет производительность на 10x?

и http://en.wikipedia.org/wiki/Denormal_number