正弦和余弦很慢,有其他替代品吗?

10

我需要让我的游戏按照特定角度移动。为了实现这一点,我通过正弦和余弦函数获取角度向量。然而,正弦和余弦函数是我的瓶颈。我相信我并不需要这么高的精度。是否有一种替代C语言sin和cos函数以及查找表的方法,可以在保持相当精确的同时速度非常快?

我曾经发现过这个:

float Skeleton::fastSin( float x )
{
    const float B = 4.0f/pi;
    const float C = -4.0f/(pi*pi);

    float y = B * x + C * x * abs(x);

    const float P = 0.225f;

    return P * (y * abs(y) - y) + y; 
}

很不幸,这似乎行不通。当我使用这个正弦函数而不是C的正弦函数时,我得到了显著不同的结果。

谢谢


1
标准正弦函数的偏差从哪里开始? - James McLeod
9
如果 C 语言中的 sin 和 cos 函数对于你的游戏确实太慢,那么每帧需要调用它们多少次?你确定你没有重复计算相同角度的正弦和余弦值吗? - user180247
8个回答

8

查找表是标准解决方案。您还可以使用两个查找表,一个用于角度,一个用于角度的十分之一,并利用sin(A + B) = sin(a)cos(b) + cos(A)sin(b)


4
几乎从来不是一个好主意。在 MathUtils 上使用静态方法会更好。 - duffymo
6
一个Singleton作为面向对象的概念是一个玩笑。更重要的是,在这种情况下,这个对象会是什么? - Puppy
我也很好奇一个全局变量(单例)如何符合面向对象的设计原则?无论如何,如果调用sin/cos函数速度不够快,我想你真的需要考虑是否需要使用面向对象的方法。 - devoured elysium
13
罪孽并不是什么好事。 - rerun
最近有人告诉我,查找表可能比简单计算慢,这取决于内存层次结构(缓存等)涉及的层数。那个fastsin函数很可能比从RAM中获取页面更快。除了是整数之外,查找表代码并不会更简单,因此可能无法证明该内存访问的合理性。 - user180247
显示剩余2条评论

7
对于您的fastSin()函数,您应该查看其文档以了解它适用的范围。您所使用的单位可能过大或过小,并将其缩放以适合该函数预期的范围可能会使其更好地工作。
编辑:
有人提到通过减去PI将其转换为所需的范围,但显然有一个名为fmod的函数可以对浮点数/双精度数进行模除运算,因此这应该可以实现:
#include <iostream>
#include <cmath>

float fastSin( float x ){
    x = fmod(x + M_PI, M_PI * 2) - M_PI; // restrict x so that -M_PI < x < M_PI
    const float B = 4.0f/M_PI;
    const float C = -4.0f/(M_PI*M_PI);

    float y = B * x + C * x * std::abs(x);

    const float P = 0.225f;

    return P * (y * std::abs(y) - y) + y; 
}

int main() {
    std::cout << fastSin(100.0) << '\n' << std::sin(100.0) << std::endl;
}

我不知道fmod的价格如何,所以接下来我会进行一个快速的基准测试。

基准测试结果

我使用-O2编译并使用Unix的time程序运行了这个结果:

int main() {
    float a = 0;
    for(int i = 0; i < REPETITIONS; i++) {
        a += sin(i); // or fastSin(i);
    }
    std::cout << a << std::endl;
}

结果是,如果fastSin需要5秒钟的时间,那么sin大约要慢1.8倍(即需要9秒)。准确性也似乎相当不错。
如果您选择这种方法,请确保使用优化编译(在gcc中使用-O2)。

