• Nenhum resultado encontrado

Reutiliza¸c˜ ao de componentes: inclus˜ ao impl´ıcita vs expl´ıcita

4.3 Dolphin Internal Representation

5.1.1 Reutiliza¸c˜ ao de componentes: inclus˜ ao impl´ıcita vs expl´ıcita

Implementar integralmente uma solu¸c˜ao sob a forma de um ´unico componente, faz com que este contenha tudo o que ´e necess´ario `a sua execu¸c˜ao, ´e por assim dizer aut´onomo dos restantes componentes e a sua execu¸c˜ao apenas obedece a uma determinada sequˆencia imposta pela estrutura do pr´oprio compilador. Por exemplo, um componente que implemente integralmente um front-end pode ser executado de forma independente dos demais. A ´unica restri¸c˜ao `a qual deve obedecer, prende-se com a pr´opria estrutura do compilador, que for¸ca a execu¸c˜ao deste componente antes de todos os outros. Diz-se assim existir uma dependˆencia estrutural, entre o front-end e os demais componentes.

A decomposi¸c˜ao de uma solu¸c˜ao em v´arios componentes (um principal e um ou mais de suporte), cria dependˆencias funcionais entre esses componentes, isto ´e, a execu¸c˜ao de um componente principal passa a requerer a execu¸c˜ao de todos os componentes que o suportam. O que poder´a ser feito antes, durante ou ap´os a execu¸c˜ao das tarefas inerentes ao componente principal.

Este tipo de dependˆencia, que advˆem do facto da solu¸c˜ao se encontrar decomposta em v´arios componentes, por si s´o n˜ao cria grandes problemas. Basta que o componente principal inclua os componentes de suporte e tudo funcionar´a como antes. O componente de suporte ´e assim utilizado implicitamente, dado que ´e inserido no compilador por outro componente e n˜ao por parte de quem especifica a estrutura do compilador.

A principal vantagem da inclus˜ao impl´ıcita adv´em do facto de ocultar do utilizador os componentes de suporte, fazendo com que a especifica¸c˜ao de um compilador seja mais simples e como tal mais acess´ıvel de descrever. O Exemplo5.1 ilustra como ´e que a inclus˜ao impl´ıcita dos componentes de suporte simplifica a especifica¸c˜ao dos compiladores.

Exemplo 5.1

AStatic Single Assignment (SSA) é uma forma de RIC que facilita a implementação de muitas rotinas, nomeadamente de análise e optimização de código. O Dolphin fornece um componente, o cnv2SSA, que permite converter a RIC da forma normal para a forma SSA. Componente esse que reutiliza outros componentes, como se encontra ilustrado na Figura5.1. Se considerarmos que esses componentes de suporte são incluídos implicitamente pelo cnv2SSA, o utilizador não necessita sequer de saber da sua existência e muito menos tem que lidar com eles, basta instanciar e executar o cnv2SSA como está exemplicado na Figura 5.2.

Ÿ

Figura 5.1: cnv2SSA e respectivos componentes de suporte.

(1) DIR d(argc,argv); // Instanciação do objecto que contém a RIC

(2) littleC fe(&d); // Instanciação do front-end

(3) fe.execute(); // Execução do front-end

(4) // para construção da RIC

(5) ...

(6) cnv2SSA cnv(&d); // Instanciação de cnv2SSA

(7) ...

(8) cnv.execute(); // Execução de cnv2SSA para converter a RIC

(9) // da forma normal para a forma SSA

(10) ...

