In Java, è più efficiente usare byte o short invece di int e float invece di double?


91

Ho notato che ho sempre usato int e double, non importa quanto piccolo o grande debba essere il numero. Quindi, in Java, è più efficiente da usare byteo shortinvece di inte floatinvece di double?

Quindi supponi di avere un programma con molti int e doppi. Varrebbe la pena passare attraverso e cambiare i miei int in byte o corti se sapessi che il numero si adatterebbe?

So che java non ha tipi non firmati, ma c'è qualcosa in più che potrei fare se sapessi che il numero sarebbe solo positivo?

Per efficiente intendo principalmente elaborazione. Presumo che il garbage collector sarebbe molto più veloce se tutte le variabili fossero di dimensioni dimezzate e che anche i calcoli sarebbero probabilmente un po 'più veloci. (Immagino che dal momento che sto lavorando su Android devo preoccuparmi un po 'anche della ram)

(Presumo che il garbage collector si occupi solo di oggetti e non primitivo ma cancella comunque tutte le primitive negli oggetti abbandonati, giusto?)

L'ho provato con una piccola app per Android che ho ma non ho notato alcuna differenza. (Anche se non ho misurato "scientificamente" nulla.)

Sbaglio nel ritenere che dovrebbe essere più veloce ed efficiente? Non vorrei passare attraverso e cambiare tutto in un programma enorme per scoprire che ho perso il mio tempo.

Varrebbe la pena farlo dall'inizio quando inizio un nuovo progetto? (Voglio dire, penso che ogni piccola cosa aiuterebbe, ma poi di nuovo se è così, perché non sembra che qualcuno lo faccia.)

Risposte:


107

Sbaglio nel ritenere che dovrebbe essere più veloce ed efficiente? Non vorrei passare attraverso e cambiare tutto in un programma enorme per scoprire che ho perso il mio tempo.

Risposta breve

Sì, ti sbagli. Nella maggior parte dei casi, fa poca differenza in termini di spazio utilizzato.

Non vale la pena tentare di ottimizzarlo ... a meno che non si disponga di prove chiare che è necessaria l'ottimizzazione. E se è necessario ottimizzare l'utilizzo della memoria in particolare dei campi oggetto, sarà probabilmente necessario adottare altre misure (più efficaci).

Risposta più lunga

La Java Virtual Machine modella stack e campi oggetto utilizzando offset che sono (in effetti) multipli di una dimensione di cella primitiva a 32 bit. Quindi, quando dichiari una variabile locale o un campo oggetto come (diciamo) a byte, la variabile / campo verrà archiviata in una cella a 32 bit, proprio come un file int.

Ci sono due eccezioni a questo:

  • longe i doublevalori richiedono 2 celle primitive a 32 bit
  • array di tipi primitivi sono rappresentati in forma compressa, in modo che (per esempio) un array di byte contenga 4 byte per parola a 32 bit.

Quindi potrebbe valere la pena ottimizzare l'uso di longe double... e grandi array di primitive. Ma in generale no.

In teoria, un JIT potrebbe essere in grado di ottimizzarlo, ma in pratica non ho mai sentito parlare di un JIT che lo fa. Un ostacolo è che il JIT in genere non può essere eseguito fino a quando non sono state create istanze della classe da compilare. Se il JIT ottimizzasse il layout della memoria, si potrebbero avere due (o più) "versioni" di oggetto della stessa classe ... e ciò presenterebbe enormi difficoltà.


Rivisitazione

Guardando i risultati del benchmark nella risposta di @ meriton, sembra che l'utilizzo di shorte byteinvece di intincorrere in una penalizzazione delle prestazioni per la moltiplicazione. Infatti, se si considerano le operazioni isolatamente, la sanzione è significativa. (Non dovresti considerarli isolatamente ... ma questo è un altro argomento.)

