【问题标题】:What is the fastest way to compute sin and cos together?一起计算 sin 和 cos 的最快方法是什么?
【发布时间】:2011-02-10 15:11:47
【问题描述】:

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

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

  • Vlad 说,有一个 asm 命令 FSINCOS 计算它们(与单独调用 FSIN 几乎同时)

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

  • caf指出,函数sincossincosf可能是可用的,只需包含math.h即可直接调用

  • tanascius 使用查找表的方法存在争议。 (但在我的计算机上和基准测试场景中,它的运行速度比 sincos 快 3 倍,而 32 位浮点的精度几乎相同。)

  • Joel Goodwin 链接到一种有趣的方法,该方法是一种具有相当高准确度的极快近似技术(对我来说,这比查表还要快)

【问题讨论】:

  • 另请参阅有关 sin/cos 原生实现的问题:stackoverflow.com/questions/1640595
  • 如果您关心速度而不是准确性,请尝试使用 sinx ~ x-x^3/6cosx~1-x^2/4 作为近似值。您可以在任一系列中添加术语,因为您更加重视准确性(en.wikipedia.org/wiki/Taylor_series 向下滚动到 trig taylor 系列。)请注意,这是逼近您想要的任何可微分 n 次的函数的一般方法。因此,如果你有一些更大的函数,那个正弦和余弦属于你,如果你近似它而不是独立的 sin,cos,你将获得更大的速度。
  • 这是一种很差的技术,准确度很差。见乔尔·古德温的帖子。泰勒系列已在下面发布。请将其发布为答案。
  • 这取决于您的要求,如果您想要准确度,泰勒级数将是一个良好的近似值,仅当您需要接近某个点 x 的值 x_0,然后将泰勒级数扩展到x_0 而不是 0。这将在x_0 附近为您提供出色的准确度,但您走得越远结果越差。当您查看给定的 asnwer 并尝试使用远离 0 的值时,您可能认为准确性很差。答案是 sin,cos 扩大到 0 左右。

标签: c# c++ c algorithm math


【解决方案1】:

现代 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 处理器上添加,因此您几乎找不到不支持它的处理器。

编辑:
Intel's documentation 指出 FSINCOS 仅比 FDIV 慢 5 倍(即浮点除法)。

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

编辑:
第一个示例链接已失效,但有still a version at the Wayback Machine

【讨论】:

  • @phkahler:那太好了。不知道现代编译器是否使用了这种优化。
  • fsincos 指令不是“相当快”。英特尔自己的优化手册引用它在最近的微架构上需要 119 到 250 个周期。相比之下,英特尔的数学库(与 ICC 一起分发)可以在不到 100 个周期内单独计算 sincos,使用使用 SSE 而不是 x87 单元的软件实现。同时计算两者的类似软件实现可能更快。
  • @Vlad:ICC 数学库不是开源的,我没有重新分发它们的许可证,所以我不能发布程序集。但是,我可以告诉您,没有内置的sin 计算可供他们利用;他们使用与其他所有人相同的 SSE 指令。对于您的第二条评论,相对于fdiv 的速度无关紧要;如果有两种方法可以做某事,其中一种方法的速度是另一种的两倍,那么将较慢的方法称为“快”是没有意义的,无论相对于某些完全不相关的任务需要多长时间。
  • 他们库中的软件sin 函数可提供完整的双精度精度。 fsincos 指令提供了更高的精度(双倍扩展),但在大多数调用 sin 函数的程序中,额外的精度会被丢弃,因为它的结果通常会通过以后的算术运算或存储到内存中四舍五入为双精度.在大多数情况下,它们在实际使用中提供相同的准确性。
  • 还要注意fsincos 本身并不是一个完整的实现;您需要一个额外的范围缩减步骤来将参数放入fsincos 指令的有效输入范围内。库 sincos 函数包括这种缩减以及核心计算,因此它们甚至比我列出的周期时间可能表明的更快(相比之下)。
【解决方案2】:

