Desempenho Java - Java performance

No desenvolvimento de software , a linguagem de programação Java foi historicamente considerada mais lenta do que as linguagens tipadas de 3ª geração mais rápidas , como C e C ++ . O principal motivo é o design de uma linguagem diferente, em que, após a compilação, os programas Java são executados em uma máquina virtual Java (JVM) ao invés de diretamente no processador do computador como código nativo , como fazem os programas C e C ++. O desempenho era motivo de preocupação porque muitos softwares de negócios foram escritos em Java depois que a linguagem rapidamente se tornou popular no final dos anos 1990 e início dos anos 2000.

Desde o final da década de 1990, a velocidade de execução de programas Java melhorou significativamente com a introdução da compilação just-in-time (JIT) (em 1997 para Java 1.1 ), a adição de recursos de linguagem que suportam uma melhor análise de código e otimizações na JVM (como como HotSpot se tornando o padrão para JVM da Sun em 2000). A execução de bytecode Java em hardware, como o oferecido pelo Jazelle da ARM , também foi explorada para oferecer melhorias de desempenho significativas.

O desempenho de um programa Java compilado por bytecode Java depende de quão otimamente suas tarefas são gerenciadas pela máquina virtual Java host (JVM), e quão bem a JVM explora os recursos do hardware do computador e do sistema operacional (SO) ao fazê-lo. Assim, qualquer teste de desempenho ou comparação de Java deve sempre relatar a versão, fornecedor, sistema operacional e arquitetura de hardware da JVM usada. De maneira semelhante, o desempenho do programa equivalente compilado nativamente dependerá da qualidade de seu código de máquina gerado, então o teste ou comparação também deve relatar o nome, versão e fornecedor do compilador usado e suas diretivas de otimização de compilador ativadas .

Métodos de otimização de máquina virtual

Muitas otimizações melhoraram o desempenho da JVM ao longo do tempo. No entanto, embora Java tenha sido frequentemente a primeira máquina virtual a implementá-los com sucesso, eles também têm sido usados ​​em outras plataformas semelhantes.

Compilação just-in-time

Os primeiros JVMs sempre interpretavam os bytecodes Java . Isso teve uma grande penalidade de desempenho entre um fator de 10 e 20 para Java versus C em aplicativos médios. Para combater isso, um compilador just-in-time (JIT) foi introduzido no Java 1.1. Devido ao alto custo de compilação, um sistema adicionado chamado HotSpot foi introduzido no Java 1.2 e se tornou o padrão no Java 1.3. Usando essa estrutura, a máquina virtual Java analisa continuamente o desempenho do programa para pontos de acesso que são executados com freqüência ou repetidamente. Em seguida, eles são direcionados para otimização , levando a execução de alto desempenho com um mínimo de sobrecarga para código menos crítico de desempenho. Alguns benchmarks mostram um ganho de velocidade de 10 vezes por esse meio. No entanto, devido a restrições de tempo, o compilador não pode otimizar totalmente o programa e, portanto, o programa resultante é mais lento do que as alternativas de código nativo.

Otimização adaptativa

A otimização adaptativa é um método em ciência da computação que realiza recompilação dinâmica de partes de um programa com base no perfil de execução atual. Com uma implementação simples, um otimizador adaptável pode simplesmente fazer uma troca entre as instruções de compilação e interpretação just-in-time. Em outro nível, a otimização adaptativa pode explorar as condições de dados locais para otimizar ramificações externas e usar a expansão em linha.

Uma máquina virtual Java como o HotSpot também pode desotimizar o código anteriormente JITed. Isso permite a execução de otimizações agressivas (e potencialmente inseguras), ao mesmo tempo em que pode desotimizar o código posteriormente e voltar para um caminho seguro.

Coleta de lixo

As Java Virtual Machines (JVMs) 1.0 e 1.1 usavam um coletor de varredura de marcação , que poderia fragmentar o heap após uma coleta de lixo. A partir do Java 1.2, as JVMs mudaram para um coletor de geração , que tem um comportamento de desfragmentação muito melhor. As JVMs modernas usam uma variedade de métodos que melhoraram ainda mais o desempenho da coleta de lixo .

Outros métodos de otimização

Oops compactado

Oops compactados permitem que o Java 5.0+ resolva até 32 GB de heap com referências de 32 bits. Java não suporta acesso a bytes individuais, apenas objetos que são alinhados por 8 bytes por padrão. Por causa disso, os 3 bits mais baixos de uma referência de heap sempre serão 0. Reduzindo a resolução das referências de 32 bits para blocos de 8 bytes, o espaço endereçável pode ser aumentado para 32 GB. Isso reduz significativamente o uso de memória em comparação com o uso de referências de 64 bits, pois o Java usa referências muito mais do que algumas linguagens como C ++. Java 8 suporta alinhamentos maiores, como alinhamento de 16 bytes para suportar até 64 GB com referências de 32 bits.

Verificação de bytecode dividido

Antes de executar uma classe , o Sun JVM verifica seus bytecodes Java (consulte o verificador de bytecode ). Esta verificação é realizada de forma preguiçosa: os bytecodes das classes só são carregados e verificados quando a classe específica é carregada e preparada para uso, e não no início do programa. No entanto, como as bibliotecas de classes Java também são classes Java regulares, elas também devem ser carregadas quando são usadas, o que significa que o tempo de inicialização de um programa Java costuma ser maior do que para programas C ++ , por exemplo.

Um método denominado verificação em tempo parcial , introduzido pela primeira vez na plataforma Java, Micro Edition (J2ME), é usado na JVM desde a versão 6 do Java . Ele divide a verificação do bytecode Java em duas fases:

  • Tempo de design - ao compilar uma classe da fonte ao bytecode
  • Runtime - ao carregar uma classe.

Na prática, esse método funciona capturando o conhecimento que o compilador Java tem do fluxo da classe e anotando os bytecodes do método compilado com uma sinopse das informações do fluxo da classe. Isso não torna a verificação do tempo de execução consideravelmente menos complexa, mas permite alguns atalhos.

Análise de escape e engrossamento de bloqueio

Java é capaz de gerenciar multithreading no nível da linguagem. Multithreading é um método que permite que os programas executem vários processos simultaneamente, produzindo programas mais rápidos em sistemas de computador com vários processadores ou núcleos. Além disso, um aplicativo multithread pode permanecer responsivo à entrada, mesmo durante a execução de tarefas de longa duração.

No entanto, os programas que usam multithreading precisam tomar cuidado extra com os objetos compartilhados entre as threads, bloqueando o acesso a métodos ou blocos compartilhados quando eles são usados ​​por uma das threads. O bloqueio de um bloco ou objeto é uma operação demorada devido à natureza da operação envolvida no nível do sistema operacional subjacente (consulte o controle de simultaneidade e a granularidade do bloqueio ).

Como a biblioteca Java não sabe quais métodos serão usados ​​por mais de um thread, a biblioteca padrão sempre bloqueia blocos quando necessário em um ambiente multithread.

Antes do Java 6, a máquina virtual sempre bloqueava objetos e blocos quando solicitada pelo programa, mesmo que não houvesse risco de um objeto ser modificado por duas threads diferentes ao mesmo tempo. Por exemplo, neste caso, um local vector foi bloqueado antes de cada uma das operações de adição para garantir que não seria modificado por outros threads (o vetor é sincronizado), mas como é estritamente local para o método, isso é desnecessário:

public String getNames() {
     Vector<String> v = new Vector<>();
     v.add("Me");
     v.add("You");
     v.add("Her");
     return v.toString();
}

A partir do Java 6, os blocos de código e objetos são bloqueados apenas quando necessário, portanto, no caso acima, a máquina virtual não bloquearia o objeto Vector de forma alguma.

Desde a versão 6u23, o Java inclui suporte para análise de escape.

Melhorias de alocação de registro

