Se os registros são tão incrivelmente rápidos, por que não temos mais deles?

88

Em 32 bits, tínhamos 8 registradores de "uso geral". Com 64 bits, a quantidade dobra, mas parece independente da própria mudança de 64 bits.
Agora, se os registradores são tão rápidos (sem acesso à memória), por que não existem mais naturalmente? Os construtores de CPU não deveriam trabalhar tantos registros quanto possível na CPU? Qual é a restrição lógica para a razão de termos apenas a quantia que temos?

Xeo
fonte
CPUs e GPUs ocultam latência principalmente por caches e massivo multithreading respectivamente. Portanto, as CPUs têm (ou precisam) poucos registros, enquanto as GPUs têm dezenas de milhares de registros. Veja meu artigo de pesquisa sobre o arquivo de registro da GPU, que discute todas essas vantagens e desvantagens e fatores.
user984260

Respostas:

119

Há muitos motivos pelos quais você não tem apenas um grande número de registros:

  • Eles estão altamente ligados à maioria dos estágios do pipeline. Para começar, você precisa rastrear sua vida útil e encaminhar os resultados de volta aos estágios anteriores. A complexidade se torna intratável muito rapidamente e o número de fios (literalmente) envolvidos cresce na mesma proporção. É caro na área, o que significa que é caro em energia, preço e desempenho depois de um certo ponto.
  • Ele ocupa espaço de codificação de instruções. 16 registradores ocupam 4 bits para origem e destino, e outros 4 se você tiver instruções de 3 operandos (por exemplo, ARM). É uma quantidade enorme de espaço de codificação de conjunto de instruções, ocupada apenas para especificar o registro. Isso eventualmente afeta a decodificação, o tamanho do código e novamente a complexidade.
  • Existem melhores maneiras de obter o mesmo resultado ...

Atualmente, temos muitos registradores - eles apenas não estão programados explicitamente. Temos "renomeação de registro". Enquanto você acessa apenas um pequeno conjunto (8-32 registradores), eles na verdade são apoiados por um conjunto muito maior (por exemplo, 64-256). A CPU então rastreia a visibilidade de cada registro e os aloca para o conjunto renomeado. Por exemplo, você pode carregar, modificar e, em seguida, armazenar em um registro muitas vezes seguidas e ter cada uma dessas operações realmente executada de forma independente, dependendo das falhas de cache etc. No ARM:

ldr r0, [r4]
add r0, r0, #1
str r0, [r4]
ldr r0, [r5]
add r0, r0, #1
str r0, [r5]

Os núcleos do Cortex A9 registram a renomeação, então a primeira carga para "r0" na verdade vai para um registro virtual renomeado - vamos chamá-lo de "v0". O carregamento, incremento e armazenamento acontecem na "v0". Enquanto isso, também executamos um carregamento / modificação / armazenamento em r0 novamente, mas isso será renomeado para "v1" porque esta é uma sequência totalmente independente usando r0. Digamos que a carga do ponteiro em "r4" parou devido a uma falha no cache. Tudo bem - não precisamos esperar que "r0" esteja pronto. Por ser renomeado, podemos executar a próxima sequência com "v1" (também mapeado para r0) - e talvez seja um acerto de cache e acabamos de ter uma grande vitória de desempenho.

ldr v0, [v2]
add v0, v0, #1
str v0, [v2]
ldr v1, [v3]
add v1, v1, #1
str v1, [v3]

Eu acho que o x86 é até um número gigantesco de registros renomeados atualmente (estimativa 256). Isso significaria ter 8 bits vezes 2 para cada instrução apenas para dizer qual é a origem e o destino. Isso aumentaria enormemente o número de fios necessários ao longo do núcleo e seu tamanho. Portanto, há um ponto ideal em torno de 16-32 registradores que a maioria dos designers se conformaram, e para designs de CPU fora de ordem, a renomeação de registradores é a maneira de mitigar isso.

Editar : A importância da execução fora de ordem e renomeação de registro neste. Depois de ter OOO, o número de registros não importa muito, porque eles são apenas "marcas temporárias" e são renomeados para o conjunto de registros virtuais muito maior. Você não quer que o número seja muito pequeno, porque fica difícil escrever pequenas sequências de código. Este é um problema para x86-32, porque os 8 registros limitados significam que muitos temporários acabam passando pela pilha, e o núcleo precisa de lógica extra para encaminhar leituras / gravações para a memória. Se você não tem OOO, geralmente está falando de um núcleo pequeno; nesse caso, um grande conjunto de registros é um benefício de baixo custo / desempenho.

Portanto, há um ponto ideal natural para o tamanho do banco de registradores que atinge o máximo em cerca de 32 registradores arquitetados para a maioria das classes de CPU. x86-32 tem 8 registros e é definitivamente muito pequeno. ARM foi com 16 registros e é um bom compromisso. 32 registros é um pouco demais - você acaba não precisando dos últimos 10 ou mais.

Nada disso afeta os registros extras que você obtém para SSE e outros coprocessadores de ponto flutuante vetorial. Eles fazem sentido como um conjunto extra porque são executados independentemente do núcleo inteiro e não aumentam a complexidade da CPU exponencialmente.

John Ripley
fonte
12
Excelente resposta - eu gostaria de acrescentar outro motivo à mistura - quanto mais registros alguém tem, mais tempo leva para colocá-los / retirá-los da pilha durante a troca de contexto. Definitivamente, não é o problema principal, mas uma consideração.
Will A
7
@WillA bom ponto. No entanto, arquiteturas com muitos registros têm maneiras de mitigar esse custo. A ABI normalmente terá callee-save da maioria dos registradores, então você só precisa salvar um conjunto básico. A troca de contexto é geralmente cara o suficiente para que o salvamento / restauração extra não custem muito em comparação com todas as outras burocracias. O SPARC, na verdade, contorna isso tornando o banco de registradores uma "janela" em uma área de memória, de forma que ele se adapte a isso (meio que acenando com a mão).
John Ripley
4
Considere minha mente explodida por uma resposta tão completa que eu com certeza não esperava. Além disso, obrigado pela explicação de por que não precisamos de tantos registradores nomeados, isso é muito interessante! Gostei muito de ler sua resposta, porque estou totalmente interessado no que está acontecendo "por baixo do capô". :) Vou esperar um pouco mais antes de aceitar uma resposta, porque nunca se sabe, mas o meu +1 é certo.
Xeo
1
independentemente de onde reside a responsabilidade de salvar registros, o tempo que leva é a sobrecarga administrativa. OK, então a troca de contexto pode não ser o caso mais frequente, mas as interrupções são. Rotinas codificadas manualmente podem economizar nos registradores, mas se os drivers forem escritos em C, as chances são de que a função declarada pela interrupção salvará todos os registradores, chamará o isr e restaurará todos os registradores salvos. IA-32 teve uma vantagem de interrupção com seus 15-20 regs em comparação com 32 + algo regs de arquiteturas RISC.
Olof Forshell
1
Excelente resposta, mas discordarei da comparação direta de registros "renomeados" com registros endereçáveis ​​"reais". No x86-32, mesmo com 256 registros internos, você não pode usar mais de 8 valores temporários armazenados em registros em qualquer ponto de execução. Basicamente, a renomeação de registradores é apenas um curioso subproduto do OOE, nada mais.
noop
12

Nós Não ter mais deles

Como quase todas as instruções devem selecionar 1, 2 ou 3 registros arquitetonicamente visíveis, expandir o número deles aumentaria o tamanho do código em vários bits em cada instrução e, assim, reduziria a densidade do código. Também aumenta a quantidade de contexto que deve ser salvo como estado de thread e parcialmente salvo no registro de ativação de uma função . Essas operações ocorrem com freqüência. Os intertravamentos do pipeline devem verificar um placar para cada registro e isso tem tempo quadrático e complexidade espacial. E talvez o maior motivo seja simplesmente a compatibilidade com o conjunto de instruções já definido.

Mas acontece que, graças à renomeação de registros , realmente temos muitos registros disponíveis e nem mesmo precisamos salvá-los. A CPU, na verdade, possui muitos conjuntos de registros e alterna automaticamente entre eles conforme o código é executado. Ele faz isso apenas para obter mais registros.

Exemplo:

load  r1, a  # x = a
store r1, x
load  r1, b  # y = b
store r1, y

Em uma arquitetura que tem apenas r0-r7, o código a seguir pode ser reescrito automaticamente pela CPU como algo como:

load  r1, a
store r1, x
load  r10, b
store r10, y

Nesse caso, r10 é um registro oculto que é substituído por r1 temporariamente. A CPU pode dizer que o valor de r1 nunca é usado novamente após o primeiro armazenamento. Isso permite que o primeiro carregamento seja atrasado (mesmo um acerto de cache no chip geralmente leva vários ciclos) sem exigir o atraso do segundo carregamento ou do segundo armazenamento.

DigitalRoss
fonte
2

Eles adicionam registros o tempo todo, mas geralmente estão vinculados a instruções para fins especiais (por exemplo, SIMD, SSE2, etc.) ou exigem a compilação para uma arquitetura de CPU específica, o que diminui a portabilidade. As instruções existentes geralmente funcionam em registros específicos e não poderiam tirar proveito de outros registros se eles estivessem disponíveis. Conjunto de instruções legado e tudo.

Seth Robertson
fonte
1

Para adicionar informações interessantes aqui, você notará que ter 8 registradores do mesmo tamanho permite que os opcodes mantenham consistência com a notação hexadecimal. Por exemplo, a instrução push axé opcode 0x50 em x86 e vai até 0x57 para o último registro di. Em seguida, a instrução pop axcomeça em 0x58 e vai até 0x5F pop dipara completar a primeira base-16. A consistência hexadecimal é mantida com 8 registros por tamanho.


fonte
2
Em x86 / 64, os prefixos de instrução REX estendem os índices de registro com mais bits.
Alexey Frunze