Уменьшение диапазона Плохая точность для одноточечной плавающей точки
Я пытаюсь реализовать уменьшение диапазона как первый шаг реализации синусоиды.
Я следую методу, описанному в статье "СОКРАЩЕНИЕ АРГУМЕНТОМ ДЛЯ БОЛЬШИХ АРГУМЕНТОВ" К.К. NG
Я получаю ошибку размером 0,002339146 при использовании диапазона ввода x от 0 до 20000. Моя ошибка, очевидно, не должна быть такой большой, и я не уверен, как я могу ее уменьшить. Я заметил, что величина ошибки связана с входной величиной theta-величиной с косинусом/синусом.
Мне удалось получить код nearpi.c, который упоминается в документе, но я не уверен, как использовать код для одинарной точности с плавающей запятой. Если кому-то интересно, файл nearpi.c можно найти по этой ссылке: nearpi.c
Вот мой код MATLAB:
x = 0:0.1:20000;
% Perform range reduction
% Store constant 2/pi
twooverpi = single(2/pi);
% Compute y
y = (x.*twooverpi);
% Compute k (round to nearest integer
k = round(y);
% Solve for f
f = single(y-k);
% Solve for r
r = single(f*single(pi/2));
% Find last two bits of k
n = bitand(fi(k,1,32,0),fi(3,1,32,0));
n = single(n);
% Preallocate for speed
z(length(x)) = 0;
for i = 1:length(x)
switch(n(i))
case 0
z(i)=sin(r(i));
case 1
z(i) = single(cos(r(i)));
case 2
z(i) = -sin(r(i));
case 3
z(i) = single(-cos(r(i)));
otherwise
end
end
maxerror = max(abs(single(z - single(sin(single(x))))))
minerror = min(abs(single(z - single(sin(single(x))))))
Я редактировал программу nearpi.c так, чтобы она компилировалась. Однако я не уверен, как интерпретировать вывод. Также файл ожидает ввода, который я должен был ввести вручную, также я не уверен в значении ввода.
Ниже приведен пример работы nearpi.c:
/*
============================================================================
Name : nearpi.c
Author :
Version :
Copyright : Your copyright notice
Description : Hello World in C, Ansi-style
============================================================================
*/
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
/*
* Global macro definitions.
*/
# define hex( double ) *(1 + ((long *) &double)), *((long *) &double)
# define sgn(a) (a >= 0 ? 1 : -1)
# define MAX_k 2500
# define D 56
# define MAX_EXP 127
# define THRESHOLD 2.22e-16
/*
* Global Variables
*/
int CFlength, /* length of CF including terminator */
binade;
double e,
f; /* [e,f] range of D-bit unsigned int of f;
form 1X...X */
// Function Prototypes
int dbleCF (double i[], double j[]);
void input (double i[]);
void nearPiOver2 (double i[]);
/*
* This is the start of the main program.
*/
int main (void)
{
int k; /* subscript variable */
double i[MAX_k],
j[MAX_k]; /* i and j are continued fractions
(coeffs) */
// fp = fopen("/src/cfpi.txt", "r");
/*
* Compute global variables e and f, where
*
* e = 2 ^ (D-1), i.e. the D bit number 10...0
* and
* f = 2 ^ D - 1, i.e. the D bit number 11...1 .
*/
e = 1;
for (k = 2; k <= D; k = k + 1)
e = 2 * e;
f = 2 * e - 1;
/*
* Compute the continued fraction for (2/e)/(pi/2) , i.e.
* q starting value for the first binade, given the continued
* fraction for pi as input; set the global variable CFlength
* to the length of the resulting continued fraction (including
* its negative valued terminator). One should use as many
* partial coefficients of pi as necessary to resolve numbers
* of the width of the underflow plus the overflow threshold.
* A rule of thumb is 0.97 partial coefficients are generated
* for every decimal digit of pi .
*
* Note: for radix B machines, subroutine input should compute
* the continued fraction for (B/e)/(pi/2) where e = B ^ (D - 1).
*/
input (i);
/*
* Begin main loop over all binades:
* For each binade, find the nearest multiples of pi/2 in that binade.
*
* [ Note: for hexadecimal machines ( B = 16 ), the rest of the main
* program simplifies(!) to
*
* B_ade = 1;
* while (B_ade < MAX_EXP)
* {
* dbleCF (i, j);
* dbleCF (j, i);
* dbleCF (i, j);
* CFlength = dbleCF (j, i);
* B_ade = B_ade + 1;
* }
* }
*
* because the alternation of source & destination are no longer necessary. ]
*/
binade = 1;
while (binade < MAX_EXP)
{
/*
* For the current (odd) binade, find the nearest multiples of pi/2.
*/
nearPiOver2 (i);
/*
* Double the continued fraction to get to the next (even) binade.
* To save copying arrays, i and j will alternate as the source
* and destination for the continued fractions.
*/
CFlength = dbleCF (i, j);
binade = binade + 1;
/*
* Check for main loop termination again because of the
* alternation.
*/
if (binade >= MAX_EXP)
break;
/*
* For the current (even) binade, find the nearest multiples of pi/2.
*/
nearPiOver2 (j);
/*
* Double the continued fraction to get to the next (odd) binade.
*/
CFlength = dbleCF (j, i);
binade = binade + 1;
}
return 0;
} /* end of Main Program */
/*
* Subroutine DbleCF doubles a continued fraction whose partial
* coefficients are i[] into a continued fraction j[], where both
* arrays are of a type sufficient to do D-bit integer arithmetic.
*
* In my case ( D = 56 ) , I am forced to treat integers as double
* precision reals because my machine does not have integers of
* sufficient width to handle D-bit integer arithmetic.
*
* Adapted from a Basic program written by W. Kahan.
*
* Algorithm based on Hurwitz method of doubling continued
* fractions (see Knuth Vol. 3, p.360).
*
* A negative value terminates the last partial quotient.
*
* Note: for the non-C programmers, the statement break
* exits a loop and the statement continue skips to the next
* case in the same loop.
*
* The call modf ( l / 2, &l0 ) assigns the integer portion of
* half of L to L0.
*/
int dbleCF (double i[], double j[])
{
double k,
l,
l0,
j0;
int n,
m;
n = 1;
m = 0;
j0 = i[0] + i[0];
l = i[n];
while (1)
{
if (l < 0)
{
j[m] = j0;
break;
};
modf (l / 2, &l0);
l = l - l0 - l0;
k = i[n + 1];
if (l0 > 0)
{
j[m] = j0;
j[m + 1] = l0;
j0 = 0;
m = m + 2;
};
if (l == 0) {
/*
* Even case.
*/
if (k < 0)
{
m = m - 1;
break;
}
else
{
j0 = j0 + k + k;
n = n + 2;
l = i[n];
continue;
};
}
/*
* Odd case.
*/
if (k < 0)
{
j[m] = j0 + 2;
break;
};
if (k == 0)
{
n = n + 2;
l = l + i[n];
continue;
};
j[m] = j0 + 1;
m = m + 1;
j0 = 1;
l = k - 1;
n = n + 1;
continue;
};
m = m + 1;
j[m] = -99999;
return (m);
}
/*
* Subroutine input computes the continued fraction for
* (2/e) / (pi/2) , where e = 2 ^ (D-1) , given pi 's
* continued fraction as input. That is, double the continued
* fraction of pi D-3 times and place a zero at the front.
*
* One should use as many partial coefficients of pi as
* necessary to resolve numbers of the width of the underflow
* plus the overflow threshold. A rule of thumb is 0.97
* partial coefficients are generated for every decimal digit
* of pi . The last coefficient of pi is terminated by a
* negative number.
*
* I'll be happy to supply anyone with the partial coefficients
* of pi . My ARPA address is [email protected] .
*
* I computed the partial coefficients of pi using a method of
* Bill Gosper's. I need only compute with integers, albeit
* large ones. After writing the program in bc and Vaxima ,
* Prof. Fateman suggested FranzLisp . To my surprise, FranzLisp
* ran the fastest! the reason? FranzLisp Bignum package is
* hand coded in assembler. Also, FranzLisp can be compiled.
*
*
* Note: for radix B machines, subroutine input should compute
* the continued fraction for (B/e)/(pi/2) where e = B ^ (D - 1).
* In the case of hexadecimal ( B = 16 ), this is done by repeated
* doubling the appropriate number of times.
*/
void input (double i[])
{
int k;
double j[MAX_k];
/*
* Read in the partial coefficients of pi from a precalculated file
* until a negative value is encountered.
*/
k = -1;
do
{
k = k + 1;
scanf ("%lE", &i[k]);
printf("hello\n");
printf("%d", k);
} while (i[k] >= 0);
/*
* Double the continued fraction for pi D-3 times using
* i and j alternately as source and destination. On my
* machine D = 56 so D-3 is odd; hence the following code:
*
* Double twice (D-3)/2 times,
*/
for (k = 1; k <= (D - 3) / 2; k = k + 1)
{
dbleCF (i, j);
dbleCF (j, i);
};
/*
* then double once more.
*/
dbleCF (i, j);
/*
* Now append a zero on the front (reciprocate the continued
* fraction) and the return the coefficients in i .
*/
i[0] = 0;
k = -1;
do
{
k = k + 1;
i[k + 1] = j[k];
} while (j[k] >= 0);
/*
* Return the length of the continued fraction, including its
* terminator and initial zero, in the global variable CFlength.
*/
CFlength = k;
}
/*
* Given a continued fraction coefficients in an array i ,
* subroutine nearPiOver2 finds all machine representable
* values near a integer multiple of pi/2 in the current binade.
*/
void nearPiOver2 (double i[])
{
int k, /* subscript for recurrences (see
handout) */
K; /* like k , but used during cancel. elim.
*/
double p[MAX_k], /* product of the q (see
handout) */
q[MAX_k], /* successive tail evals of CF (see
handout) */
j[MAX_k], /* like convergent numerators (see
handout) */
tmp, /* temporary used during cancellation
elim. */
mk0, /* m[k - 1] (see
handout) */
mk, /* m[k] is one of the few ints (see
handout) */
mkAbs, /* absolute value of m sub k
*/
mK0, /* like mk0 , but used during cancel.
elim. */
mK, /* like mk , but used during cancel.
elim. */
z, /* the object of our quest (the argument)
*/
m0, /* the mantissa of z as a D-bit integer
*/
x, /* the reduced argument (see
handout) */
ldexp (), /* sys routine to multiply by a power of
two */
fabs (), /* sys routine to compute FP absolute
value */
floor (), /* sys routine to compute greatest int <=
value */
ceil (); /* sys routine to compute least int >=
value */
/*
* Compute the q by evaluating the continued fraction from
* bottom up.
*
* Start evaluation with a big number in the terminator position.
*/
q[CFlength] = 1.0 + 30;
for (k = CFlength - 1; k >= 0; k = k - 1)
q[k] = i[k] + 1 / q[k + 1];
/*
* Let THRESHOLD be the biggest | x | that we are interesed in
* seeing.
*
* Compute the p and j by the recurrences from the top down.
*
* Stop when
*
* 1 1
* ----- >= THRESHOLD > ------ .
* 2 |j | 2 |j |
* k k+1
*/
p[0] = 1;
j[0] = 0;
j[1] = 1;
k = 0;
do
{
p[k + 1] = -q[k + 1] * p[k];
if (k > 0)
j[1 + k] = j[k - 1] - i[k] * j[k];
k = k + 1;
} while (1 / (2 * fabs (j[k])) >= THRESHOLD);
/*
* Then mk runs through the integers between
*
* k + k +
* (-1) e / p - 1/2 & (-1) f / p - 1/2 .
* k k
*/
for (mkAbs = floor (e / fabs (p[k]));
mkAbs <= ceil (f / fabs (p[k])); mkAbs = mkAbs + 1)
{
mk = mkAbs * sgn (p[k]);
/*
* For each mk , mk0 runs through integers between
*
* +
* m q - p THRESHOLD .
* k k k
*/
for (mk0 = floor (mk * q[k] - fabs (p[k]) * THRESHOLD);
mk0 <= ceil (mk * q[k] + fabs (p[k]) * THRESHOLD);
mk0 = mk0 + 1)
{
/*
* For each pair { mk , mk0 } , check that
*
* k
* m = (-1) ( j m - j m )
* 0 k-1 k k k-1
*/
m0 = (k & 1 ? -1 : 1) * (j[k - 1] * mk - j[k] * mk0);
/*
* lies between e and f .
*/
if (e <= fabs (m0) && fabs (m0) <= f)
{
/*
* If so, then we have found an
*
* k
* x = ((-1) m / p - m ) / j
* 0 k k k
*
* = ( m q - m ) / p .
* k k k-1 k
*
* But this later formula can suffer cancellation. Therefore,
* run the recurrence for the mk to get mK with minimal
* | mK | + | mK0 | in the hope mK is 0 .
*/
K = k;
mK = mk;
mK0 = mk0;
while (fabs (mK) > 0)
{
p[K + 1] = -q[K + 1] * p[K];
tmp = mK0 - i[K] * mK;
if (fabs (tmp) > fabs (mK0))
break;
mK0 = mK;
mK = tmp;
K = K + 1;
};
/*
* Then
* x = ( m q - m ) / p
* K K K-1 K
*
* as accurately as one could hope.
*/
x = (mK * q[K] - mK0) / p[K];
/*
* To return z and m0 as positive numbers,
* x must take the sign of m0 .
*/
x = x * sgn (m0);
m0 = fabs (m0);
/*d
* Set z = m0 * 2 ^ (binade+1-D) .
*/
z = ldexp (m0, binade + 1 - D);
/*
* Print z (hex), z (dec), m0 (dec), binade+1-D, x (hex), x (dec).
*/
printf ("%08lx %08lx Z=%22.16E M=%17.17G L+1-%d=%3d %08lx %08lx x=%23.16E\n", hex (z), z, m0, D, binade + 1 - D, hex (x), x);
}
}
}
}
Ответы
Ответ 1
Теория
Сначала обратите внимание на разницу, использующую арифметику с одной точностью.
-
[Уравнение 8] Минимальное значение f
может быть больше. Поскольку числа с двойной точностью являются супер-множеством чисел с одной точностью, ближайший single
до кратного 2/pi
может быть только дальше, чем ~ 2.98e-19
, поэтому число начальных нулей в фиксированной арифметике представление f
должно быть не более 61 ведущих нулей (но, вероятно, будет меньше). Обозначим эту величину fdigits
.
-
[Уравнение до 9] Следовательно, вместо 121 бит y
должен быть точным до fdigits
+ 24 (отличные от нуля значащие бит в одной точности) + 7 (дополнительные защитные биты) = fdigits
+ 31 и не более 92.
-
[Уравнение 9] "Поэтому вместе с шириной показателя x
2/pi
должно содержать 127 (максимальный показатель single
) + 31 + fdigits
или 158 + fdigits
и не более 219 бит.
-
[ПОДРАЗДЕЛ 2.5] Размер A
определяется числом нулей в x
до двоичной точки (и не изменяется на ход до single
), а размер C
определяется уравнением до 9.
- Для больших
x
(x
>= 2 ^ 24), x
выглядит следующим образом: [24 бит, M нулей]. Умножая его на A
, размер которого является первым битом M
2/pi
, приведет к целому числу (нули x
будут просто смещать все в целые числа).
- Выбор
C
для начала с M+d
бит 2/pi
приведет к тому, что продукт x*C
будет иметь размер не более d-24
. В двойной точности d
выбрано равным 174 (а вместо 24 - 53), так что произведение будет иметь размер не более 121. В single
достаточно выбрать d
, чтобы d-24 <= 92
, точнее, d-24 <= fdigits+31
. То есть d
можно выбрать как fdigits
+55 или не более 116.
- В результате
B
должен иметь размер не более 116 бит.
Поэтому мы имеем две проблемы:
- Вычисление
fdigits
. Это связано с чтением ссылки 6 из связанной бумаги и ее пониманием. Не может быть так просто.:) Насколько я вижу, это единственное место, где используется nearpi.c
.
- Вычислить
B
, соответствующие биты 2/pi
. Поскольку M
ограничен снизу на 127, мы можем просто вычислить первые 127 + 116 бит 2/pi
в автономном режиме и сохранить их в массиве. См. Wikipedia.
- Вычисление
y=x*B
. Это включает в себя умножение x
на 116-битное число. Здесь используется Раздел 3. Размер блоков выбирается равным 24, потому что 2 * 24 + 2 (умножение двух 24-битовых чисел и добавление 3 таких чисел) меньше точности double
, 53 (и потому, что 24 делит 96). Мы можем использовать блоки размером 11 бит для арифметики single
по тем же причинам.
Примечание. Трюк с B
применим только к числам, показатели которых положительны (x >= 2 ^ 24).
Подводя итог - во-первых, вам нужно решить проблему с точностью double
. Ваш код Matlab
также не работает в точности double
(попробуйте удалить single
и вычислить sin(2^53)
, потому что ваш twooverpi
имеет только 53 значащих бита, а не 175 (и в любом случае вы не можете напрямую умножать такие точные числа в Matlab). Во-вторых, схема должна быть адаптирована для работы с single
, и снова ключевая проблема представляет собой 2/pi
достаточно точно и поддерживает умножение высокоточных чисел. Наконец, когда все работает, вы можете попытаться найти лучшее fdigits
, чтобы уменьшить количество бит, которое вы должны хранить и умножать.
Надеюсь, что я не совсем ушел - комментарии и противоречия приветствуются.
Пример
В качестве примера давайте вычислим sin(x)
где x = single(2^24-1)
, у которого нет нулей после значимых бит (M
= 0). Это упрощает поиск B
, поскольку B
состоит из первых 116 бит 2/pi
. Поскольку x
имеет точность 24 бит и B
116 бит, продукт
y = x * B
будет иметь 92 бит точности, если требуется.
В разделе 3 в связанном документе описывается, как выполнять этот продукт с достаточной точностью; тот же алгоритм можно использовать с блоками размера 11 для вычисления y
в нашем случае. Будучи тяжелым, я надеюсь, что я извинился за то, что не делал этого явно, вместо этого полагаясь на Matlab
символическую математическую панель инструментов. Этот набор инструментов предоставляет нам функцию vpa
, которая позволяет нам определять точность числа в десятичных разрядах. Итак,
vpa('2/pi', ceil(116*log10(2)))
будет производить приближение 2/pi
не менее 116 бит точности. Поскольку vpa
принимает только целые числа для аргумента точности, мы обычно не можем точно определить двоичную точность числа, поэтому мы используем следующий лучший.
Следующий код вычисляет sin(x)
в соответствии с документом в точности single
:
x = single(2^24-1);
y = x * vpa('2/pi', ceil(116*log10(2))); % Precision = 103.075
k = round(y);
f = single(y - k);
r = f * single(pi) / 2;
switch mod(k, 4)
case 0
s = sin(r);
case 1
s = cos(r);
case 2
s = -sin(r);
case 3
s = -cos(r);
end
sin(x) - s % Expected value: exactly zero.
(Точность y
получается с помощью Mathematica
, который оказался намного лучшим числовым инструментом, чем Matlab
:))
В libm
Другой ответ на этот вопрос (который был удален с тех пор) приводит меня к реализации в libm
, которая, хотя и работает с номерами с двойной точностью, очень тщательно следует связанной статье.
См. файл s_sin.c для обертки (таблица 2 из связанной бумаги отображается как оператор switch
в конце файла) и e_rem_pio2.c для кода сокращения аргументов (особый интерес представляет массив, содержащий первые 396 шестнадцатеричных цифр 2/pi
, начиная с строки 69).