"Encuentra una razón para todo y entenderás mucho"
Quizás mis lectores habituales (bueno, no puede ser que no lo fueran) recuerden que en mi publicación me quedé perplejo porque el atributo unsigned se usó para describir los registros de dispositivos externos. En los comentarios, se sugirió que esto se hiciera para evitar un comportamiento indefinido durante los turnos y acepté. Como descubrí recientemente, hay otra razón para este uso del atributo, y puede aplicarse no solo a los registros, sino también a las variables ordinarias.
Entonces, estamos empezando.
Para empezar, una pequeña introducción al hierro.Como plataforma de destino, consideraremos un MK de 8 bits sin batería (este es un intento tan patético de ocultar el nombre AVR comprometido), que tiene los siguientes comandos implementados en hardware:
lsl / lsr desplazamiento lógico izquierda / derecha, se borra el bit bajo / alto;
rol / ror desplazamiento cíclico izquierda / derecha mediante transferencia (desplazamiento de 9 bits);
asr cambio aritmético a la derecha, se guarda el bit más significativo (con signo) (prestamos atención al hecho de que realizar este tipo de cambio a la izquierda es generalmente imposible en principio).
Todos estos comandos se ejecutan en el byte operando y son la base para la implementación de todos los otros cambios posibles. Por ejemplo, la siguiente secuencia implementa un cambio de palabra (2 bytes rh, rl) con un signo a la derecha de 1 dígito:
asr rh; ror rl;
Considere un ejemplo de código simple y el código de ensamblador correspondiente para MK con el sistema de comando AVR, como siempre, obtenido en godbolt.org. (implica que la optimización está habilitada y la variable se encuentra en el registro r24)
int8_t byte; byte = byte << 1;
clr r25 sbrc r24,7 com r25 lsl r24 rol r25
y ves que la operación lleva cinco equipos?
Nota: Si alguien en los comentarios le dice cómo organizar este fragmento (y los siguientes) en 2 columnas, se lo agradeceré.
Se puede ver en el código del ensamblador que la variable de byte se expande a un tipo entero (16 bits) en los primeros tres comandos, y en los dos siguientes, el número de doble byte se desplaza realmente; de alguna manera es extraño, por decir lo menos.
Desplazarse a la derecha no es mejor
byte = byte >> 1; clr r25 sbrc r24,7 com r25 asr r25 ror r24
- Los mismos cinco equipos. Mientras tanto, es obvio que, de hecho, para realizar la última operación, necesita un solo comando
sr r24
y para la primera operación no más. En repetidas ocasiones he declarado que el compilador actualmente está creando un código de ensamblador no peor que un programador (aunque era un sistema de comando ARM), especialmente si lo ayudas un poco, y de repente es un fastidio. Pero intente ayudar al compilador a crear el código correcto, tal vez se trate de mezclar tipos en una operación de turno e intente
byte = byte >> (int8_t) 1;
- no ayudó, de la palabra "completamente", pero la opción
byte=(uint8_t) byte >> 1;
da un resultado ligeramente mejor
ldi r25,lo8(0) asr r25 ror r24
- tres equipos, dado que la expansión general ahora ocupa un equipo - es mejor, aunque no perfecto, la misma imagen para
byte=(uint8_t) byte << 1;
- tres equipos Bueno, para no escribir lanzamientos adicionales, hacemos que la variable en sí misma no esté firmada
uint8_t byteu;
y BINGO: el código de ensamblador cumple totalmente con nuestras expectativas
byteu = byteu << 1; lsr r24
Es extraño cómo parecería, qué diferencia, indicar el tipo correcto de una variable de inmediato, o llevarlo directamente a una operación, pero resulta que hay una diferencia.
Otros estudios mostraron que el código del ensamblador tiene en cuenta el tipo de variable a la que se asigna el resultado, ya que
byteu = byte << 1;
funciona bien y produce un código mínimo, y la opción
byte = byteu << 1;
no puede prescindir de tres equipos.
Seguramente este comportamiento se describe en el estándar del lenguaje, les pregunto a los que saben en el comentario, pero una vez más declararé con orgullo que "el Chukchi no es un lector" y continuaré la historia.
Entonces, tal técnica no ayudó a cambiar a la derecha, como antes, había 3 equipos (bueno, que no son 5, en cuanto a la versión de signos) y no pude mejorar el resultado de ninguna manera.
Pero en cualquier caso, vemos que las operaciones de turno con un número sin signo se llevan a cabo más rápido que con su oponente. Por lo tanto, si no vamos a tratar el bit de orden superior de un número como un signo (y en el caso de los registros, este suele ser el caso), entonces definitivamente debemos agregar el atributo sin signo, que haremos en el futuro.
Resulta que con los cambios en general, todo es extremadamente interesante, comencemos a aumentar el número de posiciones al cambiar a la izquierda y ver los resultados: << 1 toma 1 ciclo de reloj, << 2 - 2, << 3 - 3, 4 - 2 inesperadamente, el compilador aplicó una optimización difícil
swap r24 andi r24,lo8(-16)
donde el comando s
wap intercambia dos mordiscos en un byte. Además, basado en la última optimización << 5 - 3, << 6 - 4, << 7 - 3 nuevamente inesperadamente, hay otra optimización
ror r24 clr r24 ror r24
se utiliza el bit de transferencia, << 8 - 0 medidas, ya que simplemente resulta 0, no tiene sentido buscar más.
Por cierto, aquí hay una tarea interesante para usted: por qué tiempo mínimo puede realizar una operación
uint16_t byteu; byteu = byteu << 4;
que traduce 0x1234 a 0x2340. La solución obvia es ejecutar un par de comandos 4 veces
lsl rl rol rh
conduce a 4 * 2 = 8 medidas, rápidamente se me ocurrió una opción
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 requiere 7 medidas y un registro intermedio. Entonces, el compilador genera un código de 6 comandos y no hay registros intermedios, genial, sí.
Oculto este código debajo del spoiler; intente encontrar una solución usted mismo.Sugerencia: en el conjunto de comandos MK hay un comando O EXCLUSIVO o una CANTIDAD TOTAL DOS
oAquí está, este maravilloso código swap rl ; 1243 swap rh ; 2143 andi rh,0xf0 ; 2043 eor rh,rl ; 6343 andi r2l,0xf0 ; 6340 eor rh,rl ; 2340
Solo obtengo placer estético de este fragmento.
Por lo general, para los números de 16 bits, la diferencia entre el código para los números con signo y sin signo desapareció cuando se desplazó hacia la izquierda, es extraño así.
Volvamos a nuestros bytes y comencemos a movernos a la derecha. Como recordamos, para un byte firmado tenemos 5 ciclos de reloj, para un byte sin signo - 3 y este tiempo no se puede reducir. O de todos modos, puede, sí, puede, pero es una forma muy extraña (GCC con optimizaciones activadas: "este es un lugar muy extraño"), es decir
byteu = (byteu >> 1) & 0x7F;
que genera exactamente un comando para ambas variantes del signo. Conveniente y opción
byteu = (byteu & 0xFE) >> 1;
pero solo para un número sin signo, con un signo todo se vuelve aún más deprimente: 7 medidas, por lo que continuamos explorando solo la primera opción.
No puedo decir que entiendo lo que está sucediendo, porque es obvio que la multiplicación lógica (&) por una constante después de tal cambio no tiene ningún sentido (y no lo hace), pero la presencia de la operación & afecta el código del cambio en sí. "Ves al gopher, no, y yo no veo, pero él sí".
Los cambios en 2 y así sucesivamente mostraron que es importante pagar el bit de signo, pero el número inicialmente no está firmado, en general, se obtiene algo de basura, "pero funciona", es lo único que se puede decir al respecto.
Sin embargo, es seguro decir que interpretar el contenido de los registros y la memoria como números sin signo le permite realizar una serie de operaciones (por ejemplo, desplazar o expandir un valor) con ellos más rápido y genera un código más compacto, por lo que puede ser muy recomendable para escribir programas para MK, a menos que sea diferente (la interpretación como un número es familiar) no es un requisito previo.