Antes do Java 6 , a alocação de registros era muito primitiva na máquina virtual do cliente (eles não viviam em blocos ), o que era um problema em projetos de CPU que tinham menos registros de processador disponíveis, como no x86s . Se não houver mais registros disponíveis para uma operação, o compilador deve copiar do registro para a memória (ou memória para registrar), o que leva tempo (os registros são significativamente mais rápidos de acessar). No entanto, a máquina virtual do servidor usava um alocador de gráfico de cores e não tinha esse problema.

Uma otimização da alocação de registro foi introduzida no JDK 6 da Sun; foi então possível utilizar os mesmos registradores em blocos (quando aplicável), reduzindo os acessos à memória. Isso levou a um ganho de desempenho relatado de cerca de 60% em alguns benchmarks.

Compartilhamento de dados de classe

O compartilhamento de dados de classe (denominado CDS pela Sun) é um mecanismo que reduz o tempo de inicialização de aplicativos Java e também reduz o consumo de memória . Quando o JRE é instalado, o instalador carrega um conjunto de classes do arquivo JAR do sistema (o arquivo JAR que contém toda a biblioteca de classes Java, chamado rt.jar) em uma representação interna privada e despeja essa representação em um arquivo, chamado de "arquivo compartilhado". Durante as chamadas JVM subsequentes, esse archive compartilhado é mapeado na memória , economizando o custo de carregamento dessas classes e permitindo que muitos dos metadados da JVM para essas classes sejam compartilhados entre vários processos JVM.

A melhoria correspondente no tempo de inicialização é mais óbvia para programas pequenos.

Histórico de melhorias de desempenho

Além das melhorias listadas aqui, cada versão do Java introduziu muitas melhorias de desempenho na JVM e na interface de programação de aplicativos (API) Java .

JDK 1.1.6: Primeira compilação just-in-time ( compilador JIT da Symantec )

J2SE 1.2: Uso de um coletor de geração .

J2SE 1.3: Compilação just-in-time por HotSpot .

J2SE 1.4: Veja aqui , para uma visão geral da Sun das melhorias de desempenho entre as versões 1.3 e 1.4.

Java SE 5.0: compartilhamento de dados de classe

Java SE 6:

Outras melhorias:

  • Melhorias na velocidade do pipeline Java OpenGL Java 2D
  • O desempenho do Java 2D também melhorou significativamente no Java 6

Consulte também 'Visão geral da Sun sobre melhorias de desempenho entre Java 5 e Java 6'.

Java SE 6 Atualização 10

  • O Java Quick Starter reduz o tempo de inicialização do aplicativo ao pré-carregar parte dos dados JRE na inicialização do sistema operacional no cache de disco .
  • Partes da plataforma necessárias para executar um aplicativo acessado da web quando o JRE não está instalado agora são baixadas primeiro. O JRE completo tem 12 MB, um aplicativo Swing típico só precisa fazer download de 4 MB para iniciar. As partes restantes são baixadas em segundo plano.
  • O desempenho gráfico no Windows melhorou com o uso extensivo de Direct3D por padrão e sombreadores na unidade de processamento gráfico (GPU) para acelerar operações Java 2D complexas .

Java 7

Diversas melhorias de desempenho foram lançadas para Java 7: Futuras melhorias de desempenho estão planejadas para uma atualização de Java 6 ou Java 7:

  • Fornecer suporte JVM para linguagens de programação dinâmicas , seguindo o trabalho de prototipagem atualmente realizado na Máquina Da Vinci (Máquina Virtual Multi-Linguagem),
  • Aprimore a biblioteca de simultaneidade existente, gerenciando a computação paralela em processadores multi-core ,
  • Permita que a JVM use os compiladores JIT do cliente e do servidor na mesma sessão com um método chamado compilação em camadas:
    • O cliente seria usado na inicialização (porque é bom na inicialização e para pequenos aplicativos),
    • O servidor seria usado para execução de longo prazo do aplicativo (porque supera o compilador do cliente para isso).
  • Substitua o coletor de lixo de baixa pausa concorrente existente (também chamado de coletor de varredura de marcação simultânea (CMS)) por um novo coletor chamado Garbage First (G1) para garantir pausas consistentes ao longo do tempo.

