Skip to content

perf: KLL and HLL aggregate states in DuckDB's arena, so they count toward memory_limit and spill - #27

Merged
rustyconover merged 1 commit into
v2.0from
perf/arena-states
Oct 28, 2026
Merged

rustyconover merged 1 commit into
v2.0from
perf/arena-states

Conversation

@rustyconover

Copy link
Copy Markdown
Collaborator

DuckDB 2.0 can spill aggregate states to disk, but only for states that live in the aggregate arena, have no destructor, and register export and import callbacks. Our states were heap objects with destructors. So DuckDB couldn't see their memory, and memory_limit didn't apply to them.

20M rows, 2M groups, 8 threads before after
datasketch_kll(200, v), 1 GB limit 39 GB peak, 22 s 0.77 GB, 6.0 s
datasketch_hll(12, v), 4 GB limit 3.3 GB, not counted 2.1 GB, counted
approx_quantile (built-in) out of memory at 1 GB and 4 GB —

What changed

  • DSAllocator arena mode: it allocates from the arena of the aggregate callback that's currently running (DSArenaScope) and never frees. Each sketch grows only in an arena that the current thread is allowed to use. When states combine, the source is copied into the target's arena rather than taken over.
  • Arena states for KLL and HLL: they have no destructor, and export and import their sketches as BLOBs. KLL over VARCHAR stays on the heap, because std::string items allocate their own memory.
  • KLL item buffer: DuckDB creates a state per input row when grouping reduces little (MaybeSkipLookups). So KLL states keep their first 32 items in an array that grows as needed, and build a sketch only when more arrive; results are unchanged. Small exact input sketches are buffered the same way, and buffered states export as raw items, so spilled states don't come back full size.
  • Error messages: DuckDB's own errors, such as out of memory, now pass through deserialization unchanged instead of being reported as corrupt input.

Library patches (in the vcpkg overlay, port-version 3)

Tests

New test/sql/datasketch_spill.test, run under a 128MB limit with 4 threads:

  • 400k-group KLL and HLL aggregates
  • merging stored sketches
  • EXPORT_STATE round trips
  • the VARCHAR heap path

All tests pass on EC2: 32 test files, 1953 assertions.

Still to do (next PRs)

  • HLL: it still builds a union for each per-row state, so it needs a value buffer like KLL's.
  • The other sketch families: they still use heap states.

🤖 Generated with Claude Code

… spill

Sketch states lived on the heap with a destructor, so DuckDB neither counted
them toward memory_limit nor spilled them: a 2M-group KLL aggregate used 39 GB
under a 1 GB limit. KLL (every item type but VARCHAR) and HLL states now live
in the aggregate arena (DSAllocator's new arena mode, allocating from the arena
of the running callback), have no destructor, and export and import their
sketches as BLOBs, which DuckDB 2.0 uses to spill aggregate states.

DuckDB creates one state per input row when grouping reduces little, so KLL
states keep their first 32 items in a growing buffer and build a sketch only
when more arrive; small exact input sketches are buffered too, and buffered
states export as raw items. The same KLL query now runs in 0.8 GB under a 1 GB
limit, 3.7x faster.

The vcpkg overlay carries three library fixes sent upstream:
hll-union-allocator (apache/datasketches-cpp#556), ebpps-merge-qualified-swap
(#557, which removes our copy-then-merge workaround) and kll-lazy-items (#558).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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