现代 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?
  • 请注意,-ffast-math-mfpmath 在某些情况下会导致不同的结果。
  • mfpmath=387 将强制 gcc 使用 x87 指令而不是 SSE 指令。我怀疑 MSVC 有类似的优化和标志,但我没有 MSVC 可以确定。使用 x87 指令可能会损害其他代码的性能,但您还应该查看我的其他答案,以使用 Intel 的 MKL。
  • 我来自 cygwin 的旧 gcc 3.4.4 对 fsinfcos 产生了 2 个单独的调用。 :-(
  • 已启用最高优化的 Visual Studio 2008 进行了尝试。它调用 2 个库函数 __CIsin__CIcos
【解决方案3】:

当您需要性能时,您可以使用预先计算好的 sin/cos 表(一个表就可以了,存储为字典)。嗯,这取决于您需要的准确度(可能表会很大),但它应该非常快。

【讨论】:

  • 然后输入值需要映射到 [0,2*pi] (或者更小,需要额外的检查),这个对 fmod 的调用会消耗性能。在我的(可能是次优的)实现中,我无法通过查找表获得性能。你有什么建议吗?
  • 预计算表几乎肯定会比调用sin 慢,因为预计算表会破坏缓存。
  • 这取决于表有多大。一个 256 条目的表通常足够准确,并且只使用 1Kb ......如果你经常使用它,它不会卡在缓存中而不会对应用程序的其余部分性能产生不利影响吗?
  • @Danvil:这是一个正弦查找表的示例en.wikipedia.org/wiki/Lookup_table#Computing_sines。但是,它假定您也已经将输入映射到 [0;2pi]。
  • @AndreasBrinck 我不会走那么远。视情况而定(TM)。现代缓存很大,而查找表很小。通常,如果您在内存布局上稍加注意,您的查找表就不需要对其余计算的缓存利用率产生任何影响。查找表适合缓存的事实是它如此快速的原因之一。即使在难以精确控制内存布局的 Java 中,我也通过查找表获得了巨大的性能提升。
【解决方案4】:

从技术上讲,您可以通过使用复数和Euler’s Formula 来实现这一点。因此,类似 (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();

应该一步给你正弦和余弦。这是如何在内部完成的,是正在使用的编译器和库的问题。这样做可能(并且可能)需要更长的时间(因为欧拉公式主要用于计算复杂的 exp 使用 sincos - 而不是相反)但可能有一些理论上可以优化。


编辑

GNU C++ 4.2 的&lt;complex&gt; 中的标头在polar 中使用sincos 的显式计算,因此它看起来不太适合在那里进行优化,除非编译器做了一些魔法(请参阅-ffast-math-mfpmath 切换如Chi’s answer 中所写)。

【讨论】:

  • 抱歉,欧拉公式实际上并没有告诉您如何计算某些东西,它只是一个将复指数与实三角函数相关联的恒等式(尽管非常有用) .一起计算正弦和余弦有好处,但它们涉及常见的子表达式,你的答案没有讨论这个。
【解决方案5】:

您可以计算其中一个,然后使用标识:

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

但正如@tanascius 所说,预先计算的表是可行的方法。

【讨论】:

  • 请注意,使用此方法涉及计算幂和平方根,因此如果性能很重要,请确保验证这实际上比直接计算其他三角函数更快。
  • sqrt() 通常在硬件方面进行了优化,因此它很可能比sin()cos() 更快。力量只是自我乘法,所以不要使用pow()。有一些技巧可以在没有硬件支持的情况下快速获得相当准确的平方根。最后,请务必在执行任何此操作之前进行概要分析。
  • 请注意,√(1 - cos^2 x) 不如直接计算 sin x 准确,尤其是当 x ~ 0 时。
  • 对于小 x,y=sqrt(1-x*x) 的泰勒级数非常好。您可以使用前 3 项获得良好的准确性,并且只需要几次乘法和一次移位。我在定点代码中使用过。
  • @phkahler:你的泰勒级数不适用,因为当 x ~ 0 时,因为 x ~ 1。
【解决方案6】:

如果你使用 GNU C 库,那么你可以这样做:

#define _GNU_SOURCE
#include <math.h>

您将获得 sincos()sincosf()sincosl() 函数的声明,它们一起计算这两个值 - 可能是您的目标架构的最快方式。

【讨论】:

    【解决方案7】:

    这个论坛页面上有一些非常有趣的东西,专注于寻找快速的良好近似值: 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-sine-cosine

    【讨论】:

    • 我也试过这个,它给了我很好的性能。但是 sin 和 cos 是独立计算的。
    • 我的感觉是这种正弦/余弦计算将比得到正弦并使用平方根近似得到余弦更快,但测试将验证这一点。正弦和余弦之间的主要关系是相位之一;是否可以编码,以便您可以通过考虑到这一点重新使用为相移余弦调用计算的正弦值? (这可能有点牵强,但不得不问)
    • 不是直接的(尽管问题正是问这个问题)。我需要值为 x 的 sin 和 cos,但无法知道我是否在其他地方巧合地计算了 x+pi/2 ...
    • 我在游戏中使用它来绘制一个粒子圈。既然只是视觉效果,效果就足够接近了,性​​能真的很可观。
    • 我没有留下深刻的印象; Chebyshev approximations 通常为您提供给定性能的最准确度。
    【解决方案8】:

    正如 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 章)中明确表示这是一个坏主意(目前): p>

    “另一个例子是预计算小表——例如, sin(x) 用于优化 3D 图形引擎中的旋转将 在现代机器上占用 365 × 4 字节。在处理器够用之前 比内存更快需要缓存,这是一个明显的速度 优化。如今,每次重新计算可能会更快,而不是 而不是支付由 表。

    “但是在未来,随着缓存变得越来越大,这种情况可能会再次发生变化。 更一般地说,许多优化是暂时的,很容易转向 随着成本比率的变化而陷入悲观。唯一知道的方法是 测量和观察。”(来自 Unix 编程艺术

    但是,从上面的讨论来看,并不是每个人都同意。

    【讨论】:

    • "365 x 4 字节"。您需要考虑闰年,因此实际上应该是 365.25 x 4 字节。或者,也许他的意思是使用圆周的度数而不是地球年的天数。
    • @Wallacoloo:很好的观察。我错过了。但错误在original
    • 哈哈。此外,他忽略了一个事实,即在该领域的许多电脑游戏中,您只需要有限数量的角度。如果您知道可能的角度,则没有缓存未命中。在这种情况下,我会使用表格,并给fsincos(CPU 指令!)尝试其他的。它通常与从一张大表中插入 sin 和 cos 一样快。
    【解决方案9】:

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

    但我会关注您在 AMD 的 ACML 和英特尔的 MKL 等库中找到的 SinCos 的快速实现。

    【讨论】:

      【解决方案10】:

      如果您愿意使用商业产品,并且正在同时计算多个 sin/cos 计算(因此您可以使用向量函数),您应该查看Intel's Math Kernel Library.

      它有一个sincos function

      根据该文档,在高精度模式下,核心 2 duo 上的平均 13.08 个时钟/元素,我认为这将比 fsincos 更快。

      【讨论】:

      • 类似地,在 OSX 上可以使用来自 Accelerate.framework 的 vvsincosvvsincosf。我相信 AMD 在他们的向量库中也有类似的功能。
      【解决方案11】:

      这篇文章展示了如何构造一个同时产生正弦和余弦的抛物线算法:

      DSP 技巧:Sin 和 Cos 的同时抛物线逼近

      http://www.dspguru.com/dsp/tricks/parabolic-approximation-of-sin-and-cos

      【讨论】:

      【解决方案12】:

      当性能对这类事情至关重要时,引入查找表并不罕见。

      【讨论】:

        【解决方案13】:

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

        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-余弦扩展因子。但我怀疑使用泰勒级数真的很快......
        • Chebyshev 在多项式函数逼近方面比 Taylor 好得多。不要使用泰勒近似。
        • 这里有一堆数字错误;分子和分母都很快变大,导致浮点错误。更不用说您如何确定“精度不够”是什么以及如何计算它?泰勒近似在单点附近很好;远离这一点,它们很快就会变得不准确并且需要大量的术语,这就是为什么 Timmmmm 关于切比雪夫近似(在给定间隔内创建良好的近似)的建议是一个很好的建议。
        【解决方案14】:

        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(x-y))。

        我们从查找表中得到 exp(iy)。并且因为 |x-y|很小(最多是 y 值之间距离的一半),泰勒级数将在几个方面很好地收敛,所以我们将其用于 exp(i(x-y))。然后我们只需要一个复数乘法就可以得到 exp(ix)。

        另一个不错的特性是您可以使用 SSE 对其进行矢量化。

        【讨论】:

          【解决方案15】:

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

          【讨论】:

            【解决方案16】:

            我在此处发布了一个涉及内联 ARM 汇编的解决方案,能够同时计算两个角度的正弦和余弦:Fast sine/cosine for ARMv7+NEON

            【讨论】:

              【解决方案17】:

              在 javascript 中同时准确而快速地近似 sin 和 cos 函数,可以在这里找到:http://danisraelmalta.github.io/Fmath/(轻松导入到 c/c++)

              【讨论】:

                【解决方案18】:

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

                【讨论】:

                  【解决方案19】:

                  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
                  

                  Assembly(对于 x86)没有 /fp:fast 但使用 /fp:precise(这是默认设置)调用单独的 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
                  

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

                  在我的“稍微”旧的系统(Intel Core 2 Duo E6750)上,使用最新的 MSVC 2019 编译器和适当的优化,我的基准测试显示 sincos 调用比单独的 sin 和 cos 调用快约 2.4 倍。

                  【讨论】:

                    猜你喜欢
                    • 1970-01-01
                    • 1970-01-01
                    • 2017-06-27
                    • 1970-01-01
                    • 1970-01-01
                    • 2011-02-25
                    • 1970-01-01
                    • 1970-01-01
                    • 2023-03-13
                    相关资源
                    最近更新 更多