Действительно ли "epsilon" гарантирует что-либо в вычислениях с плавающей запятой?
Чтобы сделать проблему короткой, скажем, я хочу вычислить выражение a / (b - c)
на float
s.
Чтобы убедиться, что результат имеет смысл, я могу проверить, равны ли b
и c
:
float EPS = std::numeric_limits<float>::epsilon();
if ((b - c) > EPS || (c - b) > EPS)
{
return a / (b - c);
}
но мои тесты показывают, что этого недостаточно, чтобы гарантировать либо значимые результаты, либо не дать результат, если это возможно.
Случай 1:
a = 1.0f;
b = 0.00000003f;
c = 0.00000002f;
Результат: Условие if не выполняется, но выражение создаст правильный результат 100000008 (как для точности поплавков).
Случай 2:
a = 1e33f;
b = 0.000003;
c = 0.000002;
Результат: Условие if выполнено, но выражение не дает значимого результата +1.#INF00
.
Я нашел гораздо надежнее проверять результат, а не аргументы:
const float INF = numeric_limits<float>::infinity();
float x = a / (b - c);
if (-INF < x && x < INF)
{
return x;
}
Но зачем эпсилон тогда и почему все говорят, что эпсилон хорош для использования?
Ответы
Ответ 1
"вы должны использовать epsilon при работе с поплавками" - это реакция программистов с коленом с поверхностным пониманием вычислений с плавающей запятой, для сравнения вообще (не только к нулю).
Это обычно бесполезно, потому что он не говорит вам, как минимизировать распространение ошибок округления, он не говорит вам, как избежать проблем с отменой или поглощением, и даже когда ваша проблема действительно связана с сопоставлением двух floats, , он не говорит вам, какое значение epsilon подходит для того, что вы делаете.
Если вы не читали Что каждый компьютерный ученый должен знать о арифметике с плавающей точкой, это хорошая отправная точка. Более того, если вас интересует точность результата деления в вашем примере, вы должны оценить, насколько неточными b-c
были сделаны предыдущие ошибки округления, потому что, действительно, если b-c
мал, небольшая абсолютная ошибка соответствует большой абсолютной ошибке результата. Если ваша забота заключается только в том, что разделение не должно переполняться, тогда ваш тест (по результату) является правильным. Нет никаких оснований проверять нулевой делитель с числами с плавающей запятой, вы просто проверяете переполнение результата, который фиксирует оба случая, когда делитель равен нулю и где делитель настолько мал, чтобы сделать результат не представимым с любой точности.
Что касается распространения ошибок округления, существует специализированные анализаторы, которые могут помочь вам оценить его, потому что это утомительная вещь рука.
Ответ 2
Epsilon используется для определения того, являются ли два числа, подверженные ошибке округления, достаточно близкими, чтобы считаться "равными". Обратите внимание, что лучше протестировать fabs(b/c - 1) < EPS
, чем fabs(b-c) < EPS
, и даже лучше -— благодаря дизайну IEEE floats — для тестирования abs(*(int*)&b - *(int*)&c) < EPSI
(где EPSI - некоторое небольшое целое).
Ваша проблема носит иной характер и, вероятно, требует проверки результата, а не входных данных.