sábado, 30 de maio de 2009

Java6u14: EA e LE

Sem fazer nenhum alarde (e uma semana antes do JavaOne 2009) a Sun resolveu lançar mais uma atualização para o JRE6 - o Update 14. Devido às indefinições sobre o Java7 (que ainda não possui uma JSR), a Sun tem feito, em suas atualizações do Java6, grandes modificações, entre as quais o "novo plugin" no update 10, capacidade de atualização "in place" para windows no update 11, o plugin de 64 bits no update 12 e - o principal assunto deste artigo - análise de escape e o novo garbage collector (G1) no update 14.


Escape Analysis
A análise de escape permite à JVM detectar quando um objeto é "não escapante" (ou seja, seu ciclo de vida está confinado a um escopo local). Quando um objeto desse tipo é detectado, o JIT vai (supostamente) alocá-lo na pilha (ao invés do heap) e tratar seus campos como variáveis locais e, no melhor caso, trabalhar apenas com os registradores do seu CPU. Além disso, é garantidamente seguro ignorar os monitores destes objetos - uma otimização conhecida como "lock elision".

Um pouco de história:
Na época do desenvolvimento do Java6, a análise de escape estava prometida como um dos responsáveis por um aumento de performance sobre o Java5. Algumas JVMs da época já tinham essa feature (ou pelo menos em teoria, de acordo com este artigo), mas devido ao tempo escasso, a JVM6 era capaz de fazer a análise, mas não implementava nenhuma otimização com essas informações.

Na prática...
Vamos ver como a EA (escape analysis) e LE (lock elision) se saem em alguns microbenchmarks simples.
O primeiro teste consiste em alocar um objeto simples, com dois campos, dentro de um for, alterar o valor dos seus campos, e somá-los. Estas operações são feitas através de métodos ao inves de acessar os campos diretamente, com a análise de escape desligada primeiro, depois ligada.
Antes de mais nada, a EA só pode ser ativada na jvm server (ou seja, está presente apenas na JDK6U14 e não na JRE6U14) e com uma opção não padrão (daquelas que começam com -XX)
-XX:+DoEscapeAnalysis

A parte principal do código:


public void execute() {
for (int i = 0; i < max; i++) {
Foo foo = new Foo();
foo.setNumA(i);
foo.setNumB(i * i);
total += foo.sum();
}
}



Os resultados foram:

Tempo de execução para 1000000000 iterações
client vm: 11922 ms
server vm: 8452 ms
server vm + EA: 1611ms

Ou, em iterações por millisegundo:
client vm: 83872
server vm: 118306
server vm + EA: 620578

Como pode ser visto, usando a EA, o pequeno teste ficou mais de 6x mais rápido. Claro que estes números não devem ser lidos literalmente, pois este teste foi especificamente designado para tirar vantagem da análise - em outras palavras não espere esta melhoria no eclipse do dia-a-dia ou no jboss em produção.

Lock elision
Lock elision é uma das técnicas que poderia ser uma gigantesca vantagem das linguagens dinamicamente compiladas sobre as estaticamente compiladas. E o update 14 especificamente menciona que com a análise de escape ativada, os locks também seriam eliminados. Executando o mesmo teste, agora com todos os métodos marcados como sincronizados, temos:

Alterando o código anterior para:

public void execute() {
for (int i = 0; i < max; i++) {
SFoo foo = new SFoo();
foo.setNumA(i);
foo.setNumB(i * i);
total += foo.sum();
}
}

Onde SFoo é idêntico a Foo, porém com seus métodos marcados como synchronized.

Tempo de execução para 100000000 (perceba que eu tive que usar 10x menos operações para a versão sincronizada):
client vm: 6337 ms
server vm: 4184 ms
server vm + EA: 276 ms (!!!)

Ou, em iterações por millisegundo:
client vm: 15778
server vm: 23897
server vm + EA: 361271

Ou seja, o speedup aqui foi de 21x - bem melhor do que a versão não sincronizada. Este resultado sugere que o lock elision é, de alguma forma feito. Porém, se compararmos somente os resultados da EA, a versão sincronizada é quase 2x mais lenta, o que sugere que a JIT não elimina todo o overhead da entrada e saída do monitor - quem sabe para continuar com algumas garantias de visibilidade.

Já se alterarmos o código inicial para:

public void execute() {
for (int i = 0; i < max; i++) {
Foo foo = new Foo();
synchronized (foo) {
foo.setNumA(i);
foo.setNumB(i * i);
total += foo.sum();
}
}
}

Seria esperado que os resultados fossem parecidos com o primeiro caso, já que o monitor do objeto foo só é adquirido uma vez por iteração. Vamos aos resultados (dessa vez apenas com o EA ativado), desta vez combinado com outras opções presentes antes do update 14 que poderiam afetar este teste:

Tempo em millisegundos para 1000000000 iterações:
server + EA: 9606ms
server + EA + Eliminate locks: 9543ms
server + EA + Biased locking: 9599ms
server + todas acima: 9515ms

As opções acima (Eliminate Locks e Biased Locking) são ativadas respectivamente por: -XX:+EliminateLocks e -XX:+UseBiasedLocking e, supostamente, são ativadas por padrão.
Aparentemente, um bloco sincronizado maior limita as otimizações que podem ser feitas pela análise de escape - os resultados obtidos estão aproximadamente 3x piores do que o teste anterior (com os métodos marcados com 'synchronized').

G1 e "Compressed OOPTS"
Além da EA, o update 14 trouxe também um novo garbage collector chamado G1 (garbage first), apresentado no JavaOne 2008 e ainda em estágio experimental. Trata-se de um GC paralelo e que tenta garantir uma pausa máxima (que pode ser definida pela linha de comando) e pode ser ativada pela combinação de opções:
-XX:+UnlockExperimentalVMOptions -XX:+UseG1GC
Por último, os "compressed object pointers" também foram integrados. Quem usa uma jvm de 64bits já deve ter notado que o consumo de memória é bem maior do que na jvm de 32 bits. Isso ocorre porque as referências para objetos ocupam 64 bits, mesmo quando todo o espaço de endereçamento não é necessário. Com esta opção nas JVM de 64 bits, as referências para objetos continuam com 32 bits a menos que seja necessário. Esta opção é ativada por:

-XX:+UseCompressedOops


e já existia nas JVM de "performance".


Mais informações sobre o G1 podem ser encontradas em:

e uma pequena demonstração dos ponteiros comprimidos pode ser vista em:
(ainda na jvm6 de performance)

Conclusão
Estas últimas atualizações podem trazer ganhos de performance muito bons para alguns cenários, embora haja alguma limitação no que a JVM pode fazer na presença de locks - o que pode muito bem ser corrigido em updates futuros ou quem sabe no jdk7. Embora eu espere que o JDK7 não demore a sair, acho muito interessante que a Sun não esteja se contendo em aperfeiçoar a sua JVM mesmo nas atualizações menores.







public static void main(String[] args) {
//yey
/**
* woot
*/
}

Um comentário: