All About Bitwise Operators or How to Extract Red from RGB Like a Pro
In the realm of bytes and bits, understanding bitwise operators can elevate your prowess in coding and data manipulation. This article explores the practical applications of bitwise operations: from packing and unpacking data, like extracting color channels from RGB values, to working with flags and bitmasks. We'll also dive into using shift operators for fast multiplication and division, and uncover clever tricks for manipulating ASCII characters.
Data Packing and Unpacking
This is common if you need to cram multiple smaller values into a single larger one to save space or conform to a protocol.
Let's explore a simple example of packing a date (Day, Month, Year) into a single 16-bit short integer. To achieve this, we need to carefully allocate the available bits to accommodate each value:
-
Day: To represent values from 1 to 31, we'll allocate 5 bits. The binary code for January 1st would be
0x00001
, while the code for December 31st would be0x11111
(5 bits). -
Month: We'll use 4 bits for values ranging from 1 to 12. For example, January would be represented by the binary code
0x0001
(1), while December would be0x1100
(4 bits). This allocation allows us to accommodate all 12 months using a compact 4-bit representation. -
Year: The final 7 bits will be used to represent the year since 2000. Since this is the remaining space after packing the month and day values, we can accommodate years up to 127 (
2^7 - 1
).
The goal is to pack the date values into a single integer:
let year = 25; // 2025
let month = 5; // May
let day = 31;
let packedDate = (year << 9) | (month << 5) | day;
console.log(Number(packedDate).toString(2));
// 11001010111111
We're shifting the year value (25
) 9 bits to the left using the <<
left-shift operator. This effectively moves the least significant bit of the year value (the 1
s) out of the way, making room for the month and day values:
year << 9
becomes 0b100000000
(a 16-bit binary representation with the year value shifted to the left).
We're shifting the month value (5
) 5 bits to the left using the <<
operator. This allows us to fit the month value into the available space:
month << 5
becomes 0b00001000
(a 4-bit binary representation with the month value shifted to the left).
We're not shifting the day value (31
) at all, as it's already a small value that can be represented using the
remaining bits.
The |
operator is used for bitwise OR, which combines the values:
- The year value (shifted) takes up the most significant 11 bits.
- The month value (shifted) occupies the next 4 bits.
- The day value takes up the remaining 1 bit.
The resulting packed date integer is 0b11001010111111
, which represents the original date values.
When you convert this packed date to a binary string using toString(2)
, you get: 11001010111111
Extracting Color Channels From a 32-bit RGB value
Colors are often represented as RGB values, where three numbers define the intensity of red, green, and blue. These values typically range from 0 (representing no color contribution) to 255 (maximum intensity), which is equivalent to a binary representation using exactly 8 bits (the decimal number 255 is equal to 11111111 in binary notation).
For example, a color might be written in hexadecimal as 0xAABBCC
, where:
- AA is the red component,
- BB is the green component,
- CC is the blue component.
The corresponding binary representation looks like this:
00000000 10101010 10111011 11001100
.
Here, the first 8 bits (all zeros) are often unused or represent an alpha channel (transparency) in ARGB formats. The next 8 bits (10101010, or 0xAA) are red, followed by green (10111011, or 0xBB), and blue (11001100, or 0xCC).
So how extract the red value 0xAA
using bitwise operators?
let rgb = 0xAABBCC;
let red = (rgb >> 16) & 0xFF;
In the example above (JavaScript), we first defined the RGB value as a hexadecimal number (prefix 0x
) and used the right-shift (>> 16
) operator to move all bits in rgb
to the right by 16 positions.
Shifting right by 16 bits moves the red component to the least significant byte:
00000000 00000000 00000000 10101010
.
This is 0xAA
- exactly the red value we want.
The green and blue bits have been shifted out, and zeros fill in from the left.
The &
(Bitwise AND) operator performs a bit-by-bit comparison between two numbers, yielding 1 only when both bits are 1 (& 0xFF
): This ensures that you're capturing exactly one
byte.
Below you will find a function called unpackRgb(packed: number)
in TypeScript which extracts
the individual Red, Green, and Blue channels from a 24-bit RGB value. The extracted channels are returned as an array of three integers.
/**
* Unpacks a single integer into RGB components.
* @param packed - Packed RGB value
* @returns [red, green, blue] components
*/
function unpackRgb(packed: number): [number, number, number] {
const red = (packed >> 16) & 0xff;
const green = (packed >> 8) & 0xff;
const blue = packed & 0xff;
return [red, green, blue];
}
Working with Flags and Bitmasks
A bitmask is a technique used to efficiently represent multiple binary states within a single integer value. This method is often more memory-efficient and faster than storing each state as an individual boolean variable. We will use shift operators for creating and checking flags stored within the bitmask.
Let's say you have several boolean states (on/off) for an object:
const FLAG_VISIBLE = 1 << 0; // 1 (0001)
const FLAG_SOLID = 1 << 1; // 2 (0010)
const FLAG_MOVABLE = 1 << 2; // 4 (0100)
const FLAG_PLAYER = 1 << 3; // 8 (1000)
By using 1 << N
, you are creating a value where only the N-th bit is set to 1
.
This guarantees each flag has a unique bit and they won't interfere with each other.
You can then combine these flags using bitwise OR.
// Create a movable player object
let objectState = FLAG_PLAYER | FLAG_MOVABLE; // 8 | 4 = 12 (1100)
// To check if the object is movable:
if (objectState & FLAG_MOVABLE) {
// This will be true
}
// To check if it's solid:
if (objectState & FLAG_SOLID) {
// This will be false
}
To unset the FLAG_MOVABLE
flag from the objectState
variable, you can use the bitwise NOT operator (~
) or the bitwise AND
operator with a complemented mask (& ~FLAG_MOVABLE
).
let newObjectState = objectState & ~FLAG_MOVABLE;
Fast Multiplication and Division by Powers of Two
This is one of the most well-known uses of shift operators. In low-level programming or performance-critical code, shifting bits can be significantly faster than performing actual multiplication or division CPU instructions.
-
Left Shift
<<
for Multiplication: Shifting a number one position to the left is equivalent to multiplying it by 2. Shifting byN
positions is equivalent to multiplying by $2^N$.int x = 10; // Binary: 00001010 // Multiply by 2 (2^1) int result_times_2 = x << 1; // 20 (Binary: 00010100) // Multiply by 8 (2^3) int result_times_8 = x << 3; // 80 (Binary: 01010000)
-
Right Shift
>>
for Division: Shifting a number one position to the right is equivalent to performing an integer division by 2. Shifting byN
positions is equivalent to dividing by $2^N$.int y = 40; // Binary: 00101000 // Divide by 2 (2^1) int result_div_2 = y >> 1; // 20 (Binary: 00010100) // Divide by 4 (2^2) int result_div_4 = y >> 2; // 10 (Binary: 00001010)
Note: For negative numbers, the behavior of the right shift (>>
) can vary. An arithmetic right shift preserves the sign bit, while a logical right shift fills with zeros. Most modern languages use an arithmetic shift for signed integers.
The JavaScript JIT Compiler
The crucial factor to consider is that JavaScript code does not run directly on the CPU. Instead, it is executed by sophisticated engines such as V8 (in Chrome and Node.js) or SpiderMonkey (in Firefox). These engines incorporate Just-In-Time (JIT) compilers.
These JIT compilers analyze code in real-time, they optimize "hot paths" (code that is executed frequently). When encountering x * 2
, the JIT compiler typically converts it into the highly efficient shift instruction, a classic optimization known as Strength Reduction.
Because the JIT compiler does this automatically, manually changing x * 2
to x << 1
has several potential outcomes:
- No performance improvement: The JIT compiler was already making the same optimization.
- Reduced performance: Your manual optimization might confuse the JIT compiler and prevent it from applying other, more advanced optimizations.
The Major Pitfall: Bitwise Operators and Numbers in JavaScript
In JavaScript, all numbers are internally represented as 64-bit floating-point numbers (doubles). However, bitwise operations only work on 32-bit signed integers.
When you use a shift operator (<<
or >>
), JavaScript implicitly does the following:
- Takes your floating-point number.
- Converts it to a 32-bit signed integer (truncating any decimal part).
- Performs the bitwise operation.
- Returns the resulting 32-bit integer.
When working with numbers, it's best to use the right operations for the job. Avoid using shift operators for floating-point math or other situations where precision is important. Instead, stick with standard arithmetic.
Bonus: Bitwise Tricks Beyond Colors
Uppercase / Lowercase Letter Transformation
Here’s a neat trick for converting uppercase letters to lowercase in ASCII:
The ASCII code for a capital letter (e.g., A, 0x41
) can be transformed into its lowercase version (a, 0x61
) by setting the 6th bit, which you can do with a bitwise OR (or simple addition):
let bigLetter = 'A'; // 0x41 in ASCII
let smallLetter = String.fromCharCode(bigLetter.charCodeAt(0) | 0x20);
// Result: 'a'
This works because, in ASCII, lowercase letters are exactly 32 (0x20
) greater than their uppercase counterparts. The | 0x20
operation sets the bit that makes A
into a
. Alternatively, you could just add 0x20
:
let smallLetter = String.fromCharCode(bigLetter.charCodeAt(0) + 0x20);
To go from lowercase to uppercase, we need to clear the 6th bit (counting from 0), which can be done using a bitwise AND (&
) with the complement of 0x20
(i.e., ~0x20
).
let smallLetter = 'a'; // 0x61 in ASCII
// Using bitwise AND
let bigLetter = String.fromCharCode(smallLetter.charCodeAt(0) & ~0x20); // 'A'
// Using subtraction
let bigLetterAlt = String.fromCharCode(smallLetter.charCodeAt(0) - 0x20); // 'A'
Swap Variables without using a temporary variable
You can swap two variables without using a temporary variable with the bitwise XOR operator (^
).
let a = 2;
let b = 3;
a ^= b;
b ^= a;
a ^= b;
console.log(a, b); // 3, 2
The key property of the XOR operator that makes this work is:
X ^ X = 0
(XORing a number with itself results in 0)X ^ 0 = X
(XORing a number with 0 results in the number itself)X ^ Y = Y ^ X
(Commutative property)(X ^ Y) ^ Z = X ^ (Y ^ Z)
(Associative property)
And here is the breakdown how this works for swapping the variables:
Initial values:
a = 2
(binary 10
)
b = 3
(binary 11
)
Step 1: a ^= b;
This is equivalent to a = a ^ b;
a
(binary10
) XORb
(binary11
)10 (a) ^ 11 (b) ---- 01 (result)
- Now,
a
becomes01
(decimal1
). b
remains11
(decimal3
).
Step 2: b ^= a;
This is equivalent to b = b ^ a;
Remember the current values: a = 1
(binary 01
), b = 3
(binary 11
).
b
(binary11
) XORa
(binary01
)11 (b) ^ 01 (a) ---- 10 (result)
- Now,
b
becomes10
(decimal2
). a
remains01
(decimal1
).
Step 3: a ^= b;
This is equivalent to a = a ^ b;
a
(binary01
) XORb
(binary10
)01 (a) ^ 10 (b) ---- 11 (result)
- Now,
a
becomes11
(decimal3
). b
remains10
(decimal2
).
Summarized:
a = a ^ b
(a now holds the XOR sum of original a and b)b = b ^ a
(substitutea
from step 1):b = b ^ (original_a ^ original_b)
. Sinceb ^ b = 0
, this simplifies tob = original_a ^ (b ^ b)
which meansb
now holdsoriginal_a
.a = a ^ b
(substitutea
from step 1 andb
from step 2):a = (original_a ^ original_b) ^ original_a
. Sinceoriginal_a ^ original_a = 0
, this simplifies toa = original_b
.
