Instituto de Matemática e Estatística - USP MAC Organização de Computadores. EP1 - Experimentos com o cache

Documentos relacionados
Instituto de Matemática e Estatística - USP MAC Organização de Computadores EP1. Experimentos com o cache. Tiago Andrade Togores

Organização e Arquitetura de Computadores I

SSC0611 Arquitetura de Computadores

FUNDAMENTOS DE ARQUITETURAS DE COMPUTADORES MEMÓRIA CACHE CONTINUAÇÃO CAPÍTULO 5. Cristina Boeres

Organização de Computadores

Princípio da Localidade Apenas uma parte relativamente pequena do espaço de endereçamento dos programas é acessada em um instante qualquer Localidade

Hierarquia de Memória. Sistemas de Computação André Luiz da Costa Carvalho

Memória Cache. Memória Cache. Localidade Espacial. Conceito de Localidade. Diferença de velocidade entre Processador/MP

SSC0112 Organização de Computadores Digitais I

Experimentos com o Cache Organização de Computadores

Capítulo 5 Livro do Mário Monteiro Conceituação. Elementos de projeto de memória cache

FUNDAMENTOS DE ARQUITETURAS DE COMPUTADORES MEMÓRIA CACHE CAPÍTULO 5. Cristina Boeres

Memória Cache. Aula 24

Memória virtual. Sistemas de Computação

Níveis de memória. Diferentes velocidades de acesso. Memória Cache. Memórias Auxiliar e Auxiliar-Backup

Universidade Federal de Uberlândia Faculdade de Computação. Linguagem C: ponteiros e alocação dinâmica

Gerência de Memória. Paginação

Fundamentos de Sistemas Operacionais

Gerência de Memória. Endereçamento Virtual (1) Paginação. Endereçamento Virtual (2) Endereçamento Virtual (3)

Organização e Arquitetura de Computadores

Programação: Vetores

Princípios de Desenvolvimento de Algoritmos MAC122

Organização e Arquitetura de Computadores. Ivan Saraiva Silva

Sistemas Operacionais Gerenciamento de Memória. Carlos Ferraz Jorge Cavalcanti Fonsêca

ARQUITETURA DE COMPUTADORES

Universidade Federal de Campina Grande Departamento de Sistemas e Computação Curso de Bacharelado em Ciência da Computação.

Correção de Erros. Erros de memória de semicondutores podem ser:

Organização e Arquitetura de Computadores I

Sistemas Operacionais

SISTEMAS OPERACIONAIS. 2ª. Lista de Exercícios Parte 2

PROGRAMAÇÃO A. Matrizes

Gerenciamento de memória

Universidade Federal de Mato Grosso do Sul. Implementação de um Sistema Simplificado de Memória Cache

Aula 13: Memória Cache

Capítulo 6 Nível do Sistema Operacional

Aula 12: Memória: Barramentos e Registradores

Organização e Arquitetura de Computadores I

Estrutura de Dados. Aula 07 Alocação Dinâmica

AULA Nº 11 SISTEMAS OPERACIONAIS. Técnicas de Memória Virtual

Memória. Memória Cache

Arquitetura de Sistemas Operacionais

Memoria. UNIVERSIDADE DA BEIRA INTERIOR Faculdade de Engenharia Departamento de Informática

Arquitetura de Computadores. Hierarquia de Memória. Prof. Msc. Eduardo Luzeiro Feitosa

Memória cache (cont.) Sistemas de Computação

Experimentos com a memória cache do CPU

Arquitetura Von Neumann Dados e instruções são obtidos da mesma forma, simplificando o desenho do microprocessador;

Memória cache. Sistemas de Computação

LINGUAGEM C: FUNÇÕES FUNÇÃO 08/01/2018. Funções são blocos de código que podem ser nomeados e chamados de dentro de um programa.

Aula 06. Slots para Memórias

MEMÓRIA CACHE FELIPE G. TORRES

LÓGICA DE PROGRAMAÇÃO (C) VETORES E MATRIZES. Professor Carlos Muniz

Universidade Federal de Campina Grande Departamento de Sistemas e Computação Curso de Bacharelado em Ciência da Computação.

Arquitetura e Organização de Computadores

Memória Cache. Walter Fetter Lages.

Capítulo 11: Implementação de Sistemas de Arquivos. Operating System Concepts 8th Edition

Sistemas Operacionais

Princípios de Desenvolvimento de Algoritmos MAC122

SSC510 Arquitetura de Computadores 1ª AULA

Microprocessadores. Memórias

SSC0640 Sistemas Operacionais I

Aula 17: Ponteiros e Alocação Dinâmica em C

Hierarquia de Memória

Memória Virtual. Prof. M.Sc. Bruno R. Silva CEFET-MG Campus VII

Algoritmos e Programação

Arquitectura de Computadores

1. A pastilha do processador Intel possui uma memória cache única para dados e instruções. Esse processador tem capacidade de 8 Kbytes e é

ANÁLISE DE COMPLEXIDADE DOS ALGORITMOS

Computação Eletrônica. Vetores e Matrizes. Prof: Luciano Barbosa. CIn.ufpe.br

Linguagem C Princípios Básicos (parte 1)

Memória Cache endereço de memória

AULA 03: FUNCIONAMENTO DE UM COMPUTADOR

Capítulo 8: Memória Principal. Operating System Concepts 8 th Edition

Infraestrutura de Hardware. Explorando a Hierarquia de Memória

Sistemas Operacionais I Memória Virtual

Modulo 12: alocação dinâmica de memória

Memória Cache. Adriano J. Holanda. 12 e 16/5/2017

Organização e Arquitetura de Computadores I

Fundamentos de Sistemas Operacionais

Introdução à Programação

Alocação Dinâmica de Memória

Notas da Aula 20 - Fundamentos de Sistemas Operacionais

Cache. Cache. Direct Mapping Cache. Direct Mapping Cache. Internet. Bus CPU Cache Memória. Cache. Endereço Byte offset

Computadores e Programação (DCC/UFRJ)

Sistemas de Memória II

/17. Arquitetura de Computadores Subsistemas de Memória Prof. Fred Sauer

Sistemas Opera r cionais Gerência de Memória

Edital de Seleção 024/2017 PROPESP/UFAM. Prova de Conhecimento. Caderno de Questões

Sistemas Operacionais. BC Sistemas Operacionais

Implementação de Diretórios (1)

Alocação Dinâmica em C

Ponteiros. prof. Fabrício Olivetti de França

- Mapa de memória de um processo - Ponteiros

Ponteiros. Prof. Fernando V. Paulovich *Baseado no material do Prof. Gustavo Batista

3. Linguagem de Programação C

SSC304 Introdução à Programação Para Engenharias. Alocação Dinâmica. GE4 Bio

Infraestrutura de Hardware. Funcionamento de um Computador

TÉCNICAS DE PROGRAMAÇÃO. Estrutura de dados

Linguagem C Ponteiros

Transcrição:

Instituto de Matemática e Estatística - USP MAC0412 - Organização de Computadores Relatório EP1 - Experimentos com o cache Ana Luiza Domingues Fernandez Basalo - 6431109 29 de setembro de 2010

