En java, ¿es más eficiente usar byte o short en lugar de int y float en lugar de double?

91

He notado que siempre he usado int y doubles, sin importar cuán pequeño o grande deba ser el número. Entonces, en Java, ¿es más eficiente usar byteo en shortlugar de inty en floatlugar de double?

Así que suponga que tengo un programa con muchas entradas y dobles. ¿Valdría la pena revisar y cambiar mis entradas a bytes o cortos si supiera que el número encajaría?

Sé que Java no tiene tipos sin firmar, pero ¿hay algo adicional que pudiera hacer si supiera que el número solo sería positivo?

Por eficiente me refiero principalmente a procesamiento. Asumiría que el recolector de basura sería mucho más rápido si todas las variables fueran de la mitad del tamaño y que los cálculos probablemente también serían algo más rápidos. (Supongo que, como estoy trabajando en Android, también debo preocuparme un poco por la RAM)

(Asumiría que el recolector de basura solo se ocupa de Objetos y no de primitivos, pero aún elimina todos los primitivos en los objetos abandonados, ¿verdad?)

Lo probé con una pequeña aplicación para Android que tengo, pero realmente no noté ninguna diferencia. (Aunque no medí nada "científicamente").

¿Me equivoco al suponer que debería ser más rápido y más eficiente? Odiaría pasar y cambiar todo en un programa masivo para descubrir que perdí el tiempo.

¿Valdría la pena hacerlo desde el principio cuando empiezo un nuevo proyecto? (Quiero decir, creo que todo ayudaría, pero de nuevo, si es así, ¿por qué parece que nadie lo hace?)

DisibioAaron
fuente

Respuestas:

107

¿Me equivoco al suponer que debería ser más rápido y más eficiente? Odiaría pasar y cambiar todo en un programa masivo para descubrir que perdí el tiempo.

Respuesta corta

Sí, estás equivocado. En la mayoría de los casos, hay poca diferencia en términos de espacio utilizado.

No vale la pena intentar optimizar esto ... a menos que tenga una evidencia clara de que la optimización es necesaria. Y si necesita optimizar el uso de memoria de los campos de objeto en particular, probablemente necesitará tomar otras medidas (más efectivas).

Respuesta más larga

La máquina virtual Java modela pilas y campos de objetos utilizando compensaciones que son (en efecto) múltiplos de un tamaño de celda primitiva de 32 bits. Entonces, cuando declara una variable local o un campo de objeto como (digamos) a byte, la variable / campo se almacenará en una celda de 32 bits, como un int.

Hay dos excepciones a esto:

  • longy los doublevalores requieren 2 celdas primitivas de 32 bits
  • las matrices de tipos primitivos se representan en forma empaquetada, de modo que (por ejemplo) una matriz de bytes contiene 4 bytes por palabra de 32 bits.

Por lo tanto, podría valer la pena optimizar el uso de longy double... y grandes conjuntos de primitivas. Pero en general no.

En teoría, un JIT podría optimizar esto, pero en la práctica nunca he oído hablar de un JIT que lo haga. Un impedimento es que el JIT normalmente no puede ejecutarse hasta que se hayan creado instancias de la clase que se está compilando. Si el JIT optimizara el diseño de la memoria, podría tener dos (o más) "sabores" de objeto de la misma clase ... y eso presentaría enormes dificultades.


Revisitación

Al observar los resultados de referencia en la respuesta de @ meriton, parece que usar shorty en bytelugar de intincurre en una penalización de rendimiento para la multiplicación. De hecho, si considera las operaciones de forma aislada, la penalización es significativa. (No debería considerarlos de forma aislada ... pero ese es otro tema).

Creo que la explicación es que JIT probablemente está haciendo las multiplicaciones usando instrucciones de multiplicación de 32 bits en cada caso. Pero en el caso bytey short, ejecuta instrucciones adicionales para convertir el valor intermedio de 32 bits en a byteo shorten cada iteración del ciclo. (En teoría, esa conversión podría realizarse una vez al final del ciclo ... pero dudo que el optimizador pueda resolver eso).

De todos modos, esto hace punto a otro problema con el cambio a short, y bytecomo una optimización. Se podría hacer un rendimiento peor ... en un algoritmo que es la aritmética y cálculo intensivo.

Stephen C
fuente
30
+1 no optimice a menos que tenga una clara evidencia de un problema de rendimiento
Bohemian
Erm, ¿por qué la JVM tiene que esperar a que la compilación JIT empaque el diseño de memoria de una clase? Dado que los tipos de campos se escriben en el archivo de clase, ¿no podría la JVM elegir un diseño de memoria en el momento de la carga de la clase y luego resolver los nombres de campo como bytes en lugar de compensaciones de palabras?
meriton
@meriton: estoy bastante seguro de que los diseños de los objetos se determinan en el momento de la carga de la clase y no cambian después de eso. Vea la parte de "letra pequeña" de mi respuesta. Si los diseños de memoria reales cambiaran cuando el código fue JITed, sería realmente difícil de manejar para la JVM. (Cuando dije que el JIT podría optimizar el diseño, eso es hipotético y poco práctico ... lo que podría explicar por qué nunca he oído hablar de un JIT que realmente lo haga).
Stephen C
Lo sé. Solo estaba tratando de señalar que aunque los diseños de memoria son difíciles de cambiar una vez que se crean los objetos, una JVM aún podría optimizar el diseño de memoria antes de eso, es decir, en el momento de carga de la clase. Dicho de otra manera, que la especificación de JVM describa el comportamiento de una JVM con compensaciones de palabras no implica necesariamente que una JVM deba implementarse de esa manera, aunque lo más probable es que lo sea.
meriton
@meriton: la especificación JVM habla de "delitos de palabras de máquinas virtuales" dentro de marcos / objetos locales. NO se especifica cómo se asignan a las compensaciones físicas de la máquina. De hecho, no puede especificarlo ... ya que puede haber requisitos de alineación de campo específicos del hardware.
Stephen C
29

Eso depende de la implementación de la JVM, así como del hardware subyacente. La mayor parte del hardware moderno no obtendrá bytes individuales de la memoria (ni siquiera de la caché de primer nivel), es decir, el uso de tipos primitivos más pequeños generalmente no reduce el consumo de ancho de banda de la memoria. Asimismo, las CPU modernas tienen un tamaño de palabra de 64 bits. Pueden realizar operaciones en menos bits, pero eso funciona descartando los bits adicionales, que tampoco es más rápido.

El único beneficio es que los tipos primitivos más pequeños pueden dar como resultado un diseño de memoria más compacto, sobre todo cuando se utilizan matrices. Esto ahorra memoria, lo que puede mejorar la localidad de referencia (reduciendo así el número de pérdidas de caché) y reducir la sobrecarga de recolección de basura.

Sin embargo, en términos generales, usar los tipos primitivos más pequeños no es más rápido.

Para demostrarlo, observe el siguiente punto de referencia:

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

que se imprime en mi cuaderno algo viejo (agregando espacios para ajustar columnas):

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

Como puede ver, las diferencias de rendimiento son bastante menores. La optimización de los algoritmos es mucho más importante que la elección del tipo primitivo.

meriton
fuente
3
En lugar de decir "más notablemente cuando se usan matrices", creo que podría ser más sencillo decir eso shorty byteson más eficientes cuando se almacenan en matrices que son lo suficientemente grandes como para importar (cuanto más grande es la matriz, mayor es la diferencia de eficiencia; a byte[2]podría ser más o menos eficiente que an int[2], pero no lo suficiente como para importar de cualquier manera), pero que los valores individuales se almacenan de manera más eficiente como int.
supercat
2
Lo que verifiqué: Esos puntos de referencia siempre usaban un int ('3') como factor o operando de asignación (la variante de bucle, luego convertida). Lo que hice fue utilizar factores tipados / operandos de asignación según el tipo de 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 (escrito) 89.126 ns corto [] trav 10.563 ns corto [] trav (escrito) 10.039 ns byte [] trav 8.356 ns byte [] trav (escrito) 8.338 ns Supongo que hay un mucho casting innecesario. esas pruebas se ejecutaron en una pestaña de Android.
Bondax
5

Usar en bytelugar de intpuede aumentar el rendimiento si los usa en una gran cantidad. Aquí hay un experimento:

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


}

}

Esta clase prueba la velocidad de creación de un nuevo TestClass. Cada prueba lo hace 20 millones de veces y hay 50 pruebas.

Aquí está 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; 
 }