Penso che la spiegazione sia che JIT sta probabilmente facendo le moltiplicazioni usando istruzioni di moltiplicazione a 32 bit in ogni caso. Ma nel caso bytee short, esegue istruzioni aggiuntive per convertire il valore intermedio a 32 bit in a byteo shortin ciascuna iterazione del ciclo. (In teoria, quella conversione potrebbe essere eseguita una volta alla fine del ciclo ... ma dubito che l'ottimizzatore sarebbe in grado di capirlo.)

Ad ogni modo, questo indica un altro problema con il passaggio a shorte bytecome ottimizzazione. Potrebbe peggiorare le prestazioni ... in un algoritmo aritmetico e ad alta intensità di calcolo.


30
+1 non ottimizzare a meno che tu non abbia una chiara evidenza di un problema di prestazioni
Bohemian

Ehm, perché la JVM deve attendere la compilazione JIT per impacchettare il layout di memoria di una classe? Poiché i tipi di campi vengono scritti nel file di classe, la JVM non potrebbe scegliere un layout di memoria al momento del caricamento della classe, quindi risolvere i nomi dei campi come byte anziché come offset di parola?
meriton

@meriton - Sono abbastanza sicuro che i layout degli oggetti vengono determinati al momento del caricamento della classe e non cambiano dopo. Vedi la parte "fine-stampa" della mia risposta. Se i layout di memoria effettivi cambiassero quando il codice è stato sottoposto a JIT, sarebbe davvero difficile da gestire per la JVM. (Quando ho detto che la JIT potrebbe ottimizzare il layout, è ipotetico e poco pratico ... il che potrebbe spiegare perché non ho mai sentito parlare di una JIT che lo fa effettivamente.)
Stephen C

Lo so. Stavo solo cercando di sottolineare che anche se i layout di memoria sono difficili da modificare una volta creati gli oggetti, una JVM potrebbe ancora ottimizzare il layout di memoria prima di allora, cioè al momento del caricamento della classe. In altre parole, il fatto che la specifica JVM descriva il comportamento di una JVM con offset di parole non implica necessariamente che una JVM debba essere implementata in quel modo, anche se molto probabilmente lo sono.
meriton

@meriton - La specifica JVM parla di "offset di parole della macchina virtuale" all'interno di frame / oggetti locali. Il modo in cui questi vengono mappati agli offset della macchina fisica NON è specificato. In effetti, non può specificarlo ... poiché potrebbero esserci requisiti di allineamento del campo specifici dell'hardware.
Stephen C

29

Dipende dall'implementazione della JVM e dall'hardware sottostante. La maggior parte dell'hardware moderno non preleverà singoli byte dalla memoria (o anche dalla cache di primo livello), cioè l'uso di tipi primitivi più piccoli generalmente non riduce il consumo di larghezza di banda della memoria. Allo stesso modo, le moderne CPU hanno una dimensione della parola di 64 bit. Possono eseguire operazioni con meno bit, ma questo funziona scartando i bit extra, che non è neanche più veloce.

L'unico vantaggio è che tipi primitivi più piccoli possono dare come risultato un layout di memoria più compatto, in particolare quando si utilizzano array. Ciò consente di risparmiare memoria, che può migliorare la località di riferimento (riducendo così il numero di cache miss) e ridurre il sovraccarico di raccolta dei rifiuti.

In generale, tuttavia, l'utilizzo dei tipi primitivi più piccoli non è più veloce.

Per dimostrarlo, osserva il seguente benchmark:

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);
        }
    }
}

che viene stampato sul mio taccuino un po 'vecchio (aggiungendo spazi per regolare le colonne):

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

Come puoi vedere, le differenze di prestazioni sono piuttosto minori. L'ottimizzazione degli algoritmi è molto più importante della scelta del tipo primitivo.


3
Piuttosto che dire "in particolare quando si usano array", penso che potrebbe essere più semplice dirlo shorte bytesono più efficienti se archiviati in array che sono abbastanza grandi da essere importanti (più grande è l'array, maggiore è la differenza di efficienza; a byte[2]potrebbe essere maggiore o meno efficiente di an int[2], ma non abbastanza da essere importante in entrambi i casi), ma i singoli valori vengono memorizzati in modo più efficiente come int.
supercat

2
Cosa ho controllato: quei benchmark usavano sempre un int ('3') come operando di fattore o di assegnazione (la variante del ciclo, quindi cast). Quello che ho fatto è stato utilizzare fattori tipizzati / operandi di assegnazione a seconda del tipo lvalue: int mult 76.481 ns int mult (typed) 72.581 ns short mult 87.908 ns short mult (typed) 90.772 ns byte mult 87.859 ns byte mult (typed) 89.524 ns int [] trav 88.905 ns int [] trav (typed) 89.126 ns short [] trav 10.563 ns short [] trav (typed) 10.039 ns byte [] trav 8.356 ns byte [] trav (typed) 8.338 ns Suppongo che ci sia un un sacco di casting inutili. quei test sono stati eseguiti su una scheda Android.
Bondax

5

L'utilizzo al byteposto di intpuò aumentare le prestazioni se vengono utilizzati in quantità enormi. Ecco un esperimento:

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)");


}

}

