Java效率

时间:2012-01-19 19:03:17

标签: java performance

我正在使用一些代码来计算计算某些Java代码所需的时间,以了解某些Java功能的效率或效率低下。这样做我现在陷入了一些非常奇怪的效果,我无法解释自己。也许有人可以帮助我理解它。

public class PerformanceCheck {

 public static void main(String[] args) {
    List<PerformanceCheck> removeList = new LinkedList<PerformanceCheck>();

    int maxTimes = 1000000000;

    for (int i=0;i<10;i++) {
        long time = System.currentTimeMillis();

        for (int times=0;times<maxTimes;times++) {
            // PERFORMANCE CHECK BLOCK START

            if (removeList.size() > 0) {
                testFunc(3);
            }

            // PERFORMANCE CHECK BLOCK END
        }

        long timeNow = System.currentTimeMillis();
        System.out.println("time: " + (timeNow - time));
    }
 }

 private static boolean testFunc(int test) {
    return 5 > test;
 }

}

启动这会导致相对较长的计算时间(请记住removeList为空,因此甚至不调用testFunc):

time: 2328
time: 2223
...

替换removeList.size()&gt;组合中的任何内容。 0和testFunc(3)以及其他任何东西都有更好的结果。例如:

...
if (removeList.size() == 0) {
    testFunc(3);
}
...

结果(每次调用testFunc):

time: 8
time: 7
time: 0
time: 0

即使调用两个函数彼此独立也会导致计算时间缩短:

...
if (removeList.size() == 0);
    testFunc(3);
...

结果:

time: 6
time: 5
time: 0
time: 0
...

在我的初始示例中,只有这种特殊组合需要很长时间。这让我感到恼火,我真的很想理解它。这有什么特别之处?

感谢。

增加:

在第一个例子中更改testFunc()

if (removeList.size() > 0) {
                testFunc(times);
}

到其他地方,比如

private static int testFunc2(int test) {
    return 5*test;
}

会导致再次快速。

6 个答案:

答案 0 :(得分:3)

这真的很令人惊讶。除了条件之外,生成的字节码是相同的,ifleifne

如果使用-Xint关闭JIT,结果会更加明智。第二个版本慢了2倍。所以它与JIT优化有关。

我认为它可以在第二种情况下优化检查,但不是第一种情况(无论出于何种原因)。即使这意味着它完成了函数的工作,但缺少条件会使事情变得更快。它避免了管道停滞等等。

答案 1 :(得分:2)

虽然与此问题没有直接关系,但您可以使用Caliper正确地对代码进行微观基准测试。下面是您的代码的修改版本,以便它将与Caliper一起运行。内部循环必须进行一些修改,以便VM不会优化它们。在意识到一切都没有发生时,它出人意料地聪明。

在对Java代码进行基准测试时,还有很多细微差别。我写了一些我在Java Matrix Benchmark遇到的一些问题,比如过去的历史可以影响当前的结果。您可以使用Caliper来避免许多这些问题。

  1. http://code.google.com/p/caliper/
  2. Benchmarking issues with Java Matrix Benchmark

    public class PerformanceCheck extends SimpleBenchmark {
    
    public int timeFirstCase(int reps) {
        List<PerformanceCheck> removeList = new LinkedList<PerformanceCheck>();
        removeList.add( new PerformanceCheck());
        int ret = 0;
    
        for( int i = 0; i < reps; i++ )  {
            if (removeList.size() > 0) {
                if( testFunc(i) )
                    ret++;
            }
        }
    
        return ret;
    }
    
    public int timeSecondCase(int reps) {
        List<PerformanceCheck> removeList = new LinkedList<PerformanceCheck>();
        removeList.add( new PerformanceCheck());
        int ret = 0;
    
        for( int i = 0; i < reps; i++ )  {
            if (removeList.size() == 0) {
                if( testFunc(i) )
                    ret++;
            }
        }
    
        return ret;
    }
    
    private static boolean testFunc(int test) {
        return 5 > test;
    }
    
    public static void main(String[] args) {
        Runner.main(PerformanceCheck.class, args);
    }
    }
    
  3. 输出:

     0% Scenario{vm=java, trial=0, benchmark=FirstCase} 0.60 ns; σ=0.00 ns @ 3 trials
    50% Scenario{vm=java, trial=0, benchmark=SecondCase} 1.92 ns; σ=0.22 ns @ 10 trials
    
     benchmark    ns linear runtime
     FirstCase 0.598 =========
    SecondCase 1.925 ==============================
    
    vm: java
    trial: 0
    

答案 2 :(得分:1)

好吧,我很高兴不必处理Java性能优化。我自己尝试使用Java JDK 7 64-Bit。结果是任意的;)。我使用的列表或者在进入循环之前缓存size()的结果没有区别。同样完全消除测试功能几乎没有区别(所以它也不能是分支预测命中)。 优化标志可以提高性能,但也是随意的。

这里唯一合乎逻辑的结果是JIT编译器有时能够优化掉语句(这不是那么难),但它似乎相当随意。我喜欢像C ++这样的语言的原因之一,其中行为至少是确定性的,即使它有时是任意的。

在最新的Eclipse中BTW,就像它一直在Windows上一样,通过IDE运行这段代码“运行”(没有调试)比从控制台运行它慢10倍......这就是...

答案 3 :(得分:1)

当运行时编译器可以计算出testFunc求值为常量时,我​​相信它不会评估循环,这解释了加速。

当条件为removeList.size() == 0时,函数testFunc(3)将被计算为常量。当条件为removeList.size() != 0时,内部代码永远不会被评估,因此无法加速。您可以按如下方式修改代码:

for (int times = 0; times < maxTimes; times++) {
            testFunc();  // Removing this call makes the code slow again!
            if (removeList.size() != 0) {
                testFunc();
            }
        }

private static boolean testFunc() {
    return testFunc(3);
}

当最初未调用testFunc()时,运行时编译器没有意识到testFunc()计算为常量,因此无法优化循环。

某些功能,如

private static int testFunc2(int test) {
    return 5*test;
}

编译器可能会尝试预先优化(在执行之前),但显然不是因为参数的情况是作为整数传入并在条件中进行评估。

您的基准返回时间

time: 107
time: 106
time: 0
time: 0
...

建议运行时编译器需要2次外循环迭代才能完成优化。使用-server标志进行编译可能会返回基准测试中的所有0。

答案 4 :(得分:1)

每次迭代的时间都是不切实际的。这意味着JIT检测到您的代码没有做任何事情并且已经消除了它。微妙的变化可能会混淆JIT,它无法确定代码什么都不做,而且需要一些时间。

如果你改变测试以做一些有用的事情,那么差异就会消失。

答案 5 :(得分:0)

这些基准测试很难,因为编译器非常聪明。一个猜测:由于忽略了testFunc()的结果,编译器可能会完全优化它。添加一个计数器,如

   if (testFunc(3))
     counter++;

而且,为了彻底,最后做一个System.out.println(counter)