2.7. Aumentando o desempenho com Pipeline

Pelo o que foi visto, até o momento, a execução de um programa é essencialmente sequencial, ou seja, uma instrução só é executada quando a anterior termina. Ao longo do nosso curso veremos que há dois modos de paralelismo que podem ser utilizados para melhorar ainda mais o desempenho do processador. O primeiro deles é através do chamado Paralelismo em Nível de Hardware, que é obtido quando replicamos unidades do processador para que elas funcionem em paralelo, reduzindo assim o tempo de execução dos programas. A segunda forma é através do Paralelismo em Nível de Instruções, ou ILP (do inglês, Instruction Level Parallelism). Nesse caso, as unidades do processador não são duplicadas, mas melhores organizadas para que não fiquem ociosas. Há duas formas principais de implementar o ILP, uma delas é através do Pipeline e a outra é através de processadores Superescalares. Aqui vamos tratar do Pipeline, e no capítulo sobre Processamento Paralelo, vamos tratar as outras formas de paralelismo.

Imagine que possamos dividir o Ciclo de Instrução de um determinado processador nas cinco etapas seguintes:

Carregar instrução (FI)
Traz a instrução da memória para o processador, armazena em IR (essa etapa também é chamada de Fetch de Instrução) e a decodifica para execução no passo seguinte.
Carregar operandos (FO)
Traz os operandos da operação dos registradores para a ULA, para que a operação seja realizada sobre eles, também chamada de Fetch de Operandos.
Executar instruções (EI)
Executa operação lógica ou aritmética propriamente dita.
Escrever em memória (WM)
Escreve o resultado da operação em memória, se necessário.
Escrever em registrador (WR)
Escreve o resultado da operação em um dos registradores, se necessário.

Esse é um dos Ciclos de Instrução mais simples que poderíamos imaginar, organizado em apenas 5 etapas.

[Nota]

Processadores convencionais, como os da Intel que usamos em nossos computadores, executam instruções em cerca de 18 etapas.

Cada instrução deve passar pelos cinco passos descritos para ser executada. Suponha que cada etapa necessite de apenas 1 ciclo de clock para ser executada. Quantos ciclos seriam necessários para executar um programa de 20 instruções? Essa conta é simples. Cada instrução deve passar pelas cinco etapas, e cada etapa leva 1 ciclo de clock, sendo assim, o programa levará 20 vezes 5 ciclos, ou seja, 100 ciclos de clock.

Agora vamos analisar o que acontece com cada etapa a medida em que o programa é executado. A primeira instrução vai passar pela etapa FI, que a leva para o IR e a decodifica. Em seguida ela é passada para a etapa FO, e os dados necessários para a operação são levados dos respectivos registradores para a ULA. Agora, observe. Neste exato momento, a segunda instrução do programa está parada na memória, aguardando sua vez para ser executada. Ao mesmo tempo, a etapa FI está ociosa. Por que a etapa FI não pode entrar em ação e trabalhar com a segunda instrução do programa, enquanto a primeira está na etapa FO?

O mesmo vai ocorrer com todas as etapas de execução da primeira instrução do programa. Ela vai ser executada na etapa EI, depois vai passar para a etapa WM que checará se há necessidade de copiar o resultado para a memória e, finalmente, para a WR, que copiará o resultado para um dos registradores. Quando a primeira instrução estiver na etapa WR, as etapas anteriores estarão todas ociosas. Por que não aproveitar o tempo ocioso para colocar as etapas anteriores para irem adiantando a execução das próximas instruções? É isso que propõe o Pipeline!

O Pipeline vai separar as etapas de execução de instruções em unidades físicas independentes, assim, uma etapa pode trabalhar com uma instrução, ao mesmo tempo em que uma outra unidade trabalha com uma outra instrução. É a mesma estratégia utilizada pela indústria de produção em massa para fabricar carros, por exemplo. Enquanto um chassi está sendo montado, outro está recebendo a carroceria, outro o motor, outro sendo pintado e outro recebendo o acabamento interno. Todas etapas trabalhando em paralelo e vários carros sendo tratados ao mesmo tempo. Esta estratégia aumenta o desempenho da execução das instruções de forma grandiosa.

