• Nenhum resultado encontrado

divergência de memória ). Nessa seção será exemplicado como foi realizada a junção de todas as ferramentas listadas.

Inicialmente é executada a análise de paralelismo, para o seu correto funciona- mento algumas otimizações são necessárias. Essas otimizações podem alterar muito o byte code gerado, dessa forma foi necessário a criação de uma função para armazenar quais laços encontrados são paralelos. A integridade do byte code gerado é essencial para o funcionamento da análise que infere o tamanho de arranjos. Após a coleta de todos os laços paralelos, foi criado uma análise responsável por inserir metadados7 nos

laços do byte code original, ou seja, sem as otimizações.

Ainda sobre o código otimizado são aplicadas as análises de divergência de con- trole e memória. As duas análises são executadas individualmente, nesta etapa são identicados quais laços possuem quantidade de divergência suciente para compro- meter o desempenho do código ao ser enviado para a GPU. Caso alguma das análises aponte para um laço divergente (80% ou mais das instruções e/ou acessos de memó- ria divergente), o mesmo também é marcado, através de metadados como divergente. Lembrando que esta marcação é realizada no byte code original.

Após a execução das análises citadas anteriormente, é executado a análise res- ponsável por inferir o tamanho dos arranjos. Se esta análise for executada no código otimizado a quantidade de acessos à memória analisável é reduzido. Devido a isto ela é aplicada no byte code original com poucas otimizações. Ela lê os metadados inseridos e é aplicada somente a laços paralelos e não divergentes. Na gura 3.11 é mostrado o uxo detalhado de funcionamento do anotador.

3.6 Avaliação da solução proposta

Nesse capítulo foi apresentado a solução proposta nessa dissertação. Foi feito uma pesquisa de ferramentas similares disponíveis e foi apresentado a solução preliminar que foi a motivação desse trabalho. O anotador é capaz através das análises descritas identicar quais os laços podem ser paralelizados, quais possivelmente terão um ganho de desempenho ao ser enviado para a GPU e a partir de um código em C/C++ criar um código anotado com diretivas OpenACC compilável em qualquer compilador de OpenACC.

Foi criado um script para junção de todas as análises onde é possível com apenas um comando executar o anotador proposto. O anotador se mostrou ecaz para o que

7Metadados do inglês Metadata representa uma informação opcional sobre uma instrução, ou

36 Capítulo 3. Solução Proposta Programa escrito em C ou C++ clang LLVM IR Análise de Paralelismo Análise de Divergências Divergência de Controle Divergência de Memória Inferência do tamanho de arranjos LLVM IR (metadados) Função para inserção de metadados PGCC Código para GPU Código para CPU

Figura 3.11: Fluxo detalhado de funcionamento do anotador

foi proposto. É totalmente estático, ou seja, todas as análises e alterações no código são realizadas durante a compilação do programa. É totalmente automático, como foi informado anteriormente com apenas um comando o programador(a) é capaz de executar a ferramenta.

Capítulo 4

Experimentos

O intuito dessa seção é demonstrar a efetividade do arcabouço de paralelização de código proposto nesta dissertação. Nessa seção é abordado sobre os testes realizados para vericação da ferramenta. Primeiro é detalhado o ambiente de testes utilizado: é descrito o hardware e cada benchmark. Na terceira seção são demonstrados através de grácos e tabelas os resultados obtidos com o anotador. Esta seção de resultados é separada em resultados estáticos e tempos de execução, visto que além dos speedups al- cançados outras benefícios podem ser retirados do trabalho apresentado. Para nalizar é realizado uma avaliação dos resultados obtidos.

4.1 Ambiente de Testes

Nesta seção é descrito o ambiente utilizado para a realização dos testes experimentais. Para o mesmo foi utilizado um servidor, disponível para os alunos do laboratório de compiladores, com as seguintes congurações:

Hardware: O hardware utilizado nesses experimentos é descrito a seguir:

CPU: processador Intel Xeon CPU E5-2620, 6 cores de 2.00GHz e 16 GB de RAM (DDR2). Sistema operacional: Linux Ubuntu 12.04 3.2.0;

GPU: placa gráca GeForce GTX 670, com 2 GB de memória RAM (CUDA Compute Capability 3.0).