Figura 5.2: Especifica¸c˜ao contendo as opera¸c˜oes necess´arias `a convers˜ao da forma normal para a forma SSA.

Apesar de todas as vantagens da inclus˜ao impl´ıcita de componentes, a verdade ´e que a reutiliza¸c˜ao do processo em si continua a n˜ao existir, isto ´e, reutiliza-se o c´odigo mas n˜ao o processo inerente a esse c´odigo. Isto leva a que muitas opera¸c˜oes sejam executadas in´umeras vezes sem que da´ı advenha qualquer vantagem, antes pelo contr´ario, contribui de forma muito significativa para deteriorar o tempo de compila¸c˜ao, mas n˜ao s´o.

O exemplo da Figura5.1 serve tamb´em para ilustrar esta situa¸c˜ao. ´E poss´ıvel consta- tar que determinados componentes servem de suporte a mais do que um componente. Por exemplo, DFrontiers suporta directamente cnv2SSA, mas tamb´em IDFrontiers. Incluir impli- citamente DFrontiers, faz com que existam pelo menos duas instˆancias deste componente no compilador: uma que suporta cnvSSA e outra que suporta IDFrontiers. O mesmo acontece com os componentes IDominator e Dominators. A Figura5.3representa todas as instˆancias que s˜ao necess´arias para a utiliza¸c˜ao de cnv2SSA, caso os componentes sejam inclu´ıdos de forma impl´ıcita.

As consequˆencias s˜ao claramente gravosas para o processo de compila¸c˜ao. Isto porque cada instˆancia ´e independente, e como tal tem que ser executada individualmente, con-

5.1. Afinal quais s˜ao os problemas? 75

Figura 5.3: Instˆancias requeridas por cnv2SSA, se os componentes forem inclu´ıdos implici- tamente.

sumindo espa¸co de mem´oria e tempo de processamento. O que se traduz em tempos de compila¸c˜ao mais longos (compiladores mais lentos do que eventualmente seria necess´ario).

O processo de compila¸c˜ao deteriora-se ainda mais quando os componentes de suporte, visam disponibilizar informa¸c˜ao sobre a RIC. Isto porque, a informa¸c˜ao ´e normalmente dis- ponibilizada atrav´es dos pr´oprios componentes, ou seja, ´e o pr´oprio componente que mant´em a informa¸c˜ao que previamente determinou. Em termos do processo de compila¸c˜ao, a inclus˜ao impl´ıcita dos componentes vai fazer com que uma quantidade substancial de informa¸c˜ao seja desnecessariamente replicada. O Exemplo 5.1 serve para ilustrar esta situa¸c˜ao. Como foi j´a dito, cnv2SSA converte a RIC da forma normal para a forma SSA, mas n˜ao h´a qualquer tipo de informa¸c˜ao que fique retida no componente, ou seja, na pr´atica o componente recebe a RIC (na forma normal), processa-a e coloca-a novamente na sa´ıda (agora na forma SSA). O mesmo n˜ao acontece com os componentes de suporte. Cada um destes componentes, com base na RIC, apura um determinado tipo de informa¸c˜ao. ´E essa informa¸c˜ao que na realidade ´e utilizada pelo componente principal (cnv2SSA). Por exemplo, Dominators constr´oi inter- namente um dicion´ario, que cont´em para cada nodo do Grafo de Fluxo de Controlo (GFC), os nodos que o dominam [LT79,CFR+91].

`

A dependˆencia criada pelo facto de a execu¸c˜ao de um componente requerer informa¸c˜ao previamente determinada por outro componente (componente de suporte), designou-se por dependˆencia de dados, a qual ´e um caso especial da dependˆencia funcional entre componentes, mas que tem outros requisitos.

Uma possibilidade para tornar a reutiliza¸c˜ao dos componentes mais eficiente seria utiliz´a-los de forma expl´ıcita, isto ´e, aquando da constru¸c˜ao do compilador, todos os com- ponentes seriam explicitamente instanciados e executados pelo programador. Caberia a este gerir as instˆancias dos v´arios componentes de forma a evitar a sua replica¸c˜ao, tornando assim o processo de compila¸c˜ao mais eficiente.

Para quem especifica compiladores, a inclus˜ao expl´ıcita ´e o procedimento natural de utiliza¸c˜ao dos componentes. No entanto apenas tem sido aplicada a componentes:

• Cuja execu¸c˜ao ´e recomendada mas n˜ao fundamental1;

• Cuja execu¸c˜ao ´e fundamental, resultando numa dependˆencia funcional, mas em que cabe a terceiros requerer a execu¸c˜ao desses componentes (normalmente a quem especi- fica o compilador)2.

Associa¸c˜ao de componentes

