Diego.

mai. de 2025 · 13 min de leitura

Analisando o Pipeline de Execução da CPU: Como as Instruções São Processadas e Otimizadas Através…

Nesse artigo irei abordar todos os aspectos que envolvem a execução de instruções usando a técnica de pipeline, amplamente usada por processadores atuais para aumentar a performance durante a execução de instruções.

Introdução

A CPU processa os dados em várias etapas, entre elas a busca, decodificação, execução e escrita. Pense que cada instrução passa por todas essas etapas a cada ciclo de clock. No entanto, a abordagem de execução sequencial não é nada eficiente, pois a instrução ao passar por cada etapa, deixa outras etapas ociosas.

Etapas de Processamento

Por exemplo, na execução sequencial, apenas uma etapa (representação em verde) é usada por vez pela instrução durante o ciclo de clock, enquanto as outras etapas (representação em vermelho) não são usadas conforme na imagem abaixo.

Execução Sequencial

É nesse momento que entra a execução por pipeline. Ela é uma abordagem de execução extremamente eficiente e necessária na execução de processadores modernos.

Fundamentos do Pipeline

Em processadores modernos, o pipeline é tipicamente dividido em cinco etapas a depender do processador:

  1. Busca de Instrução (Instruction Fetch — IF): busca a próxima instrução na memória de instruções (IS);
  2. Decodificação (Instruction Decode — ID): decodifica a instrução e prepara os operandos;
  3. Execução (Execution — EX): executa as operações aritméticas/lógicas e calcula endereços de desvio;
  4. Acesso à Memória (Memory Access — MEM): acessa os dados na memória (leitura/escrita);
  5. Escrita de Resultado (Write Back — WB): escreve o resultado no banco de registradores;

Ao contrário da execução sequencial, as operações (instruções) no pipeline são executadas simultaneamente durante o ciclo de clock, usando todas as etapas para a execução de diferentes tarefas, permitindo assim, um aumento significativo na eficiência do processador.

Utilização do Pipeline

Então, em vez do processador ter que buscar uma instrução, decodificá-la, executá-la e escrever de volta em um único ciclo, você utiliza cada estágio no ciclo de um clock para uma instrução diferente.

Isso significa que a cada novo ciclo, você terá uma instrução que está sendo buscada, uma que está sendo decodificada, uma que está sendo executada e uma cujo resultado está sendo escrito de volta.

A Analogia da Lavanderia: Compreendendo o Pipeline

Para compreender o funcionamento do pipeline, uma analogia frequentemente utilizada é a de uma lavanderia. Vamos considerar o processo de lavagem de roupas dividido em 4 etapas:

  1. Lavar a roupa (30 min)
  2. Secar a roupa (30min)
  3. Dobrar a roupa (30min)
  4. Guardar a roupa (30min)

Execução Sequencial (Sem Pipeline)

Em uma abordagem sem a utilização do pipeline, cada conjunto de roupas precisa passar por todas as etapas antes que o próximo conjunto possa iniciar o processo. Para processar 4 conjuntos de roupas, o tempo total seria de 08 horas (480min):

  • Conjunto 1: Lavar (0-30min) → Secar (30-60min) → Dobrar (60-90min) → Guardar (90-120min)
  • Conjunto 2: Lavar (120-150min) → Secar (150-180min) → Dobrar (180-210min) → Guardar (210-240min)
  • Conjunto 3: Lavar (240-270min) → Secar 270-300min) → Dobrar (300-330min) → Guardar (330-360min)
  • Conjunto 4: Lavar (360-390min) → Secar (390-420min) → Dobrar (420-450min) → Guardar (450-480min)

Execução com Pipeline

