Arquitectura de Computadores
Curso: LMCC
Exame 1ª Chamada
99/02/01
Duração: 2h

 

Componente teórica ; Componente prática


Componente Teórica

1. Considere a instrução HLL que efectua uma subtracção de dois valores do tipo inteiro, dentro de um procedimento folha - i.e., que não invoca outro - para execução num computador com CPU MIPS.

a) Considere que os 3 valores referidos nessa instrução (um de destino, 2 de fonte) são variáveis escalares do tipo inteiro, sendo uma das variáveis fonte o 2º parâmetro recebido pelo procedimento, e as restantes variáveis locais ao procedimento. Calcule (e apresente os cálculos) o espaço que esta mesma instrução ocupa em memória (medido em células de memória do MIPS), considerando que ela está: (i) em HLL; (ii) em assembly; (iii) em linguagem máquina.

Resolução

b) Comente as seguintes afirmações:

(i) "Para executar um programa escrito em HLL basta usar um compilador com assembler (para transformar um ficheiro de texto num executável), que ele fica logo em memória pronto a ser executado."

Resolução

(ii) "A arquitectura do MIPS é mais poderosa que a dos processadores da Intel porque suporta um instruction set enriquecido com muitas pseudo-instruções".

Resolução

c) Considere agora que o outro valor especificado como fonte é uma constante. Caracterize a arquitectura do MIPS (e respectivo instruction set) relativamente à possível localização das varáveis especificadas nesta instrução e à sua dimensão.

Resolução

d) Na continuação da alínea anterior, considere agora que a constante tem o valor -18 e que a variável destino não é escalar, mas sim o 3º campo de um record local, cujo apontador está num registo "seguro", e sendo o record declarado no procedimento após a declaração de 3 variáveis do tipo inteiro (o 1º campo do record é constituído por uma string de 8 caracteres, e o 2º campo um real de precisão simples).

Utilizando a convenção de utilização de registos, escreva o código que executa essa instrução HLL em assembly do MIPS (sem pseudo-instruções).

Resolução

e) Considerando os mesmos pressupostos que a alínea anterior e, utilizando a sintaxe do MIPS, rescreva o código que executa essa instrução HLL em assembly duma outra arquitectura com poucos registos (em que as variáveis estão normalmente armazenadas em memória) e em que só é possível especificar 2 operandos numa operação aritmética (estando um deles obrigatoriamente num registo genérico, e podendo o outro estar em memória).

Os modos de endereçamento permitidos são (R + R) e (R + deslocamento_16_bits). O endereço das variáveis fonte e destino em memória é, respectivamente, ($fp) + 8 , e Mem [($fp) - 16] + 10.

Resolução

2. Vários engenheiros foram consultados para apresentarem sugestões para a melhoria do desempenho dum dado computador. Sabe-se que o tempo de CPU necessário para a execução de um dado programa pode ser relacionado com vários factores, de acordo com a seguinte equação:

CPUtime = N.º Instr * CPI * período do clock

Comente as seguintes afirmações desses engenheiros:

a) "Vamos enriquecer o instruction set, aproximando-o duma HLL; assim consegue-se diminuir o n.º de instruções gerado pelo compilador, melhorando sempre o desempenho do computador".

Resolução

b) "Vamos diminuir a latência da sua cache no acesso à memória principal (reduz o CPI), aumentando o n.º de bancos de memória ligados aos barramentos e colocando-os em paralelo".

Resolução

 

 


Resolução da Componente Teórica
Alberto José Proença

 

1. a) Resolução

Aparentemente este exercício não parece oferecer qq dificuldade! Será que é mesmo verdade? De acordo com o enunciado, estas 5 alíneas referem-se todas a uma simples subtracção de 2 inteiros? Tanto tempo gasto nas aulas práticas com exercícios complexos, e agora no exame sai uma pergunta tão simples?

Deveria ser este o vosso espírito quando confrontados com esta questão. E com optimismo deveriam continuar...

