Какое сравнение с плавающей запятой является более точным и почему?

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

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

Поэтому я должен использовать относительные критерии. Наивно я бы использовал что-то вроде этого:

static int sqrt_good_enough(float x, float y) {
  return fabsf(y*y - x) / x < EPS;
}

И это работает очень хорошо. Но недавно я начал читать Kernighan и Plauger The Elements of Programming Style, и они дают программу Fortran для того же алгоритма в главе 1, критерии завершения которой переведены на C:

static int sqrt_good_enough(float x, float y) {
  return fabsf(x/y - y) < EPS * y;
}

Оба являются математически эквивалентными, но есть ли причина для предпочтения одной формы над другой?

Ответы

Ответ 1

Эти два фактически не эквивалентны математически, если вы не написали fabsf (y * y - x)/(y * y) < EPS для первого. (извините за опечатку в моем оригинальном комментарии)

Но я думаю, что ключевым моментом является то, чтобы выражение соответствовало вашей формуле для вычисления y в итерации Newton. Например, если ваша y-формула y = (y + x/y)/2, вы должны использовать стиль Kernighan и Plauger. Если это y = (y * y + x)/(2 * y), вы должны использовать (y * y - x)/(y * y) EPS.

Обычно критерии завершения должны заключаться в том, что abs (y (n + 1) - y (n)) достаточно мала (т.е. меньше y (n + 1) * EPS). Вот почему два выражения должны совпадать. Если они не совпадают точно, возможно, что тест завершения решает, что остаток не является достаточно маленьким, а разница в y (n) меньше, чем ошибка с плавающей точкой, из-за различного масштабирования. Результатом будет бесконечный цикл, потому что y (n) перестает меняться и критерии завершения никогда не выполняются.

Например, следующий код Matlab - это точно такой же решатель Newton, что и ваш первый пример, но он работает вечно:

x = 6.800000000000002
yprev = 0
y = 2
while abs(y*y - x) > eps*abs(y*y)
    yprev = y;
    y = 0.5*(y + x/y);
end

Версия C/С++ имеет ту же проблему.

Ответ 2

Они все еще не эквивалентны; нижняя часть математически эквивалентна fabsf(y*y - x) / (y*y) < EPS. Проблема, которую я вижу с вашей, заключается в том, что если y*y переполняется (возможно, потому что x есть FLT_MAX и y выбрано несчастливо), то завершение может никогда не произойти. В следующем взаимодействии используются двойники.

>>> import math
>>> x = (2.0 - 2.0 ** -52) * 2.0 ** 1023
>>> y = x / math.sqrt(x)
>>> y * y - x
inf
>>> y == 0.5 * (y + x / y)
True

EDIT: в качестве комментария (теперь удалено) указано, что также полезно обмениваться операциями между итерацией и тестом на завершение.

EDIT2: у обоих, вероятно, есть проблемы с субнормальным x. специалисты нормализуют x, чтобы избежать осложнений обеих крайностей.