C如何计算sin()和其他数学函数?

Han*_*ank 236 c math trigonometry

我一直在研究.NET反汇编和GCC源代码,但似乎无法找到实际实现sin()和其他数学函数的任何地方......它们似乎总是引用其他东西.

谁能帮我找到它们?我觉得C运行的所有硬件都不太可能支持硬件中的触发功能,因此某处必须有软件算法,对吧?


我知道有几种方法可以计算函数,并编写了我自己的例程来计算函数使用泰勒系列来获得乐趣.我很好奇真正的生产语言是如何做到的,因为我的所有实现总是慢几个数量级,即使我认为我的算法非常聪明(显然它们不是).

Jas*_*rff 201

在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并决定使用哪种算法.在某些地区,它使用的是看似熟悉的泰勒系列.一些算法首先计算快速结果,然后如果不够准确,则丢弃它并回退到较慢的算法.

较旧的32位版本的GCC/glibc使用了该fsin指令,这对于某些输入而言令人惊讶地不准确.有一个引人入胜的博客文章只用2行代码就可以说明这一点.

fdlibm sin在纯C中的实现比glibc简单得多,并且得到很好的评论.源代码:fdlibm/s_sin.cfdlibm/k_sin.c

  • 要看到这真的是在x86上运行的代码:编译一个调用`sin()`的程序; 输入`gdb a.out`,然后输入`break sin`,然后输入`run`,然后输入`disassemble`. (33认同)
  • @Henry:不要误以为这是好的代码.它真的很糟糕**,不学习编码那样! (5认同)
  • @Henry:`__kernel_sin`是在k_sin.c中定义的,它是纯粹的C.再次点击它 - 我第一次搞砸了URL. (3认同)
  • 链接的sysdeps代码特别有趣,因为它是正确舍入的.也就是说,它显然为所有输入值提供了最佳答案,而这种输入值最近才成为可能.在某些情况下,这可能会很慢,因为可能需要计算许多额外的数字以确保正确的舍入.在其他情况下,它非常快 - 对于足够小的数字,答案只是角度. (3认同)
  • @Andreas嗯,你是对的,与fdlibm相比,IBM代码确实看起来非常糟糕.我编辑了答案,添加了fdlibm正弦例程的链接. (2认同)
  • 我们可以修改这个答案以使其更加独立吗?IE 链接更少,代码更多。 (2认同)

Joh*_*ook 65

正弦和余弦等函数在微处理器内部的微码中实现.例如,英特尔芯片有这些的组装说明.AC编译器将生成调用这些汇编指令的代码.(相比之下,Java编译器不会.Java评估软件中的trig函数而不是硬件,因此运行速度要慢得多.)

芯片使用泰勒级数计算三角函数,至少不完全.首先,他们使用CORDIC,但他们也可能使用短的泰勒系列来完善CORDIC的结果或特殊情况,例如计算正弦,对于非常小的角度具有高相对精度.有关更多说明,请参阅此StackOverflow答案.

  • 像正弦和余弦这样的超越数学函数可以用微代码实现,也可以作为当前32位台式机和服务器处理器的硬件指令实现.情况并非总是如此,直到i486(DX)所有浮点计算都是在没​​有单独协处理器的x86系列软件("软浮点")中完成的.并非所有(FPU)都包含超越函数(例如Weitek 3167). (10认同)
  • x86/x64芯片有一个用于计算正弦(fsin)的汇编指令,但该指令有时非常不准确,因此很少使用.有关详细信息,请参阅http://randomascii.wordpress.com/2014/10/09/intel-underestimates-error-bounds-by-1-3-quintillion/.大多数其他处理器不会*具有正弦和余弦指令,因为在软件中计算它们会提供更大的灵活性,甚至可能更快. (6认同)
  • 至于"抛光"答案,假设你正在计算正弦和余弦.假设您在一个点(例如来自CORDIC)知道两者的确切值,但是想要在附近点处的值.那么对于小的差值h,你可以应用泰勒近似值f(x + h)= f(x)+ h f'(x)或f(x + h)= f(x)+ h f'(x) + h ^ 2 f''(x)/ 2. (4认同)
  • 通常不使用intel芯片内部的脏东西。首先,对于许多应用而言,操作的准确性和分辨率至关重要。当您达到第7位左右时,Cordic非常不准确,并且是不可预测的。其次,我听说它们的实现存在错误,这会导致更多问题。我看了Linux gcc的sin函数,果然,它使用了chebyshev。不使用内置的东西。哦,同样,芯片中的cordic算法比软件解决方案要慢。 (3认同)
  • 你可以说得更详细点吗?如何使用泰勒级数“完善”近似值? (2认同)

