Pebellslearn by building
0%
RoadmapsMatrixCoursesSandboxSign in
Bits & Machine Numbers  ›  Lesson

Bit Tricks That Earn Their Keep

Bit Tricks 28 minSubtracting 1 and negating change only the lowest bits, so one expression can find, clear or count them
You're building a piece ofQuarry — the record encoder
This piece — claim_page(): Finds the lowest free page in an allocation bitmap and claims it.
Scenario A write needs somewhere to go. Quarry's page allocator looks at the file's allocation bitmap, finds the lowest free page with one bit trick, and marks it used, without scanning page by page.
Your task
Build claim_page(bitmap, pages). Bit i of bitmap is 1 when page i is used. Claim the lowest free page below pages and return [page, new_bitmap], or [-1, bitmap] if every page is used. Example: claim_page(0b10111, 8) → [3, 31].

Bit Tricks That Earn Their Keep

In short: subtracting 1 turns a number's trailing zeros into ones and clears its lowest set bit, and that one fact powers a handful of O(1) one-liners: x & (x-1) clears the lowest set bit, x & -x isolates it, and ~x & (x+1) isolates the lowest zero bit instead. Clearing one set bit per pass (Kernighan's loop) counts the 1s in O(popcount) time instead of O(width), which matters once a bitmap is mostly zeros.

Four 8-bit rows for x=40: the bits of x, of ~x, of -x = ~x+1, and of x & -x, with only bit 3 surviving in green

Quarry stores records in fixed-size pages inside one big file, and it has to know which pages are free. Its answer is an allocation bitmap: one bit per page, 1 for used and 0 for free. A file with a million pages needs a bitmap of just 125 KB. But to allocate a page, Quarry has to find a 0 bit quickly, and to report usage it has to count the 1s. A loop over every bit can do both, but it's slow.

This lesson covers the handful of one-line tricks that make those questions cheap. Each works because of how subtracting 1, and negating in two's complement, change the lowest bits of a number. Once you see why, you won't need to memorise any of them.

What x - 1 does to the bits

Subtracting 1 borrows. Starting at bit 0, every trailing 0 becomes a 1 as the borrow passes through it, until the borrow reaches the lowest 1, which becomes 0. Above that, nothing changes:

x      = 0 0 1 0 1 1 0 0     (44)
x - 1  = 0 0 1 0 1 0 1 1     (43)

So x & (x - 1) keeps every bit above the lowest 1 and clears the lowest 1 and everything below it. The net effect is that the lowest set bit is switched off.

What does this print?
print(40 & -40, 40 & 39)

x & -x: the lowest set bit on its own

In two's complement, -x is ~x + 1. Flipping every bit turns the trailing 0s of x into 1s, and adding 1 carries through all of them. The carry stops at the first 0 of ~x, which is exactly where x had its lowest 1. Above that, -x is still the flipped x, so an AND with x gives 0 there. Only one bit survives. Step through it for 44 in an 8-bit view:

watch it move · grid

Adding 1 to ~x carries through the flipped trailing zeros and stops at x's lowest 1, the only bit that x and -x share.

Flip every bit. x's two trailing 0s are now 1s, and its lowest 1, at bit 2, is now a 0. 4 by 8 table. 16 of 32 cells filled.

1 / 5

This trick is called lowbit. It's the engine of the Fenwick tree in the ranges section, where i += i & -i hops from one node to the next.

A catalogue of one-liners

Question Expression Cost
Lowest set bit, isolated x & -x O(1)
Clear the lowest set bit x & (x - 1) O(1)
Is x a power of two? x > 0 and x & (x - 1) == 0 O(1)
Lowest zero bit, isolated ~x & (x + 1) O(1)
Position of the highest set bit x.bit_length() - 1 O(1)
Smallest power of two ≥ x (x ≥ 1) 1 << (x - 1).bit_length() O(1)
Number of set bits x.bit_count() (3.10+), or Kernighan's loop O(1) / O(set bits)
python

The "lowest zero" line is the lowbit trick applied to ~x. Adding 1 to x carries through its trailing 1s and stops at its lowest 0. Trace it on a bitmap where pages 0, 1, 2 and 4 are used:

step through it
1bitmap = 0b10111
2plus_one = bitmap + 1
3free = ~bitmap & plus_one
4page = free.bit_length() - 1
5print(bin(plus_one), free, page)

Page 3 is the first free one, and finding it took no loop at all.

The power-of-two test looks simple, but it's easy to get wrong. This version accepts a number it shouldn't:

broken — fix it

It prints [0, 1, 2, 4, 8, 16]. 0 has no set bit to clear, so x & (x - 1) is 0, and the test says yes.

Counting set bits: Kernighan's loop

Counting 1s by testing every position costs one pass per bit of width. Clearing the lowest set bit instead costs one pass per set bit, because each pass removes exactly one 1 and the loop stops when none are left:

What does this print?
x = 0b1011000
n = 0
while x:
    x &= x - 1
    n += 1
print(n)

For a sparse bitmap, such as a mostly empty file where each 64-bit word has two or three used pages, that's a 20× saving over testing all 64 positions. Python 3.10+ has int.bit_count(), written in C, and you should use it in real code. But Kernighan's loop is the one to know, because the same "clear the lowest bit" step drives the subset loops in the next lesson.

Here is the loop with a picture. Each frame shows x just before its lowest set bit is cleared, with the cursor on that bit. Change the starting value, or replace x &= x - 1 with x >>= 1, and count how many frames each version takes:

your code, drawn · array

Your popcount clears one set bit per pass, so the number of frames is the number of 1s, not the width.

How much does a bitmap save over keeping a list of free page numbers? A bitmap for P pages costs P/8 bytes, however many pages are free. A free list costs 8 bytes per free page. Suppose a file typically has 1,000 free pages, so its free list costs 8,000 bytes:

pages P bitmap bytes free list bytes (1,000 free)
8,000 1,000 8,000
64,000 8,000 8,000
200,000 25,000 8,000

They break even at 64,000 pages. The bitmap's cost is fixed, while the free list grows by 8 bytes per free page, so a bitmap wins whenever more than 1 page in 64 is free, which is almost always true in a live store. It also answers "is page k free?" in O(1), where a free list needs a scan.

Interview corner — two lost writes

Go back to lesson 1's replication log, where every id should appear twice. This time two different ids appear only once each. Find both in one pass with O(1) extra memory.

XORing everything gives a ^ b, not a and b separately. But a ^ b isn't zero, because the two ids differ. Its lowbit marks a position where they differ: one has a 1 there and the other has a 0. Split every id by that bit, and XOR each half separately. The pairs still cancel within each half, and a and b end up in different halves:

python

The general lesson is that when one XOR mixes two unknowns together, use a bit where they differ to separate them.

Quick practice

What does this print?
print(24 & -24, 24 & (24 - 1))
broken — fix it

It prints -1. ~bitmap & bitmap is always 0, since no bit can be both 0 and 1 at once. Fix the expression.


🎯 Your turn

Write claim_page(bitmap, pages). The file has pages pages, and bit i of bitmap is 1 when page i is used. Claim the lowest free page and return [page, new_bitmap]. If every page is taken, return [-1, bitmap] unchanged.

  • claim_page(0b10111, 8) → [3, 0b11111], which is [3, 31]
  • claim_page(0b1111, 4) → [-1, 15]

Hint — ~bitmap & (bitmap + 1) isolates the lowest 0 bit. Its position is bit_length() - 1, and if that position is >= pages the file is full.

Then press ▶ Run, try the Live App chips, and hit ✓ Check. ✅

Practice — level up

0/6 solved

Solve each in its own editor — Run to try, Check to grade. Stuck? Reveal a Hint, or the full Solution + walkthrough.

Warm-upIs it a power of two?

Quarry's page sizes must be powers of two. Write is_power_of_two(x) returning True exactly when x is 1, 2, 4, 8, … Zero and negatives are not powers of two. Use a bit trick, not a loop.

bit-tricksbitwise
DrillCount the used pages

Write popcount(x) returning the number of 1 bits in the non-negative integer x, using Kernighan's loop: clear the lowest set bit until nothing is left. Don't use bin or bit_count.

popcountbit-tricks
BuildRound a buffer up to a power of two

Quarry sizes its buffers in powers of two. Write next_power_of_two(x) returning the smallest power of two that is >= x, for x >= 1. next_power_of_two(96) is 128, and next_power_of_two(128) is 128.

bit-tricksbit-shift
BossTwo lost writes

In a replication log every record id appears exactly twice, except two different ids that appear once each. Write two_lost_writes(ids) returning those two ids as a sorted list. Ids are non-negative. Use O(1) extra memory and no dict or set.

xorlowbit
CapstoneFind k free pages in a row

Large records need several consecutive pages. Write claim_run(bitmap, pages, k) returning the lowest page index i such that pages i to i + k - 1 are all free (0 bits) and all inside the file (below pages), or -1 if there's no such run. Aim for a handful of whole-word operations rather than testing each page. k >= 1.

bit-tricksbit-manipulation
ExplainWhy clearing the lowest bit counts the ones

Explain why x & (x - 1) clears exactly the lowest set bit of x, and use it to explain how many passes Kernighan's popcount loop takes. When is it much faster than testing each of the 64 bit positions of a word, and when is there no gain?

claim_page.py