Exercícios do Módulo 06: Pipeline — Conceitos e Implementação Básica

Os três exercícios a seguir foram pensados para você fixar, em ordem crescente de dificuldade, o que estudamos sobre pipeline: a sobreposição de estágios, a distinção entre latência e throughput, a álgebra do speedup e o modelo de dois estágios do PIC18F4550 do seu kit ACEPIC PRO V8.2. O primeiro pede que você raciocine sobre o fluxo de instruções; o segundo exige cálculo quantitativo de CPI e speedup; o terceiro liga toda a teoria ao benchmark que você vai gravar no microcontrolador no Projeto Integrador. Resolva cada um antes de comparar com colegas — quem só lê a resolução pronta nunca desenvolve a intuição de prever, no papel, em que ciclo cada instrução estará em cada estágio.

Exercício 1: Lendo o Diagrama de Pipeline Ciclo a Ciclo

Nível Básico

Eu quero começar com o pipeline clássico de cinco estágios — busca de instrução (IF), decodificação (ID), execução (EX), acesso à memória (MEM) e escrita do resultado (WB), nessa ordem — porque é o vocabulário comum de toda a literatura de arquitetura. Imagine cinco instruções consecutivas, que vou chamar de I1 a I5, entrando uma após a outra nesse pipeline, e ciclos de clock numerados de 1 em diante. No ciclo 1, só I1 existe na linha, ocupando o estágio IF; os demais estágios estão vazios. A cada borda de clock, todas as instruções avançam um estágio de uma vez só, como uma linha de montagem que dá um passo coordenado, e uma instrução nova entra em IF.

Este exercício parece simples até você tentar a primeira vez — eu mesmo, quando estudei pipeline, me atrapalhei na hora de dizer quantos ciclos o preenchimento inicial custa. A ideia é você reconstruir o diagrama à mão, com lápis e papel, listando I1 a I5 na vertical e os ciclos de clock na horizontal, escrevendo em cada célula qual estágio aquela instrução ocupa naquele ciclo.

O que peço de você: (a) desenhe o diagrama de pipeline completo de I1 a I5, do ciclo 1 até o ciclo em que I5 conclui o estágio WB; (b) diga em que ciclo o pipeline fica cheio, isto é, o primeiro ciclo em que os cinco estágios estão simultaneamente ocupados por instruções diferentes, e justifique; (c) conte quantos ciclos passaram desde o início sem que nenhuma instrução tenha concluído — esse intervalo é chamado de enchimento ou preenchimento do pipeline; e (d) explique, em duas ou três frases, por que dizer “cinco instruções em execução simultânea” não significa que o processador termina cinco instruções por ciclo, usando a diferença entre a latência de uma instrução (o tempo do IF dela até o WB dela) e o throughput (a taxa com que instruções saem prontas pela ponta final da linha).

O diagrama abaixo mostra a ordem fixa dos cinco estágios pelos quais cada instrução passa, e é o esqueleto que você vai replicar, deslocado de um ciclo, para cada uma das cinco instruções:

flowchart LR
    IF["IF<br/>Busca da<br/>instrucao"] --> ID["ID<br/>Decodifica e<br/>le registradores"]
    ID --> EX["EX<br/>Calcula na<br/>ULA"]
    EX --> MEM["MEM<br/>Acessa a<br/>memoria de dados"]
    MEM --> WB["WB<br/>Escreve o<br/>resultado"]

Você terá terminado quando o seu diagrama mostrar, em alguma coluna de ciclo, os cinco estágios distintos ocupados ao mesmo tempo por cinco instruções diferentes, e quando conseguir dizer, em um número, quantos ciclos de enchimento antecederam a primeira conclusão.


Exercício 2: Calculando CPI, Throughput e o Speedup Real de um Laço

Nível Intermediário

Agora vamos sair do desenho e ir para os números, que é onde a maioria dos erros de raciocínio sobre desempenho aparece. Trabalhe com o PIC18F4550 do seu kit operando com oscilador de 48 MHz. Nesse processador, o ciclo de máquina T_{cy}, que é a unidade temporal de execução de uma instrução, corresponde a quatro ciclos do oscilador, de modo que T_{cy} = 4/48\ \text{MHz}. A maioria das instruções consome um único ciclo de máquina; as instruções de desvio quando o desvio é tomado, e as instruções que ocupam duas palavras de programa, como as chamadas de sub-rotina, custam dois ciclos de máquina, sendo o segundo ciclo uma bolha — um ciclo desperdiçado em que o prefetch buscou a instrução errada.