Os mecanismos existentes na framework original n˜ao permitem lidar com a maioria dos casos em que h´a dependˆencias funcionais, mas em especial quando h´a dependˆencias de dados. Isto porque faz falta um mecanismo que vincule o componente de suporte ao componente principal. No entanto n˜ao ´e dif´ıcil obter uma solu¸c˜ao, por exemplo, basta registar o componente de suporte no componente principal, `a semelhan¸ca do que ´e feito para o registo da RIC nos componentes. A Figura 5.4 ilustra a aplica¸c˜ao desta solu¸c˜ao, apresentando as opera¸c˜oes necess´arias `a convers˜ao da forma normal para a forma SSA, com recurso `a inclus˜ao expl´ıcita dos componentes.

´

E f´acil detectar pelos exemplos das Figura5.2e Figura5.4, que para a mesma opera¸c˜ao (convers˜ao da forma normal para a forma SSA), a especifica¸c˜ao de um compilador com base na inclus˜ao expl´ıcita de componentes ´e substancialmente mais longa e complexa do que a especifica¸c˜ao com base na inclus˜ao impl´ıcita. Mais grave ainda, ´e que a inclus˜ao expl´ıcita de componentes requer que o utilizador conhe¸ca minimamente a forma como os componentes est˜ao implementados, nomeadamente: quais s˜ao os componentes de suporte, qual a ordem pela qual devem ser aplicados, como devem ser utilizados, quais os efeitos da sua execu¸c˜ao, etc. Com a agravante de que esta desvantagem ´e recursiva, isto ´e, aplica-se aos pr´oprios componentes de suporte (agora no papel de componentes principais). O pior ´e que h´a outros factores que agravam ainda mais esta situa¸c˜ao, como se explica adiante.

Conhecer a estrutura da RIC

Como j´a foi referido, aDolphin Internal Representation (DIR) ´e um modelo de repre- senta¸c˜ao de c´odigo composto por objectos com diferentes n´ıveis de abstrac¸c˜ao. Nos n´ıveis de maior abstrac¸c˜ao temos objectos do tipo Program, Function ou mesmo de CFG. Enquanto nos n´ıveis de menor abstrac¸c˜ao temos objectos do tipo DT ou de Expression. A existˆen- cia de diversos n´ıveis de abstrac¸c˜ao, faculta a possibilidade de se escolher o n´ıvel (conjunto

1

Por exemplo, a utiliza¸c˜ao de componentes cuja execu¸c˜ao contribui, directa ou indirectamente, para criar melhores condi¸c˜oes e oportunidades para os componentes que s˜ao executados posteriormente.

2

Esta situa¸c˜ao acontece, por exemplo, com o componente que faz a convers˜ao da forma normal para a forma SSA (cnv2SSA). A forma SSA facilita a implementa¸c˜ao de muitas rotinas de an´alise e optimiza¸c˜ao de c´odigo, permitindo em muitos casos obter solu¸c˜oes mais eficientes. N˜ao significa no entanto que tais rotinas n˜ao possam ser implementadas sobre a forma normal. Existem mesmo alguns componentes que seleccionam a solu¸c˜ao a utilizar, mediante a forma em que se encontra a RIC. Mas outros componentes h´a, que apenas est˜ao aptos a executar sob uma das formas. No entanto como o processo de convers˜ao para SSA, e posterior reconvers˜ao para a forma normal, ´e bastante pesado, a convers˜ao s´o dever´a ser feita nas seguintes condi¸c˜oes: quando se pretende incluir no compilador um n´umero consider´avel de componentes a funcionarem sobre a forma SSA (e que sejam executados sequencialmente); ou utilizar componentes que sejam fundamentais e que funcionem exclusivamente sobre a forma SSA. Estes motivos s˜ao no entanto de ordem conceptual, isto ´e, dependem da concep¸c˜ao do compilador, nomeadamente do tipo de componentes que se pretende utilizar. Por isso, e apesar da convers˜ao para a forma SSA ser fundamental para alguns componentes, normalmente s´o ´e executada se for explicitamente requerida por quem especifica o compilador. Da´ı que muitos componentes da framework Dolphin, que funcionam exclusivamente sobre a forma SSA, estejam implementados de maneira a n˜ao for¸carem a convers˜ao, funcionando apenas se esta tiver sido previamente executada. Caso contr´ario, o pedido de execu¸c˜ao do componente falha.

5.1. Afinal quais s˜ao os problemas? 77

(1) DIR d(argc,argv); // Instanciação do objecto que contém a RIC

(1) littleC fe(&d); // Instanciação do front-end

(2) fe.execute(); // Execução do front-end

(3) // para construção da RIC

(4) ...

(5) var2Indx vi(&d); // Instanciação de var2Indx

(6) indx2Var iv(&d); // Instanciação de indx2Var

(7) Dominators dom(&d); // Instanciação de Dominators

(8) IDominator idom(&d);// Instanciação de IDominator

(9) IDominated ided(&d);// Instanciação de IDominated

(10) DFrontiers df(&d); // Instanciação de DFrontiers

(11) IDFrontiers idf(&d);// Instanciação de IDFrontiers

(12) cnv2SSA cnv(&d); // Finalmente, instanciação de cnv2SSA

(13) ...

(14) vi.execute(); // Execução da instância de var2Indx

(15) iv.execute(); // Execução da instância de indx2Var

(16) dom.execute(); // Execução da instância de Dominators

(17) idom.execute(); // Execução da instância de IDominator

(18) ided.execute(); // Execução da instância de IDominated

(19) df.execute(); // Execução da instância de DFrontiers

(20) idf.execute(); // Execução da instância de IDFrontiers

(21) ...

(22) cnv.execute(); // Execução da instância de cnv2SSA

(23) ...

Figura 5.4: Especifica¸c˜ao parcial de um compilador com inclus˜ao expl´ıcita de componentes.

de elementos da RIC) que melhor se adequa `a implementa¸c˜ao e execu¸c˜ao de cada um dos componentes.

