用于优化循环语句的JVM选项

时间:2012-02-17 23:06:54

标签: java loops optimization jvm

我在学校被告知修改for loop的索引变量是不好的做法:

示例:

for(int i = 0 ; i < limit ; i++){
    if(something){
        i+=2;      //bad
    }
    if(something){
        limit+=2;      //bad
    }
}

论证是某些编译器优化可以优化循环,而不是重新计算索引并在每个循环中绑定。

我在java进行了一些测试,看来默认索引和绑定每次都会重新计算。

我想知道是否可以在JVM HotSpot中激活此类功能?

例如,优化这种循环:

for(int i = 0 ; i < foo.getLength() ; i++){   }

无需写:

int length = foo.getLength()
for(int i = 0 ; i < length ; i++){   }

这只是一个例子,我很想尝试看看改进。

修改

根据Peter Lawrey的回答为什么在这个简单的例子中JVM没有内联getLenght()方法?

public static void main(String[] args) {
   Too t = new Too();
   for(int j=0; j<t.getLength();j++){
   }
}


class Too {

    int l = 10;
    public Too() {
    }
    public int getLength(){
        //System.out.println("test");
        return l;
    }
}

在输出“test”中打印10次。

我认为优化这种执行可能会很好。

编辑2: 似乎我误解了......

我删除了println,实际上,探查者告诉我,在这种情况下,方法getLength()甚至不会调用一次。

4 个答案:

答案 0 :(得分:13)

这取决于foo.getLength()的作用。如果它可以内联,它可以是有效的相同的东西。如果无法内联,则JVM无法确定结果是否相同。

顺便说一句,你可以写一个班轮。

for(int i = 0, length = foo.getLength(); i < length; i++){   }

编辑:没有任何价值;

  • 方法和循环通常在被调用10,000次之前不会被优化。
  • 分析器子样本调用以减少开销。他们可能会计算每10或100或更多,所以一个微不足道的例子可能不会出现。

答案 1 :(得分:12)

  

我在java中做了一些测试,看起来默认索引和绑定每次重新计算。

根据Java语言规范,这个:

for(int i = 0 ; i < foo.getLength() ; i++){   }

表示在每次循环迭代时调用getLength()。 Java编译器只有允许才能将getLength()调用移出循环,如果他们能够有效证明它不会改变可观察行为。 (例如,如果getLength()每次只是从同一个变量返回相同的值,那么JIT编译器很可能可以内联调用,然后然后推断它可以执行但如果getLength()涉及获取并发或同步集合的长度,则允许优化的可能性很小......因为其他线程可能会采取行动。)

这就是允许执行的编译器。

  

我想知道是否可以在JVM HotSpot中激活这种功能?

简单的答案是否。

您似乎建议使用编译器开关来告知/允许编译器忽略JLS规则。没有这样的开关。这样的转换将是 BAD IDEA 。这将导致正确/有效/工作程序中断。考虑一下:

class Test {
   int count;

   int test(String[] arg) {
       for (int i = 0; i < getLength(arg); i++) {
           // ...
       }
       return count;
   }

   int getLength(String[] arg) {
       count++;
       return arg.length;
   }
}

如果允许编译器将getLength(arg)调用移出循环,它将改变调用该方法的次数,从而更改test方法返回的值。

改变正确编写的Java程序行为的Java优化不是有效的优化。 (请注意,多线程会使水变得混乱.JLS,特别是内存模型规则,允许编译器执行优化,这可能导致不同的线程看到应用程序状态的不一致版本...如果它们不同步从开发人员的角度来看,导致行为是不正确的。但真正的问题在于应用程序,而不是编译器。)


顺便说一句,更有说服力的原因是你不应该更改循环体中的循环变量,这会让你的代码更难理解。

答案 2 :(得分:4)

不这样做的主要原因是它使得理解和维护代码变得更加困难。

无论JVM如何优化,都不会损害程序的正确性。如果由于索引在循环内被修改而无法进行优化,那么它将不会对其进行优化。如果有这样的优化,我无法看到Java测试如何显示。

无论如何,Hotspot会为你优化很多东西。而你的第二个例子是Hotspot很乐意为你做的一种明确的优化。

答案 3 :(得分:2)

在我们进行更多推理为什么之前,不会内联字段访问。也许我们应该表明是的,如果你知道你在寻找什么(这在Java中真的是非常重要的),那么字段访问就好了。

首先,我们需要对JIT如何工作有一个基本的了解 - 我真的不能在一个答案中这样做。可以说JIT仅在经常调用函数后才起作用(通常> 10k)

因此我们使用以下代码进行实际测试:

public class Test {
    private int length;

    public Test() {
        length = 10000;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 14000; i++) {
            foo();
        }
    }

    public static void foo() {
        Test bar = new Test();
        int sum = 0;
        for (int i = 0; i < bar.getLength(); i++) {
            sum += i;
        }
        System.out.println(sum);
    }

    public int getLength() {
        System.out.print("_");
        return length;
    }    
}

现在我们编译这段代码并用java.exe -XX:+UnlockDiagnosticVMOptions -XX:CompileCommand=print,*Test.foo Test >Test.txt运行它会产生一个邪恶的长输出,但有趣的部分是:

  0x023de0e7: mov    %esi,0x24(%esp)
  0x023de0eb: mov    %edi,0x28(%esp)
  0x023de0ef: mov    $0x38fba220,%edx   ;   {oop(a 'java/lang/Class' = 'java/lang/System')}
  0x023de0f4: mov    0x6c(%edx),%ecx    ;*getstatic out
                                        ; - Test::getLength@0 (line 24)
                                        ; - Test::foo@14 (line 17)
  0x023de0f7: cmp    (%ecx),%eax        ;*invokevirtual print
                                        ; - Test::getLength@5 (line 24)
                                        ; - Test::foo@14 (line 17)
                                        ; implicit exception: dispatches to 0x023de29b
  0x023de0f9: mov    $0x3900e9d0,%edx   ;*invokespecial write
                                        ; - java.io.PrintStream::print@9
                                        ; - Test::getLength@5 (line 24)
                                        ; - Test::foo@14 (line 17)
                                        ;   {oop("_")}
  0x023de0fe: nop    
  0x023de0ff: call   0x0238d1c0         ; OopMap{[32]=Oop off=132}
                                        ;*invokespecial write
                                        ; - java.io.PrintStream::print@9
                                        ; - Test::getLength@5 (line 24)
                                        ; - Test::foo@14 (line 17)
                                        ;   {optimized virtual_call}
  0x023de104: mov    0x20(%esp),%eax
  0x023de108: mov    0x8(%eax),%ecx     ;*getfield length
                                        ; - Test::getLength@9 (line 25)
                                        ; - Test::foo@14 (line 17)
  0x023de10b: mov    0x24(%esp),%esi
  0x023de10f: cmp    %ecx,%esi
  0x023de111: jl     0x023de0d8         ;*if_icmpge
                                        ; - Test::foo@17 (line 17)

这是我们实际执行的内部循环。请注意,以下0x023de108: mov 0x8(%eax),%ecx将长度值加载到寄存器中 - 上面的内容用于System.out调用(我已将其删除,因为它使其更复杂,但因为不止一个人认为这个会妨碍内联我把它留在那里)。即使你不适合x86程序集,你也可以清楚地看到:除了原生的写调用之外,没有任何调用指令。