• Nenhum resultado encontrado

3.3 Compiladores Just-In-Time

3.3.3 Os Compiladores Java da Sun

A máquina virtual Java da Sun [80] está disponível em duas versões: a máquina virtual

cliente e a máquina virtual servidora. A máquina virtual Java HotSpot cliente é para a

execução de aplicações interativas e como tal é ajustada para compilação rápida. A má- quina virtual Java HotSpot servidora é proposta para um máximo ganho de desempenho na velocidade de execução de aplicações longas. Ambas compartilham o mesmo ambiente de execução, porém usam diferentes compiladores JIT, chamados: compilador cliente e compilador servidor.

O compilador cliente [80] possui uma velocidade de compilação significativamente mais alta por não aplicar otimizações que consumam tempos longos. Por outro lado, o compilador servidor [88] é proposto para aplicações que possuem um longo tempo de execução onde o tempo de inicialização pode ser negligenciado e apenas o tempo de execução é relevante.

Como tal a estrutura interna do compilador cliente é muito mais simples do que a do compilador servidor. Ela é organizada como um frontend independente de máquina e um backend dependente de máquina. A estrutura do compilador cliente é apresentada na figura 3.14.

O compilador cliente utiliza um processo de compilação composto de cinco fases, são elas:

1. O tradutor HIR constrói uma representação intermediária de alto nível a partir dos

bytecodes.

2. O otimizador aplica apenas otimizações simples como constant folding. Nesta fase, os laços mais internos são detectados para facilitar a alocação de registradores.

Figura 3.14: Estrutura do compilador JIT cliente da Sun.

3. O tradutor LIR converte a representação de alto nível em uma representação de baixo nível similar ao código da máquina alvo.

4. O alocador de registradores realiza a alocação de registradores. A heurística usada para a alocação de registradores assume que todas as variáveis locais estão armaze- nadas na pilha. Registradores são alocados quando necessário e liberados quando o valor é armazenado em uma variável local. Se um registrador fica completamente sem uso dentro de um laço ou na entrada de um método, então este registrador é usado para armazenar a variável local usada freqüentemente. Esta abordagem re- duz o número de leituras e escritas na memória, especialmente em arquiteturas com poucos registradores.

5. O gerador de código gera código nativo para o método.

O compilador servidor [88] é um compilador otimizador completo que realiza várias otimizações clássicas tais como: eliminação de expressões comuns, loop unrolling e alo- cação de registradores baseados em coloração de grafos. Além de aplicar otimizações específicas a Java, tais como: integração de métodos virtuais [40], eliminação de che- cagem nula [63] e eliminação de checagem da faixa em arrays [52]. Estas otimizações reduzem o overhead necessário para garantir a semântica segura da linguagem Java.

Espera-se que estas otimizações gerem um código de alta qualidade e com baixo tempo de execução. Porém, estas otimizações consomem um elevado tempo de compila- ção, tendo-se uma baixa velocidade de compilação comparada com o compilador cliente

ou outros compiladores JIT. Segundo os autores, o compilador servidor é a melhor es- colha para aplicações Java que possuem um longo tempo de execução pois o tempo ini- cial necessário para a compilação pode ser negligenciado: apenas o tempo de execução do código gerado é relevante.

O compilador servidor utiliza uma representação intermediária baseada em um grafo na forma static single assignment [8, 36, 9]. Operações são representadas através de nodos, os operandos de entrada são representados por arestas para os nodos que produzem os valores de entrada (arestas de fluxo de dados). O fluxo de controle é representado por arestas explícitas que não precisam necessariamente ser as mesmas arestas de fluxo de dados. Isto permite otimizações de fluxo de dados alterando a ordem dos nodos sem destruir o correto fluxo de controle.

A estutura do compilador é apresentada na figura 3.15.

Figura 3.15: Estrutura do Compilador Server. O compilador possui portanto as seguintes fases:

1. O parser necessita de duas iterações sobre os bytecodes. A primeira iteração identi- fica os blocos básicos, onde um bloco básico é uma seqüência de bytecodes que não possui um salto em seu corpo. A segunda iteração visita todos os blocos básicos e traduz os bytecodes do bloco para nodos da representação SSA. Devido aos nodos de instrução serem também conectados por arestas de fluxo de controle, a estrutura

explícita de blocos básicos é revelada. Isto permite uma posterior reordenação dos nodos de instruções.

2. O otimizador aplica otimizações independentes de máquina. Otimizações como avaliação de expressões constantes e numeração de valores são aplicadas durante a fase inicial. Laços não podem ser otimizados completamente durante esta fase devido ao fim do laço não ser ainda conhecido quando o seu início é processado. Como tal, as otimizações descritas anteriormente juntamente com otimizações glo- bais incluindo: propagação de constantes, loop unrolling e eliminação de saltos [84] são em seguida reexecutadas até alcançar um ponto fixo, onde nenhuma otimização futura seja possível. Este processo pode requerer várias iterações sobre todos os blocos básicos, e consumir um tempo significante.

3. O selecionador de instruções seleciona as instruções da arquitetura alvo que repre- sentaram o código do método. A tradução de instruções independentes de máquina para instruções da arquitetura destino é realizada através de um sistema de reescrita de baixo para cima [90, 56]. Este sistema usa uma descrição da arquitetura destino para auxiliar na estimativa do custo de cada instrução. Quando o custo estimado de cada instrução de máquina é conhecido, torna-se possível selecionar a melhor instrução de máquina.

4. O escalonador escalona o código. A ordem final das instruções é calculada antes da fase de alocação de registradores. Instruções ligadas pelas arestas de fluxo de controle são agrupadas em blocos básicos novamente. Cada bloco tem uma fre- qüência de execução associada que é estimada a partir da profundidade de um laço e por predição de saltos. Dentro de um bloco básico, as instruções são ordenadas por um escalonador local.

5. O alocador de registradores realiza a alocação de registradores. O alocador global de registradores utiliza coloração de grafos. Primeiro, as faixas vivas são coleta- das e conservativamente agrupados, após isto cores são atribuídas aos nodos. Se a coloração falha, código para manter dados na memória é inserido e o algoritmo é repetido.

6. O otimizador peephole otimiza seqüências de código específicas de arquitetura. Esta fase também insere dados adicionais necessários para desotimização, coleta de lixo, e gerência de exceções.

7. O gerador de código finalmente, gera código executável e o instalado no ambiente de execução.

O capítulo 4 apresenta um estudo sobre o impacto dos compiladores Java Just-In-Time da Sun.