[Pereira ic'2011] explorando o paralelismo no nível de threads
[Pereira IC'2011] Otimizacoes no LLVM
1. Análises e Otimizações no LLVM
Resultados Experimentais
Marcio Machado Pereira - RA 780681
21 de dezembro de 2011
Resumo
O objetivo deste relatório foi o de fornecer uma visão geral das análises e otimizações que o LLVM
1
oferece para a melhoria do desempenho das aplicações. Além disso, a metodologia aqui empregada pode
ser vista como um passo inicial para uma sistematização na seleção das diversas análises e otimizações
oferecidas pelo LLVM, bem como determinar a sequencia adequada para melhorar o desempenho de um
determinado tipo de aplicação.
1 Introdução
As otimizações em compiladores modernos alcançaram um alto nível de sosticação. Embora possam ser
obtidas melhorias signicativas em muitos programas, o potencial de degradação de desempenho nos padrões
de determinado programa é conhecido por desenvolvedores de compiladores e muitos usuários. Aplicar as
otimizações do compilador a um determinado programa pode ter um impacto signicativo sobre o desem-
penho do mesmo. Devido à interação não-linear de otimizações do compilador, no entanto, determinar a
melhor conguração não é trivial. Diversas técnicas que buscam o espaço de opções do compilador para
encontrar boas soluções tem sido propostas, mas tais abordagens costumam ser caras. O estado da arte é
deixar os usuários lidarem com este problema através de opções do compilador. A presença de opções do
compilador reete a incapacidade dos otimizadores de hoje em fazer as melhores escolhas em tempo de com-
pilação. A indisponibilidade de dados do programa de entrada e o conhecimento insuciente da arquitetura
alvo pode limitar severamente a precisão de desempenho. Assim, a determinação da melhor combinação de
análises e otimizações do compilador para um determinado programa continua a ser uma meta não trivial.
Compiladores de hoje têm evoluído ao ponto em que eles apresentam para o usuário um grande número de
opções. Por exemplo, o LLVM inclui mais de 100 opções de otimização, além de agrupar um subconjunto
delas e oferecer em três níveis de otimização, O1, O2 e O3. Isto criou a necessidade de se desenvolver
uma metodologia que possa identicar e apresentar de maneira sistemática o comportamento de programas
submetidos a um determinado conjunto de otimizações.
Este relatório cientíco avaliou o impacto de um conjunto pré-estabelecido de análises e otimizações de
código do compilador LLVM em um conjunto de aplicações do SPEC CPU2006. A metodologia empregada
pode ser vista como um passo inicial na busca de uma sistematização para se determinar o subconjunto de
análises e otimizações mais adequado para cada tipo de aplicação. Note que a escolha deste subconjunto não
é tarefa simples. Em um determinado programa, geralmente há trechos de código cujo desempenho é crítico
e trechos de código que são menos críticos. Fazer com que as partes críticas possam executar o mais rápido
possível envolve não somente a escolha de um subconjunto de análises e otimizações bem como determinar
a sequencia ideal em que as mesmas devam ser aplicadas para que este processo resulte em melhorias no
1O LLVM - Low Level Virtual Machine - é uma infraestrutura de compilação escrita em C++ projetada para otimizações
em tempo de compilação, ligação e execução de programas escritos em uma variedade de linguagens.
1
2. desempenho do programa. Os esforços de otimização podem modicar ou eliminar códigos fora do caminho
crítico e portanto, não alcançar o resultado desejado. Veremos que a falta de uma metodologia adequada,
além de time consuming, pode frustar nossas expectativas, resultando em uma versão de programa menos
eciente do que quando se adota simplesmente as otimizações padrões O1, O2 ou O3 disponibilizados pelo
LLVM.
2 Metodologia
O LLVM possui diversos tipos de otimizações que podem ser aplicados ao código, incluindo os grupos de
otimizações padrões O1, O2 e O3. Neste experimento, primeiro avaliamos os 3 grupos padrões de compilação
e os comparamos, com base nas métricas citadas em 2.1, com o código sem otimizações produzido pelo LLVM.
Em seguida, tomando como base o grupo de otimizações que compõem o padrão O3, aplicamos uma série
de modicações para avaliar primeiro, o impacto das diversas alias analysis descritas em 2.2 e, por m, a
contribuição que algumas otimizações especícas, descritas em 2.2, exercem dentro do grupo O3.
2.1 Métricas
Três métricas foram utilizadas para se realizar as avaliações:
• tempo gasto pelos passos de otimização durante o processo de compilação
• tamanho do código gerado (text size)
• desempenho do código gerado. Para tanto, uma amostra de dez ( n = 10 ) execuções para cada
aplicação foi utilizada. A partir desta amostra estimou-se os parametros µ (média) e σ (variança):
¯X =
1
n
n
i=1
Xi, S2
=
n
i=1 (Xi − ¯X)
2
n − 1
que produziram para a amostra selecionada as estimativas pontuais. Uma maneira de expressar a
precisão da estimação é estabelecer limites que com certa probabilidade incluem o verdadeiro valor do
parâmetro. Em nossos experimentos, construímos então um intervalo de conança de 95%, i.e., um
intervalo no qual estamos 95% conantes da cobertura do verdadeiro valor do parâmetro. O intervalo
de conança para a média é dado por:
ICµ,95% = [ ¯X ± t
S
√
n
]
onde o valor de t é obtido recorrendo-se a uma tabela da distribuição normal.
2.2 Otimizações Análises
Padrões O1, O2 e O3 : Os padrões O1, O2 e O3 são composttos por uma série de passos de otimização,
inclusive com repetições, com o objetivo de alcançar o melhor desempenho em aplicações de propósito
geral. O grupo O3 apresenta duas otimizações a mais em relação ao grupo O2. São elas, argpromotion
(promove 'por referência' argumentos para escalar) e globaldce (eliminação global de código morto). Em
relação ao padrão O1, O2 e O3 apresentam os seguintes passos de otimização: loop-unroll (desenrolar
de laços), gvn (numeração de valores globais) seguido de memdep (análise de dependência de memória)
2
3. e constmerge (fusão de constantes globais duplicadas). As otimizações do O3 são apresentadas abaixo,
ressaltando em amarelo as diferenças para o padrão O2 e em azul as diferenças de O3 e O2 para o
padrão O1. Detalhes sobre cada otimização dos padrões citados podem ser encontradas em [2].
Pass Arguments: -no-aa -tbaa -basicaa -simplifycfg -domtree -scalarrepl -early-cse -targetlibinfo -no-aa
-tbaa -basicaa -globalopt -ipsccp -deadargelim -instcombine -simplifycfg -basiccg -prune-eh -inline -
functionattrs -argpromotion -scalarrepl-ssa -domtree -early-cse -simplify-libcalls -lazy-value-info
-jump-threading -correlated-propagation -simplifycfg -instcombine -tailcallelim -simplifycfg -reassociate
-domtree -loops -loop-simplify -lcssa -loop-rotate -licm -lcssa -loop-unswitch -instcombine -scalar-evolution
-loop-simplify -lcssa -iv-users -indvars -loop-idiom -loop-deletion -loop-unroll -instcombine -memdep
-gvn -memdep -memcpyopt -sccp -instcombine -lazy-value-info -jump-threading -correlated-propagation
-domtree -memdep -dse -adce -simplifycfg -strip-dead-prototypes -print-used-types
-deadtypeelim -globaldce -constmerge -preverify -domtree -verify
Alias Analysis O LLVM possui diversos métodos de alias analysis implementados. Neste experimento
avaliamos os métodos descritos a seguir, incluindo o -no-aa (No Alias Analysis). Esta é a imple-
mentação padrão da interface Alias analysis. Ela sempre retorna I don't know para as consultas
de alias. Todos os métodos foram avaliados em conjunto com os passos de otimização utilizados em
-O3 substituindo os passos de alias presente no padrão pelo alias analysis desejado:
-basicaa 2 Basic Alias Analysis implementa identidades, i.e., duas variáveis globais diferentes são
alias.
-globalsmodref-aa 3 (Simple mod/ref analysis for globals). Esta análise fornece informação para
valores globais que não têm seu endereço tomado, e mantém o controle se funções de leitura ou
gravação em memória são puros.
-scev-aa (ScalarEvolution-based Alias Analysis). Análise simples implementada em termos de con-
sultas ScalarEvolution. Difere da análise tradicional de dependência de laço onde os testes de
dependências são dentro de uma única iteração de um laço, ao invés de dependências entre as
diferentes iterações. ScalarEvolution tem uma compreensão mais completa da aritmética de pon-
teiros que a Basic Alias Analysis.
Otimizações individuais As seguintes otimizações foram avaliadas com o objetivo de medir o impacto de
suas contribuições quando desligadas (removidas) do grupo padrão O3:
dead Compõem o grupo, as otimizações Dead Argument Elimination, Aggressive Dead Code Elimi-
nation, Dead Store Elimination e Dead Global Elimination. O passo Dead Argument Elimination
exclui argumentos mortos de funções internas. Este passo remove tanto os argumentos que estão
mortos, bem como os argumentos passados apenas em chamadas de função como argumentos
mortos de outras funções. Esta otimização é freqüentemente útil como limpeza após otimizações
interprocedurais agressivas, que adicionam argumentos possivelmente mortos. O passo Aggressive
Dead Code Elimination tenta eliminar código, semelhante ao Dead Code Elimination, mas, de
forma agressiva, assume que os valores estão mortos até que se prove o contrário. Esta otimização
é semelhante ao Sparse Conditional Constant Propagation, exceto que é aplicada ao liveness de
valores. O passo Dead Store Elimination é uma otimização trivial que elimina somente stores re-
dundantes em blocos básicos. Por m, a transformação Dead Global Elimination é projetada para
eliminar dados globais que não são alcançáveis no programa. Este usa um algoritmo agressivo,
procurando dados globais que estão vivos. Depois que ele encontra todos os dados globais que
2Nas tabelas de resultados, basicaa é referenciado simplesmente por basic
3Idem para globalsmodref-aa, referido como global
3
4. são necessários, exclui o que sobra. Isso permite também excluir trechos do programa que são
inacessíveis.
gvn Global Value Numbering Este passo elimina instruções totalmente e parcialmente redundantes.
Ele também realiza eliminação de carga redundantes.
sccp Sparse Conditional Constant Propagation. Este passo de otimização assume que valores são con-
stantes e substitui-os pelas mesmas, a menos que se prove o contrário; assume que blocos básicos
estão mortos e elimina-os, a menos que se prove o contrário e, transforma desvios condicionais em
incondicionais quando factível.
simpl Simplify the CFG. Este passo executa eliminação de código morto e fusão de blocos básicos.
Especicamente, remove blocos básicos, sem predecessores; mescla um bloco básico com o seu
predecessor se houver apenas um e o predecessor só tem ele como sucessor; elimina nós PHI
(contendo φ-functions) para os blocos básicos com um único predecessor e elimina um bloco
básico que contém apenas um ramo incondicional.
3 Infraestrutura
A seguinte infraestrutura foi utilizada em nossas experimentações:
Geração de Código otimizado. Para a geração de código otimizado utilizou-se o LLVM 2.9. O Projeto
LLVM [1] é uma coleção de tecnologias de compilação modular e reutilizável. LLVM começou como um
projeto de pesquisa da Universidade de Illinois, com o objetivo de oferecer uma estratégia de compilação
moderna, baseado em SSA, capaz de suportar tanto a compilação estática e dinâmica de um número
arbitrário de linguagens de programação. Desde então, o LLVM cresceu para ser um projeto guarda-
chuva que consiste em um número de diferentes subprojetos, muitas das quais estão sendo utilizados na
produção de uma ampla variedade de projetos de código aberto e comerciais, além de ser amplamente
utilizado em pesquisas acadêmicas. O principal sub-projeto do LLVM são as bibliotecas núcleo do
LLVM que fornecem uma otimizador moderno e independente da plataforma alvo, juntamente com
o suporte de geração de código para as CPUs mais populares. Essas bibliotecas são construídas em
torno de uma representação de código bem especicada conhecida como a representação intermediária
LLVM (LLVM IR). As bibliotecas núcleo do LLVM estão bem documentados, e é particularmente
fácil portar para um compilador existente para usar o LLVM como um otimizador e gerador de código.
Maiores informações, o leitor pode encontrar em http://www.llvm.org.
Aplicações. A tabela 1 descreve o subconjunto das aplicações do SPEC CPU2006 utilizadas no nosso
experimento, divididos em aplicações que usam instruções inteiras e de ponto utuante, sua categoria
e a linguagem de programação utilizada. Maiores detalhes sobre a descrição das aplicações o leitor
encontra em [3].
Máquina alvo. Foi utilizado como máquina alvo um notebook Dell Vostro 1440 com processador Intel Core
2 Duo T7250 @ 2GHz e 2 GB de memória. O sistema operacional na máquina alvo utilizada foi o
Kubuntu 11.10 com kernel Linux versão 3.0.0-13 e interface gráca KDE 4.7.2.
4 Resultados Experimentais
A tabela 2 na página 9 apresenta os resultados brutos de todas as simulações executadas neste experimento.
Nesta tabela o tempo de execução (exec time) mostrado corresponde ao tempo médio. Os grácos usados
nas análises fornecem também o intervalo de conança. Quando dois intervalos se sobrepõe, consideramos
para efeito de análise que os resultados são iguais.
4
5. Benchmark Aplicação Linguagem Categoria
Integer
perlbench ANSI C Linguagem de programação. Interpretador Perl
bzip2 ANSI C Compressão/Descompressão de código
gcc C Compilador e Otimizador para a Linguagem C
mcf ANSI C Otimização Combinatorial
gobmk C Inteligência Articial, Jogo Go
hmmer C Modelo estatistico para busca de padrões em sequencias de DNA
sjeng C Inteligência Articial. Reconhecimento de Padrões e busca em árvore
libquantum C99 Simulador de um computador quântico
h264ref C Compressão de Vídeo
omnetpp C++ Simulação de Eventos Discretos
astar C++ Inteligência Artical. Algoritmos de busca de caminhos em mapas
xalancbmk C++ Processador XLST. Transforma Documentos XML em HTML
Floating Point
milc C Simulações em Física Quântica
namd C++ Simulaçoes em Dinâmica Molecular Clássica
dealII C++ Soluções de Equações Diferenciais Parciais
soplex ANSI C++ Solução Programação Linear pelo método Simplex
lbm ANSI C Simulação. Dinâmica de uídos
Tabela 1: Aplicações SPEC CPU2006
Isto posto, notamos que algumas análises caram prejudicadas em um conjunto de aplicações, por estas
apresentarem tempos de execução iguais. São elas, gcc, gobmk, omnetpp, sjeng e xalancbmk. A massa de
dados de entrada utilizadas para estas aplicações são massas de testes e, ao que tudo indica, não estressaram
adequadamente a aplicação. Em função do tempo exíguo proposto para este trabalho, não pudemos repetir
tais experimentos com massa de dados mais adequadas. No entanto, como o volume de aplicações é razoável,
estas não prejudicaram nosso objetivo principal.
4.1 Tempo de Otimização
Nossa primeira análise é sobre o tempo dispendido durante a otimização. Apesar de não ser um tempo crítico
no processo de desenvolvimento de uma aplicação, nota-se na comparação dos dois gracos mostrados na
gura 1 que o número de bytes otimizados por segundo decresse 25% quando se utiliza o padrão O3 (mais
agressivo) em relação ao padrão O1.
(a) padrão O1 (b) padrão O3
Figura 1: Bytes otimizados por segundo nos padrões O1 e O3
5
6. 4.2 Impacto no código
Nossa segunda análise examina o impacto no código, em termos de tamanho, em função das otimizações
realizadas. Observamos que a maioria dos experimentos produziram uma redução no tamanho do código
otimizado comparado com o código sem otimizações, produzido pelo compilador. Os gracos na gura 3
mostram que este comportamento é o mais esperado.
Figura 2: Efeito no O3
Observou-se, no entanto, que o conjunto de otimiza-
ções do padrão O3 quando aplicado a alguns progra-
mas produz um aumento no tamanho do código como
pode ser observado na aplicação omnetpp, mostrado
na gura 2. Existe um conjunto de otimizações que
podem ser responsáveis por este aumento como, por
exemplo, loop unrolling
4 Este comportamento tam-
bém foi observado nas aplicações gcc e milc mostradas
na gura 4. Uma análise das aplicações poderia
mostrar se as mesmas guardam alguma relação como,
por exemplo, um número grande de laços, o que pode-
ria explicar este comportamento.
(a) Astar (b) bzip2 (c) dealII
(d) mcf (e) h264ref (f) libquantum
Figura 3: tamanho do código em relação ao código sem otimização
Por outro lado, desligando e religando as otimizações do padrão O3, poderia determinar exatamente qual ou
quais otimizações foram responsáveis por este comportamento. Esta metodologia foi aplicada em parte na
nossa próxima análise.
4.3 Tempo de Execução
Esta é sem dúvida a análise mais importante porque estamos geralmente interessados na melhoria de desem-
penho de nossas aplicações e isto normalmente reete diretamente no tempo de execução. Uma análise dos
dados coletados nos mostra que obtivemos uma melhora de 29% (mcf ) à 70% (delaII ) no tempo de execução
4Como este passo esta presente também no padrão O2, cabe aqui uma análise mais renada se de fato loop-unroll é o
responsável e também identicar a inuência do passo argpromotion nos passos seguintes.
6
7. (a) gcc (b) milc
Figura 4: Exemplos de aumento do código para o padrão O3
quando comparado com o código sem otimização. Isto nos mostra quanto o processo de otimização é impor-
tante e faz com que uma busca pela melhor combinação de ana¨ise e otimizações para nossas aplicações seja
um objetivo a ser perseguido.
(a) mcf (b) milc
Figura 5: tempo de execução para as diversas otimizações
Note na gura 5 que para a aplicação mcf o passo de otimização simplify the CFG tem uma contribuição neg-
ativa no padrão O3. Provavelmente as simplicações realizadas esconderam oportunidades de otimização
dos passos seguintes. O comportamento normal esperado é mostrado na aplicação milc. Nesta aplicação,
a remoção de qualquer um dos grupos de otimizações individuais aqui analisados revela uma contribuição
de mais de 6% dentro do grupo O3.
5 Conclusões
O que se pode observar nas analises realizadas é que, em primeiro lugar, não se detectou uma otimização
que seja killer. Percebeu-se também que nem sempre as otimizações consideradas mais agressivas, como é o
caso do padrão O3, resultaram em um melhor desempenho. Por exemplo, o padrão O2 apresentou melhor
desempenho em relação ao padrão O3 nas aplicações (a)-Astar e (c)-gcc (gura 6). Observou-se também que
as alias analysis, em particular, globalsmodref-aa e scev-aa apresentaram uma melhora perceptiva somente
na aplicação (b)-bzip2 (ver gura 6). A conclusão que se pode chegar é que a escolha de se aplicar ou não
um passo de otimização é sensível ao tipo de aplicação.
7
8. (a) Astar (b) bzip2 (c) dealII
(d) gcc (e) h264ref (f) libquantum
Figura 6: comparação do tempo de execução em diversas otimizações
Referências
[1] Chris Lattner and Vikram Adve. The LLVM Compiler Framework and Infrastructure Tutorial, LCPC'04
Mini Workshop on Compiler Research Infrastructures, West Lafayette, Indiana, USA, Sep/2004
[2] Reid Spencer and Gordon Henriksen. LLVM Analysis and transform Passes, Documentation for the
LLVM System at http://www.llvm.org/docs/Passes.html
[3] SPEC CPU2006 Benchmark Descriptions. Descriptions written by the SPEC CPU Subcommittee and
by the original program authors posted at www.spec.org/cpu2006/Docs/. Edited by John L. Hen-
ning, Secretary, SPEC CPU Subcommittee, and Performance Engineer, Sun Microsystems. Contact
john.henning@acm.org
8