Effet de traînée de performance des assertions Java lorsqu’il est désactivé

Le code peut être compilé avec des assertions et peut être activé / désactivé en cas de besoin .

Mais si je déploie une application contenant des assertions et que celles-ci sont désactivées, quelle est la peine encourue si therm est présent et ignoré?

Contrairement aux idées reçues, les affirmations ont un impact sur les performances d’exécution. Cet impact sera probablement faible dans la plupart des cas, mais pourrait être important dans certaines circonstances. Certains des mécanismes par lesquels les assertions ralentissent au moment de l’exécution sont assez “lisses” et prévisibles (et généralement modestes), mais la dernière méthode décrite ci-dessous (échec de la mise en ligne) est délicate car c’est le problème potentiel le plus important (vous pourriez avoir une régression par ordre de grandeur) et ce n’est pas lisse 1 .

Une parsing

Assert implémentation

En ce qui concerne l’parsing de la fonctionnalité d’ assert en Java, il est intéressant de noter qu’elle n’a rien de magique au niveau du bytecode / JVM. Autrement dit, ils sont implémentés dans le fichier .class en utilisant la mécanique Java standard au moment de la compilation (fichier .java), et ils ne reçoivent aucun traitement spécial de la part de la JVM, mais s’appuient sur les optimisations habituelles code.

Jetons un coup d’œil à la manière dont ils sont implémentés sur un JDK Oracle 8 moderne (mais d’après ce que j’en sais, cela n’a pas changé depuis toujours).

Prenez la méthode suivante avec une seule affirmation:

 public int addAssert(int x, int y) { assert x > 0 && y > 0; return x + y; } 

… comstackr cette méthode et décomstackr le bytecode avec javap -c foo.bar.Main :

  public int addAssert(int, int); Code: 0: getstatic #17 // Field $assertionsDisabled:Z 3: ifne 22 6: iload_1 7: ifle 14 10: iload_2 11: ifgt 22 14: new #39 // class java/lang/AssertionError 17: dup 18: invokespecial #41 // Method java/lang/AssertionError."":()V 21: athrow 22: iload_1 23: iload_2 24: iadd 25: ireturn 

Les 22 premiers octets de bytecode sont tous associés à l’assert. D’emblée, il vérifie le champ statique $assertionsDisabled masqué et saute sur toute la logique d’ $assertionsDisabled si elle est vraie. Sinon, il effectue simplement les deux contrôles de la manière habituelle et construit et lève un object AssertionError() cas d’échec.

Il n’y a donc rien de particulier dans le support d’assert au niveau du bytecode – la seule astuce est le champ $assertionsDisabled qui, en utilisant la même sortie javap est une static final initialisée à l’heure de la classe:

  static final boolean $assertionsDisabled; static {}; Code: 0: ldc #1 // class foo/Scrap 2: invokevirtual #11 // Method java/lang/Class.desiredAssertionStatus:()Z 5: ifne 12 8: iconst_1 9: goto 13 12: iconst_0 13: putstatic #17 // Field $assertionsDisabled:Z 

Ainsi, le compilateur a créé ce static final caché et le charge en fonction de la desiredAssertionStatus() public desiredAssertionStatus() .

Donc, rien de magique. En fait, essayons de faire la même chose nous-mêmes, avec notre propre champ statique SKIP_CHECKS que nous chargeons en fonction d’une propriété système:

 public static final boolean SKIP_CHECKS = Boolean.getBoolean("skip.checks"); public int addHomebrew(int x, int y) { if (!SKIP_CHECKS) { if (!(x > 0 && y > 0)) { throw new AssertionError(); } } return x + y; } 

Ici, nous écrivons simplement à la main ce que fait l’assertion (nous pourrions même combiner les déclarations if, mais nous essaierons de correspondre à l’assertion aussi étroitement que possible). Vérifions le résultat:

  public int addHomebrew(int, int); Code: 0: getstatic #18 // Field SKIP_CHECKS:Z 3: ifne 22 6: iload_1 7: ifle 14 10: iload_2 11: ifgt 22 14: new #33 // class java/lang/AssertionError 17: dup 18: invokespecial #35 // Method java/lang/AssertionError."":()V 21: athrow 22: iload_1 23: iload_2 24: iadd 25: ireturn 

Hein, c’est à peu près octet par octet identique à la version assert.

Affirmer les coûts

Nous pouvons donc assez bien réduire la question “combien coûte une assertion” à “combien coûte un code sauté par une twig toujours prise basée sur une condition static final ?”. La bonne nouvelle est que ces twigs sont généralement complètement optimisées par le compilateur C2, si la méthode est compilée. Bien sûr, même dans ce cas, vous payez toujours des coûts:

  1. Les fichiers de classe sont plus volumineux et il y a plus de code pour JIT.
  2. Avant JIT, la version interprétée sera probablement plus lente.
  3. La taille totale de la fonction est utilisée dans les décisions d’inclusion, de sorte que la présence des assertions affecte cette décision même si elle est désactivée .

