Algortimos para Escalonamento de Instruç˜oes e - LSCAD
Transcrição
Algortimos para Escalonamento de Instruç˜oes e - LSCAD
Algortimos para Escalonamento de Instruções e Alocação de Registradores na Infraestrutura LLVM Lucas da Costa Silva Qualificação Orientação: Prof. Dr. Ricardo Ribeiro dos Santos Área de Concentração: Ciência da Computação Qualificação apresentada como um dos requisitos para a obtenção do tı́tulo de mestre em Ciência da Computação. facom ufms Faculdade de Computação Universidade Federal de Mato Grosso do Sul 24 de maio de 2011 Conteúdo 1 Introdução 1 2 Fundamentação Teórica 3 2.1 Problema de Alocação de Registradores . . . . . . . . . . . . . . . . . . . . 3 2.2 Integração de Escalonamento e Alocação de Registradores . . . . . . . . . 7 3 Alocação de Registradores para Arquitetura 2D-VLIW 13 3.1 Arquitetura 2D-VLIW . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 3.2 Algoritmo de Escalonamento e Alocação de Registradores Baseado em Isomorfismo de Subgrafos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 4 Infraestrutura de compilação LLVM 22 4.1 Compilador LLVM . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22 4.2 Escalonamento de Instruções . . . . . . . . . . . . . . . . . . . . . . . . . . 25 4.3 Alocação de Registradores . . . . . . . . . . . . . . . . . . . . . . . . . . . 26 4.4 Especificação de um Modelo de Arquitetura . . . . . . . . . . . . . . . . . 27 5 Proposta de Trabalho 30 Referências Bibliográficas 32 i Capı́tulo 1 Introdução O escalonamento de instruções e a alocação de registradores são atividades que impactam diretamente no desempenho final de uma aplicação, uma vez que determinam precisamente como o hardware será utilizado. Tais atividades devem ser executadas levando em consideração os recursos fı́sicos existentes no processador alvo. Desde a década de 70, a comunidade da área de compiladores tem dedicado esforço considerável em busca de soluções para os problemas de escalonamento e alocação de recursos em processadores. Muito desse esforço deve-se ao fato que tais problemas pertencem à classe NP-completo [1]. Com isso, surge a necessidade de pesquisas em busca de técnicas e algoritmos mais avançados. Aliado a isso, o surgimento de processadores com múltiplos núcleos (Multicores) aumenta a complexidade dessas atividades, pois há de se considerar não apenas os múltiplos recursos, mas também a maneira como estão interconectados e suas restrições de utilização. A arquitetura 2D-VLIW [16] apresenta uma topologia que explora o paralelismo em nı́vel de instrução através de uma via de dados baseada em um pipeline e um modelo de execução bidimensional. Visando a geração de código para a arquitetura 2D-VLIW, tem-se adotado uma solução para escalonamento e alocação de registradores baseada na técnica de isomorfismo de subgrafos [17]. De maneira geral, o algoritmo de escalonamento de instruções 2D-VLIW recebe as operações na forma de um Grafo Acı́clico Dirigido (DAG – Directed Acyclic Graph) e invoca um procedimento de matching que procura um subgrafo que seja isomorfo ao DAG de entrada. O principal objetivo deste projeto de mestrado é implementar a integração desse algoritmo de escalonamento e alocação para a arquitetura 2D-VLIW junto ao compilador LLVM [8]. O compilador LLVM é uma infraestrutura de compilação em que uma das suas maiores vantagens é a possibilidade de customização, que permite uma alteração em seus módulos e a integração com novos algoritmos. Além da integração, serão executados experimentos visando a validação e a verificação de desempenho desse novo algoritmo junto ao compilador LLVM. Este texto de qualificação está organizado conforme segue. O Capı́tulo 2 apresenta a fundamentação teórica dos conceitos considerados ao longo do texto, relatando as pesqui- 1 sas relacionadas e apresentando seus resultados principais. No Capı́tulo 3 é apresentada e descrita a arquitetura 2D-VLIW em uma visão de alto nı́vel para um detalhamento dos algoritmos de escalonamento de instruções e alocação de registradores baseados em isomorfismo de subgrafos. No Capı́tulo 4, detalha-se a infraestrutura de compilação LLVM, além de apresentar aspectos relevantes para integração com os algoritmos apresentados no Capı́tulo 3. A proposta de trabalho é apresentada no Capı́tulo 5. 2 Capı́tulo 2 Fundamentação Teórica Este capı́tulo apresenta as definições básicas para a resolução do problema de alocação de registradores. Além disso, apresenta também outros trabalhos na literatura da área que propõem técnicas para resolução do escalonamento de instruções e alocação de registradores de maneira integrada. O escalonamento de instruções consiste em atribuir operações a recursos fı́sicos (elementos de processamento, unidades funcionais), aptos a executá-los de acordo com uma ordem especı́fica. A alocação de registradores é uma tarefa importante na otimização de geração de código que afeta diretamente o desempenho do programa gerado. Há um vasto número de trabalhos para o problema da fase de alocação de registradores. O objetivo desta fase é mapear os registradores virtuais (utilizados pelas instruções) nos registradores fı́sicos da arquitetura alvo. Caso não hajam registradores disponı́veis suficientes, ocorre o evento spill — então instruções são adicionadas para gravar e ler da memória. Heurı́sticas são estudadas para resolver o problema da alocação de registradores e minimizar o spill. Uma técnica também utilizada é o coalescing que é a ação de unir duas variáveis, não interferentes, que são relacionadas por uma cópia de instrução para o mesmo registrador. 2.1 Problema de Alocação de Registradores Em virtude da complexidade inerente à atividade de alocação de registradores, apresenta-se a seguir definições básicas que serão usadas pelas técnicas e algoritmos descritos na sequência. A Figura 2.2 apresenta um exemplo das definições apresentadas. Definição 1 Alocação de Registradores é a otimização que faz o mapeamento dos registradores virtuais em registradores fı́sicos da arquitetura alvo. Definição 2 Live range de uma variável é definida como o perı́odo que ela permanece viva, ou seja, desde o momento que ela é definida até o momento que é redefinida ou até seu último uso [1]. 3 Definição 3 Bloco Básico é uma sequência de instruções, tal que a execução flui, sem desvios da primeira instrução do Bloco Básico até a última instrução do Bloco Básico [2]. Definição 4 Grafo de interferência é um grafo G = (V, E), em que os elementos (vértices) de V representam as live ranges existentes no código analisado e os elementos (arestas) de E representam a interseção dessas live ranges. A alocação de registradores pode ser realizada utilizando o algoritmo de Coloração de Vértices [1]. Essa coloração é feita a partir do grafo de interferência. O algoritmo procura colorir o grafo G com um número de cores K (representa o número de registradores fı́sicos) de maneira que nenhum par de vértices conectados por uma aresta tenha a mesma cor. Se uma K -coloração não for encontrada então ocorre o spill. Spill gera novas instruções e um novo grafo de interferência. O pressuposto para a K -coloração de um grafo G, é que, suponha que G tem menos do que K vizinhos, remova o vértice i e seus vizinhos de G obtendo um subgrafo G’. Uma K -coloração de G’ pode ser estendida para uma K -coloração de G atribuindo ao vértice i uma cor que não foi atribuı́da a nenhum de seus vizinhos [1]. O algoritmo que realiza uma K -coloração tem as seguintes fases, representadas na Figura 2.1: • renumber: descobre as live ranges (variáveis com interferência) • build: constrói o grafo de interferência • coalescing: une live ranges não interferentes • spill costs: calcula o custo do spill • simplify: método de coloração • spill code: insere novas instruções por causa do spill (load e store) • select: mapeia os registradores virtuais nos registradores fı́sicos build coalescing spill costs simplify renumber select spill code Figura 2.1: Fases do algoritmo de Coloração de Vértices Deve-se destacar que a fase coalescing pode aumentar o spill. Destaca-se também a possibilidade de utilizar uma nova fase denominada freeze: se não puder remover vértices por simplify nem por coalescing, então o passo freeze deve marcar um vértice para não ser considerado pelo coalescing [2]. Definição 5 Dada uma operação do tipo a := b ( move) em que as live ranges de a e b são não-conflitantes, então, os vértices que representam a e b no grafo de interferência 4 podem ser unidos e a operação pode ser eliminada. Essa união de vértices é denominada coalescing. Definição 6 Spill code é a inserção de instruções load e store no programa devido a falta de registradores. A necessidade de spill code é detectada na fase simplify, pois não há remoção de vértices do grafo de interferência [2]. A Figura 2.2 apresenta um exemplo de utilização do algoritmo de alocação de registradores através da coloração de vértices utilizando 3 registradores (K = 3). Dado o trecho de código do Algoritmo 1, tem-se a organização em blocos básicos na Figura 2.2a, as live ranges das variáveis a, b, c e d na Figura 2.2b (fase renumber ) e o grafo de interferência na Figura 2.2c (fase build ). As fases coalescing e simplify não podem ser executadas, então uma variável é selecionada para spill, fase spill cost. A variável d foi escolhida para ser spilled. Na Figura 2.2d, continuando a alocação, só restam três variáveis, cada uma pode ser colorida usando três cores. Seguindo a alocação, a variável d realmente não pode voltar ao grafo e ser colorida, então na fase spill code o código é transformado (trecho de código do Algoritmo 2), de forma que todo o processo de alocação precisa ser realizado novamente (renumber ). O novo grafo de interferência é representado na Figura 2.2e, que agora contém d1 e d2, ao invés de d. Na fase simplify primeiro remove a variável d1, como na Figura 2.2f, e depois d2, b, a e c empilhando-as. Na Figura 2.2g, fase select desempilha as variáveis a, b, c e d2 atribuindo cores, ou seja, mapeando para os registradores fı́sicos (no exemplo r1, r2 e r3) de maneira que nenhum par de vértices conectados por uma aresta tenha o mesmo registrador. Por último, desempilha a variável d1, atribuindo o registrador r1 como na Figura 2.2h. O código depois da alocação é representado no Algoritmo 3. Algoritmo 1 Algoritmo para exemplo de alocação de registradores live in (a,d) b=5 loop: c=b+a b=b-1 if b ≤ 0 goto loop live out (c,d) Algoritmo 2 Algoritmo para exemplo de alocação de registradores com spill code live in (a,d1) M[Dloc] = d1 b=5 loop: c=b+a b=b-1 if b ≤ 0 goto loop d2=M[Dloc] live out (c,d2) 5 live in(a,d) b=5 a c=a+b b=b-1 b c d live out (c,d) (a) Blocos básicos (b) Live range c b d b c a a (c) Grafo de Interferência (d) Spill variável d na c c d2 d1 b d2 b a a (e) Constrói o Grafo de Interferência depois do Spill code (f) Simplify da variável d1 c (r3) c (r3) d2 (r1) d1 (r1) b (r1) d2 (r1) b (r1) a (r2) a (r2) (g) Select nas variáveis a, b, c e d2 (h) Select na variável d1 Figura 2.2: Exemplo de Alocação de Registradores 6 Algoritmo 3 Algoritmo para exemplo de alocação de registradores com registradores fı́sicos live in (r2,r1) M[Dloc] = r1 r1=5 loop: r3=r1+r2 r1=r1-1 if r1 ≤ 0 goto loop r1=M[Dloc] live out (r3,r1) 2.2 Integração de Escalonamento e Alocação de Registradores A literatura da área de Compiladores dispõe de várias propostas de algoritmos para escalonamento de instrução e alocação de registradores de forma separada ou conjunta. Algumas dessas propostas são apresentadas a seguir. O escalonamento pode ser aplicado a uma linguagem intermediária tanto antes da alocação de registradores (prepass) quanto depois (postpass). A vantagem do prepass é a exploração do paralelismo. Implica no aumento da live range das variáveis possibilitando o aumento de spill code. Postpass não aumenta o spill code, pois a alocação já aconteceu, entretanto faz o escalonamento mais restrito. Em [6] apresenta-se as diferenças de cooperação entre escalonamento local e global com alocação local e global. Local é o rearranjo das instruções dentro de um bloco básico isolado do resto do programa. O global considera o movimento de instruções entre os blocos. Dois métodos (Integrated Prepass Scheduling — integrado no prepass — e DAGDriven Register Allocation — integrado no postpass) foram propostos como solução para escalonamento e alocação de uma forma cooperativa. No método Integrated Prepass Scheduling foram combinadas duas técnicas de escalonamento: uma para reduzir o atraso do paralelismo (CSP) — Code Scheduling for Pipelined processors que é o tempo de execução usado para computar o custo de cada vértice no DAG. O custo acumulado identifica qual vértice está no caminho crı́tico durante o escalonamento. Esses vértices no caminho crı́tico têm uma maior prioridade — e outra para minimizar o uso de registradores (CSR) — Code Scheduling to minimize Registers usage. A ideia básica é manter um número de registradores disponı́veis durante o escalonamento. Quando há registradores suficientes, o escalonador usa CSP para reduzir o atraso do paralelismo. Quando o número de registradores disponı́veis alcança um limite inferior, o escalonador troca para o CSR para controlar o uso de registradores. As simulações foram realizadas variando a quantidade de registradores de 4 a 30 e em uma máquina com alto paralelismo (considera 11 ciclos de clock — CC — para load, 3 CC para adição de inteiros, 6 CC para para multiplicação de inteiros) e médio paralelismo (similar a máquina com alto paralelismo exceto pelo clock rate menor — considera 6 CC para load, 2 CC para adição e 3 CC para multiplicação). Como medida de comparação é utilizado o número de ciclos 7 para executar o programa. Com 4 registradores há maior diferença em ambos os testes caindo a diferença exponencialmente quando aumenta a quantidade de registradores. A quantidade de número de ciclos é a mesma com 30 registradores. Na técnica DAG-Driven é utilizado um grafo de dependência. Dois termos são introduzidos: largura e altura. A largura é definida pelo máximo de vértices mutualmente independentes que precisam de um registrador de destino. A altura do DAG é o comprimento do seu caminho mais longo. Se o número de registradores é maior que a largura do DAG, a geometria permanece inalterada durante a alocação de registradores. Caso contrário, a alocação irá reduzir a largura do DAG para um número menor ou igual ao número de registradores. Enquanto a largura é reduzida, a altura é aumentada. Quanto maior a altura, maior é o caminho crı́tico. Quanto maior o caminho crı́tico, menos eficiente é o código do escalonamento. As estratégias para controlar o crescimento da altura são: exploração das dependências Write-After-Read (WAR) e o balanceamento de crescimento do DAG. O Benchmark utilizado é o Livermore loops (utilizou-se os doze primeiros programas). A medida utilizada é o número de ciclos do clock necessários para executar o programa. A quantidade de registradores variou de 4 a 30. Prepass tem um número menor de ciclos de clock comparado com o postpass, mas gera um programa com mais instruções. Ambos algoritmos se mostram efetivos em resolver o problema de interdependência entre o escalonamento e a alocação de registradores. Em um modelo de alto paralelismo o Integrated Scheduling é melhor, já o DAG-driven allocator é melhor em um modelo de médio paralelismo. O Integrated Scheduling executa o escalonamento com um número limitado de registradores e oscila entre um escalonamento baseado numa heurı́stica. Se há registradores suficientes então dá preferência para instruções que aumentam o paralelismo, senão ele prefere instruções que diminuem o número de variáveis utilizadas no mesmo instante. No DAG-driven allocator a alocação local é executada e usa a informação de dependências de dados do DAG enquanto o escalonamento é restringido pelas dependências adicionadas pela alocação. Em [14] é apresentada uma abordagem baseada na construção de um grafo de interferência paralelo (parallelizable interference graph). Definição 7 Seja Gs = (Vs , Es ) o grafo de escalonamento. Cada vértice v ∈ Vs corresponde a uma instrução do código intermediário. Há uma aresta direcionada (u, v) ∈ Es , de u para v se u deve ser executado antes de v. Definição 8 Seja Gr = (Vr , Er ) o grafo de interferência. Cada vértice v ∈ Vr representa um live range de um registrador virtual. Há um aresta não direcionada (u, v) ∈ Er se e somente se live ranges u e v se interferem. Em seguida é gerado um grafo contendo todas as falsas dependências (Gf ). As arestas do grafo serão usadas na geração do grafo de interferência paralelo. Definição 9 Seja Gf = (Vf , Ef ). Vf = Vs . Definido o conjunto Et contendo exatamente restrições da máquina no escalonamento. O par (u, v) ∈ Ef é dado por u, v ∈ Vf , u 6= v e (u, v) ∈ / Et . 8 Com o grafo de falsas dependências é definido o grafo de interferência paralelo. Basicamente esse inclui ambas arestas do grafo de falsa dependência e aquelas do grafo de interferência. Definição 10 Seja G = (V, E) o grafo de interferência paralelo. V = Vr . E = Er ∪ {{u, v} : {u, v} ∈ Ef e u, v ∈ V }. O escalonamento varre o grafo pelo incremento do número EP (número que representa o possı́vel tempo para escalonar o vértice) do vértice. Quando as operações com o mesmo número EP não podem ser escalonadas juntas então elas são postergadas e o número EP é atualizado. O procedimento de coloração é realizado primeiro gerando o grafo de interferência paralelo depois segue com a coloração exemplificada a seguir (considere r o número de registradores fı́sicos da máquina): 1. Constrói o grafo de interferência paralelo Gs . 2. Procedimento de coloração: (a) Simplify: remove v e atualiza G se há um vértice v ∈ V com grau menor 0 do que r. Usa considerações do escalonamento para remover de G e G uma aresta de falsa dependência não em Er (exemplo: uma aresta {v, u} para que o escalonamento de u com v pelo menos contribuam). (b) Spill Cost: se V não estiver vazio escolhe um vértice v ∈ V com menor custo e coloca v na lista de spill. (c) Select: colore o grafo se não há spill. (d) Spill : para cada item na lista de spill refazer o procedimento de coloração. A solução é avaliada utilizando os programas do SPEC92 benchmark. Os resultados da comparação são dados pelo tempo de execução. A melhora mais significativa é do programa 023.eqntott de 0.33% e a menos significativa é do programa 050.ear de 0.05%. Em [11] é apresentada a técnica Combined Register allocation and Instruction Scheduling Problem (CRISP) cujo objetivo é de minimizar o spill sem perder o paralelismo em nı́vel de instrução, combinando assim o escalonamento e a alocação de registradores dentro de um bloco básico como um único problema de otimização. A solução do problema é encontrar um custo C mı́nimo onde um escalonamento factı́vel T é somado ao dobro do número de elementos do conjunto de spills SV R, dado por: C = T + 2|SV R|. Estabelecese o algoritmo (α, β)-Combined Heuristic, em que os parâmetros α, β ∈ [0, 1] =⇒ α+β = 1 proveem um peso relativo para controlar a pressão nos registradores γr e considerar o paralelismo de instruções γs , dado pela função de classificação γ = αγs + βγr . Com essa classificação é realizada a ordenação das instruções em uma lista. Então executa o algoritmo greedy list scheduling. Observa-se um desempenho de melhora de 16%-21% em relação a execução de escalonamento seguido de alocação. Já para uma alocação seguida de um escalonamento o desempenho é de 4%-21%. Em [4] é apresentado o algoritmo Unified Resource Allocation using Reuse Dags and Splitting baseado no paradigma medir-e-reduzir (measure-and-reduce) tanto de registradores quanto de unidades funcionais. A técnica constrói DAG de reuso (reuse DAGs) de 9 recursos. O DAG é composto por diversas cadeias de reuso. Cada cadeira representa um conjunto de instruções que podem compartilhar o mesmo recurso. Usando reuse DAGs, essa abordagem identifica grupos de instruções nas quais o paralelismo requer mais recursos do que há disponı́vel. Então, reduções são executadas para a demanda excessiva. Se um número de cadeias excede o limite de registradores, é adicionado sequências de arestas ou código spill (spill code) no DAG de reuso para reduzir a pressão, que leva a redução do DAG de reuso. O objetivo é modificar o DAG identificando regiões de código do DAG que precisam de reduções para os recursos requeridos para os nı́veis suportados pela arquitetura. A técnica é descrita, basicamente em três passos: 1. Representação do programa e requisitos para cada recurso. 2. Algoritmos para mensurar os requisitos dos recursos. 3. Transformações para reduzir os recursos requeridos para os nı́veis suportados pela arquitetura. Uma extensão dessa técnica é apresentada em [3], baseando se na técnica IPS apresentada em [6]. A técnica apresentada é utilizado o DAG de reuso e Live range splitting para dividir a live range, reduzindo assim a demanda de registradores. O algoritmo mantém uma lista de todos os registradores sendo escalonados, quando é detectado uma alta pressão nos registradores e não há instruções prontas para escalonar então é executado a divisão da live range, que reduz o número de live ranges ativas. O live range splitting é descrito a seguir: 1. Insere instrução load na lista de escalonamento pronta. 2. Insere instrução store na lista de escalonamento não pronta. 3. Move as dependências de usos, não escalonadas, da definição original para o load inserido. Os experimentos, utilizando Testsuite Speedup, foram mensurados pelo speedup com registradores variando de 8, 16 a 32 registradores. Comparando com IPS de [6] essa abordagem teve melhor desempenho, conforme a tabela 2.1. Tabela 2.1: Comparação de speedup IPS e URSA Registradores IPS URSA 8 1,25 4,25 16 1,75 3,25 32 2 2,25 Definição 11 Um PDG ( Program Dependence Graph) [13] é um grafo direcionado que representa as dependências de controle e as dependências de dados entre as sentenças 10 do programa. Os vértices são sentenças e predicados de expressões que ocorrem em um programa. Uma aresta representa uma dependência de controle ou de dados. A Figura 2.3 apresenta um PDG de um código da mesma figura. Com arestas direcionadas para representar as dependências de dados (Data Dependence) — em tracejado; e para representar dependências de controle (Control Dependence). Os vértices estão representando tanto as sentenças e expressões de predicados. Há uma dependência de dado na instrução 1, devido a definição de i, na instrução 3, devido uso de i [12], portanto isso é representado por uma aresta direcionada tracejada do vértice 1 para o vértice 3. As sentenças while e if são representados, respectivamente, pelos vértices de predicado P 1 e P 2. O vértice de região R1 representa as condições de controle na entrada do programa. O vértice de região R2 representa as condições para entrar no laço ou para voltar em uma outra iteração. R3 representa as condições sobre a qual o corpo do laço é executado. R4 representa a região do if quando verdadeiro, e caso contrário, else, que é representado pela região R5. Figura 2.3: Exemplo de PDG (Program Dependence Graph) [12] Em [12] o escalonamento cooperativo global executa escalonamento em regiões é chamada de Register Allocation Sensitive Region (RASER). Esta técnica sobre o PDG tenta criar regiões de código com uma quantidade igual de paralelismo. O objetivo é combinar o paralelismo estimado em cada região com a quantidade de paralelismo explorável para a arquitetura alvo. A técnica é baseada no seguinte fluxo de execução: 1. Obtenção de estimativa do paralelismo disponı́vel em cada região do PDG. 2. Execução do Integrated Prepass Scheduling (IPS) em cada região, baseado em [6]. 11 3. Construção de uma lista de regiões que o número de variáveis vivas excede o número de registradores fı́sicos. 4. Redução dos spills por meio da redução da quantidade de variáveis vivas nestas regiões. 5. Aplicação de transformações para aumentar o paralelismo nas regiões. 6. Eliminação de regiões sem descendentes. O desempenho do RASER é comparado com o desempenho de um escalonamento de regiões padrão (blocos básicos) seguido pela alocação de registradores. Os experimentos são executados em uma máquina hipotética com médio paralelismo e com unidades funcionais em paralelo. Um simulador é usado para determinar o número de ciclos necessários para executar o código intermediário e o número de loads e stores executados devido ao spilling. Utiliza-se programas do benchmark Livermore loops, variando o número de registradores de 3 a 16. No estudo é indicado que para um conjunto pequeno de registradores, em geral, RASER gera código que executa mais rápido, em 17 de 24 experimentos com 3 registradores e em 19 de 24 com 4 registradores. No pior caso a redução do número de ciclos executados varia de 0.2% até 49.2% em comparação com o postpass. Esta combinação é particularmente benéfica em uma máquina com alto paralelismo. A medida utilizada é o speedup. 12 Capı́tulo 3 Alocação de Registradores para Arquitetura 2D-VLIW Este capı́tulo apresenta a arquitetura 2D-VLIW (Two-Dimensional Very Long Instruction Words), uma técnica que explora o paralelismo em nı́vel de instrução. Também apresenta uma técnica que considera a geometria dos grafos de entrada, assim como a topologia dos elementos de processamento para ser utilizado no algoritmo de Escalonamento e Alocação de Registradores Baseado em Isomorfismo de Subgrafos. O resultado do escalonamento é uma instrução 2D-VLIW. 3.1 Arquitetura 2D-VLIW Two-Dimensional Very Long Instruction Words (2D-VLIW) é uma arquitetura de alto desempenho que explora o paralelismo em nı́vel de instrução através de um arranjo bidimensional de recursos de hardware e um pipeline. Na arquitetura 2D-VLIW, o compilador é o único responsável por alocar operações para os recursos disponı́veis. Essa nova organização dos elementos de hardware vai ao encontro dos grafos de dependências de operações, procurando maximizar o desempenho na execução do programa e melhorar a escalabilidade da arquitetura. 2D-VLIW segue a terminologia EPIC de operações e instruções: uma operação corresponde a uma instrução RISC-like e uma instrução a um grupo de operações. A execução dessas operações é auxiliada por um conjunto de registradores locais, denominados registradores temporários. Os recursos de hardware, na arquitetura 2D-VLIW, são comumente organizados como uma matriz de unidades funcionais que possui uma rede de interconexão bastante simples onde uma unidade funcional na linha i, onde 0 ≤ i < Quantidade de linhas, é interconectada a outras duas unidades funcionais na linha (i + 1) mod Quantidade de linhas. Essa interconexão é estática independente do programa sendo executado. Por outro lado, é flexı́vel o bastante para mapear operações de uma gama de aplicações diferentes [17]. Na arquitetura 2D-VLIW, tem-se adotado uma solução para escalonamento de ins13 truções e alocação de registradores baseada na técnica de isomorfismo de subgrafos. De maneira geral, o algoritmo de escalonamento de instruções 2D-VLIW recebe as operações na forma de um DAG e invoca um procedimento de matching (combinação) que procura um subgrafo que seja isomorfo ao DAG de entrada. A Figura 3.1 apresenta uma visão geral da arquitetura 2D-VLIW. Nessa figura é possı́vel observar a via de dados usada por uma instrução e a organização das UFs (unidades funcionais) através da matriz 4 × 4. Por simplicidade, a hierarquia de memória e os registradores temporários não foram exibidos. Os resultados de uma UF podem ser escritos no Temp Register File (TRF) ou no Global Register File (GRF). TRF é um pequeno banco contendo dois registradores dedicados para cada UF. O GRF tem 32 registradores. O resultado de uma UF é sempre escrito em um registrador interno chamado FU Register (FUR). Figura 3.1: Visão da 2D-VLIW As instruções 2D-VLIW são formadas por operações simples, que executam sobre a matriz de unidades funcionais. O número de operações em uma instrução é equivalente à quantidade de unidades funcionais na matriz. Assim, para a matriz 4 × 4 da Figura 3.1, uma instrução 2D-VLIW possui 16 operações. Caso não seja possı́vel encontrar 16 operações no programa para preencher toda a matriz, operações especiais do tipo nop (operações nulas que não alteram os estados dos registradores) são inseridas. A execução de instruções de um programa sobre a arquitetura 2D-VLIW passa por registradores de pipeline ao longo da via de dados. Em cada ciclo de relógio, uma instrução 2D-VLIW é buscada da memória e colocada no registrador Busca/Decod. Quando alcança os estágios de execução as operações que formam a instrução são executadas, em paralelo, de acordo com o número de UFs na matriz. 14 Esta arquitetura depende da geração de código, mais precisamente, do escalonamento de instruções e alocação de registradores. No caso de 2D-VLIW, o problema de escalonamento de instruções é modelado utilizando duas heurı́sticas distintas: um algoritmo baseado em isomorfismo de subgrafos e um algoritmo guloso baseado em list scheduling. O primeiro algoritmo representa tanto as operações de um programa quanto os recursos da arquitetura através de dois grafos distintos: o grafo do programa e o grafo da arquitetura. O objetivo do algoritmo de isomorfismo é obter um subgrafo do grafo da arquitetura que seja isomorfo ao grafo do programa. Esse subgrafo indica exatamente quais recursos do processador devem ser utilizados por cada operação. O algoritmo guloso baseado em list scheduling estende o algoritmo tradicional list scheduling adaptando-o para representar a topologia da arquitetura e obedecer às restrições de interconexão. O problema da alocação de registradores na arquitetura 2D-VLIW é abordado através de uma extensão do algoritmo original de escalonamento de instrução baseado em isomorfismo de subgrafos. Nesse novo algoritmo, o grafo da arquitetura é dotado com vértices representando os registradores temporários e globais, possibilitando a alocação de registradores simultânea com a fase de escalonamento. 3.2 Algoritmo de Escalonamento e Alocação de Registradores Baseado em Isomorfismo de Subgrafos Definição 12 No problema de isomorfismo de grafos, dois grafos G1 = (V1 , E1 ) e G2 = (V2 , E2 ) são ditos isomorfos, denotado por G1 ∼ = G2 se existe uma bijeção ϕ : V1 → V2 tal que, para todo par de vértices vi , vj ∈ V1 vale que (vi , vj ) ∈ E1 se e somente se (ϕ(vi ), ϕ(vj )) ∈ E2 . [17]. Definição 13 No problema de isomorfismo de subgrafos, o grafo G1 = (V1 , E1 ) é isomorfo 0 0 ao grafo G2 = (V2 , E2 ) se existe um subgrafo de G2 , por exemplo G2 , tal que G1 ∼ = G2 [17]. A Figura 3.2 exemplifica o isomorfismo de subgrafos. As Figuras 3.2a e 3.2b mostram o grafo de entrada e o grafo base, respectivamente. O resultado do mapeamento das arestas e vértices do grafo G1 para o grafo G2 é o grafo resultante na Figura 3.2c. Observe que ϕ(a) = 6, ϕ(b) = 5, ϕ(c) = 4, ϕ(d) = 3. A Figura 3.3 mostra um exemplo de escalonamento de instruções baseado em isomorfismo de subgrafos. Essa técnica considera a geometria dos grafos de entrada assim como a topologia da matriz de unidades funcionais. O resultado do escalonamento é uma instrução 2D-VLIW, em que cada posição da instrução possui uma operação que será executada à medida que a instrução percorre a matriz de UFs. O Algoritmo 5 descreve os passos principais de escalonamento de instruções 2D-VLIW 15 b a 5 5 (b) 6 6 (a) ab ab a c 1 d 4 1 2 (a) Grafo de entrada G1 3 2 (b) Grafo base G2 4 (c) 1 3 (d) 2 0 (c) Grafo resultante G2 Figura 3.2: Exemplo do problema de isomorfismo de subgrafos. Figura 3.3: Escalonamento de instruções baseado em isomorfismo de subgrafos baseado em isomorfismo de subgrafos. Nesse algoritmo, algumas heurı́sticas são adaptadas visando facilitar a determinação do isomorfismo [17]. Considerando que o algoritmo executa o isomorfismo entre dois grafos (grafo de entrada e grafo base) que representam as dependências dos dados do programa e as caracterı́sticas da arquitetura, as seguintes restrições devem ser obedecidas: • i, j, k ∈ I, • m ∈ M, ∀ 0 ≤ i, j, k < |I| ∀ 0 ≤ m < |M | • Em uma cadeia de dependência de operações opj , opk → opi (opi depende dos resultados de opj e opk ) existente no grafo de entrada, opi pode ser escalonada em uma U Fm se e somente se opj e opk já foram escalonadas e seus resultados estão vivos na entrada de U Fm . • Uma operação opi pode ser escalonada em uma U Fm se e somente se um registrador de escrita (TRF ou GRF) alcançável por U Fm está livre. • Se não existirem registradores temporários livres no momento do escalonamento da operação opi , um código de spill deve ser inserido e escalonado a fim de garantir a 16 semântica correta de execução. A mesma ação deve ocorrer caso o registrador em questão seja global. No procedimento 5, primeiro é executado o procedimento topological order para ordenar topologicamente o DAG de entrada G1 . O procedimento subg iso sched alloc 4 0 0 encontra um subgrafo G2 , G2 ⊆ G2 , isomorfo para G1 . Caso não seja encontrado então o escalonador escolhe uma heurı́stica com base no valor da variável tag e executa essa heurı́stica sobre um dos parâmetros de entrada. Algoritmo 4 Algoritmo de isomorfismo de subgrafos. ENTRADA: Grafo de entrada G1 , grafo base G2 e tag. SAÍDA: Subgrafo G02 . subg iso sched alloc(DAG: G1 , GRAFO BASE: G2 , TAG: &tag ) 1) sched nodes = 0; 2) nodeG1 = next(G1); 3) nodeG2 = next(G2); 4) while (sched nodes < |V1 | && sched nodes ≥ 0) 5) if ( IsFeasible(nodeG1 , nodeG2 ) ) 6) G02 = AddUsefulPair(nodeG1 , nodeG2 ); 7) sched nodes = sched nodes + 1; 8) nodeG1 = next(G1); 9) nodeG2 = next(G2); 10) else 11) if ( nodeG2 .index < |V2 | ) 12) nodeG2 = next(G2); 13) else 14) backtrack(); 15) end if 16) end if 17) end while 18) tag = evaluate(G02 , G1 , G2 ); 19) return G02 ; O Algoritmo 4 executa a cada iteração uma verificação se o par de vértices selecionados nodeG1 e nodeG2 são soluções factı́veis para o isomorfismo. A cada par de vértices factı́vel encontrado (linha 5) o subgrafo G02 é incrementado (linha 6). Uma par de vértices é factı́vel se os vértices possuem as mesmas caracterı́sticas (mesmo grau de entrada, mesmo grau de saı́da, etc.) e contribuem para uma solução parcial do isomorfismo. Se um par de vértices não é uma solução factı́vel, deve-se verificar se há ainda outros vértices de G2 que podem ser pesquisados (linhas 11 e 12). Caso todos os vértices de G2 já tenham sido testados, deve-se realizar uma ação de backtracking (linha 14) a fim de que um vértice previamente escalonado de G1 escolha outro vértice de G2 como par. Antes do algoritmo finalizar, a função evaluate() (linha 18) analisa o conteúdo do subgrafo G02 a fim de detectar se o isomorfismo não pode ser concluı́do e, nesse caso, retornar o resultado para a variável tag. Caso o isomorfismo não seja detectado, a função evaluate() analisa os grafos G1 e 17 G2 , assim como o valor atual da variável tag para indicar um novo valor de retorno para essa variável. Algoritmo 5 Algoritmo de escalonamento de instruções 2D-VLIW baseado em isomorfismo de subgrafos e alocação de registradores. ENTRADA: Grafo de entrada G1 e grafo base G2 . SAÍDA: Conjunto de instruções 2D-VLIW. Sched(DAG: G1 , GRAFO BASE: G2 ) 1) topological order(G1 ); 2) G02 =subg iso sched alloc(G1 , G2 , tag); 3) while (G02 == N U LL) 4) switch(tag) 5) case 1 : base graph resize(G2 ); 6) case 2 : DAG stretch(G1 ); 7) case 3 : DAG split(G1 ); 0 8) G2 =subg iso sched alloc(G1 , G2 , tag); 9) end while 10) create 2D-VLIW instruction(G02 ); A heurı́stica base graph resize(), em 5, redimensiona o grafo base com o intuito de oferecer mais opções de combinação para o grafo G1 . A heurı́stica DAG stretch transforma o grafo G1 em um grafo mais factı́vel que possa ser facilmente combinado com o grafo G2 . Heurı́stica DAG split muda os vértices registradores do grafo. Todos os vértices registradores são tratados como registradores TRF. Se não há registradores TRF suficientes para executar a ação, então ocorre o evento spill. Assim a heurı́stica divide o DAG pela modificação dos vértices TRF para vértices GRF. Se não há vértices GRF suficientes então o spill code é inserido no código, G1 é dividido e deve ser reconstruı́do. Depois da combinação ser encontrada uma instrução 2D-VLIW é criada. A heurı́stica de ordenação topológica provê uma ordenação dos vértices do grafo G1 . A Figura 3.4 apresenta um exemplo de um DAG de entrada ordenado na Figura 3.4a e o escalonamento resultante sem a realização de qualquer passo de backtracking na Figura 3.4b. O número do lado direito de cada vértice indica a ordem a ser seguida pelo escalonador. Os retângulos em branco representam as UFs disponı́veis que podem ser usadas pelo escalonador. Os retângulos hachurados indicam UFs já ocupadas. Existem dois algoritmos de ordenação topológica implementados junto ao escalonador. Um é baseado na largura do DAG de entrada e o outro é baseado numa solução do problema de BinPacking denominado Best Fit Decreasing. Outra heurı́stica é o redimensionamento do grafo base que é executada quando o grafo base G2 não possui vértices suficientes para comportar um subgrafo isomorfo para G1 . Basicamente, a heurı́stica redimensiona o grafo base, formando assim um novo grafo base maior e com mais possibilidades de comportar um subgrafo isomorfo para G1 . A Figura 3.5 exemplifica o desenrolamento de um grafo base, a matriz de UFs da Figura 3.5a, em um grafo base ainda maior apresentado na Figura 3.5b. Esse algoritmo tem sido experimentado e comparado com outras alternativas para 18 (a) Dag de entrada ordenado (b) Estado do escalonamento sem qualquer backtracking para o DAG 3.4a Figura 3.4: Exemplo de ordenação topológica (a) Dag de entrada ordenado (b) Estado do escalonamento sem qualquer backtracking para o DAG 3.5a Figura 3.5: Exemplo de redimensionamento de grafo base os mesmos problemas. Um dos experimentos consistiu na comparação entre o algoritmo de escalonamento de instruções apresentado em [17] e o algoritmo de escalonamento de instruções integrado com a alocação de registradores (IRA – Instruction scheduling and Register Allocation). As métricas utilizadas foram: número de instruções 2D-VLIW, OPI (operações por instrução) e tempo de combinação usando programas de SPEC e MediaBench com o compilador Trimaran [5]. Para 6 de 9 programas IRA apresentou uma melhor utilização de recursos. Outro resultado é a comparação de desempenho entre escalonamento de instruções e IRA. IRA obteve melhor resultado em 4 de 9 programas, segundo o autor o resultado era esperado devido ao tratamento de spill code realizado pelo IRA. A Figura 3.6 exibe a representação dos bancos de registradores temporários como vértices do grafo base. Deve estar claro que qualquer registrador global pode ser lido e 19 escrito por qualquer UF desde que o limite do uso de portas de leitura e escrita sejam obedecidos. Outro ponto importante a ser observado é que os valores lidos do banco de registradores são enviados para as UFs através de registradores de pipeline. Figura 3.6: Registradores globais e temporários A vantagem dessa abordagem de alocação de registradores é a determinação de quais registradores serão utilizados e, principalmente, garantir que o resultado de uma operação esteja vivo na entrada das UFs das operações dependentes. O grafo de entrada é representado na Figura 3.7a e na 3.7b o mesmo grafo caracterizado com vértices representando registradores. O resultado de escalonamento e da alocação de registradores considerando esse grafo de entrada sobre a matriz de UFs com seus registradores temporários é apresentado na Figura 3.7c. 20 (a) Representação original do grafo de entrada (b) Representação do grafo de entrada com vértices indicando o uso de registradores (c) Resultado do escalonamento e da alocação de registradores Figura 3.7: Grafo de entrada representando as operações e registradores e o resultado final de escalonamento com alocação de registradores 21 Capı́tulo 4 Infraestrutura de compilação LLVM Este capı́tulo apresenta a infraestrutura de compilação Low-Level Virtual Machine (LLVM). Destacando o back-end e as fases necessárias para este trabalho como o escalonamento de instruções, a alocação de registradores e a geração de código. 4.1 Compilador LLVM O compilador Low-Level Virtual Machine (LLVM) é uma infraestrutura modular construı́da com uma coleção de ferramentas reutilizáveis. O LLVM foi iniciado num projeto da Universidade de Illinois, desde então cresceu como um projeto consistente. O código do projeto está sob a licença da “UIUC” (University of Illinois at Urbana-Champaign) BSD-Style (Berkeley Software Distribution). O LLVM, em 2010, recebeu o prêmio da ACM SIGPLAN (Special Interest Group on Programming Languages) em reconhecimento do impacto que teve na comunidade de pesquisa em compiladores, que pode ser constatado pelo grande número de publicações que trazem referências a esse compilador. As vantagens da infraestrutura do LLVM são o seu design (que faz ser simples para entender e usar), a linguagem independente e a extensibilidade [8]. A Figura 4.1 representa em alto nı́vel a arquitetura do LLVM. Em compilador estático é realizada a transformação de linguagens como C, C++ e Objective-C para uma linguagem de representação intermediária (LLVM IR), realizada no front-end. Pode-se utilizar tanto o GCC (llvm-gcc) quanto o Clang como front-end. A linguagem intermediária LLVM IR é baseada em RISC e na forma SSA (static single assignment form). Com essa linguagem intermediária o LLVM gera o LLVM Object, que pode ser executado pela infraestrutura do LLVM utilizando o tradutor Just in Time (JIT). Então, o LLVM pode trabalhar com o LLVM Object, chamado também de bitcode, realizando otimizações, gerando o código Assembly ou gerando código nativo para arquiteturas especı́ficas de máquinas como X86, ARM, PowerPC, MIPS, SPARC, XCore, entre outas. O código nativo é o código linked com as bibliotecas necessárias para execução. 22 Back-end Front-end Assembly Compilador estático LLVM IR LLVM Object Otimizações Código Nativo Figura 4.1: Diagrama representando o esquema de geração de executáveis no LLVM A forma SSA é uma representação intermediária em que cada variável tem somente uma definição no programa. Na Figura 4.2 há um exemplo de código em C e a respectiva versão na forma SSA [2]. Figura 4.2: SSA static single assignment form (Forma única de atribuição) Dentre o conjunto de ferramentas do LLVM, destacam se: • clang ou llvm-gcc: invoca o fron-end gerando um objeto LLVM IR. • llc: invoca o back-end recebendo um objeto LLVM IR. • opt: invoca o back-end fazendo otimizações em um objeto LLVM IR. • llvmc: executa a compilação e tem seus argumentos baseados no compilador GCC. O back-end do LLVM contém bibliotecas independentes para cada arquitetura. Nesta fase há a tradução da LLVM IR para instruções reais e registradores reais. Exemplo de código na representação em C e em LLVM IR, respectivamente, é apresentado na Figura 4.3: A Figura 4.4 representa uma visão abstrata dos estágios de geração de código no back-end. A geração de código está dividida nos seguintes estágios [19]: • Seleção de instrução: produz um código inicial para o conjunto de instruções. A seleção de instruções então faz uso de registradores virtuais na forma SSA e registradores fı́sicos que são necessários devido à restrição da arquitetura. Tem como entrada as instruções LLVM IR e constrói um DAG das instruções chamado de ilegal (contém instruções não suportadas) e a representação do conjunto de instruções da 23 Figura 4.3: Exemplo de código em C e em LLVM IR máquina alvo. Esta fase produz como saı́da um DAG de instruções (legais) da arquitetura alvo (SelectionDAG), utilizando um algoritmo guloso adaptado para DAGs. • Escalonamento: recebe o SelectionDAG de instruções legalizado, determina uma ordem para as instruções, então emite as instruções como instruções de máquina, utilizando uma variação do algoritmo list scheduling. • Otimização em código de máquina baseado em SSA: é uma fase opcional que opera sobre a forma SSA produzida pela seleção de instruções. Exemplos de otimizações nessa fase são modulo-scheduling e peephole. • Alocação de registradores: o código é transformado de uma infinidade de registradores virtuais numa forma SSA para registradores fı́sicos. Esta fase insere spill code e elimina todas as referências de registradores virtuais do programa. • Inserção de código prólogo/epı́logo: nesta fase códigos de prólogo e epı́logo podem ser inseridos e as localizações de referências abstratas são removidas. • Otimizações tardias: otimizações que operam no código de máquina final são realizadas nesta fase. Exemplos de otimizações nesta fase são spill code scheduling e peephole. • Emissão de código: a fase final tem como saı́da o código em formato assembly ou em código de máquina. Uma estrutura importante do LLVM é o Pass, pois ele gerencia a execução das transformações e otimizações realizadas pelo compilador. Dependendo de como o Pass trabalha, ele deve herdar métodos das classes ModulePass, CallGraphSCCPass, FunctionPass, LoopPass, RegionPass ou BasicBlockPass, que fornecem mais informações sobre a funcionalidade do Pass. Uma das principais caracterı́sticas do Pass é o escalonamento dos passes que executam de uma maneira eficiente baseado nas restrições que o Pass encontra (indicado pela classe que foi herdada). A classe que gerencia o escalonamento dos passes é a classe PassManager. Nela, os resultados de análises são compartilhados, não permitindo assim o cálculo de uma análise mais de uma vez. 24 LLVM Front-end Seleção de Instrução Escalonamento Fast Otimização ListScheduling Back-end Alocação de Registradores Fast Linear Scan Inserção de código Prólogo/Epílogo Otimizações tardias PBQP Emissão de código Assembly Código nativo Figura 4.4: Fases do LLVM na geração de código 4.2 Escalonamento de Instruções O escalonamento inicia na última fase da seleção de instrução. Esta fase recebe o DAG das instruções alvo produzidas pela fase seleção de instruções e determina uma ordem de execução para cada vértice do DAG. Então o DAG é convertido para uma lista de MachineInstrs e o SelectionDAG é destruı́do. Este passo usa a técnica tradicional prepass. Essa fase é logicamente separada da fase de seleção de instrução, mas está conectada fortemente no código porque operam sobre o SelectionDAGs [19]. Exemplo de trecho de código na Figura 4.5a é representado no SelectionDAG na Figura 4.5b, o grafo é montado de baixo para cima, mas a execução se mantém como a do código, realizando operações de ponto flutuante, primeiro a adição entre W e X, depois multiplica o seu resultado por Y e em seguida soma o resultado com Z. fadd:f32 fmul:f32 fadd:f32 W (a) Trecho de código Z Y X (b) SelectionDAG Figura 4.5: Exemplo do SelectionDAG (fadd:f32 (fmul:f32 (fadd:f32 W, X), Y), Z) O algoritmo utilizado pelo LLVM é o list scheduling, que pode ser utilizado em duas abordagens: top down e bottom up. O algoritmo basicamente usa uma fila de prioridade para escalonar. A cada instante, os vértices de mesma prioridade são retirados da fila, em ordem, e escalona se for um escalonamento legal. Uma ilegalidade pode ser devido a conflitos estruturais (pipeline ou restrições de recursos) ou porque uma entrada para a instrução não completou a execução. 25 Um outro algoritmo também utilizado para o escalonamento é Fast, que executa o list scheduling com uma fila de prioridade para escalonar a estrutura de dados FastPriorityQueue, que simplesmente é uma fila de prioridades onde todos os vértices do grafo de escalonamento têm a mesma prioridade. 4.3 Alocação de Registradores O primeiro passo para a alocação é a determinação das live ranges das variáveis vivas. As instruções PHI (φ), criadas pela forma SSA, devem ser tratados especialmente, porque somente a definição é tratada, pois os usos serão tratados em um outro bloco básico. Para cada instrução PHI do bloco básico, é simulado uma alocação até o fim do bloco básico e o bloco sucessor transversal. Se o bloco básico sucessor tem uma instrução PHI e um dos operandos está vindo do bloco corrente, então a variável é marcada como viva dentro do bloco básico corrente e todos os seus blocos básicos predecessores até o bloco básico com a definição da instrução encontrada. O LLVM denota os registradores fı́sicos denotados por números inteiros de 1 a 1023. Assim os registradores virtuais iniciam a partir do 1024. Para algumas arquiteturas como X86 alguns registradores fı́sicos são marcados como aliased, por compartilhar o mesmo endereço fı́sico (exemplo: o EAX, AX e AL, compartilham os oito primeiros bits). Registradores fı́sicos, no LLVM, são agrupados em Register Classes. Registradores fı́sicos são estaticamente definidos no arquivo TargetRegisterInfo.td enquanto que os virtuais devem ser criados chamando MachineRegisterInfo::createVirtualRegister(). Uma importante transformação acontece durante a alocação de registradores chamada SSA Deconstruction Phase. A forma SSA simplifica muitas análises que são executadas no grafo de fluxo de controle. Entretanto, conjunto de instruções tradicionais não implementam instruções PHI. Então, o compilador deve substituir as instruções PHI com outras instruções que preservam a mesma semântica [19]. A infraestrutura do LLVM provê diferentes alocações de registradores: • Linear Scan: o algoritmo utiliza uma estratégia gulosa para realizar a alocação de registradores. O algoritmo inicia transformando os blocos básicos numa sequência linear. Então, realiza a coloração baseada nas live ranges em que duas live ranges que se sobrepõem não podem ser alocadas para o mesmo registrador [15]. • Fast: aloca basicamente em nı́vel de bloco, tentando manter valores em registradores. Não há registradores vivos entre blocos. Portanto tudo é spilled no inı́cio e no final de cada bloco. Esse algoritmo é usado, principalmente, em situações de depuração de código com registradores alocados. • PBQP — (Partitioned Boolean Quadratic Programming): trabalha na construção de um problema PBQP que representa o problema de alocação de registradores, resolve o problema PBQP e converte a solução para a um problema de alocação de registradores. Se alguma variável foi selecionada para spill, então o código spill é adicionado 26 e o processo é repetido. A representação do PBQP é um grafo, onde os vértices são os registradores virtuais e as arestas são as restrições entre os registradores (como as interferências). Cada vértice é associado a um vetor de custo (que dá o custo de cada alocação para alocar o registrador virtual). Tipicamente pode-se estimar o custo de spill e o custo zero para cada registrador, entretanto pode-se atribuir valores de custo positivos ou negativos para cada registrador fı́sico priorizando-o. Cada aresta é associada com a uma matriz de custo (que é a interferência entre os vértices). O custo de uma especı́fica solução (alocação de registradores), é dada pela soma dos custos de todos os vértices e arestas do grafo. O problema é resolvido por programação dinâmica [18]. 4.4 Especificação de um Modelo de Arquitetura No projeto LLVM as descrições de dados são descritas em Tablegen. Tablegen consiste em classes e definições. Classes são registros abstratos que são usados para construir outros registros. Definições são registros concretos. Estes não têm qualquer valor indefinido. Os registros têm um único nome, uma lista de valores e uma lista de superclasses. A lista de valores contém os dados que constroem os registros. Um exemplo é representado na Figura 4.6, que contém a definição de uma instrução add registrador - registrador de 32 bits para a arquitetura X86. A palavra após “def ” indica o nome do registro “ADD32rr ”. O corpo do registro contém todos os dados que o TableGen monta para o registro, indicando que a instrução faz parte do namespace “X86”, o padrão indica como a instrução deve ser emitida no arquivo assembly, que é uma instrução de dois endereços, etc. Para escrever um compilador back-end para LLVM que converte a representação LLVM IR para código de uma arquitetura alvo, deve-se seguir os passos: 1. Criar uma subclasse da classe TargetMachine que descreve as caracterı́sticas da arquitetura alvo. 2. Descrever o conjunto de registradores do alvo. Usar TableGen para gerar código para definição de registradores, aliases de registradores e classes de registradores de um alvo especı́fico do arquivo de entrada RegisterInfo.td. Deve-se escrever um código adicional para a subclasse da classe TargetRegisterInfo que representa o arquivo de dados usado para a alocação de registradores e também descreve as interações entre registradores. 3. Descrever o conjunto de instruções para a arquitetura alvo. Usar TableGen para gerar código para as instruções especı́ficas descritas em TargetInstrFormats.td e TargetInstrInfo.td. Deve-se escrever código adicional para uma subclasse da classe TargetInstrInfo que representa as instruções de máquina suportadas pela máquina alvo. 4. Descrever a seleção e conversão do LLVM IR para uma representação em DAG das instruções para instruções nativas da arquitetura alvo. Usar TableGen para 27 Figura 4.6: Definição da instrução add em X86 gerar código que combina padrões e seleciona instruções baseadas em informação adicional da arquitetura alvo de TargetInstrInfo.td. Deve-se escrever código para XXXISelDAGToDAG.cpp, onde XXX identifica o alvo especı́fico, para executar a combinação de padrões e seleção de instruções DAG-to-DAG . Também escrever código em XXXISelLowering.cpp para substituir ou remover operações e tipos de dados que não são suportados nativamente em um SelectionDAG. 5. Escrever código para um assembly printer que converte LLVM IR para um formato GAS (GNU Assembler ) para arquitetura alvo. Deve-se adicionar assembly strings para instruções definidas na versão de TargetInstrInfo.td da arquitetura alvo. Devese escrever também um código para a subclasse AsmPrinter que executa a conversão 28 LLVM-to-assembly e uma subclasse de TargetAsmInfo. 6. Opcionalmente, para suportar sub alvos (exemplo: variantes com capacidades diferentes), deve-se também escrever código para uma subclasse da classe TargetSubtarget, que permite usar em linha de comando os parâmetros -mpcu= e -mattr=. 7. Opcionalmente, para adicionar suporte a Just In Time (JIT), cria-se um emissor de código de máquina (subclasse de TargetJITInfo) que é usado para emitir código binário diretamente na memória. 29 Capı́tulo 5 Proposta de Trabalho Esse trabalho visa o desenvolvimento dos algoritmos de escalonamento de instruções e alocação de registradores projetados para a arquitetura 2D-VLIW sobre a infraestrutura de compilação LLVM. De forma especı́fica, vislumbra-se implementar o algoritmo integrado de escalonamento e alocação de registradores apresentado no Capı́tulo 3 junto à infraestrutura de compilação LLVM além de descrever o modelo de processador (2DVLIW) que pode se beneficiar de algoritmos de escalonamento e alocação integrados. A motivação desse trabalho reside na possibilidade de realizar comparações entre soluções de escalonamento e alocação de recursos em diferentes arquiteturas compostas por múltiplas unidades funcionais. A integração com o LLVM torna-se factı́vel devido a sua modularização como explicado na Seção 4.1. O LLVM é um projeto código aberto disponibilizado sob a licença University of Illinois/NCSA Open Source License. Para ajuda há uma lista de discussão em: http: //lists.cs.uiuc.edu/mailman/listinfo/llvmdev. Os objetivos especı́ficos são: 1. Estender e integrar as fases da geração de código de forma a modificar os DAGs necessários para o algoritmo de isomorfismo de subgrafos como explicado na Seção 3.2. O código do algoritmo de escalonamento de instruções e alocação de registradores já se encontra implementado de forma independente do LLVM. Deve-se realizar uma pesquisa detalhada sobre a estrutura de dados disponibilizada pelo LLVM a fim de adequar as entradas e saı́das do algoritmo; As fases necessárias para implementar alterações no LLVM estão descritas no Capı́tulo 4; 2. Projetar e implementar no LLVM a geração de código para a máquina da arquitetura 2D-VLIW. As fases necessárias para implementar uma arquitetura alvo, no LLVM, estão descritas no Capı́tulo 4; e a descrição da arquitetura 2D-VLIW no Capı́tulo 3; 3. Avaliar experimentalmente e verificar métricas para mensurar o desempenho em diferentes arquiteturas. 30 Simulação de programas dos benchmarks Livermore loops [10], SPEC [7] e MediaBench [9] utilizando os algoritmos de escalonamento e alocação implementados, comparando os resultados com aqueles utilizados por algoritmos da literatura da área. Análise dos resultados da simulação considerando métricas como: speedup e IPC (instructions per cycle) dos programas, tempo de execução dos algoritmos, número de spill codes gerados, ciclos gerados no código como resultado do escalonamento. 4. Preparar artigos e documentos para serem submetidos em eventos e periódicos da área; Uma proposta de cronograma e atividades para o desenvolvimento deste projeto é apresentada na Tabela 5.1. Tabela 5.1: Cronograma para execução do plano de trabalho Atividades 1 2 3 4 5 M A X X M J X X 2011 J A X X X X X X S O N D X X X X X X X X X X X X 2012 J F X X X X Atividades 1. Projeto e estruturação de algoritmos de escalonamento e alocação de registradores. 2. Implementação de algoritmos de escalonamento e alocação de registradores junto à infraestrutura LLVM. 3. Experimentação e avaliação de desempenho para efeitos de comparação com trabalhos relacionados. 4. Preparar e organizar artigos e documentos cientı́ficos para serem submetidos em eventos e periódicos da área. 5. Preparação e escrita da Dissertação de Mestrado a ser submetida para o PPGCC da FACOM-UFMS. Destaca-se que os créditos necessários em disciplinas, do PPGCC da FACOM-UFMS, serão concluı́dos até Julho/2011. 31 Referências Bibliográficas [1] Alfred V. Aho, Ravi Sethi, e Jeffrey D. Ullman. Compilers: principles, techniques, and tools. Addison-Wesley Longman Publishing Co., Inc., Boston, MA, USA, 1986. ISBN 0-201-10088-6. [2] Andrew W. Appel. Modern Compiler Implementation in Java. Cambridge University Press, 1998. ISBN 0-521-58388-8. [3] David A. Berson, Rajiv Gupta, e Mary Lou Soffa. URSA: A Unified ReSource Allocator for Registers and Functional Units in VLIW Architectures. In Proceedings of the IFIP WG10.3. Working Conference on Architectures and Compilation Techniques for Fine and Medium Grain Parallelism, páginas 243–254. North-Holland Publishing Co., Amsterdam, NL, 1993. ISBN 0-444-88464-5. [4] David A. Berson, Rajiv Gupta, e Mary Lou Soffa. Integrated Instruction Scheduling and Register Allocation Techniques. In Proceedings of the 11th International Workshop on Languages and Compilers for Parallel Computing, páginas 247–262. Springer-Verlag, London, UK, 1999. ISBN 3-540-66426-2. [5] Lakshmi N. Chakrapani, John Gyllenhaal, Wen mei W. Hwu, Scott A. Mahlke, Krishna V. Palem, e Rodric M. Rabbah. Trimaran: An Infrastructure for Research in Instruction-Level Parallelism. Lecture Notes in Computer Science, 3602:32–41, 2004. [6] James R. Goodman e Wei-Chung Hsu. Code scheduling and register allocation in large basic blocks. In Proceedings of the 2nd international conference on Supercomputing, páginas 442–452. ACM, New York, NY, USA, 1988. ISBN 0-89791-272-1. [7] John L. Henning. SPEC CPU2000: Measuring CPU Performance in the New Millennium. IEEE Computer , 33:28–35, July 2000. ISSN 0018-9162. [8] Chris Lattner e Vikram Adve. LLVM: A Compilation Framework for Lifelong Program Analysis & Transformation. In Proceedings of the 2004 International Symposium on Code Generation and Optimization. Palo Alto, CA, USA, Mar 2004. [9] Chunho Lee, Miodrag Potkanjak, e William H. Mangione-Smith. MediaBench: A Tool for Evaluating and Synthesizing Multimedia and Communications Systems. In Proceedings. of the 30th International Symposium on Microarchitecture, páginas 330– 335. IEEE Computer Society, Research Triangle Park, NC, USA, December 1997. 32 [10] Frank H. McMahon. The Livermore Fortran Kernels: A Computer Test of Numerical Performance Range. Relatório Técnico UCRL-53745, Lawrence Livermore National Laboratory, Livermore, CA, USA, December 1986. [11] Rajeev Motwani, Krishna V. Palem, Vivek Sarkar, e Salem Reyen. Combining Register Allocation and Instruction Scheduling. Relatório Técnico CS-TN-95-22, Stanford University, Stanford, CA, USA, 1995. [12] Cindy Norris e Lori L. Pollock. Register allocation sensitive region scheduling. In Proceedings of the IFIP WG10.3 Working Conference on Parallel Architectures and Compilation Techniques, páginas 1–10. IFIP Working Group on Algol, Manchester, UK, 1995. ISBN 0-89791-745-6. [13] Cindy Norris e Lori L. Pollock. Experiences with Cooperating Register Allocation and Instruction Scheduling. International Journal of Parallel Programming, 26(3):241– 283, 1998. ISSN 0885-7458. [14] Shlomit S. Pinter. Register Allocation with Instruction Scheduling: a New Approach. Journal of Programming Language, 4(1):21 – 38, 1996. [15] Massimiliano Poletto e Vivek Sarkar. Linear scan register allocation. ACM Transactions on Programming Languages and Systems, 21:895–913, September 1999. ISSN 0164-0925. [16] Ricardo R. Santos. 2D-VLIW: Uma Arquitetura de Processador Baseada na Geometria da Computação. Tese de Doutoramento, Unicamp, Jun 2007. [17] Ricardo R. Santos, Rodolfo Azevedo, e Guido Araujo. Instruction Scheduling Based on Subgraph Isomorphism for a High Performance Computer Processor. Journal of Universal Computer Science, 14(21):3465–3480, 2009. [18] Bernhard Scholz e Erik Eckstein. Register allocation for irregular architectures. SIGPLAN Not., 37(7):139–148, 2002. ISSN 0362-1340. [19] The LLVM Team. Documentation for the LLVM System. Website, May 2010. http: //llvm.org/releases/2.8/docs/index.html. 33