O enunciado da 1ª alínea dá-nos alguma informação adicional, nomeadamente quanto á localização das variáveis nele presentes: "sendo uma das variáveis fonte o 2º parâmetro recebido pelo procedimento" e quanto à outras variáveis, elas são "variáveis locais ao procedimento".
Ora, no caso do MIPS, isto sugere-nos logo que os 3 operandos a especificar na operação de subtracção deverão estar, em princípio, em registos.
E se já estão em registos, então deve ser possível implementar essa operação de subtracção usando apenas uma instrução em linguagem máquina do MIPS, com formato R. Óptimo!

Que nos pede esta alínea? Que calculemos o espaço que a instrução ocupa em memória... Fácil!

(i) "em HLL": se ela estiver redigida da maneira "a = b - c", então ela é constituída por 5 caracteres visíveis ("a", "=", "b", "-", e "c") e 4 espaços entre eles (puro texto, não se esqueçam!), codificados portanto por 9 caracteres ASCII; se cada um tem a dimensão de 8 bits, e se cada célula de memória do MIPS tb tem a dimensão de 8 bits, então cada caracter ocupa uma célula de memória; logo, a resposta de (i) é 9 células de memória.

(ii) "em assembly": esta operação em assembly, de acordo com os comentários feitos anteriormente, pode ser implementada usando apenas uma instrução, do tipo "sub $t0, $a0, $t1"; mas... (atenção que é aqui que os vossos colegas falham quase sempre!) uma instrução em assembly continua a ser puro texto! Ou seja, esta instrução é constituída por 14 caracteres visíveis (se não me enganei a contá-los) mais 3 espaços, num total de 17 caracteres que serão codificados para ASCII, ocupando assim 17 células de memória.

(iii) "em linguagem máquina": uma instrução em assembly (e que não é pseudo-instrução!) corresponde a uma e só uma instrução em linguagem máquina; no caso do MIPS (e da maioria das arquitecuras RISC) todas as instruções em linguagem máquina tem sempre a mesma dimensão, que é de 32 bits (no caso do MIPS a sua codificação obedece a 3 formatos, R, I e J); ora 32 bits ocupam 4 células de memória.

Regressar ao enunciado


1. b) (i) Resolução

O enunciado pede para comentar as afirmações de 2 sub-alíneas. Cada uma delas conterá, provavelmente mais que uma afirmação, pelo que o primeiro passo será identificar quais são as afirmaçãoes que se fazem, para depois as comentar.

Neste caso, 2 afirmações são facilmente identificadas (já as viu?): "basta usar" e "fica logo". Vamos a elas:

Regressar ao enunciado


1. b) (ii) Resolução

Neste segundo caso, apenas 1 afirmação é feita, "é mais poderosa ... porque...":

Regressar ao enunciado


1. c) Resolução

De acordo agora com esta alínea, os operandos, do tipo inteiro (escalar, não-estruturado), são então: um (fonte) é um parâmetro, o outro (fonte) é uma constante, e o outro (destino) é uma variável local.
Pretende-se que se caracterize a arquitectura (e respectivo instruction set) relativamente a "localização das varáveis especificadas nesta instrução" e "sua dimensão".

Por outras palavras: onde é que o MIPS pode colocar estes operandos, e qual a sua dimensão.

