Вы находитесь на странице: 1из 72

Tcnicas de Otimizao de Cdigo para Placas de Processamento Grco

Fernando Magno Quinto Pereira

Abstract The low cost and the increasing programmability of graphics processing units, popularly know as GPUs, is contributing to bring parallel programs closer to the reality of application developers. Presently, we can have access, for the price of an ordinary desktop, to hardware boosting hundreds of processing elements. This brave new world brings, alongside the many possibilities, also difculties and challenges. Perhaps, for the rst time since the popularization of computers, it makes sense to the ordinary programmer to open the compiler books on the nal chapters, which talk about very unusual concepts, such as polytopes, iteration space and FourierMotskin transformations. This material covers, in a very condensed way, some good practices and code optimization techniques that can be used, either by the compiler or by the developers themselves, to produce efcient code to graphics processing units. We will discuss a little bit of what are GPUs, which applications should target them, and which transformations we can apply on GPU programs to take more benet from the SIMD execution model and the complex memory hiearchy that characterizes this hardware.

Resumo O baixo custo e a crescente programabilidade das placas grcas, normalmente conhecidas como GPUs, tm contribudo sobremaneira para trazer o processamento paralelo mais prximo da realidade de programadores comuns. Atualmente pode-se ter acesso, pelo preo de um computador popular, a um hardware que prov algumas centenas de processadores. Este admirvel mundo novo, junto com as muitas possibilidades, traz tambm diculdades e desaos. Talvez, pela

primeira vez desde a popularizao dos computadores, faa sentido a programadores comuns abrir os livros de compiladores nos captulos nais, onde conceitos at agora obscuros, como politopos e transformaes de Fourier-Motskin vm aguardando um momento de serem teis fora do crculo dos especialistas em computao paralela. Este material, que explica de forma condensada alguns destes conceitos, tem o propsito de disseminar um pouco da cincia que suporta s placas de processamento grco. Muitas das tcnicas descritas neste texto podem ser usadas tanto em compiladores, quanto por programadores que buscam melhorar o desempenho de suas aplicaes paralelas. Embora algumas das otimizaes descritas sejam especcas para o ambiente de desenvolvimento CUDA, vrios mtodos abordados neste texto podem ser aplicados sobre outras arquiteturas paralelas que seguem o modelo de execuo SIMD.

1.1. A Ascenso das Placas Grcas Programveis


O objetivo desta seo descrever um pouco do hardware e do ambiente de desenvolvimento que caracterizam as placas grcas. A Roda da Reencarnao: Os computadores de propsito geral que compramos nas lojas normalmente vem equipados com uma placa de processamento grco. este hardware super especializado que representa na tela do computador as janelas, botes e menus dos aplicativos mais comuns e tambm as belas imagens 3D que podemos ver nos video-games. Algumas destas placas so programveis, isto , ns, usurios podemos criar programas que sero executados neste hardware. Isto, claro, faz perfeito sentido, anal, pode-se imaginar que uma placa de processamento grco tem um enorme poder computacional, mesmo porque as imagens que vemos brotar nos monitores de nossos computadores so cada vez mais exuberantes. Mas nem sempre foi assim. At meados da dcada de 90, pouco poder-se-ia dizer sobre hardware grco. Havia ento a noo de um vetor de imagens grcas", uma possvel traduo para video graphics array, or VGA. VGAs so exatamente isto: uma rea contgua de dados que podem ser lidos e representados em uma tela. A CPU, claro, era a responsvel por preencher esta memria. E tal preenchimento deveria acontecer vrias vezes a cada segundo. Inicialmente esta carga sobre a CPU no era um terrvel problema. Os aplicativos grcos de 2

ento eram muito simples, normalmente bidimensionais, contendo poucas cores e resoluo muito baixa. Esta poca saudosa, contudo, no durou muito. Novas aplicaes foram surgindo. Jogos digitais e aplicaes de CAD passaram a exigir processamento de imagens tridimensionais; usurios tornavam-se sempre mais exigentes. Esta contnua presso sobre os aplicativos grcos fez com que engenheiros de hardware movessem algumas das atividades de processamento para o que seriam as primeiras placas grcas. Algumas atividades de processamento grco so muito intensivas. Rasterizao, isto , a converso de vetores, pontos e outros elementos geomtricos em pixels, uma destas atividades. Rasterizadores foram os primeiros processadores grcos implementados diretamente em hardware. Shaders, ou processadores de sombras, vieram logo a seguir. O sombreamento necessrio para que usurios tenham a impresso de profundidade ao visualizarem imagens tridimensionais em uma tela bidimensional. Shaders, contudo, possuem uma diferena terica muito importante, quando comparados com rasterizadores: h muitos, muitos algoritmos diferentes de sombreamento, ao passo que o processo de rasterizao no possui tantas variaes. No seria vivel, em termos de custo nanceiro, implementar todos estes algoritmos de processamento em hardware. A soluo encontrada pelos engenheiros da poca foi criar sombreadores congurveis. Isto quase um hardware programvel. Existem, inclusive, bibliotecas para a programao destes sombreadores. OpenGL o exemplo mais notrio, embora no seja o nico. O salto de shaders congurveis para processadores de propsito geral no foi exatamente simples, contudo foi o resultado de uma busca legtima e bastante natural pelo melhor aproveitamento de recursos computacionais. Um hardware congurvel pede um conjunto de instrues. Neste caso, porque no adicionar a este processador super especializado a capacidade de realizar operaes aritmticas e lgicas normalmente realizadas na CPU? E se estamos neste nvel, pouco custa que queiramos tambm desvios condicionais. Temos ento, um processador Turing completo, que, embora projetado para lidar com grcos, 3

pode realizar outras tarefas, no necessariamente relacionadas renderizao de imagens em uma tela. Chegamos aqui s GPUs, do ingls graphics processing units, ou unidades de processamento grcos. As tarefas de processamento grco foram migrando, pouco a pouco, da CPU em direo placa grca, que acabou tornando-se um hardware de propsito geral. O curioso desta estria que, em nossa incessante busca por desempenho, tenhamos adicionado novos mdulos, especializados e no programveis, s GPUs. Os mais recentes modelos, por exemplo, possuem funes de rasterizao implementadas em hardware, como era o caso das antigas CPUs. Aparentemente a roda da reencarnao deu uma volta e meia na histria das placas grcas. Arquiteturas Heterogneas. O conjunto CPU-GPU forma uma arquitetura de computadores heterognea bem diferente do modelo CPU-VGA to comum no passado. A gura 1.1 compara estas duas arquiteturas. Enquanto o vetor de processamento grco no possui qualquer componente programvel, a GPU um processador de propsito geral. De fato, temos dois processadores que trabalham juntos, ainda que em essncia eles sejam substancialmente diferentes. Arquiteturas heterogneas no so um modismo novo em cincia da computao. Vrios computadores massiamente paralelos utilizam aceleradores para determinados tipos de tarefa. Entretanto, tais computadores so muito caros e portanto, bem menos acessveis que as nossas GPUs. A GPU possui mais unidades de processamento que a CPU muito mais, na verdade. Porm, os processadores presentes na GPU so mais simples e geralmente mais lentos que os processadores das CPUs. Outra diferena entre estes dois mundos se d em termos de entrada e sada de dados. As GPUs possuem mecanismos de comunicao muito simples. Normalmente elas se comunicam somente com a CPU, via reas de memria pr-denidas. Desta forma, aplicaes muito iterativas, tais como editores de texto, no se beneciariam muito do poder de processamento das GPUs. Por outro lado, aplicaes em que exista muito paralelismo de dados brilham neste ambiente. Seria possvel pensar que a GPU um processador 4

CPU

Memria
PCI Bus

Display

CPU North Bridge Memria DDR2

GPU
Display VGA

Controlador VGA

Memria grfica

Memria da GPU

Figura 1.1. Um hardware grco composto de CPU e VGA comparado com um arranjo formado por CPU e GPU.

escravo" da CPU, pois ao nvel da CPU que decide-se quais tarefas sero executadas na placa grca. Como veremos mais adiante, o desenvolvedor cria suas aplicaes programando explicitamente o envio de dados para a GPU e a leitura dos dados que ela produz. Preparar, Apontar, Fogo! A GPU, em seu ncleo, implementa um modelo computacional chamado SIMD, do ingls Single Instruction Multiple Data, ou instruo nica, dados mltiplos". No modelo SIMD temos vrios processadores, mais somente uma unidade de processamento. Ou seja, uma mquina SIMD faz muitas coisas em paralelo, mas sempre as mesmas coisas! E qual o benefcio disto? O benefcio reside no paralelismo de dados, o qual ilustraremos com uma analogia, no menos mrbida que educativa. Em um peloto de fuzilamento temos uma parelha de soldados, todos armados com fuzis e um capito, que lhos comanda via uma sequncia de instrues bem conhecidas: preparar, apontar, fogo". Cada uma destas instrues possui o mesmo efeito sobre os soldados, porm cada soldado manipula uma arma diferente. O capito consegue, portanto, ordenar o disparo de quatro tiros em paralelo. Nesta metfora, o capito corresponde unidade de controle e cada soldado representa um processador. As armas so os dados. A GPU um magnco exemplo de hardware paralelo. Parafraseando o Manual de Boas Prticas da Nvidia [5]: As GPUs 5

Controle

ALU ALU Cache DRAM

ALU ALU

DRAM

CPU

GPU

Figura 1.2. Uma comparao entre a GPU e a CPU, conforme gura obtida do manual de programao CUDA [4].

da Nvidia suportam at 768 threads ativas por multiprocessador; algumas GPUs elevando este nmero a 1.024. Em dispositivos que possuem 30 multiprocessadores, tais como a GeForce GTX 280, isto faz com que 30,000 threads possam estar ativas simultaneamente". Este hardware poderosssimo tem permitido que algumas aplicaes pudessem executar at 100 vezes mais rapidamente que suas rivais restritas CPU [22]. A gura 1.2 mostra uma tpica representao da GPU, comparada com a CPU. A GPU separa uma poro muito grande de sua rea til para tarefas de processamento, ao passo que a CPU usa bastante desta rea para implementar a sua memria de cache. A GPU necessita de todo este paralelismo porque aplicaes grcas tendem a ser embaraosamente paralelas. Isto quer dizer que tais aplicaes so formadas por tarefas em geral independentes umas das outras. Por exemplo, podemos escurecer ou clarear os pixels em uma imagem atendo-nos a cada pixel em separado. Dada a natureza minimalista destas tarefas, a GPU pode se dar ao luxo de possuir ncleos de processamento simples, porm estes devem existir em grande nmero. Alguns autores, principalmente aqueles mais ligados NVIDIA, costumam usar a expresso STMD (Single Thread Multiple Data) para caracterizar as GPUs. Este contudo, um termo relativamente novo e ainda pouco utilizado. Muitos outros autores 6

referem-se s GPUs como arquiteturas MSIMD, isto , mquinas formadas por mltiplas unidades SIMD. Em outras palavras, a GPU possui vrios processadores, os quais executam instrues independentemente uns dos outros. Cada um destes processadores , contudo, uma mquina SIMD. Notem que ao denominar as placas grcas de mquinas SIMD no estamos as comparando com as extenses vetoriais normalmente utilizadas em processadores de propsito geral, como as instrues SSE, SSE2 e MMX presentes em processadores Intel. Alguns autores chamam tais extenses de SIMD, uma vez que as instrues vetoriais permitem a realizao da mesma operao sobre dados diferentes. Este modelo, contudo, possui uma diferena fundamental quando comparado ao modelo adotado em placas grcas: a ausncia de execuo condicional. A GPU pode desligar" algumas das threads que integram uma unidade SIMD. Tal capacidade necessria para modelar desvios condicionais. A m de reforar tal conceito com uma analogia, podemos voltar ao nosso exemplo do peloto de fuzilamento. Em alguns crculos militares tradio que um dos soldados do peloto receba uma bala de festim, incapaz de causar qualquer ferimento. Tal engodo realizado com o propsito de diluir a culpa da execuo entre os atiradores. Assim, ao efetuar os disparos, um dos soldados, embora execute a mesma ao que os outros, no produzir qualquer efeito. As placas grcas utilizam um mecanismo similar para desativar threads, quando necessrio: estas podem continuar executando a mesma ao que as demais threads, porm nenhum resultado de computao escrito de volta na memria. Tal mecanismo, quando implementado em nvel de software, muitas vezes chamado predicao. Uma linguagem de programao heterognia. natural que um hardware heterogneo venha pedir por uma linguagem de programao heterognea, em que o programador possa especicar quais partes do programa devem executar sobre a CPU e quais partes devem executar sobre a GPU. Existem linguagens assim. Entretanto, vrias destas linguagens esto bastante voltadas para o domnio das aplicaes grcas e muitas delas so, na verdade, APIs construdas sobre linguagens j conhe7

cidas. Esta categoria inclui Cg (C para grcos) e HLSL (High Level Shading Language). Cg, por exemplo, consiste em uma coleo de funes que podem ser invocadas via uma sintaxe que difere muito pouco da sintaxe da linguagem C. Existem, contudo, linguagens muito mais gerais os exemplos mais notrios sendo OpenCL e C para CUDA. Neste material ns utilizaremos C para CUDA para ilustrar a programao de GPUs. Esta linguagem bastante diferente, em termos de semntica, de outras linguagens, tais como C, C++ ou Java, que tambm permitem o desenvolvimento de aplicaes paralelas. C, C++, Java e muitas outras linguagens de programao foram projetas para atender as necessidades do hardware de propsito geral. Na verdade, estas linguagens tendem a abstrair o hardware, tornando-o quase invisvel para o desenvolvedor de aplicaes. Este pode, assim, concentrar-se em detalhes algortmicos dos problemas de programao, no se preocupando com mincias da arquitetura de computadores subjacente. Entretanto, no uma tarefa simples abstrair a heterogeneidade de CUDA. Isto pode, obviamente, ser feito em algum futuro no muito distante; porm, tal ainda no foi feito em C para CUDA. Enquanto desenvolvendo aplicaes para este ambiente, o programador decide quais partes de sua aplicao iro executar na CPU e quais sero enviadas para a GPU. Alm disto, C para CUDA precisa prover ao programador mecanismos com que este possa usufruir do grande poder computacional presente em uma placa grca. Com tal intuito, a linguagem disponibiliza trs abstraes principais: uma hierarquia de threads; memrias compartilhadas; barreiras de sincronizao. Um programa CUDA contm um corpo principal, que ser executado sobre a CPU, e vrias funes especiais, normalmente chamadas kernels, que sero executadas na GPU. Ao observar um kernel, temos a impresso de que seu texto tratase de um programa sequencial. Entretanto, este mesmo cdigo ser simultaneamente executado por centenas de threads. O 8

ambiente de execuo CUDA organiza estas threads em uma hierarquia formada por blocos e grades: Threads pertencentes ao mesmo bloco seguem o modelo SPMD (do ingls Single Program Multiple Data) de execuo. Este o modelo de paralelismo mais conhecido: temos um programa que ser executado por mltiplos processadores, no necessariamente em passo nico, como dar-se-ia em arquiteturas SIMD. Tais threads podem comunicar-se via memrias compartilhas e podem sincronizarse via barreiras. Threads pertencentes mesma grade executam o mesmo programa de forma independente, podendo comunicar-se somente via memria global. Esta hierarquia existe para que o mesmo programa escrito em C para CUDA possa ser executados em placas grcas de diferentes capacidades. Em geral uma grade de threads executada de cada vez, porm, como o hardware pode no possuir processadores sucientes para cada thread, a grade particionada em blocos e a uma quantidade maximal de blocos dada a chance de realizar trabalho. C para CUDA prov sintaxe para a identicao de threads. Cada thread criada neste ambiente de execuo possui um nome, que dado por um vetor tridimensional. Embora threads possam ser identicadas por trs inteiros, muitas vezes utilizaremos somente um ou dois deles. Na verdade, a forma como o programador organiza as threads em um programa depende muito dos dados que este programa manipula. Normalmente aplicaes que lidam com arranjos unidimensionais de dados iro utilizar somente o primeiro identicador de cada thread. Aplicaes que lidam com matrizes iro utilizar dois identicadores, e aplicaes que lidam com volumes iro utilizar todos os trs identicadores. Ilustraremos estas noes mostrando como re-escrever o programa C na gura 1.3 em C para CUDA. Dados dois vetores x e y, este programa computa o produto vetorial y = x + y, para uma constante qualquer. O programa correspondente em C para CUDA mostrado na gura 1.4. 9

