Identificação de Paralelismo Aleardo Manacero Jr.
Paralelização de programas Já vimos como identificar pontos em que não se pode paralelizar tarefas Entretanto falta examinar como identificar pontos passíveis de paralelização e como implementar o paralelismo em um programa
Paralelização de programas Um programa pode ser paralelizado, sempre que for conveniente, por dois operadores distintos: Pelo compilador Pelo programador
Paralelismo pelo compilador Usualmente é chamado de ILP (Instruction Level Parallelism) Deve identificar dependências e fazer a paralelização quando, comprovadamente, tivermos códigos independentes Mais factível para máquinas de memória compartilhada (multiprocessadores), pela ausência de código para troca de mensagens
Paralelismo pelo compilador O compilador atua se receber parâmetros específicos de paralelização O usuário pode (e deve) interferir no processo, dirigindo (ou auxiliando) o compilador na identificação dos pontos para paralelização
Exemplos DO I = 1,N A(I) = A(I+K) * B(I) ENDDO É paralelizável se K>-1 DO I = 1,N A(K(I)) = A(K(J)) + B(I) * C ENDDO É paralelizável se K(I) K(J) p/ I,J:1,N e I J
Exemplos DO I = 1,N CALL BIGSTUFF(A,B,C,I,J,K) ENDDO É paralelizável se a função é independente DO I = L,N A(I) = B(I) + C(I) ENDDO É paralelizável se for executado muitas vezes
Paralelismo pelo programador Realizado normalmente para ambientes de memória distribuída (multicomputadores) Neles é o usuário (programador) quem deve identificar e implementar o paralelismo A forma em que o paralelismo ocorrerá depende, fundamentalmente, da estrutura de conexão entre processadores
Paralelismo pelo programador A dependência da estrutura de conexão faz com que o tempo gasto com comunicação limite: o tamanho do grão de processamento o grau de paralelismo da máquina
Comunicação X tamanho do grão Se entre cada grão for necessário trocar valores (os conjuntos de saída do primeiro grão e de entrada do segundo), então o grão deve ser suficientemente grande para compensar o tempo gasto com comunicação
Grau de paralelismo Se a velocidade no recebimento de novos dados for menor do que a capacidade de processamento, então não se deve aumentar o grau de paralelismo (aumentaria a ociosidade dos elementos de processamento)
Paralelismo pelo programador A paralelização de um programa deve sempre levar as restrições de grau de paralelismo e tamanho de grão em consideração Com isso, a identificação do paralelismo possível passa a ser uma tarefa de dimensionamento do ponto ótimo de particionamento do problema
Contra-exemplos Sistemas com dominação de E/S: Aqui temos a restrição do tamanho do grão Quando existe dominação de E/S (pouco processamento, muita E/S) fica impossível paralelizar o sistema, exceto numa hipótese remota de comunicação em altíssima velocidade
Contra-exemplos Sistemas iterativos de corpo curto: Aqui temos a restrição de grau de paralelismo Nos casos em que o corpo de um processo iterativo é muito curto (poucas operações, levando a ociosidade entre iterações), então é também impossível a paralelização
Modelos de particionamento Quando a paralelização de um programa é feita pelo programador é necessário definir qual o modelo de particionamento a ser utilizado
Modelos de particionamento Modelos de particionamento são modelos que definem como o processamento paralelo ocorrerá e de que forma os programas paralelos irão interagir
Modelos de particionamento A decisão sobre qual modelo deve ser utilizado depende de: características do problema características da máquina O que significa depender de qual é o melhor tipo de casamento entre software e hardware
Modelos de particionamento Um caso clássico desse casamento surge na multiplicação de matrizes, em que o caminho entre linhas e colunas não é feito sempre da mesma forma, como visto na próxima figura
Multiplicação de matrizes
Multiplicação de matrizes Nesse caso o particionamento do problema deve minimizar a necessidade de se percorrer muitas linhas ou colunas em ordem inversa Uma forma de se fazer isso é multiplicar a matriz através de blocos
Multiplicação em blocos
Multiplicação em blocos Para os casos em que as matrizes se tornam grandes demais deve-se considerar a possibilidade de outras formas de divisão em blocos Essas formas devem, sempre, levar em consideração o tamanho dos blocos de cache
Multiplicação em blocos
Modelos de particionamento Pode-se considerar a existência de três modelos básicos de particionamento, que são: Saco de Tarefas (Bag-of-Tasks) Batimento (Heartbeat) Linha de Produção (Pipeline)
Modelo bag of tasks Implementado através de algoritmos centralizados, em que um processo assume o controle e os demais fazem o processamento de fato A idéia de saco de tarefas vem da forma de operação, em que os trabalhadores buscam novas tarefas com o coordenador (que tem o saco)
Modelo bag of tasks As tarefas são usualmente novos dados a serem processados pelo trabalhador Após a manipulação desses dados, o trabalhador devolve os resultados ao coordenador O sincronismo entre coordenador e trabalhadores é feito por requisições dos trabalhadores
Modelo bag of tasks A interação entre trabalhadores deve ser evitada, diminuindo assim a necessidade de comunicação entre esses processos Uma consequência desse modelo é o balanceamento da carga em cada trabalhador, pois novas tarefas apenas são solicitadas após a conclusão da atual
Funcionamento do modelo
Funcionamento do modelo
Funcionamento do modelo
Funcionamento do modelo
Modelo heartbeat Nesse modelo o gerenciamento é normalmente descentralizado Isso exige um volume maior de comunicação e de interação entre os programas paralelos
Modelo heartbeat O nome do modelo vem de sua estrutura de funcionamento, que imita de certo modo os batimentos cardíacos Cada processador trabalha em três fases (expansão, contração e processamento), que se repetem iterativamente até se chegar à solução
Modelo heartbeat Durante a fase expansão ocorre o envio de dados por todos os processos
Funcionamento do modelo
Modelo heartbeat Durante a fase expansão ocorre o envio de dados por todos os processos Na contração cada processo recebe os dados enviados pelos demais processos
Funcionamento do modelo
Modelo heartbeat Durante a fase expansão ocorre o envio de dados por todos os processos Na contração cada processo recebe os dados enviados pelos demais processos Na fase de processamento ocorre, obviamente, o processamento dos dados recém-recebidas
Funcionamento do modelo
Modelo pipeline Nesse modelo simula-se, em software, a operação de um pipeline de hardware Nesse caso cada elemento de processamento executa ações sobre os dados de um problema, de forma que os resultados sejam a entrada para o próximo elemento de processamento
Modelo pipeline Sistemas pipeline se prestam bem para problemas como filtros de ordenação, em que cada processador faz o merge (intercalação) da operação realizada em outros dois processadores Outra aplicação interessante é a implementação de redes neurais paralelas
Modelo pipeline Pipelines podem ser de três tipos: Aberto Fechado sem coordenador Fechado com coordenador
Funcionamento do modelo
Funcionamento do modelo
Funcionamento do modelo
Programando em paralelo Além da forma de atribuição de atividades aos elementos de processamento podemos pensar em como as atividades se relacionam Nesse sentido temos: Tarefas independentes Tarefas em workflow
Tarefas independentes Qualquer dos modelos anteriores pode ser usado Bag of Tasks se encaixa perfeitamente para essas tarefas Normalmente resulta no que se chama embarrasingly parallel system
Tarefas em workflow Qualquer dos modelos anteriores pode ser usado O problema aqui é quando uma tarefa se torna apta a ser executada Demanda cuidados com sincronismo
Programando em paralelo Além dos modelos de particionamento podemos definir estratégias na forma de programar Nesse sentido podemos pensar em: Paralelismo por dados Paralelismo por eventos/tarefas
Paralelismo por tarefas Aparece tipicamente quando se programa em sistemas MIMD, com cada elemento de processamento executando tarefas diferentes para dados iguais ou diferentes Enfatiza a natureza distribuída de processamento
Paralelismo por dados Aqui o programa paralelo é construído considerando que em todos os elementos de processamento se executa o mesmo código, sobre dados diferentes É a estratégia básica para SIMD/SPMD É também a estratégia em MapReduce
Hadoop MapReduce Projetado para tarefas independentes, executadas em batch Grandes quantidades de dados, contidos em um DFS Framework faz a distribuição dos dados e agrupamento das respostas
Hadoop MapReduce Sua ideia básica é dividir o problema em duas fases: Uma embaraçosamente paralela, que é a função map Outra de composição, dada pela função reduce Estrutura básica de dados formada por pares chave-valor
Hadoop MapReduce
Hadoop MapReduce class Mapper method Map(doc_id a, doc d) for all term t in doc d do Emit (term t, count 1) class Reducer method Reduce(term t, counts[c1, c2, ]) sum = 0 for all count c in counts[c1,c2, ] do sum = sum + c Emit (term t, count sum)
Transactional Memory Busca aplicar a ideia de transações (de bancos de dados) na paralelização Conceito proposto na década de 80 Tarefas em paralelo são executadas como se fossem independentes e sua validade é verificada no final (commit)
Transactional Memory A principal vantagem é a abordagem otimista em relação aos conflitos em regiões críticas Desvantagens aparecem quando esse otimismo não se confirma
Transactional Memory Primeiros usos vieram com implementações em software (STM), nos anos 90 Linguagens como Haskell, Scala, Clojure, C/C++ (gcc 4.7) apresentam construções para a programação de STM
Transactional Memory Implementações em hardware surgiram em 2007 Sun sparc v9, mas era ineficiente e com alto consumo de energia PowerPC A2 (2011), com HTM operando na cache L2
Transactional Memory Maiores avanços nesta década Power 8 (2013), também operando em cache, com tratamento de exceções por software Intel Haswell (2013), com HTM operando na cache L1, usando HLE (hardware lock ellision) e RTM (restricted TM),
Transactional Memory Lock ellision é uma técnica em que se executa a região crítica como uma transação e, caso não ocorra o commit dela, então bloqueia a thread que a executava Próximo slide ilustra uma possível implementação de lock ellision
Transactional Memory void elided_lock_wrapper(lock) { if (_xbegin() == _XBEGIN_STARTED) { /* Start transaction */ if (lock is free) /* Check lock and put into read-set */ return; /* Execute lock region in transaction */ } _xabort(0xff); /* Abort transaction as lock is busy */ } /* Abort comes here */ take fallback lock void elided_unlock_wrapper(lock) { if (lock is free) _xend(); /* Commit transaction */ else unlock lock; }
Considerações finais O paralelismo existente em cada problema varia bastante, exigindo sempre uma análise cuidadosa de como tal problema pode ser resolvido A solução desse problema depende, na prática, da identificação da melhor forma de particionamento do problema
Considerações finais Felizmente existem poucas formas básicas de particionamento, que são facilmente mapeáveis aos problemas típicos de computação de alto desempenho A escolha pelo modelo correto implica em grandes ganhos de eficiência e desempenho.