C如何计算sin()和其他math函数?
我一直在通过.NET的反汇编和GCC源代码,但似乎无法findsin()
和其他math函数的实际执行…他们总是似乎引用别的东西。
任何人都可以帮我find他们? 我觉得C运行的所有硬件不太可能支持硬件触发function,所以必须有一个软件algorithm,对吧?
我意识到可以计算函数的几种方法,并且已经编写了我自己的例程来使用泰勒级数来计算函数。 我很好奇真正的生产语言是怎么做的,因为我的所有实现总是慢几个数量级,尽pipe我觉得我的algorithm很聪明(显然他们不是)。
在GNU libm中, sin
的实现是依赖于系统的。 因此,您可以在每个平台的适当的sysdeps子目录中find相应的实现。
一个目录包含C中的一个实现,由IBM提供。 自2011年10月以来,这是在典型的x86-64 Linux系统上调用sin()
时实际运行的代码。 它显然比fsin
汇编指令更快。 源代码: sysdeps / ieee754 / dbl-64 / s_sin.c ,查找__sin (double x)
。
这段代码非常复杂。 没有一种软件algorithm在x值的整个范围内尽可能快并且准确,所以库实现许多不同的algorithm,其第一个工作是查看x并确定使用哪个algorithm。 在一些地区,它使用了看似熟悉的泰勒级数。 几个algorithm首先计算一个快速结果,然后如果这不够准确,丢弃它,并回落一个较慢的algorithm。
GCC / glibc的老版本的32位版本使用了fsin
指令,这对于某些input来说令人惊讶地不准确。 有一篇引人入胜的博客文章仅用两行代码就可以说明这一点 。
纯C的fdlibm的实现比glibc简单得多,并且很好的评论。 源代码: fdlibm / s_sin.c和fdlibm / k_sin.c
好的小子,专业的时间….这是我最没有经验的软件工程师投诉之一。 他们从头开始计算先验函数(使用泰勒的系列),好像从来没有人在他们的生活中做过这些计算。 不对。 这是一个明确的问题,已经被非常聪明的软件和硬件工程师接近数千次,并且有一个明确的解决scheme。 基本上,大部分超越函数都是用切比雪夫多项式来计算的。 至于使用哪个多项式取决于具体情况。 首先,关于这个问题的圣经是哈特和切尼的一本名为“计算机近似”的书。 在这本书中,您可以决定是否有硬件加法器,乘法器,除法器等,并决定哪些操作是最快的。 例如,如果你有一个非常快的分频器,计算正弦的最快方法可能是P1(x)/ P2(x),其中P1,P2是切比雪夫多项式。 如果没有快速分频器,它可能只是P(x),其中P有比P1或P2多得多的条件…所以它会更慢。 所以,第一步是确定你的硬件和它可以做什么。 然后,你select适当的切比雪夫多项式组合(例如,对于余弦,其forms通常是cos(ax)= aP(x),其中P是Chebyshev多项式)。 然后你决定你想要的小数精度。 例如,如果你想要7位数的精度,你可以在我提到的那本书的相应表中查看,它会给你(精度= 7.33)N = 4和多项式数3502. N是多项式(所以它是p4.x ^ 4 + p3.x ^ 3 + p2.x ^ 2 + p1.x + p0),因为N = 4。 然后你在3502下面的书的后面查找p4,p3,p2,p1,p0值的实际值(它们将处于浮点状态)。 然后你用软件实现你的algorithm,forms为:(((p4.x + p3).x + p2).x + p1).x + p0 ….这就是如何计算余弦到7位十进制在那个硬件上的地方。
请注意,FPU中的超越操作的大多数硬件实现通常涉及一些微代码和这样的操作(取决于硬件)。 切比雪夫多项式用于绝大多数超越,但不是全部。 例如,使用查找表首先使用牛顿拉斐逊方法的双重迭代,平方根更快。 再次,这本书“计算机近似”会告诉你。
如果你打算实施这些function,我会向任何人推荐他们获得该书的副本。 这真的是这些algorithm的圣经。 请注意,有一些替代方法来计算这些值,如cordics等,但这些往往是最好的具体algorithm,你只需要低精度。 为了保证每一次的精度,切比雪夫多项式是要走的路。 就像我说的那样,明确的问题。 现在已经解决了50年了…那是怎么做的。
这就是说,有些技术可以用Chebyshev多项式来得到一个低阶多项式的单精度结果(就像上面的余弦的例子)。 然后,还有其他技术可以在值之间进行插值,以提高精度,而无需使用更大的多项式,例如“Gal's Accurate Tables Method”。 后一种技术是指ACM文献所指的是后者。 但最终,切比雪夫多项式是用来获得90%的方式。
请享用。
正弦和余弦等function在微处理器内部以微码forms实现。 例如,英特尔芯片有这些组装说明。 AC编译器将生成调用这些汇编指令的代码。 (相比之下,Java编译器不会,Java会用软件而不是硬件来评估trig函数,所以运行起来要慢得多。)
芯片不使用泰勒级数来计算三angular函数,至less不是完全的。 首先,他们使用CORDIC ,但他们也可能使用一个短的泰勒级数来研究CORDIC的结果,或者用于特殊情况,例如以非常小的angular度以高的相对精度计算正弦。 有关更多解释,请参阅此StackOverflow答案 。
是的,也有计算sin
软件algorithm。 基本上,用数字计算机计算这些东西通常是用数值方法来完成的,比如近似代表函数的泰勒级数 。
数值方法可以将函数逼近任意精度,并且由于浮点数的精度是有限的,所以它们很适合这些任务。
这是一个复杂的问题。 类似Intel的CPU具有sin()
函数的硬件实现,但是它是x87 FPU的一部分,在64位模式(其中使用SSE2寄存器)中不再使用。 在该模式下,使用软件实现。
这里有几个这样的实现。 一个在fdlibm中 ,在Java中使用。 据我所知,glibc实现包含fdlibm的一部分,以及IBM提供的其他部分。
诸如sin()
类的超越函数的软件实现通常使用通常从泰勒级数得到的多项式的逼近。
使用泰勒系列,并试图找出系列术语之间的关系,所以你不要一次又一次地计算事情
这里是一个cosinus的例子:
double cosinus(double x,double prec) { double t , s ; int p; p = 0; s = 1.0; t = 1.0; while(fabs(t/s) > prec) { p++; t = (-t * x * x) / ((2 * p - 1) * (2 * p)); s += t; } return s;}
使用这个我们可以使用已经使用的(我们避免阶乘和x ^ 2p)得到总和的新项,
对于具体的sin
,使用泰勒展开会给你:
sin(x):= x – x ^ 3/3! + x ^ 5/5! – x ^ 7/7! + …(1)
你会不断添加条件,直到它们之间的差异低于公认的公差水平,或者只是有限的步骤(更快,但不太精确)。 一个例子是这样的:
float sin(float x) { float res=0, pow=x, fact=1; for(int i=0; i<5; ++i) { res+=pow/fact; pow*=-1*x*x; fact*=(2*(i+1))*(2*(i+1)+1); } return res; }
注意:(1)因为小angular度的近似sin(x)= x而起作用。 对于更大的angular度,你需要计算越来越多的术语来获得可接受的结果。 您可以使用while参数并继续保持一定的准确性:
double sin (double x){ int i = 1; double cur = x; double acc = 1; double fact= 1; double pow = x; while (fabs(acc) > .00000001 && i < 100){ fact *= ((2*i)*(2*i+1)); pow *= -1 * x*x; acc = pow / fact; cur += acc; i++; } return cur; }
在另一个答案中提到的切比雪夫多项式是函数和多项式之间的最大差异尽可能小的多项式。 这是一个很好的开始。
在某些情况下,最大误差不是你感兴趣的,而是最大相对误差。 例如对于正弦函数,x = 0附近的误差应该比较大的值小得多; 你想要一个小的相对误差。 所以你可以计算sin x / x的Chebyshev多项式,并用x乘以该多项式。
接下来,你必须弄清楚如何评估多项式。 你想评估它的中间值是小的,因此舍入误差很小。 否则舍入误差可能会比多项式中的误差大得多。 而像正弦函数这样的函数,如果你不小心的话,即使当x <y时,你计算sin x的结果可能大于sin y的结果。 所以需要仔细select计算顺序和舍入误差的上限计算。
例如,sin x = x – x ^ 3/6 + x ^ 5/120 – x ^ 7/5040 …如果您计算天真sin x = x *(1 – x ^ 2/6 + x ^ 4 / 120 – x ^ 6/5040 …),那么圆括号中的函数正在减less,而且如果y是下一个较大的数x,那么有时sin y将会比sin x小。 相反,计算sin x = x – x ^ 3 *(1/6 – x ^ 2/120 + x ^ 4/5040 …),这是不可能发生的。
在计算切比雪夫多项式时,例如,通常需要将系数舍入为双精度。 但是,尽pipeChebyshev多项式是最优的,但是具有被舍入到双精度的系数的Chebyshev多项式不是具有双精度系数的最优多项式!
例如,对于sin(x),需要x,x ^ 3,x ^ 5,x ^ 7等系数,您可以执行以下操作:使用多项式计算sin x的最佳近似值(ax + bx ^ 3 + cx ^ 5 + dx ^ 7)的精度高于双精度,则圆a为双精度,给出A,A与A之间的差异将相当大。 现在使用多项式(bx ^ 3 + cx ^ 5 + dx ^ 7)计算(sin x – Ax)的最佳逼近。 你得到不同的系数,因为它们适应了a和a之间的差异。圆b到双精度b。然后用一个多项式cx ^ 5 + dx ^ 7近似(sin x – Ax – Bx ^ 3)等等。 你将得到一个与原Chebyshev多项式几乎一样好的多项式,但比Chebyshev要好得多,这个舍入到双精度。
接下来你应该考虑多项式select中的舍入误差。 您find了忽略舍入误差的多项式中具有最小误差的多项式,但要优化多项式加舍入误差。 一旦你有切比雪夫多项式,你可以计算舍入误差的界限。 说f(x)是你的函数,P(x)是多项式,E(x)是舍入误差。 你不想优化| f(x) – P(x)|,您要优化| f(x)-P(x)+/- E(x)|。 你会得到一个略微不同的多项式,在舍入误差很大的情况下试图保持多项式误差下降,并且在舍入误差很小的情况下放松多项式误差。
所有这些都可以轻易地将最后一位的0.55倍的错误四舍五入,其中+, – ,*,/的舍入错误至多是最后一位的0.50倍。
库函数的实际实现取决于特定的编译器和/或库提供者。 无论是硬件还是软件,泰勒扩展与否,等等,都会有所不同。
我意识到这绝对没有帮助。
它们通常以软件实现,并且在大多数情况下不会使用相应的硬件(即,aseembly)调用。 但是,正如Jason所指出的那样,这些都是特定于实现的。
请注意,这些软件例程不是编译器资源的一部分,而是可以在相应的库中find,例如clib或GNU编译器的glibc。 请参阅http://www.gnu.org/software/libc/manual/html_mono/libc.html#Trig-Functions
如果你想要更好的控制,你应该仔细评估你需要什么。 一些典型的方法是查找表的内插,程序集调用(通常很慢),或其他的近似scheme,如平方根的Newton-Raphson。
如果你想用软件实现而不是硬件,那么寻找这个问题的明确答案的地方就是Numerical Recipes的第5章。 我的副本放在一个盒子里,所以我不能提供细节,但简短的版本(如果我记得这个权利的话)就是把tan(theta/2)
作为原始操作,并从那里计算其他值。 这个计算是用一系列的近似来完成的,但它比Taylor系列更快地收敛。
对不起,如果不把我的手放在书上,我就不能再回头了。
正如很多人指出的那样,这是实施依赖性的。 但据我了解你的问题,你有兴趣在math函数的一个真正的软件实现,但只是没有设法find一个。 如果是这种情况,那么你在这里:
- 从http://ftp.gnu.org/gnu/glibc/下载glibc源代码;
- 看文件
dosincos.c
位于解压后的glibc root \ sysdeps \ ieee754 \ dbl-64文件夹 - 同样,你可以findmath库的其余部分的实现,只要find适当的名称的文件
你也可以看看扩展名为.tbl
的文件,它们的内容不过是二进制forms的不同function的预先计算值的巨大表格。 这就是为什么执行速度如此之快:而不是计算他们使用的任何系列的所有系数,他们只是做一个快速查找,这要快得多。 顺便说一下,他们使用裁缝系列计算正弦和余弦。
我希望这有帮助。
在C程序中,我将尝试回答sin()
的情况,在当前的x86处理器(比如说Intel Core 2 Duo)上用GCC的C编译器进行编译。
在C语言中,标准C库包含通用的math函数,不包括在语言本身(例如pow
, sin
和cos
表示功率,正弦和余弦)。 其头文件包含在math.h中 。
现在在GNU / Linux系统上,这些库函数由glibc(GNU libc或GNU C Library)提供。 但GCC编译器希望使用-lm
编译器标志链接到math库 ( libm.so
)以启用这些math函数的使用。 我不知道为什么它不是标准C库的一部分。 这些将是浮点函数的软件版本,或“软浮点”。
另外:把math函数分开的原因是历史性的,据我所知,这只是为了减less非常旧的Unix系统中可执行程序的大小,可能在共享库可用之前。
现在,编译器可以优化标准C库函数sin()
(由libm.so
提供),用对CPU / FPU内置的sin()函数的本地指令的调用来替代,该函数作为FPU指令存在FSIN
for x86 / x87),像Core 2系列这样的新型处理器(几乎和i486DX一样正确)。 这将取决于传递给gcc编译器的优化标志。 如果编译器被告知编写可以在任何i386或更新的处理器上执行的代码,则不会进行这样的优化。 -mcpu=486
标志会通知编译器进行这样的优化是安全的。
现在,如果程序执行sin()函数的软件版本,它将基于CORDIC (坐标旋转数字计算机)或BKMalgorithm执行 ,或者更可能是现在通常用于计算的表格或幂级数计算这样的先验function。 [Src: http : //en.wikipedia.org/wiki/Cordic#Application]
任何近期(从2.9x版本)的gcc版本还提供了一个内置版本的__builtin_sin()
,它将用来替代对C库版本的标准调用,作为优化。
我相信这是清楚的泥巴,但希望给你更多的信息,比你期望的,以及许多跳跃点来学习更多自己。
关于像sin()
, cos()
, tan()
这样的三angular函数,在5年之后,还没有提到高质量三angular函数的另一个重要方面: 范围缩减 。
任何这些function的早期步骤是将angular度(以弧度为单位)减小到2 *π间隔的范围。 但是π是非理性的,所以像x = remainder(x, 2*M_PI)
这样简单的减lessx = remainder(x, 2*M_PI)
导致错误,因为M_PI
或者机器pi是π的近似值。 那么,怎么做x = remainder(x, 2*π)
?
早期的图书馆使用扩展的精确度或制作的程序来提供高质量的结果,但是仍然在有限的double
范围内。 当像sin(pow(2,30))
这样要求很大的值时,结果是没有意义的,或者是0.0
并且可能带有一个错误标志 ,如TLOSS
总精度损失或PLOSS
部分精度损失。
较大值的范围减小到-π到π这样的区间是一个挑战性的问题,可以与基本的三angular函数本身的挑战相媲美。
一个好的报告是争论减less的巨大的论据:好到最后一点 (1992年)。 它很好地解决了这个问题:讨论了在各种平台(SPARC,PC,HP,30+其他平台)上的需求和事情,并提供了一个解决schemealgorithm,从而提供了从-DBL_MAX
到DBL_MAX
所有 double
-DBL_MAX
DBL_MAX
。
[编辑]
如果原始参数是度数,但可能是一个很大的值,首先使用fmod()
来提高精度。
// sin(degrees2radians(x)) sin(degrees2radians(fmod(x,360)))
各种触发标识和remquo()
提供了更多的改进。 就像一个样品sind()
每当这样的function被评估,那么在某种程度上最有可能的是:
- 插值的数值表(用于快速,不准确的应用 – 例如计算机graphics学)
- 对一系列收敛于期望值的系列的评估 – 可能不是泰勒系列,更可能是基于象克莱肖 – 柯蒂斯这样的奇特正交的东西。
如果没有硬件支持,那么编译器可能会使用后一种方法,只发出汇编代码(不带debugging符号),而不是使用ac库 – 这使得在debugging器中跟踪实际代码变得非常棘手。
如果你想看看C语言中这些函数的实际GNU实现,请查看glibc的最新主干。 请参阅GNU C库 。
没有什么事情可以打到源头上,看到有人在一个常用的图书馆里实际上做了些什么; 让我们看看一个特别的C库实现。 我select了uLibC。
这是罪的function:
http://git.uclibc.org/uClibc/tree/libm/s_sin.c
看起来像是处理了一些特殊的情况,然后进行了一些参数缩减,将input映射到范围[-pi / 4,pi / 4](将参数分成两部分,大部分和尾部)打电话之前
http://git.uclibc.org/uClibc/tree/libm/k_sin.c
然后对这两部分进行操作。 如果没有尾巴,则使用13次多项式生成近似的答案。如果存在尾巴,则根据sin(x+y) = sin(x) + sin'(x')y
计算正弦/余弦/正切实际上很容易通过使用泰勒级数的代码来完成。 写一个自己需要5秒钟。
整个过程可以用这个公式在这里总结: http : //upload.wikimedia.org/math/5/4/6/546ecab719ce73dfb34a7496c942972b.png
以下是我为C编写的一些例程:
double _pow(double a, double b) { double c = 1; for (int i=0; i<b; i++) c *= a; return c; } double _fact(double x) { double ret = 1; for (int i=1; i<=x; i++) ret *= i; return ret; } double _sin(double x) { double y = x; double s = -1; for (int i=3; i<=100; i+=2) { y+=s*(_pow(x,i)/_fact(i)); s *= -1; } return y; } double _cos(double x) { double y = 1; double s = -1; for (int i=2; i<=100; i+=2) { y+=s*(_pow(x,i)/_fact(i)); s *= -1; } return y; } double _tan(double x) { return (_sin(x)/_cos(x)); }
Don't use Taylor series. Chebyshev polynomials are both faster and more accurate, as pointed out by a couple of people above. Here is an implementation (originally from the ZX Spectrum ROM): https://albertveli.wordpress.com/2015/01/10/zx-sine/
if you want sin then asm volatile ("fsin" : "=t"(vsin) : "0"(xrads)); if you want cos then asm volatile ("fcos" : "=t"(vcos) : "0"(xrads)); if you want sqrt then asm volatile ("fsqrt" : "=t"(vsqrt) : "0"(value)); so why use inaccurate code when the machine instructions will do.