Programmation des performances Java, partie 2: Le coût de la diffusion

Pour ce deuxième article de notre série sur les performances Java, l'accent est mis sur le casting - ce que c'est, ce qu'il coûte et comment nous pouvons (parfois) l'éviter. Ce mois-ci, nous commençons par un rapide examen des bases des classes, des objets et des références, puis nous faisons un suivi avec un regard sur quelques chiffres de performance hardcore (dans une barre latérale, pour ne pas offenser les délicats!) Et des directives sur le types d'opérations les plus susceptibles de donner une indigestion à votre machine virtuelle Java (JVM). Enfin, nous terminons par un examen approfondi de la façon dont nous pouvons éviter les effets de structuration de classe courants qui peuvent provoquer un casting.

Programmation des performances Java: Lisez toute la série!

  • Partie 1. Apprenez à réduire la surcharge du programme et à améliorer les performances en contrôlant la création d'objets et le garbage collection
  • Partie 2. Réduisez les frais généraux et les erreurs d'exécution grâce à un code de type sécurisé
  • Partie 3. Découvrez comment les alternatives de collecte mesurent les performances et découvrez comment tirer le meilleur parti de chaque type

Types d'objets et de références en Java

Le mois dernier, nous avons discuté de la distinction de base entre les types primitifs et les objets en Java. Le nombre de types primitifs et les relations entre eux (en particulier les conversions entre les types) sont fixés par la définition du langage. Les objets, en revanche, sont de types illimités et peuvent être liés à un nombre quelconque d'autres types.

