Capítulo 1 Organização de Computadores 1.1. Organização de um Computador As partes componentes de um computador podem ser divididas de acordo com os blocos mostrados na fig. 1. A parte operativa (datapath) é responsável pela execução de funções lógicas e aritméticas tais como soma, subtração, deslocamentos, funções E e OU bita-bit. A parte de controle comanda a parte operativa, a memória e os dispositivos de entrada e saída informando as ações que eles devem tomar e quando estas ações devem ser tomadas para executar a seqüência de instruções especificada por um programa. A memória armazena programas e dados necessários para a operação do computador. Os dispositivos de entrada capturam informação do meio externo e os dispositivos de saída fornecem informação para o meio externo. Nos computadores modernos a memória pode ser dividida em Dynamic Random Access Memory (DRAM) e Memória Cache. A DRAM possui um baixo custo por bit armazenado, mas possui um tempo de acesso relativamente elevado. A memória cache é normalmente construída com memórias SRAM (Static Random Access Memories) que possuem um custo maior por bit armazenado, mas que são bem mais rápidas. Em conseqüência, memórias caches são usualmente de pequena capacidade de armazenamento e servem como um "amortecedor" entre a memória principal (DRAM) e o processador. Todos os componentes de um computador podem ser descritos de forma hierárquica em vários níveis de abstração. Esta estratificação é importante para permitir que o projetista trabalhando em um nível ignore detalhes de descrição de camadas inferiores. Assim, um projetista de computadores trabalhando a nível de sistemas só precisa saber o tamanho da memória (espaço de endereçamento), a sua forma de organização e o seu tempo de acesso. 1 Ele não precisa saber qual a tecnologia de semicondutores ou quantos transistores (tamanho físico) foram utilizados para fabricar aquela memória. Sistema Operacional Processador Interface Parte Controle Entrada M em ória Parte Operativa Saída Fig. 1.1. Principais Blocos de um Computador. Fig. 1.2. Fotografia de uma estação de trabalho (workstation). A tela do tubo de raios catódicos (CRT) é o dispositivo de saída usual, enquanto que o teclado e o "mouse" são os dispositivos de entrada principais. Fig. 1.3. Fotografia do interior de um "mouse". 2 Fig. 1.4. Tela de um tubo de raios catódicos (Cathode Ray Tube - CRT). Um feixe de elétrons é acelerado através do vácuo contra uma tela revestida com fósforo. O feixe de elétrons é guiado em direção à tela através de uma bobina colocada na parte traseira do CRT. Este tipo de sistema, que é o mesmo usado em televisão e computadores, pinta através de uma série de pontos (pixels) uma linha de cada vez na tela. A tela é reanimada (refresh) de 30 à 60 vezes por segundo. Fig. 1.5. Cada coordenada no "buffer de quadro" na esquerda, determina o nível de cinza da coordenada correspondente para a tela do CRT, na direita. O pixel(X0, Y0) contem o padrão de bit 0011, o qual é um nível de cinza mais claro que o determinado pelo padrão de bit 1101 no pixel(X1, Y1). A fig. 1.6 mostra as tecnologias mais utilizadas ao longo dos tempos, com uma estimativa da performance relativa por custo unitário para cada uma destas tecnologias. Desde 1975, a tecnologia de circuitos integrados (CIs) define o que e com que rapidez os computadores são capazes de fazer. Assim, profissionais na área de computação devem se familiarizar com os conceitos básicos envolvendo circuitos integrados. Um transistor é simplesmente uma chave on/off controlada por eletricidade. Um CI, pode conter algumas centenas ou até mesmo alguns milhões de transistores. A taxa de aumento da integração tem sido irremarcavelmente estável ao longo dos últimos anos. A fig. 1.7 mostra o aumento da capacidade de memórias DRAM entre 1976 e 1992. A indústria de semicondutores tem quadruplicado a capacidade destes CIs a cada três anos. Esta incrível taxa de evolução na relação custo/performance e na capacidade das memórias tem governado o projeto de hardware e software de computadores e por isto, tornado obrigatório o conhecimento das tecnologias de CIs. A fabricação de um chip começa com silício (Si), uma substância encontrada na areia. O Si é chamado de semicondutor porque ele não conduz muito bem a eletricidade. Através de processos químicos, é possível se adicionar materiais ao Si, permitindo a sua transformação em: i) excelente condutor de eletricidade (similar ao cobre e ao alumínio), ou ii) excelente isolante (como plástico ou vidro), ou iii) áreas que conduzem ou isolam, de acordo com algumas condições especiais (como transistores, diodos, etc...). O processo de manufatura de CIs começa com um lingote (de 5 à 8 polegadas de diâmetro e 12 de comprimento) de cristal de silício, o qual é cortado em fatias (wafers) de cerca de 0,1 polegada de espessura. Os wafers passam então por uma série de etapas, durante as quais cria-se no Si vários elementos como transistores, diodos, capacitores, resistores, etc... Dado que um wafer custa aproximadamente o mesmo preço não importando o que está 3 construído em cima, poucos CIs implicam em altos custos porque grandes pastilhas de CIs são mais propícias a conter defeitos e assim ter de ser rejeitadas, o que reduz o rendimento do processo (process yield). Assim, projetistas de CIs tem que conhecer bem a tecnologia que está sendo utilizada para ter certeza de que o aumento no custo para produzir CIs maiores se justifica por uma melhoria na performance destes CIs. (a) (b) (c) Fig. 1.6. (a) Performance relativa por custo unitário para as tecnologias mais usadas em computadores ao longo dos tempos. (b) Gerações de computadores são normalmente determinadas pela mudança de tecnologia dominante. Tipicamente, cada geração oferece a oportunidade para desenvolver uma nova classe de computadores e de criar novas empresas. Muitos pesquisadores acreditam que o processamento paralelo para aplicações de alta performance (high-end computers) e computadores portáteis para aplicações menos críticas (low-end computers) serão a base para uma quinta geração de computadores. (c) Características de computadores comerciais desde 1950, em dólares da época, e em dólares corrigidos pela inflação para 1991. O custo de um CI pode ser expresso em três simples equações: custo da pastilha = . [ii] 1 . 2 (1 + defeitos por área x área da pastilha) [iii] pastilhas por wafer = rendimento = [i] custo do wafer . pastilhas por wafer x rendimento área do wafer área da pastilha 2 4 A primeira equação é obtida diretamente. A segunda é uma aproximação, dado que ela não subtrai a área próxima à borda do wafer que não pode acomodar pastilhas retangulares. A equação [iii] é baseada em anos de observações empíricas dos rendimentos de processos em fábricas de CIs, onde o expoente indica o número de etapas críticas no processo de manufatura. Fig. 1.7. Aumento da capacidade de chips DRAM. O eixo y é medido em Kbits (onde K = 1024 bits ou 210). A indústria de semicondutores tem quadruplicado a capacidade das memórias a cada três anos, ou 60% a cada ano, por mais de 15 anos. Fig. 1.8. Vista geral da placa do processador de uma estação de trabalho. Esta placa utiliza o MIPS R4000, o qual está localizado no lado direito, ao centro da placa. O R4000 contem memórias cache de alta velocidade implemententadas dentro da sua própria pastilha. A memória principal está contida nas placas menores, instaladas perpendicularmente à placa-mãe, no canto esquero superior. Os chips de memória DRAM estão montados nestas placas perpendiculares (chamadas SIMMs: Single In-line Memory Module) e em seguida ligados aos conectores. Os connectores na parte inferior da placa são para dispositivos de I/O externos, tais como rede (Ethernet), teclado, e vídeo. 5 Fig. 1.9. Fotografia do microprocessador MIPS R3000 encapsulado. Fig. 1.10. Fotografia de três versões diferentes do microprocessador R4000, mostrado na forma de pastilha, à esquerda no alto. Para reduzir o custo do chip, um encapsulamento menor é usado em aplicações que não necessitam alta qualidade, enquanto que um maior encapsulamento é utilizado para aplicações que necessitam alta performance: servidores e estações de trabalho. O encapsulamento grande possui pouco mais de 400 pinos, enquanto que o menor, cerca de 150 pinos. Os pinos permite um caminho de acesso mais largo entre o processador e a memória principal, e assim, permitindo transferências de dados mais rápidas e endereçamento de memórias maiores. 1.2. Clocks e Lógica Seqüencial Clocks são necessários para decidir quando um elemento que armazena o estado atual de um sistema deve ser atualizado. O sinal de clock possui um período (medido em segundos) fixo; a freqüência do clock (medida em Hertz) é o inverso do seu período. A fig. 1.11. mostra um sinal de clock identificando o seu período, sua borda de subida e sua borda de decida. Nós assumimos que todos os sinais de clocks utilizados nos sistemas que estudarmos serão acionados pelas bordas. Isto é, todas as mudanças de estado terão início quando o sinal 6 de clock mudar e valor baixo para valor alto (borda de subida) ou de valor alto para valor baixo (borda de descida). Borda de Subida Borda de Descid Período do Clock Fig. 1.11. Sinal de clock. Um sistema baseado em clock é denominado sistema síncrono. Os valores que serão escritos nos elementos de estado (também chamados elementos de memória) do sistema tem que estar válidos quando ocorrer a borda do clock que ativa a escrita. Um sinal é considerado válido quando ele está estável (seu valor não está mudando) e o valor não vai mudar mais enquanto as entradas da lógica que gera o sinal não mudarem. A fig. 1.12. mostra a relação entre os elementos de estado, a lógica combinacional e o sinal de clock em um sistema síncrono. O período do clock tem que ser suficientemente longo para garantir que as saídas da lógica combinacional (entradas do elemento de estado 2) tenham tempo para estabilizar desde a última mudança ocorrida no elemento de estado 1. Note que neste tipo de circuito, não há laços de realimentação dos elementos de memorização das saídas primárias para as entradas do circuito. Assim, o estado seguinte da lógica combinacional depende somente dos valores das suas entradas. Este tipo de circuito é denominado circuito combinacional síncrono. entradas saídas Elemento de Estado 1 Lógica Combinacional Elemento de Estado 2 Clock Fig. 1.12. Circuito Combinacional Síncrono. saídas entradas Elemento de Estado 1 Lógica Combinacional Elemento de Estado 2 Fig. 1.13. Circuito Seqüencial Síncrono. Os mesmos elementos de estado podem ser utilizados como entrada e saída do circuito de lógica combinacional. Assim, o estado seguinte da lógica combinacional depende dos valores das suas entradas e de 1 ou mais das suas saídas. Uma organização deste tipo é chamada de circuito seqüencial síncrono. (Veja fig. 1.13). 1.3. Instruções: a Linguagem da Máquina Para comandar o hardware de um computador, devemos falar a sua língua. As palavras de uma linguagem de máquina são chamadas instruções e o seu vocabulário é chamado de conjunto de instruções. 7 A representação simbólica de instruções de um computador é chamada de linguagem assembler e a representação numérica destas instruções, no formato que será executado pela máquina, é chamado de linguagem de máquina. A tradução da linguagem assembler para a linguagem de máquina é realizada por um programa chamado Assembler. Ver fig. 1.14, onde um programa escrito em C é primeiramente traduzido pelo compilador para um programa em linguagem assembler. Em seguida, o Assembler traduz o programa em linguagem assembler para a linguagem de máquina. O programa que coloca o programa em linguagem de máquina dentro da memória para ser executado é chamado de carregador (loader). O compilador é um programa que realiza a interface HW/SW do computador (fig. 1.15). Ele: i) mapeia variáveis definidas pelo programador em linguagem de alto nível diretamente para os registradores do microprocessador. ii) aloca espaços de memória para estruturas de dados simples e complexas (variáveis, arrays, matrizes,...). iii) define endereços iniciais de arrays e matrizes. iv) cria desvios e rótulos no programa compilado em linguagem de máquina antes de armazená-lo na memória principal. Fig. 1.14. Hierarquia de tradução. Um programa em linguagem de alto nível é primeiramente compilado para um programa em linguagem assembler e em seguida traduzido para um programa em linguagem de máquina. O carregador tem como função colocar o código de máquina dentro dos respectivos endereços da memória do computador para ser executado. O compilador associa variáveis de um programa aos registradores de um microprocessador. Exemplo 1: Considere o seguinte trecho de programa em linguagem C e o respectivo código assembler para o MC68000: b = 10; c = 20; c = b + c; MOVE.W MOVE.W ADD 8 #10, D0; #20, D1; D0, D1; O compilador associou a variável b ao registrador D0 e a variável c ao registrador D1. Além de associar variáveis à registradores, o compilador também aloca espaço na memória principal para armazenar estruturas de dados como arrays e matrizes. Assim, o compilador define o endereço inicial de estruturas complexas para as instruções de transferência de dados. Fig. 1.15. Programa C compilado para linguagem assembler e em seguida compilado para linguagem de máquina. Embora a tradução da linguagem de alto-nível para a linguagem de máquina seja apresentada em 2 passos, alguns compiladores eliminam a etapa intermediária e produzem a linguagem de máquina diretamente. Exemplo 2: no segmento do código C abaixo, f, g, h, i, j são variáveis. L1: if (i == j) goto L1; f = g + h; f = f - i; Assumindo que as 5 variáveis estão associadas aos 5 registradores de $16 à $20, qual é o código assembler para o MIPS? L1: Exit: beq $19, $20, L1 add $16, $17, $18 sub $16, $16, $19 # if (i == j) goto L1 (salta para L1 se i = j) # f = g + h (não executado se i = j) # f = f - i (sempre executado) Exemplo 3: compiladores freqüentemente criam desvios e rótulos quando eles não aparecem em linguagens de programação. Usando as mesmas variáveis e registradores do exemplo anterior, o código C: if (i == j) f = f - i; else f = g + h; 9 é compilado para a linguagem assembler do processador MIPS da seguinte forma: bne $19, $20, Else sub $16, $16, $19 j Exit Else: add $16, $17, $18 Exit: # salta para Else se i j # f = f - i (não executado se i j) # salta para Exit # f = g + h (não executado se i = j) Linguagens assembler são muito similares, quem sabe programar em assembler de um processador pode rapidamente aprender a programar no assembler de outros processadores. Para quem só conhece linguagens de alto nível, tais como C e Pascal, é importante entender algumas distinções fundamentais entre estas linguagens e as linguagens assembler. Saber responder adequadamente as seguintes perguntas é importante: Como existe um número limitado de registradores em um processador, o número de “variáveis” que podem ser definidas em um programa em assembler também é limitado. Existe também um número limitado de "nomes" que estas variáveis podem receber. Normalmente os nomes das variáveis em assembler são os próprios nomes dos registradores. Por exemplo, o microprocessador MC68000 fabricado pela Motorola possui oito registros de dados que são chamados D0, D1, ..., D7. Portanto dados em programas assembler do MC68000 somente podem ser armazenados em uma destas variáveis (D0 a D7). Historicamente, nos primeiros microprocessadores implementados, a limitação era o número de registradores que podiam ser implementados dentro do chip. No entanto, mesmo depois do desenvolvimento de tecnologias de alta integração em silício, o número de registradores implementados dentro de um microprocessador permaneceu reduzido. Um grande número de registradores implica nos sinais de clock terem que ser transmitidos por longas distâncias para ir de um registro a outro, resultando em um aumento do período de clock e assim tornando o microprocessador mais lento. Linguagens de programação apresentam variáveis simples contendo estruturas de dados simples como nos exemplos 1, 2 e 3, mas também podem apresentar estruturas de dados complexas, tais como arrays e matrizes. Estas estruturas de dados complexas podem conter muito mais dados do que o número de registradores no microprocessador. Como o microprocessador armazena e acessa tais estruturas complexas? Uma vez que o microprocessador pode acessar somente uma pequena parte dos dados nos registradores, estruturas de dados como arrays e matrizes são armazenados na memória principal. Operações aritméticas ocorrem somente entre registradores no microprocessador MIPS. Assim o MIPS inclui, além de instruções aritméticas, instruções de transferência de dados. Para acessar uma palavra na memória, a instrução de transferência de dados deve fornecer o seu endereço. A instrução de transferência de dados é chamada load (lw para o MIPS). O formato da instrução de load é o nome da operação seguido pelo registrador a ser carregado, o endereço de início do array e finalmente o registrador que contém o index do elemento do array a ser carregado. Assim, o endereço da memória contendo o elemento do array é formado pela soma da parte constante da instrução com o registrador. Exemplo 4: assuma que A é um array de 100 elementos cujo espaço de memória já foi alocado pelo compilador, e que o compilador associa as variáveis g, h, e i aos registradores 10 $17, $18 e $19. Assuma também que o compilador associou o endereço Astart como o endereço inicial para o array na memória. Traduza a linha de programa em C: g = h + A[i]; para o código assembler do MIPS. lw $8, Astart ($19) add $17, $18, $8 # registrador temporário $8 recebe A[i] #g = h + A[i], onde $17 = g, $18 = h, $8 = A[i] A instrução load, lw, soma o endereço de início do array Astart ao index i armazenado no registrador $19 para formar o endereço do elemento A[i]. O processador então lê a memória com o endereço A[i] (Astart ($19)) e coloca-o no registrador $8. A instrução add soma o conteúdo do registrador $18 (variável h) ao elemento do array na posição $19 (armazenado em $8). A instrução complementar de load é store. Ela transfere dados de um registrador para a memória. O formato de store é similar ao do load: o nome da operação, seguido do registrador cujo conteúdo deve ser armazenado, o endereço de início do array, e finalmente o registrador que contem o index para a posição do elemento no array a ser armazenado. Exemplo 5: assuma que a variável L está associada ao registrador $18. Assuma também que o registrador $19 contem o valor de index. Qual é o código assembler para o MIPS do seguinte comando em C: A[i] = L + A[i]; Código assembler: lw $8, Astart ($19) add $8, $18, $8 sw $8, Astart ($19) # registrador temporário $8 recebe A[i] # registrador temporário $8 recebe L + A[i], onde L = $18 # armazenar L + A[i] de volta em Astart ($19) Note que foram necessárias 3 operações para somar 2 elementos, visto que operações aritméticas só são realizadas entre registradores no MIPS. Assim, se um dos operandos está na memória principal, ele deve ser trazido para um dos registradores do microprocessador antes de ser somado. O número máximo de variáveis em um programa assembler é igual ao número de registradores no processador. Como a maioria dos programas escritos em linguagem de alto nível possuem mais variáveis do que o número de registradores disponíveis nos microprocessadores, o compilador precisa gerenciar o transbordamento de variáveis para a memória. Um compilador bem construído procura manter as variáveis utilizadas mais freqüentemente nos registradores e coloca as variáveis restantes na memória. A otimização de um compilador consiste em minimizar o número de vezes em que uma variável armazenada na memória é acessada, porque o acesso à memória é muito mais lento do que o acesso a um registrador. Ao mover uma variável para a memória para abrir espaço no conjunto de registradores para uma nova variável o compilador deve tentar mover a variável que tem a menor probabilidade de ser utilizada no futuro próximo. 11 1.4. Organização da Memória Desde os primórdios da computação, projetistas e programadores sempre desejaram grandes quantidades de memória extremamente rápida. Existem várias técnicas que permitem atingir este objetivo. Algumas destas técnicas serão apresentadas neste polígrafo, a fim de ajudar projetistas e programadores a atingir este objetivo de memória rápida e ilimitada. Definições: i) Localidade Temporal: se um item é referenciado, ele tende a ser referenciado novamente em um futuro próximo. ii) Localidade Espacial: se um item é referenciado, itens cujos endereços são vizinhos a este tendem a ser referenciados também em um futuro próximo. Localidade temporal aparece em programas a partir de estruturas simples e naturais, por exemplo, muitos programas contém loops, de forma que instruções e dados tendem a ser acessados repetidamente, mostrando altos níveis de localidade temporal. Uma vez que instruções são acessadas seqüencialmente, programas mostram um alto grau de localidade espacial, por exemplo, acessos a elementos de um array. iii) Palavra: é a unidade mais comum de trabalho de um microprocessador. Diferentes microprocessadores podem ter palavras de diferentes tamanhos (1, 2 e 4 bytes são tamanhos típicos). Nós podemos tirar vantagem do princípio de localidade para implementar uma memória de computador baseada em hierarquia. Uma hierarquia de memória consiste de múltiplos níveis de memória com diferentes velocidades e tamanhos. As memórias mais rápidas são mais caras por bit do que as memórias mais lentas e portanto elas são normalmente menores. A memória principal é normalmente implementada a partir de DRAMs (Dynamic Random Access Memories), enquanto que memórias em níveis inferiores, mais próximos do processador (memórias CACHE), são implementadas em SRAMs (Static Random Access Memories). Memórias DRAM são mais baratas por bit que as SRAM, embora as memórias dinâmicas sejam sensivelmente mais lentas. A diferença de preço se justifica porque DRAMs utilizam um menor número de transistores por bit e assim, estas memórias possuem maior capacidade de armazenar informação para a mesma área em silício. A diferença em velocidade se justifica porque as células das SRAMs armazenam informação através do chaveamento de portas inversoras que estão ligadas entre si (cross-coupled), criando uma ação de realimentação extremamente rápida, menos susceptível ao ruído e com maior capacidade de drive de corrente das linhas de bit-line, e assim, sendo mais rápidas para se ler e escrever que as pequenas células das memórias DRAM. Devido às diferenças de custo e de tempo de acesso, é vantagoso construir memórias como em hierarquia de níveis, com a memória mais rápida mais próxima do processador e a mais lenta colocada mais distante do processador (fig. 1.16). Atualmente existem três tecnologias principais para se construir hierarquias de memórias: SRAM, DRAM e disco 12 magnético. O tempo de acesso e o preço por bit variam largamente entre estas tecnologias, tal como a fig. 1.17 mostra para valores típicos de 1993. Fig. 1.16. Estrutura básica de uma hierarquia de memória. Através da implementação de um sistema de memória hierárquico, o usuário tem a ilusão de que a memória é tão grande quanto a memória presente no nível mais baixo, ao mesmo tempo que ela pode ser acessada na mesma velocidade que a memória no nível mais alto da hierarquia. Fig. 1.17. Tempo de acesso e preço por bit para as três principais tecnologias usadas na concepção de sistemas hierárquicos de memórias. Fig. 1.18. Hierarquia de memória com dois níveis: inferior (mais lento, porém com maior capacidade de armazenamento de informações) e superior (mais rápido, porém com pouca capacidade). 1.5. Quatro Princípios para Projeto de Processadores i) Simplicidade favorece regularidade: Deve-se procurar projetar conjuntos de instruções em que as instruções possuam mesmo tamanho. Os segmentos do programa em C abaixo possuem 10 variáveis (a,b,c,d,e,f,g,h,i,j): 13 Segmento 1: a = b + c; d = a - e; Código assembler para o MIPS: add a, b, c sub d, a, e Segmento 2: f = (g + h) - (i + j); Código assembler para o MIPS: add t0, g, h # variável temporária t0 contem g + h add t1, i, j # variável temporária t1 contem i + j sub f, t0, t1 # f armazena t0 - t1 Note que o compilador teve que criar 2 novas variáveis (temporárias), t0 e t1, e 2 linhas adicionais de código para traduzir o segmento 2 do programa em C para a linguagem assembler a fim de poder manter a simplicidade e a regularidade do seu conjunto de instruções através do formato: [instrução] [registro de destino/origem] [op.1] [op.2]. Ou seja, se a maioria das instruções do computador apresentarem o mesmo formato e se cada linha conter apenas 1 instrução, o hardware que implementa tais funções é bem mais simples. ii) Menor é mais rápido: Um número elevado de registradores resulta em acesso mais lento a estes registradores. Portanto, dentro dos limites de cada tecnologia, um número reduzido de registradores resulta em uma arquitetura mais rápida. iii) Bom projeto implica em compromissos: Em todo projeto de engenharia existem objetivos conflitantes, o bom projetista de computadores é aquele que consegue obter o melhor compromisso entre estes objetivos. Por exemplo, a inclusão de hardware dedicado para a execução de instruções em ponto flutuante acelera o processamento, porém aumenta a área em silício necessária para implementá-la e torna o projeto das partes de Controle e Operativa relativamente mais complexo. Outros exemplos: 1. um conjunto maior de instruções em um processador permite que ele trate mais rapidamente instruções específicas de deferentes aplicações, porém este conjunto não pode ser muito grande pois a implementação do hardware/software necessários ficariam muito complexos, comprometendo demasiadamente a velocidade de execução do processador. 2. multiplexação do número de pinos de I/O de um processador: por exemplo, um processador que multiplexa 32 de seus pinos entre dados (32 bits) e endereços (32 bits); ou então um processador que trabalha com barramento interno de dados de 64 bits e com barramento externo de 32 bits. Note que quanto maior o número de pinos, mais caro é o encapsulamento do chip, porém maior é o paralelismo obtido, resultando em um processamento mais rápido. iv) Fazer o caso comum rápido: Nada adianta fazer com que uma instrução complicada, que raramente é executada, seja rápida. O importante é que o caso comum que é executado freqüentemente seja executado com rapidez. 14 1.6. Notas Históricas (Evolução das Arquiteturas) Ao longo da história dos computadores, podem ser identificadas algumas características que tipificam algumas eras. Os microprocessadores podem ser classificados de acordo com a quantidade de registradores e a função destes registradores: i) Arquiteturas de Acumulador: a característica dos primeiros microprocessadores era a existência de um registrador chamado de acumulador. Este registrador acumulava os resultados de todas as operações aritméticas realizadas no microprocessador. Nestas máquinas as operações aritméticas eram sempre realizadas entre o valor do acumulador e um valor armazenado em memória. ii) Registradores Dedicados: (HL do 8086) no lugar de um único registrador com a função específica de acumulador, estas máquinas possuem vários registradores com funções específicas. (Ex: o par de registradores HL do 8086 é utilizado como um índice para endereçar a memória). iii) Registros de Uso Geral: a evolução dos registradores de uso dedicado resultou nas arquiteturas com registradores de uso geral. Nestes microprocessadores todos os registradores são idênticos e podem ser usados sem restrição. As arquiteturas de registradores de uso geral se sub-dividem em: a) Registro-Memória: é permitida a realização de operações aritméticas em que um dos operandos está na memória e o outro em um registrador. b) Carrega-Armazena: todas as operações são feitas exclusivamente com operandos que estão em registradores. Não é possível combinar um acesso à memória com uma operação aritmética na mesma operação. Apesar desta arquitetura requerer um número maior de instruções para realização de uma mesma tarefa, o ganho em velocidade devido à simplicidade da arquitetura compensa o número maior de instruções executadas. 15 Capítulo 2 Análise de Desempenho 2.1. Definição de Desempenho Avião Capacidade (# de passageiros) Alcance (kilometros) Boeing 737-100 Boeing 747 BAC/Sud Concorde Douglas DC-8-50 101 470 132 146 1.008 6.640 6.400 13.952 Velocidade Cruzeiro) (km/h) 957 976 2.160 870 Capacidade de Transporte (pass. x km/h) 96.637 458.720 285.120 127.078 Tabela 2.1. Capacidade, alcance e velocidade para aviões. A tabela 2.1 apresenta a capacidade medida em número de passageiros, o alcance medido em quilômetros, a velocidade cruzeiro medida em km/h e a capacidade de transporte medida em passageiros x km/h. A partir dos dados da tabela 2.1 pergunta-se qual o avião com melhor desempenho? Ao depararmo-nos com esta pergunta a primeira dúvida que surge é como se define desempenho. Três possibilidades podem ser consideradas para definir o avião com maior desempenho: • A maior velocidade cruzeiro é do Concorde; • O avião com maior alcance é o DC-8; • O avião com maior capacidade é o 747; Se optarmos por definir desempenho em termos de velocidade, ainda restam duas possibilidades: 16 1. O avião mais rápido é aquele que possui a maior velocidade cruzeiro, levando um único passageiro de um ponto a outro (Concorde). 2. O avião mais rápido é aquele que consegue transportar um grande número de passageiros de um ponto a outro no menor intervalo de tempo (Boeing 747). De maneira similar, para o usuário de um computador, o computador mais rápido é aquele que termina seu programa primeiro. Para o gerente de um centro de processamento de dados com vários computadores, é aquele que completa mais programas num curto espaço de tempo. Tempo de resposta tempo decorrido entre o início e o término de uma tarefa, incluindo acessos a disco, acessos à memória, atividades de entrada/saída, execução de outros programas no caso de multiprogramação, onde vários programas são executados concorrentemente, etc... Também chamado tempo de execução. Capacidade de processamento Quantidade total de trabalho realizado em um determinado intervalo de tempo (throughput). Ao estudar desempenho, vamos nos concentrar no tempo de resposta ou tempo de execução de um computador. O tempo de execução e o desempenho são inversamente proporcionais: Desempenho (x) = ________1__________ Tempo de Execução (x) (1) Comparando uma máquina x que possui maior desempenho do que uma máquina y, obtemos que o tempo de execução de y é maior do que o tempo de execução de x: Desempenho (x) > Desempenho (y) (2) ________1__________ > ________1__________ Tempo de Execução (x) Tempo de Execução (y) (3) Tempo de Execução (y) > Tempo de Execução (x) (4) Para obter uma medida comparativa de desempenho, nós dizemos que “x é n vezes mais rápida do que y”, significando que: Desempenho (x) = n (5) Desempenho (y) Se x é n vezes mais rápida do que y, então o tempo de execução de y é n vezes mais longo do que o tempo de x. Desempenho (x) = Tempo de Execução (y) = n Desempenho (y) Tempo de Execução (x) 17 (6) 2.2. Relacionando Medidas (tempo de CPU, ciclos de clock, frequência) Tempo de CPU de um Programa = # ciclos de clock X período de clock (7) Tempo de CPU de um Programa = (8) # ciclos de clock Freqüência do clock O projetista pode aumentar o desempenho reduzindo o período de clock ou o número de períodos de clock necessário para executar o programa. O número médio de clock por instrução é normalmente abreviado por CPI (clocks per instruction). # de ciclos de clock = # de instruções no programa X CPI (9) Tempo de CPU = # de instruções X CPI X período de clock (10) Tempo de CPU = # de instruções x CPI Freqüência de clock (11) Projetistas podem obter o número de ciclos de CPU de um programa através da seguinte equação: # Ciclos de CPU = Σ n i=1 (CPIi.Ci) (12) onde Ci é o número de instruções tipo i no programa, CPIi é o número médio de períodos de clock por instrução do tipo i e n é o número total de classes de instrução. 2.3. Medidas de Desempenho Várias métricas foram criadas para substituir o tempo como medida de desempenho de um computador. Métricas simples que são válidas em um contexto limitado são comumente usadas de forma errada. 2.3.1. MIPS MIPS (Million Instructions per Second) é a medida da freqüência de execução de uma dada instrução em uma máquina específica. Também é chamada de MIPS nativa. MIPS = Tempo de CPU X 10 MIPS = = # de instruções # de instruções 6 (13) # ciclos CPU X período clock X 10 # de instruções X Freqüência do clock # de instruções X CPI X 10 = 6 6 Freqüência do clock CPI X 10 (14) 6 Portanto: MIPS = Freqüência do clock CPI X 106 18 (15) MIPS mede a freqüência de execução de instruções, portanto MIPS especifica o desempenho inversamente ao tempo de CPU. Máquinas mais rápidas possuem um MIPS mais elevado. O uso de MIPS como medida para comparar máquinas diferentes apresenta três problemas: • MIPS especifica a freqüência de execução de instruções. Ele depende do conjunto de instruções da máquina. Não pode-se comparar máquinas com diferentes conjuntos de instruções pois o número de instruções executadas será certamente diferente. • MIPS varia entre programas do mesmo computador uma vez que ele depende do tipo das instruções executadas. Portanto, a mesma máquina pode apresentar várias medidas de MIPS. • MIPS pode variar inversamente com o desempenho. Exemplo: considere uma máquina com hardware dedicado para operações em ponto flutuante. Programas de ponto flutuante rodando no hardware opcional ao invés do hardware simples levam menos tempo de CPU, mas tem uma medida de MIPS inferior. Isto ocorre porque programas em ponto flutuante quando executados via software (isto é, rodando em HW simples) executam instruções simples depois de compilados (portanto CPI baixo, resultando em uma medida de MIPS elevada). Por outro lado, leva-se mais ciclos de clock por instrução em ponto flutuante em HW dedicado do que para uma instrução em inteiros em HW simples (portanto CPI elevado) resultando em uma medida de MIPS baixa. 2.3.2. MFLOPS MFLOPS (Million Floating-Point Operations per Second) mede o desempenho de operações em ponto flutuante. MFLOPS = # Operações de Ponto Flutuante Tempo de CPU x 10 (19) 6 Uma operação de ponto flutuante é uma adição, subtração, multiplicação ou divisão aplicada a um número representado em ponto flutuante. 19 O uso de MFLOPS como medida para comparar máquinas diferentes apresenta três problemas: • MFLOPS depende do programa. Programas diferentes requerem a execução de diferentes números de operações de ponto flutuante. • MFLOPS não pode ser aplicado para programas que não usam ponto flutuante. • MFLOPS não é uma boa medida pois o conjunto de operações de ponto flutuante disponível em cada máquina é diferente. Ex: o Cray-2 não tem instrução de divisão, enquanto que o Motorola 68882 tem divisão, raiz quadrada, seno e cosseno. Assim, são necessárias ao Cray-2 várias operações de soma/subtração em inteiros para realizar uma única operação de ponto flutuante, enquanto que o Motorola 68882 necessita apenas uma única instrução. 2.3.3. Speedup Um dos conceitos mais utilizados na avaliação de desempenho de um sistema computacional é o conceito de Speedup que pode ser traduzido simplesmente por “aumento de velocidade”. O speedup é simplesmente definido como o quociente entre o desempenho de um sistema computacional depois e antes de uma melhoria: Speedup = Desempenho depois da melhoria _______________________________________ Desempenho antes da melhoria Lembrando que o desempenho de um sistema computacional para uma determinada aplicação é igual ao inverso do tempo necessário para executar a aplicação no sistema, o speedup também pode ser medido como o quociente entre os tempos de execução antes e depois da melhoria: Speedup = Tempo de Execução antes da melhoria __________________________________________ Tempo de Execução depois da melhoria Uma das relações mais conhecidas quando se trata da avaliação de performance de computadores, especialmente na área de máquinas paralelas, é a Lei de Amdahl. A lei de Amdahl contempla o fato de que uma melhoria realizada em um sistema computacional normalmente afeta apenas uma parte das operações computacionais realizadas por aquele sistema. A Lei de Amdahl pode ser sumariada na seguinte relação: TEX/DEP = TEX/AFE + TEX/NAF QMEL Onde TEX/DEP é o tempo de execução depois da melhoria ser aplicada ao sistema, TEX/AFE é o tempo de execução afetado pela melhoria, QMEL é a quantidade de melhoria obtida na parte do sistema afetada pela melhoria (por exemplo, QMEL = 3 indica que a porção do sistema afetada pela melhoria ficou três vezes mais rápida) e TEX/NAF é o tempo de execução não afetado pela melhoria. 20 Capítulo 3 Parte Operativa 3.1 Introdução 3.1.1. Notação em Complemento de 2 A notação em complemento de 2 é a forma mais comumente utilizada para representar números com sinal em computadores. Nesta notação, se o bit mais significativo (o bit mais à esquerda quando o número é escrito) é igual a 0, o número é considerado positivo e o seu valor decimal pode ser lido diretamente pela maneira convencional de conversão de valores binários para valores decimais. No entanto, se o bit mais significativo é igual a um, o número é negativo e a sua magnitude pode ser encontrada invertendo-se bit-a-bit a representação binária, somando-se 1 ao valor invertido e fazendo-se a conversão normal de binário para decimal. Esta operação é ilustrada na fig. 3.1. 0010 = +2 1101 : inverte + + 1 : soma 1 1110 = -2 0001 : inverte 1 : soma 1 0010 + 0010 = +2 1110 = -2 0000 = 0 = +2 (a) Inversão de sinais (b) Soma de números complementares. Fig. 3.1. Operações em Complemento de 2. 21 3.1.2. Overflow Overflow ocorre quando o número de bits do resultado é maior do que a capacidade de representação da arquitetura. A fig. 3.2 mostra situações de ocorrência de overflow em aritmética em complemento de dois em uma arquitetura de 8 bits. Overflow pode ocorrer tanto em operações de adição quanto de subtração. Uma situação de overflow pode ser detectada analisando-se os sinais dos operandos, o tipo de operação e o sinal do resultado. A tabela 3.1 apresenta as situações em que ocorre overflow. 10000001 01111111 + 00000010 10000001 = + 127 = - 127 - 00000010 = +2 =+2 = - 127 ||| 10000001 + 11111110 101111111 (a) Adição = - 127 = -2 = + 127 (b) Subtração Fig. 3.2. Ocorrência de overfow em operações aritméticas. Operação Sinal do Operando A Sinal do Operando B Sinal do Resultado A+B + + - A+B - - + A-B + - - A-B - + + A+B + - não A+B - + não A-B + + não A-B - - não Tabela 3.1. Condições para ocorrência de overflow. A detecção de overflow (transbordamento) e a decisão de o que fazer no caso de ocorrência de overflow é feita pelo projetista do processador. Pode-se gerar uma exceção forçando-se assim o software a tomar uma ação no caso de ocorrência de overflow, ou pode-se apenas setar um flag indicando que ocorreu overflow. Neste último caso é responsabilidade do programador verificar este flag e tomar alguma providência no caso de ocorrência de overflow. 3.2. Unidade Lógica e Aritmética para Computadores (ULA) A Unidade Lógica e Aritmética é o componente principal da parte operativa de qualquer microprocessador. Ela realiza operações aritméticas como adição e subtração e 22 operações lógicas como "E" e "OU". A seguir, demonstramos como uma ULA pode ser construída a partir das 4 funções lógicas elementares mostradas na fig. 3.3. 1. Porta E : (c = a . b) a 0 0 1 1 b 0 1 0 1 c=a.b 0 0 0 1 2. Porta OU : (c = a + b) a 0 0 1 1 b 0 1 0 1 c=a+b 0 1 1 1 3. Inversor : (c = a) a a 0 1 c c = ^a 1 0 4. Multiplexador: então c ← a senão c ← b) (Se d = 0 d c 0 a 1 b Fig. 3.3. Funções Lógicas Elementares. Uma Unidade Lógica de um bit pode ser construída utilizando-se uma porta E, uma porta OU e um multiplexador como ilustrado na fig. 3.4. A próxima função a incluir é a soma. Para construir um somador de 1 bit, devemos considerar como a soma é realizada. Conforme indicado na fig. 3.5, cada somador de 1 bit tem três entradas: os 2 operandos e o vem-um (carry-in) que é oriundo do transbordamento do bit imediatamente anterior, e produz duas saídas: 1 bit soma e 1 bit de vai-um (carry-out) que será usado no somador do bit posterior. 23 A “tabela verdade” do somador de 1 bit é apresentada na fig. 3.6. Observe que a saída "Vai-Um" será igual a 1 se e somente se pelo menos duas das entradas forem 1 simultaneamente. A saída soma será igual a um se e somente se um número ímpar de entradas for igual a 1. Fig. 3.4. Unidade Lógica de 1 bit para as operações "E"e "OU". A B Carry-in Carry-out A B Carry-out Soma Soma Somador Completo (Full Adder) Somador Parcial (Half Adder) Fig. 3.5. Somador de 1 bit. a 0 Entradas b 0 Saídas Vem-um 0 Vai-um 0 Soma 0 0 0 1 0 1 Comentários 0+0+0=00two 0+0+1=01two 0 1 0 0 1 0+1+0=01two 0 1 1 1 0 0+1+1=10two 1 0 0 0 1 1+0+0=01two 1 0 1 1 0 1+0+1=10two 1 1 0 1 0 1+1+0=10two 1 1 1 1 1 1+1+1=11two Fig. 3.6. Tabela verdade do somador de 1 bit. 24 Juntando a unidade lógica de 1 bit e o somador de 1 bit, podemos construir uma Unidade Lógica e Aritmética (ULA) de 1 bit conforme ilustrado na fig. 3.7. Observe que agora estamos utilizando um multiplexador com três entradas, portanto precisaremos de dois bits para especificar a operação a ser realizada na ULA. A ULA mostrada na fig. 3.7 possui uma séria limitação: ela não subtrai. Lembrando que em notação de complemento de 2, pode-se obter o complemento de um número invertendo-se todos os bits e somando-se 1, podemos incluir a operação de subtração conforme ilustra a fig. 3.8, utilizando um segundo multiplexador e um inversor. Agora para realizar a operação de subtração devemos especificar a inversão do operando b e colocar 1 na entrada vem-um do somador. Fig. 3.7. Unidade Lógica e Aritmética de 1 bit. Finalmente, 32 destas ULAs de 1 bit podem ser conectadas para formar uma ULA de 32 bits, conforme ilustrado na fig. 3.9. Soma Fig. 3.8. ULA de 1 bit que soma, subtrai e realiza as operações lógicas "E" e "OU". 25 Vem-Um Inverte Operação Vem-Um a0 ALU0 b0 Resultado 0 Vai-Um Vem-Um a1 ALU1 b1 Vai-Um Resultado 1 Vem-Um a2 ALU2 b2 Resultado 2 Vai-Um : : : : Vem-Um a31 ALU31 b31 Resultado 31 Vai-Um Fig. 3.9. ULA de 32 bits. O símbolo mais comumente utilizado para representar uma ULA na representação em diagrama de blocos de o sistema é mostrado na fig. 3.10. Operação 3 Linhas de Controle da ULA Função Inverte Operação a ALU Zero Resultado Overflow b Carry-out 0 00 E 0 01 OU 0 10 soma 1 10 subtrai Fig. 3.10. Símbolo para ULA (a linha "Operação" inclui as linhas "Inverte" e "Operação" da fig. 3.8). 3.3. Operação de Soma (Carry Lookahead) O crescimento linear do delay com o tamanho da palavra de entrada (conforme visto anteriormente) pode ser melhorado através do cálculo dos carries de cada estágio em paralelo. O carry do estágio Ci pode ser expresso como: 26 Ci = Gi + Pi.Ci-1 onde Gi = Ai.Bi Pi = Ai + Bi (generate signal) (propagate signal). [1] [2] [3] Expandindo [1], temos: Ci = Gi + PiCi-1 + PiPi-1Gi-2 + ... + Pi ... P1C0 A soma Si é gerada por: Si = Ci-1 ⊕ Ai ⊕ Bi = Ci-1 ⊕ Pi A quantidade de portas lógicas necessárias para implementar este somador pode claramente explodir exponencialmente. Como conseqüência, o número de estágios do lookahead é normalmente limitado a quatro. Para um somador de quatro estágios (quatro bits), os termos apropriados são os seguintes: C1 = G1 + P1.C0 C2 = G2 + P2G1 + P2P1C0 C3 = G3 + P3G2 + P3P2G1 + P3P2P1C0 C4 = G4 + P4G3 + P4P3G2 + P4P3P2G1 + P4P3P2P1C0 Duas possíveis implementações para este somador carry lookahead podem ser vistas na fig. 3.11. (a) 27 A1 G1 B1 C0 C1 P1 G1 P1 P2 C2 A2 G2 B2 G2 P2 P3 A3 G3 B3 P3 C3 G3 A1 B1 S1 C0 A2 B2 C1 S2 A3 B3 C2 S3 (b) Fig. 3.11. Somador Carry Lookahead de 4 bits. 3.4. Operação de Multiplicação Nesta seção vamos estudar dois algoritmos de multiplicação. Começaremos com um algoritmo simples para compreender o fluxo de dados. Em seguida apresentaremos o Algoritmo de Booth, que é um dos algoritmos mais utilizados na prática. A escolha dos projetistas pelo Algoritmo de Booth se justifica pelo fato dele poder multiplicar números positivos e negativos, independentemente dos seus sinais. Os dois operandos a serem multiplicados são chamados de multiplicando e multiplicador e o resultado é chamado de produto: Multiplicando: Multiplicador: Produto: 28 B x A . C Para implementar um algoritmo de multiplicação em hardware necessitamos dois registradores: o registrador do Multiplicando e o do Produto. Se considerarmos uma multiplicação de números com n bits, o registrador do Produto deverá ter 2n bits. O registrador do Produto é dividido em duas partes: produto(alto) e produto(baixo). A parte superior do registrador do Produto, produto(alto), com n bits, é inicializada com 0s, enquanto que o multiplicando é colocado no registrador do Multiplicando e o multiplicador é colocado na parte inferior do registrador do Produto: produto(baixo). A operação deste primeiro algoritmo de multiplicação pode ser descrita conforme visto na fig. 3.12. MULTIPLICAÇÃO 1: 1. produto (alto) ← ∅ 2. produto (baixo) ← multiplicador 3. for i ← ∅ to 31 4. 5. 6. do if multiplicador (∅) = 1 then produto (alto) ← produto (alto) + multiplicando produto ← produto >> 1 Fig. 3.12. Algoritmo Multiplicação 1. Exemplo: 510 x 210 = 001012 x 000102, onde: Iteração Operação Multipli multiplicador: 00101 (510) multiplicando: 00010 (210) Produto Multiplicador(0) cando alto baixo 0 Inicialização 00010 00000 00101 1 Prod ← Prod + Mult 00010 00010 00101 Prod ← Prod >> 1 00010 00001 00010 2 Sem operação 00010 00001 00010 Prod ← Prod >> 1 00010 00000 10001 Prod ← Prod + Mult 00010 00010 10001 Prod ← Prod >> 1 00010 00001 01000 4 Sem operação 00010 00001 01000 Prod ← Prod >> 1 00010 00000 10100 5 Sem operação 00010 00000 10100 Prod ← Prod >> 1 00010 00000 01010 3 1 0 1 0 0 0 ↓ 1010 O hardware necessário para implementar este algoritmo é mostrado na fig. 3.13. Este algoritmo estudado faz a multiplicação de números inteiros positivos. Para operar números inteiros negativos, poderíamos transformá-los em positivos, realizar a multiplicação e negar o resultado se os operandos fossem de sinais opostos. Um algoritmo mais elegante para multiplicar números com sinal é o Algoritmo de Booth. Para entender este algoritmo, observamos que existem várias maneiras para calcular o produto de dois números. Ao encontrar uma seqüência de 1s no multiplicador, ao invés de 29 realizar uma soma para cada um dos 1s da seqüência, o Algoritmo de Booth faz uma subtração ao encontrar o primeiro 1 e uma soma após o último. MULTIPLICANDO 32 SOMA ALU DE 32 BITS 32 32 DESLOCA À DIREITA PRODUTO PRODUTO CONTROLE 64 BITS BITφ φ) Multiplicador( Fig. 3.13. Hardware para Multiplicação 1. x 000010 000010 001110 x 001110 000000 000000 + 000010 (soma) = 1410 - 000010 + 000010 (soma) (subtração) 000000 + 000010 (soma) 000000 000000 000000 = 210 + 000010 _ (soma) 000000 . 00000011100 00000011100 (a) = 2810 (b) Fig. 3.14. Multiplicação de +2 por +14 representados em 6 bits: (a) Método tradicional; (b) Método de Booth. Observe no exemplo apresentado na fig. 3.14 que enquanto o método tradicional necessitou de 3 operações aritméticas para efetuar a multiplicação, o método de Booth completou a multiplicação com apenas 2 duas. Podem-se construir exemplos em que o método de Booth necessita mais operações aritméticas. No entanto, como números negativos representados em complemento de dois tendem a possuir uma longa seqüência de uns em sua representação, a multiplicação destes números pelo método de Booth é em geral mais rápida. Na representação do algoritmo de multiplicação de Booth da fig. 3.15, produto(alto) representa a metade alta do registrador de produto, produto(baixo) representa a metade baixa do registrador de produto, produto(0) representa o bit menos significativo do registrador de produto e bit_à_direita representa um bit armazenado em hardware e que memoriza qual era o valor do bit à direita do bit do multiplicador que está sendo processado. O símbolo >>aritmético indica um deslocamento aritmético à direita. Quando um deslocamento aritmético é realizado, 0s são introduzidos à esquerda se o bit mais significativo do número original era 0, e 1s são introduzidos à esquerda se o bit era 1. Em outras palavras, o deslocamento preserva o sinal do operando. 30 BOOTH: 1. produto (alto) ← ∅ 2. produto (baixo) ← multiplicador 3. bit_à_direita ← ∅ 4. for i ← ∅ to 31 5. do if bit_à_direita = ∅ and produto (∅) = 1 6. then produto (alto) ← produto (alto) - multiplicando 7. else if bit_à_direita = 1 and produto (∅) = ∅ 8. 9. 10. then produto (alto) ← produto (alto) + multiplicando bit_à_direita ← produto (∅) produto ← produto >>aritmético 1 Fig. 3.15. Algoritmo de Booth. Lembrando que a operação de soma pode levar bastante tempo para ser realizada por causa do tempo necessário para propagar o “vai-um” até o bit mais significativo, e lembrando também que a subtração é feita pelo mesmo circuito do somador, o Algoritmo de Booth oferece a vantagem de redução do número de operações na ULA quando o multiplicador possui uma seqüência longa de 1s. Verifique no hardware para o algoritmo de Booth apresentado na fig. 3.16 a presença de um bit extra de armazenamento. Este bit é necessário para “lembrar” o bit à direita do bit que está sendo processado. MULTIPLICANDO 32 SOMA/SUBTRAI CONTROLE 32 Produto(0) 32 PRODUTO Bit à Direita DESLOCAMENTO Fig. 3.16. Hardware para Algoritmo de Multiplicação de Booth. No exemplo a seguir, o Algoritmo de Booth é usado para multiplicar números negativos expressos em notação complemento de 2. Exemplo 1: -310 x 210 = 0010 x 1101, onde: multiplicador: 1101 (-310) multiplicando: 0010 (210) Obs.: -210 = 1110. 31 Iteração Operação Multipli cando Produto alto baixo 0 Inicialização 0010 0000 1101 1 Prod(alto) ← Prod(alto) - Mult 0010 1110 1101 Prod ← Prod >>aritmético 1 0010 1111 0110 Prod(alto) ← Prod(alto) + Mult 0010 0001 0110 Prod ← Prod >>aritmético 1 0010 0000 1011 Prod(alto) ← Prod(alto) - Mult 0010 1110 1011 Prod ← Prod >>aritmético 1 0010 1111 0101 Sem operação 0010 1111 0101 Prod ← Prod >>aritmético 1 0010 1111 1010 2 3 4 Bit à direita/ Produto(0) 0/1 1/0 0/1 1/1 1/0 ↓ −6 Exemplo 2: -210 x -210 = 1110 x 1110, onde: multiplicador: 1110 (-210) multiplicando: 1110 (-210) Iteração Operação Multipli cando Produto alto baixo 0 Inicialização 1110 0000 1110 1 Sem operação 1110 0000 1110 Prod ← Prod >>aritmético 1 1110 0000 0111 Prod ← Prod - Mult 1110 0010 0111 Prod ← Prod >>aritmético 1 1110 0001 0011 Sem operação 1110 0001 0011 Prod ← Prod >>aritmético 1 1110 0000 1001 Sem operação 1110 0000 1001 Prod ← Prod >>aritmético 1 1110 0000 0100 2 3 4 Bit à direita/ Produto(0) 0/0 0/1 1/1 1/1 1/0 ↓ 4 3.5. Operação de Divisão Dividendo = A |B C . R = Divisor = Quociente = Resto A divisão pode ser computada com o mesmo hardware da multiplicação: DIVISOR Soma/Subtrai ALU DE 32 BITS 32 Desloca RESTO 32 CONTROLE E o algoritmo para a divisão pode ser visto na fig. 3.17. DIVISÃO: 1. resto (alto) ← ∅ 2. resto (baixo) ← dividendo 3. resto ← resto << 1 4. For i ← ∅ to 31 5. do resto (alto) ← resto (alto) - divisor 6. if resto(alto) < ∅ 7. then resto (alto) ← resto (alto) + divisor 8. 9. resto ← resto << 1 else resto ← resto <<aritmético 1 10. resto(alto) ← resto(alto) >> 1 Fig. 3.17. Algoritmo de Divisão. Exemplo 1: 710 ÷ 210 = 0111 ÷ 0010, onde: dividendo: 0111 (710) divisor: 0010 (210) Obs: -2 = 11102 Iteração 0 1 2 3 4 - Operação Divisor Resto alto baixo Inicialização 0010 0000 0111 Resto ← Resto << 1 0010 0000 1110 Resto(alto) ← Resto(alto) - Divisor 0010 1110 1110 Resto < 0 : Resto(alto) ← Resto(alto) + Divisor 0010 0000 1110 Resto ← Resto << 1 0010 0001 1100 Resto ← Resto - Divisor 0010 1111 1100 Resto(alto) < 0 : Resto(alto) ← Resto(alto) + Divisor 0010 0001 1100 Resto ← Resto << 1 0010 0011 1000 Resto ← Resto - Divisor Resto > 0 : Resto ← Resto <<aritmético 1 0010 0001 1000 0010 0011 0001 Resto ← Resto - Divisor Resto > 0 : Resto ← Resto <<aritmético 1 0010 0001 0001 0010 0010 0011 Resto (alto) ← Resto (alto) >> 1 0010 0001 0011 Resto / Quoc. Exemplo 2: 810 ÷ 310 = 01000 ÷ 00011, onde: dividendo: 01000 (810) divisor: 00011 (310) Obs: -3 = 111012 33 Iteração 0 1 2 3 4 5 - Operação Divisor Resto alto baixo Inicialização 00011 00000 01000 Resto ← Resto << 1 00011 00000 10000 Resto ← Resto - Divisor 00011 11101 10000 Resto < 0 : Resto + Divisor 00011 00000 10000 Resto ← Resto << 1 00011 00001 00000 Resto ← Resto - Divisor 00011 11110 00000 Resto < 0 : Resto + Divisor 00011 00001 00000 Resto ← Resto << 1 00011 00010 00000 Resto ← Resto - Divisor 00011 11111 00000 Resto < 0 : Resto + Divisor 00011 00010 00000 Resto ← Resto << 1 00011 00100 00000 Resto ← Resto - Divisor Resto > 0 : Resto ← Resto <<aritmético 1 00011 00001 00000 00011 00010 00001 Resto ← Resto - Divisor 00011 11111 00001 Resto < 0 : Resto + Divisor 00011 00010 00001 Resto ← Resto << 1 00011 00100 00010 Resto (alto) ← Resto (alto) >> 1 00011 00010 00010 Resto / Quoc. Até o momento números negativos foram ignorados na divisão. A maneira mais simples é lembrar dos sinais do divisor e do dividendo e então negar o quociente se os sinais são diferentes. Note que a seguinte equação deve ser verificada: Dividendo = Quociente x Divisor + Resto Assim, veja o exemplo: +7 ÷ +2: -7 ÷ +2: +7 ÷ -2: -7 ÷ -2: quociente = +3 e resto = +1 quociente = -3 e resto = -1 quociente = -3 e resto = +1 quociente = +3 e resto = -1 Desta forma, não basta apenas inverter o sinal do quociente quando os sinais do dividendo e do divisor forem diferentes, também é preciso notar que o sinal do resto sempre é o mesmo do dividendo. 3.6. Notação em Ponto Flutuante Permite a representação de números reais e de números com magnitudes muito diferentes em uma forma padrão. A representação utilizada é a notação científica em que o ponto decimal é colocado à direita do primeiro algarismo significativo e a magnitude do número é armazenada no expoente. 34 35 36 37 38 Capítulo 4 Arquiteturas em Pipeline 4.1. Organização de uma Máquina Pipeline A maioria das arquiteturas e processadores projetadas depois de 1990 possuem uma organização chamada pipeline. A tradução literal de “pipeline” seria “linha de dutos”, uma tradução mais aproximada do significado que este termo assume em arquiteturas de computadores seria “linha de montagem”. Organizar a execução de uma tarefa em pipeline significa dividir a execução desta tarefa em estágios sucessivos, exatamente como ocorre na produção de um produto em uma linha de montagem. Em um microprocessador, a execução de uma instrução é tipicamente dividida em cinco estágios: busca da instrução, decodificação da instrução, execução da instrução, acesso à memória e escrita de resultados. Esta divisão da execução em múltiplos estágios aumenta o desempenho do processador pois é possível ter uma instrução diferente executando em cada estágio ao mesmo tempo. A passagem de uma instrução através dos diferentes estágios do pipeline é similar à produção de um carro em uma linha de montagem. Existem algumas regras muito importantes que devem ser seguidas quando se projeta uma arquitetura em pipeline: 1. Todos os estágios devem ter a mesma duração de tempo. 2. Deve-se procurar manter o pipeline cheio a maior parte do tempo. 3. O intervalo mínimo entre o término de execução de duas instruções consecutivas é igual ao tempo de execução do estágio que leva mais tempo. 4. Dadas duas arquiteturas implementadas com a mesma tecnologia, a arquitetura que é construída usando pipeline não reduz o tempo de execução de instruções, mas aumenta a freqüência de execução de instruções (throughput). 39 A execução que ocorre em cada um dos estágios é descrita a seguir: 1) Busca de Instrução: O contador de programa é usado para buscar a próxima instrução. As instruções são usualmente armazenadas em uma memória cache que é lida durante o estágio de busca. 2) Decodificação de Instruções e Busca de Operandos: O código da instrução é buscado, os campos são analisados e os sinais de controle são gerados. Os campos das instruções referentes aos registradores são usados para ler os operandos no banco de registradores. 3) Execução de Instruções: A operação especificada pelo código de operação é executada. Para uma instrução de acesso à memória, o endereço efetivo da memória é calculado. 4) Acesso à Memória: Dados são carregados da memória ou escritos na memória. Uma cache de dados é tipicamente utilizada. 5) Escrita de Resultados: O resultado da operação é escrito de volta no banco de registradores. IF ID EX ME WB → → → → → Busca Instrução Decodifica Instrução Executa Instrução Acessa Memória Escreve nos Registradores Fig. 4.1. Estrutura de uma máquina pipeline. Cada um dos retângulos mostrados na fig. 4.1 representa um banco de “flip-flops” que são elementos de memória utilizados para armazenar os resultados no final de cada estágio do pipeline. Os pipelines utilizados em microprocessadores são síncronos, portanto um sinal de relógio não mostrado na fig. 4.1 habilita o elemento de memória a “passar” seus resultados para o estágio seguinte. Como existe um único relógio para comandar o pipeline, o tempo gasto em todos os estágios do pipeline é idêntico e não pode ser menor o que o tempo gasto no estágio mais lento. Este tempo gasto em cada estágio é o período do clock utilizado para comandar o pipeline e ele determina a velocidade de execução das instruções. A latência de um pipeline é o tempo necessário para uma instrução atravessar todo o pipeline. Portanto para calcular o tempo de latência de uma máquina com pipeline basta multiplicar o período do clock pelo número de estágios no pipeline. A latência é importante apenas para se determinar quanto tempo a execução da primeira instrução leva para ser completada. Ciclo de Clock Instrução 1 Instrução 2 Instrução 3 Instrução 4 0 IF 1 ID IF Estágio do pipeline onde a instrução se encontra 2 3 4 5 6 EX ME WB ID EX ME WB IF ID EX ME WB IF ID EX ME Fig. 4.2. Execução de uma seqüência de instruções num pipeline. 40 7 WB 4.2. Geração de Bolhas no Pipeline Uma "bolha" em um pipeline consiste em uma seqüência de um ou mais períodos de clock em que um estágio do pipeline está vazio. Se um estágio do pipeline estiver vazio no ciclo de clock n, consequentemente o estágio seguinte estará vazio no ciclo de clock n+1. Desta forma bolhas formadas na entrada de um pipeline propagam-se através do pipeline até desaparecerem no último estágio. Situações que geram bolhas em pipelines incluem: 1) a execução de instruções de desvio, 2) o atendimento de interrupções, 3) o tratamento de exceções, 4) o retardo na execução de instruções devido a dependências existentes com instruções que a precedem. No caso de atendimento de exceções e interrupções não existem muitas técnicas efetivas para minorar o problema da formação de bolhas no pipeline pois estas ocorrências são bastante imprevisíveis. No caso de execução de desvios condicionais a formação de bolhas pode ser reduzida através da utilização de predição de ocorrência e desvio. No caso de dependências, a formação de bolhas pode ser minorada através do reordenamento de instruções. Vamos examinar estas técnicas nas próximas seções. 4.3 Previsão de Desvios O problema introduzido por desvios condicionais em arquiteturas organizadas em pipeline é que quando o desvio é decodificado na unidade de decodificação de instruções é impossível decidir se o desvio será executado ou não. Isto é, não pode-se determinar a priori qual a próxima instrução a ser executada. Existem duas possibilidades: a) a condição que determina o desvio é falsa e o desvio não é executado, neste caso a próxima instrução a ser executada é a instrução seguinte à instrução de desvio no programa; b) condição é verdadeira e o endereço da próxima instrução a ser executada é determinada pela instrução de desvio. Como determinar qual a próxima instrução a ser buscada quando uma instrução de desvio condicional é decodificada? A forma mais simples e menos eficaz de tratar um desvio condicional consiste em simplesmente paralizar a busca de instruções quando uma instrução de desvio condicional é decodificada. Com esta forma de tratamento de desvio garantimos que todo desvio condicional gerará uma bolha no pipeline pois a busca de instrução ficará paralizada até que se possa decidir se o desvio será executado ou não. Esta técnica só deve ser utilizada quando o custo de buscar e depois ter que descartar a instrução errada é muito alto. Uma outra forma é tentar prever o que vai acontecer com o desvio. Neste caso a previsão pode ser estática ou dinâmica. A previsão estática é aquela em que dada uma instrução de desvio em um programa nós vamos sempre fazer a mesma previsão para aquela instrução. Observe que podemos fazer previsões diferentes para diferentes instruções de 41 desvio no mesmo programa. Na previsão dinâmica podemos mudar a previsão para uma mesma instrução de branch à medida que o programa é executado. 4.3.1 Previsão Estática A forma mais simples de previsão estática é aquela em que a mesma previsão é feita para um dado desvio condicional. Esta técnica de previsão é simples de implementar e pode tomar duas formas: • Os desvios condicionais nunca ocorrem: Assumindo que os desvios condicionais nunca ocorrem, simplesmente se continua o processamento normal de instruções incrementando o PC e buscando a instrução seguinte. Se for determinado mais tarde que o programa deve desviar as instruções buscadas terão que ser descartadas e os efeitos de quaisquer operações realizadas por elas devem ser anulados. • Os desvios condicionais sempre ocorrem: Assumindo que os desvios condicionais sempre ocorrem é necessário calcular o endereço de desvio muito rapidamente já na Unidade de Decodificação de Instrução para dar tempo de buscar a nova instrução no endereço especificado pela instrução de desvio. Algumas observações feitas por projetistas de computadores após analisar diversos programas indicam que um grande número de desvios ocorre no final de laços de programa. Se um laço de programa for executado n vezes, o desvio que se encontra no final do laço irá ocorrer n vezes e não ocorrer uma vez no final do laço. Alguns programadores também observaram que desvios condicionais originados por comandos IF em linguagens de ao nível tendem a não ocorrer. Portanto, existe evidências de que seria desejável possuir-se a capacidade de fazer previsão estática diferenciada para diferentes instruções de desvio em um mesmo programa (ex: beq sempre ocorre, bne nunca ocorre, etc...). Uma solução para esta situação consiste em adicionar um bit no código de instruções de desvio condicional para informar o hardware se aquele desvio será provavelmente executado ou provavelmente não executado. Este bit deverá ser especificado pelo compilador. Como a determinação de que a previsão será de execução ou não do desvio é feita em tempo de compilação, este método de previsão também é estático. 4.3.2 Previsão Dinâmica Na previsão dinâmica de desvios condicionais uma mesma instrução de desvio pode receber uma previsão de ser ou não executada em diferentes momentos do programa. Uma solução comum é criar uma tabela de desvios em hardware. Esta tabela pode ser gerenciada na forma de uma cache (Content Addressable Memory - CAM). A cada vez que uma instrução de desvio é executada ela é adicionada à tabela e um bit é setado ou resetado para indicar se o desvio foi executado ou não. Na tabela também é registrado o endereço para qual o desvio foi realizado. Desta forma na próxima vez que a mesma instrução de desvio for decodificada, esta tabela é consultada e a previsão feita é de que a mesma coisa (execução ou não execução do desvio) vai ocorrer de novo. Se a previsão for de execução do desvio o endereço no qual a nova instrução deve ser buscada já se encontra nesta tabela. 42 4.4 Processamento de Exceções A ocorrência de exceções em um computador é imprevisível e portanto numa máquina em pipeline uma exceção normalmente resulta na formação de bolhas. Uma complicação adicional é que como existem várias instruções em diferentes estágios de execução ao mesmo tempo, é difícil decidir qual foi a instrução que causou a geração da exceção. Uma solução para minorar este problema é categorizar as exceções de acordo com o estágio do pipeline em que elas ocorrem. Por exemplo uma instrução ilegal só pode ocorrer na unidade de decodificação de instruções (ID) e uma divisão por zero ou uma exceção devida a overflow só pode ocorrer na unidade de execução (EX). Algumas máquinas com arquitetura em pipeline são ditas ter exceções imprecisas. Nestas máquinas não é possível determinar qual a instrução que causou a exceção. Uma outra complicação nas arquiteturas em pipeline é o aparecimento de múltiplas exceções no mesmo ciclo de clock. Por exemplo uma instrução que causa erro aritmético pode ser seguida de uma instrução ilegal. Neste caso a unidade de execução (EX) gerará uma exceção por erro aritmético e a unidade de decodificação (ID) gerará uma exceção por instrução ilegal, ambas no mesmo ciclo de clock. A solução para este problema consiste em criar uma tabela de prioridade para o processamento de exceções. 4.5 Bolhas Causadas por Dependências Uma outra causa de formação de bolhas em arquiteturas com pipeline são as dependências existentes entre instruções num programa. Para ilustrar estre problema, vamos considerar o exemplo de pseudo programa assembler apresentado a seguir. Este pseudo código foi escrito com uma simbologia muito similar à simbologia utilizada pela Motorola para os processadores da família MC68000. O trecho de programa abaixo permuta os valores armazenados nas posições $800 e $1000 da memória. inst.: A B C D MOVE MOVE MOVE MOVE WB ME EX ID IF Período clock $800, D0; $1000, D1; D1, $800; D0, $1000; A 1 A B 2 copia o valor do endereço $800 no registrador D0 copia o valor do endereço $1000 no registrador D1 copia o valor do registrador D1 no endereço $800 copia o valor do registrador D0 no endereço $1000 A B C 3 A B C D 4 A B B C D 5 C D 6 C D 7 C D 8 C D D 9 10 Fig. 4.3. Formação de bolha devido à dependência entre instruções. Observe na fig. 4.3 que no período de clock 6 quando a instrução C deveria executar no estágio de escrita na memória (ME) para escrever o valor de D1 no endereço $800, o valor lido do endereço $1000 ainda não está disponível em D1 pois ele só será escrito quando a instrução B tiver executado no estágio de escrita nos registradores (WB), o que ocorre 43 também no período de clock 6. Portanto a execução da instrução C necessita ser atrasada por dois ciclos de relógio, gerando uma bolha no pipeline. Uma forma simples de resolver este problema consiste em reordenar as instruções no programa, conforme ilustrado a seguir. inst.: B A C D MOVE MOVE MOVE MOVE WB ME EX ID IF Período clock $1000, D1; $800, D0; D1, $800; D0, $1000; B A C 3 B A 2 B 1 copia o valor do endereço $1000 no registrador D1 copia o valor do endereço $800 no registrador D0 copia o valor do registrador D1 no endereço $800 copia o valor do registrador D0 no endereço $1000 B A B A C D 4 A C D C D 5 6 C D 7 C D D 8 9 10 Fig. 4.4. Minimização de bolhas por reordenamento de instruções. A fig. 4.4 ilustra que um simples reordenamento de instruções é suficiente neste caso para minimizar a bolha gerada pela dependência. Observe que o tempo total para a execução da seqüência de instruções foi reduzido de 1 ciclo de clock. Existem situações em que um simples reordenamento de instruções não é suficiente para minimizar (ou eliminar) bolhas causadas por dependências. Considere o programa abaixo que realiza a inversão de uma tabela de palavras localizada entre os endereços $1000 e $9000 da memória. A B C D E F G H I J K L LOOP MOVEA #$1000, A0; inicializa A0 MOVEA #$9000, A1; inicializa A1 MOVE (A0), D0; copia em D0 dado apontado por AO MOVE (A1), D1; copia em D1 dado apontado por A1 MOVE D0, (A1); copia valor em D0 no end. indicado em A1 MOVE D1, (A0); copia valor em D1 no end. indicado em A0 ADDQ #2, A0; incrementa o valor de A0 SUBQ #2, A1; decrementa o valor de A1 CMPA A0, A1; compara os endereços em A0 e A1 BGT LOOP; enquanto A1 é maior que A0, continua MOVE #$500, A0; inicia outro procedimento MOVE #$17000, A1; inicia outro procedimento WB ME EX ID IF Clk A 1 A B 2 A B C 3 B C D 4 C D E 5 C D E F 6 C D E F 7 D E F G H 9 E F G 8 F G H I 10 G H I J 11 G H H I J 12 I J 13 Fig. 4.5. Execução do programa com loop. 44 I J K 14 J K L 15 - C 16 - C D 17 Conforme ilustrado na fig. 4.5, o laço do programa que efetivamente transfere os dados para fazer a inversão da tabela de palavras necessita de 12 ciclos de clock para executar. Para inverter uma tabela com n palavras é necessário executar este laço n/2 vezes. Portanto o número de ciclos de clock necessário para realizar a inversão da tabela é 6n (supondo que cada instrução é executada em apenas 1 ciclo de clock). A fig. 4.5 indica a formação de 3 bolhas dentro da execução do loop: 1) a primeira bolha aparece no ciclo de clock 7 quando a instrução E tem que esperar até o fim do WB da instrução C. Em outras palavras, ela tem que aguardar até que o novo valor do registrador D0 produzido pela instrução C seja escrito no estágio de escrita em registradores WB, o que só vai ocorrer no ciclo de clock 6. Esta bolha tem o tamanho de 1 ciclo de clock. 2) a segunda bolha, com tamanho de 2 ciclos de clock, aparece no ciclo de clock 12 quando a instrução I tem que esperar até o fim do WB da instrução H. 3) a terceira bolha aparece no final do loop devido ao uso de uma previsão de que o desvio não será executado, o que faz com que o processador busque as instruções K e L que não serão executadas. Quando é determinado que estas instruções não serão executadas, elas são eliminadas do pipe, gerando uma bolha de dois ciclos de clock. O programa foi reescrito para que fosse eliminada a dependência entre as instruções de atualização dos endereços em A0 e A1 e a instrução de comparação. Para que isto ocorresse a atualização dos valores dos registradores foi feita no início do laço e a inicialização de A0 e A1 foi alterada apropriadamente. O novo programa é apresentado a seguir. A B C D E F G H I J K L LOOP MOVEA #$0FFE, A0; inicializa A0 MOVEA #$9002, A1; inicializa A1 ADDQ #2, A0; incrementa o valor de A0 SUBQ #2, A1; decrementa o valor de A1 MOVE (A0), D0; copia em D0 dado apontado por AO MOVE (A1), D1; copia em D1 dado apontado por A1 MOVE D0, (A1); copia valor em D0 no end. indicado em A1 MOVE D1, (A0); copia valor em D1 no end. indicado em A0 CMPA A0, A1; compara os endereços em A0 e A1 BGT LOOP; enquanto A1 é maior que A0, continua MOVE #$500, A0; inicia outro procedimento MOVE #$17000, A1; inicia outro procedimento Para eliminar a bolha no final do loop, as instruções foram reordenadas dentro do laço e a previsão estática do desvio foi alterada para uma previsão de que o desvio sempre ocorre. Assim obtemos a execução mostrada na fig. 4.6. WB ME EX ID IF Clk A 1 A B 2 A B C 3 B C D 4 C D E 5 C D E F 6 C D E F 7 D E F G H 9 E F G 8 E F G H 10 F G H I 11 G H I J 12 Fig. 4.6. Eliminação de bolhas dentro do loop. 45 H I J C 13 J C D 14 C D E 15 C D E F 16 C D E F 17 Conforme mostrado na fig. 4.6, a modificação da previsão estática de desvio para desvio executado e a eliminação de bolha por reordenação de instruções dentro do laço causaram uma redução de 30 no número de ciclos de clock necessários para executar o laço. Com um laço de 8 ciclos de clock, uma tabela com n palavras pode ser invertida em 4n ciclos de clock. Observe que a instrução C não faz nada e foi inserida apenas como uma reserva de espaço de tempo para que o laço pudesse começar ordenadamente. A inserção da instrução C é necessária por causa da dependência existente entre a instrução B e a instrução D. 4.6. Máquinas Superpipeline, Superescalar O período de clock de uma máquina pipeline é determinado pelo estágio que leva maior tempo para ser executado. Uma técnica utilizada para acelerar uma máquina pipeline é subdividir os estágios mais lentos em subestágios de menor duração. Uma máquina com um número de estágios maior do que cinco é chamada de superpipeline. A fig. 4.7 ilustra uma organização superpipeline obtida pela divisão do estágio de busca de instruções (IF) em dois subestágios e do estágio de acesso à memória em três subestágios. IF IF IF ID IF EX ID ME EX ME ME ME ME WB ME WB Fig. 4.7. Arquitetura Superpipeline. Uma outra técnica para acelerar o processamento em máquinas pipeline é a utilização de múltiplas unidades funcionais que podem operar concorrentemente. Numa arquitetura deste tipo múltiplas instruções podem ser executadas no mesmo ciclo de clock. É necessário realizar análise de dependências para determinar se as instruções começadas ao mesmo tempo não possuem interdependências. IF IF ID ID EX EX ME ME WB WB Fig. 4.8. Arquitetura Superescalar. A fig. 4.8. ilustra uma arquitetura superescalar com dois pipelines que podem operar em paralelo. Uma preocupação que surge com a implementação de máquinas superescalar é a possibilidade de término de execução fora de ordem. Isto ocorreria quando suas instruções são iniciadas ao mesmo tempo em dois pipelines e uma bolha surge em um deles causando que uma instrução leve mais tempo do que a outra. Uma solução adotada para este problema consiste em criar um buffer de reordenação que armazena o resultado das instruções até que os resultados possam ser escritos na ordem correta. A maioria dos processadores RISC são arquiteturas superescalar e superpipelined. Isto é eles possuem mais de cinco estágios no pipeline devido à subdivisão de alguns estágios e eles possuem múltiplas unidades funcionais em cada estágio. Uma configuração típica é o uso de quatro unidades funcionais em cada estágio. 46