Primeiro de tudo, o acesso à memória principal é muito caro. Atualmente, uma CPU de 2GHz (a mais lenta uma vez) possui ticks de 2G (ciclos) por segundo. Uma CPU (núcleo virtual hoje em dia) pode buscar um valor de seus registros uma vez por tick. Como um núcleo virtual consiste em várias unidades de processamento (ALU - unidade lógica aritmética, FPU etc.), ele pode realmente processar determinadas instruções em paralelo, se possível.
Um acesso à memória principal custa cerca de 70ns a 100ns (DDR4 é um pouco mais rápido). Dessa vez, é basicamente procurar o cache L1, L2 e L3 e depois bater na memória (comando send para o controlador de memória, que o envia para os bancos de memória), aguardar a resposta e pronto.
100ns significa cerca de 200 carrapatos. Então, basicamente, se um programa sempre perder os caches que cada memória acessa, a CPU gasta cerca de 99,5% de seu tempo (se apenas lê memória) ociosa aguardando a memória.
Para acelerar as coisas, existem os caches L1, L2, L3. Eles usam a memória sendo diretamente colocada no chip e usando um tipo diferente de circuitos de transistor para armazenar os bits fornecidos. Isso requer mais espaço, mais energia e é mais caro do que a memória principal, pois uma CPU geralmente é produzida usando uma tecnologia mais avançada e uma falha de produção na memória L1, L2, L3 tem a chance de tornar a CPU sem valor (defeito). caches grandes de L1, L2, L3 aumentam a taxa de erro que diminui o rendimento que diminui diretamente o ROI. Portanto, há uma grande troca quando se trata do tamanho do cache disponível.
(atualmente, cria-se mais caches L1, L2, L3 para poder desativar determinadas porções para diminuir a chance de um defeito de produção real ser a área de memória de cache que processa o defeito da CPU como um todo).
Para dar uma ideia de tempo (fonte: custos para acessar caches e memória )
- Cache L1: 1ns a 2ns (2-4 ciclos)
- Cache L2: 3ns a 5ns (6-10 ciclos)
- Cache L3: 12ns a 20ns (24-40 ciclos)
- RAM: 60ns (120 ciclos)
Como misturamos diferentes tipos de CPU, essas são apenas estimativas, mas dá uma boa idéia do que realmente está acontecendo quando um valor de memória é buscado e podemos ter um acerto ou um erro em determinada camada de cache.
Portanto, um cache basicamente acelera bastante o acesso à memória (60ns vs. 1ns).
Buscar um valor, armazená-lo no cache para a possibilidade de relê-lo é bom para variáveis que são frequentemente acessadas, mas para operações de cópia em memória ainda seria lento, pois basta ler um valor, gravar o valor em algum lugar e nunca ler o valor novamente ... nenhum acerto no cache, lento (ao lado disso pode acontecer em paralelo, pois temos execução fora de ordem).
Essa cópia de memória é tão importante que existem diferentes meios para acelerá-la. Nos primeiros dias, a memória costumava copiar memória fora da CPU. Ele foi tratado diretamente pelo controlador de memória, portanto, uma operação de cópia de memória não poluiu os caches.
Mas, além de uma cópia simples da memória, outro acesso serial à memória era bastante comum. Um exemplo é analisar uma série de informações. Ter uma matriz de números inteiros e calcular a soma, média, média ou até mais simples encontrar um determinado valor (filtro / pesquisa) foi outra classe muito importante de algoritmos executados sempre em qualquer CPU de uso geral.
Portanto, analisando o padrão de acesso à memória, ficou claro que os dados são lidos sequencialmente com muita frequência. Havia uma alta probabilidade de que se um programa ler o valor no índice i, que o programa também leia o valor i + 1. Essa probabilidade é um pouco maior que a probabilidade de o mesmo programa também ler o valor i + 2 e assim por diante.
Portanto, dado um endereço de memória, foi (e ainda é) uma boa idéia para ler adiante e buscar valores adicionais. Esta é a razão pela qual existe um modo de impulso.
O acesso à memória no modo de aumento significa que um endereço é enviado e vários valores são enviados seqüencialmente. Cada envio de valor adicional leva apenas 10ns adicionais (ou mesmo abaixo).
Outro problema foi um endereço. Enviar um endereço leva tempo. Para endereçar uma grande parte da memória, é necessário enviar endereços grandes. Nos primeiros dias, isso significava que o barramento de endereços não era grande o suficiente para enviar o endereço em um único ciclo (tick) e era necessário mais de um ciclo para enviar o endereço, adicionando mais atraso.
Uma linha de cache de 64 bytes, por exemplo, significa que a memória é dividida em blocos distintos (sem sobreposição) de memória, com tamanho de 64 bytes. 64 bytes significa que o endereço inicial de cada bloco tem os seis bits de endereço mais baixos a serem sempre zeros. Portanto, não é necessário enviar esses seis bits zero a cada vez, aumentando o espaço de endereço 64 vezes para qualquer número de largura do barramento de endereços (efeito de boas-vindas).
Outro problema que a linha de cache resolve (além de ler adiante e salvar / liberar seis bits no barramento de endereços) está na maneira como o cache é organizado. Por exemplo, se um cache seria dividido em blocos (células) de 8 bytes (64 bits), é necessário armazenar o endereço da célula de memória para a qual essa célula de cache mantém o valor. Se o endereço também tiver 64 bits, isso significa que metade do tamanho do cache é consumida pelo endereço, resultando em uma sobrecarga de 100%.
Como uma linha de cache tem 64 bytes e uma CPU pode usar 64 bits - 6 bits = 58 bits (não há necessidade de armazenar os zero bits corretamente), podemos armazenar em cache 64 bytes ou 512 bits com uma sobrecarga de 58 bits (11% de sobrecarga). Na realidade, os endereços armazenados são ainda menores que isso, mas existem informações de status (como a linha de cache é válida e precisa, suja e precisa ser gravada novamente no ram etc.).
Outro aspecto é que temos cache associativo definido. Nem todas as células de cache conseguem armazenar um determinado endereço, mas apenas um subconjunto desses. Isso torna os bits de endereço armazenados necessários ainda menores, permite acesso paralelo ao cache (cada subconjunto pode ser acessado uma vez, mas independente dos outros subconjuntos).
Mais especificamente, quando se trata de sincronizar o acesso ao cache / memória entre os diferentes núcleos virtuais, suas múltiplas unidades de processamento independentes por núcleo e, finalmente, vários processadores em uma placa principal (na qual existem placas com até 48 processadores e mais).
Essa é basicamente a ideia atual de por que temos linhas de cache. O benefício de ler adiante é muito alto e o pior caso de ler um único byte de uma linha de cache e nunca ler o resto novamente é muito pequeno, pois a probabilidade é muito pequena.
O tamanho da linha de cache (64) é uma escolha acertada entre linhas de cache maiores, tornando improvável que o último byte seja lido também no futuro próximo, a duração necessária para buscar a linha de cache completa da memória (e para gravá-lo de volta) e também a sobrecarga na organização do cache e a paralelização do acesso ao cache e à memória.