En java, est-il plus efficace d'utiliser byte ou short au lieu de int et float au lieu de double?


91

J'ai remarqué que j'ai toujours utilisé int et double, peu importe la taille du nombre. Donc, en Java, est-il plus efficace d'utiliser byteou à la shortplace de intet à la floatplace de double?

Supposons donc que j'ai un programme avec beaucoup d'entiers et de doubles. Cela vaudrait-il la peine de passer en revue et de changer mes entiers en octets ou en short si je savais que le nombre conviendrait?

Je sais que java n'a pas de types non signés, mais y a-t-il quelque chose de plus que je pourrais faire si je savais que le nombre serait uniquement positif?

Par efficace, j'entends principalement le traitement. Je suppose que le ramasse-miettes serait beaucoup plus rapide si toutes les variables avaient la moitié de la taille et que les calculs seraient probablement un peu plus rapides aussi. (Je suppose que depuis que je travaille sur Android, je dois aussi m'inquiéter pour la RAM)

(Je suppose que le garbage collector ne traite que des objets et non des primitifs, mais supprime toujours toutes les primitives des objets abandonnés, n'est-ce pas?)

Je l'ai essayé avec une petite application Android que j'ai mais je n'ai pas vraiment remarqué de différence. (Bien que je n'ai rien mesuré "scientifiquement".)

Ai-je tort de supposer que cela devrait être plus rapide et plus efficace? Je détesterais tout changer dans un programme massif pour découvrir que j'ai perdu mon temps.

Cela vaudrait-il la peine de le faire dès le début lorsque je démarre un nouveau projet? (Je veux dire, je pense que chaque petit geste aiderait, mais encore une fois, si c'est le cas, pourquoi ne semble-t-il pas que quelqu'un le fasse.)

Réponses:


107

Ai-je tort de supposer que cela devrait être plus rapide et plus efficace? Je détesterais tout changer dans un programme massif pour découvrir que j'ai perdu mon temps.

Réponse courte

Oui, vous vous trompez. Dans la plupart des cas, cela fait peu de différence en termes d'espace utilisé.

Cela ne vaut pas la peine d' essayer d'optimiser cela ... sauf si vous avez des preuves claires qu'une optimisation est nécessaire. Et si vous avez besoin d'optimiser l'utilisation de la mémoire des champs d'objets en particulier, vous devrez probablement prendre d'autres mesures (plus efficaces).

Réponse plus longue

La machine virtuelle Java modélise les piles et les champs d'objet à l'aide de décalages qui sont (en fait) des multiples d'une taille de cellule primitive de 32 bits. Ainsi, lorsque vous déclarez une variable locale ou un champ objet comme (disons) a byte, la variable / champ sera stocké dans une cellule de 32 bits, tout comme un int.

Il y a deux exceptions à cela:

  • longet les doublevaleurs nécessitent 2 cellules 32 bits primitives
  • les tableaux de types primitifs sont représentés sous forme condensée, de sorte que (par exemple) un tableau d'octets contienne 4 octets par mot de 32 bits.

Il pourrait donc être intéressant d'optimiser l'utilisation de longet double... et de grands tableaux de primitives. Mais en général non.

En théorie, un JIT pourrait être en mesure d'optimiser cela, mais en pratique, je n'ai jamais entendu parler d'un JIT qui le fasse. Un obstacle est que le JIT ne peut généralement pas s'exécuter tant que les instances de la classe en cours de compilation n'ont pas été créées. Si le JIT optimisait la disposition de la mémoire, vous pourriez avoir deux (ou plus) "saveurs" d'objets de la même classe ... et cela présenterait d'énormes difficultés.


Revisitation

En regardant les résultats de référence dans la réponse de @ meriton, il semble que l'utilisation de shortet byteau lieu de intentraîne une pénalité de performance pour la multiplication. En effet, si vous considérez les opérations isolément, la pénalité est importante. (Vous ne devriez pas les considérer isolément ... mais c'est un autre sujet.)

