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.
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.
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ção | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---|---|---|---|---|---|---|---|---|
ADD EAX, EBX | ||||||||
MOV ECX, 5 | ||||||||
INC EDX | ||||||||
SUB ESI, 2 | ||||||||
XOR EDI, EDI |
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
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.
Teste sua intuição
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.