Nos exemplos at´e aqui apresentados, como ´e o caso da especifica¸c˜ao da Figura 5.4, a utiliza¸c˜ao dos componentes faz-se exclusivamente sobre um objecto do tipo DIR (que caracte- riza toda a RIC). Tal tem sido feito para simplificar os exemplos e a explica¸c˜ao dos mesmos. Na realidade, a utiliza¸c˜ao de elementos do tipo DIR, ou mesmo do tipo Program, visa: a execu¸c˜ao de tarefas de alto n´ıvel (an´alises ou optimiza¸c˜oes de c´odigo inter-procedimentais); ou aplicar o componente de forma generalizada sobre todos os elementos de um n´ıvel de abs- trac¸c˜ao menor. Mas a maior parte dos componentes requer a utiliza¸c˜ao de elementos mais espec´ıficos, isto ´e, de menor abstrac¸c˜ao.

Com a inclus˜ao expl´ıcita, o utilizador ´e obrigado a “navegar” atrav´es dos diversos n´ıveis de abstrac¸c˜ao para alcan¸car os elementos necess´arios `a execu¸c˜ao dos componentes. O mesmo acontece com inclus˜ao impl´ıcita, mas a responsabilidade de “navegar” na RIC cabe neste caso a quem desenvolve o componente. Em ambos os casos ´e necess´ario conhecer o tipo de elementos que comp˜oem a DIR e a pr´opria estrutura da RIC. No entanto, subentende-se que tais conhecimentos ser˜ao mais acess´ıveis a quem desenvolve componentes, do que a quem vai apenas utiliz´a-los.

Dimens˜ao do problema

O problema torna-se particularmente complicado, quando se tem em conta que um programa submetido ao compilador normalmente cont´em: um ´unico objecto do tipo DIR; algumas dezenas de objectos de alto n´ıvel, do tipo Function e CFG; e v´arias centenas, sen˜ao milhares, de objectos de baixo n´ıvel do tipo DT e Expression. Significa isto, que um com- ponente que utilize elementos de baixo n´ıvel de abstrac¸c˜ao poder´a ter v´arias centenas sen˜ao milhares de instˆancias. Gerir essas instˆancias pode ser bastante complicado, nomeadamente quando os componentes s˜ao inclu´ıdos explicitamente. ´E que se com a inclus˜ao impl´ıcita, o problema fica restrito ao contexto de implementa¸c˜ao de cada componente (apenas h´a que lidar com as respectivas instˆancias dos componentes de suporte); j´a com a inclus˜ao expl´ıcita, esta situa¸c˜ao pode ocorrer para v´arios componentes, fazendo com que o n´umero de instˆancias a gerir seja consideravelmente superior.

No caso da inclus˜ao expl´ıcita, h´a ainda que ter em conta que cabe ao utilizador re- lacionar os componentes de suporte com os componentes principais, isto ´e, fazer o registo dos componentes de suporte nos componentes principais. Com tantas instˆancias envolvidas que derivam de diferentes componentes pode ocorrer que esta opera¸c˜ao, que ´e aparentemente simples, se torne bastante complexa.

Relacionamento das instˆancias

De notar que as instˆancias dos componentes, mas tamb´em dos elementos da RIC, s˜ao identificados por endere¸cos, na melhor das hip´oteses por vari´aveis. Supondo, por exemplo, que a RIC cont´em v´arios elementos do tipo A e B ({A0, . . ., An, B0, . . ., Bm}), que o

componente CA ´e aplicado a cada um dos elementos do tipo A ({CA0, . . . , CAn}), que o

componente CB ´e aplicado a cada um dos elementos do tipo B ({CB 0, . . . , CB m}), e que os

componentes do tipo CA suportam a execu¸c˜ao dos componentes do tipo CB. A quest˜ao que

se levanta ´e saber como relacionar as instˆancias de CAcom as instˆancias de CB? Isto ´e, como

fazer o registo das instˆancias de CA nas instˆancias de CB?

Para responder a esta quest˜ao, h´a que saber como ´e que os elementos da RIC est˜ao relacionados; e como aceder aos elementos do tipo A a partir dos elementos do tipo B (ou vice-versa). O que requer novamente conhecimentos sobre a DIR e sobre a estrutura da RIC, que como j´a se disse, n˜ao deveria ser um requisito necess´ario a quem s´o pretende utilizar os componentes.

Al´em disso n˜ao basta conhecer a DIR e a estrutura da RIC, para se conseguir associar as instˆancias de CA `as instˆancias de CB. Por exemplo, para se executar CB i, ´e necess´ario

