Esse exercício vai ser dividido em partes e, ao final delas, vocês devem ser capazes de:
Atenção:
Todos os arquivos que você precisa para essa atividade estão
disponíveis em /home/staff/rodolfo/mc723/download.
Em cada parte, leita todo o enunciado uma vez antes de executar suas atividades.
Utilizando como base a plataforma do exercício 3 (parte 1), você deve incluir um roteador entre o processador e a memória. O código deste roteador deve ser colocado na pasta is da plataforma. Utilize um dos programas do exercício 3.
Veja um pouco mais de detalhes a seguir, sobre a plataforma, antes das recomendações para seu código.
Todo o código da plataforma está separado em componentes, como você
já deve ter notado. Cada um dos componentes fica num diretório isolado
e eles são agrupados em diretórios segundo sua classificação. Esta
organização por diretórios é apenas para facilitar a localização e
também o entendimento, não é necessária para a montagem da plataforma
(muito embora vocês devem segui-la sempre).
Sua primeira plataforma possuia apenas dois módulos: um processador
e uma memória. Agora é hora de incluir um terceiro módulo, o roteador,
que fará o papel de barramento do sistema. Ele está sendo chamado de
roteador pois não implementará as funcionalidades de contenção de
tráfego que o barramento, nem algumas outras funcionalidades
interessantes. Então vocês trabalharão com algo simplificado neste
momento.
A comunicação entre o processador e a memória se faz através do
padrão TLM que, basicamente, realiza a ligação entre os componentes do
sistema. Só que o padrão TLM é ponto a ponto, exigindo uma porta de
cada lado para cada canal de comunicação. Além disto, o TLM inclui o
conceito de mestre e escravo, onde o mestre sempre faz as solicitações
e o escravo apenas as atende. No caso original, o processador é o
mestre e a memória é escravo. O grande problema desta configuração
inicial é a dificuldade em incluir um novo periférico pois será
necessário modificar o processador para que ele se comunique com dois
dispositivos diferentes. Assim, a solução é inluir um periférico no
meio do caminho que apenas fará o papel de roteamento, este é o
roteador que você vai fazer.
Do ponto de vista do TLM, as conexões são feitas sempre entre
portas, o processador tem uma porta Mestre e a memória tem uma porta
Escravo. No programa principal (main.cpp da plataforma), você vê a
ligação entre o processador e a memória através da linha:
mips1_proc1.DM_port(mem.target_export);
esta linha liga a porta DM_port do processador à porta target_export da memória. Olhando um pouco antes neste código, você pode ver a declaração do processador mips1_proc1 e da memória mem.
Sua tarefa é criar um novo componente, chamado roteador, que será
conectado a mips1_proc1 e também à mem. Para isto, ele precisará ser
Escravo na sua conexão com o processador e Mestre na sua conexão com a
memória. Além disto, todas as solicitações do processador devem ser transferidas para a memória neste momento.
A forma mais simples de implementar este roteador é começar pelo
código da memória, removendo a parte relacionada ao armazenamento de
dados e incluindo a parte da conexão Mestre que você poderá seguir o
exemplo do código do processador (procure pela declaração da classe mips1). você trabalhará sempre ao redor e na
implementação do método transport.
É esperado, nesta atividade, que
você localize e monte o código correto do seu roteador. Todos os
exemplos de código fonte já estão dados e você já utilizou recentemente
(exercício 3).
Meça o slowdown factor e
coloque no seu relatório. Compare com o do exercicio 3. Como você
colocou mais um componente no meio do caminho, você deve ter notado um
aumento deste número. Anote também o número de instruções por segundo
que o simulador consegue executar.
Uma visão geral da plataforma até agora: 3 componentes (processador,
roteador e memória) que são códigos executados no seu computador (x86)
e um programa que foi compilador para MIPS e está sendo executado
(simulado) dentro da plataforma.
O próximo passo é criar um novo periférico para incluir na
plataforma. Este periférico terá o código bem similar à memória, exceto
pela parte do armazenamento, e será usado fortemente no seu trabalho. A
especificação é bem simples: ele só armazena um valor de 32 bits
(inicialmente zerado), toda leitura retorna o valor armazenado e muda o
valor para 1. Toda escrita grava o valor solicitado. O funcionamento
deste periférico permite simular uma instrução de load and increment,
cuja principal finalidade é o controle de concorrência. Veja o exemplo:
suponha dois processadores executando exatamente o mesmo código
praticamente ao mesmo tempo:
volatile int *lock = (int *) ENDERECO_LOCK;
// Aguarda que o valor seja 0
while (*lock);
// Executa algo na região crítica
...
// Libera a região crítica
*lock = 0;
Um dos processadores chegará ao while primeiro e lerá o valor 0,
passando diretamente para a região crítica. Neste momento, ele poderá
executar o código que quiser sem que o outro processador atrapalhe.
Enquanto isto, o outro processador estará executando o while (lembre-se
que a primeira leitura do periférico retornará 0 e mudará o valor para
1). Após o primeiro processador encerrar a região crítica, ele pode
liberar o lock, permitindo que o segundo entre na região crítica. Isto
vale para quntos processadores forem necessários. Você pode, inclusive,
definir uma macro AquireLock com o while e outra ReleaseLock com a
última linha do exemplo.
Para incluir o periférico, inicie do código da memória, implemente o
novo periférico, inclua uma nova porta mestre no roteador e altere o
código do roteador para enviar leituras ao endereço do Lock para o Lock
e as demais para a memória. Qual o endereço desejado do Lock? Olhando o
código fonte da plataforma e do MIPS, nota-se que eles utilizam 5Mb de
memória, então você pode utilizar a próxima palavra como endereço base.
Faça um programa que leia várias vezes do periférico e mostre na
tela os valores lidos. Numa atividade futura você terá uma plataforma
multicore e poderá testar o código como o acima. Meça novamente o slowdown factor e o número de instruções por segundo.
A forma mais simples de indicar o endereço de um periférico é
através do roteador, que deve checar pelo valor do campo endereço e
enviar para o periférico. O grande problema aqui é que cada periférico
pode ter vários endereços alocados a ele. Veja o exemplo da memória que
tem 5M endereços. Sempre que um periférico tem mais de um endereço, o
seu endereço interno é sempre considerado como começando em zero.
Assim, na atividade da parte anterior, o endereço 5M deve ser visto
como endereço base e enviado como 0 para o Lock. Implemente isto na sua
plataforma.
Trabalhar com endereços base diferentes e manter os endereços
internos sempre iniciando em zero permite que você mova um periférico
de um endereço para outro sem ter que se preocupar com mais nada, pois
tudo continuará funcionando.
Meça novamente o slowdown factor e o número de instruções por segundo.
Agora é hora de utilizar um periférico para acelerar o desempenho da
plataforma. Antes vamos definir um programa como métrica. Implemente um
programa que realiza uma multiplicação de matrizes. Para facilitar,
utilize matrizes quadradas, geradas com a seguinte propriedade: a[i,j]
= i+j.
A primeira versão do seu programa deve trabalhar com números
inteiros, defina um tamanho de matriz para que o código execute em
torno de 5s. Agora transforme sua matriz de inteiro para double. Em
quanto tempo seu programa executou? Esta diferença de tempo se deve ao
fato do simulador utilizar uma biblioteca de emulação de números de
ponto flutuante, não tendo uma implementação nativa. Por isto, agora
você deve criar 1 periférico que realize a multiplicação de dois
números do tipo double. Veja o que você precisa:
Meça o tempo agora novamente e veja se seu periférico está mais rápido.
Crie um novo periférico com a funcionalidade de soma de dois números double e veja o quanto seu desempenho melhora.