Ejecuté la SpeedTestclase y al final obtuve esto:

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

Ahora estoy cambiando las entradas en bytes en TestClass y ejecutándolo de nuevo. Aquí está el resultado:

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

Creo que este experimento muestra que si está instalando una gran cantidad de variables, el uso de bytes en lugar de int puede aumentar la eficiencia.

WVrock
fuente
4
Tenga en cuenta que este punto de referencia solo mide los costos asociados con la asignación y la construcción, y solo el caso de una clase con muchos campos individuales. Si se realizaron operaciones aritméticas / de actualización en los campos, los resultados de @ meriton sugieren que bytepodría ser >> más lento << que int.
Stephen C
Es cierto que debería haberlo redactado mejor para aclararlo.
WVrock
2

Por lo general, se considera que un byte es de 8 bits. generalmente se considera que el corto es de 16 bits.

En un entorno "puro", que no es Java, ya que todas las implementaciones de bytes y longs, cortos y otras cosas divertidas generalmente están ocultas para usted, byte hace un mejor uso del espacio.

Sin embargo, su computadora probablemente no sea de 8 bits y probablemente no sea de 16 bits. esto significa que para obtener 16 u 8 bits en particular, necesitaría recurrir a "trucos" que hacen perder tiempo para pretender que tiene la capacidad de acceder a esos tipos cuando sea necesario.

En este punto, depende de cómo se implemente el hardware. Sin embargo, según me han enseñado, la mejor velocidad se logra almacenando cosas en fragmentos que sean cómodos de usar para su CPU. A un procesador de 64 bits le gusta lidiar con elementos de 64 bits, y cualquier cosa menos que eso a menudo requiere "magia de ingeniería" para fingir que le gusta lidiar con ellos.

Dmitry
fuente
3
No estoy seguro de lo que quiere decir con "magia de ingeniería" ... la mayoría / todos los procesadores modernos tienen instrucciones rápidas para cargar un byte y extenderlo con el signo, para almacenar uno desde un registro de ancho completo y para hacer el ancho de byte o aritmética de ancho corto en una parte de un registro de ancho completo. Si tiene razón, tendría sentido, siempre que sea posible, reemplazar todos los ints con longs en un procesador de 64 bits.
Ed Staub
Puedo imaginar que eso es cierto. Solo recuerdo que en el simulador Motorola 68k que usamos, la mayoría de las operaciones podían funcionar con valores de 16 bits, pero no con 32 ni 64 bits. Estaba pensando que esto significaba que los sistemas tenían un tamaño de valor preferido que puede obtener de manera óptima. Aunque puedo imaginar que los procesadores modernos de 64 bits pueden obtener 8 bits, 16 bits, 32 bits y 64 bits con la misma facilidad, en este caso no es un problema. Gracias por señalar eso.
Dmitry
"... generalmente se considera que es ..." - En realidad, se especifica << de forma clara e inequívoca >> como esos tamaños. En Java. Y el contexto de esta pregunta es Java.
Stephen C
Una gran cantidad de procesadores incluso usa la misma cantidad de ciclos para manipular y acceder a datos que no tienen el tamaño de una palabra, por lo que realmente no vale la pena preocuparse a menos que mida en una plataforma y JVM en particular.
drrob
Estoy tratando de decir con toda generalidad. Dicho esto, en realidad no estoy seguro del estándar de Java con respecto al tamaño de bytes, pero en este punto estoy bastante convencido de que si algún hereje decide que no sean bytes de 8 bits, Java no querrá tocarlos con un poste de diez pies. Sin embargo, algunos procesadores requieren una alineación multibyte, y si la plataforma Java los admite, deberá hacer las cosas más lentamente para adaptarse a estos tipos más pequeños, o representarlos mágicamente con representaciones más grandes de las que solicitó. Eso siempre prefiere int sobre otros tipos ya que siempre usa el tamaño favorito del sistema.
Dmitry
2

Una de las razones por las que short / byte / char tiene menos rendimiento es la falta de soporte directo para estos tipos de datos. Por soporte directo, es decir, las especificaciones de JVM no mencionan ningún conjunto de instrucciones para estos tipos de datos. Las instrucciones como almacenar, cargar, agregar, etc. tienen versiones para el tipo de datos int. Pero no tienen versiones para short / byte / char. Por ejemplo, considere el siguiente código java:

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