Chaque définition de classe dans un programme Java définit un nouveau type d'objet. Cela inclut toutes les classes des bibliothèques Java, donc tout programme donné peut utiliser des centaines, voire des milliers de types d'objets différents. Certains de ces types sont spécifiés par la définition du langage Java comme ayant certaines utilisations ou manipulations spéciales (comme l'utilisation de java.lang.StringBufferpour les java.lang.Stringopérations de concaténation). En dehors de ces quelques exceptions, cependant, tous les types sont traités fondamentalement de la même manière par le compilateur Java et la JVM utilisée pour exécuter le programme.

Si une définition de classe ne spécifie pas (au moyen de la extendsclause dans l'en-tête de définition de classe) une autre classe comme parent ou superclasse, elle étend implicitement la java.lang.Objectclasse. Cela signifie que chaque classe s'étend finalement java.lang.Object, soit directement, soit via une séquence d'un ou plusieurs niveaux de classes parentes.

Les objets eux-mêmes sont toujours des instances de classes et le type d' un objet est la classe dont il est une instance. En Java, cependant, nous ne traitons jamais directement les objets; nous travaillons avec des références aux objets. Par exemple, la ligne:

 java.awt.Component myComponent; 

ne crée pas d' java.awt.Componentobjet; il crée une variable de référence de type java.lang.Component. Même si les références ont des types tout comme les objets, il n'y a pas de correspondance précise entre les types de référence et d'objet - une valeur de référence peut être null, un objet du même type que la référence, ou un objet d'une sous-classe (c.-à-d. from) le type de la référence. Dans ce cas particulier, java.awt.Componentest une classe abstraite, donc nous savons qu'il ne peut jamais y avoir un objet du même type que notre référence, mais il peut certainement y avoir des objets de sous-classes de ce type de référence.

Polymorphisme et moulage

Le type d'une référence détermine la manière dont l' objet référencé - c'est-à-dire l'objet qui est la valeur de la référence - peut être utilisé. Par exemple, dans l'exemple ci-dessus, le code using myComponentpourrait invoquer l'une des méthodes définies par la classe java.awt.Component, ou l'une de ses superclasses, sur l'objet référencé.

Cependant, la méthode réellement exécutée par un appel n'est pas déterminée par le type de la référence elle-même, mais plutôt par le type de l'objet référencé. C'est le principe de base du polymorphisme - les sous-classes peuvent remplacer les méthodes définies dans la classe parente afin d'implémenter un comportement différent. Dans le cas de notre exemple de variable, si l'objet référencé était en fait une instance de java.awt.Button, le changement d'état résultant d'un setLabel("Push Me")appel serait différent de celui résultant si l'objet référencé était une instance de java.awt.Label.

Outre les définitions de classe, les programmes Java utilisent également des définitions d'interface. La différence entre une interface et une classe est qu'une interface ne spécifie qu'un ensemble de comportements (et, dans certains cas, des constantes), tandis qu'une classe définit une implémentation. Puisque les interfaces ne définissent pas les implémentations, les objets ne peuvent jamais être des instances d'une interface. Ils peuvent cependant être des instances de classes qui implémentent une interface. Les références peuvent être de types d'interface, auquel cas les objets référencés peuvent être des instances de n'importe quelle classe qui implémente l'interface (soit directement, soit via une classe ancêtre).

Le casting est utilisé pour convertir entre les types - entre les types de référence en particulier, pour le type d'opération de casting qui nous intéresse ici. Les opérations de conversion ascendante (également appelées conversions élargies dans la spécification du langage Java) convertissent une référence de sous-classe en référence de classe ancêtre. Cette opération de conversion est normalement automatique, car elle est toujours sûre et peut être implémentée directement par le compilateur.

Les opérations de conversion descendante (également appelées conversions restrictives dans la spécification du langage Java) convertissent une référence de classe ancêtre en référence de sous-classe. Cette opération de conversion crée une surcharge d'exécution, car Java nécessite que la conversion soit vérifiée au moment de l'exécution pour s'assurer qu'elle est valide. Si l'objet référencé n'est pas une instance du type cible pour la distribution ou une sous-classe de ce type, la tentative de conversion n'est pas autorisée et doit lancer un java.lang.ClassCastException.

L' instanceofopérateur Java vous permet de déterminer si une opération de transtypage spécifique est autorisée ou non sans tenter réellement l'opération. Étant donné que le coût de performance d'un contrôle est bien inférieur à celui de l'exception générée par une tentative de diffusion non autorisée, il est généralement judicieux d'utiliser un instanceoftest chaque fois que vous n'êtes pas sûr que le type de référence correspond à ce que vous aimeriez qu'elle soit. . Avant de le faire, cependant, vous devez vous assurer que vous disposez d'un moyen raisonnable de traiter une référence d'un type indésirable - sinon, vous pouvez tout aussi bien laisser l'exception être levée et la gérer à un niveau supérieur dans votre code.

Faire attention aux vents

Le casting permet l'utilisation de la programmation générique en Java, où le code est écrit pour fonctionner avec tous les objets des classes descendant d'une classe de base (souvent java.lang.Object, pour les classes utilitaires). Cependant, l'utilisation du moulage pose un ensemble unique de problèmes. Dans la section suivante, nous examinerons l'impact sur les performances, mais examinons d'abord l'effet sur le code lui-même. Voici un exemple utilisant la java.lang.Vectorclasse de collection générique :

Private Vector someNumbers; ... public void doSomething () {... int n = ... Integer number = (Integer) someNumbers.elementAt (n); ...}

Ce code présente des problèmes potentiels en termes de clarté et de maintenabilité. Si quelqu'un d'autre que le développeur d'origine devait modifier le code à un moment donné, il pourrait raisonnablement penser qu'il pourrait ajouter un java.lang.Doubleaux someNumberscollections, car il s'agit d'une sous-classe de java.lang.Number. Tout se compilerait bien s'il essayait cela, mais à un moment indéterminé de l'exécution, il obtiendrait probablement un java.lang.ClassCastExceptionjeté lorsque la tentative de conversion en a java.lang.Integerétait exécutée pour sa valeur ajoutée.

Le problème ici est que l'utilisation du casting contourne les contrôles de sécurité intégrés au compilateur Java; le programmeur finit par rechercher les erreurs lors de l'exécution, car le compilateur ne les détectera pas. Ce n'est pas désastreux en soi, mais ce type d'erreur d'utilisation se cache souvent assez intelligemment pendant que vous testez votre code, uniquement pour se révéler lorsque le programme est mis en production.

Sans surprise, la prise en charge d'une technique permettant au compilateur de détecter ce type d'erreur d'utilisation est l'une des améliorations les plus demandées de Java. Il y a un projet en cours dans le processus de la communauté Java qui étudie l'ajout de ce support: numéro de projet JSR-000014, Ajouter des types génériques au langage de programmation Java (voir la section Ressources ci-dessous pour plus de détails.) Dans la suite de cet article, le mois prochain, nous examinerons ce projet plus en détail et discuterons à la fois de la manière dont il est susceptible d'aider et des domaines dans lesquels il est susceptible de nous laisser en vouloir plus.

Le problème des performances

Il est reconnu depuis longtemps que la diffusion peut être préjudiciable aux performances en Java et que vous pouvez améliorer les performances en minimisant la diffusion dans du code très utilisé. Les appels de méthode, en particulier les appels via des interfaces, sont également souvent mentionnés comme des goulots d'étranglement potentiels en matière de performances. La génération actuelle de JVM a cependant parcouru un long chemin par rapport à ses prédécesseurs, et il vaut la peine de vérifier si ces principes tiennent bien aujourd'hui.

Pour cet article, j'ai développé une série de tests pour voir à quel point ces facteurs sont importants pour les performances des JVM actuels. Les résultats du test sont résumés dans deux tableaux dans la barre latérale, le tableau 1 montrant le surcoût d'appel de méthode et le tableau 2 le surcoût de coulée. Le code source complet du programme de test est également disponible en ligne (voir la section Ressources ci-dessous pour plus de détails).

Pour résumer ces conclusions pour les lecteurs qui ne veulent pas parcourir les détails des tableaux, certains types d'appels de méthodes et de transtypages sont encore assez chers, prenant dans certains cas presque aussi longtemps qu'une simple allocation d'objet. Dans la mesure du possible, ces types d'opérations doivent être évités dans le code qui doit être optimisé pour les performances.

En particulier, les appels aux méthodes surchargées (méthodes qui sont surchargées dans n'importe quelle classe chargée, pas seulement la classe réelle de l'objet) et les appels via les interfaces sont considérablement plus coûteux que les simples appels de méthode. La version bêta de HotSpot Server JVM 2.0 utilisée dans le test convertira même de nombreux appels de méthodes simples en code en ligne, évitant ainsi toute surcharge pour de telles opérations. Cependant, HotSpot affiche les pires performances parmi les JVM testées pour les méthodes remplacées et les appels via les interfaces.

Pour le casting (downcasting, bien sûr), les JVM testés maintiennent généralement les performances à un niveau raisonnable. HotSpot fait un travail exceptionnel avec cela dans la plupart des tests de référence et, comme pour les appels de méthode, est dans de nombreux cas simples capable d'éliminer presque complètement la surcharge de la coulée. Pour les situations plus compliquées, telles que les casts suivis d'appels à des méthodes remplacées, toutes les machines virtuelles Java testées montrent une dégradation notable des performances.

La version testée de HotSpot a également montré des performances extrêmement médiocres lorsqu'un objet était converti en différents types de référence successivement (au lieu d'être toujours casté vers le même type de cible). Cette situation se produit régulièrement dans les bibliothèques telles que Swing qui utilisent une hiérarchie profonde de classes.

In most cases, the overhead of both method calls and casting is small in comparison with the object-allocation times looked at in last month's article. However, these operations will often be used far more frequently than object allocations, so they can still be a significant source of performance problems.

In the remainder of this article, we'll discuss some specific techniques for reducing the need for casting in your code. Specifically, we'll look at how casting often arises from the way subclasses interact with base classes, and explore some techniques for eliminating this type of casting. Next month, in the second part of this look at casting, we'll consider another common cause of casting, the use of generic collections.

Base classes and casting

There are several common uses of casting in Java programs. For instance, casting is often used for the generic handling of some functionality in a base class that may be extended by a number of subclasses. The following code shows a somewhat contrived illustration of this usage:

 // simple base class with subclasses public abstract class BaseWidget { ... } public class SubWidget extends BaseWidget { ... public void doSubWidgetSomething() { ... } } ... // base class with subclasses, using the prior set of classes public abstract class BaseGorph { // the Widget associated with this Gorph private BaseWidget myWidget; ... // set the Widget associated with this Gorph (only allowed for subclasses) protected void setWidget(BaseWidget widget) { myWidget = widget; } // get the Widget associated with this Gorph public BaseWidget getWidget() { return myWidget; } ... // return a Gorph with some relation to this Gorph // this will always be the same type as it's called on, but we can only // return an instance of our base class public abstract BaseGorph otherGorph() { ... } } // Gorph subclass using a Widget subclass public class SubGorph extends BaseGorph { // return a Gorph with some relation to this Gorph public BaseGorph otherGorph() { ... } ... public void anyMethod() { ... // set the Widget we're using SubWidget widget = ... setWidget(widget); ... // use our Widget ((SubWidget)getWidget()).doSubWidgetSomething(); ... // use our otherGorph SubGorph other = (SubGorph) otherGorph(); ... } }