Оптимизирует ли компилятор 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
- Выражение:
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
- Выражение:
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 ...;
}
Bytecode:
0: iload_0
1: iload_1
2: if_icmpne 7
5: iconst_1
6: ireturn
7: iconst_0
8: ireturn
Bytecode
0: iload_0
1: iload_1
2: if_icmpne 9
5: iconst_1
6: goto 10
9: iconst_0
10: ireturn
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 даже не упоминает флаг и просто игнорирует его.)