Lo mismo se convierte en código de máquina como se muestra a continuación.

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

Ahora, considere cambiar int a short como se muestra a continuación.

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

El código de máquina correspondiente cambiará de la siguiente manera:

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

Como puede observar, para manipular el tipo de datos corto, todavía se usa la versión de instrucción de tipo de datos int y convierte explícitamente int en corto cuando es necesario. Ahora, debido a esto, el rendimiento se reduce.

Ahora, la razón citada para no brindar apoyo directo de la siguiente manera:

La máquina virtual Java proporciona el soporte más directo para datos de tipo int. Esto se debe en parte a la anticipación de implementaciones eficientes de las pilas de operandos y las matrices de variables locales de la máquina virtual Java. También está motivado por la frecuencia de datos int en programas típicos. Otros tipos integrales tienen un apoyo menos directo. No hay versiones cortas, de bytes o de caracteres de las instrucciones de almacenamiento, carga o adición, por ejemplo.

Citado de la especificación JVM presente aquí (Página 58).

Manish Bansal
fuente
Estos son códigos de bytes desensamblados; es decir, instrucciones virtuales JVM . No están optimizados por el javaccompilador y no se pueden extraer inferencias fiables sobre cómo funcionará el programa en la vida real. El compilador JIT compila estos códigos de bytes en las instrucciones nativas reales de la máquina y realiza una optimización bastante seria en el proceso. Si desea analizar el rendimiento del código, debe examinar las instrucciones del código nativo. (Y es complicado porque debe tener en cuenta el comportamiento de sincronización de una canalización x86_64 de varias etapas).
Stephen C
Creo que las especificaciones de java son para que las implementen los implementadores de javac. Así que no creo que se hagan más optimizaciones a ese nivel. De todos modos, yo también podría estar completamente equivocado. Comparta algún enlace de referencia para respaldar su declaración.
Manish Bansal
Bueno, aquí hay un hecho para respaldar mi afirmación. No encontrará cifras de tiempo (creíbles) que le indiquen cuántos ciclos de reloj toma cada instrucción de código de bytes de JVM. Ciertamente no publicado por Oracle u otros proveedores de JVM. Además, lea stackoverflow.com/questions/1397009
Stephen C
Encontré un artículo antiguo (2008) en el que alguien intentó desarrollar un modelo independiente de plataforma para predecir el rendimiento de las secuencias de códigos de bytes. Afirman que sus predicciones estaban desviadas en un 25% en comparación con las mediciones RDTSC ... en un Pentium. ¡Y estaban ejecutando la JVM con la compilación JIT desactivada! Referencia: sciencedirect.com/science/article/pii/S1571066108004581
Stephen C
Estoy confundido aquí. ¿No respalda mi respuesta los hechos que declaró en la sección de revisión?
Manish Bansal
0

¡La diferencia apenas se nota! Es más una cuestión de diseño, adecuación, uniformidad, hábito, etc ... A veces es solo cuestión de gustos. Cuando lo único que le importa es que su programa se ponga en marcha y se sustituya floatpor un intno daña la corrección, no veo ninguna ventaja en elegir uno u otro a menos que pueda demostrar que el uso de cualquiera de los tipos altera el rendimiento. El ajuste del rendimiento basado en tipos que son diferentes en 2 o 3 bytes es realmente lo último que debería preocuparle; Donald Knuth dijo una vez: "La optimización prematura es la raíz de todos los males" (no estoy seguro de que fuera él, edite si tiene la respuesta).

mrk
fuente
5
Nit: A float no puede representar todos los números enteros de una intlata; ni puede intrepresentar ningún valor no entero que floatpueda. Es decir, mientras que todos los valores int son un subconjunto de valores largos, un int no es un subconjunto de un float y un float no es un subconjunto de un int.
Espero que el respondedor tenga la intención de escribir substituting a float for a double, si es así, el respondedor debería editar la respuesta. Si no responde, debería colgar la cabeza avergonzado y volver a lo básico por las razones descritas por @pst y por muchas otras razones.
Marca de alto rendimiento
@HighPerformanceMark No, puse int y float porque eso es lo que estaba pensando. Mi respuesta no es específica de Java, aunque estaba pensando en C ... Está destinado a ser general. Comentario malo que tienes allí.
mrk