Pipelining sobrepõe fases diferentes de várias instruções. Sua finalidade principal é elevar a vazão (throughput), não necessariamente reduzir a latência de uma instrução individual. A arquitetura do conjunto de instruções (ISA) especifica resultados arquiteturais observáveis; pipeline, forwarding, stalls, previsão de desvios e especulação são técnicas de implementação que devem conservar esses resultados. Um cronograma rápido que lê registrador incorreto continua errado, ainda que apresente ótimo CPI [1].
Este capítulo prossegue a representação binária, ISA e mapeamento de cache. Vamos deduzir um modelo didático deliberadamente restrito, de cinco estágios e emissão única, confrontar seus cronogramas com restrições temporais verificadas independentemente e analisar um preditor de desvios de dois bits. Não é uma simulação de CPU moderna Intel/AMD, RISC-V real, cache coerente, espera por memória, exceções precisas ou execução fora de ordem. Esses aspectos exigem contratos e experimentos adicionais [1][2].
Cinco estágios: latência não é vazão#
No pipeline didático, IF busca a instrução; ID a decodifica e lê registradores; EX executa a operação ou calcula endereços; MEM representa o acesso a dados; WB grava o resultado arquitetural. Assumiremos exatamente um ciclo por estágio, na ordem IF→ID→EX→MEM→WB. Uma instrução emitida no ciclo s ocupa essas fases nos ciclos s, s+1, s+2, s+3, s+4.
| Emissão | Ciclo 0 | Ciclo 1 | Ciclo 2 | Ciclo 3 | Ciclo 4 | Ciclo 5 | Ciclo 6 |
|---|---|---|---|---|---|---|---|
| Instrução A | IF | ID | EX | MEM | WB | ||
| Instrução B | IF | ID | EX | MEM | WB | ||
| Instrução C | IF | ID | EX | MEM | WB |
Cada instrução isolada demora cinco ciclos modelados. Sem hazards e com emissão de uma instrução por ciclo, n instruções terminam em n+4 ciclos. Logo CPI=(n+4)/n, aproximando-se de uma instrução por ciclo no regime estável, embora cada instrução atravesse cinco estágios [1]. O termo +4 contabiliza preenchimento e esvaziamento deste modelo, não é penalidade fixa de hardware.
Reduzir a quantidade de lógica combinacional em cada estágio pode encurtar o período do clock, mas registradores entre estágios e desequilíbrio de trabalho limitam o ganho. Não é possível comparar CPUs com clocks diferentes usando apenas CPI. Emissão superscalar, etapas com durações diferentes e execução fora de ordem mudam o modelo [2][3].
Hazards: conflitos estruturais, de dados e de controle#
Um hazard estrutural aparece quando duas operações disputam um recurso não duplicado no mesmo ciclo, por exemplo uma porta única usada simultaneamente para buscar instrução e ler dados. Nosso modelo presume recursos separados; portanto, não modela hazard estrutural. Isso não significa que portas reais sejam ilimitadas.
Um hazard de dados surge quando uma instrução precisa de valor ainda não produzido por instrução anterior. O principal caso do pipeline em ordem é RAW (read after write). Já WAR e WAW são conflitos de ordenação especialmente relevantes quando instruções executam ou terminam fora da ordem. Como o exemplo confirma resultados na ordem do programa, não os agenda separadamente. Um hazard de controle resulta de desvios ou exceções que alteram a próxima instrução. Trabalho especulativo do caminho incorreto deve ter seus efeitos arquiteturais descartados [1].
Um stall segura fases dependentes; forwarding (ou bypassing) envia um resultado diretamente de determinado estágio sem esperar sua gravação no banco de registradores. As técnicas preservam o resultado exigido, mas mudam cronograma e complexidade de circuito. Dependência load-use ainda pode exigir stall: o dado chega ao fim de MEM, tarde demais para a instrução seguinte consumi-lo no início de EX [1].
Defina o instante do acesso antes de contar bubbles#
Assumiremos que o consumidor lê o banco de registradores no início de ID e consome o valor encaminhado no início de EX. Resultado de ALU surge ao final de EX; dado carregado surge ao final de MEM; gravação habitual do registrador ocorre ao final de WB. Um valor gerado num ciclo somente poderá ser lido no começo de ciclo posterior.
Se p é o ciclo de emissão do produtor e c o do consumidor:
| Produtor e modo | Emissão mínima do consumidor | Consequência |
|---|---|---|
| Sem forwarding, ALU ou carga | c >= p+4 |
3 posições vazias |
| ALU com forwarding | c >= p+1 |
Nenhuma bolha |
| Carga com forwarding | c >= p+2 |
1 posição vazia |
Deduza a primeira linha: produtor grava ao final de WB, em p+4. Consumidor começa ID em c+1. Para enxergar o novo valor, c+1 > p+4; logo c >= p+4. Para ALU encaminhada, consumidor EX começa em c+2 > p+2; para carga, precisa de c+2 > p+3. Alguns materiais permitem gravar em WB e ler em ID no mesmo ciclo, em instantes distintos; então o número da primeira linha muda. Não misture convenções [1].
Agendador executável de instruções com dependências RAW#
Cada instrução artificial tem a forma (tipo, destino, fontes): ALU produz resultado depois de EX e LOAD depois de MEM. Esses itens são metadados de agendamento, não opcodes reais nem uma implementação da codificação RISC-V. Assumimos uma única saída por instrução, com fontes disponíveis no início ou produzidas por instruções anteriores. Não modelamos stores, aliasing, especulação, exceções nem cache.
O agendador guarda o último produtor anterior de cada registrador de entrada. Como emissão e conclusão seguem a ordem do programa, esse é o produtor relevante. Para cada instrução, escolhe o primeiro ciclo de emissão que atende todas as restrições. A função valida o programa antes do cálculo, não modifica estado externo e retorna uma tupla imutável.
def issue_cycles(program, forwarding=True):
if type(forwarding) is not bool:
raise ValueError("forwarding must be boolean")
validated = []
for operation in program:
if (not isinstance(operation, tuple) or len(operation) != 3):
raise ValueError("expected (kind, destination, sources)")
kind, destination, sources = operation
if (kind not in ("ALU", "LOAD")
or not isinstance(destination, str) or not destination
or not isinstance(sources, tuple)
or any(not isinstance(src, str) or not src for src in sources)):
raise ValueError("invalid teaching instruction")
validated.append((kind, destination, sources))
writers = {}
issued = []
for kind, destination, sources in validated:
earliest = (issued[-1] + 1) if issued else 0
for source in sources:
if source in writers:
producer_issue, producer_kind = writers[source]
separation = (4 if not forwarding
else 2 if producer_kind == "LOAD" else 1)
earliest = max(earliest, producer_issue + separation)
issued.append(earliest)
writers[destination] = (earliest, kind)
return tuple(issued)
dependent = [
("LOAD", "r1", ()),
("ALU", "r2", ("r1",)),
("ALU", "r3", ("r2",)),
]
assert issue_cycles(dependent, forwarding=True) == (0, 2, 3)
assert issue_cycles(dependent, forwarding=False) == (0, 4, 8)
assert issue_cycles([("ALU", "a", ()), ("ALU", "b", ())]) == (0, 1)
def modeled_cycles(issue_times):
return 0 if not issue_times else issue_times[-1] + 5
assert modeled_cycles(issue_cycles(dependent, True)) == 8
assert modeled_cycles(issue_cycles(dependent, False)) == 13
No exemplo de três instruções, forwarding produz (0,2,3): a instrução que consome o LOAD aguarda uma posição, mas a seguinte consome ALU sem stall adicional. Sem forwarding, temos (0,4,8), com seis posições de emissão vazias. Os tempos totais modelados são respectivamente 8 e 13 ciclos, incluindo quatro estágios após a última emissão. Esses valores são resultados do modelo, não medidas de CPU física.
O invariante de correção é: depois de agendar k instruções, os ciclos de emissão são estritamente crescentes e o último produtor anterior de cada operando concluiu a fase que disponibiliza seu resultado antes da leitura pelo consumidor. Para a instrução seguinte, escolher o menor ciclo que satisfaz todas as desigualdades preserva o invariante. O algoritmo termina para programa finito e validado. Tempo esperado é proporcional ao número de instruções e operandos, presumindo nomes de registradores limitados e busca média constante em dicionários; memória proporcional ao programa e aos destinos distintos.
Previsão de desvios: contador saturante de dois bits#
Um desvio condicional cria dependência de controle: o endereço da próxima busca pode depender de resultado ainda não conhecido. A previsão permite prosseguir antes do resultado; quando está incorreta, instruções especulativas do caminho errado precisam ser descartadas, com penalidade específica da implementação. O processador não pode confirmar resultados arquiteturais incorretos por causa da previsão [1][3].
Um contador saturante de dois bits possui quatro estados: 0=fortemente não tomado, 1=fracamente não tomado, 2=fracamente tomado e 3=fortemente tomado. Prevê tomado nos estados 2/3 e não tomado nos estados 0/1. Cada resultado tomado incrementa até o limite 3; um resultado não tomado decrementa até 0. Essa histerese evita inverter forte preferência após uma única observação contrária. É um único contador didático, não um preditor moderno de histórico, BTB ou algoritmo de uma CPU real [1].
class TwoBitPredictor:
def __init__(self, state=1):
if type(state) is not int or state not in (0, 1, 2, 3):
raise ValueError("state must be a two-bit integer")
self.state = state
def predict(self):
return self.state >= 2
def observe(self, taken):
if type(taken) is not bool:
raise ValueError("branch outcome must be boolean")
predicted = self.predict()
self.state = min(3, self.state + 1) if taken else max(0, self.state - 1)
return predicted
counter = TwoBitPredictor(1)
outcomes = (True, True, False, False, False)
predictions = tuple(counter.observe(outcome) for outcome in outcomes)
assert predictions == (False, True, True, True, False)
assert sum(a != b for a, b in zip(predictions, outcomes)) == 3
assert counter.state == 0
A sequência produz três previsões erradas, incluindo dois erros sucessivos na transição descendente de estados. O invariante é 0 <= state <= 3; cada observação mantém valor inteiro válido. Entradas precisam ser bool explícito; aceitar números arbitrários mascararia um contrato incorreto. O preditor retorna a previsão calculada antes de atualizar o estado: verificar apenas o estado final não detectaria um preditor que consulta a resposta e só então afirma que a previu.
CPI, custo dos desvios e limites da fórmula#
Para um modelo simples, com penalidades independentes e não sobrepostas, podemos escrever
ciclos ≈ instruções + 4 + slots_de_stall + erros_de_previsão × penalidade.
Essa é uma aproximação analítica aditiva, não um simulador de especulação. Pressupõe emissão única, custo fixo de recuperação e ausência de sobreposição entre desvios errados, stalls, faltas de cache e trabalho independente. Numa sequência com 3 erros e penalidade suposta de 2 ciclos, acrescentamos 6 ciclos modelados: não se demonstra que uma CPU real perde dois ciclos por erro.
Para medir equipamento real são necessários contadores de desempenho e microbenchmarks, controlando processador, compilador, aquecimento, entrada e mistura de instruções. Os manuais Intel discutem otimização e previsão de desvios, mas não justificam transportar o mesmo custo para todas as microarquiteturas [3]. Faltas de cache e previsões erradas são fenômenos distintos; o capítulo de cache anterior apresenta outra fonte de atraso.
Paralelismo entre instruções, ordenação e estado preciso#
Instruções independentes podem se sobrepor; uma cadeia de dependências define um caminho crítico. Compiladores podem reorganizar cálculos independentes para esconder bolhas de carga, desde que preservem o comportamento observável. Hardware também pode encaminhar valores, especular, renomear registradores e executar fora de ordem. Porém renomeação não remove uma dependência RAW verdadeira; o consumidor precisa do resultado lógico produzido antes [2].
Exceções tornam a questão mais delicada: o estado que o software observa deve corresponder a ponto arquiteturalmente bem definido. CPUs modernas podem executar especulativamente e confirmar efeitos numa ordem consistente com o contrato da ISA. Nosso agendador não contém reorder buffer, exceções, endereços de desvios, dependência de memória ou estado especulativo; seria incorreto descrevê-lo como traço fiel de CPU [1][2].
Exercícios e verificação#
- Desenhe os cinco estágios de 3 instruções independentes e explique por que a latência individual é cinco ciclos, apesar de a vazão ideal ser uma por ciclo.
- Refaça as desigualdades RAW de ALU e carga. Se WB puder gravar antes de ID ler no mesmo ciclo, explique como muda a quantidade de bolhas.
- Agende manualmente
LOAD r1; ALU r2←r1; ALU r3←r2com e sem forwarding. Compare os ciclos obtidos com o código. - Insira uma ALU independente entre LOAD e uso. Determine em que situação a instrução independente ocupa a bolha sem modificar a semântica.
- Construa instrução com dois operandos produzidos por escritas anteriores diferentes. Verifique que prevalece a maior restrição temporal.
- Desenhe a tabela completa dos quatro estados do preditor e dois resultados possíveis; confirme que a previsão usa o estado anterior à atualização.
- Distinga afirmações de correção arquitetural, propriedades de microarquitetura e hipóteses analíticas presentes na explicação.
- Imagine uma escrita em memória no caminho errado de um desvio. Mostre por que um preditor de dois bits corretamente implementado não basta para preservar o estado arquitetural.
Próximos: memória virtual e syscalls tratam outra camada de tradução e proteção; concorrência trata interleavings entre threads. Coerência de cache multicore, ordenação real de memória, temporização RISC-V e experimentos com contadores de desempenho exigem capítulos separados [1][2][3].