Utilizando o conceito de pipeline, assim que o primeiro conjunto de roupas vai para a secadora (etapa 2), o segundo conjunto já pode entrar na máquina de lavar (etapa 1). Dessa forma, após um período inicial, teremos todas as etapas sendo executas simultaneamente por diferentes conjuntos de roupas:

  • Minutos 0–30: Conjunto 1 na lavadora
  • Minutos 30–60: Conjunto 1 na secadora, Conjunto 2 na lavadora
  • Minutos 60–90: Conjunto 1 sendo dobrado, Conjunto 2 na secadora, Conjunto 3 na lavadora
  • Minutos 90–120: Conjunto 1 sendo guardado, Conjunto 2 sendo dobrado, Conjunto 3 na secadora, Conjunto 4 na lavadora
  • Minutos 120–150: Conjunto 2 sendo guardado, Conjunto 3 sendo dobrado, Conjunto 4 na secadora
  • Minutos 150–180: Conjunto 3 sendo guardado, Conjunto 4 sendo dobrado
  • Minutos 180–210: Conjunto 4 sendo guardado

O tempo total para processar os 4 conjuntos de roupas com pipeline é de apenas 3,5 horas (210 min), representando um ganho de desempenho de aproximadamente 56% sobre o processamento sequencial (sem pipeline).

Vantagens

A implementação do pipeline em CPU’s traz diversos benefícios significativos:

Aumento do Throughput (Vazão)

O pipeline melhora significativamente o número de instruções completadas por unidade de tempo (Throughput). Em condições ideais, um pipeline de n estágios pode alcançar um speedup teórico de até n vezes em comparação com a execução sem pipeline.

Utilização Eficiente de Recursos

Em uma implementação de processamento sequencial, partes do hardware ficam ociosas durante a execução de uma instrução. Com a implementação do pipeline, a utilização dos diferentes recursos do hardware é maximizada, permitindo que várias instruções sejam processadas simultaneamente com cada parte do hardware atuando em uma instrução distinta.

Ciclos de Clock Mais Curtos

Como cada estágio do pipeline realiza apenas uma parte específica do processamento de uma instrução, o tempo necessário para cada estágio é menor do que o tempo necessário para executar toda a instrução. Isso permite ciclos de clock mais curtos, aumentando a frequência de operação do processador.

Limitações e Riscos (Hazards)

Apesar de suas vantagens, pipelines apresentam diversas limitações e riscos (também chamados de Hazards) que precisam de muita atenção na hora de mitigar problemas, evitar gargalos e problemas de performance.

Hazards (Riscos)

Hazards são situações que podem impedir que a próxima instrução seja executada no próximo ciclo de clock. Existem 3 tipos principais:

Hazards de Dados: Elas acontecem quando uma instrução depende do resultado de uma outra instrução anterior que ainda não foi concluída para que o processamento esteja completo. Por exemplo, se a instrução 2 precisa usar o resultado da instrução 1, mas a instrução 1 ainda não completou o estágio de Escrita (Write Back).

Exemplo de Hazards de Dados em Assembly.

No exemplo da imagem acima, a Instrução 2 (sub) precisa de $s0, que só estará disponível após o estágio WB (Write Back) da Instrução 1 (add).

Representação do Hazard de Dados.

Hazards de Controle: Ocorrem quando há um desvio condicional (branches) durante o fluxo de execução de programa. Quando uma branch é encontrada, o pipeline não sabe quais instruções buscar e seguir até que a condição do branch seja avaliada.

Desvios condicionais (Branches) são como o próprio nome diz, desvios ao que conhecemos na programação como if e else. Levamos como exemplo a instrução beq, que em Assembly é um operador condicional. A instrução beq compara dois valores, $s0 e $s1. Se $s0==$s1, o desvio é tomado e o pipeline pula para instrução LABEL. Se o desvio não for tomado, ou seja, $s0 != $s1, o pipeline segue a ordem das instruções normalmente.

Exemplo de Desvio Condicional em Assembly

O problema ocorre quando o desvio condicional é tomado na instrução de execução (WB) e as instruções seguintes (add e lw) que estavam sendo processadas na pipeline são descartadas conforme é mostrado na imagem abaixo.

Nesse contexto, percebe-se que 2 instruções foram perdidas e 2 ciclos foram descartados, causando stalls (bolhas) no pipeline.

Hazards Estruturais: Ocorrem quando dois ou mais estágios do pipeline tentam usar o mesmo recurso de hardware ao mesmo tempo, mas esse recurso não está disponível para ambas.

Imagine um processador simples com apenas uma memória para armazenar tanto as instruções quanto os dados. No pipeline, diferentes estágios podem precisar acessar a memória ao mesmo tempo:

  • O estágio IF (Instruction Fetch) busca a próxima instrução na memória.
  • O estágio MEM (Memory Access) lê ou escreve dados na memória.

Se, em um mesmo ciclo de clock, uma instrução está no estágio IF e outra está no estágio MEM, ambas precisam da memória ao mesmo tempo. Como só existe uma memória, ocorre um hazard estrutural.

Além disso, stalls (bolhas) são inseridos durante os ciclos para evitar que dois ou mais estágios do pipeline acessem o mesmo recurso de hardware ao mesmo tempo.

Técnicas de Mitigação de Hazards

A partir do que foi discutido anteriormente, seria possível concluir que, quanto maior o número de estágios no pipeline, maior será a taxa de execução das instruções. Isso ocorre por dois fatores:

  1. Em cada estágio do pipeline, existe um esforço extra envolvido na movimentação de dados de buffer para buffer e na realização de várias funções de preparações e entrega de dados. Esse esforço extra pode desacelerar sensivelmente o tempo total de execução de uma única instrução. Isso é significativo quando as instruções são dependentes logicamente umas das outras ou pelo uso pesado de desvios.
  2. A quantidade de lógica de controle necessária para detectar e corrigir riscos (forwarding, stalling, branch prediction). Isso pode levar a uma situação em que a lógica que controla a passagem entre os estágios é mais complexa do que os estágios sendo controlados.

Podemos visualizar o nível de complexidade de forma simples através da imagem abaixo:

Outra consideração é o tempo de resposta: leva tempo para os buffers do pipeline operarem e isso aumenta o tempo do ciclo da instrução.

Dito isso, pipeline de instrução é uma técnica poderosa para melhorar o desempenho, mas que apresenta uma séries de problemas que vimos anteriormente nesse artigo. Para corrigir esses problemas (hazards), temos algumas técnicas.

Forwarding (Bypass)

O forwarding (ou bypass) é uma técnica para resolver hazards de dados, onde uma instrução depende do resultado de outra ainda em execução. Em vez de aguardar a escrita no banco de registradores (WB), o resultado é encaminhado diretamente para a próxima instrução que o necessita.

Funcionamento:

  • Caminhos de Bypass (Forwarding Paths): São conexões físicas que permitem enviar resultados de instruções em estágios do pipeline (EX/MEM ou MEM/WB) diretamente da ULA no estágio EX da próxima instrução. Pense em 2 instruções e que a segunda depende do resultado da primeira. Com esse Forwarding, é como se houvesse um adiantamento no resultado da primeira instrução para que a segunda instrução possa utilizar e evitar que haja stalls (bolhas) no pipeline.
  • Detecção de dependências: A unidade de controle (UC) verifica se os registradores fonte da instrução em ID (decodificação) coincidem com os registradores destino das instruções em EX (execução) ou MEM (acesso à memória).

Exemplo:

Exemplo de Forwarding em Assembly.

Pegamos como exemplo as duas instruções acima. Podemos perceber que a instrução 2 depende do resultado de $s0 da instrução 1 para poder seguir na execução do pipeline. Se não aplicarmos a técnica de forwarding, os estágios ficarão da seguinte maneira:

Representação de dois estágios sem forwarding.

Como a instrução 2 precisa do resultado de $s0 da instrução 1, o pipeline insere um stall (bolha) após os estágio ID (decodificação), pois o $s0 só estará disponível no estágio WB (escrita).

Agora vamos ver como ficará o pipeline com a técnica de forwarding:

Representação de dois estágios com forwarding.

O resultado do valor $s0 da instrução add é enviado diretamente da saída da ULA (após o estágio EX/MEM) para a entrada da ULA da sub (estágio EX). Dessa forma, a instrução sub avança sem esperar o estágio WB do add.

Stalling (Pipeline Stalls)

O stalling (ou inserção de bolhas) como vimos anteriormente são usados em situações que ocorre um hazard no pipeline e umas instrução precisa pausar o pipeline da instrução até que o dado que seja necessário para continuar os estágios seja recebido pela ULA.

