110 votes

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

J'ai remarqué que j'ai toujours utilisé des int et des doubles, quelle que soit la taille du nombre. Donc en Java, est-il plus efficace d'utiliser byte o short au lieu de int y float au lieu de double ?

Supposons que j'ai un programme avec beaucoup d'entrées et de sorties. Cela vaudrait-il la peine de changer mes ints en bytes ou en shorts si je savais que le nombre serait suffisant ?

Je sais que Java n'a pas de types non signés mais y a-t-il quelque chose d'autre que je puisse faire si je sais que le nombre ne sera que positif ?

Par efficace, j'entends surtout traitement. Je suppose que le ramasseur de déchets serait beaucoup plus rapide si toutes les variables avaient la moitié de leur taille et que les calculs seraient probablement un peu plus rapides aussi. (Je suppose que puisque je travaille sur Android, je dois aussi me préoccuper de la mémoire vive).

(Je suppose que le ramasseur d'ordures ne traite que les objets et non les primitives, mais il supprime quand même 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'aie pas mesuré "scientifiquement" quoi que ce soit).

Ai-je tort de penser qu'il devrait être plus rapide et plus efficace ? Je n'aimerais pas avoir à tout modifier dans un gros programme pour m'apercevoir que j'ai perdu mon temps.

Cela vaudrait-il la peine de le faire depuis le début lorsque je commence un nouveau projet ? (Je veux dire que je pense que chaque petit geste serait utile, mais si c'est le cas, pourquoi personne ne semble le faire).

130voto

Stephen C Points 255558

Ai-je tort de penser qu'il devrait être plus rapide et plus efficace ? Je n'aimerais pas avoir à tout modifier dans un gros programme pour m'apercevoir que j'ai perdu mon temps.

Réponse courte

Oui, vous avez tort. Dans la plupart des cas, cela fait petite différence en termes d'espace utilisé.

Il est ne vaut pas la peine d'essayer d'optimiser ceci ... à moins que vous n'ayez des preuves claires qu'une optimisation est nécessaire. Et si vous le faites besoin de pour 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'objets en utilisant des décalages qui sont (en fait) des multiples de la taille d'une cellule primitive de 32 bits. Ainsi, lorsque vous déclarez une variable locale ou un champ d'objet en tant que (disons) un byte la variable/le champ sera stocké(e) dans une cellule de 32 bits, tout comme un fichier int .

Il y a deux exceptions à cette règle :

  • long y double les valeurs nécessitent 2 cellules primitives de 32 bits
  • Les tableaux de types primitifs sont représentés sous forme compacte, de sorte que (par exemple) un tableau d'octets contient 4 octets par mot de 32 bits.

Donc, il pourrait il vaut la peine d'optimiser l'utilisation de long y double ... et de grands tableaux de primitives. Mais en général non.

En théorie, un système JAT pourrait être capable d'optimiser cela, mais en pratique je n'ai jamais entendu parler d'un JIT qui le fasse. L'un des obstacles est que le JIT ne peut généralement pas s'exécuter avant que toutes les instances de la classe à compiler aient été créées. Si le JIT optimisait la disposition de la mémoire, vous pourriez avoir deux (ou plus) "saveurs" d'objet de la même classe ... et cela présenterait d'énormes difficultés.


Revisitation

En examinant les résultats du benchmark dans la réponse de @meriton, il apparaît que l'utilisation de la fonction short y byte au lieu de int subit une pénalité de performance pour la multiplication. En effet, si vous considérez les opérations de manière isolée, la pénalité est significative. (Vous ne devriez pas les considérer isolément... mais c'est un autre sujet).

Je pense que l'explication est que le JIT effectue probablement les multiplications en utilisant des instructions de multiplication de 32 bits dans chaque cas. Mais dans le byte y short Dans ce cas, il exécute extra des instructions pour convertir la valeur intermédiaire de 32 bits en un byte o short à chaque itération de la 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 comprendre cela).

Quoi qu'il en soit, cela met en évidence un autre problème lié au passage à l'euro. short y byte comme une optimisation. Cela pourrait améliorer les performances pire ... dans un algorithme qui est arithmétiquement et informatiquement intensif.


Questions secondaires

Je sais que Java n'a pas de types non signés mais y a-t-il quelque chose d'autre que je puisse faire si je sais que le nombre ne sera que positif ?

