Быстрые трансцендентные/тригонометрические функции для Java

Так как тригонометрические функции в java.lang.Math довольно медленные: есть ли библиотека, которая быстро и хорошо приближается? Кажется возможным сделать расчет в несколько раз быстрее, не теряя при этом значительной точности. (На моей машине умножение занимает 1.5ns, а java.lang.Math.sin 46ns - 116ns). К сожалению, пока еще нет возможности использовать аппаратные функции.

UPDATE: функции должны быть достаточно точными, скажем, для расчетов GPS. Это означает, что вам понадобится не менее 7 десятичных цифр, что исключает простые таблицы поиска. И это должно быть намного быстрее, чем java.lang.Math.sin на вашей базовой системе x86. В противном случае в этом не было бы никакого смысла.

Для значений над pi/4 Java помимо содержит несколько дорогостоящих вычислений. Он делает это по уважительной причине, но иногда вам больше нужна скорость, чем для последней точности бит.

Ответы

Ответ 1

Компьютерные аппроксимации от Hart. Таблицы Чебышев-сэкономили приближенные формулы для кучи функций при разных точках.

Изменить: Получив мою копию с полки, она оказалась другая книга, которая просто звучит очень похожий. Здесь функция 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;
}

Ответ 2

Здесь - коллекция низкоуровневых трюков для быстрого приближения функций триггера. В C есть пример кода, который мне трудно понять, но методы так же легко реализованы в Java.

Здесь моя эквивалентная реализация invsqrt и atan2 в Java.

Я мог бы сделать что-то подобное для других триггерных функций, но я не счел нужным, поскольку профилирование показало, что только узлы и 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;
    }
  }
}

Ответ 4

На x86 функции java.lang.Math sin и cos непосредственно не вызывают аппаратные функции, потому что Intel не всегда делала такую ​​хорошую работу, которая имплементировала их. В ошибке # 4857011 есть хорошее объяснение.

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

Возможно, вы захотите серьезно подумать о неточном результате. Забавно, как часто я трачу время на то, чтобы найти это в другом коде.

"Но в комментарии говорится о грехе..."

Ответ 5

Я удивлен, что встроенные функции Java будут настолько медленными. Разумеется, JVM вызывает собственные триггерные функции на вашем процессоре, не реализуя алгоритмы в Java. Вы уверены, что ваше узкое место - это вызов функций триггера, а не какой-то окружающий код? Может быть, некоторые выделения памяти?

Не могли бы вы переписать на С++ часть вашего кода, которая выполняет математику? Просто вызов кода С++ для вычисления функций триггера, вероятно, не ускорит работу, но перемещение некоторого контекста, как и внешнего цикла, на С++ может ускорить работу.

Если вы должны перевернуть свои собственные триггерные функции, не используйте только серии Тейлора. Алгоритмы CORDIC намного быстрее, если ваш аргумент не очень мал. Вы можете использовать CORDIC для начала работы, а затем отполировать результат с помощью короткой серии Taylor. См. Этот вопрос StackOverflow в о том, как реализовать триггерные функции.

Ответ 6

Вы можете предварительно сохранить свой грех и 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):

вы затем используете этот массив, используя градусы/целые числа вместо радиан/double.

Ответ 7

Я не слышал о каких-либо libs, возможно, потому, что он достаточно редок, чтобы видеть триггерные Java-приложения. Это также достаточно просто, чтобы свернуть JNI (с той же точностью, лучшей производительностью), численные методы (переменная точность/производительность) или простая таблица аппроксимации.

Как и при любой оптимизации, лучше всего проверить, что эти функции на самом деле являются узким местом, прежде чем пытаться изобрести колесо.

Ответ 8

Тригонометрические функции являются классическим примером для справочной таблицы. См. Отличный

Если вы ищете библиотеку для J2ME, вы можете попробовать:

  • Математическая библиотека с фиксированной точкой. MathFP

Ответ 9

Функции java.lang.Math вызывают функции аппаратного обеспечения. Должны быть простые апробации, которые вы можете сделать, но они не будут такими точными.

На моей labtop, sin и cos занимает около 144 нс.

Ответ 10

В тесте sin/cos я выполнял для целых чисел от нуля до одного миллиона. Я предполагаю, что 144 нс недостаточно для вас.

У вас есть определенное требование для скорости, в которой вы нуждаетесь?

Можете ли вы квалифицировать свои требования с точки зрения времени на операцию, которая является удовлетворительной?

Ответ 11

Откажитесь от Apache Commons Math package, если вы хотите использовать существующие материалы.

Если производительность действительно важна, вы можете сами реализовать эти функции, используя стандартные математические методы - серии Тейлор/Маклаурин.

Например, вот несколько разложений серии Тейлора, которые могут быть полезны (взяты из wikipedia):

alt text

alt text

alt text

Ответ 12

Не могли бы вы рассказать о том, что вам нужно сделать, если эти процедуры слишком медленные. Возможно, вы так или иначе сможете сделать некоторые преобразования координат.