Sobre a questão de turnos, sinais e velocidade MK

“Encontre uma razão para tudo e você entenderá muito”


Talvez meus leitores regulares (bem, não é possível que não tenham lembrado) que, em minha postagem, fiquei perplexo com o fato de o atributo não assinado ter sido usado para descrever os registros de dispositivos externos. Nos comentários, sugeriu-se que isso fosse feito para evitar comportamentos indefinidos durante os turnos, e eu concordei. Como descobri recentemente, há outra razão para esse uso do atributo, e ele pode ser aplicado não apenas a registros, mas também a variáveis ​​comuns.

Então, nós estamos começando.

Para iniciantes, uma pequena introdução ao ferro
Como plataforma de destino, consideraremos um MK de 8 bits sem bateria (essa é uma tentativa patética de ocultar o nome comprometido AVR), que possui os seguintes comandos implementados por hardware:

lsl / lsr deslocamento lógico esquerda / direita, bit baixo / alto é limpo;
deslocamento esquerdo / direito cíclico rol / ror através de transferência (deslocamento 9 bits);
Como o deslocamento aritmético para a direita, o bit mais significativo (assinado) é salvo (prestamos atenção ao fato de que executar esse tipo de deslocamento para a esquerda é geralmente impossível em princípio).

Todos esses comandos são executados no operando de bytes e são a base para a implementação de todas as outras mudanças possíveis. Por exemplo, um deslocamento de palavra (2 bytes rh, rl) com um sinal à direita de 1 dígito é implementado pela seguinte sequência:

asr rh; ror rl;

Considere um exemplo de código simples e o código de montagem correspondente para MK com o sistema de comando AVR, como sempre, obtido em godbolt.org. (implica que a otimização está ativada e a variável está localizada no registro r24)

int8_t byte; byte = byte << 1; 

 clr r25 sbrc r24,7 com r25 lsl r24 rol r25 

e ver que a operação leva cinco equipes?

Nota: Se alguém nos comentários lhe disser como organizar este fragmento (e subseqüentes) em 2 colunas, ficarei grato.

Pode ser visto no código do assembler que a variável byte se expande para um tipo inteiro (16 bits) nos três primeiros comandos e, nos próximos dois, o número de bytes duplos é realmente deslocado - é de alguma forma estranho, para dizer o mínimo.

Mudar para a direita não é melhor

 byte = byte >> 1; clr r25 sbrc r24,7 com r25 asr r25 ror r24 

- as mesmas cinco equipes. Enquanto isso, é óbvio que, de fato, para executar a última operação, você precisa de um único comando

 sr r24 

e para a primeira operação não mais. Afirmei repetidamente que o compilador está atualmente criando código assembler não pior do que um programador (embora fosse um sistema de comando ARM), especialmente se você o ajudar um pouco, e de repente essa chatice. Mas tente ajudar o compilador a criar o código correto, talvez seja uma questão de misturar tipos em uma operação shift e tente

 byte = byte >> (int8_t) 1; 

- não ajudou, da palavra "completamente", mas a opção

  byte=(uint8_t) byte >> 1; 

dá um resultado um pouco melhor

 ldi r25,lo8(0) asr r25 ror r24 

- três equipes, já que a expansão para o todo agora ocupa uma equipe - é melhor, embora não perfeito, a mesma imagem para

 byte=(uint8_t) byte << 1; 

- três equipes. Bem, para não escrever elencos extras, deixamos a própria variável sem sinal

 uint8_t byteu; 

e BINGO - código assembler atende plenamente às nossas expectativas

 byteu = byteu << 1; lsr r24 

É estranho como parece, que diferença, indicar imediatamente o tipo correto de uma variável ou trazê-la diretamente para uma operação - mas acontece que há uma diferença.

Estudos posteriores mostraram que o código assembler leva em consideração o tipo de variável à qual o resultado é atribuído, uma vez que

 byteu = byte << 1; 

funciona bem e produz código mínimo, e a opção

 byte = byteu << 1; 

não posso prescindir de três equipes.

Certamente, esse comportamento é descrito no padrão da linguagem, pergunto aos que conhecem o comentário, mas mais uma vez declararei orgulhosamente que "o Chukchi não é um leitor" e continuarei a história.

Então, essa técnica não ajudou a mudar para a direita - como antes, havia 3 equipes (bem. O que não é 5, como na versão para assinantes) e não pude melhorar o resultado de forma alguma.
Mas, em qualquer caso, vemos que as operações de turno com um número não assinado são realizadas mais rapidamente do que com seu oponente. Portanto, se não vamos tratar o bit de alta ordem de um número como um sinal (e no caso de registradores, esse geralmente é o caso), definitivamente precisamos adicionar o atributo não assinado, o que faremos no futuro.

Acontece que, com as mudanças em geral, tudo é extremamente interessante, vamos começar a aumentar o número de posições ao deslocar para a esquerda e observar os resultados: << 1 leva 1 ciclo de relógio, << 2 - 2, << 3 - 3, 4 - 2 inesperadamente, o compilador aplicou otimização complicada

 swap r24 andi r24,lo8(-16) 

onde o comando s wap troca duas mordidelas em um byte. Além disso, com base na última otimização << 5 - 3, << 6 - 4, << 7 - 3 novamente inesperadamente, há outra otimização

 ror r24 clr r24 ror r24 

o bit de transferência é usado, medidas << 8 - 0, uma vez que resulta em 0, não há sentido em procurar mais.

A propósito, aqui está uma tarefa interessante para você - por quanto tempo mínimo você pode executar uma operação

 uint16_t byteu; byteu = byteu << 4; 

que traduz 0x1234 para 0x2340. A solução óbvia é executar alguns comandos 4 vezes

 lsl rl rol rh 

leva a 4 * 2 = 8 medidas, eu vim rapidamente com uma opção

 swap rl ; 1243 swap rh ; 2143 andi rh,0xf0 ; 2043 mov tmp,rl andi tmp,0x0f or rh,tmp ; 2343 andi rl,0xf0 ; 2340 

que requer 7 medidas e um registro intermediário. Portanto, o compilador gera um código de 6 comandos e nenhum registrador intermediário - legal, sim.

Escondo esse código sob o spoiler - tente encontrar uma solução.
Dica: no conjunto de comandos MK, existe um comando EXCLUSIVE OR ou um VALOR TOTAL DOIS ou

Aqui está, este código maravilhoso
 swap rl ; 1243 swap rh ; 2143 andi rh,0xf0 ; 2043 eor rh,rl ; 6343 andi r2l,0xf0 ; 6340 eor rh,rl ; 2340 


Acabei de obter prazer estético deste fragmento.

Normalmente, para números de 16 bits, a diferença entre o código para os números assinados e não assinados desapareceu quando deslocada para a esquerda, é estranho assim.

Vamos voltar aos nossos bytes e começar a mover para a direita. Como lembramos, para um byte assinado, temos 5 ciclos de relógio, para um byte não assinado - 3 e esse tempo não pode ser reduzido. Ou, mesmo assim, você pode - sim, pode, mas é uma maneira muito estranha (o GCC com otimizações ativadas - "este é um lugar muito estranho"), a saber

 byteu = (byteu >> 1) & 0x7F; 

que gera exatamente um comando para as duas variantes do sinal. Apropriado e opção

  byteu = (byteu & 0xFE) >> 1; 

mas apenas para um número não assinado, com um assinado, tudo se torna ainda mais deprimente - 7 medidas, por isso continuamos a explorar apenas a primeira opção.

Não posso dizer que entendo o que está acontecendo, porque é óbvio que a multiplicação lógica (&) por uma constante após essa mudança não faz sentido (e não), mas a presença da operação & afeta o código da mudança em si. "Você vê o esquilo - não - e eu não vejo, mas ele é."

Mudanças de 2 e assim por diante mostraram que é importante pagar o bit de sinal, mas o número é inicialmente não assinado; em geral, é obtido algum lixo, "mas funciona", é a única coisa que se pode dizer sobre isso.

No entanto, é seguro dizer que interpretar o conteúdo dos registradores e da memória como números não assinados permite executar várias operações (por exemplo, alternar ou expandir um valor) com elas mais rapidamente e gerar um código mais compacto, por isso pode ser altamente recomendado para escrever programas para o MK, a menos que seja diferente (a interpretação como um número é familiar) não é um pré-requisito.

Source: https://habr.com/ru/post/pt436956/


All Articles