4.2 Benchmarks

Nesta seção é descrito o software utilizado para a realização dos testes experimentais. São listados todos os softwares utilizados para o desenvolvimento da ferramenta e a

38 Capítulo 4. Experimentos execução de cada experimento.

Software : Os testes mostrados nesta seção foram realizados sobre diversos bench- marks, incluindo Parboil, Rodinia e OMPSpec. Dada a grande quantidade de ben- chmarks disponíveis, é mostrado números condensados para a maior parte deles; en- tretanto, é detalhado o tempo de execução para todos os benchmarks disponíveis em Polybench1. Os tempos de execução para os outros benchmarks são semelhantes. Poly-

Bench [Liu et al., 2013] é um pacote composto de programas projetados para avaliar o desenvolvimento do modelo poliédrico de paralelismo. Há dezenas de pequenos progra- mas neste conjunto de benchmarks. Escolheu-se trabalhar com os 15 programas usados por Gray et al [Grauer-Gray et al., 2012]. Para a geração de código paralelo foi utilizado o compilador PGCC (Portland Group C Compiler) 2versão 16.1. As ags usadas em li-

nha de comando foram: -fast -Mipa=fast,inline -Msmartalloc -acc. As análises estáticas foram implementadas em LLVM 3.73.

4.3 Resultados

Devido a ferramenta possuir várias análises que podem ser executadas de maneira independente sem perda de precisão, alguns resultados serão apresentados para a fer- ramenta como um todo e para algumas análises distintas. Nessa seção separamos em duas subseções: resultados estáticos e tempos de execução. Essa separação foi realizada para o melhor entendimento e aproveitamento dos resultados obtidos.

4.3.1 Resultados Estáticos

O objetivo da ferramenta é paralelizar o maior número de laços e enviar o mínimo de dados possíveis para a GPU. O anotador é executado de forma automática e suas análises são totalmente estáticas, ou seja, são realizadas em tempo de compilação. Com as análises disponíveis no anotador, é possível coletar vários dados que demonstram a eciência da mesma.

Para inserir diretivas OpenACC, o anotador deve ser capaz de analisar todos os acessos à memória presentes em um laço, derivando limites simbólicos para os mesmos. Isto se deve à necessidade de se mover os dados de entrada de um laço para a memória interna do hardware paralelo, como detalhado no capítulo 3. A tabela 4.1 mostra que

1https://sourceforge.net/projects/polybench/ 2https://www.pgroup.com/support/compile.htm 3http://llvm.org/

4.3. Resultados 39 foi possível analisar 98% dos acessos a memória presentes em cada um dos benchmarks do Polybench, o que maximiza o número de laços que será possível anotar com o arcabouço proposto. Nos benchmarks analisados é possível anotar 234 laços, ou seja, 95% dos laços presentes. Note que esta análise é totalmente automática. Em outras palavras, ela remove do programador o ônus de inserir diretivas de cópias de dados em todos os laços encontrados em Polybench.

Laços Memória Instruções

Nome Total Anotáveis Acessos Analisáveis Total Divergentes

2DConv 8 8 76 74 325 9 2mm 22 22 101 99 406 144 3DConv 12 12 147 145 431 14 3mm 28 28 139 137 496 178 atax 12 12 72 70 290 106 bicg 13 13 80 78 330 133 correlation 22 22 232 230 476 12 covariance 18 18 128 126 367 0 fdtd2d 23 23 151 149 504 95 gemm 14 14 69 67 301 108 gesummv 7 7 84 82 258 96 gramschmidt 16 16 144 142 370 163 mvt 11 11 84 82 327 128 syr2k 13 13 96 94 305 114 syrk_m 13 0 66 64 245 87 syrk 15 15 68 66 297 107 Total 247 234 1737 1705 5728 1494

Tabela 4.1: Dados estáticos coletados com o anotador automático.

Além da memória analisada e laços anotados, foi coletado a quantidade de instru- ções divergentes. Este dado é importante para a identicação de uma possível perda de desempenho ao se enviar um laço para a GPU, como explicado na seção 3. Ainda na tabela 4.1 pode-se ver que aproximadamente 26% das instruções encontradas são divergentes. Este número pode ser pequeno olhando o somatório geral, porém o anota- dor não verica a porcentagem de instruções divergentes presentes no programa e sim a quantidade presente em um laço. Se essa porcentagem for maior que 80% não será enviado o mesmo para a GPU.