Je pense que l'explication est que JIT fait probablement les multiplications en utilisant des instructions de multiplication 32 bits dans chaque cas. Mais dans le cas byteet short, il exécute des instructions supplémentaires pour convertir la valeur intermédiaire de 32 bits en une byteou shortà chaque itération de boucle. (En théorie, cette conversion pourrait être effectuée une fois à la fin de la boucle ... mais je doute que l'optimiseur soit capable de le comprendre.)

Quoi qu'il en soit, cela indique un autre problème avec le passage à shortet byteen tant qu'optimisation. Il pourrait faire des performances pire ... dans un algorithme qui est arithmétique et calcul intensif.


30
+1 n'optimise pas à moins d'avoir des preuves claires d'un problème de performances
Bohème

Euh, pourquoi la JVM doit-elle attendre la compilation JIT pour emballer la disposition de la mémoire d'une classe? Étant donné que les types de champs sont écrits dans le fichier de classe, la machine virtuelle Java ne pourrait-elle pas choisir une disposition de mémoire au moment du chargement de la classe, puis résoudre les noms de champ comme des octets plutôt que des décalages de mots?
meriton

@meriton - Je suis à peu près sûr que les dispositions des objets sont déterminées au moment du chargement de la classe, et elles ne changent pas après cela. Voir la partie «petits caractères» de ma réponse. Si la disposition de la mémoire réelle changeait lorsque le code était JIT, ce serait vraiment difficile à gérer pour la JVM. (Quand j'ai dit que le JIT pourrait optimiser la mise en page, c'est hypothétique et peu pratique ... ce qui pourrait expliquer pourquoi je n'ai jamais entendu parler d'un JIT le faisant réellement.)
Stephen C

Je connais. J'essayais juste de souligner que même si les dispositions de la mémoire sont difficiles à changer une fois que les objets sont créés, une JVM peut encore optimiser la disposition de la mémoire avant cela, c'est-à-dire au moment du chargement de la classe. En d'autres termes, le fait que la spécification JVM décrit le comportement d'une JVM avec des décalages de mots n'implique pas nécessairement qu'une JVM doit être implémentée de cette façon - bien que le soit très probablement.
meriton

@meriton - La spécification JVM parle de "décalages de mots de machine virtuelle" dans des cadres / objets locaux. La façon dont ils sont mappés aux décalages de machine physique n'est PAS spécifiée. En effet, il ne peut pas le spécifier ... car il peut y avoir des exigences d'alignement de champ spécifiques au matériel.
Stephen C

29

Cela dépend de l'implémentation de la JVM, ainsi que du matériel sous-jacent. La plupart du matériel moderne n'extraira pas un octet de la mémoire (ou même du cache de premier niveau), c'est-à-dire que l'utilisation de types primitifs plus petits ne réduit généralement pas la consommation de bande passante mémoire. De même, les processeurs modernes ont une taille de mot de 64 bits. Ils peuvent effectuer des opérations sur moins de bits, mais cela fonctionne en supprimant les bits supplémentaires, ce qui n'est pas plus rapide non plus.

Le seul avantage est que les types primitifs plus petits peuvent entraîner une disposition de la mémoire plus compacte, notamment lors de l'utilisation de tableaux. Cela économise de la mémoire, ce qui peut améliorer la localité de référence (réduisant ainsi le nombre d'erreurs de cache) et réduire la surcharge du garbage collection.

Cependant, de manière générale, l'utilisation des types primitifs plus petits n'est pas plus rapide.

Pour le démontrer, voici le repère suivant:

package tools.bench;

import java.math.BigDecimal;

public abstract class Benchmark {

    final String name;

    public Benchmark(String name) {
        this.name = name;
    }

    abstract int run(int iterations) throws Throwable;