Questa classe verifica la velocità di creazione di un nuovo file TestClass. Ogni test lo fa 20 milioni di volte e ci sono 50 test.

Ecco il 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; 
 }

Ho condotto la SpeedTestlezione e alla fine ho ottenuto questo:

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

Ora sto cambiando gli int in byte nella TestClass e lo sto eseguendo di nuovo. Ecco il risultato:

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

Credo che questo esperimento dimostri che se installi un'enorme quantità di variabili, l'utilizzo di byte invece di int può aumentare l'efficienza


4
Si noti che questo benchmark misura solo i costi associati all'allocazione e alla costruzione e solo il caso di una classe con molti campi individuali. Se le operazioni aritmetiche / di aggiornamento sono state eseguite sui campi, i risultati di @ meriton suggeriscono che bytepotrebbe essere >> più lento << di int.
Stephen C

È vero, avrei dovuto formularlo meglio per chiarirlo.
WVrock

2

byte è generalmente considerato essere 8 bit. short è generalmente considerato di 16 bit.

In un ambiente "puro", che non è java poiché tutte le implementazioni di byte e long e short e altre cose divertenti sono generalmente nascoste, byte fa un uso migliore dello spazio.

Tuttavia, il tuo computer probabilmente non è a 8 bit e probabilmente non è a 16 bit. questo significa che per ottenere 16 o 8 bit in particolare, bisognerebbe ricorrere ad un "trucco" che fa perdere tempo per fingere di avere la capacità di accedere a quei tipi quando necessario.

A questo punto, dipende da come viene implementato l'hardware. Tuttavia, da quanto ho pensato, la migliore velocità si ottiene memorizzando le cose in blocchi che sono comodi da usare per la tua CPU. A un processore a 64 bit piace trattare con elementi a 64 bit, e qualsiasi cosa meno spesso richiede "magia ingegneristica" per fingere che gli piaccia occuparsene.


3
Non sono sicuro di cosa intendi per "magia ingegneristica" ... la maggior parte / tutti i processori moderni hanno istruzioni veloci per caricare un byte ed estenderlo con il segno, per memorizzarne uno da un registro a larghezza intera e per eseguire la larghezza di byte o aritmetica a larghezza ridotta in una porzione di un registro a larghezza intera. Se avessi ragione, avrebbe senso, ove possibile, sostituire tutti gli int con i long su un processore a 64 bit.
Ed Staub

Posso immaginare che sia vero. Ricordo solo che nel simulatore Motorola 68k che abbiamo usato, la maggior parte delle operazioni poteva funzionare con valori a 16 bit mentre non con 32 bit né 64 bit. Pensavo che questo significasse che i sistemi avevano una dimensione di valore preferita che può recuperare in modo ottimale. Anche se posso immaginare che i moderni processori a 64 bit possano recuperare 8 bit, 16 bit, 32 bit e 64 bit con la stessa facilità, in questo caso non è un problema. Grazie per la segnalazione.
Dmitry

"... è generalmente considerato come ..." - In realtà, è chiaramente, in modo inequivocabile >> specificato << come quelle dimensioni. In Java. E il contesto di questa domanda è Java.
Stephen C

Un gran numero di processori utilizza persino lo stesso numero di cicli per manipolare e accedere a dati che non sono di dimensioni di parola, quindi non vale davvero la pena preoccuparsi a meno che non si misuri su una particolare JVM e piattaforma.
drrob

Sto cercando di dire in generale. Detto questo, in realtà non sono sicuro dello standard di Java per quanto riguarda la dimensione dei byte, ma a questo punto sono abbastanza convinto che se un eretico decide i byte non a 8 bit, Java non vorrà toccarli con un palo di dieci piedi. Tuttavia, alcuni processori richiedono l'allineamento multibyte e, se la piattaforma Java li supporta, dovrà fare le cose più lentamente per adattarsi alla gestione di questi tipi più piccoli o rappresentarli magicamente con rappresentazioni più grandi di quelle richieste. Che preferisce sempre int rispetto ad altri tipi poiché utilizza sempre la dimensione preferita del sistema.
Dmitry

2

Uno dei motivi per cui short / byte / char è meno performante è la mancanza di supporto diretto per questi tipi di dati. Per supporto diretto, significa che le specifiche JVM non menzionano alcun set di istruzioni per questi tipi di dati. Istruzioni come store, load, add ecc. Hanno versioni per il tipo di dati int. Ma non hanno versioni per short / byte / char. Ad esempio, considera di seguito il codice Java:

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

