Em java, é mais eficiente usar byte ou short em vez de int e float em vez de double?

91

Percebi que sempre usei int e doubles, não importa o quão pequeno ou grande o número precise ser. Portanto, em java, é mais eficiente usar byteou em shortvez de inte em floatvez de double?

Portanto, suponha que tenho um programa com muitos ints e duplos. Valeria a pena examinar e mudar meus ints para bytes ou shorts se eu soubesse que o número caberia?

Eu sei que o java não tem tipos não assinados, mas há algo extra que eu pudesse fazer se soubesse que o número seria positivo apenas?

Por eficiente, quero dizer principalmente processamento. Eu presumiria que o coletor de lixo seria muito mais rápido se todas as variáveis ​​tivessem metade do tamanho e que os cálculos provavelmente seriam um pouco mais rápidos também. (Acho que, já que estou trabalhando no Android, preciso me preocupar um pouco com a memória RAM também)

(Eu presumo que o coletor de lixo lida apenas com objetos e não primitivos, mas ainda exclui todos os primitivos em objetos abandonados, certo?)

Eu tentei com um pequeno aplicativo Android que tenho, mas realmente não notei nenhuma diferença. (Embora eu não tenha medido nada "cientificamente".)

Estou errado em presumir que deveria ser mais rápido e eficiente? Eu odiaria mudar tudo em um programa enorme para descobrir que perdi meu tempo.

Valeria a pena fazer desde o início quando eu começar um novo projeto? (Quer dizer, acho que qualquer pequena parte ajudaria, mas então, novamente, se sim, por que não parece que alguém faz isso.)

DisibioAaron
fonte

Respostas:

107

Estou errado em presumir que deveria ser mais rápido e eficiente? Eu odiaria mudar tudo em um programa enorme para descobrir que perdi meu tempo.

Resposta curta

Sim, você está errado. Na maioria dos casos, faz pouca diferença em termos de espaço usado.

Não vale a pena tentar otimizar isso ... a menos que você tenha evidências claras de que a otimização é necessária. E se você precisar otimizar o uso de memória de campos de objeto em particular, provavelmente precisará tomar outras medidas (mais eficazes).

Resposta mais longa

A Java Virtual Machine modela pilhas e campos de objeto usando deslocamentos que são (na verdade) múltiplos de um tamanho de célula primitiva de 32 bits. Portanto, quando você declara uma variável local ou campo de objeto como (digamos) a byte, a variável / campo será armazenado em uma célula de 32 bits, assim como um int.

Existem duas exceções a isso:

  • longe os doublevalores requerem 2 células primitivas de 32 bits
  • arrays de tipos primitivos são representados na forma compactada, de forma que (por exemplo) um array de bytes mantenha 4 bytes por palavra de 32 bits.

Portanto, pode valer a pena otimizar o uso de longe double... e grandes matrizes de primitivas. Mas em geral não.

Em teoria, um JIT pode ser capaz de otimizar isso, mas na prática nunca ouvi falar de um JIT que o fizesse. Um impedimento é que o JIT normalmente não pode ser executado até que as instâncias da classe que está sendo compilada tenham sido criadas. Se o JIT otimizasse o layout da memória, você poderia ter dois (ou mais) "sabores" de objeto da mesma classe ... e isso apresentaria enormes dificuldades.


Revisão

Olhando para os resultados do benchmark na resposta de @meriton, parece que usar shorte em bytevez de intincorre em uma penalidade de desempenho para a multiplicação. Na verdade, se você considerar as operações isoladamente, a penalidade é significativa. (Você não deve considerá-los isoladamente ... mas isso é outro tópico.)

Acho que a explicação é que o JIT provavelmente está fazendo as multiplicações usando instruções de multiplicação de 32 bits em cada caso. Mas no caso bytee short, ele executa instruções extras para converter o valor intermediário de 32 bits em um byteou shortem cada iteração do loop. (Em teoria, essa conversão poderia ser feita uma vez no final do loop ... mas duvido que o otimizador consiga descobrir isso.)

De qualquer forma, isso aponta para outro problema com a mudança para shorte bytecomo uma otimização. Poderia fazer desempenho pior ... em um algoritmo que é aritmética e computação intensiva.

