Java 编译器是否优化了不必要的三元运算符?

我一直在审查代码,其中一些编码人员一直在使用冗余的三元运算符“以提高可读性”。如:

boolean val = (foo == bar && foo1 != bar) ? true : false;

显然,最好只是将语句的结果分配给变量,但是编译器关心吗?boolean


答案 1

我发现不必要的三元运算符的使用往往会使代码更加混乱,可读性更差,这与初衷相反。

话虽如此,编译器在这方面的行为可以通过比较JVM编译的字节码来轻松测试。
下面是两个模拟类来说明这一点:

情况 I(不含三元运算符):

class Class {

    public static void foo(int a, int b, int c) {
        boolean val = (a == c && b != c);
        System.out.println(val);
    }

    public static void main(String[] args) {
       foo(1,2,3);
    }
}

情况 II(使用三元运算符):

class Class {

    public static void foo(int a, int b, int c) {
        boolean val = (a == c && b != c) ? true : false;
        System.out.println(val);
    }

    public static void main(String[] args) {
       foo(1,2,3);
    }
}

案例 I 中 foo() 方法的字节码:

       0: iload_0
       1: iload_2
       2: if_icmpne     14
       5: iload_1
       6: iload_2
       7: if_icmpeq     14
      10: iconst_1
      11: goto          15
      14: iconst_0
      15: istore_3
      16: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
      19: iload_3
      20: invokevirtual #3                  // Method java/io/PrintStream.println:(Z)V
      23: return

案例 II 中 foo() 方法的字节码:

       0: iload_0
       1: iload_2
       2: if_icmpne     14
       5: iload_1
       6: iload_2
       7: if_icmpeq     14
      10: iconst_1
      11: goto          15
      14: iconst_0
      15: istore_3
      16: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
      19: iload_3
      20: invokevirtual #3                  // Method java/io/PrintStream.println:(Z)V
      23: return

请注意,在这两种情况下,字节码是相同的,即编译器在编译布尔值时忽略三元运算符。val


编辑:

关于这个问题的对话已经朝着几个方向之一发展。
如上所示,在这两种情况下(无论是否使用冗余三元),编译后的java字节码都是相同的
这是否可视为 Java 编译器的优化,在某种程度上取决于您对优化的定义。在某些方面,正如在其他答案中多次指出的那样,争论“不”是有道理的 - 它不是一种优化,而是在这两种情况下,生成的字节码都是执行此任务的最简单的堆栈操作集,而不管三元。

但是关于主要问题:

显然,最好将语句的结果分配给布尔变量,但是编译器关心吗?

简单的答案是否定的。编译器不在乎。


答案 2

Pavel HoralCodoyuvgin的答案相反,我认为编译器不会优化(或忽略)三元运算符。(澄清:我指的是Java到Bytecode编译器,而不是JIT)

请参阅测试用例。

第 1 类:计算布尔表达式,将其存储在变量中,然后返回该变量。

public static boolean testCompiler(final int a, final int b)
{
    final boolean c = ...;
    return c;
}

因此,对于不同的布尔表达式,我们检查字节码:1. 表达式:a == b

字节码

   0: iload_0
   1: iload_1
   2: if_icmpne     9
   5: iconst_1
   6: goto          10
   9: iconst_0
  10: istore_2
  11: iload_2
  12: ireturn
  1. 表达:a == b ? true : false

字节码

   0: iload_0
   1: iload_1
   2: if_icmpne     9
   5: iconst_1
   6: goto          10
   9: iconst_0
  10: istore_2
  11: iload_2
  12: ireturn
  1. 表达:a == b ? false : true

字节码

   0: iload_0
   1: iload_1
   2: if_icmpne     9
   5: iconst_0
   6: goto          10
   9: iconst_1
  10: istore_2
  11: iload_2
  12: ireturn

情况(1)和(2)编译为完全相同的字节码,不是因为编译器优化了三元运算符,而是因为它基本上需要每次都执行这个平凡的三元运算符。它需要在字节码级别指定是返回 true 还是 false。要验证这一点,请查看案例 (3)。它是完全相同的字节码,除了交换的第5行和第9行。

然后会发生什么,当反编译产生时?反编译器的选择是选择最简单的路径。a == b ? true : falsea == b

此外,基于“Class 1”实验,可以合理地假设在转换为字节码的方式上与 完全相同。然而,事实并非如此。为了测试我们检查以下“类2”,与“类1”的唯一区别是,它不将布尔结果存储在变量中,而是立即返回它。a == b ? true : falsea == b

第 2 类:计算布尔表达式并返回结果(不将其存储在变量中)

public static boolean testCompiler(final int a, final int b)
{
    return ...;
}
    1. a == b

字节码:

   0: iload_0
   1: iload_1
   2: if_icmpne     7
   5: iconst_1
   6: ireturn
   7: iconst_0
   8: ireturn
    1. a == b ? true : false

字节码

   0: iload_0
   1: iload_1
   2: if_icmpne     9
   5: iconst_1
   6: goto          10
   9: iconst_0
  10: ireturn
    1. a == b ? false : true

字节码

   0: iload_0
   1: iload_1
   2: if_icmpne     9
   5: iconst_0
   6: goto          10
   9: iconst_1
  10: ireturn

这里很明显, 表达式的编译方式不同,因为情况 (1) 和 (2) 产生不同的字节码(情况 (2) 和 (3),正如预期的那样,只有第 5,9 行被交换)。a == ba == b ? true : false

起初,我发现这令人惊讶,因为我期望所有3个案例都是相同的(不包括案例(3)的交换行5,9)。当编译器遇到时,它会计算表达式并在遇到它使用 to to line 的地方后立即返回。我理解这样做是为了在三元运算符的“true”情况下为潜在语句留出空间:在检查和行之间。即使在这种情况下它只是一个布尔值,编译器也会像在一般情况下处理它一样处理它,因为存在更复杂的块
另一方面,“Class 1”实验掩盖了这一事实,因为在分支中也有,并且不仅在情况(1)和(2)中强制命令并导致完全相同的字节码。a == ba == b ? true : falsegotoireturnif_icmpnegototruetrueistoreiloadireturngoto

作为关于测试环境的注意事项,这些字节码是使用最新的Eclipse(4.10)生成的,Eclipse使用各自的ECJ编译器,与IntelliJ IDEA使用的javac不同。

但是,在其他答案(使用IntelliJ)中读取javac生成的字节码,我相信相同的逻辑也适用于那里,至少对于存储值并且不会立即返回的“Class 1”实验也是如此。

最后,正如在其他答案(例如supercatjcsahnwaldt的答案)中已经指出的那样,在这个线程和SO的其他问题中,繁重的优化是由JIT编译器完成的,而不是从java-->java-bytecode编译器完成的,所以这些检查虽然对字节码转换很有帮助,但并不能很好地衡量最终优化代码将如何执行。

补充:jcsahnwaldt的答案比较了javac和ECJ在类似情况下产生的字节码

(作为免责声明,我没有对Java编译或反汇编进行过太多的研究,以真正了解它在引擎盖下的作用;我的结论主要基于上述实验的结果。


推荐