DSA DSA In Depth
0%
lesson bitsetsswarpopcount

Bitsets, Popcount (__builtin_popcount) & O(N/64) Word-Level Parallelism

7 min read Module 1.2: Contiguous Arrays, Bit Manipulation & Pointer Idioms Updated Oct 23, 2026
📄 Cheat Sheet
C++ Bitsets & O(N/64) Word-Level Parallelism in Competitive Programming
Errichto Algorithms C++ Bitsets & O(N/64) Word-Level Parallelism in Competitive Programming Watch on YouTube ↗

Deep-dive lecture covering Bitsets, Popcount (__builtin_popcount) & O(N/64) Word-Level Parallelism.

0:00 / --:--

SWAR (SIMD Within A Register) & Tree-Reduction Popcount

How do you count the number of set bits (Hamming weight / population count) inside a 64-bit word $x$ in $O(\log_2 64) = 6$ steps without hardware POPCNT? Treat the 64-bit integer as 64 parallel 1-bit accumulators and apply a divide-and-conquer parallel tree reduction:

  1. Pairwise 2-bit sums: Subtract the even bits shifted right by 1 from each 2-bit pair: x = x - ((x >> 1) & 0x5555555555555555ULL) (since $2b_1 + b_0 - b_1 = b_1 + b_0$).
  2. Nibble 4-bit sums: Add adjacent 2-bit pairs: x = (x & 0x3333333333333333ULL) + ((x >> 2) & 0x3333333333333333ULL).
  3. Byte 8-bit sums: Add adjacent 4-bit nibbles and mask: x = (x + (x >> 4)) & 0x0F0F0F0F0F0F0F0FULL (no overflow can occur since each byte holds at most $8 < 15$).
  4. Horizontal Multiply Reduction: Multiply by the magic constant 0x0101010101010101ULL ($\sum_{k=0}^7 2^{8k}$), which sums all 8 byte-lanes into the top byte (bits 56..63), then shift right by 56: (x * 0x0101010101010101ULL) >> 56!
Complexity Invariant: Word-RAM $O(N / w)$ Speedup for Subset Sum, Transitive Closure & Set Intersection

In the Word-RAM model with word size $w = 64$:

  • 0-1 Subset Sum DP: Instead of an $O(N \cdot S)$ loop for (s = S; s >= v; --s) dp[s] |= dp[s - v], represent all reachable sums as a bitset $B$ and update 64 sums simultaneously via B |= (B << v) in $O(N \cdot S / 64)$ operations.
  • Graph Transitive Closure / Triangle Counting: Given bit-packed adjacency rows $\text{adj}[u]$, the number of common neighbors shared by vertices $u$ and $v$ is $\sum_{block} \text{popcount}(\text{adj}[u][block] \mathrel{\&} \text{adj}[v][block])$, turning $O(N^3)$ triangle counting into $O(N^3 / 64)$ and completing $N = 2000$ in ~15 milliseconds!

Bitset Representations Across Systems & Analytics Engines

Structure Bits per Element Union / Intersection Set Bit Iteration (Density $d$) Best For
bool flags[N] (1 byte/bool) 8 bits ($8\times$ bloat) $O(N)$ scalar or $O(N/32)$ AVX2 $O(N)$ branchy scan Small arrays needing byte addressability
Flat Bitset (uint64_t[(N+63)/64]) 1 bit $O(N / 64)$ scalar / $O(N / 512)$ AVX-512 $O(N/64 + k)$ via TZCNT + BLSR Dense sets ($d > 1\%$), DP, Graph bitboards
Roaring Bitmap (Hybrid 16-bit Containers) Adaptive ($<1$ to $16$ bits) $O(C)$ container ops $O(k)$ exact Production OLAP / Inverted Indexes (Lucene, ClickHouse)

Visualizing SWAR Parallel Popcount & Multi-Word Bitshift Carry

SWAR Tree-Reduction Popcount on an 8-Bit Byte: 0b10110110 (5 set bits) 1-bit lanes: 1 | 0 | 1 | 1 | 0 | 1 | 1 | 0 2-bit sums: 01 (1) | 10 (2) | 01 (1) | 01 (1) 4-bit sums: 0011 (3) | 0010 (2) 8-bit total: 00000101 (5) Multi-Word Bitset Left Shift (B <<= k, where k = 64*q + r) words[i - q - 1] High r bits --> words[i - q] Low (64-r) bits words[i] = (words[i-q] << r) | (words[i-q-1] >> (64 - r)) Fast Set-Bit Iteration: while (w) { idx = base + countr_zero(w); w &= w - 1; } skips all 0-runs in hardware!

Fast Set-Bit Iteration via TZCNT + BLSR

flowchart LR A["Load 64-bit word
w = words[block]"] --> B{"w != 0?"} B -- "No (All 64 bits 0)" --> C["Advance to next word
64 zero bits skipped in 1 cycle!"] B -- "Yes" --> D["bit = std::countr_zero(w)
Yield index (block * 64 + bit)"] D --> E["w &= (w - 1)
Clear lowest set bit (BLSR)"] E --> B
Hardware Reality: Intel's POPCNT False Dependency Bug & AVX-512 VPOPCNTDQ

Modern x86-64 CPUs execute __builtin_popcountll(x) (std::popcount(x) in C++20) via the dedicated POPCNT instruction with a 3-cycle latency and 1-cycle throughput. Historically, Intel Sandy Bridge through Skylake processors suffered from an infamous hardware erratum: POPCNT had a false register dependency on its destination register, stalling out-of-order execution inside tight loops unless the compiler emitted a dummy xor eax, eax before popcnt rax, rdi! On modern AVX-512 (Ice Lake / Zen 4) and ARM SVE, the VPOPCNTDQ instruction (or Harley-Seal / Wojciech Muła's VPSHUFB 4-bit LUT algorithm on AVX2) computes the popcount of eight 64-bit words (512 bits) in a single clock cycle—achieving over 100 GB/s of bit-counting throughput.

Two's Complement Bit Tricks (x & -x, x & (x-1)) & 64-Bit SWAR Bitset
O(1) ALU / O(N/64) Bitset

Inspect register-level bit patterns for Two's complement negation -x = ~x + 1, lowest set bit isolation x & -x, Kernighan's popcount loop, and 64-bit word-parallel bitsets.

Step 1 / 7
Custom Input:
Timeline

Invariant & State

In Two's Complement, -x = ~x + 1 inverts all bits left of the lowest set bit and preserves the lowest set bit 2^k, so x & -x = 2^k.
🌳 Live 2D Hierarchical Tree Topology — Two's Complement Bit Tricks (x & -x, x & (x-1)) & 64-Bit SWAR Bitset Visited/Active Pivot/Root Resolved
Trailing x … [0] Bitwise ~x … [1] Carry st -x=~x… [2] LSB = 2³ x &am… [3]
[0]x = 01011000₂ (+88)Trailing 1000₂
[1]~x = 10100111₂Bitwise NOT
[2]-x=~x+1= 10101000₂ (-88)Carry stops at bit 3
[3]x & -x = 00001000₂ (8)LSB = 2³ = 8
Input x
88 (01011000₂)
Negation -x
-88 (10101000₂)
Isolated LSB (x & -x)
8 (00001000₂)
[Step 1] Isolated lowest set bit 2³ = 8 via x & -x (used in Fenwick Trees and bitmask enumeration).

Production Implementation: Dynamic 64-Bit Word-Parallel Bitset Engine

#include <bit>
#include <cstdint>
#include <vector>

class DynamicBitset64 {
  std::size_t num_bits_;
  std::vector<std::uint64_t> words_;

  void mask_tail() noexcept {
    const std::size_t rem = num_bits_ & 63;
    if (rem != 0 && !words_.empty()) {
      words_.back() &= (1ULL << rem) - 1ULL;
    }
  }

public:
  explicit DynamicBitset64(std::size_t num_bits)
      : num_bits_(num_bits), words_((num_bits + 63) / 64, 0ULL) {}

  void set(std::size_t i) noexcept {
    words_[i >> 6] |= (1ULL << (i & 63));
  }

  bool test(std::size_t i) const noexcept {
    return (words_[i >> 6] & (1ULL << (i & 63))) != 0;
  }

  // O(N / 64) word-parallel shift-OR: this |= (this << shift)
  // Accelerates 0-1 Subset Sum DP by 64x!
  void shift_left_or(std::size_t shift) noexcept {
    const std::size_t word_shift = shift >> 6;
    const std::size_t bit_shift = shift & 63;
    if (word_shift >= words_.size()) return;

    if (bit_shift == 0) {
      for (std::size_t i = words_.size() - 1; i >= word_shift; --i) {
        words_[i] |= words_[i - word_shift];
        if (i == word_shift) break;
      }
    } else {
      const std::size_t inv_shift = 64 - bit_shift;
      for (std::size_t i = words_.size() - 1; i > word_shift; --i) {
        words_[i] |= (words_[i - word_shift] << bit_shift)
                   | (words_[i - word_shift - 1] >> inv_shift);
      }
      words_[word_shift] |= (words_[0] << bit_shift);
    }
    mask_tail();
  }

  // O(N / 64) hardware popcount
  std::size_t count() const noexcept {
    std::size_t total = 0;
    for (std::uint64_t w : words_) {
      total += static_cast<std::size_t>(std::popcount(w));
    }
    return total;
  }

  // Iterate all set bit indices in O(N/64 + k) time using TZCNT + BLSR
  template <typename Callback>
  void for_each_set_bit(Callback&& cb) const {
    for (std::size_t block = 0; block < words_.size(); ++block) {
      std::uint64_t w = words_[block];
      while (w != 0) {
        const int bit = std::countr_zero(w);
        cb((block << 6) + static_cast<std::size_t>(bit));
        w &= (w - 1);
      }
    }
  }
};
Production Pitfall: Unmasked Tail Bits & Shift-by-64 Undefined Behavior in Multi-Word Shifts

When implementing a custom multi-word bitset of length $N$ where $N$ is not a multiple of $64$, two subtle bugs frequently corrupt results:

  1. Dirty Tail Bits: Operations like bitwise NOT (~w) or left shifts (shift_left_or) populate bit positions $ge N$ in the final word words_.back(). If you do not immediately mask words_.back() &= (1ULL << (N % 64)) - 1, subsequent calls to count() or any() will count phantom out-of-bounds bits!
  2. Shift by 64 - bit_shift when bit_shift == 0: When shifting across word boundaries, if shift % 64 == 0, evaluating words_[i - word_shift - 1] >> (64 - 0) shifts a 64-bit integer by 64 bits—which is Undefined Behavior in C++ and acts as a shift-by-0 on x86! Always branch on if (bit_shift == 0) separately.
Check yourself Question 1 of 4
Concept Check Word-Level Parallelism in 0-1 Subset Sum DP recall

You are given $N = 2000$ positive integers whose total sum is $S = 2 \times 10^5$, and you want to determine all reachable subset sums. A standard boolean DP array takes $O(N \cdot S)$ operations ($4 \times 10^8$ loop steps).

⚡ Interactive Challenge · Staff Level

Algorithmic Design & Complexity Check

Analyze the invariant, asymptotic bound, and hardware behavior before selecting.

Which two bugs will corrupt the result if not explicitly guarded in `shift_left_or`?

Key Takeaways

Packing boolean arrays into 64-bit words shrinks memory footprint by 64x and turns O(N) bitwise set operations and DP transitions into O(N / 64) word-parallel loops.
SWAR popcount sums 64 bits in 6 parallel reduction steps without branches; modern CPUs execute std::popcount via the 1-cycle throughput POPCNT instruction.
Scanning set bits via std::countr_zero(w) (TZCNT) followed by w &= w - 1 (BLSR) runs in O(N/64 + k) time, jumping directly from one set bit to the next.
In multi-word bitsets, always handle bit_shift == 0 separately to avoid shifting by 64 (UB), and always zero out tail bits >= N in the last word.
Was this lesson helpful? Spot an issue or have a suggestion? Let us know on GitHub.
Report an issue