Unidade Central de Processamento (CPU)
Nesse módulo vamos estudar a parte mais importante de um computador, que é a Unidade Central de Processamento, ou UCP, ou, do inglês, CPU. A CPU é responsável não apenas por executar os programas contidos na memória, mas também de controlar todos os dispositivos de entrada e saída. Seu avanço ao longo dos anos tem permitido que programas fossem executados cada vez mais rapidamente. Hoje temos processadores de vários núcleos capazes de executar várias atividades ao mesmo tempo. São esses processadores e funcionalidades que iremos estudar nesse módulo.
O que é um programa?
Nesse momento, você não apenas deve saber o que é um programa, como já deve ter até já escrito seus próprios programas e entendido um pouco como o computador funciona. Os programas são sequências finitas de passos que foram definidas por um programador para alcançar um objetivo específico. Cada passo desse programa é chamado de instrução. Não necessariamente, uma instrução escrito em uma linguagem de alto nível, como C, Java, Python, por exemplo, é diretamente transformada em uma instrução de máquina e armazenada em memória para execução da CPU. Na verdade, geralmente, uma instrução de uma linguagem de alto nível embute vários comandos e ações a serem executadas pela CPU. Essa é a principal razão da criação dessas linguagens de alto nível. O programador deve ter o menor trabalho possível ao escrever um programa. Ele deve se preocupar com o problema que está tentando solucionar, e em memorizar dezenas de comandos de uma linguagem de máquina extensa e repleta de detalhes.
Após compilado, o programa de linguagem de alto nível é transformado em um programa apenas com instruções de máquina. Cada instrução de máquina contém apenas uma única operação a ser realizada pela CPU. Para ser executado, esse programa deve ser transferido para a Memória Principal. No princípio, um Operador de Máquina, copiada todas instruções para a memória de maneira quase que manual. Hoje em dia essa operação é realizada pelo Sistema Operacional (Windows, Linux etc.). Assim que um usuário clica com o mouse, ou pressiona a tecla Enter do teclado solicitando que um determinado programa execute, o Sistema Operacional copia o programa para a memória e solicita que a CPU o execute.
Não podemos esquecer que a memória do computador apenas armazena números binários. Então, podemos dizer que um programa em linguagem de máquina é formado por instruções em binário. A cada instrução trazida da memória, a CPU lê seu código binário de operação para saber do que se trata, e inicia o processo de execução. Dependendo da operação, que pode ser de movimentação de dados, uma operação lógica, ou aritmética, ou uma operação de armazenamento de dados, a CPU envia ordens para que os outros dispositivos do computador atuem de forma a completar a operação. Essas ordens são enviadas através de pulsos elétricos passados por fios dentro do computador. Esses fios são chamados de Barramento de Controle.
Software X Hardware
O computador é composto por dois elementos, o software e o hardware. Tanto o hardware quando o software foram escritos por um programador, ou engenheiro, para se resolver um determinado problema. O início é sempre o mesmo. O profissional se depara com um problema e projeta uma solução algorítmica para ele. A diferença está na concepção. O hardware é concebido em chip, utilizando transistores interconectados. Uma vez elaborado, o hardware não pode mais ser modificado. Ele é uma solução rígida (do inglês, Hard) para o problema. Já o software é elaborado para ser armazenado numa memória e ser executado com um processador de propósito geral. Ele é uma solução flexível (do inglês, Soft) para o problema, já que o programador pode, a cada momento, modificar seu programa afim de torná-lo cada vez melhor.
Soluções em software são sempre mais lentas do que soluções equivalentes em hardware. Isso porque para executar um programa, cada instrução deve antes ser armazenada em memória, transferidas para a CPU (lembre-se que memórias são muito mais lentas do que CPUs) e, só então, ser executada pela CPU. Já as soluções em hardware não utilizam instruções, elas executam as operações diretamente.
Por outro lado, as soluções em software ganham em flexibilidade, já que os programas podem ser facilmente modificados. Já as soluções em hardware, não. Uma vez concebido, um hardware não pode mais ser modificado, ele deve ser descartado para dar lugar a uma versão mais nova. Isso torna projetos em hardware muito mais caros.
Para entender melhor, podemos citar alguns exemplos de implementações em hardware comumente utilizadas. Todas são escolhidas devido ao seu caráter de pouca necessidade de modificação, mas muito demanda por alto desempenho. Por exemplo, chips de criptografia para celulares (geralmente Smartphone), processadores aritméticos para acelerar o cálculos, aceleradores gráficos para gerar gráficos mais rápidos, alguns chips para fazer edições rápidas em fotos, geralmente acoplados às câmeras digitais. As implementações são feitas em software quando a demanda por desempenho não é tanta, ao mesmo tempo em que as atualizações são frequentes, como os Sistemas Operacionais, os jogos e aplicativos em geral.
Apesar de não ser tão rápida quanto gostaríamos, a CPU é uma solução muito boa por permitir a execução de, praticamente, qualquer tipo de programa, se tornando uma máquina de propósito geral.
Estrutura de uma CPU
Toda CPU é formada por duas unidades, como podem ser vistas na [Estrutura de uma CPU]:
- Unidade de Controle (UC)
- Unidade de Ciclo de Dados (UCD)
A Unidade de Controle é responsável por receber instruções pelo Barramento de Instruções. As instruções vêem da memória de acordo com o endereço enviado pela UC para a memória através do Barramento de Endereço das Instruções (à esquerda da UC na [Estrutura de uma CPU]). Já Unidade de Ciclo de Dados, como o próprio nome deixa entender, é responsável por tratar os dados propriamente ditos. A Unidade de Controle não executa as instruções. Ela as lê, decodifica e passa os comandos para a UCD determinando como as instruções devem ser executadas e com quais dados. Baseada nessas comandos, a UCD pode ir buscar os dados necessários na memória, executa as devidas operações e envia o resultado de volta para a memória para ser armazenado. Tudo controlado de acordo com os comandos internos enviados pela Unidade de Controle, que por sua vez se baseia na instrução decodificada. Os dados lidos, ou enviados para a memória, são transmitidos através do Barramento de Dados. Os endereços são enviados para a memória através do Barramento de Endereço.
Tudo isso é controlado por um sinal síncrono de relógio (clock, do inglês). A cada batida do relógio a unidade sabe que deve executar um passo, passar os dados para quem deve, e se preparar para o próximo passo. Quanto mais rápido é o relógio mais operações por segundo o processador consegue executar e mais rápido pode se tornar. A velocidade do relógio é medida em frequência, utilizando a unidade Hertz (abreviatura é Hz). Um Hertz significa um passo por segundo. Os processadores atuais trabalham na faixa dos poucos GHz (leia-se Giga Hertz), entre 1 GHz e 5 GHz. Um Giga Hertz significa um bilhão de passos por segundo. É por isso que os computadores são tão incríveis. Eles não executam operações extraordinárias. Pelo contrário. Executam operações extremamente simples, como somas, subtrações e multiplicações, mas fazem isso numa velocidade incrível.
Os papéis dos barramentos e da memória
Saindo um pouco de dentro da CPU, podemos enxergar os barramentos e a Memória Principal, como é apresentado na [Estrutura de uma CPU com barramentos]. Para facilitar a visualização, os Barramentos de Dados e de Endereço são apresentados replicados, tanto do lado esquerdo, quanto do direito da figura.
A comunicação da Unidade de Controle e da Unidade de Ciclo de Dados é feita sempre com a Memória Principal através dos barramentos. Os endereços são transmitidos sempre via Barramento de Endereços para a memória, sempre de forma unidirecional da CPU para a memória. Quando as instruções são transmitidas da memória para a Unidade de Controle, elas utilizam o Barramento de Dados. Isso porque as instruções são tratadas pela memória como um conteúdo como um outro qualquer. Ela não faz distinção entre dados e instruções. O mesmo Barramento de Dados é utilizado pela Unidade de Ciclo de Dados para receber os operando das operações a serem realizadas e para enviar os resultados de volta para a memória.
Fica claro então a importância da Memória Principal. Todo e qualquer programa só poderá ser executado a partir dela. Quando você, por exemplo, deseja executar um programa de um pendrive conectado pela USB do computador, ele antes precisa ser copiado para a Memória Principal. Só então ele será executado. A memória precisa ser grande o bastante para armazenar a maior quantidade possível de programas, e também precisa ser rápida o suficiente para buscar os dados e enviá-los o mais rapidamente possível à CPU, e também salvá-los no menor tempo possível. A velocidade das memórias é determinada essencialmente pela tecnologia de transistores utilizada. Essa tecnologia é relacionada ao preço. Quanto mais rápidas, mais caras elas são.
Os registradores
Os registradores são memórias elaboradas com o mínimo de transistores possível, utilizando o que há de mais moderno em tecnologia de armazenamento. Elas são as memórias mais rápidas que podem ser construídas e por isso são também as mais caras. Por essa razão, elas aparecem numa quantidade muito pequena em um computador, na casa de alguns KiloBytes. Eles podem ser divididos em dois grupos. Os registradores de propósito geral, e os de propósito específico. Como o próprio nome diz, os primeiros podem ser utilizados pelos programas para quaisquer objetivos, já os segundos são específicos para algumas tarefas. Por exemplo, há um registrador na CPU para controlar se o processador deve continuar em execução, ou entrar em modo de espera por nova ordem. Se esse registrador receber um valor diferente de zero, o processador entrará em modo de espera, até que receba a ordem de modificar esse valor. Na [Estrutura de uma CPU com registradores] os registradores de propósito específico apresentados são:
- Program Counter (PC): Contador de Programas
- Instruction Register (IR): Registrador de Instrução
- Memory Address Register (MAR): Registrador de Endereço
- Memory Buffer Register (MBR): Registrador de Dados
O PC contém o endereço de memória que será utilizado para buscar a próxima instrução a ser executada pela CPU. Antes de executar qualquer instrução, a CPU envia o conteúdo de PC para a memória através do Barramento de Endereço, a memória envia o conteúdo da memória nesse endereço através do Barramento de Dados. Esse conteúdo é então armazenado no IR. Já o IR, que recebeu a instrução que veio da memória, tem o objetivo de guardar a instrução e passá-la para a Unidade de Controle, que é quem vai lê-la e tomar as decisões necessárias para para que ela seja executada pela Unidade de Ciclo de Dados. Por se tratarem do processo de busca de instruções, o PC e o IR ficam instalados na Unidade de Controle. O PC possui conexão direta com o Barramento de Endereços, e o IR com o Barramento de Instruções.
Com relação ao MAR e ao MBR, eles possuem funções análogas ao PC e IR, respectivamente, mas referentes a dados e não a instruções. Quando uma operação precisa ser realizada com algum dado que está na memória (e não em um registrador), o endereço desse dado é passado para o MAR. A CPU então passa o conteúdo de MAR para a memória através do Barramento de Endereço, que retornará o conteúdo da memória nesse endereço através do Barramento de Dados. O conteúdo trazido pela memória será armazenado em MBR. Só então o dado poderá ser utilizado para o processamento inicialmente planejado. O MBR e MAR possuem, respectivamente, conexões diretas com os Barramentos de Dados e de Endereços. Ambos são situados na Unidade de Ciclo de Dados, por serem utilizados nas fases de processamento das instruções.
O tamanho e quantidade dos registradores de uma CPU é uma das principais decisões de projeto. Se forem grandes demais, ou em quantidade maior do que a necessária, podem resultar em desperdício e aumento desnecessário no preço do processador. Já se forem pequenos, ou em pouca quantidade, com certeza vão tornar o computador muito mais lento do que o desejado. Encontrar o tamanho e quantidade ideias é trabalhoso e geralmente é feito através de simuladores e de muito testes e anos de experiência.
Os registradores de propósito geral são utilizados para guardar as variáveis dos programas. Como eles estão presentes em quantidades muito pequenas, são poucas as variáveis que ficam armazenadas em registradores. As demais ficam na Memória Principal. Quando uma operação precisa ser realizada e seus dados estão nos Registradores de Propósito Geral, a CPU não precisa buscá-los na memória e o processamento torna-se muito mais rápido.
Nota – Lembre-se que as memórias são muito mais lentas do que os processadores!
A CPU tenta ao máximo manter as variáveis mais utilizadas nos registradores. Ela faz isso guardando aquelas mais usadas nas últimas operações. Nem sempre isso funciona, mas no geral, é a melhor solução.
Nota – Faça suas variáveis mais importantes serem bastante utilizadas. Usando-as em repetições, por exemplo. Isso aumentará as chances delas serem armazenadas em registradores, podendo acelerar a execução dos seus programas.
Unidade Lógica e Aritmética (ULA)
A Unidade Lógica e Aritmética, ou ULA, se assemelha muito com uma calculadora convencional. Ela executa operações lógicas e aritméticas. As ULAs modernas executam operações tanto com inteiros, como com números reais. A ULA recebe como entrada dois diferentes dados que são trazidos para ela dos registradores (de propósito geral, ou específicos) (veja a [Estrutura de uma CPU com registradores]). Quem decide que registradores passarão seus dados para a ULA é a Unidade de Controle baseada instrução que está sendo executada. A Unidade de Controle também envia para a ULA qual operação será realizada (soma, multiplicação, divisão, AND, OR etc.). Assim que isso é feito, a ULA executa a operação e gera um resultado na sua saída. Esse resultado também é passado para um registrador escolhido pela Unidade de Controle, baseando-se na instrução em execução.
Unidade de Controle (UC)
A Unidade de Controle, ao receber a instrução que está armazenada em IR, a decodifica e envia os sinais de controle para onde for necessário. Decodificar nada mais é do que ler um código em binário e interpretar a operação relativa a esse código. Dependendo da operação, os sinais de controle podem ser internos, por exemplo, para a ULA executar uma soma, ou para o conteúdo de um registrador ser transferido para a ULA. Ou pode ser externo, para um dispositivo de entrada e saída, por exemplo, ou mesmo para a Memória Principal. Tudo isso depende da instrução a ser executada.
Na próxima seção será apresentada a execução de instruções em mais detalhes, o que facilitará o entendimento do funcionamento das CPUs.
Ciclo de Instrução
Toda CPU trabalha em dois ciclos principais, o Ciclo de Busca e o Ciclo de Execução, como pode ser visto na [Ciclo de Instrução]. Assim que o computador é iniciado, a CPU entra no Ciclo de Busca, em seguida passa para o Ciclo de Execução e depois volta para o Ciclo de Busca. Ela continua nesse processo até que precise ser desligada, saindo do Ciclo de Execução para o estado final.
Durante o Ciclo de Busca, é a Unidade de Controle que atua. Uma nova instrução é busca da Memória para que possa ser decodificada. Nessa fase os registradores PC e IR são utilizados, como apresentados na seção anterior. O PC é logo lido para se saber que instrução será executada, essa instrução é trazida para o IR e, finalmente, é decodificada pela Unidade de Controle. Assim que esse processo termina, caso a instrução não diga respeito à um laço, ou à uma repetição, o conteúdo de PC é incrementado. Ou seja, PC recebe PC + 1. Assim, no próximo Ciclo de Busca a instrução do endereço seguinte será carregada da memória e executada. Esse comportamento garante a característica de execução sequencial dos programas.
No passo seguinte a CPU entra em Ciclo de Execução. Nessa etapa atua a Unidade de Ciclo de Dados. Agora a Unidade de Controle já sabe exatamente que operação será executada, com quais dados e o que fazer com o resultado. Essa informação é passada para a ULA e os registradores envolvidos. Durante o Ciclo de Execução há cinco possíveis tipos de operação que podem ser executadas:
- Processador e memória – trata simplesmente da transferência de dados entre CPU e memória principal;
- Processador e Entrada e Saída – diz respeito à transferência de dados entre a CPU e um dispositivo de Entrada e Saída, como teclado, mouse, monitor, rede, impressora etc.;
- Processamento de Dados – são operações simplesmente de processamento dos dados, como operação aritmética ou lógica sobre os registradores da CPU;
- Controle – são instruções que servem para controlar os dispositivos do computador, como para ligar um periférico, iniciar uma operação do disco rígido, ou transferir um dado que acabou de chegar pela Internet para a Memória Principal;
- Operações compostas – são operações que combinam uma ou mais intrusões das outras em uma mesma operação.
Busca de Dados
Em operações entre Processador e Memória, é necessário que dados sejam trazidos da memória para servirem de entrada para a ULA, e/ou o resultado seja levado para armazenamento na memória no final da execução. Para isso acontecer, é executada uma Busca de Dados. Isso é determinado durante a decodificarão da instrução, no ciclo de Busca de Instrução. Isso acontece quando um dos parâmetros de uma operação aritmética é um endereço de memória, e não um valor diretamente, nem um registrador. Para isso, parte do conteúdo de IR é transferido para o MAR. Essa parte é justamente o endereço do parâmetro da instrução. Em seguida a Unidade do Controle requisita à memória uma leitura. Assim, o endereço, agora em MAR, é passado para a memória e o conteúdo lido da memória é passado para o MBR. Agora o conteúdo é transferido para a ULA para que a operação seja executada (lógica ou aritmética).
Se a instrução tiver dois ou mais parâmetros de memória, serão necessárias outras Buscas de Dados. Como a memória é sempre mais lenta do que a CPU, instruções que necessitam Buscas de Dados são muito mais lentas do que instruções de Processamento de Dados.
Perceba que cada instrução pode exigir mais tempo de execução do que outras. Isso depende de quantos acessos à memória ela exigirá. Quanto mais acessos à memória, mais lenta a instrução. O ideal é sempre usar registradores. Mas nem sempre é possível utilizar registradores. Eles estão sempre em poucas quantidades e em menores tamanhos. Principalmente por serem caros. O que os computadores sempre tentam fazer é passar os dados da memória para os registradores assim que puderem, para que as próximas instruções sejam aceleradas.
Interrupções
Além do ciclo básico de intrusão apresentado anteriormente, a CPU pode ainda executar outro tipo de tarefa. Ela diz respeito ao processamento de pedidos oriundos dos dispositivos de Entrada e Saída. Como o Ciclo de Instrução da CPU que vimos até o momento é fechado, ou seja, a CPU sempre fica em estado de repetição até que seja desligada, ela não pode atender a nenhum evento externo que não seja a execução de um programa. Por exemplo, quando um usuário pressiona uma tecla do teclado, ou faz um movimento com o mouse, ou mesmo, quando uma mensagem chega pela Internet através da placa de rede. O que a CPU deve fazer? Se ela estiver em um Ciclo de Instrução fechado como mostrado anteriormente, nada. Ela precisa parar o que está fazendo para atender ao evento ocorrido e, só então, voltar ao Ciclo de Instruções. Esse processo de parar o Ciclo de Instrução para atender a um evento externo é chamado de Interrupção.
O Ciclo de Instrução pode agora ser visto modificado na [Ciclo de Instruções com interrupções] para atender às Interrupções. Todas interrupções são recebidas e armazenadas internamente por um dispositivo chamado Gerenciador de Interrupções. Esse dispositivo é um chip, semelhante à uma CPU, mas bem mais simples.
Na maioria dos computadores eles vêm soldados na Placa-Mãe, mas podem também vir dentro do chip da CPU. Toda interrupção possui um código de identificação. Sempre que uma nova interrupção chega nesse gerenciador, ele armazena esse código em sua memória e manda um sinal para CPU através do Barramento e Controle. Durante seu Ciclo de Instrução, sempre que uma instrução é executada, antes de voltar para o Ciclo de Busca, a CPU chega de algum sinal de interrupção foi enviado pelo Gerenciador de Interrupção.
Quando não há uma interrupção, a execução volta ao Ciclo de Busca e o programa em execução continua a ser executado. Mas se houver uma interrupção, a CPU agora vai parar a execução do programa atual para atender a interrupção. Por exemplo, vamos super que o usuário pressionou uma tecla do teclado. O código armazenado pelo Gerenciador de Interrupção indica que a interrupção veio do teclado. A CPU para sua execução do programa anterior e vai iniciar a execução de um programa especial, o Tratador de Interrupção. O código do dispositivo (aqui seria o teclado) serve para a CPU saber o endereço do Tratador de Interrupção ela vai buscar da memória. Então, ao sair do Checagem de Interrupção, a CPU muda o endereço do PC para o endereço do Tratador de Instrução. Assim, no Ciclo de Busca a próxima instrução a ser trazida da memória e posteriormente executada será a do tratador do teclado.
Cada tipo de interrupção precisa de um tratamento específico a ser feito. No caso do teclado, o tratador vai checar que tecla foi pressionada. Isso é feito através de uma leitura à memória do teclado (sim, todos dispositivos possuem uma pequena memória) para saber que tecla foi pressionada. Dependendo da tecla, uma operação diferente será executada. Geralmente, a CPU adiciona o código da tecla pressionada num endereço específico de memória. Cada programa, lendo essa informação, tomará sua própria decisão sobre o que deve ser feito. O que acontece é que apenas o programa ativo no momento, vai ler esse conteúdo, executar a ação da tecla e limpar essa área de memória. Se o programa for um editor de texto, por exemplo, o código pode representar escrever a letra pressionada na posição atual do cursor dentro do texto.
Quando esse processo encerra, o tratamento é encerrado, e a CPU deve voltar a execução do programa que havia sido interrompido. Isso só é possível porque, antes de passar a execução do Tratador de Interrupção, a CPU salva os conteúdos de todos os registradores da CPU (inclusive o PC e o IR). Então, antes de devolver a execução para o programa, CPU restaura todos os valores dos registradores antes salvos. Dessa forma, o programa retoma exatamente do ponto em que parou.
As interrupções também ocorrem se o próprio programa em execução executar uma operação ilegal. Isso é feito para evitar que a CPU entre em erro. Por exemplo, se um programa tentar acessar uma área da memória que é proibida para ele, como a área de outro programa ou do Sistema Operacional. Nesse caso, o programa é interrompido e não volta mais a executar, ele é finalizado e a execução é devolvida ao Sistema Operacional. Algo semelhante ocorre em caso de defeitos em alguns dispositivos. Por exemplo, se um programa estiver lendo um arquivo que está em um pendrive, e esse pendrive é removido subitamente, uma interrupção é lançada e o programa é encerrado, já que ele não faz mais sentido estar em execução.
Sobre o desempenho
É possível agora perceber que o desempenho das CPUs depende de muito outros fatores além da velocidade do seu clock. O computador precisa ter memórias rápidas para reduzir o tempo dos Ciclos de Busca, precisam de mais registradores para usar menos a memória e também que poucas interrupções ocorram. Cada vez que uma interrupção ocorre, o programa deve ser interrompido e a chamada deve ser atendida. Isso vai atrasar demais o tempo de execução dos programas, dando a impressão de baixo desempenho.
Basicamente, há dois tipos programas, os orientados à CPU e os orientados a Entrada e Saída. Na [Execução com várias interrupções] o comportamento dos primeiros é mostrado na parte a) e o dos segundos na parte b).
Quando um programa é orientado à CPU, há momentos longos de processamento de CPU e curtos momentos de espera por um evento de Entrada e Saída. É o exemplo de programas que fazem muitos cálculos matemáticos, como ferramentas de simulação, projetos de engenharia, computação gráfica e planilhas de cálculos. Inicialmente os dados de entrada são passados por um dispositivo de entrada, há longos momentos de cálculos e depois os resultados são passados para um dispositivo de entrada e saída.
Já nos programas orientados à Entrada e Saída (b), são aqueles chamados também de interativos. Há muitos momentos de interação e uso de dispositivos de Entrada e Saída, e poucos momentos de uso de CPU. Como é o caso de programas que usam muito de mouse e teclado, como os jogos e a própria navegação na internet.
O que temos que ter em mente é que o desempenho de um computador está muito ligado ao perfil de cada usuário. Os Sistemas Operacionais são os responsáveis por escolher que tarefa colocar para executar a cada momento e por quanto tempo ela deve executar até que uma nova tarefa entre em execução. Assim, o papel do Sistema Operacional também é fundamental e determinante no desempenho do sistema. O que ele tenta fazer no máximo que pode, é tentar ocupar os tempos de espera de um programa com a execução de outro. Tarefa nada fácil!
Exemplo de execução de um programa
Suponha que queiramos executar uma instrução de máquina que soma dois números que estão na memória e salve o resultado em outro endereço de memória. Para tal, vamos indicar que a memória (M) se comporta como um vetor (um array) e entre colchetes indicaremos o endereço do dado, ou da instrução. Sendo assim, a instrução que gostaríamos de executar seria:
200: M[100] = M[101] + M[102]
Nesse caso, vamos ler que no endereço 200 da memória há uma instrução que precisa somar o conteúdo do endereço 101, com o conteúdo do endereço 102 e salvar o resultado no endereço 100 da memória. Supondo que M[101] contenha o valor 10, e M[102] contenha o valor 20, ao final da execução, o endereço 100 de memória (M[100]) deverá conter o valor 30.
Como uma instrução como essa será executada depende de cada arquitetura. Aqui vamos utilizar uma abordagem que quebra as instruções em pequenos passos simples, que facilitam o trabalho de decodificarão da CPU.
Sendo assim, esse programa seria transformado na seguinte sequência de instruções e executado.
PC = 200; //Envia comando de leitura de instrução para a memória IR <- (M[100] = M[101] + M[102]) // Busca instrução da memória PC = PC + 1 //Instrução é passada do IR para a Unidade de Controle
A primeira ação seria realizar o Ciclo de Busca, visando trazer a instrução a ser executada da memória para o processador. O endereço da instrução (200) seria passado para o PC e um comando de leitura de instrução seria passado para a memória. Baseada no endereço trazido por PC, a memória localizaria a instrução e a enviaria para o processador, que a armazenaria no registrador IR. Antes de passar a instrução para a Unidade de Controle para dar início à execução, o registrador PC é atualizado para o próximo endereço de memória, no caso, 201.
O próximo passo será iniciar o Ciclo de Execução:
//O primeiro dado é trazido da memória para o registrador R1 MAR = 101 //Envia comando de leitura de dado para a memória MBR <- 10 R1 = MBR // valor lido da memória é passado para o registrador R1
Como os dados a serem operados estão também na memória, é antes necessário executar uma operação de Busca de Operando, ou Busca de Dado. O primeiro operando está no endereço 101. Sendo assim, o endereço 101 é passado para o registrador de endereço (MAR). Esse endereço é passado para a memória e é enviado um comando de leitura de dado. O conteúdo, o valor 10, é então localizado pela memória e enviado para o processador, que o armazena no registrador de dados (MBR). Como o MBR será utilizado nas próximas etapas de execução, seu conteúdo é salvo em um registrador de propósito específico, o R1.
Em seguida, a Unidade de Controle passa para a busca do segundo operando, contido no endereço 102:
//O segundo dado é trazido da memória para o registrador R1 MAR = 102 //Envia comando de leitura de dado para a memória MBR <- 20 R2 = MBR // valor lido da memória é passado para o registrador R2
Essa etapa ainda faz parte do Ciclo de Execução, e também diz respeito à uma Busca de Dado. A busca é mesma do passo anterior, mas agora o endereço buscado é o 102, e o conteúdo é o 20, que é repassado para o registrador R2.
O próximo passo do Ciclo de Execução é executar a operação aritmética propriamente dita. Isso geralmente é feito entre registradores de propósito geral, por serem mais rápidos do que se fosse tratar dados da memória. Os conteúdos de R1 e R2 são somados e armazenados em R3:
R3 = R1 + R2
Para finalizar o processo, o resultado deve ser armazenado de volta na memória:
MAR = 100 // Endereço é passado para MAR MBR = R3 // Resultado da operação é passado para MBR // Comando de escrita é passado para a memória M[100] <- 30 // Endereço 100 da memória recebe o valor 30
Para isso ser realizado, é preciso executar uma operação de escrita na memória. O endereço 100 é então passado para MAR e o resultado da operação, salvo em R3 é passado para MBR. Quando o comando de escrita é enviado pela Unidade de Controle para a memória, ela lê o endereço 100 pelo Barramento de Endereço e o valor 30 pelo Barramento de Dados e salva, então, o valor 30 no endereço 100.
Com isso a operação é finalizada. Essa operação foi executada em aproximadamente 14 passos. Esse valor é aproximado porque alguns deles são apenas o envio de sinal para a memória, e isso geralmente é feito em paralelo com o passo seguinte. Se cada passo for executado dentro uma batida do relógio (ou ciclo de clock), teremos 14 ciclos de clock para uma única instrução. Mas perceba que o acesso à memória é sempre mais lento do que a execução do processador. Se cada acesso à memória levar 3 ciclos de clock, teremos um total de 20 ciclos de clock.
Nota – Apenas uma memória tipo Cache poderia ser acessada com apenas 3 ciclos de clock. Uma memória principal convencional precisa de entre 10 e 15 ciclos de clock para ser lida. Depende de sua tecnologia (e preço!).
Parece bastante, mas algumas instruções podem levar muito mais ciclos do que isso, como operações com Ponto Flutuante (números reais), ou de acesso à um periférico, como o disco rígido. Isso depende muito de como o projeto do computador é elaborado.
Apesar do computador parecer pouco efetivo na execução de uma simples soma, como ele executa numa frequência de clock muito alta, ele acaba executando muitas operações por segundo. Então, utilizar apenas a frequência de clock como medida de desempenho não é uma boa ideia. O mais utilizado é medir a quantidade de operações aritméticas que o processador é capaz de executar por segundo. Hoje em dia um computador pessoal está na escala dos alguns Milhões de Instruções por Segundo (ou MIPS). Mais a seguir vamos estudar como essas e outras medidas de despenho podem ser calculadas.
Aumentando o desempenho com Pipeline
Pelo o que foi visto, até o momento, a execução de um programa é essencialmente sequencial, ou seja, uma instrução só é executada quando a anterior termina. Ao longo do nosso curso veremos que há dois modos de paralelismo que podem ser utilizados para melhorar ainda mais o desempenho do processador. O primeiro deles é através do chamado Paralelismo em Nível de Hardware, que é obtido quando replicamos unidades do processador para que elas funcionem em paralelo, reduzindo assim o tempo de execução dos programas. A segunda forma é através do Paralelismo em Nível de Instruções, ou ILP (do inglês, Instruction Level Parallelism). Nesse caso, as unidades do processador não são duplicadas, mas melhores organizadas para que não fiquem ociosas. Há duas formas principais de implementar o ILP, uma delas é através do Pipeline e a outra é através de processadores Superescalares. Aqui vamos tratar do Pipeline, e no capítulo sobre Processamento Paralelo, vamos tratar as outras formas de paralelismo.
Imagine que possamos dividir o Ciclo de Instrução de um determinado processador nas cinco etapas seguintes:
- Carregar instrução (FI) – Traz a instrução da memória para o processador, armazena em IR (essa etapa também é chamada de Fetch de Instrução) e a decodifica para execução no passo seguinte.
- Carregar operandos (FO) – Traz os operandos da operação dos registradores para a ULA, para que a operação seja realizada sobre eles, também chamada de Fetch de Operandos.
- Executar instruções (EI) – Executa operação lógica ou aritmética propriamente dita.
- Escrever em memória (WM) – Escreve o resultado da operação em memória, se necessário.
- Escrever em registrador (WR) – Escreve o resultado da operação em um dos registradores, se necessário.
Esse é um dos Ciclos de Instrução mais simples que poderíamos imaginar, organizado em apenas 5 etapas.
Nota – Processadores convencionais, como os da Intel que usamos em nossos computadores, executam instruções em cerca de 18 etapas.
Cada instrução deve passar pelos cinco passos descritos para ser executada. Suponha que cada etapa necessite de apenas 1 ciclo de clock para ser executada. Quantos ciclos seriam necessários para executar um programa de 20 instruções? Essa conta é simples. Cada instrução deve passar pelas cinco etapas, e cada etapa leva 1 ciclo de clock, sendo assim, o programa levará 20 vezes 5 ciclos, ou seja, 100 ciclos de clock.
Agora vamos analisar o que acontece com cada etapa a medida em que o programa é executado. A primeira instrução vai passar pela etapa FI, que a leva para o IR e a decodifica. Em seguida ela é passada para a etapa FO, e os dados necessários para a operação são levados dos respectivos registradores para a ULA. Agora, observe. Neste exato momento, a segunda instrução do programa está parada na memória, aguardando sua vez para ser executada. Ao mesmo tempo, a etapa FI está ociosa. Por que a etapa FI não pode entrar em ação e trabalhar com a segunda instrução do programa, enquanto a primeira está na etapa FO?
O mesmo vai ocorrer com todas as etapas de execução da primeira instrução do programa. Ela vai ser executada na etapa EI, depois vai passar para a etapa WM que checará se há necessidade de copiar o resultado para a memória e, finalmente, para a WR, que copiará o resultado para um dos registradores. Quando a primeira instrução estiver na etapa WR, as etapas anteriores estarão todas ociosas. Por que não aproveitar o tempo ocioso para colocar as etapas anteriores para irem adiantando a execução das próximas instruções? É isso que propõe o Pipeline!
O Pipeline vai separar as etapas de execução de instruções em unidades físicas independentes, assim, uma etapa pode trabalhar com uma instrução, ao mesmo tempo em que uma outra unidade trabalha com uma outra instrução. É a mesma estratégia utilizada pela indústria de produção em massa para fabricar carros, por exemplo. Enquanto um chassi está sendo montado, outro está recebendo a carroceria, outro o motor, outro sendo pintado e outro recebendo o acabamento interno. Todas etapas trabalhando em paralelo e vários carros sendo tratados ao mesmo tempo. Esta estratégia aumenta o desempenho da execução das instruções de forma grandiosa.
Na figura “Processador adaptado para trabalhar com Pipeline de cinco estágios” são apresentadas as adequações necessárias no processador para que as etapas possam ser organizadas em Pipeline. Dizemos então que esse processador trabalha com cinco Estágios de Pipeline.
A primeira mudança necessária é a separação da memória em duas partes independentes (ou duas memórias mesmo). Uma parte será utilizada apenas para instruções (representadas na figura pela palavra Fetch), e outra apenas para os dados (representada por DMem). Isso é necessário para que a etapa FI acesse a memória para buscar a próxima instrução, ao mesmo tempo em que a WM acessa a memória para salvar o resultado de outra instrução anterior. Se houvesse apenas uma memória para dados e instruções, isso não seria possível. Essa mudança vai contra o que foi projetado na ((Arquitetura de von Neumann)), e foi considerado um grande avanço. Ela foi batizada de Arquitetura Harvard.
Outra mudança importante foi a adição de memórias intermediárias entre cada etapa. Na figura “Processador adaptado para trabalhar com Pipeline de cinco estágios” essas memórias são representadas pelos retângulos preenchidos e sem nenhuma palavra sobre eles. Essas memórias são utilizadas para armazenar o resultado da etapa anterior e passá-lo para a etapa posterior no ciclo seguinte. Elas são necessárias porque as etapas não executam necessariamente sempre na mesma velocidade. Se uma etapa for concluída antes da etapa seguinte, seu resultado deve ser guardado nessas memórias para aguardar que a etapa seguinte conclua o que estava fazendo. Só então ela poderá receber o resultado da etapa anterior.
O mesmo ocorre na produção de um carro. A etapa de instalação do motor pode ser mais rápida do que a de pintura, por exemplo. Então, se um carro acabou de receber um motor, ele deve ser guardado num local temporário até que o carro anterior tenha sua pintura concluída. Assim, a etapa de instalação do motor pode receber um novo carro.
Qual o benefício da execução em Pipeline? Para isso, vamos analisar a figura “Execução em pipeline de cinco estágios”.
Nesse exemplo a dimensão horizontal (eixo X) representa o tempo, e a dimensão vertical (eixo Y) representa as instruções a serem executadas (I1, I2, I3, I4 e I5). Nessa imagem, a instrução I1 já passou por todas as etapas e está em WR, enquanto isso, I2 está em WM, I3 está em EI, I4 está em FO e I5 ainda está em FI. Como o Pipeline possui 5 estágios, ele precisa, no mínimo, de 5 instruções para encher o Pipeline e, a partir daí, inicia-se o ganho de desempenho.
Voltando ao exemplo anterior, considerando que cada etapa leve 1 ciclo de clock para ser concluída. Quantos ciclos são necessários para executar 20 instruções agora com Pipeline? No início, o Pipeline não está cheio, então a instrução I1 deve passar por todas as 5 etapas para ser concluída, levando então 5 ciclos de clock. Mas, a instrução I2 acompanhou I1 durante toda execução e terminou no ciclo seguinte, ou seja, em 6 ciclos de clock. Em seguida, a instrução I3 foi concluída em 7 ciclos de clock, I4 em 8 ciclos, I5 em 9 ciclos, assim em diante, até a conclusão da vigésima instrução, que ocorreu em 24 ciclos.
Comparado com o exemplo sem Pipeline, que executou o mesmo programa em 100 ciclos, o ganho foi de 4,17 vezes. Se o programa tivesse 200 instruções, levaria 1000 ciclos de clock sem Pipeline e 204 ciclos com Pipeline, o resultaria num ganho de 4,9 vezes. Onde queremos chegar com isso?
Importante – A medida em que a quantidade de instruções aumenta, o ganho de desempenho com Pipeline vai se aproximando da quantidade de estágios, que foi 5 nesse exemplo. Então, quanto mais instruções forem executadas e mais estágios de Pipeline tiver o processador, maior será o benefício de usar o Pipeline.
Limitações do Pipeline
Infelizmente, nem sempre o processador consegue usufruir do ganho máximo de desempenho ao usar o Pipeline. Há vários riscos que podem fazer com que o Pipeline seja interrompido e precise ser reiniciado, ou impossibilitado até de iniciar. Os riscos são:
- Riscos Estruturais
- Riscos de Dados
- Riscos de Controle
Os Riscos Estruturais são limitações físicas do processador. O exemplo mais simples é a separação da memória em Memória de Dados e Memória de Instruções. Se isso não ocorrer, as etapas de FI e WM não podem ser executadas ao mesmo tempo.
Já o Risco de Dados ocorre quando uma instrução depende do resultado de uma instrução anterior que está no Pipeline e ainda não está pronta. Imagine o trecho de programa a seguir:
I1: r1 = r2 + r3 I2: r4 = r1 - r3
A instrução I1 inicia primeiro e logo avança nas etapas do Pipeline. Logo depois dela vem I2. Quando I2 for buscar o valor de r1 na etapa FO, ele ainda não estará pronto, porque I1 ainda está em EI. A instrução I1 precisaria concluir a última etapa (WM) para que I2 pudesse executar FO. Nesses casos, dizemos que há uma Dependência de Dados. Isso cria uma bolha no Pipeline, o processador tem que avançar I1 até a conclusão de WM, e parar a execução de I2 e todas instruções seguintes até então. Só depois da conclusão de I1 é que o I2 e as próximas instruções seriam liberadas para continuar a execução.
O último tipo de risco é o Risco de Controle. Esse ocorre quando qualquer mudança no fluxo de controle do processador. Ou seja, quando a execução deixa de ser meramente sequencial. Como vimos anteriormente, o Pipeline vale a pena quando temos uma grande sequência de instruções sendo executadas. O processador confia que depois da instrução I1 ele executará a I2, depois a I3, e assim sucessivamente. Mas o que acontece, por exemplo, se o processador estiver executando a instrução I10, e essa instrução ordenar que o programa salte para a instrução I30? Isso pode ocorrer se a instrução se tratar de uma repetição, ou uma chamada a uma função. A mudança de controle também pode ocorrer por meio de interrupção, provocada por um dispositivo de entrada e saída, ou pelo pelo próprio Sistema Operacional, quando determina que um programa seja interrompido para passar a execução para um outro. Quando há uma mudança no fluxo de execução desta maneira, todas instruções que estão no Pipeline são removidas, o fluxo é modificado, e o Pipeline começa a ser preenchido todo novamente.
As técnicas de Pipeline avançaram bastante e várias medidas foram tomadas para amenizar o prejuízo causado pelos riscos mencionados. Entretanto, nenhuma técnica é capaz de evitar todas as possíveis perdas de desempenho. Até boas práticas de programação podem ajudar a otimizar a execução dos programas, e os compiladores também ajudam bastante neste aspecto.
Na próxima sessão vamos estudar um pouco como podemos realmente medir o desempenho dos processadores e entender melhor o que faz um processador mais eficiente do que outro.
O desempenho dos computadores
O desempenho dos processadores e dos computadores é muito valorizado pelas empresas por agregarem muito valor a elas. Como se costuma dizer, tempo é dinheiro. Então quanto menos tempo se espera para um computador realizar uma tarefa, mais tempo resta para a empresa se dedicar a outras atividades. O desempenho é tão importante, que há uma corrida silenciosa entre empresas, universidades e governos para saber quem é capaz de produzir o computador mais rápido do mundo. A organização chamada Top 500 organiza uma competição para conhecer quem são esses campeões de desempenho e anualmente geral uma lista com os 500 computadores mais velozes. Atualmente a China e os Estados Unidos disputam o topo da lista do Top 500. Nas décadas após a Segunda Guerra Mundial os países disputavam uma guerra silenciosa (a Guerra Fria) para saber quem era o país mais poderoso em poder bélico e em tecnologias, como a corrida espacial.
Hoje a Guerra Fria já terminou, mas a corrida pela liderança tecnológica e econômica mundial continua e possui um novo concorrente forte disputando de igual para igual com os Estados Unidos, a China. Nessa corrida o poder computacional é importantíssimo! Ter um computador poderoso não significa apenas ser capaz de realizar tarefa mais rapidamente, mas ser também capaz de realizar certas tarefas que seriam impossíveis em computadores menos poderosos. Um exemplo disso é a construção de um computador que haja de forma semelhante ao cérebro humano. Chegar a esse ponto significa ser capaz de construir sistemas que possam substituir o homem em várias tarefas complexas, como dirigir máquinas e até mesmo operar computadores. Outro exemplo seria simular o comportamento perfeito da reação do corpo humano a drogas. Assim, não seria mais necessário o uso de cobaias para testar medicamentos. Esse avanço traria um poder incalculável a quem o dominasse!
Acesso o site da Top 500 em http://www.top500.org e conheça as super máquinas da computação!
Medidas de desempenho
Para medir o desempenho dos computadores, três métricas principais são mais usadas:
Ciclos de Clock por Instrução (CPI) – determina quantos ciclos de clock são necessários para executar uma determinada instrução. Vimos que o Ciclo de Instrução é organizado em várias etapas e que isso depende de instrução para instrução. Se uma instrução acessar mais memória do que outra, ela será mais lenta. Instruções que operam com Pontos Flutuantes são mais lentas do que as operações com números inteiros. É fácil perceber a razão disso. Operações com números reais são mais complexas de serem resolvidas, porque devem ser realizadas para a parte fracionária e para a inteira, e depois o resultado deve ser consolidado. Assim, simulações são realizadas com um processador e são calculados quantos ciclos de clock cada tipo de instrução necessita em média para ser completada. Este é o CPI!
Milhões de Instruções por Segundo (MIPS) – o CPI é uma medida utilizada para medir o desempenho do processador para cada tipo de instrução, mas não é muito boa para medir o desempenho para a execução de programas, que é o objetivo de todo computador. Isso porque os programas são geralmente formados por instruções de todos os tipos, com inteiros, ponto flutuante, acessando muita ou pouca memória. Outro fator é que fatores como, Pipeline, tecnologia de memória e tamanho da memória Cache, podem fazer com que uma instrução seja executada lenta agora, e rápida logo em seguida. Para contornar isso, uma métrica muito utilizada é o MIPS. Ela determina quantos Milhões de Instruções são executadas pelo computador a cada segundo. Nesse caso, programas que demandam muito esforço do computador são executados, a quantidade de instruções é contada e depois dividida pela quantidade de segundos da execução. Caso o CPI médio (também chamado de CPI Efetivo) do computador já tenha sido calculado anteriormente, o MIPS pode ser calculado pela fórmula:
Onde, Clock é a frequência do relógio do processador em Hertz, CPI é o CPI médio e M é um milhão (). É necessário dividir o resultado por M porque a medida do MIPS é sempre dada na escala de milhões. Por exemplo, se um processador de 2 GHz e CPI médio de 4 ciclos por instrução, o MIPS desse processador será:
O resultado dessa operação será 0,5M, ou 500K. Isso porque 2 dividido por 4 é 0,5, e 1 Giga dividido por 1 Mega, resulta em 1 Mega.
Nota – Lembre-se sempre de considerar a grandeza do Clock no cálculo. Um giga Hertz é muito diferente de um kilo, ou um mega Hertz!
Milhões de Instruções em Ponto Flutuante por Segundo (MFLOPS)
uma alternativa para o MIPS é o MFLOPS. O MIPS é muito eficiente, mas não para comparar programas diferentes. Para calcular o MFLOPS, são executadas apenas instruções que operam com Ponto Flutuante e são calculados quantos segundos se passaram para cada milhão delas. Assim, ela pode ser definida da seguinte forma:
A única diferença para o cálculo do MIPS é que apenas as instruções que operam com Ponto Flutuante são consideradas. Assim, diz respeito a quantos ciclos de clock em média são necessários para executar uma instrução de ponto flutuante.
Os computadores pessoais e comerciais de hoje trabalham na escala MFLOPS.
Já os supercomputadores trabalham na escala de GFLOPS (Giga FLOPS). Aqueles computadores que lideram a lista do Top 500, e são capazes até de mudar o PIB de um país, trabalham na escala do TFLOPS (Teta FLOPS).
Exemplos de calcular o desempenho de um processador
Suponha que um programa é executado num processador de 40MHz. A tabela “Exemplo de configuração de um processador” apresenta os CPIs coletados para cada tipo de instrução, bem como sua quantidade de instruções para um determinado programa com 100.000 instruções.
Tipo de instrução | CPI | Número de instruções |
Aritmética com Inteiros | 1 | 45.000 |
Operações de acesso à Memória | 4 | 32.000 |
Operações com Ponto Flutuante | 2 | 15.000 |
Instruções de salto e desvio | 5 | 8.000 |
Para este exemplo, vamos calcular o CPI efetivo, o MIPS e o MFLOPS.
O CPI Efetivo é simplesmente a média ponderada dos CPIs apresentados para o programa. Isso pode ser feito da seguinte forma:
Já o MIPS pode ser calculado como:
Ou seja, para o programa examinado, o processador teve um desempenho de 16,46 milhões de instruções por segundo. Se o objetivo for calcular o MIPS geral, e não específico para esse programa, deve-se utilizar a média aritmética de todos os CPI calculados, e não a média ponderada.
Para calcular o MFLOPS seguimos a mesma estratégia, mas dessa vez utilizamos apenas o CPI para instruções de ponto flutuante. Ou seja:
Isso significa que esse processador apresentou um desempenho de 20 milhões de instruções de ponto flutuante por segundo.
Recapitulando
Vimos em mais detalhe neste capítulo como os processadores executam nossos programas, e como alguns aspectos de organização e arquitetura são importantes. Não apenas a frequência do clock do processador é relevante, mas como o Pipeline é aplicado, se há muitas interrupções e se o programa é bem escrito a ponto de explorar melhor o Pipeline. Também foram apresentados três diferentes métricas para se avaliar o desempenho de um sistema, o CPI, o MIPS e o MFLOPS.
No próximo módulo vamos estudar mais profundamente a CPU através do estudo de sua Unidade de Controle e como ela faz para decodificar e executar instruções.