Considere um laço de processamento de amostras de um sensor que, a cada iteração, executa exatamente doze instruções: nove são instruções comuns de fluxo sequencial, de um ciclo cada; uma é uma chamada de sub-rotina de formatação, que ocupa duas palavras e custa dois ciclos; e uma é o desvio de retorno do laço, que é tomado em todas as iterações menos a última, custando dois ciclos. O laço roda mil iterações. Lembre que o CPI, sigla de ciclos por instrução, é o número médio de ciclos de máquina gastos por instrução executada, e que o throughput é a taxa de instruções concluídas por segundo.

O que peço de você: (a) calcule, para uma iteração típica do laço, o número total de ciclos de máquina consumidos e o CPI efetivo dessa iteração, mostrando a conta \text{CPI}_{ef} = \text{ciclos}/\text{instruções}; (b) usando T_{cy} a 48 MHz, calcule o throughput efetivo do laço em instruções por segundo, isto é, \Theta = N_{instr}/(C \cdot T_{cy}), onde C é a contagem de ciclos e N_{instr} o número de instruções; (c) compare esse throughput efetivo com o throughput de pico de doze milhões de instruções por segundo (doze MIPS), que o processador atingiria se todas as instruções custassem um ciclo, e diga que percentual do pico você obteve; e (d) suponha que você aplique desdobramento de laço processando oito amostras por iteração, de forma que o desvio de retorno tomado passe a ocorrer uma vez a cada oito amostras em vez de uma vez por amostra — recalcule o CPI efetivo aproximado por amostra e explique, em duas ou três frases, por que essa reorganização aproxima o throughput do teto teórico.

Use, para o desvio de retorno, o modelo de CPI médio que vimos no módulo, em que a fração de instruções penalizadas determina o acréscimo sobre o ciclo base:

\text{CPI}_{ef} = 1 + f_d,

onde f_d é a fração de instruções que custam um ciclo extra. O diagrama abaixo resume o perfil de custo das instruções dessa iteração, que você vai usar para montar a contagem de ciclos:

flowchart TB
    ITER["Uma iteracao<br/>12 instrucoes"] --> SEQ["9 instrucoes sequenciais<br/>1 ciclo cada"]
    ITER --> CALL["1 chamada de sub-rotina<br/>2 palavras = 2 ciclos"]
    ITER --> RET["1 desvio de retorno tomado<br/>2 ciclos = 1 ciclo + 1 bolha"]
    SEQ --> TOTAL["Some os ciclos<br/>e divida pelas 12 instrucoes"]
    CALL --> TOTAL
    RET --> TOTAL

Você terá terminado quando tiver, em números com unidades, o CPI efetivo das duas versões do laço, o throughput em instruções por segundo da versão original, o percentual do pico de doze MIPS alcançado, e uma frase justificando por que o desdobramento diluiu a penalidade do desvio.


Exercício 3: Prevendo e Medindo o Throughput do Seu Benchmark no Kit

Nível Desafiador

Chegou a hora de amarrar a teoria à primeira tarefa do Projeto Integrador deste módulo, em que você vai gravar no PIC18F4550 do kit ACEPIC PRO V8.2 um benchmark que mede o throughput efetivo do seu sistema e confronta a medição com a previsão teórica. A metodologia usa o Timer0 como cronômetro de ciclos de máquina: configurado sem prescaler, o Timer0 incrementa exatamente uma vez por ciclo de máquina, e a diferença entre a leitura final e a inicial do contador é o número exato de ciclos consumidos pelo trecho cronometrado. O resultado da contagem de bolhas você vai exibir nos bits baixos do PORTD, observando os LEDs do kit acenderem conforme o padrão binário do número medido.

Quero que você projete, no papel, o procedimento de medição e a previsão teórica que ele vai confirmar, antes de escrever uma linha de código. Há três armadilhas de método que separam uma medição confiável de uma enganosa, e elas são o coração deste exercício. A primeira é a leitura do contador de dezesseis bits do Timer0, que é dividido em dois bytes, TMR0L (byte baixo) e TMR0H (byte alto); o hardware só trava o byte alto em um registrador-tampão no instante em que o byte baixo é lido, e ler na ordem errada pode capturar um valor inconsistente no exato momento em que o contador cruza um múltiplo de 256. A segunda é o sobrecusto da própria instrumentação: as instruções que zeram e leem o Timer0 também consomem ciclos, e esse custo fixo precisa ser medido em torno de um trecho vazio para depois ser subtraído. A terceira é repetir o trecho medido um número grande e conhecido de vezes, dividindo a contagem total por esse número.

O que peço de você: (a) explique, em prosa, qual a ordem correta de leitura entre TMR0L e TMR0H e por que a ordem inversa pode produzir uma contagem errada — não escreva o código, descreva o raciocínio; (b) descreva o procedimento completo de medição de um trecho de código, incluindo como você isola e desconta o sobrecusto da instrumentação e como a repetição reduz o efeito desse sobrecusto sobre o resultado; (c) suponha que você cronometre dois núcleos de trabalho que produzem o mesmo resultado — um com fluxo sequencial e fração de desvios tomados f_d = 0{,}02, outro atravessado por desvios frequentes com f_d = 0{,}25 — e, usando \text{CPI}_{ef} = 1 + f_d, preveja o CPI efetivo de cada um e a razão entre os dois throughputs; e (d) descreva como você levaria a contagem de bolhas para os bits baixos do PORTD e o que o padrão de LEDs acesos significaria, conectando isso à terceira tarefa do projeto, que é identificar e documentar as instruções do seu código que causam stalls ou bolhas.

Para guiar a parte (b), siga o fluxo de medição esquematizado abaixo, que separa a calibração do sobrecusto da medição do trecho real:

flowchart TB
    START["Zera Timer0"] --> CALIB["Cronometra trecho vazio<br/>repetido N vezes"]
    CALIB --> OVER["Obtem sobrecusto fixo<br/>da instrumentacao"]
    OVER --> MEAS["Cronometra trecho real<br/>repetido N vezes"]
    MEAS --> SUB["Subtrai o sobrecusto<br/>e divide por N"]
    SUB --> CPI["Calcula CPI medido<br/>C / N_instr"]
    CPI --> CONFR["Confronta com a previsao<br/>CPI = 1 + f_d"]
    CONFR --> PORTD["Exibe contagem de bolhas<br/>nos LEDs do PORTD"]

Quando o CPI que você mediu no Timer0 coincidir, dentro de uma margem pequena, com o CPI que você previu a partir da fração de desvios tomados que contou no código, você terá evidência empírica de que entendeu o modelo de temporização do processador — e esse é exatamente o critério de pronto deste exercício. Se eles divergirem, investigar a causa da divergência vai te ensinar mais sobre o pipeline do que a coincidência ensinaria, e essa investigação é parte legítima do entregável.


Orientações para Resolução

O propósito destes exercícios não é só chegar ao número certo, mas treinar seu raciocínio sobre a troca que o pipeline oferece: um aumento modesto de latência em troca de um aumento substancial de throughput. Antes de calcular qualquer coisa, releia, no material do módulo, as seções sobre os cinco estágios, a distinção entre latência e throughput, a equação fundamental do desempenho e o modelo de CPI do PIC18F4550.

Para o exercício básico, resista à tentação de olhar um diagrama pronto e desenhe o seu à mão — é o traçado manual que fixa a regra do avanço sincronizado. Para o intermediário, escreva todos os passos da conta com unidades, sem pular etapas, e confira se o CPI que você obteve faz sentido em relação ao piso de um ciclo. Para o desafiador, trate a previsão teórica e o método de medição como duas peças que precisam encaixar: descreva o procedimento com rigor suficiente para que outra pessoa o reproduza no kit e chegue ao mesmo resultado.

Discuta suas soluções com os colegas do seu grupo e traga suas dúvidas para a aula teórica, onde vamos comparar as previsões de cada um com as medições reais no Timer0.