Cache e Memória: Localização de Programas no Sistema

O princípio de localidade desempenha um papel crucial no desempenho da CPU. Considere este código simples que demonstra o acesso a memória contígua:

#include <stdio.h>
int main() {
    int valores[5] = {10, 20, 30, 40, 50};
    int total = 0;
    for (int idx = 0; idx < 5; idx++) {
        total += valores[idx];
        printf("Valor %d: %d\n", idx, valores[idx]);
    }
    printf("Total: %d\n", total);
    return 0;
}

Neste exemplo, a CPU frequentemente acessa endereços adjacentes no array valores, ilustrando a localidade espacial. Isso ocorre porque, após a compilação, as instruções e dados ocupam endereços fixos na memória, e a CPU tende a reutilizar ou acessar dados vizinhos.

A memória física, ou DRAM, é composta por chips de armazenamento na placa de circuito impresso, com capacitores que representam bits através de níveis de carga. Esses capacitores sofrem vazamento ao longo do tempo, exigindo atualização periódica para manter a integridade dos dados. O controlador de memória, historicamente integrado no chipset norte, agora reside dentro da CPU, otimizando o acesso.

Logicamente, a memória pode ser vista como um vasto array de bytes, onde cada endereço corresponde a um índice. Quando a CPU solicita dados e a memória não está pronta, o barramento insere ciclos de espera, tornando a memória um gargalo potencial para o desempenho do sistema.

Para mitigar esse problema, utiliza-se o cache, uma memória rápida e pequena posicionada entre a CPU e a memória principal. Ele armazena cópias dos dados recentemente acessados, explorando o princípio de localidade. O cache é composto por memória estática de alta velocidade, um módulo de conversão de endereços e um algoritmo de substituição de linhas.

O fluxo de trabalho do cache envolve a segmentação do endereço da CPU em número de conjunto, número de linha e deslocamento dentro da linha. Se o dado estiver no cache, é retornado imediatamente; caso contrário, é carregado da memória e armazenado no cache, substituindo linhas menos usadas conforme necessário. Esse processo é transparente para o softawre.

Entretanto, o cache introduz desafios de consistência de dados. Em um CPU com múltiplos núcleos e cache multinível, as inconsistências podem surgir entre caches de instruções e dados no mesmo núcleo, entre caches de diferentes núcleos e entre cache e memória de dispositivos.

Por exemplo, em código auto-modificável, alterações na memória podem entrar no cache de dados, mas o cache de instruções pode conter versões desatualizadas. A solução envolve escrever os dados no cache de dados para a memória e invalidar o cache de instruções. Para caches entre núcleos, protocolos como MESI gerenciam a consistência, definindo estados como Modificado, Exclusivo, Compartilhado e Inválido para sincronizar os dados.

Para ativar o cache em sistemas x86, manipulam-se bits específicos no registrador CR0. O código a seguir demonstra isso usando instruções assembly, com variáveis renomeadas para clareza:

mov ecx, cr0
and ecx, 0xFFFFFFFFBFFFFFFF  ; Limpa o bit CD (bit 30)
and ecx, 0xFFFFFFFFDFFFFFFF  ; Limpa o bit NW (bit 29)
mov cr0, ecx

Este código desativa os bits CD e NW para habilitar o cache, garantindo que a CPU mantenha a consistência da memória.

Para obter o mapa de memória durante a inicialização, o BIOS oferece serviços de interrupção no modo real. O código abaixo itera para coletar informações de memória usando a interrupção 15h, com parâmetros ajustados para coletar estruturas de dados:

_obter_mapa_memoria:
    xor esi, esi               ; Inicializa índice
    mov edi, ENDERECO_MAPA     ; Destino dos dados
ciclo:
    mov eax, 0x0000E820        ; Função para mapa de memória
    mov ecx, 20                ; Tamanho de cada entrada
    mov edx, 0x534D4150        ; Assinatura
    int 0x15                   ; Executa interrupção
    jc erro                    ; Trata erros
    add edi, 20                ; Avança para próxima entrada
    cmp esi, 0                 ; Verifica continuação
    jne ciclo
    ret
erro:
    ; Rotina de tratamento de erro

Os dados coletados podem ser representados em C como uma estrutura para mapeamento de memória:

#define MEMORIA_UTILIZAVEL 1
#define MEMORIA_RESERVADA 2
#define MEMORIA_ACPI 3
#define MEMORIA_NVS 4
#define MEMORIA_DEFEITUOSA 5

typedef struct {
    unsigned long long endereco_base;
    unsigned long long comprimento;
    unsigned int tipo;
} entrada_mapa_memoria;

Tags: cache Memória princípio de localidade CPU DRAM

Publicado em 6-19 21:12