Don*_*ray 63

好孩子,专业人士的时间......这是我对没有经验的软件工程师的最大抱怨之一.他们从头开始计算超越函数(使用泰勒系列),好像在他们的生活中没有人曾经做过这些计算.不对.这是一个定义明确的问题,非常聪明的软件和硬件工程师已经接触过数千次,并且有一个定义明确的解决方案.基本上,大多数超越函数使用切比雪夫多项式来计算它们.至于使用哪种多项式取决于具体情况.首先,关于这件事的圣经是哈特和切尼的一本名为"计算机近似"的书.在该书中,您可以决定是否有硬件加法器,乘法器,除法器等,并确定哪些操作最快.例如,如果你有一个非常快的分频器,计算正弦的最快方法可能是P1(x)/ P2(x),其中P1,P2是切比雪夫多项式.没有快速分频器,它可能只是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.然后在3502下查看本书后面的p4,p3,p2,p1,p0值的实际值(它们将处于浮点状态).然后在软件中以以下形式实现算法:(((p4.x + p3).x + p2).x + p1).x + p0 ....这就是你如何计算余弦到7的十进制数硬件上的位置.

请注意,FPU中的超越操作的大多数硬件实现通常涉及一些微代码和这样的操作(取决于硬件).切比雪夫多项式用于大多数超越,但不是全部.例如,Square root使用查找表首先使用Newton raphson方法的双重迭代更快.同样,那本书"计算机近似"会告诉你.

如果您计划对这些功能进行推理,我建议任何人都能获得该书的副本.它确实是这类算法的圣经.请注意,有许多替代方法可用于计算这些值,如cordics等,但这些方法往往最适合您只需要低精度的特定算法.为了保证每次精度,切比雪夫多项式是最佳选择.就像我说的,明确的问题.已经解决了50年了......那就是它如何完成的.

现在,正如所说的那样,有些技术可以使用切比雪夫多项式来获得具有低次多项式的单精度结果(如上面的余弦的例子).然后,还有其他技术可以在值之间进行插值以提高精度,而无需使用更大的多项式,例如"Gal的精确表格方法".后一种技术是指ACM文献所指的内容.但最终,Chebyshev多项式是用来获得90%的方式.

请享用.

  • -1对于不专业和漫无边际(并且有点粗鲁)的语气,以及这个答案的实际非冗余*内容*被剥夺了漫无边际和屈尊俯就的事实,基本归结为"他们经常使用切比雪夫多项式;见这本书了解更多细节,真的很棒!" 你知道,这可能是绝对正确的,但这并不是我们想要的那种自足的*答案.尽管如此,它还是会对这个问题做出正确的评论. (153认同)
  • 我对前几句话完全赞同.此外,值得回顾的是,计算保证精度的特殊功能是一个难题**.你提到的聪明人大部分时间都是这样做的.此外,在更技术性的说明中,min-max多项式是受欢迎的graal,而Chebyshev多项式对它们来说是更简单的代理. (6认同)
  • 我经常使用嵌入式系统中的查找表和比特(而不是弧度),但这适用于专门的应用程序(如游戏).我认为那个人对c编译器如何为浮点数计算sin感兴趣.... (4认同)
  • 早在游戏开发的早期阶段,通常就是查找表对速度的关键需求.我们通常没有使用标准的lib函数来处理这些事情. (2认同)
  • @IlmariKaronen 这比前两个答案要好很多。#1 是一般性描述,指向边缘不可读的代码而不是实际的算法,而 #2 几乎完全是漫无目的的。你只是对这里的那个人不关心在互联网上变得“专业”感到恼火,因为你走路时脊椎上有一根棍子。如果他不对,那就太粗鲁和居高临下了,但他确实是对的。作为一个正在寻找在嵌入式环境中实现的解决方案的人,这里的其他一切都是小孩子的废话 - 即使按照只短暂使用过数值方法的人的标准来看也是如此。 (2认同)

Bli*_*ndy 15

对于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;
}
Run Code Online (Sandbox Code Playgroud)

注意:(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;

}
Run Code Online (Sandbox Code Playgroud)

  • 您可以用 DBL_EPSILON 替换这个神奇的 .000…01 吗? (2认同)

Han*_*sir 13

