operators - operadores - rotacion de bits en c




¿Qué son los operadores de cambio de bit(cambio de bit) y cómo funcionan? (6)

He intentado aprender C en mi tiempo libre, y otros idiomas (C #, Java, etc.) tienen el mismo concepto (y, a menudo, los mismos operadores) ...

Lo que me pregunto es, a nivel central, ¿qué hace el desplazamiento de bits (<<, >>, >>>), qué problemas puede ayudar a resolver y qué errores se esconden en la curva? En otras palabras, una guía absoluta para principiantes sobre el cambio de bits en toda su bondad.


Enmascaramiento y desplazamiento de bits

El desplazamiento de bits se utiliza a menudo en la programación de gráficos de bajo nivel. Por ejemplo, un valor de color de píxel dado codificado en una palabra de 32 bits.

 Pixel-Color Value in Hex:    B9B9B900
 Pixel-Color Value in Binary: 10111001  10111001  10111001  00000000

Para una mejor comprensión, el mismo valor binario etiquetado con qué secciones representa qué parte del color.

                                 Red     Green     Blue       Alpha
 Pixel-Color Value in Binary: 10111001  10111001  10111001  00000000

Digamos, por ejemplo, que queremos obtener el valor verde de este color de píxeles. Podemos obtener fácilmente ese valor enmascarando y cambiando .

Nuestra mascara

                  Red      Green      Blue      Alpha
 color :        10111001  10111001  10111001  00000000
 green_mask  :  00000000  11111111  00000000  00000000

 masked_color = color & green_mask

 masked_color:  00000000  10111001  00000000  00000000

El operador & lógico se asegura de que solo se mantengan los valores donde la máscara es 1. Lo último que tenemos que hacer ahora es obtener el valor entero correcto desplazando todos esos bits a la derecha en 16 lugares (desplazamiento lógico a la derecha) .

 green_value = masked_color >>> 16

Y luego, tenemos el entero que representa la cantidad de verde en el color de los píxeles:

 Pixels-Green Value in Hex:     000000B9
 Pixels-Green Value in Binary:  00000000 00000000 00000000 10111001 
 Pixels-Green Value in Decimal: 185

Esto se usa a menudo para codificar o decodificar formatos de imagen como jpg , png , ...


Digamos que tenemos un solo byte:

0110110

Aplicando un solo bithift izquierdo nos consigue:

1101100

El cero de la izquierda se desplazó del byte y se agregó un nuevo cero al extremo derecho del byte.

Los bits no vuelven; son descartados. Eso significa que si cambia a la izquierda 1101100 y luego a la derecha, no obtendrá el mismo resultado.

Desplazar a la izquierda por N es equivalente a multiplicar por 2 N.

Desplazar hacia la derecha por N es (si está utilizando el complemento de unos ) equivale a dividir por 2 N y redondear a cero.

Bitshifting se puede usar para la multiplicación y división increíblemente rápidas, siempre que esté trabajando con una potencia de 2. Casi todas las rutinas de gráficos de bajo nivel utilizan el cambio de bits.

Por ejemplo, hace mucho tiempo atrás, usamos el modo 13h (320x200 256 colores) para los juegos. En el Modo 13h, la memoria de video se distribuyó secuencialmente por píxel. Eso significaba que para calcular la ubicación de un píxel, usaría los siguientes cálculos matemáticos:

memoryOffset = (row * 320) + column

Ahora, en esa época, la velocidad era crítica, por lo que usaríamos bitshifts para realizar esta operación.

Sin embargo, 320 no es una potencia de dos, por lo tanto, para solucionar esto, tenemos que descubrir qué potencia de dos suman a 320:

(row * 320) = (row * 256) + (row * 64)

Ahora podemos convertir eso en turnos de izquierda:

(row * 320) = (row << 8) + (row << 6)

Para un resultado final de:

memoryOffset = ((row << 8) + (row << 6)) + column

Ahora obtenemos el mismo desplazamiento que antes, excepto que en lugar de una operación de multiplicación costosa, usamos los dos cambios de bits ... en x86 sería algo como esto (nota, ha pasado una eternidad desde que hice el ensamblaje (nota del editor: corregida un par de errores y añadió un ejemplo de 32 bits)):

mov ax, 320; 2 cycles
mul word [row]; 22 CPU Cycles
mov di,ax; 2 cycles
add di, [column]; 2 cycles
; di = [row]*320 + [column]

; 16-bit addressing mode limitations:
; [di] is a valid addressing mode, but [ax] isn't, otherwise we could skip the last mov

Total: 28 ciclos en cualquier CPU antigua que tuviera estos tiempos.

Vrs

mov ax, [row]; 2 cycles
mov di, ax; 2
shl ax, 6;  2
shl di, 8;  2
add di, ax; 2    (320 = 256+64)
add di, [column]; 2
; di = [row]*(256+64) + [column]

12 ciclos en la misma CPU antigua.

Sí, trabajaríamos tan duro para eliminar 16 ciclos de CPU.

En el modo de 32 o 64 bits, ambas versiones son mucho más cortas y más rápidas. Las modernas CPU de ejecución fuera de orden como Intel Skylake (ver http://agner.org/optimize/ ) tienen hardware muy rápido multiplicado (baja latencia y alto rendimiento), por lo que la ganancia es mucho menor. La familia de Bulldozer de AMD es un poco más lenta, especialmente para la multiplicación de 64 bits. En las CPU Intel y AMD Ryzen, dos turnos tienen una latencia ligeramente menor pero más instrucciones que una multiplicación (lo que puede llevar a un rendimiento más bajo):

imul edi, [row], 320    ; 3 cycle latency from [row] being ready
add  edi, [column]      ; 1 cycle latency (from [column] and edi being ready).
; edi = [row]*(256+64) + [column],  in 4 cycles from [row] being ready.

contra

mov edi, [row]
shl edi, 6               ; row*64.   1 cycle latency
lea edi, [edi + edi*4]   ; row*(64 + 64*4).  1 cycle latency
add edi, [column]        ; 1 cycle latency from edi and [column] both being ready
; edi = [row]*(256+64) + [column],  in 3 cycles from [row] being ready.

Los compiladores harán esto por usted: vea cómo gcc, clang y MSVC usan shift + lea cuando optimizan el return 320*row + col; .

Lo más interesante a tener en cuenta aquí es que x86 tiene una instrucción de desplazamiento y adición ( LEA ) que puede hacer pequeños cambios a la izquierda y agregar al mismo tiempo, con la instrucción de rendimiento y add . ARM es aún más poderoso: un operando de cualquier instrucción se puede cambiar a la izquierda oa la derecha de forma gratuita. Así que escalar por una constante de tiempo de compilación que se sabe que es una potencia de 2 puede ser incluso más eficiente que una multiplicación.

De acuerdo, en los tiempos modernos ... algo más útil ahora sería usar el cambio de bits para almacenar dos valores de 8 bits en un entero de 16 bits. Por ejemplo, en C #:

// Byte1: 11110000
// Byte2: 00001111

Int16 value = ((byte)(Byte1 >> 8) | Byte2));

// value = 000011111110000;

En C ++, los compiladores deberían hacer esto por usted si utilizó una struct con dos miembros de 8 bits, pero en la práctica no siempre.


Los operadores de cambio de bits hacen exactamente lo que su nombre implica. Ellos cambian bits. Aquí hay una breve (o no tan breve) introducción a los diferentes operadores de turno.

Los operadores

  • >> es el operador de desplazamiento a la derecha aritmético (o firmado).
  • >>> es el operador de desplazamiento a la derecha lógico (o sin signo).
  • << es el operador de desplazamiento a la izquierda, y satisface las necesidades de los cambios lógicos y aritméticos.

Todos estos operadores pueden aplicarse a valores enteros ( int , long , posiblemente short y byte o char ). En algunos idiomas, la aplicación de los operadores de cambio a cualquier tipo de datos más pequeño que int redimensiona automáticamente el operando para que sea un int .

Tenga en cuenta que <<< no es un operador, porque sería redundante. También tenga en cuenta que C y C ++ no distinguen entre los operadores de desplazamiento correctos. Proporcionan solo el operador >> , y el comportamiento de desplazamiento hacia la derecha es la implementación definida para los tipos firmados.

Desplazamiento a la izquierda (<<)

Los enteros se almacenan, en memoria, como una serie de bits. Por ejemplo, el número 6 almacenado como un int 32 bits sería:

00000000 00000000 00000000 00000110

Desplazar este patrón de bits a la izquierda una posición ( 6 << 1 ) resultaría en el número 12:

00000000 00000000 00000000 00001100

Como puede ver, los dígitos se han desplazado hacia la izquierda una posición, y el último dígito de la derecha se rellena con un cero. También puede tener en cuenta que desplazarse hacia la izquierda equivale a la multiplicación por potencias de 2. Entonces 6 << 1 es equivalente a 6 * 2 , y 6 << 3 equivale a 6 * 8 . Un buen compilador de optimización reemplazará las multiplicaciones con turnos cuando sea posible.

Desplazamiento no circular

Tenga en cuenta que estos no son turnos circulares. Desplazando este valor a la izquierda una posición ( 3,758,096,384 << 1 ):

11100000 00000000 00000000 00000000

Resultados en 3,221,225,472:

11000000 00000000 00000000 00000000

El dígito que se desplaza "fuera del final" se pierde. No se envuelve alrededor.

Cambio lógico a la derecha (>>>)

Un cambio lógico a la derecha es lo contrario al cambio a la izquierda. En lugar de mover los bits a la izquierda, simplemente se mueven a la derecha. Por ejemplo, cambiando el número 12:

00000000 00000000 00000000 00001100

a la derecha por una posición ( 12 >>> 1 ) recuperaremos nuestros 6 originales:

00000000 00000000 00000000 00000110

Entonces vemos que desplazarse hacia la derecha es equivalente a la división por potencias de 2.

Los bits perdidos se han ido

Sin embargo, un cambio no puede reclamar bits "perdidos". Por ejemplo, si cambiamos este patrón:

00111000 00000000 00000000 00000110

a la izquierda 4 posiciones ( 939,524,102 << 4 ), obtenemos 2,147,483,744:

10000000 00000000 00000000 01100000

y luego retrocediendo ( (939,524,102 << 4) >>> 4 ) obtenemos 134,217,734:

00001000 00000000 00000000 00000110

No podemos recuperar nuestro valor original una vez que hemos perdido bits.

Derecha aritmética (>>)

El cambio aritmético a la derecha es exactamente igual al cambio lógico a la derecha, excepto que en lugar de rellenar con cero, rellena con el bit más significativo. Esto se debe a que el bit más significativo es el bit de signo o el bit que distingue los números positivos y negativos. Al rellenar con el bit más significativo, el cambio aritmético a la derecha conserva los signos.

Por ejemplo, si interpretamos este patrón de bits como un número negativo:

10000000 00000000 00000000 01100000

Tenemos el número -2,147,483,552. Cambiar esto a la derecha 4 posiciones con el cambio aritmético (-2,147,483,552 >> 4) nos daría:

11111000 00000000 00000000 00000110

o el número -134,217,722.

Así que vemos que hemos conservado el signo de nuestros números negativos utilizando el cambio a la derecha aritmética, en lugar del cambio a la derecha lógica. Y una vez más, vemos que estamos realizando división por potencias de 2.


Solo estoy escribiendo sugerencias y trucos, puede ser útil en las pruebas / exámenes.

  1. n = n*2 : n = n<<1
  2. n = n/2 : n = n>>1
  3. Comprobando si n es la potencia de 2 (1,2,4,8, ...): !(n & (n-1)) cheque !(n & (n-1))
  4. Obtención de x th bit de n : n |= (1 << x)
  5. Comprobando si x es par o impar: x&1 == 0 (par)
  6. Alternar el bit n de x: x ^ (1<<n)

Tenga en cuenta que solo la versión de PHP de 32 bits está disponible en la plataforma Windows.

Luego, si por ejemplo cambia << o >> más de 31 bits, los resultados son inesperados. Por lo general, se devolverá el número original en lugar de ceros, y puede ser un error realmente complicado.

Por supuesto, si usa la versión de 64 bits de PHP (unix), debe evitar los cambios de más de 63 bits. Sin embargo, por ejemplo, MySQL usa el BIGINT de 64 bits, por lo que no debería haber ningún problema de compatibilidad.

ACTUALIZACIÓN: Desde PHP7 Windows, las compilaciones php finalmente pueden usar enteros de 64 bits completos: el tamaño de un entero depende de la plataforma, aunque un valor máximo de aproximadamente dos mil millones es el valor habitual (es decir, 32 bits con signo). Las plataformas de 64 bits generalmente tienen un valor máximo de alrededor de 9E18, excepto en Windows antes de PHP 7, donde siempre fue de 32 bits.


Una consecuencia es que lo siguiente depende de la implementación (de acuerdo con el estándar ANSI):

char x = -1;
x >> 1;

x ahora puede ser 127 (01111111) o aún -1 (11111111).

En la práctica, suele ser lo último.







binary-operators