void s a x p y _ s e r i a l ( i n t n , f l o a t alpha , f l o a t x , f l o a t y ) { f o r ( i n t i = 0 ; i < n ; i ++) y [ i ] = alphax [ i ] + y [ i ] ; } / / Invoke t h e s e r i a l f u n c t i o n : saxpy_serial (n , 2.0 , x , y ) ;

Figura 1.3. Um programa em C que calcula o produto vetorial y = Ax + y. Este exemplo foi criado por Nickolls e Kirk [29].

__global__ void s a x p y _ p a r a l l e l ( i n t n , f l o a t alpha , f l o a t x , f l o a t y ) { i n t i = b l o c k I d x . x blockDim . x + t h r e a d I d x . x ; i f ( i < n ) y [ i ] = alpha x [ i ] + y [ i ] ; } / / Invoke t h e p a r a l l e l k e r n e l : i n t n b l o c k s = ( n + 255) / 256; s a x p y _ p a r a l l e l <<<nblocks , 256>>>(n , 2 . 0 , x , y ) ;

Figura 1.4. Um programa em C para CUDA que calcula o produto vetorial y = Ax + y. Exemplo retirado de Nickolls e Kirk [29].

Traduzindo programas sequenciais escritos em C para programas paralelos escritos em C para CUDA. Muitos programas CUDA so traduo direta de programas originalmente escritos para mquinas monoprocessadas. Tal traduo pode, at certo ponto, ser feita mecanicamente, por um compilador paralelizante, segundo tcnicas j bem conhecidas [23]. Tais mtodos utilizam pesadamente conceitos de algebra linear, tais como espaos vetoriais e transformaes ans. Ns evitaremos esta bela teoria, porm iremos manter, nesta dicusso, o conceito de espao de iteraes, o qual, aliado a uma boa dose de intuio, ser nossa principal ferramenta de traduo. O espao de iterao de um loop o conjunto formado por cada possvel combinao dos ndices deste loop. Por exemplo, o programa C da gura 1.5 adiciona duas matrizes de mesma dimenso, 10

void matSum ( f l o a t s , f l o a t a , f l o a t b , unsigned s i d e ) { int i , j ; f o r ( i = 0 ; i < s i d e ; i ++) { f o r ( j = 0 ; j < s i d e ; j ++) { s[ i ][ j ] = a[ i ][ j ] + b[ i ][ j ]; } } }

Figura 1.5. Programa C que calcula a soma matricial S = A + B.

(10, 10)

(0, 0)

Figura 1.6. Espao de iteraes para o programa da gura 1.5.

produzindo uma terceira matriz com o resultado desta soma. Supondo side = 10, ento o espao de iteraes desta aplicao dado pela gura 1.6. Esta gura descreve o cenrio ideal em termos de paralelismo: no existe dependncias temporais entre quaisquer pares de pontos deste espao de iteraes. Isto , em um modelo PRAM, poder-se-ia delegar cada soma Ai, j + Bi, j para um processador, de forma que toda a adio matricial fosse feita em tempo constante. Algo bastante si11

_ _ g l o b a l _ _ void matSumKernel ( f l o a t S , f l o a t A , f l o a t B , i n t s i d e ) { i n t i = b l o c k I d x . yblockDim . y + t h r e a d I d x . y ; i n t j = b l o c k I d x . xblockDim . x + t h r e a d I d x . x ; i n t i j = i s i d e + j ; i f ( i j < s i d es i d e ) { S[ i j ] = A[ i j ] + B[ i j ] ; } }

Figura 1.7. Kernel CUDA equivalente ao programa C da gura 1.5

milar pode ser feito em CUDA. A gura 1.7 mostra o kernel que responsvel por realizar a soma matricial. Uma constante em nossos exemplos ser a forma de representar matrizes: estas sero sempre linearizadas. Conforme car mais claro adiante, a transmisso de dados entre CPU e GPU por demais lenta. Por isto, prefervel enviar estes dados em uma nica viagem, em vez de termos de transmitir para a GPU as muitas colunas que compem uma matriz alocada dinamicamente. O programa da gura 1.5 descreve um problema embaraosamente paralelo, uma vez que no h dependncias entre as iteraes do lao. Este tipo de aplicao mais comum que a princpio poderamos imaginar; porm, existem tambm muitas aplicaes em que as dependncias temporais esto presentes. Consideremos, por exemplo, o programa da gura 1.8, cujo espao de iteraes est representado na gura 1.9. As setas mostram a direo das dependncias entre as iteraes. Este programa no to paralelo quanto aquele na gura 1.5, uma vez que a iterao (i, j) depende da iterao (i, j 1). No modelo PRAM, poderamos atribuir uma linha deste espao para cada processador; desta forma, convertendo um algoritmo de complexidade quadrtica para um algoritmo equivalente de complexidade linear. Isto sem que aumentssemos a carga de trabalho total necessria para a soluo do problema; isto , cada algoritmo totaliza uma quantidade quadrtica de somas. A gura 1.10 nos d o programa CUDA que corresponde 12

void depSum ( f l o a t s , unsigned i n t s i d e ) { int i , j ; f o r ( i = 0 ; i < s i d e ; i ++) { f o r ( j = 1 ; j < s i d e ; j ++) { s [ i ] [ j ] = s [ i ] [ j ] s [ i ] [ j 1]; } } }

Figura 1.8. Um programa em C que ilustra dependncias temporais.

(10, 10)

(0, 0)

Figura 1.9. Espao de iteraes para o programa da gura 1.8.

ao programa da gura 1.8. Conforme discutimos anteriormente, este programa possui complexidade linear. Existe uma bonita intuio geomtrica que explica esta complexidade. Podemos imaginar que o espao de iteraes descreve um dgrafo, em que cada ponto um vrtice e as setas representam arestas, isto , existe uma aresta ligando v1 a v2 se e somente se, v1 depende de v2 . Podemos paralelizar o problema descrito por este espao de iteraes assinalando um processador para cada com13

_ _ g l o b a l _ _ void depSumKernel ( f l o a t S , i n t s i d e ) { i n t i = b l o c k I d x . xblockDim . x + t h r e a d I d x . x ; i f ( i < side ) { f o r ( i n t j = 1 ; j < s i d e ; j ++) { int i j = i side + j ; S[ i j ] = S[ i j ] S[ i j 1]; } } }

Figura 1.10. Programa CUDA equivalente ao programa visto na gura 1.8.

void digSum ( f l o a t s , unsigned i n t s i d e ) { int i , j ; f o r ( i = 1 ; i < s i d e ; i ++) { f o r ( j = 1 ; j < s i d e ; j ++) { s [ i ] [ j ] = s [ i ] [ j ] s [ i 1][ j 1]; } } }

Figura 1.11. Programa C com cadeias de dependncias temporais de diferentes tamanhos.

ponente conexo deste grafo. Em nosso exemplo, cada um destes componentes uma linha e nossa abordagem demandar tantos processadores quanto existam linhas em nosso grafo. Nos exemplos que vimos at aqui, todos os processadores usados nas solues paralelas realizam a mesma quantidade de trabalho. Isto quer dizer que em um mundo perfeitamente SIMD, todos os processadores terminariam ao mesmo tempo. Existem, contudo, muitos problemas em que tal regularidade no est presente. Um exemplo dado pelo programa C da gura 1.11, cujo espao de iteraes dado na gura 1.12. Uma converso trivial mecnica do programa da gura 1.11 para CUDA leva ao programa da gura 1.13. Este programa possui um espao de iteraes mais largo que o programa original. 14

(10, 10)

(0, 0)

Figura 1.12. Espao de iteraes para o programa da gura 1.11.

_ _ g l o b a l _ _ void digSumKernel ( f l o a t s , i n t s i d e ) { i n t t x = b l o c k I d x . xblockDim . x + t h r e a d I d x . x ; f o r ( i n t i t =1; i t < s i d e ; i t ++) { int i = i t ; i n t j = t x ( side 1) + i t ; i f ( j >= 1 && j < s i d e ) { int i j = j + i side ; i n t i j p = j 1 + ( i 1) side ; s[ ij ] = s[ ij ] s[ ijp ]; } } }

Figura 1.13. Programa CUDA equivalente ao programa da gura 1.5

A rea cinza na gura representa os pontos em que trabalho til produzido pelo kernel. O trabalho produzido fora desta regio no ser utilizado para criar a resposta pedida pelo problema. O comando condicional em nosso exemplo garante que estes resultados inteis" no sero escritos em memria. Conforme veremos posteriormente, comandos condicionais possuem uma 15

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19

treadIdx.x

Figura 1.14. Espao de iteraes para o programa da gura 1.13.

semntica bastante peculiar quando analisados sob a lupa de mquinas SIMD: quando um processador no possui trabalho til para realizar, ento ele dorme", at que as threads possam ser novamente postas em sincronia. O Assembly de CUDA. Um programa escrito em uma linguagem de alto nvel normalmente traduzido para cdigo binrio a m de ser executado. Em CUDA isto no diferente. Existem compiladores que traduzem CUDA para instrues Direct3D, por exemplo. Neste material ns utilizaremos uma linguagem assembly conhecida por PTX, do ingls Parallel Thread Execution. O programa PTX que corresponde ao nosso primeiro kernel, visto na gura 1.4, dado na gura 1.15. PTX, sendo uma linguagem assembly, manipula tipos de dados muito simples, como inteiros de 8, 16, 32 e 64 bits e nmeros de ponto utuante de 16, 32 e 64 bits. PTX descreve um tpico cdigo de trs endereos, contendo instrues aritmticas (add, sub, mul, etc), lgicas (eq, leq, etc), de uxo de controle (bra, ret, call, etc) e de transmisso de dados (load, store). PTX tambm contm muitas instrues para calcular funes transcendentais como senos e cossenos. Alm destas instrues, tambm presentes em linguagens assembly mais tradicionais, 16

. e n t r y saxpy ( . param . s32 n , . param . f 3 2 alpha , . param . u64 x , . param . u64 y ) { . reg . u16 %rh <4 >; . reg . u32 %r <6 >; . reg . u64 %rd <8 >; . reg . f 3 2 %f <6 >; . reg . pred %p<3 >; $LBB1__Z9saxpy_GPUifPfS_ : mov . u16 %rh1 , %c t a i d . x ; mov . u16 %rh2 , %n t i d . x ; mul . wide . u16 %r1 , %rh1 , %rh2 ; c v t . u32 . u16 %r2 , %t i d . x ; add . u32 %r3 , %r2 , %r 1 ; l d . param . s32 %r4 , [ n ] ; s e t p . l e . s32 %p1 , %r4 , %r 3 ; @ %p1 bra $Lt_0_770 ; c v t . u64 . s32 %rd1 , %r 3 ; mul . l o . u64 %rd2 , %rd1 , 4 ; l d . param . u64 %rd3 , [ y ] ; add . u64 %rd4 , %rd3 , %rd2 ; l d . g l o b a l . f32 %f1 , [%rd4 + 0 ] ; l d . param . u64 %rd5 , [ x ] ; add . u64 %rd6 , %rd5 , %rd2 ; l d . g l o b a l . f32 %f2 , [%rd6 + 0 ] ; l d . param . f 3 2 %f3 , [ alpha ] ; mad . f 3 2 %f4 , %f2 , %f3 , %f 1 ; s t . g l o b a l . f32 [%rd4 + 0 ] , %f 4 ; $Lt_0_770 : exit ; }

Figura 1.15. A verso PTX do programa da gura 1.4.

tais como x86 ou PowerPC, PTX contm instrues que lidam com as particularidades da execuo paralela, incluindo aquelas que manipulam barreiras de sincronizao, leitura e escrita atmicas e acesso memrias compartilhadas. E a partir daqui? Existem duas fontes principais de otimizaes em programas CUDA: a memria e o uxo de controle dos programas. Na prxima seo daremos uma olhada no primeiro destes itens, cando o segundo para a nossa ltima seo.

1.2. Otimizaes de memria


Nesta seo descreveremos a hierarquia de memria que caracteriza as GPUs, dando nfase s boas prticas de programao que possibilitam um melhor uso desta hierarquia. A hierarquia de memria. As arquiteturas de computadores normalmente organizam a memria em uma hierarquia. Em direo ao topo desta hierarquia temos os dispositivos de armazenamento mais rpidos e tambm mais caros e menores, 17

tais como os registradores. Na direo oposta temos a memria mais abundante, mais barata e, infelizmente, de acesso mais lento, tais como os discos rgidos. E entre registradores e discos rgidos temos vrios nveis de cache e tambm a memria RAM. A GPU tambm possui uma hierarquia de memria e o conhecimento sobre a organizao desta hierarquia uma informao fundamental para o desenvolvimento de aplicaes ecientes. Uma GPU tende a manipular quantidades muito grandes de dados. Cada imagem que deve ser renderizada em uma tela de alta resoluo contm milhes de pixels. A ttulo de exemplo, a placa GeForce 8800 capaz de manipular 32 pixels por ciclo de execuo. Cada pixel contm uma cor, representada por trs bytes e uma profundidade, representada por quatro bytes. Alm disto, o processamento de um nico pixel, em mdia, leva leitura de outros 16 bytes de informao. Isto quer dizer que 736 bytes so manipulados por ciclo de execuo uma quantidade considervel, quando comparada a uma tpica instruo de CPU, que manipula cerca de 12 bytes por ciclo de execuo. Diferentes GPUs utilizam diferentes tipos de hierarquias de memria, mas um arranjo tpico consiste em separar a memria nos grupos abaixo relacionados: Registradores: registradores so rpidos, porm poucos. Cada thread possui um conjunto privado de registradores. Compartilhada: threads no mesmo bloco compartilham uma rea de memria, a qual funciona como um cache manipulado explicitamente pelo programa. Local: cada thread possui acesso a um espao de memria local, alm de seus registradores. Observe que a palavra local" no implica em acesso rpido: esta rea de memria est fora do micro-chip de processamento, junto memria global, e portanto, ambas estas memrias possuem o mesmo tempo de acesso. Global: esta memria est disponvel para todas as threads em cada bloco e em todas as grades. Na verdade, a memria global a nica maneira de threads em uma grade conversarem com threads em outra. 18

Os registradores e as memrias compartilhadas, locais e globais esto dentro da placa grca, apesar de somente a memria compartilhada estar dentro do micro-chip de processamento. Existe, contudo, uma outra memria, que tambm importante, mas que est fora da placa: trata-se da memria DRAM utilizada para que a GPU e a CPU possam comunicarse. Esta memria no parte da GPU; ao contrrio, ela est para a GPU assim como o disco rgido est para a CPU. Logo, ela encontra-se no limite inferior de nossa hierarquia de memria, sendo extremamente abundante, porm apresentando um tempo de acesso muito lento. A gura 1.16 coloca a hierarquia de memria em perspectiva com a hierarquia de threads. Existem algumas leis que devem ser obedecidas para que a memria possa ser utilizada de forma eciente em GPUs: 1. os registradores so as unidades de armazenamento mais rpidas e tambm as mais limitadas. A GPU prov um nmero xo de registradores para o bloco de threads. Assim, quanto mais registradores uma thread usar, menos threads podero existir em um bloco; 2. o acesso memria local ou global muito lento. Threads deveriam ser capazes de compartilhar a maior quantidade possvel de dados em memria compartilhada. 3. a comunicao entre dispositivos, isto , entre a CPU e a GPU, muito cara. Este canal algumas ordens de magnitude mais lento que a memria compartilhada, e mesmo as memrias globais e locais. Logo, este tipo de comunicao deve ser diminudo tanto quanto possvel. Esta uma das principais razes porque GPUs no so adequadas para aplicaes iterativas. A viagem inter-planetria. Comparemos o tempo que uma thread gasta para ler uma unidade de dado de sua memria compartilhada com o tempo que a CPU leva para enviar esta mesma unidade para a GPU. Valendo-me de uma analogia, poder-se-ia dizer que a primeira leitura equivale a atravessar uma rua deserta e sem semforos, ao passo que a segunda leitura equivale a uma viagem lua caminhando. Em outras palavras, para usar a GPU, a CPU precisa lhe enviar dados, o que feito por 19

Dispositivo (GPU)
Bloco (0, 0) Memria Compartilhada Regs Thread (0, 0)
Mem Local

Bloco (0, 1) Memria Compartilhada Regs Thread (0, 0)


Mem Local

Regs Thread (1, 0)


Mem Local

Regs Thread (1, 0)


Mem Local

A viagem internacional

Memria Global
A viagem interplanetria

DRAM

Hospedeiro (CPU)

