Trilha 07 · Assembly e baixo nível

Pipeline de execução da CPU

A CPU não executa uma instrução de cada vez — ela sobrepõe a execução de múltiplas instruções como uma linha de montagem. Entender o pipeline explica por que a ordem do código importa para performance.

① Intuição

A linha de montagem da CPU

Uma CPU sem pipeline executaria cada instrução em 4 etapas sequenciais: busca → decodifica → executa → escreve. 4 ciclos por instrução. Com pipeline, essas etapas se sobrepõem: enquanto a instrução 1 está sendo executada (EX), a instrução 2 está sendo decodificada (ID) e a instrução 3 está sendo buscada (IF). Com pipeline de 4 estágios cheio, você tem 1 instrução completada por ciclo — throughput 4× maior.

O problema são os hazards: situações onde uma instrução não pode entrar no próximo estágio porque depende do resultado de uma instrução anterior ainda não concluída. A CPU precisa inserir bolhas (NOPs, stalls) ou o compilador reordena instruções para evitá-los.

Superescalar e out-of-order: CPUs modernas (Intel Core, AMD Zen, Apple M-series) são superescalares — executam 4–6 instruções por ciclo em múltiplas unidades de execução em paralelo. Além disso, executam fora de ordem (out-of-order / OOO): se a instrução 3 não depende de 2, ela pode executar antes. O pipeline simplificado desta lição é o modelo mental; a realidade é mais complexa.
② Visualização interativa

Diagrama de pipeline

Alterne entre o pipeline ideal e o cenário com data hazard. Use +1 Ciclo para avançar ou ▶ Animar para reprodução automática. Clique em um estágio na legenda para ver sua descrição.

4 instruções independentes. Cada ciclo, uma instrução avança um estágio — pipeline cheio a partir do ciclo 4.

Instrução12345678
ADD EAX, EBX
MOV ECX, 5
INC EDX
SUB ESI, 2
XOR EDI, EDI
Ciclo 0 / 8
③ Explicação técnica

Os 5 estágios clássicos (MIPS)

// O que acontece em cada estágio do pipeline clássico de 5 estágios
// (MIPS, simplificado — x86 moderno tem 14–20 estágios)

// 1. IF — Instruction Fetch
IR = Mem[PC];     // lê instrução da memória (ou cache L1-I)
PC = PC + 4;      // avança program counter (instrução de 4 bytes no MIPS)

// 2. ID — Instruction Decode / Register Read
op    = IR[31:26]; // campo opcode (6 bits)
rs    = IR[25:21]; // registrador fonte 1
rt    = IR[20:16]; // registrador fonte 2 (ou destino)
A     = Reg[rs];   // leitura do banco de registradores
B     = Reg[rt];

// 3. EX — Execute (ALU)
ALUresult = A + B;    // para ADD
ALUresult = A + imm;  // para ADDI (com extensão de sinal)
ALUresult = A + imm;  // para LW/SW (cálculo de endereço)

// 4. MEM — Memory Access (para LOAD e STORE)
LMD = Mem[ALUresult]; // LOAD: lê dado da memória
Mem[ALUresult] = B;   // STORE: escreve dado

// 5. WB — Write Back
Reg[rd] = ALUresult;  // instrução aritmética: salva no reg destino
Reg[rt] = LMD;        // LOAD: salva dado lido

Tipos de hazards

// Tipos de hazards e como a CPU os resolve

// 1. DATA HAZARD — instrução usa resultado ainda não pronto
ADD R1, R2, R3    // WB de R1 acontece no ciclo 5
SUB R4, R1, R5    // R1 necessário no ciclo 3 (EX) — 2 ciclos antes!
// Solução: FORWARDING — passa o resultado diretamente do EX de ADD para o EX de SUB
// Sem forwarding: stall de 2 ciclos (bolhas NOP)

// 2. LOAD-USE HAZARD — load seguido de uso imediato
LW  R1, 0(R2)     // R1 pronto só após MEM (ciclo 4)
ADD R3, R1, R4    // R1 necessário em EX (ciclo 3) — 1 ciclo cedo!
// Forwarding não ajuda aqui — 1 stall inevitável
// Compilador pode reordenar: coloca instrução independente entre LW e ADD

// 3. CONTROL HAZARD — branch: destino só conhecido após EX/MEM
BEQ R1, R2, target  // pipeline busca PC+4 imediatamente (especulação)
// Se branch tomado: as 1–3 próximas instruções buscadas são descartadas (flush)
// Branch predictor: CPU prevê se o branch vai ser tomado ou não
// Acurácia típica: 95–99% — mal previsto custa 15–20 ciclos em CPUs modernas
Branch prediction em CPUs modernas: o branch predictor usa histórico de execuções anteriores para prever se um branch vai ser tomado. O Intel Skylake tem um preditor com acurácia de ~99% para loops regulares. A vulnerabilidade Spectre (2018) explorou justamente a execução especulativa pós-branch: a CPU executava código "depois" do branch antes de saber se ele seria tomado, e dados privados vazavam via cache side-channels.
④ Projeto para programar

Medindo o impacto do pipeline

Mini projeto: meça o efeito de load-use hazard: escreva dois loops em C — um que usa imediatamente o resultado de um array load (a[i] + 1), e outro que intercala acessos independentes (a[i] + b[i-1]). Compare com perf stat ou clock_gettime em N=100 milhões de iterações.

Projeto principal: implemente um simulador de pipeline de 5 estágios em Python. Represente cada estágio como um slot que contém a instrução atual (ou None para bolha). A cada ciclo: WB←MEM←EX←ID←IF. Detecte data hazards: se ID está lendo um registrador que EX ainda vai escrever, insira uma bolha. Teste com sequências de 10 instruções.

Desafio extra: use perf stat para medir IPC (instruções por ciclo) de um programa real. Compare IPC de: (1) loop simples com dependências em cadeia, (2) loop com acumulação independente (4 acumuladores), (3) memset com SIMD. IPC > 1 indica execução superescalar.

⑤ Exercícios rápidos

Teste sua intuição

Qual é o benefício real de um pipeline de 4 estágios?
O que é um data hazard e como a CPU lida com ele?
Como o branch predictor funciona e qual o custo de um erro de predição?
⑥ Aplicações no mundo real

Onde você encontra isso

Otimização de loops críticos

Código de alta performance (codecs, motores de física, redes neurais) é escrito com pipeline em mente: loops são "unrolled" (desdoblados) para esconder latências, variáveis são intercaladas para evitar load-use hazards, e acumuladores independentes permitem execução superescalar. O GCC com -O3 -march=native faz isso automaticamente.

🔐

Spectre e Meltdown (2018)

Spectre explorou execução especulativa: a CPU executava código "proibido" (fora dos limites de acesso) especulativamente. Os dados privados não eram retornados diretamente, mas deixavam rastros no cache (acessos rápidos vs. lentos revelariam quais dados foram lidos). Correção exigiu mudanças em CPU, kernel e compiladores.

📟

CPUs embarcadas sem pipeline

Microcontroladores simples como PIC (8-bit Microchip) e muitos AVR têm pipeline de 2 estágios apenas (busca + executa). CPUs de tempo-real crítico como as de airbag e controle de voo às vezes são propositalmente sem pipeline para garantir latência determinística — sabendo exatamente quantos ciclos cada instrução leva.

← Anterior: Assembly na prática Próxima: Pilha e chamadas →