Java循环优化

7

给出以下(直接的)代码:

public class pr1 {

    public static void f1(){
        long sx = 0, s;
        s = System.currentTimeMillis();
        for(long i = 0; i < Integer.MAX_VALUE; ++i){
            sx += i;
        }
        System.out.println("f1(): " + (System.currentTimeMillis() - s));
    }

    public static void f2(){
        long sx = 0, s, i;
        s = System.currentTimeMillis();
        i = Integer.MAX_VALUE;
        while(i-->0){
            sx+=i;
        }
        sx += Integer.MAX_VALUE;
        System.out.println("f2(): " + (System.currentTimeMillis() - s));
    }

    public static void f3(){
        long sx = 0, s, i;
        s = System.currentTimeMillis();
        i = Integer.MAX_VALUE;
        while(--i>0){
            sx+=i;
        }
        sx += Integer.MAX_VALUE;
        System.out.println("f3(): " + (System.currentTimeMillis() - s));
    }

    public static void f4(){
        long sx = 0, s, i;
        s = System.currentTimeMillis();
        i = Integer.MAX_VALUE;
        do{
            sx+=i;
        }while(--i>0);
        System.out.println("f4(): " + (System.currentTimeMillis() - s));
    }

    public static void main(String args[]){
        f1();
        f2();
        f3();
        f4();
    }
}

运行代码后的实际结果:

f1(): 5828
f2(): 8125
f3(): 3406
f4(): 3781

你能解释一下大时间差吗?理论上循环实现了相同的功能,但实际上每个版本之间存在着显著的时间差。

经过反复执行,结果基本相同。

稍后编辑 作为另一个测试,我已重写了主方法:

public static void main(String args[]){
    for(int i = 0; i < 4; ++i){
        f1(); f2(); f3(); f4();
    }
}

新的结果如下:

f1(): 5906
f2(): 8266
f3(): 3406
f4(): 3844
f1(): 5843
f2(): 8125
f3(): 3438
f4(): 3859
f1(): 5891
f2(): 8156
f3(): 3406
f4(): 3813
f1(): 5859
f2(): 8172
f3(): 3438
f4(): 3828

并且重复10次:

f1(): 5844
f2(): 8156
f3(): 3453
f4(): 3813
f1(): 5844
f2(): 8218
f3(): 3485
f4(): 3937
f1(): 5985
f2(): 8156
f3(): 3422
f4(): 3781
f1(): 5828
f2(): 8234
f3(): 3469
f4(): 3828
f1(): 5844
f2(): 8328
f3(): 3422
f4(): 3859
f1(): 5844
f2(): 8188
f3(): 3406
f4(): 3797
f1(): 5906
f2(): 8219
f3(): 3422
f4(): 3797
f1(): 5843
f2(): 8203
f3(): 3454
f4(): 3906
f1(): 5844
f2(): 8140
f3(): 3469
f4(): 3812
f1(): 5860
f2(): 8109
f3(): 3422
f4(): 3813

在去除循环之间的计算后,结果仍然有些不同:

public class pr2 {

    public static void f1(){
        long sx = 0, s;
        s = System.currentTimeMillis();
        for(long i = 0; i < Integer.MAX_VALUE; ++i);
        System.out.println("f1(): " + (System.currentTimeMillis() - s));
    }

    public static void f2(){
        long sx = 0, s, i;
        s = System.currentTimeMillis();
        i = Integer.MAX_VALUE;
        while(i-->0);
        System.out.println("f2(): " + (System.currentTimeMillis() - s));
    }

    public static void f3(){
        long sx = 0, s, i;
        s = System.currentTimeMillis();
        i = Integer.MAX_VALUE;
        while(--i>0);
        System.out.println("f3(): " + (System.currentTimeMillis() - s));
    }

    public static void f4(){
        long sx = 0, s, i;
        s = System.currentTimeMillis();
        i = Integer.MAX_VALUE;
        do{
        }while(--i>0);
        System.out.println("f4(): " + (System.currentTimeMillis() - s));
    }

    public static void main(String args[]){
        for(int i = 0; i < 2; ++i){
            f1(); f2(); f3(); f4();
        }
    }
}

但是时差仍然存在:

f1(): 3219
f2(): 4859
f3(): 2610
f4(): 3031
f1(): 3219
f2(): 4812
f3(): 2610
f4(): 3062

JVM:

java version "1.6.0_20"
Java(TM) SE Runtime Environment (build 1.6.0_20-b02)
Java HotSpot(TM) Client VM (build 16.3-b01, mixed mode, sharing)

后续编辑: 在第一个版本中,我使用了javac的-O参数。新的结果如下:

f1(): 3219
f2(): 4859
f3(): 2610
f4(): 3031

后续编辑

好的,我在家用一台Linux机器尝试了相同的代码,具体配置如下:

java version "1.6.0_18"
OpenJDK Runtime Environment (IcedTea6 1.8) (6b18-1.8-0ubuntu1)
OpenJDK Server VM (build 14.0-b16, mixed mode)

结果是“正常”的。现在没有问题了:

f1(): 7495
f2(): 7418
f3(): 7457
f4(): 7384

5
Java中的规则是:JVM比你聪明,不要试图智取它。 - skaffman
@Andrei - 如果我手头有编译器的话,我会这么做;) - 你能检查一下BalusC的第三个列表项并再试一次吗?现在看起来后减比前减慢得多,但是正如BalusC所展示的那样,这可能是一个“jvm-优化工件”。 - Andreas Dolk
@skaffman,那个规则太糟糕了。 - Andrei Ciobanu
1
f(3) 运行次数比 f(2) 少一次,因为在比较之前减小了该值。 - ponzao
你尝试过以不同的顺序运行函数吗?例如,先运行 f3() 再运行 f2() - ponzao
显示剩余6条评论
5个回答

10

你实际上正在对JVM进行基准测试,而不是代码。

另请参阅:


更新:好吧,那个回答有点简短。使用后缀运算符(i--)的循环似乎比使用前缀运算符(--i)的循环慢。这可能是因为在表达式求值期间更改了该值,但编译器需要保留原始值的副本以便在表达式中使用。使用前缀运算符可以避免需要保留副本,因为表达式中只使用更改后的值。

另请参阅:

总之,这种微小的优化可能只会在执行 231 次时节省一两秒钟。你真的会经常执行吗?我更喜欢可读性超过过早优化。


1
@InsertNickHere 请在您的评论后检查我插入的链接。 - BalusC
虽然这是正确的,但它并没有解释为什么在执行每个方法多次后,f2()的运行时间明显更长。我怀疑分析字节码会有所帮助。 - matt b
你把前缀和后缀搞混了。 - Dave O.
分析字节码并不能解释这个问题,你需要查看 JIT 编译器生成的本地代码。那才是正在执行的内容。 - Stephen C
1
你甚至没有以任何有用的方式对JVM进行基准测试。这些数字完全毫无意义。 - Kevin Bourrillion
显示剩余5条评论

5
当我在我的JVM上运行此代码(Java HotSpot(TM) 64-Bit Server VM (build 16.0-b13, mixed mode)),所有四个函数都会给出类似的结果:
f1(): 3234
f2(): 3132
f3(): 3114
f4(): 3089

我猜测您的JVM在某个地方没有进行相同的优化。
您可以使用javap查看不同函数生成的字节码:javap -l -c pr1。当我这样做时,对于f2(),我得到以下结果:
public static void f2();
  Code:
   0:   lconst_0
   1:   lstore_0
   2:   invokestatic    #2; //Method java/lang/System.currentTimeMillis:()J
   5:   lstore_2
   6:   ldc2_w  #3; //long 2147483647l
   9:   lstore  4
   11:  lload   4
   13:  dup2
   14:  lconst_1
   15:  lsub
   16:  lstore  4
   18:  lconst_0
   19:  lcmp
   20:  ifle    31
   23:  lload_0
   24:  lload   4
   26:  ladd
   27:  lstore_0
   28:  goto    11
   31:  lload_0
   32:  ldc2_w  #3; //long 2147483647l
   35:  ladd
   36:  lstore_0
   37:  getstatic       #5; //Field java/lang/System.out:Ljava/io/PrintStream;
   40:  new     #6; //class java/lang/StringBuilder
   43:  dup
   44:  invokespecial   #7; //Method java/lang/StringBuilder."<init>":()V
   47:  ldc     #13; //String f2():
   49:  invokevirtual   #9; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   52:  invokestatic    #2; //Method java/lang/System.currentTimeMillis:()J
   55:  lload_2
   56:  lsub
   57:  invokevirtual   #10; //Method java/lang/StringBuilder.append:(J)Ljava/lang/StringBuilder;
   60:  invokevirtual   #11; //Method java/lang/StringBuilder.toString:()Ljava/lang/String;
   63:  invokevirtual   #12; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
   66:  return

f2()较慢的一个可能原因是编译器/JVM未对while(i-->0)后置递减运算符进行优化。基本上,你需要在增加之前和之后都知道i的值,因此如果朴素地实现该操作,则需要更多的工作。


Avi,是的,我的代码和你的不同。很奇怪。你还有什么其他建议吗? - Andrei Ciobanu
@Andrei 你有没有在Eclipse中编译这段代码,而不是使用javac - matt b
现在,我正在使用命令行中的“普通”javac。我还向javac添加了-O参数。结果有些不同,但时间差异保持不变。 - Andrei Ciobanu
在OpenJDK 64位上得到相同的结果,也许这种行为与32位有关? - Denis Tulskiy

1

一些导致执行速度差异的原因

  • JVM 的垃圾回收可能在某些执行期间运行
  • 操作系统安排了其他任务来执行,并为其他应用程序提供资源。或甚至给予他们优先级,然后可能由于 do/while 和 for 编译的方式存在差异,但这些可以忽略不计。

让我们撤销我的答案。

这必须是由于操作系统或 Java 为 Windows 编译的方式,我也在 Windows 上测试了它并得到了类似你在 Windows 上的结果。


我认为5秒不可忽略。 - Andrei Ciobanu

1

经过多次运行,Hotspot编译器很可能会优化每个方法。这需要时间并且有所变化。但由于所有循环的工作方式都大致相同,因此时间最终会变得相似。


1

我怀疑这与在32位ALU的机器上进行64位算术有关。由于微妙的流水线效应,我怀疑在增量/减量之前/之后的某些测试组合在本地指令级别需要更长时间。有人报告数字在64位机器上是固定的,这支持了这个理论。确认这一点的方法是获取JIT编译器生成的本机代码转储,获得您特定CPU的文档,并弄清楚时钟周期去哪里。

但说实话,我不知道这是否值得。我们清楚地看到你的微基准测试数字依赖于CPU,并且所做的“工作”显然是不具代表性的。(为什么要在32位机器上使用循环计数器?)

而且,我也有点惊讶JIT编译器没有意识到在每种情况下都可以完全优化掉循环。


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