Un cas pour garder les primitives en Java

Les primitives font partie du langage de programmation Java depuis sa sortie initiale en 1996, et pourtant elles restent l'une des fonctionnalités de langage les plus controversées. John Moore plaide en faveur du maintien des primitives dans le langage Java en comparant de simples benchmarks Java, avec et sans primitives. Il compare ensuite les performances de Java à celles de Scala, C ++ et JavaScript dans un type d'application particulier, où les primitives font une différence notable.

Question : Quels sont les trois facteurs les plus importants dans l'achat d'un bien immobilier?

Réponse : emplacement, emplacement, emplacement.

Cet adage ancien et souvent utilisé est censé impliquer que l'emplacement domine complètement tous les autres facteurs en matière d'immobilier. Dans un argument similaire, les trois facteurs les plus importants à prendre en compte pour l'utilisation de types primitifs en Java sont les performances, les performances et les performances. Il y a deux différences entre l'argument de l'immobilier et l'argument des primitives. Premièrement, avec l'immobilier, la localisation domine dans presque toutes les situations, mais les gains de performance liés à l'utilisation de types primitifs peuvent varier considérablement d'un type d'application à l'autre. Deuxièmement, avec l'immobilier, il y a d'autres facteurs à considérer même s'ils sont généralement mineurs par rapport à l'emplacement. Avec les types primitifs, il n'y a qu'une seule raison de les utiliser: les performances; et seulement si l'application est du genre à pouvoir bénéficier de leur utilisation.

Les primitives offrent peu de valeur à la plupart des applications professionnelles et Internet qui utilisent un modèle de programmation client-serveur avec une base de données sur le backend. Mais les performances des applications dominées par les calculs numériques peuvent grandement bénéficier de l'utilisation de primitives.

L'inclusion de primitives dans Java a été l'une des décisions de conception de langage les plus controversées, comme en témoigne le nombre d'articles et de messages sur le forum liés à cette décision. Simon Ritter a noté dans son discours d'ouverture de JAX Londres en novembre 2011 que la suppression des primitives dans une future version de Java était sérieusement envisagée (voir diapositive 41). Dans cet article, je présenterai brièvement les primitives et le système à double type de Java. En utilisant des exemples de code et des benchmarks simples, je vais expliquer pourquoi les primitives Java sont nécessaires pour certains types d'applications. Je comparerai également les performances de Java à celles de Scala, C ++ et JavaScript.

Mesurer les performances du logiciel

Les performances des logiciels sont généralement mesurées en termes de temps et d'espace. Le temps peut être le temps de fonctionnement réel, tel que 3,7 minutes, ou l'ordre de croissance basé sur la taille de l'entrée, tel que O ( n 2). Des mesures similaires existent pour les performances de l'espace, qui sont souvent exprimées en termes d'utilisation de la mémoire principale, mais peuvent également s'étendre à l'utilisation du disque. L'amélioration des performances implique généralement un compromis espace-temps dans la mesure où les changements pour améliorer le temps ont souvent un effet néfaste sur l'espace, et vice versa. Une mesure de l'ordre de croissance dépend de l'algorithme, et le passage des classes wrapper aux primitives ne changera pas le résultat. Mais en ce qui concerne les performances réelles du temps et de l'espace, l'utilisation de primitives au lieu de classes wrapper offre des améliorations à la fois dans le temps et dans l'espace.

Primitives contre objets

Comme vous le savez probablement déjà si vous lisez cet article, Java dispose d'un système à double type, généralement appelé types primitifs et types d'objet, souvent abrégés simplement en tant que primitifs et objets. Il existe huit types de primitifs prédéfinis en Java, et leurs noms sont des mots-clés réservés. Des exemples couramment utilisés comprennent int, doubleet boolean. Essentiellement, tous les autres types de Java, y compris tous les types définis par l'utilisateur, sont des types d'objet. (Je dis «essentiellement» parce que les types de tableaux sont un peu hybrides, mais ils ressemblent beaucoup plus à des types d'objet qu'à des types primitifs.) Pour chaque type primitif, il existe une classe wrapper correspondante qui est un type d'objet; les exemples incluent Integerpour int, Doublepour doubleet Booleanpour boolean.

Les types primitifs sont basés sur des valeurs, mais les types d'objets sont basés sur des références, et c'est là que réside à la fois la puissance et la source de controverse des types primitifs. Pour illustrer la différence, considérez les deux déclarations ci-dessous. La première déclaration utilise un type primitif et la seconde utilise une classe wrapper.

 int n1 = 100; Integer n2 = new Integer(100); 

