一部のコーダーが「読みやすさのために」冗長な3項演算子を使用しているコードを確認しました。といった:
boolean val = (foo == bar && foo1 != bar) ? true : false;
当然のことながら、ステートメントの結果をboolean
変数に割り当てるだけの方が適切ですが、コンパイラーは気にしますか?
三元演算子を不必要に使用すると、元の意図とは逆に、コードがわかりにくくなり、読みにくくなるになる傾向があります。
そうは言っても、JVMでコンパイルされたバイトコードを比較することで、この点に関するコンパイラの動作を簡単にテストできます。
これを説明する2つのモッククラスを次に示します。
ケースI(3項演算子なし):
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);
}
}
ケース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
ブール値のコンパイル時に三項演算子を無視します。
編集:
この質問に関する会話は、いくつかの方向性の1つになっています。
上記のように、両方の場合(冗長な3値の有無にかかわらず)コンパイル済みJavaバイトコードは同一。
Javaコンパイラによってこれが最適化)と見なされるかどうかは、最適化の定義に多少依存します。いくつかの点で、他の回答で複数回指摘されているように、そうではないと主張することは理にかなっています-どちらの場合も生成されたバイトコードは、三項に関係なく、このタスクを実行します。
ただし、主な質問について:
明らかに、ステートメントの結果をブール変数に代入する方が良いでしょうが、コンパイラは気にしますか?
単純な答えはノーです。コンパイラーは関係ありません。
Pavel Horal 、 Codo および yuvgin の回答に反して、コンパイラーは最適化されないことを主張します(または無視)三項演算子。 (明確化: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
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
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項演算子を最適化するためではなく、基本的にその3項演算子を毎回実行する必要があるため、まったく同じバイトコードにコンパイルされます。 trueまたはfalseを返すかどうかをバイトコードレベルで指定する必要があります。これを確認するには、ケース(3)を見てください。スワップされる行5と9を除いて、まったく同じバイトコードです。
次に何が起こり、逆コンパイルするとa == b ? true : false
はa == b
を生成しますか?最も簡単なパスを選択するのは、逆コンパイラの選択です。
さらに、「クラス1」の実験に基づいて、a == b ? true : false
はバイトコードに変換される点でa == b
とまったく同じであると想定するのが妥当です。しかし、これは真実ではありません。次の「クラス2」を調べることをテストするために、「クラス1」との唯一の違いは、ブール結果を変数に格納せず、すぐにそれを返すことです。
クラス2:ブール式を評価し、結果を返します(変数に格納せずに)
public static boolean testCompiler(final int a, final int b)
{
return ...;
}
a == b
バイトコード:
0: iload_0
1: iload_1
2: if_icmpne 7
5: iconst_1
6: ireturn
7: iconst_0
8: ireturn
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
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
を検出すると、式を評価し、goto
を使用して行ireturn
に移動するa == b ? true : false
の検出とは逆に、すぐに戻ります。これは、三項演算子の「true」の場合、if_icmpne
チェックとgoto
行の間に評価される可能性のあるステートメントのスペースを空けるために行われることを理解しています。この場合、それが単なるブールtrue
であっても、コンパイラは、より複雑なブロックが存在する一般的な場合と同様に処理します。
一方、「クラス1」の実験では、true
ブランチと同様に、istore
、iload
もあり、ireturn
goto
コマンドを強制し、ケース(1)と(2)でまったく同じバイトコードを生成します。
テスト環境に関するメモとして、これらのバイトコードは、IntelliJ IDEAが使用するjavacとは異なり、それぞれのECJコンパイラを使用する最新のEclipse(4.10)で作成されました。
ただし、javacで生成されたバイトコードを他の回答(IntelliJを使用)で読み取ると、少なくとも同じ値が格納され、すぐには返されない「クラス1」の実験にも同じロジックが適用されると思います。
最後に、他の回答( supercat や jcsahnwaldt による回答など)ですでに指摘したように、このスレッドとSOの他の質問の両方で、重い最適化は次のように行われます。 Java-> Java-bytecodeコンパイラではなく、JITコンパイラ。したがって、これらの検査は、バイトコード変換に役立つ一方で、最終的に最適化されたコードがどのように実行されるかを適切に測定するものではありません。
補足: jcsahnwaldt の答えは、javacとECJが生成したバイトコードを同様のケースで比較します
(免責事項として、私はJavaコンパイルまたは逆アセンブリを実際に実行するためにそれほど詳しく調べていません。私の結論は主に上記の実験の結果に基づいています。)
はい、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
どちらの例も、まったく同じバイトコードになります。
Javacコンパイラは通常、バイトコードを出力する前にコードを最適化しようとしません。代わりに、Java仮想マシン(JVM)とジャストインタイム(JIT)コンパイラを使用して、バイトコードをマシンコードに変換し、構造がより単純なものと同等になるようにします。 。
これにより、Javaコンパイラの実装が正しく機能しているかどうかの判断がはるかに簡単になります。ほとんどの構成要素は、定義済みの1つのバイトコードシーケンスでしか表現できないためです。コンパイラが他のバイトコードシーケンスを生成する場合、それは壊れていますそのシーケンスが元のシーケンスと同じように動作する場合でも。
Javacコンパイラのバイトコード出力を調べることは、構造が効率的に実行されるか非効率的に実行されるかを判断する良い方法ではありません。 (someCondition ? true : false)
のような構造が(someCondition)
よりもパフォーマンスが悪いJVM実装と、同じように動作するJVM実装がある可能性があります。
IntelliJでは、コードをコンパイルし、自動的に逆コンパイルされるクラスファイルを開きました。結果は次のとおりです。
boolean val = foo == bar && foo1 != bar;
つまり、Javaコンパイラが最適化します。
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; }
_
(コードを少し簡略化しました-2つではなく1つの比較ですが、以下で説明するコンパイラーの動作は、わずかに異なる結果を含めて、基本的に同じです。)
私は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
_
6つの関数のうち5つについては、両方のコンパイラがまったく同じコードを生成します。 唯一の違い はvalReturn
にあります。javacはgoto
をireturn
に生成しますが、ecjはireturn
を生成します。 condReturn
の場合、どちらもgoto
をireturn
に生成します。 ifReturn
の場合、どちらもireturn
を生成します。
それは、コンパイラの1つがこれらのケースの1つ以上を最適化することを意味しますか? javacはifReturn
コードを最適化するとは思うかもしれませんが、valReturn
とcondReturn
を最適化できませんが、ecjはifReturn
とvalReturn
を最適化します、ただしcondReturn
を最適化できません。
しかし、私はそれが本当だとは思いません。 Javaソースコードコンパイラは基本的にコードを最適化しません。コードを最適化するコンパイラが行うコードはJITです(ジャストインタイム)コンパイラ(バイトコードをマシンコードにコンパイルするJVMの部分)、およびバイトコードが比較的単純な最適化されていません。
一言で言えば、いいえ、Javaソースコードコンパイラは、実際には何も最適化しないため、このケースを最適化しません。仕様で要求されていることを実行しますが、それ以上は実行しません。javacそして、ecj開発者は、これらのケースに対して(おそらく多かれ少なかれ恣意的な理由で)わずかに異なるコード生成戦略を選択しました。
詳細については、 theseStack Overflowquestions を参照してください。
(適例:両方のコンパイラーは現在、_-O
_フラグを無視しています。ecjオプションは明示的にそのように言っています:-O: optimize for execution time (ignored)
。javacはフラグに言及しなくなっただけで、無視します。)