Stephen C
fonte
30
+1 não otimize a menos que você tenha evidências claras de um problema de desempenho
Bohemian
Erm, por que a JVM tem que esperar pela compilação JIT para empacotar o layout de memória de uma classe? Uma vez que os tipos de campos são gravados no arquivo de classe, a JVM não poderia escolher um layout de memória no tempo de carregamento da classe e, em seguida, resolver os nomes dos campos como bytes em vez de deslocamentos de palavras?
Meriton
@meriton - Tenho quase certeza de que os layouts de objetos são determinados no momento do carregamento da classe e não mudam depois disso. Veja as "letras miúdas" da minha resposta. Se os layouts de memória reais mudassem quando o código foi JITed, seria realmente difícil para a JVM lidar. (Quando eu disse que o JIT pode otimizar o layout, isso é hipotético e impraticável ... o que poderia explicar por que nunca ouvi falar de um JIT realmente fazendo isso.)
Stephen C
Eu sei. Eu estava apenas tentando apontar que, embora os layouts de memória sejam difíceis de mudar depois que os objetos são criados, uma JVM ainda pode otimizar o layout de memória antes disso, ou seja, no momento do carregamento da classe. Em outras palavras, o fato de que a especificação JVM descreve o comportamento de uma JVM com deslocamentos de palavras não implica necessariamente que uma JVM deva ser implementada dessa maneira - embora provavelmente seja.
Meriton
@meriton - A especificação JVM está falando sobre "deslocamentos de palavra de máquina virtual" dentro de quadros / objetos locais. NÃO é especificado como eles são mapeados para deslocamentos físicos da máquina. Na verdade, ele não pode especificá-lo ... uma vez que pode haver requisitos de alinhamento de campo específicos de hardware.
Stephen C de
29

Isso depende da implementação da JVM, bem como do hardware subjacente. A maioria dos hardwares modernos não busca bytes únicos da memória (ou mesmo do cache de primeiro nível), ou seja, usar os tipos primitivos menores geralmente não reduz o consumo de largura de banda da memória. Da mesma forma, a CPU moderna tem um tamanho de palavra de 64 bits. Eles podem realizar operações em menos bits, mas isso funciona descartando os bits extras, o que também não é mais rápido.

O único benefício é que tipos primitivos menores podem resultar em um layout de memória mais compacto, principalmente ao usar arrays. Isso economiza memória, o que pode melhorar a localidade de referência (reduzindo assim o número de perdas de cache) e reduzir a sobrecarga da coleta de lixo.

De modo geral, entretanto, usar os tipos primitivos menores não é mais rápido.

Para demonstrar isso, observe a seguinte referência:

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 imprime em meu caderno um tanto antigo (adicionando espaços para ajustar colunas):

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 você pode ver, as diferenças de desempenho são mínimas. Otimizar algoritmos é muito mais importante do que a escolha do tipo primitivo.

Meriton
fonte
3
Em vez de dizer "principalmente ao usar matrizes", acho que pode ser mais simples dizer isso shorte bytesão mais eficientes quando armazenadas em matrizes grandes o suficiente para serem importantes (quanto maior a matriz, maior a diferença de eficiência; um byte[2]pode ser mais ou menos eficiente do que um int[2], mas não o suficiente para importar de qualquer maneira), mas os valores individuais são armazenados de forma mais eficiente como int.
supercat
2
O que eu verifiquei: Esses benchmarks sempre usaram um int ('3') como fator ou operando de atribuição (a variante de loop, então convertida). O que fiz foi usar fatores digitados / operandos de atribuição dependendo do tipo de lvalue: int mult 76,481 ns int mult (digitado) 72,581 ns short mult 87,908 ns short mult (digitado) 90,772 ns byte mult 87,859 ns byte mult (digitado) 89,524 ns int [] trav 88,905 ns int [] trav (digitado) 89,126 ns curto [] trav 10,563 ns curto [] trav (digitado) 10,039 ns byte [] trav 8,356 ns byte [] trav (digitado) 8,338 ns Suponho que haja um muito elenco desnecessário. esses testes foram executados em uma guia Android.
Bondax,
5

Usar em bytevez de intpode aumentar o desempenho se você os usar em grande quantidade. Aqui está uma experiência:

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 aula testa a velocidade de criação de um novo TestClass. Cada teste faz isso 20 milhões de vezes e há 50 testes.

