Skip to content

perf: Bloom, Count-Min, VarOpt, EBPPS and frequent items states in DuckDB's arena - #30

Merged
rustyconover merged 5 commits into
v2.0from
arena/items
Oct 28, 2026
Merged

rustyconover merged 5 commits into
v2.0from
arena/items

Conversation

@rustyconover

Copy link
Copy Markdown
Collaborator

Builds on #27.

20M rows, 2M groups, 1 GB limit, 8 threads before after
frequent_items / _weighted 8.5 GB, 14 s 1.01 GB, 9.6 s
varopt / _weighted (k=64) killed above 24 GB 1.01 GB, 9 s
ebpps / _weighted (k=64) 13.9 GB, 21 s 1.02 GB, 9 s
bloom_filter(1000, 0.01) killed above 24 GB 0.75 GB, 6.9 s
count_min / _weighted (3, 128) killed above 24 GB 1.02 GB, 9.5 s

How the states work

  • Buffer: each state keeps its first 32 raw inputs, with their weights, and builds the sketch only when more arrive, replaying them in order. The shared code is in src/include/datasketches_arena_buffer.hpp.
  • Bloom and Count-Min: the buffer is also capped at the sketch's own size. Every input type is covered, including VARCHAR and BLOB, whose bytes are copied into the arena while buffered. Results are byte-identical to before.
  • VarOpt and EBPPS: every item type except VARCHAR, which stays on the heap. VarOpt switches to a union kept in the arena the first time it's combined or merged.
  • Frequent items: numeric inputs are stored inline as their decimal strings, which hash, compare and serialize exactly like std::string. VARCHAR inputs and the merge aggregates stay on the heap.
  • Serialize and export: both use a scratch arena that's freed right away. Buffered states export as their raw inputs, so importing them doesn't inflate them.

Library patches (vcpkg overlay, port-version 4)

  • count-min-hash-seeds-allocator.patch: count_min_sketch kept hash_seeds in a std::vector with std::allocator, ignoring the sketch's allocator.
  • frequent-items-purge-samples.patch: reverse_purge_hash_map::purge() allocated a temporary array of up to 1024 values on every purge. The array is now on the stack.

Known limits

  • Density stays on the heap, because its kernel takes a std::vector with no allocator parameter.
  • Spilling is all or nothing per query. If any aggregate in a query still has a destructor, such as a VARCHAR sketch or the frequent-items merge, DuckDB spills nothing in that query.

Tests

New test/sql/datasketch_spill_items.test, under 128 MB with 4 threads. It covers many groups, the weighted variants, merges of stored sketches, EXPORT_STATE round trips and the VARCHAR heap paths. All tests pass.

🤖 Generated with Claude Code

rustyconover and others added 5 commits October 28, 2026 13:20
Two overlay patches, so these sketches can keep all their memory in an arena
allocator (DSAllocator's arena mode), which never frees:

- count-min-hash-seeds-allocator.patch: count_min_sketch kept its hash seeds in
  a std::vector<uint64_t> with std::allocator, ignoring the sketch's allocator.
  The vector now uses the sketch's allocator (rebound to uint64_t).
- frequent-items-purge-samples.patch: reverse_purge_hash_map::purge took its
  temporary median sample (up to 1024 values) from the sketch's allocator on
  every purge; it now lives on the stack.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…y can spill

The Bloom filter and Count-Min aggregates (build, weighted build and merge, every
input type including VARCHAR and BLOB) now keep their states in the aggregate
arena, have no destructor, and export and import them as BLOBs, so they count
toward memory_limit and DuckDB can spill them.

A filter allocates its whole bit array and a Count-Min sketch all its counters
up front, and DuckDB creates a state per input row when grouping reduces little,
so a state first buffers up to 32 raw values (with their weights; strings are
copied into the arena), bounded by the sketch's size, and builds the sketch only
when more arrive. Both sketches are order-independent, so results are byte for
byte the same. Buffered states export as their values, so importing them does
not inflate them.

The shared machinery (DSBufferedArenaState and its policies' interface, a
weighted arena operation, scratch arenas for temporaries) is in
src/include/datasketches_arena_buffer.hpp.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The VarOpt and EBPPS aggregates over every item type but VARCHAR (whose
std::string items own heap memory and keep the heap state) now keep their
states in the aggregate arena and export and import them, so they can spill.
States buffer their first 32 items with their weights (weights are checked as
they arrive, with the library's messages); an EBPPS sketch reserves K items up
front.

A VarOpt state switches to a var_opt_union (in the arena) when it is first
combined or merged, so later combines and merges update it in place instead of
leaving a replaced sketch behind in the arena. The EBPPS merge aggregate
reproduces the heap state's merge (the lighter sample is merged into the
heavier, K the smaller of the two) without swapping a heap sketch into an arena
state.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… can spill

A frequent items sketch keeps its items as std::string, which owns heap memory.
The build and weighted build aggregates over numbers now keep their items as an
inline key (the number's string, at most 24 bytes) that compares, hashes and
serializes exactly as the std::string, in a sketch in the aggregate arena, so
results are the same bytes and the states can spill. States buffer their first
32 items with their weights. VARCHAR items and the merge aggregates (whose
input sketches may hold any strings) keep the heap state.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Runs the aggregates under a 128MB limit with 4 threads: many small (buffered)
and larger groups, weighted variants, merges of stored sketches, EXPORT_STATE
round trips and the VARCHAR heap paths. Bloom and Count-Min results are compared
to the same query with plenty of memory, the others by exact facts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@rustyconover
rustyconover merged commit 7eeabc4 into v2.0 Oct 28, 2026
10 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.

1 participant