20

由于 java.lang.Math 中的三角函数非常慢:是否有一个库可以进行快速且良好的近似?在不损失太多精度的情况下,似乎可以将计算速度提高几倍。(在我的机器上,乘法需要 1.5ns,java.lang.Math.sin 需要 46ns 到 116ns)。不幸的是,还没有一种方法可以使用硬件功能。

更新:函数应该足够准确,例如 GPS 计算。这意味着您需要至少 7 个十进制数字的准确性,这排除了简单的查找表。它应该比基本 x86 系统上的 java.lang.Math.sin 快得多。否则就没有意义了。

对于超过 pi/4 的值,除了硬件功能之外,Java还会进行一些昂贵的计算。这样做是有充分理由的,但有时您更关心速度而不是最后一点的准确性。

4

12 回答 12

15

哈特的计算机近似以不同的精度将一组函数的切比雪夫经济近似公式制成表格。

编辑:把我的书从书架上拿下来,结果发现它是一本不同的书,听起来很相似。这是一个使用其表的 sin 函数。(在 C 中测试,因为这对我来说更方便。)我不知道这是否会比 Java 内置更快,但至少可以保证它不那么准确。:) 您可能需要先对参数进行范围缩小;请参阅约翰库克的建议。这本书还有arcsin和arctan。

#include <math.h>
#include <stdio.h>

// Return an approx to sin(pi/2 * x) where -1 <= x <= 1.
// In that range it has a max absolute error of 5e-9
// according to Hastings, Approximations For Digital Computers.
static double xsin (double x) {
  double x2 = x * x;
  return ((((.00015148419 * x2
             - .00467376557) * x2
            + .07968967928) * x2
           - .64596371106) * x2
          + 1.57079631847) * x;
}

int main () {
  double pi = 4 * atan (1);
  printf ("%.10f\n", xsin (0.77));
  printf ("%.10f\n", sin (0.77 * (pi/2)));
  return 0;
}
于 2009-02-07T21:14:49.317 回答
13

是一组用于快速逼近三角函数的低级技巧。我发现有一些 C 语言示例代码很难理解,但这些技术在 Java 中同样容易实现。

这是我在 Java 中对 invsqrt 和 atan2 的等效实现。

我可以为其他三角函数做类似的事情,但我发现没有必要,因为分析表明只有 sqrt 和 atan/atan2 是主要瓶颈。

public class FastTrig
{
  /** Fast approximation of 1.0 / sqrt(x).
   * See <a href="http://www.beyond3d.com/content/articles/8/">http://www.beyond3d.com/content/articles/8/</a>
   * @param x Positive value to estimate inverse of square root of
   * @return Approximately 1.0 / sqrt(x)
   **/
  public static double
  invSqrt(double x)
  {
    double xhalf = 0.5 * x; 
    long i = Double.doubleToRawLongBits(x);
    i = 0x5FE6EB50C7B537AAL - (i>>1); 
    x = Double.longBitsToDouble(i);
    x = x * (1.5 - xhalf*x*x); 
    return x; 
  }

  /** Approximation of arctangent.
   *  Slightly faster and substantially less accurate than
   *  {@link Math#atan2(double, double)}.
   **/
  public static double fast_atan2(double y, double x)
  {
    double d2 = x*x + y*y;

    // Bail out if d2 is NaN, zero or subnormal
    if (Double.isNaN(d2) ||
        (Double.doubleToRawLongBits(d2) < 0x10000000000000L))
    {
      return Double.NaN;
    }

    // Normalise such that 0.0 <= y <= x
    boolean negY = y < 0.0;
    if (negY) {y = -y;}
    boolean negX = x < 0.0;
    if (negX) {x = -x;}
    boolean steep = y > x;
    if (steep)
    {
      double t = x;
      x = y;
      y = t;
    }

    // Scale to unit circle (0.0 <= y <= x <= 1.0)
    double rinv = invSqrt(d2); // rinv ≅ 1.0 / hypot(x, y)
    x *= rinv; // x ≅ cos θ
    y *= rinv; // y ≅ sin θ, hence θ ≅ asin y

    // Hack: we want: ind = floor(y * 256)
    // We deliberately force truncation by adding floating-point numbers whose
    // exponents differ greatly.  The FPU will right-shift y to match exponents,
    // dropping all but the first 9 significant bits, which become the 9 LSBs
    // of the resulting mantissa.
    // Inspired by a similar piece of C code at
    // http://www.shellandslate.com/computermath101.html
    double yp = FRAC_BIAS + y;
    int ind = (int) Double.doubleToRawLongBits(yp);

    // Find φ (a first approximation of θ) from the LUT
    double φ = ASIN_TAB[ind];
    double cφ = COS_TAB[ind]; // cos(φ)

    // sin(φ) == ind / 256.0
    // Note that sφ is truncated, hence not identical to y.
    double sφ = yp - FRAC_BIAS;
    double sd = y * cφ - x * sφ; // sin(θ-φ) ≡ sinθ cosφ - cosθ sinφ

    // asin(sd) ≅ sd + ⅙sd³ (from first 2 terms of Maclaurin series)
    double d = (6.0 + sd * sd) * sd * ONE_SIXTH;
    double θ = φ + d;

    // Translate back to correct octant
    if (steep) { θ = Math.PI * 0.5 - θ; }
    if (negX) { θ = Math.PI - θ; }
    if (negY) { θ = -θ; }

    return θ;
  }

