UNIVERSIDADE FEDERAL DE SANTA CATARINA
DEPARTAMENTO DE INFORMÁTICA E ESTATÍSTICA
CURSO DE CIÊNCIAS DA COMPUTAÇÃO
Leonardo Maccari Rufino
Análise de Tempo de Execução
Utilizando LLVM
Florianópolis - SC
2008
2
Leonardo Maccari Rufino
Análise de Tempo de Execução
Utilizando LLVM
Trabalho de conclusão de curso
apresentado como parte dos
requisitos para obtenção do grau de
bacharel em Ciências da Computação
pela Universidade Federal de Santa
Catarina.
Orientador
Olinto José Varela Furtado
Florianópolis - SC
3
Leonardo Maccari Rufino
Análise de Tempo de Execução
Utilizando LLVM
Trabalho de conclusão de curso
apresentado como parte dos
requisitos para obtenção do grau de
bacharel em Ciências da Computação
pela Universidade Federal de Santa
Catarina.
___________________________
Olinto José Varela Furtado
Orientador
Banca Examinadora:
José Eduardo de Lucca
Luiz Cláudio Villar dos Santos
Ricardo Azambuja Silveira
Florianópolis - SC
4
RESUMO
Os sistemas embarcados dominam o mercado de diversas áreas comerciais hoje em dia.
Muitos desses sistemas podem também ser classificados como sistemas de tempo real, os
quais podem se dividir em críticos e brandos. Os sistemas de tempo real crítico necessitam de
uma validação quanto a sua correta implementação. Essa validação pode ser feita através de
técnicas estáticas ou dinâmicas. Nesse trabalho, será explicado como realizar essa análise,
dando ênfase à análise estática, explicando cada uma de suas etapas que são: análise do fluxo
de controle, análise de baixo nível e por fim o cálculo. Será feito também uma comparação
entre essas duas técnicas de análise. Também será comentado sobre a infraestrutura de
compilação LLVM, a qual está muito ativa no momento, descrevendo seus objetivos,
arquitetura e sua representação intermediária, a qual representa um dos fatores chaves que o
diferencia dos demais sistemas. Esse framework foi utilizado como base para a
implementação da ferramenta llflow, a qual será apresentada nesse trabalho. Para finalizar,
realizar-se-ão testes, correções e inclusões de novas funcionalidades em cima da ferramenta
llflow, alvo desse trabalho.
Palavras-Chave: Análise de Tempo de Execução, WCET, Tempo de Execução do Pior Caso,
Sistemas de Tempo Real, LLVM, llflow.
5
ABSTRACT
The embedded systems dominate the commercial market today. Many of these systems can
also be classified as real-time systems, which can be split into hard and soft. The critical real-
time systems require a validation about its correct implementation. This validation can be
done through static or dynamic techniques. In this work, it will be explained how to perform
this analysis, emphasizing the static analysis, explaining each of its steps that are: control-
flow analysis, low level analysis and finally the calculation. It will be made a comparison
between these two analysis techniques. It will also be commented on the LLVM compilation
infrastructure, which is very active at the time, describing their goals, architecture and its
intermediate representation, which represents one of the key factors that differentiates from
other systems. This framework was used as a basis for the implementation of the llflow tool,
which will be presented in the work. Finally, it will be testing, corrections and additions of
new features in the llflow tool, target of this work.
Keywords: Execution Time Analysis, WCET, Worst Case Execution Time, Real-Time
Systems, LLVM, llflow.
6
LISTA DE ILUSTRAÇÕES
Figura 1: Exemplo de curvas do WCET e BCET para análise estática e dinâmica ................. 14 Figura 2: Estrutura da análise estática do WCET ..................................................................... 16 Figura 3: Etapas da análise do fluxo de controle ...................................................................... 17 Figura 4: Arquitetura do sistema LLVM .................................................................................. 24 Figura 5: Código fonte em linguagem C, exemplificando o LLVM IR ................................... 29 Figura 6: Código na representação LVIS, exemplificando o LLVM IR .................................. 29 Figura 7: Arquitetura da ferramenta llflow............................................................................... 34 Figura 8: Arquivo de configuração da ferramenta llflow ......................................................... 36 Figura 9: Código fonte do programa que realiza a busca binária ............................................. 40 Figura 10: Geração dos arquivos texto e binário LLVM.......................................................... 41 Figura 11: Código na representação LVIS, sem otimização (busca binária) ........................... 42 Figura 12: Código na representação LVIS, com otimização (busca binária) ........................... 43 Figura 13: Arquivo de configuração (busca binária) ................................................................ 44 Figura 14: Chamada à ferramenta llflow .................................................................................. 44 Figura 15: Resultado da análise, sem otimização (busca binária) ............................................ 45 Figura 16: Resultado da análise, com otimização (busca binária) ........................................... 46 Figura 17: Código fonte do programa que realiza a raiz quadrada .......................................... 50 Figura 18: Código na representação LVIS, sem otimização (raiz quadrada) ........................... 52 Figura 19: Arquivo de configuração (raiz quadrada) ............................................................... 52 Figura 20: Resultado da análise, sem otimização (raiz quadrada) ........................................... 53 Figura 21: Código fonte do programa que executa a sequência de fibonacci .......................... 55 Figura 22: Código na representação LVIS, com otimização (fibonacci) ................................. 56 Figura 23: Arquivo de configuração (fibonacci) ...................................................................... 56 Figura 24: Resultado da análise, com otimização (fibonacci) .................................................. 57 Figura 25: Código fonte do programa com loops aninhados dependentes ............................... 62 Figura 26: Código na representação LVIS, sem otimização (janne complex) ......................... 64 Figura 27: Arquivo de configuração (janne complex).............................................................. 64 Figura 28: Resultado da análise da ferramenta llflow original (janne complex)...................... 65 Figura 29: Resultado da análise da ferramenta llflow modificada (janne complex) ................ 66 Figura 30: Código fonte do programa insertion sort ................................................................ 69 Figura 31: Código na representação LVIS, com otimização (insertion sort) ........................... 70 Figura 32: Arquivo de configuração (insertion sort) ................................................................ 70 Figura 33: Resultado da análise, com otimização (insertion sort) ............................................ 71 Figura 34: Código fonte do programa de busca em array multidimensional ........................... 74 Figura 35: Código na representação LVIS, sem otimização (busca array multidimensional) . 76 Figura 36: Arquivo de configuração (busca array multidimensional) ...................................... 77 Figura 37: Resultado da análise, sem otimização (busca array multidimensional) .................. 78 Figura 38: Código fonte do programa de teste de código morto .............................................. 80 Figura 39: Código na representação LVIS, com otimização (teste de código morto) .............. 80 Figura 40: Arquivo de configuração (teste de código morto) .................................................. 81 Figura 41: Resultado da análise, com otimização (teste de código morto) .............................. 81 Figura 42: Código fonte do programa cover ............................................................................ 82 Figura 43: Código na representação LVIS, sem otimização (cover) ........................................ 84 Figura 44: Arquivo de configuração (cover) ............................................................................ 85 Figura 45: Resultado da análise, sem otimização (cover) ........................................................ 86
7
LISTA DE ABREVIATURAS E SIGLAS
BCET Best Case Execution Time
CFG Control-Flow Graph
CP Constraint Programming
ILP Integer Linear Programming
IPET Implicit Path Enumeration Technique
IR Intermediate Representation
LLVM Low Level Virtual Machine
LVIS LLVM Virtual Instruction Set
SSA Static Single Assignment
WCET Worst Case Execution Time
8
SUMÁRIO
1 INTRODUÇÃO .................................................................................................................. 9 1.1 Motivação ...................................................................................................................... 11 1.2 Objetivos ........................................................................................................................ 11 1.2.1 Objetivo Geral ............................................................................................................... 11 1.2.2 Objetivos Específicos ..................................................................................................... 12 2 ANÁLISE DO TEMPO DE EXECUÇÃO DO PIOR CASO ........................................... 13 2.1 Análise Dinâmica .......................................................................................................... 14 2.2 Análise Estática ............................................................................................................. 15 2.2.1 Análise do Fluxo de Controle ........................................................................................ 16 2.2.2 Análise de Baixo Nível ................................................................................................... 18 2.2.3 Cálculo .......................................................................................................................... 19 2.2.4 Problemas ...................................................................................................................... 20 2.3 Análise Estática Vs. Análise Dinâmica ......................................................................... 21 3 LLVM................................................................................................................................ 23 3.1 Arquitetura ..................................................................................................................... 23 3.1.1 Tempo de Compilação ................................................................................................... 24 3.1.2 Tempo de Ligação ......................................................................................................... 25 3.1.3 Tempo de Execução ....................................................................................................... 26 3.1.4 Tempo Inativo ................................................................................................................ 26 3.2 LLVM IR ....................................................................................................................... 27 3.2.1 Representação Textual, Binária e Em Memória ........................................................... 29 3.2.2 Sistema de Tipo .............................................................................................................. 30 3.2.3 Alocação Explícita de Memória .................................................................................... 30 4 LLFLOW ........................................................................................................................... 32 4.1 Características ................................................................................................................ 32 4.2 Arquitetura ..................................................................................................................... 33 4.3 Entradas ......................................................................................................................... 34 4.3.1 Arquivo de Configuração .............................................................................................. 35 4.4 Saídas ............................................................................................................................. 36 5 VALIDANDO A FERRAMENTA LLFLOW .................................................................. 38 5.1 Busca Binária ................................................................................................................. 38 5.2 Raiz Quadrada ............................................................................................................... 48 5.3 Fibonacci ....................................................................................................................... 54 6 INCREMENTANDO A FERRAMENTA LLFLOW - PARTE 1 .................................... 61 6.1 Janne Complex .............................................................................................................. 62 6.2 Insertion Sort ................................................................................................................. 68 6.3 Busca em Array Multidimensional ................................................................................ 72 7 INCREMENTANDO A FERRAMENTA LLFLOW - PARTE 2 .................................... 79 7.1 Teste de Código Morto .................................................................................................. 79 7.2 Cover ............................................................................................................................. 82 8 CONCLUSÃO .................................................................................................................. 87 8.1 Considerações Finais ..................................................................................................... 87 8.2 Trabalhos Futuros .......................................................................................................... 88 REFERÊNCIAS ....................................................................................................................... 89 APÊNDICE A - ARTIGO ........................................................................................................ 91
9
1 INTRODUÇÃO
Com o passar dos tempos, o computador tornou-se uma importante ferramenta na vida
da população de qualquer ponto do planeta. Cada dia mais os computadores invadem as casas
das pessoas, muitas vezes sem que sejam percebidos. É o caso dos sistemas embarcados (ou
também chamados de sistemas embutidos, embedded systems) que são, segundo
(ENGBLOM, J, 2002), “um computador que não se parece com um computador”, ou também,
melhor explicado em (MACHADO, A., 2008), “construídos com propósitos específicos e pré-
definidos e, em função disto, possuírem características que favorecem o uso para este
propósito e dificultam o uso para outros fins”. Sistemas embarcados estão localizados em
dispositivos que tenham algum processamento feito por um microprocessador encapsulado.
Exemplos estão por toda parte, como em brinquedos, eletrodomésticos, aparelhos celulares,
automóveis, aviões e mais uma diversidade de produtos.
Sistemas embarcados muitas vezes podem também ser classificados como estando no
grupo dos sistemas de tempo real (real-time systems). Estes representam os sistemas
computacionais que possuem uma característica marcante em comum, a qual diz que para que
o sistema seja considerado correto, além de apresentar as funcionalidades esperadas, também
deve responder dentro de um tempo estabelecido aos estímulos que recebem, ou seja, deve-se
garantir que as ações sejam executadas dentro de um intervalo de tempo pré-determinado.
Os sistemas de tempo real podem ser divididos em duas classes, distinguíveis por seus
requisitos temporais e de confiabilidade, que são os sistemas de tempo real brando (soft real-
time systems) e sistemas de tempo real crítico (hard real-time systems). O primeiro
caracteriza-se por possuir um prazo de resposta mais flexível em relação ao outro, ou seja, em
sistemas brandos, o descumprimento ocasional de um prazo de tempo pode ser aceito sem
grandes problemas. Estes possuem requisitos de segurança não-críticos. Já os sistemas de
tempo real crítico, são caracterizados por possuírem um prazo de resposta estrito, ou seja, seu
comportamento deve ser previsível até mesmo quando se está executando em sobrecarga.
Estes possuem requisitos de segurança críticos, sendo que se o sistema chegar a falhar ou
mesmo não responder dentro do tempo pré-estabelecido, problemas mais graves poderão
ocorrer. Um exemplo de um sistema de tempo real brando seria o de um aparelho de som e,
10
do outro lado, um exemplo de um sistema crítico seria o controle do sistema de “air bag” de
carros.
Com a finalidade de assegurar a correta execução de sistemas de tempo real com
relação ao tempo de execução, algumas técnicas chamadas de análise de tempo de execução
surgiram. As análises podem ser realizadas dinâmica ou estaticamente, como também de uma
maneira híbrida. A análise dinâmica diz respeito à medição do tempo envolvendo a execução
do programa sobre determinadas entradas e observando o seu comportamento. Este tipo de
análise não é tão eficiente, pois não garante a análise do tempo de execução do pior e do
melhor caso (WCET e BCET respectivamente). Isto porque existe uma grande dificuldade em
se obter os dados de entrada que provoquem estes casos extremos. Já a análise de tempo de
execução estática, garante que esses tempos sejam calculados corretamente. Isto é realizado
através do cálculo do tempo de execução a partir da análise do código do programa.
A análise estática está dividida em duas fases, a análise de alto nível e a de baixo
nível. A análise de alto nível diz respeito ao fluxo de execução do programa dependente
apenas das características do programa analisado. Já a análise de baixo nível referencia-se à
simulação do comportamento de tempo do processador levando em conta detalhes mais
profundos como o uso de caches ou pipelines por exemplo.
Este trabalho está situado exatamente nesta área contextualizada até aqui, análise de
tempo de execução estática de alto nível. Para tal feito, será utilizada uma ferramenta em
ascensão no momento chamada LLVM (Low Level Virtual Machine). LLVM é um
framework que possui diversas funcionalidades como, por exemplo, o desenvolvimento de
compiladores, profundas otimizações de código, como também a análise do fluxo de
execução. Uma característica marcante desta infra-estrutura, que possibilita o uso para todas
estas áreas citadas, é o fato dela possuir uma representação intermediária própria de código
chamada LVIS (LLVM Virtual Instruction Set). Esta representação é muito interessante, pois
se trata de uma codificação de baixo nível, porém possui algumas características de alto nível
como, por exemplo, ser “tipada” (suas variáveis são declaradas sendo de determinado tipo).
11
1.1 Motivação
Segundo (MACHADO, A., 2008), “98% dos processadores vendidos são utilizados
em sistemas embarcados”. Com este elevado consumo deste tipo de produto, foi necessária
uma dedicação por parte dos desenvolvedores para garantir a qualidade do produto. Produtos
que executam o proposto de maneira satisfatória levam vantagens sobre os demais,
principalmente quando se trata de sistemas de tempo real crítico. Nestes, um fator importante
é a confiabilidade, ou seja, devem ser extremamente previsíveis quanto as suas ações.
Quando se diz que existe uma grande preocupação de que os sistemas de tempo real
críticos sejam executados dentro de um período de tempo pré-determinado, está-se afirmando
que o tempo calculado para o WCET é satisfatório. Para garantir a confiabilidade desses
sistemas críticos, foi criada a análise de tempo de execução estática, a qual através de cálculos
obtém este valor.
Então, neste cenário de aumento do consumo de sistemas embarcados de tempo real
crítico e com o objetivo de garantir sua correta execução, encaixa-se este trabalho referente à
análise de tempo de execução estática.
1.2 Objetivos
1.2.1 Objetivo Geral
A proposta deste trabalho é fazer um estudo a fundo do tema análise de tempo de
execução estática de alto nível e em paralelo, também, da infra-estrutura LLVM com o
objetivo de obter um conhecimento aprofundado de ambos. Também, serão realizados
verificações e incrementos em uma ferramenta chamada llflow, a qual realiza o cálculo do
WCET e baseia-se no framework LLVM.
12
1.2.2 Objetivos Específicos
Estudar as técnicas mais utilizadas atualmente de análise de tempo de execução
estática de alto nível;
Estudar o framework LLVM, porém dando enfoque à parte que está associada com o
tema do trabalho;
Realizar verificações e incrementos de novas funcionalidades à ferramenta llflow,
apresentada neste trabalho;
13
2 ANÁLISE DO TEMPO DE EXECUÇÃO DO PIOR CASO
O propósito da análise do WCET é prover uma informação a priori sobre o pior tempo
de execução possível de um pedaço de código antes de usá-lo em um sistema, conforme
mencionado em (ENGBLOM, J. ERMEDAHLT, A, 2000). Sendo assim, o domínio
tradicional do cálculo do WCET está situado nos sistemas de tempo real crítico, para que haja
uma garantia satisfatória do comportamento do sistema em todas as circunstâncias.
Para o cálculo da estimativa do WCET, algumas suposições são assumidas
previamente conforme (ENGBLOM, J, 2002):
Um programa específico executa isoladamente;
Execução em um determinado CPU e clock;
Compilado com um determinado compilador;
Sem interferência de atividades de fundo (background), como acesso direto a memória
(DMA) e refresh da DRAM;
Sem troca de contextos (preempções) pelo escalonador.
Como já mencionado, o cálculo do tempo de execução do pior caso pode ser realizado
através da análise dinâmica e estática. Essas serão comentadas a seguir, enfatizando a técnica
estática.
Um sistema de tempo real é composto de várias tarefas (tasks) onde cada uma delas
realiza uma funcionalidade específica. Dependendo dos dados de entrada ou de diferentes
comportamentos do ambiente, uma tarefa, tipicamente, apresenta uma variação em seu tempo
de execução. Na figura 1, é mostrado o conjunto de todos os tempos de execução das tarefas
através da curva superior, tempos de execução possíveis. O limite esquerdo representa o
menor tempo de execução, BCET, enquanto que o limite direito, o maior tempo de execução,
WCET. Encontrar esses valores extremos exatos, na maioria das vezes, é muito difícil devido
ao imenso número de caminhos de execuções possíveis, tornando inviável a exploração
exaustiva de todos eles. Na análise de tempo dinâmica, ocorre a medição do tempo de
execução da tarefa inteira para um subconjunto das possíveis execuções, obtendo assim, o
tempo de execução mínimo e máximo observados. Esses resultados, em geral, superestimam o
BCET e subestimam o WCET, sendo desta forma, valores inseguros para sistemas de tempo
14
real crítico. Esse método de análise é representado pela curva inferior na figura 1, tempos de
execução medidos, ressaltando os valores mínimo e máximo observados. Segundo
(WILHELM, R. et al, 2008), limites no tempo de execução de uma tarefa podem ser
computados por métodos que consideram todos os possíveis tempos de execução, que são
todas as possíveis execuções de uma tarefa, como é o caso da análise estática. Esses métodos
usam abstrações da tarefa para fazer a análise de tempo tornar-se viável. Porém, abstrações
causam a perda de informações, então o limite do WCET computado normalmente
superestima o WCET exato enquanto que subestima o BCET. O limite do WCET representa o
pior caso garantido pelo método ou ferramenta utilizado. Esse método é ilustrado na figura 1
através dos limites de tempo inferior e superior originados pela predição do tempo.
Figura 1: Exemplo de curvas do WCET e BCET para análise estática e dinâmica comparadas
com o valor real, adaptado de (WILHELM, R. et al, 2008).
2.1 Análise Dinâmica
Para obter resultados através da técnica de análise dinâmica, são realizadas medições
da tarefa, ou de suas partes, através da execução em um dado hardware ou um simulador, para
algum conjunto de entradas (“inputs”). Para isso, conforme comentado em (WILHELM, R. et
al, 2008), são utilizadas normalmente uma entre duas abordagens. A primeira chamada de
15
end-to-end, realiza a medição do programa inteiro de uma só vez, enquanto a outra mede o
tempo de execução de segmentos do código, tipicamente de blocos básicos do CFG. Esses
tempos de execução medidos são então combinados e analisados, normalmente por alguma
forma de cálculo de limite, para produzir estimativas do WCET ou BCET. Todas as
abordagens seguem uma metodologia em comum, como citado em (ENGBLOM, J, 2002):
Determinar a entrada de pior caso;
Executar e medir;
Adicionar uma margem segura.
Em (WILHELM, R. et al, 2008), além da entrada referente ao primeiro passo anterior,
cita-se também a necessidade de determinar o estado inicial que conduzirá à execução do
caminho do pior caso. Com esses valores em mãos, utilizando esta técnica, o problema da
análise seria facilmente resolvido. Porém alguns problemas são notados, como a dificuldade
ou impossibilidade de encontrar o valor da entrada e do estado inicial que resultarão no tempo
de execução de pior caso. Além disso, outro problema seria o fato desta técnica nunca
superestimar o valor do WCET, geralmente subestimando-o.
Desta forma, esta técnica pode ser útil para aplicações que não requerem garantias do
tempo de pior caso encontrado, sendo assim, preferivelmente utilizada pra sistemas de tempo
real não crítico, ou seja, brando. Como dito em (WILHELM, R. et al, 2008), a análise
dinâmica pode dar ao desenvolvedor uma percepção sobre o tempo de execução nos casos
comuns e também a porcentagem das ocorrências do pior caso. Garantias de que o limite
obtido é um valor seguro podem ser conseguidas somente quando a arquitetura utilizada é
simples. Além disso, esta técnica também pode ser utilizada para prover validação para
abordagens de análise estática.
2.2 Análise Estática
Neste tipo de análise, não é necessária a presença do código de execução para o
hardware real ou um simulador. Como dito em (WILHELM, R. et al, 2008), é preferivelmente
pego o código por si só, talvez junto de algumas anotações, utilizando-o para analisar o
16
conjunto de caminhos do fluxo de controle possível para a tarefa, posteriormente combinando
o fluxo de controle com alguns modelos abstratos da arquitetura do hardware, e assim,
obtendo o limite superior para essa junção. Com a análise estática, existe uma garantia de que
os resultados obtidos para o WCET sejam seguros (safe), além da tentativa de ser o mais
próximo possível do valor real (tight), sendo desta forma, valores utilizáveis.
O cálculo da estimativa do tempo de pior caso utilizando esta técnica é obtido através
de três passos como ilustrado na figura 2. Primeiramente é realizada a análise do fluxo do
programa que determina o comportamento dinâmico do programa, sem considerar o tempo
para cada unidade atômica do fluxo. Após esta etapa é feita a análise de baixo nível que
obtém o tempo de execução para partes do programa no hardware, dada a arquitetura e
características do sistema alvo. Para finalizar, é efetuado o cálculo combinando os resultados
obtidos da análise do fluxo e de baixo nível para dar a estimativa do WCET.
Figura 2: Estrutura da análise estática do WCET, adaptado de (ENGBLOM, J, 2002).
2.2.1 Análise do Fluxo de Controle
17
Nesta primeira fase da análise estática, análise do fluxo de controle (também chamada
somente de análise do fluxo), é determinado o comportamento dinâmico do programa, ou
seja, tem como propósito coletar informações sobre os caminhos de execuções possíveis. Para
isso, são necessárias algumas informações como o número de iterações de loops,
profundidade das recursões, dependências de dados de entrada, caminhos inviáveis (infeasible
paths), instâncias de funções, etc. Essas informações podem ser fornecidas por anotações
manuais ou pela análise do fluxo automática.
Segundo (WILHELM, R. et al, 2008), há algumas abordagens para análise do fluxo
automática. Alguns dos métodos são gerais, enquanto outros são especializados para certos
tipos de construções de código. Os métodos também diferem no tipo de código que analisam,
isto é, código fonte, intermediário ou código de máquina.
A figura 3 ilustra os detalhes da análise do fluxo que é dividida em três partes. Inicia-
se pela extração de informações do fluxo, o qual deriva informações sobre o comportamento
do programa, seguindo pela representação do fluxo do programa, que armazena as
informações obtidas, e por último a preparação para o cálculo que busca como utilizar a
informação obtida nos passos anteriores para o cálculo do WCET.
Figura 3: Etapas da análise do fluxo de controle, adaptado de (ENGBLOM, J, 2002).
18
2.2.2 Análise de Baixo Nível
Esta fase da análise estática visa determinar o tempo de execução para partes do
programa considerando os efeitos do hardware alvo. Aqui se trabalha com o arquivo
executável já ligado, o qual representa o programa real, pois somente ele contém todas as
informações necessárias para esta etapa.
Esta fase da análise é baseada no modelo abstrato do processador, o subsistema de
memória, os barramentos e os periféricos, que são conservativos com respeito ao
comportamento de tempo do hardware concreto, significando que o modelo nunca prediz um
tempo de execução menor do que aquele que pode ser observado no processador real. Porém,
a obtenção deste modelo de processador abstrato, que simule o original fielmente, é uma
tarefa muitas vezes complexa dependendo da classe do processador usado. Processadores
mais complexos são mais difíceis de modelar e analisar devido às caches, pipelines e até
mesmo pela quantidade de bits da arquitetura.
Conforme (WILHELM, R. et al, 2008), um típico processador contém muitos
componentes que tornam o tempo de execução dependente do contexto, tais como memória
cache, pipelines e predição de desvios. O tempo de execução de uma instrução individual,
como um acesso a memória depende do histórico de execução. Para encontrar um limite do
tempo de execução preciso para uma dada tarefa, é necessário analisar qual o estado de
ocupação desses componentes do processador para todos os caminhos que conduzem para a
instrução da tarefa analisada no momento. Diferentes estados em que as instruções podem ser
executadas podem conduzir a variações amplas no tempo de execução.
A análise de baixo nível possui dois assuntos principais que são a análise da cache e a
análise do pipeline. Na análise do pipeline a interação é feita somente com instruções
vizinhas, por isso é chamada de análise local, enquanto na análise da cache a interação é
realizada com o programa inteiro, ou seja, global. O comportamento da cache pode afetar o
pipeline, pois na análise do pipeline usam-se os acertos (hits) e erros (misses) da cache. Por
isso, o resultado da análise da cache serve como entrada para a análise do pipeline.
Hoje em dia, como citado em (WILHELM, R. et al, 2008), também se utiliza o termo
análise do comportamento do processador como sinônimo para a expressão análise de baixo
nível.
19
2.2.3 Cálculo
O objetivo desta fase é encontrar um valor que representa uma estimativa para o
WCET. Há algumas abordagens que são utilizadas para a realização desta fase de cálculo. As
três mais comentadas na literatura, sendo assim as principais, segundo (WILHELM, R. et al,
2008), são chamadas de:
Baseadas em estrutura (structure-based);
Baseadas em caminhos (path-based);
Técnica de enumeração de caminhos implícitos (IPET).
Na técnica baseada em estrutura, ou também chamada de baseada em árvore (tree-
based) em (ENGBLOM, J, 2002), é realizada uma travessia bottom-up da árvore de sintaxe da
tarefa para a realização do cálculo do limite superior. Utiliza-se de constantes de tempo para
os nodos, sendo que os nodos folhas possuem um tempo definitivo, enquanto que há regras
para o cálculo dos nodos internos. Assim, conjuntos de nodos são unidos em nodos únicos,
simultaneamente derivando um tempo para esses novos nodos, reduzindo a árvore de baixo
para cima seguindo as regras, até restar somente um único nodo, o qual conterá o tempo do
pior caso para àquelas instruções analisadas. Conforme dito em (ENGBLOM, J, 2002), este
método é simples e eficiente, porém não pode tratar caminhos inviáveis (infeasible paths).
Além disso, segundo (WILHELM, R. et al, 2008), nem todo fluxo de controle pode ser
expresso através da árvore de sintaxe, como também, essa abordagem assume uma
correspondência muito direta entre a estrutura do arquivo fonte e o programa alvo não
facilmente admitindo otimizações de código. Adicionalmente, outro problema seria que não é
possível adicionar informações adicionais do fluxo como pode ser feito no caso do IPET.
No método baseado em caminho, o objetivo é encontrar o maior caminho global da
tarefa percorrendo o grafo, o qual é previamente criado e contém todos os caminhos de
execuções possíveis representados explicitamente. O valor final encontrado para este caminho
será o WCET para este grafo. Segundo (ENGBLOM, J, 2002), esta técnica é eficiente se
implementada corretamente e ainda pode tratar algumas informações do fluxo como, por
exemplo, o limite de loops. Mas como dito em (WILHELM, R. et al, 2008), a abordagem
baseada em caminho é natural dentro de uma iteração de loop único, mas tem problemas com
fluxos de informações estendendo entre níveis de loops aninhados. Também, o número de
20
caminhos é exponencial com relação ao número de pontos de desvios, possivelmente
requerendo métodos de busca heurísticos.
No IPET, o fluxo do programa e o tempo de execução atômico são representados
usando restrições (constraints) algébricas e/ou lógicas. Nesta técnica, existem nodos e arestas,
sendo que ambos possuem uma variável contadora de execução (xentity) a qual registra a
quantidade máxima que aquela entidade é executada, informação esta obtida da análise do
fluxo. E também, os nodos, que representam os blocos básicos da tarefa, contêm uma variável
de informação de tempo (tentity) que informa o tempo que aquela entidade leva para ser
executada no hardware alvo, dado dependente da análise de baixo nível. Para obter o valor do
WCET, é feito um cálculo que corresponde a max (xentity * tentity), ou seja, é a maximização
do somatório da multiplicação das duas variáveis de cada entidade. Esta abordagem é muito
poderosa e complexa, podendo tratar muitos fluxos complexos. Porém, segundo (ENGBLOM,
J. ERMEDAHLT, A, 2000), esta técnica não encontrará o caminho de execução do pior caso
explicitamente, mas sim, só dará o contador do pior caso em cada nodo. Assim, não há
informações sobre a ordem de execução precisa. E ainda, segundo (WILHELM, R. et al,
2008), o cálculo de limites baseados na técnica IPET, utiliza-se de técnicas de Programação
Linear Inteira (ILP) ou Programação por Restrições (CP), assim, tendo uma complexidade
potencialmente exponencial com relação ao tamanho da tarefa.
2.2.4 Problemas
A abordagem estática de análise, apesar de ser mais exata e segura que as demais,
ainda assim apresenta dificuldades em sua resolução, principalmente levando em
consideração modernos processadores, os quais tornam a análise do WCET um problema
completamente complexo.
Como comentado em (FAUSTER, J.; KIRNER, R.; PUSCHNER, P, 2003), três
problemas fundamentais existem atualmente dificultando a análise do WCET:
Primeiro, a análise do WCET necessita de exato conhecimento sobre os possíveis
caminhos de execução através do código analisado. Derivar essa informação
automaticamente não é, entretanto, possível no caso geral. Isso é devido ao fato de que
o fluxo de controle de um programa tipicamente depende dos dados de entrada do
21
programa e um valor limite para o WCET, dessa forma, não pode ser previsto
puramente através da análise do código;
O segundo problema é obter modelos corretos e exatos sobre o comportamento do
tempo de processadores modernos. Esses processadores tipicamente usam
características como caches e/ou pipelines para aumentar seu desempenho de pico. Os
efeitos dessas características de hardware interferem uma na outra e são dessa forma
difíceis de predizer. Ainda pior, o comportamento do processador geralmente é
escassamente documentado. Esses fatos tomados juntos tornam difícil, se não
impossível, para as ferramentas de análise do WCET construir um correto e exato
modelo de hardware do processador alvo;
O terceiro maior problema é a complexidade da análise do WCET. Além dos
problemas em identificar os caminhos de execução possíveis e obter dados de tempo
do hardware detalhados, a complexidade da análise do WCET por si só é um
problema. O número de caminhos que devem ser analisados para calcular um limite
preciso do WCET cresce exponencialmente com o número de desvios (branches)
consecutivos. Enumeração do caminho completo, dessa maneira, torna-se inviável,
exceto para programas tendo um fluxo de controle muito simples. Para superar esses
problemas, técnicas de análise aproximativas são usadas. Essas aproximações causam
superestimação e consequentemente conduzem a um projeto de sistema com utilização
diminuída dos recursos de hardware.
Ainda em (FAUSTER, J.; KIRNER, R.; PUSCHNER, P, 2003), é apresentada uma solução
em nível de programação que seria o uso de um novo paradigma de engenharia de software
feito para o desenvolvimento de software de tempo real, chamado de programação orientada
ao WCET. A fundamental motivação desse novo paradigma é reduzir o número de instruções
do programa com o fluxo de controle dependente de dados de entrada.
2.3 Análise Estática Vs. Análise Dinâmica
22
Para simplificar as diferenças entre os métodos estáticos e dinâmicos, serão
comparados os pontos de discordância entre ambos, conforme citados em (WILHELM, R. et
al, 2008).
Primeiramente, métodos estáticos computam limites do tempo de execução utilizando
análise do fluxo de controle e cálculo de limites para cobrir todos os caminhos de execução
possíveis. Eles usam abstrações para cobrir todas as possíveis dependências de contexto do
comportamento do processador. O preço que eles pagam para obter resultados seguros é a
necessidade de modelos específicos do comportamento do processador e, possivelmente,
resultados imprecisos tal como superestimar o limite do WCET. Em favor dos métodos
estáticos está o fato de que a análise pode ser realizada sem executar o programa a ser
analisado, o que frequentemente necessita de complexos equipamentos para simular o
hardware e os periféricos do sistema alvo.
Métodos dinâmicos substituem a análise do comportamento do processador pela
medição. Portanto, a menos que todos os caminhos de execução possíveis sejam medidos ou o
processador seja simples o suficiente para permitir que cada medição seja iniciada no estado
inicial de pior caso, algumas mudanças no tempo de execução dependente de contexto podem
ser perdidas e o método, devido a isso, é taxado de inseguro. Para o passo do cálculo da
estimativa, esses métodos podem utilizar análise do fluxo de controle para incluir todos os
possíveis caminhos de execução, ou eles podem simplesmente usar os caminhos de execução
observados, como por exemplo, o número observado de iterações de loop, o qual novamente
faz do método inseguro. As vantagens alegadas para esse método são que ele é mais simples
para aplicar a novos processadores alvos, porque eles não necessitam do modelo do
comportamento do processador e que eles produzem estimativas do WCET e BCET que são
mais precisas, perto dos valores exatos, do que os limites para métodos estáticos,
especialmente para processadores e aplicações complexos.
23
3 LLVM
O LLVM, Low Level Virtual Machine, é uma infraestrutura de compilação, o qual,
segundo (LATTNER, C, 2006), provê componentes modulares e reusáveis para construção de
compiladores, assim, reduzindo o tempo e custo para construir um compilador particular. Esta
infraestrutura possui uma representação intermediária (LLVM IR) bem definida para
programas, além de muitas bibliotecas (componentes) com interfaces limpas e ferramentas
construídas pelas próprias bibliotecas. LLVM provê componentes independentes de
linguagem e máquina alvo, permitindo que códigos de diferentes linguagens possam ser
ligados e otimizados juntos.
LLVM é baseado na representação SSA que, conforme (LATTNER, C.; ADVE, V,
2009), provê segurança de tipo, operações de baixo nível, flexibilidade e a capacidade de
representar todas as linguagens de alto nível limpamente. A forma SSA é uma representação
intermediária na qual cada variável é atribuída exatamente uma vez. Para maiores detalhes
conferir (WIKIPÉDIA. A ENCICLOPÉDIA LIVRE, 2009d).
3.1 Arquitetura
Conforme comentado em (LATTNER, C.; ADVE, V, 2004a), o objetivo do
framework LLVM é permitir sofisticadas transformações em tempo de compilação, ligação,
instalação, execução e durante o tempo inativo, operando na representação LLVM de um
programa em todos os estágios. Porém, para ser posto em prática, o mesmo deve ser
transparente com relação ao desenvolvedor de aplicações e usuários finais. Também, deve ser
eficiente o suficiente para ser usado com aplicações do mundo real.
24
Figura 4: Arquitetura do sistema LLVM, adaptado de (LATTNER, C.; ADVE, V, 2004a).
A figura 4 apresenta um diagrama com uma visão geral da arquitetura de alto nível do
framework LLVM. Segundo (LATTNER, C, 2002a), compiladores tradicionais dividem o
processo de compilação em dois passos: compilar e ligar. Essa separação em duas fases provê
benefícios da compilação isolada, como a necessidade de recompilar somente as unidades
modificadas (embora a aplicação inteira deva ainda ser religada). Um compilador tradicional
compila o código fonte para um arquivo objeto (extensão .o) contendo código de máquina,
enquanto que o linker combina esses mesmos arquivos juntamente com bibliotecas para
formar um programa executável. O linker, além de concatenar os arquivos objetos, também
resolve referências de símbolos.
A abordagem utilizada pelo LLVM retém essas duas fases (compilar e ligar), porém
possui algumas diferenças referentes aos compiladores tradicionais. Essas peculiaridades do
LLVM serão descritas nas subseções seguintes, comentando cada uma das fases da arquitetura
do framework LLVM, como também algumas das otimizações feitas em cada uma dessas
etapas. As fases referidas são:
Tempo de Compilação;
Tempo de Ligação;
Tempo de Execução;
Tempo Inativo.
3.1.1 Tempo de Compilação
Nesta primeira fase da arquitetura LLVM encontram-se os front-ends, os quais são
compiladores estáticos que possuem como responsabilidade principal traduzir programas
escritos em uma determinada linguagem fonte para a representação intermediária (LVIS). O
sistema LLVM suporta front-ends de múltiplas linguagens fontes. Conforme (LATTNER, C,
2002a), além da tarefa primária, ainda nesta fase, cada compilador realiza tantas otimizações
25
estáticas específicas de linguagem quanto possível em cada unidade de tradução para reduzir a
quantidade de trabalho requerida ao otimizador de tempo de ligação. Por fim, uma terceira
funcionalidade para os front-ends seria a invocação de passos LLVM para otimizações global
e interprocedural em um nível mais restrito, que seria o próprio módulo.
Otimizações LLVM são fáceis de serem utilizadas por front-ends, pois essas são
construídas em bibliotecas, sendo modulares e compartilhadas. Assim, compiladores estáticos
podem escolher por utilizar algumas ou até mesmo todas as otimizações disponíveis na
infraestrutura LLVM para aumentar suas potencialidades de geração de código.
3.1.2 Tempo de Ligação
Conforme dito, os front-ends emitem código na representação intermediária LLVM,
os quais são unidos pelo linker LLVM. Esta fase do processo de compilação é a primeira onde
se encontra disponível a maioria do programa para análise e transformação. Dessa forma,
nessa etapa podem ser realizadas otimizações interprocedurais agressivas no programa inteiro.
Essas otimizações operam na representação LLVM diretamente, tomando proveito das
informações semânticas que a mesma contém. As otimizações interprocedurais em LLVM
referem-se a transformações tais como:
Análise de ponteiro sensível ao contexto (Data Structure Analysis);
Construção do grafo de chamadas;
Análise Mod/Ref;
Transformação interprocedural como inlining;
Eliminação de global morta;
Eliminação de argumento morto;
Eliminação de tipo morto;
Propagação de constante;
Eliminação de checagem de limites de array;
Reordenamento de campos de estrutura simples;
Automatic Pool Allocation.
Após as otimizações, as quais são opcionais, serem realizadas, um gerador de código
apropriado para uma determinada arquitetura alvo é selecionado para traduzir o código
26
LLVM para o código nativo da plataforma corrente. Segundo (LATTNER, C, 2002a), caso o
usuário decida usar otimizações pós ligação, uma cópia do bytecode LLVM comprimido é
incluída no executável. Alternativamente, pode-se utilizar um Just-In-Time Execution Engine
o qual invoca o gerador de código apropriado em runtime, traduzindo uma função em tempo
de execução ao invés de gerar código em tempo de ligação.
3.1.3 Tempo de Execução
No momento em que um programa está executando, as regiões mais frequentemente
executados são identificadas, localizando, por exemplo, regiões de loops mais comumente
acessadas. Ao detectar uma região como essa, em tempo de execução, uma biblioteca de
instrumentação runtime instrumenta o código nativo executando a identificar caminhos
executados frequentemente dentro daquela região. Uma vez que os caminhos são
identificados, o código LLVM original é duplicado e otimizações LLVM são realizadas na
sua cópia. Logo após serem realizadas, o código nativo é regenerado, inserindo desvios entre
o código original e o novo código nativo otimizado.
Segundo (LATTNER, C.; ADVE, V, 2004a), essa estratégia é poderosa, pois ela
combina as seguintes três características:
Gerador de código nativo pode ser realizado a frente do tempo usando sofisticados
algoritmos para gerar código de alto desempenho;
O gerador de código nativo e o otimizador em tempo de execução podem trabalhar
juntos desde que eles são, ambos, parte do framework LLVM, permitindo que o
otimizador em tempo de execução explore o suporte do gerador de código;
O otimizador em tempo de execução pode usar informações de alto nível da
representação intermediária LLVM para realizar sofisticas otimizações.
3.1.4 Tempo Inativo
Como dito em (LATTNER, C, 2002a), alguns tipos de aplicações não são
particularmente receptivas às otimizações em tempo de execução, por conta disso, o
27
otimizador runtime não pode permitir gastar uma quantidade de tempo significativa
melhorando um pedaço de código, embora ele pode provavelmente detectar os caminhos mais
frequentemente executados pelo programa.
A fim de suportar esses tipos de aplicações e como a representação LLVM é
preservada permanentemente, segundo (LATTNER, C.; ADVE, V, 2004a), LLVM permite
otimização offline transparente de aplicações durante o tempo inativo em um sistema de
usuário final. Tal otimizador é simplesmente uma versão modificada do otimizador
interprocedural de tempo de ligação, mas com uma maior ênfase em otimizações dirigidas por
profile e específica de alvo. Esse tipo de otimização offline permite ser muito mais agressivo
do que o otimizador runtime.
3.2 LLVM IR
Low Level Virtual Machine Intermediate Representation representa um conjunto
virtual de instruções (LVIS) utilizado pelo LLVM. Essa representação de código é um dos
fatores chaves que diferencia LLVM de outros sistemas. Segundo (LATTNER, C.; ADVE, V,
2004a), a representação é designada para prover informação de alto nível sobre programas, o
que é necessário para suportar sofisticadas análises e transformações, enquanto sendo de
baixo nível o suficiente para representar programas arbitrários e para permitir extensiva
otimização nos compiladores estáticos.
Esse conjunto de instruções captura as operações de processadores comuns, mas evita
restrições específicas de máquina tal como registradores físicos, pipelines e convenções de
chamadas de baixo nível. Conforme (LATTNER, C, 2006) e (LATTNER, C.; ADVE, V,
2009), a representação intermediária possui algumas características marcantes como:
Objetiva ser leve, de baixo nível e ao mesmo tempo expressiva;
Deve ser independente de linguagem alvo, incluindo mistura de linguagens fontes
dentro do mesmo arquivo LLVM e permitindo análise e otimização entre linguagens;
Valores escalares são sempre representados na forma SSA, nunca em memória;
IR é inteiramente “tipada” e seus tipos são rigorosamente checados para consistência;
28
Possui acessos à struct /array explícitos;
IR é facilmente extensível com funções intrínsecas;
Provê um mecanismo para implementar tratamento de exceções;
Como provê informação de tipo, hospeda uma larga variedade de otimizações e
análises.
Completando as características acima com informações de (LATTNER, C.; ADVE, V,
2004a) e (LATTNER, C.; ADVE, V, 2004b), as instruções da representação intermediária
são, na sua maioria, formadas por três endereços de código (three address code), um destino e
dois fontes, como em processadores RISC. Além disso, por usufruir da forma SSA, possui um
conjunto de registradores virtual infinito com informação de tipo, podendo manter valores de
tipos primitivos. Para finalizar, programas transferem valores entre registradores e memória
unicamente via instruções load e store, sendo que, até mesmo essas operações, possuem
ponteiros com referência a tipos.
A estrutura do programa LLVM é simples. Tudo começa com módulos, que nada mais
são do que unidades de compilação, análise e otimização, os quais possuem funções e
variáveis globais. Já as funções contêm seus itens típicos, como argumentos, tipo de retorno,
entre outros, além de blocos básicos, os quais formam o CFG da função. Os blocos básicos,
por sua vez, contêm uma lista de instruções que devem terminar com uma instrução de fluxo
de controle, também chamada de instrução terminadoura, tais como desvios, instruções de
retorno ou chamadas à função. Por fim, as instruções são formadas por um opcode e um vetor
de operandos, sendo que todos os elementos do vetor possuem um tipo associado. Como
resultado da instrução, é produzido um valor que também possui um tipo específico.
Para exemplificar o que foi dito até aqui sobre a representação intermediária LLVM,
as figura 5 e figura 6 mostram códigos, sendo que o primeiro escrito na linguagem de
programação C e, a partir dele, gera-se o código na representação LVIS visualizado na figura
posterior.
29
Figura 5: Código fonte em linguagem C, exemplificando o LLVM IR, adaptado de
(MACHADO, A., 2008).
Figura 6: Código na representação LVIS, exemplificando o LLVM IR, adaptado de
(MACHADO, A., 2008).
3.2.1 Representação Textual, Binária e Em Memória
A representação de código LLVM é designada para ser usada em três diferentes
formatos, os quais são isomórficos, ou seja, equivalentes. São eles:
Como uma representação intermediária em memória, para o compilador trabalhar;
30
Como uma representação binária comprimida em arquivo, apropriado para
carregamento rápido por um compilador Just-In-Time;
Como uma representação em texto, semelhante à linguagem assembly, legível ao
humano.
Isso, segundo (LATTNER, C.; ADVE, V, 2009), permite que o LLVM provenha uma
poderosa representação intermediária para eficientes transformações e análises do compilador,
enquanto provê um meio natural para depurar e visualizar as transformações.
3.2.2 Sistema de Tipo
O sistema de tipo LLVM é uma das características mais importantes da IR. Segundo
(LATTNER, C.; ADVE, V, 2009), sendo “tipada”, habilita um número de otimizações a
serem realizadas na representação intermediária diretamente, sem ter a necessidade de realizar
análise extra antes da transformação. Esse sistema de tipo inclui tipos primitivos independente
de linguagem fonte como, por exemplo, void, integer, floating point (single e double), entre
outros. LLVM também possui, além dos primitivos, tipos derivados, como pointer, array,
structure, vector, etc.
Conforme (LATTNER, C.; ADVE, V, 2004a), cada registrador na forma SSA e cada
objeto de memória explícito têm um tipo associado, sendo que todas as operações obedecem a
regras de tipos estritas. Essa informação de tipo é usada em associação com o opcode da
instrução para determinar a semântica exata de uma instrução. Isso porque a maioria dos
opcodes em LLVM é sobrecarregado, sendo assim, uma instrução como add, a qual realiza
uma soma, pode operar em operandos de qualquer tipo inteiro ou ponto flutuante.
Para finalizar, um forte sistema de tipo torna fácil a leitura do código gerado e permite
novas análises e transformações que não são viáveis na representação de código three address
code normal.
3.2.3 Alocação Explícita de Memória
31
O LVIS é único na maneira de tratar áreas de memória. Todos os objetos endereçáveis
(variáveis locais alocadas na pilha, variáveis globais, funções e memória alocada
dinamicamente) são explicitamente alocados. Para isso, duas instruções de alocação de
memória “tipada” são providas, além de uma adicional para liberação da memória
previamente alocada. A primeira delas, malloc, aloca um ou mais elementos de um
determinado tipo na heap, retornando um ponteiro, com o tipo especificado, para a nova área
de memória. Já para liberar esta área alocada por malloc, a instrução free é utilizada. Outra
instrução utilizada para alocação é a alloc. Essa é similar a primeira, porém ela aloca memória
na pilha (stack frame) da função corrente ao invés da heap. Para essa última, não é necessária
qualquer instrução de liberação de memória, pois ela será automaticamente liberada ao sair de
escopo. Nenhum dado residente na pilha poderá ser alocado sem que seja utilizada a instrução
alloc explicitamente, tornando LLVM um framework com alocação explícita de memória.
Segundo (LATTNER, C, 2002a), variáveis globais e funções (chamadas coletivamente
de valores globais) declaram regiões de memória alocadas estaticamente que são acessadas
através do endereço do objeto e não do efetivo objeto. Isso gera um modelo de memória
unificado no qual todas as operações de memória, incluindo instruções de chamada, ocorrem
através de ponteiros “tipados”. Essa representação também simplifica a análise de acesso à
memória, pois não ocorrem acessos a memória implicitamente.
32
4 LLFLOW
Para o desenvolvimento deste trabalho, foi utilizada como base uma ferramenta de
análise de tempo de execução de alto nível conhecida como llflow. A mesma foi desenvolvida
como trabalho de conclusão de curso por um aluno da Universidade Federal de Santa Catarina
(UFSC) e consiste na validação da infraestrutura LLVM como plataforma para análise do
WCET. Neste capítulo serão descritas algumas características e o funcionamento dessa
ferramenta. Descrições mais aprofundadas podem ser encontradas em (MACHADO, A.,
2008).
4.1 Características
Uma primeira característica da ferramenta llflow é que, como dito, ela realiza apenas a
análise de alto nível, não realizando a análise de baixo nível, a qual leva em conta o
comportamento do hardware alvo. Então, para tornar viável o trabalho, é necessário
considerar que cada instrução executada pelo processador leva uma unidade de tempo para ser
finalizada e, consequentemente, assume-se que o maior tempo de execução (WCET) é obtido
pelo caminho com o maior número de instruções (MACHADO, A., 2008).
Outra peculiaridade dessa ferramenta, talvez a mais importante de todas, é a utilização
do LLVM como sustentação para sua implementação. Como comentado em (MACHADO,
A., 2008), a riqueza de informações da representação intermediária LLVM e das análises já
disponíveis para esta plataforma fornecem ao usuário um conjunto de informações valiosas
sobre o código e, também, parte dos algoritmos necessários para o cálculo do tempo de
execução são simplificados.
33
4.2 Arquitetura
Para a utilização da ferramenta llflow, deve-se primeiramente entender como ela
funciona e sobre o que ela atua. Então, primeiramente, deve-se ter o código fonte do programa
a ser analisado em alguma linguagem de programação a qual possua um compilador que
transforme o código para o conjunto de instruções LLVM. Podem existir vários arquivos
fontes como também somente um.
Seguindo, é utilizado um front-end, o qual compila o código fonte para a
representação intermediária LLVM. Um exemplo de front-end é o llvm-gcc. Ele é uma versão
do gcc que compila programas C/ObjC em objetos nativos, bitcode LLVM (binário), ou em
linguagem assembly LLVM (texto) (LLVM TEAM, 2009). Tanto durante a compilação, com
o próprio front-end, como após, com a ferramenta de otimização (opt), podem-se realizar
algumas otimizações no código gerado, o qual, posteriormente, deverá ser ligado, com a
ferramenta llvm-link, caso possua vários módulos.
Após todas essas etapas, o código binário na representação LLVM é inserido na
ferramenta llflow para a análise do código e geração de informações relacionadas ao fluxo de
controle. Ainda em (MACHADO, A., 2008), é citada uma ferramenta llvm-wcet, a qual
necessitaria da descrição da plataforma alvo para a realização da análise de baixo nível. Essa
ferramenta dependente do hardware de destino seria a responsável não só pelo cálculo de
tempo, como pela geração de código objeto final, com instruções nativas. Porém essa
ferramenta ainda não foi desenvolvida. Todo esse processo é mostrado na figura 7.
34
Figura 7: Arquitetura da ferramenta llflow, obtida de (MACHADO, A., 2008).
4.3 Entradas
Para que a ferramenta llflow faça a análise, necessita-se que sejam fornecidos à
mesma dois arquivos essenciais, a saber:
Código binário na representação LLVM;
Arquivo de configuração com algumas informações sobre as funções existentes no
código para análise.
35
Na subseção seguinte será explicado um pouco mais sobre o arquivo de configuração
utilizado pela ferramenta llflow.
4.3.1 Arquivo de Configuração
Para direcionar a análise, é necessário um arquivo de configuração com algumas
informações úteis, tais como (MACHADO, A., 2008):
Faixas de valores que podem ser retornados por chamadas de funções externas
(funções cujo código não está disponível para análise);
Faixas de valores que podem ser retornados em parâmetros passados por referência às
funções externas;
Funções do programa que devem ser analisadas (pontos de entrada, caso apenas uma
parte do programa seja tarefa de tempo real ou quando o ponto de entrada não for a
função main), com informações sobre as faixas de valores que podem ser aceitos como
parâmetros de entrada.
A figura 8 apresenta um exemplo de um arquivo de configuração, o qual possui duas
funções externas e uma função de tempo real a ser analisada. As funções externas possuem o
formato:
nomeFunção (parâmetro1, parâmetro2, ...) = valorRetorno;
Enquanto que as funções de tempo real são formadas por:
nomeFunção (parâmetro1, parâmetro2, ...);
Onde, para os parâmetros, o valor in significa valores somente de entrada, informando que
seus valores não serão alterados pela função. Os demais valores para os parâmetros e retorno
deverão ser especificados, podendo assumir um simples valor ou um conjunto de valores,
como os demonstrados na figura 8 pelos colchetes ([ ]). Os valores entre [ ] significam os
valores mínimo e máximo que aquele parâmetro ou retorno pode assumir.
36
Figura 8: Arquivo de configuração da ferramenta llflow, retirado de (MACHADO, A., 2008).
4.4 Saídas
Em posse do arquivo binário LLVM e do arquivo de configuração, a ferramenta tem
condições de gerar os resultados esperados, os quais, segundo (MACHADO, A., 2008), são:
Grafo do fluxo de controle estendido, com indicação dos escopos de análise;
Anotações em cada escopo, indicando:
– Número de execuções de cada loop ou recursão;
– Intervalos válidos para as variáveis envolvidas nas decisões do fluxo de controle;
Caminhos possíveis e impossíveis no programa.
Porém, observando os resultados a partir de uma análise de um programa qualquer,
percebe-se que duas das saídas citadas em (MACHADO, A., 2008) não são apresentadas, as
quais são: intervalos válidos para as variáveis envolvidas nas decisões do fluxo de controle e
caminhos possíveis e impossíveis no programa.
Referente à primeira, algo parecido com o citado é apresentado ao decorrer da análise,
onde o programa mostra, quando apropriado, as divisões sofridas pelas variáveis que estão na
forma de conjunto na execução de uma instrução de comparação. Porém, isto não representa
propriamente um resultado final, e sim valores de checagem ao decorrer da execução da
ferramenta de análise.
Com relação à segunda informação citada, mas não apresentada, a ferramenta exibe
somente o caminho do pior caso, sendo que não há nada indicando os caminhos impossíveis
37
para o programa. Então, neste trabalho, foi desenvolvida esta nova funcionalidade, a qual é
apresentada no capítulo 7.
38
5 VALIDANDO A FERRAMENTA LLFLOW
Este trabalho consiste de duas partes, a saber:
Teste e correção da ferramenta de análise do tempo de execução llflow;
Incremento de novas funcionalidades à ferramenta.
Explicando melhor cada parte do trabalho, na primeira será realizado o teste do
aplicativo llflow através de um benchmark próprio para análise do WCET (MÄLARDALEN
WCET RESEARCH GROUP, 2006). Então, serão pegos alguns dos programas desse
benchmark, os quais serão compilados com e/ou sem otimização, dependendo da necessidade
do caso, e serão inseridos na ferramenta llflow para que os resultados sejam exibidos. Caso
erros venham a ocorrer, alterações serão feitas no código fonte do programa llflow para que o
mesmo apresente o resultado esperado. Essa parte será apresentada neste capítulo.
Na segunda etapa, serão incrementadas novas características ao aplicativo llflow, a fim
de que ele se torne uma ferramenta mais robusta e confiável, aumentando a quantidade de
programas que poderão usufruí-lo para o cálculo do WCET. Essa etapa será apresentada neste
e nos próximos capítulos.
Nas próximas seções deste capítulo serão apresentados alguns dos códigos fonte
retirados do benchmark citado, os quais foram utilizados na ferramenta llflow para que as
etapas de verificação e incrementação fossem executadas.
5.1 Busca Binária
O primeiro programa selecionado a partir do benchmark (MÄLARDALEN WCET
RESEARCH GROUP, 2006), apresentado aqui, realiza uma busca binária a partir de um array
com quinze elementos onde cada posição representa uma estrutura (struct) que por fim possui
39
dois inteiros, a chave e o valor. A função analisada, bynary_search, recebe um parâmetro que
representa o valor de uma chave, a qual será procurada dentro do array, retornando o valor
correspondente ou -1 em caso de não existir. A figura 9 contém o código fonte desse
programa.
40
Figura 9: Código fonte do programa que realiza a busca binária, adaptado de
(MÄLARDALEN WCET RESEARCH GROUP, 2006).
Por se tratar do primeiro programa apresentado, serão apresentadas as etapas, passo a
passo, para a realização da análise para que não haja dúvidas com relação ao processo de
análise.
De início, deve-se compilar o código fonte da figura 9 para a representação LVIS. A
realização desse processo de compilação é feita utilizando o front-end llvm-gcc. Essa
ferramenta possui a opção de passagem de parâmetros para a escolha de um código gerado
com ou sem otimização, como também a opção de geração de código binário LLVM ou em
linguagem assembly LLVM (texto). A figura 10 demonstra a geração dos arquivos texto e
binário LLVM, primeiro sem (-O0) e, por seguinte, com (-O3) otimização.
41
Figura 10: Geração dos arquivos texto e binário LLVM.
A figura 11 e a figura 12 apresentam os códigos em linguagem assembly LLVM
gerados a partir da compilação, sendo a primeira sem otimização e a segunda com.
42
Figura 11: Código na representação LVIS, sem otimização (busca binária).
43
Figura 12: Código na representação LVIS, com otimização (busca binária).
44
A figura 13 mostra o arquivo de configuração que será passado como parâmetro
juntamente com o arquivo binário do programa que realiza a busca binária na execução da
ferramenta llflow.
Figura 13: Arquivo de configuração (busca binária).
Finalmente, a figura 14 demonstra a chamada à ferramenta llflow, passando como
parâmetros o programa a ser analisado e o arquivo de configuração.
Figura 14: Chamada à ferramenta llflow.
Na execução do código relativo à busca binária pelo llflow, vários problemas foram
encontrados os quais inviabilizaram a análise do programa. Os defeitos encontrados foram:
Não era possível a criação de uma estrutura dentro de um array;
45
A instrução getelementptr não estava funcionando corretamente. Essa instrução é
usada para obter o endereço de um elemento de uma estrutura de dados agregada
(LATTNER, C.; ADVE, V, 2009);
Operações de comparação com números negativos estavam com problemas;
A instrução de deslocamento para direita (shift right) apresentava resultado errado;
E por fim, a instrução phi não estava implementada. Essa instrução é usada para
implementar o nodo φ no grafo SSA representando a função (LATTNER, C.; ADVE,
V, 2009).
Então, após a correção desses problemas (instruções getelementptr, de comparação e
shift right) e a inclusão de novas funcionalidades (instrução phi e criação de estrutura dentro
de array), tornou-se possível a análise do programa, o qual gerou os resultados mostrados na
figura 15 e na figura 16, sendo a primeira para o código sem otimização e a segunda com.
Figura 15: Resultado da análise, sem otimização (busca binária).
46
Figura 16: Resultado da análise, com otimização (busca binária).
Juntamente com os resultados apresentados, a ferramenta llflow informa o maior
caminho percorrido para os valores passados como parâmetros, de acordo com o número de
instruções executadas. Para a figura 15, obteve-se o resultado:
Comprimento do caminho: 103 instruções.
START, entry, bb5, bb, bb2, bb3, bb5, bb, bb2, bb4, bb5, bb, bb1, bb5, bb6, return
Enquanto que para a figura 16, o resultado foi o seguinte:
Comprimento do caminho: 57 instruções.
47
START, entry, bb5.outer, bb5, bb, bb2, bb3, bb5, bb, bb2, bb4, bb5.outer, bb5, bb,
bb1, bb5, bb6
Para verificar a corretude da ferramenta llflow, serão realizadas duas verificações. A
primeira verifica o valor da variável count apresentado pela ferramenta llflow. Já a segunda,
analisa o número de instruções executadas, informado como “comprimento do caminho” pela
ferramenta, conforme apresentado logo acima.
Para a primeira análise, a seguir é demonstrada a análise do fluxo de controle do
código fonte escrito em C, que representa exatamente o código LVIS sem otimização, para a
comparação com o resultado obtido pela ferramenta:
1º Iteração => Entradas: low = 0, up = 14 -- Saídas: low = 0, up = 6;
2º Iteração => Entradas: low = 0, up = 6 -- Saídas: low = 4, up = 6;
3º Iteração => Entradas: low = 4, up = 6 -- Saídas: low = 4, up = 3;
Retornado valor 250.
Analisando o cálculo realizado e verificando primeiramente o código na representação
LVIS sem otimização, percebe-se claramente que a o bloco básico que realiza a contagem é o
de nome bb5, o qual realiza a instrução while (low <= up). A resposta obtida pela figura 15
nos diz que a análise do bloco básico bb5 executou 4 (quatro) vezes, o que é realmente
correto, pois pela análise feita acima, ocorreram 3 (três) iterações do loop, e de acordo com o
código na representação LVIS, essa instrução de comparação é executada mais uma vez para
por fim sair do loop principal, totalizando o número informado pela análise.
Já para o código com otimização, percebe-se que a legibilidade do mesmo não é tão
grande quanto ao código sem otimização, porém, em compensação, ele possui um tamanho
bem reduzido comparando-os. Então, fazendo o mesmo tipo de análise que foi realizado para
o cálculo sem otimização, percebe-se que os resultados apresentados pela ferramenta são
válidos.
Para a realização da análise do número de instruções executadas, é utilizado o
interpretador fornecido pelo LLVM, chamado lli, o qual executa programas no formato
binário LLVM. Então, para executar os códigos apresentados nas representações LVIS
referentes à busca binária, deve-se adicionar a função main que representa o ponto de entrada
para o código. Logo, foram adicionadas ao código, as seguintes linhas:
define i32 @main() nounwind readonly {
entry:
call i32 @binary_search(i32 9) nounwind
48
ret i32 1
}
Assim, executando o interpretador LLVM, primeiramente para o código sem
otimização, encontra-se como resultado a seguinte informação:
105 interpreter – Number of dynamic instructions executed
Este número obtido deve ser subtraído de 2 (duas) instruções, referentes à função main
adicionada. Então, conferindo com o valor obtido pela ferramenta, 103, percebemos que
representam o mesmo valor. Provando a corretude da ferramenta llflow para esta caso.
Agora, executando o interpretador para o código com otimização, encontra-se o
seguinte resultado:
45 interpreter – Number of dynamic instructions executed
Diminuindo 2 (duas) instruções, obtêm-se 43 instruções, porém o resultado obtido pela
ferramenta llflow foi 57 instruções. Entretanto, o interpretador lli não considera as instruções
phi em sua contagem, logo, deve-se diminuir essas instruções do resultado obtido pela
ferramenta llflow. Essas instruções estão presentes nos blocos básicos bb5.outer e bb5, sendo
que o primeiro possui 3 (três), enquanto que o segundo possui 2 (duas) instruções phi. De
acordo com o caminho demonstrado como resultado pela ferramenta llflow, o bloco básico
bb5.outer é executado 2 (duas) vezes e o bb5, 4 (quatro) vezes. Então, fazendo os cálculos,
obtêm-se:
3 * 2 + 2 * 4 = 14
Subtraindo as instruções phi do total de 57 instruções, resulta em 43 instruções, exatamente o
resultado dado pelo interpretador LLVM, comprovando que a ferramenta llflow retornou o
valor correto.
5.2 Raiz Quadrada
O segundo programa retirado do benchmark (MÄLARDALEN WCET RESEARCH
GROUP, 2006) realiza a operação raiz quadrada de um número qualquer passado como
parâmetro. A figura 17 mostra o código fonte desse programa.
49
50
Figura 17: Código fonte do programa que realiza a raiz quadrada, adaptado de
(MÄLARDALEN WCET RESEARCH GROUP, 2006).
A figura 18 apresenta o código em linguagem assembly LLVM, sem otimização,
gerado a partir da compilação.
51
52
Figura 18: Código na representação LVIS, sem otimização (raiz quadrada).
A figura 19 mostra o arquivo de configuração para a ferramenta llflow.
Figura 19: Arquivo de configuração (raiz quadrada).
Ao deparar-se com o código relativo à raiz quadrada, percebe-se que nele encontram-
se muitas operações sobre ponto flutuante. Mas, a ferramenta llflow não trabalha com esse
tipo de valor, somente com valores inteiros. Logo, precisou-se incluir a opção de usar valores
ponto flutuante, tanto no código fonte como no arquivo de configuração. E, necessitou-se,
também, a implementação de instruções de comparação para ponto flutuante.
53
Com essa nova funcionalidade, foi executada a ferramenta llflow para o código que
executa a raiz quadrada, produzindo o resultado presente na figura 20.
Figura 20: Resultado da análise, sem otimização (raiz quadrada).
Completando o resultado obtido, conforme apresentado pela ferramenta, o maior
caminho encontrado foi:
Comprimento do caminho: 505 instruções.
START, entry, bb1, bb8, bb2, bb3, entry, bb, bb2, return, bb5, bb7, bb8, bb2, bb3,
entry, bb, bb2, return, bb5, bb7, bb8, bb2, bb3, entry, bb, bb2, return, bb5, bb7, bb8,
bb2, bb3, entry, bb, bb2, return, bb5, bb7, bb8, bb2, bb3, entry, bb, bb2, return, bb4,
bb5, bb7, bb8, bb2, bb6, bb7, bb8, bb2, bb6, bb7, bb8, bb2, bb6, bb7, bb8, bb2, bb6,
bb7, bb8, bb2, bb6, bb7, bb8, bb2, bb6, bb7, bb8, bb2, bb6, bb7, bb8, bb2, bb6, bb7,
54
bb8, bb2, bb6, bb7, bb8, bb2, bb6, bb7, bb8, bb2, bb6, bb7, bb8, bb2, bb6, bb7, bb8,
bb2, bb6, bb7, bb8, bb2, bb6, bb7, bb8, bb9, return
Fazer a análise referente ao valor da variável count, para esse código, não é necessário,
pois o loop que a função possui sempre executará 20 (vinte) vezes. Logo, para a função
my_sqrt, o bloco básico bb8, o qual realiza a comparação i < 20 (na realidade a instrução feita
é i <= 19, porém, como a variável i representa um valor inteiro, as operações realizam a
mesma comparação), é executado pelo número de vezes informado pela análise.
Já para a análise referente ao comprimento do caminho, é executado o código na
representação LVIS pelo interpretador LLVM, o qual resulta a informação:
506 interpreter – Number of dynamic instructions executed
Seguindo a mesma metodologia do exemplo anterior, diminuindo 2 (duas) instruções
referentes à função main, obtêm-se 504 instruções, diferentemente da ferramenta llflow, a
qual alcançou o valor 505. Assim, mostra-se que o valor obtido através da análise representa
um valor para o WCET seguro (safe), pois reflete um valor maior do que o real, e ao mesmo
tempo bem próximo a ele (tight), consistindo em um valor utilizável.
5.3 Fibonacci
O próximo programa selecionado para realização de teste foi o famoso fibonacci. Seu
código foi extraído do benchmark (MÄLARDALEN WCET RESEARCH GROUP, 2006) e o
mesmo calcula o valor do número de fibonacci para a posição passada como parâmetro. Para
maiores informações com relação à sequência de fibonacci, consultar (WIKIPÉDIA. A
ENCICLOPÉDIA LIVRE, 2009c). Esse código possui muitas chamadas recursivas, o que
leva a um grande processamento. A figura 21 apresenta o código fonte desse programa.
55
Figura 21: Código fonte do programa que executa a sequência de fibonacci, adaptado de
(MÄLARDALEN WCET RESEARCH GROUP, 2006).
A figura 22 apresenta o código em linguagem assembly LLVM, com otimização
máxima (-O3), gerado a partir da compilação.
56
Figura 22: Código na representação LVIS, com otimização (fibonacci).
A figura 23 mostra o arquivo de configuração para a ferramenta llflow.
Figura 23: Arquivo de configuração (fibonacci).
Esse código, ao ser executado pelo analisador com o arquivo de configuração da
figura 23, apresentava problemas. A ferramenta llflow nunca finalizava a analise devido ao
grande número de caminhos que deveriam ser criados. Então, para viabilizar a análise foram
necessárias algumas correções e implementações, como por exemplo:
A instrução switch que não havia sido implementada;
57
Havia problemas ao utilizar a variável i passada como parâmetro, onde em certos
momentos ela possuía um valor incorreto;
A instrução phi não implementada, também, teve que ser adicionada para esse código;
A instrução de chamada call apresentava problemas;
E quanto à utilização de um conjunto de valores no arquivo de configuração ([0, 10]),
surgiram vários problemas, principalmente relativo à divisão de contexto, que são
ocasionadas por instruções de comparação e switch.
Com todos os problemas solucionados, pode-se executar o código com a ferramenta de
análise de tempo de execução e obteve-se o resultado apresentado na figura 24.
Figura 24: Resultado da análise, com otimização (fibonacci).
58
O maior caminho percorrido, com o parâmetro da função assumindo o valor
especificado, é:
Comprimento do caminho: 594 instruções.
START, entry, bb3.i6, entry, bb3.i6, entry, bb3.i6, entry, bb3.i6, entry, fib.exit8,
fib.exit, fib.exit.i, fib.exit8, bb3.i, entry, bb4, entry, bb4, fib.exit, bb3.i.i, entry,
fib.exit8, fib.exit, entry, bb4, fib.exit.i, fib.exit8, bb3.i, entry, bb3.i6, entry, bb4,
fib.exit.i, fib.exit8, fib.exit, entry, fib.exit8, fib.exit, fib.exit, bb3.i.i, entry, bb3.i6,
entry, fib.exit8, fib.exit, fib.exit.i, fib.exit8, bb3.i, entry, bb4, entry, bb4, fib.exit,
entry, bb3.i6, entry, bb4, fib.exit.i, fib.exit8, fib.exit, fib.exit.i, fib.exit8, bb3.i, entry,
bb3.i6, entry, bb3.i6, entry, bb4, fib.exit.i, fib.exit8, fib.exit, bb3.i.i, entry, bb4, entry,
bb4, fib.exit.i, fib.exit8, bb3.i, entry, fib.exit8, fib.exit, entry, bb4, fib.exit, entry,
bb3.i6, entry, fib.exit8, fib.exit, fib.exit.i, fib.exit8, bb3.i, entry, bb4, entry, bb4,
fib.exit, fib.exit, bb3.i.i, entry, bb3.i6, entry, bb3.i6, entry, fib.exit8, fib.exit, fib.exit.i,
fib.exit8, bb3.i, entry, bb4, entry, bb4, fib.exit, bb3.i.i, entry, fib.exit8, fib.exit, entry,
bb4, fib.exit.i, fib.exit8, bb3.i, entry, bb3.i6, entry, bb4, fib.exit.i, fib.exit8, fib.exit,
entry, fib.exit8, fib.exit, fib.exit, entry, bb3.i6, entry, bb3.i6, entry, bb4, fib.exit.i,
fib.exit8, fib.exit, bb3.i.i, entry, bb4, entry, bb4, fib.exit.i, fib.exit8, bb3.i, entry,
fib.exit8, fib.exit, entry, bb4, fib.exit, fib.exit.i, fib.exit8, bb3.i, entry, bb3.i6, entry,
bb3.i6, entry, bb3.i6, entry, bb4, fib.exit.i, fib.exit8, fib.exit, bb3.i.i, entry, bb4, entry,
bb4, fib.exit.i, fib.exit8, bb3.i, entry, fib.exit8, fib.exit, entry, bb4, fib.exit, bb3.i.i,
entry, bb3.i6, entry, bb4, fib.exit.i, fib.exit8, fib.exit, entry, fib.exit8, fib.exit, fib.exit.i,
fib.exit8, bb3.i, entry, bb3.i6, entry, fib.exit8, fib.exit, fib.exit.i, fib.exit8, bb3.i, entry,
bb4, entry, bb4, fib.exit, entry, bb3.i6, entry, bb4, fib.exit.i, fib.exit8, fib.exit, fib.exit,
entry, bb3.i6, entry, bb3.i6, entry, fib.exit8, fib.exit, fib.exit.i, fib.exit8, bb3.i, entry,
bb4, entry, bb4, fib.exit, bb3.i.i, entry, fib.exit8, fib.exit, entry, bb4, fib.exit.i, fib.exit8,
bb3.i, entry, bb3.i6, entry, bb4, fib.exit.i, fib.exit8, fib.exit, entry, fib.exit8, fib.exit,
fib.exit, fib.exit
De acordo com o arquivo de configuração, o valor passado como parâmetro para a
função fib a ser analisada representa um conjunto, sendo que o valor mínimo é 0 (zero) e o
máximo 10 (dez). Para a função fibonacci, nota-se claramente, que quanto maior o valor do
parâmetro, maior será o caminho percorrido pela função para que o resultado seja obtido.
Logo, a resposta final cedida pela ferramenta llflow deverá ser o mesmo valor obtido para o
parâmetro de número 10 (dez), pois já conhecemos a priori que este caso representa o maior
caminho possível. É importante salientar que a ferramenta llflow realiza o cálculo do pior
59
caminho utilizando a definição de conjuntos, então, realizar-se-á o cálculo para todos os
valores entre 0 (zero) e 10 (dez).
A seguir, será realizado o cálculo do código da função presente na figura 22 de acordo
com as informações já mencionadas acima. A contagem dá-se pelo número de vezes que o
bloco básico entry é chamado:
i = 0 => Número Iterações = 1;
i = 1 => Número Iterações = 1;
i = 2 => Número Iterações = 1;
i = 3 => Número Iterações = (1 + Nº Iterações i = 1) = 2;
i = 4 => Número Iterações = (1 + Nº Iterações i = 2 + Nº Iterações i = 1 + Nº
Iterações i = 0) = 4;
i = 5 => Número Iterações = (1 + Nº Iterações i = 3 + Nº Iterações i = 1 + Nº
Iterações i = 0 + Nº Iterações i = 2 + Nº Iterações i = 1) = 7;
i = 6 => Número Iterações = (1 + Nº Iterações i = 4 + Nº Iterações i = 2 + Nº
Iterações i = 1 + Nº Iterações i = 3 + Nº Iterações i = 2) = 10;
i = 7 => Número Iterações = (1 + Nº Iterações i = 5 + Nº Iterações i = 3 + Nº
Iterações i = 2 + Nº Iterações i = 4 + Nº Iterações i = 3) = 17;
i = 8 => Número Iterações = (1 + Nº Iterações i = 6 + Nº Iterações i = 4 + Nº
Iterações i = 3 + Nº Iterações i = 5 + Nº Iterações i = 4) = 28;
i = 9 => Número Iterações = (1 + Nº Iterações i = 7 + Nº Iterações i = 5 + Nº
Iterações i = 4 + Nº Iterações i = 6 + Nº Iterações i = 5) = 46;
i = 10 => Número Iterações = (1 + Nº Iterações i = 8 + Nº Iterações i = 6 + Nº
Iterações i = 5 + Nº Iterações i = 7 + Nº Iterações i = 6) = 73;
Como informado pela análise apresentada, quando o valor da variável i assume 10
(dez), o bloco básico de nome entry é chamado 73 vezes ao total. Porém, para a ferramenta
llflow, como a função já se inicia na própria função entry, a primeira vez que ela é executada
não é adicionada ao valor total. Logo, para todos os valores obtidos no cálculo acima, o
analisador assumirá que o valor da variável count será sempre uma unidade menor em relação
ao demonstrado. Então, prova-se que a ferramenta apresentou o valor count esperado para a
função fibonacci com os valores de entrada sendo um intervalo.
Agora, para realizar a análise do comprimento do caminho, executa-se o interpretador
lli para todos os valores possíveis como parâmetro, de 0 (zero) a 10 (dez), e obtêm-se:
i = 0 => 4 interpreter - Number of dynamic instructions executed
60
i = 1 => 4 interpreter - Number of dynamic instructions executed
i = 2 => 6 interpreter - Number of dynamic instructions executed
i = 3 => 13 interpreter - Number of dynamic instructions executed
i = 4 => 25 interpreter - Number of dynamic instructions executed
i = 5 => 44 interpreter - Number of dynamic instructions executed
i = 6 => 67 interpreter - Number of dynamic instructions executed
i = 7 => 114 interpreter - Number of dynamic instructions executed
i = 8 => 187 interpreter - Number of dynamic instructions executed
i = 9 => 307 interpreter - Number of dynamic instructions executed
i = 10 => 492 interpreter - Number of dynamic instructions executed
Assim, o maior valor, representado pelo parâmetro 10 (dez), é 492. Subtraindo as duas
instruções da função main, é obtido o valor 490. Mas a ferramenta llflow retornou o número
594 como resultado, porém, nesta contagem estão incluídas as instruções phi, as quais não
estão presentes no cálculo realizado pelo interpretador.
Realizando o cálculo da quantidade de instruções phi para o caminho apresentado,
encontra-se 104 instruções, as quais devem ser diminuídas do total de 594, resultando em 490,
exatamente o valor encontrado pelo interpretador, provando assim, mais uma vez, a eficiência
da ferramenta llflow.
61
6 INCREMENTANDO A FERRAMENTA LLFLOW - PARTE 1
Além das adições e modificações comentadas no capítulo 5, incrementou-se ainda
uma nova funcionalidade que merece destaque e, por isso, será apresentada neste capítulo.
Essa característica foi desenvolvida devido ao problema que a ferramenta llflow possuía em
lidar com análise de loops aninhados.
No trabalho de (MACHADO, A., 2008), cita-se este problema como uma limitação do
protótipo e são sugeridos dois possíveis contornos para tal, que seriam:
Ao invés de anotar os escopos com números de iterações, anotar cada bloco básico, e
enumerar todos os caminhos no programa, mostrando como resultado o maior
caminho, e não o grafo de fluxo de controle;
Realizar anotações simbólicas, ou seja, deixar o número de iterações do loop interno
como uma função da iteração do loop externo. Esta abordagem tornaria mais
complexa a análise final para identificação do caminho no grafo que representa o pior
caso para o cálculo de tempo.
Porém, neste trabalho, realizou-se outra solução que se assemelha com a segunda
opção. O cálculo do número de iterações ainda continua a cargo dos escopos, porém a
contagem para os loops internos é realizada para cada iteração do loop externo
separadamente, resultando em um valor final para cada iteração.
A seguir, serão demonstrados os efeitos dessa nova solução para alguns exemplos
retirados do benchmark (MÄLARDALEN WCET RESEARCH GROUP, 2006), seguindo a
mesma metodologia apresentada no capítulo 5.
62
6.1 Janne Complex
Este código, chamado Janne Complex, apresenta dois loops, onde o número máximo
de iterações do loop interno depende da iteração atual do loop externo. A figura 25 apresenta
o código fonte desse programa.
Figura 25: Código fonte do programa com loops aninhados dependentes, adaptado de
(MÄLARDALEN WCET RESEARCH GROUP, 2006).
63
A figura 26 apresenta o código em linguagem assembly LLVM, sem otimização,
gerado a partir da compilação do código fonte.
64
Figura 26: Código na representação LVIS, sem otimização (janne complex).
A figura 27 mostra o arquivo de configuração para a ferramenta llflow.
Figura 27: Arquivo de configuração (janne complex).
Por fim, para este exemplo, serão apresentadas duas imagens, a figura 28 possui o
grafo gerado pela ferramenta llflow original, desenvolvida por (MACHADO, A., 2008),
enquanto que a figura 29 representa o fluxo de controle modificado neste trabalho.
65
Figura 28: Resultado da análise da ferramenta llflow original, sem otimização (janne
complex).
66
Figura 29: Resultado da análise da ferramenta llflow modificada, sem otimização (janne
complex).
67
Como demonstrado pelas figuras, o código original conduzia a erros no cálculo, pois,
informava o número total de iterações do loop interno. Porém, com as modificações
executadas, o loop interno tornou-se dependente do externo, sendo que para cada iteração do
loop externo, haverá uma contagem separada para o interno.
Abaixo, será apresentada a contagem dos loops realizada para comparação com o
valor obtido:
1º Iteração Loop Externo => 0 Iteração Loop Interno
2º Iteração Loop Externo => 9 Iterações Loop Interno
3º Iteração Loop Externo => 1 Iteração Loop Interno
4º Iteração Loop Externo => 0 Iteração Loop Interno
5º Iteração Loop Externo => 1 Iteração Loop Interno
6º Iteração Loop Externo => 0 Iteração Loop Interno
7º Iteração Loop Externo => 1 Iteração Loop Interno
8º Iteração Loop Externo => 0 Iteração Loop Interno
9º Iteração Loop Externo => 0 Iteração Loop Interno
Logo, nota-se pelo cálculo que o loop externo é executado 9 (nove) vezes, enquanto
que para cada uma dessas execuções, o número de iterações do loop interno é variável. Vale
ressaltar que, para o código LVIS, os blocos básicos que executam as contagens sempre são
executados uma vez a mais, pois, antes de finalizar os loops, deve-se fazer a comparação uma
última vez.
Assim, o resultado apresentado na figura 29 está correto, pois informa que o loop
externo executa 10 (dez) vezes, enquanto que o loop interno resulta em 1, 10, 2, 1, 2, 1, 2, 1,
1, 0, informando que na 1º iteração do loop externo, o loop interno é executado 1 (uma) única
vez, na 2º iteração do externo, 10 (dez) vezes são executadas o interno e assim
sucessivamente.
68
6.2 Insertion Sort
Este programa, selecionado para análise de loops aninhados, realiza uma operação em
arrays chamada insertion sort, o qual, segundo (WIKIPÉDIA. A ENCICLOPÉDIA LIVRE,
2009b), é um simples algoritmo de ordenação, eficiente quando aplicado a um pequeno
número de elementos. Em termos gerais, ele percorre um vetor de elementos da esquerda para
a direita e à medida que avança vai deixando os elementos mais à esquerda ordenados. Seu
código também foi extraído do benchmark (MÄLARDALEN WCET RESEARCH GROUP,
2006). A figura 30 apresenta o código fonte do programa comentado.
69
Figura 30: Código fonte do programa insertion sort, adaptado de (MÄLARDALEN WCET
RESEARCH GROUP, 2006).
A figura 31 apresenta o código em linguagem assembly LLVM, com otimização
máxima, gerado a partir da compilação.
70
Figura 31: Código na representação LVIS, com otimização (insertion sort).
Já a figura 32 apresenta o arquivo de configuração para a ferramenta llflow.
Figura 32: Arquivo de configuração (insertion sort).
Esse código, ao ser executado pelo analisador com o arquivo de configuração
apresentado, gerou o resultado da figura 33.
Conforme demonstrado no resultado, nota-se que para ordenar um array com 11
(onze) números inteiros, totalmente invertido com relação ao objetivo final, são necessárias 9
(nove) iterações do loop externo e para cada loop interno o número de iterações é
incrementado em 1 (um), começando com uma única iteração.
71
Figura 33: Resultado da análise, com otimização (insertion sort).
72
6.3 Busca em Array Multidimensional
O último exemplo apresentado neste trabalho representa um programa, também
selecionado do benchmark (MÄLARDALEN WCET RESEARCH GROUP, 2006), o qual
realiza uma busca em um array de 4 (quatro) dimensões. O código fonte está demonstrado na
figura 34.
73
74
Figura 34: Código fonte do programa de busca em array multidimensional, adaptado de
(MÄLARDALEN WCET RESEARCH GROUP, 2006).
75
A figura 35 apresenta o código em linguagem assembly LLVM, sem otimização,
gerado a partir da compilação, sendo que o código relativo à inicialização dos arrays (keys e
answer) foram suprimidos devido aos seus imensos tamanhos.
76
Figura 35: Código na representação LVIS, sem otimização (busca array multidimensional).
77
A figura 36 mostra o arquivo de configuração para a ferramenta llflow.
Figura 36: Arquivo de configuração (busca array multidimensional).
Com esse código, percebeu-se que o analisador não aceitava array com múltiplas
dimensões. Logo, foi necessário modificar a ferramenta llflow para que fosse possível a
criação de arrays com qualquer número de dimensões.
Arrumado isso, executando o programa no analisador de tempo de execução obteve-se
um resultado mais complexo do que os demais exemplos, o qual é apresentado na figura 37.
78
Figura 37: Resultado da análise, sem otimização (busca array multidimensional).
79
7 INCREMENTANDO A FERRAMENTA LLFLOW - PARTE 2
Neste capítulo será apresentada uma nova funcionalidade que talvez seja a mais
importante realizada neste trabalho. Ela refere-se à determinação dos blocos básicos
inalcançáveis, ou também chamados de mortos, significando que nunca serão atingidos por
qualquer caminho que a função possa seguir para os valores passados como parâmetros.
Isto é importante, pois se pode eliminar este pedaço de código sem causar mudanças
no resultado final obtido, conseguindo assim, uma redução no tamanho do código, item
importante quando há limitações de memória.
Outro fator que cabe ressaltar é que, com relação ao cálculo do WCET, este pedaço de
código considerado inalcançável pode ser eliminado da contagem do resultado final,
resultando assim em um valor mais próximo do real, tornando um cálculo mais preciso. Essa
característica será demonstrada através dos códigos exemplos apresentados a seguir.
7.1 Teste de Código Morto
Este código foi desenvolvido com a finalidade, única e exclusiva, de realizar testes na
ferramenta llflow com relação à nova funcionalidade introduzida, a qual representa a
descoberta de código inalcançável. A figura 38 apresenta o código fonte desse programa,
enquanto que a figura 39 demonstra o código em linguagem assembly LLVM, com
otimização, gerado a partir da compilação do código fonte. Por fim, a figura 40 representa o
arquivo de configuração para a ferramenta llflow.
80
Figura 38: Código fonte do programa de teste de código morto.
Figura 39: Código na representação LVIS, com otimização (teste de código morto).
81
Figura 40: Arquivo de configuração (teste de código morto).
Analisando o código, percebe-se que a primeira instrução da função, if ((x > y) && (y
> z)), compara as variáveis passadas como parâmetro. Logo abaixo, há outra instrução de
comparação, if (x > z), a qual se trata de uma informação sempre verdadeira, pois o if mais
externo já possui esta comparação subentendida. Então, sendo uma tautologia, o código
referente ao else desta instrução no código em C e situado no bloco básico bb4 na LLVM IR
nunca é executado. Portando, conforme implementado, ao final da análise, além do resultado
apresentado na figura 41, são apresentadas as seguintes informações:
Comprimento do caminho: 8 instruções.
START, entry, bb, bb3
BBs inviáveis: bb4
Vale ressaltar que vários outros testes foram executados, sendo que os arquivos de
configurações informavam valores diferentes para as parâmetros presentes no código. Todos
os testes mostraram-se corretos, informando no resultado final o bloco básico bb4 como sendo
inalcançável.
Figura 41: Resultado da análise, com otimização (teste de código morto).
82
7.2 Cover
Este código, retirado de (MÄLARDALEN WCET RESEARCH GROUP, 2006), tem
como objetivo testar vários caminhos, sendo que existe um loop for com várias instruções
switch case. A figura 42 apresenta o código fonte, a figura 43 o código em linguagem
assembly LLVM e, finalmente, a figura 44 apresenta o arquivo de configuração utilizado na
análise.
Figura 42: Código fonte do programa cover, adaptado de (MÄLARDALEN WCET
RESEARCH GROUP, 2006).
83
84
Figura 43: Código na representação LVIS, sem otimização (cover).
85
Figura 44: Arquivo de configuração (cover).
De acordo com o código fonte e os valores passados como parâmetro, percebe-se que
a instrução for irá iterar por no máximo 3 (três) vezes, executando assim as instruções
presentes nas três primeiras cláusulas case do switch. Desse modo, as instruções case
restantes nunca serão executados, sendo assim, são consideradas blocos básicos mortos.
Para este código, obteve-se como resultado o grafo apresentado na figura 45 e,
também, as seguintes informações:
Comprimento do caminho: 64 instruções.
START, entry, bb13, bb, bb1, bb12, bb13, bb, bb2, bb12, bb13, bb, bb3, bb12, bb13,
bb14, return
BBs inviáveis: bb4, bb5, bb6, bb7, bb8, bb9, bb10, bb11
Lembrando que este resultado foi obtido de acordo com o arquivo de configuração da
figura 44. Outro teste foi realizado, sendo que o arquivo de configuração informava o
intervalo [0, 11] à ferramenta. Para este, encontraram-se os seguintes resultados, informando
que todos os blocos básicos são alcançáveis:
Comprimento do caminho: 176 instruções.
START, entry, bb13, bb, bb1, bb12, bb13, bb, bb2, bb12, bb13, bb, bb3, bb12, bb13,
bb, bb4, bb12, bb13, bb, bb5, bb12, bb13, bb, bb6, bb12, bb13, bb, bb7, bb12, bb13,
bb, bb8, bb12, bb13, bb, bb9, bb12, bb13, bb, bb10, bb12, bb13, bb, bb11, bb12, bb13,
bb14, return
BBs inviáveis:
86
Figura 45: Resultado da análise, sem otimização (cover).
87
8 CONCLUSÃO
8.1 Considerações Finais
Os sistemas embarcados, mais especificamente os de tempo real crítico, representam
uma parcela significativa dos aplicativos atuais e, devido à sua natureza, merecem uma
atenção especial quanto a sua validação. A ferramenta llflow foi desenvolvida com a
finalidade de, para esses sistemas, obter seu fluxo de controle para que posteriormente, na
realização da análise de baixo nível, possa-se garantir o tempo de execução do pior caso
(WCET), tornando o sistema confiável e útil.
Essa ferramenta, llflow, utiliza-se do LLVM, uma infraestrutura de compilação muito
ativa no momento, a qual provê uma série de facilidades e benefícios, tornando seu uso uma
decisão extremamente oportuna. O analisador proposto e implementado por (MACHADO, A.,
2008) possui uma estrutura excelente em relação ao seu código, seu uso é extremamente
simples e seus resultados fáceis de entender. Porém, não possuía a robustez necessária para a
utilização em diversos programas, limitando-se a somente alguns mais simples.
Com as modificações feitas nesse trabalho na ferramenta llflow, sua implementação
aderiu novas funcionalidades e apresentou uma melhoria na questão da correção relativa às
características já existentes. Isso a tornou mais robusta e completa, deixando-a menos
susceptível a erros.
Logo, este trabalho foi de grande valia, pois serviu para a realização de um estudo
aprofundado sobre a área em que o situa, a saber, análise de tempo de execução e a
infraestrutura LLVM, como também para o conhecimento da ferramenta llflow,
proporcionando a análise de seu código fonte e contribuindo com sua implementação.
88
8.2 Trabalhos Futuros
A partir desse trabalho, nota-se claramente, observando a figura 7, que visando à
completude do analisador do tempo de execução para o pior caso, necessita-se da
implementação de uma ferramenta, cujo nome proposto por (MACHADO, A., 2008) em seu
trabalho foi llvm-wcet. Essa nova ferramenta fará a análise de baixo nível, tendo como
entrada o resultado da análise do fluxo de controle executada pela ferramenta llflow, como
também a descrição do hardware alvo, sendo que esta deve simular o comportamento da
plataforma fielmente.
Outras propostas, também citadas por (MACHADO, A., 2008), seriam usar mais
eficientemente as informações de análise providas pela infraestrutura LLVM e, também,
implementar as anotações de resultados da análise por bloco básico ao invés de anotações por
escopo, como são feitas atualmente.
Ainda como um possível trabalho futuro, envolvendo prioritariamente a área de
programação gráfica, poderia ser realizada a implementação de um ambiente gráfico para a
ferramenta llflow, que atualmente só funciona em modo texto. Juntamente a isso, seria
interessante também, que ao final da análise fosse gerado automaticamente um arquivo
semelhante aos apresentados nesse trabalho para demonstrar os resultados da análise (por
exemplo a figura 15), pois o resultado se dá na forma de um arquivo texto com os nodos e
arestas, tendo o usuário que desenvolver o arquivo em formato gráfico em um editor de
preferência se desejado.
89
REFERÊNCIAS
CANTU, E. M. (2008). Geração de Código para a Máquina Virtual LLVM a partir de
Programas Escritos na Linguagem de Programação JAVA (Tradutor Java – LLVM).
Trabalho de conclusão do curso de Sistemas de Informação, Universidade Federal de Santa
Catarina, Florianópolis, Santa Catarina, Brasil.
ENGBLOM, J. (2002). Worst-case Execution Time Analysis.
ENGBLOM, J. ERMEDAHLT, A. (2000). Modeling Complex Flows for Worst-case
Execution Time Analysis.
FAUSTER, J.; KIRNER, R.; PUSCHNER, P. (2003). Intelligent Editor for Writing Worst-
Case-Execution-Time-Oriented Programs.
FERDINAND, C.; HECKMANN, R. (2008). Worst-Case Execution Time - A Tool
Provider’s Perspective.
GÓES, J. A. (2000). Estimação de Tempo de Execução de Programas a partir de Arquivos-
Objeto. Trabalho de conclusão no programa de pós-graduação do curso de Engenharia
Elétrica e Informática Industrial, Centro Federal de Educação Tecnológica do Paraná, Paraná,
Curitiba, Brasil.
GUSTAFSSON, J.; ERMEDAHL, A.; LISPER, B. (2006). Algorithms for Infeasible Path
Calculation.
LATTNER, C. (2006). Introduction to the LLVM Compiler Infrastructure.
LATTNER, C. (2002a). LLVM: An Infrastructure for Multi-Stage Optimization.
LATTNER, C. (2002b). Macroscopic Data Structure Analysis and Optimization.
LATTNER, C.; ADVE, V. (2009). LLVM Language Reference Manual. Acesso em 28 de
Fevereiro de 2009, disponível em http://www.llvm.org/docs/LangRef.html
LATTNER, C.; ADVE, V. (2004a). LLVM: A Compilation Framework for Lifelong Program
Analysis & Transformation.
LATTNER, C.; ADVE, V. (2004b). The LLVM Compiler Framework and Infrastructure.
LLVM TEAM. (2009). LLVM C Front-End. Acesso em 4 de Agosto de 2009, disponível em
http://llvm.org/cmds/llvmgcc.html
LLVM TEAM. (2002). The LLVM Compiler Infrastructure Project. Acesso em 28 de
Fevereiro de 2009, disponível em http://www.llvm.org
90
MACHADO, A. (2008). Análise de Tempo de Execução em Alto Nível para Sistemas de
Tempo Real Utilizando-se o Framework LLVM. Trabalho de conclusão do curso de Sistemas
de Informação, Universidade Federal de Santa Catarina, Florianópolis, Santa Catarina, Brasil.
MÄLARDALEN WCET RESEARCH GROUP. (2006). WCET Project. Acesso em 29 de
Abril de 2009, disponível em http://www.mrtc.mdh.se/projects/wcet/benchmarks.html
OLIVEIRA, B. C.; SANTOS, M. M.; DESCHAMPS, F. (2006). Cálculo do Tempo de
Execução de Códigos no Pior Caso (WCET) em Aplicações de Tempo Real: Um Estudo de
Caso.
WIKIPÉDIA. A ENCICLOPÉDIA LIVRE. (2009a). Embedded System. Acesso em 28 de
Fevereiro de 2009, disponível em http://en.wikipedia.org/wiki/Embedded_system
WIKIPÉDIA. A ENCICLOPÉDIA LIVRE. (2009b). Insertion Sort. Acesso em 16 de Agosto
de 2009, disponível em http://pt.wikipedia.org/wiki/Insertion_sort
WIKIPÉDIA. A ENCICLOPÉDIA LIVRE. (2009c). Número de Fibonacci. Acesso em 7 de
Agosto de 2009, disponível em http://pt.wikipedia.org/wiki/Número_de_Fibonacci
WIKIPÉDIA. A ENCICLOPÉDIA LIVRE. (2009d). Static Single Assignment Form. Acesso
em 4 de Abril de 2009, disponível em
http://en.wikipedia.org/wiki/Static_single_assignment_form
WILHELM, R. et al. (2008). The Worst-Case Execution Time Problem - Overview of
Methods and Survey of Tools.
91
APÊNDICE A - ARTIGO
92
Análise de Tempo de Execução Utilizando LLVM
LEONARDO MACCARI RUFINO
UFSC - Universidade Federal de Santa Catarina
INE - Departamento de Informática e Estatística
Florianópolis (SC), Brasil
Resumo: Os sistemas embarcados dominam o mercado de diversas áreas comerciais hoje em dia.
Muitos desses sistemas podem também ser classificados como sistemas de tempo real, os quais
podem se dividir em críticos e brandos. Os sistemas de tempo real crítico necessitam de uma
validação quanto a sua correta implementação. Essa validação pode ser feita através de técnicas
estáticas ou dinâmicas. Nesse trabalho, será explicado como realizar essa análise, dando ênfase à
análise estática, explicando cada uma de suas etapas que são: análise do fluxo de controle, análise
de baixo nível e por fim o cálculo. Também será comentado sobre a infraestrutura de compilação
LLVM, a qual está muito ativa no momento, descrevendo seus objetivos e sua representação
intermediária, a qual representa um dos fatores chaves que o diferencia dos demais sistemas. Esse
framework foi utilizado como base para a implementação da ferramenta llflow, a qual será
apresentada nesse trabalho. Para finalizar, realizar-se-ão testes, correções e inclusões de novas
funcionalidades na ferramenta llflow.
Palavras-Chave: Análise de Tempo de Execução, WCET, Tempo de Execução do Pior Caso,
Sistemas de Tempo Real, LLVM, llflow.
1 Introdução
Com o passar dos tempos, o computador
tornou-se uma importante ferramenta na vida da
população de qualquer ponto do planeta. Cada dia
mais os computadores invadem as casas das
pessoas, muitas vezes sem que sejam percebidos. É
o caso dos sistemas embarcados (ou também
chamados de sistemas embutidos, embedded
systems) que são, segundo (ENGBLOM, J, 2002),
“um computador que não se parece com um
computador”, ou também, melhor explicado em
(MACHADO, A., 2008), “construídos com
propósitos específicos e pré-definidos e, em função
disto, possuírem características que favorecem o
uso para este propósito e dificultam o uso para
outros fins”. Sistemas embarcados estão localizados
em dispositivos que tenham algum processamento
feito por um microprocessador encapsulado.
Exemplos estão por toda parte, como em
brinquedos, eletrodomésticos, aparelhos celulares,
automóveis, aviões e mais uma diversidade de
produtos.
Sistemas embarcados muitas vezes podem
também ser classificados como estando no grupo
dos sistemas de tempo real (real-time systems).
Estes representam os sistemas computacionais que
possuem uma característica marcante em comum, a
qual diz que para que o sistema seja considerado
correto, além de apresentar as funcionalidades
esperadas, também deve responder dentro de um
tempo estabelecido aos estímulos que recebem, ou
seja, deve-se garantir que as ações sejam
executadas dentro de um intervalo de tempo pré-
determinado.
Os sistemas de tempo real podem ser
divididos em duas classes, distinguíveis por seus
requisitos temporais e de confiabilidade, que são os
sistemas de tempo real brando (soft real-time
systems) e sistemas de tempo real crítico (hard real-
time systems). O primeiro caracteriza-se por
possuir um prazo de resposta mais flexível em
relação ao outro, ou seja, em sistemas brandos, o
descumprimento ocasional de um prazo de tempo
pode ser aceito sem grandes problemas. Estes
possuem requisitos de segurança não-críticos. Já os
sistemas de tempo real crítico, são caracterizados
por possuírem um prazo de resposta estrito, ou seja,
seu comportamento deve ser previsível até mesmo
quando se está executando em sobrecarga. Estes
possuem requisitos de segurança críticos, sendo que
se o sistema chegar a falhar ou mesmo não
responder dentro do tempo pré-estabelecido,
problemas mais graves poderão ocorrer. Um
exemplo de um sistema de tempo real brando seria
o de um aparelho de som e, do outro lado, um
exemplo de um sistema crítico seria o controle do
sistema de “air bag” de carros.
Com a finalidade de assegurar a correta
execução de sistemas de tempo real com relação ao
tempo de execução, algumas técnicas chamadas de
análise de tempo de execução são utilizadas, as
2
quais serão apresentadas a seguir. E, também, para
a realização deste trabalho, será utilizada uma
ferramenta em ascensão no momento chamada
LLVM (Low Level Virtual Machine), apresentada
adiante.
2 Análise do Tempo de Execução do Pior Caso
O propósito da análise do WCET é prover
uma informação a priori sobre o pior tempo de
execução possível de um pedaço de código antes de
usá-lo em um sistema, conforme mencionado em
(ENGBLOM, J. ERMEDAHLT, A, 2000). Sendo
assim, o domínio tradicional do cálculo do WCET
está situado nos sistemas de tempo real crítico, para
que haja uma garantia satisfatória do
comportamento do sistema em todas as
circunstâncias.
O cálculo do tempo de execução do pior
caso pode ser realizado através da análise dinâmica
e estática. Essas serão comentadas a seguir,
enfatizando a técnica estática.
2.1 Análise Dinâmica
Para obter resultados através da técnica de
análise dinâmica, são realizadas medições da tarefa,
ou de suas partes, através da execução em um dado
hardware ou um simulador, para algum conjunto de
entradas. Como citado em (ENGBLOM, J, 2002) e
(WILHELM, R. et al, 2008), existe uma
metodologia para este tipo de análise, que seria:
Determinar a entrada e o estado inicial do
pior caso;
Executar e medir;
Adicionar uma margem segura.
Porém alguns problemas são notados,
como a dificuldade ou impossibilidade de encontrar
o valor da entrada e do estado inicial que resultarão
no tempo de execução de pior caso. Além disso,
outro problema seria o fato desta técnica nunca
superestimar o valor do WCET, geralmente
subestimando-o.
Desta forma, esta técnica pode ser útil para
aplicações que não requerem garantias do tempo de
pior caso encontrado, sendo assim, preferivelmente
utilizada pra sistemas de tempo real não crítico, ou
seja, brando. Como dito em (WILHELM, R. et al,
2008), a análise dinâmica pode dar ao
desenvolvedor uma percepção sobre o tempo de
execução nos casos comuns e também a
porcentagem das ocorrências do pior caso.
Garantias de que o limite obtido é um valor seguro
podem ser conseguidas somente quando a
arquitetura utilizada é simples. Além disso, esta
técnica também pode ser utilizada para prover
validação para abordagens de análise estática.
2.2 Análise Estática
Neste tipo de análise não é necessária a
presença do código de execução para o hardware
real ou um simulador. Como dito em (WILHELM,
R. et al, 2008), é preferivelmente pego o código por
si só, talvez junto de algumas anotações, utilizando-
o para analisar o conjunto de caminhos do fluxo de
controle possível para a tarefa, posteriormente
combinando o fluxo de controle com alguns
modelos abstratos da arquitetura do hardware, e
assim, obtendo o limite superior para essa junção.
Com a análise estática, existe uma garantia de que
os resultados obtidos para o WCET sejam seguros
(safe), além da tentativa de ser o mais próximo
possível do valor real (tight), sendo desta forma,
valores utilizáveis.
O cálculo da estimativa de tempo do pior
caso utilizando esta técnica é obtido através de três
passos como ilustrado na figura 2: análise do fluxo
de controle, análise de baixo nível e cálculo.
Figura 46: Estrutura da análise estática do WCET,
adaptado de (ENGBLOM, J, 2002).
2.2.1 Análise do Fluxo de Controle
Nesta primeira fase da análise estática é
determinado o comportamento dinâmico do
programa, ou seja, tem como propósito coletar
informações sobre os caminhos de execuções
possíveis. Para isso, são necessárias algumas
informações como o número de iterações de loops,
profundidade das recursões, dependências de dados
de entrada, caminhos inviáveis (infeasible paths),
etc. Essas informações podem ser fornecidas por
anotações manuais ou pela análise do fluxo
automática.
Esta etapa da análise pode ser dividida em
três partes. Iniciando pela extração de informações
do fluxo, a qual deriva informações sobre o
comportamento do programa, seguindo pela
representação do fluxo do programa, que armazena
as informações obtidas, e finalizando com a
preparação para o cálculo que busca como utilizar a
informação obtida nos passos anteriores para o
cálculo do WCET.
3
2.2.2 Análise de Baixo Nível
Esta fase da análise estática visa
determinar o tempo de execução para partes do
programa considerando os efeitos do hardware
alvo.
Esta fase da análise é baseada no modelo
abstrato do processador, o subsistema de memória,
os barramentos e os periféricos, que são
conservativos com respeito ao comportamento de
tempo do hardware concreto, significando que o
modelo nunca prediz um tempo de execução menor
do que aquele que pode ser observado no
processador real. Porém, a obtenção deste modelo
de processador abstrato, que simule o original
fielmente, é uma tarefa muitas vezes complexa
dependendo da classe do processador usado.
Processadores mais complexos são mais difíceis de
modelar e analisar devido às caches, pipelines e até
mesmo pela quantidade de bits da arquitetura.
A análise de baixo nível possui dois
assuntos principais que são a análise da cache e a
análise do pipeline.
2.2.3 Cálculo
O objetivo desta fase é encontrar um valor
que represente uma estimativa para o WCET.
Há algumas abordagens que são utilizadas
para a realização desta fase de cálculo. As três mais
comentadas na literatura, sendo assim as principais,
segundo (WILHELM, R. et al, 2008), são chamadas
de:
Baseadas em estrutura (structure-based);
Baseadas em caminhos (path-based);
Técnica de enumeração de caminhos
implícitos (IPET).
Maiores detalhes podem ser encontrados
em (RUFINO, L. M., 2009).
3 LLVM
O LLVM, Low Level Virtual Machine, é
uma infraestrutura de compilação, o qual, segundo
(LATTNER, C, 2006), provê componentes
modulares e reusáveis para construção de
compiladores, assim, reduzindo o tempo e custo
para construir um compilador particular. Esta
infraestrutura possui uma representação
intermediária (LLVM IR) bem definida para
programas, além de muitas bibliotecas
(componentes) com interfaces limpas e ferramentas
construídas pelas próprias bibliotecas. LLVM provê
componentes independentes de linguagem e
máquina alvo, permitindo que códigos de diferentes
linguagens possam ser ligados e otimizados juntos.
Conforme comentado em (LATTNER, C.;
ADVE, V, 2004a), o objetivo do framework LLVM
é permitir sofisticadas transformações em tempo de
compilação, ligação, instalação, execução e durante
o tempo inativo, operando na representação LLVM
de um programa em todos os estágios. Porém, para
ser posto em prática, o mesmo deve ser transparente
com relação ao desenvolvedor de aplicações e
usuários finais. Também, deve ser eficiente o
suficiente para ser usado com aplicações do mundo
real.
LLVM é baseado na representação SSA
que, conforme (LATTNER, C.; ADVE, V, 2009),
provê segurança de tipo, operações de baixo nível,
flexibilidade e a capacidade de representar todas as
linguagens de alto nível limpamente. A forma SSA
é uma representação intermediária na qual cada
variável é atribuída exatamente uma vez. Para
maiores detalhes conferir (WIKIPÉDIA. A
ENCICLOPÉDIA LIVRE, 2009d).
3.1 LLVM IR
Low Level Virtual Machine Intermediate
Representation representa um conjunto virtual de
instruções (LVIS) utilizado pelo LLVM. Essa
representação de código é um dos fatores chaves
que diferencia LLVM de outros sistemas. Segundo
(LATTNER, C.; ADVE, V, 2004a), a representação
é designada para prover informação de alto nível
sobre programas, o que é necessário para suportar
sofisticadas análises e transformações, enquanto
sendo de baixo nível o suficiente para representar
programas arbitrários e para permitir extensiva
otimização nos compiladores estáticos.
Esse conjunto de instruções captura as
operações de processadores comuns, mas evita
restrições específicas de máquina, tal como
registradores físicos, pipelines e convenções de
chamadas de baixo nível. Conforme (LATTNER,
C, 2006), (LATTNER, C.; ADVE, V, 2009),
(LATTNER, C.; ADVE, V, 2004a) e (LATTNER,
C.; ADVE, V, 2004b), a representação
intermediária possui algumas características
marcantes como:
Objetiva ser leve, de baixo nível e ao
mesmo tempo expressiva;
Deve ser independente de linguagem alvo;
Valores escalares são sempre
representados na forma SSA, nunca em
memória;
IR é inteiramente “tipada” e seus tipos são
rigorosamente checados para consistência;
Possui acessos à struct /array explícitos;
IR é facilmente extensível com funções
intrínsecas;
Provê um mecanismo para implementar
tratamento de exceções;
Hospeda uma larga variedade de
otimizações e análises;
As instruções são, na sua maioria,
formadas por três endereços de código
4
(three address code), um destino e dois
fontes, como em processadores RISC.
Por usufruir da forma SSA, possui um
conjunto de registradores virtual infinito
com informação de tipo.
Para finalizar, programas transferem
valores entre registradores e memória
unicamente via instruções load e store,
sendo que, até mesmo essas operações,
possuem ponteiros com referência a tipos.
4 LLFLOW
A ferramenta de análise de tempo de
execução de alto nível conhecida como llflow foi
desenvolvida como trabalho de conclusão de curso
por um aluno da Universidade Federal de Santa
Catarina (UFSC) e consiste na validação da
infraestrutura LLVM como plataforma para análise
do WCET. Neste capítulo serão descritas algumas
características e o funcionamento dessa ferramenta.
Descrições mais aprofundadas podem ser
encontradas em (MACHADO, A., 2008).
4.1 Características
Uma primeira característica da ferramenta
llflow é que, como dito, ela realiza apenas a análise
de alto nível, não realizando a análise de baixo
nível, a qual leva em conta o comportamento do
hardware alvo. Então, para tornar viável o trabalho,
é necessário considerar que cada instrução
executada pelo processador leva uma unidade de
tempo para ser finalizada e, consequentemente,
assume-se que o maior tempo de execução (WCET)
é obtido pelo caminho com o maior número de
instruções (MACHADO, A., 2008).
Outra peculiaridade dessa ferramenta,
talvez a mais importante de todas, é a utilização do
LLVM como sustentação para sua implementação.
Como comentado em (MACHADO, A., 2008), a
riqueza de informações da representação
intermediária LLVM e das análises já disponíveis
para esta plataforma fornecem ao usuário um
conjunto de informações valiosas sobre o código e,
também, parte dos algoritmos necessários para o
cálculo do tempo de execução são simplificados.
4.2 Funcionamento
Para a utilização da ferramenta llflow,
deve-se primeiramente entender como ela funciona
e sobre o que ela atua. Então, primeiramente, deve-
se ter o código fonte do programa a ser analisado
em alguma linguagem de programação a qual
possua um compilador que transforme o código
para o conjunto de instruções LLVM. Podem existir
vários arquivos fontes como também somente um.
Seguindo, é utilizado um front-end, o qual
compila o código fonte para a representação
intermediária LLVM. Um exemplo de front-end é o
llvm-gcc, o qual é uma versão do gcc que compila
programas C/ObjC em objetos nativos, bitcode
LLVM (binário), ou em linguagem assembly
LLVM (texto) (LLVM TEAM, 2009). Tanto
durante a compilação, com o próprio front-end,
como após, com a ferramenta de otimização (opt),
podem-se realizar algumas otimizações no código
gerado, o qual, posteriormente, deverá ser ligado,
com a ferramenta llvm-link, caso haja vários
módulos.
Após todas essas etapas, o código binário
na representação LLVM é inserido na ferramenta
llflow para a análise do código e geração de
informações relacionadas ao fluxo de controle.
Ainda em (MACHADO, A., 2008), é citada uma
ferramenta llvm-wcet, a qual necessitaria da
descrição da plataforma alvo para a realização da
análise de baixo nível. Essa ferramenta dependente
do hardware de destino seria a responsável não só
pelo cálculo de tempo, como pela geração do
código objeto final, com instruções nativas. Porém
essa ferramenta ainda não foi desenvolvida. Todo
esse processo é mostrado na figura 7.
Figura 47: Arquitetura da ferramenta llflow, obtido de
(MACHADO, A., 2008).
4.3 Entradas
Para que a ferramenta llflow faça a análise,
necessita-se que sejam fornecidos dois arquivos
essenciais, a saber:
Código binário na representação LLVM;
Arquivo de configuração com algumas
informações sobre as funções existentes no
código para análise.
Essas informações úteis presentes no
arquivo de configuração são (MACHADO, A.,
2008):
Faixas de valores que podem ser
retornados por chamadas de funções
externas (funções cujo código não está
disponível para análise);
5
Faixas de valores que podem ser
retornados em parâmetros passados por
referência às funções externas;
Funções do programa que devem ser
analisadas (pontos de entrada, caso apenas
uma parte do programa for tarefa de tempo
real ou quando o ponto de entrada não for
a função main), com informações sobre as
faixas de valores que podem ser aceitos
como parâmetros de entrada.
4.4 Saídas
Em posse do arquivo binário LLVM e do
arquivo de configuração, a ferramenta tem
condições de gerar os resultados esperados, os
quais são:
Grafo do fluxo de controle estendido, com
indicação dos escopos de análise;
Anotações em cada escopo, indicando o
número de execuções de cada loop ou
recursão;
5 Validando a Ferramenta LLFLOW
Este trabalho consiste de duas partes, a
saber:
Teste e correção da ferramenta de análise
do tempo de execução llflow;
Incremento de novas funcionalidades à
ferramenta.
Explicando melhor cada parte do trabalho,
na primeira será realizado o teste do aplicativo
llflow através de um benchmark próprio para
análise do WCET (MÄLARDALEN WCET
RESEARCH GROUP, 2006). Então, será pego um
programa desse benchmark, o qual será compilado
e inserido na ferramenta llflow para que os
resultados sejam exibidos. Caso erros venham a
ocorrer, alterações serão feitas no código fonte do
programa llflow para que o mesmo apresente o
resultado esperado. Essa parte será apresentada
neste capítulo.
Na segunda etapa, serão incrementadas
novas características ao aplicativo llflow, a fim de
que ele se torne uma ferramenta mais robusta e
confiável, aumentando a quantidade de programas
que poderão usufruí-lo para o cálculo do WCET.
Essa etapa será apresentada neste e nos próximos
capítulos.
Na próxima seção deste capítulo será
apresentado o código fonte retirado do benchmark
citado, o qual foi utilizado na ferramenta llflow
para que as etapas de verificação e incrementação
fossem executadas.
5.1 Fibonacci
Este programa, selecionado para
realização de teste, é o famoso fibonacci. Seu
código foi extraído do benchmark
(MÄLARDALEN WCET RESEARCH GROUP,
2006) e o mesmo calcula o valor do número de
fibonacci para a posição passada como parâmetro.
Para maiores informações com relação à sequência
de fibonacci, consultar (WIKIPÉDIA. A
ENCICLOPÉDIA LIVRE, 2009c). Este código
possui muitas chamadas recursivas, o que leva a um
grande processamento. A figura 48 apresenta o
código fonte desse programa, a figura 22 apresenta o
código em linguagem assembly LLVM, com
otimização máxima (-O3), gerado a partir da
compilação e, por fim, a figura 23 mostra o arquivo
de configuração que será passado como parâmetro
juntamente com o arquivo binário do programa
fibonacci na execução da ferramenta llflow.
Figura 48: Código fonte do programa que executa a
sequência de fibonacci, adaptado de (MÄLARDALEN
WCET RESEARCH GROUP, 2006).
6
Figura 49: Código na representação LVIS, com
otimização (fibonacci).
Figura 50: Arquivo de configuração (fibonacci).
Esse código, ao ser executado pelo
analisador, com o arquivo de configuração
apresentado, resultava em problemas. Então, para
viabilizar a análise foi necessário algumas
correções e implementações, como por exemplo:
A instrução switch que não havia sido
implementada;
A instrução phi não implementada,
também, teve que ser adicionada;
A instrução de chamada call apresentava
problemas;
Havia problemas ao utilizar a variável i
passada como parâmetro, onde em certos
momentos ela possuía um valor incorreto;
Quanto à utilização de um conjunto de
valores no arquivo de configuração ([0,
10]), surgiram vários problemas,
principalmente relativo à divisão de
contexto, que são ocasionadas por
instruções de comparação.
Com todos os problemas solucionados,
pode-se executar o código com a ferramenta de
análise de tempo de execução e obteve-se o
resultado apresentado na figura 24.
Figura 51: Resultado da análise, com otimização
(fibonacci).
O maior caminho percorrido, com o
parâmetro da função assumindo o valor
especificado, é:
7
Comprimento do caminho: 594 instruções.
START, entry, bb3.i6, entry, bb3.i6, entry,
bb3.i6, entry, bb3.i6, entry, fib.exit8,
fib.exit, fib.exit.i, fib.exit8, bb3.i, entry,
bb4, entry, bb4, fib.exit, bb3.i.i, entry,
fib.exit8, fib.exit, entry, bb4, fib.exit.i,
fib.exit8, bb3.i, entry, bb3.i6, entry, bb4,
fib.exit.i, fib.exit8, fib.exit, entry, fib.exit8,
fib.exit, fib.exit, bb3.i.i, entry, bb3.i6,
entry, fib.exit8, fib.exit, fib.exit.i, fib.exit8,
bb3.i, entry, bb4, entry, bb4, fib.exit,
entry, bb3.i6, entry, bb4, fib.exit.i,
fib.exit8, fib.exit, fib.exit.i, fib.exit8, bb3.i,
entry, bb3.i6, entry, bb3.i6, entry, bb4,
fib.exit.i, fib.exit8, fib.exit, bb3.i.i, entry,
bb4, entry, bb4, fib.exit.i, fib.exit8, bb3.i,
entry, fib.exit8, fib.exit, entry, bb4,
fib.exit, entry, bb3.i6, entry, fib.exit8,
fib.exit, fib.exit.i, fib.exit8, bb3.i, entry,
bb4, entry, bb4, fib.exit, fib.exit, bb3.i.i,
entry, bb3.i6, entry, bb3.i6, entry,
fib.exit8, fib.exit, fib.exit.i, fib.exit8, bb3.i,
entry, bb4, entry, bb4, fib.exit, bb3.i.i,
entry, fib.exit8, fib.exit, entry, bb4,
fib.exit.i, fib.exit8, bb3.i, entry, bb3.i6,
entry, bb4, fib.exit.i, fib.exit8, fib.exit,
entry, fib.exit8, fib.exit, fib.exit, entry,
bb3.i6, entry, bb3.i6, entry, bb4, fib.exit.i,
fib.exit8, fib.exit, bb3.i.i, entry, bb4, entry,
bb4, fib.exit.i, fib.exit8, bb3.i, entry,
fib.exit8, fib.exit, entry, bb4, fib.exit,
fib.exit.i, fib.exit8, bb3.i, entry, bb3.i6,
entry, bb3.i6, entry, bb3.i6, entry, bb4,
fib.exit.i, fib.exit8, fib.exit, bb3.i.i, entry,
bb4, entry, bb4, fib.exit.i, fib.exit8, bb3.i,
entry, fib.exit8, fib.exit, entry, bb4,
fib.exit, bb3.i.i, entry, bb3.i6, entry, bb4,
fib.exit.i, fib.exit8, fib.exit, entry, fib.exit8,
fib.exit, fib.exit.i, fib.exit8, bb3.i, entry,
bb3.i6, entry, fib.exit8, fib.exit, fib.exit.i,
fib.exit8, bb3.i, entry, bb4, entry, bb4,
fib.exit, entry, bb3.i6, entry, bb4, fib.exit.i,
fib.exit8, fib.exit, fib.exit, entry, bb3.i6,
entry, bb3.i6, entry, fib.exit8, fib.exit,
fib.exit.i, fib.exit8, bb3.i, entry, bb4, entry,
bb4, fib.exit, bb3.i.i, entry, fib.exit8,
fib.exit, entry, bb4, fib.exit.i, fib.exit8,
bb3.i, entry, bb3.i6, entry, bb4, fib.exit.i,
fib.exit8, fib.exit, entry, fib.exit8, fib.exit,
fib.exit, fib.exit
De acordo com o arquivo de configuração,
o valor passado como parâmetro para a função fib a
ser analisada representa um conjunto, sendo que o
valor mínimo é 0 (zero) e o máximo 10 (dez). Para
a função fibonacci, nota-se claramente, que quanto
maior o valor do parâmetro, maior será o caminho
percorrido pela função para que o resultado seja
obtido. Logo, a resposta final cedida pela
ferramenta llflow deverá ser o mesmo valor obtido
para o parâmetro de número 10 (dez), pois já
conhecemos a priori que este caso representa o
maior caminho possível. É importante salientar que
a ferramenta llflow realiza o cálculo do pior
caminho utilizando a definição de conjuntos, então,
realizar-se-á o cálculo para todos os valores entre 0
(zero) e 10 (dez).
A seguir, será apresentado o cálculo do
número de chamadas recursiva do código presente
na figura 22 de acordo com as informações já
mencionadas acima. A contagem dá-se pelo número
de vezes que o bloco básico entry é chamado:
i = 0 => 1;
i = 1 => 1;
i = 2 => 1;
i = 3 => 2;
i = 4 => 4;
i = 5 => 7;
i = 6 => 10;
i = 7 => 17;
i = 8 => 28;
i = 9 => 46;
i = 10 => 73;
Como informado pela análise apresentada,
quando o valor da variável i assume 10 (dez), o
bloco básico de nome entry é chamado 73 vezes ao
total. Porém, para a ferramenta llflow, como a
função já se inicia na própria função entry, a
primeira vez que ela é executada não é adicionada
ao valor total. Logo, para todos os valores obtidos
no cálculo acima, o analisador assumirá que o valor
da variável count será sempre uma unidade menor
em relação ao demonstrado. Então, prova-se que a
ferramenta apresentou o valor count esperado para
a função fibonacci com os valores de entrada sendo
um conjunto.
Para a realização da análise do número de
instruções executadas, é utilizado o interpretador
fornecido pelo LLVM, chamado lli, o qual executa
programas no formato binário LLVM. Então, para
executar os códigos apresentados nas
representações LVIS referentes à busca binária,
deve-se adicionar a função main que representa o
ponto de entrada para o código. Logo, foram
adicionadas ao código, as seguintes linhas:
define i32 @main() nounwind readonly {
entry:
call i32 @fib(i32 10) nounwind
ret i32 1
}
Assim, executando o interpretador LLVM
para todos os valores possíveis como parâmetro, de
0 (zero) a 10 (dez), obtêm-se para o número de
instruções executadas:
i = 0 => 4
i = 1 => 4
i = 2 => 6
i = 3 => 13
8
i = 4 => 25
i = 5 => 44
i = 6 => 67
i = 7 => 114
i = 8 => 187
i = 9 => 307
i = 10 => 492
Então, o maior valor, representado pelo
parâmetro 10 (dez), é 492. Subtraindo as duas
instruções da função main, é obtido o valor 490.
Mas a ferramenta llflow retornou o número 594
como resultado, porém, nesta contagem estão
incluídas as instruções phi, as quais não estão
presentes no cálculo realizado pelo interpretador.
Realizando o cálculo da quantidade de
instruções phi para o caminho apresentado,
encontra-se 104 instruções, as quais devem ser
diminuídas do total de 594, resultando em 490,
exatamente o valor encontrado pelo interpretador,
provando assim, a eficiência da ferramenta llflow.
6 Incrementando a Ferramenta LLFLOW - Parte 1
Além das adições e modificações já
comentadas, incrementou-se ainda uma nova
funcionalidade que merece destaque e, por isso,
será apresentada neste capítulo. Essa característica
foi desenvolvida devido ao problema que a
ferramenta llflow possuía em lidar com análise de
loops aninhados.
No trabalho de (MACHADO, A., 2008),
cita-se este problema como uma limitação do
protótipo e sugere dois possíveis contornos para tal,
que seriam:
Ao invés de anotar os escopos com
números de iterações, anotar cada bloco
básico, e enumerar todos os caminhos no
programa, mostrando como resultado o
maior caminho, e não o grafo de fluxo de
controle;
Realizar anotações simbólicas, ou seja,
deixar o número de iterações do loop
interno como uma função da iteração do
loop externo. Esta abordagem tornaria
mais complexa a análise final para
identificação do caminho no grafo que
representa o pior caso para o cálculo de
tempo.
Porém, neste trabalho, realizou-se outra
solução que se assemelha com a segunda opção. O
cálculo do número de iterações ainda continua a
cargo dos escopos, porém a contagem para os loops
internos são realizadas para cada iteração do loop
externo separadamente, resultando em um valor
final para cada iteração.
A seguir, serão demonstrados os efeitos
dessa nova solução para alguns exemplos retirados
do benchmark (MÄLARDALEN WCET
RESEARCH GROUP, 2006), seguindo a mesma
metodologia apresentada no capítulo anterior.
6.1 Insertion Sort
Este programa, selecionado para análise de
loops aninhados, realiza uma operação em arrays
chamada insertion sort, o qual, segundo
(WIKIPÉDIA. A ENCICLOPÉDIA LIVRE,
2009b), é um simples algoritmo de ordenação,
eficiente quando aplicado a um pequeno número de
elementos. Em termos gerais, ele percorre um vetor
de elementos da esquerda para a direita e à medida
que avança vai deixando os elementos mais à
esquerda ordenados. Seu código foi extraído do
benchmark (MÄLARDALEN WCET RESEARCH
GROUP, 2006). A figura 30 apresenta o código
fonte do programa comentado, a figura 31
apresenta o código em linguagem assembly LLVM,
com otimização máxima, gerado a partir da
compilação e, por fim, a figura 32 apresenta o
arquivo de configuração para a ferramenta llflow.
Figura 52: Código fonte do programa insertion sort,
adaptado de (MÄLARDALEN WCET RESEARCH
GROUP, 2006).
9
Figura 53: Código na representação LVIS, com
otimização (insertion sort).
Figura 54: Arquivo de configuração (insertion sort).
Esse código, ao ser executado pelo
analisador com o arquivo de configuração
apresentado, gerou o resultado da figura 33.
Conforme demonstrado no resultado, nota-
se que para ordenar um array com 11 (onze)
números inteiros, totalmente invertido com relação
ao objetivo final, são necessárias 9 (nove) iterações
do loop externo e para cada loop interno o número
de iterações é incrementado em 1 (um), começando
com uma única iteração.
A versão original da ferramenta llflow
conduzia a erros na análise, pois retornava como
resultado um único valor para a variável count do
loop interno, bloco básico bb1, a saber: 45
(quarenta e cinco). Com as modificações feitas na
ferramenta, os valores referem-se a cada iteração do
loop externo individualmente, sendo mais claro o
cálculo da análise.
Figura 55: Resultado da análise, com otimização
(insertion sort).
7 Incrementando a Ferramenta LLFLOW - Parte 2
Neste capítulo será apresentada uma nova
funcionalidade que talvez seja a mais importante
realizada neste trabalho. Ela refere-se à
determinação dos blocos básicos inalcançáveis, ou
também chamados de mortos, significando que
nunca serão atingidos por qualquer caminho que a
10
função possa seguir para os valores passados como
parâmetros.
Isto é importante, pois se pode eliminar
este pedaço de código sem causar mudanças no
resultado final obtido, conseguindo assim, uma
redução no tamanho do código, item importante
quando há limitações de memória.
Outro fator que cabe ressaltar é que, com
relação ao cálculo do WCET, este pedaço de código
considerado inalcançável pode ser eliminado da
contagem do resultado final, resultando assim em
um valor mais próximo do real, tornando um
cálculo mais preciso. Essa característica será
demonstrada através do código exemplo
apresentado a seguir.
7.1 Cover
Este código, retirado de (MÄLARDALEN
WCET RESEARCH GROUP, 2006), tem como
objetivo testar vários caminhos, sendo que existe
um loop for com várias instruções switch case. A
figura 56 apresenta o código fonte, a figura 57 o
código em linguagem assembly LLVM e,
finalmente, a figura 58 apresenta o arquivo de
configuração utilizado na análise.
Figura 56: Código fonte do programa cover, adaptado de
(MÄLARDALEN WCET RESEARCH GROUP, 2006).
11
Figura 57: Código na representação LVIS, sem
otimização (cover).
Figura 58: Arquivo de configuração (cover).
De acordo com o código fonte e os valores
passados como parâmetro, percebe-se que a
instrução for irá iterar por no máximo 3 (três)
vezes, executando assim as instruções presentes nas
três primeiras cláusulas case do switch. Desse
modo, as instruções case restantes nunca serão
executados, sendo assim, são consideradas blocos
básicos mortos.
Para este código, obteve-se como resultado
o grafo apresentado na figura 59 e, também, as
seguintes informações:
Comprimento do caminho: 64 instruções.
START, entry, bb13, bb, bb1, bb12, bb13,
bb, bb2, bb12, bb13, bb, bb3, bb12, bb13,
bb14, return
BBs inviáveis: bb4, bb5, bb6, bb7, bb8,
bb9, bb10, bb11
Figura 59: Resultado da análise, sem otimização (cover).
8 Conclusão
Os sistemas embarcados, mais
especificamente os de tempo real crítico,
12
representam uma parcela significativa dos
aplicativos atuais e, devido à sua natureza,
merecem uma atenção especial quanto a sua
validação. A ferramenta llflow foi desenvolvida
com a finalidade de, para esses sistemas, obter seu
fluxo de controle para que posteriormente, na
realização da análise de baixo nível, possa-se
garantir o tempo de execução do pior caso
(WCET), tornando o sistema confiável e útil.
Essa ferramenta, llflow, utiliza-se do
LLVM, uma infraestrutura de compilação muito
ativa no momento, o qual provê uma série de
facilidades e benefícios, tornando seu uso uma
decisão extremamente oportuna. O analisador
proposto e implementado por (MACHADO, A.,
2008) possui uma estrutura excelente em relação ao
seu código, seu uso é extremamente simples e seus
resultados fáceis de entender. Porém, não possuía a
robustez necessária para a utilização em diversos
programas, limitando-se a somente alguns mais
simples.
Com as modificações feitas nesse trabalho
na ferramenta llflow, sua implementação aderiu
novas funcionalidades e apresentou uma melhoria
na questão da correção relativa às características já
existentes. Isso a tornou mais robusta e completa,
deixando-a menos susceptível a erros.
9 Referências
CANTU, E. M. (2008). Geração de Código
para a Máquina Virtual LLVM a partir de
Programas Escritos na Linguagem de
Programação JAVA (Tradutor Java – LLVM).
Trabalho de conclusão do curso de Sistemas de
Informação, Universidade Federal de Santa
Catarina, Florianópolis, Santa Catarina, Brasil.
ENGBLOM, J. (2002). Worst-case Execution
Time Analysis.
ENGBLOM, J. ERMEDAHLT, A. (2000).
Modeling Complex Flows for Worst-case
Execution Time Analysis.
FAUSTER, J.; KIRNER, R.; PUSCHNER, P.
(2003). Intelligent Editor for Writing Worst-
Case-Execution-Time-Oriented Programs.
FERDINAND, C.; HECKMANN, R. (2008).
Worst-Case Execution Time - A Tool
Provider’s Perspective.
GÓES, J. A. (2000). Estimação de Tempo de
Execução de Programas a partir de Arquivos-
Objeto. Trabalho de conclusão no programa de
pós-graduação do curso de Engenharia Elétrica
e Informática Industrial, Centro Federal de
Educação Tecnológica do Paraná, Paraná,
Curitiba, Brasil.
GUSTAFSSON, J.; ERMEDAHL, A.; LISPER,
B. (2006). Algorithms for Infeasible Path
Calculation.
LATTNER, C. (2006). Introduction to the
LLVM Compiler Infrastructure.
LATTNER, C. (2002a). LLVM: An
Infrastructure for Multi-Stage Optimization.
LATTNER, C. (2002b). Macroscopic Data
Structure Analysis and Optimization.
LATTNER, C.; ADVE, V. (2009). LLVM
Language Reference Manual. Acesso em 28 de
Fevereiro de 2009, disponível em
http://www.llvm.org/docs/LangRef.html
LATTNER, C.; ADVE, V. (2004a). LLVM: A
Compilation Framework for Lifelong Program
Analysis & Transformation.
LATTNER, C.; ADVE, V. (2004b). The LLVM
Compiler Framework and Infrastructure.
LLVM TEAM. (2009). LLVM C Front-End.
Acesso em 4 de Agosto de 2009, disponível em
http://llvm.org/cmds/llvmgcc.html
LLVM TEAM. (2002). The LLVM Compiler
Infrastructure Project. Acesso em 28 de
Fevereiro de 2009, disponível em
http://www.llvm.org
MACHADO, A. (2008). Análise de Tempo de
Execução em Alto Nível para Sistemas de
Tempo Real Utilizando-se o Framework LLVM.
Trabalho de conclusão do curso de Sistemas de
Informação, Universidade Federal de Santa
Catarina, Florianópolis, Santa Catarina, Brasil.
MÄLARDALEN WCET RESEARCH GROUP.
(2006). WCET Project. Acesso em 29 de Abril
de 2009, disponível em
http://www.mrtc.mdh.se/projects/wcet/benchma
rks.html
OLIVEIRA, B. C.; SANTOS, M. M.;
DESCHAMPS, F. (2006). Cálculo do Tempo de
Execução de Códigos no Pior Caso (WCET) em
Aplicações de Tempo Real: Um Estudo de
Caso.
RUFINO, L. M. (2009). Análise de Tempo de
Execução Utilizando LLVM. Trabalho de
conclusão do curso de Ciências da Computação,
Universidade Federal de Santa Catarina,
Florianópolis, Santa Catarina, Brasil.
WIKIPÉDIA. A ENCICLOPÉDIA LIVRE.
(2009a). Embedded System. Acesso em 28 de
Fevereiro de 2009, disponível em
http://en.wikipedia.org/wiki/Embedded_system
WIKIPÉDIA. A ENCICLOPÉDIA LIVRE.
(2009b). Insertion Sort. Acesso em 16 de
13
Agosto de 2009, disponível em
http://pt.wikipedia.org/wiki/Insertion_sort
WIKIPÉDIA. A ENCICLOPÉDIA LIVRE.
(2009c). Número de Fibonacci. Acesso em 7 de
Agosto de 2009, disponível em
http://pt.wikipedia.org/wiki/Número_de_Fibona
cci
WIKIPÉDIA. A ENCICLOPÉDIA LIVRE.
(2009d). Static Single Assignment Form. Acesso
em 4 de Abril de 2009, disponível em
http://en.wikipedia.org/wiki/Static_single_assig
nment_form
WILHELM, R. et al. (2008). The Worst-Case
Execution Time Problem - Overview of
Methods and Survey of Tools.