使用泰勒系列并尝试找出系列术语之间的关系,这样你就不会一次又一次地计算事物

这是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;
}
Run Code Online (Sandbox Code Playgroud)

使用这个我们可以使用已经使用过的一个得到总和的新项(我们避免使用阶乘和x ^ 2p)

解释http://img514.imageshack.us/img514/1959/82098830.jpg

  • 您是否知道可以使用Google Chart API使用TeX制作这样的公式?http://code.google.com/apis/chart/docs/gallery/formulas.html (2认同)

Meh*_*ari 12

是的,还有用于计算的软件算法sin.基本上,使用数字计算机计算这些类型的东西通常使用数值方法来完成,例如近似表示函数的泰勒级数.

数值方法可以将函数逼近任意精度,并且由于浮点数中的精度是有限的,因此它们非常适合这些任务.

  • 一个真正的实现可能不会使用泰勒系列,因为有更有效的方法.您只需要在域[0 ... pi/2]中正确近似,并且有一些函数可以比泰勒级数更有效地提供良好的近似. (11认同)
  • @David:我同意.我小心翼翼地在我的回答中提到"喜欢"这个词.但泰勒扩展是一个简单的解释背后功能的方法背后的想法.也就是说,我看过使用泰勒系列的软件实现(不确定它们是否已经过优化). (2认同)

Tho*_*nin 11

这是一个复杂的问题.类似Intel的x86系列CPU具有该sin()功能的硬件实现,但它是x87 FPU的一部分,在64位模式下不再使用(其中使用SSE2寄存器).在该模式中,使用软件实现.

那里有几个这样的实现.一个是在fdlibm中,用于Java.据我所知,glibc实现包含部分fdlibm,以及IBM提供的其他部分.

超常函数的软件实现,例如sin()通常使用多项式的近似,通常从泰勒级数获得.

  • @Igor:这取决于你正在看的数学库.事实证明,x86上最优化的数学库使用SSE软件实现`sin`和`cos`,它们比FPU上的硬件指令更快.更简单,更天真的库倾向于使用`fsin`和`fcos`指令. (7认同)
  • 在x86和x64模式下,SSE2寄存器都不用于计算sin(),当然,无论模式如何,sin都是在硬件中计算的.嘿,这是2010年我们住在:) (3认同)

gna*_*729 9

在另一个答案中提到的切比雪夫多项式是多项式,其中函数和多项式之间的最大差异尽可能小.这是一个很好的开始.

在某些情况下,最大误差不是您感兴趣的,而是最大相对误差.例如,对于正弦函数,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 ^五千零四十零分之六...),则在括号中的那功能被降低,并且发生的是,如果y是一个较大数为x,则有时罪ý将比罪x小.相反,计算sin x = x - x ^ 3*(1/6 - x ^ 2/120 + x ^ 4/5040 ......),这是不可能的.

例如,在计算切比雪夫多项式时,通常需要将系数四舍五入为双精度.但是,当切比雪夫多项式是最优的时,具有舍入到双精度的系数的切比雪夫多项式不是具有双精度系数的最优多项式!

例如,对于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更好地舍入到双精度.

接下来,您应该考虑多项式选择中的舍入误差.您在多项式中找到了一个忽略舍入误差的最小误差的多项式,但是您希望优化多项式加舍入误差.一旦有了Chebyshev多项式,就可以计算出舍入误差的界限.假设f(x)是你的函数,P(x)是多项式,E(x)是舍入误差.你不想优化| f(x) - P(x)|,你想优化| f(x) - P(x)+/- E(x)|.您将获得稍微不同的多项式,该多项式试图在舍入误差较大的地方保持多项式误差,并且在舍入误差较小的情况下放松多项式误差.

所有这些都可以让您轻松舍入最后一位最多0.55倍的错误,其中+, - ,*,/的舍入误差最多为最后一位的0.50倍.

  • 这是对一个人*可能*如何有效地计算 sin(x) 的一个很好的解释,但它似乎并没有真正回答 OP 的问题,这特别是关于常见的 C 库/编译器 * 做* 计算它的方式。 (2认同)

chu*_*ica 8

关于三角函数像sin(),cos(),tan()一直没有提到,5年后,高品质的三角函数的一个重要方面:范围减少.

任何这些函数的早​​期步骤是以弧度将角度减小到2*π区间的范围.但π是不合理的,因此x = remainder(x, 2*M_PI)引入误差M_PI或机器pi 等简单的约简是π的近似值.那么,该怎么办x = remainder(x, 2*?)

