Me parecía oportuno hacer un inciso técnico sobre algo que llevo varios días mostrando en los streamings de live coding que estoy haciendo estos días: las máscaras de bit y los desplazamientos.
Una de las construcciones que más abundan el código fuente de mi emulador del CHIP-8, el cual estoy alojando en GitHub bajo una licencia GPL, es el aplicar desplazamiento y una máscara de bits sobre un resultado, por ejemplo:
mem.pc = (mem.pc + 2) & 0xFFF;
uint8_t n = opcode & 0xF;
uint8_t x = (opcode >> 8) & 0xF;
Asumiré que quien esté leyendo esto conoce más o menos cómo trabajar con bits. En cualquier caso como referencia quedan los dos episodios (el primero y el segundo) que dediqué a bits en mi videotutorial de C que grabé hace un par de años.
En el caso de las dos primeras operaciones las máscaras de bit las uso para limitar el número de bits que me quedo de la variable. Por ejemplo, en el primer caso, mem.pc es una variable de 16 bits. Pero puesto que la CHIP-8 sólo puede direccionar a 12 bits, debo asegurarme que no se guarda un valor de más de 12 bits. Puesto que 0xFFF son 12 unos, sólo los 12 bits menos significativos del contador de programa se guardarán. Los cuatro bits más significativos se perderán porque al hacer el AND, en la máscara están en 0.
Por ejemplo, supongamos que incremento un contador de programa que inicialmente vale 0x0FFE. Al sumarle 2, mi contador de programa pasa a valer 0x1000. Al hacer 0x1000 & 0x0FFF, puesto que & es un operador que hace el AND bit a bit, el resultado que obtengo es 0x000. ¿Otro ejemplo? Veamos: (0x0FFF + 2) & 0x0FFF = 0x1001 & 0x0FFF = 0x0001.
En el segundo ejemplo estoy haciendo lo mismo. La única diferencia es que la máscara de bit es 0xF, por lo que me quedo con los últimos 4 bits menos significativos del opcode. Por lo que supongamos que el opcode que he leído es 0xABCD. Al hacerle el AND, 0xABCD & 0x000F = 0x000D.
El tercer caso utiliza además desplazamientos. Lo hago porque en ese caso quiero quedarme con un valor que no está en los bits menos significativos. Lo que hago es desplazarlo a la derecha, para que sí esté en los bits menos significativos, y ya entonces le aplico la máscara. Por ejemplo, supongamos que el opcode es 0xABCD. Pues (0xABCD >> 8) & 0x000F = 0x00AB & 0x000F = 0x000B.
Una forma alternativa de hacer esta última operación aritmética hubiese sido al revés: en vez de desplazar y luego aplicar AND, primero aplicar el AND y luego desplazar a la derecha. En ese caso lo que tendría que hacer es aplicarle una máscara binaria que sólo deje activos los bits que me interesa. Por ejemplo, la tercera operación es reescribible como:
uint8_t x = (opcode & 0xF00) >> 8
En este caso la máscara de bit dejará activos los bits 8 a 11 del opcode. Luego al desplazar a la derecha esos valores pasarán a estar en las posiciones menos significativas. Por ejemplo, si el opcode es 0xABCD, lo que se hará es (0xABCD & 0x0F00) >> 8 = 0x0B00 >> 8 = 0x000B.
Y esta es más o menos la esencia que hay detrás de mis operadores de bit. ¿Por qué hacerlo así y no de otro modo? Por ejemplo, en vez de hacer esa máscara de bit al contador de programa podría haber hecho simplemente:
mem.pc = mem.pc + 2;
if (mem.pc > 0xFFF) {
mem.pc = mem.pc - 0xFFF;
}
Es claramente más simple y más fácil de comprender por alguien que no tenga mucha experiencia con máscaras de bit. Pero por otro lado es mucho más lento. Las operaciones de bit las puede hacer el procesador en una única instrucción. Las sumas, las restas y los condicionales suelen necesitar más instrucciones máquina en nuestro ordenador. Para un emulador muy simple como el de un CHIP-8 igual no hace falta ser tan exquisito con las optimizaciones porque la máquina emulada es lenta y nuestro ordenador físico tiene varios núcleos, pero cuando emulamos arquitecturas más grandes y complejas, puede significar bastante.
No hay comentarios:
Publicar un comentario