Am de demonstrar a capacidade da análise proposta de lidar com programas reais, ela foi utilizada em vários outros benchmarks além de Polybench. Foram seleci- onadas algumas funções de benchmarks para avaliar aplicações paralelas. Essas estão listadas a seguir:

40 Capítulo 4. Experimentos OMPSpec [Saito et al., 2003] foi criado para mensurar a performance de aplica- ções baseadas em OpenMP para processamento de aplicações paralelas de memória compartilhada. O pacote inclui 14 aplicações cientícas e de engenharia, incluindo computações de dinâmica de uidos computacional, modelagem molecular e manipu- lação de imagens.

Parboil [Stratton et al., 2012] é uma coleção de aplicações para estudo de per- formance e computação da taxa de transferência entre arquiteturas heterogêneas. O pacote inclui aplicações comerciais e cientícas, incluindo processamento de imagem, simulação biomolecular, dinâmica dos uidos e astronomia.

Rodinia [Che et al., 2009] é um pacote de aplicações para arquiteturas heterogenias. Conforme Che et al. esse pacote de benchmarks foi criado para ajudar arquitetos a estudar plataformas emergentes como GPUs, Rodínia inclui aplicativos e kernels que têm como alvo a plataformas de vários núcleos de CPU e GPU. Os mesmos possuem aplicações de comunicação paralela, técnicas de sincronização e consumo de energia. Além dessas aplicações o Rodinia têm permitido a análise de limitações de largura de banda de memória e a consequente importância do layout de dados.

MGBench [do Couto Teixeira et al., 2015a] é uma coleção de 8 aplicações, criadas pela autora dessa dissertação em conjunto com outros alunos do laboratório de com- piladores, para testar um anotador baseado em proling de programas. Esse pacote inclui oito aplicações escritas em C com diretivas OpenACC.

4.3. Resultados 41 Foi coletado os dados e os sumarizado nos grácos das guras 4.1 e 4.2. Como pode ser visto no gráco da gura 4.1, a análise proposta é capaz de anotar aproxima- damente 62% dos laços presentes em todos os benchmarks analisados4. Essa quantidade

é realmente grande visto que a análise é totalmente estática e não depende dos dados de entrada.

Além da quantidade de laços analisáveis a inferência de tamanhos de arranjos é capaz de encontrar limites para mais de 80% dos acessos a memória presentes nas funções, como pode ser visto no gráco da gura 4.2. Diz-se que uma operação de acesso à memória possui limites quando seu menor e maior osets válidos são conhecidos em tempo de compilação. Note que esse valor é maior que a proporção de laços que consegue-se anotar. Para anotar um laço é necessário que cada um dentre seus acessos seja analisável. Assim, basta um acesso com limites desconhecidos dentro de um laço para que que a análise deixe de anotá-lo.

Figura 4.2: Gráco: Porcentagem de memória analisável.

4.3.2 Tempo de Execução

A gura 4.2 mostra resultados para o tempo de execução dos benchmarks usados, com e sem as diretivas de paralelização de código. Cada benchmark possui entradas de cinco tamanhos diferentes - a Tabela 4.2 mostra resultados para cada uma delas. Nota-se que há grande variação de tempo de execução entre as versões CPU e GPU de cada benchmark. Além disso, alguns dos benchmarks executaram durante um tempo

4O benchmark Rodinia teve um comportamento discrepante devido a um ajuste feito na análise

para não anotar laços que contenham arranjos com duas ou mais dimensões, devido à uma limitação do compilador utilizado, mesmo sendo capaz de analisar os acessos à memória presentes.

42 Capítulo 4. Experimentos

Name Device MINI SMALL MEDIUM LARGE X LARGE

