Java效率

12

我在尝试计算一些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() > 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;
}

会导致再次变得快速。


5
这种微基准测试不是一个很好的想法。同时要意识到JIT可能会在任意时间做出各种决策,并且如果它发现实际上没有任何事情发生,它可能会完全优化掉你的代码。 - Dave Newton
1
你应该在Java中使用System.nanoTime()来测量代码执行时间,它更加精确。更多讨论请参见这个问题 - calebds
size() 函数的作用不重要。这两段代码片段都具有相同的调用方式。精度不重要。这些 JVM 标志不重要。通过 IDE 运行也不重要。这里没有 GC 问题。 - Sean Owen
@Owen:听起来你是一名纯粹的Java程序员?! - thesaint
1
所有这些关于nanoTime()或milliTime()的讨论,感觉就像在争论是否要使用创可贴还是布条来帮助一个被反坦克弹击中的人。 - lessthanoptimal
显示剩余6条评论
6个回答

3

这真是令人惊讶。生成的字节码除了条件语句外完全相同,它们分别是ifleifne

如果使用-Xint关闭JIT,则结果更加合理。第二个版本要慢2倍。所以这与JIT优化有关。

我认为它可以优化第二种情况中的检查,但不能优化第一种情况(由于某种原因)。即使这意味着执行函数的工作,但没有该条件语句可以使事情更快。它避免了流水线停顿等问题。


问题在于,这也取决于testFunc()。将第一个版本的testFunc()替换为其他任何内容,例如: private static int testFunc2(int test) { return 5*test; } 这将导致更低的计算时间。 - x4-

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);
    }
    }
    
输出:
 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

我认为问题不在于如何进行微基准测试,而在于解释观察到的行为。但你顺便提到了它; 这是JIT以意想不到的方式优化某些内容。 - Sean Owen
是的,我同意。我想指出正确的方法,因为即使在解释了那段代码之后,由于基准测试的设置方式,仍然会出现奇怪的波动。 - lessthanoptimal
谢谢您提供关于Caliper的提示。我将在未来使用它。但问题仍然存在,即为什么Java会表现出这种行为。在您的示例中,您通过向列表添加对象来切换行为。现在 timeSecondCase 的 if 条件返回 false。仍然有一个版本,在该版本中没有调用 testFunc() 函数,速度较慢。 - x4-

1

我很高兴不必处理Java性能优化。我尝试了使用Java JDK 7 64位版本。结果是随意的;). 使用哪些列表或在进入循环之前缓存size()的结果都没有任何区别。完全清除测试函数也几乎没有影响(因此也不能是分支预测命中)。 优化标志可以提高性能,但同样是随意的。

这里唯一的逻辑结果是JIT编译器有时能够优化语句(这并不难以实现),但它似乎相当随意。这是我更喜欢像C++这样的语言的许多原因之一,即使有时是随意的,行为至少是确定的。

顺便说一下,在最新的Eclipse中,就像在Windows上一直如此,在IDE“运行”(无调试)中运行此代码比从控制台运行慢10倍,所以就是这样...


1

每次迭代的时间都不真实地快。这意味着JIT检测到您的代码没有执行任何操作,并将其消除。微小的更改可能会使JIT混淆,无法确定代码是否没有执行任何操作,从而需要一些时间。

如果您将测试更改为执行某些微不足道的有用操作,则差异将消失。


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。

0

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

   if (testFunc(3))
     counter++;

为了更加全面,最后执行System.out.println(counter)


但这会让第一版更快,但实际上并不是这样。 - Sean Owen
2
可能是排序的问题,如@prelic所建议的那样。由于某些原因,JIT第一次没有优化调用,但在第二次发现并解决了这个问题。 - user949300
我已经在本地两种方式运行它,但事情并没有改变。 - Sean Owen
我尝试了包含计数器并将testFunc(3)更改为testFunc(times)。第一个版本(不调用testFunc())仍然比较慢,慢了两倍。 - x4-

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接