By Long Luo
任何一款科学计算器上都可以计算三角函数,三角函数应用于生活工作中的方方面面,但计算机是如何计算三角函数值的呢?
三角函数本质上是直角三角形的3条边的比例关系 ,计算并没有很直观,那么计算机是如何计算三角函数值的呢?
在微积分中我们学习过 泰勒公式 ,我们知道可以使用泰勒展开式来对某个值求得其近似值,例如:
sin ∠ 1 8 ∘ = 5 − 1 4 ≈ 0.3090169943 \sin \angle 18^{\circ} = \frac{\sqrt {5} – 1}{4} \approx 0.3090169943 sin ∠1 8 ∘ = 4 5 − 1 ≈ 0.3090169943
利用泰勒公式,取前 4 4 4 项:
sin x ≈ x − x 3 3 ! + x 5 5 ! − x 7 7 ! \sin x \approx x – \frac{x^3}{3!} + \frac{x^5}{5!} – \frac{x^7}{7!} sin x ≈ x − 3 ! x 3 + 5 ! x 5 − 7 ! x 7
代入可得:
sin ∠ 1 8 ∘ = sin π 10 = π 10 − 1 6 ( π 10 ) 3 + 1 120 ( π 10 ) 5 − 1 5040 ( π 10 ) 7 ≈ 0.30903399538 \sin \angle 18^{\circ} = \sin \frac{\pi}{10} = \frac{\pi}{10} – \frac{1}{6} (\frac{\pi}{10})^3 + \frac{1}{120} (\frac{\pi}{10})^5 – \frac{1}{5040} (\frac{\pi}{10})^7 \approx 0.30903399538 sin ∠1 8 ∘ = sin 10 π = 10 π − 6 1 ( 10 π ) 3 + 120 1 ( 10 π ) 5 − 5040 1 ( 10 π ) 7 ≈ 0.30903399538
可见取前 4 4 4 项时精度已经在 1 0 − 5 10^{-5} 1 0 − 5 之下,对于很多场合精度已经足够高 了。
在没有了解 CORDIC(Coordinate Rotation Digital Computer) Algorithm 1 算法之前,我一直以为计算器是利用泰勒公式去求解,最近才了解到原来在计算机中,CORDIC 算法远比 泰勒公式高效。
下面我们就来了解下泰勒公式的不足之处和 CORDIC 算法是怎么做的。
泰勒公式的缺点
上一节我们使用泰勒公式去计算三角函数值时,需要做很多次乘法运算,而计算器中乘法运算是很昂贵 的,其缺点如下所示:
展开过小则会导致展开精度不够,展开过大则会导致计算量过大;
幂运算需要使用乘法器,存在很多重复计算;
需要很多变量来存储中间值。
在之前的文章 矩阵乘法的 Strassen 算法 ,就是通过减少乘法,多做加法 ,从而大大降低了运算量,那么我们可以用相同的思想来优化运算吗?
当然可以,让我们请出今天的主角:CORDIC 算法 。
解析 CORDIC 算法
我们知道单位圆上一点 P P P ,其坐标为:( cos θ , sin θ ) (\cos \theta , \sin \theta ) ( cos θ , sin θ ) ,如下图所示:
如果我们接收一个旋转向量 M i n M_{in} M in 逆时针旋转 θ \theta θ ,将点 P ( x i n , y i n ) P(x_{in} , y_{in}) P ( x in , y in ) 旋转到 P ′ ( x R , y R ) P'(x_{R} , y_{R}) P ′ ( x R , y R ) , 如下图所示:
很容易得到如下公式:
x R = x i n c o s ( θ ) − y i n s i n ( θ ) x_R = x_{in} cos(\theta) – y_{in} sin(\theta) x R = x in cos ( θ ) − y in s in ( θ )
y R = x i n s i n ( θ ) + y i n c o s ( θ ) y_R = x_{in} sin(\theta) + y_{in} cos(\theta) y R = x in s in ( θ ) + y in cos ( θ )
实际上由 复数运算 ,我们知道复数乘法就是幅角相加,模长相乘 。我们可以将上式写成下列矩阵运算 形式:
[ x R y R ] = [ cos ( θ ) − sin ( θ ) sin ( θ ) cos ( θ ) ] [ x i n y i n ] \begin{aligned}
\begin{bmatrix} x_{R} \\ y_{R} \end{bmatrix} = \begin{bmatrix} \cos (\theta) & -\sin (\theta) \\ \sin (\theta) & \cos (\theta ) \end{bmatrix} \begin{bmatrix} x_{in} \\ y_{in} \end{bmatrix}
\end{aligned} [ x R y R ] = [ cos ( θ ) sin ( θ ) − sin ( θ ) cos ( θ ) ] [ x in y in ]
但上式运算时,只是对向量 v i n = [ x i n y i n ] v_{in} = {\begin{bmatrix} x_{in} \\ y_{in} \end{bmatrix}} v in = [ x in y in ] 进行了线性变换 ,乘以一个旋转向量 M i n M_{in} M in ,得到了旋转后的结果:向量 v R = [ x R y R ] v_{R} = {\begin{bmatrix} x_{R} \\ y_{R} \end{bmatrix}} v R = [ x R y R ] 。
但是上式仍然需要 4 4 4 次乘法和 2 2 2 次加减法操作, 复杂度没有任何降低,那怎么办呢?
当当…当!
通过上述分析,我们已经知道可以使用有限次旋转 操作来避免复杂的乘法操作,我们修改矩阵运算公式,提取 cos ( θ ) \cos (\theta ) cos ( θ ) ,则公式可以修改为:
[ x R y R ] = c o s ( θ ) [ 1 − t a n ( θ ) t a n ( θ ) 1 ] [ x i n y i n ] \begin{bmatrix} x_{R} \\ y_{R} \end{bmatrix} = cos(\theta) \begin{bmatrix} 1 & -tan(\theta) \\ tan(\theta) & 1 \end{bmatrix} \begin{bmatrix} x_{in} \\ y_{in} \end{bmatrix} [ x R y R ] = cos ( θ ) [ 1 t an ( θ ) − t an ( θ ) 1 ] [ x in y in ]
如果我们选择合适的角度值 θ i \theta_i θ i ,使得
tan ( θ i ) = 2 − i , i = 0 , 1 , … , n \tan (\theta_{i}) = 2^{-i} , i=0, 1,\dots , n tan ( θ i ) = 2 − i , i = 0 , 1 , … , n
这样和 tan ( θ i ) \tan (\theta_{i}) tan ( θ i ) 乘法操作就变成了移位 操作,我们知道计算机中移位 操作是非常快的,就可以大大加快计算速度了。
但这里仍然有 3 3 3 个问题需要解决:
对于任意角度 ,可以通过满足条件的角度累加来得到在数学上相同的结果吗?
每次旋转得到的结果仍然需要乘以 cos ( θ ) \cos(\theta ) cos ( θ ) ,这部分的计算成本如何?如何计算?
因为每次旋转角度 θ = arctan ( 2 − i ) \theta = \arctan(2^{-i}) θ = arctan ( 2 − i ) ,朝着目标角度进行旋转时,可能会出现没有超过目标角度的情况,也会存在超过目标角度的情况,这种情况如何解决呢?
对于第一个问题,答案是否定 的。可以从数学上证明只有 ∠ 4 5 ∘ \angle 45^{\circ} ∠4 5 ∘ 的倍数角才可以得到完全一致的结果。但是在工程应用中,我们只需要满足一定精度 即可,可以增加迭代次数无限逼近原始角度,如下所示提高 n n n 值以无限逼近原始角度。
θ d = ∑ i = 0 n θ i , ∀ tan ( θ i ) = 2 − i \theta_{d} = \sum_{i=0}^{n} \theta_{i} , \forall \tan(\theta_{i}) = 2^{-i} θ d = i = 0 ∑ n θ i , ∀ tan ( θ i ) = 2 − i
对于第二个问题,我们先来个例子,以 57.53 5 ∘ 57.535^{\circ} 57.53 5 ∘ 为例来看看求解过程:
57.53 5 ∘ = 4 5 ∘ + 26.56 5 ∘ − 14.0 3 ∘ 57.535^{\circ} = 45^{\circ}+26.565^{\circ}-14.03^{\circ} 57.53 5 ∘ = 4 5 ∘ + 26.56 5 ∘ − 14.0 3 ∘
那么第一次旋转:
[ x 0 y 0 ] = c o s ( 4 5 ∘ ) [ 1 − 1 1 1 ] [ x i n y i n ] \begin{bmatrix} x_{0} \\ y_{0} \end{bmatrix} = cos(45^{\circ}) \begin{bmatrix} 1 & -1 \\ 1 & 1 \end{bmatrix} \begin{bmatrix} x_{in} \\ y_{in} \end{bmatrix} [ x 0 y 0 ] = cos ( 4 5 ∘ ) [ 1 1 − 1 1 ] [ x in y in ]
第二次旋转:
[ x 1 y 1 ] = c o s ( 26.56 5 ∘ ) [ 1 − 2 − 1 2 − 1 1 ] [ x 0 y 0 ] \begin{bmatrix} x_{1} \\ y_{1} \end{bmatrix} = cos(26.565^{\circ}) \begin{bmatrix} 1 & -2^{-1} \\ 2^{-1} & 1 \end{bmatrix} \begin{bmatrix} x_{0} \\ y_{0} \end{bmatrix} [ x 1 y 1 ] = cos ( 26.56 5 ∘ ) [ 1 2 − 1 − 2 − 1 1 ] [ x 0 y 0 ]
第三次旋转:
[ x 2 y 2 ] = c o s ( − 14.0 3 ∘ ) [ 1 2 − 2 − 2 − 2 1 ] [ x 1 y 1 ] \begin{bmatrix} x_{2} \\ y_{2} \end{bmatrix} = cos(-14.03^{\circ}) \begin{bmatrix} 1 & 2^{-2} \\ -2^{-2} & 1 \end{bmatrix} \begin{bmatrix} x_{1} \\ y_{1} \end{bmatrix} [ x 2 y 2 ] = cos ( − 14.0 3 ∘ ) [ 1 − 2 − 2 2 − 2 1 ] [ x 1 y 1 ]
综合可得:
[ x 2 y 2 ] = c o s ( 4 5 ∘ ) c o s ( 26.56 5 ∘ ) c o s ( − 14.0 3 ∘ ) [ 1 − 1 1 1 ] [ 1 − 2 − 1 2 − 1 1 ] [ 1 2 − 2 − 2 − 2 1 ] [ x i n y i n ] \begin{bmatrix} x_{2} \\ y_{2} \end{bmatrix} = cos(45^{\circ}) cos(26.565^{\circ}) cos(-14.03^{\circ}) \begin{bmatrix} 1 & -1 \\ 1 & 1 \end{bmatrix} \begin{bmatrix} 1 & -2^{-1} \\ 2^{-1} & 1 \end{bmatrix} \begin{bmatrix} 1 & 2^{-2} \\ -2^{-2} & 1 \end{bmatrix} \begin{bmatrix} x_{in} \\ y_{in} \end{bmatrix} [ x 2 y 2 ] = cos ( 4 5 ∘ ) cos ( 26.56 5 ∘ ) cos ( − 14.0 3 ∘ ) [ 1 1 − 1 1 ] [ 1 2 − 1 − 2 − 1 1 ] [ 1 − 2 − 2 2 − 2 1 ] [ x in y in ]
因为 tan ( θ i ) = 2 − i \tan (\theta_{i}) = 2^{-i} tan ( θ i ) = 2 − i ,由三角公式可以计算出:
cos ( θ i ) = 1 1 + tan 2 ( θ i ) = 1 1 + 2 − 2 i \cos(\theta_{i}) = \frac {1}{\sqrt {1 + \tan ^{2}(\theta_{i})}} = \frac {1}{\sqrt {1 + 2^{-2i}}} cos ( θ i ) = 1 + tan 2 ( θ i ) 1 = 1 + 2 − 2 i 1
令 K i = cos ( θ i ) K_i = \cos(\theta_{i}) K i = cos ( θ i ) ,则当进行 n n n 次迭代之后:
K ( n ) = ∏ i = 0 n − 1 K i = ∏ i = 0 n − 1 1 1 + 2 − 2 i K(n) = \prod _{i=0}^{n-1}K_{i} = \prod _{i=0}^{n-1}{\frac {1}{\sqrt {1 + 2^{-2i}}}} K ( n ) = i = 0 ∏ n − 1 K i = i = 0 ∏ n − 1 1 + 2 − 2 i 1
当 θ i \theta_{i} θ i 越来越小时, cos θ \cos \theta cos θ 也越来越逼近 1 1 1 ,当迭代次数 n → ∞ n \to \infty n → ∞ , K ( n ) K(n) K ( n ) 极限存在,求解可得:
K = lim n → ∞ K ( n ) ≈ 0.6072529350088812561694 K = \lim _{n \to \infty }K(n) \approx 0.6072529350088812561694 K = n → ∞ lim K ( n ) ≈ 0.6072529350088812561694
由 K K K 我们实际上可以得到最终的向量 v R v_R v R 的模长极限为:
A = 1 K = lim n → ∞ ∏ i = 0 n − 1 1 + 2 − 2 i ≈ 1.64676025812107 A = {\frac {1}{K}} = \lim _{n \to \infty } \prod _{i=0}^{n – 1}{\sqrt {1 + 2^{-2i}}} \approx 1.64676025812107 A = K 1 = n → ∞ lim i = 0 ∏ n − 1 1 + 2 − 2 i ≈ 1.64676025812107
实际上当迭代次数为 6 6 6 时,可以计算出缩放比例 K K K ,就已经精确到 0.6072 0.6072 0.6072 了,如下所示:
K ≈ c o s ( 4 5 ∘ ) c o s ( 26.56 5 ∘ ) × ⋯ × c o s ( 0.89 5 ∘ ) = 0.6072 K \approx cos(45^{\circ}) cos(26.565^{\circ}) \times \dots \times cos(0.895^{\circ}) = 0.6072 K ≈ cos ( 4 5 ∘ ) cos ( 26.56 5 ∘ ) × ⋯ × cos ( 0.89 5 ∘ ) = 0.6072
实际上,任意角度只要迭代次数超过 6 6 6 ,我们可以直接使用 K = 0.6072 K = 0.6072 K = 0.6072 这个值。
对于第三个问题,稍微有点复杂,我们在下一节继续讲解!
角度累加
上一节遗留的问题是迭代旋转角度时,旋转角度不一定会落在目标角度内,我们需要引入一个角度误差,用来衡量旋转角度和目标角之间距离,如下所示:
θ e r r o r = θ d − ∑ i = 0 n θ i \theta_{error} = \theta_d – \sum_{i=0}^{n} \theta_{i} θ error = θ d − i = 0 ∑ n θ i
当 θ e r r o r > 0 \theta_{error} > 0 θ error > 0 时,我们应该逆时针旋转,而 θ e r r o r < 0 \theta_{error} < 0 θ error < 0 ,则顺时针旋转。根据精度需要,当 ∣ θ e r r o r ∣ ≤ ϵ \left | \theta_{error} \right | \le \epsilon ∣ θ error ∣ ≤ ϵ 即可退出迭代。
同时我们修改之前的公式,引入 σ i ∈ { + 1 , − 1 } \sigma_{i} \in \left \{ +1, -1 \right \} σ i ∈ { + 1 , − 1 } ,于是可以得到最终公式:
x [ i + 1 ] = x [ i ] − σ i 2 − i y [ i ] x \left [ i+1 \right ] = x \left [ i \right ] – \sigma_{i} 2^{-i} y \left [ i \right ] x [ i + 1 ] = x [ i ] − σ i 2 − i y [ i ]
y [ i + 1 ] = y [ i ] + σ i 2 − i x [ i ] y \left [ i+1 \right ] = y \left [ i \right ] + \sigma_{i} 2^{-i} x \left [ i \right ] y [ i + 1 ] = y [ i ] + σ i 2 − i x [ i ]
z [ i + 1 ] = z [ i ] − σ i t a n − 1 ( 2 − i ) z \left [ i+1 \right ] = z \left [ i \right ] – \sigma_{i} tan^{-1} ( 2^{-i} ) z [ i + 1 ] = z [ i ] − σ i t a n − 1 ( 2 − i )
举个例子
上面讲了这么多,来个实例吧,练习巩固下知识,看看自己是否真的懂了?
计算 sin 7 0 ∘ \sin 70^{\circ} sin 7 0 ∘ 和 cos 7 0 ∘ \cos 70^{\circ} cos 7 0 ∘ 的值。
从 x i n = 1 , y i n = 0 x_{in}=1, y_{in} = 0 x in = 1 , y in = 0 开始,迭代 6 6 6 次结果如下:
第 i i i 次迭代
σ i \sigma_{i} σ i
x [ i ] x \left[ i \right ] x [ i ]
y [ i ] y \left[ i \right ] y [ i ]
z [ i ] z \left[ i \right ] z [ i ]
–
–
1 1 1
0 0 0
7 0 ∘ 70^{\circ} 7 0 ∘
0 0 0
1 1 1
1 1 1
1 1 1
2 5 ∘ 25^{\circ} 2 5 ∘
1 1 1
1 1 1
0.5 0.5 0.5
1.5 1.5 1.5
− 1.565 1 ∘ -1.5651^{\circ} − 1.565 1 ∘
2 2 2
− 1 -1 − 1
0.875 0.875 0.875
1.375 1.375 1.375
12.471 1 ∘ 12.4711^{\circ} 12.471 1 ∘
3 3 3
1 1 1
0.7031 0.7031 0.7031
1.4844 1.4844 1.4844
5.346 1 ∘ 5.3461^{\circ} 5.346 1 ∘
4 4 4
1 1 1
0.6103 0.6103 0.6103
1.5283 1.5283 1.5283
1.769 8 ∘ 1.7698^{\circ} 1.769 8 ∘
5 5 5
1 1 1
0.5625 0.5625 0.5625
1.5474 1.5474 1.5474
− 0.020 1 ∘ -0.0201^{\circ} − 0.020 1 ∘
6 6 6
− 1 -1 − 1
0.5867 0.5867 0.5867
1.5386 1.5386 1.5386
0.875 1 ∘ 0.8751^{\circ} 0.875 1 ∘
迭代到第 6 6 6 次时,角度误差已经小于 1 ∘ 1^{\circ} 1 ∘ 了, 通过表格可知:
x R = 0.6072 × 0.5867 = 0.3562 x_{R} = 0.6072 \times 0.5867 = 0.3562 x R = 0.6072 × 0.5867 = 0.3562
y R = 0.6072 × 1.5386 = 0.9342 y_{R} = 0.6072 \times 1.5386 = 0.9342 y R = 0.6072 × 1.5386 = 0.9342
通过计算器可知,cos ( 7 0 ∘ ) = 0.34202 \cos(70^{\circ}) = 0.34202 cos ( 7 0 ∘ ) = 0.34202 ,sin ( 7 0 ∘ ) = 0.93969 \sin(70^{\circ}) = 0.93969 sin ( 7 0 ∘ ) = 0.93969 ,误差已经在 1 100 \frac {1}{100} 100 1 之下了,实际应用中我们会迭代 16 16 16 次,误差会非常小 。
在线 CORDIC 算法Demo
通过上面分析,我们已经知道了 CORDIC 算法的原理,下面就开始编程吧!
用 JavaScript 写了一个在线互动版本,传送门 → :
可以调整不同迭代次数,和系统库函数 Math.cos \textit{Math.cos} Math.cos ,Math.sin \textit{Math.sin} Math.sin 进行比较:
可以查看每次迭代的结果,掌握 Cordic 算法迭代原理:
CORDIC 算法的优点
CORDIC 算法相比其他算法的优点,体现在以下几个方面:
简化运算:CORDIC 算法主要使用位移、加法和减法等简单的运算,避免了复杂的乘法操作,从而提高了计算速度;
并行计算:CORDIC 算法的迭代操作是相互独立的,可以进行并行计算,利用现代计算机的多核优势,进一步提升计算效率;
硬件优化:CORDIC 算法适用于硬件实现,可以通过专用硬件电路(如FPGA)进行加速,极大地提高计算速度;
低存储需求:CORDIC 算法只需存储一小组预先计算好的旋转角度,节省了存储空间;
迭代控制:通过控制迭代次数,可以平衡计算精度和计算速度,根据需求进行调整。
总结
CORDIC 算法是一种高效计算三角函数值的方法。相比传统的泰勒展开式,它具有简单高效、低存储需求和迭代控制等优势。在需要计算三角函数值的应用中,CORDIC 算法更快速、更节省资源。
博客网站原文链接:www.longluo.me/blog/2023/0…
参考文献
CORDIC