Non. Pas en termes de performances en tout cas. (Il existe certaines méthodes dans Integer , Long etc. pour traiter int , long etc. comme non signés. Mais cela ne donne aucun avantage en termes de performances. Ce n'est pas leur but).

(Je suppose que le ramasseur d'ordures ne traite que les objets et non les primitives, mais il supprime quand même toutes les primitives des objets abandonnés, n'est-ce pas ? )

Correct. Un champ d'un objet est partie de l'objet. Il disparaît lorsque l'objet est mis au rebut. De même, les cellules d'un tableau disparaissent lorsque le tableau est collecté. Lorsque le type de champ ou de cellule est un type primitif, la valeur est stockée dans le champ/cellule ... qui fait partie de l'objet/du tableau ... et qui a été supprimé.

33voto

meriton Points 30447

Cela dépend de l'implémentation de la JVM, ainsi que du matériel sous-jacent. La plupart des matériels modernes ne vont pas chercher des octets uniques dans la mémoire (ou même dans le 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 en mémoire. De même, les CPU modernes ont une taille de mot de 64 bits. Ils peuvent effectuer des opérations sur moins de bits, mais cela fonctionne en rejetant les bits supplémentaires, ce qui n'est pas plus rapide non plus.

Le seul avantage est que des types primitifs plus petits peuvent donner lieu à une disposition de la mémoire plus compacte, notamment lors de l'utilisation de tableaux. Cela permet d'économiser de la mémoire, ce qui peut améliorer la localité de la référence (réduisant ainsi le nombre de ratés du cache) et réduire les frais de ramassage des déchets.

En général, cependant, l'utilisation des petits types primitifs n'est pas plus rapide.

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

public class Benchmark {

    public static void benchmark(String label, Code code) {
        print(25, label);

        try {
            for (int iterations = 1; ; iterations *= 2) { // detect reasonable iteration count and warm up the code under test
                System.gc(); // clean up previous runs, so we don't benchmark their cleanup
                long previouslyUsedMemory = usedMemory();
                long start = System.nanoTime();
                code.execute(iterations);
                long duration = System.nanoTime() - start;
                long memoryUsed = usedMemory() - previouslyUsedMemory;

                if (iterations > 1E8 || duration > 1E9) { 
                    print(25, new BigDecimal(duration * 1000 / iterations).movePointLeft(3) + " ns / iteration");
                    print(30, new BigDecimal(memoryUsed * 1000 / iterations).movePointLeft(3) + " bytes / iteration\n");
                    return;
                }
            }
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }

    private static void print(int desiredLength, String message) {
        System.out.print(" ".repeat(Math.max(1, desiredLength - message.length())) + message);
    }

    private static long usedMemory() {
        return Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
    }

    @FunctionalInterface
    interface Code {
        /**
         * Executes the code under test.
         * 
         * @param iterations
         *            number of iterations to perform
         * @return any value that requires the entire code to be executed (to
         *         prevent dead code elimination by the just in time compiler)
         * @throws Throwable
         *             if the test could not complete successfully
         */
        Object execute(int iterations);
    }

    public static void main(String[] args) {
        benchmark("long[] traversal", (iterations) -> {
            long[] array = new long[iterations];
            for (int i = 0; i < iterations; i++) {
                array[i] = i;
            }
            return array;
        });
        benchmark("int[] traversal", (iterations) -> {
            int[] array = new int[iterations];
            for (int i = 0; i < iterations; i++) {
                array[i] = i;
            }
            return array;
        });
        benchmark("short[] traversal", (iterations) -> {
            short[] array = new short[iterations];
            for (int i = 0; i < iterations; i++) {
                array[i] = (short) i;
            }
            return array;
        });
        benchmark("byte[] traversal", (iterations) -> {
            byte[] array = new byte[iterations];
            for (int i = 0; i < iterations; i++) {
                array[i] = (byte) i;
            }
            return array;
        });

        benchmark("long fields", (iterations) -> {
            class C {
                long a = 1;
                long b = 2;
            }

            C[] array = new C[iterations];
            for (int i = 0; i < iterations; i++) {
                array[i] = new C();
            }
            return array;
        });
        benchmark("int fields", (iterations) -> {
            class C {
                int a = 1;
                int b = 2;
            }

            C[] array = new C[iterations];
            for (int i = 0; i < iterations; i++) {
                array[i] = new C();
            }
            return array;
        });
        benchmark("short fields", (iterations) -> {
            class C {
                short a = 1;
                short b = 2;
            }

            C[] array = new C[iterations];
            for (int i = 0; i < iterations; i++) {
                array[i] = new C();
            }
            return array;
        });
        benchmark("byte fields", (iterations) -> {
            class C {
                byte a = 1;
                byte b = 2;
            }

            C[] array = new C[iterations];
            for (int i = 0; i < iterations; i++) {
                array[i] = new C();
            }
            return array;
        });

        benchmark("long multiplication", (iterations) -> {
            long result = 1;
            for (int i = 0; i < iterations; i++) {
                result *= 3;
            }
            return result;
        });
        benchmark("int multiplication", (iterations) -> {
            int result = 1;
            for (int i = 0; i < iterations; i++) {
                result *= 3;
            }
            return result;
        });
        benchmark("short multiplication", (iterations) -> {
            short result = 1;
            for (int i = 0; i < iterations; i++) {
                result *= 3;
            }
            return result;
        });
        benchmark("byte multiplication", (iterations) -> {
            byte result = 1;
            for (int i = 0; i < iterations; i++) {
                result *= 3;
            }
            return result;
        });
    }
}

Exécuté avec OpenJDK 14 sur mon processeur Intel Core i7 @ 3.5 GHz, ceci imprime :

     long[] traversal     3.206 ns / iteration      8.007 bytes / iteration
      int[] traversal     1.557 ns / iteration      4.007 bytes / iteration
    short[] traversal     0.881 ns / iteration      2.007 bytes / iteration
     byte[] traversal     0.584 ns / iteration      1.007 bytes / iteration
          long fields    25.485 ns / iteration     36.359 bytes / iteration
           int fields    23.126 ns / iteration     28.304 bytes / iteration
         short fields    21.717 ns / iteration     20.296 bytes / iteration
          byte fields    21.767 ns / iteration     20.273 bytes / iteration
  long multiplication     0.538 ns / iteration      0.000 bytes / iteration
   int multiplication     0.526 ns / iteration      0.000 bytes / iteration
 short multiplication     0.786 ns / iteration      0.000 bytes / iteration
  byte multiplication     0.784 ns / iteration      0.000 bytes / iteration

Comme vous pouvez le constater, les seuls gains de vitesse significatifs se produisent lors de la traversée de grands tableaux ; l'utilisation de champs d'objets plus petits n'apporte que des avantages négligeables, et les calculs sont en fait légèrement plus lents sur les petits types de données.

Dans l'ensemble, les différences de performances sont assez mineures. L'optimisation des algorithmes est bien plus importante que le choix du type de primitive.

6voto

WVrock Points 13

Utilisation de byte au lieu de int peuvent 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'une nouvelle TestClass . Chaque test le fait 20 millions de fois et il y a 50 tests.

Voici la classe de test :

 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 lancé le SpeedTest et à la fin j'ai obtenu ceci :

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

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

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

Je pense que cette expérience montre que si vous instanciez un grand nombre de variables, l'utilisation d'octets au lieu d'int peut augmenter l'efficacité.

2voto

Dmitry Points 886

On considère généralement qu'un octet est constitué de 8 bits. Un court est généralement considéré comme étant de 16 bits.

Dans un environnement "pur", ce qui n'est pas le cas de Java puisque toute l'implémentation des octets, des longs, des courts et autres choses amusantes vous est généralement cachée, l'octet fait un meilleur usage de l'espace.

Cependant, votre ordinateur n'est probablement pas 8 bits, et il n'est probablement pas 16 bits. cela signifie que pour obtenir 16 ou 8 bits en particulier, il devrait recourir à une "ruse" qui lui fait perdre du temps afin de prétendre qu'il a la capacité d'accéder à ces types en cas de besoin.

À ce stade, cela dépend de la manière dont le matériel est mis en œuvre. Cependant, d'après ce qu'on m'a dit, la meilleure vitesse est obtenue en stockant les choses en morceaux qui sont confortables à utiliser pour votre CPU. Un processeur 64 bits aime traiter des éléments 64 bits, et tout ce qui est inférieur à cela nécessite souvent une "magie d'ingénierie" pour prétendre qu'il aime les traiter.

2voto

Manish Bansal Points 129

L'une des raisons pour lesquelles les formats short/byte/char sont moins performants est l'absence de support direct pour ces types de données. Par support direct, on entend que les spécifications de la JVM ne mentionnent aucun jeu d'instructions pour ces types de données. Des instructions comme store, load, add etc. ont des versions pour le type de données int. Mais elles n'ont pas de versions pour les types short/byte/char. Par exemple, considérez le code java ci-dessous :

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

Le même est converti 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, envisageons 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 sera modifié 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 le type de données court, il utilise toujours la version d'instruction du type de données int et convertit explicitement int en short lorsque cela est nécessaire. De ce fait, les performances sont réduites.

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

La machine virtuelle Java fournit le support le plus direct pour les données de type 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 locales de la machine virtuelle Java. Elle est également motivée par la fréquence des données de type int dans les applications typiques de la machine virtuelle Java. programmes typiques. Les autres types d'intégrales ont un support moins direct. Il n'y a pas de Il n'existe pas de versions octet, char ou courte des instructions store, load ou add, par exemple.

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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X