11399 - A máquina virtual Java e a otimização inline um estudo de

Propaganda
A MÁQUINA VIRTUAL JAVA E A OTIMIZAÇÃO INLINE: UM ESTUDO DE CASO
THE JAVA VIRTUAL MACHINE AND THE INLINE OPTIMIZATION: A CASE STUDY
Francis Rangel1
Anderson Faustino da Silva2
Resumo. Embora seja conhecido que a otimização
inline produza benefícios consideráveis é um desafio
desenvolver uma boa heurística de sua aplicação, de
forma que o ganho de desempenho seja efetivo para
diversas classes de programas. O objetivo deste
trabalho é realizar uma análise do impacto de inline,
na execução de programas Java e demonstrar que a
estratégia utilizada durante a aplicação de inline nem
sempre alcança o objetivo proposto.
Palavras Chave: Máquina Virtual Java. Otimização.
Inline.
1
B. Sc. Universidade
Departamento
de
[email protected]
2
D. Sc. Universidade
Departamento
de
[email protected]
Revista Tecnológica
Estadual de Maringá,
Informática.
Email:
Estadual de Maringá,
Informática.
Email:
Abstract. Although it is well-known that the inline
optimization produces goods results it remains a
challenge to develop a good heuristic of its
application, so that the performance profit is
effective for many applications. The goal of this
paper is to present an experimental analysis about
the impact of inline on Java programs, and
demonstrate that the inline strategy not always
reaches the considered goal.
Keywords: Java Virtual Machine. Optimization.
Inline.
Maringá, v. 21. p. 103-118, 2012
A máquina virtual Java e a otimização inline: um estudo de caso
104
1. INTRODUÇÃO
Diversas linguagens de programação
interpretadas (DEITEL e DEITEL, 2010; LUTZ,
2007) utilizam uma máquina virtual (CRAIG,
2006) para executar seus programas. O
objetivo na utilização de máquinas virtuais e
linguagens interpretadas é obter a máxima
independência possível de plataforma.
Portabilidade possibilita ao desenvolvedor
projetar um determinado programa que será
executado em diferentes plataformas de
hardware, sem a necessidade de refazê-lo
para cada uma destas. Esta é a maior
vantagem das linguagens interpretadas.
A grande desvantagem de se utilizar
programas em ambientes interpretados é que
estes são mais lentos, quando comparados
com programas desenvolvidos em ambientes
compilados. Isto devido ao fato de cada
instrução do código intermediário ser lida,
decodificada, traduzida para código de
máquina e depois executada. Este processo
ocasiona um atraso na execução do
programa. Já em ambientes compilados o
código gerado é o código de máquina, sendo
necessário apenas executá-lo.
Para melhorar o desempenho de
linguagens interpretadas (SEBESTA, 2011),
diversos ambientes de execução utilizam um
mecanismo
de
compilação
dinâmica,
denominado Just-in-Time Compilation (JIT)
(SCOTT, 2008). Este mecanismo é responsável
por gerar código nativo otimizado durante a
execução do programa, não sendo mais
necessário interpretar novamente algumas
porções do código do programa. Atualmente,
a maioria das implementações da Máquina
Virtual Java (JVM – Java Virtual Machine)
(MEYER e DOWNING, 1997) possuem um
compilador
JIT em sua arquitetura
(MICROSYSTEMS, 2010; CIERNIAK et al., 2000;
BURKE et al., 1999).
Um ambiente de compilação dinâmica
além de gerar código nativo, aplica diversas
otimizações como o objetivo de melhorar a
qualidade do código gerado. No contexto de
linguagens orientadas a objetos uma
otimização que possui um alto potencial é
inline (MUCHNICK, 1997). Isto devido ao fato
de programas orientados a objetos possuírem
uma grande quantidade de invocações de
métodos. Embora seja conhecido que esta
otimização seja efetiva, os trabalhos que
Revista Tecnológica
desenvolvem ambientes de compilação dinâmica
para Java (BURKE et al, 1999; CIERNIAK et al, 2000,
GU et al, 2000; MICROSYSTEMS, 2010; SUGANUMA
et al, 2000) não demonstram o quão esta otimização
é efetiva, nem os custos decorrentes de sua má
utilização.
O objetivo deste artigo é avaliar o impacto
efetivo da otimização inline em programas Java
executados pela JVM da Sun Microsystems Inc.
(MICROSYSTEMS, 2010), visando entender o
funcionamento desta otimização, bem como os
motivos que a levam a degradar o desempenho do
sistema para alguns programas. Uma análise deste
porte é fundamental para possíveis alterações na
estratégia de aplicação desta otimização,
consequentemente, melhorar o desempenho de
sistemas que a utilizam.
O restante deste artigo está organizado da
seguinte forma. A Seção 2 descreve a arquitetura da
JVM da Sun. A Seção 3 descreve a otimização inline e
seu funcionamento na JVM da Sun. A Seção 4
apresenta uma avaliação experimental realizada
para avaliar o impacto da otimização inline em
programas Java. E, finalmente, a Seção 5 conclui
este artigo.
2. A MÁQUINA VIRTUAL JAVA DA SUN
A Máquina Virtual Java é um computador
abstrato (LINDHOLM, 1999), capaz de carregar
classes e executar os bytecodes, que são instruções
codificadas no formato da Máquina Virtual Java,
nelas contidos. Ela é composta por três elementos:
1.
Um carregador de classe, que carrega as
classes da API Java e as do programa a ser
executado;
2. Uma heap, a região de dados que armazena
as classes; e
3. Um motor de execução responsável por
interpretar os bytecodes que implementam os
métodos das classes.
A Figura 1 apresenta os componentes da JVM. A
heap é uma área de dados na qual todas as instâncias
de classes e arrays são armazenados. A heap é criada
durante a iniciação da JVM. O armazenamento de
dados na heap é gerenciado por um coletor de lixo,
pois objetos Java não são desalocados
explicitamente (VERNNERS, 1999). A heap pode ter
um tamanho fixo, ou pode expandir caso o
programa crie vários objetos e contrair quando
objetos não são mais referenciados.
Maringá, v. 21. p. 103-118, 2012
Rangel e Silvaiiiiii
105
Figura 1. Arquitetura da Máquina Virtual da Sun
O motor de execução suporta a execução
de instruções da máquina virtual em dois
modos de execução: um modo interpretado e
um modo misto de execução. No modo
interpretado, o motor de execução simples
apenas interpreta os bytecodes, um por um.
Por outro lado, no modo misto, a máquina
virtual inicia interpretando bytecodes, porém
o programa em execução é monitorado para
detecção das áreas de código executadas
com frequência, os chamados hot-spots.
Assim, durante a execução do programa, a
máquina virtual Java gera código nativo para
os hot-spots e continua interpretando os
outros bytecodes.
Uma máquina virtual Java possui dois tipos
de métodos: métodos Java e métodos
nativos. Um método Java é escrito em
linguagem Java (DEITEL e DEITEL, 2010),
compilado para bytecodes e armazenado em
arquivos de classes. Um método nativo é
escrito em outra linguagem, tal como C
(DAMAS, 2007), e compilado para código de
máquina nativo de um particular processador.
Métodos nativos são armazenados em
bibliotecas dinâmicas. Quando um programa
Java invoca um método nativo, a máquina
virtual carrega a biblioteca dinâmica que
contém a implementação do método e o
invoca.
2.1. O CARREGADOR DE CLASSES
A JVM possui uma arquitetura flexível para
carregadores de classes, permitindo a um
programa Java carregar dinamicamente
classes tanto do disco local, como da Internet.
O carregador de classes (LIANG, 1998) é
responsável não apenas por localizar e
importar os dados binários das classes. Ele
também tem as funções de verificar os dados
importados, alocar e inicializar memória para
Revista Tecnológica
as variáveis das classes e resolver as referências
simbólicas. Estas atividades são realizadas na
seguinte ordem:
1.
Carregar: encontrar e importar os dados
binários para uma classe.
2. Ligar: executar a verificação, a preparação, e
opcionalmente a definição.
(a) Verificar: assegurar a exatidão da classe
importada.
(b) Preparar: alocar memória para variáveis da
classe e iniciar a memória alocada com o valor
padrão zero (0).
(c) Definir: transformar referências simbólicas em
referências diretas.
3. Iniciar: invocar o código Java que inicia as
variáveis da classe com seus valores apropriados.
Um programa Java pode fazer uso de diferentes
carregadores de classes. Existe um carregador
padrão, que é um componente da implementação da
máquina Java, e podem existir carregadores
definidos pelo usuário, capaz de carregar classes por
meios não convencionais.
Enquanto o carregador padrão é parte da
implementação da JVM, o carregador definido pelo
usuário é uma classe Java, inicialmente carregada
pela JVM e instanciado como qualquer outro objeto.
Para cada classe carregada, a máquina virtual
mantém um histórico contendo qual carregador
carregou a classe. Quando uma classe faz referência
a uma classe não carregada, a máquina virtual Java
utiliza para a carga da classe referenciada o
carregador da classe que contém a referência. O uso
deste mecanismo pressupõe que uma classe apenas
referencie classes carregadas pelo mesmo
carregador.
2.2. A HEAP
Em uma instância da JVM, informações sobre os
tipos carregados são armazenadas em uma área
lógica da memória denominada área de métodos
(VERNNERS, 1999). A área de métodos é
Maringá, v. 21. p. 103-118, 2012
A máquina virtual Java e a otimização inline: um estudo de caso
106
compartilhada por todos os threads do
programa. Porém, quando dois threads
tentam encontrar uma classe que ainda não
foi carregada, apenas uma carrega a classe,
enquanto a outra fica bloqueada em espera.
Quando um programa Java em execução
cria uma nova instância de uma classe, a
memória para este novo objeto é alocada em
uma heap. A máquina Java possui apenas uma
heap, que é compartilhada por todos os
threads do programa. Note que threads
podem,
portanto
acessar
objetos
pertencentes a outra thread. Java fornece
primitivas de sincronização, tais como wait,
notify e notifyall para que o programador
possa evitar condições de corrida.
A JVM possui uma instrução que aloca
memória na heap para um novo objeto,
porém não possui uma instrução para liberar
memória. A liberação das áreas de memória
ocupadas por objetos que não são
referenciados
pelo
programa
é
de
responsabilidade do coletor de lixo (APPEL,
1998) da JVM. O coletor de lixo é um
componente
fundamental
da
JVM
responsável por gerenciar a heap e a área de
métodos. O coletor de lixo, além de liberar as
áreas utilizadas pelos objetos e classes que
não estão sendo referenciados, possui a
capacidade de realocar classes e suas
instâncias para reduzir a fragmentação da
área de métodos e/ou da heap.
Em geral, o tamanho das áreas de
métodos e da heap não são fixos. Durante a
execução de um programa Java, estas podem
ser expandidas ou contraídas pela JVM.
2.3. O MOTOR DE EXECUÇÃO
O núcleo da JVM é seu motor de execução
(VERNNERS, 1999), cujo comportamento é
definido por um conjunto de instruções.
Cada thread do programa é uma instância
do motor de execução. Durante o período de
vida do thread, ela está executando bytecodes
ou métodos nativos. O thread pode executar
bytecodes diretamente, interpretando, ou
indiretamente, executando o código nativo
resultante da compilação.
A JVM pode usar threads que não são
visíveis ao programa, por exemplo, o thread
que executa o coletor de lixo. Tais threads não
precisam ser instâncias do motor de
execução. Entretanto, os threads que
Revista Tecnológica
pertencem ao programa são motores de execução
em ação.
O motor de execução funciona executando
bytecodes de uma instrução por vez. Este processo
ocorre para cada thread do programa. Uma
seqüência de bytecodes é uma sequência de
instruções. Cada instrução consiste de um código de
operação seguido por zero ou mais operandos. O
código de operação indica a operação a ser
executada. Os operandos fornecem os dados
necessários para executar a operação especificada.
No próprio código de operação é implícita a
existência ou não de operandos e dos mecanismos
para acessá-los.
O motor de execução busca um código de
operação e se esse código possuir operandos busca
os operandos, após executa a ação solicitada pelo
código e em seguida busca outro código. A execução
dos bytecodes continua até que um thread termine
retornando de seu método inicial.
Cada tipo de código do conjunto de instruções da
Máquina Virtual Java possui um mnemônio, no estilo
típico de linguagem Assembly (STREIB, 2011).
2.4. O COMPILADOR JIT
O compilador JIT (MICROSYSTEMS, 2010) traduz
código fonte em código de máquina a medida que
este código é executado. Consequentemente, o
tempo de compilação passa a estar inserido no
tempo total de execução da aplicação. Desta forma,
o compilador JIT da JVM da Sun apenas traduz as
unidades cujo tempo gasto em tradução seja
amortizado pelo ganho de desempenho em código
nativo. Além disto, este compilador utiliza técnicas
de otimização de código para obter código de alta
qualidade.
A JVM da Sun utiliza a abordagem de interpretar
primeiro e depois compilar, baseada na observação
de que a maioria dos programas gasta a maior parte
do seu tempo em uma pequena faixa de código. Esta
abordagem compila apenas as partes do código
executadas frequentemente. Para tal, as unidades
de código são instrumentadas com contadores.
Cada unidade possui dois contadores: um
contador de entrada e um contador de retorno. O
primeiro é incrementado no início da execução de
cada unidade. O outro é incrementado quando um
salto de retorno à unidade é executado. Se estes
contadores excedem um limite pré-definido a
unidade é escalonada para compilação.
Por outro lado, os contadores das unidades que
não são executados frequentemente, por exemplo,
apenas uma vez no início do programa, nunca
Maringá, v. 21. p. 103-118, 2012
Rangel e Silvaiiiiii
107
atingirão
o
limite
determinado
e
consequentemente nunca serão compilados.
Isto reduz drasticamente o número de
unidades que são compiladas. Desta maneira,
o compilador gera menos código e pode
gastar mais tempo otimizando o código das
unidades mais importantes. É esperado que
os contadores de frequência, de todas as
unidades executadas, alcancem o limite
determinado e que estas unidades sejam
compiladas sem gastar demasiado tempo
com sua interpretação.
A implementação da JVM da Sun possui
dois compiladores, os quais são denominados
Cliente e Servidor. O compilador Cliente é
mais simples e aplica o mínimo de otimizações
possíveis no código, pois tem o objetivo de
reduzir as pausas do sistema. Por outro lado,
o compilador Servidor é mais agressivo e
utiliza diversas otimizações para tentar
melhorar o desempenho do código
compilado. Porém, esta agressividade do
compilador Servidor pode ser prejudicial.
Dependendo das características do programa
em execução pode ocorrer um crescimento
prejudicial do código e/ou aumento na
pressão por registradores, entre outros
problemas.
A estrutura do compilador Cliente é
formada por um frontend, independente de
máquina, e por um backend, parcialmente
dependente de máquina. O frontend constrói
uma representação intermediária de alto nível
(em inglês, HIR). Apenas otimizações simples
são utilizadas neste momento, como
propagação de constantes. Após isto, os laços
mais internos são detectados para facilitar a
alocação de registradores, realizados pelo
backend.
O backend converte o HIR para uma
representação intermediária de baixo nível.
Registradores
são
alocados
quando
necessário e liberados quando o seu valor é
armazenado em uma variável local.
Registradores não utilizados armazenam o
valor da variável local mais utilizada. Para
identificar estes registradores, o gerador de
código utiliza duas passagens. Primeiramente,
a geração de código é desabilitada e a
alocação de registradores monitorada. Em
seguida, a geração de código é acionada e o
gerador executa a segunda passagem.
O compilador Servidor utiliza um grafo
para a representação intermediária. Os
Revista Tecnológica
seguintes passos são realizados durante a
compilação: tradução de bytecodes, otimizações
independentes de máquina, seleção de instruções,
escalonamento global de código, alocação de
registradores, otimização peephole e, por último,
geração de código.
Duas passagens pelos bytecodes são necessárias
para a compilação do método. Durante ambas as
passagens otimizações são utilizadas. A alocação de
registradores é realizada utilizando uma estratégia
de coloração de grafo. Após este processo a
otimização peephole é aplicada e, enfim, o código é
gerado.
3. INLINE
Quando um procedimento é invocado existe uma
série de operações necessárias para que este seja
executado (AHO et al, 2007). De maneira
simplificada, o computador necessita realizar os
seguintes passos: criar um registro de ativação
(APPEL, 1998) em memória, para armazenar os
dados gerenciais do procedimento; ajustar
registradores e armazenar informações de retorno
para o procedimento corrente; executar o
procedimento; retornar o resultado, se necessário;
ajustar o ponto de execução seguinte à invocação do
procedimento; e voltar a execução normal do
procedimento anterior. Todo este processo causa
uma perda considerável de desempenho, quando
realizado inúmeras vezes.
Para melhorar o desempenho de programas que
contenham inúmeras chamadas a procedimentos foi
desenvolvida a otimização inline (ARNOLD et al,
2000). O objetivo desta otimização é eliminar a
chamada do procedimento, trocando a chamada por
uma cópia do seu corpo.
Realizar a cópia do corpo do programa no local
de sua chamada significa eliminar as operações
necessárias durante a gerência da execução de um
procedimento. Com isso, o tempo de execução do
procedimento é reduzido. O único ajuste necessário
ao se realizar inlining é ajustar os argumentos
formais, antes passados como argumentos reais,
agora presentes no corpo do procedimento que
realizaria a chamada. Isto porque os argumentos
deixaram de existir, visto que a chamada do
procedimento não mais existe mais.
No início, algumas linguagens de programação
não utilizavam inline, mas um recurso semelhante.
Macro (CHIBA, 1998), um recurso disponível na
linguagem Lisp (BAKER, 1992), realiza um processo
semelhante ao funcionamento de inline. Por outro
lado, outras linguagens começaram a utilizar esta
Maringá, v. 21. p. 103-118, 2012
A máquina virtual Java e a otimização inline: um estudo de caso
108
otimização por meio de instruções especiais.
Um exemplo é o que ocorreu com a
linguagem Ada (DEPARTAMENT OF DEFENSE,
2006). Nesta linguagem o inline era tratado
como um pragma (LEDGARD, 1983), sendo
necessário que o programador selecionasse
os procedimentos aos quais ele gostaria que
fosse aplicado inline.
Esta flexibilidade na utilização da
otimização nem sempre é vantajosa, visto que
inline pode ocasionar alguns problemas se
não for aplicado de maneira correta. Uma
escolha equivocada de procedimento pode
ocasionar um efeito colateral ao proposto
pela otimização, ocasionando uma perda de
desempenho (DA SILVA e SANTOS COSTA,
2006). Por este motivo, algumas vezes esta
decisão é tomada pelo próprio compilador.
Isto ocorre porque o compilador pode coletar
informações sobre as características do
progama e usá-las no processo de seleção de
procedimentos para aplicação de inline. Estas
informações podem ser coletadas tanto em
tempo
de
compilação,
denominadas
informações estáticas, quanto em tempo de
execução,
denominadas
informações
dinâmicas ou de tempo de execução, e não
estão disponíveis ao desenvolvedor enquanto
este está codificando seu programa.
Com a utilização destas informações, um
compilador pode ser mais seletivo no
processo de inlining. Sendo assim, um
conjunto mais restrito de procedimentos
sofre a aplicação da otimização, garantindo
que este conjunto seleto, com inline aplicado,
melhore a qualidade do código gerado e,
consequentemente, reduza o tempo total de
execução do programa.
Em linguagens orientadas a objeto
(STROUSTRUP, 1991) a utilização de inline se
tornou extremamente necessária pela
quantidade de métodos pequenos que são
criados para realizar o encapsulamento
proposto pelo paradigma. Um exemplo deste
comportamento é o acesso a atributos de
classes. Geralmente, um atributo possui, ao
menos, dois métodos dentro da classe, sendo
um para retornar e outro para alterar seu
valor. Somente este processo implica na
criação de muitos métodos por classe, os
quais são invocados muitas vezes durante a
execução do programa, consequentemente,
impactando em alto custo à execução do
programa.
Revista Tecnológica
3.1. VANTAGENS DE INLINE
A aplicação de inline ocasiona uma série de
vantagens durante a compilação de um programa.
As duas principais são: a invocação de métodos não
ocasiona a criação de registros de ativação, incluindo
os procedimentos de controle dos mesmos; e
aumento do escopo do programa, sendo possível
aplicar outras otimizações onde antes não era
possível.
Em um programa com inúmeras classes e
métodos, reduzir a quantidade de invocações é
crucial para um bom desempenho, pois muitos
métodos estarão integrados ao código dos seus
chamadores. Isto implica em certo custo para o
compilador e para o tempo total de execução, em
um ambiente com compilação dinâmica.
Inline possui uma relação direta com o uso da
cache (TANENBAUM, 2006). Após aplicar esta
otimização, instruções relacionadas são dispostas
juntas, reduzindo as chances de conflitos na cache
(ZHAO, 2003). Se o método for invocado de maneira
convencional, um registro de ativação será criado e
seu código se encontrará segmentado do código do
seu chamador. Isto pode significar uma troca de
dados na cache, mesmo que os métodos tenham um
relacionamento muito grande e atuem sobre as
mesmas informações.
O acesso a informações na cache é muito mais
rápido, em relação a acessos na memória principal.
Sendo assim, quanto mais instruções puderem ser
agregadas e compartilharem a cache, melhor será o
desempenho do programa em execução.
3.2. DESVANTAGENS DE INLINE
Apesar de ser uma técnica com grande potencial,
inline também possui certas desvantagens,
principalmente relacionadas à má utilização da
otimização.
O principal problema desta otimização é o
crescimento excessivo do tamanho do código
(SERRANO, 1995). Desta forma, um dos fatores mais
importantes no momento da seleção para realizar
inlining de um método é o tamanho do seu corpo,
assim como o tamanho do corpo do seu chamador.
Anteriormente foi citada uma vantagem quanto
ao sequenciamento de instruções para melhor
utilizar a cache (CHEN et al, 1989). Isto ocorre
quando o código, tanto do método chamador,
quanto do método chamado, utilizam instruções que
a memória cache comporta, efetuando assim uma
utilização extremamente mais vantajosa de acessos
à memória. Porém, quando o código gerado possui
Maringá, v. 21. p. 103-118, 2012
Rangel e Silvaiiiiii
109
instruções demasiadas para execução dos
métodos, agora compartilhando do mesmo
corpo, isto pode se tornar um problema.
Além de ocasionar problemas de acesso a
cache, inlining de métodos muito grandes
pode gerar problemas para o alocador de
registradores (LUEH, 1997). Se inline gerar um
código com um número elevado de instruções
e operações sobre dados, aumentará a
pressão por registradores. O problema é
semelhante ao que ocorre com a memória
cache. Várias operações e instruções serão
realizadas no mesmo momento, sendo
necessário utilizar muitas vezes a memória
para armazenar valores dos registradores
enquanto se executa todas as operações.
Os melhores métodos para sofrerem a
aplicação de inline são aqueles com um corpo
pequeno e que são os mais utilizados dentro
do programa. Selecionar métodos que são
pouco utilizados não significa uma melhora no
desempenho do programa.
Para realizar o controle do crescimento do
código gerado, muitos compiladores utilizam
informações referentes ao aumento do
código, em percentual. Geralmente, existe um
limite estabelecido para o tamanho final do
código gerado. Assim, durante o processo de
seleção dos métodos para aplicar inline o
tamanho do código é verificado, visando
avaliar o benefício obtido pela aplicação da
otimização no método em questão. Isto limita
a seleção de métodos muito grandes, assim
como a utilização demasiada de inline.
3.3. FUNCIONAMENTO DE INLINE NA JVM DA
SUN
O processo de compilação inicia no
momento em que o método que está sendo
interpretado atinge um limite de invocações.
Este processo verifica algumas propriedades
do método, para avaliar se é possível ou não
compilá-lo. O tamanho do código gerado não
pode ultrapassar o valor máximo estabelecido
de oito mil (8000) bytes. Se o tamanho do
código ultrapassar este limite, o método não
será compilado. Além desta verificação,
outras são realizadas. O método precisa estar
carregado, a estrutura de controle de
bytecode precisa estar criada e sua análise de
fluxo deve estar presente.
Uma compilação diferente é invocada para
métodos virtuais e não-virtuais. Neste
Revista Tecnológica
momento, a JVM possui condições e informações de
perfil3 suficientes para identificar o tipo de chamada
de cada método. Se for um método não-virtual, a
JVM verifica a aplicação de inline para este método.
Para o caso de inline de métodos virtuais, o
processo realizado é diferente. Neste caso é
executada uma tentativa de tornar o método nãovirtual. Caso obtenha sucesso, este método sofre a
aplicação de inline como se fosse um método nãovirtual. Não sendo possível a aplicação de inline, é
utilizada uma árvore de inline. Isto ocorre pelo fato
de que um método virtual pode possuir inúmeras
implementações. A implementação do método atual
é localizada nesta árvore de inline, e então ocorre a
compilação e aplicação da otimização para a
implementação em questão e o método é retornado,
já com seu código utilizando inlining.
Tanto para inlining de métodos virtuais, quanto
de não-virtuais, é necessário que este tenha sido
utilizado uma grande quantidade de vezes. Existe
um parâmetro que controla a quantidade de
invocações de um método, antes que este seja
selecionado para ser aplicada a otimização. O valor
padrão para este parâmetro, na JVM analisada, é de
duzentos e cinquenta (250) execuções. Sendo assim,
um método pode ser compilado, mas pode não
sofrer inline, caso alguma das verificações da
otimização falhe.
Caso o método candidato a utilizar a otimização
possua chamadas a outros métodos, existe um
parâmetro que controla o nível em que o compilador
pode chegar ao tentar aplicar a otimização às
chamadas aninhadas. Por padrão, este parâmetro
permite que até nove (9) níveis sejam alcançados,
antes que o compilador pare de aplicar a otimização
em métodos aninhados. Existe a mesma verificação
de nível para o caso de métodos recursivos. Porém,
o valor padrão deste parâmetro é um (1). Isto limita
razoavelmente a aplicação de inline em métodos que
possuam chamadas recursivas.
A JVM da Sun utiliza uma classificação de
métodos por temperatura que é composta de três
níveis: frio, morno e quente. Para avaliar a
temperatura de um determinado método alguns
parâmetros são utilizados e, com estes, uma
heurística é aplicada. Esta classificação é realizada
com base em médias, estimativas do perfil do
método e do histórico do mesmo. Os valores
3
O perfil de um método é composto por informações
como o tamanho do código, a quantidade de invocações e
a quantidade de vezes que foi interpretado, entre outras
informações. Este perfil é utilizado durante toda a vida do
programa, sendo essencial no processo de compilação.
Maringá, v. 21. p. 103-118, 2012
A máquina virtual Java e a otimização inline: um estudo de caso
110
utilizados neste processo são: contagem,
benefício, trabalho, tamanho e temperatura.
Contagem é o número de vezes que se
espera executar uma determinada chamada.
Valores mais altos são considerados melhores
para inline, evitando o processo de invocação
convencional um grande número de vezes.
Benefício é uma estimativa de tempo que será
economizado, por execução, após a aplicação
de inline a este método. O valor um (1) indica
que está ocorrendo overhead. Valores altos
facilitam a execução da otimização, enquanto
valores negativos desabilitam a mesma.
Trabalho é uma estimativa de quanto
tempo uma chamada convencional, para este
método, será necessária. Valores menores são
favoráveis para a otimização, visto que
métodos pequenos têm tempo de execução
menor e estes são os mais proveitosos para
sofrerem inlining. Tamanho é a quantidade de
nós que se espera gerar para este método, no
grafo de fluxo de controle. Este valor não
considera o inline de chamadas aninhadas
neste método. Para avaliar este valor são
utilizados o código de máquina, se já foi
gerado em uma compilação posterior, e o
bytecode do método. Valores menores de
tamanho são melhores para inline, pois como
mencionado
anteriormente,
métodos
pequenos tendem a gerar um benefício maior
quando se aplica inline.
Temperatura é o resultado da heurística
utilizada. Este valor é a base da classificação,
com os métodos podendo ser considerados
frios, mornos ou quentes. Segundo os
desenvolvedores da JVM, se pudessem ser
oniscientes, haveria dois valores para
temperatura: com e sem inline. Este valor é
conseguido utilizando a expressão:
Contagem x Benefício.
Este valor é ajustado automaticamente
para cada método analisado, levando em
consideração que os valores utilizados na
expressão são atualizados de acordo com o
perfil de cada programa. Porém, a heurística é
muito simplificada, podendo falhar durante o
processo de aplicação da otimização.
Caso alguma das verificações realizadas
antes de aplicar inline do método falhe, este
será classificado como frio e gerado com o
código normal, sem que seja realizada a cópia
de seu corpo no local onde há chamadas para
Revista Tecnológica
o mesmo. Portanto, métodos muito grandes, pouco
utilizados, que estejam aninhados em muitos níveis,
entre outras características, não sofrerão a aplicação
de inline e serão classificados como frios. Porém,
mesmo um método não sofrendo a aplicação
imediata da otimização, ele pode ser selecionado
para aguardar em uma fila de métodos mornos. Isto
ocorre quando o resultado da heurística de
classificação não foi considerado bom o suficiente
para aplicar inline do método, mas seus parâmetros
não fazem com que este seja considerado um
método frio. Por isto este método que não é frio,
mas também não é quente, é considerado um
método morno.
Os métodos que compõem a fila de métodos
mornos são os que não possuem um grande
benefício, mas que podem aumentar, mesmo que
pouco, o desempenho do programa se sofrerem a
aplicação de inline. A utilização de métodos desta fila
depende dos métodos anteriormente utilizados para
aplicar a otimização. Existe um controle do tamanho
máximo do programa após a compilação e aplicação
de inlining. A diferença entre este valor máximo e o
valor utilizado para aplicar a otimização
anteriormente é considerada um espaço disponível
para a aplicação de inline em outros métodos.
Caso ainda exista recursos disponíveis para
aplicação da otimização, a fila de métodos mornos é
percorrida. Se um método couber no espaço
disponível, este é selecionado para sofrer inline. Caso
o espaço não seja suficiente, este método é
removido da fila e o próximo é verificado. O
tamanho do método, para verifica se há espaço
disponível ou não, encontra-se no seu perfil.
Enquanto ainda há espaço disponível e métodos na
fila, novos métodos são avaliados.
Se o espaço disponível for totalmente utilizado, a
fila de métodos candidatos a inline é esvaziada. Isto
é realizado para que, se esta tentativa, de aplicar a
otimização em métodos armazenados na fila,
ocorrer novamente, nada seja executado e o
processo finalize sem desperdício de tempo. Afinal,
sem espaço disponível para realizar inline, não faz
sentido tentar aplicar a otimização em outros
métodos. Este espaço disponível é atualizado no
momento que métodos e classes são desalocados.
Após aplicar inline, outras otimizações são
executadas no código já expandido. Esta é uma
grande vantagem desta otimização, pois agora as
outras otimizações tem um escopo maior para
serem aplicadas onde antes havia apenas uma
chamada a método e não era possível otimizar esta
parte do código. Algumas das otimizações
executadas
são:
peephole,
alocação
de
Maringá, v. 21. p. 103-118, 2012
Rangel e Silvaiiiiii
registradores,
conditional
constant
propagation e loop unrolling, entre outras
(MUCHNICK, 1997).
Logo em seguida, a execução do
programa é retomada normalmente. Agora,
possivelmente, com um método não mais
interpretado, mas compilado, aplicado inline e
com outras otimizações realizadas em seu
novo corpo. Isto pode ocasionar um ganho de
desempenho considerável, mesmo com todos
estes processos sendo realizados em tempo
de execução.
8.10, com kernel 2.6.27-14 generic. A JVM da Sun
utilizada foi a versão 1.6.19.
A Tabela 1 fornece uma descrição dos programas
utilizados na avaliação experimental. Os oito
primeiros fazem parte do DaCapo Benchmark
(BLACKBURN et al, 2006), os próximos cinco
programas fazem parte do Java Grande Fórum
Benchmark (BULL et al, 2000) e os últimos sete
fazem parte do Shootout Benchmark (FULGHAM,
2010). Cada conjunto de benchmark possui um foco
diferente. Três aspectos da execução são explorados
pelos programas: utilização de memória, utilização
de processador e operações de entrada e saída. Os
programas do DaCapo Benchmark exploram os três
aspectos. Já os programas do Java Grande Fórum
Benchmark exploram mais o uso do processador e
acessos à memória. Por fim, os programas do
Shootout Benchmark exploram as operações de
entrada e saída.
4. AVALIAÇÃO EXPERIMENTAL
A avaliação experimental foi realizada em
um computador com processador Intel Dual
Core T2160 2.1 GHZ, 2GB de memória principal,
e utilizando o sistema operacional Ubuntu
Programa
Características
Entrada
Descrição
Classes
Métodos
ANTLR
Gerador de analisador sintático
675
3368
Large
BLOAT
Otimizador de bytecode Java
831
3180
Large
CHART
JFree Chart
1531
6474
Large
ECLIPSE
Testes na IDE Eclipse
2627
7276
Large
JYTHON
Interpretador Python
1329
4889
Large
LUSEARCH
Busca Lucene
688
3252
Large
PMD
Analisador de arquivos Java
1192
3805
Large
XALAN
Transforma XML em HTML
1170
3727
Large
EULER
Resolução de equações de Euler
406
2300
sizeB
MOLDYN
Simulação molecular
441
2433
sizeB
MONTECARLO
Simulação Monte Carlo
420
2332
sizeB
RAYTRACER
Renderização 3D
409
2310
sizeB
SEARCH
Busca alfa-beta
402
2297
sizeB
BINARY-TREES
Gerador de árvores binárias
345
2132
22
CHAMENEOS
Simulação de criaturas
366
2219
80000000
FASTA
Gerador de sequências de DNA
344
2133
500000
K-NUCLEOTIDE
Atualização de hash
493
2546
484 MB
MANDELBROT
Gerador bitmap
412
2327
6000
N-BODY
Simulação de corpos
440
2425
100000000
SPECTRAL
Calcula da norma espectral
438
2401
10000x10000
Tabela 1. Programas utilizados na avaliação
4.1.
ANÁLISE DE DESEMPENHO
A Figura 2 apresenta o tempo médio de
execução de cada programa, com inline ligado
e desligado. Em cada figura, as duas primeiras
Revista Tecnológica
colunas representam a execução do programa
utilizando o compilador Cliente, enquanto as duas
últimas a execução com o compilador Servidor. A
primeira e a terceira coluna representam a
otimização inline desligada e as outras duas colunas
Maringá, v. 21. p. 103-118, 2012
111
A máquina virtual Java e a otimização inline: um estudo de caso
112
a otimização ligada. Além disto, cada barra
está dividida em três componentes, a saber:
tempo gasto em métodos interpretados,
tempo gasto em métodos compilados e tempo
gasto em outras atividades.
Figura 2. Tempo total de execução em segundos, para cada programa.
4.1.1. Impacto de Inline no Compilador Cliente
O compilador Cliente obteve um resultado
interessante quando a otimização foi ligada.
Revista Tecnológica
Pode-se verificar que a sua estratégia
superior à adotada pelo compilador
Apenas FASTA apresentou queda de
desempenho quando a otimização foi
é muito
Servidor.
7,21% no
ligada e
Maringá, v. 21. p. 103-118, 2012
Rangel e Silvaiiiiii
113
apenas
SEARCH
obteve
um
ganho
insignificante de desempenho. Os programas
executados utilizando este compilador
apresentaram desempenho, em média, 26,25%
superior quando a otimização inline foi
utilizada.
A variação de ganho de desempenho foi
de 0,20% (SEARCH) a 81,39% (CHAMENEOS).
Mesmo entre os piores resultados, o ganho
de desempenho com inline é razoável.
Portanto, faz muito sentido utilizar esta
otimização no compilador Cliente.
Os programas que obtiveram os piores
resultados são os considerados kernels. Estes
programas possuem poucos métodos e
geralmente, apenas um método é invocado
frequentemente. Portanto, os resultados
demonstraram que inline não faz sentido em
programas muito pequenos, com pouca
quantidade de métodos. Esta otimização atua
melhor quando diferentes métodos são
invocados frequentemente, pois neste caso o
overhead da não utilização de inline seria
maior. Além disso, uma maior porção do
código é disponibilizada para sofrer a
aplicação de outras otimizações. Outro ponto
importante a ressaltar é o fato de inline
reduzir consideravelmente a chamada de
métodos nativos.
Com o objetivo de reduzir o slowdown
ocasionado por inline para FASTA e SEARCH, a
entrada destes programas foi alterada para
obter um aumento do tempo de execução.
No caso de FASTA, ao invés de uma perda de
desempenho de 7,21%, este obte um ganho de
0,42%. Mesmo não sendo um ganho
significativo, este ainda é melhor do que uma
perda. Contudo, estes resultados somente
foram obtidos quando o tempo total de
execução
deste
programa
atingiu,
aproximadamente, uma hora. Já para SEARCH
O resultado obtido com o aumento do tempo
de execução foi significativo. Este programa
passou de um ganho 0,20% para 7,21%. Embora
os resultados obtidos pelo compilador Cliente
Revista Tecnológica
sejam significantes, estes últimos resultados
demonstram que a estratégia utilizada pela JVM
(parametrização estática de inline) é vantajosa, em
alguns casos, apenas para longos tempos de
execução.
4.1.2. Impacto de Inline no Compilador Servidor
Para o compilador Servidor, a estratégia da
aplicação de inline não é tão eficiente quanto a do
compilador Cliente. Quando comparado com o
tempo de execução do compilador Cliente, o
Servidor se mostrou mais rápido, tanto para a
otimização ligada quanto desligada. Porém, a
redução do tempo de execução com a aplicação de
inline é mais significativo no Cliente do que no
Servidor.
A variação do desempenho, considerando apenas
os programas que obtiveram ganho de
desempenho, foi de 0,09% (N-BODY) a 39,73%
(RAYTRACER). Esta variação tem um limite superior
inferior à variação constatada no compilador Cliente.
Além disso, a quantidade de programas que
obtiveram um melhor resultado com a otimização
ligada foi menor.
Com base nos resultados é possível concluir que
o compilador Servidor age melhor durante a
execução de programas que despendem um tempo
maior de execução. Um dos fatores para que isto
ocorra está no fato de que o compilador Servidor
tende a adiar, mais que o compilador Cliente, a
compilação de métodos. Isto pode ser verificado
com base no percentual de compilação de métodos.
A Tabela 2 apresenta informações sobre a
requisição e compilação de métodos, feita pelos dois
compiladores. Estas informações foram coletadas
durante a execução dos programas com a
otimização inline ativada. Comparando os dados de
um compilador com o outro, nota-se que o Servidor
é mais seletivo quanto a compilação de métodos.
Estes dados demonstram que este compilador adia
mais a decisão de compilar um método, quando
comparado ao compilador Cliente.
Maringá, v. 21. p. 103-118, 2012
A máquina virtual Java e a otimização inline: um estudo de caso
114
Programas
Cliente
Servidor
Métodos
Métodos
Requisitados
Compilados
Requisitados
Compilados
ANTLR
18054
497 (2,75%)
8086
335 (4,15%)
BLOAT
7229
748 (10,35%)
16328
562 (3,44%)
CHART
2439
675 (27,68%)
12493
343 (2,75%)
ECLIPSE
15839
3311 (20,90%)
50785
2240 (4,41%)
JYTHON
16402
1170(7,13%)
16934
963 (5,69%)
LUSEARCH
4440
420 (9,46%)
3655
360 (9,85%)
PMD
2876
991 (34,46%)
10393
607 (5,84%)
XALAN
4179
1581 (37,83%)
12704
1314 (10,34%)
EULER
524
70 (13,36%)
1446
50 (3,46%)
MOLDYN
155
27 (17,42%)
153
27 (5,88%)
MONTECARLO
680
146 (21,47%)
524
146 (21,56%)
RAYTRACER
323
35 (10,84)
394
23 (5,84%)
SEARCH
192
24 (12,50%)
1281
11 (0,86%)
BINARY-TREES
107
21 (19,63%)
65
8 (12,31%)
CHAMENEOS
145
69 (47,59%)
578
51 (8,82%)
FASTA
109
25 (22,94%)
49
3 (6,12%)
K-NUCLEOTIDE
252
64 (25,40%)
348
56 (16,09%)
MANDELBROT
49
18 (36,73%)
45
2 (4,44%)
N-BODY
85
18 (21,18%)
61
2 (3,28%)
SPECTRAL
245
23 (9,39%)
62
5 (8,06%)
Tabela 2. Histórico de compilação
No caso de um programa possuir um
tempo de execução curto e ser executado
pelo ambiente justamente com o compilador
Servidor, pode ocorrer de seus métodos
serem interpretados a maior parte deste
tempo, pois este compilador ainda estará
reunindo informações para decidir sobre a
compilação do método e a possível aplicação
de inline. Esta decisão de adiar a compilação e
otimização do método pode ocasionar uma
perca de desempenho em programas
menores. Porém, esta decisão se mostra
eficiente quando o programa possui muitos
métodos e um longo tempo de execução.
Assim como realizado com o compilador
Cliente, o tamanho da entrada dos programas
foi aumentado para avaliar os resultados do
compilador Servidor para programas com um
tempo maior de execução. Contudo,
diferentemente dos resultados obtidos pelo
Revista Tecnológica
compilador Cliente, em alguns casos o degradação
do desempenho se agrava.
Surpreendentemente o ganho de desempenho
não ocorreu na execução dos programas kernel,
utilizando o compilador Servidor. Porém, o problema
diminuiu consideravelmente na maioria dos
programas. Por exemplo, FASTA passou de uma
perda de 14,78% para, apenas, 0,28%, para um tempo
de execução de aproximadamente uma hora.
Embora esta perda seja insignificante, este programa
ainda não atingiu o que se espera da otimização. A
mesma situação ocorre com MOLDYN. Este passou de
uma perda de 27,79% para 9,68%, para um tempo de
execução aproximadamente de cinquenta minutos.
Por outro lado, outros programas obtiveram um
resultado positivo, tendo o ganho de desempenho
variando entre 3,90% e 69,38%. Isto ocorreu para
EULER (+3,90%), CHART (+20,84%), LUSEARCH (+30,19%)
e ANTLR (+69,38%). Estes que anteriormente não
obtiveram um bom desempenho (no caso de ANTLR,
Maringá, v. 21. p. 103-118, 2012
Rangel e Silvaiiiiii
115
inline ocasionava uma perda de 17,31%), foram
consideravelmente
beneficiados
pela
aplicação de inline, isto para um tempo de
execução superior a uma hora.
No caso do compilador Servidor, os
resultados demonstram que um ganho considerável de desempenho somente é obtido
por programas com longo tempo de
execução. Além disto, no compilador Servidor
a estratégia de aplicação de inline funciona
melhor para programas reais.
4.1.3. Compilador Cliente x Compilador
Servidor
Como mencionado anteriormente, a
estratégia do compilador Cliente é superior a
do Servidor. Quando a otimização foi ligada,
em média, os programas obtiveram um ganho
de 26,25%, contra apenas 6,67% para o
compilador Servidor. Porém, o compilador
Cliente não é o mais vantajoso entre os dois.
Este ganho de desempenho não significa que
o tempo total de execução do programa será
menor. Foram poucos os programas
utilizados em que isto ocorreu. Geralmente, o
compilador Servidor obtém um desempenho
muito superior ao do compilador Cliente,
mesmo com o problema da otimização inline
ocorrendo.
Com os dados da Tabela 2 é possível traçar
um comparativo entre o comportamento dos
dois compiladores. O compilador Servidor,
apesar de receber mais requisições, é muito
mais seletivo que o compilador Cliente. Seu
percentual de compilação foi de apenas 5,18%
das requisições recebidas. Enquanto isso, o
compilador Cliente compilou 13,36% das
requisições que recebeu. Mesmo compilando
um número menor de métodos, o compilador
Servidor obtém um tempo total de execução
menor que o compilador Cliente. Isto
demonstra que os problemas ocasionados
por inline são contornados pela aplicação de
outras otimizações.
Apenas um programa apresentou um
maior percentual de métodos compilados
pelo Servidor. ANTLR saltou de 2,75% para
4,15%. Contudo, seu desempenho no
compilador Servidor com inline ligado foi
17,31% pior do que com o inline desligado. Já
no compilador Cliente, este programa obteve
um ganho de desempenho de 24,03% no seu
tempo total de execução, quando inline foi
Revista Tecnológica
ativado. Isto caracteriza os problemas que podem
ocorrer pela agressividade do compilador Servidor e
também pela sua má utilização da otimização inline,
sendo uma delas a parametrização estática, o que
não leva em consideração as características do
programa.
4.1.4. Decisões de Inline
Para compreender o que ocasionou uma perda
de desempenho para os programas é necessário
analisar os seus perfis. Desta forma, os bytecodes
dos programas ANTLR, CHART, LUSEARCH, EULER,
MOLDYN, FASTA, N-BODY e SPECTRAL foram analisados
para gerar um perfil dos bytecodes utilizados. Devido
as restrições de espaço, os dados coletados não
serão apresentados.
Como o padrão de bytecodes dá origem ao
código de máquina, no momento em que o método
é compilado, programas com um padrão semelhante
de bytecodes irão gerar um padrão semelhante de
código de máquina. Desta forma, existe a
possibilidade de programas com as mesmas
características sofrerem o mesmo problema. Um
ponto interessante em relação aos bytecodes dos
programas que obtiveram uma perda de
desempenho é que o seu padrão foi alterado quando
utilizado o compilador Servidor.
Os bytecodes mais utilizados nestes programas
realizam operações aritméticas, o controle para o
carregamento de valores e cálculo. Este último gera
um alto custo de processamento. Além destas
instruções, outro tipo muito utilizado é o de
instruções de acesso e gravação de atributos de
objetos e invocações de métodos virtuais.
A maior parte das instruções de carregamento e
armazenamento de valores é substituída no
compilador Servidor por instruções com o prefixo
fast. Este tipo de instrução não foi utilizada pelo
compilador Cliente. Com isto, pode-se avaliar que
este tipo de instrução tem o objetivo de otimizar a
execução. Porém, este perfil de programa pode
sofrer degradação de desempenho para casos onde
a quantidade de métodos é pequena e exista uma
grande quantidade de bytecodes utilizados para
operações aritméticas.
Instruções fast, quando utilizado inline, fazem
com o que o tempo total de execução aumente. Isto
ocorre pelo fato de que a cópia de um método para
dentro de outro, utilizando este perfil de bytecodes,
degrada a geração de código. Isto ocasiona um
crescimento do código gerando pressão por
registradores e misses na cache. Portanto, um
programa que gere este perfil de instruções de
Maringá, v. 21. p. 103-118, 2012
A máquina virtual Java e a otimização inline: um estudo de caso
116
bytecode está propenso a sofrer destes
problemas, quando a otimização inline é
ativada.
Utilizando as informações sobre a
compilação
de
métodos
nos
dois
compiladores, juntamente com esta decisão
de substituir os bytecodes durante a execução
com o compilador Servidor, pode-se concluir
que estas não formam uma estratégia
consistente para programas pequenos.
Porém, esta estratégia tende a melhorar o
desempenho da maior parte dos programas
quando o tempo de execução é elevado.
4.2. ANÁLISE DETALHADA
Além de medir o tempo de execução com
inline desligado e ligado, durante a análise
experimental foram coletadas para alguns
programas informações de hardware por
meio da ferramenta Performance Application
Programming Interface (PAPI) (TERPSTRA et
al, 2010). As informações mais importantes
quando se trata da otimização inline são:
quantidade de instruções por ciclo (IPC) e
acessos à memória cache.
Para
o
compilador
Cliente
os
experimentos foram realizados para SEARCH
e FASTA. SEARCH apresentou o mesmo IPC,
para inline desligado ou ligado. Já FASTA
obteve uma redução do IPC, passando de
0,84 para 0,75 instruções por ciclo, quando a
otimização inline foi ligada. Estes resultados
demonstram que a otimização causa um
efeito negativo para FASTA, quanto à
quantidade de instruções executadas por
ciclo. Esta redução de IPC é uma das causas da
degradação de desempenho deste programa.
Para
o
compilador
Servidor
os
experimentos foram realizados apenas para
EULER, MOLDYN, FASTA, N-BODY e SPECTRAL. Não
foram coletadas informações para os outros
programas (que obtiveram uma queda de
desempenho) devido ao fato da ferramenta
utilizada não funcionar corretamente para o
benchmark DaCapo.
EULER e MOLDYN apresentaram um
pequeno aumento no IPC utilizando inline
(0,91 para 0,92 e 1,45 para 1,46
respectivamente). Fasta obteve uma redução
de IPC passando de 0,92 para 0,89. N-BODY se
manteve estável, este foi um dos motivos que
o levou a ter um desempenho insignificante.
Revista Tecnológica
Outros experimentos foram realizados com os
mesmos programas para avaliar o acesso à memória.
Para o compilador Cliente os dois programas
apresentaram uma redução na quantidade de
acertos à cache (de 1,64% para SEARCH e 1,93% para
FASTA). Para SEARCH esta redução não ocasionou
perda de desempenho. FASTA ainda apresentou um
aumento de 3% na quantidade de acessos à cache,
ocasionando perda de desempenho. Isto demonstra
que a qualidade do código gerado pelo compilador
cliente (utilizando inline) para estes programas não
possui uma boa localidade.
Já no compilador Servidor os resultados foram
mais variados. EULER apresentou um aumento, tanto
para os acertos, quanto para os erros de acesso à
cache. Porém, os erros aumentaram em 5,17%,
enquanto os acertos aumentaram em apenas 1,43%.
Este é um dos motivos deste programa ter perda de
desempenho, pois muitos erros de acesso a
memória cache ocorrem, quando a otimização é
ligada.
Para MOLDYN OS aumentos na quantidade de
acessos e erros foram insignificantes (de 0,09% e
0,58% respectivamente). Embora estes dados não
indiquem o que ocasionou a perda de desempenho
de 27,79%, possivelmente isto foi ocasionado pelo
tempo gasto pelo compilador. Como o compilador
Servidor é um compilador agressivo, ele tende a
gerar um alto tempo de compilação.
FASTA apresentou números negativos nos dois
aspectos. A quantidade de erros de acesso
aumentou em 4,25% e a quantidade de acertos de
memória aumentou em 19,05%. Estes dados
demonstram que a utilização da cache, quando a
otimização inline foi ligada, foi prejudicial para a
execução. Neste caso, ocorreu o mesmo problema
ocasionado com o compilador Cliente, o código
gerado pelo compilador Servidor possui uma
qualidade baixa, ocasionando uma má localidade.
N-BODY se manteve estável também quanto aos
acessos à cache, obtendo apenas um aumento na
quantidade de erros de 0,87% e um aumento
insignificante na quantidade de acertos de 0,001%.
Mesmo com este aumento nos erros de acesso, o
desempenho deste programa se manteve
consideravelmente estável. For fim, para SPECTRAL o
uso de inline não teve impacto na quantidade de
acessos à cache, contudo ocasionou uma redução de
35,14%
na
quantidade
de
acertos.
Consequentemente, ocasionando uma perda de
desempenho.
5. CONCLUSÕES E TRABALHOS FUTUROS
Maringá, v. 21. p. 103-118, 2012
Rangel e Silvaiiiiii
117
Em geral, os trabalhos que descrevem
ambientes Java com compilação dinâmica,
não apresentam uma análise detalhada do
impacto das otimizações aplicadas pelo
compilador. Embora, alguns trabalhos
(BURKE et al, 1999; CIERNIAK et al, 2000; GU
et al, 2000; SUGANUMA et al, 2000)
destaquem a importância da aplicação de
inline não descrevem a heurística utilizada,
nem apresentam o ganho real obtido por esta
otimização. Este trabalho descreveu de forma
detalhada a heurística utilizada na aplicação
de inline pela máquina Virtual Java da Sun,
como também apresentou uma análise
detalhada do ganho de desempenho obtido
por esta otimização.
A aplicação de inline realizada pelo
compilador Cliente se mostrou mais eficiente.
Apenas um programa, obteve perda de
desempenho. Por outro lado, a agressividade
do compilador Servidor se torna prejudicial
em muitos casos, ocasionando uma perca de
desempenho significativa em alguns casos.
Outro aspecto interessante é que o tempo de
execução é drasticamente reduzido quando
chamadas de métodos nativos sofrem a
aplicação de inline. Em média, os programas
utilizando o compilador Cliente obtiveram
uma melhora de 26,25% no seu desempenho.
Por outro lado, utilizando o compilador
Servidor, estes programas obtiveram uma
média geral de 6,67%. Esta diferença
comprova que nem sempre a agressividade é
a escolha correta.
O problema para o compilador Servidor
ocorre quando comparamos a sua execução
com inline ligado e desligado. Porém, o
compilador Servidor se mostra mais eficiente,
considerando-se apenas o tempo total de
execução. Os programas obtiveram na média
um tempo total de execução menor quando
este compilador foi utilizado. Contudo, este
tempo poderia ser ainda melhor se a
estratégia utilizada na aplicação de inline
fosse ajustada para cada programa.
Uma alteração nos algoritmos que fazem
parte da aplicação desta otimização
possivelmente melhoraria o benefício de
inlining. Uma alteração que, pelo menos,
impeça a degradação do desempenho dos
programas já seria interessante. Informações
sobre o perfil de bytecodes, a quantidade de
chamadas a métodos, o tamanho e o tempo
de execução esperado para um determinado
Revista Tecnológica
programa podem ser utilizadas para realizar esta
alteração.
Um trabalho futuro será alterar a política de
aplicação de inline, inicialmente com o objetivo de
evitar a perda de desempenho que ocorre em alguns
programas, de forma que inline possibilite um ganho
de desempenho para todos os programas que
possuam um determinado perfil.
O objetivo principal é alcançar um ganho de
desempenho significativo para todas os programas
Java que utilizem esta JVM, realizando o que é
proposto pela otimização inline. Isto pode ser
alcançado com a utilização de parâmetros dinâmicos
para o controle da utilização da otimização. Hoje
estes parâmetros são fixos e independem do perfil
do programa. Sendo assim, é possível utilizar as
informações sobre as características dos programas
para parametrizar a seleção e aplicação de inline de
métodos. Este é um trabalho futuro mais ambicioso,
alterar dinamicamente a parametrização de inline.
REFERÊNCIAS
AHO, A. V., SETHI, R., ULLMAN, J. D. Compiladores,
Princípios, Técnicas e Ferramentas. São Paulo, Brasil:
Pearson, 2007.
APPEL, A. W. Modern Compiler Implementation in C.
New York, USA: Cambridge University Press, 1998.
ARNOLD, M., FINK, S. J., SARKAR, V., SWEENEY, P. F.
A Comparative Study of Static and Profile-based
Heuristics for Inlining. In: ACM SIGPLAN Workshop
on Dynamic and Adaptive Compilation and
Optimization. USA: ACM Press, 2000.
BAKER, H. G. Inlining Semantics for Subroutines
which are Recursive. ACM Sigplan Notices, v. 27, p.
39–46, 1992.
BLACKBURN, S. M., GARNER, R., HOFFMAN, C.,
KHAN, A. M., McKINLEY, K. S., BENTZUR, R., DIVAN,
A., FEINBERG, D., FRAMPTON, D., GUYER, S. Z.,
HIRZEL, M., HOSKING, A., JUMP, M., LEE, H., MOSS,
J. E. B., PHANSALKAR, A., STEFANOVI ´ C, D.,
VANDRUNEN,
T.,
von
DINCKLAGE,
D.,
WIEDERMANN, B. The DaCapo Benchmarks: Java
Benchmarking Development and Analysis. In:
Proceedings of the Conference on Object-oriented
Programming Systems, Languages, and Applications,
volume 41, pages 169-190, USA. ACM, 2006.
BULL, M., SMITH, L., WESTHEAD, M., HENTY, D.,
DAVEY, R. Benchmarking Java Grande Applications.
In: Proceedings of the International Conference on
The Practical Applications of Java, pages 63-73, 2000.
BURKE, M. G., CHOI, J.-D. The Japaleno Dynamic
Optimizing Compiler for Java. In: Proceedings of the
Java Grande Conference, pages 129-141, 1999.
Maringá, v. 21. p. 103-118, 2012
A máquina virtual Java e a otimização inline: um estudo de caso
118
CHEN, W. Y., CHANG, P. P., CONTE, T. M.,
HWU, W. W. The Effect of Code Expanding
Optimizations on Instruction Cache Design.
IEEE Transactions on Computers, v.42 n.9,
p.1045-1057, 1989.
CHIBA, S. Macro Processing in ObjectOriented Languages. In: Proceedings of the
Technology of Object-Oriented Languages
and Systems. Tsukuba, Japão: IEEE, IEEE
Press, 1998.
CIERNIAK, M., LUEH, G.-Y, STICHNOTH, J. M.
Practicing JUDO: Java Under Dynamic
Optimizations. In Proceedings of the
Conference on Programming Languages
Design and Implementation, Vancouver,
Canadá. ACM SIGPLAN, 2000.
CRAIG, I. D. Virtual Machines. Inglaterra:
Springer-Verlag London Limited, 2006.
DAMAS, L. M. D. Linguagem C. Brasil: LTC,
2007.
DA SILVA, A. F., SANTOS COSTA, V. Our
Experiences with Optimizations in Sun's Java
Just-in-time Compilers. In Proceedings of the
Brazilian Symposium on Programming
Languages, pages 51-65, Brasil. SBC, 2006.
DEITEL, H. M., DEITEL, P. J. Java Como
Programar. Porto Alegre, Brasil: Prentice Hall,
2010.
DEPARTMENT OF DEFENSE. Ada 2005
Reference Manual. USA, 2006.
FULGHAM, B. The Computer Language
Benchmark
Game.
http://
shootout.
alioth.debian.org/; acesso em 20 de Março
2010.
GU, W., BURNS, N. A., et al. The Evolution of a
High-Performing Java Virtual Machine. IBM
Sytems Journal, 39(1): 135-150, 2000.
LEDGARD, H. Reference Manual for the ADA
Programming Language. USA: Springer-Verlag
New York, Inc., 1983.
LIANG, S., BRACHA, G. Dynamic Class
Loadding in Java VirtualMachine. In:
Proceedings of the Conference on ObjectOriented Programming Systems, Languages
and Applications, pp. 36–44, Vancouver,
Canada, October 1998.
LINDHOLM, T., YELLIN, F. The Java Virtual
Machine
Specification
Second
Edition.
California, USA: Addison Wesley, 1999.
Revista Tecnológica
LUEH, G.-Y.; GROSS, T.; ADL-TABATABAI, A.-R. Global
Register Allocation Based on Graph Fusion. In:
Proceedings of the International Workshop on
Languages and Compilers for Parallel Computing.
Londres: Springer-Verlag, 1997.
LUTZ, M., ASCHER, D. Aprendendo Python. Porto
Alegre, Brasil: Bookman, 2007.
MEYER, J., DOWNING, T. Java Virtual Machine. USA:
O'Reilly, 1997.
MICROSYSTEMS, S. The Java HotSpot Virtual
Machine. Technical report, Sun Developer Network
Community, 2010.
MUCHNICK, S. S. Advanced Compiler Design And
Implementation. USA: Morgan Kauf-mann, 1997.
SCOTT, M. L. Programming Language Pragmatics.
USA: Elsevier, 2008.
SEBESTA, R. W. Conceitos de Linguagens de
Programção. Brasil: Bookman, 2011.
SERRANO, M. A Fresh Look to Inlining Decision. In:
Proceedings of the International Computer
Symposium. Cidade do México, México: Université
de Montréal, 1995.
STREIB, J. T. Guide To Assembly Language. USA:
Springer Verlag, 2011.
SUGANUMA, T., OGASAWARE, T., TAKEUCHI, M.,
YASUE, T., KAWAHITO, M., ISHIZAKI, K., KOMATSU,
H., NAKATANI, T. Overview of the IBM Java Just-inTime Compiler. IBM Systems Journal, v. 39, n. 1, p.
175–193, 2000.
STROUSTRUP, B. What is “Object-Oriented
Programming”? In: Proceedings of the European
Conference on Object Oriented Programming. Paris,
França: Springer-Verlag, 1991.
TANENBAUM, A. S. Organizacão Estruturada de
Computadores. Brasil: Prentice-Hall, 2006.
TERPSTRA, D., JAGODE, H., YOU, H., DONGARRA, J.
Collecting Performance Data with PAPI-C. In
Proceedings of the Parallel Tools Workshop, pages
63-73, Germany. Springer Verlag, 2010.
VERNNERS, B. Inside the Java 2 Virtual Machine. New
York, USA: Mc Graw Hill, 1999.
ZHAO, P.; AMARAL, J. N. To Inline or Not to Inline?
Enhanced Inlining Decisions. In Proceedings of the
Workshop on Languages and Compilers for Parallel
Computing, 2003.
Maringá, v. 21. p. 103-118, 2012
Download