⚙️
CPU: pipeline, cache L1/L2/L3, branch prediction
⏱ 17 min de leitura·+85 XP
A CPU não executa instruções uma por vez — usa pipeline, execução especulativa e hierarquia de cache para processamento altamente paralelizado. Entender esse modelo mental transforma intuições vagas de performance em previsões verificáveis.
O ciclo Fetch-Decode-Execute e o pipeline
Cada instrução passa por estágios: Fetch (buscar da memória), Decode (interpretar), Execute (calcular), Memory (acessar memória), Writeback (salvar resultado). CPUs modernas têm pipelines de 14-24 estágios — múltiplas instruções em estágios diferentes simultaneamente.
# Impacto do pipeline na prática — medindo com timeit
import timeit
# Acesso sequencial vs aleatório (cache-friendly vs cache-unfriendly)
import random
SIZE = 10_000_000
dados_sequenciais = list(range(SIZE))
dados_aleatorios = dados_sequenciais.copy()
random.shuffle(dados_aleatorios)
# Criar índices para acesso
indices_seq = list(range(SIZE))
indices_rand = list(range(SIZE))
random.shuffle(indices_rand)
def acesso_sequencial():
total = 0
for i in indices_seq:
total += dados_sequenciais[i]
return total
def acesso_aleatorio():
total = 0
for i in indices_rand:
total += dados_aleatorios[i]
return total
# Resultado típico:
# sequencial: ~0.8s
# aleatório: ~1.5s (2x mais lento — cache misses)
# Melhor em numpy (acesso vetorizado + SIMD):
import numpy as np
arr = np.arange(SIZE, dtype=np.int64)
# np.sum(arr) → ~5ms (vs 800ms Python puro — 160x mais rápido)Hierarquia de cache: latências reais
| Nível | Tamanho típico | Latência | Compartilhado? |
|---|---|---|---|
| Registradores | ~1 KB total | <1 ns | Por thread (não compartilhado) |
| L1 (instrução + dado) | 32-64 KB por core | ~0.5 ns | Por core (privado) |
| L2 | 256 KB - 1 MB por core | ~5 ns | Por core (privado) |
| L3 (LLC) | 6-64 MB total | ~30 ns | Todos os cores (compartilhado) |
| RAM (DRAM) | GBs | ~100 ns | Todos os cores |
| SSD NVMe | TBs | ~100 µs | Todos os processos |
| HDD | TBs | ~10 ms | Todos os processos |
# Demonstrando cache miss em Python # (números são relativos — Python tem overhead grande de intérprete) import timeit # Lista de listas — péssimo para cache (pointer chasing) matriz_ruim = [[i * 1000 + j for j in range(1000)] for i in range(1000)] # Numpy array — contíguo na memória (cache-friendly) import numpy as np matriz_boa = np.zeros((1000, 1000), dtype=np.int64) # Row-major (C order, NumPy default) — acesso por linhas é rápido t1 = timeit.timeit(lambda: np.sum(matriz_boa[0, :]), number=10000) # linha t2 = timeit.timeit(lambda: np.sum(matriz_boa[:, 0]), number=10000) # coluna # t1 << t2 (acesso por coluna pula 1000 elementos na memória — mais cache misses) # Linha de cache tem 64 bytes = 8 × int64 # Carregar um elemento de numpy float64 carrega os 7 vizinhos na mesma linha de cache
Branch prediction: código amigável ao preditor
import timeit
import random
# Branch prediction favorece padrões previsíveis
dados = list(range(10_000))
def com_branch_previsivel():
# loop sem if imprevisível — quase sempre mesmo caminho
total = 0
for x in dados:
total += x # sem branch aqui
return total
dados_aleatorios = [random.randint(0, 100) for _ in range(10_000)]
def com_branch_imprevisivel():
# condição depende de dados aleatórios — preditor erra ~50%
total = 0
for x in dados_aleatorios:
if x > 50: # ← branch imprevisível
total += x
return total
def sem_branch():
# elimina o branch com operação vetorizada
return sum(x for x in dados_aleatorios if x > 50)
# Numpy elimina branches com operações vetorizadas:
import numpy as np
arr = np.array(dados_aleatorios)
# np.sum(arr[arr > 50]) — sem Python branch, operação SIMD na CPU
# Estratégias para código cache/branch-friendly:
# 1. Prefira loops sobre arrays contíguos (numpy, bytearray) vs listas de objetos
# 2. Estruture condições para o caso comum primeiro (profiling ajuda)
# 3. Use numpy/pandas para operações vetorizadas que evitam Python branching
# 4. Evite linked lists para dados que serão iterados — use arrays
# 5. Prefira struct-of-arrays vs array-of-structs para SIMD efficiency✅
Modelo mental: cache miss é o inimigo número 1 de performance em código com dados grandes. Prefira acesso sequencial a aleatório. Use numpy/pandas que alocam arrays contíguos. Evite estruturas com pointer-chasing (linked list, dict de dicts) em hot paths. O profiler é seu amigo — sempre meça antes de otimizar.
💡
Próximo: Memória: stack, heap e virtual memory — como o SO gerencia memória para todos os processos simultaneamente.
🧩
Quiz rápido
3 perguntas · Acerte tudo e ganhe o badge 🎯 Gabarito
Continue lendo