    private BigDecimal time() {
        try {
            int nextI = 1;
            int i;
            long duration;
            do {
                i = nextI;
                long start = System.nanoTime();
                run(i);
                duration = System.nanoTime() - start;
                nextI = (i << 1) | 1; 
            } while (duration < 100000000 && nextI > 0);
            return new BigDecimal((duration) * 1000 / i).movePointLeft(3);
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }   

    @Override
    public String toString() {
        return name + "\t" + time() + " ns";
    }

    public static void main(String[] args) throws Exception {
        Benchmark[] benchmarks = {
            new Benchmark("int multiplication") {
                @Override int run(int iterations) throws Throwable {
                    int x = 1;
                    for (int i = 0; i < iterations; i++) {
                        x *= 3;
                    }
                    return x;
                }
            },
            new Benchmark("short multiplication") {                   
                @Override int run(int iterations) throws Throwable {
                    short x = 0;
                    for (int i = 0; i < iterations; i++) {
                        x *= 3;
                    }
                    return x;
                }
            },
            new Benchmark("byte multiplication") {                   
                @Override int run(int iterations) throws Throwable {
                    byte x = 0;
                    for (int i = 0; i < iterations; i++) {
                        x *= 3;
                    }
                    return x;
                }
            },
            new Benchmark("int[] traversal") {                   
                @Override int run(int iterations) throws Throwable {
                    int[] x = new int[iterations];
                    for (int i = 0; i < iterations; i++) {
                        x[i] = i;
                    }
                    return x[x[0]];
                }
            },
            new Benchmark("short[] traversal") {                   
                @Override int run(int iterations) throws Throwable {
                    short[] x = new short[iterations];
                    for (int i = 0; i < iterations; i++) {
                        x[i] = (short) i;
                    }
                    return x[x[0]];
                }
            },
            new Benchmark("byte[] traversal") {                   
                @Override int run(int iterations) throws Throwable {
                    byte[] x = new byte[iterations];
                    for (int i = 0; i < iterations; i++) {
                        x[i] = (byte) i;
                    }
                    return x[x[0]];
                }
            },
        };
        for (Benchmark bm : benchmarks) {
            System.out.println(bm);
        }
    }
}

qui imprime sur mon cahier un peu vieux (en ajoutant des espaces pour ajuster les colonnes):

int       multiplication    1.530 ns
short     multiplication    2.105 ns
byte      multiplication    2.483 ns
int[]     traversal         5.347 ns
short[]   traversal         4.760 ns
byte[]    traversal         2.064 ns

Comme vous pouvez le voir, les différences de performances sont assez mineures. L'optimisation des algorithmes est bien plus importante que le choix du type primitif.


3
Plutôt que de dire "notamment lors de l'utilisation de tableaux", je pense qu'il serait peut-être plus simple de le dire shortet ils bytesont plus efficaces lorsqu'ils sont stockés dans des tableaux suffisamment grands pour compter (plus le tableau est grand, plus la différence d'efficacité est grande; a byte[2]pourrait être plus ou moins efficace qu'un int[2], mais pas suffisamment pour avoir de l'importance dans les deux cas), mais que les valeurs individuelles sont stockées plus efficacement sous forme de int.
supercat du

2
Ce que j'ai vérifié: Ces benchmarks utilisaient toujours un int ('3') comme facteur ou opérande d'assignation (la variante de boucle, puis castée). Ce que j'ai fait était d'utiliser des facteurs typés / opérandes d'affectation en fonction du type de lvalue: int mult 76,481 ns int mult (typé) 72,581 ns court mult 87,908 ns court mult (typé) 90,772 ns byte mult 87,859 ns byte mult (typé) 89,524 ns int [] trav 88.905 ns int [] trav (typé) 89.126 ns short [] trav 10.563 ns short [] trav (typé) 10.039 ns byte [] trav 8.356 ns byte [] trav (typé) 8.338 ns Je suppose qu'il y a un beaucoup de casting inutile. ces tests ont été exécutés sur un onglet Android.
Bondax

5

L'utilisation byteau lieu de intpeut augmenter les performances si vous les utilisez en grande quantité. Voici une expérience:

import java.lang.management.*;

public class SpeedTest {

/** Get CPU time in nanoseconds. */
public static long getCpuTime() {
    ThreadMXBean bean = ManagementFactory.getThreadMXBean();
    return bean.isCurrentThreadCpuTimeSupported() ? bean
            .getCurrentThreadCpuTime() : 0L;
}

public static void main(String[] args) {
    long durationTotal = 0;
    int numberOfTests=0;

    for (int j = 1; j < 51; j++) {
        long beforeTask = getCpuTime();
        // MEASURES THIS AREA------------------------------------------
        long x = 20000000;// 20 millions
        for (long i = 0; i < x; i++) {
                           TestClass s = new TestClass(); 

        }
        // MEASURES THIS AREA------------------------------------------
        long duration = getCpuTime() - beforeTask;
        System.out.println("TEST " + j + ": duration = " + duration + "ns = "
                + (int) duration / 1000000);
        durationTotal += duration;
        numberOfTests++;
    }
    double average = durationTotal/numberOfTests;
    System.out.println("-----------------------------------");
    System.out.println("Average Duration = " + average + " ns = "
            + (int)average / 1000000 +" ms (Approximately)");


}

}

Cette classe teste la vitesse de création d'un nouveau fichier TestClass. Chaque test le fait 20 millions de fois et il y a 50 tests.

Voici la TestClass:

 public class TestClass {
     int a1= 5;
     int a2= 5; 
     int a3= 5;
     int a4= 5; 
     int a5= 5;
     int a6= 5; 
     int a7= 5;
     int a8= 5; 
     int a9= 5;
     int a10= 5; 
     int a11= 5;
     int a12=5; 
     int a13= 5;
     int a14= 5; 
 }

J'ai dirigé la SpeedTestclasse et à la fin j'ai ceci:

 Average Duration = 8.9625E8 ns = 896 ms (Approximately)

Maintenant, je change les entiers en octets dans TestClass et je l'exécute à nouveau. Voici le résultat:

 Average Duration = 6.94375E8 ns = 694 ms (Approximately)

Je crois que cette expérience montre que si vous instanciez une énorme quantité de variables, utiliser byte au lieu de int peut augmenter l'efficacité


4
Notez que ce benchmark ne mesure que les coûts associés à l'allocation et à la construction, et uniquement le cas d'une classe comportant de nombreux champs individuels. Si des opérations arithmétiques / de mise à jour ont été effectuées sur les champs, les résultats de @ meriton suggèrent que cela bytepourrait être >> plus lent << que int.
Stephen C

Certes, j'aurais dû mieux le formuler pour le clarifier.
WVrock

2

byte est généralement considéré comme 8 bits. short est généralement considéré comme 16 bits.

Dans un environnement "pur", qui n'est pas java car toutes les implémentations d'octets et de longs, et les courts-circuits et autres choses amusantes vous sont généralement cachés, byte fait un meilleur usage de l'espace.

Cependant, votre ordinateur n'est probablement pas 8 bits, et probablement pas 16 bits. cela signifie que pour obtenir 16 ou 8 bits en particulier, il lui faudrait recourir à la «supercherie» qui fait perdre du temps pour prétendre avoir la capacité d'accéder à ces types en cas de besoin.

À ce stade, cela dépend de la façon dont le matériel est mis en œuvre. Cependant, d'après ce que j'ai appris, la meilleure vitesse est obtenue en stockant des choses en morceaux qui sont confortables pour votre CPU à utiliser. Un processeur 64 bits aime traiter des éléments 64 bits, et rien de moins que cela nécessite souvent de la "magie d'ingénierie" pour faire semblant de les aimer.


3
Je ne sais pas ce que vous entendez par «magie de l'ingénierie» ... la plupart / tous les processeurs modernes ont des instructions rapides pour charger un octet et le prolonger de signe, pour en stocker un à partir d'un registre pleine largeur et pour faire une largeur d'octet ou arithmétique de courte largeur dans une partie d'un registre de pleine largeur. Si vous aviez raison, il serait logique, dans la mesure du possible, de remplacer tous les entiers par des longs sur un processeur 64 bits.
Ed Staub

Je peux imaginer que c'est vrai. Je me souviens juste que dans le simulateur Motorola 68k que nous avons utilisé, la plupart des opérations pouvaient fonctionner avec des valeurs 16 bits, mais pas avec 32 bits ni 64 bits. Je pensais que cela signifiait que les systèmes avaient une taille de valeur préférée qu'ils pouvaient récupérer de manière optimale. Bien que je puisse imaginer que les processeurs 64 bits modernes peuvent extraire 8 bits, 16 bits, 32 bits et 64 bits avec la même facilité, dans ce cas, ce n'est pas un problème. Merci d'avoir fait remarquer cela.
Dmitry

"... est généralement considéré comme ..." - En fait, il est clairement, sans ambiguïté >> spécifié << qu'il s'agit de ces tailles. En Java. Et le contexte de cette question est Java.
Stephen C

Un grand nombre de processeurs utilisent même le même nombre de cycles pour manipuler et accéder à des données qui ne sont pas de la taille d'un mot, donc cela ne vaut pas vraiment la peine de s'inquiéter à moins que vous ne mesuriez sur une JVM et une plate-forme particulières.
drrob

J'essaie de dire en toute généralité. Cela dit, je ne suis pas vraiment sûr de la norme de Java en ce qui concerne la taille des octets, mais à ce stade, je suis assez convaincu que si un hérétique décide des octets non 8 bits, Java ne voudra pas les toucher avec un poteau de dix pieds. Cependant, certains processeurs nécessitent un alignement multi-octets, et si la plate-forme Java les prend en charge, elle devra faire les choses plus lentement pour s'adapter à ces types plus petits, ou les représenter comme par magie avec des représentations plus grandes que ce que vous avez demandé. Cela préfère toujours int aux autres types car il utilise toujours la taille préférée du système.
Dmitry

2

L'une des raisons pour lesquelles short / byte / char est moins performant est le manque de prise en charge directe de ces types de données. Par support direct, cela signifie que les spécifications JVM ne mentionnent aucun jeu d'instructions pour ces types de données. Les instructions telles que stocker, charger, ajouter, etc. ont des versions pour le type de données int. Mais ils n'ont pas de versions pour short / byte / char. Par exemple, considérez ci-dessous le code java:

void spin() {
 int i;
 for (i = 0; i < 100; i++) {
 ; // Loop body is empty
 }
}

La même chose est convertie en code machine comme ci-dessous.

0 iconst_0 // Push int constant 0
1 istore_1 // Store into local variable 1 (i=0)
2 goto 8 // First time through don't increment
5 iinc 1 1 // Increment local variable 1 by 1 (i++)
8 iload_1 // Push local variable 1 (i)
9 bipush 100 // Push int constant 100
11 if_icmplt 5 // Compare and loop if less than (i < 100)
14 return // Return void when done

Maintenant, envisagez de changer int en short comme ci-dessous.

void sspin() {
 short i;
 for (i = 0; i < 100; i++) {
 ; // Loop body is empty
 }
}

Le code machine correspondant change comme suit:

0 iconst_0
1 istore_1
2 goto 10
5 iload_1 // The short is treated as though an int
6 iconst_1
7 iadd
8 i2s // Truncate int to short
9 istore_1
10 iload_1
11 bipush 100
13 if_icmplt 5
16 return

Comme vous pouvez le constater, pour manipuler un type de données court, il utilise toujours la version d'instruction de type de données int et convertit explicitement int en short lorsque cela est nécessaire. Maintenant, à cause de cela, les performances sont réduites.

Maintenant, la raison invoquée pour ne pas donner de soutien direct est la suivante:

La machine virtuelle Java fournit la prise en charge la plus directe des données de type int. Ceci est en partie en prévision d'implémentations efficaces des piles d'opérandes et des tableaux de variables locales de la machine virtuelle Java. Il est également motivé par la fréquence des données int dans les programmes typiques. D'autres types intégraux ont un support moins direct. Il n'y a pas de version octet, char ou courte des instructions de stockage, de chargement ou d'ajout, par exemple.

Extrait de la spécification JVM présente ici (Page 58).


Ce sont des bytecodes démontés; c'est-à-dire des instructions virtuelles JVM . Ils ne sont pas optimisés par le javaccompilateur et vous ne pouvez en tirer aucune conclusion fiable sur les performances du programme dans la vie réelle. Le compilateur JIT compile ces bytecodes en instructions machine natives réelles , et effectue une optimisation assez sérieuse dans le processus. Si vous souhaitez analyser les performances du code, vous devez examiner les instructions du code natif. (Et c'est compliqué car vous devez prendre en compte le comportement de synchronisation d'un pipeline x86_64 à plusieurs étages.)
Stephen C

Je crois que les spécifications java sont destinées aux implémenteurs javac à implémenter. Donc je ne pense pas qu'il y ait plus d'optimisations faites à ce niveau. Quoi qu'il en soit, je pourrais me tromper complètement aussi. Veuillez partager un lien de référence pour soutenir votre déclaration.
Manish Bansal

Eh bien, voici un fait pour étayer ma déclaration. Vous ne trouverez pas de chiffres de synchronisation (crédibles) qui vous indiquent combien de cycles d'horloge chaque instruction de bytecode JVM prend. Certainement pas publié par Oracle ou d'autres fournisseurs de JVM. Lisez également stackoverflow.com/questions/1397009
Stephen C

J'ai trouvé un ancien article (2008) dans lequel quelqu'un a essayé de développer un modèle indépendant de la plate-forme pour prédire les performances des séquences de bytecode. Ils affirment que leurs prédictions étaient décalées de 25% par rapport aux mesures RDTSC ... sur un Pentium. Et ils exécutaient la JVM avec la compilation JIT désactivée! Référence: sciencedirect.com/science/article/pii/S1571066108004581
Stephen C

Je suis juste confus ici. Ma réponse n'appuie-t-elle pas les faits que vous avez énoncés dans la section de révision?
Manish Bansal

0

La différence est à peine perceptible! C'est plus une question de design, de pertinence, d'uniformité, d'habitude, etc ... Parfois c'est juste une question de goût. Lorsque tout ce qui vous importe, c'est que votre programme soit opérationnel et que le remplacement d'un floatpar un intne nuirait pas à l'exactitude, je ne vois aucun avantage à opter pour l'un ou l'autre à moins que vous ne puissiez démontrer que l'utilisation de l'un ou l'autre type altère les performances. L'optimisation des performances en fonction de types différents sur 2 ou 3 octets est vraiment la dernière chose dont vous devriez vous soucier; Donald Knuth a dit un jour: "L'optimisation prématurée est la racine de tout mal" (pas sûr que ce soit lui, éditez si vous avez la réponse).


5
Nit: A float ne peut pas représenter tous les entiers d'une intboîte; et ne peut intpas non plus représenter une valeur non entière qui le floatpeut. Autrement dit, alors que toutes les valeurs int sont un sous-ensemble de valeurs longues, un int n'est pas un sous-ensemble d'un float et un float n'est pas un sous-ensemble d'un int.

Je m'attends à ce que le répondant ait l'intention d'écrire substituting a float for a double, si tel est le cas, le répondeur devrait modifier la réponse. Si ce n'est pas le cas, le répondeur devrait baisser la tête de honte et revenir à l'essentiel pour les raisons exposées par @pst et pour de nombreuses autres raisons.
High Performance Mark

@HighPerformanceMark Non, j'ai mis int et float parce que c'est ce que je pensais. Ma réponse n'est pas spécifique à Java bien que je pensais C ... C'est censé être général. Commentaire moyen que vous y êtes.
mrk
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.