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