18.06.2013 Views

MAC0412 – EP1 1 Introduç˜ao 2 Primeiro programa

MAC0412 – EP1 1 Introduç˜ao 2 Primeiro programa

MAC0412 – EP1 1 Introduç˜ao 2 Primeiro programa

SHOW MORE
SHOW LESS

You also want an ePaper? Increase the reach of your titles

YUMPU automatically turns print PDFs into web optimized ePapers that Google loves.

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

Hooray! Your file is uploaded and ready to be published.

Saved successfully!

Ooh no, something went wrong!