Sumário 1 Introdução 1 2 Visão geral - cache 1 2.1 Motivação.......................................... 1 2.2 Organização/Arquitetura................................. 2 2.3 Funcionamento....................................... 3 3 Testes 4 3.1 Sistema em que foram feitos os testes........................... 4 3.2 teste1.c........................................... 4 3.2.1 Testes com o código-fonte original........................ 4 3.2.2 Testes com o código alterado........................... 6 3.2.3 Por que percorrer matrizes por linhas ou por colunas faz tanta diferença?.. 13 3.2.4 Com que tamanhos de matrizes essa diferença começa a ser perceptível?... 14 3.2.5 Qual a relação deste valor com o tamanho do cache?.............. 14 3.3 teste2.c........................................... 14 3.3.1 O que acontece quando percorremos vetores com deslocamentos?....... 18 3.3.2 Para quais deslocamentos o desempenho fica pior?............... 18 3.3.3 Você sabe explicar por que este valor causa tal queda no desempenho?.... 18 3.4 teste3.c........................................... 18 3.4.1 Por que o programa teste3.c demora tanto para rodar?............. 18 3.4.2 Você tem alguma solução melhor para este problema?............. 19 4 Programas adicionais 19 4.1 EP2 de MAC0300 - Segundo Semestre de 2009..................... 19 4.2 teste4............................................ 19 4.2.1 teste4.c....................................... 19 4.2.2 teste4-mod.c.................................... 21 5 Conclusão e Considerações finais 22 i

1 Introdução O presente documento é o Relatório exigido pelo Exercício-Programa 1 da disciplina MAC0412 - Organização de Computadores. O Relatório está organizado da seguinte forma: primeiro é apresentada uma visão geral do cache (o que é, como funciona e sua relação com a performance de programas). A seguir, o documento fornece resultados e análises de testes feitos com os programas do arquivo Arquivos do EP1 (EP1.tar.gz) do PACA da disciplina. Ainda no que concerne a testes, serão mostrados mais alguns realizados sobre programas adicionas (não presentes no supracitado arquivo). Finalmente, o Relatório apresenta uma conclusão sintetizando os conceitos e resultados obtidos. 2 Visão geral - cache O cache da CPU é um cache usado pela Unidade Central de Processamento para reduzir o tempo médio de acesso a memória. O cache é um tipo de memória mais rápido e menor, que armazena cópias dos dados das posições mais acessadas da memória principal. Se a maioria dos acessos à memória forem acessos ao cache, então a latência média de acessos será mais próxima à latência do cache do que à latência da memória principal. Quando o processador precisa fazer uma operação de leitura ou escrita na memória principal, ele primeiro checa se uma cópia do dado está no cache. Se for esse o caso, o processador imediatamente lê ou escreve no cache, o que é muito mais rápido do que ler ou escrever direto na memória principal. A maioria das CPUs atuais possui pelo menos três caches independentes: um cache de instruções para acelerar a busca de instruções, um cache de dados para acelerar a busca e o armazenamento de dados e um buffer de tradução usado para acelerar a tradução (virtual para física) de endereços de instruções e dados. Daqui para a frente, iremos nos referir ao cache da CPU como cache indistintamente. 2.1 Motivação CPUs hoje são muito mais sofisticadas do que eram há 25 anos, quando a frequência do core da CPU era de um nível equivalente ao do barramento de memória: os acessos à memória eram apenas um pouco mais lentos do que os acessos aos registradores. Mas isso mudou drasticamente nos anos 90, quando os designers de CPU aumentaram a frequência do core da CPU mas a frequência do barramento de memória e das memórias RAM não aumentou proporcionalmente. Isso não ocorreu porque memórias RAM mais rápidas não podiam ser construídas. Na verdade, teoricamente, era possível desenvolver memórias RAM tão rápidas quanto o core da CPU, o problema é que, na prática, tais memórias não eram economicamente viáveis. Essa diferença entre as frequências da CPU e da memória RAM levava a uma queda considerável no desempenho das aplicações. Os caches surgiram então como uma solução para esse problema: um tipo de memória mais próximo ao core da CPU, que seria, portanto, mais rápido de ser acessado que a memória principal. Observando os acessos à memória, percebeu-se que tanto o código como os dados de um programa em execução tem uma localização (temporal e espacial). Isso significa que, durante curtos intervalos de tempo, há uma boa chance de que o mesmo código ou dado seja usado novamente. Para código, isso significa, idealmente, que há um laço, e, portanto, a mesma porção de código será executada muitas vezes consecutivas. No caso dos dados, idealmente, os acessos estão restritos a regiões pequenas da memória, logo, há uma grande chance de que os mesmos dados sejam usados novamente dentro de um intervalo de tempo não muito longo. Essas observações são a chave do funcionamento dos caches de CPUs atuais: a ideia, simplificadamente, é manter nos caches os dados e instruções que são usados com mais frequência, evitando ter de acessá-los 1

na memória principal, o que causaria prejuízos à performance dos programas. Um cálculo simples pode mostrar o quanto os caches podem ser úteis. Considere a seguinte situação hipotética: suponha que o acesso à memória principal leva 200 ciclos e o acesso à memória cache leva 15 ciclos. Então um código que use 100 elementos de dados 100 vezes cada levará 2000000 ciclos em operações com memória se não houver cache e apenas 168500 ciclos se todos os dados puderem ser armazenados no cache. Isso significa uma melhora de 91.5%. É claro que o exemplo apresentado é apenas uma situação teórica. Na prática, é impossível armazenar sempre todos os programas e seus dados inteiramente no cache. De fato, o tamanho dos caches é em torno de 1/1000 do tamanho da memória principal (atualmente, em média, 4MB de cache para 4GB de RAM). O tamanho relativamente limitado dos caches requer estratégias para determinar o que deve ser armazenado no cache num dado momento. É claro que o próprio programador pode construir programas que aumentem o uso do cache. Falaremos sobre isso nas seções subsequentes. 2.2 Organização/Arquitetura Antes de abordarmos detalhes do funcionamento dos caches, apresentamos uma breve explicação da arquitetura (organização) de um sistema de caches. Cache: Configuração mínima A figura acima apresenta, de maneira simplista, a configuração mínima de um sistema que usa cache: corresponde à arquitetura dos primeiros sistemas que começaram a usar cache. Após a introdução do cache, o sistema logo começou a tornar-se mais complexo. A diferença de velocidade entre o acesso ao cache e o acesso à memória aumentou novamente, ao ponto de um outro nível de cache precisar ser adicionado (um nível com maior espaço de armazenamento, mas um pouco mais lento que o primeiro nível). Isso porque aumentar o tamanho do primeiro nível de cache não era algo economicamente viável. Atualmente, há sistemas com três níveis de cache. Cache: níveis L1i = cache L1 de instruções L1d = cache L1 de dados Note que o diagrama acima é apenas ilustrativo de um sistema com três níveis de cache: o fluxo dos dados, na realidade, não precisa passar para níveis superiores de cache para ir até a memória principal. 2

Além disso, atualmente, temos processadores com múltiplos cores, cada qual rodando múltiplas threads: Múltiplos processadores, múltiplos cores e múltiplas threads Neste esquema, há dois processadores, cada um com dois cores, cada qual rodando duas threads. As threads compartilham os caches L1. Os cores (em cinza escuro) possuem caches L1 individuais. Todos os cores da CPU compartilham caches de níveis superiores. Os dois processadores (em cinza claro) não compartilham caches. É claro que nem todo sistema funciona assim, é apenas um esquema ilustrativo para facilitar o entendimento. 2.3 Funcionamento Sabemos que (uma parte do) o conteúdo da memória principal é copiado para o cache, para que o processador possa acessar os dados e instruções de que necessita de maneira mais rápida. As perguntas mais naturais quanto a esse processo seriam: como são carregados os dados da memória principal para o cache? Quando um certo dado não está no cache quais são os procedimentos? Como é decidido quais são os dados e instruções que deverão permanecer no cache? Esta seção aborda as respostas para essas questões num nível mais ou menos abstrato. Seguindo os princípios da localização (espacial e temporal) discutidos na apresentação da motivação, é fácil de entender porque os caches são tipicamente organizados de modo que, quando um certo dado ou instrução é carregado, os objetos próximos a ele na memória são carregados também: quando a primeira instrução de um laço é carregada no cache, um tempo razoável será economizado se as posições vizinhas da memória forem carregadas ao mesmo tempo. Dessa forma, o processador não terá que ir até a memória principal em busca das instruções seguintes. Por esse motivo, quando o cache carrega um certo dado ou código, o bloco dos itens vizinhos é também carregado. Cada bloco carregado no cache é identificado com um número chamado tag, que pode ser usado para determinar o endereço original dos dados ou instruções na memória principal. Desse modo, quando o processador está procurando por um dado ou código (chamados de word daqui em diante), ele precisa apenas conferir as tags para descobrir se a word está no cache. Quando o processador precisa de uma word, ele então checa o cache, como descrito no parágrafo anterior. Se a word for encontrada no cache, então dizemos que houve um cache hit. Caso contrário, dizemos que ocorreu um cache miss. Neste último caso, a word requisitada é carregada numa linha (o bloco de words vizinhas mais tag correspondente) no cache e a word é então enviada ao processador. Dependendo do design da interface do cache/processador, a word é carregada primeiro no cache e depois enviada ao processador ou carregada e enviada simultaneamente. Se o cache já estiver cheio e for preciso carregar um novo bloco da memória principal, é necessário decidir quais blocos do cache serão descartados. Existem várias formas de tomar tal decisão, baseadas em algoritmos chamados de algoritmos de substituição: ˆ Least Recently Used (LRU) Esse método substitui o bloco que não foi lido pelo processador há mais tempo. 3

ˆ ˆ First In First Out (FIFO) - Esse método substitui o bloco que está no cache há mais tempo. Least Frequently Used (LFU) - Esse método substitui o bloco que foi menos acessado desde que carregado no cache. ˆ Aleatório - Substitui um bloco selecionado aleatoriamente. 3 Testes 3.1 Sistema em que foram feitos os testes Todos os testes foram executados numa plataforma com a seguinte configuração: ˆ ˆ ˆ Processador Intel Core2Duo E4500, 2.20GHz, 64KB L1, 2MB L2 (shared); 2GB de RAM; Fedora 13 - Linux (2.6.34.7-56.fc13.i686); ˆ GCC 4.4.4; ˆ Valgrind 3.5.0. É importante salientar que os testes foram rodados em condições semelhantes (mesma quantidade de processos rodando etc.), para garantir resultados mais precisos. 3.2 teste1.c Esta subseção apresenta alguns resultados de testes feitos sobre teste1.c. O arquivo teste1.c mostra um exemplo simples, no qual uma matriz grande é alocada e, em seguida, preenchida com zeros. A questão está na forma como percorremos a matriz. Quando nenhum argumento é passado na linha de comando, a matriz é percorrida por linhas, caso contrário, ela é percorrida por colunas. 3.2.1 Testes com o código-fonte original O código teste1.c foi compilado usando o seguinte comando: $ gcc teste1.c -o teste1 A seguir, o programa teste1 foi executado de duas maneiras: Sem passar nenhum argumento pela linha de comando (a matriz foi percorrida por linhas): $ time./teste1 real user sys 0m0.684s 0m0.302s 0m0.237s $ time./teste1 real user sys 0m0.699s 0m0.296s 0m0.245s Passando um argumento pela linha de comando (a matriz foi percorrida por colunas): 4

$ time./teste1 argumento real user sys 0m1.998s 0m1.547s 0m0.360s $ time./teste1 argumento real user sys 0m2.274s 0m1.687s 0m0.400s Agora compilando com otimização: $ gcc -O2 teste1.c -o teste1 A seguir, novamente, o programa teste1 foi executado de duas maneiras: Sem passar nenhum argumento pela linha de comando (a matriz foi percorrida por linhas): $./teste1 real user sys 0m0.374s 0m0.073s 0m0.342s $./teste1 real user sys 0m0.371s 0m0.072s 0m0.364s Passando um argumento pela linha de comando (a matriz foi percorrida por colunas): $ time./teste1 argumento real user sys 0m1.871s 0m1.413s 0m0.393s $ time./teste1 argumento real user sys 0m1.867s 0m1.437s 0m0.388s Claramente, o programa foi consideravelmente mais rápido ao percorrer a matriz por linha. A seguir, o programa (compilado sem otimização), foi executado com o programa Valgrind, para que o comportamento em relação ao cache pudesse ser observado. Primeiro, execução sem argumentos (percorrendo por linhas): $ valgrind --tool=cachegrind./teste1 ==2187== Cachegrind, a cache and branch-prediction profiler ==2187== Copyright (C) 2002-2009, and GNU GPL d, by Nicholas Nethercote et al. ==2187== Using Valgrind-3.5.0 and LibVEX; rerun with -h for copyright info ==2187== Command:./teste1 5

==2187== ==2187== ==2187== I refs: 1,104,752,462 ==2187== I1 misses: 716 ==2187== L2i misses: 712 ==2187== I1 miss rate: 0.00% ==2187== ==2187== ==2187== D refs: 702,490,091 (601,646,842 rd + 100,843,249 wr) ==2187== D1 misses: 6,276,635 ( 13,519 rd + 6,263,116 wr) ==2187== L2d misses: 6,276,345 ( 13,271 rd + 6,263,074 wr) ==2187== D1 miss rate: 0.8% ( 0.0% + 6.2% ) ==2187== L2d miss rate: 0.8% ( 0.0% + 6.2% ) ==2187== ==2187== L2 refs: 6,277,351 ( 14,235 rd + 6,263,116 wr) ==2187== L2 misses: 6,277,057 ( 13,983 rd + 6,263,074 wr) ==2187== L2 miss rate: 0.3% ( 0.0% + 6.2% ) Depois, execução com argumento (percorrendo por colunas): $ valgrind --tool=cachegrind./teste1 argumento ==2197== Cachegrind, a cache and branch-prediction profiler ==2197== Copyright (C) 2002-2009, and GNU GPL d, by Nicholas Nethercote et al. ==2197== Using Valgrind-3.5.0 and LibVEX; rerun with -h for copyright info ==2197== Command:./teste1 argumento ==2197== ==2197== ==2197== I refs: 1,104,752,471 ==2197== I1 misses: 716 ==2197== L2i misses: 712 ==2197== I1 miss rate: 0.00% ==2197== ==2197== ==2197== D refs: 702,490,096 (601,646,845 rd + 100,843,251 wr) ==2197== D1 misses: 106,284,463 ( 6,272,892 rd + 100,011,571 wr) ==2197== L2d misses: 98,399,552 ( 94,246 rd + 98,305,306 wr) ==2197== D1 miss rate: 15.1% ( 1.0% + 99.1% ) ==2197== L2d miss rate: 14.0% ( 0.0% + 97.4% ) ==2197== ==2197== L2 refs: 106,285,179 ( 6,273,608 rd + 100,011,571 wr) ==2197== L2 misses: 98,400,264 ( 94,958 rd + 98,305,306 wr) ==2197== L2 miss rate: 5.4% ( 0.0% + 97.4% ) A quantidade de falhas de cache é mais elevada quando a matriz é percorrida por colunas. 3.2.2 Testes com o código alterado O código-fonte de teste1.c foi alterado (anexado com o nome de teste1-mod.c) para que pudesse apresentar o seguinte comportamento: teste1-mod é chamado com mais três argumentos na linha de comando, a saber, um inteiro que representa o número de linhas da matriz a ser alocada, um inteiro que representa o número de colunas da supracitada matriz e uma flag, que, quando assume o valor 0, percorre a matriz por linha e, quando assume outro valor, percorre a matriz por colunas. $./teste1-mod [número de linhas] [número de colunas] [flag] 6

O programa teste1-mod interpreta os argumentos fornecidos, aloca uma matriz com as dimensões especificadas e, de acordo com a flag - se esta for 0, preenche a matriz com zeros percorrendo-a por linhas; caso contrário, preenche a matriz com zeros percorrendo-a por colunas. A ideia é testar o programa com várias dimensões de matrizes, observando os resultados para as diferentes formas de percorrer a matriz. Foram usados alguns scripts para realizar os testes e exibir os resultados. O programa foi executado com várias dimensões de matrizes, ora por linha, ora por coluna, para comparar os tempos de execução. Em seguida, o programa foi executado com o Valgrind, para analisar falhas de cache. Os resultados destes testes vem a seguir. A tabela seguinte apresenta os tempos de execuções de teste1-mod para determinados números de linhas e de colunas, tanto para varredura da matriz por linha como por coluna. Dimensões da matriz Tempo de execução (ms) Elementos Linhas Colunas Orientada a linha Orientada a coluna 1 1 1 0m0.001s 0m0.001s 10 1 10 0m0.001s 0m0.001s 10 10 1 0m0.001s 0m0.001s 100 1 100 0m0.001s 0m0.001s 100 10 10 0m0.001s 0m0.001s 100 100 1 0m0.001s 0m0.001s 1000 1 1000 0m0.001s 0m0.001s 1000 10 100 0m0.001s 0m0.001s 1000 100 10 0m0.001s 0m0.001s 1000 1000 1 0m0.001s 0m0.001s 10000 1 10000 0m0.001s 0m0.001s 10000 10 1000 0m0.001s 0m0.001s 10000 100 100 0m0.001s 0m0.001s 10000 1000 10 0m0.001s 0m0.001s 10000 10000 1 0m0.003s 0m0.003s 100000 1 100000 0m0.002s 0m0.003s 100000 10 10000 0m0.002s 0m0.002s 100000 100 1000 0m0.002s 0m0.002s 100000 1000 100 0m0.003s 0m0.003s 100000 10000 10 0m0.004s 0m0.004s 100000 100000 1 0m0.015s 0m0.010s 1000000 1 1000000 0m0.008s 0m0.013s 1000000 10 100000 0m0.009s 0m0.014s 1000000 100 10000 0m0.009s 0m0.009s 1000000 1000 1000 0m0.009s 0m0.015s 1000000 10000 100 0m0.010s 0m0.015s 1000000 100000 10 0m0.017s 0m0.037s 1000000 1000000 1 0m0.096s 0m0.096s 10000000 1 10000000 0m0.077s 0m0.104s 10000000 10 1000000 0m0.077s 0m0.138s 10000000 100 100000 0m0.078s 0m0.122s 10000000 1000 10000 0m0.078s 0m0.170s 10000000 10000 1000 0m0.081s 0m0.147s 10000000 100000 100 0m0.093s 0m0.392s 10000000 1000000 10 0m0.164s 0m0.367s 10000000 10000000 1 0m0.935s 0m0.936s 7

100000000 1 100000000 0m0.761s 0m1.045s 100000000 10 10000000 0m0.759s 0m1.402s 100000000 100 1000000 0m0.757s 0m1.190s 100000000 1000 100000 0m0.762s 0m3.948s 100000000 10000 10000 0m0.762s 0m1.878s 100000000 100000 1000 0m0.792s 0m3.890s 100000000 1000000 100 0m0.915s 0m3.866s 100000000 10000000 10 0m1.612s 0m3.638s 100000000 100000000 1 0m57.955s 1m10.623s Apresentamos os tempos referentes às execuções para algumas matrizes em um gráfico, para melhor visualização: Observando os resultados, percebe-se que, para matrizes de até 100000 elementos não há diferença substancial entre os tempos de execução para a varredura orientada a linha e para a varredura orientada a coluna. Quando consideramos matrizes com número de elementos maior ou igual a 1000000, podemos observar que as execuções orientadas a linha foram, à medida que o número de elementos aumentava, progressivamente mais rápidas. É preciso fazer uma observação com relação a um caso: o caso da matriz 100000x1 (100000 linhas por 1 coluna) foi o único caso em que o programa orientado a coluna foi mais rápido que o orientado a linha. A tabela seguinte apresenta as falhas de cache de teste1-mod para determinados números de linhas e de colunas, tanto para varredura da matriz por linha como por coluna. Dimensões da matriz Falhas de cache Elementos Linhas Colunas Orientada a linha Orientada a coluna 1 1 1 I1 miss rate: 0.38% I1 miss rate: 0.38% L2i miss rate: 0.37% L2i miss rate: 0.37% D1 miss rate: 1.7% D1 miss rate: 1.7% L2d miss rate: 1.4% L2d miss rate: 1.4% L2 miss rate: 0.7% L2 miss rate: 0.7% 8

10 1 10 I1 miss rate: 0.38% I1 miss rate: 0.38% L2i miss rate: 0.37% L2i miss rate: 0.37% D1 miss rate: 1.7% D1 miss rate: 1.7% L2d miss rate: 1.4% L2d miss rate: 1.4% L2 miss rate: 0.7% L2 miss rate: 0.7% 10 10 1 I1 miss rate: 0.37% I1 miss rate: 0.37% L2i miss rate: 0.37% L2i miss rate: 0.37% D1 miss rate: 1.7% D1 miss rate: 1.7% L2d miss rate: 1.4% L2d miss rate: 1.4% L2 miss rate: 0.7% L2 miss rate: 0.7% 100 1 100 I1 miss rate: 0.38% I1 miss rate: 0.38% L2i miss rate: 0.38% L2i miss rate: 0.38% D1 miss rate: 1.7% D1 miss rate: 1.7% L2d miss rate: 1.4% L2d miss rate: 1.4% L2 miss rate: 0.7% L2 miss rate: 0.7% 100 10 10 I1 miss rate: 0.37% I1 miss rate: 0.37% L2i miss rate: 0.37% L2i miss rate: 0.36% D1 miss rate: 1.7% D1 miss rate: 1.7% L2d miss rate: 1.4% L2d miss rate: 1.4% L2 miss rate: 0.7% L2 miss rate: 0.7% 100 100 1 I1 miss rate: 0.31% I1 miss rate: 0.32% L2i miss rate: 0.31% L2i miss rate: 0.32% D1 miss rate: 1.7% D1 miss rate: 1.5% L2d miss rate: 1.4% L2d miss rate: 1.2% L2 miss rate: 0.6% L2 miss rate: 0.6% 1000 1 1000 I1 miss rate: 0.36% I1 miss rate: 0.34% L2i miss rate: 0.36% L2i miss rate: 0.34% D1 miss rate: 1.6% D1 miss rate: 1.5% L2d miss rate: 1.3% L2d miss rate: 1.3% L2 miss rate: 0.7% L2 miss rate: 0.6% 1000 10 100 I1 miss rate: 0.35% I1 miss rate: 0.35% L2i miss rate: 0.35% L2i miss rate: 0.35% D1 miss rate: 1.6% D1 miss rate: 1.6% L2d miss rate: 1.3% L2d miss rate: 1.3% L2 miss rate: 0.6% L2 miss rate: 0.6% 1000 100 10 I1 miss rate: 0.30% I1 miss rate: 0.30% L2i miss rate: 0.30% L2i miss rate: 0.30% D1 miss rate: 1.4% D1 miss rate: 1.4% L2d miss rate: 1.2% L2d miss rate: 1.2% L2 miss rate: 0.6% L2 miss rate: 0.6% 1000 1000 1 I1 miss rate: 0.13% I1 miss rate: 0.13% L2i miss rate: 0.13% L2i miss rate: 0.13% D1 miss rate: 0.7% D1 miss rate: 0.8% L2d miss rate: 0.6% L2d miss rate: 0.7% L2 miss rate: 0.2% L2 miss rate: 0.3% 10000 1 10000 I1 miss rate: 0.23% I1 miss rate: 0.18% L2i miss rate: 0.23% L2i miss rate: 0.18% D1 miss rate: 1.3% D1 miss rate: 0.9% L2d miss rate: 1.1% L2d miss rate: 0.8% L2 miss rate: 0.5% L2 miss rate: 0.4% 10000 10 1000 I1 miss rate: 0.22% I1 miss rate: 0.22% L2i miss rate: 0.22% L2i miss rate: 0.22% 9

D1 miss rate: 1.4% D1 miss rate: 1.4% L2d miss rate: 1.2% L2d miss rate: 1.2% L2 miss rate: 0.5% L2 miss rate: 0.5% 10000 100 100 I1 miss rate: 0.20% I1 miss rate: 0.20% L2i miss rate: 0.20% L2i miss rate: 0.20% D1 miss rate: 1.2% D1 miss rate: 1.2% L2d miss rate: 1.0% L2d miss rate: 1.0% L2 miss rate: 0.5% L2 miss rate: 0.5% 10000 1000 10 I1 miss rate: 0.10% I1 miss rate: 0.10% L2i miss rate: 0.10% L2i miss rate: 0.10% D1 miss rate: 1.2% D1 miss rate: 3.4% L2d miss rate: 0.6% L2d miss rate: 0.7% L2 miss rate: 0.2% L2 miss rate: 0.3% 10000 10000 1 I1 miss rate: 0.01% I1 miss rate: 0.01% L2i miss rate: 0.01% L2i miss rate: 0.01% D1 miss rate: 0.6% D1 miss rate: 0.6% L2d miss rate: 0.2% L2d miss rate: 0.2% L2 miss rate: 0.0% L2 miss rate: 0.0% 100000 1 100000 I1 miss rate: 0.05% I1 miss rate: 0.03% L2i miss rate: 0.05% L2i miss rate: 0.03% D1 miss rate: 0.8% D1 miss rate: 0.5% L2d miss rate: 0.8% L2d miss rate: 0.5% L2 miss rate: 0.3% L2 miss rate: 0.2% 100000 10 10000 I1 miss rate: 0.05% I1 miss rate: 0.04% L2i miss rate: 0.05% L2i miss rate: 0.04% D1 miss rate: 0.8% D1 miss rate: 0.8% L2d miss rate: 0.8% L2d miss rate: 0.7% L2 miss rate: 0.3% L2 miss rate: 0.3% 100000 100 1000 I1 miss rate: 0.05% I1 miss rate: 0.04% L2i miss rate: 0.04% L2i miss rate: 0.04% D1 miss rate: 0.8% D1 miss rate: 0.8% L2d miss rate: 0.8% L2d miss rate: 0.8% L2 miss rate: 0.3% L2 miss rate: 0.3% 100000 1000 100 I1 miss rate: 0.04% I1 miss rate: 0.04% L2i miss rate: 0.03% L2i miss rate: 0.04% D1 miss rate: 0.9% D1 miss rate: 10.0% L2d miss rate: 0.7% L2d miss rate: 0.7% L2 miss rate: 0.2% L2 miss rate: 0.2% 100000 10000 10 I1 miss rate: 0.01% I1 miss rate: 0.01% L2i miss rate: 0.01% L2i miss rate: 0.01% D1 miss rate: 1.0% D1 miss rate: 3.9% L2d miss rate: 0.3% L2d miss rate: 0.3% L2 miss rate: 0.1% L2 miss rate: 0.1% 100000 100000 1 I1 miss rate: 0.00% I1 miss rate: 0.00% D1 miss rate: 0.5% D1 miss rate: 0.5% L2d miss rate: 0.1% L2d miss rate: 0.1% L2 miss rate: 0.0% L2 miss rate: 0.0% 1000000 1 1000000 I1 miss rate: 0.00% I1 miss rate: 0.00% D1 miss rate: 0.7% D1 miss rate: 0.4% L2d miss rate: 0.7% L2d miss rate: 0.4% 10

L2 miss rate: 0.3% L2 miss rate: 0.1% 1000000 10 100000 I1 miss rate: 0.00% I1 miss rate: 0.00% D1 miss rate: 0.7% D1 miss rate: 11.5% L2d miss rate: 0.7% L2d miss rate: 0.7% L2 miss rate: 0.3% L2 miss rate: 0.2% 1000000 100 10000 I1 miss rate: 0.00% I1 miss rate: 0.00% D1 miss rate: 0.7 D1 miss rate: 0.7% L2d miss rate: 0.7% L2d miss rate: 0.7% L2 miss rate: 0.3% L2 miss rate: 0.3% 1000000 1000 1000 I1 miss rate: 0.00% I1 miss rate: 0.00% D1 miss rate: 0.8% D1 miss rate: 12.8% L2d miss rate: 0.7% L2d miss rate: 0.7% L2 miss rate: 0.3% L2 miss rate: 0.3% 1000000 10000 100 I1 miss rate: 0.00% I1 miss rate: 0.00% D1 miss rate: 0.8% D1 miss rate: 10.7% L2d miss rate: 0.7% L2d miss rate: 0.8% L2 miss rate: 0.3% L2 miss rate: 0.3% 1000000 100000 10 I1 miss rate: 0.00% I1 miss rate: 0.00% D1 miss rate: 0.9% D1 miss rate: 3.9% L2d miss rate: 0.9% L2d miss rate: 3.9% L2 miss rate: 0.3% L2 miss rate: 1.3% 1000000 1000000 1 I1 miss rate: 0.00% I1 miss rate: 0.00% D1 miss rate: 0.5% D1 miss rate: 0.5% L2d miss rate: 0.5% L2d miss rate: 0.5% L2 miss rate: 0.1% L2 miss rate: 0.1% 10000000 1 10000000 I1 miss rate: 0.00% I1 miss rate: 0.00% D1 miss rate: 0.7% D1 miss rate: 0.4% L2d miss rate: 0.7% L2d miss rate: 0.4% L2 miss rate: 0.3% L2 miss rate: 0.1% 10000000 10 1000000 I1 miss rate: 0.00% I1 miss rate: 0.00% D1 miss rate: 0.7% D1 miss rate: 11.6% L2d miss rate: 0.7% L2d miss rate: 0.7% L2 miss rate: 0.3% L2 miss rate: 0.2% 10000000 100 100000 I1 miss rate: 0.00% I1 miss rate: 0.00% D1 miss rate: 0.7% D1 miss rate: 12.4% L2d miss rate: 0.7% L2d miss rate: 0.7% L2 miss rate: 0.3% L2 miss rate: 0.3% 10000000 1000 10000 I1 miss rate: 0.00% I1 miss rate: 0.00% D1 miss rate: 0.7% D1 miss rate: 11.3% L2d miss rate: 0.7% L2d miss rate: 10.0% L2 miss rate: 0.3% L2 miss rate: 4.0% 10000000 10000 1000 I1 miss rate: 0.00% I1 miss rate: 0.00% 11

D1 miss rate: 0.7% D1 miss rate: 12.9% L2d miss rate: 0.7% L2d miss rate: 10.0% L2 miss rate: 0.3% L2 miss rate: 4.0% 10000000 100000 100 I1 miss rate: 0.00% I1 miss rate: 0.00% D1 miss rate: 0.8% D1 miss rate: 10.8% L2d miss rate: 0.8% L2d miss rate: 10.0% L2 miss rate: 0.3% L2 miss rate: 3.8% 10000000 1000000 10 I1 miss rate: 0.00% I1 miss rate: 0.00% D1 miss rate: 0.9% D1 miss rate: 3.9% L2d miss rate: 0.9% L2d miss rate: 3.9% L2 miss rate: 0.3% L2 miss rate: 1.3% 10000000 10000000 1 I1 miss rate: 0.00% I1 miss rate: 0.00% D1 miss rate: 0.5% D1 miss rate: 0.5% L2d miss rate: 0.5% L2d miss rate: 0.5% L2 miss rate: 0.1% L2 miss rate: 0.1% 100000000 1 100000000 I1 miss rate: 0.00% I1 miss rate: 0.00% D1 miss rate: 0.7% D1 miss rate: 0.4% L2d miss rate: 0.7% L2d miss rate: 0.4% L2 miss rate: 0.3% L2 miss rate: 0.1% 100000000 10 10000000 I1 miss rate: 0.00% I1 miss rate: 0.00% D1 miss rate: 0.7% D1 miss rate: 11.6% L2d miss rate: 0.7% L2d miss rate: 0.7% L2 miss rate: 0.3% L2 miss rate: 0.2% 100000000 100 1000000 I1 miss rate: 0.00% I1 miss rate: 0.00% D1 miss rate: 0.7% D1 miss rate: 12.4% L2d miss rate: 0.7% L2d miss rate: 0.7% L2 miss rate: 0.3% L2 miss rate: 0.3% 100000000 1000 100000 I1 miss rate: 0.00% I1 miss rate: 0.00% D1 miss rate: 0.7% D1 miss rate: 12.4% L2d miss rate: 0.7% L2d miss rate: 10.5% L2 miss rate: 0.3% L2 miss rate: 4.2% 100000000 10000 10000 I1 miss rate: 0.00% I1 miss rate: 0.00% D1 miss rate: 0.7% D1 miss rate: 13.2% L2d miss rate: 0.7% L2d miss rate: 12.2% L2 miss rate: 0.3% L2 miss rate: 4.9% 100000000 100000 1000 I1 miss rate: 0.00% I1 miss rate: 0.00% D1 miss rate: 0.7% D1 miss rate: 12.9% L2d miss rate: 0.7% L2d miss rate: 12.0% L2 miss rate: 0.3% L2 miss rate: 4.8% 100000000 1000000 100 I1 miss rate: 0.00% I1 miss rate: 0.00% D1 miss rate: 0.8% D1 miss rate: 10.5% 12

L2d miss rate: 0.8% L2d miss rate: 10.5% L2 miss rate: 0.3% L2 miss rate: 4.0% 100000000 10000000 10 I1 miss rate: 0.00% I1 miss rate: 0.00% D1 miss rate: 0.9% D1 miss rate: 3.9% L2d miss rate: 0.9% L2d miss rate: 3.9% L2 miss rate: 0.3% L2 miss rate: 1.3% 100000000 100000000 1 I1 miss rate: 0.00% I1 miss rate: 0.00% D1 miss rate: 0.5% D1 miss rate: 0.5% L2d miss rate: 0.5% L2d miss rate: 0.5% L2 miss rate: 0.1% L2 miss rate: 0.1% Assim como no caso do tempo de execução, percebe-se que, para matrizes de até 100000 elementos não há diferença substancial entre as taxas de falhas de cache para a varredura orientada a linha e para a varredura orientada a coluna. Quando consideramos matrizes com número de elementos maior ou igual a 1000000, podemos observar que as execuções orientadas a linha tiveram menos falhas que as orientadas a coluna (ou, em alguns poucos casos, porcentagens de falhas similares). 3.2.3 Por que percorrer matrizes por linhas ou por colunas faz tanta diferença? Os resultados dos testes mostram que é melhor percorrer matrizes por linhas (quando usando a linguagem C). Em C, matrizes são armazenadas na memória como se as linhas fossem concatenadas umas após as outras. Em outras palavras, C armazena vetores multidimensionais por linhas. Assim, quando a CPU necessita de um dado de uma certa posição de uma matriz (chamemos tal dado de m[i][j]), m[i][j] e os vizinhos dele estão ou serão carregados no cache (se possível). Mas os vizinhos de m[i][j] são os elementos da matriz que estão na mesma linha ou nas adjacentes (i.e. em colunas adjacentes da mesma linha ou no início ou no final de uma linha adjacente) - são os elementos m[i][j+1], etc., ou mesmo m[i+1][0] ou m[i-1][k] - onde k é o número de colunas subtraído de um. Assim sendo, se o programa for tal que o próximo dado necessário após m[i][j] segue uma varredura por linha, é grande a chance de que ele já 13

esteja no cache (i.e. o programa necessita de m[i][j+1], etc). Se, ao contrário, a varredura for feita por colunas, então seria necessário um dado de uma mesma coluna mas de outra linha (m[i+1][j]). Mas, nesse caso, seria maior a chance do dado não estar no cache, e portanto, provavelmente seria necessário ir até a memória principal, o que é mais custoso. Daí as maiores taxas de falhas de cache e tempos de execução mais longos para os programas que percorrem a matriz por colunas. 3.2.4 Com que tamanhos de matrizes essa diferença começa a ser perceptível? Os resultados dos testes mostram que uma diferença substancial só apareceu quando as matrizes tinham número de elementos da ordem de 1000000. Mais alguns testes, para tentar detectar um ponto em que as diferenças começam a ser perceptíveis: Dimensões da matriz Tempo de execução Elementos Linhas Colunas Orientada a linha Orientada a coluna 160000 400 400 0m0.004s 0m0.004s 250000 500 500 0m0.005s 0m0.006s 360000 600 600 0m0.006s 0m0.009s 490000 700 700 0m0.006s 0m0.011s 640000 800 800 0m0.008s 0m0.012s 810000 900 900 0m0.009s 0m0.014s 1000000 1000 1000 0m0.009s 0m0.015s Há uma diferença sutil para 250000 elementos. As diferenças mais substanciais começaram a aparecer com matrizes de tamanho entre 360000 e 490000 elementos. 3.2.5 Qual a relação deste valor com o tamanho do cache? A partir dos resultados, podemos afirmar que a diferença começa a ser perceptível quando o tamanho da matriz é em torno de 0.7 do tamanho do cache (L2). 3.3 teste2.c Esta subseção apresenta alguns resultados de testes feitos sobre teste2.c. O arquivo teste2.c mostra um programa que aloca três vetores, inicializa os dois primeiros, depois soma os dois posição a posição, guardando a resposta no terceiro. Porém, se um número é passado na linha de comando, o programa pula este número de posições a cada soma. O programa foi compilado com o seguinte comando: $ gcc teste2.c -o teste2 A tabela seguinte apresenta os tempos de execuções de teste2 para cada deslocamento. Deslocamento Tempo de execução 1 0m0.673s 2 0m0.697s 4 0m0.747s 8 0m0.888s 16 0m1.157s 32 0m1.290s 14

64 0m1.331s 128 0m1.288s 256 0m1.295s 512 0m1.381s 1024 0m1.566s 2048 0m1.541s 4096 0m1.548s 8192 0m1.560s 16384 0m1.585s 32768 0m1.555s 65536 0m1.223s 131072 0m1.008s 262144 0m0.959s 524288 0m0.951s 1048576 0m0.934s 2097152 0m0.754s 4194304 0m0.744s 8388608 0m0.692s O gráfico a seguir sumariza os dados da tabela: Os resultados dos testes mostram que a execução foi mais rápida quando o vetor foi percorrido sequencialmente. Os tempos de execução foram crescendo à medida que o deslocamento aumentava. Porém, a partir de 32768, os tempos de execução começaram a diminuir. Agora as falhas de cache: Deslocamento Falhas de cache 1 I1 miss rate: 0.00% D1 miss rate: 0.2% L2d miss rate: 0.2% L2 miss rate: 0.0% 2 I1 miss rate: 0.00% 15

D1 miss rate: 0.3% L2d miss rate: 0.3% L2 miss rate: 0.1% 4 I1 miss rate: 0.00% D1 miss rate: 0.6% L2d miss rate: 0.6% L2 miss rate: 0.2% 8 I1 miss rate: 0.00% D1 miss rate: 1.2% L2d miss rate: 1.2% L2 miss rate: 0.4% 16 I1 miss rate: 0.00% 32 I1 miss rate: 0.00% 64 I1 miss rate: 0.00% 128 I1 miss rate: 0.00% 256 I1 miss rate: 0.00% 512 I1 miss rate: 0.00% 1024 I1 miss rate: 0.00% 2048 I1 miss rate: 0.00% 16

4096 I1 miss rate: 0.00% 8192 I1 miss rate: 0.00% 16384 I1 miss rate: 0.00% 32768 I1 miss rate: 0.00% 65536 I1 miss rate: 0.00% 131072 I1 miss rate: 0.00% 262144 I1 miss rate: 0.00% 524288 I1 miss rate: 0.00% 1048576 I1 miss rate: 0.00% 2097152 I1 miss rate: 0.00% L2d miss rate: 0.2% L2 miss rate: 0.0% 4194304 I1 miss rate: 0.00% 17

D1 miss rate: 1.2% L2d miss rate: 0.2% L2 miss rate: 0.0% 8388608 I1 miss rate: 0.00% D1 miss rate: 0.2% L2d miss rate: 0.2% L2 miss rate: 0.0% 3.3.1 O que acontece quando percorremos vetores com deslocamentos? Como mostraram os testes, o desempenho é pior ao percorrermos vetores com deslocamentos ao invés de sequencialmente. 3.3.2 Para quais deslocamentos o desempenho fica pior? A partir dos testes, observa-se que o desempenho é pior do intervalo de 1024 a 32768 deslocamentos. 3.3.3 Você sabe explicar por que este valor causa tal queda no desempenho? Percorrer vetores sequencialmente é a melhor ideia porque os elementos contíguos do vetor são armazenados em posições contíguas da memória. O intervalo de pior desempenho pode ser interpretado como se estivéssemos percorrendo uma matriz por colunas (já que matrizes podem ser vistas como concatenações de suas linhas). A explicação para os valores maiores e menores possuírem o mesmo desempenho reside no fato deles poderem ser considerados equivalentes. Considere o exemplo simples a seguir: seja v um vetor com 10 elementos (v[0], v[1],...). Um deslocamento de dois em dois (v[0], v[2], v[4], v[6], v[8], v[1], v[3], v[5], v[7], v[9]) é equivalente a um deslocamento de oito em oito - só que para a esquerda (v[0], v[9], v[7], v[5], v[3], v[1], v[8], v[6], v[4], v[2]) 3.4 teste3.c O arquivo teste3.c é um programa que se divide em dois processos que compartilham uma estrutura com duas variáveis, sendo que cada processo altera uma destas variáveis. Compilado com o seguinte comando: $ gcc teste3.c -o teste3 Ao ser executado, teste3 apresentou o seguinte tempo de execução: $ time./teste3 real user sys 0m1.363s 0m1.215s 0m0.004s 3.4.1 Por que o programa teste3.c demora tanto para rodar? Pelo número de operações que faz (200000), podemos dizer que o programa teste3 demora para rodar. Essa demora deve-se a falhas de cache que ocorrem porque os dois cores tentam modificar simultaneamente a mesma estrutura de dados compartilhada (de 8 bytes) que está numa mesma linha do cache. 18

3.4.2 Você tem alguma solução melhor para este problema? Uma solução para este problema pode ser facilmente desenvolvida alterando ligeiramente a estrutura compartilhada (em anexo segue teste3-mod.c que contém tais modificações): struct shared_data_struct { unsigned int data1; char block[60]; unsigned int data2; }; A inclusão do vetor block na estrutura garante que data1 e data2 serão armazenados cada um em uma linha diferente do cache (o tamanho 60 escolhido para block é ideal porque o alinhamento do cache da CPU na qual foram rodados os testes é 64). A seguir, o tempo de execução para o programa com a estrutura modificada como acima: $ time./teste3-mod real user sys 0m0.600s 0m0.558s 0m0.004s Reiterando que o tamanho de block foi escolhido de acordo com a arquitetura do processador em que foram realizados os testes (mais informações: 3.1). 4 Programas adicionais Esta seção apresenta alguns programas adicionais, sendo que, para cada um, há uma versão cache-friendly (i.e. que usa bem o cache) e uma versão equivalente que impede otimizações de cache. 4.1 EP2 de MAC0300 - Segundo Semestre de 2009 Segue anexado o EP2 de MAC0300 (Métodos Numéricos da Álgebra Linear) do Semestre de 2009. Ele resolve sistemas lineares usando uma série de algoritmos vistos na supracitada 2º disciplina (Cholesky, LU, etc.). Tais algoritmos foram implementados orientados a linha e orientados a coluna, sendo que os do primeiro caso foram mais rápidos que os do segundo. Este programa foi incluído porque é um exemplo real (um programa que faz algo útil). 4.2 teste4 Com vistas a realizar um programa que não usasse bem o cache, foi criado o programa teste4. 4.2.1 teste4.c Esta versão não usa bem o cache. Vamos analisar o que o programa faz: Macros usadas no programa: #include <stdlib.h> #define SIZE 1000000 #define MAX 80 19

