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

时间:2019-02-02 18:06:28

标签: java compiler-optimization code-readability

我一直在审查一些编码员为“提高可读性”而使用冗余三元运算符的代码,例如:

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

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

6 个答案:

答案 0 :(得分:28)

我发现三元运算符的不必要用法倾向于使代码更加混乱,并且可读性较低,这与最初的意图相反。

话虽如此,通过比较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);
    }
}

案例1中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编译器是否可以将其视为优化,在某种程度上取决于您对优化的定义。在某些方面,如指出,在其他的答案多次,是有意义的争论,没有 - 它不是一个优化的这么多,因为这是事实,在这两种情况下,生成的字节码是最简单的堆栈操作的组执行此任务,与三元无关。

但是关于主要问题:

  

显然,这将是最好只分配语句的结果   布尔变量,但是编译器在乎吗?

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

答案 1 :(得分:9)

Pavel Horal的答案相反, Codoyuvgin我认为,编译器的不优化远(或无视)三元运算即可。 (澄清:我指的是Java来字节码编译,而不是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 : false反编译后会产生a == b?反编译器的选择选择了最简单的路径。

此外,基于“第1类”实验,可以合理地假设a == b ? true : falsea == b完全相同,即它被转换为字节码。但是,这是不正确的。为了测试我们考察下面的“2级”,用“1级”是,这并不布尔结果存储在一个变量,而是立即返回它的唯一区别。

<强> 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

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

起初,我发现这很令人惊讶,因为我希望所有3个案例都相同(不包括案例(3)的5,9换行)。当编译器遇到a == b时,它会计算表达式并在遇到a == b ? true : false之后立即返回,而遇到goto时,编译器使用ireturn转到行if_icmpne。我知道这样做是为了在三元运算符的“ true”情况下(在goto检查和true行之间)为潜在的语句留出空间。即使在这种情况下,它只是一个布尔值true编译器也会像在通常情况下那样处理更复杂的块一样处理它。

另一方面,“第1类”实验掩盖了这一事实,因为在istore分支中还存在iloadireturn,而不仅仅是goto强迫{{ 1}}命令,并在情况(1)和(2)中产生完全相同的字节码。

有关测试环境的注释,这些字节码是使用最新的Eclipse(4.10)生成的,该Eclipse使用了各自的ECJ编译器,与IntelliJ IDEA使用的javac不同。

但是,在其他答案(使用IntelliJ)中读取javac产生的字节码,我相信同样的逻辑也适用于此,至少对于“ Class 1”实验而言,该值已存储且没有立即返回。 / p>

最后,正如在该线程和SO的其他问题中其他答案中所指出的那样,繁琐的优化工作是由JIT编译器而不是java-> java-bytecode编译器完成的,因此在进行这些检查时翔实的字节码翻译不属于最终的优化代码将如何执行的良好量度。

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

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

答案 2 :(得分:6)

是的,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

两个例子都结束了完全相同的字节码。

答案 3 :(得分:4)

在javac编译器通常不输出字节码之前尝试优化代码。相反,它依赖于在Java虚拟机(JVM)和刚刚在时间(JIT)编译器,其将字节码到机器代码的情况下的构建体将相当于一个简单的一个。

这使确定Java编译器的实现是否正确运行变得容易得多,因为大多数构造只能由一个预定义的字节码序列表示。如果一个编译器生成的任何其它的字节码序列,它被打破,即使序列将以相同的方式表现为原始

检查javac编译器的字节码输出不是判断构造可能高效执行还是无效执行的好方法。这似乎可能的是可能有一些JVM实现方式,其中的构建体如(someCondition ? true : false)将执行差于(someCondition),以及一些在那里他们将执行相同

答案 4 :(得分:1)

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

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

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

答案 5 :(得分:1)

我想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 difference位于valReturn中:javac生成一个gotoireturn的一个ireturn,但是ecj生成一个condReturn的一个。对于goto,它们都生成ireturnifReturn。对于ireturn,它们都生成一个ifReturn

这是否意味着其中一个编译器可以优化其中一种或多种情况?有人可能认为javac优化了valReturn代码,但未能优化condReturnifReturn,而ecj优化了valReturncondReturn,但未能优化-O

但是我认为那不是真的。 Java源代码编译器基本上根本不会优化代码。 优化代码的编译器是JIT(即时)编译器(JVM中将字节码编译成机器代码的部分),如果满足以下条件,则JIT编译器可以做得更好:字节码相对简单,即未优化。

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

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

(要点:今天,两个编译器都忽略-O: optimize for execution time (ignored)标志。ecj选项明确地这样说:namespace WebApplication8.Models { public class Person { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string URL { get; set; } public string DocName { get; set; } public IList<IFormFile> docs {get;set;} } } 。javac甚至不再提及该标志,而是忽略它。)