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