【问题标题】:Fast sine and cosine function in javajava中的快速正弦和余弦函数
【发布时间】:2013-06-05 02:24:50
【问题描述】:

我知道 Math.sin()Math.cos() 函数,但我想知道是否有一种方法可以创建(或使用已经存在的)更快的函数,因为我不关心精确度.我正在寻找执行基本的 sin 或 cos 计算,并让它尽可能快地执行。简单地迭代 sigma 几次会比 Math.sin() 快​​吗?

【问题讨论】:

  • 您可以预先计算所需的值。那将是最快的。
  • 通常,JDK 库函数包含给定问题陈述的最佳实践实现。我建议你使用 Math 类,除非你真的需要构建实时应用程序的性能。
  • 除了已经给出的建议之外,您还可以使用small-angle approximation,并实现一些基本逻辑将较大的角度转换为较小的补码,以便可以在大多数情况下使用近似值(如果不是全部)案例——假设您的精度要求如您所建议的那样宽松。

标签: java optimization trigonometry


【解决方案1】:

由于您不太关心准确性,因此将其存储在预先计算或仅计算一次的表中,当我想避免调用 Math 时,我会这样做,因为这样做会很昂贵。

大概

public class CosSineTable {
double[] cos = new double[361];
double[] sin = new double[361];
private static CosSineTable table = new CosSineTable();

private CosSineTable() {
    for (int i = 0; i <= 360; i++) {
        cos[i] = Math.cos(Math.toRadians(i));
        sin[i] = Math.sin(Math.toRadians(i));
    }
}

public double getSine(int angle) {
    int angleCircle = angle % 360;
    return sin[angleCircle];
}

public double getCos(int angle) {
    int angleCircle = angle % 360;
    return cos[angleCircle];
}

public static CosSineTable getTable() {
    return table;
}
}

我把循环和方法的优化留给你。

【讨论】:

  • 我喜欢这个计划,但它不适用于负角。 % 不是数学意义上的模数,而是余数。不过很容易修复。
  • 是的,只需要返回 -sin[angle] 为负数,cos 是一样的。
【解决方案2】:

预先计算好的表格是最好的选择。这是一个实现:

static final int precision = 100; // gradations per degree, adjust to suit

static final int modulus = 360*precision;
static final float[] sin = new float[modulus]; // lookup table
static { 
    // a static initializer fills the table
    // in this implementation, units are in degrees
    for (int i = 0; i<sin.length; i++) {
        sin[i]=(float)Math.sin((i*Math.PI)/(precision*180));
    }
}
// Private function for table lookup
private static float sinLookup(int a) {
    return a>=0 ? sin[a%(modulus)] : -sin[-a%(modulus)];
}

// These are your working functions:
public static float sin(float a) {
    return sinLookup((int)(a * precision + 0.5f));
}
public static float cos(float a) {
    return sinLookup((int)((a+90f) * precision + 0.5f));
}

在我的笔记本电脑上,这些速度比 Math.sin 快大约 6 倍。

我只使用了一张表——将余弦转换为正弦的成本并不是很明显。

我使用浮点数,假设您可能会在计算中使用浮点数,因为您更喜欢性能而不是精度。在这里没有太大区别,因为瓶颈实际上只是数组查找。

这是我的基准测试:

public static void main(String[] args) {
    int reps = 1<<23;
    int sets = 4;

    Q.pl("  Trial  sinTab  cosTab  sinLib");
    for(int i = 0; i<sets; i++) {
        Q.pf("%7d\t%7.2f\t%7.2f\t%7.2f\n", i, testSinTab(reps), testCosTab(reps), testSinLib(reps));
    }
}

private static float[] sample(int n) {
    Random rand = new Random();
    float[] values = new float[n];
    for (int i=0; i<n; i++) {
        values[i] = 400*(rand.nextFloat()*2-1);
    }
    return values;
}
private static float testSinTab(int n) {
    float[] sample = sample(n);
    long time = -System.nanoTime();
    for (int i=0; i<n; i++) {
        sample[i] = sin(sample[i]);
    }
    time += System.nanoTime();
    return (time/1e6f);
}
private static float testCosTab(int n) {
    float[] sample = sample(n);
    long time = -System.nanoTime();
    for (int i=0; i<n; i++) {
        sample[i] = cos(sample[i]);
    }
    time += System.nanoTime();
    return time/1e6f;
}
private static float testSinLib(int n) {
    float[] sample = sample(n);
    long time = -System.nanoTime();
    for (int i=0; i<n; i++) {
        sample[i] = (float) Math.sin(sample[i]);
    }
    time += System.nanoTime();
    return time/1e6f;
}

输出:

  Trial  sinTab  cosTab  sinLib
      0  102.51  111.19  596.57
      1   93.72   92.20  578.22
      2  100.06  107.20  600.68
      3  103.65  102.67  629.86

【讨论】:

  • LUT 基准测试会很快,因为它们可以使用 CPU 缓存。 FWIW,LUT 会破坏 CPU 缓存以供实际应用程序使用。
【解决方案3】:

你可以试试 http://sourceforge.net/projects/jafama/

它使用查找表,因此它实际上可能会更慢 比数学,特别是如果表经常从 CPU 缓存中逐出, 但是对于数千个连续调用,它可能会更快。

在类加载期间它似乎也更慢(也许 JIT 还没有启动), 所以你可能想在那个特定的用例中避​​免它。

【讨论】:

  • OP 要求更快的解决方案,而不是缓慢的解决方案。
  • 如果在一个八分圆(272 字节)内使用包含 17 个正弦/余弦对的表格,则四阶多项式近似足以达到大约十亿分之一的精度。根据 CPU 缓存的大小和使用模式,将一个象限扩展到 33 对可能会有所帮助[允许表在连续的阵列插槽中保存正弦/余弦对而不需要有条件地交换它们],甚至使用switch 语句来选择 17 对公式中的一个,或 33 个公式或其中的一对(取决于一个人想要的是 sin、cos 还是两者兼而有之)。
  • 折衷方案是使用 33 组 5 个 double 值,这些值将在选择一个象限后用于带有余数的多项式逼近,或 129 组 5 个用于整个圆(避免条件逻辑)。较大的表大约需要 5K,这对于具有小缓存的机器来说可能是个问题,但在单独计算正弦时会用 3 个连续项获取替换 4 个倍数,或者在计算两者时用 8 个连续项获取替换 8 个乘法。跨度>
【解决方案4】:

我知道这个问题很老,但我认为它是最快的 java 实现,可精确到 65536 个元素。

public class MathHelper {

    private static double[] a = new double[65536];

    public static final double sin(float f) {
        return a[(int) (f * 10430.378F) & '\uffff'];
    }

    public static final double cos(float f) {
        return a[(int) (f * 10430.378F + 16384.0F) & '\uffff'];
    }

    static {
        for (int i = 0; i < 65536; ++i) {
            a[i] = Math.sin((double) i * 3.141592653589793D * 2.0D / 65536.0D);
        }
    }
}

来源:https://github.com/Bukkit/mc-dev/blob/master/net/minecraft/server/MathHelper.java

【讨论】:

  • 不,这并不可悲。它实际上很慢,以至于在游戏被修改时它通常会被替换(参见:BetterFPS/Optifine,它们都覆盖了它)。那是 Notch 的实现,并不是那么好:P
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-12-23
  • 1970-01-01
  • 2016-06-16
相关资源
最近更新 更多