MAC0412 – EP1 1 Introduç˜ao 2 Primeiro programa
MAC0412 – EP1 1 Introduç˜ao 2 Primeiro programa
MAC0412 – EP1 1 Introduç˜ao 2 Primeiro programa
Create successful ePaper yourself
Turn your PDF publications into a flip-book with our unique Google optimized e-Paper software.
1 Introdução<br />
<strong>MAC0412</strong> <strong>–</strong> <strong>EP1</strong><br />
Experimentos com o cache<br />
Pedro Matiello<br />
Neste exercício, analisamos o comportamento de três pequenos <strong>programa</strong>s<br />
fornecidos pelo professor. O objetivo, em particular, é identificar possíveis<br />
problemas de desempenho decorrentes do uso inadequado do cache do processador.<br />
Sabemos, o cache é uma memória de tamanho reduzido, mas de acesso<br />
mais rápido do que a memória principal. O uso apropriado deste recurso pode<br />
oferecer ganhos significativos de desempenho a alguns <strong>programa</strong>s, reduzindo<br />
o tempo em que o processador fica ocioso para leitura ou escrita da memória.<br />
2 <strong>Primeiro</strong> <strong>programa</strong><br />
O primeiro <strong>programa</strong> fornecido aloca uma grande região de memória como<br />
uma matriz e preenche com zeros. Este preenchimento pode ser realizado de<br />
duas maneiras, de acordo com argumentos passados na linha de comando:<br />
• Percorrendo a matriz por linhas;<br />
• Percorrendo a matriz por colunas.<br />
Apesar de executarem o mesmo número de operações, a primeira maneira<br />
se mostra mais rápida do que a segunda em testes realizados com o <strong>programa</strong><br />
time.<br />
Modo de Execução Tempo Total<br />
Por Linha 0.874s<br />
Por Coluna 3.748s<br />
1
Este comportamento, presenciado anteriormente na disciplina de MAC0300<br />
na implementação de algoritmos para fatoração de matrizes, pode ser explicado<br />
pela disposição dos elementos da matriz na memória. Este primeiro<br />
<strong>programa</strong> está escrito em C, e esta linguagem armazena matrizes concatenando<br />
suas linhas, uma após a outra. Quando um elemento da matriz é lido<br />
da memória, todos os elementos armazenados na mesma página são armazenados<br />
no cache; o acesso posterior a estes elementos, então, é feito com<br />
menor latência.<br />
Ora, devido à disposição por linhas da matriz, os elementos próximos a<br />
um elemento específico tendem a ser aqueles que estão na mesma linha, e<br />
o acesso por colunas não irá obter as vantagens oferecidas pelo cache. Isto<br />
pode ser verificado pela contagem das falhas de cache, realizada através do<br />
<strong>programa</strong> valgrind:<br />
Modo de Execução Falhas de Cache<br />
Por Linha 6,281,171<br />
Por Coluna 98,170,003<br />
Este problema das falhas de cache no acesso por colunas é, contudo,<br />
bastante reduzido ou mesmo eliminado em matrizes menores, que podem<br />
ser totalmente ou em grande parte armazenadas no cache. Em matrizes<br />
maiores, porém, páginas de acesso mais recente tomam o lugar de páginas<br />
de acesso mais antigo no cache, e acessos posteriores a estas devem fazer uso<br />
da memória principal.<br />
3 Segundo Programa<br />
O segundo <strong>programa</strong> realiza a soma de dois vetores, armazenando o resultado<br />
no terceiro. Um número pode ser passado como argumento pela linha<br />
de comando, e o <strong>programa</strong> então realizará a soma pulando este número de<br />
posições a cada iteração (mas, ainda assim, realizando o mesmo número de<br />
somas no final).<br />
Também aqui, a ordem em que as operações são realizadas afeta o desempenho<br />
do <strong>programa</strong>. O gráfico abaixo apresenta a média do tempo total<br />
de três execuções do <strong>programa</strong> para cada valor de salto entre 0 e 100. Novamente,<br />
os dados foram obtidos através do <strong>programa</strong> time.<br />
Pode-se observar que, para saltos de tamanho entre 0 e 30, o tempo to-<br />
2
1.4<br />
1.2<br />
1<br />
0.8<br />
0.6<br />
0.4<br />
0.2<br />
0<br />
0 20 40 60 80 100<br />
Figura 1: Tempo de execução (s) × Tamanho do salto<br />
tal de execução aumenta a medida que o tamanho do salto aumenta. A<br />
partir deste valor, o tempo total de execução não sofre variações significativas.<br />
Podemos atribuir este comportamento, novamente, às falhas de cache:<br />
no intervalo 0 <strong>–</strong> 30 estas aumentam com o aumento do tamanho do salto<br />
mas, a partir deste valor limite, as posições acessadas nos vetores já estão<br />
suficientemente distantes para provocar falhas de cache após um número de<br />
iterações muito similar. A tabela abaixo apresenta os valores determinados<br />
pelo valgrind para o número de falhas de cache para alguns valores de salto:<br />
Tamanho do Salto Falhas de Cache<br />
1 3,127,502<br />
10 20,002,511<br />
30 31,252,499<br />
90 31,252,499<br />
4 Terceiro Programa<br />
O terceiro <strong>programa</strong> instancia uma estrutura contendo duas variáveis de<br />
tipo inteiro. A seguir, dois processos distintos compartilham o acesso a esta<br />
estrutura, de modo que o primeiro processo acesse exclusivamente a primeira<br />
variável e o segundo processo acesse exclusivamente a segunda variável.<br />
Apesar de não ocorrer acesso compartilhado a nenhuma variável, as duas<br />
variáveis da estrutura são alocadas na mesma página de memória. Se cada<br />
3
processo é executado em um core diferente do processador, a escrita em uma<br />
destas variáveis por um dos processos irá acarretar na invalidação do cache<br />
para esta página no core que executa o outro processo, forçando um acesso<br />
desnecessário à memória principal.<br />
O acesso à memória principal, por sua vez, é mais custoso, produzindo<br />
um desempenho inferior ao possível. Podemos eliminar o problema, contudo,<br />
alterando o código do segundo processo para realizar todas as suas operações<br />
em uma variável temporária local, atualizando a variável compartilhada apenas<br />
no fim de sua computação.<br />
A tabela abaixo exibe o tempo consumido pela versão original (que altera<br />
frequentemente a variável compartilhada) e pela versão modificada (que<br />
altera a variável compartilhada apenas no final da computação).<br />
Versão Tempo de Execução<br />
Original 1.561s<br />
Modificada 0.656s<br />
5 Outra Forma de Atrapalhar o Cache<br />
Outras maneiras de minimizar o efeito da ação do cache são possíveis. Uma<br />
delas é sobrecarregá-lo com dados que são desnecessários na computação<br />
sendo realizada. Considere, por exemplo, a estrutura abaixo:<br />
struct Registro {<br />
int campo0;<br />
int campo1[1024];<br />
int campo2[1024];<br />
int campo3[1024];<br />
int campo4[1024];<br />
int campo5[1024];<br />
int campo6[1024];<br />
};<br />
O código abaixo percorre 2000000 um vetor de 500 registros do tipo especificado<br />
acima, realizando operações exclusivamente sobre o campo zero:<br />
struct Registro registros[500];<br />
4
for (i = 0; i < 2000000; i++) {<br />
for (j =0; j < 500; j++) {<br />
registros[j].campo0 = i+j;<br />
}<br />
}<br />
O tamanho expressivo do struct Registro faz com apenas uma pequena<br />
parte do vetor possa ser armazenada no cache em um dado momento. Contudo,<br />
como os campos de 1 a 6 são irrelevantes no laço acima, podemos<br />
removê-los para uma estrutura auxiliar:<br />
struct Registro {<br />
int campo0;<br />
};<br />
struct Registro_aux {<br />
int campo1[1024];<br />
int campo2[1024];<br />
int campo3[1024];<br />
int campo4[1024];<br />
int campo5[1024];<br />
int campo6[1024];<br />
};<br />
O código antes do laço deve ser alterado para declarar também as estruturas<br />
auxiliares, para uma justa comparação:<br />
struct Registro registros[500];<br />
struct Registro_aux registros_aux[500];<br />
for (i = 0; i < 2000000; i++) {<br />
for (j =0; j < 500; j++) {<br />
registros[j].campo0 = i+j;<br />
}<br />
}<br />
E, novamente através de testes com o comando time, podemos verificar<br />
que o tamanho das estruturas de dados utilizadas pode afetar negativamente<br />
o desempenho:<br />
5
Versão Tempo de Execução<br />
Original 11.505s<br />
Modificada 4.222s<br />
6