Сравните double с zero с помощью epsilon

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

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?

11 ответов


предполагая 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

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


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

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

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


есть числа, которые существуют между 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 и один.

таким образом, эта функция принимает значения между +/- 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;
}

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

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

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

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

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


Я думаю, что это зависит от точность вашего компьютера. Взгляните на это стол: вы можете увидеть, что если ваш Эпсилон представлен двойным, но точность выше, сравнение не эквивалентно

someValue == 0.0

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


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


разницу между 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;
}

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


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

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


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

Читайте также: почему изменение 0.1 f на 0 замедляет производительность на 10x?

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