当x = 0

时间:2015-12-09 21:55:58

标签: java performance math benchmarking pow

背景

注意到我正在处理的java程序的执行速度比预期慢,我决定修补我认为可能导致问题的代码区域 - 调用Math.pow(x,2)来自for循环。与another questions on this site相反,我创建的一个简单基准(结尾处的代码)发现用x * x替换Math.pow(x,2)实际上将循环加速了近70倍:

x*x: 5.139383ms
Math.pow(x, 2): 334.541166ms

请注意,我知道基准测试并不完美,而且价值当然应该用一点点盐 - 基准的目的是获得一个大概的数字。

问题

虽然基准测试给出了有趣的结果,但它没有准确地模拟我的数据,因为我的数据主要由0组成。因此,更准确的测试是运行基准测试,而不将for循环标记为可选。根据Math.pow()的javadoc

  

如果第一个参数为正零,第二个参数为   大于零,或第一个参数是正无穷大和   第二个参数小于零,则结果为正零。

所以预计这个基准测试运行得更快吧!?但实际上,这再次慢得多:

x*x: 4.3490535ms
Math.pow(x, 2): 3082.1720006ms

当然,有人可能会认为math.pow()代码比简单的x * x代码运行得慢一点,因为它需要适用于一般情况,但速度要慢700倍?到底是怎么回事!?为什么0情况比Math.random()情况慢得多?

更新: 根据@Stephen C的建议更新了代码和时间。然而,这几乎没有什么区别。

用于基准测试的代码

请注意,重新排序这两个测试可以忽略不计。

public class Test {
    public Test(){
        int iterations = 100;
        double[] exampleData = new double[5000000];
        double[] test1Results = new double[iterations];
        double[] test2Results = new double[iterations];

        //Optional
        for (int i = 0; i < exampleData.length; i++) {
            exampleData[i] = Math.random();
        }

        for (int i = 0; i < iterations; i++) {
            test1Results[i] = test1(exampleData);
            test2Results[i] = test2(exampleData);
        }
        System.out.println("x*x: " + calculateAverage(test1Results) / 1000000 + "ms");
        System.out.println("Math.pow(x, 2): " + calculateAverage(test2Results) / 1000000 + "ms");
    }

    private long test1(double[] exampleData){
        double total = 0;
        long startTime;
        long endTime;
        startTime = System.nanoTime();
        for (int j = 0; j < exampleData.length; j++) {
            total += exampleData[j] * exampleData[j];
        }
        endTime = System.nanoTime();
        System.out.println(total);
        return endTime - startTime;
    }

    private long test2(double[] exampleData){
        double total = 0;
        long startTime;
        long endTime;
        startTime = System.nanoTime();
        for (int j = 0; j < exampleData.length; j++) {
            total += Math.pow(exampleData[j], 2);
        }
        endTime = System.nanoTime();
        System.out.println(total);
        return endTime - startTime;
    }

    private double calculateAverage(double[] array){
        double total = 0;
        for (int i = 0; i < array.length; i++) {
            total += array[i];
        }
        return total/array.length;
    }

    public static void main(String[] args){
        new Test();
    }
}

4 个答案:

答案 0 :(得分:7)

虽然这是一个糟糕的基准,但它幸运地揭示了一个有趣的效果。

数字表明您显然在“客户端”VM下运行基准测试。它没有非常强大的JIT编译器(称为C1编译器),缺乏许多优化。难怪它没有人们期望的那么好。

  • 即使没有副作用,客户端VM也不够智能,无法消除Math.pow来电。
  • 此外,它既没有Y=2也没有X=0的专门快速路径。至少,它直到Java 9才有。最近在JDK-8063086修复了这个问题,然后在JDK-8132207进一步优化。

但有趣的是,Math.pow对于使用C1编译器的X=0确实较慢!

但为什么呢?由于实施细节。

x86架构不提供计算X ^ Y的硬件指令。但还有其他有用的说明:

  • FYL2X计算Y * log 2 X
  • F2XM1计算2 ^ X - 1

因此,X ^ Y = 2 ^(Y * log 2 X)。由于log 2 X仅定义为X> 1。 0,FYL2XX=0的例外结束并返回-Inf。因此,X=0在慢速异常路径中处理,而不是在专门的快速路径中处理。

那该怎么办?

首先,停止使用客户端VM,特别是如果您关心性能。切换到64位版本的最新JDK 8,您将获得最佳的C2优化JIT编译器。当然,它很好地处理Math.pow(x, 2)等。然后使用correct benchmark之类的适当工具编写JMH

答案 1 :(得分:4)

可能与JDK 7中的这种回归有关:http://bugs.java.com/bugdatabase/view_bug.do?bug_id=8029302

来自错误报告:

  

有一个Math.pow性能回归,其中2个输入的功率运行   比其他值慢。

我已经替换了所有对Math.pow()的调用,因为:

public static double pow(final double a, final double b) {
    if (b == 2.0) {
        return (a * a);
    } else {
        return Math.pow(a, b);
    }
}

根据错误报告,它已在JDK 8中修复,符合上述@BartKiers评论。

答案 2 :(得分:3)

虽然@whiskeyspider发现的错误报告是相关的,但我不认为这是完整的解释。根据错误报告,回归大约减少4倍。但在这里,我们看到大约1000倍的放缓。差异太大,不容忽视。

我认为我们在这里看到的部分问题是基准本身。看看这个:

        for (int j = 0; j < exampleData.length; j++) {
            double output = exampleData[j] * exampleData[j];
        }

正文中的语句分配给未使用的局部变量。它可以通过JIT编译器进行优化。 (事实上​​,整个循环可以被优化掉......虽然凭经验看似不会发生在这里。)

相比之下:

        for (int j = 0; j < exampleData.length; j++) {
            double output = Math.pow(exampleData[j], 2);
        }

除非JIT编译器知道pow是无副作用的,否则无法进行优化。由于pow的实现是在本机代码中,因此必须以制作方法的方式传授这些知识&#34;内在的&#34; ......引擎盖下。从错误报告分析,&#34; intinsification&#34;不同Java版本/发行版之间是回归的根本原因。我怀疑OP的基准测试中的缺陷是放大效果。

修复是为了确保使用output值,以便JIT编译器无法对其进行优化; e.g。

        double blackhole = 0;  // declared at start ...
        ...
        for (int j = 0; j < exampleData.length; j++) {
            blackhole += exampleData[j] * exampleData[j];
        }
        ...
        for (int j = 0; j < exampleData.length; j++) {
            blackhole += Math.pow(exampleData[j], 2);
        }

参考:https://stackoverflow.com/a/513259/139985 ...尤其是规则#6。

答案 3 :(得分:0)

使用Math类中的任何方法需要更长的时间,然后只使用一个简单的运算符(如果可能)。这是因为程序必须将Math。方法()的输入传递给Math类,然后Math类将执行操作,然后Math类将返回从Math计算的值方法()。所有这些都需要比使用*,/,+或 - 等基本运算符更多的处理能力。