Skip to content

Encourage vectorization in Hll8Array::mergeHll - #553

Merged
proost merged 2 commits into
apache:masterfrom
hadrian-reppas:master
Oct 30, 2026
Merged

proost merged 2 commits into
apache:masterfrom
hadrian-reppas:master

Conversation

@hadrian-reppas

@hadrian-reppas hadrian-reppas commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

I was benchmarking DataSketches against other HLL implementations and the union was slower than expected. It turns out these loops were not getting vectorized. With this change, taking a union gets 20x faster on my machine.

type lg_k master patched speedup speedup with -march=skylake
HLL_8 10 4.75 µs 0.33 µs 14.5x 22.7x
HLL_8 12 18.3 µs 1.11 µs 16.5x 26.3x
HLL_8 14 72.2 µs 4.32 µs 16.7x 33.8x
HLL_8 16 291.0 µs 16.7 µs 17.4x 20.4x
HLL_6 10 7.24 µs 5.26 µs 1.4x 1.5x
HLL_6 12 28.2 µs 20.3 µs 1.4x 1.5x
HLL_6 14 111.8 µs 80.4 µs 1.4x 1.5x
HLL_6 16 448.2 µs 320.2 µs 1.4x 1.5x

Here is the benchmarking code:

#include <benchmark/benchmark.h>
#include <hll.hpp>

#include <cstddef>
#include <cstdint>
#include <utility>
#include <vector>

constexpr size_t NUM_SKETCHES = 16;

// Insert 4 * k distinct keys so we aren't benchmarking list/set modes
std::vector<datasketches::hll_sketch> makeDenseSketches(uint8_t lg_k, datasketches::target_hll_type type)
{
    const uint64_t num_keys = uint64_t(4) << lg_k;
    std::vector<datasketches::hll_sketch> sketches;
    sketches.reserve(NUM_SKETCHES);
    for (size_t s = 0; s < NUM_SKETCHES; ++s)
    {
        datasketches::hll_sketch sketch(lg_k, type);
        for (uint64_t i = 0; i < num_keys; ++i)
            sketch.update(s * num_keys + i);
        sketches.push_back(std::move(sketch));
    }
    return sketches;
}

void BM_HllUnionUpdateDense(benchmark::State & state, datasketches::target_hll_type type)
{
    const auto lg_k = static_cast<uint8_t>(state.range(0));
    const auto sketches = makeDenseSketches(lg_k, type);

    // Make sure `u` is dense so we always go through `Hll8Array::mergeHll`
    datasketches::hll_union u(lg_k);
    for (const auto & sketch : sketches)
        u.update(sketch);

    for (auto _ : state)
    {
        for (const auto & sketch : sketches)
            u.update(sketch);

        benchmark::ClobberMemory();
    }

    state.SetItemsProcessed(state.iterations() * static_cast<int64_t>(sketches.size()));
    state.SetBytesProcessed(state.iterations() * static_cast<int64_t>(sketches.size() << lg_k));
}

BENCHMARK_CAPTURE(BM_HllUnionUpdateDense, hll_8, datasketches::HLL_8)->DenseRange(10, 16, 2);
BENCHMARK_CAPTURE(BM_HllUnionUpdateDense, hll_6, datasketches::HLL_6)->DenseRange(10, 16, 2);

@proost proost left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you show me benchmark code too?

@hadrian-reppas

Copy link
Copy Markdown
Contributor Author

I just updated the PR with the benchmark and a similar refactor for HLL_6

@proost proost left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What i found is performance improvement depends on the arch type or compiler version.

can you use "size_t" instead? I guess that uint32_t is inappropriate for pointer type to fully vectorized during compile time.

Comment thread hll/include/Hll8Array-internal.hpp
Comment thread hll/include/Hll8Array-internal.hpp Outdated

@proost proost left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!

@proost
proost merged commit bf60ea6 into apache:master Oct 30, 2026
21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants