【问题标题】:Java Loops OptimizationJava 循环优化
【发布时间】:2012-09-25 08:51:09
【问题描述】:

给出以下(直截了当的)代码:

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

你能解释一下大的时差吗?从理论上讲,这些循环实现了相同的功能,但在实践中,这四个版本中的每一个似乎都存在相关的时间差。

重复执行后,结果几乎相同。

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

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

【问题讨论】:

  • 这是可重现的还是单发的?
  • java 中的规则是:JVM 比你聪明,不要试图以智取胜
  • @Andreas_D 可重现。 - 自己尝试代码。
  • @Andrei - 如果我手头有编译器,我会这样做的 ;) - 你能检查 BalusC 的第三个列表项并再试一次吗?没错,看起来后减量比减量前慢得多,但是 - 正如 BalusC 所示,它可能是一个“jvm-optimizing-artifact”
  • f(3) 的运行次数比 f(2) 少一倍,因为在比较之前会递减值。

标签: java performance optimization


【解决方案1】:

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

另见:


更新:好的,回答得有点生硬。使用后缀运算符 (i--) 的循环似乎比使用前缀运算符 (--i) 的循环慢。这可能是真的,因为值在表达式的评估期间发生了变化,但是编译器需要保存原始值的副本以在表达式中使用。使用前缀运算符避免了保留副本的需要,因为表达式中只会使用更改后的值。

另见:

毕竟,这种微优化会在 231 次执行中为您节省一到两秒的时间。您是否也经常执行它?比起过早的优化,我更喜欢可读性。

【讨论】:

  • @InsertNickHere 请检查我在您发表评论后插入的链接。
  • @BalusC 能否请您给出一些当前代码的应用示例?
  • 虽然这是真的,但这并不能解释为什么 f2() 在多次执行每个方法后运行时间明显更长。我怀疑分析字节码会有所帮助。
  • 你混淆了前缀和后缀
  • 您甚至没有以任何有用的方式对 JVM 进行基准测试。这些数字完全没有意义。
【解决方案2】:

当我在我的 JVM(Java HotSpot(TM) 64 位服务器 VM(内部版本 16.0-b13,混合模式))上运行此代码时,所有四个函数都给出了相似的结果:

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--&gt;0) 后减运算符不了解。基本上,在增量前后都需要i 的值,所以如果天真地实现此操作会涉及更多工作。

【讨论】:

  • Avi,是的,我的代码与你的不同。奇怪的。您还有什么建议吗?
  • @Andrei 您是否有机会在 Eclipse 中编译此代码,而不是 javac
  • 现在,我从命令行使用“普通”javac。我还向 javac 添加了 -O 参数。结果略有不同,但时差保持不变。
  • 在 OpenJDK 64 位上的结果相同,也许这种行为是 32 位特有的?
【解决方案3】:

多次运行后,Hotspot 编译器可能会优化每个方法。这需要时间并且有变化。但由于循环都以大致相同的方式工作,时间最终会变得相似似乎是合理的。

【讨论】:

    【解决方案4】:

    我怀疑这与在具有 32 位 ALU 的机器上执行 64 位算术有关。我怀疑由于微妙的流水线效应,在本机指令级别之前/之后的某些测试组合需要更长的时间。有人报告说数字在 64 位机器上持平的事实支持了这一理论。确认这一点的方法是获取 JIT 编译器生成的本机代码的转储,获取特定 CPU 的文档并找出时钟周期的去向。

    但老实说,我不知道这是否值得。我们有明确的证据表明您的微基准数字与 CPU 相关,并且所做的“工作”显然不具代表性。 (为什么要在 32 位机器上使用 long 循环计数器?)

    我也有点惊讶 JIT 编译器没有发现循环可以(在每种情况下)被完全优化掉。

    【讨论】:

      【解决方案5】:

      执行速度不同的几点原因

      • JVM 的垃圾收集可以 在某些执行期间运行过
      • 您的操作系统已安排其他 要执行的任务,因此也为其他应用程序提供了资源。甚至优先考虑他们 然后由于 do/while 和 for 的编译方式可能存在一些差异,但这些应该可以忽略不计

      让我们从头开始回答。

      这一定是由于操作系统或为 Windows 编译 Java 的方式,我已经在 Windows 上对其进行了测试,并在 Windows 上得到了和你一样的结果。

      【讨论】:

      • 我不认为 5 秒可以忽略。
      猜你喜欢
      • 2016-02-23
      • 1970-01-01
      • 1970-01-01
      • 2018-03-01
      • 1970-01-01
      • 2019-02-19
      • 2019-05-01
      • 2018-05-22
      • 1970-01-01
      相关资源
      最近更新 更多