Inicialmente, escrevi este documento há vários anos, como engenheiro de verificação do núcleo de execução no ARM. Obviamente, minha opinião foi influenciada por um trabalho aprofundado com os núcleos executivos de diferentes processadores. Faça isso com desconto, por favor: talvez eu seja muito categórico.
No entanto, ainda acredito que os criadores do RISC-V poderiam fazer muito melhor. Por outro lado, se eu tivesse projetado um processador de 32 ou 64 bits hoje, provavelmente teria implementado exatamente essa arquitetura para tirar proveito das ferramentas existentes.
O artigo descreveu originalmente o conjunto de instruções RISC-V 2.0. Para a versão 2.2, ele fez algumas atualizações.
Prefácio original: algumas opiniões pessoais
O conjunto de instruções RISC-V foi reduzido ao mínimo absoluto. É prestada muita atenção à minimização do número de instruções, à normalização da codificação etc. Esse desejo de minimalismo levou a uma falsa ortogonalidade (como a reutilização da mesma instrução para transições, chamadas e retornos) e à verbosidade obrigatória, que aumenta o tamanho e a quantidade instruções.
Por exemplo, aqui está o código C:
int readidx(int *p, size_t idx) { return p[idx]; }
Este é um caso simples de indexação de uma matriz, uma operação muito comum. Esta é a compilação para x86_64:
mov eax, [rdi+rsi*4] ret
ou BRAÇO:
ldr r0, [r0, r1, lsl #2] bx lr // return
No entanto, para o RISC-V, o seguinte código é necessário:
slli a1, a1, 2 add a0, a1, a1 lw a0, a0, 0 jalr r0, r1, 0 // return
Simplificação O RISC-V simplifica o decodificador (isto é, o front-end da CPU) executando mais instruções. Mas escalar a largura do pipeline é um problema difícil, enquanto a decodificação de instruções irregulares (ou fortemente) irregulares é bem implementada (a principal dificuldade surge quando é difícil determinar o comprimento da instrução: isso é especialmente evidente no conjunto de instruções x86 com vários prefixos).
A simplificação do conjunto de instruções não deve ser levada ao limite. O registro e a adição de registros com um deslocamento da memória do registro são uma instrução simples e muito comum nos programas e é muito fácil para o processador implementá-lo efetivamente. Se o processador não puder implementar a instrução diretamente, pode ser relativamente fácil decompô-la em seus componentes; esse é um problema muito mais simples do que mesclar seqüências de operações simples.
Devemos distinguir entre instruções específicas “complexas” dos processadores CISC - instruções complicadas, raramente usadas e ineficientes - das instruções “funcionais” comuns aos processadores CISC e RISC, que combinam uma pequena sequência de operações. Estes últimos são usados com freqüência e com alto desempenho.
Implementação medíocre
- Extensibilidade quase ilimitada. Embora esse seja o objetivo do RISC-V, ele cria um ecossistema fragmentado e incompatível que deve ser gerenciado com extrema cautela.
- A mesma instrução (
JALR
) é usada para chamadas e retornos e para ramificações indiretas de registro, onde decodificação adicional é necessária para a previsão de ramificação
- Chamada:
Rd
= R1
- Retorno:
Rd
= R0
, Rs
= R1
- Transição indireta:
Rd
= R0
, Rs
≠ R1
- (Transição estranha:
Rd
≠ R0
, Rd
≠ R1
)
- A codificação com um comprimento variável do campo de gravação não é auto-sincronizada (isso geralmente é encontrado - por exemplo, um problema semelhante com x86 e Thumb-2 - mas isso causa vários problemas de implementação e segurança, por exemplo, programação orientada a reversa, ou seja, ataques ROP )
- O RV64I requer uma extensão de caractere para todos os valores de 32 bits. Isso leva ao fato de que a metade superior dos registros de 64 bits se torna impossível de usar para armazenar resultados intermediários, o que leva a um posicionamento especial desnecessário da metade superior dos registros. É mais ideal usar a extensão com zeros (já que reduz o número de comutações e geralmente pode ser otimizado rastreando o bit "zero" quando a metade superior é conhecida como zero)
- A multiplicação é opcional. Embora os blocos de multiplicação rápida possam ocupar uma área substancial em pequenos cristais, você sempre pode usar circuitos um pouco mais lentos que ativamente usam a ALU existente para vários ciclos de multiplicação.
LR
/ SC
requisitos rígidos de progressão para um subconjunto limitado de aplicativos. Embora essa restrição seja bastante rigorosa, ela potencialmente cria alguns problemas para pequenas implementações (especialmente sem cache)
- Parece um substituto para a instrução CAS, veja o comentário abaixo
- Os bits da memória FP e o modo de arredondamento estão no mesmo registro. Isso requer a serialização do canal FP, se a operação RMW for realizada para alterar o modo de arredondamento.
FP
instruções FP
são codificadas para precisão de 32, 64 e 128 bits, mas não de 16 bits (o que é muito mais comum em hardware que 128 bits)
- Isso pode ser facilmente corrigido: o código da dimensão
0b10
gratuito.
- Atualização: espaço reservado decimal apareceu na versão 2.2, mas não há espaço reservado de meia precisão. A mente é incompreensível.
- A maneira como os valores FP são representados no arquivo de registro FP não está definida, mas é observável (via carga / armazenamento)
- Autores de emuladores vão te odiar
- A migração de máquinas virtuais pode se tornar impossível
- Atualização: a versão 2.2 requer valores maiores de NaN-boxing
Ruim
- Não há códigos de condição e, em vez disso, as instruções de comparação e ramificação são usadas. Isso não é um problema em si, mas as consequências são desagradáveis:
- Espaço de codificação reduzido em ramificações condicionais devido à necessidade de codificar um ou dois especificadores de registro
- Nenhuma opção condicional (útil para transições muito imprevisíveis)
- Nenhuma transferência / subtração com transferência ou empréstimo
- (Observe que isso ainda é melhor do que conjuntos de comandos que gravam sinalizadores no registro geral e depois alternam para os sinalizadores recebidos)
- Parece que os contadores de alta precisão (ciclos de hardware) são necessários em um ISA sem privilégios. Na prática, fornecer aplicativos a eles é um excelente vetor para ataques a canais de terceiros
- Multiplicação e divisão fazem parte da mesma extensão, e parece que, se uma é implementada, a outra também deve ser. A multiplicação é muito mais simples que a divisão e é comum na maioria dos processadores, mas a divisão não é.
- Não há instruções atômicas na arquitetura básica do conjunto de instruções. Microcontroladores de múltiplos núcleos estão se tornando mais comuns, portanto instruções atômicas como LL / SC são baratas (para implementação mínima em um único processador [de vários núcleos], é necessário apenas 1 bit de estado do processador)
LR
/ SC
estão na mesma extensão que instruções atômicas mais complexas, o que limita a flexibilidade para pequenas implementações
- Instruções atômicas gerais (não
LR
/ SC
) não incluem CAS
primitivo
- O
CmpHi:CmpLo
evitar a necessidade de uma instrução que leia cinco registros ( Addr
, CmpHi:CmpLo
, SwapHi:SwapLo
), mas isso provavelmente imporá menos sobrecarga de implementação do que o LR
/ SC
avançado garantido, que é fornecido como substituições
- São fornecidas instruções atômicas que funcionam com valores de 32 e 64 bits, mas não de 8 ou 16 bits
- Para o RV32I, não há como transferir o valor DP FP entre um número inteiro e um arquivo de registro FP, exceto através da memória, ou seja, dos registros inteiros de 32 bits, é impossível criar um número de ponto flutuante de precisão dupla de 64 bits; você deve primeiro gravar o valor intermediário na memória e carregar ele no arquivo de registro de lá
- Por exemplo, a instrução
ADD
32 bits no RV32I e o ADD
64 bits no RVI64 têm as mesmas codificações e, no RVI64, é ADD.W
outra codificação ADD.W
Essa é uma complicação desnecessária para um processador que implementa ambas as instruções - seria preferível adicionar uma nova codificação de 64 bits.
- Nenhuma instrução
MOV
. O código mnemônico do comando MV
é traduzido pelo assembler na instrução MV rD, rS
-> ADDI rD, rS, 0
. Os processadores de alto desempenho normalmente otimizam as instruções MOV
, enquanto reordenam extensivamente as instruções. Uma instrução com um operando direto de 12 bits foi escolhida como a forma canônica da instrução MV
no RISC-V.
- Na ausência de
MOV
a instrução ADD rD, rS, r0
torna-se preferível ao MOV
canônico, pois é mais fácil de decodificar, e as operações com registro zero (r0) na CPU geralmente são otimizadas
Horrível
JAL
gasta 5 bits na codificação do registro de comunicação, que é sempre igual a R1
(ou R0
para transições)
- Isso significa que o RV32I usa deslocamento de ramificação de 21 bits. Isso não é suficiente para aplicativos grandes - por exemplo, navegadores da web - sem usar várias seqüências de comandos e / ou "ilhas de ramificação"
- Esta é uma deterioração em comparação com a versão 1.0 da arquitetura de comandos!
- Apesar do grande esforço para codificar de maneira uniforme, as instruções de carregamento / armazenamento são codificadas de maneira diferente (os casos e os campos imediatos mudam)
- Aparentemente, a ortogonalidade de codificação do registro de saída era preferível à ortogonalidade de codificação de duas instruções fortemente relacionadas. Essa escolha parece um pouco estranha, pois a geração de endereços é mais crítica em termos de tempo.
- Não há instruções de carregamento de memória com compensações de registro (
Rbase
+ Roffset
) ou índices ( Rbase
+ Rindex
<< Scale
).
FENCE.I
implica uma sincronização completa do cache de instruções com todos os repositórios anteriores, com ou sem proteção. As implementações precisam limpar todos os I $ em cima do muro ou procurar D $ e o buffer de armazenamento
- No RV32I, a leitura dos contadores de 64 bits requer a leitura da metade superior duas vezes, comparação e ramificação no caso de transferência entre a metade inferior e a parte superior durante uma operação de leitura
- Normalmente, os ISAs de 32 bits incluem uma instrução de leitura de par de registro especial para evitar esse problema.
- Não há espaço definido arquitetonicamente para a codificação de dicas, para que as instruções desse espaço não causem erros nos processadores mais antigos (processados como
NOP
), mas façam algo nas CPUs mais modernas
- Exemplos típicos de dicas de NOP puros são coisas como rendimento de spinlock
- Os processadores mais recentes também têm dicas mais sofisticadas (com efeitos colaterais visíveis nos processadores mais recentes; por exemplo, instruções de verificação de borda x86 são codificadas no espaço de dicas, para que os binários permaneçam compatíveis com versões anteriores)