Se analisarmos um programa típico (como muitos pesquisadores analisaram), descobriremos que ele tende a acessar as mesmas posições de memória repetidamente. Além disto, também descobriremos que um programa frequentemente acessa posições de memória adjacentes. Os nomes técnicos dados a estes fenômenos são localidade temporal de referência e localidade espacial de referência. Quando demonstra localidade espacial, um programa acessa posições de memória vizinhas. Quando exibe localidade temporal de referência, um programa acessa repetidamente a mesma posição de memória durante um curto espaço de tempo. Ambas as formas de localidade ocorrem no seguinte segmento de código Pascal:
for i := 0 to 10 do
A [i] := 0;
Há duas ocorrências de cada uma das localidades, espacial e temporal, dentro deste loop. Vamos considerar a primeira e a mais óbvia.
No código Pascal acima, o programa referencia a variável i muitas vezes. O loop for compara i com 10 para ver se o loop terminou. Ele também incrementa i no final do loop. A operação de atribuição também utiliza i como um índice de array. Isto mostra a localidade temporal de referência em ação, uma vez que a CPU acessa i em três pontos num curto intervalo de tempo.
Este programa também apresenta localidade espacial de referência. O próprio loop zera os elementos do array A escrevendo um zero na primeira posição de A, depois na segunda posição de A, e assim por diante. Assumindo que o Pascal armazena os elementos de A dentro de posições de memória consecutivas, cada iteração do loop acessa posições de memória adjacentes.
Há um exemplo adicional de localidade temporal e espacial no exemplo acima, embora ele não seja tão óbvio. Instruções de computador que indicam uma tarefa específica que o sistema deve realizar também aparecem na memória. Essas instruções aparecem sequencialmente na memória - a localidade espacial. O computador também executa estas instruções repetidamente, uma vez para cada iteração - a localidade temporal.
Se olharmos para o perfil de execução de um programa comum, descobriremos que, em geral, o programa executa menos da metade das instruções. Um programa comum, geralmente, utilizaria apenas 10 a 20% da memória destinada a ele. Num dado momento, um programa de um megabyte poderia talvez acessar de quatro a oito kilobytes de dados e código. Então, se gastamos uma soma escandalosa de dinheiro por uma RAM cara de zero estados de espera, não estaremos utilizando a maior parte dela em qualquer dado momento! Não seria melhor se pudéssemos comprar uma pequena quantidade de RAMs rápidas e redeterminar dinamicamente seus endereços à medida que o programa fosse sendo executado?
É exatamente isto o que a memória cache faz. A memória cache fica entre a CPU e a memória principal. É uma pequena porção de memória muito rápida (com zero estados de espera). Ao contrário da memória convencional, os bytes que aparecem dentro de uma cache não têm endereços fixos. A memória cache pode redeterminar o endereço de um dado. Isto permite que o sistema mantenha os valores acessados recentemente na cache. Endereços que a CPU nunca acessou ou não acessou recentemente ficam na memória principal (lenta). Já que a maior parte dos acessos à memória correspondem a variáveis acessadas recentemente (ou em posições próximas de posições acessadas recentemente), o dado geralmente aparece na memória cache.
A memória cache não é perfeita. Embora um programa possa gastar um tempo considerável executando código num local, ele eventualmente chamará um procedimento ou desviará para alguma seção distante de código, fora da memória cache. Nestes casos, a CPU precisa acessar a memória principal para buscar os dados. Como a memória principal é lenta, haverá a necessidade de inserir estados de espera.
Um acerto de cache ("cache hit") ocorre sempre que a CPU acessar a memória e encontrar o dado procurado na cache. Neste caso, a CPU pode realmente acessar o dado com zero estados de espera. Uma falha de cache ("cache miss") ocorre se a CPU acessar a memória e o dado não estiver presente na cache. Então a CPU precisa ler o dado da memória principal, causando uma perda de performance. Para tirar vantagem da localidade de referência, a CPU copia dados para dentro da cache sempre que ela acessar um endereço ausente na cache. Como é provável que o sistema acesse aquela mesma posição pouco tempo depois, aquele dado na cache faz com que o sistema economize estados de espera.
Como descrito acima, a memória cache trata dos aspectos temporais de acesso à memória, mas não dos aspectos espaciais. Armazenar posições da memória quando estas são acessadas não agilizará o programa se acessarmos constantemente posições consecutivas (localidade espacial). Para resolver este problema, muitos sistemas de cache lêem muitos bytes consecutivos da memória quando ocorre uma falha de cache. O 80486, por exemplo, lê 16 bytes de uma vez quando a cache falha. Se lermos 16 bytes, porque lê-los em blocos ao invés de quando precisarmos deles? Muitos chips de memória disponíveis atualmente têm modos especiais que permitem o acesso rápido de várias posições de memória consecutivas no chip. A cache tira proveito desta capacidade para reduzir o número médio de estados de espera necessários para acessar a memória.
Se escrevermos um programa que acessa a memória randomicamente, utilizar a memória cache, na verdade, pode torná-lo mais lento. Ler 16 bytes a cada falha de cache é caro se apenas acessarmos uns poucos bytes na linha de cache correspondente. Apesar de tudo, os sistemas de memória cache funcionam muito bem.
Não deve ser surpresa que a proporção entre acertos e falhas de cache aumenta com o tamanho (em bytes) do subsistema de memória cache. O chip 80486, por exemplo, tem 8.192 bytes de cache por chip. A Intel declara obter uma taxa de acerto de 80 a 95% com esta cache (significa que em 80 a 95% das vezes a CPU encontra o dado na cache). Isto parece muito impressionante. Contudo, se brincarmos um pouco com os números, veremos que isto não é tão impressionante assim. Suponha que consideremos os 80% do número. Então, na média, um em cada cinco acessos à memória não estará na cache. Se tivermos um processador de 50 MHz e um tempo de acesso à memória de 90 ns, quatro dos cinco acessos precisam de apenas um ciclo de clock (já que eles estão na cache) e o quinto necessitará de aproximadamente 10 estados de espera. No total, o sistema necessitará de 15 ciclos de clock para acessar cinco posições de memória ou, em média, três ciclos de clock por acesso. Isto é o equivalente a dois estados de espera adicionados a cada acesso à memória. Você acredita agora que sua máquina roda com zero estados de espera?
Há duas formas de melhorar a situação. Primeiro, podemos adicionar mais memória cache. Isto melhora a taxa de acerto de cache, reduzindo o número de estados de espera. Por exemplo, aumentando a taxa de acerto de 80% para 90% permite que 10 posições de memória sejam acessadas em 20 ciclos. Isto reduz o número médio de estados de espera por acesso à memória para um estado de espera - uma melhora substancial. Só que não podemos desmontar um chip 80486 e soldar mais memória cache no chip. Contudo, a CPU do 80586/Pentium tem uma cache significantemente maior do que a do 80486 e opera com muito menos estados de espera.

