Оптимизирует ли компилятор Java ненужный троичный оператор?

Я просматривал код, в котором некоторые кодеры использовали избыточные троичные операторы "для удобства чтения". Такие как:

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

Очевидно, что было бы лучше просто присвоить результат операторов boolean переменной, но заботится ли компилятор?

Ответ 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);
    }
}

Случай 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);
    }
}

Байт-код для метода foo() в случае I:

       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() в случае II:

       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 boolean.


РЕДАКТИРОВАТЬ:

Разговор по этому вопросу прошел в одном из нескольких направлений.
Как показано выше, в обоих случаях (с или без ненужного троичного) скомпилированный байт-код Java идентичен.
Может ли это рассматриваться оптимизацией компилятором Java, зависит в некоторой степени от вашего определения оптимизации. В некоторых отношениях, как указывалось несколько раз в других ответах, имеет смысл утверждать, что нет - это не столько оптимизация, сколько факт, что в обоих случаях сгенерированный байт-код является простейшим набором операций стека, который выполняет это задание независимо от троичного.

Однако относительно основного вопроса:

Очевидно, что было бы лучше просто присвоить результат операторов булевой переменной, но заботится ли компилятор?

Простой ответ - нет. Компилятору все равно.

Ответ 2

Вопреки ответам Павла Хорала, Кодо и Ювгина, я утверждаю, что компилятор НЕ оптимизирует (или игнорирует) троичный оператор. (Пояснение: я имею в виду компилятор Java to Bytecode, а не JIT)

Смотрите тестовые случаи.

Класс 1. Оцените логическое выражение, сохраните его в переменной и верните эту переменную.

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

Итак, для различных логических выражений мы проверяем байт-код: 1. Выражение: a == b

Bytecode

   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 a == b? true: false

Bytecode

   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 a == b? false: true

Bytecode

   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? true: false при декомпиляции выдает a == b? Это выбор декомпилятора, который выбирает самый простой путь.

Кроме того, исходя из эксперимента "Класс 1", разумно предположить, что a == b? true: false a == b? true: false точно так же, как a == b, в том смысле, как оно переводится в байт-код. Однако это не так. Чтобы проверить, что мы исследуем следующий "класс 2", единственное отличие от "класса 1" состоит в том, что он не сохраняет логический результат в переменной, а сразу же возвращает его.

Класс 2. Оцените логическое выражение и верните результат (не сохраняя его в переменной).

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

Bytecode:

   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

Bytecode

   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

Bytecode

   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 a == b? true: false выражения компилируются по-разному, так как case (1) и (2) генерируют разные байт-коды (в случаях (2) и (3), как и ожидалось, только их строки 5,9 поменялись местами).

Сначала я обнаружил, что это удивительно, так как я ожидал, что все 3 случая будут одинаковыми (исключая измененные строки 5,9 случая (3)). Когда компилятор встречает a == b, он вычисляет выражение и возвращает его сразу после того, как это противоречит a == b? true: false a == b? true: false где используется goto для перехода к строке ireturn. Я понимаю, что это сделано для того, чтобы оставить место для потенциальных операторов, которые будут оцениваться в "истинном" случае троичного оператора: между проверкой if_icmpne и строкой goto. Даже если в этом случае это просто логическое значение true, компилятор обрабатывает его так же, как и в общем случае, когда будет присутствовать более сложный блок.
С другой стороны, эксперимент "Класс 1" скрыл этот факт, так как в true ветке были также istore, iload и не только ireturn что ireturn команду goto и приводило к абсолютно одинаковому байт-коду в случаях (1) и (2),

Как примечание относительно тестовой среды, эти байт-коды были созданы с использованием последней версии Eclipse (4.10), в которой используется соответствующий компилятор ECJ, в отличие от javac, который использует IntelliJ IDEA.

Однако, читая полученный в javac байт-код в других ответах (которые используют IntelliJ), я полагаю, что та же логика применима и там, по крайней мере, для эксперимента "Класс 1", где значение было сохранено и не возвращено немедленно.

Наконец, как уже указывалось в других ответах, как в этом потоке, так и в других вопросах SO, интенсивная оптимизация выполняется компилятором JIT, а не компилятором java--> java-bytecode, поэтому эти проверки являются информативными для преобразование байт-кода не является хорошей мерой того, как будет выполняться окончательный оптимизированный код.

Дополнение: ответ jcsahnwaldt сравнивает байт-код, созданный javac и ECJ, для похожих случаев

(Как выражение об отказе от ответственности, я не изучал компиляцию или дизассемблирование Java так много, чтобы действительно знать, что он делает под капотом; мои выводы в основном основаны на результатах вышеупомянутых экспериментов.)

Ответ 3

Да, компилятор Java оптимизирует. Это можно легко проверить:

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

После javac Main1.java и javap -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.java и javap -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) и компилятор Just-in-Time (JIT), который преобразует байт-код в машинный код в ситуациях, когда конструкция будет эквивалентна более простой.

Это значительно упрощает определение правильности работы реализации компилятора Java, поскольку большинство конструкций может быть представлено только одной предопределенной последовательностью байт-кодов. Если компилятор создает любую другую последовательность байт-кода, она нарушается, даже если эта последовательность будет вести себя так же, как и оригинал.

Изучение выходных данных байт-кода компилятора javac не является хорошим способом определить, будет ли конструкция выполняться эффективно или неэффективно. Может показаться вероятным, что может быть какая-то реализация JVM, где конструкции вроде (someCondition? true: false) будут работать хуже, чем (someCondition), а некоторые - там, где они будут работать одинаково.

Ответ 5

Я только что попробовал то, что вы сказали, с помощью Блокнота и Visual Studio Code, и ни один из них, похоже, не заботился. И действительно, было бы лучше присвоить результат оператора логическому значению.

Ответ 6

В IntelliJ я скомпилировал ваш код и открыл файл класса, который автоматически декомпилируется. Результат:

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

Так что да, компилятор Java оптимизирует его.

Ответ 7

Я хотел бы обобщить отличную информацию, приведенную в предыдущих ответах.

Давайте посмотрим, что 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

Для пяти из шести функций оба компилятора генерируют один и тот же код. Единственное различие в valReturn: Javac генерирует goto к ireturn, но СЕС генерирует ireturn. Для condReturn, оба они генерируют goto к ireturn. Для ifReturn они оба генерируют ireturn.

Означает ли это, что один из компиляторов оптимизирует один или несколько таких случаев? Можно подумать, что javac оптимизирует код ifReturn, но не оптимизирует valReturn и condReturn, а ecj оптимизирует ifReturn и и valReturn, но не оптимизирует condReturn.

Но я не думаю, что это правда. Компиляторы исходного кода Java в основном вообще не оптимизируют код. Компилятором, который оптимизирует код, является JIT-компилятор (точно по времени) (часть JVM, которая компилирует байт-код в машинный код), и JIT-компилятор может лучше выполнять работу, если байт-код относительно прост, т.е. не был оптимизирован.

В двух словах: нет, компиляторы исходного кода Java не оптимизируют этот случай, потому что они на самом деле ничего не оптимизируют. Они делают то, что от них требуют спецификации, но не более того. Разработчики javac и ecj просто выбрали несколько разные стратегии генерации кода для этих случаев (предположительно по более или менее произвольным причинам).

Посмотрите эти вопросы переполнения стека для более подробной информации.

(-O пример: оба компилятора в настоящее время игнорируют флаг -O. Опции ecj прямо говорят об этом: -O: optimize for execution time (ignored). Javac даже не упоминает флаг и просто игнорирует его.)