Operadores Bitwise en Javascript

06 Jun 2012

Introducción

Los operadores a nivel de bits, conocidos como Bitwise, son por lo general poco conocidos. Su uso se limita muchas veces al ámbito exclusivamente matemático, al desarrollo de algoritmos complejos o al de los cálculos necesarios durante el desarrollo de videojuegos 3D. Sin embargo, existen otros escenarios donde conocer el funcionamiento de estos operadores puede significar una mejora notable en el rendimiento de nuestras aplicaciones o dar lugar a estructuras más elegantes y sostenibles.

Echemos un vistazo a cómo funcionan para pasar después a ver algunos casos prácticos de uso…

Los operadores a nivel de bits en Javascript

Una operación bit a bit (o Bitwise) opera sobre números binarios a nivel de bits individuales. Es una acción primitiva sustancialmente más rápida que las que se llevan a cabo sobre el valor real de los operandos.

Un detalle a tener en cuenta es que estos Bitwise son, en Javascript, operadores de 32-bits, lo que significa que en el manejo de valores binarios, un número como 0101 se procesa internamente como 00000000000000000000000000000101. Sin embargo, todos los ceros de la izquierda pueden despreciarse ya que, como en el caso de los números decimales, no tienen ningún significado o valor.

Estos operadores están presente en la mayoría de lenguajes de programación (C#, PHP, Java, Ruby…) y son muy similares a los operadores lógicos que solemos manejar.

En Javascript, tenemos a nuestra disposición los siguientes operadores:

Operador Uso Descripción rápida
& (AND) a & b Devuelve 1 si ambos operandos son 1
| (OR) a | b Devuelve 1 donde uno o ambos operandos son 1
^ (XOR) a ^ b Devuelve 1 donde un operando, pero no ambos, es 1
~ (NOT) ~a Invierte el valor del operando
<< (Desplazamiento a la izquierda) a << b Desplaza a la izquierda un número especificado de bits.
>> (Desplazamiento a la derecha) a >> b Desplaza a la derecha un número especificado de bits.
>>> (Desplazamiento a la derecha con acarreo) a >>> b Desplaza la derecha un número especificado de bits descartando los bits desplazados y sustituyéndolos por ceros.

Lo primero que llama la atención a quienes se acercan por primera vez a estos operadores es que resultan muy similares a los lógicos tradicionales. En concreto, los dos primeros son la versión reducida de los frecuentes AND (&&) y OR (||).

Pasemos a ver cómo operan cada uno de ellos:

OPERADOR BIT A BIT  AND

El AND bit a bit toma dos números enteros y realiza la operación AND lógica en cada par correspondiente de bits. El resultado en cada posición es 1 si el bit correspondiente de los dos operandos es 1, y 0 de lo contrario:

Valor A Valor B A AND B
0 0 0
0 1 0
1 0 0
1 1 1

OPERADOR BIT A BIT OR

Una operación OR de bit a bit, o bitwise, toma dos números enteros y realiza la operación OR inclusivo en cada par correspondiente de bits. El resultado en cada posición es 1 si el bit correspondiente de cualquiera de los dos operandos es 1, y 0 si ambos bits son 0:

Valor A Valor B A OR B
0 0 0
0 1 1
1 0 1
1 1 1

OPERADOR EXCLUSIVE OR (XOR)

El XOR bit a bit, o bitwise, toma dos números enteros y realiza la operación OR exclusivo en cada par correspondiente de bits. El resultado en cada posición es 1 si el par de bits son diferentes y cero si el par de bits son iguales:

Valor A Valor B A XOR B
0 0 0
0 1 1
1 0 1
1 1 0

OPERADOR DE NEGACIÓN NOT

El NOT bit a bit, o bitwise, o complemento, es una operación unaria que realiza la negación lógica en cada bit, invirtiendo los bits del número, de tal manera que los ceros se convierten en 1 y viceversa:

Valor A NOT A
0 1
1 0

DESPLAZAMIENTO LÓGICO DE BITS

El desplazamiento lógico se usa para mover bits hacia la izquierda o hacia la derecha para colocarlos en la posición adecuada. En programación, el primer operando representa el binario que queremos modificar mientras que el segundo indica las posiciones a desplazar:

Desplazamiento a la Izquierda Desplazamiento a la Derecha
00011100 << 1 = 00111000 00011100 >> 1 = 00001110
00011100 << 2 = 01110000 00011100 >> 2 = 00000111

En los desplazamientos hacia la izquierda, el bit más significativo (más a la izquierda) se pierde, y se le asigna un 0 al menos significativo (el de la derecha). En los desplazamientos a la derecha ocurre lo contrario.

Hay que tener en cuenta que los desplazamientos no son rotaciones; los bits que salen por la izquierda se pierden mientras que los que entran por la derecha son rellenados con ceros. Este tipo de desplazamientos se denominan lógicos a diferencia de los cíclicos o rotacionales.

Un ejemplo frecuente: los FLAGS

Visto lo anterior, es hora de tomar un caso práctico de uso para ver cómo implementar este tipo de comparaciones.

Un ejemplo típico es el de identificar flags en nuestras aplicaciones de un modo simple evitando con ello estructuras de flujo complejas o la declaración de variables innecesarias.

Pensemos en una función que opera según una serie de atributos que se han ido agregando a un determinado objeto. A estos atributos, los identificaremos con flags de forma lineal y podrían ser los siguientes:

  • Flag A: el usuario tiene un PC
  • Flag B: el usuario tiene un SmartPhone
  • Flag C: el usuario tiene un tablet
  • Flag D: el usuario tiene una eReader

Estos indicadores pueden asociarse al usuario en una secuencia, por ejemplo, del tipo DCBA donde un 0 indicaría false y 1 true. De este modo, un usuario que tuviese los flags A y C, se correspondería con la siguiente cadena:

0101 // Flag C and Flag A activate

La explicación al detalle sería la siguiente:

  • El primer dígito, cero, indica que el Flag D no está activado, lo que quiere decir que el usuario no tiene eReader.
  • El segundo dígito, uno, indica que el Flag C está activo: el usuario tiene un tablet.
  • El tercer dígito, Flag B, está a cero: el usuario no tiene SmartPhone.
  • El cuarto dígito, Flag A, está activo: el usuario tiene un PC

Con esto, podríamos establecer fácilmente las máscaras que identifican cada flags en nuestro supuesto usuario de la siguiente forma:

var FLAG_A = parseInt( '0001', 2 );
var FLAG_B = parseInt( '0010', 2 );
var FLAG_C = parseInt( '0100', 2 );
var FLAG_D = parseInt( '1000', 2 );

Algo que llama la atención a primera vista es el uso de parseInt para identificar los valores: los operadores Bitwise, como hemos mencionado antes, trabajan con valores binarios pero nuestra cadena la hemos montado de forma ‘manual’: necesitamos que sea un valor válido con el que pueda operar el intérprete. Más adelante se explica un poco mejor este paso pero la idea es que, para convertir una cadena binaria dada en su valor decimal real, utilizamos la función parseInt al que modificamos su argumento radix (raíz) para establecerlo en base 2.

Con este patrón ya identificado, crear un flujo que pregunte a nuestro usuario por un determinado flag pasa a ser muy sencillo si actuamos a nivel de bits.

Si necesitáramos preguntar por el FLAG_A a nuestro usuario anterior, utilizaríamos un simple Bitwise AND:

var userFlags = parseInt( '0101', 2 );
 
if( userFlags & FLAG_A ){
  // User have a PC
}

Limpio y claro, verdad? Gracias a esta estructura de control, podemos preguntar por varios flags a la vez con la misma facilidad. Para ello, lo ideal es preparar una máscara previa que indique de forma explícita por los valores que se pretenden combinar:

var smartPhone_AND_tablet = FLAG_B | FLAG_C;

Con esto, podríamos montar un código de la siguiente forma:

var userFlags = parseInt( '0101', 2 ); // Our user have the FLAG_C and the FLAG_A active
 
var smartPhone_AND_tablet = FLAG_B | FLAG_C;
 
if( userFlags & smartPhone_AND_tablet ){
  // User have a smartPhone or a tablet
}

El uso de una máscara puede simplificar la lectura del código, pero no es imprescindible; el siguiente código sería equivalente al anterior:

var userFlags = parseInt( '0101', 2 ); // Our user have the FLAG_C and the FLAG_A active
 
if( ( userFlags & FLAG_C ) || ( userFlags & FLAG_A ) ){
  // User have a smartPhone or a tablet
}

Estos dos ejemplos sirven para comprobar si existe en la definición de nuestro usuario uno u otro flag, es decir, estaríamos preguntando por un OR clásico. Si queremos comprobar que dos o más flags están presentes a la vez, es decir, un AND, modificaríamos el operador intermedio:

var userFlags = parseInt( '0101', 2 ); // Our user have the FLAG_C and the FLAG_A active
 
if( ( userFlags & FLAG_C ) && ( userFlags & FLAG_A ) ){
  // User have a smartPhone and a tablet
}

 

Usando los Bitwise para mejorar el rendimiento

Cuando dentro de nuestros códigos realizamos comparaciones, lo más habitual es hacerlo a nivel de valor o de objeto. En cuestión de rendimiento, este tipo de estructuras pueden mejorarse en determinados casos recurriendo a una comparación a nivel de bits.

Tomemos por ejemplo el siguiente código extraído de Underscore para codificar su función sortedIndex:

// Use a comparator function to figure out the smallest index at which
// an object should be inserted so as to maintain order. Uses binary search.
_.sortedIndex = function(array, obj, iterator) {
  iterator || (iterator = _.identity);
  var value = iterator(obj);
  var low = 0, high = array.length;
  while (low < high) {
    var mid = (low + high) >> 1;
    iterator(array[mid]) < value ? low = mid + 1 : high = mid;
  }
  return low;
};

El código no es demasiado complejo pero puede resultar poco intuitivo. Su finalidad es localizar el índice más pequeño de un array en el que ha de insertarse un valor dado.

El quid de la cuestión está en el bloque:

// ...
while (low < high) {
  var mid = (low + high) >> 1;
  iterator(array[mid]) < value ? low = mid + 1 : high = mid;
}
// ...

Ahí, se está utilizando la operación Desplazamiento a la derecha, para establecer el valor del índice del array (mid) que le permite avanzar en la iteración a través de una comparación posterior. Una forma elegante y potente de conseguir la funcionalidad buscada con un rendimiento sobresaliente.

Curiosidad: Uso de los operadores Bitwise como ‘shortcuts’ matemáticos

Con lo visto anteriormente, podemos extraer que, en determinadas circunstancias, podemos utilizar estos operadores como ‘atajos’ a la hora de realizar algunas operaciones matemáticas. Esto es así porque dichas operaciones están implicitas en el propio proceso que se ejecutan cuando se llaman.

Es posible que el resultado se poco legible para aquellos desarrolladores que no estén acostumbrados a su manejo, pero también es cierto que dan un toque elegante (quien quiera que lea aquí friqui) a nuestros códigos a la vez que reducen su tamaño.

El ejemplo más recurrente es el de obtener la parte entera de un número en coma flotante utilizando bien una comparación OR a 0 o el operador XOR también sobre el valor 0:

12.65 | 0 // 12
13.000001 | 0 // 13
15.999999 | 0 // 15
 
12.65 ^ 0 // 12
13.000001 ^ 0 // 13
15.999999 ^ 0 // 15

Este ‘truncado’, o redondeo, es similar al que obtendríamos con el método nativo Math.floor(), pero ahorrándonos lo que pueden resultar algunos valiosos caracteres:

Math.floor( 12.65 ) // 12
Math.floor( 13.000001 ) // 13
Math.floor( 15.999999 ) // 15

Otra característica matemática de estos Bitwise la podemos observar en el operador NOT: aplicado a cualquier número x, se obtiene el resultado -(x + 1).Por ejemplo:

console.log( ~2 ); // -3
console.log( ~5 ); // -6
console.log( ~-5 ); // 4 => -( -5 + 1 ) => - (-4) => 4

Cómo pasar un número decimal a binario

Como ya hemos visto, para exprimir las posibilidades de los Bitwise, es preferible trabajar con valores binarios de forma nativa.

Para pasar un número en base decimal a binario, utilizamos la función toString indicando como argumento la base 2. Ejmpl:

(2).toString(2); // "10"
(5).toString(2); // "101"
(10).toString(2); // "1010"

Como sabemos, podemos modificar la longitud de un número en binario sin alterar su valor añadiendo tantos ceros a su izquierda como precisemos. Esto resulta muy útil a la hora de realizar comparaciones a nivel de bits en la que es necesario que ambos términos tengan la misma longitud:

'00010' = '10'; // 2 decimal
'00000000101' = '101' // 5 decimal

Cómo pasar un número binario a decimal

La operación inversa a la anterior, esto es obtener un número decimal a partir de uno binario, se realiza mediante la función parseInt donde indicaremos en su segundo parámetro que estamos trabajando con base binaria (2):

parseInt('10', 2); // 2
parseInt('101', 2); // 5
parseInt('1010', 2); // 10
parseInt('00000000101', 2); // 5

Conclusión

Los operadores Bitwise no son muy frecuentes en los códigos que solemos encontrar. Sin embargo, su uso puede resultar muy útil cuando queremos operar a nivel de bits como, por ejemplo, en el caso práctico anterior.

Algunas bibliotecas también recurren a su uso cuando se trata de exprimir el rendimiento de una determinada función o algoritmo como hemos visto en el caso de Underscore.

Conocerlos nunca está de más y ahora que hemos trabajado un poco con ellos, probablemente encontremos ocasiones en las que pueden sernos de utilidad.

Más información en:

Más:

{4} Comentarios.

  1. Santiago

    Muy buena entrada, con buenos ejemplos, de un aspecto no muy conocido de Javascript.

    Otro recurso para ampliar conocimientos: https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Operators/Bitwise_Operators

  2. pykiss

    quería aclarar un poco el funcionamiento de está linea de código:

    var mid = (low + high) >> 1;

    Lo mismo que en decimal mover las cifras de un número a la derecha equivale a dividirlo entre 10 y moverlas a la izquierda supone multiplicarlo por 10, en binario dicho desplazamiento se traduce en una división o multiplicación de 2.

    150 >> 1 = 15
    150/10 = 15

    5 <> 1 = 101
    10/2 = 5

    • Carlos Benítez

      Perfecto;
      gracias por la aclaración!

      Saludos!

  3. Gracias! 😉

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *