C ++(和数学):三角函数的快速逼近

时间:2012-06-29 11:44:18

标签: c++ performance trigonometry

我知道这是一个反复出现的问题,但我还没有真正找到有用的答案。我基本上是在寻找C ++中函数acos的快速近似值,我想知道我是否可以大大超过标准函数。

但是你们中的一些人可能对我的具体问题有所了解:我正在编写一个我需要非常快速的科学计划。主算法的复杂性归结为计算以下表达式(多次使用不同的参数):

sin( acos(t_1) + acos(t_2) + ... + acos(t_n) )

其中t_i是已知的实数(双)数,n非常小(如小于6)。我需要至少1e-10的精度。我目前正在使用标准sinacos C ++函数。

你认为我能以某种方式显着提高速度吗?对于那些了解某些数学的人,你认为扩展该正弦以获得t_i(仅涉及平方根)的代数表达是明智的吗?

谢谢你的答案。

7 个答案:

答案 0 :(得分:5)

以下代码提供了sin()acos()的简单实现,这些实现应满足您的准确性要求,并且您可能需要尝试。请注意,您的平台上的数学库实现很可能针对该平台的特定硬件功能进行了高度调整,并且可能还在汇编中进行编码以实现最高效率,因此不能满足硬件细节的简单编译C代码不太可能提供更高的性能,即使精度要求从完全双倍精度稍微放松。正如Viktor Latypov指出的那样,搜索不需要昂贵调用超越数学函数的算法替代方案也是值得的。

在下面的代码中,我试图坚持使用简单,可移植的结构。如果您的编译器支持rint()函数[由C99和C ++ 11指定],您可能希望使用它而不是my_rint()。在某些平台上,对floor()的调用可能很昂贵,因为它需要动态更改机器状态。我们希望内联函数my_rint()sin_core()cos_core()asin_core()以获得最佳性能。您的编译器可以在高优化级别自动执行此操作(例如,使用-O3进行编译时),或者您可以为这些函数添加适当的内联属性,例如:内联或__inline取决于您的工具链。

我对平台一无所知我选择了简单的多项式近似,使用Estrin方案和Horner方案进行评估。有关这些评估方案的说明,请参阅维基百科:

http://en.wikipedia.org/wiki/Estrin%27s_schemehttp://en.wikipedia.org/wiki/Horner_scheme

近似值本身属于极小极大类型,并使用Remez算法为此答案自定义生成:

http://en.wikipedia.org/wiki/Minimax_approximation_algorithmhttp://en.wikipedia.org/wiki/Remez_algorithm

acos()中参数减少中使用的身份在注释中注明,sin()我使用了Cody / Waite风格的参数减少,如下面的书所述:

W上。 J. Cody,W。Waite,基本功能软件手册。普伦蒂斯 - 霍尔,1980年

评论中提到的错误界限是近似值,并未经过严格测试或验证。

/* not quite rint(), i.e. results not properly rounded to nearest-or-even */
double my_rint (double x)
{
  double t = floor (fabs(x) + 0.5);
  return (x < 0.0) ? -t : t;
}

/* minimax approximation to cos on [-pi/4, pi/4] with rel. err. ~= 7.5e-13 */
double cos_core (double x)
{
  double x8, x4, x2;
  x2 = x * x;
  x4 = x2 * x2;
  x8 = x4 * x4;
  /* evaluate polynomial using Estrin's scheme */
  return (-2.7236370439787708e-7 * x2 + 2.4799852696610628e-5) * x8 +
         (-1.3888885054799695e-3 * x2 + 4.1666666636943683e-2) * x4 +
         (-4.9999999999963024e-1 * x2 + 1.0000000000000000e+0);
}

/* minimax approximation to sin on [-pi/4, pi/4] with rel. err. ~= 5.5e-12 */
double sin_core (double x)
{
  double x4, x2, t;
  x2 = x * x;
  x4 = x2 * x2;
  /* evaluate polynomial using a mix of Estrin's and Horner's scheme */
  return ((2.7181216275479732e-6 * x2 - 1.9839312269456257e-4) * x4 + 
          (8.3333293048425631e-3 * x2 - 1.6666666640797048e-1)) * x2 * x + x;
}

/* minimax approximation to arcsin on [0, 0.5625] with rel. err. ~= 1.5e-11 */
double asin_core (double x)
{
  double x8, x4, x2;
  x2 = x * x;
  x4 = x2 * x2;
  x8 = x4 * x4;
  /* evaluate polynomial using a mix of Estrin's and Horner's scheme */
  return (((4.5334220547132049e-2 * x2 - 1.1226216762576600e-2) * x4 +
           (2.6334281471361822e-2 * x2 + 2.0596336163223834e-2)) * x8 +
          (3.0582043602875735e-2 * x2 + 4.4630538556294605e-2) * x4 +
          (7.5000364034134126e-2 * x2 + 1.6666666300567365e-1)) * x2 * x + x; 
}

/* relative error < 7e-12 on [-50000, 50000] */
double my_sin (double x)
{
  double q, t;
  int quadrant;
  /* Cody-Waite style argument reduction */
  q = my_rint (x * 6.3661977236758138e-1);
  quadrant = (int)q;
  t = x - q * 1.5707963267923333e+00;
  t = t - q * 2.5633441515945189e-12;
  if (quadrant & 1) {
    t = cos_core(t);
  } else {
    t = sin_core(t);
  }
  return (quadrant & 2) ? -t : t;
}

/* relative error < 2e-11 on [-1, 1] */
double my_acos (double x)
{
  double xa, t;
  xa = fabs (x);
  /* arcsin(x) = pi/2 - 2 * arcsin (sqrt ((1-x) / 2)) 
   * arccos(x) = pi/2 - arcsin(x)
   * arccos(x) = 2 * arcsin (sqrt ((1-x) / 2))
   */
  if (xa > 0.5625) {
    t = 2.0 * asin_core (sqrt (0.5 * (1.0 - xa)));
  } else {
    t = 1.5707963267948966 - asin_core (xa);
  }
  /* arccos (-x) = pi - arccos(x) */
  return (x < 0.0) ? (3.1415926535897932 - t) : t;
}

答案 1 :(得分:4)

sin( acos(t1) + acos(t2) + ... + acos(tn) )

归结为

的计算
sin( acos(x) ) and cos(acos(x))=x

,因为

sin(a+b) = cos(a)sin(b)+sin(a)cos(b).

首先是

sin( acos(x) )  = sqrt(1-x*x)

sqrt的泰勒级数展开将问题简化为多项式计算。

为了澄清,这里是扩展到n = 2,n = 3:

sin( acos(t1) + acos(t2) ) = sin(acos(t1))cos(acos(t2)) + sin(acos(t2))cos(acos(t1) = sqrt(1-t1*t1) * t2 + sqrt(1-t2*t2) * t1

cos( acos(t2) + acos(t3) ) = cos(acos(t2)) cos(acos(t3)) - sin(acos(t2))sin(acos(t3)) = t2*t3 - sqrt(1-t2*t2)*sqrt(1-t3*t3)

sin( acos(t1) + acos(t2) + acos(t3)) = 
sin(acos(t1))cos(acos(t2) + acos(t3)) + sin(acos(t2)+acos(t3) )cos(acos(t1)=
sqrt(1-t1*t1) * (t2*t3 - sqrt(1-t2*t2)*sqrt(1-t3*t3)) + (sqrt(1-t2*t2) * t3 + sqrt(1-t3*t3) * t2 ) * t1

等等。

可以使用

计算(-1,1)中x的sqrt()
x_0 is some approximation, say, zero

x_(n+1) = 0.5 * (x_n + S/x_n)  where S is the argument.

编辑:我的意思是“巴比伦方法”,详见Wikipedia's article。你需要不超过5-6次迭代才能达到1e-10,其中x为(0,1)。

答案 2 :(得分:3)

正如Jonas Wielicki在评论中提到的那样,你可以做出很多精确的权衡。

最好的办法是尝试使用处理器内在函数来处理函数(如果你的编译器没有这样做)并使用一些数学来减少必要的计算量。

同样非常重要的是将所有内容保持为CPU友好格式,确保缓存未命中等等。

如果您正在计算大量的函数,例如acos,那么移植到GPU可能是一种选择吗?

答案 3 :(得分:2)

您可以尝试创建查找表,并使用它们而不是标准的c ++函数,看看是否有任何性能提升。

答案 4 :(得分:1)

通过将数据中的内存和流式传输到内核,可以获得显着的收益。大多数情况下,这使通过重新创建数学函数可以获得的收益相形见绌。想一想如何改进内核操作员的内存访问。

使用缓冲技术可以改善内存访问。这取决于您的硬件平台。如果您在DSP上运行它,您可以将数据DMA到L2缓存并安排指令,以便乘数单元完全占用。

如果您使用的是通用CPU,那么大多数情况下您可以使用对齐的数据,通过预取来提供缓存行。如果你有嵌套循环,那么最里面的循环应该来回(即向前迭代,然后向后迭代),以便利用缓存行等。

您还可以考虑使用多个核心并行化计算的方法。如果你可以使用GPU,这可以显着提高性能(虽然精度较低)。

答案 5 :(得分:1)

除了其他人所说的,这里有一些速度优化技术:

资料

找出大部分时间在代码中的位置。 只有优化该区域才能获得好处。

展开循环

处理器不喜欢分支或跳转或执行路径的变化。通常,处理器必须重新加载指令管道,该指令管道使用可用于计算的时间。这包括函数调用。

技术是放置更多&#34;套&#34;循环中的操作,减少迭代次数。

将变量声明为寄存器

经常使用的变量应声明为register。尽管SO的许多成员都声称编译器忽略了这个建议,但我发现了其他的建议。最糟糕的情况是,你浪费了一些时间打字。

保持强烈的计算简短&amp;简单

许多处理器在其指令管道中有足够的空间来容纳小for个循环。这减少了重新加载指令管道所花费的时间。

将您的大计算循环分配给许多小计算循环。

对阵列的小部分进行工作&amp;基质

许多处理器都有数据缓存,这是非常接近处理器的超快内存。处理器喜欢从处理器外存储器加载一次数据缓存。更多的负载需要花费在计算上的时间。在网上搜索&#34;面向数据的设计缓存&#34;。

考虑并行处理器术语

更改计算设计,以便它们可以轻松适应多个处理器。许多CPU具有多个可以并行执行指令的内核。某些处理器具有足够的智能来自动将指令委托给其多个核心。

某些编译器可以优化并行处理的代码(查找编译器的编译器选项)。设计并行处理的代码将使编译器更容易进行优化。

分析函数的汇编列表

打印出函数的汇编语言列表。 更改函数的设计以匹配汇编语言的设计或帮助编译器生成更优化的汇编语言。

如果您确实需要更高的效率,请优化汇编语言并将其作为内联汇编代码或单独模块输入。我通常更喜欢后者。

实施例

在您的情况下,采用泰勒展开的前10个项,分别计算并放入单个变量中:

double term1, term2, term3, term4;
double n, n1, n2, n3, n4;
n = 1.0;
for (i = 0; i < 100; ++i)
{
  n1 = n + 2;
  n2 = n + 4;
  n3 = n + 6;
  n4 = n + 8;

  term1 = 4.0/n;
  term2 = 4.0/n1;
  term3 = 4.0/n2;
  term4 = 4.0/n3;

然后总结你的所有条款:

  result = term1 - term2 + term3 - term4;
  // Or try sorting by operation, if possible:
  // result = term1 + term3;
  // result -= term2 + term4;

  n = n4 + 2;
  }

答案 6 :(得分:0)

让我们先考虑两个术语:

cos(a+b) = cos(a)*cos(b) - sin(a)*sin(b)

cos(a+b) = cos(a)*cos(b) - sqrt(1-cos(a)*cos(a))*sqrt(1-cos(b)*cos(b))

将cos带入RHS

a+b = acos( cos(a)*cos(b) - sqrt(1-cos(a)*cos(a))*sqrt(1-cos(b)*cos(b)) ) ... 1

这里cos(a)= t_1和cos(b)= t_2 a = acos(t_1)和b = acos(t_2)

通过在等式(1)中代入,我们得到

acos(t_1) + acos(t_2) = acos(t_1*t_2 - sqrt(1 - t_1*t_1) * sqrt(1 - t_2*t_2))

在这里你可以看到你将两个acos合二为一。因此,您可以递归地配对所有acos并形成二叉树。最后,您将留下sin(acos(x))形式的表达式,其等于sqrt(1 - x*x)

这将提高时间复杂度。

但是,我不确定计算sqrt()的复杂性。