Comparação com outras línguas

Comparar objetivamente o desempenho de um programa Java e um equivalente escrito em outra linguagem, como C ++, precisa de um benchmark construído de maneira cuidadosa e cuidadosa que compare programas que completam tarefas idênticas. A plataforma de destino do compilador de bytecode Java é a plataforma Java , e o bytecode é interpretado ou compilado em código de máquina pela JVM. Outros compiladores quase sempre visam uma plataforma de hardware e software específica, produzindo código de máquina que permanecerá praticamente inalterado durante a execução. Cenários muito diferentes e difíceis de comparar surgem dessas duas abordagens diferentes: compilações e recompilações estáticas vs. dinâmicas , a disponibilidade de informações precisas sobre o ambiente de execução e outros.

O Java geralmente é compilado just-in-time no tempo de execução pela máquina virtual Java , mas também pode ser compilado antecipadamente , como o C ++. Quando compilados just-in-time, os micro-benchmarks do The Computer Language Benchmarks Game indicam o seguinte sobre seu desempenho:

  • mais lento do que linguagens compiladas, como C ou C ++ ,
  • semelhante a outras linguagens compiladas just-in-time, como C # ,
  • muito mais rápido do que as linguagens sem um compilador de código nativo eficaz ( JIT ou AOT ), como Perl , Ruby , PHP e Python .

Velocidade do programa

Os benchmarks geralmente medem o desempenho de pequenos programas numericamente intensivos. Em alguns programas raros da vida real, o Java supera o C. Um exemplo é o benchmark do Jake2 (um clone do Quake II escrito em Java, traduzindo o código GPL C original ). A versão Java 5.0 tem um desempenho melhor em algumas configurações de hardware do que sua contraparte C. Embora não seja especificado como os dados foram medidos (por exemplo, se o executável Quake II original compilado em 1997 foi usado, o que pode ser considerado ruim, pois os compiladores C atuais podem alcançar melhores otimizações para Quake), ele observa como o mesmo código-fonte Java pode ter um grande aumento de velocidade apenas atualizando a VM, algo impossível de se conseguir com uma abordagem 100% estática.

Para outros programas, a contraparte C ++ pode, e geralmente o faz, rodar significativamente mais rápido do que o equivalente em Java. Um benchmark realizado pelo Google em 2011 mostrou um fator 10 entre C ++ e Java. No outro extremo, um benchmark acadêmico realizado em 2012 com um algoritmo de modelagem 3D mostrou que o Java 6 JVM era de 1,09 a 1,91 vezes mais lento do que o C ++ no Windows.

Algumas otimizações que são possíveis em Java e linguagens semelhantes podem não ser possíveis em certas circunstâncias em C ++:

  • O uso de ponteiros estilo C pode dificultar a otimização em linguagens que suportam ponteiros,
  • O uso de métodos de análise de escape é limitado em C ++ , por exemplo, porque um compilador C ++ nem sempre sabe se um objeto será modificado em um determinado bloco de código devido a ponteiros ,
  • Java pode acessar métodos de instância derivados mais rápido do que C ++ pode acessar métodos virtuais derivados devido à consulta extra de tabela virtual do C ++. No entanto, os métodos não virtuais em C ++ não sofrem de gargalos de desempenho da tabela v e, portanto, exibem desempenho semelhante ao Java.

A JVM também é capaz de realizar otimizações específicas do processador ou expansão em linha . E, a capacidade de desotimizar código já compilado ou embutido às vezes permite que ele execute otimizações mais agressivas do que aquelas executadas por linguagens tipadas estaticamente quando funções de biblioteca externa estão envolvidas.

Os resultados para microbenchmarks entre Java e C ++ dependem altamente de quais operações são comparadas. Por exemplo, ao comparar com Java 5.0:


Notas

Desempenho multi-core

