• Nenhum resultado encontrado

Análise de Tempo de Execução Utilizando LLVM

N/A
N/A
Protected

Academic year: 2021

Share "Análise de Tempo de Execução Utilizando LLVM"

Copied!
104
0
0

Texto

(1)

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

(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

(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,

(5)

ABSTRACT

The embedded systems dominate the commercial market today. Many of these systems can also be classified as 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

(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

(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

(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-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).

(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.

(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.

(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;

(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:

(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.

(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)
(43)
(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:

(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.

(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:

(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:

(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)

Referências

Documentos relacionados

O primeiro conjunto de artigos, uma reflexão sobre atores, doenças e instituições, particularmente no âmbito da hanse- níase, do seu espaço, do seu enquadramento ou confinamen- to

Este dado diz respeito ao número total de contentores do sistema de resíduos urbanos indiferenciados, não sendo considerados os contentores de recolha

Na experiência em análise, os professores não tiveram formação para tal mudança e foram experimentando e construindo, a seu modo, uma escola de tempo

O DIRETOR DO LABORATÓRIO NACIONAL DE COMPUTAÇÃO CIENTÍFICA DO MINISTÉRIO DA CIÊNCIA, TECNOLOGIA, INOVAÇÕES E COMUNICAÇÕES, no uso da competência que lhe foi

[r]

Outro aspecto a ser observado é que, apesar da maioria das enfermeiras referirem ter aprendido e executado as fases do processo na graduação, as dificuldades na prática

I: hum hum.. 125 M: A forma como intarigir com eles… Saber estar com eles, e essa foi uma das maiores lições. Sabermos nós, saber lidar com eles. Ah, conhece-los mais. No âmbito em

Como objetivos específicos pretendeu-se iden- tificar os taxa existentes nesta gruta, determinar a riqueza de es- pécies de sua comunidade; verificar a influência de fatores