garantir a execu¸c˜ao pr´evia da correspondente instˆancia ou instˆancias de CA. Aceder a essa

instˆancia, que vamos supor que ´e representada por CAj, requer aceder ao elemento Bi, e a

partir deste aceder ao elemento Aj. Ambas opera¸c˜oes s˜ao poss´ıveis e relativamente f´aceis de

executar, desde que se conhe¸ca a DIR e a estrutura da RIC. O que n˜ao ´e poss´ıvel, ou pelo menos f´acil, ´e aceder a CAj a partir de Aj. Esta situa¸c˜ao encontra-se ilustrada na Figura5.5.

Neste caso em concreto, a solu¸c˜ao passa pelo utilizador assumir a responsabilidade de implementar mecanismos que associem os elementos de A com as instˆancias de CA. Gene-

ralizando a solu¸c˜ao, significa implementar mecanismos que permitam determinar para cada elemento da RIC, quais os componentes que lhe est˜ao associados. ´E de notar que a rela¸c˜ao inversa ´e estabelecida quando se faz o registo do elemento da RIC no componente. Conv´em no entanto real¸car que:

• A cada instˆancia de um componente est´a associado um ´unico elemento da RIC; mas a cada elemento da RIC podem estar associadas instˆancias de v´arios componentes;

5.1. Afinal quais s˜ao os problemas? 79

Figura 5.5: Rela¸c˜ao entre componentes e elementos da RIC.

• Componentes que s˜ao dependentes, funcionam tipicamente sobre elementos do mesmo n´ıvel de abstrac¸c˜ao, o que contribui para aumentar o n´umero de instˆancias e como tal, o n´umero de dependˆencias.

Em suma, foram apresentadas duas solu¸c˜oes: uma suportada pela inclus˜ao impl´ıcita dos componentes, que apesar de facilitar a especifica¸c˜ao dos compiladores, faz com que estes sejam muito pouco eficientes; e outra suportada pela inclus˜ao expl´ıcita dos componentes, que permite produzir compiladores mais eficientes, mas exigindo a quem especifica o compilador, muito mais trabalho, conhecimento e experiˆencia.