【问题标题】:Why is Math.pow(int,int) slower than my naive implementation?为什么 Math.pow(int,int) 比我幼稚的实现慢?
【发布时间】:2016-04-10 20:11:30
【问题描述】:

昨天看到一个问题问Math.pow(int,int)为什么这么慢,但是这个问题措辞不好,也没有研究,所以很快就关闭了。

我自己做了一个小测试,发现在处理整数参数时,Math.pow 方法与我自己的幼稚实现(甚至不是特别有效的实现)相比确实运行得非常慢。下面是我运行来测试的代码:

class PowerTest {

    public static double myPow(int base, int exponent) {
        if(base == 0) return 0;
        if(exponent == 0) return 1;
        int absExponent = (exponent < 0)? exponent * -1 : exponent;
        double result = base;
        for(int i = 1; i < absExponent; i++) {
            result *= base;
        }
        if(exponent < 1) result = 1 / result;
        return result;
    }

    public static void main(String args[]) {
        long startTime, endTime;

        startTime = System.nanoTime();
        for(int i = 0; i < 5000000; i++) {
            Math.pow(2,2);
        }
        endTime = System.nanoTime();
        System.out.printf("Math.pow took %d milliseconds.\n", (endTime - startTime) / 1000000);

        startTime = System.nanoTime();
        for(int i = 0; i < 5000000; i++) {
            myPow(2,2);
        }
        endTime = System.nanoTime();
        System.out.printf("myPow took %d milliseconds.\n", (endTime - startTime) / 1000000);
    }

}

在我的计算机(intel x86_64 cpu 上的 Linux)上,输出几乎总是报告 Math.pow 花了 10 毫秒,而 myPow 花了 2 毫秒。这偶尔会在这里或那里波动一毫秒,但 Math.pow 的平均运行速度约为 5 倍。

我做了一些研究,根据grepcode 的说法,Math.pow 只提供了一个类型签名为(double, double) 的方法,并将其推迟到作为本机方法调用的StrictMath.pow 方法。

Math 库仅提供处理双打的pow 函数这一事实似乎表明可能回答了这个问题。显然,与只处理整数的算法相比,必须处理 double 类型的基数或指数的可能性的幂算法将花费更长的时间来执行。然而,最后,它归结为依赖于架构的本机代码(它几乎总是比 JVM 字节码运行得更快,在我的例子中可能是 C 或汇编)。 似乎在这个级别上会进行优化以检查数据类型并尽可能运行更简单的算法。

鉴于此信息,为什么在给定整数参数时,本机 Math.pow 方法始终比我未优化和幼稚的 myPow 方法运行得慢得多?

【问题讨论】:

  • 我对此发表评论:int absExponent = (exponent
  • 由于您的实现在指数上使用循环,因此计算平方是一个非常有偏见的基准。为什么不在 2⁸ 上同时尝试这两种方法:) (当然,您可以使用不那么简单的算法做得更好,但即便如此,2¹⁵ 可能会显示不同的结果。)

标签: java algorithm performance


【解决方案1】:

没有pow(int,int) 函数。您正在将苹果与橙子进行比较,并简化了浮点数可以忽略的假设。

【讨论】:

  • 但它会被传递到本机代码。肯定会在本机级别进行优化吗?
  • @WoodrowBarlow 传递给本机代码的值为double。您是否建议如果本机代码未检测到小数部分,则应使用备用路径?将参数转换为整数类型并使用整数运算符进行操作的路径?
  • 好吧,当然。有什么理由认为这是个坏主意吗?如果这是一个坏主意,为什么不用整数类型的变体重载 pow 方法呢?对我来说,这似乎是一个高价值的优化,但我知道我可能缺少一些东西。
  • 每次调用pow() 时检查和分支都会花费额外的时间,并且整数参数的情况是非典型的。提高到更大的功率时测试性能,您会发现测试并不像“有小数部分吗?”那么简单。因此,您会减慢典型用例的速度,但收效甚微。重载pow() 函数可以避免这个问题,但是当你自己很容易实现的时候,用这样一个专门的函数来弄乱API可能不值得。
【解决方案2】:

Math.pow 很慢,因为它处理的是一般意义上的方程,使用分数幂将其提升到给定的幂。当计算需要更多时间时,它必须进行查找。

简单地将数字相乘通常更快,因为 Java 中的本地调用效率更高。

编辑:值得注意的是,数学函数使用双精度,这也可能比使用整数花费更长的时间。

【讨论】:

    【解决方案3】:

    正如其他人所说,你不能忽略double 的使用,因为浮点运算几乎肯定会更慢。但是,这不是唯一的原因 - 如果您更改实现以使用它们,它仍然更快。

    这是因为两件事:第一是2^2(指数,而不是异或)是一个非常快速的计算,所以你的算法可以很好地使用 - 尝试使用来自Random#nextInt的两个值(或nextDouble),您会发现Math#pow 实际上要快得多。

    另一个原因是调用本机方法有开销,这在这里实际上是有意义的,因为2^2 计算速度非常快,而您调用Math#pow 这么多次。有关更多信息,请参阅What makes JNI calls slow?。

    【讨论】:

    • 啊哈!我用对nextInt(1000) 的调用替换了我的所有参数,现在Math.pow 方法的运行速度比我的实现快得多。所以事实证明这是一个糟糕的测试用例。
    【解决方案4】:

    Math.pow(x, y)可能实现为exp(y log x)。这允许使用分数指数并且速度非常快。

    但如果您只需要小的正整数参数,您将能够通过编写自己的版本来超越这种性能。

    可以说 Java 可以为您进行这项检查,但对于大整数来说,内置版本会更快。它还必须定义一个适当的整数返回类型,并且溢出的风险是显而易见的。定义分支区域周围的行为会很棘手。

    顺便说一句,您的整数类型版本可能会更快。对平方求幂做一些研究。

    【讨论】:

      猜你喜欢
      • 2021-09-18
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-08-25
      • 2023-01-20
      • 1970-01-01
      • 1970-01-01
      • 2017-04-25
      相关资源
      最近更新 更多