早期的图书馆使用扩展精度或精心编程来提供高质量的结果,但仍然在有限的范围内double.当请求大值时sin(pow(2,30)),结果没有意义,或者0.0可能将错误标志设置为TLOSS完全丢失精度或PLOSS部分精度损失.

将大值的大范围减小到像-π到π的区间是一个具有挑战性的问题,可以与基本的三角函数相媲美,就像sin()它本身一样.

一个好的报告是对于大论证的论证减少:好到最后一点(1992).它涵盖了问题得好:讨论的需要,事情是如何在各种平台(SPARC,PC,HP,其他30+),并提供了一个解决方案算法给出了质量的结果 double来自-DBL_MAXDBL_MAX.


如果原始参数以度为单位,但可能具有较大值,请fmod()首先使用以提高精度.好的fmod()将不会引入错误,因此可以提供出色的范围缩小.

// sin(degrees2radians(x))
sin(degrees2radians(fmod(x, 360.0))); // -360.0 < fmod(x,360) < +360.0
Run Code Online (Sandbox Code Playgroud)

各种触发身份并remquo()提供更多改进.示例:sind()


Joh*_*ode 6

库函数的实际实现取决于特定的编译器和/或库提供程序.无论是硬件还是软件,是否是泰勒扩展等,都会有所不同.

我意识到这绝对没有帮助.


mne*_*syn 5

它们通常以软件实现,并且在大多数情况下不会使用相应的硬件(即,aseembly)调用.但是,正如Jason指出的那样,这些都是特定于实现的.

请注意,这些软件例程不是编译器源的一部分,而是可以在诸如clib的对应库中找到,或者用于GNU编译器的glibc.请参阅http://www.gnu.org/software/libc/manual/html_mono/libc.html#Trig-Functions

如果您想要更好的控制,您应该仔细评估您的需求.一些典型的方法是查找表的插值,汇编调用(通常很慢),或其他近似方案,如平方根的Newton-Raphson.


mct*_*ylr 5

我将尝试回答sin()一个C程序的情况,在当前的x86处理器上用GCC的C编译器编译(假设是Intel Core 2 Duo).

在C语言中的标准C库包括普通的数学函数,不包括在语言本身(例如pow,sincos用于电源,正弦,余弦及分别).其标题包含在math.h中.

现在在GNU/Linux系统上,这些库函数由glibc(GNU libc或GNU C Library)提供.但是GCC编译器希望您使用编译器标志链接到数学库(libm.so)-lm以启用这些数学函数的使用.我不确定为什么它不是标准C库的一部分.这些将是浮点函数的软件版本,或"软浮点".

旁白:将数学函数分开的原因是历史性的,并且仅仅是为了减少非常旧的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]

任何最近的(自2.9x左右)版本的gcc也提供了sin的内置版本,__builtin_sin()它将用于替换对C库版本的标准调用,作为优化.

我确信它和泥浆一样清晰,但希望能为您提供比您预期更多的信息,还有许多跳跃点可以让您自己了解更多信息.


Nor*_*sey 5

如果你想在软件而不是硬件中实现,那么寻找这个问题的明确答案的地方就是数字食谱的第5章.我的副本放在一个盒子里,所以我不能提供详细信息,但简短的版本(如果我记得这个权利)是你tan(theta/2)作为原始操作并从那里计算其他人.计算是通过一系列近似来完成的,但它的收敛速度比泰勒级数快得多.

对不起,如果不把手放在书上,我就无法记住更多.


Igo*_*hov 5

正如许多人所指出的那样,它依赖于实现.但据我了解你的问题,你对数学函数的真实软件实现很感兴趣,但只是没有设法找到它.如果是这种情况,那么你在这里:

  • http://ftp.gnu.org/gnu/glibc/下载glibc源代码
  • 查看dosincos.c位于解压缩的glibc root\sysdeps\ieee754\dbl-64文件夹中的文件
  • 类似地,您可以找到数学库其余部分的实现,只需查找具有适当名称的文件即可

您还可以查看具有.tbl扩展名的文件,它们的内容只不过是二进制形式的不同函数的预计算值的巨大表.这就是为什么实现如此之快:不是计算他们使用的任何系列的所有系数,而是快速查找,这快得多.顺便说一句,他们确实使用Tailor系列来计算正弦和余弦.

我希望这有帮助.


Mos*_*ops 5

没有什么比击中源代码并看到某个人在一个常用的库中实际完成它的方式; 让我们看一下特别是一个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