Teu progresso
0 / 83 módulos0%
Estágio 01 · 01-01
DesbloqueadoTodo software roda em hardware. Entender como o hardware executa código é a diferença entre escrever "código que funciona" e "código que escala". Sem este módulo, conceitos posteriores (event loop, garbage collection, cache strategies, performance, concurrency) não fazem sentido, são abstrações em cima de um modelo que você não conhece.
Exemplos concretos onde desconhecimento custa caro:
Este módulo te dá o modelo mental do computador que vai sustentar tudo daqui pra frente.
Um computador moderno, simplificado, é:
┌──────────────┐ ┌──────────────────────────────────┐
│ │ │ │
│ CPU │◄──►│ Memória │
│ │ │ (RAM, contém código + dados) │
└──────┬───────┘ └──────────────────────────────────┘
│
│ I/O bus
▼
┌──────────────────┐
│ Disco, rede, etc │
└──────────────────┘
A CPU executa instruções uma por vez (conceitualmente, superscalar/pipelined na prática). Cada instrução faz coisas como:
Registradores são a memória dentro da CPU. Há poucos (~16-32 de propósito geral em x86_64), mas são acessados em 1 ciclo de clock (ordem de 0.3ns numa CPU de 3GHz). Tudo o mais é mais lento.
A "memória" não é uma coisa única. É uma hierarquia com trade-offs entre velocidade, capacidade e custo:
| Nível | Latência típica | Capacidade típica | Onde mora |
|---|---|---|---|
| Registradores | ~0.3 ns | ~1 KB | Dentro da CPU |
| L1 cache | ~1 ns | 32-64 KB por core | Dentro da CPU |
| L2 cache | ~3-10 ns | 256 KB - 1 MB por core | Dentro da CPU |
| L3 cache | ~10-30 ns | 4-64 MB compartilhado | Dentro da CPU |
| RAM (DRAM) | ~50-100 ns | 4-128 GB | Fora da CPU, na placa-mãe |
| SSD NVMe | ~10 µs | 100 GB - 4 TB | I/O bus |
| SSD SATA | ~100 µs | 100 GB - 4 TB | I/O bus |
| HDD | ~5 ms | 500 GB - 20 TB | I/O bus |
| Rede (data center) | ~0.5 ms | ∞ | NIC + switches |
| Rede (continental) | ~50-200 ms | ∞ | Internet |
Memorize as ordens de grandeza:
Isso explica por quê:
A CPU não lê 1 byte por vez da RAM. Lê em cache lines de 64 bytes (em x86_64 moderno). Quando você acessa um endereço de memória, 64 bytes contíguos são copiados pra L1, porque é provável que você acesse os bytes adjacentes em seguida.
Isso explica spatial locality: estruturas contíguas (arrays) são radicalmente mais rápidas que estruturas espalhadas (linked lists), porque um acesso já carrega vizinhos no cache.
E temporal locality: dados acessados recentemente provavelmente serão acessados de novo, então cache mantém eles.
Cache miss types (3 C's):
A CPU também tem prefetcher: hardware que detecta padrões de acesso (ex: você acessa arr[0], arr[1], arr[2]...) e antecipa carregando os próximos. Acesso linear é amigo do prefetcher; acesso aleatório (linked list) destrói ele.
A memória de um processo é dividida em regiões:
Endereços altos
┌─────────────────┐
│ Stack │ ← cresce pra baixo
│ (variáveis │
│ locais, args, │
│ return address)│
├─────────────────┤
│ ↓ │
│ (cresce) │
├─────────────────┤
│ ↑ │
│ (cresce) │
├─────────────────┤
│ Heap │ ← cresce pra cima
│ (alocação │
│ dinâmica: │
│ malloc, new) │
├─────────────────┤
│ BSS / Data │ globais, estáticas
├─────────────────┤
│ Text (código) │ instruções do programa
└─────────────────┘
Endereços baixos
Stack:
RSP em x86_64).stack overflow.Heap:
malloc (C), new (C++), ou implicitamente em linguagens managed (JS objects, classes em Java).free, delete) ou automática via garbage collector.Em JavaScript:
Cada processo "vê" um espaço de endereços de 64 bits (em x86_64), aparentemente próprio. Mas a RAM física é compartilhada e finita. Como?
Virtual memory. O OS + MMU (Memory Management Unit, hardware na CPU) traduzem endereços virtuais (que o processo usa) pra endereços físicos (RAM real).
A tradução acontece em páginas (default 4 KB). O OS mantém uma page table por processo:
Quando o processo acessa um endereço virtual:
Por que isso importa pra você:
CPUs modernas não executam uma instrução de cada vez. Têm um pipeline com 10-20 estágios, cada instrução passa por: fetch → decode → execute → memory → writeback (simplificado).
Várias instruções estão em estágios diferentes simultaneamente, paralelismo de instrução. Isso requer:
if, a CPU adivinha qual ramo será tomado e começa a executá-lo especulativamente. Se acertar: zero custo. Se errar (branch misprediction): pipeline é descartado, custa ~10-20 ciclos.Implicações:
Quando função A chama função B, há um protocolo:
Em x86_64 Linux (System V ABI), os primeiros 6 args inteiros vão em RDI, RSI, RDX, RCX, R8, R9; resto na stack. Return value em RAX.
Você raramente mexe nisso direto, mas é o que sustenta debuggers, profilers e o conceito de stack trace.
Pra passar o Portão Conceitual, você deve conseguir, sem consultar nada:
Implementar e medir cache effects empiricamente.
Em TypeScript (Node), construa um benchmark que mede o impacto de cache locality em performance real.
Crie duas estruturas de dados equivalentes em conteúdo:
Float64Array) com 10 milhões de números aleatórios.{ value: number, next: Node | null }), com os mesmos números, mas alocados em ordem aleatória no heap (pra forçar não-contiguidade, alterne com outras alocações descartáveis pra fragmentar).Faça 4 benchmarks:
Float64Array, iteração linear (for (let i=0; i<n; i++) sum += arr[i]).next.Float64Array em ordem aleatória (acesso a índices aleatórios pré-computados).Float64Array, mas com salto de stride 16 (i.e., acessa só arr[0], arr[16], arr[32], ...), força carregar uma cache line por elemento.Use process.hrtime.bigint() pra medir cada um. Rode cada benchmark 5x, pegue mediana.
Imprima os resultados e a razão entre eles (ex: B é 5.3x mais lento que A).
node e tsc).Pra passar o portão prático: você deve conseguir explicar verbalmente cada resultado com base nos conceitos da Teoria Hard. Se a ordem dos resultados surpreender você, investigue até entender: não passe o portão sem explicação.
Faça o mesmo benchmark em C ou Rust e compare com Node. Discuta no README quanto da diferença vem de:
int[] em Cshared_buffers) na RAM. Quando query precisa de página fora do cache, vira I/O. Latência de query muda de microssegundos pra milissegundos.perf (Linux): profiler de baixo nível. Comandos como perf stat, perf record, perf report te mostram cache misses, branch mispredictions reais.cachegrind (parte do Valgrind): simula cache, conta misses por linha de código.src/heap/ pra ver GC; src/objects/ pra ver layout de objects.Encerramento: ao terminar este módulo, você passa a ler "performance" com olhos diferentes. Toda decisão posterior, escolher Array vs Map, usar Buffer ou string, fazer query com índice ou sequential scan, decidir cache strategy, vai ter como pano de fundo o que você aprendeu aqui.
Threshold de Maestria
Acerte todas as 5 pra marcar o módulo como concluído. Sem pressa, sem timer. Tudo fica salvo no teu navegador.
Q1Por que iterar um array contíguo costuma ser muito mais rápido que percorrer uma linked list com os mesmos elementos?
Q2Você cria 10 milhões de objetos pequenos num programa Node e a memória cresce sem controle. Qual a explicação mais alinhada com o que o módulo discute?
Q3Comparando latências típicas, qual ordem de grandeza está correta?
Q4O que acontece quando a CPU sofre um branch misprediction num pipeline moderno?
Q5Por que um page fault é considerado caríssimo (~100x mais lento que cache miss)?
Destrava
01-01 é prereq dos seguintes módulos: