2012-02-17 98 views
7

我已经在学校告知,这是一个不好的做法,修改for loop的指标变量:JVM选项以优化循环语句

例子:

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++){ } 

这只是一个例子,我很好奇,想试试,看看改进措施。

编辑

据彼得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()甚至没有在这种情况下调用一次。

+2

您似乎误解*内联函数的作用。每个编译器优化的101个是生成的代码在功能上等同于JLS要求的行为。这意味着我们可以内联一个函数调用,但是我们不能删除一个'println()'调用。此外,你真的不应该担心这样的编译器优化 - 或者如果你这样做,你必须至少理解如何测试这种代码。 – Voo 2012-02-18 00:19:32

+0

好吧我不知道,我很新,仍然学到很多东西。这种“进步?”知识在(我)学校没有教学,所以我试图自己理解,而且我经常犯错误:s – 2012-02-18 00:40:14

+0

很自由,实际上仔细查看了代码 - 很快回答你的问题:JIT将内联'getLength ()'很好,独立于你是否有println()语句。如果你想要的细节我发布了下面的简短摘要;) – Voo 2012-02-18 01:09:31

回答

11

我在java中做了一些测试,似乎默认情况下每次都会重新计算索引和边界。

根据Java语言规范,这:

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

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

这就是编译器允许允许要做的事情。

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

简单的答案是否定的

你似乎在暗示一个编译器开关,它告诉/允许编译器忽略JLS规则。没有这样的开关。这样的开关将是坏主意。这可能会导致正确/有效/工作计划中断。试想一下:

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,特别是内存模型规则允许编译器执行优化,这可能导致不同线程看到应用程序状态的不一致版本......如果它们不同步不当,造成行为是从开发人员的角度不正确。但真正的问题是与应用程序,而不是编译器。)


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

+0

感谢您的信息。对于你答案的第二部分,我同意用工作程序启用(如果可能的话)这个特性可能是一个坏主意。但是如果你知道这个规则开始一个项目,我认为它可以提高性能。 – 2012-02-18 00:17:15

+0

实际判断一块Java是否比另一块运行速度更快的唯一方法是使用专门为Java设计的基准测试工具,它可以预热JVM和所有事情。 – 2012-02-18 00:23:08

+0

@LouisWasserman我使用了netbeans分析器。您对'println'严格要求,请参阅我的编辑。 – 2012-02-18 00:42:11

13

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

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

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

编辑:这是没有价值的;

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

感谢您的提示!请看看我的编辑。 – 2012-02-18 00:10:20

4

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

无论JVM如何优化,它都不会影响程序的正确性。如果因为索引在循环内部被修改而无法进行优化,那么它不会优化它。如果存在或不存在这样的优化,我无法看到Java测试如何显示。

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

+0

HotSpot对我来说是一个神秘的黑盒子,但它真的会做出这种优化吗?如果多线程修改foo使得getLength()正在改变,假设foo不是String,getLength是不可变的?即假定一个可变集合并使用getSize()来代替。 – 2012-02-17 23:46:59

+0

对于第二个示例,我不确定JVM Hotspot是否进行了任何优化。假设'foo.getLength()'返回10,那么getLength()方法将被执行10次。 – 2012-02-17 23:51:12

+1

由于优化,您将永远不会在程序的_behavior_中看到任何实际差异。如果您删除了'System.out.println(“test”)''这行,那么JVM将完全可能进行优化。 – 2012-02-18 00:22:02

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::[email protected] (line 24) 
             ; - Test::[email protected] (line 17) 
    0x023de0f7: cmp (%ecx),%eax  ;*invokevirtual print 
             ; - Test::[email protected] (line 24) 
             ; - Test::[email protected] (line 17) 
             ; implicit exception: dispatches to 0x023de29b 
    0x023de0f9: mov $0x3900e9d0,%edx ;*invokespecial write 
             ; - java.io.PrintStream::[email protected] 
             ; - Test::[email protected] (line 24) 
             ; - Test::[email protected] (line 17) 
             ; {oop("_")} 
    0x023de0fe: nop  
    0x023de0ff: call 0x0238d1c0   ; OopMap{[32]=Oop off=132} 
             ;*invokespecial write 
             ; - java.io.PrintStream::[email protected] 
             ; - Test::[email protected] (line 24) 
             ; - Test::[email protected] (line 17) 
             ; {optimized virtual_call} 
    0x023de104: mov 0x20(%esp),%eax 
    0x023de108: mov 0x8(%eax),%ecx  ;*getfield length 
             ; - Test::[email protected] (line 25) 
             ; - Test::[email protected] (line 17) 
    0x023de10b: mov 0x24(%esp),%esi 
    0x023de10f: cmp %ecx,%esi 
    0x023de111: jl  0x023de0d8   ;*if_icmpge 
             ; - Test::[email protected] (line 17) 

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

+0

ubuntu上的不幸,我收到了一个充满“_”的文件。我没有看到任何mov行... – 2012-02-18 09:48:15

+0

@alain确保你已经安装了必要的插件 - 并且更好地移除打印语句进行测试,以确保您不会错过它(或者在系统中安装空流。出) – Voo 2012-02-18 11:39:54

+0

如果有人感兴趣,这里找到了解决方案!如何使用-XX:+ UnlockDiagnosticVMOptions -XX:使用JVM HotSpot的CompileCommand =打印选项 – 2012-02-23 17:30:07