Amethyst Studio
2104 words
11 minutes
计算机是如何计算三角函数的

近0端多项式逼近#

sin(x)的泰勒展开:

sin(x)=x13!x3+15!x517!x7+19!x9+o(x9)\sin(x) = x - \frac{1}{3!}x^3+\frac{1}{5!}x^5-\frac{1}{7!}x^7+\frac{1}{9!}x^9+o(x^9)

cos(x)的泰勒展开:

cos(x)=112!x2+14!x416!x6+18!x8+o(x8)\cos(x) = 1 - \frac{1}{2!}x^2+\frac{1}{4!}x^4-\frac{1}{6!}x^6+\frac{1}{8!}x^8+o(x^8)

当然,因为展开项数限制和数的精度限制,x的范围只在靠近0的时候才有比较高的精度。

float sinf_poly (float x) {
    float r, t, s = x * x;
    r =         2.86567956e-6f; //  0x1.80a000p-19
    r = r * s - 1.98559923e-4f; // -0x1.a0690cp-13
    r = r * s + 8.33338592e-3f; //  0x1.111182p-07
    r = r * s - 1.66666672e-1f; // -0x1.555556p-03
    t = x * s + 0.0f; // ensure -0 is passed through
    r = r * t + x;
    return r;
}

float cosf_poly (float x) {
    float r, s = x * x;
    r =         2.44677067e-5f;
    r = r * s - 1.38877297e-3f;
    r = r * s + 4.16666567e-2f;
    r = r * s - 5.00000000e-1f;
    r = r * s + 1.00000000e+0f;
    return r;
}

因此,需要进行Reduction,这是这一篇的重点。

Reduction#

Cody-Waite Reduction#

因为上面的多项式要在靠近0的时候才会有比较高的精度,因此,就需要把x归约到0附近。理论上来说,我们实际上只需要把x归约到[π2,π2][-\frac{\pi}{2}, \frac{\pi}{2}]附近即可。数学上的方法为:

x=xxπ2×π2x' =x - \lfloor \frac{x}{\frac{\pi}{2}} \rfloor \times \frac{\pi}{2}

例如,当x = 16时,有如下的计算过程:

x=16.0xπ/210.1859xπ/2=10xπ/2×π2=15.7079xxπ/2×π2=0.2920\begin{aligned} &x = 16.0 \\ &\frac{x}{\pi/2} \approx 10.1859 \\ &\lfloor \frac{x}{\pi/2} \rfloor = 10 \\ &\lfloor \frac{x}{\pi/2} \rfloor \times \frac{\pi}{2} = 15.7079 \\ &x - \lfloor \frac{x}{\pi/2} \rfloor \times \frac{\pi}{2} = 0.2920 \end{aligned}

注意这里xπ/2=10\lfloor \frac{x}{\pi/2} \rfloor = 10,这也就意味着我们得到的xx'xx有:

sin(x)=sin(x+10×π2)=sin(x+π)=sin(x)\sin(x) = \sin(x'+10\times \frac{\pi}{2}) = \sin(x'+\pi) = -\sin(x')

因此,将x'代入到sin的多项式中,再取负号即可。

再举一个例子,当x=18时,我们有xπ/2=11\lfloor \frac{x}{\pi/2} \rfloor = 11,此时xx'xx有:

sin(x)=sin(x+11×π2)=sin(x+3π2)=sin(x+π2)=cos(x)\sin(x) = \sin(x'+11\times \frac{\pi}{2}) = \sin(x'+\frac{3\pi}{2}) = -\sin(x'+\frac{\pi}{2}) = -\cos(x')

因此此时,是把x'代入到cos的多项式中,再取负号。

按照上面的理论,可以有代码:

float j; int q;
j = a * 6.36619747e-1f + 1.2582912e+7f;  // 6.36619747e-1即为2/pi的近似值
j = j - 1.25829120e+7f;
r = a - j * 1.57078552e+00f;
r = r - j * 1.08043314e-05f;
r = r - j * 2.56334407e-12f;
q = (int)j;

不过可惜的是,如果给的x过大的话,上面的代码会失效。其中的一个重点问题是通过单精度浮点数0x3f22f983表示的2/π2/\pi精度是不够的,假设这里的x的值是100000000,那么算出来的j的结果是63661976,但是实际上,如果换用double的话,可以算出j的实际值为63661977,这样一来就造成了偏差。因此,对于大数而言,我们必须另外想办法。

Payne-Hanek Reduction#

对于比较大的数,Cody-Waite的算法就会失效,主要的问题在于Cody-Waite算法只使用到固定精度的浮点数,对于超大的数精度就会不够。1983年Payne与Hanek提出把浮点运算转换为大整数运算,这样来解决比较大型的浮点数的归约问题。但相对于Cody-Waite算法来说,Payne-Hanek算法相当不好理解。需要一些数学上的推理才可以表述清楚。

Mathematical representation of Cody-Waite#

对于给定的数xx我们希望把xx给归约到CC中,那么我们使用的公式为:

xkC, where k=xCx - kC, \text{ where } k = \lfloor \frac{x}{C}\rfloor

Mathematical representation of Payne-Hanek#

Payne与Hanek的论文中主要是针对三角函数的归约问题,这一点很重要,如果不是三角函数的话是不能用这个算法的,也就是说,现在给定xx我们希望计算:

y=xkC, where k=xC,C=π2(1)y = x - kC, \text{ where } k = \lfloor \frac{x}{C}\rfloor, C = \frac{\pi}{2} \tag{1}

对于IEEE754下的浮点数xx,我们规定xx的阶码exe_x,尾数位pp,那么xx的尾数XXxx有如下的数学关系:

x=X×2expx = X \times 2 ^ {e_x - p }

or:

X=x×2pex(2)X = x\times 2^{p-e_x} \tag{2}

例如对于一个float数384,1.12×281.1_2\times 2^8,十进制表示为384.0,十六进制表示为0x43c00000,它的尾数是0x400000,但是根据IEE754标准,我们知道要填一个1,所以它的尾数就要变成0xc00000,也就是12582912。如果直接运算的话就是:

X=384×2238=12582912X = 384 \times 2^{23 - 8} = 12582912

现在我们把(2)给代入到(1)当中,可以得到:

y=X×2expkC(3)y = X\times 2^{e_x-p} - kC \tag{3}

然后把C=π/2C=\pi/2代入,把(3)式子变形成:

y=π2(2π2expXk)(4)y = \frac{\pi}{2}(\frac{2}{\pi} 2^{e_x - p}X - k) \tag{4}

接下来的问题是我们怎么去处理这个2/π×2exp2/\pi \times 2^{e_x - p},我们把2/π2/\pi的200位二进制写在下面:

0.10100010111110001100000110110111001001110010001000001010
100101001111111000010011101010111110100011111010100110100
110111011100000011011011011000101001010110011001001111000
100001110010000010000011111111001010001011...

我们把这个2/π2/\pi,看成如下的形式:

0.v1v2v3v4v5...0.v_1v_2v_3v_4v_5...

这样一来我们就有如下的数学表达式:

2π=v121+v222+...=i=1vi2i, where vi=0 or 1(5)\frac{2}{\pi} = v_12^{-1} + v_22^{-2}+... = \sum_{i=1}^\infty v_i2^{-i}, \text{ where } v_i = 0 \text{ or } 1 \tag{5}

然后我们把(5)式再代入带(4)当中:

y=i=1viπ22expiXπ2k=i=1vi2expi1Xπ2ky = \sum_{i=1}^\infty v_i\frac{\pi}{2} 2^{e_x-p-i}X - \frac{\pi}{2}k =\sum_{i=1}^\infty v_i 2^{e_x-p-i-1}X - \frac{\pi}{2}k

我们考察其中连加式的第i项,是vi2expi1v_i 2^{e_x-p-i-1},这里的viv_i要么是0要么是1,当然我们不去理会它是0的情况,只考虑它是1,那么这一项就变成了2expi1π2^{e_x-p-i-1}\pi。注意回顾一下我们本来的目的,我们的目的是要把一个大数xx进行归约。但是要针对三角函数。三角函数都是以2π2\pi为周期的周期函数。那么如果2expi1π2^{e_x-p-i-1}\pi2π2\pi的倍数,自然也就不需要计算了,换句话说,对于iexp1i\leq e_x-p-1的项而言,我们是可以不必计算的,只需要计算iexp1i\ge e_x-p-1之后的项即可。

let m=exp1 change the formula to i=1vi2miXπ2k\text{let } m = e_x -p -1 \text{ change the formula to } \sum_{i=1}^\infty v_i 2^{m-i}X - \frac{\pi}{2}k

我们先假定m>0m > 0,大到这个连加式的前几项mim-i都是整数,那么把这个式子变成:

y=i=1m1vi2miX+i=mvi2miXπ2ky = \sum_{i=1}^{m-1} v_i 2^{m-i}X +\sum_{i=m}^{\infty} v_i 2^{m-i}X - \frac{\pi}{2}k

这个式子中的第一个连加项必定是2π2\pi的倍数,因此我们直接用一个2Nπ2N\pi来代替就好,仅仅考虑后面的项:

y=i=1m1vi2miX+i=mvi2miXπ2k=2Nπ+i=mvi2miXπ2ky = \sum_{i=1}^{m-1} v_i 2^{m-i}X +\sum_{i=m}^{\infty} v_i 2^{m-i}X - \frac{\pi}{2}k = 2N\pi+\sum_{i=m}^{\infty} v_i 2^{m-i}X-\frac{\pi}{2}k

对于中间的项,实际当中我们不可能加到无穷项,假设我们只加到m+lm+l项,那么我们做如下的处理:

i=mm+lvi2miX=2(m+l)2m+li=mm+lvi2miX=2(m+l)i=mm+lvi22m+liX\sum_{i=m}^{m+l} v_i 2^{m-i}X = 2^{-(m+l)}2^{m+l} \sum_{i=m}^{m+l} v_i 2^{m-i}X = 2^{-(m+l)}\sum_{i=m}^{m+l} v_i 2^{2m+l-i}X

现在我们来关注一个问题,这个公式的含义:

i=mm+lvi22m+li\sum_{i=m}^{m+l} v_i 2^{2m+l-i}

实际上代表了π/2\pi/2从第m位开始,到第m+lm+l位为止的二进制整数。这个整数乘以XX,再乘以2(m+l)2^{-(m+l)},可以得到一个数,让这个kk取这个数的整数部分,那么这个数的小数部分乘以π/2\pi/2就是我们要求的yy值了。

Example:#

x=1.12×230x=1.1_2\times 2^{30}经过Paney-Hanek算法归约后的数。经过上述的论证,我们可以得到以下的一些变量:

x=1.12×230=1.5×230{ex=30p=23X=12582912m=exp1=6x = 1.1_2\times 2^{30} = 1.5\times 2^{30} \Rightarrow \begin{cases} e_x = 30 \\ p = 23 \\ X = 12582912 \\ m = e_x-p-1=6 \end{cases}

因此我们从π/2\pi/2的第6个小数位开始取,连续取64位,取的位数必须超过p的值,否则最后算的时候会发现没有小数部分。取得位数越多越精确。这里是1011111000110000011011011100100111001000100000101010010100111111,它的十进制值是13704574379508278591,然后我们可以得到一个GG

G=13704574379508278591264G = \frac{13704574379508278591}{2^{64}}

然后计算G×XG\times X,得到37392713.36464738,我们只要令k=37392713,那么小数部分0.36464738乘以π/2\pi/2,就可以得到0.5727867650793,这也就是我们要求的y值。


不过上面的论述有一点不足,就是上面我们是假定m>0m>0的,有的时候xx也可能没有达到一定程度,因而mm也可能小于0,我们重新来论述一下:

y=i=1vi2miXπ2ky = \sum_{i=1}^\infty v_i 2^{m-i}X - \frac{\pi}{2}k

这里面的mm小于0,将其用n-n来替代:

y=i=1vi2miXπ2k=i=1vi2niXπ2k=2ni=1vi2iXπ2k\begin{aligned} y &= \sum_{i=1}^\infty v_i 2^{m-i}X - \frac{\pi}{2}k \\ &=\sum_{i=1}^\infty v_i 2^{-n-i}X - \frac{\pi}{2}k \\ &=2^{-n}\sum_{i=1}^\infty v_i 2^{-i}X - \frac{\pi}{2}k \end{aligned}

上面的公式表明我们取数的时候要从2/π2/\pi的第一个小数点就要开始取了,假定我们往后取ll项:

y=2ni=1lvi2iXπ2k=2n2l2li=1lvi2iXπ2k=2(n+l)i=1lvi2liXπ2k\begin{aligned} y &= 2^{-n}\sum_{i=1}^l v_i 2^{-i}X - \frac{\pi}{2}k \\ &= 2^{-n}2^l2^{-l}\sum_{i=1}^l v_i 2^{-i}X - \frac{\pi}{2}k \\ & = 2^{-(n+l)}\sum_{i=1}^l v_i 2^{l-i}X - \frac{\pi}{2}k \end{aligned}

这里面的i=1lvi2li\sum_{i=1}^l v_i 2^{l-i}就代表着2/π2/\pi从第1个小数位到第ll个小数位的整数,然后这个整数要除以2(n+l)2^{-(n+l)},乘以XX后得到的数跟先前类似,得到的小数部分与π/2\pi/2相乘即可得到最终的归约值。

Example#

来看先前x=384x=384的例子:

x=384{ex=8p=23X=12582912n=(8231)=16x = 384 \Rightarrow \begin{cases} e_x = 8 \\ p = 23 \\ X = 12582912 \\ n = -(8-23-1) = 16\\ \end{cases}

l=48l=48,这样n+l=64n+l=64,取出的数为10100010111110001100000110110111001001110010001000001010,十进制表示为179189285594914,然后运算:

G×X=179189285594914×12582912263G \times X = \frac{179189285594914 \times 12582912 }{2^{63}}

注意这里是63。

然后可以得到小数部分为:0.457558794,乘以π/2\pi/2得到0.718731673,非常接近我们要计算的值。

计算机是如何计算三角函数的
https://ziyue.cafe/posts/how-computer-compute-trig-functions/
Author
Kaida Amethyst
Published at
2023-04-13