Assembly NumaBoa - Capítulo 3

ORGANIZAÇÃO DE SISTEMAS IV

O processador 886

O processador 886 é o mais lento da família x86. Os tempos para cada uma das instruções já foram discutidos na seção anterior. A isntrução mov, por exemplo, precisa de cinco a doze ciclos de clock para ser executada, dependendo dos operandos. A tabela seguinte mostra os tempos para as várias formas de instrução nos processadores 886.

Modo de endereçamento da instruçãomov (ambas as formas)add, sub, cmp, and, ornotjmpjxx
reg, reg57
reg, xxxx6-78-9
reg, [bx]7-89-10
reg, [xxxx]8-1010-12
reg, [xxxx+bx]10-1212-14
[bx], reg7-8
[xxxx], reg8-10
[xxxx+bx], reg10-12
reg6
[bx]9-11
[xxxx]10-13
[xxxx+bx]12-15
xxxx6-76-8

Existem três coisas importantes que precisam ser ressaltadas. Primeiro, instruções mais longas precisam de mais tempo para serem executadas. Segundo, instruções que não fazem referência à memória geralmente são executadas mais rápido. Isto é ainda mais relevante se existirem estados de espera associados ao acesso à memória (a tabela acima pressupõe estado de espera zero). Finalmente, instruções que usam modos de endereçamento complexos rodam mais devagar. Instruções que utilizam operandos registradores são mais curtas, não acessam a memória e não usam modos de endereçamento complexos. Este é o motivo pelo qual devemos tentar manter as variáveis em registradores.

O processador 8286

O segredo para aumentar a velocidade de um processador é realizar operações em paralelo. Se, nos tempos dados para o 886, pudéssemos realizar duas operações a cada ciclo de clock, a CPU executaria instruções com o dobro da velocidade na mesma velocidade de clock. Entretanto, não é tão simples assim decidir executar duas operações por ciclo de clock. Muitos dos passos da execução de uma instrução compartilham unidades funcionais da CPU (unidades funcionais são grupos de lógica que realizam uma operação em comum, por exemplo, a ALU e a unidade de controle). Uma unidade funcional só comporta uma operação por vez. Portanto, não é possível realizar, simultaneamente, duas operações que usem a mesma unidade funcional (por exemplo, incrementar o registrador IP e adicionar dois valores). Outra dificuldade que pode ocorrer com certas operações concorrentes é quando uma das operações depende do resultado da outra. Por exemplo, os dois últimos passos da instrução add envolvem a soma de valores e o armazenamento do resultado. Não podemos armazenar a soma num registrador antes de ter calculado a soma. Existem também outros recursos que a CPU não pode compartilhar entre as etapas de uma instrução. Por exemplo, existe apenas um barramento de dados. A CPU não pode buscar um opcode de instrução no momento em que estiver tentando armazenar dados na memória. O truque no projeto de uma CPU que execute diversos passos em paralelo é organizar estes passos para reduzir conflitos ou então adicionar lógica de modo que as duas (ou mais) operações possam ocorrer simultaneamente, executando-as em unidades funcionais diferentes. Considere novamente as etapas que a instrução mov reg, mem/reg/const necessita:

  1. Buscar o byte da instrução na memória.
  2. Atualizar o registrador IP para que aponte para o próximo byte.
  3. Decodificar a instrução para ver o que ela faz.
  4. Se necessário, buscar um operando de 16 bits da instrução na memória.
  5. Se necessário, atualizar o IP para que aponte após o operando.
  6. Calcular o endereço do operando, se necessário (isto é, bx+xxxx).
  7. Buscar o operando.
  8. Armazenar o valor obtido no resgistrador de destino.

A primeira etapa usa o valor do registrador IP (de modo que não podemos sobrepor esta etapa com a incrementação do IP) e usa o barramento para buscar o opcode da instrução na memória. Cada passo que se segue depende do opcode, tornando pouco provável a sobreposição desta etapa com qualquer outra. O segundo e o terceiro passos não compartilham nenhuma unidade funcional, nem a decodificação de um opcode depende do valor no registrador IP. Portanto, podemos modificar com facilidade a unidade de controle de modo que ela incremente o registrador IP ao mesmo tempo em que decodifica a instrução. Isto corta um ciclo na execução da instrução mov. A terceira e a quarta etapa (decodificar e opcionalmente buscar o operando de 16 bits) não parecem ser apropriadas para serem executadas em paralelo porque a instrução precisa ser decodificada para se determinar se a CPU precisa ou não buscar um operando de 16 bits na memória. Entretanto, poderíamos projetar a CPU para se adiantar e sempre buscar o operando, de modo que estivesse disponível se por acaso for requisitado. Apesar disso, esta idéia apresenta um problema: precisamos do endereço do operando (o valor no registrador IP) e precisamos esperar até que o IP esteja atualizado antes de buscarmos este operando. Se estivermos incrementando o IP no mesmo momento em que estivermos decodificando a instrução, teremos que aguardar o próximo ciclo para buscar o operando. Uma vez que os próximos três passos são opcionais, existem várias sequências de instruções possíveis neste ponto: 1. (passos 4, 5, 6 e 7) por exemplo, mov ax, [1000+bx]; 2. (passos 4, 5 e 7) por exemplo, mov ax, [1000]; 3. (passos 6 e 7) por exemplo mov ax, [bx] e 4. (passo 7) por exemplo mov ax, bx. Nos passos indicados, a etapa 7 depende sempre do conjunto das anteriores. Portanto, a etapa 7 não pode ser executada em paralelo com qualquer uma das outras. A etapa 6 também depende da etapa 4. O passo 5 não pode ser executado em paralelo com o passo 4 porque este usa o valor do registrador IP, mas pode ser executado em paralelo com qualquer outro passo. Portanto, podemos cortar um ciclo das primeiras duas sequências citadas, ou seja: 1. (passos 4, 5/6 e 7); 2. (passos 4 e 5/7); 3. passos 6 e 7) e 4. (passo 7). É óbvio que não existe maneira de sobrepor a execução dos passos 7 e 8 na instrução mov porque ela precisa buscar o valor antes de armazená-lo. Combinando estes passos obtemos os seguintes para a isntrução mov:

  1. Buscar o byte da instrução na memória.
  2. Decodificar a instrução e atualizar IP.
  3. Se necessário, buscar um operando de 16 bits da instrução na memória.
  4. Calcular o endereço do operando, se necessário (isto é, bx+xxxx).
  5. Buscar o operando, se necessário atualizar IP para apontar após xxxx.
  6. Armazenar o valor obtido no resgistrador de destino.

Adicionando uma pequena quantidade de lógica à CPU, cortamos um ou dois ciclos na execução da instrução mov. Esta otimização simples funciona também para a maioria das outras instruções. Um outro problema com a execução da instrução mov refere-se ao alinhamento do opcode. Considere a instrução mov ax, [1000] que aparece na localização 100 na memória. A CPU gasta um ciclo buscando o opcode e, depois de decodificar a instrução e determinando que ela possui um operando de 16 bits, gasta dois ciclos adicionais para buscar este operando na memória (porque este operando aparece no endereço ímpar 101). O que é engraçado é que este ciclo de clock extra, para obter estes dois bytes, é desnecessário. Afinal de contas, a CPU obteve o byte menos significativo do operando quando foi buscar o opcode (lembre-se, as CPUs x86 são processadores de 16 bits e sempre pegam 16 bits da memória), então porque não salvar este byte e usar apenas um ciclo de clock adicional para obter o byte mais significativo? Isto economizaria um ciclo no tempo de execução se a instrução começar num endereço par (de modo que o operando caia no endereço ímpar). Haveria a necessidade de adicionar apenas um registrador de um byte e de uma pequena quantidade adicional de lógica para atingir este objetivo, um esforço que valeria a pena. Enquanto adicionamos um registrador para funcionar como buffer de bytes de operandos, vamos considerar algumas otimizações adicionais que usariam a mesma lógica. Por exemplo, verifique o que acontece quando a instrução mov acima citada é executada. Se buscarmos o opcode e o byte menos significativo do operando no primeiro ciclo e o byte mais significativo do operando no segundo ciclo, na verdade lemos quatro bytes, e não três. Este quarto byte é o opcode da próxima instrução. Se pudermos salvar este opcode até a execução da próxima instrução, poderíamos cortar um ciclo do seu tempo de execução porque não precisaria buscar o byte do opcode. Mais do que isto, como a decodificador da instrução está ocioso enquanto a CPU estiver executando a instrução mov, podemos decodificar a próxima instrução enquanto a instrução atual estiver sendo executada e cortamos mais um ciclo da execução da próxima instrução. Em média, buscaremos este byte extra em cada uma das instruções seguintes. Portanto, implementando este esquema simples, conseguiremos cortar dois ciclos de cerca de 50% das instruções que executarmos. Podemos fazer mais alguma coisa com os outros 50% das instruções? A resposta é sim.

Prefetch queue

Fig.19 - A fila de busca antecipada ou prefetch queue

Note que a execução de uma instrução mov não acessa a memória a cada ciclo de clock. Por exemplo, enquanto armazenamos dados no registrador de destino, o barramento está ocioso. Nestes intervalos de tempo, quando o barramento está ocioso, podemos antecipar a busca (pre-fetch) de opcodes de instruções e de operandos, salvando estes valores para a execução da próxima instrução. O maior aperfeiçoamento do processador 8286 em relação ao 886 é a fila de busca antecipada (pre-fetch queue). Sempre que a CPU não estiver usando a Unidade de Interface do Barramento (Bus Interface Unit - BIU), a BIU pode buscar bytes adicionais do fluxo de instruções. Sempre que a CPU precisar de uma instrução ou de um byte de operando, ela pega o próximo byte disponível na fila de busca antecipada. Como a BIU pega dois bytes por vez da memória e a CPU geralmente consome menos do que dois bytes por ciclo de clock, qualquer byte que a CPU, normalmente, fosse buscar do fluxo de instruções já estará na fila de pré-busca. Note, entretanto, que não temos garantia nenhuma que todas as instruções e operandos estejam na fila quando precisarmos deles. Por exemplo, a instrução jmp 1000 tornará o conteúdo da fila inválido. Se esta instrução aparecer nas localizações 400, 401 e 402 da memória, a fila de busca antecipada conterá os bytes dos endereços 403, 404, 405, 406, 407, etc. Após carregar o IP com 1000, os bytes dos endereços 403, etc não nos servem mais. Neste caso o sistema precisa fazer uma pausa para buscar o double word no endereço 1000 antes de poder continuar. Outro aperfeiçoamento que podemos fazer é sobrepor a decodificação de instruções ao último passo da instrução anterior. Depois que a CPU processar o operando, o próximo byte disponível na fila de pré-busca é um opcode e a CPU pode decodificá-lo antes da sua execução. É claro que, se a instrução atual modificar o registrador IP, qualquer tempo gasto decodificando a próxima instrução será perdido mas, como isto ocorre em paralelo com outras operações, não torna o sistema mais lento. A sequência de otimizações do sistema requer algumas modificações de hardware. Um diagrama do sistema pode ser visto na Fig.19.

A sequência de execução de instruções agora assume a ocorrência dos seguintes eventos no fundo - Eventos de pré-busca da CPU:

Assim como para o 886, não vamos considerar os tempos de execução das outras instruções x86 porque a maioria deles são indeterminados. As instruções de salto parecem ser executadas com grande rapidez no 8286. Na realidade, podem ser muito lentas. Não se esqueça de que o salto de uma localidade para outra invalida o conteúdo da fila de pré-busca. Assim, apesar de parecer que a instrução jmp seja executada em um ciclo de clock, ela força a CPU a descartar a fila de busca antecipada e, portanto, gastar vários ciclos para buscar a próxima instrução, operandos adicionais e decodificando a instruçao. Na realidade, apenas após duas ou três instruções após a instrução jmp é que a CPU volta ao ponto em que a fila de pré-busca esteja funcionando adequadamente e a CPU esteja decodificando opcodes em paralelo com a execução da instrução anterior. Isto revela um aspecto muito importante: se quisermos escrever programas rápidos, é melhor evitar saltos de uma lado para outro. Note que as instruções de saltos condicionais apenas invalidam a fila de pré-busca caso sejam realizados. Se a condição for falsa, a execução continua com a próxima instrução e os valores da fila de pré-busca e os opcodes pré-decodificados são usados normalmente. Portanto se, enquanto estivermos escrevendo um programa, for possível determinar qual das condições é a mais provável (por exemplo, menor que versus não menor que), deveríamos optar pela condição menos provável para o desvio para que a mais provável continue na linha de execução.

O tamanho das instruções (em bytes) também pode afetar a performance da fila de busca antecipada. Buscar um único byte de instrução nunca gasta mais do que um ciclo de clock, mas buscar uma instrução de três bytes sempre gasta dois ciclos. Portanto, se o objetivo de uma instrução de desvio forem duas instruções de um byte, a BIU poderá buscar as duas instruções num único ciclo de clock e começar a decodificar a segunda enquanto a primeira estiver sendo executada. Se estas instruções forem de três bytes, a CPU pode não ter tido tempo suficiente para buscar e decodificar a segunda ou a terceira instrução no momento em que tiver terminado a primeira. Portanto, sempre que possível, devemos usar instruções curtas para melhorar a performance da fila de busca antecipada. A tabela seguinte mostra os tempos de execução (otimistas) das instruções do 8286:

Modo de endereçamento da instruçãomov (ambas as formas)add, sub, cmp, and, ornotjmpjxx
reg, reg24
reg, xxxx13
reg, [bx]3-45-6
reg, [xxxx]3-45-6
reg, [xxxx+bx]4-56-7
[bx], reg3-45-6
[xxxx], reg3-45-6
[xxxx+bx], reg4-56-7
reg3
[bx]5-7
[xxxx]5-7
[xxxx+bx]6-8
xxxx1+pfd2 2+pfd

Observe como a instrução mov é muito mais rápida no 8286 que no 886. Isto se deve à fila de pré-busca, a qual permite que o processador sobreponha a execução de instruções adjacentes. Entretanto, esta tabela mostra um quadro exageradamente otimista. Não se esqueça da premissa "assumindo que o opcode esteja presenta na fila de busca antecipada e que tenha sido decodificado". Imagine a seguinte sequência de três instruções:

	????:	jmp	10001000
	    :	jmp	20002000
	    :	mov	cx, 3000

A segunda e a terceira instruções não serão executadas tão rapidamente quanto sugerem os tempos da tabela acima. Sempre que o valor do registrador IP for modificado, a CPU descarrega a fila de pré-busca. Neste caso, a CPU não pode buscar e decodificar a próxima instrução. Pelo contrário, ela precisa buscar o opcode, decodificá-lo, etc, aumentando o tempo de execução destas instruções. Neste ponto, o único progresso que fizemos foi executar a operação de "atualizar o IP" em paralelo com alguma outra etapa. Habitualmente, a inclusão da fila de pré-busca melhora a performance. Este é o motivo pelo qual a Intel fornece uma fila de pré-busca em todos os modelos 80x86, a partir do 8088. Nestes processadores, a BIU constantemente busca dados para a fila de pré-busca sempre que o programa não estiver lendo ou escrevendo dados. As filas de busca antecipada funcionam melhor quando existir um barramento de dados largo. O processador 8286 roda muito mais rápido do que o 886 porque é capaz de manter a fila de pré-busca cheia. Entretanto, observe as seguintes instruções:

	100:	mov     ax, [1000]
	105:	mov     bx, [2000]
	10A:	mov     cx, [3000]

Como os registradores ax, bx, cx e dx são de 16 bits, aqui está o que acontece (assumindo que a primeira instrução esteja na fila de pré-busca e decodificada):

Fim da primeira instrução. Há dois bytes na fila de pré-busca.

Fim da segunda instrução. Há três bytes na fila de pré-busca.

Como podemos ver, a segunda instrução precisa de um ciclo de clock a mais do que as outras duas. Isto ocorre porque a BIU não pode preencher a fila de pré-busca na mesma velocidade com que a CPU executa as instruções. O problema fica ainda mais acentuado quando o tamanho da fila de busca antecipada estiver limitado a um certo número de bytes. Este problema não ocorre no processador 8286 mas, quase que com certeza, ocorre nos processadores 80x86. Logo veremos que os processadores 80x86 tendem a esvaziar a fila de pré-busca com certa facilidade. É claro que, quando a fila de pré-busca está vazia, a CPU precisa esperar que a BIU busque novos opcodes na memória e o programa fica mais lento. Executar instruções mais curtas ajuda a manter a fila de pré-busca preenchida. Por exemplo, o 8286 é capaz de carregar instruções de dois bytes com um único ciclo de memória, mas leva 1.5 ciclo de clock para buscar uma única instrução de três bytes. A execução de quatro instruções de um byte normalmente é mais demorada do que a execução de uma instrução de três bytes, mas isto dá tempo para que a fila de pré-busca seja preenchida e que novas instruções sejam decodificadas. Em sistemas que possuam uma fila de busca antecipada é possível encontrar oito instruções de dois bytes que operam mais rápido do que um conjunto equivalente de quatro instruções de quatro bytes. O motivo é que a fila de pré-busca tem tempo de ser preenchida quando as instruções são mais curtas. Moral da história: quando se programa para um processador com uma fila de busca antecipada, usar sempre as instruções mais curtas possíveis para realizar as tarefas necessárias.

Comentários

Está intoxicado com as etapas de execução? Não se preocupe, o importante é ter entendido a lógica de "comer" ciclos e a importância da fila de busca antecipada. Apesar de representar um grande avanço, isto ainda não é tudo. Prepare-se para os processadores 8486 e 4686 ;))))



| AAAA | Página Inicial | Mapa do Site | Novidades | Busca | Indique esta página | Mestre da Teia | Voltar |
| Localizador || @ Info NumaBoa > Assembly NumaBoa > Processadores hipotéticos > Processador 886 > Processador 8486
Autoria: Randall Hyde - Art of Assembly Language Programming. Tradução: vovó Vicki

webdesign sobMedida by vickiSoft - /informatica/assembly/cap3_4.php (21.01.04) versão 1.0 de 22.01.04
Licença Creative Commons 1998-2006 Aldeia NumaBoa
Exceto onde especificamente declarado, todo material deste site é disponibilizado de acordo com a Licença Creative Commons.