flowchart LR
subgraph Sequencial["Sem sobreposicao (6 h)"]
direction LR
L1["Lavar C1"] --> S1["Secar C1"] --> D1["Dobrar C1"] --> L2["Lavar C2"]
end
subgraph Pipeline["Com sobreposicao (3 h)"]
direction TB
T1["t=30: Lavar C1"]
T2["t=60: Secar C1<br/>Lavar C2"]
T3["t=90: Dobrar C1<br/>Secar C2<br/>Lavar C3"]
T1 --> T2 --> T3
end
Sequencial -. mesma latencia<br/>menor throughput .-> Pipeline
Módulo 06: Pipeline — Conceitos e Implementação Básica — Resumo
Esta é a versão de revisão do Módulo 06, para você reler na véspera da prova ou antes de mexer no benchmark do Projeto Integrador. As demonstrações completas, os exemplos extras e as deduções passo a passo estão em 06_material.qmd e, com ainda mais fôlego, em 06_livro.qmd. Aqui eu vou direto ao que precisa ficar na sua cabeça.
Um processador a 1 GHz tem ciclo de clock de um nanosegundo, e nesse tempo um sinal elétrico mal atravessa o próprio chip. Ainda assim a máquina conclui bilhões de instruções por segundo. Como? A resposta curta é que ela não executa uma instrução inteira por vez. Ela mantém várias andando ao mesmo tempo, cada uma numa etapa diferente — e esse truque, o pipeline, já rodava silenciosamente no PIC18F4550 que você programou no Módulo 05. O objetivo deste módulo é sair da intuição e chegar ao número: quanto desempenho o pipeline entrega, por que o caso real fica abaixo do ideal, e como medir isso no Timer0 do seu kit.
A ideia do pipeline: sobrepor trabalho
Lembra da lavanderia? Lavar, secar e dobrar quatro cargas, trinta minutos cada etapa. Do jeito ingênuo, uma carga inteira antes da próxima: noventa minutos por carga, e a lavadora parada enquanto a primeira seca. O jeito esperto é botar a segunda carga para lavar assim que a primeira sai para a secadora. As três estações trabalham ao mesmo tempo, cada uma numa carga. A primeira ainda leva noventa minutos, mas a partir daí sai uma carga pronta a cada trinta. Guarde esses dois números: noventa (uma carga isolada) e trinta (o intervalo entre cargas prontas). Eles voltam com nome formal.
No processador é a mesma coisa, só que mais rápido. As cargas são as instruções; as estações são os estágios, blocos de hardware que fazem uma fração do processamento. A cada borda de clock, todas as instruções avançam um estágio de uma vez. Aqui mora a confusão mais comum, e eu mesmo demorei a sacar: num pipeline de cinco estágios, dizer que cinco instruções estão “em execução simultânea” não significa cinco instruções por ciclo. Cada instrução ainda leva cinco ciclos do começo ao fim. O que acontece ao mesmo tempo é o trabalho de estágios diferentes sobre instruções diferentes. Nenhuma anda mais rápido; o que aumenta é a frequência com que elas saem prontas pela ponta.
Tem uma condição escondida: para dois estágios trabalharem juntos, eles precisam de hardware físico distinto. O estágio de busca usa a porta da memória de programa; o de acesso a dados usa a porta da memória de dados. Se houvesse uma única memória com uma única porta, os dois colidiriam — e essa colisão é o hazard estrutural do Módulo 07. Não é coincidência que arquiteturas com pipeline gostem da separação física entre instruções e dados, a organização Harvard que você reconhece de cara no PIC18F4550.
O pipeline clássico de cinco estágios
Por que cinco? Porque é o menor número que separa bem as tarefas de uma arquitetura load-store: buscar usa a memória de programa; decodificar e ler operandos usa o banco de registradores; calcular usa a ULA; acessar dados usa a memória de dados; escrever de volta usa o banco outra vez. Cinco tarefas, cada uma com seu recurso predominante. O número não é sagrado — o PIC usa dois, os desktops dos anos 2000 passaram de vinte — mas virou o modelo didático canônico, herdado do MIPS dos anos 1980.
flowchart LR
IF["IF<br/>Busca<br/>memoria de programa"] --> R1[["Reg IF/ID"]]
R1 --> ID["ID<br/>Decodifica<br/>le registradores"]
ID --> R2[["Reg ID/EX"]]
R2 --> EX["EX<br/>ULA<br/>calcula e decide desvio"]
EX --> R3[["Reg EX/MEM"]]
R3 --> MEM["MEM<br/>Acesso<br/>memoria de dados"]
MEM --> R4[["Reg MEM/WB"]]
R4 --> WB["WB<br/>Escreve<br/>banco de registradores"]
O IF (busca) lê a instrução apontada pelo PC e já o incrementa — no PIC, de dois, porque cada instrução ocupa duas posições de byte. Repara na aposta otimista: o IF precisa saber qual instrução buscar antes de a anterior ser decodificada, então aposta que é a do endereço seguinte. Vale quase sempre, porque programa é quase sempre sequencial, mas falha num desvio tomado. O ID (decodificação) interpreta o opcode e lê os operandos no banco. O EX (execução) é onde a ULA do Módulo 03 calcula e onde a condição do desvio é resolvida — num pipeline de cinco estágios, dois ciclos depois de o desvio entrar, o que gera penalidade de dois ciclos. O MEM acessa a memória de dados (só cargas e armazenamentos fazem algo útil aqui; as demais atravessam vazias, de propósito, para todas terem o mesmo número de estágios). O WB escreve o resultado de volta no banco, e aí a instrução sai do pipeline.
Acompanha com lápis: cinco instruções, I1 a I5. No ciclo 1, só I1 em IF. No ciclo 3, I1 em EX, I2 em ID, I3 em IF. No ciclo 5 o pipeline está cheio, com uma instrução em cada estágio, e a partir daí sai uma instrução pronta por ciclo. Os quatro ciclos iniciais de enchimento, em que nada conclui, são o transiente que pesa só em programas curtos.
flowchart TB
subgraph C1["Ciclo 1"]
a1["I1: IF"]
end
subgraph C2["Ciclo 2"]
a2["I1: ID"]
b2["I2: IF"]
end
subgraph C3["Ciclo 3"]
a3["I1: EX"]
b3["I2: ID"]
c3["I3: IF"]
end
subgraph C5["Ciclo 5 (cheio)"]
a5["I1: WB"]
b5["I2: MEM"]
c5["I3: EX"]
d5["I4: ID"]
e5["I5: IF"]
end
C1 --> C2 --> C3 --> C5
a5 -. uma conclusao por ciclo .-> Saida["I1 concluida"]
Registradores de pipeline e temporização
A lavanderia esconde uma dificuldade física: roupa fica onde foi posta, sinal elétrico não. Os fios que saem do IF são os que alimentam o ID; se nada os separasse, ao buscar a próxima instrução o IF sobrescreveria o que o ID ainda usa. A solução é pôr, entre cada par de estágios, um banco de registradores que captura os sinais na borda do clock e os segura estáveis durante o ciclo seguinte. Num pipeline de cinco estágios são quatro deles, e cada um carrega dados e também os sinais de controle ainda não usados — eles viajam pelo pipeline como passageiros que descem em pontos diferentes.
Esses registradores impõem a regra que governa a velocidade do chip. A cada ciclo, o sinal precisa atravessar a lógica de um estágio e chegar estável à entrada do registrador antes da próxima borda. Sendo t_i o atraso da lógica do estágio i e t_{reg} o atraso dos registradores, o período mínimo é
T_{clk} \geq \max_{1 \le i \le k}\,(t_i) + t_{reg}.
O estágio que atinge esse máximo é o gargalo, e é só ele que manda na frequência máxima. Não adianta quatro estágios velozes e um lento: o clock se acomoda ao lento e os outros desperdiçam o tempo que sobra, em todo ciclo. Por isso projetistas suam para balancear os estágios.
E aí vem a tentação: se o gargalo é o estágio mais lento, por que não picar tudo em pedaços menores, aumentando k e encurtando o ciclo? Dois freios. O próprio t_{reg}, que se soma a cada registrador novo e a certa altura passa a dominar o ciclo. E, mais sério, pipelines profundos sofrem penalidades maiores quando a aposta da busca falha — assunto do Módulo 07. Profundidade é parâmetro com retornos decrescentes, não botão que se gira à vontade.
Latência e throughput: a distinção que organiza tudo
Aqui está o conceito que, se sobrar um só na sua memória, quero que seja esse. Diante de um processador dá para fazer duas perguntas que parecem iguais e não são. Quanto tempo leva uma instrução do início ao fim? Isso é latência. Quantas instruções saem por segundo em regime permanente? Isso é throughput. Num pipeline de k estágios com período T_{clk}, a latência sem stalls é L = k \cdot T_{clk} e o throughput com o pipeline cheio é \Theta = 1 / T_{clk}, uma instrução por ciclo. A latência cresce com o número de estágios; o throughput não depende dele, só do período. É isso que resolve o paradoxo da abertura: o pipeline não acelera nenhuma instrução — dividir em mais estágios até aumenta um pouco a latência —, ele melhora o throughput ao encurtar o ciclo.
Pense no seu projeto: o sistema lê um sensor e atualiza o display LCD Sunstar 2004A. Se o usuário aperta um botão de emergência no pino RB0 e dispara uma interrupção, o que importa para a reação rápida — o throughput da rotina de fundo ou a latência do tratamento? Trocar um pelo outro leva a um sistema que processa muito mas demora a reagir.
Quando um fabricante diz que o processador “executa k vezes mais rápido” por causa de um pipeline de k estágios, ele fala de throughput em condições ideais, nunca da latência de uma instrução isolada. Em carga real, com desvios e dependências, o ganho efetivo é sempre menor.
Formalizando o desempenho: CPI, speedup e o ganho ideal
Para calcular ganho com rigor, parto da equação que liga o tempo total às grandezas mensuráveis:
T_{exec} = N_{instr} \cdot \text{CPI} \cdot T_{clk}.
São três alavancas para reduzir o tempo: menos instruções (compilador e ISA), menor período de clock (tecnologia e balanceamento) e menor CPI (organização interna). É no CPI que o pipeline mete a mão.
flowchart LR
T["Tempo de execucao"] --> N["N de instrucoes<br/>compilador e ISA"]
T --> CPI["CPI<br/>organizacao e pipeline"]
T --> CLK["Periodo de clock<br/>tecnologia e gargalo"]
CPI --> P["Penalidades (p)<br/>desvios tomados<br/>instrucoes de 2 palavras"]
Sem pipeline, cada instrução percorre k etapas inteiras antes da próxima começar, e o CPI é k. Com pipeline ideal, uma instrução conclui por ciclo e o CPI tende a 1 — é essa queda de k para 1 que dá o ganho. Mas desconfie de “ideal”: o CPI igual a 1 supõe que o pipeline nunca esvazia, que nenhuma instrução depende de resultado de outra ainda em voo e que nenhum desvio quebra o fluxo. Nada disso vale plenamente em programa real, então o CPI efetivo é sempre maior que 1. Comparando uma máquina sem pipeline com uma pipelined de k estágios, para N instruções o speedup teórico é
S = \frac{N \cdot k}{N + k - 1}, \qquad \lim_{N \to \infty} S = k.
O ganho de até k vezes só aparece em programas longos. Com k=5, cinco instruções dão S \approx 2{,}78; cem instruções, S \approx 4{,}81; dez mil, praticamente 5. Nos laços do seu firmware, que rodam milhões de vezes, o enchimento some na contabilidade. O ganho real fica abaixo de k por desbalanceamento dos estágios, pelo sobrecusto t_{reg} e, sobretudo, pelos hazards. Modelando a penalidade média por p ciclos por instrução,
\text{CPI}_{ef} = 1 + p, \qquad S_{ef} = \frac{k}{1 + p}.
Essa última é a fórmula que você vai usar no projeto: medindo o speedup efetivo e conhecendo k, você inverte e estima p. Mas cuidado com um erro fino — reduzir o CPI nem sempre reduz o tempo, porque o tempo depende também de N_{instr}. Ao inlinar uma sub-rotina você baixa o CPI mas pode aumentar o número de instruções. Olhe sempre o produto inteiro N_{instr} \cdot \text{CPI}, nunca um fator isolado.
O pipeline do PIC18F4550 na prática
Hora de aterrissar no chip que está na sua mão. O PIC18F4550 não tem cinco estágios; tem dois, busca e execução. Essa simplicidade é boa para você, porque deixa observar os fenômenos do pipeline sem o emaranhado de cinco. A 48 MHz, o ciclo de máquina é
T_{cy} = \frac{4}{48\ \text{MHz}} = 83{,}3\ \text{ns},
um throughput de pico de doze MIPS para instruções de um ciclo. Esse é o teto que seu benchmark vai tentar alcançar. A aposta da busca falha sempre que o fluxo deixa de ser sequencial: num desvio tomado, a instrução já pré-buscada é descartada e gasta-se um ciclo extra buscando a do destino — a bolha de pipeline. E repara na assimetria: um desvio condicional cuja condição é falsa não custa nada, porque a instrução pré-buscada era mesmo a certa. Essa é a base daquela otimização do Módulo 05 — organizar o código para que o caso frequente caia no ramo não tomado.
flowchart TB
subgraph Continuo["Fluxo sequencial: 1 instrucao por ciclo"]
p1["Busca I2<br/>Executa I1"] --> p2["Busca I3<br/>Executa I2"] --> p3["Busca I4<br/>Executa I3"]
end
subgraph Desvio["Desvio tomado em I3: 1 ciclo de bolha"]
q1["Busca I4 (errada)<br/>Executa I3 = desvio"] --> q2["Busca destino<br/>BOLHA: sem execucao util"] --> q3["Executa destino"]
end
Continuo -. desvio tomado quebra o prefetch .-> Desvio
A maioria das instruções gasta um ciclo; desvios tomados e instruções de duas palavras (saltos absolutos, chamadas) gastam dois. Sendo f_d a fração dessas, o CPI do PIC é \text{CPI}_{ef} = 1 + f_d. Código puramente sequencial dá f_d = 0 e CPI igual a 1; quanto mais desvios tomados, mais o CPI degrada.
A primeira tarefa do projeto pede um benchmark do throughput efetivo. Use o Timer0 como cronômetro: sem prescaler, ele incrementa uma vez por ciclo de máquina, então a diferença entre a leitura final e a inicial é o número exato de ciclos. Três cuidados separam medição confiável de enganosa: leia TMR0L antes de TMR0H, porque o hardware só trava o byte alto quando você lê o baixo; desconte o sobrecusto cronometrando um trecho vazio; e repita o trecho um número grande e conhecido de vezes, dividindo a contagem. O código abaixo lê o Timer0 e devolve a contagem de ciclos.
De posse da contagem C de um trecho com N_{instr} instruções, o throughput efetivo é \Theta = N_{instr} / (C \cdot T_{cy}) e o CPI efetivo é C / N_{instr}. Confrontar esse CPI medido com o previsto pela fração de desvios é o coração do exercício: quando batem, você entendeu o modelo; quando divergem, investigar a divergência ensina mais que a coincidência. A segunda tarefa pede a visualização do fluxo pelo pipeline, e eu insisto que você desenhe os primeiros diagramas à mão — instruções na vertical, ciclos na horizontal, cada instrução deslocada um ciclo à direita, começando pelo modelo de dois estágios do PIC. Quem só usa a ferramenta nunca cria a intuição de prever em que ciclo cada instrução estará. A terceira tarefa pede identificar as instruções que causam stalls ou bolhas: fluxo sequencial e desvios não tomados custam um ciclo sem bolha; desvios tomados e instruções de duas palavras custam dois, e o segundo é a bolha.
Síntese: da linha de montagem ao Timer0
Voltando ao paradoxo: a máquina conclui várias instruções no tempo de um sinal porque não as executa uma por vez — mantém várias em estágios diferentes, aumentando o throughput sem reduzir a latência de cada uma. Tudo o mais foi elaboração disso: a equação T_{exec} = N_{instr} \cdot \text{CPI} \cdot T_{clk}, o speedup ideal que tende a k, e o efetivo S_{ef} = k/(1+p) que sempre fica abaixo por causa das penalidades. As três tarefas do projeto saem daqui: o benchmark cronometrado por Timer0 com o modelo \text{CPI}_{ef} = 1 + f_d, a visualização do fluxo à mão, e a análise de stalls e bolhas. E fica a promessa aberta: ao derivar o ganho ideal, supus que o pipeline nunca para, que nenhuma instrução depende de outra em voo, que nenhum desvio quebra o fluxo. Tudo falso em programa real, e é o desmoronar dessas suposições que produz os hazards do Módulo 07.