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:
- 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$). - Nibble 4-bit sums: Add adjacent 2-bit pairs:
x = (x & 0x3333333333333333ULL) + ((x >> 2) & 0x3333333333333333ULL). - 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$). - 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!
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 viaB |= (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
Fast Set-Bit Iteration via TZCNT + BLSR
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
POPCNT False Dependency Bug & AVX-512 VPOPCNTDQModern 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.
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.
Invariant & State
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);
}
}
}
};
When implementing a custom multi-word bitset of length $N$ where $N$ is not a multiple of $64$, two subtle bugs frequently corrupt results:
- Dirty Tail Bits: Operations like bitwise NOT (
~w) or left shifts (shift_left_or) populate bit positions $ge N$ in the final wordwords_.back(). If you do not immediately maskwords_.back() &= (1ULL << (N % 64)) - 1, subsequent calls tocount()orany()will count phantom out-of-bounds bits! - Shift by
64 - bit_shiftwhenbit_shift == 0: When shifting across word boundaries, ifshift % 64 == 0, evaluatingwords_[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 onif (bit_shift == 0)separately.