Сравните двойное с нулем, используя эпсилон

214

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

Себастьян Крысманский
источник
3
Эпсилон около 1, скорее всего, будет намного выше, чем около 0, поэтому, вероятно, будут значения от 0 до 0 + epsilon_at_1. Я предполагаю, что автор этого раздела хотел использовать что-то маленькое, но он не хотел использовать магическую константу, поэтому он просто использовал это по существу произвольное значение.
enobayram
2
Сравнение чисел с плавающей запятой сложно, и использование эпсилон или пороговое значение даже рекомендуется. Пожалуйста, обратитесь: cs.princeton.edu/introcs/91float и cygnus-software.com/papers/comparingfloats/comparingfloats.htm
Адитья Кумар Пандей
40
Первая ссылка 403.99999999
graham.reeds
6
ИМО, в этом случае использование numeric_limits<>::epsilonвводит в заблуждение и не имеет значения. Нам нужно принять 0, если фактическое значение отличается не более чем на 0 от 0. И ε следует выбирать на основе спецификации задачи, а не на машинно-зависимом значении. Я подозреваю, что текущий эпсилон бесполезен, поскольку даже несколько операций FP могут накапливать ошибку больше, чем эта.
Андрей Вихров
1
+1. Эпсилон не наименьший возможный, но может служить данной цели в большинстве практических инженерных задач, если вы знаете, какая точность вам нужна и что вы делаете.
Шепурин

Ответы:

192

Предполагая, что 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% всех положительных представимых чисел ...

Яков Галка
источник
27
Настолько странно, что можно сказать «47% положительных чисел» :)
конфигуратор
13
@configurator: нет, вы не можете этого сказать (не существует «естественной» конечной меры). Но вы можете сказать «47% положительных представимых чисел».
Яков Галка
1
@ybungalobill Я не могу понять это. Экспонент имеет 11 битов: 1 знаковый бит и 10 битов значения. Почему 2 ^ -1022, а не 2 ^ -1024 - это наименьшее положительное число?
Павло Дыбан
3
@PavloDyban: просто потому, что экспоненты не имеют знакового бита. Они закодированы как смещения: если закодированный показатель степени равен, 0 <= e < 2048тогда мантисса умножается на 2 до степени e - 1023. Например, показатель степени 2^0кодируется как e=1023, 2^1как e=1024и 2^-1022как e=1. Значение e=0зарезервировано для субнормалей и реального нуля.
Яков Галка
2
@PavloDyban: также 2^-1022является наименьшим нормальным числом. Наименьшее число на самом деле 0.0000 00000000 00000000 00000000 00000000 00000000 00000001 × 2^-1022 = 2^-1074. Это субнормально, это означает, что часть мантиссы меньше 1, поэтому она кодируется показателем степени e=0.
Яков Галка
17

Тест, конечно, не то же самое, что 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, а может быть, что это не так.

Стив Джессоп
источник
2
Хороший вопрос, но даже если это так, лучше придерживаться привязки ошибки к переменной с разумным именем и использовать ее для сравнения. В нынешнем виде оно ничем не отличается от магической константы.
enobayram
Возможно, мне следовало быть более ясным в своем вопросе: я не задавался вопросом, был ли эпсилон достаточно большим «порогом», чтобы покрыть вычислительную ошибку, но равно ли это сравнение someValue == 0.0или нет.
Себастьян Крысманский
13

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

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

Skizz
источник
5

Аппроксимация эпсилона (наименьшая возможная разница) вокруг числа (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;
}
pbhd
источник
2
Какие реализации вы проверили? Это определенно не относится к GCC 4.7.
Антон Голов
3

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

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

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

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

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

Якк - Адам Невраумонт
источник
3

Разница между 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;
}
Даниэль Лугт
источник
2

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

someValue == 0.0

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

Лука Даванцо
источник
2

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

Арсений Фомин
источник
2

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

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

Supercat
источник
Разве «1 / (1-1)» (из вашего примера) не бесконечность, а не ноль?
Себастьян Крысманский
Величины (1-1), (1 / INF) и (-1 / INF) все представляют ноль, но деление положительного числа на каждое из них должно теоретически дать три разных результата (IEEE математика рассматривает первые два как идентичные ).
суперкат
1

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

cababunga
источник
За исключением нуля, я не думаю, что есть значения между -1e-20 и + 1e-20. Но только потому, что я думаю, что это не делает это правдой.
Себастьян Крысманский
@SebastianKrysmanski: это неправда, существует множество значений с плавающей точкой от 0 до epsilon. Потому что это с плавающей точкой, а не с фиксированной точкой.
Стив Джессоп
Наименьшее представимое значение, отличное от нуля, ограничено количеством битов, выделенных для представления показателя степени. Таким образом, если double имеет 11-битную экспоненту, наименьшее число будет 1e-1023.
Cababunga
0

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

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

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

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