Na Figura 2.7, “Processador adaptado para trabalhar com Pipeline de cinco estágios” são apresentadas as adequações necessárias no processador para que as etapas possam ser organizadas em Pipeline. Dizemos então que esse processador trabalha com cinco Estágios de Pipeline.

Figura 2.7. Processador adaptado para trabalhar com Pipeline de cinco estágios


A primeira mudança necessária é a separação da memória em duas partes independentes (ou duas memórias mesmo). Uma parte será utilizada apenas para instruções (representadas na figura pela palavra Fetch), e outra apenas para os dados (representada por DMem). Isso é necessário para que a etapa FI acesse a memória para buscar a próxima instrução, ao mesmo tempo em que a WM acessa a memória para salvar o resultado de outra instrução anterior. Se houvesse apenas uma memória para dados e instruções, isso não seria possível. Essa mudança vai contra o que foi projetado na ((Arquitetura de von Neumann)), e foi considerado um grande avanço. Ela foi batizada de Arquitetura Harvard.

Outra mudança importante foi a adição de memórias intermediárias entre cada etapa. Na Figura 2.7, “Processador adaptado para trabalhar com Pipeline de cinco estágios” essas memórias são representadas pelos retângulos preenchidos e sem nenhuma palavra sobre eles. Essas memórias são utilizadas para armazenar o resultado da etapa anterior e passá-lo para a etapa posterior no ciclo seguinte. Elas são necessárias porque as etapas não executam necessariamente sempre na mesma velocidade. Se uma etapa for concluída antes da etapa seguinte, seu resultado deve ser guardado nessas memórias para aguardar que a etapa seguinte conclua o que estava fazendo. Só então ela poderá receber o resultado da etapa anterior.

O mesmo ocorre na produção de um carro. A etapa de instalação do motor pode ser mais rápida do que a de pintura, por exemplo. Então, se um carro acabou de receber um motor, ele deve ser guardado num local temporário até que o carro anterior tenha sua pintura concluída. Assim, a etapa de instalação do motor pode receber um novo carro.

Qual o benefício da execução em Pipeline? Para isso, vamos analisar a Figura 2.8, “Execução em pipeline de cinco estágios”.

Figura 2.8. Execução em pipeline de cinco estágios


Nesse exemplo a dimensão horizontal (eixo X) representa o tempo, e a dimensão vertical (eixo Y) representa as instruções a serem executadas (I1, I2, I3, I4 e I5). Nessa imagem, a instrução I1 já passou por todas as etapas e está em WR, enquanto isso, I2 está em WM, I3 está em EI, I4 está em FO e I5 ainda está em FI. Como o Pipeline possui 5 estágios, ele precisa, no mínimo, de 5 instruções para encher o Pipeline e, a partir daí, inicia-se o ganho de desempenho.

Voltando ao exemplo anterior, considerando que cada etapa leve 1 ciclo de clock para ser concluída. Quantos ciclos são necessários para executar 20 instruções agora com Pipeline? No início, o Pipeline não está cheio, então a instrução I1 deve passar por todas as 5 etapas para ser concluída, levando então 5 ciclos de clock. Mas, a instrução I2 acompanhou I1 durante toda execução e terminou no ciclo seguinte, ou seja, em 6 ciclos de clock. Em seguida, a instrução I3 foi concluída em 7 ciclos de clock, I4 em 8 ciclos, I5 em 9 ciclos, assim em diante, até a conclusão da vigésima instrução, que ocorreu em 24 ciclos.

Comparado com o exemplo sem Pipeline, que executou o mesmo programa em 100 ciclos, o ganho foi de 4,17 vezes. Se o programa tivesse 200 instruções, levaria 1000 ciclos de clock sem Pipeline e 204 ciclos com Pipeline, o resultaria num ganho de 4,9 vezes. Onde queremos chegar com isso?

[Importante]

A medida em que a quantidade de instruções aumenta, o ganho de desempenho com Pipeline vai se aproximando da quantidade de estágios, que foi 5 nesse exemplo. Então, quanto mais instruções forem executadas e mais estágios de Pipeline tiver o processador, maior será o benefício de usar o Pipeline.