Bitwise operators
8 minutes
If you've been programming for quite a while, chances are you have come across low level operations like bitwise operations. And perhaps if you're like me you didn't get them easily, at least not at first glance. So this time, we're gonna be poking around with them and unveiling their secrets. For the sake of this article we'll be using javascript as programming language, but feel free to use the one of your choice.
So first off, what are bitwise operations ?
As you know computers native language is binary, i.e zeros (0) and ones (1), in other words bits. All the operations performed in the binary realm and namely in a set of bits are called bitwise operations.
These are the different bitwise operations we can perform:
- And
- Or
- Not
- Xor
- Left shift
- Right shift
Bitwise And (&)
The bitwise AND (&) returns 1 when both operands are 1. e.g:
const a = 5; // 00000000000000000000000000000101
const b = 3; // 00000000000000000000000000000011
console.log(a & b); // 00000000000000000000000000000001
Bitwise Or (|)
The bitwise OR (|) returns 1 when either operand is 1. e.g:
const a = 5; // 00000000000000000000000000000101
const b = 3; // 00000000000000000000000000000011
console.log(a | b); // 00000000000000000000000000000111
Bitwise Not (~)
The bitwise NOT (~) returns 0 when the operand is 1 and vice versa. e.g:
const a = 5; // 00000000000000000000000000000101
console.log(~a); // 11111111111111111111111111111010
Bitwise Xor (^)
The Bitwise XOR (^) returns 1 only if one of the bits is zero. e.g:
const a = 5; // 00000000000000000000000000000101
const b = 3; // 00000000000000000000000000000011
console.log(a ^ b); // 11111111111111111111111111111110
Bitwise Shifts
Bitwise shifts shift all the bits of a binary object in one direction, left or right. The left operand is the value to be shifted and the right one means the number of positions to be shifted.
Bitwise Left Shift (<<)
The bitwise left shift simply adds n times zeros to the end of the binary object. A simple way of thinking about this is with this formula:
a << b = a * (2 to the power of b)
const a = 5; // 00000000000000000000000000000101
console.log(a << 2);// 00000000000000000000000000010100
Bitwise Right Shift (>>)
Coversely, the bitwise right shift adds n times zeros to the beginning of the binary object. The following formula will make your life easier:
a >> b = a / (2 to the power of b)
const a = 5; // 00000000000000000000000000000101
console.log(a << 2);// 00000000000000000000000000000001
You might be wondering, why are those equivalent binary numbers shown as (32) bits rather than as (64) bits. Well, generally javascript does a conversion to (32) bits i.e (4) bytes signed integers before performing bitwise operations, and a convertion back to (64) bits afterwards, since javascript naturally stores numbers as (64) bits floating point numbers.
You don't necessarily have to represent numbers in decimal format, you can use the binary syntax in javascript as well. In order to do so simply start your binary number with a leading zero(0), followed by a (b) in lowercase or uppercase, e.g :
const a = 0b1111 // 15
const b = 0B100110010 // 306
const c = 0b10 // 2
So in which case escenario do we use bitwise operators ?
Even though they have several applications, me personally have used them for:
- Bit flags
This is a technique for assigning meaning to individual bits within a binary number. They're the most efficient way of representing something whose state is defined by several "yes or no" properties. If you have let's say 4 discrete permissions (read, write, execute, change policy), it's better to store this in 1 byte rather than waste 4.
In the figure (0) you can see how a simple byte carries different meanings in the context of a pop up dialog.
We could use bitwise logical operators on numbers to query and change these bit values. This is a quick reference that will be useful when dealing with bit operations:
And (&) Query the value of a bit
Or (|) Sets a bit to true
Xor (^) Toggles the value of a bit
So in the context of a pop up dialog we could have the following inmutable data:
export const DIALOG_BUTTON_ACCEPT = (1 << 0) // 0b1 // 1 // 0000 0001
export const DIALOG_BUTTON_CANCEL = (1 << 1) // 0b10 // 2 // 0000 0010
export const DIALOG_BORDER_SHADOW = (1 << 2) // 0b100 // 4 // 0000 0100
Using bit flags you know that a dialog with accept and cancel buttons will hold the value of (3), so code-wise say we receive a options prop on our dialog:
Dialog.Show({ options: DIALOG_BUTTON_ACCEPT | DIALOG_BUTTON_CANCEL | DIALOG_BORDER_SHADOW })
Given those options if you have a keen eye you might have noticed that options is holding the value of 0b111 (7). Now how do we know if the DIALOG_BUTTON_ACCEPT flag was set ? Simple, by using the query operator (&).
if ( ( options & DIALOG_BUTTON_ACCEPT ) == DIALOG_BUTTON_ACCEPT ) {
// flag DIALOG_BUTTON_ACCEPT is set
}
The above logic comparison is analogous to this:
0000 0111 &
0000 0001
__________
0000 0001
Since the and operation returns 1 only if both operands are 1 it will end up returning exactly the flag we're checking for, therefore we can conclude the flag is set.
- Some tricks
If you wanna leave trail of wisdom at your workplace, you can take into account the following tricks:
To check the parity of a x number you can try out doing
x & 1
This is akin to x % 2 in some programming languages so keep in mind that one.
Based on the fact that a negative number is the bitwise not(~) of the number plus one (1), let us first showcase how to convert a number to a relative figure.
But beforehand, we must understand how the binary addition is carried out at a low level. So lets take a look at figure (1).
From figure (1) we could infer that:
1 + 0or0 + 1is 1, therefore, the sum expression isa ^ b( bitwise xor ).- The carry expression is
a & b( bitwise and ).
With that being said, we can define steps for performing the bitwise sum and come up with some code:
- Given two positive numbers a and b.
- Check that b is not equal to zero (0).
- Find the carry value ( a & b ).
- Get the sum result ( a ^ b ) and stores it in variable a.
- Shift the carry to the left by 1 bit and store it in variable b.
- Go back to step 2.
- When b is zero return the sum.
/**
* bitwise addition
* @param {number} a - a binary number e.g 0b10
* @param {number} b - a binary number e.g 0b11
* @returns {number} the result value
*/
const bitwise_add = (a, b) => {
if (b == 0) return a
return bitwise_add( a ^ b, (a & b) << 1)
}
So now, how do you get the negative number of 0b11 (3) ?
bitwise_add(~0b11, 0b1) // - 3
Conclusion
Taking time to poke around and try to understand how these bitwise operations work is a good endeavor that eventually pays off, as they will give you a myriad number of possibilities and ideas on how to solve some problems leveraging binary features some programming languages have.
Comments