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

Does the Java compiler optimize an unnecessary ternary operator?

我一直在审查一些编码人员使用冗余三元运算符“为了可读性”的代码。如:

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

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

是的,Java 编译器确实进行了优化。很容易验证:

public class Main1 {
  public static boolean test(int foo, int bar, int baz) {
    return foo == bar && bar == baz ? true : false;
  }
}

javac Main1.javajavap -c Main1 之后:

  public static boolean test(int, int, int);
    Code:
       0: iload_0
       1: iload_1
       2: if_icmpne     14
       5: iload_1
       6: iload_2
       7: if_icmpne     14
      10: iconst_1
      11: goto          15
      14: iconst_0
      15: ireturn

public class Main2 {
  public static boolean test(int foo, int bar, int baz) {
    return foo == bar && bar == baz;
  }
}

javac Main2.javajavap -c Main2 之后:

  public static boolean test(int, int, int);
    Code:
       0: iload_0
       1: iload_1
       2: if_icmpne     14
       5: iload_1
       6: iload_2
       7: if_icmpne     14
      10: iconst_1
      11: goto          15
      14: iconst_0
      15: ireturn

两个示例都以完全相同的字节码结尾。

在 IntelliJ 中,我编译了您的代码并打开了 class 文件,该文件会自动反编译。结果是:

boolean val = foo == bar && foo1 != bar;

所以是的,Java 编译器对其进行了优化。

我发现不必要地使用三元运算符会使代码更加混乱,可读性降低,这与初衷背道而驰。

也就是说,通过比较 JVM 编译的字节码,可以很容易地测试编译器在这方面的行为。
这里有两个 mock 类 来说明这一点:

情况一(没有三元运算符):

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);
    }
}

案例二(使用三元运算符):

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 编译器视为 优化 在某种程度上取决于您对优化的定义。在某些方面,正如在其他答案中多次指出的那样,争论不是有道理的——这不是一种优化,因为在这两种情况下,生成的字节码都是执行的最简单的堆栈操作集这个任务,不管三元。

但是关于主要问题:

Obviously it would be better to just assign the statement’s result to the boolean variable, but does the compiler care?

简单的答案是否定的。编译器不关心。

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

查看测试用例。

Class 1:计算布尔表达式,将其存储在一个变量中,return那个变量.

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) 编译为完全相同的字节码,不是因为编译器优化掉了三元运算符,而是因为它本质上每次都需要执行那个简单的三元运算符。它需要在字节码级别指定 return 是真还是假。要验证这一点,请查看案例 (3)。除了交换第 5 行和第 9 行之外,它与字节码完全相同。

当反编译生成 a == ba == b ? true : false 会发生什么?选择最简单的路径是反编译器的选择。

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

Class 2:计算一个布尔表达式和return结果(不将其存储在变量)

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

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

一开始我觉得这很奇怪,因为我原以为所有 3 个案例都是一样的(不包括案例 (3) 的交换行 5,9)。当编译器遇到 a == b 时,它会在遇到 a == b ? true : false 之后立即计算表达式和 returns,而在遇到 a == b ? true : false 时它会使用 goto 转到行 ireturn.我知道这样做是为了留下 space 以便在三元运算符的 'true' 情况下评估潜在的语句:在 if_icmpne 检查和 goto 行之间。即使在这种情况下它只是一个布尔值 true 编译器也会像在一般情况下那样处理它,其中会出现更复杂的块.
另一方面,"Class 1" 实验掩盖了这一事实,因为在 true 分支中还有 istoreiload 而不仅仅是 ireturn 迫使 goto 命令并在情况 (1) 和 (2) 中产生完全相同的字节码。

关于测试环境的说明,这些字节码是使用最新的 Eclipse (4.10) 生成的,它使用各自的 ECJ 编译器,不同于 IntelliJ IDEA 使用的 javac。

但是,阅读其他答案(使用 IntelliJ)中的 javac 生成的字节码,我相信同样的逻辑也适用于那里,至少对于 "Class 1" 实验,其中值已存储但未立即 returned。

最后,正如其他答案(例如 and 的答案)中已经指出的那样,在此线程和 SO 的其他问题中,大量优化是由 JIT 编译器完成的,而不是来自java-->java-字节码编译器,所以这些检查虽然对字节码翻译提供了信息,但并不是衡量最终优化代码将如何执行的好方法。

补充: 的回答比较了 javac 和 ECJ 针对类似情况生成的字节码

(作为免责声明,我没有研究 Java 编译或反汇编那么多以真正了解它在幕后做了什么;我的结论主要基于上述实验的结果。)

javac 编译器通常不会在输出字节码之前尝试优化代码。相反,它依赖于 Java 虚拟机 (JVM) 和即时 (JIT) 编译器,将字节码转换为机器码,从而使构造等同于更简单的构造。

这使得确定 Java 编译器的实现是否正常工作变得更加容易,因为大多数结构只能由一个预定义的字节码序列表示。如果编译器生成任何其他字节码序列,它就会被破坏,即使该序列的行为方式与原始序列相同