Lo stesso viene convertito in codice macchina come di seguito.

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

Ora, considera di cambiare int in short come di seguito.

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

Il codice macchina corrispondente cambierà come segue:

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

Come puoi osservare, per manipolare il tipo di dati short, utilizza ancora la versione dell'istruzione del tipo di dati int e converte esplicitamente int in short quando richiesto. Ora, a causa di ciò, le prestazioni vengono ridotte.

Ora, motivo addotto per non fornire supporto diretto come segue:

La Java Virtual Machine fornisce il supporto più diretto per i dati di tipo int. Ciò è in parte in previsione di implementazioni efficienti degli stack di operandi e degli array di variabili locali della Java Virtual Machine. È anche motivato dalla frequenza dei dati int nei programmi tipici. Altri tipi integrali hanno un supporto meno diretto. Ad esempio, non ci sono byte, char o versioni brevi delle istruzioni per memorizzare, caricare o aggiungere.

Citato dalla specifica JVM presente qui (pagina 58).


Questi sono bytecode smontati; cioè istruzioni virtuali JVM . Non sono ottimizzati dal javaccompilatore e da essi non è possibile trarre alcuna deduzione affidabile su come il programma si esibirà nella vita reale. Il compilatore JIT compila questi bytecode nelle effettive istruzioni della macchina nativa e nel processo esegue un'ottimizzazione piuttosto seria. Se si desidera analizzare le prestazioni del codice, è necessario esaminare le istruzioni del codice nativo. (Ed è complicato perché è necessario prendere in considerazione il comportamento temporale di una pipeline x86_64 a più stadi.)
Stephen C

Credo che le specifiche java debbano essere implementate dagli implementatori javac. Quindi non credo che ci siano altre ottimizzazioni fatte a quel livello. Comunque, potrei anche sbagliarmi completamente. Per favore condividi alcuni link di riferimento per supportare la tua dichiarazione
Manish Bansal

Bene, ecco un fatto a sostegno della mia affermazione. Non troverai cifre di temporizzazione (credibili) che ti dicano quanti cicli di clock richiede ciascuna istruzione di bytecode JVM. Certamente non pubblicato da Oracle o da altri fornitori di JVM. Inoltre, leggi stackoverflow.com/questions/1397009
Stephen C

Ho trovato un vecchio documento (2008) in cui qualcuno ha cercato di sviluppare un modello indipendente dalla piattaforma per prevedere le prestazioni delle sequenze di bytecode. Affermano che le loro previsioni erano sbagliate del 25% rispetto alle misurazioni RDTSC ... su un Pentium. E stavano eseguendo la JVM con la compilazione JIT disabilitata! Riferimento: sciencedirect.com/science/article/pii/S1571066108004581
Stephen C

Sono solo confuso qui. La mia risposta non supporta i fatti che hai dichiarato nella sezione di revisione?
Manish Bansal

0

La differenza è appena percettibile! È più una questione di design, adeguatezza, uniformità, abitudine, ecc ... A volte è solo una questione di gusti. Quando tutto ciò che ti interessa è che il tuo programma sia attivo e funzionante e sostituire una floatcon una intnon danneggerebbe la correttezza, non vedo alcun vantaggio nell'usare l'uno o l'altro a meno che tu non possa dimostrare che l'uso di entrambi i tipi altera le prestazioni. L'ottimizzazione delle prestazioni in base a tipi diversi in 2 o 3 byte è davvero l'ultima cosa di cui dovresti preoccuparti; Donald Knuth una volta disse: "L'ottimizzazione prematura è la radice di tutti i mali" (non sono sicuro che fosse lui, modifica se hai la risposta).


5
Nit: A float non può rappresentare tutti i numeri interi di una intlattina; né può intrappresentare un valore non intero che floatpuò. Cioè, mentre tutti i valori int sono un sottoinsieme di valori lunghi, un int non è un sottoinsieme di un float e un float non è un sottoinsieme di un int.

Mi aspetto che chi substituting a float for a doublerisponde intendeva scrivere , in tal caso dovrebbe modificare la risposta. In caso contrario, chi risponde dovrebbe abbassare la testa per la vergogna e tornare alle origini per i motivi delineati da @pst e per molti altri motivi.
High Performance Mark

@HighPerformanceMark No ho messo int e float perché è quello che stavo pensando. La mia risposta non è specifica per Java, anche se pensavo che C ... è inteso come generale. Commento meschino che hai ricevuto.
mrk
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.