Organização de Computadores
EP1
Experimentos com cache
Pedro Morhy Borges Leal
5893830
Dependendo do modo que um programa é desenvolvido ele pode aproveitar muito mal um
recurso muito importante do computador que é o cache. O cache serve de auxilio aos computadores
para realizar tarefas e acessar dados de um modo mais rápido. É uma memória muito rápida que fica
perto do processador para consultar dados e instruções que geralmente ocorrem com bastante
frequência em um programa.
No primeiro exemplo passado, o teste1.c, o main() realiza duas tarefas dependendo se o
usuário coloca algum argumento ao chamar o programa ou não. Se ele não colocar nenhum
argumento, o main() irá percorrer uma matriz linha a linha e definir cada elemento com o numero 0.
Caso contrário ele irá realizar a mesma tarefa porém percorrendo coluna por coluna.
A partir de testes utilizando a ferramenta time percebemos que a partir de um determinado
tamanho da matriz (baseado no SIZE) o tempo de execução das duas tarefas se diferenciam
bastante. A tabela abaixo mostra essa diferença:
SIZE Tamanho Matriz
Tempo Linhas
Tempo Colunas
Diferença Tempo
100
10K
0,004s
0,004s
0s
200
40K
0,004s
0,004s
0s
400
160K
0,005s
0,006s
0,001s
800
640K
0,010s
0,015s
0,005s
1,6K
2,56M
0,024s
0,061s
0,037s
3,2K
10,24M
0,077s
0,178s
0,101s
6,4K
40,96M
0,429s
2,557s
2,128s
Como podemos conforme vamos aumentando o SIZE o tempo de tarefa que percorre a
matriz por colunas começa a ficar bem maior que a tarefa que percorre a matriz por linhas. Isso
ocorre porque em C uma matriz é uma array de linhas, isso é, na memória cada elemento de uma
linha é guardado um ao lado do outro, e ao fim desta linha começa a nova linha e assim por diante.
Então quando um elemento é acessado dessa matriz, parte dos dados perto dessa matriz são
carregados junto no cache e o dados perto deste cache são os elementos da mesma linha.
Logo na tarefa que percorre a matriz por linhas utiliza muito bem os recursos do cache,
assim o processador não precisa buscar na memória mais devagar os dados da maioria dos
elementos. Enquanto na outra abordagem a partir de um certo SIZE o cache não é bem utilizado e o
processador precisa “ir buscar” na memória “lerda” os dados da matriz com uma frequência maior.
Além disso podemos perceber que na tarefa de colunas do SIZE 3,2K para o 6,4K há um
salto muito grande no tempo, maior do que o normal. Utilizando a ferramenta valgrind com a opção
cachegrind para analisar como o programa está utilizando o cache do computador para diferentes
SIZEs podemos perceber que aproximadamente do SIZE 3,4K ao SIZE 3,5K a porcentagem do
miss rate do L2, para cache de dados, cresce muito como aparece na tabela abaixo.
SIZE
D1(%)
200
400
800
1600
3200
3400
3450
3475
3500
3585
3600
3700
3710
3713
3800
3900
4000
4100
4200
4400
4800
5000
5500
5800
6000
6200
6400
8000
10000
L2d(%)
13,5
26,8
38,7
45,1
48
48,1
48,2
47,9
47,9
48,2
48,3
48,3
48,3
48,1
48,2
47,7
47,7
47,6
47,7
47,8
48
48,1
48,3
48,4
48,5
48,5
48,6
48,9
52,1
1,3
1,9
2,5
2,8
3
3
3
47,9
47,9
11,2
11,1
10,9
10,9
48,1
48,2
47,6
47,7
47,6
47,7
47,8
48
48,1
48,3
48,4
48,4
48,5
48,6
48,6
49
Os dados do valgrind para a tarefa “linhas” em ambas as colunas chegaram no máximo em
3%. Ou seja no cache 1 a diferença entre as duas já foi visível com SIZE menor que 200. Agora
para o cache L2 esse diferença só ocorre a partir do SIZE 3475. O computador utilizado para rodar
esse teste tem um cache L2 de 3 MB e um cache de nível 1 de 64 KB.
Com o SIZE 3475 a matriz fica com o tamanho de 12M de elementos o que é
aproximadamente 4 vezes o tamanho do cache. Foi testado também em uma maquina com o cache
de 2 MB para analisar se este padrão continuava, e de fato continuou. O SIZE neste caso que
aumentou bruscamente o miss rate foi 2635, então a matriz com tamanho aproximadamente 7M de
elementos e quase 4 vezes o tamanho do L2. Claro que não exatamente pois temos outras coisas a
considerar, mas chegamos um pouco a essa taxa.
O gráfico abaixo mostra o gráfico da tabela acima, podemos ver como é repentina a
mudança do miss rate do L2 e por alguma razão essa porcentagem sobre um pouco de alteração
logo depois e depois fica quase constante. A linha azul é a coluna D1 e a linha vermelha é a coluna
L2D.
O processador procura por um determinado dado no cache utilizando o endereço de
memória de onde ele está localizado na mémoria principal. No cache os dados são mapeados a
partir dos endereços de memória. Chamado de direct-mapped cache a estrutura do cache é indexada
pelos primeiros bits do endereço de memória. Então por exemplo os dados do endereço 101100
ficariam indexados na posição 100 enquanto os 101000 e 111000 ficariam indexados no 000. Vale
lembrar que nessa forma apenas um dado fica armazenado em cada posição, então no exemplo
anterior o ultimo utilizado ficaria armazenado na posição 000. Mas os processadores de hoje em dia
utilizam a associatividade em que para cada index do cache existem mais de uma posição.
O teste 2 serve para mostrar essa estrutura. O gráfico acima mostra a execução dele com
diversos tipos de deslocamento e o tempo que demorou para realizar a tarefa.
Além da questão já discutida anteriormente, que os elementos mais próximos de um
elemento de um vetor recentemente utilizado vão para o cache. Neste caso percebemos que nos
pulos de tamanho que são potência de 2 demoram mais dos que em sua volta. Isso se deve ao fato
de que esses números são dessa forma: 1...000000000. Ou seja, supomos que a posição 1 de um
vetor seja 1, quando pegamos esse valor colocamos ele no index 001 do cache, quando damos um
pulo de potência de 2 (números grandes), por exemplo 512, que é 1000000000 o próximo elemento
consultado terá endereço 1000000001(Obs.: estou desconsiderando que os endereços andam de 4
em 4 para facilitar a explicação, mas funcionaria do mesmo modo). Quando formos procurar esse
endereço no cache provavelmente não acharemos pois o ultimo elemento adicionado lá tem o
mesmo index que este (001), e então iremos adiciona-lo no cache. O próximo elemento também terá
fim 001 e assim por diante, ou seja, sempre iremos trocar os elementos do cache e nunca poderemos
nos beneficiar dele.
Abaixo, utilizando o valgrind para os pulos 2047, 2048, 2049 vemos a grande diferença
entre os da ponta com o do meio que é uma potência de 2.
Deslocamento L2D miss rate
2047
0,60%
2048
6,60%
2049
0,60%
Um outro problema para cache é quando utilizamos processos diferentes utilizando uma
memória compartilhada. Supondo que estamos trabalhando com dois cores diferentes e dois
processos que compartilham um certo dado, então temos um problema de race condition. Além tem
termos dois caches diferentes para cada processo, o sistema tem que tomar conta deste
compartilhamento de dados para não ocorrer problemas mas isso custa muito para o processador.
O teste3.c mostra isso, após rodar algumas vezes a média de tempo foi de 1,73 segundos,
que é bastante coisa. Uma tentativa de otimização foi fazer com que os dados que estavam sendo
compartilhados não fossem os numeros em si e sim os endereços de memória deles, assim o sistema
não precisaria atualizar o cache, que custa muito. A média de tempo foi te 1,53 segundos, diminuiu
um pouco mas continua muito alto. A questão aqui é o que o sistema está protegendo as variáveis
para problemas de race condition e para ficar mais rápido o melhor seria não compartilhar a
memória ou não dividir em dois processos paralelos.
Um exemplo realizado, o teste4.c, tem uma abordagem parecida com ao teste1.c porém ao
invez do caso não ótimo percorrer a matriz por colunas o que foi feito é que os elementos da matriz
são acessados de forma aleatória. E os testes provaram que desse modo também o cache não é bem
utilizado por que o sistema armazena os dados que estão próximos do elemento recém consultado
mas na próxima consulta não quer dizer que eles serão requisitados dado a abordagem aleatoria.
O tempo médio no caso normal deste programa foi de 0,830 segundos enquanto na outra foi
de 1,480 segundos, quase o dobro. Utilizando a ferramenta valgrind obtivemos os seguintes
resultados:
Caso normal:
• D1 miss rate: 0,2%
• L2d miss rate: 0,2%
Caso acesso aleatório:
• D1 miss rate: 3,9%
• L2d miss rate: 2,8%
Download

Organização de Computadores