Fig.13 - Cache de dois níveis
Uma outra forma de melhorar a performance é construir um sistema de cache de dois níveis. Muitos sistemas 80486 funcionam assim. O primeiro nível é a chache de 8.192 bytes no chip. O nível seguinte, entre a cache no chip e a memória principal, é uma cache secundária construída no sistema de circuitos da placa do computador.
Uma cache secundária comum possui algo em torno de 32.768 bytes a um megabyte de memória. Tamanhos comuns em subsistemas de PC são 65.536 e 262.144 bytes de cache.
Você poderia perguntar "Por que se incomodar com uma cache de dois níveis? Por que não utilizar uma cache que tenha 262.144 bytes?" Bem, a cache secundária geralmente não opera com zero estados de espera. Circuitos que oferecem 262.144 bytes de memória de 10 ns (com tempo total de acesso de 20 ns) seriam muito caros. Por isso a maioria dos projetistas de sistemas utilizam memórias mais lentas que requerem um ou dois estados de espera. Isto ainda é muito mais rápido do que a memória principal. Combinada com a cache no chip da CPU, obtém-se uma melhor performance do sistema.
Considere o exemplo anterior, com uma taxa de acerto de 80%. Se a cache secundária precisar de dois ciclos para cada acesso à memória e de três ciclos para o primeiro acesso, então uma falha de cache na cache do chip precisará de seis ciclos de clock. Tudo indica que a média da performance do sistema será de dois clocks por acesso à memória, o que é um pouco mais rápido do que os três necessários pelo sistema sem a cache secundária. Além do mais, a cache secundária pode atualizar seus valores em paralelo com a CPU. Deste modo, o número de falhas de cache (o que afeta a performance da CPU) diminui.
Você provavelmente está pensando "Até agora tudo isto parece interessante, mas o que tem a ver com programação?" Na verdade, pouco. Escrevendo seus programas cuidadosamente para tirar vantagem da forma como o sistema de memória cache funciona, você pode melhorar a performance dos mesmos. Colocando as variáveis usadas com mais frequência na mesma linha de cache, você pode forçar o sistema de cache a carregar essas variáveis como um grupo, economizando estados de espera extras em cada acesso.
Se você organizar seu programa do forma que ele tenha a tendência de executar a mesma sequência de instruções repetidamente, ele terá um alto grau de localidade temporal de referência e, desta forma, será executado mais rapidamente.