Сравните двойник к нул используя Эпсилон



сегодня я просматривал какой-то код 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?

821   11  

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 и 1.001 * 10^0 и epsilon - это 1.000 * 10^-3. Но 1.000 * 10^-4 также представимо, предполагая, что экспонента может хранить -4. Вы можете поверить мне на слово, что IEEE double можете хранить показатели меньше, чем показатель epsilon.

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

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

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

апроксимация Эпсилона (наименьшая возможная разница) вокруг числа (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, и в этот момент наименьшая часть мантиссы стоит одной части в 32 м. А при отрицательном 16 показателе значение составляет около одной части в 32k, намного ближе к нулю, чем Эпсилон вокруг одного, который мы рассчитали выше!

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

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

someValue == 0.0

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

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

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

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

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

Comments

    Ничего не найдено.