Aqui está o 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; 
 }

Eu dirigi a SpeedTestaula e no final consegui isso:

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

Agora estou transformando os ints em bytes no TestClass e executando-o novamente. Aqui está o resultado:

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

Eu acredito que este experimento mostra que se você está criando uma instância de uma grande quantidade de variáveis, usar byte em vez de int pode aumentar a eficiência

WVrock
fonte
4
Observe que esse benchmark mede apenas os custos associados à alocação e construção, e apenas o caso de uma classe com muitos campos individuais. Se operações aritméticas / de atualização foram realizadas nos campos, os resultados de @meriton sugerem que bytepodem ser >> mais lentas << do que int.
Stephen C
Verdade, eu deveria ter formulado melhor para esclarecer.
WVrock
2

byte é geralmente considerado como 8 bits. curto é geralmente considerado como 16 bits.

Em um ambiente "puro", que não é java, já que todas as implementações de bytes e longs, shorts e outras coisas divertidas geralmente ficam escondidas de você, o byte faz melhor uso do espaço.

No entanto, seu computador provavelmente não é de 8 bits e provavelmente não é de 16 bits. isso significa que, para obter 16 ou 8 bits em particular, seria necessário recorrer a "artifícios" que perdem tempo para fingir que tem a capacidade de acessar esses tipos quando necessário.

Neste ponto, depende de como o hardware é implementado. No entanto, pelo que fui ensinado, a melhor velocidade é obtida armazenando coisas em blocos que sejam confortáveis ​​para o uso da CPU. Um processador de 64 bits gosta de lidar com elementos de 64 bits e qualquer coisa menos do que isso geralmente requer "mágica de engenharia" para fingir que gosta de lidar com eles.

Dmitry
fonte
3
Não tenho certeza do que você quer dizer com "mágica da engenharia" ... a maioria / todos os processadores modernos têm instruções rápidas para carregar um byte e estendê-lo de sinal, para armazenar um de um registrador de largura total e para fazer a largura de byte ou aritmética de largura curta em uma parte de um registro de largura total. Se você estivesse certo, faria sentido, quando possível, substituir todos os ints por longos em um processador de 64 bits.
Ed Staub de
Posso imaginar que isso seja verdade. Eu apenas me lembro que no simulador Motorola 68k que usamos, a maioria das operações poderia funcionar com valores de 16 bits, mas não com 32 bits nem 64 bits. Eu estava pensando que isso significava que os sistemas tinham um tamanho de valor preferido que pode buscar de forma otimizada. Embora eu possa imaginar que os processadores modernos de 64 bits podem buscar 8 bits, 16 bits, 32 bits e 64 bits com a mesma facilidade, neste caso não é um problema. Obrigado por apontar isso.
Dmitry
"... é geralmente considerado como ..." - Na verdade, é clara e inequivocamente >> especificado << como esses tamanhos. Em Java. E o contexto dessa questão é Java.
Stephen C
Um grande número de processadores usa o mesmo número de ciclos para manipular e acessar dados que não têm tamanho de palavra, então não vale a pena se preocupar, a menos que você faça medições em uma JVM e plataforma específica.
drrob,
Estou tentando dizer em geral. Dito isso, não tenho certeza sobre o padrão do Java com relação ao tamanho do byte, mas neste ponto estou bastante convencido de que se algum herege decidir bytes que não sejam de 8 bits, o Java não vai querer tocá-los com uma vara de três metros. No entanto, alguns processadores requerem alinhamento multibyte e, se a plataforma Java os suportar, será necessário fazer as coisas mais lentamente para acomodar o tratamento desses tipos menores ou representá-los magicamente com representações maiores do que você solicitou. Que sempre prefere int em relação a outros tipos, pois sempre usa o tamanho favorito do sistema.
Dmitry
2

Uma das razões para short / byte / char ter menos desempenho é a falta de suporte direto para esses tipos de dados. Por suporte direto, isso significa que as especificações JVM não mencionam nenhum conjunto de instruções para esses tipos de dados. Instruções como armazenar, carregar, adicionar etc. têm versões para o tipo de dados int. Mas eles não têm versões para short / byte / char. Por exemplo, considere o código java abaixo:

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