Comecemos pela dimensão: estamos a falar de inteiros; vimos nas aulas teóricas e práticas como é que estes valores podem ser representados (não se esqueçam que a classe dos inteiros inclui os valores negativos e os positivos), com especial destaque para a representação dos valores negativos (em complemento para 2 em todas as arquitecturas actuais), e chamando a atenção para o facto de que, embora a utilização de um nº pequeno de bits para representar os inteiros poderia trazer economia nos requesitos de memória, essa solução poderia ser inaceitável para a maioria das aplicações (por ex., com 16 bits não consigo representar o nº 50 000!).
Assim, todas as arquitecturas RISC actuais trabalham com registos (onde se encontram as variáveis na altura das operações aritméticas/lógicas) e unidades aritméticas de 32 ou 64 bits, para permitirem a representação e operação (rápida) de inteiros com 32 ou 64 bits.
E o caso das constantes que vem especificadas na própria instrução? Com um formato de instrução de 32 bits, não é possível incluir lá tb um valor numérico de 32 bits. No caso do MIPS, este apenas suporta um formato de instrução em que é possível especificar uma constante (formato I), e essa constante não pode ter mais 16 bits, i.e., está em complemento para 2, e pertence ao intervalo [-32k , +32k[ .

Quanto à sua colocação: na altura da execução da operação aritmética, os operandos têm de estar em registo (regra comum a todas as arquitecturas RISC): se forem variáveis, estarão num dos 32 registos genéricos do processador; se for uma constante, estará no registo que contém a instrução, uma vez que o valor da constante vem especificado no próprio formato da instrução.
Se os operandos não estiverem em registos - em princípio as variáveis escalares são alocadas a registos, mas se não houver registos que cheguem, as variáveis terão de ser alocadas em posições de memória - então é preciso transferi-las primeiro da memória para os registos.
A arquitectura do MIPS ao nível do seu instruction set possui instruções para este tipo de tranasferência de informação, mas o MIPS apenas dispõe de um modo de especificar um endereço de memória: fornecendo no formato da instrução a referência a um registo, e um valor numérico de 16-bits; o endereço é calculado adicionando o conteúdo desse registo ao valor numérico de 16-bits (com um valor em complemento para 2).

Regressar ao enunciado


1. d) Resolução

Relembrando o enunciado, o que se estava a analisar era a operação de subtracção de inteiros " a = b - c ".
Já se sabe agora que "a" é "o 3º campo de um record local, cujo apontador está num registo seguro", que "b" (ou "c") é um parâmetro recebido pelo procedimento, e que "c" (ou "b") é a constante com o valor -18.
Analisando melhor o enunciado, há os seguintes aspectos a comentar:

E pronto: parece que já temos todos os dados para resolver o pb, e todas as sugestões para lá chegarem sem dificuldade! Queriam mais simples? Uma resposta com apenas 2 instruções, um addi seguido de um sw?

Regressar ao enunciado


1. e) Resolução

Uff! Não é preciso estudar outra vez cuidadosamente o enunciado, uma vez que se diz "Considerando os mesmos pressupostos"; e além disso, posso tb usar uma sintaxe familiar, que é a do MIPS. Por outro lado, tenho que ver em que é que esta "outra arquitectura" difere da do MIPS, para ver se consigo perceber onde tenho de mudar o raciocínio, relativamente à execução da alínea anterior.

Quatro grandes diferenças são apontadas no enunciado (já as detectou? Pense um bocado antes de avançar, pois no dia do exame não vai ter esta ajuda...). São elas:

  1. "as variáveis estão normalmente armazenadas em memória"; não tenho registos que cheguem para alocar variáveis locais nem para passar parâmetros; isto faz-me lembrar a arquitectura do i86, conforme analisado na aula e documentado em http://gec.di.uminho.pt/discip/TextoAC/cap6.html#i86; com uma diferença: o enunciado não diz que esta arquitectura apenas trabalha com inteiros de 16-bits (caso do i86) e que quando invoca uma sub-rotina guarda na stack o valor do PC (com o endereço de retorno da sub-rotina); se não diz, vou considerar que, relativamente a estes 2 aspectos, esta arquitectura é semelhante ao MIPS, i.e., inteiros são de 32-bits e existe um registo especial para o endereço de retorno;
  2. "só é possível especificar 2 operandos numa operação aritmética"; no MIPS posso sempre especificar 3; neste caso, e se vou usar a sintaxe do MIPS, isto vai obrigar a que, sempre que uma operação aritmética/lógica (tipo R) especifique 3 operandos no formato da instrução, 2 deles têm de ser iguais (um fonte e um destino);
  3. "podendo o outro estar em memória"; no MIPS nenhum operando pode estar em memória; aqui posso ter 1 operando em memória, e não mais que 1, pois o enunciado diz claramente "estando um deles obrigatoriamente num registo genérico"; como ele tem poucos registos, vou considerar que todos os que tem serão usados apenas para valores temporários, e vou chamá-los de $t;
  4. "Os modos de endereçamento permitidos são (R + R) e (R + deslocamento_16_bits)"; o MIPS apenas suporta o 2º modo de endereçamento aqui especificado; em termos de sintaxe, se precisar de usar o 1º modo representá-lo-ei na forma $x($y).

Quais são então as implicações destas diferenças?

A implicação maior é certamente a resultante do facto de as variáveis estarem agora todas em memória; conforme estudado no cap.6 (referido acima), as arquitecturas que não tenham registos suficientes para alocar as variáveis locias e os parâmetros, utilizam uma estrutura de dados na stack - para conter a informação pertinente às variáveis necessárias na execução de qq função ou procedimento - que é designada de activation record, normalmente constituída pelos seguintes campos (informação retirada das notas de estudo):

Se quiséssemos saber a localização de cada uma das variáveis, é possível calculá-las com alguns pressupostos. Neste exame tal não é necessário, uma vez que o enunciado diz que "o endereço das variáveis fonte e destino em memória é, respectivamente...". Se o seu endereço não fosse indicado, tb se poderia lá chegar, através da seguinte análise de cada campo do activation record:

Com base nesta informação (a estrutura do activation record) seria possível então saber a localização do operando fonte - 2º parâmetro, em +4($fp) - e do operando destino - 3º campo do record; o início do record está em -28($fp), estando o 3º campo 12 posições depois, em -16($fp).

Contudo, neste exame não se pede tanto, uma vez que se fornecem valores (fictícios) para a sua localização!
Embora a localização do operando destino tb não seja imediata: em -16($fp) está o apontador para uma posição de memória; ao conteúdo dessa posição de memória (que deverá ser um inteiro) ter-se-á de adicionar o valor +10 para se ter o apontador para o operando destino!
Quanto ao operando fonte, não parece existir dificuldade: ele encontra-se em +8($fp).

A 2ª maior implicação das diferenças identificadas acima diz respeito ao número de operandos permitido em cada operação. Isto significa que nossa operação " a = b - c ", que já tinha sido alterada para " a = b + (-c) " e para " a = b + 18 ", vai ter de ter uma implementação do tipo:

temp = b (uma operação de load para um registo que poderemos chamar de $t0)
temp = temp + 18 ( uma operação de addi)
a = temp (uma operação de store)

Atenção a quem se sentir tentado a poupar aqui uma instrução, substituindo as 2 primeiras por " b = b + 18 ", pois isso significa que vai alterar o valor da variável "b" sem estar na especificação do problema!

Resta-nos ver agora se as outras diferenças e se as localizações dos operandos permitem que a implementação final fique com apenas estas 3 instruções.

A primeira instrução é pacífica, pois estando localizada em em +8($fp) a instrução lw executa o pretendido, para $t0.
Idêntico comentário se aplica à 2ª instrução: é apenas um addi.
A terceira é mais complicada, tanto mais que o modo de endereçamento adicional disponibilizado por esta arquitectura não nos vai servir de nada. Esta operação ( a = temp ) tem de ser implementada por mais que uma instrução: primeiro é preciso ler da memória (para um outro registo temporário, por ex. $t1) o conteúdo da célula que tem o endereço -16($fp), e só depois se pode efectuar a operação de store (sw) do conteúdo de $t0 para a posição +10($t1).

Comentário final: esta alínea não era fácil; tinha como objectivo identificar os estudantes que dominavam melhor a matéria, i.e., que sabiam aplicar os conhecimentos adquiridos, para além de um simples despejar da informação memorizada na véspera.

 

Regressar ao enunciado

 


2. a) Resolução

