470 likes | 707 Views
Hierarquia de Memória. Bruno C. Bourbon Jarbas J. de O. Júnior {bcb, jjoj} @ cin.ufpe.br. Roteiro. Motivação Introdução Organização da Cache Alinhamento do bloco da Cache Prefetching (pré-busca) Intercâmbio de laços Bloqueamento (Blocking) Garbage Collection e hierarquia de memória
E N D
Hierarquia de Memória Bruno C. Bourbon Jarbas J. de O. Júnior {bcb, jjoj} @ cin.ufpe.br
Roteiro • Motivação • Introdução • Organização da Cache • Alinhamento do bloco da Cache • Prefetching (pré-busca) • Intercâmbio de laços • Bloqueamento (Blocking) • Garbage Collection e hierarquia de memória • Conclusões • Referências
Motivação • No últimos vinte anos: • Velocidade da CPU aumentou 60% / ano • Velocidade da Memória apenas 10% / ano • A laguna pode ser preenchida com memória cache • A cache é sub-utilizada • Aproveitamento mínimo das grandes caches • Uso ineficiente = baixa performace • Como aumentar o uso? Conscientização da cache
Mais Motivos • Paralelismo da instruções: • Instruções SIMD consomem dados de 2 a 8 vezes mais que uma instrução normal • Lei de Probesting: • “Melhorias das tecnologias de compiladores dobra o desempenho dos computadores a cada 18 anos.” • Corolário: “Não espere que o compilador faça o serviço por você.” • Lei de Moore: • Consoles não a acompanham • Hardware fixo • Títulos de 2ª e 3ª gerações tem que melhor de alguma maneira
Introdução • Precisamos lembrar a arquitetura de memória:
Organização da Cache • Princípio da Localidade • Localidade Temporal: • Num futuro próximo, o programa irá referenciar as instruções e dados referenciados recentemente. • Localidade Espacial: • Num futuro próximo, o programa irá referenciar as instruções e dados que tenham endereços próximos das últimas referências.
Organização da Cache • Mapeamento • Direto • Completamente Associativo • Associativo por conjunto • Leitura • Busca em demanda (Fetch On Demand ou Fetch) • Pré-Busca (Prefetch) • Escrita • Write-Through • Write-Back
Mapeamento Direto • Cada bloco na memória principal é mapeado em uma única linha do cache • Módulo o número de blocos • Endereço é dividido em duas partes: • w bits menos significativos identificam um byte na linha • s bits mais significativos identificam um bloco • Os s bits são divididos em um campo que identifica a linha com r bits e em uma Tag de s-r bits
Mapeamento Completamente Associativo • Qualquer bloco da memória pode ser levado para qualquer linha do cache • O endereço é dividido em uma Tag que identifica a linha e no identificador do byte
Mapeamento Associativo por Conjunto • A cache é dividida em um número de conjuntos (set) • Cada conjunto tem um certo número de linha (define a associatividade) • Um dado bloco da memória pode ser carregado em qualquer linha de um único conjunto na cache • Módulo número de sets
Leitura da Cache (Busca) • Estratégias para busca de palavras ou linhas da memória principal • busca por demanda (fetch) • Pré-busca (prefetch) • Fetch por demanda • Fetch da linha quando ocorre miss • Estratégia mais simples, não exige hardware adicional • Prefetch • Fetch da linha antes que ela seja necessária • p.ex: Prefetch da linha i+1 quando a linha i é inicialmente referenciada
Escrita na Cache • Leitura na cache: não há discrepância entre cache e memória principal • Escrita na cache: cópias da palavra na cache e na memória principal podem ter valores diferentes • Valores deveriam ficar iguais em razão de: • Acessos de E/S feitos através da memória principal • Acessos à memória principal por múltiplos processadores • Tempo médio de acesso à cache é aumentado pela necessidade de atualizações da memória principal • Mecanismos de coerência de escrita • write-through • write-back
Alinhamento de Blocos da Cache • Blocos de Cache tem tipicamente o tamanho de um objeto. • Esperamos que um algoritmo que utilize um campo de um objeto utilize outros campos do mesmo. • Se x ocupa um Múltiplo do limite de B, então ele ocupa dois diferentes blocos da cache. • Se x não ultrapassa o limite um múltiplo de B, então os campos serão acessados em apenas um bloco de cache.
Alinhamento de Blocos da Cache • Alocar objetos seqüencialmente: • Se o próximo objeto não cabe na porção restante do bloco atual, pule para o começo do próximo bloco. • Aloque os objetos de tamanhos T num área de memória, todos alinhados com limite de multiplicidade T (em relação ao tamanho do bloco). • Elimina o cruzamento de blocos (block-crossing), sem desperdiçar espaço entre objetos de tamanho comum.
Alinhamento de Blocos da Cache • Desperdício de espaço: • Espaço vazio no final de cada bloco • Ganho de velocidade: • Se um conjunto S de objetos é frequentemente acessado, o alinhamento pode reduzir o espaço ocupado por S. Ajustando ao um espaço que caiba ele. • Alinhamento pode ser aplicado tanto a dados globais e estáticos como dinâmicos(heap). • Para dados globais e estáticos utiliza-se as diretivas de alocação da linguagem assembler. • Para variáveis dinâmicas(heap) o alocador de memória deverá cuidar disso em tempo de execução.
Alinhamento de Instruções • Instruções ocupa cache assim como dados: • Alinhamento e block-crossing são aplicavéis • Alinhe o inicio de conjuntos de instruções mais usadas no limite de um “múltiplo de B” • Instruções poucos usadas não deve se alinhadas com as mais utilizadas
Prefetching • Um miss na cache custa vários ciclos. • Se for na cache secundária custa ainda mais. • Em alguns casos pode ser previsto a utilização do dado. • O compilador pode utilizar uma instrução de prefetch para antecipar o carregamento de um dado (ou vários).
Prefetching • Se prefetching falhar não afetará a corretude do programa. • Muitas processadores possuem algum tipo de instrução de prefetch • A utilização de reordenação por levar ao efeito de prefetch.
Instruçõesde Prefetch • Algumas processadores na possuem instruções de prefetch, mas possuem instruções de load que não bloqueiam. • Ou seja utilizar um load para levar um dado para cache, mesmo que não seja utilizado naquele momento. • A dica: carrega os dados somente quando múltiplos do tamanho de um bloco
Prefetch para armazenamento • Às vezes podemos prever, em tempo de compilação, quando haverá uma falta de cache (cache miss) em uma instrução . for i <-to N – 1 A[i] <- i ------------------------------------------ for i <- 0 to N – 1 if i mod blocksize = 0 then prefetch A[i + K] A[i] <- i
Prefetch (resumo) • É aplicável quando: • A máquina possui uma instrução de prefetch; • A máquina não reordena as instruções dinamicamente; • O dado em questão é maior que a cache, ou não se espera que esteja na cache.
Intercâmbio de Laços • Considere o seguinte laço: for i <- 0 to N – 1 for j <- 0 to M – 1 for k <- 0 to P – 1 A[i,j,k] <- (B[i,j-1,k]+B[i,j,k]+B[i,j+1,k]) O valor B[i,j+1,k] é reusado na próxima iteração do laço do j (no qual seu “nome” é B[i,j,k])
k = 0 j = 1 i = 0 k = 1 j = 1 i = 0 k = 2 j = 1 i = 0 k = 0 j = 2 i = 0 Falta! Intercâmbio de Laços • for i <- 0 to N – 1 • for j <- 0 to M – 1 • for k <- 0 to P – 1 • A[i,j,k] <- (B[i,j-1,k]+B[i,j,k]+B[i,j+1,k]) j0 j1 j2 j3 ... k0 k1 k2 k3 ...
k = 0 j = 1 i = 0 k = 0 j = 2 i = 0 k = 0 j = 3 i = 0 Intercâmbio de Laços Trocando-se os laços... • for i <- 0 to N – 1 • for k <- 0 to P – 1 • for j <- 0 to M – 1 • A[i,j,k] <- (B[i,j-1,k]+B[i,j,k]+B[i,j+1,k]) j0 j1 j2 j3 ... k0 k1 k2 k3 ...
Intercâmbio de Laços MAS ATENÇÃO! • Para que um intercâmbio de laços seja válido,não pode haver dependência de dados entre as iterações. • É necessário examinar o grafo de dependência de dados do cálculo: • Dizemos que a iteração (j,k) depende da (j’,k’) se: • (j’,k’) calcular valores que são usados por (j,k) (leitura-depois-de-escrita) • (j’,k’) armazena valores que são sobre-escritos por (j,k) (escrita-depois-de-escrita) • Ou (j’,k’) lê valores que são sobre-escritos por (j,k) (escrita-depois-de-leitura)
Bloqueamento • Caso geral • Substituição Escalar • Bloqueamentoem todos os níveis da hierarquia de memória • Desenrolando “as parada” (Unroll and Jam)
Bloqueamento • Considere o seguinte laço para multiplicação de matrizes, C=A.B for i <- 0 to N – 1 for j <- 0 to N – 1 for k <- 0 to N – 1 C[i,j] <- C[i,j] + A[i,k].B[k,j] • Supondo que apenas as matrizes A e B e nada mais coubesse simultaneamente na memória cache, o laço k roda sem faltas, havendo somente uma falta para C[i,j] em cada iteração do laço j.
Bloqueamento for i <- 0 to N – 1 for j <- 0 to N – 1 for k <- 0 to N – 1 C[i,j] <- C[i,j] + A[i,k].B[k,j] j0 j1 j2 j3 ... j0 j1 j2 j3 ... i0 i0 i1 i1 i2 i2 MEM. CACHE i3 i3 ... ... A B
Bloqueamento • E se a cache, for menor que o suficiente para armazenar as duas matrizes simultaneamente? • Mais exatamente: se a cache suportar no máximo 2.c.N elementos (pontos flutuantes), onde 1 < c < N? • Nesse caso, para as referências a B[k,j], haverão sempre faltas.
Linha i1 de A Linha i0 de A Coluna j0 de B Coluna j(2c) de B Coluna j0 de B Coluna j(2c+1) de B Coluna j1 de B Linha i1 Linha i0 Coluna j(2c+2) de B Coluna j2 de B 2.c < N !!! Então sempre ocorrerá falta de B[k,j] depois que a cache encher pela primeira vez Bloqueamento N for i <- 0 to N – 1 for j <- 0 to N – 1 for k <- 0 to N – 1 C[i,j] <- C[i,j] + A[i,k].B[k,j] 0 1 j0 j1 j2 j3 ... j0 j1 j2 j3 ... 2 i0 3 i1 ... i2 i3 MEM. CACHE 2.c ... A B
Bloqueamento for i <- 0 to N – 1 for j <- 0 to N – 1 for k <- 0 to N – 1 C[i,j] <- C[i,j] + A[i,k].B[k,j] • Neste caso, o intercâmbio de laço não resolve, pois se o laço j ficar para fora, os elementos de A sofrerão faltas. E se o laço k ficar para fora, os elementos de C sofrerão faltas!
Bloqueamento • O pulo da gato: • Reusar as linhas da matriz A e as colunas de B enquanto ainda estão na cache. • Para isso podemos calcular o bloco c x c da matriz C a partir de c linhas de A e c colunas de B. Já que nossa memória cache possui tamanho 2.c.N: for i <- i0 to i0 + c - 1 for j <- j0 to j0 + c -1 for k <- 0 to N – 1 C[i,j] <- C[i,j] + A[i,k].B[k,j]
Lin i1 A Lin i0 A Lin i2 A Col j0 B Col j1 B Lin i3 A Linha i0 Col j2 B Linha i1 Linha i2 Linha i3 Col j(2c) B Bloqueamento 2c c c for i <- i0 to i0 + c - 1 for j <- j0 to j0 + c -1 for k <- 0 to N – 1 C[i,j] <- C[i,j] + A[i,k].B[k,j] 0 1 j0 j1 j2 j3 ... j0 j1 j2 j3 ... 2 i0 3 i1 ... i2 i3 MEM. CACHE N ... A B
Bloqueamento • Agora só falta juntar os blocos: for i0 <- 0 to N – 1 by c for j0 <- 0 to N – 1 by c for i <- i0 to min(i0 + c – 1, N – 1) for j <- j0 to min (j0 + c –1, N – 1) for k <- 0 to N – 1 C[i,j] <- C[i,j] + A[i,k].B[k,j]
Bloqueamento • Faltas: • As faltas agora são apenas para carregar na cache as novas colunas e as novas linhas cada vez que se for calcular um novo bloco c x c. • Para carregar as novas c linhas e c colunas ocorrem 2.c.N faltas. E para calcular um bloco c x c são necessárias c.c.N iterações. • Por tanto, o total de faltas por iteração é: 2.c.N / c.c.N = 2/c
Bloqueamento:Substituição escalar • Mesmo que o acesso a C[i,j] quase nunca provoque uma falta na cache, nós poderíamos ainda deixá-la um nível de memória acima: nos registradores. for i <- i0 to i0 + c - 1 for j <- j0 to j0 + c –1 s <- C[i,j] for k <- 0 to N – 1 s <- s + A[i,k].B[k,j] C[i,j] <- s
Bloqueamento:Em todos os níveis da hierarquia • Se quisermos usar d registradores de ponto flutuante, podemos reescrever o código do cálculo do bloco c x c como: for i <- i0 to i0 + c – 1 for k0 <- 0 to N – 1 byd for k <- k0 to k0 + d – 1 T[k-k0] <- A[i,k] for j <- j0 to j0 + c –1 s <- C[i,k] for k <- k0 to k0 + d – 1 s <- s + T[k-k0].B[k,j] C[i,j] <- s Preenchendo os registradores
Bloqueamento:Desenrolando “as parada” (Unroll and Jam) • Porém, para usar bloqueamento no nível de registradores, precisamos “desenrolar” o laço pois os registradores não podem ser indexados por subscripts. • Assim, supondo d = 3 (3 registadores): for i <- i0 to i0 + c – 1 for k0 <- 0 to N – 1 by3 t0 <- A[i,k0]; t1 <- A[i,k0+1]; t2 <- A[i,k0+2] for j <- j0 to j0 + c –1 C[i,j] <- C[i,j] + t0.B[k0,j] + t1.B[k0+1,j] + t2.B[k0+2,j]
Garbage Collection e Hierarquia de Memória • Sistemas que usam Garbage Collection têm a fama de não usar a memória de forma otimizada. • Porém, pode ser organizado para gerenciar a memória de uma melhor forma: • Gerações • Usar cache secundária para guardar a geração mais jovem • Alocação Seqüencial • Poucos conflitos • Prefetching para Alocação • Agrupando objetos relacionados. • Busca em profundidade • Busca em largura
Conclusão • A otimização do uso da memória cache pode aumentar muito o desempenho de um programa; • As técnicas vistas são úteis tanto para projeto de compiladores como para desenvolvimento de software em geral; • Exemplo concreto da importância do estudo de compiladores por desenvolvedores.
Referências • APPEL, Andrew W., Modern compiler implementation in Java. • ERICSON, Christer, Memory Otimization, GDC 2003 • SILVA, Ivan S., CASSILO, Leonardo. Organização e Arquitetura de Computadores I: Memória. DIMAP – UFRN. • BARROS, Edna N. S., Infra-Estrutura de Hardware: Memória e Hierarquia de Memória. CIn - UFPE