O mesmo é convertido em código de máquina conforme abaixo.

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

Agora, considere alterar int para short conforme abaixo.

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

O código de máquina correspondente mudará da seguinte forma:

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 você pode observar, para manipular o tipo de dados curto, ele ainda está usando a versão de instrução do tipo de dados int e explicitamente convertendo int em curto quando necessário. Agora, devido a isso, o desempenho fica reduzido.

Agora, o motivo citado para não dar suporte direto como segue:

A Java Virtual Machine fornece o suporte mais direto para dados do tipo int. Isso ocorre em parte em antecipação às implementações eficientes das pilhas de operandos e matrizes de variáveis ​​locais da Java Virtual Machine. Também é motivado pela frequência de dados internos em programas típicos. Outros tipos integrais têm menos suporte direto. Não há byte, char ou versões curtas das instruções de armazenamento, carregamento ou adição, por exemplo.

Citado da especificação JVM presente aqui (Página 58).

Manish Bansal
fonte
Esses são bytecodes desmontados; ou seja, instruções virtuais JVM . Eles não são otimizados pelo javaccompilador, e você não pode tirar nenhuma inferência confiável sobre o desempenho do programa na vida real. O compilador JIT compila esses bytecodes em instruções reais da máquina nativa e faz uma otimização bastante séria no processo. Se você deseja analisar o desempenho do código, é necessário examinar as instruções do código nativo. (E é complicado porque você precisa levar em consideração o comportamento de tempo de um pipeline x86_64 de vários estágios.)
Stephen C
Acredito que as especificações java são para os implementadores javac implementar. Portanto, não acho que haja mais otimizações feitas nesse nível. De qualquer forma, também posso estar completamente errado. Compartilhe algum link de referência para apoiar sua declaração.
Manish Bansal
Bem, aqui está um fato para apoiar minha declaração. Você não encontrará nenhum valor de tempo (credível) que diga quantos ciclos de clock cada instrução bytecode da JVM leva. Certamente não publicado pela Oracle ou outros fornecedores JVM. Além disso, leia stackoverflow.com/questions/1397009
Stephen C
Eu encontrei um artigo antigo (2008) onde alguém tentou desenvolver um modelo independente de plataforma para prever o desempenho de sequências de bytecode. Eles afirmam que suas previsões estavam erradas em 25% em comparação com as medições RDTSC ... em um Pentium. E eles estavam executando a JVM com a compilação JIT desativada! Referência: sciencedirect.com/science/article/pii/S1571066108004581
Stephen C
Estou apenas confuso aqui. Minha resposta não apoia os fatos que você declarou na seção de revisitação?
Manish Bansal
0

A diferença é quase imperceptível! É mais uma questão de design, adequação, uniformidade, hábito, etc ... Às vezes é só uma questão de gosto. Quando tudo o que você importa é que seu programa comece a funcionar e substituir um floatpor um intnão prejudicaria a correção, não vejo nenhuma vantagem em escolher um ou outro, a menos que você possa demonstrar que o uso de qualquer um dos tipos altera o desempenho. Ajustar o desempenho com base em tipos que são diferentes em 2 ou 3 bytes é realmente a última coisa com a qual você deve se preocupar; Donald Knuth disse uma vez: "A otimização prematura é a raiz de todos os males" (não tenho certeza se foi ele, edite se você tiver a resposta).

mrk
fonte
5
Nit: A float não pode representar todos os inteiros e uma intlata; nem pode intrepresentar qualquer valor não inteiro que floatpossa. Ou seja, embora todos os valores int sejam um subconjunto de valores longos, um int não é um subconjunto de um float e um float não é um subconjunto de um int.
Espero que o respondente pretendia escrever substituting a float for a double, se for o caso, o respondente deve editar a resposta. Se não, o respondente deve ficar de cabeça baixa de vergonha e voltar ao básico pelos motivos descritos por @pst e por muitos outros motivos.
Marco de alto desempenho
@HighPerformanceMark Não, coloquei int e float porque era isso que estava pensando. Minha resposta não é específica para Java, embora eu estivesse pensando C ... É para ser geral. Comentário maldoso que você tem aí.
mrk