ATIVIDADE 1 - GCOM - GESTÃO DA INFORMAÇÃO - 54_2024.docx
II EPI - Estudo e Avaliação do Problema de Otimização da Multiplicação de Cadeias de Matrizes
1. Estudo e Avaliação do Problema de
Otimização da Multiplicação de
Cadeias de Matrizes
Eduardo de Lucena Falcão
Alisson Vasconcelos de Brito
Alexandre Nóbrega Duarte
2. Sumário
•Introdução
•O Problema da Multiplicação de Cadeias de Matrizes
•O Problema do Cache Miss
•Análise e Detalhamento de Possíveis Soluções para o Problema de
Multiplicação de Cadeias de Matrizes
•Abordagem Sequencial
•Abordagem com Programação Dinâmica
•Abordagem com Minimização do Cache Miss
•Abordagem Distribuída com Threads
•Abordagem Distribuída com OpenMP
•Resultados
•Conclusão
•Referências
4. Introdução
- Além de aspectos de hardware como frequência do
processador e tamanho da memória, é importante
utilizar as estruturas de dados e técnicas de algoritmos
que mais se adéquam ao estilo de problema a ser
resolvido. Adicionalmente, podemos perceber que as
escolhas de ferramentas, Linguagens de Programação
e APIs também podem ter certo impacto no
desempenho da solução.
5. Introdução
-O objetivo do presente trabalho é tornar evidentes os
vários fatores que podem influenciar no desempenho
de um programa:
-utilizar a memória cache de maneira inteligente;
-estruturas de dados e técnicas de programação mais
adequados ao problema;
-ferramentas e LPs a serem utilizadas.
-Para tal, será apresentado o problema da
Multiplicação de Cadeias de Matrizes, avaliando seu
tempo de execução, quando projetado de diferentes
modos.
6. O Problema da Multiplicação de
Cadeias de Matrizes
-Entrada: cadeia com n matrizes
-Saída: matriz resultante do produto das n matrizes
-O problema em sua essência é trivial, pois se multiplicarmos
sequencialmente as n matrizes obteremos facilmente o
resultado esperado. O maior desafio que reside neste problema
é otimizar o desempenho do programa que o resolve. Para tal,
podemos explorar técnicas de programação aplicadas ao
problema específico, além de buscar sempre explorar o
processamento máximo do computador de maneira inteligente.
7. O Problema do Cache Miss
-Acontece quando o processador necessita de um dado, e este
não está na memória cache. Tal fato obriga o processador a
buscar um bloco de dados –um número fixo de palavras -
diretamente na memória RAM, trazendo-os para a memória
cache e em seguida fornecendo a palavra requerida ao
processador.
-Em razão do fenômeno da localidade espacial, quando um
bloco de dados é trazido para a memória cache para satisfazer
uma dada referência à memória, é provável que futuras
referências sejam feitas para outras palavras desse bloco
[Stallings 2010].
8. O Problema do Cache Miss
-Soma de todos os elementos de uma matriz quadrada
de tamanho 10000 (Java):
-Percorrendo as linhas: 53 ms
-Percorrendo as colunas: 2886 ms
-Speedup superior a 50 vezes
9. Análise e Detalhamento de Possíveis
Soluções para o Problema de Multiplicação
de Cadeias de Matrizes
-Neste tópico serão descritas algumas abordagens
possíveis para a solução deste problema, que em
alguns casos são complementares e em outros são
antagônicas.
-Abordagem Sequencial
-Abordagem com Programação Dinâmica
-Abordagem com Minimização de Cache Miss
-Abordagem Distribuída com Threads
-Abordagem Distribuída com OpenMP
10. Abordagem Sequencial
-Multiplicam-se sequencialmente as n matrizes para
obter seu produto.
-Ex.: para uma cadeia com quatro matrizes a ordem de
multiplicação seria
-Complexidade:
- , sendo n o tam. das matrizes; ou
- , para t matrizes
1 for(int i=0; i<a.length ;i++){
2 for(int j=0; j<b[0].length ;j++){
3 c[i][j] = 0;
4 for(int k=0; k<a[0].length ;k++)
5 c[i][j] += a[i][k] * b[k][j];
6 }
7 }
11. Abordagem com Programação
Dinâmica
-Analisando mais cuidadosamente o código da Tabela anterior,
pode-se perceber que o número de multiplicações escalares
relacionadas à multiplicação de duas matrizes respeita o
seguinte cálculo:
-O referente à multiplicação pode ser descrito de maneira mais
detalhada por
-Sendo assim, pode-se observar que a ordem com que as matrizes são
multiplicadas pode tornar o programa mais eficiente, ou seja, reduzir o
número de multiplicações.
12. Abordagem com Programação
Dinâmica
-Por ex., para multiplicar uma cadeia de matrizes contendo três
matrizes existem duas parentizações possíveis.
-Ex.:
Nº de mult.
Ordem de mult.
escalares
1 75000
2 7500
-Speedup de 10x. É interessante perceber que para o exemplo anterior a
abordagem sequencial é a mais rápida. Mas, deve ser ressaltado que isso foi
apenas uma coincidência, e que a probabilidade da abordagem sequencial
ser a mais rápida torna-se bastante remota quando o número de matrizes da
cadeia aumenta, o que proporciona um ganho expressivo quando se utiliza a
abordagem com a parentização otimizada.
13. Abordagem com Programação
Dinâmica
-Mas como proceder para encontrar a parentização otimizada?
-Através da força bruta, o nº de soluções seria exponencial em n (
), sendo n o número de multiplicações, tornando essa verificação inviável
[Cormen et al 2002]. Uma alternativa viável para este problema é a utilização
da estratégia de Programação Dinâmica.
-A Programação Dinâmica (PD) geralmente é aplicada a problemas de
otimização, ou seja, problemas que possuem várias soluções, mas deseja-se
encontrar uma solução ótima, como é o caso do corrente problema. Para
aplicar a PD ao problema da multiplicação de cadeias de matrizes são
necessárias quatro etapas: caracterizar a estrutura de uma solução
ótima, que nesse caso é a estrutura de uma colocação ótima dos parênteses;
definir recursivamente o valor de uma solução ótima para o problema;
calcular o valor de uma parentização ótima; construir a solução ótima a
partir das informações coletadas. Mais detalhes sobre descrição e
implementação desse problema através da PD são fornecidos em [Cormen et
14. Abordagem com Minimização
do Cache Miss
-Com relação ao problema de multiplicação de matrizes, deve-se
primeiramente ressaltar que a própria natureza de seu algoritmo o faz mais
suscetível à ocorrência de cache miss. Pois é sabido que para multiplicar
duas matrizes, deve-se percorrer a linha da primeira e a coluna da segunda,
e quando se percorre a coluna de uma matriz a probabilidade de ocorrência
do cache miss aumenta.
-Baseado no funcionamento do algoritmo, observou-se que é possível
minimizar a ocorrência do cache miss invertendo as linhas 1 e 2 da Tabela 1
(quando pertinentes), com o objetivo de que o programa reutilize dados
previamente armazenados na memória cache.
1 for(int i=0; i<a.length ;i++){
2 for(int j=0; j<b[0].length ;j++){
3 c[i][j] = 0;
4 for(int k=0; k<a[0].length ;k++)
5 c[i][j] += a[i][k] * b[k][j];
6 }
7 }
15. Abordagem com Minimização
do Cache Miss
-Pensou-se o seguinte: se a quantidade de linhas da primeira matriz é maior
que a quantidade de linhas da segunda matriz, deve-se manter o código da
Tabela 1, caso contrário, devem ser invertidas as linhas 1 e 2 da Tabela 1.
Com essa verificação, o algoritmo sempre percorrerá a “maior linha
possível”, o que minimiza o cache miss.
if(a.length > b.length)
<Código da Tabela 1>
else
<Código da Tabela 1 com linhas 1 e 2 invertidas>
1 for(int i=0; i<a.length ;i++){
2 for(int j=0; j<b[0].length ;j++){
3 c[i][j] = 0;
4 for(int k=0; k<a[0].length ;k++)
5 c[i][j] += a[i][k] * b[k][j];
6 }
7 }
16. Abordagem com Minimização
do Cache Miss
-Outra maneira de se reduzir o cache miss para o problema da multiplicação
de matrizes seria multiplicar a primeira matriz pela matriz transposta da
segunda. Pode-se perceber que para isto seria necessário mudar o código
relativo à multiplicação das matrizes: ao invés de multiplicar linhas da
primeira matriz por colunas da segunda, multiplicar-se-iam linhas da
primeira matriz por linhas da segunda, o que implicaria numa redução
significativa do cache miss. Para evitar um custo adicional para o cálculo da
transposta da segunda matriz, poderia apenas ser alterado o método de
criação das mesmas para já criá-las como sendo suas próprias transpostas.
17. Abordagem Distribuída com
Threads
-A abordagem distribuída utilizando threads visa explorar a potencialidade
máxima dos processadores atuais, que geralmente possuem mais de um
núcleo físico e/ou lógico. Contudo, a abordagem distribuída com threads
utilizada no presente trabalho não caracteriza uma solução ótima, apesar de
explorar a capacidade máxima do hardware.
- A estratégia utilizada para esta abordagem divide a cadeia
pelo número de threads a serem criadas. Desse modo, cada thread fica
encarregada de realizar a multiplicação de uma sub-cadeia, e ao final,
quando todas terminarem sua execução, a thread principal realizará a
multiplicação das matrizes resultantes.
18. Abordagem Distribuída com
Threads
-Como exemplo, considere uma cadeia com doze matrizes
utilizando a abordagem distribuída com duas threads. Esta cadeia seria
quebrada ao meio em duas sub-cadeias e ,
e depois passaria pelo processo de se encontrar a parentização ótima através
da estratégia de PD. Porém, é fácil perceber que a parentização ótima para a
cadeia muito dificilmente será alcançada com a divisão da
cadeia. Para alcançarmos tanto a parentização ótima e ainda explorar a
capacidade máxima do processador outra estratégia deve ser utilizada, como
por exemplo, a abordagem distribuída utilizando a biblioteca OpenMP.
19. Abordagem Distribuída com
OpenMP
-A abordagem distribuída utilizando a biblioteca OpenMP (Open Multi-
Processing) não divide a cadeia de matrizes em sub-cadeias, e portanto,
mantém a parentização otimizada encontrada através da abordagem por PD.
Ao invés disso, utilizam-se diretivas em trechos de código que são
interpretadas pelo compilador, e que ao executar o programa, ativam as
threads dos processadores de acordo com as mesmas. Portanto, o trecho de
código que se pretende distribuir deve ser minuciosamente estudado para
que nenhuma violação lógica ou física seja cometida.
-Para o problema da multiplicação de matrizes, foi percebido que os
elementos da matriz resultante não possuem interdependências, o que
possibilita a paralelização desse trecho de código, mais especificamente do
loop for da linha 1. 1 for(int i=0; i<a.length ;i++){
2 for(int j=0; j<b[0].length ;j++){
#pragma omp parallel for 3 c[i][j] = 0;
4 for(int k=0; k<a[0].length ;k++)
<Código da Tabela 1> 5 c[i][j] += a[i][k] * b[k][j];
6 }
7 }
20. Abordagem Distribuída com
OpenMP
-A biblioteca OpenMP usa o modelo de paralelismo fork-and-join, que ramifica a
thread principal em threads paralelas, e quando estas terminam de executar, a thread
principal reassume o controle do programa [Chandra et al 2001]. Existem diversas
diretivas da biblioteca OpenMP que permitem os programadores tornarem seus
programas distribuídos. A diretiva que foi utilizada atua de forma simples, ela
subdivide a quantidade total de iterações no loop for pelo número de threads a
serem utilizadas, e cada thread executará uma parte das iterações que antes eram
executadas por apenas uma thread. No problema da multiplicação de matrizes, se a
matriz resultante tiver 10 linhas, e for especificado que o processador deve trabalhar
com duas threads, a primeira computará o produto das 5 primeiras linhas da matriz
resultante, e a segunda computará as linhas restantes.
21. Resultados
-Para avaliar algumas das abordagens estudadas pensou-se em realizar cinco
experimentos, aliando diferentes algoritmos e técnicas de programação.
1.Linguagem de Programação Java:
a) Abordagem sequencial, “maximizando o cache miss”;
b)Utilizando técnicas de PD, “maximizando o cache miss”;
c) Utilizando técnicas de PD, minimizando o cache miss;
d)Utilizando técnicas de PD, minimizando o cache miss, além de tornar a lógica
do algoritmo distribuída em 2 Threads;
e)Utilizando técnicas de PD, minimizando o cache miss, além de tornar a lógica
do algoritmo distribuída em 4 Threads;
-“Maximizar o cache miss” tem o objetivo de tornar mais evidente a ocorrência do
mesmo, e prover uma noção de tempo desperdiçado quando o mesmo ocorre. Para
minimizar o cache miss foi utilizada o código explicado anteriormente (slide 15). E
para tornar a lógica do algoritmo distribuído foi utilizada a abordagem distribuída
com threads. Para fins de padronização as matrizes foram preenchidas com 1’s.
22. Resultados
-Para tais experimentos, foi utilizado um notebook Dell Vostro 3450, Sistema
Operacional Windows 7 Professional Service Pack 1 de 64 Bits, 4 GB de
memória RAM, processador Intel® Core™ i5-250M de 2.5 GHz.
-A cadeia de matrizes escolhida para os experimentos foi a seguinte:
<7000,50,30,5000,32,40,5000,80,20,4000,20,10,1000,50>, o que equivale às matrizes
-Pode-se perceber que todas as matrizes da cadeia possuem o número de linhas
muito superior ao número de colunas, ou justamente o inverso, pois a ocorrência de
cache miss torna-se mais acentuada nesses casos.
23. Resultados
1.Linguagem de Programação Java:
a) Abordagem sequencial, “maximizando o cache miss”;
b)Utilizando técnicas de PD, “maximizando o cache miss”;
c) Utilizando técnicas de PD, minimizando o cache miss;
d)Utilizando técnicas de PD, minimizando o cache miss, além de tornar a lógica do algoritmo distribuída em 2 Threads;
e)Utilizando técnicas de PD, minimizando o cache miss, além de tornar a lógica do algoritmo distribuída em 2 Threads;
36.7
40
35 1.a
30 1.b
25 1.c
20
1.d
15 10.9
6.2 6.6 6.4 1.e
10
5
0
-Diferença de desempenho da abordagem que usa PD para a sequencial 25.8 s
-Pode-se notar que quando é utilizada uma estratégia de minimização de cache miss obteve-se
4.7 segundos de speedup, que representa uma evolução de quase 50%
-Ao analisar a estratégia distribuída com threads, pode-se perceber que tanto utilizando 2 como
4 threads, houve perda de desempenho
24. Conclusão
- O objetivo do presente trabalho foi mostrar a importância de dominar técnicas
relacionadas a projeto de algoritmos, e também conceitos de arquitetura de
computadores para obter melhor desempenho do software e hardware. Ao utilizar a
técnica de PD aliada à minimização de cache miss, obteve-se um speedup de 6x em
relação ao desempenho da solução mais simples (sequencial). É interessante
perceber que todos os algoritmos estão na mesma ordem de complexidade de tempo
( ), e mesmo assim foram obtidos ganhos de desempenho.
- Destes resultados, pode-se colher como aprendizado, que ao receber um problema
é necessário estudá-lo, visualizá-lo de diferentes perspectivas para adotar a estratégia
de solução mais eficiente. Para tal, devem ser analisados os algoritmos
pertinentes, ferramentas (APIs, bibliotecas, etc.) e LPs a serem utilizadas, além de
conceitos sobre funcionamento de software/hardware, de sistemas operacionais e da
arquitetura do computador de uma forma generalizada.
25. Trabalhos Futuros
Como possibilidades para trabalhos futuros, pode ser considerada a implementação
das abordagens já citadas na LP C++, uma vez que geralmente os programas escritos
nessa LP tendem a ter melhor desempenho que em Java. Além disso, a abordagem
Distribuída com OpenMP parece ser a mais eficiente. O fato de aproveitar o
desempenho da LP C++, realizando a distribuição do algoritmo em quantas threads
desejar, mantendo a parentização ótima alcançada pela PD, e minimizar a ocorrência
de cache miss com a abordagem da matriz transposta, aparenta ser a melhor solução
analisada no presente trabalho. Outra tarefa interessante seria implementar os
algoritmos para multiplicação de matrizes desenvolvidos por Strassen, que possui
complexidade de , ou o desenvolvido por Coppersmith e Winograd com
ordem de complexidade , aliando à alguma abordagem de minimização de cache miss
para comparação dos resultados [Robinson 2005].
26. Referências
- Koomey, J. G., Berard, S., Sanchez, M., Wong, H. (2009) “Assessing Trends in the
Electrical Efficiency”. Final report to Microsoft Corporation and Intel Corporation.
- Schaller, R. R. “Moore's law: past, present and future”. (1997) IEEE Spectrum, Vol
34, pp. 52-59.
- Alted, F. “Why Modern CPUs are Starving and what can be done about it”. (2010)
Computing in Science & Engineering, pp. 68–71.
- Stallings, W. “Arquitetura e Organização de Computadores”. (2010) Editora Prentice
Hall, pp. 122-144.
- Cormen, T. H., Leiserson, C. E., Rivest, R. L., Stein, C. “Algoritmos: Teoria e Prática”.
(2002) Editora Campus, pp. 259-295.
- Chandra, R., Dagum, L., Kohr, D., Maydan, D., McDonald, J., Menon, R. “Parallel
Programming in OpenMP”. (2001) Editora Morgan Kaufmann.
- Robinson, S. “Toward an Optimal Algorithm for Matrix Multiplication”. (2005)
Society for Industrial and Applied Mathematics. Vol. 38, Nº 9.
Explicar o contexto do trabalho:disciplina de Arquitetura de Computadores + disciplina de EDCA do PPGI.
Desde 1946, com a criação do primeiro computador eletrônico para fins genéricos (o ENIAC), pode-se perceber a rápida evolução dos processadores ao longo de décadas [1]. A principal razão para tal é a evolução tecnológica que permite cada vez mais a produção eficiente dos transistores. Esta eficiência tem proporcionado a possibilidade de construção de transistores sempre menores e mais baratos, de tal modo que mais ou menos a cada dois anos seu número dobrasse nos processadores, seguindo uma tendência conhecida como Lei de Moore [2]. A rápida evolução dos processadores já é um fator que constantemente tem tornado a execução de programas mais eficiente. Contudo, o aumento da velocidade dos processadores é apenas um dos fatores que podem elevar o desempenho de um programa e, além disto, é limitado por outros fatores que têm influência direta nesta melhoria, dentre eles, a memória [3]. De nada adianta ter processadores super velozes, se a memória não é rápida o suficiente para fornecê-los dados para processamento sem que eles fiquem ociosos. Na década de 80 até meados dos anos 90, ambos evoluíram a uma taxa similar: 1 MHz a 16 MHz. Logo em seguida, a evolução dos processadores continuou a aumentar de forma intensa, ao passo que as taxas de frequência da memória evoluíam em um ritmo mais lento [3]. Deste modo, foi preciso adotar uma nova estratégia para diminuir o tempo de espera do processador: a memória cache. A memória cache é um tipo bastante rápido de memória que serve para armazenar os dados mais frequentemente usados pelo processador, evitando na maioria das vezes que ele tenha que recorrer à “lenta” memória RAM. É interessante perceber o modo de funcionamento da memória cache com o objetivo de usá-las (programaticamente) da maneira mais eficiente possível, como por exemplo, tentar minimizar o cache miss.
Sendo assim, é fácil perceber que quando acontece vários cache miss na execução de um programa, seu desempenho tenderá a cair. Os programadores podem utilizar a propriedade da localidade espacial dentre outras relacionadas ao funcionamento da memória cache no intuito de minimizar os cache miss e tornar a execução dos programas mais rápidos.
A Figura 1 ilustra o fenômeno da localidade espacial ocorrendo no problema da soma dos elementos de uma matriz. Quando o primeiro elemento da matriz () é buscado para efetuar a soma, o processador buscará um bloco de dados sequencial (uma linha do array, ou parte dela) para a memória cache. Se forem percorridas as linhas da matriz, provavelmente os próximos elementos a serem somados (representados através da cor verde e amarela na Figura 1) estarão na memória cache, o que ocasiona um cache hit, tornando o acesso ao dado mais rápido. Se forem percorridas as colunas, há maior probabilidade de que os próximos elementos a serem somados (vermelho) não estejam na memória cache, ocorrendo um cache miss.