The Population Count (POPCNT) Instruction: Why Counting Set Bits Got Its Own Silicon

2026-07-13

Counting the number of 1-bits in a word sounds like a party trick until you realize how many hot loops depend on it: bitmap indexes in databases, Hamming distance for nearest-neighbor search, chess engine piece counting, error-correcting codes, sparse vector intersection, and the entire cardinality-estimation family (HyperLogLog). Before dedicated hardware, this operation was one of the ugliest microbenchmarks in existence.

The classic software approach — the Hamming weight bit-twiddle — looks like this:

That's ~12 dependent instructions per 64-bit word, forming a serial chain. It sits on integer ALUs and clogs the reorder buffer. A CPU doing bitmap intersection over gigabytes of data would spend most cycles inside this dance.

POPCNT collapses all of it into a single micro-op. Introduced with SSE4.2 on Nehalem (2008) and mandatory in ARMv8's CNT instruction, it has a latency of 3 cycles on modern Intel/AMD, throughput of 1 per cycle, and executes on a single integer port. The silicon underneath is a Wallace tree of half-adders and full-adders: bits are combined in a log-depth reduction tree, so the critical path is O(log₂ 64) = 6 gate delays instead of a serial chain.

Concrete example — Roaring Bitmaps (used by Lucene, Druid, ClickHouse) store dense chunks as raw 64-bit words. Computing the cardinality of an intersection is literally popcnt(a & b) in a loop. On a 1 MB bitmap chunk (16,384 words):

The gotcha nobody warns you about: on pre-Ice Lake Intel chips, POPCNT has a false dependency on its destination register. Because it's encoded like a two-operand instruction, the renamer thinks the destination is also read. Tight loops like popcnt rax, [rdi]; popcnt rbx, [rsi+8] serialize on rax/rbx even though there's no real dependency. The fix: XOR-zero the destination first, or upgrade to Ice Lake (which eliminated the false dep).

Rule of thumb: if your algorithm counts bits in a hot loop and you're not using __builtin_popcountll (GCC/Clang) or _mm_popcnt_u64 (MSVC), you're leaving a 3–10× speedup on the table. AVX-512's VPOPCNTDQ extends this to 512-bit vectors — 8 popcounts per cycle.

Key Takeaway: POPCNT replaces a 12-instruction bit-twiddle chain with a single 3-cycle Wallace-tree reduction, but watch for the pre-Ice Lake false destination dependency that silently serializes tight loops.

All newsletters