A number in memory is a row of switches. 44 is not stored as the digits 4 and
4 — it is stored as eight on-or-off bits, each standing for a power of two:
0 0 1 0 1 1 0 0128 64 32 16 8 4 2 1 32 + 8 + 4 = 44Flip the switches and watch the value change. The three readouts underneath update live — they are explained just below:
Click a bit to flip it
00101100=44
- x & -x
- 00000100 = 4
- x & (x - 1)
- 00101000 = 40
- popcount(x)
- 3
isolates the lowest set bit
clears the lowest set bit
how many bits are set
Everything in this article follows from that picture.
The five operators
| Operator | Reads as | Example |
|---|---|---|
& | AND — 1 only where both are 1 | 0b1100 & 0b1010 = 0b1000 |
| | OR — 1 where either is 1 | 0b1100 | 0b1010 = 0b1110 |
^ | XOR — 1 where they differ | 0b1100 ^ 0b1010 = 0b0110 |
~ | NOT — flip every bit | ~0b1100 = 0b…0011 |
<< >> | shift left / right | 0b0011 << 2 = 0b1100 |
Shifting left by one doubles; shifting right by one halves and throws away the
remainder. x << 3 is x * 8.
The four things you will actually write
const check = (x: number, n: number) => (x >> n & 1) === 1; // is bit n on?const set = (x: number, n: number) => x | (1 << n); // turn it onconst clear = (x: number, n: number) => x & ~(1 << n); // turn it offconst toggle = (x: number, n: number) => x ^ (1 << n); // flip itEach one builds 1 << n — a mask with a single bit — and combines it. clear
inverts the mask first, so it is all 1s except at position n, and ANDing with
that keeps everything except that bit.
XOR is the interesting one
Three properties, each surprising the first time:
x ^ x === 0 // anything cancels itselfx ^ 0 === x // zero changes nothinga ^ b === b ^ a // order does not matterPut them together. Take an array where every value appears twice except one:
const odd = values.reduce((acc, v) => acc ^ v, 0);Every pair cancels to 0, order is irrelevant, and the loner is left standing. One pass, no memory, no hash map.
Two tricks worth memorising
x & (x - 1) clears the lowest set bit.
Subtracting 1 flips the lowest 1 to 0 and turns every 0 below it into 1. ANDing keeps only the bits above, which erases exactly that one bit:
x = 0b0010_1100x - 1 = 0b0010_1011x & … = 0b0010_1000 ← the lowest 1 is goneSo counting set bits takes as many iterations as there are bits set, not 32:
let count = 0;while (x !== 0) { x &= x - 1; count += 1; }And x & (x - 1) === 0 tests for a power of two: exactly one bit set means
clearing it leaves nothing.
x & -x isolates the lowest set bit.
Negation in two’s complement is “flip every bit, then add 1”, which leaves the lowest 1 in place and inverts everything above it. AND them and only that bit survives. This is what Fenwick trees use to walk their index structure.
Bitmasks as sets
If your universe has at most 32 items, a whole subset fits in one integer.
let inParty = 0;inParty |= 1 << WIZARD; // addinParty &= ~(1 << THIEF); // removeconst hasWizard = (inParty >> WIZARD & 1) === 1;
const both = partyA & partyB; // intersectionconst either = partyA | partyB; // unionconst onlyA = partyA & ~partyB; // differenceSet operations become single instructions, and for (let mask = 0; mask < 1 << n; mask++)
enumerates every subset. This is what makes bitmask
dynamic programming practical for
or so.
When it is worth it
Rarely, for speed. Compilers already turn x * 8 into a shift, and modern CPUs
have a single instruction for population count. Writing x << 3 to “optimise” a
multiply is noise.
It is worth it when bits are genuinely the model:
- Flags and permissions — one integer holding sixteen booleans, which is why
Unix file modes look like
0644. - Subset enumeration in DP and search.
- Bloom filters and other probabilistic structures.
- Fenwick trees, whose entire index arithmetic is
i & -i. - Hardware and protocol work, where the layout is fixed by a spec.
The rest of the time, prefer the readable version. A comment explaining a bit trick is a sign the trick was not worth it.