Figura 1.16. A organizao de memrias dentro da GPU.

um barramento PCI (Peripheral Component Interconnect). Esta tranferncia muito lenta, comparada com o tempo de acesso de dados dentro do micro-chip CUDA. A transferncia de dados entre CPU e GPU ilustrada pelo programa na gura 1.17. As funes cudaMalloc, cudaFree e cudaMemcpy so parte da biblioteca de desenvolvimento CUDA. A primeira funo reserva uma rea na memria da GPU, ao passo que a segunda funo libera esta rea para outros usos. Finalmente, cudaMemcpy copia dados da CPU para a GPU, ou vice-versa, dependendo de seu ltimo argumento. 20

void Mul ( const f l o a t A , const f l o a t B , i n t width , f l o a t C) i n t size = width width sizeof ( f l o a t ) ; / / Load A and B t o t h e d e v i c e f l o a t Ad ; cudaMalloc ( ( void)&Ad , s i z e ) ; cudaMemcpy ( Ad , A , s i z e , cudaMemcpyHostToDevice ) ; f l o a t Bd ; cudaMalloc ( ( void)&Bd , s i z e ) ; cudaMemcpy ( Bd , B , s i z e , cudaMemcpyHostToDevice ) ; / / A l l o c a t e C on t h e d e v i c e f l o a t Cd ; cudaMalloc ( ( void)&Cd , s i z e ) ; / / Compute t h e e x e c u t i o n c o n f i g u r a t i o n assuming / / t h e m a t r i x dimensions are m u l t i p l e s o f BLOCK_SIZE dim3 dimBlock ( BLOCK_SIZE , BLOCK_SIZE ) ; dim3 dimGrid (wB / dimBlock . x , hA / dimBlock . y ) ; / / Launch t h e d e v i c e computation Muld<<<dimGrid , dimBlock >>>(Ad , Bd , width , Cd ) ; / / Read C from t h e d e v i c e cudaMemcpy (C, Cd , s i z e , cudaMemcpyDeviceToHost ) ; / / Free d e v i c e memory cudaFree ( Ad ) ; cudaFree ( Bd ) ; cudaFree ( Cd ) ; }

Figura 1.17. Cdigo escrito para a CPU, que invoca um kernel da GPU, passando-lhe matrizes de dados e lendo de volta uma matriz de dados.

Existem importantes aspectos de desempenho que precisam ser considerados, quando escrevemos programas CUDA, no que toca a comunicao entre GPU e CPU. Em primeiro lugar, justicvel o envio de dados para a GPU somente quando o trabalho que ser executado sobre estes dados possui alta complexidade. Por exemplo, o programa na gura 1.5 adiciona duas matrizes de mesma dimenso, efetuando uma soma para cada posio destas matrizes. O programa CUDA correspondente, visvel na gura 1.7, atribui cada thread uma quantidade de trabalho constante, isto , cuja complexidade assimpttica O(1), alm disto, esta constante, neste caso particular, bem 21

pequena. Logo, o trabalho de enviar os dados para a GPU, o que feito de forma sequencial, j maior que o trabalho realizado dentro da placa grca. Em vista destas observaes, fcil concluir que a razo entre o trabalho para transferir dados e o trabalho realizado sobre estes dados no boa. A m de transmitir as matrizes A, B e S, so realizadas 3N 2 acessos memria. O kernel realiza N 2 adies. Portanto, a razo entre transferncia e trabalho que encontramos de 1:3. Por outro lado, no caso de multiplicao de matrizes temos um cenrio muito melhor. Este algoritmo transfere a mesma quantidade de dados que o algoritmo de adio de matrizes, porm realiza N 3 adies e multiplicaes. Temos ento uma razo entre transferncia e trabalho de N 3 /3N 2 = O(N). Em segundo lugar, importante manter dados na memria da GPU tanto quanto possvel e necessrio. Uma vez que dados so enviados GPU, eles permanecem na memria da placa grca, mesmo que o kernel que os processou j tenha terminado. Assim, programas que usam mltiplos kernels podem evitar a viagem interplanetria" simplesmente invocando novos kernels sobre dados j transferidos. Esta abordagem pode ser benca mesmo que o trabalho intermedirio a ser realizado sobre tais dados pudesse ser mais rapidamente feito na CPU um algoritmo sequencial executado na GPU pode ser mais rpido que o uso do barramento externo de memria. Transferncia assncrona de dados. possvel melhorar o desempenho de aplicaes sobrepondo o tempo gasto em transferncia de dados e o tempo gasto em computaes. C para CUDA fornece ao programador uma forma de obter tal sobreposio, via transmisses assncronas de dados. Esta assincronia pode ser implementada usando-se a funo cudaMemcpy Async em vez da funo cudaMemcpy. O programa na gura 1.18, retirado do manual de boas prticas da Nvida [5], compara a sintaxe utilizada em transferncias sncronas e assncronas. O programa assncrono envia nStreams uxos de dados da CPU para a GPU, invocando um kernel para cuidar de cada um destes lotes. A funo cudaMemcpyAsync no bloqueia a execuo da CPU enquanto ela est sendo invocada. Isto , uma vez chamada, esta funo retorna o controle de execuo para 22

/ / Synchronous data t r a n s f e r : cudaMemcpy ( d e s t i n a t i o n , source , N s i z e o f ( f l o a t ) , d i r ) ; k e r n e l <<<N / nThreads , nThreads >>>( d e s t i n a t i o n ) ; / / Asynchronous data t r a n s f e r : s i z e = N s i z e o f ( f l o a t ) / nStreams ; f o r ( i = 0 ; i < nStreams ; i ++) { o f f s e t = i N / nStreams ; cudaMemcpyAsync ( d e s t i n a t i o n + o f f s e t , source + o f f s e t , s i z e , d i r , stream [ i ] ) ; } f o r ( i =0; i <nStreams ; i ++) { o f f s e t = i N / nStreams ; k e r n e l <<<N / ( nThreads nStreams ) , nThreads , 0 , stream [ i ] > > >( a_d+ o f f s e t ) ; }

Figura 1.18. Comparao entre transferncias sncronas e assncronas de dados.

Tranferncia de dados Execuo do Kernel

Tranferncia de dados Execuo do Kernel

Figura 1.19. Transferncia sncrona versus transferncia assncrona.

a CPU, mesmo que a transferncia de dados ainda no tenha sido feita completamente, ao contrrio de cudaMemcpy. A gura 1.19, retirada do guia de boas prticas da NVIDIA [5, p.18], esquematiza a sobreposio de dados que obtemos utilizando a funo de transferncia assncrona. Acesso agrupado memria global. Os dados passados GPU pela CPU cam armazenados em uma rea de dados conhecida como memria global. Nesta seo discutiremos alguns fatores que inuenciam o bom uso desta memria. Ns utilizaremos o programa de multiplicao matricial usado por Ryoo 23

/ / Computes t h e m a t r i x p r o d u c t u s i n g l i n e m a t r i c e s : void mulMatCPU ( f l o a t B , f l o a t C, f l o a t A , unsigned W) { f o r ( unsigned i n t i = 0 ; i < W; ++ i ) { f o r ( unsigned i n t j = 0 ; j < W; ++ j ) { A[ i W + j ] = 0.0; f o r ( unsigned i n t k = 0 ; k < W; ++k ) { A [ i W + j ] += B [ i W + k ] C [ k W + j ] ; } } } }

Figura 1.20. C program that performs the matrix product A = B C, using linearized matrices.

_ _ g l o b a l _ _ void matMul ( f l o a t B , f l o a t C, f l o a t A , i n t W) { f l o a t Pvalue = 0 . 0 ; i n t t x = b l o c k I d x . x blockDim . x + t h r e a d I d x . x ; i n t t y = b l o c k I d x . y blockDim . y + t h r e a d I d x . y ; f o r ( i n t k = 0 ; k < W; ++k ) { Pvalue += B [ t x W + k ] C [ k W + t y ] ; } A [ t y + t x W] = Pvalue ; }

Figura 1.21. Kernel que realiza o produto matricial A = B C. Esta uma paralelizao do algoritmo visto na gura 1.20.

et al. [32] a m de demonstrar o uso eciente tanto da memria global quanto de outros nveis de memria, os quais sero apresentados em sees posteriores. Este kernel obtido da paralelizao do programa visto na gura 1.20. Em um modelo PRAM de processamento possvel realizar a multiplicao matricial em tempo O(ln N). Esta soluo, contudo, no exatamente trivial e ns iremos utilizar, em vez dela, uma soluo O(N) no modelo PRAM, em que N a largura da matriz. O algoritmo escrito em C para CUDA dado na gura 1.21. Cada thread reponsvel pela produo de um 24

nico elemento Ai j na matriz nal A. Para produzir tal elemento, a thread deve realizar um produto escalar entre a i-sima linha da matriz B e a j-sima linha da matriz C. Placas grcas diferentes agrupam os dados em memria global de vrias maneiras; porm, suporemos que esta memria est particionada em segmentos de 16 palavras cada. Caso 16 threads que formam a metade de um warp precisem ler dados localizados em um mesmo segmento, ento podemos efetuar esta transao via somente uma viagem memria global. Dizemos, neste caso, que o acesso memria agrupado. Por outro lado, se estas threads buscarem dados em segmentos diferentes, ento ser feita uma viagem para casa segmento distinto. A m de garantir que a leitura ou escrita de memria global seja agrupada, podemos utilizar os seguintes passos de vericao: 1. Considere a thread t, cujo identicador seja um mltiplo de 16, isto , t = 16 n. 2. determine o segmento de dados s que t est usando. 3. para cada thread t + i, 1 i 15, verique se t + i est tambm usando s. A m de entender o impacto de acessos agrupados sobre o desempenho de aplicaes CUDA, consideremos o kernel mat Mul da gura 1.21. Lembrando que o ndice the uma thread em um arranjo bidimensional de threads, dado por tid = x + y Dx, sendo Dx o nmero de threads no eixo x, temos que o ndice x de uma thread varia mais rapidamente. Logo, para que tenhamos o acesso agrupado, preciso que as threads (0, 0), (1, 0), . . . , (15, 0) leiam dados em um mesmo segmento. Este alinhamento no acontece nas leituras da matriz B, pois, supondo tx = 0, 1, 2, . . . , 15 e W = 600, estaramos lendo as posies B[0], B[600], . . ., B[9000]. O acesso agrupado tambm no ocorre na escrita da matriz A, pois o ndice tx , que varia mais rapidamente que o ndice ty , est multiplicando a varivel W. Uma forma de agrupar os acessos memria global, neste caso, via uma simples re-ordenao de ndices, conforme feito na gura 1.22. V-se que este programa usa acessos agrupados em todas as leituras e escritas de dados. Por exemplo, no25

_ _ g l o b a l _ _ void matMulCoalesced ( f l o a t B , f l o a t C, f l o a t A , i n t W) { f l o a t Pvalue = 0 . 0 ; i n t t x = b l o c k I d x . x blockDim . x + t h r e a d I d x . x ; i n t t y = b l o c k I d x . y blockDim . y + t h r e a d I d x . y ; f o r ( i n t k = 0 ; k < W; ++k ) { Pvalue += B [ t y W + k ] C [ k W + t x ] ; } A [ t x + t y W] = Pvalue ; }

Figura 1.22. Modicao do programa da gura 1.21 para usar acessos agrupados memria global.

vamente supondo (tx ,ty ) = (0, 0), (1, 0), . . . , (15, 0), e W = 600, as posies lidas da matrix B sero sempre as mesmas para todas as threads em meio warp e as posies lidas da matriz C sero consecutivas e portanto contidas em um segmento de dados. Esta simples modicao de cdigo nos d um programa que quase 10 vezes mais rpido que aquele programa visto na gura 1.21, quando executado em uma placa GPU 9400M! A memria global e a memria compartilhada. Uma boa maneira de medirmos o desempenho de uma funo em C para CUDA contarmos quantas operaes de ponto-utuante esta funo realiza por segundo [32]. Este nmero conhecido por (FLOPS) e para obt-lo, faz-se conveniente observarmos o programa PTX produzido pelo lao mais interno do programa na gura 1.22, o qual mostrado na gura 1.23. Aproximadamente 1/8 das instrues dentro do lao deste programa so operaes de ponto utuante: existem 16 instrues, uma delas, multiply-add, equivale a duas operaes. Considerando-se que uma placa GTX 8800 realiza 172.8 bilhes de operaes de ponto-utuante por segundo, ento temos, neste hardware, uma taxa de trabalho de 21.6 GFLOPS. Entretanto, ao medir-se o desempenho deste programa em tal placa, percebeu-se que a taxa de trabalho real bem menor: 10.58 GFLOPS [32]. O gargalo, neste caso, a forma de utilizao da memria. 26

mov . f 3 2 mov . s32 $Lt_0_1282 : c v t . u64 . u32 mul . l o . u64 l d . param . u64 add . u64 l d . g l o b a l . f32 c v t . u64 . u32 mul . l o . u64 l d . param . u64 add . u64 l d . g l o b a l . f32 mad . f 3 2 add . u32 l d . param . s32 add . u32 s e t p . ne . s32 @ %p2 bra bra . u n i

%f1 , 0f00000000 ; %r10 , %r 5 ;

/ / 0 . 0F

%rd3 , %r 7 ; %rd4 , %rd3 , 4 ; %rd2 , [ B ] ; %rd5 , %rd2 , %rd4 ; %f2 , [%rd5 + 0 ] ; %rd6 , %r 9 ; %rd7 , %rd6 , 4 ; %rd1 , [ C ] ; %rd8 , %rd1 , %rd7 ; %f3 , [%rd8 + 0 ] ; %f1 , %f2 , %f3 , %f 1 ; %r7 , %r7 , 1 ; %r3 , [ Width ] ; %r9 , %r3 , %r 9 ; %p2 , %r7 , %r 8 ; $Lt_0_1282 ; $Lt_0_770 ;

Figura 1.23. The PTX version of the program in Figure 1.21.

O programa da gura 1.22 realiza muitos acessos redundante memria global. Cada thread l dois vetores de tamanho N e escreve um vetor de tamanho N. Ou seja, dado que temos N 2 threads, uma para cada posio da matriz produto, ento temos 3N 3 acessos memria global. Poderamos diminuir esta redundncia via um cache. Poucas GPUs provem um cache para a memria global; entretanto, o desenvolvedor pode programar um cache explicitamente usando a memria compartilhada. A memria compartilhada cerca de 100 vezes mais rpida que a memria global. Esta memria pode ser entendida como um cache manipulado diretamente pelo desenvolvedor CUDA. Isto , arquiteturas tradicionais tendem a controlar o cache, de forma tal que o programador no precise se preocupar em denir quais dados devem ser alocados l. O hardware , atualmente, muito bom em controlar caches. GPUs mais antigas, contudo, no possuem este tipo de gerncia automtica de cache ca a cargo do programador decidir quais dados devem ser armazenados na memria de rpido acesso. A soluo vista na gura 1.22 relativamente ingnua, pois as threads no compartilham dados, ainda que elas usem uma 27