A escalabilidade e o desempenho de aplicativos Java em sistemas multi-core são limitados pela taxa de alocação de objetos. Esse efeito às vezes é chamado de "parede de alocação". No entanto, na prática, os algoritmos modernos do coletor de lixo usam vários núcleos para realizar a coleta de lixo, o que até certo ponto alivia esse problema. Alguns coletores de lixo são relatados para sustentar taxas de alocação de mais de um gigabyte por segundo, e existem sistemas baseados em Java que não têm problemas de escalonamento para várias centenas de núcleos de CPU e heaps com várias centenas de GB.

O gerenciamento automático de memória em Java permite o uso eficiente de estruturas de dados sem bloqueio e imutáveis ​​que são extremamente difíceis ou às vezes impossíveis de implementar sem algum tipo de coleta de lixo. Java oferece várias dessas estruturas de alto nível em sua biblioteca padrão no pacote java.util.concurrent, enquanto muitas linguagens historicamente usadas para sistemas de alto desempenho como C ou C ++ ainda não as possuem.

Tempo de inicialização

O tempo de inicialização do Java geralmente é muito mais lento do que muitas linguagens, incluindo C , C ++ , Perl ou Python , porque muitas classes (e antes de todas as classes das bibliotecas de classes da plataforma ) devem ser carregadas antes de serem usadas.

Quando comparado com tempos de execução populares semelhantes, para pequenos programas executados em uma máquina Windows, o tempo de inicialização parece ser semelhante ao do Mono e um pouco mais lento do que o do .NET .

Parece que muito do tempo de inicialização é devido a operações vinculadas de entrada-saída (IO), em vez de inicialização da JVM ou carregamento de classe (o arquivo de dados de classe rt.jar sozinho tem 40 MB e a JVM deve buscar muitos dados neste grande arquivo) . Alguns testes mostraram que, embora o novo método de verificação de bytecode dividido tenha melhorado o carregamento de classes em cerca de 40%, ele percebeu apenas 5% de melhoria na inicialização para programas grandes.

Embora seja uma pequena melhoria, é mais visível em pequenos programas que realizam uma operação simples e depois saem, porque o carregamento de dados da plataforma Java pode representar muitas vezes a carga da operação real do programa.

A partir do Java SE 6 Update 10, o Sun JRE vem com um Quick Starter que pré-carrega os dados da classe na inicialização do sistema operacional para obter dados do cache de disco em vez do disco.

A Excelsior JET aborda o problema pelo outro lado. Seu Startup Optimizer reduz a quantidade de dados que devem ser lidos do disco na inicialização do aplicativo e torna as leituras mais sequenciais.

Em novembro de 2004, Nailgun , um "cliente, protocolo e servidor para executar programas Java a partir da linha de comando sem incorrer na sobrecarga de inicialização da JVM" foi lançado publicamente. introduzindo pela primeira vez uma opção para scripts de usar um JVM como um daemon , para executar um ou mais aplicativos Java sem sobrecarga de inicialização de JVM. O daemon Nailgun é inseguro: "todos os programas são executados com as mesmas permissões do servidor". Onde a segurança multiusuário é necessária, Nailgun é inadequada sem precauções especiais. Scripts em que a inicialização de JVM por aplicativo domina o uso de recursos, veja melhorias de desempenho de tempo de execução de uma a duas ordens de magnitude .

Uso de memória

