Подтвердить что ты не робот

Почему (1-x) (1 + x) предпочтительнее (1-x ^ 2)?

Я рассматривал реализацию библиотеки времени выполнения arcsin, которая была реализована путем вычисления:

ArcTan(X, Sqrt(1 - X*X))

Однако код, который вычислял 1 - X*X, фактически оценивал (1-X)*(1+X). Есть ли веская причина для предпочтения последнего? Я подозреваю, что последний уменьшает погрешность округления для X близко к нулю, но я не могу объяснить, почему это было бы так.

4b9b3361

Ответ 1

Производная от ArcTan(X, Sqrt(1-X*X)) по X равна 1/Sqrt(1-X*X). Это продолжается до бесконечности при | X | переходит в 1. Поэтому, когда X близок к 1 или -1, любая ошибка в оценке оказывает огромное влияние на результат. Таким образом, крайне важно, чтобы оценка минимизировала ошибку в этих случаях.

Когда X близок к 1, оценка 1-X не имеет ошибки (в IEEE 754 или любой хорошей системе с плавающей запятой, поскольку масштаб результата таков, что его младший значащий бит по меньшей мере такой же низкий, как минимум значащий бит в 1 или X, поэтому точный математический результат не имеет битов вне доступных значащих бит). Так как 1-X является точным, рассмотрим эффект ошибки в 1+X, рассмотрев производную от ArcTan(X, Sqrt((1-X)*(1+X+e)) по e, где e - ошибка, введенная в операции 1+X. Производная есть, когда X близок к 1, а е мало, приблизительно -1/10. (Принимая производную с Maple и подставляя 1 для x, получаем -1/(sqrt(4+2e)*(5+2e). Тогда подстановка 0 для e дает -1/10.) Таким образом, ошибка в 1+X не является критичной.

Следовательно, оценка выражения как ArcTan(X, Sqrt((1-X)*(1+X)) является хорошим способом его оценки.

Ситуация симметрична для X вблизи -1. (1+X не имеет ошибки, а 1-X не является критическим.)

Обратно, если мы рассмотрим ошибку в X*X, то производная от ArcTan(X, Sqrt(1-X*X+e)) по e будет, когда X будет около 1, приблизительно -1/(2 * sqrt (e) * (1 + e)), поэтому он большой, когда e мало. Таким образом, небольшая ошибка при оценке X*X приведет к большой ошибке в результате, когда X близок к 1.


Спросите Паскаля Куока, при оценке функции f (x) нас обычно интересует минимизация относительной ошибки в конечном результате. И, как я уже указывал, ошибки, возникающие во время вычисления, обычно являются относительными ошибками промежуточных результатов из-за округления с плавающей запятой. Я мог проигнорировать это в приведенном выше, потому что я рассматривал функцию, когда X близок к 1, поэтому оба рассматриваемых промежуточных значения (1 + X и X * X) и конечное значение имели величины около 1, поэтому деление значений по этим величинам ничего не изменит.

Однако, для полноты, я более внимательно изучил ситуацию. В Maple я написал g := arctan(x, sqrt((1-x*x*(1+e0))*(1+e1))*(1+e2)), что позволяет относительные ошибки e0, e1 и e2 в вычислениях X*X, 1-x*x и sqrt соответственно, и я написал h:= arctan(x, sqrt((1-x)*(1+x)*(1+e0))*(1+e2)) для альтернативы. Заметим, что e0 в этом случае объединяет три ошибки в 1-X, 1+X и их умножение; полный член ошибки может быть (1+ea)*(1+eb)*(1+ec), но это эффективно 1+e0 с большим возможным диапазоном для e0.

Затем я исследовал производные этих функций по (по одному за раз) e0, e1 и e2, деленному на abs (f (x)), где f была идеальной функцией ArcTan(X, Sqrt(1-X*X)). Например, в Maple я рассмотрел diff(g, e0) / abs(f(x)). Я не проводил их полной аналитической оценки; Я изучил значения для некоторых значений x вблизи 0 и около 1 и для значений e0, e1 и e2 в одном из своих пределов -2 -54.

При x около 0 значения были примерно 1 или меньше. То есть любая относительная ошибка в вычислении привела к аналогичной относительной ошибке в результате или меньше.

При x вблизи 1 значения с производными от e1 и e2 были крошечными, около 10 -8 или меньше. Однако значения с производными e0 были сильно различны для двух методов. Для метода 1-x*x значение было около 2 • 10 7 (с использованием x = 1-2 -53). Для метода (1-x)*(1+x) значение было около 5 • 10 -9.

Таким образом, эти два метода мало отличаются друг от друга вблизи x = 0, но последний метод значительно лучше вблизи x = 1.

Ответ 2

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

#include <float.h>
#include <math.h>
#include <stdio.h>

long double d1, d2, rel1, rel2;
float i1, i2;
int main() {
  float f;
  for (f = nextafterf(0, 2); f <= 1; f = nextafterf(f, 2))
    {
      long double o = 1.0L - ((long double)f * f);
      float r1 = (1 - f) * (1 + f);
      float r2 = 1 - f * f;
      long double c1 = fabsl(o - r1);
      long double c2 = fabsl(o - r2);
      if (c1 > d1) d1 = c1;
      if (c2 > d2) d2 = c2;
      if (c1 / o > rel1) rel1 = c1 / o, i1 = f;
      if (c2 / o > rel2) rel2 = c2 / o, i2 = f;
    }

  printf("(1-x)(1+x) abs:%Le  relative:%Le\n", d1, rel1);
  printf("1-x*x      abs:%Le  relative:%Le\n\n", d2, rel2);

  printf("input1: %a 1-x:%a 1+x:%a (1-x)(1+x):%a o:%a\n", i1, 1-i1, 1+i1, (1-i1)*(1+i1), (double)(1 - ((long double)i1 * i1)));
  printf("input2: %a x*x:%a 1-x*x:%a o:%a\n", i2, i2*i2, 1 - i2*i2, (double)(1 - ((long double)i2 * i2)));
}

Несколько замечаний:

  • Я использовал 80-разрядный long double для вычисления метаданных. Этого недостаточно, чтобы точно представить ошибку, сделанную для всех значений x, но я боюсь, что программа станет слишком медленной с более высокой точностью.
  • Исходное значение o вычисляется как 1.0L - ((long double)f * f). Это всегда ближайший long double номер к реальному результату, потому что (long double)f * f является точным (см., Уже кажется, что форма 1 - x*x иногда может быть лучше:)).

Я получил следующие результаты:

(1-x)(1+x) abs:8.940394e-08  relative:9.447410e-08
1-x*x      abs:4.470348e-08  relative:8.631498e-05

input1: 0x1.6a046ep-1 1-x:0x1.2bf724p-2 1+x:0x1.b50238p+0 (1-x)(1+x):0x1.0007bep-1 o:0x1.0007bc6a305ep-1
input2: 0x1.ffe96p-1 x*x:0x1.ffd2cp-1 1-x*x:0x1.6ap-12 o:0x1.69f8007p-12

В соответствии с этими результатами 1 - x*x имеет лучшую абсолютную точность, а (1-x)*(1+x) имеет большую лучшую относительную точность. Плавающая точка относится к относительной точности (вся система предназначена для обеспечения относительно точного представления малых и больших значений), поэтому последняя форма является предпочтительной.

EDIT: Вычисление окончательной ошибки имеет больше смысла, как показано в ответе Эрика. Подвыражение в выражении, таком как ArcTan(X, Sqrt(1 - X*X)), могло быть выбрано не из-за его лучшей точности в целом, а потому, что оно было точным там, где оно имело значение. Добавление строк ниже в тело цикла:

  long double a = atan2l(f, sqrtl(o));
  float a1 = atan2f(f, sqrtf(r1));
  float a2 = atan2f(f, sqrtf(r2));
  long double e1 = fabsl(a - a1);
  long double e2 = fabsl(a - a2);
  if (e1 / a > ae1) ae1 = e1 / a, i1 = f;
  if (e2 / a > ae2) ae2 = e2 / a, i2 = f;

Это может иметь смысл использовать atan2l(f, sqrtf(r1)), потому что у меня нет такой же функции ArcTan, что и ваша система. Во всяком случае, с этими предостережениями для полного выражения максимальная относительная ошибка на интервале [-1... 1] составляет 1.4e-07 для версии (1-x) (1 + x) и 5.5e-7 для 1 -x 2 версия.