_ _ g l o b a l _ _ void matMulTiled ( f l o a t B , f l o a t C, f l o a t A , i n t Width ) { __shared__ f l o a t Bs [ TILE_WIDTH ] [ TILE_WIDTH ] ; __shared__ f l o a t Cs [ TILE_WIDTH ] [ TILE_WIDTH ] ; int tx = threadIdx . x ; int ty = threadIdx . y ; / / I d e n t i f y t h e row and column o f t h e A element t o work on i n t Row = b l o c k I d x . x TILE_WIDTH + t x ; i n t Col = b l o c k I d x . y TILE_WIDTH + t y ; f l o a t Pvalue = 0 ; / / Loop over t h e B and C t i l e s r e q u i r e d t o compute t h e A element f o r ( i n t m = 0 ; m < Width / TILE_WIDTH ; ++m) { / / C o o l a b o r a t i v e l o a d i n g o f B and C t i l e s i n t o shared memory Bs [ t y ] [ t x ] = B [ Col Width + (m TILE_WIDTH + t x ) ] ; Cs [ t y ] [ t x ] = C [ Row + (m TILE_WIDTH + t y ) Width ] ; __syncthreads ( ) ; #pragma u n r o l l 1 f o r ( i n t k = 0 ; k < TILE_WIDTH ; ++k ) Pvalue += Bs [ t y ] [ k ] Cs [ k ] [ t x ] ; __syncthreads ( ) ; } A [ Col Width + Row ] = Pvalue ; }

Figura 1.24. Verso ladrilhada do kernel de multiplicao de matrizes.

grande quantidade de informao redundante. Um quarto das instrues na gura 1.23 lem ou escrevem dados em memria global. A m de utilizar-se completamente a GPU, seria necessrio uma banda de transmisso de dados de 173GB/s (128 threads 1/4 4 Bytes 1.35GHz), a qual consideravelmente maior que a banda fornecida pela placa: 86.4GB/s. O uso eciente da memria compartilhada pode reduzir este problema. Ladrilhamento. Uma conhecida forma de otimizao de cdigo, usada para aumentar a proximidade entre os dados manipulados dentro de laos de execuo e conhecida por ladrilhamento [21]. Nesta otimizao, os dados a serem manipulados so divididos em blocos e dados nos mesmos blocos, supostamente prximos no cache, so usados em conjunto. O ladrilhamento particularmente til em nosso caso, pois podemos dividir a tarefa de multiplicar duas grandes matrizes em vrias tarefas de multiplicar matrizes menores, com um mnimo de in28

tx 0 1 0 1

ty 0 0 1 1

Col * Width + (m * TILE_WIDTH + tx) m=0 m=1 0 1 4 5 2 3 6 7

Row + (m * TILE_WIDTH + ty) * Width m=0 m=1 0 1 4 5 8 9 12 13

2 6 8 12 9 13 10 14

3 7 11 15

0 4

1 5 9 13

2 6 10 14

3 7 11 15

0 4 8 12

1 5 9 13

2 6 10 14

3 7 11 15

8 12

Figura 1.25. Duas iteraes do bloco de threads (0, 0) do programa da gura 1.24.

terferncia. A gura 1.24 mostra o nosso exemplo ladrilhado. O ladrilhamento pode levar a programas de cdigo bastante complicado e o programa da gura 1.24 no foge regra. Normalmente ns calcularamos uma clula da matriz A percorrendo uma linha da matriz B e uma coluna da matriz C. No programa ladrilhado, contudo, estamos fazendo isto em partes. Copiamos um ladrilho de dados da matriz B e outro da matriz C para a memria compartilhada. Ento operamos sobre estes ladrilhos, efetuando um mini-produto" matricial, computando, assim, o resultado parcial de vrias clulas de A. A m de obter o valor nal destas clulas, precisamos processar uma linha completa de ladrilhos de B e uma coluna completa de ladrilhos de C, conforme ilustramos na gura 1.25, em que estamos supondo uma matriz de tamanho 44 e ladrilhos de tamanho 22. A gura mostra as duas iteraes necessrias para produzir o ladrilho do canto superior esquerdo de A. O programa ladrilhado ainda contm leituras repetidas de dados da memria global, porm, a quantidade de redundncias bem menor quando comparada com as redundncias do programa visto na gura 1.22. A m de demonstrar este fato, 29

_ _ g l o b a l _ _ void m a t M u l U n r o l l ( f l o a t B , f l o a t C, f l o a t Pd , i n t Width ) { __shared__ f l o a t Bs [ TILE_WIDTH ] [ TILE_WIDTH ] ; __shared__ f l o a t Cs [ TILE_WIDTH ] [ TILE_WIDTH ] ; int tx = threadIdx . x ; int ty = threadIdx . y ; i n t Row = b l o c k I d x . x TILE_WIDTH + t x ; i n t Col = b l o c k I d x . y TILE_WIDTH + t y ; f l o a t Pvalue = 0 ; f o r ( i n t m = 0 ; m < Width / TILE_WIDTH ; ++m) { Bs [ t y ] [ t x ] = B [ Col Width + (m TILE_WIDTH + t x ) ] ; Cs [ t y ] [ t x ] = C [ Row + (m TILE_WIDTH + t y ) Width ] ; __syncthreads ( ) ; Pvalue += Bs [ t y ] [ 0 ] Cs [ 0 ] [ t x ] ; Pvalue += Bs [ t y ] [ 1 ] Cs [ 1 ] [ t x ] ; Pvalue += Bs [ t y ] [ 2 ] Cs [ 2 ] [ t x ] ; Pvalue += Bs [ t y ] [ 3 ] Cs [ 3 ] [ t x ] ; Pvalue += Bs [ t y ] [ 4 ] Cs [ 4 ] [ t x ] ; Pvalue += Bs [ t y ] [ 5 ] Cs [ 5 ] [ t x ] ; Pvalue += Bs [ t y ] [ 6 ] Cs [ 6 ] [ t x ] ; Pvalue += Bs [ t y ] [ 7 ] Cs [ 7 ] [ t x ] ; Pvalue += Bs [ t y ] [ 8 ] Cs [ 8 ] [ t x ] ; Pvalue += Bs [ t y ] [ 9 ] Cs [ 9 ] [ t x ] ; Pvalue += Bs [ t y ] [ 1 0 ] Cs [ 1 0 ] [ t x ] ; Pvalue += Bs [ t y ] [ 1 1 ] Cs [ 1 1 ] [ t x ] ; Pvalue += Bs [ t y ] [ 1 2 ] Cs [ 1 2 ] [ t x ] ; Pvalue += Bs [ t y ] [ 1 3 ] Cs [ 1 3 ] [ t x ] ; Pvalue += Bs [ t y ] [ 1 4 ] Cs [ 1 4 ] [ t x ] ; Pvalue += Bs [ t y ] [ 1 5 ] Cs [ 1 5 ] [ t x ] ; __syncthreads ( ) ; } Pd [ Col Width + Row ] = Pvalue ; }

Figura 1.26. Multiplicao de matrizes aps a aplicao do desenrolamento.

seja N 2 /2 o tamanho de cada ladrilho. Temos ento, um total de 2(N/(N/2))2 ladrilhos. Cada ladrilho lido N/(N/2) vezes. Portanto, temos 4N 2 leituras da memria global. Dadas as 2N 3 leituras do programa original, observamos uma reduo substancial destes acesso, com consequncias no menos notveis para o desempenho da aplicao ladrilhada: o programa da gura 1.24 cerca de 25% mais rpido que o programa da gura 1.22, quando executado em uma placa GPU 9400M. Desenlao. Aumentamos a taxa de trabalho til realizado por um kernel diminuindo a quantidade de instrues que no realizam operaes de ponto-utuante no caso de um programa que produz sada neste formato. Entre estas instrues, destacam-se as operaes de acesso memria e as operaes que controlam o uxo do programa: desvios condicionais e incon30

dicionais. Tendo lidado com o primeiro grupo de operaes na seo anterior, voltamos agora nossa ateno para o segundo. Laos normalmente contm um desvio que testa uma condio sobre uma varivel de induo. O desenlao, uma otimizao de cdigo clssica, diminui o nmero de execues destes testes, podendo, inclusive, diminuir a necessidade de registradores no interior do lao. Continuando com o exemplo de Ryoo et al. [32], vemos, na gura 1.26, o resultado do desenlao aplicado ao programa da gura 1.24. Nesta verso do programa estamos supondo ladrilhos de tamanho 16 16, e, indo contra todos os bons princpios de programao, mas justicados pela simplicidade da exposio, ns utilizamos esta quantidade explicitamente na gura 1.26. O cdigo PTX do programa visto na gura 1.26 revela que o lao mais interior deste algoritmo contm 59 instrues e destas, 16 so de ponto-utuante. Temos, assim, uma taxa de trabalho de 93.72 GFLOPS. Dado que ns desenrolamos completamente o lao mais interno da gura 1.24, o registrador utilizado para armazenar a varivel de induo m no mais necessrio. Desta forma, o desenlao no somente aumentou a taxa de trabalho til realizado pelo programa, mas tambm reduziu a presso por registradores. Conforme veremos a seguir, esta reduo importante para aumentar a quantidade de threads que podem ser executadas simultaneamente na placa grca. O programa desenlaado 2.25x mais rpido que o programa da gura 1.24. A tabela 1.1 contm um sumrio destes resultados. Conforme nos mostra esta tabela, o kernel matMulTiled, primeiro a utilizar a memria compartilhada, no capaz de aproveitar a mxima quantidade de threads disponveis, pois ele necessita de mais registradores que os outros kernels. Registradores. Tanto em CPUs quanto em GPUs, registradores so as unidades de armazenamento que permitem a leitura e escrita mais rpidas. E, assim como em CPUs, GPUs tambm fornecem uma quantidade limitada de registradores para a execuo de programas. Por exemplo, a placa GeForce 8800 possui 16 multiprocessadores e cada um deles possui 8.192 registradores. Este nmero pode parecer grande a princpio, porm ele ser compartilhado entre at 768 threads. Logo, a m 31

Kernel matMul matMulCoalesced matMulTiled matMulUnroll

Figura 1.21 1.22 1.24 1.26

Speed-up 1 9.54 11.75 26.28

Registradores 9 9 11 9

Ocupao 1 1 2/3 1

Tabela 1.1. Resultados obtidos durante a otimizao do kernel de multiplicao matricial.

de obtermos a mxima ocupao do hardware, cada thread poderia utilizar no mximo 10 registradores. Desta forma, existe uma tenso entre o nmero de registradores disponveis para cada thread e a quantidade de threads que podem ser executadas simultaneamente. Por exemplo, o programa da gura 1.21 usa 9 registradores. Portanto, ele pode executar a quantidade mxima de 768 threads, uma vez que 9 768 < 8, 192. Ou seja, uma placa GeForce 8800 pode escalonar trs blocos, cada um com 256 threads, para executarem juntos. Todavia, caso a presso de registradores aumente em duas unidades, como deu-se no programa da gura 1.24, ento teramos 256 11 3 = 8, 488 > 8, 192. Neste caso apenas dois blocos de threads poderiam ser executados em conjunto, resultando em uma taxa de ocupao do hardware de 2/3. O clculo da presso de registradores, isto , o nmero mximo de registradores demandado por um programa um problema difcil e muitas de suas variaes so problemas NPcompletos. Duas variveis podem ser alocadas no mesmo registrador se elas no esto vivas" no mesmo ponto do programa. Dizemos que uma varivel v est viva em um determinado ponto de programa p se existe um caminho no uxo de execuo do programa que vai de p at um outro ponto de programa p onde v usada. Este caminho no deve passar por nenhuma denio da varivel v. A gura 1.27 ilustra estas noes. Nesta gura temos uma funo que possui um parmetro de entrada e duas variveis. Este parmetro e as variveis precisam ser armazenados em alguma rea de memria. Idealmente 32

p foo (int p) { x = p + 1 y = x + p return y }

R1
p(R1)

R2

x(R2) y(R2)

Figura 1.27. Presso de registradores em uma sequncia linear de cdigo.

gostaramos de usar registradores para tal armazenamento. As linhas de vida das variveis, isto , os pontos de programa onde estas variveis esto vivas, so mostradas como segmentos de linha na gura. fcil notar que a linha de vida de y no se sobrepe linha de vida de nenhuma das outras variveis. Logo, y pode compartilhar um registrador com qualquer destas variveis. A direita da gura mostramos como seria uma possvel alocao usando dois registradores, em que s variveis y e x foi dado o mesmo registrador. A presso de registradores na gura 1.27 dois o nmero mximo de linhas de vida que se sobrepem. No seria possvel, por exemplo, armazenar p e x no mesmo registrador: estas variveis esto vivas no mesmo ponto de programa. O clculo deste valor neste exemplo, contudo, bastante simples, pois o programa no possui desvios condicionais. O grafo de uxo de controle do programa uma linha e as linhas de vida formam um grafo de intervalos, o qual pode ser colorido em tempo polinomial. O problema naturalmente torna-se mais complicado uma vez que os programas passem a ter desvios condicionais e laos. Appel e Palsberg fornecem uma explicao detalhada sobre um algoritmo de alocao de registradores baseado na colorao de grafos [3]. 33

1.3. Anlises e Otimizaes de Divergncia


Muitas das otimizaes que vimos na Seo 1.2, tais como ladrilhamento e alocao de registradores, so tcnicas clssicas de compilao [1], podendo ser usadas tanto em programas sequenciais quanto em programas paralelos. Existem, contudo, otimizaes de cdigo que fazem sentido apenas quando usadas sobre mquinas SIMD, como os processadores de warps em CUDA. Esta peculiaridade deve-se a um fenmeno denominado execuo divergente, o qual descreveremos nesta seo. Warps de threads. Em uma GPU, threads so organizadas em grupos que executam em passo nico, chamados warps no jargo da NVIDIA e frentes de onda no jargo da ATI. Para entender as regras que governam o funcionamento das threads em um mesmo warp, imaginemos que cada warp pode usar vrias unidades lgicas e aritmticas; porm, existe somente um buscador de instrues por warp. Por exemplo, a placa GeForce 8800 capaz de executar 16 warps simultaneamente. Cada warp agrupa 32 threads e pode usar 8 unidades lgicas e aritmticas. Como existe somente um buscador de instrues, cada warp pode executar 32 instncias da mesma instruo em quatro ciclos de pipeline da GPU. Cada uma destas instrues, embora designando a mesma operao, no precisa usar os mesmos dados. Trazendo de volta nossa analogia do peloto de fuzilamento, cada soldado seria uma unidade de processamento, o capito que lhes grita as ordens faz o papel do buscador de instrues e cada fuzil representa os dados sendo processados. Evidentemente este modelo de execuo mais eciente quando usado sobre aplicaes regulares em que todas as threads podem executar as mesmas tarefas. Infelizmente muitas das aplicaes que gostaramos de paralelizar no so to regulares e divergncias podem acontecer entre as threads. Dizemos que um programa divergente se durante sua execuo threads agrupadas no mesmo warp seguem caminhos diferentes logo aps terem processado um desvio condicional. Divergncias ocorrem, ento, devido desvios condicionais. A condio de desvio pode ser verdadeira para algumas threads e falsa para outras. Dado que cada warp possui acesso no mximo a uma instruo a cada ciclo de execuo, em vista 34

de divergncias algumas threads tero de esperar ociosas enquanto as outras perfazem trabalho. Desta forma, divergncias podem ser uma das grandes fontes de degradao de desempenho em aplicaes GPU. A ttulo de ilustrao, Baghsorkhi et al [7] mostraram, analiticamente, que aproximadamente um tero do tempo de execuo do soma paralela de prexos [18], disponvel no pacote de desenvolvimento da NVIDIA, perde-se devido s divergncias. A transformao de um programa com o intuito de diminuir os efeitos de divergncias no uma tarefa imediata por vrias razes. Em primeiro lugar, alguns algoritmos so inerentemente divergentes, isto , seu comportamento, divergente ou no, depende de dados de entrada. Em segundo lugar, o problema de encontrar os pontos de programa onde ocorrem as mais srias divergncias coloca uma carga relativamente grande sobre o programador, que precisa escanear manualmente cdigos muitas vezes longos e complicados. Ordenao bitnica. Ilustraremos os conceitos de divergncia, bem como as tcnicas de otimizao que podem ser usadas em programas divergentes, usando como exemplo o algoritmo de ordenao bitnica. A ordenao vetorial no to regular quanto, por exemplo, a multiplicao matricial que vimos na seo anterior. A ordenao exige uma quantidade razovel de coordenao entre as threads, e divergncias acontecem naturalmente, devido ao vetor a ser ordenado. Uma boa abordagem para a ordenao paralela so as redes de ordenao. Uma das redes de ordenao mais conhecidas o algoritmo de ordenao bitnica [8]. Este algoritmo apresenta uma complexidade assimpttica de O(n ln2 n), quando restrito a um hardware sequencial. Ainda assim, a habilidade de realizar n/2 comparaes em paralelo, onde n o tamanho do vetor de entrada, torna este algoritmo muito atrativo para o ambiente SIMD de uma GPU. Nesta seo utilizaremos uma popular implementao da ordenao bitnica 1 . Este kernel frequentemente utilizado como um mdulo auxiliar em outras implementaes CUDA. Em particular, a ordenao bitnica parte da
1

http://www.cs.chalmers.se/ dcs/gpuqsortdcs.html

35

_ _ g l o b a l _ _ s t a t i c void b i t o n i c S o r t ( i n t v a l u e s ) { extern __shared__ i n t shared [ ] ; const unsigned i n t t i d = t h r e a d I d x . x ; shared [ t i d ] = v a l u e s [ t i d ] ; __syncthreads ( ) ; f o r ( unsigned i n t k = 2 ; k <= NUM; k = 2 ) { f o r ( unsigned i n t j = k / 2 ; j >0; j / = 2 ) { unsigned i n t i x j = t i d ^ j ; if ( ixj > tid ) { i f ( ( t i d & k ) == 0 ) { i f ( shared [ t i d ] > shared [ i x j ] ) { swap ( shared [ t i d ] , shared [ i x j ] ) ; } } else { i f ( shared [ t i d ] < shared [ i x j ] ) { swap ( shared [ t i d ] , shared [ i x j ] ) ; } } } __syncthreads ( ) ; } } v a l u e s [ t i d ] = shared [ t i d ] ; }

Figura 1.28. Implementao da ordenao bitnica.

implementao do quicksort paralelo de Cederman e Tsigas [9]. Neste caso, a ordenao bitnica invocada para ordenar os pequenos arranjos que o quicksort produz via a contnua aplicao do algoritmo de partio pivotal. A implementao da ordenao bitnica, conforme obtida no pacote de desenvolvimento da NVIDIA, dada na gura 1.28. Dado que a GPU est presa ao modelo de processamento SIMD, provvel que divergncias venham a acontecer durante a execuo da ordenao bitnica. Ou seja, invariavelmente a condio (tid & k) == 0, em que tid o identicador da thread, ser verdadeira para algumas threads e falsa para outras. A m de observarmos o impacto de divergncias sobre o desempenho da aplicao CUDA, faz-se necessrio deixarmos o alto nvel da linguagem fonte, para nos atermos aos detalhes do cdigo PTX, cujo assembly mostrado na gura 1.29. A gura 1.29 usa uma notao que omitimos at agora: tratase do grafo de uxo de controle (CFG). Este CFG descreve as instrues PTX geradas para o bloco de cdigo da gura 1.28 que existe no escopo do teste ixj > tid. O CFG possui um 36

L1

%ixj = xor %tid %j 1 %p1 = gt %ixj %tid 2 3 bra %p1 L2

5 %t1 = and %tid %k 6 %p2 = eq %t1 0 7 bra %p2 L3

L2

L3

14 15 16 17

%t2 %t3 %p3 bra

= ld %shared[%tid] = ld %shared[%ixj] = gt %t2 %t3 %p3 L7

L4

8 9 10 11

%t4 %t5 %p4 bra

= ld %shared[%tid] = ld %shared[%ixj] = lt %t4 %t5 %p4 L7

18 st %shared[%tid] %t3 19 st %shared[%ixj] %t2

L5

12 st %shared[%tid] %t5 13 st %shared[%ixj] %t4

L6

L7

sync

Figura 1.29. O grafo de uxo de controle, em PTX simplicado, que representa o kernel da gura 1.28.

nodo para cada bloco bsico do programa assembly, sendo um bloco bsico uma sequncia maximal de instrues sem desvios, sejam estes condicionais ou no. Os possveis uxos de execuo entre os blocos bsicos so descrito pelas arestas do grafo. Esta descrio esttica: o programa pode nunca vir a seguir um determinado uxo de execuo, ainda que esta possibilidade exista, conforme descrito pelas arestas do CFG. A gura 1.30 mostra um trao de execuo do programa da gura 1.29, em um cenrio em que temos quadro unidades funcionais e quatro threads por warp: t0 ,t1 ,t2 e t3 . Supondo um vetor de entrada igual a [4, 3, 2, 1], quando k = 2 e j = 1, temos duas divergncias. A primeira diviso acontece no ciclo i = 3, 37

ciclo 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19

rt. L1

L2

L3

L4

L5 L6 L7

Instruo xor %ixj %tid %j gt %p1 %ixj %tid bra %p1 L2 and %t1 %tid %k eq %p2 %t1 0 bra %p2 L3 load %t2 %shared load %t3 %shared gt %p3 %t2 %t3 bra %p3 L7 load %t4 %shared load %t5 %shared lt %p4 %t4 %t5 bra %p3 L7 store %tid %t3 store %tid %t2 store %tid %t5 store %tid %t4 sync

%tid %ixj

%tid %ixj

t0 1 2 3 5 6 7 14 15 16 17 18 19 4

t1 1 2 3 4

t2 1 2 3 5 6 7 8 9 10 11 12 13 4

t3 1 2 3 4

Figura 1.30. Uma curta trilha de execuo do programa 1.29, mostrando as primeiras iteraes deste cdigo, supondo quatro threads por warps e v = {4, 3, 2, 1}.

devido ao desvio bra %p1, L2 e esta separa as threads t0 e t2 das threads t1 e t3 . Esta divergncia acontece porque a condio %ixj > %tid verdadeira apenas para as threads t0 e t2 . A segunda diviso acontece no ciclo i = 6, devido ao desvio bra %p2, L3 e ela separa as threads t0 e t2 . Medindo divergncias via proling. Como detectar a localizao e o volume das divergncias que inuenciam o desempenho de uma aplicao? Divergncias ocorrem somente em desvios condicionais, porm, nem todo desvio condicional divergente, e, dentre aqueles que o so, certamente divergncias ocorrem em diferentes intensidades. Existem duas abordagens para a deteco de divergncias: proling e anlise esttica. Proling 38

um mtodo dinmico: o programa deve ser executado em um ambiente monitorado. A anlise esttica justamente o contrrio: a ferramenta de anlise estuda o cdigo do programa tentando provar quais desvios condicionais so passveis de causar divergncias e quais nunca o faro. Falaremos de proling primeiro, deixando a anlise esttica para o nal desta seo. Prolers so aliados de longa data de desenvolvedores de aplicao de alto desempenho. Desde a introduo de gprof, uma ferramenta extremamente popular [17], muitos outros prolers foram desenvolvidos, com vrios propsitos, que vo desde guiar os compiladores otimizantes [10] at a depurao de erros [28]. Os primeiros prolers, naturalmente, eram destinados a medir o desempenho de aplicaes sequenciais [17, 24, 28, 35]. Trabalhos subsequentes, contundo, estenderam o escopo de tais ferramentas tambm aos programas paralelos [15, 19, 25, 37, 41]. Ainda assim, prolers de aplicaes paralelas, tais como os trabalhos desenvolvidos por Tallent et al. [37] ou Eyerman e Eeckhout [15], so utilizados em modelos de execuo onde threads trabalham de forma relativamente independente uma das outras. Prolers que atuem com sucesso sobre o ambiente SIMD de GPUs so ainda raridade. Existem pelo menos dois prolers que capturam o comportamento divergente de programas CUDA. Um deles proler visual" da NVIDIA (CVP) 2 . Esta ferramenta permite ao desenvolvedor CUDA aferir vrios nmeros referentes execuo do programa paralelo, incluindo a quantidade de divergncias que ocorreram durante esta execuo. Contudo, CVP no aponta os pontos de programa em que as divergncias aconteceram, em vez disto, informando um valor absoluto de divergncias. Tal informao pode, contudo, ser obtida usando-se o proler desenvolvido por Coutinho et al. [12], o qual descrevemos a seguir. Proling via instrumentao: A mensurao dinmica de programas CUDA um tanto diferente da mensurao de aplicaes sequenciais e mesmo aplicaes paralelas que atuem sobre modelos diferentes de SIMD, anal, temos muitas threads sendo executadas em paralelo, e elas esto agrupadas em
2

http://forums.nvidia.com/index.php?showtopic=57443

39

unidades que executam trabalho em passo nico. A m de detectarmos divergncias, um proler deve ser capaz de averiguar que em determinado desvio algumas threads escolheram um caminho, enquanto as demais threads optaram pelo outro. Tal pode ser feito via instrumentao do cdigo assembly a ser testado. Esta instrumentao armazena os resultados do proler em uma estrutura de dados que chamamos de mapa de divergncias. Tal estrutura consiste em dois arranjos de inteiros, os quais denominaremos e , sendo que [b] armazena o nmero de warps que visitaram cada bloco bsico b e [b] armazena a quantidade de visitas que resultaram em divergncias. Esta instrumentao inserida automaticamente, em trs passos: 1. Inicializao: memria reservada no dispositivo para armazenar o mapa de divergncias; 2. Mensurao: o mapa da divergncias para uma determinada execuo do programa construdo; 3. Leitura: o mapa de divergncias copiado da GPU de volta para a CPU. As fases de inicializao e leitura so triviais; logo, no que resta desta seo, descreveremos o processo de mensurao. O cdigo de instrumentao de divergncias mostrado na gura 1.31. Estas instrues so inseridas sobre cada desvio condicional. Esta gura mostra a instrumentao inserida sobre o bloco L2 do exemplo visto na gura 1.29. Cada block b a ser investigado dividido em trs blocos: bup , bbottom e b f ind writer . A instrumentao realiza duas tarefas: (i) nos blocos bup e b f ind writer uma thread selecionada a thread escritora para atualizar os arranjos e . (ii) no bloco bbottom a divergncia detectada e reportada. A thread escritora sempre aquela com o menor identicador. A m de selecionar um escritor no podemos simplesmente atribuir esta tarefa thread zero, por exemplo, uma vez que tal thread pode no estar ativa em um trecho de cdigo divergente, exatamente devido a uma divergncia anterior. Na gura 1.31 a varivel %laneid denota o identicador da thread dentro do 40

int writer = 0; bool gotWriter = false; while (!gotWriter) { bool iAmWriter = false; if (laneid == writer) { iAmWriter = true; } if ( t w | iAmWriter == true) { gotWriter = true; } else { writer++; } High level description }

of block L2(find writer)

L2(up)
1 2 3 %t1 = and %tid %k %p2 = eq %t1 0 %writer = mov 0

L2(find writer)
4 5 6 7 8 %iAmWriter = eq %laneid, %writer %gotWriter = vote.any %iAmWriter %continue = neg %gotWriter %writer = add %writer, 1 bra %continue $L2_find_writer

L2(bottom) 9 @iAmWriter %[L2] = atom.add %[L2] 1 %consensus = vote.uni %p2 10 %dvrg = neg %consensus 11 %wrDv = and %iAmWriter %dvrg 12 %[L2] = atom.add %[L2] 1 13 @wrDv bra %p2 L3 14

Figura 1.31. Cdigo inserido para instrumentar o bloco L2 na gura 1.29. O cdigo presente no programa original est marcado em cinza. A gura expe tambm uma descrio de alto nvel do bloco L2( f ind writer) responsvel por encontrar uma thread escritora. Cdigo presente no programa original est marcado em cinza.

warp. Os blocos bup e b f ind writer percorrem todas as threads, at encontrar a thread ativa de menor %laneid. Tendo sido eleito um escritor, descobrimos a divergncia via votao. A instruo PTX %p = vote.uni.pred, %q leva a 41

um valor verdadeiro em %p se todas as threads no warp encontrarem o mesmo valor para o predicado q. Assim, votam as threads com respeito ao predicado usado no desvio condicional. Se todas as threads no warp enxergam o mesmo valor para tal predicado, ento nenhuma divergncia acontece; doutro modo, uma divergncia faz-se notar, levando ento, ao incremento do arranjo . Tal adio realizada pela instruo atmica @iAmWriter % [L2 ] = atom.add % [L2 ] 1, a qual executada somente se o predicado @iAmWriter for verdadeiro. A instrumentao impe uma pesada carga sobre o programa instrumentado. Em termos de tamanho, este cresce entre 2 e 3 vezes. J em termos de tempo de execuo, a carga ainda maior: foram j reportadas perdas de desempenho de at 1,500x [12], o que no est em desacordo com outors prolers baseados em instrumentao e usados em ambientes sequenciais [27]. De qualquer forma, o ponto mais importante que a instrumentao no modica a semntica do programa original, uma vez que aquela no altera nenhuma das variveis deste. Logo, a observao das divergncias, ainda que intrusiva, no causa a existncia de novas divergncias, ou a ausncia de divergncias existentes no programa original. Usando informao de proling para otimizar aplicaes: Nesta seo mostraremos como obter mais desempenho de aplicaes CUDA usando para isto o resultado da informao de proling. Otimizaremos o algoritmo de ordenao bitnica mostrado na gura 1.28, porm mostraremos resultados obtidos para a implementao de quicksort de Cederman et al. [9]. Este algoritmo usa o kernel da gura 1.28 para ordenar arranjos de tamanho abaixo de um certo limiar. Os ganhos de desempenho que reportaremos nesta seo referem-se, portanto, ao algoritmo quicksort inteiro, embora ns mudaremos somente a ordenao bitnica, que um sub-mdulo daquele algoritmo. Veremos, assim, que possvel obter entre 6-10% de melhorias de desempenho modicando entre 10-12 instrues assembly de um programa de 895 instrues! A gura 1.32 mostra alguns dos resultados de proling que obtemos para a ordenao bitnica. Esta gura nos mostra que os condicionais aninhados no lao mais interno do kernel pade42

cem de uma grande quantidade de divergncias. Mais especicamente, a condio (tid & k) == 0 visitada por 28 milhes de warps e um tero destas visitas apresentam divergncias. O mapa tambm mostra que as divergncias so comuns em ambas as sub-clusulas do bloco condicional, a saber, o teste shared[tid] > shared[ixj] e o teste shared[tid] < shared[ixj]. A m de melhorar este kernel, atentemos para o fato de que o caminho formado pelo bloco L3 seguido pelo bloco L5 muito parecido com o caminho formado pelos blocos L4 e L6 . A nica diferena deve-se aos condicionais nos pontos de programa 9 e 13. De posse desta observao, costuramos os blocos L5 e L6 juntos, usando um pequeno truque de manipulao de ndices, obtendo assim o programa da gura 1.33 (a). Divergncias podem, obviamente, ainda acontecer, contudo, onde o maior caminho divergente da gura 1.29 contm oito instrues, o pior caso da gura 1.33 (b) contm apenas seis. Esta otimizao d-nos um ganho de desempenho de 6.75% em uma placa GTX 8800. O cdigo da gura 1.33 ainda nos fornece oportunidades de otimizao. Podemos usar os operadores ternrios disponveis em CUDA para casar os blocos bsicos L3 e L4 , assim removendo a divergncia ao nal do bloco L2 . O programa fonte modicado mostrado na gura 1.34 (a), e o cdigo PTX equivalente dado pela gura 1.34 (b). Uma instruo como %a = sel %tid %ixj %p atribui %tid a %a se %p for diferente de zero. O valor %ixj usado %a em caso contrrio. Neste novo programa, o mais longo caminho divergente possui somente duas instrues Esta ltima otimizao d-nos cerca de 9.2% de ganho de desempenho. Anlise esttica de programas: Um proler, ainda que muito til, possui algumas desvantagens: 1. a informao produzida pelo proler dependente dos dados de entrada. Caso o programa instrumentado seja alimentado com dados que no causem qualquer divergncia, ento obteremos um histograma de divergncias incondizente com a realidade do programa. 2. prolers baseados em instrumentao podem aumentar o tempo de execuo do programa sob anlise em at 43

__global__ static void bitonicSort(int * values) { extern __shared__ int shared[]; const unsigned int tid = threadIdx.x; shared[tid] = values[tid]; __syncthreads(); for (unsigned int k = 2; k <= NUM; k *= 2) { for (unsigned int j = k / 2; j>0; j /= 2) { unsigned int ixj = tid ^ j; if (ixj > tid) { 7,329,816 / 28,574,321 if ((tid & k) == 0) { 15,403,445 / 20,490,780 if (shared[tid] > shared[ixj]) { swap(shared[tid], shared[ixj]); } } else { 4,651,153 / 8,083,541 if (shared[tid] < shared[ixj]) { swap(shared[tid], shared[ixj]); } } } __syncthreads(); } } values[tid] = shared[tid]; }

Figura 1.32. Informaes produzidas pelo proler usando a entrada padro do algoritmo de ordenao quicksort de Cederman et al. [9]. A ordenao bitnica usada como um mdulo daquele quicksort.

1000x, conforme demonstrado anteriorment [12]! 3. um proler no pode garantir que uma divergncia nunca acontecer em um certo teste condicional. O ltimo item desta lista particularmente problemtico: a m de implementar otimizaes automaticamente, o compilador precisa de garantias concretas acerca do comportamento do pro44

(a)
unsigned int a, b; if ((tid & k) == 0){ b = tid; a = ixj; } else { b = ixj; a = tid; } if (sh[b] > sh[a]){ swap(sh[b],sh[a]); }

(b)

L2 %t1 = and %tid %k %p2 = eq %t1 0 bra %p2 L3 L3 %b = mov %tid %a = mov %ixj L3/4 %t2 %t3 %p3 bra L4 %b = mov %ixj %a = mov %tid

= ld %shared[%b] = ld %shared[%a] = gt %t2 %t3 %p3 L7 L5/6 st %shared[%a] %t3 st %shared[%b] %t2

L7 sync

Figura 1.33. Programa otimizado aps a juno dos blocos L5 e L6 da gura 1.29.

(a)
int p = (tid & k) == 0; unsigned b = p?tid:ixj; unsigned a = p?ixj:tid; if (sh[b] > sh[a]) { swap(sh[b], sh[a]); }

(b)

L2

%t1 = and %tid %k %p = eq %t1 0 %a = sel %tid %ixj %p %b = sel %ixj %tid %p %t2 = ld %shared[%b] %t3 = ld %shared[%a] %p3 = gt %t2 %t3 bra %p3 L7
L5/6

st %shared[%a] %t3 st %shared[%b] %t2

L7 sync

Figura 1.34. Cdigo que resulta do uso de operadores ternrios no programa visto na gura 1.33.

grama fonte. Uma forma de contornar estas decincias via 45

anlise esttica de programas. Faz-se necessrio salientar que a anlise esttica apresenta desvantagens tambm. O maior problema , provavelmente, o fato de que, a m de fornecer resultados sempre corretos a anlise esttica precisa ser bastante conservadora. Em nosso contexto, a anlise esttica dever marcar como divergente todos os testes condicionais que ela no pode provar que so no divergentes. Ainda assim, estas duas tcnicas: proling e anlise esttica, complementam uma a outra, frequentemente fornecendo mais informao em conjunto que a soma de cada parte em separado. O proler, por exemplo, til para calibrar a anlise esttica. Esta, por seu turno, permite reduzir a quantidade de instrumentao inserida nos programas, uma vez que no necessrio instrumentar desvios que nunca sero divergentes. Dado um programa CUDA P, estamos interessados em determinar uma aproximao conservativa, porm no trivial do conjunto de variveis divergentes de P. Uma varivel v divergente se existem duas threads no mesmo warp tal que cada thread enxerga v com um valor diferente, dado um mesmo momento da execuo do programa. Na prxima seo ns descreveremos uma anlise de divergncias, uma anlise esttica que encontra um conjunto de variveis divergentes. Esta anlise semelhante inferncia de barreiras de Aiken and Gay [2]; todavia, enquanto a inferncia de barreiras supe um modelo de execuo SPMD, a anlise de divergncias supe uma arquitetura SIMD. A m de explicar como se d a anlise de divergncias, usaremos alguns conceitos de teoria de compiladores, os quais so introduzidos na prxima seo. Atribuio esttica nica: Compiladores frequentemente utilizam representaes intermedirias para facilitar a tarefa de compreender automaticamente os programas. Uma das mais conhecidas representaes intermedirias o chamado formato de atribuio esttica nica, uma possvel traduo da nomenclatura original Static Single Assignment (SSA) [13]. Dada a popularidade da sigla SSA, ns a utilizaremos para referir-monos a esta representao intermediria. Programas neste formato possuem uma propriedade fundamental: cada varivel 46

denida somente uma vez no texto do programa. Dada que esta propriedade simplica enormemente a manipulao de programas, no que se segue iremos supor que todos os nossos cdigos PTX esto neste formato.
L3 %b = mov %tid %a = mov %ixj L4 %b = mov %ixj %a = mov %tid L3 %b1 = mov %tid %a1 = mov %ixj L4 %b2 = mov %ixj %a2 = mov %tid

L3/4

%t2 %t3 %p3 bra

= ld %shared[%b] = ld %shared[%a] = gt %t2 %t3 %p3 L7

L3/4

%a %a1 = %b %b1 %t2 %t3 %p3 bra

%a2 %b2

(a)

(b)

= ld %shared[%b] = ld %shared[%a] = gt %t2 %t3 %p3 L7

Figura 1.35. A representao de atribuio esttica nica (SSA). (a) programa original. (b) programa em formato SSA.

A gura 1.35 ilustra a converso de parte do cdigo visto na gura 1.33 para o formato SSA. Esta converso feita segundo duas aes: cria-se novos nomes para as vrias denies da mesma varivel e inserem-se funes no texto do programa, a m de mesclar as linhas de vidas destas variveis em um mesmo nome. Por exemplo o nome de varivel a possui duas denies na gura 1.35 (a); logo, este programa no est no formato SSA. A m de fazer a converso, renomeamos estas denies para a1 e a2. Existe, entretanto, um novo problema: a varivel a usada no bloco bsico L3/4 ; fato este que nos impe a questo de qual nome deveria ser usado naquele bloco: a1 ou a2? A resposta, neste caso, ambos os nomes. Se o uxo de execuo do programa alcana o bloco L3/4 vindo do bloco L3 , ento o nome correto a1, doutro modo o uxo de execuo pode somente provir de L4 e o nome que precisa ser usado a2. Esta ligao entre a semntica dinmica do programa e seu texto esttico feita por uma abstrao que denominamos fun47

o . Estas so instrues especiais, que funcionam como multiplexadores. A funo presente no bloco L3/4 , isto , a = phi(a1, a2) atribuir varivel a ou o valor de a1 ou o valor de a2, dependendo de onde ui a execuo que alcana L3/4 . Funes so uma abstrao notacional: elas no existem realmente em programas assembly. Compiladores que utilizam o formato SSA possuem, portanto, um passo adicional, que consiste na implementao destas instrues usando-se instrues concretas normalmente cpias, logo antes da gerao de cdigo de mquina. Alm do formato SSA, ns lanaremos mo tambm do conceito de ps-dominncia. Vamos supor que cada instruo em um programa PTX representada por um rtulo l. Um rtulo l p ps-domina outro rtulo l se e somente se, todo caminho de l at a sada do programa passa necessariamente por l p . Mais ainda, dizemos que l p o ps dominador imediato de l se l p = l e qualquer outro rtulo que ps-domina l tambm ps-domina l p . Fung et al. [16] mostraram que o melhor ponto para reconvergir threads divergentes o ps-dominador do desvio que causou as divergncias. O conceito de ps-dominncia usa a noo de caminho de programa, que denimos da seguinte forma: Dado um programa P, ns dizemos que um rtulo l vai para um rtulo l , o que indicamos por l l , se uma das trs condies verdadeira: l a prxima instruo aps l e l no rotula um desvio incondicional; l um desvio condicional sendo l um de seus alvos; l um desvio incondicional cujo destino l . Dizemos que l1 ln se l1 = ln , ou l1 l2 e l2 ln . Se l p o ps-dominador imediato de l, ento ns denimos a regio de inuncia de l, que denotamos por IR(l), como a unio de todos os caminhos l l p que contm l p exatamente uma vez e por conseguinte ao nal. Dizemos que uma varivel v pertencente a um programa P alcana um rtulo l se P contm dois rtulos ld e lu tais que: v denida por uma instruo em ld ; 48

B0 %i0 = ld v[%tid] %j0 = 0

B1 %i =(%i0, %j =(%j0, %p0 = %i < branch %p0

%i1) %j3) 100 B2

B5

sync %p2 = %j > 100 branch %p2 B7 B7 %x1 = 2

B6 %x0 = 1 jump B8

B2 %i1 = %i + 1 %j1 = %j + 1 %t0 = %j1 mod 2 %p1 = %t0 = 0 branch %p1 B4 B3 %j2 = %j1 - 3 B4 %j3 =(%j2, %j1) sync jump B1

B8 %x =(%x0, %x1) sync st %v[%tid] %x0 stop

Figura 1.36. Programa exemplo para ilustrar a anlise de divergncias.

v usada por uma instruo em lu ; P contm um caminho ld l; P contm um caminho l lu . Usaremos o programa da gura 1.36 para ilustrar os diversos conceitos expostos nesta seo. O ps-dominador imediato da instruo branch %p0 B2 , ao nal do bloco B1 , a instruo sync no incio do bloco B5 . A regio de inuncia deste desvio condicional compraz todas as instrues nos blocos bsicos B1 , B2 , B3 e B4 . A busca pelas variveis divergentes: Dados os conceitos sobre os quais falamos previamente, estamos agora prontos para encontrar as variveis divergentes em um programa. Com tal propsito, usaremos o Teorema 1.3.1, cuja prova pode ser en49

contrada on-line 3 . Teorema 1.3.1 ( VARIVEL D IVERGENTE) Uma varivel v P divergente se e somente se, uma das condies abaixo for verdadeira: 1. v = tid ou qualquer outra varivel que possui um valor pr-denido e particular para cada thread, como por exemplo laneId. 2. v denida por uma instruo atmica do tipo leia-modiqueescreva, como por exemplo atomic { v = *a; *a = *a + b }. 3. v possui uma dependncia de dados de alguma varivel divergente. 4. v possui uma dependncia de sincronizao de alguma varivel divergente. Antes de adentrar os pormenores de dependncias de dados e de sincronizao, procuremos entender os dois primeiros itens do Teorema 1.3.1. A varivel tid divergente por denio, uma vez que ela contm um valor diferente para cada thread. J uma incremento atmico como atomic { v = *a; *a = *a + 1 } executado exclusivamente por cada thread. Esta serializao faz com que cada thread obtenha um valor diferente para v, pois todas elas lem e alteram o mesmo endereo de memria e esta leitura no simultnea. Dado um programa P, a varivel v P possui uma dependncia de dados com relao varivel u P se P contm uma instruo que cria v e usa u. Por exemplo, na gura 1.36 ns temos que %i depende de %i1, devido funo no bloco B1 . Temos tambm que %i1 depende de %i, devido atribuio %i1 = %i + 1 no bloco B2 . O problema de determinar o fecho transitivo das dependncias de dados um tipo de fatiamento de programas [40] e ele pode ser resolvido por uma simples busca em grafo. Este grafo, convenientemente chamado grafo de dependncia de dados, denido da seguinte forma:
3

http://divmap.wordpress.com

50

para cada varivel v P, seja nv um vrtice de G; se P contm uma instruo que dene a varivel v e usa a varivel u, ento adicionamos uma aresta entre nu e nv . A m de encontrar o conjunto de variveis divergentes em P, ns comeamos a travessia do grafo de dependncias a partir de ntid , mais os vrtices que representam variveis denidas por instrues atmicas. Marcamos ento todos os outros nodos alcanveis a partir deste conjunto inicial como divergentes. Existem, contudo, variveis divergentes que no seriam capturadas por este algoritmo. Tratam-se das dependncias de sincronizao, que denimos abaixo: Denio 1.3.2 Dada uma instruo branch %p B, dizemos que v possui uma dependncia de sincronizao de %p se e somente se, o valor de v no ps-dominador imediato deste desvio condicional depende do resultado do predicado %p. A m de ilustrar este novo conceito, retornemos gura 1.36. A varivel %x iniciada por uma funo , cujo resultado depende de qual caminho utilizado pelo programa para alcanar o bloco B8 . Este caminho, por sua vez, depende do resultado do teste branch %p2 B7 . Assim, a varivel %x possui uma dependncia de sincronizao com relao ao predicado %p2. Existe um algoritmo simples para encontrar este tipo de dependncia e tal algoritmo resultado do Teorema 1.3.3, o qual, novamente, declaramos sem provar. Teorema 1.3.3 Seja branch %p B um desvio condicional e seja l p o ps-dominador imediato desta instruo. A varivel v dependente por sincronizao da varivel p se e somente se, v denida por uma instruo localizada dentro da regio de inuncia do desvio condicional e v alcana l p . De posse do Teorema 1.3.3 podemos transformar todas as dependncias de sincronizao em dependncias de dados. Para tanto utilizamos uma antiga e at certo ponto obscura, representao intermediria chamada formato SSA chaveado, (traduo livre de Gated SSA form GSA) [30]. Neste formato algumas 51

funes so aumentadas com predicados. No mostraremos este resultado aqui, mas somente variveis denidas por funes possuem dependncias de sincronizao. Logo, aumentando estas funes com os predicados que as controlam, estamos efetivamente adicionando uma dependncia de dados entre a varivel denida pela funo e estes predicados. Dizemos que uma funo aumentada com um predicado p chaveada por p. Por exemplo, abaixo temos uma funo chaveada pelos predicados p1 e p2 . Usando a nossa noo de dependncias de dados, pode-se dizer que v dependente destes dois predicados. v = (v1 , . . . , vn ), p1 , p2 O algoritmo na gura 1.37 converte um programa em formato SSA para um programa no formato GSA. Para um algoritmo mais eciente, recomendamos o trabalho de Tu e Padua [39]. A gura 1.38 mostra o resultado de executar o algoritmo da gura 1.37 no programa da gura 1.36. As funes em B4 e B8 foram marcadas no passo (1.a) de nosso algoritmo. A varivel %j3 dependente por sincronizao da varivel %p1 e a varivel %x dependente por sincronizao da varivel %p2. A funo de um parmetro em B5 foi criada pelo passo (1.b.i) do Algoritmo 1.37. Tendo chaveado as funes , as dependncias de dados passam a incorporar tambm as dependncias de sincronizao. Isto , ao criar uma instruo como v = (v1 , . . . , vn ), p1 , . . . , pk , ns estamos armando que v possui uma dependncia de dados no somente por vi , 1 i n, mas tambm pelos predicados p j , 1 j k. Dando sequncia ao nosso exemplo, a gura 1.39 mostra o grafo de dependncias criado para o programa chaveado da gura 1.38. Surpreendentemente, notamos que a instruo branch %p1 B4 no pode causar uma divergncia, ainda que o predicado %p1 possua uma dependncia de dados pela varivel %j1, que por sua vez criada dentro de um lao controlado por uma varivel divergente. Isto , a varivel %j1 no divergente, embora o seja a varivel %p0 que controla o lao em que %j1 criada. A varivel %j4, por outro lado, criada pelo passo (1.b.i) do al52

Algoritmo 1.37: chaveamento de programas em formato SSA para cada instruo branch p B cujo ps-dominador imediato l p faa: 1. para cada varivel v, denida em IR(p) e que alcana l p faa: (a) se v usada em l p como o parmetro de uma funo v = (. . . , v, . . .), chaveada ou no, ento substitua esta instruo por uma nova funo chaveada por p; (b) se v usada em uma instruo de atribuio x = f (. . . , v, . . .), em l p ou em algum rtulo lx que dominado por l p , ento faa: i. divida a linha de vida de v, inserindo uma nova instruo v = (v, . . . , v), p em l p , com um parmetro para cada predecessor de l p ; ii. renomeie cada cpia de v para v em l p , ou em qualquer rtulo dominado por l p ; iii. reconverta P para o formato SSA. Esta ao necessria devido nova denio de v.

Figura 1.37. Algoritmo que converte um programa em formato SSA em um programa em formato GSA.

goritmo 1.37, divergente, pois ela depende deste mesmo predicado %p0. O valor de %j4, na verdade, depende de quantas vezes as diferentes threads iro iterar sobre o lao controlado por %p0. Ainda que este fato no possa causar uma divergncia dentro do lao, conforme mostra o Algoritmo 1.37, a possibilidade de diferentes threads iterando sobre o lao quantidades diferentes de vezes pode levar a divergncias fora dele. Por exemplo, devido natureza divergente de %j4, o desvio condicional branch, %p2, B7 , tambm divergente, isto , threads que visitem este desvio podem tomar caminhos diferentes. 53

B0 %i0 = ld v[%tid] %j0 = 0

B1 %i =(%i0, %j =(%j0, %p0 = %i < branch %p0

%i1) %j3) 100 B2

B5

%j4 =(%j), %p0 sync %p2 = %j4 > 100 branch %p2 B7 B7 %x1 = 2

B6 %x0 = 1 jump B8

B2 %i1 = %i + 1 %j1 = %j + 1 %t0 = %j1 mod 2 %p1 = %t0 = 0 branch %p1 B4 B3 %j2 = %j1 - 3 B4 %j3 =(%j2, %j1), %p1 sync jump B1

B8 %x =(%x0, %x1), %p2 sync st %v[%tid] %x0 stop

Figura 1.38. Resultado de aplicar o algoritmo 1.37 ao programa da Figura 1.36.

Otimizaes de caminhos divergentes: Existem algumas tcnicas de otimizao de cdigo que usam as informaes produzidas pela anlise de divergncia para melhorar o desempenho de programas em face s divergncias. Algumas destas otimizaes so descritas abaixo. Realocao de threads Em presena de divergncias, podese reagrupar as threads entre diferentes warps, de modo que a maior parte dos warps contenha threads que tomem decises similares. Zhang et al. [43] implementaram a realocao de threads manualmente, obtendo ganhos de desempenho de at 37% em algumas aplicaes CUDA. De forma similar, Fung et al. [16] propuseram uma nova forma de realizar a realocao de threads ao nvel de hardware, inferindo, via simulao, ganhos de at 20% 54

j0 p1 j3 j2 t0 j1 j

x0 j4 p0 i1

x p2 i i0

x1

tid

Figura 1.39. O grafo de dependncias criado para o programa chaveado visto na gura 1.38. Variveis divergentes esto coloridas com cinza.

de desempenho. Otimizaes Peephole PTX, o assembly de CUDA, possui duas verses para vrias de suas instrues. Uma destas verses, que chamaremos unicada, considera que a instruo receber os mesmos dados para todas as threads, enquanto a outra, mais genrica, lida com dados divergentes. A utilizao de instrues unicadas sempre que possvel tende a levar a cdigos PTX mais ecientes. Compartilhamento de variveis Variveis no divergentes podem ser armazenadas em memria compartilhada. Ou seja, sabendo que uma certa varivel v no divergente, podemos remov-la dos registradores e armazen-la em memria compartilhada, possivelmente aumentando o nmero de threads que podem executar simultaneamente. Collange et al. [11] projetaram um mecanismo, ao nvel de hardware, que detecta variveis no divergentes e as realocam em memria compartilhada. Esta tcnica identica aproximadamente 19% dos valores lidos em registradores como no-divergentes. Fuso de caminhos divergentes Esta otimizao tenta juntar em um nico caminho de cdigo as instrues redundan55

tes em dois caminhos divergentes, a m de maximizar a quantidade de trabalho realizado pelas threads de um warp. Tal otimizao pode ser implementada de vrias maneiras. Ns descreveremos uma forma de fuso baseada no algoritmo de sequenciamento gentico conhecido como SmithWaterman. Otimizaes Peephole: Otimizaes peephole so uma categoria de melhorias de cdigo que consiste em substituir pequenas sequncias de instrues por outras, mais ecientes. A linguagem assembly PTX possui vrias instrues que admitem uma implementao mais eciente quando todas as threads executam tais instrues com os mesmos dados, isto , dados no divergentes. Um exemplo tpico a instruo de desvio condicional. A implementao de tais desvios, ao nvel de hardware, bastante complexa, anal preciso lidar com a diviso e a sincronizao de threads divergentes. Entretanto, tal complexidade no deveria ser imposta sobre testes condicionais no divergentes. Por isto, o conjunto de instrues fornecido por PTX contm uma instruo bra.uni, a qual pode ser usada na ausncia de divergncias. O manual de programao PTX contm o seguinte texto sobre tal instruo 4 :
All control constructs are assumed to be divergent points unless the control-ow instruction is marked as uniform, using the uni sufx. For divergent control ow, the optimizing code generator automatically determines points of re-convergence. Therefore, a compiler or code author targeting PTX can ignore the issue of divergent threads, but has the opportunity to improve performance by marking branch points as uniform when the compiler or author can guarantee that the branch point is non-divergent."

Assim como desvios condicionais, existem outras instrues que podem ser aumentadas com o prexo uni. Existem tambm instrues que no possuem anlogo no divergente: ldu uma instruo de carregamento uniforme, que supe que o endereo de origem dos dados no divergente, isto , todas as threads vem o mesmo endereo. call.uni uma chamada de funo no divergente. Chamadas de funo podem ser acrescidas de um predicado, de
4

PTX programmers manual, 2008-10-17, SP-03483-001_v1.3, ISA 1.3

56

forma tal que a funo ser chamada somente para aquelas threads cujo predicado seja verdadeiro. Esta instruo supe que tal predicado possui o mesmo valor para todas as threads. ret.uni instruo de retorno. Um retorno divergente suspende as threads at que todas elas tenham retornado ao uxo normal de execuo. Desta forma, diferentes threads podem terminar uma funo em momentos diferentes. Por outro lado, caso todas as threads terminem a funo juntas, o retorno unicado pode ser usado para melhorar a ecincia do programa. Compartilhamento de variveis: Quando a anlise de divergncias prova que uma varivel no divergente, ento esta varivel pode ser compartilhada entre todas as threads. Existem duas formas bsicas de compartilhamento: interno e externo. O compartilhamento interno consiste na alocao de variveis na rea de memria compartilhada da GPU. O compartilhamento externo uma alterao mais substancial do programa fonte, e consiste na migrao de trabalho realizado na GPU para a CPU. A principal vantagem do compartilhamento de dados a possvel diminuio da presso de registradores no kernel otimizado, o que tem o efeito benco de aumentar a quantidade de threads que podem usar o hardware grco simultaneamente. Recordando a discusso acerca de alocao de registradores na Seo 1.2, a GPU possui uma quantidade xa destas unidades de armazenamento; por exemplo 8,192 registradores em uma placa GTX 8800. Para que a placa alcance a ocupao mxima, estes registradores devem ser distribudos entre o teto de 768 threads que podem existir ao mesmo tempo. Assim, uma aplicao que requer mais de 10 registradores por thread no ser capaz de usar todas as threads possveis. Iniciaremos esta discusso explicando o compartilhamento interno de dados, o que faremos via o exemplo da gura 1.40. Nesta seo utilizaremos exemplos que, como este da gura 1.40, embora bastante articiais, tm a vantagem de ilustrar em poucas linhas nossas idias. O kernel em questo preenche as clulas de um vetor Out com valores calculados a partir das colunas de uma matriz In e mais um conjunto de quatro va57

_ _ g l o b a l _ _ void nonShared1 ( f l o a t In , f l o a t Out , i n t Width ) { i n t t i d = b l o c k I d x . x blockDim . x + t h r e a d I d x . x ; i f ( t i d < Width ) { Out [ t i d ] = 0 . 0 ; f l o a t a = 2 . 0F , b = 3 . 0F , c = 5 . 0F , d = 7 . 0F ; f o r ( i n t k = t i d ; k < Width Width ; k += Width ) { Out [ t i d ] += I n [ k ] / ( a b ) ; Out [ t i d ] = I n [ k ] / ( c d ) ; f l o a t aux = a ; a = b; b = c; c = d; d = aux ; } } }

Figura 1.40. Um kernel CUDA em que a presso de registradores 11, levando um tero da ocupao de threads ativas.

riveis. Este kernel usa 11 registrados, quando compilado via nvcc -O3 e, desta forma, utiliza apenas 2/3 das 768 threads disponveis. A anlise de divergncias revela que as variveis a, b, c e d possuem sempre os mesmos valores para todas as threads ativas. Logo, algumas destas variveis podem ser mantidas em memria compartilhada, conforme mostra a gura 1.41. Escolhemos mapear duas destas variveis, c e d, para as variveis compartilhadas common0 e common1. Note que evitamos condies de corrida fazendo com que somente a thread zero escreva sobre estas variveis. Como todas as threads escreveriam sempre o mesmo valor na rea de memria compartilhada, o acesso exclusivo no necessrio para a corretude do programa; porm, tal controle diminui tambm a utilizao do barramento de memria, aumentando a ecincia deste kernel. Conforme observamos no programa da gura 1.41, a thread 0 responsvel por realizar as computaes que alteram a rea de memria compartilhada. Existem situaes, contudo, em que possvel migrar tais computaes, totalmente ou em parte, para a CPU. Tal no possvel na gura 1.41, pois os dados compartilhados so usados em estgios intermedirios do cl58

_ _ g l o b a l _ _ void regPress2 ( f l o a t In , f l o a t Out , i n t Width ) { i n t t i d = b l o c k I d x . x blockDim . x + t h r e a d I d x . x ; i f ( t i d < Width ) { __shared__ f l o a t common0 , common1 ; f l o a t c = 5 . 0F ; f l o a t d = 7 . 0F ; i f ( t h r e a d I d x . x == 0 ) { common0 = 2 . 0F ; common1 = 3 . 0F ; } __syncthreads ( ) ; Out [ t i d ] = 0 . 0 ; f o r ( i n t k = t i d ; k < Width Width ; k += Width ) { Out [ t i d ] += I n [ k ] / ( common0 common1 ) ; Out [ t i d ] = I n [ k ] / ( c d ) ; f l o a t aux = common0 ; i f ( t h r e a d I d x . x == 0 ) { common0 = common1 ; common1 = c ; } __syncthreads ( ) ; c = d; d = aux ; } } }

Figura 1.41. Uso de variveis compartilhadas para diminuir a presso de registradores. Este cdigo 10% mais eciente que o kernel da gura 1.40 em uma GPU 9400M.

culo de valores no compartilhados. Entretanto, o exemplo da gura 1.42 ilustra uma situao diferente. Na gura 1.42, as variveis a, b, c e d so no divergentes. Mais ainda, valores intermedirios destas variveis no contribuem em nada para o clculo de variveis divergentes. Podese, portanto, extrair a fatia do programa responsvel pelo clculo destes valores no divergentes, de modo que eles possam ser calculados pela CPU, conforme podemos ver na gura 1.43. A vantagem, neste caso, que a CPU pode realizar estas computaes mais rapidamente que uma nica thread da GPU, como foi feito pela thread 0 na gura 1.41. O exemplo nal, mostrado o cdigo da GPU bem como o cdigo da CPU, mostrado na gura 1.43. Fuso de caminhos divergentes: Desvios condicionais do 59

_ _ g l o b a l _ _ void nonShared2 ( f l o a t In , f l o a t Out , i n t Width ) { i n t t i d = b l o c k I d x . x blockDim . x + t h r e a d I d x . x ; i f ( t i d < Width ) { Out [ t i d ] = 0 . 0 ; f l o a t a = 2 . 0F , b = 3 . 0F , c = 5 . 0F , d = 7 . 0F ; f o r ( i n t k = 0 ; k < Width ; k ++) { i n t i n d e x = t i d + ( k Width ) ; Out [ t i d ] += I n [ i n d e x ] / k ; Out [ t i d ] = I n [ i n d e x ] / ( k + 1 ) ; f l o a t aux = a ; a = b; b = c; c = d; d = aux ; } Out [ t i d ] / = ( a b ) ( c d ) ; } }

Figura 1.42. Este kernel, cuja presso de registradores 11, possui computaes que podem ser compartilhadas externamente GPU.

tipo if-then-else" podem ter muitas instrues em comum. A otimizao de cdigo chamada fuso de caminhos divergentes (traduo livre de branch fusion) consiste em mover estas instrues comuns para caminhos compartilhados do grafo de uxo de controle. A fuso de caminhos divergentes um tipo mais extensivo de eliminao parcial de redundncias [26]. Usaremos o exemplo da gura 1.44 para explicar esta otimizao. Podemos ver que as sequncias de instrues que comeam nos rtulos l4 e l13 possuem muitas operaes e alguns operandos em comum. Representemos uma instruo store por e uma instruo load por . Podemos assim representar as duas sequncias de instrues nos caminhos divergentes da gura 1.44 (a) por T = {, , , , , /, /, , +, } e F = {, , , , /, , , +, }. Este exemplo levanta trs questes, as quais enunciamos abaixo. 1. Seria vantajoso unicar partes destes caminhos divergentes em termos de tempo de execuo? 2. Quais os trechos de cdigo que mais benefcio trariam em caso destes serem unicados? Caso estejamos ape60

_ _ g l o b a l _ _ void s h a r e d E x t e r n a l l y ( f l o a t In , f l o a t Out , i n t Width , f l o a t alpha ) { i n t t i d = b l o c k I d x . x blockDim . x + t h r e a d I d x . x ; i f ( t i d < Width ) { Out [ t i d ] = 0 . 0 ; f o r ( i n t k = 0 ; k < Width ; k ++) { i n t i n d e x = t i d + ( k Width ) ; Out [ t i d ] += I n [ i n d e x ] / k ; Out [ t i d ] = I n [ i n d e x ] / ( k + 1 ) ; } Out [ t i d ] / = alpha ; } } / / P a r t e do programa executada na CPU: f l o a t a = 2 . 0F , b = 3 . 0F , c = 5 . 0F , d = 7 . 0F ; f o r ( i n t k = 0 ; k < m a t r i x _ s i d e _ s i z e ; k ++) { f l o a t aux = a ; a = b; b = c; c = d; d = aux ; } f l o a t alpha = ( a b ) ( c d ) ; s h a r e d E x t e r n a l l y <<< g , b >>>( In , Out , Width , alpha ) ;

Figura 1.43. Compartilhamento externo de valores no divergentes.

nas procurando as sequncias mais longas, ento a gura 1.44 (b) mostra uma possvel resposta. 3. Existe alguma forma sistemtica de unicarmos estes caminhos, produzindo assim um programa equivalente ao cdigo original? Em nosso exemplo, a gura 1.44 (c) mostra uma possvel verso do programa unicado. Usamos operadores ternrios (sel) para escolher os operandos fontes de instrues unicadas. Em arquiteturas em que tais operadores no existam, pode-se recorrer a converso de desvios (if-conversion) [20, 33]. No restante desta seo tentaremos responder a cada uma destas questes. Smith-Waterman: Atendo-nos primeira questo levantada, precisamos encontrar quais os trechos de cdigo cuja unicao maior benefcio trariam ao desempenho do programa otimizado. Em geral existem muitos diferentes programas que realizam a mesma computao, e encontrar a mais eciente dentre tantas 61

(a)

B1 t0 = ld v[tid] p0 = t0 0.0 branch p0, B13


T F

(c)

B4 t1 = ld a[tid] t2 = t1 * t1 t3 = t2 * t1 t4 = t3 * 3.14 t5 = t4 / t0 t6 = t5 / t0 t7 = t1 * 2.71 t8 = t6 + t7 st a[tid] t8

B13 t9 = ld a[tid] t10 = t9 * t9 t11 = t10 * 3.14 t12 = t11 / 2.00 t13 = t9 * 2.71 t14 = t13 * t9 t15 = t12 + t14 st a[tid] t15 jump B22

l1 load(t0, tid) p0 = t0 0.0 load(t1-9, tid) t2-10 = t1-9 * t1-9 s1 = sel(p0, t2-10, 3.14) t3-11 = t2-10 * s1 branch(p0, l10) t4 = t3-11 * 3.14 t5 = t4 / t0 l10 t5-3-11 = (t5, t3-11) sync s3 = sel(p0, t0, 2.0) t6-12 = s5-3-11 / s3 t7-13 = t1-9 * 2.71 branch(p0, l16) l15 t = t 14 7-13 * t1-9

l8

B22 sync stop

(b)

l16
T =

/ /

+ +

F =

t7-14 = (t7-13, t14) sync t8-15 = t6-12 + t7-14 store(t8-15, tid)

Figura 1.44. (a) The example that we use to explain branch fusion. (b) A possible instruction alignment. (c) Code after branch fusion.

verses um problema muito difcil. Por exemplo, se pudssemos re-ordenar as instrues em um caminho divergente talvez fosse possvel produzir uma sequncia de unicaes mais eciente. Todavia, o espao de buscas neste caso seria enorme. Iremos supor ento que no podemos transformar o programa a m de obter melhores sequncias para unicao. Em vez disto, trabalharemos sobre um problema mais simples, o qual chamaremos de alinhamento bi-dimensional de instrues, o qual denimos abaixo: Denio 1.3.4 A LINHAMENTO BI - DIMENSIONAL DE INSTRUES Instncia: dados os seguintes parmetros: dois arranjos de instrues, T = {i1 , . . . , in } e F = { j1 , . . . , jm }; 62

uma funo de lucro s, tal que s(i, j) seja o lucro de unicar as instrues i e j; uma funo de custo c, tal que c(i, j) seja o custo de unicar as instrues i e j; b, o custo constante que advm de quebras entre sequncias unicadas. Isto , b deve ser pago sempre que houver discontinuidades entre trechos de cdigo a serem unicados. Em geral b o custo de se inserir desvios condicionais dentro das sequncias unicadas. Problema: encontre uma sequncia ordenada de pares A = (x1 , y1 ), ..., (xk , yk ) , tais que: se (x, y) A, ento 1 x n, 1 y m se r > s ento xr xs se r > s ento yr ys (s(x, y) c(x, y)) b G mximo, sendo (x, y) A, e G o nmero de descontinuidades nas sequncias de unicaes. Existe um lucro e um custo, em termos de ciclos de execuo, para unicar um par de instrues, o que normalmente derivamos do manual do programador. Por exemplo, se uma instruo de load custa 100 ciclos, ento o casamento de duas delas nos economizaria este custo em um caminho divergente. Por outro lado, a m de efetuar tal unicao pode ser necessrio que tenhamos de inserir seletores no programa, os quais adicionam tempo de processamento ao cdigo modicado. A gura 1.45 ilustra as operaes necessrias para unicar duas instrues de diviso localizadas em caminhos divergentes do programa visto na gura 1.44 (a). esquerda temos parte do programa original e direita temos o programa aps a fuso. O custo que acabamos de mencionar inclui a seleo dos parmetros da nova instruo (2cs ) e a execuo da operao de diviso (c/ ). Quando o custo dos seletores menor que o lucro da unicao, dizemos que tal transformao lucrativa. No exemplo da gura 1.45, temos um lucro s = 2c/ 2cs c/ = c/ 2cs . Observe que se um dos operandos nas duas instrues unicadas 63

branch p0, B13 c/: t6 = t5 / t0 c/: t12 = t11 / 2.00

cs: s2 = sel(p0, t5, t3-11) cs: s3 = sel(p0, t0, 2.0) c/: t6-12 = s2 / s3
cost = 2cs + c/

cost = 2c/

Figura 1.45. Comparao entre o custo de unicao e execuo divergente.

for o mesmo, ento este operando no demanda um seletor e seu custo abatido do lucro da unicao. Podemos casar instrues que no so necessariamente as mesmas. Por exemplo, possvel unicar uma adio e uma multiplicao em arquiteturas que provem uma instruo do tipo multiply-add, que permite efetuar ambas as operaes. De modo semelhante, podemos unicar instrues de comparao tais como maior-que" e menor-que" via transformaes simples de seus operandos. Na verdade, possvel levar este abuso de identidades entre instrues ao extremo, usando tcnicas mais avanadas, como a saturao de igualdades [38], que normaliza todas as instrues em um trecho de cdigo. O problema de encontrar sequncias similares de instrues muito similar ao problema de sequenciamento gentico: dadas duas sequncias de genes, pede-se pelas sequncias mais longas de genes em comum. Tal problema, j muito estudado em biologia, possui uma soluo muito elegante: o algoritmo de sequencialmento de Smith e Waterman [34]. Ns utilizaremos este mesmo algoritmo para determinar as sequncias mais lucrativas de unicaes. Este algoritmo possui dois passos: primeiro, constri-se uma matriz de ganhos que mostra o lucro de unicar cada possvel par de instrues. Segundo, encontra-se nesta matriz a sequncia de unicaes mais lucrativa. A m de construir a matriz de ganhos H ns escrevemos uma das sequncias de instrues ao longo da linha superior da matriz e escrevemos a outra ao longo da coluna mais esquerda da matriz. Cada posio da matriz associada a um 64

valor g, isto : H[i, j] = g, onde g o mximo lucro obtido a partir de qualquer forma possvel de unicar instrues at os ndices i e j. Para calcular H[i, j] ns usamos a frmula abaixo, sendo o signicado dos parmetros s, c e b os mesmos da Denio 1.3.4: H[i 1, j 1] c(i, j) b, H[i, j 1], H[i, j] = s(i, j) + MAX H[i 1, j], 0 Continuando com nosso exemplo, a gura 1.46 mostra a matriz de ganhos que construmos para o programa da gura 1.44 (a). Estamos usando a seguinte funo de lucratividade: s(, ) = s( , ) = 100, s(, ) = 2, s(/, /) = 8, s(+, +) = 2. Supomos o custo de descontinuidade b = 2. Note que este custo pago somente uma vez por descontinuidade, quando deixamos uma sequncia diagonal de clulas mais lucrativas e caminhamos para uma posio vertical ou horizontal na matriz. Por simplicidade estamos fazendo c = 0. Tendo construdo a matriz de ganhos, entramos uma soluo para o problema do alinhamento de instrues em dois passos. Primeiro visitamos as clulas da matriz para encontrar a posio H[x, y] com o maior lucro. Depois atravessamos a matriz, comeando por H[x, y] e indo em direo posio H[0, 0], seguindo o sentido em que cada clula foi atualizada durante a construo da matriz. Isto , se H[i, j] foi atualizada a partir de H[i 1, j 1], ento ns continuamos nosso passeio a partir desta ltima clula. Em nosso exemplo, a clula de maior ganho H[9, 8] e a sequncia mais lucrativa A = (1, 1), (2, 2), (3, 3), (4, 3), (5, 4), (6, 4), (7, 5), (7, 6), (8, 7) , (9, 8) . Na gura 1.46 ns aumentamos cada clula da matriz de ganho com a direo de sua atualizao. As posies coloridas em cinza marcam o caminho mais lucrativo dentro desta matriz. Este caminho denota exatamente o alinhamento visto na gura 1.44 (c). Damos s atualizaes horizontais e verticais preferncia sobre atualizaes diagonais. Em outras palavras, quando o ganho de unicar duas instrues for o mesmo de 65

0 1 2 3 4 5 6 7 8 9

2 0 0 0 0 0 0 0 0

100 98 98 98 98 98 98 98

* * * / / * +
0

0 0 0

98

102 100 100 100 100 100 100

98

100 100

104 102 102 102 102 102


102
3 4

104 104 104 104

0 0 0 0 0 0

98 98

102 102

100

102

110 108 108 108 108 110 108 108 108 108

100

98 98 98 98
1

102 102 102

112 110 110 110

100 100 100


2

108 108

110 110
5

113

112 110

108

111
6 7

110

Figura 1.46. A matriz de ganhos produzida para o programa da gura 1.44 (a).

manter estas instrues em caminhos divergentes, escolhemos a segunda opo. Desta forma tendemos a diminuir o nmero de descontinuidades na sequncia alinhada, evitando a insero de desvios condicionais no cdigo alvo. Por exemplo, na gura 1.46, poderamos ter atualizado H[2, 4] a partir de H[1, 3]. Entretanto, a unicao das duas prximas instrues multiplicao e diviso possui um lucro muito pequeno. Assim, seramos forados a inserir um desvio condicional no cdigo. Por outro lado, dado que a atualizao de H[2, 4] deu-se por um caminho vertical, o custo da insero deste desvio j foi pago anteriormente. 66

212
8

1.4. O Caldeiro no Final do Arco-ris


Chegamos assim ao m deste curso. Espero que vocs tenham aprendido novos conceitos e que estes venham a lhes ser teis de alguma forma. A otimizao de cdigo no uma rea nova de pesquisa. Ao contrrio, desde os primeiros passos neste campo, dados por pioneiros como John Backus [6], muito j foi feito, tanto pela indstria quanto pela academia. Ainda assim, as GPUs, com seu modelo de execuo ainda pouco conhecido, traz novos desaos para os projetistas de compiladores e para os desenvolvedores de aplicaes de alto desempenho. Existem muitas maneiras de manter-mo-nos atualizados sobre esta rpida evoluo tecnolgica. Recomendo principalmente as boas conferncias e simpsios. Atualmente no difcil encontrar verses dos artigos mais importantes na Internet. Como existem muitas conferncias de ponta sobre esta linha de pesquisa, eu separei uma lista de minhas preferncias: PLDI Esta a maior conferncia acerca do projeto e da implementao de linguagens de programao. Linguagens heterogneas e paralelismo de dados so constantes nesta conferncia e aos poucos artigos especcos sobre GPUs comeam a encontrar espao em seus anais. A ttulo de exemplo, temos o compilador otimizante de Yang et al. [42]. POPL Outra conferncia de primeira linha. Trabalhos publicados em POPL tendem a ser mais tericos que aqueles vistos em PLDI. Entre os artigos pertinentes, temos a implementao da anlise de dataow em CUDA [31]. PPoPP Esta conferncia mais especializada em linguagens e compiladores para processamento paralelo. Muitos artigos importantes sobre GPUs j foram publicados em PPoPP, incluindo as tcnicas de Ryoo et al. [32] que eu usei em nosso exemplo de multiplicao de matrizes. PACT Outra conferncia bem conhecida sobre ferramentas e compiladores para programao paralela. Entre os artigos encontrados em PACT, recomendo o trabalho de Diamos et al., que descreve Ocelot, um otimizador de cdigo PTX [14]. 67

MICRO A principal conferncia de arquitetura e organizao de computadores. GPUs foram j o assunto principal de muitos artigos de MICRO. Por exemplo, o trabalho de Fung et al. [16] mostra como reconvergir threads divergentes. CGO e CC Duas conferncias especcas de compiladores. Uma verso inicial da anlise de divergncias foi apresentada em CGO [36]. Alm das conferncias existem muitas listas de discusso, revistas eletrnicas e blogs que discorrem acerca de GPUs e CUDA e muitos internautas sentir-se-o felizes em poder sanar dvidas de outrem. Certamente ser muito interessante descobrir onde toda esta nova tecnologia nos levar. Como tudo em Cincia da Computao, o limite de nossos avanos a nossa prpria criatividade!

68

Referncias
[1] Alfred V. Aho, Monica S. Lam, Ravi Sethi, and Jeffrey D. Ullman. Compilers: Principles, Techniques, and Tools (2nd Edition). Addison Wesley, 2006. [2] Alexander Aiken and David Gay. Barrier inference. POPL, pages 342354. ACM Press, 1998. In

[3] Andrew W. Appel and Jens Palsberg. Modern Compiler Implementation in Java. Cambridge University Press, 2nd edition, 2002. [4] Vrios autores. NVIDIA CUDA Compute Uned Device Architecture Programming Guide. NVIDIA, 1.0 edition, 2007. [5] Vrios autores. NVIDIA CUDA C Programming Best Practices Guide CUDA Toolkit 2.3. NVIDIA, 1.0 edition, 2009. [6] John Backus. The history of fortran i, ii, and iii. SIGPLAN Not., 13(8):165180, 1978. [7] Sara S. Baghsorkhi, Matthieu Delahaye, Sanjay J. Patel, William D. Gropp, and Wen-mei W. Hwu. An adaptive performance modeling tool for gpu architectures. In PPoPP, pages 105114. ACM, 2010. [8] Kenneth. E. Batcher. Sorting networks and their applications. In AFIPS, pages 307314. ACM, 1968. [9] Daniel Cederman and Philippas Tsigas. GPU-quicksort: A practical quicksort algorithm for graphics processors. Journal of Experimental Algorithmics, 14(1):424, 2009. [10] Pohua P. Chang, Scott A. Mahlke, and Wen-mei W. Hwu. Using prole information to assist classic code optimizations. Software Practice and Experience, 21(12):13011321, 1991. [11] Sylvain Collange, David Defour, and Yao Zhang. Dynamic detection of uniform and afne vectors in GPGPU computations. In HPPC, pages 4655. Springer, 2009. [12] Bruno Coutinho, Diogo Sampaio, Fernando Magno Quint ao Pereira, and Wagner Meira Jr. Performance debugging of GPGPU applications with the divergence map. In SBACPAD, pages 33 44. IEEE, 2010. 69

[13] Ron Cytron, Jeanne Ferrante, Barry K. Rosen, Mark N. Wegman, and F. Kenneth Zadeck. An efcient method of computing static single assignment form. In POPL, pages 2535, 1989. [14] Gregory Diamos, Andrew Kerr, Sudhakar Yalamanchili, and Nathan Clark. Ocelot, a dynamic optimization framework for bulk-synchronous applications in heterogeneous systems. In PACT, pages 354364, 2010. [15] Stijn Eyerman and Lieven Eeckhout. Per-thread cycle accounting in smt processors. ASPLOS, 44(3):133144, 2009. [16] Wilson Fung, Ivan Sham, George Yuan, and Tor M. Aamodt. Dynamic warp formation and scheduling for efcient GPU control ow. In MICRO, pages 407420. IEEE, 2007. [17] Susan L. Graham, Peter B. Kessler, and Marshall K. McKusick. gprof: a call graph execution proler (with retrospective). In Best of PLDI, pages 4957, 1982. [18] Mark Harris. The parallel prex sum (scan) with CUDA. Technical Report Initial release on February 14, 2007, NVIDIA, 2008. [19] Minwen Ji, Edward W. Felten, and Kai Li. Performance measurements for multithreaded programs. In SIGMETRICS, pages 161170. ACM, 1998. [20] Ken Kennedy and Kathryn S. McKinley. Loop distribution with arbitrary control ow. In Supercomputing, pages 407 416. IEEE, 1990. [21] M. S Lam, E. E. Rothberg, and M. E. Wolf. The cache performance and optimizations of blocked algorithms. In ASPLOS, pages 6374. ACM, 1991. [22] Victor W Lee, Changkyu Kim, Jatin Chhugani, Michael Deisher, Daehyun Kim, Anthony D Nguyen, Nadathur Satish, Mikhail Smelyanskiy, Srinivas Chennupaty, Per Hammarlund, Ronak Singhal, and Pradeep Dubey. Debunking the 100X GPU vs. CPU myth: an evaluation of throughput computing on CPU and GPU. In ISCA, pages 451460. ACM, 2010. 70

[23] A. W. Lim and M. S. Lam. Maximizing parallelism and minimizing synchronization with afne transforms. In POPL, pages 201214, 1997. [24] Peter S. Magnusson, Magnus Christensson, Jesper Eskilson, Daniel Forsgren, Gustav Hallberg, Johan Hogberg, Fredrik Larsson, Andreas Moestedt, and Bengt Werner. Simics: A full system simulation platform. Computer, 35:50 58, 2002. [25] Barton Miller, Mark Callaghan, Jonathan Cargille, Jeffrey Hollingsworth, Bruce Irvin, Karen Karavanic, Krishna Kunchithapadam, and Tia Newhall. The paradyn parallel performance measurement tool. Computer, 28:3746, 1995. [26] E. Morel and C. Renvoise. Global optimization by suppression of partial redundancies. Commun. ACM, 22(2):96 103, 1979. [27] Todd Mytkowicz, Amer Diwan, Matthias Hauswirth, and Peter F. Sweeney. Evaluating the accuracy of java prolers. In PLDI, pages 187197. ACM, 2010. [28] Nicholas Nethercote and Julian Seward. Valgrind: a framework for heavyweight dynamic binary instrumentation. In PLDI, pages 89100. ACM, 2007. [29] John Nickolls and David Kirk. Graphics and Computing GPUs. Computer Organization and Design, (Patterson and Hennessy), chapter A, pages A.1 A.77. Elsevier, 4th edition, 2009. [30] Karl J. Ottenstein, Robert A. Ballance, and Arthur B. MacCabe. The program dependence web: a representation supporting control-, data-, and demand-driven interpretation of imperative languages. In PLDI, pages 257271. ACM, 1990. [31] Tarun Prabhu, Shreyas Ramalingam, Matthew Might, and Mary Hall. EigenCFA: Accelerating ow analysis with GPUs. In POPL. ACM, 2011. [32] Shane Ryoo, Christopher I. Rodrigues, Sara S. Baghsorkhi, Sam S. Stone, David B. Kirk, and Wen mei W. Hwu. Optimization principles and application performance evaluation of 71

a multithreaded gpu using cuda. In PPoPP, pages 7382. ACM, 2008. [33] Jaewook Shin. Introducing control ow into vectorized code. In PACT, pages 280291. IEEE, 2007. [34] Temple F Smith and Michael S Waterman. Identication of common molecular subsequences. Journal of Molecular Biology, 147(1):195 197, 1981. [35] Amitabh Srivastava and Alan Eustace. Atom: a system for building customized program analysis tools. In PLDI, pages 196205. ACM, 1994. [36] John A. Stratton, Vinod Grover, Jaydeep Marathe, Bastiaan Aarts, Mike Murphy, Ziang Hu, and Wen-mei W. Hwu. Efcient compilation of ne-grained SPMD-threaded programs for multicore CPUs. In CGO, page 111. IEEE, 2010. [37] Nathan R. Tallent and John M. Mellor-Crummey. Effective performance measurement and analysis of multithreaded applications. PPOPP, 44(4):229240, 2009. [38] Ross Tate, Michael Stepp, Zachary Tatlock, and Sorin Lerner. Equality saturation: a new approach to optimization. In POPL, pages 264276. ACM, 2009. [39] Peng Tu and David Padua. Efcient building and placing of gating functions. In PLDI, pages 4755. ACM, 1995. [40] Mark Weiser. Program slicing. In ICSE, pages 439449. IEEE, 1981. [41] Zhichen Xu, Barton P. Miller, and Oscar Naim. Dynamic instrumentation of threaded applications. In PPoPP, pages 4959. ACM, 1999. [42] Yi Yang, Ping Xiang, Jingfei Kong, and Huiyang Zhou. A GPGPU compiler for memory optimization and parallelism management. In PLDI, pages 8697. ACM, 2010. [43] Eddy Z. Zhang, Yunlian Jiang, Ziyu Guo, and Xipeng Shen. Streamlining GPU applications on the y: thread divergence elimination through runtime thread-data remapping. In ICS, pages 115126. ACM, 2010.

72