Estrutura usada no programa (é uma lista encadeada). Note como a estrutura foi declarada (os campos da estrutura). Funções. typedef struct node *link; struct node { int ind; int n; int seq[max]; link next; int square[max]; }; void square(int n, int a[]) { int i; for(i = 0; i < n; i++) a[i] = i*i; } void seq(int n, int a[]) { int i; for(i = 0; i < n; i++) a[i] = i; } Função principal do programa: Inicialização: int main() { int i, x; link head, p; head = (link)malloc(sizeof(struct node)); head->next = NULL; } for(i = 0; i < SIZE; i++) { p = (link)malloc(sizeof(struct node)); p->ind = i; p->n = rand()%max; p->next = head->next; head->next = p; srand(6431109); Neste loop ocorrerão as falhas de cache. for(i = 0; i < MAX*10; i++) { x = rand()%size; for(p = head->next; p!= NULL; p = p->next) { if(x == p->ind) { seq(p->n, p->seq); 20

} } } square(p->n, p->square); } return 0; Note que o programa gera um número aleatório x entre 0 e SIZE-1. O programa varre a lista à procura de um elemento que possua ind igual a x. Quando um elemento desejado é encontrado, são necessários os campos n, seq e square - sendo os dois últimos em sequência. Isso sugere que esses dados deveriam estar próximos na estrutura, mas não estão. Daí as falhas de cache. Tempo de execução: $ time./teste4 real user sys 0m55.597s 0m54.291s 0m0.841s Falhas de cache $ valgrind --tool=cachegrind./teste4 ==6172== Cachegrind, a cache and branch-prediction profiler ==6172== Copyright (C) 2002-2009, and GNU GPL d, by Nicholas Nethercote et al. ==6172== Using Valgrind-3.5.0 and LibVEX; rerun with -h for copyright info ==6172== Command:./teste4 ==6172== ==6172== ==6172== I refs: 7,515,139,769 ==6172== I1 misses: 718 ==6172== L2i misses: 714 ==6172== I1 miss rate: 0.00% ==6172== ==6172== ==6172== D refs: 5,771,731,742 (4,907,562,768 rd + 864,168,974 wr) ==6172== D1 misses: 1,602,018,589 (1,600,010,559 rd + 2,008,030 wr) ==6172== L2d misses: 1,599,100,113 (1,597,093,371 rd + 2,006,742 wr) ==6172== D1 miss rate: 27.7% ( 32.6% + 0.2% ) ==6172== L2d miss rate: 27.7% ( 32.5% + 0.2% ) ==6172== ==6172== L2 refs: 1,602,019,307 (1,600,011,277 rd + 2,008,030 wr) ==6172== L2 misses: 1,599,100,827 (1,597,094,085 rd + 2,006,742 wr) ==6172== L2 miss rate: 12.0% ( 12.8% + 0.2% ) É necessário salientar que o programa teste4 foi projetado de modo a promover muitas falhas de cache para a arquitetura especificada (3.1). 4.2.2 teste4-mod.c O arquivo teste4-mod.c apresenta uma versão equivalente ao programa teste4, porém, tenta fazer melhor uso do cache. Para que isso fosse possível, a estrutura usada teve de ser alterada - a ideia aqui é manter próximos dados que serão usados juntos: 21

typedef struct node *link; struct node { int ind; int n; int seq[max]; int square[max]; link next; }; O programa não sofreu alterações, apenas essa na estrutura mesmo. O loop principal do programa é o mesmo: os campos seq e square serão usados em sequência. A ordem de declaração dos campos da estrutura de lista encadeada fez com que o cache fosse melhor utilizado. Tempo de execução: $ time./teste4-mod real user sys 0m34.146s 0m33.106s 0m0.659s Falhas de cache $ valgrind --tool=cachegrind./teste4-mod ==6197== Cachegrind, a cache and branch-prediction profiler ==6197== Copyright (C) 2002-2009, and GNU GPL d, by Nicholas Nethercote et al. ==6197== Using Valgrind-3.5.0 and LibVEX; rerun with -h for copyright info ==6197== Command:./teste4-mod ==6197== ==6197== ==6197== I refs: 7,515,139,782 ==6197== I1 misses: 718 ==6197== L2i misses: 714 ==6197== I1 miss rate: 0.00% ==6197== ==6197== ==6197== D refs: 5,771,731,743 (4,907,562,769 rd + 864,168,974 wr) ==6197== D1 misses: 801,513,515 ( 800,505,563 rd + 1,007,952 wr) ==6197== L2d misses: 796,255,992 ( 795,250,099 rd + 1,005,893 wr) ==6197== D1 miss rate: 13.8% ( 16.3% + 0.1% ) ==6197== L2d miss rate: 13.7% ( 16.2% + 0.1% ) ==6197== ==6197== L2 refs: 801,514,233 ( 800,506,281 rd + 1,007,952 wr) ==6197== L2 misses: 796,256,706 ( 795,250,813 rd + 1,005,893 wr) ==6197== L2 miss rate: 5.9% ( 6.4% + 0.1% ) 5 Conclusão e Considerações finais CPUs são rápidas e estão ficando mais e mais rápidas. Memória, por outro lado, é algo lento. Diante desse impasse, surgem os caches: memórias menores mas mais rápidas, que armazenam os conteúdos mais utilizados, evitando idas à memória principal (o que é custoso e causa prejuízos à performance dos programas). Vimos através dos testes expostos por este Relatório que a eficiência do uso do cache está relacionada com uma série de práticas em nível de programação. Em linguagem C, isto significa: 22

ˆ Percorrer matrizes por linhas, uma vez que matrizes em C são armazenadas por linhas na memória. (teste1.c e EP de 300) ˆ Percorrer vetores sequencialmente, em vez de percorrer por deslocamentos. (teste2.c) ˆ Num contexto de compartilhamento de memória, declarar as variáveis de modo que um processo ou uma thread não sejam impedidos inutilmente de usar uma certa variável. (teste3.c) ˆ Manter próximos os dados que serão usados juntos. (teste4.c) Em vários casos, práticas como as descritas acima podem significar diferenças favoráveis na eficiência de um programa. 23

Referências [1] Memory Part 2: CPU caches [LWN.net], disponível in http://lwn.net/articles/252125/ [Setembro de 2010] [2] CPU cache - Wikipedia, the free encyclopedia disponível in http://en.wikipedia.org/wiki/ CPU_cache [Setembro de 2010] [3] TARNOFF, David. COMPUTER ORGANIZATION AND DESIGN FUNDAMENTALS - Examining Computer Hardware from the Bottom to the Top, p. 278-296 [2007] 24