1
事实上,在我的计算器上运行这个小程序,我发现它在0到3.2之间给出了足够好的结果,但是超过这个范围,它会非常快地出现错误。 :) - sarnold
2
那个fastsin函数只对+/-pi有效。我使用它的一个版本,采用定点(16位整数),在整个圆上获得约10位精度。它具有完美的对称性,并且恰好命中0和1,这对我的应用程序非常重要。 - phkahler
常量0.225f并不精确,因此我认为fastSin无法完全匹配任何值... - R.. GitHub STOP HELPING ICE
@R,将其插入并自行编译。phkahler的评论是正确的。 - Brendan Long
1
sinf()函数在这里比fastSin快大约1.6倍,以下是我进行基准测试的代码:float a = 0.0f; for(int j =0;j < 10;j++) { DWORD start = timeGetTime(); for(int i = 0; i < 10000000;i++) { a = // sinf(a); fastSin(a);; a+=0.107f; } cout< - hevi
你也可以使用以下规则,通过角度的正弦值来计算其余弦值:cos^2(x) + sin^2(x) = 1,因此 cos(x) = sqrt(1-sin^2(x)),sin(x) = sqrt(1-cos^2(x))。 - user274595

6
我知道这已经是一个老话题了,但对于有同样问题的人来说,这里有一个技巧。
在2D和3D旋转中,许多时候所有向量都会以固定角度旋转。不要在每个循环周期内调用cos()sin(),而是在循环之前创建一个变量,其中包含cos(angle)sin(angle)的值。您可以在循环中使用此变量。这样函数只需要被调用一次。

1
并非每个编译器都会对此进行优化。我使用的一些图像处理函数在调试模式下需要超过十秒钟,而在发布模式下只需要几分之一秒。如果您经常使用该功能,则非常不方便。尽量不要过度依赖编译器优化代码,而是自己轻松地完成它。 - JMRC
1
你在开玩笑吗?所以你抱怨编译器在调试模式下没有进行优化?那又怎样?学习一下强度降低和公共子表达式...说真的,你不会在生产环境中使用调试编译,对吧? - Dredok
2
不,但我宁愿花一个小时进行调试,也不愿意花10个小时或一天的时间进行调试,而代替一周的时间。尤其是如果重写代码只需要不到一分钟的时间。保持代码整洁并没有错,这是一个好习惯。 - JMRC

4
如果你将fastSin中的return重新表述为
return (1-P) * y + P * (y * abs(y))

并将y重写为(对于x>0)

y = 4 * x * (pi-x) / (pi * pi)

你可以看到,y是对sin(x)的二次抛物线一阶逼近,选择它使它穿过(0,0),(pi/2,1)和(pi,0),并且关于x=pi/2对称。因此,我们只能期望我们的函数在0到pi之间是一个很好的逼近值。如果我们想要超出那个范围的值,我们可以使用sin(x)的2-pi周期性和sin(x+pi)=-sin(x)的性质。
y*abs(y)是一个“修正项”,也通过这三个点。 (我不确定为什么使用y*abs(y)而不只是y*y,因为在0-pi范围内y是正的)。
这种总体逼近函数的形式保证了两个函数y和y*y的线性混合,(1-P)*y+P*y*y也会穿过(0,0),(pi/2,1)和(pi,0)。
我们可能期望ysin(x)的一个不错的近似值,但希望通过选择一个好的P值来获得更好的近似值。
一个问题是“如何选择P?”。个人而言,我会选择在0到pi/2区间上产生最小RMS误差的P值。(不确定这是否是选择 P的方法)

Expresion for approximation error

将此最小化得到P

Derivative of approximation error

这可以被重新排列并解决为p

enter image description here

沃尔夫拉姆阿尔法将初始积分计算为二次方程。
E = (16 π^5 p^2 - (96 π^5 + 100800 π^2 - 967680)p + 651 π^5 - 20160 π^2)/(1260 π^4)

最小值为

min(E) = -11612160/π^9 + 2419200/π^7 - 126000/π^5 - 2304/π^4 + 224/π^2 + (169 π)/420 
       ≈ 5.582129689596371e-07

p = 3 + 30240/π^5 - 3150/π^3 
  ≈ 0.2248391013559825

这个非常接近指定的 P=0.225

您可以通过添加额外的校正项来提高逼近精度,使其形式类似于 return (1-a-b)*y + a y * abs(y) + b y * y * abs(y)。我会像上面那样找到ab,这次需要解决两个关于ab的线性方程组,而不是一个关于p的方程。我不会推导,因为它很繁琐,转换成LaTeX图片也很麻烦... ;)

注意:回答另一个问题时,我想到了另一个有效的P选择。 问题在于使用反射将曲线延伸到(-pi,0)处在x=0处留下了一个折痕。但是,我怀疑我们可以选择P,使得折痕变得平滑。 为此,在x=0处取左右导数,并确保它们相等。这给出了一个关于P的方程。


实现了这个改进版本,适用于0-pi范围,并与现有的fast_sin近似进行比较。代码在这里:http://gist.github.com/988459,实时版本在这里:http://ideone.com/oWdkW。看起来它可以获得两个更多的小数精度,代价是一个额外的乘法和加法。 - Michael Anderson

3

你可以计算一个包含256个值的表S,从sin(0)到sin(2 * pi)。然后,如果要找到sin(x),将x带回[0, 2*pi],可以从表中选取两个值S[a]和S[b],使得a < x < b。通过线性插值,应该可以得到一个相当不错的近似值。

  • 内存节省技巧:实际上只需要存储[0,pi / 2]范围内的值,并使用sin(x)的对称性。
  • 增强技巧:由于非平滑导数问题,线性插值可能会出现问题,人眼在动画和图形方面很擅长发现这种故障。因此可以使用三次插值。

但是正弦和余弦函数是无限可微的,因此使用这些函数不存在非光滑导数的情况。 - duffymo
1
@duffymo - 我猜这意味着正弦的分段线性插值近似具有非平滑导数,在一条线结束和下一条线开始的点处会出现突变。获得平滑插值的一种方法是使用立方插值,使得端点具有正确的导数。原理类似于矢量图形中的平滑Bezier点,但数学可能更简单(我记得Bezier数学很麻烦)。 - user180247

2

对于度数,如何处理:

x*(0.0174532925199433-8.650935142277599*10^-7*x^2)

对于弧度,如何处理:

x*(1-0.162716259904269*x^2)

分别在 -45、45 和 -pi/4pi/4 时的结果是什么?


1
这个(即fastsin函数)是使用抛物线逼近正弦函数。我怀疑它只适用于-π和+π之间的值。幸运的是,您可以不断添加或减去2π,直到进入此范围。(编辑以指定使用抛物线逼近正弦函数。)

1
哈!我的意思是引用的函数是一个抛物线。实际上是两个抛物线。 - James McLeod
1
无论这是否是一个好主意,取决于它是否为给定的应用程序提供足够的准确性和速度 - 除了原帖作者以外,很难有人确定这一点。 - James McLeod
1
@詹姆斯 - 顺带一提,我想这就意味着我是一个无效的开发者。你怎么敢!愿你的所有函数都没有闭合形式! - user180247
2
我没有做出任何建议,先生。我指出了建议的函数(fastSin)使用抛物线,因此它可能只有有限的范围。 - James McLeod
1
@duffymo Taylor级数并不一定是在一系列值范围内最好的逼近方法。为此,您需要更复杂的东西。对于0-pi/2范围内的sin(x),抛物线4*x*(pi-x)/pi*pi表现得非常出色。 - Michael Anderson
显示剩余9条评论

1

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接