为什么我的JVM会进行某些运行时循环优化并导致我的代码出现错误?

10

Consider the following java code:

public int main() {
    int i = 1111;

    for (; rules(i) != true && i < Integer.MAX_VALUE; i++) {
        //LOG.debug("Testing i: " + i);
    }

    System.out.println("The mystery number is: " + i);

    return i;
}

protected boolean rules(int nb) {
    //...
}

我发现即使for循环继续评估为true,当其主体为空时,循环也会停止执行。 main的最终结果是错误的(i在98%的时间内为16698,有时会稍微高一些/低一些)。 如果我取消循环主体中的LOG语句,循环将一直运行,直到循环继续评估为false
我使用的JVM是MacOS X VM 1.6.0。
  • 它是否正在进行某种运行时优化
  • 这种运行时优化是否可以视为错误?
  • 还是Java规范中说for继续评估不应运行功能操作?
ps:完整的源代码+单元测试在此处可用:https://gist.github.com/dirtyhenry/5804130 更新:
  • 我只通过junit运行了代码。junit是否对此行为负责?
更新2:
java -version

返回值:

java version "1.6.0_51"
Java(TM) SE Runtime Environment (build 1.6.0_51-b11-456-11M4508)
Java HotSpot(TM) 64-Bit Server VM (build 20.51-b01-456, mixed mode)

尝试从 int 切换到 long。也许存在静默值溢出的情况。 - spas
@spas:有趣的是,当我从 int 切换到 long 并保持循环为空时,我得到了正确的结果。虽然有点奇怪,因为 16698 远远小于 Java 中 int 的最大值(2147483647,请参见 http://docs.oracle.com/javase/tutorial/java/nutsandbolts/datatypes.html)。 - Dirty Henry
如果您能提及“正确”的答案是什么,那可能会有所帮助。(如果不是16 698,那是什么?)此外,您能否用更简单的“rules(i)”测试重现这个问题? - A.M.
2
我认为“正确”的答案是Integer.MAX_VALUE,即当i增加到预期的最大值时,循环条件得到满足。 - Mark Chorley
是的。在查看源代码页面上的所有“规则”后,我最终看了一下 &&。 - A.M.
显示剩余6条评论
7个回答

3

我在这里有一个类似的问题:Near empty Java For-Loop acts strange 你可以尝试使用JVM 1.7或者尝试使用while循环:

public int main() {
    int i = 1111;

    while(i < Integer.MAX_VALUE){
        if (!rules(i)) {
            break;
        }
        i++
    }

    System.out.println("The mystery number is: " + i);

    return i;
}

2

我在SO上看到一些关于循环优化只在超过10000次迭代后才发生的参考。也许这就是为什么“魔法数字”通常在16000左右的原因?

这里有一个非常好的讨论:

JVM选项来优化循环语句


1
我使用JDK 1.7.0_21测试了您的代码,无论是否有LOG.debug语句,都返回相同的结果942210。
    int i = 0;
    for( ; !rules(i) && i < Integer.MAX_VALUE ; i++ ){
        //LOG.debug( "test " + i );
    }
    System.out.println( "i is " + i );

我也打印了两个版本(空循环)的字节码。

   0: aload_0       
   1: invokespecial #3                  // Method java/lang/Object."<init>":()V
   4: iconst_0      
   5: istore_1      
   6: aload_0       
   7: iload_1       
   8: invokevirtual #4                  // Method rules:(I)Z
  11: ifne          26
  14: iload_1       
  15: ldc           #5                  // int 2147483647
  17: if_icmpge     26
  20: iinc          1, 1
  23: goto          6
  26: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
  29: new           #7                  // class java/lang/StringBuilder
  32: dup           
  33: invokespecial #8                  // Method java/lang/StringBuilder."<init>":()V
  36: ldc           #9                  // String i is 
  38: invokevirtual #10                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  41: iload_1       
  42: invokevirtual #11                 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
  45: invokevirtual #12                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
  48: invokevirtual #13                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
  51: return 

循环并带有调试输出
   0: aload_0       
   1: invokespecial #3                  // Method java/lang/Object."<init>":()V
   4: iconst_0      
   5: istore_1      
   6: aload_0       
   7: iload_1       
   8: invokevirtual #4                  // Method rules:(I)Z
  11: ifne          48
  14: iload_1       
  15: ldc           #5                  // int 2147483647
  17: if_icmpge     48
  20: new           #6                  // class java/lang/StringBuilder
  23: dup           
  24: invokespecial #7                  // Method java/lang/StringBuilder."<init>":()V
  27: ldc           #8                  // String test 
  29: invokevirtual #9                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  32: iload_1       
  33: invokevirtual #10                 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
  36: invokevirtual #11                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
  39: invokestatic  #12                 // Method LOG.debug:(Ljava/lang/String;)V
  42: iinc          1, 1
  45: goto          6
  48: getstatic     #13                 // Field java/lang/System.out:Ljava/io/PrintStream;
  51: new           #6                  // class java/lang/StringBuilder
  54: dup           
  55: invokespecial #7                  // Method java/lang/StringBuilder."<init>":()V
  58: ldc           #14                 // String i is 
  60: invokevirtual #9                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  63: iload_1       
  64: invokevirtual #10                 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
  67: invokevirtual #11                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
  70: invokevirtual #15                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
  73: return 

正如您所看到的,循环结构是相同的。20-39只是构造字符串并调用LOG.debug。

因此,这可能是一个jdk问题。尝试使用1.7版本,应该可以解决。


0

我已经尝试过将其转换为常规的Java/main程序(即public static void main( String[] args ),不要将'main'名称用于其他目的),并实现了规则(nb)作为'return false;',它总是停在2147483647。我还尝试了'return Math.random() < 0.01',平均停在1200左右,正如几何分布所预测的那样。

我认为在你的情况下,rules()中发生了一些事情,javac不能跳过循环,只因为主体为空,你在条件中编写的代码可能需要被遍历。我一直像这样编写循环:while ( DORunThis() ); 并且它们总是运行到方法返回false,正如预期的那样。

顺便说一句:

!rules ( i )比丑陋的rules( i ) != true更紧凑、优雅。

也许你真正想要的不是:i < Integer.MAX_VALUE,而是:

for (; !rules ( i ) && i != Integer.MAX_VALUE; i++ );

它能够对MAX_VALUE的规则()进行评估。


0

你的另一个选择是在块内运行规则测试。JIT编译器很可能会将循环作为空操作而忽略掉它。尝试像这样:

public int main() {
    int i = 1111;

    for (; i < Integer.MAX_VALUE; i++) {
        boolean completed = !rules(i);
        if (completed) {
            break;
        }
    }

    System.out.println("The mystery number is: " + i);

    return i;
}

0
尝试用任何其他(未注释的)有效语句替换日志记录语句,循环也将按预期运行。这绝对是JVM代码优化策略。我认为JVM没有理由无缘无故地在循环周围循环。

0

空循环和条件语句可以被JIT编译器优化掉,这绝对是正在发生的事情。


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