a nova gerac¸˜ao do yap prolog: um ambiente experimental de...
TRANSCRIPT
UNIVERSIDADE ESTADUAL DE MARINGA
CENTRO DE TECNOLOGIA
DEPARTAMENTO DE INFORMATICA
PROGRAMA DE POS-GRADUACAO EM CIENCIA DA COMPUTACAO
GEORGE SOUZA OLIVEIRA
A Nova Geracao do YAP Prolog: Um ambiente experimental decompilacao just-in-time baseada em tracos de execucao
Maringa
2013
GEORGE SOUZA OLIVEIRA
A Nova Geracao do YAP Prolog: Um ambiente experimental decompilacao just-in-time baseada em tracos de execucao
Dissertacao apresentada ao Programa dePos-Graduacao em Ciencia da Computacaodo Departamento de Informatica, Centrode Tecnologia da Universidade Estadualde Maringa, como requisito parcial paraobtencao do tıtulo de Mestre em Ciencia daComputacao.
Orientador: Prof. Dr. Anderson Faustinoda Silva
Maringa2013
Dados Internacionais de Catalogação na Publicação (CIP)
(Biblioteca Central - UEM, Maringá, PR, Brasil) Oliveira, George Souza O48n A nova geração do YAP Prolog : um ambien te
experimental de compilação just-in-time baseada em traços de execução / George Souza Oliveira. -- Maringá, 2013.
159 f. : figs., tabs. Orientador: Prof. Dr. Anderson Faustino da Silva. Dissertação (mestrado) - Universidade Es tadual de
Maringá, Centro de Tecnologia, Departamento de Informática, Programa de Pós-Graduação em Ciência d a Computação, 2013.
1. YAP Prolog (Interpretador de linguage ns
lógicas). 2. Compilação just-in-time (Computação). 3. Compilação baseada em traços de execução (Computação). I. Silva, Anderson Faustino da, orient. II. Universidade Estadual de Maringá. Centr o de Tecnologia. Departamento de Informática. Program a de Pós-Graduação em Ciência da Computação. III. Título.
CDD 21.ed. 005.115
GVS-000693
Ao meu Deus, Jesus Cristo, Criador dos ceus e da terra.
AGRADECIMENTOS
Primeiramente, gostaria de agradecer a todos da minha famılia, que apesar de terem
sido privados da minha presenca, me compreenderam, dedicaram carinho e atencao a
todo o momento. Agradeco tambem ao meu orientador, Anderson Faustino da Silva, pela
dedicacao e exemplo prestados. Anderson, voce e um brilhante orientador e amigo de
verdade.
Agradeco tambem, a todos os meus amigos, aqueles presentes pelos momentos de
descontracao e trocas de ideias, e aqueles distantes que sempre estiveram torcendo
pelo meu sucesso. Dentre eles, agradeco especialmente ao Juliano, que por ser mais
experimente, foi capaz de me dar ideias brilhantes para que eu pudesse utilizar no meu
trabalho.
Por fim, e nao menos importante, agradeco a Coordenacao de Aperfeicoamento de
Pessoal de Nıvel Superior (CAPES) pelo apoio financeiro concedido a este trabalho.
A Nova Geracao do YAP Prolog: Um ambiente experimental de compilacaojust-in-time baseada em tracos de execucao
RESUMO
As pesquisas voltadas para a implementacao eficiente da linguagem Java apostaram no
uso de um sistema de compilacao just-in-time para otimizar codigo em tempo de execucao.
Alem disso, pesquisas mais recentes tem demonstrado que, ao inves de compilar unidades
de codigo limitados a um unico escopo, compilar os tracos de execucao mais frequentes
acarreta um melhor ganho de desempenho. Baseado neste contexto, este trabalho descreve
uma nova arquitetura para o sistema YAP Prolog, que e capaz de construir tracos de
execucao e entao, gerar codigo nativo para eles. Os resultados obtidos indicaram que a
compilacao just-in-time baseada nessa estrategia alcanca um ganho de desempenho de
25, 2%, variando entre 1, 006% e 134, 4%.
Palavras-chave: YAP Prolog. Compilacao just-in-time. Compilacao baseada em tracos
de execucao.
New Generation of YAP Prolog: An experimental environment of trace-basedjust-in-time compilation
ABSTRACT
Researches directed to efficient implementation of Java relied on a just-in-time (JIT)
system to optimize code at runtime. Furthermore, recent researches has shown that
compiling common execution traces, instead of code units limited to a single scope, leads to
a better performance gain. Based on this context, this work introduces a new architecture
for the YAP Prolog, which is able to build traces and then generate native code from
them. The results indicated trace-based JIT compilation reaches 25.2% of performance
gain, ranging from 1.006% to 134.4%.
Keywords: YAP Prolog. Just-in-time compilation. Trace-based compilation.
LISTA DE FIGURAS
Figura - 2.1 Arquitetura generica de um sistema JIT. . . . . . . . . . . . . . . 24
Figura - 3.1 Organizacao da Memoria na WAM. . . . . . . . . . . . . . . . . . 57
Figura - 4.1 Estrutura interna da versao atual do sistema YAP (adaptado de
Costa et al. (2012)). . . . . . . . . . . . . . . . . . . . . . . . . . 78
Figura - 4.2 Estrutura interna do sistema proposto. . . . . . . . . . . . . . . . 79
Figura - 4.3 Fases do Compilador JIT. . . . . . . . . . . . . . . . . . . . . . . 86
Figura - 4.4 Exemplo de um traco gerado para uma clausula com 3 instrucoes. 89
Figura - 4.5 Trecho de um traco com caracterıstica linear em (b) que repre-
senta o fluxo de execucao (marcado em vermelho) em (a). . . . . . 91
Figura - 4.6 Trecho do traco representado na Figura - 4.5, apos o fluxo de
execucao (marcado em vermelho) mudar apos o teste condicional. 91
Figura - 4.7 Trecho de um traco em uma instrucao YAAM que causa uma
excecao para coleta de lixo. Isso implica que, em codigo nativo,
as condicoes para esse tipo de excecao sao as mesmas para codigo
interpretado. O efeito e que, mesmo executando codigo nativo, a
coleta de lixo sempre ocorre em codigo interpretado. . . . . . . . . 92
Figura - 4.8 Trecho do traco representado na Figura - 4.6, mas usando um
bloco elementar para suportar instrucoes condicionais onde um
dos blocos de destino causam backtrack. . . . . . . . . . . . . . . . 93
Figura - 4.9 Exemplo de traco que contem uma instrucao de indexacao. . . . . 95
Figura - 4.10 Traco da Figura - 4.9-(b) apos ser reconstruıdo e recompilado. . . 96
Figura - 4.11 Organizacao do sistema com suporte aos novos predicados. . . . . 97
Figura - 5.1 Impacto do Interpretador Instrumentado no ambiente de execucao.
Quanto menor a barra, maior a desaceleracao. . . . . . . . . . . . 115
Figura - 5.2 Aceleracao/desaceleracao dos programas teste no modo smart
JIT (com e sem recompilacao) sobre o modo somente interpretar.
Valores menores que 1 indicam desaceleracao. . . . . . . . . . . . 117
Figura - 5.3 Percentuais do fluxo de execucao nos componentes ativos no modo
smart JIT. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117
Figura - 5.4 Aceleracao/desaceleracao dos programas teste no modo compilac~ao
contınua (com e sem recompilacao) sobre o modo somente interpretar.
Valores menores que 1 indicam desaceleracao. . . . . . . . . . . . 121
Figura - 5.5 Percentuais do fluxo de execucao nos componentes ativos no modo
compilac~ao contınua. . . . . . . . . . . . . . . . . . . . . . . . . 122
Figura - 5.6 Aceleracao/desaceleracao dos programas teste no modo somente
compilar sobre o modo somente interpretar. Valores menores
que 1 indicam desaceleracao. . . . . . . . . . . . . . . . . . . . . . 124
Figura - 5.7 Percentuais do fluxo de execucao nos componentes ativos no
modo somente compilar. O grafico esta representado em escala
logaritmica para destacar os tempos do Compilador JIT. . . . . . . 124
Figura - 5.8 Tempos de construcao, compilacao e execucao dos tracos na
configuracao smart JIT sem recompilacao. Ti representa o traco
de numero i. Valores em vermelho indicam tracos nao uteis. . . . 126
Figura - 5.9 Tempos de construcao, compilacao e execucao dos tracos na
configuracao smart JIT com recompilacao. Ti representa o traco
de numero i. Valores em vermelho indicam tracos nao uteis. . . . 127
Figura - 5.10 Taxas do fluxo de execucao na manutencao das areas de memoria
do sistema sobre o tempo total de execucao de cada programa
avaliado. Os valores sao praticamente os mesmos em todos os
modos de execucao. . . . . . . . . . . . . . . . . . . . . . . . . . . 130
Figura - 6.1 Arquitetura proposta para trabalhos futuros. . . . . . . . . . . . . 137
LISTA DE TABELAS
Tabela - 2.1 Comparacao entre os sistemas JIT. . . . . . . . . . . . . . . . . . 49
Tabela - 5.1 Programas-teste habitualmente utilizados pela comunidade ci-
entıfica de Prolog. . . . . . . . . . . . . . . . . . . . . . . . . . . 112
Tabela - 5.2 Programas-teste do benchmark shootout. . . . . . . . . . . . . . . 113
SUMARIO
1 Introducao 12
2 Compilacao JIT 17
2.1 Historico . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
2.2 Arquitetura de Sistemas com Compilacao JIT . . . . . . . . . . . . . . . . 23
2.2.1 Compilador Base e Compilador Otimizador . . . . . . . . . . . . . . 25
2.2.2 Offline Profiler . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
2.2.3 Online Profiler . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
2.2.4 Monitor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
2.2.5 Gerente de Codigo . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
2.2.6 Gerente de Compilacao . . . . . . . . . . . . . . . . . . . . . . . . . 28
2.2.7 Repositorio de Codigo . . . . . . . . . . . . . . . . . . . . . . . . . 28
2.2.8 Motor de Execucao . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
2.3 Princıpios de Compilacao JIT . . . . . . . . . . . . . . . . . . . . . . . . . 29
2.3.1 Manutencao dos Sistemas de Compilacao e Execucao . . . . . . . . 30
2.3.2 Acionamento do Sistema de Compilacao . . . . . . . . . . . . . . . 31
2.3.3 Selecao de Unidades de Compilacao . . . . . . . . . . . . . . . . . . 33
2.3.4 Geracao de Versoes Especializadas . . . . . . . . . . . . . . . . . . . 36
2.3.5 Implementacao e Desempenho . . . . . . . . . . . . . . . . . . . . . 39
2.4 Sistemas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
2.5 Consideracoes Finais . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
3 Prolog 53
3.1 Controle da Linguagem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
3.2 Maquina Abstrata de Warren . . . . . . . . . . . . . . . . . . . . . . . . . 54
3.2.1 O Estado Interno da WAM e a Organizacao da Memoria . . . . . . 55
3.2.2 Conjunto de Instrucoes . . . . . . . . . . . . . . . . . . . . . . . . . 57
3.3 Melhorando o Desempenho da WAM . . . . . . . . . . . . . . . . . . . . . 62
3.3.1 Especializar a Unificacao . . . . . . . . . . . . . . . . . . . . . . . . 62
3.3.2 Otimizar Selecao de Clausulas . . . . . . . . . . . . . . . . . . . . . 63
3.3.3 Gerar Codigo Nativo . . . . . . . . . . . . . . . . . . . . . . . . . . 64
3.3.4 Utilizar Analise Global . . . . . . . . . . . . . . . . . . . . . . . . . 65
3.3.5 Utilizar Memoizacao . . . . . . . . . . . . . . . . . . . . . . . . . . 66
3.4 Implementacoes Prolog . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
3.5 Consideracoes Finais . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70
4 O Ambiente Experimental de Compilacao Just-in-Time Baseada em
Tracos de Execucao 73
4.1 Yet Another Prolog . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
4.1.1 Estruturas de Dados e Organizacao da Memoria . . . . . . . . . . . 74
4.1.2 Principais Diferencas com a WAM . . . . . . . . . . . . . . . . . . . 75
4.1.3 Estrutura das Clausulas . . . . . . . . . . . . . . . . . . . . . . . . 77
4.1.4 Organizacao do Sistema . . . . . . . . . . . . . . . . . . . . . . . . 77
4.2 A Nova Geracao do YAP . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
4.3 Os Modulos da Nova Geracao do YAP . . . . . . . . . . . . . . . . . . . . 81
4.3.1 Motor de Execucao . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
4.3.2 Compilador JIT . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
4.3.3 Gerente de Codigo . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
4.3.4 Construtor de Tracos . . . . . . . . . . . . . . . . . . . . . . . . . . 88
4.3.5 Modulo de Recompilacao . . . . . . . . . . . . . . . . . . . . . . . . 95
4.4 Os Predicados Suportados na Nova Geracao do YAP . . . . . . . . . . . . 96
4.4.1 Predicados para Analises de Codigo . . . . . . . . . . . . . . . . . . 98
4.4.2 Predicados para Transformacoes de Codigo . . . . . . . . . . . . . . 99
4.4.3 Predicados para Geracao de Codigo . . . . . . . . . . . . . . . . . . 100
4.4.4 Predicados para Configuracao Geral . . . . . . . . . . . . . . . . . . 101
4.4.5 Predicados para Depuracao . . . . . . . . . . . . . . . . . . . . . . 104
4.4.6 Predicados para Coleta de Perfil de Execucao . . . . . . . . . . . . 109
4.5 Consideracoes Finais . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109
5 Avaliacao Experimental 111
5.1 Metodologia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112
5.2 Impacto do Interpretador Instrumentado . . . . . . . . . . . . . . . . . . . 113
5.3 Desempenho dos Diferentes Modos de Execucao . . . . . . . . . . . . . . . 115
5.3.1 O Desempenho de Smart JIT . . . . . . . . . . . . . . . . . . . . . 117
5.3.2 O Desempenho de Compilacao Contınua . . . . . . . . . . . . . . . 121
5.3.3 O Desempenho de Somente Compilar . . . . . . . . . . . . . . . . . 123
5.4 Desempenho na Construcao dos Tracos . . . . . . . . . . . . . . . . . . . . 125
5.5 Consideracoes Finais . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 131
6 Conclusoes e Trabalhos Futuros 133
6.1 Motivacoes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 134
6.2 A Nova Geracao do YAP . . . . . . . . . . . . . . . . . . . . . . . . . . . . 134
6.3 Resultados Alcancados . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 135
6.4 Trabalhos Futuros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136
6.4.1 Gerente de Codigo . . . . . . . . . . . . . . . . . . . . . . . . . . . 136
6.4.2 Compilador JIT . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137
6.4.3 Motor de Execucao . . . . . . . . . . . . . . . . . . . . . . . . . . . 138
6.4.4 Offline Profiler . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139
6.4.5 Online Profiler . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 140
6.4.6 Cache . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 141
6.5 Consideracoes Finais . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142
REFERENCIAS 144
12
1
Introducao
Linguagens que implementam programacao logica (Colmerauer, 1990; Henderson e So-
mogyi, 2002) possuem uma clara correspondencia a logica matematica e expressam a
computacao sem descrever o seu fluxo de controle. Em outras palavras, a descricao do
programa e baseada em “o que” realizar, em vez de “como” realizar, ao contrario do que
ocorre na programacao imperativa (Sebesta, 2009). Isso e baseado na premissa de que
qualquer algoritmo consiste de uma especificacao logica (a descricao das propriedades do
resultado esperado) e um controle (a forma de executar esta especificacao). De posse da
especificacao logica, o proprio sistema e encarregado de prover o controle e encontrar a
solucao do problema (caso esta exista). Esse estilo de programacao torna as linguagens
logicas tanto mais faceis quanto mais difıceis se comparadas a outras estrategias de
programacao. Facil porque a forma de encontrar a solucao fica a cargo do ambiente
de execucao da propria linguagem e difıcil porque esquemas de programacao mais comuns
nao sao suportados.
A primeira linguagem de programacao logica surgiu no inıcio dos anos 70 (Kowalski,
1988) como um provador de teoremas especializado, que eventualmente evoluiu para uma
linguagem mais poderosa chamada Prolog (Colmerauer, 1990; Sterling e Shapiro, 1994).
Esta ultima descreve a programacao como um subconjunto de logica de primeira ordem,
as clausulas de Horn (Horn, 1951), sendo portanto, uma linguagem declarativa (Lloyd,
1994). Essa caracterıstica torna Prolog uma linguagem de programacao simples e eficiente
na implementacao dos conceitos de unificacao e busca.
A implementacao de linguagens declarativas, de Prolog em particular, e uma tarefa
difıcil, visto que as maquinas tradicionais sao sequenciais, uma caracterıstica nao existente
neste tipo de linguagem (pelo menos nao de forma explıcita). Alem disso, como o
13
controle de execucao fica a cargo da propria linguagem, o desenvolvedor se ve obrigado
a considera-lo e projeta-lo. Isso significa que as estrategias empregadas para criacao
de compiladores ou interpretadores para linguagens imperativas nao sao suficientes para
a criacao de compiladores ou interpretadores para linguagens declarativas, e portanto,
logicas.
Uma alternativa para implementacao de Prolog surgiu com a Maquina Abstrata
de Warren (WAM) (Aıt-Kaci, 1991; Warren, 1983), um modelo que permite compilar
codigo Prolog para uma representacao intermediaria sequencial. Com esse modelo, um
ambiente Prolog pode ser implementado tanto por hardware (Van Roy e Despain, 1992)
quanto por software (Carlsson e Mildner, 2012; Costa et al., 2012; Diaz et al., 2012;
Hermenegildo et al., 2012; Swift e Warren, 2012; Taylor, 1996; Van Roy, 1990; Wielemaker
et al., 2012). Na verdade, a implementacao por hardware consiste apenas de um conceito
teorico, visto que e uma implementacao difıcil devido a alta granularidade das instrucoes
WAM. Alem disso, o desenvolvimento do hardware ao longo dos anos e o aumento
do numero de instrucoes com baixa granularidade para as arquiteturas atuais mostrou
que o uso desse tipo de abordagem e inviavel. Mesmo que um hardware especializado
fosse criado, seria difıcil, mesmo para os mais adeptos, adquirir uma maquina que so
executasse Prolog. Por fim, a dificuldade em obter bom desempenho em hardware
tradicional somado ao custo de implementacao de hardware exclusivo justificam que
ambientes Prolog tenham sido essencialmente projetados por software.
Por outro lado, a implementacao por software acarreta um alto overhead proveniente
da interpretacao de codigo WAM para codigo de maquina equivalente. Alem disso, a
alta granularidade das instrucoes WAM impedem a aplicacao de diversas otimizacoes de
codigo (Muchnick, 1997). Uma alternativa e implementar Prolog para a geracao de codigo
nativo. Os primeiros sistemas a utilizarem essa alternativa foram Aquarius (Van Roy,
1990) e Parma (Taylor, 1996), que utilizavam analise global (Warren et al., 1988) para
especializar a unificacao e otimizar a selecao de clausulas. Embora essas implementacoes
tenham demonstrado que a execucao de Prolog e comparavel a das linguagens imperativas
em alguns casos, as mesmas foram abandonadas por serem muito difıceis de manter.
Dessa forma, os sistemas Prolog mais comuns sao atualmente implementados como
interpretadores (Carlsson e Mildner, 2012; Costa et al., 2012; Diaz et al., 2012; Her-
menegildo et al., 2012; Swift e Warren, 2012; Tarau, 1991; Wielemaker et al., 2012).
Como alternativa, alguns deles tambem oferecem execucao de codigo nativo, como
GNU-Prolog (Diaz et al., 2012), SICStus Prolog (Carlsson e Mildner, 2012), CIAO
(Hermenegildo et al., 2012) BinProlog (Tarau, 1991) e XSB Prolog (Swift e Warren,
2012). Dentre as implementacoes existentes, YAPc (Silva e Costa, 2007) se destaca por
14
empregar uma abordagem diferente, que consiste em gerar codigo em tempo de execucao
para as regioes executadas com mais frequencia. YAPc foi projetado para ser utilizado
com o interpretador YAP (Costa et al., 2012), a fim de torna-lo um ambiente de modo
misto de execucao. Um sistema deste porte possui a vantagem de utilizar informacoes
coletadas em tempo de execucao para especializar o codigo nativo a ser gerado, sem a
necessidade de implementar um analisador global (Warren et al., 1988).
Em sıntese, o princıpio basico de YAPc e o mesmo dos compiladores just-in-time
(JIT) desenvolvidos para sistemas Java, como Jikes (Arnold et al., 2000; Burke et al.,
1999), JUDO (Cierniak et al., 2000) e Sun Hotspot (Kotzmann et al., 2008; Paleczny et
al., 2001): em primeiro lugar, cada unidade de codigo (que no contexto de YAPc, se trata
de uma clausula) e instrumentada com contadores e entao interpretada. Cada contador
e incrementado na medida em que as unidades associadas sao invocadas. Durante este
processo, um profiler coleta informacoes sobre o programa em execucao e, ao atingir um
certo limite de invocacao, a unidade e selecionada para compilacao. Por fim, o compilador
JIT e acionado a fim de gerar codigo especializado baseado nas informacoes anteriormente
coletadas.
Silva e Costa (2007) mostraram que YAP obtem ganho de desempenho na execucao de
programas com longas entradas quando o YAPc e acionado. Apesar de suas vantagens,
YAPc possui algumas restricoes de implementacao que o impede de gerar resultados
melhores, principalmente para programas com pouco tempo de execucao (Silva, 2006),
que sao:
1. Uma quantidade limitada de otimizacoes (transformacoes) de codigo. Geralmente,
a escolha das otimizacoes para determinadas unidades de codigo influenciam o
desempenho geral do sistema e o numero reduzido delas limita a variedade de
escolhas.
2. Um alocador de registradores baseado em um algoritmo de coloracao de grafo
(Muchnick, 1997), que e capaz de gerar bons resultados, mas que possui um custo
que nao compensa ser aplicado em ambientes de compilacao JIT. O fato e que
compiladores JIT compilam codigo em tempo de execucao e, por esta razao, devem
ser leves e eficientes de tal modo que gerem codigo aceitavel no menor tempo possıvel.
3. O compilador mostrou ser apenas uma prova de conceito. Embora o trabalho com
o YAPc tenha demonstrado ser uma boa alternativa de implementacao de Prolog,
este nao se tornou uma implementacao disponibilizada nas versoes do YAP.
15
Em vista disso, o principal objetivo deste trabalho e projetar e implementar uma
nova arquitetura de compilacao JIT para o YAP, mais precisamente, um modulo de
compilacao JIT baseada em tracos de execucao. O esquema de compilacao baseada em
tracos de execucao difere da estrategia adotada em YAPc, bem como da maioria dos
sistemas JIT atuais, por nao se limitar ao escopo de uma unidade de codigo tradicional,
como um metodo, funcao ou clausula.
Um objetivo secundario deste trabalho e fornecer ao YAP a capacidade de ser um
ambiente experimental de compilacao JIT para Prolog. Esta ultima proposta pode ser
concretizada pelo desenvolvimento de predicados nativos, que auxiliam na configuracao
do sistema quanto a forma que o mesmo executa, compila e gerencia o codigo. Em
especıfico, tais predicados podem: (1) modificar os modos de execucao de codigo, (2)
aplicar transformacoes de codigo na variedade e ordem que o usuario desejar, (3) habilitar
analises de codigo, tais como o tempo gasto para compilar e executar codigo nativo e (4)
realizar tarefas de depuracao quando determinada condicao for satisfeita.
Este trabalho pretende conciliar os objetivos estabelecidos com o padrao de funcio-
namento que tem sido considerado desde a implementacao do YAPc, que consiste em
preservar a ilusao de que o sistema executa diretamente o programa na forma que o
programador escreveu, invocando o ambiente de compilacao e otimizando codigo de forma
transparente. Esse padrao traz alguns pontos a serem considerados (Silva, 2006):
1. O programador deve ser livre para editar qualquer clausula do sistema;
2. O programador deve ser capaz de entender a execucao do programa e qualquer erro
que ocorra no programa em termos do codigo fonte e construcoes da linguagem; e
3. O programador deve se isolar do fato dos programas serem compilados.
Portanto, a proposta deste trabalho busca alcancar dois resultados finais:
1. Tornar Prolog mais proximo das linguagens tradicionais, como C e Java, ou que,
no mınimo, a arquitetura proposta seja capaz de executar codigo mais rapidamente
que a versao atual do sistema.
2. Flexibilidade dos parametros e novos modos de execucao, para que o usuario realize
diversos experimentos;
Os resultados obtidos na avaliacao deste trabalho mostraram ganho de desempenho
para a maioria dos programas, que variou de 1, 006% a 134, 4%, com uma media de
25, 2% sobre a versao atual do sistema. Esses valores foram obtidos a partir da execucao
16
do sistema em diversas configuracoes diferentes, com o uso de predicados que modificam
os parametros e modos de execucao.
Isso mostra, portanto, que os resultados buscados neste trabalho foram alcancados.
Alem disso, os resultados obtidos apontaram diversos trabalhos futuros, com o objetivo
de diminuir custos e aperfeicoar os tracos construıdos para cada programa.
A fim de apresentar os conceitos iniciais, a nova arquitetura, resultados e trabalhos
futuros, esta dissertacao esta organizada da seguinte forma: O Capıtulo 2 contem a
teoria relacionada a compilacao just-in-time, seus conceitos, historico e princıpios, alem
de exemplos de alguns sistemas. O Capıtulo 3 contem os fundamentos de Prolog, com
o foco na WAM e suas formas de implementacao. O Capıtulo 4 apresenta a arquitetura
do novo sistema como uma extensao da arquitetura atual, apresentada por Costa et al.
(2012). O Capıtulo 5 apresenta os resultados obtidos e, por fim, o Capıtulo 6 apresenta
as conclusoes e trabalhos futuros.
17
2
Compilacao JIT
As implementacoes originais de linguagens de programacao interativas e de alto-nıvel
(Scott, 2009; Sebesta, 2009) focavam no desenvolvimento de maquinas virtuais eficientes
que provessem portabilidade aos programas desenvolvidos nessas linguagens. Mais re-
centemente diversas pesquisas tem focado nao apenas no desenvolvimento de maquinas
virtuais que proveem portabilidade, mas tambem de maquinas virtuais cuja execucao
seja eficiente, em termos de uso de recursos e tempo de execucao. Para alcancar
este objetivo, as maquinas virtuais atuais incluem em sua arquitetura um sistema de
compilacao just-in-time (JIT).
Em contraste com compiladores tradicionais, em uma maquina virtual com compilacao
JIT o tempo de compilacao esta incluıdo no tempo total de execucao, pois a compilacao
ocorre durante a execucao do programa. Portanto, e uma questao crıtica decidir quando,
o que e como compilar os programas. Mais especificamente, o sistema de compilacao deve
apenas compilar codigos se o tempo gasto na compilacao for amortizado pelo desempenho
ganho pelo codigo compilado. O custo de compilacao e o desempenho obtido e uma
questao crucial em ambientes com compilacao JIT. Porem, tais ambientes possuem um
grande benefıcio: eles podem explorar informacoes obtidas em tempo de execucao para
decidir quais otimizacoes aplicar, alem de quais porcoes de codigo compilar (Santos et al.,
2013).
As primeiras maquinas virtuais com compilacao JIT utilizavam estrategias estaticas
simples para escolher as porcoes de codigo que seriam compiladas. Tipicamente, tais
maquinas compilavam cada porcao de codigo com um conjunto fixo de otimizacoes durante
a primeira invocacao destas. Estas maquinas incluem os trabalhos com Smalltalk-80
18
(Deutsch e Schiffman, 1984), SELF-91 (Chambers e Ungar, 1989, 1990; Chambers, 1992),
e Kaffe (Wilkinson e Mehlitz, ????).
Maquinas virtuais mais sofisticadas, em vez de utilizarem estrategias estaticas simples,
selecionam dinamicamente subconjuntos de codigos para otimizar. Esses subconjuntos
sao as regioes quentes do programa, isto e, porcoes de codigo executadas com alta
frequencia. Exemplos de maquinas virtuais desta categoria incluem SELF-93 (Holzle,
1995; Holzle e Ungar, 1994) e a primeira versao da Maquina Virtual Java (JVM - do
ingles Java Virtual Machine) da IBM (Suganuma et al., 2000). Embora, estas maquinas
virtuais tambem incluam formas limitadas de otimizacoes baseadas em informacoes
obtidas em tempo de execucao, estes trabalhos nao desenvolveram mecanismos gerais
para otimizacoes baseadas em tais informacoes.
Recentemente diversos trabalhos tem explorado formas mais agressivas de compilacao
JIT (Burke et al., 1999; Silva e Costa, 2007; Suganuma et al., 2004), utilizando informacoes
obtidas em tempo de compilacao e/ou execucao, para adaptar o ambiente as caracterısticas
do programa. Contudo, alguns destes trabalhos nao sao totalmente automaticos, desta
forma, nao aparecendo em maquinas virtuais populares. Porem, estes tem demonstrado
que otimizacoes baseadas em tais informacoes melhoram substancialmente o desempenho
da maquina virtual.
Isto mostra que o componente principal desta nova geracao de maquinas virtuais e um
mecanismo sofisticado de otimizacao baseado em informacoes coletadas dinamicamente.
Contudo, tais tecnicas nao sao aplicaveis em todos os programas. De fato, em alguns casos
(por exemplo, programas com curto tempo de execucao), o alto custo de tais estrategias
geralmente ocasionam uma perda de desempenho (Silva e Costa, 2006).
Tecnicas de compilacao JIT existem desde 1960 com LISP (McCarthy, 1960) e se
propagaram por diversas linguagens de programacao ao longo dos anos. Contudo, somente
com o advento da JVM (Lindholm e Yellin, 1999) e que o termo JIT ficou realmente
conhecido pela comunidade cientıfica (Aycock, 2003).
Portanto, seguindo este contexto, o objetivo deste capıtulo e abordar questoes relativas
a essa tecnica. A Secao 2.1 apresenta um historico e como essa tecnica evoluiu ao longo
dos anos. A Secao 2.2 apresenta uma arquitetura generica de um sistema de compilacao
JIT e como seus componentes se inter-relacionam. A Secao 2.3 descreve os princıpios
de compilacao just-in-time, em especıfico os tipos de abordagens empregados e a forma
com que as regioes quentes de codigo sao detectadas e tratadas. A tecnica de compilacao
baseada em tracos de execucao, que e a motivacao deste trabalho, tambem e discutida
nessa secao. A Secao 2.4 apresenta as principais caracterısticas de sistemas atuais de
compilacao JIT e, por fim, o capıtulo finaliza com algumas consideracoes (Secao 2.5).
19
2.1 Historico
O primeiro trabalho relacionado a compilacao JIT data de 1960 e trata sobre a linguagem
LISP (McCarthy, 1960). Neste trabalho, McCarthy menciona a compilacao de funcoes
para uma linguagem de maquina rapida o suficiente para que a saıda do compilador
nao necessitasse ser salva. Em 1968, os desenvolvedores da linguagem LC2 (ou LCC)
observaram que o codigo compilado pode ser gerado em tempo de execucao a partir
de um interpretador, simplesmente anotando as acoes realizadas (Mitchell et al., 1968).
Dois anos depois, APL (Abrams, 1970) incorporou duas tecnicas que se relacionam
com compilacao JIT. Drag-along, que consistia em adiar a avaliacao de expressoes ate que
informacoes de contexto fossem reunidas e beating, que transformava codigo para reduzir
a quantidade de manipulacao de dados envolvidos durante a avaliacao das expressoes.
Em APL, os tipos dos dados nao sao conhecidos ate o tempo de execucao, o que forcava o
ambiente adiar a aplicacao dessas tecnicas.
Em 1971, Knuth, por meio de dados de estudos empıricos, observou que a maior
quantidade de tempo de execucao gasto em um programa decorre de pequenas porcoes
de codigo. Esta base serviu para que um trabalho posterior, realizado por Dakin e
Poole (1973), investisse em tecnicas que permitissem um balanceamento entre tempo de
execucao e espaco, os quais fundamentavam o argumento para compilacao JIT na epoca.
Dentre as tecnicas desenvolvidas, uma tinha como objetivo executar os programas em duas
versoes de codigo: a nativa, para as regioes quentes, e a interpretada, para as demais. Essa
tecnica foi denominada codigo misto e ainda e aplicavel em alguns sistemas JIT atuais.
Por volta do ano de 1976, Brown desenvolveu a tecnica throw-away (Brown, 1976), que
buscava otimizar o espaco compilando partes do programa sob-demanda, em vez de aplicar
compilacao estatica. Assim, esgotando a memoria, todo o codigo ja compilado (ou partes
dele) seria descartado e recompilado posteriormente, caso necessario.
No trabalho de Hansen com Adaptive Fortran, de 1974, foi apresentada uma visao
fundamental para sistemas JIT em geral (Hansen, 1974). Esta visao agregava tres
princıpios basicos sobre a forma com que o codigo deveria ser tratado pelos sistemas
JIT. Hansen descreveu a importancia de qual codigo compilar, alem de quando e como
compila-los. Ao mesmo tempo, Hansen procurou satisfazer tais fundamentos, utilizando
e mantendo contadores vinculados a cada regiao de codigo para identificar aquelas que
sao quentes, embora ainda aplicasse otimizacoes de codigo (o como) de forma muito
conservativa sobre tais regioes.
Por outro lado, no desenvolvimento de Smalltalk-80 em 1984 (Deutsch e Schiffman,
1984), Deutsch e Schiffman optaram por gerar codigo nativo em tempo de execucao na
20
medida em os metodos eram invocados, em vez de compilar somente os metodos quentes,
como fez Hansen. O codigo nativo era entao armazenado em uma cache, para serem
recuperados posteriormente. Mais tarde, Smalltalk-80 serviu de influencia para uma
implementacao de SELF-91 (Chambers e Ungar, 1989, 1990; Chambers, 1992).
SELF-91 possui caracterısticas bem peculiares, quando comparada com trabalhos
anteriores. Nesta linguagem toda acao e dinamica, os tipos nao sao conhecidos ate
o momento da execucao e o uso de classes e substituıda a favor de prototipos. Essas
caracterısticas levaram ao desenvolvimento de tecnicas de compilacao e otimizacao JIT
mais agressivas ate aquele momento. Uma delas foi a tecnica de customizacao de metodos
desenvolvida por Chambers e Ungar em 1989, cuja funcionalidade era especializar codigo
para um tipo de dado especıfico coletado no tempo de execucao (Chambers e Ungar, 1989).
O sistema desenvolvido ainda introduziu o conceito de compilacao adiantada para casos
incomuns: eventos como overflow aritmetico nao teriam, a princıpio, codigo compilado.
Em vez disso, uma informacao seria vinculada para que codigo fosse gerado somente se
tais eventos ocorressem.
Em 1995 o trabalho de Holzle, com a terceira geracao de SELF (SELF-93) (Holzle,
1995; Holzle e Ungar, 1994), inovou com a implementacao da tecnica de otimizacao
adaptativa: primeiramente, todo metodo invocado era compilado por um compilador nao
otimizador rapido e somente as regioes quentes, detectados por meio de contadores, eram
recompiladas por um compilador otimizador. Quatro anos antes, a linguagem Java surgiu
para ser puramente interpretada. As JVMs iniciais, entretanto, eram interpretadores
puros e resultavam em execucoes muito lentas (Cramer et al., 1997; Tyma, 1998), o que
obrigou os seus desenvolvedores a buscarem nos conceitos de compilacao JIT a solucao
para resolver tal problema.
A visao inicial de compilacao JIT para Java foi dada por Cramer et al. (1997). Neste
trabalho, os autores observaram que somente a traducao de codigo Java para codigo
nativo nao era suficiente e alem disso, a otimizacao de codigo era tambem necessaria.
Seguindo essa visao, em 1997, Plezbert e Cytron apresentaram diferentes modelos teoricos
de sistemas JIT, dentre eles, um que compila e executa codigo de forma concorrente. Dois
destes modelos interpretavam as regioes nao quentes e compilavam aquelas que sao quentes
apos um certo tempo.
Porem, contrario as versoes apresentadas por Plezbert e Cytron, Burke et al. (1999)
apresentaram a maquina virtual Jalapeno, que somente compilava codigo, embora ainda
incorporasse um compilador JIT na sua estrutura. A mesma ideia foi seguida um ano
depois por Cierniak et al. (2000) no desenvolvimento de JUDO.
21
No mesmo ano do surgimento de JUDO, o trabalho teorico de Reinholtz relatou
a possibilidade de Java se tornar mais rapida que C++ (Reinholtz, 2000). O autor se
baseou nos compiladores JIT para Java na epoca que eram auxiliados por informacoes
coletadas dinamicamente, alem do surgimento de sistemas embarcados que, segundo
o autor, forcariam a necessidade para o desenvolvimento de tecnicas mais agressivas
e eficientes de otimizacao de codigo, visto que tais sistemas possuem recursos muito
limitados de hardware. Contudo, em 2010, Foleiss e Silva demonstraram que Java nao e
mais rapida que C++.
Na verdade, as JVMs atuais obtem melhor desempenho de execucao dos programas em
comparacao as JVMs primitivas, e uma parcela significativa desse ganho foi obtido pela
selecao correta de regioes quentes dos programas, ao contrario do que previa Reinholtz.
Embora otimizacoes de codigo sejam importantes, como descrito por Cramer et al., a sua
aplicacao nao e suficiente. Tal afirmacao foi justificada anos mais tarde, quando a maioria
dos trabalhos focaram na precisao da obtencao de regioes quentes, obtendo resultados
satisfatorios. O trabalho de Krintz, de 2003, empregou offline e online profiling para
detectar tais regioes e alcancou melhorias de desempenho de 9%, em media (Krintz, 2003).
No mesmo ano, Suganuma et al. (2003) adotaram o modelo de compilacao baseada em
regiao e obtiveram codigo 5% melhor, em termos de execucao, alem de reduzir o overhead
de compilacao entre 20 a 30% em comparacao aos compiladores baseados em sub-rotinas
(que eram os modelos mais comuns da epoca). Um ano depois, Kumar empregou no
seu trabalho uma tecnica denominada estimacao relativa que, em tempo de execucao
determinava a criticidade dos metodos. Essa abordagem garantiu melhorias de 4% nos
programas avaliados (Kumar, 2004).
Em 2006, Agosta et al. propuseram uma heurıstica estatica para identificar blocos
basicos e estimar o numero de lacos do programas. Algumas metricas foram elaboradas
para fornecer estimativas da complexidade de um metodo. Essas estimativas acom-
panhavam o uso de contadores de frequencia para detectar regioes quentes de forma
mais precisa. Mais tarde, Lee at al. (2008) propuseram um trabalho similar, com a
caracterıstica adicional de prever a contagem de frequencia dos metodos menos comuns,
tomando por base a frequencia dos metodos ja compilados. Esta abordagem reduziu o
custo de manutencao dos contadores dos metodos e alcancou bom desempenho.
Outra abordagem foi utilizada por Gal et al. (2006) na implementacao de Hot-
pathVM. Neste trabalho, os autores utilizaram o recurso de compilacao baseada em
tracos de execucao, anteriormente apresentado em Dynamo (Bala et al., 2000). O
desempenho obtido nao foi comparavel aquele obtido por Lee et al. (2008), mas mostrou
ser uma tecnica vantajosa na aplicacao de diversas otimizacoes de codigo (Zhao et al.,
22
2008). Cinco anos depois, Hayashizaki et al. (2011) propuseram uma tecnica para
deteccao e remocao de falso laco dentro de um traco de execucao e conseguiram uma
melhoria de desempenho de 37% na execucao dos programas avaliados.
Por outro lado, no que tange as otimizacoes de codigo, ha ainda poucas contribuicoes.
Na verdade o estado-da-arte de compilacao JIT consiste da otimizacao adaptativa, ja
presente em Jikes RVM (antes chamada Jalapeno) desde 1999 (Burke et al., 1999).
Este sistema implementa diferentes nıveis de otimizacao, que consistem de um conjunto
restrito de otimizacoes atribuıdas manualmente. Recentemente, um trabalho atribuıdo a
Hoste et al. (2010) tem procurado utilizar busca evolucionaria para atribuir otimizacoes
para cada nıvel de forma automatica, evitando assim o esforco manual.
Outro sistema que tambem implementa otimizacao adaptativa e aMaquina Virtual
da IBM (Suganuma et al., 2004), porem, os conjuntos de otimizacoes atribuıdas sao
baseados em resultados de estudos empıricos realizados por Ishizaki et al. (2003). Embora
a contribuicao de Hoste et al. tenha sido focada em Jikes RVM, sua contribuicao pode
ser empregada em qualquer sistema que implementa otimizacoes em nıveis.
As pesquisas voltadas a compilacao JIT, no entanto, nao se limitam somente na
precisao na deteccao de regioes quentes ou nas otimizacoes de codigo. Trabalhos mais
recentes tem focado na descoberta de boas polıticas de compilacao JIT em maquinas
multi-core (Stallings, 2005). Em 2007, Kulkarni et al. abordaram questoes sobre o
escalonamento da thread de compilacao e descobriu que o desempenho de execucao dos
sistemas JIT e proporcional ao aumento de sua prioridade. Quatro anos depois, Kulkarni
(2011) procurou alcancar, de forma experimental, a melhor polıtica de compilacao para
maquinas multi-core, e descobriu que o aumento do numero de threads de compilacao,
juntamente com a aceleracao na deteccao de regioes quentes e uma boa estrategia para
obter bons resultados. Sistemas JIT, como Jikes RVM (Arnold et al., 2000; Burke et
al., 1999), Hotspot Server (Paleczny et al., 2001), ILDJIT (Campanoni et al., 2008,
2010) e a Maquina Virtual da IBM (Suganuma et al., 2004) nao seguem a melhor
polıtica descoberta nesses trabalhos, mas usufruem do real paralelismo oferecido pelas
arquiteturas atuais.
De fato, a pressao sobre uma JVM rapida impulsionou diversas pesquisas voltadas a
compilacao JIT para a linguagem Java (Aycock, 2003), que resultou em diversas tecnicas
e princıpios que foram gradualmente inseridos e aperfeicoados para a construcao dos
diversos sistemas que surgiram. Atualmente sao conhecidas diversas JVMs, tais como
Sun HotSpot (Kotzmann et al., 2008; Paleczny et al., 2001), JUDO (Cierniak et
al., 2000) e Jikes RVM (Arnold et al., 2000; Burke et al., 1999) que empregam um
conjunto de otimizacoes sobre os programas. Nao obstante, os mesmos princıpios foram
23
tambem empregados na construcao de diversos outros sistemas para outras linguagens,
como Erlang (Pettersson et al., 2002), Lua (Pall, ????), Prolog (Silva e Costa, 2007),
JavaScript (Chang et al., 2009) e PHP (Homescu e Suhan, 2011).
2.2 Arquitetura de Sistemas com Compilacao JIT
No decorrer da evolucao de sistemas JIT, diversas tecnicas e princıpios foram elaborados
e incorporados na medida em que vinham sendo criados. Atualmente, alguns sistemas
apostam na deteccao de regioes quentes viaveis para otimizacao/compilacao (Gal et al.,
2009; Pall, ????; Pettersson et al., 2002) e outros sistematizam o emprego de otimizacoes
de codigo (Arnold et al., 2000; Suganuma et al., 2004). Contudo, mesmo que o objetivo de
tais sistemas seja compilar e executar codigo nativo sem gerar degradacao no desempenho,
os meios empregados para alcancar esse objetivo podem ser bastante diversificados.
Um sistema JIT e projetado para funcionar em uma das seguintes formas: inter-
pretacao mais compilacao ou compilacao mais recompilacao. Sistemas baseados na
primeira forma interpretam todas as unidades de codigo do programa (funcao, metodo
ou clausula, dependendo das caracterısticas da linguagem de programacao para a qual
o sistema foi desenvolvido) e geram codigo nativo para as regioes quentes com um
compilador otimizador. Por outro lado, sistemas que compilam e recompilam sempre
executam codigo nativo. Nestes, toda unidade de codigo invocada e compilada por um
compilador base rapido e as regioes quentes sao:
• Recompiladas apenas uma vez por um compilador otimizador; ou
• Recompiladas varias vezes, caso o sistema verifique que as unidades ja compiladas
se tornaram invalidas (como no caso de um ou mais de seus dados terem o conteudo
modificado para outro tipo) ou se o gerente de compilacao julgar que uma unidade
ja otimizada necessita ser reotimizada com tecnicas mais agressivas.
A arquitetura padrao de um sistema JIT e como apresentado na Figura - 2.11. Dentre
os componentes do sistema de compilacao, o principal e o compilador otimizador. As
otimizacoes aplicadas por ele e o modo como elas sao aplicadas satisfazem a decisao
de projeto de cada desenvolvedor. Tais otimizacoes sao adicionalmente guiadas por in-
formacoes de perfil (como tipos de dados), coletadas em tempo de compilacao (pelo offline
profiler) ou execucao (pelo online profiler), para especializar o codigo e consequentemente
torna-lo mais eficiente. Adicionalmente, tais informacoes podem ser enviadas ao gerente
1Essa organizacao foi proposta neste trabalho.
24
de compilacao, caso existir, para que este crie um plano de compilacao que sera enviado
ao compilador otimizador para auxilia-lo na otimizacao e geracao de codigo.
Monitor
Repositório (Áreas)de código
Fila de (re)compilação
PerfiladorOnline
CompiladorBase
PerfiladorOnline
CompiladorOtimizador
PerfiladorOnline
Gerente deCompilação
PerfiladorOnline
Gerente deCódigo
PerfiladorOnline
Motor deExecução
PerfiladorOnlineOfflineProfiler
Informações coletadas em tempo de execução
Informações coletadasem tempo de execução
Versão correta decódigo para executar
Gerenciamento das
versões de código
Parâmetros para contarfrequência das unidades
Informações coletadas em tempo de compilação ou carga
Alerta gerente decódigo queuma unidadese tornou crítica
Código (re
)compilado
Requisição de (re)compilação(uma região crítica)
Região crítica
Plano de compilação Códigocompilado
Informações coletadasem tempo de compilaçãoou carga
Versãointerpretada
Versãocompilada
Sistema de Compilação Sistema de Execução
PerfiladorOnlineOnlineProfiler
Região quente
Monitor
CompiladorBase
CompiladorOtimizador
Gerente deCompilação
Gerente deCódigo
OfflineProfiler
OnlineProfiler
Motor deExecução
Figura 2.1: Arquitetura generica de um sistema JIT.
Independente da forma de funcionamento, o sistema implementa um componente,
chamado monitor, cuja finalidade e instrumentar cada unidade de codigo e medir a
frequencia de execucao destas. Este monitoramento pode ser feito por meio de contadores
ou tempo. Contadores sao incrementados a cada invocacao/termino de execucao da
unidade de compilacao (Plezbert e Cytron, 1997) e fracoes de tempo sao verificadas
durante a execucao. Em outras palavras, a funcao do monitor e detectar regioes quentes,
isto e, regioes de codigo cujo contador (ou tempo) excedeu um limite e emitir um alerta
ao gerente de codigo para que o mesmo envie a regiao quente recem detectada para
(re)compilacao. Existem ainda outros sistemas que geram codigo com um compilador
otimizador no momento em que as unidades sao invocadas porem, para tais sistemas, o
monitor nao e utilizado.
Dessa forma, na medida em que as regioes quentes sao detectadas, estas sao encami-
nhados do gerente de codigo para o gerente de compilacao e inseridas na fila de compilacao.
A partir de entao, o compilador otimizador compila a regiao quente do inıcio da fila e
encaminha o codigo gerado para o gerente de codigo.
Por sua vez, o gerente de codigo gerencia as versoes de codigo para cada unidade de
codigo existente no ambiente. Embora este componente possa nao estar enquadrado como
um componente particular de alguns sistema JIT, suas funcionalidades subsistem, sendo
elas:
25
• Enviar a versao correta de codigo para ser executada pelo motor de execucao. Em
sistemas que interpretam e compilam, se uma unidade esta disponıvel nas duas
versoes, a prioridade de execucao e da versao compilada, pois esta e mais rapida.
Por outro lado, em sistemas que compilam e recompilam, a prioridade e da versao
mais recente. O ultimo caso prevalece em sistemas que recompilam varias vezes,
pois nestes, o codigo tende a ser mais eficiente a cada recompilacao.
• Enviar codigo para o gerente de compilacao, que por sua vez, solicita (re)compilacao.
• Receber codigo provido pelo compilador base ou otimizador apos uma unidade (ou
regiao quente) ser compilada e armazena-la no repositorio de codigo.
O repositorio de codigo consiste de estruturas que armazenam as versoes do codigo:
versao interpretada e versao nativa. A versao interpretada e padrao dos sistemas de modo
misto e pode ser gerada estaticamente, como os bytecodes das JVMs, ou no tempo de carga
do sistema, como o codigo YAAM (Costa, 1999; Lopes, 1996) do YAP com compilacao
JIT (Silva e Costa, 2007). A versao nativa, por sua vez, consiste de codigo de maquina
gerado pelo compilador base ou ainda das regioes quentes que foram geradas em tempo de
execucao. Embora o repositorio de codigo tenha tanto codigo intepretado quanto nativo,
apenas os sistemas de modo misto executam ambas as versoes.
O motor de execucao e responsavel por executar o codigo recebido do gerente de codigo,
independente de sua versao. Este componente pode variar entre varios modos de execucao
para partes especıficas do programa. Sistemas que interpretam unidades nao frequentes e
compilam as demais funcionam em um modo misto de execucao. Analogamente, sistemas
que (re)compilam codigo funcionam apenas no modo de execucao de codigo nativo.
As proximas subsecoes fornecem mais detalhes sobre os possıveis componentes da
arquitetura de um sistema JIT.
2.2.1 Compilador Base e Compilador Otimizador
O compilador base gera codigo nativo em tempo de execucao para todas as unidades de
codigo invocadas pelo sistema, imediatamente antes da primeira invocacao. O objetivo
deste compilador e ser um gerador rapido de codigo nativo. Desta forma, o compilador
base nao aplica as unidades compiladas um conjunto de otimizacoes.
Com uma abordagem diferente do compilador base, o compilador otimizador gera
codigo nativo em tempo de execucao para as regioes quentes. Por padrao, todo codigo
compilado e otimizado com um conjunto de otimizacoes pre-definido, independente do
programa em execucao. Contudo, o sistema pode ajustar automaticamente o compilador
26
para que este altere o nıvel de otimizacao que sera aplicado em uma determinada regiao
quente. Isto possibilita que o sistema recompile unidades de codigo buscando melhorar
o desempenho, como tambem proporciona que o sistema se ajuste automaticamente as
caracterısticas do programa em execucao.
2.2.2 Offline Profiler
O offline profiler e utilizado para derivar informacoes em tempo de compilacao ou
carregamento do programa, que possam guiar a geracao de codigo especializado. Desta
forma, o objetivo do uso de tais informacoes e melhorar o desempenho do codigo
compilado, aumentando a velocidade de execucao e/ou reduzindo o tamanho do codigo.
A ideia geral e executar o programa sobre um domınio conservativo, a fim de satisfazer
um conjunto de restricoes. No termino da execucao, os resultados encontrados fornecem
uma aproximacao das informacoes sobre o programa analisado2. Tais informacoes incluem
tipos de dados (no caso de sistemas para linguagens com tipagem dinamica), tamanho
das unidades de codigo (metodo, funcao ou clausula), existencia de chamadas aninhadas,
quantidade e tipos dos parametros, existencia de recursividade ou mesmo regioes quentes
de codigo.
2.2.3 Online Profiler
O online profiler permite que o sistema JIT se adapte as caracterısticas do programa
em execucao. Para alcancar tal objetivo, o online profiler monitora a execucao dos
programas e coleta informacoes que possibilitam a geracao de codigo especializado. Isto e
independente da forma que o sistema foi projetado, ou seja, e independente do uso de um
modo de interpretacao mais compilacao ou compilacao mais recompilacao. Nesse sentido,
o online profiler pode coletar as seguintes informacoes:
Tipos de variaveis Sistemas JIT empregados em linguagens com tipos dinamicos po-
dem se beneficiar da coleta do tipo das variaveis. Neste caso especıfico, o sistema
e tradicionalmente implementado de forma generica para todos tipos de dados
possıveis, o que reflete em instrucoes de checagem de tipos durante a execucao.
Portanto, o uso de informacoes desse tipo permite que tais checagens sejam remo-
vidas.
2As aproximacoes resultantes do processo podem ser corretas, mas ainda existem casos extremosem que o profiler nao visita regioes de codigo visitados pelo programa executado com entradas reais.Portanto, em casos extremos, nao e possıvel definir a corretude das aproximacoes coletadas.
27
Tempo de interpretacao Essa informacao e util dependendo da metrica utilizada para
determinar se uma unidade e quente ou nao e consequentemente acionar o sistema
de compilacao. As metricas utilizadas pelos sistemas JIT serao descritas na Secao
2.3.2.
Tempo de compilacao Assim como o tempo de interpretacao, o tempo de compilacao
e outra informacao necessaria ao sistema, dependendo da metrica utilizada, porque
permite definir um tempo ideal de interpretacao para que as unidades de codigo se
tornem regioes quentes. O uso de tal informacao tambem sera descrito na Secao
2.3.2.
E importante observar que as informacoes coletadas podem ser facilmente modificadas
durante a execucao, dependendo das caracterısticas da linguagem de programacao para a
qual o sistema foi desenvolvido.
2.2.4 Monitor
O sistema JIT instrumenta as unidades de codigo do programa com parametros de
frequencia a fim de medir a frequencia de execucao de tais unidades. Esta instrumentacao
pode ser feita com o uso de contadores ou fracao de tempo. Desta forma, e papel do
monitor atualizar o parametro de frequencia e, na medida em que tal parametro atinja
um limite pre-definido, alertar ao gerente de codigo que tal unidade se tornou frequente.
2.2.5 Gerente de Codigo
O gerente de codigo gerencia as areas de codigo do sistema. Suas tarefas consistem
basicamente em:
1. Enviar a versao correta da unidade acionada para execucao, priorizando o envio do
codigo nativo mais atual (ou seja, mais especializado);
2. Enviar uma unidade para compilacao.
A interacao do gerente de codigo com os demais componentes, bem como o seu compor-
tamento com uma unidade de codigo e definido pelo estado atual da unidade em questao.
Em modo de execucao interpretacao mais compilacao, cada unidade do programa pode
assumir um dos seguintes estados: interpretado nao-frequente, interpretado frequente ou
nativo. No modo compilacao mais recompilacao, os estados de uma unidade sao: nativo
nao-frequente, nativo frequente ou nativo recompilado.
28
Durante a execucao do programa, o gerente de codigo interage com o monitor para
identificar quais unidades sao frequentes. Desta forma, assim que uma determinada
unidade e detectada, o gerente de codigo a envia para o gerente de compilacao para
que este providencie uma nova versao da unidade. Apos o processo de especializacao (seja
ele compilar ou recompilar, dependendo da forma de implementacao do sistema JIT) de
uma unidade e papel do gerente fornecer ao motor de execucao a nova versao da unidade.
Normalmente, o gerente de codigo verifica a existencia de codigo especializado e
o envia para execucao, permanecendo nesse processo ate nao encontrar mais codigo
especializado para executar ou ate finalizar o programa. Adicionalmente, a execucao
de codigo especializado pode ser interrompida no caso de ocorrer alguma excecao. Nesse
caso, o gerente de codigo precisa restaurar o estado do sistema ate um ponto anterior ao
qual ocorreu a excecao e enviar ao motor de execucao uma versao nao especializada da
unidade.
2.2.6 Gerente de Compilacao
O gerente de compilacao e responsavel por criar um plano de (re)compilacao para o
(re)compilador otimizador utilizar durante a geracao de codigo. Basicamente, as in-
formacoes coletadas pelo offline e/ou online profilers sao inferidas para uma regiao quente
especıfica e enviadas ao gerente de compilacao para que este crie o plano de compilacao
adequado. Em geral, este plano define um conjunto de otimizacoes pre-determinado
pelo desenvolvedor do sistema, que e o mais adequado aquela regiao quente (Ishizaki et
al., 2003). Alem disto, tal plano pode definir um conjunto de otimizacoes mediante a
analise do proprio programa em execucao. Portanto, neste ultimo caso as otimizacoes
serao habilitadas durante a execucao do programa, bem como a ordem em que elas serao
aplicadas (Cavazos e O’Boyle, 2005; Hoste et al., 2010; Triantafyllis et al., 2003).
2.2.7 Repositorio de Codigo
O repositorio de codigo consiste de estruturas que armazenam versoes de codigo interpre-
tado e/ou compilado. Em sistemas que interpretam e compilam, o codigo interpretado e o
padrao na execucao. Este pode ser criado a partir de uma representacao de mais alto nıvel
e entao simplesmente carregado na inicializacao do sistema ou criado durante o tempo de
carregamento do programa. Por outro lado, sistemas que compilam e recompilam mantem
somente a area de codigo nativo.
A area de codigo nativo se expande na medida em que regioes quentes sao compiladas.
Nesse sentido, sistemas de compilacao JIT podem implementar polıticas para garantir que
29
tal regiao nao cresca mais que um limite determinado, descartando codigo pouco invocado
e recompilando-o quando necessario, como acontece em Smalltalk (Deutsch e Schiffman,
1984).
Alem disso, sistemas que empregam online e/ou offline profiler podem construir
versoes de codigo especializadas para cada tipo de informacao coletada. Portanto, nestes
sistemas a prioridade de execucao e sempre do codigo especializado para o comportamento
ativo. Porem, e sempre importante manter uma versao generica para garantir estabilidade
caso o comportamento de uma regiao de codigo altere, por exemplo, caso uma variavel
mude de tipo. Outra consideracao importante e a construcao de um coletor de lixo (Aho
et al., 1986) associado a sistemas desse tipo. Visto que varias versoes de codigo nativo
induzem a uma expansao contınua da area de codigo nativo, e importante conter essa
expansao. Geralmente, isto consiste em eliminar codigo inutilizavel (nao executado por
um longo perıodo de tempo) ou invalido (especializado para uma informacao que nao e
equivalente ao comportamento atual do programa).
2.2.8 Motor de Execucao
O motor de execucao e o componente que executa as unidades de codigo, independente
do seu estado. Em sistemas que empregam interpretacao mais compilacao, o motor de
execucao e capaz de interpretar codigo como tambem invocar codigo nativo. Isto indica
que para esta forma de implementacao do sistema, o motor de execucao e um interpretador
que possui um mecanismo de invocacao de codigo nativo. Para sistemas que empregam
compilacao mais recompilacao, o motor de execucao e um modulo composto apenas de
um mecanismo de invocacao de codigo nativo.
2.3 Princıpios de Compilacao JIT
Visto que em sistemas JIT os compiladores compilam e otimizam codigo em tempo de
execucao e necessario que estes sejam rapidos, efetivos, leves e capazes de gerar codigo
nativo de alta qualidade. Entre essas necessidades, entretanto, existe um tradeoff entre
o tempo de compilacao e eficiencia do codigo gerado, que forma a base desses sistemas.
Em outras palavras, um compilador JIT deve ser sensıvel a eficiencia de tempo e espaco
dos seus algoritmos de otimizacao, pois uma compilacao lenta pode desacelerar o tempo
de resposta do programa. Alem disso, e preciso saber o que compilar, pois devido ao fato
da compilacao JIT ocorrer em tempo de execucao, nem todo trecho de codigo deve ser
30
compilado. Na verdade, gerar codigo nativo para uma unidade especıfica, cuja execucao
nao compense o overhead de compilacao geralmente nao trara resultados satisfatorios.
Alem de ser importante conhecer quando compilar, o que compilar e como compilar, e
necessario considerar de forma precisa o mecanismo utilizado na manutencao dos sistemas
de compilacao e execucao, como tambem as questoes referentes aos atrasos que podem
ocorrer, pois se considerados de forma ingenua, podem ocasionar uma degradacao no
desempenho. Por essa razao, as seguintes questoes devem ser analisadas:
1. O atraso decorrente da espera da unidade de codigo para que se torne uma regiao
quente; e
2. O atraso decorrente da espera da regiao quente na fila de atendimento de compilacao.
Formas de tratar estes atrasos sao difıceis de projetar ou implementar, pois uma
configuracao negligente nas formas de deteccao de regioes quentes podem ocasionar um
alto overhead ao sistema como um todo. Pesquisadores tem tentado contornar o atraso
decorrente da unidade a espera para se tornar regiao quente, como e o caso dos trabalhos
de Krintz (2003), Namjoshi e Kulkarni (2010) e Campanoni et al. (2009), porem com o
custo de requerer execucao previa do sistema ou um processador adicional. Com respeito
ao atraso decorrente da espera na fila de compilacao, diversos trabalhos procuram utilizar
prioridades nas threads (Tanenbaum, 2007) do compilador (Arnold et al., 2000; Kulkarni,
2011; Kulkarni et al., 2007; Suganuma et al., 2004).
2.3.1 Manutencao dos Sistemas de Compilacao e Execucao
Plezbert e Cytron (1997) classificaram as possıveis abordagens para o gerenciamento entre
a compilacao e a execucao em um sistema JIT da seguinte maneira:
JIT Neste tipo de sistema, a unidade de codigo e compilada imediatamente antes de sua
execucao e uma unica thread e utilizada para alternar entre compilacao e execucao.
Isso significa que o motor de execucao e suspenso durante a atividade do compilador.
Como essa abordagem traduz codigo sob-demanda, ela evita que o sistema compile
inutilmente alguns trechos de codigo, devido ao fato de grandes programas possuirem
porcoes de codigo que as vezes nao sao executados. Um sistema que segue essa
abordagem e a JVM Kaffe (Wilkinson e Mehlitz, ????).
Smart JIT Apesar da vantagem inerente aos sistemas que utilizam a abordagem ante-
rior, esta pode acarretar perda de desempenho para uma situacao na qual o sistema
31
compile todas as unidades de codigo invocadas. Como a maior parte do tempo de
execucao da maioria dos programas esta relacionado a execucao de uma pequena
faixa de codigo (Arnold et al., 2004; Bruening e Duesterwald, 2000), pode nao ser
viavel compilar faixas se elas nao forem executadas com frequencia. Em outras
palavras, uma compilacao eficiente deve saber escolher o que compilar. Smart JIT
emprega tecnicas que visam detectar unidades de codigo frequentes e entao, compilar
somente estas. Em tais sistemas tambem e utilizado somente uma thread, o que torna
obrigatorio suspender para compilacao a execucao de codigo. Sistemas smart JIT
sao: SELF-93 (Holzle, 1995; Holzle e Ungar, 1994), JUDO (Cierniak et al., 2000),
Jikes RVM (Arnold et al., 2000), YAPc (Silva e Costa, 2007), LuaJIT (Pall,
????) e HiPE (Pettersson et al., 2002).
Compilacao Contınua A compilac~ao contınua foi inicialmente apresentada como um
modelo teorico e tem sido empregada em alguns sistemas atuais (Arnold et al., 2000;
Paleczny et al., 2001; Suganuma et al., 2004), principalmente apos o surgimento (e
expansao) de arquiteturas multi-core. A explicacao para a implantacao de com-
pilacao contınua apos o surgimento dessas arquiteturas e que tal abordagem utiliza,
mais de uma thread : uma para execucao e as demais para compilacao. Embora,
ainda detecte regioes quentes, assim como smart JIT, a vantagem intrınseca e que
o ambiente nao precisa suspender a execucao para gerar codigo nativo. Entretanto,
o custo de implementacao e maior: as threads precisam ser gerenciadas, bem como
o acesso aos modulos/estruturas compartilhados(as), tais como o gerente de codigo
e a fila de regioes quentes escalonadas para compilacao (esta ultima se houver mais
de uma thread de compilacao).
2.3.2 Acionamento do Sistema de Compilacao
Basicamente, um sistema de compilacao JIT funciona da seguinte forma: em primeiro
lugar, ele instrumenta cada unidade de codigo com informacoes que medem a frequencia
de sua execucao. Em seguida, o programa e interpretado (Chang et al., 2009; Gal et al.,
2006; Kotzmann et al., 2008; Paleczny et al., 2001; Suganuma et al., 2004) ou compilado
com um compilador rapido (Arnold et al., 2000; Cierniak et al., 2000; Holzle e Ungar,
1994) e entao executado. Durante a execucao do programa, o sistema monitora as
unidades de codigo com o objetivo de detectar unidades quentes, isto e, unidades de
codigo mais frequentes. Este monitoramento e realizado verificando a informacao que
mede a frequencia de execucao de cada unidade. Desta forma, uma unidade de codigo
32
e entao dita quente se sua informacao de frequencia atinge o limite pre-definido pelo
sistema.
Para deteccao das regioes quentes, sistemas JIT atuais empregam contadores (Chang
et al., 2009; Hansen, 1974; Holzle e Ungar, 1994; Kotzmann et al., 2008; Pall, ????;
Pettersson et al., 2002; Silva e Costa, 2007), amostras de tempo (Arnold et al., 2000)
ou uma combinacao entre contadores e amostras de tempo (Cierniak et al., 2000). O
uso de contadores requer que o sistema conte o numero de invocacoes de cada unidade,
enquanto a amostragem de tempo precisa interromper a execucao periodicamente para
atualizar o tempo da unidade. De qualquer forma, independente da abordagem utilizada,
a unidade e enviada para compilacao quando a respectiva metrica alcancar um limite
pre-definido.
A escolha de um limite correto e crucial para alcancar um bom desempenho de
execucao. Um limite muito baixo pode compilar codigo em demasia, o que aumenta o
overhead de compilacao e gera muito codigo nativo, que provavelmente nao proporcionara
ganhos de desempenho em execucoes posteriores (se ocorrer). Por outro lado, um limite
muito alto pode tornar o sistema muito conservativo que impede que regioes quentes ideais
sejam compiladas.
Uma base teorica para ajustar a metrica utilizada pelo compilador foi dada por Karp
(1992). Tal base afirma que o ideal e a unidade ser interpretada um numero de vezes
suficiente para compensar o overhead de compilacao. O trabalho de Plezbert e Cytron
(1997) explora outras metricas, a saber: crossover e balance. Crossover utiliza o tempo de
compilacao da unidade, juntamente com um parametro chamado ponto de crossover para
encontrar um limite ideal para a amostra de tempo. Enquanto isso, balance envia para
compilacao as unidades cujo tempo de interpretacao ultrapassa seu tempo de compilacao.
Tais abordagens, entretanto, sao difıceis de implementar porque na pratica, encontrar o
tempo de compilacao das unidades requer uma estimativa (que nao e precisa) ou uma
passagem adicional pelo programa (que pode nao ser desejavel em alguns casos).
No entanto, alguns trabalhos recentes tem buscado formas de enviar as unidades de
codigo o mais rapido possıvel para compilacao. O trabalho de Krintz (2003) explora
um mecanismo em tempo de compilacao para detectar previamente as regioes quentes,
enquanto que Namjoshi e Kulkarni (2010) propuseram uma tecnica de predicao utilizando
um processador adicional. Outra tecnica de predicao, implementada no trabalho de
Campanoni et al. (2009), procura dar enfase as unidades mais proximas da unidade
em execucao atual, julgando-as possuırem maior probabilidade de serem invocados fu-
turamente. Alem disso, as arquiteturas multi-core tem sido exploradas ultimamente
para manter threads especıficas somente para compilar ou executar codigo. Sistemas
33
que utilizam esta abordagem sao: Sun HotSpot (Kotzmann et al., 2008; Paleczny et
al., 2001), Jikes RVM (Arnold et al., 2000) e ILDJIT (Campanoni et al., 2008, 2010).
2.3.3 Selecao de Unidades de Compilacao
Um fundamento importante relacionado as questoes abordadas por Hansen (1974), coube
a Holzle no seu trabalho com SELF-93 (Holzle, 1995; Holzle e Ungar, 1994). Holze
observou que o mecanismo de selecao das unidades que serao efetivamente compiladas (o
que compilar) e mais importante que o mecanismo para disparo de compilacao (quando
compilar). O mecanismo de selecao de unidades e uma forma de “previsao” de frequencia
de execucao das unidades de codigo, pois, como nao e possıvel identificar o numero de vezes
que uma unidade sera executada no futuro, e considerado que sua frequencia de execucao
a partir de um determinado ponto seja proporcional a sua frequencia de execucoes antes
deste ponto. Desde os primeiros trabalhos relacionados a compilacao JIT ate os mais
recentes, tres estrategias de selecao de unidades viaveis para compilacao foram propostas,
sao eles: (1) selecao baseada em sub-rotinas; (2) selecao baseada em regioes; e (3) selecao
baseada em tracos de execucao.
Selecao Baseada em Sub-rotinas
Compiladores JIT possuem, em muitos casos, uma estrutura similar a dos compiladores
estaticos e como os tais, herdam os mesmos fundamentos. Basicamente, um compilador
estatico constroi, para cada sub-rotina, um grafo de fluxo de controle e aplica uma serie
de otimizacoes sobre ele. A sub-rotina completa, bem como todas as demais alcancadas
a partir desta sao compiladas e otimizadas sem qualquer preocupacao quanto ao tempo,
pois neste caso, esse e um parametro irrelevante. Como mencionado, compiladores JIT
funcionam de forma similar, mas com a unica diferenca de que compilam as sub-rotinas sob
demanda, ou na primeira invocacao ou apenas aquelas que sao consideradas importantes
para o desempenho, sejam elas funcoes (Pall, ????; Pettersson et al., 2002), metodos
(Cierniak et al., 2000; Deutsch e Schiffman, 1984; Wilkinson e Mehlitz, ????) ou clausulas
(Silva e Costa, 2007).
No entanto, regioes quentes possuem trechos de codigo que sao pouco ou nunca
executados (Bruening e Duesterwald, 2000; Whaley, 2001), o que pode causar efeitos
adversos e reduzir a efetividade das otimizacoes (Suganuma et al., 2006). Um exemplo
tıpico e a aplicacao de inline que e muitas vezes restringida a sub-rotinas cujo tamanho
do codigo ultrapasse um limite pre-estabelecido, para evitar possıveis explosoes de codigo
(Serrano, 1997). Consequentemente, uma sub-rotina ja integrada, porem com muito
34
codigo infrequente, pode impedir que sub-rotinas frequentes sejam tambem integradas
a sub-rotina principal na medida que inline se expande.
A escolha de sub-rotinas como a unidade basica de compilacao em sistema JIT e, de
fato, uma escolha conveniente (facil de manter), mas se for assumido o fato de que a
maioria das sub-rotinas sao compostas por porcoes de codigo nao frequentes, o ideal seria
detectar e eliminar estas porcoes, a fim de reduzir o esforco das otimizacoes e gerar um
codigo mais eficiente.
Selecao Baseada em Regioes
No contexto de compilacao JIT, esta tecnica procura por porcoes de codigo pouco frequen-
tes dentro de uma unidade e as eliminam do mecanismo de selecao. Portanto, as regioes
quentes sao os trechos de codigo frequentes dentro das unidades e nao necessariamente
uma sub-rotina.
Essa estrategia foi inicialmente baseada em informacoes coletadas estaticamente e
explorada por um framework de compilacao para um ambiente estatico (Hank et al., 1995).
Basicamente, estrategias para formacao de regioes sao dependentes da classe do sistema
que a implementa. Por outro lado, compiladores JIT nao sao beneficiados no todo por
informacoes coletadas estaticamente. Alem disso, utilizar apenas contadores ou amostras
de tempo pode ocasionar a exclusao de blocos quentes nao dominantes na formacao de
uma regiao. Um exemplo e um bloco if-then-else, no qual, tanto then quanto else sao
executados frequentemente. Neste caso, informacoes coletadas dinamicamente auxiliariam
na escolha de apenas um destes blocos (aquele com uma frequencia levemente superior)
para a formacao da regiao de compilacao. Se isso nao ocorrer, ha grandes chances de que
o bloco ignorado leve a degradacao o desempenho do sistema (Suganuma et al., 2006).
O trabalho de Suganuma et al. (2006) foi o primeiro a abordar a tecnica para deteccao
de regioes para compiladores JIT, em sistemas Java. A ideia basica e inicialmente
assumir que cada metodo seja representado como um grafo de fluxo de controle com
apenas uma entrada e uma saıda. Em seguida, os blocos basicos (que consistem nos
nos do grafo) sao marcados como frequentes ou nao frequentes com base em algumas
heurısticas. Normalmente, blocos que contem classes nao referenciadas, que terminam
com uma instrucao de excecao ou que manipulam excecoes sao considerados nao frequentes
e blocos que finalizam com instrucoes de retorno normais sao marcados como frequentes.
Adicionalmente, informacoes coletadas em tempo de execucao podem ajudar a detectar
blocos nunca executados, marcando-os tambem como nao frequentes. Finalmente, uma
passagem pelo grafo de fluxo analisa cada bloco basico, marcando-o como nao frequente
35
na sua saıda somente se a entrada de todos os seus sucessores definirem este como nao
frequente. Desta forma, e definido como nao frequente todo bloco basico definido como
nao frequente na sua entrada e sua saıda. Tais blocos, enfim, podem ser desconsiderados
dentro do metodo e nao terao codigo final gerado.
Selecao Baseada em Tracos de Execucao
Sistemas baseados em selecao por tracos de execucao (Chang et al., 2009; Gal et al., 2009,
2006) procuram definir como regioes quentes os tracos de execucao frequentes que podem
ultrapassar os limites de uma sub-rotina. O primeiro trabalho a explorar os conceitos
desta tecnica foi um sistema para traducao binaria, chamado Dynamo (Bala et al., 2000).
Este sistema utiliza informacoes coletadas em tempo de execucao para detectar e otimizar
tracos quentes de execucao. Contudo, foi somente com o HotpathVM (Gal et al., 2006)
que essa tecnica foi utilizada em compilacao JIT.
Basicamente, a deteccao de tracos quentes de execucao funciona da seguinte maneira:
um contador e mantido no inıcio de cada backward branch (desvios cujo endereco alvo e
menor que o endereco da instrucao de desvio), que e incrementado toda vez que o desvio
e executado. Quando o valor de um contador de destino excede um limite, o traco de
execucao e gravado, compilado, armazenado em uma cache especıfica e invocado pelo
interpretador ou pelo ponto de saıda de outro traco de execucao compilado.
Dessa forma, as partes constituintes de um traco de execucao sao o inıcio dele (a
cabeca), que e uma instrucao ou unidade executada frequentemente, e um corpo que e
estendido ao longo do codigo, a partir da cabeca ate um ponto onde uma das seguintes
condicoes ocorrem:
• Um ciclo de instrucoes e detectado;
• O traco gravado excede um tamanho pre-definido;
• Uma instrucao incomum, como um manipulador de excecoes, e encontrada; ou
• Uma instrucao que forma a cabeca de um traco de execucao e encontrada.
Desde HotpathVM, muitos compiladores JIT baseados em selecao de tracos foram
implementados, dentre eles, TraceMonkey (Gal et al., 2009), LuaJIT (Pall, ????),
Tamarin-Trace (Chang et al., 2009) e HappyJIT (Homescu e Suhan, 2011).
Provavelmente a proxima tendencia na construcao de compiladores JIT utilizara
selecao por tracos de execucao para detectar regioes quentes mais eficientes. Hayashizaki
et al. (2011) demonstraram que a remocao de falsos lacos dos tracos detectados e uma
36
boa estrategia para melhorar o desempenho, embora outro trabalho, atribuıdo a Inoue
et al. (2011), tenha demonstrado que detectar grandes tracos de execucao e escalona-los
para compilacao e tambem uma boa estrategia. Adicionalmente, a vantagem em manter
grandes tracos de execucao e que, quanto maior for, maior as oportunidades de otimizacao
(Zhao et al., 2008), que convergem em codigo mais eficiente.
2.3.4 Geracao de Versoes Especializadas
Com a ocorrencia de varias pesquisas, tecnicas que auxiliam compilacao JIT foram
criadas e aperfeicoadas, como drag-along (Abrams, 1970), comumente conhecida como
avaliacao tardia e codigo misto (Dakin e Poole, 1973) e ainda empregada em sistemas
que interpretam e compilam. No contexto de linguagens orientadas a objetos, SELF
(Chambers e Ungar, 1989, 1990; Chambers, 1992; Holzle, 1995; Holzle e Ungar, 1994)
inovou no desenvolvimento de otimizacoes mais agressivas, como a customizacao de codigo
que e utilizada para criar metodos especializados por tipo de dado e type feedback, que
extrai informacoes de tipos de execucoes previas para auxiliar no processo de compilacao.
O processo de otimizacao visa transformar unidades de codigo para torna-los mais
eficientes em termos de tempo, espaco e/ou consumo de energia. Diversas tecnicas que
integram esse processo sao especializadas para um conjunto de tarefas, como simplificar
expressoes constantes (Aho et al., 1986), eliminar expressoes comuns (Aho et al., 1977)
ou simplificar lacos dos programas (Bacon et al., 1994). A saıda da unidade otimizada
deve ser mantida e a unica diferenca notada pelo usuario final e o benefıcio gerado por
tais tecnicas, como a execucao rapida de codigo e reducoes do espaco ocupado e energia
consumida. No que se refere a compilacao JIT, e esperado que o processo de otimizacao
consuma pouco tempo de compilacao.
A aplicacao de tecnicas de otimizacao (Muchnick, 1997), em compiladores JIT em
particular, gera um tradeoff entre o tempo de compilacao e a eficiencia do codigo final. Em
compiladores estaticos, o overhead inerente pode ser ignorado, portanto tais tecnicas sao
aplicadas em tempo de compilacao. Como resultado, otimizacoes mais agressivas podem
ser aplicadas arbitrariamente. Por outro lado, ambientes de compilacao JIT precisam
ser criteriosos, uma vez que o tempo de compilacao (e otimizacao) integra o tempo total
de execucao. Por esta razao, uma decisao negligente pode resultar em uma degradacao
no desempenho do sistema como um todo. Nesse sentido diversos sistemas tem adotado
diferentes abordagens.
Adaptive Fortran (Hansen, 1974) escolhe e aplica tecnicas diferentes a cada in-
vocacao de uma unidade codigo. Por outro lado, os compiladores de SELF-93 (Holzle,
37
1995; Holzle e Ungar, 1994), JUDO (Cierniak et al., 2000), SUN HotSpot (Kotzmann
et al., 2008; Paleczny et al., 2001) apostam em um conjunto de otimizacoes pre-definidas e
pre-ordenadas aplicadas uma unica vez por unidade de codigo. Embora, apenas SELF e
Sun HotSpot Client evitem otimizacoes mais agressivas. Jikes RVM (Arnold et al.,
2000; Burke et al., 1999) implementa um sistema de otimizacao adaptativa. Geralmente,
o sistema utiliza um calculo de custo-benefıcio para julgar o nıvel de otimizacao que uma
unidade prestes a ser compilada recebera. Outro sistema que implementa otimizacoes em
nıvel e a Maquina Virtual da IBM (Suganuma et al., 2004). Contudo, as otimizacoes
aplicadas em cada nıvel sao tecnicas atribuıdas manualmente pelos desenvolvedores e
consomem um tempo dispendioso para avaliacao. Nesse sentido, um trabalho recente,
realizado por Hoste et al. (2010), tem buscado aplicar busca evolucionaria para encontrar
planos de otimizacao que regulam o tradeoff existente entre o tempo de compilacao e a
eficiencia do codigo.
Em geral, as otimizacoes aplicadas pelo sistema de compilacao JIT utilizam analises de
fluxo de dados que precisam encontrar os usos de cada definicao de variavel ou a definicao
de cada uso em cada expressao. O canal def-uso e uma estrutura de dados que torna este
acesso mais eficiente: para cada declaracao no grafo de fluxo, o compilador mantem uma
lista de ponteiros para todos os usos da variavel definida nesta declaracao, e para cada
instrucao uma lista de ponteiros para todas as definicoes das variaveis usadas nela. Desta
maneira, o compilador pode rapidamente saltar da definicao para o uso ou vice-versa.
Uma melhoria nesta ideia e o uso da representacao Static Single-Assignment (SSA)
(Brandis e Mossenbock, 1994; Sreedhar et al., 1999), uma representacao intermediaria
na qual cada variavel possui apenas uma definicao no programa. A unica (estatica)
definicao pode estar em um laco que e executado diversas (dinamicas) vezes, logo o nome
representacao static single-assignment ao inves de representacao single assignment (onde
variaveis nunca sao redefinidas). A representacao SSA e popular em sistemas JIT por
diversas razoes:
1. Analises de fluxo de dados e algoritmos de otimizacao podem ser implementados de
maneira simples quando cada variavel possui apenas uma definicao.
2. Se a variavel possui N usos e M definicoes, o espaco necessario para representar as
estruturas def-uso e proporcional a N ×M . Para a maioria dos programas reais, o
tamanho da representacao SSA e linear no tamanho do programa original.
3. Usos e definicoes das variaveis na representacao SSA simplificam algoritmos como
construcao de um grafo de interferencia, que e utilizado por alocadores de registrado-
38
res baseados em coloracao de grafo, como o empregado pelo sistema Sun HotSpot
Server.
4. Usos nao relacionados da mesma variavel em um programa tornam-se diferentes
variaveis na representacao SSA, eliminando relacionamentos desnecessarios.
A escolha por esta representacao esta baseada nas razoes descritas acima. Contudo,
entender o comportamento do programa e crucial para obter um bom desempenho.
Embora o uso de uma representacao SSA torne os algoritmos de otimizacoes mais leves
e simples, ela possui o problema de aumentar o tamanho do codigo gerado e a pressao
por registradores. O tamanho do codigo aumenta devido ao uso de uma unica definicao
para cada variavel, acarretando o aumento dos acessos a memoria. Consequentemente,
o aumento da quantidade de variaveis aumenta a pressao por registradores. Em uma
arquitetura com poucos registradores isto pode acarretar uma quantidade expressiva de
spills, isto e, uma expressiva representacao de variaveis na memoria, ocorrida pela falta
de registradores.
Alem do uso de uma representacao SSA, sistemas modernos utilizam diferentes
profilers para tambem auxiliar o processo de geracao de versoes especializadas. Profilers
(Krintz, 2003; Namjoshi e Kulkarni, 2010) sao projetados para coletar informacoes sobre os
programas, como tipos de dados, modos de execucao e backward branches, estaticamente
e/ou dinamicamente. Type feedback profiler foi implementado pela primeira vez no
compilador SELF-93 (Holzle, 1995; Holzle e Ungar, 1994) de modo a extrair informacoes
de tipo de execucoes previas e retorna-las ao compilador. Este retorno poderia acontecer
dinamicamente ou estaticamente. Para tal, o sistema instrumentava o codigo para gravar
os tipos coletados e propaga-los ao sistema de compilacao na medida em que a execucao
prosseguia. Dessa forma, baseado nas informacoes coletadas, o compilador poderia prever
provaveis classes receptoras (no contexto de SELF), ou tipos de dados (em um contexto
geral) e gerar codigo mais eficiente. Em uma situacao onde type feedback indicar que uma
determinada variavel sempre assumira um valor inteiro, o compilador podera gerar codigo
especializado para esta situacao.
Alem das questoes ja abordadas, um fato que deve ser observado no desenvolvimento
de sistemas de compilacao JIT e o fato de algumas otimizacoes altamente efetivas serem
complicadas por causa da flexibilidade de determinadas linguagens de programacao. No
contexto de Java, por exemplo, metodos virtuais limitam a aplicacao de inline. Aplicar
esta tecnica e difıcil para metodos virtuais porque nao se sabe estaticamente que metodo
sera invocado.
39
Remover o codigo compilado resulta em um retorno ao interpretador. Uma solucao
para este caso e substituir o codigo compilado pelo interpretado. Esta situacao mostra
uma vantagem de ter simultaneamente codigo compilado e interpretado. A transicao entre
codigo compilado e interpretado e chamada de desotimizacao. Para tal, o compilador deve
gerar uma estrutura de dados que permita a reconstrucao do estado do interpretador ate
o ponto de chamada do codigo compilado.
Desotimizacao e fundamental para que o compilador possa realizar otimizacoes agres-
sivas que aceleram a execucao normal enquanto seja capaz de gerenciar situacoes onde
uma determinada otimizacao deva ser desfeita. Existem casos crıticos onde o metodo
compilado e desotimizado, por exemplo, quando ocorrem excecoes. Com desotimizacao,
o codigo compilado nao precisa gerenciar tais situacoes, que em geral sao casos incomuns.
Sistemas que utilizam este recurso sao SELF (Chambers e Ungar, 1989, 1990; Chambers,
1992; Holzle, 1995; Holzle e Ungar, 1994), Sun HotSpot Client (Kotzmann et al.,
2008) e Sun HotSpot Server (Paleczny et al., 2001).
2.3.5 Implementacao e Desempenho
Uma questao importante no tocante a implementacao de um sistema JIT e a escolha
adequada dos princıpios que nortearao tal implementacao. As escolhas de projeto devem
balancear o tradeoff existente entre tempo de compilacao e a eficiencia do codigo gerado.
De fato, implementacao e desempenho sao duas questoes que estao relacionadas. Portanto,
o projeto de um sistema deste porte deve considerar as seguintes questoes:
Modo de implementacao A escolha entre um sistema misto ou um com compilacao
e recompilacao possui vantagens e desvantagens. Sistemas que utilizam um
interpretador proveem um ambiente portavel, seguro e de facil implementacao, mas
incorrem no overhead de interpretacao. Por outro lado, embora o overhead nao
ocorra em sistemas que utilizam apenas compilacao, estes sao mais complexos para
manter. Alem disso, a tendencia e que sistemas que utilizam apenas compilacao
tenham um desempenho superior aqueles que ainda interpretam codigo (isto em
relacao a sistemas que utilizam as mesmas abordagens para implementar/solucionar
as demais questoes), pelo fato, do tempo gasto em interpretacao ser geralmente uma
ordem de grandeza maior que a execucao de codigo compilado.
Sistemas de Compilacao e Execucao A abordagem ideal utilizada no mecanismo
de compilacao e execucao deve aproveitar ao maximo a capacidade das arquiteturas
de hardware atuais. Com a limitacao no aumento gradual do clock do processador
40
(Patterson e Hennessy, 2005), as arquiteturas modernas tem investido em maquinas
multi-core. Tais maquinas proveem diversos nucleos de processamento, que sao
utilizados para executar diversas tarefas simultaneamente, sendo, portanto, ideais
para serem exploradas em um sistema de compilac~ao contınua. De fato, a
abordagem JIT incorre em pelo menos dois problemas. Primeiro, ela nao utiliza
a deteccao de regioes quentes e consequentemente pode incorrer em overhead de
compilacao de unidades que nao amortizaram o tempo gasto pelo compilador.
Segundo, ela nao utiliza informacoes coletadas em tempo de execucao e, como cada
unidade e compilada imediatamente antes de sua invocacao, nao existe no sistema
informacoes sobre a unidade, ja que esta nao foi ainda executada. Porem, e possıvel
que tal abordagem seja beneficiada pelo uso de um offline profiler. A abordagem
smart JIT possui o atrativo de minimizar o overhead de compilacao, gerando codigo
especializado apenas para regioes quentes. Contudo, incorre no problema de parar o
motor de execucao para que o sistema de compilacao seja acionado. A compilac~ao
contınua, alem de possuir o atrativo de minimizar os problemas inerentes as outras
duas abordagens, possui o potencial de explorar as arquiteturas modernas. Isto
decorre no uso de pelo menos duas threads, uma para o motor de execucao e outra
para o sistema de compilacao.
Acionamento do Sistema de Compilacao O uso de contadores para acionar o sis-
tema de compilacao possui o atrativo de ser uma abordagem simples de imple-
mentar, em comparacao com a abordagem de utilizar amostragem de tempo.
Balance necessita da implementacao de um mecanismo de monitoramento de tempo
de execucao e crossover necessita que o programa seja compilado previamente.
Abordagens que utilizam amostragem de tempo nao sao adequadas para sistemas
interativos. Mas possuem potencial para sistemas servidores, nos quais nao existe a
interacao direta com o usuario e geralmente as aplicacoes possuem um longo tempo
de execucao. E importante ressaltar, que embora com o uso de crossover exista
a necessidade de compilar previamente o programa, a estimativa correta do limite
maximo do contador requer informacoes empıricas, situacao que tambem requer
execucoes previas da aplicacao. De fato, uma boa estimativa ira requerer diversos
experimentos, por outro lado crossover requer apenas uma unica execucao. Nesta
questao, o trabalho de Suganuma et al. (2005) demonstrou que o uso de contadores
pode ser mais eficiente do que o uso de amostras de tempo. Portanto, embora a
estimativa do limite do contador seja uma tarefa dispendiosa, esta pode proporcionar
um bom desempenho ao sistema.
41
Selecao de Unidades de Compilacao A escolha de sub-rotinas como unidades de
compilacao possui o atrativo de ser uma abordagem natural. Contudo, o fluxo de
execucao de um determinado programa pode se concentrar apenas em uma porcao de
codigo contida em uma sub-rotina. O uso de regioes possui o atrativo de eliminar
porcoes de codigo que nao sao executados frequentemente e o uso de tracos de
execucao, alem de possuir este atrativo, possui o potencial de integrar em uma unica
porcao compilada diversas unidades de codigo. Esta integracao possui a tendencia
de minimizar o custo do chaveamento entre diversas versoes de codigo, como tambem
de aumentar o escopo de aplicacao de otimizacoes, possibilitando assim, o aumento
no ganho de desempenho do sistema. Por outro lado, tracos de execucao requer um
esforco maior de implementacao do que as outras duas abordagens, o que pode ser
evidenciado pelo fato desta estrategia requerer algoritmos mais elaborados para sua
implementacao.
Geracao de Versoes Especializadas Esta tarefa e uma necessidade em sistemas JIT,
devido ao fato de estas terem por objetivo aumentar o desempenho de ambientes
projetados para executar programas. Independente do modo de implementacao do
sistema, o projetista deve prover um mecanismo que seja capaz de especializar o
codigo que esta sendo gerado durante a execucao do programa, em outras palavras
o projetista deve implementar um mecanismo que aplique otimizacoes ao codigo
compilado. Uma abordagem conservativa, embora simples do ponto de vista da
implementacao, possui a tendencia de nao prover um codigo com uma alta eficiencia,
embora a geracao de codigo de alta qualidade seja um problema complexo de
ser resolvido. O ideal e que o sistema se adapte as caracterısticas do programa
em execucao e, desta forma, escolha dinamicamente quais otimizacoes aplicar.
Ainda nesta questao, e papel do desenvolvedor do sistema escolher quais algoritmos
utilizar para implementar cada otimizacao, o que e essencial pelo fato de existirem
algoritmos que sao mais eficientes em situacoes mais especıficas, pois a eficiencia
de um algoritmo de otimizacao esta relacionada ao contexto de sua aplicacao como
tambem a representacao interna utilizada para o codigo do programa.
2.4 Sistemas
Mesmo que os primeiros sistemas de compilacao JIT tenham surgido a partir de 1960, com
LISP, somente uma decada apos, com o trabalho de Hansen, e que os desafios a serem
enfrentados por tais sistemas foram formalizados (Hansen, 1974). Desde entao, sistemas de
42
compilacao JIT tem incorporado diversos princıpios que diversificaram decisoes de projeto,
arquitetura e tecnicas. Tais decisoes sao destacadas na apresentacao dos sistemas a seguir.
Adaptive Fortran foi projetado para suportar execucao de codigo em modo misto
(Hansen, 1974). Todos as unidades de codigo do programa sao instrumentadas
com contadores, que sao atualizados a cada chamada da unidade. Dessa forma,
uma unidade se torna um candidato para a “proxima otimizacao” caso seu contador
ultrapasse o limite de frequencia pre-estabelecido. Durante esse passo, um codigo
supervisor e invocado entre as unidades de codigo, que acessa os contadores e aplica
otimizacoes (se necessario). Por fim, o sistema transfere o controle de execucao para
a proxima unidade, dando preferencia na execucao da versao nativa dessa unidade,
se existir. As unidades de codigo escalonadas para compilacao sao otimizados por
uma tecnica de otimizacao por vez e recompilados com uma tecnica de otimizacao
diferente caso se tornem regioes crıticas novamente. Esta estrategia mostrou ser
uma esquema de (re)compilacao primitivo, que foi mais tarde aprimorado por outros
sistemas (Arnold et al., 2000; Cierniak et al., 2000; Holzle e Ungar, 1994; Suganuma
et al., 2004).
Sistema de Smalltalk-80 traduz codigo da Maquina Virtual Smalltalk (Gold-
berg e Robson, 1983) para codigo nativo no momento em que um metodo e invocado
(Deutsch e Schiffman, 1984). Apos esse processo, o codigo gerado e armazenado
em uma cache para execucoes posteriores. O sistema projetado e vinculado a um
gerenciamento de memoria, que impede a paginacao de codigo nativo caso a cache
ultrapasse um limite de armazenamento. Se por algum momento esse limite for
alcancado, o codigo nativo e simplesmente descartado e novamente gerado, caso
necessario.
SELF possui um ambiente de execucao que implementa o mecanismo de compilacao mais
recompilacao (Holzle, 1995; Holzle e Ungar, 1994). Basicamente, um compilador
rapido nao otimizador compila as unidades de codigo que estao prestes a serem exe-
cutadas e, utilizando contadores, o sistema identifica os metodos crıticos e compila-os
com um compilador otimizador. Este ultimo emprega compilacao baseada em regiao,
porem diferente daquela apresentado por Suganuma et al. (Suganuma et al., 2006).
Para tal, um sistema de predicao de tipos e utilizado para detectar e remover codigo
das mensagens enviadas as classes pouco comuns, o que significa que apenas codigo
de mensagens enviadas as classes frequentes sao compilados.
43
Kaffe pode executar codigo tanto em forma interpretada quanto compilada (Wilkinson e
Mehlitz, ????). No modo de compilacao,Kaffe compila todos metodos no momento
de sua invocacao e nao executa codigo misto. Isso significa que o sistema se enquadra
no modelo JIT, segundo a visao de Plezbert e Cytron (Plezbert e Cytron, 1997). O
compilador JIT incorporado converte os bytecodes de entrada em uma representacao
intermediaria chamada KaffeIR, que e mapeada para codigo nativo do hardware
utilizado, por meio de um conjunto de macros pre-definidas.
Jikes RVM emprega a estrategia de compilacao mais recompilacao (Arnold et al.,
2000; Burke et al., 1999). Ele compila todos os metodos na medida em que sao
invocados por um compilador base nao otimizador e recompila as regioes crıticas
com um compilador otimizador (Burke et al., 1999), que emprega varias otimizacoes
separadas em tres nıveis. O primeiro consiste de um conjunto de otimizacoes
aplicadas durante a traducao dos bytecodes para uma representacao intermediaria
utilizada pelo compilador. O segundo nıvel aplica todas as otimizacoes do primeiro
nıvel, em adicao com inline e outras otimizacoes locais. Por fim, o nıvel mais
agressivo emprega todas as otimizacoes do nıvel anterior, em adicao com otimizacoes
baseadas na representacao SSA para variaveis escalares (Cytron et al., 1991).
Basicamente, Jikes RVM detecta regioes crıticas empregando amostras de tempo.
As regioes crıticas sao, portanto, encaminhadas para uma fila e julgadas propıcias
para (re)compilacao (em nıveis maiores de otimizacao) ou nao, baseado em uma
analise de custo-benefıcio realizada pelo sistema. Dessa forma, se parecer rentavel,
a regiao crıtica e (re)otimizada de acordo com o nıvel de otimizacao avaliado por uma
thread especıfica, de modo a evitar pausas na execucao e se beneficiar de paralelismo.
JUDO (Java Under Dynamic Optimization) compila todo o codigo por um gerador
rapido de codigo e instrumenta-o de modo que o sistema possa coletar informacoes
em tempo de execucao (Cierniak et al., 2000). Por fim, as regioes crıticas detec-
tadas sao escalonadas para compilacao por um compilador otimizador, que aplica
otimizacoes agressivas. Tais regioes sao detectadas de duas formas possıveis: (1)
utilizando contadores, ou (2) utilizando tempo. Na primeira abordagem, cada
metodo e instrumentado com um contador fixado com um valor limite. No momento
em que o metodo e chamado, seu contador e decrementado e, chegando a zero, o
metodo e imediatamente enviado para compilacao. Ja, na segunda abordagem uma
thread separada e invocada em determinados intervalos de tempo para verificar o
valor-limiar (medido em segundos) de todos os metodos (seu tempo de execucao)
e enviar para compilacao aqueles que se tornaram regioes crıticas. Com o uso de
44
contadores, o metodo e enviado para compilacao imediatamente apos o contador
vinculado atingir o valor zero, contadores que sao decrementados antes da execucao
do metodo. Por outro lado, com o uso de tempo, se o sistema identificar que um
metodo atingiu um limiar de execucao, este so sera enviado para compilacao na sua
proxima chamada.
HiPE e um compilador JIT para a linguagem Erlang baseado em compilacao de sub-rotinas
(Pettersson et al., 2002). Este compilador ainda inclui extensoes para que o sistema
suporte execucao em modo misto de codigo. Todas as sub-rotinas sao interpretadas
pela maquina virtual no inıcio da execucao e somente aquelas frequentemente
executadas sao compiladas. Medidas utilizadas para verificar a frequencia de
chamada de sub-rotinas incluem contadores e tempo.
Maquina Virtual da IBM gera codigo nativo somente para os metodos crıticos e
interpreta os demais (Suganuma et al., 2004). A contagem de frequencia se baseia no
uso de contadores, que sao incrementados a cada chamada ao metodo. Unidades de
codigo que representam lacos frequentes tambem sao considerados para compilacao.
Tal como em Jikes RVM, o sistema implementa compilacao em multi-nıvel, cuja
configuracao foi baseada em medidas empıricas de custo versus benefıcio obtidas
do trabalho de Ishizaki et al (Ishizaki et al., 2003). Tres nıveis de otimizacao
sao aplicados: o primeiro nıvel aplica um conjunto muito reduzido de otimizacoes,
proprio para minimizar o overhead de compilacao, enquanto os demais aplicam todas
as otimizacoes disponıveis no compilador repetidas vezes, porem em quantidade
limitada no nıvel intermediario. Na medida em que um metodo e compilado,
este e monitorado periodicamente pelo sistema. Dependendo do valor do contador
encontrado (a frequencia), o metodo e promovido com otimizacoes de nıvel 2 ou 3.
No nıvel 1, a compilacao e regida pela propria thread de aplicacao (configuracao
Smart JIT ), porem os nıveis 2 e 3, empregam threads separadas para compilacao
em segundo plano (compilacao contınua).
HotPathVM e um compilador JIT baseado em caminhos de execucao projetado para a
maquina virtual JamVM (Gal et al., 2006; Lougher, ????), propria para dispositivos
embarcados. O sistema primeiramente interpreta os bytecodes Java construindo os
caminhos para os trechos de codigo executados em maior frequencia e, em seguida,
gera codigo nativo para estes. Infelizmente, informacoes relacionadas a forma de
deteccao de regioes crıticas nao sao fornecidas. Segundo os autores, HotpathVM
45
utiliza apenas 150 KB de memoria, incluindo codigo e dados, e consegue gerar codigo
compatıvel aqueles gerados por outros compiladores JIT de grande porte.
YAP com compilacao JIT foi projetado para melhorar o desempenho de linguagens
logicas (Silva e Costa, 2007). Inicialmente todas as clausulas sao instrumentadas
com contadores e interpretadas. Cada invocacao, contudo, incremeta o contador
em uma unidade. Dessa forma, uma clausula se torna uma regiao crıtica caso seu
contador atinja um limite pre-estabelecido, embora somente as clausulas com um
tamanho mınimo sejam escalonadas para compilacao. Esta ultima restricao e para
evitar que o compilador gaste tempo compilando uma unidade que possivelmente nao
trara benefıcio na execucao. O sistema em si aplica poucas otimizacoes de codigo
se comparado a outros sistemas JIT existentes, mas consegue acelerar de forma
significativa a execucao de Prolog frente a sua contraparte puramente interpretada.
Sun HotSpot inicialmente interpreta todos os metodos e vincula a estes contadores
que medem a frequencia de execucao. Portanto, cada metodo e escalonado para
compilacao quando o seu contador atinge um limite. Adicionalmente, metodos
com alto tempo de execucao tambem podem ser compilados, mesmo se invocados
poucas vezes. O ambiente de execucao compartilha dois compiladores: o compilador
cliente (Kotzmann et al., 2008) e o compilador servidor (Paleczny et al., 2001), que
podem ser escolhidos manualmente a cada execucao. O compilador Sun HotSpot
Client (Kotzmann et al., 2008) e indicado para programas interativos, cujo tempo
de resposta e mais importante que o desempenho, sendo entao projetado para
alcancar um tradeoff entre a velocidade de execucao e o desempenho do codigo
final gerado. A compilacao e realizada em diversas etapas, que transformam o
codigo gradualmente ate gerar o codigo final da plataforma alvo. O compilador
Sun HotSpot Server (Paleczny et al., 2001) e indicado para programas de longa
duracao, cujo tempo de execucao de codigo nativo compense o tempo de compilacao
gasto. Este compilador, se comparado ao cliente, e mais agressivo nas otimizacoes
e algumas delas sao guiadas por analise global. Alem disso, o compilador servidor
e capaz de detectar metodos virtuais pouco comuns e referencias para classes nao
inicializadas para evitar gerar codigo para essas partes. Na visao de Plezbert e
Cytron (Plezbert e Cytron, 1997), Sun HotSpot Server e classificado como um
sistema de compilacao contınua, pois pode se beneficiar de compilacao em segundo
plano, caso o hardware possua multiplos processadores.
46
Tamarin-Trace e um compilador para JavaScript baseado em compilacao por selecao
de caminhos de execucao (Chang et al., 2009). Cada backward branch do programa
possui, vinculado ao seu destino, um contador que mede a frequencia de execucao.
A partir disso, considerando que a maior parte do destino do desvio e um loop
header, o interpretador nao somente interpreta o codigo, como tambem constroi
o caminho de execucao a partir dele. Quando a gravacao do caminho alcanca
novamente o loop header, o processo e finalizado e o caminho e transformado para
uma representacao intermediaria de baixo nıvel que, por sua vez, e traduzido para
codigo de maquina. Como no momento da gravacao do caminho, os tipos das
variaveis sao conhecidos, o compilador pode realizar especializacao de codigo para
cada tipo detectado. Adicionalmente, decisoes em tempo de execucao, como desvios
condicionais, sao marcados para que a execucao retorne ao modo interpretado caso
um fluxo de controle ainda nao compilado seja encontrado. Se isso ocorrer, o novo
caminho e gravado e acoplado ao caminho anterior, formando assim, uma arvore de
caminhos de execucao, que e entao tratada com qualquer caminho gravado.
TraceMonkey e um compilador JIT para JavaScript baseada em compilacao por
caminhos de execucao, que segue o modelo de interpretacao e compilacao (Gal et
al., 2009). Os pontos crıticos do programa sao detectados a partir de contadores e
consistem de arestas de retorno dos lacos. Quando um destes pontos e detectado, o
interpretador entra em um modo especial para gravar o caminho de execucao a partir
dele, enquanto ainda interpreta codigo. A gravacao do caminho e finalizado quando
o ponto inicial da gravacao e encontrado. Apos a geracao do codigo nativo, o sistema
mantem a execucao do codigo gerado ate que a avaliacao de tipos retorne um tipo
diferente, ou ate que um caminho nao-compilado seja tomado. Em qualquer destes
casos, o sistema retorna ao modo interpretado e reinicia a construcao do caminho
de execucao.
LuaJIT implementa compilacao baseada em caminhos de execucao (Pall, ????). Primei-
ramente, o sistema interpreta codigo e mantem estatısticas de execucao dos lacos e
chamadas de funcoes, que por sua vez, e medida com contadores. Quando um limite
e alcancado, a unidade de codigo (funcao ou laco) e considerada crıtica e, a partir
dela, o caminho de execucao e construıdo. A interpretacao permanece ate que o
caminho seja definitivamente construıdo. As condicoes para o termino de construcao
do caminho e um pouco diferente do apresentado no decorrer do texto. Basicamente,
LuaJIT finaliza um caminho de execucao apos: (1) uma instrucao nao-tratavel,
como uma excecao, for encontrada ou quando (2) o inıcio do caminho em construcao
47
for encontrado. No primeiro caso, o caminho e descartado e no segundo, e enviado
para compilacao. Apos a compilacao, o sistema prioriza a execucao da versao nativa,
como em todo o sistema baseado em codigo misto. Durante este passo, decisoes
tomadas em tempo de execucao, como desvios condicionais e checagem de tipos,
podem causar a saıda do caminho de execucao. Isso pode ocorrer caso um caminho
diferente do fluxo de controle (ainda nao compilado) for tomado ou quando uma
checagem de tipos falha. Em qualquer destes casos, o sistema retorna a execucao
ao modo interpretado e reinicia o processo de contrucao do caminho.
ILDJIT e um sistema de compilacao JIT que utiliza a polıtica de interpretacao mais
compila-cao (Campanoni et al., 2008, 2010). A ideia desse sistema e se beneficiar
do paralelismo oferecido pelas maquinas multi-core atuais. A regiao crıtica basica
de ILDJIT e flexıvel, embora um metodo seja a unidade de compilacao mınima. A
deteccao de tais regioes e garantida pela polıtica ahead-of-time, utilizando a tecnica
look ahead compilation (Campanoni et al., 2009). Com essa tecnica, o sistema
verifica a constituicao do programa a partir do metodo atualmente em execucao
e seleciona os metodos mais proximos a este, enviando-os para compilacao mais
cedo. Isso e baseado em uma heurıstica, que espera que tais metodos possuam
maiores chances de serem invocados mais recentemente que outros. O sistema
ainda explora automaticamente as configuracoes da maquina adjacente e dispara
threads de compilacao conforme a maquina oferece. O paralelismo entre execucao e
compilacao e garantido pelo proprio sistema.
HappyJIT e um compilador JIT (Homescu e Suhan, 2011) para PHP desenvolvido a partir
de PyPy (Rigo e Pedroni, 2006), um framework para construcao de maquinas
virtuais que e composto por dois componentes: (1) um interpretador Python
(Lutz e Ascher, 2007) escrito em um subconjunto restrito da linguagem, chamada
RPython (Ancona et al., 2007) e (2) um tradutor, que gera codigo nativo para
o primeiro componente. O objetivo dos autores com essa estrategia e facilitar
a implementacao de linguagens dinamicas, de modo que o projetista se preocupe
apenas em escrever o interpretador para sua linguagem em RPython. Os detalhes
de baixo nıvel e a geracao de codigo fica a cargo do proprio framework. A
fim de facilitar o esforco de implementacao, HappyJIT reutiliza o parser do
motor Zend (Zend, ????), que converte codigo PHP internamente para um bytecode
linear. Esse bytecode e recuperado e traduzido para a representacao suportada pelo
interpretador construıdo. Dessa forma, somente as estruturas de dados necessarias
para representar os tipos de dados de PHP em memoria e este interpretador foram
48
implementados. Por fim, o interpretador construıdo e passado como entrada para
PyPy. Isso significa que, durante a execucao ha dois interpretadores envolvidos: o
projetado para interpretar o programa PHP do usuario e o utilizado para interpretar
este interpretador. Essa forma de execucao so nao possui um alto overhead por
causa do compilador JIT acoplado ao segundo interpretador (Bolz et al., 2009).
Este compilador gera codigo para caminhos de execucao frequentes e consegue gerar
bons resultados, segundo os proprios autores.
Os princıpios de compilacao JIT descritos na Secao 2.3 podem ser utilizados para
classificar os sistemas que empregam este tipo de compilacao. Desta forma, os sistemas
descritos no presente capıtulo sao classificados da seguinte forma:
• Tipo do sistema que pode ser: interpretacao mais compilacao, compilacao mais
recompilacao ou ainda uma variacao deste ultimo, na qual cada unidade de codigo
e compilada imediatamente antes de sua primeira invocacao, sem ser recompilada.
• Manutencao do sistema, podendo ser: JIT, smart JIT, ou compilacao contınua.
• Forma de deteccao das regioes crıticas que pode ser por meio de contadores ou
fracao de tempo. E importante ressaltar que em implementacoes cuja manutencao
do sistema utiliza a abordagem JIT nao existe um mecanismo de deteccao de regioes
crıticas, pelo fato de cada unidade de codigo invocada ser compilada antes de sua
invocacao. Portanto, em tais sistemas nao existe uma forma de deteccao de regioes
crıticas.
• Estrutura das regioes crıticas podendo ser: sub-rotina, regiao ou caminho de
execucao.
• Polıtica de aplicacao de otimizacoes de codigo que indica se o processo de
compilacao emprega ou nao otimizacoes com o objetivo de gerar um codigo de
qualidade. As possıveis polıticas sao: ingenua (poucas otimizacoes sao aplicadas),
agressiva (diversas otimizacoes sao aplicadas), ou nıveis (o sistema alterna durante
a execucao o nıvel de otimizacao que sera aplicado).
A Tabela - 2.1 apresenta uma comparacao entre os sistemas apresentados nesta secao.
Nesta tabela, a primeira coluna apresenta o nome do sistema, a segunda o seu tipo,
a terceira a forma utilizada na manutencao, a quarta a forma de deteccao de regioes
crıticas, a quinta a estrutura das regioes crıticas e por fim, a ultima coluna apresenta a
polıtica utilizada na aplicacao de otimizacoes de codigo. As siglas nesta tabela significam:
49
(IC) interpretacao mais compilacao; (CR) compilacao mais recompilacao; (C) compilacao
- para aqueles sistemas que compilam na primeira invocacao sem recompilar; (CC)
compilacao contınua; (NE) nao existente; (INF) informacao nao fornecida.
Estrutura
SistemaTipo de Formas de Forma de
das RegioesPolıtica de
Sistema Manutencao DeteccaoCrıticas
Otimizacoes
AdaptiveFortran
IC Smart JIT Contadores Sub-rotina Ingenua
Sistema deSmalltalk-80
CR JIT NE Sub-rotina INF
SELF CR Smart JIT Contadores Regiao IngenuaKaffe C JIT NE Sub-rotina INF
Jikes RVM CR CC Tempo Sub-rotina NıveisContadores
JUDO CR Smart JITTempo
Sub-rotina Agressiva
ContadoresHiPE IC Smart JIT
TempoSub-rotina INF
Maquina Smart JITVirtual IBM
ICCC
Contadores Sub-rotina Nıveis
HotPathVM IC Smart JIT INF Caminho INFYAP com JIT IC Smart JIT Contadores Sub-rotina IngenuaSun Hotspot Contadores
ClientIC Smart JIT
TempoSub-rotina Ingenua
Sun Hotspot ContadoresServer
IC CCTempo
Sub-rotina Agressiva
Tamarin-Trace IC Smart JIT Contadores Caminho AgressivaTraceMonkey IC Smart JIT Contadores Caminho INF
LuaJIT IC Smart JIT Contadores Caminho IngenuaILDJIT IC Smart JIT INF Sub-rotina INF
HappyJIT IC Smart JIT Contadores Caminho INF
Tabela 2.1: Comparacao entre os sistemas JIT.
Em sıntese, a maioria dos sistemas apostam na estrategia de interpretar e compilar
codigo. Provavelmente, a pouca quantidade de sistemas que compilam e recompilam se
deve pelo trabalho de implementar um compilador-base que, apesar de simples, e ainda
mais complexo que um interpretador. Outro ponto importante e referente as estrategias
de otimizacao de codigo, que mostram ser pouco investigadas no campo de compilacao
JIT. Por outro lado, mecanismos para detectar regioes crıticas tem se mostrado mais
importantes. De fato, diversos sistema tem procurado alcancar desempenho por meio da
50
deteccao e geracao de codigo para porcoes de codigo que nao se limitam ao escopo de uma
sub-rotina.
2.5 Consideracoes Finais
Uma caracterıstica diferencial de compiladores JIT frente ao compiladores tradicionais
e que a geracao de codigo nativo ocorre em tempo de execucao. Mesmo que isso seja
preocupante no quesito de desempenho, sistemas que empregam compiladores JIT sao
atualmente, os principais componentes integrados que incrementam o desempenho das
maquinas virtuais. Isso se deve principalmente a criacao e aprimoramento de tecnicas
que se baseiam em hipoteses relacionadas a organizacao e comportamento dos programas
que sao, infelizmente, difıceis (senao impossıveis) de se prever, mesmo durante o tempo
de execucao. Dessa forma, as pesquisas nao possuem outra alternativa senao (1) unir
estrategias para obtencao de outra (Krintz, 2003), (2) estender as existentes (Gal et al.,
2006; Lee et al., 2008; Suganuma et al., 2003) ou (3) buscar melhorias nas arquiteturas
de hardware (Campanoni et al., 2009; Kulkarni, 2011; Kulkarni et al., 2007). Portanto, e
de se esperar que sistemas JIT futuros mantenham o uso das mesmas estrategias, porem
aprimoradas, empregadas nos sistemas atuais.
As estrategias de interpretacao mais compilacao e compilacao mais recompilacao ainda
permanecerao por muito tempo. Para sistemas de interpretacao mais compilacao existe
o overhead da interpretacao. Por outro lado, em sistemas que utilizam compilacao mais
recompilacao existe o overhead de compilar o codigo de todo o programa, embora sem
ou com poucas otimizacoes. Por isso e difıcil avaliar qual abordagem se sobressaira sobre
outra.
Relativo a isso, Suganuma et al. (2005) conseguiram importantes informacoes de
desempenho relativas as abordagens classicas de compilacao JIT. Com um framework
especıfico, os autores observaram que a estrategia de interpretacao mais compilacao exe-
cutou programas de forma mais rapida que a estrategia de compilacao mais recompilacao.
No mesmo trabalho, o uso de contadores na deteccao de regioes quentes provou ser mais
preciso que o uso de tempo. Tais resultados, entretanto, nao foram necessariamente
adotados na construcao de todos os sistemas JIT que vieram depois, contudo, podem
ser considerados (juntamente com o resultado de outros trabalhos) no projeto de futuras
maquinas virtuais que incorporam compilacao JIT.
Outra questao que permanecera e o uso de um sistema de otimizacao adaptativa.
O fato de a tendencia dos sistemas JIT convergir ao uso de nıveis de otimizacao se
deve ao fato de que tais sistemas se adaptam melhor as caracterısticas dos programas.
51
Programas rapidos, quando compilados com o uso de otimizacoes mais leves, ocasionam
pouco overhead de compilacao. E programas com longo tempo de execucao quando
compilados com o uso de otimizacoes mais agressivas ocasionam um codigo mais eficiente
(Arnold et al., 2000).
Diversas pesquisas tem apresentado tendencias para a proxima geracao de sistemas
com compilacao dinamica. Pesquisas na area de otimizacao (Cavazos e O’Boyle, 2005;
Hoste et al., 2010; Triantafyllis et al., 2003) tem demonstrado que os sistemas poderao
se beneficiar de estudos sobre os campos de acao das otimizacoes, projetadas automa-
ticamente por algoritmos heurısticos. Adicionalmente, os sistemas poderao se basear
em aprendizagem de maquina, para que baseado nas otimizacoes e caracterısticas de
programas executados anteriormente, o sistema se adapte com o tempo (Agakov et al.,
2006; Cavazos e O’Boyle, 2006).
Bruening e Duesterwald (2000) demonstraram que a escolha de metodos (e similares)
como unidades de compilacao e uma escolha ruim. Adicionalmente, na visao de sistemas
JIT, a compilacao baseada em regioes e muito conservativa. Portanto, baseado nas
observacoes de Inoue et al. (2011), sistemas JIT baseados em grandes tracos de execucao
podem ser a melhor escolha de implementacao. Adicionalmente, Kulkarni (2011) destaca
que o sistema JIT pode ajustar a quantidade de threads de compilacao e o limite (contador
ou tempo) para tornar uma regiao quente da melhor forma possıvel, desde que o aumento
do numero de threads de compilacao ativas incorra na diminuicao do valor de limite,
segundo as observacoes do proprio autor.
Provavelmente uma diferenca substancial vira do uso de ferramentas como GNU
Lightning (Bonzini, ????) e LLVM (Lattner e Adve, 2004) na producao de novos
compiladores, devido as facilidades oferecidas por elas. Relacionado a isso, e de esperar
que em nao muito tempo novos compiladores estarao sendo desenvolvidos com base
nestas ferramentas. GNU Lightning apresenta uma serie de desvantagens: possui
uma quantidade limitada de registradores, nao implementa um otimizador peephole, nao
implementa um escalonador de instrucoes, e nao possui uma ferramenta para analise de
codigo, porem, e uma ferramenta rapida na construcao dos compiladores, ocupa pouco
espaco em memoria e garante portabilidade. LLVM e uma ferramenta mais robusta e
oferece recursos de modo que o projetista se preocupe somente em implementar um parser
que transforme a linguagem fonte na representacao intermediaria suportada. Alem disso,
LLVM prove diversas otimizacoes em tempo de compilacao, ligacao e execucao. Outra
vantagem desta ferramenta e a possibilidade de ordenar as otimizacoes da melhor forma
possıvel, o que abre oportunidades para a incorporacao de planos de otimizacao especıficos.
52
Portanto, e baseado nas funcionalidades de LLVM que o sistema relativo a este
trabalho foi desenvolvido. Alem de empregar a coleta de tracos de execucao, os trechos
quentes de codigo sao compilados por essa ferramenta. O Capıtulo 4 apresenta o sistema
desenvolvido e como LLVM foi utilizado para executar essa tarefa. Contudo, antes de
apresentar o sistema em si, e necessario descrever os conceitos de Prolog, que foram
relevantes neste trabalho. Tais conceitos sao apresentados no proximo capıtulo.
53
3
Prolog
Prolog e uma linguagem de programacao em logica baseada em um subconjunto de logica
de primeira ordem denominada clausulas de Horn (Horn, 1951). Sua principal utilizacao
reside no domınio da programacao simbolica, em que a solucao dos problemas envolve
relacoes entre objetos. Exemplos de sua aplicacao incluem processamento de linguagem
natural (Quintano e Rodrigues, 2006) e construcao de sistemas dedutivos (Ramakrishnan
et al., 2006).
Na pratica, Prolog pode ser implementado tanto por interpretacao de codigo quanto
por compilacao. Interpretadores sao mais faceis de serem mantidos, mas sao prejudicados
na velocidade pela necessidade de traduzir codigo em tempo de execucao. Por outro lado,
compiladores garantem codigo mais rapido, mas sao mais complexos que interpretadores.
Independente do modo de implementacao, a maioria dos sistemas seguem um modelo de
implementacao Prolog desenvolvido por Warren (1983), chamada Maquina Abstrata de
Warren (WAM) (Aıt-Kaci, 1991; Warren, 1983), que define uma organizacao de memoria
e um conjunto de instrucoes.
O objetivo deste capıtulo e conceituar a WAM e apresentar algumas tecnicas utilizadas
para melhorar o desempenho de Prolog. A Secao 3.1 apresenta a forma de controle
da linguagem Prolog, necessaria para introduzir conceitos necessarios para as proximas
secoes. As Secoes 3.2 e 3.3 descrevem a WAM e algumas tecnicas empregadas para
melhorar o seu desempenho. A Secao 3.4, apresenta alguns exemplos de implementacao
para a linguagem e, por fim, a Secao 3.5 apresenta as consideracoes finais.
54
3.1 Controle da Linguagem
Os programas escritos em Prolog sao especificacoes de um determinado problema por
meio de sentencas em clausula de Horn (Horn, 1951). As clausulas podem ser classificadas
como fatos, que sao clausulas com corpo vazio, ou regras, que sao clausulas formadas por
uma cabeca e um corpo. A cabeca e representada pelo predicado que se deseja provar
(juntamente com os seus argumentos) e o corpo consiste de um conjunto de objetivos que
devem ser satisfeitos a fim de que o predicado presente na cabeca seja verdadeiro.
Durante a execucao, Prolog tenta satisfazer as clausulas na ordem em que elas
aparecem no programa, utilizando uma execucao para frente. Basicamente, ele seleciona
um literal e o unifica (Lloyd, 1987) ate nao existirem mais literais. Quando o sistema
invoca um predicado com mais de uma clausula, o ambiente de execucao tenta satisfazer
a primeira e constroi pontos de escolha para as outras. Nesse processo, se o sistema
nao puder satisfazer a clausula, um retrocesso ocorre e o sistema retorna para o ponto de
escolha mais recente para tentar satisfazer a proxima clausula. E importante ressaltar que
ao ocorrer retrocesso, todas as ligacoes realizadas a fim de tornar a clausula verdadeira sao
desfeitas, pois executar a proxima clausula pode fornecer valores diferentes as variaveis. O
ambiente de execucao procura satisfazer as clausulas a fim de encontrar as condicoes para
que uma determinada consulta (passada como entrada) seja verdadeira. Dessa forma, se
o sistema nao encontrar um padrao de unificacao que satisfaca todas as clausulas e nao
houver outra clausula para executar naquele predicado, o sistema informa que nao ha
resposta para a consulta passada.
Por outro lado, se o ambiente encontrar no mınimo um padrao de unificacao que torne
verdadeira a condicao passada, o sistema retorna sucesso na execucao.
3.2 Maquina Abstrata de Warren
A Maquina Abstrata de Warren (WAM) e um modelo para execucao de Prolog criado
por David Warren em 1983, que rapidamente se tornou padrao nas implementacoes para a
linguagem. A motivacao original deste projeto era implementa-lo em hardware (Van Roy
e Despain, 1992), contudo esta ideia nao obteve sucesso por duas principais razoes: (1) o
hardware deveria ser especializado para suporta-lo, limitando a sua procura no mercado,
afinal ha poucas pessoas interessadas em obter uma maquina que execute apenas Prolog; e
(2) por consequencia de baixa procura, o hardware especializado teria pouco investimento
frente aos demais microprocessadores.
55
Em contrapartida, implementacoes da WAM por meio de interpretadores (Carlsson
e Mildner, 2012; Costa et al., 2012; Diaz et al., 2012; Hermenegildo et al., 2012; Swift
e Warren, 2012; Tarau, 1991; Wielemaker et al., 2012) se tornaram bastante populares
devido a facilidade de implementacao. Algumas destas implementacoes adquirem um bom
desempenho, contudo, todas elas sao prejudicadas pelo overhead intrınseco a traducao
das instrucoes para a arquitetura em tempo de execucao (Romer et al., 1996), alem
de limitarem a aplicacao de otimizacoes devido a alta granularidade das instrucoes
WAM. Implementacoes que geram codigo nativo alcancam um bom desempenho, mas
tornam a transformacao de codigo uma tarefa complexa, resultando em sistemas de difıcil
manutencao.
3.2.1 O Estado Interno da WAM e a Organizacao da Memoria
Prolog e dinamicamente tipado, isto e, variaveis podem conter objetos de qualquer tipo
em tempo de execucao. Os termos da WAM sao repesentados como palavras compostas
por um rotulo, que define o tipo do termo e um valor, que e utilizado para diferentes
propositos dependendo do seu tipo. Valores podem incluir inteiros, enderecos de variaveis
e termos compostos (listas ou estruturas). Variaveis indefinidas sao implementadas como
ponteiros para elas mesmas e sao resolvidas durante a unificacao, onde uma variavel passa
a apontar para outra. Essa forma de representacao pode criar uma cadeia de ponteiros
entre variaveis, sendo necessario percorrer toda a cadeia para encontrar o valor de uma
variavel qualquer. Essa operacao e chamada de desreferenciacao.
A WAM mantem o estado interno da computacao atual definido por registradores que,
dentre os principais, sao:
• P: localizacao do programa (Program Counter).
• CP: apontador para a continuacao de clausulas (continuation pointer).
• E: apontador para o ambiente atual. Um ambiente normalmente esta associado
a uma clausula e armazena enderecos de retorno e todas as variaveis locais desta
clausula.
• B: apontador para o ultimo ponto de escolha. Os pontos de escolha guardam
informacoes referentes ao estado de execucao, a fim de possibilitar o retrocesso.
• A: topo da pilha local.
• TR: topo da trilha.
56
• H: topo da pilha global (Heap).
• HB: valor de H no ponto de escolha mais recente.
• S: e usado pela unificacao de termos compostos. Este registrador aponta para o
argumento do termo que esta sendo unificado: na WAM os argumentos sao acessados
por meio de sucessivos incrementos de S.
• Mode: e usado para identificar o modo de execucao de certas instrucoes, leitura ou
escrita. Na WAM existem dois modos de execucao: modo leitura e modo escrita.
O modo de leitura ocorre quando o objeto unificado com a cabeca da clausula e
um termo ja construıdo. O modo de escrita ocorre quando a cabeca da clausula e
unificada com um variavel livre, e o termo precisa ser construıdo.
• A1, ..., AN : registros dos argumentos.
• X1, ..., XN : variaveis temporarias.
• Y1, ..., YN : variaveis permanentes.
Os registradores Ai e Xi sao, na verdade, identicos; a diferenca nos nomes meramente
refletem usos diferentes. Os registradores Ai sao usados para passar os argumentos para
um procedimento enquanto que os registradores Xi sao usados para manter os valores de
variaveis temporarias da clausula.
Uma variavel temporaria e uma variavel que tem sua primeira ocorrencia na cabeca
da clausula e nao ocorre em mais de um objetivo no corpo. Essas variaveis nao precisam
ser armazenadas no ambiente da clausula. Por sua vez, uma variavel permanente e
qualquer variavel nao classificada como variavel temporaria. Variaveis permanentes
sao armazenadas em um ambiente e sao enderecadas por deslocamentos do ponteiro de
ambiente. Estas sao referenciadas como Y1, Y2, etc. Variaveis permanentes sao organizadas
no seu ambiente de tal forma que elas podem ser descartadas na medida em que nao sao
mais necessarias.
Com respeito ao estado externo, a WAM dispoe de cinco regioes na memoria que
consistem de quatro pilhas para tratamentos especıficos e uma area de codigo contendo
instrucoes e dados relativos ao programa. As pilhas se expandem no momento da chamada
de uma clausula e contraem no retrocesso, sao elas: pilha global, pilha local, trilha e a
PDL (push down list). A Figura - 3.1 apresenta a organizacao da memoria na WAM.
A pilha global (ou heap) armazena as estruturas compostas do programa Prolog, isto
e, listas e estruturas. A pilha local contem dois tipos de objetos: os ambientes e os pontos
57
Figura 3.1: Organizacao da Memoria na WAM.
de escolha. Um ambiente normalmente esta associado a uma clausula e armazena todas
as variaveis locais desta clausula enquanto que os pontos de escolha guardam informacoes
referentes ao estado de execucao, a fim de possibilitar o retrocesso. A trilha armazena
referencias de variaveis que foram definidas durante a unificacao, mas que devem ser
indefinidas durante o retrocesso e a PDL auxilia na unificacao dos termos.
3.2.2 Conjunto de Instrucoes
Os programas Prolog sao codificados em instrucoes sequenciais. Ha, basicamente, uma
instrucao para cada sımbolo do programa. Uma instrucao consiste de um codigo de
operacao, ou simplesmente opcode, mais os operandos, que podem ser inteiros, desloca-
mentos ou enderecos. A maioria das instrucoes Prolog possui dois operandos. O modelo
original da WAM consistia dos seguintes grupos de instrucoes: instrucoes get, instrucoes
put, instrucoes unify, instrucoes de procedimento e instrucoes de indexacao.
58
As instrucoes get sao responsaveis por unificar os argumentos do predicado com
os valores armazenados nos registradores A. As principais instrucoes deste grupo estao
descritas a seguir1.
get variable Vn, Ai atribui o valor de Ai em Vn;
get value Vn, Ai unifica o valor do registrador Ai com o conteudo de Vn, armazenando
o resultado da unificacao em Vn, se Vn for uma variavel temporaria;
get constant C, Ai obtem o valor de Ai e desreferencia-o. Se o resultado for uma
variavel, C e atribuıdo a ela. Caso contrario, ocorrera um retrocesso se este resultado
for diferente de C;
get nil Ai executa da mesma forma que get constant C, Ai, mas neste caso a cons-
tante e [];
get structure F, Ai desreferencia o valor em Ai e prossegue conforme os casos a seguir:
se o resultado for uma variavel, esta recebe um novo ponteiro que aponta no topo
da heap, o functor F e inserido na heap e a execucao prossegue em modo de escrita;
se o resultado for um functor identico a F, o registrador S e ajustado para apontar
para o endereco da heap imediatamente apos o endereco de F e a execucao prossegue
em modo de leitura. Qualquer outro caso resulta em retrocesso.
get list Ai desreferencia o valor em Ai e prossegue conforme os casos a seguir: se o
resultado for uma variavel, esta recebe um novo ponteiro para o topo da heap e a
execucao prossegue em modo de escrita; se o resultado e uma lista, o registrador
S e ajustado para apontar para o endereco da heap onde a lista esta e a execucao
prossegue em modo de leitura. Qualquer outro caso diferente resulta em retrocesso.
A instrucao get variable Vn, Ai e utilizada se Vn nao estiver instanciada, ou seja,
ela e utilizada somente na primeira ocorrencia da variavel na clausula. Caso contrario, a
instrucao get value Vn, Ai e utilizada.
As instrucoes put sao responsaveis por armazenar os argumentos dos predicados nos
registradores A. As instrucoes deste grupo sao:
put variable Yn, Ai cria uma referencia para uma variavel permanente nao instanciada,
representada por Yn, armazenando-a em Ai e Yn;
1Na descricao de todas as instrucoes, Ai representa o registrador de argumento i, C e F representamrespectivamente, uma constante e um functor e Vn representa tanto uma variavel temporaria (Xn) comouma variavel permanente (Yn).
59
put variable Xn, Ai cria uma variavel nao instanciada na heap, armazenando-a em Ai
e Xn;
put value Vn, Ai atribui o valor de Vn no registrador Ai;
put unsafe value Yn, Ai atribui o valor de Yn no registrador Ai e globaliza Yn;
put const C, Ai atribui a constante C no registrador Ai;
put nil Ai atribui a constante [] no registrador Ai;
put structure F, Ai inicializa uma estrutura movendo o functor F para uma nova
posicao na heap e atualiza Ai com o ponteiro da posicao criada. A execucao prossegue
em modo de escrita;
put list Ai atribui no registrador Ai o ponteiro da lista correspondente ao topo da heap
e prossegue a execucao em modo de escrita.
A instrucao put unsafe value e utilizada no lugar de put value para tratar variaveis
inseguras que aparecem no ultimo objetivo da clausula. Uma variavel insegura e uma
variavel permanente cuja primeira ocorrencia nao acontece em estruturas ou na cabeca
da clausula, isto e, e uma variavel outrora inicializada pela instrucao put variable.
A falta de um tratamento especial para tais variaveis permitem que estas mantenham
referencias para uma celula dentro de um ambiente mesmo apos a sua exclusao pela
instrucao deallocate, produzindo uma cadeia de referencias que podem levar a um
valor indeterminado para um registrador A. Desta forma, put unsafe value garante que
a variavel seja desreferenciada para algo externo ao ambiente atual, instanciando uma
variavel na heap para ela.
As instrucoes unify sao precedidas por uma instrucao get ou put que leem ou escrevem
estruturas ou listas. Isso significa que tais instrucoes computam conforme o modo de
execucao atual: modo de leitura ou modo de escrita. Basicamente, em modo de leitura,
as instrucoes unify realizam a unificacao de argumentos sucessivos de uma estrutura
existente, enderecada pelo registrador S enquanto que em modo de escrita elas constroem
os argumentos sucessivos de uma nova estrutura, enderecada pelo registrador H. As
instrucoes sao:
unify void N salta N argumentos a partir de S quando esta em modo de leitura. Em
modo de escrita, esta instrucao insere N novas variaveis (indefinidas) na heap;
60
unify variable Vn armazena o argumento apontado por S em Vn quando esta em modo
de leitura. Em modo de escrita, a instrucao insere uma nova variavel (indefinida)
na heap, e armazena sua referencia em Vn;
unify value Vn unifica o argumento apontado por S com o valor contido em Vn quando
esta em modo de leitura. Alem disso, se Vn for um temporario, o resultado da
unificacao e armazenado nela. Em modo de escrita, a instrucao armazena o valor
da variavel Vn na heap;
unify local value Vn processa da mesma forma que unify value Vn, exceto que no
modo de escrita, o valor de Vn e desreferenciado e e somente inserido na heap se o
resultado nao for uma referencia para uma variavel na pilha local. Caso o resultado
for uma referencia para uma variavel na pilha local, uma nova variavel (indefinida)
e inserida na heap e a variavel na pilha local e ajustada para apontar para esta nova
variavel. Caso Vn for uma variavel temporaria, Vn tambem e ajustado para apontar
para esta nova variavel.
unify constant C operando em modo de leitura desreferencia o argumento apontado por
S e, sendo esta uma variavel, armazena nela a constante C. Se nao for uma variavel,
o valor e comparado com C e, sendo diferente, o retrocesso ocorre. Em modo de
escrita, a constante C e simplesmente inserida na heap.
A instrucao unify void N representa uma sequencia de variaveis de ocorrencia unica,
ou seja, nenhuma variavel e necessaria para processar esta instrucao.
As instrucoes de procedimento realizam a transferencia de controle e alocacao do
ambiente associado com o predicado a ser chamado. As instrucoes que compoem este
grupo sao:
allocate aloca um espaco para um novo ambiente na pilha local;
deallocate : restaura o estado mais recente e retira o ultimo ambiente da pilha local.
call Pred, N : finaliza um objetivo do corpo da clausula e faz o program counter P
apontar para o predicado Pred com aridade N;
execute Pred : finaliza o ultimo objetivo do corpo da clausula e faz o program counter
P apontar para o predicado Pred;
proceed : finaliza a execucao de uma clausula, fazendo o program counter P receber o
valor de CP.
61
Por fim, as instrucoes de indexacao interligam clausulas diferentes apos uma instrucao
de procedimento, a fim de selecionar a clausula correta a executar dentro de um predicado.
A indexacao e baseada em uma chave que e o functor principal do primeiro argumento
do procedimento (armazenado em A1). As instrucoes deste grupo estao representadas a
seguir. Aqui, L, Lv, Lc, Ll e Ls sao enderecos de clausulas (ou conjuntos de clausulas), e
Table e uma tabela hash de tamanho N.
try me else L cria um novo ponto de escolha armazenando na pilha local o endereco L
da proxima clausula, os valores dos registradores E, CP, B, TR e H e os argumentos
do predicado. Apos isso, HB e B sao ajustados com o ponteiro atual da heap e com
o topo da pilha local, respectivamente;
retry me else L atualiza o ponto de escolha atual com o endereco da proxima clausula
contido em L;
trust me else fail descarta o ponto de escolha atual e recupera o estado mais recente
de B e HB;
try L cria um novo ponto de escolha armazenando na pilha local o endereco da proxima
instrucao, os valores dos registradores E, CP, B, TR e H e os argumentos do predicado.
Apos isso, HB e B sao ajustados com o ponteiro atual da heap e com o topo da
pilha local. Por fim, o program counter P e ajustado com o endereco L da proxima
clausula;
retry L atualiza o ponto de escolha atual com o endereco da proxima instrucao e ajusta
P com o endereco L da proxima clausula;
trust L descarta o ponto de escolha atual e recupera o estado mais recente de B e HB.
Por fim, o program counter P e ajustado com o endereco L da proxima clausula;
switch on term Lv, Lc, Ll, Ls desreferencia o valor contido em A1, e ajusta P com
um dos enderecos Lv, Lc, Ll ou Ls dependendo se o resultado da desreferenciacao
for uma variavel, constante, lista nao vazia ou estrutura, respectivamente.
switch on constant N, Table utiliza a constante armazenada A1 como chave para en-
contrar o endereco que sera armazenado no program counter P. Essa busca e feita
na tabela hash Table, cujo tamanho e N. Ocorrera retrocesso se a busca falhar;
switch on structure N, Table processa da mesma forma que switch on constant,
porem a chave utilizada e o functor principal armazenado em A1.
62
3.3 Melhorando o Desempenho da WAM
A WAM foi um grande passo para a execucao eficiente de Prolog. A partir do ponto
de vista de provas de teoremas, Prolog e extremamente rapido. Mas existe ainda uma
grande lacuna entre a eficiencia de WAM e implementacoes de linguagens imperativas
(Van Roy, 1994). A seguir, sao apresentadas algumas tecnicas ja propostas para melhorar
ainda mais a execucao de programas Prolog em sistemas baseados na WAM.
3.3.1 Especializar a Unificacao
O esquema de compilacao de termos compostos na WAM foi projetado para ser completo
e compacto, sem levar em conta questoes de eficiencia. Na verdade, as instrucoes de
unificacao padrao da WAM geram operacoes redundantes, principalmente porque a visita
dos termos em estruturas compostas e feito em largura (Meier, 1990). Alem disso, o modo
de escrita nao e propagado para as subestruturas. De fato, sempre que uma estrutura e
unificada em modo de escrita, e claro que todas as suas subestruturas tambem devem
ser unificadas nesse modo. Nao obstante, a WAM necessita utilizar varias variaveis
temporarias no decorrer deste processo.
A alta granularidade das instrucoes WAM provem de tais caracterısticas, visto que na
unificacao dos subtermos, as instrucoes devem testar o modo a cada momento, evitando a
aplicacao de otimizacoes para um determinado contexto do programa. O trabalho de Turk
(1986) descreve algumas otimizacoes para a WAM, inclusive um metodo para reducao do
overhead de manter o registrador de modo. Marien (1988) apresenta um metodo para
compilar unificacao no modo de escrita que usa um numero mınimo de operacoes. Uma
linguagem intermediaria para auxiliar nas operacoes de unificacao da WAM foi introduzida
por Van Roy (1989), no custo de aumentar o tamanho do codigo. A eliminacao das
redundancias nas operacoes de unificacao so foram retiradas com o metodo proposto por
Meier (1990), onde os termos das estruturas eram visitados em profundidade. O metodo
de Meier possibilitava ainda propagar o modo de escrita, alem de utilizar menos variaveis
temporarias que a WAM.
Dentre as implementacoes Prolog atuais, YAP (Costa et al., 2012) implementa o
metodo proposto por Meier. SICStus Prolog (Carlsson e Mildner, 2012; Nassen, 2001)
emprega uma forma menos geral, onde somente os termos de listas sao visitados em
profundidade. Os demais sistemas nao fornecem informacoes quanto a especializacao das
instrucoes de unificacao.
63
3.3.2 Otimizar Selecao de Clausulas
Como apresentado na Secao 3.2.2, a WAM possui instrucoes que escolhem clausulas
dependendo da constante ou do functor principal do primeiro argumento. Porem, se
todas as clausulas dentro de um predicado contem functors (ou constantes) diferentes,
entao uma tabela hash precisa ser construıda, a fim de possibilitar uma busca eficiente
para a clausula correta, alem de evitar a criacao de um ponto de escolha excedente (que
seria inevitavel sem esta estrutura). Isso significa que, no caso geral, os predicados podem
ser compilados para criar no maximo um ponto de escolha entre o ponto de entrada e a
execucao da primeira clausula (Carlsson, 1987; Van Roy, 1984).
Esta caracterıstica facilita a implementacao da selecao de clausulas em sistemas
baseados na WAM, mas muitos programas podem nao se beneficiar desta estrategia,
como por exemplo, em programas cuja selecao depende de mais de um argumento. Nesse
sentido, um algoritmo ideal de selecao de clausula deveria gerar codigo com as seguintes
propriedades (Van Roy, 1994):
• Ele testa somente as clausulas que podem ser invocadas, baseado nos tipo dos
argumentos;
• Ele evita todas as criacoes inuteis de pontos de escolha;
• Seu tamanho e linear com o tamanho do programa;
• Ele cria pontos de escolha de forma incremental, isto e, os pontos de escolha contem
somente partes do estado de execucao que necessitam ser salvos;
• A degradacao do desempenho e gradual conforme a insuficiencia da informacao sobre
os tipos.
Infelizmente, nao existe um metodo publicado que satisfaca todas essas condicoes.
O que existe sao algoritmos que satisfazem algumas delas e realizam melhor selecao de
clausulas que a WAM. Van Roy et al. (1987) apresentaram um algoritmo que gera uma
arvore de selecao ingenua e cria pontos de escolha de forma incremental. Uma forma mais
restrita desta tecnica, chamada shallow backtracking, foi implementada por Nassen nas
primeiras versoes do sistema SICStus Prolog (Nassen, 2001). Para isso, o conjunto de
instrucoes foi extendido com uma instrucao especial chamada neck. A versao atual de
SICStus implementa uma versao ainda mais simples desta tecnica (Carlsson e Mildner,
2012).
64
Hickey e Mudambi (1989) apresentaram um algoritmo para gerar uma arvore de testes
de modo a minimizar o overhead do retrocesso. No pior caso, o tamanho da arvore tem
uma proporcao quadratica ao tamanho do codigo. Alem disso, seu trabalho cria pontos de
escolha de forma incremental, possibilitando que o coletor de lixo recupere mais memoria.
Um ano mais tarde, Kliger e Shapiro (1990) utilizaram grafo acıclico direcionado para
atingir o mesmo proposito e garantiram que, independente de qualquer programa, a selecao
de clausulas sempre seria melhor que a selecao de clausulas original da WAM.
O sistema Aquarius (Van Roy, 1990; Van Roy e Despain, 1992) produz um grafo de
selecao para disjuncoes contendo testes para unificacoes, tipo e comparacoes aritmeticas.
Duas transformacoes sao utilizas para possibilitar os testes: (1) type enrichment, que
adiciona informacoes de tipo para um predicado que carece dela e (2) factoring que permite
o sistema tomar vantagem de testes em variaveis realizando a unificacao de um termo para
todas as ocorrencias dele. Parma (Taylor, 1996) e outro sistema que implementa uma
estrategia semelhante.
Outras implementacoes atuais de Prolog, tais como SWI-Prolog (Wielemaker,
2003; Wielemaker et al., 2012), XSB (Sagonas et al., 1993; Swift e Warren, 2012)
realizam a selecao de clausulas baseada em multiplos argumentos, mas requerem um
esforco adicional por parte do programador para possibilitar este processo. Este esforco
nao e necessario em ilProlog (Troncon et al., 2007), que emprega uma heurıstica
em tempo de compilacao para realizar a mesma tarefa. Infelizmente, este tipo de
selecao sofre o risco de indexar argumentos de saıda, cujo efeito e aumentar o tempo
de compilacao de forma desnecessaria. De fato, analise global poderia evitar este risco,
prevendo os modos de execucao possıveis em tempo de compilacao. Contudo, nenhum
destes sistemas implementam analise global. Uma forma sofisticada de contornar este
problema foi apresentada por Costa et al. (2007), com a indexacao por demanda,
que permite a indexacao de predicados estaticos e dinamicos baseados somente nos
argumentos de entrada, sem necessitar de analise global ou qualquer intervencao por
parte do programador. Atualmente este metodo e o principal esquema de selecao de
clausulas do sistema YAP (Costa et al., 2012).
3.3.3 Gerar Codigo Nativo
Uma base teorica para acelerar a execucao de programas Prolog e representar o codigo
em simples instrucoes. Isso diminui a granularidade e da oportunidades para a aplicacao
de diversas otimizacoes de codigo. Os primeiros experimentos publicados utilizando esta
ideia foram realizados por Tamura (1986) e Komatsu et al. (1987), que demonstraram
65
que a chave para o alto desempenho de Prolog nao esta associado necessariamente a um
hardware especializado.
Por volta de 1988, Taylor e Van Roy trabalharam nos sistemas Parma (Taylor, 1996)
e Aquarius (Van Roy, 1990; Van Roy e Despain, 1992) que compilam codigo Prolog
para nativo, utilizando analise global para fornecer informacoes para as otimizacoes. Ao
contrario de Komatsu et al. e Tamura, Taylor e Van Roy ignoraram o uso da WAM, pois
estavam confiantes que a baixa granularidade do conjunto de instrucoes permitiria que
todas as otimizacoes fossem expressadas. Parma compilava codigo Prolog utilizando
uma representacao intermediaria de tres enderecos e Aquarius se baseava na Berkeley
Abstract Machine (BAM), que continha mais instrucoes que aWAM (porem mais simples),
inclusive algumas especializadas em tempo de execucao. O resultado nao foi diferente,
apesar da dificuldade de manutencao destes sistemas, Aquarius e Parma superaram
outras implementacoes existentes.
Essa forma de execucao, no entanto, nao ficou restrita somente a esses sistemas.
Atualmente, uma boa parte das implementacoes Prolog geram codigo nativo, tais como
GNU-Prolog (Diaz et al., 2012), SICStus Prolog (Carlsson e Mildner, 2012; Nassen,
2001), CIAO (Hermenegildo et al., 2012) BinProlog (Tarau, 1991) e XSB (Sagonas et
al., 1993; Swift e Warren, 2012).
Uma variacao dessa estrategia e a geracao de codigo nativo em tempo de execucao,
com o uso de compiladores just-in-time. O unico compilador JIT para YAP existente
e o YAPc, desenvolvido para o sistema YAP por Silva e Costa (2007). Com este
compilador, as clausulas mais frequentes de um programa sao especializadas em tempo
de execucao. Resultados mostraram que alguns programas obtiveram uma reducao do
tempo de execucao entre 2 a 7 vezes, sob uma execucao que somente interpreta (Silva e
Costa, 2007). YAPc foi utilizado com um prova de conceito para execucao eficiente de
Prolog e nao chegou a integrar a arquitetura do YAP.
3.3.4 Utilizar Analise Global
Tradicionalmente, analise global para programas logicos e usado para derivar informacoes,
como de tipo e modo, com o objetivo de aumentar a velocidade de execucao e/ou reduzir
o tamanho do codigo. Os algoritmos de analise global estudados ate o momento sao
todas instancias de um metodo geral chamado de interpretacao abstrata, cuja ideia geral
e executar o programa sobre um domınio simples, isto e, um domınio conservativo, a fim
de satisfazer um pequeno conjunto de restricoes (Mellish, 1981). No termino da execucao,
os resultados encontrados fornecem uma aproximacao correta de informacoes sobre o
66
programa analisado. Le Charlier et al. (1990, 1993) realizaram um estudo extensivo de
algoritmos e domınios de interpretacao abstrata e sua efetividade em derivar tipos. Por
outro lado, Getzinger (1993) apresentou uma taxonomia extensa de domınios de analises
e estudou seus efeitos sobre o tempo de execucao e tamanho do codigo.
As primeiras evidencias de que analise global seria considerada util para imple-
mentacoes Prolog vieram nos trabalhos de Mellish (1981, 1985), mas as primeiras medidas
de praticidade foram publicadas em 1988 por Warren et al. (1988), ao avaliar dois sistemas
de analise global: MA3 e Ms. O trabalho concluiu que ambos os analisadores sao efetivos
em derivar tipos e nao aumentam o tempo de compilacao de forma consideravel. Van
Roy e Taylor tambem obtiveram bons resultados nos sistemas Aquarius (Van Roy,
1990; Van Roy e Despain, 1992) e Parma (Taylor, 1996). Atualmente, poucos sistemas
nao-obsoletos empregam analise global, a exemplo, o sistema CIAO (Hermenegildo et al.,
2012).
3.3.5 Utilizar Memoizacao
Memoizacao (do ingles memoization) e uma tecnica para manter em cache a solucao
de predicados ja processados. A adicao desta tecnica no mecanismo de resolucao de
Prolog permite obter um novo modelo de execucao que realiza execucao de baixo para
cima e execucao de cima para baixo. Para certos algoritmos, como algoritmos de
programacao dinamica, esse novo modelo permite executar definicoes logicas com uma
baixa complexidade, se comparado ao mecanismo tradicional da WAM.
Uma implementacao de memoizacao e a resolucao OLDT (Ordered Linear Resolution
of Definite Clauses with Tabulation) (Swift e Warren, 1992) e uma generalizacao dela e
a resolucao SLG (Linear resolution with Selection function for General logic programs)
(Chen e Warren, 1993), que lida tambem com negacao e foi implementada primeiramente
no sistemaXSB (Sagonas et al., 1993; Swift e Warren, 2012). Essa implementacao executa
codigo Prolog com menos de 10% de overhead relativo a WAM e e muito mais rapido
que sistemas de banco de dados dedutivos (Swift e Warren, 1993). Memoizacao tambem
e empregada pelo sistema YAP (Rocha et al., 2001) e esta em fase de implementacao no
sistema GNU-Prolog. (Diaz et al., 2012)
3.4 Implementacoes Prolog
Geralmente as implementacoes da WAM por software sao uma ordem de magnitude mais
lenta do que linguagens imperativas. Por um lado, o uso de interpretadores possui o
67
atrativo de tornar o sistema portavel e interativo. Por outro lado, compiladores conseguem
gerar codigo mais eficiente. As implementacoes citadas neste trabalho sao descritas a
seguir e uma boa parte delas suportam ambos os modos de execucao.
Aquarius foi desenvolvido por Peter Van Roy, sendo o primeiro sistema a compilar
codigo nativo sem um estagio intermediario baseado na WAM (Van Roy, 1990;
Van Roy e Despain, 1992). Aquarius adotava o modelo de execucao BAM
(Berkeley Abstract Machine) que, ao contrario da WAM, empregava instrucoes mais
simples, o que permitia aplicar extensas otimizacoes. Outras caracterısticas deste
compilador incluem: (1) exploracao do determinismo, que permitia simplificar o
retrocesso por saltos condicionais; (2) especializacao de unificacoes, simplificando
a unificacoes para simples atribuicoes, quando possıvel e (3) analise de fluxo de
dados, que fornecia informacoes necessarias para a exploracao do determinismo e
especializacao da unificacao. Aquarius mostrou que a execucao de programas
Prolog pode competir com programas gerados por um compilador otimizador C
para uma classe de programas nao-triviais (Van Roy e Despain, 1992). Contudo,
em vista das tecnicas empregadas, somado ao cuidado sobre algumas instrucoes da
BAM (que necessitavam ser especializadas em tempo de execucao), Aquarius se
tornou um sistema muito complexo para ser mantido. Em consequencia, Aquarius
nao teve continuidade, sendo abandonado em 1993.
BinProlog foi desenvolvido por Tarau, sendo um sistema Prolog capaz de interpretar
codigo, gerar codigo C/C++ e ainda gerar codigo executavel (Tarau, 1991). Bin-
Prolog prove um alto nıvel de abstracao para o desenvolvimento de aplicacoes
distribuıdas e para Internet, alem de uma interface para as linguagens C (Ritchie,
1993), C++ (Stroustrup, 2011) e Java (Arnold et al., 2005), alem de Tcl/Tk (Flynt,
2012).
CIAO e um ambiente de programacao multiparadigma desenvolvido na Universidade
Politecnica de Madri (Hermenegildo et al., 2012). Este sistema foi inicialmente
projetado para transformar codigo Prolog para C (Ritchie, 1993), deixando para o
compilador desta linguagem gerar o codigo executavel para a arquitetura subjacente.
Nas versoes atuais, CIAO fornece um ambiente completo de desenvolvimento
incluindo um interpretador, um compilador, uma IDE, um analisador estatico de
codigo, alem de um pre-processador potente capaz de gerar diversas otimizacoes,
inclusive paralelizacao automatica. O sistema oferece ainda uma linguagem de
declaracao, que possibilita a insercao estatica de informacoes de tipo e modo,
68
fornecidas pelo proprio usuario. Estas informacoes sao utilizadas para realizar
diversas otimizacoes de alto nıvel, incluindo especializacao abstrata multipla (Puebla
e Hermenegildo, 1995), avaliacao parcial (Puebla et al., 2006) e reducao de con-
correncia (Puebla et al., 1997). Na ausencia de tais informacoes, o proprio sistema
tenta inferi-las, utilizando os metodos propostos em (Muthukumar e Hermenegildo,
1992), (Saglam e Gallagher, 1995) e (Vaucheret e Bueno, 2002).
GNU-Prolog e um sistema Prolog desenvolvido por Diaz et al. (2012), que teve inıcio
em 1996 com o nome de Calypso e que foi posteriormente lancado como um
produto GNU com o nome de GNU Prolog. O sistema fornece dois modos de
execucao a saber: interpretado e nativo. Embora o compilador gere codigo nativo
de alta qualidade, um trabalho publicado recentemente mostrou que o interpretador
nao tem um bom desempenho frente a outros sistemas interpretados, como CIAO,
SICStus e YAP (Martins e Silva, 2011). GNU-Prolog esta em desenvolvimento
para prover suporte a um coletor de lixo e a memoizacao. Metas de longo prazo
estipulam a criacao de um compilador mais sofisticado, alem da integracao com
LLVM (Lattner e Adve, 2004).
Parma e um compilador Prolog experimental para arquitetura MIPS, desenvolvido na
Universidade New South Wales em Sidney, Australia (Taylor, 1996). O componente
mais importante no desempenho do Parma e a fase de analise global, que examina o
programa como um todo para reunir informacoes que sao utilizadas durante a com-
pilacao do programa. Informacoes sobre as caracterısticas operacionais do programa,
como cadeias de desreferenciamento, sao utilizadas para remover muitas operacoes
inerentes da linguagem, que sequer eram tratadas em outras implementacoes. Em
Parma, as instrucoes sao compiladas utilizando uma representacao intermediaria
de tres enderecos, embora o modelo de memoria seja ainda similar ao da WAM.
Alem disso, Parma emprega uma forma de armazenamento de modo a reduzir o
custo do retrocesso. Infelizmente, assim como Aquarius, Parma foi abandonado
devido a sua difıcil manutencao.
SICStus Prolog foi desenvolvido pelo Swedish Institute of Computer Science (SICS).
A implementacao deste sistema foi baseada na especificacao da WAM, porem
implementa alguns recursos adicionais como o corte, predicados aritmeticos, testes
de tipo, alem de ter suporte para coleta de lixo, corrotinas e manipulacao de
interrupcoes (Carlsson e Mildner, 2012; Nassen, 2001). SICStus Prolog ainda
implementa instrucoes de teste aritmeticos sofisticados, baseado numa variacao mais
69
simples do metodo shallow backtracking (Carlsson, 1989). Indexacao de clausulas
e retrocesso sao tratados dentro das instrucoes call e execute, ao contrario da
especificacao original da WAM, em que havia instrucoes especıficas para essas
tarefas. SICStus pode executar codigo Prolog em tres modos de execucao
distintos: (1) interpretado, onde as clausulas sao representadas como termos, a
fim de acelerar a atualizacao na base de dados, alem de dar suporte para depuracao
de codigo; (2) nativo, para execucao rapida e (3) emulado, que simplifica a carga do
sistema e o gerenciamento de memoria.
SWI-Prolog foi desenvolvido por Wielemaker, com o proposito de ser um sistema simples
e compacto (Wielemaker, 2003; Wielemaker et al., 2012). SWI-Prolog possui
compatibilidade com outros sistemas comoCIAO eGNU-Prolog. Com o objetivo
de ser um ambiente de desenvolvimento e academico, possui um framework de
desenvolvimento completo, incluindo uma IDE, um analisador e uma interface para
diferentes linguagens de programacao, como C (Ritchie, 1993), C++ (Stroustrup,
2011) e Java (Arnold et al., 2005). Uma caracterıstica interessante e o fato deste
sistema possuir um modulo para servidores web multithreaded, capaz de gerar codigo
HTML (Silva, 2011), autorizacao HTTP (Shiflett, 2003), alem de gerenciar sessoes.
XSB e um sistema Prolog que foi desenvolvido por diversas instituicoes, com o intuito de
prover uma abordagem alternativa para criar sistemas de banco de dados dedutivos
(Sagonas et al., 1993; Swift e Warren, 2012). XSB emprega memoizacao para evitar
avaliacoes nao-finitas e redundancia na resolucao dos predicados, que sao inerentes
ao algoritmo de inferencia presente nas implementacoes-padrao de Prolog. Isso e
importante no contexto do sistema, visto que o reuso de informacoes ja computadas
e uma boa abordagem para sistemas que manipulam grande quantidade de dados. A
execucao de programas neste sistema pode ser feito da forma interpretada e nativa.
A execucao interpretada e lenta, mas predicados compilados sao considerados
estaticos por XSB no momento da compilacao, o que pode ser indesejavel ao
manipular muitos dados. Como uma alternativa para compilacao, XSB permite
predicados dinamicos, cujo codigo pode ser modificado durante a execucao.
Uma comparacao do desempenho de alguns destes sistemas foi descrita no trabalho
de Martins e Silva (Martins e Silva, 2011).
70
3.5 Consideracoes Finais
A principal motivacao para o uso de programacao em logica e permitir que os progra-
madores descrevam o que eles querem separadamente de como alcancar este objetivo.
Isto e baseado na premissa de que qualquer algoritmo consiste de duas partes: uma
especificacao logica, a “logica”, e uma descricao de como executar esta especificacao,
o “controle”. Programas logicos sao declaracoes descrevendo propriedades do resultado
esperado, com o controle para entender o sistema. A maior parte deste controle pode ser
automaticamente provida pelo sistema, o que o mantem claramente separado da logica.
Prolog (Casanova et al., 1987; Sterling e Shapiro, 1994) e uma linguagem que foi
originalmente criada para resolver problemas em linguagem natural. A semantica de
Prolog ataca um balanceamento entre eficiente implementacao e completude logica
(Meier, 1990; Warren, 1983). Isto atenta para descrever a programacao como um
subconjunto de logica de primeira ordem, que nao e apenas um provador simples de
teorema, mas tambem uma linguagem de programacao usual devido a simplicidade e a
implementacao eficiente dos conceitos de unificacao e busca.
A primeira implementacao de Prolog foi um interpretador desenvolvido por Roussel
e Colmerauer na decada de 70 (Colmerauer e Roussel, 1996). Em 1977, David Warren
criou o primeiro compilador de Prolog, o DEC-10 Prolog (Warren, 1977), que gerava
codigo assembly para o DEC-10. A investigacao na implementacao de Prolog continuou
com a Maquina Abstrata de David Warren (a WAM), uma linguagem intermediaria para
a compilacao de Prolog. A WAM oferecia varias vantagens, tais como facil compilacao,
portabilidade e codigo compacto. Por essas razoes, a WAM se revelou uma forma eficiente
e elegante de permitir a execucao de programas Prolog numa maquina sequencial e desta
forma se tornou rapidamente como modelo para implementacoes de Prolog.
A motivacao original no projeto de maquinas abstratas era a construcao de hardware
para suportar Prolog eficientemente. Porem as instrucoes WAM realizam operacoes muito
complexas, como a unificacao, enquanto o desenvolvimento das novas arquiteturas seguiu
uma direcao oposta, possuir um pequeno conjunto de instrucoes simples. As dificuldades
em obter bom desempenho em arquiteturas tradicionais e o custo de desenvolver hard-
ware para suportar Prolog justificam que a WAM tenha sido principalmente usada em
implementacoes por software. Estas implementacoes tradicionalmente compilam o codigo
Prolog para o codigo de uma maquina abstrata que depois e interpretado, como realizado
em: YAP (Costa et al., 2012), SICStus Prolog (Carlsson e Mildner, 2012; Nassen,
2001), SWI-Prolog (Wielemaker, 2003; Wielemaker et al., 2012), GNU-Prolog (Diaz
71
et al., 2012), BinProlog (Tarau, 1991) e XSB (Sagonas et al., 1993; Swift e Warren,
2012).
Apos o advento dos interpretadores para Prolog, os sistemas Aquarius (Van Roy,
1990) e Parma (Taylor, 1996) provaram que em alguns casos, as linguagens logicas podem
ter desempenho comparavel ao das linguagens imperativas. O bom desempenho destes
sistemas e devido basicamente a dois fatores: a geracao de codigo nativo e o uso de
analise global. Contudo tais sistemas foram descontinuados devido a alta complexidade
de manutencao.
A geracao de codigo nativo para Prolog e um problema complexo. Usualmente o pro-
blema e solucionado organizando a computacao em um conjunto de fases especializadas,
como ocorre no sistema SICStus Prolog que transforma o codigo Prolog em codigo
WAM, para depois transformar em uma nova linguagem intermediaria (Haygood, 1994).
Uma alternativa a esta abordagem e a geracao de codigo C a partir de Prolog (Morales
et al., 2003). A filosofia desta abordagem e deixar o trabalho de geracao de codigo, como
tambem da otimizacao, para o compilador C. Outra alternativa e a geracao de codigo
nativo em tempo de execucao, como ocorre no sistema YAP com uso de compilacao JIT
(Silva e Costa, 2007).
Cada abordagem possui alguns problemas inerentes. Primeiro, a complexidade de
alguns sistemas os tornam difıceis de serem mantidos. Consequentemente, sistemas como
Aquarius e Parma foram abandonados, nao existindo mais uma atualizacao para eles.
Segundo, o uso de analise global nao consegue obter uma aceleracao maior que tres. Alem
disso, esta tecnica nao funciona para qualquer tipo de programa. Terceiro, o overhead dos
interpretadores, como por exemplo o YAP, e baixo, desta forma os sistemas que geram
codigo nativo nao conseguem um desempenho consideravelmente melhor.
Utilizando uma abordagem totalmente diferente, Mercury (Conway et al., 1995;
Henderson e Somogyi, 2002) mudou a linguagem. O objetivo foi melhorar o desempenho
de programas logicos. Contudo, surgiu um novo problema: Mercury nao e Prolog.
Consequentemente, nem todos programas Prolog sao executados por Mercury.
Nos ultimos anos ocorreram melhorias significativas na compilacao de Prolog, re-
sultando no desenvolvimento de sistemas eficientes, como SWI-Prolog e YAP. Tais
melhorias foram decorrentes das tecnicas propostas para melhorar o desempenho do
modelo padrao de execucao Prolog: a Maquina Abstrata de Warren.
Por fim, com relacao ao YAP e possıvel que este alcance um desempenho ainda maior
visto que, com a nova arquitetura, YAP tambem sera capaz de gerar codigo nativo. A
visao de alto nıvel e a arquitetura do YAP, bem como a arquitetura do sistema proposto,
que permitira essa tarefa, serao apresentadas no Capıtulo 4. Porem antes de prosseguir
72
para a proposta deste trabalho, a proxima secao apresenta os trabalhos relacionados em
respeito a compilacao JIT e, mais especificamente, a compilacao JIT baseada em tracos
de execucao.
73
4
O Ambiente Experimental de
Compilacao Just-in-Time Baseada em
Tracos de Execucao
A implementacao eficiente de linguagens tipicamente interpretadas atualmente apostam
no uso de um sistema de compilacao JIT para compilar e otimizar codigo em tempo de
execucao. Neste contexto, uma contribuicao esperada com este trabalho e a elaboracao
de um ambiente de execucao mista de codigo para o sistema YAP, que seja capaz de
alcancar melhor desempenho na execucao por meio dessa abordagem.
Desta forma, o objetivo deste capıtulo e descrever a nova arquitetura do YAP, um
sistema capaz de gerar codigo nativo em tempo de execucao para os tracos (ou caminhos)
de execucao mais frequentes. Adicionalmente, este capıtulo apresenta a nova interface do
ambiente, que e capaz de proporcionar aos usuarios a facilidade de realizar experimentos
diversos no contexto de compilacao JIT para Prolog.
Este capıtulo esta organizado como se segue. A Secao 4.1 apresenta a visao de alto nıvel
do YAP, bem como a sua arquitetura interna. Em seguida, na Secao 4.2 e apresentada a
nova arquitetura do YAP, proposto neste trabalho, e a relacao entre os seus componentes.
As principais alteracoes realizadas na arquitetura atual para que esta pudesse ser integrada
a arquitetura proposta tambem sao mencionadas nessa secao.
A Secao 4.4 descreve esses novos predicados do YAP, cujo objetivo e torna-lo um
ambiente experimental. Por fim, este capıtulo e finalizado com algumas consideracoes na
Secao 4.5.
74
4.1 Yet Another Prolog
Yet Another Prolog (YAP) (Costa et al., 2012) e uma implementacao de Prolog desenvol-
vida na Universidade do Porto desde 1985. Este sistema suporta or -paralelismo (Gupta,
1994) e threads, sendo o primeiro trabalho a conter estas caracterısticas juntamente com
memoizacao (Rocha et al., 2001). YAP utiliza um algoritmo de indexacao dinamica, que
e capaz de indexar multiplos argumentos e termos compostos, alem de tornar irrelevante
a forma de como os argumentos de uma clausula estao dispostos (que, no contexto de
sistemas Prolog em geral, e uma causa do aumento do tempo de execucao para alguns
programas). O sistema de indexacao dinamica, denominada JITI (Costa, 2009; Costa et
al., 2007), em conjunto com memoizacao tornam YAP uma boa alternativa para criacao
de sistemas de banco de dados dedutivos, assim como XSB Prolog (Sagonas et al.,
1993; Swift e Warren, 2012).
4.1.1 Estruturas de Dados e Organizacao da Memoria
Assim como na WAM tradicional, YAP mantem o estado interno de execucao em seis
regioes na memoria: a pilha global, pilha local, trilha, pilha auxiliar, a area de codigo e
uma regiao de memoria para o coletor de lixo. As pilhas global e local tambem funcionam
de forma semelhante a WAM, armazenando objetos, pontos de escolha e ambientes.
Basicamente, YAP define seis tipos concretos para os objetos:
1. Pequenos inteiros;
2. Atomos, que sao constantes nao numericas;
3. Aplicacoes, que em um contexto generico, sao as estruturas compostas;
4. Pares, que sao compostos por uma cabeca e uma cauda e sao geralmente usadas
para definir listas;
5. Referencias, ou variaveis, que sao ponteiros para outros objetos. Assim como na
WAM, YAP desreferencia uma variavel para encontrar o seu valor. Variaveis nao
instanciadas sao referencias para elas mesmas ou para NULL; e
6. Extensoes, que sao utilizadas para definir grandes inteiros e ponto flutuante, alem
de estruturas de dados mais complexas, como strings e arrays multidimensionais.
No que se refere ao estado de execucao interno, YAP define diversos registradores. Os
principais sao:
75
• P: ponteiro para a localizacao do programa (program counter).
• CP: endereco de retorno.
• H: topo da pilha global.
• HB: valor de H no ponto de escolha mais recente.
• B: apontador para o ultimo ponto de escolha.
• BB: topo da pilha local no ultimo ponto de escolha.
• ASP: topo da pilha local.
• TR: topo da trilha.
• AuxTop: topo da pilha auxiliar.
• ENV e YENV: apontadores para o ambiente atual, onde um pode ser diferente do
outro.
• S: apontador para os termos compostos, a fim de possibilitar a unificacao.
• A1, ..., AN : argumentos da clausula em execucao, onde N determina a quantidade
de argumentos.
YAP nao utiliza um registrador especial para armazenar o modo de execucao atual.
Ao contrario da WAM, sua forma de unificar termos compostos dispensa o uso de tal
registrador.
4.1.2 Principais Diferencas com a WAM
YAP compila cada clausula dos programas Prolog para uma representacao baseada na
WAM, chamada YAAM (Costa, 1999; Lopes, 1996), cuja principal diferenca esta no modo
como e realizada a unificacao de termos compostos. O algoritmo de unificacao utilizado
foi publicado em 1990 por Meier (Meier, 1990). Nele, os sub-termos sao compilados em
instrucoes de unificacao por meio de uma busca em profundidade, enquanto na WAM
isso era feito por uma busca em largura. Em adicao, a YAAM assume uma pilha de
76
unificacao, que e inicializada por uma instrucao get list1 ou get struct2 e estendida
por uma instrucao unify list3 ou unify struct4.
Alem de ser mais simples de implementar, esse algoritmo nao aplica pressao sobre
registradores e permite a heranca de modo. Em outras palavras, se o sistema comeca a
processar uma estrutura em modo de escrita, todas as suas sub-estruturas tambem serao
processadas neste modo. Essa caracterıstica motiva o esquema de duplo-opcode presente
em YAP (Costa, 1999; Lopes, 1996), em que cada instrucao unify possui duas versoes:
uma executada em modo de leitura e outra executada em modo de escrita. Dessa forma,
toda instrucao YAAM executada em modo de escrita realiza menos verificacoes, pois
obedecem as seguintes propriedades:
1. Toda instrucao unify que e executada em modo de escrita e seguida por outra
instrucao em modo de escrita se a execucao procede em um mesmo sub-termo;
2. Toda instrucao unify que e executada em modo de escrita e seguida por uma
instrucao pop5 se a execucao procede para um sub-termo pai;
3. Toda instrucao unify que e executada em modo de escrita e seguida por uma
instrucao get ou uma instrucao de controle se a execucao procede para fora do
termo.
A YAAM ainda emprega outras formas diferenciadas para tratar termos compostos,
que incluem:
• O uso de uma instrucao unify last em vez de uma instrucao unify para tra-
tar o ultimo sub-termo de um termo composto. Instrucoes last nao precisam
atualizar o registrador S, simplificando o codigo. Alem disso, unify last list
e unify last struct nao precisam atualizar a pilha de unificacao; e
• A finalizacao de um sub-termo quando uma instrucao unify last atom, unify last var
ou unify last val e executada. Em seguida, uma instrucao pop e necessaria para
1Similar a instrucao get list da WAM2Similar a instrucao get structure da WAM3No modo de leitura, unify list unifica o termo apontado pelo registrador S se o argumento nao e
variavel. Se o argumento for variavel, unify list inicia uma nova lista. No modo de escrita, unify list
escreve o termo da lista no registrador S e o insere na heap.4No modo de leitura, unify struct unifica o termo apontado pelo registrador S se o argumento nao
e variavel. Se o argumento for variavel, unify struct inicia uma nova estrutura. No modo de escrita,unify struct escreve o termo da estrutura no registrador S e o insere na heap.
5Pop recupera o registrador S e salta para uma instrucao em modo de escrita caso a pilha auxiliarutilizada para unificacao indicar que ainda ha termos para serem unificados. Nao existindo termos, osalto ocorre para uma instrucao em modo de leitura.
77
retornar um sub-termo acima e configurar o modo para escrita ou leitura e direcionar
a execucao da proxima instrucao.
Em vista disso, e possıvel perceber que a YAAM possui uma quantidade maior de
instrucoes que a WAM, embora mais simples. Isto facilita a implementacao e possibilita
a aplicacao de diversas otimizacoes de codigo.
4.1.3 Estrutura das Clausulas
YAP estrutura as clausulas como uma sequencia de instrucoes YAAM que sao executadas
da primeira ate a ultima, desde que nao ocorra alguma excecao ou operacao de corte.
Basicamente, todas as clausulas possuem as seguintes caracterısticas:
• Podem iniciar com qualquer instrucao YAAM, mas a ultima instrucao e sempre
execute, dexecute ou procceed;
• Na maioria da vezes, logo apos a execucao de uma instrucao call, fcall, execute
ou dexecute, instrucoes de indexacao sao necessarias para invocar a clausula correta.
Tendo em vista que YAP utiliza uma estrutura para representar predicados, isto
e, um conjunto de clausulas com mesmo nome e aridade, essas instrucoes sao
necessarias para determinar a clausula dentro do predicado que precisa ser invocada
apos uma chamada;
• Toda clausula e executada da primeira ate a ultima instrucao YAAM que a compoe,
desde que nao ocorra alguma excecao. Se entende por excecao todo fluxo que
direciona para a execucao de codigo que nao pertence a clausula, que incluem acoes
para backtracking6 e tratamento de overflow nas regioes de memoria do sistema.
Este conceito de excecao sera utilizado deste ponto ate o final do texto.
• Instrucoes de corte bem sucedidas nao geram excecoes, mas evitam que algumas
instrucoes dentro da mesma clausula sejam executadas.
4.1.4 Organizacao do Sistema
As subsecoes anteriores apresentaram uma visao de alto nıvel do YAP, que se resumiram,
de fato, nas principais diferencas com a WAM. Ao contrario disso, o foco desta secao
e apresentar uma visao geral de sua arquitetura interna. O interesse nesse contexto
6Um exemplo de backtrack ocorre quando uma instrucao get list recebe como argumento um termoque nao e uma lista.
78
e apresentar a forma com que seus principais componentes se relacionam para, entao,
descrever as modificacoes que foram realizadas para adapta-lo ao sistema desenvolvido.
A Figura - 4.1 apresenta a arquitetura do sistema YAP, adaptada de Costa et al. (2012).
Arquitetura YAP (atual)
BibliotecasBibliotecas
Interpretador YAP
Engine CompiladorCompilador
Áreade
código
Área deCódigoYAAM
Carregamentoinicial
Requisição decompilação
Início dainterpretação
Cláusula Prologcompilada (YAAM)
Código YAAMpara interpretar
Arquitetura YAP (atual)
Interpretador YAP
Área deCódigoYAAM
Engine
Figura 4.1: Estrutura interna da versao atual do sistema YAP (adaptado de Costa etal. (2012)).
A inicializacao do sistema ocorre em uma biblioteca de alto nıvel escrita em Prolog e
sua manutencao e feita por bibliotecas escritas em C. Ambas bibliotecas sao denominadas
bibliotecas de alto nıvel e compoem o componente de mesmo nome apresentado na Figura
- 4.1. Apos o carregamento das bibliotecas de alto nıvel, a Engine carrega as demais
bibliotecas do sistema, denominadas bibliotecas de baixo nıvel, que dao suporte a threads
e aos predicados nativos de YAP.
A Engine inicializa o Compilador, que gera as sequencias de instrucoes YAAM a partir
das clausulas escritas em Prolog. Basicamente, o compilador gera codigo YAAM em tres
fases distintas. Primeiramente, ele compila a cabeca da clausula e depois o seu corpo. Na
segunda etapa ele identifica e otimiza variaveis temporarias. Por fim, ele otimiza o codigo
gerado eliminando instrucoes superfluas, similar a uma otimizacao peephole (Muchnick,
1997). Apos essas tres etapas, o compilador cria as instrucoes de indexacao e armazenada
o codigo final na Area de Codigo YAAM para serem interpretadas pelo Interpretador YAP.
Apos a geracao de codigo, o controle segue para o Interpretador YAP, que executa as
instrucoes outrora geradas pelo Compilador.
79
4.2 A Nova Geracao do YAP
Para que o YAP suporte a execucao de codigo misto, sua arquitetura foi modificada para
conter novos componentes. A nova organizacao do sistema esta representada na Figura -
4.2. Assim como na organizacao anterior, a inicializacao do sistema ocorre nas bibliotecas
de alto nıvel, contudo, alem da Engine, novos componentes tambem sao inicializados.
Dentre estes novos componentes, estao as duas novas areas de codigo: a Area de Codigo
Nativo, responsavel por armazenar os tracos de execucao compilados e a Area de Codigo
Intermediario, responsavel por armazenar os tracos em construcao.
Em nıvel de implementacao, essas areas sao representadas como estruturas de dados
dinamicas, que crescem conforme novos tracos sao coletados e/ou compilados. Juntamente
a essas estruturas estao associadas informacoes de perfil, que registram as estatısticas de
execucao, como a quantidade de tracos coletados/executados e o tamanho ocupado por
cada area de memoria.
Gerentede código
Motor de Execução
Monitor
Compilador JIT
Fila de Compilação
Arquitetura YAPCódigoYAAM
CódigoInterm.
Códigonativo
Áreas de código
Áreas de código
Inicialização das áreas decódigo e do sistema em geral
Código compilado
Requisição decompilação e
código compilado
Versão correta decódigo para executar
Gerenciamento das áreasde código (busca e inserção)
Início da interpretação
Bibliotecas Engine Compilador
InterpretadorInstrumentado
InterpretadorYAP Construtor
de TraçosBlocosbásicos
Traços deexecução
Instrumentaçãodas cláusulas
ProfilerInformaçõesúteis para recompilação
Informaçõescoletadas
Módulo de
Recompilação
Bibliotecas Engine Compilador
Construtorde Traços
ProfilerCompilador JIT Gerentede código
Motor de Execução
Figura 4.2: Estrutura interna do sistema proposto.
O Motor de Execucao e responsavel por executar os programas em diversos modos
de execucao, que incluem somente interpretar, smart JIT, compilac~ao contınua e
somente compilar. Todo codigo nativo e armazenado na Area de Codigo Nativo apos
a geracao de codigo pelo Compilador JIT. Basicamente, todas as clausulas do programa
sao instrumentadas com contadores inteiros ou faixa de tempo pelo Monitor e enviadas
ao Compilador JIT quando se tornam quentes. Nesse contexto, o que define uma clausula
80
quente e o fato de seu codigo de instrumentacao atingir um determinado limite, conforme
a mesma definicao de regiao quente utilizada no decorrer do texto.
O gerenciamento das clausulas, sejam elas interpretadas ou nativas, e realizado pelo
Gerente de Codigo. Este envia a versao correta da clausula para execucao (priorizando o
codigo nativo), invoca o Monitor para que o mesmo atualize o parametro de frequencia
da respectiva clausula (na forma de codigo intermediario) e envia clausulas para serem
compiladas.
O Interpretador Instrumentado, o qual e acionado somente nos modos de execucao
com codigo misto, emite os blocos basicos, que simbolizam a sequencia de instrucoes
recentemente executadas naquela clausula. O Coletor de Tracos, no perıodo em que a
clausula se torna crıtica (quando o parametro de frequencia esta prestes a alcancar um
limite) ate o momento que ela se torne quente (quando o parametro alcanca o limite),
recebe os blocos basicos emitidos pelo Interpretador Instrumentado e constroi o traco de
execucao a partir deles. Apos a construcao do traco, este e armazenado na Area de Codigo
Intermediario.
Por fim, o Modulo de Recompilacao permite que tracos outrora compilados, porem
invalidados com o tempo, sejam recompilados. Este modulo utiliza um Profiler, que coleta
o ponto do codigo a partir do qual o traco precisa ser reconstruıdo e o Coletor de Tracos,
que reconstroi o traco.
Em especıfico, as principais alteracoes realizadas na versao atual para suportar a nova
arquitetura do YAP foram:
Suporte ao novo interpretador
O Interpretador Instrumentado e identico ao Interpretador YAP do sistema atual,
exceto pela capacidade de emitir blocos basicos para o Construtor de Tracos. Este
interpretador e invocado substituindo a chamada ao primeiro interpretador quando
o modo de execucao ativo for um modo de codigo misto. Nenhuma mudanca
na estrutura de codigo do sistema atual, exceto na chamada ao interpretador,
foi necessaria, pois todos os predicados nativos7 sao executados pelo Interpretador
YAP. Portanto, por exemplo, se um predicado especıfico para mudanca do modo
de execucao (um dos predicados desenvolvidos e apresentado na Secao 4.4.4) for
invocado no inıcio do programa, o sistema apenas realiza um teste do modo de
execucao ativo para entao, invocar o interpretador correto;
7Os predicados nativos se assemelham as funcoes pre-existentes nas linguagens comumente usadas.Estes incluem tanto os predicados da versao atual do sistema, como os predicados desenvolvidos nestetrabalho.
81
Suporte ao Compilador JIT e ao novo Gerente de Codigo
Todo o acesso as novas areas de codigo por parte do Gerente de Codigo e invocacao
ao Compilador JIT e feita por meio de uma estrutura associada a cada clausula
dos programas. Essa estrutura contem o valor atual do parametro de frequencia,
endereco do traco em construcao na Area de Codigo Intermediario, endereco do
codigo nativo na Area de Codigo Nativo e sub-estruturas para armazenar informacoes
utilizadas pelos predicados de depuracao (Secao 4.4.5). O acesso a essa estrutura e
feita na execucao de uma nova instrucao YAAM denominada jit handler, que e
gerada pelo Compilador (de codigo Prolog para YAAM) quando o modo de execucao
requerer acesso a devida estrutura.
Suporte ao Modulo de Recompilacao
Basicamente, a saıda do fluxo de execucao do codigo nativo para o interpretador
ocorre quando uma excecao precisa ser tratada no interpretador ou quando o traco
em execucao se tornar invalido a partir de um bloco basico. Em especıfico, o segundo
caso ocorre quando houver a necessidade de executar um bloco basico que nao foi
inserido no traco durante a sua construcao. Nesse sentido, e necessario que o sistema
identifique se a causa da saıda do fluxo de execucao ocorreu por este ultimo caso.
Para isso, dois registradores foram inseridos no sistema: K, para identificar o caso e
BADDR, para identificar o ponto exato onde o fluxo de execucao necessitou retornar
ao interpretador. Dessa forma, se o registrador K marcar verdade, o traco sera
reconstruıdo a partir do endereco marcado por BADDR.
4.3 Os Modulos da Nova Geracao do YAP
A arquitetura da nova geracao do YAP consiste de um conjunto de componentes que se
inter-relacionam de modo que as tarefas necessarias para manter e gerenciar o fluxo de
execucao entre codigo nativo e interpretado sejam bem distribuıdas. O objetivo desta
secao e apresentar uma descricao mais detalhada dos componentes do sistema, mantendo
o foco em suas especificacoes individuais.
4.3.1 Motor de Execucao
O Motor de Execucao foi projetado para executar os programas em diversos modos de
execucao. Um modo de execucao define quais componentes estarao ativos durante a
execucao de um programa, influenciando a forma que os programas serao tratados. A
vantagem de disponibilizar modos de execucao diferentes no sistema e a garantia de
82
torna-lo adequado para a realizacao de diversos experimentos, o que complementa um
dos objetivos deste trabalho. Alem disso, alguns modos de execucao empregam codigo
misto que compilam unidades de codigo quentes em tempo de execucao, o que garante a
execucao de codigo mais eficiente em um estagio final de implementacao, como e o caso
de outros sistemas que utilizam esse modelo (Arnold et al., 2000; Campanoni et al., 2010;
Chang et al., 2009; Gal et al., 2009; Homescu e Suhan, 2011; Kotzmann et al., 2008;
Paleczny et al., 2001; Suganuma et al., 2004). Relativo a isso, o sistema proposto suporta
tanto a execucao de codigo misto como a execucao de codigo unico, cujas caracterısticas
estao descritas nas subsecoes que seguem.
Modos de Execucao de Codigo Misto Modos de execucao que utilizam codigo misto
nao existem na versao atual do sistemaYAP. Baseado nisso, o sistema foi modificado para
conter duas novas areas de codigo para suportar esse tipo de execucao. A primeira area
armazena os tracos de execucao do programa corrente, que sao construıdos pelo Construtor
de Tracos para que sejam compilados no momento adequado. Essa area e denominada Area
de Codigo Intermediario, que contem as clausulas quentes em representacao intermediaria
e sao enviadas como entrada para o Compilador JIT. A outra area de codigo, denominada
de Area de Codigo Nativo e a regiao de memoria onde o codigo compilado e armazenado.
Basicamente, o sistema proposto suporta dois modos mistos de execucao a saber:
smart JIT e compilac~ao contınua.
Em smart JIT, o Motor de Execucao interpreta as clausulas do programa ate que
alguma se torne crıtica. Durante a interpretacao, em vez de utilizar o interpretador padrao
do sistema, um interpretador especial, denominado Interpretador Instrumentado executa os
programas enquanto emite os blocos basicos que compoem exatamente o fluxo de execucao
corrente. Os blocos basicos emitidos so serao tratados pelo Construtor de Tracos apos a
clausula de partida do traco se tornar crıtica. Nesse contexto, se entende por clausula
crıtica, as clausulas cujos parametros de frequencia alcancarem um percentual do limite
estabelecido para esta se tornar quente.
No decorrer deste processo, quando alguma clausula se tornar quente, o traco de
execucao e enviado ao Compilador JIT, que se encarrega de gerar codigo nativo. Todo
codigo nativo e armazenado na Area de Codigo Nativo e e priorizado durante a execucao.
Em outras palavras, em smart JIT as instrucoes dos programas so sao interpretadas
(somente pelo Interpretador Instrumentado) se nao ha uma versao nativa para ser executada.
Em especial, neste modo uma unica thread e compartilhada entre o Motor de Execucao
e o Compilador JIT. Basicamente, enquanto o ambiente de execucao nao detectar clausulas
quentes, a thread e inteiramente dedicada ao Interpretador Instrumentado. Quando uma
83
clausula se torna uma clausula crıtica e, depois disso, uma clausula quente, ela e enviada
para compilacao e o Motor de Execucao suspende a sua atividade ate que o codigo nativo
seja gerado. Apos esse processo, o Motor de Execucao comeca a executar codigo nativo
e permanece nessa iteracao ate que (1) a clausula seja executada ate o final e a clausula
seguinte, a qual nao foi ainda compilada, precisa ser executada ou (2) uma excecao ocorra
e a clausula em execucao precise ser executada na versao interpretada. De qualquer
forma, se o fluxo de execucao retornar ao Interpretador Instrumentado, o controle retorna
novamente para executar codigo nativo quando o traco compilado for novamente invocado.
No modo compilac~ao contınua as clausulas sao compiladas sem a necessidade de
interromper o Motor de Execucao. Alem da thread principal, que e dedicada para a
execucao de codigo, seja ele nativo ou interpretado, no mınimo outra thread adicional e
utilizada para compilar os tracos de execucao coletados. Esse modo de execucao necessita
que o sistema mantenha uma Fila de Compilacao para armazenar os tracos que necessitam
ser compilados.
Em compilac~ao contınua, a forma como as clausulas sao detectadas como crıticas (e
depois quentes) e como os tracos sao coletados acontecem da mesma forma que em smart
JIT. A unica diferenca e que sempre havera uma thread dedicada ao Motor de Execucao,
nao sendo necessaria a sua suspensao durante a geracao de codigo.
Modos de Execucao de Codigo Unico Os modos de execucao que utilizam versoes
unicas de codigo sao dois dentro do sistema: o modo somente interpretar, que e padrao
desde a primeira versao do YAP e o modo somente compilar (tambem chamado de
JIT por Plezbert e Cytron (1997). O modo somente compilar, assim como o modo
smart JIT compartilha uma unica thread entre o Motor de Execucao e o Compilador
JIT. A principal diferenca entre smart JIT e somente compilar e que neste ultimo
todas as clausulas sao compiladas imediatamente antes de serem invocadas, evitando,
por consequencia, o custo de atualizar o parametro de frequencia de clausula, o custo da
interpretacao inicial e o custo de coletar os tracos de execucao.
No modo somente compilar a execucao de codigo nativo permanece ate que alguma
excecao ocorra, pois esta deve ser tratada pelo interpretador. Isso na verdade aponta um
problema, pois forca o modo de execucao a interpretar codigo para tratar tais casos 8.
Alem disso, esse modo apresenta ainda tres desvantagens:
1. Consome muita memoria, visto que todas as clausulas sao compiladas;
8Isso implica que, mesmo neste modo, o sistema proposto continua mantendo codigo misto. Um dostrabalhos futuros propostos consiste em resolver este problema, permitindo que as excecoes sejam tratadastambem no codigo nativo.
84
2. Ha maiores chances de que a execucao de alguma clausula nao compense o custo de
sua compilacao, no caso de programas compostos de clausulas pouco frequentes; e
3. O codigo nao pode ser especializado. Como toda clausula e compilada na primeira
execucao, nao ha como obter informacoes dinamicamente sobre o comportamento
da aplicacao e utiliza-las para especializar a clausula sendo compilada.
Os modos de execucao supracitados definem comportamentos diferentes do Motor de
Execucao, o qual e composto por outros tres modulos, que sao habilitados conforme a
modo de execucao ativo. Estes tres modulos sao: o Monitor, o Interpretador YAP e o
Interpretador Instrumentado.
Monitor O Monitor instrumenta as clausulas do programa com parametros de frequencia
a fim de medir a frequencia de execucao das clausulas. Em modos de execucao com codigo
misto, cada clausula e instrumentada com contadores ou fracao de tempo. Desta forma,
e papel do Monitor atualizar o parametro de frequencia antes que o Motor de Execucao
execute a primeira instrucao da referida clausula.
Interpretador YAP O Interpretador YAP executa codigo conforme a versao atual do
YAP. Ele e acionado no modo de execucao somente interpretar, embora ainda seja
ativado no modo de execucao somente compilar, quando alguma excecao precisa ser
tratada.
Interpretador Instrumentado O Interpretador Instrumentado e similar ao Interpretador
YAP, porem ele e o interpretador utilizado em modos de execucao com codigo misto. Ba-
sicamente, todo bloco basico executado e emitido ao Construtor de Tracos, que se encarrega
de construir os tracos de execucao e armazena-los na Area de Codigo Intermediario. Para
possibilitar essa tarefa o interpretador instrumentado e composto por instrucoes YAAM
estendidas, as quais executam codigo enquanto emitem blocos basicos para o Construtor
de Tracos. Nesse interpretador, toda execucao e similar ao Interpretador YAP. Isso significa
que, mesmo que os tracos tenham sido construıdos e compilados, o Interpretador YAP ainda
e utilizado para tratar excecoes geradas no codigo nativo.
4.3.2 Compilador JIT
O Compilador JIT gera codigo nativo em tempo de execucao para os tracos de execucao (no
caso de modos smart JIT e compilac~ao contınua) ou clausulas de um programa (no caso
85
do modo somente compilar). Por padrao, todo codigo compilado e otimizado com um
conjunto de otimizacoes pre-definido, independente do programa em execucao. Contudo,
o sistema permite que o usuario aplique as otimizacoes (ou conjunto de otimizacoes) que
desejar.
LLVM (Low Level Virtual Machine) (Lattner e Adve, 2004) e o framework utilizado
para implementar o Compilador JIT. O atrativo deste framework e o fato deste fornecer um
conjunto de bibliotecas que podem ser utilizadas para configurar o processo de geracao
de codigo. A principal razao para utilizar LLVM neste trabalho e pela sua capacidade
de gerar codigo nativo em memoria. Alem desta razao e possıvel enumerar as seguintes:
1. A possibilidade de gerar codigo para diversas arquiteturas, o que e importante para
o sistema proposto, pois a versao original do YAP possui esta caracterıstica.
2. O mapeamento automatico dos enderecos de variaveis globais, que garante que
toda modificacao em estruturas globais realizada na execucao de codigo nativo seja
mantida na interpretacao.
3. A possibilidade de aplicar transformacoes e analise de codigo de forma arbitraria,
alem de coletar estatısticas sobre as tarefas realizadas. Em nıvel de codigo gerado,
LLVM ainda permite a selecao arbitraria do alocador de registradores, escalona-
dor de instrucoes e formas de tratar operacoes com ponto flutuante. Tudo isso
reflete uma maior flexibilidade ao sistema, possibilitando a execucao de programas
em diversas configuracoes. Em particular, este ponto e garantido pelo ambiente
experimental descrito na Secao 4.4.
Em consideracao a geracao de codigo nativo, o Compilador JIT realiza esse processo
em, basicamente, duas etapas: primeiramente, ele transforma a entrada, que consiste
de um traco de execucao, ou mesmo uma clausula completa, em codigo LLVA (que e
a representacao intermediaria de LLVM) (Adve et al., 2003) e, por fim, traduz essa
representacao para codigo nativo utilizando a LLVM, de acordo com as configuracoes
ajustadas para ela. Mesmo realizando a tarefa em dois passos, em nıvel de implementacao,
o Compilador JIT consiste de quatro fases, as quais estao representadas na Figura - 4.3 e
descritas nas proximas subsecoes.
Pre-processamento
A fase de pre-processamento transforma a estrutura de entrada dos tracos de execucao
(em modos de codigo misto) ou clausulas (no modo somente compilar) em codigo inter-
86
TraduçãoTraduçãoGeraçãode Código
Geraçãode Código
Análises eTransformações
de Código
Análises eTransformações
de Código
Código LLVA Código LLVAotimizado eanalisado
Código nativoCláusula ou traço
(estrutura de entrada)
Pré-processamento Cláusula ou traço(Código intermediário)
Pré-processamento
Figura 4.3: Fases do Compilador JIT.
mediario, que serve como entrada para a proxima fase do Compilador JIT. A transformacao
e direta e nao aplica qualquer instrumentacao sobre o codigo resultante.
Traducao
A fase de traducao transforma um traco ou uma clausula inteira para codigo LLVA. Como
a caracterıstica do sistema e preservar a livre escolha do usuario, nenhuma tecnica de
transformacao (otimizacao) ou analise de codigo e aplicada nessa fase. Todo codigo, neste
caso, e traduzido para gerar codigo LLVA a partir do codigo intermediario.
Analises e Transformacoes de Codigo
Essa fase aplica analises e transformacoes de codigo sobre o codigo gerado na fase anterior,
de acordo com a configuracao padrao do sistema ou de acordo com as configuracoes
definidas pelo usuario. Em geral, suas tarefas consistem em:
• Analises e transformacoes de codigo. Por padrao e aplicado apenas as
analises e transformacoes habilitadas no nıvel de otimizacao O3. Contudo, com
o uso de predicados do ambiente experimental, o usuario pode habilitar as analises
e transformacoes de codigo que ele deseja aplicar.
• Pre-configuracao da geracao de codigo, que configura a LLVM para que
esta utilize o escalonador de instrucoes, alocador de registradores e modelos de
tratamento de ponto flutuante que o usuario deseja aplicar durante a geracao de
codigo. Por padrao, essas configuracoes sao aquelas configuradas como padrao em
LLVM.
Geracao de Codigo
A fase final do Compilador JIT e gerar codigo nativo em memoria a partir do codigo LLVA.
E tambem papel do Compilador JIT instalar o codigo gerado na Area de Codigo Nativo.
87
4.3.3 Gerente de Codigo
O Gerente de Codigo gerencia as areas de codigo do sistema. Suas tarefas consistem
basicamente em:
1. Enviar a versao correta da clausula para execucao, priorizando o envio de codigo
nativo em modos de execucao com codigo misto;
2. Enviar uma clausula ou o traco de execucao coletado para compilacao.
3. Invocar o Monitor para que este atualize o parametro de frequencia da clausula.
A interacao do Gerente de Codigo com os demais componentes, bem como o seu
comportamento com uma clausula e definido pelo estado atual da clausula em questao.
Em modos de execucao com codigo misto, cada clausula do programa pode assumir,
no maximo, tres estados diferentes: interpretado, crıtico e nativo. No modo somente
interpretar, cada clausula assume somente o primeiro estado e no modo somente
compilar, cada clausula pode assumir um dos dois ultimos estados. As proximas
subsecoes descrevem o comportamento do Gerente de Codigo baseado no estado ativo
de uma clausula.
Tratamento das Clausulas Interpretadas
Para clausulas interpretadas, o Gerente de Codigo pode ter tres comportamentos distintos,
dependendo do modo de execucao ativo. Em primeiro lugar, ele e simplesmente desativado
no modo de execucao somente interpretar. Em modos de execucao com codigo misto
ele envia a correta versao da clausula para ser executada pelo Motor de Execucao e invoca
o Monitor para que ele atualize o parametro de frequencia da clausula. Por fim, no modo
somente compilar o Gerente de Codigo executa apenas algumas funcoes (enviar ao Motor
de Execucao o codigo interpretado que precisa tratar as excecoes e depois disso, retornar
ao codigo nativo).
Nenhum tratamento especial foi necessario para o primeiro caso, pois todo o codigo
YAAM esperando para ser executado sera interpretado. Em outras palavras, como o
Gerente de Codigo e desativado no modo somente interpretar, as clausulas sao tratadas
da mesma forma que a versao atual do sistema. Em segundo, quando se trata de codigo
misto, esse gerenciamento precisa ser intermediado pelo Gerente de Codigo. Como nos
modos com codigo misto o codigo nativo e priorizado, o Gerente de Codigo precisa enviar
ao Motor de Execucao a versao de codigo que deve ser executada. Por fim, no modo de
88
execucao somente compilar o Gerente de Codigo tem a sua funcao reduzida, pois apenas
intermedia o retorno ao interpretador, que ainda e necessario na ocorrencia de excecoes.
Tratamento das Clausulas Crıticas
Clausulas crıticas sao clausulas que estao prestes a serem compiladas. Necessariamente,
em modos de execucao com codigo misto, uma clausula se torna crıtica quando o seu
parametro de frequencia esta prestes a atingir um limite. Quando isso ocorre, o Gerente de
Codigo passa a enviar codigo interpretado para o Motor de Execucao, mais especificamente
o Interpretador Instrumentado, para que este emita o fluxo de execucao da clausula crıtica.
Por fim, quando o parametro de frequencia efetivamente atingir um limite, o traco coletado
e enviado para o Compilador JIT.
Por outro lado, quando se trata do modo somente compilar, toda clausula e crıtica na
sua primeira invocacao. Nesse caso, o Gerente de Codigo interage apenas com o Compilador
JIT, que se encarrega de gerar codigo nativo na primeira invocacao da clausula.
Tratamento das Clausulas Nativas
Para tratar clausulas ja compiladas, o Gerente de Codigo volta a interagir com o Motor
de Execucao, independente do modo de execucao ativo. Normalmente, o Gerente de
Codigo verifica a existencia de codigo nativo e o envia para execucao, permanecendo
nesse processo ate nao encontrar mais codigo nativo para executar ou ate finalizar o
programa. Adicionalmente, a execucao de codigo nativo pode ser interrompida no caso
de ocorrer alguma excecao. Nesse caso, o Gerente de Codigo precisa direcionar o controle
para o interpretador ativo e enviar codigo nativo para execucao somente apos a excecao
ser tratada.
4.3.4 Construtor de Tracos
Em nıvel de implementacao, cada instrucao YAAM contem blocos basicos que sempre
serao executados e outros que so sao alcancados a partir de desvios condicionais. Nesse
sentido, como uma clausula e constituıda por uma sequencia bem definida de instrucoes
YAAM, o processo de eliminar os testes condicionais e reconstruir a clausula somente com
os blocos executados pode garantir que o codigo compilado seja compacto e eficiente em
termos de velocidade de execucao. Basicamente, a coleta dos blocos basicos se inicia a
partir da primeira instrucao YAAM de uma clausula e continua ate a ultima instrucao da
mesma clausula.
89
Quando um traco de execucao e inicializado, a clausula de partida se torna a clausula
principal e sua primeira instrucao YAAM se torna a cabeca do traco. A partir de entao,
todo bloco basico executado e emitido pelo Interpretador Instrumentado para o Construtor de
Tracos para formar uma funcao equivalente. Nesse sentido, toda sequencia de instrucoes
YAAM e perdida, para entao formar uma sequencia de blocos basicos equivalente que
forma a referida funcao. Essa sequencia e constituıda por um conjunto composto pelos
fluxos de execucao de cada instrucao YAAM (representada por uma sequencia ordenada
de blocos basicos), iniciada com um rotulo e finalizada com um desvio incondicional para o
rotulo da proxima instrucao. Todos os rotulos sao identificados pelo endereco da instrucao
referente a ela, a fim de distinguir instrucoes de mesmo nome na mesma sequencia. Como
exemplo, a Figura - 4.4 mostra um traco gerado a partir de uma clausula composta de
tres instrucoes YAAM que nao invoca nenhuma outra clausula.
Bloco 1 da Instrução 1Bloco 2 da Instrução 1...Bloco N da instrução 1
Label_inst1_address1:
goto Label_inst2_address2:
Bloco 1 da Instrução 2Bloco 2 da Instrução 2...Bloco N da instrução 2
Label_inst2_address2:
goto Label_inst3_address3:
Bloco 1 da Instrução 3Bloco 2 da Instrução 3...Bloco N da instrução 3
Label_inst3_address3:
retorno ao interpretador
Instrução 1Instrução 2Instrução 3
Cláusula
Figura 4.4: Exemplo de um traco gerado para uma clausula com 3 instrucoes.
Idealmente, uma sequencia otima gerada pelo Construtor de Tracos deve ser constituıda
unicamente por blocos basicos executados, o que elimina qualquer necessidade de manter
instrucoes condicionais ou outro bloco basico qualquer. Contudo, a construcao desse tipo
de traco nem sempre e viavel, pois ele pode garantir a eficiencia em um programa, mas
inviabilizar a execucao de outro programa distinto9. Por essa razao, no contexto deste
trabalho, todo traco construıdo, alem de ser eficiente em termos de tempo de execucao
deve tambem (1) garantir o termino do programa e (2) uma saıda correta. Basicamente,
tais condicoes sao satisfeitas obedecendo os seguintes criterios:
9Isso foi observado durante a implementacao do Construtor de Tracos.
90
1. O traco construıdo deve ser apropriado para o tipo de termo10 para o qual ele foi
construıdo;
2. Condicoes sobre a ocorrencia de excecoes, isto e, condicoes para que o fluxo de
execucao saia do codigo nativo para o interpretador, devem ser mantidas;
3. A operacao de desreferenciacao (Secao 3.2.1) deve ser concluıda com sucesso. Isto
e, o percurso na cadeia de variaveis precisa encontrar o valor correto do termo
desreferenciado;
4. Durante a chamada de clausulas, as instrucoes de indexacao devem invocar clausulas
corretas;
5. Nem todas as instrucoes condicionais podem ser ignoradas.
Em seguida, as formas de como os criterios supracitados foram implementados sao
apresentadas nas proximas subsecoes.
Tratamento dos Testes de Tipos dos Termos
Na pratica, todas as instrucoes YAAM do YAP sao genericas para todos os tipos dos
termos. Portanto, o Construtor de Tracos garante um traco eficiente eliminando testes
condicionais para esses casos. Contudo, tendo em vista que os termos em Prolog podem
mudar de tipo durante a execucao, o Construtor de Tracos ajusta os tracos construıdos
para gerar codigo correto para todos os tipos detectados durante a construcao. Dessa
forma, enquanto nenhum termo alterar o seu tipo durante a construcao de um traco que
ele pertence, o traco tera uma caracterıstica linear.
A Figura - 4.5 mostra um trecho de traco linear quando o Interpretador Instrumentado
executa uma instrucao YAAM que possui um teste condicional sobre o tipo de um
termo. Contudo, se um teste condicional anteriormente bem-sucedido chegue a falhar,
e necessario que o traco seja modificado para conter essa instrucao juntamente com os
blocos alcancados por ela. Ao final, tal modificacao, assegura o fluxo de controle exato
para os tipos daquele termo durante o tempo de execucao. Um exemplo de um traco
modificado dessa forma esta representado na Figura - 4.6.
Tratamento das Condicoes para Ocorrencia de Excecoes
No contexto do sistema YAP, se entende por excecao todo fluxo de execucao que nao
pertence a uma clausula em execucao. Na pratica, ocorrem excecoes durante o backtrack
10Um termo e para uma clausula o que um argumento e para uma funcao.
91
Instrução YAAM que possui5 blocos e uma instrução paratestar o tipo de um termo
Instrução condicional paratestar tipo de um termo
B1
B2
B3 B4
B5
(a)
Trecho de um Traçocom característica linear
B1
B2
B4
B5
(b)
Figura 4.5: Trecho de um traco com caracterıstica linear em (b) que representa o fluxode execucao (marcado em vermelho) em (a).
Instrução YAAM que possui5 blocos e uma instrução paratestar o tipo de um termo
Instrução condicional paratestar tipo de um termo
B1
B2
B3 B4
B5
(a)
Traço com característicanão-linear
Instrução condicional paratestar tipo de um termo
B1
B2
B3 B4
B5
(b)
Figura 4.6: Trecho do traco representado na Figura - 4.5, apos o fluxo de execucao(marcado em vermelho) mudar apos o teste condicional.
(quando, por exemplo, um termo passado para get list nao for uma lista) e durante a
coleta de lixo (quando a pilha e a trilha precisam ser tratadas). Ambas as excecoes sao
priorizadas pelo Construtor de Tracos, o que infere um traco construıdo cujas condicoes
para ocorrencia de excecoes nao sejam ignoradas.
Em nıvel de implementacao, instrucoes condicionais que acarretam excecoes para
coleta de lixo nao sao precedidas por instrucoes de teste de tipos e, portanto, constituem
um bloco basico unico em um traco construıdo. A Figura - 4.7 mostra o trecho de um
92
traco construıdo com esse tipo de bloco. Nela, o bloco basico GARBAGECOLLECTION EXCEPT
representa uma instrucao condicional que, se bem sucedida, retorna um ponteiro para o
tratamento daquela excecao ao interpretador11.
Bloco 1Bloco 2GARBAGECOLLECTION_EXCEPT...Bloco N
Label_inst_address:
Se a condição para exceção for satisfeita então:retorna ao interpretador
Figura 4.7: Trecho de um traco em uma instrucao YAAM que causa uma excecao paracoleta de lixo. Isso implica que, em codigo nativo, as condicoes para essetipo de excecao sao as mesmas para codigo interpretado. O efeito e que,mesmo executando codigo nativo, a coleta de lixo sempre ocorre em codigointerpretado.
Por outro lado, excecoes causadas por backtrack sempre sao precedidas por instrucoes
condicionais, inclusive instrucoes para testes de tipos de termos. Ao contrario das excecoes
para coleta de lixo, estas sao tratadas no proprio codigo nativo e nao devem ter as
instrucoes condicionais que a precedem ignoradas. Por exemplo, a condicao do traco
representado na Figura - 4.6 e valida desde que os blocos basicos posteriores a instrucao
condicional nao precedam uma excecao causada por backtrack. Caso contrario, o traco
precisa ser construıdo por um bloco basico mais robusto, denominado neste contexto,
de bloco elementar, um tipo de estrutura que contem instrucao(oes) condicional(is),
juntamente com os seus blocos de destino e finalizado por desvios multiplos, ativados
de acordo com o fluxo de execucao tomado. Neste contexto, se um dos blocos basicos
posteriores a instrucao condicional da Figura - 4.6 conter uma excecao para tratar
backtrack, o traco construıdo deve ser como o representado na Figura - 4.8.
Tratamento da Desreferenciacao
A desreferenciacao, explicada na Secao 3.2.1, e implementada em YAP como um laco
de repeticao para percorrer uma cadeia de ponteiros construıda durante a unificacao dos
termos. Esse laco e finalizado quando o valor do termo de partida for encontrado ou
quando for determinado que um termo ainda nao foi unificado com um valor. Antes
que esse laco seja executado, um teste condicional para verificar se o termo e variavel e
realizado e a desreferenciacao ocorre somente se esse teste tiver sucesso.
11Isso indica que tratamentos de excecao para coleta de lixo nao sao tratados em codigo nativo, mesmoem modos de execucao que compilam codigo.
93
Trecho de um traço comum bloco elementar
Bloco Elementar
B1
B2
Se executou o bloco B3então:
Salta para o código quetrata backtrack
Continuanormalmente
Instrução condicional
B4
B5
B3Tratamento de backtrack
Sim Não
Figura 4.8: Trecho do traco representado na Figura - 4.6, mas usando um blocoelementar para suportar instrucoes condicionais onde um dos blocos dedestino causam backtrack.
Tracos de execucao que possibilitam instrucoes com o mesmo nome, como e no contexto
deste trabalho, possibilitam especializar a desreferenciacao pelo fato de tais instrucoes
serem distinguidas pelos seus enderecos. Tais enderecos sao como uma delimitacao de
escopo de clausula e, portanto, possuem o mesmo comportamento no que se refere ao fluxo
de execucao. Em outras palavras, se um termo e desreferenciado durante a execucao de
uma instrucao de endereco unico, a desreferenciacao sempre ocorrera quando a instrucao
neste endereco for executada novamente. Analogamente, a mesma situacao ocorrera
em casos em que a desreferenciacao nao ocorre. Portanto, considerando o esquema
de desreferenciacao implementado em YAP, a operacao de desreferenciacao pode ser
construıda dentro de um traco de uma das seguintes formas:
• Se o teste condicional executado antes do laco da desreferenciacao determinar que o
termo nao e variavel, a operacao nao e inserida no traco. Este, portanto, e o caso em
que o traco gera desempenho sobre a desreferenciacao, pois os custos da instrucao
condicional e do laco para percorrer a cadeia de ponteiros sao eliminados do traco
e, consequentemente, do codigo nativo;
• Por outro lado, se o termo for variavel, um bloco elementar contendo a operacao
de desreferenciacao sera inserida no traco. Esse bloco contem dois saltos: um deles
sera executado somente se a desreferenciacao terminar com o termo ainda sendo
variavel e, portanto, saltara para um bloco basico que trata variaveis. O segundo
salto, por sua vez, sera executado se, no final da desreferenciacao, for encontrado
94
um valor nao-variavel para o termo. O destino desse salto e um bloco basico que
trata termos nao-variaveis12.
Chamada de Clausulas e Tratamento das Instrucoes de Indexacao
Na pratica, o Construtor de Tracos trata as instrucoes de chamada de clausulas com
a tecnica de integracao de clausulas, que permite substituir a chamada pelo corpo da
clausula invocada. Basicamente, toda vez que uma nova clausula for invocada seus
blocos basicos executados tambem sao inseridos no traco em construcao, como se eles
pertencessem a clausula invocadora. Alem disso, se instrucoes de indexacao tambem
forem invocadas nesse intervalo, estas tambem sao inseridas neste traco13.
Na pratica, as instrucoes de indexacao nao sao perfiladas como as demais instrucoes
e sao mantidas por inteiro dentro do traco, mesmo que todos os seus blocos basicos
nao sejam executados. Neste processo e importante notar que antes que uma clausula
invocada seja efetivamente executada, as instrucoes de indexacao precisam ser executadas
(e consequentemente coletadas) primeiro, a fim de que o sistema identifique a clausula
correta a invocar. Como na maioria dos programas a condicao de parada esta relacionada
a mudanca de tipo de algum argumento, essa condicao nao e detectada pelo Construtor de
Tracos. Portanto, a estrategia utilizada e manter uma construcao conservativa sobre as
instrucoes de indexacao, a fim de detectar esses casos, mesmo que os tais nunca tenham
ocorrido durante a construcao dos tracos.
Alem disso, as instrucoes de indexacao sao geradas durante o tempo de execucao,
sendo difıcil determinar todas as clausulas que serao invocadas apos elas, o que reforca
ainda mais a necessidade de manter uma estrategia conservativa. Por essa razao, as
instrucoes de indexacao sao construıdas como blocos elementares de multiplas instrucoes
de desvio. Cada instrucao de desvio realiza um salto para o inıcio de uma clausula-destino
daquela instrucao de indexacao, desde que esta tenha sido detectada durante a construcao
do traco. Alem dos desvios, os blocos elementares contem tambem um retorno para o
interpretador, caso a instrucao de indexacao necessite invocar uma clausula que nao tenha
sido inserida no traco.
A Figura - 4.9 mostra um exemplo de um traco que contem uma instrucao de indexacao
no seu corpo. (a) mostra uma clausula crıtica com uma instrucao YAAM de invocacao,
cujas instrucoes de indexacao podem invocar as clausulas de 2 a 6. Ao supor que o perıodo
12E importante salientar que o tratamento para termos variaveis e nao variaveis sao especıficos paracada instrucao YAAM. Alem disso, nem todas as instrucoes desreferenciam termos.
13Instrucoes de indexacao sao criadas em tempo de execucao no YAP e sao invocadas logo apos umainstrucao de chamada para invocar a clausula correta dentro de um predicado.
95
Cláusula 1
B1
B2
B3
Instrução YAAM queinvoca outra cláusula
Instruções deindexação
Cláusula 2
Cláusula 6
Cláusula 3
Cláusula 4
Exemplo de traço iniciado na cláusula 1 com um blocoelementar para as instruções de indexação
B1
B2
B3
Instrução YAAM queinvoca outra cláusula
Bloco elementar paraas instruções de indexação
Se o program counter atualé igual à primeira
instrução da cláusula 2então:
retorno para ointerpretadorCláusula 2
Sim Não
(a)
(b)
Cláusula 5
Figura 4.9: Exemplo de traco que contem uma instrucao de indexacao.
em que a clausula crıtica se torna quente a unica instrucao invocada e a clausula 2, o traco
final construıdo (e compilado) e como o mostrado em (b). A instrucao de retorno inserida
no traco construıdo e para garantir a corretude do programa caso a instrucao de indexacao
invocar uma clausula diferente da clausula 2.
Tratamento das Instrucoes Condicionais
Por fim, as demais instrucoes condicionais que formam as instrucoes YAAM nao sao
eliminadas (exceto aquelas relacionadas a desreferenciacao e testes de tipos). Tais
instrucoes sao tratadas dessa forma porque os operadores para condicao sao os valores dos
termos (em vez dos tipos) e os registradores do sistema, que se caracterizam por mudar
frequentemente.
4.3.5 Modulo de Recompilacao
O Modulo de Recompilacao e o componente responsavel por recompilar um traco an-
teriormente compilado quando o fluxo de execucao e modificado no codigo nativo.
Normalmente, todo traco construıdo contem, no mınimo, um bloco elementar, que possui
multiplos destinos. Nesses blocos, e ainda inserido um destino adicional, para permitir
que o fluxo de execucao retorne ao interpretador quando for identificado que um bloco
basico qualquer ainda nao foi perfilado, isto e, inserido no traco. Alem disso, um Profiler e
utilizado para coletar as informacoes que estarao contidas nos registradores K e BADDR, que
sao informacoes uteis para que a recompilacao seja efetivamente realizada. Primeiramente,
ele detecta se o endereco de codigo onde houve retorno ocorreu por um destino de um
bloco elementar ou por uma excecao para coleta de lixo (essa informacao e armazenada em
K) e, em segundo lugar, se o retorno ocorrer a partir de um bloco elementar, ele identifica
96
o endereco deste bloco para que, a partir dele, o traco seja reconstruıdo (essa informacao
e armazenada em BADDR).
Em outras palavras, ocorrendo um retorno ao interpretador por um bloco elementar, a
recompilacao e ativada, a representacao intermediaria do codigo nativo que acabou de ser
executado e recuperada da Area de Codigo Intermediario (pois esta nao e excluıda apos a
compilacao) e novos blocos executados sao inseridos nela. A partir deste ponto, quando o
interpretador alcancar a cabeca do traco, este e recompilado e invocado no lugar do traco
anterior. Ainda nesta acao, o novo traco compilado simplesmente substitui o anterior,
pois assim diminui os custos de espaco e evita a manutencao de um coletor de lixo. Por
exemplo, se na Figura - 4.9-(b) a instrucao de indexacao passar a invocar a clausula 3 em
vez da clausula 2, o fluxo de execucao retorna ao codigo interpretado e, com a recompilacao
ativa o traco e reconstruıdo e recompilado, se tornando como o traco mostrado na Figura
- 4.10 (que, depois disso, ocupara o mesmo espaco outrora ocupado pelo traco da Figura
- 4.9-(b)).
B1
B2
B3
Instrução YAAM queinvoca outra cláusula
Bloco elementar paraas instruções de indexação
Se o program counter atualé igual à primeira
instrução da cláusula 2então:
retorno para ointerpretador
Cláusula 2
Sim Não
Se o program counter atualé igual à primeira
instrução da cláusula 3então:
Cláusula 3
B3
Figura 4.10: Traco da Figura - 4.9-(b) apos ser reconstruıdo e recompilado.
4.4 Os Predicados Suportados na Nova Geracao do YAP
O ambiente experimental e uma interface que estende a arquitetura proposta a fim de
prover um conjunto de predicados que auxiliam na configuracao do sistema. Os predicados
podem configurar a forma que o ambiente de execucao se comportara enquanto os
programas sao executados, alem de poder reconfigura-lo sem a necessidade de reinicia-lo.
Um exemplo de sua aplicacao pode ser visto no seguinte codigo:
O codigo a seguir configura o ambiente de execucao antes que o programa principal
(invocado por run) comeca a executar. Nesse codigo, o predicado main e o ponto de
partida do programa e os predicados execution mode, transform passes e disable t
97
1 initialization(main).
2 main :-
3 execution mode(smartjit),
4 transform passes(all),
5 run(args ),
6 disabe transform pass(all),
7 run(args ).
sao predicados do ambiente experimental, responsaveis por configurar os componentes
da arquitetura. Nesse exemplo, execution mode(smartjit) configura o sistema para
funcionar no modo de execucao smart JIT. Em seguida transform passes(all) con-
figura o Compilador JIT para que este aplique todas as transformacoes (otimizacoes)
de codigo disponıveis, durante a geracao de codigo nativo. Neste exemplo, quando
o programa for executado pela segunda vez (na linha 7), o Compilador JIT gerara
codigo nao otimizado, pois todas as transformacoes de codigo foram desabilitadas por
disabe transform pass(all) na linha anterior. Isso mostra que o benefıcio imediato
dessa interface e a praticidade ao projetar experimentos voltados na area de compilacao
JIT para Prolog.
Interface
Predicadosdo
Ambiente
Usuário
Arquivo deConfiguração
Predicadosdo
Ambiente
Gerentede código
Motor de Execução
Monitor
Compilador JIT
Fila de Compilação
Arquitetura YAPCódigoYAAM
CódigoInterm.
Códigonativo
Áreas de código
Áreas de código
Inicialização das áreas decódigo e do sistema em geral
Código compilado
Requisição decompilação e
código compilado
Versão correta decódigo para executar
Gerenciamento das áreasde código (busca e inserção)
Início da interpretação
Bibliotecas Engine Compilador
InterpretadorInstrumentado
InterpretadorYAP Construtor
de TraçosBlocosbásicos
Traços deexecução
Instrumentaçãodas cláusulas
ProfilerInformaçõesúteis para recompilação
Informaçõescoletadas
Módulo de
Recompilação
Bibliotecas Engine Compilador
Construtorde Traços
ProfilerCompilador JIT Gerentede código
Motor de Execução
Figura 4.11: Organizacao do sistema com suporte aos novos predicados.
A visao de alto nıvel do ambiente experimental esta representada na Figura - 4.11.
Nesta figura, a Interface se relaciona com um Arquivo de Configuracao, cujos valores
influenciam no comportamento de cada componente da arquitetura do sistema, mas
98
que sao modificados pelos Predicados do Ambiente (acessados pelos codigos escritos pelo
Usuario).
Os Predicados do Ambiente estao subdivididos em classes de predicados de acordo com
as suas aplicacoes, a saber: predicados para analises de codigo, predicados para trans-
formacoes de codigo, predicados para geracao de codigo, predicados para configuracao
geral, predicados para depuracao e predicados para coleta de perfil de execucao. Dentre
eles, os predicados de transformacao, analise e geracao de codigo utilizam as bibliotecas
da LLVM enquanto que os demais interagem diretamente com os modulos da arquitetura.
4.4.1 Predicados para Analises de Codigo
Os predicados para analise habilitam as analises de codigo que serao realizadas pelo
Compilador JIT na fase de analises e transformacoes de codigo e toda a interacao destes
predicados com o sistema e feita com o uso das bibliotecas da LLVM. Os predicados que
integram esse conjunto sao:
analysis pass(an )
Recebe como argumento um atomo an que indica o passo de analise a ser ativado.
A analise deve estar implementada pela versao de LLVM instalada no sistema.
analysis passes(L )
Recebe como argumento uma lista L , que contem todos os passos de analise na
ordem que serao ativados.
analysis passes(all )
Recebe como argumento um atomo all, que ativa todas as analises de codigo.
all analysis passes
Tem o mesmo efeito de analysis passes(all ).
disable analysis pass(an )
Desabilita a analise de codigo an . Se for all, todas as analises sao desabilitadas.
enable analysis pass(an )
Tem o mesmo efeito de analysis pass(an ). Se for all, funcionara como all analysis passes.
enable stats
Mostra na saıda de erro padrao as estatısticas dos passos empregados durante a
geracao de codigo nativo. Assim como as analises, as estatısticas coletadas sao
99
dependentes da versao de LLVM instalada no sistema. Exemplos de estatısticas
incluem a quantidade de variaveis globais do codigo, o numero de spills e o numero
de instrucoes de maquina emitidas.
disable stats
Desabilita enable stats.
enable time passes
Mostra na saıda de erro padrao os tempos dos passos empregados durante a geracao
de codigo nativo. Assim como as analises e estatısticas, os tempos coletadas sao
dependentes da versao de LLVM instalada no sistema. Exemplos destes incluem
o tempo de cada fase do compilador de LLVM e o tempo de cada fase das
transformacoes de codigo aplicadas.
disable time passes
Desabilita enable time passes.
enable checking hotclauses
Permite verificar a corretude do codigo LLVA gerado apos a fase de traducao do
Compilador JIT. Ocorrendo erro, uma mensagem e emitida e o sistema e finalizado.
Este predicado esta desabilitado, por padrao, mas e util quando o usuario suspeite
que o Compilador JIT nao esteja gerando codigo correto para um determinado
programa.
disable checking hotclauses Desabilita enable checking hotclauses.
4.4.2 Predicados para Transformacoes de Codigo
Os predicados para transformac~oes de codigo configuram a etapa de analises e
transformacoes de codigo do Compilador JIT e nao influenciam no comportamento dos
demais componentes. Assim como os predicados de analise, estes tambem utilizam
as bibliotecas de LLVM. Os predicados para transformacao sao:
transform pass(opt )
Recebe como argumento um atomo opt , que indica o passo de transformacao a ser
ativado. A transformacao deve estar implementada pela versao da LLVM instalada
no sistema. Em pratica, as transformacoes sao aplicadas a toda clausula que sera
compilada, na ordem em que sao ativadas por este predicado.
100
transform passes(L )
Recebe como argumento uma lista L que contem todos os passos de transformacao
na ordem que serao ativados.
transform passes(all )
Recebe como argumento um atomo all, que ativa todas as transformacoes de codigo.
all transform passes
O mesmo que transform passes(all ).
disable transform pass(opt )
Desabilita a transformacao de codigo opt . Se for all, todas as otimizacoes sao
desabilitadas.
enable transform pass(opt )
Tem o mesmo efeito de transform pass(opt ). Se for all, funcionara como
all transform passes.
transform level(n )
Recebe como argumento um inteiro n , que indica o nıvel de otimizacao a ser
aplicado. As transformacoes de codigo que sao aplicadas em cada nıvel dependem da
versao de LLVM instalada no sistema. Atualmente, quatro nıveis de transformacao
sao suportados, a saber: O0, O1, O2 e O3.
4.4.3 Predicados para Geracao de Codigo
Os predicados para gerac~ao de codigo tambem utilizam as bibliotecas de LLVM e,
como os demais, somente alteram o comportamento do Compilador JIT.
enable excess fp precision
Ativa otimizacoes que podem aumentar a precisao de ponto flutuante. Este predi-
cado e ativado por padrao no sistema.
disable excess fp precision
Desativa enable excess fp precision.
enable unsafe fp math
Ativa otimizacoes que podem diminuir a precisao de ponto flutuante. O efeito e o
mesmo de disable excess fp precision.
101
disable unsafe fp precision
Desativa enable unsafe fp precision.
soft float
Configura o Compilador JIT para que este utilize bibliotecas para tratar ponto
flutuante.
no soft float
Desativa soft float e configura o Compilador JIT para que este utilize instrucoes de
hardware equivalentes para tratar ponto flutuante. Tratamento de ponto flutuante
por hardware e o padrao no Compilador JIT.
code model(cm )
Recebe como argumento um atomo cm que indica o modelo de codigo que o
Compilador JIT utilizara. Os modelos de codigo disponıveis sao implementados em
LLVM e sao os seguintes: default , small , kernel , medium e large .
register allocator(reg )
Seleciona o alocador de registradores que sera utilizado pelo Compilador JIT. Os
disponıveis na versao da LLVM utilizada sao: basic, fast, greedy e pbqb. O alocador
padrao do sistema e o greedy.
default code model
Restaura o modelo de codigo padrao do sistema. O mesmo que code model(default ).
4.4.4 Predicados para Configuracao Geral
Os predicados para configurac~ao geral alteram o comportamento dos componentes
do sistema, exceto do Compilador JIT. A unica diferenca para esse conjunto com os demais
apresentados anteriormente e que este nao utiliza as bibliotecas de LLVM.
execution mode(i )
Altera o modo de execucao para o modo representado pelo inteiro i . Os valores
validos de i sao:
• 1 : Modifica o modo de execucao para somente interpretar;
• 2 : Modifica o modo de execucao para smart JIT;
• 3 : Modifica o modo de execucao para compilac~ao contınua; ou
• 4 : Modifica o modo de execucao para comente compilar;
102
execution mode(mode name )
Altera o modo de execucao pelo modo representado pelo atomo mode name . Os
valores validos de mode name sao:
• interpreted : Modifica o modo de execucao para somente interpretar;
• smartjit : Modifica o modo de execucao para smart JIT;
• continuous : Modifica o modo de execucao para compilac~ao contınua;
• justcompiled : Modifica o modo de execucao para somente compilar;
interpreted mode
Tem o mesmo efeito de execution mode(1 ) ou execution mode(interpreted ).
smartjit mode
Tem o mesmo efeito de execution mode(2 ) ou execution mode(smartjit ).
continuous mode
Tem o mesmo efeito de execution mode(3 ) ou execution mode(continuous ).
justcompiled mode
Tem o mesmo efeito de execution mode(4 ) ou execution mode(justcompiled ).
frequencyty1(type )
Altera o parametro de frequencia representado pelo atomo type (somente em modos
smart JIT e compilac~ao contınua). Os valores validos para type sao:
• count : Configura o Monitor para utilizar contadores para medir a frequencia
das clausulas. O sistema utiliza o valor 1024 como limite padrao do contador
de frequencia para que uma clausula seja considerada quente.
• time : Configura o Monitor para utilizar tempo para medir a frequencia das
clausulas. o sistema utiliza o valor 0, 02 (segundos) como limite padrao de
tempo de frequencia para que uma clausula seja considerada quente.
frequencyty2(type, bound )
Tem o mesmo efeito de frequencyty1(type ), mas o limite maximo do contador
(ou tempo) sao configurados para bound .
frequency bound(value )
Altera o limite do parametro de frequencia ativo para o valor definido por value .
Value pode ser inteiro ou ponto flutuante, mas se para este ultimo caso o parametro
de frequencia for contador, seu valor sera truncado.
103
profiling start point(f )
O ponto flutuante f define como um limite L o resultado de f × value, sendo value
o valor do parametro de frequencia ativo. Dessa forma, quando o parametro de
frequencia atinge o limite L a clausula se torna crıtica e o Construtor de Tracos
inicializa a construcao do traco do traco de execucao. A faixa de valores aceitavel
para f e o intervalo (0 .. 1), sendo 0.5 o valor padrao do sistema.
main clause ty(i )
Define como clausula principal do traco de execucao o formato da clausula repre-
sentado pelo inteiro i . A clausula principal do traco e a primeira clausula que o
compoe. Os valores validos de i sao:
• 1 : A clausula principal e a primeira que se torna crıtica;
• 2 : A clausula principal deve ser crıtica e conter, no mınimo, uma instrucao
call ou fcall. Esta e a configuracao padrao do sistema.
• 3 : A clausula principal deve ser crıtica e conter mais instrucoes que as demais
clausulas; ou
• 4 : A clausula principal deve ser crıtica e conter um historico de ocorrencia de
excecoes menor que todas as outras clausulas;
main clause ty(type )
Define como clausula principal do traco de execucao o formato da clausula repre-
sentado pelo atomo type . A clausula principal do traco e a primeira clausula que
o compoe. Os valores validos de type sao:
• justhot : A clausula principal e a primeira que se torna crıtica;
• hotandcallee : A clausula principal deve ser crıtica e conter, no mınimo, uma
instrucao call ou fcall. Esta e a configuracao padrao do sistema.
• hotandgreater : A clausula principal deve ser crıtica e conter mais instrucoes
que as demais clausulas; ou
• hotandless : A clausula principal deve ser crıtica e conter um historico de
ocorrencia de excecoes menor que todas as outras clausulas;
compilation threads(i )
Define como a quantidade de threads de compilacao o valor representado por i . O
valor padrao e a quantidade de nucleos de processamento menos 1.
104
default
Restaura a configuracao padrao.
4.4.5 Predicados para Depuracao
Os predicados para depurac~ao foram projetados para uso especıfico dos desenvolvedo-
res do sistema. Todos eles emitem uma informacao na saıda de erro padrao ou arquivo,
que ajudam a identificar falhas provenientes de erros de programacao, ou mesmo verificar
a corretude do codigo a partir da saıda gerada. Os predicados para depuracao sao:
print this(cond )
Emite na saıda de erro padrao um determinado conteudo quando uma condicao
correta for satisfeita. Ambos conteudo e condicao estao relacionados com o atomo
cond , que e passado como argumento. Sempre que tal condicao tiver sucesso, o
conteudo sera emitido. Os possıveis valores de cond sao:
print basic <nome da instr> Emite na saıda de erro padrao o nome da instrucao
que esta entre ‘‘<’’ e ‘‘>’’ toda vez que esta for executada no Interpretador
YAP;
print profiled <nome da instr> Emite na saıda de erro padrao o nome da
instrucao que esta entre ‘‘<’’ e ‘‘>’’ toda vez que esta for executada no
Interpretador Instrumentado;
print native <nome da instr> Emite na saıda de erro padrao o nome da ins-
trucao que esta entre ‘‘<’’ e ‘‘>’’ toda vez que esta for executada no
codigo nativo;
print emitted BB Emite na saıda de erro padrao o bloco basico representado por
BB toda vez que este for emitido;
print put BB Emite na saıda de erro padrao o bloco basico representado por BB
toda vez que este for tratado pelo Construtor de Tracos;
print main when head Emite na saıda de erro padrao o clausula inteira quando
sua cabeca for executada, mas somente em modos de execucao que compilam
codigo;
print main on trace Emite na saıda de erro padrao a clausula principal do traco
(a primeira clausula). Ao contrario de print main when head , este predicado
habilita a emissao da clausula somente uma vez, quando esta se tornar crıtica.
105
print calls Emite na saıda de erro padrao as chamadas entre clausulas (clausula
chamadora ⇒ clausula chamada);
print callsi Emite na saıda de erro padrao as chamadas entre clausulas (clausula
chamadora ⇒ indexacao ⇒ clausula chamada);
print trace on jit Emite na saıda de erro padrao o traco construıdo em forma
de blocos basicos (somente em smart JIT ou compilac~ao contınua) antes da
etapa de traducao do Compilador JIT;
print yaam on jit Emite na saıda de erro padrao a clausula em forma de ins-
trucoes YAAM (somente no modo somente compilar) antes da etapa de
traducao do Compilador JIT;
print intermediate on jit Emite na saıda de erro padrao o traco coletado (ou
clausula) como codigo intermediario antes da etapa de traducao do Compilador
JIT;
print llvabefore on jit Emite na saıda de erro padrao o traco coletado (ou
clausula) como codigo LLVA antes da etapa de analises e transformacoes de
codigo do Compilador JIT;
print llvaafter on jit Emite na saıda de erro padrao o traco coletado (ou
clausula) como codigo LLVA apos a etapa de analises e transformacoes de
codigo do Compilador JIT;
print this2(cond, n )
Tem o mesmo efeito de print this(cond ). Contudo, o conteudo sera emitido
apenas na ocorrencia de n condicoes bem sucedidas.
print this2(cond, msg )
Tem o mesmo efeito de print this( cond) , mas a string msg sera tambem emitida,
porem, apos o conteudo.
print this2(msg, cond )
Tem o mesmo efeito de print this( cond) , mas a string msg sera tambem emitida,
porem, antes do conteudo.
print this3(cond, msg, n )
Tem o mesmo efeito de print this2( cond, msg) , porem, para n condicoes bem
sucedidas.
106
print this3(msg, cond, n )
Tem o mesmo efeito de print this( msg, cond) , porem, mesmo que a condicao
tenha sucesso, os valores serao impressos na quantidade de vezes representada por
n .
print this3(msg1, cond, msg2 )
Emite na saıda de erro padrao um determinado conteudo quando uma condicao
correta for satisfeita. Ambos conteudo e condicao estao relacionados com o atomo
cond , que e passado como argumento. As strings msg1 e msg2 tambem serao
emitidas: msg1 estara antes e msg2 estara depois do conteudo. Sempre que a
condicao tiver sucesso, o conteudo sera emitido.
print this4(msg1, cond, msg2, n )
Tem o mesmo efeito de print this( msg1, cond, msg2) , porem, mesmo que a
condicao tenha sucesso, os valores serao emitidos na quantidade de vezes represen-
tada por n .
no print this(cond ) Desabilita print this(cond ) (bem como as suas variacoes) re-
lativo a cond .
print me(cond, msg )
Emite na saıda de erro padrao a mensagem msg sempre que a condicao cond for
satisfeita durante o tempo de execucao. Os possıveis valores de cond sao:
interpreted backtrack Emite na saıda de erro padrao msg quando ocorre back-
tracking em codigo interpretado;
native backtrack Emite na saıda de erro padrao msg quando ocorre backtracking
em codigo nativo;
treati heap Emite na saıda de erro padrao msg quando o sistema precisa tratar
a pilha por alguma excecao ocorrida em codigo interpretado;
treatn heap Emite na saıda de erro padrao msg quando o sistema precisa tratar
a pilha por alguma excecao ocorrida em codigo nativo;
treati trail Emite na saıda de erro padrao msg quando o sistema precisa tratar
a trilha por alguma excecao ocorrida em codigo interpretado;
treatn trail Emite na saıda de erro padrao msg quando o sistema precisa tratar
a trilha por alguma excecao ocorrida em codigo nativo;
107
criticals Emite na saıda de erro padrao msg toda vez que uma clausula se tornar
crıtica;
at compilation Emite na saıda de erro padrao msg no momento em que um traco
(ou clausula) for compilado;
at recompilation Emite na saıda de erro padrao msg no momento em que um
traco for recompilado;
nativerun init Emite na saıda de erro padrao msg quando o sistema comecar a
executar qualquer codigo nativo;
nativerun exit by exception Emite na saıda de erro padrao msg quando a
execucao de codigo nativo retornar ao interpretador pela ocorrencia de alguma
excecao;
nativerun exit by fail Emite na saıda de erro padrao msg quando a execucao
de codigo nativo retornar ao interpretador para executar um bloco ainda nao
perfilado;
nativerun exit Emite na saıda de erro padrao msg quando a execucao de codigo
nativo retornar ao interpretador (por qualquer razao);
no print me(cond )
Desabilita print me(cond, msg ) relativo a condicao cond .
copy to file(filename )
Copia para o arquivo filename toda saıda emitida na saıda de erro padrao.
emit to file(filename )
Deixa de emitir erro na saıda de erro padrao para faze-lo no arquivo filename .
nofile
Toda saıda e emitida para a saıda de erro padrao e todo arquivo aberto (por
copy to file(filename ) ou por emit to file(filename )) sao fechados.
print predicate msgs(L )
Recebe como argumento uma lista L que aceita, no maximo, tres atomos success ,
warning e error , que indica quais tipos de mensagens os predicados podem emitir.
Por exemplo, print predicates msgs(success ) permitira que todos os predicados
emitam uma mensagem quando estes tiverem sucesso (retornarem verdade).
print all predicate msgs
Habilita a emissao de todos os tipos de mensagens por parte dos predicados.
108
print success predicate msgs
Habilita a emissao de mensagens de sucesso por parte dos predicados. Por padrao,
mensagens de sucesso nao sao emitidas.
print warning predicate msgs
Habilita a emissao de mensagens de advertencia por parte dos predicados. Por
padrao, mensagens de advertencia sempre sao emitidas.
print error predicate msgs
Habilita a emissao de mensagens de erro por parte dos predicados. Por padrao,
mensagens de erro sempre sao emitidas.
no print predicate msgs(type )
Recebe como argumento um atomo type que pode ser success , warning e error ,
que desabilita os respectivos predicados para emissao de mensagens. Pode tambem
aceitar o atomo all , que desabilita todas as mensagens.
print default predicate msgs
Restaura a configuracao padrao de emissao de mensagens: mensagens de sucesso
nao sao emitidas, mas mensagens de advertencia e erro sao emitidas.
exit on warning
Finaliza o sistema quando uma condicao de advertencia ocorre, mas mensagens de
advertencia nao sao emitidas. Por padrao, este predicado e desabilitado.
no exit on warning
Desabilita exit on warning.
disable on warning
Nao permite alteracoes pelos predicados quando ocorrem condicoes de advertencia,
mas a execucao continua normalmente. Por padrao, este predicado e desabilitado.
enable on warning
Desabilita disable on warning. A execucao continua normalmente.
exit on error
Finaliza o sistema quando uma condicao de erro ocorre. Alem disso, mensagens de
erro ainda sao emitidas. Por padrao, este predicado e desabilitado.
109
no exit on error
Desabilita exit on error. Mensagens de erro ainda sao emitidas, mas o sistema
nao e finalizado.
nodebug
Desativa todo predicado de depuracao ativo.
4.4.6 Predicados para Coleta de Perfil de Execucao
Os predicados para coleta de perfil de execuc~ao sao predicados uteis para habili-
tar e mostrar os perfis de execucao dos programas. Nesse contexto, se entende por perfis de
execucao, os valores relacionados ao tempo de compilacao e construcao dos tracos, tempo
de interpretacao e tamanho de codigo. Atualmente, existem apenas dois predicados dentro
deste grupo, que habilitam a coleta de informacoes que tem alguma relacao com a Area
de Codigo Intermediario ou a Area de Codigo Nativo. Esses predicados sao:
statistics jit
Emite na saıda de erro padrao estatısticas a respeito do ambiente de execucao. Esse
predicado e habilitado somente em modos de execucao que compilam codigo e as
informacoes emitidas incluem: tempo total para construir os tracos de execucao (se
o modo de execucao ativo permitir), tempo total para compilar todos os tracos de
execucao, tempo de execucao dos tracos, tamanho da Area de Codigo Intermediario
e tamanho da Area de Codigo Nativo.
detailed statistics jit
Emite na saıda de erro padrao informacoes mais detalhadas a respeito do ambiente
de execucao. As informacoes incluem as mesmas apresentadas por statistics jit,
incluindo: tempo para construir, compilar e executar cada traco de execucao, tempo
tratando excecoes no interpretador (excecoes causadas no codigo nativo), tamanho
de cada traco compilado e representado em codigo intermediario, quantidade de
invocacao de cada traco e proporcao de excecoes causadas em cada traco.
4.5 Consideracoes Finais
Este capıtulo apresentou em detalhes a nova arquitetura proposta para o YAP. Como
observado, a nova arquitetura e uma extensao da arquitetura existente, com acrescimo de
novos modulos e novas areas de codigo. De fato, a principal contribuicao dessa proposta
e que o novo ambiente de execucao e capaz de gerar codigo nativo em tempo de execucao
110
para os tracos mais frequentes dos programas. Outra importante contribuicao e a interface
do ambiente experimental, que possibilita configurar o comportamento do sistema por
meio dos novos predicados desenvolvidos.
Para validar a arquitetura proposta, o proximo capıtulo apresenta a avaliacao do
sistema desenvolvido. Os resultados que serao apresentados demonstram que a coleta
de tracos e uma boa estrategia desde que estes sejam bem construıdos. O mesmo sera
demonstrado para os predicados do ambiente experimental, que mostraram ser uteis para
coletar as informacoes de perfis de execucao.
111
5
Avaliacao Experimental
No decorrer deste trabalho, uma nova arquitetura para o YAP foi desenvolvida como
uma extensao da sua arquitetura atual. Os principais componentes sao o Compilador JIT,
o Construtor de Tracos e o Interpretador Instrumentado. Basicamente, a interacao entre
estes componentes definem uma das contribuicoes deste trabalho, que e a construcao,
compilacao e execucao de tracos de execucao. Este capıtulo apresenta uma avaliacao do
sistema desenvolvido, em torno do impacto dos principais modulos e do desempenho de
execucao alcancado. A avaliacao esta dividida em duas partes:
• Avaliacao de desempenho: esta avaliacao tem por objetivo apresentar os
resultados quanto ao desempenho relativo a execucao e a criacao dos tracos. Esta
avaliacao foi dividida em duas partes: a primeira parte apresenta o impacto do
Interpretador Instrumentado e a segunda parte apresenta os resultados de desempenho
dos diferentes modos de execucao.
• Avaliacao da estrategia de construcao de tracos : esta avaliacao apresenta
os resultados sobre a eficiencia do Construtor de Tracos.
Em todas as avaliacoes os resultados foram obtidos em um Intel(R) Core(TM) i7-3770
CPU 3.40GHz com 4GB de memoria, executando sistema operacional Linux com kernel
versao 3.5.0. Nenhuma ferramenta externa foi utilizada para coletar os resultados e apenas
os predicados nativos do YAP foram utilizados. A Secao 5.1 apresenta a metodologia
considerada em todas as fases de avaliacao supracitadas. Em seguida, a Secao 5.2
apresenta o impacto do Interpretador Instrumentado sobre o ambiente de execucao. A Secao
5.3 apresenta o desempenho dos diferentes modos de execucao. A Secao 5.4 apresenta
112
questoes de desempenho quanto a eficiencia do Construtor de Tracos e, por fim, o capıtulo
finaliza com algumas consideracoes na Secao 5.5.
5.1 Metodologia
Para avaliar o sistema de acordo com os criterios definidos no inıcio deste capıtulo
foram utilizados 18 programas-teste com aplicacoes distintas, sendo estas divididas em
dois diferentes grupos. O primeiro grupo consiste de nucleos de programas completos
habitualmente usados pela comunidade cientıfica de Prolog. O segundo grupo consiste
de um conjunto de programas do benchmark shootout (Shootout, ????).
Uma breve descricao dos programas-teste do primeiro grupo e apresentada na Tabela
Tabela - 5.1 e do segundo grupo na Tabela Tabela - 5.2.
Tabela 5.1: Programas-teste habitualmente utilizados pela comunidade cientıfica deProlog.
Numero de
Programa Predicados Descricao Entrada
Insere uma lista de N elementos noappend 1
inıcio de outra.10000000
Torre de hanoi contendo 3 pinos ehanoi 1
N discos.24 discos
nreverse 2 Inverte uma lista com N elementos. 100000Ordena uma lista de N elementos
quicksort 2pelo metodo quick-sort.
8000000
Funcao recursiva controlada portak 1
operacoes aritmeticas simples.57, 21, 36
Quebra-cabeca logico baseado emzebra 5 restricoes, resolvido por busca Nao ha entrada
exaustiva.
Visto que um dos objetivos do trabalho e fornecer um conjunto de predicados que
possibilitam realizar experimentos voltados a compilacao JIT para Prolog, nenhuma
avaliacao apresentada nas proximas secoes utilizou ferramentas externas para coletar os
perfis de execucao. Isso foi proposital a fim de mostrar que os predicados propostos sao
suficientes para realizar essa tarefa. As configuracoes do sistema e os predicados utilizados
sao apresentados no inıcio de cada secao que prossegue.
113
Tabela 5.2: Programas-teste do benchmark shootout.Numero de
Programa Predicados Descricao Entrada
Aloca e desaloca varias arvoresbinary trees 6
binarias.19
Realiza todas as permutacoes defannkuch 11
um conjunto de tamanho N.11
Gera uma sequencia aleatoria defasta 13
DNAs.8000000
Conta todas as sequencias de Knucleotıdeos. O valor de K varia Saıda de
k nucleotide 19entre 3, 4, 6, 12 e 18 durante a fasta 100000execucao.Gera um conjunto de Mandelbrot
mandelbrot 5e escreve um bitmap portatil.
1800
Realiza uma simulacao com Nn-body 9
planetas jovianos.1800000
Conta a quantidade de numerosnsieve 9 primos de 2 a M usando o 6
algoritmo Crivo de Erastotenes.Conta a quantidade de numerosprimos de 2 a M usando oalgoritmo Crivo de Erastotenes.
nsieve bits 9A diferenca para o algoritmo
12
anterior e que este emprega vetorde bits.Realiza uma serie de somas
partial sum 3 parciais que necessitam de 18000000precisao dupla.Calcula os N primeiros dıgitos de
pidigits 7Pi.
18000000
Calcula tres funcoes numericasrecursive 4 simples: ackermann, fibonnaci 12
e tak.Encontra um valor proprio usando
spectral norm 13power method.
1000
5.2 Impacto do Interpretador Instrumentado
O Interpretador Instrumentado compoe a arquitetura do sistema para executar codigo em
modos de execucao com codigo misto, enquanto ainda emite blocos basicos para serem
tratados pelo Construtor de Tracos. Essa emissao e contınua mesmo antes de qualquer
114
clausula se tornar crıtica e se mantem mesmo na ocorrencia de excecoes dentro do
codigo nativo (pois estas sao tratadas neste mesmo interpretador). O objetivo desta
secao, portanto, e avaliar o Interpretador Instrumentado e o seu impacto no ambiente
de execucao em comparacao ao Interpretador YAP. Os resultados obtidos servirao para
justificar algumas questoes de desempenho discutidas nas proximas secoes.
Para esta analise, as seguintes situacoes foram consideradas:
• Cada programa foi executado 10 vezes com o Interpretador YAP e com o Interpretador
Instrumentado;
• O Compilador JIT foi desativado;
• Nenhum predicado apresentado na Secao 4.4 foi utilizado;
• O tempo de execucao para todos os programas foram coletados pelo predicado
statistics, que esta presente na versao atual do YAP e que e util para mostrar
perfis de execucao do sistema quando este nao invoca o Compilador JIT.
• Os resultados foram apresentados em grafico, em termos da aceleracao (ou desace-
leracao) obtida. A Formula 5.1 mostra como esta grandeza foi calculada no contexto
deste trabalho.
Acelerac~ao =Tempo Interpretador YAP
Tempo Interpretador Instrumentado(5.1)
Os resultados obtidos estao representados na Figura - 5.1. Nesta figura, cada barra
representa a aceleracao dos programas executados, de acordo com a Formula 5.1. Alem
disso, para fins de comparacao, a figura apresenta tambem os tempos de execucao (em
segundos) dos programas executados pelo Interpretador YAP.
A Figura - 5.1 mostra que, para todos os programas, o valor da aceleracao foi menor
que 1, o que aponta desaceleracao por parte do Interpretador Instrumentado ou, em outras
palavras, uma execucao mais lenta de codigo. Em valores, este e, em media, 11, 31 vezes
mais lento que o Interpretador YAP, com perdas de desempenho variando de 1, 018 vezes
para o programa zebra, a quase 36 vezes para o programa mandelbrot.
A principal razao para o alto custo do Interpretador Instrumentado e a sua funcao de
emitir blocos basicos, que e uma tarefa contınua durante toda a execucao. Os blocos
emitidos sao tratados pelo Construtor de Tracos (apos a clausula-cabeca do traco se tornar
crıtica) para que, apos um tempo, os tracos compilados sejam executados a fim de alcancar
ganhos de desempenho em modos de execucao com codigo misto. Por um lado, os valores
115
appe
ndhan
oi
nreve
rse
quick
sort tak
zebr
a
binar
y tre
es
fannku
chfa
sta
k nucle
otide
mande
lbrot
n-bod
y
nsieve
nsieve
bits
parti
al su
m
pidigi
ts
recu
rsive
spec
tral n
orm
0
0.1
0.2
0.3
0.4
0.5
0.6
0.7
0.8
0.9
1Aceleração do InterpretadorInstrumentado
append -- 70,702hanoi -- 53,635nreverse -- 225,458quicksort -- 85,453tak -- 68,105zebra -- 0,082binary trees -- 114,158fannkuch -- 402,198fasta -- 76,965k nucleotide -- 153,574mandelbrot -- 86,382n-body -- 68,630nsieve -- 238,316nsieve bits -- 95,491partial sum -- 73,298pidigits -- 72,654recursive -- 171,146spectral norm -- 173,259
Tempo de execução com o interpretador YAP (s)
Figura 5.1: Impacto do Interpretador Instrumentado no ambiente de execucao. Quantomenor a barra, maior a desaceleracao.
de desaceleracao observados permitem concluir que, nestes modos de execucao, o essencial
e manter o fluxo de execucao em codigo nativo pelo maior tempo possıvel, de modo que o
custo anterior a compilacao seja amortizado. Por outro lado, programas que sao incapazes
de gerar tracos que mapeiem um escopo consideravel do codigo para manter esse tipo de
comportamento possuem grandes chances de obterem perdas de desempenho devido ao
Interpretador Instrumentado.
Portanto, neste contexto, a modificacao do interpretador para que este emita blocos
basicos somente no momento da construcao dos tracos seria a estrategia ideal para
amenizar os custos relacionados a esse problema. Basicamente, o comportamento do
Interpretador Instrumentado seria guiado pelo estado da clausula que ele esta prestes a
executar, similar ao comportamento do Gerente de Codigo. Em primeiro lugar, quando
nenhuma clausula esta em estado crıtico, o interpretador executaria instrucoes nao
instrumentadas e os blocos basicos so seriam emitidos para clausulas que, com o passar do
tempo, se tornassem crıticas. Nesse momento, em vez de uma instrucao YAAM padrao,
a sua contraparte instrumentada seria executada. Por fim, o comportamento retornaria
a configuracao padrao apos a geracao de codigo nativo, garantindo que qualquer excecao
seja executada sem o custo adicional de emitir blocos basicos.
5.3 Desempenho dos Diferentes Modos de Execucao
No decorrer deste texto foi apresentado uma arquitetura para a nova geracao do YAP com
suporte a construcao e geracao de codigo de tracos de execucao com o proposito de alcancar
um dos objetivos deste trabalho, que visa obter uma execucao eficiente dos programas
frente a versao atual do sistema. Esta secao apresenta analises de desempenho dos
116
programas-teste selecionados e discussoes sobre os resultados coletados para cada modo
de execucao implementado. Para essa secao, os seguintes parametros foram considerados:
• Todos os programas-teste foram executados 10 vezes nos modos somente interpretar,
smart JIT, compilac~ao contınua e somente compilar.
• Considerando os modos smart JIT e compilac~ao contınua, os seguintes parametros
foram considerados:
– Ambos foram executados com a recompilacao ativada e desativada, com os
programas-teste executados 10 vezes para cada configuracao;
– O mecanismo para selecao da clausula principal de um traco foi definido pelo
predicado main clause ty, no seu valor padrao.
– O parametro de frequencia foi contador, com um limite de 1024.
– O valor do contador ajustado para detectar uma clausula como crıtica foi a
metade do limite estabelecido, isto e, 512.
– Os predicados para analise, transformacao e geracao de codigo nao foram
utilizados. Logo, a configuracao do Compilador JIT e a configuracao padrao
do sistema.
• Nenhum predicado para depuracao foi utilizado.
• Os perfis de execucao foram coletados pelo predicado detailed statistics jit.
Assim como na analise do Interpretador Instrumentado, os graficos apresentados nas
proximas secoes foram construıdos com base nos valores de aceleracao/desaceleracao
calculados pela Formula 5.1, adaptada para medir o impacto dos modos de execucao
sobre o modo somente interpretar. A partir da aceleracao, foi calculada a melhoria
dos programas (Formula 5.2), cujos valores foram apenas mencionados no decorrer do
texto.
Melhoria = (Acelerac~ao - 1)× 100 (5.2)
A analise dos resultados para essa fase da avaliacao esta dividida para cada modo de
execucao nas proximas subsecoes.
117
5.3.1 O Desempenho de Smart JIT
Os resultados obtidos no modo smart JIT sobre o modo somente interpretar estao
representados na Figura - 5.2. Nesta figura, o desempenho de cada programa e apresentado
em duas barras. Da esquerda para direita, a primeira barra representa o desempenho
no modo smart JIT com recompilacao e a segunda barra representa o desempenho no
modo smart JIT sem recompilacao. Cada barra indica a aceleracao obtida pelo programa
executado no seu respectivo modo, em que valores menores que 1 indicam programas com
um desempenho pior que o modo somente interpretar. Adicionalmente, a Figura - 5.3
indica o percentual em que o fluxo de execucao se manteve no Interpretador Instrumentado,
no Construtor de Tracos, no Compilador JIT e na execucao do codigo nativo.
appe
ndhan
oi
nreve
rse
quick
sort tak
zebr
a
binar
y tre
es
fannku
chfa
sta
k nucle
otide
mande
lbrot
n-bod
y
nsieve
nsieve
bits
parti
al su
m
pidigi
ts
recu
rsive
spec
tral n
orm
00.20.40.60.8
11.21.41.61.8
22.22.4
Com recompilação
Sem recompilação
Figura 5.2: Aceleracao/desaceleracao dos programas teste no modo smart JIT (com esem recompilacao) sobre o modo somente interpretar. Valores menoresque 1 indicam desaceleracao.
appe
ndhan
oi
nreve
rse
quick
sort tak
zebr
a
binar
y tre
es
fannku
chfa
sta
k nucle
otide
mande
lbrot
n-bod
y
nsieve
nsieve
bits
parti
al su
m
pidigi
ts
recu
rsive
spec
tral n
orm
0%10%20%30%40%50%60%70%80%90%
100%
Interpretador InstrumentadoConstrutor de TraçosCompilador JITCódigo Nativo
Figura 5.3: Percentuais do fluxo de execucao nos componentes ativos no modo smart
JIT.
Em primeiro lugar, e possıvel perceber que, para a maioria dos programas, os custos
do Construtor de Tracos e do Compilador JIT sao mınimos, ou ate mesmo desprezıveis,
118
nao ocasionando perda de desempenho. Relativo a esses dados, em media, o tempo
para construir os tracos variou na proporcao de 0, 0001% (k nucleotide) a 0, 013%
(fasta) do tempo total de execucao quando a recompilacao foi desativada. Com a
recompilacao ativa essa proporcao foi de 0, 006% (quicksort, tak e fannkuch) a 25, 6%
(k nucleotide). Nessa ultima configuracao, a razao media do tempo construcao de
tracos pelo tempo de execucao total foi de 1, 5% para todos os programas, mas a alta
razao relacionada ao programa k nucleotide e devido a alta concentracao de execucao
em codigo interpretado. Como a recompilacao ainda permanece ativa, parte desse fluxo
precisa garantir a reconstrucao de tracos outrora construıdos. Com excecao do programa
k nucleotide, o maior tempo de construcao de tracos ficou para o programa spectral
norm, que ocupou 0, 5% do tempo de execucao.
Relacionado ao Compilador JIT, o tempo medio de sua execucao no modo sem re-
compilar foi de apenas 0, 105 segundos. Para este, a proporcao variou de 0, 001% (k
nucleotide) a 0, 2% (pidigits). Como esperado, o tempo deste componente foi um
pouco maior com a recompilacao ativada, tendo media de 0, 746 segundos, variando
na razao de 0, 005% (nreverse) a 0, 6% (mandelbrot) do tempo total de execucao.
Tais valores mostram que, embora o Compilador JIT seja invocado diversas vezes para
recompilacao, o efeito de sua execucao e pouco significativo no impacto de desempenho.
Por essa razao, e valido dizer que, pela estrategia de construcao de tracos im-
plementada neste trabalho, o ideal e manter a recompilacao sempre ativa. Embora
nessa configuracao, programas como hanoi, fasta, nsieve, partial sum, recursive
e spectral norm tenham sido executados por mais tempo que a configuracao sem
recompilacao, o desempenho se manteve compatıvel em geral. Dentre tais programas,
hanoi foi o que obteve maior diferenca entre ambas as configuracoes, mas com uma
diferenca de apenas 4, 8% (o programa zebra nao entra nesta comparacao porque nao foi
gerado codigo nativo em sua execucao). Por outro lado, programas cujos desempenhos
foram piores na configuracao sem recompilacao, a diferenca foi maior. Dentre eles, os
programas k nucleotide e mandelbrot chegaram a ser, respectivamente, 540% e 1000%
mais lentos do que a execucao com recompilacao.
Novamente, com base nos resultados coletados, estes demonstram que a maior parte
da execucao de todos os programas se concentra na interpretacao (pelo Interpretador YAP)
e/ou na execucao de codigo nativo. Levando em consideracao a eficiencia do sistema
de compilacao no modo com recompilacao (cujo desempenho, em geral foi melhor), 13
dentre os 18 programas-teste obtiveram um ganho de desempenho quando executados no
modo smart JIT, sao eles: nreverse, quicksort, tak, binary trees, fannkuch, fasta,
mandelbrot, n-body, nsieve, nsieve bits, partial sum, recursive e spectral norm.
119
Analisando a Figura - 5.2, e possıvel verificar que o modo smart JIT com recompilacao
obteve o melhor ganho de desempenho sobre o modo somente interpretar para o
programa mandelbrot, sendo este ganho de 134, 4%. No outro extremo esta o programa
tak, que proporcionou apenas um ganho de 1, 006%. Entre esta faixa de desempenho
se encontra, portanto, os outros nove programas, cujos ganhos de desempenho foram:
50, 5% para n-body, 26, 7% para fasta, 22, 4% para nreverse, 19, 6% para fannkuch,
18, 4% para partial sum, 14, 1% para nsieve bits, 13, 9% para recursive, 10, 8% para
spectral norm, 6, 5% para binary trees, 5, 5% para quicksort e 3, 6% para nsieve.
O que pode ser notado de imediato na Figura - 5.3 e que, para a maioria dos programas
com o tempo de execucao reduzido pelo modo smart JIT (nesse caso, com recompilacao
ativa), o fluxo de execucao se manteve a maior parte na execucao de codigo nativo. Isso,
portanto, comprova que a principal razao para o ganho de desempenho obtido neste
modo e a alta proporcao de tempo que o fluxo permanece executando codigo nativo. Na
pratica, quanto maior e a proporcao de tempo gasto em codigo nativo, maior e o ganho
de desempenho. Isso e uma vantagem, porque minimiza o custo inerente do Interpretador
Instrumentado. Por outro lado, isso aponta um problema porque requer tecnicas mais
sofisticadas para detectar e construir tracos de execucao cada vez melhores.
Embora a maioria dos programas avaliados provessem tracos de execucao, um dos
fatores para a alta concentracao do tempo de execucao em codigo nativo nao se deve,
necessariamente, a construcao de tracos em si, mas ao mecanismo de deteccao de clausula
principal do traco. De fato, iniciar a construcao de um traco a partir de uma clausula
que esta no final de uma cadeia de chamadas proporciona um traco reduzido porque
somente as clausulas invocadas apos a clausula inicial serao inseridas no corpo do traco.
Por outro lado, se o mecanismo de deteccao detectar clausulas no inıcio de uma cadeia
de chamadas, o traco construıdo sera maior e, consequentemente, isso resultara em fluxos
de execucao mantidos em codigo nativo por mais tempo. Embora esse mecanismo tenha
detectado tracos otimos para alguns programas-teste, ele e ainda muito instavel, pois foi
registrada alta degradacao de desempenho em alguns programas, como k nucleotide e
mandelbrot. A recompilacao surgiu como uma forma de amenizar essa instabildade, o
que realmente mostrou ser eficiente, porem nao para todos casos.
Alem disso, existe outro problema que impede a manutencao do fluxo de execucao em
codigo nativo, mas este sera mencionado na proxima secao, que avalia a efetividade do
Construtor de Tracos.
Em relacao aos programas para os quais smart JIT resultou em perdas de desem-
penho, a media de desaceleracao foi de 55, 07 vezes na execucao sem recompilacao e de
14, 247 vezes com recompilacao. Na primeira configuracao, a desaceleracao variou de
120
1, 002 (append) a 4, 691 vezes (mandelbrot), embora o alto valor da media tenha sido
decorrente da desaceleracao proveniente do programa k nucleotide, que chegou a ser
429, 638 vezes mais lento. Por outro lado, a desaceleracao da maioria dos programas com
a recompilacao ativada variou de apenas 1, 001 (append) a 1, 047 vezes (hanoi), mas com
um salto de desaceleracao para 67, 092 vezes tambem para o programa k nucleotide.
Analisando, portanto, mais uma vez a Figura - 5.2, e possıvel observar que a maioria
dos programas que obtiveram queda no desempenho foi devido a alta concentracao
do fluxo de execucao no Interpretador Instrumentado. Os programas pidigits (com e
sem recompilacao) e nsieve bits (sem recompilacao) tambem apresentaram queda no
desempenho, mesmo concentrando grande parte da execucao em codigo nativo, porem
este fato pode ser explicado pela falta de otimizacoes dos tracos construıdos para o
programa, que, como foi mostrado na Secao 4.3.4, nao sao aplicaveis a todos os casos.
Adicionalmente, quedas de desempenho podem tambem estar relacionadas as excecoes
trataveis em codigo interpretado, que se trata da manipulacao das regioes de memoria do
YAP (heap, pilha local e trilha), mas as causas para este caso serao apresentadas somente
na Secao 5.4, que avalia o Construtor de Tracos.
Por fim, exceto os programas pidigits e nsieve bits, para todos os programas
que apresentaram queda de desempenho, o fluxo de execucao permaneceu a maior parte
no Interpretador Instrumentado. Isso da evidencias de tracos superficiais construıdos,
isto e, tracos que nao puderam mapear muitas clausulas dentro de uma cadeia de
invocacoes. A consequencia imediata e justamente a concentracao de execucao dentro
de um interpretador lento (conforme discutido na Secao 5.2).
Mapear a maior quantidade possıvel de clausulas dentro de um unico traco de execucao
nao e uma tarefa trivial. Tracos cujo fluxo de execucao sao mantidos na maior parte do
tempo sao difıceis de encontrar, pois o comportamento da execucao e dependente de
diversas informacoes, a saber: do tamanho da entrada, dos tipos das variaveis Prolog
e nas possibilidades de ocorrer excecoes. Uma forma viavel de investigar esse problema
seria empregar analise global (Mellish, 1981) para detectar clausulas que estao presentes
no inıcio de uma cadeia de invocacao, para entao criar tracos de execucao a partir delas
(onde o traco construıdo sera maior). Outra alternativa e empregar online profiling
para realizar a mesma tarefa. Basicamente, online profiling pode investigar a cadeia
de invocacoes durante a interpretacao enquanto nenhuma clausula se torna crıtica. Essa
ultima estrategia e mais simples de implementar, mas sua aplicacao e inviavel no modo
somente compilar. Ambas as implementacoes sao viaveis no sistema proposto, porque o
custo do Construtor de Tracos e desprezıvel ao tempo de execucao total, portanto e valido
que qualquer esforco seja aplicado, mesmo que algum custo adicional seja gerado.
121
Adicionalmente, outra estrategia consiste em modificar o interpretador utilizado em
modos de execucao com codigo misto, para que este emita blocos basicos somente quando
for necessario construir um traco. A ideia desse novo interpretador ja foi apresentada
na Secao 5.2. Na verdade, isso nao construiria tracos melhores, mas o fato de a
execucao manter mais tempo interpretando que executando codigo nativo nao prejudicaria
o desempenho do sistema pela emissao desnecessaria de blocos basicos.
5.3.2 O Desempenho de Compilacao Contınua
Os resultados obtidos no modo compilac~ao contınua sobre o modo somente interpretar
estao representados na Figura - 5.4. Nesta figura, o desempenho de cada programa
tambem e apresentado em duas barras. Da esquerda para direita, a primeira barra
representa o desempenho no modo compilac~ao contınua com recompilacao e a segunda
barra representa o desempenho no modo compilac~ao contınua sem recompilacao. Alem
disso, assim como na Figura - 5.2, as barras representam a aceleracao (ou desaceleracao) do
respectivos modos sobre o modo somente interpretar. Adicionalmente, a Figura - 5.5
indica o percentual em que o fluxo de execucao se manteve no Interpretador Instrumentado,
no Construtor de Tracos e na execucao do codigo nativo, sem considerar o tempo gasto no
Compilador JIT, devido o fato de todo codigo ser compilado por threads dedicadas.
appe
ndhan
oi
nreve
rse
quick
sort tak
zebr
a
binar
y tre
es
fannku
chfa
sta
k nucle
otide
mande
lbrot
n-bod
y
nsieve
nsieve
bits
parti
al su
m
pidigi
ts
recu
rsive
spec
tral n
orm
00.20.40.60.8
11.21.41.61.8
22.22.4
Com recompilação
Sem recompilação
Figura 5.4: Aceleracao/desaceleracao dos programas teste no modo compilac~ao
contınua (com e sem recompilacao) sobre o modo somente interpretar.Valores menores que 1 indicam desaceleracao.
Como esperado, os tempos gastos na construcao dos tracos dos programas executados
no modo compilac~ao contınua foram similares ao modo smart JIT. Executando sem
recompilacao, a proporcao do tempo de construcao de tracos pelo tempo de execucao
variou da mesma forma que no modo smart JIT sem recompilacao: a menor proporcao
foi de 0, 0001% (k nucleotide) e a maior foi de 0, 01% (fasta). Com a recompilacao
122
appe
ndhan
oi
nreve
rse
quick
sort tak
zebr
a
binar
y tre
es
fannku
chfa
sta
k nucle
otide
mande
lbrot
n-bod
y
nsieve
nsieve
bits
parti
al su
m
pidigi
ts
recu
rsive
spec
tral n
orm
0%10%20%30%40%50%60%70%80%90%
100%
Interpretador InstrumentadoConstrutor de TraçosCódigo Nativo
Figura 5.5: Percentuais do fluxo de execucao nos componentes ativos no modocompilac~ao contınua.
ativa a variacao entre as proporcoes foram semelhantes para a maioria dos programas.
Nesse ultimo caso, a unica excecao ocorreu para o programa k nucleotide que, desta vez
teve um tempo de construcao de tracos muito maior, ocupando cerca de 60% do tempo
de execucao total.
Essa alta proporcao se deve a alta dinamicidade do programa k nucleotide ou, em
outras palavras, a constante modificacao do comportamento deste programa em tempo
de execucao. Em particular, os tracos compilados para este programa se tornam invalidos
muito rapidamente. Em nıvel de implementacao, isso significa que os blocos elementares
de multiplos destinos dentro dos tracos compilados regularmente precedem blocos basicos
que nunca foram tratados pelo Construtor de Tracos, fazendo entao, que o fluxo de execucao
retorne ao interpretador e a reconstrucao dos tracos. Esse comportamento serve para
explicar o porque este programa teve altas quedas de desempenho nos modos smart JIT
e compilac~ao contınua. Apenas para tıtulo de comparacao, k nucleotide teve 12
tracos construıdos, com cada um deles recompilados tres vezes. Por outro lado, dentre os
demais programas, aquele que mais recompilou construiu apenas 3 tracos e, em media,
cada um deles foi recompilado apenas duas vezes.
De qualquer forma, embora ainda compilasse programas com threads separadas, o
modo compilac~ao contınua teve um tempo de execucao total compatıvel com smart JIT
devido a dois fatores. O primeiro fator esta relacionado com o Interpretador Instrumentado,
pois, visto que na compilac~ao contınua o Compilador JIT e invocado por threads
separadas, a execucao ainda mantem, mesmo que por pouco tempo, a execucao em codigo
interpretado ate que o codigo compilado seja instalado na Area de Codigo Nativo. Nesse
sentido, isso amortiza qualquer ganho obtido durante a geracao de codigo.
O segundo fator esta relacionado ao Compilador JIT. Na verdade, este componente
e muito rapido para gerar codigo e, portanto, dificilmente o ganho de desempenho
123
relacionado e perceptıvel. O modo compilac~ao contınua com recompilacao obteve
melhor desempenho que smart JIT (tambem com recompilacao) ao executar os programas
hanoi, mandelbrot, n-body, partial sum e pidigits. Para estes, a melhoria foi de
4, 7%, 2, 3%, 0, 6%, 0, 02% e 0, 3%, respectivamente. Sem recompilacao, entretanto, os
programas que apresentaram melhoria de desempenho foram novamente mandelbrot
(7, 9%), n-body (1, 7%), pidigits (3%), alem de tak (1, 9%), fannkuch (4, 5%), k
nucleotide (4, 8%) e spectral norm (2, 1%). Esses resultados, juntamente com aqueles
apresentados na Secao 5.3.1 permitem calcular a media de desempenho dos modos de
execucao. Em primeiro lugar, o melhor modo e smart JIT com recompilacao, cuja
melhoria sobre o modo somente interpretar foi de 12%, em media. Em seguida foi
o modo compilac~ao contınua com recompilacao (10, 6%), depois o modo smart JIT
sem recompilacao (−1, 6%) e, por fim, o modo compilac~ao contınua sem recompilacao
(−2, 4%)1.
Por fim, e possıvel concluir que o modo de compilac~ao contınua e, em geral, pior
que o modo smart JIT porque, em media, o baixo tempo de compilacao nao e capaz
de amortizar o tempo total de execucao devido ao custo de Interpretador Instrumentado,
que precisa permanecer ativo durante a geracao de codigo. Isso, portanto, demonstra que
a unica alternativa para aumentar o desempenho dos programas no modo compilac~ao
contınua e utilizar um interpretador mais sofisticado, que emita blocos basicos somente
quando necessario, como descrito na Secao 5.2.
5.3.3 O Desempenho de Somente Compilar
Os resultados obtidos no modo somente compilar sobre o modo somente interpretar
estao representados na Figura - 5.6. Nesta figura, o desempenho de cada programa e
apresentado em uma barra que representa a (des)aceleracao dos programas. Alem desta
figura, a Figura - 5.7 ainda indica o percentual em que o fluxo de execucao se manteve no
Compilador JIT e na execucao do codigo nativo.
Mesmo nao invocando o Interpretador Instrumentado, o modo somente compilar nao
alcancou melhor desempenho em nenhum programa em comparacao aos demais modos e
e, portanto, considerado o modo com pior desempenho. Ainda assim, apenas os programas
desacelerados nos modos smart JIT e compilac~ao contınua tiveram melhor desempenho
no modo somente compilar.
1Os valores negativos e devido a alta desaceleracao apresentada nos programas k nucleotide emandelbrot.
124
appe
ndhan
oi
nreve
rse
quick
sort tak
zebr
a
binar
y tre
es
fannku
chfa
sta
k nucle
otide
mande
lbrot
n-bod
y
nsieve
nsieve
bits
parti
al su
m
pidigi
ts
recu
rsive
spec
tral n
orm
0
0.1
0.2
0.3
0.4
0.5
0.6
0.7
0.8
0.9
1
Figura 5.6: Aceleracao/desaceleracao dos programas teste no modo somente compilar
sobre o modo somente interpretar. Valores menores que 1 indicamdesaceleracao.
appe
ndhan
oi
nreve
rse
quick
sort tak
zebr
a
binar
y tre
es
fannku
chfa
sta
k nucle
otide
mande
lbrot
n-bod
y
nsieve
nsieve
bits
parti
al su
m
pidigi
ts
recu
rsive
spec
tral n
orm
0.01%
0.1%
1%
10%
100%
Compilador JITCódigo Nativo
Figura 5.7: Percentuais do fluxo de execucao nos componentes ativos no modo somente
compilar. O grafico esta representado em escala logaritmica para destacaros tempos do Compilador JIT.
No modo somente compilar, todas as clausulas sao compiladas na primeira invocacao.
Se uma clausula contem instrucoes call ou fcall, estas sao compiladas normalmente,
como qualquer outra, mas logo apos estas, um codigo adicional e inserido para que a
clausula nativa seja capaz de compilar e chamar a clausula que ela precisa invocar. O
custo desta instrumentacao, somado ao custo de compilacao e invocacao e manutencao de
funcoes e o que justifica porque nenhum programa executado no modo somente compilar
alcancaram desempenho melhor que o modo somente interpretar. Neste ponto e
importante ressaltar que a queda do desempenho nao esta associado a interpretacao de
codigo. De fato, o modo somente compilar ainda interpreta codigo para tratar excecoes
ocorridas em codigo nativo, mas como nao ha necessidade de construir tracos de execucao,
o interpretador utilizado e o Interpretador YAP, que nao atribui custos consideraveis frente
ao Interpretador Instrumentado.
125
Enfim, tais resultados demonstram que a unica alternativa para obter desempe-
nho no modo somente compilar e que o sistema consiga inferir tracos de execucao
nao-superficiais (tracos que mapeiam a maior quantidade de codigo de uma cadeia de
chamadas), antes mesmo da primeira invocacao da primeira clausula. Analise global
seria util nesse sentido, ao analisar o corpo das clausulas e, consequentemente, uma
cadeia de invocacoes de clausulas. Realizar essa analise no tempo de carregamento do
programa seria ainda mais interessante pois, conhecendo o tamanho da entrada, seria
possıvel descobrir (ou inferir) quantas vezes um determinado traco seria invocado. Na
verdade, o principal problema relacionado a isso, e o fato de que o YAP gera instrucoes
de indexacao em tempo de execucao, sendo difıcil definir quais delas integrarao os tracos
que serao inferidos. Para este ultimo caso, uma solucao seria investigar em mais detalhes
o algoritmo para geracao de tais instrucoes (Costa, 2009; Costa et al., 2007) e entao,
implementar um mecanismo para integrar as conclusoes obtidas nesta investigacao na
tecnica de analise global empregada.
5.4 Desempenho na Construcao dos Tracos
A Secao 5.3 mostrou que para casos em que os programas alcancam melhor desempenho
os tracos construıdos mapeiam o maior escopo possıvel dos programas, ocasionando um
fluxo de execucao que se mantem a maior parte do tempo. Nesta secao e apresentada
uma avaliacao sobre a eficiencia do Construtor de Tracos. Os resultados servirao para
mostrar o porque que determinados programas nao obtiveram um ganho de desempenho
na execucao.
Para esta parte da analise, todos os dados relevantes foram coletados a partir da
execucao no modo smart JIT, conforme a configuracao apresentada na Secao 5.3. Mas,
como o foco nesta parte e avaliar a eficiencia do Construtor de Tracos, as conclusoes foram
baseadas nos resultados obtidos na configuracao sem recompilacao. A configuracao com
recompilacao foi considerada apenas para comparacao, a fim de avaliar a efetividade do
Modulo de Recompilacao.
Os resultados obtidos sao mostrados na Figura - 5.8 (sem recompilacao) e na Figura -
5.9 (com recompilacao). Nessas figuras, cada traco e representado por uma barra dividida
em tres campos que indicam o percentual do tempo em que o fluxo permanece construindo
tracos, compilando e executando codigo nativo. Este percentual foi calculado dividindo
o tempo gasto em cada tarefa pelo tempo somado das tres tarefas, resultando em um
valor que indica a dominancia da tarefa. Baseado nisso, e possıvel definir que a tarefa
126
dominante em cada traco e aquela representada pela maior sub-barra que compoe a barra
do traco.
T1 T1 T1 T2 T1 T2 T10,001%
0,01%
0,1%
1%
10%
100%
ConstruçãoCompilaçãoExecução
T1=(0,004; 0,006; 0,209; 0,050) T1=(0,004; 0,013; 1,958; 0,486)
T2=(0,004; 0,008; 122,965; 0,018)
T1=(0,004; 0,004; 0,005; 289,912)
T2=(0,004; 0,017; 18,223; 0,167)
T1=(0,004; 0,018; 1,733; 1,794) T1=(0,004; 0,013; 4,818; 0,244)
append hanoi nreverse quicksort tak
T1 T2 T3 T1 T2 T3 T4 T1 T2 T1 T2 T3 T4 T5 T6 T7 T8 T9 T10 T110,001%
0,01%
0,1%
1%
10%
100%
ConstruçãoCompilaçãoExecução
T2=(0,004; 0,042; 55,840; 0,049)
T1=(0,004; 0,023; 15,013; 0,201)
T2=(0,004; 0,020; 33,113; 0,080)
T3=(0,004; 0,027; 14,248; 0,243)
T1=(0,004; 0,052; 265,751; 0,072)
T2= (0,004; 0,055; 42,766, 0,466)
T4= (0,004; 0,052; 25,068; 0,763)
T1=(0,004; 0,072; 0,160; 31339,096) T2=(0,004; 0,080; 0,892; 6213,055)
T4=(0,004; 0,060; 13,381; 315,586)
T5=(0,004; 0,028; 24,826; 85,049) T6=(0,004; 0,168; 0,024; 472865,030)
T7=(0,008; 0,152; 1,964; 5374,919) T8=(0,004; 0,156; 1,852; 5699,948)
T9=(0,004; 0,060; 8,133; 519,250) T10= (0,004; 0,016; 0,064; 20619,115)
T11=(0,004; 0,008; 0,016; 49485,875)
binary trees fannkuch fasta k nucleotide
T3= (0,004; 0,052; 0,429; 44,607)
T1= (0,004; 0,050; 0,834; 3,909)
T3=(0,004; 0,016; 55,864; 23,622)
T1 T2 T1 T1 T2 T3 T1 T2 T3 T1 T1 T2 T1 T2 T3 T1 T2 T30,001%
0,01%
0,1%
1%
10%
100%
ConstruçãoCompilaçãoExecução
T1=(0,004; 0,055; 0,729; 32,672)
T1=(0,004; 0,077; 44,439; 0,084)
T1=(0,004; 0,010; 33,390; 0,103)
T2=(0,004; 0,014; 92,423; 0,047)
T3=(0,004; 0,028; 100,079; 0,079)
T2=(0,004; 0,020; 31,643; 0,074)
T3=(0,004; 0,030; 48,376; 0,070)
T1=(0,004; 0,008; 0,348; 3,496)
T1=(0,004; 0,052; 60,360; 0,057)
T1= (0,004; 0,066; 8,915; 0,561)
T2=(0,004; 0,068; 60,316; 0,085)
T1=(0,004; 0,016; 50,482; 0,058)
T2=(0,004; 0,018; 39,354; 0,084)
T3=(0,004; 0,019; 55,405; 0,061)
T2=(0,004; 0,033; 15,206; 0,357)
T3=(0,004; 0,033; 16,654; 0,326)
T1=(0,004; 0,020; 0,004; 818,972)
mandelbrot n-body nsieve nsieve bits partial sum pidigits recursive spectral norm
T2=(0,004; 0,056; 32,685; 0,739)
Figura 5.8: Tempos de construcao, compilacao e execucao dos tracos na configuracaosmart JIT sem recompilacao. Ti representa o traco de numero i. Valoresem vermelho indicam tracos nao uteis.
127
T1 T1 T1 T2 T1 T10,001%
0,01%
0,1%
1%
10%
100%
ConstruçãoCompilaçãoExecução
T1=(0,005; 0,006; 0,212; 0,055) T1=(0,004; 0,010; 2,016; 0,372)
T2=(0,006; 0,004; 123,302; 0,014)
T1=(0,034; 0,004; 0,005; 1473,366) T1=(0,005; 0,070; 20,659; 0,305) T1=(0,004; 0,021; 4,690; 0,372)
append hanoi nreverse quicksort tak
T1 T2 T1 T2 T3 T4 T1 T2 T1 T2 T3 T4 T5 T6 T7 T8 T9 T10 T110,001%
0,01%
0,1%
1%
10%
100%
ConstruçãoCompilaçãoExecução
T2=(0,010; 0,158; 55,728; 0,183)
T1=(0,078; 0,338; 58,998; 0,756)
T2= (0,008; 0,190; 4,804; 4,411)
T1=(0,005; 0,181; 268,706; 0,237)
T2= (0,004; 0,118; 35,863; 1,168)
T3= (0,008; 0,053; 25,178; 0,821)
T1= (641,179; 0,442; 54,691; 120879,278) T2= (312,669; 2,330; 11,464; 283104,998)
T3=(179,205; 1,254; 1,083; 1716446,449) T4= (240,227; 1,494; 3,360; 741276,185)
T5= (112,213; 1,282; 7,189; 162675,149) T6=(91,306; 1,288; 7,260; 131416,610)
T7=(202,111; 1,066; 1,793; 1167844,436) T8=( 339,606; 0,268; 0,077; 45361580,801)
T9=(245,695; 0,012; 0,004; 632914585,253) T10=(145,444; 0,480; 0,045; 33561116,722)
T11=(126,750; 0,254; 0,034; 38487969,858)
binary trees fannkuch fasta k nucleotide
T4=(0,004; 0,059; 0,176; 122,363)
T1=(0,244; 0,041; 0,830; 20,843)
T1 T1 T1 T2 T1 T2 T1 T1 T2 T1 T2 T3 T1 T2 T30,001%
0,01%
0,1%
1%
10%
100%
ConstruçãoCompilaçãoExecução
T1=(0,006; 0,222; 35,393; 0,237)
T1=(0,020; 0,081; 44,020; 0,104)
T1= (0,044; 0,112; 33,456; 1,119)
T2=(0,007; 0,168; 193,793; 0,217)
T2=(0,005; 0,266; 77,642; 0,297)
T1=(0,090; 0,164; 0,647; 33,397)
T1=(0,005; 0,048; 60,497; 0,054)
T1= (0,004; 0,068; 9,006; 0,553)
T2=(0,006; 0,069; 60,097; 0,087)
T1=(0,220; 0,020; 49,327; 0,730)
T2=(0,054; 0,021; 40,115; 0,283)
T3=(0,006; 0,140; 56,426; 0,388)
T2= (0,156; 0,208; 15,255; 3,658)
T3= (0,143; 0,174; 16,475; 2,955)
T1=(0,455; 0,244; 0,076; 1411,250)
mandelbrot n-body nsieve nsieve bits partial sum pidigits recursive spectral norm
Figura 5.9: Tempos de construcao, compilacao e execucao dos tracos na configuracaosmart JIT com recompilacao. Ti representa o traco de numero i. Valoresem vermelho indicam tracos nao uteis.
Alem das barras, essas figuras apresentam, para cada traco, uma quadrupla da forma
Ti(W, X, Y , Z), em que i representa o numero do traco relativo ao programa, W, X,
128
Y representam, respectivamente, o tempo de construcao, compilacao e execucao (em
segundos) e Z o nıvel de nao utilidade do traco. Em especıfico, este ultimo valor foi
calculado pela Formula 5.3 e indica o nıvel de utilidade do traco, isto e, o quanto o traco
e importante para o desempenho do programa. Basicamente, quanto mais proximo de
zero e o nıvel de nao utilidade do traco, maior o tempo em que o fluxo de execucao
mantem executando-o e, consequentemente, maior e a sua efetividade para obtencao de
ganhos de desempenho. Analogamente, quanto maior for o nıvel de nao utilidade do
traco (geralmente, maior do que 1 para o modo sem recompilacao ou maior do que 20
para o modo com recompilacao), maior e a dominancia das outras tarefas (construcao ou
compilacao) e menor e a sua eficiencia na execucao de codigo nativo.
Nıvel de n~ao-utilidade =Tempoconstruindo + Tempocompilando
Tempoexecutando× Tempototal (5.3)
Tempo de execuc~ao util = Tempoexecutando - Tempoconstruindo - Tempocompilando (5.4)
Outra formula utilizada, mas com valores nao implicitamente representados, e a
formula 5.4, que calcula o tempo de execucao util dos tracos ou, em outras palavras,
quanto tempo restou executando o traco depois que o mesmo foi construıdo e compilado.
Um resultado negativo nesta ultima formula indica que o tempo de execucao nao foi
suficiente para amenizar o custo da construcao e compilacao do traco. Tracos com este
tipo de comportamento sao classificados como tracos nao uteis e sao identificados na
Figura - 5.8 e na Figura - 5.9 com suas respectivas quadruplas marcadas em vermelho.
Alem disso, tracos com um tempo de execucao util menor que o nıvel de nao-utilidade
tambem possuem valores marcados em vermelho, por terem sido executados por muito
pouco tempo (na maioria das vezes, menos que 1 segundo).
Primeiramente, em respeito a quantidade de tracos na configuracao sem recompilacao,
em media, foi construıdo 1 traco para os kernels de Prolog e 3 tracos para os programas
do shootout. Dentre todos os programas, hanoi, binary trees, n-body, nsieve, partial
sum e recursive foram os unicos programas que obtiveram desempenho executando todos
os tracos uteis. Os programas nreverse, fannkuch, fasta e spectral norm, apesar de
tambem obterem ganho de desempenho, tiveram ainda um traco nao util construıdo.
Isso mostra que o ganho de desempenho nao e dependente da execucao de tracos uteis
unicamente, mas pode ser otimo se tracos nao uteis puderem ser evitados, principalmente
129
tracos com nıvel de nao-utilidade muito alto, como, por exemplo, o primeiro traco
dos programas nreverse e spectral norm. Nesse sentido, a recompilacao foi muito
importante pois, alem de alcancar um desempenho melhor em geral, pode tambem evitar
que tracos nao uteis fossem construıdos para alguns programas. Esta tecnica detecta
a condicao de saıda de um traco e recompila-o com o novo fluxo de execucao tomado,
permitindo que o fluxo de execucao mantenha mais tempo em codigo nativo. Dentre
os programas avaliados, os programas quicksort e mandelbrot, por exemplo, puderam
obter desempenho devido a eliminacao de tracos nao uteis.
Contudo, mesmo a recompilacao nao foi suficiente para todos os casos. No caso
mais crıtico, que se refere ao programa k nucleotide, por exemplo, a recompilacao nao
evitou a maioria dos tracos nao uteis e mesmo alcancando desempenho sobre o modo
sem recompilacao, nao foi suficiente para superar o modo somente interpretar. Por
essa razao, empregar analise global tambem neste caso pode garantir que mais tracos nao
uteis sejam evitados para outros programas. Com analise global, e possıvel investigar
a estrutura do codigo Prolog e entao presumir a partir de qual iteracao uma estrutura
mudara o seu tipo (por exemplo, quando uma lista se tornara nula), que e uma condicao
para tornar um traco invalido, e entao, verificar a quantidade de iteracoes realizadas ate
a construcao do traco e, a partir disso, definir se compensa compila-lo ou nao.
Outro ponto importante a considerar, em respeito a comparacao entre o modo sem
recompilacao e o modo com recompilacao, e que neste ultimo o aumento do tempo de
construcao e compilacao dos tracos nao prejudicou o desempenho dos programas que
antes tinham alcancado desempenho no modo sem recompilacao (sobre o modo somente
interpretar), mostrando que o custo da recompilacao e mınimo, no caso de programas
com desempenho melhorado. A unica excecao a esta regra e o programa hanoi, mas para
este, o caso se refere as excecoes trataveis unicamente pelo interpretador. Por outro lado,
a compilacao e, principalmente, a construcao dos tracos sao os principais fatores para o
alto tempo de execucao nos casos em que houve queda de desempenho.
De fato, o alto custo de Construtor de Tracos, visivelmente apresentado no programa
k nucleotide no modo com recompilacao, e consequencia tambem do alto custo do
Interpretador Instrumentado. Alem disso, programas com muitos tracos construıdos mos-
traram que o mecanismo de selecao de clausulas principais nao e otimo. O caso para
os tracos que poderiam ser evitados indicam que estes comecaram a ser construıdos no
final de uma cadeia de invocacoes, onde o traco resultante costuma ser menor. Quando
a execucao continua e uma clausula no inıcio da cadeia de invocacoes se torna crıtica, o
traco anteriormente construıdo e descartado, porque neste momento ele fara parte de um
traco maior.
130
Outra situacao que ocorre ainda para o programa k nucleotide (brevemente descrita
na Secao 5.3.2) e a sua alta dinamicidade, que afeta ate mesmo grandes tracos. Esta
dinamicidade tem relacao com a mudanca dos tipos dos termos e clausulas de destino
referenciadas apos a execucao de instrucoes de indexacao e infelizmente nao pode ser
detectada com o mecanismo atual de construcao de tracos implementado. Esse fator
tambem foi responsavel pela alta desaceleracao deste programa em todos os modos, pois
ele invalida tracos anteriormente construıdos, fazendo com que o fluxo de execucao retorne
ao interpretador sempre que esta situacao ocorre.
Adicionalmente, outro problema e que, no sistema desenvolvido, a construcao de tracos
otimos (somente) nao e garantia de desempenho para programas com alta concentracao
de tempo na manipulacao de excecoes. Isso pode ser comprovado a partir da Figura -
5.10 (que mostra a proporcao do tempo em que cada programa necessita para realizar
certas tarefas trataveis unicamente em codigo interpretado, normalmente relacionadas a
manipulacao das regioes de memoria do YAP), que demonstra o porque que determinados
programas, como hanoi e tak, nao puderam alcancar melhoria em todos os modos de
execucao avaliados, mesmo tendo todos os tracos uteis construıdos. Esta figura tambem
demonstra que o alto tempo dessas tarefas para os programas binary tress e spectral
norm foi tambem outro fator que impediu que tais programas nao alcancassem maiores
ganho de desempenho.
appe
ndhan
oi
nreve
rse
quick
sort tak
zebr
a
binar
y tre
es
fannku
chfa
sta
k nucle
otide
mande
lbrot
n-bod
y
nsieve
nsieve
bits
parti
al su
m
pidigi
ts
recu
rsive
spec
tral n
orm
0 %
10 %
20 %
30 %
40 %
50 %
60 %
70 %
80 %
90 %
100 %
Figura 5.10: Taxas do fluxo de execucao na manutencao das areas de memoria dosistema sobre o tempo total de execucao de cada programa avaliado. Osvalores sao praticamente os mesmos em todos os modos de execucao..
Por fim, baseado nos problemas identificados, uma tarefa importante e modificar o
sistema para que o mesmo trate as tarefas de manipulacao das areas de memoria tambem
em codigo nativo, a fim de minimizar este problema e garantir uma execucao eficiente
de todos os programas, desde que todos os tracos construıdos sejam uteis. A tarefa de
131
evitar tracos nao uteis funciona para alguns casos no sistema desenvolvido, desde que a
recompilacao esteja ativada, mas pode ser aprimorada pelo uso de analise global e/ou
online profiling, a fim de detectar clausulas de partida de tracos que estejam no inıcio de
uma cadeia de invocacoes.
Baseado nos resultados obtidos a partir da execucao dos programas avaliados, o uso das
tecnicas supracitadas pode ser suficientes para alcancar maiores ganhos de desempenho.
Entretanto, no caso do programa k nucleotide, em especıfico, e necessario desenvolver
uma tecnica que identifique as mudancas relativas a dinamicidade inerente de Prolog e
usar isso a favor do Construtor de Tracos para que este gere tracos validos por mais tempo.
5.5 Consideracoes Finais
Os modos de execucao de codigo misto desenvolvidos permitiram alcancar um dos
objetivos propostos neste trabalho, pois alcancaram desempenho sobre o modo somente
interpretar para a maioria dos programas. Nesse aspecto, a recompilacao foi muito
importante pois, alem de obter o melhor tempo de execucao, em media, pode tambem
evitar que tracos nao uteis fossem construıdos para alguns programas. Entretanto, o
modo somente compilar requer uma atencao especial em trabalhos futuros, pois este
nao alcancou desempenho em nenhum programa avaliado. A deficiencia deste ultimo
modo e a carencia de profilers no sistema, os quais permitiram coletar informacoes uteis
que auxiliassem a geracao de codigo.
Alem do primeiro objetivo, a construcao (e utilizacao) dos predicados nativos para
coletar perfis de execucao mostraram ser suficientes para avaliar os casos apresentados
neste capıtulo. Com as informacoes coletadas, foi possıvel identificar a eficiencia dos
tracos de execucao em respeito a quantidade por programa e desempenho na execucao. No
melhor caso, 13 dentre todos os programas avaliados obtiveram ganho de desempenho em
modo de execucao com codigo misto, em comparacao a um modo que somente interpreta,
1 nao teve traco construıdo/compilado e 4 nao apresentaram ganho de desempenho.
A principal causa para a perda de desempenho de alguns programas e devido a alta
concentracao do fluxo de execucao no Interpretador Instrumentado que, em media, e 11
vezes mais lento que o Interpretador YAP. Basicamente, a principal razao para essa alta
concentracao e devido a dois importantes fatores: (1) a baixa precisao do sistema em
identificar clausulas iniciais em uma cadeia de invocacoes, onde os tracos construıdos
a partir delas sao maiores e (2) a incapacidade do Construtor de Tracos em identificar
mudancas do comportamento dos programas em tempo de execucao, que invalida qualquer
traco anteriormente construıdo. Alem disso, outra causa para a queda no desempenho
132
(ou obstaculo para maiores ganhos no desempenho) e a execucao obrigatoria em codigo
interpretado para realizacao de tarefas relacionadas a manipulacao das regioes de memoria
do YAP.
Por fim, no decorrer deste capıtulo, diversas ideias para aprimorar o sistema de-
senvolvido foram propostas, sendo as principais a construcao de um novo conjunto de
instrucoes, que permita enviar blocos basicos aos Construtor de Tracos somente
quando necessario, o uso de analise global e o tratamento de excecoes tambem em codigo
nativo. O proximo capıtulo apresenta a conclusao deste trabalho e uma visao mais
detalhada destes e outros trabalhos a serem desenvolvidos futuramente.
133
6
Conclusoes e Trabalhos Futuros
Desde o desenvolvimento do primeiro interpretador Prolog conhecido (Colmerauer e
Roussel, 1996), diversas tecnicas para melhorar a execucao da linguagem foram pro-
postas (Diaz et al., 2012; Hermenegildo et al., 2012; Hickey e Mudambi, 1989; Marien,
1988; Meier, 1990; Taylor, 1996; Turk, 1986; Van Roy, 1989, 1990; Van Roy et al.,
1987). Algumas delas sao aplicaveis aos interpretadores que surgiram posteriormente,
os quais consistem da maioria das implementacoes existentes atualmente, mas certamente
a geracao de codigo nativo juntamente com a analise global e a tecnica que alcanca melhor
desempenho conhecido.
A analise global aplicada a coleta de informacoes para especializar a geracao de codigo
foi considerada util para implementacoes Prolog a partir dos trabalhos de Mellish em
1981 e 1985 (Mellish, 1981, 1985) e tambem de Warren et al. (1988). Depois disso, os
resultados apresentados serviram como uma motivacao para os trabalhos de Van Roy, com
o sistema Aquarius (Van Roy, 1990; Van Roy e Despain, 1992), e Taylor, com Parma
(Taylor, 1996), confirmando a eficiencia da analise global em derivar tipos e consequente
geracao de codigo de boa qualidade, que serviram para convergir nos otimos resultados
obtidos por estes sistemas.
Infelizmente, apesar dos resultados, Aquarius e Parma foram descontinuados porque
analise global e geracao de codigo representam alta complexidade na manutencao dos
sistemas. Por outro lado, YAPc (Silva e Costa, 2007) surgiu como uma proposta de
compilador JIT simples de manter, que substitui o uso da analise global por informacoes
coletadas dinamicamente, enquanto ainda compila codigo para regioes de codigo frequen-
tes utilizando esse tipo de informacoes. Esse compilador foi um prova de conceito de que
vale o custo de investir nesse tipo de estrategia na implementacao de sistemas Prologmais
134
eficientes e embora nao tenha sido acoplado a qualquer interpretador em uso atualmente,
este serviu como a principal motivacao para o desenvolvimento deste trabalho.
A proxima secao apresenta as motivacoes que levaram ao projeto e implementacao
deste trabalho. Logo depois, sao apresentados uma sıntese do trabalho, uma breve
discussao sobre os resultados alcancados, os trabalhos futuros e as consideracoes finais.
6.1 Motivacoes
YAPc foi desenvolvido como modelo conceitual de compilacao JIT para Prolog e mostrou
que e possıvel alcancar melhorias de desempenho sem o uso de analise global. Contudo, no
que se refere o uso de tecnicas de compilacao JIT, YAPc e muito limitado: a selecao de
unidades e baseada em regioes e a quantidade de otimizacoes de codigo e muito reduzido.
Alem disso, o alocador de registradores e baseado em coloracao de grafo, que nao e
indicado no desenvolvimento de compiladores JIT devido ao seu alto custo.
Somado a isso, sao conhecidas poucas implementacoes de compilacao JIT para Prolog.
Na verdade, apenas YAPc e conhecido pela comunidade cientıfica e todos os demais com-
piladores JIT existentes foram projetados para linguagens essencialmente nao declarativas.
Isso significa que toda pesquisa imposta na busca de tecnicas de compilacao JIT mais
eficientes nao sao garantidas ate que as tais sejam efetivamente empregadas no contexto
desejado. Portanto, nao somente YAPc com suas caracterısticas e limitacoes se tornou
motivacao para a realizacao deste trabalho, mas tambem a necessidade de direcionar as
pesquisas realizadas no campo da linguagem Prolog.
Portanto, em respeito a primeira motivacao, o fato de projetar o sistema deste trabalho
com diversas configuracoes possıveis, como diversos modos de execucao e parametros de
frequencia, e pela limitacao das pesquisas no campo de compilacao JIT para Prolog,
conforme citado anteriormente. Por outro lado, a ultima motivacao e o que justifica
os predicados implementados neste trabalho, pois os mesmos facilitam, por meio de
experiencias realizadas, a identificar a melhor configuracao do sistema (no sentido de
alcancar o melhor desempenho possıvel), alem de facilitar a depuracao de codigo.
6.2 A Nova Geracao do YAP
A arquitetura da nova geracao do YAP consiste de diversos modulos independentes
que se inter-relacionam para a manutencao de um sistema que compila codigo em
tempo de execucao. O sistema desenvolvido prove quatro modos de execucao: somente
135
interpretar, somente compilar, compilac~ao contınua e smart JIT, dos quais, os dois
ultimos suportam execucao em codigo misto.
Alem de suportar execucao em codigo misto, os modos smart JIT e compilac~ao
contınua empregam um interpretador especial, o qual e capaz de emitir blocos basicos
recem executados para outro modulo especıfico, chamado Construtor de Tracos, que
constroi os tracos de execucao dos programas para que codigo nativo sejam gerados para
eles. Por outro lado, o modo somente compilar trata como unidades de compilacao uma
clausula recem-invocada porque, para este, nao ha como construir tracos de execucao.
A vantagem dos modos de execucao com codigo unico (somente interpretar e
somente compilar) e que estes nao possuem o custo adicional de invocar o Monitor,
o que nao ocorre com modos de execucao de codigo misto, nos quais este componente
e necessario para detectar clausulas crıticas. As clausulas crıticas sao pontos iniciais
dos tracos, onde o Construtor de Tracos comeca a tratar os blocos basicos emitidos pelo
interpretador. Os tracos sao construıdos como um grafo de fluxo de controle, cujos nos
sao compostos somente de blocos basicos efetivamente executados. Outra vantagem dos
modos de execucao de codigo unico e que tambem nao e necessario invocar o Interpretador
Instrumentado, embora o modo somente compilar, em especıfico, uma maior atencao seja
necessaria para reduzir sua baixa eficiencia demonstrada.
6.3 Resultados Alcancados
Um dos objetivos deste trabalho foi alcancado ao demonstrar que todos os programas
que construıram somente tracos uteis obtiveram ganho de desempenho sobre o modo
somente interpretar. Na verdade, as unicas excecoes sao para os programas append,
hanoi e tak que so nao alcancaram desempenho devido a grande quantidade de dados
manipulada e ao alto tempo gasto no coletor de lixo (exceto em um ou outro modo
de execucao) que, no sistema implementado e tratado unicamente no interpretador (no
Interpretador Instrumentado nos modos de codigo misto e Interpretador YAP nos demais
modos). O conceito de tracos nao uteis foi apresentado na Secao 5.4 e a execucao destes
foi responsavel para que alguns programas, como quicksort, k nucleotide, mandelbrot
e nsieve bits, obtivessem altos ındices de desaceleracao.
Nesse aspecto, a recompilacao se mostrou muito importante na execucao dos pro-
gramas pois, alem de executar os programas de forma mais eficiente (tanto no modo
Smart JIT como compilac~ao contınua) foi tambem capaz de evitar que tracos nao uteis
fossem construıdos para alguns programas, como foi para o programa mandelbrot, que
136
pode variar o tempo de execucao de 405, 2 segundos (sem recompilacao) para apenas 35, 4
segundos em smart JIT, por exemplo.
Por fim, o custo relacionado a criacao de tracos nao uteis e atribuıdo ao Interpretador
Instrumentado, que apesar de permitir a criacao de tracos pela emissao de blocos, tem um
alto custo em relacao ao Interpretador YAP, que e padrao no sistema. Na verdade, isso e
consequencia de outros problemas, que estao relacionados a construcao de tracos que se
iniciam em clausulas no final de uma cadeia de invocacoes ou na falta de mecanismos que
possibilitem identificar a mudanca de comportamento dos programas.
Dessa forma, a proxima secao apresenta propostas de trabalhos futuros que tem como
principal objetivo melhorar o desempenho da proposta desenvolvida neste trabalho, por
meio da resolucao dos problemas identificados. Portanto, todas as propostas sao baseadas
em ideias de implementacao discutidas no decorrer do Capıtulo 5.
6.4 Trabalhos Futuros
Como visto nos resultados apresentados no Capıtulo 5, os programas append, hanoi,
quicksort, tak, k nucleotide, mandelbrot, nsieve, nsieve bits e pidigits apre-
sentaram uma perda de desempenho nos modo de execucao smart JIT e compilac~ao
contınua (quicksort, mandelbrot e nsieve bits so tiveram perda na configuracao sem
recompilacao). Alem disso, todos os programas apresentaram perda de desempenho no
modo somente compilar. No decorrer do Capıtulo 5 varias ideias foram introduzidas com
o proposito de que o sistema alcance desempenho para todos os programas, independente
do modo de execucao ativo.
A Figura - 6.1 apresenta uma extensao da arquitetura proposta a fim de suportar novas
funcionalidades, que poderao ser implementadas como trabalhos futuros. O objetivo
desta segunda geracao consiste em melhorar os perfis de execucao apresentados nos
resultados e obter desempenho para possivelmente todos os programas em todos os modos
de execucao desenvolvidos. Em seguida, sao apresentadas as propostas para trabalhos
futuros. Primeiramente, sao apresentadas as modificacoes a serem realizadas nos modulos
ja existentes. Em seguida, sao apresentados os novos componentes.
6.4.1 Gerente de Codigo
No sistema desenvolvido, o Gerente de Codigo e o componente responsavel por gerenciar
as areas de codigo, interagir com o Compilador JIT e enviar a versao correta de codigo para
o Motor de Execucao executar. Uma nova proposta consiste em estender este componente
137
Gerentede código
Motor de Execução
Monitor
Compilador JIT
Fila de Compilação
Arquitetura YAPCódigoYAAM
CódigoInterm.
Códigonativo
Áreas de código
Áreas de código
Inicialização das áreas decódigo e do sistema em geral
Código compilado
Requisição decompilação e
código compilado
Versão correta decódigo para executar
Gerenciamento das áreasde código (busca e inserção)
Início da interpretação
Bibliotecas Engine Compilador
InterpretadorYAP Construtor
de Traços
Blocosbásicos
Traços deexecução
Instrumentaçãodas cláusulas
OfflineProfiler
OnlineProfiler
Cache
Informações coletadasem tempo de execução
Informações coletadasem tempo de compilação ou carga
Informaçõescoletadas
Código nativopara armazenar
Código nativopara armazenar
Informações coletadaspara armazenar
Bibliotecas Engine Compilador
Gerentede código
Motor de Execução
Compilador JIT
Construtorde Traços
OfflineProfiler
OnlineProfiler
Cache
Figura 6.1: Arquitetura proposta para trabalhos futuros.
para que, no final da execucao, ele envie os tracos (ou clausulas) contidos dentro da Area
de Codigo Nativo para a Cache.
6.4.2 Compilador JIT
O Compilador JIT e responsavel por gerar codigo nativo em quatro etapas distintas:
pre-processamento, traducao, analises e transformacoes de codigo e geracao de codigo.
Dentre elas, a traducao transforma codigo intermediario por meio de um parser externo.
Embora nao tenha um impacto significativo no tempo de execucao final, uma das
propostas e descartar o uso deste parser e construir outro que seja capaz de traduzir o
codigo YAAM dos tracos diretamente para codigo LLVA, que e a representacao de entrada
para a etapa de analises e transformacoes de codigo). Uma consequencia disso e a troca
da Area de Codigo Intermediario pela Area de Armazenamento de Tracos, cuja funcao e
armazenar os tracos de execucao construıdos. A principal razao para o uso dessa estrategia
esta relacionado simplesmente a elegancia do sistema, e nao ao desempenho.
Neste contexto, outro trabalho futuro e aprimorar o Compilador JIT para que este
gere codigo nativo de maior qualidade. Portanto, outra proposta consiste em aprimorar
este componente integrando a ele um sistema de compilacao adaptativa, similar a Jikes
138
RVM (Arnold et al., 2000; Burke et al., 1999). Um sistema de compilacao adaptativa
permite que o compilador selecione automaticamente o conjunto de otimizacoes de codigo
mais adequado para o programa em execucao. A diferenca para com Jikes RVM, e
que, em vez de criar um conjunto pre-determinado de otimizacoes para aplicar aquele
mais adequado para o programa em execucao, a proposta original e capacitar o sistema
para que ele ative e desative otimizacoes individuais, e ordene da melhor maneira possıvel
aquelas que precisam ser aplicadas durante a geracao de codigo. Pesquisas nesse campo
ainda sao recentes e a maioria delas requerem que os programa sejam executados ao menos
uma vez para que um padrao adequado de otimizacoes seja definido (Agakov et al., 2006;
Cavazos e O’Boyle, 2005, 2006; Hoste et al., 2010; Triantafyllis et al., 2003). Contudo,
tais trabalhos demonstraram que a correta escolha das otimizacoes que eram aplicadas
ocasionaram ganhos de desempenho.
6.4.3 Motor de Execucao
O Motor de Execucao da segunda geracao pode ser modificado para suportar as alteracoes
descritas ao longo do Capıtulo 5. Essa modificacao consiste em retirar o Interpretador
Instrumentado e modificar o Interpretador YAP para que este ultimo tambem emita os
blocos basicos ao Construtor de Tracos. A ideia consiste em executar as instrucoes YAAM
padrao do sistema ate que alguma clausula crıtica seja detectada. A partir deste ponto, o
program counter (registrador P) e incrementado com um deslocamento adicional para que
as instrucoes da clausula crıtica, bem como de todas as clausulas invocadas por ela, sejam
executadas como instrucoes profiled, que sao instrumentadas com macros que emitem
os blocos basicos executados. Ao compilar o traco, o deslocamento e desconsiderado,
garantindo que toda excecao ocorrida em codigo nativo seja tratado nas instrucoes YAAM
padrao.
Outro trabalho importante estaria relacionado ao tempo de espera para que as
clausulas do programa se tornem crıticas. Idealmente isso pode ser feito considerando,
primeiramente, a seguinte heurıstica: se um programa leva t segundos para ser compilado,
entao um programa com o dobro do tamanho levaria o dobro do tempo para ser
compilado, considerando as mesmas otimizacoes de codigo. A heurıstica estabelecida
pode nao ser intuitiva em alguns (ou todos os) casos, mas nao deixa de ser uma ideia
valida. Depois disso, o sistema poderia utilizar calculo amostral (Cochran, 2007) e,
em conjunto com a heurıstica anteriormente citada, predizer o tempo de compilacao
de qualquer programa. Calculo amostral permite determinar os valores medios (neste
caso, o tempo de compilacao) para uma dada populacao (todos os programas), baseado
139
em uma pequena amostra (poucos programas), que possuam as mesmas caracterısticas
(os mesmos tamanhos medios). Isso conduziria o sistema proposto para um sistema
adaptativo com o tempo, com a possibilidade de realizar melhores previsoes na medida em
que os programas sao executados. Como vantagem, isso poderia, por exemplo, determinar
o limite de tempo ideal para interpretar uma clausula quando oMonitor estiver configurado
para utilizar fracao de tempo para detectar clausulas crıticas (e quentes). Alem disso, o
Monitor poderia ser integrado com outros dois tipos de parametros de frequencia, que
sao crossover e balance (que foram apresentados na Secao 2.3.2), visto que os mesmos
requerem, como parametro, o tempo de compilacao das unidades.
Por fim, outras propostas consistem em aprimorar o modo de execucao somente
compilar para que este suporte manipulacao de excecao no codigo nativo, que atualmente
e feita no interpretador, permitir a emissao de blocos basicos relativos aos trechos de codigo
que manipulam o coletor de lixo, atualmente realizada tambem no interpretador, para
todos os modos, e integrar dois novos modos de execucao: anotador e compile-quente.
Anotador
No modo de execucao anotador, os programas sao interpretados do inıcio ao fim. O Com-
pilador JIT e desabilitado e um novo componente, o Online Profiler, monitora a execucao
dos programas para coletar informacoes em tempo de execucao. Essas informacoes
sao armazenadas no arquivo contendo o codigo Prolog do programa executado e sao
posteriormente recuperadas no modo de execucao compile-quente.
Compile-Quente
No modo compile-quente as informacoes coletadas no modo anotador sao recuperadas
e utilizadas para gerar codigo especializado na primeira invocacao das clausulas. Embora
todas as informacoes possam ser utilizadas em uma unica especializacao, a ideia original
e permitir que o usuario escolha quais informacoes recuperar e utilizar.
6.4.4 Offline Profiler
O Offline Profiler e um componente a ser implementado em versoes posteriores, como um
modulo para aplicar analise global aos programas que serao executados. Analise global
foi mencionada no decorrer de todo o Capıtulo 5 como uma tecnica primordial para que
o sistema alcance desempenho para todos os programas executados. Ela pode ser util nos
seguintes casos:
140
• Encontrar uma clausula dentro de uma cadeia de chamadas que possibilita o sistema
construir, a partir de tal clausula, o maior traco de execucao possıvel (traco otimo);
• Inferir as instrucoes de indexacao que serao invocadas entre uma clausula e outra.
Isso retiraria a necessidade de manter tais instrucoes no corpo dos tracos ou, no
mınimo, possibilitaria a emissao de blocos basicos para elas.
• Inferir, em tempo de carga do programa, a quantidade de vezes que determinada
clausula sera invocada de acordo com o tamanho da entrada do programa. Essa
tatica funciona em programas onde a condicao de parada de uma cadeia de in-
vocacoes e dependente de uma instrucao condicional que contem, como um dos
operandos, a entrada do programa.
• Inferir, em tempo de carga do programa, a quantidade de vezes que determinada
clausula sera invocada de acordo com os tipos das estruturas. Essa tatica funciona
em programas onde a condicao de parada de uma cadeia de invocacoes e dependente
de uma estrutura que, com o tempo, certamente mudara o seu tipo, por exemplo,
uma estrutura do tipo lista que, apos algumas iteracoes se tornara nula.
• Inferir, em tempo de compilacao ou carga, traco(s) de execucao uteis quando o modo
de execucao ativo for somente compilar.
6.4.5 Online Profiler
O Online Profiler, assim como o Offline Profiler, tem por objetivo encontrar uma clausula
dentro de uma cadeia de chamadas cujo traco de execucao seja maior se construıdo a
partir dela. Em modos de execucao com codigo misto isto se torna possıvel ao investigar
o fluxo de execucao antes de qualquer clausula se tornar crıtica. Usando essa mesma
abordagem, o Online Profiler pode descobrir quais instrucoes de indexacao sao invocadas
entre as clausulas, tornando possıvel que a emissao de blocos tambem ocorra para eles.
Em casos melhores isso permitiria eliminar instrucoes de indexacao dentro do corpo dos
tracos construıdos.
Alem de tais informacoes serem uteis em modos de execucao com codigo misto, estas
podem ser coletadas no modo anotador e recuperadas no modo compile-quente. Outras
informacoes coletadas, porem somente no modo anotador, sao:
• Tipos de variaveis. Programas Prolog podem variar os tipos das variaveis em tempo
de execucao, o que significa que todas as instrucoes YAAM sao projetadas para
141
suportar todos os tipos existentes. O uso de informacoes sobre os tipos de variaveis
permite que as instrucoes YAAM sejam especializadas para um tipo especıfico;
• Existencia de variaveis e sucesso na desreferenciacao. Algumas instrucoes YAAM
verificam a existencia de variaveis e saltam para uma instrucao em modo de escrita
quando a operacao de desreferenciacao e executada com sucesso. Tais informacoes,
portanto, evitam que algumas checagens sejam ignoradas;
• Realizacao de corte. Quando uma operacao de corte tem sucesso, determinadas
instrucoes YAAM que estruturam a clausula nao sao executadas. Saber se uma
operacao de corte tera sucesso permite que o compilador gere codigo menor ao
remover as instrucoes que nao serao executadas.
• Tempo de compilacao. Coletar o tempo de compilacao de determinadas clausulas
(ou tracos) oferece outra possibilidade de configurar o Monitor para usar crossover
e balance, sem necessitar empregar calculo amostral.
E importante notar que, como Prolog permite a criacao de programas nao-determinısticos,
todas as informacoes coletadas podem ser facilmente modificadas durante a execucao.
Na pratica, isso reflete a uma modificacao constante em algumas informacoes coletadas,
como os tipos das variaveis e o modo de execucao ativo. Portanto, e importante no
desenvolvimento do Online Profiler incluir um mecanismo para verificar a validade das
informacoes coletadas. Descobrir que uma informacao mudou indica que um traco (ou
clausula) especializado(a) se tornou invalido(a) e precisa ser recompilado(a) com a nova
informacao.
Alem disso, outra funcao deste componente e realizar a tarefa que, no sistema
desenvolvido, cabe ao Profiler.
6.4.6 Cache
Com uma Cache o sistema pode ser capaz de armazenar as clausulas anteriormente
compiladas em disco, para que estas sejam reutilizadas em execucoes posteriores do
sistema. As clausulas compiladas sao mantidas em memoria ate o final de execucao,
quando sao entao armazenadas no disco rıgido. Com este componente, e possıvel eliminar
o custo relacionado as etapas anteriores de compilacao, isto e, instrumentacao de codigo,
execucao em modo interpretado e profiling, buscando diretamente o codigo nativo no inıcio
da execucao do programa.
A ideia basica por tras da Cache e simples: toda clausula ou traco compilado e
instrumentado(a) com um cabecalho que contem duas informacoes: uma flag que indica de
142
onde o codigo nativo foi proveniente (de clausula ou traco) e uma estrutura que armazena
a estrutura do codigo. Se o codigo foi proveniente de um traco compilado, essa estrutura
armazena os blocos basicos perfilados (e consequentemente compilados). Por outro lado,
se o codigo foi proveniente de uma clausula inteira (no caso do modo de execucao somente
compilar), essa estrutura armazena as instrucoes YAAM que compoem a clausula.
Dessa forma, nao importa se duas clausulas possuem um nome diferente, desde que a
clausula em execucao possua a mesma estrutura de outra ja armazenada em disco, a
clausula armazenada sera recuperada e imediatamente executada. Vale ressaltar que, em
vista disso, e necessario que o programa seja executado pelo menos uma vez no modo
interpretado para que a sua estrutura seja verificada.
Alem de eliminar o custo relacionado a interpretacao, profiling e compilacao, outra
vantagem da Cache e a possibilidade de compartilhar clausulas comuns entre os programas.
Programas que ainda nao foram executados podem se beneficiar da execucao nativa
imediata se sua composicao conter clausulas ja compiladas durante a execucao de outros
programas.
6.5 Consideracoes Finais
O primeiro trabalho relacionado a compilacao JIT surgiu em 1960 com McCarthy sobre
a linguagem LISP (McCarthy, 1960), em que foi verificado as primeiras possibilidades de
gerar codigo nativo em tempo de execucao a partir de um interpretador. Depois disso,
Dakin e Poole (1973) foi o primeiro trabalho a apresentar um sistema de compilacao JIT
de codigo misto, que foi baseado em medidas empıricas anteriormente apresentadas por
Knuth (1971), que afirmava que a maior parte da execucao de um programa se concentrava
em uma pequena parcela de codigo.
Mais tarde, Hansen (1974) formulou tres fundamentos basicos que formam um com-
pilador JIT eficiente. Dentre eles, a forma de selecao de unidades quentes de codigo, bem
como da estrutura de tais unidades, foram largamente estudadas ao longo dos anos e,
atualmente, diversos trabalhos acerca de tais assuntos sao conhecidos. Contudo, dentre
os princıpios apresentados por Hansen, a forma de como as unidades quentes devem ser
compiladas (ou seja, quais otimizacoes devem ser aplicadas), ainda carece de estudos. O
que comprova essa afirmacao e o fato de que a maioria dos sistemas de compilacao JIT
conhecidos aplicam otimizacoes de codigo de forma estatica. Alem disso, outro fato que
comprova e o fato de que sistemas que utilizam compilacao adaptativa, a exemplo, Jikes
RVM (Arnold et al., 2000; Burke et al., 1999), alcancam melhor desempenho na execucao
143
quando otimizacoes de codigo sao aplicadas conforme o comportamento dos programas
durante a execucao.
Outro ponto consideravel, que indica a permanencia dos estudos acerca de compilacao
JIT e o surgimento de maquinas com capacidade de computacao paralela, que leva
as pesquisas na area a outro nıvel, principalmente quanto a forma que as tecnicas ja
desenvolvidas devem ser empregadas no sistema a fim de que a capacidade de computacao
desse tipo de maquina seja aproveitada da melhor forma possıvel. Atualmente, poucos
sistemas de compilacao JIT recorrem ao paralelismo (Arnold et al., 2000; Paleczny et
al., 2001; Suganuma et al., 2004), os quais seguem o modelo apresentado por (Plezbert e
Cytron, 1997). Contudo, um modelo teorico mais recente sobre o assunto foi publicado
somente em 2011, por Kulkarni.
Com relacao a Prolog, tambem e valido dizer que as pesquisas precisam nao somente
permanecer, como tambem serem aperfeicoadas, pelo fato de que a maioria das publicacoes
produzidas ate o momento tiveram foco na implementacao de linguagens imperativas.
Afinal, os resultados apresentados neste trabalho, bem como no seu antecessor (Silva
e Costa, 2007), mostraram que empregar compiladores JIT em interpretadores Prolog
realmente geram desempenho (embora nem para todos os programas). De fato, se tecnicas
elaboradas nao forem utilizadas, dificilmente os resultados alcancados serao compatıveis
com aqueles obtidos em sistemas para linguagens imperativas devido, principalmente, a
estrutura e organizacao diferenciadas de Prolog, que limitam diversas especializacoes de
codigo sem o uso de informacoes especıficas.
144
REFERENCIAS
Abrams, P. S. An APL Machine. Tese de Doutoramento, Stanford, CA, USA, 1970.
Adve, V.; Lattner, C.; Brukman, M.; Shukla, A.; Gaeke, B. LLVA: A
Low-level Vitual Instruction Set Architecture. In: Proceedings of the 36th annual
IEEE/ACM International Symposium on Microarchitecture (MICRO-36), San Diego,
California: IEEE Computer Society, 2003, p. 205–216.
Agakov, F.; Bonilla, E.; Cavazos, J.; Franke, B.; Fursin, G.; O’Boyle, M.
F. P.; Thomson, J.; Toussaint, M.; Williams, C. K. I. Using Machine Learning
to Focus Iterative Optimization. In: Proceedings of the International Symposium on Code
Generation and Optimization, Washington, DC, USA: IEEE Computer Society, 2006, p.
295–305.
Aho, A. V.; Johnson, S. C.; Ullman, J. D. Code Generation for Expressions with
Common Subexpressions. Journal of the ACM, v. 24, p. 146–160, 1977.
Aho, A. V.; Sethi, R.; Ullman, J. D. Compilers: Principles, Techniques, and
Tools. Boston, MA, USA: Addison-Wesley Longman Publishing Co., Inc., 1986.
Ancona, D.; Ancona, M.; Cuni, A.; Matsakis, N. D. RPython: a Step Towards
Reconciling Dynamically and Statically Typed OO Languages. In: Proceedings of the
Symposium on Dynamic Languages, Montreal, Quebec, Canada: ACM, 2007, p. 53–64.
Arnold, K.; Gosling, J.; Holmes, D. The Java(TM) Programming Language (4th
Edition). Addison-Wesley Professional, 2005.
Arnold, M.; Fink, S.; Grove, D.; Hind, M.; Sweeney, P. F. Adaptive Optimiza-
tion in the Jalapeno JVM. In: Proceedings of the ACM Conference on Object-Oriented
Programming, Systems, Languages, and Applications, Minneapolis, Minnesota, United
States: ACM, 2000, p. 47–65.
145
Arnold, M.; J., F. S.; D., G.; M., H.; F., S. P. A Survey of Adaptive Optimization
in Virtual Machines. In: Proceedings of the IEEE. Special Issue on Program Generation,
Optimization, and Adaptation, 2004.
Aıt-Kaci, H. Warren’s Abstract Machine – A Tutorial Reconstruction. Cambridge:
MIT Press, 1991.
Aycock, J. A Brief History of Just-In-Time. ACM Computing Surveys, v. 35,
p. 97–113, 2003.
Bacon, D. F.; Graham, S. L.; Sharp, O. J. Compiler Transformations for
High-Performance Computing. ACM Computing Surveys, v. 26, p. 345–420, 1994.
Bala, V.; Duesterwald, E.; Banerjia, S. Dynamo: a Transparent Dynamic
Optimization System. In: Proceedings of the ACM Conference on Programming Language
Design and Implementation, Vancouver, British Columbia, Canada: ACM, 2000, p. 1–12.
Bolz, C. F.; Cuni, A.; Fijalkowski, M.; Rigo, A. Tracing the Meta-Level:
PyPy’s Tracing JIT Compiler. In: Proceedings of the Workshop on the Implementation,
Compilation, Optimization of Object-Oriented Languages and Programming Systems,
Genova, Italy: ACM, 2009, p. 18–25.
Bonzini, P. GNU Lightning. Disponıvel em http://www.gnu.org/s/lightning/
manual/lightning.html. Acesso em: 13 set. 2012, ????
Brandis, M. M.; Mossenbock, H. Single-pass Generation of Static
Single-Assignment Form for Structured Languages. ACM Transactions on Programming
Languages and Systems, v. 16, p. 1684–1698, 1994.
Brown, P. J. Throw-Away Compiling. Software: Practice and Experience, v. 6, n. 3,
p. 423–434, 1976.
Bruening, D.; Duesterwald, E. Exploring Optimal Compilation Unit Shapes
for an Embedded Just-In-Time Compiler. In: Proceedings of the ACM Workshop on
Feedback-Directed and Dynamic Optimization, Monterey, California, 2000, p. 13–20.
Burke, M. G.; Choi, J.-D.; Fink, S.; Grove, D.; Hind, M.; Sarkar, V.;
Serrano, M. J.; Sreedhar, V. C.; Srinivasan, H.; Whaley, J. The Jalapeno
Dynamic Optimizing Compiler for Java. In: Proceedings of the ACM Conference on Java
Grande, San Francisco, California, United States: ACM, 1999, p. 129–141.
146
Campanoni, S.; Agosta, G.; Reghizzi, S. C. A Parallel Dynamic Compiler for CIL
Bytecode. SIGPLAN Notices, v. 43, p. 11–20, 2008.
Campanoni, S.; Agosta, G.; Reghizzi, S. C.; Di Biagio, A. A Highly Flexible,
Parallel Virtual Machine: Design and Experience of ILDJIT. Software–Practice &
Experience, v. 40, p. 177–207, 2010.
Campanoni, S.; Sykora, M.; Agosta, G.; Reghizzi, S. C. Dynamic Look Ahead
Compilation: A Technique to Hide JIT Compilation Latencies in Multicore Environment.
In: Proceedings of the International Conference on Compiler Construction: Held as Part
of the Joint European Conferences on Theory and Practice of Software, Berlin, Heidelberg:
Springer-Verlag, 2009, p. 220–235.
Carlsson, M. Freeze, Indexing, and Other Implementation Issues in the WAM. In:
Proceedings of the International Conference on Logic Programming, MIT Press, 1987, p.
40–58.
Carlsson, M. On the Efficiency of Optimising Shallow Backtracking in Compiled
Prolog. In: Proceedings of the International Conference on Logic Programming, Lisbon,
Portugal: MIT Press, 1989, p. 3–16.
Carlsson, M.; Mildner, P. SICSTUS Prolog – The First 25 Years. Theory and
Practice of Logic Programming, v. 12, n. 1-2, p. 35–66, 2012.
Casanova, M. A.; Giorno, F. A. C.; Furtado, A. L. Programacao em Logica e
a Linguagem Prolog. 1 ed. Sao Paulo, Brasil: Edgard Blucher LTDA, 1987.
Cavazos, J.; O’Boyle, M. F. P. Automatic Tuning of Inlining Heuristics. In:
Proceedings of the ACM/IEEE Conference on Supercomputing, Washington, DC, USA:
IEEE Computer Society, 2005, p. 14–24.
Cavazos, J.; O’Boyle, M. F. P. Method-Specific Dynamic Compilation Using
Logistic Regression. In: Proceedings of the ACM Conference on Object-oriented
Programming Systems, Languages, and Applications, Portland, Oregon, USA: ACM, 2006,
p. 229–240.
Chambers, C.; Ungar, D. Customization: Optimizing Compiler Technology for
SELF, a Dynamically-Typed Object-Oriented Programming Language. In: Proceedings
of the ACM Conference on Programming language Design and Implementation, Portland,
Oregon, United States: ACM, 1989, p. 146–160.
147
Chambers, C.; Ungar, D. Iterative Type Analysis and Extended Message Splitting:
Optimizing Dynamically-Typed Object-Oriented Programs. In: Proceedings of the
Conference on Programming Language Design and Implementation, 1990, p. 150–164.
Chambers, C. D. The Design and Implementation of the SELF Compiler, an Opti-
mizing Compiler for Object-Oriented Programming Languages. Tese de Doutoramento,
Department of Computer Science, Stanford, CA, USA, 1992.
Chang, M.; Smith, E.; Reitmaier, R.; Bebenita, M.; Gal, A.; Wimmer, C.;
Eich, B.; Franz, M. Tracing for Web 3.0: Trace Compilation for the Next Generation
Web Applications. In: Proceedings of the ACM International Conference on Virtual
Execution Environments, Washington, DC, USA: ACM, 2009, p. 71–80.
Chen, W.; Warren, D. S. Query Evaluation under the Well-founded Semantics.
In: Proceedings of the ACM SIGACT-SIGMOD-SIGART Symposium on Principles of
Database Systems, Washington, D.C., United States: ACM Press, 1993, p. 168–179.
Cierniak, M.; Lueh, G.-Y.; Stichnoth, J. M. Practicing JUDO: Java Under
Dynamic Optimizations. In: Proceedings of the ACM Conference on Programming
language Design and Implementation, Vancouver, British Columbia, Canada: ACM, 2000,
p. 13–26.
Cochran, W. G. Sampling Techniques. 3 ed. Wiley India Pvt. Limited, 2007.
Colmerauer, A. An introduction to prolog iii. Commun. ACM, v. 33, n. 7, p. 69–90,
1990.
Colmerauer, A.; Roussel, P. History of programming languages. New York, NY,
USA: ACM, 331–367 p., 1996.
Conway, T.; Henderson, F.; Somogyi, Z. Code Generation for Mercury. In:
Proceedings of International Logic Programming Symposium, Portland, Oregon, USA,
1995, p. 242–256.
Disponıvel em citeseer.nj.nec.com/conway94code.html
Costa, V. S. Optimising Bytecode Emulation for Prolog. In: Proceedings of the
International Conference on Principles and Practice of Declarative Programming, London,
UK, UK: Springer-Verlag, 1999, p. 261–277.
148
Costa, V. S. On Just in Time Indexing of Dynamic Predicates in Prolog. In:
Proceedings of the Portuguese Conference on Artificial Intelligence: Progress in Artificial
Intelligence, Berlin, Heidelberg: Springer-Verlag, 2009, p. 126–137.
Costa, V. S.; Rocha, R.; Damas, L. The YAP Prolog System. Theory and Practice
of Logic Programming, v. 12, n. 1-2, p. 5–34, 2012.
Costa, V. S.; Sagonas, K.; Lopes, R. Demand-driven Indexing of Prolog Clauses.
In: Proceedings of the International Conference on Logic Programming, Porto, Portugal:
Springer-Verlag, 2007, p. 395–409.
Cramer, T.; Friedman, R.; Miller, T.; Seberger, D.; Wilson, R.; Wolczko,
M. Compiling Java Just in Time. IEEE Micro, v. 17, p. 36–43, 1997.
Cytron, R.; Ferrante, J.; Rosen, B. K.; Wegman, M. N.; Zadeck, F. K.
Efficiently Computing Static Single Assignment Form and the Control Dependence Graph.
ACM Transactions on Programming Languages and Systems, v. 13, p. 451–490, 1991.
Dakin, R. J.; Poole, P. C. A Mixed Code Approach. The Computer Journal, v. 16,
n. 3, p. 219–222, 1973.
Deutsch, L. P.; Schiffman, A. M. Efficient Implementation of the Smalltalk-80
System. In: Proceedings of the ACM Symposium on Principles of Programming
Languages, Salt Lake City, Utah, United States: ACM, 1984, p. 297–302.
Diaz, D.; Abreu, S.; Codognet, P. On the Implementation of GNU Prolog. Theory
and Practice of Logic Programming, v. 12, n. 1-2, p. 253–282, 2012.
Flynt, C. Tcl/Tk: A Developer’s Guide. 1 ed. New York, USA: Elsevier Science,
2012.
Gal, A.; Eich, B.; Shaver, M.; Anderson, D.; Mandelin, D.; Haghighat,
M. R.; Kaplan, B.; Hoare, G.; Zbarsky, B.; Orendorff, J.; Ruderman,
J.; Smith, E. W.; Reitmaier, R.; Bebenita, M.; Chang, M.; Franz, M.
Trace-Based Just-In-Time Type Specialization for Dynamic Languages. In: Proceedings
of the ACM Conference on Programming language Design and Implementation, Dublin,
Ireland: ACM, 2009, p. 465–478.
Gal, A.; Probst, C. W.; Franz, M. HotpathVM: an Effective JIT Compiler
for Resource-Constrained Devices. In: Proceedings of the International Conference on
Virtual Execution Environments, Ottawa, Ontario, Canada: ACM, 2006, p. 144–153.
149
Goldberg, A.; Robson, D. Smalltalk-80: the Language and its Implementation.
Boston, MA, USA: Addison-Wesley Longman Publishing Co., Inc., 1983.
Gupta, G. Multiprocessor Execution of Logic Programs. Norwell, MA, USA: Kluwer
Academic Publishers, 1994.
Hank, R. E.; Hwu, W.-M. W.; Rau, B. R. Region-Based Compilation: an
Introduction and Motivation. In: Proceedings of the International Symposium on
Microarchitecture, Ann Arbor, Michigan, United States: IEEE Computer Society Press,
1995, p. 158–168.
Hansen, G. J. Adaptive Systems for the Dynamic Run-Time Optimization of Programs.
Tese de Doutoramento, Pittsburgh, PA, USA, 1974.
Haygood, R. C. Native Code Compilation in SICStus Prolog. 1 ed. New York, USA:
MIT Press, 1994.
Henderson, F.; Somogyi, Z. Compiling Mercury to High-Level C Code. In:
Proceedings of the International Conference on Compiler Construction, London, UK:
Springer-Verlag, 2002, p. 197–212.
Hermenegildo, M. V.; Bueno, F.; Carro, M.; Lıpez-Garcıa, P.; Mera, E.;
Morales, J. F.; Puebla, G. An Overview of CIAO and its Design Philosophy.
Theory and Practice of Logic Programming, v. 12, n. 1-2, p. 219–252, 2012.
Hickey, T.; Mudambi, S. Global Compilation of Prolog. Journal of Logic
Programming, v. 7, n. 3, p. 193–230, 1989.
Holzle, U. Adaptive Optimization for SELF: Reconciling High Performance with
Exploratory Programming. Tese de Doutoramento, Stanford, CA, USA, 1995.
Holzle, U.; Ungar, D. A Third-Generation SELF Implementation: Reconciling
Responsiveness with Performance. In: Proceedings of the ninth annual Conference on
Object-oriented Programming Systems, Language, and Applications, Portland, Oregon,
United States: ACM, 1994, p. 229–243.
Homescu, A.; Suhan, A. HappyJIT: a Tracing JIT Compiler for PHP. In:
Proceedings of the 7th Symposium on Dynamic Languages, Portland, Oregon, USA: ACM,
2011, p. 25–36.
150
Horn, A. On Sentences Which are True of Direct Unions of Algebras. Journal of
Symbolic Logic, v. 16, n. 1, p. 14–21, 1951.
Hoste, K.; Georges, A.; Eeckhout, L. Automated Just-In-Time Compiler
Tuning. In: Proceedings of the 8th annual IEEE/ACM International Symposium on
Code Generation and Optimization, Toronto, Ontario, Canada: ACM, 2010, p. 62–72.
Ishizaki, K.; Takeuchi, M.; Kawachiya, K.; Suganuma, T.; Gohda, O.;
Inagaki, T.; Koseki, A.; Ogata, K.; Kawahito, M.; Yasue, T.; Ogasawara,
T.; Onodera, T.; Komatsu, H.; Nakatani, T. Effectiveness of Cross-Platform Op-
timizations for a Java Just-in-time Compiler. In: Proceedings of the ACM Conference on
Object-oriented Programing, Systems, Languages, and Applications, Anaheim, California,
USA: ACM, 2003, p. 187–204.
Kotzmann, T.; Wimmer, C.; Mossenbock, H.; Rodriguez, T.; Russell, K.;
Cox, D. Design of the Java HotSpot Client Compiler for Java 6. ACM Transactions
on Architecture and Code Optimization, v. 5, p. 1–32, 2008.
Kowalski, R. A. The Early Years of Logic Programming. Communications of the
ACM, v. 31, p. 38–43, 1988.
Krintz, C. Coupling On-Line and Off-Line Profile Information to Improve Program
Performance. In: Proceedings of the International Symposium on Code Generation and
Optimization: Feedback-Directed and Runtime Optimization, San Francisco, California:
IEEE Computer Society, 2003, p. 69–78.
Kulkarni, P. A. JIT Compilation Policy for Modern Machines. In: Proceedings of
the ACM International Conference on Object Oriented Programming Systems Languages
and Applications, Portland, Oregon, USA: ACM, 2011, p. 773–788.
Kulkarni, P. A.; Arnold, M.; Hind, M. Dynamic Compilation: the Benefits of
Early Investing. In: Proceedings of the International Conference on Virtual Execution
Environments, San Diego, California, USA: ACM, 2007, p. 94–104.
Kumar, K. V. S. When and What to Compile/Optimize in a Virtual Machine? ACM
SIGPLAN Notices, v. 39, p. 38–45, 2004.
Lattner, C.; Adve, V. LLVM: A Compilation Framework for Lifelong Program
Analysis & Transformation. In: Proceedings of the International Symposium on Code
Generation and Optimization, Palo Alto, California: IEEE Computer Society, 2004, p.
75–86.
151
Lee, S.-W.; Moon, S.-M.; Kim, S.-M. Enhanced Hot Spot Detection Heuristics for
Embedded Java Just-In-Time Compilers. In: Proceedings of the ACM Conference on
Languages, Compilers, and Tools for Embedded Systems, Tucson, AZ, USA: ACM, 2008,
p. 13–22.
Lindholm, T.; Yellin, F. Java Virtual Machine Specification. 2nd ed. Boston,
MA, USA: Addison-Wesley Longman Publishing Co., Inc., 1999.
Lloyd, J. W. Foundations of logic programming; (2nd extended ed.). New York, NY,
USA: Springer-Verlag New York, Inc., 1987.
Lloyd, J. W. Practical Advantages of Declarative Programming. In: Proceedings of
the Joint Conference on Declarative Programming, 1994, p. 1–15.
Lopes, R. N. d. S. Execucao de Prolog com Alto Desempenho. Dissertacao de
Mestrado, Universidade do Minho, Braga, Portugal, 1996.
Lougher, R. Jam Virtual Machine. Disponıvel em http://jamvm.sourceforge.net/.
Acesso em: 13 set. 2012, ????
Lutz, M.; Ascher, D. Aprendendo Python. Porto Alegre, PR, Brasil: Bookman,
2007.
Marien, A. An Optimal Intermediate Code for Structure Creation in a WAM-based
Prolog Implementation. Relatorio Tecnico T1988:01, Katholicke Universiteit Leuven,
Heverlee, Belgium, 1988.
Martins, A. L.; Silva, A. F. d. Benchmarking Prolog Interpreters. IEEE Latin
America Transactions, v. 9, n. 7, p. 1079–1086, 2011.
McCarthy, J. Recursive Functions of Symbolic Expressions and Their Computation
by Machine, Part I. Communications of the ACM, v. 3, n. 4, p. 184–195, 1960.
Meier, M. Compilation of Compound Terms in Prolog. In: Proceedings of the North
American Conference on Logic Programming, Austin, Texas, United States: MIT Press,
1990, p. 63–79.
Mellish, C. S. The Automatic Generation of Mode Declarations for Prolog Programs.
Logic programming for Intelligent Systems, v. 13, n. 2-3, p. 103–179, 1981.
Mellish, C. S. Some Global Optimizations for a Prolog Compiler. Journal of Logic
Programming, v. 1, p. 43–66, 1985.
152
Mitchell, J. G.; Perlis, A. J.; Van Zoeren, H. R. LC2: A Language for
Conversational Computing. In: Proceedings of 1967 ACM Symposium, New York, NY,
USA: Academic Press, 1968.
Morales, J.; Carro, M.; Hermenegildo, M. Improved Compilation of Prolog to
C Using Moded Types and Determinism Information. In: Proceedings of the Colloquium
on Implementation of Constraint and Logic Programming Systems, 2003, p. 197–212.
Disponıvel em citeseer.nj.nec.com/henderson01compiling.html
Muchnick, S. S. Advanced Compiler Design and Implementation. San Francisco, CA,
USA: Morgan Kaufmann Publishers, 1997.
Muthukumar, K.; Hermenegildo, M. Compile-time Derivation of Variable
Dependency Using Abstract Interpretation. Journal of Logic Programming, v. 13, n.
2-3, p. 315–347, 1992.
Namjoshi, M. A.; Kulkarni, P. A. Novel Online Profiling for Virtual Machines.
In: Proceedings of the ACM International Conference on Virtual Execution Environments,
Pittsburgh, Pennsylvania, USA: ACM, 2010, p. 133–144.
Nassen, H. Optimizing the SICStus Prolog Virtual Machine Instruction Set. Relatorio
Tecnico T2000:01, Intelligent Systems Laboratory, Uppsala University, Uppsala, Sweden,
2001.
Paleczny, M.; Vick, C.; Click, C. The Java Hotspot Server Compiler. In:
Proceedings of the Java Virtual Machine Research and Technology Symposium, Monterey,
CA, USA, 2001, p. 1–12.
Pall, M. LuaJIT. Disponıvel em http://luajit.org. Acesso em: 13 set. 2012, ????
Patterson, D. A.; Hennessy, J. L. Organizacao e projeto de computadores: Interface
hardware e software. 3 ed. Rio de Janeiro: Campus, 800 p., 2005.
Pettersson, M.; Sagonas, K. F.; Johansson, E. The HiPE/x86 Erlang Compiler:
System Description and Performance Evaluation. In: Proceedings of the International
Symposium on Functional and Logic Programming, London, UK: Springer-Verlag, 2002,
p. 228–244.
Plezbert, M. P.; Cytron, R. K. Does “Just In Time” = “Better Late than Never”?
In: Proceedings of the ACM Symposium on Principles of Programming Languages, Paris,
France: ACM, 1997, p. 120–131.
153
Puebla, G.; Albert, E.; Hermenegildo, M. Abstract Interpretation with
Specialized Definitions. In: Proceedings of the International Conference on Static
Analysis, Berlin, Heidelberg: Springer-Verlag, 2006, p. 107–126.
Puebla, G.; de la Banda, M. J. G.; Marriott, K.; Stuckey, P. J. Optimization
of Logic Programs with Dynamic Scheduling. In: Proceedings of the International
Conference on Logic Programming, Cambridge: MIT Press, 1997, p. 93–107.
Puebla, G.; Hermenegildo, M. Implementation of Multiple Specialization in Logic
Programs. In: Proceedings of the ACM SIGPLAN Symposium on Partial Evaluation and
Semantics-based Program Manipulation, La Jolla, California, United States: ACM, 1995,
p. 77–87.
Quintano, L.; Rodrigues, I. Using a Logic Programming Framework to Control
Database Query Dialogues in Natural Language. In: Proceedings of the International
Conference on Logic Programming, Seattle, WA: Springer-Verlag, 2006, p. 406–420.
Ramakrishnan, C. R.; Ramakrishnan, I. V.; Warren, D. S. Deductive
Spreadsheets Using Tabled Logic Programming. In: Proceedings of the International
Conference on Logic Programming, Seattle, WA: Springer-Verlag, 2006, p. 391–405.
Reinholtz, K. Java will be Faster than C++. ACM SIGPLAN Notices, v. 35,
p. 25–28, 2000.
Rigo, A.; Pedroni, S. PyPy’s Approach to Virtual Machine Construction. In:
Proceedings of the ACM Symposium on Object-oriented Programming Systems, Languages,
and Applications, Portland, Oregon, USA: ACM, 2006, p. 944–953.
Ritchie, D. M. The Development of the C Language. In: The second ACM SIGPLAN
Conference on History of Programming Languages, Cambridge, Massachusetts, USA:
ACM, 1993, p. 201–208.
Rocha, R.; Silva, F. M. A.; Costa, V. S. On a Tabling Engine That Can Exploit
Or-Parallelism. In: Proceedings of the International Conference on Logic Programming,
London, UK, UK: Springer-Verlag, 2001, p. 43–58.
Romer, T. H.; Lee, D.; Voelker, G. M.; Wolman, A.; Wong, W. A.; Baer,
J.-L.; Bershad, B. N.; Levy, H. M. The Structure and Performance of Interpreters.
In: Proceedings of the International Conference on Architectural Support for Programming
Languages and Operating Systems, Cambridge, Massachusetts, USA: ACM Press, 1996,
p. 150–159.
154
Saglam, H.; Gallagher, J. P. Approximating Constraint Logic Programs Using
Polymorphic Types and Regular Descriptions. Relatorio Tecnico CSTR-95-17, Depart-
ment of Computer Science, University of Bristol, Bristol, UK, UK, 1995.
Sagonas, K. F.; Swift, T.; Warren, D. S. The XSB Programming System. In:
Proceedings of the Workshop on Programming with Logic Databases, Vancouver, British
Columbia, Canada, 1993, p. 164–195.
Santos, H. N.; Alves, P.; Costa, I.; Quintao Pereira, F. M. Just-in-time Value
Specialization. In: Proceedings of the International Symposium on Code Generation and
Optimization, Washington, DC, USA: IEEE Computer Society, 2013, p. 1–11.
Scott, M. L. Programming languages pragmatics. San Francisco, CA, USA: Morgan
Kaufmann Publishers, 2009.
Sebesta, R. W. Concepts of programming languages. San Francisco, CA, USA:
Addison Wesley, 2009.
Serrano, M. Inline Expansion: When and How? In: Proceedings of the Interna-
tional Symposium on Programming Languages: Implementations, Logics, and Programs,
London, UK: Springer-Verlag, 1997, p. 143–157.
Shiflett, C. HTTP Developer’s Handbook. 1 ed. New York, USA: Smas Publishing,
2003.
Shootout The Computer Language Benchmarks Game. Disponıvel em http://
benchmarksgame.alioth.debian.org/. Acesso em: 29 jan. 2013, ????
Silva, A. F. d. Projeto e Implementacao do Compilador YAPc: Um Compilador
Otimizador para Linguagens de Programacao em Logica. Tese de Doutoramento,
Universidade Federal do Rio de Janeiro, Rio de Janeiro, RJ, Brasil, 2006.
Silva, A. F. d.; Costa, V. S. Our Experiences with Optimizations in Sun’s Java
Just-In-Time Compilers. Journal of Universal Computer Science, v. 12, n. 7, p. 788–810,
2006.
Silva, A. F. d.; Costa, V. S. Design, Implementation, and Evaluation of a Dynamic
Compilation Framework for the YAP System. In: Proceedings of the International
Conference on Logic Programming, Porto, Portugal: Springer-Verlag, 2007, p. 410–424.
155
Silva, S. M. HTML 5 - A Linguagem de Marcacao que Revolucionou. 1 ed. Rio de
Janeiro, Brasil: NOVATEC, 2011.
Sreedhar, V. C.; Ju, R. D.-C.; Gillies, D. M.; Santhanam, V. Translating Out
of Static Single Assignment Form. In: Proceedings of the International Symposium on
Static Analysis, London, UK: Springer-Verlag, 1999, p. 194–210.
Stallings, W. Arquitetura e organizacao de computadores. 8 ed. Porto Alegre:
Prentice Hall, 640 p., 2005.
Sterling, L.; Shapiro, E. The Art of Prolog: Advanced Programming Techniques.
2 ed. Cambridge, MA, USA: MIT Press, 1994.
Stroustrup, B. Princıpios e Praticas de Programacao C++. 1 ed. Porto Alegre,
Brasil: Bookman, 2011.
Suganuma, T.; Ogasawara, T.; Kawachiya, K.; Takeuchi, M.; Ishizaki, K.;
Koseki, A.; Inagaki, T.; Yasue, T.; Kawahito, M.; Onodera, T.; Komatsu,
H.; Nakatani, T. Evolution of a Java Just-In-Time Compiler for IA-32 Platforms.
IBM Journal of Research and Development, v. 48, p. 767–795, 2004.
Suganuma, T.; Ogasawara, T.; Takeuchi, M.; Yasue, T.; Kawahito, M.;
Ishizaki, K.; Komatsu, H.; Nakatani, T. Overview of the IBM Java Just-in-time
Compiler. IBM Systems Journal, v. 39, p. 175–193, 2000.
Suganuma, T.; Yasue, T.; Nakatani, T. A Region-Based Compilation Technique for
a Java Just-In-Time Compiler. In: Proceedings of the ACM Conference on Programming
Language Design and Implementation, San Diego, California, USA: ACM, 2003, p.
312–323.
Suganuma, T.; Yasue, T.; Nakatani, T. A Region-Based Compilation Technique
for Dynamic Compilers. ACM Transactions on Programming Languages and Systems,
v. 28, p. 134–174, 2006.
Swift, T.; Warren, D. S. Compiling OLDT Evaluation: Background and Overview.
Relatorio Tecnico 92/04, SUNY Stony Brook, 1992.
Swift, T.; Warren, D. S. Performance of Sequential SLG Evaluation. In:
Proceedings of the Symposium on Logic Programming, MIT Press, 1993, p. 219–238.
156
Swift, T.; Warren, D. S. XSB: Extending Prolog with Tabled Logic Programming.
Theory and Practice of Logic Programming, v. 12, n. 1-2, p. 157–187, 2012.
Tanenbaum, A. S. Modern Operating Systems. 3 ed. Upper Saddle River, NJ, USA:
Prentice Hall Press, 2007.
Tarau, P. A Compiler and a Simplified Abstract Machine for the Execution of Binary
Metaprograms. In: Proceedings of the Logic Programming Conference, London, UK, UK:
Springer-Verlag, 1991, p. 119–128.
Taylor, A. Parma – Bridging the Performance GAP Between Imperative and Logic
Programming. Journal of Logic Programming, v. 29, n. 1-3, p. 5–16, 1996.
Triantafyllis, S.; Vachharajani, M.; Vachharajani, N.; August, D. I. Com-
piler optimization-Space Exploration. In: Proceedings of the International Symposium
on Code Generation and Optimization: Feedback-Directed and Runtime Optimization, San
Francisco, California: IEEE Computer Society, 2003, p. 204–215.
Troncon, R.; Janssens, G.; Demoen, B.; Vandecasteele, H. Fast Frequent
Querying with Lazy Control Flow Compilation. Theory and Practice of Logic Program-
ming, v. 7, n. 4, p. 481–498, 2007.
Turk, A. K. Compiler Optimizations for the WAM. In: Proceedings of the
International Conference on Logic Programming, London, UK, UK: Springer-Verlag, 1986,
p. 657–662.
Tyma, P. Why Are We Using Java Again? Communications of the ACM, v. 41,
p. 38–42, 1998.
Van Roy, P. A Prolog Compiler for the PLM. Relatorio Tecnico UCB/CSD 84/203,
University of California, Berkeley, California, USA, 1984.
Van Roy, P. An Intermediate Language to Support Prolog’s Unification. In:
Proceedings of North American Conference on Logic Programming, Cleveland, Ohio, USA:
MIT Press, 1989, p. 1148–1164.
Van Roy, P. Can Logic Programming Execute as Fast as Imperative Programming?
Tese de Doutoramento, Berkeley, California, USA, 1990.
Van Roy, P. 1983-1993: The Wonder Years of Sequential Prolog Implementation.
”Journal of Logic Programming”, v. 29, n. 1-3, p. 5–16, 1994.
157
Van Roy, P.; Demoen, B.; Willems, Y. D. Improving the Execution Speed of
Compiled Prolog with Modes, Clause Selection, and Determinism. In: Proceedings of
the Theory and Practice of Software Development, v. 250, Springer Berlin Heidelberg, p.
111–125, 1987.
Van Roy, P.; Despain, A. M. High-Performance Logic Programming with the
Aquarius Prolog Compiler. IEEE Computer, v. 25, n. 1, p. 54–68, 1992.
Vaucheret, C.; Bueno, F. More Precise Yet Efficient Type Inference for Logic
Programs. In: Proceedings of the International Symposium on Static Analysis, London,
UK, UK: Springer-Verlag, 2002, p. 102–116.
Warren, D. H. D. Implementing Prolog - Compiling Predicate Logic Programs.
Relatorio Tecnico 39-40, Department of Artificial Intelligence, University of Edinburgh,
1977.
Warren, D. H. D. An Abstract Prolog Instruction Set. Relatorio Tecnico 309,
Artificial Intelligence Center, SRI International, Menlo Park, U.S.A, 1983.
Warren, R.; Hermenegildo, M. V.; Debray, S. K. On the Practicality of Global
Flow Analysis of Logic Programs. In: Proceedings of the International Conference and
Symposium on Logic Programming, Seattle, Washington, USA, 1988, p. 684–699.
Whaley, J. Partial Method Compilation Using Dynamic Profile Information. In: Pro-
ceedings of the ACM Conference on Object-oriented Programming, Systems, Languages,
and Applications, Tampa Bay, FL, USA, 2001, p. 166–179.
Wielemaker, J. An Overview of the SWI-Prolog Programming Environment.
In: Proceedings of the International Workshop on Logic Programming Environments,
Heverlee, Belgium: Katholieke Universiteit Leuven, 2003, p. 1–16.
Wielemaker, J.; Schrijvers, T.; Triska, M.; Lager, T. Swi-Prolog. Theory
and Practice of Logic Programming, v. 12, n. 1-2, p. 67–96, 2012.
Wilkinson, T.; Mehlitz, P. The Kaffe Virtual Machine. Disponıvel em http:
//kaffe.org. Acesso em: 13 set. 2012, ????
Zend PHP and Zend Engine. Disponıvel em http://www.zend.com/en/community/
php/. Acesso em: 13 set. 2012, ????
158
Zhao, C.; Wu, Y.; G., J.; Amza, C. Lengthening Traces to Improve Opportunities
for Dynamic Optimization. In: Proceedings of the Workshop on Interaction Between
Compilers and Computer Architectures, Salt Lake City, UT, 2008, p. 1–10.