检查 javac 编译器的字节码输出并不是判断构造是否可能有效执行的好方法。看起来可能存在某些 JVM 实现,其中 (someCondition ? true : false) 等构造的性能比 (someCondition) 差,而某些 JVM 实现的性能相同。

我想要 synthesize 之前答案中提供的极好的信息。

下面代码看看Oracle的javac和Eclipse的ecj做了什么:

boolean  valReturn(int a, int b) { return a == b; }
boolean condReturn(int a, int b) { return a == b ? true : false; }
boolean   ifReturn(int a, int b) { if (a == b) return true; else return false; }

void  valVar(int a, int b) { boolean c = a == b; }
void condVar(int a, int b) { boolean c = a == b ? true : false; }
void   ifVar(int a, int b) { boolean c; if (a == b) c = true; else c = false; }

(我稍微简化了您的代码 - 一次比较而不是两次比较 - 但下面描述的编译器的行为基本相同,包括它们略有不同的结果。)

我用javac和ecj编译代码,然后用Oracle的javap反编译。

这是 javac 的结果(我尝试了 javac 9.0.4 和 11.0.2 - 它们生成完全相同的代码):

boolean valReturn(int, int);
  Code:
     0: iload_1
     1: iload_2
     2: if_icmpne     9
     5: iconst_1
     6: goto          10
     9: iconst_0
    10: ireturn

boolean condReturn(int, int);
  Code:
     0: iload_1
     1: iload_2
     2: if_icmpne     9
     5: iconst_1
     6: goto          10
     9: iconst_0
    10: ireturn

boolean ifReturn(int, int);
  Code:
     0: iload_1
     1: iload_2
     2: if_icmpne     7
     5: iconst_1
     6: ireturn
     7: iconst_0
     8: ireturn

void valVar(int, int);
  Code:
     0: iload_1
     1: iload_2
     2: if_icmpne     9
     5: iconst_1
     6: goto          10
     9: iconst_0
    10: istore_3
    11: return

void condVar(int, int);
  Code:
     0: iload_1
     1: iload_2
     2: if_icmpne     9
     5: iconst_1
     6: goto          10
     9: iconst_0
    10: istore_3
    11: return

void ifVar(int, int);
  Code:
     0: iload_1
     1: iload_2
     2: if_icmpne     10
     5: iconst_1
     6: istore_3
     7: goto          12
    10: iconst_0
    11: istore_3
    12: return

这是 ecj(版本 3.16.0)的结果:

boolean valReturn(int, int);
  Code:
     0: iload_1
     1: iload_2
     2: if_icmpne     7
     5: iconst_1
     6: ireturn
     7: iconst_0
     8: ireturn

boolean condReturn(int, int);
  Code:
     0: iload_1
     1: iload_2
     2: if_icmpne     9
     5: iconst_1
     6: goto          10
     9: iconst_0
    10: ireturn

boolean ifReturn(int, int);
  Code:
     0: iload_1
     1: iload_2
     2: if_icmpne     7
     5: iconst_1
     6: ireturn
     7: iconst_0
     8: ireturn

void valVar(int, int);
  Code:
     0: iload_1
     1: iload_2
     2: if_icmpne     9
     5: iconst_1
     6: goto          10
     9: iconst_0
    10: istore_3
    11: return

void condVar(int, int);
  Code:
     0: iload_1
     1: iload_2
     2: if_icmpne     9
     5: iconst_1
     6: goto          10
     9: iconst_0
    10: istore_3
    11: return

void ifVar(int, int);
  Code:
     0: iload_1
     1: iload_2
     2: if_icmpne     10
     5: iconst_1
     6: istore_3
     7: goto          12
    10: iconst_0
    11: istore_3
    12: return

对于六个函数中的五个,两个编译器生成的代码完全相同。 The only differencevalReturn 中:javac 生成 gotoireturn,但 ecj 生成 ireturn。对于 condReturn,它们都生成 gotoireturn。对于 ifReturn,它们都生成一个 ireturn

这是否意味着其中一个编译器优化了其中一种或多种情况?可能有人会认为javac优化了ifReturn代码,但是没有优化valReturncondReturn,而ecj优化了ifReturnvalReturn,但是没有优化condReturn.

但我认为那不是真的。 Java 源代码编译器基本上根本不优化代码。 优化代码的编译器是JIT(即时)编译器(JVM中将字节码编译成机器码的部分),JIT编译器可以做一个如果字节码相对简单,即被优化,效果会更好。

简而言之:不,Java 源代码编译器不会优化这种情况,因为它们并没有真正优化任何东西。他们只做规范要求他们做的事,仅此而已。 javac 和 ecj 开发人员只是为这些情况选择了稍微不同的代码生成策略(大概是出于或多或少的任意原因)。

有关更多详细信息,请参阅 these Stack Overflow questions

(恰当的例子:现在两个编译器都忽略了 -O 标志。ecj 选项明确这样说:-O: optimize for execution time (ignored)。javac 甚至不再提及该标志,只是忽略它。)