Mesmo que seja usado diversas técnicas de mitigação de hazards, há situações em que nem mesmo essas técnicas tem efeito, criando hazards de dados não mitigáveis ou hazards estruturais (quando há conflito de recursos na memória).

Branch Prediction (Previsão de Desvios)

Branch prediction é uma técnica implementada para mitigar hazards de controle, onde ocorrem desvios condicionais (branches) durante os estágios do pipeline.

Com o branch prediction, o processador tenta prever o resultado de branches condicionais, evitando stalls (bolhas) no pipeline. Há 3 tipos de branch prediction:

Previsão Estática: Essa técnica prevê que todos os desvios condicionais sejam ou não tomados, independente se o caminho tomado for errado. Quando há todos os desvios são tomados, chamamos de Always Taken. Já para os desvios que nunca são tomados, damos o nome de Not Taken.

Esse tipo de técnica é menos custosa em termos de processamento dentre os três tipos de branch prediction. Contudo, ela é ideal para hardwares menores (muito utilizado em hardware antigos), pois a decisão de sempre tomar ou não o desvio pode gerar miss-predictions e um alto desperdício de ciclos, comprometendo o desempenho do processador.

Previsão Dinâmica: Os métodos dinâmicos se adaptam durante a execução, aprendendo com o comportamento do programa. A previsão dinâmica usa o histórico de execução para prever desvios condicionais nos ciclos de processamento.

Há alguns mecanismos que guardam o histórico de branches (desvios) anteriores como o One-bit predictor, Two-bit predictor, Branch History Table e o Branch Target Buffer.

O One-bit predictor armazena somente se o último desvio foi tomado ou não para poder prever o próximo desvio a ser feito. O Two-bit predictor usa um contador de 2 bits para armazenar o histórico de desvio recente (ex: os últimos 3 desvios foram tomados) com o objetivo de resistir a mudanças ocasionais. O Branch History Table (BHT) armazena um histórico maior de desvios anteriores e o Branch Target Buffer (BTB) guarda as informações sobre os destinos futuros dos branches.

RSB (Return Stack Buffer)

Essa técnica é capaz de guardar os “endereços de retorno” (onde o programa deve voltar após uma função terminar) e evitar que stalls (bolhas) surjam durante os estágios do pipeline. Além disso, como o próprio nome diz, ele é um tipo de buffer, ou seja, guarda os endereços de forma mais rápida, evitando que o processador precise buscar os endereços na memória, aumentando assim a performance.

Execução Especulativa

A execução especulativa permite que as instruções sejam executadas antes da resolução de branches, com base em previsões. Se a previsão estiver correta, o desempenho melhora; se não, as instruções são descartadas.

Reordenação de Instruções

A reordenação de instruções (estática ou dinâmica) evita hazards ao ajustar a ordem de execução preservando a semântica do programa.

Abordagens

  • Estática: Reordenação das instruções durante a compilação, ou seja, antes da execução do programa.
  • Dinâmica: Reordenação das instrução em runtime, ou seja, em tempo de execução do programa.

Conclusão

Em resumo, a abordagem de divisão de execução através de estágios com o pipeline permitiu que processadores atuais pudessem aumentar sua performance em tarefas complexas. Contudo, vimos que apesar de suas vantagens, há limitações e riscos (hazards) se não for utilizada a técnica de mitigação correta no contexto certo e, apesar desses problemas, processadores atuais possuem uma taxa de acertos por volta de ~95%. Mesmo que haja 5% de erros em contextos onde há milhares de instruções, o prejuízo normalmente é mínimo em relação ao desempenho geral do processador.

Deixarei abaixo algumas das fontes que usei para escrever esse artigo. Caso queira se aprofundar nesse e em outros assuntos relacionados à arquitetura de computadores, recomendo a leitura do livro Arquitetura e Organização de Computadores escrito por William Stallings. Também deixarei o link do livro disponível abaixo. Espero que tenha gostado da leitura!

https://medium.com/media/6d6d6daa40a74af4f4aadcae0027a4e7/hrefhttps://medium.com/media/3a00ae7a7844f377b2a5d080ef98e3bd/href
Ler o original no Medium ↗
Próximo artigoComo a Cache Hierárquica e a Localidade de Memória Afetam a Performance em Sistemas Multithread