一起计算正弦和余弦的最快方法是什么?


100

我想同时计算一个值的正弦和余弦(例如,创建旋转矩阵)。当然,我可以像一样一个接一个地分别计算它们a = cos(x); b = sin(x);,但是我想知道在同时使用两个值时是否有更快的方法。

编辑: 总结到目前为止的答案:

  • 弗拉德说,那是asm命令FSINCOS计算这两个(几乎在FSIN单独调用的同时)

  • 注意到的那样,这种优化有时已经由编译器完成(使用优化标志时)。

  • caf指出,函数sincossincosf可能是可用的,可以通过直接包括math.h

  • 关于使用查找表的 tanascius方法存在争议。(但是,在我的计算机上和在基准测试情况下,它的运行速度比以前快3倍sincos 32位浮点的精度几乎。)

  • 乔尔·古德温(Joel Goodwin)链接了一种极快的近似技术的有趣方法,具有很高的准确度(对我来说,这比查表还要快)


1
另请参阅有关sin / cos的本机实现的以下问题:stackoverflow.com/questions/1640595
Joel Goodwin,2010年

1
如果您更关心速度而不是准确性,请尝试sinx ~ x-x^3/6cosx~1-x^2/4作为近似值。您可以在任何一个系列中添加术语,因为您更加注重准确性(en.wikipedia.org/wiki/Taylor_series向下滚动到trig taylor系列)n。因此,如果您具有正弦和余弦所属的更大的函数,那么如果您近似地估计它而不是正弦的余弦,您将获得更大的速度。
ldog

这是一种糟糕的技术,准确性非常差。参见Joel Goodwin的帖子。泰勒系列已发布在下面。请发布它作为答案。
Danvil 2010年

1
好吧,这取决于您的要求,如果您希望精度,只有在需要接近某个点的值时,泰勒级数才是一个很好的近似值,然后将您的泰勒级数展开,而不是将其扩展为0。这将为您提供卓越的精度,但您走得更远结果越差。您可能认为精度是很糟糕的,因为您查看了给定的增量并尝试了远离的值。这个答案与罪孽有关,cos扩展到0左右xx_0x_0x_00
。– ldog

Answers:


52

现代的Intel / AMD处理器具有FSINCOS用于同时计算正弦和余弦函数的指令。如果需要强大的优化,也许应该使用它。

这是一个小示例:http : //home.broadpark.no/~alein/fsincos.html

这是另一个示例(用于MSVC):http : //www.codeguru.com/forum/showthread.php? t=328669

这是另一个示例(带有gcc):http : //www.allegro.cc/forums/thread/588470

希望其中之一能有所帮助。(抱歉,我自己没有使用此指令。)

由于它们在处理器级别受支持,因此我希望它们比表查找要快得多。

编辑:
Wikipedia建议FSINCOS在387个处理器中添加该处理器,因此您几乎找不到不支持该处理器的处理器。

编辑:
英特尔的文档指出,该FSINCOS速度仅比FDIV(即浮点除法)慢5倍。

编辑:
请注意,并非所有现代编译器都会将sine和cosine的计算优化为对的调用FSINCOS。特别是我的VS 2008并不是那样。

编辑:
第一个示例链接已失效,但Wayback Machine上仍有一个版本


1
@phkahler:太好了。不知道现代编译器是否使用过这种优化。
弗拉德(Flad)2010年

12
fsincos指令不是 “很快”。英特尔自己的优化手册指出,在最新的微体系结构上需要119至250个周期。相比之下,使用使用SSE而不是x87单元的软件实现,英特尔的数学库(随ICC分发)可以分别计算sincos在不到100个周期内进行计算。同时计算两者的类似软件实现可能会更快。
斯蒂芬·佳能

