He estado tratando de aprender C en mi tiempo libre, y otros lenguajes (C #, Java, etc.) tienen el mismo concepto (y a menudo los mismos operadores) ...
Lo que me pregunto es, en un nivel básico, lo que hace de desplazamiento de bits ( <<
, >>
, >>>
) hacer, qué problemas puede ayudar a resolver, y qué trampas están al acecho alrededor de la curva? En otras palabras, una guía absoluta para principiantes para cambiar un poco en toda su bondad.
operators
bit-manipulation
bit-shift
binary-operators
John Rudy
fuente
fuente
Respuestas:
Los operadores de desplazamiento de bits hacen exactamente lo que su nombre implica. Cambian pedazos. 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 con signo).>>>
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 se puede aplicar a los valores enteros (
int
,long
, posiblementeshort
ybyte
ochar
). En algunos idiomas, la aplicación de los operadores de desplazamiento a cualquier tipo de datos más pequeño queint
redimensiona automáticamente el operando para que sea unint
.Tenga en cuenta que
<<<
no es un operador, ya que sería redundante.También tenga en cuenta que C y C ++ no distinguen entre los operadores de desplazamiento a la derecha . Proporcionan solo el
>>
operador, y el comportamiento de desplazamiento hacia la derecha es la implementación definida para los tipos con signo. El resto de la respuesta usa los operadores C # / Java.(En todas las implementaciones principales de C y C ++, incluidas GCC y Clang / LLVM, los
>>
tipos con signo son aritméticos. Algunos códigos asumen esto, pero no es algo que el estándar garantice. Sin embargo, no está indefinido ; el estándar requiere implementaciones para definirlo como uno Sin embargo, los desplazamientos a la izquierda de los números con signo negativo es un comportamiento indefinido (desbordamiento de entero con signo). Por lo tanto, a menos que necesite un desplazamiento aritmético a la derecha, generalmente es una buena idea hacer su desplazamiento de bits con tipos sin signo).Desplazamiento a la izquierda (<<)
Los enteros se almacenan, en la memoria, como una serie de bits. Por ejemplo, el número 6 almacenado como 32 bits
int
sería:Cambiar este patrón de bits a la posición de la izquierda (
6 << 1
) daría como resultado el número 12:Como puede ver, los dígitos se han desplazado hacia la izquierda en una posición, y el último dígito a la derecha se llena con un cero. También puede notar que desplazar a la izquierda es equivalente a la multiplicación por potencias de 2. Entonces,
6 << 1
es equivalente a6 * 2
, y6 << 3
es equivalente a6 * 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 por una posición (
3,758,096,384 << 1
):resulta en 3,221,225,472:
El dígito que se desplaza "del final" se pierde. No se envuelve.
Desplazamiento lógico a la derecha (>>>)
Un desplazamiento lógico a la derecha es el inverso al desplazamiento a la izquierda. En lugar de mover los bits hacia la izquierda, simplemente se mueven hacia la derecha. Por ejemplo, cambiando el número 12:
a la derecha por una posición (
12 >>> 1
) volverá nuestro 6 original:Entonces vemos que desplazarse hacia la derecha es equivalente a la división por potencias de 2.
Los trozos perdidos se han ido
Sin embargo, un cambio no puede reclamar bits "perdidos". Por ejemplo, si cambiamos este patrón:
a la izquierda 4 posiciones (
939,524,102 << 4
), obtenemos 2,147,483,744:y luego retrocediendo (
(939,524,102 << 4) >>> 4
) obtenemos 134,217,734:No podemos recuperar nuestro valor original una vez que hemos perdido bits.
Desplazamiento aritmético a la derecha (>>)
El desplazamiento aritmético a la derecha es exactamente como el desplazamiento 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 desplazamiento aritmético a la derecha conserva los signos.
Por ejemplo, si interpretamos este patrón de bits como un número negativo:
tenemos el número -2,147,483,552. Desplazar esto a la derecha 4 posiciones con el desplazamiento aritmético (-2,147,483,552 >> 4) nos daría:
o el número -134,217,722.
Entonces vemos que hemos preservado el signo de nuestros números negativos al usar el desplazamiento aritmético a la derecha, en lugar del desplazamiento lógico a la derecha. Y una vez más, vemos que estamos realizando una división por potencias de 2.
fuente
A good optimizing compiler will substitute shifts for multiplications when possible.
¿Qué? Los cambios de bits son órdenes de magnitud más rápidos cuando se trata de operaciones de bajo nivel de una CPU, un buen compilador de optimización haría exactamente lo contrario, es decir, convertir las multiplicaciones ordinarias por potencias de dos en cambios de bits.Digamos que tenemos un solo byte:
La aplicación de un único desplazamiento a la izquierda nos consigue:
El cero más a la izquierda se desplazó fuera del byte, y se agregó un nuevo cero al extremo derecho del byte.
Los bits no se vuelcan; Se descartan. Eso significa que si dejó el desplazamiento 1101100 y luego lo desplazó hacia la derecha, no obtendrá el mismo resultado.
Cambiantes dada por N es equivalente a multiplicar por 2 N .
Desplazar a la derecha por N es (si está utilizando el complemento de unos ) es el equivalente a dividir por 2 N y redondear a cero.
Bitshifting se puede usar para una multiplicación y división increíblemente rápida, siempre que trabaje con una potencia de 2. Casi todas las rutinas gráficas de bajo nivel usan bithifting.
Por ejemplo, en los viejos tiempos, utilizamos el modo 13h (320x200 256 colores) para los juegos. En el modo 13h, la memoria de video se dispuso secuencialmente por píxel. Eso significaba calcular la ubicación de un píxel, usaría las siguientes matemáticas:
Ahora, en esa época, la velocidad era crítica, por lo que usaríamos cambios de bits para hacer esta operación.
Sin embargo, 320 no es una potencia de dos, por lo que para solucionar esto tenemos que descubrir cuál es la potencia de dos que sumados hacen 320:
Ahora podemos convertir eso en desplazamientos a la izquierda:
Para un resultado final de:
Ahora tenemos el mismo desplazamiento que antes, excepto que en lugar de una costosa operación de multiplicación, 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 agregó un ejemplo de 32 bits)):
Total: 28 ciclos en cualquier CPU antigua que tuviera estos tiempos.
Vrs
12 ciclos en la misma CPU antigua.
Sí, trabajaríamos duro para reducir 16 ciclos de CPU.
En el modo de 32 o 64 bits, ambas versiones se vuelven mucho más cortas y rápidas. Las CPU modernas de ejecución fuera de orden como Intel Skylake (consulte http://agner.org/optimize/ ) tienen una multiplicación de hardware muy rápida (baja latencia y alto rendimiento), por lo que la ganancia es mucho menor. La familia AMD Bulldozer es un poco más lenta, especialmente para la multiplicación de 64 bits. En las CPU Intel y AMD Ryzen, dos cambios tienen una latencia ligeramente menor pero más instrucciones que una multiplicación (lo que puede conducir a un menor rendimiento):
vs.
Los compiladores harán esto por usted: vea cómo GCC, Clang y Microsoft Visual C ++ usan shift + lea cuando optimizan
return 320*row + col;
.Lo más interesante a tener en cuenta aquí es que x86 tiene una instrucción shift-and-add (
LEA
) que puede hacer pequeños cambios a la izquierda y agregar al mismo tiempo, con el rendimiento como unaadd
instrucción. ARM es aún más poderoso: un operando de cualquier instrucción se puede desplazar hacia la izquierda o hacia la derecha de forma gratuita. Por lo tanto, escalar mediante 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.OK, en los tiempos modernos ... algo más útil ahora sería usar el desplazamiento de bits para almacenar dos valores de 8 bits en un entero de 16 bits. Por ejemplo, en C #:
En C ++, los compiladores deberían hacer esto por usted si utilizó un
struct
con dos miembros de 8 bits, pero en la práctica no siempre lo hacen.fuente
c=4*d
obtendrás un turno. Si escribesk = (n<0)
eso también se puede hacer con turnos:k = (n>>31)&1
para evitar una rama. En pocas palabras, esta mejora en la inteligencia de los compiladores significa que ahora no es necesario usar estos trucos en el código C, y comprometen la legibilidad y la portabilidad. Sigue siendo muy bueno saberlos si está escribiendo, por ejemplo, código vectorial SSE; o cualquier situación en la que lo necesite rápidamente y haya un truco que el compilador no esté utilizando (por ejemplo, código GPU).if(x >= 1 && x <= 9)
que se puede hacer, ya queif( (unsigned)(x-1) <=(unsigned)(9-1))
cambiar dos pruebas condicionales a una puede ser una gran ventaja de velocidad; especialmente cuando permite la ejecución predicada en lugar de ramas. Utilicé esto durante años (donde estaba justificado) hasta que noté hace unos 10 años que los compiladores habían comenzado a hacer esta transformación en el optimizador, luego paré. Todavía es bueno saberlo, ya que hay situaciones similares en las que el compilador no puede realizar la transformación por usted. O si estás trabajando en un compilador.Las operaciones bit a bit, incluido el cambio de bit, son fundamentales para hardware de bajo nivel o programación integrada. Si lee una especificación para un dispositivo o incluso algunos formatos de archivos binarios, verá bytes, palabras y palabras clave, divididas en campos de bits alineados sin bytes, que contienen varios valores de interés. Acceder a estos campos de bits para leer / escribir es el uso más común.
Un ejemplo real simple en la programación gráfica es que un píxel de 16 bits se representa de la siguiente manera:
Para obtener el valor verde, haría esto:
Explicación
Para obtener SOLAMENTE el valor de verde, que comienza en el desplazamiento 5 y termina en 10 (es decir, 6 bits de largo), debe usar una máscara (bit), que cuando se aplica a todo el píxel de 16 bits, producirá solo los bits que nos interesan.
La máscara apropiada es 0x7E0 que en binario es 0000011111100000 (que es 2016 en decimal).
Para aplicar una máscara, use el operador AND (&).
Después de aplicar la máscara, terminará con un número de 16 bits que en realidad es solo un número de 11 bits ya que su MSB está en el 11 ° bit. El verde en realidad solo tiene 6 bits de longitud, por lo que debemos reducirlo utilizando un desplazamiento a la derecha (11 - 6 = 5), de ahí el uso de 5 como desplazamiento (
#define GREEN_OFFSET 5
).También es común usar cambios de bits para la multiplicación y división rápidas por potencias de 2:
fuente
Enmascaramiento y desplazamiento de bits
El desplazamiento de bits se usa 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.
Para una mejor comprensión, el mismo valor binario etiquetado con qué secciones representan qué parte de color.
Digamos, por ejemplo, que queremos obtener el valor verde del color de este píxel. Podemos obtener ese valor fácilmente enmascarando y cambiando .
Nuestra máscara:
El
&
operador lógico garantiza 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) .Et voilà, tenemos el número entero que representa la cantidad de verde en el color del píxel:
Esto a menudo se utiliza para la codificación o descodificación de los formatos de imagen como
jpg
,png
, etc.fuente
Un problema es que lo siguiente depende de la implementación (de acuerdo con el estándar ANSI):
x ahora puede ser 127 (01111111) o aún -1 (11111111).
En la práctica, suele ser lo último.
fuente
Solo escribo consejos y trucos. Puede ser útil en pruebas y exámenes.
n = n*2
:n = n<<1
n = n/2
:n = n>>1
!(n & (n-1))
n
:n |= (1 << x)
x&1 == 0
(par)x ^ (1<<n)
fuente
Tenga en cuenta que en la implementación de Java, el número de bits para cambiar se modifica por el tamaño de la fuente.
Por ejemplo:
es igual a 2. Puede esperar que el desplazamiento de los bits a la derecha 65 veces ponga a cero todo, pero en realidad es el equivalente de:
Esto es cierto para <<, >> y >>>. No lo he probado en otros idiomas.
fuente
gcc 5.4.0
da una advertencia, pero da2
5 >> 65; también.Algunas operaciones / manipulaciones de bits útiles en Python.
Implementé la respuesta de Ravi Prakash en Python.
fuente
Tenga en cuenta que solo la versión de PHP de 32 bits está disponible en la plataforma Windows.
Entonces, si, por ejemplo, desplaza << 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 el cambio en más de 63 bits. Sin embargo, por ejemplo, MySQL usa BIGINT de 64 bits, por lo que no debería haber ningún problema de compatibilidad.
ACTUALIZACIÓN: desde PHP 7 Windows, las compilaciones de PHP finalmente pueden usar enteros completos de 64 bits: el tamaño de un entero depende de la plataforma, aunque un valor máximo de aproximadamente dos mil millones es el valor habitual (32 bits con signo). Las plataformas de 64 bits generalmente tienen un valor máximo de aproximadamente 9E18, excepto en Windows anterior a PHP 7, donde siempre era de 32 bits.
fuente