Vou repetir a afirmação do engenheiro que devemos comentar:
"Vamos enriquecer o instruction set, aproximando-o duma HLL; assim consegue-se diminuir o n.º de instruções gerado pelo compilador, melhorando sempre o desempenho do computador."

Vamos analisar esta frase, para identificarmos quais as afirmações que ela contém e que precisam de ser comentadas. Tentem lá chegar antes de continuar a ler o texto.

Eis as afirmações nela presentes:

  1. "Vamos enriquecer o instruction set... assim consegue-se diminuir..." Será verdade o que ele diz? Faz sentido?
  2. "... assim consegue-se diminuir ... melhorando sempre o desempenho..." Melhora? E se melhora, melhora sempre?

Vamos aos comentários.

  1. Efectivamente, uma das formas de enriquecer o instruction set é aproximando-o duma HLL; agora se este enriquecimento vai "diminuir o n.º de instruções gerado pelo compilador" é o que precisamos de ver. Em princípio, quanto mais próximo estiver uma linguagem máquina duma HLL, menor será o nº de instruções geradas pelo compilador para executar as tarefas pretendidas com o código que se escreveu. Portanto, esta afirmação parece correcta e faz sentido.
  2. Mas será que melhora o desempenho? De acordo com a fórmula apresentada, que relaciona o tempo de execução de uma aplicação no CPU com diversos factores (3, mais concretamente), se se melhorar um desses factores (neste caso, o nº de instruções executadas) sem alterar os outros, não há dúvida que temos uma melhoria do desempenho. Mas se se enriquecer o instruction set, não vai afectar os outros factores?
    Um instruction set mais rico implica normalmente a existência de instruções mais complexas, e como tal, mais demoradas a executar; isto vai afectar (negativamente) o tempo médio de execução de cada instrução, incluído num outro factor da fórmula referida (o CPI). Se o que se ganhar com a redução do nº de instruções executadas não compensar o que se perder com o aumento do CPI, então pode não melhorar.
    Por outro lado, um instruction set mais rico pode implicar, não apenas a existência de instruções mais complexas, mas tb de um maior nº de instruções; o hardware para a descodificação da instrução poderá ser mais complexo, podendo ter como consequência uma maior dificuldade ao nível da microelectrónica em se manter uma frequência de clock tão elevada, i.e., poderá impôr um aumento do período do clock (que é outro factor na fórmula acima referida).
    Ou seja, muito provavelmente o enriquecimento do instruction set não irá melhorar o desempenho; e de certeza que afirmar "melhorando sempre o desempenho" não faz sentido.

 

Regressar ao enunciado

 


2. b) Resolução

Vou repetir a afirmação do engenheiro que devemos comentar:
"Vamos diminuir a latência da sua cache no acesso à memória principal (reduz o CPI), aumentando o n.º de bancos de memória ligados aos barramentos e colocando-os em paralelo".

Vamos analisar esta frase, para identificarmos quais as afirmações que ela contém e que precisam de ser comentadas. Tentem lá chegar antes de continuar a ler o texto.

Eis as afirmações nela presentes:

  1. "Vamos diminuir a latência da sua cache no acesso à memória... reduz o CPI..." Será verdade o que ele diz? Faz sentido?
  2. "Vamos diminuir a latência da sua cache no acesso à memória... aumentando o n.º de bancos de memória...". É assim que se diminui a latência?
  3. "Vamos diminuir a latência da sua cache no acesso à memória... colocando-os (aos bancos de memória) em paralelo". É assim que se diminui a latência?

Vamos aos comentários.

  1. O CPI é uma medida do tempo médio de execução de uma instrução (a unidade não é o segundo, mas sim o período de clock), o qual depende não apenas do tempo que a instrução leva ser executada no CPU, mas tb do tempo adicional que ela possa precisar para aceder à memória, quando para tal não é suficiente o que se previu para a sua execução no CPU.
    Esse tempo adicional depende essencialmente de 2 factores: (i) o nº de vezes que, em média, cada instrução tem de aceder à memória (que sabemos ser sempre superior a 1, pois para além de ser necessário ir buscar a própria instrução à memória, uma dada percentagem das instruções de um programa ainda tem de ir à memória durante a sua execução, como é o caso das instruções de load/store), e (ii) a penalização que ocorre por cada acesso.
    Esta penalização tem tb pelo menos 2 parcelas: (i) a penalização por aceder ao nível da hierarquia de memória em causa (neste caso, o acesso à própria cache), e (ii) a penalização por aceder ao nível inferior da hierarquia de memória (normalmente mais pesada que a anterior); este problema debruça-se sobre esta segunda parcela, tal como está escrito: "latência da sua cache no acesso à memória principal".
    Assim, se o CPI depende do tempo adicional para aceder à memória, se este tempo depende da penalização que ocorre por cada aceso, e se essa penalização contém a parcela referente ao acesso ao nível inferior da hierarquia de memória, é óbvio que se "diminuir a latência da sua cache no acesso à memória" se "reduz o CPI".
  2. A questão que se coloca agora é como diminuir então essa latência no acesso à memória principal? Para se melhorar o desempenho no acesso à memória, há 2 características a considerar: o tempo para se ir buscar um bloco de informação, e o nº de bytes de informação que podem ser transferidos por unidade de tempo. À 1º característica é que se designa de latência, enquanto a 2ª é mais conhecida por largura de banda. A essência das afirmações contidas nesta alínea têm a ver apenas com a latência.
    O aumento do nº de bancos de memória sem mais nada, não tem qq efeito. Por exemplo, o simples aumento da capacidade de cozinha das cantinas (nº de almoços confeccionados) não tem qq influência na redução do tempo que cada um leva a encher o seu tabuleiro, i.e., a latência de ser servido (assumindo que nunca se espera que a refeição seja confeccionada).
    Agora, se para além de se aumentar o nº de bancos de memória se faz algo mais... então vamos ver a afirmação seguinte.
  3. Aumentando "o n.º de bancos de memória ligados aos barramentos e colocando-os em paralelo" já parece fazer mais sentido. Poderia corresponder na nossa metáfora, a dizer que se criava mais um outro balcão para as pessoas se servirem. Mas isto vai reduzir a latência de ser servido? Ou vai apenas melhorar o nº de pessoas que são servidas por unidade de tempo (correspondente à largura de banda)? Mas tb poderia corresponder a uma disposição diferente dos diversos elementos a servir no mesmo balcão, de modo a que cada um se pudesse servir simultaneamente dos talheres, da sopa, do sumo, ...
    Por outras palavras, colocar mais bancos de memória em paralelo vai reduzir o tempo de acesso a cada bloco de informação? Bem, depende da dimensão do bloco... e da maneira como são colocados em paralelo: se no mesmo barramento, ou se se aumenta a largura do barramento.
    Se a dimensão inicial do barramento era já a do bloco de informação que é necessário transferir em cada transacção (neste caso, uma linha da cache), então a latência no acesso não ganha nada em se aumentar o nº de bancos e na sua colocação em paralelo.
    Se a dimensão inicial do barramento era inferior à da linha da cache (por ex, um barramento de dados de 32 bits e uma cache com linhas de 2 ou 4 palavras cada), então o colocar mais bancos de memória em paralelo pode melhorar se... (veja lá se descobre o que deve vir a seguir a este se...) o acesso a esses bancos não for feito sequencialmente, mas com alguma forma de paralelismo, i.e., com pipeline (interleaving) ou com aumento da largura do barramento de dados para a dimensão da linha da cache.

Um comentário final: não se assustem com esta proposta de resolução tão comprida, pois não é necessário escreverem tudo isto para terem 100%! Simplesmente entendi que os alunos mais fracos precisavam de tanto detalhe para uma melhor compreensão da matéria, enquanto que os melhores alunos gostariam que eu fosse até às últimas consequências na análise destes casos. Na correcção dos exames não sou tão exigente...

Regressar ao enunciado