2
@Vlad:ICC数学库不是开源的,并且我没有重新分发它们的许可证,因此我无法发布程序集。我可以告诉您,没有内置sin计算可供他们利用。他们使用与其他所有人相同的SSE指令。第二点,相对速度fdiv无关紧要。如果有两种方法可以做某事,而一种方法的速度是另一种方法的两倍,则将较慢的一个称为“快速”是没有意义的,无论相对于某些完全无关的任务需要花费多长时间。
斯蒂芬·佳能

1
sin其库中的软件功能可提供完全的双精度精度。该fsincos指令提供了更高的精度(双精度扩展),但是在大多数调用该sin函数的程序中,多余的精度被抛弃了,因为其结果通常通过以后的算术运算或存储到内存中而四舍五入为双精度。在大多数情况下,它们为实际使用提供相同的精度。
斯蒂芬·佳能

4
还要注意,这fsincos并不是一个完整的实现。您需要执行其他范围缩小步骤,以将参数放入fsincos指令的有效输入范围。库sincos函数包括这种减少以及核心计算,因此(通过比较)它们甚至比我列出的周期时序还要快(通过比较)。
斯蒂芬·佳能

39

现代的x86处理器具有fsincos指令,该指令可以完全满足您的要求-同时计算sin和cos。一个好的优化编译器应检测出计算出相同值的sin和cos的代码,并使用fsincos命令执行此操作。

花了一些时间才能使编译器标志起作用,但是:

$ gcc --version
i686-apple-darwin9-gcc-4.0.1 (GCC) 4.0.1 (Apple Inc. build 5488)
Copyright (C) 2005 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

$ cat main.c
#include <math.h> 

struct Sin_cos {double sin; double cos;};

struct Sin_cos fsincos(double val) {
  struct Sin_cos r;
  r.sin = sin(val);
  r.cos = cos(val);
  return r;
}

$ gcc -c -S -O3 -ffast-math -mfpmath=387 main.c -o main.s

$ cat main.s
    .text
    .align 4,0x90
.globl _fsincos
_fsincos:
    pushl   %ebp
    movl    %esp, %ebp
    fldl    12(%ebp)
    fsincos
    movl    8(%ebp), %eax
    fstpl   8(%eax)
    fstpl   (%eax)
    leave
    ret $4
    .subsections_via_symbols

Tada,它使用fsincos指令!


这很酷!您能解释一下-mfpmath = 387在做什么吗?它也可以与MSVC一起使用吗?
Danvil 2010年

1
请注意,-ffast-math-mfpmath导致在某些情况下不同的结果。
Debilski

3
mfpmath = 387将强制gcc使用x87指令而不是SSE指令。我怀疑MSVC具有类似的优化和标志,但是我不确定要使用MSVC。使用x87指令可能会损害其他代码的性能,但您也应该看看我的其他答案,才能使用英特尔的MKL。
Chi

我来自cygwin的旧版gcc 3.4.4产生了对fsin和的两个单独调用fcos。:-(
Vlad

尝试在Visual Studio 2008中启用最高优化。它调用2个库函数__CIsin__CIcos
弗拉德(Flad)2010年

13

当您需要性能时,可以使用预先计算的sin / cos表(一个表可以作为字典存储)。好吧,这取决于您需要的精度(也许表格会很大),但是它应该非常快。


然后,需要将输入值映射到[0,2 * pi](或更小值,需要附加检查),并且对fmod的调用会削弱性能。在我的(可能不太理想的)实现中,我无法通过查找表获得性能。您在这里有什么建议吗?
Danvil 2010年

11
预计算表几乎肯定会比仅调用慢,sin因为预计算表将破坏高速缓存。
Andreas Brinck

1
这取决于桌子有多大。256个条目的表通常足够准确,并且仅使用1Kb ...如果您经常使用它,它是否会卡在缓存中而不会对应用程序的其他性能产生不利影响?
男孩先生2010年

@Danvil:这是一个正弦查找表的示例,网址为en.wikipedia.org/wiki/Lookup_table#Computing_sines。但是,它假定您也已经将输入映射到[0; 2pi]。
tanascius

@AndreasBrinck我不会走那么远。它取决于(TM)。现代缓存很大,查找表很小。通常,如果您在内存布局上稍加注意,查找表无需对其余计算的缓存利用率产生任何影响。查找表适合高速缓存的事实是它如此之快的原因之一。即使在难以精确控制内存布局的Java中,使用查找表也获得了巨大的性能胜利。
贾罗德·史密斯

13

从技术上讲,您可以通过使用复数和Euler公式来实现。因此,类似(C ++)

complex<double> res = exp(complex<double>(0, x));
// or equivalent
complex<double> res = polar<double>(1, x);
double sin_x = res.imag();
double cos_x = res.real();

应该一步就给您正弦和余弦。在内部如何完成此操作是所使用的编译器和库的问题。用这种方法可能(而且可能)花费更长的时间(只是因为Euler公式主要用于exp使用sincos-而不是反过来计算复数),但是可能会有一些理论上的优化可能。


编辑

在标题<complex>为GNU C ++ 4.2使用的明确的计算sincos里面polar,因此它不寻求优化技术太好那里,除非编译器做了一些魔术(见-ffast-math-mfpmath开关写在驰的答案)。


抱歉,但是欧拉公式实际上并没有告诉您如何计算某些东西,它只是一个将复指数与实际三角函数相关联的标识(尽管非常有用)。一起计算正弦和余弦是有好处的,但是它们涉及共同的子表达式,您的答案不在此讨论。
Jason S

12

您可以计算其中一个,然后使用身份:

cos(x)2 = 1-sin(x)2

但是正如@tanascius所说,要使用预先计算的表。


8
并且请注意,使用此方法涉及计算幂和平方根,因此,如果性能很重要,请确保验证它实际上比直接计算其他触发函数要快。
泰勒·麦克亨利

4
sqrt()通常在硬件上进行了优化,因此它可能会比sin()或更快cos()。功效只是自我乘法,因此请勿使用pow()。有一些技巧可以在没有硬件支持的情况下快速获得合理准确的平方根。最后,在执行任何此操作之前,请确保先进行概要分析。
deft_code 2010年

12
注意,√(1 - COS ^ 2×)比计算罪直接X不太准确,尤其是当x为0〜
kennytm

1
对于小x,y = sqrt(1-x * x)的泰勒级数非常好。您可以使用前3个项获得良好的准确性,并且只需要几个乘法和一个移位即可。我在定点代码中使用过它。
phkahler

1
@phkahler:您的泰勒级数不适用,因为当x〜0时,cos
x〜1。– kennytm 2010年

10

如果使用GNU C库,则可以执行以下操作:

#define _GNU_SOURCE
#include <math.h>

并且您将获得sincos()sincosf()sincosl()一起计算两个值的函数的声明-大概是针对目标体系结构的最快方式。


8

该论坛页面上有很多非常有趣的内容,这些内容专注于快速找到良好的近似值:http : //www.devmaster.net/forums/showthread.php? t= 5784

免责声明:我自己没有使用过这些东西。

2018年2月22日更新:Wayback Machine是现在访问原始页面的唯一方法:https ://web.archive.org/web/20130927121234/http: //devmaster.net/posts/9648/fast-and-accurate-正弦余弦


我也尝试过这个,它给了我很好的表现。但是,正弦和余弦是独立计算的。
Danvil 2010年

我的感觉是,这种正弦/余弦计算将比获得正弦并使用平方根近似来获得余弦更快,但是通过测试可以证明这一点。正弦和余弦之间的主要关系是相位之一。是否可以进行编码,以便考虑到这一点而重新使用为相移余弦调用计算的正弦值?(这可能是个难题,但不得不问)
Joel Goodwin

不直接(尽管问题确实如此)。我需要sin和值x的cos,并且没有办法知道是否在其他地方巧合地计算了x + pi / 2 ...
Danvil 2010年

我在游戏中使用它绘制了一个粒子圆圈。由于这只是视觉效果,因此效果足够接近,而且性能确实令人印象深刻。
Maxim Kamalov 2015年

我没有留下深刻的印象;切比雪夫(Chebyshev)逼近通常可为您提供给定性能的最高精度。
杰森S

7

如caf所示,许多C数学库已经具有sincos()。值得注意的例外是MSVC。

  • Sun至少从1987年(二十三年;我有一份精装的手册页)开始拥有sincos()
  • HPUX 11于1997年推出(但HPUX 10.20中没有)
  • 在2.1版(1999年2月)中添加到glibc
  • 成为gcc 3.4(2004)中的内置__builtin_sincos()。

关于查找,Eric S. Raymond在“ Unix编程艺术”(2004年)(第12章)中明确表示这是一个坏主意(目前)。

“另一个例子是预先计算小型表,例如,按度数计算的sin(x)表用于优化3D图形引擎中的旋转,在现代计算机上将占用365×4字节。在处理器获得比内存更快的速度之前,需要进行缓存,这显然是一种速度优化,如今,每次重新计算可能要快得多,而不是要为表造成的额外高速缓存未命中的百分比付费。

“但是将来,随着高速缓存的增长,这种情况可能会再次发生。更广泛地讲,许多优化是暂时的,并且随着成本比率的变化很容易变成悲观。唯一的了解方法就是进行衡量和观察。” (摘自Unix编程艺术

但是,从上面的讨论来看,并非所有人都同意。


10
“ 365 x 4字节”。您需要考虑leap年,因此实际上应该是365.25 x 4字节。或者,也许他的意思是使用圆圈中的度数而不是地球年中的天数。
Ponkadoodle 2012年

@Wallacoloo:很好的观察。我错过了它。但是错误是在原来的
约瑟夫·昆西

大声笑。另外,他忽略了以下事实:在该区域的许多计算机游戏中,您只需要有限数量的角度。如果您知道可能的角度,那么就不会有缓存丢失。在这种情况下,我将完全使用表,并fsincos尝试(CPU指令!)其他表。它的速度通常与从一张大桌子上插入正弦和余弦的速度一样快。
Erich Schubert

5

我认为查找表不一定是解决此问题的好主意。除非您的精度要求非常低,否则表格必须很大。而且,当从主存储器中获取值时,现代CPU可以执行大量计算。这不是可以通过论证(甚至不是我的),测试,测量和考虑数据来正确回答的问题之一。

但是我希望您能在诸如AMD的ACML和英特尔的MKL之类的库中找到SinCos的快速实现。


3

如果您愿意使用商业产品,并且同时计算许多sin / cos计算(以便可以使用矢量函数),则应查看Intel的Math Kernel Library。

具有sincos功能

根据该文档,在高精度模式下,内核2二重奏的平均时钟时间为13.08个时钟/元素,我认为这将甚至快于fsincos。


1
同样,在OSX上,可以使用vvsincosvvsincosf Accelerate.framework从中。我相信AMD在其向量库中也具有类似的功能。
斯蒂芬·佳能



2

对于创造性的方法,如何扩展泰勒级数?由于它们具有相似的术语,因此您可以执行以下伪操作:

numerator = x
denominator = 1
sine = x
cosine = 1
op = -1
fact = 1

while (not enough precision) {
    fact++
    denominator *= fact
    numerator *= x

    cosine += op * numerator / denominator

    fact++
    denominator *= fact
    numerator *= x

    sine += op * numerator / denominator

    op *= -1
}

这意味着您要执行以下操作:从x和1开始计算正弦和余弦,遵循以下模式-减去x ^ 2/2!从余弦中减去x ^ 3/3!从正弦开始,加x ^ 4/4!余弦,加x ^ 5/5!正弦...

我不知道这是否会表现出色。如果您需要的精度比内置sin()和cos()所提供的精度低,则可以选择。


实际上,i-正弦扩展因子是x / i乘以i-余弦扩展因子。但我会怀疑使用泰勒级数真的很快...
Danvil 2010年

1
对于多项式函数逼近,切比雪夫比泰勒好得多。不要使用泰勒近似。
Timmmm

这里有很多数字化的假人。分子和分母都迅速变大,并导致浮点错误。更不用说您如何确定什么是“精度不够”以及如何计算?在单点附近,泰勒近似值很好。远离这一点,它们很快就会变得不准确,需要大量的术语,这就是为什么Timmmm关于Chebyshev逼近的建议(在给定的时间间隔内创建好的逼近)是一个很好的建议。
杰森S

2

CEPHES库中有一个很好的解决方案,它可以非常快地运行,您可以灵活地添加/删除精度,而这会花费更多/更少的CPU时间。

请记住,cos(x)和sin(x)是exp(ix)的实部和虚部。因此,我们要计算exp(ix)来获得两者。我们预先计算了一些介于0和2pi之间的y离散值的exp(iy)。我们将x移到间隔[0,2pi)。然后我们选择最接近x的y并写出
exp(ix)= exp(iy +(ix-iy))= exp(iy)exp(i(xy))。

我们从查找表中获得exp(iy)。而且| xy | 很小(最多y值之间的距离的一半),泰勒级数将在短时间内很好地收敛,因此我们将其用于exp(i(xy))。然后我们只需要一个复杂的乘法就可以得到exp(ix)。

另一个不错的特性是您可以使用SSE将其向量化。


2

您可能想看看http://gruntthepeon.free.fr/ssemath/,它提供了一个受CEPHES库启发的SSE矢量化实现。它具有良好的准确度(与sin / cos的最大偏差约为5e-8)和速度(在一次调用中略胜过fsincos,并且在多个值上均是明显的赢家)。




0

您是否考虑过为两个函数声明查找表?您仍然必须“计算” sin(x)和cos(x),但是如果您不需要很高的准确性,那肯定会更快。


0

MSVC编译器可以使用(内部)SSE2函数

 ___libm_sse2_sincos_ (for x86)
 __libm_sse2_sincos_  (for x64)

在优化的版本中,如果指定了适当的编译器标志(至少/ O2 / arch:SSE2 / fp:fast)。这些函数的名称似乎暗示它们不计算单独的sin和cos,而是“一步一步”计算。

例如:

void sincos(double const x, double & s, double & c)
{
  s = std::sin(x);
  c = std::cos(x);
}

使用/ fp:fast汇编(对于x86):

movsd   xmm0, QWORD PTR _x$[esp-4]
call    ___libm_sse2_sincos_
mov     eax, DWORD PTR _s$[esp-4]
movsd   QWORD PTR [eax], xmm0
mov     eax, DWORD PTR _c$[esp-4]
shufpd  xmm0, xmm0, 1
movsd   QWORD PTR [eax], xmm0
ret     0

不带/ fp:fast但带/ fp:precise(这是默认设置)的汇编程序(对于x86)调用单独的sin和cos:

movsd   xmm0, QWORD PTR _x$[esp-4]
call    __libm_sse2_sin_precise
mov     eax, DWORD PTR _s$[esp-4]
movsd   QWORD PTR [eax], xmm0
movsd   xmm0, QWORD PTR _x$[esp-4]
call    __libm_sse2_cos_precise
mov     eax, DWORD PTR _c$[esp-4]
movsd   QWORD PTR [eax], xmm0
ret     0

因此,/ fp:fast是sincos优化所必需的。

但请注意

___libm_sse2_sincos_

可能不像

__libm_sse2_sin_precise
__libm_sse2_cos_precise

由于名称末尾缺少“精确度”。

在我的“略微”较旧的系统(英特尔酷睿2 Duo E6750)上,使用最新的MSVC 2019编译器并进行了适当的优化,我的基准测试显示,sincos调用比单独的sin和cos调用快约2.4倍。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.