web-dev-qa-db-fra.com

Le compilateur Java optimise-t-il un opérateur ternaire inutile?

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?

25
Bakna

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.

27
yuvgin

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
  1. Expression: 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. Expression: 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 ...;
}
    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

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

9
tryman

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.

6
Pavel Horal

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.

4
supercat

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.

1
Codo

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