Les points (1) et (2) résultent directement de la suppression de l’assertion lors de la compilation à l’exécution (JIT), plutôt qu’au moment de la compilation du fichier java. C’est une différence essentielle avec les assertions C et C ++ (mais en échange, vous devez décider d’utiliser des assertions à chaque lancement du fichier binary, plutôt que de les comstackr dans cette décision).

Le point (3) est probablement le plus critique, rarement mentionné et difficile à parsingr. L’idée de base est que le JIT utilise deux seuils de taille lors de la prise de décisions d’inclinaison – un petit seuil (environ 30 octets) dans lequel il est presque toujours aligné, et un autre seuil plus important (environ 300 octets) sur lequel il n’est jamais intégré. Entre les seuils, si elle est en ligne ou non, cela dépend de si la méthode est active ou non, et d’autres méthodes heuristiques, telles que le fait de savoir si elle a déjà été insérée ailleurs.

Étant donné que les seuils sont basés sur la taille du bytecode, l’utilisation d’assertions peut affecter considérablement ces décisions – dans l’exemple ci-dessus, 22 des 26 octets de la fonction étaient liés à l’assertion. Surtout lorsque vous utilisez beaucoup de petites méthodes, il est facile pour les assertions de pousser une méthode au-dessus des seuils en ligne. Maintenant, les seuils ne sont que des heuristiques, il est donc possible que le passage d’une méthode inline à non-line améliore les performances dans certains cas – mais en général, vous souhaitez plus que moins d’inlining car il s’agit d’une optimisation grandiose. ça arrive.

Atténuation

Une approche pour contourner ce problème consiste à déplacer la majeure partie de la logique d’affirmation vers une fonction spéciale, comme suit:

 public int addAssertOutOfLine(int x, int y) { assertInRange(x,y); return x + y; } private static void assertInRange(int x, int y) { assert x > 0 && y > 0; } 

Cela comstack pour:

  public int addAssertOutOfLine(int, int); Code: 0: iload_1 1: iload_2 2: invokestatic #46 // Method assertInRange:(II)V 5: iload_1 6: iload_2 7: iadd 8: ireturn 

… et a donc réduit la taille de cette fonction de 26 à 9 octets, dont 5 sont liés à l’assertion. Bien sûr, le bytecode manquant vient de passer à l’autre fonction, mais c’est bien parce qu’il sera considéré séparément dans les décisions en ligne et que JIT-comstack en mode non-op lorsque les assertions sont désactivées.

True Comstack-Time Asserts

Enfin, il est intéressant de noter que vous pouvez obtenir si vous le souhaitez une compilation de type C / C ++. Ce sont des assertions dont le statut on / off est statiquement compilé dans le binary (au moment de javac ). Si vous souhaitez activer les assertions, vous avez besoin d’un nouveau fichier binary. Par contre, ce type d’affirmation est vraiment gratuit au moment de l’exécution.

Si nous modifions la version static final de l’homebrew SKIP_CHECKS pour qu’elle soit connue au moment de la compilation, comme suit:

 public static final boolean SKIP_CHECKS = true; 

puis addHomebrew comstack jusqu’à:

  public int addHomebrew(int, int); Code: 0: iload_1 1: iload_2 2: iadd 3: ireturn 

C’est-à-dire qu’il ne rest aucune trace de l’assertion. Dans ce cas, nous pouvons vraiment dire qu’il n’y a aucun coût d’exécution. Vous pouvez rendre cela plus pratique dans un projet en ayant une seule classe StaticAssert qui SKIP_CHECKS variable SKIP_CHECKS Vous pouvez également utiliser ce sucre d’ SKIP_CHECKS existant pour créer une version à 1 ligne:

 public int addHomebrew2(int x, int y) { assert SKIP_CHECKS || (x > 0 && y > 0); return x + y; } 

Encore une fois, cela comstack au moment de javac en bytecode sans trace de l’affirmation. Vous allez cependant avoir à faire face à un avertissement de l’IDE sur le code mort (au moins dans Eclipse).


1 Par ceci, je veux dire que cette question peut avoir un effet nul, puis qu’après un petit changement anodin du code environnant, cela peut soudainement avoir un effet considérable. Fondamentalement, les différents niveaux de pénalité sont fortement quantifiés en raison de l’effet binary des décisions “inline ou inline”.

2 Au moins pour l’essentiel de la compilation / exécution du code relatif aux assertions au moment de l’exécution. Bien entendu, la JVM prend en charge une petite partie de l’acceptation de l’argument de ligne de commande -ea et du statut d’assertion par défaut (mais comme ci-dessus, vous pouvez obtenir le même effet générique avec les propriétés).

Très très peu. Je crois qu’ils sont supprimés pendant le chargement de la classe.

La chose la plus proche de ma preuve est: La spécification d’affirmation assert dans la spécification Langauge Java. Il semble être libellé de manière à ce que les instructions assert puissent être traitées au moment du chargement de la classe.

La désactivation des assertions élimine entièrement leur perte de performances. Une fois désactivés, ils sont essentiellement équivalents aux instructions vides en sémantique et en performance.

La source