SYRK GPUCPU 0.0000.623 0.0000.620 0.0060.665 0.4761.622 5.0635.358 SYRK_M GPUCPU 0.0010.001 0.0060.009 0.0450.051 0.2930.362 2.8852.888 GEMM GPUCPU 0.0000.561 0.0000.624 0.0360.629 10.8910.772 141.5641.390 CORR GPUCPU 0.0020.727 0.0131.328 0.1856.365 47.4075.843 490.83575.948 COVAR GPUCPU 0.0020.716 0.0131.331 0.1806.386 47.5025.841 489.86874.172 3MM GPUCPU 0.0000.621 0.0090.622 0.9690.742 32.5981.078 293.5132.424 2MM GPUCPU 0.0000.623 0.0060.612 0.6700.698 21.6650.923 194.5121.773 ATAX GPUCPU 0.0000.622 0.0000.629 0.0010.629 0.0140.754 0.0481.145 GRAMSCHM GPUCPU 0.0000.683 0.0071.076 0.0693.807 27.3990.991 251.83222.426 MVT GPUCPU 0.0000.621 0.0000.624 0.0010.610 0.0590.655 0.2680.752 BICG GPUCPU 0.0000.623 0.0000.612 0.0010.629 0.0110.739 0.0351.139 FDTD-2D GPUCPU 0.0000.620 0.0020.629 0.0480.654 4.5231.407 37.8314.200 2DCONV GPUCPU 0.0010.612 0.0020.612 0.0200.673 0.0570.790 0.2741.529 GESUMMV GPUCPU 0.0000.628 0.0000.623 0.0010.630 0.0030.658 0.0130.815 3DCONV GPUCPU 0.0000.619 0.0010.624 0.0100.631 0.0720.788 0.0101.768 Tabela 4.2: Tempo de execução de programas paralelizados automaticamente, com entradas de cinco diferentes tamanhos.

muito curto, menos de um segundo, mesmo com sua maior entrada. Nesses casos, houve grande variação de tempo relativo entre versões paralelas e sequenciais. Assim, para evitar distorções, a discussão que acontece no restante desta seção fará referência somente aos benchmarks que executaram por mais de 1 segundo.

A Figura 4.3 compara esses resultados. Novamente: mostram-se speedups relati- vos somente entre benchmarks que executaram por mais de um segundo. Pela gura, nota-se que três dos benchmarks (GEMM, 2MM e 3MM) experimentaram grandes me- lhorias de tempo de execução na GPU. Houve, contudo, pelo menos dois benchmarks

4.3. Resultados 43 que experimentaram lentidão. A razão por trás dessa lentidão é reúso de dados: alguns desses benchmarks possuem complexidade linear. Contudo, o tempo de copiar os dados para a memória da GPU é também linear. Assim, o tempo de cópia termina por remo- ver possíveis ganhos que se obtém com o maior poder computacional da GPU. Técnicas de análise de complexidade computacional, como os trabalhos feitos por Gawlitza et al. [Gawlitza & Monniaux, 2012] e Gulavani et al. [Gulavani & Gulwani, 2008] podem ser usados para mitigar esse problema. Nesse caso, envia-se para a GPU somente tra- balho super-linear. Tal abordagem foi usada com sucesso por Etino [do Couto Teixeira et al., 2015a], porém, naquele caso, usou-se um perlador para inferir a complexidade assimptótica dos algoritmos paralelizáveis.

-20 0 20 40 60 80 100 120 140 GRAMS CHM COVAR CORR 2DCONV SYRK SYRK _M FDTD-2 D GEMM 2MM 3MM

Figura 4.3: Gráco: Speedup relativo entre os diferentes programas de Polybench que executaram por mais de um segundo com as maiores entradas disponíveis.

O maior speedup observado dentre tais programas, como pode ser visto no gráco da gura 4.3 e na tabela 4.2, foi de 121x na execução da multiplicação de matrizes tridimensionais 3MM (293.513secs na CPU vs 2.424secs na GPU). Para detalhar melhor o resultado foi criado o gráco 4.4. Nele é possível vericar a evolução do tempo de execução5do algoritmo. Para matrizes com dimensão acima 512 já se começa a ter um

ganho de desempenho ao ser enviado para GPU. Este resultado já era esperado, visto que conforme Etino já havia demonstrado multiplicação de matrizes tem um ganho ao ser paralelizada, pois o custo computacional do algoritmo é superior ao custo de envio dos dados.