En utilisant l'autoboxing, une fonctionnalité ajoutée à JDK 5, je pourrais raccourcir la deuxième déclaration simplement

 Integer n2 = 100; 

mais la sémantique sous-jacente ne change pas. L'autoboxing simplifie l'utilisation des classes wrapper et réduit la quantité de code qu'un programmeur doit écrire, mais cela ne change rien à l'exécution.

La différence entre l'objet primitif n1et l'objet wrapper n2est illustrée par le diagramme de la figure 1.

John I. Moore, Jr.

La variable n1contient une valeur entière, mais la variable n2contient une référence à un objet, et c'est l'objet qui contient la valeur entière. En outre, l'objet référencé par n2contient également une référence à l'objet de classe Double.

Le problème des primitives

Avant d'essayer de vous convaincre de la nécessité des types primitifs, je dois reconnaître que beaucoup de gens ne seront pas d'accord avec moi. Sherman Alpert dans "Les types primitifs considérés comme nuisibles" soutient que les primitives sont nuisibles parce qu'elles mélangent "la sémantique procédurale dans un modèle orienté objet par ailleurs uniforme. Les primitives ne sont pas des objets de première classe, mais elles existent dans un langage qui implique, principalement, des objets de classe. " Les primitives et les objets (sous la forme de classes wrapper) fournissent deux façons de gérer des types logiquement similaires, mais ils ont une sémantique sous-jacente très différente. Par exemple, comment comparer deux instances pour l'égalité? Pour les types primitifs, on utilise l' ==opérateur, mais pour les objets, le choix préféré est d'appeler leequals()méthode, qui n'est pas une option pour les primitives. De même, différentes sémantiques existent lors de l'attribution de valeurs ou de la transmission de paramètres. Même les valeurs par défaut sont différentes; par exemple, 0pour intcontre nullpour Integer.

Pour plus d'informations sur cette question, consultez le billet de blog d'Eric Bruno, «Une discussion primitive moderne», qui résume certains des avantages et des inconvénients des primitives. Un certain nombre de discussions sur Stack Overflow se concentrent également sur les primitives, notamment "Pourquoi les gens utilisent-ils encore des types primitifs en Java?" et "Y a-t-il une raison de toujours utiliser des objets au lieu de primitives?". Les programmeurs Stack Exchange organisent une discussion similaire intitulée "Quand utiliser primitive vs classe en Java?".

Utilisation de la mémoire

A doubleen Java occupe toujours 64 bits en mémoire, mais la taille d'une référence dépend de la machine virtuelle Java (JVM). Mon ordinateur exécute la version 64 bits de Windows 7 et une JVM 64 bits, et donc une référence sur mon ordinateur occupe 64 bits. Sur la base du diagramme de la figure 1 j'attendre un seul doubletel que n1pour occuper 8 octets (64 bits), et je s'attendre à un seul Doubletel que n2pour occuper 24 octets - 8 pour la référence à l'objet, 8 pour la doublevaleur stockée dans l'objet, et 8 pour la référence à l'objet de classe pour Double. De plus, Java utilise de la mémoire supplémentaire pour prendre en charge le garbage collection pour les types d'objets mais pas pour les types primitifs. Regardons ça.

En utilisant une approche similaire à celle de Glen McCluskey dans "Java primitive types vs. wrappers", la méthode illustrée dans le Listing 1 mesure le nombre d'octets occupés par une matrice n-par-n (tableau bidimensionnel) de double.