O uso de memória Java é muito maior do que o uso de memória C ++ porque:

  • Há uma sobrecarga de 8 bytes para cada objeto e 12 bytes para cada array em Java. Se o tamanho de um objeto não for um múltiplo de 8 bytes, ele será arredondado para o próximo múltiplo de 8. Isso significa que um objeto contendo um campo de byte ocupa 16 bytes e precisa de uma referência de 4 bytes. C ++ também aloca um ponteiro (geralmente 4 ou 8 bytes) para cada objeto cuja classe declara direta ou indiretamente funções virtuais .
  • A falta de aritmética de endereço torna a criação de contêineres com eficiência de memória, como estruturas bem espaçadas e listas vinculadas XOR , atualmente impossível ( o projeto OpenJDK Valhalla visa mitigar esses problemas, embora não pretenda introduzir a aritmética de ponteiros; isso não pode ser feito em um ambiente de coleta de lixo).
  • Ao contrário de malloc e new, a sobrecarga média de desempenho da coleta de lixo assintoticamente se aproxima de zero (mais precisamente, um ciclo de CPU) conforme o tamanho do heap aumenta.
  • Partes da Biblioteca de Classes Java devem ser carregadas antes da execução do programa (pelo menos as classes usadas em um programa). Isso leva a uma sobrecarga de memória significativa para aplicativos pequenos.
  • As recompilações binárias e nativas do Java normalmente estarão na memória.
  • A máquina virtual usa memória substancial.
  • Em Java, um objeto composto (classe A que usa instâncias de B e C) é criado usando referências a instâncias alocadas de B e C. Em C ++ o custo de memória e desempenho desses tipos de referências pode ser evitado quando a instância de B e / ou C existe dentro de A.

Na maioria dos casos, um aplicativo C ++ consumirá menos memória do que um aplicativo Java equivalente devido à grande sobrecarga da máquina virtual Java, carregamento de classe e redimensionamento automático da memória. Para programas nos quais a memória é um fator crítico para escolher entre linguagens e ambientes de tempo de execução, uma análise de custo / benefício é necessária.

Funções trigonométricas

O desempenho de funções trigonométricas é ruim em comparação com C, porque Java tem especificações rígidas para os resultados de operações matemáticas, que podem não corresponder à implementação de hardware subjacente. No subconjunto de ponto flutuante x87 , Java desde 1.4 faz redução de argumento para sin e cos no software, causando um grande impacto no desempenho para valores fora do intervalo. O JDK (11 e acima) tem um progresso significativo na velocidade de avaliação das funções trigonométricas em comparação ao JDK 8.

Interface Nativa Java

A Java Native Interface invoca uma alta sobrecarga, tornando caro cruzar a fronteira entre o código em execução na JVM e o código nativo. O Java Native Access (JNA) fornece aos programas Java acesso fácil a bibliotecas nativas compartilhadas ( biblioteca de vínculo dinâmico (DLLs) no Windows) apenas por meio de código Java, sem JNI ou código nativo. Esta funcionalidade é comparável à plataforma / Invoke do Windows e ctypes do Python . O acesso é dinâmico em tempo de execução, sem geração de código. Mas tem um custo e o JNA geralmente é mais lento do que o JNI.

Interface de usuário

O Swing tem sido percebido como mais lento do que os kits de ferramentas de widget nativos , porque delega a renderização de widgets à API Java 2D pura . No entanto, os benchmarks que comparam o desempenho do Swing com o Standard Widget Toolkit , que delega a renderização para as bibliotecas GUI nativas do sistema operacional, não mostram um vencedor claro e os resultados dependem muito do contexto e dos ambientes. Além disso, a estrutura JavaFX mais recente , destinada a substituir o Swing, trata de muitos dos problemas inerentes ao Swing.

Use para computação de alto desempenho

Algumas pessoas acreditam que o desempenho Java para computação de alto desempenho (HPC) é semelhante ao Fortran em benchmarks de computação intensiva, mas que as JVMs ainda têm problemas de escalabilidade para realizar comunicação intensiva em uma rede de computação em grade .

No entanto, os aplicativos de computação de alto desempenho escritos em Java ganharam competições de benchmark. Em 2008 e 2009, um cluster baseado em Apache Hadoop (um projeto de computação de alto desempenho de código aberto escrito em Java) foi capaz de classificar um terabyte e petabyte de inteiros o mais rápido. A configuração de hardware dos sistemas concorrentes não foi corrigida, no entanto.

Em concursos de programação

Os programas em Java iniciam mais lentamente do que em outras linguagens compiladas. Portanto, alguns sistemas de jurados online, principalmente aqueles hospedados por universidades chinesas, usam limites de tempo mais longos para que os programas Java sejam justos com os concorrentes que usam Java.

Veja também

Referências

links externos