J'ai examiné le code où certains codeurs utilisaient des opérateurs ternaires redondants "pour plus de lisibilité". Tel que:
boolean val = (foo == bar && foo1 != bar) ? true : false;
De toute évidence, il serait préférable d'affecter simplement le résultat de l'instruction à la variable boolean
, mais le compilateur s'en soucie-t-il?
Je trouve que l'utilisation inutile de l'opérateur ternaire a tendance à rendre le code plus confus et moins lisible, contrairement à l'intention initiale.
Cela étant dit, le comportement du compilateur à cet égard peut facilement être testé en comparant le bytecode tel que compilé par la JVM.
Voici deux classes simulées pour illustrer cela:
Cas I (sans l'opérateur ternaire):
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);
}
}
Cas II (avec l'opérateur ternaire):
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);
}
}
Bytecode pour la méthode foo () dans le cas 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
Bytecode pour la méthode foo () dans le cas 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
Notez que dans les deux cas, le bytecode est identique, c'est-à-dire que le compilateur ignore l'opérateur ternaire lors de la compilation de la valeur du booléen val
.
MODIFIER:
La conversation concernant cette question a pris plusieurs directions.
Comme indiqué ci-dessus, dans les deux cas (avec ou sans le ternaire redondant) le bytecode compilé Java est identique.
Que cela puisse être considéré comme une optimisation par le Java dépend quelque peu de votre définition de l'optimisation À certains égards, comme cela a été souligné à plusieurs reprises dans d'autres réponses, il est logique de soutenir que non - ce n'est pas une optimisation autant que le fait que dans les deux cas le bytecode généré est l'ensemble le plus simple d'opérations de pile qui effectue cette tâche, quel que soit le ternaire.
Cependant en ce qui concerne la question principale:
De toute évidence, il serait préférable d'affecter simplement le résultat de l'instruction à la variable booléenne, mais le compilateur s'en soucie-t-il?
La réponse simple est non. Le compilateur s'en fiche.
Contrairement aux réponses de Pavel Horal , Codo et yuvgin je soutiens que le compilateur n'optimise PAS loin (ou ignorer) l'opérateur ternaire . (Clarification: je me réfère au Java au compilateur Bytecode, pas au JIT)
Voir les cas de test.
Classe 1: Évaluez l'expression booléenne, stockez-la dans une variable et renvoyez cette variable.
public static boolean testCompiler(final int a, final int b)
{
final boolean c = ...;
return c;
}
Ainsi, pour différentes expressions booléennes, nous inspectons le bytecode: 1. Expression: 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
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
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
Les cas (1) et (2) compilent exactement le même bytecode, non pas parce que le compilateur optimise l'opérateur ternaire, mais parce qu'il doit essentiellement exécuter cet opérateur ternaire trivial à chaque fois. Il doit spécifier au niveau du bytecode s'il doit renvoyer true ou false. Pour vérifier cela, regardez le cas (3). C'est exactement le même bytecode sauf les lignes 5 et 9 qui sont permutées.
Que se passe-t-il alors et a == b ? true : false
décompilé produit a == b
? C'est le choix du décompilateur qui sélectionne le chemin le plus simple.
En outre, sur la base de l'expérience "Classe 1", il est raisonnable de supposer que a == b ? true : false
est exactement le même que a == b
, dans la façon dont il est traduit en bytecode. Cependant, ce n'est pas vrai. Pour tester que nous examinons la "Classe 2" suivante, la seule différence avec la "Classe 1" étant que cela ne stocke pas le résultat booléen dans une variable mais le renvoie immédiatement.
Classe 2: évalue une expression booléenne et retourne le résultat (sans la stocker dans une variable)
public static boolean testCompiler(final int a, final int b)
{
return ...;
}
a == b
Bytecode:
0: iload_0
1: iload_1
2: if_icmpne 7
5: iconst_1
6: ireturn
7: iconst_0
8: ireturn
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
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
Ici, il est évident que le a == b
et a == b ? true : false
les expressions sont compilées différemment , car les cas (1) et (2) produisent des bytecodes différents (cas (2) et (3), comme prévu, ont seulement leurs lignes 5,9 échangées).
Au début, j'ai trouvé cela surprenant, car je m'attendais à ce que les 3 cas soient identiques (à l'exclusion des lignes permutées 5,9 du cas (3)). Lorsque le compilateur rencontre a == b
, il évalue l'expression et revient immédiatement après contrairement à la rencontre de a == b ? true : false
où il utilise goto
pour aller à la ligne ireturn
. Je comprends que cela est fait pour laisser de la place pour les déclarations potentielles à évaluer à l'intérieur du "vrai" cas de l'opérateur ternaire: entre le if_icmpne
check et la ligne goto
. Même si dans ce cas c'est juste un booléen true
, le compilateur le gère comme il le ferait dans le cas général où un bloc plus complexe serait présent.
D'autre part, l'expérience "Classe 1" a occulté ce fait, car dans la branche true
il y avait aussi istore
, iload
et pas seulement ireturn
forçant une commande goto
et donnant exactement le même bytecode dans les cas (1) et (2).
En ce qui concerne l'environnement de test, ces bytecodes ont été produits avec la dernière Eclipse (4.10) qui utilise le compilateur ECJ respectif, différent du javac qu'IntelliJ IDEA utilise.
Cependant, en lisant le bytecode produit par javac dans les autres réponses (qui utilisent IntelliJ), je pense que la même logique s'applique là aussi, au moins pour l'expérience "Classe 1" où la valeur a été stockée et n'est pas retournée immédiatement.
Enfin, comme déjà souligné dans d'autres réponses (telles que celles de supercat et jcsahnwaldt ), à la fois dans ce fil et dans d'autres questions de SO, l'optimisation lourde se fait par du compilateur JIT et non du compilateur Java -> Java-bytecode, donc ces inspections tout en informant la traduction du bytecode ne sont pas une bonne mesure de la façon dont le code optimisé final s'exécutera.
Complément: jcsahnwaldt la réponse compare javac et ECJ's bytecode produit pour des cas similaires
(Comme avertissement, je n'ai pas étudié le Java compilation ou désassemblage autant pour vraiment savoir ce qu'il fait sous le capot; mes conclusions sont principalement basées sur les résultats des expériences ci-dessus.)
Oui, le compilateur Java optimise. Il peut être facilement vérifié:
public class Main1 {
public static boolean test(int foo, int bar, int baz) {
return foo == bar && bar == baz ? true : false;
}
}
Après javac Main1.Java
et 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;
}
}
Après javac Main2.Java
et 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
Les deux exemples se retrouvent avec exactement le même bytecode.
Le compilateur javac n'essaie généralement pas d'optimiser le code avant de sortir le bytecode. Au lieu de cela, il s'appuie sur le compilateur Java machine virtuelle (JVM) et juste à temps (JIT) qui convertit le bytecode en code machine dans des situations où une construction serait équivalente à une construction plus simple .
Cela rend beaucoup plus facile de déterminer si une implémentation d'un compilateur Java fonctionne correctement, car la plupart des constructions ne peuvent être représentées que par une séquence prédéfinie de bytecodes. Si un compilateur produit une autre séquence de bytecode, il est cassé, même si cette séquence se comporterait de la même manière que l'original.
L'examen de la sortie du bytecode du compilateur javac n'est pas un bon moyen de juger si une construction est susceptible de s'exécuter efficacement ou inefficacement. Il semblerait probable qu'il puisse y avoir une implémentation JVM où des constructions comme (someCondition ? true : false)
serait moins performant que (someCondition)
, et certains où ils se produiraient de manière identique.
Dans IntelliJ, j'ai compilé votre code et ouvert le fichier de classe, qui est automatiquement décompilé. Le résultat est:
boolean val = foo == bar && foo1 != bar;
Alors oui, le compilateur Java l'optimise.
Je voudrais synthétiser les excellentes informations données dans les réponses précédentes.
Voyons ce que javac d'Oracle et ecj d'Eclipse font avec le code suivant:
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; }
(J'ai un peu simplifié votre code - une comparaison au lieu de deux - mais le comportement des compilateurs décrits ci-dessous est essentiellement le même, y compris leurs résultats légèrement différents.)
J'ai compilé le code avec javac et ecj puis décompilé avec le javap d'Oracle.
Voici le résultat pour javac (j'ai essayé javac 9.0.4 et 11.0.2 - ils génèrent exactement le même code):
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
Et voici le résultat pour ecj (version 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
Pour cinq des six fonctions, les deux compilateurs génèrent exactement le même code. La seule différence est dans valReturn
: javac génère un goto
vers un ireturn
, mais ecj génère un ireturn
. Pour condReturn
, ils génèrent tous les deux un goto
vers un ireturn
. Pour ifReturn
, ils génèrent tous les deux un ireturn
.
Est-ce à dire que l'un des compilateurs optimise un ou plusieurs de ces cas? On pourrait penser que javac optimise le code ifReturn
, mais ne parvient pas à optimiser valReturn
et condReturn
, tandis que ecj optimise ifReturn
et et valReturn
, mais ne parvient pas à optimiser condReturn
.
Mais je ne pense pas que ce soit vrai. Java n'optimisent pas du tout le code. Le compilateur qui optimise le code est le JIT (juste à temps) le compilateur (la partie de la JVM qui compile le code octet en code machine), et le compilateur JIT peut faire un meilleur travail si le code octet est relativement simple, c'est-à-dire a pas optimisé.
En résumé: non, Java n'optimisent pas ce cas, car ils n'optimisent vraiment rien. Ils font ce que les spécifications leur demandent de faire, mais rien de plus. Le javac et les développeurs ecj ont simplement choisi des stratégies de génération de code légèrement différentes pour ces cas (probablement pour des raisons plus ou moins arbitraires).
Voir cesdébordement de pilequestions pour plus de détails.
(Exemple: les deux compilateurs ignorent de nos jours le drapeau -O
. Les options ecj le disent explicitement: -O: optimize for execution time (ignored)
. Javac ne mentionne même plus le drapeau et l'ignore simplement.)