  private static final double ONE_SIXTH = 1.0 / 6.0;
  private static final int FRAC_EXP = 8; // LUT precision == 2 ** -8 == 1/256
  private static final int LUT_SIZE = (1 << FRAC_EXP) + 1;
  private static final double FRAC_BIAS =
    Double.longBitsToDouble((0x433L - FRAC_EXP) << 52);
  private static final double[] ASIN_TAB = new double[LUT_SIZE];
  private static final double[] COS_TAB = new double[LUT_SIZE];

  static
  {
    /* Populate trig tables */
    for (int ind = 0; ind < LUT_SIZE; ++ ind)
    {
      double v = ind / (double) (1 << FRAC_EXP);
      double asinv = Math.asin(v);
      COS_TAB[ind] = Math.cos(asinv);
      ASIN_TAB[ind] = asinv;
    }
  }
}
于 2009-02-07T10:52:55.267 回答
5

这可能会成功:http: //sourceforge.net/projects/jafama/

于 2010-07-02T23:24:52.943 回答
4

在 x86 上,java.lang.Math sin 和 cos 函数不直接调用硬件函数,因为 Intel 并不总是能很好地实现它们。在错误 #4857011 中有一个很好的解释。

http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4857011

您可能需要认真考虑一个不精确的结果。有趣的是,我经常花时间在其他代码中找到它。

“但是评论说Sin……”

于 2009-02-07T12:56:33.330 回答
4

我很惊讶内置的 Java 函数会这么慢。当然,JVM 调用的是 CPU 上的本机三角函数,而不是在 Java 中实现算法。你确定你的瓶颈是调用触发函数而不是一些周围的代码吗?也许一些内存分配?

你能用 C++ 重写你的代码中进行数学运算的部分吗?仅仅调用 C++ 代码来计算三角函数可能不会加快速度,但是将一些上下文(如外循环)移动到 C++ 可能会加快速度。

如果您必须滚动自己的三角函数,请不要单独使用泰勒级数。除非您的论点非常小,否则 CORDIC 算法要快得多。您可以使用 CORDIC 开始,然后使用简短的泰勒级数完善结果。请参阅有关如何实现三角函数的 StackOverflow 问题。

于 2009-02-07T13:36:55.917 回答
3

如果您只需要一些近似值,您可以将您的 sin 和 cos 预先存储在一个数组中。例如,如果要存储 0° 到 360° 的值:

double sin[]=new double[360];
for(int i=0;i< sin.length;++i) sin[i]=Math.sin(i/180.0*Math.PI):

然后,您可以使用度数/整数而不是弧度/双精度来使用此数组。

于 2009-02-07T10:18:47.893 回答
1

我还没有听说过任何库,可能是因为很少能看到触发繁重的 Java 应用程序。使用 JNI(相同的精度,更好的性能)、数值方法(可变精度/性能)或简单的近似表也很容易实现。

与任何优化一样,最好在重新发明轮子之前测试这些功能实际上是一个瓶颈。

于 2009-02-07T10:01:10.283 回答
1

三角函数是查找表的经典示例。见优秀

如果您正在搜索 J2ME 库,您可以尝试:

  • 定点整数数学MathFP
于 2009-02-07T11:23:15.873 回答
0

java.lang.Math 函数调用硬件函数。您应该可以做出简单的认可,但它们不会那么准确。

在我的实验室上,sin 和 cos 大约需要 144 ns。

于 2009-02-07T09:55:08.627 回答
0

在 sin/cos 测试中,我对 0 到 100 万的整数进行了测试。我假设 144 ns 对你来说不够快。

您对所需的速度有特定要求吗?

您能否以令人满意的每次操作时间来确定您的要求?

于 2009-02-08T21:48:08.613 回答
-1

如果您想使用现有的东西,请查看Apache Commons Math 包。

如果性能真的很重要,那么您可以使用标准数学方法自行实现这些功能 - 泰勒/麦克劳林级数,特别是。

例如,这里有几个可能有用的泰勒级数展开式(取自维基百科):

替代文字

替代文字

替代文字

于 2009-02-07T10:02:04.727 回答
-1

如果这些例程太慢,您能否详细说明您需要做什么。您可能能够以某种方式提前进行一些坐标转换。

于 2009-02-07T20:54:40.137 回答