【问题标题】:Does the Java compiler optimize an unnecessary ternary operator?Java 编译器是否优化了不必要的三元运算符?
【发布时间】:2019-06-27 00:01:37
【问题描述】:

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

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

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

【问题讨论】:

  • 出于好奇,这些编码人员是否会执行if( foo == true )“为了可读性”之类的工作?
  • 对不起。我也必须与这样做的编码人员一起工作。根据我的经验,你很快就会失去信心。 ;)
  • 除了使用 java 反编译器或字节码查看器尝试之外,真的没有其他办法知道,即使那样它也可能取决于编译器版本。我的意思是,假设有人出现并说“不,它不会”,你为什么要相信他们?我的猜测是它会,如果不是,那么 JIT 很可能会,但相信我的话。
  • 他们是否对所有布尔表达式都这样做?这很奇怪。我很好奇他们是否也做if ( isValid(input) ? true : false )之类的事情。
  • 我会少担心编译器,多担心编码标准和代码审查。在分析时,表达式永远不会成为您的热点,所以我主要担心的是这些表达式实际上不太可读,而不是它们可能会使程序减慢一两条指令。

标签: java compiler-optimization code-readability


【解决方案1】:

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

话虽如此,编译器在这方面的行为可以很容易地通过比较 JVM 编译的字节码来测试。
这里有两个模拟类来说明这一点:

案例一(没有三元运算符):

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

案例一中 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

案例二中 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 表示“我发现不必要地使用三元运算符会使代码更加混乱和可读性降低,这与初衷相反。”如果我是你,我会把它移到开头。
  • 作者所说的“可读性”可能是指“绝对新手的可理解性”,这可能是一个有效的观点。 if(myBoolean) 让新手有些困惑,而 if(myBoolean == true) 直截了当。
  • 您永远不应该从编译器示例中推断语言行为。可能优化是可选的,你可能会在不同的编译器上得到不同的结果。在做出这些决定时,请始终参考语言规范。
【解决方案2】:

Pavel Horal的回答相反, Codoyuvgin 我认为编译器不会优化(或忽略)三元运算符。 (澄清:我指的是 Java to 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)编译成完全相同的字节码,不是因为编译器优化掉了三元运算符,而是因为它本质上每次都需要执行那个微不足道的三元运算符。它需要在字节码级别指定是返回真还是假。要验证这一点,请查看案例 (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 检查和goto 行之间。即使在这种情况下它只是一个布尔值 true编译器也会像在存在更复杂块的一般情况下一样处理它
另一方面,“1 类”实验掩盖了这一事实,因为在 true 分支中也有 istoreiload 而不仅仅是 ireturn 强制使用 goto 命令并导致完全相同情况(1)和(2)中的字节码。

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

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

最后,正如在其他答案(例如supercatjcsahnwaldt)中已经指出的那样,无论是在这个线程中还是在 SO 的其他问题中,重度优化都是由 JIT 编译器完成的,而不是来自java-->java-bytecode 编译器,因此这些检查虽然对字节码翻译提供了信息,但并不能很好地衡量最终优化代码将如何执行。

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

(作为免责声明,我没有对 Java 编译或反汇编进行过多研究,无法真正了解它的底层功能;我的结论主要基于上述实验的结果。)

【讨论】:

  • 没错。 a == b ? true : false a == b。它们的字面意思是相同的,在前一种情况下不需要执行额外的操作。这不是优化,它只是两个完全等效的语句产生相同的翻译代码的情况,这应该不足为奇。 Java 代码是程序的描述;这是一个抽象。默认情况下,它不是英语单词到 CPU 或 JVM 指令的一对一映射。这里没有逻辑分支,因此不存在实际分支的期望。
  • @HenningMakholm 我知道这一点;这不是我说的。我的观点是,并非将源代码转换为实际程序的过程的每个部分都是“优化”,我认为您在“只是正常的翻译过程”和“优化”之间划清了界线。优化产生的逻辑虽然具有相同的语义,但在其方法中与原始代码的逻辑明显不同。这不是 - 它实际上是相同的。尽管具有讽刺意味的是,这实际上只是对语义的争论。
  • @LightnessRacesinOrbit:不,这里显示的不是天真的翻译。简单的翻译会将boolean 类型的每个表达式转换为在堆栈上生成布尔值的代码,与上下文无关。根据定义,做任何与本地翻译不同的事情都是“优化”(除非它是一个错误)。
  • @supercat:语言规范要求对“无法访问的代码”错误使用非常精确定义的常量传播算法。为此目的,不允许使用更聪明的愚蠢的方法。根据您生成的实际代码,这不是一个好主意。这会阻止您在该阶段可能想做的太多小的代码生成优化。
  • 我想更有趣的实验可能是while (a==b ? false : false)someBool = (a==b) ? false:false;。我没有方便的 javac,但这些情况会很有趣,因为循环体将是静态无法访问的,但我不认为 cond ? const1 : const1 被视为常量表达式。
【解决方案3】:

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

两个示例最终得到完全相同的字节码。

【讨论】:

    【解决方案4】:

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

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

    检查 javac 编译器的字节码输出并不是判断一个构造是否可能高效执行的好方法。在某些 JVM 实现中,(someCondition ? true : false) 之类的构造的性能似乎比 (someCondition) 差,而在某些 JVM 实现中,它们的性能却相同。

    【讨论】:

    • 检查字节码输出确实表明编译器执行了某些优化。它没有做很多许多种优化,而是将boolean表达式的值表示为控制流或堆栈上的值的基本优化(并发出代码以在两者之间进行转换)如果表达式想要产生与上下文想要使用的表示不同的表示,则表示)由字节码编译器完成的。 (是的,这是一种优化;从 Java 到字节码的“最简单可能正确”的翻译不会这样做)。
    • @supercat 我相信这个问题是相关的并且通常支持你的情况:Optimization by java compiler?
    【解决方案5】:

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

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

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

    【讨论】:

    • 我认为这不是测试它的准确方法。反编译器可以对输出进行一些调整,使其更易于阅读。不同的反编译器也可以给出不同的结果。比较实际的字节码本身更有意义。
    【解决方案6】:

    我想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 开发人员只是为这些情况选择了稍微不同的代码生成策略(可能或多或少是出于任意原因)。

    有关更多详细信息,请参阅theseStack Overflowquestions

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

    【讨论】:

      猜你喜欢
      • 2019-09-07
      • 2013-05-17
      • 2013-10-08
      • 1970-01-01
      • 1970-01-01
      • 2013-08-06
      • 1970-01-01
      • 2010-11-22
      • 2011-05-16
      相关资源
      最近更新 更多