O maior slowdown observado dentre os programas testados, foi de 11x em GRAMSCHM (22.426secs na CPU vs 266 251.832secs na GPU), como pode ser visto no gráco da gura 4.3 e na tabela 4.2. Para detalhar melhor o resultado foi criado

44 Capítulo 4. Experimentos

Figura 4.4: Gráco: Tempo de execução da multiplicação de matrizes tridimensionais 3MM. Eixo x: Tempo de Execução; Eixo y: Tamanho da entrada.

o gráco5 4.5. A aplicação Gramschm ou decomposição Gram-Schmidt é um método

para normalização ortogonal de um conjunto de vetores em um espaço de produto interno. No resultado apresentado mesmo para valores grandes não se alcançou um ganho de desempenho na GPU. Este benchmark possui divergência de memória nas funções marcadas como paralelas. Porém a análise proposta não foi capaz de detec- tar todos os acessos uncoalesced, devido a forma de acesso ao vetor. Contudo após a modicação do benchmark a análise detectou na função principal 7 dos 10 acessos da função principal são divergentes.

Figura 4.5: Gráco: Tempo de execução da decomposição Gram-Schimidt Gramschm. Eixo x: Tempo de Execução; Eixo y: Tamanho da entrada.

Impacto de divergências: Como foi explicado anteriormente divergência de memória e de controle tem uma inuência negativa no desempenho do programa ao ser executado

4.3. Resultados 45 na GPU. Nos testes feitos com os benchmarks do Polybench pôde-se vericar que em alguns casos não se obtiveram o resultado esperado ao ser enviado o laço para execução na GPU. A Figura 4.5 exemplica o impacto da divergência de memória em um programa.

Para determinar a porcentagem de divergência que é capaz de reduzir o desem- penho do programa ao ser enviado para a GPU, foram executados testes em alguns benchmarks. A divergência de controle e a divergência de memória foram analisadas separadamente. Para se determinar este impacto foram ignoradas as demais análises e para identicação do laço a ser paralelizado, foi considerado o laço mais externo. A seguir são apresentados dois testes realizados. No primeiro teste foi considerado a divergência de controle e no segundo a divergência de memória.

1. Na Figura 4.6, é mostrado o benchmark Convolução 3D disponível no pacote do Polybench. Utilizando a ferramenta não foi possível identicar laços na função primcipal como paralelos. Para executar os testes e garantir a conabilidade dos mesmos, foi realizada a análise manual e como os laços não possuem dependências os mesmos foram anotados como paralelos. Nesses laços foi executada a análise de divergências e a mesma foi capaz de identicar 85% de instruções divergentes. Como pode ser visto no gráco mesmo para matrizes com tamanhos acima de 1024 o tempo de execução na GPU é muito superior ao tempo de execução na CPU. Logo, conclui-se que divergiencia de controle possui um impac†o negativo no tempo de execução do programa ao ser enviado para a GPU 6.

3.00E+02 2.00E+02 1.00E+02 0.00E+00 -1.00E+02 32 128 512 1024 2048 GPU CPU

Figura 4.6: Gráco: Tempo de execução da Convolução 3D 3DConv. Eixo x: Tamanho da entrada; Eixo y: Tempo de execução.

6Foram realizados outros testes para denir que a porcentagem de 80% seria a considerada limite

46 Capítulo 4. Experimentos 2. Na Figura 4.7, é mostrado o benchmark K-Nearest, ou K vizinhos mais próxi- mos, o qual está disponível no pacote MGBench. No arquivo original devido a forma de acesso do vetor não é possível identicar que o acesso é não agrupado (uncoalesced). Devido a isto foi alterado a forma de acesso aos arranjos utiliza- dos e a análise proposta foi capaz de identicar 70% de divergência na função principal. No gráco apresentado são mostrados três tempos de execução: (i) tempo de execução na CPU; (ii) tempo de execução na GPU; e (iii) o tempo de excução na GPU do programa modicado. Nesse último caso foi alterado a ordem dos laços dessa forma a análise de divergências retornou apenas 30% de acessos divergentes e como pode ser visto reduziu consideravelmente o tempo de execução.

X

Figura 4.7: Gráco: Tempo de execução do algoritmo que encontra os k vizinhos mais próximos K-Nearest Eixo x: Tamanho da entrada; Eixo y: Tempo de execução.

Documentos relacionados