我一直在研究.NET反汇编和GCC源代码,但似乎找不到任何地方的实际实现sin()
以及其他数学函数...它们似乎总是在引用其他内容。
谁能帮我找到他们?我觉得C不能运行的所有硬件都不太可能支持硬件中的trig函数,因此在某处一定有软件算法,对吗?
我所知道的几种方法,功能,可以进行计算,并写了我自己的程序来使用泰勒级数为乐趣的计算功能。我对生产语言是多么真实感到好奇,因为尽管我认为我的算法非常聪明(显然并非如此),但我所有的实现总是慢几个数量级。
我一直在研究.NET反汇编和GCC源代码,但似乎找不到任何地方的实际实现sin()
以及其他数学函数...它们似乎总是在引用其他内容。
谁能帮我找到他们?我觉得C不能运行的所有硬件都不太可能支持硬件中的trig函数,因此在某处一定有软件算法,对吗?
我所知道的几种方法,功能,可以进行计算,并写了我自己的程序来使用泰勒级数为乐趣的计算功能。我对生产语言是多么真实感到好奇,因为尽管我认为我的算法非常聪明(显然并非如此),但我所有的实现总是慢几个数量级。
Answers:
在GNU libm中,的实现sin
取决于系统。因此,您可以在sysdeps的相应子目录中的某个位置找到每种平台的实现。
一个目录包含IBM提供的C语言实现。自2011年10月以来,这是在sin()
典型的x86-64 Linux系统上调用时实际运行的代码。它显然比fsin
汇编指令要快。源代码:sysdeps / ieee754 / dbl-64 / s_sin.c,查找__sin (double x)
。
此代码非常复杂。没有一个软件算法在x值的整个范围内都尽可能快且准确,因此该库实现了几种不同的算法,其首要任务是查看x并确定使用哪种算法。
当x非常非常接近0时,sin(x) == x
是正确的答案。
再往前走一点,sin(x)
使用熟悉的泰勒级数。但是,这仅在0附近才是准确的,所以...
当角度大于约7°时,将使用另一种算法,计算sin(x)和cos(x)的泰勒级数逼近,然后使用来自预先计算表的值来细化该逼近。
何时| x | > 2,以上算法都不起作用,因此代码首先从计算一些可以接近sin
或接近0的值开始cos
。
还有另一个分支可以处理x为NaN或无穷大。
这段代码使用了一些我从未见过的数字技巧,尽管据我所知,它们在浮点专家中可能是众所周知的。有时,几行代码需要几个段落来解释。例如,这两行
double t = (x * hpinv + toint);
double xn = t - toint;
(有时)用于将x减小到接近于0的值,该值与x的差为π/ 2的倍数,特别是xn
×π/ 2。无需分割或分支的完成方式相当聪明。但是,根本没有任何评论!
GCC / glibc的较旧的32位版本使用了该fsin
指令,这对于某些输入而言出乎意料地不准确。有一篇引人入胜的博客文章,仅用两行代码说明了这一点。
fdlibm sin
在纯C中的实现比glibc 的实现简单得多,并且受到好评。源代码:fdlibm / s_sin.c和fdlibm / k_sin.c
sin()
;键入gdb a.out
,然后break sin
,然后run
,然后disassemble
。
__kernel_sin
不过,它是在k_sin.c中定义的,它是纯C语言。再次单击它-我第一次破坏了URL。
正弦和余弦之类的功能在微处理器内部的微代码中实现。例如,英特尔芯片具有这些芯片的组装说明。AC编译器将生成调用这些汇编指令的代码。(相比之下,Java编译器则不会。Java在软件而不是硬件中评估触发功能,因此运行起来要慢得多。)
芯片不使用泰勒级数来计算触发函数,至少不是完全没有。首先,他们使用CORDIC,但也可以使用较短的泰勒级数来完善CORDIC的结果,或者在特殊情况下(例如,在很小的角度下以相对高的精度计算正弦)。有关更多说明,请参见此StackOverflow答案。
好的,小子,为专业人士准备的时间。...这是我对经验不足的软件工程师的最大抱怨之一。他们从头开始计算先验函数(使用泰勒级数),好像从来没有人做过这些计算。不对。这是一个定义明确的问题,非常聪明的软件和硬件工程师已解决了数千次,并且拥有定义明确的解决方案。基本上,大多数先验函数都使用Chebyshev多项式来计算它们。至于使用哪种多项式取决于情况。首先,关于此事的圣经是哈特和切尼写的一本书,叫做《计算机近似》。在那本书中,您可以确定是否具有硬件加法器,乘法器,除法器等,并确定哪些操作最快。例如,如果您有一个非常快的分频器,计算正弦的最快方法可能是P1(x)/ P2(x),其中P1,P2是Chebyshev多项式。如果没有快速除法器,则可能只是P(x),其中P的项比P1或P2的项多得多。因此,第一步是确定您的硬件及其功能。然后,选择适当的Chebyshev多项式组合(例如,对于余弦,通常形式为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。然后,您查询p4,p3,p2,p1的实际值,在书本背面的p0值低于3502(它们将处于浮点状态)。然后以以下形式在软件中实现算法:(((p4.x + p3).x + p2).x + p1).x + p0 ....这是将余弦值计算为7位小数的方式放置在该硬件上。
请注意,FPU中超验操作的大多数硬件实现通常都涉及一些微代码和类似的操作(取决于硬件)。Chebyshev多项式用于大多数先验,但不是全部。例如,平方根更快,首先使用查找表使用牛顿拉夫森方法的两次迭代。同样,那本书《计算机近似》将告诉您。
如果您打算实现这些功能,那么我建议任何人获得该书的副本。这些算法确实是圣经。请注意,有许多替代方法可用于计算这些值,例如cordic等,但是这些方法最适合只需要低精度的特定算法。为了每次都保证精度,切比雪夫多项式是必经之路。就像我说的,定义明确的问题。已经解决了50年了.....那就是它的完成方式。
就是说,现在有一些技术可以使用Chebyshev多项式以低次多项式获得单精度结果(例如上述余弦的示例)。然后,还有其他一些技术可以在值之间进行插值以提高精度,而不必使用更大的多项式,例如“ Gal的精确表方法”。后一种技术是指ACM文献所引用的文章。但是最终,切比雪夫多项式被用来获得90%的结果。
请享用。
对于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)之所以起作用,是因为小角度的近似sin(x)= x。对于更大的角度,您需要计算越来越多的项才能获得可接受的结果。您可以使用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;
}
是的,也有用于计算的软件算法sin
。基本上,通常使用数字方法(例如,近似表示该函数的泰勒级数)使用数字计算机来计算此类物质。
数值方法可以将函数近似于任意精度,并且由于您在浮点数中的精度是有限的,因此它们非常适合这些任务。
这是余弦的一个示例:
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)
这是一个复杂的问题。x86系列的类似Intel的CPU具有该sin()
功能的硬件实现,但是它是x87 FPU的一部分,并且不再用于64位模式(使用SSE2寄存器代替)。在那种模式下,使用软件实现。
有几种这样的实现。其中之一是在fdlibm中,并在Java中使用。据我所知,glibc实现包含fdlibm的一部分以及IBM贡献的其他部分。
超越函数的软件实现(例如sin()
通常使用多项式的近似值)通常是从泰勒级数中获得的。
sin
并且cos
比FPU上的硬件指令要快。更简单,更幼稚的库倾向于使用fsin
和fcos
指令。
FSIN
全精度计算正弦更快。如果您告诉我这些快速库的名称,我将不胜感激,看看这很有趣。
sin()
恰好比fsin
计算的实现快大约两倍(正是因为它的执行精度较低)。请注意,已知x87的实际精度要比其宣布的79位低。
如另一个答案所述,切比雪夫多项式是多项式,其中函数和多项式之间的最大差值尽可能小。那是一个很好的开始。
在某些情况下,最大误差不是您感兴趣的,而是最大相对误差。例如,对于正弦函数,x = 0附近的误差应该比较大值的误差小得多。你想要一个小亲戚误差。因此,您将计算sin x / x的Chebyshev多项式,然后将该多项式乘以x。
接下来,您必须弄清楚如何评估多项式。您希望以这样的方式进行评估:中间值较小,因此舍入误差较小。否则,舍入误差可能会比多项式误差大得多。对于正弦函数之类的函数,如果您粗心大意,那么即使x <y,您为sin x计算的结果也可能大于sin y的结果。因此,需要仔细选择计算顺序并计算舍入误差的上限。
例如,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 ...),则括号中的该函数将减小,并且如果y是x的下一个大数,则有时sin y会比sin x小。取而代之的是计算出sin x = x-x ^ 3 *(1/6-x ^ 2/120 + x ^ 4/5040 ...)的情况。
例如,在计算切比雪夫多项式时,通常需要将系数取整到双精度。但是,尽管Chebyshev多项式是最佳的,但系数舍入为双精度的Chebyshev多项式并不是双精度系数的最优多项式!
例如,对于sin(x),您需要x,x ^ 3,x ^ 5,x ^ 7等的系数。您可以执行以下操作:用多项式(x + 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舍入为双精度的要好得多。
接下来,应在选择多项式时考虑舍入误差。您发现在多项式中误差最小的多项式,忽略了舍入误差,但是您想优化多项式加舍入误差。拥有Chebyshev多项式后,就可以计算舍入误差的范围。假设f(x)是您的函数,P(x)是多项式,E(x)是舍入误差。您不想优化| f(x)-P(x)|,您想优化| f(x)-P(x)+/- E(x)|。您将得到一个略有不同的多项式,它试图在舍入误差较大的情况下将多项式误差减小,而在舍入误差较小的情况下将多项式误差放宽一些。
所有这一切将使您轻松舍入误差至多为最后一位的0.55倍,其中+,-,*,/舍入误差为至多最后一位的0.50倍。
关于像三角函数sin()
,cos()
,tan()
一直没有提到,5年后,高品质的三角函数的一个重要方面:范围减少。
这些功能中的任何一个的早期步骤都是将弧度角减小到2 *π间隔的范围。但是π是非理性的,因此简单的归约,例如x = remainder(x, 2*M_PI)
引入误差as M_PI
或机器pi是π的近似值。那么,该怎么办x = remainder(x, 2*π)
?
早期的库使用扩展的精度或精心设计的程序来提供质量结果,但是仍然在有限的范围内double
。当像这样要求较大的值时sin(pow(2,30))
,结果将毫无意义,或者0.0
可能将错误标志设置为TLOSS
总精度或PLOSS
部分精度损失。
将较大的值良好地减小到-π到π的范围是一个具有挑战性的问题,它可以与基本触发函数(例如sin()
)本身的挑战相匹敌。
一个很好的报告是对大量论据的论证减少:对最后一点的贡献(1992年)。它很好地涵盖了该问题:讨论了各种平台(SPARC,PC,HP,30多种)上的需求以及它们的运行方式,并提供了一种解决方案算法,可提供从到的所有 质量结果。double
-DBL_MAX
DBL_MAX
如果原始参数以度为单位,但可能有很大的价值,请fmod()
首先使用以提高精度。好fmod()
将不会导致错误,因此可提供出色的范围缩小。
// sin(degrees2radians(x))
sin(degrees2radians(fmod(x, 360.0))); // -360.0 < fmod(x,360) < +360.0
各种触发身份,remquo()
甚至可以提供更多改进。范例:sind()
它们通常以软件实现,并且在大多数情况下不会使用相应的硬件(即,组装)调用。但是,正如Jason指出的那样,这些都是特定于实现的。
请注意,这些软件例程不是编译器源代码的一部分,而是可以在相应的库中找到,例如clib或GNU编译器的glibc。参见http://www.gnu.org/software/libc/manual/html_mono/libc.html#Trig-Functions
如果您想要更好的控制,则应仔细评估您的需求。一些典型的方法是对查询表进行插值,汇编调用(通常很慢)或其他近似方案,例如平方根的Newton-Raphson。
如果要用软件而不是硬件来实现,则可以在《数字配方》的第5章中找到对此问题的明确答案。我的副本在一个盒子里,所以我无法提供细节,但是简短的版本(如果我没记错的话)是您将其tan(theta/2)
作为原始操作并从那里计算其他操作。计算是一系列近似完成,但它的东西,收敛得多快于泰勒级数。
抱歉,如果不把手放在书上,我再也记不起来了。
没有什么比打源和了解某人在常用的库中实际完成它的方式更好的了。让我们特别看一下一个C库实现。我选择了uLibC。
这是sin函数:
http://git.uclibc.org/uClibc/tree/libm/s_sin.c
看起来像处理了几种特殊情况,然后进行了一些参数约简,以将输入映射到范围[-pi / 4,pi / 4],(将参数分为两部分,大部分和一条尾巴)打电话之前
http://git.uclibc.org/uClibc/tree/libm/k_sin.c
然后在这两个部分上进行操作。如果没有尾巴,则使用次数为13的多项式生成近似答案。如果存在尾巴,则根据以下原理可以得到少量的校正加法:sin(x+y) = sin(x) + sin'(x')y
正如许多人指出的那样,它取决于实现。但是据我了解您的问题,您对数学函数的实际软件实现感兴趣,但是却没有找到答案。如果是这种情况,那么您在这里:
dosincos.c
位于解压后的glibc根 \ sysdeps \ ieee754 \ dbl-64文件夹中的文件您还可以查看带有.tbl
扩展名的文件,它们的内容无非就是二进制形式的不同功能的预计算值的大表。这就是实现如此之快的原因:与其计算所有使用的序列的所有系数,不如进行快速查找,这要快得多。顺便说一句,他们确实使用Tailor级数来计算正弦和余弦。
我希望这有帮助。
我将尝试回答sin()
在当前的x86处理器(例如Intel Core 2 Duo)上使用GCC的C编译器编译的C程序中的情况。
在C语言中的标准C库包括普通的数学函数,不包括在语言本身(例如pow
,sin
和cos
分别为电源,正弦,余弦)。其标头包含在math.h中。
现在,在GNU / Linux系统上,这些库函数由glibc(GNU libc或GNU C库)提供。但是GCC编译器希望您使用编译器标志链接到数学库(libm.so
),-lm
以启用这些数学函数的用法。我不确定为什么它不是标准C库的一部分。这些将是浮点函数或“ soft-float”的软件版本。
另外:将数学函数分开的原因是历史性的,据我所知,它可能仅是在非常老的Unix系统中减小可执行程序的大小,可能是在共享库可用之前。
现在,编译器可以优化标准C库函数sin()
(由提供libm.so
),以替换为对CPU / FPU内置sin()函数的本机指令的调用,该指令作为FPU指令(FSIN
对于x86 / x87)存在于像Core 2系列这样的较新处理器(这可以回溯到i486DX之前的大部分时间)这将取决于传递给gcc编译器的优化标志。如果告诉编译器编写可以在任何i386或更高版本的处理器上执行的代码,则不会进行这种优化。该-mcpu=486
标志将通知编译器进行这样的优化是安全的。
现在,如果程序执行了sin()函数的软件版本,它将基于CORDIC(坐标旋转数字计算机)或BKM算法,或更可能是现在通常用于计算的表格或幂级数计算来执行这样的先验功能。[Src:http://en.wikipedia.org/wiki/Cordic#Application]
gcc的任何最新版本(约2.9x起)也提供了sin的内置版本,__builtin_sin()
该版本将作为优化来替代对C库版本的标准调用。
我敢肯定,这就像泥泞一样清晰,但希望能给您比您期望的更多的信息,并且有很多要点来学习您自己。
不要使用泰勒级数。正如上面的几个人所指出的那样,切比雪夫多项式既更快又更准确。这是一个实现(最初来自ZX Spectrum ROM):https : //albertveli.wordpress.com/2015/01/10/zx-sine/
实际上,使用泰勒级数通过代码可以很容易地计算正弦/余弦/正切值。自己写一个大概需要5秒钟。
整个过程可以用下面的公式总结:
这是我为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));
}
来自Blindy答案的改进代码版本
#define EPSILON .0000000000001
// this is smallest effective threshold, at least on my OS (WSL ubuntu 18)
// possibly because factorial part turns 0 at some point
// and it happens faster then series element turns 0;
// validation was made against sin() from <math.h>
double ft_sin(double x)
{
int k = 2;
double r = x;
double acc = 1;
double den = 1;
double num = x;
// precision drops rapidly when x is not close to 0
// so move x to 0 as close as possible
while (x > PI)
x -= PI;
while (x < -PI)
x += PI;
if (x > PI / 2)
return (ft_sin(PI - x));
if (x < -PI / 2)
return (ft_sin(-PI - x));
// not using fabs for performance reasons
while (acc > EPSILON || acc < -EPSILON)
{
num *= -x * x;
den *= k * (k + 1);
acc = num / den;
r += acc;
k += 2;
}
return (r);
}
如果你想要的sin
话
__asm__ __volatile__("fsin" : "=t"(vsin) : "0"(xrads));
如果你想要的cos
话
__asm__ __volatile__("fcos" : "=t"(vcos) : "0"(xrads));
如果你想要的sqrt
话
__asm__ __volatile__("fsqrt" : "=t"(vsqrt) : "0"(value));
那么,当机器指令执行时,为什么要使用不正确的代码呢?