Listing 1. Calcul de l'utilisation de la mémoire de type double

 public static long getBytesUsingPrimitives(int n) { System.gc(); // force garbage collection long memStart = Runtime.getRuntime().freeMemory(); double[][] a = new double[n][n]; // put some random values in the matrix for (int i = 0; i < n; ++i) { for (int j = 0; j < n; ++j) a[i][j] = Math.random(); } long memEnd = Runtime.getRuntime().freeMemory(); return memStart - memEnd; } 

Modifying the code in Listing 1 with the obvious type changes (not shown), we can also measure the number of bytes occupied by an n-by-n matrix of Double. When I test these two methods on my computer using 1000-by-1000 matrices, I get the results shown in Table 1 below. As illustrated, the version for primitive type double equates to a little more than 8 bytes per entry in the matrix, roughly what I expected. However, the version for object type Double required a little more than 28 bytes per entry in the matrix. Thus, in this case, the memory utilization of Double is more than three times the memory utilization of double, which should not be a surprise to anyone who understands the memory layout illustrated in Figure 1 above.

Table 1. Memory utilization of double versus Double

Version Total bytes Bytes per entry
Using double 8,380,768 8.381
Using Double 28,166,072 28.166

Runtime performance

To compare the runtime performances for primitives and objects, we need an algorithm dominated by numerical calculations. For this article I have chosen matrix multiplication, and I compute the time required to multiply two 1000-by-1000 matrices. I coded matrix multiplication for double in a straightforward manner as shown in Listing 2 below. While there may be faster ways to implement matrix multiplication (perhaps using concurrency), that point is not really relevant to this article. All I need is common code in two similar methods, one using the primitive double and one using the wrapper class Double. The code for multiplying two matrices of type Double is exactly like that in Listing 2 with the obvious type changes.

Listing 2. Multiplying two matrices of type double

 public static double[][] multiply(double[][] a, double[][] b) { if (!checkArgs(a, b)) throw new IllegalArgumentException("Matrices not compatible for multiplication"); int nRows = a.length; int nCols = b[0].length; double[][] result = new double[nRows][nCols]; for (int rowNum = 0; rowNum < nRows; ++rowNum) { for (int colNum = 0; colNum < nCols; ++colNum) { double sum = 0.0; for (int i = 0; i < a[0].length; ++i) sum += a[rowNum][i]*b[i][colNum]; result[rowNum][colNum] = sum; } } return result; } 

I ran the two methods to multiply two 1000-by-1000 matrices on my computer several times and measured the results. The average times are shown in Table 2. Thus, in this case, the runtime performance of double is more than four times as fast as that of Double. That is simply too much of a difference to ignore.

Table 2. Runtime performance of double versus Double

Version Seconds
Using double 11.31
Using Double 48.48

The SciMark 2.0 benchmark

Jusqu'à présent, j'ai utilisé le benchmark unique et simple de la multiplication matricielle pour démontrer que les primitives peuvent produire des performances de calcul nettement supérieures à celles des objets. Pour renforcer mes affirmations, j'utiliserai une référence plus scientifique. SciMark 2.0 est une référence Java pour le calcul scientifique et numérique disponible auprès du National Institute of Standards and Technology (NIST). J'ai téléchargé le code source de ce benchmark et créé deux versions, la version originale utilisant des primitives et une deuxième version utilisant des classes wrapper. Pour la deuxième version, j'ai remplacé intpar Integeret doublepar Doublepour obtenir le plein effet de l'utilisation des classes wrapper. Les deux versions sont disponibles dans le code source de cet article.

télécharger Benchmarking Java: Téléchargez le code source John I. Moore, Jr.

The SciMark benchmark measures performance of several computational routines and reports a composite score in approximate Mflops (millions of floating point operations per second). Thus, larger numbers are better for this benchmark. Table 3 gives the average composite scores from several runs of each version of this benchmark on my computer. As shown, the runtime performances of the two versions of the SciMark 2.0 benchmark were consistent with the matrix multiplication results above in that the version with primitives was almost five times faster than the version using wrapper classes.

Table 3. Runtime performance of the SciMark benchmark

SciMark version Performance (Mflops)
Using primitives 710.80
Using wrapper classes 143.73

You've seen a few variations of Java programs doing numerical calculations, using both a homegrown benchmark and a more scientific one. But how does Java compare to other languages? I'll conclude with a quick look at how Java's performance compares to that of three other programming languages: Scala, C++, and JavaScript.

Benchmarking Scala

Scala est un langage de programmation qui s'exécute sur la JVM et semble gagner en popularité. Scala a un système de type unifié, ce qui signifie qu'il ne fait pas la distinction entre les primitives et les objets. Selon Erik Osheim dans la classe de type numérique de Scala (Pt. 1), Scala utilise des types primitifs lorsque cela est possible mais utilisera des objets si nécessaire. De même, la description de Martin Odersky des tableaux de Scala dit que "... un tableau Scala Array[Int]est représenté comme un Java int[], an Array[Double]est représenté comme un Java double[]..."

Cela signifie-t-il que le système de type unifié de Scala aura des performances d'exécution comparables aux types primitifs de Java? Voyons voir.