Skip to content

Add RunEndBool encoding for efficient boolean array compression#8467

Draft
joseph-isaacs wants to merge 4 commits into
developfrom
claude/runend-bool-compression-r148yw
Draft

Add RunEndBool encoding for efficient boolean array compression#8467
joseph-isaacs wants to merge 4 commits into
developfrom
claude/runend-bool-compression-r148yw

Conversation

@joseph-isaacs

@joseph-isaacs joseph-isaacs commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR introduces a new RunEndBool encoding specialized for compressing boolean arrays with long runs. Since boolean values strictly alternate in run-end encoding, we can store only the run end positions plus a single start flag, rather than maintaining a separate values array like the generic RunEnd encoding.

Changes

  1. New vortex-runend-bool crate with:

    • RunEndBoolArray type and metadata serialization
    • Specialized compression/decompression for boolean runs
    • Compute kernels: filter, take, invert, is_constant, is_sorted, min_max
    • Scalar access and slicing operations
    • Efficient run-preserving filtering that merges adjacent runs with identical values
  2. Shared run-end indexing logic extracted to encodings/runend/src/shared.rs:

    • RunEndIndex trait for common "ends" indexing operations
    • find_physical_index() and find_slice_end_index() free functions
    • validate_ends() validation helper
    • Both RunEnd and RunEndBool now implement this trait
  3. Integration with compression pipeline:

    • New BoolRunEndScheme in vortex-btrblocks for automatic bool array compression
    • Estimates compression ratio based on run count (profitable when average run length > 8)
    • Registered in the default compression scheme list
  4. File format support:

    • RunEndBool arrays serialize/deserialize correctly in Vortex file format
    • Added round-trip test in vortex-file

Design Notes

  • Boolean runs strictly alternate, so run i has value value_at_index(i, start) where even indices equal start and odd indices equal !start
  • The encoding preserves run structure through filtering and slicing operations
  • Sparse filters (< 10% of runs kept) decode via per-index binary search; dense filters scan runs once
  • All compute operations respect optional validity arrays

…election

Re-introduces the `vortex-runend-bool` encoding crate (encoding id
`vortex.runend_bool`) specialized for boolean arrays. Boolean runs strictly
alternate, so the array stores only the run `ends` plus a `start` flag and an
optional validity child -- no values array.

- New crate `encodings/runend-bool` mirroring `vortex-runend`: VTable, prost
  metadata serde, canonicalize, scalar_at, take, filter, slice, invert, and
  is_constant / is_sorted / min_max aggregate kernels.
- Registered in the default session (file round-trip) and the workspace.
- New BtrBlocks `BoolRunEndScheme` so the compressor auto-selects run-end for
  run-heavy bool columns; the run `ends` child is cascaded for further
  compression.
- Unify shared run-end index logic (find_physical_index, find_slice_end_index,
  validate_ends, logical_len_from_ends) into `vortex-runend::shared` behind a
  `RunEndIndex` trait plus orphan-rule-safe free-function forms, reused by both
  encodings.

Signed-off-by: Joe Isaacs <joe.isaacs@live.co.uk>

https://claude.ai/code/session_01SKPEzTk1Wme4y96bC7wps1
- `encode_runend_bool` now takes `ArrayView<'_, Bool>`, mirroring how
  `runend_encode` takes `ArrayView<'_, Primitive>`, and drops the
  `&BoolArray` / `TypedArrayRef::to_owned` detour at every call site
  (including the BtrBlocks `BoolRunEndScheme`).
- The metadata test now uses the `check_metadata` goldenfile harness with
  `#[cfg_attr(miri, ignore)]`, matching `runend`; adds
  `goldenfiles/runend_bool.metadata`.
- Add a usage doctest to `RunEndBool::new`, mirroring `RunEnd`.

Signed-off-by: Joe Isaacs <joe.isaacs@live.co.uk>

https://claude.ai/code/session_01SKPEzTk1Wme4y96bC7wps1
`RunEndBool::filter` previously decoded every kept element with a per-index
binary search (`find_physical_index`), which is O(kept * log runs). Mirror
`runend`'s threshold dispatch: keep that path for sparse masks, but for dense
masks scan the run ends once (O(runs + len)) and emit a `RunEndBool`, avoiding
the binary searches and preserving the run-end encoding in the output.

Because boolean runs strictly alternate, dropping an entire run can leave two
kept runs with the same value adjacent; these are merged so the result still
alternates and round-trips through a single `start` flag. Adds tests for the
dense path and the run-merging case.

Signed-off-by: Joe Isaacs <joe.isaacs@live.co.uk>

https://claude.ai/code/session_01SKPEzTk1Wme4y96bC7wps1
@codspeed-hq

codspeed-hq Bot commented Jun 17, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

⚡ 3 improved benchmarks
❌ 4 regressed benchmarks
✅ 457 untouched benchmarks
⏩ 1091 skipped benchmarks1

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Simulation decompress_rd[f64, (100000, 0.0)] 845.5 µs 1,025.2 µs -17.53%
Simulation decompress_rd[f32, (100000, 0.0)] 499.1 µs 587.5 µs -15.04%
Simulation bitwise_not_vortex_buffer_mut[128] 186.1 ns 215.3 ns -13.55%
Simulation bitwise_not_vortex_buffer_mut[1024] 246.4 ns 275.6 ns -10.58%
Simulation null_count_run_end[(10000, 4, 0.01)] 125.4 µs 91.4 µs +37.2%
Simulation decompress_rd[f64, (100000, 0.01)] 981.2 µs 846.5 µs +15.92%
Simulation decompress_rd[f64, (100000, 0.1)] 981.2 µs 846.6 µs +15.9%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing claude/runend-bool-compression-r148yw (6046f48) with develop (679e2c5)2

Open in CodSpeed

Footnotes

  1. 1091 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

  2. No successful run was found on develop (9f494a1) during the generation of this report, so 679e2c5 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@joseph-isaacs joseph-isaacs added the changelog/feature A new feature label Jun 17, 2026 — with Claude
The shared `RunEndIndex` trait now provides `ends()`/`offset()`/
`find_physical_index()` for `RunEnd` arrays, but Rust requires the defining
trait in scope to call them. Consumer crates that import only `RunEndArrayExt`
(vortex-cuda, vortex-duckdb, and a vortex bench) failed to compile with E0599.
Bring `RunEndIndex` into scope alongside `RunEndArrayExt` in those files.

Signed-off-by: Joe Isaacs <joe.isaacs@live.co.uk>

https://claude.ai/code/session_01SKPEzTk1Wme4y96bC7wps1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changelog/feature A new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant