HyperLogLog: Estimating Billions of Unique Elements in Kilobytes of Memory

In real-time analytics, calculating the exact number of unique elements (the cardinality or `COUNT(DISTINCT)`) across massive streams—such as daily active users (DAUs), distinct IP addresses, or unique search queries—traditionally requires storing every seen key in a Hash Set or Balanced Search Tree.

For a dataset containing 100 million 64-bit identifiers, an exact hash set requires gigabytes of RAM. Invented by Philippe Flajolet, Éric Fusy, Olivier Gandouet, and Frédéric Meunier in 2007, **HyperLogLog (HLL)** is a probabilistic data structure that estimates cardinality with a typical standard error of ~1.04 / sqrt(m) while consuming a fixed memory footprint of roughly **12 KB**, regardless of whether the dataset contains 10,000 or 10 billion distinct items.

The Core Probability Intuition: Counting Leading Zeros

Consider tossing a fair coin repeatedly until getting 'Heads' (1). The probability of seeing a sequence starting with $k$ consecutive 'Tails' (0s) followed by a Head is $(1/2)^{k+1}$:

  • Seeing '1...' (0 leading zeros): Happens ~50% of the time (expected trials: 2).
  • Seeing '01...' (1 leading zero): Happens ~25% of the time (expected trials: 4).
  • Seeing '00001...' (4 leading zeros): Happens ~3.125% of the time (expected trials: 32).
  • Seeing a maximum run of $R$ leading zeros suggests we have likely observed approximately $2^{R+1}$ distinct random events.

By hashing input items uniformly across a 64-bit binary space, the pattern of leading zeros in the hash outputs directly mirrors this coin-tossing probability model.

Register Bucketing and Harmonic Mean Averaging

Relying on a single maximum run of leading zeros yields high statistical variance. HyperLogLog eliminates variance through two structural techniques:

1. Stochastic Averaging across m Registers

HLL divides its state into $m = 2^p$ independent registers (buckets). For standard implementations (e.g., $p = 14$), there are $m = 16,384$ registers. When an item is added:

  1. Compute a 64-bit uniform hash of the item.
  2. Use the first $p$ bits of the hash to select the target register index $j$ ($0 \le j < m$).
  3. Count the number of leading zeros $\rho$ in the remaining $(64 - p)$ bits (plus 1).
  4. Update the register value: $M[j] = \max(M[j], \rho)$.

2. Harmonic Mean Estimation Formula

Instead of using the arithmetic mean (which is easily corrupted by single outlier runs), HLL computes the **Harmonic Mean** of $2^{-M[j]}$ across all registers to strongly discount extreme values:

E = alpha_m * m^2 * ( sum_{j=1}^{m} 2^(-M[j]) )^(-1)

Where $\alpha_m$ is a predetermined bias-correction constant (for $m = 16384$, $\alpha_m \approx 0.7213 / (1 + 1.079 / m)$).

Range Corrections and Distributed Merging

1. Small Range Correction (Linear Counting)

When total cardinality is small relative to $m$ and many registers remain 0, raw HLL estimates suffer non-linear bias. In this range ($E < 2.5 \cdot m$), HLL switches to Linear Counting based on the number of empty registers $V$: $E^* = m \cdot \ln(m / V)$.

2. Large Range Correction (64-Bit Space)

When using 32-bit hashes, hash collisions occur near $2^{32}$, requiring large-range asymptotic corrections. Modern 64-bit implementations (such as HyperLogLog++) eliminate this boundary, allowing seamless counting up to $2^{64}$ with zero overflow risk.

3. Lossless Distributed Merge (O(m) Time)

Two or more HyperLogLog instances configured with the same register size $m$ can be merged without accessing original data points by taking the element-wise maximum across registers: $M_{merged}[j] = \max(M_A[j], M_B[j])$. This allows parallel, decentralized metric aggregation across distributed worker nodes.

C++ Implementation Blueprint (Standard HLL Engine)

#include <vector>
#include <string>
#include <cmath>
#include <algorithm>
#include <cstdint>

class HyperLogLog {
private:
    static constexpr int P = 14;              // 14 bits -> 16384 registers
    static constexpr int M = 1 << P;         // 16384
    static constexpr double ALPHA = 0.7213 / (1.0 + 1.079 / M);
    std::vector<uint8_t> registers;

    uint64_t hash64(const std::string& key) const {
        // 64-bit MurmurHash3 or std::hash wrapper
        return std::hash<std::string>{}(key);
    }

    int countLeadingZeros(uint64_t val, int max_bits) const {
        if (val == 0) return max_bits;
        int zeros = 0;
        while ((val & (1ULL << (max_bits - 1 - zeros))) == 0 && zeros < max_bits) {
            zeros++;
        }
        return zeros;
    }

public:
    HyperLogLog() : registers(M, 0) {}

    void add(const std::string& item) {
        uint64_t h = hash64(item);
        uint32_t j = h >> (64 - P); // Top P bits for register index
        uint64_t w = h & ((1ULL << (64 - P)) - 1); // Remaining 64-P bits
        int leading_zeros = countLeadingZeros(w, 64 - P) + 1;
        registers[j] = std::max(registers[j], static_cast<uint8_t>(leading_zeros));
    }

    double count() const {
        double sum = 0.0;
        int empty_registers = 0;
        for (int j = 0; j < M; ++j) {
            sum += std::pow(2.0, -registers[j]);
            if (registers[j] == 0) empty_registers++;
        }

        double estimate = ALPHA * M * M / sum;

        // Linear counting for small range correction
        if (estimate <= 2.5 * M && empty_registers > 0) {
            estimate = M * std::log(static_cast<double>(M) / empty_registers);
        }
        return estimate;
    }

    void merge(const HyperLogLog& other) {
        for (int j = 0; j < M; ++j) {
            registers[j] = std::max(registers[j], other.registers[j]);
        }
    }
};

Real-World Distributed and Big Data Systems

  1. Redis In-Memory Analytics: Provides native O(1) commands (`PFADD`, `PFCOUNT`, `PFMERGE`) using 12 KB dense register allocations per key.
  2. Cloud Data Warehouses (Google BigQuery, Snowflake, AWS Redshift): Accelerating `APPROX_COUNT_DISTINCT()` analytical SQL queries across petabyte tables in seconds.
  3. High-Throughput OLAP Engines (ClickHouse, Apache Pinot): Storing intermediate HLL sketches in aggregate projection tables for sub-second unique visitor reporting.
  4. Network Traffic & DDoS Monitoring: Tracking unique destination IPs and flow endpoints per second across edge router interfaces without unbounded memory growth.